SecularAndHarmonic.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.utils;

  18. import java.util.ArrayList;
  19. import java.util.Collection;
  20. import java.util.List;

  21. import org.hipparchus.analysis.ParametricUnivariateFunction;
  22. import org.hipparchus.fitting.AbstractCurveFitter;
  23. import org.hipparchus.fitting.PolynomialCurveFitter;
  24. import org.hipparchus.fitting.WeightedObservedPoint;
  25. import org.hipparchus.linear.DiagonalMatrix;
  26. import org.hipparchus.optim.nonlinear.vector.leastsquares.LeastSquaresBuilder;
  27. import org.hipparchus.optim.nonlinear.vector.leastsquares.LeastSquaresProblem;
  28. import org.hipparchus.util.FastMath;
  29. import org.hipparchus.util.SinCos;
  30. import org.orekit.time.AbsoluteDate;

  31. /** Class for fitting evolution of osculating orbital parameters.
  32.  * <p>
  33.  * This class allows conversion from osculating parameters to mean parameters.
  34.  * </p>
  35.  *
  36.  * @author Luc Maisonobe
  37.  */
  38. public class SecularAndHarmonic {

  39.     /** Degree of polynomial secular part. */
  40.     private final int secularDegree;

  41.     /** Pulsations of harmonic part. */
  42.     private final double[] pulsations;

  43.     /** Reference date for the model. */
  44.     private AbsoluteDate reference;

  45.     /** Fitted parameters. */
  46.     private double[] fitted;

  47.     /** Observed points. */
  48.     private List<WeightedObservedPoint> observedPoints;

  49.     /** RMS for convergence.
  50.      * @since 10.3
  51.      */
  52.     private double convergenceRMS;

  53.     /** Maximum number of iterations.
  54.      * @since 10.3
  55.      */
  56.     private int maxIter;

  57.     /** Simple constructor.
  58.      * @param secularDegree degree of polynomial secular part
  59.      * @param pulsations pulsations of harmonic part
  60.      */
  61.     public SecularAndHarmonic(final int secularDegree, final double... pulsations) {
  62.         this.secularDegree  = secularDegree;
  63.         this.pulsations     = pulsations.clone();
  64.         this.observedPoints = new ArrayList<>();
  65.         this.convergenceRMS = 0.0;
  66.         this.maxIter        = Integer.MAX_VALUE;
  67.     }

  68.     /** Reset fitting.
  69.      * @param date reference date
  70.      * @param initialGuess initial guess for the parameters
  71.      * @see #getReferenceDate()
  72.      */
  73.     public void resetFitting(final AbsoluteDate date, final double... initialGuess) {
  74.         reference = date;
  75.         fitted    = initialGuess.clone();
  76.         observedPoints.clear();
  77.     }

  78.     /** Set RMS for convergence.
  79.      * <p>
  80.      * The RMS is the square-root of the sum of squared of
  81.      * the residuals, divided by the number of measurements.
  82.      * </p>
  83.      * @param convergenceRMS RMS below which convergence is considered to have been reached
  84.      * @since 10.3
  85.      */
  86.     public void setConvergenceRMS(final double convergenceRMS) {
  87.         this.convergenceRMS = convergenceRMS;
  88.     }

  89.     /** Set maximum number of iterations.
  90.      * @param maxIter maximum number of iterations
  91.      * @since 10.3
  92.      */
  93.     public void setMaxIter(final int maxIter) {
  94.         this.maxIter = maxIter;
  95.     }

  96.     /** Add a fitting point.
  97.      * <p>
  98.      * The point weight is set to 1.0
  99.      * </p>
  100.      * @param date date of the point
  101.      * @param osculatingValue osculating value
  102.      * @see #addWeightedPoint(AbsoluteDate, double, double)
  103.      */
  104.     public void addPoint(final AbsoluteDate date, final double osculatingValue) {
  105.         addWeightedPoint(date, osculatingValue, 1.0);
  106.     }

  107.     /** Add a weighted fitting point.
  108.      * @param date date of the point
  109.      * @param osculatingValue osculating value
  110.      * @param weight weight of the points
  111.      * @since 12.0
  112.      */
  113.     public void addWeightedPoint(final AbsoluteDate date, final double osculatingValue, final double weight) {
  114.         observedPoints.add(new WeightedObservedPoint(weight, date.durationFrom(reference), osculatingValue));
  115.     }

  116.     /** Get the reference date.
  117.      * @return reference date
  118.      * @see #resetFitting(AbsoluteDate, double...)
  119.      */
  120.     public AbsoluteDate getReferenceDate() {
  121.         return reference;
  122.     }

  123.     /** Get degree of polynomial secular part.
  124.      * @return degree of polynomial secular part
  125.      * @since 12.0
  126.      */
  127.     public int getSecularDegree() {
  128.         return secularDegree;
  129.     }

  130.     /** Get the pulsations of harmonic part.
  131.      * @return pulsations of harmonic part
  132.      * @since 12.0
  133.      */
  134.     public double[] getPulsations() {
  135.         return pulsations.clone();
  136.     }

  137.     /** Get an upper bound of the fitted harmonic amplitude.
  138.      * @return upper bound of the fitted harmonic amplitude
  139.      */
  140.     public double getHarmonicAmplitude() {
  141.         double amplitude = 0;
  142.         for (int i = 0; i < pulsations.length; ++i) {
  143.             amplitude += FastMath.hypot(fitted[secularDegree + 2 * i + 1],
  144.                                         fitted[secularDegree + 2 * i + 2]);
  145.         }
  146.         return amplitude;
  147.     }

  148.     /** Fit parameters.
  149.      * @see #getFittedParameters()
  150.      */
  151.     public void fit() {

  152.         final AbstractCurveFitter fitter = new AbstractCurveFitter() {
  153.             /** {@inheritDoc} */
  154.             @Override
  155.             protected LeastSquaresProblem getProblem(final Collection<WeightedObservedPoint> observations) {
  156.                 // Prepare least-squares problem.
  157.                 final int len = observations.size();
  158.                 final double[] target  = new double[len];
  159.                 final double[] weights = new double[len];

  160.                 int i = 0;
  161.                 for (final WeightedObservedPoint obs : observations) {
  162.                     target[i]  = obs.getY();
  163.                     weights[i] = obs.getWeight();
  164.                     ++i;
  165.                 }

  166.                 final AbstractCurveFitter.TheoreticalValuesFunction model =
  167.                         new AbstractCurveFitter.TheoreticalValuesFunction(new LocalParametricFunction(), observations);

  168.                 // build a new least squares problem set up to fit a secular and harmonic curve to the observed points
  169.                 return new LeastSquaresBuilder().
  170.                         maxEvaluations(Integer.MAX_VALUE).
  171.                         maxIterations(maxIter).
  172.                         checker((iteration, previous, current) -> current.getRMS() <= convergenceRMS).
  173.                         start(fitted).
  174.                         target(target).
  175.                         weight(new DiagonalMatrix(weights)).
  176.                         model(model.getModelFunction(), model.getModelFunctionJacobian()).
  177.                         build();

  178.             }
  179.         };

  180.         fitted = fitter.fit(observedPoints);

  181.     }

  182.     /** Local parametric function used for fitting. */
  183.     private class LocalParametricFunction implements ParametricUnivariateFunction {

  184.         /** {@inheritDoc} */
  185.         public double value(final double x, final double... parameters) {
  186.             return truncatedValue(secularDegree, pulsations.length, x, parameters);
  187.         }

  188.         /** {@inheritDoc} */
  189.         public double[] gradient(final double x, final double... parameters) {
  190.             final double[] gradient = new double[secularDegree + 1 + 2 * pulsations.length];

  191.             // secular part
  192.             double xN = 1.0;
  193.             for (int i = 0; i <= secularDegree; ++i) {
  194.                 gradient[i] = xN;
  195.                 xN *= x;
  196.             }

  197.             // harmonic part
  198.             for (int i = 0; i < pulsations.length; ++i) {
  199.                 final SinCos sc = FastMath.sinCos(pulsations[i] * x);
  200.                 gradient[secularDegree + 2 * i + 1] = sc.cos();
  201.                 gradient[secularDegree + 2 * i + 2] = sc.sin();
  202.             }

  203.             return gradient;
  204.         }

  205.     }

  206.     /** Get a copy of the last fitted parameters.
  207.      * @return copy of the last fitted parameters.
  208.      * @see #fit()
  209.      */
  210.     public double[] getFittedParameters() {
  211.         return fitted.clone();
  212.     }

  213.     /** Get fitted osculating value.
  214.      * @param date current date
  215.      * @return osculating value at current date
  216.      */
  217.     public double osculatingValue(final AbsoluteDate date) {
  218.         return truncatedValue(secularDegree, pulsations.length,
  219.                               date.durationFrom(reference), fitted);
  220.     }

  221.     /** Get fitted osculating derivative.
  222.      * @param date current date
  223.      * @return osculating derivative at current date
  224.      */
  225.     public double osculatingDerivative(final AbsoluteDate date) {
  226.         return truncatedDerivative(secularDegree, pulsations.length,
  227.                                    date.durationFrom(reference), fitted);
  228.     }

  229.     /** Get fitted osculating second derivative.
  230.      * @param date current date
  231.      * @return osculating second derivative at current date
  232.      */
  233.     public double osculatingSecondDerivative(final AbsoluteDate date) {
  234.         return truncatedSecondDerivative(secularDegree, pulsations.length,
  235.                                          date.durationFrom(reference), fitted);
  236.     }

  237.     /** Get mean value, truncated to first components.
  238.      * @param date current date
  239.      * @param degree degree of polynomial secular part to consider
  240.      * @param harmonics number of harmonics terms to consider
  241.      * @return mean value at current date
  242.      */
  243.     public double meanValue(final AbsoluteDate date, final int degree, final int harmonics) {
  244.         return truncatedValue(degree, harmonics, date.durationFrom(reference), fitted);
  245.     }

  246.     /** Get mean derivative, truncated to first components.
  247.      * @param date current date
  248.      * @param degree degree of polynomial secular part to consider
  249.      * @param harmonics number of harmonics terms to consider
  250.      * @return mean derivative at current date
  251.      */
  252.     public double meanDerivative(final AbsoluteDate date, final int degree, final int harmonics) {
  253.         return truncatedDerivative(degree, harmonics, date.durationFrom(reference), fitted);
  254.     }

  255.     /** Approximate an already fitted model to polynomial only terms.
  256.      * <p>
  257.      * This method is mainly used in order to combine the large amplitude long
  258.      * periods with the secular part as a new approximate polynomial model over
  259.      * some time range. This should be used rather than simply extracting the
  260.      * polynomial coefficients from {@link #getFittedParameters()} when some
  261.      * periodic terms amplitudes are large (for example Sun resonance effects
  262.      * on local solar time in sun synchronous orbits). In theses cases, the pure
  263.      * polynomial secular part in the coefficients may be far from the mean model.
  264.      * </p>
  265.      * @param combinedDegree desired degree for the combined polynomial
  266.      * @param combinedReference desired reference date for the combined polynomial
  267.      * @param meanDegree degree of polynomial secular part to consider
  268.      * @param meanHarmonics number of harmonics terms to consider
  269.      * @param start start date of the approximation time range
  270.      * @param end end date of the approximation time range
  271.      * @param step sampling step
  272.      * @return coefficients of the approximate polynomial (in increasing degree order),
  273.      * using the user provided reference date
  274.      */
  275.     public double[] approximateAsPolynomialOnly(final int combinedDegree, final AbsoluteDate combinedReference,
  276.                                                 final int meanDegree, final int meanHarmonics,
  277.                                                 final AbsoluteDate start, final AbsoluteDate end,
  278.                                                 final double step) {
  279.         final List<WeightedObservedPoint> points = new ArrayList<>();
  280.         for (AbsoluteDate date = start; date.compareTo(end) < 0; date = date.shiftedBy(step)) {
  281.             points.add(new WeightedObservedPoint(1.0,
  282.                                                  date.durationFrom(combinedReference),
  283.                                                  meanValue(date, meanDegree, meanHarmonics)));
  284.         }
  285.         return PolynomialCurveFitter.create(combinedDegree).fit(points);
  286.     }

  287.     /** Get mean second derivative, truncated to first components.
  288.      * @param date current date
  289.      * @param degree degree of polynomial secular part
  290.      * @param harmonics number of harmonics terms to consider
  291.      * @return mean second derivative at current date
  292.      */
  293.     public double meanSecondDerivative(final AbsoluteDate date, final int degree, final int harmonics) {
  294.         return truncatedSecondDerivative(degree, harmonics, date.durationFrom(reference), fitted);
  295.     }

  296.     /** Get value truncated to first components.
  297.      * @param degree degree of polynomial secular part
  298.      * @param harmonics number of harmonics terms to consider
  299.      * @param time time parameter
  300.      * @param parameters models parameters (must include all parameters,
  301.      * including the ones ignored due to model truncation)
  302.      * @return truncated value
  303.      */
  304.     private double truncatedValue(final int degree, final int harmonics,
  305.                                   final double time, final double... parameters) {

  306.         double value = 0;

  307.         // secular part
  308.         double tN = 1.0;
  309.         for (int i = 0; i <= degree; ++i) {
  310.             value += parameters[i] * tN;
  311.             tN    *= time;
  312.         }

  313.         // harmonic part
  314.         for (int i = 0; i < harmonics; ++i) {
  315.             final SinCos sc = FastMath.sinCos(pulsations[i] * time);
  316.             value += parameters[secularDegree + 2 * i + 1] * sc.cos() +
  317.                      parameters[secularDegree + 2 * i + 2] * sc.sin();
  318.         }

  319.         return value;

  320.     }

  321.     /** Get derivative truncated to first components.
  322.      * @param degree degree of polynomial secular part
  323.      * @param harmonics number of harmonics terms to consider
  324.      * @param time time parameter
  325.      * @param parameters models parameters (must include all parameters,
  326.      * including the ones ignored due to model truncation)
  327.      * @return truncated derivative
  328.      */
  329.     private double truncatedDerivative(final int degree, final int harmonics,
  330.                                        final double time, final double... parameters) {

  331.         double derivative = 0;

  332.         // secular part
  333.         double tN = 1.0;
  334.         for (int i = 1; i <= degree; ++i) {
  335.             derivative += i * parameters[i] * tN;
  336.             tN         *= time;
  337.         }

  338.         // harmonic part
  339.         for (int i = 0; i < harmonics; ++i) {
  340.             final SinCos sc = FastMath.sinCos(pulsations[i] * time);
  341.             derivative += pulsations[i] * (-parameters[secularDegree + 2 * i + 1] * sc.sin() +
  342.                                             parameters[secularDegree + 2 * i + 2] * sc.cos());
  343.         }

  344.         return derivative;

  345.     }

  346.     /** Get second derivative truncated to first components.
  347.      * @param degree degree of polynomial secular part
  348.      * @param harmonics number of harmonics terms to consider
  349.      * @param time time parameter
  350.      * @param parameters models parameters (must include all parameters,
  351.      * including the ones ignored due to model truncation)
  352.      * @return truncated second derivative
  353.      */
  354.     private double truncatedSecondDerivative(final int degree, final int harmonics,
  355.                                              final double time, final double... parameters) {

  356.         double d2 = 0;

  357.         // secular part
  358.         double tN = 1.0;
  359.         for (int i = 2; i <= degree; ++i) {
  360.             d2 += (i - 1) * i * parameters[i] * tN;
  361.             tN *= time;
  362.         }

  363.         // harmonic part
  364.         for (int i = 0; i < harmonics; ++i) {
  365.             final SinCos sc = FastMath.sinCos(pulsations[i] * time);
  366.             d2 += -pulsations[i] * pulsations[i] *
  367.                   (parameters[secularDegree + 2 * i + 1] * sc.cos() +
  368.                    parameters[secularDegree + 2 * i + 2] * sc.sin());
  369.         }

  370.         return d2;

  371.     }

  372. }