use serde::{Deserialize, Serialize}; use regex::Regex; use super::plate::Plate; use crate::plate::PlateType; use crate::util; use crate::Well; use std::fmt; use std::sync::LazyLock; #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)] pub struct CustomRegion { src: Vec, dest: Vec, } impl CustomRegion { pub fn new(src: Vec, dest: Vec) -> Self { CustomRegion { src, dest } } } #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)] pub enum Region { Rect(Well, Well), Point(Well), Custom(CustomRegion), } impl Default for Region { fn default() -> Self { Region::Point(Well { row: 1, col: 1 }) } } impl Display for Region { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Region::Rect(ul, br) => { write!( f, "{}{}:{}{}", util::num_to_letters(ul.col).unwrap(), ul.row, util::num_to_letters(br.col).unwrap(), br.row ) } Region::Point(w) => { write!(f, "{}{}", util::num_to_letters(w.col).unwrap(), w.row) } Region::Custom(..) => { write!(f, "Custom Region") } } } } impl TryFrom for (Well, Well) { 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.") } } } impl Region { pub fn new_custom(transfers: &Vec<(Well, Well)>) -> Self { let mut src_pts: Vec = Vec::with_capacity(transfers.len()); let mut dest_pts: Vec = 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, }) } pub fn new_from_wells(w1: Well, w2: Option) -> Self { if w2.is_none() { Self::Point(w1) } else { let w2 = w2.unwrap(); let (w1, w2) = standardize_rectangle(&w1, &w2); Self::Rect(w1, w2) } } pub fn try_parse_str(input: &str) -> Option { static POINT_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap()); static RECT_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"([A-Z,a-z]+)(\d+):([A-Z,a-z]+)(\d+)").unwrap()); log::info!("{:?}", input); if let Some(captures) = RECT_REGEX.captures(input) { if let (Some(row1), Some(col1), Some(row2), Some(col2)) = ( crate::util::letters_to_num(&captures[1]), captures[2].parse::().ok(), crate::util::letters_to_num(&captures[3]), captures[4].parse::().ok(), ) { return Some(Region::Rect( Well { row: row1, col: col1, }, Well { row: row2, col: col2, }, )); } else { return None; } } else if let Some(captures) = POINT_REGEX.captures(input) { if let (Some(row), Some(col)) = ( crate::util::letters_to_num(&captures[1]), captures[2].parse::().ok(), ) { return Some(Region::Point(Well { row, col })); } else { return None; } } else { None } } pub fn contains_well(&self, w: &Well, pt: Option) -> bool { match self { Self::Point(x) => x == w, Self::Rect(ul, br) => { w.row <= br.row && w.row >= ul.row && w.col <= br.col && w.col >= ul.col } Self::Custom(xs) => match pt { Some(PlateType::Source) => xs.src.contains(w), Some(PlateType::Destination) => xs.dest.contains(w), None => { unreachable!("Cannot check if custom contains well without knowing the type") } }, } } } #[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug, Hash)] 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 { match &self.source_region { Region::Rect(c1, c2) => { let mut wells = Vec::::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.row..=br.row).step_by(i8::abs(interleave_i) as usize) { for j in (ul.col..=br.col).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(Well { row: i, col: j }) } } wells } Region::Point(p) => vec![*p], Region::Custom(c) => c.src.clone(), } } pub fn get_destination_wells(&self) -> Vec { match &self.source_region { Region::Custom(c) => c.dest.clone(), _ => { let map = self.calculate_map(); let source_wells = self.get_source_wells(); let mut wells = Vec::::new(); for well in source_wells { if let Some(mut dest_wells) = map(well) { wells.append(&mut dest_wells); } } 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. :) if let Err(msg) = self.validate() { log::error!("{}\nThis transfer will be empty.", msg); 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: (Well, Well) = match self.source_region { Region::Point(w) => (w, w), Region::Rect(c1, c2) => (c1, c2), Region::Custom(_) => (Well { row: 0, col: 0 }, Well { row: 0, col: 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(dest_point) => { // Breaking from form somewhat, we really should return an entirely different // function if a point-dest can't fit the whole source. // If the bottom-right well of the source won't fit in the dest, // we can abort. let source_bottom_right = match self.source_region { Region::Point(x) => x, Region::Rect(w1, w2) => standardize_rectangle(&w1, &w2).1, Region::Custom(_) => unreachable!("A point destination region cannot be paired with a custom source destination?"), }; let bottom_right_mapped = Self::point_destination_region_calc( source_bottom_right, source_ul, il_source, il_dest, *dest_point, ); if bottom_right_mapped.row > self.dest_plate.plate_format.rows() || bottom_right_mapped.col > self.dest_plate.plate_format.columns() { return Box::new(|_| None); } Box::new(move |input| { if source_wells.contains(&input) { // Validity here already checked by self.validate() Some(vec![Self::point_destination_region_calc( input, source_ul, il_source, il_dest, *dest_point, )]) } else { None } }) } Region::Rect(c1, c2) => { Box::new(move |w| { let Well { row: i, col: j } = w; if source_wells.contains(&w) { 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.row.checked_sub(s_ul.row).unwrap() + 1, s_br.col.checked_sub(s_ul.col).unwrap() + 1, ); let d_dims = ( d_br.row.checked_sub(d_ul.row).unwrap() + 1, d_br.col.checked_sub(d_ul.col).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? if il_dest.0.unsigned_abs() == 0 { 1 } else { (1..) .position(|n| { (n * number_used_src_wells.0 * il_dest.0.unsigned_abs()) .saturating_sub(il_dest.0.unsigned_abs()) + 1 > d_dims.0 }) .unwrap() as u8 }, if il_dest.1.unsigned_abs() == 0 { 1 } else { (1..) .position(|n| { (n * number_used_src_wells.1 * il_dest.1.unsigned_abs()) .saturating_sub(il_dest.1.unsigned_abs()) + 1 > d_dims.1 }) .unwrap() as u8 }, ); let i = i .saturating_sub(s_ul.row) .saturating_div(il_source.0.unsigned_abs()); let j = j .saturating_sub(s_ul.col) .saturating_div(il_source.1.unsigned_abs()); let checked_il_dest = ( u8::max(il_dest.0.unsigned_abs(), 1u8), u8::max(il_dest.1.unsigned_abs(), 1u8), ); let row_modulus = number_used_src_wells.0 * checked_il_dest.0; let column_modulus = number_used_src_wells.1 * checked_il_dest.1; Some( possible_destination_wells .into_iter() .filter(|Well { row: x, .. }| { x.checked_sub(d_ul.row).unwrap() % row_modulus // Counter along x == (il_dest.0.unsigned_abs() *i) % row_modulus }) .filter(|Well { col: y, .. }| { y.checked_sub(d_ul.col).unwrap() % column_modulus // Counter along u == (il_dest.1.unsigned_abs() *j) % column_modulus }) .filter(|Well { row: x, col: y }| { // How many times have we replicated? < How many are we allowed // to replicate? x.checked_sub(d_ul.row).unwrap().div_euclid(row_modulus) < count.0 && y.checked_sub(d_ul.col) .unwrap() .div_euclid(column_modulus) < count.1 }) .collect(), ) } else { None } }) } Region::Custom(c) => Box::new(move |Well { row: i, col: j }| { let src = c.src.clone(); let dest = c.dest.clone(); let points: Vec = src .iter() .enumerate() .filter(|(_index, Well { row: x, col: y })| *x == i && *y == j) .map(|(index, _)| dest[index]) .collect(); if points.is_empty() { None } else { Some(points) } }), } } fn point_destination_region_calc( input: Well, source_ul: Well, il_source: (i8, i8), il_dest: (i8, i8), dest_point: Well, ) -> Well { Well { row: dest_point.row + input .row .checked_sub(source_ul.row) .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()), col: dest_point.col + input .col .checked_sub(source_ul.col) .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()), } } 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 { /* * Note 04Jan2025: * I genuinely cannot think of a reason why we should need to validate a source * point region??? * Like, why would it *not* be in the plate? */ 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.row == 0 || s1.col == 0 || s2.row == 0 || s2.col == 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.row > source_max.0 || s2.row > source_max.0 { return Err("Source region is out-of-bounds! (Too tall)"); } if s1.col > source_max.1 || s2.col > source_max.1 { // log::debug!("s1.1: {}, max.1: {}", s1.1, source_max.1); return Err("Source region is out-of-bounds! (Too wide)"); } if il_dest == (0, 0) { return Err("Refusing to pool both dimensions in a rectangular transfer!\nPlease select a point in the destination plate."); } } Region::Custom(_) => return Ok(()), } if il_source.0 == 0 || il_source.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: &Well, c2: &Well) -> Vec { // Creates a vector of every point between two corners let (c1, c2) = standardize_rectangle(c1, c2); let mut points = Vec::::new(); for i in c1.row..=c2.row { for j in c1.col..=c2.col { points.push(Well { row: i, col: j }); } } points } fn standardize_rectangle(c1: &Well, c2: &Well) -> (Well, Well) { let upper_left_i = u8::min(c1.row, c2.row); let upper_left_j = u8::min(c1.col, c2.col); let bottom_right_i = u8::max(c1.row, c2.row); let bottom_right_j = u8::max(c1.col, c2.col); ( Well { row: upper_left_i, col: upper_left_j, }, Well { row: bottom_right_i, col: bottom_right_j, }, ) } #[cfg(debug_assertions)] use std::fmt; use std::{fmt::Display, 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(&Well { row: i, col: 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(&Well { row: i, col: j }) { dest_string.push('x') } else { dest_string.push('.') } } dest_string.push('\n'); } write!(f, "{}", dest_string) } } #[cfg(test)] mod tests { use crate::plate::*; use crate::transfer_region::*; #[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(Well { row: 1, col: 1 }, Well { row: 1, col: 1 }), dest_plate: destination, dest_region: Region::Point(Well { row: 3, col: 3 }), interleave_source: (1, 1), interleave_dest: (1, 1), }; let transfer1_map = transfer1.calculate_map(); assert_eq!( transfer1_map(Well { row: 1, col: 1 }), Some(vec! {Well {row: 3, col: 3}}), "Failed basic shift transfer 1" ); assert_eq!( transfer1_map(Well { row: 1, col: 2 }), Some(vec! {Well{ row: 3, col: 4}}), "Failed basic shift transfer 2" ); assert_eq!( transfer1_map(Well { row: 2, col: 2 }), Some(vec! {Well{ row: 4, col: 4}}), "Failed basic shift transfer 3" ); let transfer2 = TransferRegion { source_plate: source, source_region: Region::Rect(Well { row: 1, col: 1 }, Well { row: 3, col: 3 }), dest_plate: destination, dest_region: Region::Point(Well { row: 3, col: 3 }), interleave_source: (2, 2), interleave_dest: (1, 1), }; let transfer2_map = transfer2.calculate_map(); assert_eq!( transfer2_map(Well { row: 1, col: 1 }), Some(vec! {Well{ row: 3, col: 3}}), "Failed source interleave, type simple 1" ); assert_eq!( transfer2_map(Well { row: 1, col: 2 }), None, "Failed source interleave, type simple 2" ); assert_eq!( transfer2_map(Well { row: 2, col: 2 }), None, "Failed source interleave, type simple 3" ); assert_eq!( transfer2_map(Well { row: 3, col: 3 }), Some(vec! {Well{ row: 4, col: 4}}), "Failed source interleave, type simple 4" ); let transfer3 = TransferRegion { source_plate: source, source_region: Region::Rect(Well { row: 1, col: 1 }, Well { row: 3, col: 3 }), dest_plate: destination, dest_region: Region::Point(Well { row: 3, col: 3 }), interleave_source: (1, 1), interleave_dest: (2, 3), }; let transfer3_map = transfer3.calculate_map(); assert_eq!( transfer3_map(Well { row: 1, col: 1 }), Some(vec! {Well{ row: 3, col: 3}}), "Failed destination interleave, type simple 1" ); assert_eq!( transfer3_map(Well { row: 2, col: 1 }), Some(vec! {Well{ row: 5, col: 3}}), "Failed destination interleave, type simple 2" ); assert_eq!( transfer3_map(Well { row: 1, col: 2 }), Some(vec! {Well{ row: 3, col: 6}}), "Failed destination interleave, type simple 3" ); assert_eq!( transfer3_map(Well { row: 2, col: 2 }), Some(vec! {Well{ row: 5, col: 6}}), "Failed destination interleave, type simple 4" ); } #[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(Well { row: 1, col: 1 }, Well { row: 2, col: 2 }), dest_plate: destination, dest_region: Region::Rect(Well { row: 2, col: 2 }, Well { row: 11, col: 11 }), interleave_source: (1, 1), interleave_dest: (3, 3), }; let transfer1_map = transfer1.calculate_map(); assert_eq!( transfer1_map(Well { row: 1, col: 1 }), Some( vec! {Well{ row: 2, col: 2}, Well{ row: 2, col: 8}, Well{ row: 8, col: 2}, Well{ row: 8, col: 8}} ), "Failed type replicate 1" ); assert_eq!( transfer1_map(Well { row: 2, col: 1 }), Some( vec! {Well{ row: 5, col: 2}, Well{ row: 5, col: 8}, Well{ row: 11, col: 2}, Well{ row: 11, col: 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(Well { row: 1, col: 1 }, Well { row: 2, col: 3 }), dest_region: Region::Rect(Well { row: 2, col: 2 }, Well { row: 11, col: 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![ Well { row: 1, col: 1 }, Well { row: 1, col: 2 }, Well { row: 1, col: 3 }, Well { row: 2, col: 1 }, Well { row: 2, col: 2 }, Well { row: 2, col: 3 } ], "Failed type replicate 2 source" ); assert_eq!( transfer2_dest, vec![ Well { row: 2, col: 2 }, Well { row: 2, col: 8 }, Well { row: 6, col: 2 }, Well { row: 6, col: 8 }, Well { row: 2, col: 4 }, Well { row: 2, col: 10 }, Well { row: 6, col: 4 }, Well { row: 6, col: 10 }, Well { row: 2, col: 6 }, Well { row: 2, col: 12 }, Well { row: 6, col: 6 }, Well { row: 6, col: 12 }, Well { row: 4, col: 2 }, Well { row: 4, col: 8 }, Well { row: 8, col: 2 }, Well { row: 8, col: 8 }, Well { row: 4, col: 4 }, Well { row: 4, col: 10 }, Well { row: 8, col: 4 }, Well { row: 8, col: 10 }, Well { row: 4, col: 6 }, Well { row: 4, col: 12 }, Well { row: 8, col: 6 }, Well { row: 8, col: 12 } ], "Failed type replicate 2 destination" ); } #[test] fn test_pooling_transfer() { use std::collections::HashSet; let transfer1 = TransferRegion { source_plate: Plate::new(PlateType::Source, PlateFormat::W384), dest_plate: Plate::new(PlateType::Destination, PlateFormat::W384), source_region: Region::Rect(Well { row: 1, col: 4 }, Well { row: 3, col: 7 }), dest_region: Region::Point(Well { row: 1, col: 9 }), interleave_source: (1, 1), interleave_dest: (0, 2), }; //let transfer1_source = transfer1.get_source_wells(); let transfer1_dest: HashSet = transfer1.get_destination_wells().into_iter().collect(); let transfer1_map = transfer1.calculate_map(); // Skipping source check---it's just 12 wells. assert_eq!( transfer1_dest, vec![ Well { row: 1, col: 9 }, Well { row: 1, col: 11 }, Well { row: 1, col: 13 }, Well { row: 1, col: 15 } ] .into_iter() .collect(), "Failed type pool 1 dest" ); assert_eq!( transfer1_map(Well { row: 2, col: 6 }), Some(vec![Well { row: 1, col: 13 }]), "Failed type pool 1 map 1" ); assert_eq!( transfer1_map(Well { row: 3, col: 7 }), Some(vec![Well { row: 1, col: 15 }]), "Failed type pool 1 map 2" ); } }