FieldOrbitHermiteInterpolator.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.orbits;

import org.hipparchus.CalculusFieldElement;
import org.hipparchus.Field;
import org.hipparchus.analysis.interpolation.FieldHermiteInterpolator;
import org.hipparchus.util.MathArrays;
import org.hipparchus.util.MathUtils;
import org.orekit.errors.OrekitInternalError;
import org.orekit.frames.Frame;
import org.orekit.time.FieldAbsoluteDate;
import org.orekit.time.FieldTimeInterpolator;
import org.orekit.utils.CartesianDerivativesFilter;
import org.orekit.utils.TimeStampedFieldPVCoordinates;
import org.orekit.utils.TimeStampedFieldPVCoordinatesHermiteInterpolator;

import java.util.List;
import java.util.stream.Stream;

/**
 * Class using a Hermite interpolator to interpolate orbits.
 * <p>
 * Depending on given sample orbit type, the interpolation may differ :
 * <ul>
 *    <li>For Keplerian, Circular and Equinoctial orbits, the interpolated instance is created by polynomial Hermite
 *    interpolation, using derivatives when available. </li>
 *    <li>For Cartesian orbits, the interpolated instance is created using the cartesian derivatives filter given at
 *    instance construction. Hence, it will fall back to Lagrange interpolation if this instance has been designed to not
 *    use derivatives.
 * </ul>
 * <p>
 * In any case, 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 <KK> type of the field element
 *
 * @author Luc Maisonobe
 * @author Vincent Cucchietti
 * @see FieldOrbit
 * @see FieldHermiteInterpolator
 */
public class FieldOrbitHermiteInterpolator<KK extends CalculusFieldElement<KK>> extends AbstractFieldOrbitInterpolator<KK> {

    /** Filter for derivatives from the sample to use in position-velocity-acceleration interpolation. */
    private final CartesianDerivativesFilter pvaFilter;

    /** Field of the elements. */
    private Field<KK> field;

    /** Fielded zero. */
    private KK zero;

    /** Fielded one. */
    private KK one;

    /**
     * Constructor with :
     * <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 outputInertialFrame output inertial frame
     */
    public FieldOrbitHermiteInterpolator(final Frame outputInertialFrame) {
        this(DEFAULT_INTERPOLATION_POINTS, outputInertialFrame);
    }

    /**
     * Constructor with :
     * <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).
     *
     * @param interpolationPoints number of interpolation points
     * @param outputInertialFrame output inertial frame
     */
    public FieldOrbitHermiteInterpolator(final int interpolationPoints, final Frame outputInertialFrame) {
        this(interpolationPoints, outputInertialFrame, CartesianDerivativesFilter.USE_PVA);
    }

    /**
     * Constructor with default extrapolation threshold value ({@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s).
     * <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 outputInertialFrame output inertial frame
     * @param pvaFilter filter for derivatives from the sample to use in position-velocity-acceleration interpolation
     */
    public FieldOrbitHermiteInterpolator(final int interpolationPoints, final Frame outputInertialFrame,
                                         final CartesianDerivativesFilter pvaFilter) {
        this(interpolationPoints, DEFAULT_EXTRAPOLATION_THRESHOLD_SEC, outputInertialFrame, pvaFilter);
    }

    /**
     * Constructor.
     * <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 outputInertialFrame output inertial frame
     * @param pvaFilter filter for derivatives from the sample to use in position-velocity-acceleration interpolation
     */
    public FieldOrbitHermiteInterpolator(final int interpolationPoints, final double extrapolationThreshold,
                                         final Frame outputInertialFrame, final CartesianDerivativesFilter pvaFilter) {
        super(interpolationPoints, extrapolationThreshold, outputInertialFrame);
        this.pvaFilter = pvaFilter;
    }

