1   /* Copyright 2002-2025 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.measurements;
18  
19  import java.util.Map;
20  
21  import org.hipparchus.CalculusFieldElement;
22  import org.hipparchus.analysis.differentiation.Gradient;
23  import org.hipparchus.geometry.euclidean.threed.FieldRotation;
24  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
25  import org.hipparchus.geometry.euclidean.threed.Rotation;
26  import org.hipparchus.geometry.euclidean.threed.RotationConvention;
27  import org.hipparchus.geometry.euclidean.threed.Vector3D;
28  import org.hipparchus.util.FastMath;
29  import org.orekit.errors.OrekitException;
30  import org.orekit.errors.OrekitMessages;
31  import org.orekit.frames.FieldStaticTransform;
32  import org.orekit.frames.FieldTransform;
33  import org.orekit.frames.StaticTransform;
34  import org.orekit.frames.Transform;
35  import org.orekit.frames.TransformProvider;
36  import org.orekit.time.AbsoluteDate;
37  import org.orekit.time.FieldAbsoluteDate;
38  import org.orekit.time.TimeOffset;
39  import org.orekit.time.UT1Scale;
40  import org.orekit.utils.IERSConventions;
41  import org.orekit.utils.ParameterDriver;
42  
43  /** Class modeling an Earth frame whose Earth Orientation Parameters can be estimated.
44   * <p>
45   * This class adds parameters for an additional polar motion
46   * and an additional prime meridian orientation on top of an underlying regular Earth
47   * frame like {@link org.orekit.frames.FramesFactory#getITRF(IERSConventions, boolean) ITRF}.
48   * The polar motion and prime meridian orientation are applied <em>after</em> regular Earth
49   * orientation parameters, so the value of the estimated parameters will be correction to EOP,
50   * they will not be the complete EOP values by themselves. Basically, this means that for
51   * Earth, the following transforms are applied in order, between inertial frame and this frame:
52   * </p>
53   * <ol>
54   *   <li>precession/nutation, as theoretical model plus celestial pole EOP parameters</li>
55   *   <li>body rotation, as theoretical model plus prime meridian EOP parameters</li>
56   *   <li>polar motion, which is only from EOP parameters (no theoretical models)</li>
57   *   <li>additional body rotation, controlled by {@link #getPrimeMeridianOffsetDriver()} and {@link #getPrimeMeridianDriftDriver()}</li>
58   *   <li>additional polar motion, controlled by {@link #getPolarOffsetXDriver()}, {@link #getPolarDriftXDriver()},
59   *   {@link #getPolarOffsetYDriver()} and {@link #getPolarDriftYDriver()}</li>
60   * </ol>
61   * @author Luc Maisonobe
62   * @since 9.1
63   */
64  public class EstimatedEarthFrameProvider implements TransformProvider {
65  
66      /** Earth Angular Velocity, in rad/s, from TIRF model. */
67      public static final double EARTH_ANGULAR_VELOCITY = 7.292115146706979e-5;
68  
69      /** Angular scaling factor.
70       * <p>
71       * We use a power of 2 to avoid numeric noise introduction
72       * in the multiplications/divisions sequences.
73       * </p>
74       */
75      private static final double ANGULAR_SCALE = FastMath.scalb(1.0, -22);
76  
77      /** Underlying raw UT1. */
78      private final UT1Scale baseUT1;
79  
80      /** Estimated UT1. */
81      private final transient UT1Scale estimatedUT1;
82  
83      /** Driver for prime meridian offset. */
84      private final transient ParameterDriver primeMeridianOffsetDriver;
85  
86      /** Driver for prime meridian drift. */
87      private final transient ParameterDriver primeMeridianDriftDriver;
88  
89      /** Driver for pole offset along X. */
90      private final transient ParameterDriver polarOffsetXDriver;
91  
92      /** Driver for pole drift along X. */
93      private final transient ParameterDriver polarDriftXDriver;
94  
95      /** Driver for pole offset along Y. */
96      private final transient ParameterDriver polarOffsetYDriver;
97  
98      /** Driver for pole drift along Y. */
99      private final transient ParameterDriver polarDriftYDriver;
100 
101     /** Build an estimated Earth frame.
102      * <p>
103      * The initial values for the pole and prime meridian parametric linear models
104      * ({@link #getPrimeMeridianOffsetDriver()}, {@link #getPrimeMeridianDriftDriver()},
105      * {@link #getPolarOffsetXDriver()}, {@link #getPolarDriftXDriver()},
106      * {@link #getPolarOffsetXDriver()}, {@link #getPolarDriftXDriver()}) are set to 0.
107      * </p>
108      * @param baseUT1 underlying base UT1
109      * @since 9.1
110      */
111     public EstimatedEarthFrameProvider(final UT1Scale baseUT1) {
112 
113         this.primeMeridianOffsetDriver = new ParameterDriver("prime-meridian-offset",
114                                                              0.0, ANGULAR_SCALE,
115                                                             -FastMath.PI, FastMath.PI);
116 
117         this.primeMeridianDriftDriver = new ParameterDriver("prime-meridian-drift",
118                                                             0.0, ANGULAR_SCALE,
119                                                             Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
120 
121         this.polarOffsetXDriver = new ParameterDriver("polar-offset-X",
122                                                       0.0, ANGULAR_SCALE,
123                                                       -FastMath.PI, FastMath.PI);
124 
125         this.polarDriftXDriver = new ParameterDriver("polar-drift-X",
126                                                      0.0, ANGULAR_SCALE,
127                                                      Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
128 
129         this.polarOffsetYDriver = new ParameterDriver("polar-offset-Y",
130                                                       0.0, ANGULAR_SCALE,
131                                                       -FastMath.PI, FastMath.PI);
132 
133         this.polarDriftYDriver = new ParameterDriver("polar-drift-Y",
134                                                      0.0, ANGULAR_SCALE,
135                                                      Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
136 
137         this.baseUT1      = baseUT1;
138         this.estimatedUT1 = new EstimatedUT1Scale();
139 
140     }
141 
142     /** Get a driver allowing to add a prime meridian rotation.
143      * <p>
144      * The parameter is an angle in radians. In order to convert this
145      * value to a DUT1 in seconds, the value must be divided by
146      * {@link #EARTH_ANGULAR_VELOCITY} (nominal Angular Velocity of Earth).
147      * </p>
148      * @return driver for prime meridian rotation
149      */
150     public ParameterDriver getPrimeMeridianOffsetDriver() {
151         return primeMeridianOffsetDriver;
152     }
153 
154     /** Get a driver allowing to add a prime meridian rotation rate.
155      * <p>
156      * The parameter is an angle rate in radians per second. In order to convert this
157      * value to a LOD in seconds, the value must be multiplied by -86400 and divided by
158      * {@link #EARTH_ANGULAR_VELOCITY} (nominal Angular Velocity of Earth).
159      * </p>
160      * @return driver for prime meridian rotation rate
161      */
162     public ParameterDriver getPrimeMeridianDriftDriver() {
163         return primeMeridianDriftDriver;
164     }
165 
166     /** Get a driver allowing to add a polar offset along X.
167      * <p>
168      * The parameter is an angle in radians
169      * </p>
170      * @return driver for polar offset along X
171      */
172     public ParameterDriver getPolarOffsetXDriver() {
173         return polarOffsetXDriver;
174     }
175 
176     /** Get a driver allowing to add a polar drift along X.
177      * <p>
178      * The parameter is an angle rate in radians per second
179      * </p>
180      * @return driver for polar drift along X
181      */
182     public ParameterDriver getPolarDriftXDriver() {
183         return polarDriftXDriver;
184     }
185 
186     /** Get a driver allowing to add a polar offset along Y.
187      * <p>
188      * The parameter is an angle in radians
189      * </p>
190      * @return driver for polar offset along Y
191      */
192     public ParameterDriver getPolarOffsetYDriver() {
193         return polarOffsetYDriver;
194     }
195 
196     /** Get a driver allowing to add a polar drift along Y.
197      * <p>
198      * The parameter is an angle rate in radians per second
199      * </p>
200      * @return driver for polar drift along Y
201      */
202     public ParameterDriver getPolarDriftYDriver() {
203         return polarDriftYDriver;
204     }
205 
206     /** Get the estimated UT1 time scale.
207      * @return estimated UT1 time scale
208      */
209     public UT1Scale getEstimatedUT1() {
210         return estimatedUT1;
211     }
212 
213     /** {@inheritDoc} */
214     @Override
215     public Transform getTransform(final AbsoluteDate date) {
216 
217         // take parametric prime meridian shift into account
218         final double theta    = linearModel(date, primeMeridianOffsetDriver, primeMeridianDriftDriver);
219         final double thetaDot = primeMeridianDriftDriver.getValue();
220         final Transform meridianShift =
221                         new Transform(date,
222                                       new Rotation(Vector3D.PLUS_K, theta, RotationConvention.FRAME_TRANSFORM),
223                                       new Vector3D(0, 0, thetaDot));
224 
225         // take parametric pole shift into account
226         final double xpNeg     = -linearModel(date, polarOffsetXDriver, polarDriftXDriver);
227         final double ypNeg     = -linearModel(date, polarOffsetYDriver, polarDriftYDriver);
228         final double xpNegDot  = -polarDriftXDriver.getValue();
229         final double ypNegDot  = -polarDriftYDriver.getValue();
230         final Transform poleShift =
231                         new Transform(date,
232                                       new Transform(date,
233                                                     new Rotation(Vector3D.PLUS_J, xpNeg, RotationConvention.FRAME_TRANSFORM),
234                                                     new Vector3D(0.0, xpNegDot, 0.0)),
235                                       new Transform(date,
236                                                     new Rotation(Vector3D.PLUS_I, ypNeg, RotationConvention.FRAME_TRANSFORM),
237                                                     new Vector3D(ypNegDot, 0.0, 0.0)));
238 
239         return new Transform(date, meridianShift, poleShift);
240 
241     }
242 
243     /** {@inheritDoc} */
244     @Override
245     public StaticTransform getStaticTransform(final AbsoluteDate date) {
246 
247         // take parametric prime meridian shift into account
248         final double theta    = linearModel(date, primeMeridianOffsetDriver, primeMeridianDriftDriver);
249         final StaticTransform meridianShift = StaticTransform.of(
250                 date,
251                 new Rotation(Vector3D.PLUS_K, theta, RotationConvention.FRAME_TRANSFORM)
252         );
253 
254         // take parametric pole shift into account
255         final double xpNeg     = -linearModel(date, polarOffsetXDriver, polarDriftXDriver);
256         final double ypNeg     = -linearModel(date, polarOffsetYDriver, polarDriftYDriver);
257         final StaticTransform poleShift = StaticTransform.compose(
258                 date,
259                 StaticTransform.of(
260                         date,
261                         new Rotation(Vector3D.PLUS_J, xpNeg, RotationConvention.FRAME_TRANSFORM)),
262                 StaticTransform.of(
263                         date,
264                         new Rotation(Vector3D.PLUS_I, ypNeg, RotationConvention.FRAME_TRANSFORM)));
265 
266         return StaticTransform.compose(date, meridianShift, poleShift);
267 
268     }
269 
270     /** {@inheritDoc} */
271     @Override
272     public <T extends CalculusFieldElement<T>> FieldTransform<T> getTransform(final FieldAbsoluteDate<T> date) {
273 
274         final T zero = date.getField().getZero();
275 
276         // prime meridian shift parameters
277         final T theta    = linearModel(date, primeMeridianOffsetDriver, primeMeridianDriftDriver);
278         final T thetaDot = zero.newInstance(primeMeridianDriftDriver.getValue());
279 
280         // pole shift parameters
281         final T xpNeg    = linearModel(date, polarOffsetXDriver, polarDriftXDriver).negate();
282         final T ypNeg    = linearModel(date, polarOffsetYDriver, polarDriftYDriver).negate();
283         final T xpNegDot = zero.subtract(polarDriftXDriver.getValue());
284         final T ypNegDot = zero.subtract(polarDriftYDriver.getValue());
285 
286         return getTransform(date, theta, thetaDot, xpNeg, xpNegDot, ypNeg, ypNegDot);
287 
288     }
289 
290     /** {@inheritDoc} */
291     @Override
292     public <T extends CalculusFieldElement<T>> FieldStaticTransform<T> getStaticTransform(final FieldAbsoluteDate<T> date) {
293 
294         // take parametric prime meridian shift into account
295         final T theta    = linearModel(date, primeMeridianOffsetDriver, primeMeridianDriftDriver);
296         final FieldStaticTransform<T> meridianShift = FieldStaticTransform.of(
297                 date,
298                 new FieldRotation<>(FieldVector3D.getPlusK(date.getField()), theta, RotationConvention.FRAME_TRANSFORM)
299         );
300 
301         // take parametric pole shift into account
302         final T xpNeg     = linearModel(date, polarOffsetXDriver, polarDriftXDriver).negate();
303         final T ypNeg     = linearModel(date, polarOffsetYDriver, polarDriftYDriver).negate();
304         final FieldStaticTransform<T> poleShift = FieldStaticTransform.compose(
305                 date,
306                 FieldStaticTransform.of(
307                         date,
308                         new FieldRotation<>(FieldVector3D.getPlusJ(date.getField()), xpNeg, RotationConvention.FRAME_TRANSFORM)),
309                 FieldStaticTransform.of(
310                         date,
311                         new FieldRotation<>(FieldVector3D.getPlusI(date.getField()), ypNeg, RotationConvention.FRAME_TRANSFORM)));
312 
313         return FieldStaticTransform.compose(date, meridianShift, poleShift);
314 
315     }
316 
317     /** Get the transform with derivatives.
318      * @param date date of the transform
319      * @param freeParameters total number of free parameters in the gradient
320      * @param indices indices of the estimated parameters in derivatives computations
321      * @return computed transform with derivatives
322      * @since 10.2
323      */
324     public FieldTransform<Gradient> getTransform(final FieldAbsoluteDate<Gradient> date,
325                                                  final int freeParameters,
326                                                  final Map<String, Integer> indices) {
327 
328         // prime meridian shift parameters
329         final Gradient theta    = linearModel(freeParameters, date,
330                                               primeMeridianOffsetDriver, primeMeridianDriftDriver,
331                                               indices);
332         final Gradient thetaDot = primeMeridianDriftDriver.getValue(freeParameters, indices, date.toAbsoluteDate());
333 
334         // pole shift parameters
335         final Gradient xpNeg    = linearModel(freeParameters, date,
336                                                          polarOffsetXDriver, polarDriftXDriver, indices).negate();
337         final Gradient ypNeg    = linearModel(freeParameters, date,
338                                                          polarOffsetYDriver, polarDriftYDriver, indices).negate();
339         final Gradient xpNegDot = polarDriftXDriver.getValue(freeParameters, indices, date.toAbsoluteDate()).negate();
340         final Gradient ypNegDot = polarDriftYDriver.getValue(freeParameters, indices, date.toAbsoluteDate()).negate();
341 
342         return getTransform(date, theta, thetaDot, xpNeg, xpNegDot, ypNeg, ypNegDot);
343 
344     }
345 
346     /** Get the transform with derivatives.
347      * @param date date of the transform
348      * @param theta angle of the prime meridian
349      * @param thetaDot angular rate of the prime meridian
350      * @param xpNeg opposite of the angle of the pole motion along X
351      * @param xpNegDot opposite of the angular rate of the pole motion along X
352      * @param ypNeg opposite of the angle of the pole motion along Y
353      * @param ypNegDot opposite of the angular rate of the pole motion along Y
354      * @param <T> type of the field elements
355      * @return computed transform with derivatives
356      */
357     private <T extends CalculusFieldElement<T>> FieldTransform<T> getTransform(final FieldAbsoluteDate<T> date,
358                                                                            final T theta, final T thetaDot,
359                                                                            final T xpNeg, final T xpNegDot,
360                                                                            final T ypNeg, final T ypNegDot) {
361 
362         final T                zero  = date.getField().getZero();
363         final FieldVector3D<T> plusI = FieldVector3D.getPlusI(date.getField());
364         final FieldVector3D<T> plusJ = FieldVector3D.getPlusJ(date.getField());
365         final FieldVector3D<T> plusK = FieldVector3D.getPlusK(date.getField());
366 
367         // take parametric prime meridian shift into account
368         final FieldTransform<T> meridianShift =
369                         new FieldTransform<>(date,
370                                              new FieldRotation<>(plusK, theta, RotationConvention.FRAME_TRANSFORM),
371                                              new FieldVector3D<>(zero, zero, thetaDot));
372 
373         // take parametric pole shift into account
374         final FieldTransform<T> poleShift =
375                         new FieldTransform<>(date,
376                                       new FieldTransform<>(date,
377                                                            new FieldRotation<>(plusJ, xpNeg, RotationConvention.FRAME_TRANSFORM),
378                                                            new FieldVector3D<>(zero, xpNegDot, zero)),
379                                       new FieldTransform<>(date,
380                                                            new FieldRotation<>(plusI, ypNeg, RotationConvention.FRAME_TRANSFORM),
381                                                            new FieldVector3D<>(ypNegDot, zero, zero)));
382 
383         return new FieldTransform<>(date, meridianShift, poleShift);
384 
385     }
386 
387     /** Evaluate a parametric linear model.
388      * @param date current date
389      * @param offsetDriver driver for the offset parameter
390      * @param driftDriver driver for the drift parameter
391      * @return current value of the linear model
392      */
393     private double linearModel(final AbsoluteDate date,
394                                final ParameterDriver offsetDriver, final ParameterDriver driftDriver) {
395         if (offsetDriver.getReferenceDate() == null) {
396             throw new OrekitException(OrekitMessages.NO_REFERENCE_DATE_FOR_PARAMETER,
397                                       offsetDriver.getName());
398         }
399         final double dt     = date.durationFrom(offsetDriver.getReferenceDate());
400         final double offset = offsetDriver.getValue();
401         final double drift  = driftDriver.getValue();
402         return dt * drift + offset;
403     }
404 
405     /** Evaluate a parametric linear model.
406      * @param date current date
407      * @param offsetDriver driver for the offset parameter
408      * @param driftDriver driver for the drift parameter
409      * @return current value of the linear model
410      * @param <T> type of the filed elements
411      */
412     private <T extends CalculusFieldElement<T>> T linearModel(final FieldAbsoluteDate<T> date,
413                                                           final ParameterDriver offsetDriver,
414                                                           final ParameterDriver driftDriver) {
415         if (offsetDriver.getReferenceDate() == null) {
416             throw new OrekitException(OrekitMessages.NO_REFERENCE_DATE_FOR_PARAMETER,
417                                       offsetDriver.getName());
418         }
419         final T dt          = date.durationFrom(offsetDriver.getReferenceDate());
420         final double offset = offsetDriver.getValue();
421         final double drift  = driftDriver.getValue();
422         return dt.multiply(drift).add(offset);
423     }
424 
425     /** Evaluate a parametric linear model.
426      * @param freeParameters total number of free parameters in the gradient
427      * @param date current date
428      * @param offsetDriver driver for the offset parameter
429      * @param driftDriver driver for the drift parameter
430      * @param indices indices of the estimated parameters in derivatives computations
431      * @return current value of the linear model
432      * @since 10.2
433      */
434     private Gradient linearModel(final int freeParameters, final FieldAbsoluteDate<Gradient> date,
435                                  final ParameterDriver offsetDriver, final ParameterDriver driftDriver,
436                                  final Map<String, Integer> indices) {
437         if (offsetDriver.getReferenceDate() == null) {
438             throw new OrekitException(OrekitMessages.NO_REFERENCE_DATE_FOR_PARAMETER,
439                                       offsetDriver.getName());
440         }
441         final Gradient dt     = date.durationFrom(offsetDriver.getReferenceDate());
442         final Gradient offset = offsetDriver.getValue(freeParameters, indices, date.toAbsoluteDate());
443         final Gradient drift  = driftDriver.getValue(freeParameters, indices, date.toAbsoluteDate());
444         return dt.multiply(drift).add(offset);
445     }
446 
447     /** Local time scale for estimated UT1. */
448     private class EstimatedUT1Scale extends UT1Scale {
449 
450         /** Simple constructor.
451          */
452         EstimatedUT1Scale() {
453             super(baseUT1.getEOPHistory(), baseUT1.getUTCScale());
454         }
455 
456         /** {@inheritDoc} */
457         @Override
458         public <T extends CalculusFieldElement<T>> T offsetFromTAI(final FieldAbsoluteDate<T> date) {
459             final T dut1 = linearModel(date, primeMeridianOffsetDriver, primeMeridianDriftDriver).divide(EARTH_ANGULAR_VELOCITY);
460             return baseUT1.offsetFromTAI(date).add(dut1);
461         }
462 
463         /** {@inheritDoc} */
464         @Override
465         public TimeOffset offsetFromTAI(final AbsoluteDate date) {
466             final double dut1 = linearModel(date, primeMeridianOffsetDriver, primeMeridianDriftDriver) / EARTH_ANGULAR_VELOCITY;
467             return baseUT1.offsetFromTAI(date).add(new TimeOffset(dut1));
468         }
469 
470         /** {@inheritDoc} */
471         @Override
472         public String getName() {
473             return baseUT1.getName() + "/estimated";
474         }
475 
476     }
477 }