SpacecraftStateInterpolator.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.util.Pair;
import org.orekit.attitudes.Attitude;
import org.orekit.attitudes.AttitudeInterpolator;
import org.orekit.attitudes.AttitudeProvider;
import org.orekit.attitudes.FrameAlignedProvider;
import org.orekit.errors.OrekitIllegalArgumentException;
import org.orekit.errors.OrekitInternalError;
import org.orekit.errors.OrekitMessages;
import org.orekit.frames.Frame;
import org.orekit.orbits.Orbit;
import org.orekit.orbits.OrbitHermiteInterpolator;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.AbstractTimeInterpolator;
import org.orekit.time.TimeInterpolator;
import org.orekit.time.TimeStamped;
import org.orekit.time.TimeStampedDouble;
import org.orekit.time.TimeStampedDoubleHermiteInterpolator;
import org.orekit.utils.AbsolutePVCoordinates;
import org.orekit.utils.AbsolutePVCoordinatesHermiteInterpolator;
import org.orekit.utils.AngularDerivativesFilter;
import org.orekit.utils.CartesianDerivativesFilter;
import org.orekit.utils.DoubleArrayDictionary;
import org.orekit.utils.PVCoordinatesProvider;
import org.orekit.utils.TimeStampedAngularCoordinatesHermiteInterpolator;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * Generic class for spacecraft state interpolator.
 * <p>
 * The user can specify what interpolator to use for each attribute of the spacecraft state. However, at least one
 * interpolator for either orbit or absolute position-velocity-acceleration is needed. All the other interpolators can be
 * left to null if the user do not want to interpolate these values.
 *
 * @author Luc Maisonobe
 * @author Vincent Cucchietti
 * @see SpacecraftState
 */
public class SpacecraftStateInterpolator extends AbstractTimeInterpolator<SpacecraftState> {

    /**
     * Output frame.
     * <p><b>Must be inertial</b> if interpolating spacecraft states defined by orbit</p>
     */
    private final Frame outputFrame;

    /** Orbit interpolator. */
    private final TimeInterpolator<Orbit> orbitInterpolator;

    /** Absolute position-velocity-acceleration interpolator. */
    private final TimeInterpolator<AbsolutePVCoordinates> absPVAInterpolator;

    /** Mass interpolator. */
    private final TimeInterpolator<TimeStampedDouble> massInterpolator;

    /** Attitude interpolator. */
    private final TimeInterpolator<Attitude> attitudeInterpolator;

    /** Additional state interpolator. */
    private final TimeInterpolator<TimeStampedDouble> additionalStateInterpolator;

    /**
     * Simplest constructor to create a default Hermite interpolator for every spacecraft state field.
     * <p>
     * The interpolators will have the following configuration :
     * <ul>
     *     <li>Same frame for coordinates and attitude </li>
     *     <li>Default number of interpolation points of {@code DEFAULT_INTERPOLATION_POINTS}</li>
     *     <li>Default extrapolation threshold of {@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s</li>
     *     <li>Use of position and two time derivatives for absolute position-velocity-acceleration coordinates interpolation</li>
     *     <li>Use of angular and first time derivative for attitude interpolation</li>
     * </ul>
     * <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> output frame <b>must be inertial</b> if this instance is going to interpolate between
     * tabulated spacecraft states defined by orbit, will throw an error otherwise.
     *
     * @param outputFrame output frame
     *
     * @see AbstractTimeInterpolator
     */
    public SpacecraftStateInterpolator(final Frame outputFrame) {
        this(DEFAULT_INTERPOLATION_POINTS, outputFrame);
    }

    /**
     * Constructor to create a customizable Hermite interpolator for every spacecraft state field.
     * <p>
     * The interpolators will have the following configuration :
     * <ul>
     *     <li>Same frame for coordinates and attitude </li>
     *     <li>Default extrapolation threshold of {@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s</li>
     *     <li>Use of position and two time derivatives for absolute position-velocity-acceleration coordinates interpolation</li>
     *     <li>Use of angular and first time derivative for attitude interpolation</li>
     * </ul>
     * <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> output frame <b>must be inertial</b> if this instance is going to interpolate between
     * tabulated spacecraft states defined by orbit, will throw an error otherwise.
     *
     * @param interpolationPoints number of interpolation points
     * @param outputFrame output frame
     *
     * @see AbstractTimeInterpolator
     */
    public SpacecraftStateInterpolator(final int interpolationPoints, final Frame outputFrame) {
        this(interpolationPoints, DEFAULT_EXTRAPOLATION_THRESHOLD_SEC, outputFrame, outputFrame);
    }

