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 org.hipparchus.CalculusFieldElement;
20  import org.hipparchus.Field;
21  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
22  import org.hipparchus.geometry.euclidean.threed.Rotation;
23  import org.hipparchus.geometry.euclidean.threed.RotationOrder;
24  import org.hipparchus.geometry.euclidean.threed.Vector3D;
25  import org.hipparchus.ode.events.Action;
26  import org.hipparchus.ode.nonstiff.AdaptiveStepsizeIntegrator;
27  import org.hipparchus.ode.nonstiff.DormandPrince853Integrator;
28  import org.hipparchus.util.Binary64Field;
29  import org.hipparchus.util.FastMath;
30  import org.junit.jupiter.api.Assertions;
31  import org.junit.jupiter.api.BeforeEach;
32  import org.junit.jupiter.api.Test;
33  import org.orekit.Utils;
34  import org.orekit.bodies.CelestialBodyFactory;
35  import org.orekit.bodies.GeodeticPoint;
36  import org.orekit.bodies.OneAxisEllipsoid;
37  import org.orekit.errors.OrekitException;
38  import org.orekit.errors.OrekitMessages;
39  import org.orekit.forces.gravity.HolmesFeatherstoneAttractionModel;
40  import org.orekit.forces.gravity.potential.GravityFieldFactory;
41  import org.orekit.forces.gravity.potential.ICGEMFormatReader;
42  import org.orekit.frames.FramesFactory;
43  import org.orekit.frames.LOFType;
44  import org.orekit.frames.TopocentricFrame;
45  import org.orekit.orbits.FieldKeplerianOrbit;
46  import org.orekit.orbits.FieldOrbit;
47  import org.orekit.orbits.KeplerianOrbit;
48  import org.orekit.orbits.Orbit;
49  import org.orekit.propagation.*;
50  import org.orekit.propagation.analytical.EcksteinHechlerPropagator;
51  import org.orekit.propagation.analytical.FieldEcksteinHechlerPropagator;
52  import org.orekit.propagation.analytical.KeplerianPropagator;
53  import org.orekit.propagation.events.*;
54  import org.orekit.propagation.events.handlers.ContinueOnEvent;
55  import org.orekit.propagation.events.handlers.CountAndContinue;
56  import org.orekit.propagation.numerical.NumericalPropagator;
57  import org.orekit.time.AbsoluteDate;
58  import org.orekit.time.FieldAbsoluteDate;
59  import org.orekit.time.TimeScalesFactory;
60  import org.orekit.utils.*;
61  
62  import java.util.ArrayList;
63  import java.util.List;
64  
65  import static org.mockito.Mockito.mock;
66  import static org.mockito.Mockito.when;
67  
68  public class AttitudesSequenceTest {
69  
70      private AbsoluteDate lastChange;
71      private boolean inEclipse;
72  
73      @Test
74      void testDayNightSwitch() {
75          //  Initial state definition : date, orbit
76          final AbsoluteDate initialDate = new AbsoluteDate(2004, 1, 1, 23, 30, 00.000, TimeScalesFactory.getUTC());
77          final Vector3D position  = new Vector3D(-6142438.668, 3492467.560, -25767.25680);
78          final Vector3D velocity  = new Vector3D(505.8479685, 942.7809215, 7435.922231);
79          final Orbit initialOrbit = new KeplerianOrbit(new PVCoordinates(position, velocity),
80                                                        FramesFactory.getEME2000(), initialDate,
81                                                        Constants.EIGEN5C_EARTH_MU);
82  
83          final
84  
85          // Attitudes sequence definition
86          EventsLogger logger = new EventsLogger();
87          final AttitudesSequence attitudesSequence = new AttitudesSequence();
88          final AttitudeProvider dayObservationLaw = new LofOffset(initialOrbit.getFrame(), LOFType.LVLH_CCSDS,
89                                                                   RotationOrder.XYZ, FastMath.toRadians(20), FastMath.toRadians(40), 0);
90          final AttitudeProvider nightRestingLaw   = new LofOffset(initialOrbit.getFrame(), LOFType.LVLH_CCSDS);
91          final ExtendedPositionProvider sun = CelestialBodyFactory.getSun();
92          final EclipseDetector ed =
93                          new EclipseDetector(sun, 696000000.,
94                                              new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
95                                                                   0.0,
96                                                                   FramesFactory.getGTOD(IERSConventions.IERS_2010, true))).
97                                  withMaxCheck(600.).
98                  withHandler(new ContinueOnEvent() {
99                      public Action eventOccurred(final SpacecraftState s, final EventDetector d, final boolean increasing) {
100                         setInEclipse(s.getDate(), !increasing);
101                         return Action.RESET_STATE;
102                     }
103                 });
104         final EventDetector monitored = logger.monitorDetector(ed);
105         final Handler dayToNightHandler = new Handler(dayObservationLaw, nightRestingLaw);
106         final Handler nightToDayHandler = new Handler(nightRestingLaw, dayObservationLaw);
107         final double transitionTime = 300.;
108         attitudesSequence.addSwitchingCondition(dayObservationLaw, nightRestingLaw,
109                                                 monitored, false, true, transitionTime,
110                                                 AngularDerivativesFilter.USE_RRA, dayToNightHandler);
111         attitudesSequence.addSwitchingCondition(nightRestingLaw, dayObservationLaw,
112                                                 monitored, true, false, transitionTime,
113                                                 AngularDerivativesFilter.USE_RRA, nightToDayHandler);
114         SpacecraftState initialState = new SpacecraftState(initialOrbit);
115         initialState = initialState.addAdditionalData("fortyTwo", 42.0);
116         if (ed.g(initialState) >= 0) {
117             // initial position is in daytime
118             setInEclipse(initialDate, false);
119             attitudesSequence.resetActiveProvider(dayObservationLaw);
120         } else {
121             // initial position is in nighttime
122             setInEclipse(initialDate, true);
123             attitudesSequence.resetActiveProvider(nightRestingLaw);
124         }
125 
126         // Propagator : consider the analytical Eckstein-Hechler model
127         final Propagator propagator = new EcksteinHechlerPropagator(initialOrbit, attitudesSequence,
128                                                                     Constants.EIGEN5C_EARTH_EQUATORIAL_RADIUS,
129                                                                     Constants.EIGEN5C_EARTH_MU,  Constants.EIGEN5C_EARTH_C20,
130                                                                     Constants.EIGEN5C_EARTH_C30, Constants.EIGEN5C_EARTH_C40,
131                                                                     Constants.EIGEN5C_EARTH_C50, Constants.EIGEN5C_EARTH_C60);
132 
133         propagator.setStepHandler(60.0, currentState -> {
134             // the Earth position in spacecraft frame should be along spacecraft Z axis
135             // during night time and away from it during day time due to roll and pitch offsets
136             final Vector3D earth = currentState.toStaticTransform().transformPosition(Vector3D.ZERO);
137             final double pointingOffset = Vector3D.angle(earth, Vector3D.PLUS_K);
138 
139             // the g function is the eclipse indicator, its an angle between Sun and Earth limb,
140             // positive when Sun is outside of Earth limb, negative when Sun is hidden by Earth limb
141             final double eclipseAngle = ed.g(currentState);
142 
143             if (currentState.getDate().durationFrom(lastChange) > 300) {
144                 if (inEclipse) {
145                     Assertions.assertTrue(eclipseAngle <= 0);
146                     Assertions.assertEquals(0.0, pointingOffset, 1.0e-6);
147                 } else {
148                     Assertions.assertTrue(eclipseAngle >= 0);
149                     Assertions.assertEquals(0.767215, pointingOffset, 1.0e-6);
150                 }
151             } else {
152                 // we are in transition
153                 Assertions.assertTrue(pointingOffset <= 0.7672155,
154                         pointingOffset + " " + (0.767215 - pointingOffset));
155             }
156         });
157 
158         // Propagate from the initial date for the fixed duration
159         propagator.propagate(initialDate.shiftedBy(initialOrbit.getKeplerianPeriod() * 2));
160 
161         // as we have 2 switch events (even if they share the same underlying event detector),
162         // and these events are triggered at both eclipse entry and exit, we get 8
163         // raw events on 2 orbits
164         Assertions.assertEquals(8, logger.getLoggedEvents().size());
165 
166         // we have 4 attitudes switch on 2 orbits, 2 of each type
167         Assertions.assertEquals(2, dayToNightHandler.dates.size());
168         Assertions.assertEquals(2, nightToDayHandler.dates.size());
169 
170     }
171 
172     @Test
173     void testDayNightSwitchField() {
174         doTestDayNightSwitchField(Binary64Field.getInstance());
175     }
176 
177     private <T extends CalculusFieldElement<T>> void doTestDayNightSwitchField(final Field<T> field)
178         {
179 
180         //  Initial state definition : date, orbit
181         final FieldAbsoluteDate<T> initialDate = new FieldAbsoluteDate<>(field, 2004, 1, 1, 23, 30, 00.000, TimeScalesFactory.getUTC());
182         final FieldVector3D<T> position  = new FieldVector3D<>(field,
183                                                                new Vector3D(-6142438.668, 3492467.560, -25767.25680));
184         final FieldVector3D<T> velocity  = new FieldVector3D<>(field,
185                                                                new Vector3D(505.8479685, 942.7809215, 7435.922231));
186         final FieldOrbit<T> initialOrbit = new FieldKeplerianOrbit<>(new FieldPVCoordinates<>(position, velocity),
187                                                                      FramesFactory.getEME2000(), initialDate,
188                                                                      field.getZero().add(Constants.EIGEN5C_EARTH_MU));
189 
190         // Attitudes sequence definition
191         EventsLogger logger = new EventsLogger();
192         final AttitudesSequence attitudesSequence = new AttitudesSequence();
193         final AttitudeProvider dayObservationLaw = new LofOffset(initialOrbit.getFrame(), LOFType.LVLH_CCSDS,
194                                                                  RotationOrder.XYZ, FastMath.toRadians(20), FastMath.toRadians(40), 0);
195         final AttitudeProvider nightRestingLaw   = new LofOffset(initialOrbit.getFrame(), LOFType.LVLH_CCSDS);
196         final ExtendedPositionProvider sun = CelestialBodyFactory.getSun();
197         final EclipseDetector ed =
198                 new EclipseDetector(sun, 696000000.,
199                                     new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
200                                                          0.0,
201                                                          FramesFactory.getGTOD(IERSConventions.IERS_2010, true))).
202                         withMaxCheck(600).
203                 withHandler(new CountAndContinue(0) {
204                     @Override
205                     public Action eventOccurred(final SpacecraftState s,
206                                                              final EventDetector d,
207                                                              final boolean increasing) {
208                         setInEclipse(s.getDate(), !increasing);
209                         if (getCount() == 7) {
210                             increment();
211                             return Action.STOP;
212                         } else {
213                             switch (getCount() % 3) {
214                                 case 0 :
215                                     return Action.CONTINUE;
216                                 case 1 :
217                                     return Action.RESET_DERIVATIVES;
218                                 default :
219                                     return Action.RESET_STATE;
220                             }
221                         }
222                     }
223                 });
224         final EventDetector monitored = logger.monitorDetector(ed);
225         final Handler dayToNightHandler = new Handler(dayObservationLaw, nightRestingLaw);
226         final Handler nightToDayHandler = new Handler(nightRestingLaw, dayObservationLaw);
227         final double transitionTime = 300.;
228         attitudesSequence.addSwitchingCondition(dayObservationLaw, nightRestingLaw,
229                                                 monitored, false, true, transitionTime,
230                                                 AngularDerivativesFilter.USE_RRA, dayToNightHandler);
231         attitudesSequence.addSwitchingCondition(nightRestingLaw, dayObservationLaw,
232                                                 monitored, true, false, transitionTime,
233                                                 AngularDerivativesFilter.USE_RRA, nightToDayHandler);
234         FieldSpacecraftState<T> initialState = new FieldSpacecraftState<>(initialOrbit);
235         initialState = initialState.addAdditionalData("fortyTwo", field.getZero().add(42.0));
236         if (ed.g(initialState.toSpacecraftState()) >= 0) {
237             // initial position is in daytime
238             setInEclipse(initialDate.toAbsoluteDate(), false);
239             attitudesSequence.resetActiveProvider(dayObservationLaw);
240         } else {
241             // initial position is in nighttime
242             setInEclipse(initialDate.toAbsoluteDate(), true);
243             attitudesSequence.resetActiveProvider(nightRestingLaw);
244         }
245 
246         // Propagator : consider the analytical Eckstein-Hechler model
247         final FieldPropagator<T> propagator = new FieldEcksteinHechlerPropagator<>(initialOrbit, attitudesSequence,
248                                                                                    Constants.EIGEN5C_EARTH_EQUATORIAL_RADIUS,
249                                                                                    field.getZero().add(Constants.EIGEN5C_EARTH_MU),
250                                                                                    Constants.EIGEN5C_EARTH_C20,
251                                                                                    Constants.EIGEN5C_EARTH_C30, Constants.EIGEN5C_EARTH_C40,
252                                                                                    Constants.EIGEN5C_EARTH_C50, Constants.EIGEN5C_EARTH_C60);
253 
254 
255         propagator.setStepHandler(field.getZero().add(60.0), currentState -> {
256             // the Earth position in spacecraft frame should be along spacecraft Z axis
257             // during night time and away from it during day time due to roll and pitch offsets
258             final FieldVector3D<T> earth = currentState.toTransform().transformPosition(Vector3D.ZERO);
259             final T pointingOffset = FieldVector3D.angle(earth, Vector3D.PLUS_K);
260 
261             // the g function is the eclipse indicator, its an angle between Sun and Earth limb,
262             // positive when Sun is outside of Earth limb, negative when Sun is hidden by Earth limb
263             final double eclipseAngle = ed.g(currentState.toSpacecraftState());
264 
265             if (currentState.getDate().durationFrom(lastChange).getReal() > 300) {
266                 if (inEclipse) {
267                     Assertions.assertTrue(eclipseAngle <= 0);
268                     Assertions.assertEquals(0.0, pointingOffset.getReal(), 1.0e-6);
269                 } else {
270                     Assertions.assertTrue(eclipseAngle >= 0);
271                     Assertions.assertEquals(0.767215, pointingOffset.getReal(), 1.0e-6);
272                 }
273             } else {
274                 // we are in transition
275                 Assertions.assertTrue(pointingOffset.getReal() <= 0.7672155,
276                         pointingOffset.getReal() + " " + (0.767215 - pointingOffset.getReal()));
277             }
278         });
279 
280         // Propagate from the initial date for the fixed duration
281         propagator.propagate(initialDate.shiftedBy(12600.));
282 
283         // as we have 2 switch events (even if they share the same underlying event detector),
284         // and these events are triggered at both eclipse entry and exit, we get 8
285         // raw events on 2 orbits
286         Assertions.assertEquals(8, logger.getLoggedEvents().size());
287 
288         // we have 4 attitudes switch on 2 orbits, 2 of each type
289         Assertions.assertEquals(2, dayToNightHandler.dates.size());
290         Assertions.assertEquals(2, nightToDayHandler.dates.size());
291 
292     }
293 
294     @Test
295     void testBackwardPropagation() {
296 
297         //  Initial state definition : date, orbit
298         final AbsoluteDate initialDate = new AbsoluteDate(2004, 1, 1, 23, 30, 00.000, TimeScalesFactory.getUTC());
299         final Vector3D position  = new Vector3D(-6142438.668, 3492467.560, -25767.25680);
300         final Vector3D velocity  = new Vector3D(505.8479685, 942.7809215, 7435.922231);
301         final Orbit initialOrbit = new KeplerianOrbit(new PVCoordinates(position, velocity),
302                                                       FramesFactory.getEME2000(), initialDate,
303                                                       Constants.EIGEN5C_EARTH_MU);
304 
305         final AttitudesSequence attitudesSequence = new AttitudesSequence();
306         final AttitudeProvider past    = new FrameAlignedProvider(Rotation.IDENTITY);
307         final AttitudeProvider current = new FrameAlignedProvider(Rotation.IDENTITY);
308         final AttitudeProvider future  = new FrameAlignedProvider(Rotation.IDENTITY);
309         final Handler handler = new Handler(current, past);
310         attitudesSequence.addSwitchingCondition(past, current,
311                                                 new DateDetector(initialDate.shiftedBy(-500.0)),
312                                                 true, false, 10.0, AngularDerivativesFilter.USE_R, handler);
313         attitudesSequence.addSwitchingCondition(current, future,
314                                                 new DateDetector(initialDate.shiftedBy(+500.0)),
315                                                 true, false, 10.0, AngularDerivativesFilter.USE_R, null);
316         attitudesSequence.resetActiveProvider(current);
317 
318         SpacecraftState initialState = new SpacecraftState(initialOrbit);
319         initialState = initialState.addAdditionalData("fortyTwo", 42.0);
320         final Propagator propagator = new EcksteinHechlerPropagator(initialOrbit, attitudesSequence,
321                                                                     Constants.EIGEN5C_EARTH_EQUATORIAL_RADIUS,
322                                                                     Constants.EIGEN5C_EARTH_MU,  Constants.EIGEN5C_EARTH_C20,
323                                                                     Constants.EIGEN5C_EARTH_C30, Constants.EIGEN5C_EARTH_C40,
324                                                                     Constants.EIGEN5C_EARTH_C50, Constants.EIGEN5C_EARTH_C60);
325         propagator.resetInitialState(initialState);
326         Assertions.assertEquals(42.0, propagator.getInitialState().getAdditionalState("fortyTwo")[0], 1.0e-10);
327 
328         SpacecraftState finalState = propagator.propagate(initialDate.shiftedBy(-10000.0));
329         Assertions.assertEquals(42.0, finalState.getAdditionalState("fortyTwo")[0], 1.0e-10);
330         Assertions.assertEquals(1, handler.dates.size());
331         Assertions.assertEquals(-500.0, handler.dates.get(0).durationFrom(initialDate), 1.0e-3);
332         Assertions.assertEquals(-490.0, finalState.getDate().durationFrom(initialDate), 1.0e-3);
333 
334     }
335 
336     @Test
337     void testTooShortTransition() {
338         double threshold      = 1.5;
339         double transitionTime = 0.5;
340         try {
341             new AttitudesSequence().addSwitchingCondition(new FrameAlignedProvider(Rotation.IDENTITY),
342                                                           new FrameAlignedProvider(Rotation.IDENTITY),
343                                                           new DateDetector(AbsoluteDate.J2000_EPOCH).
344                                                           withMaxCheck(1000.0).
345                                                           withThreshold(threshold),
346                                                           true, false, transitionTime,
347                                                           AngularDerivativesFilter.USE_R, null);
348             Assertions.fail("an exception should have been thrown");
349         } catch (OrekitException oe) {
350             Assertions.assertEquals(OrekitMessages.TOO_SHORT_TRANSITION_TIME_FOR_ATTITUDES_SWITCH, oe.getSpecifier());
351             Assertions.assertEquals(transitionTime, (Double) oe.getParts()[0], 1.0e-10);
352             Assertions.assertEquals(threshold, (Double) oe.getParts()[1], 1.0e-10);
353         }
354     }
355 
356     @Test
357     void testOutOfSyncCalls() {
358         //  Initial state definition : date, orbit
359         final AbsoluteDate initialDate = new AbsoluteDate(2004, 1, 1, 23, 30, 00.000, TimeScalesFactory.getUTC());
360         final Vector3D position  = new Vector3D(-6142438.668, 3492467.560, -25767.25680);
361         final Vector3D velocity  = new Vector3D(505.8479685, 942.7809215, 7435.922231);
362         final Orbit initialOrbit = new KeplerianOrbit(new PVCoordinates(position, velocity),
363                                                       FramesFactory.getEME2000(), initialDate,
364                                                       Constants.EIGEN5C_EARTH_MU);
365 
366         final OneAxisEllipsoid earth = new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
367                                                             Constants.WGS84_EARTH_FLATTENING,
368                                                             FramesFactory.getITRF(IERSConventions.IERS_2010, true));
369         final TopocentricFrame volgograd = new TopocentricFrame(earth,
370                                                                 new GeodeticPoint(FastMath.toRadians(48.7),
371                                                                                   FastMath.toRadians(44.5),
372                                                                                   24.0),
373                                                              "Волгоград");
374         final AttitudesSequence attitudesSequence = new AttitudesSequence();
375         final double            transitionTime    = 250.0;
376         final AttitudeProvider  nadirPointing     = new NadirPointing(initialOrbit.getFrame(), earth);
377         final AttitudeProvider  targetPointing    = new TargetPointing(initialOrbit.getFrame(), volgograd.getPoint(), earth);
378         final ElevationDetector eventDetector     = new ElevationDetector(volgograd).
379                                                     withConstantElevation(FastMath.toRadians(5.0)).
380                                                     withHandler(new ContinueOnEvent());
381         final Handler nadirToTarget =  new Handler(nadirPointing, targetPointing);
382         attitudesSequence.addSwitchingCondition(nadirPointing, targetPointing, eventDetector,
383                                                 true, false, transitionTime, AngularDerivativesFilter.USE_RR,
384                                                 nadirToTarget);
385         final Handler targetToNadir =  new Handler(targetPointing, nadirPointing);
386         attitudesSequence.addSwitchingCondition(targetPointing, nadirPointing, eventDetector,
387                                                 false, true, transitionTime, AngularDerivativesFilter.USE_RR,
388                                                 targetToNadir);
389         final double[][] tolerance = ToleranceProvider.getDefaultToleranceProvider(10.).getTolerances(initialOrbit, initialOrbit.getType());
390         final AdaptiveStepsizeIntegrator integrator = new DormandPrince853Integrator(0.001, 300.0, tolerance[0], tolerance[1]);
391         final NumericalPropagator propagator = new NumericalPropagator(integrator);
392         GravityFieldFactory.addPotentialCoefficientsReader(new ICGEMFormatReader("g007_eigen_05c_coef", false));
393         propagator.addForceModel(new HolmesFeatherstoneAttractionModel(earth.getBodyFrame(),
394                                                                        GravityFieldFactory.getNormalizedProvider(8, 8)));
395         propagator.setInitialState(new SpacecraftState(initialOrbit,
396                                                        nadirPointing.getAttitude(initialOrbit,
397                                                                                  initialOrbit.getDate(),
398                                                                                  initialOrbit.getFrame())));
399         propagator.setAttitudeProvider(attitudesSequence);
400         propagator.setStepHandler(10, state -> {
401 
402             Attitude nadirAttitude  = nadirPointing.getAttitude(state.getOrbit(), state.getDate(), state.getFrame());
403             Attitude targetAttitude = targetPointing.getAttitude(state.getOrbit(), state.getDate(), state.getFrame());
404             Attitude stateAttitude  = state.getAttitude();
405 
406             if (nadirToTarget.dates.isEmpty() || state.getDate().durationFrom(nadirToTarget.dates.get(0)) < 0) {
407                 // we are stabilized in nadir pointing, before first switch
408                 checkEqualAttitudes(nadirAttitude, stateAttitude);
409             } else if (state.getDate().durationFrom(nadirToTarget.dates.get(0)) <= transitionTime) {
410                 // we are in transition from nadir to target
411                 checkBetweenAttitudes(nadirAttitude, targetAttitude, stateAttitude);
412             } else if (targetToNadir.dates.isEmpty() || state.getDate().durationFrom(targetToNadir.dates.get(0)) < 0) {
413                 // we are stabilized in target pointing between the two switches
414                 checkEqualAttitudes(targetAttitude, stateAttitude);
415             } else if (state.getDate().durationFrom(targetToNadir.dates.get(0)) <= transitionTime) {
416                 // we are in transition from target to nadir
417                 checkBetweenAttitudes(targetAttitude, nadirAttitude, stateAttitude);
418             } else {
419                 // we are stabilized back in nadir pointing, after second switch
420                 checkEqualAttitudes(nadirAttitude, stateAttitude);
421             }
422 
423         });
424         propagator.propagate(initialDate.shiftedBy(6000));
425 
426     }
427 
428     /**
429      * this test have been completed to test the issue 552 fix
430      */
431     @Test
432     void testResetDuringTransitionForward() {
433         //  Initial state definition : date, orbit
434         final AbsoluteDate initialDate = new AbsoluteDate(2004, 1, 1, 23, 30, 00.000, TimeScalesFactory.getUTC());
435         final Vector3D position  = new Vector3D(-6142438.668, 3492467.560, -25767.25680);
436         final Vector3D velocity  = new Vector3D(505.8479685, 942.7809215, 7435.922231);
437         final Orbit initialOrbit = new KeplerianOrbit(new PVCoordinates(position, velocity),
438                                                       FramesFactory.getEME2000(), initialDate,
439                                                       Constants.EIGEN5C_EARTH_MU);
440 
441         final OneAxisEllipsoid earth = new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
442                                                             Constants.WGS84_EARTH_FLATTENING,
443                                                             FramesFactory.getITRF(IERSConventions.IERS_2010, true));
444         final TopocentricFrame volgograd = new TopocentricFrame(earth,
445                                                                 new GeodeticPoint(FastMath.toRadians(48.7),
446                                                                                   FastMath.toRadians(44.5),
447                                                                                   24.0),
448                                                              "Волгоград");
449         final AttitudesSequence attitudesSequence = new AttitudesSequence();
450         final double            transitionTime    = 250.0;
451         final AttitudeProvider  nadirPointing     = new NadirPointing(initialOrbit.getFrame(), earth);
452         final AttitudeProvider  targetPointing    = new TargetPointing(initialOrbit.getFrame(), volgograd.getPoint(), earth);
453         final ElevationDetector eventDetector     = new ElevationDetector(volgograd).
454                                                     withConstantElevation(FastMath.toRadians(5.0)).
455                                                     withHandler(new ContinueOnEvent());
456         final List<AbsoluteDate> nadirToTarget = new ArrayList<>();
457         attitudesSequence.addSwitchingCondition(nadirPointing, targetPointing, eventDetector,
458                                                 true, false, transitionTime, AngularDerivativesFilter.USE_RR,
459                                                 (previous, next, state) -> nadirToTarget.add(state.getDate()));
460         final double[][] tolerance = ToleranceProvider.getDefaultToleranceProvider(10.).getTolerances(initialOrbit, initialOrbit.getType());
461         final AdaptiveStepsizeIntegrator integrator = new DormandPrince853Integrator(0.001, 300.0, tolerance[0], tolerance[1]);
462         final NumericalPropagator propagator = new NumericalPropagator(integrator);
463         GravityFieldFactory.addPotentialCoefficientsReader(new ICGEMFormatReader("g007_eigen_05c_coef", false));
464         propagator.addForceModel(new HolmesFeatherstoneAttractionModel(earth.getBodyFrame(),
465                                                                        GravityFieldFactory.getNormalizedProvider(8, 8)));
466         propagator.setInitialState(new SpacecraftState(initialOrbit,
467                                                        nadirPointing.getAttitude(initialOrbit,
468                                                                                  initialOrbit.getDate(),
469                                                                                  initialOrbit.getFrame())));
470         propagator.setAttitudeProvider(attitudesSequence);
471         propagator.propagate(initialDate.shiftedBy(6000));
472 
473         // check that if we restart a forward propagation from an intermediate state
474         // we properly get an interpolated attitude despite we missed the event trigger
475 
476         final AbsoluteDate midTransition = nadirToTarget.get(0).shiftedBy(0.5 * transitionTime);
477         SpacecraftState state   = propagator.propagate(midTransition.shiftedBy(-60), midTransition);
478         Rotation nadirR  = nadirPointing.getAttitude(state.getOrbit(), state.getDate(), state.getFrame()).getRotation();
479         Rotation targetR = targetPointing.getAttitude(state.getOrbit(), state.getDate(), state.getFrame()).getRotation();
480         final double reorientationAngle = Rotation.distance(nadirR, targetR);
481         Assertions.assertEquals(0.5 * reorientationAngle, Rotation.distance(state.getAttitude().getRotation(), nadirR),
482                 0.03 * reorientationAngle);
483 
484         // check that if we restart a forward propagation from an intermediate state
485         // we properly get the "after" attitude law despite we missed the event trigger
486         // This check have been added to the test after the issue #552 fix
487 
488         final AbsoluteDate afterTransition = midTransition.shiftedBy(transitionTime);
489         state = propagator.propagate(midTransition, afterTransition);
490         targetR = targetPointing.getAttitude(state.getOrbit(), state.getDate(), state.getFrame()).getRotation();
491 
492         Assertions.assertEquals(targetR.getQ0(), state.getAttitude().getRotation().getQ0(), 1.0e-16);
493         Assertions.assertEquals(targetR.getQ1(), state.getAttitude().getRotation().getQ1(), 1.0e-16);
494         Assertions.assertEquals(targetR.getQ2(), state.getAttitude().getRotation().getQ2(), 1.0e-16);
495         Assertions.assertEquals(targetR.getQ3(), state.getAttitude().getRotation().getQ3(), 1.0e-16);
496     }
497 
498     @Test
499     void testResetDuringTransitionBackward() {
500         //  Initial state definition : date, orbit
501         final AbsoluteDate initialDate = new AbsoluteDate(2004, 1, 1, 23, 30, 00.000, TimeScalesFactory.getUTC());
502         final Vector3D position  = new Vector3D(-6142438.668, 3492467.560, -25767.25680);
503         final Vector3D velocity  = new Vector3D(505.8479685, 942.7809215, 7435.922231);
504         final AbsolutePVCoordinates initialPV = new AbsolutePVCoordinates(FramesFactory.getEME2000(), initialDate, new PVCoordinates(position, velocity));
505 
506         final OneAxisEllipsoid earth = new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
507                                                             Constants.WGS84_EARTH_FLATTENING,
508                                                             FramesFactory.getITRF(IERSConventions.IERS_2010, true));
509         final TopocentricFrame volgograd = new TopocentricFrame(earth,
510                                                                 new GeodeticPoint(FastMath.toRadians(48.7),
511                                                                                   FastMath.toRadians(44.5),
512                                                                                   24.0),
513                                                              "Волгоград");
514         final AttitudesSequence attitudesSequence = new AttitudesSequence();
515         final double            transitionTime    = 250.0;
516         final AttitudeProvider  nadirPointing     = new NadirPointing(initialPV.getFrame(), earth);
517         final AttitudeProvider  targetPointing    = new TargetPointing(initialPV.getFrame(), volgograd.getPoint(), earth);
518         final ElevationDetector eventDetector     = new ElevationDetector(volgograd).
519                                                     withConstantElevation(FastMath.toRadians(5.0)).
520                                                     withHandler(new ContinueOnEvent());
521         final List<AbsoluteDate> nadirToTarget = new ArrayList<>();
522         attitudesSequence.addSwitchingCondition(nadirPointing, targetPointing, eventDetector,
523                                                 true, false, transitionTime, AngularDerivativesFilter.USE_RR,
524                                                 (previous, next, state) -> nadirToTarget.add(state.getDate()));
525         final double[][] tolerance = ToleranceProvider.getDefaultToleranceProvider(10.).getTolerances(initialPV);
526         final AdaptiveStepsizeIntegrator integrator = new DormandPrince853Integrator(0.001, 300.0, tolerance[0], tolerance[1]);
527         final NumericalPropagator propagator = new NumericalPropagator(integrator);
528         GravityFieldFactory.addPotentialCoefficientsReader(new ICGEMFormatReader("g007_eigen_05c_coef", false));
529         propagator.addForceModel(new HolmesFeatherstoneAttractionModel(earth.getBodyFrame(),
530                                                                        GravityFieldFactory.getNormalizedProvider(8, 8)));
531         propagator.setInitialState(new SpacecraftState(initialPV,
532                                                        nadirPointing.getAttitude(initialPV,
533                                                                                  initialPV.getDate(),
534                                                                                  initialPV.getFrame())));
535         propagator.setAttitudeProvider(attitudesSequence);
536         propagator.setOrbitType(null);
537         propagator.propagate(initialDate.shiftedBy(6000));
538 
539         // check that if we restart a backward propagation from an intermediate state
540         // we properly get an interpolated attitude despite we missed the event trigger
541         final AbsoluteDate midTransition = nadirToTarget.get(0).shiftedBy(0.5 * transitionTime);
542         SpacecraftState state   = propagator.propagate(midTransition.shiftedBy(+60), midTransition);
543         Rotation nadirR  = nadirPointing.getAttitude(state.getAbsPVA(), state.getDate(), state.getFrame()).getRotation();
544         Rotation targetR = targetPointing.getAttitude(state.getAbsPVA(), state.getDate(), state.getFrame()).getRotation();
545         final double reorientationAngle = Rotation.distance(nadirR, targetR);
546         Assertions.assertEquals(0.5 * reorientationAngle, Rotation.distance(state.getAttitude().getRotation(), targetR),
547                 0.08 * reorientationAngle);
548 
549 
550     }
551 
552     /**
553      * Test for the issue 551 fix
554      */
555     @Test
556     void testAnalyticalPropagatorTransition() {
557         // Define initial state
558         final AbsoluteDate initialDate = new AbsoluteDate(2017, 03, 27, 0, 0, 00.000, TimeScalesFactory.getUTC());
559         final Vector3D position  = new Vector3D(-39098981.4866597, -15784239.3610601, 78908.2289853595);
560         final Vector3D velocity  = new Vector3D(1151.00321021175, -2851.14864755189, -2.02133248357321);
561         final Orbit initialOrbit = new KeplerianOrbit(new PVCoordinates(position, velocity),
562                                                       FramesFactory.getGCRF(), initialDate,
563                                                       Constants.WGS84_EARTH_MU);
564 
565         // Define attitude laws
566         final AttitudeProvider before  = new FrameAlignedProvider(new Rotation(0, 0, 0, 1, false));
567         final AttitudeProvider current = new FrameAlignedProvider(Rotation.IDENTITY);
568         final AttitudeProvider after   = new FrameAlignedProvider(new Rotation(0, 0, 0, -1, false));
569 
570         // Define attitude sequence
571         final AbsoluteDate forwardSwitchDate = initialDate.shiftedBy(600);
572         final AbsoluteDate backwardSwitchDate = initialDate.shiftedBy(-600);
573         final DateDetector forwardSwitchDetector = new DateDetector(forwardSwitchDate).withHandler(new ContinueOnEvent());
574         final DateDetector backwardSwitchDetector = new DateDetector(backwardSwitchDate).withHandler(new ContinueOnEvent());
575 
576         // Initialize the attitude sequence
577         final AttitudesSequence attitudeSequence = new AttitudesSequence();
578         attitudeSequence.resetActiveProvider(current);
579         attitudeSequence.addSwitchingCondition(before, current, backwardSwitchDetector, true, true, 60, AngularDerivativesFilter.USE_RR, null);
580         attitudeSequence.addSwitchingCondition(current, after, forwardSwitchDetector, true, true, 60, AngularDerivativesFilter.USE_RR, null);
581 
582         // Initialize analytical propagator
583         Propagator propagator = new KeplerianPropagator(initialOrbit);
584         propagator.setAttitudeProvider(attitudeSequence);
585 
586         SpacecraftState stateAfter = propagator.propagate(initialDate, initialDate.shiftedBy(1200));
587         SpacecraftState stateBefore = propagator.propagate(initialDate, initialDate.shiftedBy(-1200));
588 
589         // Check that the dates are correct
590         Assertions.assertEquals(1200, stateAfter.getDate().durationFrom(initialDate), 1.0E-3);
591         Assertions.assertEquals(-1200, stateBefore.getDate().durationFrom(initialDate), 1.0E-3);
592 
593         // Check that the attitudes are correct
594         Assertions.assertEquals(
595                 before.getAttitude(stateBefore.getOrbit(), stateBefore.getDate(), stateBefore.getFrame()).getRotation().getQ0(),
596                 stateBefore.getAttitude().getRotation().getQ0(), 1.0E-16);
597         Assertions.assertEquals(
598                 before.getAttitude(stateBefore.getOrbit(), stateBefore.getDate(), stateBefore.getFrame()).getRotation().getQ1(),
599                 stateBefore.getAttitude().getRotation().getQ1(), 1.0E-16);
600         Assertions.assertEquals(
601                 before.getAttitude(stateBefore.getOrbit(), stateBefore.getDate(), stateBefore.getFrame()).getRotation().getQ2(),
602                 stateBefore.getAttitude().getRotation().getQ2(), 1.0E-16);
603         Assertions.assertEquals(
604                 before.getAttitude(stateBefore.getOrbit(), stateBefore.getDate(), stateBefore.getFrame()).getRotation().getQ3(),
605                 stateBefore.getAttitude().getRotation().getQ3(), 1.0E-16);
606 
607         Assertions.assertEquals(
608                 after.getAttitude(stateAfter.getOrbit(), stateAfter.getDate(), stateAfter.getFrame()).getRotation().getQ0(),
609                 stateAfter.getAttitude().getRotation().getQ0(), 1.0E-16);
610         Assertions.assertEquals(
611                 after.getAttitude(stateAfter.getOrbit(), stateAfter.getDate(), stateAfter.getFrame()).getRotation().getQ1(),
612                 stateAfter.getAttitude().getRotation().getQ1(), 1.0E-16);
613         Assertions.assertEquals(
614                 after.getAttitude(stateAfter.getOrbit(), stateAfter.getDate(), stateAfter.getFrame()).getRotation().getQ2(),
615                 stateAfter.getAttitude().getRotation().getQ2(), 1.0E-16);
616         Assertions.assertEquals(
617                 after.getAttitude(stateAfter.getOrbit(), stateAfter.getDate(), stateAfter.getFrame()).getRotation().getQ3(),
618                 stateAfter.getAttitude().getRotation().getQ3(), 1.0E-16);
619     }
620 
621     /** Issue 1387. */
622     @Test
623     void testGetSwitches() {
624         // GIVEN
625         final double                   transitionTime             = 1;
626         final AttitudeProvider         mockPastAttitudeProvider   = mock(AttitudeProvider.class);
627         final AttitudeProvider         mockFutureAttitudeProvider = mock(AttitudeProvider.class);
628         final boolean                  switchOnIncrease           = true;
629         final boolean                  switchOnDecrease           = true;
630         final AngularDerivativesFilter filter                     = AngularDerivativesFilter.USE_R;
631         final EventDetector            mockSwitchEvent            = mock(EventDetector.class);
632         when(mockSwitchEvent.getThreshold()).thenReturn(transitionTime - 0.5);
633 
634         // Create attitudes sequence
635         final AttitudesSequence attitudesSequence = new AttitudesSequence();
636         attitudesSequence.addSwitchingCondition(mockPastAttitudeProvider, mockFutureAttitudeProvider, mockSwitchEvent,
637                                                 switchOnIncrease, switchOnDecrease, transitionTime, filter, null);
638 
639 
640         // WHEN
641         final List<AttitudesSequence.Switch> switches1 = attitudesSequence.getSwitches();
642         final List<AttitudesSequence.Switch> switches2 = attitudesSequence.getSwitches();
643 
644         // THEN
645         Assertions.assertEquals(1, switches1.size());
646         Assertions.assertNotSame(switches1, switches2);
647     }
648 
649     private static class Handler implements AttitudeSwitchHandler {
650 
651         private final AttitudeProvider   expectedPrevious;
652         private final AttitudeProvider   expectedNext;
653         private final List<AbsoluteDate> dates;
654 
655         public Handler(final AttitudeProvider expectedPrevious, final AttitudeProvider expectedNext) {
656             this.expectedPrevious = expectedPrevious;
657             this.expectedNext     = expectedNext;
658             this.dates            = new ArrayList<>();
659         }
660 
661         @Override
662         public void switchOccurred(AttitudeProvider previous, AttitudeProvider next,
663                                    SpacecraftState state) {
664             Assertions.assertSame(previous, expectedPrevious);
665             Assertions.assertSame(next, expectedNext);
666             dates.add(state.getDate());
667         }
668 
669     }
670 
671     private void setInEclipse(AbsoluteDate lastChange, boolean inEclipse) {
672         this.lastChange = lastChange;
673         this.inEclipse = inEclipse;
674     }
675 
676     private void checkEqualAttitudes(final Attitude expected, final Attitude tested) {
677         Assertions.assertEquals(0.0, Rotation.distance(expected.getRotation(), tested.getRotation()), 1.0e-14);
678         Assertions.assertEquals(0.0, Vector3D.distance(expected.getSpin(), tested.getSpin()), 1.0e-11);
679         Assertions.assertEquals(0.0,
680                 Vector3D.distance(expected.getRotationAcceleration(), tested.getRotationAcceleration()), 1.0e-9);
681     }
682 
683     private void checkBetweenAttitudes(final Attitude limit1, final Attitude limit2, final Attitude tested) {
684         final Rotation r1 = limit1.getRotation();
685         final Rotation r2 = limit2.getRotation();
686         final Rotation t  = tested.getRotation();
687         final double reorientationAngle = Rotation.distance(r1, r2);
688         Assertions.assertTrue(Rotation.distance(t, r1) < reorientationAngle);
689         Assertions.assertTrue(Rotation.distance(t, r2) < reorientationAngle);
690     }
691 
692     @BeforeEach
693     public void setUp() {
694         Utils.setDataRoot("regular-data:potential");
695     }
696 
697 }
698