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 org.hipparchus.util.FastMath;
20  import org.hipparchus.util.SinCos;
21  import org.orekit.time.DateComponents;
22  import org.orekit.time.DateTimeComponents;
23  import org.orekit.time.TimeComponents;
24  
25  /**
26   * This class performs the computation of the parameters used by the NeQuick model.
27   *
28   * @author Bryan Cazabonne
29   *
30   * @see "European Union (2016). European GNSS (Galileo) Open Service-Ionospheric Correction
31   *       Algorithm for Galileo Single Frequency Users. 1.2."
32   * @see <a href="https://www.itu.int/rec/R-REC-P.531/en">ITU-R P.531</a>
33   *
34   * @since 10.1
35   */
36  public class NeQuickParameters {
37  
38      /** Solar zenith angle at day night transition, degrees. */
39      private static final double X0 = 86.23292796211615;
40  
41      /** Current date time components.
42       * @since 13.0
43       */
44      private final DateTimeComponents dateTime;
45  
46      /** Effective sunspot number.
47       * @since 13.0
48       */
49      private final double azr;
50  
51      /** F2 layer critical frequency.
52       * @since 13.0
53       */
54      private final double foF2;
55  
56      /** F2 layer maximum density. */
57      private final double nmF2;
58  
59      /** F2 layer maximum density height [km]. */
60      private final double hmF2;
61  
62      /** F1 layer maximum density height [km]. */
63      private final double hmF1;
64  
65      /** E layer maximum density height [km]. */
66      private final double hmE;
67  
68      /** F2 layer bottom thickness parameter [km]. */
69      private final double b2Bot;
70  
71      /** F1 layer top thickness parameter [km]. */
72      private final double b1Top;
73  
74      /** F1 layer bottom thickness parameter [km]. */
75      private final double b1Bot;
76  
77      /** E layer top thickness parameter [km]. */
78      private final double beTop;
79  
80      /** E layer bottom thickness parameter [km]. */
81      private final double beBot;
82  
83      /** Layer amplitudes. */
84      private final double[] amplitudes;
85  
86      /**
87       * Build a new instance.
88       * @param dateTime current date time components
89       * @param flattenF2 F2 coefficients used by the F2 layer (flatten array)
90       * @param flattenFm3 Fm3 coefficients used by the M(3000)F2 layer (flatten array)
91       * @param latitude latitude of a point along the integration path, in radians
92       * @param longitude longitude of a point along the integration path, in radians
93       * @param az effective ionisation level
94       * @param modip modip
95       */
96      public NeQuickParameters(final DateTimeComponents dateTime,
97                               final double[] flattenF2, final double[] flattenFm3,
98                               final double latitude, final double longitude, final double az, final double modip) {
99          this(new FourierTimeSeries(dateTime, az, flattenF2, flattenFm3),
100              latitude, longitude, modip);
101     }
102 
103     /**
104      * Build a new instance.
105      * @param fourierTimeSeries Fourier time series for foF2 and M(3000)F2 layers
106      * @param latitude latitude of a point along the integration path, in radians
107      * @param longitude longitude of a point along the integration path, in radians
108      * @param modip modip
109      * @since 13.0.1
110      */
111     public NeQuickParameters(final FourierTimeSeries fourierTimeSeries,
112                              final double latitude, final double longitude, final double modip) {
113 
114         this.dateTime = fourierTimeSeries.getDateTime();
115         this.azr      = fourierTimeSeries.getAzr();
116 
117         // Date and Time components
118         final DateComponents date = dateTime.getDate();
119         final TimeComponents time = dateTime.getTime();
120         // Hours
121         final double hours  = time.getSecondsInUTCDay() / 3600.0;
122         // Effective solar zenith angle in radians
123         final double xeff = computeEffectiveSolarAngle(date.getMonth(), hours, latitude, longitude);
124 
125         // E layer maximum density height in km (Eq. 78)
126         this.hmE = 120.0;
127         // E layer critical frequency in MHz
128         final double foE = computefoE(date.getMonth(), fourierTimeSeries.getAz(), xeff, latitude);
129         // E layer maximum density in 10^11 m⁻³ (Eq. 36)
130         final double nmE = 0.124 * foE * foE;
131 
132         // F2 layer critical frequency in MHz
133         final double[] scL = FourierTimeSeries.sinCos(longitude, 8);
134         this.foF2 = computefoF2(modip, fourierTimeSeries.getCf2Reference(), latitude, scL);
135         // Maximum Usable Frequency factor
136         final double mF2  = computeMF2(modip, fourierTimeSeries.getCm3Reference(), latitude, scL);
137         // F2 layer maximum density in 10^11 m⁻³
138         this.nmF2 = 0.124 * foF2 * foF2;
139         // F2 layer maximum density height in km
140         this.hmF2 = computehmF2(foE, mF2);
141 
142         // F1 layer critical frequency in MHz
143         final double foF1 = computefoF1(foE);
144         // F1 layer maximum density in 10^11 m⁻³
145         final double nmF1;
146         if (foF1 <= 0.0 && foE > 2.0) {
147             final double foEpopf = foE + 0.5;
148             nmF1 = 0.124 * foEpopf * foEpopf;
149         } else {
150             nmF1 = 0.124 * foF1 * foF1;
151         }
152         // F1 layer maximum density height in km
153         this.hmF1 = 0.5 * (hmF2 + hmE);
154 
155         // Thickness parameters (Eq. 85 to 89)
156         final double a = 0.01 * clipExp(-3.467 + 0.857 * FastMath.log(foF2 * foF2) + 2.02 * FastMath.log(mF2));
157         this.b2Bot = 0.385 * nmF2 / a;
158         this.b1Top = 0.3 * (hmF2 - hmF1);
159         this.b1Bot = 0.5 * (hmF1 - hmE);
160         this.beTop = FastMath.max(b1Bot, 7.0);
161         this.beBot = 5.0;
162 
163         // Layer amplitude coefficients
164         this.amplitudes = computeLayerAmplitudes(nmE, nmF1, foF1);
165 
166     }
167 
168     /**
169      * Get current date time components.
170      * @return current date time components
171      * @since 13.0
172      */
173     public DateTimeComponents getDateTime() {
174         return dateTime;
175     }
176 
177     /**
178      * Get effective sunspot number.
179      * @return effective sunspot number
180      * @since 13.0
181      */
182     public double getAzr() {
183         return azr;
184     }
185 
186     /**
187      * Get F2 layer critical frequency.
188      * @return F2 layer critical frequency
189      * @since 13.0
190      */
191     public double getFoF2() {
192         return foF2;
193     }
194 
195      /**
196      * Get the F2 layer maximum density.
197      * @return nmF2
198      */
199     public double getNmF2() {
200         return nmF2;
201     }
202 
203     /**
204      * Get the F2 layer maximum density height.
205      * @return hmF2 in km
206      */
207     public double getHmF2() {
208         return hmF2;
209     }
210 
211     /**
212      * Get the F1 layer maximum density height.
213      * @return hmF1 in km
214      */
215     public double getHmF1() {
216         return hmF1;
217     }
218 
219     /**
220      * Get the E layer maximum density height.
221      * @return hmE in km
222      */
223     public double getHmE() {
224         return hmE;
225     }
226 
227     /**
228      * Get the F2 layer thickness parameter (bottom).
229      * @return B2Bot in km
230      */
231     public double getB2Bot() {
232         return b2Bot;
233     }
234 
235     /**
236      * Get the F1 layer thickness parameter.
237      * @param h current height (km)
238      * @return B1 in km
239      * @since 13.0
240      */
241     public double getBF1(final double h) {
242         // Eq. 110
243         return (h > hmF1) ? b1Top : b1Bot;
244     }
245 
246     /**
247      * Get the E layer thickness parameter.
248      * @param h current height (km)
249      * @return Be in km
250      * @since 13.0
251      */
252     public double getBE(final double h) {
253         // Eq. 109
254         return (h > hmE) ? beTop : beBot;
255     }
256 
257     /**
258      * Get the F2, F1 and E layer amplitudes.
259      * <p>
260      * The resulting element is an array having the following form:
261      * <ul>
262      * <li>double[0] = A1 → F2 layer amplitude
263      * <li>double[1] = A2 → F1 layer amplitude
264      * <li>double[2] = A3 → E  layer amplitude
265      * </ul>
266      * @return layer amplitudes
267      */
268     public double[] getLayerAmplitudes() {
269         return amplitudes.clone();
270     }
271 
272     /**
273      * This method computes the effective solar zenith angle.
274      * <p>
275      * The effective solar zenith angle is compute as a function of the
276      * solar zenith angle and the solar zenith angle at day night transition.
277      * </p>
278      * @param month current month of the year
279      * @param hours universal time (hours)
280      * @param latitude in radians
281      * @param longitude in radians
282      * @return the effective solar zenith angle, radians
283      */
284     private double computeEffectiveSolarAngle(final int month,
285                                               final double hours,
286                                               final double latitude,
287                                               final double longitude) {
288         // Local time (Eq.4)
289         final double lt = hours + longitude / FastMath.toRadians(15.0);
290         // Day of year at the middle of the month (Eq. 20)
291         final double dy = 30.5 * month - 15.0;
292         // Time (Eq. 21)
293         final double t = dy + (18 - hours) / 24;
294         // Arguments am and al (Eq. 22 and 23)
295         final double am = FastMath.toRadians(0.9856 * t - 3.289);
296         final double al = am + FastMath.toRadians(1.916 * FastMath.sin(am) + 0.020 * FastMath.sin(2.0 * am) + 282.634);
297         // Sine and cosine of solar declination (Eq. 24 and 25)
298         final double sDec = 0.39782 * FastMath.sin(al);
299         final double cDec = FastMath.sqrt(1. - sDec * sDec);
300         // Solar zenith angle, deg (Eq. 26 and 27)
301         final SinCos scLat   = FastMath.sinCos(latitude);
302         final double coef    = (FastMath.PI / 12) * (12 - lt);
303         final double cZenith = scLat.sin() * sDec + scLat.cos() * cDec * FastMath.cos(coef);
304         final double angle   = FastMath.atan2(FastMath.sqrt(1.0 - cZenith * cZenith), cZenith);
305         final double x       = FastMath.toDegrees(angle);
306         // Effective solar zenith angle (Eq. 28)
307         final double xeff = join(90.0 - 0.24 * clipExp(20.0 - 0.2 * x), x, 12.0, x - X0);
308         return FastMath.toRadians(xeff);
309     }
310 
311     /**
312      * This method computes the E layer critical frequency at a given location.
313      * @param month current month
314      * @param az ffective ionisation level
315      * @param xeff effective solar zenith angle in radians
316      * @param latitude latitude in radians
317      * @return the E layer critical frequency at a given location in MHz
318      */
319     private double computefoE(final int month, final double az,
320                               final double xeff, final double latitude) {
321         // The latitude has to be converted in degrees
322         final double lat = FastMath.toDegrees(latitude);
323         // Square root of the effective ionisation level
324         final double sqAz = FastMath.sqrt(az);
325         // seas parameter (Eq. 30 to 32)
326         final int seas;
327         if (month == 1 || month == 2 || month == 11 || month == 12) {
328             seas = -1;
329         } else if (month == 3 || month == 4 || month == 9 || month == 10) {
330             seas = 0;
331         } else {
332             seas = 1;
333         }
334         // Latitudinal dependence (Eq. 33 and 34)
335         final double ee = clipExp(0.3 * lat);
336         final double seasp = seas * ((ee - 1.0) / (ee + 1.0));
337         // Critical frequency (Eq. 35)
338         final double coef = 1.112 - 0.019 * seasp;
339         return FastMath.sqrt(coef * coef * sqAz * FastMath.pow(FastMath.cos(xeff), 0.6) + 0.49);
340 
341     }
342 
343     /**
344      * Computes the F2 layer height of maximum electron density.
345      * @param foE E layer layer critical frequency in MHz
346      * @param mF2 maximum usable frequency factor
347      * @return hmF2 in km
348      */
349     private double computehmF2(final double foE, final double mF2) {
350         // Ratio
351         final double fo = foF2 / foE;
352         final double ratio = join(fo, 1.75, 20.0, fo - 1.75);
353 
354         // deltaM parameter
355         double deltaM = -0.012;
356         if (foE >= 1e-30) {
357             deltaM += 0.253 / (ratio - 1.215);
358         }
359 
360         // hmF2 Eq. 80
361         final double mF2Sq = mF2 * mF2;
362         final double temp  = FastMath.sqrt((0.0196 * mF2Sq + 1) / (1.2967 * mF2Sq - 1.0));
363         return ((1490.0 * mF2 * temp) / (mF2 + deltaM)) - 176.0;
364 
365     }
366 
367     /**
368      * This method computes the F2 layer critical frequency.
369      * @param modip modified DIP latitude, in degrees
370      * @param cf2 Fourier time series for foF2
371      * @param latitude latitude in radians
372      * @param scL sines/cosines array of longitude argument
373      * @return the F2 layer critical frequency, MHz
374      */
375     private double computefoF2(final double modip, final double[] cf2,
376                                final double latitude, final double[] scL) {
377 
378         // Legendre grades (Eq. 63)
379         final int[] q = new int[] {
380             12, 12, 9, 5, 2, 1, 1, 1, 1
381         };
382 
383         double frequency = cf2[0];
384 
385         // ModipGrid coefficients Eq. 57
386         final double sinMODIP = FastMath.sin(FastMath.toRadians(modip));
387         final double[] m = new double[12];
388         m[0] = 1.0;
389         for (int i = 1; i < q[0]; i++) {
390             m[i] = sinMODIP * m[i - 1];
391             frequency += m[i] * cf2[i];
392         }
393 
394         // latitude and longitude terms
395         int index = 12;
396         final double cosLat1 = FastMath.cos(latitude);
397         double cosLatI = cosLat1;
398         for (int i = 1; i < q.length; i++) {
399             final double c = cosLatI * scL[2 * i - 1];
400             final double s = cosLatI * scL[2 * i - 2];
401             for (int j = 0; j < q[i]; j++) {
402                 frequency += m[j] * c * cf2[index++];
403                 frequency += m[j] * s * cf2[index++];
404             }
405             cosLatI *= cosLat1; // Eq. 58
406         }
407 
408         return frequency;
409 
410     }
411 
412     /**
413      * This method computes the Maximum Usable Frequency factor.
414      * @param modip modified DIP latitude, in degrees
415      * @param cm3 Fourier time series for M(3000)F2
416      * @param latitude latitude in radians
417      * @param scL sines/cosines array of longitude argument
418      * @return the Maximum Usable Frequency factor
419      */
420     private double computeMF2(final double modip, final double[] cm3,
421                               final double latitude, final double[] scL) {
422 
423         // Legendre grades (Eq. 71)
424         final int[] r = new int[] {
425             7, 8, 6, 3, 2, 1, 1
426         };
427 
428         double m3000 = cm3[0];
429 
430         // ModipGrid coefficients Eq. 57
431         final double sinMODIP = FastMath.sin(FastMath.toRadians(modip));
432         final double[] m = new double[12];
433         m[0] = 1.0;
434         for (int i = 1; i < 12; i++) {
435             m[i] = sinMODIP * m[i - 1];
436             if (i < 7) {
437                 m3000 += m[i] * cm3[i];
438             }
439         }
440 
441         // latitude and longitude terms
442         int index = 7;
443         final double cosLat1 = FastMath.cos(latitude);
444         double cosLatI = cosLat1;
445         for (int i = 1; i < r.length; i++) {
446             final double c = cosLatI * scL[2 * i - 1];
447             final double s = cosLatI * scL[2 * i - 2];
448             for (int j = 0; j < r[i]; j++) {
449                 m3000 += m[j] * c * cm3[index++];
450                 m3000 += m[j] * s * cm3[index++];
451             }
452             cosLatI *= cosLat1; // Eq. 58
453         }
454 
455         return m3000;
456 
457     }
458 
459     /**
460      * This method computes the F1 layer critical frequency.
461      * <p>
462      * This computation performs the algorithm exposed in Annex F
463      * of the reference document.
464      * </p>
465      * @param foE the E layer critical frequency, MHz
466      * @return the F1 layer critical frequency, MHz
467      */
468     private double computefoF1(final double foE) {
469         final double temp  = join(1.4 * foE, 0.0, 1000.0, foE - 2.0);
470         final double temp2 = join(0.0, temp, 1000.0, foE - temp);
471         final double value = join(temp2, 0.85 * temp2, 60.0, 0.85 * foF2 - temp2);
472         if (value < 1.0E-6) {
473             return 0.0;
474         } else {
475             return value;
476         }
477     }
478 
479     /**
480      * This method allows the computation of the F2, F1 and E layer amplitudes.
481      * <p>
482      * The resulting element is an array having the following form:
483      * <ul>
484      * <li>double[0] = A1 → F2 layer amplitude
485      * <li>double[1] = A2 → F1 layer amplitude
486      * <li>double[2] = A3 → E  layer amplitude
487      * </ul>
488      * </p>
489      * @param nmE E layer maximum density in 10^11 m⁻³
490      * @param nmF1 F1 layer maximum density in 10^11 m⁻³
491      * @param foF1 F1 layer critical frequency in MHz
492      * @return a three components array containing the layer amplitudes
493      */
494     private double[] computeLayerAmplitudes(final double nmE, final double nmF1, final double foF1) {
495         // Initialize array
496         final double[] amplitude = new double[3];
497 
498         // F2 layer amplitude (Eq. 90)
499         final double a1 = 4.0 * nmF2;
500         amplitude[0]   = a1;
501 
502         // F1 and E layer amplitudes (Eq. 91 to 98)
503         if (foF1 < 0.5) {
504             amplitude[1] = 0.0;
505             amplitude[2] = 4.0 * (nmE - epst(a1, hmF2, b2Bot, hmE));
506         } else {
507             double a2a = 0.0;
508             double a3a = 4.0 * nmE;
509             for (int i = 0; i < 5; i++) {
510                 a2a = 4.0 * (nmF1 - epst(a1, hmF2, b2Bot, hmF1) - epst(a3a, hmE, beTop, hmF1));
511                 a2a = join(a2a, 0.8 * nmF1, 1.0, a2a - 0.8 * nmF1);
512                 a3a = 4.0 * (nmE - epst(a2a, hmF1, b1Bot, hmE) - epst(a1, hmF2, b2Bot, hmE));
513             }
514             amplitude[1] = a2a;
515             amplitude[2] = join(a3a, 0.05, 60.0, a3a - 0.005);
516         }
517 
518         return amplitude;
519     }
520 
521     /**
522      * A clipped exponential function.
523      * <p>
524      * This function, describe in section F.2.12.2 of the reference document, is
525      * recommanded for the computation of exponential values.
526      * </p>
527      * @param power power for exponential function
528      * @return clipped exponential value
529      */
530     private double clipExp(final double power) {
531         if (power > 80.0) {
532             return 5.5406E34;
533         } else if (power < -80) {
534             return 1.8049E-35;
535         } else {
536             return FastMath.exp(power);
537         }
538     }
539 
540     /**
541      * Allows smooth joining of functions f1 and f2
542      * (i.e. continuous first derivatives) at origin.
543      * <p>
544      * This function, describe in section F.2.12.1 of the reference document, is
545      * recommended for computational efficiency.
546      * </p>
547      * @param dF1 first function
548      * @param dF2 second function
549      * @param dA width of transition region
550      * @param dX x value
551      * @return the computed value
552      */
553     double join(final double dF1, final double dF2, final double dA, final double dX) {
554         final double ee = clipExp(dA * dX);
555         return (dF1 * ee + dF2) / (ee + 1.0);
556     }
557 
558     /**
559      * The Epstein function.
560      * <p>
561      * This function, describe in section 2.5.1 of the reference document, is used
562      * as a basis analytical function in NeQuick for the construction of the ionospheric layers.
563      * </p>
564      * @param x x parameter
565      * @param y y parameter
566      * @param z z parameter
567      * @param w w parameter
568      * @return value of the epstein function
569      */
570     private double epst(final double x, final double y,
571                         final double z, final double w) {
572         final double ex  = clipExp((w - y) / z);
573         final double opex = 1.0 + ex;
574         return x * ex / (opex * opex);
575 
576     }
577 
578 }