use std::collections::BTreeMap;
use crate::astro::dynamics::density::DensityProvider;
use crate::astro::dynamics::forces::{
CannonballSrp, CentralBodyRelativity1Pn, Conical, Cylindrical, DragForce,
EmpiricalAcceleration, Geopotential, NoEclipse, ShadowModel, ThirdBody, TwoBody, J2,
};
use crate::astro::dynamics::{EARTH_J2, GM_EARTH, R_EARTH};
use crate::time::JulianDate;
use qtty::{AreaToMass, DragCoefficient, KmPerSecondsSquared, Second, SrpCoefficient};
use super::empirical_periodic::{EmpiricalPeriodicAcceleration, PeriodicHarmonic};
use crate::pod::force::{DynSiderustForceModel, SiderustCompositeModel};
use crate::pod::propagation::pod_error::PodDynamicsError;
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum ForceModelParams {
None,
Geopotential {
degree: usize,
order: usize,
},
Drag {
cd: DragCoefficient,
area_to_mass: AreaToMass,
},
SrpCannonball {
cr: SrpCoefficient,
area_to_mass: AreaToMass,
shadow: ShadowModel,
},
EmpiricalConstant {
radial: KmPerSecondsSquared,
transverse: KmPerSecondsSquared,
normal: KmPerSecondsSquared,
},
EmpiricalPeriodic {
harmonic: PeriodicHarmonic,
epoch_ref: JulianDate,
period: Second,
coeffs: [KmPerSecondsSquared; 6],
},
Custom(String),
}
#[derive(Debug, Clone)]
pub struct ForceModelSpec {
pub name: String,
pub params: ForceModelParams,
}
impl ForceModelSpec {
pub fn named(name: impl Into<String>) -> Self {
Self {
name: name.into(),
params: ForceModelParams::None,
}
}
pub fn with_params(name: impl Into<String>, params: ForceModelParams) -> Self {
Self {
name: name.into(),
params,
}
}
}
pub trait ForceModelFactory: Send + Sync {
fn name(&self) -> &'static str;
fn build(
&self,
params: &ForceModelParams,
) -> Result<Box<DynSiderustForceModel>, PodDynamicsError>;
}
pub struct ForceModelRegistry {
factories: BTreeMap<String, Box<dyn ForceModelFactory>>,
}
impl Default for ForceModelRegistry {
fn default() -> Self {
Self::with_builtins()
}
}
impl ForceModelRegistry {
pub fn new() -> Self {
Self {
factories: BTreeMap::new(),
}
}
pub fn with_builtins() -> Self {
let mut r = Self::new();
r.register(Box::new(TwoBodyFactory));
r.register(Box::new(J2Factory));
r.register(Box::new(GeopotentialFactory));
r.register(Box::new(ThirdBodySunFactory));
r.register(Box::new(ThirdBodyMoonFactory));
r.register(Box::new(ThirdBodySunMoonFactory));
r.register(Box::new(DragFactory));
r.register(Box::new(SrpCannonballFactory));
r.register(Box::new(RelativityFactory));
r.register(Box::new(EmpiricalConstantFactory));
r.register(Box::new(Empirical1CprFactory));
r.register(Box::new(Empirical2CprFactory));
r
}
pub fn register(&mut self, f: Box<dyn ForceModelFactory>) {
self.factories.insert(f.name().to_string(), f);
}
pub fn is_registered(&self, name: &str) -> bool {
self.factories.contains_key(name)
}
pub fn names(&self) -> Vec<&str> {
self.factories.keys().map(|s| s.as_str()).collect()
}
pub fn build_one(
&self,
spec: &ForceModelSpec,
) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
let f = self
.factories
.get(&spec.name)
.ok_or_else(|| PodDynamicsError::UnknownModel(spec.name.clone()))?;
f.build(&spec.params)
}
pub fn build(
&self,
specs: &[ForceModelSpec],
) -> Result<SiderustCompositeModel, PodDynamicsError> {
let mut out = SiderustCompositeModel::empty();
for s in specs {
out = out.push(self.build_one(s)?);
}
Ok(out)
}
}
struct TwoBodyFactory;
impl ForceModelFactory for TwoBodyFactory {
fn name(&self) -> &'static str {
"two_body"
}
fn build(&self, _p: &ForceModelParams) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
Ok(Box::new(TwoBody::new(GM_EARTH)))
}
}
struct J2Factory;
impl ForceModelFactory for J2Factory {
fn name(&self) -> &'static str {
"j2"
}
fn build(&self, _p: &ForceModelParams) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
Ok(Box::new(J2::new(GM_EARTH, R_EARTH, EARTH_J2)))
}
}
struct GeopotentialFactory;
impl ForceModelFactory for GeopotentialFactory {
fn name(&self) -> &'static str {
"geopotential"
}
fn build(&self, p: &ForceModelParams) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
match p {
ForceModelParams::Geopotential { degree, order } => {
Ok(Box::new(Geopotential::new(*degree, *order)))
}
_ => Err(PodDynamicsError::InvalidParameters {
name: "geopotential".into(),
reason: "expected ForceModelParams::Geopotential { degree, order }",
}),
}
}
}
struct ThirdBodySunFactory;
impl ForceModelFactory for ThirdBodySunFactory {
fn name(&self) -> &'static str {
"third_body_sun"
}
fn build(&self, _p: &ForceModelParams) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
Ok(Box::new(ThirdBody::new().with_sun()))
}
}
struct ThirdBodyMoonFactory;
impl ForceModelFactory for ThirdBodyMoonFactory {
fn name(&self) -> &'static str {
"third_body_moon"
}
fn build(&self, _p: &ForceModelParams) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
Ok(Box::new(ThirdBody::new().with_moon()))
}
}
struct ThirdBodySunMoonFactory;
impl ForceModelFactory for ThirdBodySunMoonFactory {
fn name(&self) -> &'static str {
"third_body_sun_moon"
}
fn build(&self, _p: &ForceModelParams) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
Ok(Box::new(ThirdBody::sun_and_moon()))
}
}
struct DragFactory;
impl ForceModelFactory for DragFactory {
fn name(&self) -> &'static str {
"drag"
}
fn build(&self, p: &ForceModelParams) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
match p {
ForceModelParams::Drag { cd, area_to_mass } => {
Ok(Box::new(DragForce::new(*cd, *area_to_mass)))
}
_ => Err(PodDynamicsError::InvalidParameters {
name: "drag".into(),
reason: "expected ForceModelParams::Drag { cd, area_to_mass }",
}),
}
}
}
struct SrpCannonballFactory;
impl ForceModelFactory for SrpCannonballFactory {
fn name(&self) -> &'static str {
"srp_cannonball"
}
fn build(&self, p: &ForceModelParams) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
match p {
ForceModelParams::SrpCannonball {
cr,
area_to_mass,
shadow,
} => {
let force: Box<DynSiderustForceModel> = match shadow {
ShadowModel::None => {
Box::new(CannonballSrp::<NoEclipse>::new(*cr, *area_to_mass))
}
ShadowModel::Cylindrical => {
Box::new(CannonballSrp::<Cylindrical>::new(*cr, *area_to_mass))
}
ShadowModel::Conical => {
Box::new(CannonballSrp::<Conical>::new(*cr, *area_to_mass))
}
};
Ok(force)
}
_ => Err(PodDynamicsError::InvalidParameters {
name: "srp_cannonball".into(),
reason: "expected ForceModelParams::SrpCannonball { cr, area_to_mass, shadow }",
}),
}
}
}
struct RelativityFactory;
impl ForceModelFactory for RelativityFactory {
fn name(&self) -> &'static str {
"relativity"
}
fn build(&self, _p: &ForceModelParams) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
Ok(Box::new(CentralBodyRelativity1Pn::earth()))
}
}
struct EmpiricalConstantFactory;
impl ForceModelFactory for EmpiricalConstantFactory {
fn name(&self) -> &'static str {
"empirical_constant"
}
fn build(&self, p: &ForceModelParams) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
match p {
ForceModelParams::EmpiricalConstant {
radial,
transverse,
normal,
} => Ok(Box::new(EmpiricalAcceleration::rtn(
*radial,
*transverse,
*normal,
))),
_ => Err(PodDynamicsError::InvalidParameters {
name: "empirical_constant".into(),
reason:
"expected ForceModelParams::EmpiricalConstant { radial, transverse, normal }",
}),
}
}
}
fn build_periodic(
expected: PeriodicHarmonic,
label: &'static str,
p: &ForceModelParams,
) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
match p {
ForceModelParams::EmpiricalPeriodic {
harmonic,
epoch_ref,
period,
coeffs,
} if *harmonic == expected => Ok(Box::new(EmpiricalPeriodicAcceleration::new(
*harmonic, *epoch_ref, *period, coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4],
coeffs[5],
))),
_ => Err(PodDynamicsError::InvalidParameters {
name: label.into(),
reason: "expected ForceModelParams::EmpiricalPeriodic with matching harmonic",
}),
}
}
struct Empirical1CprFactory;
impl ForceModelFactory for Empirical1CprFactory {
fn name(&self) -> &'static str {
"empirical_1cpr"
}
fn build(&self, p: &ForceModelParams) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
build_periodic(PeriodicHarmonic::OncePerRev, "empirical_1cpr", p)
}
}
struct Empirical2CprFactory;
impl ForceModelFactory for Empirical2CprFactory {
fn name(&self) -> &'static str {
"empirical_2cpr"
}
fn build(&self, p: &ForceModelParams) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
build_periodic(PeriodicHarmonic::TwicePerRev, "empirical_2cpr", p)
}
}
pub type AtmosphereDensityProvider = dyn DensityProvider + Send + Sync;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtins_have_expected_names() {
let r = ForceModelRegistry::with_builtins();
for name in [
"two_body",
"j2",
"geopotential",
"third_body_sun",
"third_body_moon",
"third_body_sun_moon",
"drag",
"srp_cannonball",
"relativity",
"empirical_constant",
"empirical_1cpr",
"empirical_2cpr",
] {
assert!(r.is_registered(name), "missing builtin: {name}");
}
}
#[test]
fn unknown_model_errors() {
let r = ForceModelRegistry::with_builtins();
let e = r.build_one(&ForceModelSpec::named("nope")).err().unwrap();
assert!(matches!(e, PodDynamicsError::UnknownModel(ref s) if s == "nope"));
}
#[test]
fn drag_requires_typed_params() {
let r = ForceModelRegistry::with_builtins();
let e = r.build_one(&ForceModelSpec::named("drag")).err().unwrap();
assert!(matches!(e, PodDynamicsError::InvalidParameters { .. }));
let ok = r.build_one(&ForceModelSpec::with_params(
"drag",
ForceModelParams::Drag {
cd: DragCoefficient::new(2.2),
area_to_mass: AreaToMass::new(0.01),
},
));
assert!(ok.is_ok());
}
#[test]
fn geopotential_requires_params() {
let r = ForceModelRegistry::with_builtins();
let e = r
.build_one(&ForceModelSpec::named("geopotential"))
.err()
.unwrap();
assert!(matches!(e, PodDynamicsError::InvalidParameters { .. }));
let ok = r.build_one(&ForceModelSpec::with_params(
"geopotential",
ForceModelParams::Geopotential {
degree: 4,
order: 4,
},
));
assert!(ok.is_ok());
}
#[test]
fn srp_cannonball_all_shadow_models() {
let r = ForceModelRegistry::with_builtins();
for shadow in [
ShadowModel::None,
ShadowModel::Cylindrical,
ShadowModel::Conical,
] {
let ok = r.build_one(&ForceModelSpec::with_params(
"srp_cannonball",
ForceModelParams::SrpCannonball {
cr: SrpCoefficient::new(1.5),
area_to_mass: AreaToMass::new(0.01),
shadow,
},
));
assert!(ok.is_ok(), "srp_cannonball with shadow={shadow:?} failed");
}
}
#[test]
fn srp_cannonball_no_params_is_error() {
let r = ForceModelRegistry::with_builtins();
let e = r
.build_one(&ForceModelSpec::named("srp_cannonball"))
.err()
.unwrap();
assert!(matches!(e, PodDynamicsError::InvalidParameters { .. }));
}
#[test]
fn empirical_constant_builds_ok() {
let r = ForceModelRegistry::with_builtins();
let ok = r.build_one(&ForceModelSpec::with_params(
"empirical_constant",
ForceModelParams::EmpiricalConstant {
radial: KmPerSecondsSquared::new(1e-8),
transverse: KmPerSecondsSquared::new(0.0),
normal: KmPerSecondsSquared::new(0.0),
},
));
assert!(ok.is_ok());
}
#[test]
fn empirical_1cpr_builds_with_once_per_rev() {
let r = ForceModelRegistry::with_builtins();
let ok = r.build_one(&ForceModelSpec::with_params(
"empirical_1cpr",
ForceModelParams::EmpiricalPeriodic {
harmonic: PeriodicHarmonic::OncePerRev,
epoch_ref: crate::J2000,
period: Second::new(5400.0),
coeffs: [KmPerSecondsSquared::new(0.0); 6],
},
));
assert!(ok.is_ok());
}
#[test]
fn empirical_2cpr_wrong_harmonic_is_error() {
let r = ForceModelRegistry::with_builtins();
let e = r
.build_one(&ForceModelSpec::with_params(
"empirical_2cpr",
ForceModelParams::EmpiricalPeriodic {
harmonic: PeriodicHarmonic::OncePerRev, epoch_ref: crate::J2000,
period: Second::new(5400.0),
coeffs: [KmPerSecondsSquared::new(0.0); 6],
},
))
.err()
.unwrap();
assert!(matches!(e, PodDynamicsError::InvalidParameters { .. }));
}
#[test]
fn names_returns_all_builtins_sorted() {
let r = ForceModelRegistry::with_builtins();
let names = r.names();
assert!(names.windows(2).all(|w| w[0] <= w[1]), "names not sorted");
assert!(names.contains(&"two_body"));
assert!(names.contains(&"empirical_2cpr"));
}
#[test]
fn register_custom_factory() {
struct MyFactory;
impl ForceModelFactory for MyFactory {
fn name(&self) -> &'static str {
"my_custom"
}
fn build(
&self,
_p: &ForceModelParams,
) -> Result<Box<DynSiderustForceModel>, PodDynamicsError> {
Ok(Box::new(TwoBody::new(GM_EARTH)))
}
}
let mut r = ForceModelRegistry::with_builtins();
assert!(!r.is_registered("my_custom"));
r.register(Box::new(MyFactory));
assert!(r.is_registered("my_custom"));
}
#[test]
fn build_batch_returns_composite() {
let r = ForceModelRegistry::with_builtins();
let composite = r
.build(&[
ForceModelSpec::named("two_body"),
ForceModelSpec::named("j2"),
])
.unwrap();
assert_eq!(composite.len(), 2);
}
}