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::rngs::SysRng;
27use rand_pcg::Pcg64Mcg;
28
29use crate::Spacecraft;
30use crate::State;
31use crate::io::ConfigError;
32use crate::md::trajectory::Interpolatable;
33use crate::od::GroundStation;
34use crate::od::msr::TrackingDataArc;
35use crate::od::prelude::Strand;
36use crate::od::simulator::Cadence;
37use crate::{linalg::DefaultAllocator, md::prelude::Traj};
38use crate::{linalg::allocator::Allocator, od::TrackingDevice};
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::try_from_rng(&mut SysRng).unwrap();
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, ConfigError> {
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(ConfigError::InvalidConfig {
181 msg: format!(
182 "schedule for {name} must be built before generating measurements"
183 ),
184 });
185 } else {
186 warn!(
187 "scheduler for {name} is ignored, using the defined tracking strands instead"
188 )
189 }
190 }
191
192 let init_msr_count = measurements.len();
193 let tick = Epoch::now().unwrap();
194
195 match cfg.strands.as_ref() {
196 Some(strands) => {
197 'strands: for (ii, strand) in strands.iter().enumerate() {
199 for epoch in
201 TimeSeries::inclusive(strand.start, strand.end, cfg.sampling)
202 {
203 match device.measure(
204 epoch,
205 &self.trajectory,
206 Some(&mut self.rng),
207 almanac.clone(),
208 ) {
209 Ok(msr_opt) => {
210 if let Some(msr) = msr_opt {
211 measurements.insert(epoch, msr);
212 }
213 }
214 Err(e) => {
215 if epoch != strand.end {
216 warn!(
217 "Skipping the remaining strand #{ii} ending on {}: {e}",
218 strand.end
219 );
220 }
221 continue 'strands;
222 }
223 }
224 }
225 }
226
227 info!(
228 "Simulated {} measurements for {name} for {} tracking strands in {}",
229 measurements.len() - init_msr_count,
230 strands.len(),
231 (Epoch::now().unwrap() - tick).round(1.0_f64.milliseconds())
232 );
233 }
234 None => {
235 warn!("No tracking strands defined for {name}, skipping");
236 }
237 }
238 }
239 }
240
241 let trk_data = TrackingDataArc {
243 measurements,
244 source: None,
245 moduli: None,
246 force_reject: false,
247 };
248
249 Ok(trk_data)
250 }
251}
252
253impl<MsrIn, D> Display for TrackingArcSim<MsrIn, D>
254where
255 D: TrackingDevice<MsrIn>,
256 MsrIn: Interpolatable,
257 DefaultAllocator: Allocator<<MsrIn as State>::Size>
258 + Allocator<<MsrIn as State>::Size, <MsrIn as State>::Size>
259 + Allocator<<MsrIn as State>::VecLength>,
260{
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 write!(
263 f,
264 "Tracking Arc Simulator on {} with devices {:?} over {}",
265 self.trajectory,
266 self.devices.keys(),
267 self.time_series
268 )
269 }
270}
271
272impl TrackingArcSim<Spacecraft, GroundStation> {
274 pub fn generate_schedule(
288 &self,
289 almanac: Arc<Almanac>,
290 ) -> Result<BTreeMap<String, TrkConfig>, AnalysisError> {
291 let mut loc_dataset = LocationDataSet::default();
293 let mut loc_ids = Vec::with_capacity(self.devices.len());
294 for (dno, (name, device)) in self.devices.iter().enumerate() {
295 let loc_id = dno as i32 + 1_000;
296 loc_dataset
297 .push(device.location.clone(), Some(loc_id), Some(name))
298 .map_err(|e| AnalysisError::GenericAnalysisError {
299 err: format!("{e}"),
300 })?;
301 loc_ids.push((name.clone(), loc_id));
302 }
303
304 let almanac = (*almanac).clone();
306 let almanac = almanac.with_location_data(loc_dataset);
307
308 let mut built_cfg = self.configs.clone();
309
310 let traj = &self.trajectory;
311
312 for (name, loc_id) in &loc_ids {
313 if let Some(cfg) = self.configs.get(name)
314 && let Some(scheduler) = cfg.scheduler
315 {
316 info!("Building schedule for {name}");
317 if scheduler.handoff == Handoff::Overlap {
318 warn!(
319 "Overlapping measurements on {name} is no longer supported on identical epochs."
320 );
321 }
322 built_cfg.get_mut(name).unwrap().scheduler = None;
323 built_cfg.get_mut(name).unwrap().strands = Some(Vec::new());
324
325 let visibilty_arcs = almanac.report_visibility_arcs(
326 traj,
327 *loc_id,
328 traj.first().epoch(),
329 traj.last().epoch(),
330 cfg.sampling,
331 None,
332 )?;
333
334 for arc in visibilty_arcs {
335 let strand_start = arc.rise.orbit.epoch;
336 let strand_end = arc.fall.orbit.epoch;
337
338 if strand_end - strand_start < cfg.sampling * i64::from(scheduler.min_samples) {
339 info!(
340 "Too few samples from {name} opportunity from {strand_start} to {strand_end}, discarding strand",
341 );
342 continue;
343 }
344
345 let mut strand_range = Strand {
346 start: strand_start,
347 end: strand_end,
348 };
349
350 if let Some(alignment) = scheduler.sample_alignment {
352 strand_range.start = strand_range.start.round(alignment);
353 strand_range.end = strand_range.end.round(alignment);
354 }
355
356 if let Cadence::Intermittent { on, off } = scheduler.cadence {
357 if let Some(prev_strand) = built_cfg[name].strands.as_ref().unwrap().last()
359 && prev_strand.end + off > strand_range.start
360 {
361 strand_range.start = prev_strand.end + off;
363 if strand_range.start > strand_end {
365 info!(
367 "Discarding {name} opportunity from {strand_start} to {strand_end} due to cadence {:?}",
368 scheduler.cadence
369 );
370 continue;
371 }
372 }
373 if strand_range.end - strand_range.start > on {
375 strand_range.end = strand_range.start + on;
376 }
377 }
378
379 built_cfg
381 .get_mut(name)
382 .unwrap()
383 .strands
384 .as_mut()
385 .unwrap()
386 .push(strand_range);
387 }
388
389 info!(
390 "Built {} tracking strands for {name}",
391 built_cfg[name].strands.as_ref().unwrap().len()
392 );
393 }
394 }
395 let mut cfg_as_vec = Vec::new();
397 for (name, cfg) in &built_cfg {
398 if let Some(strands) = &cfg.strands {
399 for (ii, strand) in strands.iter().enumerate() {
400 cfg_as_vec.push((name.clone(), ii, *strand));
401 }
402 }
403 }
404 cfg_as_vec.sort_by_key(|(_, _, strand)| strand.start);
406 for (ii, (this_name, this_pos, this_strand)) in
407 cfg_as_vec.iter().take(cfg_as_vec.len() - 1).enumerate()
408 {
409 if let Some(config) = self.configs[this_name].scheduler.as_ref() {
411 if let Some((next_name, next_pos, next_strand)) = cfg_as_vec.get(ii + 1) {
413 if config.handoff == Handoff::Greedy && this_strand.end >= next_strand.start {
414 let next_config = built_cfg.get_mut(next_name).unwrap();
416 let new_start = this_strand.end + next_config.sampling;
417 next_config.strands.as_mut().unwrap()[*next_pos].start = new_start;
418 info!(
419 "{this_name} configured as {:?}, so {next_name} now starts on {new_start}",
420 config.handoff
421 );
422 } else if config.handoff == Handoff::Eager
423 && this_strand.end >= next_strand.start
424 {
425 let this_config = built_cfg.get_mut(this_name).unwrap();
426 let new_end = next_strand.start - this_config.sampling;
427 this_config.strands.as_mut().unwrap()[*this_pos].end = new_end;
428 info!(
429 "{this_name} now hands off to {next_name} on {new_end} because it's configured as {:?}",
430 config.handoff
431 );
432 }
433 } else {
434 break;
436 }
437 }
438 }
439
440 Ok(built_cfg)
441 }
442
443 pub fn build_schedule(&mut self, almanac: Arc<Almanac>) -> Result<(), AnalysisError> {
445 self.configs = self.generate_schedule(almanac)?;
446
447 Ok(())
448 }
449}