    /**
     * Constructor to create a customizable Hermite interpolator for every spacecraft state field.
     * <p>
     * The interpolators will have the following configuration :
     * <ul>
     *     <li>Same frame for coordinates and attitude </li>
     *     <li>Use of position and two time derivatives for absolute position-velocity-acceleration coordinates interpolation</li>
     *     <li>Use of angular and first time derivative for attitude interpolation</li>
     * </ul>
     * <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> output frame <b>must be inertial</b> if this instance is going to interpolate between
     * tabulated spacecraft states defined by orbit, will throw an error otherwise.
     *
     * @param interpolationPoints number of interpolation points
     * @param extrapolationThreshold extrapolation threshold beyond which the propagation will fail
     * @param outputFrame output frame
     * @since 12.1
     * @see AbstractTimeInterpolator
     */
    public SpacecraftStateInterpolator(final int interpolationPoints, final double extrapolationThreshold, final Frame outputFrame) {
        this(interpolationPoints, extrapolationThreshold, outputFrame, outputFrame);
    }

    /**
     * Constructor to create a customizable Hermite interpolator for every spacecraft state field.
     * <p>
     * The interpolators will have the following configuration :
     * <ul>
     *     <li>Default extrapolation threshold of {@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s</li>
     *     <li>Use of position and two time derivatives for absolute position-velocity-acceleration coordinates interpolation</li>
     *     <li>Use of angular and first time derivative for attitude interpolation</li>
     * </ul>
     * <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> output frame <b>must be inertial</b> if this instance is going to interpolate between
     * tabulated spacecraft states defined by orbit, will throw an error otherwise.
     *
     * @param interpolationPoints number of interpolation points
     * @param outputFrame output frame
     * @param attitudeReferenceFrame reference frame from which attitude is defined
     *
     * @see AbstractTimeInterpolator
     */
    public SpacecraftStateInterpolator(final int interpolationPoints, final Frame outputFrame,
                                       final Frame attitudeReferenceFrame) {
        this(interpolationPoints, DEFAULT_EXTRAPOLATION_THRESHOLD_SEC, outputFrame, attitudeReferenceFrame,
             CartesianDerivativesFilter.USE_PVA, AngularDerivativesFilter.USE_RR);
    }

    /**
     * Constructor to create a customizable Hermite interpolator for every spacecraft state field.
     * <p>
     * The interpolators will have the following configuration :
     * <ul>
     *     <li>Use of position and two time derivatives for absolute position-velocity-acceleration coordinates interpolation</li>
     *     <li>Use of angular and first time derivative for attitude interpolation</li>
     * </ul>
     * <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> output frame <b>must be inertial</b> if this instance is going to interpolate between
     * tabulated spacecraft states defined by orbit, will throw an error otherwise.
     *
     * @param interpolationPoints number of interpolation points
     * @param extrapolationThreshold extrapolation threshold beyond which the propagation will fail
     * @param outputFrame output frame
     * @param attitudeReferenceFrame reference frame from which attitude is defined
     */
    public SpacecraftStateInterpolator(final int interpolationPoints, final double extrapolationThreshold,
                                       final Frame outputFrame, final Frame attitudeReferenceFrame) {
        this(interpolationPoints, extrapolationThreshold, outputFrame, attitudeReferenceFrame,
             CartesianDerivativesFilter.USE_PVA, AngularDerivativesFilter.USE_RR);
    }

