Skip to main content

nyx_space/od/
snc.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 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    /// Time at which this SNC starts to become applicable
55    pub start_time: Option<Epoch>,
56    /// Specify the local frame of this SNC
57    pub local_frame: Option<LocalFrame>,
58    /// Enables state noise compensation (process noise) only be applied if the time between measurements is less than the disable_time
59    pub disable_time: Duration,
60    // Stores the initial epoch when the SNC is requested, needed for decay. Kalman filter will edit this automatically.
61    pub init_epoch: Option<Epoch>,
62    diag: OVector<f64, A>,
63    decay_diag: Option<Vec<f64>>,
64    // Stores the previous epoch of the SNC request, needed for disable time
65    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    /// Initialize a state noise compensation structure from the diagonal values
121    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    /// Initialize an SNC with a time at which it should start
145    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    /// Initialize an exponentially decaying SNC with initial SNC and decay constants.
152    /// Decay constants in seconds since start of the tracking pass.
153    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    /// Returns the SNC matrix (_not_ incl. Gamma matrix approximation) at the provided Epoch.
170    /// May be None if:
171    ///  1. Start time of this matrix is _after_ epoch
172    ///  2. Time between epoch and previous epoch (set in the Kalman filter!) is longer than disabling time
173    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                // This SNC applies only later
177                debug!("@{epoch} SNC starts at {start_time}");
178                return None;
179            }
180        }
181
182        // Check the disable time, and return no SNC if the previous SNC was computed too long ago
183        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        // Build a static matrix
193        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's apply the decay to the diagonals
200            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                // Rotate the SNC from the definition frame into the state frame.
226                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                // Note: the SNC must be a diagonal matrix, so we only update the diagonals!
237                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's compute the Gamma matrix, an approximation of the time integral
263            // which assumes that the acceleration is constant between these two measurements.
264            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                    // For first block
272                    // (0, 0) (1, 1) (2, 2) <=> \Delta t^2/2
273                    // (3, 0) (4, 1) (5, 2) <=> \Delta t
274                    // Second block
275                    // (6, 3) (7, 4) (8, 5) <=> \Delta t^2/2
276                    // (9, 3) (10, 4) (11, 5) <=> \Delta t
277                    // * \Delta t^2/2
278                    // (i, i) when blk = 0
279                    // (i + A::dim() * blk, i + 3) when blk = 1
280                    // (i + A::dim() * blk, i + 3 * blk)
281                    // * \Delta t
282                    // (i + 3, i) when blk = 0
283                    // (i + 3, i + 9) when blk = 1 (and I think i + 12 + 3)
284                    // (i + 3 + A::dim() * blk, i + 3 * blk)
285                    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    /// Initialize the process noise from velocity errors over time
298    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}