NeQuickParameters.java

  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. import org.hipparchus.util.FastMath;
  19. import org.hipparchus.util.SinCos;
  20. import org.orekit.time.DateComponents;
  21. import org.orekit.time.DateTimeComponents;
  22. import org.orekit.time.TimeComponents;

  23. /**
  24.  * This class performs the computation of the parameters used by the NeQuick model.
  25.  *
  26.  * @author Bryan Cazabonne
  27.  *
  28.  * @see "European Union (2016). European GNSS (Galileo) Open Service-Ionospheric Correction
  29.  *       Algorithm for Galileo Single Frequency Users. 1.2."
  30.  * @see <a href="https://www.itu.int/rec/R-REC-P.531/en">ITU-R P.531</a>
  31.  *
  32.  * @since 10.1
  33.  */
  34. public class NeQuickParameters {

  35.     /** Solar zenith angle at day night transition, degrees. */
  36.     private static final double X0 = 86.23292796211615;

  37.     /** Current date time components.
  38.      * @since 13.0
  39.      */
  40.     private final DateTimeComponents dateTime;

  41.     /** Effective sunspot number.
  42.      * @since 13.0
  43.      */
  44.     private final double azr;

  45.     /** F2 layer critical frequency.
  46.      * @since 13.0
  47.      */
  48.     private final double foF2;

  49.     /** F2 layer maximum density. */
  50.     private final double nmF2;

  51.     /** F2 layer maximum density height [km]. */
  52.     private final double hmF2;

  53.     /** F1 layer maximum density height [km]. */
  54.     private final double hmF1;

  55.     /** E layer maximum density height [km]. */
  56.     private final double hmE;

  57.     /** F2 layer bottom thickness parameter [km]. */
  58.     private final double b2Bot;

  59.     /** F1 layer top thickness parameter [km]. */
  60.     private final double b1Top;

  61.     /** F1 layer bottom thickness parameter [km]. */
  62.     private final double b1Bot;

  63.     /** E layer top thickness parameter [km]. */
  64.     private final double beTop;

  65.     /** E layer bottom thickness parameter [km]. */
  66.     private final double beBot;

  67.     /** Layer amplitudes. */
  68.     private final double[] amplitudes;

  69.     /**
  70.      * Build a new instance.
  71.      * @param dateTime current date time components
  72.      * @param flattenF2 F2 coefficients used by the F2 layer (flatten array)
  73.      * @param flattenFm3 Fm3 coefficients used by the M(3000)F2 layer (flatten array)
  74.      * @param latitude latitude of a point along the integration path, in radians
  75.      * @param longitude longitude of a point along the integration path, in radians
  76.      * @param az effective ionisation level
  77.      * @param modip modip
  78.      */
  79.     public NeQuickParameters(final DateTimeComponents dateTime,
  80.                              final double[] flattenF2, final double[] flattenFm3,
  81.                              final double latitude, final double longitude, final double az, final double modip) {
  82.         this(new FourierTimeSeries(dateTime, az, flattenF2, flattenFm3),
  83.              latitude, longitude, modip);
  84.     }

  85.     /**
  86.      * Build a new instance.
  87.      * @param fourierTimeSeries Fourier time series for foF2 and M(3000)F2 layers
  88.      * @param latitude latitude of a point along the integration path, in radians
  89.      * @param longitude longitude of a point along the integration path, in radians
  90.      * @param modip modip
  91.      * @since 13.0.1
  92.      */
  93.     public NeQuickParameters(final FourierTimeSeries fourierTimeSeries,
  94.                              final double latitude, final double longitude, final double modip) {

  95.         this.dateTime = fourierTimeSeries.getDateTime();
  96.         this.azr      = fourierTimeSeries.getAzr();

  97.         // Date and Time components
  98.         final DateComponents date = dateTime.getDate();
  99.         final TimeComponents time = dateTime.getTime();
  100.         // Hours
  101.         final double hours  = time.getSecondsInUTCDay() / 3600.0;
  102.         // Effective solar zenith angle in radians
  103.         final double xeff = computeEffectiveSolarAngle(date.getMonth(), hours, latitude, longitude);

  104.         // E layer maximum density height in km (Eq. 78)
  105.         this.hmE = 120.0;
  106.         // E layer critical frequency in MHz
  107.         final double foE = computefoE(date.getMonth(), fourierTimeSeries.getAz(), xeff, latitude);
  108.         // E layer maximum density in 10^11 m⁻³ (Eq. 36)
  109.         final double nmE = 0.124 * foE * foE;

  110.         // F2 layer critical frequency in MHz
  111.         final double[] scL = FourierTimeSeries.sinCos(longitude, 8);
  112.         this.foF2 = computefoF2(modip, fourierTimeSeries.getCf2Reference(), latitude, scL);
  113.         // Maximum Usable Frequency factor
  114.         final double mF2  = computeMF2(modip, fourierTimeSeries.getCm3Reference(), latitude, scL);
  115.         // F2 layer maximum density in 10^11 m⁻³
  116.         this.nmF2 = 0.124 * foF2 * foF2;
  117.         // F2 layer maximum density height in km
  118.         this.hmF2 = computehmF2(foE, mF2);

  119.         // F1 layer critical frequency in MHz
  120.         final double foF1 = computefoF1(foE);
  121.         // F1 layer maximum density in 10^11 m⁻³
  122.         final double nmF1;
  123.         if (foF1 <= 0.0 && foE > 2.0) {
  124.             final double foEpopf = foE + 0.5;
  125.             nmF1 = 0.124 * foEpopf * foEpopf;
  126.         } else {
  127.             nmF1 = 0.124 * foF1 * foF1;
  128.         }
  129.         // F1 layer maximum density height in km
  130.         this.hmF1 = 0.5 * (hmF2 + hmE);

  131.         // Thickness parameters (Eq. 85 to 89)
  132.         final double a = 0.01 * clipExp(-3.467 + 0.857 * FastMath.log(foF2 * foF2) + 2.02 * FastMath.log(mF2));
  133.         this.b2Bot = 0.385 * nmF2 / a;
  134.         this.b1Top = 0.3 * (hmF2 - hmF1);
  135.         this.b1Bot = 0.5 * (hmF1 - hmE);
  136.         this.beTop = FastMath.max(b1Bot, 7.0);
  137.         this.beBot = 5.0;

  138.         // Layer amplitude coefficients
  139.         this.amplitudes = computeLayerAmplitudes(nmE, nmF1, foF1);

  140.     }

  141.     /**
  142.      * Get current date time components.
  143.      * @return current date time components
  144.      * @since 13.0
  145.      */
  146.     public DateTimeComponents getDateTime() {
  147.         return dateTime;
  148.     }

  149.     /**
  150.      * Get effective sunspot number.
  151.      * @return effective sunspot number
  152.      * @since 13.0
  153.      */
  154.     public double getAzr() {
  155.         return azr;
  156.     }

  157.     /**
  158.      * Get F2 layer critical frequency.
  159.      * @return F2 layer critical frequency
  160.      * @since 13.0
  161.      */
  162.     public double getFoF2() {
  163.         return foF2;
  164.     }

  165.      /**
  166.      * Get the F2 layer maximum density.
  167.      * @return nmF2
  168.      */
  169.     public double getNmF2() {
  170.         return nmF2;
  171.     }

  172.     /**
  173.      * Get the F2 layer maximum density height.
  174.      * @return hmF2 in km
  175.      */
  176.     public double getHmF2() {
  177.         return hmF2;
  178.     }

  179.     /**
  180.      * Get the F1 layer maximum density height.
  181.      * @return hmF1 in km
  182.      */
  183.     public double getHmF1() {
  184.         return hmF1;
  185.     }

  186.     /**
  187.      * Get the E layer maximum density height.
  188.      * @return hmE in km
  189.      */
  190.     public double getHmE() {
  191.         return hmE;
  192.     }

  193.     /**
  194.      * Get the F2 layer thickness parameter (bottom).
  195.      * @return B2Bot in km
  196.      */
  197.     public double getB2Bot() {
  198.         return b2Bot;
  199.     }

  200.     /**
  201.      * Get the F1 layer thickness parameter.
  202.      * @param h current height (km)
  203.      * @return B1 in km
  204.      * @since 13.0
  205.      */
  206.     public double getBF1(final double h) {
  207.         // Eq. 110
  208.         return (h > hmF1) ? b1Top : b1Bot;
  209.     }

  210.     /**
  211.      * Get the E layer thickness parameter.
  212.      * @param h current height (km)
  213.      * @return Be in km
  214.      * @since 13.0
  215.      */
  216.     public double getBE(final double h) {
  217.         // Eq. 109
  218.         return (h > hmE) ? beTop : beBot;
  219.     }

  220.     /**
  221.      * Get the F2, F1 and E layer amplitudes.
  222.      * <p>
  223.      * The resulting element is an array having the following form:
  224.      * <ul>
  225.      * <li>double[0] = A1 → F2 layer amplitude
  226.      * <li>double[1] = A2 → F1 layer amplitude
  227.      * <li>double[2] = A3 → E  layer amplitude
  228.      * </ul>
  229.      * @return layer amplitudes
  230.      */
  231.     public double[] getLayerAmplitudes() {
  232.         return amplitudes.clone();
  233.     }

  234.     /**
  235.      * This method computes the effective solar zenith angle.
  236.      * <p>
  237.      * The effective solar zenith angle is compute as a function of the
  238.      * solar zenith angle and the solar zenith angle at day night transition.
  239.      * </p>
  240.      * @param month current month of the year
  241.      * @param hours universal time (hours)
  242.      * @param latitude in radians
  243.      * @param longitude in radians
  244.      * @return the effective solar zenith angle, radians
  245.      */
  246.     private double computeEffectiveSolarAngle(final int month,
  247.                                               final double hours,
  248.                                               final double latitude,
  249.                                               final double longitude) {
  250.         // Local time (Eq.4)
  251.         final double lt = hours + longitude / FastMath.toRadians(15.0);
  252.         // Day of year at the middle of the month (Eq. 20)
  253.         final double dy = 30.5 * month - 15.0;
  254.         // Time (Eq. 21)
  255.         final double t = dy + (18 - hours) / 24;
  256.         // Arguments am and al (Eq. 22 and 23)
  257.         final double am = FastMath.toRadians(0.9856 * t - 3.289);
  258.         final double al = am + FastMath.toRadians(1.916 * FastMath.sin(am) + 0.020 * FastMath.sin(2.0 * am) + 282.634);
  259.         // Sine and cosine of solar declination (Eq. 24 and 25)
  260.         final double sDec = 0.39782 * FastMath.sin(al);
  261.         final double cDec = FastMath.sqrt(1. - sDec * sDec);
  262.         // Solar zenith angle, deg (Eq. 26 and 27)
  263.         final SinCos scLat   = FastMath.sinCos(latitude);
  264.         final double coef    = (FastMath.PI / 12) * (12 - lt);
  265.         final double cZenith = scLat.sin() * sDec + scLat.cos() * cDec * FastMath.cos(coef);
  266.         final double angle   = FastMath.atan2(FastMath.sqrt(1.0 - cZenith * cZenith), cZenith);
  267.         final double x       = FastMath.toDegrees(angle);
  268.         // Effective solar zenith angle (Eq. 28)
  269.         final double xeff = join(90.0 - 0.24 * clipExp(20.0 - 0.2 * x), x, 12.0, x - X0);
  270.         return FastMath.toRadians(xeff);
  271.     }

  272.     /**
  273.      * This method computes the E layer critical frequency at a given location.
  274.      * @param month current month
  275.      * @param az ffective ionisation level
  276.      * @param xeff effective solar zenith angle in radians
  277.      * @param latitude latitude in radians
  278.      * @return the E layer critical frequency at a given location in MHz
  279.      */
  280.     private double computefoE(final int month, final double az,
  281.                               final double xeff, final double latitude) {
  282.         // The latitude has to be converted in degrees
  283.         final double lat = FastMath.toDegrees(latitude);
  284.         // Square root of the effective ionisation level
  285.         final double sqAz = FastMath.sqrt(az);
  286.         // seas parameter (Eq. 30 to 32)
  287.         final int seas;
  288.         if (month == 1 || month == 2 || month == 11 || month == 12) {
  289.             seas = -1;
  290.         } else if (month == 3 || month == 4 || month == 9 || month == 10) {
  291.             seas = 0;
  292.         } else {
  293.             seas = 1;
  294.         }
  295.         // Latitudinal dependence (Eq. 33 and 34)
  296.         final double ee = clipExp(0.3 * lat);
  297.         final double seasp = seas * ((ee - 1.0) / (ee + 1.0));
  298.         // Critical frequency (Eq. 35)
  299.         final double coef = 1.112 - 0.019 * seasp;
  300.         return FastMath.sqrt(coef * coef * sqAz * FastMath.pow(FastMath.cos(xeff), 0.6) + 0.49);

  301.     }

  302.     /**
  303.      * Computes the F2 layer height of maximum electron density.
  304.      * @param foE E layer layer critical frequency in MHz
  305.      * @param mF2 maximum usable frequency factor
  306.      * @return hmF2 in km
  307.      */
  308.     private double computehmF2(final double foE, final double mF2) {
  309.         // Ratio
  310.         final double fo = foF2 / foE;
  311.         final double ratio = join(fo, 1.75, 20.0, fo - 1.75);

  312.         // deltaM parameter
  313.         double deltaM = -0.012;
  314.         if (foE >= 1e-30) {
  315.             deltaM += 0.253 / (ratio - 1.215);
  316.         }

  317.         // hmF2 Eq. 80
  318.         final double mF2Sq = mF2 * mF2;
  319.         final double temp  = FastMath.sqrt((0.0196 * mF2Sq + 1) / (1.2967 * mF2Sq - 1.0));
  320.         return ((1490.0 * mF2 * temp) / (mF2 + deltaM)) - 176.0;

  321.     }

  322.     /**
  323.      * This method computes the F2 layer critical frequency.
  324.      * @param modip modified DIP latitude, in degrees
  325.      * @param cf2 Fourier time series for foF2
  326.      * @param latitude latitude in radians
  327.      * @param scL sines/cosines array of longitude argument
  328.      * @return the F2 layer critical frequency, MHz
  329.      */
  330.     private double computefoF2(final double modip, final double[] cf2,
  331.                                final double latitude, final double[] scL) {

  332.         // Legendre grades (Eq. 63)
  333.         final int[] q = new int[] {
  334.             12, 12, 9, 5, 2, 1, 1, 1, 1
  335.         };

  336.         double frequency = cf2[0];

  337.         // ModipGrid coefficients Eq. 57
  338.         final double sinMODIP = FastMath.sin(FastMath.toRadians(modip));
  339.         final double[] m = new double[12];
  340.         m[0] = 1.0;
  341.         for (int i = 1; i < q[0]; i++) {
  342.             m[i] = sinMODIP * m[i - 1];
  343.             frequency += m[i] * cf2[i];
  344.         }

  345.         // latitude and longitude terms
  346.         int index = 12;
  347.         final double cosLat1 = FastMath.cos(latitude);
  348.         double cosLatI = cosLat1;
  349.         for (int i = 1; i < q.length; i++) {
  350.             final double c = cosLatI * scL[2 * i - 1];
  351.             final double s = cosLatI * scL[2 * i - 2];
  352.             for (int j = 0; j < q[i]; j++) {
  353.                 frequency += m[j] * c * cf2[index++];
  354.                 frequency += m[j] * s * cf2[index++];
  355.             }
  356.             cosLatI *= cosLat1; // Eq. 58
  357.         }

  358.         return frequency;

  359.     }

  360.     /**
  361.      * This method computes the Maximum Usable Frequency factor.
  362.      * @param modip modified DIP latitude, in degrees
  363.      * @param cm3 Fourier time series for M(3000)F2
  364.      * @param latitude latitude in radians
  365.      * @param scL sines/cosines array of longitude argument
  366.      * @return the Maximum Usable Frequency factor
  367.      */
  368.     private double computeMF2(final double modip, final double[] cm3,
  369.                               final double latitude, final double[] scL) {

  370.         // Legendre grades (Eq. 71)
  371.         final int[] r = new int[] {
  372.             7, 8, 6, 3, 2, 1, 1
  373.         };

  374.         double m3000 = cm3[0];

  375.         // ModipGrid coefficients Eq. 57
  376.         final double sinMODIP = FastMath.sin(FastMath.toRadians(modip));
  377.         final double[] m = new double[12];
  378.         m[0] = 1.0;
  379.         for (int i = 1; i < 12; i++) {
  380.             m[i] = sinMODIP * m[i - 1];
  381.             if (i < 7) {
  382.                 m3000 += m[i] * cm3[i];
  383.             }
  384.         }

  385.         // latitude and longitude terms
  386.         int index = 7;
  387.         final double cosLat1 = FastMath.cos(latitude);
  388.         double cosLatI = cosLat1;
  389.         for (int i = 1; i < r.length; i++) {
  390.             final double c = cosLatI * scL[2 * i - 1];
  391.             final double s = cosLatI * scL[2 * i - 2];
  392.             for (int j = 0; j < r[i]; j++) {
  393.                 m3000 += m[j] * c * cm3[index++];
  394.                 m3000 += m[j] * s * cm3[index++];
  395.             }
  396.             cosLatI *= cosLat1; // Eq. 58
  397.         }

  398.         return m3000;

  399.     }

  400.     /**
  401.      * This method computes the F1 layer critical frequency.
  402.      * <p>
  403.      * This computation performs the algorithm exposed in Annex F
  404.      * of the reference document.
  405.      * </p>
  406.      * @param foE the E layer critical frequency, MHz
  407.      * @return the F1 layer critical frequency, MHz
  408.      */
  409.     private double computefoF1(final double foE) {
  410.         final double temp  = join(1.4 * foE, 0.0, 1000.0, foE - 2.0);
  411.         final double temp2 = join(0.0, temp, 1000.0, foE - temp);
  412.         final double value = join(temp2, 0.85 * temp2, 60.0, 0.85 * foF2 - temp2);
  413.         if (value < 1.0E-6) {
  414.             return 0.0;
  415.         } else {
  416.             return value;
  417.         }
  418.     }

  419.     /**
  420.      * This method allows the computation of the F2, F1 and E layer amplitudes.
  421.      * <p>
  422.      * The resulting element is an array having the following form:
  423.      * <ul>
  424.      * <li>double[0] = A1 → F2 layer amplitude
  425.      * <li>double[1] = A2 → F1 layer amplitude
  426.      * <li>double[2] = A3 → E  layer amplitude
  427.      * </ul>
  428.      * </p>
  429.      * @param nmE E layer maximum density in 10^11 m⁻³
  430.      * @param nmF1 F1 layer maximum density in 10^11 m⁻³
  431.      * @param foF1 F1 layer critical frequency in MHz
  432.      * @return a three components array containing the layer amplitudes
  433.      */
  434.     private double[] computeLayerAmplitudes(final double nmE, final double nmF1, final double foF1) {
  435.         // Initialize array
  436.         final double[] amplitude = new double[3];

  437.         // F2 layer amplitude (Eq. 90)
  438.         final double a1 = 4.0 * nmF2;
  439.         amplitude[0]   = a1;

  440.         // F1 and E layer amplitudes (Eq. 91 to 98)
  441.         if (foF1 < 0.5) {
  442.             amplitude[1] = 0.0;
  443.             amplitude[2] = 4.0 * (nmE - epst(a1, hmF2, b2Bot, hmE));
  444.         } else {
  445.             double a2a = 0.0;
  446.             double a3a = 4.0 * nmE;
  447.             for (int i = 0; i < 5; i++) {
  448.                 a2a = 4.0 * (nmF1 - epst(a1, hmF2, b2Bot, hmF1) - epst(a3a, hmE, beTop, hmF1));
  449.                 a2a = join(a2a, 0.8 * nmF1, 1.0, a2a - 0.8 * nmF1);
  450.                 a3a = 4.0 * (nmE - epst(a2a, hmF1, b1Bot, hmE) - epst(a1, hmF2, b2Bot, hmE));
  451.             }
  452.             amplitude[1] = a2a;
  453.             amplitude[2] = join(a3a, 0.05, 60.0, a3a - 0.005);
  454.         }

  455.         return amplitude;
  456.     }

  457.     /**
  458.      * A clipped exponential function.
  459.      * <p>
  460.      * This function, describe in section F.2.12.2 of the reference document, is
  461.      * recommanded for the computation of exponential values.
  462.      * </p>
  463.      * @param power power for exponential function
  464.      * @return clipped exponential value
  465.      */
  466.     private double clipExp(final double power) {
  467.         if (power > 80.0) {
  468.             return 5.5406E34;
  469.         } else if (power < -80) {
  470.             return 1.8049E-35;
  471.         } else {
  472.             return FastMath.exp(power);
  473.         }
  474.     }

  475.     /**
  476.      * Allows smooth joining of functions f1 and f2
  477.      * (i.e. continuous first derivatives) at origin.
  478.      * <p>
  479.      * This function, describe in section F.2.12.1 of the reference document, is
  480.      * recommended for computational efficiency.
  481.      * </p>
  482.      * @param dF1 first function
  483.      * @param dF2 second function
  484.      * @param dA width of transition region
  485.      * @param dX x value
  486.      * @return the computed value
  487.      */
  488.     double join(final double dF1, final double dF2, final double dA, final double dX) {
  489.         final double ee = clipExp(dA * dX);
  490.         return (dF1 * ee + dF2) / (ee + 1.0);
  491.     }

  492.     /**
  493.      * The Epstein function.
  494.      * <p>
  495.      * This function, describe in section 2.5.1 of the reference document, is used
  496.      * as a basis analytical function in NeQuick for the construction of the ionospheric layers.
  497.      * </p>
  498.      * @param x x parameter
  499.      * @param y y parameter
  500.      * @param z z parameter
  501.      * @param w w parameter
  502.      * @return value of the epstein function
  503.      */
  504.     private double epst(final double x, final double y,
  505.                         final double z, final double w) {
  506.         final double ex  = clipExp((w - y) / z);
  507.         final double opex = 1.0 + ex;
  508.         return x * ex / (opex * opex);

  509.     }

  510. }