PythonAttitudeProviderModifier implementation and unexpected behavior

Dear Orekit team,
This is my first post on this forum, thanks already for your time reading this.

I am trying to implement a custom attitude provider for a number of different applications. I am currently using Orekit in Python via your Python wrapper.

While testing my implementation, I noticed an unexpected behaviour of a custom (Python)AttitudeProviderModifier. So I have two questions for you.

The first one is - can you please tell me if this is the correct implementation of a custom attitude provider modifier? It simply wraps a YawCompensation provider, returning the original attitude (I am using this just to test that everything is in order).

### ATTITUDE PROVIDER MODIFIER - CUSTOM IMPLEMENTATION
class TestAttProviderModifier(PythonAttitudeProviderModifier):
    EME2000 = FramesFactory.getEME2000()
    itrf    = FramesFactory.getITRF(IERSConventions.IERS_2010, True)
    earth = OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS, Constants.WGS84_EARTH_FLATTENING, itrf)

    nadir_pointing_att_provider = NadirPointing(EME2000, earth)
    yaw_comp_att_provider =  YawCompensation(EME2000, nadir_pointing_att_provider)

    def getAttitude(self, pv_coordinates_provider, date, frame):
        match (pv_coordinates_provider):
            case PVCoordinatesProvider():
                original_attitude = TestAttProviderModifier.yaw_comp_att_provider.getAttitude(pv_coordinates_provider, date, frame)
                return original_attitude
            
            case FieldPVCoordinatesProvider():
                original_attitude = TestAttProviderModifier.yaw_comp_att_provider.getAttitude(pv_coordinates_provider, date, frame)
                return FieldAttitude(date, frame, original_attitude.getRotation(), original_attitude.getRotationAcceleration())
            
            case _:
                raise RuntimeError(f'Not supported type of PVCoordinatesProvider: {type(pv_coordinates_provider).__name__}')
            
    def getUnderlyingAttitudeProvider(self):
        return TestAttProviderModifier.yaw_comp_att_provider

Then, assuming the implementation above is at least correct (but feel free to let me know if there is a nicer way to do it), I have found the following unexpected behaviour, seemingly only appearing in one specific condition, that is, if I propagate a non-equatorial orbit with a low-thrust manoeuvre taking place at a certain point during the propagation. In these conditions, it looks like the attitude using my custom AttitudeProviderModifier diverges from the underlying attitude even though I am just returning the same underlying attitude. And since the thrust direction is, of course, attitude dependent, the orbit also tends to diverge given time, especially if further low thrust manoeuvres take place at a later stage.

I have managed to reproduce this behaviour with a short code, which I am attaching down here. Let me know if you can have a look and spot any mistake, or if this is something you expect, for any reason.

Here you can see the “rotation distance” between the two attitudes. This was generated using the code below, where you can find all the details, and it’s for a LEO with a 1h-long low thrust manoeuvre happening after 5 days of propagation. 20 days are propagated in total.

Thank you very much for your help!

Here is the code (remember to change the path to the orekit data)

import orekit
from orekit import JArray_double
from pathlib import Path
from org.orekit.attitudes import NadirPointing, YawCompensation, PythonAttitudeProviderModifier, FieldAttitude
from  org.orekit.utils import PVCoordinatesProvider, FieldPVCoordinatesProvider
from org.orekit.bodies import OneAxisEllipsoid
from org.hipparchus.ode.nonstiff import DormandPrince853Integrator
from org.hipparchus.util import FastMath
from org.orekit.frames import FramesFactory
from org.orekit.orbits import EquinoctialOrbit, PositionAngleType, OrbitType
from org.orekit.propagation import SpacecraftState
from org.orekit.propagation.numerical import NumericalPropagator
from org.orekit.time import AbsoluteDate, TimeScalesFactory
from org.orekit.utils import Constants, IERSConventions
from org.orekit.forces import maneuvers 
from org.orekit.forces.maneuvers import trigger 
from  org.hipparchus.geometry.euclidean.threed import Vector3D
from org.orekit.forces.maneuvers.propulsion import ThrustDirectionAndAttitudeProvider as thrust_dir_prov
from org.hipparchus.geometry.euclidean.threed import Rotation
import matplotlib.pyplot as plt

