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