Skip to main content

nyx_space/od/estimate/
kfestimate.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::{Estimate, State};
20use crate::cosmic::{AstroError, AstroPhysicsSnafu};
21use crate::linalg::allocator::Allocator;
22use crate::linalg::{DefaultAllocator, DimName, Matrix, OMatrix, OVector};
23use crate::mc::{MvnSpacecraft, StateDispersion};
24use crate::{NyxError, Spacecraft};
25use anise::analysis::prelude::OrbitalElement;
26use anise::astro::orbit_gradient::OrbitGrad;
27use nalgebra::Const;
28use nalgebra::SMatrix;
29use rand::SeedableRng;
30use rand::rngs::SysRng;
31use rand_distr::Distribution;
32use rand_pcg::Pcg64Mcg;
33use snafu::ResultExt;
34use std::cmp::PartialEq;
35use std::error::Error;
36use std::fmt;
37use std::ops::Mul;
38
39/// Kalman filter Estimate
40#[derive(Debug, Copy, Clone, PartialEq)]
41pub struct KfEstimate<T: State>
42where
43    DefaultAllocator: Allocator<<T as State>::Size>
44        + Allocator<<T as State>::Size, <T as State>::Size>
45        + Allocator<<T as State>::Size>
46        + Allocator<<T as State>::VecLength>,
47    <DefaultAllocator as Allocator<<T as State>::Size>>::Buffer<f64>: Copy,
48    <DefaultAllocator as Allocator<<T as State>::Size, <T as State>::Size>>::Buffer<f64>: Copy,
49{
50    /// The estimated state
51    pub nominal_state: T,
52    /// The state deviation
53    pub state_deviation: OVector<f64, <T as State>::Size>,
54    /// The Covariance of this estimate
55    pub covar: OMatrix<f64, <T as State>::Size, <T as State>::Size>,
56    /// The predicted covariance of this estimate
57    pub covar_bar: OMatrix<f64, <T as State>::Size, <T as State>::Size>,
58    /// Whether or not this is a predicted estimate from a time update, or an estimate from a measurement
59    pub predicted: bool,
60    /// The STM used to compute this Estimate
61    pub stm: OMatrix<f64, <T as State>::Size, <T as State>::Size>,
62}
63
64impl<T: State> KfEstimate<T>
65where
66    DefaultAllocator: Allocator<<T as State>::Size>
67        + Allocator<<T as State>::Size, <T as State>::Size>
68        + Allocator<<T as State>::Size>
69        + Allocator<<T as State>::VecLength>,
70    <DefaultAllocator as Allocator<<T as State>::Size>>::Buffer<f64>: Copy,
71    <DefaultAllocator as Allocator<<T as State>::Size, <T as State>::Size>>::Buffer<f64>: Copy,
72{
73    /// Initializes a new filter estimate from the nominal state (not dispersed) and the full covariance
74    pub fn from_covar(
75        nominal_state: T,
76        covar: OMatrix<f64, <T as State>::Size, <T as State>::Size>,
77    ) -> Self {
78        Self {
79            nominal_state,
80            state_deviation: OVector::<f64, <T as State>::Size>::zeros(),
81            covar,
82            covar_bar: covar,
83            predicted: true,
84            stm: OMatrix::<f64, <T as State>::Size, <T as State>::Size>::identity(),
85        }
86    }
87
88    /// Initializes a new filter estimate from the nominal state (not dispersed) and the diagonal of the covariance
89    pub fn from_diag(nominal_state: T, diag: OVector<f64, <T as State>::Size>) -> Self {
90        let covar = Matrix::from_diagonal(&diag);
91        Self {
92            nominal_state,
93            state_deviation: OVector::<f64, <T as State>::Size>::zeros(),
94            covar,
95            covar_bar: covar,
96            predicted: true,
97            stm: OMatrix::<f64, <T as State>::Size, <T as State>::Size>::identity(),
98        }
99    }
100}
101
102impl KfEstimate<Spacecraft> {
103    /// Generates an initial Kalman filter state estimate dispersed from the nominal state using the provided standard deviation parameters.
104    /// The resulting estimate will have a diagonal covariance matrix constructed from the variances of each parameter.
105    /// The state of this estimate includes dispersions; the nominal state is left unchanged;
106    /// the dispersions are set in the state_deviation field.
107    /// XXX: Will this cause the filters in the tests to converge more quickly?!
108    pub fn from_dispersions(
109        nominal_state: Spacecraft,
110        dispersions: Vec<StateDispersion>,
111        seed: Option<u128>,
112    ) -> Result<Self, Box<dyn Error>> {
113        let generator = MvnSpacecraft::new(nominal_state, dispersions)?;
114
115        let mut rng = match seed {
116            Some(seed) => Pcg64Mcg::new(seed),
117            None => Pcg64Mcg::try_from_rng(&mut SysRng).unwrap(),
118        };
119        let dispersed_state = generator.sample(&mut rng);
120
121        // Compute the difference between both states
122        let delta_orbit = (nominal_state.orbit - dispersed_state.state.orbit).unwrap();
123
124        let state_deviation = [
125            delta_orbit.radius_km.x,
126            delta_orbit.radius_km.y,
127            delta_orbit.radius_km.z,
128            delta_orbit.velocity_km_s.x,
129            delta_orbit.velocity_km_s.y,
130            delta_orbit.velocity_km_s.z,
131            (nominal_state.srp.coeff_reflectivity - dispersed_state.state.srp.coeff_reflectivity),
132            (nominal_state.drag.coeff_drag - dispersed_state.state.drag.coeff_drag),
133            (nominal_state.mass.prop_mass_kg - dispersed_state.state.mass.prop_mass_kg),
134        ];
135
136        // Build the covariance as three times the absolute value of the error, squared.
137        let diag_data = state_deviation
138            .iter()
139            .map(|v| (3.0 * v.abs()).powi(2))
140            .collect::<Vec<f64>>();
141
142        let diag = OVector::<f64, Const<9>>::from_iterator(diag_data);
143
144        // Build the covar from the diagonal
145        let covar = Matrix::from_diagonal(&diag);
146
147        Ok(Self {
148            nominal_state, //: dispersed_state.state,
149            state_deviation: OVector::<f64, Const<9>>::from_iterator(state_deviation),
150            covar,
151            covar_bar: covar,
152            predicted: true,
153            stm: OMatrix::<f64, Const<9>, Const<9>>::identity(),
154        })
155    }
156
157    /// Builds a multivariate random variable spacecraft from this estimate's nominal state and covariance, zero mean.
158    pub fn to_random_variable(&self) -> Result<MvnSpacecraft, Box<NyxError>> {
159        MvnSpacecraft::from_spacecraft_cov(self.nominal_state, self.covar, self.state_deviation)
160    }
161
162    /// Returns the 1-sigma uncertainty for a given parameter, in that parameter's unit
163    ///
164    /// This method uses the [OrbitDual] structure to compute the estimate in the hyperdual space
165    /// and rotate the nominal covariance into that space.
166    pub fn sigma_for(&self, param: OrbitalElement) -> Result<f64, AstroError> {
167        // Build the rotation matrix using Orbit Dual.
168        let mut rotmat = SMatrix::<f64, 1, 6>::zeros();
169        let orbit_dual = OrbitGrad::from(self.nominal_state.orbit);
170
171        let xf_partial = orbit_dual.partial_for(param).context(AstroPhysicsSnafu)?;
172        for (cno, val) in [
173            xf_partial.wrt_x(),
174            xf_partial.wrt_y(),
175            xf_partial.wrt_z(),
176            xf_partial.wrt_vx(),
177            xf_partial.wrt_vy(),
178            xf_partial.wrt_vz(),
179        ]
180        .iter()
181        .copied()
182        .enumerate()
183        {
184            rotmat[(0, cno)] = val;
185        }
186
187        Ok((rotmat * self.covar.fixed_view::<6, 6>(0, 0) * rotmat.transpose())[(0, 0)].sqrt())
188    }
189
190    /// Returns the 6x6 covariance (i.e. square of the sigma/uncertainty) of the SMA, ECC, INC, RAAN, AOP, and True Anomaly.
191    pub fn keplerian_covar(&self) -> SMatrix<f64, 6, 6> {
192        // Build the rotation matrix using Orbit Dual.
193        let mut rotmat = SMatrix::<f64, 6, 6>::zeros();
194        let orbit_dual = OrbitGrad::from(self.nominal_state.orbit);
195        for (pno, param) in [
196            OrbitalElement::SemiMajorAxis,
197            OrbitalElement::Eccentricity,
198            OrbitalElement::Inclination,
199            OrbitalElement::RAAN,
200            OrbitalElement::AoP,
201            OrbitalElement::TrueAnomaly,
202        ]
203        .iter()
204        .copied()
205        .enumerate()
206        {
207            let xf_partial = orbit_dual.partial_for(param).unwrap();
208            for (cno, val) in [
209                xf_partial.wrt_x(),
210                xf_partial.wrt_y(),
211                xf_partial.wrt_z(),
212                xf_partial.wrt_vx(),
213                xf_partial.wrt_vy(),
214                xf_partial.wrt_vz(),
215            ]
216            .iter()
217            .copied()
218            .enumerate()
219            {
220                rotmat[(pno, cno)] = val;
221            }
222        }
223
224        rotmat * self.covar.fixed_view::<6, 6>(0, 0) * rotmat.transpose()
225    }
226}
227
228impl<T: State> Estimate<T> for KfEstimate<T>
229where
230    DefaultAllocator: Allocator<<T as State>::Size>
231        + Allocator<<T as State>::Size, <T as State>::Size>
232        + Allocator<<T as State>::Size>
233        + Allocator<<T as State>::VecLength>,
234    <DefaultAllocator as Allocator<<T as State>::Size>>::Buffer<f64>: Copy,
235    <DefaultAllocator as Allocator<<T as State>::Size, <T as State>::Size>>::Buffer<f64>: Copy,
236{
237    fn zeros(nominal_state: T) -> Self {
238        Self {
239            nominal_state,
240            state_deviation: OVector::<f64, <T as State>::Size>::zeros(),
241            covar: OMatrix::<f64, <T as State>::Size, <T as State>::Size>::zeros(),
242            covar_bar: OMatrix::<f64, <T as State>::Size, <T as State>::Size>::zeros(),
243            predicted: true,
244            stm: OMatrix::<f64, <T as State>::Size, <T as State>::Size>::identity(),
245        }
246    }
247
248    fn nominal_state(&self) -> T {
249        self.nominal_state
250    }
251
252    fn state_deviation(&self) -> OVector<f64, <T as State>::Size> {
253        self.state_deviation
254    }
255
256    fn covar(&self) -> OMatrix<f64, <T as State>::Size, <T as State>::Size> {
257        self.covar
258    }
259
260    fn predicted_covar(&self) -> OMatrix<f64, <T as State>::Size, <T as State>::Size> {
261        self.covar_bar
262    }
263
264    fn predicted(&self) -> bool {
265        self.predicted
266    }
267    fn stm(&self) -> &OMatrix<f64, <T as State>::Size, <T as State>::Size> {
268        &self.stm
269    }
270    fn set_state_deviation(&mut self, new_state: OVector<f64, <T as State>::Size>) {
271        self.state_deviation = new_state;
272    }
273    fn set_covar(&mut self, new_covar: OMatrix<f64, <T as State>::Size, <T as State>::Size>) {
274        self.covar = new_covar;
275    }
276}
277
278impl<T: State> fmt::Display for KfEstimate<T>
279where
280    DefaultAllocator: Allocator<<T as State>::Size>
281        + Allocator<<T as State>::Size, <T as State>::Size>
282        + Allocator<<T as State>::Size>
283        + Allocator<<T as State>::VecLength>,
284    <DefaultAllocator as Allocator<<T as State>::Size>>::Buffer<f64>: Copy,
285    <DefaultAllocator as Allocator<<T as State>::Size, <T as State>::Size>>::Buffer<f64>: Copy,
286{
287    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
288        let dim = <T as State>::Size::dim();
289        let word = if self.predicted {
290            "Prediction"
291        } else {
292            "Estimate"
293        };
294        let mut fmt_cov = Vec::with_capacity(dim);
295        for i in 0..dim {
296            let unit = if i < 3 {
297                "m"
298            } else if i < 6 {
299                "m/s"
300            } else {
301                ""
302            };
303            // Convert from km to meter
304            let val = &self.covar[(i, i)] * 1e3;
305            if val.abs() < 1e-3 {
306                fmt_cov.push(format!("{val:.6e} {unit}"));
307            } else {
308                fmt_cov.push(format!("{val:.6} {unit}"));
309            }
310        }
311        write!(
312            f,
313            "=== {word} @ {} -- within 3 sigma: {} ===\nstate {}\nsigmas [{}]\n",
314            &self.epoch(),
315            self.within_3sigma(),
316            &self.state(),
317            fmt_cov.join(", ")
318        )
319    }
320}
321
322impl<T: State> fmt::LowerExp for KfEstimate<T>
323where
324    DefaultAllocator: Allocator<<T as State>::Size>
325        + Allocator<<T as State>::Size, <T as State>::Size>
326        + Allocator<<T as State>::Size>
327        + Allocator<<T as State>::VecLength>,
328    <DefaultAllocator as Allocator<<T as State>::Size>>::Buffer<f64>: Copy,
329    <DefaultAllocator as Allocator<<T as State>::Size, <T as State>::Size>>::Buffer<f64>: Copy,
330{
331    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
332        let dim = <T as State>::Size::dim();
333        let word = if self.predicted {
334            "Prediction"
335        } else {
336            "Estimate"
337        };
338        let mut fmt_cov = Vec::with_capacity(dim);
339        for i in 0..dim {
340            let unit = if i < 3 {
341                "km"
342            } else if i < 6 {
343                "km/s"
344            } else {
345                ""
346            };
347            fmt_cov.push(format!("{:e} {unit}", &self.covar[(i, i)]));
348        }
349        write!(
350            f,
351            "=== {} @ {} -- within 3 sigma: {} ===\nstate {}\nsigmas [{}]\n",
352            word,
353            &self.epoch(),
354            self.within_3sigma(),
355            &self.state(),
356            fmt_cov.join(", ")
357        )
358    }
359}
360
361impl<T: State> Mul<f64> for KfEstimate<T>
362where
363    DefaultAllocator: Allocator<<T as State>::Size>
364        + Allocator<<T as State>::Size, <T as State>::Size>
365        + Allocator<<T as State>::Size>
366        + Allocator<<T as State>::VecLength>,
367    <DefaultAllocator as Allocator<<T as State>::Size>>::Buffer<f64>: Copy,
368    <DefaultAllocator as Allocator<<T as State>::Size, <T as State>::Size>>::Buffer<f64>: Copy,
369{
370    type Output = Self;
371
372    fn mul(mut self, rhs: f64) -> Self::Output {
373        self.covar *= rhs.powi(2);
374        self
375    }
376}
377
378#[cfg(test)]
379mod ut_kfest {
380    use crate::{
381        GMAT_EARTH_GM, Spacecraft, mc::StateDispersion, md::StateParameter,
382        od::estimate::KfEstimate,
383    };
384    use anise::analysis::prelude::OrbitalElement;
385    use anise::{constants::frames::EARTH_J2000, prelude::Orbit};
386    use hifitime::Epoch;
387
388    #[test]
389    fn test_estimate_from_disp() {
390        let eme2k = EARTH_J2000.with_mu_km3_s2(GMAT_EARTH_GM);
391        let dt = Epoch::from_gregorian_tai_at_midnight(2020, 1, 1);
392        let initial_state = Spacecraft::builder()
393            .orbit(Orbit::keplerian(
394                22000.0, 0.01, 30.0, 80.0, 40.0, 0.0, dt, eme2k,
395            ))
396            .build();
397
398        let initial_estimate = KfEstimate::from_dispersions(
399            initial_state,
400            vec![
401                StateDispersion::builder()
402                    .param(StateParameter::Element(OrbitalElement::SemiMajorAxis))
403                    .std_dev(1.1)
404                    .build(),
405                StateDispersion::zero_mean(
406                    StateParameter::Element(OrbitalElement::Inclination),
407                    0.2,
408                ),
409                StateDispersion::zero_mean(StateParameter::Element(OrbitalElement::RAAN), 0.2),
410                StateDispersion::zero_mean(StateParameter::Element(OrbitalElement::AoP), 0.2),
411            ],
412            Some(0),
413        )
414        .unwrap();
415
416        let initial_state_dev = initial_estimate.nominal_state;
417
418        let (init_rss_pos_km, init_rss_vel_km_s, _) =
419            initial_state.rss(&initial_state_dev).unwrap();
420
421        let delta = (initial_state.orbit - initial_state_dev.orbit).unwrap();
422
423        println!("Truth initial state:\n{initial_state}\n{initial_state:x}");
424        println!("Filter initial state:\n{initial_state_dev}\n{initial_state_dev:x}");
425        println!(
426            "Initial state dev:\t{init_rss_pos_km:.6} km\t{init_rss_vel_km_s:.6} km/s\n{delta}",
427        );
428        println!("covariance: {:.6}", initial_estimate.covar);
429
430        // Check that the error is in the square root of the covariance
431        assert!(delta.radius_km.x < initial_estimate.covar[(0, 0)].sqrt());
432        assert!(delta.radius_km.y < initial_estimate.covar[(1, 1)].sqrt());
433        assert!(delta.radius_km.z < initial_estimate.covar[(2, 2)].sqrt());
434        assert!(delta.velocity_km_s.x < initial_estimate.covar[(3, 3)].sqrt());
435        assert!(delta.velocity_km_s.y < initial_estimate.covar[(4, 4)].sqrt());
436        assert!(delta.velocity_km_s.z < initial_estimate.covar[(5, 5)].sqrt());
437    }
438}