use crate::error::{PeacoQCError, Result};
use crate::qc::{PeacoQCConfig, PeacoQCResult};
use serde::Serialize;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QCExportFormat {
BooleanCsv,
NumericCsv,
JsonMetadata,
}
#[derive(Debug, Clone)]
pub struct QCExportOptions {
pub column_name: String,
pub good_value: u16,
pub bad_value: u16,
}
impl Default for QCExportOptions {
fn default() -> Self {
Self {
column_name: "PeacoQC".to_string(),
good_value: 2000,
bad_value: 6000,
}
}
}
#[derive(Debug, Serialize, serde::Deserialize)]
struct QCJsonMetadata {
n_events_before: usize,
n_events_after: usize,
n_events_removed: usize,
percentage_removed: f64,
it_percentage: Option<f64>,
mad_percentage: Option<f64>,
consecutive_percentage: f64,
n_bins: usize,
events_per_bin: usize,
channels_analyzed: Vec<String>,
config: QCConfigJson,
}
#[derive(Debug, Serialize, serde::Deserialize)]
struct QCConfigJson {
qc_mode: String,
mad: f64,
it_limit: f64,
consecutive_bins: usize,
remove_zeros: bool,
}
impl From<&PeacoQCConfig> for QCConfigJson {
fn from(config: &PeacoQCConfig) -> Self {
Self {
qc_mode: format!("{:?}", config.determine_good_cells),
mad: config.mad,
it_limit: config.it_limit,
consecutive_bins: config.consecutive_bins,
remove_zeros: config.remove_zeros,
}
}
}
pub fn export_csv_boolean(
result: &PeacoQCResult,
path: impl AsRef<Path>,
column_name: Option<&str>,
) -> Result<()> {
let path = path.as_ref();
let column_name = column_name.unwrap_or("PeacoQC");
if result.good_cells.is_empty() {
return Err(PeacoQCError::ExportError(
"Cannot export empty QC results".to_string(),
));
}
let file = File::create(path).map_err(|e| {
PeacoQCError::WriteError(format!("Failed to create file {}: {}", path.display(), e))
})?;
let mut writer = BufWriter::new(file);
writeln!(writer, "{}", column_name).map_err(|e| {
PeacoQCError::WriteError(format!("Failed to write header: {}", e))
})?;
for &is_good in &result.good_cells {
let value = if is_good { 1 } else { 0 };
writeln!(writer, "{}", value).map_err(|e| {
PeacoQCError::WriteError(format!("Failed to write data: {}", e))
})?;
}
writer.flush().map_err(|e| {
PeacoQCError::WriteError(format!("Failed to flush file: {}", e))
})?;
Ok(())
}
pub fn export_csv_numeric(
result: &PeacoQCResult,
path: impl AsRef<Path>,
good_value: u16,
bad_value: u16,
column_name: Option<&str>,
) -> Result<()> {
let path = path.as_ref();
let column_name = column_name.unwrap_or("PeacoQC");
if result.good_cells.is_empty() {
return Err(PeacoQCError::ExportError(
"Cannot export empty QC results".to_string(),
));
}
let file = File::create(path).map_err(|e| {
PeacoQCError::WriteError(format!("Failed to create file {}: {}", path.display(), e))
})?;
let mut writer = BufWriter::new(file);
writeln!(writer, "{}", column_name).map_err(|e| {
PeacoQCError::WriteError(format!("Failed to write header: {}", e))
})?;
for &is_good in &result.good_cells {
let value = if is_good { good_value } else { bad_value };
writeln!(writer, "{}", value).map_err(|e| {
PeacoQCError::WriteError(format!("Failed to write data: {}", e))
})?;
}
writer.flush().map_err(|e| {
PeacoQCError::WriteError(format!("Failed to flush file: {}", e))
})?;
Ok(())
}
pub fn export_json_metadata(
result: &PeacoQCResult,
config: &PeacoQCConfig,
path: impl AsRef<Path>,
) -> Result<()> {
let path = path.as_ref();
let n_events_before = result.good_cells.len();
let n_events_after = result.good_cells.iter().filter(|&&x| x).count();
let n_events_removed = n_events_before - n_events_after;
let metadata = QCJsonMetadata {
n_events_before,
n_events_after,
n_events_removed,
percentage_removed: result.percentage_removed,
it_percentage: result.it_percentage,
mad_percentage: result.mad_percentage,
consecutive_percentage: result.consecutive_percentage,
n_bins: result.n_bins,
events_per_bin: result.events_per_bin,
channels_analyzed: config.channels.clone(),
config: QCConfigJson::from(config),
};
let file = File::create(path).map_err(|e| {
PeacoQCError::WriteError(format!("Failed to create file {}: {}", path.display(), e))
})?;
serde_json::to_writer_pretty(file, &metadata).map_err(|e| {
PeacoQCError::ExportError(format!("Failed to serialize JSON: {}", e))
})?;
Ok(())
}
#[cfg(feature = "flow-fcs")]
pub fn export_fcs_with_column(
_fcs: &flow_fcs::Fcs,
_result: &PeacoQCResult,
_output_path: impl AsRef<Path>,
_filter: bool,
) -> Result<()> {
Err(PeacoQCError::ExportError(
"FCS writing not yet implemented in flow-fcs crate".to_string(),
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::qc::{PeacoQCConfig, PeacoQCResult, QCMode};
use std::collections::HashMap;
use std::fs;
use tempfile::TempDir;
fn create_test_result() -> PeacoQCResult {
PeacoQCResult {
good_cells: vec![true, true, false, true, false],
percentage_removed: 40.0,
it_percentage: Some(20.0),
mad_percentage: Some(20.0),
consecutive_percentage: 0.0,
peaks: HashMap::new(),
n_bins: 10,
events_per_bin: 50,
}
}
fn create_test_config() -> PeacoQCConfig {
PeacoQCConfig {
channels: vec!["FL1-A".to_string(), "FL2-A".to_string()],
determine_good_cells: QCMode::All,
mad: 6.0,
it_limit: 0.6,
consecutive_bins: 5,
remove_zeros: false,
..Default::default()
}
}
#[test]
fn test_export_csv_boolean() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test_boolean.csv");
let result = create_test_result();
export_csv_boolean(&result, &path, None).unwrap();
let content = fs::read_to_string(&path).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines[0], "PeacoQC");
assert_eq!(lines[1], "1");
assert_eq!(lines[2], "1");
assert_eq!(lines[3], "0");
assert_eq!(lines[4], "1");
assert_eq!(lines[5], "0");
}
#[test]
fn test_export_csv_numeric() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test_numeric.csv");
let result = create_test_result();
export_csv_numeric(&result, &path, 2000, 6000, None).unwrap();
let content = fs::read_to_string(&path).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines[0], "PeacoQC");
assert_eq!(lines[1], "2000");
assert_eq!(lines[2], "2000");
assert_eq!(lines[3], "6000");
assert_eq!(lines[4], "2000");
assert_eq!(lines[5], "6000");
}
#[test]
fn test_export_json_metadata() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test_metadata.json");
let result = create_test_result();
let config = create_test_config();
export_json_metadata(&result, &config, &path).unwrap();
let content = fs::read_to_string(&path).unwrap();
let metadata: QCJsonMetadata = serde_json::from_str(&content).unwrap();
assert_eq!(metadata.n_events_before, 5);
assert_eq!(metadata.n_events_after, 3);
assert_eq!(metadata.n_events_removed, 2);
assert_eq!(metadata.percentage_removed, 40.0);
assert_eq!(metadata.channels_analyzed, vec!["FL1-A", "FL2-A"]);
}
#[test]
fn test_export_empty_result() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test_empty.csv");
let result = PeacoQCResult {
good_cells: vec![],
percentage_removed: 0.0,
it_percentage: None,
mad_percentage: None,
consecutive_percentage: 0.0,
peaks: HashMap::new(),
n_bins: 0,
events_per_bin: 0,
};
assert!(export_csv_boolean(&result, &path, None).is_err());
}
#[test]
fn test_custom_column_name() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test_custom.csv");
let result = create_test_result();
export_csv_boolean(&result, &path, Some("QC_Result")).unwrap();
let content = fs::read_to_string(&path).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines[0], "QC_Result");
}
}