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 }