use anise::astro::{Aberration, AzElRange, PhysicsResult};
use anise::errors::AlmanacResult;
use anise::prelude::{Almanac, Frame, Orbit};
use super::msr::RangeDoppler;
use super::noise::StochasticNoise;
use super::{ODAlmanacSnafu, ODError, ODTrajSnafu, TrackingDeviceSim};
use crate::errors::EventError;
use crate::io::ConfigRepr;
use crate::md::prelude::{Interpolatable, Traj};
use crate::md::EventEvaluator;
use crate::time::Epoch;
use crate::Spacecraft;
use hifitime::{Duration, Unit};
use nalgebra::{allocator::Allocator, DefaultAllocator, OMatrix};
use rand_pcg::Pcg64Mcg;
use serde_derive::{Deserialize, Serialize};
use snafu::ResultExt;
use std::fmt;
use std::sync::Arc;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3(module = "nyx_space.orbit_determination"))]
pub struct GroundStation {
pub name: String,
pub elevation_mask_deg: f64,
pub latitude_deg: f64,
pub longitude_deg: f64,
pub height_km: f64,
pub frame: Frame,
#[serde(skip)]
pub integration_time: Option<Duration>,
pub light_time_correction: bool,
pub timestamp_noise_s: Option<StochasticNoise>,
pub range_noise_km: Option<StochasticNoise>,
pub doppler_noise_km_s: Option<StochasticNoise>,
}
impl GroundStation {
pub fn from_point(
name: String,
latitude_deg: f64,
longitude_deg: f64,
height_km: f64,
frame: Frame,
) -> Self {
Self {
name,
elevation_mask_deg: 0.0,
latitude_deg,
longitude_deg,
height_km,
frame,
integration_time: None,
light_time_correction: false,
timestamp_noise_s: None,
range_noise_km: None,
doppler_noise_km_s: None,
}
}
pub fn dss65_madrid(
elevation_mask: f64,
range_noise_km: StochasticNoise,
doppler_noise_km_s: StochasticNoise,
iau_earth: Frame,
) -> Self {
Self {
name: "Madrid".to_string(),
elevation_mask_deg: elevation_mask,
latitude_deg: 40.427_222,
longitude_deg: 4.250_556,
height_km: 0.834_939,
frame: iau_earth,
integration_time: None,
light_time_correction: false,
timestamp_noise_s: None,
range_noise_km: Some(range_noise_km),
doppler_noise_km_s: Some(doppler_noise_km_s),
}
}
pub fn dss34_canberra(
elevation_mask: f64,
range_noise_km: StochasticNoise,
doppler_noise_km_s: StochasticNoise,
iau_earth: Frame,
) -> Self {
Self {
name: "Canberra".to_string(),
elevation_mask_deg: elevation_mask,
latitude_deg: -35.398_333,
longitude_deg: 148.981_944,
height_km: 0.691_750,
frame: iau_earth,
integration_time: None,
light_time_correction: false,
timestamp_noise_s: None,
range_noise_km: Some(range_noise_km),
doppler_noise_km_s: Some(doppler_noise_km_s),
}
}
pub fn dss13_goldstone(
elevation_mask: f64,
range_noise_km: StochasticNoise,
doppler_noise_km_s: StochasticNoise,
iau_earth: Frame,
) -> Self {
Self {
name: "Goldstone".to_string(),
elevation_mask_deg: elevation_mask,
latitude_deg: 35.247_164,
longitude_deg: 243.205,
height_km: 1.071_149_04,
frame: iau_earth,
integration_time: None,
light_time_correction: false,
timestamp_noise_s: None,
range_noise_km: Some(range_noise_km),
doppler_noise_km_s: Some(doppler_noise_km_s),
}
}
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).unwrap(),
obstructing_body,
ab_corr,
)
}
pub fn to_orbit(&self, epoch: Epoch, almanac: &Almanac) -> PhysicsResult<Orbit> {
use anise::constants::usual_planetary_constants::MEAN_EARTH_ANGULAR_VELOCITY_DEG_S;
Orbit::try_latlongalt(
self.latitude_deg,
self.longitude_deg,
self.height_km,
MEAN_EARTH_ANGULAR_VELOCITY_DEG_S,
epoch,
almanac.frame_from_uid(self.frame).unwrap(),
)
}
fn noises(
&mut self,
epoch: Epoch,
rng: Option<&mut Pcg64Mcg>,
) -> Result<(f64, f64, f64), ODError> {
let timestamp_noise_s;
let range_noise_km;
let doppler_noise_km_s;
match rng {
Some(rng) => {
range_noise_km = self
.range_noise_km
.ok_or(ODError::NoiseNotConfigured { kind: "Range" })?
.sample(epoch, rng);
doppler_noise_km_s = self
.doppler_noise_km_s
.ok_or(ODError::NoiseNotConfigured { kind: "Doppler" })?
.sample(epoch, rng);
if let Some(mut timestamp_noise) = self.timestamp_noise_s {
timestamp_noise_s = timestamp_noise.sample(epoch, rng);
} else {
timestamp_noise_s = 0.0;
}
}
None => {
timestamp_noise_s = 0.0;
range_noise_km = 0.0;
doppler_noise_km_s = 0.0;
}
};
Ok((timestamp_noise_s, range_noise_km, doppler_noise_km_s))
}
}
impl ConfigRepr for GroundStation {}
impl TrackingDeviceSim<Spacecraft, RangeDoppler> for GroundStation {
fn measure(
&mut self,
epoch: Epoch,
traj: &Traj<Spacecraft>,
rng: Option<&mut Pcg64Mcg>,
almanac: Arc<Almanac>,
) -> Result<Option<RangeDoppler>, ODError> {
match self.integration_time {
Some(integration_time) => {
let rx_0 = traj.at(epoch - integration_time).context(ODTrajSnafu)?;
let rx_1 = traj.at(epoch).context(ODTrajSnafu)?;
let obstructing_body = if !self.frame.ephem_origin_match(rx_0.frame()) {
Some(rx_0.frame())
} else {
None
};
let aer_t0 = self
.azimuth_elevation_of(rx_0.orbit, obstructing_body, &almanac)
.context(ODAlmanacSnafu {
action: "computing AER",
})?;
let aer_t1 = self
.azimuth_elevation_of(rx_1.orbit, obstructing_body, &almanac)
.context(ODAlmanacSnafu {
action: "computing AER",
})?;
if aer_t0.elevation_deg < self.elevation_mask_deg
|| aer_t1.elevation_deg < self.elevation_mask_deg
{
debug!(
"{} (el. mask {:.3} deg) but object moves from {:.3} to {:.3} deg -- no measurement",
self.name, self.elevation_mask_deg, aer_t0.elevation_deg, aer_t1.elevation_deg
);
return Ok(None);
}
let (timestamp_noise_s, range_noise_km, doppler_noise_km_s) =
self.noises(epoch - integration_time * 0.5, rng)?;
Ok(Some(RangeDoppler::two_way(
aer_t0,
aer_t1,
timestamp_noise_s,
range_noise_km,
doppler_noise_km_s,
)))
}
None => self.measure_instantaneous(traj.at(epoch).context(ODTrajSnafu)?, rng, almanac),
}
}
fn name(&self) -> String {
self.name.clone()
}
fn location(&self, epoch: Epoch, frame: Frame, almanac: Arc<Almanac>) -> AlmanacResult<Orbit> {
almanac.transform_to(self.to_orbit(epoch, &almanac).unwrap(), frame, None)
}
fn measure_instantaneous(
&mut self,
rx: Spacecraft,
rng: Option<&mut Pcg64Mcg>,
almanac: Arc<Almanac>,
) -> Result<Option<RangeDoppler>, ODError> {
let obstructing_body = if !self.frame.ephem_origin_match(rx.frame()) {
Some(rx.frame())
} else {
None
};
let aer = self
.azimuth_elevation_of(rx.orbit, obstructing_body, &almanac)
.context(ODAlmanacSnafu {
action: "computing AER",
})?;
if aer.elevation_deg >= self.elevation_mask_deg {
let (timestamp_noise_s, range_noise_km, doppler_noise_km_s) =
self.noises(rx.orbit.epoch, rng)?;
Ok(Some(RangeDoppler::one_way(
aer,
timestamp_noise_s,
range_noise_km,
doppler_noise_km_s,
)))
} else {
debug!(
"{} {} (el. mask {:.3} deg), object at {:.3} deg -- no measurement",
self.name, rx.orbit.epoch, self.elevation_mask_deg, aer.elevation_deg
);
Ok(None)
}
}
fn measurement_covar(
&mut self,
epoch: Epoch,
) -> Result<
OMatrix<
f64,
<RangeDoppler as super::Measurement>::MeasurementSize,
<RangeDoppler as super::Measurement>::MeasurementSize,
>,
ODError,
> {
let range_noise_km2 = self
.range_noise_km
.ok_or(ODError::NoiseNotConfigured { kind: "Range" })?
.covariance(epoch);
let doppler_noise_km2_s2 = self
.doppler_noise_km_s
.ok_or(ODError::NoiseNotConfigured { kind: "Doppler" })?
.covariance(epoch);
let mut msr_noises = OMatrix::<
f64,
<RangeDoppler as super::Measurement>::MeasurementSize,
<RangeDoppler as super::Measurement>::MeasurementSize,
>::zeros();
msr_noises[(0, 0)] = range_noise_km2;
msr_noises[(1, 1)] = doppler_noise_km2_s2;
Ok(msr_noises)
}
}
impl fmt::Display for GroundStation {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{} (lat.: {:.4} deg long.: {:.4} deg alt.: {:.3} m) [{}]",
self.name,
self.latitude_deg,
self.longitude_deg,
self.height_km * 1e3,
self.frame,
)
}
}
impl<S: Interpolatable> EventEvaluator<S> for &GroundStation
where
DefaultAllocator: Allocator<S::Size> + Allocator<S::Size, S::Size> + Allocator<S::VecLength>,
{
fn eval(&self, rx_gs_frame: &S, almanac: Arc<Almanac>) -> Result<f64, EventError> {
let dt = rx_gs_frame.epoch();
let tx_gs_frame = self.to_orbit(dt, &almanac).unwrap();
let from = tx_gs_frame.frame.orientation_id * 1_000 + 1;
let dcm_topo2fixed = tx_gs_frame
.dcm_from_topocentric_to_body_fixed(from)
.unwrap()
.transpose();
let rx_sez = (dcm_topo2fixed * rx_gs_frame.orbit()).unwrap();
let tx_sez = (dcm_topo2fixed * tx_gs_frame).unwrap();
let rho_sez = (rx_sez - tx_sez).unwrap();
Ok(rho_sez.declination_deg() - self.elevation_mask_deg)
}
fn eval_string(&self, state: &S, almanac: Arc<Almanac>) -> Result<String, EventError> {
Ok(format!(
"Elevation from {} is {:.6} deg on {}",
self.name,
self.eval(state, almanac)? + self.elevation_mask_deg,
state.epoch()
))
}
fn epoch_precision(&self) -> Duration {
1 * Unit::Second
}
fn value_precision(&self) -> f64 {
1e-3
}
}
#[cfg(test)]
mod gs_ut {
use anise::constants::frames::IAU_EARTH_FRAME;
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::var("CARGO_MANIFEST_DIR").unwrap(),
"data".to_string(),
"tests".to_string(),
"config".to_string(),
"one_ground_station.yaml".to_string(),
]
.iter()
.collect();
assert!(test_data.exists(), "Could not find the test data");
let gs = GroundStation::load(test_data).unwrap();
dbg!(&gs);
let expected_gs = GroundStation {
name: "Demo ground station".to_string(),
frame: IAU_EARTH_FRAME,
elevation_mask_deg: 5.0,
range_noise_km: Some(StochasticNoise {
bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
..Default::default()
}),
doppler_noise_km_s: Some(StochasticNoise {
bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
..Default::default()
}),
latitude_deg: 2.3522,
longitude_deg: 48.8566,
height_km: 0.4,
light_time_correction: false,
timestamp_noise_s: None,
integration_time: None,
};
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::var("CARGO_MANIFEST_DIR").unwrap(),
"data".to_string(),
"tests".to_string(),
"config".to_string(),
"many_ground_stations.yaml".to_string(),
]
.iter()
.collect();
let stations = GroundStation::load_many(test_file).unwrap();
dbg!(&stations);
let expected = vec![
GroundStation {
name: "Demo ground station".to_string(),
frame: IAU_EARTH_FRAME.with_mu_km3_s2(398600.435436096),
elevation_mask_deg: 5.0,
range_noise_km: Some(StochasticNoise {
bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
..Default::default()
}),
doppler_noise_km_s: Some(StochasticNoise {
bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
..Default::default()
}),
latitude_deg: 2.3522,
longitude_deg: 48.8566,
height_km: 0.4,
light_time_correction: false,
timestamp_noise_s: None,
integration_time: None,
},
GroundStation {
name: "Canberra".to_string(),
frame: IAU_EARTH_FRAME.with_mu_km3_s2(398600.435436096),
elevation_mask_deg: 5.0,
range_noise_km: Some(StochasticNoise {
bias: Some(GaussMarkov::new(1.days(), 5e-3).unwrap()),
..Default::default()
}),
doppler_noise_km_s: Some(StochasticNoise {
bias: Some(GaussMarkov::new(1.days(), 5e-5).unwrap()),
..Default::default()
}),
latitude_deg: -35.398333,
longitude_deg: 148.981944,
height_km: 0.691750,
light_time_correction: false,
timestamp_noise_s: None,
integration_time: None,
},
];
assert_eq!(expected, stations);
let reser = serde_yaml::to_string(&expected).unwrap();
dbg!(reser);
}
}