suns 0.0.3

Sun celestial simulation crate for the MilkyWay SolarSystem workspace
Documentation
use suns::layers::chromosphere::{
    Chromosphere, Spicule, number_of_spicules, total_spicule_mass_flux,
    transition_region_temperature,
};
use suns::layers::convective_zone::{
    ConvectiveZone, convective_overshoot_depth, differential_rotation_angular_velocity,
    entropy_rain_speed, meridional_flow_speed,
};
use suns::layers::photosphere::{
    Photosphere, effective_temperature_from_luminosity, granulation_lifetime,
    spectral_irradiance_planck, supergranulation_lifetime, wilson_depression_depth,
};
use suns::layers::radiative_zone::{
    RadiativeZone, Tachocline, gravity_wave_period, photon_escape_time_estimate,
};

// ---- Photosphere ----

#[test]
fn photosphere_temperature_about_5778k() {
    let p = Photosphere::new();
    assert!(
        (p.temperature_k - 5778.0).abs() < 100.0,
        "T={}",
        p.temperature_k
    );
}

#[test]
fn photosphere_surface_flux_matches_stefan_boltzmann() {
    let p = Photosphere::new();
    let sigma = 5.670374419e-8;
    let expected = sigma * p.temperature_k.powi(4);
    let ratio = p.surface_flux() / expected;
    assert!((ratio - 1.0).abs() < 0.1, "flux ratio={ratio}");
}

#[test]
fn photosphere_spectral_peak_near_500nm() {
    let p = Photosphere::new();
    let lambda = p.spectral_peak_wavelength_m();
    assert!(lambda > 400e-9 && lambda < 600e-9, "λ_peak={lambda}m");
}

#[test]
fn photosphere_sound_speed_positive() {
    assert!(Photosphere::new().sound_speed() > 0.0);
}

#[test]
fn photosphere_density_decreases_with_height() {
    let p = Photosphere::new();
    let rho0 = p.density_at_height(0.0);
    let rho1 = p.density_at_height(100e3);
    assert!(rho1 < rho0);
}

#[test]
fn limb_darkening_at_center() {
    let p = Photosphere::new();
    let i = p.limb_darkening_intensity(0.0);
    assert!((i - 1.0).abs() < 0.1);
}

#[test]
fn limb_darkening_decreases_toward_limb() {
    let p = Photosphere::new();
    let i_center = p.limb_darkening_intensity(0.0);
    let i_limb = p.limb_darkening_intensity(1.3);
    assert!(i_limb < i_center);
}

#[test]
fn granulation_lifetime_minutes() {
    let t = granulation_lifetime();
    assert!(t > 60.0 && t < 3600.0, "granulation τ={t}s");
}

#[test]
fn supergranulation_lifetime_hours() {
    let t = supergranulation_lifetime();
    assert!(t > 3600.0 && t < 2e5, "supergranulation τ={t}s");
}

#[test]
fn effective_temperature_from_luminosity_near_5778() {
    let t = effective_temperature_from_luminosity(suns::SOLAR_LUMINOSITY);
    assert!((t - 5778.0).abs() < 200.0, "T_eff={t}");
}

#[test]
fn planck_spectral_irradiance_positive() {
    let b = spectral_irradiance_planck(500e-9, 5778.0);
    assert!(b > 0.0);
}

#[test]
fn planck_peak_near_500nm() {
    let b400 = spectral_irradiance_planck(400e-9, 5778.0);
    let b500 = spectral_irradiance_planck(500e-9, 5778.0);
    let b700 = spectral_irradiance_planck(700e-9, 5778.0);
    assert!(b500 > b400 || b500 > b700, "500nm should be near peak");
}

#[test]
fn wilson_depression_increases_with_field() {
    let d1 = wilson_depression_depth(0.1);
    let d2 = wilson_depression_depth(0.3);
    assert!(d2 > d1);
}

// ---- Chromosphere ----

#[test]
fn chromosphere_temperature_inversion() {
    let c = Chromosphere::new();
    assert!(c.top_temperature_k > c.base_temperature_k);
}

#[test]
fn chromosphere_thickness_about_2000km() {
    let c = Chromosphere::new();
    let km = c.thickness_m / 1e3;
    assert!(km > 1000.0 && km < 5000.0, "thickness={km}km");
}

#[test]
fn chromosphere_temperature_increases_with_height() {
    let c = Chromosphere::new();
    let t_low = c.temperature_at_height(0.1);
    let t_high = c.temperature_at_height(0.9);
    assert!(t_high > t_low);
}

#[test]
fn chromosphere_density_decreases_with_height() {
    let c = Chromosphere::new();
    let rho1 = c.density_at_height(100e3);
    let rho2 = c.density_at_height(1000e3);
    assert!(rho2 < rho1);
}

#[test]
fn chromosphere_scale_height_positive() {
    let c = Chromosphere::new();
    assert!(c.scale_height(1e4) > 0.0);
}

