1   /* Copyright 2013-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.rugged;
18  
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  
22  import java.lang.reflect.Field;
23  import java.lang.reflect.Modifier;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.Map;
27  
28  import org.hipparchus.geometry.euclidean.threed.Rotation;
29  import org.hipparchus.geometry.euclidean.threed.RotationConvention;
30  import org.hipparchus.geometry.euclidean.threed.Vector3D;
31  import org.hipparchus.ode.nonstiff.DormandPrince853Integrator;
32  import org.hipparchus.util.FastMath;
33  import org.junit.jupiter.api.Assertions;
34  import org.orekit.attitudes.AttitudeProvider;
35  import org.orekit.attitudes.NadirPointing;
36  import org.orekit.attitudes.YawCompensation;
37  import org.orekit.bodies.BodyShape;
38  import org.orekit.bodies.CelestialBodyFactory;
39  import org.orekit.bodies.GeodeticPoint;
40  import org.orekit.bodies.OneAxisEllipsoid;
41  import org.orekit.data.DataContext;
42  import org.orekit.forces.gravity.HolmesFeatherstoneAttractionModel;
43  import org.orekit.forces.gravity.ThirdBodyAttraction;
44  import org.orekit.forces.gravity.potential.GravityFieldFactory;
45  import org.orekit.forces.gravity.potential.NormalizedSphericalHarmonicsProvider;
46  import org.orekit.frames.Frame;
47  import org.orekit.frames.FramesFactory;
48  import org.orekit.frames.Transform;
49  import org.orekit.orbits.CircularOrbit;
50  import org.orekit.orbits.FieldCartesianOrbit;
51  import org.orekit.orbits.FieldCircularOrbit;
52  import org.orekit.orbits.FieldEquinoctialOrbit;
53  import org.orekit.orbits.FieldKeplerianOrbit;
54  import org.orekit.orbits.Orbit;
55  import org.orekit.orbits.OrbitType;
56  import org.orekit.orbits.PositionAngleType;
57  import org.orekit.propagation.CartesianToleranceProvider;
58  import org.orekit.propagation.Propagator;
59  import org.orekit.propagation.SpacecraftState;
60  import org.orekit.propagation.ToleranceProvider;
61  import org.orekit.propagation.analytical.KeplerianPropagator;
62  import org.orekit.propagation.numerical.NumericalPropagator;
63  import org.orekit.propagation.semianalytical.dsst.utilities.JacobiPolynomials;
64  import org.orekit.propagation.semianalytical.dsst.utilities.NewcombOperators;
65  import org.orekit.rugged.linesensor.SensorPixel;
66  import org.orekit.rugged.los.LOSBuilder;
67  import org.orekit.rugged.los.TimeDependentLOS;
68  import org.orekit.time.AbsoluteDate;
69  import org.orekit.time.TimeScale;
70  import org.orekit.time.TimeScalesFactory;
71  import org.orekit.utils.Constants;
72  import org.orekit.utils.IERSConventions;
73  import org.orekit.utils.PVCoordinates;
74  import org.orekit.utils.TimeStampedAngularCoordinates;
75  import org.orekit.utils.TimeStampedPVCoordinates;
76  
77  /**
78   * @author Luc Maisonobe
79   * @author Guylaine Prat
80   */
81  public class TestUtils {
82      
83      /**
84       * Clean up of factories for JUnit 
85       * TBN: copied from Utils of Test suite of Orekit 9.0
86       * @since 2.0
87       */
88      public static void clearFactories() {
89  
90          clearFactoryMaps(CelestialBodyFactory.class);
91          CelestialBodyFactory.clearCelestialBodyLoaders();
92          clearFactoryMaps(FramesFactory.class);
93          clearFactoryMaps(TimeScalesFactory.class);
94          clearFactory(TimeScalesFactory.class, TimeScale.class);
95          clearFactoryMaps(FieldCartesianOrbit.class);
96          clearFactoryMaps(FieldKeplerianOrbit.class);
97          clearFactoryMaps(FieldCircularOrbit.class);
98          clearFactoryMaps(FieldEquinoctialOrbit.class);
99          clearFactoryMaps(JacobiPolynomials.class);
100         clearFactoryMaps(NewcombOperators.class);
101         for (final Class<?> c : NewcombOperators.class.getDeclaredClasses()) {
102             if (c.getName().endsWith("PolynomialsGenerator")) {
103                 clearFactoryMaps(c);
104             }
105         }
106         FramesFactory.clearEOPHistoryLoaders();
107         FramesFactory.setEOPContinuityThreshold(5 * Constants.JULIAN_DAY);
108         TimeScalesFactory.clearUTCTAIOffsetsLoaders();
109         GravityFieldFactory.clearPotentialCoefficientsReaders();
110         GravityFieldFactory.clearOceanTidesReaders();
111         DataContext.getDefault().getDataProvidersManager().clearProviders();
112         DataContext.getDefault().getDataProvidersManager().clearLoadedDataNames();
113     }
114 
115     /** Clean up of factory map
116      * @param factoryClass
117      * @since 2.0
118      */
119     private static void clearFactoryMaps(Class<?> factoryClass) {
120         try {
121             
122             for (Field field : factoryClass.getDeclaredFields()) {
123                 if (Modifier.isStatic(field.getModifiers()) &&
124                     Map.class.isAssignableFrom(field.getType())) {
125                     field.setAccessible(true);
126                     ((Map<?, ?>) field.get(null)).clear();
127                 }
128             }
129         } catch (IllegalAccessException iae) {
130             Assertions.fail(iae.getMessage());
131         }
132     }
133     
134     /** Clean up of a factory
135      * @param factoryClass
136      * @param cachedFieldsClass
137      * @since 2.0
138      */
139     private static void clearFactory(Class<?> factoryClass, Class<?> cachedFieldsClass) {
140         try {
141             
142             for (Field field : factoryClass.getDeclaredFields()) {
143                 if (Modifier.isStatic(field.getModifiers()) &&
144                         cachedFieldsClass.isAssignableFrom(field.getType())) {
145                     field.setAccessible(true);
146                     field.set(null, null);
147                 }
148             }
149         } catch (IllegalAccessException iae) {
150             Assertions.fail(iae.getMessage());
151         }
152     }
153     
154     
155     /**
156      * Generate satellite ephemeris.
157      */
158     public static void addSatellitePV(TimeScale gps, Frame eme2000, Frame itrf,
159                                       ArrayList<TimeStampedPVCoordinates> satellitePVList,
160                                       String absDate,
161                                       double px, double py, double pz, double vx, double vy, double vz) {
162         
163         AbsoluteDate ephemerisDate = new AbsoluteDate(absDate, gps);
164         Vector3D position = new Vector3D(px, py, pz);
165         Vector3D velocity = new Vector3D(vx, vy, vz);
166         PVCoordinates pvITRF = new PVCoordinates(position, velocity);
167         Transform transform = itrf.getTransformTo(eme2000, ephemerisDate);
168         Vector3D pEME2000 = transform.transformPosition(pvITRF.getPosition());
169         Vector3D vEME2000 = transform.transformVector(pvITRF.getVelocity());
170         satellitePVList.add(new TimeStampedPVCoordinates(ephemerisDate, pEME2000, vEME2000, Vector3D.ZERO));
171     }
172 
173     /**
174      * Generate satellite attitudes.
175      */
176     public static void addSatelliteQ(TimeScale gps, ArrayList<TimeStampedAngularCoordinates> satelliteQList,
177                                      String absDate, double q0, double q1, double q2, double q3) {
178         
179         AbsoluteDate attitudeDate = new AbsoluteDate(absDate, gps);
180         Rotation rotation = new Rotation(q0, q1, q2, q3, true);
181         TimeStampedAngularCoordinates pair =
182                 new TimeStampedAngularCoordinates(attitudeDate, rotation, Vector3D.ZERO, Vector3D.ZERO);
183         satelliteQList.add(pair);
184     }
185 
186     /** Create an Earth for Junit tests.
187      */
188     public static BodyShape createEarth() {
189         
190         return new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
191                                     Constants.WGS84_EARTH_FLATTENING,
192                                     FramesFactory.getITRF(IERSConventions.IERS_2010, true));
193     }
194 
195     /** Created a gravity field.
196      * @return normalized spherical harmonics coefficients
197      */
198     public static NormalizedSphericalHarmonicsProvider createGravityField() {
199         
200         return GravityFieldFactory.getNormalizedProvider(12, 12);
201     }
202 
203     /** Create an orbit.
204      * @param mu Earth gravitational constant
205      * @return the orbit
206      */
207     public static Orbit createOrbit(double mu) {
208         
209         // the following orbital parameters have been computed using
210         // Orekit tutorial about phasing, using the following configuration:
211         //
212         //  orbit.date                          = 2012-01-01T00:00:00.000
213         //  phasing.orbits.number               = 143
214         //  phasing.days.number                 =  10
215         //  sun.synchronous.reference.latitude  = 0
216         //  sun.synchronous.reference.ascending = false
217         //  sun.synchronous.mean.solar.time     = 10:30:00
218         //  gravity.field.degree                = 12
219         //  gravity.field.order                 = 12
220         AbsoluteDate date = new AbsoluteDate("2012-01-01T00:00:00.000", TimeScalesFactory.getUTC());
221         Frame eme2000 = FramesFactory.getEME2000();
222         
223         // Observation satellite about 800km above ground
224         return new CircularOrbit(7173352.811913891,
225                                  -4.029194321683225E-4, 0.0013530362644647786,
226                                  FastMath.toRadians(98.63218182243709),
227                                  FastMath.toRadians(77.55565567747836),
228                                  FastMath.PI, PositionAngleType.TRUE,
229                                  eme2000, date, mu);
230     }
231  
232     /** Create the propagator of an orbit.
233      * @return propagator of the orbit
234      */
235     public static Propagator createPropagator(BodyShape earth,
236                                               NormalizedSphericalHarmonicsProvider gravityField,
237                                               Orbit orbit) {
238 
239         AttitudeProvider yawCompensation = new YawCompensation(orbit.getFrame(), new NadirPointing(orbit.getFrame(), earth));
240         SpacecraftState state = new SpacecraftState(orbit,
241                                                     yawCompensation.getAttitude(orbit,
242                                                                                 orbit.getDate(),
243                                                                                 orbit.getFrame())).withMass(1180.0);
244 
245         // numerical model for improving orbit
246         OrbitType type = OrbitType.CIRCULAR;
247         double[][] tolerances = ToleranceProvider.of(CartesianToleranceProvider.of(0.1)).getTolerances(orbit, type);
248         DormandPrince853Integrator integrator =
249                 new DormandPrince853Integrator(1.0e-4 * orbit.getKeplerianPeriod(),
250                                                1.0e-1 * orbit.getKeplerianPeriod(),
251                                                tolerances[0], tolerances[1]);
252         integrator.setInitialStepSize(1.0e-2 * orbit.getKeplerianPeriod());
253         NumericalPropagator numericalPropagator = new NumericalPropagator(integrator);
254         numericalPropagator.addForceModel(new HolmesFeatherstoneAttractionModel(earth.getBodyFrame(), gravityField));
255         numericalPropagator.addForceModel(new ThirdBodyAttraction(CelestialBodyFactory.getSun()));
256         numericalPropagator.addForceModel(new ThirdBodyAttraction(CelestialBodyFactory.getMoon()));
257         numericalPropagator.setOrbitType(type);
258         numericalPropagator.setInitialState(state);
259         numericalPropagator.setAttitudeProvider(yawCompensation);
260         return numericalPropagator;
261 
262     }
263 
264     /** Create a perfect Line Of Sight list
265      * @return the perfect LOS list
266      */
267     public static LOSBuilder createLOSPerfectLine(Vector3D center, Vector3D normal, double halfAperture, int n) {
268 
269         List<Vector3D> list = new ArrayList<Vector3D>(n);
270         for (int i = 0; i < n; ++i) {
271             double alpha = (halfAperture * (2 * i + 1 - n)) / (n - 1);
272             list.add(new Rotation(normal, alpha, RotationConvention.VECTOR_OPERATOR).applyTo(center));
273         }
274         return new LOSBuilder(list);
275     }
276 
277     /** Create a Line Of Sight which depends on time.
278      * @return the dependent of time LOS
279      */
280     public static TimeDependentLOS createLOSCurvedLine(Vector3D center, Vector3D normal,
281                                                  double halfAperture, double sagitta, int n) {
282         
283         Vector3D u = Vector3D.crossProduct(center, normal);
284         List<Vector3D> list = new ArrayList<Vector3D>(n);
285         for (int i = 0; i < n; ++i) {
286             double x = (2.0 * i + 1.0 - n) / (n - 1);
287             double alpha = x * halfAperture;
288             double beta  = x * x * sagitta;
289             list.add(new Rotation(normal, alpha, RotationConvention.VECTOR_OPERATOR).
290                      applyTo(new Rotation(u, beta, RotationConvention.VECTOR_OPERATOR).
291                      applyTo(center)));
292         }
293         return new LOSBuilder(list).build();
294     }
295 
296     /** Propagate an orbit between 2 given dates
297      * @return a list of TimeStampedPVCoordinates
298      */
299     public static List<TimeStampedPVCoordinates> orbitToPV(Orbit orbit, BodyShape earth,
300                                                            AbsoluteDate minDate, AbsoluteDate maxDate,
301                                                            double step)  {
302         
303         Propagator propagator = new KeplerianPropagator(orbit);
304         propagator.setAttitudeProvider(new YawCompensation(orbit.getFrame(), new NadirPointing(orbit.getFrame(), earth)));
305         propagator.propagate(minDate);
306         final List<TimeStampedPVCoordinates> list = new ArrayList<TimeStampedPVCoordinates>();
307         propagator.getMultiplexer().add(step, currentState -> list.add(new TimeStampedPVCoordinates(currentState.getDate(),
308                                                                                                     currentState.getPVCoordinates().getPosition(),
309                                                                                                     currentState.getPVCoordinates().getVelocity(),
310                                                                                                     Vector3D.ZERO)));
311         propagator.propagate(maxDate);
312         return list;
313     }
314 
315     /** Propagate an attitude between 2 given dates
316      * @return a list of TimeStampedAngularCoordinates
317      */
318     public static List<TimeStampedAngularCoordinates> orbitToQ(Orbit orbit, BodyShape earth,
319                                                          AbsoluteDate minDate, AbsoluteDate maxDate,
320                                                          double step) {
321         
322         Propagator propagator = new KeplerianPropagator(orbit);
323         propagator.setAttitudeProvider(new YawCompensation(orbit.getFrame(), new NadirPointing(orbit.getFrame(), earth)));
324         propagator.propagate(minDate);
325         final List<TimeStampedAngularCoordinates> list = new ArrayList<TimeStampedAngularCoordinates>();
326         propagator.getMultiplexer().add(step, currentState -> list.add(new TimeStampedAngularCoordinates(currentState.getDate(),
327                                                                                                          currentState.getAttitude().getRotation(),
328                                                                                                          Vector3D.ZERO, Vector3D.ZERO)));
329         propagator.propagate(maxDate);
330         return list;
331     }
332     
333     /**
334      * Asserts that two SensorPixel are equal to within a positive delta for each component.
335      * For more details see: 
336      * org.junit.assert.assertEquals(double expected, double actual, double delta)
337      */
338     static public void sensorPixelAssertEquals(SensorPixel expected, SensorPixel result, double delta) {
339         
340         assertEquals(expected.getLineNumber(), result.getLineNumber(), delta);
341         assertEquals(expected.getPixelNumber(), result.getPixelNumber(), delta);
342     }
343 
344     /**
345      * Tell if two SensorPixel are the same, where each components are equal to within a positive delta.
346      * For more details see: 
347      * org.junit.assert.assertEquals(String message, double expected, double actual, double delta)
348      * @param expected expected sensor pixel
349      * @param result actual sensor pixel
350      * @param delta delta within two values are consider equal
351      * @return true if the two SensorPixel are the same, false otherwise
352      */
353     static public Boolean sameSensorPixels(SensorPixel expected, SensorPixel result, double delta) {
354         
355         Boolean sameSensorPixel = false;
356         
357         // To have same pixel (true)
358         sameSensorPixel = !(isDifferent(expected.getLineNumber(), result.getLineNumber(), delta) ||
359                             isDifferent(expected.getPixelNumber(), result.getPixelNumber(), delta));
360         
361         return sameSensorPixel;
362     }
363     
364     /**
365      * Tell if two SensorPixel are the same, where each components are equal to within a positive delta.
366      * For more details see: 
367      * org.junit.assert.assertEquals(String message, double expected, double actual, double delta)
368      * @param expected expected sensor pixel
369      * @param result actual sensor pixel
370      * @param delta delta within two values are consider equal
371      * @return true if the two SensorPixel are the same, false otherwise
372      */
373     static public Boolean sameGeodeticPoints(GeodeticPoint expected, GeodeticPoint result, double delta) {
374         
375         Boolean sameGeodeticPoint = false;
376         
377         // To have same GeodeticPoint (true)
378         sameGeodeticPoint = !(isDifferent(expected.getLatitude(), result.getLatitude(), delta) ||
379                             isDifferent(expected.getLongitude(), result.getLongitude(), delta) ||
380                             isDifferent(expected.getAltitude(), result.getAltitude(), delta));
381         
382         return sameGeodeticPoint;
383     }
384 
385     
386     /** Return true if two double values are different within a positive delta.
387      * @param val1 first value to compare
388      * @param val2 second value to compare 
389      * @param delta delta within the two values are consider equal
390      * @return true is different, false if equal within the positive delta
391      */
392     static private boolean isDifferent(double val1, double val2, double delta) {
393         
394         if (Double.compare(val1, val2) == 0) {
395             return false;
396         }
397         if ((FastMath.abs(val1 - val2) <= delta)) {
398             return false;
399         }
400         return true;
401     }
402 
403 
404 }
405