CanonicalSaastamoinenModel.java

  1. /* Copyright 2022-2025 Thales Alenia Space
  2.  * Licensed to CS Communication & Systèmes (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.troposphere;

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

  20. import org.hipparchus.CalculusFieldElement;
  21. import org.hipparchus.Field;
  22. import org.hipparchus.analysis.interpolation.LinearInterpolator;
  23. import org.hipparchus.analysis.polynomials.PolynomialSplineFunction;
  24. import org.hipparchus.util.FastMath;
  25. import org.orekit.bodies.FieldGeodeticPoint;
  26. import org.orekit.bodies.GeodeticPoint;
  27. import org.orekit.models.earth.weather.FieldPressureTemperatureHumidity;
  28. import org.orekit.models.earth.weather.HeightDependentPressureTemperatureHumidityConverter;
  29. import org.orekit.models.earth.weather.PressureTemperatureHumidity;
  30. import org.orekit.models.earth.weather.PressureTemperatureHumidityProvider;
  31. import org.orekit.time.AbsoluteDate;
  32. import org.orekit.time.FieldAbsoluteDate;
  33. import org.orekit.utils.FieldTrackingCoordinates;
  34. import org.orekit.utils.ParameterDriver;
  35. import org.orekit.utils.TrackingCoordinates;

  36. /** The canonical Saastamoinen model.
  37.  * <p>
  38.  * Estimates the path delay imposed to
  39.  * electro-magnetic signals by the troposphere according to the formula:
  40.  * \[
  41.  * \delta = \frac{0.002277}{\cos z} \left[P+(\frac{1255}{T}+0.005)e - B(h) \tan^2 z\right]
  42.  * \]
  43.  * with the following input data provided to the model:
  44.  * <ul>
  45.  * <li>z: zenith angle</li>
  46.  * <li>P: atmospheric pressure</li>
  47.  * <li>T: temperature</li>
  48.  * <li>e: partial pressure of water vapor</li>
  49.  * </ul>
  50.  * @author Luc Maisonobe
  51.  * @see "J Saastamoinen, Atmospheric Correction for the Troposphere and Stratosphere in Radio
  52.  * Ranging of Satellites"
  53.  * @since 12.1
  54.  */
  55. public class CanonicalSaastamoinenModel implements TroposphericModel {

  56.     /** Default lowest acceptable elevation angle [rad]. */
  57.     public static final double DEFAULT_LOW_ELEVATION_THRESHOLD = 0.05;

  58.     /** Base delay coefficient. */
  59.     private static final double L0 = 2.2768e-5;

  60.     /** Temperature numerator. */
  61.     private static final double T_NUM = 1255;

  62.     /** Wet offset. */
  63.     private static final double WET_OFFSET = 0.05;

  64.     /** X values for the B function (table 1 in reference paper). */
  65.     private static final double[] X_VALUES_FOR_B = {
  66.         0.0, 200.0, 400.0, 600.0, 800.0, 1000.0, 1500.0, 2000.0, 2500.0, 3000.0, 4000.0, 5000.0, 6000.0
  67.     };

  68.     /** Y values for the B function (table 1 in reference paper).
  69.      * <p>
  70.      * The values have been scaled up by a factor 100.0 due to conversion from hPa to Pa.
  71.      * </p>
  72.      */
  73.     private static final double[] Y_VALUES_FOR_B = {
  74.         116.0, 113.0, 110.0, 107.0, 104.0, 101.0, 94.0, 88.0, 82.0, 76.0, 66.0, 57.0, 49.0
  75.     };

  76.     /** Interpolation function for the B correction term. */
  77.     private static final PolynomialSplineFunction B_FUNCTION;

  78.     static {
  79.         B_FUNCTION = new LinearInterpolator().interpolate(X_VALUES_FOR_B, Y_VALUES_FOR_B);
  80.     }

  81.     /** Lowest acceptable elevation angle [rad]. */
  82.     private double lowElevationThreshold;

  83.     /** Provider for pressure, temperature and humidity. */
  84.     private final PressureTemperatureHumidityProvider pthProvider;

  85.     /**
  86.      * Create a new Saastamoinen model for the troposphere using the given environmental
  87.      * conditions and table from the reference book.
  88.      *
  89.      * @see HeightDependentPressureTemperatureHumidityConverter
  90.      */
  91.     public CanonicalSaastamoinenModel() {
  92.         this(TroposphericModelUtils.STANDARD_ATMOSPHERE_PROVIDER);
  93.     }

  94.     /**
  95.      * Create a new Saastamoinen model for the troposphere using the given environmental
  96.      * conditions and table from the reference book.
  97.      * @param pthProvider provider for pressure, temperature and humidity
  98.      * @since 13.0
  99.      */
  100.     public CanonicalSaastamoinenModel(final PressureTemperatureHumidityProvider pthProvider) {
  101.         this.lowElevationThreshold = DEFAULT_LOW_ELEVATION_THRESHOLD;
  102.         this.pthProvider           = pthProvider;
  103.     }

  104.     /** {@inheritDoc}
  105.      * <p>
  106.      * The Saastamoinen model is not defined for altitudes below 0.0. for continuity
  107.      * reasons, we use the value for h = 0 when altitude is negative.
  108.      * </p>
  109.      * <p>
  110.      * There are also numerical issues for elevation angles close to zero. For continuity reasons,
  111.      * elevations lower than a threshold will use the value obtained
  112.      * for the threshold itself.
  113.      * </p>
  114.      * @see #getLowElevationThreshold()
  115.      * @see #setLowElevationThreshold(double)
  116.      */
  117.     @Override
  118.     public TroposphericDelay pathDelay(final TrackingCoordinates trackingCoordinates, final GeodeticPoint point,
  119.                                        final double[] parameters, final AbsoluteDate date) {

  120.         // there are no data in the model for negative altitudes and altitude bigger than 6000 m
  121.         // limit the height to a range of [0, 5000] m
  122.         final double fixedHeight = FastMath.min(FastMath.max(point.getAltitude(), X_VALUES_FOR_B[0]),
  123.                                                 X_VALUES_FOR_B[X_VALUES_FOR_B.length - 1]);

  124.         // interpolate the b correction term
  125.         final double B = B_FUNCTION.value(fixedHeight);

  126.         // calculate the zenith angle from the elevation
  127.         final double z = FastMath.abs(0.5 * FastMath.PI - FastMath.max(trackingCoordinates.getElevation(),
  128.                                                                        lowElevationThreshold));

  129.         // compute weather parameters
  130.         final PressureTemperatureHumidity weather = pthProvider.getWeatherParameters(point, date);

  131.         // calculate the path delay
  132.         final double invCos = 1.0 / FastMath.cos(z);
  133.         final double tan    = FastMath.tan(z);
  134.         final double zh     = L0 * weather.getPressure();
  135.         final double zw     = L0 * (T_NUM / weather.getTemperature() + WET_OFFSET) * weather.getWaterVaporPressure();
  136.         final double sh     = zh * invCos;
  137.         final double sw     = (zw - L0 * B * tan * tan) * invCos;
  138.         return new TroposphericDelay(zh, zw, sh, sw);

  139.     }

  140.     /** {@inheritDoc}
  141.      * <p>
  142.      * The Saastamoinen model is not defined for altitudes below 0.0. for continuity
  143.      * reasons, we use the value for h = 0 when altitude is negative.
  144.      * </p>
  145.      * <p>
  146.      * There are also numerical issues for elevation angles close to zero. For continuity reasons,
  147.      * elevations lower than a threshold will use the value obtained
  148.      * for the threshold itself.
  149.      * </p>
  150.      * @see #getLowElevationThreshold()
  151.      * @see #setLowElevationThreshold(double)
  152.      */
  153.     @Override
  154.     public <T extends CalculusFieldElement<T>> FieldTroposphericDelay<T> pathDelay(final FieldTrackingCoordinates<T> trackingCoordinates,
  155.                                                                                    final FieldGeodeticPoint<T> point,
  156.                                                                                    final T[] parameters, final FieldAbsoluteDate<T> date) {

  157.         final Field<T> field = date.getField();
  158.         final T zero = field.getZero();

  159.         // there are no data in the model for negative altitudes and altitude bigger than 5000 m
  160.         // limit the height to a range of [0, 5000] m
  161.         final T fixedHeight = FastMath.min(FastMath.max(point.getAltitude(), X_VALUES_FOR_B[0]),
  162.                                            X_VALUES_FOR_B[X_VALUES_FOR_B.length - 1]);

  163.         // interpolate the b correction term
  164.         final T B = B_FUNCTION.value(fixedHeight);

  165.         // calculate the zenith angle from the elevation
  166.         final T z = FastMath.abs(zero.getPi().multiply(0.5).
  167.                                  subtract(FastMath.max(trackingCoordinates.getElevation(), lowElevationThreshold)));

  168.         // compute weather parameters
  169.         final FieldPressureTemperatureHumidity<T> weather = pthProvider.getWeatherParameters(point, date);

  170.         // calculate the path delay in m
  171.         final T invCos = FastMath.cos(z).reciprocal();
  172.         final T tan    = FastMath.tan(z);
  173.         final T zh     = weather.getPressure().multiply(L0);
  174.         final T zw     = weather.getTemperature().reciprocal().multiply(T_NUM).add(WET_OFFSET).
  175.                          multiply(weather.getWaterVaporPressure()).multiply(L0);
  176.         final T sh     = zh.multiply(invCos);
  177.         final T sw     = zw.subtract(B.multiply(tan).multiply(tan).multiply(L0)).multiply(invCos);
  178.         return new FieldTroposphericDelay<>(zh, zw, sh, sw);

  179.     }

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

  185.     /** Get the low elevation threshold value for path delay computation.
  186.      * @return low elevation threshold, in rad.
  187.      * @see #pathDelay(TrackingCoordinates, GeodeticPoint, double[], AbsoluteDate)
  188.      * @see #pathDelay(FieldTrackingCoordinates, FieldGeodeticPoint, CalculusFieldElement[], FieldAbsoluteDate)
  189.      */
  190.     public double getLowElevationThreshold() {
  191.         return lowElevationThreshold;
  192.     }

  193.     /** Set the low elevation threshold value for path delay computation.
  194.      * @param lowElevationThreshold The new value for the threshold [rad]
  195.      * @see #pathDelay(TrackingCoordinates, GeodeticPoint, double[], AbsoluteDate)
  196.      * @see #pathDelay(FieldTrackingCoordinates, FieldGeodeticPoint, CalculusFieldElement[], FieldAbsoluteDate)
  197.      */
  198.     public void setLowElevationThreshold(final double lowElevationThreshold) {
  199.         this.lowElevationThreshold = lowElevationThreshold;
  200.     }

  201. }