View Javadoc
1   /*
2    * Licensed to the Hipparchus project under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The Hipparchus project 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    *      https://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.hipparchus.ode;
18  
19  import java.util.Arrays;
20  
21  import org.hipparchus.geometry.euclidean.threed.Rotation;
22  import org.hipparchus.geometry.euclidean.threed.RotationConvention;
23  import org.hipparchus.geometry.euclidean.threed.RotationOrder;
24  import org.hipparchus.geometry.euclidean.threed.Vector3D;
25  import org.hipparchus.linear.DecompositionSolver;
26  import org.hipparchus.linear.MatrixUtils;
27  import org.hipparchus.linear.QRDecomposer;
28  import org.hipparchus.linear.RealMatrix;
29  import org.hipparchus.linear.RealVector;
30  import org.hipparchus.ode.nonstiff.DormandPrince853Integrator;
31  import org.hipparchus.special.elliptic.jacobi.CopolarN;
32  import org.hipparchus.special.elliptic.jacobi.JacobiElliptic;
33  import org.hipparchus.special.elliptic.jacobi.JacobiEllipticBuilder;
34  import org.hipparchus.special.elliptic.legendre.LegendreEllipticIntegral;
35  import org.hipparchus.util.FastMath;
36  
37  /**
38   * This class is used in the junit tests for the ODE integrators.
39  
40   * <p>This specific problem correspond to torque-free motion of a solid body
41   * with moments of inertia I₁, I₂, and I₃ with respect to body axes x, y, and z.
42   * We use here the notations from Landau and Lifchitz Course of Theoretical Physics,
43   * Mechanics vol 1.
44   * </p>
45   * <p>
46   * The equations of torque-free motion are given in the solid body frame by
47   * equation 36.5:
48   * <pre>
49   *    I₁ dΩ₁/dt + (I₃ - I₂) Ω₂ Ω₃ = 0
50   *    I₂ dΩ₂/dt + (I₁ - I₃) Ω₃ Ω₁ = 0
51   *    I₃ dΩ₃/dt + (I₂ - I₁) Ω₁ Ω₂ = 0
52   * </pre>
53   * <p>
54   * This problem solves the full motion (rotation rate and rotation), whereas
55   * {@code TestProblem7} only solves the rotation rate part.
56   * </p>
57   */
58  public class TestProblem8 extends TestProblemAbstract {
59  
60      /** Inertia tensor. */
61      final RealMatrix inertiaTensor;
62  
63      /** Solver for inertia tensor. */
64      final DecompositionSolver inertiaSolver;
65  
66      /** Inertia sorted to get a motion about axis 3. */
67      final Inertia sortedInertia;
68  
69      /** State scaling factor. */
70      final double o1Scale;
71  
72      /** State scaling factor. */
73      final double o2Scale;
74  
75      /** State scaling factor. */
76      final double o3Scale;
77  
78      /** Jacobi elliptic function. */
79      final JacobiElliptic jacobi;
80  
81      /** Time scaling factor. */
82      public final double tScale;
83  
84      /** Time reference for rotation rate. */
85      final double tRef;
86  
87      /** Offset rotation  between initial inertial frame and the frame with moment vector and Z axis aligned. */
88      Rotation inertToAligned;
89  
90      /** Rotation to switch to the converted axes frame. */
91      final Rotation sortedToBody;
92  
93      /** Period of rotation rate. */
94      final double period;
95  
96      /** Slope of the linear part of the phi model. */
97      final double phiSlope;
98  
99      /** DenseOutputModel of phi. */
100     final DenseOutputModel phiQuadratureModel;
101 
102     /** Integral part of quadrature model over one period. */
103     final double integOnePeriod;
104 
105     /**
106      * Simple constructor.
107      * @param t0 initial time
108      * @param t1 final time
109      * @param omega0 initial rotation rate
110      * @param r0 initial rotation
111      * @param i1 inertia along first axis
112      * @param a1 first principal inertia axis
113      * @param i2 inertia along second axis
114      * @param a2 second principal inertia axis
115      * @param i3 inertia along third axis
116      * @param a3 third principal inertia axis
117      */
118     public TestProblem8(final double t0, final double t1, final Vector3D omega0, final Rotation r0,
119                         final double i1, final Vector3D a1,
120                         final double i2, final Vector3D a2,
121                         final double i3, final Vector3D a3) {
122         // Arguments in the super constructor :
123         // Initial time, Primary state (o1, o2, o3, q0, q1, q2, q3), Final time, Error scale
124         super(t0,
125               new double[] {
126                   omega0.getX(), omega0.getY(), omega0.getZ(),
127                   r0.getQ0(), r0.getQ1(), r0.getQ2(), r0.getQ3()
128               },
129               t1,
130               new double[] { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 });
131 
132         // build inertia tensor
133         final Vector3D n1  = a1.normalize();
134         final Vector3D n2  = a2.normalize();
135         final Vector3D n3  = (Vector3D.dotProduct(Vector3D.crossProduct(a1, a2), a3) > 0 ?
136                               a3.normalize() : a3.normalize().negate());
137         final RealMatrix q = MatrixUtils.createRealMatrix(3, 3);
138         q.setEntry(0, 0, n1.getX());
139         q.setEntry(0, 1, n1.getY());
140         q.setEntry(0, 2, n1.getZ());
141         q.setEntry(1, 0, n2.getX());
142         q.setEntry(1, 1, n2.getY());
143         q.setEntry(1, 2, n2.getZ());
144         q.setEntry(2, 0, n3.getX());
145         q.setEntry(2, 1, n3.getY());
146         q.setEntry(2, 2, n3.getZ());
147         final RealMatrix d = MatrixUtils.createRealDiagonalMatrix(new double[] { i1, i2, i3});
148         this.inertiaTensor = q.multiply(d.multiplyTransposed(q));
149         this.inertiaSolver = new QRDecomposer(1.0e-10).decompose(inertiaTensor);
150 
151         final Vector3D m0 = new Vector3D(i1 * Vector3D.dotProduct(omega0, n1), n1,
152                                          i2 * Vector3D.dotProduct(omega0, n2), n2,
153                                          i3 * Vector3D.dotProduct(omega0, n3), n3);
154 
155         // sort axes in increasing moments of inertia order
156         Inertia inertia = new Inertia(new InertiaAxis(i1, n1), new InertiaAxis(i2, n2), new InertiaAxis(i3, n3));
157         if (inertia.getInertiaAxis1().getI() > inertia.getInertiaAxis2().getI()) {
158             inertia = inertia.swap12();
159         }
160         if (inertia.getInertiaAxis2().getI() > inertia.getInertiaAxis3().getI()) {
161             inertia = inertia.swap23();
162         }
163         if (inertia.getInertiaAxis1().getI() > inertia.getInertiaAxis2().getI()) {
164             inertia = inertia.swap12();
165         }
166 
167         // in order to simplify implementation, we want the motion to be about axis 3
168         // which is either the minimum or the maximum inertia axis
169         final double  o1                = Vector3D.dotProduct(omega0, n1);
170         final double  o2                = Vector3D.dotProduct(omega0, n2);
171         final double  o3                = Vector3D.dotProduct(omega0, n3);
172         final double  o12               = o1 * o1;
173         final double  o22               = o2 * o2;
174         final double  o32               = o3 * o3;
175         final double  twoE              = i1 * o12 + i2 * o22 + i3 * o32;
176         final double  m2                = i1 * i1 * o12 + i2 * i2 * o22 + i3 * i3 * o32;
177         final double  separatrixInertia = (twoE == 0) ? 0.0 : m2 / twoE;
178         final boolean clockwise;
179         if (separatrixInertia < inertia.getInertiaAxis2().getI()) {
180             // motion is about minimum inertia axis
181             // we swap axes to put them in decreasing moments order
182             // motion will be clockwise about axis 3
183             clockwise = true;
184             inertia   = inertia.swap13();
185         } else {
186             // motion is about maximum inertia axis
187             // we keep axes in increasing moments order
188             // motion will be counter-clockwise about axis 3
189             clockwise = false;
190         }
191         sortedInertia = inertia;
192 
193         final double i1C = inertia.getInertiaAxis1().getI();
194         final double i2C = inertia.getInertiaAxis2().getI();
195         final double i3C = inertia.getInertiaAxis3().getI();
196         final double i32 = i3C - i2C;
197         final double i31 = i3C - i1C;
198         final double i21 = i2C - i1C;
199 
200         // convert initial conditions to Euler angles such the M is aligned with Z in aligned computation frame
201         sortedToBody   = new Rotation(Vector3D.PLUS_I, Vector3D.PLUS_J, inertia.getInertiaAxis1().getA(), inertia.getInertiaAxis2().getA());
202         final Vector3D omega0Sorted = sortedToBody.applyInverseTo(omega0);
203         final Vector3D m0Sorted     = sortedToBody.applyInverseTo(m0);
204         final double   phi0         = 0; // this angle can be set arbitrarily, so 0 is a fair value (Eq. 37.13 - 37.14)
205         final double   theta0       = FastMath.acos(m0Sorted.getZ() / m0Sorted.getNorm());
206         final double   psi0         = FastMath.atan2(m0Sorted.getX(), m0Sorted.getY()); // it is really atan2(x, y), not atan2(y, x) as usual!
207 
208         // compute offset rotation between inertial frame aligned with momentum and regular inertial frame
209         final Rotation alignedToSorted0 = new Rotation(RotationOrder.ZXZ, RotationConvention.FRAME_TRANSFORM,
210                                                        phi0, theta0, psi0);
211         inertToAligned = alignedToSorted0.applyInverseTo(sortedToBody.applyInverseTo(r0));
212 
213         // Ω is always o1Scale * cn((t-tref) * tScale), o2Scale * sn((t-tref) * tScale), o3Scale * dn((t-tref) * tScale)
214         tScale  = FastMath.copySign(FastMath.sqrt(i32 * (m2 - twoE * i1C) / (i1C * i2C * i3C)),
215                                     clockwise ? -omega0Sorted.getZ() : omega0Sorted.getZ());
216         o1Scale = FastMath.sqrt((twoE * i3C - m2) / (i1C * i31));
217         o2Scale = FastMath.sqrt((twoE * i3C - m2) / (i2C * i32));
218         o3Scale = FastMath.copySign(FastMath.sqrt((m2 - twoE * i1C) / (i3C * i31)), omega0Sorted.getZ());
219 
220         final double k2 = (twoE == 0) ? 0.0 : i21 * (twoE * i3C - m2) / (i32 * (m2 - twoE * i1C));
221         jacobi = JacobiEllipticBuilder.build(k2);
222         period = 4 * LegendreEllipticIntegral.bigK(k2) / tScale;
223 
224         if (o1Scale == 0) {
225             // special case where twoE * i3C = m2, then o2Scale is also zero
226             // motion is exactly along one axis
227             tRef = t0;
228         } else {
229             if (FastMath.abs(omega0Sorted.getX()) >= FastMath.abs(omega0Sorted.getY())) {
230                 if (omega0Sorted.getX() >= 0) {
231                     // omega is roughly towards +I
232                     tRef = t0 - jacobi.arcsn(omega0Sorted.getY() / o2Scale) / tScale;
233                 } else {
234                     // omega is roughly towards -I
235                     tRef = t0 + jacobi.arcsn(omega0Sorted.getY() / o2Scale) / tScale - 0.5 * period;
236                 }
237             } else {
238                 if (omega0Sorted.getY() >= 0) {
239                     // omega is roughly towards +J
240                     tRef = t0 - jacobi.arccn(omega0Sorted.getX() / o1Scale) / tScale;
241                 } else {
242                     // omega is roughly towards -J
243                     tRef = t0 + jacobi.arccn(omega0Sorted.getX() / o1Scale) / tScale;
244                 }
245             }
246         }
247 
248         phiSlope           = FastMath.sqrt(m2) / i3C;
249         phiQuadratureModel = computePhiQuadratureModel(t0);
250         integOnePeriod     = phiQuadratureModel.getInterpolatedState(phiQuadratureModel.getFinalTime()).getPrimaryState()[0];
251 
252     }
253 
254     private DenseOutputModel computePhiQuadratureModel(final double t0) {
255 
256         final double i1C = sortedInertia.getInertiaAxis1().getI();
257         final double i2C = sortedInertia.getInertiaAxis2().getI();
258         final double i3C = sortedInertia.getInertiaAxis3().getI();
259 
260         final double i32 = i3C - i2C;
261         final double i31 = i3C - i1C;
262         final double i21 = i2C - i1C;
263 
264         // coefficients for φ model
265         final double b = phiSlope * i32 * i31;
266         final double c = i1C * i32;
267         final double d = i3C * i21;
268 
269         // integrate the quadrature phi term on one period
270         final DormandPrince853Integrator integ = new DormandPrince853Integrator(1.0e-6 * period, 1.0e-2 * period,
271                                                                                 phiSlope * period * 1.0e-13, 1.0e-13);
272         final DenseOutputModel model = new DenseOutputModel();
273         integ.addStepHandler(model);
274 
275         integ.integrate(new OrdinaryDifferentialEquation() {
276 
277             /** {@inheritDoc} */
278             @Override
279             public int getDimension() {
280                 return 1;
281             }
282 
283             /** {@inheritDoc} */
284            @Override
285             public double[] computeDerivatives(final double t, final double[] y) {
286                 final double sn = jacobi.valuesN((t - tRef) * tScale).sn();
287                 return new double[] {
288                         b / (c + d * sn * sn)
289                 };
290             }
291 
292         }, new ODEState(t0, new double[1]), t0 + period);
293 
294         return model;
295 
296     }
297 
298     public double[] computeTheoreticalState(double t) {
299 
300         final double t0            = getInitialTime();
301 
302         // angular velocity
303         final CopolarN valuesN     = jacobi.valuesN((t - tRef) * tScale);
304         final Vector3D omegaSorted = new Vector3D(o1Scale * valuesN.cn(), o2Scale * valuesN.sn(), o3Scale * valuesN.dn());
305         final Vector3D omegaBody   = sortedToBody.applyTo(omegaSorted);
306 
307         // first Euler angles are directly linked to angular velocity
308         final double   psi         = FastMath.atan2(sortedInertia.getInertiaAxis1().getI() * omegaSorted.getX(),
309                                                     sortedInertia.getInertiaAxis2().getI() * omegaSorted.getY());
310         final double   theta       = FastMath.acos(omegaSorted.getZ() / phiSlope);
311         final double   phiLinear   = phiSlope * (t - t0);
312 
313         // third Euler angle results from a quadrature
314         final int    nbPeriods     = (int) FastMath.floor((t - t0) / period);
315         final double tStartInteg   = t0 + nbPeriods * period;
316         final double integPartial  = phiQuadratureModel.getInterpolatedState(t - tStartInteg).getPrimaryState()[0];
317         final double phiQuadrature = nbPeriods * integOnePeriod + integPartial;
318         final double phi           = phiLinear + phiQuadrature;
319 
320         // rotation between computation frame (aligned with momentum) and sorted computation frame
321         // (it is simply the angles equations provided by Landau & Lifchitz)
322         final Rotation alignedToSorted = new Rotation(RotationOrder.ZXZ, RotationConvention.FRAME_TRANSFORM,
323                                                       phi, theta, psi);
324 
325         // combine with offset rotation to get back from regular inertial frame to body frame
326         Rotation inertToBody = sortedToBody.applyTo(alignedToSorted.applyTo(inertToAligned));
327 
328         return new double[] {
329             omegaBody.getX(), omegaBody.getY(), omegaBody.getZ(),
330             inertToBody.getQ0(), inertToBody.getQ1(), inertToBody.getQ2(), inertToBody.getQ3()
331         };
332 
333     }
334 
335     public double[] doComputeDerivatives(double t, double[] y) {
336 
337         final  double[] yDot = new double[getDimension()];
338 
339         // compute the derivatives using Euler equations
340         final double[]   omega    = Arrays.copyOfRange(y, 0, 3);
341         final double[]   minusOiO = Vector3D.crossProduct(new Vector3D(omega),
342                                                           new Vector3D(inertiaTensor.operate(omega))).negate().toArray();
343         final RealVector omegaDot = inertiaSolver.solve(MatrixUtils.createRealVector(minusOiO));
344         yDot[0] = omegaDot.getEntry(0);
345         yDot[1] = omegaDot.getEntry(1);
346         yDot[2] = omegaDot.getEntry(2);
347 
348         // compute the derivatives using Qdot = 0.5 * Omega_inertialframe * Q
349         yDot[3] = 0.5 * (-y[0] * y[4] - y[1] * y[5] - y[2] * y[6]);
350         yDot[4] = 0.5 * ( y[0] * y[3] + y[2] * y[5] - y[1] * y[6]);
351         yDot[5] = 0.5 * ( y[1] * y[3] - y[2] * y[4] + y[0] * y[6]);
352         yDot[6] = 0.5 * ( y[2] * y[3] + y[1] * y[4] - y[0] * y[5]);
353 
354         return yDot;
355 
356     }
357 
358     /** Container for inertia of a 3D object.
359      * <p>
360      * Instances of this class are immutable
361      * </p>
362     */
363    public static class Inertia {
364 
365        /** Inertia along first axis. */
366        private final InertiaAxis iA1;
367 
368        /** Inertia along second axis. */
369        private final InertiaAxis iA2;
370 
371        /** Inertia along third axis. */
372        private final InertiaAxis iA3;
373 
374        /** Simple constructor from principal axes.
375         * @param iA1 inertia along first axis
376         * @param iA2 inertia along second axis
377         * @param iA3 inertia along third axis
378         */
379        public Inertia(final InertiaAxis iA1, final InertiaAxis iA2, final InertiaAxis iA3) {
380            this.iA1 = iA1;
381            this.iA2 = iA2;
382            this.iA3 = iA3;
383        }
384 
385        /** Swap axes 1 and 2.
386         * @return inertia with swapped axes
387         */
388        public Inertia swap12() {
389            return new Inertia(iA2, iA1, iA3.negate());
390        }
391 
392        /** Swap axes 1 and 3.
393         * @return inertia with swapped axes
394         */
395        public Inertia swap13() {
396            return new Inertia(iA3, iA2.negate(), iA1);
397        }
398 
399        /** Swap axes 2 and 3.
400         * @return inertia with swapped axes
401         */
402        public Inertia swap23() {
403            return new Inertia(iA1.negate(), iA3, iA2);
404        }
405 
406        /** Get inertia along first axis.
407         * @return inertia along first axis
408         */
409        public InertiaAxis getInertiaAxis1() {
410            return iA1;
411        }
412 
413        /** Get inertia along second axis.
414         * @return inertia along second axis
415         */
416        public InertiaAxis getInertiaAxis2() {
417            return iA2;
418        }
419 
420        /** Get inertia along third axis.
421         * @return inertia along third axis
422         */
423        public InertiaAxis getInertiaAxis3() {
424            return iA3;
425        }
426 
427    }
428 
429    /** Container for moment of inertia and associated inertia axis.
430      * <p>
431      * Instances of this class are immutable
432      * </p>
433      */
434     public static class InertiaAxis {
435 
436         /** Moment of inertia. */
437         private final double i;
438 
439         /** Inertia axis. */
440         private final Vector3D a;
441 
442         /** Simple constructor to pair a moment of inertia with its associated axis.
443          * @param i moment of inertia
444          * @param a inertia axis
445          */
446         public InertiaAxis(final double i, final Vector3D a) {
447             this.i = i;
448             this.a = a;
449         }
450 
451         /** Reverse the inertia axis.
452          * @return new container with reversed axis
453          */
454         public InertiaAxis negate() {
455             return new InertiaAxis(i, a.negate());
456         }
457 
458         /** Get the moment of inertia.
459          * @return moment of inertia
460          */
461         public double getI() {
462             return i;
463         }
464 
465         /** Get the inertia axis.
466          * @return inertia axis
467          */
468         public Vector3D getA() {
469             return a;
470         }
471 
472     }
473 
474 }