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.analysis.differentiation.DerivativeStructure;
20  import org.hipparchus.analysis.differentiation.UnivariateDerivative1;
21  import org.hipparchus.analysis.differentiation.UnivariateDerivative2;
22  import org.hipparchus.exception.MathIllegalArgumentException;
23  import org.hipparchus.geometry.euclidean.threed.FieldRotation;
24  import org.hipparchus.geometry.euclidean.threed.Rotation;
25  import org.hipparchus.geometry.euclidean.threed.RotationConvention;
26  import org.hipparchus.geometry.euclidean.threed.Vector3D;
27  import org.hipparchus.ode.ODEIntegrator;
28  import org.hipparchus.ode.ODEState;
29  import org.hipparchus.ode.ODEStateAndDerivative;
30  import org.hipparchus.ode.OrdinaryDifferentialEquation;
31  import org.hipparchus.ode.nonstiff.DormandPrince853Integrator;
32  import org.hipparchus.ode.sampling.ODEFixedStepHandler;
33  import org.hipparchus.ode.sampling.StepNormalizer;
34  import org.hipparchus.random.RandomGenerator;
35  import org.hipparchus.random.Well1024a;
36  import org.hipparchus.util.FastMath;
37  import org.hipparchus.util.MathArrays;
38  import org.hipparchus.util.Precision;
39  import org.junit.jupiter.api.Assertions;
40  import org.junit.jupiter.api.Test;
41  import org.orekit.errors.OrekitException;
42  import org.orekit.errors.OrekitMessages;
43  
44  import java.lang.reflect.InvocationTargetException;
45  import java.lang.reflect.Method;
46  
47  public class AngularCoordinatesTest {
48  
49      @Test
50      public void testAccelerationModeling() {
51          double rate = 2 * FastMath.PI / (12 * 60);
52          double acc  = 0.01;
53          double dt   = 1.0;
54          int    n    = 2000;
55          final AngularCoordinates quadratic =
56                  new AngularCoordinates(Rotation.IDENTITY,
57                                         new Vector3D(rate, Vector3D.PLUS_K),
58                                         new Vector3D(acc,  Vector3D.PLUS_J));
59  
60          final OrdinaryDifferentialEquation ode = new OrdinaryDifferentialEquation() {
61              public int getDimension() {
62                  return 4;
63              }
64              public double[] computeDerivatives(final double t, final double[] q) {
65                  final double omegaX = quadratic.getRotationRate().getX() + t * quadratic.getRotationAcceleration().getX();
66                  final double omegaY = quadratic.getRotationRate().getY() + t * quadratic.getRotationAcceleration().getY();
67                  final double omegaZ = quadratic.getRotationRate().getZ() + t * quadratic.getRotationAcceleration().getZ();
68                  return new double[] {
69                      0.5 * MathArrays.linearCombination(-q[1], omegaX, -q[2], omegaY, -q[3], omegaZ),
70                      0.5 * MathArrays.linearCombination( q[0], omegaX, -q[3], omegaY,  q[2], omegaZ),
71                      0.5 * MathArrays.linearCombination( q[3], omegaX,  q[0], omegaY, -q[1], omegaZ),
72                      0.5 * MathArrays.linearCombination(-q[2], omegaX,  q[1], omegaY,  q[0], omegaZ)
73                  };
74              }
75          };
76          ODEIntegrator integrator = new DormandPrince853Integrator(1.0e-6, 1.0, 1.0e-12, 1.0e-12);
77          integrator.addStepHandler(new StepNormalizer(dt / n, new ODEFixedStepHandler() {
78              private double   tM4, tM3, tM2, tM1, t0, tP1, tP2, tP3, tP4;
79              private double[] yM4, yM3, yM2, yM1, y0, yP1, yP2, yP3, yP4;
80              private double[] ydM4, ydM3, ydM2, ydM1, yd0, ydP1, ydP2, ydP3, ydP4;
81              public void handleStep(ODEStateAndDerivative s, boolean isLast) {
82                  tM4 = tM3; yM4 = yM3; ydM4 = ydM3;
83                  tM3 = tM2; yM3 = yM2; ydM3 = ydM2;
84                  tM2 = tM1; yM2 = yM1; ydM2 = ydM1;
85                  tM1 = t0 ; yM1 = y0 ; ydM1 = yd0 ;
86                  t0  = tP1; y0  = yP1; yd0  = ydP1;
87                  tP1 = tP2; yP1 = yP2; ydP1 = ydP2;
88                  tP2 = tP3; yP2 = yP3; ydP2 = ydP3;
89                  tP3 = tP4; yP3 = yP4; ydP3 = ydP4;
90                  tP4  = s.getTime();
91                  yP4  = s.getPrimaryState();
92                  ydP4 = s.getPrimaryDerivative();
93  
94                  if (yM4 != null) {
95                      double dt = (tP4 - tM4) / 8;
96                      final double c = 1.0 / (840 * dt);
97                      final double[] ydd0 = {
98                          -3 * c * (ydP4[0] - ydM4[0]) + 32 * c * (ydP3[0] - ydM3[0]) - 168 * c * (ydP2[0] - ydM2[0]) + 672 * c * (ydP1[0] - ydM1[0]),
99                          -3 * c * (ydP4[1] - ydM4[1]) + 32 * c * (ydP3[1] - ydM3[1]) - 168 * c * (ydP2[1] - ydM2[1]) + 672 * c * (ydP1[1] - ydM1[1]),
100                         -3 * c * (ydP4[2] - ydM4[2]) + 32 * c * (ydP3[2] - ydM3[2]) - 168 * c * (ydP2[2] - ydM2[2]) + 672 * c * (ydP1[2] - ydM1[2]),
101                         -3 * c * (ydP4[3] - ydM4[3]) + 32 * c * (ydP3[3] - ydM3[3]) - 168 * c * (ydP2[3] - ydM2[3]) + 672 * c * (ydP1[3] - ydM1[3]),
102                     };
103                     AngularCoordinates ac = new AngularCoordinates(new FieldRotation<>(new UnivariateDerivative2(y0[0], yd0[0], ydd0[0]),
104                                     new UnivariateDerivative2(y0[1], yd0[1], ydd0[1]),
105                                     new UnivariateDerivative2(y0[2], yd0[2], ydd0[2]),
106                                     new UnivariateDerivative2(y0[3], yd0[3], ydd0[3]),
107                                     false));
108                     Assertions.assertEquals(0.0,
109                                             Vector3D.distance(quadratic.getRotationAcceleration(),
110                                                               ac.getRotationAcceleration()),
111                                             4.0e-13);
112                 }
113 
114            }
115         }));
116 
117         double[] y = new double[] {
118             quadratic.getRotation().getQ0(),
119             quadratic.getRotation().getQ1(),
120             quadratic.getRotation().getQ2(),
121             quadratic.getRotation().getQ3()
122         };
123         integrator.integrate(ode, new ODEState(0, y), dt);
124 
125     }
126 
127     @Test
128     public void testDefaultConstructor() {
129         AngularCoordinates ac = new AngularCoordinates();
130         Assertions.assertEquals(0.0, ac.getRotationAcceleration().getNorm(), 1.0e-15);
131         Assertions.assertEquals(0.0, ac.getRotationRate().getNorm(), 1.0e-15);
132         Assertions.assertEquals(0.0, Rotation.distance(ac.getRotation(), Rotation.IDENTITY), 1.0e-10);
133     }
134 
135     @Test
136     public void testDerivativesStructuresNeg() {
137         try {
138             AngularCoordinates.IDENTITY.toDerivativeStructureRotation(-1);
139             Assertions.fail("an exception should have been thrown");
140         } catch (OrekitException oe) {
141             Assertions.assertEquals(OrekitMessages.OUT_OF_RANGE_DERIVATION_ORDER, oe.getSpecifier());
142             Assertions.assertEquals(-1, ((Integer) (oe.getParts()[0])).intValue());
143         }
144 
145     }
146 
147     @Test
148     public void testDerivativesStructures3() {
149         try {
150             AngularCoordinates.IDENTITY.toDerivativeStructureRotation(3);
151             Assertions.fail("an exception should have been thrown");
152         } catch (OrekitException oe) {
153             Assertions.assertEquals(OrekitMessages.OUT_OF_RANGE_DERIVATION_ORDER, oe.getSpecifier());
154             Assertions.assertEquals(3, ((Integer) (oe.getParts()[0])).intValue());
155         }
156 
157     }
158 
159     @Test
160     public void testDerivativesStructures0() {
161         RandomGenerator random = new Well1024a(0x18a0a08fd63f047al);
162 
163         Rotation r    = randomRotation(random);
164         Vector3D o    = randomVector(random, 1.0e-2);
165         Vector3D oDot = randomVector(random, 1.0e-2);
166         AngularCoordinates ac = new AngularCoordinates(r, o, oDot);
167         AngularCoordinates rebuilt = new AngularCoordinates(ac.toDerivativeStructureRotation(0));
168         Assertions.assertEquals(0.0, Rotation.distance(ac.getRotation(), rebuilt.getRotation()), 1.0e-15);
169         Assertions.assertEquals(0.0, rebuilt.getRotationRate().getNorm(), 1.0e-15);
170         Assertions.assertEquals(0.0, rebuilt.getRotationAcceleration().getNorm(), 1.0e-15);
171     }
172 
173     @Test
174     public void testDerivativesStructures1() {
175         RandomGenerator random = new Well1024a(0x8f8fc6d27bbdc46dl);
176 
177         Rotation r    = randomRotation(random);
178         Vector3D o    = randomVector(random, 1.0e-2);
179         Vector3D oDot = randomVector(random, 1.0e-2);
180         AngularCoordinates ac = new AngularCoordinates(r, o, oDot);
181         AngularCoordinates rebuilt = new AngularCoordinates(ac.toDerivativeStructureRotation(1));
182         Assertions.assertEquals(0.0, Rotation.distance(ac.getRotation(), rebuilt.getRotation()), 1.0e-15);
183         Assertions.assertEquals(0.0, Vector3D.distance(ac.getRotationRate(), rebuilt.getRotationRate()), 1.0e-15);
184         Assertions.assertEquals(0.0, rebuilt.getRotationAcceleration().getNorm(), 1.0e-15);
185     }
186 
187     @Test
188     public void testDerivativesStructures2() {
189         RandomGenerator random = new Well1024a(0x1633878dddac047dl);
190 
191         Rotation r    = randomRotation(random);
192         Vector3D o    = randomVector(random, 1.0e-2);
193         Vector3D oDot = randomVector(random, 1.0e-2);
194         AngularCoordinates ac = new AngularCoordinates(r, o, oDot);
195         AngularCoordinates rebuilt = new AngularCoordinates(ac.toDerivativeStructureRotation(2));
196         Assertions.assertEquals(0.0, Rotation.distance(ac.getRotation(), rebuilt.getRotation()), 1.0e-15);
197         Assertions.assertEquals(0.0, Vector3D.distance(ac.getRotationRate(), rebuilt.getRotationRate()), 1.0e-15);
198         Assertions.assertEquals(0.0, Vector3D.distance(ac.getRotationAcceleration(), rebuilt.getRotationAcceleration()), 1.0e-15);
199     }
200 
201     @Test
202     public void testUnivariateDerivative1() {
203         RandomGenerator random = new Well1024a(0x6de8cce747539904l);
204 
205         Rotation r    = randomRotation(random);
206         Vector3D o    = randomVector(random, 1.0e-2);
207         Vector3D oDot = randomVector(random, 1.0e-2);
208         AngularCoordinates ac = new AngularCoordinates(r, o, oDot);
209         FieldRotation<UnivariateDerivative1> rotationUD = ac.toUnivariateDerivative1Rotation();
210         FieldRotation<DerivativeStructure>   rotationDS = ac.toDerivativeStructureRotation(1);
211         Assertions.assertEquals(rotationDS.getQ0().getReal(), rotationUD.getQ0().getReal(), 1.0e-15);
212         Assertions.assertEquals(rotationDS.getQ1().getReal(), rotationUD.getQ1().getReal(), 1.0e-15);
213         Assertions.assertEquals(rotationDS.getQ2().getReal(), rotationUD.getQ2().getReal(), 1.0e-15);
214         Assertions.assertEquals(rotationDS.getQ3().getReal(), rotationUD.getQ3().getReal(), 1.0e-15);
215         Assertions.assertEquals(rotationDS.getQ0().getPartialDerivative(1), rotationUD.getQ0().getFirstDerivative(), 1.0e-15);
216         Assertions.assertEquals(rotationDS.getQ1().getPartialDerivative(1), rotationUD.getQ1().getFirstDerivative(), 1.0e-15);
217         Assertions.assertEquals(rotationDS.getQ2().getPartialDerivative(1), rotationUD.getQ2().getFirstDerivative(), 1.0e-15);
218         Assertions.assertEquals(rotationDS.getQ3().getPartialDerivative(1), rotationUD.getQ3().getFirstDerivative(), 1.0e-15);
219 
220         AngularCoordinates rebuilt = new AngularCoordinates(rotationUD);
221         Assertions.assertEquals(0.0, Rotation.distance(ac.getRotation(), rebuilt.getRotation()), 1.0e-15);
222         Assertions.assertEquals(0.0, Vector3D.distance(ac.getRotationRate(), rebuilt.getRotationRate()), 1.0e-15);
223 
224     }
225 
226     @Test
227     public void testUnivariateDerivative2() {
228         RandomGenerator random = new Well1024a(0x255710c8fa2247ecl);
229 
230         Rotation r    = randomRotation(random);
231         Vector3D o    = randomVector(random, 1.0e-2);
232         Vector3D oDot = randomVector(random, 1.0e-2);
233         AngularCoordinates ac = new AngularCoordinates(r, o, oDot);
234         FieldRotation<UnivariateDerivative2> rotationUD = ac.toUnivariateDerivative2Rotation();
235         FieldRotation<DerivativeStructure>   rotationDS = ac.toDerivativeStructureRotation(2);
236         Assertions.assertEquals(rotationDS.getQ0().getReal(), rotationUD.getQ0().getReal(), 1.0e-15);
237         Assertions.assertEquals(rotationDS.getQ1().getReal(), rotationUD.getQ1().getReal(), 1.0e-15);
238         Assertions.assertEquals(rotationDS.getQ2().getReal(), rotationUD.getQ2().getReal(), 1.0e-15);
239         Assertions.assertEquals(rotationDS.getQ3().getReal(), rotationUD.getQ3().getReal(), 1.0e-15);
240         Assertions.assertEquals(rotationDS.getQ0().getPartialDerivative(1), rotationUD.getQ0().getFirstDerivative(), 1.0e-15);
241         Assertions.assertEquals(rotationDS.getQ1().getPartialDerivative(1), rotationUD.getQ1().getFirstDerivative(), 1.0e-15);
242         Assertions.assertEquals(rotationDS.getQ2().getPartialDerivative(1), rotationUD.getQ2().getFirstDerivative(), 1.0e-15);
243         Assertions.assertEquals(rotationDS.getQ3().getPartialDerivative(1), rotationUD.getQ3().getFirstDerivative(), 1.0e-15);
244         Assertions.assertEquals(rotationDS.getQ0().getPartialDerivative(2), rotationUD.getQ0().getSecondDerivative(), 1.0e-15);
245         Assertions.assertEquals(rotationDS.getQ1().getPartialDerivative(2), rotationUD.getQ1().getSecondDerivative(), 1.0e-15);
246         Assertions.assertEquals(rotationDS.getQ2().getPartialDerivative(2), rotationUD.getQ2().getSecondDerivative(), 1.0e-15);
247         Assertions.assertEquals(rotationDS.getQ3().getPartialDerivative(2), rotationUD.getQ3().getSecondDerivative(), 1.0e-15);
248 
249         AngularCoordinates rebuilt = new AngularCoordinates(rotationUD);
250         Assertions.assertEquals(0.0, Rotation.distance(ac.getRotation(), rebuilt.getRotation()), 1.0e-15);
251         Assertions.assertEquals(0.0, Vector3D.distance(ac.getRotationRate(), rebuilt.getRotationRate()), 1.0e-15);
252         Assertions.assertEquals(0.0, Vector3D.distance(ac.getRotationAcceleration(), rebuilt.getRotationAcceleration()), 1.0e-15);
253 
254     }
255 
256     @Test
257     public void testZeroRate() {
258         AngularCoordinates ac =
259                 new AngularCoordinates(new Rotation(0.48, 0.64, 0.36, 0.48, false), Vector3D.ZERO, Vector3D.ZERO);
260         Assertions.assertEquals(Vector3D.ZERO, ac.getRotationRate());
261         double dt = 10.0;
262         AngularCoordinates shifted = ac.shiftedBy(dt);
263         Assertions.assertEquals(Vector3D.ZERO, shifted.getRotationAcceleration());
264         Assertions.assertEquals(Vector3D.ZERO, shifted.getRotationRate());
265         Assertions.assertEquals(0.0, Rotation.distance(ac.getRotation(), shifted.getRotation()), 1.0e-15);
266     }
267 
268     @Test
269     public void testShiftWithoutAcceleration() {
270         double rate = 2 * FastMath.PI / (12 * 60);
271         AngularCoordinates ac =
272                 new AngularCoordinates(Rotation.IDENTITY,
273                                        new Vector3D(rate, Vector3D.PLUS_K),
274                                        Vector3D.ZERO);
275         Assertions.assertEquals(rate, ac.getRotationRate().getNorm(), 1.0e-10);
276         double dt = 10.0;
277         double alpha = rate * dt;
278         AngularCoordinates shifted = ac.shiftedBy(dt);
279         Assertions.assertEquals(rate, shifted.getRotationRate().getNorm(), 1.0e-10);
280         Assertions.assertEquals(alpha, Rotation.distance(ac.getRotation(), shifted.getRotation()), 1.0e-15);
281 
282         Vector3D xSat = shifted.getRotation().applyInverseTo(Vector3D.PLUS_I);
283         Assertions.assertEquals(0.0, xSat.subtract(new Vector3D(FastMath.cos(alpha), FastMath.sin(alpha), 0)).getNorm(), 1.0e-15);
284         Vector3D ySat = shifted.getRotation().applyInverseTo(Vector3D.PLUS_J);
285         Assertions.assertEquals(0.0, ySat.subtract(new Vector3D(-FastMath.sin(alpha), FastMath.cos(alpha), 0)).getNorm(), 1.0e-15);
286         Vector3D zSat = shifted.getRotation().applyInverseTo(Vector3D.PLUS_K);
287         Assertions.assertEquals(0.0, zSat.subtract(Vector3D.PLUS_K).getNorm(), 1.0e-15);
288 
289     }
290 
291     @Test
292     public void testShiftWithAcceleration() {
293         double rate = 2 * FastMath.PI / (12 * 60);
294         double acc  = 0.001;
295         double dt   = 1.0;
296         int    n    = 2000;
297         final AngularCoordinates quadratic =
298                 new AngularCoordinates(Rotation.IDENTITY,
299                                        new Vector3D(rate, Vector3D.PLUS_K),
300                                        new Vector3D(acc,  Vector3D.PLUS_J));
301         final AngularCoordinates linear =
302                 new AngularCoordinates(quadratic.getRotation(), quadratic.getRotationRate(), Vector3D.ZERO);
303 
304         final OrdinaryDifferentialEquation ode = new OrdinaryDifferentialEquation() {
305             public int getDimension() {
306                 return 4;
307             }
308             public double[] computeDerivatives(final double t, final double[] q) {
309                 final double omegaX = quadratic.getRotationRate().getX() + t * quadratic.getRotationAcceleration().getX();
310                 final double omegaY = quadratic.getRotationRate().getY() + t * quadratic.getRotationAcceleration().getY();
311                 final double omegaZ = quadratic.getRotationRate().getZ() + t * quadratic.getRotationAcceleration().getZ();
312                 return new double[] {
313                     0.5 * MathArrays.linearCombination(-q[1], omegaX, -q[2], omegaY, -q[3], omegaZ),
314                     0.5 * MathArrays.linearCombination( q[0], omegaX, -q[3], omegaY,  q[2], omegaZ),
315                     0.5 * MathArrays.linearCombination( q[3], omegaX,  q[0], omegaY, -q[1], omegaZ),
316                     0.5 * MathArrays.linearCombination(-q[2], omegaX,  q[1], omegaY,  q[0], omegaZ)
317                 };
318             }
319         };
320         ODEIntegrator integrator = new DormandPrince853Integrator(1.0e-6, 1.0, 1.0e-12, 1.0e-12);
321         integrator.addStepHandler(new StepNormalizer(dt / n, new ODEFixedStepHandler() {
322             public void handleStep(ODEStateAndDerivative s, boolean isLast) {
323                 final double   t = s.getTime();
324                 final double[] y = s.getPrimaryState();
325                 Rotation reference = new Rotation(y[0], y[1], y[2], y[3], true);
326 
327                 // the error in shiftedBy taking acceleration into account is cubic
328                 double expectedCubicError     = 1.4544e-6 * t * t * t;
329                 Assertions.assertEquals(expectedCubicError,
330                                     Rotation.distance(reference, quadratic.shiftedBy(t).getRotation()),
331                                     0.0001 * expectedCubicError);
332                 Assertions.assertEquals(expectedCubicError,
333                                     Rotation.distance(reference, quadratic.rotationShiftedBy(t)),
334                                     0.0001 * expectedCubicError);
335 
336                 // the error in shiftedBy not taking acceleration into account is quadratic
337                 double expectedQuadraticError = 5.0e-4 * t * t;
338                 Assertions.assertEquals(expectedQuadraticError,
339                                     Rotation.distance(reference, linear.shiftedBy(t).getRotation()),
340                                     0.00001 * expectedQuadraticError);
341                 Assertions.assertEquals(expectedQuadraticError,
342                                     Rotation.distance(reference, linear.rotationShiftedBy(t)),
343                                     0.00001 * expectedQuadraticError);
344 
345             }
346         }));
347 
348         double[] y = new double[] {
349             quadratic.getRotation().getQ0(),
350             quadratic.getRotation().getQ1(),
351             quadratic.getRotation().getQ2(),
352             quadratic.getRotation().getQ3()
353         };
354         integrator.integrate(ode, new ODEState(0, y), dt);
355 
356     }
357 
358     @Test
359     public void testSpin() {
360         double rate = 2 * FastMath.PI / (12 * 60);
361         AngularCoordinates angularCoordinates =
362                 new AngularCoordinates(new Rotation(0.48, 0.64, 0.36, 0.48, false),
363                                        new Vector3D(rate, Vector3D.PLUS_K));
364         Assertions.assertEquals(rate, angularCoordinates.getRotationRate().getNorm(), 1.0e-10);
365         double dt = 10.0;
366         AngularCoordinates shifted = angularCoordinates.shiftedBy(dt);
367         Assertions.assertEquals(rate, shifted.getRotationRate().getNorm(), 1.0e-10);
368         Assertions.assertEquals(rate * dt, Rotation.distance(angularCoordinates.getRotation(), shifted.getRotation()), 1.0e-10);
369 
370         Vector3D shiftedX  = shifted.getRotation().applyInverseTo(Vector3D.PLUS_I);
371         Vector3D shiftedY  = shifted.getRotation().applyInverseTo(Vector3D.PLUS_J);
372         Vector3D shiftedZ  = shifted.getRotation().applyInverseTo(Vector3D.PLUS_K);
373         Vector3D originalX = angularCoordinates.getRotation().applyInverseTo(Vector3D.PLUS_I);
374         Vector3D originalY = angularCoordinates.getRotation().applyInverseTo(Vector3D.PLUS_J);
375         Vector3D originalZ = angularCoordinates.getRotation().applyInverseTo(Vector3D.PLUS_K);
376         Assertions.assertEquals( FastMath.cos(rate * dt), Vector3D.dotProduct(shiftedX, originalX), 1.0e-10);
377         Assertions.assertEquals( FastMath.sin(rate * dt), Vector3D.dotProduct(shiftedX, originalY), 1.0e-10);
378         Assertions.assertEquals( 0.0,                 Vector3D.dotProduct(shiftedX, originalZ), 1.0e-10);
379         Assertions.assertEquals(-FastMath.sin(rate * dt), Vector3D.dotProduct(shiftedY, originalX), 1.0e-10);
380         Assertions.assertEquals( FastMath.cos(rate * dt), Vector3D.dotProduct(shiftedY, originalY), 1.0e-10);
381         Assertions.assertEquals( 0.0,                 Vector3D.dotProduct(shiftedY, originalZ), 1.0e-10);
382         Assertions.assertEquals( 0.0,                 Vector3D.dotProduct(shiftedZ, originalX), 1.0e-10);
383         Assertions.assertEquals( 0.0,                 Vector3D.dotProduct(shiftedZ, originalY), 1.0e-10);
384         Assertions.assertEquals( 1.0,                 Vector3D.dotProduct(shiftedZ, originalZ), 1.0e-10);
385 
386         Vector3D forward = AngularCoordinates.estimateRate(angularCoordinates.getRotation(), shifted.getRotation(), dt);
387         Assertions.assertEquals(0.0, forward.subtract(angularCoordinates.getRotationRate()).getNorm(), 1.0e-10);
388 
389         Vector3D reversed = AngularCoordinates.estimateRate(shifted.getRotation(), angularCoordinates.getRotation(), dt);
390         Assertions.assertEquals(0.0, reversed.add(angularCoordinates.getRotationRate()).getNorm(), 1.0e-10);
391 
392     }
393 
394     @Test
395     public void testReverseOffset() {
396         RandomGenerator random = new Well1024a(0x4ecca9d57a8f1611l);
397         for (int i = 0; i < 100; ++i) {
398             Rotation r = randomRotation(random);
399             Vector3D o = randomVector(random, 1.0e-3);
400             AngularCoordinates ac = new AngularCoordinates(r, o);
401             AngularCoordinates sum = ac.addOffset(ac.revert());
402             Assertions.assertEquals(0.0, sum.getRotation().getAngle(), 1.0e-15);
403             Assertions.assertEquals(0.0, sum.getRotationRate().getNorm(), 1.0e-15);
404             Assertions.assertEquals(0.0, sum.getRotationAcceleration().getNorm(), 1.0e-15);
405         }
406     }
407 
408     @Test
409     public void testNoCommute() {
410         AngularCoordinates ac1 =
411                 new AngularCoordinates(new Rotation(0.48,  0.64, 0.36, 0.48, false), Vector3D.ZERO);
412         AngularCoordinates ac2 =
413                 new AngularCoordinates(new Rotation(0.36, -0.48, 0.48, 0.64, false), Vector3D.ZERO);
414 
415         AngularCoordinates add12 = ac1.addOffset(ac2);
416         AngularCoordinates add21 = ac2.addOffset(ac1);
417 
418         // the rotations are really different from each other
419         Assertions.assertEquals(2.574, Rotation.distance(add12.getRotation(), add21.getRotation()), 1.0e-3);
420 
421     }
422 
423     @Test
424     public void testRoundTripNoOp() {
425         RandomGenerator random = new Well1024a(0x1e610cfe89306669l);
426         for (int i = 0; i < 100; ++i) {
427 
428             Rotation r1    = randomRotation(random);
429             Vector3D o1    = randomVector(random, 1.0e-2);
430             Vector3D oDot1 = randomVector(random, 1.0e-2);
431             AngularCoordinates ac1 = new AngularCoordinates(r1, o1, oDot1);
432 
433             Rotation r2    = randomRotation(random);
434             Vector3D o2    = randomVector(random, 1.0e-2);
435             Vector3D oDot2 = randomVector(random, 1.0e-2);
436             AngularCoordinates ac2 = new AngularCoordinates(r2, o2, oDot2);
437 
438             AngularCoordinates roundTripSA = ac1.subtractOffset(ac2).addOffset(ac2);
439             Assertions.assertEquals(0.0, Rotation.distance(ac1.getRotation(), roundTripSA.getRotation()), 5.0e-16);
440             Assertions.assertEquals(0.0, Vector3D.distance(ac1.getRotationRate(), roundTripSA.getRotationRate()), 2.0e-17);
441             Assertions.assertEquals(0.0, Vector3D.distance(ac1.getRotationAcceleration(), roundTripSA.getRotationAcceleration()), 2.0e-17);
442 
443             AngularCoordinates roundTripAS = ac1.addOffset(ac2).subtractOffset(ac2);
444             Assertions.assertEquals(0.0, Rotation.distance(ac1.getRotation(), roundTripAS.getRotation()), 5.0e-16);
445             Assertions.assertEquals(0.0, Vector3D.distance(ac1.getRotationRate(), roundTripAS.getRotationRate()), 2.0e-17);
446             Assertions.assertEquals(0.0, Vector3D.distance(ac1.getRotationAcceleration(), roundTripAS.getRotationAcceleration()), 2.0e-17);
447 
448         }
449     }
450 
451     @Test
452     public void testRodriguesSymmetry() {
453 
454         // check the two-way conversion result in identity
455         RandomGenerator random = new Well1024a(0xb1e615aaa8236b52l);
456         for (int i = 0; i < 1000; ++i) {
457             Rotation rotation             = randomRotation(random);
458             Vector3D rotationRate         = randomVector(random, 0.01);
459             Vector3D rotationAcceleration = randomVector(random, 0.01);
460             AngularCoordinates ac         = new AngularCoordinates(rotation, rotationRate, rotationAcceleration);
461             AngularCoordinates rebuilt    = AngularCoordinates.createFromModifiedRodrigues(ac.getModifiedRodrigues(1.0));
462             Assertions.assertEquals(0.0, Rotation.distance(rotation, rebuilt.getRotation()), 1.0e-14);
463             Assertions.assertEquals(0.0, Vector3D.distance(rotationRate, rebuilt.getRotationRate()), 1.0e-15);
464             Assertions.assertEquals(0.0, Vector3D.distance(rotationAcceleration, rebuilt.getRotationAcceleration()), 1.0e-15);
465         }
466 
467     }
468 
469     @Test
470     public void testRodriguesSpecialCases() {
471 
472         // identity
473         double[][] identity = new AngularCoordinates(Rotation.IDENTITY, Vector3D.ZERO, Vector3D.ZERO).getModifiedRodrigues(1.0);
474         for (double[] row : identity) {
475             for (double element : row) {
476                 Assertions.assertEquals(0.0, element, Precision.SAFE_MIN);
477             }
478         }
479         AngularCoordinates acId = AngularCoordinates.createFromModifiedRodrigues(identity);
480         Assertions.assertEquals(0.0, acId.getRotation().getAngle(), Precision.SAFE_MIN);
481         Assertions.assertEquals(0.0, acId.getRotationRate().getNorm(), Precision.SAFE_MIN);
482 
483         // PI angle rotation (which is singular for non-modified Rodrigues vector)
484         RandomGenerator random = new Well1024a(0x2158523e6accb859l);
485         for (int i = 0; i < 100; ++i) {
486             Vector3D axis = randomVector(random, 1.0);
487             AngularCoordinates original = new AngularCoordinates(new Rotation(axis, FastMath.PI, RotationConvention.VECTOR_OPERATOR),
488                                                                  Vector3D.ZERO, Vector3D.ZERO);
489             AngularCoordinates rebuilt = AngularCoordinates.createFromModifiedRodrigues(original.getModifiedRodrigues(1.0));
490             Assertions.assertEquals(FastMath.PI, rebuilt.getRotation().getAngle(), 1.0e-15);
491             Assertions.assertEquals(0.0, FastMath.sin(Vector3D.angle(axis, rebuilt.getRotation().getAxis(RotationConvention.VECTOR_OPERATOR))), 1.0e-15);
492             Assertions.assertEquals(0.0, rebuilt.getRotationRate().getNorm(), 1.0e-16);
493         }
494 
495     }
496 
497     @Test
498     public void testInverseCrossProducts()
499         {
500         checkInverse(Vector3D.PLUS_K, Vector3D.PLUS_I, Vector3D.PLUS_J);
501         checkInverse(Vector3D.ZERO,   Vector3D.ZERO,   Vector3D.ZERO);
502         checkInverse(Vector3D.ZERO,   Vector3D.ZERO,   Vector3D.PLUS_J);
503         checkInverse(Vector3D.PLUS_K, Vector3D.PLUS_K, Vector3D.PLUS_J);
504         checkInverse(Vector3D.ZERO,   Vector3D.PLUS_K, Vector3D.ZERO);
505         checkInverse(Vector3D.PLUS_K, Vector3D.PLUS_I, Vector3D.PLUS_K);
506         checkInverse(Vector3D.PLUS_K, Vector3D.PLUS_I, Vector3D.PLUS_I);
507         checkInverse(Vector3D.PLUS_K, Vector3D.PLUS_I, new Vector3D(1, 0, -1).normalize());
508         checkInverse(Vector3D.ZERO, Vector3D.PLUS_I, Vector3D.ZERO,   Vector3D.PLUS_J,  Vector3D.ZERO);
509     }
510 
511     @Test
512     public void testInverseCrossProductsFailures() {
513         checkInverseFailure(Vector3D.PLUS_K, Vector3D.ZERO,   Vector3D.PLUS_J, Vector3D.PLUS_I,  Vector3D.PLUS_K);
514         checkInverseFailure(Vector3D.PLUS_K, Vector3D.ZERO,   Vector3D.ZERO,   Vector3D.ZERO,    Vector3D.PLUS_K);
515         checkInverseFailure(Vector3D.PLUS_I, Vector3D.PLUS_I, Vector3D.ZERO,   Vector3D.MINUS_I, Vector3D.PLUS_K);
516         checkInverseFailure(Vector3D.PLUS_I, Vector3D.PLUS_I, Vector3D.ZERO,   Vector3D.PLUS_J,  Vector3D.PLUS_J);
517         checkInverseFailure(Vector3D.PLUS_I, Vector3D.PLUS_I, Vector3D.PLUS_J, Vector3D.PLUS_J,  Vector3D.ZERO);
518         checkInverseFailure(Vector3D.PLUS_I, Vector3D.PLUS_I, Vector3D.PLUS_J, Vector3D.ZERO,    Vector3D.PLUS_J);
519     }
520 
521     @Test
522     public void testRandomInverseCrossProducts() {
523         RandomGenerator generator = new Well1024a(0x52b29d8f6ac2d64bl);
524         for (int i = 0; i < 10000; ++i) {
525             Vector3D omega = randomVector(generator, 10 * generator.nextDouble() + 1.0);
526             Vector3D v1    = randomVector(generator, 10 * generator.nextDouble() + 1.0);
527             Vector3D v2    = randomVector(generator, 10 * generator.nextDouble() + 1.0);
528             checkInverse(omega, v1, v2);
529         }
530     }
531 
532     private void checkInverse(Vector3D omega, Vector3D v1, Vector3D v2) {
533         checkInverse(omega,
534                      v1, Vector3D.crossProduct(omega, v1),
535                      v2, Vector3D.crossProduct(omega, v2));
536     }
537 
538     private void checkInverseFailure(Vector3D omega, Vector3D v1, Vector3D c1, Vector3D v2, Vector3D c2) {
539         try {
540             checkInverse(omega, v1, c1, v2, c2);
541             Assertions.fail("an exception should have been thrown");
542         } catch (MathIllegalArgumentException miae) {
543             // expected
544         }
545     }
546 
547     private void checkInverse(Vector3D omega, Vector3D v1, Vector3D c1, Vector3D v2, Vector3D c2)
548         throws MathIllegalArgumentException {
549         try {
550             Method inverse;
551             inverse = AngularCoordinates.class.getDeclaredMethod("inverseCrossProducts",
552                                                                  Vector3D.class, Vector3D.class,
553                                                                  Vector3D.class, Vector3D.class,
554                                                                  double.class);
555             inverse.setAccessible(true);
556             Vector3D rebuilt = (Vector3D) inverse.invoke(null, v1, c1, v2, c2, 1.0e-9);
557             Assertions.assertEquals(0.0, Vector3D.distance(omega, rebuilt), 5.0e-12 * omega.getNorm());
558         } catch (NoSuchMethodException e) {
559             Assertions.fail(e.getLocalizedMessage());
560         } catch (SecurityException e) {
561             Assertions.fail(e.getLocalizedMessage());
562         } catch (IllegalAccessException e) {
563             Assertions.fail(e.getLocalizedMessage());
564         } catch (IllegalArgumentException e) {
565             Assertions.fail(e.getLocalizedMessage());
566         } catch (InvocationTargetException e) {
567             throw (MathIllegalArgumentException) e.getCause();
568         }
569     }
570 
571     @Test
572     public void testRandomPVCoordinates() {
573         RandomGenerator generator = new Well1024a(0x49eb5b92d1f94b89l);
574         for (int i = 0; i < 100; ++i) {
575             Rotation r           = randomRotation(generator);
576             Vector3D omega       = randomVector(generator, 10    * generator.nextDouble() + 1.0);
577             Vector3D omegaDot    = randomVector(generator, 0.1   * generator.nextDouble() + 0.01);
578             AngularCoordinates ref = new AngularCoordinates(r, omega, omegaDot);
579             AngularCoordinates inv = ref.revert();
580             for (int j = 0; j < 100; ++j) {
581                 PVCoordinates v1 = randomPVCoordinates(generator, 1000, 1.0, 0.001);
582                 PVCoordinates v2 = randomPVCoordinates(generator, 1000, 1.0, 0.0010);
583                 PVCoordinates u1 = inv.applyTo(v1);
584                 PVCoordinates u2 = inv.applyTo(v2);
585                 AngularCoordinates rebuilt = new AngularCoordinates(u1, u2, v1, v2, 1.0e-9);
586                 Assertions.assertEquals(0.0,
587                                     Rotation.distance(r, rebuilt.getRotation()),
588                                     4.0e-14);
589                 Assertions.assertEquals(0.0,
590                                     Vector3D.distance(omega, rebuilt.getRotationRate()),
591                                     3.0e-12 * omega.getNorm());
592                 Assertions.assertEquals(0.0,
593                                     Vector3D.distance(omegaDot, rebuilt.getRotationAcceleration()),
594                                     2.0e-6 * omegaDot.getNorm());
595             }
596         }
597     }
598 
599     @Test
600     public void testCancellingDerivatives() {
601         PVCoordinates u1 = new PVCoordinates(new Vector3D(-0.4466591282528639,   -0.009657376949231283,  -0.894652087807798),
602                                              new Vector3D(-8.897296517803556E-4,  2.7825250920407674E-4,  4.411979658413134E-4),
603                                              new Vector3D( 4.753127475302486E-7,  1.0209400376727623E-8,  9.515403756524403E-7));
604         PVCoordinates u2 = new PVCoordinates(new Vector3D( 0.23723907259910096,   0.9628700806685033,    -0.1288364474275361),
605                                              new Vector3D(-7.98741002062555E-24,  2.4979687659429984E-24, 3.9607863426704016E-24),
606                                              new Vector3D(-3.150541868418562E-23, 9.856329862034835E-24,  1.5648124883326986E-23));
607         PVCoordinates v1 = new PVCoordinates(Vector3D.PLUS_K, Vector3D.ZERO, Vector3D.ZERO);
608         PVCoordinates v2 = new PVCoordinates(Vector3D.MINUS_J, Vector3D.ZERO, Vector3D.ZERO);
609         AngularCoordinates ac = new AngularCoordinates(u1, u2, v1, v2, 1.0e-9);
610         PVCoordinates v1Computed = ac.applyTo(u1);
611         PVCoordinates v2Computed = ac.applyTo(u2);
612         Assertions.assertEquals(0, Vector3D.distance(v1.getPosition(),     v1Computed.getPosition()),     1.0e-15);
613         Assertions.assertEquals(0, Vector3D.distance(v2.getPosition(),     v2Computed.getPosition()),     1.0e-15);
614         Assertions.assertEquals(0, Vector3D.distance(v1.getVelocity(),     v1Computed.getVelocity()),     1.0e-15);
615         Assertions.assertEquals(0, Vector3D.distance(v2.getVelocity(),     v2Computed.getVelocity()),     1.0e-15);
616         Assertions.assertEquals(0, Vector3D.distance(v1.getAcceleration(), v1Computed.getAcceleration()), 1.0e-15);
617         Assertions.assertEquals(0, Vector3D.distance(v2.getAcceleration(), v2Computed.getAcceleration()), 1.0e-15);
618     }
619 
620     private Vector3D randomVector(RandomGenerator random, double norm) {
621         double n = random.nextDouble() * norm;
622         double x = random.nextDouble();
623         double y = random.nextDouble();
624         double z = random.nextDouble();
625         return new Vector3D(n, new Vector3D(x, y, z).normalize());
626     }
627 
628     private PVCoordinates randomPVCoordinates(RandomGenerator random,
629                                               double norm0, double norm1, double norm2) {
630         Vector3D p0 = randomVector(random, norm0);
631         Vector3D p1 = randomVector(random, norm1);
632         Vector3D p2 = randomVector(random, norm2);
633         return new PVCoordinates(p0, p1, p2);
634     }
635 
636     private Rotation randomRotation(RandomGenerator random) {
637         double q0 = random.nextDouble() * 2 - 1;
638         double q1 = random.nextDouble() * 2 - 1;
639         double q2 = random.nextDouble() * 2 - 1;
640         double q3 = random.nextDouble() * 2 - 1;
641         double q  = FastMath.sqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3);
642         return new Rotation(q0 / q, q1 / q, q2 / q, q3 / q, false);
643     }
644 
645 }
646