1   /* Copyright 2022-2026 Thales Alenia Space
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.time.clocks;
18  
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.HashMap;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.function.DoubleFunction;
25  
26  import org.hipparchus.CalculusFieldElement;
27  import org.hipparchus.Field;
28  import org.hipparchus.analysis.differentiation.Gradient;
29  import org.hipparchus.analysis.differentiation.GradientField;
30  import org.hipparchus.analysis.polynomials.PolynomialFunction;
31  import org.hipparchus.util.FastMath;
32  import org.hipparchus.util.MathArrays;
33  import org.orekit.errors.OrekitException;
34  import org.orekit.errors.OrekitMessages;
35  import org.orekit.time.AbsoluteDate;
36  import org.orekit.time.FieldAbsoluteDate;
37  import org.orekit.time.TimeInterval;
38  import org.orekit.utils.drivers.ParameterDriver;
39  
40  /** Polynomial clock model.
41   *
42   * @author Luc Maisonobe
43   * @since 12.1
44   *
45   */
46  public class PolynomialClockModel implements ClockModel {
47  
48      /**
49       * Clock offset scaling factor.
50       * <p>
51       * We use a power of 2 to avoid numeric noise introduction
52       * in the multiplications/divisions sequences.
53       * </p>
54       */
55      private static final double CLOCK_OFFSET_SCALE = FastMath.scalb(1.0, -10);
56  
57      /** Prefix for parameters names. */
58      private final String prefix;
59  
60      /** List of terms. */
61      private final List<ParameterDriver> terms;
62  
63      /** Cached field-based models.
64       * @since 14.0
65       */
66      private final Map<Field<? extends CalculusFieldElement<?>>, PolynomialFieldClockModel<?>> fieldModels;
67  
68      /** Simple constructor.
69       * @param referenceDate reference date (may be null)
70       * @param prefix        prefix for the parameter names
71       */
72      public PolynomialClockModel(final AbsoluteDate referenceDate,
73                                  final String prefix) {
74          this(referenceDate, prefix, 0.0);
75      }
76  
77      /** Simple constructor.
78       * @param referenceDate reference date (may be null if there is only one term)
79       * @param prefix        prefix for the parameter names
80       * @param terms         the polynomial terms in order.
81       */
82      public PolynomialClockModel(final AbsoluteDate referenceDate, final String prefix,
83                                  final double... terms) {
84  
85          this.prefix = prefix;
86  
87          // set up a safe reference date
88          final AbsoluteDate safeReferenceDate;
89          if (referenceDate == null) {
90              if (terms.length == 1) {
91                  // we accept null reference date for constant polynomials
92                  // we just change it to arbitrary to avoid null pointer exceptions
93                  safeReferenceDate = AbsoluteDate.ARBITRARY_EPOCH;
94              } else {
95                  throw new OrekitException(OrekitMessages.NO_REFERENCE_DATE_FOR_PARAMETER,
96                                            prefix + getAcceptedTermSuffix(0));
97              }
98          } else {
99              safeReferenceDate = referenceDate;
100         }
101 
102         final List<ParameterDriver> convertedTerms = new ArrayList<>();
103         for (int i = 0; i < terms.length; ++i) {
104             final String name = prefix + getAcceptedTermSuffix(i);
105             final ParameterDriver parameterTerm =
106                 new ParameterDriver(name, 0.0, CLOCK_OFFSET_SCALE,
107                                     Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY,
108                                     TimeInterval.UNLIMITED);
109             parameterTerm.setValue(terms[i]);
110             parameterTerm.setReferenceDate(safeReferenceDate);
111             convertedTerms.add(parameterTerm);
112         }
113         this.terms = convertedTerms;
114 
115         this.fieldModels = new HashMap<>();
116 
117     }
118 
119     /**
120      * Simple constructor.
121      * <p>
122      * The reference date for computing the polynom is the reference
123      * date of the first parameter driver.
124      * </p>
125      * @param terms the parameter driver terms (they must have names that end
126      *              with the {@link #getAcceptedTermSuffix(int) accepted suffixes})
127      */
128     public PolynomialClockModel(final ParameterDriver... terms) {
129 
130         // extract prefix
131         if (terms.length == 0) {
132             prefix = "";
133         } else {
134             final String firstName   = terms[0].getName();
135             final String firstSuffix = getAcceptedTermSuffix(0);
136             if (firstName.endsWith(firstSuffix)) {
137                 prefix = firstName.substring(0, firstName.length() - firstSuffix.length());
138             } else {
139                 // there will be an error triggered in the following loop
140                 prefix = firstName;
141             }
142         }
143 
144         int idx = 0;
145         for (final ParameterDriver term : terms) {
146             final String acceptedName = prefix + getAcceptedTermSuffix(idx);
147             if (!term.getName().equals(acceptedName)) {
148                 throw new OrekitException(OrekitMessages.UNSUPPORTED_PARAMETER_NAME,
149                                           term.getName(), acceptedName);
150             }
151             idx++;
152         }
153         this.terms  = Arrays.asList(terms);
154 
155         this.fieldModels = new HashMap<>();
156 
157     }
158 
159     /**
160      * Simple constructor.
161      *
162      * @param terms the parameter driver terms
163      */
164     public PolynomialClockModel(final List<ParameterDriver> terms) {
165         this(terms.toArray(new ParameterDriver[0]));
166     }
167 
168     /** {@inheritDoc}
169      * <p>
170      * Validity is extracted from the first parameter driver.
171      * </p>
172      */
173     @Override
174     public AbsoluteDate getValidityStart() {
175         return terms.getFirst().getValidity().getStartDate();
176     }
177 
178     /** {@inheritDoc}
179      * <p>
180      * Validity is extracted from the first parameter driver.
181      * </p>
182      */
183     @Override
184     public AbsoluteDate getValidityEnd() {
185         return terms.getFirst().getValidity().getEndDate();
186     }
187 
188     /** {@inheritDoc} */
189     @Override
190     public List<ParameterDriver> getParametersDrivers() {
191         return terms;
192     }
193 
194     /** Add a parameter driver term in a given index to the list of parameters.
195      * If parameters prior to the one requested don't exist, it will create empty ones.
196      * This allows adding a velocity, acceleration, or above without explicitly defining the terms below.
197      *
198      * @param index the index at which to add the parameter driver
199      * @param driver the parameter driver to add
200      */
201     public void addParameterDriver(final int index, final ParameterDriver driver) {
202         final List<ParameterDriver> parameters = getParametersDrivers();
203         if (parameters.size() < index) {
204             // Recursively add empty parameters to fill gaps
205             addParameterDriver(index - 1, null);
206             // After filling gaps, add the driver at the target index
207             addParameterDriver(index, driver);
208         } else if (parameters.size() == index) {
209             if (driver != null) {
210                 // Add the driver at the correct index
211                 parameters.add(driver);
212             } else {
213                 // Create empty parameter with correct name for this index
214                 final ParameterDriver empty = new ParameterDriver(prefix + getAcceptedTermSuffix(index),
215                                                                   0.0, CLOCK_OFFSET_SCALE,
216                                                                   Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY,
217                                                                   TimeInterval.UNLIMITED);
218                 empty.setReferenceDate(AbsoluteDate.ARBITRARY_EPOCH);
219                 parameters.add(empty);
220             }
221         }
222     }
223 
224     /** {@inheritDoc} */
225     @Override
226     public ClockOffset getOffset(final AbsoluteDate date) {
227         final double[] result = new double[3];
228         if (terms.isEmpty()) {
229             return new ClockOffset(date, result);
230         }
231 
232         final AbsoluteDate referenceDate = terms.getFirst().getReferenceDate();
233         final double dt;
234         if (referenceDate == null) {
235             if (terms.size() == 1) {
236                 // this is a constant polynomial, we accept a missing reference date
237                 dt = 0;
238             } else {
239                 throw new OrekitException(OrekitMessages.NO_REFERENCE_DATE_FOR_PARAMETER,
240                                           terms.getFirst().getName());
241             }
242         } else {
243             dt = date.durationFrom(referenceDate);
244         }
245 
246         final double[] convertedTerms = terms.
247                                         stream().
248                                         map(ParameterDriver::getValue).
249                                         mapToDouble(Double::doubleValue).
250                                         toArray();
251         // Turn the terms into a polynomial function
252         PolynomialFunction function = new PolynomialFunction(convertedTerms);
253         // Loop over all of the terms in order
254         for (int ii = 0; ii < 3; ii++) {
255             result[ii] = function.value(dt);
256             function = function.polynomialDerivative();
257         }
258         return new ClockOffset(date, result);
259     }
260 
261     /** {@inheritDoc} */
262     @Override
263     public <T extends CalculusFieldElement<T>> PolynomialFieldClockModel<T> toField(final DoubleFunction<T> converter) {
264         // build aggregated models may be costly, so we cache the results
265         return (PolynomialFieldClockModel<T>) fieldModels.computeIfAbsent(converter.apply(0.0).getField(),
266                                                                           f -> buildFieldModel(converter));
267     }
268 
269     /**
270      * Build a field model.
271      * @param <T> type of the field elements
272      * @param converter converter to field elements
273      * @return field version of the instance
274      * @since 14.0
275      */
276     private <T extends CalculusFieldElement<T>> PolynomialFieldClockModel<T> buildFieldModel(final DoubleFunction<T> converter) {
277 
278         final Field<T> field = converter.apply(0.0).getField();
279 
280         // handle special case
281         if (terms.isEmpty()) {
282             return new PolynomialFieldClockModel<>(FieldAbsoluteDate.getArbitraryEpoch(field), "");
283         }
284 
285         // convert reference date
286         if (terms.size() > 1 && terms.getFirst().getReferenceDate() == null) {
287             throw new OrekitException(OrekitMessages.NO_REFERENCE_DATE_FOR_PARAMETER,
288                                       terms.getFirst().getName());
289         }
290         final FieldAbsoluteDate<T> fieldDate =
291             new FieldAbsoluteDate<>(field, terms.getFirst().getReferenceDate());
292 
293         // convert polynomial coefficients
294         final T[] fieldTerms = MathArrays.buildArray(field, terms.size());
295         for (int i = 0; i < terms.size(); ++i) {
296             fieldTerms[i] = converter.apply(terms.get(i).getValue());
297         }
298 
299         return new PolynomialFieldClockModel<>(fieldDate, prefix, fieldTerms);
300 
301     }
302 
303     /** {@inheritDoc} */
304     @Override
305     public PolynomialFieldClockModel<Gradient> toGradient(final int freeParameters, final Map<String, Integer> indices) {
306 
307         final GradientField field = GradientField.getField(freeParameters);
308 
309         // handle special case
310         if (terms.isEmpty()) {
311             return new PolynomialFieldClockModel<>(FieldAbsoluteDate.getArbitraryEpoch(field), "");
312         }
313 
314         // convert reference date
315         if (terms.size() > 1 && terms.getFirst().getReferenceDate() == null) {
316             throw new OrekitException(OrekitMessages.NO_REFERENCE_DATE_FOR_PARAMETER,
317                                       terms.getFirst().getName());
318         }
319         final FieldAbsoluteDate<Gradient> fieldDate =
320             new FieldAbsoluteDate<>(field, terms.getFirst().getReferenceDate());
321 
322         // convert polynomial coefficients
323         final Gradient[] fieldTerms = new Gradient[terms.size()];
324         for (int i = 0; i < terms.size(); ++i) {
325             fieldTerms[i] = terms.get(i).getValue(freeParameters, indices);
326         }
327 
328         return new PolynomialFieldClockModel<>(fieldDate, prefix, fieldTerms);
329 
330     }
331 
332 }