vm = orekit.initVM()
from orekit.pyhelpers import setup_orekit_curdir
dir_path = Path(__file__).parent
orekit_data = Path(dir_path, <PATH_TO_OREKIT_DATA>)
setup_orekit_curdir(str(orekit_data))

### CONSTANTS
UTC = TimeScalesFactory.getUTC()
INERTIAL = FramesFactory.getEME2000()
ITRF = FramesFactory.getITRF(IERSConventions.IERS_2010, True)
EARTH_BODYSHAPE = OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS, Constants.WGS84_EARTH_FLATTENING, ITRF)
EARTH_RADIUS = Constants.WGS84_EARTH_EQUATORIAL_RADIUS
INITIAL_DATE = AbsoluteDate(2024, 8, 30, 15, 0, 00.000, UTC)

### ATTITUDE PROVIDER MODIFIER - CUSTOM IMPLEMENTATION
class TestAttProviderModifier(PythonAttitudeProviderModifier):
    EME2000 = FramesFactory.getEME2000()
    itrf    = FramesFactory.getITRF(IERSConventions.IERS_2010, True)
    earth = OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS, Constants.WGS84_EARTH_FLATTENING, itrf)

    nadir_pointing_att_provider = NadirPointing(EME2000, earth)
    yaw_comp_att_provider =  YawCompensation(EME2000, nadir_pointing_att_provider)

    def getAttitude(self, pv_coordinates_provider, date, frame):
        match (pv_coordinates_provider):
            case PVCoordinatesProvider():
                original_attitude = TestAttProviderModifier.yaw_comp_att_provider.getAttitude(pv_coordinates_provider, date, frame)
                return original_attitude
            
            case FieldPVCoordinatesProvider():
                original_attitude = TestAttProviderModifier.yaw_comp_att_provider.getAttitude(pv_coordinates_provider, date, frame)
                return FieldAttitude(date, frame, original_attitude.getRotation(), original_attitude.getRotationAcceleration())
            
            case _:
                raise RuntimeError(f'Not supported type of PVCoordinatesProvider: {type(pv_coordinates_provider).__name__}')
            
    def getUnderlyingAttitudeProvider(self):
        return TestAttProviderModifier.yaw_comp_att_provider
    
### HELPER METHODS
def get_numerical_propagator(initial_orbit, attitude_provider):
    # Standard numerical propagator, nothing fancy here
    min_step = 0.001
    maxstep = 1000.0
    init_step = 60.0
    position_tolerance = 0.000001
    tolerances = NumericalPropagator.tolerances(
        position_tolerance,
        initial_orbit,
        OrbitType.EQUINOCTIAL
        )
    integrator = DormandPrince853Integrator(
        min_step, maxstep,                                  
        JArray_double.cast_(tolerances[0]),
        JArray_double.cast_(tolerances[1])
        )
    integrator.setInitialStepSize(init_step)
    return NumericalPropagator(integrator, attitude_provider)

def get_orbit():
    #  LEO with non-null inclination
    a = 500. * 1000. + EARTH_RADIUS
    return EquinoctialOrbit(
        a, 0.001, 0.001, 0.7, 0.7,
        FastMath.toRadians(30.), PositionAngleType.TRUE, INERTIAL,
        INITIAL_DATE, Constants.WGS84_EARTH_MU
        )

def get_manoeuvre(man_start_epoch : AbsoluteDate):
        # Get a 1h long low thrust manoeuvre without attitude override, aligned with body x direction, starting at a given date.
        maneuver_trigger = trigger.DateBasedManeuverTriggers(man_start_epoch, 3600.)
        thrust_direction   = Vector3D(1., 0., 0.) 
        thrust_direction_provider = thrust_dir_prov.buildFromFixedDirectionInSatelliteFrame(thrust_direction)
        thrust = 0.001
        isp = 3000.
        low_thrust_man = maneuvers.ConfigurableLowThrustManeuver(
            thrust_direction_provider, 
            maneuver_trigger, 
            thrust, 
            isp
            )
        return low_thrust_man 

