dspk_synapse/streaming/
config.rs1use dspk_ops::peaks::DistanceRule;
4use dspk_ops::pipeline::Pipeline;
5use dspk_core::{DspError, DspResult};
6
7use crate::detection::SpikePolarity;
8
9pub const SINC_RESAMPLE_MARGIN: usize = crate::extraction::SINC_KERNEL_RADIUS;
12
13const MIN_WINDOW_MS: f64 = 0.1;
15const MIN_BATCH_SEC: f64 = 0.5;
16const MIN_CALIBRATION_SEC: f64 = 0.1;
17const MIN_SAMPLE_RATE_HZ: f64 = 1.0;
19
20#[derive(Debug, Clone)]
22pub struct StreamingDetectionConfig {
23 pub batch_duration_sec: f64,
25 pub calibration_duration_sec: f64,
28 pub calibration_chunks: usize,
31 pub threshold_factor: f32,
33 pub refractory_ms: f64,
35 pub polarity: SpikePolarity,
37 pub distance_rule: DistanceRule,
41 pub spatial_radius_um: f32,
43 pub k_neighbors: usize,
45 pub pre_ms: f64,
47 pub post_ms: f64,
49 pub apply_sinc_shift: bool,
51 pub locate: bool,
54}
55
56impl Default for StreamingDetectionConfig {
57 fn default() -> Self {
58 Self {
59 batch_duration_sec: 10.0,
60 calibration_duration_sec: 5.0,
61 calibration_chunks: 5,
62 threshold_factor: 5.0,
63 refractory_ms: 1.0,
64 polarity: SpikePolarity::Negative,
65 distance_rule: DistanceRule::LocallyExclusive,
66 spatial_radius_um: 150.0,
67 k_neighbors: 4,
68 pre_ms: 1.0,
69 post_ms: 2.0,
70 apply_sinc_shift: true,
71 locate: true,
72 }
73 }
74}
75
76impl StreamingDetectionConfig {
77 pub fn threshold_detector(&self) -> crate::detection::ThresholdSpikeDetector {
80 crate::detection::ThresholdSpikeDetector {
81 threshold_factor: self.threshold_factor,
82 refractory_ms: self.refractory_ms,
83 polarity: self.polarity,
84 distance_rule: self.distance_rule,
85 }
86 }
87
88 #[inline]
90 pub fn refractory_samples(&self, sample_rate: f64) -> usize {
91 ((self.refractory_ms.max(MIN_WINDOW_MS) * 1e-3) * sample_rate.max(MIN_SAMPLE_RATE_HZ)).round() as usize
92 }
93
94 #[inline]
96 pub fn pre_samples(&self, sample_rate: f64) -> usize {
97 ((self.pre_ms.max(MIN_WINDOW_MS) * 1e-3) * sample_rate.max(MIN_SAMPLE_RATE_HZ)).round() as usize
98 }
99
100 #[inline]
102 pub fn post_samples(&self, sample_rate: f64) -> usize {
103 ((self.post_ms.max(MIN_WINDOW_MS) * 1e-3) * sample_rate.max(MIN_SAMPLE_RATE_HZ)).round() as usize
104 }
105
106 #[inline]
108 pub fn snippet_samples(&self, sample_rate: f64) -> usize {
109 self.pre_samples(sample_rate) + self.post_samples(sample_rate)
110 }
111
112 #[inline]
114 pub fn batch_samples(&self, sample_rate: f64) -> u64 {
115 ((self.batch_duration_sec.max(MIN_BATCH_SEC) * sample_rate.max(MIN_SAMPLE_RATE_HZ)).round() as u64).max(1)
116 }
117
118 pub fn detection_range(&self, sample_rate: f64, total_samples: u64) -> std::ops::Range<u64> {
121 let margin = crate::extraction::extraction_margin(self.apply_sinc_shift);
122 let start = (self.pre_samples(sample_rate) + margin) as u64;
123 let end = total_samples.saturating_sub((self.post_samples(sample_rate) + margin) as u64);
124 start..end.max(start)
125 }
126
127 pub fn calibration_chunks(&self, sample_rate: f64, total_samples: u64) -> Vec<std::ops::Range<u64>> {
130 let k = self.calibration_chunks.max(1) as u64;
131 let total_len = ((self.calibration_duration_sec.max(MIN_CALIBRATION_SEC) * sample_rate.max(MIN_SAMPLE_RATE_HZ)).round() as u64).max(k);
132 let len = (total_len / k).min(total_samples);
133 if len == 0 {
134 return Vec::new();
135 }
136 let span = total_samples - len;
137 (0..k)
138 .map(|i| {
139 let start = if k == 1 { 0 } else { span * i / (k - 1) };
140 start..start + len
141 })
142 .collect()
143 }
144
145 pub fn compute_halos(&self, sample_rate: f64, pipeline: &Pipeline) -> DspResult<(u64, u64)> {
158 let (settle_left, settle_right) = pipeline
159 .settling(sample_rate)
160 .map_err(|e| DspError::InvalidConfig(e.to_string()))?;
161 let pre = self.pre_samples(sample_rate);
162 let post = self.post_samples(sample_rate);
163 let refrac = self.refractory_samples(sample_rate);
164
165 let left = (settle_left + pre + refrac + SINC_RESAMPLE_MARGIN) as u64;
166 let right = (settle_right + post + refrac + SINC_RESAMPLE_MARGIN) as u64;
167 Ok((left, right))
168 }
169}