    /**
     * Constructor to create a customizable Hermite interpolator for every spacecraft state field.
     * <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> output frame <b>must be inertial</b> if this instance is going to interpolate between
     * tabulated spacecraft states defined by orbit, will throw an error otherwise.
     *
     * @param interpolationPoints number of interpolation points
     * @param extrapolationThreshold extrapolation threshold beyond which the propagation will fail
     * @param outputFrame output frame
     * @param attitudeReferenceFrame reference frame from which attitude is defined
     * @param pvaFilter filter for derivatives from the sample to use in position-velocity-acceleration interpolation
     * @param angularFilter filter for derivatives from the sample to use in attitude interpolation
     */
    public SpacecraftStateInterpolator(final int interpolationPoints, final double extrapolationThreshold,
                                       final Frame outputFrame, final Frame attitudeReferenceFrame,
                                       final CartesianDerivativesFilter pvaFilter,
                                       final AngularDerivativesFilter angularFilter) {
        this(interpolationPoints, extrapolationThreshold, outputFrame,
             new OrbitHermiteInterpolator(interpolationPoints, extrapolationThreshold, outputFrame, pvaFilter),
             new AbsolutePVCoordinatesHermiteInterpolator(interpolationPoints, extrapolationThreshold, outputFrame,
                                                          pvaFilter),
             new TimeStampedDoubleHermiteInterpolator(interpolationPoints, extrapolationThreshold),
             new AttitudeInterpolator(attitudeReferenceFrame,
                                      new TimeStampedAngularCoordinatesHermiteInterpolator(interpolationPoints,
                                                                                           extrapolationThreshold,
                                                                                           angularFilter)),
             new TimeStampedDoubleHermiteInterpolator(interpolationPoints, extrapolationThreshold));
    }

    /**
     * Constructor.
     * <p>
     * At least one interpolator for either orbit or absolute position-velocity-acceleration is needed. All the other
     * interpolators can be left to null if the user do not want to interpolate these values.
     * <p>
     * <b>BEWARE:</b> output frame <b>must be inertial</b> if interpolated spacecraft states are defined by orbit. Throws an
     * error otherwise.
     * <p>
     * <b>BEWARE:</b> it is up to the user to check the consistency of input interpolators.
     *
     * @param interpolationPoints number of interpolation points
     * @param extrapolationThreshold extrapolation threshold beyond which the propagation will fail
     * @param outputFrame output frame (inertial if the user is planning to use the orbit interpolator)
     * @param orbitInterpolator orbit interpolator (can be null if absPVAInterpolator is defined)
     * @param absPVAInterpolator absolute position-velocity-acceleration (can be null if orbitInterpolator is defined)
     * @param massInterpolator mass interpolator (can be null)
     * @param attitudeInterpolator attitude interpolator (can be null)
     * @param additionalStateInterpolator additional state interpolator (can be null)
     *
     * @since 12.0.1
     */
    public SpacecraftStateInterpolator(final int interpolationPoints, final double extrapolationThreshold,
                                       final Frame outputFrame, final TimeInterpolator<Orbit> orbitInterpolator,
                                       final TimeInterpolator<AbsolutePVCoordinates> absPVAInterpolator,
                                       final TimeInterpolator<TimeStampedDouble> massInterpolator,
                                       final TimeInterpolator<Attitude> attitudeInterpolator,
                                       final TimeInterpolator<TimeStampedDouble> additionalStateInterpolator) {
        super(interpolationPoints, extrapolationThreshold);
        checkAtLeastOneInterpolator(orbitInterpolator, absPVAInterpolator);
        this.outputFrame                 = outputFrame;
        this.orbitInterpolator           = orbitInterpolator;
        this.absPVAInterpolator          = absPVAInterpolator;
        this.massInterpolator            = massInterpolator;
        this.attitudeInterpolator        = attitudeInterpolator;
        this.additionalStateInterpolator = additionalStateInterpolator;
    }

    /**
     * Check that an interpolator exist for given sample state definition.
     *
     * @param sample sample (non empty)
     * @param orbitInterpolatorIsPresent flag defining if an orbit interpolator has been defined for this instance
     * @param absPVInterpolatorIsPresent flag defining if an absolute position-velocity-acceleration interpolator has been
     * defined for this instance
     *
     * @throws OrekitIllegalArgumentException if there is no defined interpolator for given sample spacecraft state
     * definition type
     */
    public static void checkSampleAndInterpolatorConsistency(final List<SpacecraftState> sample,
                                                             final boolean orbitInterpolatorIsPresent,
                                                             final boolean absPVInterpolatorIsPresent) {
        // Get first state definition
        final SpacecraftState earliestState = sample.get(0);

        if (earliestState.isOrbitDefined() && !orbitInterpolatorIsPresent ||
                !earliestState.isOrbitDefined() && !absPVInterpolatorIsPresent) {
            throw new OrekitIllegalArgumentException(OrekitMessages.WRONG_INTERPOLATOR_DEFINED_FOR_STATE_INTERPOLATION);
        }
    }