### MAIN
if __name__ == '__main__':

    # Get initial orbit
    initial_orbit = get_orbit()

    # Get two identical attitudes, but one is generated using TestAttProviderModifier(), see definition above.
    # Expected behaviour: the two attitude should behave exactly in the same manner, since TestAttProviderModifier 
    # simply returns the attitude generated using YawCompensation. (right?)
    # Yaw compensation
    nadir_pointing_att_provider = NadirPointing(INERTIAL, EARTH_BODYSHAPE)
    yaw_comp_att_provider =  YawCompensation(INERTIAL, nadir_pointing_att_provider)
    # Wrapped yaw compensation
    CUSTOM_attitude_provider = TestAttProviderModifier()

    # Initialize two numerical propagators using the two attitude providers just initialized (and the same orbit)
    propagator_yaw_compensation = get_numerical_propagator(initial_orbit, yaw_comp_att_provider)
    propagator_CUSTOM_attitude  = get_numerical_propagator(initial_orbit, CUSTOM_attitude_provider)

    # Create initial state
    initial_state = SpacecraftState(initial_orbit)

    # Get a low thrust manoeuvre (1h long, starting 5 days after propagation start)
    man = get_manoeuvre(initial_orbit.getDate().shiftedBy(5* 24*3600.))

    # Compute date list for propagation and plots
    propagation_target_date = initial_orbit.getDate().shiftedBy(20* 24*3600.)
    date_list = [initial_orbit.getDate().shiftedBy(h*100.) for h in range(36*24*20)] # for plotting

    # Setup both propagators
    propagator_yaw_compensation.setOrbitType(OrbitType.EQUINOCTIAL)
    propagator_CUSTOM_attitude.setOrbitType( OrbitType.EQUINOCTIAL)

    propagator_yaw_compensation.setInitialState(initial_state)
    propagator_CUSTOM_attitude.setInitialState( initial_state)

    propagator_yaw_compensation.addForceModel(man)
    propagator_CUSTOM_attitude.addForceModel( man)

    eph_generator_yaw_compensation = propagator_yaw_compensation.getEphemerisGenerator()
    eph_generator_CUSTOM_attitude  = propagator_CUSTOM_attitude.getEphemerisGenerator()


    # Propagate and extract attitude rotations at all times
    propagator_yaw_compensation.propagate(propagation_target_date)
    bound_prop_yaw_compensation = eph_generator_yaw_compensation.getGeneratedEphemeris()

    propagator_CUSTOM_attitude.propagate(propagation_target_date)
    bound_prop_CUSTOM_attitude = eph_generator_CUSTOM_attitude.getGeneratedEphemeris()

    rotations_yaw_compensation = [bound_prop_yaw_compensation.propagate(t).getAttitude().getRotation() for t in date_list] 
    rotations_CUSTOM_attitude  = [bound_prop_CUSTOM_attitude.propagate(t).getAttitude().getRotation()  for t in date_list] 

    rotation_distance          = [FastMath.toDegrees(Rotation.distance(rotations_yaw_compensation[i], rotations_CUSTOM_attitude[i])) for i in range(len(date_list))]

    # Plot rotation distances
    time_vec = [i*100/(3600*24) for i in range(len(date_list))]
    plt.plot(time_vec, rotation_distance)

Hello @Riccardo_rct and welcome to the Orekit forum !

First of all thank you for providing an example as well as a detailed explanation.

I have started investigating your issue but I am afraid i will have to create an equivalent Java code to dig deeper as I did not find anything wrong in your code sample.

I have other work to attend to so I’m afraid it will take some time :thinking: .

Cheers,
Vincent

1 Like

In the field version, I am not sure original_attitude.getRotationAcceleration() is correct. Aren’t you missing the spin (i.e. first time derivative, not second time derivative)?

Hi there,

Could you try swapping the order in which you propagate? So custom first and then native yaw steering.
I remember once having a weird behaviour with Orekit (in python too, but it might have been nothing to do with it). I was doing propagation and computation using Earth Orientation Parameters data. I noticed that I was getting slightly different results if I was doing a first, identical propagation beforehand. I never investigated but I suspect it could have been linked to some caching in the EOP interpolation.

Cheers,
Romain.

