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.nequick;
18  
19  import java.util.Collections;
20  import java.util.List;
21  
22  import org.hipparchus.CalculusFieldElement;
23  import org.hipparchus.Field;
24  import org.hipparchus.util.FastMath;
25  import org.hipparchus.util.MathArrays;
26  import org.orekit.bodies.BodyShape;
27  import org.orekit.bodies.FieldGeodeticPoint;
28  import org.orekit.bodies.GeodeticPoint;
29  import org.orekit.frames.TopocentricFrame;
30  import org.orekit.models.earth.ionosphere.IonosphericModel;
31  import org.orekit.propagation.FieldSpacecraftState;
32  import org.orekit.propagation.SpacecraftState;
33  import org.orekit.time.AbsoluteDate;
34  import org.orekit.time.DateComponents;
35  import org.orekit.time.DateTimeComponents;
36  import org.orekit.time.FieldAbsoluteDate;
37  import org.orekit.time.TimeScale;
38  import org.orekit.utils.ParameterDriver;
39  import org.orekit.utils.units.Unit;
40  
41  /**
42   * NeQuick ionospheric delay model.
43   *
44   * @author Bryan Cazabonne
45   *
46   * @see "European Union (2016). European GNSS (Galileo) Open Service-Ionospheric Correction
47   *       Algorithm for Galileo Single Frequency Users. 1.2."
48   * @see <a href="https://www.itu.int/rec/R-REC-P.531/en">ITU-R P.531</a>
49   *
50   * @since 10.1
51   */
52  public abstract class NeQuickModel implements IonosphericModel {
53  
54      /** Mean Earth radius in m (Ref Table 2.5.2). */
55      public static final double RE = 6371200.0;
56  
57      /** Factor for the electron density computation. */
58      private static final double DENSITY_FACTOR = 1.0e11;
59  
60      /** Factor for the path delay computation. */
61      private static final double DELAY_FACTOR = 40.3e16;
62  
63      /** F2 coefficients used by the F2 layer (flatten array for cache efficiency). */
64      private final double[][] flattenF2;
65  
66      /** Fm3 coefficients used by the M(3000)F2 layer(flatten array for cache efficiency). */
67      private final double[][] flattenFm3;
68  
69      /** UTC time scale. */
70      private final TimeScale utc;
71  
72      /** Simple constructor.
73       * @param utc UTC time scale
74       * @since 13.0
75       */
76      protected NeQuickModel(final TimeScale utc) {
77  
78          this.utc = utc;
79  
80          // F2 layer values
81          this.flattenF2  = new double[12][];
82          this.flattenFm3 = new double[12][];
83  
84      }
85  
86      /** Get UTC time scale.
87       * @return UTC time scale
88       * @since 13.0
89       */
90      public TimeScale getUtc() {
91          return utc;
92      }
93  
94      /** {@inheritDoc} */
95      @Override
96      public double pathDelay(final SpacecraftState state, final TopocentricFrame baseFrame,
97                              final double frequency, final double[] parameters) {
98          // Point
99          final GeodeticPoint recPoint = baseFrame.getPoint();
100         // Date
101         final AbsoluteDate date = state.getDate();
102 
103         // Reference body shape
104         final BodyShape ellipsoid = baseFrame.getParentShape();
105         // Satellite geodetic coordinates
106         final GeodeticPoint satPoint = ellipsoid.transform(state.getPosition(ellipsoid.getBodyFrame()),
107                                                            ellipsoid.getBodyFrame(), state.getDate());
108 
109         // Total Electron Content
110         final double tec = stec(date, recPoint, satPoint);
111 
112         // Ionospheric delay
113         final double factor = DELAY_FACTOR / (frequency * frequency);
114         return factor * tec;
115     }
116 
117     /** {@inheritDoc} */
118     @Override
119     public <T extends CalculusFieldElement<T>> T pathDelay(final FieldSpacecraftState<T> state,
120                                                            final TopocentricFrame baseFrame,
121                                                            final double frequency,
122                                                            final T[] parameters) {
123         // Date
124         final FieldAbsoluteDate<T> date = state.getDate();
125         // Point
126         final FieldGeodeticPoint<T> recPoint = baseFrame.getPoint(date.getField());
127 
128 
129         // Reference body shape
130         final BodyShape ellipsoid = baseFrame.getParentShape();
131         // Satellite geodetic coordinates
132         final FieldGeodeticPoint<T> satPoint = ellipsoid.transform(state.getPosition(ellipsoid.getBodyFrame()), ellipsoid.getBodyFrame(), state.getDate());
133 
134         // Total Electron Content
135         final T tec = stec(date, recPoint, satPoint);
136 
137         // Ionospheric delay
138         final double factor = DELAY_FACTOR / (frequency * frequency);
139         return tec.multiply(factor);
140     }
141 
142     /** {@inheritDoc} */
143     @Override
144     public List<ParameterDriver> getParametersDrivers() {
145         return Collections.emptyList();
146     }
147 
148     /**
149      * This method allows the computation of the Slant Total Electron Content (STEC).
150      * @param date current date
151      * @param recP receiver position
152      * @param satP satellite position
153      * @return the STEC in TECUnits
154      */
155     public double stec(final AbsoluteDate date, final GeodeticPoint recP, final GeodeticPoint satP) {
156         return stec(date.getComponents(utc), new Ray(recP, satP));
157     }
158 
159     /**
160      * This method allows the computation of the Slant Total Electron Content (STEC).
161      * @param <T> type of the elements
162      * @param date current date
163      * @param recP receiver position
164      * @param satP satellite position
165      * @return the STEC in TECUnits
166      */
167     public <T extends CalculusFieldElement<T>> T stec(final FieldAbsoluteDate<T> date,
168                                                       final FieldGeodeticPoint<T> recP,
169                                                       final FieldGeodeticPoint<T> satP) {
170         return stec(date.getComponents(utc), new FieldRay<>(recP, satP));
171     }
172 
173     /** Compute modip for a location.
174      * @param latitude latitude
175      * @param longitude longitude
176      * @return modip at specified location
177      * @since 13.0
178      */
179     protected abstract double computeMODIP(double latitude, double longitude);
180 
181     /** Compute modip for a location.
182      * @param <T> type of the field elements
183      * @param latitude latitude
184      * @param longitude longitude
185      * @return modip at specified location
186      * @since 13.0
187      */
188     protected abstract <T extends CalculusFieldElement<T>> T computeMODIP(T latitude, T longitude);
189 
190     /**
191      * Computes the electron density at a given height.
192      * @param dateTime date
193      * @param az effective ionization level
194      * @param latitude latitude along the integration path
195      * @param longitude longitude along the integration path
196      * @param h height along the integration path in m
197      * @return electron density [m⁻³]
198      * @since 13.0
199      */
200     public double electronDensity(final DateTimeComponents dateTime, final double az,
201                                   final double latitude, final double longitude, final double h) {
202 
203         // Load the correct CCIR file
204         loadsIfNeeded(dateTime.getDate());
205 
206         final double modip = computeMODIP(latitude, longitude);
207         final NeQuickParameters parameters = new NeQuickParameters(dateTime,
208                                                                    flattenF2[dateTime.getDate().getMonth() - 1],
209                                                                    flattenFm3[dateTime.getDate().getMonth() - 1],
210                                                                    latitude, longitude, az, modip);
211         // Convert height in kilometers
212         final double hInKm = Unit.KILOMETRE.fromSI(h);
213         // Electron density
214         final double n;
215         if (hInKm <= parameters.getHmF2()) {
216             n = bottomElectronDensity(hInKm, parameters);
217         } else {
218             n = topElectronDensity(hInKm, parameters);
219         }
220         return n;
221     }
222 
223     /**
224      * Computes the electron density at a given height.
225      * @param <T> type of the elements
226      * @param dateTime date
227      * @param az effective ionization level
228      * @param latitude latitude along the integration path
229      * @param longitude longitude along the integration path
230      * @param h height along the integration path in m
231      * @return electron density [m⁻³]
232      * @since 13.0
233      */
234     public <T extends CalculusFieldElement<T>> T electronDensity(final DateTimeComponents dateTime, final T az,
235                                                                  final T latitude, final T longitude, final T h) {
236 
237 
238         // Load the correct CCIR file
239         loadsIfNeeded(dateTime.getDate());
240 
241         final T modip = computeMODIP(latitude, longitude);
242         final FieldNeQuickParameters<T> parameters =
243             new FieldNeQuickParameters<>(dateTime,
244                                          flattenF2[dateTime.getDate().getMonth() - 1],
245                                          flattenFm3[dateTime.getDate().getMonth() - 1],
246                                          latitude, longitude, az, modip);
247 
248         // Convert height in kilometers
249         final T hInKm = Unit.KILOMETRE.fromSI(h);
250         // Electron density
251         final T n;
252         if (hInKm.getReal() <= parameters.getHmF2().getReal()) {
253             n = bottomElectronDensity(hInKm, parameters);
254         } else {
255             n = topElectronDensity(hInKm, parameters);
256         }
257         return n;
258     }
259 
260     /**
261      * Computes the electron density of the bottomside.
262      * @param h height in km
263      * @param parameters NeQuick model parameters
264      * @return the electron density N in m⁻³
265      */
266     private double bottomElectronDensity(final double h, final NeQuickParameters parameters) {
267 
268         // Select the relevant B parameter for the current height (Eq. 109 and 110)
269         final double be  = parameters.getBE(h);
270         final double bf1 = parameters.getBF1(h);
271         final double bf2 = parameters.getB2Bot();
272 
273         // Useful array of constants
274         final double[] ct = new double[] {
275             1.0 / bf2, 1.0 / bf1, 1.0 / be
276         };
277 
278         // Compute the exponential argument for each layer (Eq. 111 to 113)
279         final double[] arguments = computeExponentialArguments(h, parameters);
280 
281         // S coefficients
282         final double[] s = new double[3];
283         // Array of corrective terms
284         final double[] ds = new double[3];
285 
286         // Layer amplitudes
287         final double[] amplitudes = parameters.getLayerAmplitudes();
288 
289         // Fill arrays (Eq. 114 to 118)
290         for (int i = 0; i < 3; i++) {
291             if (FastMath.abs(arguments[i]) > 25.0) {
292                 s[i]  = 0.0;
293                 ds[i] = 0.0;
294             } else {
295                 final double expA   = clipExp(arguments[i]);
296                 final double opExpA = 1.0 + expA;
297                 s[i]  = amplitudes[i] * (expA / (opExpA * opExpA));
298                 ds[i] = ct[i] * ((1.0 - expA) / (1.0 + expA));
299             }
300         }
301 
302         // Electron density
303         final double aNo = s[0] + s[1] + s[2];
304         if (applyChapmanParameters(h)) {
305             // Chapman parameters (Eq. 119 and 120)
306             final double bc = 1.0 - 10.0 * (MathArrays.linearCombination(s[0], ds[0], s[1], ds[1], s[2], ds[2]) / aNo);
307             final double z  = 0.1 * (h - 100.0);
308             // Electron density (Eq. 121)
309             return aNo * clipExp(1.0 - bc * z - clipExp(-z)) * DENSITY_FACTOR;
310         } else {
311             return aNo * DENSITY_FACTOR;
312         }
313 
314     }
315 
316     /**
317      * Computes the electron density of the bottomside.
318      * @param <T> type of the elements
319      * @param h height in km
320      * @param parameters NeQuick model parameters
321      * @return the electron density N in m⁻³
322      */
323     private <T extends CalculusFieldElement<T>> T bottomElectronDensity(final T h,
324                                                                         final FieldNeQuickParameters<T> parameters) {
325 
326         // Zero and One
327         final Field<T> field = h.getField();
328         final T zero = field.getZero();
329         final T one  = field.getOne();
330 
331         // Select the relevant B parameter for the current height (Eq. 109 and 110)
332         final T be  = parameters.getBE(h);
333         final T bf1 = parameters.getBF1(h);
334         final T bf2 = parameters.getB2Bot();
335 
336         // Useful array of constants
337         final T[] ct = MathArrays.buildArray(field, 3);
338         ct[0] = bf2.reciprocal();
339         ct[1] = bf1.reciprocal();
340         ct[2] = be.reciprocal();
341 
342         // Compute the exponential argument for each layer (Eq. 111 to 113)
343         final T[] arguments = computeExponentialArguments(h, parameters);
344 
345         // S coefficients
346         final T[] s = MathArrays.buildArray(field, 3);
347         // Array of corrective terms
348         final T[] ds = MathArrays.buildArray(field, 3);
349 
350         // Layer amplitudes
351         final T[] amplitudes = parameters.getLayerAmplitudes();
352 
353         // Fill arrays (Eq. 114 to 118)
354         for (int i = 0; i < 3; i++) {
355             if (FastMath.abs(arguments[i]).getReal() > 25.0) {
356                 s[i]  = zero;
357                 ds[i] = zero;
358             } else {
359                 final T expA   = clipExp(arguments[i]);
360                 final T opExpA = expA.add(1.0);
361                 s[i]  = amplitudes[i].multiply(expA.divide(opExpA.multiply(opExpA)));
362                 ds[i] = ct[i].multiply(expA.negate().add(1.0).divide(expA.add(1.0)));
363             }
364         }
365 
366         // Electron density
367         final T aNo = s[0].add(s[1]).add(s[2]);
368         if (applyChapmanParameters(h.getReal())) {
369             // Chapman parameters (Eq. 119 and 120)
370             final T bc = one.linearCombination(s[0], ds[0], s[1], ds[1], s[2], ds[2]).divide(aNo).multiply(10.0).negate().add(1.0);
371             final T z  = h.subtract(100.0).multiply(0.1);
372             // Electron density (Eq. 121)
373             return aNo.multiply(clipExp(bc.multiply(z).add(clipExp(z.negate())).negate().add(1.0))).multiply(DENSITY_FACTOR);
374         } else {
375             return aNo.multiply(DENSITY_FACTOR);
376         }
377 
378     }
379 
380     /**
381      * Computes the electron density of the topside.
382      * @param h height in km
383      * @param parameters NeQuick model parameters
384      * @return the electron density N in m⁻³
385      */
386     private double topElectronDensity(final double h, final NeQuickParameters parameters) {
387 
388         // Constant parameters (Eq. 122 and 123)
389         final double g = 0.125;
390         final double r = 100.0;
391 
392         // Arguments deltaH and z (Eq. 124 and 125)
393         final double deltaH = h - parameters.getHmF2();
394         final double h0     = computeH0(parameters);
395         final double z      = deltaH / (h0 * (1.0 + (r * g * deltaH) / (r * h0 + g * deltaH)));
396 
397         // Exponential (Eq. 126)
398         final double ee = clipExp(z);
399 
400         // Electron density (Eq. 127)
401         if (ee > 1.0e11) {
402             return (4.0 * parameters.getNmF2() / ee) * DENSITY_FACTOR;
403         } else {
404             final double opExpZ = 1.0 + ee;
405             return ((4.0 * parameters.getNmF2() * ee) / (opExpZ * opExpZ)) * DENSITY_FACTOR;
406         }
407     }
408 
409     /**
410      * Computes the electron density of the topside.
411      * @param <T> type of the elements
412      * @param h height in km
413      * @param parameters NeQuick model parameters
414      * @return the electron density N in m⁻³
415      */
416     private <T extends CalculusFieldElement<T>> T topElectronDensity(final T h,
417                                                                      final FieldNeQuickParameters<T> parameters) {
418 
419         // Constant parameters (Eq. 122 and 123)
420         final double g = 0.125;
421         final double r = 100.0;
422 
423         // Arguments deltaH and z (Eq. 124 and 125)
424         final T deltaH = h.subtract(parameters.getHmF2());
425         final T h0     = computeH0(parameters);
426         final T z      = deltaH.divide(h0.multiply(deltaH.multiply(r).multiply(g).divide(h0.multiply(r).add(deltaH.multiply(g))).add(1.0)));
427 
428         // Exponential (Eq. 126)
429         final T ee = clipExp(z);
430 
431         // Electron density (Eq. 127)
432         if (ee.getReal() > 1.0e11) {
433             return parameters.getNmF2().multiply(4.0).divide(ee).multiply(DENSITY_FACTOR);
434         } else {
435             final T opExpZ = ee.add(1.0);
436             return parameters.getNmF2().multiply(4.0).multiply(ee).divide(opExpZ.multiply(opExpZ)).multiply(DENSITY_FACTOR);
437         }
438     }
439 
440     /**
441      * Lazy loading of CCIR data.
442      * @param date current date components
443      */
444     private void loadsIfNeeded(final DateComponents date) {
445 
446         // Month index
447         final int monthIndex = date.getMonth() - 1;
448 
449         // Check if CCIR has already been loaded for this month
450         if (flattenF2[monthIndex] == null) {
451 
452             // Read file
453             final CCIRLoader loader = new CCIRLoader();
454             loader.loadCCIRCoefficients(date);
455 
456             // Update arrays
457             this.flattenF2[monthIndex]  = flatten(loader.getF2());
458             this.flattenFm3[monthIndex] = flatten(loader.getFm3());
459         }
460     }
461 
462     /** Flatten a 3-dimensions array.
463      * <p>
464      * This method convert 3-dimensions arrays into 1-dimension arrays
465      * optimized to avoid cache misses when looping over all elements.
466      * </p>
467      * @param original original array a[i][j][k]
468      * @return flatten array, for embedded loops on j (outer), k (intermediate), i (inner)
469      */
470     private double[] flatten(final double[][][] original) {
471         final double[] flatten = new double[original.length * original[0].length * original[0][0].length];
472         int index = 0;
473         for (int j = 0; j < original[0].length; j++) {
474             for (int k = 0; k < original[0][0].length; k++) {
475                 for (final double[][] doubles : original) {
476                     flatten[index++] = doubles[j][k];
477                 }
478             }
479         }
480         return flatten;
481     }
482 
483     /**
484      * A clipped exponential function.
485      * <p>
486      * This function, describe in section F.2.12.2 of the reference document, is
487      * recommended for the computation of exponential values.
488      * </p>
489      * @param power power for exponential function
490      * @return clipped exponential value
491      */
492     protected double clipExp(final double power) {
493         if (power > 80.0) {
494             return 5.5406E34;
495         } else if (power < -80) {
496             return 1.8049E-35;
497         } else {
498             return FastMath.exp(power);
499         }
500     }
501 
502     /**
503      * A clipped exponential function.
504      * <p>
505      * This function, describe in section F.2.12.2 of the reference document, is
506      * recommended for the computation of exponential values.
507      * </p>
508      * @param <T> type of the elements
509      * @param power power for exponential function
510      * @return clipped exponential value
511      */
512     protected <T extends CalculusFieldElement<T>> T clipExp(final T power) {
513         if (power.getReal() > 80.0) {
514             return power.newInstance(5.5406E34);
515         } else if (power.getReal() < -80) {
516             return power.newInstance(1.8049E-35);
517         } else {
518             return FastMath.exp(power);
519         }
520     }
521 
522     /**
523      * This method allows the computation of the Slant Total Electron Content (STEC).
524      *
525      * @param dateTime current date
526      * @param ray      ray-perigee parameters
527      * @return the STEC in TECUnits
528      */
529     abstract double stec(DateTimeComponents dateTime, Ray ray);
530 
531     /**
532      * This method allows the computation of the Slant Total Electron Content (STEC).
533      *
534      * @param <T>      type of the field elements
535      * @param dateTime current date
536      * @param ray      ray-perigee parameters
537      * @return the STEC in TECUnits
538      */
539     abstract <T extends CalculusFieldElement<T>> T stec(DateTimeComponents dateTime, FieldRay<T> ray);
540 
541     /**
542      * Check if Chapman parameters should be applied.
543      *
544      * @param hInKm height in kilometers
545      * @return true if Chapman parameters should be applied
546      * @since 13.0
547      */
548     abstract boolean applyChapmanParameters(double hInKm);
549 
550     /**
551      * Compute exponential arguments.
552      * @param h height in km
553      * @param parameters NeQuick model parameters
554      * @return exponential arguments
555      * @since 13.0
556      */
557     abstract double[] computeExponentialArguments(double h, NeQuickParameters parameters);
558 
559     /**
560      * Compute exponential arguments.
561      * @param <T>   type of the field elements
562      * @param h height in km
563      * @param parameters NeQuick model parameters
564      * @return exponential arguments
565      * @since 13.0
566      */
567     abstract <T extends CalculusFieldElement<T>> T[] computeExponentialArguments(T h,
568                                                                                  FieldNeQuickParameters<T> parameters);
569 
570     /**
571      * Compute topside thickness parameter.
572      * @param parameters NeQuick model parameters
573      * @return topside thickness parameter
574      * @since 13.0
575      */
576     abstract double computeH0(NeQuickParameters parameters);
577 
578     /**
579      * Compute topside thickness parameter.
580      * @param <T>   type of the field elements
581      * @param parameters NeQuick model parameters
582      * @return topside thickness parameter
583      * @since 13.0
584      */
585     abstract <T extends CalculusFieldElement<T>> T computeH0(FieldNeQuickParameters<T> parameters);
586 
587 }