use dspk_ops::peaks::DistanceRule;
use dspk_ops::pipeline::Pipeline;
use dspk_core::{DspError, DspResult};
use crate::detection::SpikePolarity;
pub const SINC_RESAMPLE_MARGIN: usize = crate::extraction::SINC_KERNEL_RADIUS;
const MIN_WINDOW_MS: f64 = 0.1;
const MIN_BATCH_SEC: f64 = 0.5;
const MIN_CALIBRATION_SEC: f64 = 0.1;
const MIN_SAMPLE_RATE_HZ: f64 = 1.0;
#[derive(Debug, Clone)]
pub struct StreamingDetectionConfig {
pub batch_duration_sec: f64,
pub calibration_duration_sec: f64,
pub calibration_chunks: usize,
pub threshold_factor: f32,
pub refractory_ms: f64,
pub polarity: SpikePolarity,
pub distance_rule: DistanceRule,
pub spatial_radius_um: f32,
pub k_neighbors: usize,
pub pre_ms: f64,
pub post_ms: f64,
pub apply_sinc_shift: bool,
}
impl Default for StreamingDetectionConfig {
fn default() -> Self {
Self {
batch_duration_sec: 10.0,
calibration_duration_sec: 5.0,
calibration_chunks: 5,
threshold_factor: 5.0,
refractory_ms: 1.0,
polarity: SpikePolarity::Negative,
distance_rule: DistanceRule::LocallyExclusive,
spatial_radius_um: 150.0,
k_neighbors: 4,
pre_ms: 1.0,
post_ms: 2.0,
apply_sinc_shift: true,
}
}
}
impl StreamingDetectionConfig {
#[inline]
pub fn refractory_samples(&self, sample_rate: f64) -> usize {
((self.refractory_ms.max(MIN_WINDOW_MS) * 1e-3) * sample_rate.max(MIN_SAMPLE_RATE_HZ)).round() as usize
}
#[inline]
pub fn pre_samples(&self, sample_rate: f64) -> usize {
((self.pre_ms.max(MIN_WINDOW_MS) * 1e-3) * sample_rate.max(MIN_SAMPLE_RATE_HZ)).round() as usize
}
#[inline]
pub fn post_samples(&self, sample_rate: f64) -> usize {
((self.post_ms.max(MIN_WINDOW_MS) * 1e-3) * sample_rate.max(MIN_SAMPLE_RATE_HZ)).round() as usize
}
#[inline]
pub fn snippet_samples(&self, sample_rate: f64) -> usize {
self.pre_samples(sample_rate) + self.post_samples(sample_rate)
}
#[inline]
pub fn batch_samples(&self, sample_rate: f64) -> u64 {
((self.batch_duration_sec.max(MIN_BATCH_SEC) * sample_rate.max(MIN_SAMPLE_RATE_HZ)).round() as u64).max(1)
}
pub fn detection_range(&self, sample_rate: f64, total_samples: u64) -> std::ops::Range<u64> {
let margin = crate::extraction::extraction_margin(self.apply_sinc_shift);
let start = (self.pre_samples(sample_rate) + margin) as u64;
let end = total_samples.saturating_sub((self.post_samples(sample_rate) + margin) as u64);
start..end.max(start)
}
pub fn calibration_chunks(&self, sample_rate: f64, total_samples: u64) -> Vec<std::ops::Range<u64>> {
let k = self.calibration_chunks.max(1) as u64;
let total_len = ((self.calibration_duration_sec.max(MIN_CALIBRATION_SEC) * sample_rate.max(MIN_SAMPLE_RATE_HZ)).round() as u64).max(k);
let len = (total_len / k).min(total_samples);
if len == 0 {
return Vec::new();
}
let span = total_samples - len;
(0..k)
.map(|i| {
let start = if k == 1 { 0 } else { span * i / (k - 1) };
start..start + len
})
.collect()
}
pub fn compute_halos(&self, sample_rate: f64, pipeline: &Pipeline) -> DspResult<(u64, u64)> {
let (settle_left, settle_right) = pipeline
.settling(sample_rate)
.map_err(|e| DspError::InvalidConfig(e.to_string()))?;
let pre = self.pre_samples(sample_rate);
let post = self.post_samples(sample_rate);
let refrac = self.refractory_samples(sample_rate);
let left = (settle_left + pre + refrac + SINC_RESAMPLE_MARGIN) as u64;
let right = (settle_right + post + refrac + SINC_RESAMPLE_MARGIN) as u64;
Ok((left, right))
}
}