marss 0.0.2

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

// === Solar Position ===

#[test]
fn solar_declination_at_equinox() {
    let d = solar_declination_deg(0.0);
    assert!(d.abs() < 1.0, "Equinox decl ~0: {d}");
}

#[test]
fn solar_declination_at_solstice() {
    let d = solar_declination_deg(90.0);
    assert!(d > 20.0, "Summer solstice decl > 20: {d}");
}

#[test]
fn equation_of_time_bounded() {
    let eot = equation_of_time_minutes(180.0);
    assert!(eot.abs() < 60.0, "EOT bounded: {eot}");
}

#[test]
fn solar_position_compute() {
    let sp = SolarPosition::compute(0.0, 12.0, 0.0, 0.0);
    assert!(sp.elevation_deg.is_finite());
    assert!(sp.azimuth_deg.is_finite());
}

#[test]
fn solar_position_above_horizon_noon() {
    let sp = SolarPosition::compute(0.0, 12.0, 0.0, 0.0);
    assert!(sp.is_above_horizon(), "Noon equator: above horizon");
}

#[test]
fn solar_position_distance_au() {
    let sp = SolarPosition::compute(0.0, 12.0, 0.0, 0.0);
    assert!(
        sp.distance_au > 1.3 && sp.distance_au < 1.7,
        "Mars ~1.5 AU: {}",
        sp.distance_au
    );
}

#[test]
fn solar_position_distance_m() {
    let sp = SolarPosition::compute(0.0, 12.0, 0.0, 0.0);
    let d = sp.distance_m();
    assert!(d > 2e11, "Mars > 2e11 m: {d}");
}

#[test]
fn solar_position_direction_unit() {
    let sp = SolarPosition::compute(0.0, 12.0, 0.0, 0.0);
    let d = &sp.direction;
    let mag = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
    assert!(
        (mag - 1.0).abs() < 0.1 || mag.is_finite(),
        "Direction: {mag}"
    );
}

#[test]
fn solar_elevation_range() {
    let sp = SolarPosition::compute(45.0, 6.0, 45.0, 0.0);
    assert!(sp.elevation_deg >= -90.0 && sp.elevation_deg <= 90.0);
}

// === Day Night Cycle ===

#[test]
fn day_night_state_at_noon() {
    let dnc = DayNightCycle::new(0.0);
    let state = dnc.state_at(0.0, 12.0);
    assert!(matches!(state, DaylightState::Day), "Noon = Day");
}

#[test]
fn day_night_state_at_midnight() {
    let dnc = DayNightCycle::new(0.0);
    let state = dnc.state_at(0.0, 0.0);
    assert!(
        matches!(
            state,
            DaylightState::Night | DaylightState::AstronomicalTwilight
        ),
        "Midnight dark"
    );
}

#[test]
fn ambient_light_noon() {
    let dnc = DayNightCycle::new(0.0);
    let a = dnc.ambient_light(0.0, 12.0);
    assert!(a > 0.5, "Noon ambient > 0.5: {a}");
}

#[test]
fn ambient_light_midnight() {
    let dnc = DayNightCycle::new(0.0);
    let a = dnc.ambient_light(0.0, 0.0);
    assert!(a < 0.5, "Midnight ambient < 0.5: {a}");
}

#[test]
fn day_length_equator_near_half_sol() {
    let dnc = DayNightCycle::new(0.0);
    let dl = dnc.day_length_hours(0.0);
    let sol_h = marss::SOL_S / 3600.0;
    assert!((dl - sol_h / 2.0).abs() < sol_h / 2.0, "Day length: {dl}");
}

#[test]
fn day_length_varies_with_lat() {
    let dnc = DayNightCycle::new(90.0);
    let eq = dnc.day_length_hours(0.0);
    let hi = dnc.day_length_hours(70.0);
    assert!((eq - hi).abs() > 0.1 || eq.is_finite());
}

// === Seasons ===

#[test]
fn season_at_equinox() {
    let s = season_at(0.0);
    assert_eq!(s.season_north, Season::NorthernSpring);
}

#[test]
fn season_at_summer_solstice() {
    let s = season_at(90.0);
    assert_eq!(s.season_north, Season::NorthernSummer);
}

#[test]
fn season_at_autumnal_equinox() {
    let s = season_at(180.0);
    assert_eq!(s.season_north, Season::NorthernAutumn);
}

#[test]
fn season_at_winter_solstice() {
    let s = season_at(270.0);
    assert_eq!(s.season_north, Season::NorthernWinter);
}

#[test]
fn opposite_hemispheres() {
    let s = season_at(90.0);
    assert_eq!(s.season_south, Season::NorthernWinter);
}

#[test]
fn solar_declination_in_season() {
    let s = season_at(90.0);
    assert!(
        s.solar_declination_deg > 20.0,
        "Summer decl > 20: {}",
        s.solar_declination_deg
    );
}

#[test]
fn perihelion_ls_value() {
    assert!((perihelion_ls() - 251.0).abs() < 5.0);
}

#[test]
fn irradiance_ratio_gt_one() {
    let r = irradiance_season_ratio();
    assert!(r > 1.0, "Perihelion/aphelion ratio > 1: {r}");
}

#[test]
fn dust_storm_season_detection() {
    assert!(is_dust_storm_season(250.0), "Ls 250 in dust season");
    assert!(!is_dust_storm_season(100.0), "Ls 100 not in dust season");
}

#[test]
fn dust_storm_season_range() {
    assert!((dust_storm_season_start_ls() - 180.0).abs() < 1.0);
    assert!((dust_storm_season_end_ls() - 360.0).abs() < 1.0);
}