anise/almanac/
bpc.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::naif::daf::NAIFSummaryRecord;
20use crate::naif::pck::BPCSummaryRecord;
21use crate::naif::BPC;
22use crate::orientations::{NoOrientationsLoadedSnafu, OrientationError};
23use crate::{naif::daf::DAFError, NaifId};
24
25use super::{Almanac, MAX_LOADED_BPCS};
26
27impl Almanac {
28    pub fn from_bpc(bpc: BPC) -> Result<Almanac, OrientationError> {
29        let me = Self::default();
30        me.with_bpc(bpc)
31    }
32
33    /// Loads a Binary Planetary Constants kernel.
34    pub fn with_bpc(&self, bpc: BPC) -> Result<Self, OrientationError> {
35        // This is just a bunch of pointers so it doesn't use much memory.
36        let mut me = self.clone();
37        let mut data_idx = MAX_LOADED_BPCS;
38        for (idx, item) in self.bpc_data.iter().enumerate() {
39            if item.is_none() {
40                data_idx = idx;
41                break;
42            }
43        }
44        if data_idx == MAX_LOADED_BPCS {
45            return Err(OrientationError::StructureIsFull {
46                max_slots: MAX_LOADED_BPCS,
47            });
48        }
49        me.bpc_data[data_idx] = Some(bpc);
50        Ok(me)
51    }
52
53    pub fn num_loaded_bpc(&self) -> usize {
54        let mut count = 0;
55        for maybe in &self.bpc_data {
56            if maybe.is_none() {
57                break;
58            } else {
59                count += 1;
60            }
61        }
62
63        count
64    }
65
66    /// Returns the summary given the name of the summary record if that summary has data defined at the requested epoch and the BPC where this name was found to be valid at that epoch.
67    pub fn bpc_summary_from_name_at_epoch(
68        &self,
69        name: &str,
70        epoch: Epoch,
71    ) -> Result<(&BPCSummaryRecord, usize, usize), OrientationError> {
72        for (no, maybe_bpc) in self
73            .bpc_data
74            .iter()
75            .take(self.num_loaded_bpc())
76            .rev()
77            .enumerate()
78        {
79            let bpc = maybe_bpc.as_ref().unwrap();
80            if let Ok((summary, idx_in_bpc)) = bpc.summary_from_name_at_epoch(name, epoch) {
81                return Ok((summary, no, idx_in_bpc));
82            }
83        }
84
85        // If we're reached this point, there is no relevant summary at this epoch.
86        Err(OrientationError::BPC {
87            action: "searching for BPC summary",
88            source: DAFError::SummaryNameAtEpochError {
89                kind: "BPC",
90                name: name.to_string(),
91                epoch,
92            },
93        })
94    }
95
96    /// Returns the summary given the name of the summary record if that summary has data defined at the requested epoch
97    pub fn bpc_summary_at_epoch(
98        &self,
99        id: i32,
100        epoch: Epoch,
101    ) -> Result<(&BPCSummaryRecord, usize, usize), OrientationError> {
102        for (no, maybe_bpc) in self
103            .bpc_data
104            .iter()
105            .take(self.num_loaded_bpc())
106            .rev()
107            .enumerate()
108        {
109            let bpc = maybe_bpc.as_ref().unwrap();
110            if let Ok((summary, idx_in_bpc)) = bpc.summary_from_id_at_epoch(id, epoch) {
111                // NOTE: We're iterating backward, so the correct BPC number is "total loaded" minus "current iteration".
112                return Ok((summary, self.num_loaded_bpc() - no - 1, idx_in_bpc));
113            }
114        }
115
116        // If we're reached this point, there is no relevant summary at this epoch.
117        Err(OrientationError::BPC {
118            action: "searching for BPC summary",
119            source: DAFError::SummaryIdAtEpochError {
120                kind: "BPC",
121                id,
122                epoch,
123            },
124        })
125    }
126
127    /// Returns the summary given the name of the summary record.
128    pub fn bpc_summary_from_name(
129        &self,
130        name: &str,
131    ) -> Result<(&BPCSummaryRecord, usize, usize), OrientationError> {
132        for (bpc_no, maybe_bpc) in self
133            .bpc_data
134            .iter()
135            .take(self.num_loaded_bpc())
136            .rev()
137            .enumerate()
138        {
139            let bpc = maybe_bpc.as_ref().unwrap();
140            if let Ok((summary, idx_in_bpc)) = bpc.summary_from_name(name) {
141                return Ok((summary, bpc_no, idx_in_bpc));
142            }
143        }
144
145        // If we're reached this point, there is no relevant summary at this epoch.
146        Err(OrientationError::BPC {
147            action: "searching for BPC summary",
148            source: DAFError::SummaryNameError {
149                kind: "BPC",
150                name: name.to_string(),
151            },
152        })
153    }
154
155    /// Returns the summary given the name of the summary record if that summary has data defined at the requested epoch
156    pub fn bpc_summary(
157        &self,
158        id: i32,
159    ) -> Result<(&BPCSummaryRecord, usize, usize), OrientationError> {
160        for (no, maybe_bpc) in self
161            .bpc_data
162            .iter()
163            .take(self.num_loaded_bpc())
164            .rev()
165            .enumerate()
166        {
167            let bpc = maybe_bpc.as_ref().unwrap();
168            if let Ok((summary, idx_in_bpc)) = bpc.summary_from_id(id) {
169                // NOTE: We're iterating backward, so the correct BPC number is "total loaded" minus "current iteration".
170                return Ok((summary, self.num_loaded_bpc() - no - 1, idx_in_bpc));
171            }
172        }
173
174        // If we're reached this point, there is no relevant summary
175        Err(OrientationError::BPC {
176            action: "searching for BPC summary",
177            source: DAFError::SummaryIdError { kind: "BPC", id },
178        })
179    }
180}
181
182#[cfg_attr(feature = "python", pymethods)]
183impl Almanac {
184    /// 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.
185    ///
186    /// # Warning
187    /// This function performs a memory allocation.
188    ///
189    /// :type id: int
190    /// :rtype: typing.List
191    pub fn bpc_summaries(&self, id: NaifId) -> Result<Vec<BPCSummaryRecord>, OrientationError> {
192        let mut summaries = vec![];
193
194        for maybe_bpc in self.bpc_data.iter().take(self.num_loaded_bpc()).rev() {
195            let bpc = maybe_bpc.as_ref().unwrap();
196            if let Ok(these_summaries) = bpc.data_summaries() {
197                for summary in these_summaries {
198                    if summary.id() == id {
199                        summaries.push(*summary);
200                    }
201                }
202            }
203        }
204
205        if summaries.is_empty() {
206            // If we're reached this point, there is no relevant summary
207            Err(OrientationError::BPC {
208                action: "searching for BPC summary",
209                source: DAFError::SummaryIdError { kind: "BPC", id },
210            })
211        } else {
212            Ok(summaries)
213        }
214    }
215
216    /// Returns the applicable domain of the request id, i.e. start and end epoch that the provided id has loaded data.
217    ///
218    /// :type id: int
219    /// :rtype: typing.Tuple
220    pub fn bpc_domain(&self, id: NaifId) -> Result<(Epoch, Epoch), OrientationError> {
221        let summaries = self.bpc_summaries(id)?;
222
223        // We know that the summaries is non-empty because if it is, the previous function call returns an error.
224        let start = summaries
225            .iter()
226            .min_by_key(|summary| summary.start_epoch())
227            .unwrap()
228            .start_epoch();
229
230        let end = summaries
231            .iter()
232            .max_by_key(|summary| summary.end_epoch())
233            .unwrap()
234            .end_epoch();
235
236        Ok((start, end))
237    }
238
239    /// Returns a map of each loaded BPC ID to its domain validity.
240    ///
241    /// # Warning
242    /// This function performs a memory allocation.
243    ///
244    /// :rtype: typing.Dict
245    pub fn bpc_domains(&self) -> Result<HashMap<NaifId, (Epoch, Epoch)>, OrientationError> {
246        ensure!(self.num_loaded_bpc() > 0, NoOrientationsLoadedSnafu);
247
248        let mut domains = HashMap::new();
249        for maybe_bpc in self.bpc_data.iter().take(self.num_loaded_bpc()).rev() {
250            let bpc = maybe_bpc.as_ref().unwrap();
251            if let Ok(these_summaries) = bpc.data_summaries() {
252                for summary in these_summaries {
253                    let this_id = summary.id();
254                    match domains.get_mut(&this_id) {
255                        Some((ref mut cur_start, ref mut cur_end)) => {
256                            if *cur_start > summary.start_epoch() {
257                                *cur_start = summary.start_epoch();
258                            }
259                            if *cur_end < summary.end_epoch() {
260                                *cur_end = summary.end_epoch();
261                            }
262                        }
263                        None => {
264                            domains.insert(this_id, (summary.start_epoch(), summary.end_epoch()));
265                        }
266                    }
267                }
268            }
269        }
270
271        Ok(domains)
272    }
273}
274
275#[cfg(test)]
276mod ut_almanac_bpc {
277    use crate::prelude::{Almanac, Epoch};
278
279    #[test]
280    fn summaries_nothing_loaded() {
281        let almanac = Almanac::default();
282
283        let e = Epoch::now().unwrap();
284
285        assert!(
286            almanac.bpc_summary(0).is_err(),
287            "empty Almanac should report an error"
288        );
289        assert!(
290            almanac.bpc_summary_at_epoch(0, e).is_err(),
291            "empty Almanac should report an error"
292        );
293        assert!(
294            almanac.bpc_summary_from_name("invalid name").is_err(),
295            "empty Almanac should report an error"
296        );
297        assert!(
298            almanac
299                .bpc_summary_from_name_at_epoch("invalid name", e)
300                .is_err(),
301            "empty Almanac should report an error"
302        );
303    }
304}