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