anise/almanac/
spk.rs

1/*
2 * ANISE Toolkit
3 * Copyright (C) 2021-onward Christopher Rabotin <christopher.rabotin@gmail.com> et al. (cf. AUTHORS.md)
4 * This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7 *
8 * Documentation: https://nyxspace.com/
9 */
10
11use std::collections::HashMap;
12
13use hifitime::Epoch;
14
15#[cfg(feature = "python")]
16use pyo3::prelude::*;
17use snafu::ensure;
18
19use crate::ephemerides::NoEphemerisLoadedSnafu;
20use crate::naif::daf::DAFError;
21use crate::naif::daf::NAIFSummaryRecord;
22use crate::naif::spk::summary::SPKSummaryRecord;
23use crate::naif::SPK;
24use crate::{ephemerides::EphemerisError, NaifId};
25use log::error;
26
27use super::{Almanac, MAX_LOADED_SPKS};
28
29impl Almanac {
30    pub fn from_spk(spk: SPK) -> Result<Almanac, EphemerisError> {
31        let me = Self::default();
32        me.with_spk(spk)
33    }
34
35    /// Loads a new SPK file into a new context.
36    /// This new context is needed to satisfy the unloading of files. In fact, to unload a file, simply let the newly loaded context drop out of scope and Rust will clean it up.
37    pub fn with_spk(&self, spk: SPK) -> Result<Self, EphemerisError> {
38        // This is just a bunch of pointers so it doesn't use much memory.
39        let mut me = self.clone();
40        // Parse as SPK and place into the SPK list if there is room
41        let mut data_idx = MAX_LOADED_SPKS;
42        for (idx, item) in self.spk_data.iter().enumerate() {
43            if item.is_none() {
44                data_idx = idx;
45                break;
46            }
47        }
48        if data_idx == MAX_LOADED_SPKS {
49            return Err(EphemerisError::StructureIsFull {
50                max_slots: MAX_LOADED_SPKS,
51            });
52        }
53        me.spk_data[data_idx] = Some(spk);
54        Ok(me)
55    }
56}
57
58impl Almanac {
59    pub fn num_loaded_spk(&self) -> usize {
60        let mut count = 0;
61        for maybe in &self.spk_data {
62            if maybe.is_none() {
63                break;
64            } else {
65                count += 1;
66            }
67        }
68
69        count
70    }
71
72    /// Returns the summary given the name of the summary record if that summary has data defined at the requested epoch and the SPK where this name was found to be valid at that epoch.
73    pub fn spk_summary_from_name_at_epoch(
74        &self,
75        name: &str,
76        epoch: Epoch,
77    ) -> Result<(&SPKSummaryRecord, usize, usize), EphemerisError> {
78        for (spk_no, maybe_spk) in self
79            .spk_data
80            .iter()
81            .take(self.num_loaded_spk())
82            .rev()
83            .enumerate()
84        {
85            let spk = maybe_spk.as_ref().unwrap();
86            if let Ok((summary, idx_in_spk)) = spk.summary_from_name_at_epoch(name, epoch) {
87                return Ok((summary, spk_no, idx_in_spk));
88            }
89        }
90
91        // If we're reached this point, there is no relevant summary at this epoch.
92        error!("Almanac: No summary {name} valid at epoch {epoch}");
93        Err(EphemerisError::SPK {
94            action: "searching for SPK summary",
95            source: DAFError::SummaryNameAtEpochError {
96                kind: "SPK",
97                name: name.to_string(),
98                epoch,
99            },
100        })
101    }
102
103    /// Returns the summary given the name of the summary record if that summary has data defined at the requested epoch
104    pub fn spk_summary_at_epoch(
105        &self,
106        id: i32,
107        epoch: Epoch,
108    ) -> Result<(&SPKSummaryRecord, usize, usize), EphemerisError> {
109        for (spk_no, maybe_spk) in self
110            .spk_data
111            .iter()
112            .take(self.num_loaded_spk())
113            .rev()
114            .enumerate()
115        {
116            let spk = maybe_spk.as_ref().unwrap();
117            if let Ok((summary, idx_in_spk)) = spk.summary_from_id_at_epoch(id, epoch) {
118                // NOTE: We're iterating backward, so the correct SPK number is "total loaded" minus "current iteration".
119                return Ok((summary, self.num_loaded_spk() - spk_no - 1, idx_in_spk));
120            }
121        }
122
123        error!("Almanac: No summary {id} valid at epoch {epoch}");
124        // If we're reached this point, there is no relevant summary at this epoch.
125        Err(EphemerisError::SPK {
126            action: "searching for SPK summary",
127            source: DAFError::SummaryIdAtEpochError {
128                kind: "SPK",
129                id,
130                epoch,
131            },
132        })
133    }
134
135    /// Returns the most recently loaded summary by its name, if any with that ID are available
136    pub fn spk_summary_from_name(
137        &self,
138        name: &str,
139    ) -> Result<(&SPKSummaryRecord, usize, usize), EphemerisError> {
140        for (spk_no, maybe_spk) in self
141            .spk_data
142            .iter()
143            .take(self.num_loaded_spk())
144            .rev()
145            .enumerate()
146        {
147            let spk = maybe_spk.as_ref().unwrap();
148            if let Ok((summary, idx_in_spk)) = spk.summary_from_name(name) {
149                return Ok((summary, spk_no, idx_in_spk));
150            }
151        }
152
153        // If we're reached this point, there is no relevant summary at this epoch.
154        error!("Almanac: No summary {name} valid");
155
156        Err(EphemerisError::SPK {
157            action: "searching for SPK summary",
158            source: DAFError::SummaryNameError {
159                kind: "SPK",
160                name: name.to_string(),
161            },
162        })
163    }
164
165    /// Returns the most recently loaded summary by its ID, if any with that ID are available
166    pub fn spk_summary(
167        &self,
168        id: i32,
169    ) -> Result<(&SPKSummaryRecord, usize, usize), EphemerisError> {
170        for (spk_no, maybe_spk) in self
171            .spk_data
172            .iter()
173            .take(self.num_loaded_spk())
174            .rev()
175            .enumerate()
176        {
177            let spk = maybe_spk.as_ref().unwrap();
178            if let Ok((summary, idx_in_spk)) = spk.summary_from_id(id) {
179                // NOTE: We're iterating backward, so the correct SPK number is "total loaded" minus "current iteration".
180                return Ok((summary, self.num_loaded_spk() - spk_no - 1, idx_in_spk));
181            }
182        }
183
184        error!("Almanac: No summary {id} valid");
185        // If we're reached this point, there is no relevant summary
186        Err(EphemerisError::SPK {
187            action: "searching for SPK summary",
188            source: DAFError::SummaryIdError { kind: "SPK", id },
189        })
190    }
191}
192
193#[cfg_attr(feature = "python", pymethods)]
194impl Almanac {
195    /// Returns a vector of the summaries whose ID matches the desired `id`, in the order in which they will be used, i.e. in reverse loading order.
196    ///
197    /// # Warning
198    /// This function performs a memory allocation.
199    ///
200    /// :type id: int
201    /// :rtype: typing.List
202    pub fn spk_summaries(&self, id: NaifId) -> Result<Vec<SPKSummaryRecord>, EphemerisError> {
203        let mut summaries = vec![];
204        for maybe_spk in self.spk_data.iter().take(self.num_loaded_spk()).rev() {
205            let spk = maybe_spk.as_ref().unwrap();
206            if let Ok(these_summaries) = spk.data_summaries() {
207                for summary in these_summaries {
208                    if summary.id() == id {
209                        summaries.push(*summary);
210                    }
211                }
212            }
213        }
214
215        if summaries.is_empty() {
216            error!("Almanac: No summary {id} valid");
217            // If we're reached this point, there is no relevant summary
218            Err(EphemerisError::SPK {
219                action: "searching for SPK summary",
220                source: DAFError::SummaryIdError { kind: "SPK", id },
221            })
222        } else {
223            Ok(summaries)
224        }
225    }
226
227    /// Returns the applicable domain of the request id, i.e. start and end epoch that the provided id has loaded data.
228    ///
229    /// :type id: int
230    /// :rtype: typing.Tuple
231    pub fn spk_domain(&self, id: NaifId) -> Result<(Epoch, Epoch), EphemerisError> {
232        let summaries = self.spk_summaries(id)?;
233
234        // We know that the summaries is non-empty because if it is, the previous function call returns an error.
235        let start = summaries
236            .iter()
237            .min_by_key(|summary| summary.start_epoch())
238            .unwrap()
239            .start_epoch();
240
241        let end = summaries
242            .iter()
243            .max_by_key(|summary| summary.end_epoch())
244            .unwrap()
245            .end_epoch();
246
247        Ok((start, end))
248    }
249
250    /// Returns a map of each loaded SPK ID to its domain validity.
251    ///
252    /// # Warning
253    /// This function performs a memory allocation.
254    ///
255    /// :rtype: typing.Dict
256    pub fn spk_domains(&self) -> Result<HashMap<NaifId, (Epoch, Epoch)>, EphemerisError> {
257        ensure!(self.num_loaded_spk() > 0, NoEphemerisLoadedSnafu);
258
259        let mut domains = HashMap::new();
260        for maybe_spk in self.spk_data.iter().take(self.num_loaded_spk()).rev() {
261            let spk = maybe_spk.as_ref().unwrap();
262            if let Ok(these_summaries) = spk.data_summaries() {
263                for summary in these_summaries {
264                    let this_id = summary.id();
265                    match domains.get_mut(&this_id) {
266                        Some((ref mut cur_start, ref mut cur_end)) => {
267                            if *cur_start > summary.start_epoch() {
268                                *cur_start = summary.start_epoch();
269                            }
270                            if *cur_end < summary.end_epoch() {
271                                *cur_end = summary.end_epoch();
272                            }
273                        }
274                        None => {
275                            domains.insert(this_id, (summary.start_epoch(), summary.end_epoch()));
276                        }
277                    }
278                }
279            }
280        }
281
282        Ok(domains)
283    }
284}
285
286#[cfg(test)]
287mod ut_almanac_spk {
288    use crate::{
289        constants::frames::{EARTH_J2000, MOON_J2000},
290        prelude::{Almanac, Epoch},
291    };
292
293    #[test]
294    fn summaries_nothing_loaded() {
295        let almanac = Almanac::default();
296        let e = Epoch::now().unwrap();
297
298        assert!(
299            almanac.spk_summary(0).is_err(),
300            "empty Almanac should report an error"
301        );
302        assert!(
303            almanac.spk_summary_at_epoch(0, e).is_err(),
304            "empty Almanac should report an error"
305        );
306        assert!(
307            almanac.spk_summary_from_name("invalid name").is_err(),
308            "empty Almanac should report an error"
309        );
310        assert!(
311            almanac
312                .spk_summary_from_name_at_epoch("invalid name", e)
313                .is_err(),
314            "empty Almanac should report an error"
315        );
316    }
317
318    #[test]
319    fn queries_nothing_loaded() {
320        let almanac = Almanac::default();
321        let e = Epoch::now().unwrap();
322
323        assert!(
324            almanac.try_find_ephemeris_root().is_err(),
325            "empty Almanac should report an error"
326        );
327
328        assert!(
329            almanac.ephemeris_path_to_root(MOON_J2000, e).is_err(),
330            "empty Almanac should report an error"
331        );
332
333        assert!(
334            almanac
335                .common_ephemeris_path(MOON_J2000, EARTH_J2000, e)
336                .is_err(),
337            "empty Almanac should report an error"
338        );
339    }
340}