use crate::box_centroid::box_centroid;
use crate::codec::encode_frame_type2;
use crate::crop::CropGate;
use crate::dia_ms1::{DiaMs1Gate, TofScanBox};
use crate::dia_window::{filter_per_window, in_window_mask};
use crate::error::{DnoiseError, Result};
use crate::filter::filter_iterated;
use crate::frame::FlatFrame;
use crate::halo::horizontal_halo_keep_mask;
use crate::msms::{MsmsKeep, build_msms_keep};
use crate::params::{
CropParams, DiaMs1WindowParams, FilterParams, HaloParams, Ms1PolygonParams, MsmsFilterParams,
Stages,
};
use crate::polygon::PolygonGate;
use crate::smooth::box_average;
use crate::tdf::{self, DiaWindows, FrameUpdate};
use crate::watershed::watershed_centroid;
use rayon::prelude::*;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::io::{BufWriter, Read, Write};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use timsrust::converters::ConvertableDomain;
use timsrust::readers::{FrameReader, MetadataReader};
use tracing::{debug, info, warn};
const CHUNK: usize = 2048;
const MAX_CENTROIDS: usize = 100_000;
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct DenoiseStats {
pub frames: usize,
pub ms1_frames: usize,
pub msms_frames: usize,
pub cropped_frames: usize,
pub processed_frames: usize,
pub raw_points: u64,
pub kept_points: u64,
pub raw_ms1_points: u64,
pub kept_ms1_points: u64,
pub raw_summed_intensity: u64,
pub kept_summed_intensity: u64,
pub dry_run: bool,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct Progress {
pub frames_done: usize,
pub frames_total: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct SampleSpec {
pub fraction: f64,
pub seed: u64,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RunOptions<'a> {
pub force: bool,
pub dry_run: bool,
pub crop: Option<&'a CropParams>,
pub crop_only: bool,
pub sample: Option<SampleSpec>,
pub cancel: Option<&'a AtomicBool>,
}
fn frame_sampled(index: usize, seed: u64, fraction: f64) -> bool {
let mut z = seed
.wrapping_add(index as u64)
.wrapping_mul(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
let threshold = (fraction.clamp(0.0, 1.0) * u64::MAX as f64) as u64;
z <= threshold
}
pub fn denoise(
input: &Path,
output: &Path,
params: &FilterParams,
stages: &Stages,
force: bool,
) -> Result<DenoiseStats> {
let opts = RunOptions {
force,
..RunOptions::default()
};
denoise_with_options(input, output, params, stages, &opts, |_| {})
}
pub fn denoise_with_options<F: FnMut(Progress)>(
input: &Path,
output: &Path,
params: &FilterParams,
stages: &Stages,
options: &RunOptions,
progress: F,
) -> Result<DenoiseStats> {
run(input, output, params, stages, options, progress)
}
pub fn denoise_with_progress<F: FnMut(Progress)>(
input: &Path,
output: &Path,
params: &FilterParams,
stages: &Stages,
force: bool,
progress: F,
) -> Result<DenoiseStats> {
let opts = RunOptions {
force,
..RunOptions::default()
};
run(input, output, params, stages, &opts, progress)
}
fn run<F: FnMut(Progress)>(
input: &Path,
output: &Path,
params: &FilterParams,
stages: &Stages,
options: &RunOptions,
mut progress: F,
) -> Result<DenoiseStats> {
let &RunOptions {
force,
dry_run,
crop,
crop_only,
sample,
cancel,
} = options;
let &Stages {
halo,
denoise_msms,
dia_window,
dda_window,
dia_per_window,
dia_ms1,
ms1_polygon,
..
} = stages;
let in_tdf = input.join("analysis.tdf");
let in_bin = input.join("analysis.tdf_bin");
if !in_tdf.is_file() || !in_bin.is_file() {
return Err(DnoiseError::NotADotD(input.to_path_buf()));
}
if !dry_run {
if output.exists() {
if force {
fs::remove_dir_all(output)?;
} else {
return Err(DnoiseError::OutputExists(output.to_path_buf()));
}
}
copy_dir_except(input, output, "analysis.tdf_bin")?;
}
let reader = FrameReader::new(input).map_err(|e| DnoiseError::OpenFrames(e.to_string()))?;
let n_frames = reader.len();
let meta = tdf::read_frame_meta(&in_tdf)?;
let n_ms1 = meta.iter().filter(|m| m.is_ms1()).count();
let n_empty = meta.iter().filter(|m| m.num_peaks == 0).count();
let scheme = if meta.iter().any(|m| m.ms_ms_type == 9) {
"diaPASEF"
} else if meta.iter().any(|m| m.ms_ms_type == 8) {
"ddaPASEF"
} else {
"MS1-only/unknown"
};
info!(input = %input.display(), output = %output.display(), "denoise: starting");
info!(
scheme,
frames = n_frames,
ms1 = n_ms1,
msms = n_frames - n_ms1,
empty = n_empty,
"denoise: frame inventory"
);
let ms1_indices: Vec<usize> = (0..n_frames).filter(|&i| meta[i].is_ms1()).collect();
let mut ms1_pos: Vec<Option<usize>> = vec![None; n_frames];
for (p, &gi) in ms1_indices.iter().enumerate() {
ms1_pos[gi] = Some(p);
}
let (msms_keep, dia_msms) = match denoise_msms {
Some(mp) => {
let windows = tdf::read_pasef_msms(&in_tdf)?;
if windows.is_empty() {
info!("MS/MS denoise: diaPASEF whole-frame path (no PasefFrameMsMsInfo)");
(None, Some(mp))
} else {
let keep = build_msms_keep(&reader, &meta, &windows, mp, halo)?;
info!(
isolation_events = windows.len(),
"MS/MS denoise: ddaPASEF per-precursor path"
);
(Some(keep), None)
}
}
None => (None, None),
};
let msms_ref = msms_keep.as_ref();
let dia_windows: Option<DiaWindows> = if dia_window.is_some() || dia_per_window {
let w = tdf::read_dia_windows(&in_tdf)?;
if w.is_empty() {
debug!("diaPASEF window feature requested but no windows found (ddaPASEF?) — skipped");
None
} else {
info!("diaPASEF isolation-window scheme loaded");
Some(w)
}
} else {
None
};
let dia_windows_ref = dia_windows.as_ref();
let dda_windows: Option<DiaWindows> = if dda_window.is_some() {
let w = DiaWindows::from_pasef(&tdf::read_pasef_msms(&in_tdf)?);
if w.is_empty() {
debug!(
"ddaPASEF window gate requested but no isolation events found (diaPASEF?) — skipped"
);
None
} else {
info!("ddaPASEF MS/MS out-of-window gate active");
Some(w)
}
} else {
None
};
let dda_windows_ref = dda_windows.as_ref();
let dia_ms1_gate = match dia_ms1 {
Some(mp) => build_dia_ms1_gate(&in_tdf, mp, &meta)?,
None => None,
};
if dia_ms1.is_some() {
match &dia_ms1_gate {
Some(_) => info!("diaPASEF MS1 out-of-window gate active"),
None => debug!("diaPASEF MS1 gate requested but no isolation windows — skipped"),
}
}
let dia_ms1_ref = dia_ms1_gate.as_ref();
let polygon_gate = match ms1_polygon {
Some(pp) => build_polygon_gate(&in_tdf, pp, &meta)?,
None => None,
};
if ms1_polygon.is_some() {
match &polygon_gate {
Some(_) => info!("MS1 selection-polygon gate active"),
None => {
debug!("MS1 polygon gate requested but run stores no usable polygon — skipped")
}
}
}
let polygon_ref = polygon_gate.as_ref();
if polygon_ref.is_some() || dia_ms1_ref.is_some() {
let (mz_cals, tims_cals) = tdf::count_calibration_segments(&in_tdf)?;
if mz_cals > 1 || tims_cals > 1 {
warn!(
mz_calibrations = mz_cals,
tims_calibrations = tims_cals,
"run carries multiple calibration segments; the acquisition \
gates were converted with the run-level calibration and may be \
slightly offset on frames referencing other segments"
);
}
}
let crop_gate = match crop {
Some(cp) if !cp.is_empty() => {
let md =
MetadataReader::new(&in_tdf).map_err(|e| DnoiseError::Metadata(e.to_string()))?;
let num_scans = meta.iter().map(|m| m.num_scans).max().unwrap_or(0);
let g = CropGate::build(
cp,
num_scans,
|mz| md.mz_converter.invert(mz),
|k0| md.im_converter.invert(k0),
);
info!(
point_crop = g.is_active(),
rt_crop = cp.has_rt(),
crop_only,
"crop: region-of-interest gate built"
);
g.is_active().then_some(g)
}
_ => None,
};
let crop_ref = crop_gate.as_ref();
let rt_keep: Vec<bool> = match crop {
Some(cp) if cp.has_rt() => {
let lo = cp.rt_min.map(|m| m * 60.0).unwrap_or(f64::NEG_INFINITY);
let hi = cp.rt_max.map(|m| m * 60.0).unwrap_or(f64::INFINITY);
meta.iter().map(|m| m.rt >= lo && m.rt <= hi).collect()
}
_ => vec![true; n_frames],
};
let selected: Vec<usize> = match sample {
Some(s) if dry_run => (0..n_frames)
.filter(|&i| frame_sampled(i, s.seed, s.fraction))
.collect(),
_ => (0..n_frames).collect(),
};
match sample {
Some(_) if !dry_run => {
warn!("--sample ignored without --dry-run (a real run must process every frame)")
}
Some(s) => info!(
sampled = selected.len(),
total = n_frames,
fraction = s.fraction,
"dry-run: processing a frame sample"
),
None => {}
}
let n_process = selected.len();
let header_len = tdf::binary_header_len(&in_tdf)?;
let mut bin = if dry_run {
None
} else {
let mut b = BufWriter::new(fs::File::create(output.join("analysis.tdf_bin"))?);
if header_len > 0 {
let mut header = vec![0u8; header_len as usize];
fs::File::open(&in_bin).and_then(|mut f| f.read_exact(&mut header))?;
b.write_all(&header)?;
}
Some(b)
};
progress(Progress {
frames_done: 0,
frames_total: n_process,
});
let ctx = FrameCtx {
msms: msms_ref,
dia_msms,
dia_windows: dia_windows_ref,
dda_windows: dda_windows_ref,
dia_ms1: dia_ms1_ref,
polygon: polygon_ref,
crop: crop_ref,
crop_only,
rt_keep: &rt_keep,
ms1_indices: &ms1_indices,
ms1_pos: &ms1_pos,
};
let mut offset: u64 = header_len;
let mut updates: Vec<FrameUpdate> = Vec::with_capacity(n_process);
let mut raw_points: u64 = 0;
let mut kept_points: u64 = 0;
let mut raw_ms1: u64 = 0;
let mut kept_ms1: u64 = 0;
let mut raw_summed: u64 = 0;
let mut kept_summed: u64 = 0;
let mut cropped_frames: usize = 0;
let mut frames_done: usize = 0;
for chunk in selected.chunks(CHUNK) {
if let Some(c) = cancel {
if c.load(Ordering::Relaxed) {
return Err(DnoiseError::Cancelled);
}
}
let processed: Vec<ProcessedFrame> = chunk
.par_iter()
.map(|&i| process_frame(&reader, &meta, i, params, stages, &ctx))
.collect::<Result<_>>()?;
for pf in processed {
raw_points += pf.raw_points;
kept_points += pf.num_peaks;
raw_summed += pf.raw_summed;
kept_summed += pf.summed_intensities;
if pf.is_ms1 {
raw_ms1 += pf.raw_points;
kept_ms1 += pf.num_peaks;
}
if pf.cropped {
cropped_frames += 1;
}
if let Some(b) = bin.as_mut() {
b.write_all(&pf.record)?;
updates.push(FrameUpdate {
frame_id: pf.frame_id,
tims_id: offset,
num_peaks: pf.num_peaks,
max_intensity: pf.max_intensity,
summed_intensities: pf.summed_intensities,
});
offset += pf.record.len() as u64;
}
frames_done += 1;
progress(Progress {
frames_done,
frames_total: n_process,
});
}
}
if let Some(b) = bin.as_mut() {
b.flush()?;
}
drop(bin);
if !dry_run {
tdf::update_metadata(&output.join("analysis.tdf"), &updates)?;
}
let kept_pct = if raw_points > 0 {
((10_000.0 * kept_points as f64 / raw_points as f64).round()) / 100.0
} else {
0.0
};
info!(
dry_run,
processed_frames = n_process,
raw_points,
kept_points,
kept_pct,
"denoise: complete"
);
Ok(DenoiseStats {
frames: n_frames,
ms1_frames: n_ms1,
msms_frames: n_frames - n_ms1,
cropped_frames,
processed_frames: n_process,
raw_points,
kept_points,
raw_ms1_points: raw_ms1,
kept_ms1_points: kept_ms1,
raw_summed_intensity: raw_summed,
kept_summed_intensity: kept_summed,
dry_run,
})
}
struct ProcessedFrame {
frame_id: usize,
record: Vec<u8>,
raw_points: u64,
num_peaks: u64,
max_intensity: u32,
summed_intensities: u64,
raw_summed: u64,
is_ms1: bool,
cropped: bool,
}
pub struct FrameCtx<'a> {
msms: Option<&'a MsmsKeep>,
dia_msms: Option<&'a MsmsFilterParams>,
dia_windows: Option<&'a DiaWindows>,
dda_windows: Option<&'a DiaWindows>,
dia_ms1: Option<&'a DiaMs1Gate>,
polygon: Option<&'a PolygonGate>,
crop: Option<&'a CropGate>,
crop_only: bool,
rt_keep: &'a [bool],
ms1_indices: &'a [usize],
ms1_pos: &'a [Option<usize>],
}
pub struct DecodedFrame {
pub frame_id: usize,
pub is_ms1: bool,
pub num_scans: usize,
pub rt_seconds: f64,
pub survivors: Vec<(u32, u32, u32)>,
pub raw_points: u64,
pub raw_summed: u64,
pub cropped: bool,
empty_record: bool,
}
fn process_frame(
reader: &FrameReader,
meta: &[tdf::FrameMeta],
i: usize,
params: &FilterParams,
stages: &Stages,
ctx: &FrameCtx,
) -> Result<ProcessedFrame> {
let d = process_frame_decoded(reader, meta, i, params, stages, ctx)?;
let num_peaks = d.survivors.len() as u64;
let summed_intensities: u64 = d.survivors.iter().map(|&(_, _, it)| it as u64).sum();
let max_intensity = d.survivors.iter().map(|&(_, _, it)| it).max().unwrap_or(0);
let record = if d.empty_record {
crate::codec::encode_empty_frame_type2(d.num_scans)
} else {
encode_frame_type2(d.num_scans, &d.survivors)
};
Ok(ProcessedFrame {
frame_id: d.frame_id,
record,
raw_points: d.raw_points,
num_peaks,
max_intensity,
summed_intensities,
raw_summed: d.raw_summed,
is_ms1: d.is_ms1,
cropped: d.cropped,
})
}
pub fn process_frame_decoded(
reader: &FrameReader,
meta: &[tdf::FrameMeta],
i: usize,
params: &FilterParams,
stages: &Stages,
ctx: &FrameCtx,
) -> Result<DecodedFrame> {
let &Stages {
filter_all_frames,
frame_half_width,
halo,
smooth,
watershed,
box_centroid: box_centroid_params,
dia_window,
dda_window,
dia_per_window,
..
} = stages;
let &FrameCtx {
msms,
dia_msms,
dia_windows,
dda_windows,
dia_ms1,
polygon,
crop,
crop_only,
rt_keep,
ms1_indices,
ms1_pos,
} = ctx;
let meta_i = &meta[i];
let is_ms1 = meta_i.is_ms1();
if meta_i.num_peaks == 0 {
return Ok(DecodedFrame {
frame_id: meta_i.id,
is_ms1,
num_scans: meta_i.num_scans,
rt_seconds: meta_i.rt,
survivors: Vec::new(),
raw_points: 0,
raw_summed: 0,
cropped: false,
empty_record: true,
});
}
if !rt_keep[i] {
return Ok(DecodedFrame {
frame_id: meta_i.id,
is_ms1,
num_scans: meta_i.num_scans,
rt_seconds: meta_i.rt,
survivors: Vec::new(),
raw_points: meta_i.num_peaks,
raw_summed: 0,
cropped: true,
empty_record: true,
});
}
let frame = reader.get(i).map_err(|e| DnoiseError::FrameRead {
index: i,
message: e.to_string(),
})?;
let flat = FlatFrame::from_frame(&frame);
let raw_points = flat.len() as u64;
let raw_summed: u64 = flat.intensity.iter().map(|&it| it as u64).sum();
let num_scans = flat.num_scans;
let frame_id = flat.frame_id;
let neighborhood_keys: Option<HashSet<u64>> =
if frame_half_width > 0 && meta_i.is_ms1() && meta_i.num_peaks > 0 {
let p = ms1_pos[i].expect("MS1 frame must have an MS1-stream position");
let lo = p.saturating_sub(frame_half_width);
let hi = (p + frame_half_width).min(ms1_indices.len() - 1);
let mut neighbors: Vec<FlatFrame> = Vec::with_capacity(hi - lo);
for &gi in &ms1_indices[lo..=hi] {
if gi == i || meta[gi].num_peaks == 0 {
continue;
}
let nf = reader.get(gi).map_err(|e| DnoiseError::FrameRead {
index: gi,
message: e.to_string(),
})?;
neighbors.push(FlatFrame::from_frame(&nf));
}
let mut window: Vec<&FlatFrame> = neighbors.iter().collect();
window.push(&flat);
Some(neighborhood_keep_keys(num_scans, &window, params, halo))
} else {
None
};
let to_filter: &FlatFrame = ♭
let dia_iv = dia_windows.and_then(|dw| dw.intervals(meta_i.id));
let mut keep = if crop_only {
vec![true; to_filter.len()]
} else if meta_i.is_ms1() {
let mut keep = if let Some(keys) = &neighborhood_keys {
(0..to_filter.len())
.map(|j| keys.contains(&frame_key(to_filter.scan[j], to_filter.tof[j])))
.collect()
} else {
let mut keep = filter_iterated(to_filter, params);
if let Some(hp) = halo {
apply_halo(to_filter, hp, &mut keep);
}
keep
};
if let Some(gate) = dia_ms1 {
for (slot, in_win) in keep
.iter_mut()
.zip(gate.keep_mask(&to_filter.scan, &to_filter.tof))
{
*slot &= in_win;
}
}
if let Some(gate) = polygon {
for (slot, inside) in keep
.iter_mut()
.zip(gate.keep_mask(&to_filter.scan, &to_filter.tof))
{
*slot &= inside;
}
}
keep
} else if let Some(mk) = msms {
mk.keep_mask(to_filter, meta_i.id)
} else if let Some(mp) = dia_msms {
let fp = mp.as_filter_params();
match dia_iv {
Some(iv) if dia_per_window => filter_per_window(to_filter, iv, &fp, halo),
_ => {
let mut keep = filter_iterated(to_filter, &fp);
if let Some(hp) = halo {
apply_halo(to_filter, hp, &mut keep);
}
keep
}
}
} else if filter_all_frames {
match dia_iv {
Some(iv) if dia_per_window => filter_per_window(to_filter, iv, params, halo),
_ => {
let mut keep = filter_iterated(to_filter, params);
if let Some(hp) = halo {
apply_halo(to_filter, hp, &mut keep);
}
keep
}
}
} else {
vec![true; to_filter.len()]
};
if !crop_only {
if let (Some(dp), Some(iv)) = (dia_window, dia_iv) {
let mask = in_window_mask(&to_filter.scan, iv, dp.scan_pad);
for (slot, keep_pt) in keep.iter_mut().zip(mask) {
*slot &= keep_pt;
}
}
let dda_iv = dda_windows.and_then(|w| w.intervals(meta_i.id));
if let (Some(dp), Some(iv)) = (dda_window, dda_iv) {
let mask = in_window_mask(&to_filter.scan, iv, dp.scan_pad);
for (slot, keep_pt) in keep.iter_mut().zip(mask) {
*slot &= keep_pt;
}
}
}
if let Some(cg) = crop {
cg.apply(
&to_filter.scan,
&to_filter.tof,
&to_filter.intensity,
&mut keep,
);
}
let survivors = to_filter.survivors(&keep);
let filtered_here = !crop_only
&& (meta_i.is_ms1() || dia_msms.is_some() || (msms.is_none() && filter_all_frames));
let survivors = match smooth {
Some(sp) if filtered_here => box_average(&survivors, num_scans, sp),
_ => survivors,
};
let survivors = match watershed {
Some(wp) if filtered_here => watershed_centroid(&survivors, wp, MAX_CENTROIDS),
_ => survivors,
};
let survivors = match box_centroid_params {
Some(bp) if filtered_here => box_centroid(&survivors, bp),
_ => survivors,
};
Ok(DecodedFrame {
frame_id,
is_ms1,
num_scans,
rt_seconds: meta_i.rt,
survivors,
raw_points,
raw_summed,
cropped: false,
empty_record: false,
})
}
fn build_dia_ms1_gate(
in_tdf: &Path,
p: &DiaMs1WindowParams,
meta: &[tdf::FrameMeta],
) -> Result<Option<DiaMs1Gate>> {
let boxes = tdf::read_dia_ms1_boxes(in_tdf)?;
if boxes.is_empty() {
return Ok(None);
}
let md = MetadataReader::new(in_tdf).map_err(|e| DnoiseError::Metadata(e.to_string()))?;
let num_scans = meta.iter().map(|m| m.num_scans).max().unwrap_or(0);
if num_scans == 0 {
return Ok(None);
}
let tof_boxes: Vec<TofScanBox> = boxes
.iter()
.map(|b| {
let t0 = md.mz_converter.invert(b.mz_lo - p.mz_pad);
let t1 = md.mz_converter.invert(b.mz_hi + p.mz_pad);
let tof_lo = t0.min(t1).floor().max(0.0) as u32;
let tof_hi = t1.max(t0).ceil().max(0.0) as u32;
let im0 = md.im_converter.convert(b.scan_begin);
let im1 = md.im_converter.convert(b.scan_end);
let s0 = md.im_converter.invert(im0.max(im1) + p.im_pad);
let s1 = md.im_converter.invert(im0.min(im1) - p.im_pad);
let scan_lo = s0.min(s1).floor().max(0.0) as u32;
let scan_hi = (s0.max(s1).ceil().max(0.0) as u32).min(num_scans as u32 - 1);
TofScanBox {
scan_lo,
scan_hi,
tof_lo,
tof_hi,
}
})
.collect();
Ok(DiaMs1Gate::build(&tof_boxes, num_scans))
}
fn build_polygon_gate(
in_tdf: &Path,
p: &Ms1PolygonParams,
meta: &[tdf::FrameMeta],
) -> Result<Option<PolygonGate>> {
if !tdf::read_dia_windows(in_tdf)?.is_empty() {
return Ok(None); }
let Some((mz, im)) = tdf::read_selection_polygon(in_tdf)? else {
return Ok(None);
};
let md = MetadataReader::new(in_tdf).map_err(|e| DnoiseError::Metadata(e.to_string()))?;
let num_scans = meta.iter().map(|m| m.num_scans).max().unwrap_or(0);
if num_scans == 0 {
return Ok(None);
}
Ok(PolygonGate::build(
&mz,
&im,
num_scans,
|s| md.im_converter.convert(s),
|mz| md.mz_converter.invert(mz),
p.mz_pad,
p.im_pad,
))
}
fn apply_halo(frame: &FlatFrame, hp: &HaloParams, keep: &mut [bool]) {
let idx: Vec<usize> = (0..frame.len()).filter(|&i| keep[i]).collect();
if idx.is_empty() {
return;
}
let scan: Vec<u32> = idx.iter().map(|&i| frame.scan[i]).collect();
let tof: Vec<u32> = idx.iter().map(|&i| frame.tof[i]).collect();
let inten: Vec<u32> = idx.iter().map(|&i| frame.intensity[i]).collect();
let hmask = horizontal_halo_keep_mask(&scan, &tof, &inten, frame.num_scans, hp);
for (k, &i) in idx.iter().enumerate() {
if !hmask[k] {
keep[i] = false;
}
}
}
fn frame_key(scan: u32, tof: u32) -> u64 {
((scan as u64) << 32) | tof as u64
}
fn neighborhood_keep_keys(
num_scans: usize,
window: &[&FlatFrame],
params: &FilterParams,
halo: Option<&HaloParams>,
) -> HashSet<u64> {
let mut acc: HashMap<u64, u64> = HashMap::new();
for f in window {
for k in 0..f.len() {
*acc.entry(frame_key(f.scan[k], f.tof[k])).or_insert(0) += f.intensity[k] as u64;
}
}
let mut scan = Vec::with_capacity(acc.len());
let mut tof = Vec::with_capacity(acc.len());
let mut intensity = Vec::with_capacity(acc.len());
for (&k, &sum) in &acc {
scan.push((k >> 32) as u32);
tof.push((k & 0xFFFF_FFFF) as u32);
intensity.push(sum.min(u32::MAX as u64) as u32);
}
let combined = FlatFrame {
frame_id: 0,
num_scans,
scan,
tof,
intensity,
};
let mut keep = filter_iterated(&combined, params);
if let Some(hp) = halo {
apply_halo(&combined, hp, &mut keep);
}
let mut out = HashSet::new();
for ((&keep_k, &scan), &tof) in keep.iter().zip(&combined.scan).zip(&combined.tof) {
if keep_k {
out.insert(frame_key(scan, tof));
}
}
out
}
fn copy_dir_except(src: &Path, dst: &Path, skip_top: &str) -> Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let name = entry.file_name();
let from = entry.path();
let to = dst.join(&name);
if from.is_dir() {
copy_dir_recursive(&from, &to)?;
} else if name != skip_top {
fs::copy(&from, &to)?;
}
}
Ok(())
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let from = entry.path();
let to = dst.join(entry.file_name());
if from.is_dir() {
copy_dir_recursive(&from, &to)?;
} else {
fs::copy(&from, &to)?;
}
}
Ok(())
}
pub struct Calibration {
tof2mz: timsrust::converters::Tof2MzConverter,
scan2im: timsrust::converters::Scan2ImConverter,
}
impl Calibration {
pub fn tof_to_mz(&self, tof: u32) -> f64 {
self.tof2mz.convert(tof as f64)
}
pub fn scan_to_im(&self, scan: u32) -> f64 {
self.scan2im.convert(scan as f64)
}
}
pub struct RunContext<'a> {
reader: FrameReader,
meta: Vec<tdf::FrameMeta>,
params: FilterParams,
stages: Stages<'a>,
msms_keep: Option<MsmsKeep>,
dia_msms: Option<&'a MsmsFilterParams>,
dia_windows: Option<DiaWindows>,
dda_windows: Option<DiaWindows>,
dia_ms1_gate: Option<DiaMs1Gate>,
polygon_gate: Option<PolygonGate>,
rt_keep: Vec<bool>,
ms1_indices: Vec<usize>,
ms1_pos: Vec<Option<usize>>,
calibration: Calibration,
n_ms1: usize,
}
impl<'a> RunContext<'a> {
pub fn open(input: &Path, params: &FilterParams, stages: &'a Stages<'a>) -> Result<Self> {
let in_tdf = input.join("analysis.tdf");
let in_bin = input.join("analysis.tdf_bin");
if !in_tdf.is_file() || !in_bin.is_file() {
return Err(DnoiseError::NotADotD(input.to_path_buf()));
}
let reader = FrameReader::new(input).map_err(|e| DnoiseError::OpenFrames(e.to_string()))?;
let meta = tdf::read_frame_meta(&in_tdf)?;
let n_frames = meta.len();
let n_ms1 = meta.iter().filter(|m| m.is_ms1()).count();
let ms1_indices: Vec<usize> = (0..n_frames).filter(|&i| meta[i].is_ms1()).collect();
let mut ms1_pos: Vec<Option<usize>> = vec![None; n_frames];
for (p, &gi) in ms1_indices.iter().enumerate() {
ms1_pos[gi] = Some(p);
}
let (msms_keep, dia_msms) = match stages.denoise_msms {
Some(mp) => {
let windows = tdf::read_pasef_msms(&in_tdf)?;
if windows.is_empty() {
(None, Some(mp))
} else {
let keep = build_msms_keep(&reader, &meta, &windows, mp, stages.halo)?;
(Some(keep), None)
}
}
None => (None, None),
};
let dia_windows = if stages.dia_window.is_some() || stages.dia_per_window {
let w = tdf::read_dia_windows(&in_tdf)?;
if w.is_empty() { None } else { Some(w) }
} else {
None
};
let dda_windows = if stages.dda_window.is_some() {
let w = DiaWindows::from_pasef(&tdf::read_pasef_msms(&in_tdf)?);
if w.is_empty() { None } else { Some(w) }
} else {
None
};
let dia_ms1_gate = match stages.dia_ms1 {
Some(mp) => build_dia_ms1_gate(&in_tdf, mp, &meta)?,
None => None,
};
let polygon_gate = match stages.ms1_polygon {
Some(pp) => build_polygon_gate(&in_tdf, pp, &meta)?,
None => None,
};
if polygon_gate.is_some() || dia_ms1_gate.is_some() {
let (mz_cals, tims_cals) = tdf::count_calibration_segments(&in_tdf)?;
if mz_cals > 1 || tims_cals > 1 {
warn!(
mz_calibrations = mz_cals,
tims_calibrations = tims_cals,
"run carries multiple calibration segments; the acquisition \
gates were converted with the run-level calibration and may be \
slightly offset on frames referencing other segments"
);
}
}
let rt_keep = vec![true; n_frames];
let md = MetadataReader::new(&in_tdf).map_err(|e| DnoiseError::Metadata(e.to_string()))?;
let calibration = Calibration {
tof2mz: md.mz_converter,
scan2im: md.im_converter,
};
Ok(RunContext {
reader,
meta,
params: *params,
stages: *stages,
msms_keep,
dia_msms,
dia_windows,
dda_windows,
dia_ms1_gate,
polygon_gate,
rt_keep,
ms1_indices,
ms1_pos,
calibration,
n_ms1,
})
}
pub fn len(&self) -> usize {
self.meta.len()
}
pub fn is_empty(&self) -> bool {
self.meta.is_empty()
}
pub fn ms1_frames(&self) -> usize {
self.n_ms1
}
pub fn is_ms1(&self, i: usize) -> bool {
self.meta[i].is_ms1()
}
pub fn calibration(&self) -> &Calibration {
&self.calibration
}
pub fn process(&self, i: usize) -> Result<DecodedFrame> {
let ctx = FrameCtx {
msms: self.msms_keep.as_ref(),
dia_msms: self.dia_msms,
dia_windows: self.dia_windows.as_ref(),
dda_windows: self.dda_windows.as_ref(),
dia_ms1: self.dia_ms1_gate.as_ref(),
polygon: self.polygon_gate.as_ref(),
crop: None,
crop_only: false,
rt_keep: &self.rt_keep,
ms1_indices: &self.ms1_indices,
ms1_pos: &self.ms1_pos,
};
process_frame_decoded(
&self.reader,
&self.meta,
i,
&self.params,
&self.stages,
&ctx,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_sampled_fraction_one_keeps_every_frame() {
assert!((0..1000).all(|i| frame_sampled(i, 42, 1.0)));
}
#[test]
fn frame_sampled_fraction_zero_keeps_essentially_none() {
let kept = (0..1000).filter(|&i| frame_sampled(i, 42, 0.0)).count();
assert!(kept <= 1, "expected ~0 kept at fraction 0.0, got {kept}");
}
#[test]
fn frame_sampled_is_deterministic() {
assert!((0..500).all(|i| frame_sampled(i, 7, 0.5) == frame_sampled(i, 7, 0.5)));
}
#[test]
fn frame_sampled_roughly_matches_the_requested_fraction() {
let n = 20_000;
let kept = (0..n).filter(|&i| frame_sampled(i, 123, 0.25)).count();
let frac = kept as f64 / n as f64;
assert!((0.22..0.28).contains(&frac), "fraction {frac} out of band");
}
#[test]
fn denoise_stats_default_is_zeroed() {
let s = DenoiseStats::default();
assert_eq!(s.frames, 0);
assert_eq!(s.raw_points, 0);
assert_eq!(s.kept_points, 0);
assert!(!s.dry_run);
}
#[test]
fn copy_dir_except_skips_named_top_level_entry_and_recurses() {
let base = std::env::temp_dir().join(format!(
"dnoise_copy_except_{}_{}",
std::process::id(),
"writer"
));
let src = base.join("src");
let dst = base.join("dst");
let _ = fs::remove_dir_all(&base);
fs::create_dir_all(src.join("sub")).unwrap();
fs::write(src.join("keep.txt"), b"a").unwrap();
fs::write(src.join("analysis.tdf_bin"), b"skip me").unwrap();
fs::write(src.join("sub").join("nested.txt"), b"b").unwrap();
copy_dir_except(&src, &dst, "analysis.tdf_bin").unwrap();
assert!(dst.join("keep.txt").is_file());
assert!(dst.join("sub").join("nested.txt").is_file());
assert!(
!dst.join("analysis.tdf_bin").exists(),
"the skipped top-level entry must not be copied"
);
let _ = fs::remove_dir_all(&base);
}
}