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