use crate::site::atmosphere::rayleigh_optical_depth_local_pressure;
use crate::site::AtmosphericConditions;
use crate::units::angular::{Degrees, Radian};
use crate::units::dimensionless::Transmittances;
use siderust::atmosphere::mie_optical_depth;
use siderust::qtty::Nanometers;
pub(crate) const NOLL_AIRGLOW_SCATTERING_FIT_MAX_ZENITH_DEG: f64 = 60.0;
const AIRGLOW_AIRMASS_SIN2_COEFF: f64 = 0.972;
const F_RAYLEIGH_SLOPE: f64 = 1.669;
const F_RAYLEIGH_INTERCEPT: f64 = -0.146;
const F_MIE_SLOPE: f64 = 1.732;
#[allow(clippy::approx_constant)] const F_MIE_INTERCEPT: f64 = -0.318;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct NollAirglowScatteringGeometry {
pub effective_airmass: f64,
pub f_rayleigh: f64,
pub f_mie: f64,
}
#[inline]
pub(crate) fn effective_airglow_airmass(zenith: Degrees) -> f64 {
let sin_z = zenith.to::<Radian>().value().sin();
let denom = 1.0 - AIRGLOW_AIRMASS_SIN2_COEFF * sin_z * sin_z;
if denom <= 0.0 {
return f64::INFINITY;
}
denom.powf(-0.5)
}
#[inline]
pub(crate) fn noll_scattering_factors(zenith: Degrees) -> (f64, f64) {
let x_ag = effective_airglow_airmass(zenith);
let log_x = x_ag.log10();
let f_rayleigh = F_RAYLEIGH_SLOPE * log_x + F_RAYLEIGH_INTERCEPT;
let f_mie = F_MIE_SLOPE * log_x + F_MIE_INTERCEPT;
(f_rayleigh, f_mie)
}
pub(crate) fn noll_airglow_scattering_geometry(zenith: Degrees) -> NollAirglowScatteringGeometry {
let effective_airmass = effective_airglow_airmass(zenith);
let (f_rayleigh, f_mie) = noll_scattering_factors(zenith);
NollAirglowScatteringGeometry {
effective_airmass,
f_rayleigh,
f_mie,
}
}
pub(crate) fn spectral_airglow_scattering_transmission_with_geometry(
wavelength: Nanometers,
atmosphere: AtmosphericConditions,
geometry: &NollAirglowScatteringGeometry,
) -> Transmittances {
let tau_rayleigh = rayleigh_optical_depth_local_pressure(wavelength, atmosphere).value();
let tau_mie = mie_optical_depth(&atmosphere.mie_params, wavelength).value();
let tau_eff = geometry.f_rayleigh * tau_rayleigh + geometry.f_mie * tau_mie;
let exponent = -geometry.effective_airmass * tau_eff;
if !exponent.is_finite() {
return Transmittances::new(0.0);
}
Transmittances::new(exponent.exp())
}
#[cfg(test)]
mod tests {
use super::*;
pub(crate) fn bodhaine_rayleigh_tau_sea_level(wavelength_um: f64) -> f64 {
let l2 = wavelength_um * wavelength_um;
let inv_l2 = 1.0 / l2;
0.0021520 * (1.0455996 - 341.29061 * inv_l2 - 0.90230850 * l2)
/ (1.0 + 0.0027059889 * inv_l2 - 85.968563 * l2)
}
pub(crate) fn spectral_airglow_scattering_transmission(
wavelength: Nanometers,
zenith: Degrees,
atmosphere: AtmosphericConditions,
) -> Transmittances {
let geometry = noll_airglow_scattering_geometry(zenith);
spectral_airglow_scattering_transmission_with_geometry(wavelength, atmosphere, &geometry)
}
use siderust::atmosphere::profile::AtmosphereProfile;
use siderust::atmosphere::{mie_optical_depth, rayleigh_optical_depth_bodhaine99};
use siderust::qtty::{Hectopascals, Kilometers};
const SEA_LEVEL_PRESSURE_HPA: f64 = 1013.25;
fn paranal_atmosphere() -> AtmosphericConditions {
AtmosphericConditions::from_profile_without_altitude(AtmosphereProfile::EL_PARANAL)
}
#[test]
fn effective_airglow_airmass_at_zenith_is_unity() {
let x = effective_airglow_airmass(Degrees::new(0.0));
assert!((x - 1.0).abs() < 1e-12, "X_ag at zenith: {x}");
}
#[test]
fn scattering_factors_are_negative_near_zenith() {
let (f_r, f_m) = noll_scattering_factors(Degrees::new(0.0));
assert!(f_r < 0.0, "f_R at zenith should be negative: {f_r}");
assert!(f_m < 0.0, "f_M at zenith should be negative: {f_m}");
}
#[test]
fn rayleigh_local_pressure_is_not_double_reduced_by_altitude() {
let atmosphere = paranal_atmosphere();
let wavelength = Nanometers::new(550.0);
let local = rayleigh_optical_depth_local_pressure(wavelength, atmosphere).value();
let tau_sea = bodhaine_rayleigh_tau_sea_level(0.550);
let expected_local = atmosphere.surface_pressure.value() / SEA_LEVEL_PRESSURE_HPA * tau_sea;
assert!(
(local - expected_local).abs() < 1e-12,
"local-pressure Rayleigh depth: got {local}, expected {expected_local}"
);
let double_reduced = rayleigh_optical_depth_bodhaine99(
wavelength,
atmosphere.surface_pressure,
Kilometers::new(2.635),
atmosphere.rayleigh_scale_height,
)
.value();
assert!(
(double_reduced - local * (-2.635_f64 / 8.0).exp()).abs() < 1e-12,
"Siderust with site altitude should apply the extra exp(-h/H) factor"
);
assert!(
local > double_reduced,
"local pressure must not be reduced twice by altitude"
);
}
#[test]
fn transmission_is_wavelength_dependent_at_fixed_zenith() {
let atmosphere = paranal_atmosphere();
let zenith = Degrees::new(45.0);
let blue =
spectral_airglow_scattering_transmission(Nanometers::new(350.0), zenith, atmosphere)
.value();
let red =
spectral_airglow_scattering_transmission(Nanometers::new(600.0), zenith, atmosphere)
.value();
assert!(blue.is_finite() && red.is_finite());
assert!(
blue < red,
"Rayleigh-dominated shorter wavelengths should attenuate more: blue={blue}, red={red}"
);
}
#[test]
fn noll_transmission_matches_independent_paranal_reference_at_45_degrees() {
let atmosphere = paranal_atmosphere();
let zenith = Degrees::new(45.0);
let wavelength = Nanometers::new(550.0);
let expected_x_ag = 1.394_820_881_629_177;
let expected_f_r = 0.095_201_277_198_442;
let expected_f_m = -0.067_694_061_049_909;
let expected_tau_r = 0.071_272_180_636_833;
let expected_tau_m = 0.05;
let expected_tau_eff = expected_f_r * expected_tau_r + expected_f_m * expected_tau_m;
let expected_transmission = 0.995_268_142_865_769;
let geometry = noll_airglow_scattering_geometry(zenith);
assert!((geometry.effective_airmass - expected_x_ag).abs() < 1e-12);
assert!((geometry.f_rayleigh - expected_f_r).abs() < 1e-12);
assert!((geometry.f_mie - expected_f_m).abs() < 1e-12);
let tau_r = rayleigh_optical_depth_local_pressure(wavelength, atmosphere).value();
let tau_m = mie_optical_depth(&atmosphere.mie_params, wavelength).value();
assert!((tau_r - expected_tau_r).abs() < 1e-12);
assert!((tau_m - expected_tau_m).abs() < 1e-12);
let tau_eff = geometry.f_rayleigh * tau_r + geometry.f_mie * tau_m;
assert!((tau_eff - expected_tau_eff).abs() < 1e-15);
let got = spectral_airglow_scattering_transmission_with_geometry(
wavelength, atmosphere, &geometry,
)
.value();
assert!(
(got - expected_transmission).abs() < 1e-12,
"transmission: got {got}, expected {expected_transmission}"
);
let without_x_ag = (-tau_eff).exp();
assert!(
(without_x_ag - got).abs() > 1e-4,
"X_ag must remain in the Noll transmission exponent"
);
}
#[test]
fn different_atmospheres_change_transmission() {
let zenith = Degrees::new(45.0);
let wl = Nanometers::new(500.0);
let low_pressure = AtmosphericConditions {
surface_pressure: Hectopascals::new(600.0),
..paranal_atmosphere()
};
let high_pressure = AtmosphericConditions {
surface_pressure: Hectopascals::new(900.0),
..paranal_atmosphere()
};
let low = spectral_airglow_scattering_transmission(wl, zenith, low_pressure).value();
let high = spectral_airglow_scattering_transmission(wl, zenith, high_pressure).value();
assert_ne!(low, high);
}
}