Hi all, thanks for your answers so far.
@Vincent, thanks for checking, and no rush :slight_smile:

@luc ,
I tried doing as you said, that is:

case FieldPVCoordinatesProvider():
                original_attitude = TestAttProviderModifier.yaw_comp_att_provider.getAttitude(pv_coordinates_provider, date, frame)
                return FieldAttitude(date, frame, original_attitude.getRotation(), original_attitude.getSpin(), original_attitude.getRotationAcceleration())

or

case FieldPVCoordinatesProvider():
                original_attitude = TestAttProviderModifier.yaw_comp_att_provider.getAttitude(pv_coordinates_provider, date, frame)
                return FieldAttitude(date, frame, original_attitude.getRotation(), original_attitude.getSpin())

(I tried both), but it looks like it doesn’t have an impact on this.
Actually, after some debugging I believe that this “field” case is never even accessed at run time.

@Serrof ,
If this is what you meant,

    # Propagate and extract attitude rotations at all times
    propagator_CUSTOM_attitude.propagate(propagation_target_date)
    bound_prop_CUSTOM_attitude = eph_generator_CUSTOM_attitude.getGeneratedEphemeris()
    
    propagator_yaw_compensation.propagate(propagation_target_date)
    bound_prop_yaw_compensation = eph_generator_yaw_compensation.getGeneratedEphemeris()

…then I have just tried it, but with no luck. It looks like it doesn’t make a difference. Or, at least, it looks like the angular error is the same.

Hello everyone,

So I’ve made a java equivalent of your python code but i have not managed to reproduce the issue :confused:.

You will find both codes below (don’t forget to set your own path to orekit data)

EDIT : It seems that the issue comes from thrust_dir_prov.buildFromFixedDirectionInSatelliteFrame(thrust_direction)

As using thrust_dir_prov.buildFromDirectionInLOF(LOFType.TNW, ConstantThrustDirectionProvider(thrust_direction),thrust_direction) fixes the issue

Python code (my own slightly modified version)

import numpy as np
import orekit
from orekit import JArray_double
from pathlib import Path
from org.orekit.attitudes import NadirPointing, YawCompensation, PythonAttitudeProviderModifier, FieldAttitude
from org.orekit.propagation.sampling import PythonOrekitFixedStepHandler
from org.orekit.utils import PVCoordinatesProvider, FieldPVCoordinatesProvider
from org.orekit.bodies import OneAxisEllipsoid
from org.hipparchus.ode.nonstiff import DormandPrince853Integrator
from org.hipparchus.util import FastMath
from org.orekit.frames import FramesFactory
from org.orekit.orbits import EquinoctialOrbit, PositionAngleType, OrbitType
from org.orekit.propagation import SpacecraftState
from org.orekit.propagation.numerical import NumericalPropagator
from org.orekit.time import AbsoluteDate, TimeScalesFactory
from org.orekit.utils import Constants, IERSConventions
from org.orekit.forces import maneuvers
from org.orekit.forces.maneuvers import trigger
from org.hipparchus.geometry.euclidean.threed import Vector3D
from org.orekit.forces.maneuvers.propulsion import ThrustDirectionAndAttitudeProvider as thrust_dir_prov
from org.hipparchus.geometry.euclidean.threed import Rotation
import matplotlib.pyplot as plt

vm = orekit.initVM()
from orekit.pyhelpers import setup_orekit_curdir

dir_path = Path(__file__).parent
orekit_data = Path(dir_path, "PATH/TO/OREKIT/DATA")
setup_orekit_curdir(str(orekit_data))

### CONSTANTS
UTC = TimeScalesFactory.getUTC()
INERTIAL = FramesFactory.getEME2000()
ITRF = FramesFactory.getITRF(IERSConventions.IERS_2010, True)
EARTH_BODYSHAPE = OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS, Constants.WGS84_EARTH_FLATTENING, ITRF)
EARTH_RADIUS = Constants.WGS84_EARTH_EQUATORIAL_RADIUS
INITIAL_DATE = AbsoluteDate(2024, 8, 30, 15, 0, 00.000, UTC)


