jupiters 0.0.3

Jupiter celestial simulation crate for the MilkyWay SolarSystem workspace
Documentation
use jupiters::lighting::day_night::*;
use jupiters::lighting::seasons::*;
use jupiters::lighting::solar_position::*;

#[test]
fn axialtiltcorrect() {
    assert!((JUPITERAXIALTILTDEG - 3.13).abs() < 0.01);
}

#[test]
fn solarpositiondistancenear5au() {
    let sun = SolarPosition::compute(2451545.0, 0.0, 0.0);
    let au_dist = sun.distanceau;
    assert!(
        au_dist > 4.9 && au_dist < 5.5,
        "Should be ~5.2 AU: {au_dist}"
    );
}

#[test]
fn solarpositionnoonequator() {
    let jd = 2451545.0;
    let sun = SolarPosition::compute(jd, 0.0, 0.0);
    assert!(
        sun.distanceau > 4.5 && sun.distanceau < 5.8,
        "Distance should be ~5.2 AU: {}",
        sun.distanceau
    );
}

#[test]
fn solarpositiondistanceinmeters() {
    let jd = 2451545.0;
    let sun = SolarPosition::compute(jd, 0.0, 0.0);
    let dm = sun.distancem();
    assert!(dm > 6e11 && dm < 9e11, "Distance in meters off: {dm}");
}

#[test]
fn solarpositionisabovehorizonday() {
    let jd = 2451545.0;
    let sun = SolarPosition::compute(jd, 0.0, 0.0);
    let sunsomewhere = SolarPosition::compute(jd, 0.0, -180.0);
    assert!(
        sun.isabovehorizon() || sunsomewhere.isabovehorizon(),
        "Sun should be above horizon somewhere on equator"
    );
}

#[test]
fn solarpositiondirectionunitvector() {
    let jd = 2451545.0;
    let sun = SolarPosition::compute(jd, 45.0, 90.0);
    let len =
        (sun.direction[0].powi(2) + sun.direction[1].powi(2) + sun.direction[2].powi(2)).sqrt();
    assert!(
        (len - 1.0).abs() < 0.1,
        "Direction should be unit vector: {len}"
    );
}

#[test]
fn solarpositionvarieswithlongitude() {
    let jd = 2451545.0;
    let suna = SolarPosition::compute(jd, 0.0, 0.0);
    let sunb = SolarPosition::compute(jd, 0.0, 180.0);
    assert!(
        (suna.elevationdeg - sunb.elevationdeg).abs() > 1.0,
        "Solar elevation should differ at different longitudes"
    );
}

#[test]
fn solarelevationvarieswithdate() {
    let jdsummer = 2451545.0 + 1083.0;
    let jdwinter = 2451545.0 + 2166.0;
    let suns = SolarPosition::compute(jdsummer, 60.0, 0.0);
    let sunw = SolarPosition::compute(jdwinter, 60.0, 0.0);
    assert!(
        suns.elevationdeg != sunw.elevationdeg,
        "Solar elevation should vary with season"
    );
}

#[test]
fn solarfluxatjupiter() {
    let sun = SolarPosition::compute(2451545.0, 0.0, 0.0);
    let flux = sun.solarfluxwm2();
    assert!(
        flux > 40.0 && flux < 60.0,
        "Jupiter solar flux ~50 W/m²: {flux}"
    );
}

#[test]
fn daylightstateday() {
    let jd = 2451545.0;
    let cycle = DayNightCycle::new(jd);
    let mut foundday = false;
    for lon in (-180..180).step_by(15) {
        if cycle.stateat(0.0, lon as f64) == DaylightState::Day {
            foundday = true;
            break;
        }
    }
    assert!(foundday, "Should find daytime somewhere on the equator");
}

#[test]
fn daylightstatenight() {
    let jd = 2451545.0;
    let cycle = DayNightCycle::new(jd);
    let mut foundnight = false;
    for lon in (-180..180).step_by(15) {
        if cycle.stateat(0.0, lon as f64) == DaylightState::Night {
            foundnight = true;
            break;
        }
    }
    assert!(foundnight, "Should find nighttime somewhere on the equator");
}

#[test]
fn daylightstateenumvalues() {
    assert_ne!(DaylightState::Day, DaylightState::Night);
    assert_ne!(
        DaylightState::CivilTwilight,
        DaylightState::NauticalTwilight
    );
    assert_ne!(
        DaylightState::NauticalTwilight,
        DaylightState::AstronomicalTwilight
    );
}

