earths 0.0.4

High-fidelity Earth simulation engine — orbit, atmosphere, geology, hydrology, biosphere, terrain, lighting, rendering, satellites, and temporal systems with full scientific coupling
Documentation
use earths::lighting::day_night::*;
use earths::lighting::seasons::*;
use earths::lighting::solar_position::*;

#[test]
fn axialtiltcorrect() {
    assert!((EARTHAXIALTILTDEG - 23.4393).abs() < 0.001);
}

#[test]
fn solarpositionnoonequatorequinox() {
    let jd = 2451545.0;
    let sun = SolarPosition::compute(jd, 0.0, 0.0);
    assert!(
        sun.distanceau > 0.98 && sun.distanceau < 1.02,
        "Distance should be ~1 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 > 1.4e11 && dm < 1.6e11, "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.01,
        "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 + 172.0;
    let jdwinter = 2451545.0 + 355.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 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!(
            pt[0] >= -90.0 && pt[0] <= 90.0,
            "Lat out of range: {}",
            pt[0]
        );
        assert!(
            pt[1] >= -180.0 && pt[1] <= 180.0,
            "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.3, "Daytime ambient should be bright: {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 axialtiltconsistent() {
    assert!((AXIALTILTDEG - EARTHAXIALTILTDEG).abs() < 1e-10);
}

#[test]
fn tropicalyeardays() {
    assert!((TROPICALYEARDAYS - 365.24219).abs() < 0.001);
}

#[test]
fn seasonatsummersolsticenorthern() {
    let jd = VERNALEQUINOXJD + 92.0;
    let s = seasonat(jd, 45.0);
    assert_eq!(s.seasonnorth, Season::Summer);
    assert_eq!(s.seasonsouth, Season::Winter);
}

#[test]
fn seasonatwintersolsticenorthern() {
    let jd = VERNALEQUINOXJD + 275.0;
    let s = seasonat(jd, 45.0);
    assert_eq!(s.seasonnorth, Season::Winter);
    assert_eq!(s.seasonsouth, Season::Summer);
}

#[test]
fn seasonsolardeclinationsummer() {
    let jd = VERNALEQUINOXJD + 92.0;
    let s = seasonat(jd, 0.0);
    assert!(
        s.solardeclinationdeg > 20.0,
        "Declination should be near max at summer solstice: {}",
        s.solardeclinationdeg
    );
}

#[test]
fn seasonsolardeclinationequinox() {
    let s = seasonat(VERNALEQUINOXJD, 0.0);
    assert!(
        s.solardeclinationdeg.abs() < 2.0,
        "Declination should be near 0 at equinox: {}",
        s.solardeclinationdeg
    );
}

#[test]
fn daylengthequatorequinox() {
    let s = seasonat(VERNALEQUINOXJD, 0.0);
    assert!(
        (s.daylengthhours - 12.0).abs() < 1.0,
        "Equator at equinox should have ~12h day: {}",
        s.daylengthhours
    );
}

#[test]
fn daylengtharcticsummer() {
    let jd = VERNALEQUINOXJD + 92.0;
    let s = seasonat(jd, 70.0);
    assert!(
        s.daylengthhours > 20.0,
        "Arctic summer should have very long days: {}",
        s.daylengthhours
    );
}

#[test]
fn subsolarpointdeclination() {
    let (dec, lon) = subsolarpoint(2451545.0);
    assert!(lon.is_finite());
    assert!(dec.abs() < 25.0, "Declination should be bounded: {dec}");
}

#[test]
fn subsolarpointlongituderange() {
    let (dec2, lon) = subsolarpoint(2451545.0);
    assert!(dec2.is_finite());
    assert!(
        lon.abs() < 360.0,
        "Subsolar longitude should be bounded: {lon}"
    );
}

#[test]
fn seasonenumoppositehemispheres() {
    let jd = VERNALEQUINOXJD + 5.0;
    let s = seasonat(jd, 45.0);
    assert_eq!(s.seasonnorth, Season::Spring);
    assert_eq!(s.seasonsouth, Season::Autumn);
}