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
/*
 * 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::{OrbitalElement, ScalarExpr};
use crate::{
    analysis::AnalysisError,
    astro::{Aberration, AzElRange, Location},
    prelude::{Almanac, Frame, Orbit},
};
use hifitime::{Duration, Epoch, Unit};
use log::warn;
use serde::{Deserialize, Serialize};
use std::fmt;

#[cfg(feature = "python")]
use pyo3::prelude::*;

#[cfg(feature = "python")]
use super::python::PyScalarExpr;
#[cfg(feature = "python")]
use pyo3::exceptions::PyException;
#[cfg(feature = "python")]
use pyo3::types::PyType;

/// Defines an event condition
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3(module = "anise.analysis"))]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum Condition {
    Equals(f64),
    Between(f64, f64),
    LessThan(f64),
    GreaterThan(f64),
    Minimum(),
    Maximum(),
}

/// Defines a state parameter event finder from the desired value of the scalar expression to compute, precision on timing and value, and the aberration.
///
/// :type scalar: ScalarExpr
/// :type condition: Condition
/// :type epoch_precision: Duration
/// :type ab_corr: Aberration, optional
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3(module = "anise.analysis"))]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Event {
    /// Scalar expression to evaluate
    pub scalar: ScalarExpr,
    /// Condition that defines the bounded values of the event.
    pub condition: Condition,
    /// The duration precision used in the adaptive step scanner, and to consider an Event to have converged. Typically use 100 ms.
    pub epoch_precision: Duration,
    pub ab_corr: Option<Aberration>,
}

impl Event {
    /// Builds a new event where the epoch precision is set to its default of 10 milliseconds
    #[must_use]
    pub fn new(scalar: ScalarExpr, condition: Condition) -> Self {
        Self {
            scalar,
            condition,
            epoch_precision: Unit::Millisecond * 10,
            ab_corr: None,
        }
    }

    /// Apoapsis event finder
    pub fn apoapsis() -> Self {
        Event {
            scalar: ScalarExpr::Element(OrbitalElement::TrueAnomaly),
            condition: Condition::Equals(180.0),
            epoch_precision: Unit::Second * 0.1,
            ab_corr: None,
        }
    }

    /// Periapsis event finder
    pub fn periapsis() -> Self {
        Event {
            scalar: ScalarExpr::Element(OrbitalElement::TrueAnomaly),
            condition: Condition::Equals(0.0),
            epoch_precision: Unit::Millisecond * 10,
            ab_corr: None,
        }
    }

    /// Total eclipse event finder: returns events where the eclipsing percentage is greater than 99%.
    pub fn total_eclipse(eclipsing_frame: Frame) -> Self {
        Event {
            scalar: ScalarExpr::SolarEclipsePercentage { eclipsing_frame },
            condition: Condition::GreaterThan(99.0),
            epoch_precision: Unit::Millisecond * 10,
            ab_corr: None,
        }
    }

    /// Eclipse event finder, including penumbras: returns events where the eclipsing percentage is greater than 1%.
    pub fn eclipse(eclipsing_frame: Frame) -> Self {
        Event {
            scalar: ScalarExpr::SolarEclipsePercentage { eclipsing_frame },
            condition: Condition::GreaterThan(1.0),
            epoch_precision: Unit::Millisecond * 10,
            ab_corr: None,
        }
    }

    /// Penumbral eclipse event finder: returns events where the eclipsing percentage is greater than 1% and less than 99%.
    pub fn penumbra(eclipsing_frame: Frame) -> Self {
        Event {
            scalar: ScalarExpr::SolarEclipsePercentage { eclipsing_frame },
            condition: Condition::Between(1.0, 99.0),
            epoch_precision: Unit::Millisecond * 10,
            ab_corr: None,
        }
    }

    /// Report events where the object is above the terrain (or horizon if terrain is not set) when seen from the provided location ID.
    pub fn visible_from_location_id(location_id: i32, obstructing_body: Option<Frame>) -> Self {
        Event {
            scalar: ScalarExpr::ElevationFromLocation {
                location_id,
                obstructing_body,
            },
            condition: Condition::GreaterThan(0.0),
            epoch_precision: Unit::Millisecond * 10,
            ab_corr: None,
        }
    }

    /// Export this Event to S-Expression / LISP syntax
    pub fn to_s_expr(&self) -> Result<String, serde_lexpr::Error> {
        Ok(serde_lexpr::to_value(self)?.to_string())
    }

    /// Load this Event from an S-Expression / LISP syntax
    pub fn from_s_expr(expr: &str) -> Result<Self, serde_lexpr::Error> {
        serde_lexpr::from_str(expr)
    }
}

#[cfg_attr(feature = "python", pymethods)]
impl Event {
    /// Compute the event finding function of this event provided an Orbit and Almanac.
    /// If we're "in the event", the evaluation will be greater or equal to zero.
    ///
    /// :type orbit: Orbit
    /// :type almanac: Almanac
    /// :rtype: float
    pub fn eval(&self, orbit: Orbit, almanac: &Almanac) -> Result<f64, AnalysisError> {
        let mut current_val = self.scalar.evaluate(orbit, self.ab_corr, almanac)?;

        // Special handling for angular values when we need to find a root.
        if let Condition::Equals(mut desired_val) = self.condition {
            let use_trig = self.scalar.is_angle()
                || self.scalar.is_local_time()
                || matches!(self.scalar, ScalarExpr::Modulo { .. });

            if use_trig {
                // Scale to be akin to a full circle.
                if self.scalar.is_local_time() {
                    current_val *= 360.0 / 24.0;
                    desired_val *= 360.0 / 24.0;
                } else if let ScalarExpr::Modulo { v: _, ref m } = self.scalar {
                    let modmax = m.evaluate(orbit, self.ab_corr, almanac)?;
                    if modmax >= 1e-12 {
                        current_val *= 360.0 / modmax;
                        desired_val *= 360.0 / modmax;
                    }
                }

                // Use the arctan function because it's smooth around zero, but convert back to degrees for the comparison.
                let current_rad = current_val.to_radians();
                let desired_rad = desired_val.to_radians();

                // Convert the angles to points on a unit circle
                let (cur_sin, cur_cos) = current_rad.sin_cos();
                let (des_sin, des_cos) = desired_rad.sin_cos();

                // Calculate the difference vector and find its angle with atan2.
                // This will be zero only when the angles are identical.
                let y = cur_sin * des_cos - cur_cos * des_sin; // sin(current - desired)
                let x = cur_cos * des_cos + cur_sin * des_sin; // cos(current - desired)

                return Ok(y.atan2(x).to_degrees());
            }
        }

        // For all non-angular scalars, or for conditions other than Equals.
        match self.condition {
            Condition::Equals(val) => Ok(current_val - val),
            Condition::Between(min_val, max_val) => {
                // Return positive if inside, negative if outside.
                // Smallest of the two distances to the boundaries.
                let dist_to_min = current_val - min_val;
                let dist_to_max = max_val - current_val;
                Ok(dist_to_min.min(dist_to_max))
            }
            Condition::LessThan(val) => Ok(val - current_val), // Positive if current_val < val
            Condition::GreaterThan(val) => Ok(current_val - val), // Positive if current_val > val
            Condition::Minimum() | Condition::Maximum() => Err(AnalysisError::InvalidEventEval {
                err: format!(
                    "cannot call Eval on {:?}, it must be handled by finding the derivative of the scalar",
                    self.condition
                ),
            }),
        }
    }

    /// Pretty print the evaluation of this event for the provided Orbit and Almanac
    ///
    /// :type orbit: Orbit
    /// :type almanac: Almanac
    /// :rtype: str
    pub fn eval_string(&self, orbit: Orbit, almanac: &Almanac) -> Result<String, AnalysisError> {
        if let Condition::Equals(desired_val) = self.condition {
            let val = self.eval(orbit, almanac)?;
            if desired_val.abs() > 1e3 {
                Ok(format!(
                    "|{} - {desired_val:e}| = {val:e} on {}",
                    self.scalar, orbit.epoch
                ))
            } else if desired_val.abs() > 1e-2 {
                Ok(format!(
                    "|{} - {desired_val:.3}| = {val:.3} on {}",
                    self.scalar, orbit.epoch
                ))
            } else {
                Ok(format!("|{}| = {val:.3} on {}", self.scalar, orbit.epoch))
            }
        } else {
            let current_val = self.scalar.evaluate(orbit, self.ab_corr, almanac)?;
            // For other conditions, just show the current value of the scalar.
            if current_val.abs() > 1e3 || (current_val.abs() < 1e-2 && current_val != 0.0) {
                Ok(format!(
                    "{} = {current_val:e} on {}",
                    self.scalar, orbit.epoch
                ))
            } else {
                Ok(format!(
                    "{} = {current_val:.3} on {}",
                    self.scalar, orbit.epoch
                ))
            }
        }
    }
}

#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl Event {
    /// Convert the S-Expression to a Event
    /// :type expr: str
    /// :rtype: Event
    #[classmethod]
    #[pyo3(name = "from_s_expr")]
    fn py_from_s_expr(_cls: Bound<'_, PyType>, expr: &str) -> Result<Self, PyErr> {
        Self::from_s_expr(expr).map_err(|e| PyException::new_err(e.to_string()))
    }

    /// Converts this Event to its S-Expression
    /// :rtype: str
    #[pyo3(name = "to_s_expr")]
    fn py_to_s_expr(&self) -> Result<String, PyErr> {
        self.to_s_expr()
            .map_err(|e| PyException::new_err(e.to_string()))
    }

    #[classmethod]
    #[pyo3(name = "apoapsis")]
    /// Apoapsis event finder, with an epoch precision of 0.1 seconds
    /// :rtype: Event
    fn py_apoapsis(_cls: Bound<'_, PyType>) -> Self {
        Event::apoapsis()
    }

    /// Periapsis event finder, with an epoch precision of 0.1 seconds
    /// :rtype: Event
    #[classmethod]
    #[pyo3(name = "periapsis")]
    fn py_periapsis(_cls: Bound<'_, PyType>) -> Self {
        Event::periapsis()
    }

    /// Total eclipse event finder: returns events where the eclipsing percentage is greater than 98.9%.
    ///
    /// :type eclipsing_frame: Frame
    /// :rtype: Event
    #[classmethod]
    #[pyo3(name = "total_eclipse")]
    fn py_total_eclipse(_cls: Bound<'_, PyType>, eclipsing_frame: Frame) -> Self {
        Event::total_eclipse(eclipsing_frame)
    }

    /// Eclipse event finder, including penumbras: returns events where the eclipsing percentage is greater than 1%.
    ///
    /// :type eclipsing_frame: Frame
    /// :rtype: Event
    #[classmethod]
    #[pyo3(name = "eclipse")]
    fn py_eclipse(_cls: Bound<'_, PyType>, eclipsing_frame: Frame) -> Self {
        Event::eclipse(eclipsing_frame)
    }

    /// Penumbral eclipse event finder: returns events where the eclipsing percentage is greater than 1% and less than 99%.
    ///
    /// :type eclipsing_frame: Frame
    /// :rtype: Event
    #[classmethod]
    #[pyo3(name = "penumbra")]
    fn py_penumbra(_cls: Bound<'_, PyType>, eclipsing_frame: Frame) -> Self {
        Event::penumbra(eclipsing_frame)
    }

    /// Report events where the object is above the terrain (or horizon if terrain is not set) when seen from the provided location ID.
    ///
    /// :type location_id: int
    /// :type obstructing_body: Frame, optional
    /// :rtype: Event
    #[classmethod]
    #[pyo3(name = "visible_from_location_id", signature=(location_id, obstructing_body=None))]
    fn py_visible_from_location_id(
        _cls: Bound<'_, PyType>,
        location_id: i32,
        obstructing_body: Option<Frame>,
    ) -> Self {
        Event::visible_from_location_id(location_id, obstructing_body)
    }

    #[new]
    #[pyo3(signature=(scalar, condition, epoch_precision, ab_corr=None))]
    fn py_new(
        scalar: PyScalarExpr,
        condition: Condition,
        epoch_precision: Duration,
        ab_corr: Option<Aberration>,
    ) -> Self {
        let scalar = ScalarExpr::from(scalar);

        Self {
            scalar,
            condition,
            epoch_precision,
            ab_corr,
        }
    }

    /// The scalar expression to compute
    /// :rtype: ScalarExpr
    #[getter]
    fn scalar(&self) -> Result<PyScalarExpr, PyErr> {
        PyScalarExpr::try_from(self.scalar.clone())
    }

    /// The desired self.desired_value, must be in the same units as the state parameter
    /// :rtype: Condition
    #[getter]
    fn condition(&self) -> Condition {
        self.condition
    }
    /// The duration precision after which the solver will report that it cannot find any more precise
    /// :rtype: Duration
    #[getter]
    fn epoch_precision(&self) -> Duration {
        self.epoch_precision
    }
    /// :rtype: Aberration
    #[getter]
    fn ab_corr(&self) -> Option<Aberration> {
        self.ab_corr
    }

    /// :type scalar: ScalarExpr
    #[setter]
    fn set_scalar(&mut self, scalar: PyScalarExpr) {
        self.scalar = scalar.into();
    }

    /// :type desired_value: float
    #[setter]
    fn set_condition(&mut self, condition: Condition) {
        self.condition = condition;
    }

    /// :type epoch_precision: Duration
    #[setter]
    fn set_epoch_precision(&mut self, epoch_precision: Duration) {
        self.epoch_precision = epoch_precision;
    }

    /// type ab_corr: Aberration, optional
    #[setter]
    fn set_ab_corr(&mut self, ab_corr: Option<Aberration>) {
        self.ab_corr = ab_corr;
    }

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

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

    fn __eq__(&self, other: &Self) -> bool {
        self == other
    }
    fn __ne__(&self, other: &Self) -> bool {
        self != other
    }
}

impl fmt::Display for Event {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:?}", self.scalar)?;
        match self.condition {
            Condition::Equals(val) => {
                if val.abs() > 1e3 {
                    write!(f, " = {val:e} (± {})", self.epoch_precision)
                } else {
                    write!(f, " = {val} (± {})", self.epoch_precision)
                }
            }
            Condition::Between(a, b) => {
                write!(f, " in [{a}, {b}] (± {})", self.epoch_precision)
            }
            Condition::LessThan(val) => {
                if val.abs() > 1e3 {
                    write!(f, " <= {val:e} (± {})", self.epoch_precision)
                } else {
                    write!(f, " <= {val} (± {})", self.epoch_precision)
                }
            }
            Condition::GreaterThan(val) => {
                if val.abs() > 1e3 {
                    write!(f, " >= {val:e} (± {})", self.epoch_precision)
                } else {
                    write!(f, " >= {val} (± {})", self.epoch_precision)
                }
            }
            Condition::Minimum() => write!(f, " minimum value (± {})", self.epoch_precision),
            Condition::Maximum() => write!(f, " maximum value (± {})", self.epoch_precision),
        }
    }
}
/// Enumerates the possible edges of an event in a trajectory.
///
/// `EventEdge` is used to describe the nature of a trajectory event, particularly in terms of its temporal dynamics relative to a specified condition or threshold. This enum helps in distinguishing whether the event is occurring at a rising edge, a falling edge, or if the edge is unclear due to insufficient data or ambiguous conditions.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3(module = "anise.analysis"))]
pub enum EventEdge {
    /// Represents a rising edge of the event. This indicates that the event is transitioning from a lower to a higher evaluation of the event. For example, in the context of elevation, a rising edge would indicate an increase in elevation from a lower angle.
    Rising,
    /// Represents a falling edge of the event. This is the opposite of the rising edge, indicating a transition from a higher to a lower value of the event evaluator. For example, if tracking the elevation of an object, a falling edge would signify a
    Falling,
    /// Represents a local minimum of the event. This indicates that the previous and next values are both greater than the current value.
    LocalMin,
    /// Represents a local maximum of the event. This indicates that the previous and next values are both lower than the current value.
    LocalMax,
    /// If the edge cannot be clearly defined, it will be marked as unclear. This happens if the event is at a saddle point and the epoch precision is too large to find the exact slope.
    Unclear,
}

#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl EventEdge {
    fn __eq__(&self, other: &Self) -> bool {
        self == other
    }
    fn __ne__(&self, other: &Self) -> bool {
        self != other
    }
}

/// Represents the details of an event occurring along a trajectory.
///
/// `EventDetails` encapsulates the state at which a particular event occurs in a trajectory, along with additional information about the nature of the event. This struct is particularly useful for understanding the dynamics of the event, such as whether it represents a rising or falling edge, or if the edge is unclear.
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3(module = "anise.analysis", get_all))]
pub struct EventDetails {
    /// The state of the trajectory at the found event.
    /// :rtype: Orbit
    pub orbit: Orbit,
    /// Indicates whether the event is a rising edge, falling edge, or unclear. This helps in understanding the direction of change at the event point.
    /// :rtype: EventEdge
    pub edge: EventEdge,
    /// Numerical evaluation of the event condition, e.g. if seeking the apoapsis, this returns the near zero
    /// :rtype: float
    pub value: f64,
    /// Numertical evaluation of the event condition one epoch step before the found event (used to compute the rising/falling edge).
    /// :rtype: float
    pub prev_value: Option<f64>,
    /// Numertical evaluation of the event condition one epoch step after the found event (used to compute the rising/falling edge).
    /// :rtype: float
    pub next_value: Option<f64>,
    /// Precision of the epoch for this value
    /// :rtype: Duration
    pub pm_duration: Duration,
    /// Store the representation of this event as a string because we can't move or clone the event reference
    /// :rtype: str
    pub repr: String,
}

impl EventDetails {
    /// Generates detailed information about an event at a specific epoch in a trajectory.
    ///
    /// This takes an `Epoch` as an input and returns a `Result<Self, EventError>`.
    /// It is designed to determine the state of a trajectory at a given epoch, evaluate the specific event at that state, and ascertain the nature of the event (rising, falling, or unclear).
    /// The initialization intelligently determines the edge type of the event by comparing the event's value at the current, previous, and next epochs.
    /// It ensures robust event characterization in trajectories.
    ///
    /// # Returns
    /// - `Ok(EventDetails<S>)` if the state at the given epoch can be determined and the event details are successfully evaluated.
    /// - `Err(EventError)` if there is an error in retrieving the state at the specified epoch.
    ///
    pub fn new(
        state: Orbit,
        value: f64,
        event: &Event,
        prev_state: Option<Orbit>,
        next_state: Option<Orbit>,
        almanac: &Almanac,
    ) -> Result<Self, AnalysisError> {
        let prev_value = if let Some(state) = prev_state {
            Some(event.eval(state, almanac)?)
        } else {
            None
        };

        let next_value = if let Some(state) = next_state {
            Some(event.eval(state, almanac)?)
        } else {
            None
        };

        let edge = if let Some(prev_value) = prev_value {
            if let Some(next_value) = next_value {
                if prev_value > value {
                    if value > next_value {
                        EventEdge::Falling
                    } else {
                        EventEdge::LocalMin
                    }
                } else if prev_value < value {
                    if value < next_value {
                        EventEdge::Rising
                    } else {
                        EventEdge::LocalMax
                    }
                } else {
                    EventEdge::Unclear
                }
            } else if prev_value > value {
                EventEdge::Falling
            } else {
                EventEdge::Rising
            }
        } else if let Some(next_value) = next_value {
            if next_value > value {
                EventEdge::Rising
            } else {
                EventEdge::Falling
            }
        } else {
            warn!(
                "could not determine edge of {event} because state could be queried around {}",
                state.epoch
            );
            EventEdge::Unclear
        };

        Ok(EventDetails {
            edge,
            orbit: state,
            value,
            prev_value,
            next_value,
            pm_duration: event.epoch_precision,
            repr: event.eval_string(state, almanac)?,
        })
    }
}

#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl EventDetails {
    /// :rtype: str
    fn describe(&self) -> String {
        format!("{self:?}")
    }
    fn __str__(&self) -> String {
        format!("{self}")
    }

    fn __repr__(&self) -> String {
        format!("{self}@{self:p}")
    }
    fn __eq__(&self, other: &Self) -> bool {
        self == other
    }
    fn __ne__(&self, other: &Self) -> bool {
        self != other
    }
}

impl fmt::Display for EventDetails {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} ({:?})", self.repr, self.edge)
    }
}

impl fmt::Debug for EventDetails {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let prev_fmt = match self.prev_value {
            Some(value) => format!("{value:.6}"),
            None => "".to_string(),
        };

        let next_fmt = match self.next_value {
            Some(value) => format!("{value:.6}"),
            None => "".to_string(),
        };

        write!(
            f,
            "{} and is {:?} (roots with {} intervals: {}, {:.6}, {})",
            self.repr, self.edge, self.pm_duration, prev_fmt, self.value, next_fmt
        )
    }
}

#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3(module = "anise.analysis", get_all))]
#[derive(Clone, PartialEq)]
pub struct EventArc {
    /// rise event of this arc
    /// :rtype: EventDetails
    pub rise: EventDetails,
    /// fall event of this arc
    /// :rtype: EventDetails
    pub fall: EventDetails,
}

#[cfg_attr(feature = "python", pymethods)]
impl EventArc {
    /// :rtype: Duration
    pub fn duration(&self) -> Duration {
        self.end_epoch() - self.start_epoch()
    }

    /// :rtype: Epoch
    pub fn start_epoch(&self) -> Epoch {
        self.rise.orbit.epoch
    }

    /// :rtype: Epoch
    pub fn end_epoch(&self) -> Epoch {
        self.fall.orbit.epoch
    }

    /// :rtype: Epoch
    pub fn midpoint_epoch(&self) -> Epoch {
        self.start_epoch() + 0.5 * self.duration()
    }

    #[cfg(feature = "python")]
    fn __str__(&self) -> String {
        format!("{self}")
    }
    #[cfg(feature = "python")]
    fn __repr__(&self) -> String {
        format!("{self}@{self:p}")
    }
}

impl fmt::Display for EventArc {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{} until {} (lasts {})",
            self.start_epoch(),
            self.end_epoch(),
            self.duration()
        )
    }
}

impl fmt::Debug for EventArc {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} until {}", self.rise, self.fall)
    }
}

#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3(module = "anise.analysis", get_all))]
#[derive(Clone, Debug, PartialEq)]
pub struct VisibilityArc {
    /// rise event of this arc
    /// :rtype: EventDetails
    pub rise: EventDetails,
    /// fall event of this arc
    /// :rtype: EventDetails
    pub fall: EventDetails,
    /// :rtype: str
    pub location_ref: String,
    /// :rtype: Location
    pub location: Location,
    /// Azimuth, Elevation, Range, Range-rate
    /// :rtype: list
    pub aer_data: Vec<AzElRange>,
    /// :rtype: Duration
    pub sample_rate: Duration,
}

#[cfg_attr(feature = "python", pymethods)]
impl VisibilityArc {
    /// :rtype: Duration
    pub fn duration(&self) -> Duration {
        self.end_epoch() - self.start_epoch()
    }

    /// :rtype: Epoch
    pub fn start_epoch(&self) -> Epoch {
        self.rise.orbit.epoch
    }

    /// :rtype: Epoch
    pub fn end_epoch(&self) -> Epoch {
        self.fall.orbit.epoch
    }

    #[cfg(feature = "python")]
    fn __str__(&self) -> String {
        format!("{self}")
    }
    #[cfg(feature = "python")]
    fn __repr__(&self) -> String {
        format!("{self}@{self:p}")
    }
}

impl fmt::Display for VisibilityArc {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{} ({}) visible from {} until {} ({}) ({} AER data)",
            self.location_ref,
            self.location,
            self.start_epoch(),
            self.end_epoch(),
            self.duration(),
            self.aer_data.len()
        )
    }
}