use super::{Converter, Equivalency};
use crate::constants::SPEED_OF_LIGHT;
use crate::dimension::{Dimension, Rational16};
use crate::quantity::Quantity;
use crate::unit::Unit;
pub const AB_ZERO_POINT_JY: f64 = 3631.0;
pub const AB_ZERO_POINT_SI: f64 = 3631.0e-26;
fn flux_per_frequency_dimension() -> Dimension {
Dimension::MASS.mul(&Dimension::TIME.pow(Rational16::new(-2, 1)))
}
fn flux_per_wavelength_dimension() -> Dimension {
Dimension::MASS
.mul(&Dimension::LENGTH.pow(Rational16::new(-1, 1)))
.mul(&Dimension::TIME.pow(Rational16::new(-3, 1)))
}
fn is_flux_per_frequency(unit: &Unit) -> bool {
unit.dimension() == flux_per_frequency_dimension()
}
fn is_flux_per_wavelength(unit: &Unit) -> bool {
unit.dimension() == flux_per_wavelength_dimension()
}
fn is_magnitude(unit: &Unit) -> bool {
unit.dimension() == Dimension::MAGNITUDE
}
pub fn spectral_density(spectral_coord: Quantity) -> Equivalency {
let coord_dim = spectral_coord.unit().dimension();
let coord_si = spectral_coord.value() * spectral_coord.unit().scale();
let is_wavelength = coord_dim == Dimension::LENGTH;
let is_freq = coord_dim == Dimension::TIME.inv();
let lambda_si = if is_wavelength {
coord_si
} else if is_freq {
SPEED_OF_LIGHT / coord_si
} else {
0.0
};
Equivalency::new("spectral_density", move |from, to| {
if lambda_si <= 0.0 {
return None;
}
let c = SPEED_OF_LIGHT;
let lambda_squared = lambda_si * lambda_si;
if is_flux_per_frequency(from) && is_flux_per_wavelength(to) {
let factor = c / lambda_squared;
return Some(Converter::new(
move |f_nu_si| {
if f_nu_si < 0.0 {
return Err("flux density cannot be negative".to_string());
}
Ok(f_nu_si * factor)
},
move |f_lambda_si| {
if f_lambda_si < 0.0 {
return Err("flux density cannot be negative".to_string());
}
Ok(f_lambda_si / factor)
},
));
}
if is_flux_per_wavelength(from) && is_flux_per_frequency(to) {
let factor = lambda_squared / c;
return Some(Converter::new(
move |f_lambda_si| {
if f_lambda_si < 0.0 {
return Err("flux density cannot be negative".to_string());
}
Ok(f_lambda_si * factor)
},
move |f_nu_si| {
if f_nu_si < 0.0 {
return Err("flux density cannot be negative".to_string());
}
Ok(f_nu_si / factor)
},
));
}
None
})
}
pub fn ab_magnitude() -> Equivalency {
Equivalency::new("ab_magnitude", move |from, to| {
if is_flux_per_frequency(from) && is_magnitude(to) {
return Some(Converter::new(
move |f_nu_si| {
if f_nu_si <= 0.0 {
return Err(
"flux density must be positive for magnitude conversion".to_string()
);
}
let mag = -2.5 * (f_nu_si / AB_ZERO_POINT_SI).log10();
Ok(mag)
},
move |mag| {
let f_nu = AB_ZERO_POINT_SI * 10.0_f64.powf(-0.4 * mag);
Ok(f_nu)
},
));
}
if is_magnitude(from) && is_flux_per_frequency(to) {
return Some(Converter::new(
move |mag| {
let f_nu = AB_ZERO_POINT_SI * 10.0_f64.powf(-0.4 * mag);
Ok(f_nu)
},
move |f_nu_si| {
if f_nu_si <= 0.0 {
return Err(
"flux density must be positive for magnitude conversion".to_string()
);
}
let mag = -2.5 * (f_nu_si / AB_ZERO_POINT_SI).log10();
Ok(mag)
},
));
}
None
})
}
pub fn ab_magnitude_lambda(wavelength: Quantity) -> Equivalency {
let lambda_si = wavelength.value() * wavelength.unit().scale();
Equivalency::new("ab_magnitude_lambda", move |from, to| {
if lambda_si <= 0.0 {
return None;
}
let c = SPEED_OF_LIGHT;
let lambda_squared = lambda_si * lambda_si;
if is_flux_per_wavelength(from) && is_magnitude(to) {
let factor = lambda_squared / c;
return Some(Converter::new(
move |f_lambda_si| {
if f_lambda_si <= 0.0 {
return Err(
"flux density must be positive for magnitude conversion".to_string()
);
}
let f_nu_si = f_lambda_si * factor;
let mag = -2.5 * (f_nu_si / AB_ZERO_POINT_SI).log10();
Ok(mag)
},
move |mag| {
let f_nu_si = AB_ZERO_POINT_SI * 10.0_f64.powf(-0.4 * mag);
let f_lambda_si = f_nu_si / factor;
Ok(f_lambda_si)
},
));
}
if is_magnitude(from) && is_flux_per_wavelength(to) {
let factor = lambda_squared / c;
return Some(Converter::new(
move |mag| {
let f_nu_si = AB_ZERO_POINT_SI * 10.0_f64.powf(-0.4 * mag);
let f_lambda_si = f_nu_si / factor;
Ok(f_lambda_si)
},
move |f_lambda_si| {
if f_lambda_si <= 0.0 {
return Err(
"flux density must be positive for magnitude conversion".to_string()
);
}
let f_nu_si = f_lambda_si * factor;
let mag = -2.5 * (f_nu_si / AB_ZERO_POINT_SI).log10();
Ok(mag)
},
));
}
None
})
}
pub fn ab_mag_to_jansky(mag: f64) -> f64 {
AB_ZERO_POINT_JY * 10.0_f64.powf(-0.4 * mag)
}
pub fn jansky_to_ab_mag(flux_jy: f64) -> f64 {
-2.5 * (flux_jy / AB_ZERO_POINT_JY).log10()
}
pub fn f_nu_to_f_lambda(f_nu: f64, wavelength_m: f64) -> f64 {
f_nu * SPEED_OF_LIGHT / (wavelength_m * wavelength_m)
}
pub fn f_lambda_to_f_nu(f_lambda: f64, wavelength_m: f64) -> f64 {
f_lambda * wavelength_m * wavelength_m / SPEED_OF_LIGHT
}
#[cfg(test)]
mod tests {
use super::*;
use crate::systems::astrophysical::JANSKY;
#[cfg(feature = "logarithmic")]
use crate::systems::logarithmic::MAG;
use crate::systems::si::{HZ, M, NM, W};
fn f_lambda_unit() -> Unit {
W / (M * M * M)
}
fn f_nu_unit() -> Unit {
W / (M * M * HZ)
}
#[cfg(feature = "logarithmic")]
#[test]
fn test_ab_zero_point() {
let flux = 3631.0 * JANSKY;
let mag = flux.to_equiv(&MAG, ab_magnitude()).unwrap();
assert!((mag.value() - 0.0).abs() < 1e-10);
}
#[cfg(feature = "logarithmic")]
#[test]
fn test_ab_mag_1jy() {
let flux = 1.0 * JANSKY;
let mag = flux.to_equiv(&MAG, ab_magnitude()).unwrap();
let expected = -2.5 * (1.0 / 3631.0_f64).log10();
assert!((mag.value() - expected).abs() < 1e-10);
assert!((mag.value() - 8.9).abs() < 0.05);
}
#[cfg(feature = "logarithmic")]
#[test]
fn test_ab_mag_to_flux() {
let mag = 0.0 * MAG;
let flux = mag.to_equiv(&JANSKY, ab_magnitude()).unwrap();
assert!((flux.value() - 3631.0).abs() < 1e-6);
}
#[cfg(feature = "logarithmic")]
#[test]
fn test_ab_mag_20() {
let mag = 20.0 * MAG;
let flux = mag.to_equiv(&JANSKY, ab_magnitude()).unwrap();
let expected = 3631.0 * 10.0_f64.powf(-0.4 * 20.0);
assert!((flux.value() - expected).abs() / expected < 1e-10);
}
#[cfg(feature = "logarithmic")]
#[test]
fn test_ab_mag_roundtrip() {
let original_flux = 100.0 * JANSKY;
let mag = original_flux.to_equiv(&MAG, ab_magnitude()).unwrap();
let recovered = mag.to_equiv(&JANSKY, ab_magnitude()).unwrap();
assert!((recovered.value() - 100.0).abs() < 1e-10);
}
#[test]
fn test_f_nu_to_f_lambda_conversion() {
let wavelength = 500.0 * NM;
let f_nu = 1.0 * JANSKY;
let f_lambda = f_nu
.to_equiv(&f_lambda_unit(), spectral_density(wavelength))
.unwrap();
let lambda_m = 500e-9;
let expected = 1e-26 * SPEED_OF_LIGHT / (lambda_m * lambda_m);
let actual_si = f_lambda.value() * f_lambda.unit().scale();
assert!((actual_si - expected).abs() / expected < 1e-10);
}
#[test]
fn test_f_lambda_to_f_nu_conversion() {
let wavelength = 500.0 * NM;
let f_lambda_val = 1e-10; let f_lambda = f_lambda_val * f_lambda_unit();
let f_nu = f_lambda
.to_equiv(&f_nu_unit(), spectral_density(wavelength.clone()))
.unwrap();
let lambda_m = 500e-9;
let expected = f_lambda_val * lambda_m * lambda_m / SPEED_OF_LIGHT;
let actual_si = f_nu.value() * f_nu.unit().scale();
assert!((actual_si - expected).abs() / expected < 1e-10);
}
#[test]
fn test_spectral_density_roundtrip() {
let wavelength = 600.0 * NM;
let original = 1.0 * JANSKY;
let f_lambda = original
.to_equiv(&f_lambda_unit(), spectral_density(wavelength.clone()))
.unwrap();
let recovered = f_lambda
.to_equiv(&JANSKY, spectral_density(wavelength))
.unwrap();
assert!((recovered.value() - 1.0).abs() < 1e-10);
}
#[test]
fn test_spectral_density_with_frequency() {
let frequency = 6e14 * HZ; let f_nu = 1.0 * JANSKY;
let f_lambda = f_nu
.to_equiv(&f_lambda_unit(), spectral_density(frequency))
.unwrap();
let lambda_m = SPEED_OF_LIGHT / 6e14;
let expected = 1e-26 * SPEED_OF_LIGHT / (lambda_m * lambda_m);
let actual_si = f_lambda.value() * f_lambda.unit().scale();
assert!((actual_si - expected).abs() / expected < 1e-6);
}
#[test]
fn test_negative_flux_fails() {
let wavelength = 500.0 * NM;
let f_nu = -1.0 * JANSKY;
let result = f_nu.to_equiv(&f_lambda_unit(), spectral_density(wavelength));
assert!(result.is_err());
}
#[cfg(feature = "logarithmic")]
#[test]
fn test_ab_negative_flux_fails() {
let flux = -1.0 * JANSKY;
let result = flux.to_equiv(&MAG, ab_magnitude());
assert!(result.is_err());
}
#[test]
fn test_convenience_functions() {
assert!((ab_mag_to_jansky(0.0) - 3631.0).abs() < 1e-10);
assert!((ab_mag_to_jansky(20.0) - 3.631e-5).abs() < 1e-10);
assert!((jansky_to_ab_mag(3631.0) - 0.0).abs() < 1e-10);
assert!((jansky_to_ab_mag(1.0) - 8.9).abs() < 0.01);
let mag = 15.5;
let flux = ab_mag_to_jansky(mag);
let recovered = jansky_to_ab_mag(flux);
assert!((recovered - mag).abs() < 1e-10);
}
#[test]
fn test_f_nu_f_lambda_functions() {
let wavelength_m = 500e-9;
let f_nu = 1e-26;
let f_lambda = f_nu_to_f_lambda(f_nu, wavelength_m);
let f_nu_back = f_lambda_to_f_nu(f_lambda, wavelength_m);
assert!((f_nu_back - f_nu).abs() / f_nu < 1e-10);
}
#[cfg(feature = "logarithmic")]
#[test]
fn test_ab_magnitude_lambda() {
let wavelength = 500.0 * NM;
let lambda_m = 500e-9;
let f_lambda_zero_mag = AB_ZERO_POINT_SI * SPEED_OF_LIGHT / (lambda_m * lambda_m);
let f_lambda = f_lambda_zero_mag * f_lambda_unit();
let mag = f_lambda
.to_equiv(&MAG, ab_magnitude_lambda(wavelength.clone()))
.unwrap();
assert!((mag.value() - 0.0).abs() < 1e-6);
}
}