use crate::qtty::radiometry::{
WattPerSquareMeterSteradianNanometer, WattsPerSquareMeterSteradianNanometer,
};
use crate::qtty::{Albedos, IlluminationFractions};
use crate::qtty::{Kilometers, Nanometers, Radians};
#[cfg(not(feature = "std"))]
use crate::qtty::{Real, Scalar, Transcendental};
pub const MEAN_MOON_RADIUS: Kilometers = Kilometers::new(1_737.4);
pub const MEAN_MOON_DISTANCE: Kilometers = Kilometers::new(384_400.0);
pub fn lunar_full_moon_albedo_jones2013(wavelength: Nanometers) -> Albedos {
let lambda = wavelength
.clamp(Nanometers::new(300.0), Nanometers::new(1_100.0))
.value();
let t = (lambda - 300.0) / 800.0;
Albedos::new(0.075 + 0.065 * t)
}
pub fn lunar_phase_attenuation_jones2013(phase_angle: Radians) -> IlluminationFractions {
let a = phase_angle.abs().value().to_degrees();
IlluminationFractions::new(10f64.powf(-0.4 * (0.026 * a + 4.0e-9 * a.powi(4))))
}
pub fn lunar_albedo_jones2013(phase_angle: Radians, wavelength: Nanometers) -> Albedos {
let a = lunar_full_moon_albedo_jones2013(wavelength).value();
let p = lunar_phase_attenuation_jones2013(phase_angle).value();
Albedos::new(a * p)
}
pub type LunarSpectralIrradianceUnit = crate::qtty::Per<
crate::qtty::Per<crate::qtty::unit::Watt, crate::qtty::unit::SquareMeter>,
crate::qtty::unit::Nanometer,
>;
pub fn reflected_lunar_spectral_irradiance_jones2013(
solar_irradiance_w_m2_nm: f64,
wavelength: Nanometers,
phase_angle: Radians,
moon_distance: Kilometers,
) -> crate::qtty::Quantity<LunarSpectralIrradianceUnit> {
let distance = moon_distance.value();
if !solar_irradiance_w_m2_nm.is_finite()
|| !wavelength.is_finite()
|| !phase_angle.is_finite()
|| !distance.is_finite()
|| distance <= 0.0
{
return crate::qtty::Quantity::new(f64::NAN);
}
let albedo = lunar_albedo_jones2013(phase_angle, wavelength).value();
let radius_ratio: f64 = MEAN_MOON_RADIUS / moon_distance;
crate::qtty::Quantity::new(solar_irradiance_w_m2_nm * radius_ratio.powi(2) * albedo)
}
pub fn reflected_lunar_spectral_radiance_jones2013(
solar_irradiance_w_m2_nm: f64,
wavelength: Nanometers,
phase_angle: Radians,
moon_distance: Kilometers,
) -> WattsPerSquareMeterSteradianNanometer {
WattsPerSquareMeterSteradianNanometer::new(
reflected_lunar_spectral_irradiance_jones2013(
solar_irradiance_w_m2_nm,
wavelength,
phase_angle,
moon_distance,
)
.value(),
)
}
pub type LunarSpectralRadianceUnit = WattPerSquareMeterSteradianNanometer;
#[cfg(test)]
mod tests {
use super::*;
use crate::qtty::{unit::Radian, Degrees};
#[test]
fn albedo_increases_from_blue_to_red() {
let blue = lunar_full_moon_albedo_jones2013(Nanometers::new(400.0));
let red = lunar_full_moon_albedo_jones2013(Nanometers::new(800.0));
assert!(red.value() > blue.value());
assert!(blue.value() > 0.0);
}
#[test]
fn phase_attenuation_is_one_at_full_and_small_near_new() {
let full = lunar_phase_attenuation_jones2013(Degrees::new(0.0).to::<Radian>());
let new = lunar_phase_attenuation_jones2013(Degrees::new(180.0).to::<Radian>());
assert!((full.value() - 1.0).abs() < 1.0e-12);
assert!(new.value() < 1.0e-3);
}
#[test]
fn reflected_radiance_is_positive_for_full_moon() {
let l = reflected_lunar_spectral_radiance_jones2013(
1.8,
Nanometers::new(550.0),
Degrees::new(0.0).to::<Radian>(),
MEAN_MOON_DISTANCE,
);
assert!(l.value() > 0.0);
}
#[test]
fn beam_irradiance_obeys_disc_geometry_and_inverse_square_law() {
let wave = Nanometers::new(551.0);
let phase = Degrees::new(30.0).to::<Radian>();
let albedo = lunar_albedo_jones2013(phase, wave).value();
let reference =
reflected_lunar_spectral_irradiance_jones2013(1.8, wave, phase, MEAN_MOON_DISTANCE);
for d in [350_000.0, 384_400.0, 405_000.0, 768_800.0] {
let actual =
reflected_lunar_spectral_irradiance_jones2013(1.8, wave, phase, Kilometers::new(d));
let disc_radiance = 1.8 * albedo / core::f64::consts::PI;
let omega = core::f64::consts::PI * (1_737.4 / d).powi(2);
assert!((actual.value() / (disc_radiance * omega) - 1.0).abs() < 1e-14);
assert!((actual.value() / reference.value() - (384_400.0 / d).powi(2)).abs() < 1e-14);
let legacy =
reflected_lunar_spectral_radiance_jones2013(1.8, wave, phase, Kilometers::new(d));
assert_eq!(legacy.value(), actual.value());
}
let band = reference * Nanometers::new(10.0);
assert!((band.value() / reference.value() - 10.0).abs() < 1e-14);
}
#[test]
fn lunar_beam_zero_illumination_and_invalid_inputs() {
let wave = Nanometers::new(550.0);
let phase = Radians::new(0.0);
assert_eq!(
reflected_lunar_spectral_irradiance_jones2013(0.0, wave, phase, MEAN_MOON_DISTANCE,)
.value(),
0.0
);
for distance in [0.0, -1.0, f64::NAN, f64::INFINITY] {
assert!(reflected_lunar_spectral_irradiance_jones2013(
1.0,
wave,
phase,
Kilometers::new(distance),
)
.value()
.is_nan());
}
assert!(reflected_lunar_spectral_irradiance_jones2013(
1.0,
Nanometers::new(f64::NAN),
phase,
MEAN_MOON_DISTANCE,
)
.value()
.is_nan());
assert!(reflected_lunar_spectral_irradiance_jones2013(
1.0,
wave,
Radians::new(f64::NAN),
MEAN_MOON_DISTANCE,
)
.value()
.is_nan());
}
}