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.modifiers;
18  
19  import java.util.Arrays;
20  import java.util.List;
21  
22  import org.hipparchus.CalculusFieldElement;
23  import org.hipparchus.analysis.differentiation.Gradient;
24  import org.orekit.attitudes.FrameAlignedProvider;
25  import org.orekit.estimation.measurements.EstimatedMeasurement;
26  import org.orekit.estimation.measurements.EstimatedMeasurementBase;
27  import org.orekit.estimation.measurements.EstimationModifier;
28  import org.orekit.estimation.measurements.GroundStation;
29  import org.orekit.estimation.measurements.TurnAroundRange;
30  import org.orekit.frames.TopocentricFrame;
31  import org.orekit.models.earth.ionosphere.IonosphericModel;
32  import org.orekit.propagation.FieldSpacecraftState;
33  import org.orekit.propagation.SpacecraftState;
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  import org.orekit.utils.TimeSpanMap.Span;
39  
40  /** Class modifying theoretical TurnAroundRange measurement with ionospheric delay.
41   * <p>
42   * The effect of ionospheric correction on the TurnAroundRange is directly computed
43   * through the computation of the ionospheric delay.
44   * </p>
45   * <p>
46   * The ionospheric delay depends on the frequency of the signal (GNSS, VLBI, ...).
47   * For optical measurements (e.g. SLR), the ray is not affected by ionosphere charged particles.
48   * </p>
49   * <p>
50   * Since 10.0, state derivatives and ionospheric parameters derivates are computed
51   * using automatic differentiation.
52   * </p>
53   * @author Maxime Journot
54   * @since 9.0
55   */
56  public class TurnAroundRangeIonosphericDelayModifier implements EstimationModifier<TurnAroundRange> {
57  
58      /** Ionospheric delay model. */
59      private final IonosphericModel ionoModel;
60  
61      /** Frequency [Hz]. */
62      private final double frequency;
63  
64      /** Constructor.
65       *
66       * @param model  Ionospheric delay model appropriate for the current TurnAroundRange measurement method.
67       * @param freq frequency of the signal in Hz
68       */
69      public TurnAroundRangeIonosphericDelayModifier(final IonosphericModel model,
70                                                     final double freq) {
71          ionoModel = model;
72          frequency = freq;
73      }
74  
75      /** {@inheritDoc} */
76      @Override
77      public String getEffectName() {
78          return "ionosphere";
79      }
80  
81      /** Compute the measurement error due to ionosphere.
82       * @param station station
83       * @param state spacecraft state
84       * @return the measurement error due to ionosphere
85       */
86      private double rangeErrorIonosphericModel(final GroundStation station,
87                                                final SpacecraftState state) {
88          // Base frame associated with the station
89          final TopocentricFrame baseFrame = station.getBaseFrame();
90          // Delay in meters
91          return ionoModel.pathDelay(state, baseFrame, frequency, ionoModel.getParameters(state.getDate()));
92      }
93  
94      /** Compute the measurement error due to ionosphere.
95       * @param <T> type of the elements
96       * @param station station
97       * @param state spacecraft state
98       * @param parameters ionospheric model parameters
99       * @return the measurement error due to ionosphere
100      */
101     private <T extends CalculusFieldElement<T>> T rangeErrorIonosphericModel(final GroundStation station,
102                                                                              final FieldSpacecraftState<T> state,
103                                                                              final T[] parameters) {
104         // Base frame associated with the station
105         final TopocentricFrame baseFrame = station.getBaseFrame();
106         // Delay in meters
107         return ionoModel.pathDelay(state, baseFrame, frequency, parameters);
108     }
109 
110     /** Compute the Jacobian of the delay term wrt state using
111     * automatic differentiation.
112     *
113     * @param derivatives ionospheric delay derivatives
114     *
115     * @return Jacobian of the delay wrt state
116     */
117     private double[][] rangeErrorJacobianState(final double[] derivatives) {
118         final double[][] finiteDifferencesJacobian = new double[1][6];
119         System.arraycopy(derivatives, 0, finiteDifferencesJacobian[0], 0, 6);
120         return finiteDifferencesJacobian;
121     }
122 
123 
124     /** Compute the derivative of the delay term wrt parameters.
125      *
126      * @param station ground station
127      * @param driver driver for the station offset parameter
128      * @param state spacecraft state
129      * @return derivative of the delay wrt station offset parameter
130      */
131     private double rangeErrorParameterDerivative(final GroundStation station,
132                                                  final ParameterDriver driver,
133                                                  final SpacecraftState state) {
134 
135         final ParameterFunction rangeError = new ParameterFunction() {
136             /** {@inheritDoc} */
137             @Override
138             public double value(final ParameterDriver parameterDriver, final AbsoluteDate date) {
139                 return rangeErrorIonosphericModel(station, state);
140             }
141         };
142 
143         final ParameterFunction rangeErrorDerivative =
144                         Differentiation.differentiate(rangeError, 3, 10.0 * driver.getScale());
145 
146         return rangeErrorDerivative.value(driver, state.getDate());
147 
148     }
149 
150     /** Compute the derivative of the delay term wrt parameters using
151     * automatic differentiation.
152     *
153     * @param derivatives ionospheric delay derivatives
154     * @param freeStateParameters dimension of the state.
155     * @return derivative of the delay wrt ionospheric model parameters
156     */
157     private double[] rangeErrorParameterDerivative(final double[] derivatives, final int freeStateParameters) {
158         // 0 ... freeStateParameters - 1 -> derivatives of the delay wrt state
159         // freeStateParameters ... n     -> derivatives of the delay wrt ionospheric parameters
160         return Arrays.copyOfRange(derivatives, freeStateParameters, derivatives.length);
161     }
162 
163     /** {@inheritDoc} */
164     @Override
165     public List<ParameterDriver> getParametersDrivers() {
166         return ionoModel.getParametersDrivers();
167     }
168 
169     @Override
170     public void modifyWithoutDerivatives(final EstimatedMeasurementBase<TurnAroundRange> estimated) {
171 
172         final TurnAroundRange measurement      = estimated.getObservedMeasurement();
173         final GroundStation   primaryStation   = measurement.getPrimaryStation();
174         final GroundStation   secondaryStation = measurement.getSecondaryStation();
175         final SpacecraftState state            = estimated.getStates()[0];
176 
177         // Update estimated value taking into account the ionospheric delay.
178         // The ionospheric delay is directly added to the TurnAroundRange.
179         final double[] newValue     = estimated.getEstimatedValue();
180         final double primaryDelay   = rangeErrorIonosphericModel(primaryStation, state);
181         final double secondaryDelay = rangeErrorIonosphericModel(secondaryStation, state);
182         newValue[0] = newValue[0] + primaryDelay + secondaryDelay;
183         estimated.modifyEstimatedValue(this, newValue);
184 
185     }
186 
187     @Override
188     public void modify(final EstimatedMeasurement<TurnAroundRange> estimated) {
189         final TurnAroundRange measurement      = estimated.getObservedMeasurement();
190         final GroundStation   primaryStation   = measurement.getPrimaryStation();
191         final GroundStation   secondaryStation = measurement.getSecondaryStation();
192         final SpacecraftState state            = estimated.getStates()[0];
193 
194         // Update estimated derivatives with Jacobian of the measure wrt state
195         final ModifierGradientConverter converter =
196                 new ModifierGradientConverter(state, 6, new FrameAlignedProvider(state.getFrame()));
197         final FieldSpacecraftState<Gradient> gState = converter.getState(ionoModel);
198         final Gradient[] gParameters        = converter.getParametersAtStateDate(gState, ionoModel);
199         final Gradient primaryGDelay        = rangeErrorIonosphericModel(primaryStation, gState, gParameters);
200         final Gradient secondaryGDelay      = rangeErrorIonosphericModel(secondaryStation, gState, gParameters);
201         final double[] primaryDerivatives   = primaryGDelay.getGradient();
202         final double[] secondaryDerivatives = secondaryGDelay.getGradient();
203 
204         final double[][] primaryDjac      = rangeErrorJacobianState(primaryDerivatives);
205         final double[][] secondaryDjac    = rangeErrorJacobianState(secondaryDerivatives);
206         final double[][] stateDerivatives = estimated.getStateDerivatives(0);
207         for (int irow = 0; irow < stateDerivatives.length; ++irow) {
208             for (int jcol = 0; jcol < stateDerivatives[0].length; ++jcol) {
209                 stateDerivatives[irow][jcol] += primaryDjac[irow][jcol] + secondaryDjac[irow][jcol];
210             }
211         }
212         estimated.setStateDerivatives(0, stateDerivatives);
213 
214         int indexPrimary = 0;
215         for (final ParameterDriver driver : getParametersDrivers()) {
216             if (driver.isSelected()) {
217                 for (Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
218                     // update estimated derivatives with derivative of the modification wrt ionospheric parameters
219                     double parameterDerivative = estimated.getParameterDerivatives(driver, span.getStart())[0];
220                     final double[] derivatives = rangeErrorParameterDerivative(primaryDerivatives, converter.getFreeStateParameters());
221                     parameterDerivative += derivatives[indexPrimary];
222                     estimated.setParameterDerivatives(driver, span.getStart(), parameterDerivative);
223                     indexPrimary += 1;
224                 }
225             }
226 
227         }
228 
229         int indexSecondary = 0;
230         for (final ParameterDriver driver : getParametersDrivers()) {
231             if (driver.isSelected()) {
232                 for (Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
233                     // update estimated derivatives with derivative of the modification wrt ionospheric parameters
234                     double parameterDerivative = estimated.getParameterDerivatives(driver, span.getStart())[0];
235                     final double[] derivatives = rangeErrorParameterDerivative(secondaryDerivatives, converter.getFreeStateParameters());
236                     parameterDerivative += derivatives[indexSecondary];
237                     estimated.setParameterDerivatives(driver, span.getStart(), parameterDerivative);
238                     indexSecondary += 1;
239                 }
240             }
241 
242         }
243 
244         // Update derivatives with respect to primary station position
245         for (final ParameterDriver driver : Arrays.asList(primaryStation.getClockOffsetDriver(),
246                                                           primaryStation.getEastOffsetDriver(),
247                                                           primaryStation.getNorthOffsetDriver(),
248                                                           primaryStation.getZenithOffsetDriver())) {
249             if (driver.isSelected()) {
250                 for (Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
251 
252                     double parameterDerivative = estimated.getParameterDerivatives(driver, span.getStart())[0];
253                     parameterDerivative += rangeErrorParameterDerivative(primaryStation, driver, state);
254                     estimated.setParameterDerivatives(driver, span.getStart(), parameterDerivative);
255                 }
256             }
257         }
258 
259         // Update derivatives with respect to secondary station position
260         for (final ParameterDriver driver : Arrays.asList(secondaryStation.getEastOffsetDriver(),
261                                                           secondaryStation.getNorthOffsetDriver(),
262                                                           secondaryStation.getZenithOffsetDriver())) {
263             if (driver.isSelected()) {
264                 for (Span<String> span = driver.getNamesSpanMap().getFirstSpan(); span != null; span = span.next()) {
265 
266                     double parameterDerivative = estimated.getParameterDerivatives(driver, span.getStart())[0];
267                     parameterDerivative += rangeErrorParameterDerivative(secondaryStation, driver, state);
268                     estimated.setParameterDerivatives(driver, span.getStart(), parameterDerivative);
269                 }
270             }
271         }
272 
273         // Update estimated value taking into account the ionospheric delay.
274         // The ionospheric delay is directly added to the TurnAroundRange.
275         final double[] newValue = estimated.getEstimatedValue();
276         newValue[0] = newValue[0] + primaryGDelay.getReal() + secondaryGDelay.getReal();
277         estimated.modifyEstimatedValue(this, newValue);
278     }
279 
280 }