use astrodyn_quantities::aliases::{InertiaTensor, Torque};
use astrodyn_quantities::frame::{BodyFrame, Vehicle};
use glam::{DMat3, DVec3};
pub fn compute_gravity_torque(grav_grad: &DMat3, t_parent_this: &DMat3, inertia: &DMat3) -> DVec3 {
let g = *t_parent_this * *grav_grad * t_parent_this.transpose();
let g00 = g.col(0)[0];
let g01 = g.col(1)[0];
let g02 = g.col(2)[0];
let g11 = g.col(1)[1];
let g12 = g.col(2)[1];
let g22 = g.col(2)[2];
let i00 = inertia.col(0)[0];
let i01 = inertia.col(1)[0];
let i02 = inertia.col(2)[0];
let i11 = inertia.col(1)[1];
let i12 = inertia.col(2)[1];
let i22 = inertia.col(2)[2];
let tx = g12 * (i22 - i11) - g02 * i01 + g01 * i02 - i12 * (g22 - g11);
let ty = g02 * (i00 - i22) + g12 * i01 - g01 * i12 - i02 * (g00 - g22);
let tz = g01 * (i11 - i00) - g12 * i02 + g02 * i12 - i01 * (g11 - g00);
DVec3::new(tx, ty, tz)
}
pub fn compute_gravity_torque_typed<V: Vehicle>(
grav_grad: &DMat3,
t_parent_this: &DMat3,
inertia: InertiaTensor<BodyFrame<V>>,
) -> Torque<BodyFrame<V>> {
let inertia_dmat = inertia.as_dmat3();
let untyped_torque = compute_gravity_torque(grav_grad, t_parent_this, &inertia_dmat);
Torque::<BodyFrame<V>>::from_raw_si(untyped_torque)
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn spherical_body_zero_torque() {
let inertia = DMat3::from_diagonal(DVec3::new(100.0, 100.0, 100.0));
let mu = 3.986_004_415e14;
let r = 7_000_000.0;
let r3 = r * r * r;
let grad = DMat3::from_diagonal(DVec3::new(-2.0, 1.0, 1.0)) * (mu / r3);
let angle = PI / 4.0;
let c = angle.cos();
let s = angle.sin();
let t = DMat3::from_cols(
DVec3::new(c, s, 0.0),
DVec3::new(-s, c, 0.0),
DVec3::new(0.0, 0.0, 1.0),
);
let torque = compute_gravity_torque(&grad, &t, &inertia);
assert!(
torque.length() < 1e-20,
"Torque on spherical body should be zero, got {:?} (magnitude {})",
torque,
torque.length()
);
}
#[test]
fn analytical_gravity_gradient_torque() {
let mu = 3.986_004_415e14;
let r = 7_000_000.0;
let r3 = r * r * r;
let ia = 10.0;
let ic = 30.0;
let inertia = DMat3::from_diagonal(DVec3::new(ia, 20.0, ic));
let grad = DMat3::from_diagonal(DVec3::new(1.0, 1.0, -2.0)) * (mu / r3);
let theta = 30.0_f64.to_radians();
let c = theta.cos();
let s = theta.sin();
let t = DMat3::from_cols(
DVec3::new(c, 0.0, -s),
DVec3::new(0.0, 1.0, 0.0),
DVec3::new(s, 0.0, c),
);
let torque = compute_gravity_torque(&grad, &t, &inertia);
let expected_ty = 3.0 * mu / (2.0 * r3) * (2.0 * theta).sin() * (ic - ia);
assert!(
torque.x.abs() < 1e-15,
"X torque should be ~0, got {}",
torque.x
);
assert!(
torque.z.abs() < 1e-15,
"Z torque should be ~0, got {}",
torque.z
);
let rel_err = (torque.y - expected_ty).abs() / expected_ty.abs();
assert!(
rel_err < 1e-10,
"Y torque: expected {expected_ty}, got {}, relative error {rel_err}",
torque.y
);
}
#[test]
fn non_diagonal_inertia() {
let mu = 3.986_004_415e14;
let r = 7_000_000.0;
let r3 = r * r * r;
let inertia = DMat3::from_cols(
DVec3::new(100.0, -5.0, 3.0),
DVec3::new(-5.0, 200.0, -7.0),
DVec3::new(3.0, -7.0, 150.0),
);
let grad = DMat3::from_diagonal(DVec3::new(-2.0, 1.0, 1.0)) * (mu / r3);
let t = DMat3::IDENTITY;
let torque = compute_gravity_torque(&grad, &t, &inertia);
let scale = mu / r3;
let expected_tx = -(-7.0) * (scale - scale); let expected_ty = -3.0 * (-2.0 * scale - scale); let expected_tz = -(-5.0) * (scale - (-2.0 * scale));
assert!(
(torque.x - expected_tx).abs() < 1e-10,
"X torque: expected {expected_tx}, got {}",
torque.x
);
assert!(
(torque.y - expected_ty).abs() / expected_ty.abs() < 1e-10,
"Y torque: expected {expected_ty}, got {}",
torque.y
);
assert!(
(torque.z - expected_tz).abs() / expected_tz.abs() < 1e-10,
"Z torque: expected {expected_tz}, got {}",
torque.z
);
}
#[test]
fn zero_gradient_zero_torque() {
let inertia = DMat3::from_diagonal(DVec3::new(10.0, 20.0, 30.0));
let grad = DMat3::ZERO;
let t = DMat3::IDENTITY;
let torque = compute_gravity_torque(&grad, &t, &inertia);
assert_eq!(torque, DVec3::ZERO);
}
#[test]
fn torque_scales_with_inertia_difference() {
let mu = 3.986_004_415e14;
let r = 7_000_000.0;
let r3 = r * r * r;
let grad = DMat3::from_diagonal(DVec3::new(1.0, 1.0, -2.0)) * (mu / r3);
let theta: f64 = 0.5; let c = theta.cos();
let s = theta.sin();
let t = DMat3::from_cols(
DVec3::new(c, 0.0, -s),
DVec3::new(0.0, 1.0, 0.0),
DVec3::new(s, 0.0, c),
);
let inertia_small = DMat3::from_diagonal(DVec3::new(100.0, 100.0, 101.0));
let torque_small = compute_gravity_torque(&grad, &t, &inertia_small);
let inertia_large = DMat3::from_diagonal(DVec3::new(100.0, 100.0, 200.0));
let torque_large = compute_gravity_torque(&grad, &t, &inertia_large);
assert!(
torque_large.length() > torque_small.length(),
"Torque should increase with inertia asymmetry"
);
}
#[test]
fn compute_gravity_torque_typed_matches_untyped() {
use astrodyn_quantities::frame::TestVehicle;
let mu = 3.986_004_415e14;
let r = 7_000_000.0;
let r3 = r * r * r;
let grad = DMat3::from_diagonal(DVec3::new(1.0, 1.0, -2.0)) * (mu / r3);
let theta: f64 = 0.4;
let c = theta.cos();
let s = theta.sin();
let t = DMat3::from_cols(
DVec3::new(c, 0.0, -s),
DVec3::new(0.0, 1.0, 0.0),
DVec3::new(s, 0.0, c),
);
let inertia_dmat = DMat3::from_diagonal(DVec3::new(800.0, 1200.0, 600.0));
let inertia_typed =
InertiaTensor::<BodyFrame<TestVehicle>>::from_dmat3_unchecked(inertia_dmat);
let untyped_torque = compute_gravity_torque(&grad, &t, &inertia_dmat);
let typed_torque = compute_gravity_torque_typed::<TestVehicle>(&grad, &t, inertia_typed);
assert_eq!(typed_torque.raw_si(), untyped_torque);
}
}