Skip to main content

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::Spacecraft;
21use crate::io::ConfigError;
22use crate::md::prelude::Traj;
23use crate::od::msr::measurement::Measurement;
24use crate::od::msr::two_way::solve_two_way_picard;
25use crate::od::msr::{IntegrationRef, MeasurementType};
26use crate::time::Epoch;
27use anise::astro::Aberration;
28use anise::errors::AlmanacResult;
29use anise::frames::Frame;
30use anise::prelude::{Almanac, Orbit};
31use hifitime::TimeUnits;
32use indexmap::IndexSet;
33use log::debug;
34use rand_pcg::Pcg64Mcg;
35use snafu::ResultExt;
36
37use super::GroundStation;
38
39impl TrackingDevice<Spacecraft> for GroundStation {
40    fn measurement_types(&self) -> &IndexSet<MeasurementType> {
41        &self.measurement_types
42    }
43
44    /// Perform a measurement from the ground station to the receiver (rx).
45    /// The epoch MUST be the station reception epoch.
46    fn measure(
47        &mut self,
48        epoch: Epoch,
49        traj: &Traj<Spacecraft>,
50        rng: Option<&mut Pcg64Mcg>,
51        almanac: &Almanac,
52    ) -> Result<Option<Measurement>, ODError> {
53        let mut msr = Measurement::new(self.name.clone(), epoch);
54        msr.doppler_config = self.doppler_config;
55
56        if self.light_time_correction {
57            // Solve for Relativistic Picard Light-Time
58
59            // Step 2a: Solve Downlink Leg (t3 -> t2) for Range & Angles
60            let two_way_sol = match solve_two_way_picard(epoch, self, traj, almanac) {
61                Ok(sol) => sol,
62                Err(_) => return Ok(None),
63            };
64
65            let rx = traj.at(two_way_sol.t2_bounce).context(ODTrajSnafu {
66                details: "fetching state for bounce epoch".to_string(),
67            })?;
68
69            // Evaluate Azimuth/Elevation from Downlink Look Direction at t3
70            // Construct the apparent target state using the solved bounce state r_sc(t2)
71            // XXX Is this correct? Should this be at the t3 epoch of reception? The body fixed frame
72            // should probably be computed at reception, but then what is the state of the vehicle when it
73            // was emitting ... that the state at t2. I need to think more about this.
74            let aer_downlink = almanac
75                .azimuth_elevation_range_sez_from_location(
76                    rx.orbit,
77                    self.location.clone(),
78                    self.obstructing_body.map(|b| b.into()),
79                    None, // Position r_sc(t2) is already retarded; do not apply LT twice
80                )
81                .context(ODAlmanacSnafu {
82                    action: "computing downlink AER",
83                })?;
84
85            if aer_downlink.elevation_above_mask_deg() < 0.0 || aer_downlink.is_obstructed() {
86                return Ok(None);
87            }
88
89            let noises = self.noises(epoch, rng)?;
90
91            for (ii, msr_type) in self.measurement_types.iter().enumerate() {
92                let noise = noises[ii + 1];
93                let val = match msr_type {
94                    MeasurementType::Range => two_way_sol.range_km() + noise,
95
96                    MeasurementType::Azimuth => aer_downlink.azimuth_deg + noise,
97                    MeasurementType::Elevation => aer_downlink.elevation_deg + noise,
98
99                    MeasurementType::Doppler => {
100                        let doppler_cfg = msr
101                            .doppler_config
102                            .ok_or_else(|| ODError::ODConfigError { source: ConfigError::InvalidConfig {
103                                msg: "Doppler measurement requires doppler_config on GroundStation".to_string()
104                            }})?;
105
106                        let integr_time = doppler_cfg.integration_time;
107
108                        // Compute integration window boundaries from integration_ref
109                        let (t_start, t_end) = match doppler_cfg.integration_ref {
110                            IntegrationRef::Start => (epoch, epoch + integr_time),
111                            IntegrationRef::Middle => {
112                                (epoch - integr_time * 0.5, epoch + integr_time * 0.5)
113                            }
114                            IntegrationRef::End => (epoch - integr_time, epoch),
115                        };
116
117                        // Evaluate two-way ranges at window boundaries
118                        let r_start =
119                            solve_two_way_picard(t_start, self, traj, almanac)?.range_km();
120                        let r_end = solve_two_way_picard(t_end, self, traj, almanac)?.range_km();
121
122                        // Differenced range rate + Doppler noise
123                        ((r_end - r_start) / integr_time.to_seconds()) + noise
124                    }
125
126                    _ => {
127                        return Err(ODError::ODLimitation {
128                            action: format!("MeasurementType::{msr_type:?} is unsupported"),
129                        });
130                    }
131                };
132
133                msr.push(*msr_type, val);
134            }
135
136            Ok(Some(msr))
137        } else {
138            let rx = traj.at(epoch).context(ODTrajSnafu {
139                details: "fetching state for instantaneous measurement".to_string(),
140            })?;
141
142            if let Some(obstruction_body) = self.obstructing_body {
143                let observer =
144                    Spacecraft::from(self.to_orbit(epoch, almanac).context(ODAlmanacSnafu {
145                        action: "building ground station orbit",
146                    })?);
147                let ab_corr = Aberration::NONE;
148                let is_obstructed = almanac
149                    .line_of_sight_obstructed(
150                        observer.orbit,
151                        rx.orbit,
152                        obstruction_body.into(),
153                        ab_corr,
154                    )
155                    .context(ODAlmanacSnafu {
156                        action: "computing line of sight",
157                    })?;
158
159                if is_obstructed {
160                    return Ok(None);
161                }
162            }
163
164            let aer = almanac
165                .azimuth_elevation_range_sez_from_location(
166                    rx.orbit,
167                    self.location.clone(),
168                    None,
169                    None,
170                )
171                .context(ODAlmanacSnafu {
172                    action: "computing AER",
173                })?;
174
175            if aer.elevation_above_mask_deg() >= 0.0 && !aer.is_obstructed() {
176                // Only update the noises if the measurement is valid.
177                let noises = self.noises(rx.orbit.epoch, rng)?;
178
179                let mut msr =
180                    Measurement::new(self.name.clone(), rx.orbit.epoch + noises[0].seconds());
181                msr.doppler_config = self.doppler_config;
182
183                for (ii, msr_type) in self.measurement_types.iter().enumerate() {
184                    let msr_value = if msr_type == &MeasurementType::Doppler {
185                        if let Some(doppler_cfg) = msr.doppler_config {
186                            let integr_time = doppler_cfg.integration_time;
187
188                            // Compute integration window boundaries from integration_ref
189                            let (t_start, t_end) = match doppler_cfg.integration_ref {
190                                IntegrationRef::Start => (epoch, epoch + integr_time),
191                                IntegrationRef::Middle => {
192                                    (epoch - integr_time * 0.5, epoch + integr_time * 0.5)
193                                }
194                                IntegrationRef::End => (epoch - integr_time, epoch),
195                            };
196
197                            // Evaluate two-way ranges at window boundaries
198                            let sc_start = traj.at(t_start).context(ODTrajSnafu {
199                                details: "fetching state for start of integration".to_string(),
200                            })?;
201                            let sc_end = traj.at(t_end).context(ODTrajSnafu {
202                                details: "fetching state for end of integration".to_string(),
203                            })?;
204                            let aer_start = almanac
205                                .azimuth_elevation_range_sez_from_location(
206                                    sc_start.orbit,
207                                    self.location.clone(),
208                                    None,
209                                    None,
210                                )
211                                .context(ODAlmanacSnafu {
212                                    action: "computing AER at start of integration time",
213                                })?;
214
215                            let aer_end = almanac
216                                .azimuth_elevation_range_sez_from_location(
217                                    sc_end.orbit,
218                                    self.location.clone(),
219                                    None,
220                                    None,
221                                )
222                                .context(ODAlmanacSnafu {
223                                    action: "computing AER at end of integration time",
224                                })?;
225
226                            // Differenced range rate + Doppler noise
227                            ((aer_end.range_km - aer_start.range_km) / integr_time.to_seconds())
228                                + noises[ii + 1]
229                        } else {
230                            msr_type.compute_one_way(aer, noises[ii + 1])?
231                        }
232                    } else {
233                        msr_type.compute_one_way(aer, noises[ii + 1])?
234                    };
235                    msr.push(*msr_type, msr_value);
236                }
237
238                Ok(Some(msr))
239            } else {
240                debug!(
241                    "{} {} object at {:.3} deg -- no measurement",
242                    self.name,
243                    rx.orbit.epoch,
244                    aer.elevation_above_mask_deg(),
245                );
246                Ok(None)
247            }
248        }
249    }
250
251    fn name(&self) -> String {
252        self.name.clone()
253    }
254
255    fn location(&self, epoch: Epoch, frame: Frame, almanac: &Almanac) -> AlmanacResult<Orbit> {
256        almanac.transform_to(self.to_orbit(epoch, almanac).unwrap(), frame, None)
257    }
258
259    fn measure_instantaneous(
260        &mut self,
261        rx: Spacecraft,
262        rng: Option<&mut Pcg64Mcg>,
263        almanac: &Almanac,
264    ) -> Result<Option<Measurement>, ODError> {
265        // HACK This function should be avoided. A future version will remove the instantaneous measurement
266        // because it isn't physically adequate.
267        if let Some(obstruction_body) = self.obstructing_body {
268            let observer = Spacecraft::from(self.to_orbit(rx.orbit.epoch, almanac).context(
269                ODAlmanacSnafu {
270                    action: "building ground station orbit",
271                },
272            )?);
273            let ab_corr = if self.light_time_correction {
274                Aberration::LT
275            } else {
276                Aberration::NONE
277            };
278            let is_obstructed = almanac
279                .line_of_sight_obstructed(
280                    observer.orbit,
281                    rx.orbit,
282                    obstruction_body.into(),
283                    ab_corr,
284                )
285                .context(ODAlmanacSnafu {
286                    action: "computing line of sight",
287                })?;
288
289            if is_obstructed {
290                return Ok(None);
291            }
292        }
293
294        let aer = almanac
295            .azimuth_elevation_range_sez_from_location(rx.orbit, self.location.clone(), None, None)
296            .context(ODAlmanacSnafu {
297                action: "computing AER",
298            })?;
299
300        if aer.elevation_above_mask_deg() >= 0.0 && !aer.is_obstructed() {
301            // Only update the noises if the measurement is valid.
302            let noises = self.noises(rx.orbit.epoch, rng)?;
303
304            let mut msr = Measurement::new(self.name.clone(), rx.orbit.epoch + noises[0].seconds());
305            msr.doppler_config = self.doppler_config;
306
307            for (ii, msr_type) in self.measurement_types.iter().enumerate() {
308                let msr_value = msr_type.compute_one_way(aer, noises[ii + 1])?;
309                msr.push(*msr_type, msr_value);
310            }
311
312            Ok(Some(msr))
313        } else {
314            debug!(
315                "{} {} object at {:.3} deg -- no measurement",
316                self.name,
317                rx.orbit.epoch,
318                aer.elevation_above_mask_deg(),
319            );
320            Ok(None)
321        }
322    }
323
324    /// Returns the measurement noise of this ground station.
325    ///
326    /// # Methodology
327    /// Noises are modeled using a [StochasticNoise] process, defined by the sigma on the turn-on bias and on the steady state noise.
328    /// The measurement noise is computed assuming that all measurements are independent variables, i.e. the measurement matrix is
329    /// 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
330    /// second item is the Doppler noise (in km/s), set to the square of the steady state sigma of that Gauss Markov process.
331    fn measurement_covar(&self, msr_type: MeasurementType, epoch: Epoch) -> Result<f64, ODError> {
332        let stochastics = self.stochastic_noises.as_ref().unwrap();
333
334        Ok(stochastics
335            .get(&msr_type)
336            .ok_or(ODError::NoiseNotConfigured {
337                kind: format!("{msr_type:?}"),
338            })?
339            .covariance(epoch))
340    }
341
342    fn measurement_bias(&self, msr_type: MeasurementType, _epoch: Epoch) -> Result<f64, ODError> {
343        let stochastics = self.stochastic_noises.as_ref().unwrap();
344
345        if let Some(gm) = stochastics
346            .get(&msr_type)
347            .ok_or(ODError::NoiseNotConfigured {
348                kind: format!("{msr_type:?}"),
349            })?
350            .bias
351        {
352            Ok(gm.constant.unwrap_or(0.0))
353        } else {
354            Ok(0.0)
355        }
356    }
357}