### ATTITUDE PROVIDER MODIFIER - CUSTOM IMPLEMENTATION
class TestAttProviderModifier(PythonAttitudeProviderModifier):

    def __init__(self):
        super().__init__()
        self.nadir_pointing_att_provider = NadirPointing(INERTIAL, EARTH_BODYSHAPE)
        self.yaw_comp_att_provider = YawCompensation(INERTIAL, nadir_pointing_att_provider)

    def getAttitude(self, pv_coordinates_provider, date, frame):
        return self.yaw_comp_att_provider.getAttitude(pv_coordinates_provider, date, frame)

    def getUnderlyingAttitudeProvider(self):
        return self.yaw_comp_att_provider


### Simple step handler
class RotationStepHandler(PythonOrekitFixedStepHandler):

    def __init__(self):
        super().__init__()
        self.rotations = []

    def init(self, spacecraftState, absoluteDate, double):
        pass

    def finish(self, spacecraftState):
        pass

    def handleStep(self, spacecraftState):
        self.rotations.append(spacecraftState.getAttitude().getRotation())

    def get_rotation(self):
        return self.rotations


### HELPER METHODS
def get_numerical_propagator(initial_orbit, attitude_provider):
    # Standard numerical propagator, nothing fancy here
    min_step = 0.001
    maxstep = 1000.0
    init_step = 60.0
    position_tolerance = 1.
    tolerances = NumericalPropagator.tolerances(
        position_tolerance,
        initial_orbit,
        OrbitType.EQUINOCTIAL
    )
    integrator = DormandPrince853Integrator(
        min_step, maxstep,
        JArray_double.cast_(tolerances[0]),
        JArray_double.cast_(tolerances[1])
    )
    integrator.setInitialStepSize(init_step)
    return NumericalPropagator(integrator, attitude_provider)


def get_orbit():
    #  LEO with non-null inclination
    a = 500. * 1000. + EARTH_RADIUS
    return EquinoctialOrbit(
        a, 0.001, 0.001, 0.7, 0.7,
        FastMath.toRadians(30.), PositionAngleType.TRUE, INERTIAL,
        INITIAL_DATE, Constants.WGS84_EARTH_MU
    )


def get_manoeuvre(man_start_epoch: AbsoluteDate):
    # Get a 1h long low thrust manoeuvre without attitude override, aligned with body x direction, starting at a given date.
    maneuver_trigger = trigger.DateBasedManeuverTriggers(man_start_epoch, 3600.)
    thrust_direction = Vector3D(1., 0., 0.)
    thrust_direction_provider = thrust_dir_prov.buildFromFixedDirectionInSatelliteFrame(thrust_direction)
    thrust = 0.001
    isp = 3000.
    low_thrust_man = maneuvers.ConfigurableLowThrustManeuver(
        thrust_direction_provider,
        maneuver_trigger,
        thrust,
        isp
    )
    return low_thrust_man


