suns 0.0.4

Sun celestial simulation crate for the MilkyWay SolarSystem workspace
Documentation
use suns::plasma::corona::{
    CoronalRegion, active_region_corona, coronal_euv_flux, coronal_hole, coronal_loop_density,
    coronal_loop_temperature, coronal_streamer, coronal_x_ray_luminosity, density_at_height,
    hydrostatic_scale_height, quiet_corona, thompson_scattering_brightness,
    type_iii_burst_frequency,
};
use suns::plasma::prominences::{
    Prominence, coronal_rain_velocity, drain_rate, magnetic_dip_angle,
    prominence_oscillation_period,
};
use suns::plasma::solar_flare::{
    FlareClass, SolarFlare, flare_frequency_per_day, flare_ribbon_speed, goes_class_from_flux,
    magnetic_energy_available,
};
use suns::plasma::solar_wind::{
    SolarWindState, alfven_radius, bow_shock_standoff, corotation_radius,
    heliospheric_magnetic_field_at, parker_critical_radius, parker_solution_speed,
    solar_wind_mass_loss_rate,
};

// ---- Corona ----

#[test]
fn corona_temperatures_above_million_k() {
    for r in [quiet_corona(), active_region_corona()] {
        assert!(r.temperature_k >= 1e6, "{} T too low", r.name);
    }
}

#[test]
fn active_region_hotter_than_quiet() {
    assert!(active_region_corona().temperature_k > quiet_corona().temperature_k);
}

#[test]
fn coronal_hole_less_dense() {
    assert!(coronal_hole().electron_density_m3 < quiet_corona().electron_density_m3);
}

#[test]
fn streamer_denser_than_quiet() {
    assert!(coronal_streamer().electron_density_m3 > quiet_corona().electron_density_m3);
}

#[test]
fn plasma_beta_low_in_corona() {
    let beta = quiet_corona().plasma_beta();
    assert!(beta < 10.0, "coronal β={beta} too high");
}

#[test]
fn alfven_speed_supersonic_in_active_corona() {
    let r = active_region_corona();
    assert!(
        r.alfven_speed() > r.sound_speed(),
        "v_A should > c_s in active corona"
    );
}

#[test]
fn debye_length_positive() {
    assert!(quiet_corona().debye_length() > 0.0);
}

#[test]
fn plasma_frequency_positive() {
    assert!(quiet_corona().plasma_frequency() > 0.0);
}

#[test]
fn density_decreases_with_height() {
    let n0 = 1e14;
    let n1 = density_at_height(n0, 1e8, 1.5e6);
    let n2 = density_at_height(n0, 5e8, 1.5e6);
    assert!(n1 > n2);
    assert!(n2 > 0.0);
}

#[test]
fn hydrostatic_scale_height_proportional_to_temperature() {
    let h1 = hydrostatic_scale_height(1e6);
    let h2 = hydrostatic_scale_height(2e6);
    assert!(h2 > h1);
}

#[test]
fn coronal_xray_luminosity_positive() {
    assert!(coronal_x_ray_luminosity() > 0.0);
}

#[test]
fn coronal_loop_temperature_positive() {
    let t = coronal_loop_temperature(1e8, 1e-4);
    assert!(t > 0.0);
}

#[test]
fn thompson_scattering_decreases_with_distance() {
    let b1 = thompson_scattering_brightness(1e14, 1.5);
    let b2 = thompson_scattering_brightness(1e14, 3.0);
    assert!(b1 > b2);
}

#[test]
fn type_iii_burst_frequency_proportional_to_density_sqrt() {
    let f1 = type_iii_burst_frequency(1e14);
    let f2 = type_iii_burst_frequency(4e14);
    // f ∝ sqrt(n_e), so f2/f1 ≈ 2
    let ratio = f2 / f1;
    assert!((ratio - 2.0).abs() < 0.1, "ratio={ratio}");
}

// ---- Solar Flare ----

#[test]
fn x_class_more_energetic_than_c() {
    let x = SolarFlare::from_class(FlareClass::X, 1.0);
    let c = SolarFlare::from_class(FlareClass::C, 1.0);
    assert!(x.total_energy_joules() > c.total_energy_joules());
}

#[test]
fn flare_peak_temperature_positive() {
    let f = SolarFlare::from_class(FlareClass::M, 5.0);
    assert!(f.peak_temperature() > 0.0);
}

#[test]
fn flare_reconnection_speed_positive() {
    let f = SolarFlare::from_class(FlareClass::X, 1.0);
    assert!(f.reconnection_inflow_speed() > 0.0);
}

#[test]
fn flare_frequency_decreases_with_class() {
    let c_rate = flare_frequency_per_day(FlareClass::C);
    let x_rate = flare_frequency_per_day(FlareClass::X);
    assert!(c_rate > x_rate, "C-class more frequent than X-class");
}

#[test]
fn goes_classification_roundtrip() {
    assert!(matches!(goes_class_from_flux(1e-6), FlareClass::C));
    assert!(matches!(goes_class_from_flux(1e-5), FlareClass::M));
    assert!(matches!(goes_class_from_flux(1e-4), FlareClass::X));
}

#[test]
fn magnetic_energy_available_positive() {
    let e = magnetic_energy_available(0.1, 1e20);
    assert!(e > 0.0);
}

#[test]
fn flare_confinement_time_positive() {
    let f = SolarFlare::from_class(FlareClass::M, 1.0);
    assert!(f.confinement_time() > 0.0);
}

// ---- Prominences ----

#[test]
fn quiescent_prominence_cooler_than_corona() {
    let p = Prominence::quiescent();
    assert!(p.temperature_k < 1e6);
}

