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;
#[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(),
}
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3(module = "anise.analysis"))]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Event {
pub scalar: ScalarExpr,
pub condition: Condition,
pub epoch_precision: Duration,
pub ab_corr: Option<Aberration>,
}
impl Event {
#[must_use]
pub fn new(scalar: ScalarExpr, condition: Condition) -> Self {
Self {
scalar,
condition,
epoch_precision: Unit::Millisecond * 10,
ab_corr: None,
}
}
pub fn apoapsis() -> Self {
Event {
scalar: ScalarExpr::Element(OrbitalElement::TrueAnomaly),
condition: Condition::Equals(180.0),
epoch_precision: Unit::Second * 0.1,
ab_corr: None,
}
}
pub fn periapsis() -> Self {
Event {
scalar: ScalarExpr::Element(OrbitalElement::TrueAnomaly),
condition: Condition::Equals(0.0),
epoch_precision: Unit::Millisecond * 10,
ab_corr: None,
}
}
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,
}
}
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,
}
}
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,
}
}
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,
}
}
pub fn to_s_expr(&self) -> Result<String, serde_lexpr::Error> {
Ok(serde_lexpr::to_value(self)?.to_string())
}
pub fn from_s_expr(expr: &str) -> Result<Self, serde_lexpr::Error> {
serde_lexpr::from_str(expr)
}
}
#[cfg_attr(feature = "python", pymethods)]
impl Event {
pub fn eval(&self, orbit: Orbit, almanac: &Almanac) -> Result<f64, AnalysisError> {
let mut current_val = self.scalar.evaluate(orbit, self.ab_corr, almanac)?;
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 {
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;
}
}
let current_rad = current_val.to_radians();
let desired_rad = desired_val.to_radians();
let (cur_sin, cur_cos) = current_rad.sin_cos();
let (des_sin, des_cos) = desired_rad.sin_cos();
let y = cur_sin * des_cos - cur_cos * des_sin; let x = cur_cos * des_cos + cur_sin * des_sin;
return Ok(y.atan2(x).to_degrees());
}
}
match self.condition {
Condition::Equals(val) => Ok(current_val - val),
Condition::Between(min_val, max_val) => {
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), Condition::GreaterThan(val) => Ok(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
),
}),
}
}
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)?;
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 {
#[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()))
}
#[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")]
fn py_apoapsis(_cls: Bound<'_, PyType>) -> Self {
Event::apoapsis()
}
#[classmethod]
#[pyo3(name = "periapsis")]
fn py_periapsis(_cls: Bound<'_, PyType>) -> Self {
Event::periapsis()
}
#[classmethod]
#[pyo3(name = "total_eclipse")]
fn py_total_eclipse(_cls: Bound<'_, PyType>, eclipsing_frame: Frame) -> Self {
Event::total_eclipse(eclipsing_frame)
}
#[classmethod]
#[pyo3(name = "eclipse")]
fn py_eclipse(_cls: Bound<'_, PyType>, eclipsing_frame: Frame) -> Self {
Event::eclipse(eclipsing_frame)
}
#[classmethod]
#[pyo3(name = "penumbra")]
fn py_penumbra(_cls: Bound<'_, PyType>, eclipsing_frame: Frame) -> Self {
Event::penumbra(eclipsing_frame)
}
#[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,
}
}
#[getter]
fn scalar(&self) -> Result<PyScalarExpr, PyErr> {
PyScalarExpr::try_from(self.scalar.clone())
}
#[getter]
fn condition(&self) -> Condition {
self.condition
}
#[getter]
fn epoch_precision(&self) -> Duration {
self.epoch_precision
}
#[getter]
fn ab_corr(&self) -> Option<Aberration> {
self.ab_corr
}
#[setter]
fn set_scalar(&mut self, scalar: PyScalarExpr) {
self.scalar = scalar.into();
}
#[setter]
fn set_condition(&mut self, condition: Condition) {
self.condition = condition;
}
#[setter]
fn set_epoch_precision(&mut self, epoch_precision: Duration) {
self.epoch_precision = epoch_precision;
}
#[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),
}
}
}
#[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 {
Rising,
Falling,
LocalMin,
LocalMax,
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
}
}
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3(module = "anise.analysis", get_all))]
pub struct EventDetails {
pub orbit: Orbit,
pub edge: EventEdge,
pub value: f64,
pub prev_value: Option<f64>,
pub next_value: Option<f64>,
pub pm_duration: Duration,
pub repr: String,
}
impl EventDetails {
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 {
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 {
pub rise: EventDetails,
pub fall: EventDetails,
}
#[cfg_attr(feature = "python", pymethods)]
impl EventArc {
pub fn duration(&self) -> Duration {
self.end_epoch() - self.start_epoch()
}
pub fn start_epoch(&self) -> Epoch {
self.rise.orbit.epoch
}
pub fn end_epoch(&self) -> Epoch {
self.fall.orbit.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 {
pub rise: EventDetails,
pub fall: EventDetails,
pub location_ref: String,
pub location: Location,
pub aer_data: Vec<AzElRange>,
pub sample_rate: Duration,
}
#[cfg_attr(feature = "python", pymethods)]
impl VisibilityArc {
pub fn duration(&self) -> Duration {
self.end_epoch() - self.start_epoch()
}
pub fn start_epoch(&self) -> Epoch {
self.rise.orbit.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()
)
}
}