use crate::context::Context;
use crate::coordinate::Frame;
use crate::error::{Error, Result};
use crate::observers::{Observer, obs_code_from_bytes};
use crate::orbit::Orbit;
use crate::propagate::{ForceModelTier, PropagationConfig, UncertaintyMethod};
use std::ffi::CStr;
#[derive(Debug, Clone, PartialEq)]
pub struct EphemerisEntry {
pub orbit_id: String,
pub epoch: crate::Epoch,
pub ra_deg: f64,
pub dec_deg: f64,
pub rho_au: f64,
pub vrho_au_day: f64,
pub vra_deg_day: f64,
pub vdec_deg_day: f64,
pub light_time_days: f64,
pub phase_angle_deg: f64,
pub elongation_deg: f64,
pub heliocentric_distance_au: f64,
pub mag: f64,
pub mag_sigma: f64,
pub zenith_angle_deg: f64,
pub azimuth_deg: f64,
pub hour_angle_deg: f64,
pub lunar_elongation_deg: f64,
pub position_angle_deg: f64,
pub sky_rate_deg_day: f64,
pub obs_code: String,
}
impl EphemerisEntry {
pub(crate) fn from_ffi(e: &empyrean_sys::EmpyreanEphemerisEntry) -> Self {
let orbit_id = if e.orbit_id.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(e.orbit_id).to_string_lossy().into_owned() }
};
Self {
orbit_id,
epoch: crate::Epoch::from_mjd_tdb(e.epoch_mjd_tdb),
ra_deg: e.ra_deg,
dec_deg: e.dec_deg,
rho_au: e.rho_au,
vrho_au_day: e.vrho_au_day,
vra_deg_day: e.vra_deg_day,
vdec_deg_day: e.vdec_deg_day,
light_time_days: e.light_time_days,
phase_angle_deg: e.phase_angle_deg,
elongation_deg: e.elongation_deg,
heliocentric_distance_au: e.heliocentric_distance_au,
mag: e.mag,
mag_sigma: e.mag_sigma,
zenith_angle_deg: e.zenith_angle_deg,
azimuth_deg: e.azimuth_deg,
hour_angle_deg: e.hour_angle_deg,
lunar_elongation_deg: e.lunar_elongation_deg,
position_angle_deg: e.position_angle_deg,
sky_rate_deg_day: e.sky_rate_deg_day,
obs_code: obs_code_from_bytes(&e.obs_code),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ObservationSensitivity {
pub orbit_id: String,
pub object_id: Option<String>,
pub obs_code: String,
pub epoch_mjd_tdb: f64,
pub n_params: u8,
pub jacobian: Vec<f64>,
pub hessian: Vec<f64>,
pub frame: i32,
pub origin: i32,
}
impl ObservationSensitivity {
pub(crate) fn from_ffi(e: &empyrean_sys::EmpyreanObservationSensitivity) -> Self {
let orbit_id = if e.orbit_id.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(e.orbit_id).to_string_lossy().into_owned() }
};
let object_id = if e.object_id.is_null() {
None
} else {
Some(unsafe { CStr::from_ptr(e.object_id).to_string_lossy().into_owned() })
};
let jacobian = if e.jacobian.is_null() {
Vec::new()
} else {
unsafe { std::slice::from_raw_parts(e.jacobian, e.jacobian_len).to_vec() }
};
let hessian = if e.hessian.is_null() {
Vec::new()
} else {
unsafe { std::slice::from_raw_parts(e.hessian, e.hessian_len).to_vec() }
};
Self {
orbit_id,
object_id,
obs_code: obs_code_from_bytes(&e.obs_code),
epoch_mjd_tdb: e.epoch_mjd_tdb,
n_params: e.n_params,
jacobian,
hessian,
frame: e.frame,
origin: e.origin,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct EphemerisResult {
pub entries: Vec<EphemerisEntry>,
pub sensitivity: Vec<ObservationSensitivity>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct EphemerisConfig {
pub propagation: PropagationConfig,
pub max_light_time_iterations: usize,
pub light_time_tolerance_days: f64,
pub compute_diagnostics: bool,
}
impl Default for EphemerisConfig {
fn default() -> Self {
Self {
propagation: PropagationConfig::default(),
max_light_time_iterations: 0,
light_time_tolerance_days: 0.0,
compute_diagnostics: true,
}
}
}
impl EphemerisConfig {
pub(crate) fn to_ffi_with(
&self,
) -> (
empyrean_sys::EmpyreanEphemerisConfig,
crate::propagate::PropConfigKeep,
) {
let (prop_ffi, keep) = self.propagation.to_ffi_with();
let cfg = empyrean_sys::EmpyreanEphemerisConfig {
propagation: prop_ffi,
max_light_time_iterations: self.max_light_time_iterations,
light_time_tolerance_days: self.light_time_tolerance_days,
compute_diagnostics: u8::from(self.compute_diagnostics),
};
(cfg, keep)
}
pub fn with_force_model(force_model: ForceModelTier) -> Self {
Self {
propagation: PropagationConfig {
force_model,
frame: Frame::EclipticJ2000,
..PropagationConfig::default()
},
..Self::default()
}
}
}
impl Context {
pub fn generate_ephemeris(
&self,
orbits: &[Orbit],
observers: &[Observer],
config: &EphemerisConfig,
) -> Result<EphemerisResult> {
let _ = (Frame::ICRF, UncertaintyMethod::FirstOrder); let (ffi_orbits, _orbit_keep) = crate::orbit::orbits_to_ffi(orbits)?;
let ffi_observers = observers_to_ffi(observers)?;
let mut result = empyrean_sys::EmpyreanEphemerisResult {
entries: std::ptr::null_mut(),
num_entries: 0,
sensitivity: std::ptr::null_mut(),
num_sensitivity: 0,
};
let (ffi_config, _config_keep) = config.to_ffi_with();
let code = unsafe {
empyrean_sys::empyrean_generate_ephemeris(
self.as_raw(),
ffi_orbits.as_ptr(),
ffi_orbits.len(),
ffi_observers.as_ptr(),
ffi_observers.len(),
&ffi_config,
&mut result,
)
};
if code != 0 {
return Err(Error::capture(code));
}
Ok(marshal_ephemeris_result(&mut result))
}
}
pub(crate) fn observers_to_ffi(
observers: &[Observer],
) -> Result<Vec<empyrean_sys::EmpyreanObserver>> {
observers
.iter()
.map(|o| {
let mut code_bytes = [0u8; 4];
let src = o.obs_code.as_bytes();
if src.len() > 3 {
return Err(Error::invalid_input(format!(
"observatory code \"{}\" is longer than 3 bytes; \
4-character MPC codes are not yet supported by the \
engine's observatory registry",
o.obs_code
)));
}
code_bytes[..src.len()].copy_from_slice(src);
Ok(empyrean_sys::EmpyreanObserver {
obs_code: code_bytes,
epoch_mjd_tdb: o.epoch.mjd_tdb()?,
x: o.position[0],
y: o.position[1],
z: o.position[2],
vx: o.velocity[0],
vy: o.velocity[1],
vz: o.velocity[2],
observing_night: o.observing_night,
})
})
.collect::<Result<Vec<_>>>()
}
pub(crate) fn marshal_ephemeris_result(
result: &mut empyrean_sys::EmpyreanEphemerisResult,
) -> EphemerisResult {
let entries = if result.entries.is_null() {
Vec::new()
} else {
unsafe {
std::slice::from_raw_parts(result.entries, result.num_entries)
.iter()
.map(EphemerisEntry::from_ffi)
.collect()
}
};
let sensitivity = if result.sensitivity.is_null() {
Vec::new()
} else {
unsafe {
std::slice::from_raw_parts(result.sensitivity, result.num_sensitivity)
.iter()
.map(ObservationSensitivity::from_ffi)
.collect()
}
};
unsafe { empyrean_sys::empyrean_ephemeris_result_free(result) };
EphemerisResult {
entries,
sensitivity,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::observers::Observer;
#[test]
fn four_char_obs_code_is_rejected() {
let observer = Observer {
obs_code: "W68a".to_string(),
epoch: crate::Epoch::from_mjd_tdb(61000.0),
position: [1.0, 0.0, 0.0],
velocity: [0.0, 0.01, 0.0],
observing_night: -1,
};
let err = observers_to_ffi(&[observer])
.expect_err("4-character observatory code must not marshal");
let msg = err.to_string();
assert!(msg.contains("W68a"), "error names the code: {msg}");
assert!(
msg.contains("longer than 3 bytes"),
"error states the contract: {msg}"
);
}
#[test]
fn three_char_obs_code_marshals() {
let observer = Observer {
obs_code: "W68".to_string(),
epoch: crate::Epoch::from_mjd_tdb(61000.0),
position: [1.0, 0.0, 0.0],
velocity: [0.0, 0.01, 0.0],
observing_night: -1,
};
let ffi = observers_to_ffi(&[observer]).expect("3-character code marshals");
assert_eq!(&ffi[0].obs_code, b"W68\0");
}
}