PropagatorsParallelizer.java
/* Copyright 2002-2024 CS GROUP
* Licensed to CS GROUP (CS) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* CS licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.orekit.propagation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import org.hipparchus.exception.LocalizedCoreFormats;
import org.hipparchus.util.FastMath;
import org.orekit.errors.OrekitException;
import org.orekit.propagation.sampling.MultiSatFixedStepHandler;
import org.orekit.propagation.sampling.MultiSatStepHandler;
import org.orekit.propagation.sampling.MultisatStepNormalizer;
import org.orekit.propagation.sampling.OrekitStepHandler;
import org.orekit.propagation.sampling.OrekitStepInterpolator;
import org.orekit.propagation.sampling.StepHandlerMultiplexer;
import org.orekit.time.AbsoluteDate;
/** This class provides a way to propagate simultaneously several orbits.
*
* <p>
* Multi-satellites propagation is based on multi-threading. Therefore,
* care must be taken so that all propagators can be run in a multi-thread
* context. This implies that all propagators are built independently and
* that they rely on force models that are also built independently. An
* obvious mistake would be to reuse a maneuver force model, as these models
* need to cache the firing/not-firing status. Objects used by force models
* like atmosphere models for drag force or others may also cache intermediate
* variables, so separate instances for each propagator must be set up.
* </p>
* <p>
* This class <em>will</em> create new threads for running the propagators.
* It adds a new {@link MultiSatStepHandler global step handler} to manage
* the steps all at once, in addition to the existing individual step
* handlers that are preserved.
* </p>
* <p>
* All propagators remain independent of each other (they don't even know
* they are managed by the parallelizer) and advance their simulation
* time following their own algorithm. The parallelizer will block them
* at the end of each step and allow them to continue in order to maintain
* synchronization. The {@link MultiSatStepHandler global handler} will
* experience perfectly synchronized steps, but some propagators may already
* be slightly ahead of time as depicted in the following rendering; were
* simulation times flows from left to right:
* </p>
* <pre>
* propagator 1 : -------------[++++current step++++]>
* |
* propagator 2 : ----[++++current step++++]--------->
* | |
* ... | |
* propagator n : ---------[++++current step++++]---->
* | |
* V V
* global handler : -------------[global step]--------->
* </pre>
* <p>
* The previous sketch shows that propagator 1 has already computed states
* up to the end of the propagation, but propagators 2 up to n are still late.
* The global step seen by the handler will be the common part between all
* propagators steps. Once this global step has been handled, the parallelizer
* will let the more late propagator (here propagator 2) to go one step further
* and a new global step will be computed and handled, until all propagators
* reach the end.
* </p>
* <p>
* This class does <em>not</em> provide multi-satellite events. As events
* may truncate steps and even reset state, all events (including multi-satellite
* events) are handled at a very low level within each propagators and cannot be
* managed from outside by the parallelizer. For accurate handling of multi-satellite
* events, the event detector should be registered <em>within</em> the propagator
* of one satellite and have access to an independent propagator (typically an
* analytical propagator or an ephemeris) of the other satellite. As the embedded
* propagator will be called by the detector which itself is called by the first
* propagator, it should really be a dedicated propagator and should not also
* appear as one of the parallelized propagators, otherwise conflicts will appear here.
* </p>
* @author Luc Maisonobe
* @since 9.0
*/
public class PropagatorsParallelizer {
/** Waiting time to avoid getting stuck waiting for interrupted threads (ms). */
private static long MAX_WAIT = 10;
/** Underlying propagators. */
private final List<Propagator> propagators;
/** Global step handler. */
private final MultiSatStepHandler globalHandler;
/** Simple constructor.
* @param propagators list of propagators to use
* @param globalHandler global handler for managing all spacecrafts
* simultaneously
*/
public PropagatorsParallelizer(final List<Propagator> propagators,
final MultiSatStepHandler globalHandler) {
this.propagators = propagators;
this.globalHandler = globalHandler;
}
/** Simple constructor.
* @param propagators list of propagators to use
* @param h fixed time step (sign is not used)
* @param globalHandler global handler for managing all spacecrafts
* simultaneously
* @since 12.0
*/
public PropagatorsParallelizer(final List<Propagator> propagators,
final double h,
final MultiSatFixedStepHandler globalHandler) {
this.propagators = propagators;
this.globalHandler = new MultisatStepNormalizer(h, globalHandler);
}
/** Get an unmodifiable list of the underlying mono-satellite propagators.
* @return unmodifiable list of the underlying mono-satellite propagators
*/
public List<Propagator> getPropagators() {
return Collections.unmodifiableList(propagators);
}
/** Propagate from a start date towards a target date.
* @param start start date from which orbit state should be propagated
* @param target target date to which orbit state should be propagated
* @return propagated states
*/
public List<SpacecraftState> propagate(final AbsoluteDate start, final AbsoluteDate target) {
if (propagators.size() == 1) {
// special handling when only one propagator is used
propagators.get(0).getMultiplexer().add(new SinglePropagatorHandler(globalHandler));
return Collections.singletonList(propagators.get(0).propagate(start, target));
}
final double sign = FastMath.copySign(1.0, target.durationFrom(start));
// start all propagators in concurrent threads
final ExecutorService executorService = Executors.newFixedThreadPool(propagators.size());
final List<PropagatorMonitoring> monitors = new ArrayList<>(propagators.size());
for (final Propagator propagator : propagators) {
final PropagatorMonitoring monitor = new PropagatorMonitoring(propagator, start, target, executorService);
monitor.waitFirstStepCompletion();
monitors.add(monitor);
}
// main loop
AbsoluteDate previousDate = start;
final List<SpacecraftState> initialStates = new ArrayList<>(monitors.size());
for (final PropagatorMonitoring monitor : monitors) {
initialStates.add(monitor.parameters.initialState);
}
globalHandler.init(initialStates, target);
for (boolean isLast = false; !isLast;) {
// select the earliest ending propagator, according to propagation direction
PropagatorMonitoring selected = null;
AbsoluteDate selectedStepEnd = null;
for (PropagatorMonitoring monitor : monitors) {
final AbsoluteDate stepEnd = monitor.parameters.interpolator.getCurrentState().getDate();
if (selected == null || sign * selectedStepEnd.durationFrom(stepEnd) > 0) {
selected = monitor;
selectedStepEnd = stepEnd;
}
}
// restrict steps to a common time range
for (PropagatorMonitoring monitor : monitors) {
final OrekitStepInterpolator interpolator = monitor.parameters.interpolator;
final SpacecraftState previousState = interpolator.getInterpolatedState(previousDate);
final SpacecraftState currentState = interpolator.getInterpolatedState(selectedStepEnd);
monitor.restricted = interpolator.restrictStep(previousState, currentState);
}
// handle all states at once
final List<OrekitStepInterpolator> interpolators = new ArrayList<>(monitors.size());
for (final PropagatorMonitoring monitor : monitors) {
interpolators.add(monitor.restricted);
}
globalHandler.handleStep(interpolators);
if (selected.parameters.finalState == null) {
// step handler can still provide new results
// this will wait until either handleStep or finish are called
selected.retrieveNextParameters();
} else {
// this was the last step
isLast = true;
/* For NumericalPropagators :
* After reaching the finalState with the selected monitor,
* we need to do the step with all remaining monitors to reach the target time.
* This also triggers the StoringStepHandler, producing ephemeris.
*/
for (PropagatorMonitoring monitor : monitors) {
if (monitor != selected) {
monitor.retrieveNextParameters();
}
}
}
previousDate = selectedStepEnd;
}
// stop all remaining propagators
executorService.shutdownNow();
// extract the final states
final List<SpacecraftState> finalStates = new ArrayList<>(monitors.size());
for (PropagatorMonitoring monitor : monitors) {
try {
finalStates.add(monitor.future.get());
} catch (InterruptedException | ExecutionException e) {
// sort out if exception was intentional or not
monitor.manageException(e);
// this propagator was intentionally stopped,
// we retrieve the final state from the last available interpolator
finalStates.add(monitor.parameters.interpolator.getInterpolatedState(previousDate));
}
}
globalHandler.finish(finalStates);
return finalStates;
}
/** Local exception to stop propagators. */
private static class PropagatorStoppingException extends OrekitException {
/** Serializable UID.*/
private static final long serialVersionUID = 20170629L;
/** Simple constructor.
* @param ie interruption exception
*/
PropagatorStoppingException(final InterruptedException ie) {
super(ie, LocalizedCoreFormats.SIMPLE_MESSAGE, ie.getLocalizedMessage());
}
}
/** Local class for handling single propagator steps. */
private static class SinglePropagatorHandler implements OrekitStepHandler {
/** Global handler. */
private final MultiSatStepHandler globalHandler;
/** Simple constructor.
* @param globalHandler global handler to call
*/
SinglePropagatorHandler(final MultiSatStepHandler globalHandler) {
this.globalHandler = globalHandler;
}
/** {@inheritDoc} */
@Override
public void init(final SpacecraftState s0, final AbsoluteDate t) {
globalHandler.init(Collections.singletonList(s0), t);
}
/** {@inheritDoc} */
@Override
public void handleStep(final OrekitStepInterpolator interpolator) {
globalHandler.handleStep(Collections.singletonList(interpolator));
}
/** {@inheritDoc} */
@Override
public void finish(final SpacecraftState finalState) {
globalHandler.finish(Collections.singletonList(finalState));
}
}
/** Local class for handling multiple propagator steps. */
private static class MultiplePropagatorsHandler implements OrekitStepHandler {
/** Previous container handed off. */
private ParametersContainer previous;
/** Queue for passing step handling parameters. */
private final SynchronousQueue<ParametersContainer> queue;
/** Simple constructor.
* @param queue queue for passing step handling parameters
*/
MultiplePropagatorsHandler(final SynchronousQueue<ParametersContainer> queue) {
this.previous = new ParametersContainer(null, null, null);
this.queue = queue;
}
/** Hand off container to parallelizer.
* @param container parameters container to hand-off
*/
private void handOff(final ParametersContainer container) {
try {
previous = container;
queue.put(previous);
} catch (InterruptedException ie) {
// use a dedicated exception to stop thread almost gracefully
throw new PropagatorStoppingException(ie);
}
}
/** {@inheritDoc} */
@Override
public void init(final SpacecraftState s0, final AbsoluteDate t) {
handOff(new ParametersContainer(s0, null, null));
}
/** {@inheritDoc} */
@Override
public void handleStep(final OrekitStepInterpolator interpolator) {
handOff(new ParametersContainer(previous.initialState, interpolator, null));
}
/** {@inheritDoc} */
@Override
public void finish(final SpacecraftState finalState) {
handOff(new ParametersContainer(previous.initialState, previous.interpolator, finalState));
}
}
/** Container for parameters passed by propagators to step handlers. */
private static class ParametersContainer {
/** Initial state. */
private final SpacecraftState initialState;
/** Interpolator set up for last seen step. */
private final OrekitStepInterpolator interpolator;
/** Final state. */
private final SpacecraftState finalState;
/** Simple constructor.
* @param initialState initial state
* @param interpolator interpolator set up for last seen step
* @param finalState final state
*/
ParametersContainer(final SpacecraftState initialState,
final OrekitStepInterpolator interpolator,
final SpacecraftState finalState) {
this.initialState = initialState;
this.interpolator = interpolator;
this.finalState = finalState;
}
}
/** Container for propagator monitoring. */
private static class PropagatorMonitoring {
/** Queue for handing off step handler parameters. */
private final SynchronousQueue<ParametersContainer> queue;
/** Future for retrieving propagation return value. */
private final Future<SpacecraftState> future;
/** Last step handler parameters received. */
private ParametersContainer parameters;
/** Interpolator restricted to time range shared with other propagators. */
private OrekitStepInterpolator restricted;
/** Simple constructor.
* @param propagator managed propagator
* @param start start date from which orbit state should be propagated
* @param target target date to which orbit state should be propagated
* @param executorService service for running propagator
*/
PropagatorMonitoring(final Propagator propagator, final AbsoluteDate start, final AbsoluteDate target,
final ExecutorService executorService) {
// set up queue for handing off step handler parameters synchronization
// the main thread will let underlying propagators go forward
// by consuming the step handling parameters they will put at each step
queue = new SynchronousQueue<>();
// Remove former instances of "MultiplePropagatorsHandler" from step handlers multiplexer
clearMultiplePropagatorsHandler(propagator);
// Add MultiplePropagatorsHandler step handler
propagator.getMultiplexer().add(new MultiplePropagatorsHandler(queue));
// start the propagator
future = executorService.submit(() -> propagator.propagate(start, target));
}
/** Wait completion of first step.
*/
public void waitFirstStepCompletion() {
// wait until both the init method and the handleStep method
// of the current propagator step handler have been called,
// thus ensuring we have one step available to compare propagators
// progress with each other
while (parameters == null || parameters.initialState == null || parameters.interpolator == null) {
retrieveNextParameters();
}
}
/** Retrieve next step handling parameters.
*/
public void retrieveNextParameters() {
try {
ParametersContainer params = null;
while (params == null && !future.isDone()) {
params = queue.poll(MAX_WAIT, TimeUnit.MILLISECONDS);
// Check to avoid loop on future not done, in the case of reached finalState.
if (parameters != null) {
if (parameters.finalState != null) {
break;
}
}
}
if (params == null) {
// call Future.get just for the side effect of retrieving the exception
// in case the propagator ended due to an exception
future.get();
}
parameters = params;
} catch (InterruptedException | ExecutionException e) {
manageException(e);
parameters = null;
}
}
/** Convert exceptions.
* @param exception exception caught
*/
private void manageException(final Exception exception) {
if (exception.getCause() instanceof PropagatorStoppingException) {
// this was an expected exception, we deliberately shut down the propagators
// we therefore explicitly ignore this exception
return;
} else if (exception.getCause() instanceof OrekitException) {
// unwrap the original exception
throw (OrekitException) exception.getCause();
} else {
throw new OrekitException(exception.getCause(),
LocalizedCoreFormats.SIMPLE_MESSAGE, exception.getLocalizedMessage());
}
}
/** Clear existing instances of MultiplePropagatorsHandler in a monitored propagator.
* <p>
* Removes former instances of "MultiplePropagatorsHandler" from step handlers multiplexer.
* <p>
* This is done to avoid propagation getting stuck after several calls to PropagatorsParallelizer.propagate(...)
* <p>
* See issue <a href="https://gitlab.orekit.org/orekit/orekit/-/issues/1105">1105</a>.
* @param propagator monitored propagator whose MultiplePropagatorsHandlers must be cleared
*/
private void clearMultiplePropagatorsHandler(final Propagator propagator) {
// First, list instances of MultiplePropagatorsHandler in the propagator multiplexer
final StepHandlerMultiplexer multiplexer = propagator.getMultiplexer();
final List<OrekitStepHandler> existingMultiplePropagatorsHandler = new ArrayList<>();
for (final OrekitStepHandler handler : multiplexer.getHandlers()) {
if (handler instanceof MultiplePropagatorsHandler) {
existingMultiplePropagatorsHandler.add(handler);
}
}
// Then, clear all MultiplePropagatorsHandler instances from multiplexer.
// This is done in two steps because method "StepHandlerMultiplexer.remove(...)" already loops on the OrekitStepHandlers,
// leading to a ConcurrentModificationException if attempting to do everything in a single loop
for (final OrekitStepHandler handler : existingMultiplePropagatorsHandler) {
multiplexer.remove(handler);
}
}
}
}