Coordinate Frames¶
Rotations between frames are unit quaternions (satkit::Quaternion); applying one to a Vector3 with * rotates the vector into the target frame. The Coordinate Frames and Geodetic Coordinates tutorials explain the frames and the IERS 2010 reduction; this page shows the Rust API.
Rotations: frametransform¶
The Frame enum names the frames: ITRF, TIRS, CIRS, GCRF, EME2000, ICRF, TEME, and the orbit-local RTN, NTW and LVLH. Three functions in frametransform cover every pair:
| Function | Frames | Returns |
|---|---|---|
rotation(from, to, &t) |
any pair of Earth / inertial frames, full IERS 2010 reduction | Result<Quaternion> |
rotation_approx(from, to, &t) |
ITRF and the inertial frames, the cheaper ~1″ reduction | Result<Quaternion> |
rotation_with_state(from, to, &t, &pos, &vel) |
any pair, including RTN / NTW / LVLH, whose axes come from a GCRF position and velocity | Result<Quaternion> |
transform_state(from, to, &t, &pos, &vel) |
position and velocity, including the Earth-rotation term ω × r between rotating and inertial frames | Result<(Vector3, Vector3)> |
The fixed-pair functions (qitrf2gcrf, qgcrf2itrf, qteme2itrf, qteme2gcrf, ...) return the quaternion directly, with no Result; qteme2itrf is the one SGP4 output needs. to_gcrf(frame, &pos, &vel) / from_gcrf give the RTN / NTW / LVLH rotation as a 3×3 matrix, and gcrf_to_rtn and friends name each one.
The rotations to and from ITRF read the Earth orientation parameters (UT1 − UTC, polar motion, celestial-pole offsets) from finals2000A.all. earth_orientation_params::status(&t) says whether an epoch is observed, predicted, extrapolated or not covered. Without the file the rotations warn once and use zeros, which is off by up to ~12″; run satkit::utils::update_datafiles to fetch or refresh it.
Geodetic coordinates: ITRFCoord¶
ITRFCoord is an Earth-fixed position. Build it from geodetic latitude, longitude (degrees or radians) and height above the WGS84 ellipsoid, or from an ITRF Vector3; read back latitude_deg(), longitude_deg(), hae() or to_geodetic(). to_enu(&origin) / to_ned(&origin) give the vector from origin in its local East-North-Up / North-East-Down frame, and geodesic_distance the distance and headings along the ellipsoid (Vincenty).
Example¶
//! Coordinate frames: geodetic coordinates, ITRF <-> GCRF / TEME rotations,
//! state transforms, and local (ENU) and orbit-local (RTN) frames.
//!
//! The IERS nutation tables are compiled in, so this runs offline. The
//! Earth-fixed rotations also use Earth orientation parameters from
//! `finals2000A.all` (downloaded on first use, refreshed by
//! `satkit::utils::update_datafiles`); without that file satkit warns once
//! and uses zero polar motion and UT1 = UTC, which is off by up to ~12".
//!
//! ```text
//! cargo run --example frames
//! ```
use satkit::earth_orientation_params as eop;
use satkit::frametransform::{self, rotation, rotation_approx, transform_state};
use satkit::{Frame, ITRFCoord, Instant, Vector3};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let t = Instant::from_datetime(2024, 6, 15, 12, 0, 0.0)?;
// Earth orientation parameters in effect at `t`
println!("EOP status: {:?}", eop::status(&t));
if let Some([dut1, xp, yp, ..]) = eop::get(&t) {
println!("UT1-UTC = {dut1:.4} s, polar motion = ({xp:.4}\", {yp:.4}\")");
}
// A ground station from geodetic coordinates (degrees, meters)
let station = ITRFCoord::from_geodetic_deg(42.3601, -71.0589, 20.0);
println!("station: {station}");
println!(" ITRF = {:.1} m", station.itrf);
// Rotation between frames, as a quaternion: full IERS 2010 reduction
let q = rotation(Frame::ITRF, Frame::GCRF, &t)?;
let p_gcrf = q * station.itrf;
println!(" GCRF = {p_gcrf:.1} m");
// The approximate reduction is cheaper and good to about an arcsecond
let q_approx = rotation_approx(Frame::ITRF, Frame::GCRF, &t)?;
let (_, angle) = (q_approx.conjugate() * q).to_axis_angle();
println!(
"full vs approximate reduction: {:.3} arcsec",
angle.to_degrees() * 3600.0
);
// State transform: velocity picks up the Earth-rotation term w x r
let (p, v) = transform_state(
Frame::ITRF,
Frame::GCRF,
&t,
&station.itrf,
&Vector3::zeros(),
)?;
println!(
"station inertial speed = {:.1} m/s at {:.1} km",
v.norm(),
p.norm() / 1e3
);
// TEME (the SGP4 output frame) to ITRF, and back to geodetic
let p_teme = Vector3::from_array([6_778_137.0, 0.0, 0.0]);
let sat = ITRFCoord::from(rotation(Frame::TEME, Frame::ITRF, &t)? * p_teme);
println!("TEME x-axis point at 400 km: {sat}");
// Local East-North-Up vector from the station to another point
let target = ITRFCoord::from_geodetic_deg(40.7128, -74.0060, 10.0);
let enu = target.to_enu(&station);
let azimuth = enu[0].atan2(enu[1]).to_degrees().rem_euclid(360.0);
let (dist, heading, _) = station.geodesic_distance(&target);
println!(
"to target: ENU = {:.1} km, azimuth {azimuth:.2} deg",
enu / 1e3
);
println!(
" geodesic distance {:.1} km, initial heading {:.2} deg",
dist / 1e3,
heading.to_degrees().rem_euclid(360.0)
);
// Orbit-local frames (RTN, NTW, LVLH) need the orbit state
let r = Vector3::from_array([6_778_137.0, 0.0, 0.0]);
let vel = Vector3::from_array([0.0, 5_000.0, 5_000.0]);
let q_rtn = frametransform::rotation_with_state(Frame::GCRF, Frame::RTN, &t, &r, &vel)?;
println!("velocity in RTN = {:.1} m/s", q_rtn * vel);
Ok(())
}
$ cargo run --example frames
EOP status: Observed
UT1-UTC = -0.0164 s, polar motion = (0.0545", 0.4695")
station: ITRFCoord(lat: 42.3601 deg, lon: -71.0589 deg, altitude: 0.02 km)
ITRF = [1532143.8, -4464572.7, 4275257.9] m
GCRF = [4611302.5, 1053168.8, 4264320.4] m
full vs approximate reduction: 0.453 arcsec
station inertial speed = 344.2 m/s at 6368.5 km
TEME x-axis point at 400 km: ITRFCoord(lat: -0.0001 deg, lon: -84.2628 deg, altitude: 400.00 km)
to target: ENU = [-248.9, -178.6, -7.4] km, azimuth 234.34 deg
geodesic distance 306.5 km, initial heading 234.34 deg
velocity in RTN = [0.0, 7071.1, -0.0] m/s
See also¶
- Coordinate Frames, Quaternions and Geodetic Coordinates.
- Maneuver Coordinate Frames: the RTN, NTW and LVLH axis conventions.
- Data Coverage: what the EOP table covers.