### MAIN
if __name__ == '__main__':
    # Get initial orbit
    initial_orbit = get_orbit()

    # Get two identical attitudes, but one is generated using TestAttProviderModifier(), see definition above.
    # Expected behaviour: the two attitude should behave exactly in the same manner, since TestAttProviderModifier
    # simply returns the attitude generated using YawCompensation. (right?)
    # Yaw compensation
    nadir_pointing_att_provider = NadirPointing(INERTIAL, EARTH_BODYSHAPE)
    yaw_comp_att_provider = YawCompensation(INERTIAL, nadir_pointing_att_provider)
    # Wrapped yaw compensation
    CUSTOM_attitude_provider = TestAttProviderModifier()

    # Initialize two numerical propagators using the two attitude providers just initialized (and the same orbit)
    propagator_yaw_compensation = get_numerical_propagator(initial_orbit, yaw_comp_att_provider)
    propagator_CUSTOM_attitude = get_numerical_propagator(initial_orbit, CUSTOM_attitude_provider)

    # Create initial state
    initial_state = SpacecraftState(initial_orbit)

    # Get a low thrust manoeuvre (1h long, starting 5 days after propagation start)
    man = get_manoeuvre(initial_orbit.getDate().shiftedBy(5 * 24 * 3600.))

    # Compute date list for propagation and plots
    propagation_target_date = initial_orbit.getDate().shiftedBy(20 * 24 * 3600.)
    date_list = [initial_orbit.getDate().shiftedBy(h * 100.) for h in range(36 * 24 * 20)]  # for plotting

    # Setup both propagators
    propagator_yaw_compensation.setOrbitType(OrbitType.EQUINOCTIAL)
    propagator_CUSTOM_attitude.setOrbitType(OrbitType.EQUINOCTIAL)

    propagator_yaw_compensation.setInitialState(initial_state)
    propagator_CUSTOM_attitude.setInitialState(initial_state)

    propagator_yaw_compensation.addForceModel(man)
    propagator_CUSTOM_attitude.addForceModel(man)

    eph_generator_yaw_compensation = propagator_yaw_compensation.getEphemerisGenerator()
    eph_generator_CUSTOM_attitude = propagator_CUSTOM_attitude.getEphemerisGenerator()

    # Propagate and extract attitude rotations at all times
    propagator_yaw_compensation.propagate(propagation_target_date)
    bound_prop_yaw_compensation = eph_generator_yaw_compensation.getGeneratedEphemeris()

    propagator_CUSTOM_attitude.propagate(propagation_target_date)
    bound_prop_CUSTOM_attitude = eph_generator_CUSTOM_attitude.getGeneratedEphemeris()

    # Use step handlers
    yaw_step_handler = RotationStepHandler()
    custom_step_handler = RotationStepHandler()

    bound_prop_yaw_compensation.setStepHandler(60., yaw_step_handler)
    bound_prop_CUSTOM_attitude.setStepHandler(60., custom_step_handler)

    bound_prop_yaw_compensation.propagate(propagation_target_date)
    bound_prop_CUSTOM_attitude.propagate(propagation_target_date)

    rotations_yaw_compensation = yaw_step_handler.get_rotation()
    rotations_CUSTOM_attitude = custom_step_handler.get_rotation()
    rotation_distance = [
        FastMath.toDegrees(Rotation.distance(rotations_yaw_compensation[i], rotations_CUSTOM_attitude[i])) for i in
        range(len(rotations_yaw_compensation))]

    # Plot
    time_array = np.array(range(len(rotations_yaw_compensation))) * 60 / (24 * 3600)
    plt.plot(time_array, rotation_distance)
    plt.show()

Java equivalent

import org.hipparchus.CalculusFieldElement;
import org.hipparchus.geometry.euclidean.threed.Rotation;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.ode.nonstiff.DormandPrince853Integrator;
import org.hipparchus.util.FastMath;
import org.orekit.attitudes.Attitude;
import org.orekit.attitudes.AttitudeProvider;
import org.orekit.attitudes.FieldAttitude;
import org.orekit.attitudes.NadirPointing;
import org.orekit.attitudes.YawCompensation;
import org.orekit.bodies.OneAxisEllipsoid;
import org.orekit.errors.OrekitException;
import org.orekit.forces.maneuvers.ConfigurableLowThrustManeuver;
import org.orekit.forces.maneuvers.propulsion.ThrustDirectionAndAttitudeProvider;
import org.orekit.forces.maneuvers.trigger.DateBasedManeuverTriggers;
import org.orekit.frames.Frame;
import org.orekit.frames.FramesFactory;
import org.orekit.orbits.EquinoctialOrbit;
import org.orekit.orbits.OrbitType;
import org.orekit.orbits.PositionAngleType;
import org.orekit.propagation.BoundedPropagator;
import org.orekit.propagation.EphemerisGenerator;
import org.orekit.propagation.SpacecraftState;
import org.orekit.propagation.numerical.NumericalPropagator;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.FieldAbsoluteDate;
import org.orekit.time.TimeScalesFactory;
import org.orekit.utils.Constants;
import org.orekit.utils.FieldPVCoordinatesProvider;
import org.orekit.utils.IERSConventions;
import org.orekit.utils.PVCoordinatesProvider;
import output.BenchmarkUtils;

import java.util.ArrayList;
import java.util.List;

public class ForumIssueAttitudeProviderModifier {

