234 lines
8.7 KiB
Rust
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)
|
|
}
|