use serde::{Deserialize, Serialize};
use crate::orbit_dynamics::gravity::GravityModelType;
use crate::spice::SPKKernel;
use crate::utils::BraheError;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ParameterSource {
Value(f64),
ParameterIndex(usize),
}
impl ParameterSource {
pub fn get_value(&self, params: Option<&nalgebra::DVector<f64>>) -> f64 {
match self {
ParameterSource::Value(v) => *v,
ParameterSource::ParameterIndex(idx) => params.map(|p| p[*idx]).unwrap_or_else(|| {
panic!("Parameter vector missing or index {} out of bounds", idx)
}),
}
}
pub fn requires_params(&self) -> bool {
matches!(self, ParameterSource::ParameterIndex(_))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForceModelConfig {
pub gravity: GravityConfiguration,
pub drag: Option<DragConfiguration>,
pub srp: Option<SolarRadiationPressureConfiguration>,
pub third_body: Option<ThirdBodyConfiguration>,
pub relativity: bool,
pub mass: Option<ParameterSource>,
}
impl Default for ForceModelConfig {
fn default() -> Self {
Self {
gravity: GravityConfiguration::SphericalHarmonic {
source: GravityModelSource::default(),
degree: 20,
order: 20,
},
drag: Some(DragConfiguration {
model: AtmosphericModel::NRLMSISE00,
area: ParameterSource::ParameterIndex(1),
cd: ParameterSource::ParameterIndex(2),
}),
srp: Some(SolarRadiationPressureConfiguration {
area: ParameterSource::ParameterIndex(3),
cr: ParameterSource::ParameterIndex(4),
eclipse_model: EclipseModel::Conical,
}),
third_body: Some(ThirdBodyConfiguration {
ephemeris_source: EphemerisSource::DE440s,
bodies: vec![ThirdBody::Sun, ThirdBody::Moon],
}),
relativity: false,
mass: Some(ParameterSource::ParameterIndex(0)),
}
}
}
impl ForceModelConfig {
pub fn requires_params(&self) -> bool {
if let Some(ref mass) = self.mass
&& mass.requires_params()
{
return true;
}
if let Some(ref drag) = self.drag
&& (drag.area.requires_params() || drag.cd.requires_params())
{
return true;
}
if let Some(ref srp) = self.srp
&& (srp.area.requires_params() || srp.cr.requires_params())
{
return true;
}
false
}
fn max_required_param_index(&self) -> Option<usize> {
let mut max_idx: Option<usize> = None;
if let Some(ParameterSource::ParameterIndex(idx)) = self.mass {
max_idx = Some(max_idx.map_or(idx, |m| m.max(idx)));
}
if let Some(ref drag) = self.drag {
if let ParameterSource::ParameterIndex(idx) = drag.area {
max_idx = Some(max_idx.map_or(idx, |m| m.max(idx)));
}
if let ParameterSource::ParameterIndex(idx) = drag.cd {
max_idx = Some(max_idx.map_or(idx, |m| m.max(idx)));
}
}
if let Some(ref srp) = self.srp {
if let ParameterSource::ParameterIndex(idx) = srp.area {
max_idx = Some(max_idx.map_or(idx, |m| m.max(idx)));
}
if let ParameterSource::ParameterIndex(idx) = srp.cr {
max_idx = Some(max_idx.map_or(idx, |m| m.max(idx)));
}
}
max_idx
}
pub fn validate_params(
&self,
params: Option<&nalgebra::DVector<f64>>,
) -> Result<(), crate::utils::errors::BraheError> {
if let Some(max_idx) = self.max_required_param_index() {
match params {
None => {
return Err(crate::utils::errors::BraheError::Error(format!(
"Force model configuration references parameter index {} but no parameter \
vector was provided. Use ForceModelConfig::earth_gravity() for \
propagation without parameters, or provide a parameter vector with at \
least {} elements.",
max_idx,
max_idx + 1
)));
}
Some(p) if p.len() <= max_idx => {
return Err(crate::utils::errors::BraheError::Error(format!(
"Parameter vector length {} is insufficient; force model configuration \
requires at least {} elements (max index: {})",
p.len(),
max_idx + 1,
max_idx
)));
}
_ => {} }
}
if let Some(drag_config) = &self.drag
&& matches!(drag_config.model, AtmosphericModel::NRLMSISE00)
&& !crate::space_weather::get_global_sw_initialization()
{
return Err(crate::utils::errors::BraheError::Error(
"NRLMSISE-00 atmospheric model requires space weather data. \
Call initialize_sw() before creating a propagator with NRLMSISE-00, \
initialize another space weather data provider, \
or use AtmosphericModel::HarrisPriester which does not require \
space weather data."
.to_string(),
));
}
Ok(())
}
pub fn high_fidelity() -> Self {
Self {
gravity: GravityConfiguration::SphericalHarmonic {
source: GravityModelSource::ModelType(GravityModelType::EGM2008_360),
degree: 120,
order: 120,
},
drag: Some(DragConfiguration {
model: AtmosphericModel::NRLMSISE00,
area: ParameterSource::ParameterIndex(1),
cd: ParameterSource::ParameterIndex(2),
}),
srp: Some(SolarRadiationPressureConfiguration {
area: ParameterSource::ParameterIndex(3),
cr: ParameterSource::ParameterIndex(4),
eclipse_model: EclipseModel::Conical,
}),
third_body: Some(ThirdBodyConfiguration {
ephemeris_source: EphemerisSource::DE440s,
bodies: vec![
ThirdBody::Sun,
ThirdBody::Moon,
ThirdBody::Venus,
ThirdBody::Mars,
ThirdBody::Jupiter,
ThirdBody::Saturn,
ThirdBody::Uranus,
ThirdBody::Neptune,
ThirdBody::Mercury,
],
}),
relativity: true,
mass: Some(ParameterSource::ParameterIndex(0)),
}
}
pub fn earth_gravity() -> Self {
Self {
gravity: GravityConfiguration::SphericalHarmonic {
source: GravityModelSource::ModelType(GravityModelType::EGM2008_360),
degree: 20,
order: 20,
},
drag: None,
srp: None,
third_body: None,
relativity: false,
mass: None,
}
}
pub fn two_body_gravity() -> Self {
Self {
gravity: GravityConfiguration::PointMass,
drag: None,
srp: None,
third_body: None,
relativity: false,
mass: None,
}
}
pub fn conservative_forces() -> Self {
Self {
gravity: GravityConfiguration::SphericalHarmonic {
source: GravityModelSource::ModelType(GravityModelType::EGM2008_360),
degree: 80,
order: 80,
},
drag: None,
srp: None,
third_body: Some(ThirdBodyConfiguration {
ephemeris_source: EphemerisSource::DE440s,
bodies: vec![ThirdBody::Sun, ThirdBody::Moon],
}),
relativity: true,
mass: None,
}
}
pub fn leo_default() -> Self {
Self {
gravity: GravityConfiguration::SphericalHarmonic {
source: GravityModelSource::ModelType(GravityModelType::EGM2008_360),
degree: 30,
order: 30,
},
drag: Some(DragConfiguration {
model: AtmosphericModel::NRLMSISE00,
area: ParameterSource::ParameterIndex(1),
cd: ParameterSource::ParameterIndex(2),
}),
srp: Some(SolarRadiationPressureConfiguration {
area: ParameterSource::ParameterIndex(3),
cr: ParameterSource::ParameterIndex(4),
eclipse_model: EclipseModel::Conical,
}),
third_body: Some(ThirdBodyConfiguration {
ephemeris_source: EphemerisSource::DE440s,
bodies: vec![ThirdBody::Sun, ThirdBody::Moon],
}),
relativity: false,
mass: Some(ParameterSource::ParameterIndex(0)),
}
}
pub fn geo_default() -> Self {
Self {
gravity: GravityConfiguration::SphericalHarmonic {
source: GravityModelSource::ModelType(GravityModelType::EGM2008_360),
degree: 8,
order: 8,
},
drag: None,
srp: Some(SolarRadiationPressureConfiguration {
area: ParameterSource::ParameterIndex(3),
cr: ParameterSource::ParameterIndex(4),
eclipse_model: EclipseModel::Conical,
}),
third_body: Some(ThirdBodyConfiguration {
ephemeris_source: EphemerisSource::DE440s,
bodies: vec![ThirdBody::Sun, ThirdBody::Moon],
}),
relativity: false,
mass: Some(ParameterSource::ParameterIndex(0)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum GravityModelSource {
Global,
ModelType(GravityModelType),
}
impl Default for GravityModelSource {
fn default() -> Self {
GravityModelSource::ModelType(GravityModelType::EGM2008_360)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum GravityConfiguration {
PointMass,
SphericalHarmonic {
source: GravityModelSource,
degree: usize,
order: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DragConfiguration {
pub model: AtmosphericModel,
pub area: ParameterSource,
pub cd: ParameterSource,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AtmosphericModel {
HarrisPriester,
NRLMSISE00,
Exponential {
scale_height: f64,
rho0: f64,
h0: f64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SolarRadiationPressureConfiguration {
pub area: ParameterSource,
pub cr: ParameterSource,
pub eclipse_model: EclipseModel,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum EclipseModel {
None,
Cylindrical,
Conical,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ThirdBodyConfiguration {
pub ephemeris_source: EphemerisSource,
pub bodies: Vec<ThirdBody>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum EphemerisSource {
LowPrecision,
DE440s,
DE440,
SPK(SPKKernel),
}
impl TryFrom<EphemerisSource> for SPKKernel {
type Error = BraheError;
fn try_from(source: EphemerisSource) -> Result<Self, Self::Error> {
match source {
EphemerisSource::DE440s => Ok(SPKKernel::DE440s),
EphemerisSource::DE440 => Ok(SPKKernel::DE440),
EphemerisSource::SPK(kernel) => Ok(kernel),
EphemerisSource::LowPrecision => Err(BraheError::Error(
"LowPrecision is not a valid DE kernel - use DE440s, DE440, or EphemerisSource::SPK(...)"
.to_string(),
)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum ThirdBody {
Sun,
Moon,
Mercury,
Venus,
Mars,
Jupiter,
Saturn,
Uranus,
Neptune,
}
pub struct DefaultParameterLayout;
impl DefaultParameterLayout {
pub const MASS: usize = 0;
pub const DRAG_AREA: usize = 1;
pub const CD: usize = 2;
pub const SRP_AREA: usize = 3;
pub const CR: usize = 4;
pub const NUM_STANDARD_PARAMS: usize = 5;
pub fn create(mass: f64, drag_area: f64, cd: f64, srp_area: f64, cr: f64) -> Vec<f64> {
vec![mass, drag_area, cd, srp_area, cr]
}
pub fn default_values() -> Vec<f64> {
vec![1000.0, 10.0, 2.2, 10.0, 1.3]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_configuration() {
let config = ForceModelConfig::default();
match config.gravity {
GravityConfiguration::SphericalHarmonic { degree, order, .. } => {
assert_eq!(degree, 20);
assert_eq!(order, 20);
}
_ => panic!("Expected spherical harmonic gravity"),
}
assert!(config.drag.is_some());
assert!(config.srp.is_some());
assert!(config.third_body.is_some());
assert!(!config.relativity);
}
#[test]
fn test_high_fidelity_configuration() {
let config = ForceModelConfig::high_fidelity();
match config.gravity {
GravityConfiguration::SphericalHarmonic { degree, order, .. } => {
assert_eq!(degree, 120);
assert_eq!(order, 120);
}
_ => panic!("Expected spherical harmonic gravity"),
}
let drag = config.drag.unwrap();
assert!(matches!(drag.model, AtmosphericModel::NRLMSISE00));
let tb = config.third_body.unwrap();
assert!(matches!(tb.ephemeris_source, EphemerisSource::DE440s));
assert!(config.relativity);
assert!(matches!(
config.mass,
Some(ParameterSource::ParameterIndex(0))
));
}
#[test]
fn test_ephemeris_source_to_spk_kernel() {
assert_eq!(
SPKKernel::try_from(EphemerisSource::DE440s).unwrap(),
SPKKernel::DE440s
);
assert_eq!(
SPKKernel::try_from(EphemerisSource::DE440).unwrap(),
SPKKernel::DE440
);
assert_eq!(
SPKKernel::try_from(EphemerisSource::SPK(SPKKernel::DE440s)).unwrap(),
SPKKernel::DE440s
);
assert!(SPKKernel::try_from(EphemerisSource::LowPrecision).is_err());
}
#[test]
fn test_gravity_only_configuration() {
let config = ForceModelConfig::earth_gravity();
assert!(matches!(
config.gravity,
GravityConfiguration::SphericalHarmonic { .. }
));
assert!(config.drag.is_none());
assert!(config.srp.is_none());
assert!(config.third_body.is_none());
assert!(!config.relativity);
assert!(config.mass.is_none());
}
#[test]
fn test_leo_configuration() {
let config = ForceModelConfig::leo_default();
assert!(config.drag.is_some());
assert!(config.srp.is_some());
assert!(config.third_body.is_some());
assert!(matches!(
config.mass,
Some(ParameterSource::ParameterIndex(0))
));
}
#[test]
fn test_geo_configuration() {
let config = ForceModelConfig::geo_default();
assert!(config.drag.is_none());
assert!(config.srp.is_some());
assert!(config.third_body.is_some());
}
#[test]
fn test_default_parameter_layout() {
let params = DefaultParameterLayout::default_values();
assert_eq!(params.len(), DefaultParameterLayout::NUM_STANDARD_PARAMS);
assert_eq!(params[DefaultParameterLayout::MASS], 1000.0);
assert_eq!(params[DefaultParameterLayout::DRAG_AREA], 10.0);
assert_eq!(params[DefaultParameterLayout::CD], 2.2);
assert_eq!(params[DefaultParameterLayout::SRP_AREA], 10.0);
assert_eq!(params[DefaultParameterLayout::CR], 1.3);
}
#[test]
fn test_custom_parameter_layout() {
let params = DefaultParameterLayout::create(500.0, 5.0, 2.0, 8.0, 1.5);
assert_eq!(params[DefaultParameterLayout::MASS], 500.0);
assert_eq!(params[DefaultParameterLayout::DRAG_AREA], 5.0);
assert_eq!(params[DefaultParameterLayout::CD], 2.0);
assert_eq!(params[DefaultParameterLayout::SRP_AREA], 8.0);
assert_eq!(params[DefaultParameterLayout::CR], 1.5);
}
#[test]
fn test_serialization() {
let config = ForceModelConfig::default();
let json = serde_json::to_string(&config).unwrap();
let deserialized: ForceModelConfig = serde_json::from_str(&json).unwrap();
assert!(matches!(
deserialized.gravity,
GravityConfiguration::SphericalHarmonic { .. }
));
assert!(deserialized.drag.is_some());
}
#[test]
fn test_requires_params_with_mass_param_index() {
let config = ForceModelConfig {
mass: Some(ParameterSource::ParameterIndex(5)),
drag: None,
srp: None,
..ForceModelConfig::earth_gravity()
};
assert!(config.requires_params());
}
#[test]
fn test_requires_params_with_mass_value() {
let config = ForceModelConfig {
mass: Some(ParameterSource::Value(1000.0)),
drag: None,
srp: None,
..ForceModelConfig::earth_gravity()
};
assert!(!config.requires_params());
}
}