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