EventDetector behavior 'inconsistent' with propagator in slave mode

Hello,

I’m building some propagation models with simple impulsive maneuvers using the python wrapper, and looking at the implementation difference between using master vs slave mode in propagation for custom orekit handlers vs custom application handlers, along with maneuvers in both cases.

Using slave mode with application-level handler reads a bit more clearly in python imo for making a custom actions at given time-steps, but in looking at that I noticed strange behavior for the maneuvers.

If in slave mode, DSST propagator, Keplerian orbit, I add the event detector and maneuver, then propagate using a single line to the end of the time span, t_end, such as:
propagator.propagate(startDate.shiftedBy(t_end))
then the maneuver happens.

But if I wrap this in a loop to manage the step size ‘t’ and iterate to t_end, the maneuver does NOT happen (all else equal).

The event detector is a PositionAngleDetector to trigger of a given mean anomaly, and I’m making sure to actually pass through that in each propagation.

So does something happen to detectors/propagators when you repeatedly make the propagate() call, that perhaps destroys the original state? I tried testing this by adding the
propagator.addEventDetector(maneuver)
into my loop, but that didn’t fix the issue. If I make the step size of the loop beyond the time t where the condition will first be encountered to trigger the maneuver, it also works. So there’s something with the n+1 calls to propagate() that seems to un-set something… any ideas?

Thank you

Hi @ShikaDing,

Did you manage to fix your issue?
If not, could you provide a failing test case so we can look into it faster?

Regards,
Maxime

Hello @MaximeJ,

Unfortunately no luck figuring this out yet - I’ve been looking at using the master-mode which is probably more ‘orekit-ian’ anyway, but it would be good to understand this behavior in slave mode.

Here is a setup in python that shows the issue. If you toggle the ‘stepwise’ flag (True/False), this will walk the slave-mode propagator either through defined time steps up to final time t, OR input final time t as the single propagation time. The maneuvers only happen in the later case, when it’s set to False.

A side issue I’ve been scratching my head over is that the span duration must be 2.0 * the orbit period (T) or no maneuver happens. For example span = 1.0 or 1.8T no maneuver, and the trigger is mean anomaly, so anything >1.0T should always work. Maybe I’ll make another post for that though.

Thanks!

import orekit
from orekit.pyhelpers import setup_orekit_curdir
from org.orekit.attitudes import LofOffset
from org.orekit.utils import Constants, IERSConventions
from org.orekit.forces.maneuvers import ImpulseManeuver
from org.orekit.frames import FramesFactory, LOFType
from org.orekit.propagation import PropagationType, SpacecraftState
from org.orekit.propagation.conversion import DSSTPropagatorBuilder, DormandPrince853IntegratorBuilder
from org.orekit.propagation.events import PositionAngleDetector, EventDetector, EventEnablingPredicateFilter
from org.orekit.time import TimeScalesFactory, AbsoluteDate, UT1Scale, TimeScales
from org.orekit.orbits import KeplerianOrbit, PositionAngle, OrbitType
from org.hipparchus.geometry.euclidean.threed import Vector3D
import math

vm = orekit.initVM()
setup_orekit_curdir()

eme2000 = FramesFactory.getEME2000()
utc = TimeScalesFactory.getUTC()
mu = Constants.EIGEN5C_EARTH_MU
startDate = AbsoluteDate(2021, 1, 21, 12, 58, 59.00, utc)

initialOrbit2 = KeplerianOrbit(1000_000.0,  # a
                               0.01,  # e
                               math.radians(50.0),  # inc
                               math.radians(140.0),  # pa
                               math.radians(10.0),  # raan
                               math.radians(10.0),  # anomaly
                               PositionAngle.MEAN,
                               eme2000,
                               startDate,
                               mu
                               )

'''
Set sat attitude frame:
- VNC:  Velocity - Normal - Co-normal frame (X axis aligned with velocity, Y axis aligned with orbital momentum)
'''
attitudeOverride = LofOffset(eme2000, LOFType.VNC)
sc_state2 = SpacecraftState(initialOrbit2)

# Propagator:
min_step = 1.0
max_step = 100.0
pos_err = 10.0

prop_builder = DSSTPropagatorBuilder(initialOrbit2,
                                     DormandPrince853IntegratorBuilder(min_step, max_step, pos_err),
                                     1.0,  # position 'scaling factor'. Typically standard dev of pos err
                                     PropagationType.MEAN,  # type OF THE ORBIT used for prop
                                     PropagationType.MEAN  # type of elements used to define orbital state
                                     )
propagator2 = prop_builder.buildPropagator(prop_builder.getSelectedNormalizedParameters())

# Create maneuver trigger on mean anomaly, specified below:
angle = math.radians(0.0)
trigger = PositionAngleDetector(OrbitType.KEPLERIAN, PositionAngle.TRUE, angle)

