1   /* Copyright 2002-2026 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.forces.gravity;
18  
19  
20  import java.util.Collections;
21  import java.util.List;
22  
23  import org.hipparchus.CalculusFieldElement;
24  import org.hipparchus.analysis.differentiation.Gradient;
25  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
26  import org.hipparchus.geometry.euclidean.threed.SphericalCoordinates;
27  import org.hipparchus.geometry.euclidean.threed.Vector3D;
28  import org.hipparchus.linear.Array2DRowRealMatrix;
29  import org.hipparchus.linear.RealMatrix;
30  import org.hipparchus.util.FastMath;
31  import org.hipparchus.util.MathArrays;
32  import org.orekit.forces.ForceModel;
33  import org.orekit.forces.gravity.potential.NormalizedSphericalHarmonicsProvider;
34  import org.orekit.forces.gravity.potential.NormalizedSphericalHarmonicsProvider.NormalizedSphericalHarmonics;
35  import org.orekit.forces.gravity.potential.TideSystem;
36  import org.orekit.forces.gravity.potential.TideSystemProvider;
37  import org.orekit.frames.FieldStaticTransform;
38  import org.orekit.frames.Frame;
39  import org.orekit.frames.StaticTransform;
40  import org.orekit.propagation.FieldSpacecraftState;
41  import org.orekit.propagation.SpacecraftState;
42  import org.orekit.time.AbsoluteDate;
43  import org.orekit.time.FieldAbsoluteDate;
44  import org.orekit.time.TimeInterval;
45  import org.orekit.utils.FieldPVCoordinates;
46  import org.orekit.utils.drivers.ParameterDriver;
47  
48  /** This class represents the gravitational field of a celestial body.
49   * <p>
50   * The algorithm implemented in this class has been designed by S. A. Holmes
51   * and W. E. Featherstone from Department of Spatial Sciences, Curtin University
52   * of Technology, Perth, Australia. It is described in their 2002 paper: <a
53   * href="https://www.researchgate.net/publication/226460594_A_unified_approach_to_the_Clenshaw_summation_and_the_recursive_computation_of_very_high_degree_and_order_normalised_associated_Legendre_functions">
54   * A unified approach to he Clenshaw summation and the recursive computation of
55   * very high degree and order normalised associated Legendre functions</a>
56   * (Journal of Geodesy (2002) 76: 279–299).
57   * </p>
58   * <p>
59   * This model directly uses normalized coefficients and stable recursion algorithms
60   * so it is more suited to high degree gravity fields than the classical Cunningham
61   * Droziner models which use un-normalized coefficients.
62   * </p>
63   * <p>
64   * Among the different algorithms presented in Holmes and Featherstone paper, this
65   * class implements the <em>modified forward row method</em>. All recursion coefficients
66   * are precomputed and stored for greater performance. This caching was suggested in the
67   * paper but not used due to the large memory requirements. Since 2002, even low end
68   * computers and mobile devices do have sufficient memory so this caching has become
69   * feasible nowadays.
70   * </p>
71   * @author Luc Maisonobe
72   * @since 6.0
73   */
74  
75  public class HolmesFeatherstoneAttractionModel implements ForceModel, TideSystemProvider {
76  
77      /** Exponent scaling to avoid floating point overflow.
78       * <p>The paper uses 10^280, we prefer a power of two to preserve accuracy thanks to
79       * {@link FastMath#scalb(double, int)}, so we use 2^930 which has the same order of magnitude.
80       */
81      private static final int SCALING = 930;
82  
83      /** Central attraction scaling factor.
84       * <p>
85       * We use a power of 2 to avoid numeric noise introduction
86       * in the multiplications/divisions sequences.
87       * </p>
88       */
89      private static final double MU_SCALE = FastMath.scalb(1.0, 32);
90  
91      /** Driver for gravitational parameter. */
92      private final ParameterDriver gmParameterDriver;
93  
94      /** Provider for the spherical harmonics. */
95      private final NormalizedSphericalHarmonicsProvider provider;
96  
97      /** Rotating body. */
98      private final Frame bodyFrame;
99  
100     /** Recursion coefficients g<sub>n,m</sub>/√j. */
101     private final double[] gnmOj;
102 
103     /** Recursion coefficients h<sub>n,m</sub>/√j. */
104     private final double[] hnmOj;
105 
106     /** Recursion coefficients e<sub>n,m</sub>. */
107     private final double[] enm;
108 
109     /** Scaled sectorial Pbar<sub>m,m</sub>/u<sup>m</sup> &times; 2<sup>-SCALING</sup>. */
110     private final double[] sectorial;
111 
112     /** Creates a new instance.
113      * @param centralBodyFrame rotating body frame
114      * @param provider provider for spherical harmonics
115      * @since 6.0
116      */
117     public HolmesFeatherstoneAttractionModel(final Frame centralBodyFrame,
118                                              final NormalizedSphericalHarmonicsProvider provider) {
119 
120         gmParameterDriver = new ParameterDriver(NewtonianAttraction.CENTRAL_ATTRACTION_COEFFICIENT,
121                                                 provider.getMu(), MU_SCALE,
122                                                 0.0, Double.POSITIVE_INFINITY, TimeInterval.UNLIMITED);
123 
124         this.provider  = provider;
125         this.bodyFrame = centralBodyFrame;
126 
127         // the pre-computed arrays hold coefficients from triangular arrays in a single
128         // storing neither diagonal elements (n = m) nor the non-diagonal element n=1, m=0
129         final int degree = provider.getMaxDegree();
130         final int size = FastMath.max(0, degree * (degree + 1) / 2 - 1);
131         gnmOj = new double[size];
132         hnmOj = new double[size];
133         enm   = new double[size];
134 
135         // pre-compute the recursion coefficients corresponding to equations 19 and 22
136         // from Holmes and Featherstone paper
137         // for cache efficiency, elements are stored in the same order they will be used
138         // later on, i.e. from rightmost column to leftmost column
139         int index = 0;
140         for (int m = degree; m >= 0; --m) {
141             final int j = (m == 0) ? 2 : 1;
142             for (int n = FastMath.max(2, m + 1); n <= degree; ++n) {
143                 final double f = ((double) (n - m)) * (n + m + 1);
144                 gnmOj[index] = 2 * (m + 1) / FastMath.sqrt(j * f);
145                 hnmOj[index] = FastMath.sqrt((n + m + 2) * (n - m - 1) / (j * f));
146                 enm[index]   = FastMath.sqrt(f / j);
147                 ++index;
148             }
149         }
150 
151         // scaled sectorial terms corresponding to equation 28 in Holmes and Featherstone paper
152         sectorial    = new double[degree + 1];
153         sectorial[0] = FastMath.scalb(1.0, -SCALING);
154         if (degree > 0) {
155             sectorial[1] = FastMath.sqrt(3) * sectorial[0];
156         }
157         for (int m = 2; m < sectorial.length; ++m) {
158             sectorial[m] = FastMath.sqrt((2 * m + 1) / (2.0 * m)) * sectorial[m - 1];
159         }
160 
161     }
162 
163     /** {@inheritDoc} */
164     @Override
165     public boolean dependsOnPositionOnly() {
166         return true;
167     }
168 
169     /** {@inheritDoc} */
170     public TideSystem getTideSystem() {
171         return provider.getTideSystem();
172     }
173 
174     /** Get the central attraction coefficient μ.
175      * @return mu central attraction coefficient (m³/s²),
176      * will throw an exception if gm PDriver has several
177      * values driven (in this case the method
178      * {@link #getMu(AbsoluteDate)} must be used.
179      */
180     public double getMu() {
181         return gmParameterDriver.getValue();
182     }
183 
184     /** Get the central attraction coefficient μ.
185      * @param date date at which mu wants to be known
186      * @return mu central attraction coefficient (m³/s²)
187      */
188     public double getMu(final AbsoluteDate date) {
189         return gmParameterDriver.getValue();
190     }
191 
192     /** Compute the value of the gravity field.
193      * @param date current date
194      * @param position position at which gravity field is desired in body frame
195      * @param mu central attraction coefficient to use
196      * @return value of the gravity field (central and non-central parts summed together)
197      */
198     public double value(final AbsoluteDate date, final Vector3D position,
199                         final double mu) {
200         return mu / position.getNorm() + nonCentralPart(date, position, mu);
201     }
202 
203     /** Compute the non-central part of the gravity field.
204      * @param date current date
205      * @param position position at which gravity field is desired in body frame
206      * @param mu central attraction coefficient to use
207      * @return value of the non-central part of the gravity field
208      */
209     public double nonCentralPart(final AbsoluteDate date, final Vector3D position, final double mu) {
210 
211         final int degree = provider.getMaxDegree();
212         final int order  = provider.getMaxOrder();
213         final NormalizedSphericalHarmonics harmonics = provider.onDate(date);
214 
215         // allocate the columns for recursion
216         double[] pnm0Plus2 = new double[degree + 1];
217         double[] pnm0Plus1 = new double[degree + 1];
218         double[] pnm0      = new double[degree + 1];
219 
220         // compute polar coordinates
221         final double x    = position.getX();
222         final double y    = position.getY();
223         final double z    = position.getZ();
224         final double x2   = x * x;
225         final double y2   = y * y;
226         final double z2   = z * z;
227         final double rho2 = x2 + y2;
228         final double r2   = rho2 + z2;
229         final double r    = FastMath.sqrt(r2);
230         final double rho  = FastMath.sqrt(rho2);
231         final double t    = z / r;   // cos(theta), where theta is the polar angle
232         final double u    = rho / r; // sin(theta), where theta is the polar angle
233         final double tOu  = z / rho;
234 
235         // compute distance powers
236         final double[] aOrN = createDistancePowersArray(provider.getAe() / r);
237 
238         // compute longitude cosines/sines
239         final double[][] cosSinLambda = createCosSinArrays(x / rho, y / rho);
240 
241         // outer summation over order
242         int    index = 0;
243         double value = 0;
244         for (int m = degree; m >= 0; --m) {
245 
246             // compute tesseral terms without derivatives
247             index = computeTesseral(m, degree, index, t, u, tOu,
248                                     pnm0Plus2, pnm0Plus1, null, pnm0, null, null);
249 
250             if (m <= order) {
251                 // compute contribution of current order to field (equation 5 of the paper)
252 
253                 // inner summation over degree, for fixed order
254                 double sumDegreeS        = 0;
255                 double sumDegreeC        = 0;
256                 for (int n = FastMath.max(2, m); n <= degree; ++n) {
257                     sumDegreeS += pnm0[n] * aOrN[n] * harmonics.getNormalizedSnm(n, m);
258                     sumDegreeC += pnm0[n] * aOrN[n] * harmonics.getNormalizedCnm(n, m);
259                 }
260 
261                 // contribution to outer summation over order
262                 value = value * u + cosSinLambda[1][m] * sumDegreeS + cosSinLambda[0][m] * sumDegreeC;
263 
264             }
265 
266             // rotate the recursion arrays
267             final double[] tmp = pnm0Plus2;
268             pnm0Plus2 = pnm0Plus1;
269             pnm0Plus1 = pnm0;
270             pnm0      = tmp;
271 
272         }
273 
274         // scale back
275         value = FastMath.scalb(value, SCALING);
276 
277         // apply the global mu/r factor
278         return mu * value / r;
279 
280     }
281 
282     /** Compute the gradient of the non-central part of the gravity field.
283      * <p>
284      * If U represents the non-central part of the gravity field,
285      * this method returns the negative gradient of U (-grad(U)).
286      * </p>
287      * @param date current date
288      * @param position position at which gravity field is desired in body frame
289      * @param mu central attraction coefficient to use
290      * @return gradient of the non-central part of the gravity field
291      */
292     public double[] gradient(final AbsoluteDate date, final Vector3D position, final double mu) {
293 
294         final int degree = provider.getMaxDegree();
295         final int order  = provider.getMaxOrder();
296         final NormalizedSphericalHarmonics harmonics = provider.onDate(date);
297 
298         // allocate the columns for recursion
299         double[] pnm0Plus2  = new double[degree + 1];
300         double[] pnm0Plus1  = new double[degree + 1];
301         double[] pnm0       = new double[degree + 1];
302         final double[] pnm1 = new double[degree + 1];
303 
304         // compute polar coordinates
305         final double x    = position.getX();
306         final double y    = position.getY();
307         final double z    = position.getZ();
308         final double x2   = x * x;
309         final double y2   = y * y;
310         final double z2   = z * z;
311         final double r2   = x2 + y2 + z2;
312         final double r    = FastMath.sqrt (r2);
313         final double rho2 = x2 + y2;
314         final double rho  = FastMath.sqrt(rho2);
315         final double t    = z / r;   // cos(theta), where theta is the polar angle
316         final double u    = rho / r; // sin(theta), where theta is the polar angle
317         final double tOu  = z / rho;
318 
319         // compute distance powers
320         final double[] aOrN = createDistancePowersArray(provider.getAe() / r);
321 
322         // compute longitude cosines/sines
323         final double[][] cosSinLambda = createCosSinArrays(x / rho, y / rho);
324 
325         // outer summation over order
326         int    index = 0;
327         double value = 0;
328         final double[] gradient = new double[3];
329         for (int m = degree; m >= 0; --m) {
330 
331             // compute tesseral terms with derivatives
332             index = computeTesseral(m, degree, index, t, u, tOu,
333                                     pnm0Plus2, pnm0Plus1, null, pnm0, pnm1, null);
334 
335             if (m <= order) {
336                 // compute contribution of current order to field (equation 5 of the paper)
337 
338                 // inner summation over degree, for fixed order
339                 double sumDegreeS        = 0;
340                 double sumDegreeC        = 0;
341                 double dSumDegreeSdR     = 0;
342                 double dSumDegreeCdR     = 0;
343                 double dSumDegreeSdTheta = 0;
344                 double dSumDegreeCdTheta = 0;
345                 for (int n = FastMath.max(2, m); n <= degree; ++n) {
346                     final double qSnm  = aOrN[n] * harmonics.getNormalizedSnm(n, m);
347                     final double qCnm  = aOrN[n] * harmonics.getNormalizedCnm(n, m);
348                     final double nOr   = n / r;
349                     final double s0    = pnm0[n] * qSnm;
350                     final double c0    = pnm0[n] * qCnm;
351                     final double s1    = pnm1[n] * qSnm;
352                     final double c1    = pnm1[n] * qCnm;
353                     sumDegreeS        += s0;
354                     sumDegreeC        += c0;
355                     dSumDegreeSdR     -= nOr * s0;
356                     dSumDegreeCdR     -= nOr * c0;
357                     dSumDegreeSdTheta += s1;
358                     dSumDegreeCdTheta += c1;
359                 }
360 
361                 // contribution to outer summation over order
362                 // beware that we need to order gradient using the mathematical conventions
363                 // compliant with the SphericalCoordinates class, so our lambda is its theta
364                 // (and hence at index 1) and our theta is its phi (and hence at index 2)
365                 final double sML = cosSinLambda[1][m];
366                 final double cML = cosSinLambda[0][m];
367                 value            = value       * u + sML * sumDegreeS        + cML * sumDegreeC;
368                 gradient[0]      = gradient[0] * u + sML * dSumDegreeSdR     + cML * dSumDegreeCdR;
369                 gradient[1]      = gradient[1] * u + m * (cML * sumDegreeS - sML * sumDegreeC);
370                 gradient[2]      = gradient[2] * u + sML * dSumDegreeSdTheta + cML * dSumDegreeCdTheta;
371 
372             }
373 
374             // rotate the recursion arrays
375             final double[] tmp = pnm0Plus2;
376             pnm0Plus2 = pnm0Plus1;
377             pnm0Plus1 = pnm0;
378             pnm0      = tmp;
379 
380         }
381 
382         // scale back
383         value       = FastMath.scalb(value,       SCALING);
384         gradient[0] = FastMath.scalb(gradient[0], SCALING);
385         gradient[1] = FastMath.scalb(gradient[1], SCALING);
386         gradient[2] = FastMath.scalb(gradient[2], SCALING);
387 
388         // apply the global mu/r factor
389         final double muOr = mu / r;
390         value            *= muOr;
391         gradient[0]       = muOr * gradient[0] - value / r;
392         gradient[1]      *= muOr;
393         gradient[2]      *= muOr;
394 
395         // convert gradient from spherical to Cartesian
396         return new SphericalCoordinates(position).toCartesianGradient(gradient);
397 
398     }
399 
400     /** Compute the gradient of the non-central part of the gravity field.
401      * <p>
402      * If U represents the non-central part of the gravity field,
403      * this method returns the negative gradient of U (-grad(U)).
404      * </p>
405      * @param date current date
406      * @param position position at which gravity field is desired in body frame
407      * @param mu central attraction coefficient to use
408      * @param <T> type of field used
409      * @return gradient of the non-central part of the gravity field
410      */
411     public <T extends CalculusFieldElement<T>> T[] gradient(final FieldAbsoluteDate<T> date, final FieldVector3D<T> position,
412                                                         final T mu) {
413 
414         final int degree = provider.getMaxDegree();
415         final int order  = provider.getMaxOrder();
416         final NormalizedSphericalHarmonics harmonics = provider.onDate(date.toAbsoluteDate());
417         final T zero = date.getField().getZero();
418         // allocate the columns for recursion
419         T[] pnm0Plus2  = MathArrays.buildArray(date.getField(), degree + 1);
420         T[] pnm0Plus1  = MathArrays.buildArray(date.getField(), degree + 1);
421         T[] pnm0       = MathArrays.buildArray(date.getField(), degree + 1);
422         final T[] pnm1 = MathArrays.buildArray(date.getField(), degree + 1);
423 
424         // compute polar coordinates
425         final T x    = position.getX();
426         final T y    = position.getY();
427         final T z    = position.getZ();
428         final T x2   = x.square();
429         final T y2   = y.square();
430         final T rho2 = x2.add(y2);
431         final T rho  = rho2.sqrt();
432         final T z2   = z.square();
433         final T r2   = rho2.add(z2);
434         final T r    = r2.sqrt();
435         final T t    = z.divide(r);   // cos(theta), where theta is the polar angle
436         final T u    = rho.divide(r); // sin(theta), where theta is the polar angle
437         final T tOu  = z.divide(rho);
438 
439         // compute distance powers
440         final T[] aOrN = createDistancePowersArray(r.reciprocal().multiply(provider.getAe()));
441 
442         // compute longitude cosines/sines
443         final T[][] cosSinLambda = createCosSinArrays(x.divide(rho), y.divide(rho));
444         // outer summation over order
445         int    index = 0;
446         T value = zero;
447         final T[] gradient = MathArrays.buildArray(zero.getField(), 3);
448         for (int m = degree; m >= 0; --m) {
449 
450             // compute tesseral terms with derivatives
451             index = computeTesseral(m, degree, index, t, u, tOu,
452                                     pnm0Plus2, pnm0Plus1, null, pnm0, pnm1, null);
453             if (m <= order) {
454                 // compute contribution of current order to field (equation 5 of the paper)
455 
456                 // inner summation over degree, for fixed order
457                 T sumDegreeS        = zero;
458                 T sumDegreeC        = zero;
459                 T dSumDegreeSdR     = zero;
460                 T dSumDegreeCdR     = zero;
461                 T dSumDegreeSdTheta = zero;
462                 T dSumDegreeCdTheta = zero;
463                 for (int n = FastMath.max(2, m); n <= degree; ++n) {
464                     final T qSnm  = aOrN[n].multiply(harmonics.getNormalizedSnm(n, m));
465                     final T qCnm  = aOrN[n].multiply(harmonics.getNormalizedCnm(n, m));
466                     final T nOr   = r.reciprocal().multiply(n);
467                     final T s0    = pnm0[n].multiply(qSnm);
468                     final T c0    = pnm0[n].multiply(qCnm);
469                     final T s1    = pnm1[n].multiply(qSnm);
470                     final T c1    = pnm1[n].multiply(qCnm);
471                     sumDegreeS        = sumDegreeS       .add(s0);
472                     sumDegreeC        = sumDegreeC       .add(c0);
473                     dSumDegreeSdR     = dSumDegreeSdR    .subtract(nOr.multiply(s0));
474                     dSumDegreeCdR     = dSumDegreeCdR    .subtract(nOr.multiply(c0));
475                     dSumDegreeSdTheta = dSumDegreeSdTheta.add(s1);
476                     dSumDegreeCdTheta = dSumDegreeCdTheta.add(c1);
477                 }
478 
479                 // contribution to outer summation over order
480                 // beware that we need to order gradient using the mathematical conventions
481                 // compliant with the SphericalCoordinates class, so our lambda is its theta
482                 // (and hence at index 1) and our theta is its phi (and hence at index 2)
483                 final T sML = cosSinLambda[1][m];
484                 final T cML = cosSinLambda[0][m];
485                 value            = value      .multiply(u).add(sML.multiply(sumDegreeS   )).add(cML.multiply(sumDegreeC));
486                 gradient[0]      = gradient[0].multiply(u).add(sML.multiply(dSumDegreeSdR)).add(cML.multiply(dSumDegreeCdR));
487                 gradient[1]      = gradient[1].multiply(u).add(cML.multiply(sumDegreeS).subtract(sML.multiply(sumDegreeC)).multiply(m));
488                 gradient[2]      = gradient[2].multiply(u).add(sML.multiply(dSumDegreeSdTheta)).add(cML.multiply(dSumDegreeCdTheta));
489             }
490             // rotate the recursion arrays
491             final T[] tmp = pnm0Plus2;
492             pnm0Plus2 = pnm0Plus1;
493             pnm0Plus1 = pnm0;
494             pnm0      = tmp;
495 
496         }
497         // scale back
498         value       = value.scalb(SCALING);
499         gradient[0] = gradient[0].scalb(SCALING);
500         gradient[1] = gradient[1].scalb(SCALING);
501         gradient[2] = gradient[2].scalb(SCALING);
502 
503         // apply the global mu/r factor
504         final T muOr = r.reciprocal().multiply(mu);
505         value            = value.multiply(muOr);
506         gradient[0]      = muOr.multiply(gradient[0]).subtract(value.divide(r));
507         gradient[1]      = gradient[1].multiply(muOr);
508         gradient[2]      = gradient[2].multiply(muOr);
509 
510         // convert gradient from spherical to Cartesian
511         // Cartesian coordinates
512         // remaining spherical coordinates
513 
514         // intermediate variables
515         final T xPos    = position.getX();
516         final T yPos    = position.getY();
517         final T zPos    = position.getZ();
518         final T rho2Pos = x.square().add(y.square());
519         final T rhoPos  = rho2.sqrt();
520         final T r2Pos   = rho2.add(z.square());
521         final T rPos    = r2Pos.sqrt();
522 
523         final T[][] jacobianPos = MathArrays.buildArray(zero.getField(), 3, 3);
524 
525         // row representing the gradient of r
526         jacobianPos[0][0] = xPos.divide(rPos);
527         jacobianPos[0][1] = yPos.divide(rPos);
528         jacobianPos[0][2] = zPos.divide(rPos);
529 
530         // row representing the gradient of theta
531         jacobianPos[1][0] =  yPos.negate().divide(rho2Pos);
532         jacobianPos[1][1] =  xPos.divide(rho2Pos);
533         // jacobian[1][2] is already set to 0 at allocation time
534 
535         // row representing the gradient of phi
536         final T rhoPosTimesR2Pos = rhoPos.multiply(r2Pos);
537         jacobianPos[2][0] = xPos.multiply(zPos).divide(rhoPosTimesR2Pos);
538         jacobianPos[2][1] = yPos.multiply(zPos).divide(rhoPosTimesR2Pos);
539         jacobianPos[2][2] = rhoPos.negate().divide(r2Pos);
540         final T[] cartGradPos = MathArrays.buildArray(zero.getField(), 3);
541         cartGradPos[0] = gradient[0].multiply(jacobianPos[0][0]).add(gradient[1].multiply(jacobianPos[1][0])).add(gradient[2].multiply(jacobianPos[2][0]));
542         cartGradPos[1] = gradient[0].multiply(jacobianPos[0][1]).add(gradient[1].multiply(jacobianPos[1][1])).add(gradient[2].multiply(jacobianPos[2][1]));
543         cartGradPos[2] = gradient[0].multiply(jacobianPos[0][2])                                      .add(gradient[2].multiply(jacobianPos[2][2]));
544         return cartGradPos;
545 
546     }
547 
548     /** Compute both the gradient and the hessian of the non-central part of the gravity field.
549      * <p>
550      * If U represents the non-central part of the gravity field,
551      * this method returns the negative gradient and hessian of U (-grad(U) and -hessian(U)).
552      * </p>
553      * @param date current date
554      * @param position position at which gravity field is desired in body frame
555      * @param mu central attraction coefficient to use
556      * @return gradient and hessian of the non-central part of the gravity field
557      */
558     private GradientHessian gradientHessian(final AbsoluteDate date, final Vector3D position, final double mu) {
559 
560         final int degree = provider.getMaxDegree();
561         final int order  = provider.getMaxOrder();
562         final NormalizedSphericalHarmonics harmonics = provider.onDate(date);
563 
564         // allocate the columns for recursion
565         double[] pnm0Plus2  = new double[degree + 1];
566         double[] pnm0Plus1  = new double[degree + 1];
567         double[] pnm0       = new double[degree + 1];
568         double[] pnm1Plus1  = new double[degree + 1];
569         double[] pnm1       = new double[degree + 1];
570         final double[] pnm2 = new double[degree + 1];
571 
572         // compute polar coordinates
573         final double x    = position.getX();
574         final double y    = position.getY();
575         final double z    = position.getZ();
576         final double x2   = x * x;
577         final double y2   = y * y;
578         final double z2   = z * z;
579         final double rho2 = x2 + y2;
580         final double rho  = FastMath.sqrt(rho2);
581         final double r2   = rho2 + z2;
582         final double r    = FastMath.sqrt(r2);
583         final double t    = z / r;   // cos(theta), where theta is the polar angle
584         final double u    = rho / r; // sin(theta), where theta is the polar angle
585         final double tOu  = z / rho;
586 
587         // compute distance powers
588         final double[] aOrN = createDistancePowersArray(provider.getAe() / r);
589 
590         // compute longitude cosines/sines
591         final double[][] cosSinLambda = createCosSinArrays(x / rho, y / rho);
592 
593         // outer summation over order
594         int    index = 0;
595         double value = 0;
596         final double[]   gradient = new double[3];
597         final double[][] hessian  = new double[3][3];
598         for (int m = degree; m >= 0; --m) {
599 
600             // compute tesseral terms
601             index = computeTesseral(m, degree, index, t, u, tOu,
602                                     pnm0Plus2, pnm0Plus1, pnm1Plus1, pnm0, pnm1, pnm2);
603 
604             if (m <= order) {
605                 // compute contribution of current order to field (equation 5 of the paper)
606 
607                 // inner summation over degree, for fixed order
608                 double sumDegreeS               = 0;
609                 double sumDegreeC               = 0;
610                 double dSumDegreeSdR            = 0;
611                 double dSumDegreeCdR            = 0;
612                 double dSumDegreeSdTheta        = 0;
613                 double dSumDegreeCdTheta        = 0;
614                 double d2SumDegreeSdRdR         = 0;
615                 double d2SumDegreeSdRdTheta     = 0;
616                 double d2SumDegreeSdThetadTheta = 0;
617                 double d2SumDegreeCdRdR         = 0;
618                 double d2SumDegreeCdRdTheta     = 0;
619                 double d2SumDegreeCdThetadTheta = 0;
620                 for (int n = FastMath.max(2, m); n <= degree; ++n) {
621                     final double qSnm         = aOrN[n] * harmonics.getNormalizedSnm(n, m);
622                     final double qCnm         = aOrN[n] * harmonics.getNormalizedCnm(n, m);
623                     final double nOr          = n / r;
624                     final double nnP1Or2      = nOr * (n + 1) / r;
625                     final double s0           = pnm0[n] * qSnm;
626                     final double c0           = pnm0[n] * qCnm;
627                     final double s1           = pnm1[n] * qSnm;
628                     final double c1           = pnm1[n] * qCnm;
629                     final double s2           = pnm2[n] * qSnm;
630                     final double c2           = pnm2[n] * qCnm;
631                     sumDegreeS               += s0;
632                     sumDegreeC               += c0;
633                     dSumDegreeSdR            -= nOr * s0;
634                     dSumDegreeCdR            -= nOr * c0;
635                     dSumDegreeSdTheta        += s1;
636                     dSumDegreeCdTheta        += c1;
637                     d2SumDegreeSdRdR         += nnP1Or2 * s0;
638                     d2SumDegreeSdRdTheta     -= nOr * s1;
639                     d2SumDegreeSdThetadTheta += s2;
640                     d2SumDegreeCdRdR         += nnP1Or2 * c0;
641                     d2SumDegreeCdRdTheta     -= nOr * c1;
642                     d2SumDegreeCdThetadTheta += c2;
643                 }
644 
645                 // contribution to outer summation over order
646                 final double sML = cosSinLambda[1][m];
647                 final double cML = cosSinLambda[0][m];
648                 value            = value         * u + sML * sumDegreeS + cML * sumDegreeC;
649                 gradient[0]      = gradient[0]   * u + sML * dSumDegreeSdR + cML * dSumDegreeCdR;
650                 gradient[1]      = gradient[1]   * u + m * (cML * sumDegreeS - sML * sumDegreeC);
651                 gradient[2]      = gradient[2]   * u + sML * dSumDegreeSdTheta + cML * dSumDegreeCdTheta;
652                 hessian[0][0]    = hessian[0][0] * u + sML * d2SumDegreeSdRdR + cML * d2SumDegreeCdRdR;
653                 hessian[1][0]    = hessian[1][0] * u + m * (cML * dSumDegreeSdR - sML * dSumDegreeCdR);
654                 hessian[2][0]    = hessian[2][0] * u + sML * d2SumDegreeSdRdTheta + cML * d2SumDegreeCdRdTheta;
655                 hessian[1][1]    = hessian[1][1] * u - m * m * (sML * sumDegreeS + cML * sumDegreeC);
656                 hessian[2][1]    = hessian[2][1] * u + m * (cML * dSumDegreeSdTheta - sML * dSumDegreeCdTheta);
657                 hessian[2][2]    = hessian[2][2] * u + sML * d2SumDegreeSdThetadTheta + cML * d2SumDegreeCdThetadTheta;
658 
659             }
660 
661             // rotate the recursion arrays
662             final double[] tmp0 = pnm0Plus2;
663             pnm0Plus2 = pnm0Plus1;
664             pnm0Plus1 = pnm0;
665             pnm0      = tmp0;
666             final double[] tmp1 = pnm1Plus1;
667             pnm1Plus1 = pnm1;
668             pnm1      = tmp1;
669 
670         }
671 
672         // scale back
673         value = FastMath.scalb(value, SCALING);
674         for (int i = 0; i < 3; ++i) {
675             gradient[i] = FastMath.scalb(gradient[i], SCALING);
676             for (int j = 0; j <= i; ++j) {
677                 hessian[i][j] = FastMath.scalb(hessian[i][j], SCALING);
678             }
679         }
680 
681 
682         // apply the global mu/r factor
683         final double muOr = mu / r;
684         value         *= muOr;
685         gradient[0]    = muOr * gradient[0] - value / r;
686         gradient[1]   *= muOr;
687         gradient[2]   *= muOr;
688         hessian[0][0]  = muOr * hessian[0][0] - 2 * gradient[0] / r;
689         hessian[1][0]  = muOr * hessian[1][0] -     gradient[1] / r;
690         hessian[2][0]  = muOr * hessian[2][0] -     gradient[2] / r;
691         hessian[1][1] *= muOr;
692         hessian[2][1] *= muOr;
693         hessian[2][2] *= muOr;
694 
695         // convert gradient and Hessian from spherical to Cartesian
696         final SphericalCoordinates sc = new SphericalCoordinates(position);
697         return new GradientHessian(sc.toCartesianGradient(gradient),
698                                    sc.toCartesianHessian(hessian, gradient));
699 
700 
701     }
702 
703     /** Container for gradient and Hessian. */
704     private static class GradientHessian {
705 
706         /** Gradient. */
707         private final double[] gradient;
708 
709         /** Hessian. */
710         private final double[][] hessian;
711 
712         /** Simple constructor.
713          * <p>
714          * A reference to the arrays is stored, they are <strong>not</strong> cloned.
715          * </p>
716          * @param gradient gradient
717          * @param hessian hessian
718          */
719         GradientHessian(final double[] gradient, final double[][] hessian) {
720             this.gradient = gradient;
721             this.hessian  = hessian;
722         }
723 
724         /** Get a reference to the gradient.
725          * @return gradient (a reference to the internal array is returned)
726          */
727         public double[] getGradient() {
728             return gradient;
729         }
730 
731         /** Get a reference to the Hessian.
732          * @return Hessian (a reference to the internal array is returned)
733          */
734         public double[][] getHessian() {
735             return hessian;
736         }
737 
738     }
739 
740     /** Compute a/r powers array.
741      * @param aOr a/r
742      * @return array containing (a/r)<sup>n</sup>
743      */
744     private double[] createDistancePowersArray(final double aOr) {
745 
746         // initialize array
747         final double[] aOrN = new double[provider.getMaxDegree() + 1];
748         aOrN[0] = 1;
749         if (provider.getMaxDegree() > 0) {
750             aOrN[1] = aOr;
751         }
752 
753         // fill up array
754         for (int n = 2; n < aOrN.length; ++n) {
755             final int p = n / 2;
756             final int q = n - p;
757             aOrN[n] = aOrN[p] * aOrN[q];
758         }
759 
760         return aOrN;
761 
762     }
763     /** Compute a/r powers array.
764      * @param aOr a/r
765      * @param <T> type of field used
766      * @return array containing (a/r)<sup>n</sup>
767      */
768     private <T extends CalculusFieldElement<T>> T[] createDistancePowersArray(final T aOr) {
769 
770         // initialize array
771         final T[] aOrN = MathArrays.buildArray(aOr.getField(), provider.getMaxDegree() + 1);
772         aOrN[0] = aOr.getField().getOne();
773         if (provider.getMaxDegree() > 0) {
774             aOrN[1] = aOr;
775         }
776 
777         // fill up array
778         for (int n = 2; n < aOrN.length; ++n) {
779             final int p = n / 2;
780             final int q = n - p;
781             aOrN[n] = aOrN[p].multiply(aOrN[q]);
782         }
783 
784         return aOrN;
785 
786     }
787 
788     /** Compute longitude cosines and sines.
789      * @param cosLambda cos(λ)
790      * @param sinLambda sin(λ)
791      * @return array containing cos(m &times; λ) in row 0
792      * and sin(m &times; λ) in row 1
793      */
794     private double[][] createCosSinArrays(final double cosLambda, final double sinLambda) {
795 
796         // initialize arrays
797         final double[][] cosSin = new double[2][provider.getMaxOrder() + 1];
798         cosSin[0][0] = 1;
799         cosSin[1][0] = 0;
800         if (provider.getMaxOrder() > 0) {
801             cosSin[0][1] = cosLambda;
802             cosSin[1][1] = sinLambda;
803 
804             // fill up array
805             for (int m = 2; m < cosSin[0].length; ++m) {
806 
807                 // m * lambda is split as p * lambda + q * lambda, trying to avoid
808                 // p or q being much larger than the other. This reduces the number of
809                 // intermediate results reused to compute each value, and hence should limit
810                 // as much as possible roundoff error accumulation
811                 // (this does not change the number of floating point operations)
812                 final int p = m / 2;
813                 final int q = m - p;
814 
815                 cosSin[0][m] = cosSin[0][p] * cosSin[0][q] - cosSin[1][p] * cosSin[1][q];
816                 cosSin[1][m] = cosSin[1][p] * cosSin[0][q] + cosSin[0][p] * cosSin[1][q];
817             }
818         }
819 
820         return cosSin;
821 
822     }
823 
824     /** Compute longitude cosines and sines.
825      * @param cosLambda cos(λ)
826      * @param sinLambda sin(λ)
827      * @param <T> type of field used
828      * @return array containing cos(m &times; λ) in row 0
829      * and sin(m &times; λ) in row 1
830      */
831     private <T extends CalculusFieldElement<T>> T[][] createCosSinArrays(final T cosLambda, final T sinLambda) {
832 
833         final T one = cosLambda.getField().getOne();
834         final T zero = cosLambda.getField().getZero();
835         // initialize arrays
836         final T[][] cosSin = MathArrays.buildArray(one.getField(), 2, provider.getMaxOrder() + 1);
837         cosSin[0][0] = one;
838         cosSin[1][0] = zero;
839         if (provider.getMaxOrder() > 0) {
840             cosSin[0][1] = cosLambda;
841             cosSin[1][1] = sinLambda;
842 
843             // fill up array
844             for (int m = 2; m < cosSin[0].length; ++m) {
845 
846                 // m * lambda is split as p * lambda + q * lambda, trying to avoid
847                 // p or q being much larger than the other. This reduces the number of
848                 // intermediate results reused to compute each value, and hence should limit
849                 // as much as possible roundoff error accumulation
850                 // (this does not change the number of floating point operations)
851                 final int p = m / 2;
852                 final int q = m - p;
853 
854                 cosSin[0][m] = cosSin[0][p].multiply(cosSin[0][q]).subtract(cosSin[1][p].multiply(cosSin[1][q]));
855                 cosSin[1][m] = cosSin[1][p].multiply(cosSin[0][q]).add(cosSin[0][p].multiply(cosSin[1][q]));
856 
857             }
858         }
859 
860         return cosSin;
861 
862     }
863 
864     /** Compute one order of tesseral terms.
865      * <p>
866      * This corresponds to equations 27 and 30 of the paper.
867      * </p>
868      * @param m current order
869      * @param degree max degree
870      * @param index index in the flattened array
871      * @param t cos(θ), where θ is the polar angle
872      * @param u sin(θ), where θ is the polar angle
873      * @param tOu t/u
874      * @param pnm0Plus2 array containing scaled P<sub>n,m+2</sub>/u<sup>m+2</sup>
875      * @param pnm0Plus1 array containing scaled P<sub>n,m+1</sub>/u<sup>m+1</sup>
876      * @param pnm1Plus1 array containing scaled dP<sub>n,m+1</sub>/u<sup>m+1</sup>
877      * (may be null if second derivatives are not needed)
878      * @param pnm0 array to fill with scaled P<sub>n,m</sub>/u<sup>m</sup>
879      * @param pnm1 array to fill with scaled dP<sub>n,m</sub>/u<sup>m</sup>
880      * (may be null if first derivatives are not needed)
881      * @param pnm2 array to fill with scaled d²P<sub>n,m</sub>/u<sup>m</sup>
882      * (may be null if second derivatives are not needed)
883      * @return new value for index
884      */
885     private int computeTesseral(final int m, final int degree, final int index,
886                                 final double t, final double u, final double tOu,
887                                 final double[] pnm0Plus2, final double[] pnm0Plus1, final double[] pnm1Plus1,
888                                 final double[] pnm0, final double[] pnm1, final double[] pnm2) {
889 
890         final double u2 = u * u;
891 
892         // initialize recursion from sectorial terms
893         int n = FastMath.max(2, m);
894         if (n == m) {
895             pnm0[n] = sectorial[n];
896             ++n;
897         }
898 
899         // compute tesseral values
900         int localIndex = index;
901         while (n <= degree) {
902 
903             // value (equation 27 of the paper)
904             pnm0[n] = gnmOj[localIndex] * t * pnm0Plus1[n] - hnmOj[localIndex] * u2 * pnm0Plus2[n];
905 
906             ++localIndex;
907             ++n;
908 
909         }
910 
911         if (pnm1 != null) {
912 
913             // initialize recursion from sectorial terms
914             n = FastMath.max(2, m);
915             if (n == m) {
916                 pnm1[n] = m * tOu * pnm0[n];
917                 ++n;
918             }
919 
920             // compute tesseral values and derivatives with respect to polar angle
921             localIndex = index;
922             while (n <= degree) {
923 
924                 // first derivative (equation 30 of the paper)
925                 pnm1[n] = m * tOu * pnm0[n] - enm[localIndex] * u * pnm0Plus1[n];
926 
927                 ++localIndex;
928                 ++n;
929 
930             }
931 
932             if (pnm2 != null) {
933 
934                 // initialize recursion from sectorial terms
935                 n = FastMath.max(2, m);
936                 if (n == m) {
937                     pnm2[n] = m * (tOu * pnm1[n] - pnm0[n] / u2);
938                     ++n;
939                 }
940 
941                 // compute tesseral values and derivatives with respect to polar angle
942                 localIndex = index;
943                 while (n <= degree) {
944 
945                     // second derivative (differential of equation 30 with respect to theta)
946                     pnm2[n] = m * (tOu * pnm1[n] - pnm0[n] / u2) - enm[localIndex] * u * pnm1Plus1[n];
947 
948                     ++localIndex;
949                     ++n;
950 
951                 }
952 
953             }
954 
955         }
956 
957         return localIndex;
958 
959     }
960 
961     /** Compute one order of tesseral terms.
962      * <p>
963      * This corresponds to equations 27 and 30 of the paper.
964      * </p>
965      * @param m current order
966      * @param degree max degree
967      * @param index index in the flattened array
968      * @param t cos(θ), where θ is the polar angle
969      * @param u sin(θ), where θ is the polar angle
970      * @param tOu t/u
971      * @param pnm0Plus2 array containing scaled P<sub>n,m+2</sub>/u<sup>m+2</sup>
972      * @param pnm0Plus1 array containing scaled P<sub>n,m+1</sub>/u<sup>m+1</sup>
973      * @param pnm1Plus1 array containing scaled dP<sub>n,m+1</sub>/u<sup>m+1</sup>
974      * (may be null if second derivatives are not needed)
975      * @param pnm0 array to fill with scaled P<sub>n,m</sub>/u<sup>m</sup>
976      * @param pnm1 array to fill with scaled dP<sub>n,m</sub>/u<sup>m</sup>
977      * (may be null if first derivatives are not needed)
978      * @param pnm2 array to fill with scaled d²P<sub>n,m</sub>/u<sup>m</sup>
979      * (may be null if second derivatives are not needed)
980      * @param <T> instance of field element
981      * @return new value for index
982      */
983     private <T extends CalculusFieldElement<T>> int computeTesseral(final int m, final int degree, final int index,
984                                                                 final T t, final T u, final T tOu,
985                                                                 final T[] pnm0Plus2, final T[] pnm0Plus1, final T[] pnm1Plus1,
986                                                                 final T[] pnm0, final T[] pnm1, final T[] pnm2) {
987 
988         final T u2 = u.square();
989         final T zero = u.getField().getZero();
990         // initialize recursion from sectorial terms
991         int n = FastMath.max(2, m);
992         if (n == m) {
993             pnm0[n] = zero.newInstance(sectorial[n]);
994             ++n;
995         }
996 
997         // compute tesseral values
998         int localIndex = index;
999         while (n <= degree) {
1000 
1001             // value (equation 27 of the paper)
1002             pnm0[n] = t.multiply(gnmOj[localIndex]).multiply(pnm0Plus1[n]).subtract(u2.multiply(pnm0Plus2[n]).multiply(hnmOj[localIndex]));
1003             ++localIndex;
1004             ++n;
1005 
1006         }
1007         if (pnm1 != null) {
1008 
1009             // initialize recursion from sectorial terms
1010             n = FastMath.max(2, m);
1011             if (n == m) {
1012                 pnm1[n] = tOu.multiply(m).multiply(pnm0[n]);
1013                 ++n;
1014             }
1015 
1016             // compute tesseral values and derivatives with respect to polar angle
1017             localIndex = index;
1018             while (n <= degree) {
1019 
1020                 // first derivative (equation 30 of the paper)
1021                 pnm1[n] = tOu.multiply(m).multiply(pnm0[n]).subtract(u.multiply(enm[localIndex]).multiply(pnm0Plus1[n]));
1022 
1023                 ++localIndex;
1024                 ++n;
1025 
1026             }
1027 
1028             if (pnm2 != null) {
1029 
1030                 // initialize recursion from sectorial terms
1031                 n = FastMath.max(2, m);
1032                 if (n == m) {
1033                     pnm2[n] =   tOu.multiply(pnm1[n]).subtract(pnm0[n].divide(u2)).multiply(m);
1034                     ++n;
1035                 }
1036 
1037                 // compute tesseral values and derivatives with respect to polar angle
1038                 localIndex = index;
1039                 while (n <= degree) {
1040 
1041                     // second derivative (differential of equation 30 with respect to theta)
1042                     pnm2[n] = tOu.multiply(pnm1[n]).subtract(pnm0[n].divide(u2)).multiply(m).subtract(u.multiply(pnm1Plus1[n]).multiply(enm[localIndex]));
1043                     ++localIndex;
1044                     ++n;
1045 
1046                 }
1047 
1048             }
1049 
1050         }
1051         return localIndex;
1052 
1053     }
1054 
1055     /** {@inheritDoc} */
1056     @Override
1057     public Vector3D acceleration(final SpacecraftState s, final double[] parameters) {
1058 
1059         final double mu = parameters[0];
1060 
1061         // get the position in body frame
1062         final AbsoluteDate date       = s.getDate();
1063         final StaticTransform fromBodyFrame = bodyFrame.getStaticTransformTo(s.getFrame(), date);
1064         final StaticTransform toBodyFrame   = fromBodyFrame.getInverse();
1065         final Vector3D position       = toBodyFrame.transformPosition(s.getPosition());
1066 
1067         // gradient of the non-central part of the gravity field
1068         return fromBodyFrame.transformVector(new Vector3D(gradient(date, position, mu)));
1069 
1070     }
1071 
1072     /** {@inheritDoc} */
1073     public <T extends CalculusFieldElement<T>> FieldVector3D<T> acceleration(final FieldSpacecraftState<T> s,
1074                                                                          final T[] parameters) {
1075 
1076         final T mu = parameters[0];
1077 
1078         // check for faster computation dedicated to derivatives with respect to state and gravitational parameter
1079         if (s.getDate().hasZeroField() && isGradientStateDerivative(s) && isGradientConstantOrMuDerivative(mu)) {
1080             @SuppressWarnings("unchecked")
1081             final FieldVector3D<Gradient> p = (FieldVector3D<Gradient>) s.getPosition();
1082             @SuppressWarnings("unchecked")
1083             final FieldVector3D<T> a = (FieldVector3D<T>) accelerationWrtState(s.getDate().toAbsoluteDate(),
1084                     s.getFrame(), p,
1085                     (Gradient) mu);
1086             return a;
1087         }
1088 
1089         // get the position in body frame
1090         final FieldAbsoluteDate<T> date             = s.getDate();
1091         final FieldStaticTransform<T> fromBodyFrame = bodyFrame.getStaticTransformTo(s.getFrame(), date);
1092         final FieldStaticTransform<T> toBodyFrame   = fromBodyFrame.getInverse();
1093         final FieldVector3D<T> position             = toBodyFrame.transformPosition(s.getPosition());
1094 
1095         // gradient of the non-central part of the gravity field
1096         return fromBodyFrame.transformVector(new FieldVector3D<>(gradient(date, position, mu)));
1097 
1098     }
1099 
1100     /** Check if a field state corresponds to derivatives with respect to state.
1101      * @param state state to check
1102      * @param <T> type of the filed elements
1103      * @return true if state corresponds to derivatives with respect to state
1104      * @since 10.2
1105      */
1106     private <T extends CalculusFieldElement<T>> boolean isGradientStateDerivative(final FieldSpacecraftState<T> state) {
1107         if (state.getMass() instanceof final Gradient gMass) {
1108             final int p = gMass.getFreeParameters();
1109             if (p < 3) {
1110                 return false;
1111             }
1112             @SuppressWarnings("unchecked") final FieldPVCoordinates<Gradient> pv = (FieldPVCoordinates<Gradient>) state.getPVCoordinates();
1113             return isVariable(pv.getPosition().getX(), 0) &&
1114                     isVariable(pv.getPosition().getY(), 1) &&
1115                     isVariable(pv.getPosition().getZ(), 2);
1116         } else {
1117             return false;
1118         }
1119     }
1120 
1121     /**
1122      * Check if a field gravitational parameter is constant or represents an independent variable.
1123      * @param mu field gravitational parameter
1124      * @return true if the field gravitational parameter is constant or represents an independent variable
1125      * @param <T> type of field
1126      * @since 13.1.3
1127      */
1128     private <T extends CalculusFieldElement<T>> boolean isGradientConstantOrMuDerivative(final T mu) {
1129         try {
1130             final double[] derivatives = ((Gradient) mu).getGradient();
1131             boolean check = true;
1132             for (int i = 0; i < derivatives.length; i++) {
1133                 if (i == 3) {
1134                     check &= derivatives[i] == 0.0 || derivatives[i] == 1.0;
1135                 } else {
1136                     check &= derivatives[i] == 0.0;
1137                 }
1138             }
1139             return check;
1140         } catch (ClassCastException cce) {
1141             return false;
1142         }
1143     }
1144 
1145     /** Check if a derivative represents a specified variable.
1146      * @param g derivative to check
1147      * @param index index of the variable
1148      * @return true if the derivative represents a specified variable
1149      * @since 10.2
1150      */
1151     private boolean isVariable(final Gradient g, final int index) {
1152         final double[] derivatives = g.getGradient();
1153         boolean check = true;
1154         for (int i = 0; i < derivatives.length; ++i) {
1155             check &= derivatives[i] == ((index == i) ? 1.0 : 0.0);
1156         }
1157         return check;
1158     }
1159 
1160     /** Compute acceleration derivatives with respect to state parameters.
1161      * <p>
1162      * From a theoretical point of view, this method computes the same values
1163      * as {@link #acceleration(FieldSpacecraftState, CalculusFieldElement[])} in the
1164      * specific case of {@link Gradient} with respect to state, so
1165      * it is less general. However, it is *much* faster in this important case.
1166      * <p>
1167      * <p>
1168      * The derivatives should be computed with respect to position. The input
1169      * parameters already take into account the free parameters (6 or 7 depending
1170      * on derivation with respect to mass being considered or not) and order
1171      * (always 1). Free parameters at indices 0, 1 and 2 correspond to derivatives
1172      * with respect to position. Free parameters at indices 3, 4 and 5 correspond
1173      * to derivatives with respect to velocity (these derivatives will remain zero
1174      * as acceleration due to gravity does not depend on velocity). Free parameter
1175      * at index 6 (if present) corresponds to to derivatives with respect to mass
1176      * (this derivative will remain zero as acceleration due to gravity does not
1177      * depend on mass).
1178      * </p>
1179      * @param date current date
1180      * @param frame inertial reference frame for state (both orbit and attitude)
1181      * @param position position of spacecraft in inertial frame
1182      * @param mu central attraction coefficient to use
1183      * @return acceleration with all derivatives specified by the input parameters
1184      * own derivatives
1185      * @since 10.2
1186      */
1187     private FieldVector3D<Gradient> accelerationWrtState(final AbsoluteDate date, final Frame frame,
1188                                                          final FieldVector3D<Gradient> position,
1189                                                          final Gradient mu) {
1190 
1191         // free parameters
1192         final int freeParameters = mu.getFreeParameters();
1193 
1194         // get the position in body frame
1195         final StaticTransform fromBodyFrame = bodyFrame.getStaticTransformTo(frame, date);
1196         final StaticTransform toBodyFrame   = fromBodyFrame.getInverse();
1197         final Vector3D positionBody   = toBodyFrame.transformPosition(position.toVector3D());
1198 
1199         // compute gradient and Hessian
1200         final GradientHessian gh   = gradientHessian(date, positionBody, mu.getReal());
1201 
1202         // gradient of the non-central part of the gravity field
1203         final double[] gInertial = fromBodyFrame.transformVector(new Vector3D(gh.getGradient())).toArray();
1204 
1205         // Hessian of the non-central part of the gravity field
1206         final RealMatrix hBody     = new Array2DRowRealMatrix(gh.getHessian(), false);
1207         final RealMatrix rot       = new Array2DRowRealMatrix(toBodyFrame.getRotation().getMatrix());
1208         final RealMatrix hInertial = rot.transposeMultiply(hBody).multiply(rot);
1209 
1210         // distribute all partial derivatives in a compact acceleration vector
1211         final double[] derivatives = new double[freeParameters];
1212         final Gradient[] accDer = new Gradient[3];
1213         for (int i = 0; i < 3; ++i) {
1214 
1215             // Jacobian of acceleration (i.e. Hessian of field)
1216             derivatives[0] = hInertial.getEntry(i, 0);
1217             derivatives[1] = hInertial.getEntry(i, 1);
1218             derivatives[2] = hInertial.getEntry(i, 2);
1219 
1220             // next element is derivative with respect to parameter mu
1221             if (derivatives.length > 3 && isVariable(mu, 3)) {
1222                 derivatives[3] = gInertial[i] / mu.getReal();
1223             }
1224 
1225             accDer[i] = new Gradient(gInertial[i], derivatives);
1226 
1227         }
1228 
1229         return new FieldVector3D<>(accDer);
1230 
1231     }
1232 
1233     /** {@inheritDoc} */
1234     public List<ParameterDriver> getParametersDrivers() {
1235         return Collections.singletonList(gmParameterDriver);
1236     }
1237 
1238 }