use arika::epoch::Epoch;
use nalgebra::Vector3;
use super::ExternalLoads;
use crate::effector::StateEffector;
use crate::model::HasAttitude;
#[derive(Debug, Clone)]
pub struct ReactionWheel {
axis: Vector3<f64>,
pub inertia: f64,
pub max_momentum: f64,
pub max_torque: f64,
}
impl ReactionWheel {
pub fn new(axis: Vector3<f64>, inertia: f64, max_momentum: f64, max_torque: f64) -> Self {
let norm = axis.magnitude();
assert!(norm > 1e-15, "Wheel axis must be non-zero");
assert!(
max_momentum >= 0.0,
"max_momentum must be non-negative, got {max_momentum}"
);
assert!(
max_torque >= 0.0,
"max_torque must be non-negative, got {max_torque}"
);
Self {
axis: axis / norm,
inertia,
max_momentum,
max_torque,
}
}
pub fn axis(&self) -> &Vector3<f64> {
&self.axis
}
}
#[derive(Clone)]
pub struct ReactionWheelAssembly {
wheels: Vec<ReactionWheel>,
pub commanded_torque: Vector3<f64>,
}
impl ReactionWheelAssembly {
pub fn new(wheels: Vec<ReactionWheel>) -> Self {
Self {
wheels,
commanded_torque: Vector3::zeros(),
}
}
pub fn three_axis(inertia: f64, max_momentum: f64, max_torque: f64) -> Self {
Self::new(vec![
ReactionWheel::new(Vector3::x(), inertia, max_momentum, max_torque),
ReactionWheel::new(Vector3::y(), inertia, max_momentum, max_torque),
ReactionWheel::new(Vector3::z(), inertia, max_momentum, max_torque),
])
}
pub fn wheels(&self) -> &[ReactionWheel] {
&self.wheels
}
}
impl<S: HasAttitude + Send + Sync> StateEffector<S> for ReactionWheelAssembly {
fn name(&self) -> &str {
"reaction_wheels"
}
fn state_dim(&self) -> usize {
self.wheels.len()
}
fn aux_bounds(&self) -> Vec<(f64, f64)> {
self.wheels
.iter()
.map(|w| (-w.max_momentum, w.max_momentum))
.collect()
}
fn derivatives(
&self,
_t: f64,
state: &S,
aux: &[f64],
aux_rates: &mut [f64],
_epoch: Option<&Epoch>,
) -> ExternalLoads {
let omega = &state.attitude().angular_velocity;
let mut total_reaction_torque = Vector3::zeros();
let mut h_rw = Vector3::zeros();
for (i, wheel) in self.wheels.iter().enumerate() {
h_rw += aux[i] * wheel.axis;
}
for (i, wheel) in self.wheels.iter().enumerate() {
let h_i = aux[i];
let mut tau_cmd = -self.commanded_torque.dot(&wheel.axis);
tau_cmd = tau_cmd.clamp(-wheel.max_torque, wheel.max_torque);
if (h_i >= wheel.max_momentum && tau_cmd > 0.0)
|| (h_i <= -wheel.max_momentum && tau_cmd < 0.0)
{
tau_cmd = 0.0;
}
aux_rates[i] = tau_cmd;
total_reaction_torque -= tau_cmd * wheel.axis;
}
total_reaction_torque -= omega.cross(&h_rw);
ExternalLoads::torque(total_reaction_torque)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::attitude::AttitudeState;
use nalgebra::Vector4;
fn test_state_at_rest() -> AttitudeState {
AttitudeState::identity()
}
#[test]
fn three_axis_has_three_wheels() {
let rw = ReactionWheelAssembly::three_axis(0.01, 1.0, 0.1);
assert_eq!(rw.wheels().len(), 3);
assert_eq!(StateEffector::<AttitudeState>::state_dim(&rw), 3);
}
#[test]
fn name_is_reaction_wheels() {
let rw = ReactionWheelAssembly::three_axis(0.01, 1.0, 0.1);
assert_eq!(StateEffector::<AttitudeState>::name(&rw), "reaction_wheels");
}
#[test]
fn zero_command_zero_output() {
let rw = ReactionWheelAssembly::three_axis(0.01, 1.0, 0.1);
let state = test_state_at_rest();
let aux = vec![0.0, 0.0, 0.0];
let mut rates = vec![0.0, 0.0, 0.0];
let loads = rw.derivatives(0.0, &state, &aux, &mut rates, None);
assert_eq!(rates, vec![0.0, 0.0, 0.0]);
assert!(loads.torque_body.magnitude() < 1e-15);
}
#[test]
fn commanded_torque_z_produces_rates() {
let mut rw = ReactionWheelAssembly::three_axis(0.01, 1.0, 0.1);
rw.commanded_torque = Vector3::new(0.0, 0.0, 0.05);
let state = test_state_at_rest();
let aux = vec![0.0, 0.0, 0.0];
let mut rates = vec![0.0, 0.0, 0.0];
let loads = rw.derivatives(0.0, &state, &aux, &mut rates, None);
assert!((rates[0]).abs() < 1e-15);
assert!((rates[1]).abs() < 1e-15);
assert!((rates[2] - (-0.05)).abs() < 1e-15);
assert!((loads.torque_body.z() - 0.05).abs() < 1e-15);
}
#[test]
fn torque_rate_limiting() {
let mut rw = ReactionWheelAssembly::three_axis(0.01, 1.0, 0.1);
rw.commanded_torque = Vector3::new(10.0, 0.0, 0.0);
let state = test_state_at_rest();
let aux = vec![0.0, 0.0, 0.0];
let mut rates = vec![0.0, 0.0, 0.0];
rw.derivatives(0.0, &state, &aux, &mut rates, None);
assert!((rates[0] - (-0.1)).abs() < 1e-15);
}
#[test]
fn momentum_saturation_positive() {
let mut rw = ReactionWheelAssembly::three_axis(0.01, 1.0, 0.1);
rw.commanded_torque = Vector3::new(-0.05, 0.0, 0.0);
let state = test_state_at_rest();
let aux = vec![1.0, 0.0, 0.0];
let mut rates = vec![0.0, 0.0, 0.0];
rw.derivatives(0.0, &state, &aux, &mut rates, None);
assert!((rates[0]).abs() < 1e-15);
}
#[test]
fn momentum_saturation_negative() {
let mut rw = ReactionWheelAssembly::three_axis(0.01, 1.0, 0.1);
rw.commanded_torque = Vector3::new(0.05, 0.0, 0.0);
let state = test_state_at_rest();
let aux = vec![-1.0, 0.0, 0.0];
let mut rates = vec![0.0, 0.0, 0.0];
rw.derivatives(0.0, &state, &aux, &mut rates, None);
assert!((rates[0]).abs() < 1e-15);
}
#[test]
fn momentum_saturation_allows_opposite_direction() {
let mut rw = ReactionWheelAssembly::three_axis(0.01, 1.0, 0.1);
rw.commanded_torque = Vector3::new(0.05, 0.0, 0.0);
let state = test_state_at_rest();
let aux = vec![1.0, 0.0, 0.0];
let mut rates = vec![0.0, 0.0, 0.0];
rw.derivatives(0.0, &state, &aux, &mut rates, None);
assert!((rates[0] - (-0.05)).abs() < 1e-15);
}
#[test]
fn reaction_torque_opposes_wheel_acceleration_at_rest() {
let mut rw = ReactionWheelAssembly::three_axis(0.01, 1.0, 0.1);
rw.commanded_torque = Vector3::new(0.05, 0.03, 0.02);
let state = test_state_at_rest();
let aux = vec![0.0, 0.0, 0.0];
let mut rates = vec![0.0, 0.0, 0.0];
let loads = rw.derivatives(0.0, &state, &aux, &mut rates, None);
let tb = loads.torque_body.into_inner();
for i in 0..3 {
assert!(
(tb[i] + rates[i]).abs() < 1e-15,
"Reaction torque[{i}] should equal -rates[{i}]"
);
}
}
#[test]
fn gyroscopic_coupling_with_spinning_body() {
let rw = ReactionWheelAssembly::three_axis(0.01, 10.0, 0.5);
let state = AttitudeState {
quaternion: Vector4::new(1.0, 0.0, 0.0, 0.0),
angular_velocity: Vector3::new(0.0, 0.0, 1.0), };
let aux = vec![5.0, 0.0, 0.0];
let mut rates = vec![0.0, 0.0, 0.0];
let loads = rw.derivatives(0.0, &state, &aux, &mut rates, None);
assert!(
(loads.torque_body.x()).abs() < 1e-15,
"tau_x should be 0, got {}",
loads.torque_body.x()
);
assert!(
(loads.torque_body.y() - (-5.0)).abs() < 1e-15,
"tau_y should be -5, got {}",
loads.torque_body.y()
);
assert!(
(loads.torque_body.z()).abs() < 1e-15,
"tau_z should be 0, got {}",
loads.torque_body.z()
);
}
}