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