# Create maneuver with trigger (in-track, +ve dir)
dv_dir = Vector3D.PLUS_I.scalarMultiply(100.0)
maneuver = ImpulseManeuver(trigger,
                           attitudeOverride,
                           dv_dir,  # delta-v dir, along x-axis (i,j,k)
                           500.0  # ISP - required but NOT used for impuslive. weird
                           )

# add maneuver to propagator
propagator2.addEventDetector(maneuver)

print(f"- Initial attributes:")
print(f"SMA, km: {round(initialOrbit2.getA(),3) / 1000.0 }")
print(f"RAAN: {round(math.degrees(initialOrbit2.getRightAscensionOfAscendingNode()),3)}")
print(f"Inc: {round(math.degrees(initialOrbit2.getI()),3)}")

# propagate
span_sec = 2.0 * initialOrbit2.getKeplerianPeriod()
step_sec = 100.0

# Now propagate one of two different ways, both using slave mode, but one with predefined steps, the other right
# to the end of time span:
stepwise = True    # toggle this to see the difference

if stepwise:    # propagate and print in defined steps
    print(f"Mean_Anom(deg), SMA (km)")
    for t in range(1, round(span_sec / step_sec)):
        state = propagator2.propagate(startDate.shiftedBy(t*step_sec))

        kep_orb = KeplerianOrbit(state.getOrbit())
        SMA = round(kep_orb.getA(), 2)
        mean_anom = round(math.degrees(kep_orb.getMeanAnomaly()), 3)
        print(f"{mean_anom}: {SMA / 1000.0}")
else:           # give the propagator the final end time
    state = propagator2.propagate(startDate.shiftedBy(span_sec))

finalOrbit = state.getOrbit()
finalOrb_Kep = KeplerianOrbit(finalOrbit)
final_SMA = round(finalOrbit.getA() / 1000, 5)
final_RAAN = round(math.degrees(finalOrb_Kep.getRightAscensionOfAscendingNode()), 5)
final_inc = round(math.degrees(finalOrb_Kep.getI()), 5)

print(f"\n- Final attributes:")
print(f"SMA, km: {final_SMA}")
print(f"RAAN: {final_RAAN}")
print(f"Inc: {final_inc}")

Seems that a < 6378137 which could be causing some issues for an Earth orbit. ISP is used to update mass.

If you’re just using central attraction I’m not sure why you’re seeing the effect you are. My guess is that it is because you’re using an adaptive step size integrator. Try setting minStep=maxStep=step_sec, or use a fixed step integrator and my guess is you’ll see more similar results. Integrator error is a random walk, so once the integrator takes a different step it is on a different trajectory. They’re both within the tolerance of the integrator. My guess is that you have a case that is highly sensitive to integrator error. In your case using anomaly with a nearly circular orbit may be part of the problem. Maybe there is an issue with event detection, but these other issues should be addressed first to facilitate a clear discussion of the event detection issues.

Regards,
Evan

A few updates:

  • a was meant to be that value + ae (orekit earth rad constant) - dropped that in my distilling for this example. But I think since I’m not using any kind of earth model, it shouldn’t matter
  • tried minStep=maxStep=step_sec, no change
  • circular orbit: eccentricity is set to 0.01, is that really considered nearly circular? Same results with 0.1, or 0.8
  • implemented a fixed step integrator, same result.

Below is an update with the notes above, configured to integrate with the classical Runge-Kutta integrator, step size 100. Both integrators give the same results to each other whether the flag is set to True or False (for how to propagate up to time t)

import orekit
from orekit.pyhelpers import setup_orekit_curdir
from org.orekit.attitudes import LofOffset
from org.orekit.utils import Constants, IERSConventions
from org.orekit.forces.maneuvers import ImpulseManeuver
from org.orekit.frames import FramesFactory, LOFType
from org.orekit.propagation import PropagationType, SpacecraftState
from org.orekit.propagation.conversion import DSSTPropagatorBuilder, DormandPrince853IntegratorBuilder, ClassicalRungeKuttaIntegratorBuilder
from org.orekit.propagation.events import PositionAngleDetector, EventDetector, EventEnablingPredicateFilter
from org.orekit.time import TimeScalesFactory, AbsoluteDate, UT1Scale, TimeScales
from org.orekit.orbits import KeplerianOrbit, PositionAngle, OrbitType
from org.hipparchus.geometry.euclidean.threed import Vector3D
import math

vm = orekit.initVM()
setup_orekit_curdir()

eme2000 = FramesFactory.getEME2000()
utc = TimeScalesFactory.getUTC()
mu = Constants.EIGEN5C_EARTH_MU
ae = Constants.WGS84_EARTH_EQUATORIAL_RADIUS
startDate = AbsoluteDate(2021, 1, 21, 12, 58, 59.00, utc)

