NeQuickModel.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 java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.InputStreamReader;
  22. import java.nio.charset.StandardCharsets;
  23. import java.util.Collections;
  24. import java.util.List;
  25. import java.util.regex.Pattern;

  26. import org.hipparchus.CalculusFieldElement;
  27. import org.hipparchus.Field;
  28. import org.hipparchus.util.FastMath;
  29. import org.hipparchus.util.FieldSinCos;
  30. import org.hipparchus.util.MathArrays;
  31. import org.hipparchus.util.SinCos;
  32. import org.orekit.annotation.DefaultDataContext;
  33. import org.orekit.bodies.BodyShape;
  34. import org.orekit.bodies.FieldGeodeticPoint;
  35. import org.orekit.bodies.GeodeticPoint;
  36. import org.orekit.data.DataContext;
  37. import org.orekit.errors.OrekitException;
  38. import org.orekit.errors.OrekitMessages;
  39. import org.orekit.frames.TopocentricFrame;
  40. import org.orekit.propagation.FieldSpacecraftState;
  41. import org.orekit.propagation.SpacecraftState;
  42. import org.orekit.time.AbsoluteDate;
  43. import org.orekit.time.DateComponents;
  44. import org.orekit.time.DateTimeComponents;
  45. import org.orekit.time.FieldAbsoluteDate;
  46. import org.orekit.time.TimeScale;
  47. import org.orekit.utils.ParameterDriver;

  48. /**
  49.  * NeQuick ionospheric delay model.
  50.  *
  51.  * @author Bryan Cazabonne
  52.  *
  53.  * @see "European Union (2016). European GNSS (Galileo) Open Service-Ionospheric Correction
  54.  *       Algorithm for Galileo Single Frequency Users. 1.2."
  55.  *
  56.  * @since 10.1
  57.  */
  58. public class NeQuickModel implements IonosphericModel {

  59.     /** NeQuick resources base directory. */
  60.     private static final String NEQUICK_BASE = "/assets/org/orekit/nequick/";

  61.     /** Pattern for delimiting regular expressions. */
  62.     private static final Pattern SEPARATOR = Pattern.compile("\\s+");

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

  65.     /** Meters to kilometers converter. */
  66.     private static final double M_TO_KM = 0.001;

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

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

  71.     /** The three ionospheric coefficients broadcast in the Galileo navigation message. */
  72.     private final double[] alpha;

  73.     /** MODIP grid. */
  74.     private final double[][] stModip;

  75.     /** Month used for loading CCIR coefficients. */
  76.     private int month;

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

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

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

  83.     /**
  84.      * Build a new instance.
  85.      *
  86.      * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
  87.      *
  88.      * @param alpha effective ionisation level coefficients
  89.      * @see #NeQuickModel(double[], TimeScale)
  90.      */
  91.     @DefaultDataContext
  92.     public NeQuickModel(final double[] alpha) {
  93.         this(alpha, DataContext.getDefault().getTimeScales().getUTC());
  94.     }

  95.     /**
  96.      * Build a new instance.
  97.      * @param alpha effective ionisation level coefficients
  98.      * @param utc UTC time scale.
  99.      * @since 10.1
  100.      */
  101.     public NeQuickModel(final double[] alpha,
  102.                         final TimeScale utc) {
  103.         // F2 layer values
  104.         this.month      = 0;
  105.         this.flattenF2  = null;
  106.         this.flattenFm3 = null;
  107.         // Read modip grid
  108.         final MODIPLoader parser = new MODIPLoader();
  109.         parser.loadMODIPGrid();
  110.         this.stModip = parser.getMODIPGrid();
  111.         // Ionisation level coefficients
  112.         this.alpha = alpha.clone();
  113.         this.utc = utc;
  114.     }

  115.     @Override
  116.     public double pathDelay(final SpacecraftState state, final TopocentricFrame baseFrame,
  117.                             final double frequency, final double[] parameters) {
  118.         // Point
  119.         final GeodeticPoint recPoint = baseFrame.getPoint();
  120.         // Date
  121.         final AbsoluteDate date = state.getDate();

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

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

  128.         // Ionospheric delay
  129.         final double factor = DELAY_FACTOR / (frequency * frequency);
  130.         return factor * tec;
  131.     }

  132.     @Override
  133.     public <T extends CalculusFieldElement<T>> T pathDelay(final FieldSpacecraftState<T> state, final TopocentricFrame baseFrame,
  134.                                                        final double frequency, final T[] parameters) {
  135.         // Date
  136.         final FieldAbsoluteDate<T> date = state.getDate();
  137.         // Point
  138.         final FieldGeodeticPoint<T> recPoint = baseFrame.getPoint(date.getField());


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

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

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

  149.     @Override
  150.     public List<ParameterDriver> getParametersDrivers() {
  151.         return Collections.emptyList();
  152.     }

  153.     /**
  154.      * This method allows the computation of the Stant Total Electron Content (STEC).
  155.      * <p>
  156.      * This method follows the Gauss algorithm exposed in section 2.5.8.2.8 of
  157.      * the reference document.
  158.      * </p>
  159.      * @param date current date
  160.      * @param recP receiver position
  161.      * @param satP satellite position
  162.      * @return the STEC in TECUnits
  163.      */
  164.     public double stec(final AbsoluteDate date, final GeodeticPoint recP, final GeodeticPoint satP) {

  165.         // Ray-perigee parameters
  166.         final Ray ray = new Ray(recP, satP);

  167.         // Load the correct CCIR file
  168.         final DateTimeComponents dateTime = date.getComponents(utc);
  169.         loadsIfNeeded(dateTime.getDate());

  170.         // Tolerance for the integration accuracy. Defined inside the reference document, section 2.5.8.1.
  171.         final double h1 = recP.getAltitude();
  172.         final double tolerance;
  173.         if (h1 < 1000000.0) {
  174.             tolerance = 0.001;
  175.         } else {
  176.             tolerance = 0.01;
  177.         }

  178.         // Integration
  179.         int n = 8;
  180.         final Segment seg1 = new Segment(n, ray);
  181.         double gn1 = stecIntegration(seg1, dateTime);
  182.         n *= 2;
  183.         final Segment seg2 = new Segment(n, ray);
  184.         double gn2 = stecIntegration(seg2, dateTime);

  185.         int count = 1;
  186.         while (FastMath.abs(gn2 - gn1) > tolerance * FastMath.abs(gn1) && count < 20) {
  187.             gn1 = gn2;
  188.             n *= 2;
  189.             final Segment seg = new Segment(n, ray);
  190.             gn2 = stecIntegration(seg, dateTime);
  191.             count += 1;
  192.         }

  193.         // If count > 20 the integration did not converge
  194.         if (count == 20) {
  195.             throw new OrekitException(OrekitMessages.STEC_INTEGRATION_DID_NOT_CONVERGE);
  196.         }

  197.         // Eq. 202
  198.         return (gn2 + ((gn2 - gn1) / 15.0)) * 1.0e-16;
  199.     }

  200.     /**
  201.      * This method allows the computation of the Stant Total Electron Content (STEC).
  202.      * <p>
  203.      * This method follows the Gauss algorithm exposed in section 2.5.8.2.8 of
  204.      * the reference document.
  205.      * </p>
  206.      * @param <T> type of the elements
  207.      * @param date current date
  208.      * @param recP receiver position
  209.      * @param satP satellite position
  210.      * @return the STEC in TECUnits
  211.      */
  212.     public <T extends CalculusFieldElement<T>> T stec(final FieldAbsoluteDate<T> date,
  213.                                                   final FieldGeodeticPoint<T> recP,
  214.                                                   final FieldGeodeticPoint<T> satP) {

  215.         // Field
  216.         final Field<T> field = date.getField();

  217.         // Ray-perigee parameters
  218.         final FieldRay<T> ray = new FieldRay<>(field, recP, satP);

  219.         // Load the correct CCIR file
  220.         final DateTimeComponents dateTime = date.getComponents(utc);
  221.         loadsIfNeeded(dateTime.getDate());

  222.         // Tolerance for the integration accuracy. Defined inside the reference document, section 2.5.8.1.
  223.         final T h1 = recP.getAltitude();
  224.         final double tolerance;
  225.         if (h1.getReal() < 1000000.0) {
  226.             tolerance = 0.001;
  227.         } else {
  228.             tolerance = 0.01;
  229.         }

  230.         // Integration
  231.         int n = 8;
  232.         final FieldSegment<T> seg1 = new FieldSegment<>(field, n, ray);
  233.         T gn1 = stecIntegration(field, seg1, dateTime);
  234.         n *= 2;
  235.         final FieldSegment<T> seg2 = new FieldSegment<>(field, n, ray);
  236.         T gn2 = stecIntegration(field, seg2, dateTime);

  237.         int count = 1;
  238.         while (FastMath.abs(gn2.subtract(gn1)).getReal() > FastMath.abs(gn1).multiply(tolerance).getReal() && count < 20) {
  239.             gn1 = gn2;
  240.             n *= 2;
  241.             final FieldSegment<T> seg = new FieldSegment<>(field, n, ray);
  242.             gn2 = stecIntegration(field, seg, dateTime);
  243.             count += 1;
  244.         }

  245.         // If count > 20 the integration did not converge
  246.         if (count == 20) {
  247.             throw new OrekitException(OrekitMessages.STEC_INTEGRATION_DID_NOT_CONVERGE);
  248.         }

  249.         // Eq. 202
  250.         return gn2.add(gn2.subtract(gn1).divide(15.0)).multiply(1.0e-16);
  251.     }

  252.     /**
  253.      * This method perfoms the STEC integration.
  254.      * @param seg coordinates along the integration path
  255.      * @param dateTime current date and time componentns
  256.      * @return result of the integration
  257.      */
  258.     private double stecIntegration(final Segment seg, final DateTimeComponents dateTime) {
  259.         // Integration points
  260.         final double[] heightS    = seg.getHeights();
  261.         final double[] latitudeS  = seg.getLatitudes();
  262.         final double[] longitudeS = seg.getLongitudes();

  263.         // Compute electron density
  264.         double density = 0.0;
  265.         for (int i = 0; i < heightS.length; i++) {
  266.             final NeQuickParameters parameters = new NeQuickParameters(dateTime, flattenF2, flattenFm3,
  267.                                                                        latitudeS[i], longitudeS[i],
  268.                                                                        alpha, stModip);
  269.             density += electronDensity(heightS[i], parameters);
  270.         }

  271.         return 0.5 * seg.getInterval() * density;
  272.     }

  273.     /**
  274.      * This method perfoms the STEC integration.
  275.      * @param <T> type of the elements
  276.      * @param field field of the elements
  277.      * @param seg coordinates along the integration path
  278.      * @param dateTime current date and time componentns
  279.      * @return result of the integration
  280.      */
  281.     private <T extends CalculusFieldElement<T>> T stecIntegration(final Field<T> field,
  282.                                                                   final FieldSegment<T> seg,
  283.                                                                   final DateTimeComponents dateTime) {
  284.         // Integration points
  285.         final T[] heightS    = seg.getHeights();
  286.         final T[] latitudeS  = seg.getLatitudes();
  287.         final T[] longitudeS = seg.getLongitudes();

  288.         // Compute electron density
  289.         T density = field.getZero();
  290.         for (int i = 0; i < heightS.length; i++) {
  291.             final FieldNeQuickParameters<T> parameters = new FieldNeQuickParameters<>(dateTime, flattenF2, flattenFm3,
  292.                                                                                       latitudeS[i], longitudeS[i],
  293.                                                                                       alpha, stModip);
  294.             density = density.add(electronDensity(field, heightS[i], parameters));
  295.         }

  296.         return seg.getInterval().multiply(density).multiply(0.5);
  297.     }

  298.     /**
  299.      * Computes the electron density at a given height.
  300.      * @param h height in m
  301.      * @param parameters NeQuick model parameters
  302.      * @return electron density [m^-3]
  303.      */
  304.     private double electronDensity(final double h, final NeQuickParameters parameters) {
  305.         // Convert height in kilometers
  306.         final double hInKm = h * M_TO_KM;
  307.         // Electron density
  308.         final double n;
  309.         if (hInKm <= parameters.getHmF2()) {
  310.             n = bottomElectronDensity(hInKm, parameters);
  311.         } else {
  312.             n = topElectronDensity(hInKm, parameters);
  313.         }
  314.         return n;
  315.     }

  316.     /**
  317.      * Computes the electron density at a given height.
  318.      * @param <T> type of the elements
  319.      * @param field field of the elements
  320.      * @param h height in m
  321.      * @param parameters NeQuick model parameters
  322.      * @return electron density [m^-3]
  323.      */
  324.     private <T extends CalculusFieldElement<T>> T electronDensity(final Field<T> field,
  325.                                                               final T h,
  326.                                                               final FieldNeQuickParameters<T> parameters) {
  327.         // Convert height in kilometers
  328.         final T hInKm = h.multiply(M_TO_KM);
  329.         // Electron density
  330.         final T n;
  331.         if (hInKm.getReal() <= parameters.getHmF2().getReal()) {
  332.             n = bottomElectronDensity(field, hInKm, parameters);
  333.         } else {
  334.             n = topElectronDensity(field, hInKm, parameters);
  335.         }
  336.         return n;
  337.     }

  338.     /**
  339.      * Computes the electron density of the bottomside.
  340.      * @param h height in km
  341.      * @param parameters NeQuick model parameters
  342.      * @return the electron density N in m-3
  343.      */
  344.     private double bottomElectronDensity(final double h, final NeQuickParameters parameters) {

  345.         // Select the relevant B parameter for the current height (Eq. 109 and 110)
  346.         final double be;
  347.         if (h > parameters.getHmE()) {
  348.             be = parameters.getBETop();
  349.         } else {
  350.             be = parameters.getBEBot();
  351.         }
  352.         final double bf1;
  353.         if (h > parameters.getHmF1()) {
  354.             bf1 = parameters.getB1Top();
  355.         } else {
  356.             bf1 = parameters.getB1Bot();
  357.         }
  358.         final double bf2 = parameters.getB2Bot();

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

  363.         // Compute the exponential argument for each layer (Eq. 111 to 113)
  364.         // If h < 100km we use h = 100km as recommended in the reference document
  365.         final double   hTemp = FastMath.max(100.0, h);
  366.         final double   exp   = clipExp(10.0 / (1.0 + FastMath.abs(hTemp - parameters.getHmF2())));
  367.         final double[] arguments = new double[3];
  368.         arguments[0] = (hTemp - parameters.getHmF2()) / bf2;
  369.         arguments[1] = ((hTemp - parameters.getHmF1()) / bf1) * exp;
  370.         arguments[2] = ((hTemp - parameters.getHmE()) / be) * exp;

  371.         // S coefficients
  372.         final double[] s = new double[3];
  373.         // Array of corrective terms
  374.         final double[] ds = new double[3];

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

  377.         // Fill arrays (Eq. 114 to 118)
  378.         for (int i = 0; i < 3; i++) {
  379.             if (FastMath.abs(arguments[i]) > 25.0) {
  380.                 s[i]  = 0.0;
  381.                 ds[i] = 0.0;
  382.             } else {
  383.                 final double expA   = clipExp(arguments[i]);
  384.                 final double opExpA = 1.0 + expA;
  385.                 s[i]  = amplitudes[i] * (expA / (opExpA * opExpA));
  386.                 ds[i] = ct[i] * ((1.0 - expA) / (1.0 + expA));
  387.             }
  388.         }

  389.         // Electron density
  390.         final double aNo = MathArrays.linearCombination(s[0], 1.0, s[1], 1.0, s[2], 1.0);
  391.         if (h >= 100) {
  392.             return aNo * DENSITY_FACTOR;
  393.         } else {
  394.             // Chapman parameters (Eq. 119 and 120)
  395.             final double bc = 1.0 - 10.0 * (MathArrays.linearCombination(s[0], ds[0], s[1], ds[1], s[2], ds[2]) / aNo);
  396.             final double z  = 0.1 * (h - 100.0);
  397.             // Electron density (Eq. 121)
  398.             return aNo * clipExp(1.0 - bc * z - clipExp(-z)) * DENSITY_FACTOR;
  399.         }
  400.     }

  401.     /**
  402.      * Computes the electron density of the bottomside.
  403.      * @param <T> type of the elements
  404.      * @param field field of the elements
  405.      * @param h height in km
  406.      * @param parameters NeQuick model parameters
  407.      * @return the electron density N in m-3
  408.      */
  409.     private <T extends CalculusFieldElement<T>> T bottomElectronDensity(final Field<T> field,
  410.                                                                     final T h,
  411.                                                                     final FieldNeQuickParameters<T> parameters) {

  412.         // Zero and One
  413.         final T zero = field.getZero();
  414.         final T one  = field.getOne();

  415.         // Select the relevant B parameter for the current height (Eq. 109 and 110)
  416.         final T be;
  417.         if (h.getReal() > parameters.getHmE().getReal()) {
  418.             be = parameters.getBETop();
  419.         } else {
  420.             be = parameters.getBEBot();
  421.         }
  422.         final T bf1;
  423.         if (h.getReal() > parameters.getHmF1().getReal()) {
  424.             bf1 = parameters.getB1Top();
  425.         } else {
  426.             bf1 = parameters.getB1Bot();
  427.         }
  428.         final T bf2 = parameters.getB2Bot();

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

  434.         // Compute the exponential argument for each layer (Eq. 111 to 113)
  435.         // If h < 100km we use h = 100km as recommended in the reference document
  436.         final T   hTemp = FastMath.max(zero.newInstance(100.0), h);
  437.         final T   exp   = clipExp(field, FastMath.abs(hTemp.subtract(parameters.getHmF2())).add(1.0).divide(10.0).reciprocal());
  438.         final T[] arguments = MathArrays.buildArray(field, 3);
  439.         arguments[0] = hTemp.subtract(parameters.getHmF2()).divide(bf2);
  440.         arguments[1] = hTemp.subtract(parameters.getHmF1()).divide(bf1).multiply(exp);
  441.         arguments[2] = hTemp.subtract(parameters.getHmE()).divide(be).multiply(exp);

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

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

  448.         // Fill arrays (Eq. 114 to 118)
  449.         for (int i = 0; i < 3; i++) {
  450.             if (FastMath.abs(arguments[i]).getReal() > 25.0) {
  451.                 s[i]  = zero;
  452.                 ds[i] = zero;
  453.             } else {
  454.                 final T expA   = clipExp(field, arguments[i]);
  455.                 final T opExpA = expA.add(1.0);
  456.                 s[i]  = amplitudes[i].multiply(expA.divide(opExpA.multiply(opExpA)));
  457.                 ds[i] = ct[i].multiply(expA.negate().add(1.0).divide(expA.add(1.0)));
  458.             }
  459.         }

  460.         // Electron density
  461.         final T aNo = one.linearCombination(s[0], one, s[1], one, s[2], one);
  462.         if (h.getReal() >= 100) {
  463.             return aNo.multiply(DENSITY_FACTOR);
  464.         } else {
  465.             // Chapman parameters (Eq. 119 and 120)
  466.             final T bc = s[0].multiply(ds[0]).add(one.linearCombination(s[0], ds[0], s[1], ds[1], s[2], ds[2])).divide(aNo).multiply(10.0).negate().add(1.0);
  467.             final T z  = h.subtract(100.0).multiply(0.1);
  468.             // Electron density (Eq. 121)
  469.             return aNo.multiply(clipExp(field, bc.multiply(z).add(clipExp(field, z.negate())).negate().add(1.0))).multiply(DENSITY_FACTOR);
  470.         }
  471.     }

  472.     /**
  473.      * Computes the electron density of the topside.
  474.      * @param h height in km
  475.      * @param parameters NeQuick model parameters
  476.      * @return the electron density N in m-3
  477.      */
  478.     private double topElectronDensity(final double h, final NeQuickParameters parameters) {

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

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

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

  487.         // Electron density (Eq. 127)
  488.         if (ee > 1.0e11) {
  489.             return (4.0 * parameters.getNmF2() / ee) * DENSITY_FACTOR;
  490.         } else {
  491.             final double opExpZ = 1.0 + ee;
  492.             return ((4.0 * parameters.getNmF2() * ee) / (opExpZ * opExpZ)) * DENSITY_FACTOR;
  493.         }
  494.     }

  495.     /**
  496.      * Computes the electron density of the topside.
  497.      * @param <T> type of the elements
  498.      * @param field field of the elements
  499.      * @param h height in km
  500.      * @param parameters NeQuick model parameters
  501.      * @return the electron density N in m-3
  502.      */
  503.     private <T extends CalculusFieldElement<T>> T topElectronDensity(final Field<T> field,
  504.                                                                  final T h,
  505.                                                                  final FieldNeQuickParameters<T> parameters) {

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

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

  512.         // Exponential (Eq. 126)
  513.         final T ee = clipExp(field, z);

  514.         // Electron density (Eq. 127)
  515.         if (ee.getReal() > 1.0e11) {
  516.             return parameters.getNmF2().multiply(4.0).divide(ee).multiply(DENSITY_FACTOR);
  517.         } else {
  518.             final T opExpZ = ee.add(field.getOne());
  519.             return parameters.getNmF2().multiply(4.0).multiply(ee).divide(opExpZ.multiply(opExpZ)).multiply(DENSITY_FACTOR);
  520.         }
  521.     }

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

  527.         // Current month
  528.         final int currentMonth = date.getMonth();

  529.         // Check if date have changed or if f2 and fm3 arrays are null
  530.         if (currentMonth != month || flattenF2 == null || flattenFm3 == null) {
  531.             this.month = currentMonth;

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

  535.             // Update arrays
  536.             this.flattenF2  = flatten(loader.getF2());
  537.             this.flattenFm3 = flatten(loader.getFm3());
  538.         }
  539.     }

  540.     /** Flatten a 3-dimensions array.
  541.      * <p>
  542.      * This method convert 3-dimensions arrays into 1-dimension arrays
  543.      * optimized to avoid cache misses when looping over all elements.
  544.      * </p>
  545.      * @param original original array a[i][j][k]
  546.      * @return flatten array, for embedded loops on j (outer), k (intermediate), i (inner)
  547.      */
  548.     private double[] flatten(final double[][][] original) {
  549.         final double[] flatten = new double[original.length * original[0].length * original[0][0].length];
  550.         int index = 0;
  551.         for (int j = 0; j < original[0].length; j++) {
  552.             for (int k = 0; k < original[0][0].length; k++) {
  553.                 for (final double[][] doubles : original) {
  554.                     flatten[index++] = doubles[j][k];
  555.                 }
  556.             }
  557.         }
  558.         return flatten;
  559.     }

  560.     /**
  561.      * A clipped exponential function.
  562.      * <p>
  563.      * This function, describe in section F.2.12.2 of the reference document, is
  564.      * recommanded for the computation of exponential values.
  565.      * </p>
  566.      * @param power power for exponential function
  567.      * @return clipped exponential value
  568.      */
  569.     private double clipExp(final double power) {
  570.         if (power > 80.0) {
  571.             return 5.5406E34;
  572.         } else if (power < -80) {
  573.             return 1.8049E-35;
  574.         } else {
  575.             return FastMath.exp(power);
  576.         }
  577.     }

  578.     /**
  579.      * A clipped exponential function.
  580.      * <p>
  581.      * This function, describe in section F.2.12.2 of the reference document, is
  582.      * recommanded for the computation of exponential values.
  583.      * </p>
  584.      * @param <T> type of the elements
  585.      * @param field field of the elements
  586.      * @param power power for exponential function
  587.      * @return clipped exponential value
  588.      */
  589.     private <T extends CalculusFieldElement<T>> T clipExp(final Field<T> field, final T power) {
  590.         final T zero = field.getZero();
  591.         if (power.getReal() > 80.0) {
  592.             return zero.newInstance(5.5406E34);
  593.         } else if (power.getReal() < -80) {
  594.             return zero.newInstance(1.8049E-35);
  595.         } else {
  596.             return FastMath.exp(power);
  597.         }
  598.     }

  599.     /** Get a data stream.
  600.      * @param name file name of the resource stream
  601.      * @return stream
  602.      */
  603.     private static InputStream getStream(final String name) {
  604.         return NeQuickModel.class.getResourceAsStream(name);
  605.     }

  606.     /**
  607.      * Parser for Modified Dip Latitude (MODIP) grid file.
  608.      * <p>
  609.      * The MODIP grid allows to estimate MODIP μ [deg] at a given point (φ,λ)
  610.      * by interpolation of the relevant values contained in the support file.
  611.      * </p> <p>
  612.      * The file contains the values of MODIP (expressed in degrees) on a geocentric grid
  613.      * from 90°S to 90°N with a 5-degree step in latitude and from 180°W to 180°E with a
  614.      * 10-degree in longitude.
  615.      * </p>
  616.      */
  617.     private static class MODIPLoader {

  618.         /** Supported name for MODIP grid. */
  619.         private static final String SUPPORTED_NAME = NEQUICK_BASE + "modip.txt";

  620.         /** MODIP grid. */
  621.         private double[][] grid;

  622.         /**
  623.          * Build a new instance.
  624.          */
  625.         MODIPLoader() {
  626.             this.grid = null;
  627.         }

  628.         /** Returns the MODIP grid array.
  629.          * @return the MODIP grid array
  630.          */
  631.         public double[][] getMODIPGrid() {
  632.             return grid.clone();
  633.         }

  634.         /**
  635.          * Load the data using supported names.
  636.          */
  637.         public void loadMODIPGrid() {
  638.             try (InputStream in = getStream(SUPPORTED_NAME)) {
  639.                 loadData(in, SUPPORTED_NAME);
  640.             } catch (IOException e) {
  641.                 throw new OrekitException(OrekitMessages.INTERNAL_ERROR, e);
  642.             }

  643.             // Throw an exception if MODIP grid was not loaded properly
  644.             if (grid == null) {
  645.                 throw new OrekitException(OrekitMessages.MODIP_GRID_NOT_LOADED, SUPPORTED_NAME);
  646.             }
  647.         }

  648.         /**
  649.          * Load data from a stream.
  650.          * @param input input stream
  651.          * @param name name of the file
  652.          * @throws IOException if data can't be read
  653.          */
  654.         public void loadData(final InputStream input, final String name)
  655.             throws IOException {

  656.             // Grid size
  657.             final int size = 39;

  658.             // Initialize array
  659.             final double[][] array = new double[size][size];

  660.             // Open stream and parse data
  661.             int   lineNumber = 0;
  662.             String line      = null;
  663.             try (InputStreamReader isr = new InputStreamReader(input, StandardCharsets.UTF_8);
  664.                  BufferedReader    br = new BufferedReader(isr)) {

  665.                 for (line = br.readLine(); line != null; line = br.readLine()) {
  666.                     ++lineNumber;
  667.                     line = line.trim();

  668.                     // Read grid data
  669.                     if (!line.isEmpty()) {
  670.                         final String[] modip_line = SEPARATOR.split(line);
  671.                         for (int column = 0; column < modip_line.length; column++) {
  672.                             array[lineNumber - 1][column] = Double.parseDouble(modip_line[column]);
  673.                         }
  674.                     }

  675.                 }

  676.             } catch (NumberFormatException nfe) {
  677.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  678.                                           lineNumber, name, line);
  679.             }

  680.             // Clone parsed grid
  681.             grid = array.clone();

  682.         }
  683.     }

  684.     /**
  685.      * Parser for CCIR files.
  686.      * <p>
  687.      * Numerical grid maps which describe the regular variation of the ionosphere.
  688.      * They are used to derive other variables such as critical frequencies and transmission factors.
  689.      * </p> <p>
  690.      * The coefficients correspond to low and high solar activity conditions.
  691.      * </p> <p>
  692.      * The CCIR file naming convention is ccirXX.asc where each XX means month + 10.
  693.      * </p> <p>
  694.      * Coefficients are store into tow arrays, F2 and Fm3. F2 coefficients are used for the computation
  695.      * of the F2 layer critical frequency. Fm3 for the computation of the F2 layer maximum usable frequency factor.
  696.      * The size of these two arrays is fixed and discussed into the section 2.5.3.2
  697.      * of the reference document.
  698.      * </p>
  699.      */
  700.     private static class CCIRLoader {

  701.         /** Default supported files name pattern. */
  702.         public static final String DEFAULT_SUPPORTED_NAME = "ccir**.asc";

  703.         /** Total number of F2 coefficients contained in the file. */
  704.         private static final int NUMBER_F2_COEFFICIENTS = 1976;

  705.         /** Rows number for F2 and Fm3 arrays. */
  706.         private static final int ROWS = 2;

  707.         /** Columns number for F2 array. */
  708.         private static final int TOTAL_COLUMNS_F2 = 76;

  709.         /** Columns number for Fm3 array. */
  710.         private static final int TOTAL_COLUMNS_FM3 = 49;

  711.         /** Depth of F2 array. */
  712.         private static final int DEPTH_F2 = 13;

  713.         /** Depth of Fm3 array. */
  714.         private static final int DEPTH_FM3 = 9;

  715.         /** Regular expression for supported file name. */
  716.         private String supportedName;

  717.         /** F2 coefficients used for the computation of the F2 layer critical frequency. */
  718.         private double[][][] f2Loader;

  719.         /** Fm3 coefficients used for the computation of the F2 layer maximum usable frequency factor. */
  720.         private double[][][] fm3Loader;

  721.         /**
  722.          * Build a new instance.
  723.          */
  724.         CCIRLoader() {
  725.             this.supportedName = DEFAULT_SUPPORTED_NAME;
  726.             this.f2Loader  = null;
  727.             this.fm3Loader = null;
  728.         }

  729.         /**
  730.          * Get the F2 coefficients used for the computation of the F2 layer critical frequency.
  731.          * @return the F2 coefficients
  732.          */
  733.         public double[][][] getF2() {
  734.             return f2Loader.clone();
  735.         }

  736.         /**
  737.          * Get the Fm3 coefficients used for the computation of the F2 layer maximum usable frequency factor.
  738.          * @return the F2 coefficients
  739.          */
  740.         public double[][][] getFm3() {
  741.             return fm3Loader.clone();
  742.         }

  743.         /** Load the data for a given month.
  744.          * @param dateComponents month given but its DateComponents
  745.          */
  746.         public void loadCCIRCoefficients(final DateComponents dateComponents) {

  747.             // The files are named ccirXX.asc where XX substitute the month of the year + 10
  748.             final int currentMonth = dateComponents.getMonth();
  749.             this.supportedName = NEQUICK_BASE + String.format("ccir%02d.asc", currentMonth + 10);
  750.             try (InputStream in = getStream(supportedName)) {
  751.                 loadData(in, supportedName);
  752.             } catch (IOException e) {
  753.                 throw new OrekitException(OrekitMessages.INTERNAL_ERROR, e);
  754.             }
  755.             // Throw an exception if F2 or Fm3 were not loaded properly
  756.             if (f2Loader == null || fm3Loader == null) {
  757.                 throw new OrekitException(OrekitMessages.NEQUICK_F2_FM3_NOT_LOADED, supportedName);
  758.             }

  759.         }

  760.         /**
  761.          * Load data from a stream.
  762.          * @param input input stream
  763.          * @param name name of the file
  764.          * @throws IOException if data can't be read
  765.          */
  766.         public void loadData(final InputStream input, final String name)
  767.             throws IOException {

  768.             // Initialize arrays
  769.             final double[][][] f2Temp  = new double[ROWS][TOTAL_COLUMNS_F2][DEPTH_F2];
  770.             final double[][][] fm3Temp = new double[ROWS][TOTAL_COLUMNS_FM3][DEPTH_FM3];

  771.             // Placeholders for parsed data
  772.             int    lineNumber       = 0;
  773.             int    index            = 0;
  774.             int    currentRowF2     = 0;
  775.             int    currentColumnF2  = 0;
  776.             int    currentDepthF2   = 0;
  777.             int    currentRowFm3    = 0;
  778.             int    currentColumnFm3 = 0;
  779.             int    currentDepthFm3  = 0;
  780.             String line             = null;

  781.             try (InputStreamReader isr = new InputStreamReader(input, StandardCharsets.UTF_8);
  782.                  BufferedReader    br = new BufferedReader(isr)) {

  783.                 for (line = br.readLine(); line != null; line = br.readLine()) {
  784.                     ++lineNumber;
  785.                     line = line.trim();

  786.                     // Read grid data
  787.                     if (!line.isEmpty()) {
  788.                         final String[] ccir_line = SEPARATOR.split(line);
  789.                         for (final String field : ccir_line) {

  790.                             if (index < NUMBER_F2_COEFFICIENTS) {
  791.                                 // Parse F2 coefficients
  792.                                 if (currentDepthF2 >= DEPTH_F2 && currentColumnF2 < (TOTAL_COLUMNS_F2 - 1)) {
  793.                                     currentDepthF2 = 0;
  794.                                     currentColumnF2++;
  795.                                 } else if (currentDepthF2 >= DEPTH_F2 && currentColumnF2 >= (TOTAL_COLUMNS_F2 - 1)) {
  796.                                     currentDepthF2 = 0;
  797.                                     currentColumnF2 = 0;
  798.                                     currentRowF2++;
  799.                                 }
  800.                                 f2Temp[currentRowF2][currentColumnF2][currentDepthF2++] = Double.parseDouble(field);
  801.                                 index++;
  802.                             } else {
  803.                                 // Parse Fm3 coefficients
  804.                                 if (currentDepthFm3 >= DEPTH_FM3 && currentColumnFm3 < (TOTAL_COLUMNS_FM3 - 1)) {
  805.                                     currentDepthFm3 = 0;
  806.                                     currentColumnFm3++;
  807.                                 } else if (currentDepthFm3 >= DEPTH_FM3 && currentColumnFm3 >= (TOTAL_COLUMNS_FM3 - 1)) {
  808.                                     currentDepthFm3 = 0;
  809.                                     currentColumnFm3 = 0;
  810.                                     currentRowFm3++;
  811.                                 }
  812.                                 fm3Temp[currentRowFm3][currentColumnFm3][currentDepthFm3++] = Double.parseDouble(field);
  813.                                 index++;
  814.                             }

  815.                         }
  816.                     }

  817.                 }

  818.             } catch (NumberFormatException nfe) {
  819.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  820.                                           lineNumber, name, line);
  821.             }

  822.             f2Loader  = f2Temp.clone();
  823.             fm3Loader = fm3Temp.clone();

  824.         }

  825.     }

  826.     /**
  827.      * Container for ray-perigee parameters.
  828.      * By convention, point 1 is at lower height.
  829.      */
  830.     private static class Ray {

  831.         /** Threshold for ray-perigee parameters computation. */
  832.         private static final double THRESHOLD = 1.0e-10;

  833.         /** Distance of the first point from the ray perigee [m]. */
  834.         private final double s1;

  835.         /** Distance of the second point from the ray perigee [m]. */
  836.         private final double s2;

  837.         /** Ray-perigee radius [m]. */
  838.         private final double rp;

  839.         /** Ray-perigee latitude [rad]. */
  840.         private final double latP;

  841.         /** Ray-perigee longitude [rad]. */
  842.         private final double lonP;

  843.         /** Sine and cosine of ray-perigee latitude. */
  844.         private final SinCos scLatP;

  845.         /** Sine of azimuth of satellite as seen from ray-perigee. */
  846.         private final double sinAzP;

  847.         /** Cosine of azimuth of satellite as seen from ray-perigee. */
  848.         private final double cosAzP;

  849.         /**
  850.          * Constructor.
  851.          * @param recP receiver position
  852.          * @param satP satellite position
  853.          */
  854.         Ray(final GeodeticPoint recP, final GeodeticPoint satP) {

  855.             // Integration limits in meters (Eq. 140 and 141)
  856.             final double r1 = RE + recP.getAltitude();
  857.             final double r2 = RE + satP.getAltitude();

  858.             // Useful parameters
  859.             final double lat1     = recP.getLatitude();
  860.             final double lat2     = satP.getLatitude();
  861.             final double lon1     = recP.getLongitude();
  862.             final double lon2     = satP.getLongitude();
  863.             final SinCos scLatSat = FastMath.sinCos(lat2);
  864.             final SinCos scLatRec = FastMath.sinCos(lat1);
  865.             final SinCos scLon21  = FastMath.sinCos(lon2 - lon1);

  866.             // Zenith angle computation (Eq. 153 to 155)
  867.             // with added protection against numerical noise near zenith observation
  868.             final double cosD = FastMath.min(1.0,
  869.                                              scLatRec.sin() * scLatSat.sin() +
  870.                                              scLatRec.cos() * scLatSat.cos() * scLon21.cos());
  871.             final double sinD = FastMath.sqrt(1.0 - cosD * cosD);
  872.             final double z    = FastMath.atan2(sinD, cosD - (r1 / r2));
  873.             final SinCos scZ  = FastMath.sinCos(z);

  874.             // Ray-perigee computation in meters (Eq. 156)
  875.             this.rp = r1 * scZ.sin();

  876.             // Ray-perigee latitude and longitude
  877.             if (FastMath.abs(FastMath.abs(lat1) - 0.5 * FastMath.PI) < THRESHOLD) {
  878.                 // receiver is almost at North or South pole

  879.                 // Ray-perigee latitude (Eq. 157)
  880.                 this.latP = FastMath.copySign(z, lat1);

  881.                 // Ray-perigee longitude (Eq. 164)
  882.                 if (z < 0) {
  883.                     this.lonP = lon2;
  884.                 } else {
  885.                     this.lonP = lon2 + FastMath.PI;
  886.                 }

  887.             } else if (FastMath.abs(scZ.sin()) < THRESHOLD) {
  888.                 // satellite is almost on receiver zenith

  889.                 this.latP = recP.getLatitude();
  890.                 this.lonP = recP.getLongitude();

  891.             } else {

  892.                 // Ray-perigee latitude (Eq. 158 to 163)
  893.                 final double sinAz    = scLon21.sin() * scLatSat.cos() / sinD;
  894.                 final double cosAz    = (scLatSat.sin() - cosD * scLatRec.sin()) / (sinD * scLatRec.cos());
  895.                 final double sinLatP  = scLatRec.sin() * scZ.sin() - scLatRec.cos() * scZ.cos() * cosAz;
  896.                 final double cosLatP  = FastMath.sqrt(1.0 - sinLatP * sinLatP);
  897.                 this.latP = FastMath.atan2(sinLatP, cosLatP);

  898.                 // Ray-perigee longitude (Eq. 165 to 167)
  899.                 final double sinLonP = -sinAz * scZ.cos() / cosLatP;
  900.                 final double cosLonP = (scZ.sin() - scLatRec.sin() * sinLatP) / (scLatRec.cos() * cosLatP);
  901.                 this.lonP = FastMath.atan2(sinLonP, cosLonP) + lon1;

  902.             }

  903.             // Sine and cosine of ray-perigee latitude
  904.             this.scLatP = FastMath.sinCos(latP);

  905.             if (FastMath.abs(FastMath.abs(latP) - 0.5 * FastMath.PI) < THRESHOLD ||
  906.                 FastMath.abs(scZ.sin()) < THRESHOLD) {
  907.                 // Eq. 172 and 173
  908.                 this.sinAzP = 0.0;
  909.                 this.cosAzP = -FastMath.copySign(1, latP);
  910.             } else {
  911.                 final SinCos scLon = FastMath.sinCos(lon2 - lonP);
  912.                 // Sine and cosine of azimuth of satellite as seen from ray-perigee
  913.                 final SinCos scPsi = FastMath.sinCos(greatCircleAngle(scLatSat, scLon));
  914.                 // Eq. 174 and 175
  915.                 this.sinAzP =  scLatSat.cos() * scLon.sin()                 /  scPsi.sin();
  916.                 this.cosAzP = (scLatSat.sin() - scLatP.sin() * scPsi.cos()) / (scLatP.cos() * scPsi.sin());
  917.             }

  918.             // Integration en points s1 and s2 in meters (Eq. 176 and 177)
  919.             this.s1 = FastMath.sqrt(r1 * r1 - rp * rp);
  920.             this.s2 = FastMath.sqrt(r2 * r2 - rp * rp);
  921.         }

  922.         /**
  923.          * Get the distance of the first point from the ray perigee.
  924.          * @return s1 in meters
  925.          */
  926.         public double getS1() {
  927.             return s1;
  928.         }

  929.         /**
  930.          * Get the distance of the second point from the ray perigee.
  931.          * @return s2 in meters
  932.          */
  933.         public double getS2() {
  934.             return s2;
  935.         }

  936.         /**
  937.          * Get the ray-perigee radius.
  938.          * @return the ray-perigee radius in meters
  939.          */
  940.         public double getRadius() {
  941.             return rp;
  942.         }

  943.         /**
  944.          * Get the ray-perigee latitude.
  945.          * @return the ray-perigee latitude in radians
  946.          */
  947.         public double getLatitude() {
  948.             return latP;
  949.         }

  950.         /**
  951.          * Get the ray-perigee longitude.
  952.          * @return the ray-perigee longitude in radians
  953.          */
  954.         public double getLongitude() {
  955.             return lonP;
  956.         }

  957.         /**
  958.          * Get the sine of azimuth of satellite as seen from ray-perigee.
  959.          * @return the sine of azimuth
  960.          */
  961.         public double getSineAz() {
  962.             return sinAzP;
  963.         }

  964.         /**
  965.          * Get the cosine of azimuth of satellite as seen from ray-perigee.
  966.          * @return the cosine of azimuth
  967.          */
  968.         public double getCosineAz() {
  969.             return cosAzP;
  970.         }

  971.         /**
  972.          * Compute the great circle angle from ray-perigee to satellite.
  973.          * <p>
  974.          * This method used the equations 168 to 171 pf the reference document.
  975.          * </p>
  976.          * @param scLat sine and cosine of satellite latitude
  977.          * @param scLon sine and cosine of satellite longitude minus receiver longitude
  978.          * @return the great circle angle in radians
  979.          */
  980.         private double greatCircleAngle(final SinCos scLat, final SinCos scLon) {
  981.             if (FastMath.abs(FastMath.abs(latP) - 0.5 * FastMath.PI) < THRESHOLD) {
  982.                 return FastMath.abs(FastMath.asin(scLat.sin()) - latP);
  983.             } else {
  984.                 final double cosPhi = scLatP.sin() * scLat.sin() +
  985.                                       scLatP.cos() * scLat.cos() * scLon.cos();
  986.                 final double sinPhi = FastMath.sqrt(1.0 - cosPhi * cosPhi);
  987.                 return FastMath.atan2(sinPhi, cosPhi);
  988.             }
  989.         }
  990.     }

  991.     /**
  992.      * Container for ray-perigee parameters.
  993.      * By convention, point 1 is at lower height.
  994.      */
  995.     private static class FieldRay <T extends CalculusFieldElement<T>> {

  996.         /** Threshold for ray-perigee parameters computation. */
  997.         private static final double THRESHOLD = 1.0e-10;

  998.         /** Distance of the first point from the ray perigee [m]. */
  999.         private final T s1;

  1000.         /** Distance of the second point from the ray perigee [m]. */
  1001.         private final T s2;

  1002.         /** Ray-perigee radius [m]. */
  1003.         private final T rp;

  1004.         /** Ray-perigee latitude [rad]. */
  1005.         private final T latP;

  1006.         /** Ray-perigee longitude [rad]. */
  1007.         private final T lonP;

  1008.         /** Sine and cosine of ray-perigee latitude. */
  1009.         private final FieldSinCos<T> scLatP;

  1010.         /** Sine of azimuth of satellite as seen from ray-perigee. */
  1011.         private final T sinAzP;

  1012.         /** Cosine of azimuth of satellite as seen from ray-perigee. */
  1013.         private final T cosAzP;

  1014.         /**
  1015.          * Constructor.
  1016.          * @param field field of the elements
  1017.          * @param recP receiver position
  1018.          * @param satP satellite position
  1019.          */
  1020.         FieldRay(final Field<T> field, final FieldGeodeticPoint<T> recP, final FieldGeodeticPoint<T> satP) {

  1021.             // Integration limits in meters (Eq. 140 and 141)
  1022.             final T r1 = recP.getAltitude().add(RE);
  1023.             final T r2 = satP.getAltitude().add(RE);

  1024.             // Useful parameters
  1025.             final T pi   = r1.getPi();
  1026.             final T lat1 = recP.getLatitude();
  1027.             final T lat2 = satP.getLatitude();
  1028.             final T lon1 = recP.getLongitude();
  1029.             final T lon2 = satP.getLongitude();
  1030.             final FieldSinCos<T> scLatSat = FastMath.sinCos(lat2);
  1031.             final FieldSinCos<T> scLatRec = FastMath.sinCos(lat1);
  1032.             final FieldSinCos<T> scLon21  = FastMath.sinCos(lon2.subtract(lon1));

  1033.             // Zenith angle computation (Eq. 153 to 155)
  1034.             final T cosD = scLatRec.sin().multiply(scLatSat.sin()).
  1035.                             add(scLatRec.cos().multiply(scLatSat.cos()).multiply(scLon21.cos()));
  1036.             final T sinD = FastMath.sqrt(cosD.multiply(cosD).negate().add(1.0));
  1037.             final T z = FastMath.atan2(sinD, cosD.subtract(r1.divide(r2)));
  1038.             final FieldSinCos<T> scZ  = FastMath.sinCos(z);

  1039.             // Ray-perigee computation in meters (Eq. 156)
  1040.             this.rp = r1.multiply(scZ.sin());

  1041.             // Ray-perigee latitude and longitude
  1042.             if (FastMath.abs(FastMath.abs(lat1).subtract(pi.multiply(0.5)).getReal()) < THRESHOLD) {

  1043.                 // Ray-perigee latitude (Eq. 157)
  1044.                 this.latP = FastMath.copySign(z, lat1);

  1045.                 // Ray-perigee longitude (Eq. 164)
  1046.                 if (z.getReal() < 0) {
  1047.                     this.lonP = lon2;
  1048.                 } else {
  1049.                     this.lonP = lon2.add(pi);
  1050.                 }

  1051.             } else if (FastMath.abs(scZ.sin().getReal()) < THRESHOLD) {
  1052.                 // satellite is almost on receiver zenith

  1053.                 this.latP = recP.getLatitude();
  1054.                 this.lonP = recP.getLongitude();

  1055.             } else {

  1056.                 // Ray-perigee latitude (Eq. 158 to 163)
  1057.                 final T sinAz    = FastMath.sin(lon2.subtract(lon1)).multiply(scLatSat.cos()).divide(sinD);
  1058.                 final T cosAz    = scLatSat.sin().subtract(cosD.multiply(scLatRec.sin())).divide(sinD.multiply(scLatRec.cos()));
  1059.                 final T sinLatP  = scLatRec.sin().multiply(scZ.sin()).subtract(scLatRec.cos().multiply(scZ.cos()).multiply(cosAz));
  1060.                 final T cosLatP  = FastMath.sqrt(sinLatP.multiply(sinLatP).negate().add(1.0));
  1061.                 this.latP = FastMath.atan2(sinLatP, cosLatP);

  1062.                 // Ray-perigee longitude (Eq. 165 to 167)
  1063.                 final T sinLonP = sinAz.negate().multiply(scZ.cos()).divide(cosLatP);
  1064.                 final T cosLonP = scZ.sin().subtract(scLatRec.sin().multiply(sinLatP)).divide(scLatRec.cos().multiply(cosLatP));
  1065.                 this.lonP = FastMath.atan2(sinLonP, cosLonP).add(lon1);

  1066.             }

  1067.             // Sine and cosine of ray-perigee latitude
  1068.             this.scLatP = FastMath.sinCos(latP);

  1069.             if (FastMath.abs(FastMath.abs(latP).subtract(pi.multiply(0.5)).getReal()) < THRESHOLD ||
  1070.                 FastMath.abs(scZ.sin().getReal()) < THRESHOLD) {
  1071.                 // Eq. 172 and 173
  1072.                 this.sinAzP = field.getZero();
  1073.                 this.cosAzP = FastMath.copySign(field.getOne(), latP).negate();
  1074.             } else {
  1075.                 final FieldSinCos<T> scLon = FastMath.sinCos(lon2.subtract(lonP));
  1076.                 // Sine and cosine of azimuth of satellite as seen from ray-perigee
  1077.                 final FieldSinCos<T> scPsi = FastMath.sinCos(greatCircleAngle(scLatSat, scLon));
  1078.                 // Eq. 174 and 175
  1079.                 this.sinAzP = scLatSat.cos().multiply(scLon.sin()).divide(scPsi.sin());
  1080.                 this.cosAzP = scLatSat.sin().subtract(scLatP.sin().multiply(scPsi.cos())).divide(scLatP.cos().multiply(scPsi.sin()));
  1081.             }

  1082.             // Integration en points s1 and s2 in meters (Eq. 176 and 177)
  1083.             this.s1 = FastMath.sqrt(r1.multiply(r1).subtract(rp.multiply(rp)));
  1084.             this.s2 = FastMath.sqrt(r2.multiply(r2).subtract(rp.multiply(rp)));
  1085.         }

  1086.         /**
  1087.          * Get the distance of the first point from the ray perigee.
  1088.          * @return s1 in meters
  1089.          */
  1090.         public T getS1() {
  1091.             return s1;
  1092.         }

  1093.         /**
  1094.          * Get the distance of the second point from the ray perigee.
  1095.          * @return s2 in meters
  1096.          */
  1097.         public T getS2() {
  1098.             return s2;
  1099.         }

  1100.         /**
  1101.          * Get the ray-perigee radius.
  1102.          * @return the ray-perigee radius in meters
  1103.          */
  1104.         public T getRadius() {
  1105.             return rp;
  1106.         }

  1107.         /**
  1108.          * Get the ray-perigee latitude.
  1109.          * @return the ray-perigee latitude in radians
  1110.          */
  1111.         public T getLatitude() {
  1112.             return latP;
  1113.         }

  1114.         /**
  1115.          * Get the ray-perigee longitude.
  1116.          * @return the ray-perigee longitude in radians
  1117.          */
  1118.         public T getLongitude() {
  1119.             return lonP;
  1120.         }

  1121.         /**
  1122.          * Get the sine of azimuth of satellite as seen from ray-perigee.
  1123.          * @return the sine of azimuth
  1124.          */
  1125.         public T getSineAz() {
  1126.             return sinAzP;
  1127.         }

  1128.         /**
  1129.          * Get the cosine of azimuth of satellite as seen from ray-perigee.
  1130.          * @return the cosine of azimuth
  1131.          */
  1132.         public T getCosineAz() {
  1133.             return cosAzP;
  1134.         }

  1135.         /**
  1136.          * Compute the great circle angle from ray-perigee to satellite.
  1137.          * <p>
  1138.          * This method used the equations 168 to 171 pf the reference document.
  1139.          * </p>
  1140.          * @param scLat sine and cosine of satellite latitude
  1141.          * @param scLon sine and cosine of satellite longitude minus receiver longitude
  1142.          * @return the great circle angle in radians
  1143.          */
  1144.         private T greatCircleAngle(final FieldSinCos<T> scLat, final FieldSinCos<T> scLon) {
  1145.             if (FastMath.abs(FastMath.abs(latP).getReal() - 0.5 * FastMath.PI) < THRESHOLD) {
  1146.                 return FastMath.abs(FastMath.asin(scLat.sin()).subtract(latP));
  1147.             } else {
  1148.                 final T cosPhi = scLatP.sin().multiply(scLat.sin()).add(
  1149.                                  scLatP.cos().multiply(scLat.cos()).multiply(scLon.cos()));
  1150.                 final T sinPhi = FastMath.sqrt(cosPhi.multiply(cosPhi).negate().add(1.0));
  1151.                 return FastMath.atan2(sinPhi, cosPhi);
  1152.             }
  1153.         }
  1154.     }

  1155.     /** Performs the computation of the coordinates along the integration path. */
  1156.     private static class Segment {

  1157.         /** Threshold for zenith segment. */
  1158.         private static final double THRESHOLD = 1.0e-3;

  1159.         /** Latitudes [rad]. */
  1160.         private final double[] latitudes;

  1161.         /** Longitudes [rad]. */
  1162.         private final double[] longitudes;

  1163.         /** Heights [m]. */
  1164.         private final double[] heights;

  1165.         /** Integration step [m]. */
  1166.         private final double deltaN;

  1167.         /**
  1168.          * Constructor.
  1169.          * @param n number of points used for the integration
  1170.          * @param ray ray-perigee parameters
  1171.          */
  1172.         Segment(final int n, final Ray ray) {
  1173.             // Integration en points
  1174.             final double s1 = ray.getS1();
  1175.             final double s2 = ray.getS2();

  1176.             // Integration step (Eq. 195)
  1177.             this.deltaN = (s2 - s1) / n;

  1178.             // Segments
  1179.             final double[] s = getSegments(n, s1);

  1180.             // Useful parameters
  1181.             final double rp = ray.getRadius();
  1182.             final SinCos scLatP = FastMath.sinCos(ray.getLatitude());

  1183.             // Geodetic coordinates
  1184.             final int size = s.length;
  1185.             heights    = new double[size];
  1186.             latitudes  = new double[size];
  1187.             longitudes = new double[size];
  1188.             for (int i = 0; i < size; i++) {
  1189.                 // Heights (Eq. 178)
  1190.                 heights[i] = FastMath.sqrt(s[i] * s[i] + rp * rp) - RE;

  1191.                 if (rp < THRESHOLD) {
  1192.                     // zenith segment
  1193.                     latitudes[i]  = ray.getLatitude();
  1194.                     longitudes[i] = ray.getLongitude();
  1195.                 } else {
  1196.                     // Great circle parameters (Eq. 179 to 181)
  1197.                     final double tanDs = s[i] / rp;
  1198.                     final double cosDs = 1.0 / FastMath.sqrt(1.0 + tanDs * tanDs);
  1199.                     final double sinDs = tanDs * cosDs;

  1200.                     // Latitude (Eq. 182 to 183)
  1201.                     final double sinLatS = scLatP.sin() * cosDs + scLatP.cos() * sinDs * ray.getCosineAz();
  1202.                     final double cosLatS = FastMath.sqrt(1.0 - sinLatS * sinLatS);
  1203.                     latitudes[i] = FastMath.atan2(sinLatS, cosLatS);

  1204.                     // Longitude (Eq. 184 to 187)
  1205.                     final double sinLonS = sinDs * ray.getSineAz() * scLatP.cos();
  1206.                     final double cosLonS = cosDs - scLatP.sin() * sinLatS;
  1207.                     longitudes[i] = FastMath.atan2(sinLonS, cosLonS) + ray.getLongitude();
  1208.                 }
  1209.             }
  1210.         }

  1211.         /**
  1212.          * Computes the distance of a point from the ray-perigee.
  1213.          * @param n number of points used for the integration
  1214.          * @param s1 lower boundary
  1215.          * @return the distance of a point from the ray-perigee in km
  1216.          */
  1217.         private double[] getSegments(final int n, final double s1) {
  1218.             // Eq. 196
  1219.             final double g = 0.5773502691896 * deltaN;
  1220.             // Eq. 197
  1221.             final double y = s1 + (deltaN - g) * 0.5;
  1222.             final double[] segments = new double[2 * n];
  1223.             int index = 0;
  1224.             for (int i = 0; i < n; i++) {
  1225.                 // Eq. 198
  1226.                 segments[index] = y + i * deltaN;
  1227.                 index++;
  1228.                 segments[index] = y + i * deltaN + g;
  1229.                 index++;
  1230.             }
  1231.             return segments;
  1232.         }

  1233.         /**
  1234.          * Get the latitudes of the coordinates along the integration path.
  1235.          * @return the latitudes in radians
  1236.          */
  1237.         public double[] getLatitudes() {
  1238.             return latitudes;
  1239.         }

  1240.         /**
  1241.          * Get the longitudes of the coordinates along the integration path.
  1242.          * @return the longitudes in radians
  1243.          */
  1244.         public double[] getLongitudes() {
  1245.             return longitudes;
  1246.         }

  1247.         /**
  1248.          * Get the heights of the coordinates along the integration path.
  1249.          * @return the heights in m
  1250.          */
  1251.         public double[] getHeights() {
  1252.             return heights;
  1253.         }

  1254.         /**
  1255.          * Get the integration step.
  1256.          * @return the integration step in meters
  1257.          */
  1258.         public double getInterval() {
  1259.             return deltaN;
  1260.         }
  1261.     }

  1262.     /** Performs the computation of the coordinates along the integration path. */
  1263.     private static class FieldSegment <T extends CalculusFieldElement<T>> {

  1264.         /** Threshold for zenith segment. */
  1265.         private static final double THRESHOLD = 1.0e-3;

  1266.         /** Latitudes [rad]. */
  1267.         private final T[] latitudes;

  1268.         /** Longitudes [rad]. */
  1269.         private final T[] longitudes;

  1270.         /** Heights [m]. */
  1271.         private final T[] heights;

  1272.         /** Integration step [m]. */
  1273.         private final T deltaN;

  1274.         /**
  1275.          * Constructor.
  1276.          * @param field field of the elements
  1277.          * @param n number of points used for the integration
  1278.          * @param ray ray-perigee parameters
  1279.          */
  1280.         FieldSegment(final Field<T> field, final int n, final FieldRay<T> ray) {
  1281.             // Integration en points
  1282.             final T s1 = ray.getS1();
  1283.             final T s2 = ray.getS2();

  1284.             // Integration step (Eq. 195)
  1285.             this.deltaN = s2.subtract(s1).divide(n);

  1286.             // Segments
  1287.             final T[] s = getSegments(field, n, s1);

  1288.             // Useful parameters
  1289.             final T rp = ray.getRadius();
  1290.             final FieldSinCos<T> scLatP = FastMath.sinCos(ray.getLatitude());

  1291.             // Geodetic coordinates
  1292.             final int size = s.length;
  1293.             heights    = MathArrays.buildArray(field, size);
  1294.             latitudes  = MathArrays.buildArray(field, size);
  1295.             longitudes = MathArrays.buildArray(field, size);
  1296.             for (int i = 0; i < size; i++) {
  1297.                 // Heights (Eq. 178)
  1298.                 heights[i] = FastMath.sqrt(s[i].multiply(s[i]).add(rp.multiply(rp))).subtract(RE);

  1299.                 if (rp.getReal() < THRESHOLD) {
  1300.                     // zenith segment
  1301.                     latitudes[i]  = ray.getLatitude();
  1302.                     longitudes[i] = ray.getLongitude();
  1303.                 } else {
  1304.                     // Great circle parameters (Eq. 179 to 181)
  1305.                     final T tanDs = s[i].divide(rp);
  1306.                     final T cosDs = FastMath.sqrt(tanDs.multiply(tanDs).add(1.0)).reciprocal();
  1307.                     final T sinDs = tanDs.multiply(cosDs);

  1308.                     // Latitude (Eq. 182 to 183)
  1309.                     final T sinLatS = scLatP.sin().multiply(cosDs).add(scLatP.cos().multiply(sinDs).multiply(ray.getCosineAz()));
  1310.                     final T cosLatS = FastMath.sqrt(sinLatS.multiply(sinLatS).negate().add(1.0));
  1311.                     latitudes[i] = FastMath.atan2(sinLatS, cosLatS);

  1312.                     // Longitude (Eq. 184 to 187)
  1313.                     final T sinLonS = sinDs.multiply(ray.getSineAz()).multiply(scLatP.cos());
  1314.                     final T cosLonS = cosDs.subtract(scLatP.sin().multiply(sinLatS));
  1315.                     longitudes[i] = FastMath.atan2(sinLonS, cosLonS).add(ray.getLongitude());
  1316.                 }
  1317.             }
  1318.         }

  1319.         /**
  1320.          * Computes the distance of a point from the ray-perigee.
  1321.          * @param field field of the elements
  1322.          * @param n number of points used for the integration
  1323.          * @param s1 lower boundary
  1324.          * @return the distance of a point from the ray-perigee in km
  1325.          */
  1326.         private T[] getSegments(final Field<T> field, final int n, final T s1) {
  1327.             // Eq. 196
  1328.             final T g = deltaN.multiply(0.5773502691896);
  1329.             // Eq. 197
  1330.             final T y = s1.add(deltaN.subtract(g).multiply(0.5));
  1331.             final T[] segments = MathArrays.buildArray(field, 2 * n);
  1332.             int index = 0;
  1333.             for (int i = 0; i < n; i++) {
  1334.                 // Eq. 198
  1335.                 segments[index] = y.add(deltaN.multiply(i));
  1336.                 index++;
  1337.                 segments[index] = y.add(deltaN.multiply(i)).add(g);
  1338.                 index++;
  1339.             }
  1340.             return segments;
  1341.         }

  1342.         /**
  1343.          * Get the latitudes of the coordinates along the integration path.
  1344.          * @return the latitudes in radians
  1345.          */
  1346.         public T[] getLatitudes() {
  1347.             return latitudes;
  1348.         }

  1349.         /**
  1350.          * Get the longitudes of the coordinates along the integration path.
  1351.          * @return the longitudes in radians
  1352.          */
  1353.         public T[] getLongitudes() {
  1354.             return longitudes;
  1355.         }

  1356.         /**
  1357.          * Get the heights of the coordinates along the integration path.
  1358.          * @return the heights in m
  1359.          */
  1360.         public T[] getHeights() {
  1361.             return heights;
  1362.         }

  1363.         /**
  1364.          * Get the integration step.
  1365.          * @return the integration step in meters
  1366.          */
  1367.         public T getInterval() {
  1368.             return deltaN;
  1369.         }
  1370.     }

  1371. }