   protected static Frame            INERTIAL;
   protected static AbsoluteDate     INITIAL_DATE;
   protected static double           EARTH_RADIUS;
   protected static OneAxisEllipsoid EARTH_BODYSHAPE;

   public static void main(String[] args) throws OrekitException {
      // Initialize Orekit data path
      BenchmarkUtils.loadOrekitData();

      // Initialize variables
      INERTIAL        = FramesFactory.getEME2000();
      INITIAL_DATE    = new AbsoluteDate(2024, 8, 30, 15, 0, 0.0, TimeScalesFactory.getUTC());
      EARTH_RADIUS    = Constants.WGS84_EARTH_EQUATORIAL_RADIUS;
      EARTH_BODYSHAPE = new OneAxisEllipsoid(EARTH_RADIUS,
                                             Constants.WGS84_EARTH_FLATTENING,
                                             FramesFactory.getITRF(IERSConventions.IERS_2010, true)
      );
      // Initialize initial orbit
      EquinoctialOrbit initialOrbit = getOrbit();

      // Initialize attitude provider
      NadirPointing   nadirPointing   = new NadirPointing(INERTIAL, EARTH_BODYSHAPE);
      YawCompensation yawCompensation = new YawCompensation(INERTIAL, nadirPointing);

      // Initialize wrapped attitude provider
      AttitudeProvider customAttitudeProvider = new TestAttProviderModifier();

      // Initialize propagators
      NumericalPropagator propagatorYawCompensation = getNumericalPropagator(initialOrbit, yawCompensation);
      NumericalPropagator propagatorCustomAttitude  = getNumericalPropagator(initialOrbit, customAttitudeProvider);

      // Initialize state
      SpacecraftState initialState = new SpacecraftState(initialOrbit);

      // Add maneuver force model
      AbsoluteDate                  maneuverStart = initialOrbit.getDate().shiftedBy(5 * 24 * 3600.);
      ConfigurableLowThrustManeuver maneuver      = getManeuver(maneuverStart);

      // Setup both propagators
      propagatorYawCompensation.setInitialState(initialState);
      propagatorCustomAttitude.setInitialState(initialState);

      propagatorYawCompensation.setOrbitType(OrbitType.EQUINOCTIAL);
      propagatorCustomAttitude.setOrbitType(OrbitType.EQUINOCTIAL);

      propagatorYawCompensation.addForceModel(maneuver);
      propagatorCustomAttitude.addForceModel(maneuver);

      final EphemerisGenerator ephGeneratorYawCompensation = propagatorYawCompensation.getEphemerisGenerator();
      final EphemerisGenerator ephGeneratorCustomAttitude  = propagatorCustomAttitude.getEphemerisGenerator();

      // Propagate and extract attitude rotations at all times
      AbsoluteDate propagationEndDate = initialOrbit.getDate().shiftedBy(20 * 24 * 3600.);
      propagatorYawCompensation.propagate(propagationEndDate);
      propagatorCustomAttitude.propagate(propagationEndDate);

      // Get generated ephemeris
      final BoundedPropagator ephemerisYawCompensation = ephGeneratorYawCompensation.getGeneratedEphemeris();
      final BoundedPropagator ephemerisCustomAttitude  = ephGeneratorCustomAttitude.getGeneratedEphemeris();

      // Set step handlers to store rotations
      final List<Rotation> rotationsYawCompensation = new ArrayList<>();
      final List<Rotation> rotationsCustomAttitude  = new ArrayList<>();

      ephemerisYawCompensation.setStepHandler(60.,
                                              (spacecraftState -> rotationsYawCompensation.add(spacecraftState.getAttitude()
                                                                                                              .getRotation())));
      ephemerisCustomAttitude.setStepHandler(60.,
                                             (spacecraftState -> rotationsCustomAttitude.add(spacecraftState.getAttitude()
                                                                                                            .getRotation())));

      // Propagate ephemeris
      ephemerisYawCompensation.propagate(propagationEndDate);
      ephemerisCustomAttitude.propagate(propagationEndDate);

      // Compute distance between rotation
      final List<Double> rotationDistances = computeRotationDistance(rotationsYawCompensation, rotationsCustomAttitude);
      System.out.println(rotationDistances);

   }

