1   /* Copyright 2002-2019 CS Systèmes d'Information
2    * Licensed to CS Systèmes d'Information (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  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.stream.Stream;
26  
27  import org.hipparchus.analysis.interpolation.HermiteInterpolator;
28  import org.hipparchus.exception.LocalizedCoreFormats;
29  import org.hipparchus.exception.MathIllegalStateException;
30  import org.hipparchus.geometry.euclidean.threed.Rotation;
31  import org.hipparchus.geometry.euclidean.threed.Vector3D;
32  import org.hipparchus.util.FastMath;
33  import org.orekit.attitudes.Attitude;
34  import org.orekit.attitudes.LofOffset;
35  import org.orekit.errors.OrekitException;
36  import org.orekit.errors.OrekitIllegalArgumentException;
37  import org.orekit.errors.OrekitMessages;
38  import org.orekit.frames.Frame;
39  import org.orekit.frames.LOFType;
40  import org.orekit.frames.Transform;
41  import org.orekit.orbits.Orbit;
42  import org.orekit.time.AbsoluteDate;
43  import org.orekit.time.TimeInterpolable;
44  import org.orekit.time.TimeShiftable;
45  import org.orekit.time.TimeStamped;
46  import org.orekit.utils.TimeStampedAngularCoordinates;
47  import org.orekit.utils.TimeStampedPVCoordinates;
48  
49  
50  /** This class is the representation of a complete state holding orbit, attitude
51   * and mass information at a given date.
52   *
53   * <p>It contains an {@link Orbit orbital state} at a current
54   * {@link AbsoluteDate} both handled by an {@link Orbit}, plus the current
55   * mass and attitude. Orbit and state are guaranteed to be consistent in terms
56   * of date and reference frame. The spacecraft state may also contain additional
57   * states, which are simply named double arrays which can hold any user-defined
58   * data.
59   * </p>
60   * <p>
61   * The state can be slightly shifted to close dates. This shift is based on
62   * a simple Keplerian model for orbit, a linear extrapolation for attitude
63   * taking the spin rate into account and no mass change. It is <em>not</em>
64   * intended as a replacement for proper orbit and attitude propagation but
65   * should be sufficient for either small time shifts or coarse accuracy.
66   * </p>
67   * <p>
68   * The instance <code>SpacecraftState</code> is guaranteed to be immutable.
69   * </p>
70   * @see org.orekit.propagation.numerical.NumericalPropagator
71   * @author Fabien Maussion
72   * @author V&eacute;ronique Pommier-Maurussane
73   * @author Luc Maisonobe
74   */
75  public class SpacecraftState
76      implements TimeStamped, TimeShiftable<SpacecraftState>, TimeInterpolable<SpacecraftState>, Serializable {
77  
78      /** Serializable UID. */
79      private static final long serialVersionUID = 20130407L;
80  
81      /** Default mass. */
82      private static final double DEFAULT_MASS = 1000.0;
83  
84      /**
85       * tolerance on date comparison in {@link #checkConsistency(Orbit, Attitude)}. 100 ns
86       * corresponds to sub-mm accuracy at LEO orbital velocities.
87       */
88      private static final double DATE_INCONSISTENCY_THRESHOLD = 100e-9;
89  
90      /** Orbital state. */
91      private final Orbit orbit;
92  
93      /** Attitude. */
94      private final Attitude attitude;
95  
96      /** Current mass (kg). */
97      private final double mass;
98  
99      /** Additional states. */
100     private final Map<String, double[]> additional;
101 
102     /** Build a spacecraft state from orbit only.
103      * <p>Attitude and mass are set to unspecified non-null arbitrary values.</p>
104      * @param orbit the orbit
105      */
106     public SpacecraftState(final Orbit orbit) {
107         this(orbit,
108              new LofOffset(orbit.getFrame(), LOFType.VVLH).getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
109              DEFAULT_MASS, null);
110     }
111 
112     /** Build a spacecraft state from orbit and attitude provider.
113      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
114      * @param orbit the orbit
115      * @param attitude attitude
116      * @exception IllegalArgumentException if orbit and attitude dates
117      * or frames are not equal
118      */
119     public SpacecraftState(final Orbit orbit, final Attitude attitude)
120         throws IllegalArgumentException {
121         this(orbit, attitude, DEFAULT_MASS, null);
122     }
123 
124     /** Create a new instance from orbit and mass.
125      * <p>Attitude law is set to an unspecified default attitude.</p>
126      * @param orbit the orbit
127      * @param mass the mass (kg)
128      */
129     public SpacecraftState(final Orbit orbit, final double mass) {
130         this(orbit,
131              new LofOffset(orbit.getFrame(), LOFType.VVLH).getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
132              mass, null);
133     }
134 
135     /** Build a spacecraft state from orbit, attitude provider and mass.
136      * @param orbit the orbit
137      * @param attitude attitude
138      * @param mass the mass (kg)
139      * @exception IllegalArgumentException if orbit and attitude dates
140      * or frames are not equal
141      */
142     public SpacecraftState(final Orbit orbit, final Attitude attitude, final double mass)
143         throws IllegalArgumentException {
144         this(orbit, attitude, mass, null);
145     }
146 
147     /** Build a spacecraft state from orbit only.
148      * <p>Attitude and mass are set to unspecified non-null arbitrary values.</p>
149      * @param orbit the orbit
150      * @param additional additional states
151      */
152     public SpacecraftState(final Orbit orbit, final Map<String, double[]> additional) {
153         this(orbit,
154              new LofOffset(orbit.getFrame(), LOFType.VVLH).getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
155              DEFAULT_MASS, additional);
156     }
157 
158     /** Build a spacecraft state from orbit and attitude provider.
159      * <p>Mass is set to an unspecified non-null arbitrary value.</p>
160      * @param orbit the orbit
161      * @param attitude attitude
162      * @param additional additional states
163      * @exception IllegalArgumentException if orbit and attitude dates
164      * or frames are not equal
165      */
166     public SpacecraftState(final Orbit orbit, final Attitude attitude, final Map<String, double[]> additional)
167         throws IllegalArgumentException {
168         this(orbit, attitude, DEFAULT_MASS, additional);
169     }
170 
171     /** Create a new instance from orbit and mass.
172      * <p>Attitude law is set to an unspecified default attitude.</p>
173      * @param orbit the orbit
174      * @param mass the mass (kg)
175      * @param additional additional states
176      */
177     public SpacecraftState(final Orbit orbit, final double mass, final Map<String, double[]> additional) {
178         this(orbit,
179              new LofOffset(orbit.getFrame(), LOFType.VVLH).getAttitude(orbit, orbit.getDate(), orbit.getFrame()),
180              mass, additional);
181     }
182 
183     /** Build a spacecraft state from orbit, attitude provider and mass.
184      * @param orbit the orbit
185      * @param attitude attitude
186      * @param mass the mass (kg)
187      * @param additional additional states (may be null if no additional states are available)
188      * @exception IllegalArgumentException if orbit and attitude dates
189      * or frames are not equal
190      */
191     public SpacecraftState(final Orbit orbit, final Attitude attitude,
192                            final double mass, final Map<String, double[]> additional)
193         throws IllegalArgumentException {
194         checkConsistency(orbit, attitude);
195         this.orbit      = orbit;
196         this.attitude   = attitude;
197         this.mass       = mass;
198         if (additional == null) {
199             this.additional = Collections.emptyMap();
200         } else {
201             this.additional = new HashMap<String, double[]>(additional.size());
202             for (final Map.Entry<String, double[]> entry : additional.entrySet()) {
203                 this.additional.put(entry.getKey(), entry.getValue().clone());
204             }
205         }
206     }
207 
208     /** Add an additional state.
209      * <p>
210      * {@link SpacecraftState SpacecraftState} instances are immutable,
211      * so this method does <em>not</em> change the instance, but rather
212      * creates a new instance, which has the same orbit, attitude, mass
213      * and additional states as the original instance, except it also
214      * has the specified state. If the original instance already had an
215      * additional state with the same name, it will be overridden. If it
216      * did not have any additional state with that name, the new instance
217      * will have one more additional state than the original instance.
218      * </p>
219      * @param name name of the additional state
220      * @param value value of the additional state
221      * @return a new instance, with the additional state added
222      * @see #hasAdditionalState(String)
223      * @see #getAdditionalState(String)
224      * @see #getAdditionalStates()
225      */
226     public SpacecraftState addAdditionalState(final String name, final double... value) {
227         final Map<String, double[]> newMap = new HashMap<String, double[]>(additional.size() + 1);
228         newMap.putAll(additional);
229         newMap.put(name, value.clone());
230         return new SpacecraftState(orbit, attitude, mass, newMap);
231     }
232 
233     /** Check orbit and attitude dates are equal.
234      * @param orbit the orbit
235      * @param attitude attitude
236      * @exception IllegalArgumentException if orbit and attitude dates
237      * are not equal
238      */
239     private static void checkConsistency(final Orbit orbit, final Attitude attitude)
240         throws IllegalArgumentException {
241         if (FastMath.abs(orbit.getDate().durationFrom(attitude.getDate())) >
242             DATE_INCONSISTENCY_THRESHOLD) {
243             throw new OrekitIllegalArgumentException(OrekitMessages.ORBIT_AND_ATTITUDE_DATES_MISMATCH,
244                                                      orbit.getDate(), attitude.getDate());
245         }
246         if (orbit.getFrame() != attitude.getReferenceFrame()) {
247             throw new OrekitIllegalArgumentException(OrekitMessages.FRAMES_MISMATCH,
248                                                      orbit.getFrame().getName(),
249                                                      attitude.getReferenceFrame().getName());
250         }
251     }
252 
253     /** Get a time-shifted state.
254      * <p>
255      * The state can be slightly shifted to close dates. This shift is based on
256      * simple models. For orbits, the model is a Keplerian one if no derivatives
257      * are available in the orbit, or Keplerian plus quadratic effect of the
258      * non-Keplerian acceleration if derivatives are available. For attitude,
259      * a polynomial model is used. Neither mass nor additional states change.
260      * Shifting is <em>not</em> intended as a replacement for proper orbit
261      * and attitude propagation but should be sufficient for small time shifts
262      * or coarse accuracy.
263      * </p>
264      * <p>
265      * As a rough order of magnitude, the following table shows the extrapolation
266      * errors obtained between this simple shift method and an {@link
267      * org.orekit.propagation.numerical.NumericalPropagator numerical
268      * propagator} for a low Earth Sun Synchronous Orbit, with a 20x20 gravity field,
269      * Sun and Moon third bodies attractions, drag and solar radiation pressure.
270      * Beware that these results will be different for other orbits.
271      * </p>
272      * <table border="1" cellpadding="5">
273      * <caption>Extrapolation Error</caption>
274      * <tr bgcolor="#ccccff"><th>interpolation time (s)</th>
275      * <th>position error without derivatives (m)</th><th>position error with derivatives (m)</th></tr>
276      * <tr><td bgcolor="#eeeeff"> 60</td><td>  18</td><td> 1.1</td></tr>
277      * <tr><td bgcolor="#eeeeff">120</td><td>  72</td><td> 9.1</td></tr>
278      * <tr><td bgcolor="#eeeeff">300</td><td> 447</td><td> 140</td></tr>
279      * <tr><td bgcolor="#eeeeff">600</td><td>1601</td><td>1067</td></tr>
280      * <tr><td bgcolor="#eeeeff">900</td><td>3141</td><td>3307</td></tr>
281      * </table>
282      * @param dt time shift in seconds
283      * @return a new state, shifted with respect to the instance (which is immutable)
284      * except for the mass and additional states which stay unchanged
285      */
286     public SpacecraftState shiftedBy(final double dt) {
287         return new SpacecraftState(orbit.shiftedBy(dt), attitude.shiftedBy(dt),
288                                    mass, additional);
289     }
290 
291     /** {@inheritDoc}
292      * <p>
293      * The additional states that are interpolated are the ones already present
294      * in the instance. The sample instances must therefore have at least the same
295      * additional states has the instance. They may have more additional states,
296      * but the extra ones will be ignored.
297      * </p>
298      * <p>
299      * As this implementation of interpolation is polynomial, it should be used only
300      * with small samples (about 10-20 points) in order to avoid <a
301      * href="http://en.wikipedia.org/wiki/Runge%27s_phenomenon">Runge's phenomenon</a>
302      * and numerical problems (including NaN appearing).
303      * </p>
304      */
305     public SpacecraftState interpolate(final AbsoluteDate date,
306                                        final Stream<SpacecraftState> sample) {
307 
308         // prepare interpolators
309         final List<Orbit> orbits = new ArrayList<>();
310         final List<Attitude> attitudes = new ArrayList<>();
311         final HermiteInterpolator massInterpolator = new HermiteInterpolator();
312         final Map<String, HermiteInterpolator> additionalInterpolators =
313                 new HashMap<String, HermiteInterpolator>(additional.size());
314         for (final String name : additional.keySet()) {
315             additionalInterpolators.put(name, new HermiteInterpolator());
316         }
317 
318         // extract sample data
319         sample.forEach(state -> {
320             final double deltaT = state.getDate().durationFrom(date);
321             orbits.add(state.getOrbit());
322             attitudes.add(state.getAttitude());
323             massInterpolator.addSamplePoint(deltaT,
324                                             new double[] {
325                                                  state.getMass()
326                                             });
327             for (final Map.Entry<String, HermiteInterpolator> entry : additionalInterpolators.entrySet()) {
328                 entry.getValue().addSamplePoint(deltaT, state.getAdditionalState(entry.getKey()));
329             }
330         });
331 
332         // perform interpolations
333         final Orbit interpolatedOrbit       = orbit.interpolate(date, orbits);
334         final Attitude interpolatedAttitude = attitude.interpolate(date, attitudes);
335         final double interpolatedMass       = massInterpolator.value(0)[0];
336         final Map<String, double[]> interpolatedAdditional;
337         if (additional.isEmpty()) {
338             interpolatedAdditional = null;
339         } else {
340             interpolatedAdditional = new HashMap<String, double[]>(additional.size());
341             for (final Map.Entry<String, HermiteInterpolator> entry : additionalInterpolators.entrySet()) {
342                 interpolatedAdditional.put(entry.getKey(), entry.getValue().value(0));
343             }
344         }
345 
346         // create the complete interpolated state
347         return new SpacecraftState(interpolatedOrbit, interpolatedAttitude,
348                                    interpolatedMass, interpolatedAdditional);
349 
350     }
351 
352     /** Gets the current orbit.
353      * @return the orbit
354      */
355     public Orbit getOrbit() {
356         return orbit;
357     }
358 
359     /** Get the date.
360      * @return date
361      */
362     public AbsoluteDate getDate() {
363         return orbit.getDate();
364     }
365 
366     /** Get the inertial frame.
367      * @return the frame
368      */
369     public Frame getFrame() {
370         return orbit.getFrame();
371     }
372 
373     /** Check if an additional state is available.
374      * @param name name of the additional state
375      * @return true if the additional state is available
376      * @see #addAdditionalState(String, double[])
377      * @see #getAdditionalState(String)
378      * @see #getAdditionalStates()
379      */
380     public boolean hasAdditionalState(final String name) {
381         return additional.containsKey(name);
382     }
383 
384     /** Check if two instances have the same set of additional states available.
385      * <p>
386      * Only the names and dimensions of the additional states are compared,
387      * not their values.
388      * </p>
389      * @param state state to compare to instance
390      * @exception MathIllegalStateException if an additional state does not have
391      * the same dimension in both states
392      */
393     public void ensureCompatibleAdditionalStates(final SpacecraftState state)
394         throws MathIllegalStateException {
395 
396         // check instance additional states is a subset of the other one
397         for (final Map.Entry<String, double[]> entry : additional.entrySet()) {
398             final double[] other = state.additional.get(entry.getKey());
399             if (other == null) {
400                 throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE,
401                                           entry.getKey());
402             }
403             if (other.length != entry.getValue().length) {
404                 throw new MathIllegalStateException(LocalizedCoreFormats.DIMENSIONS_MISMATCH,
405                                                     other.length, entry.getValue().length);
406             }
407         }
408 
409         if (state.additional.size() > additional.size()) {
410             // the other state has more additional states
411             for (final String name : state.additional.keySet()) {
412                 if (!additional.containsKey(name)) {
413                     throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE,
414                                               name);
415                 }
416             }
417         }
418 
419     }
420 
421     /** Get an additional state.
422      * @param name name of the additional state
423      * @return value of the additional state
424           * @see #addAdditionalState(String, double[])
425      * @see #hasAdditionalState(String)
426      * @see #getAdditionalStates()
427      */
428     public double[] getAdditionalState(final String name) {
429         if (!additional.containsKey(name)) {
430             throw new OrekitException(OrekitMessages.UNKNOWN_ADDITIONAL_STATE, name);
431         }
432         return additional.get(name).clone();
433     }
434 
435     /** Get an unmodifiable map of additional states.
436      * @return unmodifiable map of additional states
437      * @see #addAdditionalState(String, double[])
438      * @see #hasAdditionalState(String)
439      * @see #getAdditionalState(String)
440      */
441     public Map<String, double[]> getAdditionalStates() {
442         return Collections.unmodifiableMap(additional);
443     }
444 
445     /** Compute the transform from orbite/attitude reference frame to spacecraft frame.
446      * <p>The spacecraft frame origin is at the point defined by the orbit,
447      * and its orientation is defined by the attitude.</p>
448      * @return transform from specified frame to current spacecraft frame
449      */
450     public Transform toTransform() {
451         final AbsoluteDate date = orbit.getDate();
452         return new Transform(date,
453                              new Transform(date, orbit.getPVCoordinates().negate()),
454                              new Transform(date, attitude.getOrientation()));
455     }
456 
457     /** Get the central attraction coefficient.
458      * @return mu central attraction coefficient (m^3/s^2)
459      */
460     public double getMu() {
461         return orbit.getMu();
462     }
463 
464     /** Get the Keplerian period.
465      * <p>The Keplerian period is computed directly from semi major axis
466      * and central acceleration constant.</p>
467      * @return Keplerian period in seconds
468      */
469     public double getKeplerianPeriod() {
470         return orbit.getKeplerianPeriod();
471     }
472 
473     /** Get the Keplerian mean motion.
474      * <p>The Keplerian mean motion is computed directly from semi major axis
475      * and central acceleration constant.</p>
476      * @return Keplerian mean motion in radians per second
477      */
478     public double getKeplerianMeanMotion() {
479         return orbit.getKeplerianMeanMotion();
480     }
481 
482     /** Get the semi-major axis.
483      * @return semi-major axis (m)
484      */
485     public double getA() {
486         return orbit.getA();
487     }
488 
489     /** Get the first component of the eccentricity vector (as per equinoctial parameters).
490      * @return e cos(ω + Ω), first component of eccentricity vector
491      * @see #getE()
492      */
493     public double getEquinoctialEx() {
494         return orbit.getEquinoctialEx();
495     }
496 
497     /** Get the second component of the eccentricity vector (as per equinoctial parameters).
498      * @return e sin(ω + Ω), second component of the eccentricity vector
499      * @see #getE()
500      */
501     public double getEquinoctialEy() {
502         return orbit.getEquinoctialEy();
503     }
504 
505     /** Get the first component of the inclination vector (as per equinoctial parameters).
506      * @return tan(i/2) cos(Ω), first component of the inclination vector
507      * @see #getI()
508      */
509     public double getHx() {
510         return orbit.getHx();
511     }
512 
513     /** Get the second component of the inclination vector (as per equinoctial parameters).
514      * @return tan(i/2) sin(Ω), second component of the inclination vector
515      * @see #getI()
516      */
517     public double getHy() {
518         return orbit.getHy();
519     }
520 
521     /** Get the true longitude argument (as per equinoctial parameters).
522      * @return v + ω + Ω true longitude argument (rad)
523      * @see #getLE()
524      * @see #getLM()
525      */
526     public double getLv() {
527         return orbit.getLv();
528     }
529 
530     /** Get the eccentric longitude argument (as per equinoctial parameters).
531      * @return E + ω + Ω eccentric longitude argument (rad)
532      * @see #getLv()
533      * @see #getLM()
534      */
535     public double getLE() {
536         return orbit.getLE();
537     }
538 
539     /** Get the mean longitude argument (as per equinoctial parameters).
540      * @return M + ω + Ω mean longitude argument (rad)
541      * @see #getLv()
542      * @see #getLE()
543      */
544     public double getLM() {
545         return orbit.getLM();
546     }
547 
548     // Additional orbital elements
549 
550     /** Get the eccentricity.
551      * @return eccentricity
552      * @see #getEquinoctialEx()
553      * @see #getEquinoctialEy()
554      */
555     public double getE() {
556         return orbit.getE();
557     }
558 
559     /** Get the inclination.
560      * @return inclination (rad)
561      * @see #getHx()
562      * @see #getHy()
563      */
564     public double getI() {
565         return orbit.getI();
566     }
567 
568     /** Get the {@link TimeStampedPVCoordinates} in orbit definition frame.
569      * Compute the position and velocity of the satellite. This method caches its
570      * results, and recompute them only when the method is called with a new value
571      * for mu. The result is provided as a reference to the internally cached
572      * {@link TimeStampedPVCoordinates}, so the caller is responsible to copy it in a separate
573      * {@link TimeStampedPVCoordinates} if it needs to keep the value for a while.
574      * @return pvCoordinates in orbit definition frame
575      */
576     public TimeStampedPVCoordinates getPVCoordinates() {
577         return orbit.getPVCoordinates();
578     }
579 
580     /** Get the {@link TimeStampedPVCoordinates} in given output frame.
581      * Compute the position and velocity of the satellite. This method caches its
582      * results, and recompute them only when the method is called with a new value
583      * for mu. The result is provided as a reference to the internally cached
584      * {@link TimeStampedPVCoordinates}, so the caller is responsible to copy it in a separate
585      * {@link TimeStampedPVCoordinates} if it needs to keep the value for a while.
586      * @param outputFrame frame in which coordinates should be defined
587      * @return pvCoordinates in orbit definition frame
588      */
589     public TimeStampedPVCoordinates getPVCoordinates(final Frame outputFrame) {
590         return orbit.getPVCoordinates(outputFrame);
591     }
592 
593     /** Get the attitude.
594      * @return the attitude.
595      */
596     public Attitude getAttitude() {
597         return attitude;
598     }
599 
600     /** Gets the current mass.
601      * @return the mass (kg)
602      */
603     public double getMass() {
604         return mass;
605     }
606 
607     /** Replace the instance with a data transfer object for serialization.
608      * @return data transfer object that will be serialized
609      */
610     private Object writeReplace() {
611         return new DTO(this);
612     }
613 
614     /** Internal class used only for serialization. */
615     private static class DTO implements Serializable {
616 
617         /** Serializable UID. */
618         private static final long serialVersionUID = 20140617L;
619 
620         /** Orbit. */
621         private final Orbit orbit;
622 
623         /** Attitude and mass double values. */
624         private double[] d;
625 
626         /** Additional states. */
627         private final Map<String, double[]> additional;
628 
629         /** Simple constructor.
630          * @param state instance to serialize
631          */
632         private DTO(final SpacecraftState state) {
633 
634             this.orbit      = state.orbit;
635             this.additional = state.additional.isEmpty() ? null : state.additional;
636 
637             final Rotation rotation             = state.attitude.getRotation();
638             final Vector3D spin                 = state.attitude.getSpin();
639             final Vector3D rotationAcceleration = state.attitude.getRotationAcceleration();
640             this.d = new double[] {
641                 rotation.getQ0(), rotation.getQ1(), rotation.getQ2(), rotation.getQ3(),
642                 spin.getX(), spin.getY(), spin.getZ(),
643                 rotationAcceleration.getX(), rotationAcceleration.getY(), rotationAcceleration.getZ(),
644                 state.mass
645             };
646 
647         }
648 
649         /** Replace the deserialized data transfer object with a {@link SpacecraftState}.
650          * @return replacement {@link SpacecraftState}
651          */
652         private Object readResolve() {
653             return new SpacecraftState(orbit,
654                                        new Attitude(orbit.getFrame(),
655                                                     new TimeStampedAngularCoordinates(orbit.getDate(),
656                                                                                       new Rotation(d[0], d[1], d[2], d[3], false),
657                                                                                       new Vector3D(d[4], d[5], d[6]),
658                                                                                       new Vector3D(d[7], d[8], d[9]))),
659                                        d[10], additional);
660         }
661 
662     }
663 
664 }