1   /* Copyright 2002-2022 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.propagation.semianalytical.dsst.utilities;
18  
19  import java.util.Map;
20  import java.util.TreeMap;
21  
22  import org.hipparchus.Field;
23  import org.hipparchus.CalculusFieldElement;
24  import org.hipparchus.analysis.polynomials.PolynomialFunction;
25  import org.hipparchus.analysis.polynomials.PolynomialsUtils;
26  import org.hipparchus.complex.Complex;
27  import org.hipparchus.exception.NullArgumentException;
28  import org.hipparchus.random.MersenneTwister;
29  import org.hipparchus.util.CombinatoricsUtils;
30  import org.hipparchus.util.Decimal64Field;
31  import org.hipparchus.util.FastMath;
32  import org.hipparchus.util.MathArrays;
33  import org.junit.Assert;
34  import org.junit.Test;
35  import org.orekit.errors.OrekitException;
36  import org.orekit.propagation.semianalytical.dsst.utilities.CoefficientsFactory.NSKey;
37  
38  public class CoefficientFactoryTest {
39  
40      private static final double eps0  = 0.;
41      private static final double eps10 = 1e-10;
42      private static final double eps12 = 1e-12;
43  
44      /** Map of the Qns derivatives, for each (n, s) couple. */
45      private static Map<NSKey, PolynomialFunction> QNS_MAP = new TreeMap<NSKey, PolynomialFunction>();
46  
47      @Test
48      public void testVns() {
49          final int order = 100;
50          TreeMap<NSKey, Double> Vns = CoefficientsFactory.computeVns(order);
51  
52          // Odd terms are null
53          for (int i = 0; i < order; i++) {
54              for (int j = 0; j < i + 1; j++) {
55                  if ((i - j) % 2 != 0) {
56                      Assert.assertEquals(0d, Vns.get(new NSKey(i, j)), eps0);
57                  }
58              }
59          }
60  
61          // Check the first coefficients :
62          Assert.assertEquals(1, Vns.get(new NSKey(0, 0)), eps0);
63          Assert.assertEquals(0.5, Vns.get(new NSKey(1, 1)), eps0);
64          Assert.assertEquals(-0.5, Vns.get(new NSKey(2, 0)), eps0);
65          Assert.assertEquals(1 / 8d, Vns.get(new NSKey(2, 2)), eps0);
66          Assert.assertEquals(-1 / 8d, Vns.get(new NSKey(3, 1)), eps0);
67          Assert.assertEquals(1 / 48d, Vns.get(new NSKey(3, 3)), eps0);
68          Assert.assertEquals(3 / 8d, Vns.get(new NSKey(4, 0)), eps0);
69          Assert.assertEquals(-1 / 48d, Vns.get(new NSKey(4, 2)), eps0);
70          Assert.assertEquals(1 / 384d, Vns.get(new NSKey(4, 4)), eps0);
71          Assert.assertEquals(1 / 16d, Vns.get(new NSKey(5, 1)), eps0);
72          Assert.assertEquals(-1 / 384d, Vns.get(new NSKey(5, 3)), eps0);
73          Assert.assertEquals(1 / 3840d, Vns.get(new NSKey(5, 5)), eps0);
74          Assert.assertEquals(Vns.lastKey().getN(), order - 1);
75          Assert.assertEquals(Vns.lastKey().getS(), order - 1);
76      }
77  
78      /**
79       * Test the direct computation method : the getVmns is using the Vns computation to compute the
80       * current element
81       */
82      @Test
83      public void testVmns() {
84          Assert.assertEquals(getVmns2(0, 0, 0), CoefficientsFactory.getVmns(0, 0, 0), eps0);
85          Assert.assertEquals(getVmns2(0, 1, 1), CoefficientsFactory.getVmns(0, 1, 1), eps0);
86          Assert.assertEquals(getVmns2(0, 2, 2), CoefficientsFactory.getVmns(0, 2, 2), eps0);
87          Assert.assertEquals(getVmns2(0, 3, 1), CoefficientsFactory.getVmns(0, 3, 1), eps0);
88          Assert.assertEquals(getVmns2(0, 3, 3), CoefficientsFactory.getVmns(0, 3, 3), eps0);
89          Assert.assertEquals(getVmns2(2, 2, 2), CoefficientsFactory.getVmns(2, 2, 2), eps0);
90          final double vmnsp = getVmns2(12, 26, 20);
91          Assert.assertEquals(vmnsp,
92                              CoefficientsFactory.getVmns(12, 26, 20),
93                              FastMath.abs(eps12 * vmnsp));
94          final double vmnsm = getVmns2(12, 27, -21);
95          Assert.assertEquals(vmnsm,
96                              CoefficientsFactory.getVmns(12, 27, -21),
97                              Math.abs(eps12 * vmnsm));
98      }
99  
100     /** Error if m > n */
101     @Test(expected = OrekitException.class)
102     public void testVmnsError() {
103         // if m > n
104         CoefficientsFactory.getVmns(3, 2, 1);
105     }
106 
107     @Test
108     public void testKey() {
109         // test cases mostly written to improve coverage and make SonarQube happy...
110         NSKey key21 = new NSKey(2, 1);
111         Assert.assertEquals(key21, key21);
112         Assert.assertEquals(key21, new NSKey(2, 1));
113         Assert.assertNotEquals(key21, null);
114         Assert.assertNotEquals(key21, new NSKey(2, 0));
115         Assert.assertNotEquals(key21, new NSKey(3, 1));
116         Assert.assertEquals(-1719365209, key21.hashCode());
117         Assert.assertEquals(-1719365465, new NSKey(3, 1).hashCode());
118     }
119 
120     /**
121      * Qns test based on two computation method. As methods are independent, if they give the same
122      * results, we assume them to be consistent.
123      */
124     @Test
125     public void testQns() {
126         Assert.assertEquals(1., getQnsPolynomialValue(0, 0, 0), 0.);
127         // Method comparison :
128         final int nmax = 10;
129         final int smax = 10;
130         final MersenneTwister random = new MersenneTwister(123456789);
131         for (int g = 0; g < 1000; g++) {
132             final double gamma = random.nextDouble();
133             double[][] qns = CoefficientsFactory.computeQns(gamma, nmax, smax);
134             for (int n = 0; n <= nmax; n++) {
135                 final int sdim = FastMath.min(smax + 2, n);
136                 for (int s = 0; s <= sdim; s++) {
137                     final double qp = getQnsPolynomialValue(gamma, n, s);
138                     Assert.assertEquals(qns[n][s], qp, FastMath.abs(eps10 * qns[n][s]));
139                 }
140             }
141         }
142     }
143 
144     @Test
145     public void testQnsField() {
146         doTestQnsField(Decimal64Field.getInstance());
147     }
148 
149     /**
150      * Qns test based on two computation method. As methods are independent, if they give the same
151      * results, we assume them to be consistent.
152      */
153     private <T extends CalculusFieldElement<T>> void doTestQnsField(Field<T> field) {
154         final T zero = field.getZero();
155         Assert.assertEquals(1., getQnsPolynomialValue(0, 0, 0), 0.);
156         // Method comparison :
157         final int nmax = 10;
158         final int smax = 10;
159         final MersenneTwister random = new MersenneTwister(123456789);
160         for (int g = 0; g < 1000; g++) {
161             final T gamma = zero.add(random.nextDouble());
162             T[][] qns = CoefficientsFactory.computeQns(gamma, nmax, smax);
163             for (int n = 0; n <= nmax; n++) {
164                 final int sdim = FastMath.min(smax + 2, n);
165                 for (int s = 0; s <= sdim; s++) {
166                     final T qp = getQnsPolynomialValue(gamma, n, s);
167                     Assert.assertEquals(qns[n][s].getReal(), qp.getReal(), FastMath.abs(qns[n][s].multiply(eps10)).getReal());
168                 }
169             }
170         }
171     }
172 
173     /** Gs and Hs computation test based on 2 independent methods.
174      *  If they give same results, we assume them to be consistent.
175      */
176     @Test
177     public void testGsHs() {
178         final int s = 50;
179         final MersenneTwister random = new MersenneTwister(123456789);
180         for (int i = 0; i < 10; i++) {
181             final double k = random.nextDouble();
182             final double h = random.nextDouble();
183             final double a = random.nextDouble();
184             final double b = random.nextDouble();
185             final double[][] GH = CoefficientsFactory.computeGsHs(k, h, a, b, s);
186             for (int j = 1; j < s; j++) {
187                 final double[] GsHs = getGsHs(k, h, a, b, j);
188                 Assert.assertEquals(GsHs[0], GH[0][j], FastMath.abs(eps12 * GsHs[0]));
189                 Assert.assertEquals(GsHs[1], GH[1][j], FastMath.abs(eps12 * GsHs[1]));
190             }
191         }
192     }
193 
194     @Test
195     public void testGsHsField() {
196         doTestGsHsField(Decimal64Field.getInstance());
197     }
198 
199     /** Gs and Hs computation test based on 2 independent methods.
200      *  If they give same results, we assume them to be consistent.
201      */
202     private <T extends CalculusFieldElement<T>> void doTestGsHsField(Field<T> field) {
203         final T zero = field.getZero();
204         final int s = 50;
205         final MersenneTwister random = new MersenneTwister(123456789);
206         for (int i = 0; i < 10; i++) {
207             final T k = zero.add(random.nextDouble());
208             final T h = zero.add(random.nextDouble());
209             final T a = zero.add(random.nextDouble());
210             final T b = zero.add(random.nextDouble());
211             final T[][] GH = CoefficientsFactory.computeGsHs(k, h, a, b, s, field);
212             for (int j = 1; j < s; j++) {
213                 final T[] GsHs = getGsHs(k, h, a, b, j, field);
214                 Assert.assertEquals(GsHs[0].getReal(), GH[0][j].getReal(), FastMath.abs(GsHs[0].multiply(eps12)).getReal());
215                 Assert.assertEquals(GsHs[1].getReal(), GH[1][j].getReal(), FastMath.abs(GsHs[1].multiply(eps12)).getReal());
216             }
217         }
218     }
219 
220     /**
221      * Direct computation for the Vmns coefficient from equation 2.7.1 - (6)
222      */
223     private static double getVmns2(final int m,
224                                    final int n,
225                                    final int s) {
226         double vmsn = 0d;
227         if ((n - s) % 2 == 0) {
228             final int coef = (s > 0 || s % 2 == 0) ? 1 : -1;
229             final int ss = (s > 0) ? s : -s;
230             final double num = FastMath.pow(-1, (n - ss) / 2) *
231                                CombinatoricsUtils.factorialDouble(n + ss) *
232                                CombinatoricsUtils.factorialDouble(n - ss);
233             final double den = FastMath.pow(2, n) *
234                                CombinatoricsUtils.factorialDouble(n - m) *
235                                CombinatoricsUtils.factorialDouble((n + ss) / 2) *
236                                CombinatoricsUtils.factorialDouble((n - ss) / 2);
237             vmsn = coef * num / den;
238         }
239         return vmsn;
240     }
241 
242     /** Get the Q<sub>ns</sub> value from 2.8.1-(4) evaluated in γ This method is using the
243      * Legendre polynomial to compute the Q<sub>ns</sub>'s one. This direct computation method
244      * allows to store the polynomials value in a static map. If the Q<sub>ns</sub> had been
245      * computed already, they just will be evaluated at γ
246      *
247      * @param gamma γ angle for which Q<sub>ns</sub> is evaluated
248      * @param n n value
249      * @param s s value
250      * @return the polynomial value evaluated at γ
251      */
252     private static double getQnsPolynomialValue(final double gamma, final int n, final int s) {
253         PolynomialFunction derivative;
254         if (QNS_MAP.containsKey(new NSKey(n, s))) {
255             derivative = QNS_MAP.get(new NSKey(n, s));
256         } else {
257             final PolynomialFunction legendre = PolynomialsUtils.createLegendrePolynomial(n);
258             derivative = legendre;
259             for (int i = 0; i < s; i++) {
260                 derivative = (PolynomialFunction) derivative.polynomialDerivative();
261             }
262             QNS_MAP.put(new NSKey(n, s), derivative);
263         }
264         return derivative.value(gamma);
265     }
266 
267     /** Get the Q<sub>ns</sub> value from 2.8.1-(4) evaluated in γ This method is using the
268      * Legendre polynomial to compute the Q<sub>ns</sub>'s one. This direct computation method
269      * allows to store the polynomials value in a static map. If the Q<sub>ns</sub> had been
270      * computed already, they just will be evaluated at γ
271      *
272      * @param gamma γ angle for which Q<sub>ns</sub> is evaluated
273      * @param n n value
274      * @param s s value
275      * @return the polynomial value evaluated at γ
276      */
277     private static <T extends CalculusFieldElement<T>> T getQnsPolynomialValue(final T gamma, final int n, final int s) {
278         PolynomialFunction derivative;
279         if (QNS_MAP.containsKey(new NSKey(n, s))) {
280             derivative = QNS_MAP.get(new NSKey(n, s));
281         } else {
282             final PolynomialFunction legendre = PolynomialsUtils.createLegendrePolynomial(n);
283             derivative = legendre;
284             for (int i = 0; i < s; i++) {
285                 derivative = (PolynomialFunction) derivative.polynomialDerivative();
286             }
287             QNS_MAP.put(new NSKey(n, s), derivative);
288         }
289         return derivative.value(gamma);
290     }
291 
292     /** Compute directly G<sub>s</sub> and H<sub>s</sub> coefficients from equation 3.1-(4).
293      * @param k x-component of the eccentricity vector
294      * @param h y-component of the eccentricity vector
295      * @param a 1st direction cosine
296      * @param b 2nd direction cosine
297      * @param s development order
298      * @return Array of G<sub>s</sub> and H<sub>s</sub> values for s.<br>
299      *         The 1st element contains the G<sub>s</sub> value.
300      *         The 2nd element contains the H<sub>s</sub> value.
301      */
302     private static double[] getGsHs(final double k, final double h,
303                                     final double a, final double b, final int s) {
304         final Complex as   = new Complex(k, h).pow(s);
305         final Complex bs   = new Complex(a, -b).pow(s);
306         final Complex asbs = as.multiply(bs);
307         return new double[] {asbs.getReal(), asbs.getImaginary()};
308     }
309 
310     /** Compute directly G<sub>s</sub> and H<sub>s</sub> coefficients from equation 3.1-(4).
311      * @param k x-component of the eccentricity vector
312      * @param h y-component of the eccentricity vector
313      * @param a 1st direction cosine
314      * @param b 2nd direction cosine
315      * @param s development order
316      * @return Array of G<sub>s</sub> and H<sub>s</sub> values for s.<br>
317      *         The 1st element contains the G<sub>s</sub> value.
318      *         The 2nd element contains the H<sub>s</sub> value.
319      */
320     private static <T extends CalculusFieldElement<T>> T[] getGsHs(final T k, final T h,
321                                     final T a, final T b, final int s,
322                                     final Field<T> field) {
323         final FieldComplex<T> as   = new FieldComplex<>(k, h).pow(s);
324         final FieldComplex<T> bs   = new FieldComplex<>(a, b.negate()).pow(s);
325         final FieldComplex<T> asbs = as.multiply(bs);
326         final T[] values = MathArrays.buildArray(field, 2);
327         values[0] = asbs.getReal();
328         values[1] = asbs.getImaginary();
329         return values;
330     }
331 
332     private static class FieldComplex <T extends CalculusFieldElement<T>> {
333 
334         /** The imaginary part. */
335         private final T imaginary;
336 
337         /** The real part. */
338         private final T real;
339 
340         /**
341          * Create a complex number given the real and imaginary parts.
342          *
343          * @param real Real part.
344          * @param imaginary Imaginary part.
345          */
346         FieldComplex(final T real, final T imaginary) {
347             this.real = real;
348             this.imaginary = imaginary;
349         }
350 
351         /**
352          * Access the real part.
353          *
354          * @return the real part.
355          */
356         public T getReal() {
357             return real;
358         }
359 
360         /**
361          * Access the imaginary part.
362          *
363          * @return the imaginary part.
364          */
365         public T getImaginary() {
366             return imaginary;
367         }
368 
369         /**
370          * Create a complex number given the real and imaginary parts.
371          *
372          * @param realPart Real part.
373          * @param imaginaryPart Imaginary part.
374          * @return a new complex number instance.
375          *
376          * @see #valueOf(double, double)
377          */
378         protected FieldComplex<T> createComplex(final T realPart, final T imaginaryPart) {
379             return new FieldComplex<>(realPart, imaginaryPart);
380         }
381 
382         /**
383          * Returns a {@code Complex} whose value is {@code this * factor}.
384          * Implements preliminary checks for {@code NaN} and infinity followed by
385          * the definitional formula:
386          * <p>
387          *   {@code (a + bi)(c + di) = (ac - bd) + (ad + bc)i}
388          * </p>
389          * <p>
390          * Returns finite values in components of the result per the definitional
391          * formula in all remaining cases.</p>
392          *
393          * @param  factor value to be multiplied by this {@code Complex}.
394          * @return {@code this * factor}.
395          * @throws NullArgumentException if {@code factor} is {@code null}.
396          */
397         public FieldComplex<T> multiply(final FieldComplex<T> factor) throws NullArgumentException {
398             return createComplex(real.multiply(factor.real).subtract(imaginary.multiply(factor.imaginary)),
399                                  real.multiply(factor.imaginary).add(imaginary.multiply(factor.real)));
400         }
401 
402         /**
403          * Returns a {@code Complex} whose value is {@code this * factor}, with {@code factor}
404          * interpreted as a integer number.
405          *
406          * @param  factor value to be multiplied by this {@code Complex}.
407          * @return {@code this * factor}.
408          * @see #multiply(Complex)
409          */
410         public FieldComplex<T> multiply(final int factor) {
411             return createComplex(real.multiply(factor), imaginary.multiply(factor));
412         }
413 
414         /**
415          * Returns of value of this complex number raised to the power of {@code x}.
416          *
417          * @param  x exponent to which this {@code Complex} is to be raised.
418          * @return <code>this<sup>x</sup></code>.
419          * @see #pow(Complex)
420          */
421          public FieldComplex<T> pow(int x) {
422             return this.log().multiply(x).exp();
423         }
424 
425          /**
426           * Compute the
427           * <a href="http://mathworld.wolfram.com/NaturalLogarithm.html" TARGET="_top">
428           * natural logarithm</a> of this complex number.
429           * Implements the formula:
430           * <pre>
431           *  <code>
432           *   log(a + bi) = ln(|a + bi|) + arg(a + bi)i
433           *  </code>
434           * </pre>
435           * where ln on the right hand side is {@link FastMath#log},
436           * {@code |a + bi|} is the modulus, {@link Complex#abs},  and
437           * {@code arg(a + bi) = }{@link FastMath#atan2}(b, a).
438           * <p>
439           * Returns {@link Complex#NaN} if either real or imaginary part of the
440           * input argument is {@code NaN}.
441           * </p>
442           * Infinite (or critical) values in real or imaginary parts of the input may
443           * result in infinite or NaN values returned in parts of the result.
444           * <pre>
445           *  Examples:
446           *  <code>
447           *   log(1 &plusmn; INFINITY i) = INFINITY &plusmn; (&pi;/2)i
448           *   log(INFINITY + i) = INFINITY + 0i
449           *   log(-INFINITY + i) = INFINITY + &pi;i
450           *   log(INFINITY &plusmn; INFINITY i) = INFINITY &plusmn; (&pi;/4)i
451           *   log(-INFINITY &plusmn; INFINITY i) = INFINITY &plusmn; (3&pi;/4)i
452           *   log(0 + 0i) = -INFINITY + 0i
453           *  </code>
454           * </pre>
455           *
456           * @return the value <code>ln &nbsp; this</code>, the natural logarithm
457           * of {@code this}.
458           */
459          public FieldComplex<T> log() {
460              return createComplex(FastMath.log(abs()),
461                                   FastMath.atan2(imaginary, real));
462          }
463 
464          /**
465           * Compute the
466           * <a href="http://mathworld.wolfram.com/ExponentialFunction.html" TARGET="_top">
467           * exponential function</a> of this complex number.
468           * Implements the formula:
469           * <pre>
470           *  <code>
471           *   exp(a + bi) = exp(a)cos(b) + exp(a)sin(b)i
472           *  </code>
473           * </pre>
474           * where the (real) functions on the right-hand side are
475           * {@link FastMath#exp}, {@link FastMath#cos}, and
476           * {@link FastMath#sin}.
477           * <p>
478           * Returns {@link Complex#NaN} if either real or imaginary part of the
479           * input argument is {@code NaN}.
480           * </p>
481           * Infinite values in real or imaginary parts of the input may result in
482           * infinite or NaN values returned in parts of the result.
483           * <pre>
484           *  Examples:
485           *  <code>
486           *   exp(1 &plusmn; INFINITY i) = NaN + NaN i
487           *   exp(INFINITY + i) = INFINITY + INFINITY i
488           *   exp(-INFINITY + i) = 0 + 0i
489           *   exp(&plusmn;INFINITY &plusmn; INFINITY i) = NaN + NaN i
490           *  </code>
491           * </pre>
492           *
493           * @return <code><i>e</i><sup>this</sup></code>.
494           */
495          public FieldComplex<T> exp() {
496              T expReal = FastMath.exp(real);
497              return createComplex(expReal.multiply(FastMath.cos(imaginary)),
498                                   expReal.multiply(FastMath.sin(imaginary)));
499          }
500 
501          /**
502           * Return the absolute value of this complex number.
503           * Returns {@code NaN} if either real or imaginary part is {@code NaN}
504           * and {@code Double.POSITIVE_INFINITY} if neither part is {@code NaN},
505           * but at least one part is infinite.
506           *
507           * @return the absolute value.
508           */
509          public T abs() {
510              if (FastMath.abs(real).getReal() < FastMath.abs(imaginary).getReal()) {
511                  if (imaginary.getReal() == 0.0) {
512                      return FastMath.abs(real);
513                  }
514                  T q = real.divide(imaginary);
515                  return FastMath.abs(imaginary).multiply(FastMath.sqrt(q.multiply(q).add(1.)));
516              } else {
517                  if (real.getReal() == 0.0) {
518                      return FastMath.abs(imaginary);
519                  }
520                  T q = imaginary.divide(real);
521                  return FastMath.abs(real).multiply(FastMath.sqrt(q.multiply(q).add(1.)));
522              }
523          }
524 
525     }
526 }