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