    /** Get filter for derivatives from the sample to use in position-velocity-acceleration interpolation.
     * @return filter for derivatives from the sample to use in position-velocity-acceleration interpolation
     */
    public CartesianDerivativesFilter getPVAFilter() {
        return pvaFilter;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Depending on given sample orbit type, the interpolation may differ :
     * <ul>
     *    <li>For Keplerian, Circular and Equinoctial orbits, the interpolated instance is created by polynomial Hermite
     *    interpolation, using derivatives when available. </li>
     *    <li>For Cartesian orbits, the interpolated instance is created using the cartesian derivatives filter given at
     *    instance construction. Hence, it will fall back to Lagrange interpolation if this instance has been designed to not
     *    use derivatives.
     * </ul>
     * If orbit interpolation on large samples is needed, using the {@link
     * org.orekit.propagation.analytical.Ephemeris} class is a better way than using this
     * low-level interpolation. The Ephemeris class automatically handles selection of
     * a neighboring sub-sample with a predefined number of point from a large global sample
     * in a thread-safe way.
     *
     * @param interpolationData interpolation data
     *
     * @return interpolated instance for given interpolation data
     */
    @Override
    protected FieldOrbit<KK> interpolate(final InterpolationData interpolationData) {

        // Get interpolation date
        final FieldAbsoluteDate<KK> interpolationDate = interpolationData.getInterpolationDate();

        // Get orbit sample
        final List<FieldOrbit<KK>> sample = interpolationData.getNeighborList();

        // Get first entry
        final FieldOrbit<KK> firstEntry = sample.get(0);

        // Get orbit type for interpolation
        final OrbitType orbitType = firstEntry.getType();

        // Extract field
        this.field = firstEntry.getA().getField();
        this.zero  = field.getZero();
        this.one   = field.getOne();

        if (orbitType == OrbitType.CARTESIAN) {
            return interpolateCartesian(interpolationDate, sample);
        }
        else {
            return interpolateCommon(interpolationDate, sample, orbitType);
        }

    }

    /**
     * Interpolate Cartesian orbit using specific method for Cartesian orbit.
     *
     * @param interpolationDate interpolation date
     * @param sample orbits sample
     *
     * @return interpolated Cartesian orbit
     */
    private FieldCartesianOrbit<KK> interpolateCartesian(final FieldAbsoluteDate<KK> interpolationDate,
                                                         final List<FieldOrbit<KK>> sample) {

        // Create time stamped position-velocity-acceleration Hermite interpolator
        final FieldTimeInterpolator<TimeStampedFieldPVCoordinates<KK>, KK> interpolator =
                new TimeStampedFieldPVCoordinatesHermiteInterpolator<>(getNbInterpolationPoints(),
                                                                       getExtrapolationThreshold(),
                                                                       pvaFilter);

        // Convert sample to stream
        final Stream<FieldOrbit<KK>> sampleStream = sample.stream();

        // Map time stamped position-velocity-acceleration coordinates
        final Stream<TimeStampedFieldPVCoordinates<KK>> sampleTimeStampedPV = sampleStream.map(FieldOrbit::getPVCoordinates);

        // Interpolate PVA
        final TimeStampedFieldPVCoordinates<KK> interpolated =
                interpolator.interpolate(interpolationDate, sampleTimeStampedPV);

        // Use first entry gravitational parameter
        final KK mu = sample.get(0).getMu();

        return new FieldCartesianOrbit<>(interpolated, getOutputInertialFrame(), interpolationDate, mu);
    }

    /**
     * Method gathering common parts of interpolation between circular, equinoctial and keplerian orbit.
     *
     * @param interpolationDate interpolation date
     * @param orbits orbits sample
     * @param orbitType interpolation method to use
     *
     * @return interpolated orbit
     */
    private FieldOrbit<KK> interpolateCommon(final FieldAbsoluteDate<KK> interpolationDate,
                                             final List<FieldOrbit<KK>> orbits,
                                             final OrbitType orbitType) {

        // First pass to check if derivatives are available throughout the sample
        boolean useDerivatives = true;
        for (final FieldOrbit<KK> orbit : orbits) {
            useDerivatives = useDerivatives && orbit.hasNonKeplerianAcceleration();
        }

        // Use first entry gravitational parameter
        final KK mu = orbits.get(0).getMu();

        // Interpolate and build a new instance
        final KK[][] interpolated;
        switch (orbitType) {
            case CIRCULAR:
                interpolated = interpolateCircular(interpolationDate, orbits, useDerivatives);
                return new FieldCircularOrbit<>(interpolated[0][0], interpolated[0][1], interpolated[0][2],
                                                interpolated[0][3], interpolated[0][4], interpolated[0][5],
                                                interpolated[1][0], interpolated[1][1], interpolated[1][2],
                                                interpolated[1][3], interpolated[1][4], interpolated[1][5],
                                                PositionAngleType.MEAN, getOutputInertialFrame(), interpolationDate, mu);
            case KEPLERIAN:
                interpolated = interpolateKeplerian(interpolationDate, orbits, useDerivatives);
                return new FieldKeplerianOrbit<>(interpolated[0][0], interpolated[0][1], interpolated[0][2],
                                                 interpolated[0][3], interpolated[0][4], interpolated[0][5],
                                                 interpolated[1][0], interpolated[1][1], interpolated[1][2],
                                                 interpolated[1][3], interpolated[1][4], interpolated[1][5],
                                                 PositionAngleType.MEAN, getOutputInertialFrame(), interpolationDate, mu);
            case EQUINOCTIAL:
                interpolated = interpolateEquinoctial(interpolationDate, orbits, useDerivatives);
                return new FieldEquinoctialOrbit<>(interpolated[0][0], interpolated[0][1], interpolated[0][2],
                                                   interpolated[0][3], interpolated[0][4], interpolated[0][5],
                                                   interpolated[1][0], interpolated[1][1], interpolated[1][2],
                                                   interpolated[1][3], interpolated[1][4], interpolated[1][5],
                                                   PositionAngleType.MEAN, getOutputInertialFrame(), interpolationDate, mu);
            default:
                // Should never happen
                throw new OrekitInternalError(null);
        }

    }

    /**
     * Build interpolating functions for circular orbit parameters.
     *
     * @param interpolationDate interpolation date
     * @param orbits orbits sample
     * @param useDerivatives flag defining if derivatives are available throughout the sample
     *
     * @return interpolating functions for circular orbit parameters
     */
    private KK[][] interpolateCircular(final FieldAbsoluteDate<KK> interpolationDate, final List<FieldOrbit<KK>> orbits,
                                       final boolean useDerivatives) {

        // set up an interpolator
        final FieldHermiteInterpolator<KK> interpolator = new FieldHermiteInterpolator<>();

        // second pass to feed interpolator
        FieldAbsoluteDate<KK> previousDate   = null;
        KK                    previousRAAN   = zero.add(Double.NaN);
        KK                    previousAlphaM = zero.add(Double.NaN);
        for (final FieldOrbit<KK> orbit : orbits) {
            final FieldCircularOrbit<KK> circ = (FieldCircularOrbit<KK>) OrbitType.CIRCULAR.convertType(orbit);
            final KK                     continuousRAAN;
            final KK                     continuousAlphaM;
            if (previousDate == null) {
                continuousRAAN   = circ.getRightAscensionOfAscendingNode();
                continuousAlphaM = circ.getAlphaM();
            }
            else {
                final KK dt       = circ.getDate().durationFrom(previousDate);
                final KK keplerAM = previousAlphaM.add(circ.getKeplerianMeanMotion().multiply(dt));
                continuousRAAN   = MathUtils.normalizeAngle(circ.getRightAscensionOfAscendingNode(), previousRAAN);
                continuousAlphaM = MathUtils.normalizeAngle(circ.getAlphaM(), keplerAM);
            }
            previousDate   = circ.getDate();
            previousRAAN   = continuousRAAN;
            previousAlphaM = continuousAlphaM;
            final KK[] toAdd = MathArrays.buildArray(one.getField(), 6);
            toAdd[0] = circ.getA();
            toAdd[1] = circ.getCircularEx();
            toAdd[2] = circ.getCircularEy();
            toAdd[3] = circ.getI();
            toAdd[4] = continuousRAAN;
            toAdd[5] = continuousAlphaM;
            if (useDerivatives) {
                final KK[] toAddDot = MathArrays.buildArray(one.getField(), 6);
                toAddDot[0] = circ.getADot();
                toAddDot[1] = circ.getCircularExDot();
                toAddDot[2] = circ.getCircularEyDot();
                toAddDot[3] = circ.getIDot();
                toAddDot[4] = circ.getRightAscensionOfAscendingNodeDot();
                toAddDot[5] = circ.getAlphaMDot();
                interpolator.addSamplePoint(circ.getDate().durationFrom(interpolationDate),
                                            toAdd, toAddDot);
            }
            else {
                interpolator.addSamplePoint(circ.getDate().durationFrom(interpolationDate),
                                            toAdd);
            }
        }

        return interpolator.derivatives(zero, 1);
    }

    /**
     * Build interpolating functions for keplerian orbit parameters.
     *
     * @param interpolationDate interpolation date
     * @param orbits orbits sample
     * @param useDerivatives flag defining if derivatives are available throughout the sample
     *
     * @return interpolating functions for keplerian orbit parameters
     */
    private KK[][] interpolateKeplerian(final FieldAbsoluteDate<KK> interpolationDate, final List<FieldOrbit<KK>> orbits,
                                        final boolean useDerivatives) {

        // Set up an interpolator
        final FieldHermiteInterpolator<KK> interpolator = new FieldHermiteInterpolator<>();

        // Second pass to feed interpolator
        FieldAbsoluteDate<KK> previousDate = null;
        KK                    previousPA   = zero.add(Double.NaN);
        KK                    previousRAAN = zero.add(Double.NaN);
        KK                    previousM    = zero.add(Double.NaN);
        for (final FieldOrbit<KK> orbit : orbits) {
            final FieldKeplerianOrbit<KK> kep = (FieldKeplerianOrbit<KK>) OrbitType.KEPLERIAN.convertType(orbit);
            final KK                      continuousPA;
            final KK                      continuousRAAN;
            final KK                      continuousM;
            if (previousDate == null) {
                continuousPA   = kep.getPerigeeArgument();
                continuousRAAN = kep.getRightAscensionOfAscendingNode();
                continuousM    = kep.getMeanAnomaly();
            }
            else {
                final KK dt      = kep.getDate().durationFrom(previousDate);
                final KK keplerM = previousM.add(kep.getKeplerianMeanMotion().multiply(dt));
                continuousPA   = MathUtils.normalizeAngle(kep.getPerigeeArgument(), previousPA);
                continuousRAAN = MathUtils.normalizeAngle(kep.getRightAscensionOfAscendingNode(), previousRAAN);
                continuousM    = MathUtils.normalizeAngle(kep.getMeanAnomaly(), keplerM);
            }
            previousDate = kep.getDate();
            previousPA   = continuousPA;
            previousRAAN = continuousRAAN;
            previousM    = continuousM;
            final KK[] toAdd = MathArrays.buildArray(field, 6);
            toAdd[0] = kep.getA();
            toAdd[1] = kep.getE();
            toAdd[2] = kep.getI();
            toAdd[3] = continuousPA;
            toAdd[4] = continuousRAAN;
            toAdd[5] = continuousM;
            if (useDerivatives) {
                final KK[] toAddDot = MathArrays.buildArray(field, 6);
                toAddDot[0] = kep.getADot();
                toAddDot[1] = kep.getEDot();
                toAddDot[2] = kep.getIDot();
                toAddDot[3] = kep.getPerigeeArgumentDot();
                toAddDot[4] = kep.getRightAscensionOfAscendingNodeDot();
                toAddDot[5] = kep.getMeanAnomalyDot();
                interpolator.addSamplePoint(kep.getDate().durationFrom(interpolationDate),
                                            toAdd, toAddDot);
            }
            else {
                interpolator.addSamplePoint(this.zero.add(kep.getDate().durationFrom(interpolationDate)),
                                            toAdd);
            }
        }

        return interpolator.derivatives(zero, 1);
    }

    /**
     * Build interpolating functions for equinoctial orbit parameters.
     *
     * @param interpolationDate interpolation date
     * @param orbits orbits sample
     * @param useDerivatives flag defining if derivatives are available throughout the sample
     *
     * @return interpolating functions for equinoctial orbit parameters
     */
    private KK[][] interpolateEquinoctial(final FieldAbsoluteDate<KK> interpolationDate, final List<FieldOrbit<KK>> orbits,
                                          final boolean useDerivatives) {

        // set up an interpolator
        final FieldHermiteInterpolator<KK> interpolator = new FieldHermiteInterpolator<>();

        // second pass to feed interpolator
        FieldAbsoluteDate<KK> previousDate = null;
        KK                    previousLm   = zero.add(Double.NaN);
        for (final FieldOrbit<KK> orbit : orbits) {
            final FieldEquinoctialOrbit<KK> equi = (FieldEquinoctialOrbit<KK>) OrbitType.EQUINOCTIAL.convertType(orbit);
            final KK                        continuousLm;
            if (previousDate == null) {
                continuousLm = equi.getLM();
            }
            else {
                final KK dt       = equi.getDate().durationFrom(previousDate);
                final KK keplerLm = previousLm.add(equi.getKeplerianMeanMotion().multiply(dt));
                continuousLm = MathUtils.normalizeAngle(equi.getLM(), keplerLm);
            }
            previousDate = equi.getDate();
            previousLm   = continuousLm;
            final KK[] toAdd = MathArrays.buildArray(field, 6);
            toAdd[0] = equi.getA();
            toAdd[1] = equi.getEquinoctialEx();
            toAdd[2] = equi.getEquinoctialEy();
            toAdd[3] = equi.getHx();
            toAdd[4] = equi.getHy();
            toAdd[5] = continuousLm;
            if (useDerivatives) {
                final KK[] toAddDot = MathArrays.buildArray(one.getField(), 6);
                toAddDot[0] = equi.getADot();
                toAddDot[1] = equi.getEquinoctialExDot();
                toAddDot[2] = equi.getEquinoctialEyDot();
                toAddDot[3] = equi.getHxDot();
                toAddDot[4] = equi.getHyDot();
                toAddDot[5] = equi.getLMDot();
                interpolator.addSamplePoint(equi.getDate().durationFrom(interpolationDate),
                                            toAdd, toAddDot);
            }
            else {
                interpolator.addSamplePoint(equi.getDate().durationFrom(interpolationDate),
                                            toAdd);
            }
        }

        return interpolator.derivatives(zero, 1);
    }
}