Compare commits

...

6 Commits

Author SHA1 Message Date
Emilia Allison 9366d29202
lib: fix stupid typo 2024-02-13 20:32:42 -05:00
Emilia Allison c2a64d679a
web: auto button 2024-02-13 20:27:58 -05:00
Emilia Allison 1aa4c1b7bb
web: use new utility function in callback 2024-02-13 19:58:14 -05:00
Emilia Allison 6c8533f7a6
lib: auto creation from csv 2024-02-13 19:56:47 -05:00
Emilia Allison d7c98d37a2
Reorg csv in lib 2024-02-13 18:33:42 -05:00
Emilia Allison bfa1fef9d8
update readme 2024-02-13 18:23:22 -05:00
11 changed files with 291 additions and 62 deletions

1
Cargo.lock generated
View File

@ -637,6 +637,7 @@ version = "0.1.0"
dependencies = [
"csv",
"getrandom",
"lazy_static",
"log",
"rand",
"regex",

View File

@ -78,13 +78,6 @@ To add a new plate, click the "New Plate" button:
Keep in mind that this will overwrite any work you currently have open,
so you may wish to export first (see above).
#### Import Transfer from CSV (Using a picklist as a transfer)
If you have a CSV generated by another tool (or plate-tool),
you can import it as a single transfer.
To do so, mouse over the "File" tab, then "Import", and finally "Import Transfer from CSV".
When creating transfers via this method, the transfer cannot be edited.
This is useful if you have a pre-existing picklist that you would like to visualize in plate-tool.
_Note 1_: JSON files are plaintext!
By default there is little whitespace (this makes comprehending them a challenge)
but if we pass it through a "JSON Beautifier" (enter this into your search engine of choice)
@ -99,6 +92,18 @@ To add a new plate, click the "New Plate" button:
that this application does not "phone home".
Your data is stored locally (unless you choose to export it and distribute it yourself).
#### Import Transfer from CSV (Using a picklist as a transfer)
If you have a CSV generated by another tool (or plate-tool),
you can import it as a single transfer.
To do so, mouse over the "File" tab, then "Import", and finally "Import Transfer from CSV".
When creating transfers via this method, the transfer cannot be edited.
This is useful if you have a pre-existing picklist that you would like to visualize in plate-tool.
_Note_: If you try to use this feature and no plates are available to select,
there was likely an issue parsing your picklist.
Your browser's console may have guidance as to why parsing failed;
plate-tool was probably expecting a different name for a column than was in your file.
### Other Neat Features
#### Taking Pictures of Plates
@ -130,7 +135,7 @@ To add a new plate, click the "New Plate" button:
Plate tool is hosted [here](https://ilia.moe/cool-stuff/plate-tool/) for your convenience.
However, you're absolutely welcome to host your own instance (even locally).
Here's how:
(_Note:_ If you run Windows you're probably best off doing the following in WSL2)
(_Note:_ ~~If you run Windows you're probably best off doing the following in WSL2~~ You're absolutely fine to install rustup in Powershell, and the subsequent steps should be very similar but likely with different filepaths.)
1. Make sure you have a working Rust toolchain
1. Installing `rustup` is the easiest way to do this. See [their website](https://rustup.rs/),
@ -140,7 +145,7 @@ Here's how:
2. Install [trunk](https://trunkrs.dev/)
- Run `cargo install --locked trunk`
3. Clone this repository using git
4. Enter the project directory and run `trunk serve`
4. Enter the plate-tool-web directory and run `trunk serve`
- You may need to check where `cargo` is installing binaries by default. For me, they're at `~/.cargo/bin`.
If trunk is not automatically placed in your path, you would then run `/your/path/to/.cargo/bin/trunk serve`.
- You can instead run `trunk build --release` for a more performant binary.

View File

@ -14,3 +14,4 @@ serde_json = "1.0"
csv = "1.2"
getrandom = { version = "0.2", features = ["js"] }
rand = { version = "0.8", features = ["small_rng"] }
lazy_static = "1.4"

View File

@ -0,0 +1,173 @@
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_region::{CustomRegion, Region, TransferRegion};
use std::collections::HashSet;
pub struct AutoOutput {
pub sources: Vec<PlateInstance>,
pub destinations: Vec<PlateInstance>,
pub transfers: Vec<Transfer>,
}
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;
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 }
}
pub fn read_csv_auto(data: &str) -> AutoOutput {
let transfer_records = read_csv(data);
auto(&transfer_records)
}
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,
}
}
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((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
}
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<(u8, u8)> = HashSet::new();
let mut destination_wells: HashSet<(u8, u8)> = HashSet::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);
}
}
let source_wells_vec: Vec<(u8, u8)> = source_wells.into_iter().collect();
let destination_wells_vec: Vec<(u8, u8)> = destination_wells.into_iter().collect();
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);
Some(Transfer::new(
source.clone(),
destination.clone(),
transfer_region,
transfer_name,
))
}

View File

