1   /* Copyright 2002-2023 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.io.Serializable;
20  
21  import org.hipparchus.exception.LocalizedCoreFormats;
22  import org.hipparchus.exception.MathIllegalStateException;
23  import org.hipparchus.geometry.euclidean.threed.Rotation;
24  import org.hipparchus.geometry.euclidean.threed.Vector3D;
25  import org.hipparchus.util.FastMath;
26  import org.orekit.attitudes.Attitude;
27  import org.orekit.attitudes.AttitudeProvider;
28  import org.orekit.attitudes.FrameAlignedProvider;
29  import org.orekit.errors.OrekitException;
30  import org.orekit.errors.OrekitIllegalArgumentException;
31  import org.orekit.errors.OrekitIllegalStateException;
32  import org.orekit.errors.OrekitMessages;
33  import org.orekit.frames.Frame;
34  import org.orekit.frames.StaticTransform;
35  import org.orekit.frames.Transform;
36  import org.orekit.orbits.Orbit;
37  import org.orekit.time.AbsoluteDate;
38  import org.orekit.time.TimeShiftable;
39  import org.orekit.time.TimeStamped;
40  import org.orekit.utils.AbsolutePVCoordinates;
41  import org.orekit.utils.DoubleArrayDictionary;
42  import org.orekit.utils.TimeStampedAngularCoordinates;
43  import org.orekit.utils.TimeStampedPVCoordinates;
44  
45  /** This class is the representation of a complete state holding orbit, attitude
46   * and mass information at a given date, meant primarily for propagation.
47   *
48   * <p>It contains an {@link Orbit}, or an {@link AbsolutePVCoordinates} if there
49   * is no definite central body, plus the current mass and attitude at the intrinsic
50   * {@link AbsoluteDate}. Quantities are guaranteed to be consistent in terms
51   * of date and reference frame. The spacecraft state may also contain additional
52   * states, which are simply named double arrays which can hold any user-defined
53   * data.
54   * </p>
55   * <p>
56   * The state can be slightly shifted to close dates. This actual shift varies
57   * between {@link Orbit} and {@link AbsolutePVCoordinates}.
58   * For attitude it is a linear extrapolation taking the spin rate into account
59   * and no mass change. It is <em>not</em> intended as a replacement for proper
60   * orbit and attitude propagation but should be sufficient for either small
61   * time shifts or coarse accuracy.
62   * </p>
63   * <p>
64   * The instance <code>SpacecraftState</code> is guaranteed to be immutable.
65   * </p>
66   * @see org.orekit.propagation.numerical.NumericalPropagator
67   * @author Fabien Maussion
68   * @author V&eacute;ronique Pommier-Maurussane
69   * @author Luc Maisonobe
70   */
71  public class SpacecraftState
72      implements TimeStamped, TimeShiftable<SpacecraftState>, Serializable {
73  
74      /** Default mass. */
75      public static final double DEFAULT_MASS = 1000.0;
76  
77      /** Serializable UID. */
78      private static final long serialVersionUID = 20211119L;
79  
80      /**
81       * tolerance on date comparison in {@link #checkConsistency(Orbit, Attitude)}. 100 ns
82       * corresponds to sub-mm accuracy at LEO orbital velocities.
83       */
84      private static final double DATE_INCONSISTENCY_THRESHOLD = 100e-9;
85  
86      /** Orbital state. */
87      private final Orbit orbit;
88  
89      /** Trajectory state, when it is not an orbit. */
90      private final AbsolutePVCoordinates absPva;
91  
92      /** Attitude. */
93      private final Attitude attitude;
94  
95      /** Current mass (kg). */
96      private final double mass;
97  
98      /** Additional states. */
99      private final DoubleArrayDictionary additional;
100 
101     /** Additional states derivatives.
102      * @since 11.1
103      */
104     private final DoubleArrayDictionary additionalDot;
105 
106     /** Build a spacecraft state from orbit only.
107      * <p>Attitude and mass are set to unspecified non-null arbitrary values.</p>
108      * @param orbit the orbit
109      */
110     public SpacecraftState(final Orbit orbit) {
111         this(orbit, getDefaultAttitudeProvider(orbit.getFrame())
112                         .getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
113              DEFAULT_MASS, (DoubleArrayDictionary) null);
114     }
115 
116     /** Build a spacecraft state from orbit and attitude.
117      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
118      * @param orbit the orbit
119      * @param attitude attitude
120      * @exception IllegalArgumentException if orbit and attitude dates
121      * or frames are not equal
122      */
123     public SpacecraftState(final Orbit orbit, final Attitude attitude)
124         throws IllegalArgumentException {
125         this(orbit, attitude, DEFAULT_MASS, (DoubleArrayDictionary) null);
126     }
127 
128     /** Create a new instance from orbit and mass.
129      * <p>Attitude law is set to an unspecified default attitude.</p>
130      * @param orbit the orbit
131      * @param mass the mass (kg)
132      */
133     public SpacecraftState(final Orbit orbit, final double mass) {
134         this(orbit, getDefaultAttitudeProvider(orbit.getFrame())
135                         .getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
136              mass, (DoubleArrayDictionary) null);
137     }
138 
139     /** Build a spacecraft state from orbit, attitude and mass.
140      * @param orbit the orbit
141      * @param attitude attitude
142      * @param mass the mass (kg)
143      * @exception IllegalArgumentException if orbit and attitude dates
144      * or frames are not equal
145      */
146     public SpacecraftState(final Orbit orbit, final Attitude attitude, final double mass)
147         throws IllegalArgumentException {
148         this(orbit, attitude, mass, (DoubleArrayDictionary) null);
149     }
150 
151     /** Build a spacecraft state from orbit and additional states.
152      * <p>Attitude and mass are set to unspecified non-null arbitrary values.</p>
153      * @param orbit the orbit
154      * @param additional additional states
155      * @since 11.1
156      */
157     public SpacecraftState(final Orbit orbit, final DoubleArrayDictionary additional) {
158         this(orbit, getDefaultAttitudeProvider(orbit.getFrame())
159                         .getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
160              DEFAULT_MASS, additional);
161     }
162 
163     /** Build a spacecraft state from orbit, attitude and additional states.
164      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
165      * @param orbit the orbit
166      * @param attitude attitude
167      * @param additional additional states
168      * @exception IllegalArgumentException if orbit and attitude dates
169      * or frames are not equal
170      * @since 11.1
171      */
172     public SpacecraftState(final Orbit orbit, final Attitude attitude, final DoubleArrayDictionary additional)
173         throws IllegalArgumentException {
174         this(orbit, attitude, DEFAULT_MASS, additional);
175     }
176 
177     /** Create a new instance from orbit, mass and additional states.
178      * <p>Attitude law is set to an unspecified default attitude.</p>
179      * @param orbit the orbit
180      * @param mass the mass (kg)
181      * @param additional additional states
182      * @since 11.1
183      */
184     public SpacecraftState(final Orbit orbit, final double mass, final DoubleArrayDictionary additional) {
185         this(orbit, getDefaultAttitudeProvider(orbit.getFrame())
186                         .getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
187              mass, additional);
188     }
189 
190     /** Build a spacecraft state from orbit, attitude, mass and additional states.
191      * @param orbit the orbit
192      * @param attitude attitude
193      * @param mass the mass (kg)
194      * @param additional additional states (may be null if no additional states are available)
195      * @exception IllegalArgumentException if orbit and attitude dates
196      * or frames are not equal
197      * @since 11.1
198      */
199     public SpacecraftState(final Orbit orbit, final Attitude attitude,
200                            final double mass, final DoubleArrayDictionary additional)
201         throws IllegalArgumentException {
202         this(orbit, attitude, mass, additional, null);
203     }
204 
205     /** Build a spacecraft state from orbit, attitude, mass, additional states and derivatives.
206      * @param orbit the orbit
207      * @param attitude attitude
208      * @param mass the mass (kg)
209      * @param additional additional states (may be null if no additional states are available)
210      * @param additionalDot additional states derivatives (may be null if no additional states derivatives are available)
211      * @exception IllegalArgumentException if orbit and attitude dates
212      * or frames are not equal
213      * @since 11.1
214      */
215     public SpacecraftState(final Orbit orbit, final Attitude attitude, final double mass,
216                            final DoubleArrayDictionary additional, final DoubleArrayDictionary additionalDot)
217         throws IllegalArgumentException {
218         checkConsistency(orbit, attitude);
219         this.orbit      = orbit;
220         this.absPva     = null;
221         this.attitude   = attitude;
222         this.mass       = mass;
223         if (additional == null) {
224             this.additional = new DoubleArrayDictionary();
225         } else {
226             this.additional = additional;
227         }
228         if (additionalDot == null) {
229             this.additionalDot = new DoubleArrayDictionary();
230         } else {
231             this.additionalDot = new DoubleArrayDictionary(additionalDot);
232         }
233     }
234 
235     /** Build a spacecraft state from position-velocity-acceleration only.
236      * <p>Attitude and mass are set to unspecified non-null arbitrary values.</p>
237      * @param absPva position-velocity-acceleration
238      */
239     public SpacecraftState(final AbsolutePVCoordinates absPva) {
240         this(absPva, getDefaultAttitudeProvider(absPva.getFrame())
241                         .getAttitude(absPva, absPva.getDate(), absPva.getFrame()),
242              DEFAULT_MASS, (DoubleArrayDictionary) null);
243     }
244 
245     /** Build a spacecraft state from position-velocity-acceleration and attitude.
246      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
247      * @param absPva position-velocity-acceleration
248      * @param attitude attitude
249      * @exception IllegalArgumentException if orbit and attitude dates
250      * or frames are not equal
251      */
252     public SpacecraftState(final AbsolutePVCoordinates absPva, final Attitude attitude)
253         throws IllegalArgumentException {
254         this(absPva, attitude, DEFAULT_MASS, (DoubleArrayDictionary) null);
255     }
256 
257     /** Create a new instance from position-velocity-acceleration and mass.
258      * <p>Attitude law is set to an unspecified default attitude.</p>
259      * @param absPva position-velocity-acceleration
260      * @param mass the mass (kg)
261      */
262     public SpacecraftState(final AbsolutePVCoordinates absPva, final double mass) {
263         this(absPva, getDefaultAttitudeProvider(absPva.getFrame())
264                         .getAttitude(absPva, absPva.getDate(), absPva.getFrame()),
265              mass, (DoubleArrayDictionary) null);
266     }
267 
268     /** Build a spacecraft state from position-velocity-acceleration, attitude and mass.
269      * @param absPva position-velocity-acceleration
270      * @param attitude attitude
271      * @param mass the mass (kg)
272      * @exception IllegalArgumentException if orbit and attitude dates
273      * or frames are not equal
274      */
275     public SpacecraftState(final AbsolutePVCoordinates absPva, final Attitude attitude, final double mass)
276         throws IllegalArgumentException {
277         this(absPva, attitude, mass, (DoubleArrayDictionary) null);
278     }
279 
280     /** Build a spacecraft state from position-velocity-acceleration and additional states.
281      * <p>Attitude and mass are set to unspecified non-null arbitrary values.</p>
282      * @param absPva position-velocity-acceleration
283      * @param additional additional states
284      * @since 11.1
285      */
286     public SpacecraftState(final AbsolutePVCoordinates absPva, final DoubleArrayDictionary additional) {
287         this(absPva, getDefaultAttitudeProvider(absPva.getFrame())
288                         .getAttitude(absPva, absPva.getDate(), absPva.getFrame()),
289              DEFAULT_MASS, additional);
290     }
291 
292     /** Build a spacecraft state from position-velocity-acceleration, attitude and additional states.
293      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
294      * @param absPva position-velocity-acceleration
295      * @param attitude attitude
296      * @param additional additional states
297      * @exception IllegalArgumentException if orbit and attitude dates
298      * or frames are not equal
299      * @since 11.1
300      */
301     public SpacecraftState(final AbsolutePVCoordinates absPva, final Attitude attitude, final DoubleArrayDictionary additional)
302         throws IllegalArgumentException {
303         this(absPva, attitude, DEFAULT_MASS, additional);
304     }
305 
306     /** Create a new instance from position-velocity-acceleration, mass and additional states.
307      * <p>Attitude law is set to an unspecified default attitude.</p>
308      * @param absPva position-velocity-acceleration
309      * @param mass the mass (kg)
310      * @param additional additional states
311      * @since 11.1
312      */
313     public SpacecraftState(final AbsolutePVCoordinates absPva, final double mass, final DoubleArrayDictionary additional) {
314         this(absPva, getDefaultAttitudeProvider(absPva.getFrame())
315                         .getAttitude(absPva, absPva.getDate(), absPva.getFrame()),
316              mass, additional);
317     }
318 
319     /** Build a spacecraft state from position-velocity-acceleration, attitude, mass and additional states.
320      * @param absPva position-velocity-acceleration
321      * @param attitude attitude
322      * @param mass the mass (kg)
323      * @param additional additional states (may be null if no additional states are available)
324      * @exception IllegalArgumentException if orbit and attitude dates
325      * or frames are not equal
326      * @since 11.1
327      */
328     public SpacecraftState(final AbsolutePVCoordinates absPva, final Attitude attitude,
329                            final double mass, final DoubleArrayDictionary additional)
330         throws IllegalArgumentException {
331         this(absPva, attitude, mass, additional, null);
332     }
333 
334     /** Build a spacecraft state from position-velocity-acceleration, attitude, mass and additional states and derivatives.
335      * @param absPva position-velocity-acceleration
336      * @param attitude attitude
337      * @param mass the mass (kg)
338      * @param additional additional states (may be null if no additional states are available)
339      * @param additionalDot additional states derivatives(may be null if no additional states derivatives are available)
340      * @exception IllegalArgumentException if orbit and attitude dates
341      * or frames are not equal
342      * @since 11.1
343      */
344     public SpacecraftState(final AbsolutePVCoordinates absPva, final Attitude attitude, final double mass,
345                            final DoubleArrayDictionary additional, final DoubleArrayDictionary additionalDot)
346         throws IllegalArgumentException {
347         checkConsistency(absPva, attitude);
348         this.orbit      = null;
349         this.absPva     = absPva;
350         this.attitude   = attitude;
351         this.mass       = mass;
352         if (additional == null) {
353             this.additional = new DoubleArrayDictionary();
354         } else {
355             this.additional = new DoubleArrayDictionary(additional);
356         }
357         if (additionalDot == null) {
358             this.additionalDot = new DoubleArrayDictionary();
359         } else {
360             this.additionalDot = new DoubleArrayDictionary(additionalDot);
361         }
362     }
363 
364     /** Add an additional state.
365      * <p>
366      * {@link SpacecraftState SpacecraftState} instances are immutable,
367      * so this method does <em>not</em> change the instance, but rather
368      * creates a new instance, which has the same orbit, attitude, mass
369      * and additional states as the original instance, except it also
370      * has the specified state. If the original instance already had an
371      * additional state with the same name, it will be overridden. If it
372      * did not have any additional state with that name, the new instance
373      * will have one more additional state than the original instance.
374      * </p>
375      * @param name name of the additional state (names containing "orekit"
376      * with any case are reserved for the library internal use)
377      * @param value value of the additional state
378      * @return a new instance, with the additional state added
379      * @see #hasAdditionalState(String)
380      * @see #getAdditionalState(String)
381      * @see #getAdditionalStatesValues()
382      */
383     public SpacecraftState addAdditionalState(final String name, final double... value) {
384         final DoubleArrayDictionary newDict = new DoubleArrayDictionary(additional);
385         newDict.put(name, value.clone());
386         if (isOrbitDefined()) {
387             return new SpacecraftState(orbit, attitude, mass, newDict, additionalDot);
388         } else {
389             return new SpacecraftState(absPva, attitude, mass, newDict, additionalDot);
390         }
391     }
392 
393     /** Add an additional state derivative.
394      * <p>
395      * {@link SpacecraftState SpacecraftState} instances are immutable,
396      * so this method does <em>not</em> change the instance, but rather
397      * creates a new instance, which has the same components as the original
398      * instance, except it also has the specified state derivative. If the
399      * original instance already had an additional state derivative with the
400      * same name, it will be overridden. If it did not have any additional
401      * state derivative with that name, the new instance will have one more
402      * additional state derivative than the original instance.
403      * </p>
404      * @param name name of the additional state derivative (names containing "orekit"
405      * with any case are reserved for the library internal use)
406      * @param value value of the additional state derivative
407      * @return a new instance, with the additional state added
408      * @see #hasAdditionalStateDerivative(String)
409      * @see #getAdditionalStateDerivative(String)
410      * @see #getAdditionalStatesDerivatives()
411      * @since 11.1
412      */
413     public SpacecraftState addAdditionalStateDerivative(final String name, final double... value) {
414         final DoubleArrayDictionary newDict = new DoubleArrayDictionary(additionalDot);
415         newDict.put(name, value.clone());
416         if (isOrbitDefined()) {
417             return new SpacecraftState(orbit, attitude, mass, additional, newDict);
418         } else {
419             return new SpacecraftState(absPva, attitude, mass, additional, newDict);
420         }
421     }
422 
423     /** Check orbit and attitude dates are equal.
424      * @param orbit the orbit
425      * @param attitude attitude
426      * @exception IllegalArgumentException if orbit and attitude dates
427      * are not equal
428      */
429     private static void checkConsistency(final Orbit orbit, final Attitude attitude)
430         throws IllegalArgumentException {
431         if (FastMath.abs(orbit.getDate().durationFrom(attitude.getDate())) >
432             DATE_INCONSISTENCY_THRESHOLD) {
433             throw new OrekitIllegalArgumentException(OrekitMessages.ORBIT_AND_ATTITUDE_DATES_MISMATCH,
434                                                      orbit.getDate(), attitude.getDate());
435         }
436         if (orbit.getFrame() != attitude.getReferenceFrame()) {
437             throw new OrekitIllegalArgumentException(OrekitMessages.FRAMES_MISMATCH,
438                                                      orbit.getFrame().getName(),
439                                                      attitude.getReferenceFrame().getName());
440         }
441     }
442 
443     /** Defines provider for default Attitude when not passed to constructor.
444      * Currently chosen arbitrarily as aligned with input frame.
445      * It is also used in {@link FieldSpacecraftState}.
446      * @param frame reference frame
447      * @return default attitude provider
448      * @since 12.0
449      */
450     static AttitudeProvider getDefaultAttitudeProvider(final Frame frame) {
451         return new FrameAlignedProvider(frame);
452     }
453 
454     /** Check if the state contains an orbit part.
455      * <p>
456      * A state contains either an {@link AbsolutePVCoordinates absolute
457      * position-velocity-acceleration} or an {@link Orbit orbit}.
458      * </p>
459      * @return true if state contains an orbit (in which case {@link #getOrbit()}
460      * will not throw an exception), or false if the state contains an
461      * absolut position-velocity-acceleration (in which case {@link #getAbsPVA()}
462      * will not throw an exception)
463      */
464     public boolean isOrbitDefined() {
465         return orbit != null;
466     }
467 
468     /** Check AbsolutePVCoordinates and attitude dates are equal.
469      * @param absPva position-velocity-acceleration
470      * @param attitude attitude
471      * @exception IllegalArgumentException if orbit and attitude dates
472      * are not equal
473      */
474     private static void checkConsistency(final AbsolutePVCoordinates absPva, final Attitude attitude)
475         throws IllegalArgumentException {
476         if (FastMath.abs(absPva.getDate().durationFrom(attitude.getDate())) >
477             DATE_INCONSISTENCY_THRESHOLD) {
478             throw new OrekitIllegalArgumentException(OrekitMessages.ORBIT_AND_ATTITUDE_DATES_MISMATCH,
479                                                      absPva.getDate(), attitude.getDate());
480         }
481         if (absPva.getFrame() != attitude.getReferenceFrame()) {
482             throw new OrekitIllegalArgumentException(OrekitMessages.FRAMES_MISMATCH,
483                                                      absPva.getFrame().getName(),
484                                                      attitude.getReferenceFrame().getName());
485         }
486     }
487 
488     /** Get a time-shifted state.
489      * <p>
490      * The state can be slightly shifted to close dates. This shift is based on
491      * simple models. For orbits, the model is a Keplerian one if no derivatives
492      * are available in the orbit, or Keplerian plus quadratic effect of the
493      * non-Keplerian acceleration if derivatives are available. For attitude,
494      * a polynomial model is used. Neither mass nor additional states change.
495      * Shifting is <em>not</em> intended as a replacement for proper orbit
496      * and attitude propagation but should be sufficient for small time shifts
497      * or coarse accuracy.
498      * </p>
499      * <p>
500      * As a rough order of magnitude, the following table shows the extrapolation
501      * errors obtained between this simple shift method and an {@link
502      * org.orekit.propagation.numerical.NumericalPropagator numerical
503      * propagator} for a low Earth Sun Synchronous Orbit, with a 20x20 gravity field,
504      * Sun and Moon third bodies attractions, drag and solar radiation pressure.
505      * Beware that these results will be different for other orbits.
506      * </p>
507      * <table border="1">
508      * <caption>Extrapolation Error</caption>
509      * <tr style="background-color: #ccccff"><th>interpolation time (s)</th>
510      * <th>position error without derivatives (m)</th><th>position error with derivatives (m)</th></tr>
511      * <tr><td style="background-color: #eeeeff; padding:5px"> 60</td><td>  18</td><td> 1.1</td></tr>
512      * <tr><td style="background-color: #eeeeff; padding:5px">120</td><td>  72</td><td> 9.1</td></tr>
513      * <tr><td style="background-color: #eeeeff; padding:5px">300</td><td> 447</td><td> 140</td></tr>
514      * <tr><td style="background-color: #eeeeff; padding:5px">600</td><td>1601</td><td>1067</td></tr>
515      * <tr><td style="background-color: #eeeeff; padding:5px">900</td><td>3141</td><td>3307</td></tr>
516      * </table>
517      * @param dt time shift in seconds
518      * @return a new state, shifted with respect to the instance (which is immutable)
519      * except for the mass and additional states which stay unchanged
520      */
521     @Override
522     public SpacecraftState shiftedBy(final double dt) {
523         if (isOrbitDefined()) {
524             return new SpacecraftState(orbit.shiftedBy(dt), attitude.shiftedBy(dt),
525                                        mass, shiftAdditional(dt), additionalDot);
526         } else {
527             return new SpacecraftState(absPva.shiftedBy(dt), attitude.shiftedBy(dt),
528                                        mass, shiftAdditional(dt), additionalDot);
529         }
530     }
531 
532     /** Shift additional states.
533      * @param dt time shift in seconds
534      * @return shifted additional states
535      * @since 11.1.1
536      */
537     private DoubleArrayDictionary shiftAdditional(final double dt) {
538 
539         // fast handling when there are no derivatives at all
540         if (additionalDot.size() == 0) {
541             return additional;
542         }
543 
544         // there are derivatives, we need to take them into account in the additional state
545         final DoubleArrayDictionary shifted = new DoubleArrayDictionary(additional);
546         for (final DoubleArrayDictionary.Entry dotEntry : additionalDot.getData()) {
547             final DoubleArrayDictionary.Entry entry = shifted.getEntry(dotEntry.getKey());
548             if (entry != null) {
549                 entry.scaledIncrement(dt, dotEntry);
550             }
551         }
552 
553         return shifted;
554 
555     }
556 
557     /** Get the absolute position-velocity-acceleration.
558      * <p>
559      * A state contains either an {@link AbsolutePVCoordinates absolute
560      * position-velocity-acceleration} or an {@link Orbit orbit}. Which
561      * one is present can be checked using {@link #isOrbitDefined()}.
562      * </p>
563      * @return absolute position-velocity-acceleration
564      * @exception OrekitIllegalStateException if position-velocity-acceleration is null,
565      * which mean the state rather contains an {@link Orbit}
566      * @see #isOrbitDefined()
567      * @see #getOrbit()
568      */
569     public AbsolutePVCoordinates getAbsPVA() throws OrekitIllegalStateException {
570         if (isOrbitDefined()) {
571             throw new OrekitIllegalStateException(OrekitMessages.UNDEFINED_ABSOLUTE_PVCOORDINATES);
572         }
573         return absPva;
574     }
575 
576     /** Get the current orbit.
577      * <p>
578      * A state contains either an {@link AbsolutePVCoordinates absolute
579      * position-velocity-acceleration} or an {@link Orbit orbit}. Which
580      * one is present can be checked using {@link #isOrbitDefined()}.
581      * </p>
582      * @return the orbit
583      * @exception OrekitIllegalStateException if orbit is null,
584      * which means the state rather contains an {@link AbsolutePVCoordinates absolute
585      * position-velocity-acceleration}
586      * @see #isOrbitDefined()
587      * @see #getAbsPVA()
588      */
589     public Orbit getOrbit() throws OrekitIllegalStateException {
590         if (orbit == null) {
591             throw new OrekitIllegalStateException(OrekitMessages.UNDEFINED_ORBIT);
592         }
593         return orbit;
594     }
595 
596     /** {@inheritDoc} */
597     @Override
598     public AbsoluteDate getDate() {
599         return (absPva == null) ? orbit.getDate() : absPva.getDate();
600     }
601 
602     /** Get the defining frame.
603      * @return the frame in which state is defined
604      */
605     public Frame getFrame() {
606         return isOrbitDefined() ? orbit.getFrame() : absPva.getFrame();
607     }
608 
609     /** Check if an additional state is available.
610      * @param name name of the additional state
611      * @return true if the additional state is available
612      * @see #addAdditionalState(String, double[])
613      * @see #getAdditionalState(String)
614      * @see #getAdditionalStatesValues()
615      */
616     public boolean hasAdditionalState(final String name) {
617         return additional.getEntry(name) != null;
618     }
619 
620     /** Check if an additional state derivative is available.
621      * @param name name of the additional state derivative
622      * @return true if the additional state derivative is available
623      * @see #addAdditionalStateDerivative(String, double[])
624      * @see #getAdditionalStateDerivative(String)
625      * @see #getAdditionalStatesDerivatives()
626      * @since 11.1
627      */
628     public boolean hasAdditionalStateDerivative(final String name) {
629         return additionalDot.getEntry(name) != null;
630     }
631 
632     /** Check if two instances have the same set of additional states available.
633      * <p>
634      * Only the names and dimensions of the additional states are compared,
635      * not their values.
636      * </p>
637      * @param state state to compare to instance
638      * @exception MathIllegalStateException if an additional state does not have
639      * the same dimension in both states
640      */
641     public void ensureCompatibleAdditionalStates(final SpacecraftState state)
642         throws MathIllegalStateException {
643 
644         // check instance additional states is a subset of the other one
645         for (final DoubleArrayDictionary.Entry entry : additional.getData()) {
646             final double[] other = state.additional.get(entry.getKey());
647             if (other == null) {
648                 throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE,
649                                           entry.getKey());
650             }
651             if (other.length != entry.getValue().length) {
652                 throw new MathIllegalStateException(LocalizedCoreFormats.DIMENSIONS_MISMATCH,
653                                                     other.length, entry.getValue().length);
654             }
655         }
656 
657         // check instance additional states derivatives is a subset of the other one
658         for (final DoubleArrayDictionary.Entry entry : additionalDot.getData()) {
659             final double[] other = state.additionalDot.get(entry.getKey());
660             if (other == null) {
661                 throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE,
662                                           entry.getKey());
663             }
664             if (other.length != entry.getValue().length) {
665                 throw new MathIllegalStateException(LocalizedCoreFormats.DIMENSIONS_MISMATCH,
666                                                     other.length, entry.getValue().length);
667             }
668         }
669 
670         if (state.additional.size() > additional.size()) {
671             // the other state has more additional states
672             for (final DoubleArrayDictionary.Entry entry : state.additional.getData()) {
673                 if (additional.getEntry(entry.getKey()) == null) {
674                     throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE,
675                                               entry.getKey());
676                 }
677             }
678         }
679 
680         if (state.additionalDot.size() > additionalDot.size()) {
681             // the other state has more additional states
682             for (final DoubleArrayDictionary.Entry entry : state.additionalDot.getData()) {
683                 if (additionalDot.getEntry(entry.getKey()) == null) {
684                     throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE,
685                                               entry.getKey());
686                 }
687             }
688         }
689 
690     }
691 
692     /** Get an additional state.
693      * @param name name of the additional state
694      * @return value of the additional state
695      * @see #addAdditionalState(String, double[])
696      * @see #hasAdditionalState(String)
697      * @see #getAdditionalStatesValues()
698      */
699     public double[] getAdditionalState(final String name) {
700         final DoubleArrayDictionary.Entry entry = additional.getEntry(name);
701         if (entry == null) {
702             throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE, name);
703         }
704         return entry.getValue();
705     }
706 
707     /** Get an additional state derivative.
708      * @param name name of the additional state derivative
709      * @return value of the additional state derivative
710      * @see #addAdditionalStateDerivative(String, double[])
711      * @see #hasAdditionalStateDerivative(String)
712      * @see #getAdditionalStatesDerivatives()
713      * @since 11.1
714      */
715     public double[] getAdditionalStateDerivative(final String name) {
716         final DoubleArrayDictionary.Entry entry = additionalDot.getEntry(name);
717         if (entry == null) {
718             throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE, name);
719         }
720         return entry.getValue();
721     }
722 
723     /** Get an unmodifiable map of additional states.
724      * @return unmodifiable map of additional states
725      * @see #addAdditionalState(String, double[])
726      * @see #hasAdditionalState(String)
727      * @see #getAdditionalState(String)
728      * @since 11.1
729      */
730     public DoubleArrayDictionary getAdditionalStatesValues() {
731         return additional.unmodifiableView();
732     }
733 
734     /** Get an unmodifiable map of additional states derivatives.
735      * @return unmodifiable map of additional states derivatives
736      * @see #addAdditionalStateDerivative(String, double[])
737      * @see #hasAdditionalStateDerivative(String)
738      * @see #getAdditionalStateDerivative(String)
739      * @since 11.1
740      */
741     public DoubleArrayDictionary getAdditionalStatesDerivatives() {
742         return additionalDot.unmodifiableView();
743     }
744 
745     /** Compute the transform from state defining frame to spacecraft frame.
746      * <p>The spacecraft frame origin is at the point defined by the orbit
747      * (or absolute position-velocity-acceleration), and its orientation is
748      * defined by the attitude.</p>
749      * @return transform from specified frame to current spacecraft frame
750      */
751     public Transform toTransform() {
752         final TimeStampedPVCoordinates pv = getPVCoordinates();
753         return new Transform(pv.getDate(),
754                              new Transform(pv.getDate(), pv.negate()),
755                              new Transform(pv.getDate(), attitude.getOrientation()));
756     }
757 
758     /** Compute the static transform from state defining frame to spacecraft frame.
759      * @return static transform from specified frame to current spacecraft frame
760      * @see #toTransform()
761      * @since 12.0
762      */
763     public StaticTransform toStaticTransform() {
764         return StaticTransform.of(getDate(), getPosition().negate(), attitude.getRotation());
765     }
766 
767     /** Get the central attraction coefficient.
768      * @return mu central attraction coefficient (m^3/s^2), or {code Double.NaN} if the
769      * state contains an absolute position-velocity-acceleration rather than an orbit
770      */
771     public double getMu() {
772         return isOrbitDefined() ? orbit.getMu() : Double.NaN;
773     }
774 
775     /** Get the Keplerian period.
776      * <p>The Keplerian period is computed directly from semi major axis
777      * and central acceleration constant.</p>
778      * @return Keplerian period in seconds, or {code Double.NaN} if the
779      * state contains an absolute position-velocity-acceleration rather
780      * than an orbit
781      */
782     public double getKeplerianPeriod() {
783         return isOrbitDefined() ? orbit.getKeplerianPeriod() : Double.NaN;
784     }
785 
786     /** Get the Keplerian mean motion.
787      * <p>The Keplerian mean motion is computed directly from semi major axis
788      * and central acceleration constant.</p>
789      * @return Keplerian mean motion in radians per second, or {code Double.NaN} if the
790      * state contains an absolute position-velocity-acceleration rather
791      * than an orbit
792      */
793     public double getKeplerianMeanMotion() {
794         return isOrbitDefined() ? orbit.getKeplerianMeanMotion() : Double.NaN;
795     }
796 
797     /** Get the semi-major axis.
798      * @return semi-major axis (m), or {code Double.NaN} if the
799      * state contains an absolute position-velocity-acceleration rather
800      * than an orbit
801      */
802     public double getA() {
803         return isOrbitDefined() ? orbit.getA() : Double.NaN;
804     }
805 
806     /** Get the first component of the eccentricity vector (as per equinoctial parameters).
807      * @return e cos(ω + Ω), first component of eccentricity vector, or {code Double.NaN} if the
808      * state contains an absolute position-velocity-acceleration rather
809      * than an orbit
810      * @see #getE()
811      */
812     public double getEquinoctialEx() {
813         return isOrbitDefined() ? orbit.getEquinoctialEx() : Double.NaN;
814     }
815 
816     /** Get the second component of the eccentricity vector (as per equinoctial parameters).
817      * @return e sin(ω + Ω), second component of the eccentricity vector, or {code Double.NaN} if the
818      * state contains an absolute position-velocity-acceleration rather
819      * than an orbit
820      * @see #getE()
821      */
822     public double getEquinoctialEy() {
823         return isOrbitDefined() ? orbit.getEquinoctialEy() : Double.NaN;
824     }
825 
826     /** Get the first component of the inclination vector (as per equinoctial parameters).
827      * @return tan(i/2) cos(Ω), first component of the inclination vector, or {code Double.NaN} if the
828      * state contains an absolute position-velocity-acceleration rather
829      * than an orbit
830      * @see #getI()
831      */
832     public double getHx() {
833         return isOrbitDefined() ? orbit.getHx() : Double.NaN;
834     }
835 
836     /** Get the second component of the inclination vector (as per equinoctial parameters).
837      * @return tan(i/2) sin(Ω), second component of the inclination vector, or {code Double.NaN} if the
838      * state contains an absolute position-velocity-acceleration rather
839      * than an orbit
840      * @see #getI()
841      */
842     public double getHy() {
843         return isOrbitDefined() ? orbit.getHy() : Double.NaN;
844     }
845 
846     /** Get the true latitude argument (as per equinoctial parameters).
847      * @return v + ω + Ω true longitude argument (rad), or {code Double.NaN} if the
848      * state contains an absolute position-velocity-acceleration rather
849      * than an orbit
850      * @see #getLE()
851      * @see #getLM()
852      */
853     public double getLv() {
854         return isOrbitDefined() ? orbit.getLv() : Double.NaN;
855     }
856 
857     /** Get the eccentric latitude argument (as per equinoctial parameters).
858      * @return E + ω + Ω eccentric longitude argument (rad), or {code Double.NaN} if the
859      * state contains an absolute position-velocity-acceleration rather
860      * than an orbit
861      * @see #getLv()
862      * @see #getLM()
863      */
864     public double getLE() {
865         return isOrbitDefined() ? orbit.getLE() : Double.NaN;
866     }
867 
868     /** Get the mean longitude argument (as per equinoctial parameters).
869      * @return M + ω + Ω mean latitude argument (rad), or {code Double.NaN} if the
870      * state contains an absolute position-velocity-acceleration rather
871      * than an orbit
872      * @see #getLv()
873      * @see #getLE()
874      */
875     public double getLM() {
876         return isOrbitDefined() ? orbit.getLM() : Double.NaN;
877     }
878 
879     // Additional orbital elements
880 
881     /** Get the eccentricity.
882      * @return eccentricity, or {code Double.NaN} if the
883      * state contains an absolute position-velocity-acceleration rather
884      * than an orbit
885      * @see #getEquinoctialEx()
886      * @see #getEquinoctialEy()
887      */
888     public double getE() {
889         return isOrbitDefined() ? orbit.getE() : Double.NaN;
890     }
891 
892     /** Get the inclination.
893      * @return inclination (rad)
894      * @see #getHx()
895      * @see #getHy()
896      */
897     public double getI() {
898         return isOrbitDefined() ? orbit.getI() : Double.NaN;
899     }
900 
901     /** Get the position in orbit definition frame.
902      * @return position in orbit definition frame
903      * @since 12.0
904      * @see #getPVCoordinates()
905      */
906     public Vector3D getPosition() {
907         return isOrbitDefined() ? orbit.getPosition() : absPva.getPosition();
908     }
909 
910     /** Get the {@link TimeStampedPVCoordinates} in orbit definition frame.
911      * <p>
912      * Compute the position and velocity of the satellite. This method caches its
913      * results, and recompute them only when the method is called with a new value
914      * for mu. The result is provided as a reference to the internally cached
915      * {@link TimeStampedPVCoordinates}, so the caller is responsible to copy it in a separate
916      * {@link TimeStampedPVCoordinates} if it needs to keep the value for a while.
917      * </p>
918      * @return pvCoordinates in orbit definition frame
919      */
920     public TimeStampedPVCoordinates getPVCoordinates() {
921         return isOrbitDefined() ? orbit.getPVCoordinates() : absPva.getPVCoordinates();
922     }
923 
924     /** Get the position in given output frame.
925      * @param outputFrame frame in which position should be defined
926      * @return position in given output frame
927      * @since 12.0
928      * @see #getPVCoordinates(Frame)
929      */
930     public Vector3D getPosition(final Frame outputFrame) {
931         return isOrbitDefined() ? orbit.getPosition(outputFrame) : absPva.getPosition(outputFrame);
932     }
933 
934     /** Get the {@link TimeStampedPVCoordinates} in given output frame.
935      * <p>
936      * Compute the position and velocity of the satellite. This method caches its
937      * results, and recompute them only when the method is called with a new value
938      * for mu. The result is provided as a reference to the internally cached
939      * {@link TimeStampedPVCoordinates}, so the caller is responsible to copy it in a separate
940      * {@link TimeStampedPVCoordinates} if it needs to keep the value for a while.
941      * </p>
942      * @param outputFrame frame in which coordinates should be defined
943      * @return pvCoordinates in orbit definition frame
944      */
945     public TimeStampedPVCoordinates getPVCoordinates(final Frame outputFrame) {
946         return isOrbitDefined() ? orbit.getPVCoordinates(outputFrame) : absPva.getPVCoordinates(outputFrame);
947     }
948 
949     /** Get the attitude.
950      * @return the attitude.
951      */
952     public Attitude getAttitude() {
953         return attitude;
954     }
955 
956     /** Gets the current mass.
957      * @return the mass (kg)
958      */
959     public double getMass() {
960         return mass;
961     }
962 
963     /** Replace the instance with a data transfer object for serialization.
964      * @return data transfer object that will be serialized
965      */
966     private Object writeReplace() {
967         return isOrbitDefined() ? new DTOO(this) : new DTOA(this);
968     }
969 
970     /** Internal class used only for serialization. */
971     private static class DTOO implements Serializable {
972 
973         /** Serializable UID. */
974         private static final long serialVersionUID = 20211121L;
975 
976         /** Orbit. */
977         private final Orbit orbit;
978 
979         /** Attitude and mass double values. */
980         private double[] d;
981 
982         /** Additional states. */
983         private final DoubleArrayDictionary additional;
984 
985         /** Additional states derivatives. */
986         private final DoubleArrayDictionary additionalDot;
987 
988         /** Simple constructor.
989          * @param state instance to serialize
990          */
991         private DTOO(final SpacecraftState state) {
992 
993             this.orbit         = state.orbit;
994             this.additional    = state.additional.getData().isEmpty()    ? null : state.additional;
995             this.additionalDot = state.additionalDot.getData().isEmpty() ? null : state.additionalDot;
996 
997             final Rotation rotation             = state.attitude.getRotation();
998             final Vector3D spin                 = state.attitude.getSpin();
999             final Vector3D rotationAcceleration = state.attitude.getRotationAcceleration();
1000             this.d = new double[] {
1001                 rotation.getQ0(), rotation.getQ1(), rotation.getQ2(), rotation.getQ3(),
1002                 spin.getX(), spin.getY(), spin.getZ(),
1003                 rotationAcceleration.getX(), rotationAcceleration.getY(), rotationAcceleration.getZ(),
1004                 state.mass
1005             };
1006 
1007         }
1008 
1009         /** Replace the de-serialized data transfer object with a {@link SpacecraftState}.
1010          * @return replacement {@link SpacecraftState}
1011          */
1012         private Object readResolve() {
1013             return new SpacecraftState(orbit,
1014                                        new Attitude(orbit.getFrame(),
1015                                                     new TimeStampedAngularCoordinates(orbit.getDate(),
1016                                                                                       new Rotation(d[0], d[1], d[2], d[3], false),
1017                                                                                       new Vector3D(d[4], d[5], d[6]),
1018                                                                                       new Vector3D(d[7], d[8], d[9]))),
1019                                        d[10], additional, additionalDot);
1020         }
1021 
1022     }
1023 
1024     /** Internal class used only for serialization. */
1025     private static class DTOA implements Serializable {
1026 
1027         /** Serializable UID. */
1028         private static final long serialVersionUID = 20211121L;
1029 
1030         /** Absolute position-velocity-acceleration. */
1031         private final AbsolutePVCoordinates absPva;
1032 
1033         /** Attitude and mass double values. */
1034         private double[] d;
1035 
1036         /** Additional states. */
1037         private final DoubleArrayDictionary additional;
1038 
1039         /** Additional states derivatives. */
1040         private final DoubleArrayDictionary additionalDot;
1041 
1042         /** Simple constructor.
1043          * @param state instance to serialize
1044          */
1045         private DTOA(final SpacecraftState state) {
1046 
1047             this.absPva        = state.absPva;
1048             this.additional    = state.additional.getData().isEmpty()    ? null : state.additional;
1049             this.additionalDot = state.additionalDot.getData().isEmpty() ? null : state.additionalDot;
1050 
1051             final Rotation rotation             = state.attitude.getRotation();
1052             final Vector3D spin                 = state.attitude.getSpin();
1053             final Vector3D rotationAcceleration = state.attitude.getRotationAcceleration();
1054             this.d = new double[] {
1055                 rotation.getQ0(), rotation.getQ1(), rotation.getQ2(), rotation.getQ3(),
1056                 spin.getX(), spin.getY(), spin.getZ(),
1057                 rotationAcceleration.getX(), rotationAcceleration.getY(), rotationAcceleration.getZ(),
1058                 state.mass
1059             };
1060 
1061         }
1062 
1063         /** Replace the deserialized data transfer object with a {@link SpacecraftState}.
1064          * @return replacement {@link SpacecraftState}
1065          */
1066         private Object readResolve() {
1067             return new SpacecraftState(absPva,
1068                                        new Attitude(absPva.getFrame(),
1069                                                     new TimeStampedAngularCoordinates(absPva.getDate(),
1070                                                                                       new Rotation(d[0], d[1], d[2], d[3], false),
1071                                                                                       new Vector3D(d[4], d[5], d[6]),
1072                                                                                       new Vector3D(d[7], d[8], d[9]))),
1073                                        d[10], additional, additionalDot);
1074         }
1075     }
1076 
1077     @Override
1078     public String toString() {
1079         return "SpacecraftState{" +
1080                 "orbit=" + orbit +
1081                 ", attitude=" + attitude +
1082                 ", mass=" + mass +
1083                 ", additional=" + additional +
1084                 ", additionalDot=" + additionalDot +
1085                 '}';
1086     }
1087 }