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]>
114 lines
3 KiB
Rust
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);
|
|
}
|
|
}
|