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.Field;
23  import org.hipparchus.analysis.differentiation.Gradient;
24  import org.hipparchus.geometry.euclidean.threed.FieldRotation;
25  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
26  import org.hipparchus.geometry.euclidean.threed.Rotation;
27  import org.hipparchus.geometry.euclidean.threed.Vector3D;
28  import org.hipparchus.util.FastMath;
29  import org.orekit.bodies.BodyShape;
30  import org.orekit.bodies.FieldGeodeticPoint;
31  import org.orekit.bodies.GeodeticPoint;
32  import org.orekit.data.BodiesElements;
33  import org.orekit.data.FundamentalNutationArguments;
34  import org.orekit.errors.OrekitException;
35  import org.orekit.errors.OrekitMessages;
36  import org.orekit.frames.EOPHistory;
37  import org.orekit.frames.FieldStaticTransform;
38  import org.orekit.frames.FieldTransform;
39  import org.orekit.frames.Frame;
40  import org.orekit.frames.FramesFactory;
41  import org.orekit.frames.StaticTransform;
42  import org.orekit.frames.TopocentricFrame;
43  import org.orekit.frames.Transform;
44  import org.orekit.models.earth.displacement.StationDisplacement;
45  import org.orekit.time.AbsoluteDate;
46  import org.orekit.time.FieldAbsoluteDate;
47  import org.orekit.time.UT1Scale;
48  import org.orekit.utils.ParameterDriver;
49  
50  /** Class modeling a ground station that can perform some measurements.
51   * <p>
52   * This class adds a position offset parameter to a base {@link TopocentricFrame
53   * topocentric frame}.
54   * </p>
55   * <p>
56   * Since 9.0, this class also adds parameters for an additional polar motion
57   * and an additional prime meridian orientation. Since these parameters will
58   * have the same name for all ground stations, they will be managed consistently
59   * and allow to estimate Earth orientation precisely (this is needed for precise
60   * orbit determination). The polar motion and prime meridian orientation will
61   * be applied <em>after</em> regular Earth orientation parameters, so the value
62   * of the estimated parameters will be correction to EOP, they will not be the
63   * complete EOP values by themselves. Basically, this means that for Earth, the
64   * following transforms are applied in order, between inertial frame and ground
65   * station frame (for non-Earth based ground stations, different precession nutation
66   * models and associated planet oritentation parameters would be applied, if available):
67   * </p>
68   * <p>
69   * Since 9.3, this class also adds a station clock offset parameter, which manages
70   * the value that must be subtracted from the observed measurement date to get the real
71   * physical date at which the measurement was performed (i.e. the offset is negative
72   * if the ground station clock is slow and positive if it is fast).
73   * </p>
74   * <ol>
75   *   <li>precession/nutation, as theoretical model plus celestial pole EOP parameters</li>
76   *   <li>body rotation, as theoretical model plus prime meridian EOP parameters</li>
77   *   <li>polar motion, which is only from EOP parameters (no theoretical models)</li>
78   *   <li>additional body rotation, controlled by {@link #getPrimeMeridianOffsetDriver()} and {@link #getPrimeMeridianDriftDriver()}</li>
79   *   <li>additional polar motion, controlled by {@link #getPolarOffsetXDriver()}, {@link #getPolarDriftXDriver()},
80   *   {@link #getPolarOffsetYDriver()} and {@link #getPolarDriftYDriver()}</li>
81   *   <li>station clock offset, controlled by {@link #getClockOffsetDriver()}</li>
82   *   <li>station position offset, controlled by {@link #getEastOffsetDriver()},
83   *   {@link #getNorthOffsetDriver()} and {@link #getZenithOffsetDriver()}</li>
84   * </ol>
85   * @author Luc Maisonobe
86   * @since 8.0
87   */
88  public class GroundStation {
89  
90      /** Suffix for ground station position and clock offset parameters names. */
91      public static final String OFFSET_SUFFIX = "-offset";
92  
93      /** Suffix for ground clock drift parameters name. */
94      public static final String DRIFT_SUFFIX = "-drift-clock";
95  
96      /** Suffix for ground clock drift parameters name.
97       * @since 12.1
98       */
99      public static final String ACCELERATION_SUFFIX = "-acceleration-clock";
100 
101     /** Clock offset scaling factor.
102      * <p>
103      * We use a power of 2 to avoid numeric noise introduction
104      * in the multiplications/divisions sequences.
105      * </p>
106      */
107     private static final double CLOCK_OFFSET_SCALE = FastMath.scalb(1.0, -10);
108 
109     /** Position offsets scaling factor.
110      * <p>
111      * We use a power of 2 (in fact really 1.0 here) to avoid numeric noise introduction
112      * in the multiplications/divisions sequences.
113      * </p>
114      */
115     private static final double POSITION_OFFSET_SCALE = FastMath.scalb(1.0, 0);
116 
117     /** Provider for Earth frame whose EOP parameters can be estimated. */
118     private final EstimatedEarthFrameProvider estimatedEarthFrameProvider;
119 
120     /** Earth frame whose EOP parameters can be estimated. */
121     private final Frame estimatedEarthFrame;
122 
123     /** Base frame associated with the station. */
124     private final TopocentricFrame baseFrame;
125 
126     /** Fundamental nutation arguments. */
127     private final FundamentalNutationArguments arguments;
128 
129     /** Displacement models. */
130     private final StationDisplacement[] displacements;
131 
132     /** Driver for clock offset. */
133     private final ParameterDriver clockOffsetDriver;
134 
135     /** Driver for clock drift. */
136     private final ParameterDriver clockDriftDriver;
137 
138     /** Driver for clock acceleration.
139      * @since 12.1
140      */
141     private final ParameterDriver clockAccelerationDriver;
142 
143     /** Driver for position offset along the East axis. */
144     private final ParameterDriver eastOffsetDriver;
145 
146     /** Driver for position offset along the North axis. */
147     private final ParameterDriver northOffsetDriver;
148 
149     /** Driver for position offset along the zenith axis. */
150     private final ParameterDriver zenithOffsetDriver;
151 
152     /**
153      * Build a ground station ignoring {@link StationDisplacement station displacements}.
154      * <p>
155      * The initial values for the pole and prime meridian parametric linear models
156      * ({@link #getPrimeMeridianOffsetDriver()}, {@link #getPrimeMeridianDriftDriver()},
157      * {@link #getPolarOffsetXDriver()}, {@link #getPolarDriftXDriver()}, {@link #getPolarOffsetXDriver()},
158      * {@link #getPolarDriftXDriver()}) are set to 0. The initial values for the station offset model
159      * ({@link #getClockOffsetDriver()}, {@link #getEastOffsetDriver()}, {@link #getNorthOffsetDriver()},
160      * {@link #getZenithOffsetDriver()}) are set to 0. This implies that as long as these values are not changed, the
161      * offset frame is the same as the {@link #getBaseFrame() base frame}. As soon as some of these models are changed,
162      * the offset frame moves away from the {@link #getBaseFrame() base frame}.
163      * </p>
164      *
165      * @param baseFrame base frame associated with the station, without *any* parametric model
166      *                  (no station offset, no polar motion, no meridian shift)
167      * @see #GroundStation(TopocentricFrame, EOPHistory, StationDisplacement...)
168      * @since 13.0
169      */
170     public GroundStation(final TopocentricFrame baseFrame) {
171         this(baseFrame, FramesFactory.findEOP(baseFrame));
172     }
173 
174     /**
175      * Simple constructor.
176      * <p>
177      * The initial values for the pole and prime meridian parametric linear models
178      * ({@link #getPrimeMeridianOffsetDriver()}, {@link #getPrimeMeridianDriftDriver()},
179      * {@link #getPolarOffsetXDriver()}, {@link #getPolarDriftXDriver()}, {@link #getPolarOffsetXDriver()},
180      * {@link #getPolarDriftXDriver()}) are set to 0. The initial values for the station offset model
181      * ({@link #getClockOffsetDriver()}, {@link #getEastOffsetDriver()}, {@link #getNorthOffsetDriver()},
182      * {@link #getZenithOffsetDriver()}, {@link #getClockOffsetDriver()}) are set to 0. This implies that as long as
183      * these values are not changed, the offset frame is the same as the {@link #getBaseFrame() base frame}. As soon as
184      * some of these models are changed, the offset frame moves away from the {@link #getBaseFrame() base frame}.
185      * </p>
186      *
187      * @param baseFrame     base frame associated with the station, without *any* parametric model (no station offset,
188      *                      no polar motion, no meridian shift)
189      * @param eopHistory    EOP history associated with Earth frames
190      * @param displacements ground station displacement model (tides, ocean loading, atmospheric loading, thermal
191      *                      effects...)
192      * @since 12.1
193      */
194     public GroundStation(final TopocentricFrame baseFrame, final EOPHistory eopHistory,
195                          final StationDisplacement... displacements) {
196 
197         this.baseFrame = baseFrame;
198 
199         if (eopHistory == null) {
200             throw new OrekitException(OrekitMessages.NO_EARTH_ORIENTATION_PARAMETERS);
201         }
202 
203         final UT1Scale baseUT1 = eopHistory.getTimeScales()
204                 .getUT1(eopHistory.getConventions(), eopHistory.isSimpleEop());
205         this.estimatedEarthFrameProvider = new EstimatedEarthFrameProvider(baseUT1);
206         this.estimatedEarthFrame = new Frame(baseFrame.getParent(), estimatedEarthFrameProvider,
207                                              baseFrame.getParent() + "-estimated");
208 
209         if (displacements.length == 0) {
210             arguments = null;
211         } else {
212             arguments = eopHistory.getConventions().getNutationArguments(
213                     estimatedEarthFrameProvider.getEstimatedUT1(),
214                     eopHistory.getTimeScales());
215         }
216 
217         this.displacements = displacements.clone();
218 
219         this.clockOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-clock",
220                                                      0.0, CLOCK_OFFSET_SCALE,
221                                                      Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
222 
223         this.clockDriftDriver = new ParameterDriver(baseFrame.getName() + DRIFT_SUFFIX,
224                                                     0.0, CLOCK_OFFSET_SCALE,
225                                                     Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
226 
227         this.clockAccelerationDriver = new ParameterDriver(baseFrame.getName() + ACCELERATION_SUFFIX,
228                                                     0.0, CLOCK_OFFSET_SCALE,
229                                                     Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
230 
231         this.eastOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-East",
232                                                     0.0, POSITION_OFFSET_SCALE,
233                                                     Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
234 
235         this.northOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-North",
236                                                      0.0, POSITION_OFFSET_SCALE,
237                                                      Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
238 
239         this.zenithOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-Zenith",
240                                                       0.0, POSITION_OFFSET_SCALE,
241                                                       Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
242 
243     }
244 
245     /** Get the displacement models.
246      * @return displacement models (empty if no model has been set up)
247      * @since 9.1
248      */
249     public StationDisplacement[] getDisplacements() {
250         return displacements.clone();
251     }
252 
253     /** Get a driver allowing to change station clock (which is related to measurement date).
254      * @return driver for station clock offset
255      * @since 9.3
256      */
257     public ParameterDriver getClockOffsetDriver() {
258         return clockOffsetDriver;
259     }
260 
261     /** Get a driver allowing to change station clock drift (which is related to measurement date).
262      * @return driver for station clock drift
263      * @since 10.3
264      */
265     public ParameterDriver getClockDriftDriver() {
266         return clockDriftDriver;
267     }
268 
269     /** Get a driver allowing to change station clock acceleration (which is related to measurement date).
270      * @return driver for station clock acceleration
271      * @since 12.1
272      */
273     public ParameterDriver getClockAccelerationDriver() {
274         return clockAccelerationDriver;
275     }
276 
277     /** Get a driver allowing to change station position along East axis.
278      * @return driver for station position offset along East axis
279      */
280     public ParameterDriver getEastOffsetDriver() {
281         return eastOffsetDriver;
282     }
283 
284     /** Get a driver allowing to change station position along North axis.
285      * @return driver for station position offset along North axis
286      */
287     public ParameterDriver getNorthOffsetDriver() {
288         return northOffsetDriver;
289     }
290 
291     /** Get a driver allowing to change station position along Zenith axis.
292      * @return driver for station position offset along Zenith axis
293      */
294     public ParameterDriver getZenithOffsetDriver() {
295         return zenithOffsetDriver;
296     }
297 
298     /** Get a driver allowing to add a prime meridian rotation.
299      * <p>
300      * The parameter is an angle in radians. In order to convert this
301      * value to a DUT1 in seconds, the value must be divided by
302      * {@code ave = 7.292115146706979e-5} (which is the nominal Angular Velocity
303      * of Earth from the TIRF model).
304      * </p>
305      * @return driver for prime meridian rotation
306      */
307     public ParameterDriver getPrimeMeridianOffsetDriver() {
308         return estimatedEarthFrameProvider.getPrimeMeridianOffsetDriver();
309     }
310 
311     /** Get a driver allowing to add a prime meridian rotation rate.
312      * <p>
313      * The parameter is an angle rate in radians per second. In order to convert this
314      * value to a LOD in seconds, the value must be multiplied by -86400 and divided by
315      * {@code ave = 7.292115146706979e-5} (which is the nominal Angular Velocity
316      * of Earth from the TIRF model).
317      * </p>
318      * @return driver for prime meridian rotation rate
319      */
320     public ParameterDriver getPrimeMeridianDriftDriver() {
321         return estimatedEarthFrameProvider.getPrimeMeridianDriftDriver();
322     }
323 
324     /** Get a driver allowing to add a polar offset along X.
325      * <p>
326      * The parameter is an angle in radians
327      * </p>
328      * @return driver for polar offset along X
329      */
330     public ParameterDriver getPolarOffsetXDriver() {
331         return estimatedEarthFrameProvider.getPolarOffsetXDriver();
332     }
333 
334     /** Get a driver allowing to add a polar drift along X.
335      * <p>
336      * The parameter is an angle rate in radians per second
337      * </p>
338      * @return driver for polar drift along X
339      */
340     public ParameterDriver getPolarDriftXDriver() {
341         return estimatedEarthFrameProvider.getPolarDriftXDriver();
342     }
343 
344     /** Get a driver allowing to add a polar offset along Y.
345      * <p>
346      * The parameter is an angle in radians
347      * </p>
348      * @return driver for polar offset along Y
349      */
350     public ParameterDriver getPolarOffsetYDriver() {
351         return estimatedEarthFrameProvider.getPolarOffsetYDriver();
352     }
353 
354     /** Get a driver allowing to add a polar drift along Y.
355      * <p>
356      * The parameter is an angle rate in radians per second
357      * </p>
358      * @return driver for polar drift along Y
359      */
360     public ParameterDriver getPolarDriftYDriver() {
361         return estimatedEarthFrameProvider.getPolarDriftYDriver();
362     }
363 
364     /** Get the base frame associated with the station.
365      * <p>
366      * The base frame corresponds to a null position offset, null
367      * polar motion, null meridian shift
368      * </p>
369      * @return base frame associated with the station
370      */
371     public TopocentricFrame getBaseFrame() {
372         return baseFrame;
373     }
374 
375     /** Get the estimated Earth frame, including the estimated linear models for pole and prime meridian.
376      * <p>
377      * This frame is bound to the {@link #getPrimeMeridianOffsetDriver() driver for prime meridian offset},
378      * {@link #getPrimeMeridianDriftDriver() driver prime meridian drift},
379      * {@link #getPolarOffsetXDriver() driver for polar offset along X},
380      * {@link #getPolarDriftXDriver() driver for polar drift along X},
381      * {@link #getPolarOffsetYDriver() driver for polar offset along Y},
382      * {@link #getPolarDriftYDriver() driver for polar drift along Y}, so its orientation changes when
383      * the {@link ParameterDriver#setValue(double) setValue} methods of the drivers are called.
384      * </p>
385      * @return estimated Earth frame
386      * @since 9.1
387      */
388     public Frame getEstimatedEarthFrame() {
389         return estimatedEarthFrame;
390     }
391 
392     /** Get the estimated UT1 scale, including the estimated linear models for prime meridian.
393      * <p>
394      * This time scale is bound to the {@link #getPrimeMeridianOffsetDriver() driver for prime meridian offset},
395      * and {@link #getPrimeMeridianDriftDriver() driver prime meridian drift}, so its offset from UTC changes when
396      * the {@link ParameterDriver#setValue(double) setValue} methods of the drivers are called.
397      * </p>
398      * @return estimated Earth frame
399      * @since 9.1
400      */
401     public UT1Scale getEstimatedUT1() {
402         return estimatedEarthFrameProvider.getEstimatedUT1();
403     }
404 
405     /** Get the station displacement.
406      * @param date current date
407      * @param position raw position of the station in Earth frame
408      * before displacement is applied
409      * @return station displacement
410      * @since 9.1
411      */
412     private Vector3D computeDisplacement(final AbsoluteDate date, final Vector3D position) {
413         Vector3D displacement = Vector3D.ZERO;
414         if (arguments != null) {
415             final BodiesElements elements = arguments.evaluateAll(date);
416             for (final StationDisplacement sd : displacements) {
417                 // we consider all displacements apply to the same initial position,
418                 // i.e. they apply simultaneously, not according to some order
419                 displacement = displacement.add(sd.displacement(elements, estimatedEarthFrame, position));
420             }
421         }
422         return displacement;
423     }
424 
425     /** Get the geodetic point at the center of the offset frame.
426      * @param date current date (may be null if displacements are ignored)
427      * @return geodetic point at the center of the offset frame
428      * @since 9.1
429      */
430     public GeodeticPoint getOffsetGeodeticPoint(final AbsoluteDate date) {
431 
432         // take station offset into account
433         final double    x          = eastOffsetDriver.getValue();
434         final double    y          = northOffsetDriver.getValue();
435         final double    z          = zenithOffsetDriver.getValue();
436         final BodyShape baseShape  = baseFrame.getParentShape();
437         final StaticTransform baseToBody = baseFrame.getStaticTransformTo(baseShape.getBodyFrame(), date);
438         Vector3D        origin     = baseToBody.transformPosition(new Vector3D(x, y, z));
439 
440         if (date != null) {
441             origin = origin.add(computeDisplacement(date, origin));
442         }
443 
444         return baseShape.transform(origin, baseShape.getBodyFrame(), date);
445 
446     }
447 
448     /** Get the geodetic point at the center of the offset frame.
449      * @param <T> type of the field elements
450      * @param date current date(<em>must</em> be non-null, which is a more stringent condition
451      *      *                    than in {@link #getOffsetGeodeticPoint(AbsoluteDate)}
452      * @return geodetic point at the center of the offset frame
453      * @since 12.1
454      */
455     public <T extends CalculusFieldElement<T>> FieldGeodeticPoint<T> getOffsetGeodeticPoint(final FieldAbsoluteDate<T> date) {
456 
457         // take station offset into account
458         final double    x          = eastOffsetDriver.getValue();
459         final double    y          = northOffsetDriver.getValue();
460         final double    z          = zenithOffsetDriver.getValue();
461         final BodyShape baseShape  = baseFrame.getParentShape();
462         final FieldStaticTransform<T> baseToBody = baseFrame.getStaticTransformTo(baseShape.getBodyFrame(), date);
463         FieldVector3D<T> origin    = baseToBody.transformPosition(new Vector3D(x, y, z));
464         origin = origin.add(computeDisplacement(date.toAbsoluteDate(), origin.toVector3D()));
465 
466         return baseShape.transform(origin, baseShape.getBodyFrame(), date);
467 
468     }
469 
470     /** Get the transform between offset frame and inertial frame.
471      * <p>
472      * The offset frame takes the <em>current</em> position offset,
473      * polar motion and the meridian shift into account. The frame
474      * returned is disconnected from later changes in the parameters.
475      * When the {@link ParameterDriver parameters} managing these
476      * offsets are changed, the method must be called again to retrieve
477      * a new offset frame.
478      * </p>
479      * @param inertial inertial frame to transform to
480      * @param date date of the transform
481      * @param clockOffsetAlreadyApplied if true, the specified {@code date} is as read
482      * by the ground station clock (i.e. clock offset <em>not</em> compensated), if false,
483      * the specified {@code date} was already compensated and is a physical absolute date
484      * @return transform between offset frame and inertial frame, at <em>real</em> measurement
485      * date (i.e. with clock, Earth and station offsets applied)
486      */
487     public Transform getOffsetToInertial(final Frame inertial,
488                                          final AbsoluteDate date, final boolean clockOffsetAlreadyApplied) {
489 
490         // take clock offset into account
491         final AbsoluteDate offsetCompensatedDate = clockOffsetAlreadyApplied ?
492                                                    date :
493                                                    new AbsoluteDate(date, -clockOffsetDriver.getValue());
494 
495         // take Earth offsets into account
496         final Transform intermediateToBody = estimatedEarthFrameProvider.getTransform(offsetCompensatedDate).getInverse();
497 
498         // take station offsets into account
499         final double    x          = eastOffsetDriver.getValue();
500         final double    y          = northOffsetDriver.getValue();
501         final double    z          = zenithOffsetDriver.getValue();
502         final BodyShape baseShape  = baseFrame.getParentShape();
503         final StaticTransform baseToBody = baseFrame
504                 .getStaticTransformTo(baseShape.getBodyFrame(), offsetCompensatedDate);
505         Vector3D        origin     = baseToBody.transformPosition(new Vector3D(x, y, z));
506         origin = origin.add(computeDisplacement(offsetCompensatedDate, origin));
507 
508         final GeodeticPoint originGP = baseShape.transform(origin, baseShape.getBodyFrame(), offsetCompensatedDate);
509         final Transform offsetToIntermediate =
510                         new Transform(offsetCompensatedDate,
511                                       new Transform(offsetCompensatedDate,
512                                                     new Rotation(Vector3D.PLUS_I, Vector3D.PLUS_K,
513                                                                  originGP.getEast(), originGP.getZenith()),
514                                                     Vector3D.ZERO),
515                                       new Transform(offsetCompensatedDate, origin));
516 
517         // combine all transforms together
518         final Transform bodyToInert        = baseFrame.getParent().getTransformTo(inertial, offsetCompensatedDate);
519 
520         return new Transform(offsetCompensatedDate, offsetToIntermediate, new Transform(offsetCompensatedDate, intermediateToBody, bodyToInert));
521 
522     }
523 
524     /** Get the transform between offset frame and inertial frame with derivatives.
525      * <p>
526      * As the East and North vectors are not well defined at pole, the derivatives
527      * of these two vectors diverge to infinity as we get closer to the pole.
528      * So this method should not be used for stations less than 0.0001 degree from
529      * either poles.
530      * </p>
531      * @param inertial inertial frame to transform to
532      * @param clockDate date of the transform as read by the ground station clock (i.e. clock offset <em>not</em> compensated)
533      * @param freeParameters total number of free parameters in the gradient
534      * @param indices indices of the estimated parameters in derivatives computations, must be driver
535      * span name in map, not driver name or will not give right results (see {@link ParameterDriver#getValue(int, Map)})
536      * @return transform between offset frame and inertial frame, at <em>real</em> measurement
537      * date (i.e. with clock, Earth and station offsets applied)
538      * @see #getOffsetToInertial(Frame, FieldAbsoluteDate, int, Map)
539      * @since 10.2
540      */
541     public FieldTransform<Gradient> getOffsetToInertial(final Frame inertial,
542                                                         final AbsoluteDate clockDate,
543                                                         final int freeParameters,
544                                                         final Map<String, Integer> indices) {
545         // take clock offset into account
546         final Gradient offset = clockOffsetDriver.getValue(freeParameters, indices, clockDate);
547         final FieldAbsoluteDate<Gradient> offsetCompensatedDate =
548                         new FieldAbsoluteDate<>(clockDate, offset.negate());
549 
550         return getOffsetToInertial(inertial, offsetCompensatedDate, freeParameters, indices);
551     }
552 
553     /** Get the transform between offset frame and inertial frame with derivatives.
554      * <p>
555      * As the East and North vectors are not well defined at pole, the derivatives
556      * of these two vectors diverge to infinity as we get closer to the pole.
557      * So this method should not be used for stations less than 0.0001 degree from
558      * either poles.
559      * </p>
560      * @param inertial inertial frame to transform to
561      * @param offsetCompensatedDate date of the transform, clock offset and its derivatives already compensated
562      * @param freeParameters total number of free parameters in the gradient
563      * @param indices indices of the estimated parameters in derivatives computations, must be driver
564      * span name in map, not driver name or will not give right results (see {@link ParameterDriver#getValue(int, Map)})
565      * @return transform between offset frame and inertial frame, at specified date
566      * @since 10.2
567      */
568     public FieldTransform<Gradient> getOffsetToInertial(final Frame inertial,
569                                                         final FieldAbsoluteDate<Gradient> offsetCompensatedDate,
570                                                         final int freeParameters,
571                                                         final Map<String, Integer> indices) {
572 
573         final Field<Gradient>         field = offsetCompensatedDate.getField();
574         final FieldVector3D<Gradient> zero  = FieldVector3D.getZero(field);
575         final FieldVector3D<Gradient> plusI = FieldVector3D.getPlusI(field);
576         final FieldVector3D<Gradient> plusK = FieldVector3D.getPlusK(field);
577 
578         // take Earth offsets into account
579         final FieldTransform<Gradient> intermediateToBody =
580                         estimatedEarthFrameProvider.getTransform(offsetCompensatedDate, freeParameters, indices).getInverse();
581 
582         // take station offsets into account
583         final Gradient                       x          = eastOffsetDriver.getValue(freeParameters, indices);
584         final Gradient                       y          = northOffsetDriver.getValue(freeParameters, indices);
585         final Gradient                       z          = zenithOffsetDriver.getValue(freeParameters, indices);
586         final BodyShape                      baseShape  = baseFrame.getParentShape();
587         final FieldStaticTransform<Gradient> baseToBody = baseFrame.getStaticTransformTo(baseShape.getBodyFrame(), offsetCompensatedDate);
588 
589         FieldVector3D<Gradient> origin = baseToBody.transformPosition(new FieldVector3D<>(x, y, z));
590         origin = origin.add(computeDisplacement(offsetCompensatedDate.toAbsoluteDate(), origin.toVector3D()));
591         final FieldGeodeticPoint<Gradient> originGP = baseShape.transform(origin, baseShape.getBodyFrame(), offsetCompensatedDate);
592         final FieldTransform<Gradient> offsetToIntermediate =
593                         new FieldTransform<>(offsetCompensatedDate,
594                                              new FieldTransform<>(offsetCompensatedDate,
595                                                                   new FieldRotation<>(plusI, plusK,
596                                                                                       originGP.getEast(), originGP.getZenith()),
597                                                                   zero),
598                                              new FieldTransform<>(offsetCompensatedDate, origin));
599 
600         // combine all transforms together
601         final FieldTransform<Gradient> bodyToInert = baseFrame.getParent().getTransformTo(inertial, offsetCompensatedDate);
602 
603         return new FieldTransform<>(offsetCompensatedDate,
604                                     offsetToIntermediate,
605                                     new FieldTransform<>(offsetCompensatedDate, intermediateToBody, bodyToInert));
606 
607     }
608 
609 }