@ -1,38 +1,12 @@
use crate::transfer::Transfer;
use crate::util::*;
use super::TransferRecord;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
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")]
#[serde(default = "volume_default")]
pub volume: f32,
#[serde(rename = "Concentration", alias = "concentration")]
pub concentration: Option<f32>,
}
pub fn volume_default() -> f32 {
Transfer::default().volume
}
pub fn transfer_to_records(
tr: &Transfer,
src_barcode: &str,
@ -70,6 +44,21 @@ pub fn records_to_csv(trs: Vec<TransferRecord>) -> Result<String, Box<dyn Error>
Ok(data)
}
pub fn string_well_to_pt(input: &str) -> Option<(u8, u8)> {
lazy_static! {
static ref REGEX: Regex = Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap();
}
if let Some(c1) = REGEX.captures(input) {
if let (Some(row), Some(col)) = (letters_to_num(&c1[1]), c1[2].parse::<u8>().ok()) {
Some((row, col))
} else {
None
}
} else {
None
}
}
pub fn read_csv(data: &str) -> Vec<TransferRecord> {
let (header, data) = data.split_at(data.find('\n').unwrap());
let modified: String = header.to_lowercase() + data;

View File

@ -0,0 +1,9 @@
mod transfer_record;
mod conversion;
mod auto;
pub use transfer_record::volume_default;
pub use transfer_record::TransferRecord;
pub use conversion::*;
pub use auto::{auto, read_csv_auto};

View File

@ -0,0 +1,33 @@
use serde::{Deserialize, Serialize};
use crate::transfer::Transfer;
#[derive(Serialize, Deserialize, Debug, Clone)]
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")]
#[serde(default = "volume_default")]
pub volume: f32,
#[serde(rename = "Concentration", alias = "concentration")]
pub concentration: Option<f32>,
}
pub fn volume_default() -> f32 {
Transfer::default().volume
}

View File

@ -2,7 +2,7 @@ use super::plate::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(PartialEq, Clone, Serialize, Deserialize)]
#[derive(PartialEq, Clone, Serialize, Deserialize, Debug)]
pub struct PlateInstance {
pub plate: Plate,
#[serde(rename = "id_v7")]

View File

@ -8,6 +8,12 @@ pub struct CustomRegion {
dest: Vec<(u8, u8)>,
}
impl CustomRegion {
pub fn new(src: Vec<(u8, u8)>, dest: Vec<(u8, u8)>) -> Self {
CustomRegion { src, dest }
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum Region {
Rect((u8, u8), (u8, u8)),

View File

@ -33,7 +33,7 @@ pub fn num_to_letters(num: u8) -> Option<String> {
#[cfg(test)]
mod tests {
use super::{letters_to_num, num_to_letters, RegionDisplay};
use super::{letters_to_num, num_to_letters};
#[test]
fn test_letters_to_num() {

View File

@ -1,22 +1,19 @@
use std::collections::HashSet;
use lazy_static::lazy_static;
use regex::Regex;
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{
FileReader, HtmlButtonElement, HtmlDialogElement, HtmlFormElement, HtmlInputElement,
HtmlOptionElement, HtmlSelectElement,
FileReader, HtmlButtonElement, HtmlDialogElement, HtmlElement, HtmlFormElement,
HtmlInputElement, HtmlOptionElement, HtmlSelectElement,
};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::MainState;
use plate_tool_lib::util::letters_to_num;
use plate_tool_lib::transfer::Transfer;
use plate_tool_lib::transfer_region::{Region, TransferRegion};
use plate_tool_lib::csv::TransferRecord;
use plate_tool_lib::csv::{auto, string_well_to_pt, TransferRecord};
use super::main_window_callbacks::create_close_button;
@ -115,6 +112,17 @@ pub fn import_transfer_csv_onload_callback(
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let auto_button = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
auto_button.set_inner_text("Auto");
let auto_button_callback = auto_callback(main_dispatch.clone(), &records);
auto_button.set_onclick(Some(auto_button_callback.as_ref().unchecked_ref()));
auto_button_callback.forget();
let form = document
.create_element("form")
.unwrap()
@ -211,6 +219,7 @@ pub fn import_transfer_csv_onload_callback(
form.append_child(&to_dest).unwrap();
modal.append_child(&submit).unwrap();
modal.append_child(&form).unwrap();
modal.append_child(&auto_button).unwrap();
}
})
}
@ -229,28 +238,14 @@ pub fn import_transfer_csv_submit_callback(
let from_dest = from_dest.value();
let to_dest = to_dest.value();
lazy_static! {
static ref REGEX: Regex = Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap();
}
let records: Vec<((u8, u8), (u8, u8))> = records
.iter()
.filter(|record| record.source_plate == from_source)
.filter(|record| record.destination_plate == from_dest)
.map(|record| {
let c1 = REGEX.captures(&record.source_well).unwrap();
let c2 = REGEX.captures(&record.destination_well).unwrap();
log::debug!("{} {}", &record.source_well, &record.destination_well);
log::debug!("{},{} {},{}", &c1[1], &c1[2], &c2[1], &c2[2]);
(
(
letters_to_num(&c1[1]).unwrap(),
c1[2].parse::<u8>().unwrap(),
),
(
letters_to_num(&c2[1]).unwrap(),
c2[2].parse::<u8>().unwrap(),
),
string_well_to_pt(&record.source_well).unwrap(),
string_well_to_pt(&record.destination_well).unwrap(),
)
})
.collect();
@ -291,3 +286,20 @@ pub fn import_transfer_csv_submit_callback(
});
})
}
fn auto_callback(
main_dispatch: Dispatch<MainState>,
records: &[TransferRecord],
) -> Closure<dyn FnMut(Event)> {
let records = Vec::from(records);
Closure::<dyn FnMut(_)>::new(move |_| {
let res = auto(&records);
main_dispatch.reduce_mut(|state| {
state.source_plates.extend(res.sources.into_iter());
state
.destination_plates
.extend(res.destinations.into_iter());
state.transfers.extend(res.transfers.into_iter());
});
})
}