SsrVtecIonosphericModel.java
/* Copyright 2002-2024 CS GROUP
* Licensed to CS GROUP (CS) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* CS licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.orekit.models.earth.ionosphere;
import java.util.Collections;
import java.util.List;
import org.hipparchus.CalculusFieldElement;
import org.hipparchus.Field;
import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.FastMath;
import org.hipparchus.util.FieldSinCos;
import org.hipparchus.util.MathUtils;
import org.hipparchus.util.SinCos;
import org.orekit.bodies.FieldGeodeticPoint;
import org.orekit.bodies.GeodeticPoint;
import org.orekit.frames.TopocentricFrame;
import org.orekit.gnss.metric.messages.ssr.subtype.SsrIm201;
import org.orekit.gnss.metric.messages.ssr.subtype.SsrIm201Data;
import org.orekit.gnss.metric.messages.ssr.subtype.SsrIm201Header;
import org.orekit.propagation.FieldSpacecraftState;
import org.orekit.propagation.SpacecraftState;
import org.orekit.utils.Constants;
import org.orekit.utils.FieldLegendrePolynomials;
import org.orekit.utils.LegendrePolynomials;
import org.orekit.utils.ParameterDriver;
/**
* Ionospheric model based on SSR IM201 message.
* <p>
* Within this message, the ionospheric VTEC is provided
* using spherical harmonic expansions. For a given ionospheric
* layer, the slant TEC value is calculated using the satellite
* elevation and the height of the corresponding layer. The
* total slant TEC is computed by the sum of the individual slant
* TEC for each layer.
* </p>
* @author Bryan Cazabonne
* @since 11.0
* @see "IGS State Space Representation (SSR) Format, Version 1.00, October 2020."
*/
public class SsrVtecIonosphericModel implements IonosphericModel {
/** Earth radius in meters (see reference). */
private static final double EARTH_RADIUS = 6370000.0;
/** Multiplication factor for path delay computation. */
private static final double FACTOR = 40.3e16;
/** SSR Ionosphere VTEC Spherical Harmonics Message.. */
private final transient SsrIm201 vtecMessage;
/**
* Constructor.
* @param vtecMessage SSR Ionosphere VTEC Spherical Harmonics Message.
*/
public SsrVtecIonosphericModel(final SsrIm201 vtecMessage) {
this.vtecMessage = vtecMessage;
}
/** {@inheritDoc} */
@Override
public double pathDelay(final SpacecraftState state, final TopocentricFrame baseFrame,
final double frequency, final double[] parameters) {
// Elevation in radians
final Vector3D position = state.getPosition(baseFrame);
final double elevation = position.getDelta();
// Only consider measures above the horizon
if (elevation > 0.0) {
// Azimuth angle in radians
double azimuth = FastMath.atan2(position.getX(), position.getY());
if (azimuth < 0.) {
azimuth += MathUtils.TWO_PI;
}
// Initialize slant TEC
double stec = 0.0;
// Message header
final SsrIm201Header header = vtecMessage.getHeader();
// Loop on ionospheric layers
for (final SsrIm201Data data : vtecMessage.getData()) {
stec += stecIonosphericLayer(data, header, elevation, azimuth, baseFrame.getPoint());
}
// Return the path delay
return FACTOR * stec / (frequency * frequency);
}
// Delay is equal to 0.0
return 0.0;
}
/** {@inheritDoc} */
@Override
public <T extends CalculusFieldElement<T>> T pathDelay(final FieldSpacecraftState<T> state, final TopocentricFrame baseFrame,
final double frequency, final T[] parameters) {
// Field
final Field<T> field = state.getDate().getField();
// Elevation in radians
final FieldVector3D<T> position = state.getPosition(baseFrame);
final T elevation = position.getDelta();
// Only consider measures above the horizon
if (elevation.getReal() > 0.0) {
// Azimuth angle in radians
T azimuth = FastMath.atan2(position.getX(), position.getY());
if (azimuth.getReal() < 0.) {
azimuth = azimuth.add(MathUtils.TWO_PI);
}
// Initialize slant TEC
T stec = field.getZero();
// Message header
final SsrIm201Header header = vtecMessage.getHeader();
// Loop on ionospheric layers
for (SsrIm201Data data : vtecMessage.getData()) {
stec = stec.add(stecIonosphericLayer(data, header, elevation, azimuth, baseFrame.getPoint(field)));
}
// Return the path delay
return stec.multiply(FACTOR).divide(frequency * frequency);
}
// Delay is equal to 0.0
return field.getZero();
}
/** {@inheritDoc} */
@Override
public List<ParameterDriver> getParametersDrivers() {
return Collections.emptyList();
}
/**
* Calculates the slant TEC for a given ionospheric layer.
* @param im201Data ionospheric data for the current layer
* @param im201Header container for data contained in the header
* @param elevation satellite elevation angle [rad]
* @param azimuth satellite azimuth angle [rad]
* @param point geodetic point
* @return the slant TEC for the current ionospheric layer
*/
private static double stecIonosphericLayer(final SsrIm201Data im201Data, final SsrIm201Header im201Header,
final double elevation, final double azimuth,
final GeodeticPoint point) {
// Geodetic point data
final double phiR = point.getLatitude();
final double lambdaR = point.getLongitude();
final double hR = point.getAltitude();
// Data contained in the message
final double hI = im201Data.getHeightIonosphericLayer();
final int degree = im201Data.getSphericalHarmonicsDegree();
final int order = im201Data.getSphericalHarmonicsOrder();
final double[][] cnm = im201Data.getCnm();
final double[][] snm = im201Data.getSnm();
// Spherical Earth's central angle
final double psiPP = calculatePsi(hR, hI, elevation);
// Sine and cosine of useful angles
final SinCos scA = FastMath.sinCos(azimuth);
final SinCos scPhiR = FastMath.sinCos(phiR);
final SinCos scPsi = FastMath.sinCos(psiPP);
// Pierce point latitude and longitude
final double phiPP = calculatePiercePointLatitude(scPhiR, scPsi, scA);
final double lambdaPP = calculatePiercePointLongitude(scA, phiPP, psiPP, phiR, lambdaR);
// Mean sun fixed longitude (modulo 2pi)
final double lambdaS = calculateSunLongitude(im201Header, lambdaPP);
// VTEC
// According to the documentation, negative VTEC values must be ignored and shall be replaced by 0.0
final double vtec = FastMath.max(0.0, calculateVTEC(degree, order, cnm, snm, phiPP, lambdaS));
// Return STEC for the current ionospheric layer
return vtec / FastMath.sin(elevation + psiPP);
}
/**
* Calculates the slant TEC for a given ionospheric layer.
* @param im201Data ionospheric data for the current layer
* @param im201Header container for data contained in the header
* @param elevation satellite elevation angle [rad]
* @param azimuth satellite azimuth angle [rad]
* @param point geodetic point
* @param <T> type of the elements
* @return the slant TEC for the current ionospheric layer
*/
private static <T extends CalculusFieldElement<T>> T stecIonosphericLayer(final SsrIm201Data im201Data, final SsrIm201Header im201Header,
final T elevation, final T azimuth,
final FieldGeodeticPoint<T> point) {
// Geodetic point data
final T phiR = point.getLatitude();
final T lambdaR = point.getLongitude();
final T hR = point.getAltitude();
// Data contained in the message
final double hI = im201Data.getHeightIonosphericLayer();
final int degree = im201Data.getSphericalHarmonicsDegree();
final int order = im201Data.getSphericalHarmonicsOrder();
final double[][] cnm = im201Data.getCnm();
final double[][] snm = im201Data.getSnm();
// Spherical Earth's central angle
final T psiPP = calculatePsi(hR, hI, elevation);
// Sine and cosine of useful angles
final FieldSinCos<T> scA = FastMath.sinCos(azimuth);
final FieldSinCos<T> scPhiR = FastMath.sinCos(phiR);
final FieldSinCos<T> scPsi = FastMath.sinCos(psiPP);
// Pierce point latitude and longitude
final T phiPP = calculatePiercePointLatitude(scPhiR, scPsi, scA);
final T lambdaPP = calculatePiercePointLongitude(scA, phiPP, psiPP, phiR, lambdaR);
// Mean sun fixed longitude (modulo 2pi)
final T lambdaS = calculateSunLongitude(im201Header, lambdaPP);
// VTEC
// According to the documentation, negative VTEC values must be ignored and shall be replaced by 0.0
final T vtec = FastMath.max(phiR.getField().getZero(), calculateVTEC(degree, order, cnm, snm, phiPP, lambdaS));
// Return STEC for the current ionospheric layer
return vtec.divide(FastMath.sin(elevation.add(psiPP)));
}
/**
* Calculates the spherical Earth’s central angle between station position and
* the projection of the pierce point to the spherical Earth’s surface.
* @param hR height of station position in meters
* @param hI height of ionospheric layer in meters
* @param elevation satellite elevation angle in radians
* @return the spherical Earth’s central angle in radians
*/
private static double calculatePsi(final double hR, final double hI,
final double elevation) {
final double ratio = (EARTH_RADIUS + hR) / (EARTH_RADIUS + hI);
return MathUtils.SEMI_PI - elevation - FastMath.asin(ratio * FastMath.cos(elevation));
}
/**
* Calculates the spherical Earth’s central angle between station position and
* the projection of the pierce point to the spherical Earth’s surface.
* @param hR height of station position in meters
* @param hI height of ionospheric layer in meters
* @param elevation satellite elevation angle in radians
* @param <T> type of the elements
* @return the spherical Earth’s central angle in radians
*/
private static <T extends CalculusFieldElement<T>> T calculatePsi(final T hR, final double hI,
final T elevation) {
final T ratio = hR.add(EARTH_RADIUS).divide(EARTH_RADIUS + hI);
return hR.getPi().multiply(0.5).subtract(elevation).subtract(FastMath.asin(ratio.multiply(FastMath.cos(elevation))));
}
/**
* Calculates the latitude of the pierce point in the spherical Earth model.
* @param scPhiR sine and cosine of the geocentric latitude of the station
* @param scPsi sine and cosine of the spherical Earth's central angle
* @param scA sine and cosine of the azimuth angle
* @return the latitude of the pierce point in the spherical Earth model in radians
*/
private static double calculatePiercePointLatitude(final SinCos scPhiR, final SinCos scPsi, final SinCos scA) {
return FastMath.asin(scPhiR.sin() * scPsi.cos() + scPhiR.cos() * scPsi.sin() * scA.cos());
}
/**
* Calculates the latitude of the pierce point in the spherical Earth model.
* @param scPhiR sine and cosine of the geocentric latitude of the station
* @param scPsi sine and cosine of the spherical Earth's central angle
* @param scA sine and cosine of the azimuth angle
* @param <T> type of the elements
* @return the latitude of the pierce point in the spherical Earth model in radians
*/
private static <T extends CalculusFieldElement<T>> T calculatePiercePointLatitude(final FieldSinCos<T> scPhiR,
final FieldSinCos<T> scPsi,
final FieldSinCos<T> scA) {
return FastMath.asin(scPhiR.sin().multiply(scPsi.cos()).add(scPhiR.cos().multiply(scPsi.sin()).multiply(scA.cos())));
}
/**
* Calculates the longitude of the pierce point in the spherical Earth model.
* @param scA sine and cosine of the azimuth angle
* @param phiPP the latitude of the pierce point in the spherical Earth model in radians
* @param psiPP the spherical Earth’s central angle in radians
* @param phiR the geocentric latitude of the station in radians
* @param lambdaR the geocentric longitude of the station
* @return the longitude of the pierce point in the spherical Earth model in radians
*/
private static double calculatePiercePointLongitude(final SinCos scA,
final double phiPP, final double psiPP,
final double phiR, final double lambdaR) {
// arcSin(sin(PsiPP) * sin(Azimuth) / cos(PhiPP))
final double arcSin = FastMath.asin(FastMath.sin(psiPP) * scA.sin() / FastMath.cos(phiPP));
// Return
return verifyCondition(scA.cos(), psiPP, phiR) ? lambdaR + FastMath.PI - arcSin : lambdaR + arcSin;
}
/**
* Calculates the longitude of the pierce point in the spherical Earth model.
* @param scA sine and cosine of the azimuth angle
* @param phiPP the latitude of the pierce point in the spherical Earth model in radians
* @param psiPP the spherical Earth’s central angle in radians
* @param phiR the geocentric latitude of the station in radians
* @param lambdaR the geocentric longitude of the station
* @param <T> type of the elements
* @return the longitude of the pierce point in the spherical Earth model in radians
*/
private static <T extends CalculusFieldElement<T>> T calculatePiercePointLongitude(final FieldSinCos<T> scA,
final T phiPP, final T psiPP,
final T phiR, final T lambdaR) {
// arcSin(sin(PsiPP) * sin(Azimuth) / cos(PhiPP))
final T arcSin = FastMath.asin(FastMath.sin(psiPP).multiply(scA.sin()).divide(FastMath.cos(phiPP)));
// Return
return verifyCondition(scA.cos().getReal(), psiPP.getReal(), phiR.getReal()) ?
lambdaR.add(arcSin.getPi()).subtract(arcSin) : lambdaR.add(arcSin);
}
/**
* Calculate the mean sun fixed longitude phase.
* @param im201Header header of the IM201 message
* @param lambdaPP the longitude of the pierce point in the spherical Earth model in radians
* @return the mean sun fixed longitude phase in radians
*/
private static double calculateSunLongitude(final SsrIm201Header im201Header, final double lambdaPP) {
final double t = getTime(im201Header);
return MathUtils.normalizeAngle(lambdaPP + (t - 50400.0) * FastMath.PI / 43200.0, FastMath.PI);
}
/**
* Calculate the mean sun fixed longitude phase.
* @param im201Header header of the IM201 message
* @param lambdaPP the longitude of the pierce point in the spherical Earth model in radians
* @param <T> type of the elements
* @return the mean sun fixed longitude phase in radians
*/
private static <T extends CalculusFieldElement<T>> T calculateSunLongitude(final SsrIm201Header im201Header, final T lambdaPP) {
final double t = getTime(im201Header);
return MathUtils.normalizeAngle(lambdaPP.add(lambdaPP.getPi().multiply(t - 50400.0).divide(43200.0)), lambdaPP.getPi());
}
/**
* Calculate the VTEC contribution for a given ionospheric layer.
* @param degree degree of spherical expansion
* @param order order of spherical expansion
* @param cnm cosine coefficients for the layer in TECU
* @param snm sine coefficients for the layer in TECU
* @param phiPP geocentric latitude of ionospheric pierce point for the layer in radians
* @param lambdaS mean sun fixed and phase shifted longitude of ionospheric pierce point
* @return the VTEC contribution for the current ionospheric layer in TECU
*/
private static double calculateVTEC(final int degree, final int order,
final double[][] cnm, final double[][] snm,
final double phiPP, final double lambdaS) {
// Initialize VTEC value
double vtec = 0.0;
// Compute Legendre Polynomials Pnm(sin(phiPP))
final LegendrePolynomials p = new LegendrePolynomials(degree, order, FastMath.sin(phiPP));
// Calculate VTEC
for (int n = 0; n <= degree; n++) {
for (int m = 0; m <= FastMath.min(n, order); m++) {
// Legendre coefficients
final SinCos sc = FastMath.sinCos(m * lambdaS);
final double pCosmLambda = p.getPnm(n, m) * sc.cos();
final double pSinmLambda = p.getPnm(n, m) * sc.sin();
// Update VTEC value
vtec += cnm[n][m] * pCosmLambda + snm[n][m] * pSinmLambda;
}
}
// Return the VTEC
return vtec;
}
/**
* Calculate the VTEC contribution for a given ionospheric layer.
* @param degree degree of spherical expansion
* @param order order of spherical expansion
* @param cnm cosine coefficients for the layer in TECU
* @param snm sine coefficients for the layer in TECU
* @param phiPP geocentric latitude of ionospheric pierce point for the layer in radians
* @param lambdaS mean sun fixed and phase shifted longitude of ionospheric pierce point
* @param <T> type of the elements
* @return the VTEC contribution for the current ionospheric layer in TECU
*/
private static <T extends CalculusFieldElement<T>> T calculateVTEC(final int degree, final int order,
final double[][] cnm, final double[][] snm,
final T phiPP, final T lambdaS) {
// Initialize VTEC value
T vtec = phiPP.getField().getZero();
// Compute Legendre Polynomials Pnm(sin(phiPP))
final FieldLegendrePolynomials<T> p = new FieldLegendrePolynomials<>(degree, order, FastMath.sin(phiPP));
// Calculate VTEC
for (int n = 0; n <= degree; n++) {
for (int m = 0; m <= FastMath.min(n, order); m++) {
// Legendre coefficients
final FieldSinCos<T> sc = FastMath.sinCos(lambdaS.multiply(m));
final T pCosmLambda = p.getPnm(n, m).multiply(sc.cos());
final T pSinmLambda = p.getPnm(n, m).multiply(sc.sin());
// Update VTEC value
vtec = vtec.add(pCosmLambda.multiply(cnm[n][m]).add(pSinmLambda.multiply(snm[n][m])));
}
}
// Return the VTEC
return vtec;
}
/**
* Get the SSR epoch time of computation modulo 86400 seconds.
* @param im201Header header data
* @return the SSR epoch time of computation modulo 86400 seconds
*/
private static double getTime(final SsrIm201Header im201Header) {
final double ssrEpochTime = im201Header.getSsrEpoch1s();
return ssrEpochTime - FastMath.floor(ssrEpochTime / Constants.JULIAN_DAY) * Constants.JULIAN_DAY;
}
/**
* Verify the condition for the calculation of the pierce point longitude.
* @param scACos cosine of the azimuth angle
* @param psiPP the spherical Earth’s central angle in radians
* @param phiR the geocentric latitude of the station in radians
* @return true if the condition is respected
*/
private static boolean verifyCondition(final double scACos, final double psiPP,
final double phiR) {
// tan(PsiPP) * cos(Azimuth)
final double tanPsiCosA = FastMath.tan(psiPP) * scACos;
// Verify condition
return phiR >= 0 && tanPsiCosA > FastMath.tan(MathUtils.SEMI_PI - phiR) ||
phiR < 0 && -tanPsiCosA > FastMath.tan(MathUtils.SEMI_PI + phiR);
}
}