#[test]
fn spicule_type_ii_faster() {
    let s1 = Spicule::type_i();
    let s2 = Spicule::type_ii();
    assert!(s2.velocity_ms > s1.velocity_ms);
}

#[test]
fn spicule_type_ii_shorter_lived() {
    let s1 = Spicule::type_i();
    let s2 = Spicule::type_ii();
    assert!(s2.lifetime() < s1.lifetime());
}

#[test]
fn spicule_kinetic_energy_positive() {
    assert!(Spicule::type_i().kinetic_energy() > 0.0);
}

#[test]
fn number_of_spicules_large() {
    assert!(number_of_spicules() > 1e4);
}

#[test]
fn total_spicule_mass_flux_positive() {
    assert!(total_spicule_mass_flux() > 0.0);
}

#[test]
fn transition_region_temperature_increases() {
    let t1 = transition_region_temperature(0.0);
    let t2 = transition_region_temperature(1000.0);
    assert!(t2 > t1);
}

// ---- Convective Zone ----

#[test]
fn convective_zone_extends_to_surface() {
    let cz = ConvectiveZone::new();
    assert!((cz.outer_radius_frac - 1.0).abs() < 0.01);
}

#[test]
fn convective_zone_base_at_about_0_71() {
    let cz = ConvectiveZone::new();
    assert!(
        (cz.inner_radius_frac - 0.713).abs() < 0.05,
        "base={}",
        cz.inner_radius_frac
    );
}

#[test]
fn convective_zone_temperature_decreases_outward() {
    let cz = ConvectiveZone::new();
    let t_deep = cz.temperature_at_frac(0.75);
    let t_surf = cz.temperature_at_frac(0.99);
    assert!(t_deep > t_surf);
}

#[test]
fn convective_zone_density_decreases_outward() {
    let cz = ConvectiveZone::new();
    let rho_deep = cz.density_at_frac(0.75);
    let rho_surf = cz.density_at_frac(0.99);
    assert!(rho_deep > rho_surf);
}

#[test]
fn convective_velocity_positive() {
    let cz = ConvectiveZone::new();
    assert!(cz.convective_velocity(0.85) > 0.0);
}

#[test]
fn rossby_number_positive() {
    let cz = ConvectiveZone::new();
    assert!(cz.rossby_number(0.85) > 0.0);
}

#[test]
fn brunt_vaisala_defined() {
    let cz = ConvectiveZone::new();
    // In convective zone, BV frequency may be imaginary (negative N²), just check it's finite
    let n = cz.brunt_vaisala_frequency(0.85);
    assert!(n.is_finite());
}

#[test]
fn differential_rotation_equator_faster_than_pole() {
    let omega_eq = differential_rotation_angular_velocity(0.0);
    let omega_60 = differential_rotation_angular_velocity(60.0);
    assert!(omega_eq > omega_60);
}

#[test]
fn meridional_flow_speed_positive_midlat() {
    let v = meridional_flow_speed(30.0);
    assert!(v.abs() > 0.0);
}

#[test]
fn convective_overshoot_depth_positive() {
    assert!(convective_overshoot_depth() > 0.0);
}

#[test]
fn entropy_rain_speed_positive() {
    assert!(entropy_rain_speed(100.0) > 0.0);
}

// ---- Radiative Zone ----

#[test]
fn radiative_zone_temperature_decreases_outward() {
    let rz = RadiativeZone::new();
    let t_inner = rz.temperature_at_frac(0.30);
    let t_outer = rz.temperature_at_frac(0.65);
    assert!(t_inner > t_outer);
}

#[test]
fn radiative_zone_opacity_positive() {
    let rz = RadiativeZone::new();
    assert!(rz.opacity_at_frac(0.5) > 0.0);
}

#[test]
fn radiative_zone_mean_free_path_positive() {
    let rz = RadiativeZone::new();
    assert!(rz.mean_free_path_at_frac(0.5) > 0.0);
}

#[test]
fn photon_random_walk_time_large() {
    let rz = RadiativeZone::new();
    let t = rz.photon_random_walk_time();
    assert!(t > 1e10, "walk time={t}s too short");
}

#[test]
fn radiation_pressure_at_base_large() {
    let rz = RadiativeZone::new();
    let p = rz.radiation_pressure_at_frac(0.25);
    assert!(p > 0.0);
}

#[test]
fn tachocline_thickness_about_30000km() {
    let t = Tachocline::new();
    let km = t.thickness_m / 1e3;
    assert!(km > 5000.0 && km < 100000.0, "tachocline thickness={km}km");
}

#[test]
fn tachocline_shear_rate_positive() {
    assert!(Tachocline::new().shear_rate() > 0.0);
}

#[test]
fn tachocline_diffusion_time_long() {
    let t = Tachocline::new().diffusion_time();
    assert!(t > 1e10, "diffusion time={t}s");
}

#[test]
fn gravity_wave_period_positive() {
    let p = gravity_wave_period(0.5, 1e-4);
    assert!(p > 0.0);
}

#[test]
fn photon_escape_time_estimate_positive() {
    assert!(photon_escape_time_estimate() > 0.0);
}