StateCovarianceKeplerianHermiteInterpolator.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.propagation;

import org.hipparchus.analysis.interpolation.HermiteInterpolator;
import org.hipparchus.linear.BlockRealMatrix;
import org.hipparchus.linear.MatrixUtils;
import org.hipparchus.linear.RealMatrix;
import org.orekit.errors.OrekitInternalError;
import org.orekit.frames.Frame;
import org.orekit.frames.LOFType;
import org.orekit.orbits.Orbit;
import org.orekit.orbits.OrbitType;
import org.orekit.orbits.PositionAngleType;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.TimeInterpolator;
import org.orekit.time.TimeStampedPair;
import org.orekit.utils.CartesianDerivativesFilter;

import java.util.ArrayList;
import java.util.List;

/**
 * State covariance Keplerian quintic interpolator.
 * <p>
 * Its purpose is to interpolate state covariance between tabulated state covariances using polynomial interpolation. To do
 * so, it uses a {@link HermiteInterpolator} and compute the first and second order derivatives at tabulated states assuming
 * a standard Keplerian motion depending on given derivatives filter.
 * <p>
 * It gives very accurate results as explained <a
 * href="https://orekit.org/doc/technical-notes/Implementation_of_covariance_interpolation_in_Orekit.pdf">here</a>. In the
 * very poorly tracked test case evolving in a highly dynamical environment mentioned in the linked thread, the user can
 * expect at worst errors of less than 0.2% in position sigmas and less than 0.35% in velocity sigmas with steps of 40mn
 * between tabulated values.
 * <p>
 * However, note that this method does not guarantee the positive definiteness of the computed state covariance as opposed to
 * {@link StateCovarianceBlender}.
 *
 * @author Vincent Cucchietti
 * @see HermiteInterpolator
 * @see StateCovarianceBlender
 */
public class StateCovarianceKeplerianHermiteInterpolator extends AbstractStateCovarianceInterpolator {

    /**
     * Filter defining if only the state covariance value are used or if first or/and second Keplerian derivatives should be
     * used.
     */
    private final CartesianDerivativesFilter filter;

    /**
     * Constructor using an output local orbital frame and :
     * <ul>
     *     <li>Default number of interpolation points of {@code DEFAULT_INTERPOLATION_POINTS}</li>
     *     <li>Default extrapolation threshold value ({@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s)</li>
     *     <li>Use of position and two time derivatives during interpolation</li>
     * </ul>
     * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
     * points (about 10-20 points) in order to avoid <a href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
     * phenomenon</a> and numerical problems (including NaN appearing).
     * <p>
     * <b>BEWARE:</b> If the output local orbital frame is not considered pseudo-inertial, all the covariance components
     * related to the velocity will be poorly interpolated. <b>Only the position covariance should be considered in this
     * case.</b>
     *
     * @param orbitInterpolator orbit interpolator
     * @param outLOF output local orbital frame
     */
    public StateCovarianceKeplerianHermiteInterpolator(final TimeInterpolator<Orbit> orbitInterpolator,
                                                       final LOFType outLOF) {
        this(DEFAULT_INTERPOLATION_POINTS, orbitInterpolator, outLOF);
    }

    /**
     * Constructor using an output local orbital frame and :
     * <ul>
     *     <li>Default extrapolation threshold value ({@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s)</li>
     *     <li>Use of position and two time derivatives during interpolation</li>
     * </ul>
     * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
     * points (about 10-20 points) in order to avoid <a href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
     * phenomenon</a> and numerical problems (including NaN appearing).
     * <p>
     * <b>BEWARE:</b> If the output local orbital frame is not considered pseudo-inertial, all the covariance components
     * related to the velocity will be poorly interpolated. <b>Only the position covariance should be considered in this
     * case.</b>
     *
     * @param interpolationPoints number of interpolation points
     * @param orbitInterpolator orbit interpolator
     * @param outLOF output local orbital frame
     */
    public StateCovarianceKeplerianHermiteInterpolator(final int interpolationPoints,
                                                       final TimeInterpolator<Orbit> orbitInterpolator,
                                                       final LOFType outLOF) {
        this(interpolationPoints, orbitInterpolator, CartesianDerivativesFilter.USE_PVA,
             outLOF);
    }

