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}