SsrVtecIonosphericModel.java

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

  18. import java.util.Collections;
  19. import java.util.List;

  20. import org.hipparchus.CalculusFieldElement;
  21. import org.hipparchus.Field;
  22. import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
  23. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  24. import org.hipparchus.util.FastMath;
  25. import org.hipparchus.util.FieldSinCos;
  26. import org.hipparchus.util.MathUtils;
  27. import org.hipparchus.util.SinCos;
  28. import org.orekit.bodies.FieldGeodeticPoint;
  29. import org.orekit.bodies.GeodeticPoint;
  30. import org.orekit.frames.TopocentricFrame;
  31. import org.orekit.gnss.metric.messages.ssr.subtype.SsrIm201;
  32. import org.orekit.gnss.metric.messages.ssr.subtype.SsrIm201Data;
  33. import org.orekit.gnss.metric.messages.ssr.subtype.SsrIm201Header;
  34. import org.orekit.propagation.FieldSpacecraftState;
  35. import org.orekit.propagation.SpacecraftState;
  36. import org.orekit.utils.Constants;
  37. import org.orekit.utils.FieldLegendrePolynomials;
  38. import org.orekit.utils.LegendrePolynomials;
  39. import org.orekit.utils.ParameterDriver;

  40. /**
  41.  * Ionospheric model based on SSR IM201 message.
  42.  * <p>
  43.  * Within this message, the ionospheric VTEC is provided
  44.  * using spherical harmonic expansions. For a given ionospheric
  45.  * layer, the slant TEC value is calculated using the satellite
  46.  * elevation and the height of the corresponding layer. The
  47.  * total slant TEC is computed by the sum of the individual slant
  48.  * TEC for each layer.
  49.  * </p>
  50.  * @author Bryan Cazabonne
  51.  * @since 11.0
  52.  * @see "IGS State Space Representation (SSR) Format, Version 1.00, October 2020."
  53.  */
  54. public class SsrVtecIonosphericModel implements IonosphericModel {

  55.     /** Earth radius in meters (see reference). */
  56.     private static final double EARTH_RADIUS = 6370000.0;

  57.     /** Multiplication factor for path delay computation. */
  58.     private static final double FACTOR = 40.3e16;

  59.     /** SSR Ionosphere VTEC Spherical Harmonics Message.. */
  60.     private final transient SsrIm201 vtecMessage;

  61.     /**
  62.      * Constructor.
  63.      * @param vtecMessage SSR Ionosphere VTEC Spherical Harmonics Message.
  64.      */
  65.     public SsrVtecIonosphericModel(final SsrIm201 vtecMessage) {
  66.         this.vtecMessage = vtecMessage;
  67.     }

  68.     /** {@inheritDoc} */
  69.     @Override
  70.     public double pathDelay(final SpacecraftState state, final TopocentricFrame baseFrame,
  71.                             final double frequency, final double[] parameters) {

  72.         // Elevation in radians
  73.         final Vector3D position  = state.getPosition(baseFrame);
  74.         final double   elevation = position.getDelta();

  75.         // Only consider measures above the horizon
  76.         if (elevation > 0.0) {

  77.             // Azimuth angle in radians
  78.             double azimuth = FastMath.atan2(position.getX(), position.getY());
  79.             if (azimuth < 0.) {
  80.                 azimuth += MathUtils.TWO_PI;
  81.             }

  82.             // Initialize slant TEC
  83.             double stec = 0.0;

  84.             // Message header
  85.             final SsrIm201Header header = vtecMessage.getHeader();

  86.             // Loop on ionospheric layers
  87.             for (final SsrIm201Data data : vtecMessage.getData()) {
  88.                 stec += stecIonosphericLayer(data, header, elevation, azimuth, baseFrame.getPoint());
  89.             }

  90.             // Return the path delay
  91.             return FACTOR * stec / (frequency * frequency);

  92.         }

  93.         // Delay is equal to 0.0
  94.         return 0.0;

  95.     }

  96.     /** {@inheritDoc} */
  97.     @Override
  98.     public <T extends CalculusFieldElement<T>> T pathDelay(final FieldSpacecraftState<T> state, final TopocentricFrame baseFrame,
  99.                                                        final double frequency, final T[] parameters) {

  100.         // Field
  101.         final Field<T> field = state.getDate().getField();

  102.         // Elevation in radians
  103.         final FieldVector3D<T> position  = state.getPosition(baseFrame);
  104.         final T                elevation = position.getDelta();

  105.         // Only consider measures above the horizon
  106.         if (elevation.getReal() > 0.0) {

  107.             // Azimuth angle in radians
  108.             T azimuth = FastMath.atan2(position.getX(), position.getY());
  109.             if (azimuth.getReal() < 0.) {
  110.                 azimuth = azimuth.add(MathUtils.TWO_PI);
  111.             }

  112.             // Initialize slant TEC
  113.             T stec = field.getZero();

  114.             // Message header
  115.             final SsrIm201Header header = vtecMessage.getHeader();

  116.             // Loop on ionospheric layers
  117.             for (SsrIm201Data data : vtecMessage.getData()) {
  118.                 stec = stec.add(stecIonosphericLayer(data, header, elevation, azimuth, baseFrame.getPoint(field)));
  119.             }

  120.             // Return the path delay
  121.             return stec.multiply(FACTOR).divide(frequency * frequency);

  122.         }

  123.         // Delay is equal to 0.0
  124.         return field.getZero();

  125.     }

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

  131.     /**
  132.      * Calculates the slant TEC for a given ionospheric layer.
  133.      * @param im201Data ionospheric data for the current layer
  134.      * @param im201Header container for data contained in the header
  135.      * @param elevation satellite elevation angle [rad]
  136.      * @param azimuth satellite azimuth angle [rad]
  137.      * @param point geodetic point
  138.      * @return the slant TEC for the current ionospheric layer
  139.      */
  140.     private static double stecIonosphericLayer(final SsrIm201Data im201Data, final SsrIm201Header im201Header,
  141.                                                final double elevation, final double azimuth,
  142.                                                final GeodeticPoint point) {

  143.         // Geodetic point data
  144.         final double phiR    = point.getLatitude();
  145.         final double lambdaR = point.getLongitude();
  146.         final double hR      = point.getAltitude();

  147.         // Data contained in the message
  148.         final double     hI     = im201Data.getHeightIonosphericLayer();
  149.         final int        degree = im201Data.getSphericalHarmonicsDegree();
  150.         final int        order  = im201Data.getSphericalHarmonicsOrder();
  151.         final double[][] cnm    = im201Data.getCnm();
  152.         final double[][] snm    = im201Data.getSnm();

  153.         // Spherical Earth's central angle
  154.         final double psiPP = calculatePsi(hR, hI, elevation);

  155.         // Sine and cosine of useful angles
  156.         final SinCos scA    = FastMath.sinCos(azimuth);
  157.         final SinCos scPhiR = FastMath.sinCos(phiR);
  158.         final SinCos scPsi  = FastMath.sinCos(psiPP);

  159.         // Pierce point latitude and longitude
  160.         final double phiPP    = calculatePiercePointLatitude(scPhiR, scPsi, scA);
  161.         final double lambdaPP = calculatePiercePointLongitude(scA, phiPP, psiPP, phiR, lambdaR);

  162.         // Mean sun fixed longitude (modulo 2pi)
  163.         final double lambdaS = calculateSunLongitude(im201Header, lambdaPP);

  164.         // VTEC
  165.         // According to the documentation, negative VTEC values must be ignored and shall be replaced by 0.0
  166.         final double vtec = FastMath.max(0.0, calculateVTEC(degree, order, cnm, snm, phiPP, lambdaS));

  167.         // Return STEC for the current ionospheric layer
  168.         return vtec / FastMath.sin(elevation + psiPP);

  169.     }

  170.     /**
  171.      * Calculates the slant TEC for a given ionospheric layer.
  172.      * @param im201Data ionospheric data for the current layer
  173.      * @param im201Header container for data contained in the header
  174.      * @param elevation satellite elevation angle [rad]
  175.      * @param azimuth satellite azimuth angle [rad]
  176.      * @param point geodetic point
  177.      * @param <T> type of the elements
  178.      * @return the slant TEC for the current ionospheric layer
  179.      */
  180.     private static <T extends CalculusFieldElement<T>> T stecIonosphericLayer(final SsrIm201Data im201Data, final SsrIm201Header im201Header,
  181.                                                                           final T elevation, final T azimuth,
  182.                                                                           final FieldGeodeticPoint<T> point) {

  183.         // Geodetic point data
  184.         final T phiR    = point.getLatitude();
  185.         final T lambdaR = point.getLongitude();
  186.         final T hR      = point.getAltitude();

  187.         // Data contained in the message
  188.         final double     hI     = im201Data.getHeightIonosphericLayer();
  189.         final int        degree = im201Data.getSphericalHarmonicsDegree();
  190.         final int        order  = im201Data.getSphericalHarmonicsOrder();
  191.         final double[][] cnm    = im201Data.getCnm();
  192.         final double[][] snm    = im201Data.getSnm();

  193.         // Spherical Earth's central angle
  194.         final T psiPP = calculatePsi(hR, hI, elevation);

  195.         // Sine and cosine of useful angles
  196.         final FieldSinCos<T> scA    = FastMath.sinCos(azimuth);
  197.         final FieldSinCos<T> scPhiR = FastMath.sinCos(phiR);
  198.         final FieldSinCos<T> scPsi  = FastMath.sinCos(psiPP);

  199.         // Pierce point latitude and longitude
  200.         final T phiPP    = calculatePiercePointLatitude(scPhiR, scPsi, scA);
  201.         final T lambdaPP = calculatePiercePointLongitude(scA, phiPP, psiPP, phiR, lambdaR);

  202.         // Mean sun fixed longitude (modulo 2pi)
  203.         final T lambdaS = calculateSunLongitude(im201Header, lambdaPP);

  204.         // VTEC
  205.         // According to the documentation, negative VTEC values must be ignored and shall be replaced by 0.0
  206.         final T vtec = FastMath.max(phiR.getField().getZero(), calculateVTEC(degree, order, cnm, snm, phiPP, lambdaS));

  207.         // Return STEC for the current ionospheric layer
  208.         return vtec.divide(FastMath.sin(elevation.add(psiPP)));

  209.     }

  210.     /**
  211.      * Calculates the spherical Earth’s central angle between station position and
  212.      * the projection of the pierce point to the spherical Earth’s surface.
  213.      * @param hR height of station position in meters
  214.      * @param hI height of ionospheric layer in meters
  215.      * @param elevation satellite elevation angle in radians
  216.      * @return the spherical Earth’s central angle in radians
  217.      */
  218.     private static double calculatePsi(final double hR, final double hI,
  219.                                        final double elevation) {
  220.         final double ratio = (EARTH_RADIUS + hR) / (EARTH_RADIUS + hI);
  221.         return MathUtils.SEMI_PI - elevation - FastMath.asin(ratio * FastMath.cos(elevation));
  222.     }

  223.     /**
  224.      * Calculates the spherical Earth’s central angle between station position and
  225.      * the projection of the pierce point to the spherical Earth’s surface.
  226.      * @param hR height of station position in meters
  227.      * @param hI height of ionospheric layer in meters
  228.      * @param elevation satellite elevation angle in radians
  229.      * @param <T> type of the elements
  230.      * @return the spherical Earth’s central angle in radians
  231.      */
  232.     private static <T extends CalculusFieldElement<T>> T calculatePsi(final T hR, final double hI,
  233.                                                                   final T elevation) {
  234.         final T ratio = hR.add(EARTH_RADIUS).divide(EARTH_RADIUS + hI);
  235.         return hR.getPi().multiply(0.5).subtract(elevation).subtract(FastMath.asin(ratio.multiply(FastMath.cos(elevation))));
  236.     }

  237.     /**
  238.      * Calculates the latitude of the pierce point in the spherical Earth model.
  239.      * @param scPhiR sine and cosine of the geocentric latitude of the station
  240.      * @param scPsi sine and cosine of the spherical Earth's central angle
  241.      * @param scA sine and cosine of the azimuth angle
  242.      * @return the latitude of the pierce point in the spherical Earth model in radians
  243.      */
  244.     private static double calculatePiercePointLatitude(final SinCos scPhiR, final SinCos scPsi, final SinCos scA) {
  245.         return FastMath.asin(scPhiR.sin() * scPsi.cos() + scPhiR.cos() * scPsi.sin() * scA.cos());
  246.     }

  247.     /**
  248.      * Calculates the latitude of the pierce point in the spherical Earth model.
  249.      * @param scPhiR sine and cosine of the geocentric latitude of the station
  250.      * @param scPsi sine and cosine of the spherical Earth's central angle
  251.      * @param scA sine and cosine of the azimuth angle
  252.      * @param <T> type of the elements
  253.      * @return the latitude of the pierce point in the spherical Earth model in radians
  254.      */
  255.     private static <T extends CalculusFieldElement<T>> T calculatePiercePointLatitude(final FieldSinCos<T> scPhiR,
  256.                                                                                   final FieldSinCos<T> scPsi,
  257.                                                                                   final FieldSinCos<T> scA) {
  258.         return FastMath.asin(scPhiR.sin().multiply(scPsi.cos()).add(scPhiR.cos().multiply(scPsi.sin()).multiply(scA.cos())));
  259.     }

  260.     /**
  261.      * Calculates the longitude of the pierce point in the spherical Earth model.
  262.      * @param scA sine and cosine of the azimuth angle
  263.      * @param phiPP the latitude of the pierce point in the spherical Earth model in radians
  264.      * @param psiPP the spherical Earth’s central angle in radians
  265.      * @param phiR the geocentric latitude of the station in radians
  266.      * @param lambdaR the geocentric longitude of the station
  267.      * @return the longitude of the pierce point in the spherical Earth model in radians
  268.      */
  269.     private static double calculatePiercePointLongitude(final SinCos scA,
  270.                                                         final double phiPP, final double psiPP,
  271.                                                         final double phiR, final double lambdaR) {

  272.         // arcSin(sin(PsiPP) * sin(Azimuth) / cos(PhiPP))
  273.         final double arcSin = FastMath.asin(FastMath.sin(psiPP) * scA.sin() / FastMath.cos(phiPP));

  274.         // Return
  275.         return verifyCondition(scA.cos(), psiPP, phiR) ? lambdaR + FastMath.PI - arcSin : lambdaR + arcSin;

  276.     }

  277.     /**
  278.      * Calculates the longitude of the pierce point in the spherical Earth model.
  279.      * @param scA sine and cosine of the azimuth angle
  280.      * @param phiPP the latitude of the pierce point in the spherical Earth model in radians
  281.      * @param psiPP the spherical Earth’s central angle in radians
  282.      * @param phiR the geocentric latitude of the station in radians
  283.      * @param lambdaR the geocentric longitude of the station
  284.      * @param <T> type of the elements
  285.      * @return the longitude of the pierce point in the spherical Earth model in radians
  286.      */
  287.     private static <T extends CalculusFieldElement<T>> T calculatePiercePointLongitude(final FieldSinCos<T> scA,
  288.                                                                                    final T phiPP, final T psiPP,
  289.                                                                                    final T phiR, final T lambdaR) {

  290.         // arcSin(sin(PsiPP) * sin(Azimuth) / cos(PhiPP))
  291.         final T arcSin = FastMath.asin(FastMath.sin(psiPP).multiply(scA.sin()).divide(FastMath.cos(phiPP)));

  292.         // Return
  293.         return verifyCondition(scA.cos().getReal(), psiPP.getReal(), phiR.getReal()) ?
  294.                                                lambdaR.add(arcSin.getPi()).subtract(arcSin) : lambdaR.add(arcSin);

  295.     }

  296.     /**
  297.      * Calculate the mean sun fixed longitude phase.
  298.      * @param im201Header header of the IM201 message
  299.      * @param lambdaPP the longitude of the pierce point in the spherical Earth model in radians
  300.      * @return the mean sun fixed longitude phase in radians
  301.      */
  302.     private static double calculateSunLongitude(final SsrIm201Header im201Header, final double lambdaPP) {
  303.         final double t = getTime(im201Header);
  304.         return MathUtils.normalizeAngle(lambdaPP + (t - 50400.0) * FastMath.PI / 43200.0, FastMath.PI);
  305.     }

  306.     /**
  307.      * Calculate the mean sun fixed longitude phase.
  308.      * @param im201Header header of the IM201 message
  309.      * @param lambdaPP the longitude of the pierce point in the spherical Earth model in radians
  310.      * @param <T> type of the elements
  311.      * @return the mean sun fixed longitude phase in radians
  312.      */
  313.     private static <T extends CalculusFieldElement<T>> T calculateSunLongitude(final SsrIm201Header im201Header, final T lambdaPP) {
  314.         final double t = getTime(im201Header);
  315.         return MathUtils.normalizeAngle(lambdaPP.add(lambdaPP.getPi().multiply(t - 50400.0).divide(43200.0)), lambdaPP.getPi());
  316.     }

  317.     /**
  318.      * Calculate the VTEC contribution for a given ionospheric layer.
  319.      * @param degree degree of spherical expansion
  320.      * @param order order of spherical expansion
  321.      * @param cnm cosine coefficients for the layer in TECU
  322.      * @param snm sine coefficients for the layer in TECU
  323.      * @param phiPP geocentric latitude of ionospheric pierce point for the layer in radians
  324.      * @param lambdaS mean sun fixed and phase shifted longitude of ionospheric pierce point
  325.      * @return the VTEC contribution for the current ionospheric layer in TECU
  326.      */
  327.     private static double calculateVTEC(final int degree, final int order,
  328.                                         final double[][] cnm, final double[][] snm,
  329.                                         final double phiPP, final double lambdaS) {

  330.         // Initialize VTEC value
  331.         double vtec = 0.0;

  332.         // Compute Legendre Polynomials Pnm(sin(phiPP))
  333.         final LegendrePolynomials p = new LegendrePolynomials(degree, order, FastMath.sin(phiPP));

  334.         // Calculate VTEC
  335.         for (int n = 0; n <= degree; n++) {

  336.             for (int m = 0; m <= FastMath.min(n, order); m++) {

  337.                 // Legendre coefficients
  338.                 final SinCos sc = FastMath.sinCos(m * lambdaS);
  339.                 final double pCosmLambda = p.getPnm(n, m) * sc.cos();
  340.                 final double pSinmLambda = p.getPnm(n, m) * sc.sin();

  341.                 // Update VTEC value
  342.                 vtec += cnm[n][m] * pCosmLambda + snm[n][m] * pSinmLambda;

  343.             }

  344.         }

  345.         // Return the VTEC
  346.         return vtec;

  347.     }

  348.     /**
  349.      * Calculate the VTEC contribution for a given ionospheric layer.
  350.      * @param degree degree of spherical expansion
  351.      * @param order order of spherical expansion
  352.      * @param cnm cosine coefficients for the layer in TECU
  353.      * @param snm sine coefficients for the layer in TECU
  354.      * @param phiPP geocentric latitude of ionospheric pierce point for the layer in radians
  355.      * @param lambdaS mean sun fixed and phase shifted longitude of ionospheric pierce point
  356.      * @param <T> type of the elements
  357.      * @return the VTEC contribution for the current ionospheric layer in TECU
  358.      */
  359.     private static <T extends CalculusFieldElement<T>> T calculateVTEC(final int degree, final int order,
  360.                                                                    final double[][] cnm, final double[][] snm,
  361.                                                                    final T phiPP, final T lambdaS) {

  362.         // Initialize VTEC value
  363.         T vtec = phiPP.getField().getZero();

  364.         // Compute Legendre Polynomials Pnm(sin(phiPP))
  365.         final FieldLegendrePolynomials<T> p = new FieldLegendrePolynomials<>(degree, order, FastMath.sin(phiPP));

  366.         // Calculate VTEC
  367.         for (int n = 0; n <= degree; n++) {

  368.             for (int m = 0; m <= FastMath.min(n, order); m++) {

  369.                 // Legendre coefficients
  370.                 final FieldSinCos<T> sc = FastMath.sinCos(lambdaS.multiply(m));
  371.                 final T pCosmLambda = p.getPnm(n, m).multiply(sc.cos());
  372.                 final T pSinmLambda = p.getPnm(n, m).multiply(sc.sin());

  373.                 // Update VTEC value
  374.                 vtec = vtec.add(pCosmLambda.multiply(cnm[n][m]).add(pSinmLambda.multiply(snm[n][m])));

  375.             }

  376.         }

  377.         // Return the VTEC
  378.         return vtec;

  379.     }

  380.     /**
  381.      * Get the SSR epoch time of computation modulo 86400 seconds.
  382.      * @param im201Header header data
  383.      * @return the SSR epoch time of computation modulo 86400 seconds
  384.      */
  385.     private static double getTime(final SsrIm201Header im201Header) {
  386.         final double ssrEpochTime = im201Header.getSsrEpoch1s();
  387.         return ssrEpochTime - FastMath.floor(ssrEpochTime / Constants.JULIAN_DAY) * Constants.JULIAN_DAY;
  388.     }

  389.     /**
  390.      * Verify the condition for the calculation of the pierce point longitude.
  391.      * @param scACos cosine of the azimuth angle
  392.      * @param psiPP the spherical Earth’s central angle in radians
  393.      * @param phiR the geocentric latitude of the station in radians
  394.      * @return true if the condition is respected
  395.      */
  396.     private static boolean verifyCondition(final double scACos, final double psiPP,
  397.                                            final double phiR) {

  398.         // tan(PsiPP) * cos(Azimuth)
  399.         final double tanPsiCosA = FastMath.tan(psiPP) * scACos;

  400.         // Verify condition
  401.         return phiR >= 0 && tanPsiCosA > FastMath.tan(MathUtils.SEMI_PI - phiR) ||
  402.                         phiR < 0 && -tanPsiCosA > FastMath.tan(MathUtils.SEMI_PI + phiR);

  403.     }

  404. }