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 }