Skip to main content

nyx_space/od/simulator/
arc.rs

1/*
2    Nyx, blazing fast astrodynamics
3    Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU Affero General Public License as published
7    by the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU Affero General Public License for more details.
14
15    You should have received a copy of the GNU Affero General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use 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    /// Map of devices from their names.
58    pub devices: BTreeMap<String, D>,
59    /// Receiver trajectory
60    pub trajectory: Traj<MsrIn>,
61    /// Configuration of each device
62    pub configs: BTreeMap<String, TrkConfig>,
63    /// Random number generator used for this tracking arc, ensures repeatability
64    rng: Pcg64Mcg,
65    /// Greatest common denominator time series that allows this arc to meet all of the conditions.
66    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    /// Build a new tracking arc simulator using the provided seeded random number generator.
80    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        // Check that each device has an associated configurations.
87        // We don't care if there are more configurations than chosen devices.
88        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        // The overall time series is the one going from the start to the end of the trajectory with the smallest time step
113        // of all the tracking configurations.
114        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    /// Build a new tracking arc simulator using the provided seed to initialize the random number generator.
135    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    /// Build a new tracking arc simulator using the system entropy to seed the random number generator.
147    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    /// Generates measurements for the tracking arc using the defined strands
158    ///
159    /// # Warning
160    /// This function will return an error if any of the devices defines as a scheduler.
161    /// You must create the schedule first using `build_schedule` first.
162    ///
163    /// # Notes
164    /// Although mutable, this function may be called several times to generate different measurements.
165    ///
166    /// # Algorithm
167    /// For each tracking device, and for each strand within that device, sample the trajectory at the sample
168    /// rate of the tracking device, adding a measurement whenever the spacecraft is visible.
169    /// Build the measurements as a vector, ordered chronologically.
170    ///
171    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 are defined at this point
197                        'strands: for (ii, strand) in strands.iter().enumerate() {
198                            // Build the time series for this strand, sampling at the correct rate
199                            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        // Build the tracking arc.
241        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
271// Literally the same as above, but can't make it generic =(
272impl TrackingArcSim<Spacecraft, GroundStation> {
273    /// Builds the schedule provided the config. Requires the tracker to be a ground station.
274    ///
275    /// # Algorithm
276    ///
277    /// 1. For each tracking device:
278    /// 2. Find when the vehicle trajectory has an elevation greater or equal to zero, and use that as the first start of the first tracking arc for this station
279    /// 3. Find when the vehicle trajectory has an elevation less than zero (i.e. disappears below the horizon), after that initial epoch
280    /// 4. Repeat 2, 3 until the end of the trajectory
281    /// 5. Build each of these as "tracking strands" for this tracking device.
282    /// 6. Organize all of the built tracking strands chronologically.
283    /// 7. Iterate through all of the strands:
284    ///    7.a. if that tracker is marked as `Greedy` and it ends after the start of the next strand, change the start date of the next strand.
285    ///    7.b. if that tracker is marked as `Eager` and it ends after the start of the next strand, change the end date of the current strand.
286    pub fn generate_schedule(
287        &self,
288        almanac: Arc<Almanac>,
289    ) -> Result<BTreeMap<String, TrkConfig>, AnalysisError> {
290        // Build a Location Dataset so we can use ANISE's visibility finder.
291        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        // Deep clone of the Almanac so we can insert the locations of these ground stations.
304        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 there is an alignment, apply it
349                        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                            // Check that the next start time is within the allocated time
356                            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                                    // We're turning on the tracking sooner than the schedule allows, so let's fix that.
361                                    strand_range.start = prev_strand.end + off;
362                                    // Check that we didn't eat into the whole tracking opportunity
363                                    if strand_range.start > strand_end {
364                                        // Lost this whole opportunity.
365                                        info!("Discarding {name} opportunity from {strand_start} to {strand_end} due to cadence {:?}", scheduler.cadence);
366                                        continue;
367                                    }
368                                }
369                            }
370                            // Check that we aren't tracking for longer than configured
371                            if strand_range.end - strand_range.start > on {
372                                strand_range.end = strand_range.start + on;
373                            }
374                        }
375
376                        // We've found when the spacecraft is below the horizon, so this is a new strand.
377                        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        // Build all of the strands, remembering which tracker they come from.
394        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        // Iterate through the strands by chronological order. Cannot use maps because we change types.
403        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            // Grab the config
408            if let Some(config) = self.configs[this_name].scheduler.as_ref() {
409                // Grab the next strand, chronologically
410                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                        // Modify the built configurations to change the start time of the next strand because the current one is greedy.
413                        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                    // Reached the end
433                    break;
434                }
435            }
436        }
437
438        Ok(built_cfg)
439    }
440
441    /// Sets the schedule to that built in `build_schedule`
442    pub fn build_schedule(&mut self, almanac: Arc<Almanac>) -> Result<(), AnalysisError> {
443        self.configs = self.generate_schedule(almanac)?;
444
445        Ok(())
446    }
447}