    /**
     * Constructor using an output local orbital frame and :
     * <ul>
     *     <li>Use of position and two time derivatives during interpolation</li>
     * </ul>
     * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
     * points (about 10-20 points) in order to avoid <a href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
     * phenomenon</a> and numerical problems (including NaN appearing).
     * <p>
     * <b>BEWARE:</b> If the output local orbital frame is not considered pseudo-inertial, all the covariance components
     * related to the velocity will be poorly interpolated. <b>Only the position covariance should be considered in this
     * case.</b>
     *
     * @param interpolationPoints number of interpolation points
     * @param orbitInterpolator orbit interpolator
     * @param outLOF output local orbital frame
     * @param filter filter for derivatives from the sample to use in position-velocity-acceleration interpolation
     */
    public StateCovarianceKeplerianHermiteInterpolator(final int interpolationPoints,
                                                       final TimeInterpolator<Orbit> orbitInterpolator,
                                                       final CartesianDerivativesFilter filter,
                                                       final LOFType outLOF) {
        this(interpolationPoints, DEFAULT_EXTRAPOLATION_THRESHOLD_SEC, orbitInterpolator, filter, outLOF);
    }

    /**
     * Constructor using an output local orbital frame.
     * <p>
     * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
     * points (about 10-20 points) in order to avoid <a href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
     * phenomenon</a> and numerical problems (including NaN appearing).
     * <p>
     * <b>BEWARE:</b> If the output local orbital frame is not considered pseudo-inertial, all the covariance components
     * related to the velocity will be poorly interpolated. <b>Only the position covariance should be considered in this
     * case.</b>
     *
     * @param interpolationPoints number of interpolation points
     * @param extrapolationThreshold extrapolation threshold beyond which the propagation will fail
     * @param orbitInterpolator orbit interpolator
     * @param outLOF output local orbital frame
     * @param filter filter defining if only the state covariance value are used or if first or/and second Keplerian
     * derivatives should be used during the interpolation.
     */
    public StateCovarianceKeplerianHermiteInterpolator(final int interpolationPoints, final double extrapolationThreshold,
                                                       final TimeInterpolator<Orbit> orbitInterpolator,
                                                       final CartesianDerivativesFilter filter, final LOFType outLOF) {
        super(interpolationPoints, extrapolationThreshold, orbitInterpolator, outLOF);
        this.filter = filter;
    }

    /**
     * Constructor using an output frame and :
     * <ul>
     *     <li>Default number of interpolation points of {@code DEFAULT_INTERPOLATION_POINTS}</li>
     *     <li>Default extrapolation threshold value ({@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s)</li>
     *     <li>Use of position and two time derivatives during interpolation</li>
     * </ul>
     * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
     * points (about 10-20 points) in order to avoid <a href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
     * phenomenon</a> and numerical problems (including NaN appearing).
     *
     * @param orbitInterpolator orbit interpolator
     * @param outFrame output frame
     * @param outOrbitType output orbit type
     * @param outPositionAngleType output position angle
     */
    public StateCovarianceKeplerianHermiteInterpolator(final TimeInterpolator<Orbit> orbitInterpolator, final Frame outFrame,
                                                       final OrbitType outOrbitType, final PositionAngleType outPositionAngleType) {
        this(DEFAULT_INTERPOLATION_POINTS, orbitInterpolator, outFrame, outOrbitType, outPositionAngleType);
    }

    /**
     * Constructor using an output frame and :
     * <ul>
     *     <li>Default number of interpolation points of {@code DEFAULT_INTERPOLATION_POINTS}</li>
     *     <li>Use of position and two time derivatives during interpolation</li>
     * </ul>
     * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
     * points (about 10-20 points) in order to avoid <a href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
     * phenomenon</a> and numerical problems (including NaN appearing).
     *
     * @param interpolationPoints number of interpolation points
     * @param orbitInterpolator orbit interpolator
     * @param outFrame output frame
     * @param outOrbitType output orbit type
     * @param outPositionAngleType output position angle
     */
    public StateCovarianceKeplerianHermiteInterpolator(final int interpolationPoints,
                                                       final TimeInterpolator<Orbit> orbitInterpolator, final Frame outFrame,
                                                       final OrbitType outOrbitType, final PositionAngleType outPositionAngleType) {
        this(interpolationPoints, DEFAULT_EXTRAPOLATION_THRESHOLD_SEC, orbitInterpolator, CartesianDerivativesFilter.USE_PVA,
             outFrame, outOrbitType, outPositionAngleType);
    }

