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 }