anise 0.9.6

Core of the ANISE library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
/*
 * ANISE Toolkit
 * Copyright (C) 2021-onward Christopher Rabotin <christopher.rabotin@gmail.com> et al. (cf. AUTHORS.md)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 *
 * Documentation: https://nyxspace.com/
 */

use super::{planetary::PlanetaryDataError, Almanac};
use crate::constants::orientations::J2000;
use crate::ephemerides::ephemeris::Ephemeris;
use crate::errors::EphemerisSnafu;
use crate::{
    astro::{Aberration, AzElRange, Location, Occultation},
    ephemerides::EphemerisError,
    errors::AlmanacResult,
    math::{cartesian::CartesianState, rotation::DCM},
    orientations::OrientationError,
    prelude::{Frame, Orbit},
    NaifId,
};
use hifitime::{Epoch, TimeScale, TimeSeries};
use ndarray::Array1;
use numpy::PyArray1;
use pyo3::prelude::*;
use pyo3::types::PyType;
use rayon::prelude::*;
use snafu::ResultExt;

#[pymethods]
impl Almanac {
    /// Returns the frame information (gravitational param, shape) as defined in this Almanac from an empty frame
    /// :type uid: Frame
    /// :rtype: Frame
    #[pyo3(name = "frame_info", signature=(uid))]
    fn py_frame_info(&self, uid: Frame) -> Result<Frame, PlanetaryDataError> {
        self.frame_info(uid)
    }

    /// Initializes a new Almanac from the provided file path, guessing at the file type
    #[new]
    fn py_new(path: &str) -> AlmanacResult<Self> {
        Self::new(path)
    }

    /// Initializes a new Almanac from a file path to CCSDS OEM file, after converting to to SPICE SPK/BSP
    ///
    /// :type path: str
    /// :type naif_id: int
    /// :rtype: Almanac
    #[classmethod]
    #[pyo3(name = "from_ccsds_oem_file")]
    fn py_from_ccsds_oem_file(
        _cls: Bound<'_, PyType>,
        path: &str,
        naif_id: NaifId,
    ) -> AlmanacResult<Self> {
        let ephem = Ephemeris::from_ccsds_oem_file(path).context(EphemerisSnafu {
            action: "loading CCSDS OEM",
        })?;
        // Convert to BSP
        let spk = ephem.to_spice_bsp(naif_id, None).context(EphemerisSnafu {
            action: "converting CCSDS OEM to SPICE BSP",
        })?;
        Ok(Self::default().with_spk(spk))
    }

    fn __str__(&self) -> String {
        format!("{self}")
    }

    fn __repr__(&self) -> String {
        format!("{self} (@{self:p})")
    }