    /**
     * Constructor using an output frame and :
     * <ul>
     *     <li>Default extrapolation threshold value ({@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s)</li>
     * </ul>
     * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
     * points (about 10-20 points) in order to avoid <a href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
     * phenomenon</a> and numerical problems (including NaN appearing).
     *
     * @param interpolationPoints number of interpolation points
     * @param orbitInterpolator orbit interpolator
     * @param filter filter defining if only the state covariance value are used or if first or/and second Keplerian
     * derivatives should be used during the interpolation.
     * @param outFrame output frame
     * @param outOrbitType output orbit type
     * @param outPositionAngleType output position angle
     */
    public StateCovarianceKeplerianHermiteInterpolator(final int interpolationPoints,
                                                       final TimeInterpolator<Orbit> orbitInterpolator,
                                                       final CartesianDerivativesFilter filter, final Frame outFrame,
                                                       final OrbitType outOrbitType, final PositionAngleType outPositionAngleType) {
        this(interpolationPoints, DEFAULT_EXTRAPOLATION_THRESHOLD_SEC, orbitInterpolator, filter, outFrame, outOrbitType,
                outPositionAngleType);
    }

    /**
     * Constructor using an output frame.
     * <p>
     * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
     * points (about 10-20 points) in order to avoid <a href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
     * phenomenon</a> and numerical problems (including NaN appearing).
     *
     * @param interpolationPoints number of interpolation points
     * @param extrapolationThreshold extrapolation threshold beyond which the propagation will fail
     * @param orbitInterpolator orbit interpolator
     * @param filter filter defining if only the state covariance value are used or if first or/and second Keplerian
     * derivatives should be used during the interpolation.
     * @param outFrame output frame
     * @param outOrbitType output orbit type
     * @param outPositionAngleType output position angle
     */
    public StateCovarianceKeplerianHermiteInterpolator(final int interpolationPoints, final double extrapolationThreshold,
                                                       final TimeInterpolator<Orbit> orbitInterpolator,
                                                       final CartesianDerivativesFilter filter, final Frame outFrame,
                                                       final OrbitType outOrbitType, final PositionAngleType outPositionAngleType) {
        super(interpolationPoints, extrapolationThreshold, orbitInterpolator, outFrame, outOrbitType, outPositionAngleType);
        this.filter = filter;
    }

    /** Get Filter defining if only the state covariance value are used or if first or/and second Keplerian derivatives
     * should be used.
     * @return Filter defining if only the state covariance value are used or if first or/and second Keplerian derivatives
     * should be used.
     */
    public CartesianDerivativesFilter getFilter() {
        return filter;
    }

    /** {@inheritDoc} */
    @Override
    protected StateCovariance computeInterpolatedCovarianceInOrbitFrame(
            final List<TimeStampedPair<Orbit, StateCovariance>> uncertainStates,
            final Orbit interpolatedOrbit) {

        // Compute and combine covariances value and time derivatives
        final List<double[][][]> covarianceValueAndDerivativesList = new ArrayList<>();
        for (TimeStampedPair<Orbit, StateCovariance> uncertainState : uncertainStates) {
            final double[][][] currentCovarianceValueAndDerivatives =
                    computeAndCombineCovarianceValueAndDerivatives(uncertainState, interpolatedOrbit);

            covarianceValueAndDerivativesList.add(currentCovarianceValueAndDerivatives);
        }

        // Interpolate covariance matrix in equinoctial elements
        final RealMatrix interpolatedCovarianceMatrixInEqui =
                computeInterpolatedStateCovariance(interpolatedOrbit.getDate(),
                                                   uncertainStates,
                                                   covarianceValueAndDerivativesList);

        return new StateCovariance(interpolatedCovarianceMatrixInEqui,
                                   interpolatedOrbit.getDate(), interpolatedOrbit.getFrame(),
                                   OrbitType.EQUINOCTIAL, DEFAULT_POSITION_ANGLE);
    }

