1use anise::almanac::Almanac;
19use anise::astro::Aberration;
20use anise::errors::AlmanacResult;
21use anise::prelude::{Frame, Orbit};
22use hifitime::{Duration, Epoch, TimeUnits};
23use indexmap::{IndexMap, IndexSet};
24use rand_pcg::Pcg64Mcg;
25use serde::{Deserialize, Serialize};
26use snafu::{ResultExt, ensure};
27
28use crate::Spacecraft;
29use crate::State;
30use crate::io::ConfigRepr;
31use crate::md::Trajectory;
32use crate::md::prelude::Traj;
33use crate::od::TrackingDevice;
34use crate::od::msr::MeasurementType;
35use crate::od::noise::StochasticNoise;
36use crate::od::prelude::{Measurement, NoiseNotConfiguredSnafu, ODError};
37use crate::od::{ODAlmanacSnafu, ODTrajSnafu};
38
39#[derive(Clone, Debug)]
42pub struct InterlinkTxSpacecraft {
43 pub traj: Trajectory,
45 pub measurement_types: IndexSet<MeasurementType>,
47 pub integration_time: Option<Duration>,
49 pub timestamp_noise_s: Option<StochasticNoise>,
50 pub stochastic_noises: Option<IndexMap<MeasurementType, StochasticNoise>>,
51 pub ab_corr: Option<Aberration>,
53}
54
55impl InterlinkTxSpacecraft {
56 pub fn noises(
58 &mut self,
59 epoch: Epoch,
60 rng: Option<&mut Pcg64Mcg>,
61 ) -> Result<Vec<f64>, ODError> {
62 let mut noises = vec![0.0; self.measurement_types.len() + 1];
63
64 if let Some(rng) = rng {
65 ensure!(
66 self.stochastic_noises.is_some(),
67 NoiseNotConfiguredSnafu {
68 kind: "ground station stochastics".to_string(),
69 }
70 );
71 if let Some(mut timestamp_noise) = self.timestamp_noise_s {
74 noises[0] = timestamp_noise.sample(epoch, rng);
75 }
76
77 let stochastics = self.stochastic_noises.as_mut().unwrap();
78
79 for (ii, msr_type) in self.measurement_types.iter().enumerate() {
80 noises[ii + 1] = stochastics
81 .get_mut(msr_type)
82 .ok_or(ODError::NoiseNotConfigured {
83 kind: format!("{msr_type:?}"),
84 })?
85 .sample(epoch, rng);
86 }
87 }
88
89 Ok(noises)
90 }
91}
92
93impl TrackingDevice<Spacecraft> for InterlinkTxSpacecraft {
94 fn name(&self) -> String {
95 self.traj.name.clone().unwrap_or("unnamed".to_string())
96 }
97
98 fn measurement_types(&self) -> &IndexSet<MeasurementType> {
99 &self.measurement_types
100 }
101
102 fn location(&self, epoch: Epoch, frame: Frame, almanac: &Almanac) -> AlmanacResult<Orbit> {
103 almanac.transform_to(self.traj.at(epoch).unwrap().orbit, frame, self.ab_corr)
104 }
105
106 fn measure(
107 &mut self,
108 epoch: Epoch,
109 traj: &Traj<Spacecraft>,
110 rng: Option<&mut Pcg64Mcg>,
111 almanac: &Almanac,
112 ) -> Result<Option<Measurement>, ODError> {
113 match self.integration_time {
114 Some(integration_time) => {
115 let rx_0 = match traj.at(epoch - integration_time).context(ODTrajSnafu {
118 details: format!(
119 "fetching state {epoch} at start of ground station integration time {integration_time}"
120 ),
121 }) {
122 Ok(rx) => rx,
123 Err(_) => return Ok(None),
124 };
125
126 let rx_1 = match traj.at(epoch).context(ODTrajSnafu {
127 details: format!(
128 "fetching state {epoch} at end of ground station integration time"
129 ),
130 }) {
131 Ok(rx) => rx,
132 Err(_) => return Ok(None),
133 };
134
135 let msr_t0_opt = self.measure_instantaneous(rx_0, None, almanac)?;
137
138 let msr_t1_opt = self.measure_instantaneous(rx_1, None, almanac)?;
140
141 if let Some(msr_t0) = msr_t0_opt {
142 if let Some(msr_t1) = msr_t1_opt {
143 let noises = self.noises(epoch - integration_time * 0.5, rng)?;
147
148 let mut msr = Measurement::new(
149 <InterlinkTxSpacecraft as TrackingDevice<Spacecraft>>::name(self),
150 epoch + noises[0].seconds(),
151 );
152
153 for (ii, msr_type) in self.measurement_types.iter().enumerate() {
154 let msr_value_0 = msr_t0.data[msr_type];
155 let msr_value_1 = msr_t1.data[msr_type];
156
157 let msr_value =
158 (msr_value_1 + msr_value_0) * 0.5 + noises[ii + 1] / 2.0_f64.sqrt();
159 msr.push(*msr_type, msr_value);
160 }
161
162 Ok(Some(msr))
163 } else {
164 Ok(None)
165 }
166 } else {
167 Ok(None)
168 }
169 }
170 None => self.measure_instantaneous(
171 traj.at(epoch).context(ODTrajSnafu {
172 details: "fetching state for instantaneous measurement".to_string(),
173 })?,
174 rng,
175 almanac,
176 ),
177 }
178 }
179
180 fn measure_instantaneous(
181 &mut self,
182 rx: Spacecraft,
183 rng: Option<&mut Pcg64Mcg>,
184 almanac: &Almanac,
185 ) -> Result<Option<Measurement>, ODError> {
186 let observer = self.traj.at(rx.epoch()).context(ODTrajSnafu {
187 details: format!("fetching state {} for interlink", rx.epoch()),
188 })?;
189
190 let is_obstructed = almanac
191 .line_of_sight_obstructed(observer.orbit, rx.orbit, observer.orbit.frame, self.ab_corr)
192 .context(ODAlmanacSnafu {
193 action: "computing line of sight",
194 })?;
195
196 if is_obstructed {
197 Ok(None)
198 } else {
199 let rx_in_tx_frame = almanac
201 .transform_to(rx.orbit, observer.orbit.frame, self.ab_corr)
202 .context(ODAlmanacSnafu {
203 action: "transforming receiver to transmitter frame",
204 })?;
205
206 let rho_tx_frame = rx_in_tx_frame.radius_km - observer.orbit.radius_km;
207
208 let range_rate_km_s =
210 rho_tx_frame.dot(&rx_in_tx_frame.velocity_km_s) / rho_tx_frame.norm();
211
212 let noises = self.noises(observer.epoch(), rng)?;
213
214 let mut msr = Measurement::new(
215 <InterlinkTxSpacecraft as TrackingDevice<Spacecraft>>::name(self),
216 rx.orbit.epoch + noises[0].seconds(),
217 );
218
219 for (ii, msr_type) in self.measurement_types.iter().enumerate() {
220 let msr_value = match *msr_type {
221 MeasurementType::Range => rho_tx_frame.norm(),
222 MeasurementType::Doppler => range_rate_km_s,
223 _ => unreachable!("unsupported measurement type for interlink: {:?}", msr_type),
225 } + noises[ii + 1];
226 msr.push(*msr_type, msr_value);
227 }
228
229 Ok(Some(msr))
230 }
231 }
232
233 fn measurement_covar(&self, msr_type: MeasurementType, epoch: Epoch) -> Result<f64, ODError> {
241 let stochastics = self.stochastic_noises.as_ref().unwrap();
242
243 Ok(stochastics
244 .get(&msr_type)
245 .ok_or(ODError::NoiseNotConfigured {
246 kind: format!("{msr_type:?}"),
247 })?
248 .covariance(epoch))
249 }
250
251 fn measurement_bias(&self, msr_type: MeasurementType, _epoch: Epoch) -> Result<f64, ODError> {
252 let stochastics = self.stochastic_noises.as_ref().unwrap();
253
254 if let Some(gm) = stochastics
255 .get(&msr_type)
256 .ok_or(ODError::NoiseNotConfigured {
257 kind: format!("{msr_type:?}"),
258 })?
259 .bias
260 {
261 Ok(gm.constant.unwrap_or(0.0))
262 } else {
263 Ok(0.0)
264 }
265 }
266}
267
268impl Serialize for InterlinkTxSpacecraft {
269 fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
270 where
271 S: serde::Serializer,
272 {
273 unimplemented!("interlink spacecraft cannot be serialized")
274 }
275}
276
277impl<'de> Deserialize<'de> for InterlinkTxSpacecraft {
278 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
279 where
280 D: serde::Deserializer<'de>,
281 {
282 unimplemented!("interlink spacecraft cannot be deserialized")
283 }
284}
285
286impl ConfigRepr for InterlinkTxSpacecraft {}