Performance of numerical propagator with hundreds of finite maneuvers in sequence

I want to numerically propagate a low-thrust LEO satellite which executes hundreds of small burns. For concreteness, let each burn be a half-hour long, and let the time from the start of one burn to the next be an hour. Let the propagation duration be one month. Here is some sample code:

// Define scenario time.
final AbsoluteDate scenarioStartDate = new AbsoluteDate(2015, 01, 01, 00, 00, 00.000, OrekitGlobals.UTC);
final AbsoluteDate scenarioStopDate = new AbsoluteDate(2015, 02, 01, 00, 00, 00.000, OrekitGlobals.UTC);

// Define satellite parameters.
final double mass = 100.; // kg
final double specificImpulse = 1000.; // s
final double thrustForce = 0.01; // N

// Define satellite orbit.
final Orbit orbit = new KeplerianOrbit(7000000., 0.05, Math.toRadians(28.5), Math.toRadians(0.),
        Math.toRadians(0.), Math.toRadians(0.), PositionAngle.MEAN, FramesFactory.getGCRF(), scenarioStartDate, MU);
final SpacecraftState initialSpacecraftState = new SpacecraftState(orbit, mass);

// Define propagator.
final double[][] tolerances = NumericalPropagator.tolerances(1e-6, orbit, OrbitType.CARTESIAN);
final var integrator = new DormandPrince853Integrator(1., 4. * 60., tolerances[0], tolerances[1]);
final NumericalPropagator propagator = new NumericalPropagator(integrator);

// Configure propagator.
propagator.setOrbitType(OrbitType.CARTESIAN);
propagator.setInitialState(initialSpacecraftState);

// Add force models.
propagator.addForceModel(new NewtonianAttraction(MU));
for (int i = 0; i < 31 * 24; ++i) {
    final AbsoluteDate maneuverStartDate = scenarioStartDate.shiftedBy(3600. * i);
    final double maneuverDuration = 1800.;
    propagator.addForceModel(new ConstantThrustManeuver(maneuverStartDate, maneuverDuration, thrustForce,
            specificImpulse, Vector3D.PLUS_I));
}

// Propagate.
long tick = System.currentTimeMillis();
propagator.propagate(scenarioStopDate);
tick = System.currentTimeMillis() - tick;
System.out.println(tick);

This takes 60 seconds to execute on my machine. If I comment out the addition of the maneuver force models then it takes less than one second. In fact if you play around with the number of maneuvers you quickly find that the runtime depends linearly on the number of maneuvers. Presumably the issue is that the numerical integrator has to call each force model at each time step to obtain a force contribution, so at each time step there is a huge for loop.

The simplest way around this that I’ve been able to invent is to sequentially create a propagator for each maneuver, initializing each one with the propagation result of the last. However, I would really like to have a single propagator that incorporates all the maneuvers. I was not able to come up with a simple way, using the existing OreKit classes, to create a single ForceModel which incorporates all the maneuvers. I looked into using a Maneuver with a custom ManeuverTriggers implementation but I think this runs into the same problem because one of the methods on ManeuverTriggers is getEventsDetectors(). There is a DateBasedManeuverTriggers class for a single maneuver over a time interval which returns 2 event detectors from this method, one for the start of the time interval and one for the end of the time interval. I think I would have to return 2*n event detectors where n is the number of maneuvers–so I haven’t avoided the issue of lots of evaluations at every timestep. (Full disclosure: I haven’t actually pushed through to get a fully working implementation of what I’m describing here, but given the way the maneuvers package is architected I don’t think it’s going to work.)

Before I spend much more time on this I thought I’d ask the forum: is there a clean way to do what I want in OreKit which doesn’t suffer from performance issues?

I think you are on the right track with a custom ManeuverTriggers, which could inn fact be quite similar (but not identical) to DateBasedManeuverTriggers. In your implementation, you could set up the start and end detectors directly at construction and save them, and then by calling in a loop a custom method addFiring(startDate, endDate) you could add these dates to the two already existing detectors. This would work because DateDetector can handle multiple dates (see DateDetector.addEventDate(). You could even add the dates after the propagation have started (this feature is used for example when using DateDetector to implement countdowns, where an initial event starts a countdown during propagation, and a second event should be triggered after some time has elapsed).

You would then have only two detectors. They would handle many dates, but the underlying check is probably fast enough, has it has been designed to handle gracefully cases when the g function call evolve smoothly (like a propagation) by simply looking for the closest switch always starting the search from where last search stopped. So basically when your propagator is at orbit 2345, it looks for maneuvers dates that are around this time range first, and should find the closest one fast.

Note that as you could define the dates during the propagation, this means you can have dates that properly take into account the fact orbit changed due to the previous maneuvers. One way to do that is to have the event handler not only saving the last triggeredStart and triggeredEnd encountered, but also predicting when next maneuver should occur, i.e. add new dates to the detectors. With this setup, the dates are both computed and used on the fly during propagation.

Thanks for the advice! I’ll give your suggestion a shot and I’ll post on the forum if I’m successful.