    /**
     * Compute and combine state covariance matrix and its two Keplerian time derivatives.
     *
     * @param uncertainState orbit and its associated covariance
     * @param interpolatedOrbit interpolated orbit
     *
     * @return state covariance matrix and its two time derivatives
     */
    private double[][][] computeAndCombineCovarianceValueAndDerivatives(
            final TimeStampedPair<Orbit, StateCovariance> uncertainState, final Orbit interpolatedOrbit) {

        // Get orbit and associated covariance
        final Orbit           orbit      = uncertainState.getFirst();
        final StateCovariance covariance = uncertainState.getSecond();

        // Express covariance in interpolated orbit frame for consistency among the sample
        final StateCovariance covarianceInOrbitFrame = covariance.changeCovarianceFrame(orbit, interpolatedOrbit.getFrame());

        // Convert to equinoctial elements to avoid singularities
        final StateCovariance covarianceInOrbitFrameInEqui =
                covarianceInOrbitFrame.changeCovarianceType(orbit, OrbitType.EQUINOCTIAL, DEFAULT_POSITION_ANGLE);

        // Get matrix
        final RealMatrix covarianceInOrbitFrameInEquiMatrix = covarianceInOrbitFrameInEqui.getMatrix();

        // Compute covariance first and second time derivative according to instance filter
        final int dim = StateCovariance.STATE_DIMENSION;

        final RealMatrix covarianceMatrixFirstDerInKep;
        final RealMatrix covarianceMatrixSecondDerInKep;

        switch (filter) {
            case USE_P:
                covarianceMatrixFirstDerInKep = MatrixUtils.createRealMatrix(dim, dim);
                covarianceMatrixSecondDerInKep = MatrixUtils.createRealMatrix(dim, dim);
                break;
            case USE_PV:
                covarianceMatrixFirstDerInKep = computeCovarianceFirstDerivative(orbit, covarianceInOrbitFrameInEquiMatrix);
                covarianceMatrixSecondDerInKep = MatrixUtils.createRealMatrix(dim, dim);
                break;
            case USE_PVA:
                covarianceMatrixFirstDerInKep = computeCovarianceFirstDerivative(orbit, covarianceInOrbitFrameInEquiMatrix);
                covarianceMatrixSecondDerInKep =
                        computeCovarianceSecondDerivative(orbit, covarianceInOrbitFrameInEquiMatrix);
                break;
            default:
                // Should never happen
                throw new OrekitInternalError(null);
        }

        // Combine and output the state covariance and its first two time derivative in a single array
        return combineCovarianceValueAndDerivatives(covarianceInOrbitFrameInEquiMatrix,
                                                    covarianceMatrixFirstDerInKep,
                                                    covarianceMatrixSecondDerInKep);
    }

    /**
     * Compute interpolated state covariance in equinoctial elements using a Hermite interpolator.
     *
     * @param interpolationDate interpolation date
     * @param uncertainStates list of orbits and their associated covariances
     * @param covarianceValueAndDerivativesList list of covariances and their associated first and second time derivatives
     *
     * @return interpolated state covariance in equinoctial elements
     *
     * @see HermiteInterpolator
     */
    private RealMatrix computeInterpolatedStateCovariance(final AbsoluteDate interpolationDate,
                                                          final List<TimeStampedPair<Orbit, StateCovariance>> uncertainStates,
                                                          final List<double[][][]> covarianceValueAndDerivativesList) {

        final RealMatrix interpolatedCovarianceMatrix = new BlockRealMatrix(new double[ROW_DIM][COLUMN_DIM]);

        // Interpolate each element in the covariance matrix
        for (int i = 0; i < ROW_DIM; i++) {
            for (int j = 0; j < COLUMN_DIM; j++) {

                // Create an interpolator for each element
                final HermiteInterpolator tempInterpolator = new HermiteInterpolator();

                // Fill interpolator with all samples value and associated derivatives
                for (int k = 0; k < uncertainStates.size(); k++) {
                    final TimeStampedPair<Orbit, StateCovariance> currentUncertainStates = uncertainStates.get(k);

                    final double[][][] currentCovarianceValueAndDerivatives = covarianceValueAndDerivativesList.get(k);

                    final double deltaT = currentUncertainStates.getDate().durationFrom(interpolationDate);

                    addSampleToInterpolator(tempInterpolator, deltaT, currentCovarianceValueAndDerivatives[i][j]);
                }

                // Interpolate
                interpolatedCovarianceMatrix.setEntry(i, j, tempInterpolator.value(0)[0]);
            }
        }

        return interpolatedCovarianceMatrix;
    }

    /**
     * Add sample to given interpolator.
     *
     * @param interpolator interpolator to add sample to
     * @param deltaT abscissa for interpolation
     * @param valueAndDerivatives value and associated derivatives to add
     */
    private void addSampleToInterpolator(final HermiteInterpolator interpolator, final double deltaT,
                                         final double[] valueAndDerivatives) {
        switch (filter) {
            case USE_P:
                interpolator.addSamplePoint(deltaT, new double[] { valueAndDerivatives[0] });
                break;
            case USE_PV:
                interpolator.addSamplePoint(deltaT,
                                            new double[] { valueAndDerivatives[0] },
                                            new double[] { valueAndDerivatives[1] });
                break;
            case USE_PVA:
                interpolator.addSamplePoint(deltaT,
                                            new double[] { valueAndDerivatives[0] },
                                            new double[] { valueAndDerivatives[1] },
                                            new double[] { valueAndDerivatives[2] });
                break;
            default:
                // Should never happen
                throw new OrekitInternalError(null);
        }
    }

