use photom::observation_dataset::observation::Observation;
use crate::{
cache::OutfitCache,
differential_orbit_correction::{
least_square::{
angular_diff, solve_weighted_least_squares, ObservationEquation, OrbitalUncertainty,
},
obs_fit_data::ObsFitData,
},
ephemeris::observation_ephemeris::ObservationEphemeris,
propagator::PropagatorKind,
EquinoctialElements, JPLEphem, OutfitError,
};
#[derive(Debug, Clone)]
pub struct SingleIterationResult {
pub corrected_elements: EquinoctialElements,
pub updated_obs_fit_data: Vec<ObsFitData>,
pub observation_equations: Vec<ObservationEquation>,
pub correction_norm: f64,
pub normalised_rms: f64,
pub uncertainty: OrbitalUncertainty,
pub num_measurements: usize,
}
#[allow(clippy::too_many_arguments)]
pub fn single_iteration(
observations: &[Observation],
obs_fit_data: &[ObsFitData],
elements: &EquinoctialElements,
free_elements: &[bool; 6],
cache: &OutfitCache,
jpl: &JPLEphem,
apply_correction: bool,
propagator: &PropagatorKind,
) -> Result<SingleIterationResult, OutfitError> {
assert_eq!(
observations.len(),
obs_fit_data.len(),
"observations and obs_fit_data must have the same length"
);
let (equations, updated_obs_fit_data): (Vec<ObservationEquation>, Vec<ObsFitData>) =
observations
.iter()
.zip(obs_fit_data.iter())
.map(|(obs, fit_data)| {
if !fit_data.is_active() {
return (
ObservationEquation::uncorrelated(
nalgebra::Vector6::zeros(),
nalgebra::Vector6::zeros(),
0.0,
0.0,
1.0, 1.0,
false,
),
fit_data.clone(),
);
}
let partials_result = match propagator {
PropagatorKind::TwoBody => {
obs.compute_obs_and_partials_2body(cache, jpl, elements)
}
PropagatorKind::NBody(nbody_config) => {
obs.compute_obs_and_partials_nbody(cache, jpl, elements, nbody_config)
}
};
match partials_result {
Ok(partials) => {
let obs_ra_debiased = obs.equ_coord().ra - fit_data.bias_ra;
let residual_ra = angular_diff(obs_ra_debiased, partials.ra);
let obs_dec_debiased = obs.equ_coord().dec - fit_data.bias_dec;
let residual_dec = obs_dec_debiased - partials.dec;
let chi = ((residual_ra / fit_data.sigma_ra).powi(2)
+ (residual_dec / fit_data.sigma_dec).powi(2))
.sqrt();
let eq = ObservationEquation::uncorrelated(
partials.d_ra_d_elem,
partials.d_dec_d_elem,
residual_ra,
residual_dec,
fit_data.sigma_ra,
fit_data.sigma_dec,
true,
);
let updated = ObsFitData {
residual_ra,
residual_dec,
chi,
..fit_data.clone()
};
(eq, updated)
}
Err(err) => {
eprintln!(
"single_iteration: propagation failed for observation at MJD {:.4}: {}. \
Observation skipped for this iteration.",
obs.mjd_tt(),
err
);
(
ObservationEquation::uncorrelated(
nalgebra::Vector6::zeros(),
nalgebra::Vector6::zeros(),
0.0,
0.0,
1.0, 1.0,
false,
),
fit_data.clone(),
)
}
}
})
.unzip();
let ls_result = solve_weighted_least_squares(&equations, free_elements)?;
let dx = &ls_result.element_correction;
let c = &ls_result.uncertainty.normal_matrix;
let correction_norm = (dx.dot(&(c * dx))).sqrt();
let corrected_elements = if apply_correction {
let mut new_coord = [
elements.semi_major_axis,
elements.eccentricity_sin_lon,
elements.eccentricity_cos_lon,
elements.tan_half_incl_sin_node,
elements.tan_half_incl_cos_node,
elements.mean_longitude,
];
for j in 0..6 {
if free_elements[j] {
new_coord[j] += dx[j];
}
}
EquinoctialElements {
reference_epoch: elements.reference_epoch,
semi_major_axis: new_coord[0],
eccentricity_sin_lon: new_coord[1],
eccentricity_cos_lon: new_coord[2],
tan_half_incl_sin_node: new_coord[3],
tan_half_incl_cos_node: new_coord[4],
mean_longitude: new_coord[5],
}
} else {
elements.clone()
};
Ok(SingleIterationResult {
corrected_elements,
updated_obs_fit_data,
observation_equations: equations,
correction_norm,
normalised_rms: ls_result.normalised_rms,
uncertainty: ls_result.uncertainty,
num_measurements: ls_result.num_measurements,
})
}
#[cfg(test)]
mod single_iteration_tests {
use super::*;
use crate::{
differential_orbit_correction::obs_fit_data::ObsSelection,
test_fixture::{JPL_EPHEM_HORIZON, UT1_PROVIDER},
};
use approx::assert_abs_diff_eq;
use photom::{
coordinates::equatorial::EquCoord,
observation_dataset::{observation::ObservationInput, ObsDataset},
observer::{
dataset::ObserverId,
error_model::{ModelCorrection, ObsErrorModel},
},
photometry::{Filter, Photometry},
};
fn circular_elements(epoch: f64) -> EquinoctialElements {
EquinoctialElements {
reference_epoch: epoch,
semi_major_axis: 1.8,
eccentricity_sin_lon: 0.1,
eccentricity_cos_lon: 0.05,
tan_half_incl_sin_node: 0.01,
tan_half_incl_cos_node: 0.1,
mean_longitude: 1.0,
}
}
fn make_dataset_and_cache(t0: f64, time_span: f64, n: usize) -> (ObsDataset, OutfitCache) {
let step = if n > 1 {
time_span / (n - 1) as f64
} else {
0.0
};
let inputs: Vec<ObservationInput> = (0..n)
.map(|i| {
let t_obs = t0 + i as f64 * step;
ObservationInput::new(
i as u64,
EquCoord {
ra: 0.0,
ra_error: 0.0,
dec: 0.0,
dec_error: 0.0,
},
Photometry {
magnitude: 15.0,
error: 0.1,
filter: Filter::Int(0),
},
t_obs,
Some(ObserverId::MpcCode(*b"F51")),
)
})
.collect();
let obs_dataset = {
let mut ds = ObsDataset::empty();
for input in inputs {
ds = ds.push_observation(vec![input]).unwrap().0;
}
ds.with_error_model(ObsErrorModel::FCCT14)
.apply_model_errors()
};
let cache =
OutfitCache::build(&obs_dataset, &JPL_EPHEM_HORIZON, &UT1_PROVIDER, false).unwrap();
(obs_dataset, cache)
}
#[test]
fn test_all_inactive_gives_zero_correction() {
let t0 = 59000.0;
let elements = circular_elements(t0);
let (obs_dataset, cache) = make_dataset_and_cache(t0, 365.0, 6);
let observations: Vec<Observation> = (0..6)
.map(|i| obs_dataset.get_observation(i).unwrap().clone())
.collect();
let obs_fit_data: Vec<ObsFitData> = observations
.iter()
.map(|obs| ObsFitData {
sigma_ra: obs.equ_coord().ra_error,
sigma_dec: obs.equ_coord().dec_error,
bias_ra: 0.0,
bias_dec: 0.0,
residual_ra: 0.0,
residual_dec: 0.0,
selection: ObsSelection::Rejected, chi: 0.0,
})
.collect();
let result = single_iteration(
&observations,
&obs_fit_data,
&elements,
&[true; 6],
&cache,
&JPL_EPHEM_HORIZON,
true,
&PropagatorKind::TwoBody,
)
.unwrap();
assert_abs_diff_eq!(result.correction_norm, 0.0, epsilon = 1e-15);
assert_abs_diff_eq!(result.normalised_rms, 0.0, epsilon = 1e-15);
assert_eq!(result.num_measurements, 0);
assert_abs_diff_eq!(
result.corrected_elements.semi_major_axis,
elements.semi_major_axis,
epsilon = 1e-15
);
}
#[test]
fn test_matonly_does_not_change_elements() {
let t0 = 59000.0;
let elements = circular_elements(t0);
let (obs_dataset, cache) = make_dataset_and_cache(t0, 365.0, 6);
let observations: Vec<Observation> = (0..6)
.map(|i| obs_dataset.get_observation(i).unwrap().clone())
.collect();
let obs_fit_data: Vec<ObsFitData> = observations
.iter()
.map(|obs| ObsFitData::new(obs.equ_coord().ra_error, obs.equ_coord().dec_error))
.collect();
let result = single_iteration(
&observations,
&obs_fit_data,
&elements,
&[true; 6],
&cache,
&JPL_EPHEM_HORIZON,
false, &PropagatorKind::TwoBody,
)
.unwrap();
assert_abs_diff_eq!(
result.corrected_elements.semi_major_axis,
elements.semi_major_axis,
epsilon = 1e-15
);
assert_abs_diff_eq!(
result.corrected_elements.mean_longitude,
elements.mean_longitude,
epsilon = 1e-15
);
}
#[test]
fn test_updated_weights_length_matches_input() {
let t0 = 59000.0;
let elements = circular_elements(t0);
let (obs_dataset, cache) = make_dataset_and_cache(t0, 365.0, 4);
let observations: Vec<Observation> = (0..4)
.map(|i| obs_dataset.get_observation(i).unwrap().clone())
.collect();
let obs_fit_data: Vec<ObsFitData> = observations
.iter()
.map(|obs| ObsFitData::new(obs.equ_coord().ra_error, obs.equ_coord().dec_error))
.collect();
let result = single_iteration(
&observations,
&obs_fit_data,
&elements,
&[true; 6],
&cache,
&JPL_EPHEM_HORIZON,
true,
&PropagatorKind::TwoBody,
)
.unwrap();
assert_eq!(result.updated_obs_fit_data.len(), observations.len());
}
#[test]
fn test_fixed_element_not_corrected() {
let t0 = 59000.0;
let elements = circular_elements(t0);
let (obs_dataset, cache) = make_dataset_and_cache(t0, 365.0, 6);
let observations: Vec<Observation> = (0..6)
.map(|i| obs_dataset.get_observation(i).unwrap().clone())
.collect();
let obs_fit_data: Vec<ObsFitData> = observations
.iter()
.map(|obs| ObsFitData::new(obs.equ_coord().ra_error, obs.equ_coord().dec_error))
.collect();
let mut free = [true; 6];
free[0] = false;
let result = single_iteration(
&observations,
&obs_fit_data,
&elements,
&free,
&cache,
&JPL_EPHEM_HORIZON,
true,
&PropagatorKind::TwoBody,
)
.unwrap();
assert_abs_diff_eq!(
result.corrected_elements.semi_major_axis,
elements.semi_major_axis,
epsilon = 1e-15,
);
}
#[test]
fn test_inactive_selection_flag_preserved() {
let t0 = 59000.0;
let elements = circular_elements(t0);
let (obs_dataset, cache) = make_dataset_and_cache(t0, 365.0, 6);
let observations: Vec<Observation> = (0..6)
.map(|i| obs_dataset.get_observation(i).unwrap().clone())
.collect();
let mut obs_fit_data: Vec<ObsFitData> = observations
.iter()
.map(|obs| ObsFitData::new(obs.equ_coord().ra_error, obs.equ_coord().dec_error))
.collect();
obs_fit_data[1].selection = ObsSelection::Rejected;
obs_fit_data[3].selection = ObsSelection::ForcedOut;
let result = single_iteration(
&observations,
&obs_fit_data,
&elements,
&[true; 6],
&cache,
&JPL_EPHEM_HORIZON,
true,
&PropagatorKind::TwoBody,
)
.unwrap();
assert_eq!(
result.updated_obs_fit_data[1].selection,
ObsSelection::Rejected
);
assert_eq!(
result.updated_obs_fit_data[3].selection,
ObsSelection::ForcedOut
);
}
#[test]
fn test_active_observations_residuals_are_finite() {
let t0 = 59000.0;
let elements = circular_elements(t0);
let (obs_dataset, cache) = make_dataset_and_cache(t0, 365.0, 6);
let observations: Vec<Observation> = (0..6)
.map(|i| obs_dataset.get_observation(i).unwrap().clone())
.collect();
let obs_fit_data: Vec<ObsFitData> = observations
.iter()
.map(|obs| ObsFitData::new(obs.equ_coord().ra_error, obs.equ_coord().dec_error))
.collect();
let result = single_iteration(
&observations,
&obs_fit_data,
&elements,
&[true; 6],
&cache,
&JPL_EPHEM_HORIZON,
true,
&PropagatorKind::TwoBody,
)
.unwrap();
for fit in &result.updated_obs_fit_data {
assert!(fit.residual_ra.is_finite());
assert!(fit.residual_dec.is_finite());
assert!(fit.chi.is_finite());
}
}
}