#[derive(Debug, Clone)]
pub struct MzRecalModel {
mz_min: f64,
mz_inv_width: f64,
n_mz: usize,
rt_min: f64,
rt_inv_width: f64,
n_rt: usize,
cell: Vec<Option<f64>>,
mz_marginal: Vec<Option<f64>>,
global: f64,
cell_sigma: Vec<Option<f64>>,
mz_marginal_sigma: Vec<Option<f64>>,
pub n_samples: u64,
pub global_sigma: f64,
}
impl MzRecalModel {
#[inline]
pub fn predict(&self, mz: f64, rt: f64) -> f64 {
let mi = self.mz_bin(mz);
let ri = self.rt_bin(rt);
if let Some(v) = self.cell[mi * self.n_rt + ri] {
return v;
}
if let Some(v) = self.mz_marginal[mi] {
return v;
}
self.global
}
pub fn global_offset(&self) -> f64 {
self.global
}
#[inline]
pub fn predict_sigma(&self, mz: f64, rt: f64) -> f64 {
let mi = self.mz_bin(mz);
let ri = self.rt_bin(rt);
if let Some(v) = self.cell_sigma[mi * self.n_rt + ri] {
return v;
}
if let Some(v) = self.mz_marginal_sigma[mi] {
return v;
}
self.global_sigma
}
#[inline]
fn mz_bin(&self, mz: f64) -> usize {
let b = ((mz - self.mz_min) * self.mz_inv_width) as isize;
b.clamp(0, self.n_mz as isize - 1) as usize
}
#[inline]
fn rt_bin(&self, rt: f64) -> usize {
let b = ((rt - self.rt_min) * self.rt_inv_width) as isize;
b.clamp(0, self.n_rt as isize - 1) as usize
}
}
#[derive(Debug)]
pub struct MzRecalBuilder {
samples: Vec<(f64, f64, f64)>,
max_abs_ppm: f64,
}
impl MzRecalBuilder {
pub fn new(max_abs_ppm: f64) -> Self {
Self {
samples: Vec::new(),
max_abs_ppm: max_abs_ppm.max(0.0),
}
}
#[inline]
pub fn add(&mut self, mz: f64, rt: f64, resid_ppm: f64) {
if !mz.is_finite() || !rt.is_finite() || !resid_ppm.is_finite() {
return;
}
if resid_ppm.abs() > self.max_abs_ppm {
return;
}
self.samples.push((mz, rt, resid_ppm));
}
pub fn len(&self) -> usize {
self.samples.len()
}
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
pub fn finalize(self, n_mz: usize, n_rt: usize, min_samples: usize) -> Option<MzRecalModel> {
let n_mz = n_mz.max(1);
let n_rt = n_rt.max(1);
let need = min_samples.max(1);
if self.samples.len() < need {
return None;
}
let (mut mz_lo, mut mz_hi) = (f64::INFINITY, f64::NEG_INFINITY);
let (mut rt_lo, mut rt_hi) = (f64::INFINITY, f64::NEG_INFINITY);
for &(mz, rt, _) in &self.samples {
mz_lo = mz_lo.min(mz);
mz_hi = mz_hi.max(mz);
rt_lo = rt_lo.min(rt);
rt_hi = rt_hi.max(rt);
}
let mz_span = (mz_hi - mz_lo).max(1e-9);
let rt_span = (rt_hi - rt_lo).max(1e-9);
let mz_inv_width = n_mz as f64 / (mz_span * (1.0 + 1e-9));
let rt_inv_width = n_rt as f64 / (rt_span * (1.0 + 1e-9));
let mut cell_buckets: Vec<Vec<f64>> = vec![Vec::new(); n_mz * n_rt];
let mut mz_buckets: Vec<Vec<f64>> = vec![Vec::new(); n_mz];
let mut all: Vec<f64> = Vec::with_capacity(self.samples.len());
let mz_bin = |mz: f64| -> usize {
(((mz - mz_lo) * mz_inv_width) as isize).clamp(0, n_mz as isize - 1) as usize
};
let rt_bin = |rt: f64| -> usize {
(((rt - rt_lo) * rt_inv_width) as isize).clamp(0, n_rt as isize - 1) as usize
};
for &(mz, rt, resid) in &self.samples {
let mi = mz_bin(mz);
let ri = rt_bin(rt);
cell_buckets[mi * n_rt + ri].push(resid);
mz_buckets[mi].push(resid);
all.push(resid);
}
let mut cell = Vec::with_capacity(cell_buckets.len());
let mut cell_sigma = Vec::with_capacity(cell_buckets.len());
for mut v in cell_buckets {
if v.len() >= need {
let m = median(&mut v);
cell.push(Some(m));
cell_sigma.push(Some(mad_sigma(&mut v, m)));
} else {
cell.push(None);
cell_sigma.push(None);
}
}
let mut mz_marginal = Vec::with_capacity(mz_buckets.len());
let mut mz_marginal_sigma = Vec::with_capacity(mz_buckets.len());
for mut v in mz_buckets {
if v.is_empty() {
mz_marginal.push(None);
mz_marginal_sigma.push(None);
} else {
let m = median(&mut v);
mz_marginal.push(Some(m));
mz_marginal_sigma.push(Some(mad_sigma(&mut v, m)));
}
}
let global = median(&mut all);
let global_sigma = mad_sigma(&mut all, global);
Some(MzRecalModel {
mz_min: mz_lo,
mz_inv_width,
n_mz,
rt_min: rt_lo,
rt_inv_width,
n_rt,
cell,
mz_marginal,
global,
cell_sigma,
mz_marginal_sigma,
n_samples: self.samples.len() as u64,
global_sigma,
})
}
}
fn median(v: &mut [f64]) -> f64 {
v.sort_by(|a, b| a.total_cmp(b));
let n = v.len();
if n % 2 == 1 {
v[n / 2]
} else {
0.5 * (v[n / 2 - 1] + v[n / 2])
}
}
fn mad_sigma(v: &mut [f64], center: f64) -> f64 {
if v.len() < 2 {
return 0.0;
}
let mut dev: Vec<f64> = v.iter().map(|x| (x - center).abs()).collect();
1.4826 * median(&mut dev)
}
#[cfg(test)]
#[path = "recalibration_tests.rs"]
mod tests;