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++++]>
70 * |
71 * propagator 2 : ----[++++current step++++]--------->
72 * | |
73 * ... | |
74 * propagator n : ---------[++++current step++++]---->
75 * | |
76 * V V
77 * global handler : -------------[global step]--------->
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 }