    /**
     * Check that all state are either orbit defined or based on absolute position-velocity-acceleration.
     *
     * @param states spacecraft state sample
     */
    public static void checkStatesDefinitionsConsistency(final List<SpacecraftState> states) {
        // Check all states handle the same additional states and are defined the same way (orbit or absolute PVA)
        final SpacecraftState s0               = states.get(0);
        final boolean         s0IsOrbitDefined = s0.isOrbitDefined();
        for (final SpacecraftState state : states) {
            s0.ensureCompatibleAdditionalStates(state);
            if (s0IsOrbitDefined != state.isOrbitDefined()) {
                throw new OrekitIllegalArgumentException(OrekitMessages.DIFFERENT_STATE_DEFINITION);
            }
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * The additional states that are interpolated are the ones already present in the first neighbor instance. The sample
     * instances must therefore have at least the same additional states as this neighbor instance. They may have more
     * additional states, but the extra ones will be ignored.
     * <p>
     * All the sample instances <em>must</em> be based on similar trajectory data, i.e. they must either all be based on
     * orbits or all be based on absolute position-velocity-acceleration. Any inconsistency will trigger an
     * {@link OrekitIllegalArgumentException}.
     *
     * @throws OrekitIllegalArgumentException if there are states defined by orbits and absolute
     * position-velocity-acceleration coordinates
     * @throws OrekitIllegalArgumentException if there is no defined interpolator for given sample spacecraft state
     * definition type
     */
    @Override
    public SpacecraftState interpolate(final AbsoluteDate interpolationDate, final Collection<SpacecraftState> sample) {

        final List<SpacecraftState> sampleList = new ArrayList<>(sample);

        // If sample is empty, an error will be thrown in super method
        if (!sample.isEmpty()) {

            // Check given that given states definition are consistent
            // (all defined by either orbits or absolute position-velocity-acceleration coordinates)
            checkStatesDefinitionsConsistency(sampleList);

            // Check interpolator and sample consistency
            checkSampleAndInterpolatorConsistency(sampleList, orbitInterpolator != null, absPVAInterpolator != null);
        }

        return super.interpolate(interpolationDate, sample);
    }

    /** {@inheritDoc} */
    @Override
    public List<TimeInterpolator<? extends TimeStamped>> getSubInterpolators() {

        // Add all sub interpolators that are defined
        final List<TimeInterpolator<? extends TimeStamped>> subInterpolators = new ArrayList<>();

        addOptionalSubInterpolatorIfDefined(orbitInterpolator, subInterpolators);
        addOptionalSubInterpolatorIfDefined(absPVAInterpolator, subInterpolators);
        addOptionalSubInterpolatorIfDefined(massInterpolator, subInterpolators);
        addOptionalSubInterpolatorIfDefined(attitudeInterpolator, subInterpolators);
        addOptionalSubInterpolatorIfDefined(additionalStateInterpolator, subInterpolators);

        return subInterpolators;

    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected SpacecraftState interpolate(final InterpolationData interpolationData) {

        // Get first state definition
        final List<SpacecraftState> samples   = interpolationData.getNeighborList();
        final SpacecraftState earliestState   = samples.get(0);
        final boolean         areOrbitDefined = earliestState.isOrbitDefined();

        // Prepare samples
        final List<Attitude> attitudes = new ArrayList<>();

        final List<TimeStampedDouble> masses = new ArrayList<>();

        final List<DoubleArrayDictionary.Entry> additionalEntries = earliestState.getAdditionalStatesValues().getData();
        final Map<String, List<Pair<AbsoluteDate, double[]>>> additionalSample =
                createAdditionalStateSample(additionalEntries);

        final List<DoubleArrayDictionary.Entry> additionalDotEntries =
                earliestState.getAdditionalStatesDerivatives().getData();
        final Map<String, List<Pair<AbsoluteDate, double[]>>> additionalDotSample =
                createAdditionalStateSample(additionalDotEntries);

        // Fill interpolators with samples
        final List<Orbit>                 orbitSample  = new ArrayList<>();
        final List<AbsolutePVCoordinates> absPVASample = new ArrayList<>();
        for (SpacecraftState state : samples) {
            final AbsoluteDate currentDate = state.getDate();

            // Add orbit sample if state is defined with an orbit
            if (state.isOrbitDefined()) {
                orbitSample.add(state.getOrbit());
            }
            // Add absolute position-velocity-acceleration sample if state is defined with an absolute position-velocity-acceleration
            else {
                absPVASample.add(state.getAbsPVA());
            }

            // Add mass sample
            if (massInterpolator != null) {
                masses.add(new TimeStampedDouble(state.getMass(), state.getDate()));
            }

            // Add attitude sample if it is interpolated
            if (attitudeInterpolator != null) {
                attitudes.add(state.getAttitude());
            }

            if (additionalStateInterpolator != null) {

                // Add all additional state values if they are interpolated
                for (final Map.Entry<String, List<Pair<AbsoluteDate, double[]>>> entry : additionalSample.entrySet()) {
                    entry.getValue().add(new Pair<>(currentDate, state.getAdditionalState(entry.getKey())));
                }

                // Add all additional state derivative values if they are interpolated
                for (final Map.Entry<String, List<Pair<AbsoluteDate, double[]>>> entry : additionalDotSample.entrySet()) {
                    entry.getValue().add(new Pair<>(currentDate, state.getAdditionalStateDerivative(entry.getKey())));
                }
            }
        }

        // Interpolate mass
        final AbsoluteDate interpolationDate = interpolationData.getInterpolationDate();
        final double       interpolatedMass;
        if (massInterpolator != null) {
            interpolatedMass = massInterpolator.interpolate(interpolationDate, masses).getValue();
        } else {
            interpolatedMass = SpacecraftState.DEFAULT_MASS;
        }

        // Interpolate additional states and derivatives
        final DoubleArrayDictionary interpolatedAdditional;
        final DoubleArrayDictionary interpolatedAdditionalDot;
        if (additionalStateInterpolator != null) {
            interpolatedAdditional    = interpolateAdditionalState(interpolationDate, additionalSample);
            interpolatedAdditionalDot = interpolateAdditionalState(interpolationDate, additionalDotSample);
        } else {
            interpolatedAdditional    = null;
            interpolatedAdditionalDot = null;
        }

        // Interpolate orbit
        if (areOrbitDefined && orbitInterpolator != null) {
            final Orbit interpolatedOrbit = orbitInterpolator.interpolate(interpolationDate, orbitSample);

            final Attitude interpolatedAttitude = interpolateAttitude(interpolationDate, attitudes, interpolatedOrbit);

            return new SpacecraftState(interpolatedOrbit, interpolatedAttitude, interpolatedMass, interpolatedAdditional,
                                       interpolatedAdditionalDot);
        }
        // Interpolate absolute position-velocity-acceleration
        else if (!areOrbitDefined && absPVAInterpolator != null) {

            final AbsolutePVCoordinates interpolatedAbsPva = absPVAInterpolator.interpolate(interpolationDate, absPVASample);

            final Attitude interpolatedAttitude = interpolateAttitude(interpolationDate, attitudes, interpolatedAbsPva);

            return new SpacecraftState(interpolatedAbsPva, interpolatedAttitude, interpolatedMass, interpolatedAdditional,
                                       interpolatedAdditionalDot);
        }
        // Should never happen
        else {
            throw new OrekitInternalError(null);
        }

    }

    /**
     * Get output frame.
     *
     * @return output frame
     */
    public Frame getOutputFrame() {
        return outputFrame;
    }

    /**
     * Get orbit interpolator.
     *
     * @return optional orbit interpolator
     *
     * @see Optional
     */
    public Optional<TimeInterpolator<Orbit>> getOrbitInterpolator() {
        return Optional.ofNullable(orbitInterpolator);
    }

    /**
     * Get absolute position-velocity-acceleration interpolator.
     *
     * @return optional absolute position-velocity-acceleration interpolator
     *
     * @see Optional
     */
    public Optional<TimeInterpolator<AbsolutePVCoordinates>> getAbsPVAInterpolator() {
        return Optional.ofNullable(absPVAInterpolator);
    }

    /**
     * Get mass interpolator.
     *
     * @return optional mass interpolator
     *
     * @see Optional
     */
    public Optional<TimeInterpolator<TimeStampedDouble>> getMassInterpolator() {
        return Optional.ofNullable(massInterpolator);
    }

    /**
     * Get attitude interpolator.
     *
     * @return optional attitude interpolator
     *
     * @see Optional
     */
    public Optional<TimeInterpolator<Attitude>> getAttitudeInterpolator() {
        return Optional.ofNullable(attitudeInterpolator);
    }

    /**
     * Get additional state interpolator.
     *
     * @return optional additional state interpolator
     *
     * @see Optional
     */
    public Optional<TimeInterpolator<TimeStampedDouble>> getAdditionalStateInterpolator() {
        return Optional.ofNullable(additionalStateInterpolator);
    }

    /**
     * Check that at least one interpolator is defined.
     *
     * @param orbitInterpolatorToCheck orbit interpolator
     * @param absPVAInterpolatorToCheck absolute position-velocity-acceleration interpolator
     */
    private void checkAtLeastOneInterpolator(final TimeInterpolator<Orbit> orbitInterpolatorToCheck,
                                             final TimeInterpolator<AbsolutePVCoordinates> absPVAInterpolatorToCheck) {
        if (orbitInterpolatorToCheck == null && absPVAInterpolatorToCheck == null) {
            throw new OrekitIllegalArgumentException(OrekitMessages.NO_INTERPOLATOR_FOR_STATE_DEFINITION);
        }
    }

    /**
     * Create empty samples for given additional entries.
     *
     * @param additionalEntries tabulated additional entries
     *
     * @return empty samples for given additional entries
     */
    private Map<String, List<Pair<AbsoluteDate, double[]>>> createAdditionalStateSample(
            final List<DoubleArrayDictionary.Entry> additionalEntries) {
        final Map<String, List<Pair<AbsoluteDate, double[]>>> additionalSamples = new HashMap<>(additionalEntries.size());

        for (final DoubleArrayDictionary.Entry entry : additionalEntries) {
            additionalSamples.put(entry.getKey(), new ArrayList<>());
        }

        return additionalSamples;
    }

    /**
     * Interpolate additional state values.
     *
     * @param interpolationDate interpolation date
     * @param additionalSamples additional state samples
     *
     * @return interpolated additional state values
     */
    private DoubleArrayDictionary interpolateAdditionalState(final AbsoluteDate interpolationDate,
                                                             final Map<String, List<Pair<AbsoluteDate, double[]>>> additionalSamples) {
        final DoubleArrayDictionary interpolatedAdditional;

        if (additionalSamples.isEmpty()) {
            interpolatedAdditional = null;
        } else {
            interpolatedAdditional = new DoubleArrayDictionary(additionalSamples.size());
            for (final Map.Entry<String, List<Pair<AbsoluteDate, double[]>>> entry : additionalSamples.entrySet()) {

                // Get current entry
                final List<Pair<AbsoluteDate, double[]>> currentAdditionalSamples = entry.getValue();

                // Extract number of values for this specific entry
                final int nbOfValues = currentAdditionalSamples.get(0).getValue().length;

                // For each value of current additional state entry
                final double[] currentInterpolatedAdditional = new double[nbOfValues];
                for (int i = 0; i < nbOfValues; i++) {

                    // Create final index for lambda expression use
                    final int currentIndex = i;

                    // Create sample for specific value of current additional state values
                    final List<TimeStampedDouble> currentValueSample = new ArrayList<>();

                    currentAdditionalSamples.forEach(currentSamples -> currentValueSample.add(
                            new TimeStampedDouble(currentSamples.getValue()[currentIndex], currentSamples.getFirst())));

                    // Interpolate
                    currentInterpolatedAdditional[i] =
                            additionalStateInterpolator.interpolate(interpolationDate, currentValueSample).getValue();
                }

                interpolatedAdditional.put(entry.getKey(), currentInterpolatedAdditional);
            }
        }
        return interpolatedAdditional;
    }

    /**
     * Interpolate attitude.
     * <p>
     * If no attitude interpolator were defined, create a default inertial provider with respect to the output frame.
     *
     * @param interpolationDate interpolation date
     * @param attitudes attitudes sample
     * @param pvProvider position-velocity-acceleration coordinates provider
     *
     * @return interpolated attitude if attitude interpolator is present, default attitude otherwise
     */
    private Attitude interpolateAttitude(final AbsoluteDate interpolationDate, final List<Attitude> attitudes,
                                         final PVCoordinatesProvider pvProvider) {
        if (attitudes.isEmpty()) {
            final AttitudeProvider attitudeProvider = new FrameAlignedProvider(outputFrame);
            return attitudeProvider.getAttitude(pvProvider, interpolationDate, outputFrame);
        } else {
            return attitudeInterpolator.interpolate(interpolationDate, attitudes);
        }
    }
}