lib: auto creation from csv

This commit is contained in:
Emilia Allison 2024-02-13 19:56:47 -05:00
parent d7c98d37a2
commit 6c8533f7a6
Signed by: emilia
GPG Key ID: 05D5D1107E5100A1
8 changed files with 204 additions and 4 deletions

1
Cargo.lock generated
View File

@ -637,6 +637,7 @@ version = "0.1.0"
dependencies = [
"csv",
"getrandom",
"lazy_static",
"log",
"rand",
"regex",

View File

@ -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"

View File

@ -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<PlateInstance>,
pub destinations: Vec<PlateInstance>,
pub transfers: Vec<Transfer>,
}
struct UniquePlates {
sources: Vec<PlateInstance>,
destinations: Vec<PlateInstance>,
}
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<TransferRecord>) -> 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<PlateInstance> = 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<PlateInstance> = 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<Transfer> {
let mut transfers: Vec<Transfer> = 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<Transfer> {
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,
))
}

View File

@ -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<TransferRecord>) -> Result<String, Box<dyn Error>
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::<u8>().ok()) {
Some((row, col))
} else {
None
}
} else {
None
}
}
pub fn read_csv(data: &str) -> Vec<TransferRecord> {
let (header, data) = data.split_at(data.find('\n').unwrap());
let modified: String = header.to_lowercase() + data;

View File

@ -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};

View File

@ -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,

View File

@ -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)),

View File

@ -33,7 +33,7 @@ pub fn num_to_letters(num: u8) -> Option<String> {
#[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() {