use arika::epoch::Epoch;
use nalgebra::Vector3;
use crate::model::ExternalLoads;
use crate::model::{HasOrbit, Model};
#[derive(Debug, Clone, Copy)]
pub struct ConstantThrust {
pub name: &'static str,
pub start: Epoch,
pub end: Epoch,
acceleration: Vector3<f64>,
}
impl ConstantThrust {
pub fn new(name: &'static str, start: Epoch, end: Epoch, total_dv_kms: Vector3<f64>) -> Self {
let duration_s = (end.jd() - start.jd()) * 86_400.0;
assert!(
duration_s > 0.0,
"ConstantThrust {name:?}: end epoch must strictly follow start"
);
Self {
name,
start,
end,
acceleration: total_dv_kms / duration_s,
}
}
pub fn acceleration_kms2(&self) -> Vector3<f64> {
self.acceleration
}
pub fn duration_seconds(&self) -> f64 {
(self.end.jd() - self.start.jd()) * 86_400.0
}
pub fn total_dv_kms(&self) -> Vector3<f64> {
self.acceleration * self.duration_seconds()
}
fn is_active(&self, epoch: &Epoch) -> bool {
epoch.jd() >= self.start.jd() && epoch.jd() <= self.end.jd()
}
}
impl<S: HasOrbit> Model<S> for ConstantThrust {
fn name(&self) -> &str {
self.name
}
fn eval(&self, _t: f64, _state: &S, epoch: Option<&Epoch>) -> ExternalLoads {
let Some(epoch) = epoch else {
return ExternalLoads::acceleration(Vector3::zeros());
};
if self.is_active(epoch) {
ExternalLoads::acceleration(self.acceleration)
} else {
ExternalLoads::acceleration(Vector3::zeros())
}
}
}
const _: fn() = || {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ConstantThrust>();
};
#[cfg(test)]
mod tests {
use super::*;
use crate::OrbitalState;
use nalgebra::vector;
fn test_state() -> OrbitalState {
OrbitalState::new(vector![7000.0, 0.0, 0.0], vector![0.0, 7.5, 0.0])
}
fn epoch_seconds_from_j2000(seconds: f64) -> Epoch {
Epoch::from_jd(2_451_545.0 + seconds / 86_400.0)
}
#[test]
fn new_computes_correct_acceleration() {
let start = epoch_seconds_from_j2000(0.0);
let end = epoch_seconds_from_j2000(100.0);
let thrust = ConstantThrust::new("test", start, end, vector![0.05, 0.0, 0.0]);
let a = thrust.acceleration_kms2();
assert!((a.x - 5e-4).abs() < 1e-8);
assert_eq!(a.y, 0.0);
assert_eq!(a.z, 0.0);
}
#[test]
fn total_dv_round_trip_matches_constructor_input() {
let start = epoch_seconds_from_j2000(0.0);
let end = epoch_seconds_from_j2000(120.0);
let total = vector![0.1, -0.02, 0.05];
let thrust = ConstantThrust::new("rt", start, end, total);
let recovered = thrust.total_dv_kms();
assert!((recovered - total).magnitude() < 1e-12);
}
#[test]
#[should_panic(expected = "end epoch must strictly follow start")]
fn new_panics_on_reversed_interval() {
let start = epoch_seconds_from_j2000(100.0);
let end = epoch_seconds_from_j2000(0.0);
let _ = ConstantThrust::new("bad", start, end, vector![0.1, 0.0, 0.0]);
}
#[test]
fn eval_is_zero_before_start() {
let start = epoch_seconds_from_j2000(100.0);
let end = epoch_seconds_from_j2000(200.0);
let thrust = ConstantThrust::new("bf", start, end, vector![0.05, 0.0, 0.0]);
let probe = epoch_seconds_from_j2000(50.0);
let loads = thrust.eval(0.0, &test_state(), Some(&probe));
assert_eq!(loads.acceleration_inertial.into_inner(), Vector3::zeros());
}
#[test]
fn eval_is_zero_after_end() {
let start = epoch_seconds_from_j2000(100.0);
let end = epoch_seconds_from_j2000(200.0);
let thrust = ConstantThrust::new("af", start, end, vector![0.05, 0.0, 0.0]);
let probe = epoch_seconds_from_j2000(300.0);
let loads = thrust.eval(0.0, &test_state(), Some(&probe));
assert_eq!(loads.acceleration_inertial.into_inner(), Vector3::zeros());
}
#[test]
fn eval_returns_constant_acceleration_inside_window() {
let start = epoch_seconds_from_j2000(100.0);
let end = epoch_seconds_from_j2000(200.0);
let thrust = ConstantThrust::new("mid", start, end, vector![0.01, 0.0, 0.0]);
let expected = vector![1e-4, 0.0, 0.0];
for probe_sec in [100.0, 120.0, 150.0, 180.0, 200.0] {
let probe = epoch_seconds_from_j2000(probe_sec);
let loads = thrust.eval(0.0, &test_state(), Some(&probe));
let a = loads.acceleration_inertial.into_inner();
assert!(
(a - expected).magnitude() < 1e-8,
"at probe={probe_sec}s: expected {expected:?} got {a:?}"
);
}
}
#[test]
fn eval_with_no_epoch_returns_zero() {
let thrust = ConstantThrust::new(
"noep",
epoch_seconds_from_j2000(0.0),
epoch_seconds_from_j2000(100.0),
vector![0.05, 0.0, 0.0],
);
let loads = thrust.eval(0.0, &test_state(), None);
assert_eq!(loads.acceleration_inertial.into_inner(), Vector3::zeros());
}
#[test]
fn eval_at_exact_boundaries_is_active() {
let start = epoch_seconds_from_j2000(100.0);
let end = epoch_seconds_from_j2000(200.0);
let thrust = ConstantThrust::new("bd", start, end, vector![0.01, 0.0, 0.0]);
let expected = vector![1e-4, 0.0, 0.0];
let loads_start = thrust.eval(0.0, &test_state(), Some(&start));
let loads_end = thrust.eval(0.0, &test_state(), Some(&end));
assert!((loads_start.acceleration_inertial.into_inner() - expected).magnitude() < 1e-8);
assert!((loads_end.acceleration_inertial.into_inner() - expected).magnitude() < 1e-8);
}
#[test]
fn constant_thrust_is_clone_and_copy() {
let t1 = ConstantThrust::new(
"cc",
epoch_seconds_from_j2000(0.0),
epoch_seconds_from_j2000(50.0),
vector![0.02, 0.0, 0.0],
);
let t2 = t1;
let t3 = t1.clone();
assert_eq!(t1.acceleration_kms2(), t2.acceleration_kms2());
assert_eq!(t1.acceleration_kms2(), t3.acceleration_kms2());
}
}