//! Auto module for importing picklists without creating plates first //! //! Before the auto module, it was necessary for a user to create the plates //! used in an imported transfer before import. 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_volume::{TransferVolume, VolumeMaps}; use crate::Well; use crate::transfer_region::{CustomRegion, Region, TransferRegion}; use std::collections::{HashSet, HashMap}; pub struct AutoOutput { pub sources: Vec, pub destinations: Vec, pub transfers: Vec, } /// Two lists of plates that are guaranteed to be unique (no duplicate plates) 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; /// Main function for the auto module /// Takes a list of pre-deserialized transfer records and generates output /// This output is 3 lists of: /// 1. The source plates used in the picklist /// 2. The destination plates used in the picklist /// 3. All of the transfers between those plates pub fn auto(records: &[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 } } /// Helper function that reads a CSV and immediately dumps the resulting transfer records /// into the auto function defined in this module. pub fn read_csv_auto(data: &str) -> AutoOutput { let transfer_records = read_csv(data); auto(&transfer_records) } /// Looks through a list of transfer records and generates two lists /// of all of the unique plates found in those transfers. /// Worth noting that the Transfer struct requires two associated PlateInstances; /// these PlateInstance structs are generated here---they are not present in a TransferRecord. /// See notes on the UniquePlates struct 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_plate); destination_names.insert(&record.destination_plate); } 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, } } /// Tries to guess the format of a plate given the southeastern-est well used. /// A picklist does not necessarily contain plate format info; this method guesses /// based on the well furthest from A1 that exists in a transfer. /// It's possible for a 1536 well plate to be seen as a 384 well if all of the wells /// used could have fit in the top-left quadrant of a 384 well plate /// (i.e. all lower than P24 in this case) 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(Well { 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 } /// Given a source and destination plate, get all of the wells transferred /// from source to destination and turn this into a Transfer. /// /// Note that even if you have what looks like two separate transfers, /// this will join all transfers for a given source-dest pair. /// For example: /// Consider expanding all of src:A1 to the column dst:A /// and src:B1 -> row dst:1 /// If you were making this transfer in plate tool, it would be easiest /// to do this as two separate transfers. /// On import, this would be seen as one transfer. 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 = HashSet::new(); let mut destination_wells: HashSet = HashSet::new(); // Volume Hash Maps let mut source_only_volume_map: HashMap = HashMap::new(); let mut destination_only_volume_map: HashMap = HashMap::new(); let mut paired_volume_map: HashMap<(Well, Well), f32> = HashMap::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); source_only_volume_map.entry(source_point).and_modify(|v| *v += record.volume) .or_insert(record.volume); destination_only_volume_map.entry(destination_point).and_modify(|v| *v += record.volume) .or_insert(record.volume); paired_volume_map.entry((source_point, destination_point)).and_modify(|v| *v += record.volume) .or_insert_with(|| record.volume); // No idea why this one needs to be different } } let source_wells_vec: Vec = source_wells.into_iter().collect(); let destination_wells_vec: Vec = destination_wells.into_iter().collect(); let transfer_volume: TransferVolume = TransferVolume::WellMap(VolumeMaps { source_only: source_only_volume_map, destination_only: destination_only_volume_map, paired: paired_volume_map, }); 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); let mut transfer = Transfer::new( source.clone(), destination.clone(), transfer_region, transfer_name, ); transfer.volume = transfer_volume; Some(transfer) }