    /// Pretty prints the description of this Almanac, showing everything by default. Default time scale is TDB.
    /// If any parameter is set to true, then nothing other than that will be printed.
    ///
    /// :type spk: bool, optional
    /// :type bpc: bool, optional
    /// :type planetary: bool, optional
    /// :type spacecraft: bool, optional
    /// :type eulerparams: bool, optional
    /// :type locations: bool, optional
    /// :type time_scale: TimeScale, optional
    /// :type round_time: bool, optional
    /// :rtype: None
    #[pyo3(name = "describe", signature=(
        spk=None,
        bpc=None,
        planetary=None,
        spacecraft=None,
        eulerparams=None,
        locations=None,
        time_scale=None,
        round_time=None,
    ))]
    #[allow(clippy::too_many_arguments)]
    fn py_describe(
        &self,
        spk: Option<bool>,
        bpc: Option<bool>,
        planetary: Option<bool>,
        spacecraft: Option<bool>,
        eulerparams: Option<bool>,
        locations: Option<bool>,
        time_scale: Option<TimeScale>,
        round_time: Option<bool>,
    ) {
        self.describe(
            spk,
            bpc,
            planetary,
            spacecraft,
            eulerparams,
            locations,
            time_scale,
            round_time,
        )
    }

    /// Returns the list of loaded kernels
    ///
    /// :type spk: bool, optional
    /// :type bpc: bool, optional
    /// :type planetary: bool, optional
    /// :type spacecraft: bool, optional
    /// :type eulerparams: bool, optional
    /// :type locations: bool, optional
    /// :rtype: list
    #[pyo3(name = "list_kernels", signature=(
        spk=None,
        bpc=None,
        planetary=None,
        spacecraft=None,
        eulerparams=None,
        locations=None,
    ))]
    #[allow(clippy::too_many_arguments)]
    fn py_list_kernels(
        &self,
        spk: Option<bool>,
        bpc: Option<bool>,
        planetary: Option<bool>,
        spacecraft: Option<bool>,
        eulerparams: Option<bool>,
        locations: Option<bool>,
    ) -> Vec<String> {
        self.list_kernels(spk, bpc, planetary, spacecraft, eulerparams, locations)
    }

    /// Generic function that tries to load the provided path guessing to the file type.
    ///
    /// :type path: str
    /// :rtype: Almanac
    #[pyo3(name = "load")]
    fn py_load(&self, path: &str) -> AlmanacResult<Self> {
        self.clone().load(path)
    }

    /// Converts the provided CCSDS OEM to SPICE SPK/BSP and loads it in the Almanac.
    ///
    /// :type path: str
    /// :type naif_id: int
    /// :rtype: Almanac
    #[pyo3(name = "load_ccsds_oem_file")]
    fn py_load_ccsds_oem_file(&self, path: &str, naif_id: NaifId) -> AlmanacResult<Self> {
        let ephem = Ephemeris::from_ccsds_oem_file(path).context(EphemerisSnafu {
            action: "loading CCSDS OEM",
        })?;
        // Convert to BSP
        let spk = ephem.to_spice_bsp(naif_id, None).context(EphemerisSnafu {
            action: "converting CCSDS OEM to SPICE BSP",
        })?;
        Ok(self.clone().with_spk(spk))
    }

    /// Converts the provided Ansys STK .e file to SPICE SPK/BSP and loads it in the Almanac.
    ///
    /// :type path: str
    /// :type naif_id: int
    /// :rtype: Almanac
    #[pyo3(name = "load_stk_e_file")]
    fn py_load_stk_e_file(&self, path: &str, naif_id: NaifId) -> AlmanacResult<Self> {
        let ephem = Ephemeris::from_stk_e_file(path).context(EphemerisSnafu {
            action: "loading STK .e",
        })?;
        // Convert to BSP
        let spk = ephem.to_spice_bsp(naif_id, None).context(EphemerisSnafu {
            action: "converting STK .e to SPICE BSP",
        })?;
        Ok(self.clone().with_spk(spk))
    }

    /// Unloads (in-place) the SPK with the provided alias.
    /// **WARNING:** This causes the order of the loaded files to be perturbed, which may be an issue if several SPKs with the same IDs are loaded.
    ///
    /// :type alias: str
    /// :rtype: None
    #[pyo3(name = "spk_unload")]
    fn py_spk_unload(&mut self, alias: &str) -> Result<(), EphemerisError> {
        self.spk_unload(alias)
    }

    /// Unloads (in-place) the BPC with the provided alias.
    /// **WARNING:** This causes the order of the loaded files to be perturbed, which may be an issue if several SPKs with the same IDs are loaded.
    ///
    /// :type alias: str
    /// :rtype: None
    #[pyo3(name = "bpc_unload")]
    fn py_bpc_unload(&mut self, alias: &str) -> Result<(), OrientationError> {
        self.bpc_unload(alias)
    }

    /// Load a new DAF/SPK file in place of the one in the provided alias.
    ///
    /// This reuses the existing memory buffer, growing it only if the new file
    /// is larger than the previous capacity. This effectively adopts a
    /// "high watermark" memory strategy, where the memory usage for this slot
    /// is determined by the largest file ever loaded into it
    /// .
    /// :type alias: str
    /// :type new_spk_path: str
    /// :type new_alias: str
    /// :rtype: None
    #[pyo3(name = "spk_swap")]
    pub fn py_spk_swap(
        &mut self,
        alias: &str,
        new_spk_path: &str,
        new_alias: String,
    ) -> AlmanacResult<()> {
        self.spk_swap(alias, new_spk_path, new_alias)
    }

    /// Load a new DAF/BPC file in place of the one in the provided alias.
    ///
    /// This reuses the existing memory buffer, growing it only if the new file
    /// is larger than the previous capacity. This effectively adopts a
    /// "high watermark" memory strategy, where the memory usage for this slot
    /// is determined by the largest file ever loaded into it.
    ///
    /// :type alias: str
    /// :type new_bpc_path: str
    /// :type new_alias: str
    /// :rtype: None
    #[pyo3(name = "bpc_swap")]
    pub fn py_bpc_swap(
        &mut self,
        alias: &str,
        new_bpc_path: &str,
        new_alias: String,
    ) -> AlmanacResult<()> {
        self.bpc_swap(alias, new_bpc_path, new_alias)
    }

    /// Computes the azimuth (in degrees), elevation (in degrees), and range (in kilometers) of the
    /// receiver state (`rx`) seen from the transmitter state (`tx`), once converted into the SEZ frame of the transmitter.
    ///
    /// # Warning
    /// The obstructing body _should_ be a tri-axial ellipsoid body, e.g. IAU_MOON_FRAME.
    ///
    /// # Algorithm
    /// 1. If any obstructing_bodies are provided, ensure that none of these are obstructing the line of sight between the receiver and transmitter.
    /// 2. Compute the SEZ (South East Zenith) frame of the transmitter.
    /// 3. Rotate the receiver position vector into the transmitter SEZ frame.
    /// 4. Rotate the transmitter position vector into that same SEZ frame.
    /// 5. Compute the range as the norm of the difference between these two position vectors.
    /// 6. Compute the elevation, and ensure it is between +/- 180 degrees.
    /// 7. Compute the azimuth with a quadrant check, and ensure it is between 0 and 360 degrees.
    ///
    /// :type rx: Orbit
    /// :type tx: Orbit
    /// :type obstructing_body: Frame, optional
    /// :type ab_corr: Aberration, optional
    /// :rtype: AzElRange
    #[pyo3(name = "azimuth_elevation_range_sez", signature=(rx, tx, obstructing_body=None, ab_corr=None))]
    pub fn py_azimuth_elevation_range_sez(
        &self,
        rx: Orbit,
        tx: Orbit,
        obstructing_body: Option<Frame>,
        ab_corr: Option<Aberration>,
    ) -> AlmanacResult<AzElRange> {
        self.azimuth_elevation_range_sez(rx, tx, obstructing_body, ab_corr)
    }

    /// Computes the azimuth (in degrees), elevation (in degrees), and range (in kilometers) of the
    /// receiver states (first item in tuple) seen from the transmitter state (second item in states tuple), once converted into the SEZ frame of the transmitter.
    ///
    /// Note: if any computation fails, the error will be printed to the stderr.
    /// Note: the output AER will be chronologically sorted, regardless of transmitter.
    ///
    /// Refer to [azimuth_elevation_range_sez] for details.
    ///
    /// :type rx_tx_states: typing.List[Orbit]
    /// :type obstructing_body: Frame, optional
    /// :type ab_corr: Aberration, optional
    /// :rtype: typing.List[AzElRange]
    #[pyo3(name = "azimuth_elevation_range_sez_many", signature=(
        rx_tx_states,
        obstructing_body=None, ab_corr=None
    ))]
    fn py_azimuth_elevation_range_sez_many(
        &self,
        py: Python,
        rx_tx_states: Vec<(CartesianState, CartesianState)>,
        obstructing_body: Option<Frame>,
        ab_corr: Option<Aberration>,
    ) -> Vec<AzElRange> {
        py.detach(|| {
            let mut rslt = rx_tx_states
                .par_iter()
                .filter_map(|(rx, tx)| {
                    self.azimuth_elevation_range_sez(*rx, *tx, obstructing_body, ab_corr)
                        .map_or_else(
                            |e| {
                                println!("{e}");
                                None
                            },
                            Some,
                        )
                })
                .collect::<Vec<AzElRange>>();
            rslt.sort_by(|aer_a, aer_b| aer_a.epoch.cmp(&aer_b.epoch));
            rslt
        })
    }

    /// Computes whether the line of sight between an observer and an observed Cartesian state is obstructed by the obstructing body.
    /// Returns true if the obstructing body is in the way, false otherwise.
    ///
    /// For example, if the Moon is in between a Lunar orbiter (observed) and a ground station (observer), then this function returns `true`
    /// because the Moon (obstructing body) is indeed obstructing the line of sight.
    ///
    /// ```text
    /// Observed
    ///   o  -
    ///    +    -
    ///     +      -
    ///      + ***   -
    ///     * +    *   -
    ///     *  + + * + + o
    ///     *     *     Observer
    ///       ****
    ///```
    ///
    /// Key Elements:
    /// - `o` represents the positions of the observer and observed objects.
    /// - The dashed line connecting the observer and observed is the line of sight.
    ///
    /// Algorithm (source: Algorithm 35 of Vallado, 4th edition, page 308.):
    /// - `r1` and `r2` are the transformed radii of the observed and observer objects, respectively.
    /// - `r1sq` and `r2sq` are the squared magnitudes of these vectors.
    /// - `r1dotr2` is the dot product of `r1` and `r2`.
    /// - `tau` is a parameter that determines the intersection point along the line of sight.
    /// - The condition `(1.0 - tau) * r1sq + r1dotr2 * tau <= ob_mean_eq_radius_km^2` checks if the line of sight is within the obstructing body's radius, indicating an obstruction.
    ///
    /// :type observer: Orbit
    /// :type observed: Orbit
    /// :type obstructing_body: Frame
    /// :type ab_corr: Aberration, optional
    /// :rtype: bool
    #[pyo3(name = "line_of_sight_obstructed", signature=(
        observer,
        observed,
        obstructing_body,
        ab_corr=None,
    ))]
    fn py_line_of_sight_obstructed(
        &self,
        observer: Orbit,
        observed: Orbit,
        obstructing_body: Frame,
        ab_corr: Option<Aberration>,
    ) -> AlmanacResult<bool> {
        self.line_of_sight_obstructed(observer, observed, obstructing_body, ab_corr)
    }

    /// Computes the occultation percentage of the `back_frame` object by the `front_frame` object as seen from the observer, when according for the provided aberration correction.
    ///
    /// A zero percent occultation means that the back object is fully visible from the observer.
    /// A 100%  percent occultation means that the back object is fully hidden from the observer because of the front frame (i.e. _umbra_ if the back object is the Sun).
    /// A value in between means that the back object is partially hidden from the observser (i.e. _penumbra_ if the back object is the Sun).
    /// Refer to the [MathSpec](https://nyxspace.com/nyxspace/MathSpec/celestial/eclipse/) for modeling details.
    ///
    /// :type back_frame: Frame
    /// :type front_frame: Frame
    /// :type observer: Orbit
    /// :type ab_corr: Aberration, optional
    /// :rtype: Occultation
    #[pyo3(name = "occultation", signature=(
        back_frame,
        front_frame,
        observer,
        ab_corr=None,
    ))]
    fn py_occultation(
        &self,
        back_frame: Frame,
        front_frame: Frame,
        observer: Orbit,
        ab_corr: Option<Aberration>,
    ) -> AlmanacResult<Occultation> {
        self.occultation(back_frame, front_frame, observer, ab_corr)
    }

    /// Computes the solar eclipsing of the observer due to the eclipsing_frame.
    ///
    /// This function calls `occultation` where the back object is the Sun in the J2000 frame, and the front object
    /// is the provided eclipsing frame.
    ///
    /// :type eclipsing_frame: Frame
    /// :type observer: Orbit
    /// :type ab_corr: Aberration, optional
    /// :rtype: Occultation
    #[pyo3(name = "solar_eclipsing", signature=(
        eclipsing_frame,
        observer,
        ab_corr=None,
    ))]
    fn py_solar_eclipsing(
        &self,
        eclipsing_frame: Frame,
        observer: Orbit,
        ab_corr: Option<Aberration>,
    ) -> AlmanacResult<Occultation> {
        self.solar_eclipsing(eclipsing_frame, observer, ab_corr)
    }

    /// Computes the solar eclipsing of all the observers due to the eclipsing_frame, computed in parallel under the hood.
    ///
    /// Note: if any computation fails, the error will be printed to the stderr.
    /// Note: the output AER will be chronologically sorted, regardless of transmitter.
    ///
    /// Refer to [solar_eclipsing] for details.
    ///
    /// :type eclipsing_frame: Frame
    /// :type observers: typing.List[Orbit]
    /// :type ab_corr: Aberration, optional
    /// :rtype: typing.List[Occultation]
    #[pyo3(name = "solar_eclipsing_many", signature=(
        eclipsing_frame,
        observers,
        ab_corr=None,
    ))]
    fn py_solar_eclipsing_many(
        &self,
        py: Python,
        eclipsing_frame: Frame,
        observers: Vec<Orbit>,
        ab_corr: Option<Aberration>,
    ) -> Vec<Occultation> {
        py.detach(|| {
            let mut rslt = observers
                .par_iter()
                .filter_map(|observer| {
                    self.solar_eclipsing(eclipsing_frame, *observer, ab_corr)
                        .map_or_else(
                            |e| {
                                println!("{e}");
                                None
                            },
                            Some,
                        )
                })
                .collect::<Vec<Occultation>>();
            rslt.sort_by(|aer_a, aer_b| aer_a.epoch.cmp(&aer_b.epoch));
            rslt
        })
    }

    /// Computes the Beta angle (β) for a given orbital state, in degrees. A Beta angle of 0° indicates that the orbit plane is edge-on to the Sun, leading to maximum eclipse time. Conversely, a Beta angle of +90° or -90° means the orbit plane is face-on to the Sun, resulting in continuous sunlight exposure and no eclipses.
    ///
    /// The Beta angle (β) is defined as the angle between the orbit plane of a spacecraft and the vector from the central body (e.g., Earth) to the Sun. In simpler terms, it measures how much of the time a satellite in orbit is exposed to direct sunlight.
    /// The mathematical formula for the Beta angle is: β=arcsin(h⋅usun​)
    /// Where:
    /// - h is the unit vector of the orbital momentum.
    /// - usun​ is the unit vector pointing from the central body to the Sun.
    ///
    /// Original code from GMAT, <https://github.com/ChristopherRabotin/GMAT/blob/GMAT-R2022a/src/gmatutil/util/CalculationUtilities.cpp#L209-L219>
    ///
    /// :type state: Orbit
    /// :type ab_corr: Aberration, optional
    /// :rtype: float
    #[pyo3(name = "beta_angle_deg", signature=(
        state,
        ab_corr=None,
    ))]
    fn py_beta_angle_deg(&self, state: Orbit, ab_corr: Option<Aberration>) -> AlmanacResult<f64> {
        self.beta_angle_deg(state, ab_corr)
    }

    /// Returns the Cartesian state needed to transform the `from_frame` to the `to_frame`.
    ///
    /// # SPICE Compatibility
    /// This function is the SPICE equivalent of spkezr: `spkezr(TARGET_ID, EPOCH_TDB_S, ORIENTATION_ID, ABERRATION, OBSERVER_ID)`
    /// In ANISE, the TARGET_ID and ORIENTATION are provided in the first argument (TARGET_FRAME), as that frame includes BOTH
    /// the target ID and the orientation of that target. The EPOCH_TDB_S is the epoch in the TDB time system, which is computed
    /// in ANISE using Hifitime. THe ABERRATION is computed by providing the optional Aberration flag. Finally, the OBSERVER
    /// argument is replaced by OBSERVER_FRAME: if the OBSERVER_FRAME argument has the same orientation as the TARGET_FRAME, then this call
    /// will return exactly the same data as the spkerz SPICE call.
    ///
    /// # Note
    /// The units will be those of the underlying ephemeris data (typically km and km/s)
    ///
    /// :type target_frame: Frame
    /// :type observer_frame: Frame
    /// :type epoch: Epoch
    /// :type ab_corr: Aberration, optional
    /// :rtype: Orbit
    #[pyo3(name = "transform", signature=(
        target_frame,
        observer_frame,
        epoch,
        ab_corr=None,
    ))]
    fn py_transform(
        &self,
        target_frame: Frame,
        observer_frame: Frame,
        epoch: Epoch,
        ab_corr: Option<Aberration>,
    ) -> AlmanacResult<CartesianState> {
        self.transform(target_frame, observer_frame, epoch, ab_corr)
    }

    /// Returns a chronologically sorted list of the Cartesian states that transform the `from_frame` to the `to_frame` for each epoch of the time series, computed in parallel under the hood.
    /// Note: if any transformation fails, the error will be printed to the stderr.
    ///
    /// Refer to [transform] for details.
    ///
    /// :type target_frame: Frame
    /// :type observer_frame: Frame
    /// :type time_series: TimeSeries
    /// :type ab_corr: Aberration, optional
    /// :rtype: typing.List[Orbit]
    #[pyo3(name = "transform_many", signature=(
        target_frame,
        observer_frame,
        time_series,
        ab_corr=None,
    ))]
    fn py_transform_many(
        &self,
        py: Python,
        target_frame: Frame,
        observer_frame: Frame,
        time_series: TimeSeries,
        ab_corr: Option<Aberration>,
    ) -> Vec<CartesianState> {
        py.detach(|| {
            let mut states = time_series
                .par_bridge()
                .filter_map(|epoch| {
                    self.transform(target_frame, observer_frame, epoch, ab_corr)
                        .map_or_else(
                            |e| {
                                eprintln!("{e}");
                                None
                            },
                            Some,
                        )
                })
                .collect::<Vec<CartesianState>>();
            states.sort_by(|state_a, state_b| state_a.epoch.cmp(&state_b.epoch));
            states
        })
    }

    /// Returns the provided state as seen from the observer frame, given the aberration.
    ///
    /// :type state: Orbit
    /// :type observer_frame: Frame
    /// :type ab_corr: Aberration, optional
    /// :rtype: Orbit
    #[pyo3(name = "transform_to", signature=(
        state,
        observer_frame,
        ab_corr=None,
    ))]
    fn py_transform_to(
        &self,
        state: CartesianState,
        observer_frame: Frame,
        ab_corr: Option<Aberration>,
    ) -> AlmanacResult<CartesianState> {
        self.transform_to(state, observer_frame, ab_corr)
    }

    /// Returns a chronologically sorted list of the provided states as seen from the observer frame, given the aberration.
    /// Note: if any transformation fails, the error will be printed to the stderr.
    /// Note: the input ordering is lost: the output states will not be in the same order as the input states if these are not chronologically sorted!
    ///
    /// Refer to [transform_to] for details.
    ///
    /// :type states: typing.List[Orbit]
    /// :type observer_frame: Frame
    /// :type ab_corr: Aberration, optional
    /// :rtype: typing.List[Orbit]
    #[pyo3(name = "transform_many_to", signature=(
        states,
        observer_frame,
        ab_corr=None,
    ))]
    fn py_transform_many_to(
        &self,
        py: Python,
        states: Vec<CartesianState>,
        observer_frame: Frame,
        ab_corr: Option<Aberration>,
    ) -> Vec<CartesianState> {
        py.detach(|| {
            let mut rslt = states
                .par_iter()
                .filter_map(|state| {
                    self.transform_to(*state, observer_frame, ab_corr)
                        .map_or_else(
                            |e| {
                                println!("{e}");
                                None
                            },
                            Some,
                        )
                })
                .collect::<Vec<CartesianState>>();
            rslt.sort_by(|state_a, state_b| state_a.epoch.cmp(&state_b.epoch));
            rslt
        })
    }

    /// Returns the Cartesian state of the object as seen from the provided observer frame (essentially `spkezr`).
    ///
    /// # Note
    /// The units will be those of the underlying ephemeris data (typically km and km/s)
    ///
    /// :type object_id: int
    /// :type observer: Frame
    /// :type epoch: Epoch
    /// :type ab_corr: Aberration, optional
    /// :rtype: Orbit
    #[pyo3(name = "state_of", signature=(
        object_id,
        observer,
        epoch,
        ab_corr=None,
    ))]
    fn py_state_of(
        &self,
        object_id: NaifId,
        observer: Frame,
        epoch: Epoch,
        ab_corr: Option<Aberration>,
    ) -> AlmanacResult<CartesianState> {
        self.state_of(object_id, observer, epoch, ab_corr)
    }

    /// Alias fo SPICE's `spkezr` where the inputs must be the NAIF IDs of the objects and frames with the caveat that the aberration is moved to the last positional argument.
    ///
    /// :type target: int
    /// :type epoch: Epoch
    /// :type frame: int
    /// :type observer: int
    /// :type ab_corr: Aberration, optional
    /// :rtype: Orbit
    #[pyo3(name = "spk_ezr", signature=(
        target,
        epoch,
        frame,
        observer,
        ab_corr=None,
    ))]
    fn py_spk_ezr(
        &self,
        target: NaifId,
        epoch: Epoch,
        frame: NaifId,
        observer: NaifId,
        ab_corr: Option<Aberration>,
    ) -> AlmanacResult<CartesianState> {
        self.spk_ezr(target, epoch, frame, observer, ab_corr)
    }

    /// Returns the Cartesian state of the target frame as seen from the observer frame at the provided epoch, and optionally given the aberration correction.
    ///
    /// # SPICE Compatibility
    /// This function is the SPICE equivalent of spkezr: `spkezr(TARGET_ID, EPOCH_TDB_S, ORIENTATION_ID, ABERRATION, OBSERVER_ID)`
    /// In ANISE, the TARGET_ID and ORIENTATION are provided in the first argument (TARGET_FRAME), as that frame includes BOTH
    /// the target ID and the orientation of that target. The EPOCH_TDB_S is the epoch in the TDB time system, which is computed
    /// in ANISE using Hifitime. THe ABERRATION is computed by providing the optional Aberration flag. Finally, the OBSERVER
    /// argument is replaced by OBSERVER_FRAME: if the OBSERVER_FRAME argument has the same orientation as the TARGET_FRAME, then this call
    /// will return exactly the same data as the spkerz SPICE call.
    ///
    /// # Warning
    /// This function only performs the translation and no rotation whatsoever. Use the `transform` function instead to include rotations.
    ///
    /// # Note
    /// This function performs a recursion of no more than twice the [MAX_TREE_DEPTH].
    ///
    /// :type target_frame: Frame
    /// :type observer_frame: Frame
    /// :type epoch: Epoch
    /// :type ab_corr: Aberration, optional
    /// :rtype: Orbit
    #[pyo3(name = "translate", signature=(
        target_frame,
        observer_frame,
        epoch,
        ab_corr=None,
    ))]
    fn py_translate(
        &self,
        target_frame: Frame,
        observer_frame: Frame,
        epoch: Epoch,
        ab_corr: Option<Aberration>,
    ) -> Result<CartesianState, EphemerisError> {
        self.translate(target_frame, observer_frame, epoch, ab_corr)
    }

    /// Returns the geometric position vector, velocity vector, and acceleration vector needed to translate the `from_frame` to the `to_frame`, where the distance is in km, the velocity in km/s, and the acceleration in km/s^2.
    ///
    /// :type target_frame: Frame
    /// :type observer_frame: Frame
    /// :type epoch: Epoch
    /// :rtype: Orbit
    #[pyo3(name = "translate_geometric", signature=(
        target_frame,
        observer_frame,
        epoch,
    ))]
    fn py_translate_geometric(
        &self,
        target_frame: Frame,
        observer_frame: Frame,
        epoch: Epoch,
    ) -> Result<CartesianState, EphemerisError> {
        self.translate_geometric(target_frame, observer_frame, epoch)
    }

    /// Translates the provided Cartesian state into the requested observer frame
    ///
    /// **WARNING:** This function only performs the translation and no rotation _whatsoever_. Use the `transform_to` function instead to include rotations.
    ///
    /// :type state: Orbit
    /// :type observer_frame: Frame
    /// :type ab_corr: Aberration, optional
    /// :rtype: Orbit
    #[pyo3(name = "translate_to", signature=(
        state,
        observer_frame,
        ab_corr=None,
    ))]
    pub fn py_translate_to(
        &self,
        state: CartesianState,
        observer_frame: Frame,
        ab_corr: Option<Aberration>,
    ) -> Result<CartesianState, EphemerisError> {
        self.translate_to(state, observer_frame, ab_corr)
    }

    /// Returns the 6x6 DCM needed to rotation the `from_frame` to the `to_frame`.
    ///
    /// # Warning
    /// This function only performs the rotation and no translation whatsoever. Use the `transform_from_to` function instead to include rotations.
    ///
    /// # Note
    /// This function performs a recursion of no more than twice the MAX_TREE_DEPTH.
    ///
    /// :type from_frame: Frame
    /// :type to_frame: Frame
    /// :type epoch: Epoch
    /// :rtype: DCM
    #[pyo3(name = "rotate", signature=(
        from_frame,
        to_frame,
        epoch,
    ))]
    pub fn py_rotate(
        &self,
        from_frame: Frame,
        to_frame: Frame,
        epoch: Epoch,
    ) -> Result<DCM, OrientationError> {
        self.rotate(from_frame, to_frame, epoch)
    }

    /// Rotates the provided Cartesian state into the requested observer frame
    ///
    /// **WARNING:** This function only performs the translation and no rotation _whatsoever_. Use the `transform_to` function instead to include rotations.
    ///
    /// :type state: Orbit
    /// :type observer_frame: Frame
    /// :rtype: Orbit
    #[pyo3(name = "rotate_to", signature=(
        state,
        observer_frame,
    ))]
    pub fn py_rotate_to(
        &self,
        state: CartesianState,
        observer_frame: Frame,
    ) -> Result<CartesianState, OrientationError> {
        self.rotate_to(state, observer_frame)
    }

    /// Returns the angular velocity vector in rad/s of the from_frame wrt to the to_frame.
    ///
    /// This can be used to compute the angular velocity of the Earth ITRF93 frame with respect to the J2000 frame for example.
    ///
    /// :type from_frame: Frame
    /// :type to_frame: Frame
    /// :type epoch: Epoch
    /// :rtype: numpy.ndarray
    #[pyo3(name="angular_velocity_rad_s", signature=(from_frame, to_frame, epoch))]
    pub fn py_angular_velocity_rad_s<'py>(
        &self,
        py: Python<'py>,
        from_frame: Frame,
        to_frame: Frame,
        epoch: Epoch,
    ) -> Result<Bound<'py, PyArray1<f64>>, OrientationError> {
        let data: Vec<f64> = self
            .angular_velocity_rad_s(from_frame, to_frame, epoch)?
            .iter()
            .copied()
            .collect();

        let omega = Array1::from_shape_vec((3,), data).unwrap();

        Ok(PyArray1::<f64>::from_owned_array(py, omega))
    }

    /// Returns the angular velocity vector in rad/s of the from_frame wrt to the J2000 frame.
    ///
    /// :type from_frame: Frame
    /// :type epoch: Epoch
    /// :rtype: numpy.ndarray
    #[pyo3(name = "angular_velocity_wrt_j2000_rad_s", signature = (from_frame, epoch))]
    pub fn py_angular_velocity_wrt_j2000_rad_s<'py>(
        &self,
        py: Python<'py>,
        from_frame: Frame,
        epoch: Epoch,
    ) -> Result<Bound<'py, PyArray1<f64>>, OrientationError> {
        self.py_angular_velocity_rad_s(py, from_frame, from_frame.with_orient(J2000), epoch)
    }

    /// Returns the angular velocity vector in deg/s of the from_frame wrt to the to_frame.
    ///
    /// This can be used to compute the angular velocity of the Earth ITRF93 frame with respect to the J2000 frame for example.
    ///
    /// :type from_frame: Frame
    /// :type to_frame: Frame
    /// :type epoch: Epoch
    /// :rtype: numpy.ndarray
    #[pyo3(name="angular_velocity_deg_s", signature=(from_frame, to_frame, epoch))]
    pub fn py_angular_velocity_deg_s<'py>(
        &self,
        py: Python<'py>,
        from_frame: Frame,
        to_frame: Frame,
        epoch: Epoch,
    ) -> Result<Bound<'py, PyArray1<f64>>, OrientationError> {
        let data: Vec<f64> = self
            .angular_velocity_deg_s(from_frame, to_frame, epoch)?
            .iter()
            .copied()
            .collect();

        let omega = Array1::from_shape_vec((3,), data).unwrap();

        Ok(PyArray1::<f64>::from_owned_array(py, omega))
    }

    /// Returns the angular velocity vector in deg/s of the from_frame wrt to the J2000 frame.
    ///
    /// :type from_frame: Frame
    /// :type epoch: Epoch
    /// :rtype: numpy.ndarray
    #[pyo3(name = "angular_velocity_wrt_j2000_deg_s", signature = (from_frame, epoch))]
    pub fn py_angular_velocity_wrt_j2000_deg_s<'py>(
        &self,
        py: Python<'py>,
        from_frame: Frame,
        epoch: Epoch,
    ) -> Result<Bound<'py, PyArray1<f64>>, OrientationError> {
        self.py_angular_velocity_deg_s(py, from_frame, from_frame.with_orient(J2000), epoch)
    }

    /// Computes the azimuth (in degrees), elevation (in degrees), and range (in kilometers) of the
    /// receiver state (`rx`) seen from the location ID (as transmitter state, once converted into the SEZ frame of the transmitter.
    /// Refer to [azimuth_elevation_range_sez] for algorithm details.
    ///
    /// :type rx: Orbit
    /// :type location_id: int
    /// :type obstructing_body: Frame, optional
    /// :type ab_corr: Aberration, optional
    /// :rtype: AzElRange
    #[pyo3(name="azimuth_elevation_range_sez_from_location_id", signature=(rx, location_id, obstructing_body=None, ab_corr=None))]
    pub fn py_azimuth_elevation_range_sez_from_location_id(
        &self,
        rx: Orbit,
        location_id: i32,
        obstructing_body: Option<Frame>,
        ab_corr: Option<Aberration>,
    ) -> AlmanacResult<AzElRange> {
        self.azimuth_elevation_range_sez_from_location_id(
            rx,
            location_id,
            obstructing_body,
            ab_corr,
        )
    }

    /// Computes the azimuth (in degrees), elevation (in degrees), and range (in kilometers) of the
    /// receiver state (`rx`) seen from the location ID (as transmitter state, once converted into the SEZ frame of the transmitter.
    /// Refer to [azimuth_elevation_range_sez] for algorithm details.
    ///
    /// :type rx: Orbit
    /// :type location_name: str
    /// :type obstructing_body: Frame, optional
    /// :type ab_corr: Aberration, optional
    /// :rtype: AzElRange
    #[pyo3(name="azimuth_elevation_range_sez_from_location_name", signature=(rx, location_name, obstructing_body=None, ab_corr=None))]
    pub fn py_azimuth_elevation_range_sez_from_location_name(
        &self,
        rx: Orbit,
        location_name: &str,
        obstructing_body: Option<Frame>,
        ab_corr: Option<Aberration>,
    ) -> AlmanacResult<AzElRange> {
        self.azimuth_elevation_range_sez_from_location_name(
            rx,
            location_name,
            obstructing_body,
            ab_corr,
        )
    }

    /// Computes the azimuth (in degrees), elevation (in degrees), and range (in kilometers) of the
    /// receiver state (`rx`) seen from the provided location (as transmitter state, once converted into the SEZ frame of the transmitter.
    /// Refer to [azimuth_elevation_range_sez] for algorithm details.
    /// Location terrain masks are always applied, i.e. if the terrain masks the object, all data is set to f64::NAN, unless specified otherwise in the Location.
    ///
    /// :type rx: Orbit
    /// :type location: Location
    /// :type obstructing_body: Frame, optional
    /// :type ab_corr: Aberration, optional
    /// :rtype: AzElRange
    #[pyo3(name="azimuth_elevation_range_sez_from_location", signature=(rx, location, obstructing_body=None, ab_corr=None))]
    pub fn py_azimuth_elevation_range_sez_from_location(
        &self,
        rx: Orbit,
        location: Location,
        obstructing_body: Option<Frame>,
        ab_corr: Option<Aberration>,
    ) -> AlmanacResult<AzElRange> {
        self.azimuth_elevation_range_sez_from_location(rx, location, obstructing_body, ab_corr)
    }

    /// Returns the Location from its ID, searching through all loaded location datasets in reverse order.
    ///
    /// :type id: int
    /// :rtype: Location
    #[pyo3(name = "location_from_id")]
    pub fn py_location_from_id(&self, id: i32) -> AlmanacResult<Location> {
        self.location_from_id(id)
    }

    /// Returns the Location from its name, searching through all loaded location datasets in reverse order.
    ///
    /// :type name: str
    /// :rtype: Location
    #[pyo3(name = "location_from_name")]
    pub fn py_location_from_name(&self, name: &str) -> AlmanacResult<Location> {
        self.location_from_name(name)
    }
}