use qtty::dimensionless::OpticalDepths;
use qtty::length::{Kilometers, Nanometers};
use qtty::pressure::Hectopascals;
use qtty::unit::Micrometer;
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ScatterError {
#[error("{field} must be finite (got {value})")]
NonFinite {
field: &'static str,
value: f64,
},
#[error("{field} must be strictly positive (got {value})")]
NonPositive {
field: &'static str,
value: f64,
},
#[error("{field} must be non-negative (got {value})")]
Negative {
field: &'static str,
value: f64,
},
}
pub fn try_rayleigh_optical_depth_bodhaine99(
wavelength: Nanometers,
surface_pressure: Hectopascals,
observer_altitude: Kilometers,
scale_height: Kilometers,
) -> Result<OpticalDepths, ScatterError> {
let lam_um = wavelength.to::<Micrometer>().value();
let p_atm = surface_pressure.value() / 1013.25;
let h_km = observer_altitude.value();
let scale_km = scale_height.value();
check_finite("wavelength", lam_um)?;
check_positive("wavelength", lam_um)?;
check_finite("surface_pressure", surface_pressure.value())?;
check_non_negative("surface_pressure", surface_pressure.value())?;
check_finite("observer_altitude", h_km)?;
check_finite("scale_height", scale_km)?;
check_positive("scale_height", scale_km)?;
let lam_sq = lam_um * lam_um;
let tau_sea = 0.002_152_0 * (1.045_599_6 - 341.290_61 / lam_sq - 0.902_308_50 * lam_sq)
/ (1.0 + 0.002_705_988_9 / lam_sq - 85.968_563 * lam_sq);
Ok(OpticalDepths::new(
p_atm * tau_sea * (-(h_km / scale_km)).exp(),
))
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MieParams {
pub tau0: OpticalDepths,
pub alpha: f64,
pub lambda_ref: Nanometers,
}
impl MieParams {
pub fn try_new(tau0: f64, alpha: f64, lambda_ref_nm: f64) -> Result<Self, ScatterError> {
check_finite("tau0", tau0)?;
check_non_negative("tau0", tau0)?;
check_finite("alpha", alpha)?;
check_finite("lambda_ref", lambda_ref_nm)?;
check_positive("lambda_ref", lambda_ref_nm)?;
Ok(Self {
tau0: OpticalDepths::new(tau0),
alpha,
lambda_ref: Nanometers::new(lambda_ref_nm),
})
}
}
pub fn try_mie_optical_depth(
params: &MieParams,
wavelength: Nanometers,
) -> Result<OpticalDepths, ScatterError> {
let lambda = wavelength.value();
check_finite("wavelength", lambda)?;
check_positive("wavelength", lambda)?;
let lambda_ref = params.lambda_ref.value();
Ok(OpticalDepths::new(
params.tau0.value() * (lambda / lambda_ref).powf(params.alpha),
))
}
fn check_finite(field: &'static str, value: f64) -> Result<(), ScatterError> {
if !value.is_finite() {
return Err(ScatterError::NonFinite { field, value });
}
Ok(())
}
fn check_positive(field: &'static str, value: f64) -> Result<(), ScatterError> {
if value <= 0.0 {
return Err(ScatterError::NonPositive { field, value });
}
Ok(())
}
fn check_non_negative(field: &'static str, value: f64) -> Result<(), ScatterError> {
if value < 0.0 {
return Err(ScatterError::Negative { field, value });
}
Ok(())
}
#[cfg(test)]
mod tests {
use approx::assert_relative_eq;
use super::*;
#[test]
fn mie_power_law_matches_reference_ratio() {
let params = MieParams::try_new(0.2, -1.0, 500.0).unwrap();
let tau = try_mie_optical_depth(¶ms, Nanometers::new(1000.0)).unwrap();
assert_relative_eq!(tau.value(), 0.1, epsilon = 1.0e-12);
}
#[test]
fn mie_rejects_negative_tau0() {
assert!(matches!(
MieParams::try_new(-0.1, -1.0, 500.0),
Err(ScatterError::Negative { .. })
));
}
#[test]
fn rayleigh_validates_inputs() {
let r = try_rayleigh_optical_depth_bodhaine99(
Nanometers::new(550.0),
Hectopascals::new(1013.25),
Kilometers::new(0.0),
Kilometers::new(0.0),
);
assert!(matches!(r, Err(ScatterError::NonPositive { .. })));
}
#[test]
fn rayleigh_succeeds_for_reasonable_inputs() {
let tau = try_rayleigh_optical_depth_bodhaine99(
Nanometers::new(550.0),
Hectopascals::new(1013.25),
Kilometers::new(0.0),
Kilometers::new(8.0),
)
.unwrap();
assert!(tau.value() > 0.0 && tau.value() < 1.0);
}
}