NeQuickModel.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 java.util.Collections;
  19. import java.util.List;

  20. import org.hipparchus.CalculusFieldElement;
  21. import org.hipparchus.Field;
  22. import org.hipparchus.util.FastMath;
  23. import org.hipparchus.util.MathArrays;
  24. import org.orekit.bodies.BodyShape;
  25. import org.orekit.bodies.FieldGeodeticPoint;
  26. import org.orekit.bodies.GeodeticPoint;
  27. import org.orekit.frames.TopocentricFrame;
  28. import org.orekit.models.earth.ionosphere.IonosphericModel;
  29. import org.orekit.propagation.FieldSpacecraftState;
  30. import org.orekit.propagation.SpacecraftState;
  31. import org.orekit.time.AbsoluteDate;
  32. import org.orekit.time.DateComponents;
  33. import org.orekit.time.DateTimeComponents;
  34. import org.orekit.time.FieldAbsoluteDate;
  35. import org.orekit.time.TimeScale;
  36. import org.orekit.utils.ParameterDriver;
  37. import org.orekit.utils.units.Unit;

  38. /**
  39.  * NeQuick ionospheric delay model.
  40.  *
  41.  * @author Bryan Cazabonne
  42.  *
  43.  * @see "European Union (2016). European GNSS (Galileo) Open Service-Ionospheric Correction
  44.  *       Algorithm for Galileo Single Frequency Users. 1.2."
  45.  * @see <a href="https://www.itu.int/rec/R-REC-P.531/en">ITU-R P.531</a>
  46.  *
  47.  * @since 10.1
  48.  */
  49. public abstract class NeQuickModel implements IonosphericModel {

  50.     /** Mean Earth radius in m (Ref Table 2.5.2). */
  51.     public static final double RE = 6371200.0;

  52.     /** Factor for the electron density computation. */
  53.     private static final double DENSITY_FACTOR = 1.0e11;

  54.     /** Factor for the path delay computation. */
  55.     private static final double DELAY_FACTOR = 40.3e16;

  56.     /** F2 coefficients used by the F2 layer (flatten array for cache efficiency). */
  57.     private final double[][] flattenF2;

  58.     /** Fm3 coefficients used by the M(3000)F2 layer(flatten array for cache efficiency). */
  59.     private final double[][] flattenFm3;

  60.     /** UTC time scale. */
  61.     private final TimeScale utc;

  62.     /** Simple constructor.
  63.      * @param utc UTC time scale
  64.      * @since 13.0
  65.      */
  66.     protected NeQuickModel(final TimeScale utc) {

  67.         this.utc = utc;

  68.         // F2 layer values
  69.         this.flattenF2  = new double[12][];
  70.         this.flattenFm3 = new double[12][];

  71.     }

  72.     /** Get UTC time scale.
  73.      * @return UTC time scale
  74.      * @since 13.0
  75.      */
  76.     public TimeScale getUtc() {
  77.         return utc;
  78.     }

  79.     /** {@inheritDoc} */
  80.     @Override
  81.     public double pathDelay(final SpacecraftState state, final TopocentricFrame baseFrame,
  82.                             final double frequency, final double[] parameters) {
  83.         // Point
  84.         final GeodeticPoint recPoint = baseFrame.getPoint();
  85.         // Date
  86.         final AbsoluteDate date = state.getDate();

  87.         // Reference body shape
  88.         final BodyShape ellipsoid = baseFrame.getParentShape();
  89.         // Satellite geodetic coordinates
  90.         final GeodeticPoint satPoint = ellipsoid.transform(state.getPosition(ellipsoid.getBodyFrame()),
  91.                                                            ellipsoid.getBodyFrame(), state.getDate());

  92.         // Total Electron Content
  93.         final double tec = stec(date, recPoint, satPoint);

  94.         // Ionospheric delay
  95.         final double factor = DELAY_FACTOR / (frequency * frequency);
  96.         return factor * tec;
  97.     }

  98.     /** {@inheritDoc} */
  99.     @Override
  100.     public <T extends CalculusFieldElement<T>> T pathDelay(final FieldSpacecraftState<T> state,
  101.                                                            final TopocentricFrame baseFrame,
  102.                                                            final double frequency,
  103.                                                            final T[] parameters) {
  104.         // Date
  105.         final FieldAbsoluteDate<T> date = state.getDate();
  106.         // Point
  107.         final FieldGeodeticPoint<T> recPoint = baseFrame.getPoint(date.getField());


  108.         // Reference body shape
  109.         final BodyShape ellipsoid = baseFrame.getParentShape();
  110.         // Satellite geodetic coordinates
  111.         final FieldGeodeticPoint<T> satPoint = ellipsoid.transform(state.getPosition(ellipsoid.getBodyFrame()), ellipsoid.getBodyFrame(), state.getDate());

  112.         // Total Electron Content
  113.         final T tec = stec(date, recPoint, satPoint);

  114.         // Ionospheric delay
  115.         final double factor = DELAY_FACTOR / (frequency * frequency);
  116.         return tec.multiply(factor);
  117.     }

  118.     /** {@inheritDoc} */
  119.     @Override
  120.     public List<ParameterDriver> getParametersDrivers() {
  121.         return Collections.emptyList();
  122.     }

  123.     /**
  124.      * This method allows the computation of the Slant Total Electron Content (STEC).
  125.      * @param date current date
  126.      * @param recP receiver position
  127.      * @param satP satellite position
  128.      * @return the STEC in TECUnits
  129.      */
  130.     public double stec(final AbsoluteDate date, final GeodeticPoint recP, final GeodeticPoint satP) {
  131.         return stec(date.getComponents(utc), new Ray(recP, satP));
  132.     }

  133.     /**
  134.      * This method allows the computation of the Slant Total Electron Content (STEC).
  135.      * @param <T> type of the elements
  136.      * @param date current date
  137.      * @param recP receiver position
  138.      * @param satP satellite position
  139.      * @return the STEC in TECUnits
  140.      */
  141.     public <T extends CalculusFieldElement<T>> T stec(final FieldAbsoluteDate<T> date,
  142.                                                       final FieldGeodeticPoint<T> recP,
  143.                                                       final FieldGeodeticPoint<T> satP) {
  144.         return stec(date.getComponents(utc), new FieldRay<>(recP, satP));
  145.     }

  146.     /** Compute modip for a location.
  147.      * @param latitude latitude
  148.      * @param longitude longitude
  149.      * @return modip at specified location
  150.      * @since 13.0
  151.      */
  152.     protected abstract double computeMODIP(double latitude, double longitude);

  153.     /** Compute modip for a location.
  154.      * @param <T> type of the field elements
  155.      * @param latitude latitude
  156.      * @param longitude longitude
  157.      * @return modip at specified location
  158.      * @since 13.0
  159.      */
  160.     protected abstract <T extends CalculusFieldElement<T>> T computeMODIP(T latitude, T longitude);

  161.     /**
  162.      * Computes the electron density at a given height.
  163.      * @param dateTime date
  164.      * @param az effective ionization level
  165.      * @param latitude latitude along the integration path
  166.      * @param longitude longitude along the integration path
  167.      * @param h height along the integration path in m
  168.      * @return electron density [m⁻³]
  169.      * @since 13.0
  170.      */
  171.     public double electronDensity(final DateTimeComponents dateTime, final double az,
  172.                                   final double latitude, final double longitude, final double h) {

  173.         // Load the correct CCIR file
  174.         loadsIfNeeded(dateTime.getDate());

  175.         final double modip = computeMODIP(latitude, longitude);
  176.         final NeQuickParameters parameters = new NeQuickParameters(dateTime,
  177.                                                                    flattenF2[dateTime.getDate().getMonth() - 1],
  178.                                                                    flattenFm3[dateTime.getDate().getMonth() - 1],
  179.                                                                    latitude, longitude, az, modip);
  180.         // Convert height in kilometers
  181.         final double hInKm = Unit.KILOMETRE.fromSI(h);
  182.         // Electron density
  183.         final double n;
  184.         if (hInKm <= parameters.getHmF2()) {
  185.             n = bottomElectronDensity(hInKm, parameters);
  186.         } else {
  187.             n = topElectronDensity(hInKm, parameters);
  188.         }
  189.         return n;
  190.     }

  191.     /**
  192.      * Computes the electron density at a given height.
  193.      * @param <T> type of the elements
  194.      * @param dateTime date
  195.      * @param az effective ionization level
  196.      * @param latitude latitude along the integration path
  197.      * @param longitude longitude along the integration path
  198.      * @param h height along the integration path in m
  199.      * @return electron density [m⁻³]
  200.      * @since 13.0
  201.      */
  202.     public <T extends CalculusFieldElement<T>> T electronDensity(final DateTimeComponents dateTime, final T az,
  203.                                                                  final T latitude, final T longitude, final T h) {


  204.         // Load the correct CCIR file
  205.         loadsIfNeeded(dateTime.getDate());

  206.         final T modip = computeMODIP(latitude, longitude);
  207.         final FieldNeQuickParameters<T> parameters =
  208.             new FieldNeQuickParameters<>(dateTime,
  209.                                          flattenF2[dateTime.getDate().getMonth() - 1],
  210.                                          flattenFm3[dateTime.getDate().getMonth() - 1],
  211.                                          latitude, longitude, az, modip);

  212.         // Convert height in kilometers
  213.         final T hInKm = Unit.KILOMETRE.fromSI(h);
  214.         // Electron density
  215.         final T n;
  216.         if (hInKm.getReal() <= parameters.getHmF2().getReal()) {
  217.             n = bottomElectronDensity(hInKm, parameters);
  218.         } else {
  219.             n = topElectronDensity(hInKm, parameters);
  220.         }
  221.         return n;
  222.     }

  223.     /**
  224.      * Computes the electron density of the bottomside.
  225.      * @param h height in km
  226.      * @param parameters NeQuick model parameters
  227.      * @return the electron density N in m⁻³
  228.      */
  229.     private double bottomElectronDensity(final double h, final NeQuickParameters parameters) {

  230.         // Select the relevant B parameter for the current height (Eq. 109 and 110)
  231.         final double be  = parameters.getBE(h);
  232.         final double bf1 = parameters.getBF1(h);
  233.         final double bf2 = parameters.getB2Bot();

  234.         // Useful array of constants
  235.         final double[] ct = new double[] {
  236.             1.0 / bf2, 1.0 / bf1, 1.0 / be
  237.         };

  238.         // Compute the exponential argument for each layer (Eq. 111 to 113)
  239.         final double[] arguments = computeExponentialArguments(h, parameters);

  240.         // S coefficients
  241.         final double[] s = new double[3];
  242.         // Array of corrective terms
  243.         final double[] ds = new double[3];

  244.         // Layer amplitudes
  245.         final double[] amplitudes = parameters.getLayerAmplitudes();

  246.         // Fill arrays (Eq. 114 to 118)
  247.         for (int i = 0; i < 3; i++) {
  248.             if (FastMath.abs(arguments[i]) > 25.0) {
  249.                 s[i]  = 0.0;
  250.                 ds[i] = 0.0;
  251.             } else {
  252.                 final double expA   = clipExp(arguments[i]);
  253.                 final double opExpA = 1.0 + expA;
  254.                 s[i]  = amplitudes[i] * (expA / (opExpA * opExpA));
  255.                 ds[i] = ct[i] * ((1.0 - expA) / (1.0 + expA));
  256.             }
  257.         }

  258.         // Electron density
  259.         final double aNo = s[0] + s[1] + s[2];
  260.         if (applyChapmanParameters(h)) {
  261.             // Chapman parameters (Eq. 119 and 120)
  262.             final double bc = 1.0 - 10.0 * (MathArrays.linearCombination(s[0], ds[0], s[1], ds[1], s[2], ds[2]) / aNo);
  263.             final double z  = 0.1 * (h - 100.0);
  264.             // Electron density (Eq. 121)
  265.             return aNo * clipExp(1.0 - bc * z - clipExp(-z)) * DENSITY_FACTOR;
  266.         } else {
  267.             return aNo * DENSITY_FACTOR;
  268.         }

  269.     }

  270.     /**
  271.      * Computes the electron density of the bottomside.
  272.      * @param <T> type of the elements
  273.      * @param h height in km
  274.      * @param parameters NeQuick model parameters
  275.      * @return the electron density N in m⁻³
  276.      */
  277.     private <T extends CalculusFieldElement<T>> T bottomElectronDensity(final T h,
  278.                                                                         final FieldNeQuickParameters<T> parameters) {

  279.         // Zero and One
  280.         final Field<T> field = h.getField();
  281.         final T zero = field.getZero();
  282.         final T one  = field.getOne();

  283.         // Select the relevant B parameter for the current height (Eq. 109 and 110)
  284.         final T be  = parameters.getBE(h);
  285.         final T bf1 = parameters.getBF1(h);
  286.         final T bf2 = parameters.getB2Bot();

  287.         // Useful array of constants
  288.         final T[] ct = MathArrays.buildArray(field, 3);
  289.         ct[0] = bf2.reciprocal();
  290.         ct[1] = bf1.reciprocal();
  291.         ct[2] = be.reciprocal();

  292.         // Compute the exponential argument for each layer (Eq. 111 to 113)
  293.         final T[] arguments = computeExponentialArguments(h, parameters);

  294.         // S coefficients
  295.         final T[] s = MathArrays.buildArray(field, 3);
  296.         // Array of corrective terms
  297.         final T[] ds = MathArrays.buildArray(field, 3);

  298.         // Layer amplitudes
  299.         final T[] amplitudes = parameters.getLayerAmplitudes();

  300.         // Fill arrays (Eq. 114 to 118)
  301.         for (int i = 0; i < 3; i++) {
  302.             if (FastMath.abs(arguments[i]).getReal() > 25.0) {
  303.                 s[i]  = zero;
  304.                 ds[i] = zero;
  305.             } else {
  306.                 final T expA   = clipExp(arguments[i]);
  307.                 final T opExpA = expA.add(1.0);
  308.                 s[i]  = amplitudes[i].multiply(expA.divide(opExpA.multiply(opExpA)));
  309.                 ds[i] = ct[i].multiply(expA.negate().add(1.0).divide(expA.add(1.0)));
  310.             }
  311.         }

  312.         // Electron density
  313.         final T aNo = s[0].add(s[1]).add(s[2]);
  314.         if (applyChapmanParameters(h.getReal())) {
  315.             // Chapman parameters (Eq. 119 and 120)
  316.             final T bc = one.linearCombination(s[0], ds[0], s[1], ds[1], s[2], ds[2]).divide(aNo).multiply(10.0).negate().add(1.0);
  317.             final T z  = h.subtract(100.0).multiply(0.1);
  318.             // Electron density (Eq. 121)
  319.             return aNo.multiply(clipExp(bc.multiply(z).add(clipExp(z.negate())).negate().add(1.0))).multiply(DENSITY_FACTOR);
  320.         } else {
  321.             return aNo.multiply(DENSITY_FACTOR);
  322.         }

  323.     }

  324.     /**
  325.      * Computes the electron density of the topside.
  326.      * @param h height in km
  327.      * @param parameters NeQuick model parameters
  328.      * @return the electron density N in m⁻³
  329.      */
  330.     private double topElectronDensity(final double h, final NeQuickParameters parameters) {

  331.         // Constant parameters (Eq. 122 and 123)
  332.         final double g = 0.125;
  333.         final double r = 100.0;

  334.         // Arguments deltaH and z (Eq. 124 and 125)
  335.         final double deltaH = h - parameters.getHmF2();
  336.         final double h0     = computeH0(parameters);
  337.         final double z      = deltaH / (h0 * (1.0 + (r * g * deltaH) / (r * h0 + g * deltaH)));

  338.         // Exponential (Eq. 126)
  339.         final double ee = clipExp(z);

  340.         // Electron density (Eq. 127)
  341.         if (ee > 1.0e11) {
  342.             return (4.0 * parameters.getNmF2() / ee) * DENSITY_FACTOR;
  343.         } else {
  344.             final double opExpZ = 1.0 + ee;
  345.             return ((4.0 * parameters.getNmF2() * ee) / (opExpZ * opExpZ)) * DENSITY_FACTOR;
  346.         }
  347.     }

  348.     /**
  349.      * Computes the electron density of the topside.
  350.      * @param <T> type of the elements
  351.      * @param h height in km
  352.      * @param parameters NeQuick model parameters
  353.      * @return the electron density N in m⁻³
  354.      */
  355.     private <T extends CalculusFieldElement<T>> T topElectronDensity(final T h,
  356.                                                                      final FieldNeQuickParameters<T> parameters) {

  357.         // Constant parameters (Eq. 122 and 123)
  358.         final double g = 0.125;
  359.         final double r = 100.0;

  360.         // Arguments deltaH and z (Eq. 124 and 125)
  361.         final T deltaH = h.subtract(parameters.getHmF2());
  362.         final T h0     = computeH0(parameters);
  363.         final T z      = deltaH.divide(h0.multiply(deltaH.multiply(r).multiply(g).divide(h0.multiply(r).add(deltaH.multiply(g))).add(1.0)));

  364.         // Exponential (Eq. 126)
  365.         final T ee = clipExp(z);

  366.         // Electron density (Eq. 127)
  367.         if (ee.getReal() > 1.0e11) {
  368.             return parameters.getNmF2().multiply(4.0).divide(ee).multiply(DENSITY_FACTOR);
  369.         } else {
  370.             final T opExpZ = ee.add(1.0);
  371.             return parameters.getNmF2().multiply(4.0).multiply(ee).divide(opExpZ.multiply(opExpZ)).multiply(DENSITY_FACTOR);
  372.         }
  373.     }

  374.     /**
  375.      * Lazy loading of CCIR data.
  376.      * @param date current date components
  377.      */
  378.     private void loadsIfNeeded(final DateComponents date) {

  379.         // Month index
  380.         final int monthIndex = date.getMonth() - 1;

  381.         // Check if CCIR has already been loaded for this month
  382.         if (flattenF2[monthIndex] == null) {

  383.             // Read file
  384.             final CCIRLoader loader = new CCIRLoader();
  385.             loader.loadCCIRCoefficients(date);

  386.             // Update arrays
  387.             this.flattenF2[monthIndex]  = flatten(loader.getF2());
  388.             this.flattenFm3[monthIndex] = flatten(loader.getFm3());
  389.         }
  390.     }

  391.     /** Flatten a 3-dimensions array.
  392.      * <p>
  393.      * This method convert 3-dimensions arrays into 1-dimension arrays
  394.      * optimized to avoid cache misses when looping over all elements.
  395.      * </p>
  396.      * @param original original array a[i][j][k]
  397.      * @return flatten array, for embedded loops on j (outer), k (intermediate), i (inner)
  398.      */
  399.     private double[] flatten(final double[][][] original) {
  400.         final double[] flatten = new double[original.length * original[0].length * original[0][0].length];
  401.         int index = 0;
  402.         for (int j = 0; j < original[0].length; j++) {
  403.             for (int k = 0; k < original[0][0].length; k++) {
  404.                 for (final double[][] doubles : original) {
  405.                     flatten[index++] = doubles[j][k];
  406.                 }
  407.             }
  408.         }
  409.         return flatten;
  410.     }

  411.     /**
  412.      * A clipped exponential function.
  413.      * <p>
  414.      * This function, describe in section F.2.12.2 of the reference document, is
  415.      * recommended for the computation of exponential values.
  416.      * </p>
  417.      * @param power power for exponential function
  418.      * @return clipped exponential value
  419.      */
  420.     protected double clipExp(final double power) {
  421.         if (power > 80.0) {
  422.             return 5.5406E34;
  423.         } else if (power < -80) {
  424.             return 1.8049E-35;
  425.         } else {
  426.             return FastMath.exp(power);
  427.         }
  428.     }

  429.     /**
  430.      * A clipped exponential function.
  431.      * <p>
  432.      * This function, describe in section F.2.12.2 of the reference document, is
  433.      * recommended for the computation of exponential values.
  434.      * </p>
  435.      * @param <T> type of the elements
  436.      * @param power power for exponential function
  437.      * @return clipped exponential value
  438.      */
  439.     protected <T extends CalculusFieldElement<T>> T clipExp(final T power) {
  440.         if (power.getReal() > 80.0) {
  441.             return power.newInstance(5.5406E34);
  442.         } else if (power.getReal() < -80) {
  443.             return power.newInstance(1.8049E-35);
  444.         } else {
  445.             return FastMath.exp(power);
  446.         }
  447.     }

  448.     /**
  449.      * This method allows the computation of the Slant Total Electron Content (STEC).
  450.      *
  451.      * @param dateTime current date
  452.      * @param ray      ray-perigee parameters
  453.      * @return the STEC in TECUnits
  454.      */
  455.     abstract double stec(DateTimeComponents dateTime, Ray ray);

  456.     /**
  457.      * This method allows the computation of the Slant Total Electron Content (STEC).
  458.      *
  459.      * @param <T>      type of the field elements
  460.      * @param dateTime current date
  461.      * @param ray      ray-perigee parameters
  462.      * @return the STEC in TECUnits
  463.      */
  464.     abstract <T extends CalculusFieldElement<T>> T stec(DateTimeComponents dateTime, FieldRay<T> ray);

  465.     /**
  466.      * Check if Chapman parameters should be applied.
  467.      *
  468.      * @param hInKm height in kilometers
  469.      * @return true if Chapman parameters should be applied
  470.      * @since 13.0
  471.      */
  472.     abstract boolean applyChapmanParameters(double hInKm);

  473.     /**
  474.      * Compute exponential arguments.
  475.      * @param h height in km
  476.      * @param parameters NeQuick model parameters
  477.      * @return exponential arguments
  478.      * @since 13.0
  479.      */
  480.     abstract double[] computeExponentialArguments(double h, NeQuickParameters parameters);

  481.     /**
  482.      * Compute exponential arguments.
  483.      * @param <T>   type of the field elements
  484.      * @param h height in km
  485.      * @param parameters NeQuick model parameters
  486.      * @return exponential arguments
  487.      * @since 13.0
  488.      */
  489.     abstract <T extends CalculusFieldElement<T>> T[] computeExponentialArguments(T h,
  490.                                                                                  FieldNeQuickParameters<T> parameters);

  491.     /**
  492.      * Compute topside thickness parameter.
  493.      * @param parameters NeQuick model parameters
  494.      * @return topside thickness parameter
  495.      * @since 13.0
  496.      */
  497.     abstract double computeH0(NeQuickParameters parameters);

  498.     /**
  499.      * Compute topside thickness parameter.
  500.      * @param <T>   type of the field elements
  501.      * @param parameters NeQuick model parameters
  502.      * @return topside thickness parameter
  503.      * @since 13.0
  504.      */
  505.     abstract <T extends CalculusFieldElement<T>> T computeH0(FieldNeQuickParameters<T> parameters);

  506. }