use super::{Converter, Equivalency};
use crate::constants::{BOLTZMANN_CONSTANT, PLANCK_CONSTANT, SPEED_OF_LIGHT};
use crate::dimension::{Dimension, Rational16};
use crate::quantity::Quantity;
use crate::unit::Unit;
fn is_flux_density(unit: &Unit) -> bool {
let flux_density_dim = Dimension::MASS.mul(&Dimension::TIME.pow(Rational16::new(-2, 1)));
unit.dimension() == flux_density_dim
}
fn is_temperature(unit: &Unit) -> bool {
unit.dimension() == Dimension::TEMPERATURE
}
fn is_spectral_radiance(unit: &Unit) -> bool {
let radiance_dim = Dimension::MASS
.mul(&Dimension::TIME.pow(Rational16::new(-2, 1)))
.mul(&Dimension::SOLID_ANGLE.pow(Rational16::new(-1, 1)));
unit.dimension() == radiance_dim
}
pub fn brightness_temperature(frequency: Quantity, beam_solid_angle: Quantity) -> Equivalency {
let nu_hz = frequency.value() * frequency.unit().scale();
let omega_sr = beam_solid_angle.value() * beam_solid_angle.unit().scale();
let c_squared = SPEED_OF_LIGHT * SPEED_OF_LIGHT;
let two_k = 2.0 * BOLTZMANN_CONSTANT;
let nu_squared = nu_hz * nu_hz;
let flux_to_temp_factor = c_squared / (two_k * nu_squared * omega_sr);
let temp_to_flux_factor = two_k * nu_squared * omega_sr / c_squared;
Equivalency::new("brightness_temperature", move |from, to| {
if is_flux_density(from) && is_temperature(to) {
return Some(Converter::new(
move |s_nu_si| {
if s_nu_si < 0.0 {
return Err("flux density must be non-negative".to_string());
}
Ok(s_nu_si * flux_to_temp_factor)
},
move |t_b_si| {
if t_b_si < 0.0 {
return Err("brightness temperature must be non-negative".to_string());
}
Ok(t_b_si * temp_to_flux_factor)
},
));
}
if is_temperature(from) && is_flux_density(to) {
return Some(Converter::new(
move |t_b_si| {
if t_b_si < 0.0 {
return Err("brightness temperature must be non-negative".to_string());
}
Ok(t_b_si * temp_to_flux_factor)
},
move |s_nu_si| {
if s_nu_si < 0.0 {
return Err("flux density must be non-negative".to_string());
}
Ok(s_nu_si * flux_to_temp_factor)
},
));
}
None
})
}
pub fn brightness_temperature_intensity(frequency: Quantity) -> Equivalency {
let nu_hz = frequency.value() * frequency.unit().scale();
let c_squared = SPEED_OF_LIGHT * SPEED_OF_LIGHT;
let two_k = 2.0 * BOLTZMANN_CONSTANT;
let nu_squared = nu_hz * nu_hz;
let intensity_to_temp = c_squared / (two_k * nu_squared);
let temp_to_intensity = two_k * nu_squared / c_squared;
Equivalency::new("brightness_temperature_intensity", move |from, to| {
if is_spectral_radiance(from) && is_temperature(to) {
return Some(Converter::new(
move |i_nu_si| {
if i_nu_si < 0.0 {
return Err("spectral radiance must be non-negative".to_string());
}
Ok(i_nu_si * intensity_to_temp)
},
move |t_b_si| {
if t_b_si < 0.0 {
return Err("brightness temperature must be non-negative".to_string());
}
Ok(t_b_si * temp_to_intensity)
},
));
}
if is_temperature(from) && is_spectral_radiance(to) {
return Some(Converter::new(
move |t_b_si| {
if t_b_si < 0.0 {
return Err("brightness temperature must be non-negative".to_string());
}
Ok(t_b_si * temp_to_intensity)
},
move |i_nu_si| {
if i_nu_si < 0.0 {
return Err("spectral radiance must be non-negative".to_string());
}
Ok(i_nu_si * intensity_to_temp)
},
));
}
None
})
}
pub fn brightness_temperature_planck(
frequency: Quantity,
beam_solid_angle: Quantity,
) -> Equivalency {
let nu_hz = frequency.value() * frequency.unit().scale();
let omega_sr = beam_solid_angle.value() * beam_solid_angle.unit().scale();
let h = PLANCK_CONSTANT;
let k = BOLTZMANN_CONSTANT;
let c = SPEED_OF_LIGHT;
let c_squared = c * c;
let h_nu_over_k = h * nu_hz / k;
let two_h_nu3_over_c2 = 2.0 * h * nu_hz.powi(3) / c_squared;
let planck_prefactor_flux = two_h_nu3_over_c2 * omega_sr;
Equivalency::new("brightness_temperature_planck", move |from, to| {
if is_flux_density(from) && is_temperature(to) {
return Some(Converter::new(
move |s_nu_si| {
if s_nu_si <= 0.0 {
return Err(
"flux density must be positive for Planck conversion".to_string()
);
}
let x = planck_prefactor_flux / s_nu_si;
if x <= 0.0 {
return Err("invalid flux density for Planck inversion".to_string());
}
let ln_arg = 1.0 + x;
if ln_arg <= 1.0 {
return Err("numerical error in Planck inversion".to_string());
}
Ok(h_nu_over_k / ln_arg.ln())
},
move |t_b_si| {
if t_b_si <= 0.0 {
return Err("brightness temperature must be positive".to_string());
}
let exp_arg = h_nu_over_k / t_b_si;
if exp_arg > 700.0 {
return Ok(0.0);
}
let denominator = exp_arg.exp() - 1.0;
if denominator <= 0.0 {
return Err("numerical error in Planck function".to_string());
}
Ok(planck_prefactor_flux / denominator)
},
));
}
if is_temperature(from) && is_flux_density(to) {
return Some(Converter::new(
move |t_b_si| {
if t_b_si <= 0.0 {
return Err("brightness temperature must be positive".to_string());
}
let exp_arg = h_nu_over_k / t_b_si;
if exp_arg > 700.0 {
return Ok(0.0);
}
let denominator = exp_arg.exp() - 1.0;
if denominator <= 0.0 {
return Err("numerical error in Planck function".to_string());
}
Ok(planck_prefactor_flux / denominator)
},
move |s_nu_si| {
if s_nu_si <= 0.0 {
return Err(
"flux density must be positive for Planck conversion".to_string()
);
}
let x = planck_prefactor_flux / s_nu_si;
let ln_arg = 1.0 + x;
if ln_arg <= 1.0 {
return Err("numerical error in Planck inversion".to_string());
}
Ok(h_nu_over_k / ln_arg.ln())
},
));
}
None
})
}
pub fn rayleigh_jeans_validity_temperature(frequency_hz: f64, fractional_error: f64) -> f64 {
let h_nu = PLANCK_CONSTANT * frequency_hz;
h_nu / (2.0 * BOLTZMANN_CONSTANT * fractional_error)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::systems::astrophysical::JANSKY;
use crate::systems::si::{GHZ, HZ, K, M, SR, W};
fn spectral_radiance_unit() -> Unit {
W / (&M.pow(2) * HZ * SR)
}
#[test]
fn test_rayleigh_jeans_roundtrip() {
let freq = 1.0e9 * HZ.clone(); let beam = 1e-6 * SR.clone();
let flux = 1.0 * JANSKY.clone();
let temp = flux
.to_equiv(&K, brightness_temperature(freq.clone(), beam.clone()))
.unwrap();
let flux_back = temp
.to_equiv(&JANSKY, brightness_temperature(freq, beam))
.unwrap();
assert!((flux_back.value() - 1.0).abs() < 1e-10);
}
#[test]
fn test_rayleigh_jeans_known_value() {
let freq_hz: f64 = 1.0e9; let omega_sr: f64 = 1e-6; let s_nu_jy: f64 = 1.0;
let s_nu_si = s_nu_jy * 1e-26;
let expected_t = s_nu_si * SPEED_OF_LIGHT.powi(2)
/ (2.0 * BOLTZMANN_CONSTANT * freq_hz.powi(2) * omega_sr);
let freq = freq_hz * HZ.clone();
let beam = omega_sr * SR.clone();
let flux = s_nu_jy * JANSKY.clone();
let temp = flux
.to_equiv(&K, brightness_temperature(freq, beam))
.unwrap();
assert!((temp.value() - expected_t).abs() / expected_t < 1e-10);
}
#[test]
fn test_planck_vs_rayleigh_jeans_high_temp() {
let freq = 1.0e9 * HZ.clone(); let beam = 1e-6 * SR.clone();
let temp_high = 1000.0 * K.clone();
let flux_rj = temp_high
.to_equiv(&JANSKY, brightness_temperature(freq.clone(), beam.clone()))
.unwrap();
let flux_planck = temp_high
.to_equiv(&JANSKY, brightness_temperature_planck(freq, beam))
.unwrap();
let rel_diff = (flux_rj.value() - flux_planck.value()).abs() / flux_rj.value();
assert!(
rel_diff < 0.001,
"R-J and Planck differ by {} at 1000 K",
rel_diff
);
}
#[test]
fn test_planck_low_temp_differs() {
let freq = 100.0e9 * HZ.clone(); let beam = 1e-6 * SR.clone();
let temp_low = 10.0 * K.clone();
let flux_rj = temp_low
.to_equiv(&JANSKY, brightness_temperature(freq.clone(), beam.clone()))
.unwrap();
let flux_planck = temp_low
.to_equiv(&JANSKY, brightness_temperature_planck(freq, beam))
.unwrap();
assert!(flux_planck.value() < flux_rj.value());
}
#[test]
fn test_planck_roundtrip() {
let freq = 345.0e9 * HZ.clone(); let beam = 1e-8 * SR.clone();
let flux = 100.0 * JANSKY.clone();
let temp = flux
.to_equiv(
&K,
brightness_temperature_planck(freq.clone(), beam.clone()),
)
.unwrap();
let flux_back = temp
.to_equiv(&JANSKY, brightness_temperature_planck(freq, beam))
.unwrap();
assert!((flux_back.value() - 100.0).abs() / 100.0 < 1e-10);
}
#[test]
fn test_intensity_equivalency() {
let freq = 1.0e9 * HZ.clone();
let intensity = 1e-20 * spectral_radiance_unit();
let temp = intensity
.to_equiv(&K, brightness_temperature_intensity(freq.clone()))
.unwrap();
let intensity_back = temp
.to_equiv(
&spectral_radiance_unit(),
brightness_temperature_intensity(freq),
)
.unwrap();
assert!((intensity_back.value() - 1e-20).abs() / 1e-20 < 1e-10);
}
#[test]
fn test_rj_validity_temperature() {
let t_min = rayleigh_jeans_validity_temperature(100.0e9, 0.01);
assert!((t_min - 240.0).abs() / 240.0 < 0.1);
}
#[test]
fn test_negative_flux_fails() {
let freq = 1.0e9 * HZ.clone();
let beam = 1e-6 * SR.clone();
let flux = -1.0 * JANSKY.clone();
let result = flux.to_equiv(&K, brightness_temperature(freq, beam));
assert!(result.is_err());
}
#[test]
fn test_negative_temp_fails() {
let freq = 1.0e9 * HZ.clone();
let beam = 1e-6 * SR.clone();
let temp = -100.0 * K.clone();
let result = temp.to_equiv(&JANSKY, brightness_temperature(freq, beam));
assert!(result.is_err());
}
#[test]
fn test_ghz_frequency() {
let freq = 1.4 * GHZ.clone(); let beam = 1e-6 * SR.clone();
let flux = 1.0 * JANSKY.clone();
let temp = flux
.to_equiv(&K, brightness_temperature(freq, beam))
.unwrap();
assert!(temp.value() > 0.0);
assert!(temp.value() < 1e10); }
}