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