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.utils;
18  
19  import java.io.Serializable;
20  
21  import org.hipparchus.CalculusFieldElement;
22  import org.hipparchus.analysis.differentiation.DSFactory;
23  import org.hipparchus.analysis.differentiation.Derivative;
24  import org.hipparchus.analysis.differentiation.DerivativeStructure;
25  import org.hipparchus.analysis.differentiation.UnivariateDerivative1;
26  import org.hipparchus.analysis.differentiation.UnivariateDerivative2;
27  import org.hipparchus.exception.LocalizedCoreFormats;
28  import org.hipparchus.exception.MathIllegalArgumentException;
29  import org.hipparchus.exception.MathRuntimeException;
30  import org.hipparchus.geometry.euclidean.threed.FieldRotation;
31  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
32  import org.hipparchus.geometry.euclidean.threed.Rotation;
33  import org.hipparchus.geometry.euclidean.threed.RotationConvention;
34  import org.hipparchus.geometry.euclidean.threed.Vector3D;
35  import org.hipparchus.linear.DecompositionSolver;
36  import org.hipparchus.linear.MatrixUtils;
37  import org.hipparchus.linear.QRDecomposition;
38  import org.hipparchus.linear.RealMatrix;
39  import org.hipparchus.linear.RealVector;
40  import org.hipparchus.util.FastMath;
41  import org.hipparchus.util.MathArrays;
42  import org.orekit.errors.OrekitException;
43  import org.orekit.errors.OrekitMessages;
44  import org.orekit.time.TimeShiftable;
45  
46  /** Simple container for rotation/rotation rate/rotation acceleration triplets.
47   * <p>
48   * The state can be slightly shifted to close dates. This shift is based on
49   * an approximate solution of the fixed acceleration motion. It is <em>not</em>
50   * intended as a replacement for proper attitude propagation but should be
51   * sufficient for either small time shifts or coarse accuracy.
52   * </p>
53   * <p>
54   * This class is the angular counterpart to {@link PVCoordinates}.
55   * </p>
56   * <p>Instances of this class are guaranteed to be immutable.</p>
57   * @author Luc Maisonobe
58   */
59  public class AngularCoordinates implements TimeShiftable<AngularCoordinates>, Serializable {
60  
61      /** Fixed orientation parallel with reference frame
62       * (identity rotation, zero rotation rate and acceleration).
63       */
64      public static final AngularCoordinates IDENTITY =
65              new AngularCoordinates(Rotation.IDENTITY, Vector3D.ZERO, Vector3D.ZERO);
66  
67      /** Serializable UID. */
68      private static final long serialVersionUID = 20140414L;
69  
70      /** Rotation. */
71      private final Rotation rotation;
72  
73      /** Rotation rate. */
74      private final Vector3D rotationRate;
75  
76      /** Rotation acceleration. */
77      private final Vector3D rotationAcceleration;
78  
79      /** Simple constructor.
80       * <p> Sets the Coordinates to default : Identity, Ω = (0 0 0), dΩ/dt = (0 0 0).</p>
81       */
82      public AngularCoordinates() {
83          this(Rotation.IDENTITY, Vector3D.ZERO, Vector3D.ZERO);
84      }
85  
86      /** Builds a rotation/rotation rate pair.
87       * @param rotation rotation
88       * @param rotationRate rotation rate Ω (rad/s)
89       */
90      public AngularCoordinates(final Rotation rotation, final Vector3D rotationRate) {
91          this(rotation, rotationRate, Vector3D.ZERO);
92      }
93  
94      /** Builds a rotation/rotation rate/rotation acceleration triplet.
95       * @param rotation rotation
96       * @param rotationRate rotation rate Ω (rad/s)
97       * @param rotationAcceleration rotation acceleration dΩ/dt (rad/s²)
98       */
99      public AngularCoordinates(final Rotation rotation,
100                               final Vector3D rotationRate, final Vector3D rotationAcceleration) {
101         this.rotation             = rotation;
102         this.rotationRate         = rotationRate;
103         this.rotationAcceleration = rotationAcceleration;
104     }
105 
106     /**
107      * Builds angular coordinates with the given rotation, zero angular
108      * velocity, and zero angular acceleration.
109      *
110      * @param rotation rotation
111      */
112     public AngularCoordinates(final Rotation rotation) {
113         this(rotation, Vector3D.ZERO);
114     }
115 
116     /** Build the rotation that transforms a pair of pv coordinates into another one.
117 
118      * <p><em>WARNING</em>! This method requires much more stringent assumptions on
119      * its parameters than the similar {@link Rotation#Rotation(Vector3D, Vector3D,
120      * Vector3D, Vector3D) constructor} from the {@link Rotation Rotation} class.
121      * As far as the Rotation constructor is concerned, the {@code v₂} vector from
122      * the second pair can be slightly misaligned. The Rotation constructor will
123      * compensate for this misalignment and create a rotation that ensure {@code
124      * v₁ = r(u₁)} and {@code v₂ ∈ plane (r(u₁), r(u₂))}. <em>THIS IS NOT
125      * TRUE ANYMORE IN THIS CLASS</em>! As derivatives are involved and must be
126      * preserved, this constructor works <em>only</em> if the two pairs are fully
127      * consistent, i.e. if a rotation exists that fulfill all the requirements: {@code
128      * v₁ = r(u₁)}, {@code v₂ = r(u₂)}, {@code dv₁/dt = dr(u₁)/dt}, {@code dv₂/dt
129      * = dr(u₂)/dt}, {@code d²v₁/dt² = d²r(u₁)/dt²}, {@code d²v₂/dt² = d²r(u₂)/dt²}.</p>
130      * @param u1 first vector of the origin pair
131      * @param u2 second vector of the origin pair
132      * @param v1 desired image of u1 by the rotation
133      * @param v2 desired image of u2 by the rotation
134      * @param tolerance relative tolerance factor used to check singularities
135      */
136     public AngularCoordinates(final PVCoordinates u1, final PVCoordinates u2,
137                               final PVCoordinates v1, final PVCoordinates v2,
138                               final double tolerance) {
139 
140         try {
141             // find the initial fixed rotation
142             rotation = new Rotation(u1.getPosition(), u2.getPosition(),
143                                     v1.getPosition(), v2.getPosition());
144 
145             // find rotation rate Ω such that
146             //  Ω ⨯ v₁ = r(dot(u₁)) - dot(v₁)
147             //  Ω ⨯ v₂ = r(dot(u₂)) - dot(v₂)
148             final Vector3D ru1Dot = rotation.applyTo(u1.getVelocity());
149             final Vector3D ru2Dot = rotation.applyTo(u2.getVelocity());
150             rotationRate = inverseCrossProducts(v1.getPosition(), ru1Dot.subtract(v1.getVelocity()),
151                                                 v2.getPosition(), ru2Dot.subtract(v2.getVelocity()),
152                                                 tolerance);
153 
154             // find rotation acceleration dot(Ω) such that
155             // dot(Ω) ⨯ v₁ = r(dotdot(u₁)) - 2 Ω ⨯ dot(v₁) - Ω ⨯  (Ω ⨯ v₁) - dotdot(v₁)
156             // dot(Ω) ⨯ v₂ = r(dotdot(u₂)) - 2 Ω ⨯ dot(v₂) - Ω ⨯  (Ω ⨯ v₂) - dotdot(v₂)
157             final Vector3D ru1DotDot = rotation.applyTo(u1.getAcceleration());
158             final Vector3D oDotv1    = Vector3D.crossProduct(rotationRate, v1.getVelocity());
159             final Vector3D oov1      = Vector3D.crossProduct(rotationRate, Vector3D.crossProduct(rotationRate, v1.getPosition()));
160             final Vector3D c1        = new Vector3D(1, ru1DotDot, -2, oDotv1, -1, oov1, -1, v1.getAcceleration());
161             final Vector3D ru2DotDot = rotation.applyTo(u2.getAcceleration());
162             final Vector3D oDotv2    = Vector3D.crossProduct(rotationRate, v2.getVelocity());
163             final Vector3D oov2      = Vector3D.crossProduct(rotationRate, Vector3D.crossProduct(rotationRate, v2.getPosition()));
164             final Vector3D c2        = new Vector3D(1, ru2DotDot, -2, oDotv2, -1, oov2, -1, v2.getAcceleration());
165             rotationAcceleration     = inverseCrossProducts(v1.getPosition(), c1, v2.getPosition(), c2, tolerance);
166 
167         } catch (MathRuntimeException mrte) {
168             throw new OrekitException(mrte);
169         }
170 
171     }
172 
173     /** Build one of the rotations that transform one pv coordinates into another one.
174 
175      * <p>Except for a possible scale factor, if the instance were
176      * applied to the vector u it will produce the vector v. There is an
177      * infinite number of such rotations, this constructor choose the
178      * one with the smallest associated angle (i.e. the one whose axis
179      * is orthogonal to the (u, v) plane). If u and v are collinear, an
180      * arbitrary rotation axis is chosen.</p>
181 
182      * @param u origin vector
183      * @param v desired image of u by the rotation
184      */
185     public AngularCoordinates(final PVCoordinates u, final PVCoordinates v) {
186         this(new FieldRotation<>(u.toDerivativeStructureVector(2),
187                                  v.toDerivativeStructureVector(2)));
188     }
189 
190     /** Builds a AngularCoordinates from  a {@link FieldRotation}&lt;{@link Derivative}&gt;.
191      * <p>
192      * The rotation components must have time as their only derivation parameter and
193      * have consistent derivation orders.
194      * </p>
195      * @param r rotation with time-derivatives embedded within the coordinates
196      * @param <U> type of the derivative
197      */
198     public <U extends Derivative<U>> AngularCoordinates(final FieldRotation<U> r) {
199 
200         final double q0       = r.getQ0().getReal();
201         final double q1       = r.getQ1().getReal();
202         final double q2       = r.getQ2().getReal();
203         final double q3       = r.getQ3().getReal();
204 
205         rotation     = new Rotation(q0, q1, q2, q3, false);
206         if (r.getQ0().getOrder() >= 1) {
207             final double q0Dot    = r.getQ0().getPartialDerivative(1);
208             final double q1Dot    = r.getQ1().getPartialDerivative(1);
209             final double q2Dot    = r.getQ2().getPartialDerivative(1);
210             final double q3Dot    = r.getQ3().getPartialDerivative(1);
211             rotationRate =
212                     new Vector3D(2 * MathArrays.linearCombination(-q1, q0Dot,  q0, q1Dot,  q3, q2Dot, -q2, q3Dot),
213                                  2 * MathArrays.linearCombination(-q2, q0Dot, -q3, q1Dot,  q0, q2Dot,  q1, q3Dot),
214                                  2 * MathArrays.linearCombination(-q3, q0Dot,  q2, q1Dot, -q1, q2Dot,  q0, q3Dot));
215             if (r.getQ0().getOrder() >= 2) {
216                 final double q0DotDot = r.getQ0().getPartialDerivative(2);
217                 final double q1DotDot = r.getQ1().getPartialDerivative(2);
218                 final double q2DotDot = r.getQ2().getPartialDerivative(2);
219                 final double q3DotDot = r.getQ3().getPartialDerivative(2);
220                 rotationAcceleration =
221                         new Vector3D(2 * MathArrays.linearCombination(-q1, q0DotDot,  q0, q1DotDot,  q3, q2DotDot, -q2, q3DotDot),
222                                      2 * MathArrays.linearCombination(-q2, q0DotDot, -q3, q1DotDot,  q0, q2DotDot,  q1, q3DotDot),
223                                      2 * MathArrays.linearCombination(-q3, q0DotDot,  q2, q1DotDot, -q1, q2DotDot,  q0, q3DotDot));
224             } else {
225                 rotationAcceleration = Vector3D.ZERO;
226             }
227         } else {
228             rotationRate         = Vector3D.ZERO;
229             rotationAcceleration = Vector3D.ZERO;
230         }
231 
232     }
233 
234     /** Find a vector from two known cross products.
235      * <p>
236      * We want to find Ω such that: Ω ⨯ v₁ = c₁ and Ω ⨯ v₂ = c₂
237      * </p>
238      * <p>
239      * The first equation (Ω ⨯ v₁ = c₁) will always be fulfilled exactly,
240      * and the second one will be fulfilled if possible.
241      * </p>
242      * @param v1 vector forming the first known cross product
243      * @param c1 know vector for cross product Ω ⨯ v₁
244      * @param v2 vector forming the second known cross product
245      * @param c2 know vector for cross product Ω ⨯ v₂
246      * @param tolerance relative tolerance factor used to check singularities
247      * @return vector Ω such that: Ω ⨯ v₁ = c₁ and Ω ⨯ v₂ = c₂
248      * @exception MathIllegalArgumentException if vectors are inconsistent and
249      * no solution can be found
250      */
251     private static Vector3D inverseCrossProducts(final Vector3D v1, final Vector3D c1,
252                                                  final Vector3D v2, final Vector3D c2,
253                                                  final double tolerance)
254         throws MathIllegalArgumentException {
255 
256         final double v12 = v1.getNormSq();
257         final double v1n = FastMath.sqrt(v12);
258         final double v22 = v2.getNormSq();
259         final double v2n = FastMath.sqrt(v22);
260         final double threshold = tolerance * FastMath.max(v1n, v2n);
261 
262         Vector3D omega;
263 
264         try {
265             // create the over-determined linear system representing the two cross products
266             final RealMatrix m = MatrixUtils.createRealMatrix(6, 3);
267             m.setEntry(0, 1,  v1.getZ());
268             m.setEntry(0, 2, -v1.getY());
269             m.setEntry(1, 0, -v1.getZ());
270             m.setEntry(1, 2,  v1.getX());
271             m.setEntry(2, 0,  v1.getY());
272             m.setEntry(2, 1, -v1.getX());
273             m.setEntry(3, 1,  v2.getZ());
274             m.setEntry(3, 2, -v2.getY());
275             m.setEntry(4, 0, -v2.getZ());
276             m.setEntry(4, 2,  v2.getX());
277             m.setEntry(5, 0,  v2.getY());
278             m.setEntry(5, 1, -v2.getX());
279 
280             final RealVector rhs = MatrixUtils.createRealVector(new double[] {
281                 c1.getX(), c1.getY(), c1.getZ(),
282                 c2.getX(), c2.getY(), c2.getZ()
283             });
284 
285             // find the best solution we can
286             final DecompositionSolver solver = new QRDecomposition(m, threshold).getSolver();
287             final RealVector v = solver.solve(rhs);
288             omega = new Vector3D(v.getEntry(0), v.getEntry(1), v.getEntry(2));
289 
290         } catch (MathIllegalArgumentException miae) {
291             if (miae.getSpecifier() == LocalizedCoreFormats.SINGULAR_MATRIX) {
292 
293                 // handle some special cases for which we can compute a solution
294                 final double c12 = c1.getNormSq();
295                 final double c1n = FastMath.sqrt(c12);
296                 final double c22 = c2.getNormSq();
297                 final double c2n = FastMath.sqrt(c22);
298 
299                 if (c1n <= threshold && c2n <= threshold) {
300                     // simple special case, velocities are cancelled
301                     return Vector3D.ZERO;
302                 } else if (v1n <= threshold && c1n >= threshold) {
303                     // this is inconsistent, if v₁ is zero, c₁ must be 0 too
304                     throw new MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_TOO_LARGE, c1n, 0, true);
305                 } else if (v2n <= threshold && c2n >= threshold) {
306                     // this is inconsistent, if v₂ is zero, c₂ must be 0 too
307                     throw new MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_TOO_LARGE, c2n, 0, true);
308                 } else if (Vector3D.crossProduct(v1, v2).getNorm() <= threshold && v12 > threshold) {
309                     // simple special case, v₂ is redundant with v₁, we just ignore it
310                     // use the simplest Ω: orthogonal to both v₁ and c₁
311                     omega = new Vector3D(1.0 / v12, Vector3D.crossProduct(v1, c1));
312                 } else {
313                     throw miae;
314                 }
315             } else {
316                 throw miae;
317             }
318 
319         }
320 
321         // check results
322         final double d1 = Vector3D.distance(Vector3D.crossProduct(omega, v1), c1);
323         if (d1 > threshold) {
324             throw new MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_TOO_LARGE, d1, 0, true);
325         }
326 
327         final double d2 = Vector3D.distance(Vector3D.crossProduct(omega, v2), c2);
328         if (d2 > threshold) {
329             throw new MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_TOO_LARGE, d2, 0, true);
330         }
331 
332         return omega;
333 
334     }
335 
336     /** Transform the instance to a {@link FieldRotation}&lt;{@link DerivativeStructure}&gt;.
337      * <p>
338      * The {@link DerivativeStructure} coordinates correspond to time-derivatives up
339      * to the user-specified order.
340      * </p>
341      * @param order derivation order for the vector components
342      * @return rotation with time-derivatives embedded within the coordinates
343      */
344     public FieldRotation<DerivativeStructure> toDerivativeStructureRotation(final int order) {
345 
346         // quaternion components
347         final double q0 = rotation.getQ0();
348         final double q1 = rotation.getQ1();
349         final double q2 = rotation.getQ2();
350         final double q3 = rotation.getQ3();
351 
352         // first time-derivatives of the quaternion
353         final double oX    = rotationRate.getX();
354         final double oY    = rotationRate.getY();
355         final double oZ    = rotationRate.getZ();
356         final double q0Dot = 0.5 * MathArrays.linearCombination(-q1, oX, -q2, oY, -q3, oZ);
357         final double q1Dot = 0.5 * MathArrays.linearCombination( q0, oX, -q3, oY,  q2, oZ);
358         final double q2Dot = 0.5 * MathArrays.linearCombination( q3, oX,  q0, oY, -q1, oZ);
359         final double q3Dot = 0.5 * MathArrays.linearCombination(-q2, oX,  q1, oY,  q0, oZ);
360 
361         // second time-derivatives of the quaternion
362         final double oXDot = rotationAcceleration.getX();
363         final double oYDot = rotationAcceleration.getY();
364         final double oZDot = rotationAcceleration.getZ();
365         final double q0DotDot = -0.5 * MathArrays.linearCombination(new double[] {
366             q1, q2,  q3, q1Dot, q2Dot,  q3Dot
367         }, new double[] {
368             oXDot, oYDot, oZDot, oX, oY, oZ
369         });
370         final double q1DotDot =  0.5 * MathArrays.linearCombination(new double[] {
371             q0, q2, -q3, q0Dot, q2Dot, -q3Dot
372         }, new double[] {
373             oXDot, oZDot, oYDot, oX, oZ, oY
374         });
375         final double q2DotDot =  0.5 * MathArrays.linearCombination(new double[] {
376             q0, q3, -q1, q0Dot, q3Dot, -q1Dot
377         }, new double[] {
378             oYDot, oXDot, oZDot, oY, oX, oZ
379         });
380         final double q3DotDot =  0.5 * MathArrays.linearCombination(new double[] {
381             q0, q1, -q2, q0Dot, q1Dot, -q2Dot
382         }, new double[] {
383             oZDot, oYDot, oXDot, oZ, oY, oX
384         });
385 
386         final DSFactory factory;
387         final DerivativeStructure q0DS;
388         final DerivativeStructure q1DS;
389         final DerivativeStructure q2DS;
390         final DerivativeStructure q3DS;
391         switch (order) {
392             case 0 :
393                 factory = new DSFactory(1, order);
394                 q0DS = factory.build(q0);
395                 q1DS = factory.build(q1);
396                 q2DS = factory.build(q2);
397                 q3DS = factory.build(q3);
398                 break;
399             case 1 :
400                 factory = new DSFactory(1, order);
401                 q0DS = factory.build(q0, q0Dot);
402                 q1DS = factory.build(q1, q1Dot);
403                 q2DS = factory.build(q2, q2Dot);
404                 q3DS = factory.build(q3, q3Dot);
405                 break;
406             case 2 :
407                 factory = new DSFactory(1, order);
408                 q0DS = factory.build(q0, q0Dot, q0DotDot);
409                 q1DS = factory.build(q1, q1Dot, q1DotDot);
410                 q2DS = factory.build(q2, q2Dot, q2DotDot);
411                 q3DS = factory.build(q3, q3Dot, q3DotDot);
412                 break;
413             default :
414                 throw new OrekitException(OrekitMessages.OUT_OF_RANGE_DERIVATION_ORDER, order);
415         }
416 
417         return new FieldRotation<>(q0DS, q1DS, q2DS, q3DS, false);
418 
419     }
420 
421     /** Transform the instance to a {@link FieldRotation}&lt;{@link UnivariateDerivative1}&gt;.
422      * <p>
423      * The {@link UnivariateDerivative1} coordinates correspond to time-derivatives up
424      * to the order 1.
425      * </p>
426      * @return rotation with time-derivatives embedded within the coordinates
427      */
428     public FieldRotation<UnivariateDerivative1> toUnivariateDerivative1Rotation() {
429 
430         // quaternion components
431         final double q0 = rotation.getQ0();
432         final double q1 = rotation.getQ1();
433         final double q2 = rotation.getQ2();
434         final double q3 = rotation.getQ3();
435 
436         // first time-derivatives of the quaternion
437         final double oX    = rotationRate.getX();
438         final double oY    = rotationRate.getY();
439         final double oZ    = rotationRate.getZ();
440         final double q0Dot = 0.5 * MathArrays.linearCombination(-q1, oX, -q2, oY, -q3, oZ);
441         final double q1Dot = 0.5 * MathArrays.linearCombination( q0, oX, -q3, oY,  q2, oZ);
442         final double q2Dot = 0.5 * MathArrays.linearCombination( q3, oX,  q0, oY, -q1, oZ);
443         final double q3Dot = 0.5 * MathArrays.linearCombination(-q2, oX,  q1, oY,  q0, oZ);
444 
445         final UnivariateDerivative1 q0UD = new UnivariateDerivative1(q0, q0Dot);
446         final UnivariateDerivative1 q1UD = new UnivariateDerivative1(q1, q1Dot);
447         final UnivariateDerivative1 q2UD = new UnivariateDerivative1(q2, q2Dot);
448         final UnivariateDerivative1 q3UD = new UnivariateDerivative1(q3, q3Dot);
449 
450         return new FieldRotation<>(q0UD, q1UD, q2UD, q3UD, false);
451 
452     }
453 
454     /** Transform the instance to a {@link FieldRotation}&lt;{@link UnivariateDerivative2}&gt;.
455      * <p>
456      * The {@link UnivariateDerivative2} coordinates correspond to time-derivatives up
457      * to the order 2.
458      * </p>
459      * @return rotation with time-derivatives embedded within the coordinates
460      */
461     public FieldRotation<UnivariateDerivative2> toUnivariateDerivative2Rotation() {
462 
463         // quaternion components
464         final double q0 = rotation.getQ0();
465         final double q1 = rotation.getQ1();
466         final double q2 = rotation.getQ2();
467         final double q3 = rotation.getQ3();
468 
469         // first time-derivatives of the quaternion
470         final double oX    = rotationRate.getX();
471         final double oY    = rotationRate.getY();
472         final double oZ    = rotationRate.getZ();
473         final double q0Dot = 0.5 * MathArrays.linearCombination(-q1, oX, -q2, oY, -q3, oZ);
474         final double q1Dot = 0.5 * MathArrays.linearCombination( q0, oX, -q3, oY,  q2, oZ);
475         final double q2Dot = 0.5 * MathArrays.linearCombination( q3, oX,  q0, oY, -q1, oZ);
476         final double q3Dot = 0.5 * MathArrays.linearCombination(-q2, oX,  q1, oY,  q0, oZ);
477 
478         // second time-derivatives of the quaternion
479         final double oXDot = rotationAcceleration.getX();
480         final double oYDot = rotationAcceleration.getY();
481         final double oZDot = rotationAcceleration.getZ();
482         final double q0DotDot = -0.5 * MathArrays.linearCombination(new double[] {
483             q1, q2,  q3, q1Dot, q2Dot,  q3Dot
484         }, new double[] {
485             oXDot, oYDot, oZDot, oX, oY, oZ
486         });
487         final double q1DotDot =  0.5 * MathArrays.linearCombination(new double[] {
488             q0, q2, -q3, q0Dot, q2Dot, -q3Dot
489         }, new double[] {
490             oXDot, oZDot, oYDot, oX, oZ, oY
491         });
492         final double q2DotDot =  0.5 * MathArrays.linearCombination(new double[] {
493             q0, q3, -q1, q0Dot, q3Dot, -q1Dot
494         }, new double[] {
495             oYDot, oXDot, oZDot, oY, oX, oZ
496         });
497         final double q3DotDot =  0.5 * MathArrays.linearCombination(new double[] {
498             q0, q1, -q2, q0Dot, q1Dot, -q2Dot
499         }, new double[] {
500             oZDot, oYDot, oXDot, oZ, oY, oX
501         });
502 
503         final UnivariateDerivative2 q0UD = new UnivariateDerivative2(q0, q0Dot, q0DotDot);
504         final UnivariateDerivative2 q1UD = new UnivariateDerivative2(q1, q1Dot, q1DotDot);
505         final UnivariateDerivative2 q2UD = new UnivariateDerivative2(q2, q2Dot, q2DotDot);
506         final UnivariateDerivative2 q3UD = new UnivariateDerivative2(q3, q3Dot, q3DotDot);
507 
508         return new FieldRotation<>(q0UD, q1UD, q2UD, q3UD, false);
509 
510     }
511 
512     /** Estimate rotation rate between two orientations.
513      * <p>Estimation is based on a simple fixed rate rotation
514      * during the time interval between the two orientations.</p>
515      * @param start start orientation
516      * @param end end orientation
517      * @param dt time elapsed between the dates of the two orientations
518      * @return rotation rate allowing to go from start to end orientations
519      */
520     public static Vector3D estimateRate(final Rotation start, final Rotation end, final double dt) {
521         final Rotation evolution = start.compose(end.revert(), RotationConvention.VECTOR_OPERATOR);
522         return new Vector3D(evolution.getAngle() / dt, evolution.getAxis(RotationConvention.VECTOR_OPERATOR));
523     }
524 
525     /** Revert a rotation/rotation rate/ rotation acceleration triplet.
526      * Build a triplet which reverse the effect of another triplet.
527      * @return a new triplet whose effect is the reverse of the effect
528      * of the instance
529      */
530     public AngularCoordinates revert() {
531         return new AngularCoordinates(rotation.revert(),
532                                       rotation.applyInverseTo(rotationRate).negate(),
533                                       rotation.applyInverseTo(rotationAcceleration).negate());
534     }
535 
536 
537     /** Get a time-shifted rotation. Same as {@link #shiftedBy(double)} except
538      * only the shifted rotation is computed.
539      * <p>
540      * The state can be slightly shifted to close dates. This shift is based on
541      * an approximate solution of the fixed acceleration motion. It is <em>not</em>
542      * intended as a replacement for proper attitude propagation but should be
543      * sufficient for either small time shifts or coarse accuracy.
544      * </p>
545      * @param dt time shift in seconds
546      * @return a new state, shifted with respect to the instance (which is immutable)
547      * @see  #shiftedBy(double)
548      */
549     public Rotation rotationShiftedBy(final double dt) {
550 
551         // the shiftedBy method is based on a local approximation.
552         // It considers separately the contribution of the constant
553         // rotation, the linear contribution or the rate and the
554         // quadratic contribution of the acceleration. The rate
555         // and acceleration contributions are small rotations as long
556         // as the time shift is small, which is the crux of the algorithm.
557         // Small rotations are almost commutative, so we append these small
558         // contributions one after the other, as if they really occurred
559         // successively, despite this is not what really happens.
560 
561         // compute the linear contribution first, ignoring acceleration
562         // BEWARE: there is really a minus sign here, because if
563         // the target frame rotates in one direction, the vectors in the origin
564         // frame seem to rotate in the opposite direction
565         final double rate = rotationRate.getNorm();
566         final Rotation rateContribution = (rate == 0.0) ?
567                 Rotation.IDENTITY :
568                 new Rotation(rotationRate, rate * dt, RotationConvention.FRAME_TRANSFORM);
569 
570         // append rotation and rate contribution
571         final Rotation linearPart =
572                 rateContribution.compose(rotation, RotationConvention.VECTOR_OPERATOR);
573 
574         final double acc  = rotationAcceleration.getNorm();
575         if (acc == 0.0) {
576             // no acceleration, the linear part is sufficient
577             return linearPart;
578         }
579 
580         // compute the quadratic contribution, ignoring initial rotation and rotation rate
581         // BEWARE: there is really a minus sign here, because if
582         // the target frame rotates in one direction, the vectors in the origin
583         // frame seem to rotate in the opposite direction
584         final Rotation quadraticContribution =
585                 new Rotation(rotationAcceleration,
586                         0.5 * acc * dt * dt,
587                         RotationConvention.FRAME_TRANSFORM);
588 
589         // the quadratic contribution is a small rotation:
590         // its initial angle and angular rate are both zero.
591         // small rotations are almost commutative, so we append the small
592         // quadratic part after the linear part as a simple offset
593         return quadraticContribution
594                 .compose(linearPart, RotationConvention.VECTOR_OPERATOR);
595 
596     }
597 
598 
599     /** Get a time-shifted state.
600      * <p>
601      * The state can be slightly shifted to close dates. This shift is based on
602      * an approximate solution of the fixed acceleration motion. It is <em>not</em>
603      * intended as a replacement for proper attitude propagation but should be
604      * sufficient for either small time shifts or coarse accuracy.
605      * </p>
606      * @param dt time shift in seconds
607      * @return a new state, shifted with respect to the instance (which is immutable)
608      */
609     public AngularCoordinates shiftedBy(final double dt) {
610 
611         // the shiftedBy method is based on a local approximation.
612         // It considers separately the contribution of the constant
613         // rotation, the linear contribution or the rate and the
614         // quadratic contribution of the acceleration. The rate
615         // and acceleration contributions are small rotations as long
616         // as the time shift is small, which is the crux of the algorithm.
617         // Small rotations are almost commutative, so we append these small
618         // contributions one after the other, as if they really occurred
619         // successively, despite this is not what really happens.
620 
621         // compute the linear contribution first, ignoring acceleration
622         // BEWARE: there is really a minus sign here, because if
623         // the target frame rotates in one direction, the vectors in the origin
624         // frame seem to rotate in the opposite direction
625         final double rate = rotationRate.getNorm();
626         final Rotation rateContribution = (rate == 0.0) ?
627                                           Rotation.IDENTITY :
628                                           new Rotation(rotationRate, rate * dt, RotationConvention.FRAME_TRANSFORM);
629 
630         // append rotation and rate contribution
631         final AngularCoordinates linearPart =
632                 new AngularCoordinates(rateContribution.compose(rotation, RotationConvention.VECTOR_OPERATOR), rotationRate);
633 
634         final double acc  = rotationAcceleration.getNorm();
635         if (acc == 0.0) {
636             // no acceleration, the linear part is sufficient
637             return linearPart;
638         }
639 
640         // compute the quadratic contribution, ignoring initial rotation and rotation rate
641         // BEWARE: there is really a minus sign here, because if
642         // the target frame rotates in one direction, the vectors in the origin
643         // frame seem to rotate in the opposite direction
644         final AngularCoordinates quadraticContribution =
645                 new AngularCoordinates(new Rotation(rotationAcceleration,
646                                                     0.5 * acc * dt * dt,
647                                                     RotationConvention.FRAME_TRANSFORM),
648                                        new Vector3D(dt, rotationAcceleration),
649                                        rotationAcceleration);
650 
651         // the quadratic contribution is a small rotation:
652         // its initial angle and angular rate are both zero.
653         // small rotations are almost commutative, so we append the small
654         // quadratic part after the linear part as a simple offset
655         return quadraticContribution.addOffset(linearPart);
656 
657     }
658 
659     /** Get the rotation.
660      * @return the rotation.
661      */
662     public Rotation getRotation() {
663         return rotation;
664     }
665 
666     /** Get the rotation rate.
667      * @return the rotation rate vector Ω (rad/s).
668      */
669     public Vector3D getRotationRate() {
670         return rotationRate;
671     }
672 
673     /** Get the rotation acceleration.
674      * @return the rotation acceleration vector dΩ/dt (rad/s²).
675      */
676     public Vector3D getRotationAcceleration() {
677         return rotationAcceleration;
678     }
679 
680     /** Add an offset from the instance.
681      * <p>
682      * We consider here that the offset rotation is applied first and the
683      * instance is applied afterward. Note that angular coordinates do <em>not</em>
684      * commute under this operation, i.e. {@code a.addOffset(b)} and {@code
685      * b.addOffset(a)} lead to <em>different</em> results in most cases.
686      * </p>
687      * <p>
688      * The two methods {@link #addOffset(AngularCoordinates) addOffset} and
689      * {@link #subtractOffset(AngularCoordinates) subtractOffset} are designed
690      * so that round trip applications are possible. This means that both {@code
691      * ac1.subtractOffset(ac2).addOffset(ac2)} and {@code
692      * ac1.addOffset(ac2).subtractOffset(ac2)} return angular coordinates equal to ac1.
693      * </p>
694      * @param offset offset to subtract
695      * @return new instance, with offset subtracted
696      * @see #subtractOffset(AngularCoordinates)
697      */
698     public AngularCoordinates addOffset(final AngularCoordinates offset) {
699         final Vector3D rOmega    = rotation.applyTo(offset.rotationRate);
700         final Vector3D rOmegaDot = rotation.applyTo(offset.rotationAcceleration);
701         return new AngularCoordinates(rotation.compose(offset.rotation, RotationConvention.VECTOR_OPERATOR),
702                                       rotationRate.add(rOmega),
703                                       new Vector3D( 1.0, rotationAcceleration,
704                                                     1.0, rOmegaDot,
705                                                    -1.0, Vector3D.crossProduct(rotationRate, rOmega)));
706     }
707 
708     /** Subtract an offset from the instance.
709      * <p>
710      * We consider here that the offset rotation is applied first and the
711      * instance is applied afterward. Note that angular coordinates do <em>not</em>
712      * commute under this operation, i.e. {@code a.subtractOffset(b)} and {@code
713      * b.subtractOffset(a)} lead to <em>different</em> results in most cases.
714      * </p>
715      * <p>
716      * The two methods {@link #addOffset(AngularCoordinates) addOffset} and
717      * {@link #subtractOffset(AngularCoordinates) subtractOffset} are designed
718      * so that round trip applications are possible. This means that both {@code
719      * ac1.subtractOffset(ac2).addOffset(ac2)} and {@code
720      * ac1.addOffset(ac2).subtractOffset(ac2)} return angular coordinates equal to ac1.
721      * </p>
722      * @param offset offset to subtract
723      * @return new instance, with offset subtracted
724      * @see #addOffset(AngularCoordinates)
725      */
726     public AngularCoordinates subtractOffset(final AngularCoordinates offset) {
727         return addOffset(offset.revert());
728     }
729 
730     /** Apply the rotation to a pv coordinates.
731      * @param pv vector to apply the rotation to
732      * @return a new pv coordinates which is the image of pv by the rotation
733      */
734     public PVCoordinates applyTo(final PVCoordinates pv) {
735 
736         final Vector3D transformedP = rotation.applyTo(pv.getPosition());
737         final Vector3D crossP       = Vector3D.crossProduct(rotationRate, transformedP);
738         final Vector3D transformedV = rotation.applyTo(pv.getVelocity()).subtract(crossP);
739         final Vector3D crossV       = Vector3D.crossProduct(rotationRate, transformedV);
740         final Vector3D crossCrossP  = Vector3D.crossProduct(rotationRate, crossP);
741         final Vector3D crossDotP    = Vector3D.crossProduct(rotationAcceleration, transformedP);
742         final Vector3D transformedA = new Vector3D( 1, rotation.applyTo(pv.getAcceleration()),
743                                                    -2, crossV,
744                                                    -1, crossCrossP,
745                                                    -1, crossDotP);
746 
747         return new PVCoordinates(transformedP, transformedV, transformedA);
748 
749     }
750 
751     /** Apply the rotation to a pv coordinates.
752      * @param pv vector to apply the rotation to
753      * @return a new pv coordinates which is the image of pv by the rotation
754      */
755     public TimeStampedPVCoordinates applyTo(final TimeStampedPVCoordinates pv) {
756 
757         final Vector3D transformedP = getRotation().applyTo(pv.getPosition());
758         final Vector3D crossP       = Vector3D.crossProduct(getRotationRate(), transformedP);
759         final Vector3D transformedV = getRotation().applyTo(pv.getVelocity()).subtract(crossP);
760         final Vector3D crossV       = Vector3D.crossProduct(getRotationRate(), transformedV);
761         final Vector3D crossCrossP  = Vector3D.crossProduct(getRotationRate(), crossP);
762         final Vector3D crossDotP    = Vector3D.crossProduct(getRotationAcceleration(), transformedP);
763         final Vector3D transformedA = new Vector3D( 1, getRotation().applyTo(pv.getAcceleration()),
764                                                    -2, crossV,
765                                                    -1, crossCrossP,
766                                                    -1, crossDotP);
767 
768         return new TimeStampedPVCoordinates(pv.getDate(), transformedP, transformedV, transformedA);
769 
770     }
771 
772     /** Apply the rotation to a pv coordinates.
773      * @param pv vector to apply the rotation to
774      * @param <T> type of the field elements
775      * @return a new pv coordinates which is the image of pv by the rotation
776      * @since 9.0
777      */
778     public <T extends CalculusFieldElement<T>> FieldPVCoordinates<T> applyTo(final FieldPVCoordinates<T> pv) {
779 
780         final FieldVector3D<T> transformedP = FieldRotation.applyTo(rotation, pv.getPosition());
781         final FieldVector3D<T> crossP       = FieldVector3D.crossProduct(rotationRate, transformedP);
782         final FieldVector3D<T> transformedV = FieldRotation.applyTo(rotation, pv.getVelocity()).subtract(crossP);
783         final FieldVector3D<T> crossV       = FieldVector3D.crossProduct(rotationRate, transformedV);
784         final FieldVector3D<T> crossCrossP  = FieldVector3D.crossProduct(rotationRate, crossP);
785         final FieldVector3D<T> crossDotP    = FieldVector3D.crossProduct(rotationAcceleration, transformedP);
786         final FieldVector3D<T> transformedA = new FieldVector3D<>( 1, FieldRotation.applyTo(rotation, pv.getAcceleration()),
787                                                                   -2, crossV,
788                                                                   -1, crossCrossP,
789                                                                   -1, crossDotP);
790 
791         return new FieldPVCoordinates<>(transformedP, transformedV, transformedA);
792 
793     }
794 
795     /** Apply the rotation to a pv coordinates.
796      * @param pv vector to apply the rotation to
797      * @param <T> type of the field elements
798      * @return a new pv coordinates which is the image of pv by the rotation
799      * @since 9.0
800      */
801     public <T extends CalculusFieldElement<T>> TimeStampedFieldPVCoordinates<T> applyTo(final TimeStampedFieldPVCoordinates<T> pv) {
802 
803         final FieldVector3D<T> transformedP = FieldRotation.applyTo(rotation, pv.getPosition());
804         final FieldVector3D<T> crossP       = FieldVector3D.crossProduct(rotationRate, transformedP);
805         final FieldVector3D<T> transformedV = FieldRotation.applyTo(rotation, pv.getVelocity()).subtract(crossP);
806         final FieldVector3D<T> crossV       = FieldVector3D.crossProduct(rotationRate, transformedV);
807         final FieldVector3D<T> crossCrossP  = FieldVector3D.crossProduct(rotationRate, crossP);
808         final FieldVector3D<T> crossDotP    = FieldVector3D.crossProduct(rotationAcceleration, transformedP);
809         final FieldVector3D<T> transformedA = new FieldVector3D<>( 1, FieldRotation.applyTo(rotation, pv.getAcceleration()),
810                                                                   -2, crossV,
811                                                                   -1, crossCrossP,
812                                                                   -1, crossDotP);
813 
814         return new TimeStampedFieldPVCoordinates<>(pv.getDate(), transformedP, transformedV, transformedA);
815 
816     }
817 
818     /** Convert rotation, rate and acceleration to modified Rodrigues vector and derivatives.
819      * <p>
820      * The modified Rodrigues vector is tan(θ/4) u where θ and u are the
821      * rotation angle and axis respectively.
822      * </p>
823      * @param sign multiplicative sign for quaternion components
824      * @return modified Rodrigues vector and derivatives (vector on row 0, first derivative
825      * on row 1, second derivative on row 2)
826      * @see #createFromModifiedRodrigues(double[][])
827      */
828     public double[][] getModifiedRodrigues(final double sign) {
829 
830         final double q0    = sign * getRotation().getQ0();
831         final double q1    = sign * getRotation().getQ1();
832         final double q2    = sign * getRotation().getQ2();
833         final double q3    = sign * getRotation().getQ3();
834         final double oX    = getRotationRate().getX();
835         final double oY    = getRotationRate().getY();
836         final double oZ    = getRotationRate().getZ();
837         final double oXDot = getRotationAcceleration().getX();
838         final double oYDot = getRotationAcceleration().getY();
839         final double oZDot = getRotationAcceleration().getZ();
840 
841         // first time-derivatives of the quaternion
842         final double q0Dot = 0.5 * MathArrays.linearCombination(-q1, oX, -q2, oY, -q3, oZ);
843         final double q1Dot = 0.5 * MathArrays.linearCombination( q0, oX, -q3, oY,  q2, oZ);
844         final double q2Dot = 0.5 * MathArrays.linearCombination( q3, oX,  q0, oY, -q1, oZ);
845         final double q3Dot = 0.5 * MathArrays.linearCombination(-q2, oX,  q1, oY,  q0, oZ);
846 
847         // second time-derivatives of the quaternion
848         final double q0DotDot = -0.5 * MathArrays.linearCombination(new double[] {
849             q1, q2,  q3, q1Dot, q2Dot,  q3Dot
850         }, new double[] {
851             oXDot, oYDot, oZDot, oX, oY, oZ
852         });
853         final double q1DotDot =  0.5 * MathArrays.linearCombination(new double[] {
854             q0, q2, -q3, q0Dot, q2Dot, -q3Dot
855         }, new double[] {
856             oXDot, oZDot, oYDot, oX, oZ, oY
857         });
858         final double q2DotDot =  0.5 * MathArrays.linearCombination(new double[] {
859             q0, q3, -q1, q0Dot, q3Dot, -q1Dot
860         }, new double[] {
861             oYDot, oXDot, oZDot, oY, oX, oZ
862         });
863         final double q3DotDot =  0.5 * MathArrays.linearCombination(new double[] {
864             q0, q1, -q2, q0Dot, q1Dot, -q2Dot
865         }, new double[] {
866             oZDot, oYDot, oXDot, oZ, oY, oX
867         });
868 
869         // the modified Rodrigues is tan(θ/4) u where θ and u are the rotation angle and axis respectively
870         // this can be rewritten using quaternion components:
871         //      r (q₁ / (1+q₀), q₂ / (1+q₀), q₃ / (1+q₀))
872         // applying the derivation chain rule to previous expression gives rDot and rDotDot
873         final double inv          = 1.0 / (1.0 + q0);
874         final double mTwoInvQ0Dot = -2 * inv * q0Dot;
875 
876         final double r1       = inv * q1;
877         final double r2       = inv * q2;
878         final double r3       = inv * q3;
879 
880         final double mInvR1   = -inv * r1;
881         final double mInvR2   = -inv * r2;
882         final double mInvR3   = -inv * r3;
883 
884         final double r1Dot    = MathArrays.linearCombination(inv, q1Dot, mInvR1, q0Dot);
885         final double r2Dot    = MathArrays.linearCombination(inv, q2Dot, mInvR2, q0Dot);
886         final double r3Dot    = MathArrays.linearCombination(inv, q3Dot, mInvR3, q0Dot);
887 
888         final double r1DotDot = MathArrays.linearCombination(inv, q1DotDot, mTwoInvQ0Dot, r1Dot, mInvR1, q0DotDot);
889         final double r2DotDot = MathArrays.linearCombination(inv, q2DotDot, mTwoInvQ0Dot, r2Dot, mInvR2, q0DotDot);
890         final double r3DotDot = MathArrays.linearCombination(inv, q3DotDot, mTwoInvQ0Dot, r3Dot, mInvR3, q0DotDot);
891 
892         return new double[][] {
893             {
894                 r1,       r2,       r3
895             }, {
896                 r1Dot,    r2Dot,    r3Dot
897             }, {
898                 r1DotDot, r2DotDot, r3DotDot
899             }
900         };
901 
902     }
903 
904     /** Convert a modified Rodrigues vector and derivatives to angular coordinates.
905      * @param r modified Rodrigues vector (with first and second times derivatives)
906      * @return angular coordinates
907      * @see #getModifiedRodrigues(double)
908      */
909     public static AngularCoordinates createFromModifiedRodrigues(final double[][] r) {
910 
911         // rotation
912         final double rSquared = r[0][0] * r[0][0] + r[0][1] * r[0][1] + r[0][2] * r[0][2];
913         final double oPQ0     = 2 / (1 + rSquared);
914         final double q0       = oPQ0 - 1;
915         final double q1       = oPQ0 * r[0][0];
916         final double q2       = oPQ0 * r[0][1];
917         final double q3       = oPQ0 * r[0][2];
918 
919         // rotation rate
920         final double oPQ02    = oPQ0 * oPQ0;
921         final double q0Dot    = -oPQ02 * MathArrays.linearCombination(r[0][0], r[1][0], r[0][1], r[1][1],  r[0][2], r[1][2]);
922         final double q1Dot    = oPQ0 * r[1][0] + r[0][0] * q0Dot;
923         final double q2Dot    = oPQ0 * r[1][1] + r[0][1] * q0Dot;
924         final double q3Dot    = oPQ0 * r[1][2] + r[0][2] * q0Dot;
925         final double oX       = 2 * MathArrays.linearCombination(-q1, q0Dot,  q0, q1Dot,  q3, q2Dot, -q2, q3Dot);
926         final double oY       = 2 * MathArrays.linearCombination(-q2, q0Dot, -q3, q1Dot,  q0, q2Dot,  q1, q3Dot);
927         final double oZ       = 2 * MathArrays.linearCombination(-q3, q0Dot,  q2, q1Dot, -q1, q2Dot,  q0, q3Dot);
928 
929         // rotation acceleration
930         final double q0DotDot = (1 - q0) / oPQ0 * q0Dot * q0Dot -
931                                 oPQ02 * MathArrays.linearCombination(r[0][0], r[2][0], r[0][1], r[2][1], r[0][2], r[2][2]) -
932                                 (q1Dot * q1Dot + q2Dot * q2Dot + q3Dot * q3Dot);
933         final double q1DotDot = MathArrays.linearCombination(oPQ0, r[2][0], 2 * r[1][0], q0Dot, r[0][0], q0DotDot);
934         final double q2DotDot = MathArrays.linearCombination(oPQ0, r[2][1], 2 * r[1][1], q0Dot, r[0][1], q0DotDot);
935         final double q3DotDot = MathArrays.linearCombination(oPQ0, r[2][2], 2 * r[1][2], q0Dot, r[0][2], q0DotDot);
936         final double oXDot    = 2 * MathArrays.linearCombination(-q1, q0DotDot,  q0, q1DotDot,  q3, q2DotDot, -q2, q3DotDot);
937         final double oYDot    = 2 * MathArrays.linearCombination(-q2, q0DotDot, -q3, q1DotDot,  q0, q2DotDot,  q1, q3DotDot);
938         final double oZDot    = 2 * MathArrays.linearCombination(-q3, q0DotDot,  q2, q1DotDot, -q1, q2DotDot,  q0, q3DotDot);
939 
940         return new AngularCoordinates(new Rotation(q0, q1, q2, q3, false),
941                                       new Vector3D(oX, oY, oZ),
942                                       new Vector3D(oXDot, oYDot, oZDot));
943 
944     }
945 
946 }