use crate::{GravityControls, GravitySource};
use astrodyn_dynamics::forces::GravityAccelerationTyped;
use astrodyn_dynamics::GravityAcceleration;
use astrodyn_quantities::aliases::{Acceleration, Position, Velocity};
use astrodyn_quantities::frame::{IntegrationFrame, RootInertial};
use astrodyn_quantities::integ_origin::IntegOrigin;
use glam::{DMat3, DVec3};
pub struct ResolvedSource<'a> {
pub source: &'a GravitySource,
pub rotation: Option<&'a DMat3>,
pub position: DVec3,
pub delta_c20: f64,
pub has_delta_coeffs: bool,
}
pub fn accumulate_gravity<'a, S: Copy + std::fmt::Debug>(
position: DVec3,
controls: &GravityControls<S>,
integration_origin: DVec3,
source_lookup: impl Fn(S) -> Option<ResolvedSource<'a>>,
) -> GravityAcceleration {
let mut total = GravityAcceleration::default();
for ctrl in &controls.controls {
let resolved = source_lookup(ctrl.source_name).unwrap_or_else(|| {
panic!(
"GravityControl references source {:?} which does not exist. \
JEOD logs a non-fatal error and skips; we panic to prevent \
silently wrong physics.",
ctrl.source_name
);
});
if ctrl.requires_planet_fixed_rotation(resolved.source) && resolved.rotation.is_none() {
panic!(
"Non-spherical GravityControl (degree={}, order={}) references \
source {:?} which has no planet-fixed rotation matrix.",
ctrl.degree, ctrl.order, ctrl.source_name
);
}
let pos_relative_to_source = position - resolved.position;
let result = ctrl.evaluate(
resolved.source,
pos_relative_to_source,
resolved.rotation,
resolved.delta_c20,
resolved.has_delta_coeffs,
);
if ctrl.differential {
let frame_pos_relative_to_source = integration_origin - resolved.position;
assert!(
frame_pos_relative_to_source.length_squared() > 0.0,
"Differential (third-body) gravity source {:?} is at the integration \
frame origin. Third-body sources must be distinct from the central \
body (e.g., Sun/Moon when integrating in an Earth-centered frame).",
ctrl.source_name
);
if ctrl.battin_method && !ctrl.is_nonspherical() && !ctrl.perturbing_only {
let mu = resolved.source.mu;
let integ_pos = position - integration_origin;
let grav_pos = frame_pos_relative_to_source;
let rho_sq = grav_pos.length_squared();
let rho_mag = rho_sq.sqrt();
let rho_3rd = rho_sq * rho_mag;
let r_dot_rho = integ_pos.dot(grav_pos);
let q = (integ_pos.length_squared() + 2.0 * r_dot_rho) / rho_sq;
if q > -0.38 {
let fq = q * (3.0 + q * (3.0 + q)) / (1.0 + (1.0 + q).sqrt());
let scale = -mu / (rho_3rd * (1.0 + fq));
let accel = (integ_pos - grav_pos * fq) * scale;
total.grav_accel += accel;
} else {
let frame_accel = ctrl.evaluate_accel_only(
resolved.source,
frame_pos_relative_to_source,
resolved.rotation,
resolved.delta_c20,
resolved.has_delta_coeffs,
);
total.grav_accel += result.grav_accel - frame_accel.grav_accel;
}
} else {
let frame_accel = ctrl.evaluate_accel_only(
resolved.source,
frame_pos_relative_to_source,
resolved.rotation,
resolved.delta_c20,
resolved.has_delta_coeffs,
);
total.grav_accel += result.grav_accel - frame_accel.grav_accel;
}
} else {
total.grav_accel += result.grav_accel;
}
if ctrl.gradient {
total.grav_grad += result.grav_grad;
}
total.grav_pot += result.grav_pot;
}
total
}
pub struct ResolvedRelativisticSource {
pub mu: f64,
pub position: DVec3,
pub velocity: DVec3,
}
pub fn accumulate_relativistic_corrections<S: Copy + std::fmt::Debug + PartialEq>(
body_position: DVec3,
body_velocity: DVec3,
controls: &GravityControls<S>,
source_lookup: impl Fn(S) -> Option<ResolvedRelativisticSource>,
) -> DVec3 {
let mut total_correction = DVec3::ZERO;
for ctrl in &controls.controls {
if !ctrl.relativistic {
continue;
}
let Some(src) = source_lookup(ctrl.source_name) else {
continue;
};
let other: Vec<crate::relativistic::RelativisticSource> = controls
.controls
.iter()
.filter(|c| c.source_name != ctrl.source_name)
.filter_map(|c| {
source_lookup(c.source_name).map(|s| crate::relativistic::RelativisticSource {
mu: s.mu,
position: s.position,
})
})
.collect();
total_correction += crate::relativistic::compute_relativistic_correction(
src.mu,
src.position,
body_position,
body_velocity,
src.velocity,
&other,
);
}
total_correction
}
pub fn accumulate_gravity_typed<'a, S: Copy + std::fmt::Debug>(
position: Position<RootInertial>,
controls: &GravityControls<S>,
integration_origin: Position<RootInertial>,
source_lookup: impl Fn(S) -> Option<ResolvedSource<'a>>,
) -> GravityAccelerationTyped<RootInertial> {
let raw = accumulate_gravity(
position.raw_si(),
controls,
integration_origin.raw_si(),
source_lookup,
);
GravityAccelerationTyped::<RootInertial>::from_untyped_unchecked(&raw)
}
pub fn accumulate_relativistic_corrections_typed<S: Copy + std::fmt::Debug + PartialEq>(
body_position: Position<RootInertial>,
body_velocity: Velocity<RootInertial>,
controls: &GravityControls<S>,
source_lookup: impl Fn(S) -> Option<ResolvedRelativisticSource>,
) -> Acceleration<RootInertial> {
let raw = accumulate_relativistic_corrections(
body_position.raw_si(),
body_velocity.raw_si(),
controls,
source_lookup,
);
Acceleration::<RootInertial>::from_raw_si(raw)
}
pub fn evaluate_body_gravity_typed<'a, S: Copy + std::fmt::Debug + PartialEq>(
body_position: Position<IntegrationFrame>,
body_velocity: Velocity<IntegrationFrame>,
integ_origin: IntegOrigin,
controls: &GravityControls<S>,
resolve_source: impl Fn(S) -> Option<ResolvedSource<'a>>,
resolve_rel_source: impl Fn(S) -> Option<ResolvedRelativisticSource>,
) -> GravityAccelerationTyped<RootInertial> {
let abs_pos = integ_origin.shift_position(body_position);
let abs_vel = integ_origin.shift_velocity(body_velocity);
let mut accel =
accumulate_gravity_typed(abs_pos, controls, integ_origin.position, resolve_source);
let rel =
accumulate_relativistic_corrections_typed(abs_pos, abs_vel, controls, resolve_rel_source);
accel.grav_accel += rel;
accel
}
pub struct GravityBodyInputs<'a, S> {
pub position: Position<IntegrationFrame>,
pub velocity: Velocity<IntegrationFrame>,
pub integ_origin: IntegOrigin,
pub controls: &'a GravityControls<S>,
}
pub fn run_gravity_stage<'a, S, K, Store, BodyIter, FS, FR>(
bodies: BodyIter,
resolve_source: FS,
resolve_rel_source: FR,
) where
S: Copy + std::fmt::Debug + PartialEq + 'a,
K: Copy,
Store: FnOnce(GravityAccelerationTyped<RootInertial>),
BodyIter: IntoIterator<Item = (K, GravityBodyInputs<'a, S>, Store)>,
FS: Fn(K, S) -> Option<ResolvedSource<'a>>,
FR: Fn(K, S) -> Option<ResolvedRelativisticSource>,
{
for (key, inputs, store) in bodies {
let result = evaluate_body_gravity_typed(
inputs.position,
inputs.velocity,
inputs.integ_origin,
inputs.controls,
|s| resolve_source(key, s),
|s| resolve_rel_source(key, s),
);
store(result);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gravity_source::{GravityModel, GravitySource};
use crate::{GravityControl, GravityControls, GravityGradient};
fn point_mass_source(mu: f64) -> GravitySource {
GravitySource {
mu,
model: GravityModel::PointMass,
}
}
fn third_body_control(source_id: usize, battin: bool) -> GravityControl<usize> {
let mut ctrl = GravityControl::new_third_body(source_id);
ctrl.battin_method = battin;
ctrl
}
#[test]
fn battin_vs_direct_agree_for_leo() {
let mu = 1.0e15;
let source_pos = DVec3::new(1.0e7, 0.0, 0.0); let source = point_mass_source(mu);
let vehicle_pos = DVec3::new(6_000_000.0, 3_000_000.0, 1_000_000.0);
let integration_origin = DVec3::ZERO;
let controls_direct = GravityControls {
controls: vec![third_body_control(0, false)],
};
let result_direct =
accumulate_gravity(vehicle_pos, &controls_direct, integration_origin, |_| {
Some(ResolvedSource {
source: &source,
rotation: None,
position: source_pos,
delta_c20: 0.0,
has_delta_coeffs: false,
})
});
let controls_battin = GravityControls {
controls: vec![third_body_control(0, true)],
};
let result_battin =
accumulate_gravity(vehicle_pos, &controls_battin, integration_origin, |_| {
Some(ResolvedSource {
source: &source,
rotation: None,
position: source_pos,
delta_c20: 0.0,
has_delta_coeffs: false,
})
});
assert!(result_direct.grav_accel.length() > 0.0);
assert!(result_battin.grav_accel.length() > 0.0);
for (i, axis) in ["x", "y", "z"].iter().enumerate() {
let d = result_direct.grav_accel[i];
let b = result_battin.grav_accel[i];
let rel_err = (b - d).abs() / d.abs().max(1e-30);
assert!(
rel_err < 1e-12,
"Case 1: Battin vs direct {axis}-component relative error too large: \
{rel_err:.3e} (direct={d:.6e}, battin={b:.6e})"
);
}
let mu_sun = 1.327_124_400_18e20;
let sun_pos = DVec3::new(1.496e11, 0.0, 0.0);
let sun_source = point_mass_source(mu_sun);
let result_direct_sun =
accumulate_gravity(vehicle_pos, &controls_direct, integration_origin, |_| {
Some(ResolvedSource {
source: &sun_source,
rotation: None,
position: sun_pos,
delta_c20: 0.0,
has_delta_coeffs: false,
})
});
let result_battin_sun =
accumulate_gravity(vehicle_pos, &controls_battin, integration_origin, |_| {
Some(ResolvedSource {
source: &sun_source,
rotation: None,
position: sun_pos,
delta_c20: 0.0,
has_delta_coeffs: false,
})
});
let rel_err = (result_battin_sun.grav_accel - result_direct_sun.grav_accel).length()
/ result_direct_sun.grav_accel.length();
assert!(
rel_err < 1e-4,
"Case 2: Sun geometry relative error too large: {rel_err:.3e}"
);
assert!(
result_battin_sun
.grav_accel
.dot(result_direct_sun.grav_accel)
> 0.0,
"Case 2: Battin and direct should point in the same direction"
);
}
#[test]
fn battin_q_zero_vehicle_at_origin() {
let mu_sun = 1.327_124_400_18e20;
let sun_pos = DVec3::new(1.496e11, 0.0, 0.0);
let source = point_mass_source(mu_sun);
let vehicle_pos = DVec3::ZERO;
let integration_origin = DVec3::ZERO;
let controls = GravityControls {
controls: vec![third_body_control(0, true)],
};
let result = accumulate_gravity(vehicle_pos, &controls, integration_origin, |_| {
Some(ResolvedSource {
source: &source,
rotation: None,
position: sun_pos,
delta_c20: 0.0,
has_delta_coeffs: false,
})
});
assert!(
result.grav_accel.length() < 1e-30,
"Expected zero accel at frame origin, got {:?}",
result.grav_accel
);
}
#[test]
fn battin_fallback_q_below_threshold() {
let mu = 1.0e15;
let source_pos = DVec3::new(1000.0, 0.0, 0.0); let source = point_mass_source(mu);
let integration_origin = DVec3::ZERO;
let vehicle_pos = DVec3::new(500.0, 0.0, 0.0);
let controls_direct = GravityControls {
controls: vec![third_body_control(0, false)],
};
let result_direct =
accumulate_gravity(vehicle_pos, &controls_direct, integration_origin, |_| {
Some(ResolvedSource {
source: &source,
rotation: None,
position: source_pos,
delta_c20: 0.0,
has_delta_coeffs: false,
})
});
let controls_battin = GravityControls {
controls: vec![third_body_control(0, true)],
};
let result_battin =
accumulate_gravity(vehicle_pos, &controls_battin, integration_origin, |_| {
Some(ResolvedSource {
source: &source,
rotation: None,
position: source_pos,
delta_c20: 0.0,
has_delta_coeffs: false,
})
});
let diff = (result_battin.grav_accel - result_direct.grav_accel).length();
assert!(
diff < 1e-30,
"Battin fallback should match direct method exactly, diff = {:.3e}",
diff
);
}
#[test]
fn battin_falls_back_for_perturbing_only() {
let mu = 1.0e15;
let source_pos = DVec3::new(1e8, 0.0, 0.0);
let source = point_mass_source(mu);
let vehicle_pos = DVec3::new(6_778_000.0, 0.0, 0.0);
let integration_origin = DVec3::ZERO;
let mut ctrl: GravityControl<usize> = GravityControl::new_third_body(0_usize);
ctrl.battin_method = true;
ctrl.perturbing_only = true;
let mut ctrl_direct: GravityControl<usize> = GravityControl::new_third_body(0_usize);
ctrl_direct.battin_method = false;
ctrl_direct.perturbing_only = true;
let controls_battin = GravityControls {
controls: vec![ctrl],
};
let controls_direct = GravityControls {
controls: vec![ctrl_direct],
};
let result_battin =
accumulate_gravity(vehicle_pos, &controls_battin, integration_origin, |_| {
Some(ResolvedSource {
source: &source,
rotation: None,
position: source_pos,
delta_c20: 0.0,
has_delta_coeffs: false,
})
});
let result_direct =
accumulate_gravity(vehicle_pos, &controls_direct, integration_origin, |_| {
Some(ResolvedSource {
source: &source,
rotation: None,
position: source_pos,
delta_c20: 0.0,
has_delta_coeffs: false,
})
});
let diff = (result_battin.grav_accel - result_direct.grav_accel).length();
assert!(
diff < 1e-30,
"Battin with perturbing_only should fall back to direct, diff = {diff:.3e}"
);
}
#[test]
fn battin_method_defaults_false() {
let spherical: GravityControl<usize> =
GravityControl::new_spherical(0_usize, GravityGradient::Skip);
assert!(!spherical.battin_method);
let nonspherical: GravityControl<usize> =
GravityControl::new_nonspherical(0_usize, 4, 4, GravityGradient::Skip);
assert!(!nonspherical.battin_method);
let third_body: GravityControl<usize> = GravityControl::new_third_body(0_usize);
assert!(!third_body.battin_method);
}
#[test]
fn evaluate_body_gravity_matches_manual_composition() {
let mu = 3.986e14;
let source = point_mass_source(mu);
let source_pos = DVec3::ZERO;
let body_pos_integ = DVec3::new(7e6, 0.0, 0.0);
let body_vel_integ = DVec3::new(0.0, 7500.0, 0.0);
let integ_origin = IntegOrigin {
position: Position::<RootInertial>::from_raw_si(DVec3::new(1.5e11, 0.0, 0.0)),
velocity: Velocity::<RootInertial>::from_raw_si(DVec3::new(0.0, 30_000.0, 0.0)),
};
let controls = GravityControls {
controls: vec![GravityControl::new_spherical(
0_usize,
GravityGradient::Skip,
)],
};
let resolve_source = |_: usize| {
Some(ResolvedSource {
source: &source,
rotation: None,
position: source_pos,
delta_c20: 0.0,
has_delta_coeffs: false,
})
};
let resolve_rel_source = |_: usize| {
Some(ResolvedRelativisticSource {
mu,
position: source_pos,
velocity: DVec3::ZERO,
})
};
let kernel = evaluate_body_gravity_typed(
Position::<IntegrationFrame>::from_raw_si(body_pos_integ),
Velocity::<IntegrationFrame>::from_raw_si(body_vel_integ),
integ_origin,
&controls,
resolve_source,
resolve_rel_source,
);
let abs_pos = body_pos_integ + integ_origin.position.raw_si();
let abs_vel = body_vel_integ + integ_origin.velocity.raw_si();
let mut manual = accumulate_gravity(
abs_pos,
&controls,
integ_origin.position.raw_si(),
resolve_source,
);
manual.grav_accel +=
accumulate_relativistic_corrections(abs_pos, abs_vel, &controls, resolve_rel_source);
assert_eq!(kernel.grav_accel.raw_si(), manual.grav_accel);
assert_eq!(kernel.grav_pot, manual.grav_pot);
assert_eq!(kernel.grav_grad, manual.grav_grad);
}
#[test]
fn run_gravity_stage_drives_kernel_per_body() {
let mu = 3.986e14;
let source = point_mass_source(mu);
let source_pos = DVec3::ZERO;
let controls = GravityControls {
controls: vec![GravityControl::new_spherical(
0_usize,
GravityGradient::Skip,
)],
};
let body_a_pos = Position::<IntegrationFrame>::from_raw_si(DVec3::new(7e6, 0.0, 0.0));
let body_a_vel = Velocity::<IntegrationFrame>::from_raw_si(DVec3::new(0.0, 7500.0, 0.0));
let body_b_pos = Position::<IntegrationFrame>::from_raw_si(DVec3::new(0.0, 7e6, 1e5));
let body_b_vel = Velocity::<IntegrationFrame>::from_raw_si(DVec3::new(-7500.0, 0.0, 50.0));
let integ_origin_a = IntegOrigin::zero();
let integ_origin_b = IntegOrigin {
position: Position::<RootInertial>::from_raw_si(DVec3::new(1.5e11, 0.0, 0.0)),
velocity: Velocity::<RootInertial>::from_raw_si(DVec3::new(0.0, 30_000.0, 0.0)),
};
let resolve_source = |_body: usize, _src: usize| {
Some(ResolvedSource {
source: &source,
rotation: None,
position: source_pos,
delta_c20: 0.0,
has_delta_coeffs: false,
})
};
let resolve_rel_source = |_body: usize, _src: usize| {
Some(ResolvedRelativisticSource {
mu,
position: source_pos,
velocity: DVec3::ZERO,
})
};
let baseline_a = evaluate_body_gravity_typed(
body_a_pos,
body_a_vel,
integ_origin_a,
&controls,
|s| resolve_source(0_usize, s),
|s| resolve_rel_source(0_usize, s),
);
let baseline_b = evaluate_body_gravity_typed(
body_b_pos,
body_b_vel,
integ_origin_b,
&controls,
|s| resolve_source(1_usize, s),
|s| resolve_rel_source(1_usize, s),
);
let mut out_a = GravityAccelerationTyped::<RootInertial>::default();
let mut out_b = GravityAccelerationTyped::<RootInertial>::default();
let entries = [
(
0_usize,
GravityBodyInputs {
position: body_a_pos,
velocity: body_a_vel,
integ_origin: integ_origin_a,
controls: &controls,
},
Box::new(|r: GravityAccelerationTyped<RootInertial>| out_a = r)
as Box<dyn FnOnce(_)>,
),
(
1_usize,
GravityBodyInputs {
position: body_b_pos,
velocity: body_b_vel,
integ_origin: integ_origin_b,
controls: &controls,
},
Box::new(|r: GravityAccelerationTyped<RootInertial>| out_b = r)
as Box<dyn FnOnce(_)>,
),
];
run_gravity_stage(entries, resolve_source, resolve_rel_source);
assert_eq!(out_a.grav_accel.raw_si(), baseline_a.grav_accel.raw_si());
assert_eq!(out_a.grav_pot, baseline_a.grav_pot);
assert_eq!(out_b.grav_accel.raw_si(), baseline_b.grav_accel.raw_si());
assert_eq!(out_b.grav_pot, baseline_b.grav_pot);
}
#[test]
fn evaluate_body_gravity_zero_origin_matches_plain_accumulate() {
let mu = 3.986e14;
let source = point_mass_source(mu);
let source_pos = DVec3::ZERO;
let body_pos = DVec3::new(7e6, 0.0, 0.0);
let body_vel = DVec3::new(0.0, 7500.0, 0.0);
let integ_origin = IntegOrigin::zero();
let controls = GravityControls {
controls: vec![GravityControl::new_spherical(
0_usize,
GravityGradient::Skip,
)],
};
let resolve_source = |_: usize| {
Some(ResolvedSource {
source: &source,
rotation: None,
position: source_pos,
delta_c20: 0.0,
has_delta_coeffs: false,
})
};
let resolve_rel_source = |_: usize| {
Some(ResolvedRelativisticSource {
mu,
position: source_pos,
velocity: DVec3::ZERO,
})
};
let kernel = evaluate_body_gravity_typed(
Position::<IntegrationFrame>::from_raw_si(body_pos),
Velocity::<IntegrationFrame>::from_raw_si(body_vel),
integ_origin,
&controls,
resolve_source,
resolve_rel_source,
);
let mut plain = accumulate_gravity(body_pos, &controls, DVec3::ZERO, resolve_source);
plain.grav_accel +=
accumulate_relativistic_corrections(body_pos, body_vel, &controls, resolve_rel_source);
assert_eq!(kernel.grav_accel.raw_si(), plain.grav_accel);
assert_eq!(kernel.grav_pot, plain.grav_pot);
assert_eq!(kernel.grav_grad, plain.grav_grad);
}
}