use super::{DynamicsAlmanacSnafu, DynamicsError, DynamicsPlanetarySnafu, ForceModel};
use crate::cosmic::eclipse::ShadowModel;
use crate::cosmic::{AU, Frame, SPEED_OF_LIGHT_M_S, Spacecraft};
use crate::linalg::{Const, Matrix4x3, Vector3};
use anise::almanac::Almanac;
use anise::constants::frames::{EARTH_J2000, SUN_J2000};
use hyperdual::{Float, OHyperdual, hyperspace_from_vector, linalg::norm};
use log::warn;
use serde::{Deserialize, Serialize};
use serde_dhall::StaticType;
use snafu::ResultExt;
use std::fmt;
use std::sync::Arc;
#[allow(non_upper_case_globals)]
pub const SOLAR_FLUX_W_m2: f64 = 1367.0;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[derive(Clone, Debug, Serialize, Deserialize, StaticType)]
#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
pub struct SolarPressure {
pub phi: f64,
pub shadow_model: ShadowModel,
pub estimate: bool,
}
impl Default for SolarPressure {
fn default() -> Self {
Self {
phi: SOLAR_FLUX_W_m2,
estimate: false,
shadow_model: ShadowModel {
light_source: SUN_J2000,
shadow_bodies: vec![EARTH_J2000],
},
}
}
}
impl SolarPressure {
pub fn default_flux_raw(
shadow_bodies: Vec<Frame>,
almanac: &Almanac,
) -> Result<Self, DynamicsError> {
let shadow_model = ShadowModel {
light_source: almanac.frame_info(SUN_J2000).context({
DynamicsPlanetarySnafu {
action: "planetary data from third body not loaded",
}
})?,
shadow_bodies: shadow_bodies
.iter()
.filter_map(|object| match almanac.frame_info(object) {
Ok(loaded_obj) => Some(loaded_obj),
Err(e) => {
warn!("when initializing SRP model for {object}, {e}");
None
}
})
.collect(),
};
Ok(Self {
phi: SOLAR_FLUX_W_m2,
shadow_model,
estimate: true,
})
}
pub fn default_flux(shadow_body: Frame, almanac: &Almanac) -> Result<Arc<Self>, DynamicsError> {
Ok(Arc::new(Self::default_flux_raw(
vec![shadow_body],
almanac,
)?))
}
pub fn default_no_estimation(
shadow_bodies: Vec<Frame>,
almanac: &Almanac,
) -> Result<Arc<Self>, DynamicsError> {
let mut srp = Self::default_flux_raw(shadow_bodies, almanac)?;
srp.estimate = false;
Ok(Arc::new(srp))
}
pub fn with_flux(
flux_w_m2: f64,
shadow_bodies: Vec<Frame>,
almanac: &Almanac,
) -> Result<Arc<Self>, DynamicsError> {
let mut me = Self::default_flux_raw(shadow_bodies, almanac)?;
me.phi = flux_w_m2;
Ok(Arc::new(me))
}
pub fn new(shadow_bodies: Vec<Frame>, almanac: &Almanac) -> Result<Arc<Self>, DynamicsError> {
Ok(Arc::new(Self::default_flux_raw(shadow_bodies, almanac)?))
}
}
impl ForceModel for SolarPressure {
fn estimation_index(&self) -> Option<usize> {
if self.estimate { Some(6) } else { None }
}
fn eom(&self, ctx: &Spacecraft, almanac: &Almanac) -> Result<Vector3<f64>, DynamicsError> {
let osc = ctx.orbit;
let r_sun = almanac
.transform_to(ctx.orbit, self.shadow_model.light_source, None)
.context(DynamicsAlmanacSnafu {
action: "transforming state to vector seen from Sun",
})?
.radius_km;
let r_sun_unit = r_sun / r_sun.norm();
let occult = self
.shadow_model
.compute(osc, almanac)
.context(DynamicsAlmanacSnafu {
action: "solar radiation pressure computation",
})?
.factor();
let k: f64 = (occult - 1.0).abs();
let r_sun_au = r_sun.norm() / AU;
let flux_pressure = (k * self.phi / SPEED_OF_LIGHT_M_S) * (1.0 / r_sun_au).powi(2);
Ok(1e-3 * ctx.srp.coeff_reflectivity * ctx.srp.area_m2 * flux_pressure * r_sun_unit)
}
fn gradient(
&self,
ctx: &Spacecraft,
almanac: &Almanac,
) -> Result<(Vector3<f64>, Matrix4x3<f64>), DynamicsError> {
let osc = ctx.orbit;
let r_sun = almanac
.transform_to(ctx.orbit, self.shadow_model.light_source, None)
.context(DynamicsAlmanacSnafu {
action: "transforming state to vector seen from Sun",
})?
.radius_km;
let r_sun_d: Vector3<OHyperdual<f64, Const<9>>> = hyperspace_from_vector(&r_sun);
let r_sun_unit = r_sun_d / norm(&r_sun_d);
let occult = self
.shadow_model
.compute(osc, almanac)
.context(DynamicsAlmanacSnafu {
action: "solar radiation pressure computation",
})?
.factor();
let k: f64 = (occult - 1.0).abs();
let r_sun_au = norm(&r_sun_d) / AU;
let inv_r_sun_au = OHyperdual::<f64, Const<9>>::from_real(1.0) / (r_sun_au);
let inv_r_sun_au_p2 = inv_r_sun_au.powi(2);
let flux_pressure =
OHyperdual::<f64, Const<9>>::from_real(k * self.phi / SPEED_OF_LIGHT_M_S)
* inv_r_sun_au_p2;
let dual_force_scalar = OHyperdual::<f64, Const<9>>::from_real(
1e-3 * ctx.srp.coeff_reflectivity * ctx.srp.area_m2,
);
let mut dual_force: Vector3<OHyperdual<f64, Const<9>>> = Vector3::zeros();
dual_force[0] = dual_force_scalar * flux_pressure * r_sun_unit[0];
dual_force[1] = dual_force_scalar * flux_pressure * r_sun_unit[1];
dual_force[2] = dual_force_scalar * flux_pressure * r_sun_unit[2];
let mut dx = Vector3::zeros();
let mut grad = Matrix4x3::zeros();
for i in 0..3 {
dx[i] += dual_force[i].real();
for j in 0..3 {
grad[(i, j)] += dual_force[i][j + 1];
}
}
let wrt_cr = self.eom(ctx, almanac)? / ctx.srp.coeff_reflectivity;
for j in 0..3 {
grad[(3, j)] = wrt_cr[j];
}
Ok((dx, grad))
}
}
impl fmt::Display for SolarPressure {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"SRP with φ = {} W/m^2 and eclipse {}",
self.phi, self.shadow_model
)
}
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl SolarPressure {
#[pyo3(signature = (shadow_bodies, almanac, flux_w_m2=SOLAR_FLUX_W_m2, estimate=true))]
#[new]
fn py_new(
shadow_bodies: Vec<Frame>,
almanac: &Almanac,
flux_w_m2: f64,
estimate: bool,
) -> Result<Self, DynamicsError> {
let mut me = Self::default_flux_raw(shadow_bodies, almanac)?;
me.phi = flux_w_m2;
me.estimate = estimate;
Ok(me)
}
fn __str__(&self) -> String {
format!("{self}")
}
fn __repr__(&self) -> String {
format!("{self} @ {self:p}")
}
}