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