use anise::astro::{Aberration, AzElRange, Location};
use anise::errors::{AlmanacError, AlmanacResult};
use anise::prelude::{Almanac, Frame, Orbit};
use der::{Decode, Encode, Reader};
use indexmap::{IndexMap, IndexSet};
use snafu::ensure;
use super::msr::MeasurementType;
use super::noise::{GaussMarkov, StochasticNoise};
use super::{ODAlmanacSnafu, ODError, ODTrajSnafu, TrackingDevice};
use crate::io::ConfigRepr;
use crate::od::NoiseNotConfiguredSnafu;
use crate::time::Epoch;
use hifitime::Duration;
use rand_pcg::Pcg64Mcg;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Debug};
pub mod builtin;
pub mod trk_device;
#[cfg(feature = "python")]
use pyo3::exceptions::PyValueError;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
use pyo3::types::{PyBytes, PyType};
#[cfg(feature = "python")]
mod python;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "python", pyclass(from_py_object))]
pub struct GroundStation {
pub name: String,
pub location: Location,
pub measurement_types: IndexSet<MeasurementType>,
pub integration_time: Option<Duration>,
pub light_time_correction: bool,
pub timestamp_noise_s: Option<StochasticNoise>,
pub stochastic_noises: Option<IndexMap<MeasurementType, StochasticNoise>>,
}
#[cfg_attr(feature = "python", pymethods)]
impl GroundStation {
pub fn azimuth_elevation_of(
&self,
rx: Orbit,
obstructing_body: Option<Frame>,
almanac: &Almanac,
) -> AlmanacResult<AzElRange> {
let ab_corr = if self.light_time_correction {
Aberration::LT
} else {
Aberration::NONE
};
almanac.azimuth_elevation_range_sez(
rx,
self.to_orbit(rx.epoch, almanac)?,
obstructing_body,
ab_corr,
)
}
pub fn to_orbit(&self, epoch: Epoch, almanac: &Almanac) -> AlmanacResult<Orbit> {
Orbit::try_latlongalt(
self.location.latitude_deg,
self.location.longitude_deg,
self.location.height_km,
epoch,
almanac.frame_info(self.location.frame).map_err(|source| {
AlmanacError::GenericError {
err: source.to_string(),
}
})?,
)
.map_err(|source| AlmanacError::AlmanacPhysics {
action: "building ground station location",
source: Box::new(source),
})
}
}
impl GroundStation {
pub fn from_point(
name: String,
latitude_deg: f64,
longitude_deg: f64,
height_km: f64,
frame: Frame,
) -> Self {
Self {
name,
location: Location {
latitude_deg,
longitude_deg,
height_km,
frame: frame.into(),
terrain_mask: vec![],
terrain_mask_ignored: true,
},
measurement_types: IndexSet::new(),
integration_time: None,
light_time_correction: false,
timestamp_noise_s: None,
stochastic_noises: None,
}
}
pub fn with_msr_type(mut self, msr_type: MeasurementType, noise: StochasticNoise) -> Self {
if self.stochastic_noises.is_none() {
self.stochastic_noises = Some(IndexMap::new());
}
self.stochastic_noises
.as_mut()
.unwrap()
.insert(msr_type, noise);
self.measurement_types.insert(msr_type);
self
}
pub fn without_msr_type(mut self, msr_type: MeasurementType) -> Self {
if let Some(noises) = self.stochastic_noises.as_mut() {
noises.swap_remove(&msr_type);
}
self.measurement_types.swap_remove(&msr_type);
self
}
pub fn with_integration_time(mut self, integration_time: Option<Duration>) -> Self {
self.integration_time = integration_time;
self
}
pub fn with_msr_bias_constant(
mut self,
msr_type: MeasurementType,
bias_constant: f64,
) -> Result<Self, ODError> {
if self.stochastic_noises.is_none() {
self.stochastic_noises = Some(IndexMap::new());
}
let stochastics = self.stochastic_noises.as_mut().unwrap();
let this_noise = stochastics
.get_mut(&msr_type)
.ok_or(ODError::NoiseNotConfigured {
kind: format!("{msr_type:?}"),
})
.unwrap();
if this_noise.bias.is_none() {
this_noise.bias = Some(GaussMarkov::ZERO);
}
this_noise.bias.unwrap().constant = Some(bias_constant);
Ok(self)
}
fn noises(&mut self, epoch: Epoch, rng: Option<&mut Pcg64Mcg>) -> Result<Vec<f64>, ODError> {
let mut noises = vec![0.0; self.measurement_types.len() + 1];
if let Some(rng) = rng {
ensure!(
self.stochastic_noises.is_some(),
NoiseNotConfiguredSnafu {
kind: "ground station stochastics".to_string(),
}
);
if let Some(mut timestamp_noise) = self.timestamp_noise_s {
noises[0] = timestamp_noise.sample(epoch, rng);
}
let stochastics = self.stochastic_noises.as_mut().unwrap();
for (ii, msr_type) in self.measurement_types.iter().enumerate() {
noises[ii + 1] = stochastics
.get_mut(msr_type)
.ok_or(ODError::NoiseNotConfigured {
kind: format!("{msr_type:?}"),
})?
.sample(epoch, rng);
}
}
Ok(noises)
}
fn available_data(&self) -> u8 {
let mut bits: u8 = 0;
if self.integration_time.is_some() {
bits |= 1 << 0;
}
if self.timestamp_noise_s.is_some() {
bits |= 1 << 1;
}
if self.stochastic_noises.is_some() {
bits |= 1 << 2;
}
bits
}
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl GroundStation {
#[classmethod]
pub fn from_asn1(_cls: &Bound<'_, PyType>, data: &[u8]) -> PyResult<Self> {
match Self::from_der(data) {
Ok(obj) => Ok(obj),
Err(e) => Err(PyValueError::new_err(format!("ASN.1 decoding error: {e}"))),
}
}
pub fn to_asn1<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
let mut buf = Vec::new();
match self.encode_to_vec(&mut buf) {
Ok(_) => Ok(PyBytes::new(py, &buf)),
Err(e) => Err(PyValueError::new_err(format!("ASN.1 encoding error: {e}"))),
}
}
}
impl Default for GroundStation {
fn default() -> Self {
let mut measurement_types = IndexSet::new();
measurement_types.insert(MeasurementType::Range);
measurement_types.insert(MeasurementType::Doppler);
Self {
name: "UNDEFINED".to_string(),
measurement_types,
location: Location::default(),
integration_time: None,
light_time_correction: false,
timestamp_noise_s: None,
stochastic_noises: None,
}
}
}
impl ConfigRepr for GroundStation {}
#[derive(der::Sequence)]
struct MsrNoisePair {
msr_type: MeasurementType,
noise: StochasticNoise,
}
impl<'a> Decode<'a> for GroundStation {
fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
let name: String = decoder.decode()?;
let location = decoder.decode()?;
let msr_types_vec: Vec<MeasurementType> = decoder.decode()?;
let measurement_types = IndexSet::from_iter(msr_types_vec);
let light_time_correction = decoder.decode()?;
let flags: u8 = decoder.decode()?;
let integration_time = if flags & (1 << 0) != 0 {
Some(Duration::from_total_nanoseconds(decoder.decode()?))
} else {
None
};
let timestamp_noise_s = if flags & (1 << 1) != 0 {
Some(decoder.decode()?)
} else {
None
};
let stochastic_noises = if flags & (1 << 2) != 0 {
let stochastics_vec: Vec<MsrNoisePair> = decoder.decode()?;
let mut map = IndexMap::new();
for pair in stochastics_vec {
map.insert(pair.msr_type, pair.noise);
}
Some(map)
} else {
None
};
Ok(GroundStation {
name,
location,
measurement_types,
integration_time,
light_time_correction,
timestamp_noise_s,
stochastic_noises,
})
}
}
impl Encode for GroundStation {
fn encoded_len(&self) -> der::Result<der::Length> {
let msr_types_vec: Vec<MeasurementType> = self.measurement_types.iter().copied().collect();
let integration_time_ns = self.integration_time.map(|d| d.total_nanoseconds());
let stochastics_vec = self.stochastic_noises.as_ref().map(|map| {
map.iter()
.map(|(k, v)| MsrNoisePair {
msr_type: *k,
noise: *v,
})
.collect::<Vec<MsrNoisePair>>()
});
self.name.encoded_len()?
+ self.location.encoded_len()?
+ msr_types_vec.encoded_len()?
+ self.light_time_correction.encoded_len()?
+ self.available_data().encoded_len()?
+ integration_time_ns.encoded_len()?
+ self.timestamp_noise_s.encoded_len()?
+ stochastics_vec.encoded_len()?
}
fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
self.name.encode(encoder)?;
self.location.encode(encoder)?;
let msr_types_vec: Vec<MeasurementType> = self.measurement_types.iter().copied().collect();
msr_types_vec.encode(encoder)?;
self.light_time_correction.encode(encoder)?;
self.available_data().encode(encoder)?;
let integration_time_ns = self.integration_time.map(|d| d.total_nanoseconds());
integration_time_ns.encode(encoder)?;
self.timestamp_noise_s.encode(encoder)?;
let stochastics_vec = self.stochastic_noises.as_ref().map(|map| {
map.iter()
.map(|(k, v)| MsrNoisePair {
msr_type: *k,
noise: *v,
})
.collect::<Vec<MsrNoisePair>>()
});
stochastics_vec.encode(encoder)?;
Ok(())
}
}
impl fmt::Display for GroundStation {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} ({})", self.name, self.location)
}
}
#[cfg(test)]
mod gs_ut {
use anise::astro::{Location, TerrainMask};
use anise::constants::frames::IAU_EARTH_FRAME;
use indexmap::{IndexMap, IndexSet};
use crate::io::ConfigRepr;
use crate::od::prelude::*;
#[test]
fn test_load_single() {
use std::env;
use std::path::PathBuf;
use hifitime::TimeUnits;
let test_data: PathBuf = [
env!("CARGO_MANIFEST_DIR"),
"../data",
"03_tests",
"config",
"one_ground_station.yaml",
]
.iter()
.collect();
assert!(test_data.exists(), "Could not find the test data");
let gs = GroundStation::load(test_data).unwrap();
dbg!(&gs);
let mut measurement_types = IndexSet::new();
measurement_types.insert(MeasurementType::Range);
measurement_types.insert(MeasurementType::Doppler);
let mut stochastics = IndexMap::new();
stochastics.insert(
MeasurementType::Range,
StochasticNoise {
bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
..Default::default()
},
);
stochastics.insert(
MeasurementType::Doppler,
StochasticNoise {
bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
..Default::default()
},
);
let expected_gs = GroundStation {
name: "Demo ground station".to_string(),
location: Location {
latitude_deg: 2.3522,
longitude_deg: 48.8566,
height_km: 0.4,
frame: IAU_EARTH_FRAME.into(),
terrain_mask: TerrainMask::from_flat_terrain(5.0),
terrain_mask_ignored: false,
},
measurement_types,
stochastic_noises: Some(stochastics),
light_time_correction: false,
timestamp_noise_s: None,
integration_time: Some(60 * Unit::Second),
};
println!("{}", serde_yml::to_string(&expected_gs).unwrap());
assert_eq!(expected_gs, gs);
}
#[test]
fn test_load_many() {
use hifitime::TimeUnits;
use std::env;
use std::path::PathBuf;
let test_file: PathBuf = [
env!("CARGO_MANIFEST_DIR"),
"../data",
"03_tests",
"config",
"many_ground_stations.yaml",
]
.iter()
.collect();
let stations = GroundStation::load_many(test_file).unwrap();
dbg!(&stations);
let mut measurement_types = IndexSet::new();
measurement_types.insert(MeasurementType::Range);
measurement_types.insert(MeasurementType::Doppler);
let mut stochastics = IndexMap::new();
stochastics.insert(
MeasurementType::Range,
StochasticNoise {
bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
..Default::default()
},
);
stochastics.insert(
MeasurementType::Doppler,
StochasticNoise {
bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
..Default::default()
},
);
let expected = vec![
GroundStation {
name: "Demo ground station".to_string(),
location: Location {
latitude_deg: 2.3522,
longitude_deg: 48.8566,
height_km: 0.4,
frame: IAU_EARTH_FRAME.into(),
terrain_mask: TerrainMask::from_flat_terrain(5.0),
terrain_mask_ignored: false,
},
measurement_types: measurement_types.clone(),
stochastic_noises: Some(stochastics.clone()),
light_time_correction: false,
timestamp_noise_s: None,
integration_time: None,
},
GroundStation {
name: "Canberra".to_string(),
location: Location {
latitude_deg: -35.398333,
longitude_deg: 148.981944,
height_km: 0.691750,
frame: IAU_EARTH_FRAME.into(),
terrain_mask: TerrainMask::from_flat_terrain(5.0),
terrain_mask_ignored: false,
},
measurement_types,
stochastic_noises: Some(stochastics),
light_time_correction: false,
timestamp_noise_s: None,
integration_time: None,
},
];
assert_eq!(expected, stations);
let reser = serde_yml::to_string(&expected).unwrap();
dbg!(reser);
}
}