use glam::{DMat3, DVec3};
use astrodyn_quantities::aliases::{Position, Velocity};
use astrodyn_quantities::frame::{Lvlh, Planet, PlanetInertial, Vehicle};
use astrodyn_quantities::frame_transform::FrameTransform;
use astrodyn_quantities::quat::{JeodQuat, NormalizedQuat};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LvlhFrame {
pub t_parent_this: DMat3,
pub ang_vel_this: DVec3,
pub position: DVec3,
pub velocity: DVec3,
}
impl Default for LvlhFrame {
fn default() -> Self {
Self {
t_parent_this: DMat3::IDENTITY,
ang_vel_this: DVec3::ZERO,
position: DVec3::ZERO,
velocity: DVec3::ZERO,
}
}
}
impl LvlhFrame {
pub fn compute<P: Planet>(
position: Position<PlanetInertial<P>>,
velocity: Velocity<PlanetInertial<P>>,
) -> Self {
compute_lvlh_frame_impl(position.raw_si(), velocity.raw_si())
}
}
fn compute_lvlh_frame_impl(position: DVec3, velocity: DVec3) -> LvlhFrame {
let angmom = position.cross(velocity);
let hmag = angmom.length();
let rmagsq = position.length_squared();
let rmag = rmagsq.sqrt();
assert!(rmag > 0.0, "compute_lvlh_frame: position magnitude is zero");
assert!(
hmag > 0.0,
"compute_lvlh_frame: angular momentum is zero (radial trajectory)"
);
let wmag = hmag / rmagsq;
let z_hat = -position / rmag;
let y_hat = -angmom / hmag;
let x_hat = y_hat.cross(z_hat).normalize();
let t_parent_this = DMat3::from_cols(
DVec3::new(x_hat.x, y_hat.x, z_hat.x),
DVec3::new(x_hat.y, y_hat.y, z_hat.y),
DVec3::new(x_hat.z, y_hat.z, z_hat.z),
);
let ang_vel_this = DVec3::new(0.0, -wmag, 0.0);
LvlhFrame {
t_parent_this,
ang_vel_this,
position,
velocity,
}
}
pub fn compute_lvlh_frame_typed<P, Chief>(
position: Position<PlanetInertial<P>>,
velocity: Velocity<PlanetInertial<P>>,
) -> FrameTransform<PlanetInertial<P>, Lvlh<Chief>>
where
P: Planet,
Chief: Vehicle,
{
let lvlh = compute_lvlh_frame_impl(position.raw_si(), velocity.raw_si());
let q: JeodQuat = JeodQuat::left_quat_from_transformation(&lvlh.t_parent_this);
let q_norm = NormalizedQuat::new(q)
.unwrap_or_else(|err| panic!("left_quat_from_transformation guarantees unit norm: {err}"));
FrameTransform::<PlanetInertial<P>, Lvlh<Chief>>::from_quat(q_norm)
}
#[cfg(test)]
mod tests {
use super::*;
use compute_lvlh_frame_impl as compute_lvlh_frame;
const EARTH_MU: f64 = 3.986_004_415e14;
#[test]
fn circular_equatorial_orbit() {
let r = 6_778_137.0; let v = (EARTH_MU / r).sqrt();
let position = DVec3::new(r, 0.0, 0.0);
let velocity = DVec3::new(0.0, v, 0.0);
let lvlh = compute_lvlh_frame(position, velocity);
let z_hat = DVec3::new(
lvlh.t_parent_this.col(0).z,
lvlh.t_parent_this.col(1).z,
lvlh.t_parent_this.col(2).z,
);
assert!((z_hat.x + 1.0).abs() < 1e-14, "Z-hat x: {}", z_hat.x);
assert!(z_hat.y.abs() < 1e-14, "Z-hat y: {}", z_hat.y);
assert!(z_hat.z.abs() < 1e-14, "Z-hat z: {}", z_hat.z);
let y_hat = DVec3::new(
lvlh.t_parent_this.col(0).y,
lvlh.t_parent_this.col(1).y,
lvlh.t_parent_this.col(2).y,
);
assert!(y_hat.x.abs() < 1e-14, "Y-hat x: {}", y_hat.x);
assert!(y_hat.y.abs() < 1e-14, "Y-hat y: {}", y_hat.y);
assert!((y_hat.z + 1.0).abs() < 1e-14, "Y-hat z: {}", y_hat.z);
let x_hat = DVec3::new(
lvlh.t_parent_this.col(0).x,
lvlh.t_parent_this.col(1).x,
lvlh.t_parent_this.col(2).x,
);
assert!(x_hat.x.abs() < 1e-14, "X-hat x: {}", x_hat.x);
assert!((x_hat.y - 1.0).abs() < 1e-14, "X-hat y: {}", x_hat.y);
assert!(x_hat.z.abs() < 1e-14, "X-hat z: {}", x_hat.z);
let n = (EARTH_MU / (r * r * r)).sqrt();
assert!(lvlh.ang_vel_this.x.abs() < 1e-14);
assert!((lvlh.ang_vel_this.y + n).abs() / n < 1e-10);
assert!(lvlh.ang_vel_this.z.abs() < 1e-14);
}
#[test]
fn transformation_is_orthonormal() {
let position = DVec3::new(5e6, 3e6, 4e6);
let velocity = DVec3::new(-2000.0, 5000.0, 3000.0);
let lvlh = compute_lvlh_frame(position, velocity);
let t = lvlh.t_parent_this;
let product = t * t.transpose();
let diff = product - DMat3::IDENTITY;
assert!(diff.x_axis.length() < 1e-14);
assert!(diff.y_axis.length() < 1e-14);
assert!(diff.z_axis.length() < 1e-14);
assert!((t.determinant() - 1.0).abs() < 1e-14);
}
#[test]
fn inclined_orbit() {
let r = 6_778_137.0;
let v = (EARTH_MU / r).sqrt();
let inc = 51.6_f64.to_radians();
let position = DVec3::new(r, 0.0, 0.0);
let velocity = DVec3::new(0.0, v * inc.cos(), v * inc.sin());
let lvlh = compute_lvlh_frame(position, velocity);
let z_hat = DVec3::new(
lvlh.t_parent_this.col(0).z,
lvlh.t_parent_this.col(1).z,
lvlh.t_parent_this.col(2).z,
);
assert!((z_hat.x + 1.0).abs() < 1e-14);
assert!(z_hat.y.abs() < 1e-14);
assert!(z_hat.z.abs() < 1e-14);
let n = (EARTH_MU / (r * r * r)).sqrt();
assert!((lvlh.ang_vel_this.length() - n).abs() / n < 1e-10);
assert_eq!(lvlh.position, position);
assert_eq!(lvlh.velocity, velocity);
}
#[test]
fn half_orbit_frame_rotates() {
let r = 6_778_137.0;
let v = (EARTH_MU / r).sqrt();
let pos2 = DVec3::new(0.0, r, 0.0);
let vel2 = DVec3::new(-v, 0.0, 0.0);
let lvlh2 = compute_lvlh_frame(pos2, vel2);
let z_hat = DVec3::new(
lvlh2.t_parent_this.col(0).z,
lvlh2.t_parent_this.col(1).z,
lvlh2.t_parent_this.col(2).z,
);
assert!(z_hat.x.abs() < 1e-14);
assert!((z_hat.y + 1.0).abs() < 1e-14);
assert!(z_hat.z.abs() < 1e-14);
}
use astrodyn_quantities::frame::TestVehicle as IssChief;
#[test]
fn typed_lvlh_matches_f64_port() {
let r = 6_778_137.0; let v = (EARTH_MU / r).sqrt();
let inc = 51.6_f64.to_radians();
let position_raw = DVec3::new(r, 0.0, 0.0);
let velocity_raw = DVec3::new(0.0, v * inc.cos(), v * inc.sin());
let lvlh_f64 = compute_lvlh_frame(position_raw, velocity_raw);
let q_f64 = JeodQuat::left_quat_from_transformation(&lvlh_f64.t_parent_this);
let position = Position::<PlanetInertial<astrodyn_quantities::frame::Earth>>::from_raw_si(
position_raw,
);
let velocity = Velocity::<PlanetInertial<astrodyn_quantities::frame::Earth>>::from_raw_si(
velocity_raw,
);
let xform = compute_lvlh_frame_typed::<astrodyn_quantities::frame::Earth, IssChief>(
position, velocity,
);
let q_typed = xform.quat().inner();
assert_eq!(
q_typed.data, q_f64.data,
"typed and f64 quaternion components must be bit-identical"
);
let m = xform.matrix();
let should_be_identity = m * m.transpose();
let diff = should_be_identity - DMat3::IDENTITY;
assert!(diff.x_axis.length() < 1e-14);
assert!(diff.y_axis.length() < 1e-14);
assert!(diff.z_axis.length() < 1e-14);
assert!((m.determinant() - 1.0).abs() < 1e-14);
}
}