1   /* Copyright 2002-2025 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.propagation;
18  
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  import java.util.concurrent.ExecutionException;
23  import java.util.concurrent.ExecutorService;
24  import java.util.concurrent.Executors;
25  import java.util.concurrent.Future;
26  import java.util.concurrent.SynchronousQueue;
27  import java.util.concurrent.TimeUnit;
28  
29  import org.hipparchus.exception.LocalizedCoreFormats;
30  import org.hipparchus.util.FastMath;
31  import org.orekit.errors.OrekitException;
32  import org.orekit.propagation.sampling.MultiSatFixedStepHandler;
33  import org.orekit.propagation.sampling.MultiSatStepHandler;
34  import org.orekit.propagation.sampling.MultisatStepNormalizer;
35  import org.orekit.propagation.sampling.OrekitStepHandler;
36  import org.orekit.propagation.sampling.OrekitStepInterpolator;
37  import org.orekit.propagation.sampling.StepHandlerMultiplexer;
38  import org.orekit.time.AbsoluteDate;
39  
40  /** This class provides a way to propagate simultaneously several orbits.
41   *
42   * <p>
43   * Multi-satellites propagation is based on multi-threading. Therefore,
44   * care must be taken so that all propagators can be run in a multi-thread
45   * context. This implies that all propagators are built independently and
46   * that they rely on force models that are also built independently. An
47   * obvious mistake would be to reuse a maneuver force model, as these models
48   * need to cache the firing/not-firing status. Objects used by force models
49   * like atmosphere models for drag force or others may also cache intermediate
50   * variables, so separate instances for each propagator must be set up.
51   * </p>
52   * <p>
53   * This class <em>will</em> create new threads for running the propagators.
54   * It adds a new {@link MultiSatStepHandler global step handler} to manage
55   * the steps all at once, in addition to the existing individual step
56   * handlers that are preserved.
57   * </p>
58   * <p>
59   * All propagators remain independent of each other (they don't even know
60   * they are managed by the parallelizer) and advance their simulation
61   * time following their own algorithm. The parallelizer will block them
62   * at the end of each step and allow them to continue in order to maintain
63   * synchronization. The {@link MultiSatStepHandler global handler} will
64   * experience perfectly synchronized steps, but some propagators may already
65   * be slightly ahead of time as depicted in the following rendering; were
66   * simulation times flows from left to right:
67   * </p>
68   * <pre>
69   *    propagator 1   : -------------[++++current step++++]&gt;
70   *                                  |
71   *    propagator 2   : ----[++++current step++++]---------&gt;
72   *                                  |           |
73   *    ...                           |           |
74   *    propagator n   : ---------[++++current step++++]----&gt;
75   *                                  |           |
76   *                                  V           V
77   *    global handler : -------------[global step]---------&gt;
78   * </pre>
79   * <p>
80   * The previous sketch shows that propagator 1 has already computed states
81   * up to the end of the propagation, but propagators 2 up to n are still late.
82   * The global step seen by the handler will be the common part between all
83   * propagators steps. Once this global step has been handled, the parallelizer
84   * will let the more late propagator (here propagator 2) to go one step further
85   * and a new global step will be computed and handled, until all propagators
86   * reach the end.
87   * </p>
88   * <p>
89   * This class does <em>not</em> provide multi-satellite events. As events
90   * may truncate steps and even reset state, all events (including multi-satellite
91   * events) are handled at a very low level within each propagators and cannot be
92   * managed from outside by the parallelizer. For accurate handling of multi-satellite
93   * events, the event detector should be registered <em>within</em> the propagator
94   * of one satellite and have access to an independent propagator (typically an
95   * analytical propagator or an ephemeris) of the other satellite. As the embedded
96   * propagator will be called by the detector which itself is called by the first
97   * propagator, it should really be a dedicated propagator and should not also
98   * appear as one of the parallelized propagators, otherwise conflicts will appear here.
99   * </p>
100  * @author Luc Maisonobe
101  * @since 9.0
102  */
103 
104 public class PropagatorsParallelizer {
105 
106     /** Waiting time to avoid getting stuck waiting for interrupted threads (ms). */
107     private static long MAX_WAIT = 10;
108 
109     /** Underlying propagators. */
110     private final List<Propagator> propagators;
111 
112     /** Global step handler. */
113     private final MultiSatStepHandler globalHandler;
114 
115     /** Simple constructor.
116      * @param propagators list of propagators to use
117      * @param globalHandler global handler for managing all spacecrafts
118      * simultaneously
119      */
120     public PropagatorsParallelizer(final List<Propagator> propagators,
121                                    final MultiSatStepHandler globalHandler) {
122         this.propagators = propagators;
123         this.globalHandler = globalHandler;
124     }
125 
126     /** Simple constructor.
127      * @param propagators list of propagators to use
128      * @param h fixed time step (sign is not used)
129      * @param globalHandler global handler for managing all spacecrafts
130      * simultaneously
131      * @since 12.0
132      */
133     public PropagatorsParallelizer(final List<Propagator> propagators,
134                                    final double h,
135                                    final MultiSatFixedStepHandler globalHandler) {
136         this.propagators   = propagators;
137         this.globalHandler = new MultisatStepNormalizer(h, globalHandler);
138     }
139 
140     /** Get an unmodifiable list of the underlying mono-satellite propagators.
141      * @return unmodifiable list of the underlying mono-satellite propagators
142      */
143     public List<Propagator> getPropagators() {
144         return Collections.unmodifiableList(propagators);
145     }
146 
147     /** Propagate from a start date towards a target date.
148      * @param start start date from which orbit state should be propagated
149      * @param target target date to which orbit state should be propagated
150      * @return propagated states
151      */
152     public List<SpacecraftState> propagate(final AbsoluteDate start, final AbsoluteDate target) {
153 
154         if (propagators.size() == 1) {
155             // special handling when only one propagator is used
156             propagators.get(0).getMultiplexer().add(new SinglePropagatorHandler(globalHandler));
157             return Collections.singletonList(propagators.get(0).propagate(start, target));
158         }
159 
160         final double sign = FastMath.copySign(1.0, target.durationFrom(start));
161 
162         // start all propagators in concurrent threads
163         final ExecutorService            executorService = Executors.newFixedThreadPool(propagators.size());
164         final List<PropagatorMonitoring> monitors        = new ArrayList<>(propagators.size());
165         for (final Propagator propagator : propagators) {
166             final PropagatorMonitoring monitor = new PropagatorMonitoring(propagator, start, target, executorService);
167             monitor.waitFirstStepCompletion();
168             monitors.add(monitor);
169         }
170 
171         // main loop
172         AbsoluteDate previousDate = start;
173         final List<SpacecraftState> initialStates = new ArrayList<>(monitors.size());
174         for (final PropagatorMonitoring monitor : monitors) {
175             initialStates.add(monitor.parameters.initialState);
176         }
177         globalHandler.init(initialStates, target);
178         for (boolean isLast = false; !isLast;) {
179 
180             // select the earliest ending propagator, according to propagation direction
181             PropagatorMonitoring selected = null;
182             AbsoluteDate selectedStepEnd  = null;
183             for (PropagatorMonitoring monitor : monitors) {
184                 final AbsoluteDate stepEnd = monitor.parameters.interpolator.getCurrentState().getDate();
185                 if (selected == null || sign * selectedStepEnd.durationFrom(stepEnd) > 0) {
186                     selected        = monitor;
187                     selectedStepEnd = stepEnd;
188                 }
189             }
190 
191             // restrict steps to a common time range
192             for (PropagatorMonitoring monitor : monitors) {
193                 final OrekitStepInterpolator interpolator  = monitor.parameters.interpolator;
194                 final SpacecraftState        previousState = interpolator.getInterpolatedState(previousDate);
195                 final SpacecraftState        currentState  = interpolator.getInterpolatedState(selectedStepEnd);
196                 monitor.restricted                         = interpolator.restrictStep(previousState, currentState);
197             }
198 
199             // handle all states at once
200             final List<OrekitStepInterpolator> interpolators = new ArrayList<>(monitors.size());
201             for (final PropagatorMonitoring monitor : monitors) {
202                 interpolators.add(monitor.restricted);
203             }
204             globalHandler.handleStep(interpolators);
205 
206             if (selected.parameters.finalState == null) {
207                 // step handler can still provide new results
208                 // this will wait until either handleStep or finish are called
209                 selected.retrieveNextParameters();
210             } else {
211                 // this was the last step
212                 isLast = true;
213                 /* For NumericalPropagators :
214                  * After reaching the finalState with the selected monitor,
215                  * we need to do the step with all remaining monitors to reach the target time.
216                  * This also triggers the StoringStepHandler, producing ephemeris.
217                  */
218                 for (PropagatorMonitoring monitor : monitors) {
219                     if (monitor != selected) {
220                         monitor.retrieveNextParameters();
221                     }
222                 }
223             }
224 
225             previousDate = selectedStepEnd;
226 
227         }
228 
229         // stop all remaining propagators
230         executorService.shutdownNow();
231 
232         // extract the final states
233         final List<SpacecraftState> finalStates = new ArrayList<>(monitors.size());
234         for (PropagatorMonitoring monitor : monitors) {
235             try {
236                 finalStates.add(monitor.future.get());
237             } catch (InterruptedException | ExecutionException e) {
238 
239                 // sort out if exception was intentional or not
240                 monitor.manageException(e);
241 
242                 // this propagator was intentionally stopped,
243                 // we retrieve the final state from the last available interpolator
244                 finalStates.add(monitor.parameters.interpolator.getInterpolatedState(previousDate));
245 
246             }
247         }
248 
249         globalHandler.finish(finalStates);
250 
251         return finalStates;
252 
253     }
254 
255     /** Local exception to stop propagators. */
256     private static class PropagatorStoppingException extends OrekitException {
257 
258         /** Serializable UID.*/
259         private static final long serialVersionUID = 20170629L;
260 
261         /** Simple constructor.
262          * @param ie interruption exception
263          */
264         PropagatorStoppingException(final InterruptedException ie) {
265             super(ie, LocalizedCoreFormats.SIMPLE_MESSAGE, ie.getLocalizedMessage());
266         }
267 
268     }
269 
270     /** Local class for handling single propagator steps. */
271     private static class SinglePropagatorHandler implements OrekitStepHandler {
272 
273         /** Global handler. */
274         private final MultiSatStepHandler globalHandler;
275 
276         /** Simple constructor.
277          * @param globalHandler global handler to call
278          */
279         SinglePropagatorHandler(final MultiSatStepHandler globalHandler) {
280             this.globalHandler = globalHandler;
281         }
282 
283 
284         /** {@inheritDoc} */
285         @Override
286         public void init(final SpacecraftState s0, final AbsoluteDate t) {
287             globalHandler.init(Collections.singletonList(s0), t);
288         }
289 
290         /** {@inheritDoc} */
291         @Override
292         public void handleStep(final OrekitStepInterpolator interpolator) {
293             globalHandler.handleStep(Collections.singletonList(interpolator));
294         }
295 
296         /** {@inheritDoc} */
297         @Override
298         public void finish(final SpacecraftState finalState) {
299             globalHandler.finish(Collections.singletonList(finalState));
300         }
301 
302     }
303 
304     /** Local class for handling multiple propagator steps. */
305     private static class MultiplePropagatorsHandler implements OrekitStepHandler {
306 
307         /** Previous container handed off. */
308         private ParametersContainer previous;
309 
310         /** Queue for passing step handling parameters. */
311         private final SynchronousQueue<ParametersContainer> queue;
312 
313         /** Simple constructor.
314          * @param queue queue for passing step handling parameters
315          */
316         MultiplePropagatorsHandler(final SynchronousQueue<ParametersContainer> queue) {
317             this.previous = new ParametersContainer(null, null, null);
318             this.queue    = queue;
319         }
320 
321         /** Hand off container to parallelizer.
322          * @param container parameters container to hand-off
323          */
324         private void handOff(final ParametersContainer container) {
325             try {
326                 previous = container;
327                 queue.put(previous);
328             } catch (InterruptedException ie) {
329                 // use a dedicated exception to stop thread almost gracefully
330                 throw new PropagatorStoppingException(ie);
331             }
332         }
333 
334         /** {@inheritDoc} */
335         @Override
336         public void init(final SpacecraftState s0, final AbsoluteDate t) {
337             handOff(new ParametersContainer(s0, null, null));
338         }
339 
340         /** {@inheritDoc} */
341         @Override
342         public void handleStep(final OrekitStepInterpolator interpolator) {
343             handOff(new ParametersContainer(previous.initialState, interpolator, null));
344         }
345 
346         /** {@inheritDoc} */
347         @Override
348         public void finish(final SpacecraftState finalState) {
349             handOff(new ParametersContainer(previous.initialState, previous.interpolator, finalState));
350         }
351 
352     }
353 
354     /** Container for parameters passed by propagators to step handlers. */
355     private static class ParametersContainer {
356 
357         /** Initial state. */
358         private final SpacecraftState initialState;
359 
360         /** Interpolator set up for last seen step. */
361         private final OrekitStepInterpolator interpolator;
362 
363         /** Final state. */
364         private final SpacecraftState finalState;
365 
366         /** Simple constructor.
367          * @param initialState initial state
368          * @param interpolator interpolator set up for last seen step
369          * @param finalState final state
370          */
371         ParametersContainer(final SpacecraftState initialState,
372                             final OrekitStepInterpolator interpolator,
373                             final SpacecraftState finalState) {
374             this.initialState = initialState;
375             this.interpolator = interpolator;
376             this.finalState   = finalState;
377         }
378 
379     }
380 
381     /** Container for propagator monitoring. */
382     private static class PropagatorMonitoring {
383 
384         /** Queue for handing off step handler parameters. */
385         private final SynchronousQueue<ParametersContainer> queue;
386 
387         /** Future for retrieving propagation return value. */
388         private final Future<SpacecraftState> future;
389 
390         /** Last step handler parameters received. */
391         private ParametersContainer parameters;
392 
393         /** Interpolator restricted to time range shared with other propagators. */
394         private OrekitStepInterpolator restricted;
395 
396         /** Simple constructor.
397          * @param propagator managed propagator
398          * @param start start date from which orbit state should be propagated
399          * @param target target date to which orbit state should be propagated
400          * @param executorService service for running propagator
401          */
402         PropagatorMonitoring(final Propagator propagator, final AbsoluteDate start, final AbsoluteDate target,
403                              final ExecutorService executorService) {
404 
405             // set up queue for handing off step handler parameters synchronization
406             // the main thread will let underlying propagators go forward
407             // by consuming the step handling parameters they will put at each step
408             queue = new SynchronousQueue<>();
409 
410             // Remove former instances of "MultiplePropagatorsHandler" from step handlers multiplexer
411             clearMultiplePropagatorsHandler(propagator);
412 
413             // Add MultiplePropagatorsHandler step handler
414             propagator.getMultiplexer().add(new MultiplePropagatorsHandler(queue));
415 
416             // start the propagator
417             future = executorService.submit(() -> propagator.propagate(start, target));
418 
419         }
420 
421         /** Wait completion of first step.
422          */
423         public void waitFirstStepCompletion() {
424 
425             // wait until both the init method and the handleStep method
426             // of the current propagator step handler have been called,
427             // thus ensuring we have one step available to compare propagators
428             // progress with each other
429             while (parameters == null || parameters.initialState == null || parameters.interpolator == null) {
430                 retrieveNextParameters();
431             }
432 
433         }
434 
435         /** Retrieve next step handling parameters.
436          */
437         public void retrieveNextParameters() {
438             try {
439                 ParametersContainer params = null;
440                 while (params == null && !future.isDone()) {
441                     params = queue.poll(MAX_WAIT, TimeUnit.MILLISECONDS);
442                     // Check to avoid loop on future not done, in the case of reached finalState.
443                     if (parameters != null) {
444                         if (parameters.finalState != null) {
445                             break;
446                         }
447                     }
448                 }
449                 if (params == null) {
450                     // call Future.get just for the side effect of retrieving the exception
451                     // in case the propagator ended due to an exception
452                     future.get();
453                 }
454                 parameters = params;
455             } catch (InterruptedException | ExecutionException e) {
456                 manageException(e);
457                 parameters = null;
458             }
459         }
460 
461         /** Convert exceptions.
462          * @param exception exception caught
463          */
464         private void manageException(final Exception exception) {
465             if (exception.getCause() instanceof PropagatorStoppingException) {
466                 // this was an expected exception, we deliberately shut down the propagators
467                 // we therefore explicitly ignore this exception
468                 return;
469             } else if (exception.getCause() instanceof OrekitException) {
470                 // unwrap the original exception
471                 throw (OrekitException) exception.getCause();
472             } else {
473                 throw new OrekitException(exception.getCause(),
474                                           LocalizedCoreFormats.SIMPLE_MESSAGE, exception.getLocalizedMessage());
475             }
476         }
477 
478         /** Clear existing instances of MultiplePropagatorsHandler in a monitored propagator.
479          * <p>
480          * Removes former instances of "MultiplePropagatorsHandler" from step handlers multiplexer.
481          * <p>
482          * This is done to avoid propagation getting stuck after several calls to PropagatorsParallelizer.propagate(...)
483          * <p>
484          * See issue <a href="https://gitlab.orekit.org/orekit/orekit/-/issues/1105">1105</a>.
485          * @param propagator monitored propagator whose MultiplePropagatorsHandlers must be cleared
486          */
487         private void clearMultiplePropagatorsHandler(final Propagator propagator) {
488 
489             // First, list instances of MultiplePropagatorsHandler in the propagator multiplexer
490             final StepHandlerMultiplexer multiplexer = propagator.getMultiplexer();
491             final List<OrekitStepHandler> existingMultiplePropagatorsHandler = new ArrayList<>();
492             for (final OrekitStepHandler handler : multiplexer.getHandlers()) {
493                 if (handler instanceof MultiplePropagatorsHandler) {
494                     existingMultiplePropagatorsHandler.add(handler);
495                 }
496             }
497             // Then, clear all MultiplePropagatorsHandler instances from multiplexer.
498             // This is done in two steps because method "StepHandlerMultiplexer.remove(...)" already loops on the OrekitStepHandlers,
499             // leading to a ConcurrentModificationException if attempting to do everything in a single loop
500             for (final OrekitStepHandler handler : existingMultiplePropagatorsHandler) {
501                 multiplexer.remove(handler);
502             }
503         }
504     }
505 
506 }