open-standards/map/map-rs/crates/map-orbital/src/ecef.rs
Transaction Science ba1212361f Add aac, map, sandbox standards; catalog the standards family
Commit the previously-untracked standard directories and catalog them in the
root README alongside proof:

- aac/    — communication cargo class: portable boards + a ten-level
            offline-first cascade (Apache-2.0 code / CC-BY-4.0 spec).
- map/    — geospatial cargo class: range-requestable signed tilesets +
            an F0–F4 fidelity cascade (Apache-2.0 code / CC-BY-4.0 spec).
- sandbox/— isolated, metered, attested execution across the heterogeneous
            compute fabric (Apache-2.0 code / CC-BY-4.0 spec).
- jouleclaw-otel-authz, jouleclaw-trust — new jouleclaw reference crates.

Root README now lists aac, map, sandbox, and proof in the catalog, the tree,
and the contribution guide.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-07 17:04:36 -04:00

114 lines
3 KiB
Rust

//! ECI (TEME) to ECEF coordinate transform.
//!
//! Rotates from the True Equator Mean Equinox (TEME) frame used by
//! SGP4 to the Earth-Centered Earth-Fixed (ECEF) frame using the
//! Greenwich Mean Sidereal Time (GMST) rotation.
use serde::{Deserialize, Serialize};
use crate::julian::gmst;
use crate::sgp4::EciPosition;
/// Position in the Earth-Centered Earth-Fixed (ECEF) frame.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct EcefPosition {
/// X position (km).
pub x: f64,
/// Y position (km).
pub y: f64,
/// Z position (km).
pub z: f64,
}
/// Rotate an ECI (TEME) position to ECEF using the GMST at the given Julian Date.
///
/// The TEME-to-ECEF rotation is a simple Z-axis rotation by -GMST:
/// ```text
/// ecef_x = eci_x * cos(gmst) + eci_y * sin(gmst)
/// ecef_y = -eci_x * sin(gmst) + eci_y * cos(gmst)
/// ecef_z = eci_z
/// ```
pub fn eci_to_ecef(eci: &EciPosition, jd: f64) -> EcefPosition {
let theta = gmst(jd);
let cos_theta = theta.cos();
let sin_theta = theta.sin();
EcefPosition {
x: eci.x * cos_theta + eci.y * sin_theta,
y: -eci.x * sin_theta + eci.y * cos_theta,
z: eci.z,
}
}
/// Rotate an ECEF position to ECI (TEME) at the given Julian Date.
///
/// Inverse of [`eci_to_ecef`].
pub fn ecef_to_eci(ecef: &EcefPosition, jd: f64) -> EciPosition {
let theta = gmst(jd);
let cos_theta = theta.cos();
let sin_theta = theta.sin();
EciPosition {
x: ecef.x * cos_theta - ecef.y * sin_theta,
y: ecef.x * sin_theta + ecef.y * cos_theta,
z: ecef.z,
vx: 0.0,
vy: 0.0,
vz: 0.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ecef_roundtrip() {
let eci = EciPosition {
x: 6500.0,
y: 1000.0,
z: 3000.0,
vx: 0.0,
vy: 0.0,
vz: 0.0,
};
let jd = 2451545.0; // J2000.0
let ecef = eci_to_ecef(&eci, jd);
let eci2 = ecef_to_eci(&ecef, jd);
assert!((eci.x - eci2.x).abs() < 1e-9, "x: {} vs {}", eci.x, eci2.x);
assert!((eci.y - eci2.y).abs() < 1e-9, "y: {} vs {}", eci.y, eci2.y);
assert!((eci.z - eci2.z).abs() < 1e-9, "z: {} vs {}", eci.z, eci2.z);
}
#[test]
fn test_ecef_z_unchanged() {
let eci = EciPosition {
x: 1000.0,
y: 2000.0,
z: 5000.0,
vx: 0.0,
vy: 0.0,
vz: 0.0,
};
let ecef = eci_to_ecef(&eci, 2451545.0);
assert!((eci.z - ecef.z).abs() < 1e-12);
}
#[test]
fn test_ecef_preserves_magnitude() {
let eci = EciPosition {
x: 6500.0,
y: 1000.0,
z: 3000.0,
vx: 0.0,
vy: 0.0,
vz: 0.0,
};
let ecef = eci_to_ecef(&eci, 2460000.0);
let r_eci = (eci.x * eci.x + eci.y * eci.y + eci.z * eci.z).sqrt();
let r_ecef = (ecef.x * ecef.x + ecef.y * ecef.y + ecef.z * ecef.z).sqrt();
assert!((r_eci - r_ecef).abs() < 1e-9);
}
}