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