nyx_space/od/ground_station/
trk_device.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::{ODAlmanacSnafu, ODError, ODTrajSnafu, TrackingDevice};
20use crate::md::prelude::{Interpolatable, Traj};
21use crate::od::msr::measurement::Measurement;
22use crate::od::msr::MeasurementType;
23use crate::time::Epoch;
24use crate::Spacecraft;
25use anise::errors::AlmanacResult;
26use anise::frames::Frame;
27use anise::prelude::{Almanac, Orbit};
28use hifitime::TimeUnits;
29use indexmap::IndexSet;
30use rand_pcg::Pcg64Mcg;
31use snafu::ResultExt;
32use std::sync::Arc;
33
34use super::GroundStation;
35
36impl TrackingDevice<Spacecraft> for GroundStation {
37    fn measurement_types(&self) -> &IndexSet<MeasurementType> {
38        &self.measurement_types
39    }
40
41    /// Perform a measurement from the ground station to the receiver (rx).
42    fn measure(
43        &mut self,
44        epoch: Epoch,
45        traj: &Traj<Spacecraft>,
46        rng: Option<&mut Pcg64Mcg>,
47        almanac: Arc<Almanac>,
48    ) -> Result<Option<Measurement>, ODError> {
49        match self.integration_time {
50            Some(integration_time) => {
51                // TODO: This should support measurement alignment
52                // If out of traj bounds, return None, else the whole strand is rejected.
53                let rx_0 = match traj.at(epoch - integration_time).context(ODTrajSnafu {
54                    details: format!(
55                        "fetching state {epoch} at start of ground station integration time {integration_time}"
56                    ),
57                }) {
58                    Ok(rx) => rx,
59                    Err(_) => return Ok(None),
60                };
61
62                let rx_1 = match traj.at(epoch).context(ODTrajSnafu {
63                    details: format!(
64                        "fetching state {epoch} at end of ground station integration time"
65                    ),
66                }) {
67                    Ok(rx) => rx,
68                    Err(_) => return Ok(None),
69                };
70
71                let obstructing_body = if !self.frame.ephem_origin_match(rx_0.frame()) {
72                    Some(rx_0.frame())
73                } else {
74                    None
75                };
76
77                let aer_t0 = self
78                    .azimuth_elevation_of(rx_0.orbit, obstructing_body, &almanac)
79                    .context(ODAlmanacSnafu {
80                        action: "computing AER",
81                    })?;
82                let aer_t1 = self
83                    .azimuth_elevation_of(rx_1.orbit, obstructing_body, &almanac)
84                    .context(ODAlmanacSnafu {
85                        action: "computing AER",
86                    })?;
87
88                if aer_t0.elevation_deg < self.elevation_mask_deg
89                    || aer_t1.elevation_deg < self.elevation_mask_deg
90                {
91                    debug!(
92                        "{} (el. mask {:.3} deg) but object moves from {:.3} to {:.3} deg -- no measurement",
93                        self.name, self.elevation_mask_deg, aer_t0.elevation_deg, aer_t1.elevation_deg
94                    );
95                    return Ok(None);
96                } else if aer_t0.is_obstructed() || aer_t1.is_obstructed() {
97                    debug!(
98                        "{} obstruction at t0={}, t1={} -- no measurement",
99                        self.name,
100                        aer_t0.is_obstructed(),
101                        aer_t1.is_obstructed()
102                    );
103                    return Ok(None);
104                }
105
106                // Noises are computed at the midpoint of the integration time.
107                let noises = self.noises(epoch - integration_time * 0.5, rng)?;
108
109                let mut msr = Measurement::new(self.name.clone(), epoch + noises[0].seconds());
110
111                for (ii, msr_type) in self.measurement_types.iter().enumerate() {
112                    let msr_value = msr_type.compute_two_way(aer_t0, aer_t1, noises[ii + 1])?;
113                    msr.push(*msr_type, msr_value);
114                }
115
116                Ok(Some(msr))
117            }
118            None => self.measure_instantaneous(
119                traj.at(epoch).context(ODTrajSnafu {
120                    details: "fetching state for instantaneous measurement".to_string(),
121                })?,
122                rng,
123                almanac,
124            ),
125        }
126    }
127
128    fn name(&self) -> String {
129        self.name.clone()
130    }
131
132    fn location(&self, epoch: Epoch, frame: Frame, almanac: Arc<Almanac>) -> AlmanacResult<Orbit> {
133        almanac.transform_to(self.to_orbit(epoch, &almanac).unwrap(), frame, None)
134    }
135
136    fn measure_instantaneous(
137        &mut self,
138        rx: Spacecraft,
139        rng: Option<&mut Pcg64Mcg>,
140        almanac: Arc<Almanac>,
141    ) -> Result<Option<Measurement>, ODError> {
142        let obstructing_body = if !self.frame.ephem_origin_match(rx.frame()) {
143            Some(rx.frame())
144        } else {
145            None
146        };
147
148        let aer = self
149            .azimuth_elevation_of(rx.orbit, obstructing_body, &almanac)
150            .context(ODAlmanacSnafu {
151                action: "computing AER",
152            })?;
153
154        if aer.elevation_deg >= self.elevation_mask_deg && !aer.is_obstructed() {
155            // Only update the noises if the measurement is valid.
156            let noises = self.noises(rx.orbit.epoch, rng)?;
157
158            let mut msr = Measurement::new(self.name.clone(), rx.orbit.epoch + noises[0].seconds());
159
160            for (ii, msr_type) in self.measurement_types.iter().enumerate() {
161                let msr_value = msr_type.compute_one_way(aer, noises[ii + 1])?;
162                msr.push(*msr_type, msr_value);
163            }
164
165            Ok(Some(msr))
166        } else {
167            debug!(
168                "{} {} (el. mask {:.3} deg), object at {:.3} deg -- no measurement",
169                self.name, rx.orbit.epoch, self.elevation_mask_deg, aer.elevation_deg
170            );
171            Ok(None)
172        }
173    }
174
175    /// Returns the measurement noise of this ground station.
176    ///
177    /// # Methodology
178    /// Noises are modeled using a [StochasticNoise] process, defined by the sigma on the turn-on bias and on the steady state noise.
179    /// The measurement noise is computed assuming that all measurements are independent variables, i.e. the measurement matrix is
180    /// a diagonal matrix. The first item in the diagonal is the range noise (in km), set to the square of the steady state sigma. The
181    /// second item is the Doppler noise (in km/s), set to the square of the steady state sigma of that Gauss Markov process.
182    fn measurement_covar(&self, msr_type: MeasurementType, epoch: Epoch) -> Result<f64, ODError> {
183        let stochastics = self.stochastic_noises.as_ref().unwrap();
184
185        Ok(stochastics
186            .get(&msr_type)
187            .ok_or(ODError::NoiseNotConfigured {
188                kind: format!("{msr_type:?}"),
189            })?
190            .covariance(epoch))
191    }
192
193    fn measurement_bias(&self, msr_type: MeasurementType, _epoch: Epoch) -> Result<f64, ODError> {
194        let stochastics = self.stochastic_noises.as_ref().unwrap();
195
196        if let Some(gm) = stochastics
197            .get(&msr_type)
198            .ok_or(ODError::NoiseNotConfigured {
199                kind: format!("{msr_type:?}"),
200            })?
201            .bias
202        {
203            Ok(gm.constant.unwrap_or(0.0))
204        } else {
205            Ok(0.0)
206        }
207    }
208}