1   /* Copyright 2002-2025 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 org.hipparchus.exception.LocalizedCoreFormats;
20  import org.hipparchus.exception.MathIllegalStateException;
21  import org.hipparchus.geometry.euclidean.threed.Vector3D;
22  import org.hipparchus.util.FastMath;
23  import org.orekit.attitudes.Attitude;
24  import org.orekit.attitudes.AttitudeProvider;
25  import org.orekit.attitudes.FrameAlignedProvider;
26  import org.orekit.errors.OrekitException;
27  import org.orekit.errors.OrekitIllegalArgumentException;
28  import org.orekit.errors.OrekitIllegalStateException;
29  import org.orekit.errors.OrekitMessages;
30  import org.orekit.frames.Frame;
31  import org.orekit.frames.StaticTransform;
32  import org.orekit.frames.Transform;
33  import org.orekit.orbits.Orbit;
34  import org.orekit.propagation.numerical.NumericalPropagator;
35  import org.orekit.time.AbsoluteDate;
36  import org.orekit.time.TimeOffset;
37  import org.orekit.time.TimeShiftable;
38  import org.orekit.time.TimeStamped;
39  import org.orekit.utils.AbsolutePVCoordinates;
40  import org.orekit.utils.DataDictionary;
41  import org.orekit.utils.DoubleArrayDictionary;
42  import org.orekit.utils.TimeStampedPVCoordinates;
43  
44  /** This class is the representation of a complete state holding orbit, attitude
45   * and mass information at a given date, meant primarily for propagation.
46   *
47   * <p>It contains an {@link Orbit}, or an {@link AbsolutePVCoordinates} if there
48   * is no definite central body, plus the current mass and attitude at the intrinsic
49   * {@link AbsoluteDate}. Quantities are guaranteed to be consistent in terms
50   * of date and reference frame. The spacecraft state may also contain additional
51   * data, which are simply named.
52   * </p>
53   * <p>
54   * The state can be slightly shifted to close dates. This actual shift varies
55   * between {@link Orbit} and {@link AbsolutePVCoordinates}.
56   * For attitude it is a linear extrapolation taking the spin rate into account.
57   * Same thing for the mass, with the rate staying constant.
58   * It is <em>not</em> intended as a replacement for proper
59   * orbit and attitude propagation but should be sufficient for either small
60   * time shifts or coarse accuracy.
61   * </p>
62   * <p>
63   * The instance <code>SpacecraftState</code> is guaranteed to be immutable.
64   * </p>
65   * @see NumericalPropagator
66   * @author Fabien Maussion
67   * @author V&eacute;ronique Pommier-Maurussane
68   * @author Luc Maisonobe
69   */
70  public class SpacecraftState implements TimeStamped, TimeShiftable<SpacecraftState> {
71  
72      /** Default mass. */
73      public static final double DEFAULT_MASS = 1000.0;
74  
75      /**
76       * tolerance on date comparison in {@link #checkConsistency(Orbit, Attitude)}. 100 ns
77       * corresponds to sub-mm accuracy at LEO orbital velocities.
78       */
79      private static final double DATE_INCONSISTENCY_THRESHOLD = 100e-9;
80  
81      /** Orbital state. */
82      private final Orbit orbit;
83  
84      /** Trajectory state, when it is not an orbit. */
85      private final AbsolutePVCoordinates absPva;
86  
87      /** Attitude. */
88      private final Attitude attitude;
89  
90      /** Current mass (kg). */
91      private final double mass;
92  
93      /** Mass rate (kg/s). */
94      private final double massRate;
95  
96      /** Additional data, can be any object (String, double[], etc.). */
97      private final DataDictionary additional;
98  
99      /** Additional states derivatives.
100      * @since 11.1
101      */
102     private final DoubleArrayDictionary additionalDot;
103 
104     /** Build a spacecraft state from orbit only.
105      * <p>Attitude and mass are set to unspecified non-null arbitrary values.</p>
106      * @param orbit the orbit
107      */
108     public SpacecraftState(final Orbit orbit) {
109         this(orbit, getDefaultAttitudeProvider(orbit.getFrame())
110                         .getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
111              DEFAULT_MASS, null, null);
112     }
113 
114     /** Build a spacecraft state from orbit and attitude. Kept for performance.
115      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
116      * @param orbit the orbit
117      * @param attitude attitude
118      * @exception IllegalArgumentException if orbit and attitude dates
119      * or frames are not equal
120      */
121     public SpacecraftState(final Orbit orbit, final Attitude attitude)
122         throws IllegalArgumentException {
123         this(orbit, attitude, DEFAULT_MASS, null, null);
124         checkConsistency(orbit, attitude);
125     }
126 
127     /** Build a spacecraft state from orbit, attitude, mass, additional states and derivatives.
128      * @param orbit the orbit
129      * @param attitude attitude
130      * @param mass the mass (kg)
131      * @param additional additional data (may be null if no additional states are available)
132      * @param additionalDot additional states derivatives (may be null if no additional states derivatives are available)
133      * @exception IllegalArgumentException if orbit and attitude dates
134      * or frames are not equal
135      * @since 13.0
136      */
137     public SpacecraftState(final Orbit orbit, final Attitude attitude, final double mass,
138                            final DataDictionary additional, final DoubleArrayDictionary additionalDot)
139         throws IllegalArgumentException {
140         this(orbit, null, attitude, mass, 0., additional, additionalDot, true);
141         checkConsistency(orbit, attitude);
142     }
143 
144     /** Build a spacecraft state from position-velocity-acceleration only.
145      * <p>Attitude and mass are set to unspecified non-null arbitrary values.</p>
146      * @param absPva position-velocity-acceleration
147      */
148     public SpacecraftState(final AbsolutePVCoordinates absPva) {
149         this(absPva, getDefaultAttitudeProvider(absPva.getFrame())
150                         .getAttitude(absPva, absPva.getDate(), absPva.getFrame()),
151              DEFAULT_MASS, null, null);
152     }
153 
154     /** Build a spacecraft state from position-velocity-acceleration and attitude. Kept for performance.
155      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
156      * @param absPva position-velocity-acceleration
157      * @param attitude attitude
158      * @exception IllegalArgumentException if orbit and attitude dates
159      * or frames are not equal
160      */
161     public SpacecraftState(final AbsolutePVCoordinates absPva, final Attitude attitude)
162         throws IllegalArgumentException {
163         this(absPva, attitude, DEFAULT_MASS, null, null);
164         checkConsistency(absPva, attitude);
165     }
166 
167     /** Build a spacecraft state from position-velocity-acceleration, attitude, mass and additional states and derivatives.
168      * @param absPva position-velocity-acceleration
169      * @param attitude attitude
170      * @param mass the mass (kg)
171      * @param additional additional data (may be null if no additional data are available)
172      * @param additionalDot additional states derivatives(may be null if no additional states derivatives are available)
173      * @exception IllegalArgumentException if orbit and attitude dates
174      * or frames are not equal
175      * @since 13.0
176      */
177     public SpacecraftState(final AbsolutePVCoordinates absPva, final Attitude attitude, final double mass,
178                            final DataDictionary additional, final DoubleArrayDictionary additionalDot)
179         throws IllegalArgumentException {
180         this(null, absPva, attitude, mass, 0., additional, additionalDot, true);
181         checkConsistency(absPva, attitude);
182     }
183 
184     /** Full, private constructor.
185      * @param orbit the orbit
186      * @param absPva absolute position-velocity
187      * @param attitude attitude
188      * @param mass the mass (kg)
189      * @param massRate the mass rate (kg/s)
190      * @param additional additional data (may be null if no additional states are available)
191      * @param additionalDot additional states derivatives (may be null if no additional states derivatives are available)
192      * @param deepCopy flag to copy dictionaries (additional data and derivatives)
193      * @exception IllegalArgumentException if orbit and attitude dates
194      * or frames are not equal
195      * @since 14.0
196      */
197     private SpacecraftState(final Orbit orbit, final AbsolutePVCoordinates absPva,
198                             final Attitude attitude, final double mass, final double massRate,
199                             final DataDictionary additional, final DoubleArrayDictionary additionalDot,
200                             final boolean deepCopy)
201             throws IllegalArgumentException {
202         this.orbit      = orbit;
203         this.absPva     = absPva;
204         this.attitude   = attitude;
205         this.mass       = mass;
206         this.massRate   = massRate;
207         if (deepCopy) {
208             this.additional = additional == null ? new DataDictionary() : new DataDictionary(additional);
209             this.additionalDot = additionalDot == null ? new DoubleArrayDictionary() : new DoubleArrayDictionary(additionalDot);
210         } else {
211             this.additional = additional == null ? new DataDictionary() : additional;
212             this.additionalDot = additionalDot == null ? new DoubleArrayDictionary() : additionalDot;
213         }
214     }
215 
216     /**
217      * Create a new instance with input mass.
218      * @param newMass mass
219      * @return new state
220      * @since 13.0
221      */
222     public SpacecraftState withMass(final double newMass) {
223         return new SpacecraftState(orbit, absPva, attitude, newMass, massRate, additional, additionalDot, false);
224     }
225 
226     /**
227      * Create a new instance with input mass rate.
228      * @param newMassRate mass rate
229      * @return new state
230      * @since 14.0
231      */
232     public SpacecraftState withMassRate(final double newMassRate) {
233         return new SpacecraftState(orbit, absPva, attitude, mass, newMassRate, additional, additionalDot, false);
234     }
235 
236     /**
237      * Create a new instance with input attitude.
238      * @param newAttitude attitude
239      * @return new state
240      * @since 13.0
241      */
242     public SpacecraftState withAttitude(final Attitude newAttitude) {
243         if (isOrbitDefined()) {
244             checkConsistency(orbit, newAttitude);
245         } else {
246             checkConsistency(absPva, newAttitude);
247         }
248         return new SpacecraftState(orbit, absPva, newAttitude, mass, massRate, additional, additionalDot, false);
249     }
250 
251     /**
252      * Create a new instance with input additional data.
253      * @param dataDictionary additional data
254      * @return new state
255      * @since 13.0
256      */
257     public SpacecraftState withAdditionalData(final DataDictionary dataDictionary) {
258         return new SpacecraftState(orbit, absPva, attitude, mass, massRate, dataDictionary, additionalDot, false);
259     }
260 
261     /**
262      * Create a new instance with input additional data.
263      * @param additionalStateDerivatives additional state derivatives
264      * @return new state
265      * @since 13.0
266      */
267     public SpacecraftState withAdditionalStatesDerivatives(final DoubleArrayDictionary additionalStateDerivatives) {
268         return new SpacecraftState(orbit, absPva, attitude, mass, massRate, additional, additionalStateDerivatives, false);
269     }
270 
271     /** Add an additional data.
272      * <p>
273      * {@link SpacecraftState SpacecraftState} instances are immutable,
274      * so this method does <em>not</em> change the instance, but rather
275      * creates a new instance, which has the same orbit, attitude, mass
276      * and additional states as the original instance, except it also
277      * has the specified state. If the original instance already had an
278      * additional data with the same name, it will be overridden. If it
279      * did not have any additional state with that name, the new instance
280      * will have one more additional state than the original instance.
281      * </p>
282      * @param name name of the additional data (names containing "orekit"
283      * with any case are reserved for the library internal use)
284      * @param value value of the additional data
285      * @return a new instance, with the additional data added
286      * @see #hasAdditionalData(String)
287      * @see #getAdditionalData(String)
288      * @see #getAdditionalDataValues()
289      * @since 13.0
290      */
291     public SpacecraftState addAdditionalData(final String name, final Object value) {
292         final DataDictionary newDict = new DataDictionary(additional);
293         if (value instanceof double[]) {
294             newDict.put(name, ((double[]) value).clone());
295         } else if (value instanceof Double) {
296             newDict.put(name, new double[] {(double) value});
297         } else {
298             newDict.put(name, value);
299         }
300         return withAdditionalData(newDict);
301     }
302 
303     /** Add an additional state derivative.
304      * <p>
305      * {@link SpacecraftState SpacecraftState} instances are immutable,
306      * so this method does <em>not</em> change the instance, but rather
307      * creates a new instance, which has the same components as the original
308      * instance, except it also has the specified state derivative. If the
309      * original instance already had an additional state derivative with the
310      * same name, it will be overridden. If it did not have any additional
311      * state derivative with that name, the new instance will have one more
312      * additional state derivative than the original instance.
313      * </p>
314      * @param name name of the additional state derivative (names containing "orekit"
315      * with any case are reserved for the library internal use)
316      * @param value value of the additional state derivative
317      * @return a new instance, with the additional state added
318      * @see #hasAdditionalStateDerivative(String)
319      * @see #getAdditionalStateDerivative(String)
320      * @see #getAdditionalStatesDerivatives()
321      * @since 11.1
322      */
323     public SpacecraftState addAdditionalStateDerivative(final String name, final double... value) {
324         final DoubleArrayDictionary newDict = new DoubleArrayDictionary(additionalDot);
325         newDict.put(name, value.clone());
326         return withAdditionalStatesDerivatives(newDict);
327     }
328 
329     /** Check orbit and attitude dates are equal.
330      * @param orbit the orbit
331      * @param attitude attitude
332      * @exception IllegalArgumentException if orbit and attitude dates
333      * are not equal
334      */
335     private static void checkConsistency(final Orbit orbit, final Attitude attitude)
336         throws IllegalArgumentException {
337         checkDateAndFrameConsistency(attitude, orbit.getDate(), orbit.getFrame());
338     }
339 
340     /** Defines provider for default Attitude when not passed to constructor.
341      * Currently chosen arbitrarily as aligned with input frame.
342      * It is also used in {@link FieldSpacecraftState}.
343      * @param frame reference frame
344      * @return default attitude provider
345      * @since 12.0
346      */
347     static AttitudeProvider getDefaultAttitudeProvider(final Frame frame) {
348         return new FrameAlignedProvider(frame);
349     }
350 
351     /** Check if the state contains an orbit part.
352      * <p>
353      * A state contains either an {@link AbsolutePVCoordinates absolute
354      * position-velocity-acceleration} or an {@link Orbit orbit}.
355      * </p>
356      * @return true if state contains an orbit (in which case {@link #getOrbit()}
357      * will not throw an exception), or false if the state contains an
358      * absolut position-velocity-acceleration (in which case {@link #getAbsPVA()}
359      * will not throw an exception)
360      */
361     public boolean isOrbitDefined() {
362         return orbit != null;
363     }
364 
365     /** Check AbsolutePVCoordinates and attitude dates are equal.
366      * @param absPva position-velocity-acceleration
367      * @param attitude attitude
368      * @exception IllegalArgumentException if orbit and attitude dates
369      * are not equal
370      */
371     private static void checkConsistency(final AbsolutePVCoordinates absPva, final Attitude attitude)
372         throws IllegalArgumentException {
373         checkDateAndFrameConsistency(attitude, absPva.getDate(), absPva.getFrame());
374     }
375 
376     /** Check attitude frame and epoch.
377      * @param attitude attitude
378      * @param date epoch to verify
379      * @param frame frame to verify
380      */
381     private static void checkDateAndFrameConsistency(final Attitude attitude, final AbsoluteDate date, final Frame frame) {
382         if (FastMath.abs(date.durationFrom(attitude.getDate())) >
383                 DATE_INCONSISTENCY_THRESHOLD) {
384             throw new OrekitIllegalArgumentException(OrekitMessages.ORBIT_AND_ATTITUDE_DATES_MISMATCH,
385                     date, attitude.getDate());
386         }
387         if (frame != attitude.getReferenceFrame()) {
388             throw new OrekitIllegalArgumentException(OrekitMessages.FRAMES_MISMATCH,
389                     frame.getName(),
390                     attitude.getReferenceFrame().getName());
391         }
392     }
393 
394     /** Get a time-shifted state.
395      * <p>
396      * The state can be slightly shifted to close dates. This shift is based on
397      * simple models. For orbits, the model is a Keplerian one if no derivatives
398      * are available in the orbit, or Keplerian plus quadratic effect of the
399      * non-Keplerian acceleration if derivatives are available. For attitude,
400      * a polynomial model is used. A linear extrapolation applies to the mass,
401      * with its rate staying the same. Additional states are also changed linearly if their rates are available.
402      * Shifting is <em>not</em> intended as a replacement for proper orbit
403      * and attitude propagation but should be sufficient for small time shifts
404      * or coarse accuracy.
405      * </p>
406      * <p>
407      * As a rough order of magnitude, the following table shows the extrapolation
408      * errors obtained between this simple shift method and an {@link
409      * NumericalPropagator numerical
410      * propagator} for a low Earth Sun Synchronous Orbit, with a 20x20 gravity field,
411      * Sun and Moon third bodies attractions, drag and solar radiation pressure.
412      * Beware that these results will be different for other orbits.
413      * </p>
414      * <table border="1">
415      * <caption>Extrapolation Error</caption>
416      * <tr style="background-color: #ccccff"><th>interpolation time (s)</th>
417      * <th>position error without derivatives (m)</th><th>position error with derivatives (m)</th></tr>
418      * <tr><td style="background-color: #eeeeff; padding:5px"> 60</td><td>  18</td><td> 1.1</td></tr>
419      * <tr><td style="background-color: #eeeeff; padding:5px">120</td><td>  72</td><td> 9.1</td></tr>
420      * <tr><td style="background-color: #eeeeff; padding:5px">300</td><td> 447</td><td> 140</td></tr>
421      * <tr><td style="background-color: #eeeeff; padding:5px">600</td><td>1601</td><td>1067</td></tr>
422      * <tr><td style="background-color: #eeeeff; padding:5px">900</td><td>3141</td><td>3307</td></tr>
423      * </table>
424      * @param dt time shift in seconds
425      * @return a new state, shifted with respect to the instance (which is immutable)
426      */
427     @Override
428     public SpacecraftState shiftedBy(final double dt) {
429         if (isOrbitDefined()) {
430             return new SpacecraftState(orbit.shiftedBy(dt), null, attitude.shiftedBy(dt), mass + dt * massRate,
431                                        massRate, shiftAdditional(dt), new DoubleArrayDictionary(additionalDot), false);
432         } else {
433             return new SpacecraftState(null, absPva.shiftedBy(dt), attitude.shiftedBy(dt), mass + dt * massRate,
434                                        massRate, shiftAdditional(dt), new DoubleArrayDictionary(additionalDot), false);
435         }
436     }
437 
438     /** Get a time-shifted state.
439      * <p>
440      * The state can be slightly shifted to close dates. This shift is based on
441      * simple models. For orbits, the model is a Keplerian one if no derivatives
442      * are available in the orbit, or Keplerian plus quadratic effect of the
443      * non-Keplerian acceleration if derivatives are available. For attitude,
444      * a polynomial model is used. A linear extrapolation applies to the mass,
445      * with its rate staying the same. Additional states are also changed linearly if their rates are available.
446      * Shifting is <em>not</em> intended as a replacement for proper orbit
447      * and attitude propagation but should be sufficient for small time shifts
448      * or coarse accuracy.
449      * </p>
450      * <p>
451      * As a rough order of magnitude, the following table shows the extrapolation
452      * errors obtained between this simple shift method and an {@link
453      * NumericalPropagator numerical
454      * propagator} for a low Earth Sun Synchronous Orbit, with a 20x20 gravity field,
455      * Sun and Moon third bodies attractions, drag and solar radiation pressure.
456      * Beware that these results will be different for other orbits.
457      * </p>
458      * <table border="1">
459      * <caption>Extrapolation Error</caption>
460      * <tr style="background-color: #ccccff"><th>interpolation time (s)</th>
461      * <th>position error without derivatives (m)</th><th>position error with derivatives (m)</th></tr>
462      * <tr><td style="background-color: #eeeeff; padding:5px"> 60</td><td>  18</td><td> 1.1</td></tr>
463      * <tr><td style="background-color: #eeeeff; padding:5px">120</td><td>  72</td><td> 9.1</td></tr>
464      * <tr><td style="background-color: #eeeeff; padding:5px">300</td><td> 447</td><td> 140</td></tr>
465      * <tr><td style="background-color: #eeeeff; padding:5px">600</td><td>1601</td><td>1067</td></tr>
466      * <tr><td style="background-color: #eeeeff; padding:5px">900</td><td>3141</td><td>3307</td></tr>
467      * </table>
468      * @param dt time shift in seconds
469      * @return a new state, shifted with respect to the instance (which is immutable)
470      * @since 13.0
471      */
472     @Override
473     public SpacecraftState shiftedBy(final TimeOffset dt) {
474         final double dtDouble = dt.toDouble();
475         if (isOrbitDefined()) {
476             return new SpacecraftState(orbit.shiftedBy(dt), null, attitude.shiftedBy(dt), mass + dtDouble * massRate,
477                                        massRate, shiftAdditional(dtDouble), new DoubleArrayDictionary(additionalDot), false);
478         } else {
479             return new SpacecraftState(null, absPva.shiftedBy(dt), attitude.shiftedBy(dt), mass + dtDouble * massRate,
480                                        massRate, shiftAdditional(dtDouble), new DoubleArrayDictionary(additionalDot), false);
481         }
482     }
483 
484     /** Shift additional states.
485      * @param dt time shift in seconds
486      * @return shifted additional states
487      * @since 11.1.1
488      */
489     private DataDictionary shiftAdditional(final double dt) {
490 
491         // fast handling when there are no derivatives at all
492         if (additionalDot.size() == 0) {
493             return additional;
494         }
495 
496         // there are derivatives, we need to take them into account in the additional state
497         final DataDictionary shifted = new DataDictionary(additional);
498         for (final DoubleArrayDictionary.Entry dotEntry : additionalDot.getData()) {
499             final DataDictionary.Entry entry = shifted.getEntry(dotEntry.getKey());
500             if (entry != null) {
501                 entry.scaledIncrement(dt, dotEntry);
502             }
503         }
504 
505         return shifted;
506 
507     }
508 
509     /** Get the absolute position-velocity-acceleration.
510      * <p>
511      * A state contains either an {@link AbsolutePVCoordinates absolute
512      * position-velocity-acceleration} or an {@link Orbit orbit}. Which
513      * one is present can be checked using {@link #isOrbitDefined()}.
514      * </p>
515      * @return absolute position-velocity-acceleration
516      * @exception OrekitIllegalStateException if position-velocity-acceleration is null,
517      * which mean the state rather contains an {@link Orbit}
518      * @see #isOrbitDefined()
519      * @see #getOrbit()
520      */
521     public AbsolutePVCoordinates getAbsPVA() throws OrekitIllegalStateException {
522         if (isOrbitDefined()) {
523             throw new OrekitIllegalStateException(OrekitMessages.UNDEFINED_ABSOLUTE_PVCOORDINATES);
524         }
525         return absPva;
526     }
527 
528     /** Get the current orbit.
529      * <p>
530      * A state contains either an {@link AbsolutePVCoordinates absolute
531      * position-velocity-acceleration} or an {@link Orbit orbit}. Which
532      * one is present can be checked using {@link #isOrbitDefined()}.
533      * </p>
534      * @return the orbit
535      * @exception OrekitIllegalStateException if orbit is null,
536      * which means the state rather contains an {@link AbsolutePVCoordinates absolute
537      * position-velocity-acceleration}
538      * @see #isOrbitDefined()
539      * @see #getAbsPVA()
540      */
541     public Orbit getOrbit() throws OrekitIllegalStateException {
542         if (orbit == null) {
543             throw new OrekitIllegalStateException(OrekitMessages.UNDEFINED_ORBIT);
544         }
545         return orbit;
546     }
547 
548     /** {@inheritDoc} */
549     @Override
550     public AbsoluteDate getDate() {
551         return (absPva == null) ? orbit.getDate() : absPva.getDate();
552     }
553 
554     /** Get the defining frame.
555      * @return the frame in which state is defined
556      */
557     public Frame getFrame() {
558         return isOrbitDefined() ? orbit.getFrame() : absPva.getFrame();
559     }
560 
561     /** Check if an additional data is available.
562      * @param name name of the additional data
563      * @return true if the additional data is available
564      * @see #addAdditionalData(String, Object)
565      * @see #getAdditionalState(String)
566      * @see #getAdditionalData(String)
567      * @see #getAdditionalDataValues()
568      */
569     public boolean hasAdditionalData(final String name) {
570         return additional.getEntry(name) != null;
571     }
572 
573     /** Check if an additional state derivative is available.
574      * @param name name of the additional state derivative
575      * @return true if the additional state derivative is available
576      * @see #addAdditionalStateDerivative(String, double[])
577      * @see #getAdditionalStateDerivative(String)
578      * @see #getAdditionalStatesDerivatives()
579      * @since 11.1
580      */
581     public boolean hasAdditionalStateDerivative(final String name) {
582         return additionalDot.getEntry(name) != null;
583     }
584 
585     /** Check if two instances have the same set of additional states available.
586      * <p>
587      * Only the names and dimensions of the additional states are compared,
588      * not their values.
589      * </p>
590      * @param state state to compare to instance
591      * @exception MathIllegalStateException if an additional state does not have
592      * the same dimension in both states
593      */
594     public void ensureCompatibleAdditionalStates(final SpacecraftState state)
595         throws MathIllegalStateException {
596 
597         // check instance additional states is a subset of the other one
598         for (final DataDictionary.Entry entry : additional.getData()) {
599             final Object other = state.additional.get(entry.getKey());
600             if (other == null || !entry.getValue().getClass().equals(other.getClass())) {
601                 throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_DATA,
602                                           entry.getKey());
603             }
604             if (other instanceof double[] && ((double[]) other).length != ((double[]) entry.getValue()).length) {
605                 throw new MathIllegalStateException(LocalizedCoreFormats.DIMENSIONS_MISMATCH,
606                                                     ((double[]) other).length, ((double[]) entry.getValue()).length);
607             }
608         }
609 
610         // check instance additional states derivatives is a subset of the other one
611         for (final DoubleArrayDictionary.Entry entry : additionalDot.getData()) {
612             final double[] other = state.additionalDot.get(entry.getKey());
613             if (other == null) {
614                 throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_DATA,
615                                           entry.getKey());
616             }
617             if (other.length != entry.getValue().length) {
618                 throw new MathIllegalStateException(LocalizedCoreFormats.DIMENSIONS_MISMATCH,
619                                                     other.length, entry.getValue().length);
620             }
621         }
622 
623         if (state.additional.size() > additional.size()) {
624             // the other state has more additional states
625             for (final DataDictionary.Entry entry : state.additional.getData()) {
626                 if (additional.getEntry(entry.getKey()) == null) {
627                     throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_DATA,
628                                               entry.getKey());
629                 }
630             }
631         }
632 
633         if (state.additionalDot.size() > additionalDot.size()) {
634             // the other state has more additional states
635             for (final DoubleArrayDictionary.Entry entry : state.additionalDot.getData()) {
636                 if (additionalDot.getEntry(entry.getKey()) == null) {
637                     throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_DATA,
638                                               entry.getKey());
639                 }
640             }
641         }
642 
643     }
644 
645     /**
646      * Get an additional state.
647      *
648      * @param name name of the additional state
649      * @return value of the additional state
650      * @see #hasAdditionalData(String)
651      * @see #getAdditionalDataValues()
652      */
653     public double[] getAdditionalState(final String name) {
654         final Object data = getAdditionalData(name);
655         if (!(data instanceof double[])) {
656             if (data instanceof Double) {
657                 return new double[] {(double) data};
658             } else {
659                 throw new OrekitException(OrekitMessages.ADDITIONAL_STATE_BAD_TYPE, name);
660             }
661         }
662         return (double[]) data;
663     }
664 
665     /**
666      * Get an additional data.
667      *
668      * @param name name of the additional state
669      * @return value of the additional state
670      * @see #addAdditionalData(String, Object)
671      * @see #hasAdditionalData(String)
672      * @see #getAdditionalDataValues()
673      * @since 13.0
674      */
675     public Object getAdditionalData(final String name) {
676         final Object value = additional.get(name);
677         if (value == null) {
678             throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_DATA, name);
679         }
680         return value;
681     }
682 
683     /** Get an additional state derivative.
684      * @param name name of the additional state derivative
685      * @return value of the additional state derivative
686      * @see #addAdditionalStateDerivative(String, double[])
687      * @see #hasAdditionalStateDerivative(String)
688      * @see #getAdditionalStatesDerivatives()
689      * @since 11.1
690      */
691     public double[] getAdditionalStateDerivative(final String name) {
692         final DoubleArrayDictionary.Entry entry = additionalDot.getEntry(name);
693         if (entry == null) {
694             throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_DATA, name);
695         }
696         return entry.getValue();
697     }
698 
699     /** Get an unmodifiable map of additional data.
700      * @return unmodifiable map of additional data
701      * @see #addAdditionalData(String, Object)
702      * @see #hasAdditionalData(String)
703      * @see #getAdditionalState(String)
704      * @since 11.1
705      */
706     public DataDictionary getAdditionalDataValues() {
707         return additional;
708     }
709 
710     /** Get an unmodifiable map of additional states derivatives.
711      * @return unmodifiable map of additional states derivatives
712      * @see #addAdditionalStateDerivative(String, double[])
713      * @see #hasAdditionalStateDerivative(String)
714      * @see #getAdditionalStateDerivative(String)
715      * @since 11.1
716      */
717     public DoubleArrayDictionary getAdditionalStatesDerivatives() {
718         return additionalDot;
719     }
720 
721     /** Compute the transform from state defining frame to spacecraft frame.
722      * <p>The spacecraft frame origin is at the point defined by the orbit
723      * (or absolute position-velocity-acceleration), and its orientation is
724      * defined by the attitude.</p>
725      * @return transform from specified frame to current spacecraft frame
726      */
727     public Transform toTransform() {
728         final TimeStampedPVCoordinates pv = getPVCoordinates();
729         return new Transform(pv.getDate(), pv.negate(), attitude.getOrientation());
730     }
731 
732     /** Compute the static transform from state defining frame to spacecraft frame.
733      * @return static transform from specified frame to current spacecraft frame
734      * @see #toTransform()
735      * @since 12.0
736      */
737     public StaticTransform toStaticTransform() {
738         return StaticTransform.of(getDate(), getPosition().negate(), attitude.getRotation());
739     }
740 
741     /** Get the position in state definition frame.
742      * @return position in state definition frame
743      * @since 12.0
744      * @see #getPVCoordinates()
745      */
746     public Vector3D getPosition() {
747         return isOrbitDefined() ? orbit.getPosition() : absPva.getPosition();
748     }
749 
750     /** Get the velocity in state definition frame.
751      * @return velocity in state definition frame
752      * @since 13.1
753      * @see #getPVCoordinates()
754      */
755     public Vector3D getVelocity() {
756         return isOrbitDefined() ? orbit.getVelocity() : absPva.getVelocity();
757     }
758 
759     /** Get the {@link TimeStampedPVCoordinates} in orbit definition frame.
760      * <p>
761      * Compute the position and velocity of the satellite. This method caches its
762      * results, and recompute them only when the method is called with a new value
763      * for mu. The result is provided as a reference to the internally cached
764      * {@link TimeStampedPVCoordinates}, so the caller is responsible to copy it in a separate
765      * {@link TimeStampedPVCoordinates} if it needs to keep the value for a while.
766      * </p>
767      * @return pvCoordinates in orbit definition frame
768      */
769     public TimeStampedPVCoordinates getPVCoordinates() {
770         return isOrbitDefined() ? orbit.getPVCoordinates() : absPva.getPVCoordinates();
771     }
772 
773     /** Get the position in given output frame.
774      * @param outputFrame frame in which position should be defined
775      * @return position in given output frame
776      * @since 12.0
777      * @see #getPVCoordinates(Frame)
778      */
779     public Vector3D getPosition(final Frame outputFrame) {
780         return isOrbitDefined() ? orbit.getPosition(outputFrame) : absPva.getPosition(outputFrame);
781     }
782 
783     /** Get the {@link TimeStampedPVCoordinates} in given output frame.
784      * <p>
785      * Compute the position and velocity of the satellite. This method caches its
786      * results, and recompute them only when the method is called with a new value
787      * for mu. The result is provided as a reference to the internally cached
788      * {@link TimeStampedPVCoordinates}, so the caller is responsible to copy it in a separate
789      * {@link TimeStampedPVCoordinates} if it needs to keep the value for a while.
790      * </p>
791      * @param outputFrame frame in which coordinates should be defined
792      * @return pvCoordinates in given output frame
793      */
794     public TimeStampedPVCoordinates getPVCoordinates(final Frame outputFrame) {
795         return isOrbitDefined() ? orbit.getPVCoordinates(outputFrame) : absPva.getPVCoordinates(outputFrame);
796     }
797 
798     /** Get the attitude.
799      * @return the attitude.
800      */
801     public Attitude getAttitude() {
802         return attitude;
803     }
804 
805     /** Gets the current mass.
806      * @return the mass (kg)
807      */
808     public double getMass() {
809         return mass;
810     }
811 
812     /** Gets the current mass rate.
813      * @return the mass (kg/S)
814      * @since 14.0
815      */
816     public double getMassRate() {
817         return massRate;
818     }
819 
820     @Override
821     public String toString() {
822         return "SpacecraftState{" +
823                 "orbit=" + orbit +
824                 ", absPva=" + absPva +
825                 ", attitude=" + attitude +
826                 ", mass=" + mass +
827                 ", massRate=" + massRate +
828                 ", additional=" + additional +
829                 ", additionalDot=" + additionalDot +
830                 '}';
831     }
832 }