use crate::PeacoQCData;
use crate::error::Result;
use crate::stats::median_mad::median_mad_scaled;
#[derive(Debug, Clone, PartialEq)]
pub struct DoubletConfig {
pub channel1: String,
pub channel2: String,
pub nmad: f64,
pub b: f64,
}
impl Default for DoubletConfig {
fn default() -> Self {
Self {
channel1: "FSC-A".to_string(),
channel2: "FSC-H".to_string(),
nmad: 4.0,
b: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct DoubletResult {
pub mask: Vec<bool>,
pub median_ratio: f64,
pub mad_ratio: f64,
pub threshold: f64,
pub percentage_removed: f64,
}
pub fn remove_doublets<T: PeacoQCData>(fcs: &T, config: &DoubletConfig) -> Result<DoubletResult> {
let values1 = fcs.get_channel_f64(&config.channel1)?;
let values2 = fcs.get_channel_f64(&config.channel2)?;
let mut ratios = Vec::with_capacity(fcs.n_events());
for (a, h) in values1.iter().zip(values2.iter()) {
let ratio = *a / (1e-10 + *h + config.b);
ratios.push(ratio);
}
let (median, mad) = median_mad_scaled(&ratios)?;
let threshold = median + config.nmad * mad;
let mask: Vec<bool> = ratios.iter().map(|&r| r < threshold).collect();
let n_removed = mask.iter().filter(|&&x| !x).count();
let percentage_removed = (n_removed as f64 / fcs.n_events() as f64) * 100.0;
Ok(DoubletResult {
mask,
median_ratio: median,
mad_ratio: mad,
threshold,
percentage_removed,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fcs::SimpleFcs;
use polars::df;
use std::collections::HashMap;
use std::sync::Arc;
#[test]
fn test_remove_doublets() {
let df = Arc::new(
df![
"FSC-A" => &[100.0, 200.0, 300.0, 400.0, 1000.0], "FSC-H" => &[50.0, 100.0, 150.0, 200.0, 100.0],
]
.unwrap(),
);
let fcs = SimpleFcs {
data_frame: df,
parameter_metadata: HashMap::new(),
};
let config = DoubletConfig::default();
let result = remove_doublets(&fcs, &config).unwrap();
assert!(result.percentage_removed > 0.0);
assert!(result.threshold > result.median_ratio);
}
}