use crate::PeacoQCData;
use crate::error::{PeacoQCError, Result};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct MarginConfig {
pub channels: Vec<String>,
pub channel_specifications: Option<HashMap<String, (f64, f64)>>,
pub remove_min: Option<Vec<String>>,
pub remove_max: Option<Vec<String>>,
}
impl Default for MarginConfig {
fn default() -> Self {
Self {
channels: Vec::new(),
channel_specifications: None,
remove_min: None,
remove_max: None,
}
}
}
#[derive(Debug, Clone)]
pub struct MarginResult {
pub mask: Vec<bool>,
pub margin_matrix: HashMap<String, (usize, usize)>,
pub percentage_removed: f64,
}
pub fn remove_margins<T: PeacoQCData>(fcs: &T, config: &MarginConfig) -> Result<MarginResult> {
if config.channels.is_empty() {
return Err(PeacoQCError::ConfigError(
"No channels specified for margin removal".to_string(),
));
}
let n_events = fcs.n_events();
let mut mask = vec![true; n_events];
let mut margin_matrix = HashMap::new();
let remove_min = config.remove_min.as_ref().unwrap_or(&config.channels);
let remove_max = config.remove_max.as_ref().unwrap_or(&config.channels);
for channel in &config.channels {
let values = fcs.get_channel_f64(channel)?;
let data_min = values.iter().copied().fold(f64::INFINITY, f64::min);
let data_max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let (min_range, max_range) = if let Some(specs) = &config.channel_specifications {
if let Some(&(min, max)) = specs.get(channel) {
(min, max)
} else {
fcs.get_channel_range(channel).unwrap_or_else(|| {
(data_min.min(0.0), data_max.max(262144.0))
})
}
} else {
fcs.get_channel_range(channel).unwrap_or_else(|| {
(data_min.min(0.0), data_max.max(262144.0))
})
};
let mut min_removed = 0;
let mut max_removed = 0;
if remove_min.contains(channel) {
let threshold = min_range.min(0.0).max(data_min);
for (i, &v) in values.iter().enumerate() {
if v <= threshold {
mask[i] = false;
min_removed += 1;
}
}
}
if remove_max.contains(channel) {
let threshold = max_range.min(data_max);
for (i, &v) in values.iter().enumerate() {
if v > threshold && mask[i] {
mask[i] = false;
max_removed += 1;
}
}
}
margin_matrix.insert(channel.clone(), (min_removed, max_removed));
}
let n_removed = mask.iter().filter(|&&x| !x).count();
let percentage_removed = (n_removed as f64 / n_events as f64) * 100.0;
if percentage_removed > 10.0 {
eprintln!(
"Warning: More than {:.2}% of events removed as margin events. This should be verified.",
percentage_removed
);
}
Ok(MarginResult {
mask,
margin_matrix,
percentage_removed,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fcs::{ParameterMetadata, SimpleFcs};
use polars::df;
use std::collections::HashMap;
use std::sync::Arc;
#[test]
fn test_remove_margins_basic() {
let df = Arc::new(df![
"FSC-A" => &[100.0, 200.0, 300.0, 0.0, 262144.0, 150.0],
"SSC-A" => &[50.0, 100.0, 150.0, 200.0, 250.0, 300.0],
]
.unwrap());
let mut metadata = HashMap::new();
metadata.insert(
"FSC-A".to_string(),
ParameterMetadata {
min_range: 0.0,
max_range: 262144.0,
name: "FSC-A".to_string(),
},
);
metadata.insert(
"SSC-A".to_string(),
ParameterMetadata {
min_range: 0.0,
max_range: 262144.0,
name: "SSC-A".to_string(),
},
);
let fcs = SimpleFcs {
data_frame: df,
parameter_metadata: metadata,
};
let config = MarginConfig {
channels: vec!["FSC-A".to_string()],
..Default::default()
};
let result = remove_margins(&fcs, &config).unwrap();
assert_eq!(result.mask.iter().filter(|&&x| !x).count(), 2);
assert!(result.percentage_removed > 0.0);
}
}