#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[repr(C)]
pub struct Neutron {
pub x: f64,
pub y: f64,
pub tof: u32,
pub tot: u16,
pub n_hits: u16,
pub chip_id: u8,
#[doc(hidden)]
pub reserved: [u8; 3],
}
impl Neutron {
#[must_use]
pub fn new(x: f64, y: f64, tof: u32, tot: u16, n_hits: u16, chip_id: u8) -> Self {
Self {
x,
y,
tof,
tot,
n_hits,
chip_id,
reserved: [0; 3],
}
}
#[inline]
#[must_use]
pub fn tof_ns(&self) -> f64 {
f64::from(self.tof) * 25.0
}
#[inline]
#[must_use]
pub fn tof_ms(&self) -> f64 {
self.tof_ns() / 1_000_000.0
}
#[inline]
#[must_use]
pub fn pixel_coords(&self, super_res: f64) -> (f64, f64) {
(self.x / super_res, self.y / super_res)
}
#[must_use]
pub fn cluster_size_category(&self) -> ClusterSize {
match self.n_hits {
1 => ClusterSize::Single,
2..=4 => ClusterSize::Small,
5..=10 => ClusterSize::Medium,
_ => ClusterSize::Large,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ClusterSize {
Single,
Small,
Medium,
Large,
}
#[derive(Clone, Debug, Default)]
pub struct NeutronStatistics {
pub count: usize,
pub mean_tof: f64,
pub std_tof: f64,
pub mean_tot: f64,
pub mean_cluster_size: f64,
pub single_hit_fraction: f64,
pub x_range: (f64, f64),
pub y_range: (f64, f64),
pub tof_range: (u32, u32),
}
#[derive(Clone, Debug, Default)]
pub struct NeutronBatch {
pub x: Vec<f64>,
pub y: Vec<f64>,
pub tof: Vec<u32>,
pub tot: Vec<u16>,
pub n_hits: Vec<u16>,
pub chip_id: Vec<u8>,
}
impl NeutronBatch {
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
x: Vec::with_capacity(capacity),
y: Vec::with_capacity(capacity),
tof: Vec::with_capacity(capacity),
tot: Vec::with_capacity(capacity),
n_hits: Vec::with_capacity(capacity),
chip_id: Vec::with_capacity(capacity),
}
}
#[must_use]
pub fn len(&self) -> usize {
self.x.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.x.is_empty()
}
pub fn push(&mut self, neutron: Neutron) {
self.x.push(neutron.x);
self.y.push(neutron.y);
self.tof.push(neutron.tof);
self.tot.push(neutron.tot);
self.n_hits.push(neutron.n_hits);
self.chip_id.push(neutron.chip_id);
}
pub fn append(&mut self, other: &NeutronBatch) {
self.x.extend_from_slice(&other.x);
self.y.extend_from_slice(&other.y);
self.tof.extend_from_slice(&other.tof);
self.tot.extend_from_slice(&other.tot);
self.n_hits.extend_from_slice(&other.n_hits);
self.chip_id.extend_from_slice(&other.chip_id);
}
pub fn clear(&mut self) {
self.x.clear();
self.y.clear();
self.tof.clear();
self.tot.clear();
self.n_hits.clear();
self.chip_id.clear();
}
}
impl NeutronStatistics {
pub fn from_neutrons(neutrons: &[Neutron]) -> Self {
if neutrons.is_empty() {
return Self::default();
}
let count = neutrons.len();
let count_u32_value = u32::try_from(count).unwrap_or(u32::MAX);
let count_as_f64 = f64::from(count_u32_value);
let sum_tof: f64 = neutrons.iter().map(|n| f64::from(n.tof)).sum();
let mean_tof = sum_tof / count_as_f64;
let variance: f64 = neutrons
.iter()
.map(|n| (f64::from(n.tof) - mean_tof).powi(2))
.sum::<f64>()
/ count_as_f64;
let std_tof = variance.sqrt();
let mean_total_tot = neutrons.iter().map(|n| f64::from(n.tot)).sum::<f64>() / count_as_f64;
let mean_cluster_size =
neutrons.iter().map(|n| f64::from(n.n_hits)).sum::<f64>() / count_as_f64;
let single_hit_count =
u32::try_from(neutrons.iter().filter(|n| n.n_hits == 1).count()).unwrap_or(u32::MAX);
let single_hit_fraction = f64::from(single_hit_count) / count_as_f64;
let x_min = neutrons.iter().map(|n| n.x).fold(f64::INFINITY, f64::min);
let x_max = neutrons
.iter()
.map(|n| n.x)
.fold(f64::NEG_INFINITY, f64::max);
let y_min = neutrons.iter().map(|n| n.y).fold(f64::INFINITY, f64::min);
let y_max = neutrons
.iter()
.map(|n| n.y)
.fold(f64::NEG_INFINITY, f64::max);
let tof_min = neutrons.iter().map(|n| n.tof).min().unwrap_or(0);
let tof_max = neutrons.iter().map(|n| n.tof).max().unwrap_or(0);
Self {
count,
mean_tof,
std_tof,
mean_tot: mean_total_tot,
mean_cluster_size,
single_hit_fraction,
x_range: (x_min, x_max),
y_range: (y_min, y_max),
tof_range: (tof_min, tof_max),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_neutron_creation() {
let neutron = Neutron::new(1024.0, 2048.0, 1000, 150, 5, 0);
assert!((neutron.x - 1024.0).abs() < f64::EPSILON);
assert!((neutron.y - 2048.0).abs() < f64::EPSILON);
assert_eq!(neutron.tof, 1000);
assert_eq!(neutron.tot, 150);
assert_eq!(neutron.n_hits, 5);
}
#[test]
fn test_tof_conversions() {
let neutron = Neutron::new(0.0, 0.0, 1000, 0, 1, 0);
assert!((neutron.tof_ns() - 25_000.0).abs() < f64::EPSILON);
assert!((neutron.tof_ms() - 0.025).abs() < f64::EPSILON);
}
#[test]
fn test_pixel_coords() {
let neutron = Neutron::new(800.0, 1600.0, 0, 0, 1, 0);
let (px, py) = neutron.pixel_coords(8.0);
assert!((px - 100.0).abs() < f64::EPSILON);
assert!((py - 200.0).abs() < f64::EPSILON);
}
#[test]
fn test_cluster_size_category() {
assert_eq!(
Neutron::new(0.0, 0.0, 0, 0, 1, 0).cluster_size_category(),
ClusterSize::Single
);
assert_eq!(
Neutron::new(0.0, 0.0, 0, 0, 3, 0).cluster_size_category(),
ClusterSize::Small
);
assert_eq!(
Neutron::new(0.0, 0.0, 0, 0, 7, 0).cluster_size_category(),
ClusterSize::Medium
);
assert_eq!(
Neutron::new(0.0, 0.0, 0, 0, 15, 0).cluster_size_category(),
ClusterSize::Large
);
}
#[test]
fn test_statistics() {
let neutrons = vec![
Neutron::new(100.0, 200.0, 1000, 50, 1, 0),
Neutron::new(110.0, 210.0, 1010, 60, 3, 0),
Neutron::new(105.0, 205.0, 1005, 55, 2, 0),
];
let stats = NeutronStatistics::from_neutrons(&neutrons);
assert_eq!(stats.count, 3);
assert!((stats.mean_tof - 1005.0).abs() < 0.01);
assert!((stats.single_hit_fraction - 1.0 / 3.0).abs() < 0.01);
}
}