    /**
     * Compute state covariance first Keplerian time derivative.
     *
     * @param orbit orbit
     * @param covarianceMatrixInEqui state covariance matrix in equinoctial elements
     *
     * @return state covariance first time derivative
     */
    private RealMatrix computeCovarianceFirstDerivative(final Orbit orbit,
                                                        final RealMatrix covarianceMatrixInEqui) {

        final RealMatrix covarianceFirstDerivative = new BlockRealMatrix(ROW_DIM, COLUMN_DIM);

        // Compute common term used in papers
        final double m = orbit.getMeanAnomalyDotWrtA();

        // Compute first time derivative of each element in the covariance matrix
        for (int i = 0; i < ROW_DIM; i++) {
            for (int j = 0; j < COLUMN_DIM; j++) {
                if (i != 5 && j != 5) {
                    covarianceFirstDerivative.setEntry(i, j, 0);
                }
                else if (i == 5 && j != 5) {

                    final double value = covarianceMatrixInEqui.getEntry(0, j) * m;

                    covarianceFirstDerivative.setEntry(i, j, value);
                    covarianceFirstDerivative.setEntry(j, i, value);
                }
                else {
                    final double value = 2 * covarianceMatrixInEqui.getEntry(0, 5) * m;

                    covarianceFirstDerivative.setEntry(i, j, value);
                }
            }
        }

        return covarianceFirstDerivative;

    }

    /**
     * Compute state covariance second Keplerian time derivative.
     *
     * @param orbit orbit
     * @param covarianceMatrixInEqui state covariance matrix in equinoctial elements
     *
     * @return state covariance second time derivative
     */
    private RealMatrix computeCovarianceSecondDerivative(final Orbit orbit,
                                                         final RealMatrix covarianceMatrixInEqui) {

        final RealMatrix covarianceSecondDerivative = new BlockRealMatrix(ROW_DIM, COLUMN_DIM);

        // Compute common term used in papers
        final double m = orbit.getMeanAnomalyDotWrtA();

        // Compute second time derivative of each element in the covariance matrix
        for (int i = 0; i < ROW_DIM; i++) {
            for (int j = 0; j < COLUMN_DIM; j++) {
                if (i == 5 && j == 5) {

                    final double value = 2 * covarianceMatrixInEqui.getEntry(0, 0) * m * m;

                    covarianceSecondDerivative.setEntry(i, j, value);
                }
                else {
                    covarianceSecondDerivative.setEntry(i, j, 0);
                }
            }
        }

        return covarianceSecondDerivative;

    }

    /**
     * Combine state covariance matrix and its two Keplerian time derivatives.
     *
     * @param covarianceMatrixInEqui covariance matrix in equinoctial elements
     * @param covarianceMatrixFirstDerInEqui covariance matrix first time derivative in equinoctial elements
     * @param covarianceMatrixSecondDerInEqui covariance matrix second time derivative in equinoctial elements
     *
     * @return state covariance matrix and its two time derivatives
     */
    private double[][][] combineCovarianceValueAndDerivatives(final RealMatrix covarianceMatrixInEqui,
                                                              final RealMatrix covarianceMatrixFirstDerInEqui,
                                                              final RealMatrix covarianceMatrixSecondDerInEqui) {

        final double[][][] covarianceValueAndDerivativesArray = new double[ROW_DIM][COLUMN_DIM][3];

        // Combine covariance and its first two time derivatives in a single 3D array
        for (int i = 0; i < ROW_DIM; i++) {
            for (int j = 0; j < COLUMN_DIM; j++) {
                covarianceValueAndDerivativesArray[i][j][0] = covarianceMatrixInEqui.getEntry(i, j);
                covarianceValueAndDerivativesArray[i][j][1] = covarianceMatrixFirstDerInEqui.getEntry(i, j);
                covarianceValueAndDerivativesArray[i][j][2] = covarianceMatrixSecondDerInEqui.getEntry(i, j);
            }
        }

        return covarianceValueAndDerivativesArray;
    }
}