Frame Conversions¶
Purpose¶
This specification defines the mathematical contract for LuPNT frame
conversion functions such as ConvertFrame,
GetFrameRotationTranslation, and the Earth/Moon frame-specific conversion
helpers. The key requirements are explicit epoch scale, frame origin,
rotation, translation, and velocity conventions.
The public dispatch (ConvertFrame, GetFrameRotationTranslation,
GetFrameRotationTranslationRv, GetFrameCenter) lives in
cpp/lupnt/conversions/frame_converter.cc /
frame_converter.h; the individual body-pair rotations
(GcrfToItrf, MoonCiToPa, the IAU planet orientation, …) live in
cpp/lupnt/conversions/frame_conversions.cc / frame_conversions.h.
Epoch Contract¶
Frame conversions use epochs expressed as TDB seconds from the LuPNT epoch origin:
If an input time is in another time scale, it must be converted to TDB before calling frame conversion functions. Earth-orientation internals convert from TDB to TT, UT1, or UTC as needed to evaluate precession-nutation, Earth rotation, polar motion, and EOP corrections.
Affine Position Transform¶
For position-only conversions, LuPNT represents the mapping from frame \(A\) to frame \(B\) as an affine transform:
Here \(R_{BA}\) rotates vector components from frame \(A\) axes into frame \(B\) axes, and \(p_{BA}\) is the position of the origin of frame \(A\) expressed in frame \(B\).
GetFrameRotationTranslation returns exactly the pair
\((R_{BA}, p_{BA})\).
Implemented by
cpp/lupnt/conversions/frame_converter.cc :: GetFrameRotationTranslation,
which recovers \((R, p)\) by transforming the identity basis and the zero
vector through ConvertFrame:
Mat3 M = ConvertFrame(t_tdb, I3, from_frame, to_frame); // basis images + offset
Vec3 r = ConvertFrame(t_tdb, MatX3::Zero(1, 3), from_frame, to_frame).transpose();
Mat3 R = (M.rowwise() - r.transpose()).transpose(); // subtract offset -> rotation
return std::make_pair(R, r);
Cartesian State Transform¶
For a Cartesian state, the full transform is
For a pure time-dependent rotation and translation,
GetFrameRotationTranslationRv returns the affine six-state equivalent
\((R_{6,BA}, p_{6,BA})\) by applying ConvertFrame to the Cartesian
basis states and the zero state.
Implemented by
cpp/lupnt/conversions/frame_converter.cc :: GetFrameRotationTranslationRv:
Vec6 t6 = ConvertFrame(t_tdb, MatX6::Zero(1, 6), from_frame, to_frame, false).row(0).transpose();
MatX6 Y = ConvertFrame(t_tdb, Mat6::Identity(), from_frame, to_frame, false); // 6x6
for (int j = 0; j < 6; ++j) R6.col(j) = Y.row(j).transpose() - t6; // columns of R6
return {R6, t6};
Rotate-Only Mode¶
When rotate_only is true, ConvertFrame applies only the rotation
returned by GetFrameRotationTranslation:
This mode is useful for vector-like quantities such as accelerations or local
offsets where the origin velocity terms should not be applied. For ordinary
position-velocity state transformations, rotate_only should be false.
Implemented by the rotate_only branch of the Vec6 overload of
cpp/lupnt/conversions/frame_converter.cc :: ConvertFrame:
Vec6 ConvertFrame(Real t_tdb, const Vec6& rv_in, Frame frame_in, Frame frame_out, bool rotate_only) {
if (!rotate_only) return ConvertFrameBase(t_tdb, rv_in, frame_in, frame_out);
auto [R, r] = GetFrameRotationTranslation(t_tdb, frame_in, frame_out);
Vec6 rv_out;
rv_out.head(3) = R * rv_in.head(3) + r; // position: rotate + translate
rv_out.tail(3) = R * rv_in.tail(3); // velocity: rotate only
return rv_out;
}
Frame Graph¶
The implemented native conversion graph is centered on GCRF and MOON_CI:
Earth-fixed and Earth inertial frames attach through GCRF:
Lunar fixed and lunar orbit-plane frames attach through MOON_CI:
The Earth-Moon rotating frame EMR attaches through GCRF.
The routing itself is
cpp/lupnt/conversions/frame_converter.cc :: ConvertFrameBase (a recursive
switch on frame_in that hops through the GCRF / MOON_CI / ICRF hubs);
solar-system planet frames are split off first to
ConvertPlanetFrame:
// ConvertFrameBase -- e.g. GCRF -> any lunar frame routes through MOON_CI
case Frame::GCRF:
switch (frame_out) {
case Frame::ITRF: return GcrfToItrf(t_tdb, rv_in);
case Frame::MOON_PA: case Frame::MOON_ME: case Frame::MOON_OP: case Frame::MOON_CI:
return ConvertFrame(t_tdb, GcrfToMoonCi(t_tdb, rv_in), Frame::MOON_CI, frame_out);
}
Frame Centers¶
Each supported frame has a natural center used by ephemeris and conversion helpers:
Implemented by the frame_centers map and
cpp/lupnt/conversions/frame_converter.cc :: GetFrameCenter (also covers the
solar-system <PLANET>_CI / <PLANET>_FIXED frames).
GCRF and ICRF¶
GCRF and ICRF are related by the Earth barycentric state. If \(r_{\mathrm{SSB}\rightarrow E}\) and \(v_{\mathrm{SSB}\rightarrow E}\) are evaluated from planetary ephemerides in GCRF axes, then
The inverse subtracts the same Earth barycentric state.
Implemented by
cpp/lupnt/conversions/frame_conversions.cc :: GcrfToIcrf /
IcrfToGcrf (a pure translation by the SSB-to-Earth ephemeris state).
GCRF and ITRF¶
The transformation from the geocentric celestial frame (GCRF, an Earth-centered realization of the inertial ICRF axes) to the Earth-fixed ITRF follows the IAU 2006/2000A CIO-based (Celestial Intermediate Origin) convention. It is the product of three rotations, each evaluated at the time scale it physically depends on:
Read right to left, a GCRF vector is first carried to the Celestial Intermediate Reference System (bias–precession–nutation), then rotated about the Celestial Intermediate Pole (CIP) through the sidereal Earth Rotation Angle, and finally tilted into the terrestrial pole by polar motion.
Precession–nutation \(R_\mathrm{PN}\). LuPNT reads the CIP unit-vector coordinates \(X,Y\) and the CIO locator \(s\) from tabulated IAU 2006/2000A (SOFA) series evaluated at TT, and builds the celestial-to- intermediate matrix directly from them:
Earth rotation \(R_\mathrm{ERA}=R_z(\theta_\mathrm{ERA})\) is a rotation about the CIP by the Earth Rotation Angle, which is linear in UT1:
with \(D_\mathrm{UT1}\) the number of UT1 days since J2000. UT1 is recovered
from the fitted \(\mathrm{UT1}-\mathrm{UTC}\) when a SPICE/EOP model covers
t_tdb, otherwise from the TDB\(\to\)UT1 conversion. Because
\(\theta_\mathrm{ERA}\) advances at the Earth rotation rate
\(\omega_\oplus\approx7.292115\times10^{-5}\ \mathrm{rad/s}\), this is the
only factor whose time derivative matters for the velocity transform.
Polar motion \(R_\mathrm{polar}\) aligns the CIP with the ITRF pole using the pole offsets \(x_p,y_p\) (from the fitted EOP, or the bundled EOP table sampled at UTC) and the TIO locator \(s' = -47\ \mu''\times(t_\mathrm{TT}/36525\ \text{d})\):
The position and velocity mapping is then
where the rotation-rate term keeps only the dominant Earth-rotation derivative,
so \(\dot{R}\,r_\mathrm{GCRF}\) reproduces the \(\omega_\oplus\times r\) transport velocity (the slow precession–nutation and polar-motion rates are neglected).
The inverse is
Implemented by
cpp/lupnt/conversions/frame_conversions.cc :: GcrfToItrf (and the inverse
ItrfToGcrf); the rotation product uses RotPolarMotion /
RotSideralMotion / RotPrecessionNutation:
Mat3 R_GcrfToItrf = R_po * R_s * R_pn; // R_polar R_ERA R_PN
Mat3 R_GcrfToItrf_dot = R_po * R_s_dot * R_pn;
Vec3 r_itrf = R_GcrfToItrf * r_gcrf;
Vec3 v_itrf = R_GcrfToItrf * v_gcrf + R_GcrfToItrf_dot * r_gcrf;
Note
If InitFrameConversionFromSpice has fitted a SPICE-derived EOP model
covering t_tdb, RotPolarMotion / RotSideralMotion transparently
use the fitted EOP so this matches spice::ConvertFrameSpice closely.
GCRF and EME¶
GCRF and EME are related by the Earth frame-bias rotation
The same static rotation is applied to both position and velocity.
Implemented by the static bias matrix
cpp/lupnt/conversions/frame_conversions.cc :: RotGcrfToEme, applied by
GcrfToEme / EmeToGcrf:
Mat3d RotGcrfToEme() {
return RotX(-FRAME_BIAS_ETA0) * RotY(FRAME_BIAS_XI0) * RotZ(FRAME_BIAS_DALPHA0);
}
GCRF and MOON_CI¶
MOON_CI is Moon-centered and axis-aligned with the inertial GCRF axes. Let \(r_{E\rightarrow M}\) and \(v_{E\rightarrow M}\) be the Moon state relative to Earth. Then
The inverse adds the same Earth-to-Moon state.
Implemented by
cpp/lupnt/conversions/frame_conversions.cc :: GcrfToMoonCi /
MoonCiToGcrf (a pure translation by the Earth-to-Moon ephemeris state):
Vec6 GcrfToMoonCi(Real t_tdb, const Vec6& rv_gcrf) {
Vec6 rv_earth2moon = GetBodyPosVel(t_tdb, BodyId::EARTH, BodyId::MOON, Frame::GCRF);
return rv_gcrf - rv_earth2moon;
}
MOON_CI and MOON_PA¶
MOON_PA is the Moon principal-axis fixed frame. The native rotation is represented by lunar mantle libration angles \((\phi,\theta,\psi)\):
The velocity transform includes the exact derivative of this rotation:
The inverse subtracts the rotation-rate contribution before applying the transpose rotation.
Implemented by
cpp/lupnt/conversions/frame_conversions.cc :: RotMoonCiToPa (the ZXZ angle
reconstruction and its analytic \(\dot R\)), applied by MoonCiToPa /
MoonPaToCi:
auto [phi, theta, psi, phi_dot, theta_dot, psi_dot] = Unpack(GetLunarMantleData(t_tdb, true));
Mat3 R_mi2pa = RotZ(psi) * RotX(theta) * RotZ(phi);
*R_mi2pa_dot = RotZdot(psi, psi_dot) * RotX(theta) * RotZ(phi)
+ RotZ(psi) * RotXdot(theta, theta_dot) * RotZ(phi)
+ RotZ(psi) * RotX(theta) * RotZdot(phi, phi_dot);
Note
If InitFrameConversionFromSpice has fitted a lunar-orientation model
covering t_tdb, the SPICE-fitted MOON_CI->MOON_PA rotation (and its
exact derivative) is used in place of the DE-Chebyshev libration angles
(TryGetFittedMoonCiToPa).
MOON_PA and MOON_ME¶
MOON_ME is related to MOON_PA through a static bias rotation:
The same static rotation is applied to both position and velocity.
Implemented by the static matrix
cpp/lupnt/conversions/frame_conversions.cc :: RotMoonPaToMe, applied by
MoonPaToMe / MoonMeToPa:
Mat3d RotMoonPaToMe() {
return RotX(-0.2785 * RAD_ARCSEC) * RotY(-78.6944 * RAD_ARCSEC) * RotZ(-67.8526 * RAD_ARCSEC);
}
MOON_OP¶
MOON_OP is the lunar orbit-plane frame. It is built from the Moon-to-Earth state and the Moon mean-Earth pole. Let
and let \(\hat{p}\) be the Moon ME z-axis expressed in MOON_CI. Then
The rotation from MOON_OP to MOON_CI is
The current implementation applies this rotation to both position and velocity without an explicit \(\dot{R}\) term for MOON_OP.
Implemented by
cpp/lupnt/conversions/frame_conversions.cc :: RotMoonOpToCi, applied by
MoonOpToCi / MoonCiToOp:
Vec6 rv_m2e = GetBodyPosVel(t_tdb, BodyId::MOON, BodyId::EARTH, Frame::GCRF);
Vec3 z_op = rv_m2e.head(3).cross(rv_m2e.tail(3)).normalized();
Vec3 i_pole = ConvertFrame(t_tdb, Vec3::UnitZ(), Frame::MOON_ME, Frame::MOON_CI);
Vec3 x_op = i_pole.cross(z_op).normalized();
Vec3 y_op = z_op.cross(x_op).normalized();
R_op2ci << x_op, y_op, z_op;
Solar-System Planet Frames (IAU Orientation)¶
Each solar-system <PLANET>_CI frame is ICRF-aligned and centered on the
planet, and each <PLANET>_FIXED frame co-rotates with it under the IAU
linear orientation model. With pole right ascension/declination
\((\alpha, \delta)\) and prime-meridian angle \(W\), the ICRF-to-fixed
rotation is
with \((\alpha, \delta)\) linear in Julian centuries past J2000 (TT) and \(W\) linear in days past J2000 (TT). Only the prime-meridian spin contributes appreciably to \(\dot{R}\).
Implemented by
cpp/lupnt/conversions/frame_conversions.cc :: RotBodyCiToFixed (with the
per-body coefficients in IauOrientationTable), applied by
BodyCiToFixed / BodyFixedToCi; the frame predicates
IsPlanetFrame / IsPlanetFixedFrame / IsPlanetCiFrame classify these
frames, and
cpp/lupnt/conversions/frame_converter.cc :: ConvertPlanetFrame routes them
through the ICRF hub (with a same-body fast path):
// RotBodyCiToFixed
Real ra = (o.ra0 + o.ra0_T * T) * RAD;
Real dec = (o.dec0 + o.dec0_T * T) * RAD;
Real W = (o.w0 + o.w_dot * d) * RAD;
Mat3 R = RotZ(W) * RotX(M_PI / 2 - dec) * RotZ(M_PI / 2 + ra);
if (R_dot) *R_dot = RotZdot(W, o.w_dot * RAD / SECS_DAY) * RotX(M_PI / 2 - dec) * RotZ(M_PI / 2 + ra);
SPICE-Fitted Orientation Option¶
InitFrameConversionFromSpice can fit the native Earth and lunar
orientation paths to SPICE over a requested TDB time window. It samples SPICE
rotations at Chebyshev-Gauss nodes and fits ZXZ Euler angles
\((\phi,\theta,\psi)\) over piecewise Chebyshev segments:
At evaluation time the fitted angles reconstruct
and \(\dot{R}(t)\) is obtained by differentiating the Chebyshev polynomials and applying the product rule. The reconstructed rotation remains orthonormal to floating-point precision because it is a product of elementary rotation matrices.
If the requested epoch is outside the fitted time window, LuPNT falls back to the native analytic Earth or lunar orientation model.
Implemented by
cpp/lupnt/conversions/frame_conversions.cc :: InitFrameConversionFromSpice
(the Euler-angle fit is FitEulerAngleModel; the fitted lunar rotation is
consumed by TryGetFittedMoonCiToPa, and the fitted EOP by the Earth
rotation helpers).
Coordinate Scale and Units¶
Frame conversion functions themselves do not change numerical units. If the input state is in kilometers, the output is in kilometers. If it is in meters, the output is in meters.
Ephemeris helpers such as GetBodyPosVel can additionally scale positions
and gravitational parameters between supported coordinate scales. For a
constant coordinate scale factor \(F\), LuPNT uses
Frame conversion callers are responsible for keeping coordinate scale and unit choices consistent with the states being transformed.