nyx_space/od/process/solution/smooth.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::linalg::allocator::Allocator;
20use crate::linalg::{DefaultAllocator, DimName};
21use crate::md::trajectory::Interpolatable;
22pub use crate::od::estimate::*;
23pub use crate::od::*;
24use anise::prelude::Almanac;
25use log::info;
26use msr::sensitivity::TrackerSensitivity;
27use nalgebra::DimMin;
28use std::ops::Add;
29
30use super::ODSolution;
31
32impl<StateType, EstType, MsrSize, Trk> ODSolution<StateType, EstType, MsrSize, Trk>
33where
34 StateType: Interpolatable + Add<OVector<f64, <StateType as State>::Size>, Output = StateType>,
35 EstType: Estimate<StateType>,
36 MsrSize: DimName,
37 Trk: TrackerSensitivity<StateType, StateType>,
38 <StateType as State>::Size: DimMin<<StateType as State>::Size>,
39 <DefaultAllocator as Allocator<<StateType as State>::VecLength>>::Buffer<f64>: Send,
40 DefaultAllocator: Allocator<<StateType as State>::Size>
41 + Allocator<<StateType as State>::VecLength>
42 + Allocator<MsrSize>
43 + Allocator<MsrSize, <StateType as State>::Size>
44 + Allocator<MsrSize, MsrSize>
45 + Allocator<<StateType as State>::Size, <StateType as State>::Size>
46 + Allocator<<StateType as State>::Size, MsrSize>
47 + Allocator<<<StateType as State>::Size as DimMin<<StateType as State>::Size>>::Output>,
48{
49 /// Smoothes this OD solution, returning a new OD solution and the filter-smoother consistency ratios, with updated **postfit** residuals, and where the ratio now represents the filter-smoother consistency ratio.
50 ///
51 /// Notes:
52 /// 1. Gains will be scrubbed because the smoother process does not recompute the gain.
53 /// 2. Prefit residuals, ratios, and measurement covariances are not updated, as these depend on the filtering process.
54 /// 3. Note: this function consumes the current OD solution to prevent reusing the wrong one.
55 ///
56 ///
57 /// To assess whether the smoothing process improved the solution, compare the RMS of the postfit residuals from the filter and the smoother process.
58 ///
59 /// # Filter-Smoother consistency ratio
60 ///
61 /// The **filter-smoother consistency ratio** is used to evaluate the consistency between the state estimates produced by a filter (e.g., Kalman filter) and a smoother.
62 /// This ratio is called "filter smoother consistency test" in the ODTK MathSpec.
63 ///
64 /// It is computed as follows:
65 ///
66 /// #### 1. Define the State Estimates
67 /// **Filter state estimate**:
68 /// $ \hat{X}_{f,k} $
69 /// This is the state estimate at time step $ k $ from the filter.
70 ///
71 /// **Smoother state estimate**:
72 /// $ \hat{X}_{s,k} $
73 /// This is the state estimate at time step $ k $ from the smoother.
74 ///
75 /// #### 2. Define the Covariances
76 ///
77 /// **Filter covariance**:
78 /// $ P_{f,k} $
79 /// This is the covariance of the state estimate at time step $ k $ from the filter.
80 ///
81 /// **Smoother covariance**:
82 /// $ P_{s,k} $
83 /// This is the covariance of the state estimate at time step $ k $ from the smoother.
84 ///
85 /// #### 3. Compute the Differences
86 ///
87 /// **State difference**:
88 /// $ \Delta X_k = \hat{X}_{s,k} - \hat{X}_{f,k} $
89 ///
90 /// **Covariance difference**:
91 /// $ \Delta P_k = P_{s,k} - P_{f,k} $
92 ///
93 /// #### 4. Calculate the Consistency Ratio
94 /// For each element $ i $ of the state vector, compute the ratio:
95 ///
96 /// $$
97 /// R_{i,k} = \frac{\Delta X_{i,k}}{\sqrt{\Delta P_{i,k}}}
98 /// $$
99 ///
100 /// #### 5. Evaluate Consistency
101 /// - If $ |R_{i,k}| \leq 3 $ for all $ i $ and $ k $, the filter-smoother consistency test is satisfied, indicating good consistency.
102 /// - If $ |R_{i,k}| > 3 $ for any $ i $ or $ k $, the test fails, suggesting potential modeling inconsistencies or issues with the estimation process.
103 ///
104 pub fn smooth(self, almanac: &Almanac) -> Result<Self, ODError>
105 where
106 <StateType as State>::Size:
107 DimMin<<StateType as State>::Size, Output = <StateType as State>::Size>,
108 {
109 let l = self.estimates.len() - 1;
110
111 let mut smoothed = Self {
112 estimates: Vec::with_capacity(self.estimates.len()),
113 residuals: Vec::with_capacity(self.residuals.len()),
114 gains: Vec::with_capacity(self.estimates.len()),
115 filter_smoother_ratios: Vec::with_capacity(self.estimates.len()),
116 devices: self.devices.clone(),
117 measurement_types: self.measurement_types.clone(),
118 };
119
120 // Set the first item of the smoothed estimates to the last estimate (we cannot smooth the very last estimate)
121 smoothed
122 .estimates
123 .push(self.estimates.last().unwrap().clone());
124 smoothed
125 .residuals
126 .push(self.residuals.last().unwrap().clone());
127 smoothed.gains.push(None);
128 smoothed.filter_smoother_ratios.push(None);
129
130 loop {
131 let k = l - smoothed.estimates.len();
132 // Borrow the previously smoothed estimate of the k+1 estimate
133 let sm_est_kp1 = &self.estimates[k + 1];
134 let x_kp1_l = sm_est_kp1.state_deviation();
135 let p_kp1_l = sm_est_kp1.covar();
136 // Borrow the k-th estimate, which we're smoothing with the next estimate
137 let est_k = &self.estimates[k];
138 // Borrow the k+1-th estimate, which we're smoothing with the next estimate
139 let est_kp1 = &self.estimates[k + 1];
140
141 // Compute the STM between both steps taken by the filter
142 // The filter will reset the STM between each estimate it computes, time update or measurement update.
143 // Therefore, the STM is simply the inverse of the one we used previously.
144 // est_kp1 is the estimate that used the STM from time k to time k+1. So the STM stored there
145 // is \Phi_{k \to k+1}.
146 // We invert via LU decomposition because the STM is not PSD, so neither Cholesky nor UDU work.
147 let phi_kp1_k = est_kp1
148 .stm()
149 .clone()
150 .lu()
151 .try_inverse()
152 .ok_or(ODError::SingularStateTransitionMatrix)?;
153
154 // Compute smoothed state deviation
155 let x_k_l = &phi_kp1_k * x_kp1_l;
156 // Compute smoothed covariance
157 let p_k_l = &phi_kp1_k * p_kp1_l * &phi_kp1_k.transpose();
158 // Store into vector
159 let mut smoothed_est_k = est_k.clone();
160 // Compute the smoothed state deviation
161 smoothed_est_k.set_state_deviation(x_k_l);
162 // Compute the smoothed covariance
163 smoothed_est_k.set_covar(p_k_l);
164 // Recompute the residual if available.
165 if let Some(mut residual) = self.residuals[k + 1].clone() {
166 let tracker = residual
167 .tracker
168 .as_ref()
169 .expect("tracker unset in smoother process");
170
171 let device = smoothed
172 .devices
173 .get_mut(tracker)
174 .expect("unknown tracker in smoother process");
175
176 let new_state_est = smoothed_est_k.state();
177 let epoch = new_state_est.epoch();
178
179 if let Some(computed_meas) =
180 device.measure_instantaneous(new_state_est, None, almanac)?
181 {
182 // Only recompute the computed observation from the update state estimate.
183 residual.computed_obs = computed_meas
184 .observation::<MsrSize>(&residual.msr_types)
185 - device.measurement_bias_vector::<MsrSize>(&residual.msr_types, epoch)?;
186
187 // Update the postfit residual.
188 residual.postfit = &residual.real_obs - &residual.computed_obs;
189
190 // Store the updated data.
191 smoothed.residuals.push(Some(residual));
192 } else {
193 smoothed.residuals.push(None);
194 }
195 } else {
196 smoothed.residuals.push(None);
197 }
198
199 // Compute the filter-smoother consistency ratio.
200 let delta_covar = est_k.covar() - smoothed_est_k.covar();
201 let delta_state =
202 est_k.state().to_state_vector() - smoothed_est_k.state().to_state_vector();
203
204 let fs_ratios = OVector::<f64, <StateType as State>::Size>::from_iterator(
205 delta_state
206 .iter()
207 .enumerate()
208 .map(|(i, dx)| dx / delta_covar[(i, i)].sqrt()),
209 );
210
211 smoothed.estimates.push(smoothed_est_k);
212 smoothed.filter_smoother_ratios.push(Some(fs_ratios));
213 // Set all gains to None.
214 smoothed.gains.push(None);
215
216 if smoothed.estimates.len() == self.estimates.len() {
217 break;
218 }
219 }
220
221 // Note that we have yet to reverse the list, so we print them backward
222 info!(
223 "Smoothed {} estimates (from {} to {})",
224 smoothed.estimates.len(),
225 smoothed.estimates.last().unwrap().epoch(),
226 smoothed.estimates[0].epoch(),
227 );
228
229 // Now, let's add all of the other estimates so that the same indexing can be done
230 // between all the estimates and the smoothed estimates
231 if smoothed.estimates.len() < self.estimates.len() {
232 // Add the estimates that might have been skipped.
233 let mut k = self.estimates.len() - smoothed.estimates.len();
234 loop {
235 smoothed.estimates.push(self.estimates[k].clone());
236 if k == 0 {
237 break;
238 }
239 k -= 1;
240 }
241 }
242
243 // And reverse to maintain the order of estimates
244 smoothed.estimates.reverse();
245 smoothed.residuals.reverse();
246 smoothed.filter_smoother_ratios.reverse();
247
248 Ok(smoothed)
249 }
250}