use eulumdat::{Eulumdat, Symmetry};
#[derive(Debug, Clone)]
pub struct PhotometricWeb {
c_angles: Vec<f64>,
g_angles: Vec<f64>,
intensities: Vec<Vec<f64>>,
symmetry: Symmetry,
max_intensity: f64,
min_intensity: f64,
}
impl PhotometricWeb {
pub fn new(
c_angles: Vec<f64>,
g_angles: Vec<f64>,
intensities: Vec<Vec<f64>>,
symmetry: Symmetry,
) -> Self {
let max_intensity = intensities
.iter()
.flat_map(|row| row.iter())
.copied()
.fold(0.0, f64::max);
let min_intensity = intensities
.iter()
.flat_map(|row| row.iter())
.copied()
.fold(f64::MAX, f64::min);
Self {
c_angles,
g_angles,
intensities,
symmetry,
max_intensity,
min_intensity,
}
}
pub fn sample(&self, c_angle: f64, g_angle: f64) -> f64 {
let c_normalized = c_angle.rem_euclid(360.0);
let g_clamped = g_angle.clamp(0.0, 180.0);
let effective_c = self.apply_symmetry(c_normalized);
let (ci, cf) = self.find_interpolation_index(&self.c_angles, effective_c);
let (gi, gf) = self.find_interpolation_index(&self.g_angles, g_clamped);
self.bilinear_interpolate(ci, cf, gi, gf)
}
pub fn sample_normalized(&self, c_angle: f64, g_angle: f64) -> f64 {
if self.max_intensity <= 0.0 {
return 0.0;
}
self.sample(c_angle, g_angle) / self.max_intensity
}
pub fn max_intensity(&self) -> f64 {
self.max_intensity
}
pub fn min_intensity(&self) -> f64 {
self.min_intensity
}
pub fn symmetry(&self) -> Symmetry {
self.symmetry
}
pub fn c_angles(&self) -> &[f64] {
&self.c_angles
}
pub fn g_angles(&self) -> &[f64] {
&self.g_angles
}
fn apply_symmetry(&self, c_normalized: f64) -> f64 {
match self.symmetry {
Symmetry::None => c_normalized,
Symmetry::VerticalAxis => 0.0, Symmetry::PlaneC0C180 => {
if c_normalized <= 180.0 {
c_normalized
} else {
360.0 - c_normalized
}
}
Symmetry::PlaneC90C270 => {
if c_normalized > 180.0 {
360.0 - c_normalized } else {
c_normalized }
}
Symmetry::BothPlanes => {
let in_first_half = c_normalized <= 180.0;
let c_in_half = if in_first_half {
c_normalized
} else {
360.0 - c_normalized
};
if c_in_half <= 90.0 {
c_in_half
} else {
180.0 - c_in_half
}
}
}
}
fn find_interpolation_index(&self, angles: &[f64], target: f64) -> (usize, f64) {
if angles.is_empty() {
return (0, 0.0);
}
if target <= angles[0] {
return (0, 0.0);
}
if target >= angles[angles.len() - 1] {
return (angles.len() - 1, 0.0);
}
for i in 0..angles.len() - 1 {
if target >= angles[i] && target <= angles[i + 1] {
let fraction = (target - angles[i]) / (angles[i + 1] - angles[i]);
return (i, fraction);
}
}
(angles.len() - 1, 0.0)
}
fn bilinear_interpolate(&self, ci: usize, cf: f64, gi: usize, gf: f64) -> f64 {
let get = |c: usize, g: usize| -> f64 {
self.intensities
.get(c)
.and_then(|row| row.get(g))
.copied()
.unwrap_or(0.0)
};
let i00 = get(ci, gi);
let i01 = get(ci, gi + 1);
let i10 = get(ci + 1, gi);
let i11 = get(ci + 1, gi + 1);
let i0 = i00 * (1.0 - gf) + i01 * gf;
let i1 = i10 * (1.0 - gf) + i11 * gf;
i0 * (1.0 - cf) + i1 * cf
}
}
impl From<&Eulumdat> for PhotometricWeb {
fn from(ldt: &Eulumdat) -> Self {
Self::new(
ldt.c_angles.clone(),
ldt.g_angles.clone(),
ldt.intensities.clone(),
ldt.symmetry,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_web() -> PhotometricWeb {
PhotometricWeb::new(
vec![0.0, 90.0, 180.0, 270.0],
vec![0.0, 45.0, 90.0, 135.0, 180.0],
vec![
vec![100.0, 80.0, 50.0, 30.0, 10.0], vec![90.0, 70.0, 40.0, 25.0, 8.0], vec![80.0, 60.0, 30.0, 20.0, 5.0], vec![85.0, 65.0, 35.0, 22.0, 6.0], ],
Symmetry::None,
)
}
#[test]
fn test_sample_exact_angles() {
let web = create_test_web();
let i = web.sample(0.0, 0.0);
assert!((i - 100.0).abs() < 0.001);
let i = web.sample(90.0, 45.0);
assert!((i - 70.0).abs() < 0.001);
let i = web.sample(180.0, 90.0);
assert!((i - 30.0).abs() < 0.001);
}
#[test]
fn test_sample_interpolated() {
let web = PhotometricWeb::new(
vec![0.0, 90.0],
vec![0.0, 90.0],
vec![
vec![100.0, 0.0], vec![100.0, 0.0], ],
Symmetry::None,
);
let i = web.sample(0.0, 45.0);
assert!((i - 50.0).abs() < 0.001);
}
#[test]
fn test_sample_normalized() {
let web = create_test_web();
let n = web.sample_normalized(0.0, 0.0);
assert!((n - 1.0).abs() < 0.001);
let n = web.sample_normalized(0.0, 90.0);
assert!((n - 0.5).abs() < 0.001);
}
#[test]
fn test_symmetry_both_planes() {
let web = PhotometricWeb::new(
vec![0.0, 45.0, 90.0],
vec![0.0, 45.0, 90.0],
vec![
vec![100.0, 80.0, 50.0], vec![90.0, 70.0, 40.0], vec![80.0, 60.0, 30.0], ],
Symmetry::BothPlanes,
);
let i_c0 = web.sample(0.0, 45.0);
let i_c180 = web.sample(180.0, 45.0);
assert!((i_c0 - i_c180).abs() < 0.001);
let i_c90 = web.sample(90.0, 45.0);
let i_c270 = web.sample(270.0, 45.0);
assert!((i_c90 - i_c270).abs() < 0.001);
}
#[test]
fn test_from_eulumdat() {
let ldt = Eulumdat {
c_angles: vec![0.0, 90.0],
g_angles: vec![0.0, 90.0],
intensities: vec![vec![100.0, 50.0], vec![80.0, 40.0]],
symmetry: Symmetry::None,
..Default::default()
};
let web = PhotometricWeb::from(&ldt);
assert_eq!(web.max_intensity(), 100.0);
assert_eq!(web.c_angles().len(), 2);
}
}