use std::ffi::CStr;
use crate::observers::obs_code_from_bytes;
use crate::orbit::Orbit;
use crate::propagate::{CovarianceKind, ForceModelTier, PropagatedState};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RejectionReason {
Accepted,
ChiSquared,
SigmaClip,
CooksDistance,
Adaptive,
UnsupportedObservatory,
CMC2003,
RadarObservationsUnsupported,
OccultationObservationsUnsupported,
OutsideArc,
NotEvaluated,
}
impl RejectionReason {
pub(super) fn from_int(v: i32) -> Self {
match v {
0 => RejectionReason::Accepted,
1 => RejectionReason::ChiSquared,
2 => RejectionReason::SigmaClip,
3 => RejectionReason::CooksDistance,
4 => RejectionReason::Adaptive,
5 => RejectionReason::UnsupportedObservatory,
6 => RejectionReason::CMC2003,
7 => RejectionReason::RadarObservationsUnsupported,
8 => RejectionReason::OccultationObservationsUnsupported,
9 => RejectionReason::OutsideArc,
_ => RejectionReason::NotEvaluated,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ObservationResidual {
pub obs_id: String,
pub obs_code: String,
pub ast_cat: Option<String>,
pub epoch: crate::Epoch,
pub ra_residual_arcsec: f64,
pub dec_residual_arcsec: f64,
pub chi2: f64,
pub dof: u32,
pub probability: f64,
pub selected: bool,
pub residual_cov_ra: f64,
pub residual_cov_dec: f64,
pub residual_cov_corr: f64,
pub rejection_reason: RejectionReason,
pub rejection_criterion: f64,
pub rejection_threshold: f64,
pub rejection_effective_threshold: f64,
pub rejection_information_loss: f64,
pub cooks_distance: f64,
pub leverage: f64,
pub fractional_information: f64,
pub along_track_arcsec: f64,
pub cross_track_arcsec: f64,
pub along_track_error_arcsec: f64,
pub cross_track_error_arcsec: f64,
pub track_position_angle_deg: f64,
pub influence_information_loss: f64,
pub along_cross_covariance_arcsec2: f64,
pub radar: Option<RadarResidual>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RadarResidualKind {
Delay,
Doppler,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RadarResidual {
pub kind: RadarResidualKind,
pub residual: f64,
pub chi2: f64,
pub dof: u32,
pub probability: f64,
pub variance: Option<f64>,
}
impl ObservationResidual {
pub(super) fn from_ffi(r: &empyrean_sys::EmpyreanObservationResult) -> Self {
let obs_id = if r.obs_id.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(r.obs_id) }
.to_string_lossy()
.into_owned()
};
let ast_cat = if r.ast_cat.is_null() {
None
} else {
let s = unsafe { CStr::from_ptr(r.ast_cat) }
.to_string_lossy()
.into_owned();
(!s.is_empty()).then_some(s)
};
Self {
obs_id,
obs_code: obs_code_from_bytes(&r.obs_code),
ast_cat,
epoch: crate::Epoch::from_mjd_tdb(r.epoch_mjd_tdb),
ra_residual_arcsec: r.ra_residual_arcsec,
dec_residual_arcsec: r.dec_residual_arcsec,
chi2: r.chi2,
dof: r.dof,
probability: r.probability,
selected: r.selected != 0,
residual_cov_ra: r.residual_cov_ra,
residual_cov_dec: r.residual_cov_dec,
residual_cov_corr: r.residual_cov_corr,
rejection_reason: RejectionReason::from_int(r.rejection_reason),
rejection_criterion: r.rejection_criterion,
rejection_threshold: r.rejection_threshold,
rejection_effective_threshold: r.rejection_effective_threshold,
rejection_information_loss: r.rejection_information_loss,
cooks_distance: r.cooks_distance,
leverage: r.leverage,
fractional_information: r.fractional_information,
along_track_arcsec: r.along_track_arcsec,
cross_track_arcsec: r.cross_track_arcsec,
along_track_error_arcsec: r.along_track_error_arcsec,
cross_track_error_arcsec: r.cross_track_error_arcsec,
track_position_angle_deg: r.track_position_angle_deg,
influence_information_loss: r.influence_information_loss,
along_cross_covariance_arcsec2: r.along_cross_covariance_arcsec2,
radar: (r.has_radar != 0).then(|| RadarResidual {
kind: if r.radar_kind == empyrean_sys::EMPYREAN_RADAR_KIND_DOPPLER as u8 {
RadarResidualKind::Doppler
} else {
RadarResidualKind::Delay
},
residual: r.radar_residual,
chi2: r.radar_chi2,
dof: r.radar_dof,
probability: r.radar_probability,
variance: r.radar_variance.is_finite().then_some(r.radar_variance),
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ResidualSummary {
pub num_obs: usize,
pub num_selected: usize,
pub num_rejected: usize,
pub chi2: f64,
pub dof: usize,
pub reduced_chi2: f64,
pub rms_ra_arcsec: f64,
pub rms_dec_arcsec: f64,
pub rms_combined_arcsec: f64,
pub weighted_rms_ra_arcsec: f64,
pub weighted_rms_dec_arcsec: f64,
pub weighted_rms_combined_arcsec: f64,
pub mean_ra_arcsec: f64,
pub mean_dec_arcsec: f64,
pub std_ra_arcsec: f64,
pub std_dec_arcsec: f64,
pub rms_along_track_arcsec: f64,
pub rms_cross_track_arcsec: f64,
}
impl ResidualSummary {
pub(super) fn from_ffi(s: &empyrean_sys::EmpyreanResidualSummary) -> Self {
Self {
num_obs: s.num_obs,
num_selected: s.num_selected,
num_rejected: s.num_rejected,
chi2: s.chi2,
dof: s.dof,
reduced_chi2: s.reduced_chi2,
rms_ra_arcsec: s.rms_ra_arcsec,
rms_dec_arcsec: s.rms_dec_arcsec,
rms_combined_arcsec: s.rms_combined_arcsec,
weighted_rms_ra_arcsec: s.weighted_rms_ra_arcsec,
weighted_rms_dec_arcsec: s.weighted_rms_dec_arcsec,
weighted_rms_combined_arcsec: s.weighted_rms_combined_arcsec,
mean_ra_arcsec: s.mean_ra_arcsec,
mean_dec_arcsec: s.mean_dec_arcsec,
std_ra_arcsec: s.std_ra_arcsec,
std_dec_arcsec: s.std_dec_arcsec,
rms_along_track_arcsec: s.rms_along_track_arcsec,
rms_cross_track_arcsec: s.rms_cross_track_arcsec,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CovarianceRepresentation {
Cartesian,
Keplerian,
Cometary,
Spherical,
}
impl CovarianceRepresentation {
pub(super) fn from_int(v: i32) -> Self {
match v {
0 => Self::Cartesian,
1 => Self::Keplerian,
2 => Self::Cometary,
_ => Self::Spherical,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SolveFor {
pub marsden: bool,
pub dt: bool,
pub amrat: bool,
pub thrust_segments: u32,
}
impl SolveFor {
fn from_covariance(cov: &SolvedCovariance) -> Self {
Self {
marsden: cov.marsden_slot.is_some(),
dt: cov.dt_slot.is_some(),
amrat: cov.amrat_slot.is_some(),
thrust_segments: cov.thrust_slots.len() as u32,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SolveForParams {
StateOnly,
StateAndNonGrav,
Auto,
Explicit(SolveFor),
}
impl SolveForParams {
pub(super) fn from_result(code: i32, cov: Option<&SolvedCovariance>) -> Self {
match code {
0 => Self::StateOnly,
1 => Self::StateAndNonGrav,
3 => Self::Explicit(cov.map(SolveFor::from_covariance).unwrap_or_default()),
_ => Self::Auto,
}
}
pub(super) fn to_int(self) -> i32 {
match self {
Self::StateOnly => 0,
Self::StateAndNonGrav => 1,
Self::Auto => 2,
Self::Explicit(_) => 3,
}
}
pub(super) fn flags(self) -> SolveFor {
match self {
Self::StateOnly | Self::Auto => SolveFor::default(),
Self::StateAndNonGrav => SolveFor {
marsden: true,
..SolveFor::default()
},
Self::Explicit(sf) => sf,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum OutputEpoch {
#[default]
MidArc,
LastObservation,
IODEpoch,
Epoch(f64),
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum OriginPolicy {
#[default]
Auto,
Explicit(crate::coordinate::Origin),
}
impl From<crate::coordinate::Origin> for OriginPolicy {
fn from(origin: crate::coordinate::Origin) -> Self {
Self::Explicit(origin)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AcceptabilityReport {
pub fit_acceptable: bool,
pub extrapolation_acceptable: bool,
pub converged_ok: bool,
pub reduced_chi2_ok: bool,
pub reduced_chi2_value: f64,
pub reduced_chi2_threshold: f64,
pub rms_ok: bool,
pub rms_value_arcsec: f64,
pub rms_threshold_arcsec: f64,
pub residual_isotropy_ok: bool,
pub at_ct_ratio_value: f64,
pub at_ct_ratio_threshold: f64,
pub covariance_ok: bool,
pub arc_coverage_ok: bool,
pub arc_days_value: f64,
pub arc_days_threshold: f64,
pub fractional_sigma_a_ok: bool,
pub fractional_sigma_a_value: f64,
pub fractional_sigma_a_threshold: f64,
}
impl AcceptabilityReport {
pub(super) fn from_ffi(r: &empyrean_sys::EmpyreanAcceptabilityReport) -> Self {
Self {
fit_acceptable: r.fit_acceptable != 0,
extrapolation_acceptable: r.extrapolation_acceptable != 0,
converged_ok: r.converged_ok != 0,
reduced_chi2_ok: r.reduced_chi2_ok != 0,
reduced_chi2_value: r.reduced_chi2_value,
reduced_chi2_threshold: r.reduced_chi2_threshold,
rms_ok: r.rms_ok != 0,
rms_value_arcsec: r.rms_value_arcsec,
rms_threshold_arcsec: r.rms_threshold_arcsec,
residual_isotropy_ok: r.residual_isotropy_ok != 0,
at_ct_ratio_value: r.at_ct_ratio_value,
at_ct_ratio_threshold: r.at_ct_ratio_threshold,
covariance_ok: r.covariance_ok != 0,
arc_coverage_ok: r.arc_coverage_ok != 0,
arc_days_value: r.arc_days_value,
arc_days_threshold: r.arc_days_threshold,
fractional_sigma_a_ok: r.fractional_sigma_a_ok != 0,
fractional_sigma_a_value: r.fractional_sigma_a_value,
fractional_sigma_a_threshold: r.fractional_sigma_a_threshold,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StationBias {
pub obs_code: String,
pub n_obs: usize,
pub bias_ra_arcsec: f64,
pub sigma_ra_arcsec: f64,
pub bias_dec_arcsec: f64,
pub sigma_dec_arcsec: f64,
pub bias_timing_sec: Option<f64>,
pub sigma_timing_sec: Option<f64>,
pub significance: f64,
}
impl StationBias {
pub(super) fn from_ffi(b: &empyrean_sys::EmpyreanStationBias) -> Self {
let obs_code = if b.obs_code.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(b.obs_code) }
.to_string_lossy()
.into_owned()
};
let (bias_t, sigma_t) = if b.has_timing != 0 {
(Some(b.bias_timing_sec), Some(b.sigma_timing_sec))
} else {
(None, None)
};
Self {
obs_code,
n_obs: b.n_obs,
bias_ra_arcsec: b.bias_ra_arcsec,
sigma_ra_arcsec: b.sigma_ra_arcsec,
bias_dec_arcsec: b.bias_dec_arcsec,
sigma_dec_arcsec: b.sigma_dec_arcsec,
bias_timing_sec: bias_t,
sigma_timing_sec: sigma_t,
significance: b.significance,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SolvedCovariance {
pub matrix: Vec<Vec<f64>>,
pub width: usize,
pub marsden_slot: Option<usize>,
pub dt_slot: Option<usize>,
pub amrat_slot: Option<usize>,
pub thrust_slots: Vec<[usize; 3]>,
}
impl SolvedCovariance {
pub(super) fn from_ffi(c: &empyrean_sys::EmpyreanSolvedCovariance) -> Self {
let width = c.width as usize;
let matrix = (0..width)
.map(|i| (0..width).map(|j| c.matrix[i][j]).collect())
.collect();
let slot = |v: u32| (v != empyrean_sys::EMPYREAN_SLOT_NONE).then_some(v as usize);
let thrust_slots = (0..c.thrust_count as usize)
.map(|i| {
let r = c.thrust_slots[i];
[r[0] as usize, r[1] as usize, r[2] as usize]
})
.collect();
Self {
matrix,
width,
marsden_slot: slot(c.marsden_slot),
dt_slot: slot(c.dt_slot),
amrat_slot: slot(c.amrat_slot),
thrust_slots,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PhotometryModel {
#[default]
Auto,
HOnly,
HG,
HG12,
HG1G2,
}
impl PhotometryModel {
pub(super) fn to_int(self) -> i32 {
(match self {
Self::Auto => empyrean_sys::EMPYREAN_PHOTOMETRY_MODEL_AUTO,
Self::HOnly => empyrean_sys::EMPYREAN_PHOTOMETRY_MODEL_HONLY,
Self::HG => empyrean_sys::EMPYREAN_PHOTOMETRY_MODEL_HG,
Self::HG12 => empyrean_sys::EMPYREAN_PHOTOMETRY_MODEL_HG12,
Self::HG1G2 => empyrean_sys::EMPYREAN_PHOTOMETRY_MODEL_HG1G2,
}) as i32
}
fn from_int(v: i32) -> Self {
match v as u32 {
empyrean_sys::EMPYREAN_PHOTOMETRY_MODEL_HONLY => Self::HOnly,
empyrean_sys::EMPYREAN_PHOTOMETRY_MODEL_HG => Self::HG,
empyrean_sys::EMPYREAN_PHOTOMETRY_MODEL_HG12 => Self::HG12,
empyrean_sys::EMPYREAN_PHOTOMETRY_MODEL_HG1G2 => Self::HG1G2,
_ => Self::Auto,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BandStat {
pub band: String,
pub n: usize,
pub offset_applied: f64,
pub mean_residual: f64,
pub rms: f64,
}
impl BandStat {
fn from_ffi(b: &empyrean_sys::EmpyreanBandStat) -> Self {
let band = if b.band.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(b.band) }
.to_string_lossy()
.into_owned()
};
Self {
band,
n: b.n,
offset_applied: b.offset_applied,
mean_residual: b.mean_residual,
rms: b.rms,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct GateRecord {
pub model: PhotometryModel,
pub passed: bool,
pub reason: String,
}
impl GateRecord {
fn from_ffi(g: &empyrean_sys::EmpyreanGateRecord) -> Self {
let reason = if g.reason.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(g.reason) }
.to_string_lossy()
.into_owned()
};
Self {
model: PhotometryModel::from_int(g.model),
passed: g.passed != 0,
reason,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PhotometryResult {
pub h: f64,
pub slope1: f64,
pub slope2: f64,
pub covariance: Option<[[f64; 3]; 3]>,
pub model_used: PhotometryModel,
pub reduced_chi2: f64,
pub constraint_active: bool,
pub n_mags_used: usize,
pub n_mags_rejected_photometric: usize,
pub n_obs_without_mags: usize,
pub n_mags_from_astrometric_selected: usize,
pub n_mags_from_astrometric_rejected: usize,
pub alpha_min_deg: f64,
pub alpha_max_deg: f64,
pub alpha_span_deg: f64,
pub per_band: Vec<BandStat>,
pub gates: Vec<GateRecord>,
pub n_mags_dropped_unconvertible: usize,
pub dropped_bands: Vec<String>,
}
impl PhotometryResult {
pub(super) fn from_ffi(p: &empyrean_sys::EmpyreanODPhotometryResult) -> Self {
let covariance = (p.has_covariance != 0).then_some(p.covariance);
let per_band = if p.per_band.is_null() || p.num_per_band == 0 {
Vec::new()
} else {
unsafe {
std::slice::from_raw_parts(p.per_band, p.num_per_band)
.iter()
.map(BandStat::from_ffi)
.collect()
}
};
let gates = if p.gates.is_null() || p.num_gates == 0 {
Vec::new()
} else {
unsafe {
std::slice::from_raw_parts(p.gates, p.num_gates)
.iter()
.map(GateRecord::from_ffi)
.collect()
}
};
Self {
h: p.h,
slope1: p.slope1,
slope2: p.slope2,
covariance,
model_used: PhotometryModel::from_int(p.model_used),
reduced_chi2: p.reduced_chi2,
constraint_active: p.constraint_active != 0,
n_mags_used: p.n_mags_used,
n_mags_rejected_photometric: p.n_mags_rejected_photometric,
n_obs_without_mags: p.n_obs_without_mags,
n_mags_from_astrometric_selected: p.n_mags_from_astrometric_selected,
n_mags_from_astrometric_rejected: p.n_mags_from_astrometric_rejected,
alpha_min_deg: p.alpha_min_deg,
alpha_max_deg: p.alpha_max_deg,
alpha_span_deg: p.alpha_span_deg,
per_band,
gates,
n_mags_dropped_unconvertible: p.n_mags_dropped_unconvertible,
dropped_bands: if p.dropped_bands.is_null() || p.num_dropped_bands == 0 {
Vec::new()
} else {
unsafe {
std::slice::from_raw_parts(p.dropped_bands, p.num_dropped_bands)
.iter()
.map(|&b| {
if b.is_null() {
String::new()
} else {
CStr::from_ptr(b).to_string_lossy().into_owned()
}
})
.collect()
}
},
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TrustGateEvent {
CloseApproach {
body: String,
epoch: crate::Epoch,
distance_au: f64,
},
HighNonlinearity {
epoch: crate::Epoch,
nonlinearity: f64,
threshold: f64,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum CovarianceTrust {
Trusted,
EncounterIntervenes {
event: TrustGateEvent,
solved_width: u32,
second_order_recoverable: bool,
},
WeaklyDeterminedHighN {
solved_width: u32,
},
}
impl CovarianceTrust {
pub(super) fn from_ffi(r: &empyrean_sys::EmpyreanODResult) -> Option<Self> {
match r.covariance_trust {
empyrean_sys::EMPYREAN_COVARIANCE_TRUST_TRUSTED => Some(CovarianceTrust::Trusted),
empyrean_sys::EMPYREAN_COVARIANCE_TRUST_ENCOUNTER_INTERVENES => {
let event =
if r.trust_event_kind == empyrean_sys::EMPYREAN_TRUST_EVENT_HIGH_NONLINEARITY {
TrustGateEvent::HighNonlinearity {
epoch: crate::Epoch::from_mjd_tdb(r.trust_event_epoch_mjd_tdb),
nonlinearity: r.trust_event_nonlinearity,
threshold: r.trust_event_threshold,
}
} else {
TrustGateEvent::CloseApproach {
body: if r.trust_event_body.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(r.trust_event_body) }
.to_string_lossy()
.into_owned()
},
epoch: crate::Epoch::from_mjd_tdb(r.trust_event_epoch_mjd_tdb),
distance_au: r.trust_event_distance_au,
}
};
Some(CovarianceTrust::EncounterIntervenes {
event,
solved_width: r.trust_solved_width,
second_order_recoverable: r.trust_second_order_recoverable != 0,
})
}
empyrean_sys::EMPYREAN_COVARIANCE_TRUST_WEAKLY_DETERMINED_HIGH_N => {
Some(CovarianceTrust::WeaklyDeterminedHighN {
solved_width: r.trust_solved_width,
})
}
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DetermineResult {
pub orbit: Orbit,
pub residuals: Vec<ObservationResidual>,
pub summary: ResidualSummary,
pub iterations: u32,
pub update_norm: f64,
pub converged: bool,
pub covariance: [[f64; 6]; 6],
pub covariance_representation: CovarianceRepresentation,
pub covariance_9x9: Option<[[f64; 9]; 9]>,
pub non_grav_delta: Option<[f64; 3]>,
pub rejection_passes: u32,
pub num_oppositions_fit: u32,
pub force_model_used: ForceModelTier,
pub solve_for_used: SolveForParams,
pub acceptability: AcceptabilityReport,
pub station_biases: Vec<StationBias>,
pub solved_covariance: Option<SolvedCovariance>,
pub dt_delta: Option<f64>,
pub amrat_delta: Option<f64>,
pub thrust_delta_m_per_s: Vec<[f64; 3]>,
pub dv_frame: Option<crate::coordinate::Frame>,
pub photometry: Option<PhotometryResult>,
pub covariance_trust: Option<CovarianceTrust>,
}
impl DetermineResult {
pub fn state(&self) -> PropagatedState {
let st = &self.orbit.state;
let e = st.elements;
PropagatedState {
epoch: st.epoch,
position: [e[0], e[1], e[2]],
velocity: [e[3], e[4], e[5]],
origin: st.origin,
frame: st.frame,
covariance: Some(self.covariance),
stm: None,
stt: None,
resolved_kind: CovarianceKind::Linear,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct EvaluateResult {
pub residuals: Vec<ObservationResidual>,
pub summary: ResidualSummary,
}