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.propagation.numerical;
18  
19  
20  import org.hipparchus.analysis.differentiation.Gradient;
21  import org.hipparchus.exception.LocalizedCoreFormats;
22  import org.hipparchus.linear.DecompositionSolver;
23  import org.hipparchus.linear.MatrixUtils;
24  import org.hipparchus.linear.QRDecomposition;
25  import org.hipparchus.linear.RealMatrix;
26  import org.hipparchus.util.Precision;
27  import org.orekit.attitudes.AttitudeProvider;
28  import org.orekit.attitudes.AttitudeProviderModifier;
29  import org.orekit.errors.OrekitException;
30  import org.orekit.forces.ForceModel;
31  import org.orekit.orbits.Orbit;
32  import org.orekit.orbits.OrbitParamsType;
33  import org.orekit.orbits.PositionAngleType;
34  import org.orekit.propagation.FieldSpacecraftState;
35  import org.orekit.propagation.SpacecraftState;
36  import org.orekit.propagation.integration.AdditionalDerivativesProvider;
37  import org.orekit.propagation.integration.CombinedDerivatives;
38  import org.orekit.utils.DataDictionary;
39  import org.orekit.utils.drivers.ParameterDriver;
40  
41  import java.io.IOException;
42  import java.io.ObjectInputStream;
43  import java.io.Serial;
44  import java.util.HashMap;
45  import java.util.List;
46  import java.util.Map;
47  import java.util.Objects;
48  
49  /** Abstract generator for numerical State Transition Matrix.
50   * @author Luc Maisonobe
51   * @author Melina Vanel
52   * @author Romain Serra
53   * @since 13.1
54   */
55  abstract class AbstractStateTransitionMatrixGenerator implements AdditionalDerivativesProvider {
56  
57      /** Space dimension. */
58      protected static final int SPACE_DIMENSION = 3;
59  
60      /** Threshold for matrix solving. */
61      private static final double THRESHOLD = Precision.SAFE_MIN;
62  
63      /** Name of the Cartesian STM additional state. */
64      private final String stmName;
65  
66      /** Force models used in propagation. */
67      private final List<ForceModel> forceModels;
68  
69      /** Attitude provider used in propagation. */
70      private final AttitudeProvider attitudeProvider;
71  
72      /** Observers for partial derivatives. */
73      private final Map<String, PartialsObserver> partialsObservers;
74  
75      /** Number of state variables. */
76      private final int stateDimension;
77  
78      /** Dimension of flatten STM. */
79      private final int dimension;
80  
81      /** Simple constructor.
82       * @param stmName name of the Cartesian STM additional state
83       * @param forceModels force models used in propagation
84       * @param attitudeProvider attitude provider used in propagation
85       * @param stateDimension dimension of state vector
86       */
87      AbstractStateTransitionMatrixGenerator(final String stmName, final List<ForceModel> forceModels,
88                                             final AttitudeProvider attitudeProvider, final int stateDimension) {
89          this.stmName           = stmName;
90          this.forceModels       = forceModels;
91          this.attitudeProvider  = attitudeProvider;
92          this.stateDimension    = stateDimension;
93          this.dimension         = stateDimension * stateDimension;
94          this.partialsObservers = new HashMap<>();
95      }
96  
97      /** Register an observer for partial derivatives.
98       * <p>
99       * The observer {@link PartialsObserver#partialsComputed(SpacecraftState, double[], double[])} partialsComputed}
100      * method will be called when partial derivatives are computed, as a side effect of
101      * calling {@link #computePartials(SpacecraftState)} (SpacecraftState)}
102      * </p>
103      * @param name name of the parameter driver this observer is interested in (may be null)
104      * @param observer observer to register
105      */
106     void addObserver(final String name, final PartialsObserver observer) {
107         partialsObservers.put(name, observer);
108     }
109 
110     /** {@inheritDoc} */
111     @Override
112     public String getName() {
113         return stmName;
114     }
115 
116     /** {@inheritDoc} */
117     @Override
118     public int getDimension() {
119         return dimension;
120     }
121 
122     /**
123      * Getter for the number of state variables.
124      * @return state vector dimension
125      */
126     public int getStateDimension() {
127         return stateDimension;
128     }
129 
130     /**
131      * Protected getter for the force models.
132      * @return forces
133      */
134     protected List<ForceModel> getForceModels() {
135         return forceModels;
136     }
137 
138     /**
139      * Protected getter for the partials observers map.
140      * @return map
141      */
142     protected Map<String, PartialsObserver> getPartialsObservers() {
143         return partialsObservers;
144     }
145 
146     /**
147      * Method to build a linear system solver.
148      * @param matrix equations matrix
149      * @return solver
150      */
151     private DecompositionSolver getDecompositionSolver(final RealMatrix matrix) {
152         return new QRDecomposition(matrix, THRESHOLD).getSolver();
153     }
154 
155     /** Set the initial value of the State Transition Matrix.
156      * <p>
157      * The returned state must be added to the propagator.
158      * </p>
159      * @param state initial state
160      * @param dYdY0 initial State Transition Matrix ∂Y/∂Y₀,
161      * if null (which is the most frequent case), assumed to be 6x6 identity
162      * @param orbitParamsType orbit type used for states Y and Y₀ in {@code dYdY0}
163      * @param positionAngleType position angle used states Y and Y₀ in {@code dYdY0}
164      * @return state with initial STM (converted to Cartesian ∂C/∂Y₀) added
165      */
166     SpacecraftState setInitialStateTransitionMatrix(final SpacecraftState state, final RealMatrix dYdY0,
167                                                     final OrbitParamsType orbitParamsType,
168                                                     final PositionAngleType positionAngleType) {
169 
170         final RealMatrix nonNullDYdY0;
171         if (dYdY0 == null) {
172             nonNullDYdY0 = MatrixUtils.createRealIdentityMatrix(getStateDimension());
173         } else {
174             if (dYdY0.getRowDimension() != getStateDimension() ||
175                     dYdY0.getColumnDimension() != getStateDimension()) {
176                 throw new OrekitException(LocalizedCoreFormats.DIMENSIONS_MISMATCH_2x2,
177                         dYdY0.getRowDimension(), dYdY0.getColumnDimension(),
178                         getStateDimension(), getStateDimension());
179             }
180             nonNullDYdY0 = dYdY0;
181         }
182 
183         // convert to Cartesian STM
184         final RealMatrix dCdY0;
185         if (state.isOrbitDefined()) {
186             final RealMatrix dYdC = MatrixUtils.createRealIdentityMatrix(getStateDimension());
187             final Orbit orbit = orbitParamsType.convertType(state.getOrbit());
188             final double[][] jacobian = new double[6][6];
189             orbit.getJacobianWrtCartesian(positionAngleType, jacobian);
190             dYdC.setSubMatrix(jacobian, 0, 0);
191             final DecompositionSolver decomposition = getDecompositionSolver(dYdC);
192             dCdY0 = decomposition.solve(nonNullDYdY0);
193         } else {
194             dCdY0 = nonNullDYdY0;
195         }
196 
197         // set additional state
198         return state.addAdditionalData(getName(), flatten(dCdY0));
199 
200     }
201 
202     /**
203      * Flattens a matrix into an 1-D array.
204      * @param matrix matrix to be flatten
205      * @return array
206      */
207     double[] flatten(final RealMatrix matrix) {
208         final double[] flat = new double[getDimension()];
209         int k = 0;
210         for (int i = 0; i < getStateDimension(); ++i) {
211             for (int j = 0; j < getStateDimension(); ++j) {
212                 flat[k++] = matrix.getEntry(i, j);
213             }
214         }
215         return flat;
216     }
217 
218     /** {@inheritDoc} */
219     @Override
220     public boolean yields(final SpacecraftState state) {
221         return !state.hasAdditionalData(getName());
222     }
223 
224     /** {@inheritDoc} */
225     public CombinedDerivatives combinedDerivatives(final SpacecraftState state) {
226         final double[] factor = computePartials(state);
227 
228         // retrieve current State Transition Matrix
229         final double[] p    = state.getAdditionalState(getName());
230         final double[] pDot = new double[p.length];
231 
232         // perform multiplication
233         multiplyMatrix(factor, p, pDot, getStateDimension());
234 
235         return new CombinedDerivatives(pDot, null);
236 
237     }
238 
239     /** Compute evolution matrix product.
240      * @param factor factor matrix
241      * @param x right factor of the multiplication, as a flatten array in row major order
242      * @param y placeholder where to put the result, as a flatten array in row major order
243      * @param columns number of columns of both x and y (so their dimensions are the state one times the columns)
244      */
245     abstract void multiplyMatrix(double[] factor, double[] x, double[] y, int columns);
246 
247     /** Compute the various partial derivatives.
248      * @param state current spacecraft state
249      * @return factor matrix
250      */
251     double[] computePartials(final SpacecraftState state) {
252 
253         // set up containers for partial derivatives
254         final double[]              factor               = new double[(stateDimension - SPACE_DIMENSION) * stateDimension];
255         final Map<String, double[]> partialsDictionary = new HashMap<>();
256 
257         // evaluate contribution of all force models
258         final AttitudeProvider equivalentAttitudeProvider = wrapAttitudeProviderIfPossible();
259         final NumericalGradientConverter posOnlyConverter = new NumericalGradientConverter(state, SPACE_DIMENSION, equivalentAttitudeProvider);
260         final NumericalGradientConverter fullConverter = buildFullConverter(state, equivalentAttitudeProvider, posOnlyConverter);
261         final SpacecraftState stateForParameters = state.withAdditionalData(new LocalDoubleArrayDictionary(state.getAdditionalDataValues()));
262 
263         for (final ForceModel forceModel : getForceModels()) {
264 
265             final NumericalGradientConverter     converter    = forceModel.dependsOnPositionOnly() ? posOnlyConverter : fullConverter;
266             final FieldSpacecraftState<Gradient> dsState      = converter.getState(forceModel);
267             final Gradient[]                     parameters   = converter.getParametersAtStateDate(dsState, forceModel);
268 
269             // update partial derivatives w.r.t. state variables
270             final Gradient[] ratesPartials = computeRatesPartialsAndUpdateFactor(forceModel, dsState, parameters, factor);
271 
272             // partials derivatives with respect to parameters
273             updateFactorForParameters(forceModel, converter, ratesPartials, partialsDictionary, stateForParameters, factor);
274 
275         }
276 
277         return factor;
278 
279     }
280 
281     /**
282      * Method building a gradient converter.
283      * @param state template
284      * @param provider attitude provider
285      * @param positionOnlyConverter default, already-built converter for position only
286      * @return gradient converter
287      * @since 13.1.8
288      */
289     private NumericalGradientConverter buildFullConverter(final SpacecraftState state, final AttitudeProvider provider,
290                                                           final NumericalGradientConverter positionOnlyConverter) {
291         if (getForceModels().stream().allMatch(ForceModel::dependsOnPositionOnly)) {
292             return positionOnlyConverter;
293         }
294         // check if additional data other than STM is stored
295         final boolean keepAdditionalData = state.getAdditionalDataValues().getData().stream().anyMatch(data -> !Objects.equals(data.getKey(), getName()));
296         return new NumericalGradientConverter(state, getStateDimension(), provider, keepAdditionalData);
297     }
298 
299     /**
300      * Compute with automatic differentiation the partial derivatives of state variables' rate
301      * that are not part of the position vector.
302      * @param forceModel force model
303      * @param fieldState state in Taylor differential algebra
304      * @param parameters force parameters in Taylor differential algebra
305      * @param factor factor matrix to update
306      * @return array of rates in Taylor differential algebra
307      */
308     abstract Gradient[] computeRatesPartialsAndUpdateFactor(ForceModel forceModel,
309                                                             FieldSpacecraftState<Gradient> fieldState,
310                                                             Gradient[] parameters, double[] factor);
311 
312     /**
313      * Update factor regarding partials of force model parameters.
314      * @param forceModel force
315      * @param converter gradient converter
316      * @param ratesPartials state variables' rates evaluated in the Taylor differential algebra
317      * @param partialsDictionary dictionary storing the partials
318      * @param state spacecraft state
319      * @param factor factor matrix (flattened)
320      */
321     private void updateFactorForParameters(final ForceModel forceModel, final NumericalGradientConverter converter,
322                                            final Gradient[] ratesPartials, final Map<String, double[]> partialsDictionary,
323                                            final SpacecraftState state, final double[] factor) {
324         int paramsIndex = converter.getFreeStateParameters();
325         for (ParameterDriver driver : forceModel.getParametersDrivers()) {
326             if (driver.isSelected()) {
327                 // add name for each estimated value
328                 updateDictionaryEntry(partialsDictionary, driver.getName(), ratesPartials, paramsIndex);
329                 ++paramsIndex;
330             }
331         }
332 
333         // notify observers
334         for (Map.Entry<String, PartialsObserver> observersEntry : getPartialsObservers().entrySet()) {
335             observersEntry.getValue().partialsComputed(state, factor,
336                     partialsDictionary.getOrDefault(observersEntry.getKey(), new double[ratesPartials.length]));
337         }
338     }
339 
340     /**
341      * Update entry of dictionary with derivative information.
342      * @param partialsDictionary dictionary
343      * @param name parameter name
344      * @param ratesPartials state variables' rates evaluated in the Taylor differential algebra
345      * @param paramsIndex index of parameter as an independent variable of the differential algebra
346      */
347     private void updateDictionaryEntry(final Map<String, double[]> partialsDictionary, final String name,
348                                        final Gradient[] ratesPartials, final int paramsIndex) {
349         // get the partials derivatives for this driver
350         partialsDictionary.putIfAbsent(name, new double[ratesPartials.length]);
351 
352         // add the contribution of the current force model
353         final double[] increment = partialsDictionary.get(name);
354         for (int i = 0; i < ratesPartials.length; ++i) {
355             increment[i] += ratesPartials[i].getGradient()[paramsIndex];
356         }
357         partialsDictionary.replace(name, increment);
358     }
359 
360     /**
361      * Method that first checks if it is possible to replace the attitude provider with a computationally cheaper one
362      * to evaluate. If applicable, the new provider only computes the rotation and uses dummy rate and acceleration,
363      * since they should not be used later on.
364      * @return same provider if at least one forces used attitude derivatives, otherwise one wrapping the old one for
365      * the rotation
366      */
367     AttitudeProvider wrapAttitudeProviderIfPossible() {
368         if (forceModels.stream().anyMatch(ForceModel::dependsOnAttitudeRate)) {
369             // at least one force uses an attitude rate, need to keep the original provider
370             return attitudeProvider;
371         } else {
372             // the original provider can be replaced by a lighter one for performance
373             return AttitudeProviderModifier.getFrozenAttitudeProvider(attitudeProvider);
374         }
375     }
376 
377     /** Interface for observing partials derivatives. */
378     @FunctionalInterface
379     public interface PartialsObserver {
380 
381         /** Callback called when partial derivatives have been computed.
382          * @param state current spacecraft state
383          * @param factor factor matrix, flattened along rows
384          * @param partials partials derivatives of all state variables' rates (except from position) w.r.t. the parameter driver
385          * that was registered (zero if no parameters were not selected or parameter is unknown)
386          */
387         void partialsComputed(SpacecraftState state, double[] factor, double[] partials);
388 
389     }
390 
391     /**
392      * Local override of data dictionary using HashMap for performance.
393      */
394     private static class LocalDoubleArrayDictionary extends DataDictionary {
395 
396         /** Serialization UID. */
397         @Serial
398         private static final long serialVersionUID = 1L;
399 
400         /** Map for quick access. */
401         private transient Map<String, Object> objectMap;
402 
403         /**
404          * Constructor.
405          * @param inputDictionary dictionary whose content is to reproduce
406          */
407         LocalDoubleArrayDictionary(final DataDictionary inputDictionary) {
408             super(inputDictionary);
409             objectMap = toMap();
410         }
411 
412         /**
413          * Deserializes the object from a stream and restores the transient fields.
414          *
415          * @param ois the input stream from which the object is read
416          * @throws IOException if an I/O error occurs during deserialization
417          * @throws ClassNotFoundException if the class of a serialized object cannot be found
418          */
419         @Serial
420         private void readObject(final ObjectInputStream ois) throws IOException, ClassNotFoundException {
421             ois.defaultReadObject();
422             objectMap = toMap();
423         }
424 
425         @Override
426         public Object get(final String key) {
427             return objectMap.get(key);
428         }
429     }
430 }
431