dwarfplanetsfactory 0.0.2

Dwarf planet factory — classify, build and catalogue dwarf planets of any type: Kuiper belt, scattered disk, plutino, cold classical, detached, binary, Ceres-type, and sednoid.
Documentation
use dwarfplanetsfactory::engine::evolution;
use dwarfplanetsfactory::engine::orbits;
use dwarfplanetsfactory::engine::resonance;

// ── Orbits ──────────────────────────────────────────────────────────
#[test]
fn mean_motion_positive() {
    let n = orbits::mean_motion(39.5);
    assert!(n > 0.0);
}

#[test]
fn period_years_pluto() {
    let p = orbits::period_years(39.5);
    assert!((p - 248.0).abs() < 5.0); // Pluto ~ 248 years
}

#[test]
fn tisserand_neptune_pluto() {
    let tj = orbits::tisserand_neptune(39.5, 0.25, 17.0);
    assert!(tj > 0.0);
}

#[test]
fn tisserand_jupiter_ceres() {
    let tj = orbits::tisserand_jupiter(2.77, 0.076, 10.6);
    assert!(tj > 3.0); // Ceres is main belt
}

#[test]
fn kepler_solve_circular() {
    let ecc_anom = orbits::kepler_solve(1.0, 0.0);
    assert!((ecc_anom - 1.0).abs() < 1.0e-10);
}

#[test]
fn kepler_solve_eccentric() {
    let ecc_anom = orbits::kepler_solve(1.0, 0.5);
    let check = ecc_anom - 0.5 * ecc_anom.sin();
    assert!((check - 1.0).abs() < 1.0e-10);
}

#[test]
fn true_anomaly_at_perihelion() {
    let nu = orbits::true_anomaly(0.0, 0.25);
    assert!(nu.abs() < 1.0e-10);
}

#[test]
fn heliocentric_distance_at_perihelion() {
    let r = orbits::heliocentric_distance(39.5, 0.25, 0.0);
    let q = 39.5 * (1.0 - 0.25);
    assert!((r - q).abs() < 1.0e-6);
}

#[test]
fn synodic_period_different_from_orbital() {
    let syn = orbits::synodic_period_neptune(39.5);
    assert!(syn > 0.0);
    assert!(syn.is_finite());
}

#[test]
fn kozai_timescale_positive() {
    let tk = orbits::kozai_timescale(2.0e4, 39.5, 1.303e22);
    assert!(tk > 0.0);
}

// ── Evolution ───────────────────────────────────────────────────────
#[test]
fn collisional_lifetime_finite() {
    let lt = evolution::collisional_lifetime_years(5.0e5, 43.0);
    assert!(lt > 0.0);
    assert!(lt.is_finite());
}

#[test]
fn weathering_timescale_scaling() {
    let t1 = evolution::weathering_timescale_years(40.0);
    let t2 = evolution::weathering_timescale_years(100.0);
    assert!(t2 > t1); // farther → longer
}

#[test]
fn sublimation_mass_loss_nonneg() {
    let ml = evolution::sublimation_mass_loss(5.0e5, 0.1, 40.0, 0.3);
    assert!(ml >= 0.0);
}

#[test]
fn volatile_depletion_time_positive() {
    let vdt = evolution::volatile_depletion_time_years(5.0e5, 1800.0, 0.1, 40.0, 0.3);
    assert!(vdt > 0.0);
}

#[test]
fn surface_refreshed_bounded() {
    let frac = evolution::surface_refreshed_fraction(4.5e9, 43.0);
    assert!((0.0..=1.0).contains(&frac));
}

// ── Resonance ───────────────────────────────────────────────────────
#[test]
fn resonance_2_3_near_plutino() {
    let a_res = resonance::resonance_semi_major_axis(2, 3);
    assert!((a_res - 39.4).abs() < 1.0);
}

#[test]
fn is_near_2_3_resonance() {
    assert!(resonance::is_near_resonance(39.4, 2, 3, 1.0));
}

#[test]
fn resonance_width_positive() {
    let w = resonance::resonance_width(2, 3, 0.25);
    assert!(w > 0.0);
}

#[test]
fn libration_period_positive() {
    let lp = resonance::libration_period_years(2, 3, 0.25);
    assert!(lp > 0.0);
    assert!(lp.is_finite());
}

#[test]
fn major_resonances_populated() {
    let res = resonance::major_resonances();
    assert!(res.len() >= 4);
}

#[test]
fn closest_resonance_finds_2_3() {
    let found = resonance::closest_resonance(39.5, 1.0);
    assert!(found.is_some());
    let (p, q, _) = found.unwrap();
    assert!(p == 2 && q == 3);
}