#[test]
fn active_prominence_hotter_than_quiescent() {
    let q = Prominence::quiescent();
    let a = Prominence::active();
    assert!(a.temperature_k >= q.temperature_k);
}

#[test]
fn prominence_mass_positive() {
    assert!(Prominence::quiescent().mass_kg() > 0.0);
}

#[test]
fn prominence_magnetic_energy_greater_than_thermal() {
    let p = Prominence::quiescent();
    assert!(
        p.magnetic_energy() > p.thermal_energy(),
        "Prominence magnetically dominated"
    );
}

#[test]
fn kippenhahn_schluter_support_positive() {
    assert!(Prominence::quiescent().kippenhahn_schluter_support() > 0.0);
}

#[test]
fn eruption_speed_positive() {
    assert!(Prominence::active().eruption_speed() > 0.0);
}

#[test]
fn coronal_rain_velocity_increases_with_height() {
    let v1 = coronal_rain_velocity(1e7);
    let v2 = coronal_rain_velocity(5e7);
    assert!(v2 > v1);
}

#[test]
fn prominence_oscillation_period_positive() {
    let p = prominence_oscillation_period(1e8, 0.001, 1e-11);
    assert!(p > 0.0);
}

#[test]
fn magnetic_dip_angle_range() {
    let a = magnetic_dip_angle(0.001, 0.0005);
    assert!(a > 0.0 && a < std::f64::consts::FRAC_PI_2);
}

// ---- Solar Wind ----

#[test]
fn fast_wind_faster_than_slow() {
    let slow = SolarWindState::slow_wind_1au();
    let fast = SolarWindState::fast_wind_1au();
    assert!(fast.speed_ms > slow.speed_ms);
}

#[test]
fn slow_wind_about_400_kms() {
    let v = SolarWindState::slow_wind_1au().speed_ms;
    assert!(v > 300e3 && v < 500e3, "slow wind v={v}");
}

#[test]
fn fast_wind_about_700_kms() {
    let v = SolarWindState::fast_wind_1au().speed_ms;
    assert!(v > 600e3 && v < 900e3, "fast wind v={v}");
}

#[test]
fn density_decreases_with_distance() {
    let w = SolarWindState::slow_wind_1au();
    let w2 = w.at_distance(2.0);
    assert!(w2.density_m3 < w.density_m3);
}

#[test]
fn dynamic_pressure_positive() {
    assert!(SolarWindState::slow_wind_1au().dynamic_pressure() > 0.0);
}

#[test]
fn parker_spiral_angle_at_1au_about_45() {
    let angle = SolarWindState::slow_wind_1au().parker_spiral_angle_deg();
    assert!(angle > 20.0 && angle < 70.0, "spiral angle={angle}°");
}

#[test]
fn alfven_mach_supersonic() {
    assert!(SolarWindState::slow_wind_1au().alfven_mach_number() > 1.0);
}

#[test]
fn sonic_mach_supersonic() {
    assert!(SolarWindState::slow_wind_1au().sonic_mach_number() > 1.0);
}

#[test]
fn parker_critical_radius_few_solar_radii() {
    let r = parker_critical_radius() / suns::SOLAR_RADIUS;
    assert!(r > 1.0 && r < 30.0, "r_c={r} R_sun");
}

#[test]
fn solar_wind_mass_loss_positive() {
    let dm = solar_wind_mass_loss_rate();
    assert!(dm > 0.0);
}

#[test]
fn alfven_radius_outside_sun() {
    assert!(alfven_radius() > suns::SOLAR_RADIUS);
}

#[test]
fn corotation_radius_outside_sun() {
    assert!(corotation_radius() > suns::SOLAR_RADIUS);
}

#[test]
fn heliospheric_field_decreases_with_distance() {
    let b1 = heliospheric_magnetic_field_at(1.0);
    let b2 = heliospheric_magnetic_field_at(5.0);
    assert!(b1 > b2);
}

#[test]
fn travel_time_proportional_to_distance() {
    let w = SolarWindState::slow_wind_1au();
    let t1 = w.travel_time_to_au(2.0);
    let t2 = w.travel_time_to_au(3.0);
    assert!((t2 / t1 - 2.0).abs() < 0.1);
}

// ---- Restored import coverage ----

#[test]
fn coronal_region_struct_accessible() {
    let r: CoronalRegion = quiet_corona();
    assert!(r.thermal_pressure() > 0.0);
    assert!(r.collision_frequency() > 0.0);
    assert!(r.radiative_loss_rate() > 0.0);
}

#[test]
fn coronal_euv_flux_decreases_with_distance() {
    let f1 = coronal_euv_flux(1.0);
    let f2 = coronal_euv_flux(2.0);
    assert!(f1 > f2);
    assert!(f2 > 0.0);
}

#[test]
fn coronal_loop_density_positive() {
    let d = coronal_loop_density(2e6, 1e8);
    assert!(d > 0.0);
}

#[test]
fn drain_rate_positive() {
    let r = drain_rate(1e-10, 1e6, 274.0, 5e7);
    assert!(r > 0.0);
}

#[test]
fn flare_ribbon_speed_positive() {
    let v = flare_ribbon_speed(0.1, 1e6);
    assert!(v > 0.0);
}

#[test]
fn bow_shock_standoff_positive() {
    let r = bow_shock_standoff(8e15, 1.0);
    assert!(r > 0.0);
}

#[test]
fn parker_solution_speed_positive() {
    let v = parker_solution_speed(parker_critical_radius() * 2.0);
    assert!(v > 0.0);
}