GroundStation.java
/* Copyright 2002-2026 CS GROUP
* Licensed to CS GROUP (CS) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* CS licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.orekit.estimation.measurements;
import java.util.Map;
import org.hipparchus.CalculusFieldElement;
import org.hipparchus.Field;
import org.hipparchus.analysis.differentiation.Gradient;
import org.hipparchus.geometry.euclidean.threed.FieldRotation;
import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
import org.hipparchus.geometry.euclidean.threed.RotationConvention;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.FastMath;
import org.orekit.bodies.BodyShape;
import org.orekit.bodies.FieldGeodeticPoint;
import org.orekit.bodies.GeodeticPoint;
import org.orekit.frames.FieldStaticTransform;
import org.orekit.frames.FieldTransform;
import org.orekit.frames.Frame;
import org.orekit.frames.KinematicTransform;
import org.orekit.frames.StaticTransform;
import org.orekit.frames.TopocentricFrame;
import org.orekit.frames.TopocentricTransformProvider;
import org.orekit.frames.Transform;
import org.orekit.models.earth.displacement.StationDisplacement;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.FieldAbsoluteDate;
import org.orekit.time.clocks.QuadraticClockModel;
import org.orekit.utils.AngularCoordinates;
import org.orekit.utils.FieldAngularCoordinates;
import org.orekit.utils.FieldPVCoordinates;
import org.orekit.utils.FieldPVCoordinatesProvider;
import org.orekit.utils.PVCoordinatesProvider;
import org.orekit.utils.ParameterDriver;
import org.orekit.utils.TimeStampedFieldPVCoordinates;
/** Class modeling a ground station that can perform some measurements.
* <p>
* This class adds a position offset parameter to a base {@link TopocentricFrame
* topocentric frame}.
* </p>
* <p>
* Since 9.3, this class also adds a station clock offset parameter, which manages
* the value that must be subtracted from the observed measurement date to get the real
* physical date at which the measurement was performed (i.e. the offset is negative
* if the ground station clock is slow and positive if it is fast).
* </p>
* <ol>
* <li>station clock offset, controlled by {@link #getClockBiasDriver()}</li>
* <li>station position offset, controlled by {@link #getEastOffsetDriver()},
* {@link #getNorthOffsetDriver()} and {@link #getZenithOffsetDriver()}</li>
* </ol>
* @author Luc Maisonobe
* @since 8.0
*/
public class GroundStation extends AbstractParticipant implements Observer {
/** Position offsets scaling factor.
* <p>
* We use a power of 2 (in fact really 1.0 here) to avoid numeric noise introduction
* in the multiplications/divisions sequences.
* </p>
*/
private static final double POSITION_OFFSET_SCALE = FastMath.scalb(1.0, 0);
/** Base frame associated with the station. */
private final TopocentricFrame baseFrame;
/** Driver for position offset along the East axis. */
private final ParameterDriver eastOffsetDriver;
/** Driver for position offset along the North axis. */
private final ParameterDriver northOffsetDriver;
/** Driver for position offset along the zenith axis. */
private final ParameterDriver zenithOffsetDriver;
/**
* Build a ground station ignoring {@link StationDisplacement station displacements}.
* <p> The initial values for the station offset model
* ({@link #getClockBiasDriver()}, {@link #getEastOffsetDriver()}, {@link #getNorthOffsetDriver()},
* {@link #getZenithOffsetDriver()}) are set to 0. This implies that as long as these values are not changed, the
* offset frame is the same as the {@link #getBaseFrame() base frame}. As soon as some of these models are changed,
* the offset frame moves away from the {@link #getBaseFrame() base frame}.
* </p>
*
* @param baseFrame base frame associated with the station, without *any* parametric model (no station offset)
* @see #GroundStation(TopocentricFrame, QuadraticClockModel)
* @since 13.0
*/
public GroundStation(final TopocentricFrame baseFrame) {
this(baseFrame, createEmptyQuadraticClock(baseFrame.getName()));
}
/**
* Simple constructor.
* <p>
* The initial values for the station offset model
* ({@link #getClockBiasDriver()}, {@link #getEastOffsetDriver()}, {@link #getNorthOffsetDriver()},
* {@link #getZenithOffsetDriver()}, {@link #getClockBiasDriver()}) are set to 0. This implies that as long as
* these values are not changed, the offset frame is the same as the {@link #getBaseFrame() base frame}. As soon as
* some of these models are changed, the offset frame moves away from the {@link #getBaseFrame() base frame}.
* </p>
*
* @param baseFrame base frame associated with the station, without *any* parametric model (no station offset)
* @param clock new quadratic clock model with user-supplied displacements
* @since 12.1
*/
public GroundStation(final TopocentricFrame baseFrame, final QuadraticClockModel clock) {
super(baseFrame.getName(), clock);
this.baseFrame = baseFrame;
this.eastOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-East",
0.0, POSITION_OFFSET_SCALE,
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
this.northOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-North",
0.0, POSITION_OFFSET_SCALE,
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
this.zenithOffsetDriver = new ParameterDriver(baseFrame.getName() + OFFSET_SUFFIX + "-Zenith",
0.0, POSITION_OFFSET_SCALE,
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
// Add the ground station parameters to the master list.
addParameterDriver(this.eastOffsetDriver);
addParameterDriver(this.northOffsetDriver);
addParameterDriver(this.zenithOffsetDriver);
}
/** {@inheritDoc} */
@Override
public final boolean isSpaceBased() {
return false;
}
/** Get a driver allowing to change station position along East axis.
* @return driver for station position offset along East axis
*/
public ParameterDriver getEastOffsetDriver() {
return eastOffsetDriver;
}
/** Get a driver allowing to change station position along North axis.
* @return driver for station position offset along North axis
*/
public ParameterDriver getNorthOffsetDriver() {
return northOffsetDriver;
}
/** Get a driver allowing to change station position along Zenith axis.
* @return driver for station position offset along Zenith axis
*/
public ParameterDriver getZenithOffsetDriver() {
return zenithOffsetDriver;
}
/** Get the base frame associated with the station.
* <p>
* The base frame corresponds to a null position offset, null
* polar motion, null meridian shift
* </p>
* @return base frame associated with the station
*/
public TopocentricFrame getBaseFrame() {
return baseFrame;
}
/** Get the station displacement.
* @param date current date
* @param position raw position of the station in Earth frame
* before displacement is applied
* @return station displacement
* @since 9.1
*/
protected Vector3D computeDisplacement(final AbsoluteDate date, final Vector3D position) {
return Vector3D.ZERO;
}
/** Get the geodetic point at the center of the offset frame.
* @param date current date (may be null if displacements are ignored)
* @return geodetic point at the center of the offset frame
* @since 9.1
*/
public GeodeticPoint getOffsetGeodeticPoint(final AbsoluteDate date) {
// take station offset into account
final double x = eastOffsetDriver.getValue();
final double y = northOffsetDriver.getValue();
final double z = zenithOffsetDriver.getValue();
final BodyShape baseShape = baseFrame.getParentShape();
final StaticTransform baseToBody = baseFrame.getStaticTransformTo(baseShape.getBodyFrame(), date);
Vector3D origin = baseToBody.transformPosition(new Vector3D(x, y, z));
if (date != null) {
origin = origin.add(computeDisplacement(date, origin));
}
return baseShape.transform(origin, baseShape.getBodyFrame(), date);
}
/** Get the geodetic point at the center of the offset frame.
* @param <T> type of the field elements
* @param date current date(<em>must</em> be non-null, which is a more stringent condition
* * than in {@link #getOffsetGeodeticPoint(AbsoluteDate)}
* @return geodetic point at the center of the offset frame
* @since 12.1
*/
public <T extends CalculusFieldElement<T>> FieldGeodeticPoint<T> getOffsetGeodeticPoint(final FieldAbsoluteDate<T> date) {
// take station offset into account
final double x = eastOffsetDriver.getValue();
final double y = northOffsetDriver.getValue();
final double z = zenithOffsetDriver.getValue();
final BodyShape baseShape = baseFrame.getParentShape();
final FieldStaticTransform<T> baseToBody = baseFrame.getStaticTransformTo(baseShape.getBodyFrame(), date);
FieldVector3D<T> origin = baseToBody.transformPosition(new Vector3D(x, y, z));
origin = origin.add(computeDisplacement(date.toAbsoluteDate(), origin.toVector3D()));
return baseShape.transform(origin, baseShape.getBodyFrame(), date);
}
/** {@inheritDoc} */
@Override
public PVCoordinatesProvider getPVCoordinatesProvider() {
final GeodeticPoint offsetPoint = getOffsetGeodeticPoint(AbsoluteDate.ARBITRARY_EPOCH);
return new TopocentricFrame(baseFrame.getParentShape(), offsetPoint, "offset");
}
/** {@inheritDoc} */
@Override
public FieldPVCoordinatesProvider<Gradient> getFieldPVCoordinatesProvider(final int freeParameters,
final Map<String, Integer> parameterIndices) {
return new FieldPVCoordinatesProvider<>() {
@Override
public TimeStampedFieldPVCoordinates<Gradient> getPVCoordinates(final FieldAbsoluteDate<Gradient> date,
final Frame frame) {
// take station offsets into account
final FieldVector3D<Gradient> origin = getOrigin(date, parameterIndices);
// body-fixed body-centered to target (with linear approximation for performance)
final Transform bodyToInertNonField = baseFrame.getParent().getTransformTo(frame, date.toAbsoluteDate());
final FieldTransform<Gradient> bodyToInert = new FieldTransform<>(date.getField(),
bodyToInertNonField).shiftedBy(date.durationFrom(date.toAbsoluteDate()));
final TimeStampedFieldPVCoordinates<Gradient> zeroPV = new TimeStampedFieldPVCoordinates<>(date,
new FieldPVCoordinates<>(origin, FieldVector3D.getZero(date.getField())));
return bodyToInert.transformPVCoordinates(zeroPV);
}
@Override
public FieldVector3D<Gradient> getPosition(final FieldAbsoluteDate<Gradient> date, final Frame frame) {
// take station offsets into account
final FieldVector3D<Gradient> origin = getOrigin(date, parameterIndices);
// body-fixed body-centered to target (with linear approximation for performance)
final KinematicTransform bodyToInertNonField = baseFrame.getParent().getKinematicTransformTo(frame,
date.toAbsoluteDate());
final FieldStaticTransform<Gradient> bodyToInert = shiftKinematicTransform(bodyToInertNonField,
date.durationFrom(date.toAbsoluteDate()));
// combine by hand for performance reasons
return bodyToInert.getRotation().applyTo(bodyToInert.getTranslation().add(origin));
}
};
}
/**
* Retrieve station's position in body shape frame.
* @param date date
* @param indices mapping from parameters' name to derivatives' index.
* @return origin position
*/
protected FieldVector3D<Gradient> getOrigin(final FieldAbsoluteDate<Gradient> date,
final Map<String, Integer> indices) {
// compute position in topocentric frame
final int freeParameters = date.getField().getZero().getFreeParameters();
final AbsoluteDate absoluteDate = date.toAbsoluteDate();
final Gradient x = eastOffsetDriver.getValue(freeParameters, indices, absoluteDate);
final Gradient y = northOffsetDriver.getValue(freeParameters, indices, absoluteDate);
final Gradient z = zenithOffsetDriver.getValue(freeParameters, indices, absoluteDate);
final FieldVector3D<Gradient> position = new FieldVector3D<>(x, y, z);
// approximate linearly (for performance) static transform from topocentric to body shape frame
final Frame bodyFrame = baseFrame.getParentShape().getBodyFrame();
final KinematicTransform kinematicTopoToBody = baseFrame.getKinematicTransformTo(bodyFrame, absoluteDate);
final FieldStaticTransform<Gradient> staticTopoToBody = shiftKinematicTransform(kinematicTopoToBody,
date.durationFrom(absoluteDate));
// apply transform and displacement
final FieldVector3D<Gradient> originBeforeDisplacement = staticTopoToBody.transformPosition(position);
return originBeforeDisplacement.add(computeDisplacement(absoluteDate, originBeforeDisplacement.toVector3D()));
}
/**
* Shift a kinematic transform by a Gradient time into a FieldStaticTransform.
* @param kinematicTransform kinematic transform to shift
* @param dt time to shift by
* @return Field static transform shifted by dt
* @since 14.0
*/
protected FieldStaticTransform<Gradient> shiftKinematicTransform(final KinematicTransform kinematicTransform,
final Gradient dt) {
// shift translation
final Field<Gradient> field = dt.getField();
final AbsoluteDate date = kinematicTransform.getDate();
final FieldVector3D<Gradient> fieldVelocity = new FieldVector3D<>(field, kinematicTransform.getVelocity());
final FieldVector3D<Gradient> shiftedTranslation = fieldVelocity.scalarMultiply(dt).add(kinematicTransform.getTranslation());
// shift rotation
final FieldAngularCoordinates<Gradient> fieldAngularCoordinates = new FieldAngularCoordinates<>(field,
new AngularCoordinates(kinematicTransform.getRotation(), kinematicTransform.getRotationRate()));
final FieldVector3D<Gradient> rotationRate = fieldAngularCoordinates.getRotationRate();
final Gradient rate = rotationRate.getNorm();
final FieldRotation<Gradient> shiftedRotation = (rate.getReal() == 0.0) ?
fieldAngularCoordinates.getRotation() :
new FieldRotation<>(rotationRate, rate.multiply(dt), RotationConvention.FRAME_TRANSFORM)
.compose(fieldAngularCoordinates.getRotation(), RotationConvention.VECTOR_OPERATOR);
return FieldStaticTransform.of(new FieldAbsoluteDate<>(field, date).shiftedBy(dt), shiftedTranslation,
shiftedRotation);
}
/** {@inheritDoc} */
@Override
public Transform getOffsetToInertial(final Frame inertial, final AbsoluteDate date,
final boolean clockOffsetAlreadyApplied) {
// take clock offset into account
final AbsoluteDate offsetCompensatedDate = clockOffsetAlreadyApplied ?
date :
new AbsoluteDate(date, -getOffsetValue(date));
final TopocentricFrame topocentricFrame = (TopocentricFrame) getPVCoordinatesProvider();
return topocentricFrame.getTransformTo(inertial, offsetCompensatedDate);
}
/** {@inheritDoc} */
@Override
public FieldTransform<Gradient> getOffsetToInertial(final Frame inertial,
final FieldAbsoluteDate<Gradient> offsetCompensatedDate,
final int freeParameters,
final Map<String, Integer> indices) {
// take station offsets into account
final FieldVector3D<Gradient> origin = getOrigin(offsetCompensatedDate, indices);
final FieldGeodeticPoint<Gradient> originGP = baseFrame.getParentShape().transform(origin, baseFrame.getParent(),
offsetCompensatedDate);
final FieldStaticTransform<Gradient> staticOffsetToBody = TopocentricTransformProvider.getTransform(baseFrame.getParentShape(),
offsetCompensatedDate, originGP).getStaticInverse();
final FieldTransform<Gradient> offsetToBody = new FieldTransform<>(offsetCompensatedDate,
staticOffsetToBody.getTranslation(), staticOffsetToBody.getRotation());
// Body-fixed, body-centered frame to target one
final FieldTransform<Gradient> bodyToInert = baseFrame.getParent().getTransformTo(inertial, offsetCompensatedDate);
// combine all transforms together
return new FieldTransform<>(offsetCompensatedDate, offsetToBody, bodyToInert);
}
}