nyx_space/od/groundpnt/
trk_device.rs1use anise::almanac::Almanac;
19use anise::ephemerides::EphemerisError;
20use anise::errors::{AlmanacError, AlmanacResult};
21use anise::math::interpolation::InterpolationError;
22use anise::prelude::{Frame, Orbit};
23use hifitime::{Epoch, TimeUnits};
24use indexmap::IndexSet;
25use rand_pcg::Pcg64Mcg;
26use snafu::ResultExt;
27
28use crate::State;
29use crate::md::prelude::Traj;
30use crate::od::TrackingDevice;
31use crate::od::groundpnt::GroundAsset;
32use crate::od::interlink::InterlinkTxSpacecraft;
33use crate::od::msr::MeasurementType;
34use crate::od::prelude::{Measurement, ODError};
35use crate::od::{ODAlmanacSnafu, ODTrajSnafu};
36
37impl TrackingDevice<GroundAsset> for InterlinkTxSpacecraft {
38 fn name(&self) -> String {
39 self.traj.name.clone().unwrap_or("unnamed".to_string())
40 }
41
42 fn measurement_types(&self) -> &IndexSet<MeasurementType> {
43 &self.measurement_types
44 }
45
46 fn location(&self, epoch: Epoch, frame: Frame, almanac: &Almanac) -> AlmanacResult<Orbit> {
47 match self.traj.at(epoch) {
48 Ok(state) => almanac.transform_to(state.orbit, frame, self.ab_corr),
49 Err(_) => Err(AlmanacError::Ephemeris {
50 action: "computing location from Interlink",
51 source: Box::new(EphemerisError::EphemInterpolation {
52 source: InterpolationError::MissingInterpolationData { epoch },
53 }),
54 }),
55 }
56 }
57
58 fn measure(
59 &mut self,
60 epoch: Epoch,
61 traj: &Traj<GroundAsset>,
62 rng: Option<&mut Pcg64Mcg>,
63 almanac: &Almanac,
64 ) -> Result<Option<Measurement>, ODError> {
65 match self.integration_time {
66 Some(integration_time) => {
67 let rx_0 = match traj.at(epoch - integration_time).context(ODTrajSnafu {
70 details: format!(
71 "fetching state {epoch} at start of ground station integration time {integration_time}"
72 ),
73 }) {
74 Ok(rx) => rx,
75 Err(_) => return Ok(None),
76 };
77
78 let rx_1 = match traj.at(epoch).context(ODTrajSnafu {
79 details: format!(
80 "fetching state {epoch} at end of ground station integration time"
81 ),
82 }) {
83 Ok(rx) => rx,
84 Err(_) => return Ok(None),
85 };
86
87 let msr_t0_opt = self.measure_instantaneous(rx_0, None, almanac)?;
89
90 let msr_t1_opt = self.measure_instantaneous(rx_1, None, almanac)?;
92
93 if let Some(msr_t0) = msr_t0_opt {
94 if let Some(msr_t1) = msr_t1_opt {
95 let noises = self.noises(epoch - integration_time * 0.5, rng)?;
99
100 let mut msr = Measurement::new(
101 "GroundAsset".to_string(),
102 epoch + noises[0].seconds(),
103 );
104
105 for (ii, msr_type) in self.measurement_types.iter().enumerate() {
106 let msr_value_0 = msr_t0.data[msr_type];
107 let msr_value_1 = msr_t1.data[msr_type];
108
109 let msr_value =
110 (msr_value_1 + msr_value_0) * 0.5 + noises[ii + 1] / 2.0_f64.sqrt();
111 msr.push(*msr_type, msr_value);
112 }
113
114 Ok(Some(msr))
115 } else {
116 Ok(None)
117 }
118 } else {
119 Ok(None)
120 }
121 }
122 None => self.measure_instantaneous(
123 traj.at(epoch).context(ODTrajSnafu {
124 details: "fetching state for instantaneous measurement".to_string(),
125 })?,
126 rng,
127 almanac,
128 ),
129 }
130 }
131
132 fn measure_instantaneous(
134 &mut self,
135 rx: GroundAsset,
136 rng: Option<&mut Pcg64Mcg>,
137 almanac: &Almanac,
138 ) -> Result<Option<Measurement>, ODError> {
139 let pnt_veh = self.traj.at(rx.epoch()).context(ODTrajSnafu {
140 details: format!("fetching state {} for interlink", rx.epoch()),
141 })?;
142
143 let asset_loc = rx.to_location();
144
145 let aer = almanac
146 .azimuth_elevation_range_sez_from_location(pnt_veh.orbit, asset_loc, None, None)
147 .context(ODAlmanacSnafu {
148 action: "transforming receiver to transmitter frame",
149 })?;
150
151 if aer.elevation_above_mask_deg() < 0.0 {
152 Ok(None)
153 } else {
154 let noises = self.noises(rx.epoch, rng)?;
155
156 let mut msr =
157 Measurement::new("GroundAsset".to_string(), rx.epoch + noises[0].seconds());
158
159 for (ii, msr_type) in self.measurement_types.iter().enumerate() {
161 let msr_value = match *msr_type {
162 MeasurementType::Range => aer.range_km,
163 MeasurementType::Doppler => aer.range_rate_km_s,
164 _ => unreachable!("unsupported measurement type for interlink: {:?}", msr_type),
166 } + noises[ii + 1];
167 msr.push(*msr_type, msr_value);
168 }
169
170 Ok(Some(msr))
171 }
172 }
173
174 fn measurement_covar(&self, msr_type: MeasurementType, epoch: Epoch) -> Result<f64, ODError> {
182 let stochastics =
183 self.stochastic_noises
184 .as_ref()
185 .ok_or_else(|| ODError::NoiseNotConfigured {
186 kind: "stochastic noises for interlink transmitter".to_string(),
187 })?;
188
189 Ok(stochastics
190 .get(&msr_type)
191 .ok_or(ODError::NoiseNotConfigured {
192 kind: format!("{msr_type:?}"),
193 })?
194 .covariance(epoch))
195 }
196
197 fn measurement_bias(&self, msr_type: MeasurementType, _epoch: Epoch) -> Result<f64, ODError> {
198 let stochastics = self.stochastic_noises.as_ref().unwrap();
199
200 if let Some(gm) = stochastics
201 .get(&msr_type)
202 .ok_or(ODError::NoiseNotConfigured {
203 kind: format!("{msr_type:?}"),
204 })?
205 .bias
206 {
207 Ok(gm.constant.unwrap_or(0.0))
208 } else {
209 Ok(0.0)
210 }
211 }
212}