Record (monitor) s/c state for each time step that a propagator takes

Hello!

Preface: I’m using Java Orekit v12.2 and (of course) Hipparchus v3.1.

I’d like capture the SpacecraftState at each of the steps that a NumericalPropagator takes as it propagates to an end date.

TL;DR: Is there a way to do this already, or something better than my very hacky EventHandler?

Details:

I figured I could write a stateful EventHandler with ::g that always returns 0 to tell the propagator that every step is an event, and save the state in a NavigableSet for each event.

Unfortunately, this causes an infinite loop in reinitializeBegin where the loop tries to perturb g, but g is constant so it can’t etc. etc… Even hacking past the infinite loop with a single non-zero value doesn’t work.

So, I’ve tried a few hacks, and the only thing that mostly works reliably, is to trick findRoot by having g start at -1 and both get smaller and flip its sign every time g() is called (code pasted at the bottom for the curious).

Like I said, this mostly works - NumProp and analytic - but it’s really rather hacky, and it’s not entirely reliable (e.g., findRoot can fail).

Click to show StepCaptureDetector attempt
/** An {@link EventDetector} to capture all {@link SpacecraftState states} that a {@link Propagator} computes as it
 * steps in time.
 * 
 * @author Ryan Moser */
@NotThreadSafe
public class StepCaptureDetector implements EventDetector {
    
    /** the {@link SpacecraftState} {@link Consumer} */
    private final Consumer<SpacecraftState> scConsumer;
    
    /** an {@link AdaptableInterval} that wraps the max step size */
    private final AdaptableInterval adptIterval;
    
    /** the value to return in {@link #g(SpacecraftState)} */
    private double g;
    
    /** Constructor
     * 
     * @param scConsumer the {@link Consumer} that handles the {@link SpacecraftState} at each step. Note: if this
     *        {@link Consumer} is stateful - e.g., if updating a {@link Collection} or {@link Map} - the steps are not
     *        necessarily going to be temporally ordered, so consider using a {@link NavigableSet} or
     *        {@link NavigableMap} such as {@link TreeSet} or {@link TreeMap}.
     * 
     * @param maxStep the max step to allow. If used with a {@link NumericalPropagator}, whichever is smaller between
     *        this and the next adaptable step will be used. */
    public StepCaptureDetector(final Consumer<SpacecraftState> scConsumer, final double maxStep) {
        this.scConsumer  = requireNonNull(scConsumer, "scConsumer must be specified");
        this.adptIterval = AdaptableInterval.of(maxStep);
        this.g           = -1.0; // this will be set to -1.0 when the propagator calls "init", but set it here anyway
    }
    
    @Override
    public void init(final SpacecraftState ignored1, final AbsoluteDate ignored2) {
        this.g = -1.0;
    }
    
    @Override
    public double g(final SpacecraftState ignored) {
        this.g *= -Numerics.SQUARE_ROOT_MACHINE_EPSILON;
        /* we need the root finding algorithm to think that there's a root and that g is not constant, so -
         * unfortunately - we can't simply return 0.0, AND we need the sign to flip. This value has worked well so
         * far. */
        
        return this.g;
    }
    
    @Override
    public double getThreshold() {
        return Numerics.SQUARE_ROOT_MACHINE_EPSILON;
    }
    
    @Override
    public AdaptableInterval getMaxCheckInterval() {
        return this.adptIterval;
    }
    
    @Override
    public int getMaxIterationCount() {
        return 10;
    }
    
    @Override
    public EventHandler getHandler() {
        return new EventHandler() {
            
            @Override
            public Action eventOccurred(final SpacecraftState state,
                                        final EventDetector   ignored, // not needed
                                        final boolean         increasing) {
                
                if (increasing) { /* due to the internal workings of Orekit and the contrived "g" we have here, we need
                                   * to skip every other step, otherwise we double up. If this was a "real"
                                   * EventDetector - i.e., one with a less problematic "g" - this wouldn't be a
                                   * problem. */
                    StepCaptureDetector.this.scConsumer.accept(state); // capture the state
                }
                
                StepCaptureDetector.this.init(null, null); // reset
                
                return Action.CONTINUE;
            }
        };
    }
}

Ugh I figured it out almost immediately after asking.

The key is to use a OrekitStepHandler and add it to the propagator’s multiplexer (prior to now, I’ve always stayed away from that).

/** An {@link OrekitStepHandler} to capture all {@link SpacecraftState states} that a {@link Propagator} computes as it
 * steps in time.
 * 
 * @author Ryan Moser */
public class StepCaptureHandler implements OrekitStepHandler {
    
    /** the {@link SpacecraftState} {@link Consumer} */
    private final Consumer<SpacecraftState> scConsumer;
    
    /** Constructor
     * 
     * @param scConsumer the {@link Consumer} that handles the {@link SpacecraftState} at each step. Note: if this
     *        {@link Consumer} is stateful - e.g., if updating a {@link Collection} or {@link Map} - the steps are not
     *        necessarily going to be temporally ordered, so consider using a {@link NavigableSet} or
     *        {@link NavigableMap} such as {@link TreeSet} or {@link TreeMap}. */
    public StepCaptureHandler(final Consumer<SpacecraftState> scConsumer) {
        this.scConsumer  = requireNonNull(scConsumer, "scConsumer must be specified");
    }
    
    @Override
    public void handleStep(final OrekitStepInterpolator interpolator) {
        this.scConsumer.accept(interpolator.getCurrentState());
    }
}

You can also call getEphemerisGenerator from any propagator before starting propagation, then propagate through final time, and then call the getGeneratedEphemeris from the ephemeris generator.
Under the hood, the generator for numerical propagator does the same thing you did: it stores the step interpolators. For analytical propagators, it is smarter, it keeps a reference to the analytical propagator and call it later when needed, so it doesn’t waste too much memory.

1 Like

Hi,

On the develop branch I added a PropagationStepRecorder that does that. It’s an OrekitStepHandler.

Cheers,
Romain.

1 Like

Ah, looking through, this will do the interpolation I thought I was going to have to do!

If I’ve got this worked out properly in the case of the NumericaPropagator

I can query the generated ephemeris for some time withing range, and IntegratedEphemeris::propagate computes an interpolated ODEStateAndDerivative that’s mapped to a SpacecraftState.

And then the analytic prop.s simply reference the original (which makes sense).

I suppose it depends on the ForceModels in play, but does the interpolation generally “stick to the physics” when compared to deliberate propagation with smaller steps?

Sweet, thanks!

The interpolation is provided by the integrator itself, so it is consistent with both the force models and the order of the integrator. Also since the interpolators are provided for each step, they are consistent with step boundaries, meaning for example they obey RESET_STATE and RESET_DERIVATIVES events that change the dynamics (typically at maneuvers start/end or at eclipses entry/exit).

1 Like