use std::path::Path;
use dspk_core::{ComputeTarget, DspError, DspResult};
use dspk_synapse::{SnippetBatch, WaveformDenoiser};
use crate::manifest::{SafetensorsMap, TensorIoSpec};
use crate::runtime::{
burn_conv1d, burn_linear_2d, validate_tensor_port,
};
pub const DARTSORT_DENOISER_MODEL_ID: &str = "dartsort/singlechan-denoiser-v1";
pub struct DartsortWaveformDenoiser {
weights: SafetensorsMap,
io_spec: Option<TensorIoSpec>,
target: ComputeTarget,
}
impl DartsortWaveformDenoiser {
#[cfg(feature = "hub")]
pub fn from_hub(target: ComputeTarget) -> DspResult<Self> {
let (manifest, path) = crate::runtime::pull_model(DARTSORT_DENOISER_MODEL_ID)?;
let compute_target = target.checked().map_err(|e| DspError::ComputeError(e.to_string()))?;
let weights = SafetensorsMap::from_file(&path)
.map_err(|e| DspError::InvalidConfig(e.to_string()))?;
Ok(Self {
weights,
io_spec: manifest.io_spec,
target: compute_target,
})
}
pub fn from_safetensors_file(
path: impl AsRef<Path>,
target: ComputeTarget,
) -> DspResult<Self> {
let compute_target = target.checked().map_err(|e| DspError::ComputeError(e.to_string()))?;
let weights = SafetensorsMap::from_file(path)
.map_err(|e| DspError::InvalidConfig(e.to_string()))?;
Ok(Self {
weights,
io_spec: None,
target: compute_target,
})
}
pub fn target(&self) -> ComputeTarget {
self.target
}
}
impl WaveformDenoiser for DartsortWaveformDenoiser {
fn denoise(&self, batch: &SnippetBatch) -> DspResult<SnippetBatch> {
let [n, k, t] = batch.shape();
if n == 0 || k == 0 || t == 0 {
return Ok(batch.clone());
}
let single_chan_batches = n * k;
if let Some(io) = &self.io_spec {
if let Some(in_port) = io.inputs.first() {
validate_tensor_port(in_port, &[single_chan_batches, 1, t])?;
}
}
let (conv_w_shape, conv_w) = self
.weights
.get_raw("conv1.weight")
.map_err(|e| DspError::InvalidConfig(e.to_string()))?;
if conv_w_shape.len() != 3 {
return Err(DspError::InvalidConfig(format!(
"conv1.weight expected 3D shape, got {:?}",
conv_w_shape
)));
}
let out_ch = conv_w_shape[0];
let kernel_size = conv_w_shape[2];
let conv_b = self.weights.get_raw("conv1.bias").ok().map(|(_, d)| d);
let pad = kernel_size / 2;
let (mut hidden, out_len) = burn_conv1d(
self.target,
&batch.data,
single_chan_batches,
1,
t,
conv_w,
out_ch,
kernel_size,
conv_b,
1,
pad,
1,
1,
)?;
for v in &mut hidden {
*v = v.max(0.0);
}
let (lin_w_shape, lin_w) = self
.weights
.get_raw("fc.weight")
.map_err(|e| DspError::InvalidConfig(e.to_string()))?;
if lin_w_shape.len() != 2 {
return Err(DspError::InvalidConfig(format!(
"fc.weight expected 2D shape, got {:?}",
lin_w_shape
)));
}
let lin_b = self.weights.get_raw("fc.bias").ok().map(|(_, d)| d);
let denoised = burn_linear_2d(
self.target,
&hidden,
single_chan_batches,
out_ch * out_len,
lin_w,
lin_w_shape[0],
lin_b,
)?;
Ok(SnippetBatch {
data: denoised,
num_spikes: n,
num_channels: k,
num_samples: t,
peak_index: batch.peak_index,
primary_channels: batch.primary_channels.clone(),
center_samples: batch.center_samples.clone(),
subsample_offsets: batch.subsample_offsets.clone(),
channel_ids: batch.channel_ids.clone(),
})
}
}