nyx_space/od/ground_station/
trk_device.rs1use 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 log::debug;
31use rand_pcg::Pcg64Mcg;
32use snafu::ResultExt;
33use std::sync::Arc;
34
35use super::GroundStation;
36
37impl TrackingDevice<Spacecraft> for GroundStation {
38 fn measurement_types(&self) -> &IndexSet<MeasurementType> {
39 &self.measurement_types
40 }
41
42 fn measure(
44 &mut self,
45 epoch: Epoch,
46 traj: &Traj<Spacecraft>,
47 rng: Option<&mut Pcg64Mcg>,
48 almanac: Arc<Almanac>,
49 ) -> Result<Option<Measurement>, ODError> {
50 match self.integration_time {
51 Some(integration_time) => {
52 let rx_0 = match traj.at(epoch - integration_time).context(ODTrajSnafu {
55 details: format!(
56 "fetching state {epoch} at start of ground station integration time {integration_time}"
57 ),
58 }) {
59 Ok(rx) => rx,
60 Err(_) => return Ok(None),
61 };
62
63 let rx_1 = match traj.at(epoch).context(ODTrajSnafu {
64 details: format!(
65 "fetching state {epoch} at end of ground station integration time"
66 ),
67 }) {
68 Ok(rx) => rx,
69 Err(_) => return Ok(None),
70 };
71
72 let obstructing_body = if !self.frame.ephem_origin_match(rx_0.frame()) {
73 Some(rx_0.frame())
74 } else {
75 None
76 };
77
78 let aer_t0 = self
79 .azimuth_elevation_of(rx_0.orbit, obstructing_body, &almanac)
80 .context(ODAlmanacSnafu {
81 action: "computing AER",
82 })?;
83 let aer_t1 = self
84 .azimuth_elevation_of(rx_1.orbit, obstructing_body, &almanac)
85 .context(ODAlmanacSnafu {
86 action: "computing AER",
87 })?;
88
89 if aer_t0.elevation_deg < self.elevation_mask_deg
90 || aer_t1.elevation_deg < self.elevation_mask_deg
91 {
92 debug!(
93 "{} (el. mask {:.3} deg) but object moves from {:.3} to {:.3} deg -- no measurement",
94 self.name, self.elevation_mask_deg, aer_t0.elevation_deg, aer_t1.elevation_deg
95 );
96 return Ok(None);
97 } else if aer_t0.is_obstructed() || aer_t1.is_obstructed() {
98 debug!(
99 "{} obstruction at t0={}, t1={} -- no measurement",
100 self.name,
101 aer_t0.is_obstructed(),
102 aer_t1.is_obstructed()
103 );
104 return Ok(None);
105 }
106
107 let noises = self.noises(epoch - integration_time * 0.5, rng)?;
109
110 let mut msr = Measurement::new(self.name.clone(), epoch + noises[0].seconds());
111
112 for (ii, msr_type) in self.measurement_types.iter().enumerate() {
113 let msr_value = msr_type.compute_two_way(aer_t0, aer_t1, noises[ii + 1])?;
114 msr.push(*msr_type, msr_value);
115 }
116
117 Ok(Some(msr))
118 }
119 None => self.measure_instantaneous(
120 traj.at(epoch).context(ODTrajSnafu {
121 details: "fetching state for instantaneous measurement".to_string(),
122 })?,
123 rng,
124 almanac,
125 ),
126 }
127 }
128
129 fn name(&self) -> String {
130 self.name.clone()
131 }
132
133 fn location(&self, epoch: Epoch, frame: Frame, almanac: Arc<Almanac>) -> AlmanacResult<Orbit> {
134 almanac.transform_to(self.to_orbit(epoch, &almanac).unwrap(), frame, None)
135 }
136
137 fn measure_instantaneous(
138 &mut self,
139 rx: Spacecraft,
140 rng: Option<&mut Pcg64Mcg>,
141 almanac: Arc<Almanac>,
142 ) -> Result<Option<Measurement>, ODError> {
143 let obstructing_body = if !self.frame.ephem_origin_match(rx.frame()) {
144 Some(rx.frame())
145 } else {
146 None
147 };
148
149 let aer = self
150 .azimuth_elevation_of(rx.orbit, obstructing_body, &almanac)
151 .context(ODAlmanacSnafu {
152 action: "computing AER",
153 })?;
154
155 if aer.elevation_deg >= self.elevation_mask_deg && !aer.is_obstructed() {
156 let noises = self.noises(rx.orbit.epoch, rng)?;
158
159 let mut msr = Measurement::new(self.name.clone(), rx.orbit.epoch + noises[0].seconds());
160
161 for (ii, msr_type) in self.measurement_types.iter().enumerate() {
162 let msr_value = msr_type.compute_one_way(aer, noises[ii + 1])?;
163 msr.push(*msr_type, msr_value);
164 }
165
166 Ok(Some(msr))
167 } else {
168 debug!(
169 "{} {} (el. mask {:.3} deg), object at {:.3} deg -- no measurement",
170 self.name, rx.orbit.epoch, self.elevation_mask_deg, aer.elevation_deg
171 );
172 Ok(None)
173 }
174 }
175
176 fn measurement_covar(&self, msr_type: MeasurementType, epoch: Epoch) -> Result<f64, ODError> {
184 let stochastics = self.stochastic_noises.as_ref().unwrap();
185
186 Ok(stochastics
187 .get(&msr_type)
188 .ok_or(ODError::NoiseNotConfigured {
189 kind: format!("{msr_type:?}"),
190 })?
191 .covariance(epoch))
192 }
193
194 fn measurement_bias(&self, msr_type: MeasurementType, _epoch: Epoch) -> Result<f64, ODError> {
195 let stochastics = self.stochastic_noises.as_ref().unwrap();
196
197 if let Some(gm) = stochastics
198 .get(&msr_type)
199 .ok_or(ODError::NoiseNotConfigured {
200 kind: format!("{msr_type:?}"),
201 })?
202 .bias
203 {
204 Ok(gm.constant.unwrap_or(0.0))
205 } else {
206 Ok(0.0)
207 }
208 }
209}