use crate::error::{Result, AstroError};
pub fn airmass_plane_parallel(altitude_deg: f64) -> Result<f64> {
if !(-90.0..=90.0).contains(&altitude_deg) {
return Err(AstroError::OutOfRange {
parameter: "altitude",
value: altitude_deg,
min: -90.0,
max: 90.0,
});
}
if altitude_deg <= 0.0 {
return Ok(f64::INFINITY);
}
let zenith_angle = 90.0 - altitude_deg;
Ok(1.0 / zenith_angle.to_radians().cos())
}
pub fn airmass_young(altitude_deg: f64) -> Result<f64> {
if !(-90.0..=90.0).contains(&altitude_deg) {
return Err(AstroError::OutOfRange {
parameter: "altitude",
value: altitude_deg,
min: -90.0,
max: 90.0,
});
}
if altitude_deg <= -0.5 {
return Ok(f64::INFINITY);
}
let zenith_angle = 90.0 - altitude_deg;
let z_rad = zenith_angle.to_radians();
let cos_z = z_rad.cos();
Ok(1.0 / (cos_z + 0.50572 * (96.07995 - zenith_angle).powf(-1.6364)))
}
pub fn airmass_pickering(altitude_deg: f64) -> Result<f64> {
if !(-90.0..=90.0).contains(&altitude_deg) {
return Err(AstroError::OutOfRange {
parameter: "altitude",
value: altitude_deg,
min: -90.0,
max: 90.0,
});
}
if altitude_deg <= -0.5 {
return Ok(f64::INFINITY);
}
let h = altitude_deg.max(0.0); Ok(1.0 / (h + 244.0 / (165.0 + 47.0 * h.powf(1.1))).to_radians().sin())
}
pub fn airmass_kasten_young(altitude_deg: f64) -> Result<f64> {
if !(-90.0..=90.0).contains(&altitude_deg) {
return Err(AstroError::OutOfRange {
parameter: "altitude",
value: altitude_deg,
min: -90.0,
max: 90.0,
});
}
if altitude_deg <= 0.0 {
return Ok(f64::INFINITY);
}
let zenith_angle = 90.0 - altitude_deg;
let z_rad = zenith_angle.to_radians();
Ok(1.0 / (z_rad.cos() + 0.50572 * (96.07995 - zenith_angle).powf(-1.6364)))
}
pub fn extinction_magnitudes(airmass: f64, extinction_coefficient: f64) -> f64 {
airmass * extinction_coefficient
}
pub fn extinction_coefficient_estimate(wavelength_nm: f64) -> Result<f64> {
if wavelength_nm <= 0.0 {
return Err(AstroError::OutOfRange {
parameter: "wavelength_nm",
value: wavelength_nm,
min: f64::MIN_POSITIVE,
max: f64::MAX,
});
}
let rayleigh = 0.145 * (550.0 / wavelength_nm).powf(4.0);
let aerosol = 0.10 * (550.0 / wavelength_nm).powf(1.3);
let ozone = if wavelength_nm > 500.0 && wavelength_nm < 700.0 {
0.016
} else {
0.0
};
Ok(rayleigh + aerosol + ozone)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_airmass_zenith() {
assert!((airmass_plane_parallel(90.0).unwrap() - 1.0).abs() < 1e-6);
assert!((airmass_young(90.0).unwrap() - 1.0).abs() < 0.001);
assert!((airmass_pickering(90.0).unwrap() - 1.0).abs() < 0.01);
assert!((airmass_kasten_young(90.0).unwrap() - 1.0).abs() < 0.001);
}
#[test]
fn test_airmass_45deg() {
let expected = 2.0_f64.sqrt();
assert!((airmass_plane_parallel(45.0).unwrap() - expected).abs() < 1e-6);
assert!((airmass_young(45.0).unwrap() - expected).abs() < 0.01);
assert!((airmass_pickering(45.0).unwrap() - expected).abs() < 0.01);
}
#[test]
fn test_airmass_horizon() {
let am_young = airmass_young(0.0).unwrap();
let am_pickering = airmass_pickering(0.0).unwrap();
assert!(am_young > 30.0 && am_young < 50.0);
assert!(am_pickering > 30.0 && am_pickering < 50.0);
}
#[test]
fn test_airmass_below_horizon() {
assert!(airmass_plane_parallel(-5.0).unwrap().is_infinite());
assert!(airmass_young(-5.0).unwrap().is_infinite());
assert!(airmass_pickering(-5.0).unwrap().is_infinite());
assert!(airmass_kasten_young(-5.0).unwrap().is_infinite());
}
#[test]
fn test_extinction() {
let airmass = 2.0;
let k = 0.15; let extinction = extinction_magnitudes(airmass, k);
assert!((extinction - 0.3).abs() < 1e-6);
}
#[test]
fn test_extinction_coefficient() {
let k_blue = extinction_coefficient_estimate(450.0).unwrap();
let k_red = extinction_coefficient_estimate(650.0).unwrap();
assert!(k_blue > k_red);
assert!(k_blue > 0.15 && k_blue < 0.5);
assert!(k_red > 0.05 && k_red < 0.3);
}
}