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::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    /// Map of devices from their names.
57    pub devices: BTreeMap<String, D>,
58    /// Receiver trajectory
59    pub trajectory: Traj<MsrIn>,
60    /// Configuration of each device
61    pub configs: BTreeMap<String, TrkConfig>,
62    /// Random number generator used for this tracking arc, ensures repeatability
63    rng: Pcg64Mcg,
64    /// Greatest common denominator time series that allows this arc to meet all of the conditions.
65    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    /// Build a new tracking arc simulator using the provided seeded random number generator.
79    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        // Check that each device has an associated configurations.
86        // We don't care if there are more configurations than chosen devices.
87        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        // The overall time series is the one going from the start to the end of the trajectory with the smallest time step
112        // of all the tracking configurations.
113        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    /// Build a new tracking arc simulator using the provided seed to initialize the random number generator.
134    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    /// Build a new tracking arc simulator using the system entropy to seed the random number generator.
146    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    /// Generates measurements for the tracking arc using the defined strands
157    ///
158    /// # Warning
159    /// This function will return an error if any of the devices defines as a scheduler.
160    /// You must create the schedule first using `build_schedule` first.
161    ///
162    /// # Notes
163    /// Although mutable, this function may be called several times to generate different measurements.
164    ///
165    /// # Algorithm
166    /// For each tracking device, and for each strand within that device, sample the trajectory at the sample
167    /// rate of the tracking device, adding a measurement whenever the spacecraft is visible.
168    /// Build the measurements as a vector, ordered chronologically.
169    ///
170    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 are defined at this point
196                        'strands: for (ii, strand) in strands.iter().enumerate() {
197                            // Build the time series for this strand, sampling at the correct rate
198                            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        // Build the tracking arc.
240        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
270// Literally the same as above, but can't make it generic =(
271impl TrackingArcSim<Spacecraft, GroundStation> {
272    /// Builds the schedule provided the config. Requires the tracker to be a ground station.
273    ///
274    /// # Algorithm
275    ///
276    /// 1. For each tracking device:
277    /// 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
278    /// 3. Find when the vehicle trajectory has an elevation less than zero (i.e. disappears below the horizon), after that initial epoch
279    /// 4. Repeat 2, 3 until the end of the trajectory
280    /// 5. Build each of these as "tracking strands" for this tracking device.
281    /// 6. Organize all of the built tracking strands chronologically.
282    /// 7. Iterate through all of the strands:
283    ///    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.
284    ///    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.
285    pub fn generate_schedule(
286        &self,
287        almanac: Arc<Almanac>,
288    ) -> Result<BTreeMap<String, TrkConfig>, AnalysisError> {
289        // Build a Location Dataset so we can use ANISE's visibility finder.
290        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        // Deep clone of the Almanac so we can insert the locations of these ground stations.
303        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 there is an alignment, apply it
348                        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                            // Check that the next start time is within the allocated time
355                            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                                    // We're turning on the tracking sooner than the schedule allows, so let's fix that.
360                                    strand_range.start = prev_strand.end + off;
361                                    // Check that we didn't eat into the whole tracking opportunity
362                                    if strand_range.start > strand_end {
363                                        // Lost this whole opportunity.
364                                        info!("Discarding {name} opportunity from {strand_start} to {strand_end} due to cadence {:?}", scheduler.cadence);
365                                        continue;
366                                    }
367                                }
368                            }
369                            // Check that we aren't tracking for longer than configured
370                            if strand_range.end - strand_range.start > on {
371                                strand_range.end = strand_range.start + on;
372                            }
373                        }
374
375                        // We've found when the spacecraft is below the horizon, so this is a new strand.
376                        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        // Build all of the strands, remembering which tracker they come from.
393        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        // Iterate through the strands by chronological order. Cannot use maps because we change types.
402        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            // Grab the config
407            if let Some(config) = self.configs[this_name].scheduler.as_ref() {
408                // Grab the next strand, chronologically
409                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                        // Modify the built configurations to change the start time of the next strand because the current one is greedy.
412                        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                    // Reached the end
432                    break;
433                }
434            }
435        }
436
437        Ok(built_cfg)
438    }
439
440    /// Sets the schedule to that built in `build_schedule`
441    pub fn build_schedule(&mut self, almanac: Arc<Almanac>) -> Result<(), AnalysisError> {
442        self.configs = self.generate_schedule(almanac)?;
443
444        Ok(())
445    }
446}