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