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.models.earth.ionosphere;
18  
19  import java.util.Collections;
20  import java.util.List;
21  
22  import org.hipparchus.Field;
23  import org.hipparchus.CalculusFieldElement;
24  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
25  import org.hipparchus.geometry.euclidean.threed.Vector3D;
26  import org.hipparchus.util.FastMath;
27  import org.hipparchus.util.FieldSinCos;
28  import org.hipparchus.util.MathUtils;
29  import org.hipparchus.util.SinCos;
30  import org.orekit.annotation.DefaultDataContext;
31  import org.orekit.bodies.FieldGeodeticPoint;
32  import org.orekit.bodies.GeodeticPoint;
33  import org.orekit.data.DataContext;
34  import org.orekit.frames.FieldStaticTransform;
35  import org.orekit.frames.StaticTransform;
36  import org.orekit.frames.TopocentricFrame;
37  import org.orekit.propagation.FieldSpacecraftState;
38  import org.orekit.propagation.SpacecraftState;
39  import org.orekit.time.AbsoluteDate;
40  import org.orekit.time.DateTimeComponents;
41  import org.orekit.time.FieldAbsoluteDate;
42  import org.orekit.time.TimeScale;
43  import org.orekit.utils.Constants;
44  import org.orekit.utils.ParameterDriver;
45  
46  /**
47   * Klobuchar ionospheric delay model.
48   * Klobuchar ionospheric delay model is designed as a GNSS correction model.
49   * The parameters for the model are provided by the GPS satellites in their broadcast
50   * messsage.
51   * This model is based on the assumption the electron content is concentrated
52   * in 350 km layer.
53   *
54   * The delay refers to L1 (1575.42 MHz).
55   * If the delay is sought for L2 (1227.60 MHz), multiply the result by 1.65 (Klobuchar, 1996).
56   * More generally, since ionospheric delay is inversely proportional to the square of the signal
57   * frequency f, to adapt this model to other GNSS frequencies f, multiply by (L1 / f)^2.
58   *
59   * References:
60   *     ICD-GPS-200, Rev. C, (1997), pp. 125-128
61   *     Klobuchar, J.A., Ionospheric time-delay algorithm for single-frequency GPS users,
62   *         IEEE Transactions on Aerospace and Electronic Systems, Vol. 23, No. 3, May 1987
63   *     Klobuchar, J.A., "Ionospheric Effects on GPS", Global Positioning System: Theory and
64   *         Applications, 1996, pp.513-514, Parkinson, Spilker.
65   *
66   * @author Joris Olympio
67   * @since 7.1
68   *
69   */
70  public class KlobucharIonoModel implements IonosphericModel, IonosphericDelayModel {
71  
72      /** The 4 coefficients of a cubic equation representing the amplitude of the vertical delay. Units are sec/semi-circle^(i-1) for the i-th coefficient, i=1, 2, 3, 4. */
73      private final double[] alpha;
74  
75      /** The 4 coefficients of a cubic equation representing the period of the model. Units are sec/semi-circle^(i-1) for the i-th coefficient, i=1, 2, 3, 4. */
76      private final double[] beta;
77  
78      /** GPS time scale. */
79      private final TimeScale gps;
80  
81      /** Create a new Klobuchar ionospheric delay model, when a single frequency system is used.
82       * This model accounts for at least 50 percent of RMS error due to ionospheric propagation effect (ICD-GPS-200)
83       *
84       * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
85       *
86       * @param alpha coefficients of a cubic equation representing the amplitude of the vertical delay.
87       * @param beta coefficients of a cubic equation representing the period of the model.
88       * @see #KlobucharIonoModel(double[], double[], TimeScale)
89       */
90      @DefaultDataContext
91      public KlobucharIonoModel(final double[] alpha, final double[] beta) {
92          this(alpha, beta, DataContext.getDefault().getTimeScales().getGPS());
93      }
94  
95      /**
96       * Create a new Klobuchar ionospheric delay model, when a single frequency system is
97       * used. This model accounts for at least 50 percent of RMS error due to ionospheric
98       * propagation effect (ICD-GPS-200)
99       *
100      * @param alpha coefficients of a cubic equation representing the amplitude of the
101      *              vertical delay.
102      * @param beta  coefficients of a cubic equation representing the period of the
103      *              model.
104      * @param gps   GPS time scale.
105      * @since 10.1
106      */
107     public KlobucharIonoModel(final double[] alpha,
108                               final double[] beta,
109                               final TimeScale gps) {
110         this.alpha = alpha.clone();
111         this.beta  = beta.clone();
112         this.gps = gps;
113     }
114 
115     /**
116      * Calculates the ionospheric path delay for the signal path from a ground
117      * station to a satellite.
118      * <p>
119      * The path delay is computed for any elevation angle.
120      * </p>
121      * @param date current date
122      * @param geo geodetic point of receiver/station
123      * @param elevation elevation of the satellite in radians
124      * @param azimuth azimuth of the satellite in radians
125      * @param frequency frequency of the signal in Hz
126      * @param parameters ionospheric model parameters
127      * @return the path delay due to the ionosphere in m
128      */
129     public double pathDelay(final AbsoluteDate date, final GeodeticPoint geo,
130                             final double elevation, final double azimuth, final double frequency,
131                             final double[] parameters) {
132 
133         // Sine and cosine of the azimuth
134         final SinCos sc = FastMath.sinCos(azimuth);
135 
136         // degrees to semicircles
137         final double rad2semi = 1. / FastMath.PI;
138         final double semi2rad = FastMath.PI;
139 
140         // Earth Centered angle
141         final double psi = 0.0137 / (elevation / FastMath.PI + 0.11) - 0.022;
142 
143         // Subionospheric latitude: the latitude of the IPP (Ionospheric Pierce Point)
144         // in [-0.416, 0.416], semicircle
145         final double latIono = FastMath.min(
146                                       FastMath.max(geo.getLatitude() * rad2semi + psi * sc.cos(), -0.416),
147                                       0.416);
148 
149         // Subionospheric longitude: the longitude of the IPP
150         // in semicircle
151         final double lonIono = geo.getLongitude() * rad2semi + (psi * sc.sin() / FastMath.cos(latIono * semi2rad));
152 
153         // Geomagnetic latitude, semicircle
154         final double latGeom = latIono + 0.064 * FastMath.cos((lonIono - 1.617) * semi2rad);
155 
156         // day of week and tow (sec)
157         // Note: Sunday=0, Monday=1, Tuesday=2, Wednesday=3, Thursday=4, Friday=5, Saturday=6
158         final DateTimeComponents dtc = date.getComponents(gps);
159         final int dofweek = dtc.getDate().getDayOfWeek();
160         final double secday = dtc.getTime().getSecondsInLocalDay();
161         final double tow = dofweek * 86400. + secday;
162 
163         final double t = 43200. * lonIono + tow;
164         final double tsec = t - FastMath.floor(t / 86400.) * 86400; // Seconds of day
165 
166         // Slant factor, semicircle
167         final double slantFactor = 1.0 + 16.0 * FastMath.pow(0.53 - elevation / FastMath.PI, 3);
168 
169         // Period of model, seconds
170         final double period = FastMath.max(72000., beta[0] + (beta[1]  + (beta[2] + beta[3] * latGeom) * latGeom) * latGeom);
171 
172         // Phase of the model, radians
173         // (Max at 14.00 = 50400 sec local time)
174         final double x = 2.0 * FastMath.PI * (tsec - 50400.0) / period;
175 
176         // Amplitude of the model, seconds
177         final double amplitude = FastMath.max(0, alpha[0] + (alpha[1]  + (alpha[2] + alpha[3] * latGeom) * latGeom) * latGeom);
178 
179         // Ionospheric correction (L1)
180         double ionoTimeDelayL1 = slantFactor * (5. * 1e-9);
181         if (FastMath.abs(x) < 1.570) {
182             ionoTimeDelayL1 += slantFactor * (amplitude * (1.0 - FastMath.pow(x, 2) / 2.0 + FastMath.pow(x, 4) / 24.0));
183         }
184 
185         // Ionospheric delay for the L1 frequency, in meters, with slant correction.
186         final double ratio = FastMath.pow(1575.42e6 / frequency, 2);
187         return ratio * Constants.SPEED_OF_LIGHT * ionoTimeDelayL1;
188     }
189 
190     /** {@inheritDoc} */
191     @Override
192     public double pathDelay(final SpacecraftState state, final TopocentricFrame baseFrame,
193                             final double frequency, final double[] parameters) {
194         return pathDelay(state, baseFrame, state.getDate(), frequency, parameters);
195     }
196 
197     /** {@inheritDoc} */
198     @Override
199     public double pathDelay(final SpacecraftState state,
200                             final TopocentricFrame baseFrame, final AbsoluteDate receptionDate,
201                             final double frequency, final double[] parameters) {
202 
203         // we use transform from base frame to inert frame and invert it
204         // because base frame could be peered with inertial frame (hence improving performances with caching)
205         // but the reverse is almost never used
206         final StaticTransform base2Inert = baseFrame.getStaticTransformTo(state.getFrame(), receptionDate);
207         final Vector3D        position   = base2Inert.getInverse().transformPosition(state.getPosition());
208 
209         // Elevation in radians
210         final double   elevation = position.getDelta();
211 
212         // Only consider measures above the horizon
213         if (elevation > 0.0) {
214             // Date
215             final AbsoluteDate date = state.getDate();
216             // Geodetic point
217             final GeodeticPoint geo = baseFrame.getPoint();
218             // Azimuth angle in radians
219             double azimuth = FastMath.atan2(position.getX(), position.getY());
220             if (azimuth < 0.) {
221                 azimuth += MathUtils.TWO_PI;
222             }
223             // Delay
224             return pathDelay(date, geo, elevation, azimuth, frequency, parameters);
225         }
226 
227         return 0.0;
228     }
229 
230     /**
231      * Calculates the ionospheric path delay for the signal path from a ground
232      * station to a satellite.
233      * <p>
234      * The path delay is computed for any elevation angle.
235      * </p>
236      * @param <T> type of the elements
237      * @param date current date
238      * @param geo geodetic point of receiver/station
239      * @param elevation elevation of the satellite in radians
240      * @param azimuth azimuth of the satellite in radians
241      * @param frequency frequency of the signal in Hz
242      * @param parameters ionospheric model parameters
243      * @return the path delay due to the ionosphere in m
244      */
245     public <T extends CalculusFieldElement<T>> T pathDelay(final FieldAbsoluteDate<T> date, final FieldGeodeticPoint<T> geo,
246                                                        final T elevation, final T azimuth, final double frequency,
247                                                        final T[] parameters) {
248 
249         // Sine and cosine of the azimuth
250         final FieldSinCos<T> sc = FastMath.sinCos(azimuth);
251 
252         // Field
253         final Field<T> field = date.getField();
254         final T zero = field.getZero();
255         final T one  = field.getOne();
256 
257         // degrees to semicircles
258         final T pi       = one.getPi();
259         final T rad2semi = pi.reciprocal();
260 
261         // Earth Centered angle
262         final T psi = elevation.divide(pi).add(0.11).divide(0.0137).reciprocal().subtract(0.022);
263 
264         // Subionospheric latitude: the latitude of the IPP (Ionospheric Pierce Point)
265         // in [-0.416, 0.416], semicircle
266         final T latIono = FastMath.min(
267                                       FastMath.max(geo.getLatitude().multiply(rad2semi).add(psi.multiply(sc.cos())), zero.subtract(0.416)),
268                                       zero.newInstance(0.416));
269 
270         // Subionospheric longitude: the longitude of the IPP
271         // in semicircle
272         final T lonIono = geo.getLongitude().multiply(rad2semi).add(psi.multiply(sc.sin()).divide(FastMath.cos(latIono.multiply(pi))));
273 
274         // Geomagnetic latitude, semicircle
275         final T latGeom = latIono.add(FastMath.cos(lonIono.subtract(1.617).multiply(pi)).multiply(0.064));
276 
277         // day of week and tow (sec)
278         // Note: Sunday=0, Monday=1, Tuesday=2, Wednesday=3, Thursday=4, Friday=5, Saturday=6
279         final DateTimeComponents dtc = date.getComponents(gps);
280         final int dofweek = dtc.getDate().getDayOfWeek();
281         final double secday = dtc.getTime().getSecondsInLocalDay();
282         final double tow = dofweek * 86400. + secday;
283 
284         final T t = lonIono.multiply(43200.).add(tow);
285         final T tsec = t.subtract(FastMath.floor(t.divide(86400.)).multiply(86400.)); // Seconds of day
286 
287         // Slant factor, semicircle
288         final T slantFactor = FastMath.pow(elevation.divide(pi).negate().add(0.53), 3).multiply(16.0).add(one);
289 
290         // Period of model, seconds
291         final T period = FastMath.max(zero.newInstance(72000.), latGeom.multiply(latGeom.multiply(latGeom.multiply(beta[3]).add(beta[2])).add(beta[1])).add(beta[0]));
292 
293         // Phase of the model, radians
294         // (Max at 14.00 = 50400 sec local time)
295         final T x = tsec.subtract(50400.0).multiply(pi.multiply(2.0)).divide(period);
296 
297         // Amplitude of the model, seconds
298         final T amplitude = FastMath.max(zero, latGeom.multiply(latGeom.multiply(latGeom.multiply(alpha[3]).add(alpha[2])).add(alpha[1])).add(alpha[0]));
299 
300         // Ionospheric correction (L1)
301         T ionoTimeDelayL1 = slantFactor.multiply(5. * 1e-9);
302         if (FastMath.abs(x.getReal()) < 1.570) {
303             ionoTimeDelayL1 = ionoTimeDelayL1.add(slantFactor.multiply(amplitude.multiply(one.subtract(FastMath.pow(x, 2).multiply(0.5)).add(FastMath.pow(x, 4).divide(24.0)))));
304         }
305 
306         // Ionospheric delay for the L1 frequency, in meters, with slant correction.
307         final double ratio = FastMath.pow(1575.42e6 / frequency, 2);
308         return ionoTimeDelayL1.multiply(Constants.SPEED_OF_LIGHT).multiply(ratio);
309     }
310 
311     /** {@inheritDoc} */
312     @Override
313     public <T extends CalculusFieldElement<T>> T pathDelay(final FieldSpacecraftState<T> state, final TopocentricFrame baseFrame,
314                                                        final double frequency, final T[] parameters) {
315         return pathDelay(state, baseFrame, state.getDate(), frequency, parameters);
316     }
317 
318     /** {@inheritDoc} */
319     @Override
320     public <T extends CalculusFieldElement<T>> T pathDelay(final FieldSpacecraftState<T> state,
321                                                            final TopocentricFrame baseFrame, final FieldAbsoluteDate<T> receptionDate,
322                                                            final double frequency, final T[] parameters) {
323 
324         // we use transform from base frame to inert frame and invert it
325         // because base frame could be peered with inertial frame (hence improving performances with caching)
326         // but the reverse is almost never used
327         final FieldStaticTransform<T> base2Inert = baseFrame.getStaticTransformTo(state.getFrame(), receptionDate);
328         final FieldVector3D<T>        position   = base2Inert.getInverse().transformPosition(state.getPosition());
329 
330         // Elevation in radians
331         final T elevation = position.getDelta();
332 
333         if (elevation.getReal() > 0.0) {
334             // Date
335             final FieldAbsoluteDate<T> date = state.getDate();
336             // Geodetic point
337             final FieldGeodeticPoint<T> geo = baseFrame.getPoint(date.getField());
338             // Azimuth angle in radians
339             T azimuth = FastMath.atan2(position.getX(), position.getY());
340             if (azimuth.getReal() < 0.) {
341                 azimuth = azimuth.add(MathUtils.TWO_PI);
342             }
343             // Delay
344             return pathDelay(date, geo, elevation, azimuth, frequency, parameters);
345         }
346 
347         return elevation.getField().getZero();
348     }
349 
350     @Override
351     public List<ParameterDriver> getParametersDrivers() {
352         return Collections.emptyList();
353     }
354 }