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.estimation;
18  
19  import org.hipparchus.CalculusFieldElement;
20  import org.hipparchus.geometry.euclidean.threed.FieldRotation;
21  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
22  import org.hipparchus.geometry.euclidean.threed.Rotation;
23  import org.hipparchus.geometry.euclidean.threed.RotationConvention;
24  import org.hipparchus.geometry.euclidean.threed.Vector3D;
25  import org.hipparchus.linear.MatrixUtils;
26  import org.hipparchus.linear.RealMatrix;
27  import org.hipparchus.optim.nonlinear.vector.leastsquares.LeastSquaresOptimizer.Optimum;
28  import org.hipparchus.util.FastMath;
29  import org.junit.jupiter.api.Assertions;
30  import org.orekit.Utils;
31  import org.orekit.bodies.CelestialBodyFactory;
32  import org.orekit.bodies.OneAxisEllipsoid;
33  import org.orekit.estimation.leastsquares.BatchLSEstimator;
34  import org.orekit.estimation.measurements.EstimatedMeasurement;
35  import org.orekit.estimation.measurements.GroundStation;
36  import org.orekit.estimation.measurements.MeasurementCreator;
37  import org.orekit.estimation.measurements.ObservedMeasurement;
38  import org.orekit.estimation.sequential.KalmanEstimator;
39  import org.orekit.frames.EOPHistory;
40  import org.orekit.frames.FieldTransform;
41  import org.orekit.frames.Frame;
42  import org.orekit.frames.FramesFactory;
43  import org.orekit.frames.Transform;
44  import org.orekit.frames.TransformProvider;
45  import org.orekit.models.earth.displacement.StationDisplacement;
46  import org.orekit.models.earth.displacement.TidalDisplacement;
47  import org.orekit.orbits.Orbit;
48  import org.orekit.orbits.PositionAngleType;
49  import org.orekit.propagation.Propagator;
50  import org.orekit.propagation.analytical.tle.TLE;
51  import org.orekit.propagation.analytical.tle.TLEPropagator;
52  import org.orekit.propagation.conversion.PropagatorBuilder;
53  import org.orekit.time.AbsoluteDate;
54  import org.orekit.time.FieldAbsoluteDate;
55  import org.orekit.time.TimeScalesFactory;
56  import org.orekit.utils.Constants;
57  import org.orekit.utils.IERSConventions;
58  import org.orekit.utils.ParameterDriver;
59  
60  import java.util.Arrays;
61  import java.util.Collections;
62  import java.util.HashMap;
63  import java.util.List;
64  import java.util.Map;
65  
66  /** Utility class for orbit determination tests. */
67  public class TLEEstimationTestUtils {
68  
69      public static TLEContext eccentricContext(final String dataRoot) {
70  
71          Utils.setDataRoot(dataRoot);
72          TLEContext context = new TLEContext();
73          context.conventions = IERSConventions.IERS_2010;
74          context.earth = new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
75                                               Constants.WGS84_EARTH_FLATTENING,
76                                               FramesFactory.getITRF(context.conventions, true));
77          context.sun  = CelestialBodyFactory.getSun();
78          context.moon = CelestialBodyFactory.getMoon();
79          final EOPHistory eopHistory = FramesFactory.getEOPHistory(context.conventions, true);
80          context.utc = TimeScalesFactory.getUTC();
81          context.ut1 = TimeScalesFactory.getUT1(eopHistory);
82          context.displacements = new StationDisplacement[] {
83              new TidalDisplacement(Constants.EIGEN5C_EARTH_EQUATORIAL_RADIUS,
84                                    Constants.JPL_SSD_SUN_EARTH_PLUS_MOON_MASS_RATIO,
85                                    Constants.JPL_SSD_EARTH_MOON_MASS_RATIO,
86                                    context.sun, context.moon,
87                                    context.conventions, false)
88          };
89  
90          String line1 = "1 07276U 74026A   00055.48318287  .00000000  00000-0  22970+3 0  9994";
91          String line2 = "2 07276  71.6273  78.7838 1248323  14.0598   3.8405  4.72707036231812";
92          context.initialTLE = new TLE(line1, line2);
93  
94          context.stations = Arrays.asList(//context.createStation(-18.59146, -173.98363,   76.0, "Leimatu`a"),
95                                           context.createStation(-53.05388,  -75.01551, 1750.0, "Isla Desolación"),
96                                           context.createStation( 62.29639,   -7.01250,  880.0, "Slættaratindur")
97                                           //context.createStation( -4.01583,  103.12833, 3173.0, "Gunung Dempo")
98                          );
99  
100         // Turn-around range stations
101         // Map entry = primary station
102         // Map value = secondary station associated
103         context.TARstations = new HashMap<GroundStation, GroundStation>();
104 
105         context.TARstations.put(context.createStation(-53.05388,  -75.01551, 1750.0, "Isla Desolación"),
106                                 context.createStation(-54.815833,  -68.317778, 6.0, "Ushuaïa"));
107 
108         context.TARstations.put(context.createStation( 62.29639,   -7.01250,  880.0, "Slættaratindur"),
109                                 context.createStation( 61.405833,   -6.705278,  470.0, "Sumba"));
110 
111         return context;
112 
113     }
114 
115     public static TLEContext geoStationnaryContext(final String dataRoot) {
116 
117         Utils.setDataRoot(dataRoot);
118         TLEContext context = new TLEContext();
119         context.conventions = IERSConventions.IERS_2010;
120         context.utc = TimeScalesFactory.getUTC();
121         context.ut1 = TimeScalesFactory.getUT1(context.conventions, true);
122         context.displacements = new StationDisplacement[0];
123         String Myframename = "MyEarthFrame";
124         final AbsoluteDate datedef = new AbsoluteDate(2000, 1, 1, 12, 0, 0.0, context.utc);
125         final double omega = Constants.WGS84_EARTH_ANGULAR_VELOCITY;
126         final Vector3D rotationRate = new Vector3D(0.0, 0.0, omega);
127 
128         TransformProvider MyEarthFrame = new TransformProvider() {
129             public Transform getTransform(final AbsoluteDate date) {
130                 final double rotationduration = date.durationFrom(datedef);
131                 final Vector3D alpharot = new Vector3D(rotationduration, rotationRate);
132                 final Rotation rotation = new Rotation(Vector3D.PLUS_K, -alpharot.getZ(),
133                                                        RotationConvention.VECTOR_OPERATOR);
134                 return new Transform(date, rotation, rotationRate);
135             }
136             public <T extends CalculusFieldElement<T>> FieldTransform<T> getTransform(final FieldAbsoluteDate<T> date) {
137                 final T rotationduration = date.durationFrom(datedef);
138                 final FieldVector3D<T> alpharot = new FieldVector3D<>(rotationduration, rotationRate);
139                 final FieldRotation<T> rotation = new FieldRotation<>(FieldVector3D.getPlusK(date.getField()),
140                                                                       alpharot.getZ().negate(),
141                                                                       RotationConvention.VECTOR_OPERATOR);
142                 return new FieldTransform<>(date, rotation, new FieldVector3D<>(date.getField(), rotationRate));
143             }
144         };
145         Frame FrameTest = new Frame(FramesFactory.getEME2000(), MyEarthFrame, Myframename, true);
146 
147         // Earth is spherical, rotating in one sidereal day
148         context.earth = new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS, 0.0, FrameTest);
149         context.sun   = CelestialBodyFactory.getSun();
150         context.moon  = CelestialBodyFactory.getMoon();
151 
152         //context.stations = Arrays.asList(context.createStation(  0.0,  0.0, 0.0, "Lat0_Long0"),
153         //                                 context.createStation( 62.29639,   -7.01250,  880.0, "Slættaratindur")
154         //                );
155         context.stations = Collections.singletonList(context.createStation(0.0, 0.0, 0.0, "Lat0_Long0") );
156 
157         // TLE of GEOS-10 from near J2000 epoch
158         String line1 = "1 24786U 97019A   00002.84035656  .00000093  00000-0  00000-0 0  4663";
159         String line2 = "2 24786   0.0023 170.4335 0003424 130.3470 328.3614  1.00279571  9860";
160         final TLE tle = new TLE(line1, line2);
161 
162         // Geo-stationary Satellite Orbit, tightly above the station (l0-L0)
163         context.initialTLE = tle;
164 
165         context.stations = Collections.singletonList(context.createStation(10.0, 45.0, 0.0, "Lat10_Long45") );
166 
167         // Turn-around range stations
168         // Map entry = primary station
169         // Map value = secondary station associated
170         context.TARstations = new HashMap<GroundStation, GroundStation>();
171 
172         context.TARstations.put(context.createStation(  41.977, 13.600,  671.354, "Fucino"),
173                                 context.createStation(  43.604,  1.444,  263.0  , "ToulouMEANse"));
174 
175         context.TARstations.put(context.createStation(  49.867,  8.65 ,  144.0  , "Darmstadt"),
176                                 context.createStation( -25.885, 27.707, 1566.633, "Pretoria"));
177 
178         return context;
179 
180     }
181 
182     public static Propagator createPropagator(final Orbit initialOrbit,
183                                               final PropagatorBuilder propagatorBuilder) {
184 
185         // override orbital parameters
186         double[] orbitArray = new double[6];
187         initialOrbit.getType().mapOrbitToArray(initialOrbit, PositionAngleType.MEAN, orbitArray, null);
188         for (int i = 0; i < orbitArray.length; ++i) {
189         	// here orbital paramaters drivers have only 1 estimated values on the all time period for orbit determination
190             propagatorBuilder.getOrbitalParametersDrivers().getDrivers().get(i).setValue(orbitArray[i], initialOrbit.getDate());
191         }
192 
193         return propagatorBuilder.buildPropagator();
194 
195     }
196 
197     public static List<ObservedMeasurement<?>> createMeasurements(final Propagator propagator,
198                                                                   final MeasurementCreator creator,
199                                                                   final double startPeriod, final double endPeriod,
200                                                                   final double step) {
201 
202         propagator.setStepHandler(step, creator);
203         final double       period = propagator.getInitialState().getOrbit().getKeplerianPeriod();
204         final AbsoluteDate start  = propagator.getInitialState().getDate().shiftedBy(startPeriod * period);
205         final AbsoluteDate end    = propagator.getInitialState().getDate().shiftedBy(endPeriod   * period);
206         propagator.propagate(start, end);
207 
208         final List<ObservedMeasurement<?>> measurements = creator.getMeasurements();
209 
210         for (final ObservedMeasurement<?> measurement : measurements) {
211             for (final ParameterDriver driver : measurement.getParametersDrivers()) {
212                 if (driver.getReferenceDate() == null) {
213                     driver.setReferenceDate(propagator.getInitialState().getDate());
214                 }
215             }
216         }
217 
218         return measurements;
219 
220     }
221 
222     /**
223      * Checker for batch LS estimator validation
224      * @param context DSSTContext used for the test
225      * @param estimator Batch LS estimator
226      * @param iterations Number of iterations expected
227      * @param evaluations Number of evaluations expected
228      * @param expectedRMS Expected RMS value
229      * @param rmsEps Tolerance on expected RMS
230      * @param expectedMax Expected weighted residual maximum
231      * @param maxEps Tolerance on weighted residual maximum
232      * @param expectedDeltaPos Expected position difference between estimated orbit and initial orbit
233      * @param posEps Tolerance on expected position difference
234      * @param expectedDeltaVel Expected velocity difference between estimated orbit and initial orbit
235      * @param velEps Tolerance on expected velocity difference
236      */
237     public static void checkFit(final TLEContext context, final BatchLSEstimator estimator,
238                                 final int iterations, final int evaluations,
239                                 final double expectedRMS,      final double rmsEps,
240                                 final double expectedMax,      final double maxEps,
241                                 final double expectedDeltaPos, final double posEps,
242                                 final double expectedDeltaVel, final double velEps) {
243 
244         final Orbit initialOrbit = TLEPropagator.selectExtrapolator(context.initialTLE).getInitialState().getOrbit();
245         final Orbit estimatedOrbit = estimator.estimate()[0].getInitialState().getOrbit();
246         final Vector3D estimatedPosition = estimatedOrbit.getPosition();
247         final Vector3D estimatedVelocity = estimatedOrbit.getVelocity();
248 
249         int    k   = 0;
250         double sum = 0;
251         double max = 0;
252         for (final Map.Entry<ObservedMeasurement<?>, EstimatedMeasurement<?>> entry :
253              estimator.getLastEstimations().entrySet()) {
254             final ObservedMeasurement<?>  m = entry.getKey();
255             final EstimatedMeasurement<?> e = entry.getValue();
256             final double[]    weight      = m.getBaseWeight();
257             final double[]    sigma       = m.getTheoreticalStandardDeviation();
258             final double[]    observed    = m.getObservedValue();
259             final double[]    theoretical = e.getEstimatedValue();
260             for (int i = 0; i < m.getDimension(); ++i) {
261                 final double weightedResidual = weight[i] * (theoretical[i] - observed[i]) / sigma[i];
262                 ++k;
263                 sum += weightedResidual * weightedResidual;
264                 max = FastMath.max(max, FastMath.abs(weightedResidual));
265             }
266         }
267         final double rms = FastMath.sqrt(sum / k);
268         final double deltaPos =
269                 Vector3D.distance(initialOrbit.getPosition(), estimatedPosition);
270         final double deltaVel =
271                 Vector3D.distance(initialOrbit.getVelocity(), estimatedVelocity);
272 
273         boolean print = false;
274         if (print) {
275             System.out.format("%25s %25s %25s %25s\n", "", "Actual", "Expected", "Tolerance");
276             System.out.format("%25s %25d %25d %25d\n", "iterations", estimator.getIterationsCount(), iterations, 0);
277             System.out.format("%25s %25d %25d %25d\n", "evaluations", estimator.getEvaluationsCount(), evaluations, 0);
278             System.out.format("%25s %25e %25e %25e\n", "RMS", rms, expectedRMS, rmsEps);
279             System.out.format("%25s %25e %25e %25e\n", "DeltaPos", deltaPos, expectedDeltaPos, posEps);
280             System.out.format("%25s %25e %25e %25e\n", "DeltaVel", deltaVel, expectedDeltaVel, velEps);
281         }
282 
283         Assertions.assertEquals(iterations, estimator.getIterationsCount());
284         Assertions.assertEquals(evaluations, estimator.getEvaluationsCount());
285         Optimum optimum = estimator.getOptimum();
286         Assertions.assertEquals(iterations, optimum.getIterations());
287         Assertions.assertEquals(evaluations, optimum.getEvaluations());
288         Assertions.assertEquals(expectedRMS,
289                             rms,
290                             rmsEps);
291         Assertions.assertEquals(expectedMax,
292                             max,
293                             maxEps);
294         Assertions.assertEquals(expectedDeltaPos,
295                             deltaPos,
296                             posEps);
297         Assertions.assertEquals(expectedDeltaVel,
298                             deltaVel,
299                             velEps);
300 
301     }
302 
303     /**
304      * Checker for Kalman estimator validation
305      * @param context context used for the test
306      * @param kalman Kalman filter
307      * @param measurements List of observed measurements to be processed by the Kalman
308      * @param refOrbit Reference orbits at last measurement date
309      * @param expectedDeltaPos Expected position difference between estimated orbit and reference orbit
310      * @param posEps Tolerance on expected position difference
311      * @param expectedDeltaVel Expected velocity difference between estimated orbit and reference orbit
312      * @param velEps Tolerance on expected velocity difference
313      */
314     public static void checkKalmanFit(final TLEContext context, final KalmanEstimator kalman,
315                                       final List<ObservedMeasurement<?>> measurements,
316                                       final Orbit refOrbit, final PositionAngleType positionAngleType,
317                                       final double expectedDeltaPos, final double posEps,
318                                       final double expectedDeltaVel, final double velEps) {
319         checkKalmanFit(context, kalman, measurements,
320                        new Orbit[] { refOrbit },
321                        new PositionAngleType[] {positionAngleType},
322                        new double[] { expectedDeltaPos }, new double[] { posEps },
323                        new double[] { expectedDeltaVel }, new double[] { velEps });
324     }
325 
326     /**
327      * Checker for Kalman estimator validation
328      * @param context context used for the test
329      * @param kalman Kalman filter
330      * @param measurements List of observed measurements to be processed by the Kalman
331      * @param refOrbit Reference orbits at last measurement date
332      * @param expectedDeltaPos Expected position difference between estimated orbit and reference orbits
333      * @param posEps Tolerance on expected position difference
334      * @param expectedDeltaVel Expected velocity difference between estimated orbit and reference orbits
335      * @param velEps Tolerance on expected velocity difference
336      */
337     public static void checkKalmanFit(final TLEContext context, final KalmanEstimator kalman,
338                                       final List<ObservedMeasurement<?>> measurements,
339                                       final Orbit[] refOrbit, final PositionAngleType[] positionAngleType,
340                                       final double[] expectedDeltaPos, final double[] posEps,
341                                       final double[] expectedDeltaVel, final double []velEps) {
342 
343         // Add the measurements to the Kalman filter
344         Propagator[] estimated = kalman.processMeasurements(measurements);
345 
346         // Check the number of measurements processed by the filter
347         Assertions.assertEquals(measurements.size(), kalman.getCurrentMeasurementNumber());
348 
349         for (int k = 0; k < refOrbit.length; ++k) {
350             // Get the last estimation
351             final Orbit    estimatedOrbit    = estimated[k].getInitialState().getOrbit();
352             final Vector3D estimatedPosition = estimatedOrbit.getPosition();
353             final Vector3D estimatedVelocity = estimatedOrbit.getVelocity();
354 
355             // Get the last covariance matrix estimation
356             final RealMatrix estimatedP = kalman.getPhysicalEstimatedCovarianceMatrix();
357 
358             // Convert the orbital part to Cartesian formalism
359             // Assuming all 6 orbital parameters are estimated by the filter
360             final double[][] dCdY = new double[6][6];
361             estimatedOrbit.getJacobianWrtParameters(positionAngleType[k], dCdY);
362             final RealMatrix Jacobian = MatrixUtils.createRealMatrix(dCdY);
363             final RealMatrix estimatedCartesianP =
364                             Jacobian.
365                             multiply(estimatedP.getSubMatrix(0, 5, 0, 5)).
366                             multiply(Jacobian.transpose());
367 
368             // Get the final sigmas (ie.sqrt of the diagonal of the Cartesian orbital covariance matrix)
369             final double[] sigmas = new double[6];
370             for (int i = 0; i < 6; i++) {
371                 sigmas[i] = FastMath.sqrt(estimatedCartesianP.getEntry(i, i));
372             }
373 //          // FIXME: debug print values
374 //          final double dPos = Vector3D.distance(refOrbit[k].getPosition(), estimatedPosition);
375 //          final double dVel = Vector3D.distance(refOrbit[k].getVelocity(), estimatedVelocity);
376 //          System.out.println("Nb Meas = " + kalman.getCurrentMeasurementNumber());
377 //          System.out.println("dPos    = " + dPos + " m");
378 //          System.out.println("dVel    = " + dVel + " m/s");
379 //          System.out.println("sigmas  = " + sigmas[0] + " "
380 //                          + sigmas[1] + " "
381 //                          + sigmas[2] + " "
382 //                          + sigmas[3] + " "
383 //                          + sigmas[4] + " "
384 //                          + sigmas[5]);
385 //          //debug
386 
387             // Check the final orbit estimation & PV sigmas
388             final double deltaPosK = Vector3D.distance(refOrbit[k].getPosition(), estimatedPosition);
389             final double deltaVelK = Vector3D.distance(refOrbit[k].getVelocity(), estimatedVelocity);
390             Assertions.assertEquals(expectedDeltaPos[k], deltaPosK, posEps[k]);
391             Assertions.assertEquals(expectedDeltaVel[k], deltaVelK, velEps[k]);
392 
393         }
394     }
395 }