use std::num::NonZeroU32;
use serde::{Deserialize, Serialize};
use crate::enums::{CqDetectionMethod, LabelFormat};
use crate::types::{
DateTime, DocumentationRef, ExperimenterRef, Id, Reasons, SampleRef, TargetRef, TccRef,
};
use super::dpcr::Partitions;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Experiment {
pub id: Id,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub description: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub documentation: Vec<DocumentationRef>,
#[serde(rename = "run", skip_serializing_if = "Vec::is_empty", default)]
pub runs: Vec<Run>,
}
impl Experiment {
#[must_use]
pub fn new(id: Id) -> Self {
Self {
id,
description: None,
documentation: Vec::new(),
runs: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Run {
pub id: Id,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub description: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub documentation: Vec<DocumentationRef>,
#[serde(
rename = "experimenter",
skip_serializing_if = "Vec::is_empty",
default
)]
pub experimenters: Vec<ExperimenterRef>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub instrument: Option<String>,
#[serde(
rename = "dataCollectionSoftware",
skip_serializing_if = "Option::is_none",
default
)]
pub data_collection_software: Option<DataCollectionSoftware>,
#[serde(
rename = "backgroundDeterminationMethod",
skip_serializing_if = "Option::is_none",
default
)]
pub background_determination_method: Option<String>,
#[serde(
rename = "cqDetectionMethod",
skip_serializing_if = "Option::is_none",
default
)]
pub cq_detection_method: Option<CqDetectionMethod>,
#[serde(
rename = "thermalCyclingConditions",
skip_serializing_if = "Option::is_none",
default
)]
pub thermal_cycling_conditions: Option<TccRef>,
#[serde(rename = "pcrFormat")]
pub pcr_format: PcrFormat,
#[serde(rename = "runDate", skip_serializing_if = "Option::is_none", default)]
pub run_date: Option<DateTime>,
#[serde(rename = "react", skip_serializing_if = "Vec::is_empty", default)]
pub reacts: Vec<React>,
}
impl Run {
#[must_use]
pub fn new(id: Id, pcr_format: PcrFormat) -> Self {
Self {
id,
description: None,
documentation: Vec::new(),
experimenters: Vec::new(),
instrument: None,
data_collection_software: None,
background_determination_method: None,
cq_detection_method: None,
thermal_cycling_conditions: None,
pcr_format,
run_date: None,
reacts: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DataCollectionSoftware {
pub name: String,
pub version: String,
}
impl DataCollectionSoftware {
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
name: name.into(),
version: version.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PcrFormat {
pub rows: i32,
pub columns: i32,
#[serde(rename = "rowLabel")]
pub row_label: LabelFormat,
#[serde(rename = "columnLabel")]
pub column_label: LabelFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlateLayout {
Plate {
rows: u32,
columns: u32,
},
List,
}
impl PcrFormat {
#[must_use]
pub fn new(rows: i32, columns: i32) -> Self {
Self {
rows,
columns,
row_label: LabelFormat::Numbers,
column_label: LabelFormat::Numbers,
}
}
#[must_use]
pub fn single_well() -> Self {
Self::new(1, 1)
}
#[must_use]
pub fn plate48() -> Self {
Self::plate(6, 8)
}
#[must_use]
pub fn plate96() -> Self {
Self::plate(8, 12)
}
#[must_use]
pub fn plate384() -> Self {
Self::plate(16, 24)
}
#[must_use]
pub fn plate1536() -> Self {
Self::plate(32, 48)
}
#[must_use]
pub fn array3072() -> Self {
Self {
rows: 32,
columns: 96,
row_label: LabelFormat::A1a1,
column_label: LabelFormat::A1a1,
}
}
#[must_use]
pub fn chip5184() -> Self {
Self::plate(72, 72)
}
#[must_use]
pub fn rotor(places: u32) -> Self {
Self::new(i32::try_from(places).unwrap_or(i32::MAX), 1)
}
#[must_use]
pub fn free_format() -> Self {
Self::new(-1, 1)
}
#[must_use]
pub fn plate(rows: u32, columns: u32) -> Self {
Self {
rows: i32::try_from(rows).unwrap_or(i32::MAX),
columns: i32::try_from(columns).unwrap_or(i32::MAX),
row_label: LabelFormat::Abc,
column_label: LabelFormat::Numbers,
}
}
#[must_use]
pub fn layout(&self) -> PlateLayout {
match (u32::try_from(self.rows), u32::try_from(self.columns)) {
(Ok(rows), Ok(columns)) if rows > 0 && columns > 0 => {
PlateLayout::Plate { rows, columns }
}
_ => PlateLayout::List,
}
}
#[must_use]
pub fn react_id_at(&self, row: u32, column: u32) -> Option<NonZeroU32> {
match self.layout() {
PlateLayout::Plate { rows, columns }
if (1..=rows).contains(&row) && (1..=columns).contains(&column) =>
{
NonZeroU32::new((row - 1) * columns + column)
}
_ => None,
}
}
#[must_use]
pub fn position_of(&self, react_id: NonZeroU32) -> Option<(u32, u32)> {
match self.layout() {
PlateLayout::Plate { rows, columns } => {
let index = react_id.get() - 1;
let (row, column) = (index / columns + 1, index % columns + 1);
(row <= rows).then_some((row, column))
}
PlateLayout::List => None,
}
}
#[must_use]
pub fn well_name(&self, react_id: NonZeroU32) -> String {
let fallback = react_id.to_string();
let Some((row, column)) = self.position_of(react_id) else {
return fallback;
};
let row_part = match self.row_label {
LabelFormat::Abc => letters(row),
LabelFormat::Numbers => row.to_string(),
LabelFormat::A1a1 => return fallback,
};
match (self.columns, self.column_label) {
(1, _) => row_part,
(_, LabelFormat::A1a1) => fallback,
(_, LabelFormat::Abc) => format!("{row_part}{}", letters(column)),
(_, LabelFormat::Numbers) => format!("{row_part}{column}"),
}
}
#[must_use]
pub fn row_name(&self, row: u32) -> String {
match self.row_label {
LabelFormat::Abc => letters(row),
LabelFormat::Numbers | LabelFormat::A1a1 => row.to_string(),
}
}
#[must_use]
pub fn column_name(&self, column: u32) -> String {
match self.column_label {
LabelFormat::Abc => letters(column),
LabelFormat::Numbers | LabelFormat::A1a1 => column.to_string(),
}
}
}
fn letters(mut n: u32) -> String {
let mut out = Vec::new();
while n > 0 {
let rem = (n - 1) % 26;
out.push(b'A' + rem as u8);
n = (n - 1) / 26;
}
out.reverse();
String::from_utf8(out).expect("ASCII letters")
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct React {
pub id: NonZeroU32,
pub sample: SampleRef,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub vol: Option<f64>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub data: Vec<Data>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub partitions: Option<Partitions>,
}
impl React {
#[must_use]
pub fn new(id: NonZeroU32, sample: SampleRef) -> Self {
Self {
id,
sample,
vol: None,
data: Vec::new(),
partitions: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Data {
pub tar: TargetRef,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub cq: Option<f64>,
#[serde(rename = "N0", skip_serializing_if = "Option::is_none", default)]
pub n0: Option<f64>,
#[serde(rename = "Ncopy", skip_serializing_if = "Option::is_none", default)]
pub n_copy: Option<f64>,
#[serde(rename = "ampEffMet", skip_serializing_if = "Option::is_none", default)]
pub amp_eff_met: Option<String>,
#[serde(rename = "ampEff", skip_serializing_if = "Option::is_none", default)]
pub amp_eff: Option<f64>,
#[serde(rename = "ampEffSE", skip_serializing_if = "Option::is_none", default)]
pub amp_eff_se: Option<f64>,
#[serde(rename = "corrF", skip_serializing_if = "Option::is_none", default)]
pub corr_f: Option<f64>,
#[serde(rename = "corrP", skip_serializing_if = "Option::is_none", default)]
pub corr_p: Option<f64>,
#[serde(rename = "corrCq", skip_serializing_if = "Option::is_none", default)]
pub corr_cq: Option<f64>,
#[serde(rename = "meltTemp", skip_serializing_if = "Option::is_none", default)]
pub melt_temp: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub excl: Option<Reasons>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub note: Option<Reasons>,
#[serde(rename = "adp", skip_serializing_if = "Vec::is_empty", default)]
pub adps: Vec<AmpPoint>,
#[serde(rename = "mdp", skip_serializing_if = "Vec::is_empty", default)]
pub mdps: Vec<MeltPoint>,
#[serde(rename = "endPt", skip_serializing_if = "Option::is_none", default)]
pub end_pt: Option<f64>,
#[serde(rename = "bgFluor", skip_serializing_if = "Option::is_none", default)]
pub bg_fluor: Option<f64>,
#[serde(
rename = "bgFluorSlp",
skip_serializing_if = "Option::is_none",
default
)]
pub bg_fluor_slp: Option<f64>,
#[serde(
rename = "quantFluor",
skip_serializing_if = "Option::is_none",
default
)]
pub quant_fluor: Option<f64>,
}
impl Data {
#[must_use]
pub fn new(tar: TargetRef) -> Self {
Self {
tar,
cq: None,
n0: None,
n_copy: None,
amp_eff_met: None,
amp_eff: None,
amp_eff_se: None,
corr_f: None,
corr_p: None,
corr_cq: None,
melt_temp: None,
excl: None,
note: None,
adps: Vec::new(),
mdps: Vec::new(),
end_pt: None,
bg_fluor: None,
bg_fluor_slp: None,
quant_fluor: None,
}
}
#[must_use]
pub fn is_excluded(&self) -> bool {
self.excl.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AmpPoint {
pub cyc: f64,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub tmp: Option<f64>,
pub fluor: f64,
}
impl AmpPoint {
#[must_use]
pub fn new(cyc: f64, fluor: f64) -> Self {
Self {
cyc,
tmp: None,
fluor,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MeltPoint {
pub tmp: f64,
pub fluor: f64,
}
impl MeltPoint {
#[must_use]
pub fn new(tmp: f64, fluor: f64) -> Self {
Self { tmp, fluor }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn nz(n: u32) -> NonZeroU32 {
NonZeroU32::new(n).unwrap()
}
#[test]
fn plate_position_math() {
let f = PcrFormat::plate96();
assert_eq!(f.react_id_at(1, 1), Some(nz(1)));
assert_eq!(f.react_id_at(1, 12), Some(nz(12)));
assert_eq!(f.react_id_at(2, 1), Some(nz(13)));
assert_eq!(f.react_id_at(8, 12), Some(nz(96)));
assert_eq!(f.react_id_at(9, 1), None);
assert_eq!(f.position_of(nz(1)), Some((1, 1)));
assert_eq!(f.position_of(nz(13)), Some((2, 1)));
assert_eq!(f.position_of(nz(96)), Some((8, 12)));
assert_eq!(f.position_of(nz(97)), None);
}
#[test]
fn well_names() {
let f = PcrFormat::plate96();
assert_eq!(f.well_name(nz(1)), "A1");
assert_eq!(f.well_name(nz(15)), "B3");
assert_eq!(f.well_name(nz(96)), "H12");
assert_eq!(f.well_name(nz(200)), "200");
let rotor = PcrFormat::rotor(72);
assert_eq!(rotor.well_name(nz(17)), "17"); assert_eq!(
rotor.layout(),
PlateLayout::Plate {
rows: 72,
columns: 1
}
);
let free = PcrFormat::free_format();
assert_eq!(free.layout(), PlateLayout::List);
assert_eq!(free.well_name(nz(5)), "5");
}
#[test]
fn row_and_column_names() {
let f = PcrFormat::plate96();
assert_eq!(f.row_name(2), "B");
assert_eq!(f.column_name(12), "12");
let rotor = PcrFormat::rotor(72);
assert_eq!(rotor.row_name(17), "17");
let array = PcrFormat::array3072();
assert_eq!(array.row_name(9), "9"); }
#[test]
fn letter_labels() {
assert_eq!(letters(1), "A");
assert_eq!(letters(26), "Z");
assert_eq!(letters(27), "AA");
assert_eq!(letters(52), "AZ");
assert_eq!(letters(72), "BT");
}
}