use core::marker::PhantomData;
use affn::{Position, ReferenceCenter, ReferenceFrame};
use qtty::dimensionless::{Albedos, Ratio};
use qtty::length::LengthUnit;
use qtty::unit::Per;
use qtty::Quantity;
pub type InverseLength<U> = Per<Ratio, U>;
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum OpticalCoefficientError {
#[error("{field} must be finite (got {value})")]
NonFinite {
field: &'static str,
value: f64,
},
#[error("{field} must be non-negative (got {value})")]
Negative {
field: &'static str,
value: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OpticalCoefficients<U: LengthUnit> {
pub sigma_a: Quantity<InverseLength<U>>,
pub sigma_s: Quantity<InverseLength<U>>,
pub sigma_t: Quantity<InverseLength<U>>,
pub ssa: Albedos,
_unit: PhantomData<U>,
}
impl<U: LengthUnit> OpticalCoefficients<U> {
pub fn try_new(sigma_a: f64, sigma_s: f64) -> Result<Self, OpticalCoefficientError> {
check_coeff("sigma_a", sigma_a)?;
check_coeff("sigma_s", sigma_s)?;
let sigma_t = sigma_a + sigma_s;
let ssa = if sigma_t == 0.0 {
0.0
} else {
sigma_s / sigma_t
};
Ok(Self {
sigma_a: Quantity::new(sigma_a),
sigma_s: Quantity::new(sigma_s),
sigma_t: Quantity::new(sigma_t),
ssa: Albedos::new(ssa),
_unit: PhantomData,
})
}
#[must_use]
pub fn transparent() -> Self {
Self::try_new(0.0, 0.0).expect("zero coefficients are always valid")
}
}
fn check_coeff(field: &'static str, value: f64) -> Result<(), OpticalCoefficientError> {
if !value.is_finite() {
return Err(OpticalCoefficientError::NonFinite { field, value });
}
if value < 0.0 {
return Err(OpticalCoefficientError::Negative { field, value });
}
Ok(())
}
pub trait Medium<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> {
fn coefficients(
&self,
p: Position<C, F, U>,
wavelength: qtty::length::Nanometers,
) -> OpticalCoefficients<U>;
}
pub trait TryMedium<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> {
type Error;
fn try_coefficients(
&self,
p: Position<C, F, U>,
wavelength: qtty::length::Nanometers,
) -> Result<OpticalCoefficients<U>, Self::Error>;
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HomogeneousMedium<U: LengthUnit> {
sigma_a: Quantity<InverseLength<U>>,
sigma_s: Quantity<InverseLength<U>>,
}
impl<U: LengthUnit> HomogeneousMedium<U> {
pub fn try_new(sigma_a: f64, sigma_s: f64) -> Result<Self, OpticalCoefficientError> {
check_coeff("sigma_a", sigma_a)?;
check_coeff("sigma_s", sigma_s)?;
Ok(Self {
sigma_a: Quantity::new(sigma_a),
sigma_s: Quantity::new(sigma_s),
})
}
#[must_use]
pub fn sigma_a(&self) -> Quantity<InverseLength<U>> {
self.sigma_a
}
#[must_use]
pub fn sigma_s(&self) -> Quantity<InverseLength<U>> {
self.sigma_s
}
#[must_use]
pub fn transparent() -> Self {
Self::try_new(0.0, 0.0).expect("zero coefficients are always valid")
}
}
impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Medium<C, F, U>
for HomogeneousMedium<U>
{
fn coefficients(
&self,
_p: Position<C, F, U>,
_wavelength: qtty::length::Nanometers,
) -> OpticalCoefficients<U> {
OpticalCoefficients::try_new(self.sigma_a.value(), self.sigma_s.value())
.expect("HomogeneousMedium fields are validated at construction")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transparent_coefficients_are_zero() {
let coeffs = OpticalCoefficients::<qtty::unit::Kilometer>::transparent();
assert_eq!(coeffs.sigma_t.value(), 0.0);
assert_eq!(coeffs.ssa.value(), 0.0);
}
#[test]
fn try_new_rejects_negative_inputs() {
let r = OpticalCoefficients::<qtty::unit::Kilometer>::try_new(-1.0, 0.0);
assert!(matches!(r, Err(OpticalCoefficientError::Negative { .. })));
}
#[test]
fn try_new_rejects_nan_inputs() {
let r = OpticalCoefficients::<qtty::unit::Kilometer>::try_new(f64::NAN, 0.0);
assert!(matches!(r, Err(OpticalCoefficientError::NonFinite { .. })));
}
#[test]
fn homogeneous_medium_rejects_negative() {
assert!(HomogeneousMedium::<qtty::unit::Kilometer>::try_new(0.0, -0.1).is_err());
}
}