nyx_space/od/ground_station/
trk_device.rs1use super::{ODAlmanacSnafu, ODError, ODTrajSnafu, TrackingDevice};
20use crate::md::prelude::{Interpolatable, Traj};
21use crate::od::msr::measurement::Measurement;
22use crate::od::msr::MeasurementType;
23use crate::time::Epoch;
24use crate::Spacecraft;
25use anise::errors::AlmanacResult;
26use anise::frames::Frame;
27use anise::prelude::{Almanac, Orbit};
28use hifitime::TimeUnits;
29use indexmap::IndexSet;
30use rand_pcg::Pcg64Mcg;
31use snafu::ResultExt;
32use std::sync::Arc;
33
34use super::GroundStation;
35
36impl TrackingDevice<Spacecraft> for GroundStation {
37 fn measurement_types(&self) -> &IndexSet<MeasurementType> {
38 &self.measurement_types
39 }
40
41 fn measure(
43 &mut self,
44 epoch: Epoch,
45 traj: &Traj<Spacecraft>,
46 rng: Option<&mut Pcg64Mcg>,
47 almanac: Arc<Almanac>,
48 ) -> Result<Option<Measurement>, ODError> {
49 match self.integration_time {
50 Some(integration_time) => {
51 let rx_0 = match traj.at(epoch - integration_time).context(ODTrajSnafu {
54 details: format!(
55 "fetching state {epoch} at start of ground station integration time {integration_time}"
56 ),
57 }) {
58 Ok(rx) => rx,
59 Err(_) => return Ok(None),
60 };
61
62 let rx_1 = match traj.at(epoch).context(ODTrajSnafu {
63 details: format!(
64 "fetching state {epoch} at end of ground station integration time"
65 ),
66 }) {
67 Ok(rx) => rx,
68 Err(_) => return Ok(None),
69 };
70
71 let obstructing_body = if !self.frame.ephem_origin_match(rx_0.frame()) {
72 Some(rx_0.frame())
73 } else {
74 None
75 };
76
77 let aer_t0 = self
78 .azimuth_elevation_of(rx_0.orbit, obstructing_body, &almanac)
79 .context(ODAlmanacSnafu {
80 action: "computing AER",
81 })?;
82 let aer_t1 = self
83 .azimuth_elevation_of(rx_1.orbit, obstructing_body, &almanac)
84 .context(ODAlmanacSnafu {
85 action: "computing AER",
86 })?;
87
88 if aer_t0.elevation_deg < self.elevation_mask_deg
89 || aer_t1.elevation_deg < self.elevation_mask_deg
90 {
91 debug!(
92 "{} (el. mask {:.3} deg) but object moves from {:.3} to {:.3} deg -- no measurement",
93 self.name, self.elevation_mask_deg, aer_t0.elevation_deg, aer_t1.elevation_deg
94 );
95 return Ok(None);
96 } else if aer_t0.is_obstructed() || aer_t1.is_obstructed() {
97 debug!(
98 "{} obstruction at t0={}, t1={} -- no measurement",
99 self.name,
100 aer_t0.is_obstructed(),
101 aer_t1.is_obstructed()
102 );
103 return Ok(None);
104 }
105
106 let noises = self.noises(epoch - integration_time * 0.5, rng)?;
108
109 let mut msr = Measurement::new(self.name.clone(), epoch + noises[0].seconds());
110
111 for (ii, msr_type) in self.measurement_types.iter().enumerate() {
112 let msr_value = msr_type.compute_two_way(aer_t0, aer_t1, noises[ii + 1])?;
113 msr.push(*msr_type, msr_value);
114 }
115
116 Ok(Some(msr))
117 }
118 None => self.measure_instantaneous(
119 traj.at(epoch).context(ODTrajSnafu {
120 details: "fetching state for instantaneous measurement".to_string(),
121 })?,
122 rng,
123 almanac,
124 ),
125 }
126 }
127
128 fn name(&self) -> String {
129 self.name.clone()
130 }
131
132 fn location(&self, epoch: Epoch, frame: Frame, almanac: Arc<Almanac>) -> AlmanacResult<Orbit> {
133 almanac.transform_to(self.to_orbit(epoch, &almanac).unwrap(), frame, None)
134 }
135
136 fn measure_instantaneous(
137 &mut self,
138 rx: Spacecraft,
139 rng: Option<&mut Pcg64Mcg>,
140 almanac: Arc<Almanac>,
141 ) -> Result<Option<Measurement>, ODError> {
142 let obstructing_body = if !self.frame.ephem_origin_match(rx.frame()) {
143 Some(rx.frame())
144 } else {
145 None
146 };
147
148 let aer = self
149 .azimuth_elevation_of(rx.orbit, obstructing_body, &almanac)
150 .context(ODAlmanacSnafu {
151 action: "computing AER",
152 })?;
153
154 if aer.elevation_deg >= self.elevation_mask_deg && !aer.is_obstructed() {
155 let noises = self.noises(rx.orbit.epoch, rng)?;
157
158 let mut msr = Measurement::new(self.name.clone(), rx.orbit.epoch + noises[0].seconds());
159
160 for (ii, msr_type) in self.measurement_types.iter().enumerate() {
161 let msr_value = msr_type.compute_one_way(aer, noises[ii + 1])?;
162 msr.push(*msr_type, msr_value);
163 }
164
165 Ok(Some(msr))
166 } else {
167 debug!(
168 "{} {} (el. mask {:.3} deg), object at {:.3} deg -- no measurement",
169 self.name, rx.orbit.epoch, self.elevation_mask_deg, aer.elevation_deg
170 );
171 Ok(None)
172 }
173 }
174
175 fn measurement_covar(&self, msr_type: MeasurementType, epoch: Epoch) -> Result<f64, ODError> {
183 let stochastics = self.stochastic_noises.as_ref().unwrap();
184
185 Ok(stochastics
186 .get(&msr_type)
187 .ok_or(ODError::NoiseNotConfigured {
188 kind: format!("{msr_type:?}"),
189 })?
190 .covariance(epoch))
191 }
192
193 fn measurement_bias(&self, msr_type: MeasurementType, _epoch: Epoch) -> Result<f64, ODError> {
194 let stochastics = self.stochastic_noises.as_ref().unwrap();
195
196 if let Some(gm) = stochastics
197 .get(&msr_type)
198 .ok_or(ODError::NoiseNotConfigured {
199 kind: format!("{msr_type:?}"),
200 })?
201 .bias
202 {
203 Ok(gm.constant.unwrap_or(0.0))
204 } else {
205 Ok(0.0)
206 }
207 }
208}