nyx_space/od/ground_station/
trk_device.rs1use 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 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 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 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, )
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 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 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 ((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 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 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 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 ((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 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 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 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}