1use anise::astro::{Aberration, AzElRange, Location};
20use anise::errors::{AlmanacError, AlmanacResult};
21use anise::prelude::{Almanac, Frame, Orbit};
22use der::{Decode, Encode, Reader};
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::{Deserialize, Serialize};
35use std::fmt::{self, Debug};
36
37pub mod builtin;
38pub mod trk_device;
39
40#[cfg(feature = "python")]
41use pyo3::exceptions::PyValueError;
42#[cfg(feature = "python")]
43use pyo3::prelude::*;
44#[cfg(feature = "python")]
45use pyo3::types::{PyBytes, PyType};
46#[cfg(feature = "python")]
47mod python;
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
58#[cfg_attr(feature = "python", pyclass(from_py_object))]
59pub struct GroundStation {
60 pub name: String,
61 pub location: Location,
62 pub measurement_types: IndexSet<MeasurementType>,
63 pub integration_time: Option<Duration>,
65 pub light_time_correction: bool,
67 pub timestamp_noise_s: Option<StochasticNoise>,
69 pub stochastic_noises: Option<IndexMap<MeasurementType, StochasticNoise>>,
70}
71
72#[cfg_attr(feature = "python", pymethods)]
73impl GroundStation {
74 pub fn azimuth_elevation_of(
82 &self,
83 rx: Orbit,
84 obstructing_body: Option<Frame>,
85 almanac: &Almanac,
86 ) -> AlmanacResult<AzElRange> {
87 let ab_corr = if self.light_time_correction {
88 Aberration::LT
89 } else {
90 Aberration::NONE
91 };
92 almanac.azimuth_elevation_range_sez(
93 rx,
94 self.to_orbit(rx.epoch, almanac)?,
95 obstructing_body,
96 ab_corr,
97 )
98 }
99
100 pub fn to_orbit(&self, epoch: Epoch, almanac: &Almanac) -> AlmanacResult<Orbit> {
106 Orbit::try_latlongalt(
107 self.location.latitude_deg,
108 self.location.longitude_deg,
109 self.location.height_km,
110 epoch,
111 almanac.frame_info(self.location.frame).map_err(|source| {
112 AlmanacError::GenericError {
113 err: source.to_string(),
114 }
115 })?,
116 )
117 .map_err(|source| AlmanacError::AlmanacPhysics {
118 action: "building ground station location",
119 source: Box::new(source),
120 })
121 }
122}
123
124impl GroundStation {
125 pub fn from_point(
128 name: String,
129 latitude_deg: f64,
130 longitude_deg: f64,
131 height_km: f64,
132 frame: Frame,
133 ) -> Self {
134 Self {
135 name,
136 location: Location {
137 latitude_deg,
138 longitude_deg,
139 height_km,
140 frame: frame.into(),
141 terrain_mask: vec![],
142 terrain_mask_ignored: true,
143 },
144 measurement_types: IndexSet::new(),
145 integration_time: None,
146 light_time_correction: false,
147 timestamp_noise_s: None,
148 stochastic_noises: None,
149 }
150 }
151
152 pub fn with_msr_type(mut self, msr_type: MeasurementType, noise: StochasticNoise) -> Self {
154 if self.stochastic_noises.is_none() {
155 self.stochastic_noises = Some(IndexMap::new());
156 }
157
158 self.stochastic_noises
159 .as_mut()
160 .unwrap()
161 .insert(msr_type, noise);
162
163 self.measurement_types.insert(msr_type);
164
165 self
166 }
167
168 pub fn without_msr_type(mut self, msr_type: MeasurementType) -> Self {
170 if let Some(noises) = self.stochastic_noises.as_mut() {
171 noises.swap_remove(&msr_type);
172 }
173
174 self.measurement_types.swap_remove(&msr_type);
175
176 self
177 }
178
179 pub fn with_integration_time(mut self, integration_time: Option<Duration>) -> Self {
180 self.integration_time = integration_time;
181
182 self
183 }
184
185 pub fn with_msr_bias_constant(
187 mut self,
188 msr_type: MeasurementType,
189 bias_constant: f64,
190 ) -> Result<Self, ODError> {
191 if self.stochastic_noises.is_none() {
192 self.stochastic_noises = Some(IndexMap::new());
193 }
194
195 let stochastics = self.stochastic_noises.as_mut().unwrap();
196
197 let this_noise = stochastics
198 .get_mut(&msr_type)
199 .ok_or(ODError::NoiseNotConfigured {
200 kind: format!("{msr_type:?}"),
201 })
202 .unwrap();
203
204 if this_noise.bias.is_none() {
205 this_noise.bias = Some(GaussMarkov::ZERO);
206 }
207
208 this_noise.bias.unwrap().constant = Some(bias_constant);
209
210 Ok(self)
211 }
212
213 fn noises(&mut self, epoch: Epoch, rng: Option<&mut Pcg64Mcg>) -> Result<Vec<f64>, ODError> {
215 let mut noises = vec![0.0; self.measurement_types.len() + 1];
216
217 if let Some(rng) = rng {
218 ensure!(
219 self.stochastic_noises.is_some(),
220 NoiseNotConfiguredSnafu {
221 kind: "ground station stochastics".to_string(),
222 }
223 );
224 if let Some(mut timestamp_noise) = self.timestamp_noise_s {
227 noises[0] = timestamp_noise.sample(epoch, rng);
228 }
229
230 let stochastics = self.stochastic_noises.as_mut().unwrap();
231
232 for (ii, msr_type) in self.measurement_types.iter().enumerate() {
233 noises[ii + 1] = stochastics
234 .get_mut(msr_type)
235 .ok_or(ODError::NoiseNotConfigured {
236 kind: format!("{msr_type:?}"),
237 })?
238 .sample(epoch, rng);
239 }
240 }
241
242 Ok(noises)
243 }
244
245 fn available_data(&self) -> u8 {
246 let mut bits: u8 = 0;
247
248 if self.integration_time.is_some() {
249 bits |= 1 << 0;
250 }
251 if self.timestamp_noise_s.is_some() {
252 bits |= 1 << 1;
253 }
254 if self.stochastic_noises.is_some() {
255 bits |= 1 << 2;
256 }
257 bits
258 }
259}
260
261#[cfg(feature = "python")]
262#[cfg_attr(feature = "python", pymethods)]
263impl GroundStation {
264 #[classmethod]
269 pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
270 match Self::from_der(data) {
271 Ok(obj) => Ok(obj),
272 Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
273 }
274 }
275
276 pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
280 let mut buf = Vec::new();
281 match self.encode_to_vec(&mut buf) {
282 Ok(_) => Ok(PyBytes::new(py, &buf)),
283 Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
284 }
285 }
286}
287
288impl Default for GroundStation {
289 fn default() -> Self {
290 let mut measurement_types = IndexSet::new();
291 measurement_types.insert(MeasurementType::Range);
292 measurement_types.insert(MeasurementType::Doppler);
293 Self {
294 name: "UNDEFINED".to_string(),
295 measurement_types,
296 location: Location::default(),
297 integration_time: None,
298 light_time_correction: false,
299 timestamp_noise_s: None,
300 stochastic_noises: None,
301 }
302 }
303}
304
305impl ConfigRepr for GroundStation {}
306
307#[derive(der::Sequence)]
308struct MsrNoisePair {
309 msr_type: MeasurementType,
310 noise: StochasticNoise,
311}
312
313impl<'a> Decode<'a> for GroundStation {
314 fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
315 let name: String = decoder.decode()?;
316 let location = decoder.decode()?;
317 let msr_types_vec: Vec<MeasurementType> = decoder.decode()?;
319 let measurement_types = IndexSet::from_iter(msr_types_vec);
320
321 let light_time_correction = decoder.decode()?;
322
323 let flags: u8 = decoder.decode()?;
325
326 let integration_time = if flags & (1 << 0) != 0 {
327 Some(Duration::from_total_nanoseconds(decoder.decode()?))
328 } else {
329 None
330 };
331
332 let timestamp_noise_s = if flags & (1 << 1) != 0 {
333 Some(decoder.decode()?)
334 } else {
335 None
336 };
337
338 let stochastic_noises = if flags & (1 << 2) != 0 {
339 let stochastics_vec: Vec<MsrNoisePair> = decoder.decode()?;
343 let mut map = IndexMap::new();
344 for pair in stochastics_vec {
345 map.insert(pair.msr_type, pair.noise);
346 }
347 Some(map)
348 } else {
349 None
350 };
351
352 Ok(GroundStation {
353 name,
354 location,
355 measurement_types,
356 integration_time,
357 light_time_correction,
358 timestamp_noise_s,
359 stochastic_noises,
360 })
361 }
362}
363
364impl Encode for GroundStation {
365 fn encoded_len(&self) -> der::Result<der::Length> {
366 let msr_types_vec: Vec<MeasurementType> = self.measurement_types.iter().copied().collect();
367
368 let integration_time_ns = self.integration_time.map(|d| d.total_nanoseconds());
369
370 let stochastics_vec = self.stochastic_noises.as_ref().map(|map| {
371 map.iter()
372 .map(|(k, v)| MsrNoisePair {
373 msr_type: *k,
374 noise: *v,
375 })
376 .collect::<Vec<MsrNoisePair>>()
377 });
378
379 self.name.encoded_len()?
380 + self.location.encoded_len()?
381 + msr_types_vec.encoded_len()?
382 + self.light_time_correction.encoded_len()?
383 + self.available_data().encoded_len()?
384 + integration_time_ns.encoded_len()?
385 + self.timestamp_noise_s.encoded_len()?
386 + stochastics_vec.encoded_len()?
387 }
388
389 fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
390 self.name.encode(encoder)?;
391 self.location.encode(encoder)?;
392
393 let msr_types_vec: Vec<MeasurementType> = self.measurement_types.iter().copied().collect();
394 msr_types_vec.encode(encoder)?;
395
396 self.light_time_correction.encode(encoder)?;
397 self.available_data().encode(encoder)?;
398
399 let integration_time_ns = self.integration_time.map(|d| d.total_nanoseconds());
400 integration_time_ns.encode(encoder)?;
401
402 self.timestamp_noise_s.encode(encoder)?;
403
404 let stochastics_vec = self.stochastic_noises.as_ref().map(|map| {
405 map.iter()
406 .map(|(k, v)| MsrNoisePair {
407 msr_type: *k,
408 noise: *v,
409 })
410 .collect::<Vec<MsrNoisePair>>()
411 });
412 stochastics_vec.encode(encoder)?;
413
414 Ok(())
415 }
416}
417
418impl fmt::Display for GroundStation {
419 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
421 write!(f, "{} ({})", self.name, self.location)
422 }
423}
424
425#[cfg(test)]
426mod gs_ut {
427
428 use anise::astro::{Location, TerrainMask};
429 use anise::constants::frames::IAU_EARTH_FRAME;
430 use indexmap::{IndexMap, IndexSet};
431
432 use crate::io::ConfigRepr;
433 use crate::od::prelude::*;
434
435 #[test]
436 fn test_load_single() {
437 use std::env;
438 use std::path::PathBuf;
439
440 use hifitime::TimeUnits;
441
442 let test_data: PathBuf = [
443 env!("CARGO_MANIFEST_DIR"),
444 "../data",
445 "03_tests",
446 "config",
447 "one_ground_station.yaml",
448 ]
449 .iter()
450 .collect();
451
452 assert!(test_data.exists(), "Could not find the test data");
453
454 let gs = GroundStation::load(test_data).unwrap();
455
456 dbg!(&gs);
457
458 let mut measurement_types = IndexSet::new();
459 measurement_types.insert(MeasurementType::Range);
460 measurement_types.insert(MeasurementType::Doppler);
461
462 let mut stochastics = IndexMap::new();
463 stochastics.insert(
464 MeasurementType::Range,
465 StochasticNoise {
466 bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
467 ..Default::default()
468 },
469 );
470 stochastics.insert(
471 MeasurementType::Doppler,
472 StochasticNoise {
473 bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
474 ..Default::default()
475 },
476 );
477
478 let expected_gs = GroundStation {
479 name: "Demo ground station".to_string(),
480 location: Location {
481 latitude_deg: 2.3522,
482 longitude_deg: 48.8566,
483 height_km: 0.4,
484 frame: IAU_EARTH_FRAME.into(),
485 terrain_mask: TerrainMask::from_flat_terrain(5.0),
486 terrain_mask_ignored: false,
487 },
488 measurement_types,
489 stochastic_noises: Some(stochastics),
490
491 light_time_correction: false,
492 timestamp_noise_s: None,
493 integration_time: Some(60 * Unit::Second),
494 };
495
496 println!("{}", serde_yml::to_string(&expected_gs).unwrap());
497
498 assert_eq!(expected_gs, gs);
499 }
500
501 #[test]
502 fn test_load_many() {
503 use hifitime::TimeUnits;
504 use std::env;
505 use std::path::PathBuf;
506
507 let test_file: PathBuf = [
508 env!("CARGO_MANIFEST_DIR"),
509 "../data",
510 "03_tests",
511 "config",
512 "many_ground_stations.yaml",
513 ]
514 .iter()
515 .collect();
516
517 let stations = GroundStation::load_many(test_file).unwrap();
518
519 dbg!(&stations);
520
521 let mut measurement_types = IndexSet::new();
522 measurement_types.insert(MeasurementType::Range);
523 measurement_types.insert(MeasurementType::Doppler);
524
525 let mut stochastics = IndexMap::new();
526 stochastics.insert(
527 MeasurementType::Range,
528 StochasticNoise {
529 bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
530 ..Default::default()
531 },
532 );
533 stochastics.insert(
534 MeasurementType::Doppler,
535 StochasticNoise {
536 bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
537 ..Default::default()
538 },
539 );
540
541 let expected = vec![
542 GroundStation {
543 name: "Demo ground station".to_string(),
544 location: Location {
545 latitude_deg: 2.3522,
546 longitude_deg: 48.8566,
547 height_km: 0.4,
548 frame: IAU_EARTH_FRAME.into(),
549 terrain_mask: TerrainMask::from_flat_terrain(5.0),
550 terrain_mask_ignored: false,
551 },
552 measurement_types: measurement_types.clone(),
553 stochastic_noises: Some(stochastics.clone()),
554 light_time_correction: false,
555 timestamp_noise_s: None,
556 integration_time: None,
557 },
558 GroundStation {
559 name: "Canberra".to_string(),
560 location: Location {
561 latitude_deg: -35.398333,
562 longitude_deg: 148.981944,
563 height_km: 0.691750,
564 frame: IAU_EARTH_FRAME.into(),
565 terrain_mask: TerrainMask::from_flat_terrain(5.0),
566 terrain_mask_ignored: false,
567 },
568 measurement_types,
569 stochastic_noises: Some(stochastics),
570 light_time_correction: false,
571 timestamp_noise_s: None,
572 integration_time: None,
573 },
574 ];
575
576 assert_eq!(expected, stations);
577
578 let reser = serde_yml::to_string(&expected).unwrap();
580 dbg!(reser);
581 }
582}