1use crate::cosmic::AstroPhysicsSnafu;
20use crate::dynamics::guidance::LocalFrame;
21use crate::errors::StateAstroSnafu;
22use crate::linalg::allocator::Allocator;
23use crate::linalg::{DefaultAllocator, DimName, OMatrix, OVector, U3, U6};
24use crate::md::StateParameter;
25use crate::od::{ODError, ODStateSnafu};
26use crate::time::{Duration, Epoch};
27use anise::prelude::Orbit;
28use log::debug;
29use snafu::ResultExt;
30use std::fmt;
31
32#[allow(clippy::upper_case_acronyms)]
33pub type ProcessNoise3D = ProcessNoise<U3>;
34
35#[allow(clippy::upper_case_acronyms)]
36pub type ProcessNoise6D = ProcessNoise<U6>;
37
38#[deprecated = "SNC has been renamed to ProcessNoise"]
39#[allow(clippy::upper_case_acronyms)]
40pub type SNC<A> = ProcessNoise<A>;
41#[deprecated = "SNC has been renamed to ProcessNoise"]
42#[allow(clippy::upper_case_acronyms)]
43pub type SNC3 = ProcessNoise<U3>;
44#[deprecated = "SNC has been renamed to ProcessNoise"]
45#[allow(clippy::upper_case_acronyms)]
46pub type SNC6 = ProcessNoise<U6>;
47
48#[derive(Clone)]
49#[allow(clippy::upper_case_acronyms)]
50pub struct ProcessNoise<A: DimName>
51where
52 DefaultAllocator: Allocator<A> + Allocator<A, A>,
53{
54 pub start_time: Option<Epoch>,
56 pub local_frame: Option<LocalFrame>,
58 pub disable_time: Duration,
60 pub init_epoch: Option<Epoch>,
62 diag: OVector<f64, A>,
63 decay_diag: Option<Vec<f64>>,
64 pub prev_epoch: Option<Epoch>,
66}
67
68impl<A> fmt::Debug for ProcessNoise<A>
69where
70 A: DimName,
71 DefaultAllocator: Allocator<A> + Allocator<A, A>,
72{
73 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
74 let mut fmt_cov = Vec::with_capacity(A::dim());
75 if let Some(decay) = &self.decay_diag {
76 for (i, dv) in decay.iter().enumerate() {
77 fmt_cov.push(format!(
78 "{:.1e} × exp(- {:.1e} × t)",
79 self.diag[i] * 1e6,
80 dv
81 ));
82 }
83 } else {
84 for i in 0..A::dim() {
85 fmt_cov.push(format!("{:.1e}", self.diag[i] * 1e6));
86 }
87 }
88 write!(
89 f,
90 "SNC: diag({}) mm/s^2 {} {}",
91 fmt_cov.join(", "),
92 if let Some(lf) = self.local_frame {
93 format!("in {lf:?}")
94 } else {
95 "".to_string()
96 },
97 if let Some(start) = self.start_time {
98 format!("starting at {start}")
99 } else {
100 "".to_string()
101 }
102 )
103 }
104}
105
106impl<A> fmt::Display for ProcessNoise<A>
107where
108 A: DimName,
109 DefaultAllocator: Allocator<A> + Allocator<A, A>,
110{
111 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
112 write!(f, "{self:?}")
113 }
114}
115
116impl<A: DimName> ProcessNoise<A>
117where
118 DefaultAllocator: Allocator<A> + Allocator<A, A>,
119{
120 pub fn from_diagonal(disable_time: Duration, values: &[f64]) -> Self {
122 assert_eq!(
123 values.len(),
124 A::dim(),
125 "Not enough values for the size of the SNC matrix"
126 );
127
128 let mut diag = OVector::zeros();
129 for (i, v) in values.iter().enumerate() {
130 diag[i] = *v;
131 }
132
133 Self {
134 diag,
135 disable_time,
136 start_time: None,
137 local_frame: None,
138 decay_diag: None,
139 init_epoch: None,
140 prev_epoch: None,
141 }
142 }
143
144 pub fn with_start_time(disable_time: Duration, values: &[f64], start_time: Epoch) -> Self {
146 let mut me = Self::from_diagonal(disable_time, values);
147 me.start_time = Some(start_time);
148 me
149 }
150
151 pub fn with_decay(
154 disable_time: Duration,
155 initial_snc: &[f64],
156 decay_constants_s: &[f64],
157 ) -> Self {
158 assert_eq!(
159 decay_constants_s.len(),
160 A::dim(),
161 "Not enough decay constants for the size of the SNC matrix"
162 );
163
164 let mut me = Self::from_diagonal(disable_time, initial_snc);
165 me.decay_diag = Some(decay_constants_s.to_vec());
166 me
167 }
168
169 pub fn to_matrix(&self, epoch: Epoch) -> Option<OMatrix<f64, A, A>> {
174 if let Some(start_time) = self.start_time {
175 if start_time > epoch {
176 debug!("@{epoch} SNC starts at {start_time}");
178 return None;
179 }
180 }
181
182 if let Some(prev_epoch) = self.prev_epoch {
184 if epoch - prev_epoch > self.disable_time {
185 debug!(
186 "@{} SNC disabled: prior use greater than {}",
187 epoch, self.disable_time
188 );
189 return None;
190 }
191 }
192 let mut snc = OMatrix::<f64, A, A>::zeros();
194 for i in 0..self.diag.nrows() {
195 snc[(i, i)] = self.diag[i];
196 }
197
198 if let Some(decay) = &self.decay_diag {
199 let total_delta_t = (epoch - self.init_epoch.unwrap()).to_seconds();
201 for i in 0..self.diag.nrows() {
202 snc[(i, i)] *= (-decay[i] * total_delta_t).exp();
203 }
204 }
205
206 debug!(
207 "@{} SNC diag {:?}",
208 epoch,
209 snc.diagonal().iter().copied().collect::<Vec<f64>>()
210 );
211
212 Some(snc)
213 }
214
215 pub fn propagate<S: DimName>(
216 &self,
217 nominal_orbit: Orbit,
218 delta_t: Duration,
219 ) -> Result<Option<OMatrix<f64, S, S>>, ODError>
220 where
221 DefaultAllocator: Allocator<S> + Allocator<S, S> + Allocator<S, A> + Allocator<A, S>,
222 {
223 if let Some(mut snc_matrix) = self.to_matrix(nominal_orbit.epoch) {
224 if let Some(local_frame) = self.local_frame {
225 let dcm = local_frame
227 .dcm_to_inertial(nominal_orbit)
228 .context(AstroPhysicsSnafu)
229 .context(StateAstroSnafu {
230 param: StateParameter::Epoch(),
231 })
232 .context(ODStateSnafu {
233 action: "rotating SNC from definition frame into state frame",
234 })?;
235
236 match A::DIM {
238 3 => {
239 let new_snc = dcm.rot_mat
240 * snc_matrix.fixed_view::<3, 3>(0, 0)
241 * dcm.rot_mat.transpose();
242 for i in 0..A::DIM {
243 snc_matrix[(i, i)] = new_snc[(i, i)];
244 }
245 }
246 6 => {
247 let new_snc = dcm.state_dcm()
248 * snc_matrix.fixed_view::<6, 6>(0, 0)
249 * dcm.transpose().state_dcm();
250 for i in 0..A::DIM {
251 snc_matrix[(i, i)] = new_snc[(i, i)];
252 }
253 }
254 _ => {
255 return Err(ODError::ODLimitation {
256 action: "only process noises of size 3x3 or 6x6 are supported",
257 })
258 }
259 }
260 }
261
262 let mut gamma = OMatrix::<f64, S, A>::zeros();
265 let delta_t = delta_t.to_seconds();
266 for blk in 0..A::dim() / 3 {
267 for i in 0..3 {
268 let idx_i = i + A::dim() * blk;
269 let idx_j = i + 3 * blk;
270 let idx_k = i + 3 + A::dim() * blk;
271 gamma[(idx_i, idx_j)] = delta_t.powi(2) / 2.0;
286 gamma[(idx_k, idx_j)] = delta_t;
287 }
288 }
289 Ok(Some(&gamma * snc_matrix * &gamma.transpose()))
290 } else {
291 Ok(None)
292 }
293 }
294}
295
296impl ProcessNoise3D {
297 pub fn from_velocity_km_s(
299 velocity_noise: &[f64; 3],
300 noise_duration: Duration,
301 disable_time: Duration,
302 local_frame: Option<LocalFrame>,
303 ) -> Self {
304 let mut diag = OVector::<f64, U3>::zeros();
305 for (i, v) in velocity_noise.iter().enumerate() {
306 diag[i] = *v / noise_duration.to_seconds();
307 }
308
309 Self {
310 diag,
311 disable_time,
312 start_time: None,
313 local_frame,
314 decay_diag: None,
315 init_epoch: None,
316 prev_epoch: None,
317 }
318 }
319}
320
321#[test]
322fn test_snc_init() {
323 use crate::time::Unit;
324 let snc_expo = ProcessNoise3D::with_decay(
325 2 * Unit::Minute,
326 &[1e-6, 1e-6, 1e-6],
327 &[3600.0, 3600.0, 3600.0],
328 );
329 println!("{snc_expo}");
330
331 let snc_std = ProcessNoise3D::with_start_time(
332 2 * Unit::Minute,
333 &[1e-6, 1e-6, 1e-6],
334 Epoch::from_et_seconds(3600.0),
335 );
336 println!("{snc_std}");
337
338 let snc_vel = ProcessNoise3D::from_velocity_km_s(
339 &[1e-2, 1e-2, 1e-2],
340 Unit::Hour * 2,
341 Unit::Minute * 2,
342 Some(LocalFrame::RIC),
343 );
344
345 let diag_val = 1e-2 / (Unit::Hour * 2).to_seconds();
346 assert_eq!(
347 snc_vel
348 .to_matrix(Epoch::from_et_seconds(3600.0))
349 .unwrap()
350 .diagonal(),
351 OVector::<f64, U3>::new(diag_val, diag_val, diag_val)
352 );
353 assert_eq!(snc_vel.local_frame, Some(LocalFrame::RIC));
354}