type Vec3 = [f64; 3];
pub const MARS_RE: f64 = 3_396_200.0;
pub const MARS_ROTATION_RATE: f64 = 7.088_218e-5;
fn norm(r: Vec3) -> f64 {
(r[0] * r[0] + r[1] * r[1] + r[2] * r[2]).sqrt()
}
pub fn mars_density(altitude_m: f64) -> f64 {
const BANDS: [(f64, f64, f64); 10] = [
(0.0, 2.0000e-2, 11.100),
(25.0, 2.1032e-3, 8.500),
(50.0, 1.1106e-4, 7.500),
(75.0, 3.9619e-6, 8.000),
(100.0, 1.7407e-7, 10.000),
(125.0, 1.4289e-8, 14.000),
(150.0, 2.3959e-9, 20.000),
(200.0, 1.9667e-10, 30.000),
(250.0, 3.7146e-11, 45.000),
(300.0, 1.2228e-11, 60.000),
];
let h_km = (altitude_m / 1000.0).max(0.0);
let mut i = 0;
while i + 1 < BANDS.len() && BANDS[i + 1].0 <= h_km {
i += 1;
}
let (h0, rho0, scale) = BANDS[i];
rho0 * (-(h_km - h0) / scale).exp()
}
pub fn mars_drag_accel(r: Vec3, v: Vec3, cd_area_over_mass: f64) -> Vec3 {
let rho = mars_density(norm(r) - MARS_RE);
let w = MARS_ROTATION_RATE;
let v_rel = [v[0] + w * r[1], v[1] - w * r[0], v[2]];
let coef = -0.5 * rho * cd_area_over_mass * norm(v_rel);
[coef * v_rel[0], coef * v_rel[1], coef * v_rel[2]]
}
pub fn mars_drag_accel_scaled(r: Vec3, v: Vec3, cd_area_over_mass: f64, scale: f64) -> Vec3 {
let a = mars_drag_accel(r, v, cd_area_over_mass);
[scale * a[0], scale * a[1], scale * a[2]]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mars_density_decreases_with_altitude() {
let rho0 = mars_density(0.0);
assert!(
(rho0 - 0.020).abs() < 1e-4,
"surface density {rho0}, expected ~0.020 kg/m³"
);
assert_eq!(mars_density(-5_000.0), rho0, "clamps below the surface");
let ratio = mars_density(0.0) / mars_density(11_100.0);
assert!(
(ratio - std::f64::consts::E).abs() < 0.02,
"near-surface e-fold over ~11 km: ρ(0)/ρ(11.1 km) = {ratio}, expected ≈ e"
);
let alts = [
0.0, 25e3, 50e3, 75e3, 100e3, 150e3, 200e3, 250e3, 300e3, 400e3,
];
for w in alts.windows(2) {
let (lo, hi) = (mars_density(w[0]), mars_density(w[1]));
assert!(
hi < lo,
"density must decrease: {hi} at {} km not < {lo} at {} km",
w[1] / 1e3,
w[0] / 1e3
);
}
let below = mars_density(100e3 - 1.0);
let at = mars_density(100e3);
assert!(
(below - at).abs() / at < 1e-3,
"density jumps at the 100 km band boundary: {below} vs {at}"
);
}
#[test]
fn mars_density_local_scale_height_is_physical() {
let (h1, h2) = (5e3, 15e3);
let ratio = mars_density(h2) / mars_density(h1);
let scale_km = -(h2 - h1) / 1000.0 / ratio.ln();
assert!(
(10.0..=12.0).contains(&scale_km),
"recovered near-surface scale height {scale_km} km outside ~11 km Mars band"
);
}
#[test]
fn mars_drag_opposes_relative_velocity() {
let alt = 150e3;
let r = [MARS_RE + alt, 0.0, 0.0];
let mu_mars = 4.282_837e13; let vcirc = (mu_mars / (MARS_RE + alt)).sqrt(); let v = [0.0, vcirc, 0.0];
let a = mars_drag_accel(r, v, 0.02);
let v_rel = [0.0, vcirc - MARS_ROTATION_RATE * (MARS_RE + alt), 0.0];
let dot_av = a[0] * v_rel[0] + a[1] * v_rel[1] + a[2] * v_rel[2];
assert!(
dot_av < 0.0,
"drag must oppose the relative velocity: {dot_av}"
);
assert!(
a[0] == 0.0 && a[2] == 0.0,
"drag should be purely along −v_rel for this in-plane state: {a:?}"
);
assert!(
a[1] < 0.0,
"drag along-track component must be retrograde: {}",
a[1]
);
let mag = norm(a);
assert!(
(1e-5..=1e-3).contains(&mag),
"150 km Mars drag magnitude {mag} m/s² outside the expected ~3e-4 band"
);
}
#[test]
fn mars_drag_is_strictly_antiparallel_to_relative_velocity() {
let r = [MARS_RE + 180e3, 2.0e5, -1.0e5];
let v = [-300.0, 3300.0, 150.0];
let a = mars_drag_accel(r, v, 0.015);
let w = MARS_ROTATION_RATE;
let v_rel = [v[0] + w * r[1], v[1] - w * r[0], v[2]];
let cross = [
a[1] * v_rel[2] - a[2] * v_rel[1],
a[2] * v_rel[0] - a[0] * v_rel[2],
a[0] * v_rel[1] - a[1] * v_rel[0],
];
let scale = norm(a) * norm(v_rel);
assert!(
norm(cross) / scale < 1e-12,
"drag not anti-parallel to v_rel: cross = {cross:?}"
);
assert!(
a[0] * v_rel[0] + a[1] * v_rel[1] + a[2] * v_rel[2] < 0.0,
"drag must oppose v_rel"
);
}
#[test]
fn mars_drag_scales() {
let r = [MARS_RE + 160e3, 0.0, 0.0];
let v = [0.0, 3400.0, 0.0];
let a1 = mars_drag_accel(r, v, 0.02);
let a2 = mars_drag_accel(r, v, 0.04);
let ratio = norm(a2) / norm(a1);
assert!(
(ratio - 2.0).abs() < 1e-12,
"doubling C_D·A/m must double drag: ratio {ratio}"
);
let nominal = mars_drag_accel(r, v, 0.02);
let unit = mars_drag_accel_scaled(r, v, 0.02, 1.0);
assert_eq!(unit, nominal, "scale = 1 must equal the nominal drag");
let doubled = mars_drag_accel_scaled(r, v, 0.02, 2.0);
for k in 0..3 {
assert!(
(doubled[k] - 2.0 * nominal[k]).abs() <= 1e-18 + 1e-12 * nominal[k].abs(),
"scale = 2 must double component {k}"
);
}
}
#[test]
fn mars_drag_vanishes_above_the_atmosphere_density_floor() {
let low = norm(mars_drag_accel(
[MARS_RE + 150e3, 0.0, 0.0],
[0.0, 3475.0, 0.0],
0.02,
));
let high = norm(mars_drag_accel(
[MARS_RE + 1000e3, 0.0, 0.0],
[0.0, 3000.0, 0.0],
0.02,
));
assert!(
high < low * 1e-4,
"drag at 1000 km ({high}) should be far below 150 km ({low})"
);
}
}