Skip to main content

rustpix_core/
clustering.rs

1//! Clustering algorithm traits and configuration.
2//!
3
4// Re-export ClusteringError for convenience
5pub use crate::error::ClusteringError;
6
7/// Configuration for clustering algorithms.
8///
9/// This is a generic configuration that all clustering algorithms accept.
10/// Algorithm-specific configurations extend this.
11#[derive(Clone, Debug)]
12pub struct ClusteringConfig {
13    /// Spatial radius for neighbor detection (pixels).
14    pub radius: f64,
15    /// Temporal correlation window (nanoseconds).
16    pub temporal_window_ns: f64,
17    /// Minimum cluster size to keep.
18    pub min_cluster_size: u16,
19    /// Maximum cluster size (None = unlimited).
20    pub max_cluster_size: Option<u16>,
21}
22
23impl Default for ClusteringConfig {
24    fn default() -> Self {
25        Self {
26            radius: 5.0,
27            temporal_window_ns: 75.0,
28            min_cluster_size: 1,
29            max_cluster_size: None,
30        }
31    }
32}
33
34impl ClusteringConfig {
35    /// Create VENUS/SNS default configuration.
36    #[must_use]
37    pub fn venus_defaults() -> Self {
38        Self::default()
39    }
40
41    /// Temporal window in TOF units (25ns).
42    #[inline]
43    #[must_use]
44    pub fn window_tof(&self) -> u32 {
45        let window = (self.temporal_window_ns / 25.0).ceil();
46        if window <= 0.0 {
47            return 0;
48        }
49        if window >= f64::from(u32::MAX) {
50            return u32::MAX;
51        }
52        format!("{window:.0}").parse::<u32>().unwrap_or(u32::MAX)
53    }
54
55    /// Set spatial radius.
56    #[must_use]
57    pub fn with_radius(mut self, radius: f64) -> Self {
58        self.radius = radius;
59        self
60    }
61
62    /// Set temporal window.
63    #[must_use]
64    pub fn with_temporal_window_ns(mut self, window_ns: f64) -> Self {
65        self.temporal_window_ns = window_ns;
66        self
67    }
68
69    /// Set minimum cluster size.
70    #[must_use]
71    pub fn with_min_cluster_size(mut self, size: u16) -> Self {
72        self.min_cluster_size = size;
73        self
74    }
75
76    /// Set maximum cluster size.
77    #[must_use]
78    pub fn with_max_cluster_size(mut self, size: u16) -> Self {
79        self.max_cluster_size = Some(size);
80        self
81    }
82}
83
84/// Statistics from a clustering operation.
85#[derive(Clone, Debug, Default)]
86pub struct ClusteringStatistics {
87    /// Total number of hits processed.
88    pub hits_processed: usize,
89    /// Number of clusters found.
90    pub clusters_found: usize,
91    /// Number of hits classified as noise.
92    pub noise_hits: usize,
93    /// Size of the largest cluster encountered.
94    pub largest_cluster_size: usize,
95    /// Mean size of clusters.
96    pub mean_cluster_size: f64,
97    /// Processing time in microseconds.
98    pub processing_time_us: u64,
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_config_defaults() {
107        let config = ClusteringConfig::default();
108        assert!((config.radius - 5.0).abs() < f64::EPSILON);
109        assert!((config.temporal_window_ns - 75.0).abs() < f64::EPSILON);
110        assert_eq!(config.min_cluster_size, 1);
111        assert_eq!(config.max_cluster_size, None);
112    }
113
114    #[test]
115    fn test_window_tof_conversion() {
116        let config = ClusteringConfig::default();
117        // 75ns / 25ns = 3
118        assert_eq!(config.window_tof(), 3);
119    }
120
121    #[test]
122    fn test_config_builder() {
123        let config = ClusteringConfig::default()
124            .with_radius(10.0)
125            .with_temporal_window_ns(100.0)
126            .with_min_cluster_size(2)
127            .with_max_cluster_size(100);
128
129        assert!((config.radius - 10.0).abs() < f64::EPSILON);
130        assert!((config.temporal_window_ns - 100.0).abs() < f64::EPSILON);
131        assert_eq!(config.min_cluster_size, 2);
132        assert_eq!(config.max_cluster_size, Some(100));
133    }
134}