Skip to main content

nyx_space/dynamics/
solarpressure.rs

1/*
2    Nyx, blazing fast astrodynamics
3    Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU Affero General Public License as published
7    by the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU Affero General Public License for more details.
14
15    You should have received a copy of the GNU Affero General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use 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// Default solar flux in W/m^2
36#[allow(non_upper_case_globals)]
37pub const SOLAR_FLUX_W_m2: f64 = 1367.0;
38
39#[cfg(feature = "python")]
40use pyo3::prelude::*;
41
42/// Computation of solar radiation pressure is based on STK: <http://help.agi.com/stk/index.htm#gator/eq-solar.htm> .
43///
44/// :type shadow_bodies: list[Frame]
45/// :type almanac: Almanac
46/// :type flux_w_m2: float
47/// :type estimate: bool
48#[derive(Clone, Debug, Serialize, Deserialize, StaticType)]
49#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
50pub struct SolarPressure {
51    /// solar flux at 1 AU, in W/m^2
52    pub phi: f64,
53    pub shadow_model: ShadowModel,
54    /// Set to true to estimate the coefficient of reflectivity
55    pub estimate: bool,
56}
57
58impl Default for SolarPressure {
59    /// Important: the default will FAIL at runtime if the shadow model is not manually defined with loaded frames.
60    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    /// Will set the solar flux at 1 AU to: Phi = 1367.0
75    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    /// Accounts for the shadowing of only one body and will set the solar flux at 1 AU to: Phi = 1367.0
105    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    /// Accounts for the shadowing of only one body and will set the solar flux at 1 AU to: Phi = 1367.0
113    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    /// Must provide the flux in W/m^2
123    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    /// Solar radiation pressure force model accounting for the provided shadow bodies.
134    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        // Compute the position of the Sun as seen from the spacecraft
147        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        // ANISE returns the occultation percentage (or factor), which is the opposite as the illumination factor.
157        let occult = self
158            .shadow_model
159            .compute(osc, almanac)
160            .context(DynamicsAlmanacSnafu {
161                action: "solar radiation pressure computation",
162            })?
163            .factor();
164
165        // Compute the illumination factor.
166        let k: f64 = (occult - 1.0).abs();
167
168        let r_sun_au = r_sun.norm() / AU;
169        // in N/(m^2)
170        let flux_pressure = (k * self.phi / SPEED_OF_LIGHT_M_S) * (1.0 / r_sun_au).powi(2);
171
172        // Note the 1e-3 is to convert the SRP from m/s^2 to km/s^2
173        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        // Compute the position of the Sun as seen from the spacecraft
184        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        // ANISE returns the occultation percentage (or factor), which is the opposite as the illumination factor.
195        let occult = self
196            .shadow_model
197            .compute(osc, almanac)
198            .context(DynamicsAlmanacSnafu {
199                action: "solar radiation pressure computation",
200            })?
201            .factor();
202
203        // Compute the illumination factor.
204        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        // in N/(m^2)
210        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        // Note the 1e-3 is to convert the SRP from m/s^2 to km/s^2
215        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        // Extract result into Vector6 and Matrix6
224        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            // NOTE: Although the hyperdual state is of size 7, we're only setting the values up to 3 (Matrix3)
229            for j in 0..3 {
230                grad[(i, j)] += dual_force[i][j + 1];
231            }
232        }
233
234        // Compute the partial wrt to Cr.
235        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}