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.estimation.leastsquares;
18  
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collections;
22  import java.util.HashMap;
23  import java.util.IdentityHashMap;
24  import java.util.List;
25  import java.util.Map;
26  
27  import org.hipparchus.linear.Array2DRowRealMatrix;
28  import org.hipparchus.linear.ArrayRealVector;
29  import org.hipparchus.linear.MatrixUtils;
30  import org.hipparchus.linear.RealMatrix;
31  import org.hipparchus.linear.RealVector;
32  import org.hipparchus.optim.nonlinear.vector.leastsquares.MultivariateJacobianFunction;
33  import org.hipparchus.util.FastMath;
34  import org.hipparchus.util.Incrementor;
35  import org.hipparchus.util.Pair;
36  import org.orekit.estimation.measurements.EstimatedMeasurement;
37  import org.orekit.estimation.measurements.EstimatedMeasurementBase;
38  import org.orekit.estimation.measurements.ObservedMeasurement;
39  import org.orekit.orbits.Orbit;
40  import org.orekit.propagation.MatricesHarvester;
41  import org.orekit.propagation.Propagator;
42  import org.orekit.propagation.PropagatorsParallelizer;
43  import org.orekit.propagation.SpacecraftState;
44  import org.orekit.propagation.conversion.PropagatorBuilder;
45  import org.orekit.propagation.sampling.MultiSatStepHandler;
46  import org.orekit.time.AbsoluteDate;
47  import org.orekit.time.ChronologicalComparator;
48  import org.orekit.utils.drivers.ParameterDriver;
49  import org.orekit.utils.drivers.ParameterDriversList;
50  import org.orekit.utils.drivers.ParameterDriversList.DelegatingDriver;
51  
52  /** Bridge between {@link ObservedMeasurement measurements} and {@link
53   * org.hipparchus.optim.nonlinear.vector.leastsquares.LeastSquaresProblem
54   * least squares problems}.
55   * @author Luc Maisonobe
56   * @author Bryan Cazabonne
57   * @author Thomas Paulet
58   * @author Melina Vanel
59   * @since 11.0
60   */
61  public abstract class AbstractBatchLSModel implements MultivariateJacobianFunction {
62  
63      /** Builders for propagators. */
64      private final PropagatorBuilder[] builders;
65  
66      /** Array of each builder's selected orbit drivers. Orbit drivers
67       * should have only 1 span on their value TimeSpanMap.
68       * @since 11.1
69       */
70      private final ParameterDriversList[] estimatedOrbitalParameters;
71  
72      /** Array of each builder's selected propagation drivers. */
73      private final ParameterDriversList[] estimatedPropagationParameters;
74  
75      /** Estimated measurements parameters. */
76      private final ParameterDriversList estimatedMeasurementsParameters;
77  
78      /** Measurements. */
79      private final List<ObservedMeasurement<?>> measurements;
80  
81      /** Start columns for each estimated orbit. */
82      private final int[] orbitsStartColumns;
83  
84      /** End columns for each estimated orbit. */
85      private final int[] orbitsEndColumns;
86  
87      /** Indirection array in measurements jacobians.
88       * @since 11.2
89       */
90      private final int[] orbitsJacobianColumns;
91  
92      /** Map for propagation parameters columns. */
93      private final Map<String, Integer> propagationParameterColumns;
94  
95      /** Map for measurements parameters columns. */
96      private final Map<String, Integer> measurementParameterColumns;
97  
98      /** Last evaluations. */
99      private final Map<ObservedMeasurement<?>, EstimatedMeasurement<?>> evaluations;
100 
101     /** Observer to be notified at orbit changes. */
102     private final ModelObserver observer;
103 
104     /** Counter for the evaluations. */
105     private Incrementor evaluationsCounter;
106 
107     /** Counter for the iterations. */
108     private Incrementor iterationsCounter;
109 
110     /** Date of the first enabled measurement. */
111     private AbsoluteDate firstDate;
112 
113     /** Date of the last enabled measurement. */
114     private AbsoluteDate lastDate;
115 
116     /** Boolean indicating if the propagation will go forward or backward. */
117     private final boolean forwardPropagation;
118 
119     /** Model function value. */
120     private final RealVector value;
121 
122     /** Harvesters for extracting State Transition Matrices and Jacobians from integrated states.
123      * @since 11.1
124      */
125     private final MatricesHarvester[] harvesters;
126 
127     /** Initial states of the propagators built at the current iteration.
128      * <p>
129      * These are the states the builder parameters refer to, hence the states at which the
130      * {@link MatricesHarvester#getStateJacobianVsBuilderParameters(SpacecraftState) dY₀/dB₀} Jacobians must be
131      * evaluated.
132      * </p>
133      * @since 14.0
134      */
135     private final SpacecraftState[] initialStates;
136 
137     /** Model function Jacobian. */
138     private final RealMatrix jacobian;
139 
140     /**
141      * Constructor.
142      * @param propagatorBuilders builders to use for propagation
143      * @param measurements measurements
144      * @param estimatedMeasurementsParameters estimated measurements parameters
145      * @param observer observer to be notified at model calls
146      */
147     protected AbstractBatchLSModel(final PropagatorBuilder[] propagatorBuilders,
148                                    final List<ObservedMeasurement<?>> measurements,
149                                    final ParameterDriversList estimatedMeasurementsParameters,
150                                    final ModelObserver observer) {
151 
152         this.builders                        = propagatorBuilders.clone();
153         this.measurements                    = measurements;
154         this.estimatedMeasurementsParameters = estimatedMeasurementsParameters;
155         this.measurementParameterColumns     = new HashMap<>(estimatedMeasurementsParameters.getNbParams());
156         this.estimatedOrbitalParameters      = new ParameterDriversList[builders.length];
157         this.estimatedPropagationParameters  = new ParameterDriversList[builders.length];
158         this.evaluations                     = new IdentityHashMap<>(measurements.size());
159         this.observer                        = observer;
160         this.harvesters                      = new MatricesHarvester[builders.length];
161         this.initialStates                   = new SpacecraftState[builders.length];
162 
163         // allocate vector and matrix
164         int rows = 0;
165         for (final ObservedMeasurement<?> measurement : measurements) {
166             rows += measurement.getDimension();
167         }
168 
169         this.orbitsStartColumns    = new int[builders.length];
170         this.orbitsEndColumns      = new int[builders.length];
171         this.orbitsJacobianColumns = new int[builders.length * 6];
172         Arrays.fill(orbitsJacobianColumns, -1);
173         int columns = 0;
174         for (int i = 0; i < builders.length; ++i) {
175             this.orbitsStartColumns[i] = columns;
176             final List<ParameterDriversList.DelegatingDriver> orbitalParametersDrivers =
177                             builders[i].getOrbitalStateFactory().getOrbitalParametersDrivers().getDrivers();
178             for (int j = 0; j < orbitalParametersDrivers.size(); ++j) {
179                 if (orbitalParametersDrivers.get(j).isSelected()) {
180                     orbitsJacobianColumns[columns] = j;
181                     ++columns;
182                 }
183             }
184             this.orbitsEndColumns[i] = columns;
185         }
186 
187         // Gather all the propagation drivers names in a list
188         final List<String> estimatedPropagationParametersNames = new ArrayList<>();
189         for (int i = 0; i < builders.length; ++i) {
190             // The index i in array estimatedPropagationParameters (attribute of the class) is populated
191             // when the first call to getSelectedPropagationDriversForBuilder(i) is made
192             for (final DelegatingDriver delegating : getSelectedPropagationDriversForBuilder(i).getDrivers()) {
193 
194                 // Add the driver name if it has not been added yet and the number of estimated values for this param
195                 if (!estimatedPropagationParametersNames.contains(delegating.getName())) {
196                     estimatedPropagationParametersNames.add(delegating.getName());
197                 }
198             }
199         }
200 
201         // Populate the map of propagation drivers' columns and update the total number of columns
202         propagationParameterColumns = new HashMap<>(estimatedPropagationParametersNames.size());
203         for (final String driverName : estimatedPropagationParametersNames) {
204             propagationParameterColumns.put(driverName, columns);
205             ++columns;
206         }
207         // Populate the map of measurement drivers' columns and update the total number of columns
208         for (final ParameterDriver parameter : estimatedMeasurementsParameters.getDrivers()) {
209             measurementParameterColumns.put(parameter.getName(), columns);
210             columns++;
211         }
212 
213         // Initialize point and value
214         value    = new ArrayRealVector(rows);
215         jacobian = MatrixUtils.createRealMatrix(rows, columns);
216 
217         // Decide whether the propagation will be done forward or backward.
218         // Minimize the duration between first measurement treated and orbit determination date
219         // Propagator builder number 0 holds the reference date for orbit determination
220         final AbsoluteDate refDate = builders[0].getOrbitalStateFactory().getDate();
221 
222         // Sort the measurement list chronologically
223         measurements.sort(new ChronologicalComparator());
224         firstDate = measurements.getFirst().getDate();
225         lastDate  = measurements.getLast().getDate();
226 
227         // Decide the direction of propagation
228         forwardPropagation = FastMath.abs(refDate.durationFrom(firstDate)) <= FastMath.abs(refDate.durationFrom(lastDate));
229     }
230 
231     /** Set the counter for evaluations.
232      * @param evaluationsCounter counter for evaluations
233      */
234     public void setEvaluationsCounter(final Incrementor evaluationsCounter) {
235         this.evaluationsCounter = evaluationsCounter;
236     }
237 
238     /** Set the counter for iterations.
239      * @param iterationsCounter counter for iterations
240      */
241     public void setIterationsCounter(final Incrementor iterationsCounter) {
242         this.iterationsCounter = iterationsCounter;
243     }
244 
245     /** Return the forward propagation flag.
246      * @return the forward propagation flag
247      */
248     public boolean isForwardPropagation() {
249         return forwardPropagation;
250     }
251 
252     /** Configure the propagator to compute derivatives.
253      * @param propagator {@link Propagator} to configure
254      * @return harvester harvester to retrive the State Transition Matrix and Jacobian Matrix
255      */
256     protected abstract MatricesHarvester configureHarvester(Propagator propagator);
257 
258     /** Configure the current estimated orbits.
259      * <p>
260      * For DSST orbit determination, short period derivatives are also calculated.
261      * </p>
262      * @param harvester harvester for matrices
263      * @param propagator the orbit propagator
264      * @return the current estimated orbits
265      */
266     protected abstract Orbit configureOrbits(MatricesHarvester harvester, Propagator propagator);
267 
268     /** {@inheritDoc} */
269     @Override
270     public Pair<RealVector, RealMatrix> value(final RealVector point) {
271 
272         // Set up the propagators parallelizer
273         final Propagator[] propagators = createPropagators(point);
274         final Orbit[] orbits = new Orbit[propagators.length];
275         for (int i = 0; i < propagators.length; ++i) {
276             harvesters[i] = configureHarvester(propagators[i]);
277             orbits[i]     = configureOrbits(harvesters[i], propagators[i]);
278             // base state is used here on purpose: retrieving the full initial state would
279             // trigger the additional data providers, hence the harvester derivatives cache
280             initialStates[i] = propagators[i].getBaseInitialState();
281         }
282         final PropagatorsParallelizer parallelizer =
283                         new PropagatorsParallelizer(Arrays.asList(propagators), configureMeasurements(point));
284 
285         // Reset value and Jacobian
286         evaluations.clear();
287         value.set(0.0);
288         for (int i = 0; i < jacobian.getRowDimension(); ++i) {
289             for (int j = 0; j < jacobian.getColumnDimension(); ++j) {
290                 jacobian.setEntry(i, j, 0.0);
291             }
292         }
293 
294         // Run the propagation, gathering residuals on the fly
295         if (isForwardPropagation()) {
296             // Propagate forward from firstDate
297             parallelizer.propagate(firstDate.shiftedBy(-1.0), lastDate.shiftedBy(+1.0));
298         } else {
299             // Propagate backward from lastDate
300             parallelizer.propagate(lastDate.shiftedBy(+1.0), firstDate.shiftedBy(-1.0));
301         }
302 
303         observer.modelCalled(orbits, evaluations);
304 
305         return new Pair<>(value, jacobian);
306 
307     }
308 
309     /** Get the selected orbital drivers for a propagatorBuilder.
310      * @param iBuilder index of the builder in the builders' array
311      * @return the list of selected orbital drivers for propagatorBuilder of index iBuilder
312      * @since 11.1
313      */
314     public ParameterDriversList getSelectedOrbitalParametersDriversForBuilder(final int iBuilder) {
315 
316         // Lazy evaluation, create the list only if it hasn't been created yet
317         if (estimatedOrbitalParameters[iBuilder] == null) {
318 
319             // Gather the drivers
320             final ParameterDriversList drivers = builders[iBuilder].
321                     getOrbitalStateFactory().
322                                                  getOrbitalParametersDrivers();
323             final ParameterDriversList selectedOrbitalDrivers = new ParameterDriversList();
324             for (final DelegatingDriver delegating : drivers.getDrivers()) {
325                 if (delegating.isSelected()) {
326                     for (final ParameterDriver driver : delegating.getRawDrivers()) {
327                         selectedOrbitalDrivers.add(driver);
328                     }
329                 }
330             }
331 
332             // Add the list of selected orbital parameters drivers to the array
333             estimatedOrbitalParameters[iBuilder] = selectedOrbitalDrivers;
334         }
335         return estimatedOrbitalParameters[iBuilder];
336     }
337 
338     /** Get the selected propagation drivers for a propagatorBuilder.
339      * @param iBuilder index of the builder in the builders' array
340      * @return the list of selected propagation drivers for propagatorBuilder of index iBuilder
341      */
342     public ParameterDriversList getSelectedPropagationDriversForBuilder(final int iBuilder) {
343 
344         // Lazy evaluation, create the list only if it hasn't been created yet
345         if (estimatedPropagationParameters[iBuilder] == null) {
346 
347             // Gather the drivers
348             final ParameterDriversList selectedPropagationDrivers = new ParameterDriversList();
349             for (final DelegatingDriver delegating : builders[iBuilder].getPropagationParametersDrivers().getDrivers()) {
350                 if (delegating.isSelected()) {
351                     for (final ParameterDriver driver : delegating.getRawDrivers()) {
352                         selectedPropagationDrivers.add(driver);
353                     }
354                 }
355             }
356 
357             // List of propagation drivers are sorted in the BatchLSEstimator class.
358             // Hence we need to sort this list so the parameters' indexes match
359             selectedPropagationDrivers.sort();
360 
361             // Add the list of selected propagation drivers to the array
362             estimatedPropagationParameters[iBuilder] = selectedPropagationDrivers;
363         }
364         return estimatedPropagationParameters[iBuilder];
365     }
366 
367     /** Create the propagators and parameters corresponding to an evaluation point.
368      * @param point evaluation point
369      * @return an array of new propagators
370      */
371     public Propagator[] createPropagators(final RealVector point) {
372 
373         final Propagator[] propagators = new Propagator[builders.length];
374 
375 
376         // Set up the propagators
377         for (int i = 0; i < builders.length; ++i) {
378 
379             int element = 0;
380             // Get the number of values to estimate for selected orbital drivers in the builder
381             final int nbOrb    = orbitsEndColumns[i] - orbitsStartColumns[i];
382 
383             // Get the list of selected propagation drivers in the builder and its size
384             final ParameterDriversList selectedPropagationDrivers = getSelectedPropagationDriversForBuilder(i);
385             final int nbParams = selectedPropagationDrivers.getNbParams();
386             final int nbValuesToEstimate = selectedPropagationDrivers.getNbParams();
387 
388             // Init the array of normalized parameters for the builder
389             final double[] propagatorArray = new double[nbOrb + nbValuesToEstimate];
390 
391             // Add the orbital drivers normalized values
392             for (int j = 0; j < nbOrb; ++j) {
393                 propagatorArray[element++] = point.getEntry(orbitsStartColumns[i] + j);
394             }
395 
396             // Add the propagation drivers normalized values
397             for (int j = 0; j < nbParams; ++j) {
398                 final DelegatingDriver driver = selectedPropagationDrivers.getDrivers().get(j);
399                 propagatorArray[element++] = point.getEntry(propagationParameterColumns.get(driver.getName()));
400             }
401 
402             // Build the propagator
403             propagators[i] = builders[i].buildPropagator(propagatorArray);
404         }
405 
406         return propagators;
407 
408     }
409 
410     /** Fetch a measurement that was evaluated during propagation.
411      * @param index index of the measurement first component
412      * @param evaluation measurement evaluation
413      */
414     public void fetchEvaluatedMeasurement(final int index, final EstimatedMeasurement<?> evaluation) {
415 
416         // States and observed measurement
417         final SpacecraftState[]      evaluationStates    = evaluation.getStates();
418         final ObservedMeasurement<?> observedMeasurement = evaluation.getObservedMeasurement();
419 
420         // compute weighted residuals
421         evaluations.put(observedMeasurement, evaluation);
422         if (evaluation.getStatus() == EstimatedMeasurementBase.Status.REJECTED) {
423             return;
424         }
425 
426         final double[] evaluated = evaluation.getEstimatedValue();
427         final double[] observed  = observedMeasurement.getObservedValue();
428         final double[] sigma     = observedMeasurement.getTheoreticalStandardDeviation();
429         final double[] weight    = evaluation.getObservedMeasurement().getBaseWeight();
430         for (int i = 0; i < evaluated.length; ++i) {
431             value.setEntry(index + i, weight[i] * (evaluated[i] - observed[i]) / sigma[i]);
432         }
433 
434         for (int k = 0; k < evaluationStates.length; ++k) {
435 
436             final int p = observedMeasurement.getSatellites().get(k).getPropagatorIndex();
437 
438             // partial derivatives of the current Cartesian coordinates with respect to current orbital state
439             final double[][] aCY = new double[6][6];
440             final Orbit currentOrbit = evaluationStates[k].getOrbit();
441             currentOrbit.getJacobianWrtParameters(builders[p].getOrbitalStateFactory().getPositionAngleType(),
442                                                   aCY);
443             final RealMatrix dCdY = new Array2DRowRealMatrix(aCY, false);
444 
445             // Jacobian of the measurement with respect to current orbital state
446             final RealMatrix dMdC = new Array2DRowRealMatrix(evaluation.getStateDerivatives(k), false);
447             final RealMatrix dMdY = dMdC.multiply(dCdY);
448 
449             // Jacobian of the measurement with respect to initial orbital state
450             final ParameterDriversList selectedOrbitalDrivers = getSelectedOrbitalParametersDriversForBuilder(p);
451             final int nbOrbParams = selectedOrbitalDrivers.getNbParams();
452             if (nbOrbParams > 0) {
453                 RealMatrix dYdY0 = harvesters[p].getStateTransitionMatrix(evaluationStates[k]);
454                 if (dYdY0.getRowDimension() == 7) {
455                     // mass was included in STM propagation, removed it now
456                     dYdY0 = dYdY0.getSubMatrix(0, 5, 0, 5);
457                 }
458                 final RealMatrix dMdY0  = dMdY.multiply(dYdY0);
459                 final RealMatrix dY0dB0 = harvesters[p].getStateJacobianVsBuilderParameters(initialStates[p]);
460                 final RealMatrix dMdB0  = dY0dB0 == null ? dMdY0 : dMdY0.multiply(dY0dB0);
461                 for (int i = 0; i < dMdB0.getRowDimension(); ++i) {
462                     for (int j = orbitsStartColumns[p]; j < orbitsEndColumns[p]; ++j) {
463                         final ParameterDriver driver =
464                                         selectedOrbitalDrivers.getDrivers().get(j - orbitsStartColumns[p]);
465                         final double partial = dMdB0.getEntry(i, orbitsJacobianColumns[j]);
466                         jacobian.setEntry(index + i, j,
467                                           weight[i] * partial / sigma[i] * driver.getScale());
468                     }
469                 }
470             }
471 
472             // Jacobian of the measurement with respect to propagation parameters
473             final ParameterDriversList selectedPropagationDrivers = getSelectedPropagationDriversForBuilder(p);
474             final int nbParams = selectedPropagationDrivers.getNbParams();
475             if (nbParams > 0) {
476                 RealMatrix dYdPp = harvesters[p].getParametersJacobian(evaluationStates[k]);
477                 if (dYdPp.getRowDimension() == 7) {
478                     // mass was included in STM propagation, removed it now
479                     dYdPp = dYdPp.getSubMatrix(0, 5, 0, dYdPp.getColumnDimension() - 1);
480                 }
481                 final RealMatrix dMdPp = dMdY.multiply(dYdPp);
482 
483                 for (int i = 0; i < dMdPp.getRowDimension(); ++i) {
484                     int col = 0;
485 
486                     // Add the propagation drivers normalized values
487                     for (int j = 0; j < nbParams; ++j) {
488                         final ParameterDriver delegating = selectedPropagationDrivers.getDrivers().get(j);
489                         jacobian.addToEntry(index + i, propagationParameterColumns.get(delegating.getName()),
490                                     weight[i] * dMdPp.getEntry(i, col++) / sigma[i] * delegating.getScale());
491                     }
492                 }
493             }
494         }
495         // Jacobian of the measurement with respect to measurements parameters
496         for (final ParameterDriver driver : observedMeasurement.getParametersDrivers()) {
497             if (driver.isSelected()) {
498                 final double[] aMPm = evaluation.getParameterDerivatives(driver);
499                 for (int i = 0; i < aMPm.length; ++i) {
500                     jacobian.setEntry(index + i, measurementParameterColumns.get(driver.getName()),
501                                       weight[i] * aMPm[i] / sigma[i] * driver.getScale());
502                 }
503             }
504         }
505 
506     }
507 
508     /** Configure the multi-satellites handler to handle measurements.
509      * @param point evaluation point
510      * @return multi-satellites handler to handle measurements
511      */
512     private MultiSatStepHandler configureMeasurements(final RealVector point) {
513 
514         // Set up the measurement parameters
515         int index = orbitsEndColumns[builders.length - 1] + propagationParameterColumns.size();
516         for (final ParameterDriver parameter : estimatedMeasurementsParameters.getDrivers()) {
517             parameter.setNormalizedValue(point.getEntry(index++));
518         }
519 
520         // Set up measurements handler
521         final List<PreCompensation> precompensated = new ArrayList<>();
522         for (final ObservedMeasurement<?> measurement : measurements) {
523             if (measurement.isEnabled()) {
524                 precompensated.add(new PreCompensation(measurement, evaluations.get(measurement)));
525             }
526         }
527         precompensated.sort(new ChronologicalComparator());
528 
529         // Assign first and last date
530         firstDate = precompensated.getFirst().getDate();
531         lastDate  = precompensated.getLast().getDate();
532 
533         // Reverse the list in case of backward propagation
534         if (!forwardPropagation) {
535             Collections.reverse(precompensated);
536         }
537 
538         return new MeasurementHandler(this, precompensated);
539 
540     }
541 
542     /** Get the iterations count.
543      * @return iterations count
544      */
545     public int getIterationsCount() {
546         return iterationsCounter.getCount();
547     }
548 
549     /** Get the evaluations count.
550      * @return evaluations count
551      */
552     public int getEvaluationsCount() {
553         return evaluationsCounter.getCount();
554     }
555 
556 }