#[macro_export]
macro_rules! type_alias {
($($t:ty => $($i:ident),*);* $(;)?) => {
$($(type $i = $t;)*)*
};
}
use std::{collections::HashMap, fmt::Debug, sync::OnceLock};
use astro_float::{BigFloat, RoundingMode, Sign};
use num_traits::Zero;
use rand::Rng;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use crate::{
base::{Glue, GrowError, Tile},
canvas::{PointSafe2, PointSafeHere},
colors::get_color_or_random,
state::{State, StateEnum},
system::{Event, EvolveBounds, NeededUpdate, System, TileBondInfo},
units::*,
};
use ndarray::prelude::{Array1, Array2};
use serde::{Deserialize, Serialize};
#[cfg(feature = "python")]
use numpy::PyArrayMethods;
#[cfg(feature = "python")]
use numpy::ToPyArray;
#[cfg(feature = "python")]
use pyo3::prelude::*;
const WEST_GLUE_INDEX: usize = 0;
const BOTTOM_GLUE_INDEX: usize = 1;
const EAST_GLUE_INDEX: usize = 2;
const R: f64 = 1.98720425864083 / 1000.0; const U0: Molar = Molar(1.0);
fn bigfloat_to_f64(big_float: &BigFloat, rounding_mode: RoundingMode) -> f64 {
let mut big_float = big_float.clone();
big_float.set_precision(64, rounding_mode).unwrap();
let sign = big_float.sign().unwrap();
let exponent = big_float.exponent().unwrap();
let mantissa = big_float.mantissa_digits().unwrap()[0];
if mantissa == 0 {
return 0.0;
}
let mut exponent: isize = exponent as isize + 0b1111111111;
let mut ret = 0;
if exponent >= 0b11111111111 {
match sign {
Sign::Pos => f64::INFINITY,
Sign::Neg => f64::NEG_INFINITY,
}
} else if exponent <= 0 {
let shift = -exponent;
if shift < 52 {
ret |= mantissa >> (shift + 12);
if sign == Sign::Neg {
ret |= 0x8000000000000000u64;
}
f64::from_bits(ret)
} else {
0.0
}
} else {
let mantissa = mantissa << 1;
exponent -= 1;
if sign == Sign::Neg {
ret |= 1;
}
ret <<= 11;
ret |= exponent as u64;
ret <<= 52;
ret |= mantissa >> 12;
f64::from_bits(ret)
}
}
#[cfg_attr(feature = "python", pyclass(subclass, module = "rgrow.rgrow"))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDC {
pub strand_names: Vec<String>,
pub glue_names: Vec<String>,
pub quencher_id: Option<Tile>,
pub quencher_concentration: Molar,
pub reporter_id: Option<Tile>,
pub fluorophore_concentration: Molar,
pub scaffold: Array1<Glue>,
pub strand_concentration: Array1<Molar>,
pub scaffold_concentration: Molar,
pub strand_glues: Array2<Glue>,
pub colors: Vec<[u8; 4]>,
pub kf: PerMolarSecond,
pub friends_btm: Vec<Vec<Tile>>,
pub delta_g_matrix: Array2<KcalPerMol>,
pub entropy_matrix: Array2<KcalPerMolKelvin>,
temperature: Kelvin,
#[serde(skip)]
strand_energy_bonds: Array2<OnceLock<f64>>,
#[serde(skip)]
scaffold_energy_bonds: Array2<OnceLock<f64>>,
}
impl SDC {
fn bond_between_strands(&self, x: Tile, y: Tile) -> f64 {
*self.strand_energy_bonds[(x as usize, y as usize)].get_or_init(|| {
let x_east_glue = self.strand_glues[(x as usize, EAST_GLUE_INDEX)];
let y_west_glue = self.strand_glues[(y as usize, WEST_GLUE_INDEX)];
let glue_value = self.delta_g_matrix[(x_east_glue, y_west_glue)]
- (self.temperature - Celsius(37.0)).to_celsius()
* self.entropy_matrix[(x_east_glue, y_west_glue)];
glue_value.times_beta(self.temperature)
})
}
fn bond_with_scaffold(&self, scaffold_position: usize, strand: Tile) -> f64 {
*self.scaffold_energy_bonds[(scaffold_position, strand as usize)].get_or_init(|| {
let scaffold_glue = self.scaffold[scaffold_position];
let strand_glue = self.strand_glues[(strand as usize, BOTTOM_GLUE_INDEX)];
self.glue_glue_standard_free_energy(scaffold_glue, strand_glue)
.times_beta(self.temperature)
})
}
fn quencher_strand(&self) -> Tile {
(self.strand_names.len() - 2) as Tile
}
fn reporter_strand(&self) -> Tile {
(self.strand_names.len() - 1) as Tile
}
fn update_system(&mut self) {
self.empty_cache();
self.generate_scaffold_friends();
}
fn empty_cache(&mut self) {
let strand_count = self.strand_names.len();
let scaffold_count = self.scaffold.len();
self.strand_energy_bonds = Array2::default((strand_count, strand_count));
self.scaffold_energy_bonds = Array2::default((scaffold_count, strand_count));
}
fn generate_scaffold_friends(&mut self) {
let mut friends_btm: Vec<Vec<Tile>> = vec![Vec::new(); self.scaffold.len()];
for (scaffold_position, &scaffold_glue) in self.scaffold.iter().enumerate() {
for (strand, &strand_glue) in self
.strand_glues
.index_axis(ndarray::Axis(1), BOTTOM_GLUE_INDEX)
.iter()
.enumerate()
{
if self.delta_g_matrix[(scaffold_glue, strand_glue)] != KcalPerMol::zero()
|| self.entropy_matrix[(scaffold_glue, strand_glue)] != KcalPerMolKelvin::zero()
{
friends_btm[scaffold_position].push(strand as Tile);
}
}
}
self.friends_btm = friends_btm;
}
fn glue_glue_standard_free_energy(&self, tb: Glue, sb: Glue) -> KcalPerMol {
self.delta_g_matrix[(tb, sb)]
- (self.temperature - Celsius(37.0)) * self.entropy_matrix[(tb, sb)]
}
pub fn change_temperature_to(&mut self, temperature: impl Into<Kelvin>) {
self.temperature = temperature.into();
self.update_system();
}
pub fn n_scaffolds<S: State>(&self, state: &S) -> usize {
state.nrows_usable()
}
pub fn total_tile_count<S: State>(&self, state: &S, tile: Tile) -> usize {
let per = self.strand_concentration[tile as usize] / self.scaffold_concentration;
let net = per * self.n_scaffolds(state) as f64;
net as usize
}
#[inline(always)]
fn rtval(&self) -> f64 {
R * self.temperature.to_kelvin_m()
}
fn update_monomer_point<S: State>(&self, state: &mut S, scaffold_point: &PointSafe2) {
let mut points = Vec::with_capacity(3);
let pw = state.move_sa_w(*scaffold_point);
if state.inbounds(pw.0) {
points.push((pw, self.event_rate_at_point(state, pw)));
}
let pe = state.move_sa_e(*scaffold_point);
if state.inbounds(pe.0) {
points.push((pe, self.event_rate_at_point(state, pe)));
}
let ph = PointSafeHere(scaffold_point.0);
points.push((ph, self.event_rate_at_point(state, ph)));
state.update_multiple(&points);
}
pub fn fill_energy_array(&mut self) {
let num_of_strands = self.strand_names.len();
let glue_links = ndarray::Zip::from(&self.delta_g_matrix)
.and(&self.entropy_matrix)
.map_collect(|dg, ds| *dg - (self.temperature - Celsius(37.0)) * *ds); for strand_f in 1..num_of_strands {
let (f_west_glue, _, f_east_glue) = {
let glues = self.strand_glues.row(strand_f);
(
glues[WEST_GLUE_INDEX],
glues[BOTTOM_GLUE_INDEX],
glues[EAST_GLUE_INDEX],
)
};
for strand_s in 0..num_of_strands {
let (s_west_glue, s_east_glue) = {
let glues = self.strand_glues.row(strand_s);
(glues[WEST_GLUE_INDEX], glues[EAST_GLUE_INDEX])
};
let _ = self.strand_energy_bonds[(strand_f, strand_s)]
.set(glue_links[(f_east_glue, s_west_glue)].times_beta(self.temperature));
let _ = self.strand_energy_bonds[(strand_s, strand_f)]
.set(glue_links[(f_west_glue, s_east_glue)].times_beta(self.temperature));
}
}
for (s, &sb) in self.scaffold.iter().enumerate() {
for t in 0..num_of_strands {
let tb = self.strand_glues[(t, BOTTOM_GLUE_INDEX)];
let _ = self.scaffold_energy_bonds[(s, t)].set(
self.glue_glue_standard_free_energy(tb, sb)
.times_beta(self.temperature),
);
}
}
}
pub fn monomer_detachment_rate_at_point<S: State>(
&self,
state: &S,
scaffold_point: PointSafe2,
) -> PerSecond {
let strand = state.tile_at_point(scaffold_point);
if strand == 0
{
return PerSecond::zero();
}
let bond_energy = self.bond_energy_of_strand(state, scaffold_point, strand);
self.kf * Molar::u0_times(bond_energy.exp())
}
fn inverse_glue_id(g: Glue) -> Glue {
match g {
0 => 0,
x if x.is_multiple_of(2) => x - 1,
x => x + 1,
}
}
fn fluorophore_det_rate(&self) -> PerSecond {
if self.reporter_id.is_none() {
return PerSecond::zero();
}
let fluo_glue = self.strand_glues[(self.reporter_id.unwrap() as usize, WEST_GLUE_INDEX)];
let inv_glue = Self::inverse_glue_id(fluo_glue);
let glue_value = self.delta_g_matrix[(inv_glue, fluo_glue)]
- (self.temperature - Celsius(37.0)).to_celsius()
* self.entropy_matrix[(inv_glue, fluo_glue)];
let bond_energy = glue_value.times_beta(self.temperature);
self.kf * Molar::u0_times(bond_energy.exp())
}
fn fluorophore_att_rate(&self) -> PerSecond {
self.kf * self.fluorophore_concentration
}
fn fluorophore_probability(&self) -> f64 {
let a = self.fluorophore_att_rate().0;
let b = self.fluorophore_det_rate().0;
a / (a + b)
}
fn quencher_det_rate(&self) -> PerSecond {
if self.quencher_id.is_none() {
return PerSecond::zero();
}
let quench_glue = self.strand_glues[(self.quencher_id.unwrap() as usize, EAST_GLUE_INDEX)];
let inv_glue = Self::inverse_glue_id(quench_glue);
let glue_value = self.delta_g_matrix[(quench_glue, inv_glue)]
- (self.temperature - Celsius(37.0)).to_celsius()
* self.entropy_matrix[(quench_glue, inv_glue)];
let bond_energy = glue_value.times_beta(self.temperature);
self.kf * Molar::u0_times(bond_energy.exp())
}
fn quencher_att_rate(&self) -> PerSecond {
self.kf * self.quencher_concentration
}
fn quencher_probability(&self) -> f64 {
let a = self.quencher_att_rate().0;
let b = self.quencher_det_rate().0;
a / (a + b)
}
fn choose_quencher_attachment(&self) -> Tile {
let qid = self.quencher_id.unwrap();
let random = rand::random_range(0.0..1.0);
let prb = self.quencher_probability();
if random < prb {
self.quencher_strand()
} else {
qid
}
}
fn choose_reporter_attachment(&self) -> Tile {
let rid = self.reporter_id.unwrap();
let random = rand::random_range(0.0..1.0);
let prb = self.fluorophore_probability();
if random < prb {
self.reporter_strand()
} else {
rid
}
}
pub fn monomer_change_rate_at_point<S: State>(
&self,
state: &S,
scaffold_point: PointSafe2,
) -> PerSecond {
let strand = state.tile_at_point(scaffold_point);
let quencher_strand = self.quencher_strand();
let reporter_strand = self.reporter_strand();
match Some(strand) {
q if q == self.quencher_id => self.quencher_att_rate(),
r if r == self.reporter_id => self.fluorophore_att_rate(),
s if s == Some(quencher_strand) => self.quencher_det_rate(),
s if s == Some(reporter_strand) => self.fluorophore_det_rate(),
_ => PerSecond::zero(),
}
}
pub fn choose_monomer_change_at_point<S: State>(
&self,
state: &S,
point: PointSafe2,
mut acc: PerSecond,
) -> (bool, PerSecond, Event, f64) {
let strand = state.tile_at_point(point);
let quencher_strand = self.quencher_strand();
let reporter_strand = self.reporter_strand();
match Some(strand) {
q if q == self.quencher_id => {
let rate = self.quencher_att_rate();
acc -= rate;
if acc > PerSecond::zero() {
(true, acc, Event::None, f64::NAN)
} else {
(
true,
acc,
Event::MonomerAttachment(point, quencher_strand),
rate.into(),
)
}
}
r if r == self.reporter_id => {
let rate = self.fluorophore_att_rate();
acc -= rate;
if acc > PerSecond::zero() {
(true, acc, Event::None, f64::NAN)
} else {
(
true,
acc,
Event::MonomerAttachment(point, reporter_strand),
rate.into(),
)
}
}
s if s == Some(quencher_strand) => {
let rate = self.quencher_det_rate();
acc -= rate;
if acc > PerSecond::zero() {
(true, acc, Event::None, f64::NAN)
} else {
(
true,
acc,
Event::MonomerChange(point, self.quencher_id.unwrap()),
rate.into(),
)
}
}
s if s == Some(reporter_strand) => {
let rate = self.fluorophore_det_rate();
acc -= rate;
if acc > PerSecond::zero() {
(true, acc, Event::None, f64::NAN)
} else {
(
true,
acc,
Event::MonomerChange(point, self.reporter_id.unwrap()),
rate.into(),
)
}
}
_ => (false, acc, Event::None, f64::NAN),
}
}
pub fn choose_monomer_attachment_at_point<S: State>(
&self,
state: &S,
point: PointSafe2,
acc: PerSecond,
) -> (bool, PerSecond, Event, f64) {
self.find_monomer_attachment_possibilities_at_point(state, acc, point, false)
}
pub fn choose_monomer_detachment_at_point<S: State>(
&self,
state: &S,
point: PointSafe2,
mut acc: PerSecond,
) -> (bool, PerSecond, Event, f64) {
let rate = self.monomer_detachment_rate_at_point(state, point);
acc -= rate;
if acc > PerSecond::zero() {
return (false, acc, Event::None, rate.into());
}
(true, acc, Event::MonomerDetachment(point), rate.into())
}
fn find_monomer_attachment_possibilities_at_point<S: State>(
&self,
state: &S,
mut acc: PerSecond,
scaffold_coord: PointSafe2,
just_calc: bool,
) -> (bool, PerSecond, Event, f64) {
let point = scaffold_coord;
let tile = state.tile_at_point(point);
if tile != 0 {
return (false, acc, Event::None, f64::NAN);
}
let friends = self
.friends_btm
.get(point.0 .1)
.unwrap_or_else(|| panic!("Missing friends for scaffold position {}", point.0 .1));
let mut rand_thread = rand::rng();
for &strand in friends {
let rate = self.kf * self.strand_concentration[strand as usize];
acc -= rate;
if acc <= PerSecond::zero() && (!just_calc) {
let rand: f64 = rand_thread.random();
let total = self.total_tile_count(state, strand) as f64;
let attached = state.count_of_tile(strand) as f64;
if rand <= attached / total {
return (true, acc, Event::None, rate.into());
}
let strand = match strand {
s if Some(s) == self.quencher_id => self.choose_quencher_attachment(),
s if Some(s) == self.reporter_id => self.choose_reporter_attachment(),
other => other,
};
return (
true,
acc,
Event::MonomerAttachment(point, strand),
rate.into(),
);
}
}
(false, acc, Event::None, f64::NAN)
}
fn total_monomer_attachment_rate_at_point<S: State>(
&self,
state: &S,
scaffold_coord: PointSafe2,
) -> PerSecond {
match self.find_monomer_attachment_possibilities_at_point(
state,
PerSecond::zero(),
scaffold_coord,
true,
) {
(false, acc, _, _) => -acc,
_ => panic!(),
}
}
fn bond_energy_of_strand<S: State>(
&self,
state: &S,
scaffold_point: PointSafe2,
strand: u32,
) -> f64 {
let (w, e) = (
state.tile_to_w(scaffold_point) as usize,
state.tile_to_e(scaffold_point) as usize,
);
self.bond_with_scaffold(scaffold_point.0 .1, strand as Tile)
+ self.bond_between_strands(strand, e as Tile)
+ self.bond_between_strands(w as Tile, strand)
}
fn scaffold(&self) -> Vec<usize> {
self.scaffold.to_vec()
}
fn g_system(&self, attachments: &[u32]) -> f64 {
let mut sumg = 0.0;
for (id, strand) in attachments.iter().enumerate() {
if strand == &0 {
continue;
}
sumg += self.bond_with_scaffold(id, *strand);
if let Some(s) = attachments.get(id + 1) {
sumg += self.bond_between_strands(*strand, *s)
};
let penalty = (self.strand_concentration[*strand as usize] / U0).ln();
sumg -= penalty;
}
sumg * self.rtval()
}
pub fn system_states(&self) -> Vec<Vec<u32>> {
let scaffold = self.scaffold();
let mut acc = 1;
for i in 0..scaffold.len() {
if let Some(x) = self.friends_btm.get(i) {
acc *= x.len() + 1;
}
}
let mut possible_scaffolds: Vec<Vec<u32>> = Vec::with_capacity(acc);
possible_scaffolds.push(Vec::default());
for (i, _b) in scaffold.iter().enumerate() {
let friends = self.friends_btm.get(i).unwrap();
possible_scaffolds = possible_scaffolds
.iter()
.flat_map(|scaffold_attachments| {
let mut new_combinations: Vec<Vec<u32>> = Vec::new();
for f in friends {
let mut comb = scaffold_attachments.clone();
comb.push(*f);
new_combinations.push(comb);
}
let mut comb = scaffold_attachments.clone();
comb.push(0);
new_combinations.push(comb);
new_combinations
})
.collect();
}
possible_scaffolds
}
pub fn boltzman_function(&self, attachments: &[u32]) -> f64 {
let g_a = self.g_system(attachments);
(-g_a / self.rtval()).exp()
}
pub fn partition_function_full(&self) -> f64 {
self.system_states()
.iter()
.map(|attachments| self.boltzman_function(attachments))
.sum()
}
pub fn partition_function(&self) -> BigFloat {
let scaffold = self.scaffold();
let prec = 64;
let rm = astro_float::RoundingMode::None;
let mut cc =
astro_float::Consts::new().expect("An error occured when initializing constants");
let max_competition = self.friends_btm.iter().map(|x| x.len()).max().unwrap() + 1;
let mut z_curr = Array1::from_elem(max_competition, BigFloat::from_i32(0, prec));
let mut z_prev = Array1::from_elem(max_competition, BigFloat::from_i32(0, prec));
let mut z_sum = BigFloat::from_i64(1, prec);
let mut sum_a = BigFloat::from_i64(0, prec);
for (i, _b) in scaffold.iter().enumerate() {
for v in z_prev.iter() {
sum_a = sum_a.add(v, prec, rm);
}
z_prev.assign(&z_curr);
z_curr.fill(BigFloat::from_i32(0, prec));
let friends = match self.friends_btm.get(i) {
Some(f) => f,
None => continue,
};
for (j, &f) in friends.iter().enumerate() {
let attachment_beta_dg = self.bond_with_scaffold(i, f)
- (self.strand_concentration[f as usize] / U0).ln();
let t1 = BigFloat::from_f64(-attachment_beta_dg, prec).exp(prec, rm, &mut cc);
if i == 0 {
z_curr[j] = t1;
} else {
let mut t2 = BigFloat::from_f64(0., prec);
if let Some(ff) = self.friends_btm.get(i - 1) {
for (k, &g) in ff.iter().enumerate() {
let left_beta_dg = self.bond_between_strands(g, f);
t2 = t2.add(
&BigFloat::from_f64(-left_beta_dg, prec)
.exp(prec, rm, &mut cc)
.mul(&z_prev[k], prec, rm),
prec,
rm,
);
}
}
z_curr[j] = t1.mul(
&t2.add(&BigFloat::from_i64(1, prec), prec, rm)
.add(&sum_a, prec, rm),
prec,
rm,
);
}
z_sum = z_sum.add(&z_curr[j], prec, rm);
}
}
z_sum
}
pub fn partial_partition_function(&self, constrain_at_loc: Vec<Vec<Tile>>) -> BigFloat {
let scaffold = self.scaffold();
let prec = 64;
let rm = astro_float::RoundingMode::None;
let mut cc =
astro_float::Consts::new().expect("An error occured when initializing constants");
let max_competition = self.friends_btm.iter().map(|x| x.len()).max().unwrap() + 1;
let mut z_curr = Array1::from_elem(max_competition, BigFloat::from_i32(0, prec));
let mut z_prev = Array1::from_elem(max_competition, BigFloat::from_i32(0, prec));
let mut z_sum = BigFloat::from_i64(0, prec);
let mut prev_friends: Vec<u32> = Vec::new();
for (i, _b) in scaffold.iter().enumerate() {
z_prev.assign(&z_curr);
z_curr.fill(BigFloat::from_i32(0, prec));
let mut friends = vec![0];
if let Some(f) = self.friends_btm.get(i) {
friends.extend(f.iter().copied());
};
if !constrain_at_loc[i].is_empty() {
friends.retain(|x| constrain_at_loc[i].contains(x));
}
for (j, &f) in friends.iter().enumerate() {
let attachment_beta_dg = if f != 0 {
self.bond_with_scaffold(i, f)
- (self.strand_concentration[f as usize] / U0).ln()
} else {
0.0
};
let t1 = BigFloat::from_f64(-attachment_beta_dg, prec).exp(prec, rm, &mut cc);
if i == 0 {
z_curr[j] = t1;
} else {
let mut t2 = BigFloat::from_f64(0., prec);
for (k, &g) in prev_friends.iter().enumerate() {
let left_beta_dg = self.bond_between_strands(g, f);
t2 = t2.add(
&BigFloat::from_f64(-left_beta_dg, prec)
.exp(prec, rm, &mut cc)
.mul(&z_prev[k], prec, rm),
prec,
rm,
);
}
z_curr[j] = t1.mul(&t2, prec, rm);
}
}
prev_friends = friends;
}
for z in z_curr.iter() {
z_sum = z_sum.add(z, prec, rm);
}
z_sum
}
pub fn log_partition_function(&self) -> f64 {
let prec = 64;
let rm = astro_float::RoundingMode::None;
let mut cc =
astro_float::Consts::new().expect("An error occured when initializing constants"); bigfloat_to_f64(&self.partition_function().ln(prec, rm, &mut cc), rm)
}
pub fn log_partial_partition_function(&self, constrain_at_loc: Vec<Vec<Tile>>) -> f64 {
let prec = 64;
let rm = astro_float::RoundingMode::None;
let mut cc =
astro_float::Consts::new().expect("An error occured when initializing constants");
bigfloat_to_f64(
&self
.partial_partition_function(constrain_at_loc)
.ln(prec, rm, &mut cc),
rm,
)
}
pub fn probability_of_constrained_configurations(
&self,
constrain_at_loc: Vec<Vec<Tile>>,
) -> f64 {
(self.log_partial_partition_function(constrain_at_loc) - self.log_partition_function())
.exp()
}
pub fn probability_of_state(&self, system: &[u32]) -> f64 {
(-self.g_system(system) / self.rtval() - self.log_partition_function()).exp()
}
}
type MfeValues = Vec<(Tile, f64, Tile)>;
impl SDC {
#[inline(always)]
fn chemical_potential(&self, strand: &Tile) -> f64 {
(self.strand_concentration[*strand as usize] / U0).ln()
}
fn best_energy_for_strand(
&self,
left_possible: &MfeValues,
scaffold_position: usize,
right: &Tile,
) -> (Tile, f64) {
let (att, energy) = left_possible
.iter()
.fold(None, |acc, &(_prior_attachement, lenergy, left)| {
let nenergy = lenergy + self.bond_between_strands(left, *right);
if acc.is_none() {
return Some((left, nenergy));
}
let (acc_left, acc_value) = acc.unwrap();
if acc_value < nenergy {
Some((acc_left, acc_value))
} else {
Some((left, nenergy))
}
})
.unwrap_or((0, 0.0));
(
att,
energy + self.bond_with_scaffold(scaffold_position, *right)
- self.chemical_potential(right),
)
}
fn mfe_next_vector(
&self,
acc: &MfeValues,
scaffold_position: usize,
friends_here: Iter<Tile>,
) -> MfeValues {
let mut connection_answ = friends_here
.map(|tile| {
let (l, e) = self.best_energy_for_strand(acc, scaffold_position, tile);
(l, e, *tile)
})
.collect::<MfeValues>();
if !acc.is_empty() {
let (attached, min_energy) =
acc.iter()
.fold((0, f64::MAX), |(att_sf, min_energy_sf), &(_, e, t)| {
if min_energy_sf < e {
(att_sf, min_energy_sf)
} else {
(t, e)
}
});
connection_answ.push((attached, min_energy, 0));
} else {
connection_answ.push((0, 0.0, 0));
}
connection_answ
}
fn mfe_matrix(&self) -> Vec<MfeValues> {
let connection_matrix = self.scaffold.iter().enumerate().scan(
vec![],
|acc, (scaffold_position, &_scaffold_glue)| {
let friends = self.friends_btm.get(scaffold_position).unwrap_or_else(|| {
panic!(
"Missing friends for scaffold position {}",
scaffold_position
)
});
let n_vec = self.mfe_next_vector(acc, scaffold_position, friends.iter());
*acc = n_vec;
Some(
acc.iter()
.map(|(left, energy, tile)| (*left, energy * self.rtval(), *tile))
.collect(),
)
},
);
connection_matrix.collect()
}
pub fn mfe_configuration(&self) -> (Vec<Tile>, f64) {
let mfe_mat = self.mfe_matrix();
let l = mfe_mat.len();
let mut iterator = mfe_mat.into_iter().rev();
let Some(last) = iterator.next() else {
return (vec![], 0.0);
};
let (mut left, energy, strand) = last
.into_iter()
.min_by(|(_, energy1, _), (_, energy2, _)| energy1.partial_cmp(energy2).unwrap())
.unwrap();
let mut mfe_conf = Vec::with_capacity(l);
mfe_conf.push(strand);
for v in iterator {
let (new_left, _, strand) = v
.iter()
.find(|(_, _, strand)| *strand == left)
.expect("Could not find strand we are meant to attach to ...");
mfe_conf.push(*strand);
left = *new_left;
}
mfe_conf.reverse();
(mfe_conf, energy)
}
}
impl System for SDC {
fn update_after_event<St: State>(&self, state: &mut St, event: &Event) {
match event {
Event::None => todo!(),
Event::MonomerAttachment(scaffold_point, _)
| Event::MonomerDetachment(scaffold_point)
| Event::MonomerChange(scaffold_point, _) => {
self.update_monomer_point(state, scaffold_point)
}
_ => panic!("This event is not supported in SDC"),
}
}
fn perform_event<St: State>(&self, state: &mut St, event: &Event) -> f64 {
match event {
Event::None => panic!("Being asked to perform null event."),
Event::MonomerAttachment(point, strand) => {
state.update_attachment(*strand);
state.set_sa(point, strand);
}
Event::MonomerDetachment(point) => {
let strand = state.tile_at_point(*point);
state.update_detachment(strand);
state.set_sa(point, &0);
}
Event::MonomerChange(point, strand) => state.set_sa(point, strand),
_ => panic!("This event is not supported in SDC"),
};
f64::NAN }
fn event_rate_at_point<St: State>(
&self,
state: &St,
p: crate::canvas::PointSafeHere,
) -> PerSecond {
if !state.inbounds(p.0) {
return PerSecond::zero();
}
let scaffold_coord = PointSafe2(p.0);
match state.tile_at_point(scaffold_coord) {
0 => self.total_monomer_attachment_rate_at_point(state, scaffold_coord),
_ => {
self.monomer_detachment_rate_at_point(state, scaffold_coord)
+ self.monomer_change_rate_at_point(state, scaffold_coord)
}
}
}
fn choose_event_at_point<St: State>(
&self,
state: &St,
point: crate::canvas::PointSafe2,
acc: PerSecond,
) -> (crate::system::Event, f64) {
let (occur, acc, event, rate) = self.choose_monomer_detachment_at_point(state, point, acc);
if occur {
return (event, rate);
}
let (occur, acc, event, rate) = self.choose_monomer_attachment_at_point(state, point, acc);
if occur {
return (event, rate);
}
let (occur, acc, event, rate) = self.choose_monomer_change_at_point(state, point, acc);
if occur {
return (event, rate);
}
let mut str_builder = String::new();
let (_, rate_monomer_att, event_monomer_att, _) =
self.choose_monomer_attachment_at_point(state, point, PerSecond::zero());
str_builder.push_str(&format!(
"Attachment: rate of {rate_monomer_att:?}, event {event_monomer_att:?}\n"
));
let (_, rate_monomer_det, event_monomer_det, _) =
self.choose_monomer_detachment_at_point(state, point, PerSecond::zero());
str_builder.push_str(&format!(
"Detachment: rate of {rate_monomer_det:?}, event {event_monomer_det:?}\n"
));
let (_, rate_monomer_change, event_monomer_change, _) =
self.choose_monomer_change_at_point(state, point, PerSecond::zero());
str_builder.push_str(&format!(
"Change: rate of {rate_monomer_change:?}, event {event_monomer_change:?}\n"
));
panic!(
"{:?}\nRate: {:?}, {:?}, {:?}, {:?}",
str_builder,
acc,
point,
state,
state.raw_array()
);
}
fn seed_locs(&self) -> Vec<(crate::canvas::PointSafe2, Tile)> {
Vec::default()
}
fn calc_mismatch_locations<St: State>(&self, state: &St) -> Array2<usize> {
let threshold = -0.1; let mut mismatch_locations = Array2::<usize>::zeros((state.nrows(), state.ncols()));
for i in 0..state.nrows() {
for j in 0..state.ncols() {
if !state.inbounds((i, j)) {
continue;
}
let p = PointSafe2((i, j));
let t = state.tile_at_point(p);
if t == 0 {
continue;
}
let te = state.tile_to_e(p);
let tw = state.tile_to_w(p);
let mm_e = ((te != 0) & (self.bond_between_strands(t, te) > threshold)) as usize;
let mm_w = ((tw != 0) & (self.bond_between_strands(tw, t) > threshold)) as usize;
mismatch_locations[(i, j)] = 4 * mm_e + mm_w;
}
}
mismatch_locations
}
fn set_param(
&mut self,
name: &str,
value: Box<dyn std::any::Any>,
) -> Result<crate::system::NeededUpdate, crate::base::GrowError> {
match name {
"kf" => {
let kf = value
.downcast_ref::<f64>()
.ok_or(GrowError::WrongParameterType(name.to_string()))?;
self.kf = PerMolarSecond::from(*kf);
self.update_system();
Ok(NeededUpdate::NonZero)
}
"strand_concentrations" => {
let tile_concs = value
.downcast_ref::<Array1<Molar>>()
.ok_or(GrowError::WrongParameterType(name.to_string()))?;
self.strand_concentration.clone_from(tile_concs);
self.update_system();
Ok(NeededUpdate::NonZero)
}
"temperature" => {
let temperature = value
.downcast_ref::<f64>()
.ok_or(GrowError::WrongParameterType(name.to_string()))?;
self.change_temperature_to(Celsius(*temperature));
Ok(NeededUpdate::NonZero)
}
_ => Err(GrowError::NoParameter(name.to_string())),
}
}
fn get_param(&self, name: &str) -> Result<Box<dyn std::any::Any>, crate::base::GrowError> {
match name {
"kf" => Ok(Box::new(f64::from(self.kf))),
"strand_concentrations" => Ok(Box::new(self.strand_concentration.clone())),
"energy_bonds" => Ok(Box::new(self.strand_energy_bonds.clone())),
"temperature" => Ok(Box::new(self.temperature.to_celsius().0)),
_ => Err(GrowError::NoParameter(name.to_string())),
}
}
fn list_parameters(&self) -> Vec<crate::system::ParameterInfo> {
use crate::system::ParameterInfo;
vec![
ParameterInfo {
name: "temperature".to_string(),
units: "°C".to_string(),
default_increment: 1.0,
min_value: Some(0.0),
max_value: Some(100.0),
description: Some("Simulation temperature".to_string()),
current_value: self.temperature.to_celsius().0,
},
ParameterInfo {
name: "kf".to_string(),
units: "M/s".to_string(),
default_increment: 1e5,
min_value: Some(0.0),
max_value: None,
description: Some("Forward reaction rate constant".to_string()),
current_value: f64::from(self.kf),
},
]
}
fn system_info(&self) -> String {
format!(
"1 dimensional SDC with scaffold of length {} and {} strands",
self.scaffold.len(),
self.strand_names.len(),
)
}
}
impl TileBondInfo for SDC {
fn tile_colors(&self) -> &Vec<[u8; 4]> {
&self.colors
}
fn tile_names(&self) -> &[String] {
&self.strand_names
}
fn bond_names(&self) -> &[String] {
&self.glue_names
}
}
use std::hash::Hash;
use std::slice::Iter;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "python", derive(pyo3::FromPyObject))]
pub enum RefOrPair {
Ref(String),
Pair(String, String),
}
impl From<String> for RefOrPair {
fn from(r: String) -> Self {
RefOrPair::Ref(r)
}
}
impl From<(String, String)> for RefOrPair {
fn from(p: (String, String)) -> Self {
RefOrPair::Pair(p.0, p.1)
}
}
#[derive(Debug)]
#[cfg_attr(feature = "python", derive(pyo3::FromPyObject))]
pub enum SingleOrMultiScaffold {
Single(Vec<Option<String>>),
Multi(Vec<Vec<Option<String>>>),
}
impl From<Vec<Option<String>>> for SingleOrMultiScaffold {
fn from(v: Vec<Option<String>>) -> Self {
SingleOrMultiScaffold::Single(v)
}
}
impl From<Vec<Vec<Option<String>>>> for SingleOrMultiScaffold {
fn from(v: Vec<Vec<Option<String>>>) -> Self {
SingleOrMultiScaffold::Multi(v)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "python", derive(pyo3::FromPyObject))]
pub struct SDCStrand {
pub name: Option<String>,
pub color: Option<String>,
pub concentration: f64,
pub btm_glue: Option<String>,
pub left_glue: Option<String>,
pub right_glue: Option<String>,
}
#[derive(Debug)]
#[cfg_attr(feature = "python", derive(pyo3::FromPyObject))]
pub enum GsOrSeq {
GS((f64, f64)),
Seq(String),
}
fn gsorseq_to_gs(gsorseq: &GsOrSeq) -> (KcalPerMol, KcalPerMolKelvin) {
match gsorseq {
GsOrSeq::GS(x) => (KcalPerMol(x.0), KcalPerMolKelvin(x.1)),
GsOrSeq::Seq(s) => crate::utils::string_dna_dg_ds(s.as_str()),
}
}
#[derive(Debug)]
#[cfg_attr(feature = "python", derive(pyo3::FromPyObject))]
pub struct SDCParams {
pub strands: Vec<SDCStrand>,
pub quencher_name: Option<String>,
pub quencher_concentration: f64,
pub reporter_name: Option<String>,
pub fluorophore_concentration: f64,
pub scaffold: SingleOrMultiScaffold,
pub scaffold_concentration: f64,
pub glue_dg_s: HashMap<RefOrPair, GsOrSeq>,
pub k_f: f64,
pub k_n: f64,
pub k_c: f64,
pub temperature: f64,
pub junction_penalty_dg: Option<KcalPerMol>,
pub junction_penalty_ds: Option<KcalPerMolKelvin>,
}
fn self_and_inverse(value: &str) -> (bool, String, String) {
let filtered = value.trim_end_matches("*");
let star_count = value.len() - filtered.len();
let is_from = star_count.is_multiple_of(2);
(is_from, filtered.to_string(), format!("{filtered}*"))
}
pub(super) fn get_or_generate(
map: &mut HashMap<String, usize>,
count: &mut usize,
val: Option<String>,
) -> usize {
let str = match val {
Some(x) => x,
None => return 0,
};
let (is_from, fromval, toval) = self_and_inverse(&str);
let simpl = if is_from { &fromval } else { &toval };
let res = map.get(simpl);
if let Some(u) = res {
return *u;
}
map.insert(fromval, *count);
map.insert(toval, *count + 1);
*count += 2;
if is_from {
*count - 2
} else {
*count - 1
}
}
impl SDCParams {
fn fluo_quen_check(&self) {
let qn = self.quencher_name.clone();
let rn = self.reporter_name.clone();
if qn.is_none() && rn.is_none() {
return;
}
self.strands.iter().for_each(|SDCStrand { name, left_glue, right_glue, .. }| {
if name.clone() == qn && right_glue.is_none() {
panic!("Quenching strand must have a right glue -- No sequence provided for the quencher.");
}
if name.clone() == rn && left_glue.is_none() {
panic!("Reporter strand must have a left glue -- No sequence provided for the fluorophore.");
}
});
}
fn validity_check(&self) {
self.fluo_quen_check();
}
}
impl SDC {
pub fn from_params(params: SDCParams) -> Self {
params.validity_check();
let mut glue_name_map: HashMap<String, usize> = HashMap::new();
let strand_count = params.strands.len() + 3;
let quencher_index = strand_count - 2;
let reporter_index = strand_count - 1;
let mut strand_names: Vec<String> = Vec::with_capacity(strand_count);
let mut strand_colors: Vec<[u8; 4]> = Vec::with_capacity(strand_count);
let mut strand_concentration = Array1::<f64>::zeros(strand_count);
strand_names.push("null".to_string());
strand_colors.push([0, 0, 0, 0]);
strand_concentration[0] = 0.0;
let mut glues = Array2::<usize>::zeros((strand_count + 3, 3));
let mut gluenum = 1;
for (
id,
SDCStrand {
name,
color,
concentration,
left_glue,
btm_glue,
right_glue,
},
) in params.strands.into_iter().enumerate()
{
let strand_index = id + 1;
strand_names.push(name.unwrap_or(id.to_string()));
let color_as_str = color.as_deref();
let color_or_rand = get_color_or_random(color_as_str).unwrap();
strand_colors.push(color_or_rand);
glues[(strand_index, WEST_GLUE_INDEX)] =
get_or_generate(&mut glue_name_map, &mut gluenum, left_glue);
glues[(strand_index, BOTTOM_GLUE_INDEX)] =
get_or_generate(&mut glue_name_map, &mut gluenum, btm_glue);
glues[(strand_index, EAST_GLUE_INDEX)] =
get_or_generate(&mut glue_name_map, &mut gluenum, right_glue);
strand_concentration[strand_index] = concentration;
}
strand_names.push("quencher".to_string());
strand_names.push("fluorophore".to_string());
strand_colors.push([0, 0, 0, 0]);
strand_colors.push([0, 0, 0, 0]);
strand_concentration[quencher_index] = params.quencher_concentration;
strand_concentration[reporter_index] = params.fluorophore_concentration;
let quencher_id: Option<Tile> = params
.quencher_name
.and_then(|name| strand_names.iter().position(|x| x == &name))
.map(|index| index as Tile);
let reporter_id = params
.reporter_name
.and_then(|name| strand_names.iter().position(|x| x == &name))
.map(|index| index as Tile);
if let Some(q_id) = quencher_id {
let q_id = q_id as usize;
glues[(quencher_index, WEST_GLUE_INDEX)] = glues[(q_id, WEST_GLUE_INDEX)];
glues[(quencher_index, BOTTOM_GLUE_INDEX)] = glues[(q_id, BOTTOM_GLUE_INDEX)];
glues[(quencher_index, EAST_GLUE_INDEX)] = 0;
}
if let Some(r_id) = reporter_id {
let r_id = r_id as usize;
glues[(reporter_index, WEST_GLUE_INDEX)] = 0;
glues[(reporter_index, BOTTOM_GLUE_INDEX)] = glues[(r_id, BOTTOM_GLUE_INDEX)];
glues[(reporter_index, EAST_GLUE_INDEX)] = glues[(r_id, EAST_GLUE_INDEX)];
}
let mut glue_delta_g = Array2::<KcalPerMol>::zeros((gluenum, gluenum));
let mut glue_s = Array2::<KcalPerMolKelvin>::zeros((gluenum, gluenum));
for (k, gs_or_dna_sequence) in params.glue_dg_s.iter() {
let gs = gsorseq_to_gs(gs_or_dna_sequence);
let (i, j) = match k {
RefOrPair::Ref(r) => {
let (_, base, inverse) = self_and_inverse(r);
(base, inverse)
}
RefOrPair::Pair(r1, r2) => {
let (r1, r1f, r1t) = self_and_inverse(r1);
let (r2, r2f, r2t) = self_and_inverse(r2);
(if r1 { r1f } else { r1t }, if r2 { r2f } else { r2t })
}
};
let (i, j) = match (glue_name_map.get(&i), glue_name_map.get(&j)) {
(Some(&x), Some(&y)) => (x, y),
_ => continue,
};
glue_delta_g[[i, j]] = gs.0 + params.junction_penalty_dg.unwrap_or(KcalPerMol(0.0));
glue_delta_g[[j, i]] = gs.0 + params.junction_penalty_dg.unwrap_or(KcalPerMol(0.0));
glue_s[[i, j]] = gs.1 + params.junction_penalty_ds.unwrap_or(KcalPerMolKelvin(0.0));
glue_s[[j, i]] = gs.1 + params.junction_penalty_ds.unwrap_or(KcalPerMolKelvin(0.0));
}
let scaffold = match params.scaffold {
SingleOrMultiScaffold::Single(s) => {
let mut scaffold = Array1::<Glue>::zeros(s.len());
for (scaf_val, maybe_g) in scaffold.iter_mut().zip(s.iter()) {
if let Some(g) = maybe_g {
*scaf_val = *glue_name_map
.get(g)
.unwrap_or_else(|| panic!("ERROR: Glue {g} in scaffold not found!"));
} else {
*scaf_val = 0;
}
}
scaffold
}
SingleOrMultiScaffold::Multi(_m) => todo!(),
};
let mut glue_names = vec![String::default(); gluenum];
for (s, i) in glue_name_map.iter() {
glue_names[*i] = s.clone();
}
{
let strand_concentration = strand_concentration.mapv(Molar::new);
let scaffold_concentration = Molar::new(params.scaffold_concentration);
let kf: PerMolarSecond = PerMolarSecond::new(params.k_f);
let temperature = Celsius(params.temperature);
let strand_count = strand_names.len();
let scaffold_count = scaffold.len();
let mut s = SDC {
strand_concentration,
strand_names,
colors: strand_colors,
strand_glues: glues,
scaffold,
glue_names,
quencher_id,
quencher_concentration: Molar(params.quencher_concentration),
reporter_id,
fluorophore_concentration: Molar(params.fluorophore_concentration),
kf,
delta_g_matrix: glue_delta_g,
entropy_matrix: glue_s,
temperature: temperature.into(),
scaffold_concentration,
friends_btm: Vec::new(),
strand_energy_bonds: Array2::default((strand_count, strand_count)),
scaffold_energy_bonds: Array2::default((scaffold_count, strand_count)),
};
s.update_system();
s
}
}
}
#[cfg_attr(feature = "python", pyclass)]
pub struct AnnealProtocol {
pub temperatures: (f64, f64),
pub holds: (f64, f64),
pub anneal_time: f64,
pub seconds_per_step: f64,
pub scaffold_count: usize,
}
type AnnealOutput = (Vec<Vec<u32>>, Vec<f64>, Vec<f64>);
impl Default for AnnealProtocol {
fn default() -> Self {
AnnealProtocol {
temperatures: (80., 20.),
holds: (10. * 60., 45. * 60.),
anneal_time: 3.0 * 60.0 * 60.0,
seconds_per_step: 2.0,
scaffold_count: 100,
}
}
}
impl AnnealProtocol {
#[inline(always)]
fn initial_steps(&self) -> usize {
(self.holds.0 / self.seconds_per_step).ceil() as usize
}
#[inline(always)]
fn final_steps(&self) -> usize {
(self.holds.1 / self.seconds_per_step).ceil() as usize
}
#[inline(always)]
fn delta_steps(&self) -> usize {
(self.anneal_time / self.seconds_per_step).ceil() as usize
}
pub fn generate_arrays(&self) -> (Vec<f64>, Vec<f64>) {
let steps_init = self.initial_steps();
let steps_final = self.final_steps();
let steps_delta = self.delta_steps();
let mut temps = Vec::<f64>::with_capacity(steps_init + steps_delta + steps_final);
let mut times = Vec::<f64>::with_capacity(steps_init + steps_delta + steps_final);
let temperature_diff = self.temperatures.0 - self.temperatures.1;
let temperature_delta = temperature_diff / (steps_delta as f64);
let mut current_time = 0.0;
let mut current_temp = self.temperatures.0;
(0..steps_init).for_each(|_step_num| {
current_time += self.seconds_per_step;
temps.push(current_temp);
times.push(current_time);
});
(0..steps_delta).for_each(|_step_num| {
current_time += self.seconds_per_step;
current_temp -= temperature_delta;
temps.push(current_temp);
times.push(current_time);
});
(0..steps_final).for_each(|_step_num| {
current_time += self.seconds_per_step;
temps.push(current_temp);
times.push(current_time);
});
(temps, times)
}
pub fn run_system<St: State>(
&self,
mut sdc: SDC,
mut state: St,
) -> Result<AnnealOutput, GrowError> {
let (tmps, times) = self.generate_arrays();
let bounds = EvolveBounds::default().for_time(self.seconds_per_step);
let needed = NeededUpdate::NonZero;
let mut canvases = Vec::new();
for tmp in &tmps {
sdc.temperature = (*tmp).into();
sdc.update_system();
crate::system::System::update_state(&sdc, &mut state, &needed);
crate::system::System::evolve(&sdc, &mut state, bounds)?;
let canvas = state.raw_array().to_slice().unwrap();
canvases.push(canvas.to_vec())
}
Ok((canvases, times, tmps))
}
fn default_state(&self, sdc: &SDC) -> Result<StateEnum, GrowError> {
let scaffold_size = sdc.scaffold().len();
let shape = (self.scaffold_count, scaffold_size);
let n_tile_types = sdc.strand_names.len();
StateEnum::empty(
shape,
crate::tileset::CanvasType::Square,
crate::tileset::TrackingType::None,
n_tile_types,
)
}
pub fn run_anneal_default_system(&self, sdc: SDC) -> Result<AnnealOutput, GrowError> {
let state = self.default_state(&sdc)?;
self.run_system(sdc, state)
}
pub fn run_many_anneals_default_system(
&self,
sdcs: Vec<SDC>,
) -> Vec<Result<AnnealOutput, GrowError>> {
sdcs.par_iter()
.map(|sdc| self.run_anneal_default_system(sdc.clone()))
.collect()
}
}
#[cfg(feature = "python")]
#[pymethods]
impl AnnealProtocol {
#[new]
fn new(
from_tmp: f64,
to_tmp: f64,
initial_hold: f64,
final_hold: f64,
delta_time: f64,
scaffold_count: usize,
seconds_per_step: f64,
) -> Self {
AnnealProtocol {
temperatures: (from_tmp, to_tmp),
seconds_per_step,
anneal_time: delta_time,
holds: (initial_hold, final_hold),
scaffold_count,
}
}
fn run_one_system(&self, sdc: SDC) -> Option<AnnealOutput> {
self.run_anneal_default_system(sdc).ok()
}
fn run_many_systems(&self, sdcs: Vec<SDC>) -> Vec<Option<AnnealOutput>> {
self.run_many_anneals_default_system(sdcs)
.into_iter()
.map(|z| z.ok())
.collect()
}
}
#[cfg(feature = "python")]
#[pymethods]
impl SDC {
#[new]
fn py_new(params: SDCParams) -> Self {
SDC::from_params(params)
}
fn partition(&self) -> f64 {
self.partition_function_full()
}
fn distribution(&self) -> Vec<f64> {
let mut probability = self
.system_states()
.iter()
.map(|sys| self.probability_of_state(sys))
.collect::<Vec<_>>();
probability.sort_unstable_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
probability
}
fn set_tmp_c(&mut self, tmp: f64) {
self.temperature = Celsius(tmp).into();
self.update_system();
}
#[getter]
fn get_scaffold_energy_bonds<'py>(
&mut self,
py: Python<'py>,
) -> Bound<'py, numpy::PyArray2<f64>> {
self.fill_energy_array();
self.scaffold_energy_bonds
.map(|x| *x.get().unwrap())
.to_pyarray(py)
}
#[getter]
fn get_strand_energy_bonds<'py>(
&mut self,
py: Python<'py>,
) -> Bound<'py, numpy::PyArray2<f64>> {
self.fill_energy_array();
self.strand_energy_bonds
.map(|x| *x.get().unwrap())
.to_pyarray(py)
}
#[getter]
fn get_tile_concs<'py>(&self, py: Python<'py>) -> Bound<'py, numpy::PyArray1<f64>> {
self.strand_concentration.mapv(Molar::into).to_pyarray(py)
}
#[setter]
fn set_tile_concs(&mut self, concs: Vec<f64>) {
self.strand_concentration = Array1::from(concs).mapv(Molar::new);
self.update_system();
}
fn get_all_probs(&self) -> Vec<(Vec<u32>, f64, f64)> {
let systems = self.system_states();
let mut triples = Vec::new();
for s in systems {
let prob = self.probability_of_state(&s);
let energy = self.boltzman_function(&s);
triples.push((s, prob, energy));
}
triples.sort_unstable_by(|(_, x, _), (_, y, _)| {
x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
});
triples
}
fn quencher_rates(&self) -> String {
let att_rate = self.quencher_att_rate();
let det_rate = self.quencher_det_rate();
format!("Attachment Rate: {att_rate}, Detachment Rate: {det_rate}")
}
fn fluorophore_rates(&self) -> String {
let att_rate = self.fluorophore_att_rate();
let det_rate = self.fluorophore_det_rate();
format!("Attachment Rate: {att_rate}, Detachment Rate: {det_rate}")
}
#[pyo3(name = "partition_function")]
fn py_partition_function(&self) -> f64 {
bigfloat_to_f64(&self.partition_function(), astro_float::RoundingMode::None)
}
#[pyo3(name = "partition_function_full")]
fn py_partition_function_full(&self) -> f64 {
self.partition_function_full()
}
#[pyo3(name = "probability_of_state")]
fn py_probability_of_state(&self, state: Vec<u32>) -> f64 {
self.probability_of_state(&state)
}
#[pyo3(name = "state_g")]
fn py_state_g(&self, state: Vec<u32>) -> f64 {
self.g_system(&state)
}
#[pyo3(name = "rtval")]
fn py_rtval(&self) -> f64 {
self.rtval()
}
#[pyo3(name = "log_partition_function")]
fn py_log_partition_function(&self) -> f64 {
self.log_partition_function()
}
#[pyo3(name = "mfe_matrix")]
fn py_mfe_matrix(&self) -> Vec<Vec<(u32, f64, u32)>> {
self.mfe_matrix()
}
#[pyo3(name = "mfe_config")]
fn py_mfe_config(&self) -> (Vec<Tile>, f64) {
self.mfe_configuration()
}
#[setter]
fn set_temperature(&mut self, tmp: f64) {
self.temperature = Celsius(tmp).into();
self.update_system();
}
#[getter]
fn get_temperature(&self) -> f64 {
self.temperature.to_celsius().0
}
#[pyo3(name = "all_scaffolds_slow")]
fn py_all_scaffolds(&self) -> Vec<Vec<Tile>> {
self.system_states()
}
#[pyo3(name = "probability_of_constrained_configurations")]
fn py_probability_of_constrained_configurations(&self, constrain_at_loc: Vec<Vec<u32>>) -> f64 {
let constrain_at_loc: Vec<Vec<Tile>> = constrain_at_loc
.into_iter()
.map(|v| v.into_iter().map(|t| t as Tile).collect())
.collect();
self.probability_of_constrained_configurations(constrain_at_loc)
}
#[pyo3(name = "partial_partition_function")]
fn py_partial_partition_function(&self, constrain_at_loc: Vec<Vec<u32>>) -> f64 {
let constrain_at_loc: Vec<Vec<Tile>> = constrain_at_loc
.into_iter()
.map(|v| v.into_iter().map(|t| t as Tile).collect())
.collect();
bigfloat_to_f64(
&self.partial_partition_function(constrain_at_loc),
astro_float::RoundingMode::None,
)
}
#[pyo3(name = "log_partial_partition_function")]
fn py_log_partial_partition_function(&self, constrain_at_loc: Vec<Vec<u32>>) -> f64 {
let constrain_at_loc: Vec<Vec<Tile>> = constrain_at_loc
.into_iter()
.map(|v| v.into_iter().map(|t| t as Tile).collect())
.collect();
self.log_partial_partition_function(constrain_at_loc)
}
#[getter]
fn get_entropy_matrix<'py>(&self, py: Python<'py>) -> Bound<'py, numpy::PyArray2<f64>> {
self.entropy_matrix.mapv(|x| x.0).to_pyarray(py)
}
#[setter]
fn set_entropy_matrix(&mut self, entropy_matrix: &Bound<'_, numpy::PyArray2<f64>>) {
let array = entropy_matrix.to_owned_array();
self.entropy_matrix = array.mapv(KcalPerMolKelvin);
self.update_system();
}
#[getter]
fn get_delta_g_matrix<'py>(&self, py: Python<'py>) -> Bound<'py, numpy::PyArray2<f64>> {
self.delta_g_matrix.mapv(|x| x.0).to_pyarray(py)
}
#[setter]
fn set_delta_g_matrix(&mut self, delta_g_matrix: &Bound<'_, numpy::PyArray2<f64>>) {
let array = delta_g_matrix.to_owned_array();
self.delta_g_matrix = array.mapv(KcalPerMol);
self.update_system();
}
}
#[cfg(test)]
mod test_anneal {
use super::*;
const ANNEAL: AnnealProtocol = AnnealProtocol {
temperatures: (88., 28.),
holds: (10. * 60., 45. * 60.),
anneal_time: 3.0 * 60.0 * 60.0,
seconds_per_step: 2.0,
scaffold_count: 100,
};
fn gen_sdc() -> SDC {
let mut strands = Vec::<SDCStrand>::new();
strands.push(SDCStrand {
name: Some("0A0".to_string()),
color: None,
concentration: 1e-6,
btm_glue: Some(String::from("A")),
left_glue: None,
right_glue: Some("0e".to_string()),
});
strands.push(SDCStrand {
name: Some("-E-".to_string()),
color: None,
concentration: 1e-6,
btm_glue: Some(String::from("E")),
left_glue: None,
right_glue: None,
});
for base in "BCD".chars() {
let (leo, reo): (String, String) = if base == 'C' {
("o".to_string(), "e".to_string())
} else {
("e".to_string(), "o".to_string())
};
let name = format!("0{base}0");
let lg = format!("0{leo}*");
let rg = format!("0{reo}");
strands.push(SDCStrand {
name: Some(name),
color: None,
concentration: 1e-6,
btm_glue: Some(String::from(base)),
left_glue: Some(lg),
right_glue: Some(rg),
});
let name = format!("1{base}1");
let lg = format!("1{leo}*");
let rg = format!("1{reo}*");
strands.push(SDCStrand {
name: Some(name),
color: None,
concentration: 1e-6,
btm_glue: Some(String::from(base)),
left_glue: Some(lg),
right_glue: Some(rg),
})
}
let scaffold = SingleOrMultiScaffold::Single(vec![
None,
None,
Some("A*".to_string()),
Some("B*".to_string()),
Some("C*".to_string()),
Some("D*".to_string()),
Some("E*".to_string()),
None,
None,
]);
let glue_dg_s: HashMap<RefOrPair, GsOrSeq> = HashMap::from(
[
("0e", "GCTGAGAAGAGG"),
("1e", "GGATCGGAGATG"),
("2e", "GGCTTGGAAAGA"),
("3e", "GGCAAGGATTGA"),
("4e", "AACAGGGATGTG"),
("5e", "AATGGGACATGG"),
("6e", "GAACGTTGGTTG"),
("7e", "GACGAAGTGTGA"),
("0o", "GGTCAGGATGAG"),
("1o", "GAACGGAGTTGA"),
("2o", "AATGGTGGCATT"),
("3o", "GACAAGGGTTGT"),
("4o", "TGTTGGGAACAG"),
("5o", "GGACTGGTAGTG"),
("6o", "GACAGTGTGTGT"),
("7o", "GGACGAAAGTGA"),
("A", "TCTTTCCAGAGCCTAATTTGCCAG"),
("B", "AGCGTCCAATACTGCGGAATCGTC"),
("C", "ATAAATATTCATTGAATCCCCCTC"),
("D", "AAATGCTTTAAACAGTTCAGAAAA"),
("E", "CGAGAATGACCATAAATCAAAAAT"),
]
.map(|(r, g)| (RefOrPair::Ref(r.to_string()), GsOrSeq::Seq(g.to_string()))),
);
let sdc_params = SDCParams {
strands,
scaffold,
temperature: 20.0,
scaffold_concentration: 1e-100,
glue_dg_s,
k_f: 1e6,
k_n: 1e5,
k_c: 1e4,
junction_penalty_dg: None,
junction_penalty_ds: None,
quencher_name: None,
quencher_concentration: 0.0,
reporter_name: None,
fluorophore_concentration: 0.0,
};
let mut sdc = SDC::from_params(sdc_params);
sdc.update_system();
sdc
}
#[test]
fn test_time_and_temp_array() {
let (tmp, time) = ANNEAL.generate_arrays();
let mut expected_time = vec![];
let mut ctime = 2.0;
loop {
expected_time.push(ctime);
ctime += 2.0;
if ctime > 14100.0 {
break;
}
}
assert_eq!(time, expected_time);
(0..300).for_each(|i| {
let top = tmp[i];
assert_eq!(top, 88.0);
});
let tmps = [
87.98888683089461,
87.97777366178921,
87.96666049268383,
87.95554732357844,
87.94443415447304,
87.93332098536766,
];
(0..6).for_each(|i| {
let top = tmp[300 + i];
assert!((tmps[i] - top).abs() < 0.1);
})
}
#[test]
fn test_run_anneal() {
let sdc = gen_sdc();
ANNEAL.run_anneal_default_system(sdc).unwrap();
}
}
#[cfg(test)]
mod test_sdc_model {
use num_traits::PrimInt;
use super::*;
#[test]
fn test_self_and_inverse() {
let input = vec!["some*str", "some*str*", "some*str**"];
let acc = input
.into_iter()
.map(self_and_inverse)
.collect::<Vec<(bool, String, String)>>();
let expected = [
(true, "some*str", "some*str*"),
(false, "some*str", "some*str*"),
(true, "some*str", "some*str*"),
]
.iter()
.map(|(a, b, c)| (*a, b.to_string(), c.to_string()))
.collect::<Vec<(bool, String, String)>>();
assert_eq!(acc, expected);
}
fn scaffold_for_tests() -> SDC {
let mut strands = Vec::<SDCStrand>::new();
strands.push(SDCStrand {
name: Some("0A0".to_string()),
color: None,
concentration: 1e-6,
btm_glue: Some(String::from("A")),
left_glue: None,
right_glue: Some("0e".to_string()),
});
strands.push(SDCStrand {
name: Some("-E-".to_string()),
color: None,
concentration: 1e-6,
btm_glue: Some(String::from("E")),
left_glue: None,
right_glue: None,
});
for base in "BCD".chars() {
let (leo, reo): (String, String) = if base == 'C' {
("o".to_string(), "e".to_string())
} else {
("e".to_string(), "o".to_string())
};
let name = format!("0{base}0");
let lg = format!("0{leo}*");
let rg = format!("0{reo}");
strands.push(SDCStrand {
name: Some(name),
color: None,
concentration: 1e-6,
btm_glue: Some(String::from(base)),
left_glue: Some(lg),
right_glue: Some(rg),
});
let name = format!("1{base}1");
let lg = format!("1{leo}*");
let rg = format!("1{reo}*");
strands.push(SDCStrand {
name: Some(name),
color: None,
concentration: 1e-6,
btm_glue: Some(String::from(base)),
left_glue: Some(lg),
right_glue: Some(rg),
})
}
let scaffold = SingleOrMultiScaffold::Single(vec![
None,
None,
Some("A*".to_string()),
Some("B*".to_string()),
Some("C*".to_string()),
Some("D*".to_string()),
Some("E*".to_string()),
None,
None,
]);
let glue_dg_s: HashMap<RefOrPair, GsOrSeq> = HashMap::from(
[
("0e", "GCTGAGAAGAGG"),
("1e", "GGATCGGAGATG"),
("2e", "GGCTTGGAAAGA"),
("3e", "GGCAAGGATTGA"),
("4e", "AACAGGGATGTG"),
("5e", "AATGGGACATGG"),
("6e", "GAACGTTGGTTG"),
("7e", "GACGAAGTGTGA"),
("0o", "GGTCAGGATGAG"),
("1o", "GAACGGAGTTGA"),
("2o", "AATGGTGGCATT"),
("3o", "GACAAGGGTTGT"),
("4o", "TGTTGGGAACAG"),
("5o", "GGACTGGTAGTG"),
("6o", "GACAGTGTGTGT"),
("7o", "GGACGAAAGTGA"),
("A", "TCTTTCCAGAGCCTAATTTGCCAG"),
("B", "AGCGTCCAATACTGCGGAATCGTC"),
("C", "ATAAATATTCATTGAATCCCCCTC"),
("D", "AAATGCTTTAAACAGTTCAGAAAA"),
("E", "CGAGAATGACCATAAATCAAAAAT"),
]
.map(|(r, g)| (RefOrPair::Ref(r.to_string()), GsOrSeq::Seq(g.to_string()))),
);
let sdc_params = SDCParams {
strands,
scaffold,
temperature: 20.0,
scaffold_concentration: 1e-100,
glue_dg_s,
k_f: 1e6,
k_n: 1e5,
k_c: 1e4,
junction_penalty_dg: None,
junction_penalty_ds: None,
quencher_name: None,
quencher_concentration: 0.0,
reporter_name: None,
fluorophore_concentration: 0.0,
};
let mut sdc = SDC::from_params(sdc_params);
sdc.update_system();
sdc
}
#[test]
fn probabilities() {
let sdc = scaffold_for_tests();
let scaffold = vec![0, 0, 2, 8, 16, 18, 6, 0, 0];
assert_eq!(sdc.scaffold(), scaffold);
let systems = sdc.system_states();
assert_eq!(systems.len(), 2.pow(2) * 3.pow(3));
let mut probs = systems
.iter()
.map(|s| (s.clone(), sdc.probability_of_state(s)))
.collect::<Vec<_>>();
probs.sort_by(|(_, p1), (_, p2)| {
p2.partial_cmp(p1).unwrap_or_else(|| panic!("{p1} -- {p2}"))
});
assert_eq!(probs[0].0, vec![0, 0, 1, 3, 5, 7, 2, 0, 0]);
}
#[test]
fn mfe_test() {
let sdc = scaffold_for_tests();
let x = sdc.mfe_matrix();
for (index, v) in x.iter().enumerate() {
println!("At index {index}:");
for (left_attachment_id, energy, final_strand) in v {
let left_attachment = sdc.tile_name(*left_attachment_id);
let strand_name = sdc.tile_name(*final_strand);
println!("\t Finishing at ({left_attachment_id} = {left_attachment}) <-> ({final_strand}, {strand_name}) we have DG = {energy}")
}
}
let last_compute_domain = x[x.len() - 4].clone();
let (_, f_energy, strand_id) = last_compute_domain[0];
let (_, s_energy, _) = last_compute_domain[1];
if sdc.tile_name(strand_id).contains('0') {
assert!(f_energy < s_energy);
} else {
assert!(s_energy < f_energy);
}
let mfe_config = [0, 0, 1, 3, 5, 7, 2, 0, 0];
let (acc, _) = sdc.mfe_configuration();
assert_eq!(mfe_config.to_vec(), acc);
}
#[test]
fn test_partition_function() {
let sdc = scaffold_for_tests();
let pf_full = sdc.partition_function_full();
let pf = bigfloat_to_f64(&sdc.partition_function(), astro_float::RoundingMode::None);
let rel_diff = ((pf - pf_full).abs() / pf_full.abs()).max((pf - pf_full).abs() / pf.abs());
assert!(rel_diff < 0.0001,
"Relative difference between partition_function ({}) and partition_function_full ({}) should be less than 0.01%, got {}",
pf, pf_full, rel_diff);
assert!(pf > 0.0, "Partition function should be positive");
assert!(pf_full > 0.0, "Partition function full should be positive");
}
#[test]
fn test_partial_partition_function_no_constraints() {
let sdc = scaffold_for_tests();
let scaffold_len = sdc.scaffold().len();
let empty_constraints: Vec<Vec<Tile>> = vec![Vec::new(); scaffold_len];
let partial_pf = bigfloat_to_f64(
&sdc.partial_partition_function(empty_constraints),
astro_float::RoundingMode::None,
);
let full_pf = sdc.partition_function_full();
let relative_error = ((partial_pf - full_pf) / full_pf).abs();
assert!(relative_error < 1e-10,
"partial_partition_function with no constraints should equal partition_function_full. partial_pf={}, full_pf={}, relative_error={}",
partial_pf, full_pf, relative_error);
}
#[test]
fn test_partial_partition_function_fully_constrained() {
let sdc = scaffold_for_tests();
let scaffold_len = sdc.scaffold().len();
let test_state = vec![0, 0, 1, 3, 5, 7, 2, 0, 0];
assert_eq!(test_state.len(), scaffold_len);
let full_constraints: Vec<Vec<Tile>> = test_state.iter().map(|&tile| vec![tile]).collect();
let partial_pf = bigfloat_to_f64(
&sdc.partial_partition_function(full_constraints),
astro_float::RoundingMode::None,
);
let g_system = sdc.g_system(&test_state);
let boltzmann = sdc.boltzman_function(&test_state);
let expected_pf = (-g_system / sdc.rtval()).exp();
let relative_error = ((partial_pf - expected_pf) / expected_pf).abs();
assert!(
relative_error < 1e-10,
"partial_partition_function when fully constrained should equal Boltzmann function. partial_pf={}, expected_pf={}, relative_error={}",
partial_pf, expected_pf, relative_error
);
let boltzmann_relative_error = ((partial_pf - boltzmann) / boltzmann).abs();
assert!(
boltzmann_relative_error < 1e-10,
"partial_partition_function when fully constrained should equal boltzman_function. partial_pf={}, boltzmann={}, relative_error={}",
partial_pf, boltzmann, boltzmann_relative_error
);
}
#[test]
fn test_partial_partition_function_few_states() {
let sdc = scaffold_for_tests();
let scaffold_len = sdc.scaffold().len();
let test_states = vec![
vec![0, 0, 1, 3, 5, 7, 2, 0, 0],
vec![0, 0, 0, 3, 5, 7, 2, 0, 0],
vec![0, 0, 1, 0, 5, 7, 2, 0, 0],
];
for state in &test_states {
assert_eq!(state.len(), scaffold_len);
}
let mut constraints: Vec<Vec<Tile>> = vec![Vec::new(); scaffold_len];
for pos in 0..scaffold_len {
let mut allowed_tiles = Vec::new();
for state in &test_states {
if !allowed_tiles.contains(&state[pos]) {
allowed_tiles.push(state[pos]);
}
}
constraints[pos] = allowed_tiles;
}
let partial_pf = bigfloat_to_f64(
&sdc.partial_partition_function(constraints),
astro_float::RoundingMode::None,
);
let expected_pf: f64 = test_states
.iter()
.map(|state| sdc.boltzman_function(state))
.sum();
let relative_error = ((partial_pf - expected_pf) / expected_pf).abs();
assert!(
relative_error < 1e-10,
"partial_partition_function constrained to few states should equal sum of their Boltzmann functions. partial_pf={}, expected_pf={}, relative_error={}",
partial_pf, expected_pf, relative_error
);
}
}