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#[derive(Clone, PartialEq)]
39#[allow(clippy::upper_case_acronyms)]
40pub struct ProcessNoise<A: DimName>
41where
42 DefaultAllocator: Allocator<A> + Allocator<A, A>,
43{
44 pub start_time: Option<Epoch>,
46 pub local_frame: Option<LocalFrame>,
48 pub disable_time: Duration,
50 pub init_epoch: Option<Epoch>,
52 diag: OVector<f64, A>,
53 pub decay_diag: Option<Vec<f64>>,
54 pub prev_epoch: Option<Epoch>,
56}
57
58impl<A> fmt::Debug for ProcessNoise<A>
59where
60 A: DimName,
61 DefaultAllocator: Allocator<A> + Allocator<A, A>,
62{
63 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
64 let mut fmt_cov = Vec::with_capacity(A::dim());
65 if let Some(decay) = &self.decay_diag {
66 for (i, dv) in decay.iter().enumerate() {
67 fmt_cov.push(format!(
68 "{:.1e} × exp(- {:.1e} × t)",
69 self.diag[i] * 1e6,
70 dv
71 ));
72 }
73 } else {
74 for i in 0..A::dim() {
75 fmt_cov.push(format!("{:.1e}", self.diag[i] * 1e6));
76 }
77 }
78 write!(
79 f,
80 "Process noise: diag({}) mm/s^2 {} {}",
81 fmt_cov.join(", "),
82 if let Some(lf) = self.local_frame {
83 format!("in {lf:?}")
84 } else {
85 "".to_string()
86 },
87 if let Some(start) = self.start_time {
88 format!("starting at {start}")
89 } else {
90 "".to_string()
91 }
92 )
93 }
94}
95
96impl<A> fmt::Display for ProcessNoise<A>
97where
98 A: DimName,
99 DefaultAllocator: Allocator<A> + Allocator<A, A>,
100{
101 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
102 write!(f, "{self:?}")
103 }
104}
105
106impl<A: DimName> ProcessNoise<A>
107where
108 DefaultAllocator: Allocator<A> + Allocator<A, A>,
109{
110 pub fn from_diagonal(
112 values: &[f64],
113 disable_time: Duration,
114 local_frame: Option<LocalFrame>,
115 ) -> Self {
116 assert_eq!(
117 values.len(),
118 A::dim(),
119 "Not enough values for the size of the SNC matrix"
120 );
121
122 let mut diag = OVector::zeros();
123 for (i, v) in values.iter().enumerate() {
124 diag[i] = *v;
125 }
126
127 Self {
128 diag,
129 disable_time,
130 start_time: None,
131 local_frame,
132 decay_diag: None,
133 init_epoch: None,
134 prev_epoch: None,
135 }
136 }
137
138 pub fn with_start_time(disable_time: Duration, values: &[f64], start_time: Epoch) -> Self {
140 let mut me = Self::from_diagonal(values, disable_time, None);
141 me.start_time = Some(start_time);
142 me
143 }
144
145 pub fn with_decay(
148 values: &[f64],
149 disable_time: Duration,
150 decay_constants_s: &[f64],
151 local_frame: Option<LocalFrame>,
152 ) -> Self {
153 assert_eq!(
154 decay_constants_s.len(),
155 A::dim(),
156 "Not enough decay constants for the size of the SNC matrix"
157 );
158
159 let mut me = Self::from_diagonal(values, disable_time, local_frame);
160 me.decay_diag = Some(decay_constants_s.to_vec());
161 me
162 }
163
164 pub fn to_matrix(&self, epoch: Epoch) -> Option<OMatrix<f64, A, A>> {
169 if let Some(start_time) = self.start_time
170 && start_time > epoch
171 {
172 debug!("@{epoch} SNC starts at {start_time}");
174 return None;
175 }
176
177 if let Some(prev_epoch) = self.prev_epoch
179 && epoch - prev_epoch > self.disable_time
180 {
181 debug!(
182 "@{} SNC disabled: prior use greater than {}",
183 epoch, self.disable_time
184 );
185 return None;
186 }
187 let mut snc = OMatrix::<f64, A, A>::zeros();
189 for i in 0..self.diag.nrows() {
190 snc[(i, i)] = self.diag[i];
191 }
192
193 if let Some(decay) = &self.decay_diag {
194 let total_delta_t = (epoch - self.init_epoch.unwrap()).to_seconds();
196 for i in 0..self.diag.nrows() {
197 snc[(i, i)] *= (-decay[i] * total_delta_t).exp();
198 }
199 }
200
201 debug!(
202 "@{} SNC diag {:?}",
203 epoch,
204 snc.diagonal().iter().copied().collect::<Vec<f64>>()
205 );
206
207 Some(snc)
208 }
209
210 pub fn propagate<S: DimName>(
211 &self,
212 nominal_orbit: Orbit,
213 delta_t: Duration,
214 ) -> Result<Option<OMatrix<f64, S, S>>, ODError>
215 where
216 DefaultAllocator: Allocator<S> + Allocator<S, S> + Allocator<S, A> + Allocator<A, S>,
217 {
218 if let Some(mut snc_matrix) = self.to_matrix(nominal_orbit.epoch) {
219 if let Some(local_frame) = self.local_frame {
220 let dcm = nominal_orbit
222 .dcm_to_inertial(local_frame)
223 .context(AstroPhysicsSnafu)
224 .context(StateAstroSnafu {
225 param: StateParameter::Epoch(),
226 })
227 .context(ODStateSnafu {
228 action: "rotating SNC from definition frame into state frame",
229 })?;
230
231 match A::DIM {
233 3 => {
234 let new_snc = dcm.rot_mat
235 * snc_matrix.fixed_view::<3, 3>(0, 0)
236 * dcm.rot_mat.transpose();
237 for i in 0..A::DIM {
238 snc_matrix[(i, i)] = new_snc[(i, i)];
239 }
240 }
241 6 => {
242 let new_snc = dcm.state_dcm()
243 * snc_matrix.fixed_view::<6, 6>(0, 0)
244 * dcm.transpose().state_dcm();
245 for i in 0..A::DIM {
246 snc_matrix[(i, i)] = new_snc[(i, i)];
247 }
248 }
249 _ => {
250 return Err(ODError::ODLimitation {
251 action: "only process noises of size 3x3 or 6x6 are supported",
252 });
253 }
254 }
255 }
256
257 if delta_t > self.disable_time {
258 return Ok(None);
259 }
260
261 let mut gamma = OMatrix::<f64, S, A>::zeros();
264 let delta_t = delta_t.to_seconds();
265 for blk in 0..A::dim() / 3 {
266 for i in 0..3 {
267 let idx_i = i + A::dim() * blk;
268 let idx_j = i + 3 * blk;
269 let idx_k = i + 3 + A::dim() * blk;
270 gamma[(idx_i, idx_j)] = delta_t.powi(2) / 2.0;
285 gamma[(idx_k, idx_j)] = delta_t;
286 }
287 }
288 Ok(Some(&gamma * snc_matrix * &gamma.transpose()))
289 } else {
290 Ok(None)
291 }
292 }
293}
294
295impl ProcessNoise3D {
296 pub fn from_velocity_km_s(
298 velocity_noise: &[f64; 3],
299 noise_duration: Duration,
300 disable_time: Duration,
301 local_frame: Option<LocalFrame>,
302 ) -> Self {
303 let mut diag = OVector::<f64, U3>::zeros();
304 for (i, v) in velocity_noise.iter().enumerate() {
305 diag[i] = *v / noise_duration.to_seconds();
306 }
307
308 Self {
309 diag,
310 disable_time,
311 start_time: None,
312 local_frame,
313 decay_diag: None,
314 init_epoch: None,
315 prev_epoch: None,
316 }
317 }
318}
319
320#[test]
321fn test_snc_init() {
322 use crate::time::Unit;
323 let snc_expo = ProcessNoise3D::with_decay(
324 &[1e-6, 1e-6, 1e-6],
325 2 * Unit::Minute,
326 &[3600.0, 3600.0, 3600.0],
327 None,
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}