FieldNeQuickParameters.java

  1. /* Copyright 2002-2024 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;

  18. import org.hipparchus.Field;
  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.  * This class perfoms the computation of the parameters used by the NeQuick model.
  28.  *
  29.  * @author Bryan Cazabonne
  30.  *
  31.  * @see "European Union (2016). European GNSS (Galileo) Open Service-Ionospheric Correction
  32.  *       Algorithm for Galileo Single Frequency Users. 1.2."
  33.  *
  34.  * @since 10.1
  35.  */
  36. class FieldNeQuickParameters <T extends CalculusFieldElement<T>> {

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

  39.     /** F2 layer maximum density. */
  40.     private final T nmF2;

  41.     /** F2 layer maximum density height [km]. */
  42.     private final T hmF2;

  43.     /** F1 layer maximum density height [km]. */
  44.     private final T hmF1;

  45.     /** E layer maximum density height [km]. */
  46.     private final T hmE;

  47.     /** F2 layer bottom thickness parameter [km]. */
  48.     private final T b2Bot;

  49.     /** F1 layer top thickness parameter [km]. */
  50.     private final T b1Top;

  51.     /** F1 layer bottom thickness parameter [km]. */
  52.     private final T b1Bot;

  53.     /** E layer top thickness parameter [km]. */
  54.     private final T beTop;

  55.     /** E layer bottom thickness parameter [km]. */
  56.     private final T beBot;

  57.     /** topside thickness parameter [km]. */
  58.     private final T h0;

  59.     /** Layer amplitudes. */
  60.     private final T[] amplitudes;

  61.     /**
  62.      * Build a new instance.
  63.      * @param dateTime current date time components
  64.      * @param flattenF2 F2 coefficients used by the F2 layer (flatten array)
  65.      * @param flattenFm3 Fm3 coefficients used by the F2 layer (flatten array)
  66.      * @param latitude latitude of a point along the integration path, in radians
  67.      * @param longitude longitude of a point along the integration path, in radians
  68.      * @param alpha effective ionisation level coefficients
  69.      * @param modipGrip modip grid
  70.      */
  71.     FieldNeQuickParameters(final DateTimeComponents dateTime,
  72.                            final double[] flattenF2, final double[] flattenFm3,
  73.                            final T latitude, final T longitude,
  74.                            final double[] alpha, final double[][] modipGrip) {

  75.         // Zero
  76.         final T zero = latitude.getField().getZero();

  77.         // MODIP in degrees
  78.         final T modip = computeMODIP(latitude, longitude, modipGrip);
  79.         // Effective ionisation level Az
  80.         final T az = computeAz(modip, alpha);
  81.         // Effective sunspot number (Eq. 19)
  82.         final T azr = FastMath.sqrt(az.subtract(63.7).multiply(1123.6).add(167273.0)).subtract(408.99);
  83.         // Date and Time components
  84.         final DateComponents date = dateTime.getDate();
  85.         final TimeComponents time = dateTime.getTime();
  86.         // Hours
  87.         final double hours  = time.getSecondsInUTCDay() / 3600.0;
  88.         // Effective solar zenith angle in radians
  89.         final T xeff = computeEffectiveSolarAngle(date.getMonth(), hours, latitude, longitude);

  90.         // E layer maximum density height in km (Eq. 78)
  91.         this.hmE = zero.newInstance(120.0);
  92.         // E layer critical frequency in MHz
  93.         final T foE = computefoE(date.getMonth(), az, xeff, latitude);
  94.         // E layer maximum density in 10^11 m-3 (Eq. 36)
  95.         final T nmE = foE.multiply(foE).multiply(0.124);

  96.         // Time argument (Eq. 49)
  97.         final double t = FastMath.toRadians(15 * hours) - FastMath.PI;
  98.         // Compute Fourier time series for foF2 and M(3000)F2
  99.         final T[] scT = sinCos(zero.newInstance(t), 6);
  100.         final T[] cf2 = computeCF2(flattenF2, azr, scT);
  101.         final T[] cm3 = computeCm3(flattenFm3, azr, scT);
  102.         // F2 layer critical frequency in MHz
  103.         final T[] scL = sinCos(longitude, 8);
  104.         final T foF2 = computefoF2(modip, cf2, latitude, scL);
  105.         // Maximum Usable Frequency factor
  106.         final T mF2  = computeMF2(modip, cm3, latitude, scL);
  107.         // F2 layer maximum density in 10^11 m-3
  108.         this.nmF2 = foF2.multiply(foF2).multiply(0.124);
  109.         // F2 layer maximum density height in km
  110.         this.hmF2 = computehmF2(foE, foF2, mF2);

  111.         // F1 layer critical frequency in MHz
  112.         final T foF1 = computefoF1(foE, foF2);
  113.         // F1 layer maximum density in 10^11 m-3
  114.         final T nmF1;
  115.         if (foF1.getReal() <= 0.0 && foE.getReal() > 2.0) {
  116.             final T foEpopf = foE.add(0.5);
  117.             nmF1 = foEpopf.multiply(foEpopf).multiply(0.124);
  118.         } else {
  119.             nmF1 = foF1.multiply(foF1).multiply(0.124);
  120.         }
  121.         // F1 layer maximum density height in km
  122.         this.hmF1 = hmF2.add(hmE).multiply(0.5);

  123.         // Thickness parameters (Eq. 85 to 89)
  124.         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);
  125.         this.b2Bot = nmF2.divide(a).multiply(0.385);
  126.         this.b1Top = hmF2.subtract(hmF1).multiply(0.3);
  127.         this.b1Bot = hmF1.subtract(hmE).multiply(0.5);
  128.         this.beTop = FastMath.max(b1Bot, zero.newInstance(7.0));
  129.         this.beBot = zero.newInstance(5.0);

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

  132.         // Topside thickness parameter
  133.         this.h0 = computeH0(date.getMonth(), azr);
  134.     }

  135.     /**
  136.      * Get the F2 layer maximum density.
  137.      * @return nmF2
  138.      */
  139.     public T getNmF2() {
  140.         return nmF2;
  141.     }

  142.     /**
  143.      * Get the F2 layer maximum density height.
  144.      * @return hmF2 in km
  145.      */
  146.     public T getHmF2() {
  147.         return hmF2;
  148.     }

  149.     /**
  150.      * Get the F1 layer maximum density height.
  151.      * @return hmF1 in km
  152.      */
  153.     public T getHmF1() {
  154.         return hmF1;
  155.     }

  156.     /**
  157.      * Get the E layer maximum density height.
  158.      * @return hmE in km
  159.      */
  160.     public T getHmE() {
  161.         return hmE;
  162.     }

  163.     /**
  164.      * Get the F2 layer thickness parameter (bottom).
  165.      * @return B2Bot in km
  166.      */
  167.     public T getB2Bot() {
  168.         return b2Bot;
  169.     }

  170.     /**
  171.      * Get the F1 layer thickness parameter (top).
  172.      * @return B1Top in km
  173.      */
  174.     public T getB1Top() {
  175.         return b1Top;
  176.     }

  177.     /**
  178.      * Get the F1 layer thickness parameter (bottom).
  179.      * @return B1Bot in km
  180.      */
  181.     public T getB1Bot() {
  182.         return b1Bot;
  183.     }

  184.     /**
  185.      * Get the E layer thickness parameter (bottom).
  186.      * @return BeBot in km
  187.      */
  188.     public T getBEBot() {
  189.         return beBot;
  190.     }

  191.     /**
  192.      * Get the E layer thickness parameter (top).
  193.      * @return BeTop in km
  194.      */
  195.     public T getBETop() {
  196.         return beTop;
  197.     }

  198.     /**
  199.      * Get the F2, F1 and E layer amplitudes.
  200.      * <p>
  201.      * The resulting element is an array having the following form:
  202.      * <ul>
  203.      * <li>double[0] = A1 → F2 layer amplitude
  204.      * <li>double[1] = A2 → F1 layer amplitude
  205.      * <li>double[2] = A3 → E  layer amplitude
  206.      * </ul>
  207.      * @return layer amplitudes
  208.      */
  209.     public T[] getLayerAmplitudes() {
  210.         return amplitudes.clone();
  211.     }

  212.     /**
  213.      * Get the topside thickness parameter H0.
  214.      * @return H0 in km
  215.      */
  216.     public T getH0() {
  217.         return h0;
  218.     }

  219.     /**
  220.      * Computes the value of the modified dip latitude (MODIP) for the
  221.      * given latitude and longitude.
  222.      *
  223.      * @param lat receiver latitude, radians
  224.      * @param lon receiver longitude, radians
  225.      * @param stModip modip grid
  226.      * @return the MODIP in degrees
  227.      */
  228.     private T computeMODIP(final T lat, final T lon, final double[][] stModip) {

  229.         // Zero
  230.         final T zero = lat.getField().getZero();

  231.         // For the MODIP computation, the latitude and longitude have to be converted in degrees
  232.         final T latitude  = FastMath.toDegrees(lat);
  233.         final T longitude = FastMath.toDegrees(lon);

  234.         // Extreme cases
  235.         if (latitude.getReal() == 90.0 || latitude.getReal() == -90.0) {
  236.             return latitude;
  237.         }

  238.         // Auxiliary parameter l (Eq. 6 to 8)
  239.         final int lF = (int) ((longitude.getReal() + 180) * 0.1);
  240.         int l = lF - 2;
  241.         if (l < -2) {
  242.             l += 36;
  243.         } else if (l > 33) {
  244.             l -= 36;
  245.         }

  246.         // Auxiliary parameter a (Eq. 9 to 11)
  247.         final T a  = latitude.add(90).multiply(0.2).add(1.0);
  248.         final T aF = FastMath.floor(a);
  249.         // Eq. 10
  250.         final T x = a.subtract(aF);
  251.         // Eq. 11
  252.         final int i = (int) aF.getReal() - 2;

  253.         // zi coefficients (Eq. 12 and 13)
  254.         final T z1 = interpolate(zero.add(stModip[i + 1][l + 2]), zero.add(stModip[i + 2][l + 2]),
  255.                                       zero.add(stModip[i + 3][l + 2]), zero.add(stModip[i + 4][l + 2]), x);
  256.         final T z2 = interpolate(zero.add(stModip[i + 1][l + 3]), zero.add(stModip[i + 2][l + 3]),
  257.                                       zero.add(stModip[i + 3][l + 3]), zero.add(stModip[i + 4][l + 3]), x);
  258.         final T z3 = interpolate(zero.add(stModip[i + 1][l + 4]), zero.add(stModip[i + 2][l + 4]),
  259.                                       zero.add(stModip[i + 3][l + 4]), zero.add(stModip[i + 4][l + 4]), x);
  260.         final T z4 = interpolate(zero.add(stModip[i + 1][l + 5]), zero.add(stModip[i + 2][l + 5]),
  261.                                       zero.add(stModip[i + 3][l + 5]), zero.add(stModip[i + 4][l + 5]), x);

  262.         // Auxiliary parameter b (Eq. 14 and 15)
  263.         final T b  = longitude.add(180).multiply(0.1);
  264.         final T bF = FastMath.floor(b);
  265.         final T y  = b.subtract(bF);

  266.         // MODIP (Ref Eq. 16)
  267.         return interpolate(z1, z2, z3, z4, y);

  268.     }

  269.     /**
  270.      * This method computes the effective ionisation level Az.
  271.      * <p>
  272.      * This parameter is used for the computation of the Total Electron Content (TEC).
  273.      * </p>
  274.      * @param modip modified dip latitude (MODIP) in degrees
  275.      * @param alpha effective ionisation level coefficients
  276.      * @return the ionisation level Az
  277.      */
  278.     private T computeAz(final T modip, final double[] alpha) {
  279.         // Field
  280.         final Field<T> field = modip.getField();
  281.         // Zero
  282.         final T zero = field.getZero();
  283.         // Particular condition (Eq. 17)
  284.         if (alpha[0] == 0.0 && alpha[1] == 0.0 && alpha[2] == 0.0) {
  285.             return zero.newInstance(63.7);
  286.         }
  287.         // Az = a0 + modip * a1 + modip^2 * a2 (Eq. 18)
  288.         T az = modip.multiply(alpha[2]).add(alpha[1]).multiply(modip).add(alpha[0]);
  289.         // If Az < 0 -> Az = 0
  290.         az = FastMath.max(zero, az);
  291.         // If Az > 400 -> Az = 400
  292.         az = FastMath.min(zero.newInstance(400.0), az);
  293.         return az;
  294.     }

  295.     /**
  296.      * This method computes the effective solar zenith angle.
  297.      * <p>
  298.      * The effective solar zenith angle is compute as a function of the
  299.      * solar zenith angle and the solar zenith angle at day night transition.
  300.      * </p>
  301.      * @param month current month of the year
  302.      * @param hours universal time (hours)
  303.      * @param latitude in radians
  304.      * @param longitude in radians
  305.      * @return the effective solar zenith angle, radians
  306.      */
  307.     private T computeEffectiveSolarAngle(final int month,
  308.                                          final double hours,
  309.                                          final T latitude,
  310.                                          final T longitude) {
  311.         // Zero
  312.         final T zero = latitude.getField().getZero();
  313.         // Local time (Eq.4)
  314.         final T lt = longitude.divide(FastMath.toRadians(15.0)).add(hours);
  315.         // Day of year at the middle of the month (Eq. 20)
  316.         final double dy = 30.5 * month - 15.0;
  317.         // Time (Eq. 21)
  318.         final double t = dy + (18 - hours) / 24;
  319.         // Arguments am and al (Eq. 22 and 23)
  320.         final double am = FastMath.toRadians(0.9856 * t - 3.289);
  321.         final double al = am + FastMath.toRadians(1.916 * FastMath.sin(am) + 0.020 * FastMath.sin(2.0 * am) + 282.634);
  322.         // Sine and cosine of solar declination (Eq. 24 and 25)
  323.         final double sDec = 0.39782 * FastMath.sin(al);
  324.         final double cDec = FastMath.sqrt(1. - sDec * sDec);
  325.         // Solar zenith angle, deg (Eq. 26 and 27)
  326.         final FieldSinCos<T> scLat   = FastMath.sinCos(latitude);
  327.         final T coef    = lt.negate().add(12.0).multiply(FastMath.PI / 12);
  328.         final T cZenith = scLat.sin().multiply(sDec).add(scLat.cos().multiply(cDec).multiply(FastMath.cos(coef)));
  329.         final T angle   = FastMath.atan2(FastMath.sqrt(cZenith.multiply(cZenith).negate().add(1.0)), cZenith);
  330.         final T x       = FastMath.toDegrees(angle);
  331.         // Effective solar zenith angle (Eq. 28)
  332.         final T xeff = join(clipExp(x.multiply(0.2).negate().add(20.0)).multiply(0.24).negate().add(90.0), x,
  333.                 zero.newInstance(12.0), x.subtract(X0));
  334.         return FastMath.toRadians(xeff);
  335.     }

  336.     /**
  337.      * This method computes the E layer critical frequency at a given location.
  338.      * @param month current month
  339.      * @param az ffective ionisation level
  340.      * @param xeff effective solar zenith angle in radians
  341.      * @param latitude latitude in radians
  342.      * @return the E layer critical frequency at a given location in MHz
  343.      */
  344.     private T computefoE(final int month, final T az,
  345.                          final T xeff, final T latitude) {
  346.         // The latitude has to be converted in degrees
  347.         final T lat = FastMath.toDegrees(latitude);
  348.         // Square root of the effective ionisation level
  349.         final T sqAz = FastMath.sqrt(az);
  350.         // seas parameter (Eq. 30 to 32)
  351.         final int seas;
  352.         if (month == 1 || month == 2 || month == 11 || month == 12) {
  353.             seas = -1;
  354.         } else if (month == 3 || month == 4 || month == 9 || month == 10) {
  355.             seas = 0;
  356.         } else {
  357.             seas = 1;
  358.         }
  359.         // Latitudinal dependence (Eq. 33 and 34)
  360.         final T ee = clipExp(lat.multiply(0.3));
  361.         final T seasp = ee.subtract(1.0).divide(ee.add(1.0)).multiply(seas);
  362.         // Critical frequency (Eq. 35)
  363.         final T coef = seasp.multiply(0.019).negate().add(1.112);
  364.         return FastMath.sqrt(coef .multiply(coef).multiply(sqAz).multiply(FastMath.cos(xeff).pow(0.6)).add(0.49));

  365.     }

  366.     /**
  367.      * Computes the F2 layer height of maximum electron density.
  368.      * @param foE E layer layer critical frequency in MHz
  369.      * @param foF2 F2 layer layer critical frequency in MHz
  370.      * @param mF2 maximum usable frequency factor
  371.      * @return hmF2 in km
  372.      */
  373.     private T computehmF2(final T foE, final T foF2, final T mF2) {
  374.         // Zero
  375.         final T zero = foE.getField().getZero();
  376.         // Ratio
  377.         final T fo = foF2.divide(foE);
  378.         final T ratio = join(fo, zero.newInstance(1.75), zero.newInstance(20.0), fo.subtract(1.75));

  379.         // deltaM parameter
  380.         T deltaM = zero.subtract(0.012);
  381.         if (foE.getReal() >= 1e-30) {
  382.             deltaM = deltaM.add(ratio.subtract(1.215).divide(0.253).reciprocal());
  383.         }

  384.         // hmF2 Eq. 80
  385.         final T mF2Sq = mF2.square();
  386.         final T temp  = FastMath.sqrt(mF2Sq.multiply(0.0196).add(1.0).divide(mF2Sq.multiply(1.2967).subtract(1.0)));
  387.         return mF2.multiply(1490.0).multiply(temp).divide(mF2.add(deltaM)).subtract(176.0);

  388.     }

  389.     /** Compute sines and cosines.
  390.      * @param a argument
  391.      * @param n number of terms
  392.      * @return sin(a), cos(a), sin(2a), cos(2a) … sin(n a), cos(n a) array
  393.      * @since 12.1.3
  394.      */
  395.     private T[] sinCos(final T a, final int n) {

  396.         final FieldSinCos<T> sc0 = FastMath.sinCos(a);
  397.         FieldSinCos<T> sci = sc0;
  398.         final T[] sc = MathArrays.buildArray(a.getField(), 2 * n);
  399.         int isc = 0;
  400.         sc[isc++] = sci.sin();
  401.         sc[isc++] = sci.cos();
  402.         for (int i = 1; i < n; i++) {
  403.             sci = FieldSinCos.sum(sc0, sci);
  404.             sc[isc++] = sci.sin();
  405.             sc[isc++] = sci.cos();
  406.         }

  407.         return sc;

  408.     }

  409.     /**
  410.      * Computes cf2 coefficients.
  411.      * @param flattenF2 F2 coefficients used by the F2 layer (flatten array)
  412.      * @param azr effective sunspot number (Eq. 19)
  413.      * @param scT sines/cosines array of time argument
  414.      * @return the cf2 coefficients array
  415.      */
  416.     private T[] computeCF2(final double[] flattenF2, final T azr, final T[] scT) {

  417.         // interpolation coefficients for effective spot number
  418.         final T azr01 = azr.multiply(0.01);
  419.         final T omazr01 = azr01.negate().add(1);

  420.         // Eq. 44 and Eq. 50 merged into one loop
  421.         final T[] cf2 = MathArrays.buildArray(azr.getField(), 76);
  422.         int index = 0;
  423.         for (int i = 0; i < cf2.length; i++) {
  424.             // CHECKSTYLE: stop Indentation check
  425.             cf2[i] = omazr01.multiply(flattenF2[index     ]).add(azr01.multiply(flattenF2[index +  1])).
  426.                  add(omazr01.multiply(flattenF2[index +  2]).add(azr01.multiply(flattenF2[index +  3])).multiply(scT[ 0])).
  427.                  add(omazr01.multiply(flattenF2[index +  4]).add(azr01.multiply(flattenF2[index +  5])).multiply(scT[ 1])).
  428.                  add(omazr01.multiply(flattenF2[index +  6]).add(azr01.multiply(flattenF2[index +  7])).multiply(scT[ 2])).
  429.                  add(omazr01.multiply(flattenF2[index +  8]).add(azr01.multiply(flattenF2[index +  9])).multiply(scT[ 3])).
  430.                  add(omazr01.multiply(flattenF2[index + 10]).add(azr01.multiply(flattenF2[index + 11])).multiply(scT[ 4])).
  431.                  add(omazr01.multiply(flattenF2[index + 12]).add(azr01.multiply(flattenF2[index + 13])).multiply(scT[ 5])).
  432.                  add(omazr01.multiply(flattenF2[index + 14]).add(azr01.multiply(flattenF2[index + 15])).multiply(scT[ 6])).
  433.                  add(omazr01.multiply(flattenF2[index + 16]).add(azr01.multiply(flattenF2[index + 17])).multiply(scT[ 7])).
  434.                  add(omazr01.multiply(flattenF2[index + 18]).add(azr01.multiply(flattenF2[index + 19])).multiply(scT[ 8])).
  435.                  add(omazr01.multiply(flattenF2[index + 20]).add(azr01.multiply(flattenF2[index + 21])).multiply(scT[ 9])).
  436.                  add(omazr01.multiply(flattenF2[index + 22]).add(azr01.multiply(flattenF2[index + 23])).multiply(scT[10])).
  437.                  add(omazr01.multiply(flattenF2[index + 24]).add(azr01.multiply(flattenF2[index + 25])).multiply(scT[11]));
  438.             index += 26;
  439.             // CHECKSTYLE: resume Indentation check
  440.         }
  441.         return cf2;
  442.     }

  443.     /**
  444.      * Computes Cm3 coefficients.
  445.      * @param flattenFm3 Fm3 coefficients used by the F2 layer (flatten array)
  446.      * @param azr effective sunspot number (Eq. 19)
  447.      * @param scT sines/cosines array of time argument
  448.      * @return the Cm3 coefficients array
  449.      */
  450.     private T[] computeCm3(final double[] flattenFm3, final T azr, final T[] scT) {

  451.         // interpolation coefficients for effective spot number
  452.         final T azr01 = azr.multiply(0.01);
  453.         final T omazr01 = azr01.negate().add(1);

  454.         // Eq. 44 and Eq. 51 merged into one loop
  455.         final T[] cm3 = MathArrays.buildArray(azr.getField(), 49);
  456.         int index = 0;
  457.         for (int i = 0; i < cm3.length; i++) {
  458.             cm3[i] = omazr01.multiply(flattenFm3[index     ]).add(azr01.multiply(flattenFm3[index +  1])).
  459.                  add(omazr01.multiply(flattenFm3[index +  2]).add(azr01.multiply(flattenFm3[index +  3])).multiply(scT[ 0])).
  460.                  add(omazr01.multiply(flattenFm3[index +  4]).add(azr01.multiply(flattenFm3[index +  5])).multiply(scT[ 1])).
  461.                  add(omazr01.multiply(flattenFm3[index +  6]).add(azr01.multiply(flattenFm3[index +  7])).multiply(scT[ 2])).
  462.                  add(omazr01.multiply(flattenFm3[index +  8]).add(azr01.multiply(flattenFm3[index +  9])).multiply(scT[ 3])).
  463.                  add(omazr01.multiply(flattenFm3[index + 10]).add(azr01.multiply(flattenFm3[index + 11])).multiply(scT[ 4])).
  464.                  add(omazr01.multiply(flattenFm3[index + 12]).add(azr01.multiply(flattenFm3[index + 13])).multiply(scT[ 5])).
  465.                  add(omazr01.multiply(flattenFm3[index + 14]).add(azr01.multiply(flattenFm3[index + 15])).multiply(scT[ 6])).
  466.                  add(omazr01.multiply(flattenFm3[index + 16]).add(azr01.multiply(flattenFm3[index + 17])).multiply(scT[ 7]));
  467.             index += 18;
  468.         }
  469.         return cm3;
  470.     }

  471.     /**
  472.      * This method computes the F2 layer critical frequency.
  473.      * @param modip modified DIP latitude, in degrees
  474.      * @param cf2 Fourier time series for foF2
  475.      * @param latitude latitude in radians
  476.      * @param scL sines/cosines array of longitude argument
  477.      * @return the F2 layer critical frequency, MHz
  478.      */
  479.     private T computefoF2(final T modip, final T[] cf2,
  480.                           final T latitude, final T[] scL) {

  481.         // Legendre grades (Eq. 63)
  482.         final int[] q = new int[] {
  483.             12, 12, 9, 5, 2, 1, 1, 1, 1
  484.         };

  485.         T frequency = cf2[0];

  486.         // MODIP coefficients Eq. 57
  487.         final T sinMODIP = FastMath.sin(FastMath.toRadians(modip));
  488.         final T[] m = MathArrays.buildArray(latitude.getField(), 12);
  489.         m[0] = latitude.getField().getOne();
  490.         for (int i = 1; i < q[0]; i++) {
  491.             m[i] = sinMODIP.multiply(m[i - 1]);
  492.             frequency = frequency.add(m[i].multiply(cf2[i]));
  493.         }

  494.         // latitude and longitude terms
  495.         int index = 12;
  496.         final T cosLat1 = FastMath.cos(latitude);
  497.         T cosLatI = cosLat1;
  498.         for (int i = 1; i < q.length; i++) {
  499.             final T c = cosLatI.multiply(scL[2 * i - 1]);
  500.             final T s = cosLatI.multiply(scL[2 * i - 2]);
  501.             for (int j = 0; j < q[i]; j++) {
  502.                 frequency = frequency.add(m[j].multiply(c).multiply(cf2[index++]));
  503.                 frequency = frequency.add(m[j].multiply(s).multiply(cf2[index++]));
  504.             }
  505.             cosLatI = cosLatI.multiply(cosLat1);
  506.         }

  507.         return frequency;

  508.     }

  509.     /**
  510.      * This method computes the Maximum Usable Frequency factor.
  511.      * @param modip modified DIP latitude, in degrees
  512.      * @param cm3 Fourier time series for M(3000)F2
  513.      * @param latitude latitude in radians
  514.      * @param scL sines/cosines array of longitude argument
  515.      * @return the Maximum Usable Frequency factor
  516.      */
  517.     private T computeMF2(final T modip, final T[] cm3,
  518.                          final T latitude, final T[] scL) {

  519.         // Legendre grades (Eq. 71)
  520.         final int[] r = new int[] {
  521.             7, 8, 6, 3, 2, 1, 1
  522.         };

  523.         T m3000 = cm3[0];

  524.         // MODIP coefficients Eq. 57
  525.         final T sinMODIP = FastMath.sin(FastMath.toRadians(modip));
  526.         final T[] m = MathArrays.buildArray(latitude.getField(), 12);
  527.         m[0] = latitude.getField().getOne();
  528.         for (int i = 1; i < 12; i++) {
  529.             m[i] = sinMODIP.multiply(m[i - 1]);
  530.             if (i < 7) {
  531.                 m3000 = m3000.add(m[i].multiply(cm3[i]));
  532.             }
  533.         }

  534.         // latitude and longitude terms
  535.         int index = 7;
  536.         final T cosLat1 = FastMath.cos(latitude);
  537.         T cosLatI = cosLat1;
  538.         for (int i = 1; i < r.length; i++) {
  539.             final T c = cosLatI.multiply(scL[2 * i - 1]);
  540.             final T s = cosLatI.multiply(scL[2 * i - 2]);
  541.             for (int j = 0; j < r[i]; j++) {
  542.                 m3000 = m3000.add(m[j].multiply(c).multiply(cm3[index++]));
  543.                 m3000 = m3000.add(m[j].multiply(s).multiply(cm3[index++]));
  544.             }
  545.             cosLatI = cosLatI.multiply(cosLat1); // Eq. 58
  546.         }

  547.         return m3000;

  548.     }

  549.     /**
  550.      * This method computes the F1 layer critical frequency.
  551.      * <p>
  552.      * This computation performs the algorithm exposed in Annex F
  553.      * of the reference document.
  554.      * </p>
  555.      * @param foE the E layer critical frequency, MHz
  556.      * @return the F1 layer critical frequency, MHz
  557.      * @param foF2 the F2 layer critical frequency, MHz
  558.      */
  559.     private T computefoF1(final T foE, final T foF2) {
  560.         final T zero = foE.getField().getZero();
  561.         final T temp  = join(foE.multiply(1.4), zero, zero.newInstance(1000.0), foE.subtract(2.0));
  562.         final T temp2 = join(zero, temp, zero.newInstance(1000.0), foE.subtract(temp));
  563.         final T value = join(temp2, temp2.multiply(0.85), zero.newInstance(60.0), foF2.multiply(0.85).subtract(temp2));
  564.         if (value.getReal() < 1.0E-6) {
  565.             return zero;
  566.         } else {
  567.             return value;
  568.         }
  569.     }

  570.     /**
  571.      * This method allows the computation of the F2, F1 and E layer amplitudes.
  572.      * <p>
  573.      * The resulting element is an array having the following form:
  574.      * <ul>
  575.      * <li>double[0] = A1 → F2 layer amplitude
  576.      * <li>double[1] = A2 → F1 layer amplitude
  577.      * <li>double[2] = A3 → E  layer amplitude
  578.      * </ul>
  579.      * </p>
  580.      * @param nmE E layer maximum density in 10^11 m-3
  581.      * @param nmF1 F1 layer maximum density in 10^11 m-3
  582.      * @param foF1 F1 layer critical frequency in MHz
  583.      * @return a three components array containing the layer amplitudes
  584.      */
  585.     private T[] computeLayerAmplitudes(final T nmE, final T nmF1, final T foF1) {
  586.         // Zero
  587.         final T zero = nmE.getField().getZero();

  588.         // Initialize array
  589.         final T[] amplitude = MathArrays.buildArray(nmE.getField(), 3);

  590.         // F2 layer amplitude (Eq. 90)
  591.         final T a1 = nmF2.multiply(4.0);
  592.         amplitude[0] = a1;

  593.         // F1 and E layer amplitudes (Eq. 91 to 98)
  594.         if (foF1.getReal() < 0.5) {
  595.             amplitude[1] = zero;
  596.             amplitude[2] = nmE.subtract(epst(a1, hmF2, b2Bot, hmE)).multiply(4.0);
  597.         } else {
  598.             T a2a = zero;
  599.             T a3a = nmE.multiply(4.0);
  600.             for (int i = 0; i < 5; i++) {
  601.                 a2a = nmF1.subtract(epst(a1, hmF2, b2Bot, hmF1)).subtract(epst(a3a, hmE, beTop, hmF1)).multiply(4.0);
  602.                 a2a = join(a2a, nmF1.multiply(0.8), nmE.getField().getOne(), a2a.subtract(nmF1.multiply(0.8)));
  603.                 a3a = nmE.subtract(epst(a2a, hmF1, b1Bot, hmE)).subtract(epst(a1, hmF2, b2Bot, hmE)).multiply(4.0);
  604.             }
  605.             amplitude[1] = a2a;
  606.             amplitude[2] = join(a3a, zero.newInstance(0.05), zero.newInstance(60.0), a3a.subtract(0.005));
  607.         }

  608.         return amplitude;
  609.     }

  610.     /**
  611.      * This method computes the topside thickness parameter H0.
  612.      *
  613.      * @param month current month
  614.      * @param azr effective sunspot number
  615.      * @return H0 in km
  616.      */
  617.     private T computeH0(final int month, final T azr) {

  618.         // One
  619.         final T one = azr.getField().getOne();

  620.         // Auxiliary parameter ka (Eq. 99 and 100)
  621.         final T ka;
  622.         if (month > 3 && month < 10) {
  623.             // month = 4,5,6,7,8,9
  624.             ka = azr.multiply(0.014).add(hmF2.multiply(0.008)).negate().add(6.705);
  625.         } else {
  626.             // month = 1,2,3,10,11,12
  627.             final T ratio = hmF2.divide(b2Bot);
  628.             ka = ratio.multiply(ratio).multiply(0.097).add(nmF2.multiply(0.153)).add(-7.77);
  629.         }

  630.         // Auxiliary parameter kb (Eq. 101 and 102)
  631.         T kb = join(ka, one.newInstance(2.0), one, ka.subtract(2.0));
  632.         kb = join(one.newInstance(8.0), kb, one, kb.subtract(8.0));

  633.         // Auxiliary parameter Ha (Eq. 103)
  634.         final T hA = kb.multiply(b2Bot);

  635.         // Auxiliary parameters x and v (Eq. 104 and 105)
  636.         final T x = hA.subtract(150.0).multiply(0.01);
  637.         final T v = x.multiply(0.041163).subtract(0.183981).multiply(x).add(1.424472);

  638.         // Topside thickness parameter (Eq. 106)
  639.         return hA.divide(v);

  640.     }

  641.     /**
  642.      * A clipped exponential function.
  643.      * <p>
  644.      * This function, describe in section F.2.12.2 of the reference document, is
  645.      * recommanded for the computation of exponential values.
  646.      * </p>
  647.      * @param power power for exponential function
  648.      * @return clipped exponential value
  649.      */
  650.     private T clipExp(final T power) {
  651.         final T zero = power.getField().getZero();
  652.         if (power.getReal() > 80.0) {
  653.             return zero.newInstance(5.5406E34);
  654.         } else if (power.getReal() < -80) {
  655.             return zero.newInstance(1.8049E-35);
  656.         } else {
  657.             return FastMath.exp(power);
  658.         }
  659.     }

  660.     /**
  661.      * This method provides a third order interpolation function
  662.      * as recommended in the reference document (Ref Eq. 128 to Eq. 138)
  663.      *
  664.      * @param z1 z1 coefficient
  665.      * @param z2 z2 coefficient
  666.      * @param z3 z3 coefficient
  667.      * @param z4 z4 coefficient
  668.      * @param x position
  669.      * @return a third order interpolation
  670.      */
  671.     private T interpolate(final T z1, final T z2,
  672.                           final T z3, final T z4,
  673.                           final T x) {

  674.         if (FastMath.abs(2.0 * x.getReal()) < 1e-10) {
  675.             return z2;
  676.         }

  677.         final T delta = x.multiply(2.0).subtract(1.0);
  678.         final T g1 = z3.add(z2);
  679.         final T g2 = z3.subtract(z2);
  680.         final T g3 = z4.add(z1);
  681.         final T g4 = z4.subtract(z1).divide(3.0);
  682.         final T a0 = g1.multiply(9.0).subtract(g3);
  683.         final T a1 = g2.multiply(9.0).subtract(g4);
  684.         final T a2 = g3.subtract(g1);
  685.         final T a3 = g4.subtract(g2);
  686.         return delta.multiply(a3).add(a2).multiply(delta).add(a1).multiply(delta).add(a0).multiply(0.0625);

  687.     }

  688.     /**
  689.      * Allows smooth joining of functions f1 and f2
  690.      * (i.e. continuous first derivatives) at origin.
  691.      * <p>
  692.      * This function, describe in section F.2.12.1 of the reference document, is
  693.      * recommanded for computational efficiency.
  694.      * </p>
  695.      * @param dF1 first function
  696.      * @param dF2 second function
  697.      * @param dA width of transition region
  698.      * @param dX x value
  699.      * @return the computed value
  700.      */
  701.     private T join(final T dF1, final T dF2,
  702.                    final T dA, final T dX) {
  703.         final T ee = clipExp(dA.multiply(dX));
  704.         return dF1.multiply(ee).add(dF2).divide(ee.add(1.0));
  705.     }

  706.     /**
  707.      * The Epstein function.
  708.      * <p>
  709.      * This function, describe in section 2.5.1 of the reference document, is used
  710.      * as a basis analytical function in NeQuick for the construction of the ionospheric layers.
  711.      * </p>
  712.      * @param x x parameter
  713.      * @param y y parameter
  714.      * @param z z parameter
  715.      * @param w w parameter
  716.      * @return value of the epstein function
  717.      */
  718.     private T epst(final T x, final T y,
  719.                    final T z, final T w) {
  720.         final T ex  = clipExp(w.subtract(y).divide(z));
  721.         final T opex = ex.add(1.0);
  722.         return x.multiply(ex).divide(opex.multiply(opex));

  723.     }

  724. }