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.measurements;
18  
19  import java.util.List;
20  
21  import org.hipparchus.geometry.euclidean.threed.Vector3D;
22  import org.hipparchus.stat.descriptive.StreamingStatistics;
23  import org.hipparchus.stat.descriptive.rank.Median;
24  import org.hipparchus.util.FastMath;
25  import org.junit.jupiter.api.Assertions;
26  import org.junit.jupiter.api.Test;
27  import org.orekit.estimation.Context;
28  import org.orekit.estimation.EstimationTestUtils;
29  import org.orekit.orbits.OrbitType;
30  import org.orekit.orbits.PositionAngleType;
31  import org.orekit.propagation.Propagator;
32  import org.orekit.propagation.SpacecraftState;
33  import org.orekit.propagation.conversion.NumericalPropagatorBuilder;
34  import org.orekit.time.AbsoluteDate;
35  import org.orekit.utils.Differentiation;
36  import org.orekit.utils.ParameterDriver;
37  import org.orekit.utils.ParameterFunction;
38  
39  class AngularAzElTest {
40  
41      /** Compare observed values and estimated values.
42       *  Both are calculated with a different algorithm
43       */
44      @Test
45      void testValues() {
46  
47          Context context = EstimationTestUtils.eccentricContext("regular-data:potential:tides");
48  
49          final NumericalPropagatorBuilder propagatorBuilder =
50                          context.createBuilder(OrbitType.EQUINOCTIAL, PositionAngleType.TRUE, false,
51                                                1.0e-6, 60.0, 0.001);
52  
53          // Create perfect right-ascension/declination measurements
54          final Propagator propagator = EstimationTestUtils.createPropagator(context.initialOrbit,
55                                                                             propagatorBuilder);
56          final List<ObservedMeasurement<?>> measurements =
57                          EstimationTestUtils.createMeasurements(propagator,
58                                                                 new AngularAzElMeasurementCreator(context),
59                                                                 0.25, 3.0, 600.0);
60  
61          propagator.clearStepHandlers();
62  
63          // Prepare statistics for right-ascension/declination values difference
64          final StreamingStatistics azDiffStat = new StreamingStatistics();
65          final StreamingStatistics elDiffStat = new StreamingStatistics();
66  
67          for (final ObservedMeasurement<?> measurement : measurements) {
68  
69              // Propagate to measurement date
70              final AbsoluteDate datemeas  = measurement.getDate();
71              SpacecraftState    state     = propagator.propagate(datemeas);
72  
73              // Estimate the AZEL value
74              final EstimatedMeasurementBase<?> estimated = measurement.estimateWithoutDerivatives(new SpacecraftState[] { state });
75  
76              // Store the difference between estimated and observed values in the stats
77              azDiffStat.addValue(FastMath.abs(estimated.getEstimatedValue()[0] - measurement.getObservedValue()[0]));
78              elDiffStat.addValue(FastMath.abs(estimated.getEstimatedValue()[1] - measurement.getObservedValue()[1]));
79          }
80  
81          // Mean and std errors check
82          Assertions.assertEquals(0.0, azDiffStat.getMean(), 6.9e-9);
83          Assertions.assertEquals(0.0, azDiffStat.getStandardDeviation(), 7.2e-9);
84          Assertions.assertEquals(0.0, elDiffStat.getMean(), 5.4e-9);
85          Assertions.assertEquals(0.0, elDiffStat.getStandardDeviation(), 3.3e-9);
86  
87          // Test measurement type
88          Assertions.assertEquals(AngularAzEl.MEASUREMENT_TYPE, measurements.get(0).getMeasurementType());
89      }
90  
91      /** Test the values of the state derivatives using a numerical.
92       * finite differences calculation as a reference
93       */
94      @Test
95      void testStateDerivatives() {
96  
97          Context context = EstimationTestUtils.geoStationnaryContext("regular-data:potential:tides");
98  
99          final NumericalPropagatorBuilder propagatorBuilder =
100                         context.createBuilder(OrbitType.EQUINOCTIAL, PositionAngleType.TRUE, false,
101                                               1.0e-6, 60.0, 0.001);
102 
103         // create perfect azimuth-elevation measurements
104         final Propagator propagator = EstimationTestUtils.createPropagator(context.initialOrbit,
105                                                                            propagatorBuilder);
106         final List<ObservedMeasurement<?>> measurements =
107                         EstimationTestUtils.createMeasurements(propagator,
108                                                                new AngularAzElMeasurementCreator(context),
109                                                                0.25, 3.0, 600.0);
110 
111         propagator.clearStepHandlers();
112 
113         // Compute measurements.
114         double[] AzerrorsP = new double[3 * measurements.size()];
115         double[] AzerrorsV = new double[3 * measurements.size()];
116         double[] ElerrorsP = new double[3 * measurements.size()];
117         double[] ElerrorsV = new double[3 * measurements.size()];
118         int AzindexP = 0;
119         int AzindexV = 0;
120         int ElindexP = 0;
121         int ElindexV = 0;
122 
123         for (final ObservedMeasurement<?> measurement : measurements) {
124 
125             // parameter corresponding to station position offset
126             final GroundStation stationParameter = ((AngularAzEl) measurement).getStation();
127 
128             // We intentionally propagate to a date which is close to the
129             // real spacecraft state but is *not* the accurate date, by
130             // compensating only part of the downlink delay. This is done
131             // in order to validate the partial derivatives with respect
132             // to velocity. If we had chosen the proper state date, the
133             // angular would have depended only on the current position but
134             // not on the current velocity.
135             final AbsoluteDate datemeas  = measurement.getDate();
136             SpacecraftState    state     = propagator.propagate(datemeas);
137             final Vector3D     stationP  = stationParameter.getOffsetToInertial(state.getFrame(), datemeas, false).transformPosition(Vector3D.ZERO);
138             final double       meanDelay = AbstractMeasurement.signalTimeOfFlightAdjustableEmitter(state.getPVCoordinates(), stationP,
139                                                                                                    datemeas, state.getFrame());
140 
141             final AbsoluteDate date      = measurement.getDate().shiftedBy(-0.75 * meanDelay);
142                                state     = propagator.propagate(date);
143             final EstimatedMeasurement<?> estimated = measurement.estimate(0, 0, new SpacecraftState[] { state });
144             Assertions.assertEquals(2, estimated.getParticipants().length);
145             final double[][]   jacobian  = estimated.getStateDerivatives(0);
146 
147             // compute a reference value using finite differences
148             final double[][] finiteDifferencesJacobian =
149                 Differentiation.differentiate(state1 ->
150                                                   measurement.
151                                                       estimateWithoutDerivatives(new SpacecraftState[] { state1 }).
152                                                       getEstimatedValue(),
153                                               measurement.getDimension(),
154                                               propagator.getAttitudeProvider(),
155                                               OrbitType.CARTESIAN,
156                                               PositionAngleType.TRUE, 250.0, 4).
157                     value(state);
158 
159             Assertions.assertEquals(finiteDifferencesJacobian.length, jacobian.length);
160             Assertions.assertEquals(finiteDifferencesJacobian[0].length, jacobian[0].length);
161 
162             final double smallest = FastMath.ulp((double) 1.0);
163 
164             for (int i = 0; i < jacobian.length; ++i) {
165                 for (int j = 0; j < jacobian[i].length; ++j) {
166                     double relativeError = FastMath.abs((finiteDifferencesJacobian[i][j] - jacobian[i][j]) /
167                                                               finiteDifferencesJacobian[i][j]);
168 
169                     if ((FastMath.sqrt(finiteDifferencesJacobian[i][j]) < smallest) && (FastMath.sqrt(jacobian[i][j]) < smallest) ){
170                         relativeError = 0.0;
171                     }
172 
173                     if (j < 3) {
174                         if (i == 0) {
175                             AzerrorsP[AzindexP++] = relativeError;
176                         } else {
177                             ElerrorsP[ElindexP++] = relativeError;
178                         }
179                     } else {
180                         if (i == 0) {
181                             AzerrorsV[AzindexV++] = relativeError;
182                         } else {
183                             ElerrorsV[ElindexV++] = relativeError;
184                         }
185                     }
186                 }
187             }
188         }
189 
190         // median errors on Azimuth
191         Assertions.assertEquals(0.0, new Median().evaluate(AzerrorsP), 1.2e-10);
192         Assertions.assertEquals(0.0, new Median().evaluate(AzerrorsV), 6.1e-5);
193 
194         // median errors on Elevation
195         Assertions.assertEquals(0.0, new Median().evaluate(ElerrorsP), 7.5e-11);
196         Assertions.assertEquals(0.0, new Median().evaluate(ElerrorsV), 2.3e-5);
197     }
198 
199     /** Test the values of the parameters' derivatives using a numerical
200      * finite differences calculation as a reference
201      */
202     @Test
203     void testParameterDerivatives() {
204 
205         Context context = EstimationTestUtils.geoStationnaryContext("regular-data:potential:tides");
206 
207         final NumericalPropagatorBuilder propagatorBuilder =
208                         context.createBuilder(OrbitType.EQUINOCTIAL, PositionAngleType.TRUE, false,
209                                               1.0e-6, 60.0, 0.001);
210 
211         // create perfect azimuth-elevation measurements
212         for (final GroundStation station : context.stations) {
213             station.getClockOffsetDriver().setSelected(true);
214             station.getEastOffsetDriver().setSelected(true);
215             station.getNorthOffsetDriver().setSelected(true);
216             station.getZenithOffsetDriver().setSelected(true);
217         }
218         final Propagator propagator = EstimationTestUtils.createPropagator(context.initialOrbit,
219                                                                            propagatorBuilder);
220         final List<ObservedMeasurement<?>> measurements =
221                         EstimationTestUtils.createMeasurements(propagator,
222                                                                new AngularAzElMeasurementCreator(context),
223                                                                0.25, 3.0, 600.0);
224         propagator.clearStepHandlers();
225 
226         for (final ObservedMeasurement<?> measurement : measurements) {
227 
228             // parameter corresponding to station position offset
229             final GroundStation stationParameter = ((AngularAzEl) measurement).getStation();
230 
231             // We intentionally propagate to a date which is close to the
232             // real spacecraft state but is *not* the accurate date, by
233             // compensating only part of the downlink delay. This is done
234             // in order to validate the partial derivatives with respect
235             // to velocity. If we had chosen the proper state date, the
236             // angular would have depended only on the current position but
237             // not on the current velocity.
238             final AbsoluteDate    datemeas  = measurement.getDate();
239             final SpacecraftState stateini  = propagator.propagate(datemeas);
240             final Vector3D        stationP  = stationParameter.getOffsetToInertial(stateini.getFrame(), datemeas, false).transformPosition(Vector3D.ZERO);
241             final double          meanDelay = AbstractMeasurement.signalTimeOfFlightAdjustableEmitter(stateini.getPVCoordinates(), stationP,
242                                                                                                       datemeas, stateini.getFrame());
243 
244             final AbsoluteDate    date      = measurement.getDate().shiftedBy(-0.75 * meanDelay);
245             final SpacecraftState state     = propagator.propagate(date);
246             final ParameterDriver[] drivers = new ParameterDriver[] {
247                 stationParameter.getEastOffsetDriver(),
248                 stationParameter.getNorthOffsetDriver(),
249                 stationParameter.getZenithOffsetDriver()
250             };
251             for (int i = 0; i < 3; ++i) {
252                 final double[] gradient  = measurement.estimate(0, 0, new SpacecraftState[] { state }).getParameterDerivatives(drivers[i]);
253                 Assertions.assertEquals(2, measurement.getDimension());
254                 Assertions.assertEquals(2, gradient.length);
255 
256                 for (final int k : new int[] {0, 1}) {
257                     final ParameterFunction dMkdP =
258                                     Differentiation.differentiate(new ParameterFunction() {
259                                         /** {@inheritDoc} */
260                                         @Override
261                                         public double value(final ParameterDriver parameterDriver, AbsoluteDate date) {
262                                             return measurement.
263                                                    estimateWithoutDerivatives(new SpacecraftState[] { state }).
264                                                    getEstimatedValue()[k];
265                                         }
266                                     }, 3, 50.0 * drivers[i].getScale());
267                     final double ref = dMkdP.value(drivers[i], date);
268 
269                     if (ref > 1.e-12) {
270                         Assertions.assertEquals(ref, gradient[k], 3e-10 * FastMath.abs(ref));
271                     }
272                 }
273             }
274         }
275     }
276 }
277