1use anise::almanac::Almanac;
20use anise::analysis::AnalysisError;
21use anise::structure::LocationDataSet;
22use hifitime::{Duration, Epoch, TimeSeries, TimeUnits};
23use log::{info, warn};
24use num::integer::gcd;
25use rand::rngs::SysRng;
26use rand::SeedableRng;
27use rand_pcg::Pcg64Mcg;
28
29use crate::dynamics::NyxError;
30use crate::io::ConfigError;
31use crate::md::trajectory::Interpolatable;
32use crate::od::msr::TrackingDataArc;
33use crate::od::prelude::Strand;
34use crate::od::simulator::Cadence;
35use crate::od::GroundStation;
36use crate::Spacecraft;
37use crate::State;
38use crate::{linalg::allocator::Allocator, od::TrackingDevice};
39use crate::{linalg::DefaultAllocator, md::prelude::Traj};
40use std::collections::BTreeMap;
41use std::fmt::Display;
42use std::marker::PhantomData;
43use std::sync::Arc;
44
45use super::{Handoff, TrkConfig};
46
47#[derive(Clone)]
48pub struct TrackingArcSim<MsrIn, D>
49where
50 D: TrackingDevice<MsrIn>,
51 MsrIn: State,
52 MsrIn: Interpolatable,
53 DefaultAllocator: Allocator<<MsrIn as State>::Size>
54 + Allocator<<MsrIn as State>::Size, <MsrIn as State>::Size>
55 + Allocator<<MsrIn as State>::VecLength>,
56{
57 pub devices: BTreeMap<String, D>,
59 pub trajectory: Traj<MsrIn>,
61 pub configs: BTreeMap<String, TrkConfig>,
63 rng: Pcg64Mcg,
65 time_series: TimeSeries,
67 _msr_in: PhantomData<MsrIn>,
68}
69
70impl<MsrIn, D> TrackingArcSim<MsrIn, D>
71where
72 D: TrackingDevice<MsrIn>,
73 MsrIn: State,
74 MsrIn: Interpolatable,
75 DefaultAllocator: Allocator<<MsrIn as State>::Size>
76 + Allocator<<MsrIn as State>::Size, <MsrIn as State>::Size>
77 + Allocator<<MsrIn as State>::VecLength>,
78{
79 pub fn with_rng(
81 devices: BTreeMap<String, D>,
82 trajectory: Traj<MsrIn>,
83 configs: BTreeMap<String, TrkConfig>,
84 rng: Pcg64Mcg,
85 ) -> Result<Self, ConfigError> {
86 let mut sampling_rates_ns = Vec::with_capacity(devices.len());
89 for name in devices.keys() {
90 if let Some(cfg) = configs.get(name) {
91 if let Err(e) = cfg.sanity_check() {
92 warn!("Ignoring device {name}: {e}");
93 continue;
94 }
95 sampling_rates_ns.push(cfg.sampling.truncated_nanoseconds());
96 } else {
97 warn!("Ignoring device {name}: no associated tracking configuration",);
98 continue;
99 }
100 }
101
102 if sampling_rates_ns.is_empty() {
103 return Err(ConfigError::InvalidConfig {
104 msg: "None of the devices are properly configured".to_string(),
105 });
106 }
107
108 let common_sampling_rate_ns = sampling_rates_ns
109 .iter()
110 .fold(sampling_rates_ns[0], |a, &b| gcd(a, b));
111
112 let time_series = TimeSeries::inclusive(
115 trajectory.first().epoch(),
116 trajectory.last().epoch(),
117 Duration::from_truncated_nanoseconds(common_sampling_rate_ns),
118 );
119
120 let me = Self {
121 devices,
122 trajectory,
123 configs,
124 rng,
125 time_series,
126 _msr_in: PhantomData,
127 };
128
129 info!("{me}");
130
131 Ok(me)
132 }
133
134 pub fn with_seed(
136 devices: BTreeMap<String, D>,
137 trajectory: Traj<MsrIn>,
138 configs: BTreeMap<String, TrkConfig>,
139 seed: u64,
140 ) -> Result<Self, ConfigError> {
141 let rng = Pcg64Mcg::new(seed as u128);
142
143 Self::with_rng(devices, trajectory, configs, rng)
144 }
145
146 pub fn new(
148 devices: BTreeMap<String, D>,
149 trajectory: Traj<MsrIn>,
150 configs: BTreeMap<String, TrkConfig>,
151 ) -> Result<Self, ConfigError> {
152 let rng = Pcg64Mcg::try_from_rng(&mut SysRng).unwrap();
153
154 Self::with_rng(devices, trajectory, configs, rng)
155 }
156
157 pub fn generate_measurements(
172 &mut self,
173 almanac: Arc<Almanac>,
174 ) -> Result<TrackingDataArc, NyxError> {
175 let mut measurements = BTreeMap::new();
176
177 for (name, device) in self.devices.iter_mut() {
178 if let Some(cfg) = self.configs.get(name) {
179 if cfg.scheduler.is_some() {
180 if cfg.strands.is_none() {
181 return Err(NyxError::CustomError {
182 msg: format!(
183 "schedule for {name} must be built before generating measurements"
184 ),
185 });
186 } else {
187 warn!("scheduler for {name} is ignored, using the defined tracking strands instead")
188 }
189 }
190
191 let init_msr_count = measurements.len();
192 let tick = Epoch::now().unwrap();
193
194 match cfg.strands.as_ref() {
195 Some(strands) => {
196 'strands: for (ii, strand) in strands.iter().enumerate() {
198 for epoch in
200 TimeSeries::inclusive(strand.start, strand.end, cfg.sampling)
201 {
202 match device.measure(
203 epoch,
204 &self.trajectory,
205 Some(&mut self.rng),
206 almanac.clone(),
207 ) {
208 Ok(msr_opt) => {
209 if let Some(msr) = msr_opt {
210 measurements.insert(epoch, msr);
211 }
212 }
213 Err(e) => {
214 if epoch != strand.end {
215 warn!(
216 "Skipping the remaining strand #{ii} ending on {}: {e}",
217 strand.end
218 );
219 }
220 continue 'strands;
221 }
222 }
223 }
224 }
225
226 info!(
227 "Simulated {} measurements for {name} for {} tracking strands in {}",
228 measurements.len() - init_msr_count,
229 strands.len(),
230 (Epoch::now().unwrap() - tick).round(1.0_f64.milliseconds())
231 );
232 }
233 None => {
234 warn!("No tracking strands defined for {name}, skipping");
235 }
236 }
237 }
238 }
239
240 let trk_data = TrackingDataArc {
242 measurements,
243 source: None,
244 moduli: None,
245 force_reject: false,
246 };
247
248 Ok(trk_data)
249 }
250}
251
252impl<MsrIn, D> Display for TrackingArcSim<MsrIn, D>
253where
254 D: TrackingDevice<MsrIn>,
255 MsrIn: Interpolatable,
256 DefaultAllocator: Allocator<<MsrIn as State>::Size>
257 + Allocator<<MsrIn as State>::Size, <MsrIn as State>::Size>
258 + Allocator<<MsrIn as State>::VecLength>,
259{
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 write!(
262 f,
263 "Tracking Arc Simulator on {} with devices {:?} over {}",
264 self.trajectory,
265 self.devices.keys(),
266 self.time_series
267 )
268 }
269}
270
271impl TrackingArcSim<Spacecraft, GroundStation> {
273 pub fn generate_schedule(
287 &self,
288 almanac: Arc<Almanac>,
289 ) -> Result<BTreeMap<String, TrkConfig>, AnalysisError> {
290 let mut loc_dataset = LocationDataSet::default();
292 let mut loc_ids = Vec::with_capacity(self.devices.len());
293 for (dno, (name, device)) in self.devices.iter().enumerate() {
294 let loc_id = dno as i32 + 1_000;
295 loc_dataset
296 .push(device.location.clone(), Some(loc_id), Some(name))
297 .map_err(|e| AnalysisError::GenericAnalysisError {
298 err: format!("{e}"),
299 })?;
300 loc_ids.push((name.clone(), loc_id));
301 }
302
303 let almanac = (*almanac).clone();
305 let almanac = almanac.with_location_data(loc_dataset);
306
307 let mut built_cfg = self.configs.clone();
308
309 let traj = &self.trajectory;
310
311 for (name, loc_id) in &loc_ids {
312 if let Some(cfg) = self.configs.get(name) {
313 if let Some(scheduler) = cfg.scheduler {
314 info!("Building schedule for {name}");
315 if scheduler.handoff == Handoff::Overlap {
316 warn!("Overlapping measurements on {name} is no longer supported on identical epochs.");
317 }
318 built_cfg.get_mut(name).unwrap().scheduler = None;
319 built_cfg.get_mut(name).unwrap().strands = Some(Vec::new());
320
321 let visibilty_arcs = almanac.report_visibility_arcs(
322 traj,
323 *loc_id,
324 traj.first().epoch(),
325 traj.last().epoch(),
326 cfg.sampling,
327 None,
328 )?;
329
330 for arc in visibilty_arcs {
331 let strand_start = arc.rise.orbit.epoch;
332 let strand_end = arc.fall.orbit.epoch;
333
334 if strand_end - strand_start
335 < cfg.sampling * i64::from(scheduler.min_samples)
336 {
337 info!(
338 "Too few samples from {name} opportunity from {strand_start} to {strand_end}, discarding strand",
339 );
340 continue;
341 }
342
343 let mut strand_range = Strand {
344 start: strand_start,
345 end: strand_end,
346 };
347
348 if let Some(alignment) = scheduler.sample_alignment {
350 strand_range.start = strand_range.start.round(alignment);
351 strand_range.end = strand_range.end.round(alignment);
352 }
353
354 if let Cadence::Intermittent { on, off } = scheduler.cadence {
355 if let Some(prev_strand) =
357 built_cfg[name].strands.as_ref().unwrap().last()
358 {
359 if prev_strand.end + off > strand_range.start {
360 strand_range.start = prev_strand.end + off;
362 if strand_range.start > strand_end {
364 info!("Discarding {name} opportunity from {strand_start} to {strand_end} due to cadence {:?}", scheduler.cadence);
366 continue;
367 }
368 }
369 }
370 if strand_range.end - strand_range.start > on {
372 strand_range.end = strand_range.start + on;
373 }
374 }
375
376 built_cfg
378 .get_mut(name)
379 .unwrap()
380 .strands
381 .as_mut()
382 .unwrap()
383 .push(strand_range);
384 }
385
386 info!(
387 "Built {} tracking strands for {name}",
388 built_cfg[name].strands.as_ref().unwrap().len()
389 );
390 }
391 }
392 }
393 let mut cfg_as_vec = Vec::new();
395 for (name, cfg) in &built_cfg {
396 if let Some(strands) = &cfg.strands {
397 for (ii, strand) in strands.iter().enumerate() {
398 cfg_as_vec.push((name.clone(), ii, *strand));
399 }
400 }
401 }
402 cfg_as_vec.sort_by_key(|(_, _, strand)| strand.start);
404 for (ii, (this_name, this_pos, this_strand)) in
405 cfg_as_vec.iter().take(cfg_as_vec.len() - 1).enumerate()
406 {
407 if let Some(config) = self.configs[this_name].scheduler.as_ref() {
409 if let Some((next_name, next_pos, next_strand)) = cfg_as_vec.get(ii + 1) {
411 if config.handoff == Handoff::Greedy && this_strand.end >= next_strand.start {
412 let next_config = built_cfg.get_mut(next_name).unwrap();
414 let new_start = this_strand.end + next_config.sampling;
415 next_config.strands.as_mut().unwrap()[*next_pos].start = new_start;
416 info!(
417 "{this_name} configured as {:?}, so {next_name} now starts on {new_start}",
418 config.handoff
419 );
420 } else if config.handoff == Handoff::Eager
421 && this_strand.end >= next_strand.start
422 {
423 let this_config = built_cfg.get_mut(this_name).unwrap();
424 let new_end = next_strand.start - this_config.sampling;
425 this_config.strands.as_mut().unwrap()[*this_pos].end = new_end;
426 info!(
427 "{this_name} now hands off to {next_name} on {new_end} because it's configured as {:?}",
428 config.handoff
429 );
430 }
431 } else {
432 break;
434 }
435 }
436 }
437
438 Ok(built_cfg)
439 }
440
441 pub fn build_schedule(&mut self, almanac: Arc<Almanac>) -> Result<(), AnalysisError> {
443 self.configs = self.generate_schedule(almanac)?;
444
445 Ok(())
446 }
447}