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 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         switch (value) {
294             case double[] doubles -> newDict.put(name, doubles.clone());
295             case Double double1 -> newDict.put(name, new double[] {double1});
296             case null, default -> newDict.put(name, value);
297         }
298         return withAdditionalData(newDict);
299     }
300 
301     /** Add an additional state derivative.
302      * <p>
303      * {@link SpacecraftState SpacecraftState} instances are immutable,
304      * so this method does <em>not</em> change the instance, but rather
305      * creates a new instance, which has the same components as the original
306      * instance, except it also has the specified state derivative. If the
307      * original instance already had an additional state derivative with the
308      * same name, it will be overridden. If it did not have any additional
309      * state derivative with that name, the new instance will have one more
310      * additional state derivative than the original instance.
311      * </p>
312      * @param name name of the additional state derivative (names containing "orekit"
313      * with any case are reserved for the library internal use)
314      * @param value value of the additional state derivative
315      * @return a new instance, with the additional state added
316      * @see #hasAdditionalStateDerivative(String)
317      * @see #getAdditionalStateDerivative(String)
318      * @see #getAdditionalStatesDerivatives()
319      * @since 11.1
320      */
321     public SpacecraftState addAdditionalStateDerivative(final String name, final double... value) {
322         final DoubleArrayDictionary newDict = new DoubleArrayDictionary(additionalDot);
323         newDict.put(name, value.clone());
324         return withAdditionalStatesDerivatives(newDict);
325     }
326 
327     /** Check orbit and attitude dates are equal.
328      * @param orbit the orbit
329      * @param attitude attitude
330      * @exception IllegalArgumentException if orbit and attitude dates
331      * are not equal
332      */
333     private static void checkConsistency(final Orbit orbit, final Attitude attitude)
334         throws IllegalArgumentException {
335         checkDateAndFrameConsistency(attitude, orbit.getDate(), orbit.getFrame());
336     }
337 
338     /** Defines provider for default Attitude when not passed to constructor.
339      * Currently chosen arbitrarily as aligned with input frame.
340      * It is also used in {@link FieldSpacecraftState}.
341      * @param frame reference frame
342      * @return default attitude provider
343      * @since 12.0
344      */
345     static AttitudeProvider getDefaultAttitudeProvider(final Frame frame) {
346         return new FrameAlignedProvider(frame);
347     }
348 
349     /** Check if the state contains an orbit part.
350      * <p>
351      * A state contains either an {@link AbsolutePVCoordinates absolute
352      * position-velocity-acceleration} or an {@link Orbit orbit}.
353      * </p>
354      * @return true if state contains an orbit (in which case {@link #getOrbit()}
355      * will not throw an exception), or false if the state contains an
356      * absolut position-velocity-acceleration (in which case {@link #getAbsPVA()}
357      * will not throw an exception)
358      */
359     public boolean isOrbitDefined() {
360         return orbit != null;
361     }
362 
363     /** Check AbsolutePVCoordinates and attitude dates are equal.
364      * @param absPva position-velocity-acceleration
365      * @param attitude attitude
366      * @exception IllegalArgumentException if orbit and attitude dates
367      * are not equal
368      */
369     private static void checkConsistency(final AbsolutePVCoordinates absPva, final Attitude attitude)
370         throws IllegalArgumentException {
371         checkDateAndFrameConsistency(attitude, absPva.getDate(), absPva.getFrame());
372     }
373 
374     /** Check attitude frame and epoch.
375      * @param attitude attitude
376      * @param date epoch to verify
377      * @param frame frame to verify
378      */
379     private static void checkDateAndFrameConsistency(final Attitude attitude, final AbsoluteDate date, final Frame frame) {
380         if (FastMath.abs(date.durationFrom(attitude.getDate())) >
381                 DATE_INCONSISTENCY_THRESHOLD) {
382             throw new OrekitIllegalArgumentException(OrekitMessages.ORBIT_AND_ATTITUDE_DATES_MISMATCH,
383                     date, attitude.getDate());
384         }
385         if (frame != attitude.getReferenceFrame()) {
386             throw new OrekitIllegalArgumentException(OrekitMessages.FRAMES_MISMATCH,
387                     frame.getName(),
388                     attitude.getReferenceFrame().getName());
389         }
390     }
391 
392     /** Get a time-shifted state.
393      * <p>
394      * The state can be slightly shifted to close dates. This shift is based on
395      * simple models. For orbits, the model is a Keplerian one if no derivatives
396      * are available in the orbit, or Keplerian plus quadratic effect of the
397      * non-Keplerian acceleration if derivatives are available. For attitude,
398      * a polynomial model is used. A linear extrapolation applies to the mass,
399      * with its rate staying the same. Additional states are also changed linearly if their rates are available.
400      * Shifting is <em>not</em> intended as a replacement for proper orbit
401      * and attitude propagation but should be sufficient for small time shifts
402      * or coarse accuracy.
403      * </p>
404      * <p>
405      * As a rough order of magnitude, the following table shows the extrapolation
406      * errors obtained between this simple shift method and an {@link
407      * NumericalPropagator numerical
408      * propagator} for a low Earth Sun Synchronous Orbit, with a 20x20 gravity field,
409      * Sun and Moon third bodies attractions, drag and solar radiation pressure.
410      * Beware that these results will be different for other orbits.
411      * </p>
412      * <table border="1">
413      * <caption>Extrapolation Error</caption>
414      * <tr style="background-color: #ccccff"><th>interpolation time (s)</th>
415      * <th>position error without derivatives (m)</th><th>position error with derivatives (m)</th></tr>
416      * <tr><td style="background-color: #eeeeff; padding:5px"> 60</td><td>  18</td><td> 1.1</td></tr>
417      * <tr><td style="background-color: #eeeeff; padding:5px">120</td><td>  72</td><td> 9.1</td></tr>
418      * <tr><td style="background-color: #eeeeff; padding:5px">300</td><td> 447</td><td> 140</td></tr>
419      * <tr><td style="background-color: #eeeeff; padding:5px">600</td><td>1601</td><td>1067</td></tr>
420      * <tr><td style="background-color: #eeeeff; padding:5px">900</td><td>3141</td><td>3307</td></tr>
421      * </table>
422      * @param dt time shift in seconds
423      * @return a new state, shifted with respect to the instance (which is immutable)
424      */
425     @Override
426     public SpacecraftState shiftedBy(final double dt) {
427         if (isOrbitDefined()) {
428             return new SpacecraftState(orbit.shiftedBy(dt), null, attitude.shiftedBy(dt), mass + dt * massRate,
429                                        massRate, shiftAdditional(dt), new DoubleArrayDictionary(additionalDot), false);
430         } else {
431             return new SpacecraftState(null, absPva.shiftedBy(dt), attitude.shiftedBy(dt), mass + dt * massRate,
432                                        massRate, shiftAdditional(dt), new DoubleArrayDictionary(additionalDot), false);
433         }
434     }
435 
436     /** Get a time-shifted state.
437      * <p>
438      * The state can be slightly shifted to close dates. This shift is based on
439      * simple models. For orbits, the model is a Keplerian one if no derivatives
440      * are available in the orbit, or Keplerian plus quadratic effect of the
441      * non-Keplerian acceleration if derivatives are available. For attitude,
442      * a polynomial model is used. A linear extrapolation applies to the mass,
443      * with its rate staying the same. Additional states are also changed linearly if their rates are available.
444      * Shifting is <em>not</em> intended as a replacement for proper orbit
445      * and attitude propagation but should be sufficient for small time shifts
446      * or coarse accuracy.
447      * </p>
448      * <p>
449      * As a rough order of magnitude, the following table shows the extrapolation
450      * errors obtained between this simple shift method and an {@link
451      * NumericalPropagator numerical
452      * propagator} for a low Earth Sun Synchronous Orbit, with a 20x20 gravity field,
453      * Sun and Moon third bodies attractions, drag and solar radiation pressure.
454      * Beware that these results will be different for other orbits.
455      * </p>
456      * <table border="1">
457      * <caption>Extrapolation Error</caption>
458      * <tr style="background-color: #ccccff"><th>interpolation time (s)</th>
459      * <th>position error without derivatives (m)</th><th>position error with derivatives (m)</th></tr>
460      * <tr><td style="background-color: #eeeeff; padding:5px"> 60</td><td>  18</td><td> 1.1</td></tr>
461      * <tr><td style="background-color: #eeeeff; padding:5px">120</td><td>  72</td><td> 9.1</td></tr>
462      * <tr><td style="background-color: #eeeeff; padding:5px">300</td><td> 447</td><td> 140</td></tr>
463      * <tr><td style="background-color: #eeeeff; padding:5px">600</td><td>1601</td><td>1067</td></tr>
464      * <tr><td style="background-color: #eeeeff; padding:5px">900</td><td>3141</td><td>3307</td></tr>
465      * </table>
466      * @param dt time shift in seconds
467      * @return a new state, shifted with respect to the instance (which is immutable)
468      * @since 13.0
469      */
470     @Override
471     public SpacecraftState shiftedBy(final TimeOffset dt) {
472         final double dtDouble = dt.toDouble();
473         if (isOrbitDefined()) {
474             return new SpacecraftState(orbit.shiftedBy(dt), null, attitude.shiftedBy(dt), mass + dtDouble * massRate,
475                                        massRate, shiftAdditional(dtDouble), new DoubleArrayDictionary(additionalDot), false);
476         } else {
477             return new SpacecraftState(null, absPva.shiftedBy(dt), attitude.shiftedBy(dt), mass + dtDouble * massRate,
478                                        massRate, shiftAdditional(dtDouble), new DoubleArrayDictionary(additionalDot), false);
479         }
480     }
481 
482     /** Shift additional states.
483      * @param dt time shift in seconds
484      * @return shifted additional states
485      * @since 11.1.1
486      */
487     private DataDictionary shiftAdditional(final double dt) {
488 
489         // fast handling when there are no derivatives at all
490         if (additionalDot.size() == 0) {
491             return additional;
492         }
493 
494         // there are derivatives, we need to take them into account in the additional state
495         final DataDictionary shifted = new DataDictionary(additional);
496         for (final DoubleArrayDictionary.Entry dotEntry : additionalDot.getData()) {
497             final DataDictionary.Entry entry = shifted.getEntry(dotEntry.getKey());
498             if (entry != null) {
499                 entry.scaledIncrement(dt, dotEntry);
500             }
501         }
502 
503         return shifted;
504 
505     }
506 
507     /** Get the absolute position-velocity-acceleration.
508      * <p>
509      * A state contains either an {@link AbsolutePVCoordinates absolute
510      * position-velocity-acceleration} or an {@link Orbit orbit}. Which
511      * one is present can be checked using {@link #isOrbitDefined()}.
512      * </p>
513      * @return absolute position-velocity-acceleration
514      * @exception OrekitIllegalStateException if position-velocity-acceleration is null,
515      * which mean the state rather contains an {@link Orbit}
516      * @see #isOrbitDefined()
517      * @see #getOrbit()
518      */
519     public AbsolutePVCoordinates getAbsPVA() throws OrekitIllegalStateException {
520         if (isOrbitDefined()) {
521             throw new OrekitIllegalStateException(OrekitMessages.UNDEFINED_ABSOLUTE_PVCOORDINATES);
522         }
523         return absPva;
524     }
525 
526     /** Get the current orbit.
527      * <p>
528      * A state contains either an {@link AbsolutePVCoordinates absolute
529      * position-velocity-acceleration} or an {@link Orbit orbit}. Which
530      * one is present can be checked using {@link #isOrbitDefined()}.
531      * </p>
532      * @return the orbit
533      * @exception OrekitIllegalStateException if orbit is null,
534      * which means the state rather contains an {@link AbsolutePVCoordinates absolute
535      * position-velocity-acceleration}
536      * @see #isOrbitDefined()
537      * @see #getAbsPVA()
538      */
539     public Orbit getOrbit() throws OrekitIllegalStateException {
540         if (orbit == null) {
541             throw new OrekitIllegalStateException(OrekitMessages.UNDEFINED_ORBIT);
542         }
543         return orbit;
544     }
545 
546     /** {@inheritDoc} */
547     @Override
548     public AbsoluteDate getDate() {
549         return (absPva == null) ? orbit.getDate() : absPva.getDate();
550     }
551 
552     /** Get the defining frame.
553      * @return the frame in which state is defined
554      */
555     public Frame getFrame() {
556         return isOrbitDefined() ? orbit.getFrame() : absPva.getFrame();
557     }
558 
559     /** Check if an additional data is available.
560      * @param name name of the additional data
561      * @return true if the additional data is available
562      * @see #addAdditionalData(String, Object)
563      * @see #getAdditionalState(String)
564      * @see #getAdditionalData(String)
565      * @see #getAdditionalDataValues()
566      */
567     public boolean hasAdditionalData(final String name) {
568         return additional.getEntry(name) != null;
569     }
570 
571     /** Check if an additional state derivative is available.
572      * @param name name of the additional state derivative
573      * @return true if the additional state derivative is available
574      * @see #addAdditionalStateDerivative(String, double[])
575      * @see #getAdditionalStateDerivative(String)
576      * @see #getAdditionalStatesDerivatives()
577      * @since 11.1
578      */
579     public boolean hasAdditionalStateDerivative(final String name) {
580         return additionalDot.getEntry(name) != null;
581     }
582 
583     /** Check if two instances have the same set of additional states available.
584      * <p>
585      * Only the names and dimensions of the additional states are compared,
586      * not their values.
587      * </p>
588      * @param state state to compare to instance
589      * @exception MathIllegalStateException if an additional state does not have
590      * the same dimension in both states
591      */
592     public void ensureCompatibleAdditionalStates(final SpacecraftState state)
593         throws MathIllegalStateException {
594 
595         // check instance additional states is a subset of the other one
596         for (final DataDictionary.Entry entry : additional.getData()) {
597             final Object other = state.additional.get(entry.getKey());
598             if (other == null || !entry.getValue().getClass().equals(other.getClass())) {
599                 throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_DATA,
600                                           entry.getKey());
601             }
602             if (other instanceof double[] doubles && doubles.length != ((double[]) entry.getValue()).length) {
603                 throw new MathIllegalStateException(LocalizedCoreFormats.DIMENSIONS_MISMATCH,
604                                                     doubles.length, ((double[]) entry.getValue()).length);
605             }
606         }
607 
608         // check instance additional states derivatives is a subset of the other one
609         for (final DoubleArrayDictionary.Entry entry : additionalDot.getData()) {
610             final double[] other = state.additionalDot.get(entry.getKey());
611             if (other == null) {
612                 throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_DATA,
613                                           entry.getKey());
614             }
615             if (other.length != entry.getValue().length) {
616                 throw new MathIllegalStateException(LocalizedCoreFormats.DIMENSIONS_MISMATCH,
617                                                     other.length, entry.getValue().length);
618             }
619         }
620 
621         if (state.additional.size() > additional.size()) {
622             // the other state has more additional states
623             for (final DataDictionary.Entry entry : state.additional.getData()) {
624                 if (additional.getEntry(entry.getKey()) == null) {
625                     throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_DATA,
626                                               entry.getKey());
627                 }
628             }
629         }
630 
631         if (state.additionalDot.size() > additionalDot.size()) {
632             // the other state has more additional states
633             for (final DoubleArrayDictionary.Entry entry : state.additionalDot.getData()) {
634                 if (additionalDot.getEntry(entry.getKey()) == null) {
635                     throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_DATA,
636                                               entry.getKey());
637                 }
638             }
639         }
640 
641     }
642 
643     /**
644      * Get an additional state.
645      *
646      * @param name name of the additional state
647      * @return value of the additional state
648      * @see #hasAdditionalData(String)
649      * @see #getAdditionalDataValues()
650      */
651     public double[] getAdditionalState(final String name) {
652         final Object data = getAdditionalData(name);
653         if (!(data instanceof double[])) {
654             if (data instanceof Double double1) {
655                 return new double[] {double1};
656             } else {
657                 throw new OrekitException(OrekitMessages.ADDITIONAL_STATE_BAD_TYPE, name);
658             }
659         }
660         return (double[]) data;
661     }
662 
663     /**
664      * Get an additional data.
665      *
666      * @param name name of the additional state
667      * @return value of the additional state
668      * @see #addAdditionalData(String, Object)
669      * @see #hasAdditionalData(String)
670      * @see #getAdditionalDataValues()
671      * @since 13.0
672      */
673     public Object getAdditionalData(final String name) {
674         final Object value = additional.get(name);
675         if (value == null) {
676             throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_DATA, name);
677         }
678         return value;
679     }
680 
681     /** Get an additional state derivative.
682      * @param name name of the additional state derivative
683      * @return value of the additional state derivative
684      * @see #addAdditionalStateDerivative(String, double[])
685      * @see #hasAdditionalStateDerivative(String)
686      * @see #getAdditionalStatesDerivatives()
687      * @since 11.1
688      */
689     public double[] getAdditionalStateDerivative(final String name) {
690         final DoubleArrayDictionary.Entry entry = additionalDot.getEntry(name);
691         if (entry == null) {
692             throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_DATA, name);
693         }
694         return entry.getValue();
695     }
696 
697     /** Get an unmodifiable map of additional data.
698      * @return unmodifiable map of additional data
699      * @see #addAdditionalData(String, Object)
700      * @see #hasAdditionalData(String)
701      * @see #getAdditionalState(String)
702      * @since 11.1
703      */
704     public DataDictionary getAdditionalDataValues() {
705         return additional;
706     }
707 
708     /** Get an unmodifiable map of additional states derivatives.
709      * @return unmodifiable map of additional states derivatives
710      * @see #addAdditionalStateDerivative(String, double[])
711      * @see #hasAdditionalStateDerivative(String)
712      * @see #getAdditionalStateDerivative(String)
713      * @since 11.1
714      */
715     public DoubleArrayDictionary getAdditionalStatesDerivatives() {
716         return additionalDot;
717     }
718 
719     /** Compute the transform from state defining frame to spacecraft frame.
720      * <p>The spacecraft frame origin is at the point defined by the orbit
721      * (or absolute position-velocity-acceleration), and its orientation is
722      * defined by the attitude.</p>
723      * @return transform from specified frame to current spacecraft frame
724      */
725     public Transform toTransform() {
726         final TimeStampedPVCoordinates pv = getPVCoordinates();
727         return new Transform(pv.getDate(), pv.negate(), attitude.getOrientation());
728     }
729 
730     /** Compute the static transform from state defining frame to spacecraft frame.
731      * @return static transform from specified frame to current spacecraft frame
732      * @see #toTransform()
733      * @since 12.0
734      */
735     public StaticTransform toStaticTransform() {
736         return StaticTransform.of(getDate(), getPosition().negate(), attitude.getRotation());
737     }
738 
739     /** Get the position in state definition frame.
740      * @return position in state definition frame
741      * @since 12.0
742      * @see #getPVCoordinates()
743      */
744     public Vector3D getPosition() {
745         return isOrbitDefined() ? orbit.getPosition() : absPva.getPosition();
746     }
747 
748     /** Get the velocity in state definition frame.
749      * @return velocity in state definition frame
750      * @since 13.1
751      * @see #getPVCoordinates()
752      */
753     public Vector3D getVelocity() {
754         return isOrbitDefined() ? orbit.getVelocity() : absPva.getVelocity();
755     }
756 
757     /** Get the {@link TimeStampedPVCoordinates} in orbit definition frame.
758      * <p>
759      * Compute the position and velocity of the satellite. This method caches its
760      * results, and recompute them only when the method is called with a new value
761      * for mu. The result is provided as a reference to the internally cached
762      * {@link TimeStampedPVCoordinates}, so the caller is responsible to copy it in a separate
763      * {@link TimeStampedPVCoordinates} if it needs to keep the value for a while.
764      * </p>
765      * @return pvCoordinates in orbit definition frame
766      */
767     public TimeStampedPVCoordinates getPVCoordinates() {
768         return isOrbitDefined() ? orbit.getPVCoordinates() : absPva.getPVCoordinates();
769     }
770 
771     /** Get the position in given output frame.
772      * @param outputFrame frame in which position should be defined
773      * @return position in given output frame
774      * @since 12.0
775      * @see #getPVCoordinates(Frame)
776      */
777     public Vector3D getPosition(final Frame outputFrame) {
778         return isOrbitDefined() ? orbit.getPosition(outputFrame) : absPva.getPosition(outputFrame);
779     }
780 
781     /** Get the {@link TimeStampedPVCoordinates} in given output frame.
782      * <p>
783      * Compute the position and velocity of the satellite. This method caches its
784      * results, and recompute them only when the method is called with a new value
785      * for mu. The result is provided as a reference to the internally cached
786      * {@link TimeStampedPVCoordinates}, so the caller is responsible to copy it in a separate
787      * {@link TimeStampedPVCoordinates} if it needs to keep the value for a while.
788      * </p>
789      * @param outputFrame frame in which coordinates should be defined
790      * @return pvCoordinates in given output frame
791      */
792     public TimeStampedPVCoordinates getPVCoordinates(final Frame outputFrame) {
793         return isOrbitDefined() ? orbit.getPVCoordinates(outputFrame) : absPva.getPVCoordinates(outputFrame);
794     }
795 
796     /** Get the attitude.
797      * @return the attitude.
798      */
799     public Attitude getAttitude() {
800         return attitude;
801     }
802 
803     /** Gets the current mass.
804      * @return the mass (kg)
805      */
806     public double getMass() {
807         return mass;
808     }
809 
810     /** Gets the current mass rate.
811      * @return the mass (kg/S)
812      * @since 14.0
813      */
814     public double getMassRate() {
815         return massRate;
816     }
817 
818     @Override
819     public String toString() {
820         return "SpacecraftState{" +
821                 "orbit=" + orbit +
822                 ", absPva=" + absPva +
823                 ", attitude=" + attitude +
824                 ", mass=" + mass +
825                 ", massRate=" + massRate +
826                 ", additional=" + additional +
827                 ", additionalDot=" + additionalDot +
828                 '}';
829     }
830 }