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 hifitime::{Duration, Epoch, TimeSeries, TimeUnits};
21use log::{info, warn};
22use num::integer::gcd;
23use rand::SeedableRng;
24use rand_pcg::Pcg64Mcg;
25
26use crate::dynamics::NyxError;
27use crate::io::ConfigError;
28use crate::md::trajectory::Interpolatable;
29use crate::od::msr::TrackingDataArc;
30use crate::od::prelude::Strand;
31use crate::od::simulator::Cadence;
32use crate::od::GroundStation;
33use crate::Spacecraft;
34use crate::State;
35use crate::{linalg::allocator::Allocator, od::TrackingDevice};
36use crate::{linalg::DefaultAllocator, md::prelude::Traj};
37use std::collections::BTreeMap;
38use std::fmt::Display;
39use std::marker::PhantomData;
40use std::sync::Arc;
41
42use super::{Handoff, TrkConfig};
43
44#[derive(Clone)]
45pub struct TrackingArcSim<MsrIn, D>
46where
47    D: TrackingDevice<MsrIn>,
48    MsrIn: State,
49    MsrIn: Interpolatable,
50    DefaultAllocator: Allocator<<MsrIn as State>::Size>
51        + Allocator<<MsrIn as State>::Size, <MsrIn as State>::Size>
52        + Allocator<<MsrIn as State>::VecLength>,
53{
54    /// Map of devices from their names.
55    pub devices: BTreeMap<String, D>,
56    /// Receiver trajectory
57    pub trajectory: Traj<MsrIn>,
58    /// Configuration of each device
59    pub configs: BTreeMap<String, TrkConfig>,
60    /// Random number generator used for this tracking arc, ensures repeatability
61    rng: Pcg64Mcg,
62    /// Greatest common denominator time series that allows this arc to meet all of the conditions.
63    time_series: TimeSeries,
64    _msr_in: PhantomData<MsrIn>,
65}
66
67impl<MsrIn, D> TrackingArcSim<MsrIn, D>
68where
69    D: TrackingDevice<MsrIn>,
70    MsrIn: State,
71    MsrIn: Interpolatable,
72    DefaultAllocator: Allocator<<MsrIn as State>::Size>
73        + Allocator<<MsrIn as State>::Size, <MsrIn as State>::Size>
74        + Allocator<<MsrIn as State>::VecLength>,
75{
76    /// Build a new tracking arc simulator using the provided seeded random number generator.
77    pub fn with_rng(
78        devices: BTreeMap<String, D>,
79        trajectory: Traj<MsrIn>,
80        configs: BTreeMap<String, TrkConfig>,
81        rng: Pcg64Mcg,
82    ) -> Result<Self, ConfigError> {
83        // Check that each device has an associated configurations.
84        // We don't care if there are more configurations than chosen devices.
85        let mut sampling_rates_ns = Vec::with_capacity(devices.len());
86        for name in devices.keys() {
87            if let Some(cfg) = configs.get(name) {
88                if let Err(e) = cfg.sanity_check() {
89                    warn!("Ignoring device {name}: {e}");
90                    continue;
91                }
92                sampling_rates_ns.push(cfg.sampling.truncated_nanoseconds());
93            } else {
94                warn!("Ignoring device {name}: no associated tracking configuration",);
95                continue;
96            }
97        }
98
99        if sampling_rates_ns.is_empty() {
100            return Err(ConfigError::InvalidConfig {
101                msg: "None of the devices are properly configured".to_string(),
102            });
103        }
104
105        let common_sampling_rate_ns = sampling_rates_ns
106            .iter()
107            .fold(sampling_rates_ns[0], |a, &b| gcd(a, b));
108
109        // The overall time series is the one going from the start to the end of the trajectory with the smallest time step
110        // of all the tracking configurations.
111        let time_series = TimeSeries::inclusive(
112            trajectory.first().epoch(),
113            trajectory.last().epoch(),
114            Duration::from_truncated_nanoseconds(common_sampling_rate_ns),
115        );
116
117        let me = Self {
118            devices,
119            trajectory,
120            configs,
121            rng,
122            time_series,
123            _msr_in: PhantomData,
124        };
125
126        info!("{me}");
127
128        Ok(me)
129    }
130
131    /// Build a new tracking arc simulator using the provided seed to initialize the random number generator.
132    pub fn with_seed(
133        devices: BTreeMap<String, D>,
134        trajectory: Traj<MsrIn>,
135        configs: BTreeMap<String, TrkConfig>,
136        seed: u64,
137    ) -> Result<Self, ConfigError> {
138        let rng = Pcg64Mcg::new(seed as u128);
139
140        Self::with_rng(devices, trajectory, configs, rng)
141    }
142
143    /// Build a new tracking arc simulator using the system entropy to seed the random number generator.
144    pub fn new(
145        devices: BTreeMap<String, D>,
146        trajectory: Traj<MsrIn>,
147        configs: BTreeMap<String, TrkConfig>,
148    ) -> Result<Self, ConfigError> {
149        let rng = Pcg64Mcg::from_os_rng();
150
151        Self::with_rng(devices, trajectory, configs, rng)
152    }
153
154    /// Generates measurements for the tracking arc using the defined strands
155    ///
156    /// # Warning
157    /// This function will return an error if any of the devices defines as a scheduler.
158    /// You must create the schedule first using `build_schedule` first.
159    ///
160    /// # Notes
161    /// Although mutable, this function may be called several times to generate different measurements.
162    ///
163    /// # Algorithm
164    /// For each tracking device, and for each strand within that device, sample the trajectory at the sample
165    /// rate of the tracking device, adding a measurement whenever the spacecraft is visible.
166    /// Build the measurements as a vector, ordered chronologically.
167    ///
168    pub fn generate_measurements(
169        &mut self,
170        almanac: Arc<Almanac>,
171    ) -> Result<TrackingDataArc, NyxError> {
172        let mut measurements = BTreeMap::new();
173
174        for (name, device) in self.devices.iter_mut() {
175            if let Some(cfg) = self.configs.get(name) {
176                if cfg.scheduler.is_some() {
177                    if cfg.strands.is_none() {
178                        return Err(NyxError::CustomError {
179                            msg: format!(
180                                "schedule for {name} must be built before generating measurements"
181                            ),
182                        });
183                    } else {
184                        warn!("scheduler for {name} is ignored, using the defined tracking strands instead")
185                    }
186                }
187
188                let init_msr_count = measurements.len();
189                let tick = Epoch::now().unwrap();
190
191                match cfg.strands.as_ref() {
192                    Some(strands) => {
193                        // Strands are defined at this point
194                        'strands: for (ii, strand) in strands.iter().enumerate() {
195                            // Build the time series for this strand, sampling at the correct rate
196                            for epoch in
197                                TimeSeries::inclusive(strand.start, strand.end, cfg.sampling)
198                            {
199                                match device.measure(
200                                    epoch,
201                                    &self.trajectory,
202                                    Some(&mut self.rng),
203                                    almanac.clone(),
204                                ) {
205                                    Ok(msr_opt) => {
206                                        if let Some(msr) = msr_opt {
207                                            measurements.insert(epoch, msr);
208                                        }
209                                    }
210                                    Err(e) => {
211                                        if epoch != strand.end {
212                                            warn!(
213                                            "Skipping the remaining strand #{ii} ending on {}: {e}",
214                                            strand.end
215                                        );
216                                        }
217                                        continue 'strands;
218                                    }
219                                }
220                            }
221                        }
222
223                        info!(
224                            "Simulated {} measurements for {name} for {} tracking strands in {}",
225                            measurements.len() - init_msr_count,
226                            strands.len(),
227                            (Epoch::now().unwrap() - tick).round(1.0_f64.milliseconds())
228                        );
229                    }
230                    None => {
231                        warn!("No tracking strands defined for {name}, skipping");
232                    }
233                }
234            }
235        }
236
237        // Build the tracking arc.
238        let trk_data = TrackingDataArc {
239            measurements,
240            source: None,
241            moduli: None,
242            force_reject: false,
243        };
244
245        Ok(trk_data)
246    }
247}
248
249impl<MsrIn, D> Display for TrackingArcSim<MsrIn, D>
250where
251    D: TrackingDevice<MsrIn>,
252    MsrIn: Interpolatable,
253    DefaultAllocator: Allocator<<MsrIn as State>::Size>
254        + Allocator<<MsrIn as State>::Size, <MsrIn as State>::Size>
255        + Allocator<<MsrIn as State>::VecLength>,
256{
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        write!(
259            f,
260            "Tracking Arc Simulator on {} with devices {:?} over {}",
261            self.trajectory,
262            self.devices.keys(),
263            self.time_series
264        )
265    }
266}
267
268// Literally the same as above, but can't make it generic =(
269impl TrackingArcSim<Spacecraft, GroundStation> {
270    /// Builds the schedule provided the config. Requires the tracker to be a ground station.
271    ///
272    /// # Algorithm
273    ///
274    /// 1. For each tracking device:
275    /// 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
276    /// 3. Find when the vehicle trajectory has an elevation less than zero (i.e. disappears below the horizon), after that initial epoch
277    /// 4. Repeat 2, 3 until the end of the trajectory
278    /// 5. Build each of these as "tracking strands" for this tracking device.
279    /// 6. Organize all of the built tracking strands chronologically.
280    /// 7. Iterate through all of the strands:
281    ///    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.
282    ///    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.
283    pub fn generate_schedule(
284        &self,
285        almanac: Arc<Almanac>,
286    ) -> Result<BTreeMap<String, TrkConfig>, NyxError> {
287        // Consider using find_all via the heuristic
288        let mut built_cfg = self.configs.clone();
289
290        for (name, device) in self.devices.iter() {
291            if let Some(cfg) = self.configs.get(name) {
292                if let Some(scheduler) = cfg.scheduler {
293                    info!("Building schedule for {name}");
294                    if scheduler.handoff == Handoff::Overlap {
295                        warn!("Overlapping measurements on {name} is no longer supported on identical epochs.");
296                    }
297                    built_cfg.get_mut(name).unwrap().scheduler = None;
298                    built_cfg.get_mut(name).unwrap().strands = Some(Vec::new());
299
300                    // Convert the trajectory into the ground station frame.
301                    let traj = self.trajectory.to_frame(device.frame, almanac.clone())?;
302
303                    match traj.find_arcs(&device, None, almanac.clone()) {
304                        Err(_) => info!("No measurements from {name}"),
305                        Ok(elevation_arcs) => {
306                            for arc in elevation_arcs {
307                                let strand_start = arc.rise.state.epoch();
308                                let strand_end = arc.fall.state.epoch();
309
310                                if strand_end - strand_start
311                                    < cfg.sampling * i64::from(scheduler.min_samples)
312                                {
313                                    info!(
314                                    "Too few samples from {name} opportunity from {strand_start} to {strand_end}, discarding strand",
315                                );
316                                    continue;
317                                }
318
319                                let mut strand_range = Strand {
320                                    start: strand_start,
321                                    end: strand_end,
322                                };
323
324                                // If there is an alignment, apply it
325                                if let Some(alignment) = scheduler.sample_alignment {
326                                    strand_range.start = strand_range.start.round(alignment);
327                                    strand_range.end = strand_range.end.round(alignment);
328                                }
329
330                                if let Cadence::Intermittent { on, off } = scheduler.cadence {
331                                    // Check that the next start time is within the allocated time
332                                    if let Some(prev_strand) =
333                                        built_cfg[name].strands.as_ref().unwrap().last()
334                                    {
335                                        if prev_strand.end + off > strand_range.start {
336                                            // We're turning on the tracking sooner than the schedule allows, so let's fix that.
337                                            strand_range.start = prev_strand.end + off;
338                                            // Check that we didn't eat into the whole tracking opportunity
339                                            if strand_range.start > strand_end {
340                                                // Lost this whole opportunity.
341                                                info!("Discarding {name} opportunity from {strand_start} to {strand_end} due to cadence {:?}", scheduler.cadence);
342                                                continue;
343                                            }
344                                        }
345                                    }
346                                    // Check that we aren't tracking for longer than configured
347                                    if strand_range.end - strand_range.start > on {
348                                        strand_range.end = strand_range.start + on;
349                                    }
350                                }
351
352                                // We've found when the spacecraft is below the horizon, so this is a new strand.
353                                built_cfg
354                                    .get_mut(name)
355                                    .unwrap()
356                                    .strands
357                                    .as_mut()
358                                    .unwrap()
359                                    .push(strand_range);
360                            }
361
362                            info!(
363                                "Built {} tracking strands for {name}",
364                                built_cfg[name].strands.as_ref().unwrap().len()
365                            );
366                        }
367                    }
368                }
369            }
370        }
371        // Build all of the strands, remembering which tracker they come from.
372        let mut cfg_as_vec = Vec::new();
373        for (name, cfg) in &built_cfg {
374            if let Some(strands) = &cfg.strands {
375                for (ii, strand) in strands.iter().enumerate() {
376                    cfg_as_vec.push((name.clone(), ii, *strand));
377                }
378            }
379        }
380        // Iterate through the strands by chronological order. Cannot use maps because we change types.
381        cfg_as_vec.sort_by_key(|(_, _, strand)| strand.start);
382        for (ii, (this_name, this_pos, this_strand)) in
383            cfg_as_vec.iter().take(cfg_as_vec.len() - 1).enumerate()
384        {
385            // Grab the config
386            if let Some(config) = self.configs[this_name].scheduler.as_ref() {
387                // Grab the next strand, chronologically
388                if let Some((next_name, next_pos, next_strand)) = cfg_as_vec.get(ii + 1) {
389                    if config.handoff == Handoff::Greedy && this_strand.end >= next_strand.start {
390                        // Modify the built configurations to change the start time of the next strand because the current one is greedy.
391                        let next_config = built_cfg.get_mut(next_name).unwrap();
392                        let new_start = this_strand.end + next_config.sampling;
393                        next_config.strands.as_mut().unwrap()[*next_pos].start = new_start;
394                        info!(
395                            "{this_name} configured as {:?}, so {next_name} now starts on {new_start}",
396                            config.handoff
397                        );
398                    } else if config.handoff == Handoff::Eager
399                        && this_strand.end >= next_strand.start
400                    {
401                        let this_config = built_cfg.get_mut(this_name).unwrap();
402                        let new_end = next_strand.start - this_config.sampling;
403                        this_config.strands.as_mut().unwrap()[*this_pos].end = new_end;
404                        info!(
405                            "{this_name} now hands off to {next_name} on {new_end} because it's configured as {:?}",
406                            config.handoff
407                        );
408                    }
409                } else {
410                    // Reached the end
411                    break;
412                }
413            }
414        }
415
416        Ok(built_cfg)
417    }
418
419    /// Sets the schedule to that built in `build_schedule`
420    pub fn build_schedule(&mut self, almanac: Arc<Almanac>) -> Result<(), NyxError> {
421        self.configs = self.generate_schedule(almanac)?;
422
423        Ok(())
424    }
425}