use crate::OrbitalState;
use arika::epoch::Epoch;
use nalgebra::{Matrix3, UnitQuaternion, Vector3};
pub trait AttitudeReference: Send + Sync {
fn target(
&self,
t: f64,
orbit: &OrbitalState,
epoch: Option<&Epoch>,
) -> (UnitQuaternion<f64>, Vector3<f64>);
}
pub struct InertialPointing {
pub target_q: UnitQuaternion<f64>,
}
impl AttitudeReference for InertialPointing {
fn target(
&self,
_t: f64,
_orbit: &OrbitalState,
_epoch: Option<&Epoch>,
) -> (UnitQuaternion<f64>, Vector3<f64>) {
(self.target_q, Vector3::zeros())
}
}
pub struct NadirPointing;
impl AttitudeReference for NadirPointing {
fn target(
&self,
_t: f64,
orbit: &OrbitalState,
_epoch: Option<&Epoch>,
) -> (UnitQuaternion<f64>, Vector3<f64>) {
let r = *orbit.position();
let v = *orbit.velocity();
let r_mag = r.magnitude();
let h = r.cross(&v);
let h_mag = h.magnitude();
let z_lvlh = -r / r_mag; let y_lvlh = -h / h_mag; let x_lvlh = y_lvlh.cross(&z_lvlh);
let r_lvlh_to_inertial = Matrix3::from_columns(&[x_lvlh, y_lvlh, z_lvlh]);
let q_target = UnitQuaternion::from_rotation_matrix(
&nalgebra::Rotation3::from_matrix_unchecked(r_lvlh_to_inertial),
);
let n = h_mag / (r_mag * r_mag);
let omega_target = Vector3::new(0.0, -n, 0.0);
(q_target, omega_target)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn inertial_pointing_returns_fixed_target() {
let axis = nalgebra::Unit::new_normalize(Vector3::new(0.0, 0.0, 1.0));
let q = UnitQuaternion::from_axis_angle(&axis, PI / 4.0);
let ref_point = InertialPointing { target_q: q };
let orbit = OrbitalState::new(Vector3::new(7000.0, 0.0, 0.0), Vector3::new(0.0, 7.5, 0.0));
let (q_out, omega_out) = ref_point.target(0.0, &orbit, None);
assert!((q_out.angle() - PI / 4.0).abs() < 1e-14);
assert!(omega_out.magnitude() < 1e-15);
}
#[test]
fn nadir_z_axis_points_toward_earth() {
let orbit = OrbitalState::new(Vector3::new(7000.0, 0.0, 0.0), Vector3::new(0.0, 7.5, 0.0));
let nadir = NadirPointing;
let (q_target, _omega) = nadir.target(0.0, &orbit, None);
let r_mat = q_target.to_rotation_matrix();
let z_body_inertial = r_mat * Vector3::new(0.0, 0.0, 1.0);
let r_hat = orbit.position().normalize();
let expected = -r_hat;
let error = (z_body_inertial - expected).magnitude();
assert!(
error < 1e-14,
"Body Z should point nadir, error: {error:.2e}"
);
}
#[test]
fn nadir_omega_target_circular_orbit() {
let mu: f64 = 398600.4418;
let r = 7000.0;
let v_circ = (mu / r).sqrt();
let orbit = OrbitalState::new(Vector3::new(r, 0.0, 0.0), Vector3::new(0.0, v_circ, 0.0));
let nadir = NadirPointing;
let (_q_target, omega_target) = nadir.target(0.0, &orbit, None);
let n_expected = v_circ / r;
assert!(
omega_target[0].abs() < 1e-15,
"omega_x should be 0, got {}",
omega_target[0]
);
assert!(
(omega_target[1] + n_expected).abs() < 1e-12,
"omega_y should be -{n_expected}, got {}",
omega_target[1]
);
assert!(
omega_target[2].abs() < 1e-15,
"omega_z should be 0, got {}",
omega_target[2]
);
}
#[test]
fn nadir_orthonormal_frame() {
let orbit = OrbitalState::new(
Vector3::new(5000.0, 3000.0, 1000.0),
Vector3::new(-1.0, 6.0, 2.0),
);
let nadir = NadirPointing;
let (q_target, _) = nadir.target(0.0, &orbit, None);
let r_mat = q_target.to_rotation_matrix();
let m = r_mat.matrix();
let identity = m.transpose() * m;
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(identity[(i, j)] - expected).abs() < 1e-14,
"R^T R[{i},{j}] = {}, expected {expected}",
identity[(i, j)]
);
}
}
}
}