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.sequential;
18  
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  
23  import org.hipparchus.geometry.euclidean.threed.Vector3D;
24  import org.hipparchus.linear.LUDecomposition;
25  import org.hipparchus.linear.MatrixUtils;
26  import org.hipparchus.linear.RealMatrix;
27  import org.hipparchus.linear.RealVector;
28  import org.hipparchus.util.MerweUnscentedTransform;
29  import org.hipparchus.util.UnscentedTransformProvider;
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.estimation.Context;
34  import org.orekit.estimation.EstimationTestUtils;
35  import org.orekit.estimation.Force;
36  import org.orekit.estimation.measurements.GroundStation;
37  import org.orekit.estimation.measurements.ObservableSatellite;
38  import org.orekit.estimation.measurements.ObservedMeasurement;
39  import org.orekit.estimation.measurements.PV;
40  import org.orekit.estimation.measurements.Range;
41  import org.orekit.estimation.measurements.modifiers.Bias;
42  import org.orekit.forces.maneuvers.ConstantThrustManeuver;
43  import org.orekit.forces.radiation.RadiationSensitive;
44  import org.orekit.orbits.Orbit;
45  import org.orekit.orbits.OrbitType;
46  import org.orekit.orbits.PositionAngleType;
47  import org.orekit.propagation.SpacecraftState;
48  import org.orekit.propagation.conversion.NumericalPropagatorBuilder;
49  import org.orekit.time.AbsoluteDate;
50  import org.orekit.utils.PVCoordinates;
51  import org.orekit.utils.ParameterDriver;
52  import org.orekit.utils.ParameterDriversList;
53  
54  public class UnscentedKalmanModelTest {
55  
56      /** Orbit type for propagation. */
57      private final OrbitType orbitType = OrbitType.CARTESIAN;
58  
59      /** Position angle for propagation. */
60      private final PositionAngleType positionAngleType = PositionAngleType.TRUE;
61  
62      /** Initial orbit. */
63      private Orbit orbit0;
64  
65      /** Propagator builder. */
66      private NumericalPropagatorBuilder propagatorBuilder;
67  
68      /** Covariance matrix provider. */
69      private CovarianceMatrixProvider covMatrixProvider;
70  
71      /** Estimated measurement parameters list. */
72      private ParameterDriversList estimatedMeasurementsParameters;
73  
74      /** Kalman estimator containing models. */
75      private UnscentedKalmanEstimator kalman;
76  
77      /** Unscented transform provider. */
78      private UnscentedTransformProvider utProvider;
79  
80      /** Kalman observer. */
81      private ModelLogger modelLogger;
82  
83      /** State size. */
84      private int M;
85  
86      /** PV at t0. */
87      private PV pv;
88  
89      /** Range after t0. */
90      private Range range;
91  
92      /** Driver for satellite range bias. */
93      private ParameterDriver satRangeBiasDriver;
94  
95      /** Driver for SRP coefficient. */
96      private ParameterDriver srpCoefDriver;
97  
98      /** Tolerance for the test. */
99      private final double tol = 1.0e-16;
100 
101     @BeforeEach
102     public void setup() {
103         // Create context
104         final Context context = EstimationTestUtils.eccentricContext("regular-data:potential:tides");
105 
106         // Initial orbit and date
107         this.orbit0 = context.initialOrbit;
108         ObservableSatellite sat = new ObservableSatellite(0);
109 
110         // Create propagator builder
111         this.propagatorBuilder = context.createBuilder(orbitType, positionAngleType, true,
112                                                        1.0e-6, 60.0, 10., Force.SOLAR_RADIATION_PRESSURE);
113 
114         // Create PV at t0
115         final AbsoluteDate date0 = context.initialOrbit.getDate();
116         this.pv = new PV(date0,
117                              context.initialOrbit.getPosition(),
118                              context.initialOrbit.getPVCoordinates().getVelocity(),
119                              new double[] {1., 2., 3., 1e-3, 2e-3, 3e-3}, 1.,
120                              sat);
121 
122         // Create one 0m range measurement at t0 + 10s
123         final AbsoluteDate date  = date0.shiftedBy(10.);
124         final GroundStation station = context.stations.get(0);
125         this.range = new Range(station, true, date, 18616150., 10., 1., sat);
126         // Exact range value is 1.8616150246470984E7 m
127 
128         // Add sat range bias to PV and select it
129         final Bias<Range> satRangeBias = new Bias<Range>(new String[] {"sat range bias"},
130                                                          new double[] {100.},
131                                                          new double[] {10.},
132                                                          new double[] {0.},
133                                                          new double[] {150.});
134         this.satRangeBiasDriver = satRangeBias.getParametersDrivers().get(0);
135         satRangeBiasDriver.setSelected(true);
136         satRangeBiasDriver.setReferenceDate(date);
137         range.addModifier(satRangeBias);
138         for (ParameterDriver driver : range.getParametersDrivers()) {
139             driver.setReferenceDate(date);
140         }
141 
142         // Gather list of meas parameters (only sat range bias here)
143         this.estimatedMeasurementsParameters = new ParameterDriversList();
144         for (final ParameterDriver driver : range.getParametersDrivers()) {
145             if (driver.isSelected()) {
146                 estimatedMeasurementsParameters.add(driver);
147             }
148         }
149         // Select SRP coefficient
150         this.srpCoefDriver = propagatorBuilder.getPropagationParametersDrivers().
151                         findByName(RadiationSensitive.REFLECTION_COEFFICIENT);
152         srpCoefDriver.setReferenceDate(date);
153         srpCoefDriver.setSelected(true);
154 
155         // Create a covariance matrix using the scales of the estimated parameters
156         final double[] scales = getParametersScale(propagatorBuilder, estimatedMeasurementsParameters);
157         this.M = scales.length;
158         this.covMatrixProvider = setInitialCovarianceMatrix(scales);
159 
160         // Unscented  transform provider
161         this.utProvider = new MerweUnscentedTransform(8);
162 
163         // Initialize Kalman
164         final UnscentedKalmanEstimatorBuilder kalmanBuilder = new UnscentedKalmanEstimatorBuilder();
165         kalmanBuilder.unscentedTransformProvider(utProvider);
166         kalmanBuilder.addPropagationConfiguration(propagatorBuilder, covMatrixProvider);
167         kalmanBuilder.estimatedMeasurementsParameters(estimatedMeasurementsParameters, null);
168         this.kalman = kalmanBuilder.build();
169         this.modelLogger = new ModelLogger();
170         kalman.setObserver(modelLogger);
171     }
172 
173     /** Test of the physical matrices and vectors returned by the methods from KalmanEstimation interface.
174      *  First, we perform a check before any measurement is added. Most of the matrices should be null.
175      *  Then we process a perfect PV at t0 in the Kalman and check the matrices.
176      *  Finally we process a range measurement after t0 in the Kalman and check the matrices.
177      */
178     @Test
179     public void ModelPhysicalOutputsTest() {
180 
181         // Check model at t0 before any measurement is added
182         // -------------------------------------------------
183         checkModelAtT0();
184 
185 
186         // Check model after PV measurement at t0 is added
187         // -----------------------------------------------
188 
189         // Constant process noise covariance matrix Q
190         final RealMatrix Q = covMatrixProvider.getProcessNoiseMatrix(new SpacecraftState(orbit0),
191                                                                      new SpacecraftState(orbit0));
192 
193         // Initial covariance matrix
194         final RealMatrix P0 = covMatrixProvider.getInitialCovarianceMatrix(new SpacecraftState(orbit0));
195 
196         // Physical predicted covariance matrix at t0
197         // State transition matrix is the identity matrix at t0
198         RealMatrix Ppred = P0.add(Q);
199 
200         // Predicted orbit is equal to initial orbit at t0
201         Orbit orbitPred = orbit0;
202 
203         // Expected measurement matrix for a PV measurement is the 6-sized identity matrix for cartesian orbital parameters
204         // + zeros for other estimated parameters
205         RealMatrix expH = MatrixUtils.createRealMatrix(6, M);
206         for (int i = 0; i < 6; i++) {
207             expH.setEntry(i, i, 1.);
208         }
209 
210         // Expected state transition matrix
211         // State transition matrix is the identity matrix at t0
212         RealMatrix expPhi = MatrixUtils.createRealIdentityMatrix(M);
213         // Add PV measurement and check model afterwards
214         checkModelAfterMeasurementAdded(1, pv, Ppred, orbitPred, expPhi, expH);
215     }
216 
217     /** Check the model physical outputs at t0 before any measurement is added. */
218     private void checkModelAtT0() {
219 
220         // Instantiate a Model from attributes
221         final UnscentedKalmanModel model = new UnscentedKalmanModel(Collections.singletonList(propagatorBuilder),
222                                                   Collections.singletonList(covMatrixProvider),
223                                                   estimatedMeasurementsParameters,
224                                                   null);
225 
226         // Evaluate at t0
227         // --------------
228 
229         // Time
230         Assertions.assertEquals(0., model.getEstimate().getTime(), 0.);
231         Assertions.assertEquals(0., model.getCurrentDate().durationFrom(orbit0.getDate()), 0.);
232 
233         // Measurement number
234         Assertions.assertEquals(0, model.getCurrentMeasurementNumber());
235 
236         // Normalized state - is zeros
237         final RealVector stateN = model.getEstimate().getState();
238         Assertions.assertArrayEquals(new double[M], stateN.toArray(), tol);
239 
240         // Physical state - = initialized
241         final RealVector x = model.getPhysicalEstimatedState();
242         final RealVector expX = MatrixUtils.createRealVector(M);
243         final double[] orbitState0 = new double[6];
244         orbitType.mapOrbitToArray(orbit0, positionAngleType, orbitState0, null);
245         expX.setSubVector(0, MatrixUtils.createRealVector(orbitState0));
246         expX.setEntry(6, srpCoefDriver.getReferenceValue());
247         expX.setEntry(7, satRangeBiasDriver.getReferenceValue());
248         final double[] dX = x.subtract(expX).toArray();
249         Assertions.assertArrayEquals(new double[M], dX, tol);
250 
251         // Normalized covariance - diagonal
252         final double[][] Pn = model.getEstimate().getCovariance().getData();
253         final double[][] expPn = new double[M][M];
254         for (int i = 0; i < M; i++) {
255             expPn[i][i] = 1.;
256             Assertions.assertArrayEquals(expPn[i], Pn[i], tol, "Failed on line " + i);
257         }
258 
259         // Physical covariance = initialized
260         final RealMatrix P   = model.getPhysicalEstimatedCovarianceMatrix();
261         final RealMatrix expP = covMatrixProvider.getInitialCovarianceMatrix(new SpacecraftState(orbit0));
262         final double[][] dP = P.subtract(expP).getData();
263         for (int i = 0; i < M; i++) {
264             Assertions.assertArrayEquals(new double[M], dP[i], tol, "Failed on line " + i);
265         }
266 
267         // Check that other "physical" matrices are null
268         Assertions.assertNull(model.getEstimate().getInnovationCovariance());
269         Assertions.assertNull(model.getPhysicalInnovationCovarianceMatrix());
270         Assertions.assertNull(model.getEstimate().getKalmanGain());
271         Assertions.assertNull(model.getPhysicalKalmanGain());
272         Assertions.assertNull(model.getEstimate().getMeasurementJacobian());
273         Assertions.assertNull(model.getPhysicalMeasurementJacobian());
274         Assertions.assertNull(model.getEstimate().getStateTransitionMatrix());
275         Assertions.assertNull(model.getPhysicalStateTransitionMatrix());
276     }
277 
278     /** Add a measurement to the Kalman filter.
279      * Check model physical outputs afterwards.
280      */
281     private void checkModelAfterMeasurementAdded(final int expMeasurementNumber,
282                                                 final ObservedMeasurement<?> meas,
283                                                 final RealMatrix expPpred,
284                                                 final Orbit expOrbitPred,
285                                                 final RealMatrix expPhi,
286                                                 final RealMatrix expH) {
287 
288         // Predicted value of SRP coef and sat range bias
289         // (= value before adding measurement to the filter)
290         final double srpCoefPred = srpCoefDriver.getValue();
291         final double satRangeBiasPred = satRangeBiasDriver.getValue();
292 
293         // Expected predicted measurement
294         final double[] expMeasPred =
295                         meas.estimate(0, 0,
296                                       new SpacecraftState[] {new SpacecraftState(expOrbitPred)}).getEstimatedValue();
297 
298         // Process PV measurement in Kalman and get model
299         kalman.processMeasurements(Collections.singletonList(meas));
300         KalmanEstimation model = modelLogger.estimation;
301 
302         // Measurement size
303         final int N = meas.getDimension();
304 
305         // Time
306         Assertions.assertEquals(0., model.getCurrentDate().durationFrom(expOrbitPred.getDate()), 0.);
307 
308         // Measurement number
309         Assertions.assertEquals(expMeasurementNumber, model.getCurrentMeasurementNumber());
310 
311         // State transition matrix (null for unscented kalman filter)
312         Assertions.assertNull(model.getPhysicalStateTransitionMatrix());
313 
314         // Measurement matrix (null for unscented kalman filter)
315         Assertions.assertNull(model.getPhysicalMeasurementJacobian());
316 
317         // Measurement covariance matrix
318         final double[] measSigmas = meas.getTheoreticalStandardDeviation();
319         final RealMatrix R = MatrixUtils.createRealMatrix(N, N);
320         for (int i = 0; i < N; i++) {
321             R.setEntry(i, i, measSigmas[i] * measSigmas[i]);
322         }
323 
324         // Innovation matrix
325         final RealMatrix expS = expH.multiply(expPpred.multiplyTransposed(expH)).add(R);
326         final RealMatrix S = model.getPhysicalInnovationCovarianceMatrix();
327         final double[][] dS = S.subtract(expS).getData();
328         for (int i = 0; i < N; i++) {
329             Assertions.assertArrayEquals(new double[N], dS[i], tol*1e8, "Failed on line \" + i");
330         }
331 
332         // Kalman gain
333         final RealMatrix expK = expPpred.multiplyTransposed(expH).multiply(new LUDecomposition(expS).getSolver().getInverse());
334         final RealMatrix K = model.getPhysicalKalmanGain();
335         final double[][] dK = K.subtract(expK).getData();
336         for (int i = 0; i < M; i++) {
337             Assertions.assertArrayEquals(new double[N], dK[i], tol*1e5, "Failed on line " + i);
338         }
339 
340         // Predicted orbit
341         final Orbit orbitPred = model.getPredictedSpacecraftStates()[0].getOrbit();
342         final PVCoordinates pvOrbitPred = orbitPred.getPVCoordinates();
343         final PVCoordinates expPVOrbitPred = expOrbitPred.getPVCoordinates();
344         final double dpOrbitPred = Vector3D.distance(expPVOrbitPred.getPosition(), pvOrbitPred.getPosition());
345         final double dvOrbitPred = Vector3D.distance(expPVOrbitPred.getVelocity(), pvOrbitPred.getVelocity());
346         Assertions.assertEquals(0., dpOrbitPred, tol);
347         Assertions.assertEquals(0., dvOrbitPred, tol);
348 
349         // Predicted measurement
350         final double[] measPred = model.getPredictedMeasurement().getEstimatedValue();
351         Assertions.assertArrayEquals(expMeasPred, measPred, 1e-6);
352 
353         // Predicted state
354         final double[] orbitPredState = new double[6];
355         orbitPred.getType().mapOrbitToArray(orbitPred, PositionAngleType.TRUE, orbitPredState, null);
356         final RealVector expXpred = MatrixUtils.createRealVector(M);
357         for (int i = 0; i < 6; i++) {
358             expXpred.setEntry(i, orbitPredState[i]);
359         }
360         expXpred.setEntry(6, srpCoefPred);
361         expXpred.setEntry(7, satRangeBiasPred);
362 
363 
364         // Innovation vector
365         final RealVector observedMeas  = MatrixUtils.createRealVector(model.getPredictedMeasurement().getObservedValue());
366         final RealVector predictedMeas = MatrixUtils.createRealVector(model.getPredictedMeasurement().getEstimatedValue());
367         final RealVector innovation = observedMeas.subtract(predictedMeas);
368 
369         // Corrected state
370         final RealVector expectedXcor = expXpred.add(expK.operate(innovation));
371         final RealVector Xcor = model.getPhysicalEstimatedState();
372         final double[] dXcor = Xcor.subtract(expectedXcor).toArray();
373         Assertions.assertArrayEquals(new double[M], dXcor, tol);
374 
375         // Corrected covariance
376         final RealMatrix expectedPcor =
377                         (MatrixUtils.createRealIdentityMatrix(M).
378                         subtract(expK.multiply(expH))).multiply(expPpred);
379         final RealMatrix Pcor = model.getPhysicalEstimatedCovarianceMatrix();
380         final double[][] dPcor = Pcor.subtract(expectedPcor).getData();
381         for (int i = 0; i < M; i++) {
382             Assertions.assertArrayEquals(new double[M], dPcor[i], tol*1e8, "Failed on line " + i);
383         }
384     }
385 
386     /** Get an array of the scales of the estimated parameters.
387      * @param builder propagator builder
388      * @param estimatedMeasurementsParameters estimated measurements parameters
389      * @return array containing the scales of the estimated parameter
390      */
391     private double[] getParametersScale(final NumericalPropagatorBuilder builder,
392                                        ParameterDriversList estimatedMeasurementsParameters) {
393         final List<Double> scaleList = new ArrayList<>();
394 
395         // Orbital parameters
396         for (ParameterDriver driver : builder.getOrbitalParametersDrivers().getDrivers()) {
397             if (driver.isSelected()) {
398                 scaleList.add(driver.getScale());
399             }
400         }
401 
402         // Propagation parameters
403         for (ParameterDriver driver : builder.getPropagationParametersDrivers().getDrivers()) {
404             if (driver.isSelected()) {
405                 scaleList.add(driver.getScale());
406             }
407         }
408 
409         // Measurement parameters
410         for (ParameterDriver driver : estimatedMeasurementsParameters.getDrivers()) {
411             if (driver.isSelected()) {
412                 scaleList.add(driver.getScale());
413             }
414         }
415 
416         final double[] scales = new double[scaleList.size()];
417         for (int i = 0; i < scaleList.size(); i++) {
418             scales[i] = scaleList.get(i);
419         }
420         return scales;
421     }
422 
423     /** Create a covariance matrix provider with initial and process noise matrix constant and identical.
424      * Each diagonal element Pii of the initial covariance matrix equals:
425      * Pii = scales[i]*scales[i]
426      * @param scales scales of the estimated parameters
427      * @return covariance matrix provider
428      */
429     private CovarianceMatrixProvider setInitialCovarianceMatrix(final double[] scales) {
430 
431         final int n = scales.length;
432         final RealMatrix cov = MatrixUtils.createRealMatrix(n, n);
433         for (int i = 0; i < n; i++) {
434             cov.setEntry(i, i, scales[i] * scales[i]);
435         }
436         return new ConstantProcessNoise(cov);
437     }
438 
439 
440     /** Observer allowing to get Kalman model after a measurement was processed in the Kalman filter. */
441     public class ModelLogger implements KalmanObserver {
442         KalmanEstimation estimation;
443 
444         @Override
445         public void evaluationPerformed(KalmanEstimation estimation) {
446             this.estimation = estimation;
447         }
448     }
449 
450     /** 
451      * Test that mass depletions during the processing of a measurement are propagated back to the PropagatorBuilder
452      * so that they are taken into account during processing of subsequent measurements.
453      */
454     @Test
455     public void MassDepletionTest()  {
456         // Add a maneuver with nonzero mass expenditure between the first and second measurement dates.
457         AbsoluteDate initialDate = this.propagatorBuilder.getInitialOrbitDate();
458         double initialMass = this.propagatorBuilder.getMass();
459         AbsoluteDate maneuverDate = initialDate.shiftedBy(5.0);
460 
461         ConstantThrustManeuver maneuver = new ConstantThrustManeuver(maneuverDate, 10.0,  10.0, 100.0, new Vector3D(1.0, 0.0, 0.0));
462         this.propagatorBuilder.addForceModel(maneuver);
463     
464         kalman.processMeasurements(Collections.singletonList(pv));
465         kalman.processMeasurements(Collections.singletonList(range));
466         
467         double postManeuverPredictedStateMass = modelLogger.estimation.getPredictedSpacecraftStates()[0].getMass();
468         double postManeuverCorrectedStateMass = modelLogger.estimation.getCorrectedSpacecraftStates()[0].getMass();
469         double postManeuverPropagatorMass = propagatorBuilder.getMass();
470 
471         // The mass of the predicted state should be less than the initial mass, because mass should have been expended during the maneuver.
472         Assertions.assertTrue(postManeuverPredictedStateMass < initialMass);
473 
474         // The mass for the propagator builder should have been updated to be equal to the mass from the 
475         // newly predicted state, which includes the maneuver.
476         Assertions.assertEquals(postManeuverPredictedStateMass, postManeuverPropagatorMass); 
477 
478         // The mass of the corrected state should be equal to the mass from the propagator builder,
479         Assertions.assertEquals(postManeuverCorrectedStateMass, postManeuverPropagatorMass);    
480     }
481 
482 
483 }