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.attitudes;
18  
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.List;
22  import java.util.stream.Stream;
23  
24  import org.hipparchus.CalculusFieldElement;
25  import org.hipparchus.Field;
26  import org.hipparchus.ode.events.Action;
27  import org.orekit.errors.OrekitException;
28  import org.orekit.errors.OrekitMessages;
29  import org.orekit.frames.Frame;
30  import org.orekit.orbits.Orbit;
31  import org.orekit.propagation.SpacecraftState;
32  import org.orekit.propagation.events.EventDetector;
33  import org.orekit.propagation.events.FieldEventDetector;
34  import org.orekit.time.AbsoluteDate;
35  import org.orekit.time.FieldAbsoluteDate;
36  import org.orekit.time.FieldTimeInterpolator;
37  import org.orekit.time.TimeInterpolator;
38  import org.orekit.utils.AbsolutePVCoordinates;
39  import org.orekit.utils.AngularDerivativesFilter;
40  import org.orekit.utils.FieldPVCoordinatesProvider;
41  import org.orekit.utils.DataDictionary;
42  import org.orekit.utils.PVCoordinatesProvider;
43  import org.orekit.utils.TimeStampedAngularCoordinates;
44  import org.orekit.utils.TimeStampedAngularCoordinatesHermiteInterpolator;
45  import org.orekit.utils.TimeStampedFieldAngularCoordinates;
46  import org.orekit.utils.TimeStampedFieldAngularCoordinatesHermiteInterpolator;
47  
48  /** This classes manages a sequence of different attitude providers that are activated
49   * in turn according to switching events. It includes non-zero transition durations between subsequent modes.
50   * @author Luc Maisonobe
51   * @since 5.1
52   * @see AttitudesSwitcher
53   */
54  public class AttitudesSequence extends AbstractSwitchingAttitudeProvider {
55  
56      /** Switching events list. */
57      private final List<Switch> switches;
58  
59      /** Constructor for an initially empty sequence.
60       */
61      public AttitudesSequence() {
62          super();
63          switches = new ArrayList<>();
64      }
65  
66      /** Add a switching condition between two attitude providers.
67       * <p>
68       * The {@code past} and {@code future} attitude providers are defined with regard
69       * to the natural flow of time. This means that if the propagation is forward, the
70       * propagator will switch from {@code past} provider to {@code future} provider at
71       * event occurrence, but if the propagation is backward, the propagator will switch
72       * from {@code future} provider to {@code past} provider at event occurrence. The
73       * transition between the two attitude laws is not instantaneous, the switch event
74       * defines the start of the transition (i.e. when leaving the {@code past} attitude
75       * law and entering the interpolated transition law). The end of the transition
76       * (i.e. when leaving the interpolating transition law and entering the {@code future}
77       * attitude law) occurs at switch time plus {@code transitionTime}.
78       * </p>
79       * <p>
80       * An attitude provider may have several different switch events associated to
81       * it. Depending on which event is triggered, the appropriate provider is
82       * switched to.
83       * </p>
84       * <p>
85       * If the underlying detector has an event handler associated to it, this handler
86       * will be triggered (i.e. its {@link org.orekit.propagation.events.handlers.EventHandler#eventOccurred(SpacecraftState,
87       * EventDetector, boolean) eventOccurred} method will be called), <em>regardless</em>
88       * of the event really triggering an attitude switch or not. As an example, if an
89       * eclipse detector is used to switch from day to night attitude mode when entering
90       * eclipse, with {@code switchOnIncrease} set to {@code false} and {@code switchOnDecrease}
91       * set to {@code true}. Then a handler set directly at eclipse detector level would
92       * be triggered at both eclipse entry and eclipse exit, but attitude switch would
93       * occur <em>only</em> at eclipse entry. Note that for the sake of symmetry, the
94       * transition start and end dates should match for both forward and backward propagation.
95       * This implies that for backward propagation, we have to compensate for the {@code
96       * transitionTime} when looking for the event. An unfortunate consequence is that the
97       * {@link org.orekit.propagation.events.handlers.EventHandler#eventOccurred(SpacecraftState, EventDetector, boolean)
98       * eventOccurred} method may appear to be called out of sync with respect to the
99       * propagation (it will be called when propagator reaches transition end, despite it
100      * refers to transition start, as per {@code transitionTime} compensation), and if the
101      * method returns {@link Action#STOP}, it will stop at the end of the
102      * transition instead of at the start. For these reasons, it is not recommended to
103      * set up an event handler for events that are used to switch attitude. If an event
104      * handler is needed for other purposes, a second handler should be registered to
105      * the propagator rather than relying on the side effects of attitude switches.
106      * </p>
107      * <p>
108      * The smoothness of the transition between past and future attitude laws can be tuned
109      * using the {@code transitionTime} and {@code transitionFilter} parameters. The {@code
110      * transitionTime} parameter specifies how much time is spent to switch from one law to
111      * the other law. It should be larger than the event {@link EventDetector#getThreshold()
112      * convergence threshold} in order to ensure attitude continuity. The {@code
113      * transitionFilter} parameter specifies the attitude time derivatives that should match
114      * at the boundaries between past attitude law and transition law on one side, and
115      * between transition law and future law on the other side.
116      * {@link AngularDerivativesFilter#USE_R} means only the rotation should be identical,
117      * {@link AngularDerivativesFilter#USE_RR} means both rotation and rotation rate
118      * should be identical, {@link AngularDerivativesFilter#USE_RRA} means both rotation,
119      * rotation rate and rotation acceleration should be identical. During the transition,
120      * the attitude law is computed by interpolating between past attitude law at switch time
121      * and future attitude law at current intermediate time.
122      * </p>
123      * @param past attitude provider applicable for times in the switch event occurrence past
124      * @param future attitude provider applicable for times in the switch event occurrence future
125      * @param switchEvent event triggering the attitude providers switch
126      * @param switchOnIncrease if true, switch is triggered on increasing event
127      * @param switchOnDecrease if true, switch is triggered on decreasing event
128      * @param transitionTime duration of the transition between the past and future attitude laws
129      * @param transitionFilter specification of transition law time derivatives that
130      * should match past and future attitude laws
131      * @param switchHandler handler to call for notifying when switch occurs (may be null)
132      * @param <T> class type for the switch event
133      * @since 13.0
134      */
135     public <T extends EventDetector> void addSwitchingCondition(final AttitudeProvider past,
136                                                                 final AttitudeProvider future,
137                                                                 final T switchEvent,
138                                                                 final boolean switchOnIncrease,
139                                                                 final boolean switchOnDecrease,
140                                                                 final double transitionTime,
141                                                                 final AngularDerivativesFilter transitionFilter,
142                                                                 final AttitudeSwitchHandler switchHandler) {
143 
144         // safety check, for ensuring attitude continuity
145         if (transitionTime < switchEvent.getThreshold()) {
146             throw new OrekitException(OrekitMessages.TOO_SHORT_TRANSITION_TIME_FOR_ATTITUDES_SWITCH,
147                                       transitionTime, switchEvent.getThreshold());
148         }
149 
150         // if it is the first switching condition, assume first active law is the past one
151         if (getActivated() == null) {
152             resetActiveProvider(past);
153         }
154 
155         // add the switching condition
156         switches.add(new Switch(switchEvent, switchOnIncrease, switchOnDecrease,
157                                 past, future, transitionTime, transitionFilter, switchHandler));
158 
159     }
160 
161     @Override
162     public Stream<EventDetector> getEventDetectors() {
163         return Stream.concat(switches.stream().map(Switch.class::cast), getEventDetectors(getParametersDrivers()));
164     }
165 
166     @Override
167     public <T extends CalculusFieldElement<T>> Stream<FieldEventDetector<T>> getFieldEventDetectors(final Field<T> field) {
168         final Stream<FieldEventDetector<T>> switchesStream = switches.stream().map(sw -> getFieldEventDetector(field, sw));
169         return Stream.concat(switchesStream, getFieldEventDetectors(field, getParametersDrivers()));
170     }
171 
172     /**
173      * Gets a deep copy of the switches stored in this instance.
174      *
175      * @return deep copy of the switches stored in this instance
176      */
177     public List<Switch> getSwitches() {
178         return new ArrayList<>(switches);
179     }
180 
181     /** Switch specification. Handles the transition. */
182     public class Switch extends AbstractAttitudeSwitch {
183 
184         /** Duration of the transition between the past and future attitude laws. */
185         private final double transitionTime;
186 
187         /** Order at which the transition law time derivatives should match past and future attitude laws. */
188         private final AngularDerivativesFilter transitionFilter;
189 
190         /** Propagation direction. */
191         private boolean forward;
192 
193         /**
194          * Simple constructor.
195          *
196          * @param event event
197          * @param switchOnIncrease if true, switch is triggered on increasing event
198          * @param switchOnDecrease if true, switch is triggered on decreasing event otherwise switch is triggered on
199          * decreasing event
200          * @param past attitude provider applicable for times in the switch event occurrence past
201          * @param future attitude provider applicable for times in the switch event occurrence future
202          * @param transitionTime duration of the transition between the past and future attitude laws
203          * @param transitionFilter order at which the transition law time derivatives should match past and future attitude
204          * laws
205          * @param switchHandler handler to call for notifying when switch occurs (may be null)
206          */
207         private Switch(final EventDetector event, final boolean switchOnIncrease, final boolean switchOnDecrease,
208                        final AttitudeProvider past, final AttitudeProvider future, final double transitionTime,
209                        final AngularDerivativesFilter transitionFilter, final AttitudeSwitchHandler switchHandler) {
210             super(event, switchOnIncrease, switchOnDecrease, past, future, switchHandler);
211             this.transitionTime   = transitionTime;
212             this.transitionFilter = transitionFilter;
213         }
214 
215         /** {@inheritDoc} */
216         @Override
217         public void init(final SpacecraftState s0, final AbsoluteDate t) {
218             super.init(s0, t);
219 
220             // reset the transition parameters (this will be done once for each switch,
221             //  despite doing it only once would have sufficient; it's not really a problem)
222             forward = t.durationFrom(s0.getDate()) >= 0.0;
223             if (getActivated().getSpansNumber() > 1) {
224                 // remove transitions that will be overridden during upcoming propagation
225                 if (forward) {
226                     setActivated(getActivated().extractRange(AbsoluteDate.PAST_INFINITY, s0.getDate().shiftedBy(transitionTime)));
227                 } else {
228                     setActivated(getActivated().extractRange(s0.getDate().shiftedBy(-transitionTime), AbsoluteDate.FUTURE_INFINITY));
229                 }
230             }
231 
232         }
233 
234         /** {@inheritDoc} */
235         @Override
236         public double g(final SpacecraftState s) {
237             return getDetector().g(forward ? s : s.shiftedBy(-transitionTime));
238         }
239 
240         /** {@inheritDoc} */
241         public Action eventOccurred(final SpacecraftState s, final EventDetector detector, final boolean increasing) {
242 
243             final AbsoluteDate date = s.getDate();
244             if (getActivated().get(date) == (forward ? getPast() : getFuture()) &&
245                 (increasing && isSwitchOnIncrease() || !increasing && isSwitchOnDecrease())) {
246 
247                 if (forward) {
248 
249                     // prepare transition
250                     final AbsoluteDate transitionEnd = date.shiftedBy(transitionTime);
251                     getActivated().addValidAfter(new TransitionProvider(s.getAttitude(), transitionEnd), date, false);
252 
253                     // prepare future law after transition
254                     getActivated().addValidAfter(getFuture(), transitionEnd, false);
255 
256                     // notify about the switch
257                     if (getSwitchHandler() != null) {
258                         getSwitchHandler().switchOccurred(getPast(), getFuture(), s);
259                     }
260 
261                     return getDetector().getHandler().eventOccurred(s, getDetector(), increasing);
262 
263                 } else {
264 
265                     // estimate state at transition start, according to the past attitude law
266                     final double dt = -transitionTime;
267                     final AbsoluteDate shiftedDate = date.shiftedBy(dt);
268                     SpacecraftState sState;
269                     if (s.isOrbitDefined()) {
270                         final Orbit     sOrbit    = s.getOrbit().shiftedBy(dt);
271                         final Attitude  sAttitude = getPast().getAttitude(sOrbit, shiftedDate, s.getFrame());
272                         sState    = new SpacecraftState(sOrbit, sAttitude).withMass(s.getMass());
273                     } else {
274                         final AbsolutePVCoordinates sAPV    = s.getAbsPVA().shiftedBy(dt);
275                         final Attitude  sAttitude = getPast().getAttitude(sAPV, shiftedDate, s.getFrame());
276                         sState    = new SpacecraftState(sAPV, sAttitude).withMass(s.getMass());
277                     }
278                     for (final DataDictionary.Entry entry : s.getAdditionalDataValues().getData()) {
279                         sState = sState.addAdditionalData(entry.getKey(), entry.getValue());
280                     }
281 
282                     // prepare transition
283                     getActivated().addValidBefore(new TransitionProvider(sState.getAttitude(), date), date, false);
284 
285                     // prepare past law before transition
286                     getActivated().addValidBefore(getPast(), shiftedDate, false);
287 
288                     // notify about the switch
289                     if (getSwitchHandler() != null) {
290                         getSwitchHandler().switchOccurred(getFuture(), getPast(), sState);
291                     }
292 
293                     return getDetector().getHandler().eventOccurred(sState, getDetector(), increasing);
294 
295                 }
296 
297             } else {
298                 // trigger the underlying event despite no attitude switch occurred
299                 return getDetector().getHandler().eventOccurred(s, getDetector(), increasing);
300             }
301 
302         }
303 
304         /** Provider for transition phases.
305          * @since 9.2
306          */
307         private class TransitionProvider implements AttitudeProvider {
308 
309             /** Attitude at preceding transition. */
310             private final Attitude transitionPreceding;
311 
312             /** Date of final switch to following attitude law. */
313             private final AbsoluteDate transitionEnd;
314 
315             /** Simple constructor.
316              * @param transitionPreceding attitude at preceding transition
317              * @param transitionEnd date of final switch to following attitude law
318              */
319             TransitionProvider(final Attitude transitionPreceding, final AbsoluteDate transitionEnd) {
320                 this.transitionPreceding = transitionPreceding;
321                 this.transitionEnd       = transitionEnd;
322             }
323 
324             /** {@inheritDoc} */
325             public Attitude getAttitude(final PVCoordinatesProvider pvProv,
326                                         final AbsoluteDate date, final Frame frame) {
327 
328                 // Create sample
329                 final TimeStampedAngularCoordinates start =
330                         transitionPreceding.withReferenceFrame(frame).getOrientation();
331                 final TimeStampedAngularCoordinates end =
332                         getFuture().getAttitude(pvProv, transitionEnd, frame).getOrientation();
333                 final List<TimeStampedAngularCoordinates> sample =  Arrays.asList(start, end);
334 
335                 // Create interpolator
336                 final TimeInterpolator<TimeStampedAngularCoordinates> interpolator =
337                         new TimeStampedAngularCoordinatesHermiteInterpolator(sample.size(), transitionFilter);
338 
339                 // interpolate between the two boundary attitudes
340                 final TimeStampedAngularCoordinates interpolated = interpolator.interpolate(date, sample);
341 
342                 return new Attitude(frame, interpolated);
343 
344             }
345 
346             /** {@inheritDoc} */
347             public <S extends CalculusFieldElement<S>> FieldAttitude<S> getAttitude(final FieldPVCoordinatesProvider<S> pvProv,
348                                                                                     final FieldAbsoluteDate<S> date,
349                                                                                     final Frame frame) {
350 
351                 // create sample
352                 final TimeStampedFieldAngularCoordinates<S> start =
353                         new TimeStampedFieldAngularCoordinates<>(date.getField(),
354                                                                  transitionPreceding.withReferenceFrame(frame).getOrientation());
355                 final TimeStampedFieldAngularCoordinates<S> end =
356                         getFuture().getAttitude(pvProv,
357                                            new FieldAbsoluteDate<>(date.getField(), transitionEnd),
358                                            frame).getOrientation();
359                 final List<TimeStampedFieldAngularCoordinates<S>> sample = Arrays.asList(start, end);
360 
361                 // create interpolator
362                 final FieldTimeInterpolator<TimeStampedFieldAngularCoordinates<S>, S> interpolator =
363                         new TimeStampedFieldAngularCoordinatesHermiteInterpolator<>(sample.size(), transitionFilter);
364 
365                 // interpolate between the two boundary attitudes
366                 final TimeStampedFieldAngularCoordinates<S> interpolated = interpolator.interpolate(date, sample);
367 
368                 return new FieldAttitude<>(frame, interpolated);
369             }
370 
371         }
372 
373     }
374 
375 }