1use super::{DynamicsAlmanacSnafu, DynamicsError, DynamicsPlanetarySnafu, ForceModel};
20use crate::cosmic::eclipse::ShadowModel;
21use crate::cosmic::{AU, Frame, SPEED_OF_LIGHT_M_S, Spacecraft};
22use crate::linalg::{Const, Matrix4x3, Vector3};
23use anise::almanac::Almanac;
24#[cfg(feature = "python")]
25use anise::astro::Aberration;
26use anise::constants::frames::{EARTH_J2000, SUN_J2000};
27use hyperdual::{Float, OHyperdual, hyperspace_from_vector, linalg::norm};
28use log::warn;
29use serde::{Deserialize, Serialize};
30use serde_dhall::StaticType;
31use snafu::ResultExt;
32use std::fmt;
33use std::sync::Arc;
34
35#[allow(non_upper_case_globals)]
37pub const SOLAR_FLUX_W_m2: f64 = 1367.0;
38
39#[cfg(feature = "python")]
40use pyo3::prelude::*;
41
42#[derive(Clone, Debug, Serialize, Deserialize, StaticType)]
49#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
50pub struct SolarPressure {
51 pub phi: f64,
53 pub shadow_model: ShadowModel,
54 pub estimate: bool,
56}
57
58impl Default for SolarPressure {
59 fn default() -> Self {
61 Self {
62 phi: SOLAR_FLUX_W_m2,
63 estimate: false,
64 shadow_model: ShadowModel {
65 light_source: SUN_J2000,
66 shadow_bodies: vec![EARTH_J2000],
67 correction: None,
68 },
69 }
70 }
71}
72
73impl SolarPressure {
74 pub fn default_flux_raw(
76 shadow_bodies: Vec<Frame>,
77 almanac: &Almanac,
78 ) -> Result<Self, DynamicsError> {
79 let shadow_model = ShadowModel {
80 light_source: almanac.frame_info(SUN_J2000).context({
81 DynamicsPlanetarySnafu {
82 action: "planetary data from third body not loaded",
83 }
84 })?,
85 shadow_bodies: shadow_bodies
86 .iter()
87 .filter_map(|object| match almanac.frame_info(object) {
88 Ok(loaded_obj) => Some(loaded_obj),
89 Err(e) => {
90 warn!("when initializing SRP model for {object}, {e}");
91 None
92 }
93 })
94 .collect(),
95 correction: None,
96 };
97 Ok(Self {
98 phi: SOLAR_FLUX_W_m2,
99 shadow_model,
100 estimate: true,
101 })
102 }
103
104 pub fn default_flux(shadow_body: Frame, almanac: &Almanac) -> Result<Arc<Self>, DynamicsError> {
106 Ok(Arc::new(Self::default_flux_raw(
107 vec![shadow_body],
108 almanac,
109 )?))
110 }
111
112 pub fn default_no_estimation(
114 shadow_bodies: Vec<Frame>,
115 almanac: &Almanac,
116 ) -> Result<Arc<Self>, DynamicsError> {
117 let mut srp = Self::default_flux_raw(shadow_bodies, almanac)?;
118 srp.estimate = false;
119 Ok(Arc::new(srp))
120 }
121
122 pub fn with_flux(
124 flux_w_m2: f64,
125 shadow_bodies: Vec<Frame>,
126 almanac: &Almanac,
127 ) -> Result<Arc<Self>, DynamicsError> {
128 let mut me = Self::default_flux_raw(shadow_bodies, almanac)?;
129 me.phi = flux_w_m2;
130 Ok(Arc::new(me))
131 }
132
133 pub fn new(shadow_bodies: Vec<Frame>, almanac: &Almanac) -> Result<Arc<Self>, DynamicsError> {
135 Ok(Arc::new(Self::default_flux_raw(shadow_bodies, almanac)?))
136 }
137}
138
139impl ForceModel for SolarPressure {
140 fn estimation_index(&self) -> Option<usize> {
141 if self.estimate { Some(6) } else { None }
142 }
143
144 fn eom(&self, ctx: &Spacecraft, almanac: &Almanac) -> Result<Vector3<f64>, DynamicsError> {
145 let osc = ctx.orbit;
146 let r_sun = almanac
148 .transform_to(ctx.orbit, self.shadow_model.light_source, None)
149 .context(DynamicsAlmanacSnafu {
150 action: "transforming state to vector seen from Sun",
151 })?
152 .radius_km;
153
154 let r_sun_unit = r_sun / r_sun.norm();
155
156 let occult = self
158 .shadow_model
159 .compute(osc, almanac)
160 .context(DynamicsAlmanacSnafu {
161 action: "solar radiation pressure computation",
162 })?
163 .factor();
164
165 let k: f64 = (occult - 1.0).abs();
167
168 let r_sun_au = r_sun.norm() / AU;
169 let flux_pressure = (k * self.phi / SPEED_OF_LIGHT_M_S) * (1.0 / r_sun_au).powi(2);
171
172 Ok(1e-3 * ctx.srp.coeff_reflectivity * ctx.srp.area_m2 * flux_pressure * r_sun_unit)
174 }
175
176 fn gradient(
177 &self,
178 ctx: &Spacecraft,
179 almanac: &Almanac,
180 ) -> Result<(Vector3<f64>, Matrix4x3<f64>), DynamicsError> {
181 let osc = ctx.orbit;
182
183 let r_sun = almanac
185 .transform_to(ctx.orbit, self.shadow_model.light_source, None)
186 .context(DynamicsAlmanacSnafu {
187 action: "transforming state to vector seen from Sun",
188 })?
189 .radius_km;
190
191 let r_sun_d: Vector3<OHyperdual<f64, Const<9>>> = hyperspace_from_vector(&r_sun);
192 let r_sun_unit = r_sun_d / norm(&r_sun_d);
193
194 let occult = self
196 .shadow_model
197 .compute(osc, almanac)
198 .context(DynamicsAlmanacSnafu {
199 action: "solar radiation pressure computation",
200 })?
201 .factor();
202
203 let k: f64 = (occult - 1.0).abs();
205
206 let r_sun_au = norm(&r_sun_d) / AU;
207 let inv_r_sun_au = OHyperdual::<f64, Const<9>>::from_real(1.0) / (r_sun_au);
208 let inv_r_sun_au_p2 = inv_r_sun_au.powi(2);
209 let flux_pressure =
211 OHyperdual::<f64, Const<9>>::from_real(k * self.phi / SPEED_OF_LIGHT_M_S)
212 * inv_r_sun_au_p2;
213
214 let dual_force_scalar = OHyperdual::<f64, Const<9>>::from_real(
216 1e-3 * ctx.srp.coeff_reflectivity * ctx.srp.area_m2,
217 );
218 let mut dual_force: Vector3<OHyperdual<f64, Const<9>>> = Vector3::zeros();
219 dual_force[0] = dual_force_scalar * flux_pressure * r_sun_unit[0];
220 dual_force[1] = dual_force_scalar * flux_pressure * r_sun_unit[1];
221 dual_force[2] = dual_force_scalar * flux_pressure * r_sun_unit[2];
222
223 let mut dx = Vector3::zeros();
225 let mut grad = Matrix4x3::zeros();
226 for i in 0..3 {
227 dx[i] += dual_force[i].real();
228 for j in 0..3 {
230 grad[(i, j)] += dual_force[i][j + 1];
231 }
232 }
233
234 let wrt_cr = self.eom(ctx, almanac)? / ctx.srp.coeff_reflectivity;
236 for j in 0..3 {
237 grad[(3, j)] = wrt_cr[j];
238 }
239
240 Ok((dx, grad))
241 }
242}
243
244impl fmt::Display for SolarPressure {
245 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
246 write!(
247 f,
248 "SRP with φ = {} W/m^2 and eclipse {}",
249 self.phi, self.shadow_model
250 )
251 }
252}
253
254#[cfg(feature = "python")]
255#[cfg_attr(feature = "python", pymethods)]
256impl SolarPressure {
257 #[pyo3(signature = (shadow_bodies, almanac, correction=None, flux_w_m2=SOLAR_FLUX_W_m2, estimate=true))]
258 #[new]
259 fn py_new(
260 shadow_bodies: Vec<Frame>,
261 almanac: &Almanac,
262 correction: Option<Aberration>,
263 flux_w_m2: f64,
264 estimate: bool,
265 ) -> Result<Self, DynamicsError> {
266 let mut me = Self::default_flux_raw(shadow_bodies, almanac)?;
267 me.phi = flux_w_m2;
268 me.estimate = estimate;
269 me.shadow_model.correction = correction;
270
271 Ok(me)
272 }
273
274 fn __str__(&self) -> String {
275 format!("{self}")
276 }
277
278 fn __repr__(&self) -> String {
279 format!("{self} @ {self:p}")
280 }
281}