1   /* Copyright 2002-2026 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.propagation;
18  
19  import java.util.Comparator;
20  import org.hipparchus.util.Pair;
21  import org.orekit.attitudes.Attitude;
22  import org.orekit.attitudes.AttitudeInterpolator;
23  import org.orekit.attitudes.AttitudeProvider;
24  import org.orekit.attitudes.FrameAlignedProvider;
25  import org.orekit.errors.OrekitIllegalArgumentException;
26  import org.orekit.errors.OrekitInternalError;
27  import org.orekit.errors.OrekitMessages;
28  import org.orekit.frames.Frame;
29  import org.orekit.orbits.Orbit;
30  import org.orekit.orbits.OrbitHermiteInterpolator;
31  import org.orekit.time.AbsoluteDate;
32  import org.orekit.time.AbstractTimeInterpolator;
33  import org.orekit.time.TimeInterpolator;
34  import org.orekit.time.TimeStamped;
35  import org.orekit.time.TimeStampedDouble;
36  import org.orekit.time.TimeStampedDoubleHermiteInterpolator;
37  import org.orekit.utils.AbsolutePVCoordinates;
38  import org.orekit.utils.AbsolutePVCoordinatesHermiteInterpolator;
39  import org.orekit.utils.AngularDerivativesFilter;
40  import org.orekit.utils.CartesianDerivativesFilter;
41  import org.orekit.utils.DoubleArrayDictionary;
42  import org.orekit.utils.DataDictionary;
43  import org.orekit.utils.PVCoordinatesProvider;
44  import org.orekit.utils.TimeStampedAngularCoordinatesHermiteInterpolator;
45  
46  import java.util.ArrayList;
47  import java.util.Collection;
48  import java.util.HashMap;
49  import java.util.List;
50  import java.util.Map;
51  import java.util.Optional;
52  
53  /**
54   * Generic class for spacecraft state interpolator.
55   * <p>
56   * The user can specify what interpolator to use for each attribute of the spacecraft state. However, at least one
57   * interpolator for either orbit or absolute position-velocity-acceleration is needed. All the other interpolators can be
58   * left to null if the user do not want to interpolate these values.
59   *
60   * @author Luc Maisonobe
61   * @author Vincent Cucchietti
62   * @see SpacecraftState
63   */
64  public class SpacecraftStateInterpolator extends AbstractTimeInterpolator<SpacecraftState> {
65  
66      /**
67       * Output frame.
68       * <p><b>Must be inertial</b> if interpolating spacecraft states defined by orbit</p>
69       */
70      private final Frame outputFrame;
71  
72      /** Orbit interpolator. */
73      private final TimeInterpolator<Orbit> orbitInterpolator;
74  
75      /** Absolute position-velocity-acceleration interpolator. */
76      private final TimeInterpolator<AbsolutePVCoordinates> absPVAInterpolator;
77  
78      /** Mass interpolator. */
79      private final TimeInterpolator<TimeStampedDouble> massInterpolator;
80  
81      /** Attitude interpolator. */
82      private final TimeInterpolator<Attitude> attitudeInterpolator;
83  
84      /** Additional state interpolator. */
85      private final TimeInterpolator<TimeStampedDouble> additionalStateInterpolator;
86  
87      /**
88       * Simplest constructor to create a default Hermite interpolator for every spacecraft state field.
89       * <p>
90       * The interpolators will have the following configuration :
91       * <ul>
92       *     <li>Same frame for coordinates and attitude </li>
93       *     <li>Default number of interpolation points of {@code DEFAULT_INTERPOLATION_POINTS}</li>
94       *     <li>Default extrapolation threshold of {@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s</li>
95       *     <li>Use of position and two time derivatives for absolute position-velocity-acceleration coordinates interpolation</li>
96       *     <li>Use of angular and first time derivative for attitude interpolation</li>
97       * </ul>
98       * <p>
99       * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
100      * points (about 10-20 points) in order to avoid <a href="https://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
101      * phenomenon</a> and numerical problems (including NaN appearing).
102      * <p>
103      * <b>BEWARE:</b> output frame <b>must be inertial</b> if this instance is going to interpolate between
104      * tabulated spacecraft states defined by orbit, will throw an error otherwise.
105      *
106      * @param outputFrame output frame
107      *
108      * @see AbstractTimeInterpolator
109      */
110     public SpacecraftStateInterpolator(final Frame outputFrame) {
111         this(DEFAULT_INTERPOLATION_POINTS, outputFrame);
112     }
113 
114     /**
115      * Constructor to create a customizable Hermite interpolator for every spacecraft state field.
116      * <p>
117      * The interpolators will have the following configuration :
118      * <ul>
119      *     <li>Same frame for coordinates and attitude </li>
120      *     <li>Default extrapolation threshold of {@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s</li>
121      *     <li>Use of position and two time derivatives for absolute position-velocity-acceleration coordinates interpolation</li>
122      *     <li>Use of angular and first time derivative for attitude interpolation</li>
123      * </ul>
124      * <p>
125      * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
126      * points (about 10-20 points) in order to avoid <a href="https://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
127      * phenomenon</a> and numerical problems (including NaN appearing).
128      * <p>
129      * <b>BEWARE:</b> output frame <b>must be inertial</b> if this instance is going to interpolate between
130      * tabulated spacecraft states defined by orbit, will throw an error otherwise.
131      *
132      * @param interpolationPoints number of interpolation points
133      * @param outputFrame output frame
134      *
135      * @see AbstractTimeInterpolator
136      */
137     public SpacecraftStateInterpolator(final int interpolationPoints, final Frame outputFrame) {
138         this(interpolationPoints, DEFAULT_EXTRAPOLATION_THRESHOLD_SEC, outputFrame, outputFrame);
139     }
140 
141     /**
142      * Constructor to create a customizable Hermite interpolator for every spacecraft state field.
143      * <p>
144      * The interpolators will have the following configuration :
145      * <ul>
146      *     <li>Same frame for coordinates and attitude </li>
147      *     <li>Use of position and two time derivatives for absolute position-velocity-acceleration coordinates interpolation</li>
148      *     <li>Use of angular and first time derivative for attitude interpolation</li>
149      * </ul>
150      * <p>
151      * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
152      * points (about 10-20 points) in order to avoid <a href="https://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
153      * phenomenon</a> and numerical problems (including NaN appearing).
154      * <p>
155      * <b>BEWARE:</b> output frame <b>must be inertial</b> if this instance is going to interpolate between
156      * tabulated spacecraft states defined by orbit, will throw an error otherwise.
157      *
158      * @param interpolationPoints number of interpolation points
159      * @param extrapolationThreshold extrapolation threshold beyond which the propagation will fail
160      * @param outputFrame output frame
161      * @since 12.1
162      * @see AbstractTimeInterpolator
163      */
164     public SpacecraftStateInterpolator(final int interpolationPoints, final double extrapolationThreshold, final Frame outputFrame) {
165         this(interpolationPoints, extrapolationThreshold, outputFrame, outputFrame);
166     }
167 
168     /**
169      * Constructor to create a customizable Hermite interpolator for every spacecraft state field.
170      * <p>
171      * The interpolators will have the following configuration :
172      * <ul>
173      *     <li>Default extrapolation threshold of {@code DEFAULT_EXTRAPOLATION_THRESHOLD_SEC} s</li>
174      *     <li>Use of position and two time derivatives for absolute position-velocity-acceleration coordinates interpolation</li>
175      *     <li>Use of angular and first time derivative for attitude interpolation</li>
176      * </ul>
177      * <p>
178      * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
179      * points (about 10-20 points) in order to avoid <a href="https://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
180      * phenomenon</a> and numerical problems (including NaN appearing).
181      * <p>
182      * <b>BEWARE:</b> output frame <b>must be inertial</b> if this instance is going to interpolate between
183      * tabulated spacecraft states defined by orbit, will throw an error otherwise.
184      *
185      * @param interpolationPoints number of interpolation points
186      * @param outputFrame output frame
187      * @param attitudeReferenceFrame reference frame from which attitude is defined
188      *
189      * @see AbstractTimeInterpolator
190      */
191     public SpacecraftStateInterpolator(final int interpolationPoints, final Frame outputFrame,
192                                        final Frame attitudeReferenceFrame) {
193         this(interpolationPoints, DEFAULT_EXTRAPOLATION_THRESHOLD_SEC, outputFrame, attitudeReferenceFrame,
194              CartesianDerivativesFilter.USE_PVA, AngularDerivativesFilter.USE_RR);
195     }
196 
197     /**
198      * Constructor to create a customizable Hermite interpolator for every spacecraft state field.
199      * <p>
200      * The interpolators will have the following configuration :
201      * <ul>
202      *     <li>Use of position and two time derivatives for absolute position-velocity-acceleration coordinates interpolation</li>
203      *     <li>Use of angular and first time derivative for attitude interpolation</li>
204      * </ul>
205      * <p>
206      * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
207      * points (about 10-20 points) in order to avoid <a href="https://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
208      * phenomenon</a> and numerical problems (including NaN appearing).
209      * <p>
210      * <b>BEWARE:</b> output frame <b>must be inertial</b> if this instance is going to interpolate between
211      * tabulated spacecraft states defined by orbit, will throw an error otherwise.
212      *
213      * @param interpolationPoints number of interpolation points
214      * @param extrapolationThreshold extrapolation threshold beyond which the propagation will fail
215      * @param outputFrame output frame
216      * @param attitudeReferenceFrame reference frame from which attitude is defined
217      */
218     public SpacecraftStateInterpolator(final int interpolationPoints, final double extrapolationThreshold,
219                                        final Frame outputFrame, final Frame attitudeReferenceFrame) {
220         this(interpolationPoints, extrapolationThreshold, outputFrame, attitudeReferenceFrame,
221              CartesianDerivativesFilter.USE_PVA, AngularDerivativesFilter.USE_RR);
222     }
223 
224     /**
225      * Constructor to create a customizable Hermite interpolator for every spacecraft state field.
226      * <p>
227      * As this implementation of interpolation is polynomial, it should be used only with small number of interpolation
228      * points (about 10-20 points) in order to avoid <a href="https://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's
229      * phenomenon</a> and numerical problems (including NaN appearing).
230      * <p>
231      * <b>BEWARE:</b> output frame <b>must be inertial</b> if this instance is going to interpolate between
232      * tabulated spacecraft states defined by orbit, will throw an error otherwise.
233      *
234      * @param interpolationPoints number of interpolation points
235      * @param extrapolationThreshold extrapolation threshold beyond which the propagation will fail
236      * @param outputFrame output frame
237      * @param attitudeReferenceFrame reference frame from which attitude is defined
238      * @param pvaFilter filter for derivatives from the sample to use in position-velocity-acceleration interpolation
239      * @param angularFilter filter for derivatives from the sample to use in attitude interpolation
240      */
241     public SpacecraftStateInterpolator(final int interpolationPoints, final double extrapolationThreshold,
242                                        final Frame outputFrame, final Frame attitudeReferenceFrame,
243                                        final CartesianDerivativesFilter pvaFilter,
244                                        final AngularDerivativesFilter angularFilter) {
245         this(interpolationPoints, extrapolationThreshold, outputFrame,
246              new OrbitHermiteInterpolator(interpolationPoints, extrapolationThreshold, outputFrame, pvaFilter),
247              new AbsolutePVCoordinatesHermiteInterpolator(interpolationPoints, extrapolationThreshold, outputFrame,
248                                                           pvaFilter),
249              new TimeStampedDoubleHermiteInterpolator(interpolationPoints, extrapolationThreshold),
250              new AttitudeInterpolator(attitudeReferenceFrame,
251                                       new TimeStampedAngularCoordinatesHermiteInterpolator(interpolationPoints,
252                                                                                            extrapolationThreshold,
253                                                                                            angularFilter)),
254              new TimeStampedDoubleHermiteInterpolator(interpolationPoints, extrapolationThreshold));
255     }
256 
257     /**
258      * Constructor.
259      * <p>
260      * At least one interpolator for either orbit or absolute position-velocity-acceleration is needed. All the other
261      * interpolators can be left to null if the user do not want to interpolate these values.
262      * <p>
263      * <b>BEWARE:</b> output frame <b>must be inertial</b> if interpolated spacecraft states are defined by orbit. Throws an
264      * error otherwise.
265      * <p>
266      * <b>BEWARE:</b> it is up to the user to check the consistency of input interpolators.
267      *
268      * @param interpolationPoints number of interpolation points
269      * @param extrapolationThreshold extrapolation threshold beyond which the propagation will fail
270      * @param outputFrame output frame (inertial if the user is planning to use the orbit interpolator)
271      * @param orbitInterpolator orbit interpolator (can be null if absPVAInterpolator is defined)
272      * @param absPVAInterpolator absolute position-velocity-acceleration (can be null if orbitInterpolator is defined)
273      * @param massInterpolator mass interpolator (can be null)
274      * @param attitudeInterpolator attitude interpolator (can be null)
275      * @param additionalStateInterpolator additional state interpolator (can be null)
276      *
277      * @since 12.0.1
278      */
279     public SpacecraftStateInterpolator(final int interpolationPoints, final double extrapolationThreshold,
280                                        final Frame outputFrame, final TimeInterpolator<Orbit> orbitInterpolator,
281                                        final TimeInterpolator<AbsolutePVCoordinates> absPVAInterpolator,
282                                        final TimeInterpolator<TimeStampedDouble> massInterpolator,
283                                        final TimeInterpolator<Attitude> attitudeInterpolator,
284                                        final TimeInterpolator<TimeStampedDouble> additionalStateInterpolator) {
285         super(interpolationPoints, extrapolationThreshold);
286         checkAtLeastOneInterpolator(orbitInterpolator, absPVAInterpolator);
287         this.outputFrame                 = outputFrame;
288         this.orbitInterpolator           = orbitInterpolator;
289         this.absPVAInterpolator          = absPVAInterpolator;
290         this.massInterpolator            = massInterpolator;
291         this.attitudeInterpolator        = attitudeInterpolator;
292         this.additionalStateInterpolator = additionalStateInterpolator;
293     }
294 
295     /**
296      * Check that an interpolator exist for given sample state definition.
297      *
298      * @param sample sample (non empty)
299      * @param orbitInterpolatorIsPresent flag defining if an orbit interpolator has been defined for this instance
300      * @param absPVInterpolatorIsPresent flag defining if an absolute position-velocity-acceleration interpolator has been
301      * defined for this instance
302      *
303      * @throws OrekitIllegalArgumentException if there is no defined interpolator for given sample spacecraft state
304      * definition type
305      */
306     public static void checkSampleAndInterpolatorConsistency(final List<SpacecraftState> sample,
307                                                              final boolean orbitInterpolatorIsPresent,
308                                                              final boolean absPVInterpolatorIsPresent) {
309         // Get first state definition
310         final SpacecraftState earliestState = sample.getFirst();
311 
312         if (earliestState.isOrbitDefined() && !orbitInterpolatorIsPresent ||
313                 !earliestState.isOrbitDefined() && !absPVInterpolatorIsPresent) {
314             throw new OrekitIllegalArgumentException(OrekitMessages.WRONG_INTERPOLATOR_DEFINED_FOR_STATE_INTERPOLATION);
315         }
316     }
317 
318     /**
319      * Check that all state are either orbit defined or based on absolute position-velocity-acceleration.
320      *
321      * @param states spacecraft state sample
322      */
323     public static void checkStatesDefinitionsConsistency(final List<SpacecraftState> states) {
324         // Check all states handle the same additional states and are defined the same way (orbit or absolute PVA)
325         final SpacecraftState s0               = states.getFirst();
326         final boolean         s0IsOrbitDefined = s0.isOrbitDefined();
327         for (final SpacecraftState state : states) {
328             s0.ensureCompatibleAdditionalStates(state);
329             if (s0IsOrbitDefined != state.isOrbitDefined()) {
330                 throw new OrekitIllegalArgumentException(OrekitMessages.DIFFERENT_STATE_DEFINITION);
331             }
332         }
333     }
334 
335     /**
336      * {@inheritDoc}
337      * <p>
338      * The additional states that are interpolated are the ones already present in the first neighbor instance. The sample
339      * instances must therefore have at least the same additional states as this neighbor instance. They may have more
340      * additional states, but the extra ones will be ignored.
341      * <p>
342      * All the sample instances <em>must</em> be based on similar trajectory data, i.e. they must either all be based on
343      * orbits or all be based on absolute position-velocity-acceleration. Any inconsistency will trigger an
344      * {@link OrekitIllegalArgumentException}.
345      *
346      * @throws OrekitIllegalArgumentException if there are states defined by orbits and absolute
347      * position-velocity-acceleration coordinates
348      * @throws OrekitIllegalArgumentException if there is no defined interpolator for given sample spacecraft state
349      * definition type
350      */
351     @Override
352     public SpacecraftState interpolate(final AbsoluteDate interpolationDate,
353                                        final Collection<? extends SpacecraftState> sample) {
354 
355         final List<SpacecraftState> sampleList = new ArrayList<>(sample);
356 
357         // If sample is empty, an error will be thrown in super method
358         if (!sample.isEmpty()) {
359 
360             // Check given that given states definition are consistent
361             // (all defined by either orbits or absolute position-velocity-acceleration coordinates)
362             checkStatesDefinitionsConsistency(sampleList);
363 
364             // Check interpolator and sample consistency
365             checkSampleAndInterpolatorConsistency(sampleList, orbitInterpolator != null, absPVAInterpolator != null);
366         }
367 
368         return super.interpolate(interpolationDate, sample);
369     }
370 
371     /** {@inheritDoc} */
372     @Override
373     public List<TimeInterpolator<? extends TimeStamped>> getSubInterpolators() {
374 
375         // Add all sub interpolators that are defined
376         final List<TimeInterpolator<? extends TimeStamped>> subInterpolators = new ArrayList<>();
377 
378         addOptionalSubInterpolatorIfDefined(orbitInterpolator, subInterpolators);
379         addOptionalSubInterpolatorIfDefined(absPVAInterpolator, subInterpolators);
380         addOptionalSubInterpolatorIfDefined(massInterpolator, subInterpolators);
381         addOptionalSubInterpolatorIfDefined(attitudeInterpolator, subInterpolators);
382         addOptionalSubInterpolatorIfDefined(additionalStateInterpolator, subInterpolators);
383 
384         return subInterpolators;
385 
386     }
387 
388     /**
389      * {@inheritDoc}
390      */
391     @Override
392     protected SpacecraftState interpolate(final InterpolationData interpolationData) {
393 
394         // Get first state definition
395         final List<SpacecraftState> samples   = interpolationData.getNeighborList();
396         final SpacecraftState earliestState   = samples.getFirst();
397         final boolean         areOrbitDefined = earliestState.isOrbitDefined();
398 
399         // Prepare samples
400         final List<Attitude> attitudes = new ArrayList<>();
401 
402         final List<TimeStampedDouble> masses = new ArrayList<>();
403 
404         final List<DataDictionary.Entry> additionalEntries = earliestState.getAdditionalDataValues().getData();
405         final Map<String, List<Pair<AbsoluteDate, Object>>> additionalSample =
406                 createAdditionalDataSample(additionalEntries);
407 
408         final List<DoubleArrayDictionary.Entry> additionalDotEntries =
409                 earliestState.getAdditionalStatesDerivatives().getData();
410         final Map<String, List<Pair<AbsoluteDate, double[]>>> additionalDotSample =
411                 createAdditionalStateSample(additionalDotEntries);
412 
413         // Fill interpolators with samples
414         final List<Orbit>                 orbitSample  = new ArrayList<>();
415         final List<AbsolutePVCoordinates> absPVASample = new ArrayList<>();
416         for (SpacecraftState state : samples) {
417             final AbsoluteDate currentDate = state.getDate();
418 
419             // Add orbit sample if state is defined with an orbit
420             if (state.isOrbitDefined()) {
421                 orbitSample.add(state.getOrbit());
422             }
423             // Add absolute position-velocity-acceleration sample if state is defined with an absolute position-velocity-acceleration
424             else {
425                 absPVASample.add(state.getAbsPVA());
426             }
427 
428             // Add mass sample
429             if (massInterpolator != null) {
430                 masses.add(new TimeStampedDouble(state.getDate(), state.getMass()));
431             }
432 
433             // Add attitude sample if it is interpolated
434             if (attitudeInterpolator != null) {
435                 attitudes.add(state.getAttitude());
436             }
437 
438             if (additionalStateInterpolator != null) {
439 
440                 // Add all additional state values if they are interpolated
441                 for (final Map.Entry<String, List<Pair<AbsoluteDate, Object>>> entry : additionalSample.entrySet()) {
442                     entry.getValue().add(new Pair<>(currentDate, state.getAdditionalState(entry.getKey())));
443                 }
444 
445                 // Add all additional state derivative values if they are interpolated
446                 for (final Map.Entry<String, List<Pair<AbsoluteDate, double[]>>> entry : additionalDotSample.entrySet()) {
447                     entry.getValue().add(new Pair<>(currentDate, state.getAdditionalStateDerivative(entry.getKey())));
448                 }
449             }
450         }
451 
452         // Interpolate mass
453         final AbsoluteDate interpolationDate = interpolationData.getInterpolationDate();
454         final double       interpolatedMass;
455         if (massInterpolator != null) {
456             interpolatedMass = massInterpolator.interpolate(interpolationDate, masses).getValue();
457         } else {
458             interpolatedMass = SpacecraftState.DEFAULT_MASS;
459         }
460 
461         // Interpolate additional states and derivatives
462         final DataDictionary interpolatedAdditional;
463         final DoubleArrayDictionary interpolatedAdditionalDot;
464         if (additionalStateInterpolator != null) {
465             interpolatedAdditional    = interpolateAdditionalState(interpolationDate, additionalSample).orElse(null);
466             interpolatedAdditionalDot = interpolateAdditionalState(interpolationDate, additionalDotSample).map(DataDictionary::toDoubleDictionary).orElse(null);
467         } else {
468             interpolatedAdditional    = null;
469             interpolatedAdditionalDot = null;
470         }
471 
472         // Interpolate orbit
473         if (areOrbitDefined && orbitInterpolator != null) {
474             final Orbit interpolatedOrbit = orbitInterpolator.interpolate(interpolationDate, orbitSample);
475 
476             final Attitude interpolatedAttitude = interpolateAttitude(interpolationDate, attitudes, interpolatedOrbit);
477 
478             return new SpacecraftState(interpolatedOrbit, interpolatedAttitude, interpolatedMass, interpolatedAdditional,
479                                        interpolatedAdditionalDot);
480         }
481         // Interpolate absolute position-velocity-acceleration
482         else if (!areOrbitDefined && absPVAInterpolator != null) {
483 
484             final AbsolutePVCoordinates interpolatedAbsPva = absPVAInterpolator.interpolate(interpolationDate, absPVASample);
485 
486             final Attitude interpolatedAttitude = interpolateAttitude(interpolationDate, attitudes, interpolatedAbsPva);
487 
488             return new SpacecraftState(interpolatedAbsPva, interpolatedAttitude, interpolatedMass, interpolatedAdditional,
489                                        interpolatedAdditionalDot);
490         }
491         // Should never happen
492         else {
493             throw new OrekitInternalError(null);
494         }
495 
496     }
497 
498     /**
499      * Get output frame.
500      *
501      * @return output frame
502      */
503     public Frame getOutputFrame() {
504         return outputFrame;
505     }
506 
507     /**
508      * Get orbit interpolator.
509      *
510      * @return optional orbit interpolator
511      *
512      * @see Optional
513      */
514     public Optional<TimeInterpolator<Orbit>> getOrbitInterpolator() {
515         return Optional.ofNullable(orbitInterpolator);
516     }
517 
518     /**
519      * Get absolute position-velocity-acceleration interpolator.
520      *
521      * @return optional absolute position-velocity-acceleration interpolator
522      *
523      * @see Optional
524      */
525     public Optional<TimeInterpolator<AbsolutePVCoordinates>> getAbsPVAInterpolator() {
526         return Optional.ofNullable(absPVAInterpolator);
527     }
528 
529     /**
530      * Get mass interpolator.
531      *
532      * @return optional mass interpolator
533      *
534      * @see Optional
535      */
536     public Optional<TimeInterpolator<TimeStampedDouble>> getMassInterpolator() {
537         return Optional.ofNullable(massInterpolator);
538     }
539 
540     /**
541      * Get attitude interpolator.
542      *
543      * @return optional attitude interpolator
544      *
545      * @see Optional
546      */
547     public Optional<TimeInterpolator<Attitude>> getAttitudeInterpolator() {
548         return Optional.ofNullable(attitudeInterpolator);
549     }
550 
551     /**
552      * Get additional state interpolator.
553      *
554      * @return optional additional state interpolator
555      *
556      * @see Optional
557      */
558     public Optional<TimeInterpolator<TimeStampedDouble>> getAdditionalStateInterpolator() {
559         return Optional.ofNullable(additionalStateInterpolator);
560     }
561 
562     /**
563      * Check that at least one interpolator is defined.
564      *
565      * @param orbitInterpolatorToCheck orbit interpolator
566      * @param absPVAInterpolatorToCheck absolute position-velocity-acceleration interpolator
567      */
568     private void checkAtLeastOneInterpolator(final TimeInterpolator<Orbit> orbitInterpolatorToCheck,
569                                              final TimeInterpolator<AbsolutePVCoordinates> absPVAInterpolatorToCheck) {
570         if (orbitInterpolatorToCheck == null && absPVAInterpolatorToCheck == null) {
571             throw new OrekitIllegalArgumentException(OrekitMessages.NO_INTERPOLATOR_FOR_STATE_DEFINITION);
572         }
573     }
574 
575     /**
576      * Create empty samples for given additional entries.
577      *
578      * @param additionalEntries tabulated additional entries
579      *
580      * @return empty samples for given additional entries
581      */
582     private Map<String, List<Pair<AbsoluteDate, double[]>>> createAdditionalStateSample(
583             final List<DoubleArrayDictionary.Entry> additionalEntries) {
584         final Map<String, List<Pair<AbsoluteDate, double[]>>> additionalSamples = new HashMap<>(additionalEntries.size());
585 
586         for (final DoubleArrayDictionary.Entry entry : additionalEntries) {
587             additionalSamples.put(entry.getKey(), new ArrayList<>());
588         }
589         return additionalSamples;
590     }
591 
592     /**
593      * Create empty samples for given additional entries.
594      *
595      * @param additionalEntries tabulated additional entries
596      *
597      * @return empty samples for given additional entries
598      */
599     private Map<String, List<Pair<AbsoluteDate, Object>>> createAdditionalDataSample(
600             final List<DataDictionary.Entry> additionalEntries) {
601         final Map<String, List<Pair<AbsoluteDate, Object>>> additionalSamples = new HashMap<>(additionalEntries.size());
602 
603         for (final DataDictionary.Entry entry : additionalEntries) {
604             additionalSamples.put(entry.getKey(), new ArrayList<>());
605         }
606         return additionalSamples;
607     }
608 
609     /**
610      * Interpolate additional values.
611      *  Double arrays are interpolated whereas objects values are kept identical until change.
612      *
613      * @param <T> type of the data
614      * @param interpolationDate interpolation date
615      * @param additionalSamples additional data samples
616      *
617      * @return interpolated additional data values
618      */
619     private <T> Optional<DataDictionary> interpolateAdditionalState(final AbsoluteDate interpolationDate,
620                                                                     final Map<String, List<Pair<AbsoluteDate, T>>> additionalSamples) {
621         final Optional<DataDictionary> interpolatedAdditional;
622 
623         if (additionalSamples.isEmpty()) {
624             interpolatedAdditional = Optional.empty();
625         } else {
626             interpolatedAdditional = Optional.of(new DataDictionary(additionalSamples.size()));
627             for (final Map.Entry<String, List<Pair<AbsoluteDate, T>>> entry : additionalSamples.entrySet()) {
628                 // Get current entry
629                 final List<Pair<AbsoluteDate, T>> currentAdditionalSamples = entry.getValue();
630                 final T currentInterpolatedAdditional;
631                 if (currentAdditionalSamples.getFirst().getValue() instanceof double[]) {
632                     currentInterpolatedAdditional = (T) interpolateAdditionalSamples(interpolationDate, currentAdditionalSamples);
633                 } else {
634                     currentInterpolatedAdditional = currentAdditionalSamples.stream()
635                                                                             .filter(pair -> pair.getKey().isAfter(interpolationDate))
636                                                                             .min(Comparator.comparing(Pair::getKey))
637                                                                             .get()
638                                                                             .getValue();
639                 }
640                 interpolatedAdditional.get().put(entry.getKey(), currentInterpolatedAdditional);
641             }
642         }
643         return interpolatedAdditional;
644     }
645 
646     private <T> double[] interpolateAdditionalSamples(final AbsoluteDate interpolationDate, final List<Pair<AbsoluteDate, T>> currentAdditionalSamples) {
647         // Extract number of values for this specific entry
648         final int nbOfValues = ((double[]) currentAdditionalSamples.getFirst().getValue()).length;
649 
650         // For each value of current additional state entry
651         final double[] currentInterpolatedAdditional = new double[nbOfValues];
652         for (int i = 0; i < nbOfValues; i++) {
653 
654             // Create final index for lambda expression use
655             final int currentIndex = i;
656 
657             // Create sample for specific value of current additional state values
658             final List<TimeStampedDouble> currentValueSample = new ArrayList<>();
659 
660             currentAdditionalSamples.forEach(
661                     currentSamples -> currentValueSample.add(new TimeStampedDouble(currentSamples.getFirst(), ((double[]) currentSamples.getValue())[currentIndex])));
662 
663             // Interpolate
664             currentInterpolatedAdditional[i] = additionalStateInterpolator.interpolate(interpolationDate, currentValueSample).getValue();
665         }
666         return currentInterpolatedAdditional;
667     }
668 
669     /**
670      * Interpolate attitude.
671      * <p>
672      * If no attitude interpolator were defined, create a default inertial provider with respect to the output frame.
673      *
674      * @param interpolationDate interpolation date
675      * @param attitudes attitudes sample
676      * @param pvProvider position-velocity-acceleration coordinates provider
677      *
678      * @return interpolated attitude if attitude interpolator is present, default attitude otherwise
679      */
680     private Attitude interpolateAttitude(final AbsoluteDate interpolationDate, final List<Attitude> attitudes,
681                                          final PVCoordinatesProvider pvProvider) {
682         if (attitudes.isEmpty()) {
683             final AttitudeProvider attitudeProvider = new FrameAlignedProvider(outputFrame);
684             return attitudeProvider.getAttitude(pvProvider, interpolationDate, outputFrame);
685         } else {
686             return attitudeInterpolator.interpolate(interpolationDate, attitudes);
687         }
688     }
689 }