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(
85 devices: BTreeMap<String, D>,
86 trajectory: Traj<MsrIn>,
87 configs: BTreeMap<String, TrkConfig>,
88 rng: Pcg64Mcg,
89 ) -> Result<Self, ConfigError> {
90 let mut sampling_rates_ns = Vec::with_capacity(devices.len());
93 for name in devices.keys() {
94 if let Some(cfg) = configs.get(name) {
95 if let Err(e) = cfg.sanity_check() {
96 warn!("Ignoring device {name}: {e}");
97 continue;
98 }
99 sampling_rates_ns.push(cfg.sampling.truncated_nanoseconds());
100 } else {
101 warn!("Ignoring device {name}: no associated tracking configuration",);
102 continue;
103 }
104 }
105
106 if sampling_rates_ns.is_empty() {
107 return Err(ConfigError::InvalidConfig {
108 msg: "None of the devices are properly configured".to_string(),
109 });
110 }
111
112 let common_sampling_rate_ns = sampling_rates_ns
113 .iter()
114 .fold(sampling_rates_ns[0], |a, &b| gcd(a, b));
115
116 let time_series = TimeSeries::inclusive(
119 trajectory.first().epoch(),
120 trajectory.last().epoch(),
121 Duration::from_truncated_nanoseconds(common_sampling_rate_ns),
122 );
123
124 let me = Self {
125 devices,
126 trajectory,
127 configs,
128 rng,
129 time_series,
130 _msr_in: PhantomData,
131 };
132
133 info!("{me}");
134
135 Ok(me)
136 }
137
138 pub fn with_seed(
140 devices: BTreeMap<String, D>,
141 trajectory: Traj<MsrIn>,
142 configs: BTreeMap<String, TrkConfig>,
143 seed: u64,
144 ) -> Result<Self, ConfigError> {
145 let rng = Pcg64Mcg::new(seed as u128);
146
147 Self::with_rng(devices, trajectory, configs, rng)
148 }
149
150 pub fn new(
152 devices: BTreeMap<String, D>,
153 trajectory: Traj<MsrIn>,
154 configs: BTreeMap<String, TrkConfig>,
155 ) -> Result<Self, ConfigError> {
156 let rng = Pcg64Mcg::try_from_rng(&mut SysRng).unwrap();
157
158 Self::with_rng(devices, trajectory, configs, rng)
159 }
160
161 pub fn generate_measurements(
176 &mut self,
177 almanac: Arc<Almanac>,
178 ) -> Result<TrackingDataArc, ConfigError> {
179 let mut measurements = BTreeMap::new();
180
181 for (name, device) in self.devices.iter_mut() {
182 if let Some(cfg) = self.configs.get(name) {
183 if cfg.scheduler.is_some() {
184 if cfg.strands.is_none() {
185 return Err(ConfigError::InvalidConfig {
186 msg: format!(
187 "schedule for {name} must be built before generating measurements"
188 ),
189 });
190 } else {
191 warn!(
192 "scheduler for {name} is ignored, using the defined tracking strands instead"
193 )
194 }
195 }
196
197 let init_msr_count = measurements.len();
198 let tick = Epoch::now().unwrap();
199
200 match cfg.strands.as_ref() {
201 Some(strands) => {
202 'strands: for (ii, strand) in strands.iter().enumerate() {
204 for epoch in
206 TimeSeries::inclusive(strand.start, strand.end, cfg.sampling)
207 {
208 match device.measure(
209 epoch,
210 &self.trajectory,
211 Some(&mut self.rng),
212 almanac.clone(),
213 ) {
214 Ok(msr_opt) => {
215 if let Some(msr) = msr_opt {
216 measurements.insert(epoch, msr);
217 }
218 }
219 Err(e) => {
220 if epoch != strand.end {
221 warn!(
222 "Skipping the remaining strand #{ii} ending on {}: {e}",
223 strand.end
224 );
225 }
226 continue 'strands;
227 }
228 }
229 }
230 }
231
232 info!(
233 "Simulated {} measurements for {name} for {} tracking strands in {}",
234 measurements.len() - init_msr_count,
235 strands.len(),
236 (Epoch::now().unwrap() - tick).round(1.0_f64.milliseconds())
237 );
238 }
239 None => {
240 warn!("No tracking strands defined for {name}, skipping");
241 }
242 }
243 }
244 }
245
246 let trk_data = TrackingDataArc {
248 measurements,
249 source: None,
250 moduli: None,
251 force_reject: false,
252 };
253
254 Ok(trk_data)
255 }
256}
257
258impl<MsrIn, D> Display for TrackingArcSim<MsrIn, D>
259where
260 D: TrackingDevice<MsrIn>,
261 MsrIn: Interpolatable,
262 DefaultAllocator: Allocator<<MsrIn as State>::Size>
263 + Allocator<<MsrIn as State>::Size, <MsrIn as State>::Size>
264 + Allocator<<MsrIn as State>::VecLength>,
265{
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 write!(
268 f,
269 "Tracking Arc Simulator on {} with devices {:?} over {}",
270 self.trajectory,
271 self.devices.keys(),
272 self.time_series
273 )
274 }
275}
276
277impl TrackingArcSim<Spacecraft, GroundStation> {
279 pub fn generate_schedule(
293 &self,
294 almanac: Arc<Almanac>,
295 ) -> Result<BTreeMap<String, TrkConfig>, AnalysisError> {
296 let mut loc_dataset = LocationDataSet::default();
298 let mut loc_ids = Vec::with_capacity(self.devices.len());
299 for (dno, (name, device)) in self.devices.iter().enumerate() {
300 let loc_id = dno as i32 + 1_000;
301 loc_dataset
302 .push(device.location.clone(), Some(loc_id), Some(name))
303 .map_err(|e| AnalysisError::GenericAnalysisError {
304 err: format!("{e}"),
305 })?;
306 loc_ids.push((name.clone(), loc_id));
307 }
308
309 let almanac = (*almanac).clone();
311 let almanac = almanac.with_location_data(loc_dataset);
312
313 let mut built_cfg = self.configs.clone();
314
315 let traj = &self.trajectory;
316
317 for (name, loc_id) in &loc_ids {
318 if let Some(cfg) = self.configs.get(name)
319 && let Some(scheduler) = cfg.scheduler
320 {
321 info!("Building schedule for {name}");
322 if scheduler.handoff == Handoff::Overlap {
323 warn!("Overlap unsupported for {name} on identical epochs.");
324 }
325 built_cfg.get_mut(name).unwrap().scheduler = None;
326 built_cfg.get_mut(name).unwrap().strands = Some(Vec::new());
327
328 let visibilty_arcs = almanac.report_visibility_arcs(
329 traj,
330 *loc_id,
331 traj.first().epoch(),
332 traj.last().epoch(),
333 cfg.sampling,
334 None,
335 )?;
336
337 for arc in visibilty_arcs {
338 let strand_start = arc.rise.orbit.epoch;
339 let strand_end = arc.fall.orbit.epoch;
340
341 if strand_end - strand_start < cfg.sampling * i64::from(scheduler.min_samples) {
342 warn!(
343 "Dropped [{strand_start}, {strand_end}]: < {} samples @ {}",
344 scheduler.min_samples, cfg.sampling
345 );
346 continue;
347 }
348
349 let mut strand_range = Strand {
350 start: strand_start,
351 end: strand_end,
352 };
353
354 if let Some(alignment) = scheduler.sample_alignment {
356 strand_range.start = strand_range.start.round(alignment);
357 strand_range.end = strand_range.end.round(alignment);
358 }
359
360 if let Cadence::Intermittent { on, off } = scheduler.cadence {
361 if let Some(prev_strand) = built_cfg[name].strands.as_ref().unwrap().last()
363 && prev_strand.end + off > strand_range.start
364 {
365 strand_range.start = prev_strand.end + off;
367 if strand_range.start > strand_end {
369 warn!(
371 "Dropped {name} [{strand_start}, {strand_end}]: cadence {:?}",
372 scheduler.cadence
373 );
374 continue;
375 }
376 }
377 if strand_range.end - strand_range.start > on {
379 strand_range.end = strand_range.start + on;
380 }
381 }
382
383 built_cfg
385 .get_mut(name)
386 .unwrap()
387 .strands
388 .as_mut()
389 .unwrap()
390 .push(strand_range);
391 }
392
393 info!(
394 "Built {} tracking strands for {name}",
395 built_cfg[name].strands.as_ref().unwrap().len()
396 );
397 }
398 }
399 let mut cfg_as_vec = Vec::new();
401 for (name, cfg) in &built_cfg {
402 if let Some(strands) = &cfg.strands {
403 for (ii, strand) in strands.iter().enumerate() {
404 cfg_as_vec.push((name.clone(), ii, *strand));
405 }
406 }
407 }
408 cfg_as_vec.sort_by_key(|(_, _, strand)| strand.start);
410 for (ii, (this_name, this_pos, this_strand)) in
411 cfg_as_vec.iter().take(cfg_as_vec.len() - 1).enumerate()
412 {
413 if let Some(config) = self.configs[this_name].scheduler.as_ref() {
415 if let Some((next_name, next_pos, next_strand)) = cfg_as_vec.get(ii + 1) {
417 if config.handoff == Handoff::Greedy && this_strand.end >= next_strand.start {
418 let next_config = built_cfg.get_mut(next_name).unwrap();
420 let new_start = this_strand.end + next_config.sampling;
421 next_config.strands.as_mut().unwrap()[*next_pos].start = new_start;
422 info!("Greedy handoff for {this_name}: {next_name} delayed to {new_start}");
423 } else if config.handoff == Handoff::Eager
424 && this_strand.end >= next_strand.start
425 {
426 let this_config = built_cfg.get_mut(this_name).unwrap();
427 let new_end = next_strand.start - this_config.sampling;
428 this_config.strands.as_mut().unwrap()[*this_pos].end = new_end;
429 info!("Eager handoff for {this_name}: {this_name} terminated at {new_end}");
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}