1use anise::astro::{Aberration, AzElRange, Location};
20use anise::errors::{AlmanacError, AlmanacResult};
21use anise::frames::FrameUid;
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::od::NoiseNotConfiguredSnafu;
30use crate::time::Epoch;
31use rand_pcg::Pcg64Mcg;
32use serde::{Deserialize, Serialize};
33use std::fmt::{self, Debug};
34
35mod asn1;
36pub mod builtin;
37mod doppler_config;
38pub mod trk_device;
39
40pub use doppler_config::DopplerConfig;
41
42#[cfg(feature = "python")]
43use pyo3::exceptions::PyValueError;
44#[cfg(feature = "python")]
45use pyo3::prelude::*;
46#[cfg(feature = "python")]
47use pyo3::types::{PyBytes, PyType};
48#[cfg(feature = "python")]
49mod python;
50
51#[cfg(feature = "python")]
52use der::{Decode, Encode};
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
64#[cfg_attr(feature = "python", pyclass(from_py_object))]
65pub struct GroundStation {
66 pub name: String,
67 pub location: Location,
68 pub measurement_types: IndexSet<MeasurementType>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub doppler_config: Option<DopplerConfig>,
72 pub light_time_correction: bool,
74 pub timestamp_noise_s: Option<StochasticNoise>,
76 pub stochastic_noises: Option<IndexMap<MeasurementType, StochasticNoise>>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub obstructing_body: Option<FrameUid>,
80 #[serde(default)]
82 pub relativistic_corrections: bool,
83}
84
85#[cfg_attr(feature = "python", pymethods)]
86impl GroundStation {
87 pub fn azimuth_elevation_of(
95 &self,
96 rx: Orbit,
97 obstructing_body: Option<Frame>,
98 almanac: &Almanac,
99 ) -> AlmanacResult<AzElRange> {
100 let ab_corr = if self.light_time_correction {
101 Aberration::LT
102 } else {
103 Aberration::NONE
104 };
105 almanac.azimuth_elevation_range_sez(
106 rx,
107 self.to_orbit(rx.epoch, almanac)?,
108 obstructing_body,
109 ab_corr,
110 )
111 }
112
113 pub fn to_orbit(&self, epoch: Epoch, almanac: &Almanac) -> AlmanacResult<Orbit> {
119 Orbit::try_latlongalt(
120 self.location.latitude_deg,
121 self.location.longitude_deg,
122 self.location.height_km,
123 epoch,
124 almanac.frame_info(self.location.frame).map_err(|source| {
125 AlmanacError::GenericError {
126 err: source.to_string(),
127 }
128 })?,
129 )
130 .map_err(|source| AlmanacError::AlmanacPhysics {
131 action: "building ground station location",
132 source: Box::new(source),
133 })
134 }
135}
136
137impl GroundStation {
138 pub fn from_point(
141 name: String,
142 latitude_deg: f64,
143 longitude_deg: f64,
144 height_km: f64,
145 frame: Frame,
146 ) -> Self {
147 Self {
148 name,
149 location: Location {
150 latitude_deg,
151 longitude_deg,
152 height_km,
153 frame: frame.into(),
154 terrain_mask: vec![],
155 terrain_mask_ignored: true,
156 },
157 measurement_types: IndexSet::new(),
158 doppler_config: None,
159 light_time_correction: false,
160 timestamp_noise_s: None,
161 stochastic_noises: None,
162 obstructing_body: None,
163 relativistic_corrections: false,
164 }
165 }
166
167 pub fn with_msr_type(mut self, msr_type: MeasurementType, noise: StochasticNoise) -> Self {
169 if self.stochastic_noises.is_none() {
170 self.stochastic_noises = Some(IndexMap::new());
171 }
172
173 self.stochastic_noises
174 .as_mut()
175 .unwrap()
176 .insert(msr_type, noise);
177
178 self.measurement_types.insert(msr_type);
179
180 self
181 }
182
183 pub fn without_msr_type(mut self, msr_type: MeasurementType) -> Self {
185 if let Some(noises) = self.stochastic_noises.as_mut() {
186 noises.swap_remove(&msr_type);
187 }
188
189 self.measurement_types.swap_remove(&msr_type);
190
191 self
192 }
193
194 pub fn with_doppler_config(mut self, doppler_config: Option<DopplerConfig>) -> Self {
195 self.doppler_config = doppler_config;
196
197 self
198 }
199
200 pub fn with_obstruction_body(mut self, obstruction_body: Option<FrameUid>) -> Self {
201 self.obstructing_body = obstruction_body;
202
203 self
204 }
205
206 pub fn with_relativistic_corrections(mut self, relativistic_corrections: bool) -> Self {
207 self.relativistic_corrections = relativistic_corrections;
208
209 self
210 }
211
212 pub fn with_msr_bias_constant(
214 mut self,
215 msr_type: MeasurementType,
216 bias_constant: f64,
217 ) -> Result<Self, ODError> {
218 if self.stochastic_noises.is_none() {
219 self.stochastic_noises = Some(IndexMap::new());
220 }
221
222 let stochastics = self.stochastic_noises.as_mut().unwrap();
223
224 let this_noise = stochastics
225 .get_mut(&msr_type)
226 .ok_or(ODError::NoiseNotConfigured {
227 kind: format!("{msr_type:?}"),
228 })
229 .unwrap();
230
231 if this_noise.bias.is_none() {
232 this_noise.bias = Some(GaussMarkov::ZERO);
233 }
234
235 this_noise.bias.unwrap().constant = Some(bias_constant);
236
237 Ok(self)
238 }
239
240 fn noises(&mut self, epoch: Epoch, rng: Option<&mut Pcg64Mcg>) -> Result<Vec<f64>, ODError> {
242 let mut noises = vec![0.0; self.measurement_types.len() + 1];
243
244 if let Some(rng) = rng {
245 ensure!(
246 self.stochastic_noises.is_some(),
247 NoiseNotConfiguredSnafu {
248 kind: "ground station stochastics".to_string(),
249 }
250 );
251 if let Some(mut timestamp_noise) = self.timestamp_noise_s {
254 noises[0] = timestamp_noise.sample(epoch, rng);
255 }
256
257 let stochastics = self.stochastic_noises.as_mut().unwrap();
258
259 for (ii, msr_type) in self.measurement_types.iter().enumerate() {
260 noises[ii + 1] = stochastics
261 .get_mut(msr_type)
262 .ok_or(ODError::NoiseNotConfigured {
263 kind: format!("{msr_type:?}"),
264 })?
265 .sample(epoch, rng);
266 }
267 }
268
269 Ok(noises)
270 }
271
272 pub(crate) fn available_data(&self) -> u8 {
273 let mut bits: u8 = 0;
274
275 if self.doppler_config.is_some() {
276 bits |= 1 << 0;
277 }
278 if self.timestamp_noise_s.is_some() {
279 bits |= 1 << 1;
280 }
281 if self.stochastic_noises.is_some() {
282 bits |= 1 << 2;
283 }
284 if self.obstructing_body.is_some() {
285 bits |= 1 << 3;
286 }
287 bits
288 }
289}
290
291#[cfg(feature = "python")]
292#[cfg_attr(feature = "python", pymethods)]
293impl GroundStation {
294 #[classmethod]
299 pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
300 match Self::from_der(data) {
301 Ok(obj) => Ok(obj),
302 Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
303 }
304 }
305
306 pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
310 let mut buf = Vec::new();
311 match self.encode_to_vec(&mut buf) {
312 Ok(_) => Ok(PyBytes::new(py, &buf)),
313 Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
314 }
315 }
316}
317
318impl Default for GroundStation {
319 fn default() -> Self {
320 let mut measurement_types = IndexSet::new();
321 measurement_types.insert(MeasurementType::Range);
322 measurement_types.insert(MeasurementType::Doppler);
323 Self {
324 name: "UNDEFINED".to_string(),
325 measurement_types,
326 location: Location::default(),
327 doppler_config: None,
328 light_time_correction: false,
329 timestamp_noise_s: None,
330 stochastic_noises: None,
331 obstructing_body: None,
332 relativistic_corrections: false,
333 }
334 }
335}
336
337impl fmt::Display for GroundStation {
338 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
340 write!(f, "{} ({})", self.name, self.location)
341 }
342}
343
344#[cfg(test)]
345mod gs_ut {
346
347 use anise::astro::{Location, TerrainMask};
348 use anise::constants::frames::IAU_EARTH_FRAME;
349 use indexmap::{IndexMap, IndexSet};
350
351 use crate::io::ConfigRepr;
352 use crate::od::prelude::*;
353
354 #[ignore = "github cache is a pain, works locally"]
355 #[test]
356 fn test_load_single() {
357 use std::env;
358 use std::path::PathBuf;
359
360 use hifitime::TimeUnits;
361
362 let test_data: PathBuf = [
363 env!("CARGO_MANIFEST_DIR"),
364 "../data",
365 "03_tests",
366 "config",
367 "one_ground_station.yaml",
368 ]
369 .iter()
370 .collect();
371
372 assert!(test_data.exists(), "Could not find the test data");
373
374 let gs = GroundStation::load(test_data).unwrap();
375
376 dbg!(&gs);
377
378 let mut measurement_types = IndexSet::new();
379 measurement_types.insert(MeasurementType::Range);
380 measurement_types.insert(MeasurementType::Doppler);
381
382 let mut stochastics = IndexMap::new();
383 stochastics.insert(
384 MeasurementType::Range,
385 StochasticNoise {
386 bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
387 ..Default::default()
388 },
389 );
390 stochastics.insert(
391 MeasurementType::Doppler,
392 StochasticNoise {
393 bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
394 ..Default::default()
395 },
396 );
397
398 let expected_gs = GroundStation {
399 name: "Demo ground station".to_string(),
400 location: Location {
401 latitude_deg: 2.3522,
402 longitude_deg: 48.8566,
403 height_km: 0.4,
404 frame: IAU_EARTH_FRAME.into(),
405 terrain_mask: TerrainMask::from_flat_terrain(5.0),
406 terrain_mask_ignored: false,
407 },
408 measurement_types,
409 stochastic_noises: Some(stochastics),
410
411 light_time_correction: false,
412 timestamp_noise_s: None,
413 doppler_config: Some(DopplerConfig::default()),
414 obstructing_body: None,
415 relativistic_corrections: false,
416 };
417
418 println!("{}", serde_yml::to_string(&expected_gs).unwrap());
419
420 assert_eq!(expected_gs, gs);
421 }
422
423 #[ignore = "github cache is a pain, works locally"]
424 #[test]
425 fn test_load_many() {
426 use hifitime::TimeUnits;
427 use std::env;
428 use std::path::PathBuf;
429
430 let test_file: PathBuf = [
431 env!("CARGO_MANIFEST_DIR"),
432 "../data",
433 "03_tests",
434 "config",
435 "many_ground_stations.yaml",
436 ]
437 .iter()
438 .collect();
439
440 let stations = GroundStation::load_many(test_file).unwrap();
441
442 dbg!(&stations);
443
444 let mut measurement_types = IndexSet::new();
445 measurement_types.insert(MeasurementType::Range);
446 measurement_types.insert(MeasurementType::Doppler);
447
448 let mut stochastics = IndexMap::new();
449 stochastics.insert(
450 MeasurementType::Range,
451 StochasticNoise {
452 bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
453 ..Default::default()
454 },
455 );
456 stochastics.insert(
457 MeasurementType::Doppler,
458 StochasticNoise {
459 bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
460 ..Default::default()
461 },
462 );
463
464 let expected = vec![
465 GroundStation {
466 name: "Demo ground station".to_string(),
467 location: Location {
468 latitude_deg: 2.3522,
469 longitude_deg: 48.8566,
470 height_km: 0.4,
471 frame: IAU_EARTH_FRAME.into(),
472 terrain_mask: TerrainMask::from_flat_terrain(5.0),
473 terrain_mask_ignored: false,
474 },
475 measurement_types: measurement_types.clone(),
476 stochastic_noises: Some(stochastics.clone()),
477 light_time_correction: false,
478 timestamp_noise_s: None,
479 doppler_config: None,
480 obstructing_body: None,
481 relativistic_corrections: false,
482 },
483 GroundStation {
484 name: "Canberra".to_string(),
485 location: Location {
486 latitude_deg: -35.398333,
487 longitude_deg: 148.981944,
488 height_km: 0.691750,
489 frame: IAU_EARTH_FRAME.into(),
490 terrain_mask: TerrainMask::from_flat_terrain(5.0),
491 terrain_mask_ignored: false,
492 },
493 measurement_types,
494 stochastic_noises: Some(stochastics),
495 light_time_correction: false,
496 timestamp_noise_s: None,
497 doppler_config: None,
498 obstructing_body: None,
499 relativistic_corrections: false,
500 },
501 ];
502
503 assert_eq!(expected, stations);
504
505 let reser = serde_yml::to_string(&expected).unwrap();
507 dbg!(reser);
508 }
509}