plate-tool/plate-tool-lib/src/csv/auto.rs

234 lines
8.7 KiB
Rust

//! 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<PlateInstance>,
pub destinations: Vec<PlateInstance>,
pub transfers: Vec<Transfer>,
}
/// Two lists of plates that are guaranteed to be unique (no duplicate plates)
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;
/// 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<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,
}
}
/// 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<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
}
/// 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<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<Well> = HashSet::new();
let mut destination_wells: HashSet<Well> = HashSet::new();
// Volume Hash Maps
let mut source_only_volume_map: HashMap<Well, f32> = HashMap::new();
let mut destination_only_volume_map: HashMap<Well, f32> = 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<Well> = source_wells.into_iter().collect();
let destination_wells_vec: Vec<Well> = 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)
}