From 4d82ea5567658c5fe99a4e56c39cbd3c0b071e53 Mon Sep 17 00:00:00 2001 From: Emilia Date: Sun, 11 Feb 2024 20:03:29 -0500 Subject: [PATCH] lib migration pt3 --- plate-tool-lib/src/csv.rs | 62 +++ plate-tool-lib/src/lib.rs | 6 + plate-tool-lib/src/plate.rs | 77 ++++ plate-tool-lib/src/plate_instances.rs | 50 +++ plate-tool-lib/src/transfer.rs | 58 +++ plate-tool-lib/src/transfer_region.rs | 591 ++++++++++++++++++++++++++ plate-tool-lib/src/util.rs | 70 +++ 7 files changed, 914 insertions(+) create mode 100644 plate-tool-lib/src/csv.rs create mode 100644 plate-tool-lib/src/lib.rs create mode 100644 plate-tool-lib/src/plate.rs create mode 100644 plate-tool-lib/src/plate_instances.rs create mode 100644 plate-tool-lib/src/transfer.rs create mode 100644 plate-tool-lib/src/transfer_region.rs create mode 100644 plate-tool-lib/src/util.rs diff --git a/plate-tool-lib/src/csv.rs b/plate-tool-lib/src/csv.rs new file mode 100644 index 0000000..02feb01 --- /dev/null +++ b/plate-tool-lib/src/csv.rs @@ -0,0 +1,62 @@ +// use crate::components::states::MainState; +use crate::transfer::Transfer; +use crate::util::*; + +use serde::{Serialize, Deserialize}; +use std::error::Error; + +#[derive(Serialize, Deserialize, Debug)] +pub struct TransferRecord { + #[serde(rename = "Source Plate", alias = "source plate", alias = "src plate")] + pub source_plate: String, + #[serde(rename = "Source Well", alias = "source well", alias = "src well")] + pub source_well: String, + #[serde(rename = "Dest Plate", alias = "dest plate", alias = "destination plate")] + pub destination_plate: String, + #[serde(rename = "Destination Well", alias = "destination well", alias = "dest well")] + pub destination_well: String, + #[serde(rename = "Transfer Volume", alias = "transfer volume")] + pub volume: f32, + #[serde(rename = "Concentration", alias = "concentration")] + pub concentration: Option, +} + +/* +*/ + +pub fn transfer_to_records( + tr: &Transfer, + src_barcode: &str, + dest_barcode: &str, +) -> Vec { + let source_wells = tr.transfer_region.get_source_wells(); + let map = tr.transfer_region.calculate_map(); + + let mut records: Vec = vec![]; + + for s_well in source_wells { + let dest_wells = map(s_well); + if let Some(dest_wells) = dest_wells { + for d_well in dest_wells { + records.push(TransferRecord { + source_plate: src_barcode.to_string(), + source_well: format!("{}{}", num_to_letters(s_well.0).unwrap(), s_well.1), + destination_plate: dest_barcode.to_string(), + destination_well: format!("{}{}", num_to_letters(d_well.0).unwrap(), d_well.1), + volume: tr.volume, + concentration: None, + }) + } + } + } + records +} + +pub fn records_to_csv(trs: Vec) -> Result> { + let mut wtr = csv::WriterBuilder::new().from_writer(vec![]); + for record in trs { + wtr.serialize(record)? + } + let data = String::from_utf8(wtr.into_inner()?)?; + Ok(data) +} diff --git a/plate-tool-lib/src/lib.rs b/plate-tool-lib/src/lib.rs new file mode 100644 index 0000000..a85a4bb --- /dev/null +++ b/plate-tool-lib/src/lib.rs @@ -0,0 +1,6 @@ +pub mod csv; +pub mod plate; +pub mod plate_instances; +pub mod transfer; +pub mod transfer_region; +pub mod util; diff --git a/plate-tool-lib/src/plate.rs b/plate-tool-lib/src/plate.rs new file mode 100644 index 0000000..2331aea --- /dev/null +++ b/plate-tool-lib/src/plate.rs @@ -0,0 +1,77 @@ +use serde::{Deserialize, Serialize}; + +#[derive(PartialEq, Eq, Default, Clone, Copy, Serialize, Deserialize, Debug)] +pub struct Plate { + pub plate_type: PlateType, + pub plate_format: PlateFormat, +} + +impl Plate { + pub fn new(plate_type: PlateType, plate_format: PlateFormat) -> Self { + Plate { + plate_type, + plate_format, + } + } + + pub fn size(&self) -> (u8, u8) { + self.plate_format.size() + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)] +pub enum PlateType { + Source, + Destination, +} +impl Default for PlateType { + fn default() -> Self { + Self::Source + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)] +pub enum PlateFormat { + W6, + W12, + W24, + W48, + W96, + W384, + W1536, + W3456, +} +impl Default for PlateFormat { + fn default() -> Self { + Self::W96 + } +} +impl std::fmt::Display for PlateFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlateFormat::W6 => write!(f, "6"), + PlateFormat::W12 => write!(f, "12"), + PlateFormat::W24 => write!(f, "24"), + PlateFormat::W48 => write!(f, "48"), + PlateFormat::W96 => write!(f, "96"), + PlateFormat::W384 => write!(f, "384"), + PlateFormat::W1536 => write!(f, "1536"), + PlateFormat::W3456 => write!(f, "3456"), + } + } +} + +impl PlateFormat { + pub fn size(&self) -> (u8, u8) { + match self { + PlateFormat::W6 => (2, 3), + PlateFormat::W12 => (3, 4), + PlateFormat::W24 => (4, 6), + PlateFormat::W48 => (6, 8), + PlateFormat::W96 => (8, 12), + PlateFormat::W384 => (16, 24), + PlateFormat::W1536 => (32, 48), + PlateFormat::W3456 => (48, 72), + } + } +} diff --git a/plate-tool-lib/src/plate_instances.rs b/plate-tool-lib/src/plate_instances.rs new file mode 100644 index 0000000..c8a1e6d --- /dev/null +++ b/plate-tool-lib/src/plate_instances.rs @@ -0,0 +1,50 @@ +use super::plate::*; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(PartialEq, Clone, Serialize, Deserialize)] +pub struct PlateInstance { + pub plate: Plate, + #[serde(rename = "id_v7")] + #[serde(default = "Uuid::now_v7")] + id: Uuid, + pub name: String, +} + +impl PlateInstance { + pub fn new(sort: PlateType, format: PlateFormat, name: String) -> Self { + PlateInstance { + plate: Plate { + plate_type: sort, + plate_format: format, + }, + id: Uuid::now_v7(), + name, + } + } + + pub fn get_uuid(&self) -> Uuid { + self.id + } + + pub fn change_name(&mut self, new_name: String) { + self.name = new_name; + } +} + +impl From for PlateInstance { + fn from(value: Plate) -> Self { + PlateInstance { + plate: value, + id: Uuid::now_v7(), + name: "New Plate".to_string(), + } + } +} + +impl From<&PlateInstance> for String { + fn from(value: &PlateInstance) -> Self { + // Could have other formatting here + format!("{}, {}", value.name, value.plate.plate_format) + } +} diff --git a/plate-tool-lib/src/transfer.rs b/plate-tool-lib/src/transfer.rs new file mode 100644 index 0000000..b27868b --- /dev/null +++ b/plate-tool-lib/src/transfer.rs @@ -0,0 +1,58 @@ +use super::plate_instances::*; +use super::transfer_region::*; +use serde::Deserialize; +use serde::Serialize; +use uuid::Uuid; + +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[non_exhaustive] +pub struct Transfer { + pub source_id: Uuid, + pub dest_id: Uuid, + pub name: String, + #[serde(rename = "id_v7")] + #[serde(default = "Uuid::now_v7")] + pub id: Uuid, + pub transfer_region: TransferRegion, + #[serde(default = "default_volume")] + pub volume: f32, +} + +impl Default for Transfer { + fn default() -> Self { + Transfer { + source_id: Default::default(), + dest_id: Default::default(), + name: "New Transfer".to_string(), + id: Default::default(), + transfer_region: Default::default(), + volume: 2.5f32, + } + } +} + +fn default_volume() -> f32 { + 2.5f32 +} + +impl Transfer { + pub fn new( + source: PlateInstance, + dest: PlateInstance, + tr: TransferRegion, + name: String, + ) -> Self { + Self { + source_id: source.get_uuid(), + dest_id: dest.get_uuid(), + name, + id: Uuid::now_v7(), + transfer_region: tr, + volume: 2.5, + } + } + + pub fn get_uuid(&self) -> Uuid { + self.id + } +} diff --git a/plate-tool-lib/src/transfer_region.rs b/plate-tool-lib/src/transfer_region.rs new file mode 100644 index 0000000..5f7d55f --- /dev/null +++ b/plate-tool-lib/src/transfer_region.rs @@ -0,0 +1,591 @@ +use serde::{Deserialize, Serialize}; + +use super::plate::Plate; + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] +pub struct CustomRegion { + src: Vec<(u8, u8)>, + dest: Vec<(u8, u8)>, +} + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] +pub enum Region { + Rect((u8, u8), (u8, u8)), + Point((u8, u8)), + Custom(CustomRegion), +} +impl Default for Region { + fn default() -> Self { + Region::Point((1, 1)) + } +} +impl TryFrom for ((u8, u8), (u8, u8)) { + type Error = &'static str; + fn try_from(region: Region) -> Result { + if let Region::Rect(c1, c2) = region { + Ok((c1, c2)) + } else { + // Should consider returning a degenerate rectangle here instead + Err("Cannot convert this region to a rectangle, it was a point.") + } + } +} + +type Corner = (u8, u8); +type Rectangle = (Corner, Corner); +impl Region { + pub fn new_custom(transfers: &Vec) -> Self { + let mut src_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len()); + let mut dest_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len()); + + for transfer in transfers { + src_pts.push(transfer.0); + dest_pts.push(transfer.1); + } + + Region::Custom(CustomRegion { + src: src_pts, + dest: dest_pts, + }) + } +} + +#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug)] +pub struct TransferRegion { + pub source_plate: Plate, + pub source_region: Region, // Even if it is just a point, we don't want corners. + pub dest_plate: Plate, + pub dest_region: Region, + pub interleave_source: (i8, i8), + pub interleave_dest: (i8, i8), +} + +impl Default for TransferRegion { + fn default() -> Self { + TransferRegion { + source_plate: Plate::default(), + source_region: Region::default(), + dest_plate: Plate::default(), + dest_region: Region::default(), + interleave_source: (1, 1), + interleave_dest: (1, 1), + } + } +} + +impl TransferRegion { + pub fn get_source_wells(&self) -> Vec<(u8, u8)> { + match &self.source_region { + Region::Rect(c1, c2) => { + let mut wells = Vec::<(u8, u8)>::new(); + let (ul, br) = standardize_rectangle(c1, c2); + let (interleave_i, interleave_j) = self.interleave_source; + // NOTE: This will panic if either is 0! + // We'll reassign these values (still not mutable) just in case. + // This behaviour shouldn't be replicated for destination wells + // because a zero step permits pooling. + let (interleave_i, interleave_j) = + (i8::max(interleave_i, 1), i8::max(interleave_j, 1)); + + for i in (ul.0..=br.0).step_by(i8::abs(interleave_i) as usize) { + for j in (ul.1..=br.1).step_by(i8::abs(interleave_j) as usize) { + // NOTE: It looks like we're ignoring negative interleaves, + // because it wouldn't make a difference here---the same + // wells will still be involved in the transfer. + wells.push((i, j)) + } + } + wells + } + Region::Point(p) => vec![*p], + Region::Custom(c) => c.src.clone(), + } + } + + pub fn get_destination_wells(&self) -> Vec<(u8, u8)> { + let map = self.calculate_map(); + let source_wells = self.get_source_wells(); + + let mut wells = Vec::<(u8, u8)>::new(); + + // log::debug!("GDW:"); + for well in source_wells { + if let Some(mut dest_wells) = map(well) { + // log::debug!("Map {:?} to {:?}", well, dest_wells); + wells.append(&mut dest_wells); + } + } + // log::debug!("GDW END."); + + wells + } + + #[allow(clippy::type_complexity)] // Resolving gives inherent associated type error + pub fn calculate_map(&self) -> Box Option> + '_> { + // By validating first, we have a stronger guarantee that + // this function will not panic. :) + // log::debug!("Validating: {:?}", self.validate()); + if let Err(msg) = self.validate() { + eprintln!("{}", msg); + eprintln!("This transfer will be empty."); + return Box::new(|(_, _)| None); + } + + // log::debug!("What is ild? {:?}", self); + let source_wells = self.get_source_wells(); + let il_dest = self.interleave_dest; + let il_source = self.interleave_source; + + let source_corners: ((u8, u8), (u8, u8)) = match self.source_region { + Region::Point((x, y)) => ((x, y), (x, y)), + Region::Rect(c1, c2) => (c1, c2), + Region::Custom(_) => ((0, 0), (0, 0)), + }; + let (source_ul, _) = standardize_rectangle(&source_corners.0, &source_corners.1); + // This map is not necessarily injective or surjective, + // but we will have these properties in certain cases. + // If the transfer is not a pooling transfer (interleave == 0) + // and simple then we *will* have injectivity. + + // Non-replicate transfers: + match &self.dest_region { + Region::Point((x, y)) => { + Box::new(move |(i, j)| { + if source_wells.contains(&(i, j)) { + // Validity here already checked by self.validate() + Some(vec![( + x + i + .checked_sub(source_ul.0) + .expect("Point cannot have been less than UL") + .checked_div(il_source.0.unsigned_abs()) + .expect("Source interleave cannot be 0") + .mul(il_dest.0.unsigned_abs()), + y + j + .checked_sub(source_ul.1) + .expect("Point cannot have been less than UL") + .checked_div(il_source.1.unsigned_abs()) + .expect("Source interleave cannot be 0") + .mul(il_dest.1.unsigned_abs()), + )]) + } else { + None + } + }) + } + Region::Rect(c1, c2) => { + Box::new(move |(i, j)| { + if source_wells.contains(&(i, j)) { + let possible_destination_wells = create_dense_rectangle(c1, c2); + let (d_ul, d_br) = standardize_rectangle(c1, c2); + let (s_ul, s_br) = + standardize_rectangle(&source_corners.0, &source_corners.1); + let s_dims = ( + s_br.0.checked_sub(s_ul.0).unwrap() + 1, + s_br.1.checked_sub(s_ul.1).unwrap() + 1, + ); + let d_dims = ( + d_br.0.checked_sub(d_ul.0).unwrap() + 1, + d_br.1.checked_sub(d_ul.1).unwrap() + 1, + ); + let number_used_src_wells = ( + // Number of used source wells + (s_dims.0 + il_source.0.unsigned_abs() - 1) + .div_euclid(il_source.0.unsigned_abs()), + (s_dims.1 + il_source.1.unsigned_abs() - 1) + .div_euclid(il_source.1.unsigned_abs()), + ); + let count = ( + // How many times can we replicate? + (1..) + .position(|n| { + n * number_used_src_wells.0 * il_dest.0.unsigned_abs() - il_dest.0.unsigned_abs() + + 1 + > d_dims.0 + }) + .unwrap() as u8, + (1..) + .position(|n| { + n * number_used_src_wells.1 * il_dest.1.unsigned_abs() - il_dest.1.unsigned_abs() + + 1 + > d_dims.1 + }) + .unwrap() as u8, + ); + let i = i + .saturating_sub(s_ul.0) + .saturating_div(il_source.0.unsigned_abs()); + let j = j + .saturating_sub(s_ul.1) + .saturating_div(il_source.1.unsigned_abs()); + + Some( + possible_destination_wells + .into_iter() + .filter(|(x, _)| { + x.checked_sub(d_ul.0).unwrap() + % (number_used_src_wells.0 * il_dest.0.unsigned_abs()) // Counter along x + == (il_dest.0.unsigned_abs() *i) + % (number_used_src_wells.0 * il_dest.0.unsigned_abs()) + }) + .filter(|(_, y)| { + y.checked_sub(d_ul.1).unwrap() + % (number_used_src_wells.1 * il_dest.1.unsigned_abs()) // Counter along u + == (il_dest.1.unsigned_abs() *j) + % (number_used_src_wells.1 * il_dest.1.unsigned_abs()) + }) + .filter(|(x, y)| { + // How many times have we replicated? < How many are we allowed + // to replicate? + x.checked_sub(d_ul.0) + .unwrap() + .div_euclid(number_used_src_wells.0 * il_dest.0.unsigned_abs()) + < count.0 + && y.checked_sub(d_ul.1) + .unwrap() + .div_euclid(number_used_src_wells.1 * il_dest.1.unsigned_abs()) + < count.1 + }) + .collect(), + ) + } else { + None + } + }) + } + Region::Custom(c) => Box::new(move |(i, j)| { + let src = c.src.clone(); + let dest = c.dest.clone(); + + let points: Vec<(u8, u8)> = src + .iter() + .enumerate() + .filter(|(_index, (x, y))| *x == i && *y == j) + .map(|(index, _)| dest[index]) + .collect(); + if points.is_empty() { + None + } else { + Some(points) + } + }), + } + } + + pub fn validate(&self) -> Result<(), &'static str> { + // Checks if the region does anything suspect + // + // If validation fails, we pass a string to show to the user. + // + // We check: + // - Are the wells in the source really there? + // - In a replication region, do the source lengths divide the destination lengths? + // - Are the interleaves valid? + let il_source = self.interleave_source; + let il_dest = self.interleave_dest; + + match self.source_region { + Region::Point(_) => return Ok(()), // Should make sure it's actually in the plate, leave for + // later + Region::Rect(s1, s2) => { + // Check if all source wells exist: + if s1.0 == 0 || s1.1 == 0 || s2.0 == 0 || s2.1 == 0 { + return Err("Source region is out-of-bounds! (Too small)"); + } + // Sufficient to check if the corners are in-bounds + let source_max = self.source_plate.size(); + if s1.0 > source_max.0 || s2.0 > source_max.0 { + return Err("Source region is out-of-bounds! (Too tall)"); + } + if s1.1 > source_max.1 || s2.1 > source_max.1 { + // log::debug!("s1.1: {}, max.1: {}", s1.1, source_max.1); + return Err("Source region is out-of-bounds! (Too wide)"); + } + } + Region::Custom(_) => return Ok(()), + } + + if il_source.0 == 0 || il_dest.1 == 0 { + return Err("Source interleave cannot be zero!"); + } + + // Check if all destination wells exist: + // NOT IMPLEMENTED + // Should *not* happen in this function---otherwise + // we'd get a nasty recursive loop. + + Ok(()) + } +} + +fn create_dense_rectangle(c1: &(u8, u8), c2: &(u8, u8)) -> Vec<(u8, u8)> { + // Creates a vector of every point between two corners + let (c1, c2) = standardize_rectangle(c1, c2); + + let mut points = Vec::<(u8, u8)>::new(); + for i in c1.0..=c2.0 { + for j in c1.1..=c2.1 { + points.push((i, j)); + } + } + + points +} + +fn standardize_rectangle(c1: &(u8, u8), c2: &(u8, u8)) -> ((u8, u8), (u8, u8)) { + let upper_left_i = u8::min(c1.0, c2.0); + let upper_left_j = u8::min(c1.1, c2.1); + let bottom_right_i = u8::max(c1.0, c2.0); + let bottom_right_j = u8::max(c1.1, c2.1); + ( + (upper_left_i, upper_left_j), + (bottom_right_i, bottom_right_j), + ) +} + +#[cfg(debug_assertions)] +use std::fmt; +use std::ops::Mul; + +#[cfg(debug_assertions)] // There should be no reason to print a transfer otherwise +impl fmt::Display for TransferRegion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "Source Plate:")?; + let source_dims = self.source_plate.size(); + let source_wells = self.get_source_wells(); + let mut source_string = String::new(); + for i in 1..=source_dims.0 { + for j in 1..=source_dims.1 { + if source_wells.contains(&(i, j)) { + source_string.push('x') + } else { + source_string.push('.') + } + } + source_string.push('\n'); + } + write!(f, "{}", source_string)?; + + writeln!(f, "Dest Plate:")?; + let dest_dims = self.dest_plate.size(); + let dest_wells = self.get_destination_wells(); + let mut dest_string = String::new(); + for i in 1..=dest_dims.0 { + for j in 1..=dest_dims.1 { + if dest_wells.contains(&(i, j)) { + dest_string.push('x') + } else { + dest_string.push('.') + } + } + dest_string.push('\n'); + } + write!(f, "{}", dest_string) + } +} + +#[cfg(test)] +mod tests { + use wasm_bindgen_test::*; + + use crate::data::plate::*; + use crate::data::transfer_region::*; + + #[test] + #[wasm_bindgen_test] + fn test_simple_transfer() { + let source = Plate::new(PlateType::Source, PlateFormat::W96); + let destination = Plate::new(PlateType::Destination, PlateFormat::W384); + + let transfer1 = TransferRegion { + source_plate: source, + source_region: Region::Rect((1, 1), (3, 3)), + dest_plate: destination, + dest_region: Region::Point((3, 3)), + interleave_source: (1, 1), + interleave_dest: (1, 1), + }; + let transfer1_map = transfer1.calculate_map(); + assert_eq!( + transfer1_map((1, 1)), + Some(vec! {(3,3)}), + "Failed basic shift transfer 1" + ); + assert_eq!( + transfer1_map((1, 2)), + Some(vec! {(3,4)}), + "Failed basic shift transfer 2" + ); + assert_eq!( + transfer1_map((2, 2)), + Some(vec! {(4,4)}), + "Failed basic shift transfer 3" + ); + + let transfer2 = TransferRegion { + source_plate: source, + source_region: Region::Rect((1, 1), (3, 3)), + dest_plate: destination, + dest_region: Region::Point((3, 3)), + interleave_source: (2, 2), + interleave_dest: (1, 1), + }; + let transfer2_map = transfer2.calculate_map(); + assert_eq!( + transfer2_map((1, 1)), + Some(vec! {(3,3)}), + "Failed source interleave, type simple 1" + ); + assert_eq!( + transfer2_map((1, 2)), + None, + "Failed source interleave, type simple 2" + ); + assert_eq!( + transfer2_map((2, 2)), + None, + "Failed source interleave, type simple 3" + ); + assert_eq!( + transfer2_map((3, 3)), + Some(vec! {(4,4)}), + "Failed source interleave, type simple 4" + ); + + let transfer3 = TransferRegion { + source_plate: source, + source_region: Region::Rect((1, 1), (3, 3)), + dest_plate: destination, + dest_region: Region::Point((3, 3)), + interleave_source: (1, 1), + interleave_dest: (2, 3), + }; + let transfer3_map = transfer3.calculate_map(); + assert_eq!( + transfer3_map((1, 1)), + Some(vec! {(3,3)}), + "Failed destination interleave, type simple 1" + ); + assert_eq!( + transfer3_map((2, 1)), + Some(vec! {(5,3)}), + "Failed destination interleave, type simple 2" + ); + assert_eq!( + transfer3_map((1, 2)), + Some(vec! {(3,6)}), + "Failed destination interleave, type simple 3" + ); + assert_eq!( + transfer3_map((2, 2)), + Some(vec! {(5,6)}), + "Failed destination interleave, type simple 4" + ); + } + + #[test] + #[wasm_bindgen_test] + fn test_replicate_transfer() { + let source = Plate::new(PlateType::Source, PlateFormat::W96); + let destination = Plate::new(PlateType::Destination, PlateFormat::W384); + + let transfer1 = TransferRegion { + source_plate: source, + source_region: Region::Rect((1, 1), (2, 2)), + dest_plate: destination, + dest_region: Region::Rect((2, 2), (11, 11)), + interleave_source: (1, 1), + interleave_dest: (3, 3), + }; + let transfer1_map = transfer1.calculate_map(); + assert_eq!( + transfer1_map((1, 1)), + Some(vec! {(2, 2), (2, 8), (8, 2), (8, 8)}), + "Failed type replicate 1" + ); + assert_eq!( + transfer1_map((2, 1)), + Some(vec! {(5, 2), (5, 8), (11, 2), (11, 8)}), + "Failed type replicate 1" + ); + + let transfer2 = TransferRegion { + source_plate: Plate::new(PlateType::Source, PlateFormat::W384), + dest_plate: Plate::new(PlateType::Destination, PlateFormat::W384), + source_region: Region::Rect((1, 1), (2, 3)), + dest_region: Region::Rect((2, 2), (11, 16)), + interleave_source: (1, 1), + interleave_dest: (2, 2), + }; + let transfer2_source = transfer2.get_source_wells(); + let transfer2_dest = transfer2.get_destination_wells(); + assert_eq!( + transfer2_source, + vec![(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3)], + "Failed type replicate 2 source" + ); + assert_eq!( + transfer2_dest, + vec![ + (2, 2), + (2, 8), + (6, 2), + (6, 8), + (2, 4), + (2, 10), + (6, 4), + (6, 10), + (2, 6), + (2, 12), + (6, 6), + (6, 12), + (4, 2), + (4, 8), + (8, 2), + (8, 8), + (4, 4), + (4, 10), + (8, 4), + (8, 10), + (4, 6), + (4, 12), + (8, 6), + (8, 12) + ], + "Failed type replicate 2 destination" + ); + } + + #[test] + #[wasm_bindgen_test] + fn test_pooling_transfer() { + let transfer1 = TransferRegion { + source_plate: Plate::new(PlateType::Source, PlateFormat::W384), + dest_plate: Plate::new(PlateType::Destination, PlateFormat::W384), + source_region: Region::Rect((1, 4), (3, 7)), + dest_region: Region::Point((1, 9)), + interleave_source: (1, 1), + interleave_dest: (0, 2), + }; + //let transfer1_source = transfer1.get_source_wells(); + let mut transfer1_dest = transfer1.get_destination_wells(); + transfer1_dest.sort(); + transfer1_dest.dedup(); // Makes our check easier, otherwise we have repeated wells + let transfer1_map = transfer1.calculate_map(); + // Skipping source check---it's just 12 wells. + assert_eq!( + transfer1_dest, + vec![(1, 9), (1, 11), (1, 13), (1, 15)], + "Failed type pool 1 dest" + ); + assert_eq!( + transfer1_map((2, 6)), + Some(vec![(1, 13)]), + "Failed type pool 1 map 1" + ); + assert_eq!( + transfer1_map((3, 7)), + Some(vec![(1, 15)]), + "Failed type pool 1 map 2" + ); + } +} diff --git a/plate-tool-lib/src/util.rs b/plate-tool-lib/src/util.rs new file mode 100644 index 0000000..b8ec34d --- /dev/null +++ b/plate-tool-lib/src/util.rs @@ -0,0 +1,70 @@ +pub fn letters_to_num(letters: &str) -> Option { + let mut num: u8 = 0; + for (i, letter) in letters.to_ascii_uppercase().chars().rev().enumerate() { + log::debug!("{}, {}", i, letter); + let n = letter as u8; + if !(65..=90).contains(&n) { + return None; + } + num = num.checked_add((26_i32.pow(i as u32) * (n as i32 - 64)).try_into().ok()?)?; + } + Some(num) +} +pub fn num_to_letters(num: u8) -> Option { + if num == 0 { + return None; + } // Otherwise, we will not return none! + // As another note, we can't represent higher than "IV" anyway; + // thus there's no reason for a loop (26^n with n>1 will NOT occur). + let mut text = "".to_string(); + let mut digit1 = num.div_euclid(26u8); + let mut digit2 = num.rem_euclid(26u8); + if digit1 > 0 && digit2 == 0u8 { + digit1 -= 1; + digit2 = 26; + } + if digit1 != 0 { + text.push((64 + digit1) as char) + } + text.push((64 + digit2) as char); + + Some(text.to_string()) +} + +#[cfg(test)] +mod tests { + use super::{letters_to_num, num_to_letters, RegionDisplay}; + + #[test] + fn test_letters_to_num() { + assert_eq!(letters_to_num("D"), Some(4)); + assert_eq!(letters_to_num("d"), None); + assert_eq!(letters_to_num("AD"), Some(26 + 4)); + assert_eq!(letters_to_num("CG"), Some(3 * 26 + 7)); + } + + #[test] + fn test_num_to_letters() { + println!("27 is {:?}", num_to_letters(27)); + assert_eq!(num_to_letters(1), Some("A".to_string())); + assert_eq!(num_to_letters(26), Some("Z".to_string())); + assert_eq!(num_to_letters(27), Some("AA".to_string())); + assert_eq!(num_to_letters(111), Some("DG".to_string())); + } + + #[test] + fn test_l2n_and_n2l() { + assert_eq!( + num_to_letters(letters_to_num("A").unwrap()), + Some("A".to_string()) + ); + assert_eq!( + num_to_letters(letters_to_num("BJ").unwrap()), + Some("BJ".to_string()) + ); + for i in 1..=255 { + assert_eq!(letters_to_num(&num_to_letters(i).unwrap()), Some(i)); + } + } + +}