use arika::epoch::Epoch;
use nalgebra::{Matrix3, Vector3};
use utsuroi::DynamicalSystem;
use crate::OrbitalState;
use crate::attitude::AttitudeState;
use crate::model::ExternalLoads;
use crate::model::{HasAttitude, HasMass, HasOrbit, Model};
pub struct DecoupledContext {
pub attitude: AttitudeState,
pub orbit: OrbitalState,
pub mass: f64,
}
impl HasAttitude for DecoupledContext {
fn attitude(&self) -> &AttitudeState {
&self.attitude
}
}
impl HasOrbit for DecoupledContext {
type Frame = arika::frame::SimpleEci;
fn orbit(&self) -> &OrbitalState<arika::frame::SimpleEci> {
&self.orbit
}
}
impl HasMass for DecoupledContext {
fn mass(&self) -> f64 {
self.mass
}
}
pub struct DecoupledAttitudeSystem {
inertia: Matrix3<f64>,
inertia_inv: Matrix3<f64>,
models: Vec<Box<dyn Model<DecoupledContext>>>,
orbit_fn: Box<dyn Fn(f64) -> OrbitalState + Send + Sync>,
mass_fn: Box<dyn Fn(f64) -> f64 + Send + Sync>,
epoch_0: Option<Epoch>,
}
impl DecoupledAttitudeSystem {
pub fn new(
inertia: Matrix3<f64>,
orbit_fn: impl Fn(f64) -> OrbitalState + Send + Sync + 'static,
mass_fn: impl Fn(f64) -> f64 + Send + Sync + 'static,
) -> Self {
let inertia_inv = inertia
.try_inverse()
.expect("Inertia tensor must be invertible");
Self {
inertia,
inertia_inv,
models: Vec::new(),
orbit_fn: Box::new(orbit_fn),
mass_fn: Box::new(mass_fn),
epoch_0: None,
}
}
pub fn circular_orbit(inertia: Matrix3<f64>, mu: f64, radius: f64, mass: f64) -> Self {
let n = (mu / radius.powi(3)).sqrt(); let v = (mu / radius).sqrt(); Self::new(
inertia,
move |t| {
let theta = n * t;
OrbitalState::new(
Vector3::new(radius * theta.cos(), radius * theta.sin(), 0.0),
Vector3::new(-v * theta.sin(), v * theta.cos(), 0.0),
)
},
move |_| mass,
)
}
pub fn with_model(mut self, model: impl Model<DecoupledContext> + 'static) -> Self {
self.models.push(Box::new(model));
self
}
pub fn with_epoch(mut self, epoch: Epoch) -> Self {
self.epoch_0 = Some(epoch);
self
}
pub fn inertia(&self) -> &Matrix3<f64> {
&self.inertia
}
pub fn model_names(&self) -> Vec<&str> {
self.models.iter().map(|m| m.name()).collect()
}
}
impl DynamicalSystem for DecoupledAttitudeSystem {
type State = AttitudeState;
fn derivatives(&self, t: f64, state: &AttitudeState) -> AttitudeState {
let epoch = self.epoch_0.map(|e| e.add_seconds(t));
let context = DecoupledContext {
attitude: state.clone(),
orbit: (self.orbit_fn)(t),
mass: (self.mass_fn)(t),
};
let q_dot = state.q_dot();
let mut total = ExternalLoads::zeros();
for m in &self.models {
total += m.eval(t, &context, epoch.as_ref());
}
if total.acceleration_inertial.magnitude() > 1e-15 {
log::warn!(
"DecoupledAttitudeSystem ignoring non-zero acceleration_inertial: {:?}",
total.acceleration_inertial
);
}
if total.mass_rate.abs() > 1e-15 {
log::warn!(
"DecoupledAttitudeSystem ignoring non-zero mass_rate: {}",
total.mass_rate
);
}
let iw = self.inertia * state.angular_velocity;
let alpha =
self.inertia_inv * (total.torque_body.into_inner() - state.angular_velocity.cross(&iw));
AttitudeState::from_derivative(q_dot, alpha)
}
}
#[cfg(test)]
mod tests {
use super::*;
use nalgebra::Vector4;
fn symmetric_inertia(i: f64) -> Matrix3<f64> {
Matrix3::from_diagonal(&Vector3::new(i, i, i))
}
#[test]
fn decoupled_context_has_attitude() {
let ctx = DecoupledContext {
attitude: AttitudeState::identity(),
orbit: OrbitalState::new(Vector3::new(7000.0, 0.0, 0.0), Vector3::new(0.0, 7.5, 0.0)),
mass: 100.0,
};
assert_eq!(ctx.attitude().angular_velocity, Vector3::zeros());
}
#[test]
fn decoupled_context_has_orbit() {
let ctx = DecoupledContext {
attitude: AttitudeState::identity(),
orbit: OrbitalState::new(Vector3::new(7000.0, 0.0, 0.0), Vector3::new(0.0, 7.5, 0.0)),
mass: 100.0,
};
assert_eq!(*ctx.orbit().position(), Vector3::new(7000.0, 0.0, 0.0));
}
#[test]
fn decoupled_context_has_mass() {
let ctx = DecoupledContext {
attitude: AttitudeState::identity(),
orbit: OrbitalState::new(Vector3::new(7000.0, 0.0, 0.0), Vector3::new(0.0, 7.5, 0.0)),
mass: 42.0,
};
assert!((ctx.mass() - 42.0).abs() < 1e-15);
}
#[test]
fn torque_free_symmetric_body_zero_acceleration() {
let system = DecoupledAttitudeSystem::circular_orbit(
symmetric_inertia(10.0),
398600.4418,
7000.0,
100.0,
);
let state = AttitudeState {
quaternion: Vector4::new(1.0, 0.0, 0.0, 0.0),
angular_velocity: Vector3::new(0.1, 0.2, 0.3),
};
let deriv = system.derivatives(0.0, &state);
assert!(deriv.angular_velocity.magnitude() < 1e-15);
}
#[test]
fn circular_orbit_position_at_t0() {
let mu = 398600.4418;
let r = 7000.0;
let system = DecoupledAttitudeSystem::circular_orbit(symmetric_inertia(10.0), mu, r, 100.0);
let orbit = (system.orbit_fn)(0.0);
assert!((orbit.position()[0] - r).abs() < 1e-10);
assert!(orbit.position()[1].abs() < 1e-10);
assert!(orbit.position()[2].abs() < 1e-10);
}
#[test]
fn model_names_empty() {
let system = DecoupledAttitudeSystem::circular_orbit(symmetric_inertia(1.0), 1.0, 1.0, 1.0);
assert!(system.model_names().is_empty());
}
#[test]
fn builder_with_epoch() {
let epoch = Epoch::from_jd(2451545.0);
let system = DecoupledAttitudeSystem::circular_orbit(symmetric_inertia(1.0), 1.0, 1.0, 1.0)
.with_epoch(epoch);
assert!(system.epoch_0.is_some());
}
}