use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug)]
pub enum DetectorError {
InvalidRelativePosition(f64),
InvalidAbsolutePosition(f64),
PositionExceedsColumn { position: f64, column_length: f64 },
}
impl fmt::Display for DetectorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DetectorError::InvalidRelativePosition(r) => {
write!(f, "detector: relative position must be in [0, 1], got {r}")
}
DetectorError::InvalidAbsolutePosition(z) => {
write!(f, "detector: absolute position must be > 0, got {z}")
}
DetectorError::PositionExceedsColumn {
position,
column_length,
} => {
write!(
f,
"detector: position {position} m exceeds column length {column_length} m"
)
}
}
}
}
impl std::error::Error for DetectorError {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DetectorPosition {
Outlet,
Relative(f64),
Absolute(f64),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Detector {
pub position: DetectorPosition,
}
impl Detector {
pub fn new(position: DetectorPosition) -> Result<Self, DetectorError> {
match &position {
DetectorPosition::Outlet => {}
DetectorPosition::Relative(r) => {
if *r < 0.0 || *r > 1.0 {
return Err(DetectorError::InvalidRelativePosition(*r));
}
}
DetectorPosition::Absolute(z) => {
if *z <= 0.0 {
return Err(DetectorError::InvalidAbsolutePosition(*z));
}
}
}
Ok(Self { position })
}
pub fn outlet() -> Self {
Self {
position: DetectorPosition::Outlet,
}
}
pub fn absolute_position(&self, column_length: f64) -> f64 {
match &self.position {
DetectorPosition::Outlet => column_length,
DetectorPosition::Relative(r) => r * column_length,
DetectorPosition::Absolute(z) => *z,
}
}
pub fn validate_against_column(&self, column_length: f64) -> Result<(), DetectorError> {
if let DetectorPosition::Absolute(z) = &self.position
&& *z > column_length
{
return Err(DetectorError::PositionExceedsColumn {
position: *z,
column_length,
});
}
Ok(())
}
pub fn node_index(&self, column_length: f64, n_points: usize) -> usize {
let z = self.absolute_position(column_length);
let dz = column_length / n_points as f64;
let idx = (z / dz).round() as usize;
idx.min(n_points - 1)
}
}
impl Default for Detector {
fn default() -> Self {
Self::outlet()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detector_outlet() {
let det = Detector::outlet();
assert_eq!(det.position, DetectorPosition::Outlet);
}
#[test]
fn test_detector_relative_valid() {
assert!(Detector::new(DetectorPosition::Relative(0.0)).is_ok());
assert!(Detector::new(DetectorPosition::Relative(0.5)).is_ok());
assert!(Detector::new(DetectorPosition::Relative(1.0)).is_ok());
}
#[test]
fn test_detector_relative_invalid() {
assert!(matches!(
Detector::new(DetectorPosition::Relative(-0.1)),
Err(DetectorError::InvalidRelativePosition(_))
));
assert!(matches!(
Detector::new(DetectorPosition::Relative(1.1)),
Err(DetectorError::InvalidRelativePosition(_))
));
}
#[test]
fn test_detector_absolute_valid() {
assert!(Detector::new(DetectorPosition::Absolute(0.1)).is_ok());
}
#[test]
fn test_detector_absolute_invalid() {
assert!(matches!(
Detector::new(DetectorPosition::Absolute(0.0)),
Err(DetectorError::InvalidAbsolutePosition(_))
));
assert!(matches!(
Detector::new(DetectorPosition::Absolute(-0.1)),
Err(DetectorError::InvalidAbsolutePosition(_))
));
}
#[test]
fn test_absolute_position_outlet() {
assert!((Detector::outlet().absolute_position(0.25) - 0.25).abs() < 1e-12);
}
#[test]
fn test_absolute_position_relative() {
let det = Detector::new(DetectorPosition::Relative(0.5)).unwrap();
assert!((det.absolute_position(0.25) - 0.125).abs() < 1e-12);
}
#[test]
fn test_absolute_position_absolute() {
let det = Detector::new(DetectorPosition::Absolute(0.1)).unwrap();
assert!((det.absolute_position(0.25) - 0.1).abs() < 1e-12);
}
#[test]
fn test_validate_against_column_ok() {
let det = Detector::new(DetectorPosition::Absolute(0.1)).unwrap();
assert!(det.validate_against_column(0.25).is_ok());
}
#[test]
fn test_validate_against_column_exceeds() {
let det = Detector::new(DetectorPosition::Absolute(0.3)).unwrap();
assert!(matches!(
det.validate_against_column(0.25),
Err(DetectorError::PositionExceedsColumn { .. })
));
}
#[test]
fn test_validate_outlet_always_ok() {
assert!(Detector::outlet().validate_against_column(0.01).is_ok());
let det = Detector::new(DetectorPosition::Relative(0.9)).unwrap();
assert!(det.validate_against_column(0.01).is_ok());
}
#[test]
fn test_node_index_outlet() {
assert_eq!(Detector::outlet().node_index(0.25, 100), 99);
}
#[test]
fn test_node_index_relative_half() {
let det = Detector::new(DetectorPosition::Relative(0.5)).unwrap();
assert_eq!(det.node_index(0.25, 100), 50);
}
#[test]
fn test_node_index_clamped() {
let det = Detector::new(DetectorPosition::Absolute(0.25)).unwrap();
assert_eq!(det.node_index(0.25, 100), 99);
}
#[test]
fn test_detector_default() {
let det = Detector::default();
assert_eq!(det.position, DetectorPosition::Outlet);
}
#[test]
fn test_detector_serde_roundtrip() {
let det = Detector::new(DetectorPosition::Relative(0.5)).unwrap();
let json = serde_json::to_string(&det).unwrap();
let det2: Detector = serde_json::from_str(&json).unwrap();
assert_eq!(det.position, det2.position);
}
}