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.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 UT1Scale estimatedUT1;
82  
83      /** Driver for prime meridian offset. */
84      private final ParameterDriver primeMeridianOffsetDriver;
85  
86      /** Driver for prime meridian drift. */
87      private final ParameterDriver primeMeridianDriftDriver;
88  
89      /** Driver for pole offset along X. */
90      private final ParameterDriver polarOffsetXDriver;
91  
92      /** Driver for pole drift along X. */
93      private final ParameterDriver polarDriftXDriver;
94  
95      /** Driver for pole offset along Y. */
96      private final ParameterDriver polarOffsetYDriver;
97  
98      /** Driver for pole drift along Y. */
99      private final 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 static transform with derivatives.
347      * @param date date of the transform
348      * @param freeParameters total number of free parameters in the gradient
349      * @param indices indices of the estimated parameters in derivatives computations
350      * @return computed transform with derivatives
351      * @since 14.0
352      */
353     public FieldStaticTransform<Gradient> getStaticTransform(final FieldAbsoluteDate<Gradient> date,
354                                                  final int freeParameters,
355                                                  final Map<String, Integer> indices) {
356 
357         // prime meridian shift parameters
358         final Gradient theta    = linearModel(freeParameters, date, primeMeridianOffsetDriver, primeMeridianDriftDriver,
359                 indices);
360         final Gradient thetaDot = primeMeridianDriftDriver.getValue(freeParameters, indices, date.toAbsoluteDate());
361 
362         // pole shift parameters
363         final Gradient xpNeg    = linearModel(freeParameters, date,
364                 polarOffsetXDriver, polarDriftXDriver, indices).negate();
365         final Gradient ypNeg    = linearModel(freeParameters, date,
366                 polarOffsetYDriver, polarDriftYDriver, indices).negate();
367         final Gradient xpNegDot = polarDriftXDriver.getValue(freeParameters, indices, date.toAbsoluteDate()).negate();
368         final Gradient ypNegDot = polarDriftYDriver.getValue(freeParameters, indices, date.toAbsoluteDate()).negate();
369 
370         final Gradient                zero  = date.getField().getZero();
371         final FieldVector3D<Gradient> plusI = FieldVector3D.getPlusI(date.getField());
372         final FieldVector3D<Gradient> plusJ = FieldVector3D.getPlusJ(date.getField());
373         final FieldVector3D<Gradient> plusK = FieldVector3D.getPlusK(date.getField());
374 
375         // take parametric prime meridian shift into account
376         final FieldStaticTransform<Gradient> meridianShift =
377                 FieldStaticTransform.of(date, new FieldVector3D<>(zero, zero, thetaDot),
378                         new FieldRotation<>(plusK, theta, RotationConvention.FRAME_TRANSFORM));
379 
380         // take parametric pole shift into account
381         final FieldStaticTransform<Gradient> poleShift =
382                 FieldStaticTransform.compose(date,
383                         FieldStaticTransform.of(date, new FieldVector3D<>(zero, xpNegDot, zero),
384                                 new FieldRotation<>(plusJ, xpNeg, RotationConvention.FRAME_TRANSFORM)),
385                         FieldStaticTransform.of(date, new FieldVector3D<>(ypNegDot, zero, zero),
386                                 new FieldRotation<>(plusI, ypNeg, RotationConvention.FRAME_TRANSFORM)));
387 
388         return FieldStaticTransform.compose(date, meridianShift, poleShift);
389 
390     }
391 
392     /** Get the transform with derivatives.
393      * @param date date of the transform
394      * @param theta angle of the prime meridian
395      * @param thetaDot angular rate of the prime meridian
396      * @param xpNeg opposite of the angle of the pole motion along X
397      * @param xpNegDot opposite of the angular rate of the pole motion along X
398      * @param ypNeg opposite of the angle of the pole motion along Y
399      * @param ypNegDot opposite of the angular rate of the pole motion along Y
400      * @param <T> type of the field elements
401      * @return computed transform with derivatives
402      */
403     private <T extends CalculusFieldElement<T>> FieldTransform<T> getTransform(final FieldAbsoluteDate<T> date,
404                                                                            final T theta, final T thetaDot,
405                                                                            final T xpNeg, final T xpNegDot,
406                                                                            final T ypNeg, final T ypNegDot) {
407 
408         final T                zero  = date.getField().getZero();
409         final FieldVector3D<T> plusI = FieldVector3D.getPlusI(date.getField());
410         final FieldVector3D<T> plusJ = FieldVector3D.getPlusJ(date.getField());
411         final FieldVector3D<T> plusK = FieldVector3D.getPlusK(date.getField());
412 
413         // take parametric prime meridian shift into account
414         final FieldTransform<T> meridianShift =
415                         new FieldTransform<>(date,
416                                              new FieldRotation<>(plusK, theta, RotationConvention.FRAME_TRANSFORM),
417                                              new FieldVector3D<>(zero, zero, thetaDot));
418 
419         // take parametric pole shift into account
420         final FieldTransform<T> poleShift =
421                         new FieldTransform<>(date,
422                                       new FieldTransform<>(date,
423                                                            new FieldRotation<>(plusJ, xpNeg, RotationConvention.FRAME_TRANSFORM),
424                                                            new FieldVector3D<>(zero, xpNegDot, zero)),
425                                       new FieldTransform<>(date,
426                                                            new FieldRotation<>(plusI, ypNeg, RotationConvention.FRAME_TRANSFORM),
427                                                            new FieldVector3D<>(ypNegDot, zero, zero)));
428 
429         return new FieldTransform<>(date, meridianShift, poleShift);
430 
431     }
432 
433     /** Evaluate a parametric linear model.
434      * @param date current date
435      * @param offsetDriver driver for the offset parameter
436      * @param driftDriver driver for the drift parameter
437      * @return current value of the linear model
438      */
439     private double linearModel(final AbsoluteDate date,
440                                final ParameterDriver offsetDriver, final ParameterDriver driftDriver) {
441         if (offsetDriver.getReferenceDate() == null) {
442             throw new OrekitException(OrekitMessages.NO_REFERENCE_DATE_FOR_PARAMETER,
443                                       offsetDriver.getName());
444         }
445         final double dt     = date.durationFrom(offsetDriver.getReferenceDate());
446         final double offset = offsetDriver.getValue();
447         final double drift  = driftDriver.getValue();
448         return dt * drift + offset;
449     }
450 
451     /** Evaluate a parametric linear model.
452      * @param date current date
453      * @param offsetDriver driver for the offset parameter
454      * @param driftDriver driver for the drift parameter
455      * @return current value of the linear model
456      * @param <T> type of the filed elements
457      */
458     private <T extends CalculusFieldElement<T>> T linearModel(final FieldAbsoluteDate<T> date,
459                                                           final ParameterDriver offsetDriver,
460                                                           final ParameterDriver driftDriver) {
461         if (offsetDriver.getReferenceDate() == null) {
462             throw new OrekitException(OrekitMessages.NO_REFERENCE_DATE_FOR_PARAMETER,
463                                       offsetDriver.getName());
464         }
465         final T dt          = date.durationFrom(offsetDriver.getReferenceDate());
466         final double offset = offsetDriver.getValue();
467         final double drift  = driftDriver.getValue();
468         return dt.multiply(drift).add(offset);
469     }
470 
471     /** Evaluate a parametric linear model.
472      * @param freeParameters total number of free parameters in the gradient
473      * @param date current date
474      * @param offsetDriver driver for the offset parameter
475      * @param driftDriver driver for the drift parameter
476      * @param indices indices of the estimated parameters in derivatives computations
477      * @return current value of the linear model
478      * @since 10.2
479      */
480     private Gradient linearModel(final int freeParameters, final FieldAbsoluteDate<Gradient> date,
481                                  final ParameterDriver offsetDriver, final ParameterDriver driftDriver,
482                                  final Map<String, Integer> indices) {
483         if (offsetDriver.getReferenceDate() == null) {
484             throw new OrekitException(OrekitMessages.NO_REFERENCE_DATE_FOR_PARAMETER,
485                                       offsetDriver.getName());
486         }
487         final Gradient dt     = date.durationFrom(offsetDriver.getReferenceDate());
488         final Gradient offset = offsetDriver.getValue(freeParameters, indices, date.toAbsoluteDate());
489         final Gradient drift  = driftDriver.getValue(freeParameters, indices, date.toAbsoluteDate());
490         return dt.multiply(drift).add(offset);
491     }
492 
493     /** Local time scale for estimated UT1. */
494     private class EstimatedUT1Scale extends UT1Scale {
495 
496         /** Simple constructor.
497          */
498         EstimatedUT1Scale() {
499             super(baseUT1.getEOPHistory(), baseUT1.getUTCScale());
500         }
501 
502         /** {@inheritDoc} */
503         @Override
504         public <T extends CalculusFieldElement<T>> T offsetFromTAI(final FieldAbsoluteDate<T> date) {
505             final T dut1 = linearModel(date, primeMeridianOffsetDriver, primeMeridianDriftDriver).divide(EARTH_ANGULAR_VELOCITY);
506             return baseUT1.offsetFromTAI(date).add(dut1);
507         }
508 
509         /** {@inheritDoc} */
510         @Override
511         public TimeOffset offsetFromTAI(final AbsoluteDate date) {
512             final double dut1 = linearModel(date, primeMeridianOffsetDriver, primeMeridianDriftDriver) / EARTH_ANGULAR_VELOCITY;
513             return baseUT1.offsetFromTAI(date).add(new TimeOffset(dut1));
514         }
515 
516         /** {@inheritDoc} */
517         @Override
518         public String getName() {
519             return baseUT1.getName() + "/estimated";
520         }
521 
522     }
523 }