From 6c8533f7a686da08682697090e2659da98d5b972 Mon Sep 17 00:00:00 2001 From: Emilia Date: Tue, 13 Feb 2024 19:56:47 -0500 Subject: [PATCH] lib: auto creation from csv --- Cargo.lock | 1 + plate-tool-lib/Cargo.toml | 1 + plate-tool-lib/src/csv/auto.rs | 173 ++++++++++++++++++++++ plate-tool-lib/src/csv/conversion.rs | 20 ++- plate-tool-lib/src/csv/mod.rs | 3 + plate-tool-lib/src/csv/transfer_record.rs | 2 +- plate-tool-lib/src/transfer_region.rs | 6 + plate-tool-lib/src/util.rs | 2 +- 8 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 plate-tool-lib/src/csv/auto.rs diff --git a/Cargo.lock b/Cargo.lock index a08ee23..492180e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -637,6 +637,7 @@ version = "0.1.0" dependencies = [ "csv", "getrandom", + "lazy_static", "log", "rand", "regex", diff --git a/plate-tool-lib/Cargo.toml b/plate-tool-lib/Cargo.toml index c8103a2..05f1b09 100644 --- a/plate-tool-lib/Cargo.toml +++ b/plate-tool-lib/Cargo.toml @@ -14,3 +14,4 @@ serde_json = "1.0" csv = "1.2" getrandom = { version = "0.2", features = ["js"] } rand = { version = "0.8", features = ["small_rng"] } +lazy_static = "1.4" diff --git a/plate-tool-lib/src/csv/auto.rs b/plate-tool-lib/src/csv/auto.rs new file mode 100644 index 0000000..a35c6ce --- /dev/null +++ b/plate-tool-lib/src/csv/auto.rs @@ -0,0 +1,173 @@ +use super::{string_well_to_pt, TransferRecord, read_csv}; +use crate::plate::{PlateFormat, PlateType}; +use crate::plate_instances::PlateInstance; +use crate::transfer::Transfer; +use crate::transfer_region::{CustomRegion, Region, TransferRegion}; + +use std::collections::HashSet; + +pub struct AutoOutput { + pub sources: Vec, + pub destinations: Vec, + pub transfers: Vec, +} + +struct UniquePlates { + sources: Vec, + destinations: Vec, +} + +const W96_COL_MAX: u8 = 12; +const W96_ROW_MAX: u8 = 8; +const W384_COL_MAX: u8 = 24; +const W384_ROW_MAX: u8 = 16; +const W1536_COL_MAX: u8 = 48; +const W1536_ROW_MAX: u8 = 32; + +pub fn auto(records: &Vec) -> AutoOutput { + let unique_plates = find_unique_plates(records); + let transfers = get_transfer_for_all_pairs(records, &unique_plates); + + AutoOutput { sources: unique_plates.sources, destinations: unique_plates.destinations, transfers } +} + +pub fn read_csv_auto(data: &str) -> AutoOutput { + let transfer_records = read_csv(data); + auto(&transfer_records) +} + +fn find_unique_plates(records: &[TransferRecord]) -> UniquePlates { + let mut source_names: HashSet<&str> = HashSet::new(); + let mut destination_names: HashSet<&str> = HashSet::new(); + + for record in records { + source_names.insert(&record.source_well); + destination_names.insert(&record.destination_well); + } + + let mut sources: Vec = Vec::with_capacity(source_names.len()); + for source_name in source_names { + let filtered_records: Vec<&TransferRecord> = records + .iter() + .filter(|x| x.source_plate == source_name) + .collect(); + let format_guess = guess_plate_size(&filtered_records, PlateType::Source); + sources.push(PlateInstance::new( + PlateType::Source, + format_guess, + source_name.to_string(), + )) + } + + let mut destinations: Vec = Vec::with_capacity(destination_names.len()); + for destination_name in destination_names { + let filtered_records: Vec<&TransferRecord> = records + .iter() + .filter(|x| x.destination_plate == destination_name) + .collect(); + let format_guess = guess_plate_size(&filtered_records, PlateType::Destination); + destinations.push(PlateInstance::new( + PlateType::Destination, + format_guess, + destination_name.to_string(), + )) + } + + UniquePlates { + sources, + destinations, + } +} + +fn guess_plate_size(plate_filtered_records: &[&TransferRecord], which: PlateType) -> PlateFormat { + let mut guess = PlateFormat::W96; // Never guess smaller than 96 + for record in plate_filtered_records { + if let Some((row, col)) = string_well_to_pt(match which { + PlateType::Source => &record.source_well, + PlateType::Destination => &record.destination_well, + }) { + if row > W1536_ROW_MAX || col > W1536_COL_MAX { + return PlateFormat::W3456; + } else if row > W384_ROW_MAX || col > W384_COL_MAX { + guess = PlateFormat::W1536; + } else if row > W96_ROW_MAX || col > W96_COL_MAX { + guess = PlateFormat::W384; + } + } + } + guess +} + +fn get_transfer_for_all_pairs( + records: &[TransferRecord], + unique_plates: &UniquePlates, +) -> Vec { + let mut transfers: Vec = Vec::new(); + for source_instance in &unique_plates.sources { + for destination_instance in &unique_plates.destinations { + if let Some(transfer) = + get_transfer_for_pair(records, source_instance, destination_instance) + { + transfers.push(transfer); + } + } + } + + transfers +} + +fn get_transfer_for_pair( + records: &[TransferRecord], + source: &PlateInstance, + destination: &PlateInstance, +) -> Option { + let source_name: &str = &source.name; + let destination_name: &str = &destination.name; + + let mut filtered_records = records + .iter() + .filter(|x| x.source_plate == source_name && x.destination_plate == destination_name) + .peekable(); + + if filtered_records.peek().is_none() { + return None; + } + + let mut source_wells: HashSet<(u8, u8)> = HashSet::new(); + let mut destination_wells: HashSet<(u8, u8)> = HashSet::new(); + for record in filtered_records { + let source_point_opt = string_well_to_pt(&record.source_well); + let destination_point_opt = string_well_to_pt(&record.destination_well); + + if source_point_opt.and(destination_point_opt).is_some() { + let source_point = source_point_opt.unwrap(); + let destination_point = destination_point_opt.unwrap(); + + source_wells.insert(source_point); + destination_wells.insert(destination_point); + } + } + let source_wells_vec: Vec<(u8, u8)> = source_wells.into_iter().collect(); + let destination_wells_vec: Vec<(u8, u8)> = destination_wells.into_iter().collect(); + + let custom_region: Region = + Region::Custom(CustomRegion::new(source_wells_vec, destination_wells_vec)); + + let transfer_region = TransferRegion { + source_plate: source.plate, + dest_plate: destination.plate, + interleave_source: (1, 1), + interleave_dest: (1, 1), + source_region: custom_region.clone(), + dest_region: custom_region, + }; + + let transfer_name = format!("{} to {}", source.name, destination.name); + + Some(Transfer::new( + source.clone(), + destination.clone(), + transfer_region, + transfer_name, + )) +} diff --git a/plate-tool-lib/src/csv/conversion.rs b/plate-tool-lib/src/csv/conversion.rs index f9cfeab..ba10250 100644 --- a/plate-tool-lib/src/csv/conversion.rs +++ b/plate-tool-lib/src/csv/conversion.rs @@ -1,10 +1,11 @@ use crate::transfer::Transfer; use crate::util::*; +use super::TransferRecord; +use lazy_static::lazy_static; +use regex::Regex; use serde::{Deserialize, Serialize}; use std::error::Error; -use super::TransferRecord; - pub fn transfer_to_records( tr: &Transfer, @@ -43,6 +44,21 @@ pub fn records_to_csv(trs: Vec) -> Result Ok(data) } +pub fn string_well_to_pt(input: &str) -> Option<(u8, u8)> { + lazy_static! { + static ref REGEX: Regex = Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap(); + } + if let Some(c1) = REGEX.captures(input) { + if let (Some(row), Some(col)) = (letters_to_num(&c1[1]), c1[2].parse::().ok()) { + Some((row, col)) + } else { + None + } + } else { + None + } +} + pub fn read_csv(data: &str) -> Vec { let (header, data) = data.split_at(data.find('\n').unwrap()); let modified: String = header.to_lowercase() + data; diff --git a/plate-tool-lib/src/csv/mod.rs b/plate-tool-lib/src/csv/mod.rs index fddbe32..d72fe26 100644 --- a/plate-tool-lib/src/csv/mod.rs +++ b/plate-tool-lib/src/csv/mod.rs @@ -1,6 +1,9 @@ mod transfer_record; mod conversion; +mod auto; pub use transfer_record::volume_default; pub use transfer_record::TransferRecord; pub use conversion::*; + +pub use auto::{auto, read_csv_auto}; diff --git a/plate-tool-lib/src/csv/transfer_record.rs b/plate-tool-lib/src/csv/transfer_record.rs index 794c2ad..ac54c87 100644 --- a/plate-tool-lib/src/csv/transfer_record.rs +++ b/plate-tool-lib/src/csv/transfer_record.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::transfer::Transfer; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct TransferRecord { #[serde(rename = "Source Plate", alias = "source plate", alias = "src plate")] pub source_plate: String, diff --git a/plate-tool-lib/src/transfer_region.rs b/plate-tool-lib/src/transfer_region.rs index 5f7d55f..ced6b13 100644 --- a/plate-tool-lib/src/transfer_region.rs +++ b/plate-tool-lib/src/transfer_region.rs @@ -8,6 +8,12 @@ pub struct CustomRegion { dest: Vec<(u8, u8)>, } +impl CustomRegion { + pub fn new(src: Vec<(u8, u8)>, dest: Vec<(u8, u8)>) -> Self { + CustomRegion { src, dest } + } +} + #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] pub enum Region { Rect((u8, u8), (u8, u8)), diff --git a/plate-tool-lib/src/util.rs b/plate-tool-lib/src/util.rs index b8ec34d..477cbd2 100644 --- a/plate-tool-lib/src/util.rs +++ b/plate-tool-lib/src/util.rs @@ -33,7 +33,7 @@ pub fn num_to_letters(num: u8) -> Option { #[cfg(test)] mod tests { - use super::{letters_to_num, num_to_letters, RegionDisplay}; + use super::{letters_to_num, num_to_letters}; #[test] fn test_letters_to_num() {