Stations and Body Orientation#
Last revised by G. Fereoli on 2026 OCT 11
A GroundStation sits on a rotating body. Where it is at an epoch
depends on three things: its coordinates, the orientation of the body-fixed frame, and
the surface displacements (tides) acting on it. Each can be modelled, and most can be
estimated.
Body orientation#
All orientation models live in BodyOrientation.
Constructor |
Purpose |
|---|---|
|
Small rigid rotation of a station, in radians, on any body. |
|
The same correction expressed as Earth polar motion and UT1. |
|
Celestial pole offset (nutation correction). |
|
Absolute Earth orientation, written to an ITRF93 PCK. |
|
Absolute PCK for any body from a user rotation law. |
earth_eop takes model="EOP1" (ITRF93 legacy: IAU1976/1980, equinox-based) or
model="EOP2" (IERS 2010: IAU2006/2000A, CIO/ERA). Both PCKs use class ID 3000, so
existing ITRF93 station SPKs follow the selected orientation.
scb.SpiceManager.load_kernel("data/kernels/locked/lsk/naif0012.tls")
orientation = scb.BodyOrientation.earth_eop(
pole_x=np.deg2rad(6.4125e-5), pole_y=np.deg2rad(1.341694444444444e-4),
tai_minus_ut1=37.00446633384733, model="EOP1")
start = scb.SpiceManager.utc2et("2029 APR 25 00:00:00 UTC")
path = orientation.write_pck("earth_absolute.bpc", start, start + 86400.)
scb.SpiceManager.load_kernel(str(path)) # load after any other Earth PCK
The PCK must cover every transmit and receive epoch, including Doppler count intervals.
Signs. A correction rotates the station by \(\boldsymbol\varepsilon = (-\Delta y_p,\ -\Delta x_p,\ \omega_\oplus\Delta UT1)\), with \(\Delta\mathbf r = \boldsymbol\varepsilon\times\mathbf r\). An estimated \(\Delta UT1\) maps to the absolute input as \(\Delta(TAI-UT1) = -\Delta UT1\).
Time scale. ut1_time_scale="TAI" (default) is correct. "TDB" skips TDB−TT
(up to 1.66 ms, about 0.5 m at the equator). Use it only to reproduce a tool that
makes the same omission.
Station coordinates#
By default a station is read from its SPK, which also carries plate motion. A station can instead be given from a table:
km, deg = scb.Units.get_units(["km", "deg"])
station = scb.GroundStation.from_cylindrical(
"DSS-24", 399024, body_fixed_frame="ITRF93",
longitude=scb.ArrayWUnits(-116.8747939, deg),
cylindrical_radius=scb.ArrayWUnits(5209.482541999999, km),
z=scb.ArrayWUnits(3669.24231, km))
body_fixed_position= accepts Cartesian coordinates instead. A table station has no
SPK, so it is queried through station.nominal_state, and plate motion is the
user’s to apply. DopplerReal and
SequentialRangingReal need a station SPK and raise
Measurement.StationKernelRequiredError without one.
On another body, give the station its central body and frame:
station = scb.GroundStation("MARS_STATION", 499001,
central_body="MARS", body_fixed_frame="IAU_MARS")
station.body_orientation = scb.BodyOrientation(
rotation_vector=[1e-7, -2e-7, 3e-7], body_fixed_frame="IAU_MARS")
Surface displacement#
Tides are part of where the station is, so they enter nominal_state rather than
an estimated correction:
station.surface_displacement = [scb.SolidTide(),
scb.OceanLoading(amplitudes, phases),
scb.PoleTide(pole_x=xp, pole_y=yp)]
SolidTide: IERS degree-2 in-phase, up to about 0.4 m. Keepremove_permanent_tide=Falsefor tide-free coordinates such as ITRF and DSN tables.The same object also perturbs the gravity field, through \(k_2\) instead of \(h_2, l_2\):
sph_harm.solid_tide = tide.Other bodies have no built-in Love numbers and raise
UnknownTidalResponseError. Give them explicitly, for examplescb.SolidTide(central_body="MARS", love_h2=0.17, shida_l2=0.02, love_k={(2, 0): 0.169}, tide_bodies=("SUN", "PHOBOS")).Omitted: degree 3 (~2 mm), out-of-phase terms (a few mm), step-2 corrections (up to ~13 mm), atmospheric loading.
Estimable parameters#
Name |
Units |
Meaning |
|---|---|---|
|
length (3) |
Cartesian station offset in its body-fixed frame |
|
length (3) |
\((\Delta\rho,\ \rho\Delta\lambda,\ \Delta z)\); set
|
|
angle |
Polar motion correction |
|
time |
UT1 correction |
|
angle |
Celestial pole offset correction |
|
angle (3) |
Rigid rotation of the station frame, any body |
|
angle |
IAU pole and prime meridian of the central body |
|
deg/day |
IAU prime-meridian rate |
|
— |
Solid-tide response of the body |
|
length |
Zenith delays (Chao wet mapping; ionosphere at 2295 MHz, scaled by \((2295/f)^2\)). Real radiometric models only. |
The IAU pole parameters orient the central body’s gravity field, its optical shape and its stations together. Each estimate replaces its secular term, so start it from the secular value at the arc epoch, not from the PCK constant.
km, sec, rad = scb.Units.get_units(["km", "sec", "rad"])
itrf, earth = scb.Frame("ITRF93"), scb.Body("EARTH", 399)
gs = scb.GroundStation("DSS-24")
state_def = scb.StateDefinition()
state_def.param("gs_delta_location_CYL_DSS24", gs,
scb.ArrayWFrame(np.zeros(3), km, itrf), dynamics="static")
for name, unit in [("eop_delta_pole_x", rad), ("eop_delta_pole_y", rad),
("eop_delta_ut1", sec)]:
state_def.param(name, earth, scb.ArrayWFrame(np.array(0.), unit, itrf),
dynamics="static")
reference = scb.StateArray(scb.EpochArray(start, sys="TDB"), earth, state_def)
model = scb.RangeIdeal("DSS24 range", gs, sigma=scb.ArrayWUnits(.001, km))
model.update_reference_state(reference)
A parameter owned by the central body applies to all its stations; one owned by a station applies to that station only.
Partials are analytical. The geometric models use
\(\partial h/\partial p = -[\partial h/\partial\mathbf r,\ \partial h/\partial\mathbf v]\,\partial\mathbf X_{station}/\partial p\).
The light-time models weight the station Jacobian at both ends of the light path,
\(\mathbf g_3\,\partial\mathbf r/\partial p(t_3) + \mathbf g_1\,\partial\mathbf r/\partial p(t_1)\).
partials_method="fd" uses eps_dict, falling back to
Measurement.station_eps_defaults. The camera model
(CentroidingIdeal) has no station, so its station columns are zero.
Warning
Station coordinates and a rigid orientation are strongly correlated: a rotation can be absorbed by shifting the stations. Constrain the station priors before solving for both.
References#
ERFA (
pyerfa>=2.0)