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.propagation.semianalytical.dsst.forces;
18  
19  import java.io.IOException;
20  import java.text.ParseException;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.List;
24  
25  import org.hipparchus.exception.LocalizedCoreFormats;
26  import org.hipparchus.ode.nonstiff.ClassicalRungeKuttaIntegrator;
27  import org.hipparchus.stat.descriptive.StreamingStatistics;
28  import org.hipparchus.util.FastMath;
29  import org.junit.jupiter.api.Assertions;
30  import org.junit.jupiter.api.BeforeEach;
31  import org.junit.jupiter.api.Test;
32  import org.orekit.Utils;
33  import org.orekit.attitudes.AttitudeProvider;
34  import org.orekit.attitudes.FrameAlignedProvider;
35  import org.orekit.errors.OrekitException;
36  import org.orekit.forces.gravity.HolmesFeatherstoneAttractionModel;
37  import org.orekit.forces.gravity.potential.GRGSFormatReader;
38  import org.orekit.forces.gravity.potential.GravityFieldFactory;
39  import org.orekit.forces.gravity.potential.NormalizedSphericalHarmonicsProvider;
40  import org.orekit.forces.gravity.potential.UnnormalizedSphericalHarmonicsProvider;
41  import org.orekit.frames.Frame;
42  import org.orekit.frames.FramesFactory;
43  import org.orekit.orbits.CircularOrbit;
44  import org.orekit.orbits.EquinoctialOrbit;
45  import org.orekit.orbits.Orbit;
46  import org.orekit.orbits.PositionAngleType;
47  import org.orekit.propagation.BoundedPropagator;
48  import org.orekit.propagation.EphemerisGenerator;
49  import org.orekit.propagation.PropagationType;
50  import org.orekit.propagation.SpacecraftState;
51  import org.orekit.propagation.numerical.NumericalPropagator;
52  import org.orekit.propagation.semianalytical.dsst.DSSTPropagator;
53  import org.orekit.propagation.semianalytical.dsst.utilities.AuxiliaryElements;
54  import org.orekit.time.AbsoluteDate;
55  import org.orekit.time.DateComponents;
56  import org.orekit.time.TimeComponents;
57  import org.orekit.time.TimeScalesFactory;
58  import org.orekit.utils.IERSConventions;
59  
60  class DSSTZonalTest {
61  
62      /** Test mean elements rates computation.
63       * 
64       * First without setting the body-fixed frame, then by setting it to the inertial propagation frame.
65       * Both should give the same results.
66       */
67      @Test
68      void testGetMeanElementRate() {
69          doTestGetMeanElementRate(false);
70          doTestGetMeanElementRate(true);
71      }
72  
73      private void doTestGetMeanElementRate(final boolean testIssue1104) {
74  
75          // Central Body geopotential 4x4
76          final UnnormalizedSphericalHarmonicsProvider provider =
77                  GravityFieldFactory.getUnnormalizedProvider(4, 4);
78  
79          final Frame earthFrame = FramesFactory.getEME2000();
80          final AbsoluteDate initDate = new AbsoluteDate(2007, 04, 16, 0, 46, 42.400, TimeScalesFactory.getUTC());
81  
82          // a  = 26559890 m
83          // ex = 2.719455286199036E-4
84          // ey = 0.0041543085910249414
85          // hx = -0.3412974060023717
86          // hy = 0.3960084733107685
87          // lM = 8.566537840341699 rad
88          final Orbit orbit = new EquinoctialOrbit(2.655989E7,
89                                                   2.719455286199036E-4,
90                                                   0.0041543085910249414,
91                                                   -0.3412974060023717,
92                                                   0.3960084733107685,
93                                                   8.566537840341699,
94                                                   PositionAngleType.TRUE,
95                                                   earthFrame,
96                                                   initDate,
97                                                   3.986004415E14);
98  
99          final SpacecraftState state = new SpacecraftState(orbit, 1000.0);
100 
101         final DSSTForceModel zonal;
102         if (testIssue1104) {
103             // Non regression for issue 1104, pass-on the inertial propagation frame as body-fixed frame
104             zonal = new DSSTZonal(state.getFrame(), provider, 4, 3, 9);
105         } else {
106             // Classical way of doing the same thing
107             zonal = new DSSTZonal(provider, 4, 3, 9);
108         }
109 
110         // Force model parameters
111         final double[] parameters = zonal.getParameters(orbit.getDate());
112 
113         final AuxiliaryElements auxiliaryElements = new AuxiliaryElements(state.getOrbit(), 1);
114 
115         // Initialize force model
116         zonal.initializeShortPeriodTerms(auxiliaryElements, PropagationType.MEAN, parameters);
117 
118         final double[] elements = new double[7];
119         Arrays.fill(elements, 0.0);
120 
121         final double[] daidt = zonal.getMeanElementRate(state, auxiliaryElements, parameters);
122         for (int i = 0; i < daidt.length; i++) {
123             elements[i] = daidt[i];
124         }
125 
126         Assertions.assertEquals(0.0,                     elements[0], 1.e-25);
127         Assertions.assertEquals(1.3909396722346468E-11,  elements[1], 3.e-26);
128         Assertions.assertEquals(-2.0275977261372793E-13, elements[2], 3.e-27);
129         Assertions.assertEquals(3.087141512018238E-9,    elements[3], 1.e-24);
130         Assertions.assertEquals(2.6606317310148797E-9,   elements[4], 4.e-24);
131         Assertions.assertEquals(-3.659904725206694E-9,   elements[5], 1.e-24);
132 
133     }
134 
135     @Test
136     void testShortPeriodTerms() {
137         final SpacecraftState meanState = getGEOState();
138 
139         final UnnormalizedSphericalHarmonicsProvider provider = GravityFieldFactory.getUnnormalizedProvider(2, 0);
140         final DSSTForceModel zonal    = new DSSTZonal(provider, 2, 1, 5);
141 
142         //Create the auxiliary object
143         final AuxiliaryElements aux = new AuxiliaryElements(meanState.getOrbit(), 1);
144 
145         // Set the force models
146         final List<ShortPeriodTerms> shortPeriodTerms = new ArrayList<ShortPeriodTerms>();
147 
148         zonal.registerAttitudeProvider(null);
149         shortPeriodTerms.addAll(zonal.initializeShortPeriodTerms(aux, PropagationType.OSCULATING, zonal.getParameters(meanState.getDate())));
150         zonal.updateShortPeriodTerms(zonal.getParametersAllValues(), meanState);
151 
152         double[] y = new double[6];
153         for (final ShortPeriodTerms spt : shortPeriodTerms) {
154             final double[] shortPeriodic = spt.value(meanState.getOrbit());
155             for (int i = 0; i < shortPeriodic.length; i++) {
156                 y[i] += shortPeriodic[i];
157             }
158         }
159 
160         Assertions.assertEquals(35.005618980090276,     y[0], 1.e-15);
161         Assertions.assertEquals(3.75891551882889E-5,    y[1], 1.e-20);
162         Assertions.assertEquals(3.929119925563796E-6,   y[2], 1.e-21);
163         Assertions.assertEquals(-1.1781951949124315E-8, y[3], 1.e-24);
164         Assertions.assertEquals(-3.2134924513679615E-8, y[4], 1.e-24);
165         Assertions.assertEquals(-1.1607392915997098E-6, y[5], 1.e-21);
166     }
167 
168     private SpacecraftState getGEOState() {
169         // No shadow at this date
170         final AbsoluteDate initDate = new AbsoluteDate(new DateComponents(2003, 05, 21), new TimeComponents(1, 0, 0.),
171                                                        TimeScalesFactory.getUTC());
172         final Orbit orbit = new EquinoctialOrbit(42164000,
173                                                  10e-3,
174                                                  10e-3,
175                                                  FastMath.tan(0.001745329) * FastMath.cos(2 * FastMath.PI / 3),
176                                                  FastMath.tan(0.001745329) * FastMath.sin(2 * FastMath.PI / 3), 0.1,
177                                                  PositionAngleType.TRUE,
178                                                  FramesFactory.getEME2000(),
179                                                  initDate,
180                                                  3.986004415E14);
181         return new SpacecraftState(orbit);
182     }
183     
184     @Test
185     void testIssue625() {
186 
187         Utils.setDataRoot("regular-data:potential/grgs-format");
188         GravityFieldFactory.addPotentialCoefficientsReader(new GRGSFormatReader("grim4s4_gr", true));
189 
190         final Frame earthFrame = FramesFactory.getEME2000();
191         final AbsoluteDate initDate = new AbsoluteDate(2007, 04, 16, 0, 46, 42.400, TimeScalesFactory.getUTC());
192 
193         // a  = 2.655989E6 m
194         // ex = 2.719455286199036E-4
195         // ey = 0.0041543085910249414
196         // hx = -0.3412974060023717
197         // hy = 0.3960084733107685
198         // lM = 8.566537840341699 rad
199         final Orbit orbit = new EquinoctialOrbit(2.655989E6,
200                                                  2.719455286199036E-4,
201                                                  0.0041543085910249414,
202                                                  -0.3412974060023717,
203                                                  0.3960084733107685,
204                                                  8.566537840341699,
205                                                  PositionAngleType.TRUE,
206                                                  earthFrame,
207                                                  initDate,
208                                                  3.986004415E14);
209 
210         final SpacecraftState state = new SpacecraftState(orbit, 1000.0);
211 
212         final AuxiliaryElements auxiliaryElements = new AuxiliaryElements(state.getOrbit(), 1);
213 
214         // Central Body geopotential 32x32
215         final UnnormalizedSphericalHarmonicsProvider provider =
216                 GravityFieldFactory.getUnnormalizedProvider(32, 32);
217 
218         // Zonal force model
219         final DSSTZonal zonal = new DSSTZonal(provider, 32, 4, 65);
220         zonal.initializeShortPeriodTerms(auxiliaryElements, PropagationType.MEAN, zonal.getParameters(orbit.getDate()));
221 
222         // Zonal force model with default constructor
223         final DSSTZonal zonalDefault = new DSSTZonal(provider);
224         zonalDefault.initializeShortPeriodTerms(auxiliaryElements, PropagationType.MEAN, zonalDefault.getParameters(orbit.getDate()));
225 
226         // Compute mean element rate for the zonal force model
227         final double[] elements = zonal.getMeanElementRate(state, auxiliaryElements, zonal.getParameters(orbit.getDate()));
228 
229         // Compute mean element rate for the "default" zonal force model
230         final double[] elementsDefault = zonalDefault.getMeanElementRate(state, auxiliaryElements, zonalDefault.getParameters(orbit.getDate()));
231 
232         // Verify
233         for (int i = 0; i < 6; i++) {
234             Assertions.assertEquals(elements[i], elementsDefault[i], Double.MIN_VALUE);
235         }
236 
237     }
238 
239     @Test
240     void testOutOfRangeException() {
241         try {
242             @SuppressWarnings("unused")
243             final DSSTZonal zonal = new DSSTZonal(GravityFieldFactory.getUnnormalizedProvider(1, 0));
244             Assertions.fail("An exception should have been thrown");
245         } catch (OrekitException oe) {
246             Assertions.assertEquals(LocalizedCoreFormats.OUT_OF_RANGE_SIMPLE, oe.getSpecifier());
247         }
248     }
249     
250     /** Test for issue 1104.
251      * <p>Only J2 is used
252      * <p>Comparisons to a numerical propagator are done, with different frames as "body-fixed frames": GCRF, ITRF, TOD
253      */
254     @Test
255     void testIssue1104() {
256         
257         final boolean printResults = false;
258         
259         // Frames
260         final Frame gcrf = FramesFactory.getGCRF();
261         final Frame tod = FramesFactory.getTOD(IERSConventions.IERS_2010, true);
262         final Frame itrf = FramesFactory.getITRF(IERSConventions.IERS_2010, true);
263         
264         // GCRF/GCRF test
265         // ---------
266         
267         // Using GCRF as both inertial and body frame (behaviour before the fix)
268         double diMax  = 9.615e-5;
269         double dOmMax = 3.374e-3;
270         double dLmMax = 1.128e-2;
271         doTestIssue1104(gcrf, gcrf, printResults, diMax, dOmMax, dLmMax);
272         
273         // TOD/TOD test
274         // --------
275         
276         // Before fix, using TOD was the best choice to reduce the errors DSST vs Numerical
277         // INC is one order of magnitude better compared to GCRF/GCRF (and not diverging anymore but it's not testable here)
278         // RAAN and LM are only slightly better
279         diMax  = 1.059e-5;
280         dOmMax = 2.789e-3;
281         dLmMax = 1.040e-2;
282         doTestIssue1104(tod, tod, printResults, diMax, dOmMax, dLmMax);
283         
284         // GCRF/ITRF test
285         // ---------
286         
287         // Using ITRF as body-fixed frame and GCRF as inertial frame
288         // Results are on par with TOD/TOD
289         diMax  = 1.067e-5;
290         dOmMax = 2.789e-3;
291         dLmMax = 1.040e-2;
292         doTestIssue1104(gcrf, itrf, printResults, diMax, dOmMax, dLmMax);
293         
294         // GCRF/TOD test
295         // ---------
296         
297         // Using TOD as body-fixed frame and GCRF as inertial frame
298         // Results are identical to TOD/TOD
299         diMax  = 1.059e-5;
300         dOmMax = 2.789e-3;
301         dLmMax = 1.040e-2;
302         doTestIssue1104(tod, itrf, printResults, diMax, dOmMax, dLmMax);
303         
304         // Since ITRF is longer to compute, if another inertial frame than TOD is used,
305         // the best balance performance vs accuracy is to use TOD as body-fixed frame
306     }
307 
308     /** Implements the comparison between DSST osculating and numerical. */
309     private void doTestIssue1104(final Frame inertialFrame,
310                                  final Frame bodyFixedFrame,
311                                  final boolean printResults,
312                                  final double diMax,
313                                  final double dOmMax,
314                                  final double dLmMax) {
315         
316         // GIVEN
317         // -----
318         
319         // Parameters
320         final double step = 60.;
321         final double nOrb = 50.;
322         
323         final AbsoluteDate t0 = new AbsoluteDate();
324         
325         // Frames
326         final Frame itrf = FramesFactory.getITRF(IERSConventions.IERS_2010, true);
327         
328         // Potential coefficients providers
329         final int degree = 2;
330         final int order = 0;
331         final UnnormalizedSphericalHarmonicsProvider unnormalized =
332                         GravityFieldFactory.getConstantUnnormalizedProvider(degree, order, t0);
333         final NormalizedSphericalHarmonicsProvider normalized =
334                         GravityFieldFactory.getConstantNormalizedProvider(degree, order, t0);
335 
336         // Initial LEO osculating orbit
337         final double mass = 150.;
338         final double a  = 6906780.35;
339         final double ex = 5.09E-4;
340         final double ey = 1.24e-3;
341         final double i  = FastMath.toRadians(97.49);
342         final double raan   = FastMath.toRadians(-94.607);
343         final double alphaM = FastMath.toRadians(0.);
344         final CircularOrbit oscCircOrbit0 = new CircularOrbit(a, ex, ey, i, raan, alphaM, PositionAngleType.MEAN,
345                                                               inertialFrame, t0, unnormalized.getMu());
346         
347         final Orbit oscOrbit0 = new EquinoctialOrbit(oscCircOrbit0);
348         final SpacecraftState oscState0 = new SpacecraftState(oscOrbit0, mass);
349         final AttitudeProvider attProvider = new FrameAlignedProvider(inertialFrame);
350 
351         // Propagation duration
352         final double duration = nOrb * oscOrbit0.getKeplerianPeriod();
353         final AbsoluteDate tf = t0.shiftedBy(duration);
354         
355         // Numerical prop
356         final ClassicalRungeKuttaIntegrator integrator = new ClassicalRungeKuttaIntegrator(step);
357 
358         final NumericalPropagator numProp = new NumericalPropagator(integrator);
359         numProp.setOrbitType(oscOrbit0.getType());
360         numProp.setInitialState(oscState0);
361         numProp.setAttitudeProvider(attProvider);
362         numProp.addForceModel(new HolmesFeatherstoneAttractionModel(itrf, normalized)); // J2-only gravity field
363         final EphemerisGenerator numEphemGen = numProp.getEphemerisGenerator();
364 
365         // DSST prop: max step could be much higher but made explicitly equal to numerical to rule out a step difference
366         final ClassicalRungeKuttaIntegrator integratorDsst = new ClassicalRungeKuttaIntegrator(step);
367         final DSSTPropagator dsstProp = new DSSTPropagator(integratorDsst, PropagationType.OSCULATING);
368         dsstProp.setInitialState(oscState0, PropagationType.OSCULATING); // Initial state is OSCULATING
369         dsstProp.setAttitudeProvider(attProvider);
370         final DSSTForceModel zonal = new DSSTZonal(bodyFixedFrame, unnormalized); // J2-only with custom Earth-fixed frame
371         dsstProp.addForceModel(zonal);
372         final EphemerisGenerator dsstEphemGen = dsstProp.getEphemerisGenerator();
373         
374         // WHEN
375         // ----
376         
377         // Statistics containers: compare on INC, RAAN and anomaly since that's where there is
378         // improvement brought by fixing 1104. The in-plane parameters (a, ex, ey) are almost equal
379         final StreamingStatistics dI  = new StreamingStatistics();
380         final StreamingStatistics dOm = new StreamingStatistics();
381         final StreamingStatistics dLM = new StreamingStatistics();
382 
383         // Propagate and get ephemeris
384         numProp.propagate(t0, tf);
385         dsstProp.propagate(t0, tf);
386         
387         final BoundedPropagator numEphem  = numEphemGen.getGeneratedEphemeris();
388         final BoundedPropagator dsstEphem = dsstEphemGen.getGeneratedEphemeris();
389         
390         // Compare and fill statistics
391         for (double dt = 0; dt < duration; dt += step) {
392 
393             // Date
394             final AbsoluteDate t = t0.shiftedBy(dt);
395 
396             // Orbits and comparison
397             final CircularOrbit num  = new CircularOrbit(numEphem.propagate(t).getOrbit());
398             final CircularOrbit dsst = new CircularOrbit(dsstEphem.propagate(t).getOrbit());
399             dI.addValue(FastMath.toDegrees(dsst.getI() - num.getI()));
400             dOm.addValue(FastMath.toDegrees(dsst.getRightAscensionOfAscendingNode() - num.getRightAscensionOfAscendingNode()));
401             dLM.addValue(FastMath.toDegrees(dsst.getLM() - num.getLM()));
402         }
403         
404         // THEN
405         // ----
406         
407         // Optional: print the statistics
408         if (printResults) {
409             System.out.println("Inertial frame  : " + inertialFrame.toString());
410             System.out.println("Body-Fixed frame: " + bodyFixedFrame.toString());
411             System.out.println("\ndi\n" + dI.toString());
412             System.out.println("\ndΩ\n" + dOm.toString());
413             System.out.println("\ndLM\n" + dLM.toString());
414         }
415         
416         // Compare to reference
417         Assertions.assertEquals(diMax, FastMath.max(FastMath.abs(dI.getMax()), FastMath.abs(dI.getMin())), 1.e-8);
418         Assertions.assertEquals(dOmMax, FastMath.max(FastMath.abs(dOm.getMax()), FastMath.abs(dOm.getMin())), 1.e-6);
419         Assertions.assertEquals(dLmMax, FastMath.max(FastMath.abs(dLM.getMax()), FastMath.abs(dLM.getMin())), 1.e-5);
420     }
421 
422     @BeforeEach
423     public void setUp() throws IOException, ParseException {
424         Utils.setDataRoot("regular-data:potential/shm-format");
425     }
426 
427 }