   private static List<Double> computeRotationDistance(final List<Rotation> rotationsYawCompensation,
                                                       final List<Rotation> rotationsCustomAttitude) {
      final List<Double> rotationDistances = new ArrayList<>();

      for (int i = 0; i < rotationsYawCompensation.size(); i++) {
         final double distance = Rotation.distance(rotationsYawCompensation.get(i), rotationsCustomAttitude.get(i));
         rotationDistances.add(distance);
      }

      return rotationDistances;
   }

   public static EquinoctialOrbit getOrbit() throws OrekitException {
      double a = 500_000.0 + EARTH_RADIUS;
      return new EquinoctialOrbit(
            a, 0.001, 0.001, 0.7, 0.7,
            FastMath.toRadians(30.), PositionAngleType.TRUE,
            FramesFactory.getEME2000(), INITIAL_DATE, Constants.WGS84_EARTH_MU
      );
   }

   public static ConfigurableLowThrustManeuver getManeuver(AbsoluteDate startDate) throws OrekitException {
      DateBasedManeuverTriggers trigger         = new DateBasedManeuverTriggers(startDate, 3600.0);
      Vector3D                  thrustDirection = new Vector3D(1.0, 0.0, 0.0);
      double                    thrust          = 0.001;
      double                    isp             = 3000.0;
      ThrustDirectionAndAttitudeProvider provider =
            ThrustDirectionAndAttitudeProvider.buildFromFixedDirectionInSatelliteFrame(thrustDirection);
      return new ConfigurableLowThrustManeuver(provider, trigger, thrust, isp);
   }

   public static NumericalPropagator getNumericalPropagator(EquinoctialOrbit initialOrbit,
                                                            AttitudeProvider attitudeProvider) throws OrekitException {
      double minStep           = 0.001;
      double maxStep           = 1000.0;
      double initStep          = 60.0;
      double positionTolerance = 1;
      double[][] tolerances =
            NumericalPropagator.tolerances(positionTolerance, initialOrbit, OrbitType.EQUINOCTIAL);
      DormandPrince853Integrator integrator =
            new DormandPrince853Integrator(minStep, maxStep, tolerances[0], tolerances[1]);
      integrator.setInitialStepSize(initStep);

      NumericalPropagator propagator = new NumericalPropagator(integrator);

      propagator.setAttitudeProvider(attitudeProvider);
      return propagator;
   }

   static class TestAttProviderModifier implements AttitudeProviderModifier {
      private final YawCompensation yawCompensation;

      public TestAttProviderModifier() {
         NadirPointing nadirPointing = new NadirPointing(ForumIssueAttitudeProviderModifier.INERTIAL,
                                                         ForumIssueAttitudeProviderModifier.EARTH_BODYSHAPE);
         this.yawCompensation = new YawCompensation(ForumIssueAttitudeProviderModifier.INERTIAL, nadirPointing);
      }

      @Override
      public Attitude getAttitude(PVCoordinatesProvider pvProv, AbsoluteDate date, Frame frame) throws OrekitException {
         return yawCompensation.getAttitude(pvProv, date, frame);
      }

      @Override
      public <T extends CalculusFieldElement<T>> FieldAttitude<T> getAttitude(final FieldPVCoordinatesProvider<T> fieldPVCoordinatesProvider,
                                                                              final FieldAbsoluteDate<T> fieldAbsoluteDate,
                                                                              final Frame frame) {
         return yawCompensation.getAttitude(fieldPVCoordinatesProvider, fieldAbsoluteDate, frame);
      }

      @Override
      public AttitudeProvider getUnderlyingAttitudeProvider() {
         return yawCompensation;
      }
   }
}

Cheers,
Vincent

1 Like

Hi Vincent,

Thanks for your answer. It does indeed look like the problem comes from
thrust_dir_prov.buildFromFixedDirectionInSatelliteFrame(thrust_direction)

So, is there any way to solve this and still use a fixed direction in satellite frame?

Cheers
Riccardo

Hello @Riccardo_rct,

The workaround i proposed is equivalent to using a fixed direction in the satellite frame.

However i still need to understand why this happen with buildFromFixedDirectionInSatelliteFrame specifically.

Cheers,
Vincent