use nalgebra::Vector3;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct Detector {
bins: Vec<Vec<f64>>,
counts: Vec<Vec<u64>>,
c_resolution_deg: f64,
g_resolution_deg: f64,
num_c: usize,
num_g: usize,
total_energy: f64,
}
impl Detector {
pub fn new(c_resolution_deg: f64, g_resolution_deg: f64) -> Self {
let num_c = (360.0 / c_resolution_deg).round() as usize;
let num_g = (180.0 / g_resolution_deg).round() as usize + 1;
Self {
bins: vec![vec![0.0; num_g]; num_c],
counts: vec![vec![0; num_g]; num_c],
c_resolution_deg,
g_resolution_deg,
num_c,
num_g,
total_energy: 0.0,
}
}
pub fn record(&mut self, direction: &Vector3<f64>, energy: f64) {
let (c, g) = direction_to_cg(direction);
let ci = ((c / self.c_resolution_deg).floor() as usize).min(self.num_c - 1);
let gi = ((g / self.g_resolution_deg).round() as usize).min(self.num_g - 1);
self.bins[ci][gi] += energy;
self.counts[ci][gi] += 1;
self.total_energy += energy;
}
#[allow(clippy::needless_range_loop)]
pub fn to_candela(&self, source_flux_lm: f64) -> Vec<Vec<f64>> {
if self.total_energy <= 0.0 {
return self.bins.clone();
}
let dc_rad = self.c_resolution_deg.to_radians();
let dg_rad = self.g_resolution_deg.to_radians();
let flux_per_energy = source_flux_lm / self.total_energy;
let mut candela = vec![vec![0.0; self.num_g]; self.num_c];
for ci in 0..self.num_c {
for gi in 0..self.num_g {
let g_center_rad = (gi as f64 * self.g_resolution_deg).to_radians();
let solid_angle = solid_angle_for_bin(g_center_rad, dg_rad, dc_rad);
if solid_angle > 0.0 {
let flux_in_bin = self.bins[ci][gi] * flux_per_energy;
candela[ci][gi] = flux_in_bin / solid_angle;
}
}
}
candela
}
pub fn total_flux(&self, source_flux_lm: f64) -> f64 {
if self.total_energy <= 0.0 {
return 0.0;
}
self.total_energy * (source_flux_lm / self.total_energy)
}
pub fn total_energy(&self) -> f64 {
self.total_energy
}
pub fn num_c(&self) -> usize {
self.num_c
}
pub fn num_g(&self) -> usize {
self.num_g
}
pub fn c_resolution_deg(&self) -> f64 {
self.c_resolution_deg
}
pub fn g_resolution_deg(&self) -> f64 {
self.g_resolution_deg
}
pub fn bins(&self) -> &Vec<Vec<f64>> {
&self.bins
}
pub fn counts(&self) -> &Vec<Vec<u64>> {
&self.counts
}
pub fn merge(&mut self, other: &Detector) {
assert_eq!(self.num_c, other.num_c);
assert_eq!(self.num_g, other.num_g);
for ci in 0..self.num_c {
for gi in 0..self.num_g {
self.bins[ci][gi] += other.bins[ci][gi];
self.counts[ci][gi] += other.counts[ci][gi];
}
}
self.total_energy += other.total_energy;
}
pub fn candela_at(&self, c_deg: f64, g_deg: f64, source_flux_lm: f64) -> f64 {
if self.total_energy <= 0.0 {
return 0.0;
}
let flux_per_energy = source_flux_lm / self.total_energy;
let dc_rad = self.c_resolution_deg.to_radians();
let dg_rad = self.g_resolution_deg.to_radians();
let c_norm = c_deg.rem_euclid(360.0);
let ci_f = c_norm / self.c_resolution_deg;
let gi_f = g_deg / self.g_resolution_deg;
let ci0 = (ci_f.floor() as usize).min(self.num_c - 1);
let ci1 = (ci0 + 1) % self.num_c;
let gi0 = (gi_f.floor() as usize).min(self.num_g - 1);
let gi1 = (gi0 + 1).min(self.num_g - 1);
let cf = ci_f - ci_f.floor(); let gf = gi_f - gi_f.floor();
let cd = |ci: usize, gi: usize| -> f64 {
let g_center_rad = (gi as f64 * self.g_resolution_deg).to_radians();
let sa = solid_angle_for_bin(g_center_rad, dg_rad, dc_rad);
if sa > 0.0 {
self.bins[ci][gi] * flux_per_energy / sa
} else {
0.0
}
};
let v00 = cd(ci0, gi0);
let v10 = cd(ci1, gi0);
let v01 = cd(ci0, gi1);
let v11 = cd(ci1, gi1);
let v0 = v00 * (1.0 - cf) + v10 * cf;
let v1 = v01 * (1.0 - cf) + v11 * cf;
v0 * (1.0 - gf) + v1 * gf
}
pub fn resample(&self, c_step_deg: f64, g_step_deg: f64) -> Detector {
let mut resampled = Detector::new(c_step_deg, g_step_deg);
for ci in 0..self.num_c {
let c_angle = ci as f64 * self.c_resolution_deg;
let new_ci = ((c_angle / c_step_deg).floor() as usize).min(resampled.num_c - 1);
for gi in 0..self.num_g {
let g_angle = gi as f64 * self.g_resolution_deg;
let new_gi = ((g_angle / g_step_deg).round() as usize).min(resampled.num_g - 1);
resampled.bins[new_ci][new_gi] += self.bins[ci][gi];
resampled.counts[new_ci][new_gi] += self.counts[ci][gi];
}
}
resampled.total_energy = self.total_energy;
resampled
}
}
fn direction_to_cg(dir: &Vector3<f64>) -> (f64, f64) {
let gamma = (-dir.z).acos().to_degrees();
let c = dir.y.atan2(dir.x).to_degrees();
let c = if c < 0.0 { c + 360.0 } else { c };
(c, gamma)
}
fn solid_angle_for_bin(g_center_rad: f64, dg_rad: f64, dc_rad: f64) -> f64 {
let g_lo = (g_center_rad - dg_rad / 2.0).max(0.0);
let g_hi = (g_center_rad + dg_rad / 2.0).min(PI);
let d_cos = (g_lo.cos() - g_hi.cos()).abs();
dc_rad * d_cos
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn direction_to_cg_nadir() {
let (c, g) = direction_to_cg(&Vector3::new(0.0, 0.0, -1.0));
assert!(g.abs() < 1.0, "Nadir should be gamma ~0, got {g}");
let _ = c; }
#[test]
fn direction_to_cg_zenith() {
let (_, g) = direction_to_cg(&Vector3::new(0.0, 0.0, 1.0));
assert!(
(g - 180.0).abs() < 1.0,
"Zenith should be gamma ~180, got {g}"
);
}
#[test]
fn direction_to_cg_horizontal_front() {
let (c, g) = direction_to_cg(&Vector3::new(1.0, 0.0, 0.0));
assert!(
(g - 90.0).abs() < 1.0,
"Horizontal should be gamma ~90, got {g}"
);
assert!(c.abs() < 1.0 || (c - 360.0).abs() < 1.0, "C0 = +X, got {c}");
}
#[test]
fn direction_to_cg_horizontal_right() {
let (c, g) = direction_to_cg(&Vector3::new(0.0, 1.0, 0.0));
assert!((g - 90.0).abs() < 1.0);
assert!((c - 90.0).abs() < 1.0, "C90 = +Y, got {c}");
}
#[test]
fn detector_records_and_merges() {
let mut d1 = Detector::new(10.0, 5.0);
let mut d2 = Detector::new(10.0, 5.0);
d1.record(&Vector3::new(0.0, 0.0, -1.0), 1.0);
d2.record(&Vector3::new(0.0, 0.0, -1.0), 2.0);
d1.merge(&d2);
assert!((d1.total_energy - 3.0).abs() < 1e-10);
}
#[test]
fn solid_angle_equator_larger_than_pole() {
let dg = 1.0f64.to_radians();
let dc = 1.0f64.to_radians();
let sa_equator = solid_angle_for_bin(90.0f64.to_radians(), dg, dc);
let sa_pole = solid_angle_for_bin(0.0, dg, dc);
assert!(sa_equator > sa_pole);
}
}