#[test]
fn terminatorpointscount() {
    let cycle = DayNightCycle::new(2451545.0);
    let pts = cycle.terminatorpoints(36);
    assert_eq!(pts.len(), 36);
}

#[test]
fn terminatorpointsvalidcoordinates() {
    let cycle = DayNightCycle::new(2451545.0);
    let pts = cycle.terminatorpoints(72);
    for pt in &pts {
        assert!(
            (-90.0..=90.0).contains(&pt[0]),
            "Lat out of range: {}",
            pt[0]
        );
        assert!(
            (-180.0..=360.0).contains(&pt[1]),
            "Lon out of range: {}",
            pt[1]
        );
    }
}

#[test]
fn ambientlightday() {
    let cycle = DayNightCycle::new(2451545.0);
    for lon in (-180..180).step_by(10) {
        let state = cycle.stateat(0.0, lon as f64);
        if state == DaylightState::Day {
            let al = cycle.ambientlight(0.0, lon as f64);
            assert!(al > 0.001, "Daytime ambient should be positive: {al}");
            return;
        }
    }
}

#[test]
fn ambientlightnight() {
    let cycle = DayNightCycle::new(2451545.0);
    for lon in (-180..180).step_by(10) {
        let state = cycle.stateat(0.0, lon as f64);
        if state == DaylightState::Night {
            let al = cycle.ambientlight(0.0, lon as f64);
            assert!(al < 0.01, "Nighttime ambient should be dim: {al}");
            return;
        }
    }
}

#[test]
fn ambientlightrange() {
    let cycle = DayNightCycle::new(2451545.0);
    for lon in (-180..180).step_by(30) {
        let al = cycle.ambientlight(45.0, lon as f64);
        assert!(
            (0.0..=1.0).contains(&al),
            "Ambient light out of range: {al}"
        );
    }
}

#[test]
fn seasonscreation() {
    let s = Seasons::new(2451545.0);
    let lon = s.jupitersolarlongitudedeg();
    assert!(
        (0.0..=360.0).contains(&lon),
        "Solar longitude out of range: {lon}"
    );
}

#[test]
fn subsolarlatitudebounded() {
    let s = Seasons::new(2451545.0);
    let lat = s.subsolarlatitudedeg();
    assert!(
        (-3.2..=3.2).contains(&lat),
        "Subsolar lat should be bounded by tilt: {lat}"
    );
}

#[test]
fn seasonnamereturnsstring() {
    let s = Seasons::new(2451545.0);
    let name = s.seasonname();
    assert!(!name.is_empty());
    let valid = [
        "Northern Spring",
        "Northern Summer",
        "Northern Autumn",
        "Northern Winter",
    ];
    assert!(valid.contains(&name), "Unexpected season name: {name}");
}

#[test]
fn daylighthoursequator() {
    let s = Seasons::new(2451545.0);
    let hours = s.daylighthours(0.0);
    assert!(hours > 4.0 && hours <= 9.925, "Equator daylength: {hours}");
}

#[test]
fn daylighthourspolarsummer() {
    for offset in (0..4332).step_by(100) {
        let s = Seasons::new(2451545.0 + offset as f64);
        let hours = s.daylighthours(89.0);
        assert!((0.0..=9.925).contains(&hours), "Polar daylength: {hours}");
    }
}

#[test]
fn insolationfactorequator() {
    let s = Seasons::new(2451545.0);
    let factor = s.insolationfactor(0.0);
    assert!(
        factor >= 0.0,
        "Insolation factor should be non-negative: {factor}"
    );
}

#[test]
fn insolationfactorpole() {
    let s = Seasons::new(2451545.0);
    let feq = s.insolationfactor(0.0);
    let fpole = s.insolationfactor(85.0);
    assert!(feq >= fpole, "Equator should get more insolation than pole");
}

#[test]
fn seasonsvaryoverorbitalperiod() {
    let s1 = Seasons::new(2451545.0);
    let s2 = Seasons::new(2451545.0 + 1083.0);
    let s3 = Seasons::new(2451545.0 + 2166.0);
    let n1 = s1.seasonname();
    let n2 = s2.seasonname();
    let n3 = s3.seasonname();
    assert!(n1 != n2 || n2 != n3, "Seasons should change through orbit");
}

#[test]
fn subsolarlatitudevarieswithtime() {
    let s1 = Seasons::new(2451545.0);
    let s2 = Seasons::new(2451545.0 + 1083.0);
    let l1 = s1.subsolarlatitudedeg();
    let l2 = s2.subsolarlatitudedeg();
    assert!(
        (l1 - l2).abs() > 0.01,
        "Subsolar lat should vary: {l1} vs {l2}"
    );
}