use crate::assist_data::AssistData;
use crate::coordinates::{
bary_to_helio, cartesian_to_spherical, ecliptic_to_equatorial, equatorial_to_ecliptic,
helio_to_bary,
};
use libassist_sys::ffi;
use libassist_sys::{AssistSim, Ephemeris, IntegratorConfig, Simulation};
use crate::observatory::ObservatoryTable;
use crate::orbit::Orbit;
use crate::origin::Origin;
use crate::state::resolve_origin_state;
use crate::{Error, Result};
#[derive(Debug, Clone)]
pub struct EphemerisResult {
pub spherical: [f64; 6],
pub aberrated_state: [f64; 6],
pub light_time: f64,
pub epoch: f64,
}
#[derive(Debug, Clone)]
pub struct Observer {
pub origin: Origin,
pub epoch: f64,
}
impl Observer {
pub fn new(origin: Origin, epoch: f64) -> Self {
Self { origin, epoch }
}
}
pub fn assist_generate_ephemeris_single(
data: &AssistData,
orbit: &Orbit,
observers: &[Observer],
num_threads: Option<usize>,
integrator: &IntegratorConfig,
) -> Result<Vec<EphemerisResult>> {
if observers.is_empty() {
return Ok(vec![]);
}
let ephem = &data.ephem;
let obs_table = data.observatory.as_ref();
let c = ephem.c_au_per_day();
let mut indexed: Vec<(usize, &Observer)> = observers.iter().enumerate().collect();
indexed.sort_by(|a, b| {
a.1.epoch
.partial_cmp(&b.1.epoch)
.unwrap_or(std::cmp::Ordering::Equal)
});
let n_workers = match num_threads {
Some(0) => {
return Err(Error::Other(
"num_threads = Some(0) is not valid; use None for the default pool".into(),
));
}
Some(n) => n,
None => {
#[cfg(feature = "parallel")]
{
rayon::current_num_threads().max(1)
}
#[cfg(not(feature = "parallel"))]
{
1
}
}
};
let n_workers = n_workers.min(indexed.len()).max(1);
let chunk_size = indexed.len().div_ceil(n_workers);
let chunks: Vec<&[(usize, &Observer)]> = indexed.chunks(chunk_size).collect();
let process_chunk = |chunk: &&[(usize, &Observer)]| -> Result<Vec<(usize, EphemerisResult)>> {
let mut sim = EphemerisSim::new(ephem, orbit, integrator)?;
let mut out = Vec::with_capacity(chunk.len());
for &(orig_idx, obs) in chunk.iter() {
let r = compute_single_ephemeris(ephem, &mut sim, obs, c, obs_table)?;
out.push((orig_idx, r));
}
Ok(out)
};
let chunk_results: Vec<Vec<(usize, EphemerisResult)>> =
crate::propagate::map_with_threads(&chunks, num_threads, process_chunk)?;
let mut out: Vec<Option<EphemerisResult>> = (0..observers.len()).map(|_| None).collect();
for chunk in chunk_results {
for (idx, r) in chunk {
out[idx] = Some(r);
}
}
out.into_iter()
.map(|opt| opt.ok_or_else(|| Error::Other("missing ephemeris result for observer".into())))
.collect()
}
pub fn assist_generate_ephemeris(
data: &AssistData,
orbits: &[Orbit],
observers: &[Observer],
num_threads: Option<usize>,
integrator: &IntegratorConfig,
) -> Result<Vec<Vec<EphemerisResult>>> {
if orbits.is_empty() {
return Ok(vec![]);
}
let op = |orbit: &Orbit| -> Result<Vec<EphemerisResult>> {
assist_generate_ephemeris_single(data, orbit, observers, Some(1), integrator)
};
crate::propagate::map_with_threads(orbits, num_threads, op)
}
fn compute_single_ephemeris(
ephem: &Ephemeris,
sim: &mut EphemerisSim,
obs: &Observer,
c: f64,
obs_table: Option<&ObservatoryTable>,
) -> Result<EphemerisResult> {
let t_obs = obs.epoch;
let obs_state = resolve_origin_state(ephem, &obs.origin, t_obs, obs_table)?;
let helio_ecl_obs = sim.integrate_to(t_obs)?;
let dx = [
helio_ecl_obs[0] - obs_state[0],
helio_ecl_obs[1] - obs_state[1],
helio_ecl_obs[2] - obs_state[2],
];
let dist = (dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]).sqrt();
let mut tau = dist / c;
const MAX_ITER: usize = 10;
const TOL: f64 = 1e-13;
let mut aberrated_state = helio_ecl_obs;
let mut converged = false;
for _ in 0..MAX_ITER {
let t_emit = t_obs - tau;
aberrated_state = sim.integrate_to(t_emit)?;
let dx_new = [
aberrated_state[0] - obs_state[0],
aberrated_state[1] - obs_state[1],
aberrated_state[2] - obs_state[2],
];
let dist_new =
(dx_new[0] * dx_new[0] + dx_new[1] * dx_new[1] + dx_new[2] * dx_new[2]).sqrt();
let tau_new = dist_new / c;
if (tau_new - tau).abs() < TOL {
tau = tau_new;
converged = true;
break;
}
tau = tau_new;
}
if !converged {
return Err(Error::LightTimeConvergence(MAX_ITER));
}
let t_emit = t_obs - tau;
let t_obs_a = ephem.mjd_to_assist_time(t_obs);
let t_emit_a = ephem.mjd_to_assist_time(t_emit);
let sun_obs = ephem.get_body_state_array(ffi::ASSIST_BODY_SUN, t_obs_a)?;
let sun_emit = ephem.get_body_state_array(ffi::ASSIST_BODY_SUN, t_emit_a)?;
let obs_bary_eq = helio_to_bary(&ecliptic_to_equatorial(&obs_state), &sun_obs);
let ast_bary_eq = helio_to_bary(&ecliptic_to_equatorial(&aberrated_state), &sun_emit);
let topo_eq = bary_to_helio(&ast_bary_eq, &obs_bary_eq);
let dx_eq = [topo_eq[0], topo_eq[1], topo_eq[2]];
let dv_eq = [topo_eq[3], topo_eq[4], topo_eq[5]];
let spherical = cartesian_to_spherical(dx_eq, dv_eq);
Ok(EphemerisResult {
spherical,
aberrated_state,
light_time: tau,
epoch: t_obs,
})
}
struct EphemerisSim<'a> {
asim: AssistSim,
ephem: &'a Ephemeris,
}
impl<'a> EphemerisSim<'a> {
fn new(ephem: &'a Ephemeris, orbit: &Orbit, integrator: &IntegratorConfig) -> Result<Self> {
use crate::propagate::{
add_particles_and_variationals, apply_nongrav_scalars, configure_forces,
install_particle_params,
};
let t0 = ephem.mjd_to_assist_time(orbit.epoch);
let sun = ephem.get_body_state_array(ffi::ASSIST_BODY_SUN, t0)?;
let bary_state = helio_to_bary(&ecliptic_to_equatorial(&orbit.state), &sun);
let mut sim = Simulation::new()?;
sim.set_t(t0);
integrator.apply(&mut sim);
let mut asim = AssistSim::new(sim, ephem)?;
let has_nongrav = orbit.non_grav.is_some();
configure_forces(&mut asim, has_nongrav);
add_particles_and_variationals(&mut asim, &bary_state, 0);
if let Some(ng) = orbit.non_grav.as_ref() {
apply_nongrav_scalars(&mut asim, ng);
install_particle_params(&mut asim, ng, false);
}
Ok(Self { asim, ephem })
}
fn integrate_to(&mut self, mjd_tdb: f64) -> Result<[f64; 6]> {
let t_target = self.ephem.mjd_to_assist_time(mjd_tdb);
self.asim.integrate_or_interpolate(t_target)?;
let particles = self.asim.sim().particles();
if particles.is_empty() {
return Err(Error::Other("No particles after integration".into()));
}
let p = &particles[0];
let bary_eq = [p.x, p.y, p.z, p.vx, p.vy, p.vz];
let sun_t = self
.ephem
.get_body_state_array(ffi::ASSIST_BODY_SUN, t_target)?;
Ok(equatorial_to_ecliptic(&bary_to_helio(&bary_eq, &sun_t)))
}
}