use crate::site::atmosphere::rayleigh_optical_depth_local_pressure;
use crate::site::AtmosphericConditions;
use siderust::atmosphere::{mie_optical_depth, ozone};
use siderust::qtty::{Nanometers, OpticalDepths};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum MolecularAbsorption {
#[default]
None,
OzoneBundledTable,
}
impl MolecularAbsorption {
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::OzoneBundledTable => "ozone-bundled-table",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct ExtinctionIngredients {
pub rayleigh: bool,
pub mie: bool,
pub absorption: MolecularAbsorption,
}
impl ExtinctionIngredients {
pub const RAYLEIGH_MIE: Self = Self {
rayleigh: true,
mie: true,
absorption: MolecularAbsorption::None,
};
pub const RAYLEIGH_MIE_OZONE: Self = Self {
rayleigh: true,
mie: true,
absorption: MolecularAbsorption::OzoneBundledTable,
};
pub const fn new(rayleigh: bool, mie: bool, absorption: MolecularAbsorption) -> Self {
Self {
rayleigh,
mie,
absorption,
}
}
}
impl Default for ExtinctionIngredients {
fn default() -> Self {
Self::RAYLEIGH_MIE
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct OpticalDepthBreakdown {
pub rayleigh: OpticalDepths,
pub mie: OpticalDepths,
pub absorption: OpticalDepths,
pub total: OpticalDepths,
}
pub(crate) fn optical_depth_breakdown(
wavelength: Nanometers,
atmosphere: AtmosphericConditions,
ingredients: ExtinctionIngredients,
) -> OpticalDepthBreakdown {
let rayleigh = if ingredients.rayleigh {
rayleigh_optical_depth_local_pressure(wavelength, atmosphere)
} else {
OpticalDepths::new(0.0)
};
let mie = if ingredients.mie {
mie_optical_depth(&atmosphere.mie_params, wavelength)
} else {
OpticalDepths::new(0.0)
};
let absorption = match ingredients.absorption {
MolecularAbsorption::None => OpticalDepths::new(0.0),
MolecularAbsorption::OzoneBundledTable => ozone_vertical_optical_depth(wavelength),
};
let total = OpticalDepths::new(rayleigh.value() + mie.value() + absorption.value());
OpticalDepthBreakdown {
rayleigh,
mie,
absorption,
total,
}
}
fn ozone_vertical_optical_depth(wavelength: Nanometers) -> OpticalDepths {
let transmittance = ozone::transmittance_at(wavelength).value();
if !transmittance.is_finite() || transmittance <= 0.0 {
return OpticalDepths::new(f64::INFINITY);
}
if transmittance >= 1.0 {
return OpticalDepths::new(0.0);
}
OpticalDepths::new((-transmittance.ln()).max(0.0))
}
#[cfg(test)]
mod tests {
use super::*;
use siderust::atmosphere::AtmosphereProfile;
fn paranal() -> AtmosphericConditions {
AtmosphericConditions::from_profile_without_altitude(AtmosphereProfile::EL_PARANAL)
}
#[test]
fn zero_ingredients_means_zero_optical_depth() {
let breakdown = optical_depth_breakdown(
Nanometers::new(550.0),
paranal(),
ExtinctionIngredients::new(false, false, MolecularAbsorption::None),
);
assert_eq!(breakdown.total.value(), 0.0);
}
#[test]
fn rayleigh_mie_is_positive_at_paranal() {
let breakdown = optical_depth_breakdown(
Nanometers::new(550.0),
paranal(),
ExtinctionIngredients::RAYLEIGH_MIE,
);
assert!(breakdown.rayleigh.value() > 0.0);
assert!(breakdown.mie.value() > 0.0);
assert!(breakdown.total.value() > breakdown.rayleigh.value());
}
#[test]
fn ozone_increases_total_optical_depth_in_uv() {
let without = optical_depth_breakdown(
Nanometers::new(320.0),
paranal(),
ExtinctionIngredients::RAYLEIGH_MIE,
);
let with = optical_depth_breakdown(
Nanometers::new(320.0),
paranal(),
ExtinctionIngredients::RAYLEIGH_MIE_OZONE,
);
assert!(with.absorption.value() > 0.0);
assert!(with.total.value() > without.total.value());
}
#[test]
fn molecular_absorption_as_str_is_stable() {
assert_eq!(MolecularAbsorption::None.as_str(), "none");
assert_eq!(
MolecularAbsorption::OzoneBundledTable.as_str(),
"ozone-bundled-table"
);
}
}