SecularAndHarmonic.java

  1. /* Copyright 2002-2022 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<WeightedObservedPoint>();
  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.      * @param date date of the point
  98.      * @param osculatingValue osculating value
  99.      */
  100.     public void addPoint(final AbsoluteDate date, final double osculatingValue) {
  101.         observedPoints.add(new WeightedObservedPoint(1.0, date.durationFrom(reference), osculatingValue));
  102.     }

  103.     /** Get the reference date.
  104.      * @return reference date
  105.      * @see #resetFitting(AbsoluteDate, double...)
  106.      */
  107.     public AbsoluteDate getReferenceDate() {
  108.         return reference;
  109.     }

  110.     /** Get an upper bound of the fitted harmonic amplitude.
  111.      * @return upper bound of the fitted harmonic amplitude
  112.      */
  113.     public double getHarmonicAmplitude() {
  114.         double amplitude = 0;
  115.         for (int i = 0; i < pulsations.length; ++i) {
  116.             amplitude += FastMath.hypot(fitted[secularDegree + 2 * i + 1],
  117.                                         fitted[secularDegree + 2 * i + 2]);
  118.         }
  119.         return amplitude;
  120.     }

  121.     /** Fit parameters.
  122.      * @see #getFittedParameters()
  123.      */
  124.     public void fit() {

  125.         final AbstractCurveFitter fitter = new AbstractCurveFitter() {
  126.             /** {@inheritDoc} */
  127.             @Override
  128.             protected LeastSquaresProblem getProblem(final Collection<WeightedObservedPoint> observations) {
  129.                 // Prepare least-squares problem.
  130.                 final int len = observations.size();
  131.                 final double[] target  = new double[len];
  132.                 final double[] weights = new double[len];

  133.                 int i = 0;
  134.                 for (final WeightedObservedPoint obs : observations) {
  135.                     target[i]  = obs.getY();
  136.                     weights[i] = obs.getWeight();
  137.                     ++i;
  138.                 }

  139.                 final AbstractCurveFitter.TheoreticalValuesFunction model =
  140.                         new AbstractCurveFitter.TheoreticalValuesFunction(new LocalParametricFunction(), observations);

  141.                 // build a new least squares problem set up to fit a secular and harmonic curve to the observed points
  142.                 return new LeastSquaresBuilder().
  143.                         maxEvaluations(Integer.MAX_VALUE).
  144.                         maxIterations(maxIter).
  145.                         checker((iteration, previous, current) -> current.getRMS() <= convergenceRMS).
  146.                         start(fitted).
  147.                         target(target).
  148.                         weight(new DiagonalMatrix(weights)).
  149.                         model(model.getModelFunction(), model.getModelFunctionJacobian()).
  150.                         build();

  151.             }
  152.         };

  153.         fitted = fitter.fit(observedPoints);

  154.     }

  155.     /** Local parametric function used for fitting. */
  156.     private class LocalParametricFunction implements ParametricUnivariateFunction {

  157.         /** {@inheritDoc} */
  158.         public double value(final double x, final double... parameters) {
  159.             return truncatedValue(secularDegree, pulsations.length, x, parameters);
  160.         }

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

  164.             // secular part
  165.             double xN = 1.0;
  166.             for (int i = 0; i <= secularDegree; ++i) {
  167.                 gradient[i] = xN;
  168.                 xN *= x;
  169.             }

  170.             // harmonic part
  171.             for (int i = 0; i < pulsations.length; ++i) {
  172.                 final SinCos sc = FastMath.sinCos(pulsations[i] * x);
  173.                 gradient[secularDegree + 2 * i + 1] = sc.cos();
  174.                 gradient[secularDegree + 2 * i + 2] = sc.sin();
  175.             }

  176.             return gradient;
  177.         }

  178.     }

  179.     /** Get a copy of the last fitted parameters.
  180.      * @return copy of the last fitted parameters.
  181.      * @see #fit()
  182.      */
  183.     public double[] getFittedParameters() {
  184.         return fitted.clone();
  185.     }

  186.     /** Get fitted osculating value.
  187.      * @param date current date
  188.      * @return osculating value at current date
  189.      */
  190.     public double osculatingValue(final AbsoluteDate date) {
  191.         return truncatedValue(secularDegree, pulsations.length,
  192.                               date.durationFrom(reference), fitted);
  193.     }

  194.     /** Get fitted osculating derivative.
  195.      * @param date current date
  196.      * @return osculating derivative at current date
  197.      */
  198.     public double osculatingDerivative(final AbsoluteDate date) {
  199.         return truncatedDerivative(secularDegree, pulsations.length,
  200.                                    date.durationFrom(reference), fitted);
  201.     }

  202.     /** Get fitted osculating second derivative.
  203.      * @param date current date
  204.      * @return osculating second derivative at current date
  205.      */
  206.     public double osculatingSecondDerivative(final AbsoluteDate date) {
  207.         return truncatedSecondDerivative(secularDegree, pulsations.length,
  208.                                          date.durationFrom(reference), fitted);
  209.     }

  210.     /** Get mean value, truncated to first components.
  211.      * @param date current date
  212.      * @param degree degree of polynomial secular part to consider
  213.      * @param harmonics number of harmonics terms to consider
  214.      * @return mean value at current date
  215.      */
  216.     public double meanValue(final AbsoluteDate date, final int degree, final int harmonics) {
  217.         return truncatedValue(degree, harmonics, date.durationFrom(reference), fitted);
  218.     }

  219.     /** Get mean derivative, truncated to first components.
  220.      * @param date current date
  221.      * @param degree degree of polynomial secular part to consider
  222.      * @param harmonics number of harmonics terms to consider
  223.      * @return mean derivative at current date
  224.      */
  225.     public double meanDerivative(final AbsoluteDate date, final int degree, final int harmonics) {
  226.         return truncatedDerivative(degree, harmonics, date.durationFrom(reference), fitted);
  227.     }

  228.     /** Approximate an already fitted model to polynomial only terms.
  229.      * <p>
  230.      * This method is mainly used in order to combine the large amplitude long
  231.      * periods with the secular part as a new approximate polynomial model over
  232.      * some time range. This should be used rather than simply extracting the
  233.      * polynomial coefficients from {@link #getFittedParameters()} when some
  234.      * periodic terms amplitudes are large (for example Sun resonance effects
  235.      * on local solar time in sun synchronous orbits). In theses cases, the pure
  236.      * polynomial secular part in the coefficients may be far from the mean model.
  237.      * </p>
  238.      * @param combinedDegree desired degree for the combined polynomial
  239.      * @param combinedReference desired reference date for the combined polynomial
  240.      * @param meanDegree degree of polynomial secular part to consider
  241.      * @param meanHarmonics number of harmonics terms to consider
  242.      * @param start start date of the approximation time range
  243.      * @param end end date of the approximation time range
  244.      * @param step sampling step
  245.      * @return coefficients of the approximate polynomial (in increasing degree order),
  246.      * using the user provided reference date
  247.      */
  248.     public double[] approximateAsPolynomialOnly(final int combinedDegree, final AbsoluteDate combinedReference,
  249.                                                 final int meanDegree, final int meanHarmonics,
  250.                                                 final AbsoluteDate start, final AbsoluteDate end,
  251.                                                 final double step) {
  252.         final List<WeightedObservedPoint> points = new ArrayList<WeightedObservedPoint>();
  253.         for (AbsoluteDate date = start; date.compareTo(end) < 0; date = date.shiftedBy(step)) {
  254.             points.add(new WeightedObservedPoint(1.0,
  255.                                                  date.durationFrom(combinedReference),
  256.                                                  meanValue(date, meanDegree, meanHarmonics)));
  257.         }
  258.         return PolynomialCurveFitter.create(combinedDegree).fit(points);
  259.     }

  260.     /** Get mean second derivative, truncated to first components.
  261.      * @param date current date
  262.      * @param degree degree of polynomial secular part
  263.      * @param harmonics number of harmonics terms to consider
  264.      * @return mean second derivative at current date
  265.      */
  266.     public double meanSecondDerivative(final AbsoluteDate date, final int degree, final int harmonics) {
  267.         return truncatedSecondDerivative(degree, harmonics, date.durationFrom(reference), fitted);
  268.     }

  269.     /** Get value truncated to first components.
  270.      * @param degree degree of polynomial secular part
  271.      * @param harmonics number of harmonics terms to consider
  272.      * @param time time parameter
  273.      * @param parameters models parameters (must include all parameters,
  274.      * including the ones ignored due to model truncation)
  275.      * @return truncated value
  276.      */
  277.     private double truncatedValue(final int degree, final int harmonics,
  278.                                   final double time, final double... parameters) {

  279.         double value = 0;

  280.         // secular part
  281.         double tN = 1.0;
  282.         for (int i = 0; i <= degree; ++i) {
  283.             value += parameters[i] * tN;
  284.             tN    *= time;
  285.         }

  286.         // harmonic part
  287.         for (int i = 0; i < harmonics; ++i) {
  288.             final SinCos sc = FastMath.sinCos(pulsations[i] * time);
  289.             value += parameters[secularDegree + 2 * i + 1] * sc.cos() +
  290.                      parameters[secularDegree + 2 * i + 2] * sc.sin();
  291.         }

  292.         return value;

  293.     }

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

  304.         double derivative = 0;

  305.         // secular part
  306.         double tN = 1.0;
  307.         for (int i = 1; i <= degree; ++i) {
  308.             derivative += i * parameters[i] * tN;
  309.             tN         *= time;
  310.         }

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

  317.         return derivative;

  318.     }

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

  329.         double d2 = 0;

  330.         // secular part
  331.         double tN = 1.0;
  332.         for (int i = 2; i <= degree; ++i) {
  333.             d2 += (i - 1) * i * parameters[i] * tN;
  334.             tN *= time;
  335.         }

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

  343.         return d2;

  344.     }

  345. }