initialOrbit2 = KeplerianOrbit(1000_000.0 + ae,  # a
                               0.8,  # e
                               math.radians(50.0),  # inc
                               math.radians(140.0),  # pa
                               math.radians(10.0),  # raan
                               math.radians(10.0),  # anomaly
                               PositionAngle.MEAN,
                               eme2000,
                               startDate,
                               mu
                               )

'''
Set sat attitude frame:
- VNC:  Velocity - Normal - Co-normal frame (X axis aligned with velocity, Y axis aligned with orbital momentum)
'''
attitudeOverride = LofOffset(eme2000, LOFType.VNC)
sc_state2 = SpacecraftState(initialOrbit2)

# Propagator and integrator:
# propagate
span_sec = 2.0 * initialOrbit2.getKeplerianPeriod()
step_sec = 100.0

min_step = 0.01
max_step = step_sec / 2.0        # integrator, seconds
#minStep = maxStep = step_sec = 100.0
pos_err = 10.0
integrator = ClassicalRungeKuttaIntegratorBuilder(step_sec)
#integrator = DormandPrince853IntegratorBuilder(min_step, max_step, pos_err)

prop_builder = DSSTPropagatorBuilder(initialOrbit2,
                                     integrator,
                                     1.0,  # position 'scaling factor'. Typically standard dev of pos err
                                     PropagationType.MEAN,  # type OF THE ORBIT used for prop
                                     PropagationType.MEAN  # type of elements used to define orbital state
                                     )
prop_builder.setMass(200.0)
propagator2 = prop_builder.buildPropagator(prop_builder.getSelectedNormalizedParameters())

# Create maneuver trigger on mean anomaly, specified below:
angle = math.radians(0.0)
trigger = PositionAngleDetector(OrbitType.KEPLERIAN, PositionAngle.TRUE, angle)

# Create maneuver with trigger (in-track, +ve dir)
dv_dir = Vector3D.PLUS_I.scalarMultiply(100.0)
maneuver = ImpulseManeuver(trigger,
                           attitudeOverride,
                           dv_dir,  # delta-v dir, along x-axis (i,j,k)
                           500.0  # ISP - required but NOT used for impuslive.
                           )

# add maneuver to propagator
propagator2.addEventDetector(maneuver)

print(f"- Initial attributes:")
start_SMA = round(initialOrbit2.getA(), 3) / 1000.0
print(f"SMA, km: {start_SMA }")
print(f"RAAN: {round(math.degrees(initialOrbit2.getRightAscensionOfAscendingNode()),3)}")
print(f"Inc: {round(math.degrees(initialOrbit2.getI()),3)}")


# Now propagate one of two different ways, both using slave mode, but one with predefined steps, the other right
# to the end of time span:
stepwise = False    # toggle this to see the difference

if stepwise:    # propagate and print in defined steps
    print(f"Mean_Anom(deg), SMA (km)")
    for t in range(1, round(span_sec / step_sec)):
        state = propagator2.propagate(startDate.shiftedBy(t*step_sec))

        kep_orb = KeplerianOrbit(state.getOrbit())
        SMA = round(kep_orb.getA(), 2)
        mean_anom = round(math.degrees(kep_orb.getMeanAnomaly()), 3)
        print(f"{mean_anom}: {SMA / 1000.0}")
else:           # give the propagator the final end time
    state = propagator2.propagate(startDate.shiftedBy(span_sec))

finalOrbit = state.getOrbit()
finalOrb_Kep = KeplerianOrbit(finalOrbit)
final_SMA = round(finalOrbit.getA() / 1000, 5)
final_RAAN = round(math.degrees(finalOrb_Kep.getRightAscensionOfAscendingNode()), 5)
final_inc = round(math.degrees(finalOrb_Kep.getI()), 5)

print(f"\n- Final attributes:")
print(f"SMA, km: {final_SMA}")
print(f"RAAN: {final_RAAN}")
print(f"Inc: {final_inc}")
print(f"Delta SMA: {final_SMA - start_SMA}")

DSST propagator uses the step size for mean elements propagation and uses analytical computation for adding short periods. A consequence of this semi-analytical method is that as mean elements evolve very slowly, the step size for integrating them is several order of magnitude larger than step size for classical numerical integration. A step size that counts in seconds seems far too small for DSST. More classical values are several hours, or even one day!

This is completely different from numerical integration.

I can confirm this behavior is agnostic to propagator type between: DSST, EcksteinHechler, and Numerical.

Updated code attached to toggle between propagator types, along with previous toggle to proceed step-wise through time span or propagate directly to the end
manuevers_simple_trigger.py (6.1 KB)

Hi ShikaDing,

As mentioned in the other thread, you have to set the event handler to stop on all events to achieve the behavior you are looking for. Seems the default behavior is to stop on the second event. When you have a short propagation span (as you do) there is not enough time to reach the event a second time in the same propagation.

Regards,
Evan