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.List;
22  import java.util.Map;
23  
24  import org.hipparchus.CalculusFieldElement;
25  import org.hipparchus.analysis.differentiation.Gradient;
26  import org.hipparchus.analysis.polynomials.PolynomialFunction;
27  import org.hipparchus.util.FastMath;
28  import org.orekit.errors.OrekitException;
29  import org.orekit.errors.OrekitMessages;
30  import org.orekit.time.AbsoluteDate;
31  import org.orekit.time.FieldAbsoluteDate;
32  import org.orekit.utils.ParameterDriver;
33  
34  /** Polynomial clock model.
35   *
36   * @author Luc Maisonobe
37   * @since 12.1
38   *
39   */
40  public class PolynomialClockModel implements ClockModel {
41  
42      /**
43       * Clock offset scaling factor.
44       * <p>
45       * We use a power of 2 to avoid numeric noise introduction
46       * in the multiplications/divisions sequences.
47       * </p>
48       */
49      private static final double CLOCK_OFFSET_SCALE = FastMath.scalb(1.0, -10);
50  
51      /** List of terms. */
52      private final List<ParameterDriver> terms;
53  
54      /**
55       * Simple constructor.
56       *
57       * @param referenceDate reference date
58       */
59      public PolynomialClockModel(final AbsoluteDate referenceDate) {
60          this.terms =  new ArrayList<>();
61          final ParameterDriver parameterTerm = new ParameterDriver("-clock-bias", 0.0, CLOCK_OFFSET_SCALE,
62                  Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
63          parameterTerm.setValue(0);
64          parameterTerm.setReferenceDate(referenceDate);
65          this.terms.add(parameterTerm);
66      }
67  
68      /**
69       * Simple constructor.
70       *
71       * @param referenceDate reference date
72       * @param terms the polynomial terms in order.
73       */
74      public PolynomialClockModel(final AbsoluteDate referenceDate,
75              final double... terms) {
76          Integer ii = 0;
77          final List<ParameterDriver> convertedTerms = new ArrayList<>();
78          for (double term : terms) {
79              final String name = getAcceptedTermName(ii);
80              final ParameterDriver parameterTerm = new ParameterDriver(name, 0.0, CLOCK_OFFSET_SCALE,
81                      Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
82              parameterTerm.setValue(term);
83              parameterTerm.setReferenceDate(referenceDate);
84              convertedTerms.add(parameterTerm);
85              ++ii;
86          }
87          this.terms = convertedTerms;
88      }
89  
90      /**
91       * Simple constructor.
92       *
93       * @param terms the parameter driver terms
94       */
95      public PolynomialClockModel(final ParameterDriver... terms) {
96          Integer idx = 0;
97          for (final ParameterDriver term : terms) {
98              final String accepted_name_format = getAcceptedTermName(idx);
99              if (!term.getName().contains(accepted_name_format)) {
100                 throw new OrekitException(OrekitMessages.UNSUPPORTED_PARAMETER_NAME, term.getName(), accepted_name_format);
101             }
102             idx++;
103         }
104         this.terms = Arrays.asList(terms);
105     }
106 
107     /**
108      * Simple constructor.
109      *
110      * @param terms the parameter driver terms
111      */
112     public PolynomialClockModel(final List<ParameterDriver> terms) {
113         this(terms.toArray(new ParameterDriver[0]));
114     }
115 
116     /** {@inheritDoc} */
117     @Override
118     public AbsoluteDate getValidityStart() {
119         return AbsoluteDate.PAST_INFINITY;
120     }
121 
122     /** {@inheritDoc} */
123     @Override
124     public AbsoluteDate getValidityEnd() {
125         return AbsoluteDate.FUTURE_INFINITY;
126     }
127 
128     /** {@inheritDoc} */
129     @Override
130     public List<ParameterDriver> getParametersDrivers() {
131         return terms;
132     }
133 
134     /** Add a parameter driver term in a given index to the list of parameters.
135      * If parameters prior to the one requested don't exist, it will create empty ones.
136      * This allows adding a velocity, acceleration, or above without explicitly defining the terms below.
137      *
138      * @param index the index at which to add the parameter driver
139      * @param driver the parameter driver to add
140      */
141     public void addParameterDriver(final Integer index, final ParameterDriver driver) {
142         final List<ParameterDriver> parameters = getParametersDrivers();
143         if (parameters.size() < index) {
144             // Recursively add empty parameters to fill gaps
145             addParameterDriver(index - 1, null);
146             // After filling gaps, add the driver at the target index
147             addParameterDriver(index, driver);
148         } else if (parameters.size() == index && driver != null) {
149             // Add the driver at the correct index
150             parameters.add(driver);
151         } else if (parameters.size() == index && driver == null) {
152             // Create empty parameter with correct name for this index
153             final ParameterDriver empty = new ParameterDriver(getAcceptedTermName(index), 0.0, CLOCK_OFFSET_SCALE,
154                     Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
155             empty.setReferenceDate(AbsoluteDate.ARBITRARY_EPOCH);
156             parameters.add(empty);
157         }
158     }
159 
160     /** {@inheritDoc} */
161     @Override
162     public ClockOffset getOffset(final AbsoluteDate date) {
163         final double[] result = new double[3];
164         if (terms.isEmpty()) {
165             return new ClockOffset(date, result);
166         }
167         final double dt = date.durationFrom(getSafeReference(date));
168         final double[] convertedTerms = terms.stream().map(x -> x.getValue(date)).mapToDouble(Double::doubleValue).toArray();
169         // Turn the terms into a polynomial function
170         PolynomialFunction function = new PolynomialFunction(convertedTerms);
171         // Loop over all of the terms in order
172         for (int ii = 0; ii < 3; ii++) {
173             result[ii] = function.value(dt);
174             function = function.polynomialDerivative();
175         }
176         return new ClockOffset(date, result);
177     }
178 
179     /** {@inheritDoc} */
180     @Override
181     public <T extends CalculusFieldElement<T>> FieldClockOffset<T> getFieldOffset(final FieldAbsoluteDate<T> date) {
182         final AbsoluteDate aDate = date.toAbsoluteDate();
183         final T dt = date.durationFrom(getSafeReference(aDate));
184         final List<T> result = new ArrayList<>(3);
185         // Loop over all of the terms in order
186         // Repeat until out of terms
187         final double[] convertedTerms = terms.stream().map(x -> x.getValue(aDate)).mapToDouble(Double::doubleValue).toArray();
188         // Turn the terms into a polynomial function
189         PolynomialFunction function = new PolynomialFunction(convertedTerms);
190         for (int ii = 0; ii < 3; ii++) {
191             final T newValue = function.value(dt);
192             result.add(newValue);
193             // Take the next derivative
194             function = function.polynomialDerivative();
195         }
196         return new FieldClockOffset<>(date, result);
197     }
198 
199     /** {@inheritDoc} */
200     @Override
201     public FieldClockModel<Gradient> getFieldModel(final int freeParameters,
202             final Map<String, Integer> indices, final AbsoluteDate date) {
203         if (terms.isEmpty()) {
204             return null;
205         }
206         final Gradient[] gradients = terms.stream().map(x -> x.getValue(freeParameters, indices, date))
207                 .toArray(Gradient[]::new);
208         final FieldAbsoluteDate<Gradient> referenceDate = new FieldAbsoluteDate<>(gradients[0].getField(),
209                 getSafeReference(date));
210         return new PolynomialFieldClockModel<>(referenceDate, gradients);
211     }
212 
213     /**
214      * Get a safe reference date.
215      * <p>
216      * This method deals with parameters drivers for which no reference
217      * date has been set, which is acceptable if the model is not
218      * time-dependent.
219      * </p>
220      *
221      * @param date date at which values are requested
222      * @return safe reference date
223      */
224     private AbsoluteDate getSafeReference(final AbsoluteDate date) {
225         // If there are no terms the clock model is constant and the date is safe
226         final double EPS = 1e-9;
227         if (terms.isEmpty()) {
228             return date;
229         }
230         final ParameterDriver firstTerm = terms.getFirst();
231         if (firstTerm.getReferenceDate() == null) {
232             boolean allOtherDatesZero = true;
233             for (final ParameterDriver term: terms) {
234                 if (FastMath.abs(term.getValue(date)) > EPS) {
235                     allOtherDatesZero = false;
236                 }
237             }
238             if (allOtherDatesZero) {
239                 // it is OK to not have a reference date is clock offset is constant
240                 return date;
241             } else {
242                 throw new OrekitException(OrekitMessages.NO_REFERENCE_DATE_FOR_PARAMETER,
243                         firstTerm.getName());
244             }
245         } else {
246             return firstTerm.getReferenceDate();
247         }
248     }
249 
250 }