1 /* Copyright 2002-2024 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.attitudes;
18
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.List;
22
23 import org.hipparchus.CalculusFieldElement;
24 import org.hipparchus.Field;
25 import org.hipparchus.geometry.euclidean.threed.FieldRotation;
26 import org.hipparchus.geometry.euclidean.threed.Rotation;
27 import org.hipparchus.ode.events.Action;
28 import org.orekit.errors.OrekitException;
29 import org.orekit.errors.OrekitMessages;
30 import org.orekit.frames.Frame;
31 import org.orekit.orbits.Orbit;
32 import org.orekit.propagation.FieldPropagator;
33 import org.orekit.propagation.FieldSpacecraftState;
34 import org.orekit.propagation.Propagator;
35 import org.orekit.propagation.SpacecraftState;
36 import org.orekit.propagation.events.AdaptableInterval;
37 import org.orekit.propagation.events.EventDetector;
38 import org.orekit.propagation.events.FieldAdaptableInterval;
39 import org.orekit.propagation.events.FieldEventDetector;
40 import org.orekit.propagation.events.handlers.EventHandler;
41 import org.orekit.propagation.events.handlers.FieldEventHandler;
42 import org.orekit.time.AbsoluteDate;
43 import org.orekit.time.FieldAbsoluteDate;
44 import org.orekit.time.FieldTimeInterpolator;
45 import org.orekit.time.TimeInterpolator;
46 import org.orekit.utils.AngularDerivativesFilter;
47 import org.orekit.utils.DoubleArrayDictionary;
48 import org.orekit.utils.FieldPVCoordinatesProvider;
49 import org.orekit.utils.PVCoordinatesProvider;
50 import org.orekit.utils.TimeSpanMap;
51 import org.orekit.utils.TimeStampedAngularCoordinates;
52 import org.orekit.utils.TimeStampedAngularCoordinatesHermiteInterpolator;
53 import org.orekit.utils.TimeStampedFieldAngularCoordinates;
54 import org.orekit.utils.TimeStampedFieldAngularCoordinatesHermiteInterpolator;
55
56 /** This classes manages a sequence of different attitude providers that are activated
57 * in turn according to switching events.
58 * <p>Only one attitude provider in the sequence is in an active state. When one of
59 * the switch event associated with the active provider occurs, the active provider becomes
60 * the one specified with the event. A simple example is a provider for the sun lighted part
61 * of the orbit and another provider for the eclipse time. When the sun lighted provider is active,
62 * the eclipse entry event is checked and when it occurs the eclipse provider is activated.
63 * When the eclipse provider is active, the eclipse exit event is checked and when it occurs
64 * the sun lighted provider is activated again. This sequence is a simple loop.</p>
65 * <p>An active attitude provider may have several switch events and next provider settings, leading
66 * to different activation patterns depending on which events are triggered first. An example
67 * of this feature is handling switches to safe mode if some contingency condition is met, in
68 * addition to the nominal switches that correspond to proper operations. Another example
69 * is handling of maneuver mode.
70 * <p>
71 * Note that this attitude provider is stateful, it keeps in memory the sequence of active
72 * underlying providers with their switch dates and the transitions from one provider to
73 * the other. This implies that this provider should <em>not</em> be shared among different
74 * propagators at the same time, each propagator should use its own instance of this provider.
75 * <p>
76 * The sequence kept in memory is reset when {@link #resetActiveProvider(AttitudeProvider)}
77 * is called, and only the specify provider is kept. The sequence is also partially
78 * reset each time a propagation starts. If a new propagation is started after a first
79 * propagation has been run, all the already computed switches that occur after propagation
80 * start for forward propagation or before propagation start for backward propagation will
81 * be erased. New switches will be computed and applied properly according to the new
82 * propagation settings. The already computed switches that are not in covered are kept
83 * in memory. This implies that if a propagation is interrupted and restarted in the
84 * same direction, then attitude switches will remain in place, ensuring that even if the
85 * interruption occurred in the middle of an attitude transition the second propagation will
86 * properly complete the transition that was started by the first propagator.
87 * </p>
88 * @author Luc Maisonobe
89 * @since 5.1
90 */
91 public class AttitudesSequence implements AttitudeProvider {
92
93 /** Providers that have been activated. */
94 private transient TimeSpanMap<AttitudeProvider> activated;
95
96 /** Switching events list. */
97 private final List<Switch> switches;
98
99 /** Constructor for an initially empty sequence.
100 */
101 public AttitudesSequence() {
102 activated = null;
103 switches = new ArrayList<>();
104 }
105
106 /** Reset the active provider.
107 * <p>
108 * Calling this method clears all already seen switch history,
109 * so it should <em>not</em> be used during the propagation itself,
110 * it is intended to be used only at start
111 * </p>
112 * @param provider provider to activate
113 */
114 public void resetActiveProvider(final AttitudeProvider provider) {
115 activated = new TimeSpanMap<>(provider);
116 }
117
118 /** Register all wrapped switch events to the propagator.
119 * <p>
120 * This method must be called once before propagation, after the
121 * switching conditions have been set up by calls to {@link
122 * #addSwitchingCondition(AttitudeProvider, AttitudeProvider, EventDetector,
123 * boolean, boolean, double, AngularDerivativesFilter, SwitchHandler)
124 * addSwitchingCondition}.
125 * </p>
126 * @param propagator propagator that will handle the events
127 */
128 public void registerSwitchEvents(final Propagator propagator) {
129 for (final Switch s : switches) {
130 propagator.addEventDetector(s);
131 }
132 }
133
134 /** Register all wrapped switch events to the propagator.
135 * <p>
136 * This method must be called once before propagation, after the
137 * switching conditions have been set up by calls to {@link
138 * #addSwitchingCondition(AttitudeProvider, AttitudeProvider, EventDetector,
139 * boolean, boolean, double, AngularDerivativesFilter, SwitchHandler)
140 * addSwitchingCondition}.
141 * </p>
142 * @param field field to which the elements belong
143 * @param propagator propagator that will handle the events
144 * @param <T> type of the field elements
145 */
146 public <T extends CalculusFieldElement<T>> void registerSwitchEvents(final Field<T> field, final FieldPropagator<T> propagator) {
147 for (final Switch sw : switches) {
148 propagator.addEventDetector(new FieldEventDetector<T>() {
149
150 /** {@inheritDoc} */
151 @Override
152 public void init(final FieldSpacecraftState<T> s0,
153 final FieldAbsoluteDate<T> t) {
154 sw.init(s0.toSpacecraftState(), t.toAbsoluteDate());
155 }
156
157 /** {@inheritDoc} */
158 @Override
159 public T g(final FieldSpacecraftState<T> s) {
160 return field.getZero().newInstance(sw.g(s.toSpacecraftState()));
161 }
162
163 /** {@inheritDoc} */
164 @Override
165 public T getThreshold() {
166 return field.getZero().newInstance(sw.getThreshold());
167 }
168
169 /** {@inheritDoc} */
170 @Override
171 public FieldAdaptableInterval<T> getMaxCheckInterval() {
172 return s -> sw.getMaxCheckInterval().currentInterval(s.toSpacecraftState());
173 }
174
175 /** {@inheritDoc} */
176 @Override
177 public int getMaxIterationCount() {
178 return sw.getMaxIterationCount();
179 }
180
181 /** {@inheritDoc} */
182 @Override
183 public FieldEventHandler<T> getHandler() {
184 return new FieldEventHandler<T>() {
185 /** {@inheritDoc} */
186 @Override
187 public Action eventOccurred(final FieldSpacecraftState<T> s,
188 final FieldEventDetector<T> detector,
189 final boolean increasing) {
190 return sw.eventOccurred(s.toSpacecraftState(), sw, increasing);
191 }
192
193 /** {@inheritDoc} */
194 @Override
195 public FieldSpacecraftState<T> resetState(final FieldEventDetector<T> detector,
196 final FieldSpacecraftState<T> oldState) {
197 return new FieldSpacecraftState<>(field, sw.resetState(sw, oldState.toSpacecraftState()));
198 }
199 };
200 }
201
202 });
203 }
204 }
205
206 /** Add a switching condition between two attitude providers.
207 * <p>
208 * The {@code past} and {@code future} attitude providers are defined with regard
209 * to the natural flow of time. This means that if the propagation is forward, the
210 * propagator will switch from {@code past} provider to {@code future} provider at
211 * event occurrence, but if the propagation is backward, the propagator will switch
212 * from {@code future} provider to {@code past} provider at event occurrence. The
213 * transition between the two attitude laws is not instantaneous, the switch event
214 * defines the start of the transition (i.e. when leaving the {@code past} attitude
215 * law and entering the interpolated transition law). The end of the transition
216 * (i.e. when leaving the interpolating transition law and entering the {@code future}
217 * attitude law) occurs at switch time plus {@code transitionTime}.
218 * </p>
219 * <p>
220 * An attitude provider may have several different switch events associated to
221 * it. Depending on which event is triggered, the appropriate provider is
222 * switched to.
223 * </p>
224 * <p>
225 * The switch events specified here must <em>not</em> be registered to the
226 * propagator directly. The proper way to register these events is to
227 * call {@link #registerSwitchEvents(Propagator)} once after all switching
228 * conditions have been set up. The reason for this is that the events will
229 * be wrapped before being registered.
230 * </p>
231 * <p>
232 * If the underlying detector has an event handler associated to it, this handler
233 * will be triggered (i.e. its {@link org.orekit.propagation.events.handlers.EventHandler#eventOccurred(SpacecraftState,
234 * EventDetector, boolean) eventOccurred} method will be called), <em>regardless</em>
235 * of the event really triggering an attitude switch or not. As an example, if an
236 * eclipse detector is used to switch from day to night attitude mode when entering
237 * eclipse, with {@code switchOnIncrease} set to {@code false} and {@code switchOnDecrease}
238 * set to {@code true}. Then a handler set directly at eclipse detector level would
239 * be triggered at both eclipse entry and eclipse exit, but attitude switch would
240 * occur <em>only</em> at eclipse entry. Note that for the sake of symmetry, the
241 * transition start and end dates should match for both forward and backward propagation.
242 * This implies that for backward propagation, we have to compensate for the {@code
243 * transitionTime} when looking for the event. An unfortunate consequence is that the
244 * {@link org.orekit.propagation.events.handlers.EventHandler#eventOccurred(SpacecraftState, EventDetector, boolean)
245 * eventOccurred} method may appear to be called out of sync with respect to the
246 * propagation (it will be called when propagator reaches transition end, despite it
247 * refers to transition start, as per {@code transitionTime} compensation), and if the
248 * method returns {@link Action#STOP}, it will stop at the end of the
249 * transition instead of at the start. For these reasons, it is not recommended to
250 * set up an event handler for events that are used to switch attitude. If an event
251 * handler is needed for other purposes, a second handler should be registered to
252 * the propagator rather than relying on the side effects of attitude switches.
253 * </p>
254 * <p>
255 * The smoothness of the transition between past and future attitude laws can be tuned
256 * using the {@code transitionTime} and {@code transitionFilter} parameters. The {@code
257 * transitionTime} parameter specifies how much time is spent to switch from one law to
258 * the other law. It should be larger than the event {@link EventDetector#getThreshold()
259 * convergence threshold} in order to ensure attitude continuity. The {@code
260 * transitionFilter} parameter specifies the attitude time derivatives that should match
261 * at the boundaries between past attitude law and transition law on one side, and
262 * between transition law and future law on the other side.
263 * {@link AngularDerivativesFilter#USE_R} means only the rotation should be identical,
264 * {@link AngularDerivativesFilter#USE_RR} means both rotation and rotation rate
265 * should be identical, {@link AngularDerivativesFilter#USE_RRA} means both rotation,
266 * rotation rate and rotation acceleration should be identical. During the transition,
267 * the attitude law is computed by interpolating between past attitude law at switch time
268 * and future attitude law at current intermediate time.
269 * </p>
270 * @param past attitude provider applicable for times in the switch event occurrence past
271 * @param future attitude provider applicable for times in the switch event occurrence future
272 * @param switchEvent event triggering the attitude providers switch
273 * @param switchOnIncrease if true, switch is triggered on increasing event
274 * @param switchOnDecrease if true, switch is triggered on decreasing event
275 * @param transitionTime duration of the transition between the past and future attitude laws
276 * @param transitionFilter specification of transition law time derivatives that
277 * should match past and future attitude laws
278 * @param handler handler to call for notifying when switch occurs (may be null)
279 * @param <T> class type for the switch event
280 * @since 7.1
281 */
282 public <T extends EventDetector> void addSwitchingCondition(final AttitudeProvider past,
283 final AttitudeProvider future,
284 final T switchEvent,
285 final boolean switchOnIncrease,
286 final boolean switchOnDecrease,
287 final double transitionTime,
288 final AngularDerivativesFilter transitionFilter,
289 final SwitchHandler handler) {
290
291 // safety check, for ensuring attitude continuity
292 if (transitionTime < switchEvent.getThreshold()) {
293 throw new OrekitException(OrekitMessages.TOO_SHORT_TRANSITION_TIME_FOR_ATTITUDES_SWITCH,
294 transitionTime, switchEvent.getThreshold());
295 }
296
297 // if it is the first switching condition, assume first active law is the past one
298 if (activated == null) {
299 resetActiveProvider(past);
300 }
301
302 // add the switching condition
303 switches.add(new Switch(switchEvent, switchOnIncrease, switchOnDecrease,
304 past, future, transitionTime, transitionFilter, handler));
305
306 }
307
308 /** {@inheritDoc} */
309 @Override
310 public Attitude getAttitude(final PVCoordinatesProvider pvProv,
311 final AbsoluteDate date, final Frame frame) {
312 return activated.get(date).getAttitude(pvProv, date, frame);
313 }
314
315 /** {@inheritDoc} */
316 @Override
317 public <T extends CalculusFieldElement<T>> FieldAttitude<T> getAttitude(final FieldPVCoordinatesProvider<T> pvProv,
318 final FieldAbsoluteDate<T> date,
319 final Frame frame) {
320 return activated.get(date.toAbsoluteDate()).getAttitude(pvProv, date, frame);
321 }
322
323 /** {@inheritDoc} */
324 @Override
325 public Rotation getAttitudeRotation(final PVCoordinatesProvider pvProv, final AbsoluteDate date, final Frame frame) {
326 return activated.get(date).getAttitudeRotation(pvProv, date, frame);
327 }
328
329 @Override
330 public <T extends CalculusFieldElement<T>> FieldRotation<T> getAttitudeRotation(final FieldPVCoordinatesProvider<T> pvProv,
331 final FieldAbsoluteDate<T> date,
332 final Frame frame) {
333 return activated.get(date.toAbsoluteDate()).getAttitudeRotation(pvProv, date, frame);
334 }
335
336 /** Switch specification. */
337 private class Switch implements EventDetector, EventHandler {
338
339 /** Event. */
340 private final EventDetector event;
341
342 /** Event direction triggering the switch. */
343 private final boolean switchOnIncrease;
344
345 /** Event direction triggering the switch. */
346 private final boolean switchOnDecrease;
347
348 /** Attitude provider applicable for times in the switch event occurrence past. */
349 private final AttitudeProvider past;
350
351 /** Attitude provider applicable for times in the switch event occurrence future. */
352 private final AttitudeProvider future;
353
354 /** Duration of the transition between the past and future attitude laws. */
355 private final double transitionTime;
356
357 /** Order at which the transition law time derivatives should match past and future attitude laws. */
358 private final AngularDerivativesFilter transitionFilter;
359
360 /** Handler to call for notifying when switch occurs (may be null). */
361 private final SwitchHandler switchHandler;
362
363 /** Propagation direction. */
364 private boolean forward;
365
366 /** Simple constructor.
367 * @param event event
368 * @param switchOnIncrease if true, switch is triggered on increasing event
369 * @param switchOnDecrease if true, switch is triggered on decreasing event
370 * otherwise switch is triggered on decreasing event
371 * @param past attitude provider applicable for times in the switch event occurrence past
372 * @param future attitude provider applicable for times in the switch event occurrence future
373 * @param transitionTime duration of the transition between the past and future attitude laws
374 * @param transitionFilter order at which the transition law time derivatives
375 * should match past and future attitude laws
376 * @param switchHandler handler to call for notifying when switch occurs (may be null)
377 */
378 Switch(final EventDetector event,
379 final boolean switchOnIncrease, final boolean switchOnDecrease,
380 final AttitudeProvider past, final AttitudeProvider future,
381 final double transitionTime, final AngularDerivativesFilter transitionFilter,
382 final SwitchHandler switchHandler) {
383 this.event = event;
384 this.switchOnIncrease = switchOnIncrease;
385 this.switchOnDecrease = switchOnDecrease;
386 this.past = past;
387 this.future = future;
388 this.transitionTime = transitionTime;
389 this.transitionFilter = transitionFilter;
390 this.switchHandler = switchHandler;
391 }
392
393 /** {@inheritDoc} */
394 @Override
395 public double getThreshold() {
396 return event.getThreshold();
397 }
398
399 /** {@inheritDoc} */
400 @Override
401 public AdaptableInterval getMaxCheckInterval() {
402 return event.getMaxCheckInterval();
403 }
404
405 /** {@inheritDoc} */
406 @Override
407 public int getMaxIterationCount() {
408 return event.getMaxIterationCount();
409 }
410
411 /** {@inheritDoc} */
412 public void init(final SpacecraftState s0, final AbsoluteDate t) {
413
414 // reset the transition parameters (this will be done once for each switch,
415 // despite doing it only once would have sufficient; it's not really a problem)
416 forward = t.durationFrom(s0.getDate()) >= 0.0;
417 if (activated.getSpansNumber() > 1) {
418 // remove transitions that will be overridden during upcoming propagation
419 if (forward) {
420 activated = activated.extractRange(AbsoluteDate.PAST_INFINITY, s0.getDate().shiftedBy(transitionTime));
421 } else {
422 activated = activated.extractRange(s0.getDate().shiftedBy(-transitionTime), AbsoluteDate.FUTURE_INFINITY);
423 }
424 }
425
426 // initialize the underlying event
427 event.init(s0, t);
428
429 }
430
431 /** {@inheritDoc} */
432 public double g(final SpacecraftState s) {
433 return event.g(forward ? s : s.shiftedBy(-transitionTime));
434 }
435
436 /** {@inheritDoc} */
437 public EventHandler getHandler() {
438 return this;
439 }
440
441 /** {@inheritDoc} */
442 public Action eventOccurred(final SpacecraftState s, final EventDetector detector, final boolean increasing) {
443
444 final AbsoluteDate date = s.getDate();
445 if (activated.get(date) == (forward ? past : future) &&
446 (increasing && switchOnIncrease || !increasing && switchOnDecrease)) {
447
448 if (forward) {
449
450 // prepare transition
451 final AbsoluteDate transitionEnd = date.shiftedBy(transitionTime);
452 activated.addValidAfter(new TransitionProvider(s.getAttitude(), transitionEnd), date, false);
453
454 // prepare future law after transition
455 activated.addValidAfter(future, transitionEnd, false);
456
457 // notify about the switch
458 if (switchHandler != null) {
459 switchHandler.switchOccurred(past, future, s);
460 }
461
462 return event.getHandler().eventOccurred(s, event, increasing);
463
464 } else {
465
466 // estimate state at transition start, according to the past attitude law
467 final Orbit sOrbit = s.getOrbit().shiftedBy(-transitionTime);
468 final Attitude sAttitude = past.getAttitude(sOrbit, sOrbit.getDate(), sOrbit.getFrame());
469 SpacecraftState sState = new SpacecraftState(sOrbit, sAttitude, s.getMass());
470 for (final DoubleArrayDictionary.Entry entry : s.getAdditionalStatesValues().getData()) {
471 sState = sState.addAdditionalState(entry.getKey(), entry.getValue());
472 }
473
474 // prepare transition
475 activated.addValidBefore(new TransitionProvider(sAttitude, date), date, false);
476
477 // prepare past law before transition
478 activated.addValidBefore(past, sOrbit.getDate(), false);
479
480 // notify about the switch
481 if (switchHandler != null) {
482 switchHandler.switchOccurred(future, past, sState);
483 }
484
485 return event.getHandler().eventOccurred(sState, event, increasing);
486
487 }
488
489 } else {
490 // trigger the underlying event despite no attitude switch occurred
491 return event.getHandler().eventOccurred(s, event, increasing);
492 }
493
494 }
495
496 /** {@inheritDoc} */
497 @Override
498 public SpacecraftState resetState(final EventDetector detector, final SpacecraftState oldState) {
499 // delegate to underlying event
500 return event.getHandler().resetState(event, oldState);
501 }
502
503 /** Provider for transition phases.
504 * @since 9.2
505 */
506 private class TransitionProvider implements AttitudeProvider {
507
508 /** Attitude at preceding transition. */
509 private final Attitude transitionPreceding;
510
511 /** Date of final switch to following attitude law. */
512 private final AbsoluteDate transitionEnd;
513
514 /** Simple constructor.
515 * @param transitionPreceding attitude at preceding transition
516 * @param transitionEnd date of final switch to following attitude law
517 */
518 TransitionProvider(final Attitude transitionPreceding, final AbsoluteDate transitionEnd) {
519 this.transitionPreceding = transitionPreceding;
520 this.transitionEnd = transitionEnd;
521 }
522
523 /** {@inheritDoc} */
524 public Attitude getAttitude(final PVCoordinatesProvider pvProv,
525 final AbsoluteDate date, final Frame frame) {
526
527 // Create sample
528 final TimeStampedAngularCoordinates start =
529 transitionPreceding.withReferenceFrame(frame).getOrientation();
530 final TimeStampedAngularCoordinates end =
531 future.getAttitude(pvProv, transitionEnd, frame).getOrientation();
532 final List<TimeStampedAngularCoordinates> sample = Arrays.asList(start, end);
533
534 // Create interpolator
535 final TimeInterpolator<TimeStampedAngularCoordinates> interpolator =
536 new TimeStampedAngularCoordinatesHermiteInterpolator(sample.size(), transitionFilter);
537
538 // interpolate between the two boundary attitudes
539 final TimeStampedAngularCoordinates interpolated = interpolator.interpolate(date, sample);
540
541 return new Attitude(frame, interpolated);
542
543 }
544
545 /** {@inheritDoc} */
546 public <S extends CalculusFieldElement<S>> FieldAttitude<S> getAttitude(final FieldPVCoordinatesProvider<S> pvProv,
547 final FieldAbsoluteDate<S> date,
548 final Frame frame) {
549
550 // create sample
551 final TimeStampedFieldAngularCoordinates<S> start =
552 new TimeStampedFieldAngularCoordinates<>(date.getField(),
553 transitionPreceding.withReferenceFrame(frame).getOrientation());
554 final TimeStampedFieldAngularCoordinates<S> end =
555 future.getAttitude(pvProv,
556 new FieldAbsoluteDate<>(date.getField(), transitionEnd),
557 frame).getOrientation();
558 final List<TimeStampedFieldAngularCoordinates<S>> sample = Arrays.asList(start, end);
559
560 // create interpolator
561 final FieldTimeInterpolator<TimeStampedFieldAngularCoordinates<S>, S> interpolator =
562 new TimeStampedFieldAngularCoordinatesHermiteInterpolator<>(sample.size(), transitionFilter);
563
564 // interpolate between the two boundary attitudes
565 final TimeStampedFieldAngularCoordinates<S> interpolated = interpolator.interpolate(date, sample);
566
567 return new FieldAttitude<>(frame, interpolated);
568 }
569
570 }
571
572 }
573
574 /** Interface for attitude switch notifications.
575 * <p>
576 * This interface is intended to be implemented by users who want to be
577 * notified when an attitude switch occurs.
578 * </p>
579 * @since 7.1
580 */
581 public interface SwitchHandler {
582
583 /** Method called when attitude is switched from one law to another law.
584 * @param preceding attitude law used preceding the switch (i.e. in the past
585 * of the switch event for a forward propagation, or in the future
586 * of the switch event for a backward propagation)
587 * @param following attitude law used following the switch (i.e. in the future
588 * of the switch event for a forward propagation, or in the past
589 * of the switch event for a backward propagation)
590 * @param state state at switch time (with attitude computed using the {@code preceding} law)
591 */
592 void switchOccurred(AttitudeProvider preceding, AttitudeProvider following, SpacecraftState state);
593
594 }
595
596 }