use crate::atmosphere::extinction::transmission;
use crate::atmosphere::mie::{mie_optical_depth, MieParams};
use crate::atmosphere::rayleigh::{rayleigh_optical_depth_bodhaine99, DEFAULT_SCALE_HEIGHT};
use crate::atmosphere::Transmittances;
use crate::qtty::{Airmasses, Hectopascals, Kilometers, Nanometers, OpticalDepths};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AtmosphereProfile {
pub surface_pressure: Hectopascals,
pub observer_altitude: Kilometers,
pub rayleigh_scale_height: Kilometers,
pub mie_params: MieParams,
}
impl AtmosphereProfile {
pub const EL_PARANAL: AtmosphereProfile = AtmosphereProfile {
surface_pressure: Hectopascals::new(744.0),
observer_altitude: Kilometers::new(2.635),
rayleigh_scale_height: DEFAULT_SCALE_HEIGHT,
mie_params: MieParams::PARANAL,
};
pub const ROQUE_DE_LOS_MUCHACHOS: AtmosphereProfile = AtmosphereProfile {
surface_pressure: Hectopascals::new(770.0),
observer_altitude: Kilometers::new(2.396),
rayleigh_scale_height: DEFAULT_SCALE_HEIGHT,
mie_params: MieParams::LA_PALMA,
};
pub const MAUNA_KEA: AtmosphereProfile = AtmosphereProfile {
surface_pressure: Hectopascals::new(615.0),
observer_altitude: Kilometers::new(4.205),
rayleigh_scale_height: DEFAULT_SCALE_HEIGHT,
mie_params: MieParams::MAUNA_KEA,
};
pub const LA_SILLA: AtmosphereProfile = AtmosphereProfile {
surface_pressure: Hectopascals::new(770.0),
observer_altitude: Kilometers::new(2.400),
rayleigh_scale_height: DEFAULT_SCALE_HEIGHT,
mie_params: MieParams::LA_SILLA,
};
pub fn optical_depth(&self, lambda: Nanometers) -> OpticalDepths {
let tau_r = rayleigh_optical_depth_bodhaine99(
lambda,
self.surface_pressure,
Kilometers::new(0.0),
self.rayleigh_scale_height,
);
let tau_m = mie_optical_depth(&self.mie_params, lambda);
tau_r + tau_m
}
pub fn transmission(&self, lambda: Nanometers, airmass: Airmasses) -> Transmittances {
transmission(self.optical_depth(lambda), airmass)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_pressure_is_not_reduced_again_by_observer_altitude() {
let mut profile = AtmosphereProfile::EL_PARANAL;
let elevated = profile.optical_depth(Nanometers::new(550.0));
profile.observer_altitude = Kilometers::new(0.0);
assert_eq!(elevated, profile.optical_depth(Nanometers::new(550.0)));
let height = Kilometers::new(2.635);
let local = Hectopascals::new(1013.25 * (-2.635_f64 / 8.0).exp());
let reference = rayleigh_optical_depth_bodhaine99(
Nanometers::new(550.0),
Hectopascals::new(1013.25),
height,
DEFAULT_SCALE_HEIGHT,
);
profile.surface_pressure = local;
profile.observer_altitude = height;
let actual = profile.optical_depth(Nanometers::new(550.0))
- mie_optical_depth(&profile.mie_params, Nanometers::new(550.0));
assert!((actual.value() - reference.value()).abs() < 1e-15);
}
#[test]
fn presets_have_typed_units() {
let p = AtmosphereProfile::EL_PARANAL;
assert_eq!(p.surface_pressure.value(), 744.0);
assert_eq!(p.observer_altitude.value(), 2.635);
assert_eq!(p.rayleigh_scale_height.value(), 8.0);
}
#[test]
fn optical_depth_is_positive_and_decreases_with_wavelength() {
let p = AtmosphereProfile::EL_PARANAL;
let blue = p.optical_depth(Nanometers::new(400.0));
let red = p.optical_depth(Nanometers::new(700.0));
assert!(blue.value() > 0.0);
assert!(red.value() > 0.0);
assert!(
blue.value() > red.value(),
"Rayleigh + Mie both fall with λ"
);
}
#[test]
fn transmission_decreases_with_airmass() {
let p = AtmosphereProfile::EL_PARANAL;
let lambda = Nanometers::new(550.0);
let t1 = p.transmission(lambda, Airmasses::new(1.0));
let t2 = p.transmission(lambda, Airmasses::new(2.0));
assert!(t1.value() > t2.value());
assert!(t1.value() > 0.0 && t1.value() < 1.0);
}
#[test]
fn higher_altitude_lowers_rayleigh_contribution() {
let lambda = Nanometers::new(500.0);
let mk = AtmosphereProfile::MAUNA_KEA.optical_depth(lambda);
let pa = AtmosphereProfile::EL_PARANAL.optical_depth(lambda);
assert!(
mk.value() < pa.value(),
"Mauna Kea τ ({}) should be smaller than Paranal τ ({})",
mk.value(),
pa.value(),
);
}
}