nyx_space/od/ground_station/
mod.rs1use anise::astro::{Aberration, AzElRange, Location, PhysicsResult};
20use anise::errors::AlmanacResult;
21use anise::prelude::{Almanac, Frame, Orbit};
22use indexmap::{IndexMap, IndexSet};
23use snafu::ensure;
24
25use super::msr::MeasurementType;
26use super::noise::{GaussMarkov, StochasticNoise};
27use super::{ODAlmanacSnafu, ODError, ODTrajSnafu, TrackingDevice};
28use crate::io::ConfigRepr;
29use crate::od::NoiseNotConfiguredSnafu;
30use crate::time::Epoch;
31use hifitime::Duration;
32use rand_pcg::Pcg64Mcg;
33use serde_derive::{Deserialize, Serialize};
34use std::fmt;
35
36pub mod builtin;
37pub mod trk_device;
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
41pub struct GroundStation {
42 pub name: String,
43 pub location: Location,
44 pub measurement_types: IndexSet<MeasurementType>,
45 pub integration_time: Option<Duration>,
47 pub light_time_correction: bool,
49 pub timestamp_noise_s: Option<StochasticNoise>,
51 pub stochastic_noises: Option<IndexMap<MeasurementType, StochasticNoise>>,
52}
53
54impl GroundStation {
55 pub fn from_point(
58 name: String,
59 latitude_deg: f64,
60 longitude_deg: f64,
61 height_km: f64,
62 frame: Frame,
63 ) -> Self {
64 Self {
65 name,
66 location: Location {
67 latitude_deg,
68 longitude_deg,
69 height_km,
70 frame: frame.into(),
71 terrain_mask: vec![],
72 terrain_mask_ignored: true,
73 },
74 measurement_types: IndexSet::new(),
75 integration_time: None,
76 light_time_correction: false,
77 timestamp_noise_s: None,
78 stochastic_noises: None,
79 }
80 }
81
82 pub fn with_msr_type(mut self, msr_type: MeasurementType, noise: StochasticNoise) -> Self {
84 if self.stochastic_noises.is_none() {
85 self.stochastic_noises = Some(IndexMap::new());
86 }
87
88 self.stochastic_noises
89 .as_mut()
90 .unwrap()
91 .insert(msr_type, noise);
92
93 self.measurement_types.insert(msr_type);
94
95 self
96 }
97
98 pub fn without_msr_type(mut self, msr_type: MeasurementType) -> Self {
100 if let Some(noises) = self.stochastic_noises.as_mut() {
101 noises.swap_remove(&msr_type);
102 }
103
104 self.measurement_types.swap_remove(&msr_type);
105
106 self
107 }
108
109 pub fn with_integration_time(mut self, integration_time: Option<Duration>) -> Self {
110 self.integration_time = integration_time;
111
112 self
113 }
114
115 pub fn with_msr_bias_constant(
117 mut self,
118 msr_type: MeasurementType,
119 bias_constant: f64,
120 ) -> Result<Self, ODError> {
121 if self.stochastic_noises.is_none() {
122 self.stochastic_noises = Some(IndexMap::new());
123 }
124
125 let stochastics = self.stochastic_noises.as_mut().unwrap();
126
127 let this_noise = stochastics
128 .get_mut(&msr_type)
129 .ok_or(ODError::NoiseNotConfigured {
130 kind: format!("{msr_type:?}"),
131 })
132 .unwrap();
133
134 if this_noise.bias.is_none() {
135 this_noise.bias = Some(GaussMarkov::ZERO);
136 }
137
138 this_noise.bias.unwrap().constant = Some(bias_constant);
139
140 Ok(self)
141 }
142
143 pub fn azimuth_elevation_of(
146 &self,
147 rx: Orbit,
148 obstructing_body: Option<Frame>,
149 almanac: &Almanac,
150 ) -> AlmanacResult<AzElRange> {
151 let ab_corr = if self.light_time_correction {
152 Aberration::LT
153 } else {
154 Aberration::NONE
155 };
156 almanac.azimuth_elevation_range_sez(
157 rx,
158 self.to_orbit(rx.epoch, almanac).unwrap(),
159 obstructing_body,
160 ab_corr,
161 )
162 }
163
164 pub fn to_orbit(&self, epoch: Epoch, almanac: &Almanac) -> PhysicsResult<Orbit> {
166 Orbit::try_latlongalt(
167 self.location.latitude_deg,
168 self.location.longitude_deg,
169 self.location.height_km,
170 epoch,
171 almanac.frame_info(self.location.frame).unwrap(),
172 )
173 }
174
175 fn noises(&mut self, epoch: Epoch, rng: Option<&mut Pcg64Mcg>) -> Result<Vec<f64>, ODError> {
177 let mut noises = vec![0.0; self.measurement_types.len() + 1];
178
179 if let Some(rng) = rng {
180 ensure!(
181 self.stochastic_noises.is_some(),
182 NoiseNotConfiguredSnafu {
183 kind: "ground station stochastics".to_string(),
184 }
185 );
186 if let Some(mut timestamp_noise) = self.timestamp_noise_s {
189 noises[0] = timestamp_noise.sample(epoch, rng);
190 }
191
192 let stochastics = self.stochastic_noises.as_mut().unwrap();
193
194 for (ii, msr_type) in self.measurement_types.iter().enumerate() {
195 noises[ii + 1] = stochastics
196 .get_mut(msr_type)
197 .ok_or(ODError::NoiseNotConfigured {
198 kind: format!("{msr_type:?}"),
199 })?
200 .sample(epoch, rng);
201 }
202 }
203
204 Ok(noises)
205 }
206}
207
208impl Default for GroundStation {
209 fn default() -> Self {
210 let mut measurement_types = IndexSet::new();
211 measurement_types.insert(MeasurementType::Range);
212 measurement_types.insert(MeasurementType::Doppler);
213 Self {
214 name: "UNDEFINED".to_string(),
215 measurement_types,
216 location: Location::default(),
217 integration_time: None,
218 light_time_correction: false,
219 timestamp_noise_s: None,
220 stochastic_noises: None,
221 }
222 }
223}
224
225impl ConfigRepr for GroundStation {}
226
227impl fmt::Display for GroundStation {
228 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
230 write!(f, "{} ({})", self.name, self.location,)
231 }
232}
233
234#[cfg(test)]
235mod gs_ut {
236
237 use anise::astro::{Location, TerrainMask};
238 use anise::constants::frames::IAU_EARTH_FRAME;
239 use indexmap::{IndexMap, IndexSet};
240
241 use crate::io::ConfigRepr;
242 use crate::od::prelude::*;
243
244 #[test]
245 fn test_load_single() {
246 use std::env;
247 use std::path::PathBuf;
248
249 use hifitime::TimeUnits;
250
251 let test_data: PathBuf = [
252 env::var("CARGO_MANIFEST_DIR").unwrap(),
253 "data".to_string(),
254 "03_tests".to_string(),
255 "config".to_string(),
256 "one_ground_station.yaml".to_string(),
257 ]
258 .iter()
259 .collect();
260
261 assert!(test_data.exists(), "Could not find the test data");
262
263 let gs = GroundStation::load(test_data).unwrap();
264
265 dbg!(&gs);
266
267 let mut measurement_types = IndexSet::new();
268 measurement_types.insert(MeasurementType::Range);
269 measurement_types.insert(MeasurementType::Doppler);
270
271 let mut stochastics = IndexMap::new();
272 stochastics.insert(
273 MeasurementType::Range,
274 StochasticNoise {
275 bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
276 ..Default::default()
277 },
278 );
279 stochastics.insert(
280 MeasurementType::Doppler,
281 StochasticNoise {
282 bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
283 ..Default::default()
284 },
285 );
286
287 let expected_gs = GroundStation {
288 name: "Demo ground station".to_string(),
289 location: Location {
290 latitude_deg: 2.3522,
291 longitude_deg: 48.8566,
292 height_km: 0.4,
293 frame: IAU_EARTH_FRAME.into(),
294 terrain_mask: TerrainMask::from_flat_terrain(5.0),
295 terrain_mask_ignored: false,
296 },
297 measurement_types,
298 stochastic_noises: Some(stochastics),
299
300 light_time_correction: false,
301 timestamp_noise_s: None,
302 integration_time: Some(60 * Unit::Second),
303 };
304
305 println!("{}", serde_yml::to_string(&expected_gs).unwrap());
306
307 assert_eq!(expected_gs, gs);
308 }
309
310 #[test]
311 fn test_load_many() {
312 use hifitime::TimeUnits;
313 use std::env;
314 use std::path::PathBuf;
315
316 let test_file: PathBuf = [
317 env::var("CARGO_MANIFEST_DIR").unwrap(),
318 "data".to_string(),
319 "03_tests".to_string(),
320 "config".to_string(),
321 "many_ground_stations.yaml".to_string(),
322 ]
323 .iter()
324 .collect();
325
326 let stations = GroundStation::load_many(test_file).unwrap();
327
328 dbg!(&stations);
329
330 let mut measurement_types = IndexSet::new();
331 measurement_types.insert(MeasurementType::Range);
332 measurement_types.insert(MeasurementType::Doppler);
333
334 let mut stochastics = IndexMap::new();
335 stochastics.insert(
336 MeasurementType::Range,
337 StochasticNoise {
338 bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
339 ..Default::default()
340 },
341 );
342 stochastics.insert(
343 MeasurementType::Doppler,
344 StochasticNoise {
345 bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
346 ..Default::default()
347 },
348 );
349
350 let expected = vec![
351 GroundStation {
352 name: "Demo ground station".to_string(),
353 location: Location {
354 latitude_deg: 2.3522,
355 longitude_deg: 48.8566,
356 height_km: 0.4,
357 frame: IAU_EARTH_FRAME.into(),
358 terrain_mask: TerrainMask::from_flat_terrain(5.0),
359 terrain_mask_ignored: false,
360 },
361 measurement_types: measurement_types.clone(),
362 stochastic_noises: Some(stochastics.clone()),
363 light_time_correction: false,
364 timestamp_noise_s: None,
365 integration_time: None,
366 },
367 GroundStation {
368 name: "Canberra".to_string(),
369 location: Location {
370 latitude_deg: -35.398333,
371 longitude_deg: 148.981944,
372 height_km: 0.691750,
373 frame: IAU_EARTH_FRAME.into(),
374 terrain_mask: TerrainMask::from_flat_terrain(5.0),
375 terrain_mask_ignored: false,
376 },
377 measurement_types,
378 stochastic_noises: Some(stochastics),
379 light_time_correction: false,
380 timestamp_noise_s: None,
381 integration_time: None,
382 },
383 ];
384
385 assert_eq!(expected, stations);
386
387 let reser = serde_yml::to_string(&expected).unwrap();
389 dbg!(reser);
390 }
391}