NeQuickModel.java

/* Copyright 2002-2025 CS GROUP
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.models.earth.ionosphere.nequick;

import java.util.Collections;
import java.util.List;

import org.hipparchus.CalculusFieldElement;
import org.hipparchus.Field;
import org.hipparchus.util.FastMath;
import org.hipparchus.util.MathArrays;
import org.orekit.bodies.BodyShape;
import org.orekit.bodies.FieldGeodeticPoint;
import org.orekit.bodies.GeodeticPoint;
import org.orekit.frames.TopocentricFrame;
import org.orekit.models.earth.ionosphere.IonosphericModel;
import org.orekit.propagation.FieldSpacecraftState;
import org.orekit.propagation.SpacecraftState;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateComponents;
import org.orekit.time.DateTimeComponents;
import org.orekit.time.FieldAbsoluteDate;
import org.orekit.time.TimeScale;
import org.orekit.utils.ParameterDriver;
import org.orekit.utils.units.Unit;

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

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

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

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

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

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

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

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

        this.utc = utc;

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

    }

    /** Get UTC time scale.
     * @return UTC time scale
     * @since 13.0
     */
    public TimeScale getUtc() {
        return utc;
    }

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

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

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

        // Ionospheric delay
        final double factor = DELAY_FACTOR / (frequency * frequency);
        return factor * tec;
    }

    /** {@inheritDoc} */
    @Override
    public <T extends CalculusFieldElement<T>> T pathDelay(final FieldSpacecraftState<T> state,
                                                           final TopocentricFrame baseFrame,
                                                           final double frequency,
                                                           final T[] parameters) {
        // Date
        final FieldAbsoluteDate<T> date = state.getDate();
        // Point
        final FieldGeodeticPoint<T> recPoint = baseFrame.getPoint(date.getField());


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

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

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

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

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

    /**
     * This method allows the computation of the Slant Total Electron Content (STEC).
     * @param <T> type of the elements
     * @param date current date
     * @param recP receiver position
     * @param satP satellite position
     * @return the STEC in TECUnits
     */
    public <T extends CalculusFieldElement<T>> T stec(final FieldAbsoluteDate<T> date,
                                                      final FieldGeodeticPoint<T> recP,
                                                      final FieldGeodeticPoint<T> satP) {
        return stec(date.getComponents(utc), new FieldRay<>(recP, satP));
    }

    /** Compute modip for a location.
     * @param latitude latitude
     * @param longitude longitude
     * @return modip at specified location
     * @since 13.0
     */
    protected abstract double computeMODIP(double latitude, double longitude);

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

    /**
     * Compute Fourier time series.
     * @param dateTime current date time components
     * @param az effective ionisation level
     * @return Fourier time series
     * @since 13.0.1
     */
    public FourierTimeSeries computeFourierTimeSeries(final DateTimeComponents dateTime, final double az) {

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

        return new FourierTimeSeries(dateTime, az,
                                     flattenF2[dateTime.getDate().getMonth() - 1],
                                     flattenFm3[dateTime.getDate().getMonth() - 1]);

    }

    /**
     * Computes the electron density at a given height.
     * @param dateTime date
     * @param az effective ionization level
     * @param latitude latitude along the integration path
     * @param longitude longitude along the integration path
     * @param h height along the integration path in m
     * @return electron density [m⁻³]
     * @since 13.0
     */
    public double electronDensity(final DateTimeComponents dateTime, final double az,
                                  final double latitude, final double longitude, final double h) {
        return electronDensity(computeFourierTimeSeries(dateTime, az), latitude, longitude, h);
    }

    /**
     * Computes the electron density at a given height.
     * @param fourierTimeSeries Fourier time series for foF2 and M(3000)F2 layer (flatten array)
     * @param latitude latitude along the integration path
     * @param longitude longitude along the integration path
     * @param h height along the integration path in m
     * @return electron density [m⁻³]
     * @since 13.0.1
     */
    public double electronDensity(final FourierTimeSeries fourierTimeSeries,
                                  final double latitude, final double longitude, final double h) {

        final double modip = computeMODIP(latitude, longitude);
        final NeQuickParameters parameters = new NeQuickParameters(fourierTimeSeries, latitude, longitude, modip);

        // Convert height in kilometers
        final double hInKm = Unit.KILOMETRE.fromSI(h);

        // Electron density
        final double n;
        if (hInKm <= parameters.getHmF2()) {
            n = bottomElectronDensity(hInKm, parameters);
        } else {
            n = topElectronDensity(hInKm, parameters);
        }

        return n;

    }

    /**
     * Compute Fourier time series.
     * @param <T> type of the elements
     * @param dateTime current date time components
     * @param az effective ionisation level
     * @return Fourier time series
     * @since 13.0.1
     */
    public <T extends CalculusFieldElement<T>> FieldFourierTimeSeries<T> computeFourierTimeSeries(final DateTimeComponents dateTime,
                                                                                                  final T az) {

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

        return new FieldFourierTimeSeries<>(dateTime, az,
                                            flattenF2[dateTime.getDate().getMonth() - 1],
                                            flattenFm3[dateTime.getDate().getMonth() - 1]);

    }

    /**
     * Computes the electron density at a given height.
     * @param <T> type of the elements
     * @param dateTime date
     * @param az effective ionization level
     * @param latitude latitude along the integration path
     * @param longitude longitude along the integration path
     * @param h height along the integration path in m
     * @return electron density [m⁻³]
     * @since 13.0
     * CalculusFieldElement, CalculusFieldElement, CalculusFieldElement)}
     */
    public <T extends CalculusFieldElement<T>> T electronDensity(final DateTimeComponents dateTime, final T az,
                                                                 final T latitude, final T longitude, final T h) {
        return electronDensity(computeFourierTimeSeries(dateTime, az), latitude, longitude, h);
    }

    /**
     * Computes the electron density at a given height.
     * @param <T> type of the elements
     * @param fourierTimeSeries Fourier time series for foF2 and M(3000)F2 layer (flatten array)
     * @param latitude latitude along the integration path
     * @param longitude longitude along the integration path
     * @param h height along the integration path in m
     * @return electron density [m⁻³]
     * @since 13.0.1
     */
    public <T extends CalculusFieldElement<T>> T electronDensity(final FieldFourierTimeSeries<T> fourierTimeSeries,
                                                                 final T latitude, final T longitude, final T h) {

        final T modip = computeMODIP(latitude, longitude);
        final FieldNeQuickParameters<T> parameters = new FieldNeQuickParameters<>(fourierTimeSeries, latitude, longitude, modip);

        // Convert height in kilometers
        final T hInKm = Unit.KILOMETRE.fromSI(h);

        // Electron density
        final T n;
        if (hInKm.getReal() <= parameters.getHmF2().getReal()) {
            n = bottomElectronDensity(hInKm, parameters);
        } else {
            n = topElectronDensity(hInKm, parameters);
        }

        return n;

    }

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

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

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

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

        // S coefficients
        final double[] s = new double[3];
        // Array of corrective terms
        final double[] ds = new double[3];

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

        // Fill arrays (Eq. 114 to 118)
        for (int i = 0; i < 3; i++) {
            if (FastMath.abs(arguments[i]) > 25.0) {
                s[i]  = 0.0;
                ds[i] = 0.0;
            } else {
                final double expA   = clipExp(arguments[i]);
                final double opExpA = 1.0 + expA;
                s[i]  = amplitudes[i] * (expA / (opExpA * opExpA));
                ds[i] = ct[i] * ((1.0 - expA) / (1.0 + expA));
            }
        }

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

    }

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

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

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

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

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

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

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

        // Fill arrays (Eq. 114 to 118)
        for (int i = 0; i < 3; i++) {
            if (FastMath.abs(arguments[i]).getReal() > 25.0) {
                s[i]  = zero;
                ds[i] = zero;
            } else {
                final T expA   = clipExp(arguments[i]);
                final T opExpA = expA.add(1.0);
                s[i]  = amplitudes[i].multiply(expA.divide(opExpA.multiply(opExpA)));
                ds[i] = ct[i].multiply(expA.negate().add(1.0).divide(expA.add(1.0)));
            }
        }

        // Electron density
        final T aNo = s[0].add(s[1]).add(s[2]);
        if (applyChapmanParameters(h.getReal())) {
            // Chapman parameters (Eq. 119 and 120)
            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);
            final T z  = h.subtract(100.0).multiply(0.1);
            // Electron density (Eq. 121)
            return aNo.multiply(clipExp(bc.multiply(z).add(clipExp(z.negate())).negate().add(1.0))).multiply(DENSITY_FACTOR);
        } else {
            return aNo.multiply(DENSITY_FACTOR);
        }

    }

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

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

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

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

        // Electron density (Eq. 127)
        if (ee > 1.0e11) {
            return (4.0 * parameters.getNmF2() / ee) * DENSITY_FACTOR;
        } else {
            final double opExpZ = 1.0 + ee;
            return ((4.0 * parameters.getNmF2() * ee) / (opExpZ * opExpZ)) * DENSITY_FACTOR;
        }
    }

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

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

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

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

        // Electron density (Eq. 127)
        if (ee.getReal() > 1.0e11) {
            return parameters.getNmF2().multiply(4.0).divide(ee).multiply(DENSITY_FACTOR);
        } else {
            final T opExpZ = ee.add(1.0);
            return parameters.getNmF2().multiply(4.0).multiply(ee).divide(opExpZ.multiply(opExpZ)).multiply(DENSITY_FACTOR);
        }
    }

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

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

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

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

            // Update arrays
            this.flattenF2[monthIndex]  = flatten(loader.getF2());
            this.flattenFm3[monthIndex] = flatten(loader.getFm3());
        }
    }

    /** Flatten a 3-dimensions array.
     * <p>
     * This method convert 3-dimensions arrays into 1-dimension arrays
     * optimized to avoid cache misses when looping over all elements.
     * </p>
     * @param original original array a[i][j][k]
     * @return flatten array, for embedded loops on j (outer), k (intermediate), i (inner)
     */
    private double[] flatten(final double[][][] original) {
        final double[] flatten = new double[original.length * original[0].length * original[0][0].length];
        int index = 0;
        for (int j = 0; j < original[0].length; j++) {
            for (int k = 0; k < original[0][0].length; k++) {
                for (final double[][] doubles : original) {
                    flatten[index++] = doubles[j][k];
                }
            }
        }
        return flatten;
    }

    /**
     * A clipped exponential function.
     * <p>
     * This function, describe in section F.2.12.2 of the reference document, is
     * recommended for the computation of exponential values.
     * </p>
     * @param power power for exponential function
     * @return clipped exponential value
     */
    protected double clipExp(final double power) {
        if (power > 80.0) {
            return 5.5406E34;
        } else if (power < -80) {
            return 1.8049E-35;
        } else {
            return FastMath.exp(power);
        }
    }

    /**
     * A clipped exponential function.
     * <p>
     * This function, describe in section F.2.12.2 of the reference document, is
     * recommended for the computation of exponential values.
     * </p>
     * @param <T> type of the elements
     * @param power power for exponential function
     * @return clipped exponential value
     */
    protected <T extends CalculusFieldElement<T>> T clipExp(final T power) {
        if (power.getReal() > 80.0) {
            return power.newInstance(5.5406E34);
        } else if (power.getReal() < -80) {
            return power.newInstance(1.8049E-35);
        } else {
            return FastMath.exp(power);
        }
    }

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

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

    /**
     * Check if Chapman parameters should be applied.
     *
     * @param hInKm height in kilometers
     * @return true if Chapman parameters should be applied
     * @since 13.0
     */
    abstract boolean applyChapmanParameters(double hInKm);

    /**
     * Compute exponential arguments.
     * @param h height in km
     * @param parameters NeQuick model parameters
     * @return exponential arguments
     * @since 13.0
     */
    abstract double[] computeExponentialArguments(double h, NeQuickParameters parameters);

    /**
     * Compute exponential arguments.
     * @param <T>   type of the field elements
     * @param h height in km
     * @param parameters NeQuick model parameters
     * @return exponential arguments
     * @since 13.0
     */
    abstract <T extends CalculusFieldElement<T>> T[] computeExponentialArguments(T h,
                                                                                 FieldNeQuickParameters<T> parameters);

    /**
     * Compute topside thickness parameter.
     * @param parameters NeQuick model parameters
     * @return topside thickness parameter
     * @since 13.0
     */
    abstract double computeH0(NeQuickParameters parameters);

    /**
     * Compute topside thickness parameter.
     * @param <T>   type of the field elements
     * @param parameters NeQuick model parameters
     * @return topside thickness parameter
     * @since 13.0
     */
    abstract <T extends CalculusFieldElement<T>> T computeH0(FieldNeQuickParameters<T> parameters);

}