From 9ba74059d78672686a465c625e91b7dc147712ea Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:12:08 +0100 Subject: [PATCH 001/117] implmentation of part of the state trait --- rgrow/src/models/mod.rs | 2 + rgrow/src/models/sdc1d.rs | 331 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 rgrow/src/models/sdc1d.rs diff --git a/rgrow/src/models/mod.rs b/rgrow/src/models/mod.rs index 51a3a3a..b832eaf 100644 --- a/rgrow/src/models/mod.rs +++ b/rgrow/src/models/mod.rs @@ -6,4 +6,6 @@ pub mod ktam_fission; pub mod oldktam; pub mod oldktam_fission; +pub mod sdc1d; + pub mod covers; diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs new file mode 100644 index 0000000..5c884f7 --- /dev/null +++ b/rgrow/src/models/sdc1d.rs @@ -0,0 +1,331 @@ +/* +* TODO: +* - There are quite a few expects that need to be handled better +* +* */ + +use std::{ + collections::{HashMap, HashSet}, + usize, +}; + +use crate::{ + base::{Energy, Glue, Rate, Tile}, + canvas::PointSafe2, + state::State, + system::{Event, System, TileBondInfo}, +}; + +use ndarray::prelude::{Array1, Array2}; +use serde::{Deserialize, Serialize}; + +macro_rules! type_alias { + ($($t:ty => $($i:ident),*);* $(;)?) => { + $($(type $i = $t;)*)* + }; +} + +type_alias!( f64 => Strength, RatePerConc, Conc ); + +const WEST_GLUE_INDEX: usize = 0; +const BOTTOM_GLUE_INDEX: usize = 1; +const EAST_GLUE_INDEX: usize = 2; + +const U0: f64 = 1.0e9; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SDC { + pub strand_names: Vec, + pub glue_names: Vec, + + /// Colors of the scaffolds, strands can only stick if the + /// colors are a perfect match + /// + /// Note that this system will accept many scaffolds, thus this is a 2d array and not a 1d + /// array + pub scaffold: Array2, + /// All strands in the system, they are represented by tiles + /// with only glue on the south, west, and east (nothing can stuck to the top of a strand) + pub strands: Array1, + + pub strand_concentration: Array1, + /// Glues of a given strand by id + /// + /// Note that the glues will be sorted in the following manner: + /// [ + /// (0) -- [left glue, bottom glue, right glue] + /// ... + /// (n) -- [left glue, bottom glue, right glue] + /// ] + pub glues: Array2, + /// Binding strength between two glues + pub glue_links: Array2, + /// Each strand will be given a color so that it can be easily identified + /// when illustrated + pub colors: Vec<[u8; 4]>, + /// The (de)attachment rates will depend on this constant(for the system) value + pub kf: RatePerConc, + /// Constant G_se (TODO: Elaborate) + pub g_se: Energy, + pub alpha: Energy, + /// FIXME: Change this to a vector + /// + /// Set of tiles that can stick to scaffold gap with a given glue + /// + /// IMPROVEMENT: If the glue numbers are not random, meaning they are 0, 1, 2, 3, ..., n rather + /// than random unsigned integers, then we could make an array. If random unsigned integers are + /// used, that may result in a lot of empty space in the array. + pub friends_btm: HashMap>, + /// The energy with which two strands will bond + /// + /// This array is indexed as follows. Given strands x and y, where x is to the west of y + /// (meaning that the east of x forms a bond with the west of y), the energy of said bond + /// is given by energy_bonds[(x, y)] + energy_bonds: Array2, +} + +impl SDC { + fn update_system(&mut self) { + // Fill the energy array + self._make_energy_array(); + + // I dont think that we need to update the hasmap in this system, as it will never + // change + } + + fn _make_energy_array(&mut self) { + let num_of_strands = self.strands.len(); + + for strand_f in 0..(num_of_strands as usize) { + let (f_west_glue, f_east_glue) = { + let glues = self.glues.row(strand_f); + (glues[WEST_GLUE_INDEX], glues[EAST_GLUE_INDEX]) + }; + + for strand_s in 0..(num_of_strands as usize) { + let (s_west_glue, s_east_glue) = { + let glues = self.glues.row(strand_s); + (glues[WEST_GLUE_INDEX], glues[EAST_GLUE_INDEX]) + }; + + // Calculate the energy between the two strands + + // Case 1: First strands is to the west of second + // strand_f strand_s + self.energy_bonds[(strand_f, strand_s)] = + self.g_se * self.glue_links[(f_east_glue, s_west_glue)]; + // Case 2: First strands is to the east of second + // strand_s strand_f + self.energy_bonds[(strand_s, strand_f)] = + self.g_se * self.glue_links[(f_west_glue, s_east_glue)]; + } + } + } + + /// The detachment rate is given by + /// + /// TODO: Document the formula here + pub fn monomer_detachment_rate_at_point( + &self, + state: &S, + scaffol_point: PointSafe2, + ) -> Rate { + let strand = state.tile_at_point(scaffol_point); + + // There is no strand, thus nothing to be detached + if strand == 0 { + return 0.0; + } + + let bond_energy = self.bond_energy_of_strand(state, scaffol_point, strand); + self.kf * (U0 * (-bond_energy + self.alpha).exp()) + } + + /// x y z <- attached strands (potentially empty) + /// _ _ _ _ _ _ _ _ _ _ <- Scaffold + /// ^ point + /// + /// Should this function take in account the fact that monomers that have a good connection + /// with its neightbours will be more likely to attach ? Or are all attachements equally + /// likely here ? + fn _find_monomer_attachment_possibilities_at_point( + &self, + state: &S, + mut acc: Rate, + scaffold_coord: PointSafe2, + ) -> (bool, Rate, Event) { + let point = scaffold_coord.into(); + let tile = state.tile_at_point(point); + + // If the scaffold already has a strand binded, then nothing can attach to it + if tile != 0 { + return (false, acc, Event::None); + } + + let scaffold_glue = self.scaffold.get(point.0).expect("Invalid Index"); + let friends = match self.friends_btm.get(scaffold_glue) { + Some(hashset) => hashset, + None => todo!(), + }; + + for &strand in friends { + acc -= self.kf * self.strand_concentration[strand as usize]; + if acc <= 0.0 { + return (true, acc, Event::MonomerAttachment(point, strand)); + } + } + + (false, acc, Event::None) + } + + fn total_monomer_attachment_rate_at_poin( + &self, + state: &S, + scaffold_coord: PointSafe2, + ) -> f64 { + // If we set acc = 0, would it not be the case that we just attach to the first tile we can + // ? + match self._find_monomer_attachment_possibilities_at_point(state, 0.0, scaffold_coord) { + (false, acc, _) => -acc, + _ => panic!(), + } + } + + /// Get the sum of the energies of the bonded strands (if any) + fn bond_energy_of_strand( + &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.energy_bonds[(strand as usize, e)] + self.energy_bonds[(w, strand as usize)] + } +} + +impl System for SDC { + fn update_after_event(&self, state: &mut St, event: &crate::system::Event) { + todo!(); + } + + fn calc_n_tiles(&self, state: &St) -> crate::base::NumTiles { + todo!(); + } + + fn event_rate_at_point( + &self, + state: &St, + p: crate::canvas::PointSafeHere, + ) -> crate::base::Rate { + if !state.inbounds(p.0) { + return 0.0; + } + + let scaffold_coord = PointSafe2(p.0); + match state.tile_at_point(scaffold_coord) as u32 { + // Empty tile + 0 => self.monomer_detachment_rate_at_point(state, scaffold_coord), + // Full tile + _ => self.total_monomer_attachment_rate_at_poin(state, scaffold_coord), + } + } + + fn choose_event_at_point( + &self, + state: &St, + p: crate::canvas::PointSafe2, + acc: crate::base::Rate, + ) -> crate::system::Event { + todo!(); + } + + fn perform_event( + &self, + state: &mut St, + event: &crate::system::Event, + ) -> &Self { + match event { + // Cannot do nothing + Event::None => panic!("Being asked to perform null event."), + + // Attachments + Event::MonomerAttachment(point, tile) | Event::MonomerChange(point, tile) => { + state.set_sa(point, tile) + } + + Event::PolymerAttachment(v) | Event::PolymerChange(v) => { + v.iter().for_each(|(point, tile)| state.set_sa(point, tile)) + } + + // Detachments + Event::MonomerDetachment(point) => state.set_sa(point, &0), + Event::PolymerDetachment(vector) => { + for point in vector { + state.set_sa(point, &0); + } + } + }; + + state.add_events(1); + state.record_event(event); + self + } + + fn seed_locs(&self) -> Vec<(crate::canvas::PointSafe2, Tile)> { + panic!("This model does not contain seed tiles") + } + + fn calc_mismatch_locations(&self, state: &St) -> Array2 { + todo!() + } + + fn set_param( + &mut self, + _name: &str, + _value: Box, + ) -> Result { + todo!(); + } + + fn get_param(&self, name: &str) -> Result, crate::base::GrowError> { + todo!() + } + + fn system_info(&self) -> String { + format!( + "1 dimensional SDC with scaffold of len {} and {} strands", + self.scaffold.len(), + self.strands.len(), + ) + } +} + +impl TileBondInfo for SDC { + fn tile_color(&self, tile_number: Tile) -> [u8; 4] { + self.colors[tile_number as usize] + } + + fn tile_colors(&self) -> &Vec<[u8; 4]> { + &self.colors + } + + fn tile_name(&self, tile_number: Tile) -> &str { + self.strand_names[tile_number as usize].as_str() + } + + fn tile_names(&self) -> Vec<&str> { + self.strand_names.iter().map(|s| s.as_str()).collect() + } + + fn bond_name(&self, bond_number: usize) -> &str { + self.glue_names[bond_number].as_str() + } + + fn bond_names(&self) -> Vec<&str> { + self.glue_names.iter().map(|x| x.as_str()).collect() + } +} From 2e415edb712cb6a77834be747e183f870d084880 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:07:18 +0100 Subject: [PATCH 002/117] choose event at point started + comments / typos --- rgrow/src/models/sdc1d.rs | 68 +++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 5c884f7..5d234a1 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -1,7 +1,14 @@ /* +* Important Notes +* +* Given some PointSafe2, in this model, it will represnt two things +* 1. Which of the scaffolds has an event happening +* 2. In which position of the scaffold said event will take place +* * TODO: * - There are quite a few expects that need to be handled better -* +* - _find_monomer_attachment_possibilities_at_point is missing one parameter (because im unsure as +* to what it does) * */ use std::{ @@ -68,13 +75,9 @@ pub struct SDC { /// Constant G_se (TODO: Elaborate) pub g_se: Energy, pub alpha: Energy, - /// FIXME: Change this to a vector + /// FIXME: Change this to a vector to avoid hashing time /// /// Set of tiles that can stick to scaffold gap with a given glue - /// - /// IMPROVEMENT: If the glue numbers are not random, meaning they are 0, 1, 2, 3, ..., n rather - /// than random unsigned integers, then we could make an array. If random unsigned integers are - /// used, that may result in a lot of empty space in the array. pub friends_btm: HashMap>, /// The energy with which two strands will bond /// @@ -93,6 +96,7 @@ impl SDC { // change } + /// Fill the energy_bonds array fn _make_energy_array(&mut self) { let num_of_strands = self.strands.len(); @@ -128,27 +132,46 @@ impl SDC { pub fn monomer_detachment_rate_at_point( &self, state: &S, - scaffol_point: PointSafe2, + scaffold_point: PointSafe2, ) -> Rate { - let strand = state.tile_at_point(scaffol_point); + let strand = state.tile_at_point(scaffold_point); // There is no strand, thus nothing to be detached if strand == 0 { return 0.0; } - let bond_energy = self.bond_energy_of_strand(state, scaffol_point, strand); + let bond_energy = self.bond_energy_of_strand(state, scaffold_point, strand); self.kf * (U0 * (-bond_energy + self.alpha).exp()) } + pub fn choose_monomer_attachment_at_point( + &self, + state: &S, + point: PointSafe2, + acc: Rate, + ) -> (bool, Rate, Event) { + self.find_monomer_attachment_possibilities_at_point(state, acc, point) + } + + pub fn choose_monomer_detachment_at_point( + &self, + state: &S, + point: PointSafe2, + mut acc: Rate, + ) -> (bool, Rate, Event) { + acc -= self.monomer_detachment_rate_at_point(state, point); + + if acc > 0.0 { + return (false, acc, Event::None); + } + todo!() + } + /// x y z <- attached strands (potentially empty) /// _ _ _ _ _ _ _ _ _ _ <- Scaffold /// ^ point - /// - /// Should this function take in account the fact that monomers that have a good connection - /// with its neightbours will be more likely to attach ? Or are all attachements equally - /// likely here ? - fn _find_monomer_attachment_possibilities_at_point( + fn find_monomer_attachment_possibilities_at_point( &self, state: &S, mut acc: Rate, @@ -185,7 +208,7 @@ impl SDC { ) -> f64 { // If we set acc = 0, would it not be the case that we just attach to the first tile we can // ? - match self._find_monomer_attachment_possibilities_at_point(state, 0.0, scaffold_coord) { + match self.find_monomer_attachment_possibilities_at_point(state, 0.0, scaffold_coord) { (false, acc, _) => -acc, _ => panic!(), } @@ -237,10 +260,21 @@ impl System for SDC { fn choose_event_at_point( &self, state: &St, - p: crate::canvas::PointSafe2, + point: crate::canvas::PointSafe2, acc: crate::base::Rate, ) -> crate::system::Event { - todo!(); + // TODO: Missing choose monomer detachment + + match self.choose_monomer_attachment_at_point(state, point, acc) { + (true, _, event) => event, + (false, acc, _) => panic!( + "Rate: {:?}, {:?}, {:?}, {:?}", + acc, + point, + state, + state.raw_array() + ), + } } fn perform_event( From 57cb9e073649d85173670e1a1bd6e365e9ff409a Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:28:13 +0100 Subject: [PATCH 003/117] base implementation of sdc1d --- rgrow/src/models/sdc1d.rs | 214 ++++++++++++++++++++++++-------------- 1 file changed, 135 insertions(+), 79 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 5d234a1..11b7c99 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -1,3 +1,9 @@ +macro_rules! type_alias { + ($($t:ty => $($i:ident),*);* $(;)?) => { + $($(type $i = $t;)*)* + }; +} + /* * Important Notes * @@ -7,8 +13,9 @@ * * TODO: * - There are quite a few expects that need to be handled better -* - _find_monomer_attachment_possibilities_at_point is missing one parameter (because im unsure as +* - find_monomer_attachment_possibilities_at_point is missing one parameter (because im unsure as * to what it does) +* - Replace all use of index for glues to WEST_GLUE_INDEX ... * */ use std::{ @@ -17,21 +24,15 @@ use std::{ }; use crate::{ - base::{Energy, Glue, Rate, Tile}, - canvas::PointSafe2, + base::{Energy, Glue, GrowError, Rate, Tile}, + canvas::{PointSafe2, PointSafeHere}, state::State, - system::{Event, System, TileBondInfo}, + system::{Event, NeededUpdate, System, TileBondInfo}, }; use ndarray::prelude::{Array1, Array2}; use serde::{Deserialize, Serialize}; -macro_rules! type_alias { - ($($t:ty => $($i:ident),*);* $(;)?) => { - $($(type $i = $t;)*)* - }; -} - type_alias!( f64 => Strength, RatePerConc, Conc ); const WEST_GLUE_INDEX: usize = 0; @@ -42,9 +43,12 @@ const U0: f64 = 1.0e9; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SDC { + /// The anchor tiles for each of the scaffolds + /// + /// To get the anchor tile of the nth scaffold, anchor_tiles.get(n) + pub anchor_tiles: Vec<(PointSafe2, Tile)>, pub strand_names: Vec, pub glue_names: Vec, - /// Colors of the scaffolds, strands can only stick if the /// colors are a perfect match /// @@ -90,16 +94,44 @@ pub struct SDC { impl SDC { fn update_system(&mut self) { // Fill the energy array - self._make_energy_array(); + self.fill_energy_array(); - // I dont think that we need to update the hasmap in this system, as it will never - // change + // TODO: Do we also need to update friends here? + } + + fn polymer_update(&self, points: &Vec, state: &mut S) { + let mut points_to_update = points + .iter() + .flat_map(|&point| { + [ + PointSafeHere(point.0), + state.move_sa_w(point), + state.move_sa_e(point), + ] + }) + .collect::>(); + + points_to_update.sort_unstable(); + points_to_update.dedup(); + self.update_points(state, &points_to_update) + } + + fn update_monomer_point(&self, state: &mut S, scaffold_point: &PointSafe2) { + let points = [ + state.move_sa_w(*scaffold_point), + state.move_sa_e(*scaffold_point), + PointSafeHere(scaffold_point.0), + ] + .map(|point| (point, self.event_rate_at_point(state, point))); + + state.update_multiple(&points); } /// Fill the energy_bonds array - fn _make_energy_array(&mut self) { + fn fill_energy_array(&mut self) { let num_of_strands = self.strands.len(); + // For each *possible* pair of strands, calculate the energy bond for strand_f in 0..(num_of_strands as usize) { let (f_west_glue, f_east_glue) = { let glues = self.glues.row(strand_f); @@ -118,6 +150,7 @@ impl SDC { // strand_f strand_s self.energy_bonds[(strand_f, strand_s)] = self.g_se * self.glue_links[(f_east_glue, s_west_glue)]; + // Case 2: First strands is to the east of second // strand_s strand_f self.energy_bonds[(strand_s, strand_f)] = @@ -126,9 +159,6 @@ impl SDC { } } - /// The detachment rate is given by - /// - /// TODO: Document the formula here pub fn monomer_detachment_rate_at_point( &self, state: &S, @@ -136,8 +166,11 @@ impl SDC { ) -> Rate { let strand = state.tile_at_point(scaffold_point); + let anchor_tile = self.anchor_tiles[(scaffold_point.0).0]; + + // If we are trying to detach the anchor tile // There is no strand, thus nothing to be detached - if strand == 0 { + if strand == 0 || anchor_tile.0 == scaffold_point { return 0.0; } @@ -165,12 +198,15 @@ impl SDC { if acc > 0.0 { return (false, acc, Event::None); } - todo!() + + (true, acc, Event::MonomerDetachment(point)) } /// x y z <- attached strands (potentially empty) /// _ _ _ _ _ _ _ _ _ _ <- Scaffold /// ^ point + /// + /// TODO: Add just_calc parameter fn find_monomer_attachment_possibilities_at_point( &self, state: &S, @@ -186,10 +222,9 @@ impl SDC { } let scaffold_glue = self.scaffold.get(point.0).expect("Invalid Index"); - let friends = match self.friends_btm.get(scaffold_glue) { - Some(hashset) => hashset, - None => todo!(), - }; + + let empty_map = HashSet::default(); + let friends = self.friends_btm.get(scaffold_glue).unwrap_or(&empty_map); for &strand in friends { acc -= self.kf * self.strand_concentration[strand as usize]; @@ -232,11 +267,20 @@ impl SDC { impl System for SDC { fn update_after_event(&self, state: &mut St, event: &crate::system::Event) { - todo!(); - } - - fn calc_n_tiles(&self, state: &St) -> crate::base::NumTiles { - todo!(); + match event { + Event::None => todo!(), + Event::MonomerAttachment(scaffold_point, _) + | Event::MonomerDetachment(scaffold_point) + | Event::MonomerChange(scaffold_point, _) => { + // TODO: Make sure that this is all that needs be done for update + self.update_monomer_point(state, scaffold_point) + } + Event::PolymerDetachment(v) => self.polymer_update(v, state), + Event::PolymerAttachment(t) | Event::PolymerChange(t) => self.polymer_update( + &t.iter().map(|(p, _)| *p).collect::>(), + state, + ), + } } fn event_rate_at_point( @@ -250,10 +294,10 @@ impl System for SDC { let scaffold_coord = PointSafe2(p.0); match state.tile_at_point(scaffold_coord) as u32 { - // Empty tile - 0 => self.monomer_detachment_rate_at_point(state, scaffold_coord), - // Full tile - _ => self.total_monomer_attachment_rate_at_poin(state, scaffold_coord), + // If the tile is empty, we will return the rate at which attachment can occur + 0 => self.total_monomer_attachment_rate_at_poin(state, scaffold_coord), + // If the tile is full, we will return the rate at which detachment can occur + _ => self.monomer_detachment_rate_at_point(state, scaffold_coord), } } @@ -263,66 +307,78 @@ impl System for SDC { point: crate::canvas::PointSafe2, acc: crate::base::Rate, ) -> crate::system::Event { - // TODO: Missing choose monomer detachment - - match self.choose_monomer_attachment_at_point(state, point, acc) { + match self.choose_monomer_detachment_at_point(state, point, acc) { (true, _, event) => event, - (false, acc, _) => panic!( - "Rate: {:?}, {:?}, {:?}, {:?}", - acc, - point, - state, - state.raw_array() - ), + (false, acc, _) => match self.choose_monomer_attachment_at_point(state, point, acc) { + (true, _, event) => event, + (false, acc, _) => panic!( + "Rate: {:?}, {:?}, {:?}, {:?}", + acc, + point, + state, + state.raw_array() + ), + }, } } - fn perform_event( - &self, - state: &mut St, - event: &crate::system::Event, - ) -> &Self { - match event { - // Cannot do nothing - Event::None => panic!("Being asked to perform null event."), - - // Attachments - Event::MonomerAttachment(point, tile) | Event::MonomerChange(point, tile) => { - state.set_sa(point, tile) - } - - Event::PolymerAttachment(v) | Event::PolymerChange(v) => { - v.iter().for_each(|(point, tile)| state.set_sa(point, tile)) - } - - // Detachments - Event::MonomerDetachment(point) => state.set_sa(point, &0), - Event::PolymerDetachment(vector) => { - for point in vector { - state.set_sa(point, &0); - } - } - }; - - state.add_events(1); - state.record_event(event); - self - } - fn seed_locs(&self) -> Vec<(crate::canvas::PointSafe2, Tile)> { - panic!("This model does not contain seed tiles") + self.anchor_tiles.clone() } + // TODO: Array containing locations to "bad connections" fn calc_mismatch_locations(&self, state: &St) -> Array2 { todo!() } fn set_param( &mut self, - _name: &str, - _value: Box, + name: &str, + value: Box, ) -> Result { - todo!(); + match name { + "g_se" => { + let g_se = value + .downcast_ref::() + .ok_or(GrowError::WrongParameterType(name.to_string()))?; + self.g_se = *g_se; + self.update_system(); + Ok(NeededUpdate::NonZero) + } + "alpha" => { + let alpha = value + .downcast_ref::() + .ok_or(GrowError::WrongParameterType(name.to_string()))?; + self.alpha = *alpha; + self.update_system(); + Ok(NeededUpdate::NonZero) + } + "kf" => { + let kf = value + .downcast_ref::() + .ok_or(GrowError::WrongParameterType(name.to_string()))?; + self.kf = *kf; + self.update_system(); + Ok(NeededUpdate::NonZero) + } + "strand_concentrations" => { + let tile_concs = value + .downcast_ref::>() + .ok_or(GrowError::WrongParameterType(name.to_string()))?; + self.strand_concentration.clone_from(tile_concs); + self.update_system(); + Ok(NeededUpdate::NonZero) + } + "glue_links" => { + let glue_links = value + .downcast_ref::>() + .ok_or(GrowError::WrongParameterType(name.to_string()))?; + self.glue_links.clone_from(glue_links); + self.update_system(); + Ok(NeededUpdate::NonZero) + } + _ => Err(GrowError::NoParameter(name.to_string())), + } } fn get_param(&self, name: &str) -> Result, crate::base::GrowError> { From fc403c61c0e5779838f0814da608f85c964db464 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:37:15 +0100 Subject: [PATCH 004/117] get params function --- rgrow/src/models/sdc1d.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 11b7c99..0a40cb5 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -382,7 +382,14 @@ impl System for SDC { } fn get_param(&self, name: &str) -> Result, crate::base::GrowError> { - todo!() + match name { + "g_se" => Ok(Box::new(self.g_se)), + "alpha" => Ok(Box::new(self.alpha)), + "kf" => Ok(Box::new(self.kf)), + "strand_concentrations" => Ok(Box::new(self.strand_concentration.clone())), + "glue_links" => Ok(Box::new(self.glue_links.clone())), + _ => Err(GrowError::NoParameter(name.to_string())), + } } fn system_info(&self) -> String { From b1d8f3c7f478896aba1fea24354947ecec4bd844 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Thu, 13 Jun 2024 19:23:03 +0100 Subject: [PATCH 005/117] SDC model handling for TileSet (FromTileSet not done) --- rgrow/src/ffs.rs | 1 + rgrow/src/models/sdc1d.rs | 8 +++++++- rgrow/src/system.rs | 8 ++++++++ rgrow/src/tileset.rs | 4 ++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/rgrow/src/ffs.rs b/rgrow/src/ffs.rs index 24b9d8f..9fbf62c 100644 --- a/rgrow/src/ffs.rs +++ b/rgrow/src/ffs.rs @@ -279,6 +279,7 @@ impl TileSet { )?, )), (Model::ATAM, _, _) => Err(GrowError::FFSCannotRunATAM.into()), + (Model::SDC, _, _) => Err(GrowError::FFSCannotRunATAM.into()), // FIXME: generalize error (Model::OldKTAM, CanvasType::Square, TrackingType::None) => { Ok(Box::new(FFSRun::< QuadTreeState, diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 0a40cb5..bc9cc4b 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -27,7 +27,7 @@ use crate::{ base::{Energy, Glue, GrowError, Rate, Tile}, canvas::{PointSafe2, PointSafeHere}, state::State, - system::{Event, NeededUpdate, System, TileBondInfo}, + system::{Event, NeededUpdate, System, TileBondInfo}, tileset::FromTileSet, }; use ndarray::prelude::{Array1, Array2}; @@ -426,3 +426,9 @@ impl TileBondInfo for SDC { self.glue_names.iter().map(|x| x.as_str()).collect() } } + +impl FromTileSet for SDC { + fn from_tileset(tileset: &crate::tileset::TileSet) -> Result { + todo!() + } +} \ No newline at end of file diff --git a/rgrow/src/system.rs b/rgrow/src/system.rs index b3fc6e2..a9501bb 100644 --- a/rgrow/src/system.rs +++ b/rgrow/src/system.rs @@ -16,6 +16,7 @@ use crate::models::atam::ATAM; use crate::models::ktam::KTAM; use crate::models::oldktam::OldKTAM; +use crate::models::sdc1d::SDC; use crate::state::NullStateTracker; use crate::state::QuadTreeState; use crate::state::State; @@ -759,6 +760,7 @@ pub enum SystemEnum { KTAM, OldKTAM, ATAM, + SDC // StaticKTAMCover } @@ -768,6 +770,12 @@ pub trait SystemWithDimers { fn calc_dimers(&self) -> Vec; } +impl SystemWithDimers for SDC { + fn calc_dimers(&self) -> Vec { + panic!("Not implemented") + } +} + #[enum_dispatch] pub trait TileBondInfo { fn tile_color(&self, tile_number: Tile) -> [u8; 4]; diff --git a/rgrow/src/tileset.rs b/rgrow/src/tileset.rs index f6b06a0..a106282 100644 --- a/rgrow/src/tileset.rs +++ b/rgrow/src/tileset.rs @@ -5,6 +5,7 @@ use crate::colors::get_color_or_random; use crate::models::atam::ATAM; use crate::models::ktam::KTAM; use crate::models::oldktam::OldKTAM; +use crate::models::sdc1d::SDC; use crate::state::{NullStateTracker, QuadTreeState, StateWithCreate}; use crate::system::{DynSystem, EvolveBounds}; @@ -549,6 +550,8 @@ pub enum Model { ATAM, #[serde(alias = "OldkTAM", alias = "oldktam")] OldKTAM, + #[serde(alias = "SDC1D", alias = "sdc1d")] + SDC } use std::convert::TryFrom; @@ -622,6 +625,7 @@ impl TileSet { Model::KTAM => SystemEnum::KTAM(KTAM::from_tileset(self)?), Model::ATAM => SystemEnum::ATAM(ATAM::from_tileset(self)?), Model::OldKTAM => SystemEnum::OldKTAM(OldKTAM::from_tileset(self)?), + Model::SDC => SystemEnum::SDC(SDC::from_tileset(self)?), }) } From 75a3f647c0bc265ccfa060480a47a0941c495632 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 14 Jun 2024 22:43:30 +0100 Subject: [PATCH 006/117] rudimentary tileset->sdc implementation --- rgrow/src/models/sdc1d.rs | 67 +++++++++++++++++++++++++++++++++++---- rgrow/src/tileset.rs | 3 +- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index bc9cc4b..66b459f 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -27,7 +27,8 @@ use crate::{ base::{Energy, Glue, GrowError, Rate, Tile}, canvas::{PointSafe2, PointSafeHere}, state::State, - system::{Event, NeededUpdate, System, TileBondInfo}, tileset::FromTileSet, + system::{Event, NeededUpdate, System, TileBondInfo}, + tileset::{FromTileSet, ProcessedTileSet, Size}, }; use ndarray::prelude::{Array1, Array2}; @@ -57,8 +58,7 @@ pub struct SDC { pub scaffold: Array2, /// All strands in the system, they are represented by tiles /// with only glue on the south, west, and east (nothing can stuck to the top of a strand) - pub strands: Array1, - + // pub strands: Array1, pub strand_concentration: Array1, /// Glues of a given strand by id /// @@ -129,7 +129,7 @@ impl SDC { /// Fill the energy_bonds array fn fill_energy_array(&mut self) { - let num_of_strands = self.strands.len(); + let num_of_strands = self.strand_names.len(); // For each *possible* pair of strands, calculate the energy bond for strand_f in 0..(num_of_strands as usize) { @@ -396,7 +396,7 @@ impl System for SDC { format!( "1 dimensional SDC with scaffold of len {} and {} strands", self.scaffold.len(), - self.strands.len(), + self.strand_names.len(), ) } } @@ -429,6 +429,59 @@ impl TileBondInfo for SDC { impl FromTileSet for SDC { fn from_tileset(tileset: &crate::tileset::TileSet) -> Result { - todo!() + // This gives us parsed names / etc for tiles and glues. It makes some wrong assumptions (like + // that each tile has four edges), but it will do for now. + let pc = ProcessedTileSet::from_tileset(tileset)?; + + // Combine glue strengths (between like numbers) and glue links (between two numbers) + let n_glues = pc.glue_strengths.len(); + let mut glue_links = Array2::zeros((n_glues, n_glues)); + for (i, strength) in pc.glue_strengths.indexed_iter() { + glue_links[(i, i)] = *strength; + } + for (i, j, strength) in pc.gluelinks.iter() { + glue_links[(*i, *j)] = *strength; + } + + // Just generate the stuff that will be filled by the model. + let energy_bonds = Array2::::zeros((pc.tile_names.len(), pc.tile_names.len())); + let friends_btm = HashMap::new(); + + // We'll default to 64 scaffolds. + let (n_scaffolds, scaffold_length) = match tileset.size { + Some(Size::Single(x)) => (64, x), + Some(Size::Pair((j, x))) => (j, x), + None => panic!("Size not specified for SDC model.") + }; + + // The tileset input doesn't have a way to specify scaffolds right now. This generates a buch of 'fake' scaffolds + // each with just glues 0 to scaffold_length, which we can at least play around with. + let mut scaffold = Array2::::zeros((n_scaffolds, scaffold_length)); + for ((i, j), v) in scaffold.indexed_iter_mut() { + *v = j; + } + + let alpha = tileset.alpha.unwrap_or(0.0); + + // We'll set strand concentrations using stoic and the traditional kTAM Gmc, where + // conc = stoic * u0 * exp(-Gmc + alpha) and u0 = 1M, but we really should just have + // a way to specify concentrations directly. + let strand_concentration = pc.tile_stoics.mapv(|x| x * (-tileset.gmc.unwrap_or(16.0) + alpha).exp()); + + Ok(SDC { + strand_names: pc.tile_names, + glue_names: pc.glue_names, + glue_links, + colors: pc.tile_colors, + glues: pc.tile_edges, + anchor_tiles: Vec::new(), + scaffold, + strand_concentration, + kf: tileset.kf.unwrap_or(1.0e6), + g_se: tileset.gse.unwrap_or(5.0), + alpha, + friends_btm, + energy_bonds, + }) } -} \ No newline at end of file +} diff --git a/rgrow/src/tileset.rs b/rgrow/src/tileset.rs index a106282..c0ad790 100644 --- a/rgrow/src/tileset.rs +++ b/rgrow/src/tileset.rs @@ -564,6 +564,7 @@ impl TryFrom<&str> for Model { "ktam" => Ok(Model::KTAM), "atam" => Ok(Model::ATAM), "oldktam" => Ok(Model::OldKTAM), + "sdc1d" => Ok(Model::SDC), _ => Err(StringConvError(format!( "Unknown model {}. Valid options are kTAM, aTAM, and oldkTAM.", s @@ -964,7 +965,7 @@ impl ProcessedTileSet { tile_stoics: Array1::from_vec(tile_stoics), tile_names, tile_colors, - glue_names: Vec::new(), + glue_names: Vec::new(), // FIXME glue_strengths, has_duples, glue_map, From 2eefd20d960e022dd413b59c478f77cd39c762ba Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 14 Jun 2024 22:47:36 +0100 Subject: [PATCH 007/117] fix inadvertent doctest activation --- rgrow/src/models/sdc1d.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 66b459f..bde6187 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -202,9 +202,9 @@ impl SDC { (true, acc, Event::MonomerDetachment(point)) } - /// x y z <- attached strands (potentially empty) - /// _ _ _ _ _ _ _ _ _ _ <- Scaffold - /// ^ point + /// | x y z <- attached strands (potentially empty) + /// |_ _ _ _ _ _ _ _ _ _ <- Scaffold + /// | ^ point /// /// TODO: Add just_calc parameter fn find_monomer_attachment_possibilities_at_point( From f7bdf05962334a92888e1f7d676958aef6b166b3 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 14 Jun 2024 22:53:58 +0100 Subject: [PATCH 008/117] fix gluelinks reference after merge --- rgrow/src/models/sdc1d.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index bde6187..131e3ad 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -439,7 +439,7 @@ impl FromTileSet for SDC { for (i, strength) in pc.glue_strengths.indexed_iter() { glue_links[(i, i)] = *strength; } - for (i, j, strength) in pc.gluelinks.iter() { + for (i, j, strength) in pc.glue_links.iter() { glue_links[(*i, *j)] = *strength; } From 46c64e33fba203c5dbabc2a109dde23dc847aa8d Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 14 Jun 2024 23:02:27 +0100 Subject: [PATCH 009/117] allow energy_bonds viewing from python --- rgrow/src/models/sdc1d.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 131e3ad..1235097 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -388,6 +388,7 @@ impl System for SDC { "kf" => Ok(Box::new(self.kf)), "strand_concentrations" => Ok(Box::new(self.strand_concentration.clone())), "glue_links" => Ok(Box::new(self.glue_links.clone())), + "energy_bonds" => Ok(Box::new(self.energy_bonds.clone())), _ => Err(GrowError::NoParameter(name.to_string())), } } From ae5857b7dc9eafbbbd1bb7c1ee792e7810b2134c Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Sat, 15 Jun 2024 16:52:02 +0100 Subject: [PATCH 010/117] add missing just_calc parameter --- rgrow/src/models/sdc1d.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 1235097..23abac9 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -24,7 +24,7 @@ use std::{ }; use crate::{ - base::{Energy, Glue, GrowError, Rate, Tile}, + base::{Energy, Glue, GrowError, Rate,false Tile}, canvas::{PointSafe2, PointSafeHere}, state::State, system::{Event, NeededUpdate, System, TileBondInfo}, @@ -184,7 +184,7 @@ impl SDC { point: PointSafe2, acc: Rate, ) -> (bool, Rate, Event) { - self.find_monomer_attachment_possibilities_at_point(state, acc, point) + self.find_monomer_attachment_possibilities_at_point(state, acc, point, false) } pub fn choose_monomer_detachment_at_point( @@ -206,12 +206,12 @@ impl SDC { /// |_ _ _ _ _ _ _ _ _ _ <- Scaffold /// | ^ point /// - /// TODO: Add just_calc parameter fn find_monomer_attachment_possibilities_at_point( &self, state: &S, mut acc: Rate, scaffold_coord: PointSafe2, + just_calc: bool, ) -> (bool, Rate, Event) { let point = scaffold_coord.into(); let tile = state.tile_at_point(point); @@ -228,7 +228,7 @@ impl SDC { for &strand in friends { acc -= self.kf * self.strand_concentration[strand as usize]; - if acc <= 0.0 { + if acc <= 0.0 && (!just_calc) { return (true, acc, Event::MonomerAttachment(point, strand)); } } @@ -243,7 +243,8 @@ impl SDC { ) -> f64 { // If we set acc = 0, would it not be the case that we just attach to the first tile we can // ? - match self.find_monomer_attachment_possibilities_at_point(state, 0.0, scaffold_coord) { + match self.find_monomer_attachment_possibilities_at_point(state, 0.0, scaffold_coord, true) + { (false, acc, _) => -acc, _ => panic!(), } @@ -452,7 +453,7 @@ impl FromTileSet for SDC { let (n_scaffolds, scaffold_length) = match tileset.size { Some(Size::Single(x)) => (64, x), Some(Size::Pair((j, x))) => (j, x), - None => panic!("Size not specified for SDC model.") + None => panic!("Size not specified for SDC model."), }; // The tileset input doesn't have a way to specify scaffolds right now. This generates a buch of 'fake' scaffolds @@ -467,7 +468,9 @@ impl FromTileSet for SDC { // We'll set strand concentrations using stoic and the traditional kTAM Gmc, where // conc = stoic * u0 * exp(-Gmc + alpha) and u0 = 1M, but we really should just have // a way to specify concentrations directly. - let strand_concentration = pc.tile_stoics.mapv(|x| x * (-tileset.gmc.unwrap_or(16.0) + alpha).exp()); + let strand_concentration = pc + .tile_stoics + .mapv(|x| x * (-tileset.gmc.unwrap_or(16.0) + alpha).exp()); Ok(SDC { strand_names: pc.tile_names, From fe399f640cb662b6a946d9eb9e1129a9045c31fc Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Sat, 15 Jun 2024 16:53:55 +0100 Subject: [PATCH 011/117] typo, cargo fmt --- rgrow/src/models/sdc1d.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 23abac9..386cd9f 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -24,7 +24,7 @@ use std::{ }; use crate::{ - base::{Energy, Glue, GrowError, Rate,false Tile}, + base::{Energy, Glue, GrowError, Rate, Tile}, canvas::{PointSafe2, PointSafeHere}, state::State, system::{Event, NeededUpdate, System, TileBondInfo}, From c4646d992afc36aed6cfd862146c0c118033d3ab Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Sun, 16 Jun 2024 15:27:22 +0100 Subject: [PATCH 012/117] Fill friends, disable anchor tiles for now. --- rgrow/src/models/sdc1d.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 386cd9f..a832da5 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -447,7 +447,6 @@ impl FromTileSet for SDC { // Just generate the stuff that will be filled by the model. let energy_bonds = Array2::::zeros((pc.tile_names.len(), pc.tile_names.len())); - let friends_btm = HashMap::new(); // We'll default to 64 scaffolds. let (n_scaffolds, scaffold_length) = match tileset.size { @@ -472,7 +471,12 @@ impl FromTileSet for SDC { .tile_stoics .mapv(|x| x * (-tileset.gmc.unwrap_or(16.0) + alpha).exp()); - Ok(SDC { + let mut friends_btm = HashMap::new(); + for (t, &b) in pc.tile_edges.index_axis(ndarray::Axis(1), BOTTOM_GLUE_INDEX).indexed_iter() { + friends_btm.entry(b).or_insert(HashSet::new()).insert(t as u32); + } + + let mut sys = SDC { strand_names: pc.tile_names, glue_names: pc.glue_names, glue_links, @@ -486,6 +490,10 @@ impl FromTileSet for SDC { alpha, friends_btm, energy_bonds, - }) + }; + + sys.update_system(); + + Ok(sys) } } From 80c362a87645191d6f7dd05e482dc5cb0fd6be50 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Sun, 16 Jun 2024 15:31:29 +0100 Subject: [PATCH 013/117] rust Debug info in python for system and state --- py-rgrow/rgrow/rgrow.pyi | 6 ++++++ rgrow/src/python.rs | 8 ++++++++ rgrow/src/system.rs | 1 + 3 files changed, 15 insertions(+) diff --git a/py-rgrow/rgrow/rgrow.pyi b/py-rgrow/rgrow/rgrow.pyi index 56be76a..6a4edac 100644 --- a/py-rgrow/rgrow/rgrow.pyi +++ b/py-rgrow/rgrow/rgrow.pyi @@ -27,6 +27,9 @@ class State(object): def time(self) -> float: ... @property def total_events(self) -> int: ... + def print_debug(self) -> None: + "Print rust Debug string for the state object." + ... class System(object): @overload @@ -80,6 +83,9 @@ class System(object): ) -> "Axes": ... def get_param(self, name: str) -> Any: ... def set_param(self, name: str, value: Any): ... + def print_debug(self) -> None: + "Print rust Debug string for the system object." + ... class FissionHandling(object): ... class CanvasType(object): ... diff --git a/rgrow/src/python.rs b/rgrow/src/python.rs index c5ae351..fa2cceb 100644 --- a/rgrow/src/python.rs +++ b/rgrow/src/python.rs @@ -96,6 +96,10 @@ impl PyState { self.0.total_rate() ) } + + pub fn print_debug(&self) { + println!("{:?}", self.0); + } } #[cfg(feature = "python")] @@ -333,4 +337,8 @@ impl PySystem { fn __repr__(&self) -> String { format!("System({})", self.0.system_info()) } + + pub fn print_debug(&self) { + println!("{:?}", self.0); + } } diff --git a/rgrow/src/system.rs b/rgrow/src/system.rs index 276a027..7200b7b 100644 --- a/rgrow/src/system.rs +++ b/rgrow/src/system.rs @@ -766,6 +766,7 @@ impl DynSystem for S { } #[enum_dispatch(DynSystem, TileBondInfo, SystemWithDimers)] +#[derive(Debug, Clone)] pub enum SystemEnum { KTAM, OldKTAM, From 31d12b5c16593b53a6acc79a360f1adb1122c7cb Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Sun, 16 Jun 2024 15:31:41 +0100 Subject: [PATCH 014/117] Actually disable anchor tiles for now --- rgrow/src/models/sdc1d.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index a832da5..dff8092 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -166,11 +166,11 @@ impl SDC { ) -> Rate { let strand = state.tile_at_point(scaffold_point); - let anchor_tile = self.anchor_tiles[(scaffold_point.0).0]; + // let anchor_tile = self.anchor_tiles[(scaffold_point.0).0]; // FIXME: disabled anchor tiles for now // If we are trying to detach the anchor tile // There is no strand, thus nothing to be detached - if strand == 0 || anchor_tile.0 == scaffold_point { + if strand == 0 /*|| anchor_tile.0 == scaffold_point */{ // FIXME: disabled anchor tiles for now return 0.0; } From 62c75416fe04b5cadcf39e2e6e8d3981a3c0234b Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Mon, 17 Jun 2024 19:15:39 +0100 Subject: [PATCH 015/117] SDC: change away from g_se & alpha --- rgrow/src/models/sdc1d.rs | 40 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index dff8092..9cdef56 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -149,12 +149,12 @@ impl SDC { // Case 1: First strands is to the west of second // strand_f strand_s self.energy_bonds[(strand_f, strand_s)] = - self.g_se * self.glue_links[(f_east_glue, s_west_glue)]; + -self.glue_links[(f_east_glue, s_west_glue)]; // Case 2: First strands is to the east of second // strand_s strand_f self.energy_bonds[(strand_s, strand_f)] = - self.g_se * self.glue_links[(f_west_glue, s_east_glue)]; + -self.glue_links[(f_west_glue, s_east_glue)]; } } } @@ -170,12 +170,15 @@ impl SDC { // If we are trying to detach the anchor tile // There is no strand, thus nothing to be detached - if strand == 0 /*|| anchor_tile.0 == scaffold_point */{ // FIXME: disabled anchor tiles for now + if strand == 0 + /*|| anchor_tile.0 == scaffold_point */ + { + // FIXME: disabled anchor tiles for now return 0.0; } let bond_energy = self.bond_energy_of_strand(state, scaffold_point, strand); - self.kf * (U0 * (-bond_energy + self.alpha).exp()) + self.kf * bond_energy.exp() } pub fn choose_monomer_attachment_at_point( @@ -338,22 +341,6 @@ impl System for SDC { value: Box, ) -> Result { match name { - "g_se" => { - let g_se = value - .downcast_ref::() - .ok_or(GrowError::WrongParameterType(name.to_string()))?; - self.g_se = *g_se; - self.update_system(); - Ok(NeededUpdate::NonZero) - } - "alpha" => { - let alpha = value - .downcast_ref::() - .ok_or(GrowError::WrongParameterType(name.to_string()))?; - self.alpha = *alpha; - self.update_system(); - Ok(NeededUpdate::NonZero) - } "kf" => { let kf = value .downcast_ref::() @@ -384,8 +371,6 @@ impl System for SDC { fn get_param(&self, name: &str) -> Result, crate::base::GrowError> { match name { - "g_se" => Ok(Box::new(self.g_se)), - "alpha" => Ok(Box::new(self.alpha)), "kf" => Ok(Box::new(self.kf)), "strand_concentrations" => Ok(Box::new(self.strand_concentration.clone())), "glue_links" => Ok(Box::new(self.glue_links.clone())), @@ -472,8 +457,15 @@ impl FromTileSet for SDC { .mapv(|x| x * (-tileset.gmc.unwrap_or(16.0) + alpha).exp()); let mut friends_btm = HashMap::new(); - for (t, &b) in pc.tile_edges.index_axis(ndarray::Axis(1), BOTTOM_GLUE_INDEX).indexed_iter() { - friends_btm.entry(b).or_insert(HashSet::new()).insert(t as u32); + for (t, &b) in pc + .tile_edges + .index_axis(ndarray::Axis(1), BOTTOM_GLUE_INDEX) + .indexed_iter() + { + friends_btm + .entry(b) + .or_insert(HashSet::new()) + .insert(t as u32); } let mut sys = SDC { From ec2d890b6def4bddd4193826d7ddeb89e04cb923 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Tue, 18 Jun 2024 00:07:13 +0100 Subject: [PATCH 016/117] SDC: remove g_se and alpha from struct --- rgrow/src/models/sdc1d.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 9cdef56..2c5cd62 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -40,8 +40,6 @@ const WEST_GLUE_INDEX: usize = 0; const BOTTOM_GLUE_INDEX: usize = 1; const EAST_GLUE_INDEX: usize = 2; -const U0: f64 = 1.0e9; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SDC { /// The anchor tiles for each of the scaffolds @@ -76,9 +74,6 @@ pub struct SDC { pub colors: Vec<[u8; 4]>, /// The (de)attachment rates will depend on this constant(for the system) value pub kf: RatePerConc, - /// Constant G_se (TODO: Elaborate) - pub g_se: Energy, - pub alpha: Energy, /// FIXME: Change this to a vector to avoid hashing time /// /// Set of tiles that can stick to scaffold gap with a given glue @@ -478,8 +473,6 @@ impl FromTileSet for SDC { scaffold, strand_concentration, kf: tileset.kf.unwrap_or(1.0e6), - g_se: tileset.gse.unwrap_or(5.0), - alpha, friends_btm, energy_bonds, }; From 2bfaaac3ef7ddec0ff88a3dc54bf51bb20bd171b Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Tue, 18 Jun 2024 02:11:03 +0100 Subject: [PATCH 017/117] silliness --- rgrow/src/models/sdc1d.rs | 151 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 2c5cd62..621700a 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -482,3 +482,154 @@ impl FromTileSet for SDC { Ok(sys) } } + + +// Here is potentially another way to process this, though not done. Feel free to delete or modify. + +use std::hash::Hash; + + +use bimap::BiHashMap; + +#[cfg(python)] +use pyo3::prelude::*; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(python, derive(FromPyObject))] +pub enum RefOrPair { + Ref(String), + Pair(String, String), +} + +impl From 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(python, derive(FromPyObject))] +pub enum SingleOrMultiScaffold { + Single(Vec), + Multi(Vec>), +} + +impl From> for SingleOrMultiScaffold { + fn from(v: Vec) -> Self { + SingleOrMultiScaffold::Single(v) + } +} + +impl From>> for SingleOrMultiScaffold { + fn from(v: Vec>) -> Self { + SingleOrMultiScaffold::Multi(v) + } +} + +#[derive(Debug)] +#[cfg_attr(python, derive(FromPyObject))] +pub struct SDCParams { + pub tile_glues: Vec>>, + pub tile_concentration: Vec, + pub tile_names: Vec>, + pub tile_colors: Vec>, + pub scaffold: SingleOrMultiScaffold, + pub glue_h_s: HashMap, + pub k_f: f64, + pub k_n: f64, + pub k_c: f64, + pub temperature: f64, +} + +fn comp(a: &str) -> String { + if a.ends_with('*') { + a.trim_end_matches('*').to_string() + } else { + format!("{}*", a) + } +} + +fn base(a: &str) -> &str { + a.trim_end_matches('*') +} + +impl SDC { + pub fn from_params(params: SDCParams) -> Self { + let mut glue_name_map = BiHashMap::new(); + + let mut gluenum = 1; + let mut max_gluenum = 1; + + let mut tile_glues_int = Array2::::zeros((params.tile_glues.len(), 3)); + + for (tgl, mut r) in std::iter::zip(params.tile_glues.iter(), tile_glues_int.outer_iter_mut()) { + for (i, t) in tgl.iter().enumerate() { + match t { + None => { + r[i] = 0; + } + Some(s) => { + let j = glue_name_map.get_by_left(s); + match j { + Some(j) => { + r[i] = *j; + } + None => { + glue_name_map.insert(base(s).to_string(), gluenum); + glue_name_map.insert(format!("{}*", base(s)), gluenum + 1); + r[i] = *glue_name_map.get_by_left(s).unwrap(); // FIXME: will fail if ** is in name, and is inefficient + gluenum += 2; + max_gluenum = max_gluenum.max(gluenum); + } + } + } + } + } + } + + let mut glue_h = Array2::::zeros((max_gluenum, max_gluenum)); + let mut glue_s = Array2::::zeros((max_gluenum, max_gluenum)); + + for (k, &v) in params.glue_h_s.iter() { + match k { + RefOrPair::Ref(r) => { + let i = *glue_name_map.get_by_left(&comp(r)).unwrap(); // FIXME: fails if glue not found + let j = *glue_name_map.get_by_left(base(r)).unwrap(); // FIXME: fails if glue not found + glue_h[[i, j]] = v.0; + glue_s[[i, j]] = v.1; + glue_h[[j, i]] = v.0; + glue_s[[j, i]] = v.1; + + }, + RefOrPair::Pair(r1, r2) => { + let i = *glue_name_map.get_by_left(r1).unwrap(); // FIXME: fails if glue not found + let j = *glue_name_map.get_by_left(r2).unwrap(); // FIXME: fails if glue not found + glue_h[[i, j]] = v.0; + glue_s[[i, j]] = v.1; + glue_h[[j, i]] = v.0; + glue_s[[j, i]] = v.1; + }, + } + }; + + SDC { + anchor_tiles: Vec::new(), + strand_names: params.tile_names.iter().map(|x| x.clone().unwrap_or("".to_string())).collect(), + glue_names: todo!(), + scaffold: todo!(), + strand_concentration: Array1::from(params.tile_concentration), + glues: tile_glues_int, + glue_links: glue_h - params.temperature * glue_s, + colors: Vec::new(), + kf: params.k_f, + friends_btm: todo!(), + energy_bonds: todo!(), + } + } +} From 6701985ff96242503218abcfb45113b929a0bb64 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:27:34 +0100 Subject: [PATCH 018/117] update_system function improved + tested | cargo fmt --- rgrow/src/models/sdc1d.rs | 153 ++++++++++++++++++++++++++++++-------- 1 file changed, 123 insertions(+), 30 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 621700a..07c8006 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -67,8 +67,6 @@ pub struct SDC { /// (n) -- [left glue, bottom glue, right glue] /// ] pub glues: Array2, - /// Binding strength between two glues - pub glue_links: Array2, /// Each strand will be given a color so that it can be easily identified /// when illustrated pub colors: Vec<[u8; 4]>, @@ -78,20 +76,56 @@ pub struct SDC { /// /// Set of tiles that can stick to scaffold gap with a given glue pub friends_btm: HashMap>, + /// H in the formula to genereate the glue strengths + pub enthalpy_matrix: Array2, + /// S in the formula to geenrate the glue strengths + pub entropy_matrix: Array2, + /// Temperature of the system in kelvin + temperature: f64, /// The energy with which two strands will bond /// /// This array is indexed as follows. Given strands x and y, where x is to the west of y /// (meaning that the east of x forms a bond with the west of y), the energy of said bond /// is given by energy_bonds[(x, y)] energy_bonds: Array2, + /// Binding strength between two glues + glue_links: Array2, } impl SDC { fn update_system(&mut self) { - // Fill the energy array + // Note that order is important, we need to generate the glue matrix first, then using + // the data generated there, the energy array is filled, etc... + self.generate_glue_matrix(); self.fill_energy_array(); + self.generate_friends(); + } - // TODO: Do we also need to update friends here? + fn generate_friends(&mut self) { + let mut friends_btm = HashMap::new(); + for (t, &b) in self + .glues + .index_axis(ndarray::Axis(1), BOTTOM_GLUE_INDEX) + .indexed_iter() + { + friends_btm + .entry(b) + .or_insert(HashSet::new()) + .insert(t as u32); + } + self.friends_btm = friends_btm; + } + + /// The strenght of glues a, b is given by: + /// + /// G(a, b) = H(a,b) - T * S(a, b) + fn generate_glue_matrix(&mut self) { + self.glue_links = &self.enthalpy_matrix - self.temperature * &self.entropy_matrix; + } + + pub fn change_temperature_to(&mut self, kelvin: f64) { + self.temperature = kelvin; + self.update_system(); } fn polymer_update(&self, points: &Vec, state: &mut S) { @@ -451,18 +485,6 @@ impl FromTileSet for SDC { .tile_stoics .mapv(|x| x * (-tileset.gmc.unwrap_or(16.0) + alpha).exp()); - let mut friends_btm = HashMap::new(); - for (t, &b) in pc - .tile_edges - .index_axis(ndarray::Axis(1), BOTTOM_GLUE_INDEX) - .indexed_iter() - { - friends_btm - .entry(b) - .or_insert(HashSet::new()) - .insert(t as u32); - } - let mut sys = SDC { strand_names: pc.tile_names, glue_names: pc.glue_names, @@ -473,22 +495,24 @@ impl FromTileSet for SDC { scaffold, strand_concentration, kf: tileset.kf.unwrap_or(1.0e6), - friends_btm, + enthalpy_matrix: todo!(), + entropy_matrix: todo!(), + temperature: todo!(), + friends_btm: HashMap::new(), energy_bonds, }; + // This will generate the friends hashamp, as well as the glues, and the energy bonds sys.update_system(); Ok(sys) } } - // Here is potentially another way to process this, though not done. Feel free to delete or modify. use std::hash::Hash; - use bimap::BiHashMap; #[cfg(python)] @@ -568,7 +592,9 @@ impl SDC { let mut tile_glues_int = Array2::::zeros((params.tile_glues.len(), 3)); - for (tgl, mut r) in std::iter::zip(params.tile_glues.iter(), tile_glues_int.outer_iter_mut()) { + for (tgl, mut r) in + std::iter::zip(params.tile_glues.iter(), tile_glues_int.outer_iter_mut()) + { for (i, t) in tgl.iter().enumerate() { match t { None => { @@ -605,8 +631,7 @@ impl SDC { glue_s[[i, j]] = v.1; glue_h[[j, i]] = v.0; glue_s[[j, i]] = v.1; - - }, + } RefOrPair::Pair(r1, r2) => { let i = *glue_name_map.get_by_left(r1).unwrap(); // FIXME: fails if glue not found let j = *glue_name_map.get_by_left(r2).unwrap(); // FIXME: fails if glue not found @@ -614,22 +639,90 @@ impl SDC { glue_s[[i, j]] = v.1; glue_h[[j, i]] = v.0; glue_s[[j, i]] = v.1; - }, + } } - }; + } - SDC { + let mut sdc = SDC { anchor_tiles: Vec::new(), - strand_names: params.tile_names.iter().map(|x| x.clone().unwrap_or("".to_string())).collect(), + strand_names: params + .tile_names + .iter() + .map(|x| x.clone().unwrap_or("".to_string())) + .collect(), glue_names: todo!(), scaffold: todo!(), + enthalpy_matrix: glue_h, + entropy_matrix: glue_s, + temperature: params.temperature, strand_concentration: Array1::from(params.tile_concentration), glues: tile_glues_int, - glue_links: glue_h - params.temperature * glue_s, colors: Vec::new(), kf: params.k_f, - friends_btm: todo!(), - energy_bonds: todo!(), - } + // The ones below should be generated by the update system function + // + // It may be cleaner to have a "new" function that takes just what is needed as a + // param and generates the rest + glue_links: glue_h - params.temperature * glue_s, + friends_btm: HashMap::new(), + energy_bonds: Array2::::zeros((params.tile_names.len(), params.tile_names.len())), + }; + + sdc.update_system(); + sdc + } +} + +#[cfg(test)] +mod test_sdc_model { + use ndarray::array; + + use super::*; + #[test] + fn test_update_system() { + // a lot of the parameters here make no sense, but they wont be used in the tests so it + // doesnt matter + let mut sdc = SDC { + anchor_tiles: Vec::new(), + strand_names: Vec::new(), + glue_names: Vec::new(), + scaffold: Array2::::zeros((5, 5)), + strand_concentration: Array1::::zeros(5), + glues: array![ + [0, 0, 0], + [1, 3, 12], + [6, 8, 12], + [31, 3, 45], + [8, 4, 2], + [1, 1, 78], + [4, 8, 1], + ], + colors: Vec::new(), + kf: 0.0, + friends_btm: HashMap::new(), + entropy_matrix: array![[1., 2., 3.], [5., 1., 8.], [5., -2., 12.]], + enthalpy_matrix: array![[4., 1., -8.], [6., 1., 14.], [12., 21., -13.,]], + temperature: 5., + energy_bonds: Array2::::zeros((5, 5)), + glue_links: Array2::::zeros((5, 5)), + }; + + sdc.update_system(); + + // Check that the glue matrix is being generated as expected + let expeced_glue_matrix = array![[-1.0, -9., -23.], [-19., -4., -26.], [-13., 31., -73.]]; + assert_eq!(expeced_glue_matrix, sdc.glue_links); + + // TODO Check that the energy bonds are being generated as expected + + // Check that the friends hashmap is being generated as expected + let expected_friends = HashMap::from([ + (0, HashSet::from([0])), + (1, HashSet::from([5])), + (3, HashSet::from([1, 3])), + (4, HashSet::from([4])), + (8, HashSet::from([2, 6])), + ]); + assert_eq!(expected_friends, sdc.friends_btm); } } From ee45549cf5ec6ff29b2f336c2e63b85f9dd4494d Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Tue, 18 Jun 2024 14:23:15 +0100 Subject: [PATCH 019/117] add basic way to get sdc system in python --- rgrow/src/models/sdc1d.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 07c8006..d9c5086 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -27,7 +27,7 @@ use crate::{ base::{Energy, Glue, GrowError, Rate, Tile}, canvas::{PointSafe2, PointSafeHere}, state::State, - system::{Event, NeededUpdate, System, TileBondInfo}, + system::{Event, NeededUpdate, System, SystemEnum, TileBondInfo}, tileset::{FromTileSet, ProcessedTileSet, Size}, }; @@ -583,6 +583,15 @@ fn base(a: &str) -> &str { a.trim_end_matches('*') } +// This is not a smart way of doing this, but it works for now! +#[cfg(python)] +#[pymethods] +impl SystemEnum { + fn new_sdc(params: SDCParams) -> SystemEnum { + SystemEnum::SDC(SDC::from_params(params)) + } +} + impl SDC { pub fn from_params(params: SDCParams) -> Self { let mut glue_name_map = BiHashMap::new(); From 7c5b407141624f959fbdcd8f43887c2eb48311f7 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Wed, 19 Jun 2024 18:40:59 +0100 Subject: [PATCH 020/117] working sliding window fold - (non tested) calculation of delta G --- rgrow/src/lib.rs | 2 + rgrow/src/utils.rs | 118 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 rgrow/src/utils.rs diff --git a/rgrow/src/lib.rs b/rgrow/src/lib.rs index 03d678c..15cb377 100644 --- a/rgrow/src/lib.rs +++ b/rgrow/src/lib.rs @@ -6,6 +6,8 @@ extern crate ndarray; extern crate phf; +pub mod utils; + pub mod tileset; pub mod parser_xgrow; diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs new file mode 100644 index 0000000..aa39a32 --- /dev/null +++ b/rgrow/src/utils.rs @@ -0,0 +1,118 @@ +/// 2-sliding window generic implementatin for any iterator with a fold function +/// +/// None will be returned if the iterator is too short +fn two_window_fold(mut iter: impl Iterator, fold: F) -> Option +where + K: Default, + F: Fn(K, (&T, &T)) -> K, +{ + let mut ans = K::default(); + let mut last = iter.next()?; + let mut current = iter.next()?; + + loop { + ans = fold(ans, (&last, ¤t)); + if let Some(next) = iter.next() { + last = current; + current = next; + } else { + break; + } + } + + Some(ans) +} + +#[derive(Debug, Clone)] +enum DnaNucleotideBase { + A, + T, + G, + C, +} + +impl From for DnaNucleotideBase { + fn from(value: char) -> Self { + match value { + 'a' | 'A' => DnaNucleotideBase::A, + 'c' | 'C' => DnaNucleotideBase::C, + 'g' | 'G' => DnaNucleotideBase::G, + 't' | 'T' => DnaNucleotideBase::T, + _ => panic!("DNA sequence must contain only a,c,g,t characters in upper/lower"), + } + } +} + +/// For some given pair a, b, find (Delta G at 37 degrees C, Delta S) +/// +/// By default the values found in santalucia_thermodynamics_2004 are used +#[inline(always)] +fn dG_dS(a: &DnaNucleotideBase, b: &DnaNucleotideBase) -> (f64, f64) { + // Full name made the match statment horrible + use DnaNucleotideBase::*; + match (a, b) { + (T, T) => (-1.0, -0.0213), + (T, A) => (-0.88, -0.0204), + (A, T) => (-0.58, -0.0213), + (G, T) => (-1.45, -0.0227), + (C, A) => (-1.44, -0.0224), + (G, A) => (-1.28, -0.0210), + (C, T) => (-1.30, -0.0222), + (G, C) => (-2.17, -0.0272), + (C, G) => (-2.24, -0.0244), + (C, C) => (-1.84, -0.0199), + // TODO:Is there missing data that needs to be filled ? + _ => panic!("Could not get dG/dS of pair!!!"), + } +} + +/// Given some dna sequence eg TAGGCGTA, find dG +/// of said sequence with its "perfect fit" +/// (in this case ATCCGCAT) +/// +/// the sum of all neighbours a, b -- dG_(37 degrees C) (a, b) - (temperature - 37) dS(a, b) +fn dna_strength(dna: impl Iterator, temperature: f64) -> f64 { + two_window_fold(dna, |acc, (a, b)| { + let (dg, ds) = dG_dS(a, b); + // Calculate the sum of dG(a, b) - (T - 37) * dS(a, b) + acc + (dg - (temperature - 37.) * ds) + }) + .expect("DNA must have length of at least 2") +} + +/// Get delta g for some string dna sequence and its "perfect match" +pub fn string_dna_delta_g(dna_sequence: String, temperature: f64) -> f64 { + dna_strength( + // Convert dna_sequence string into an iterator of nucleotide bases + dna_sequence + .chars() + .into_iter() + .map(DnaNucleotideBase::from), + temperature, + ) +} + +#[cfg(test)] +mod test_utils { + use crate::utils::string_dna_delta_g; + + use super::two_window_fold; + + #[test] + fn test_sliding_window() { + let v = vec![1., 2., 0., -1., 5.]; + let expected = ((1 + 2) + (2 + 0) + (0 + (-1)) + ((-1) + 5)) as f64; + let acc = two_window_fold(v.iter(), |acc: f64, (a, b)| acc + (*a + *b)); + assert_eq!(Some(expected), acc); + let expected = ((1 * 2) + (2 * 0) + (0 * (-1)) + ((-1) * 5)) as f64; + let acc = two_window_fold(v.iter(), |acc: f64, (a, b)| acc + (*a * *b)); + assert_eq!(Some(expected), acc); + let v = Vec::::new(); + let expected = None; + let acc = two_window_fold(v.iter(), |acc: f64, (_, _)| acc); + assert_eq!(expected, acc); + } + + #[test] + fn test_dna_strength() {} +} From d3c59d8ca4d3a782da3cbc91106fa6e6960355d0 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:32:52 +0100 Subject: [PATCH 021/117] filled delta g and delta s table --- rgrow/src/utils.rs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index aa39a32..597fc66 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -1,3 +1,16 @@ +/* +* A G A A A +* ---------> +* <--------- +* T C T T T +* +* dG = +* g(T, C) + (temp - 37) s(T, C) +* + g(C, A) + (temp - 37) s(C, A) +* + g(A, A) + (temp - 37) s(A, A) +* + g(A, T) + (temp - 37) s(A, T) +* */ + /// 2-sliding window generic implementatin for any iterator with a fold function /// /// None will be returned if the iterator is too short @@ -47,22 +60,21 @@ impl From for DnaNucleotideBase { /// /// By default the values found in santalucia_thermodynamics_2004 are used #[inline(always)] +#[allow(non_snake_case)] fn dG_dS(a: &DnaNucleotideBase, b: &DnaNucleotideBase) -> (f64, f64) { // Full name made the match statment horrible use DnaNucleotideBase::*; match (a, b) { - (T, T) => (-1.0, -0.0213), - (T, A) => (-0.88, -0.0204), + (T, T) | (A, A) => (-1.0, -0.0213), + (C, C) | (G, G) => (-1.84, -0.0199), + (G, T) | (A, C) => (-1.45, -0.0227), + (C, A) | (T, G) => (-1.44, -0.0224), + (G, A) | (T, C) => (-1.28, -0.0210), + (C, T) | (A, G) => (-1.30, -0.0222), (A, T) => (-0.58, -0.0213), - (G, T) => (-1.45, -0.0227), - (C, A) => (-1.44, -0.0224), - (G, A) => (-1.28, -0.0210), - (C, T) => (-1.30, -0.0222), + (T, A) => (-0.88, -0.0204), (G, C) => (-2.17, -0.0272), (C, G) => (-2.24, -0.0244), - (C, C) => (-1.84, -0.0199), - // TODO:Is there missing data that needs to be filled ? - _ => panic!("Could not get dG/dS of pair!!!"), } } @@ -94,7 +106,6 @@ pub fn string_dna_delta_g(dna_sequence: String, temperature: f64) -> f64 { #[cfg(test)] mod test_utils { - use crate::utils::string_dna_delta_g; use super::two_window_fold; From 012d33f6ad02864f17d20279e7fdffd2ade52ef0 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:53:35 +0100 Subject: [PATCH 022/117] changed formula to use dG = dG_37 - (T - 37) dS --- rgrow/src/models/sdc1d.rs | 60 +++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index d9c5086..6154807 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -76,8 +76,8 @@ pub struct SDC { /// /// Set of tiles that can stick to scaffold gap with a given glue pub friends_btm: HashMap>, - /// H in the formula to genereate the glue strengths - pub enthalpy_matrix: Array2, + /// Delta G at 37 degrees C in the formula to genereate the glue strengths + pub delta_g_matrix: Array2, /// S in the formula to geenrate the glue strengths pub entropy_matrix: Array2, /// Temperature of the system in kelvin @@ -87,7 +87,7 @@ pub struct SDC { /// This array is indexed as follows. Given strands x and y, where x is to the west of y /// (meaning that the east of x forms a bond with the west of y), the energy of said bond /// is given by energy_bonds[(x, y)] - energy_bonds: Array2, + strand_energy_bonds: Array2, /// Binding strength between two glues glue_links: Array2, } @@ -118,9 +118,9 @@ impl SDC { /// The strenght of glues a, b is given by: /// - /// G(a, b) = H(a,b) - T * S(a, b) + /// G(a, b) = G_(37) (a,b) - (T - 37) * S(a, b) fn generate_glue_matrix(&mut self) { - self.glue_links = &self.enthalpy_matrix - self.temperature * &self.entropy_matrix; + self.glue_links = &self.delta_g_matrix - (self.temperature - 37.0) * &self.entropy_matrix; } pub fn change_temperature_to(&mut self, kelvin: f64) { @@ -177,12 +177,12 @@ impl SDC { // Case 1: First strands is to the west of second // strand_f strand_s - self.energy_bonds[(strand_f, strand_s)] = + self.strand_energy_bonds[(strand_f, strand_s)] = -self.glue_links[(f_east_glue, s_west_glue)]; // Case 2: First strands is to the east of second // strand_s strand_f - self.energy_bonds[(strand_s, strand_f)] = + self.strand_energy_bonds[(strand_s, strand_f)] = -self.glue_links[(f_west_glue, s_east_glue)]; } } @@ -294,7 +294,8 @@ impl SDC { state.tile_to_e(scaffold_point) as usize, ); - self.energy_bonds[(strand as usize, e)] + self.energy_bonds[(w, strand as usize)] + self.strand_energy_bonds[(strand as usize, e)] + + self.strand_energy_bonds[(w, strand as usize)] } } @@ -403,7 +404,7 @@ impl System for SDC { "kf" => Ok(Box::new(self.kf)), "strand_concentrations" => Ok(Box::new(self.strand_concentration.clone())), "glue_links" => Ok(Box::new(self.glue_links.clone())), - "energy_bonds" => Ok(Box::new(self.energy_bonds.clone())), + "energy_bonds" => Ok(Box::new(self.strand_energy_bonds.clone())), _ => Err(GrowError::NoParameter(name.to_string())), } } @@ -495,11 +496,11 @@ impl FromTileSet for SDC { scaffold, strand_concentration, kf: tileset.kf.unwrap_or(1.0e6), - enthalpy_matrix: todo!(), + delta_g_matrix: todo!(), entropy_matrix: todo!(), temperature: todo!(), friends_btm: HashMap::new(), - energy_bonds, + strand_energy_bonds: energy_bonds, }; // This will generate the friends hashamp, as well as the glues, and the energy bonds @@ -564,7 +565,8 @@ pub struct SDCParams { pub tile_names: Vec>, pub tile_colors: Vec>, pub scaffold: SingleOrMultiScaffold, - pub glue_h_s: HashMap, + // Pair with delta G at 37 degrees C and delta S + pub glue_dg_s: HashMap, pub k_f: f64, pub k_n: f64, pub k_c: f64, @@ -628,25 +630,26 @@ impl SDC { } } - let mut glue_h = Array2::::zeros((max_gluenum, max_gluenum)); + // Delta G at 37 degrees C + let mut glue_delta_g = Array2::::zeros((max_gluenum, max_gluenum)); let mut glue_s = Array2::::zeros((max_gluenum, max_gluenum)); - for (k, &v) in params.glue_h_s.iter() { + for (k, &v) in params.glue_dg_s.iter() { match k { RefOrPair::Ref(r) => { let i = *glue_name_map.get_by_left(&comp(r)).unwrap(); // FIXME: fails if glue not found let j = *glue_name_map.get_by_left(base(r)).unwrap(); // FIXME: fails if glue not found - glue_h[[i, j]] = v.0; + glue_delta_g[[i, j]] = v.0; glue_s[[i, j]] = v.1; - glue_h[[j, i]] = v.0; + glue_delta_g[[j, i]] = v.0; glue_s[[j, i]] = v.1; } RefOrPair::Pair(r1, r2) => { let i = *glue_name_map.get_by_left(r1).unwrap(); // FIXME: fails if glue not found let j = *glue_name_map.get_by_left(r2).unwrap(); // FIXME: fails if glue not found - glue_h[[i, j]] = v.0; + glue_delta_g[[i, j]] = v.0; glue_s[[i, j]] = v.1; - glue_h[[j, i]] = v.0; + glue_delta_g[[j, i]] = v.0; glue_s[[j, i]] = v.1; } } @@ -661,7 +664,7 @@ impl SDC { .collect(), glue_names: todo!(), scaffold: todo!(), - enthalpy_matrix: glue_h, + delta_g_matrix: glue_delta_g, entropy_matrix: glue_s, temperature: params.temperature, strand_concentration: Array1::from(params.tile_concentration), @@ -672,9 +675,12 @@ impl SDC { // // It may be cleaner to have a "new" function that takes just what is needed as a // param and generates the rest - glue_links: glue_h - params.temperature * glue_s, + glue_links: Array2::::zeros((params.tile_names.len(), params.tile_names.len())), friends_btm: HashMap::new(), - energy_bonds: Array2::::zeros((params.tile_names.len(), params.tile_names.len())), + strand_energy_bonds: Array2::::zeros(( + params.tile_names.len(), + params.tile_names.len(), + )), }; sdc.update_system(); @@ -710,17 +716,21 @@ mod test_sdc_model { kf: 0.0, friends_btm: HashMap::new(), entropy_matrix: array![[1., 2., 3.], [5., 1., 8.], [5., -2., 12.]], - enthalpy_matrix: array![[4., 1., -8.], [6., 1., 14.], [12., 21., -13.,]], + delta_g_matrix: array![[4., 1., -8.], [6., 1., 14.], [12., 21., -13.,]], temperature: 5., - energy_bonds: Array2::::zeros((5, 5)), + strand_energy_bonds: Array2::::zeros((5, 5)), glue_links: Array2::::zeros((5, 5)), }; sdc.update_system(); + // THIS TEST WILL NO LONGER PASS, SINCE NOW THE FORMULA IS DIFFERENT + // + // TODO: Update test + // Check that the glue matrix is being generated as expected - let expeced_glue_matrix = array![[-1.0, -9., -23.], [-19., -4., -26.], [-13., 31., -73.]]; - assert_eq!(expeced_glue_matrix, sdc.glue_links); + let _expeced_glue_matrix = array![[-1.0, -9., -23.], [-19., -4., -26.], [-13., 31., -73.]]; + // assert_eq!(expeced_glue_matrix, sdc.glue_links); // TODO Check that the energy bonds are being generated as expected From 2a34c8512cf577edb546e1942c3003f341934271 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:22:09 +0100 Subject: [PATCH 023/117] added function new :: minimum params needed -> SDC --- rgrow/src/models/sdc1d.rs | 85 ++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 6154807..deb75c3 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -80,7 +80,10 @@ pub struct SDC { pub delta_g_matrix: Array2, /// S in the formula to geenrate the glue strengths pub entropy_matrix: Array2, - /// Temperature of the system in kelvin + /// Temperature of the system + /// + /// Not pub so that it cant accidentally be changed other than with the setter function + /// that will also recalculate energy arrays temperature: f64, /// The energy with which two strands will bond /// @@ -93,6 +96,42 @@ pub struct SDC { } impl SDC { + fn new( + anchor_tiles: Vec<(PointSafe2, Tile)>, + strand_names: Vec, + glue_names: Vec, + scaffold: Array2, + strand_concentration: Array1, + glues: Array2, + colors: Vec<[u8; 4]>, + kf: RatePerConc, + delta_g_matrix: Array2, + entropy_matrix: Array2, + temperature: f64, + ) -> SDC { + let strand_count = strand_names.len(); + let mut s = SDC { + anchor_tiles, + strand_concentration, + strand_names, + colors, + glues, + scaffold, + glue_names, + kf, + delta_g_matrix, + entropy_matrix, + temperature, + // These will be generated by the update_system function next, so just leave them + // empty for now + friends_btm: HashMap::new(), + glue_links: Array2::::zeros((strand_count, strand_count)), + strand_energy_bonds: Array2::::zeros((strand_count, strand_count)), + }; + s.update_system(); + s + } + fn update_system(&mut self) { // Note that order is important, we need to generate the glue matrix first, then using // the data generated there, the energy array is filled, etc... @@ -655,36 +694,24 @@ impl SDC { } } - let mut sdc = SDC { - anchor_tiles: Vec::new(), - strand_names: params + SDC::new( + Vec::new(), + params .tile_names - .iter() - .map(|x| x.clone().unwrap_or("".to_string())) + .into_iter() + .enumerate() + .map(|(n, os)| os.unwrap_or(n.to_string())) .collect(), - glue_names: todo!(), - scaffold: todo!(), - delta_g_matrix: glue_delta_g, - entropy_matrix: glue_s, - temperature: params.temperature, - strand_concentration: Array1::from(params.tile_concentration), - glues: tile_glues_int, - colors: Vec::new(), - kf: params.k_f, - // The ones below should be generated by the update system function - // - // It may be cleaner to have a "new" function that takes just what is needed as a - // param and generates the rest - glue_links: Array2::::zeros((params.tile_names.len(), params.tile_names.len())), - friends_btm: HashMap::new(), - strand_energy_bonds: Array2::::zeros(( - params.tile_names.len(), - params.tile_names.len(), - )), - }; - - sdc.update_system(); - sdc + todo!(), + todo!(), + Array1::from(params.tile_concentration), + tile_glues_int, + Vec::new(), + params.k_f, + glue_delta_g, + glue_s, + params.temperature, + ) } } From dfde20ca717f1858018d3b9b00a95868989e96de Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 20 Jun 2024 13:19:27 +0100 Subject: [PATCH 024/117] Add G_Scaffold, and take it into account for detachment --- rgrow/src/models/sdc1d.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index deb75c3..5dea3d7 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -91,6 +91,8 @@ pub struct SDC { /// (meaning that the east of x forms a bond with the west of y), the energy of said bond /// is given by energy_bonds[(x, y)] strand_energy_bonds: Array2, + /// The energy with which a strand attached to scaffold + scaffold_energy_bonds: Array1, /// Binding strength between two glues glue_links: Array2, } @@ -127,6 +129,7 @@ impl SDC { friends_btm: HashMap::new(), glue_links: Array2::::zeros((strand_count, strand_count)), strand_energy_bonds: Array2::::zeros((strand_count, strand_count)), + scaffold_energy_bonds: Array1::::zeros(strand_count), }; s.update_system(); s @@ -201,9 +204,13 @@ impl SDC { // For each *possible* pair of strands, calculate the energy bond for strand_f in 0..(num_of_strands as usize) { - let (f_west_glue, f_east_glue) = { + let (f_west_glue, f_btm_glue, f_east_glue) = { let glues = self.glues.row(strand_f); - (glues[WEST_GLUE_INDEX], glues[EAST_GLUE_INDEX]) + ( + glues[WEST_GLUE_INDEX], + glues[BOTTOM_GLUE_INDEX], + glues[EAST_GLUE_INDEX], + ) }; for strand_s in 0..(num_of_strands as usize) { @@ -224,6 +231,9 @@ impl SDC { self.strand_energy_bonds[(strand_s, strand_f)] = -self.glue_links[(f_west_glue, s_east_glue)]; } + + // Calculate the binding strength of the starnd with the scaffold + self.scaffold_energy_bonds[strand_f] = -self.glue_links[(f_btm_glue, f_btm_glue)]; } } @@ -333,7 +343,8 @@ impl SDC { state.tile_to_e(scaffold_point) as usize, ); - self.strand_energy_bonds[(strand as usize, e)] + self.scaffold_energy_bonds[strand as usize] + + self.strand_energy_bonds[(strand as usize, e)] + self.strand_energy_bonds[(w, strand as usize)] } } @@ -540,6 +551,7 @@ impl FromTileSet for SDC { temperature: todo!(), friends_btm: HashMap::new(), strand_energy_bonds: energy_bonds, + scaffold_energy_bonds: todo!(), }; // This will generate the friends hashamp, as well as the glues, and the energy bonds From 6686d87e3f8b1e9df3c6d193e969eff2ba6ca759 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 20 Jun 2024 13:35:19 +0100 Subject: [PATCH 025/117] added missing parameter to test --- rgrow/src/models/sdc1d.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 5dea3d7..c45c093 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -758,6 +758,7 @@ mod test_sdc_model { delta_g_matrix: array![[4., 1., -8.], [6., 1., 14.], [12., 21., -13.,]], temperature: 5., strand_energy_bonds: Array2::::zeros((5, 5)), + scaffold_energy_bonds: Array1::::zeros(5), glue_links: Array2::::zeros((5, 5)), }; From 1a3e4c64a68a5d510dabc8de8285980216f97665 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Thu, 20 Jun 2024 15:22:27 +0100 Subject: [PATCH 026/117] =?UTF-8?q?=CE=94G=20parameter=20changes,=20use=20?= =?UTF-8?q?&str,=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 1 + rgrow/Cargo.toml | 1 + rgrow/src/utils.rs | 133 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 119 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cff3599..35f2836 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ numpy = "^0.21" enum_dispatch = "0.3" pyo3-polars = "^0.14" polars = {version = "^0.40", features = ["lazy"]} +approx = "^0.5" [workspace.package] version = "0.14.1" diff --git a/rgrow/Cargo.toml b/rgrow/Cargo.toml index 81d036b..db1fd4a 100644 --- a/rgrow/Cargo.toml +++ b/rgrow/Cargo.toml @@ -68,6 +68,7 @@ ndarray = { workspace = true } enum_dispatch = "0.3" polars = { workspace = true } pyo3-polars = {workspace = true} +approx = { workspace = true } [dependencies.clap] version = "4" diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index 597fc66..942059f 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -56,7 +56,7 @@ impl From for DnaNucleotideBase { } } -/// For some given pair a, b, find (Delta G at 37 degrees C, Delta S) +/// For some given pair 5' - a, b - 3', find (Delta G at 37 degrees C, Delta S) /// /// By default the values found in santalucia_thermodynamics_2004 are used #[inline(always)] @@ -67,14 +67,14 @@ fn dG_dS(a: &DnaNucleotideBase, b: &DnaNucleotideBase) -> (f64, f64) { match (a, b) { (T, T) | (A, A) => (-1.0, -0.0213), (C, C) | (G, G) => (-1.84, -0.0199), - (G, T) | (A, C) => (-1.45, -0.0227), - (C, A) | (T, G) => (-1.44, -0.0224), - (G, A) | (T, C) => (-1.28, -0.0210), - (C, T) | (A, G) => (-1.30, -0.0222), - (A, T) => (-0.58, -0.0213), - (T, A) => (-0.88, -0.0204), - (G, C) => (-2.17, -0.0272), - (C, G) => (-2.24, -0.0244), + (G, T) | (A, C) => (-1.44, -0.0224), + (C, A) | (T, G) => (-1.45, -0.0227), + (G, A) | (T, C) => (-1.30, -0.0222), + (C, T) | (A, G) => (-1.28, -0.0210), + (T, A) => (-0.58, -0.0213), + (A, T) => (-0.88, -0.0204), + (C, G) => (-2.17, -0.0272), + (G, C) => (-2.24, -0.0244), } } @@ -92,14 +92,18 @@ fn dna_strength(dna: impl Iterator, temperature: f64) .expect("DNA must have length of at least 2") } -/// Get delta g for some string dna sequence and its "perfect match" -pub fn string_dna_delta_g(dna_sequence: String, temperature: f64) -> f64 { +/// Get delta g for some string dna sequence and its "perfect match". For example: +/// +/// ```rust +/// use rgrow::utils::string_dna_delta_g; +/// let seq = "cgatg"; +/// assert_eq!(string_dna_delta_g(seq, 37.0), -5.8); +/// ``` +/// +pub fn string_dna_delta_g(dna_sequence: &str, temperature: f64) -> f64 { dna_strength( // Convert dna_sequence string into an iterator of nucleotide bases - dna_sequence - .chars() - .into_iter() - .map(DnaNucleotideBase::from), + dna_sequence.chars().map(DnaNucleotideBase::from), temperature, ) } @@ -107,7 +111,9 @@ pub fn string_dna_delta_g(dna_sequence: String, temperature: f64) -> f64 { #[cfg(test)] mod test_utils { + use super::string_dna_delta_g; use super::two_window_fold; + use approx::assert_ulps_eq; #[test] fn test_sliding_window() { @@ -125,5 +131,100 @@ mod test_utils { } #[test] - fn test_dna_strength() {} + #[allow(non_snake_case)] + fn test_dna_strength() { + // random sequences + let seqs = [ + "cg", + "cttcgccac", + "gacggcattatgtc", + "ct", + "tc", + "aatacgacggccag", + "caga", + "ttaaccctta", + "actatg", + "cttaatccgagaataaaaa", + "gccggggttaaaac", + "tacaaagggtg", + "tgg", + "tggtcgccatctcccgt", + "ccgttcctagat", + "agttagagcttttggacta", + "cacctttccgcagg", + "tttaacttctc", + "gcgccct", + "tatttcgtaacttgcacat", + ]; + + /* + Values are taken from stickydesign 0.9.0.a3, using + + ```python + # T is temperature, x is sequence + -sd.EnergeticsBasic(temperature=T).matching_uniform(sd.endarray([x],'S'))[0]-1.96+(T-37)*0.0057 + ``` + + The correction here is because stickydesign includes the initiation penalty + from SantaLucia. It's actually unclear whether that should be included here, + or in other places where it has been included in the past. It's worth a discussion. + */ + + let dG_at_37 = [ + -2.17, + -12.719999999999999, + -17.970000000000002, + -1.28, + -1.3, + -19.630000000000003, + -4.03, + -10.560000000000002, + -5.63, + -20.39, + -19.23, + -13.32, + -3.29, + -25.78, + -14.91, + -22.57, + -20.130000000000003, + -11.18, + -11.61, + -22.58, + ]; + + let dG_at_50 = [-1.8164, + -10.365699999999999, + -14.2065, + -1.0070000000000001, + -1.0114, + -15.8301, + -3.1733000000000002, + -8.0939, + -4.2286, + -15.3434, + -15.557500000000003, + -10.526299999999997, + -2.7362, + -21.144200000000005, + -11.8056, + -17.513, + -16.4133, + -8.381099999999998, + -9.8316, + -17.396900000000002]; + + for (&seq, &dG) in seqs.iter().zip(dG_at_37.iter()) { + let result = string_dna_delta_g(seq, 37.0); + println!("{}", seq); + assert_ulps_eq!(dG, result, max_ulps = 10); + } + + for (&seq, &dG) in seqs.iter().zip(dG_at_50.iter()) { + let result = string_dna_delta_g(seq, 50.0); + println!("{}", seq); + assert_ulps_eq!(dG, result, max_ulps = 10); + } + + } } From 2e023910f3466c79e4ccfe4f6bfafd624b9284c3 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 20 Jun 2024 15:41:52 +0100 Subject: [PATCH 027/117] =?UTF-8?q?added=20function=20to=20get=20=CE=94G?= =?UTF-8?q?=20and=20=CE=94S=20for=20entire=20sequence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rgrow/src/utils.rs | 53 +++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index 942059f..7215c5e 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -84,10 +84,14 @@ fn dG_dS(a: &DnaNucleotideBase, b: &DnaNucleotideBase) -> (f64, f64) { /// /// the sum of all neighbours a, b -- dG_(37 degrees C) (a, b) - (temperature - 37) dS(a, b) fn dna_strength(dna: impl Iterator, temperature: f64) -> f64 { - two_window_fold(dna, |acc, (a, b)| { + let (total_dg, total_ds) = dna_dg_ds(dna); + total_dg - (temperature - 37.0) * total_ds +} + +fn dna_dg_ds(dna: impl Iterator) -> (f64, f64) { + two_window_fold(dna, |(acc_dg, acc_ds), (a, b)| { let (dg, ds) = dG_dS(a, b); - // Calculate the sum of dG(a, b) - (T - 37) * dS(a, b) - acc + (dg - (temperature - 37.) * ds) + (dg + acc_dg, ds + acc_ds) }) .expect("DNA must have length of at least 2") } @@ -193,26 +197,28 @@ mod test_utils { -22.58, ]; - let dG_at_50 = [-1.8164, - -10.365699999999999, - -14.2065, - -1.0070000000000001, - -1.0114, - -15.8301, - -3.1733000000000002, - -8.0939, - -4.2286, - -15.3434, - -15.557500000000003, - -10.526299999999997, - -2.7362, - -21.144200000000005, - -11.8056, - -17.513, - -16.4133, - -8.381099999999998, - -9.8316, - -17.396900000000002]; + let dG_at_50 = [ + -1.8164, + -10.365699999999999, + -14.2065, + -1.0070000000000001, + -1.0114, + -15.8301, + -3.1733000000000002, + -8.0939, + -4.2286, + -15.3434, + -15.557500000000003, + -10.526299999999997, + -2.7362, + -21.144200000000005, + -11.8056, + -17.513, + -16.4133, + -8.381099999999998, + -9.8316, + -17.396900000000002, + ]; for (&seq, &dG) in seqs.iter().zip(dG_at_37.iter()) { let result = string_dna_delta_g(seq, 37.0); @@ -225,6 +231,5 @@ mod test_utils { println!("{}", seq); assert_ulps_eq!(dG, result, max_ulps = 10); } - } } From ce800c56a3e382f7f34bc31a648a2ad39c52d4b5 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:17:35 +0100 Subject: [PATCH 028/117] fix issue for glues with ** in the name --- rgrow/src/models/sdc1d.rs | 90 +++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index c45c093..7427799 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -624,18 +624,6 @@ pub struct SDCParams { pub temperature: f64, } -fn comp(a: &str) -> String { - if a.ends_with('*') { - a.trim_end_matches('*').to_string() - } else { - format!("{}*", a) - } -} - -fn base(a: &str) -> &str { - a.trim_end_matches('*') -} - // This is not a smart way of doing this, but it works for now! #[cfg(python)] #[pymethods] @@ -645,9 +633,21 @@ impl SystemEnum { } } +/// Return the orignial, and its inverse +fn self_and_inverse(value: &String) -> (String, String) { + // Remove all the stars at the end + let filtered = value.trim_end_matches("*"); + let star_count = value.len() - filtered.len(); + if star_count % 2 == 0 { + (filtered.to_string(), format!("{}*", filtered.to_string())) + } else { + (format!("{}*", filtered).to_string(), filtered.to_string()) + } +} + impl SDC { pub fn from_params(params: SDCParams) -> Self { - let mut glue_name_map = BiHashMap::new(); + let mut glue_name_map = BiHashMap::::new(); let mut gluenum = 1; let mut max_gluenum = 1; @@ -659,19 +659,18 @@ impl SDC { { for (i, t) in tgl.iter().enumerate() { match t { - None => { - r[i] = 0; - } + None => r[i] = 0, Some(s) => { - let j = glue_name_map.get_by_left(s); + let (s_equiv, s_equiv_inverse) = self_and_inverse(s); + let j = glue_name_map.get_by_left(&s_equiv); match j { Some(j) => { r[i] = *j; } None => { - glue_name_map.insert(base(s).to_string(), gluenum); - glue_name_map.insert(format!("{}*", base(s)), gluenum + 1); - r[i] = *glue_name_map.get_by_left(s).unwrap(); // FIXME: will fail if ** is in name, and is inefficient + glue_name_map.insert(s_equiv, gluenum); + glue_name_map.insert(s_equiv_inverse, gluenum + 1); + r[i] = gluenum; gluenum += 2; max_gluenum = max_gluenum.max(gluenum); } @@ -686,24 +685,22 @@ impl SDC { let mut glue_s = Array2::::zeros((max_gluenum, max_gluenum)); for (k, &v) in params.glue_dg_s.iter() { - match k { - RefOrPair::Ref(r) => { - let i = *glue_name_map.get_by_left(&comp(r)).unwrap(); // FIXME: fails if glue not found - let j = *glue_name_map.get_by_left(base(r)).unwrap(); // FIXME: fails if glue not found - glue_delta_g[[i, j]] = v.0; - glue_s[[i, j]] = v.1; - glue_delta_g[[j, i]] = v.0; - glue_s[[j, i]] = v.1; - } + let (i, j) = match k { + RefOrPair::Ref(r) => self_and_inverse(r), RefOrPair::Pair(r1, r2) => { - let i = *glue_name_map.get_by_left(r1).unwrap(); // FIXME: fails if glue not found - let j = *glue_name_map.get_by_left(r2).unwrap(); // FIXME: fails if glue not found - glue_delta_g[[i, j]] = v.0; - glue_s[[i, j]] = v.1; - glue_delta_g[[j, i]] = v.0; - glue_s[[j, i]] = v.1; + let (r1, _) = self_and_inverse(r1); + let (r2, _) = self_and_inverse(r2); + (r1, r2) } - } + }; + + let i = *glue_name_map.get_by_left(&i).unwrap(); // FIXME: fails if glue not found + let j = *glue_name_map.get_by_left(&j).unwrap(); // FIXME: fails if glue not found + + glue_delta_g[[i, j]] = v.0; + glue_delta_g[[j, i]] = v.0; + glue_s[[i, j]] = v.1; + glue_s[[j, i]] = v.1; } SDC::new( @@ -784,4 +781,25 @@ mod test_sdc_model { ]); assert_eq!(expected_friends, sdc.friends_btm); } + + #[test] + fn test_self_and_inverse() { + let input = vec!["some*str", "some*str*", "some*str**"]; + + let acc = input + .into_iter() + .map(|str| self_and_inverse(&str.to_string())) + .collect::>(); + + let expected = vec![ + ("some*str", "some*str*"), + ("some*str*", "some*str"), + ("some*str", "some*str*"), + ] + .iter() + .map(|(a, b)| (a.to_string(), b.to_string())) + .collect::>(); + + assert_eq!(acc, expected); + } } From 1c37e8e7df581c39529559179333d2809b25f489 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Thu, 20 Jun 2024 18:20:59 +0100 Subject: [PATCH 029/117] Add direct state creation. --- rgrow/src/python.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rgrow/src/python.rs b/rgrow/src/python.rs index fa2cceb..7597c82 100644 --- a/rgrow/src/python.rs +++ b/rgrow/src/python.rs @@ -5,7 +5,7 @@ use crate::base::{NumEvents, NumTiles, RustAny, Tile}; use crate::canvas::{Canvas, PointSafeHere}; use crate::ffs::{BoxedFFSResult, FFSRunConfig, FFSStateRef}; use crate::ratestore::RateStore; -use crate::state::{ClonableState, StateEnum, StateStatus, TrackerData}; +use crate::state::{StateEnum, StateStatus, TrackerData}; use crate::system::{ DimerInfo, DynSystem, EvolveBounds, EvolveOutcome, NeededUpdate, SystemEnum, SystemWithDimers, TileBondInfo }; @@ -25,6 +25,11 @@ pub struct PyState(pub(crate) StateEnum); #[cfg(feature = "python")] #[pymethods] impl PyState { + #[new] + pub fn empty(shape: (usize, usize), kind: &str, tracking: &str) -> PyResult { + Ok(PyState(StateEnum::empty(shape, kind.try_into()?, tracking.try_into()?)?)) + } + #[getter] /// A direct, mutable view of the state's canvas. This is potentially unsafe. pub fn canvas_view<'py>( From 3151d7d8a5740ce263161aa7fbc7babda1020121 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Thu, 20 Jun 2024 19:43:31 +0100 Subject: [PATCH 030/117] colors, python, scaffold --- rgrow/src/models/sdc1d.rs | 48 ++++++++++++++++++++++++++------------- rgrow/src/python.rs | 6 +++++ 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 7427799..5e41f8d 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -26,8 +26,9 @@ use std::{ use crate::{ base::{Energy, Glue, GrowError, Rate, Tile}, canvas::{PointSafe2, PointSafeHere}, + colors::get_color_or_random, state::State, - system::{Event, NeededUpdate, System, SystemEnum, TileBondInfo}, + system::{Event, NeededUpdate, System, TileBondInfo}, tileset::{FromTileSet, ProcessedTileSet, Size}, }; @@ -571,7 +572,7 @@ use bimap::BiHashMap; use pyo3::prelude::*; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(python, derive(FromPyObject))] +#[cfg_attr(feature = "python", derive(pyo3::FromPyObject))] pub enum RefOrPair { Ref(String), Pair(String, String), @@ -590,7 +591,7 @@ impl From<(String, String)> for RefOrPair { } #[derive(Debug)] -#[cfg_attr(python, derive(FromPyObject))] +#[cfg_attr(feature = "python", derive(pyo3::FromPyObject))] pub enum SingleOrMultiScaffold { Single(Vec), Multi(Vec>), @@ -609,7 +610,7 @@ impl From>> for SingleOrMultiScaffold { } #[derive(Debug)] -#[cfg_attr(python, derive(FromPyObject))] +#[cfg_attr(feature = "python", derive(pyo3::FromPyObject))] pub struct SDCParams { pub tile_glues: Vec>>, pub tile_concentration: Vec, @@ -624,15 +625,6 @@ pub struct SDCParams { pub temperature: f64, } -// This is not a smart way of doing this, but it works for now! -#[cfg(python)] -#[pymethods] -impl SystemEnum { - fn new_sdc(params: SDCParams) -> SystemEnum { - SystemEnum::SDC(SDC::from_params(params)) - } -} - /// Return the orignial, and its inverse fn self_and_inverse(value: &String) -> (String, String) { // Remove all the stars at the end @@ -703,6 +695,30 @@ impl SDC { glue_s[[j, i]] = v.1; } + let mut glue_names = Array1::::from_elem(max_gluenum + 1, "".to_string()); + for (s, i) in glue_name_map.iter() { + glue_names[*i] = s.clone(); + } + + let scaffold = match params.scaffold { + SingleOrMultiScaffold::Single(s) => { + let mut scaffold = Array2::::zeros((64, s.len())); + for (i, g) in s.iter().enumerate() { + scaffold + .index_axis_mut(ndarray::Axis(1), i) + .fill(*glue_name_map.get_by_left(g).unwrap()); + } + scaffold + } + SingleOrMultiScaffold::Multi(_m) => todo!(), + }; + + let colors = params + .tile_colors + .iter() + .map(|c| get_color_or_random(&c.as_ref().map(|x| x.as_str())).unwrap()) + .collect(); + SDC::new( Vec::new(), params @@ -711,11 +727,11 @@ impl SDC { .enumerate() .map(|(n, os)| os.unwrap_or(n.to_string())) .collect(), - todo!(), - todo!(), + glue_names.into_iter().collect(), // FIXME: consider types here + scaffold, Array1::from(params.tile_concentration), tile_glues_int, - Vec::new(), + colors, params.k_f, glue_delta_g, glue_s, diff --git a/rgrow/src/python.rs b/rgrow/src/python.rs index 7597c82..e27384e 100644 --- a/rgrow/src/python.rs +++ b/rgrow/src/python.rs @@ -4,6 +4,7 @@ use std::time::Duration; use crate::base::{NumEvents, NumTiles, RustAny, Tile}; use crate::canvas::{Canvas, PointSafeHere}; use crate::ffs::{BoxedFFSResult, FFSRunConfig, FFSStateRef}; +use crate::models::sdc1d::{SDC,SDCParams}; use crate::ratestore::RateStore; use crate::state::{StateEnum, StateStatus, TrackerData}; use crate::system::{ @@ -346,4 +347,9 @@ impl PySystem { pub fn print_debug(&self) { println!("{:?}", self.0); } + + #[staticmethod] + fn new_sdc(params: SDCParams) -> PySystem { + PySystem(SystemEnum::SDC(SDC::from_params(params))) + } } From 2c08f1698bd0e31cbaab7d1c7fdb80ec63e6806e Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Fri, 21 Jun 2024 10:04:11 +0100 Subject: [PATCH 031/117] remove old TODO comments --- rgrow/src/models/sdc1d.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 5e41f8d..c42fbf9 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -13,9 +13,6 @@ macro_rules! type_alias { * * TODO: * - There are quite a few expects that need to be handled better -* - find_monomer_attachment_possibilities_at_point is missing one parameter (because im unsure as -* to what it does) -* - Replace all use of index for glues to WEST_GLUE_INDEX ... * */ use std::{ From 9fa724ca617b7dda1981641ecf79ef6a9f218168 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:01:38 +0100 Subject: [PATCH 032/117] make friends join a to a* --- rgrow/src/models/sdc1d.rs | 82 ++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index c42fbf9..bd233ad 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -148,8 +148,18 @@ impl SDC { .index_axis(ndarray::Axis(1), BOTTOM_GLUE_INDEX) .indexed_iter() { + // 0 <-> Nothing + // 1 <-> 2 + // 3 <-> 4 + // ... + + if b == 0 { + continue; + } + + let b_inverse = if b % 2 == 1 { b + 1 } else { b - 1 }; friends_btm - .entry(b) + .entry(b_inverse) .or_insert(HashSet::new()) .insert(t as u32); } @@ -622,16 +632,26 @@ pub struct SDCParams { pub temperature: f64, } -/// Return the orignial, and its inverse -fn self_and_inverse(value: &String) -> (String, String) { +/// Triple (x, y, z) +/// +/// x: Original input but parsed so that there can be no errors in it (eg. No h**) +/// y: From (eg. h) +/// z: Inverse (eg. h*) +fn self_and_inverse(value: &String) -> (String, String, String) { // Remove all the stars at the end let filtered = value.trim_end_matches("*"); let star_count = value.len() - filtered.len(); - if star_count % 2 == 0 { - (filtered.to_string(), format!("{}*", filtered.to_string())) + let simplified = if star_count % 2 == 0 { + filtered.to_string() } else { - (format!("{}*", filtered).to_string(), filtered.to_string()) - } + format!("{}*", filtered.to_string()) + }; + + ( + simplified, + filtered.to_string(), + format!("{}*", filtered.to_string()), + ) } impl SDC { @@ -650,16 +670,22 @@ impl SDC { match t { None => r[i] = 0, Some(s) => { - let (s_equiv, s_equiv_inverse) = self_and_inverse(s); - let j = glue_name_map.get_by_left(&s_equiv); + let (s, s_base, s_to) = self_and_inverse(s); + let j = glue_name_map.get_by_left(&s); match j { Some(j) => { r[i] = *j; } None => { - glue_name_map.insert(s_equiv, gluenum); - glue_name_map.insert(s_equiv_inverse, gluenum + 1); + glue_name_map.insert(s_base, gluenum); + glue_name_map.insert(s_to, gluenum + 1); r[i] = gluenum; + + // The right answer here would be gluenum+1, so add one + if s.ends_with('*') { + r[i] += 1 + } + gluenum += 2; max_gluenum = max_gluenum.max(gluenum); } @@ -675,10 +701,13 @@ impl SDC { for (k, &v) in params.glue_dg_s.iter() { let (i, j) = match k { - RefOrPair::Ref(r) => self_and_inverse(r), + RefOrPair::Ref(r) => { + let (_, base, inverse) = self_and_inverse(r); + (base, inverse) + } RefOrPair::Pair(r1, r2) => { - let (r1, _) = self_and_inverse(r1); - let (r2, _) = self_and_inverse(r2); + let (r1, _, _) = self_and_inverse(r1); + let (r2, _, _) = self_and_inverse(r2); (r1, r2) } }; @@ -755,11 +784,11 @@ mod test_sdc_model { glues: array![ [0, 0, 0], [1, 3, 12], - [6, 8, 12], + [6, 2, 12], [31, 3, 45], [8, 4, 2], [1, 1, 78], - [4, 8, 1], + [4, 4, 1], ], colors: Vec::new(), kf: 0.0, @@ -786,11 +815,10 @@ mod test_sdc_model { // Check that the friends hashmap is being generated as expected let expected_friends = HashMap::from([ - (0, HashSet::from([0])), - (1, HashSet::from([5])), - (3, HashSet::from([1, 3])), - (4, HashSet::from([4])), - (8, HashSet::from([2, 6])), + (1, HashSet::from([2])), + (2, HashSet::from([5])), + (3, HashSet::from([4, 6])), + (4, HashSet::from([1, 3])), ]); assert_eq!(expected_friends, sdc.friends_btm); } @@ -802,16 +830,16 @@ mod test_sdc_model { let acc = input .into_iter() .map(|str| self_and_inverse(&str.to_string())) - .collect::>(); + .collect::>(); let expected = vec![ - ("some*str", "some*str*"), - ("some*str*", "some*str"), - ("some*str", "some*str*"), + ("some*str", "some*str", "some*str*"), + ("some*str*", "some*str", "some*str*"), + ("some*str", "some*str", "some*str*"), ] .iter() - .map(|(a, b)| (a.to_string(), b.to_string())) - .collect::>(); + .map(|(a, b, c)| (a.to_string(), b.to_string(), c.to_string())) + .collect::>(); assert_eq!(acc, expected); } From 1a9e61447a4381d377479056e86867f659d11ca9 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:04:59 +0100 Subject: [PATCH 033/117] remove reduntant line --- rgrow/src/models/sdc1d.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index bd233ad..547c79f 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -657,10 +657,7 @@ fn self_and_inverse(value: &String) -> (String, String, String) { impl SDC { pub fn from_params(params: SDCParams) -> Self { let mut glue_name_map = BiHashMap::::new(); - let mut gluenum = 1; - let mut max_gluenum = 1; - let mut tile_glues_int = Array2::::zeros((params.tile_glues.len(), 3)); for (tgl, mut r) in @@ -687,7 +684,6 @@ impl SDC { } gluenum += 2; - max_gluenum = max_gluenum.max(gluenum); } } } @@ -695,6 +691,8 @@ impl SDC { } } + let max_gluenum = gluenum; + // Delta G at 37 degrees C let mut glue_delta_g = Array2::::zeros((max_gluenum, max_gluenum)); let mut glue_s = Array2::::zeros((max_gluenum, max_gluenum)); From 24887e526859fcc7a856c8a16279739130431a0b Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:10:35 +0100 Subject: [PATCH 034/117] Removed negatives --- rgrow/src/models/sdc1d.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 547c79f..4318a7a 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -232,16 +232,16 @@ impl SDC { // Case 1: First strands is to the west of second // strand_f strand_s self.strand_energy_bonds[(strand_f, strand_s)] = - -self.glue_links[(f_east_glue, s_west_glue)]; + self.glue_links[(f_east_glue, s_west_glue)]; // Case 2: First strands is to the east of second // strand_s strand_f self.strand_energy_bonds[(strand_s, strand_f)] = - -self.glue_links[(f_west_glue, s_east_glue)]; + self.glue_links[(f_west_glue, s_east_glue)]; } // Calculate the binding strength of the starnd with the scaffold - self.scaffold_energy_bonds[strand_f] = -self.glue_links[(f_btm_glue, f_btm_glue)]; + self.scaffold_energy_bonds[strand_f] = self.glue_links[(f_btm_glue, f_btm_glue)]; } } From d62c4f401cb49a769dd6f2b48bb7c8c8799b2ae4 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:08:49 +0100 Subject: [PATCH 035/117] Improved error name --- rgrow/src/models/sdc1d.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 4318a7a..8f5107b 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -710,8 +710,15 @@ impl SDC { } }; - let i = *glue_name_map.get_by_left(&i).unwrap(); // FIXME: fails if glue not found - let j = *glue_name_map.get_by_left(&j).unwrap(); // FIXME: fails if glue not found + let i = *glue_name_map + .get_by_left(&i) + // FIXME: fails if glue not found + .expect(format!("Glue {} not found", i).as_str()); + + let j = *glue_name_map + .get_by_left(&j) + // FIXME: fails if glue not found + .expect(format!("Glue {} not found", j).as_str()); glue_delta_g[[i, j]] = v.0; glue_delta_g[[j, i]] = v.0; From 8f72ba66b4c4405c67295250676524dab96e93ed Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:49:50 +0100 Subject: [PATCH 036/117] allow for None in Python Description --- rgrow/src/models/sdc1d.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 8f5107b..cde90d4 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -600,18 +600,18 @@ impl From<(String, String)> for RefOrPair { #[derive(Debug)] #[cfg_attr(feature = "python", derive(pyo3::FromPyObject))] pub enum SingleOrMultiScaffold { - Single(Vec), - Multi(Vec>), + Single(Vec>), + Multi(Vec>>), } -impl From> for SingleOrMultiScaffold { - fn from(v: Vec) -> Self { +impl From>> for SingleOrMultiScaffold { + fn from(v: Vec>) -> Self { SingleOrMultiScaffold::Single(v) } } -impl From>> for SingleOrMultiScaffold { - fn from(v: Vec>) -> Self { +impl From>>> for SingleOrMultiScaffold { + fn from(v: Vec>>) -> Self { SingleOrMultiScaffold::Multi(v) } } @@ -734,10 +734,14 @@ impl SDC { let scaffold = match params.scaffold { SingleOrMultiScaffold::Single(s) => { let mut scaffold = Array2::::zeros((64, s.len())); - for (i, g) in s.iter().enumerate() { - scaffold - .index_axis_mut(ndarray::Axis(1), i) - .fill(*glue_name_map.get_by_left(g).unwrap()); + for (i, maybe_g) in s.iter().enumerate() { + if let Some(g) = maybe_g { + scaffold + .index_axis_mut(ndarray::Axis(1), i) + .fill(*glue_name_map.get_by_left(g).unwrap()); + } else { + scaffold.index_axis_mut(ndarray::Axis(1), i).fill(0); + } } scaffold } From 250ac14defb5f6b45b224096abc5d0dcdd39f39d Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:31:35 +0100 Subject: [PATCH 037/117] No need to put empty tile in the input --- rgrow/src/models/sdc1d.rs | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index cde90d4..085d6cc 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -658,11 +658,13 @@ impl SDC { pub fn from_params(params: SDCParams) -> Self { let mut glue_name_map = BiHashMap::::new(); let mut gluenum = 1; - let mut tile_glues_int = Array2::::zeros((params.tile_glues.len(), 3)); + let mut tile_glues_int = Array2::::zeros((params.tile_glues.len() + 1, 3)); - for (tgl, mut r) in - std::iter::zip(params.tile_glues.iter(), tile_glues_int.outer_iter_mut()) - { + for (tgl, mut r) in std::iter::zip( + params.tile_glues.iter(), + // The firs one will just be 0 + tile_glues_int.outer_iter_mut().skip(1), + ) { for (i, t) in tgl.iter().enumerate() { match t { None => r[i] = 0, @@ -748,23 +750,35 @@ impl SDC { SingleOrMultiScaffold::Multi(_m) => todo!(), }; - let colors = params + let mut more_colors: Vec<_> = params .tile_colors .iter() .map(|c| get_color_or_random(&c.as_ref().map(|x| x.as_str())).unwrap()) .collect(); + // Add color for empty tile + let mut colors = vec![[0, 0, 0, 0]]; + colors.append(&mut more_colors); + + let mut input_names: Vec<_> = params + .tile_names + .into_iter() + .enumerate() + .map(|(n, os)| os.unwrap_or(n.to_string())) + .collect(); + + let mut strand_names = vec!["empty".to_string()]; + strand_names.append(&mut input_names); + + let mut c = vec![0.0]; + c.extend(params.tile_concentration); + SDC::new( Vec::new(), - params - .tile_names - .into_iter() - .enumerate() - .map(|(n, os)| os.unwrap_or(n.to_string())) - .collect(), + strand_names, glue_names.into_iter().collect(), // FIXME: consider types here scaffold, - Array1::from(params.tile_concentration), + Array1::from(c), tile_glues_int, colors, params.k_f, From 003aaa56374afe6b98e6757bc04dd906975d45b1 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 21 Jun 2024 16:32:38 +0100 Subject: [PATCH 038/117] sdc: fix scaffold energy to do complement --- rgrow/src/models/sdc1d.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 085d6cc..2b7d78d 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -240,8 +240,11 @@ impl SDC { self.glue_links[(f_west_glue, s_east_glue)]; } - // Calculate the binding strength of the starnd with the scaffold - self.scaffold_energy_bonds[strand_f] = self.glue_links[(f_btm_glue, f_btm_glue)]; + let b_inverse = if f_btm_glue % 2 == 1 { f_btm_glue + 1 } else { f_btm_glue - 1 }; + + + // Calculate the binding strength of the strand with the scaffold + self.scaffold_energy_bonds[strand_f] = self.glue_links[(f_btm_glue, b_inverse)]; } } From d95edf2ee8a1ae3b4b3b567bc3a50cd3eb21ebae Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 21 Jun 2024 16:35:19 +0100 Subject: [PATCH 039/117] sdc: temperature get/set_param --- rgrow/src/models/sdc1d.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 2b7d78d..e108caf 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -456,6 +456,13 @@ impl System for SDC { self.update_system(); Ok(NeededUpdate::NonZero) } + "temperature" => { + let temperature = value + .downcast_ref::() + .ok_or(GrowError::WrongParameterType(name.to_string()))?; + self.change_temperature_to(*temperature); + Ok(NeededUpdate::NonZero) + } _ => Err(GrowError::NoParameter(name.to_string())), } } @@ -466,6 +473,7 @@ impl System for SDC { "strand_concentrations" => Ok(Box::new(self.strand_concentration.clone())), "glue_links" => Ok(Box::new(self.glue_links.clone())), "energy_bonds" => Ok(Box::new(self.strand_energy_bonds.clone())), + "temperature" => Ok(Box::new(self.temperature)), _ => Err(GrowError::NoParameter(name.to_string())), } } From 313d2b3a2e088ee13af6b2b7007b0be1901298be Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 21 Jun 2024 16:46:46 +0100 Subject: [PATCH 040/117] don't fill energy array for empty tile / tile 0 --- rgrow/src/models/sdc1d.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index e108caf..6a0b95b 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -209,9 +209,9 @@ impl SDC { /// Fill the energy_bonds array fn fill_energy_array(&mut self) { let num_of_strands = self.strand_names.len(); - + println!("{:?}", self); // For each *possible* pair of strands, calculate the energy bond - for strand_f in 0..(num_of_strands as usize) { + for strand_f in 1..(num_of_strands as usize) { // 1: no point in calculating for 0 let (f_west_glue, f_btm_glue, f_east_glue) = { let glues = self.glues.row(strand_f); ( @@ -240,6 +240,11 @@ impl SDC { self.glue_links[(f_west_glue, s_east_glue)]; } + // I suppose maybe we'd have weird strands with no position domain? + if f_btm_glue == 0 { + continue; + } + let b_inverse = if f_btm_glue % 2 == 1 { f_btm_glue + 1 } else { f_btm_glue - 1 }; From e7fda40194e8783f0a228893b517121a8421ac95 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 21 Jun 2024 17:24:20 +0100 Subject: [PATCH 041/117] enable sierpinski benchmark --- rgrow/Cargo.toml | 14 +++--- rgrow/benches/{ratestore.drs => ratestore.rs} | 4 +- .../benches/{sierpinski.drs => sierpinski.rs} | 46 +++++++++---------- 3 files changed, 32 insertions(+), 32 deletions(-) rename rgrow/benches/{ratestore.drs => ratestore.rs} (96%) rename rgrow/benches/{sierpinski.drs => sierpinski.rs} (56%) diff --git a/rgrow/Cargo.toml b/rgrow/Cargo.toml index db1fd4a..00f1e98 100644 --- a/rgrow/Cargo.toml +++ b/rgrow/Cargo.toml @@ -12,19 +12,19 @@ categories = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dev-dependencies] -criterion = "0.3" +criterion = { version = "0.5", features = ["html_reports"] } -# [[bench]] -# name = "sierpinski" -# harness = false +[[bench]] +name = "sierpinski" +harness = false # [[bench]] # name = "ratestore" # harness = false -[[bench]] -name = "ui" -harness = false +# [[bench]] +# name = "ui" +# harness = false [lib] name = "rgrow" diff --git a/rgrow/benches/ratestore.drs b/rgrow/benches/ratestore.rs similarity index 96% rename from rgrow/benches/ratestore.drs rename to rgrow/benches/ratestore.rs index 6239c1c..e1e1fb9 100644 --- a/rgrow/benches/ratestore.drs +++ b/rgrow/benches/ratestore.rs @@ -53,7 +53,7 @@ fn ratestore_qsta_update(c: &mut Criterion) { group.bench_with_input(BenchmarkId::new("single update", pn), &pv, |b, a| { b.iter(|| { for (p, r) in a.iter() { - rs_single.update_point(p.0, *r); + rs_single.update_point(*p, *r); } }) }); @@ -90,7 +90,7 @@ fn ratestore_qsta_update(c: &mut Criterion) { |b, a| { b.iter(|| { for (p, r) in a.iter() { - rs_single.update_point(p.0, *r); + rs_single.update_point(*p, *r); } }) }, diff --git a/rgrow/benches/sierpinski.drs b/rgrow/benches/sierpinski.rs similarity index 56% rename from rgrow/benches/sierpinski.drs rename to rgrow/benches/sierpinski.rs index dd7d134..025a8e8 100644 --- a/rgrow/benches/sierpinski.drs +++ b/rgrow/benches/sierpinski.rs @@ -36,29 +36,29 @@ fn raw_sim_run(c: &mut Criterion) { }); c.bench_function("evolve unistep", |b| { - b.iter(|| sys.state_step(&mut st, 1000000.)) + b.iter(|| sys.take_single_step(&mut st, 1000000.)) }); } - -fn sim_run(c: &mut Criterion) { - let mut ts = TileSet::from_file("examples/sierpinski.yaml").unwrap(); - - ts.seed = Some(Seed::Single(2045, 2045, 1.into())); - ts.size = Some(rgrow::tileset::Size::Single(2048)); - ts.model = Some(rgrow::tileset::Model::KTAM); - - let mut sim = TileSet::into_simulation(&ts).unwrap(); - sim.add_state().unwrap(); - - c.bench_function("evolve 10000 sim", |b| b.iter(|| sim.evolve(0, BOUNDS10K))); - - ts.model = Some(rgrow::tileset::Model::OldKTAM); - let mut sim = TileSet::into_simulation(&ts).unwrap(); - - sim.add_state().unwrap(); - - c.bench_function("evolve 10000 old", |b| b.iter(|| sim.evolve(0, BOUNDS10K))); -} - -criterion_group!(benches, raw_sim_run, sim_run); +// +// fn sim_run(c: &mut Criterion) { + // let mut ts = TileSet::from_file("examples/sierpinski.yaml").unwrap(); +// + // ts.seed = Some(Seed::Single(2045, 2045, 1.into())); + // ts.size = Some(rgrow::tileset::Size::Single(2048)); + // ts.model = Some(rgrow::tileset::Model::KTAM); +// + // let mut sim = TileSet::into_simulation(&ts).unwrap(); + // sim.add_state().unwrap(); +// + // c.bench_function("evolve 10000 sim", |b| b.iter(|| sim.evolve(0, BOUNDS10K))); +// + // ts.model = Some(rgrow::tileset::Model::OldKTAM); + // let mut sim = TileSet::into_simulation(&ts).unwrap(); +// + // sim.add_state().unwrap(); +// + // c.bench_function("evolve 10000 old", |b| b.iter(|| sim.evolve(0, BOUNDS10K))); +// } +// +criterion_group!(benches, raw_sim_run); criterion_main!(benches); From 3f08cb294ae4d56133d5f69a7dfc2cc1af301e6b Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 21 Jun 2024 22:33:41 +0100 Subject: [PATCH 042/117] remove unnecessary ?Sized --- rgrow/src/models/atam.rs | 24 ++++++++--------- rgrow/src/models/covers.rs | 22 ++++++++-------- rgrow/src/models/ktam.rs | 40 ++++++++++++++--------------- rgrow/src/models/ktam_fission.rs | 2 +- rgrow/src/models/oldktam.rs | 20 +++++++-------- rgrow/src/models/oldktam_fission.rs | 2 +- rgrow/src/models/sdc1d.rs | 24 ++++++++--------- rgrow/src/system.rs | 36 +++++++++++++------------- 8 files changed, 85 insertions(+), 85 deletions(-) diff --git a/rgrow/src/models/atam.rs b/rgrow/src/models/atam.rs index 0f403a5..55a09a8 100644 --- a/rgrow/src/models/atam.rs +++ b/rgrow/src/models/atam.rs @@ -93,7 +93,7 @@ unsafe impl Send for ATAM {} unsafe impl Sync for ATAM {} impl System for ATAM { - fn update_after_event(&self, state: &mut S, event: &Event) { + fn update_after_event(&self, state: &mut S, event: &Event) { match event { Event::None => todo!(), Event::MonomerAttachment(p, _) @@ -131,7 +131,7 @@ impl System for ATAM { } } - fn event_rate_at_point( + fn event_rate_at_point( &self, state: &S, p: crate::canvas::PointSafeHere, @@ -148,7 +148,7 @@ impl System for ATAM { } } - fn choose_event_at_point( + fn choose_event_at_point( &self, state: &S, p: PointSafe2, @@ -171,7 +171,7 @@ impl System for ATAM { } } - fn set_safe_point( + fn set_safe_point( &self, state: &mut S, point: PointSafe2, @@ -184,7 +184,7 @@ impl System for ATAM { self } - fn perform_event(&self, state: &mut S, event: &Event) -> &Self { + fn perform_event(&self, state: &mut S, event: &Event) -> &Self { match event { Event::None => panic!("Being asked to perform null event."), Event::MonomerAttachment(point, tile) => { @@ -308,7 +308,7 @@ impl System for ATAM { v } - fn calc_mismatch_locations(&self, state: &S) -> Array2 { + fn calc_mismatch_locations(&self, state: &S) -> Array2 { let threshold = self.threshold / 4.0; // FIXME: this is a hack let mut mismatch_locations = Array2::::zeros((state.nrows(), state.ncols())); @@ -417,7 +417,7 @@ impl ATAM { TileShape::Single } - pub fn total_monomer_attachment_rate_at_point( + pub fn total_monomer_attachment_rate_at_point( &self, state: &S, p: PointSafe2, @@ -428,7 +428,7 @@ impl ATAM { } } - pub fn choose_attachment_at_point( + pub fn choose_attachment_at_point( &self, state: &S, p: PointSafe2, @@ -437,7 +437,7 @@ impl ATAM { self.choose_monomer_attachment_at_point(state, p, acc) } - pub fn choose_monomer_attachment_at_point( + pub fn choose_monomer_attachment_at_point( &self, state: &S, p: PointSafe2, @@ -446,7 +446,7 @@ impl ATAM { self._find_monomer_attachment_possibilities_at_point(state, p, acc, false) } - fn _find_monomer_attachment_possibilities_at_point( + fn _find_monomer_attachment_possibilities_at_point( &self, state: &S, p: PointSafe2, @@ -531,7 +531,7 @@ impl ATAM { (false, acc, Event::None) } - pub fn bond_energy_of_tile_type_at_point_hypothetical( + pub fn bond_energy_of_tile_type_at_point_hypothetical( &self, state: &S, p: PointSafe2, @@ -579,7 +579,7 @@ impl ATAM { energy } - fn points_to_update_around( + fn points_to_update_around( &self, state: &S, p: &PointSafe2, diff --git a/rgrow/src/models/covers.rs b/rgrow/src/models/covers.rs index 9575194..0187973 100644 --- a/rgrow/src/models/covers.rs +++ b/rgrow/src/models/covers.rs @@ -47,7 +47,7 @@ pub struct StaticKTAMCover { } impl System for StaticKTAMCover { - fn update_after_event(&self, state: &mut S, event: &Event) { + fn update_after_event(&self, state: &mut S, event: &Event) { match event { Event::None => { panic!("Being asked to update after a dead event.") @@ -112,7 +112,7 @@ impl System for StaticKTAMCover { } } - fn event_rate_at_point(&self, state: &S, p: PointSafeHere) -> Rate { + fn event_rate_at_point(&self, state: &S, p: PointSafeHere) -> Rate { let t = state.v_sh(p); if !state.inbounds(p.0) { @@ -131,7 +131,7 @@ impl System for StaticKTAMCover { } } - fn choose_event_at_point( + fn choose_event_at_point( &self, state: &S, p: PointSafe2, @@ -158,11 +158,11 @@ impl System for StaticKTAMCover { self.inner.seed_locs() } - fn calc_mismatch_locations(&self, state: &S) -> Array2 { + fn calc_mismatch_locations(&self, state: &S) -> Array2 { self.inner.calc_mismatch_locations(state) } - fn take_single_step( + fn take_single_step( &self, state: &mut S, max_time_step: f64, @@ -185,7 +185,7 @@ impl System for StaticKTAMCover { StepOutcome::HadEventAt(time_step) } - fn set_safe_point( + fn set_safe_point( &self, state: &mut S, point: PointSafe2, @@ -200,7 +200,7 @@ impl System for StaticKTAMCover { self } - fn perform_event(&self, state: &mut S, event: &Event) -> &Self { + fn perform_event(&self, state: &mut S, event: &Event) -> &Self { match event { Event::None => panic!("Being asked to perform null event."), Event::MonomerAttachment(point, tile) | Event::MonomerChange(point, tile) => { @@ -235,7 +235,7 @@ impl SystemWithDimers for StaticKTAMCover { } impl StaticKTAMCover { - fn cover_to_composite_rate( + fn cover_to_composite_rate( &self, state: &S, p: PointSafe2, @@ -257,7 +257,7 @@ impl StaticKTAMCover { total_rate } - fn choose_cover_to_composite( + fn choose_cover_to_composite( &self, state: &S, p: PointSafe2, @@ -281,7 +281,7 @@ impl StaticKTAMCover { PossibleChoice::Remainder(acc) } - fn composite_to_cover_rate( + fn composite_to_cover_rate( &self, state: &S, p: PointSafe2, @@ -301,7 +301,7 @@ impl StaticKTAMCover { total_rate } - fn choose_composite_to_cover( + fn choose_composite_to_cover( &self, state: &S, p: PointSafe2, diff --git a/rgrow/src/models/ktam.rs b/rgrow/src/models/ktam.rs index 72b805b..a217f30 100644 --- a/rgrow/src/models/ktam.rs +++ b/rgrow/src/models/ktam.rs @@ -132,7 +132,7 @@ pub struct KTAM { } impl System for KTAM { - fn update_after_event(&self, state: &mut S, event: &Event) { + fn update_after_event(&self, state: &mut S, event: &Event) { match event { Event::None => todo!(), Event::MonomerAttachment(p, _) @@ -161,11 +161,11 @@ impl System for KTAM { } } - fn calc_n_tiles(&self, state: &S) -> crate::base::NumTiles { + fn calc_n_tiles(&self, state: &S) -> crate::base::NumTiles { state.calc_n_tiles_with_tilearray(&self.should_be_counted) } - fn event_rate_at_point( + fn event_rate_at_point( &self, state: &S, p: crate::canvas::PointSafeHere, @@ -205,7 +205,7 @@ impl System for KTAM { } } - fn choose_event_at_point( + fn choose_event_at_point( &self, state: &S, p: PointSafe2, @@ -228,7 +228,7 @@ impl System for KTAM { } } - fn perform_event(&self, state: &mut S, event: &Event) -> &Self { + fn perform_event(&self, state: &mut S, event: &Event) -> &Self { match event { Event::None => panic!("Being asked to perform null event."), Event::MonomerAttachment(point, tile) => { @@ -554,7 +554,7 @@ impl System for KTAM { self._seed_locs() } - fn calc_mismatch_locations(&self, state: &S) -> Array2 { + fn calc_mismatch_locations(&self, state: &S) -> Array2 { let threshold = 0.5; // Todo: fix this let mut mismatch_locations = Array2::::zeros((state.nrows(), state.ncols())); @@ -1046,7 +1046,7 @@ impl KTAM { } } - pub fn monomer_detachment_rate_at_point( + pub fn monomer_detachment_rate_at_point( &self, state: &S, p: PointSafe2, @@ -1086,7 +1086,7 @@ impl KTAM { v } - pub fn choose_detachment_at_point( + pub fn choose_detachment_at_point( &self, state: &S, p: PointSafe2, @@ -1210,7 +1210,7 @@ impl KTAM { return (false, acc, Event::None); } - pub fn total_monomer_attachment_rate_at_point( + pub fn total_monomer_attachment_rate_at_point( &self, state: &S, p: PointSafe2, @@ -1221,7 +1221,7 @@ impl KTAM { } } - pub fn choose_attachment_at_point( + pub fn choose_attachment_at_point( &self, state: &S, p: PointSafe2, @@ -1230,7 +1230,7 @@ impl KTAM { self.choose_monomer_attachment_at_point(state, p, acc) } - pub fn choose_monomer_attachment_at_point( + pub fn choose_monomer_attachment_at_point( &self, state: &S, p: PointSafe2, @@ -1239,14 +1239,14 @@ impl KTAM { self._find_monomer_attachment_possibilities_at_point(state, p, acc, false) } - pub fn setup_state(&self, state: &mut S) -> Result<(), GrowError> { + pub fn setup_state(&self, state: &mut S) -> Result<(), GrowError> { for (p, t) in self.seed_locs() { self.set_point(state, p.0, t)?; } Ok(()) } - fn _find_monomer_attachment_possibilities_at_point( + fn _find_monomer_attachment_possibilities_at_point( &self, state: &S, p: PointSafe2, @@ -1351,7 +1351,7 @@ impl KTAM { (false, acc, Event::None) } - pub fn bond_energy_of_tile_type_at_point( + pub fn bond_energy_of_tile_type_at_point( &self, state: &S, p: PointSafe2, @@ -1422,7 +1422,7 @@ impl KTAM { } } - fn _update_monomer_points(&self, state: &mut S, p: &PointSafe2) { + fn _update_monomer_points(&self, state: &mut S, p: &PointSafe2) { let points = [ ( state.move_sa_n(*p), @@ -1480,7 +1480,7 @@ impl KTAM { state.update_multiple(&points); } - fn points_to_update_around( + fn points_to_update_around( &self, state: &S, p: &PointSafe2, @@ -1537,7 +1537,7 @@ impl KTAM { } // Dimer detachment rates are written manually. - fn dimer_s_detach_rate( + fn dimer_s_detach_rate( &self, canvas: &C, p: PointSafeHere, @@ -1561,7 +1561,7 @@ impl KTAM { } // Dimer detachment rates are written manually. - fn dimer_e_detach_rate( + fn dimer_e_detach_rate( &self, canvas: &C, p: PointSafeHere, @@ -1584,7 +1584,7 @@ impl KTAM { } } - fn chunk_detach_rate(&self, canvas: &C, p: PointSafe2, t: Tile) -> Rate { + fn chunk_detach_rate(&self, canvas: &C, p: PointSafe2, t: Tile) -> Rate { match self.chunk_size { ChunkSize::Single => 0.0, ChunkSize::Dimer => { @@ -1595,7 +1595,7 @@ impl KTAM { } } - fn choose_chunk_detachment( + fn choose_chunk_detachment( &self, canvas: &C, p: PointSafe2, diff --git a/rgrow/src/models/ktam_fission.rs b/rgrow/src/models/ktam_fission.rs index 932eabf..c73725a 100644 --- a/rgrow/src/models/ktam_fission.rs +++ b/rgrow/src/models/ktam_fission.rs @@ -224,7 +224,7 @@ pub enum FissionResult { } impl KTAM { - pub fn determine_fission( + pub fn determine_fission( &self, canvas: &S, possible_start_points: &[PointSafe2], diff --git a/rgrow/src/models/oldktam.rs b/rgrow/src/models/oldktam.rs index 34aa5cf..e7b1863 100644 --- a/rgrow/src/models/oldktam.rs +++ b/rgrow/src/models/oldktam.rs @@ -206,7 +206,7 @@ impl OldKTAM { } } - pub(crate) fn points_to_update_around( + pub(crate) fn points_to_update_around( &self, state: &C, p: &PointSafe2, @@ -322,7 +322,7 @@ impl OldKTAM { /// Unsafe because does not check bounds of p: assumes inbounds (with border if applicable). /// This requires the tile to be specified because it is likely you've already accessed it. - pub(crate) fn bond_strength_of_tile_at_point( + pub(crate) fn bond_strength_of_tile_at_point( &self, canvas: &C, p: PointSafe2, @@ -348,7 +348,7 @@ impl OldKTAM { } // Dimer detachment rates are written manually. - fn dimer_s_detach_rate( + fn dimer_s_detach_rate( &self, canvas: &C, p: Point, @@ -371,7 +371,7 @@ impl OldKTAM { } // Dimer detachment rates are written manually. - fn dimer_e_detach_rate( + fn dimer_e_detach_rate( &self, canvas: &C, p: Point, @@ -393,7 +393,7 @@ impl OldKTAM { } } - fn chunk_detach_rate(&self, canvas: &C, p: Point, t: Tile) -> Rate { + fn chunk_detach_rate(&self, canvas: &C, p: Point, t: Tile) -> Rate { match self.chunk_size { ChunkSize::Single => 0.0, ChunkSize::Dimer => { @@ -404,7 +404,7 @@ impl OldKTAM { } } - fn choose_chunk_detachment( + fn choose_chunk_detachment( &self, canvas: &C, p: PointSafe2, @@ -480,7 +480,7 @@ impl OldKTAM { } impl System for OldKTAM { - fn event_rate_at_point(&self, canvas: &S, point: PointSafeHere) -> Rate { + fn event_rate_at_point(&self, canvas: &S, point: PointSafeHere) -> Rate { let p = if canvas.inbounds(point.0) { PointSafe2(point.0) } else { @@ -574,7 +574,7 @@ impl System for OldKTAM { } } - fn choose_event_at_point( + fn choose_event_at_point( &self, canvas: &S, p: PointSafe2, @@ -733,7 +733,7 @@ impl System for OldKTAM { v } - fn update_after_event(&self, state: &mut S, event: &Event) { + fn update_after_event(&self, state: &mut S, event: &Event) { match event { Event::None => { panic!("Being asked to update after a dead event.") @@ -798,7 +798,7 @@ impl System for OldKTAM { } } - fn calc_mismatch_locations(&self, state: &S) -> Array2 { + fn calc_mismatch_locations(&self, state: &S) -> Array2 { let threshold = 0.1; let mut arr = Array2::zeros(state.raw_array().raw_dim()); diff --git a/rgrow/src/models/oldktam_fission.rs b/rgrow/src/models/oldktam_fission.rs index 3c2b18b..dcb3269 100644 --- a/rgrow/src/models/oldktam_fission.rs +++ b/rgrow/src/models/oldktam_fission.rs @@ -224,7 +224,7 @@ pub enum FissionResult { } impl OldKTAM { - pub fn determine_fission( + pub fn determine_fission( &self, canvas: &C, possible_start_points: &[PointSafe2], diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 6a0b95b..8b7698d 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -178,7 +178,7 @@ impl SDC { self.update_system(); } - fn polymer_update(&self, points: &Vec, state: &mut S) { + fn polymer_update(&self, points: &Vec, state: &mut S) { let mut points_to_update = points .iter() .flat_map(|&point| { @@ -195,7 +195,7 @@ impl SDC { self.update_points(state, &points_to_update) } - fn update_monomer_point(&self, state: &mut S, scaffold_point: &PointSafe2) { + fn update_monomer_point(&self, state: &mut S, scaffold_point: &PointSafe2) { let points = [ state.move_sa_w(*scaffold_point), state.move_sa_e(*scaffold_point), @@ -253,7 +253,7 @@ impl SDC { } } - pub fn monomer_detachment_rate_at_point( + pub fn monomer_detachment_rate_at_point( &self, state: &S, scaffold_point: PointSafe2, @@ -275,7 +275,7 @@ impl SDC { self.kf * bond_energy.exp() } - pub fn choose_monomer_attachment_at_point( + pub fn choose_monomer_attachment_at_point( &self, state: &S, point: PointSafe2, @@ -284,7 +284,7 @@ impl SDC { self.find_monomer_attachment_possibilities_at_point(state, acc, point, false) } - pub fn choose_monomer_detachment_at_point( + pub fn choose_monomer_detachment_at_point( &self, state: &S, point: PointSafe2, @@ -303,7 +303,7 @@ impl SDC { /// |_ _ _ _ _ _ _ _ _ _ <- Scaffold /// | ^ point /// - fn find_monomer_attachment_possibilities_at_point( + fn find_monomer_attachment_possibilities_at_point( &self, state: &S, mut acc: Rate, @@ -333,7 +333,7 @@ impl SDC { (false, acc, Event::None) } - fn total_monomer_attachment_rate_at_poin( + fn total_monomer_attachment_rate_at_poin( &self, state: &S, scaffold_coord: PointSafe2, @@ -348,7 +348,7 @@ impl SDC { } /// Get the sum of the energies of the bonded strands (if any) - fn bond_energy_of_strand( + fn bond_energy_of_strand( &self, state: &S, scaffold_point: PointSafe2, @@ -366,7 +366,7 @@ impl SDC { } impl System for SDC { - fn update_after_event(&self, state: &mut St, event: &crate::system::Event) { + fn update_after_event(&self, state: &mut St, event: &crate::system::Event) { match event { Event::None => todo!(), Event::MonomerAttachment(scaffold_point, _) @@ -383,7 +383,7 @@ impl System for SDC { } } - fn event_rate_at_point( + fn event_rate_at_point( &self, state: &St, p: crate::canvas::PointSafeHere, @@ -401,7 +401,7 @@ impl System for SDC { } } - fn choose_event_at_point( + fn choose_event_at_point( &self, state: &St, point: crate::canvas::PointSafe2, @@ -427,7 +427,7 @@ impl System for SDC { } // TODO: Array containing locations to "bad connections" - fn calc_mismatch_locations(&self, state: &St) -> Array2 { + fn calc_mismatch_locations(&self, state: &St) -> Array2 { todo!() } diff --git a/rgrow/src/system.rs b/rgrow/src/system.rs index e641484..85031ba 100644 --- a/rgrow/src/system.rs +++ b/rgrow/src/system.rs @@ -267,11 +267,11 @@ pub trait System: Debug + Sync + Send + TileBondInfo { fn system_info(&self) -> String; - fn calc_n_tiles(&self, state: &St) -> NumTiles { + fn calc_n_tiles(&self, state: &St) -> NumTiles { state.calc_n_tiles() } - fn take_single_step( + fn take_single_step( &self, state: &mut St, max_time_step: f64, @@ -295,7 +295,7 @@ pub trait System: Debug + Sync + Send + TileBondInfo { StepOutcome::HadEventAt(time_step) } - fn evolve( + fn evolve( &self, state: &mut St, bounds: EvolveBounds, @@ -365,7 +365,7 @@ pub trait System: Debug + Sync + Send + TileBondInfo { .collect() } - fn set_point( + fn set_point( &self, state: &mut St, point: Point, @@ -378,7 +378,7 @@ pub trait System: Debug + Sync + Send + TileBondInfo { } } - fn set_safe_point( + fn set_safe_point( &self, state: &mut St, point: PointSafe2, @@ -392,7 +392,7 @@ pub trait System: Debug + Sync + Send + TileBondInfo { self } - fn set_points( + fn set_points( &self, state: &mut St, changelist: &[(Point, Tile)], @@ -411,7 +411,7 @@ pub trait System: Debug + Sync + Send + TileBondInfo { self } - fn set_safe_points( + fn set_safe_points( &self, state: &mut St, changelist: &[(PointSafe2, Tile)], @@ -425,7 +425,7 @@ pub trait System: Debug + Sync + Send + TileBondInfo { self } - fn configure_empty_state(&self, state: &mut St) -> Result<(), GrowError> { + fn configure_empty_state(&self, state: &mut St) -> Result<(), GrowError> { for (p, t) in self.seed_locs() { self.set_point(state, p.0, t)?; } @@ -434,7 +434,7 @@ pub trait System: Debug + Sync + Send + TileBondInfo { /// Perform a particular event/change to a state. Do not update the state's time/etc, /// or rates, which should be done in update_after_event and take_single_step. - fn perform_event(&self, state: &mut St, event: &Event) -> &Self { + fn perform_event(&self, state: &mut St, event: &Event) -> &Self { match event { Event::None => panic!("Being asked to perform null event."), Event::MonomerAttachment(point, tile) | Event::MonomerChange(point, tile) => { @@ -457,14 +457,14 @@ pub trait System: Debug + Sync + Send + TileBondInfo { self } - fn update_after_event(&self, state: &mut St, event: &Event); + fn update_after_event(&self, state: &mut St, event: &Event); /// Returns the total event rate at a given point. These should correspond with the events chosen by `choose_event_at_point`. - fn event_rate_at_point(&self, state: &St, p: PointSafeHere) -> Rate; + fn event_rate_at_point(&self, state: &St, p: PointSafeHere) -> Rate; /// Given a point, and an accumulated random rate choice `acc` (which should be less than the total rate at the point), /// return the event that should take place. - fn choose_event_at_point( + fn choose_event_at_point( &self, state: &St, p: PointSafe2, @@ -475,15 +475,15 @@ pub trait System: Debug + Sync + Send + TileBondInfo { fn seed_locs(&self) -> Vec<(PointSafe2, Tile)>; /// Returns an array of mismatch locations. At each point, mismatches are designated by 8*N+4*E+2*S+1*W. - fn calc_mismatch_locations(&self, state: &St) -> Array2; + fn calc_mismatch_locations(&self, state: &St) -> Array2; - fn calc_mismatches(&self, state: &St) -> usize { + fn calc_mismatches(&self, state: &St) -> usize { let mut arr = self.calc_mismatch_locations(state); arr.map_inplace(|x| *x = (*x & 0b01) + ((*x & 0b10) / 2)); arr.sum() } - fn update_points(&self, state: &mut St, points: &[PointSafeHere]) { + fn update_points(&self, state: &mut St, points: &[PointSafeHere]) { let p = points .iter() .map(|p| (*p, self.event_rate_at_point(state, *p))) @@ -492,7 +492,7 @@ pub trait System: Debug + Sync + Send + TileBondInfo { state.update_multiple(&p); } - fn update_all(&self, state: &mut St, needed: &NeededUpdate) { + fn update_all(&self, state: &mut St, needed: &NeededUpdate) { let ncols = state.ncols(); let nrows = state.nrows(); @@ -519,7 +519,7 @@ pub trait System: Debug + Sync + Send + TileBondInfo { } #[cfg(not(feature = "ui"))] - fn evolve_in_window( + fn evolve_in_window( &self, _state: &mut St, _block: Option, @@ -529,7 +529,7 @@ pub trait System: Debug + Sync + Send + TileBondInfo { } #[cfg(feature = "ui")] - fn evolve_in_window( + fn evolve_in_window( &self, state: &mut St, block: Option, From d0adc73f76152eae4824af5e81b8d3f7dc0d9685 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 21 Jun 2024 22:57:35 +0100 Subject: [PATCH 043/117] ktam optimization --- rgrow/benches/{ratestore.rs => ratestore.drs} | 2 +- rgrow/src/models/ktam.rs | 115 +++++++----------- rgrow/src/system.rs | 2 +- 3 files changed, 47 insertions(+), 72 deletions(-) rename rgrow/benches/{ratestore.rs => ratestore.drs} (98%) diff --git a/rgrow/benches/ratestore.rs b/rgrow/benches/ratestore.drs similarity index 98% rename from rgrow/benches/ratestore.rs rename to rgrow/benches/ratestore.drs index e1e1fb9..891b949 100644 --- a/rgrow/benches/ratestore.rs +++ b/rgrow/benches/ratestore.drs @@ -39,7 +39,7 @@ fn ratestore_qsta_update(c: &mut Criterion) { ("all_shuffle", &allchanges_shuffled[..]), ] { group.bench_with_input(BenchmarkId::new("small update", pn), &pv, |b, a| { - b.iter(|| rs._update_multiple_small(a)) + b.iter(|| rs._update_multiple_small(a.clone())) }); group.bench_with_input(BenchmarkId::new("large update", pn), &pv, |b, a| { diff --git a/rgrow/src/models/ktam.rs b/rgrow/src/models/ktam.rs index a217f30..f8956be 100644 --- a/rgrow/src/models/ktam.rs +++ b/rgrow/src/models/ktam.rs @@ -847,8 +847,8 @@ impl KTAM { k_f: Option, seed: Option, fission_handling: Option, - chunk_handling: Option, - chunk_size: Option, + chunk_handling: Option, + chunk_size: Option, tile_names: Option>, tile_colors: Option>, ) -> Self { @@ -1046,11 +1046,7 @@ impl KTAM { } } - pub fn monomer_detachment_rate_at_point( - &self, - state: &S, - p: PointSafe2, - ) -> Rate { + pub fn monomer_detachment_rate_at_point(&self, state: &S, p: PointSafe2) -> Rate { // If the point is a seed, then there is no detachment rate. // ODD HACK: we set a very low detachment rate for seeds and duple bottom/right, to allow // rate-based copying. We ignore these below. @@ -1097,7 +1093,7 @@ impl KTAM { // FIXME: may slow things down if self.is_seed(p) || ((self.has_duples) && self.is_fake_duple(state.tile_at_point(p))) { - return (true, acc, Event::None) + return (true, acc, Event::None); } else { let mut possible_starts = Vec::new(); let mut now_empty = Vec::new(); @@ -1183,7 +1179,9 @@ impl KTAM { //println!("Fission handling {:?} {:?} {:?} {:?} {:?} {:?} {:?} {:?} {:?} {:?}", p, tile, possible_starts, now_empty, tn, te, ts, tw, canvas.calc_ntiles(), g.map.len()); match self.fission_handling { FissionHandling::NoFission => (true, acc, Event::None), - FissionHandling::JustDetach => (true, acc, Event::PolymerDetachment(now_empty)), + FissionHandling::JustDetach => { + (true, acc, Event::PolymerDetachment(now_empty)) + } FissionHandling::KeepSeeded => { let sl = self._seed_locs(); ( @@ -1204,7 +1202,7 @@ impl KTAM { ), } } - } + }; } return (false, acc, Event::None); @@ -1423,68 +1421,45 @@ impl KTAM { } fn _update_monomer_points(&self, state: &mut S, p: &PointSafe2) { - let points = [ - ( - state.move_sa_n(*p), - self.event_rate_at_point(state, state.move_sa_n(*p)), - ), - ( - state.move_sa_w(*p), - self.event_rate_at_point(state, state.move_sa_w(*p)), - ), - ( - PointSafeHere(p.0), - self.event_rate_at_point(state, PointSafeHere(p.0)), - ), - ( - state.move_sa_e(*p), - self.event_rate_at_point(state, state.move_sa_e(*p)), - ), - ( - state.move_sa_s(*p), - self.event_rate_at_point(state, state.move_sa_s(*p)), - ), - ( - state.move_sa_nn(*p), - self.event_rate_at_point(state, state.move_sa_nn(*p)), - ), - ( - state.move_sa_ne(*p), - self.event_rate_at_point(state, state.move_sa_ne(*p)), - ), - ( - state.move_sa_ee(*p), - self.event_rate_at_point(state, state.move_sa_ee(*p)), - ), - ( - state.move_sa_se(*p), - self.event_rate_at_point(state, state.move_sa_se(*p)), - ), - ( - state.move_sa_ss(*p), - self.event_rate_at_point(state, state.move_sa_ss(*p)), - ), - ( - state.move_sa_sw(*p), - self.event_rate_at_point(state, state.move_sa_sw(*p)), - ), - ( - state.move_sa_ww(*p), - self.event_rate_at_point(state, state.move_sa_ww(*p)), - ), - ( - state.move_sa_nw(*p), - self.event_rate_at_point(state, state.move_sa_nw(*p)), - ), - ]; - state.update_multiple(&points); + #[inline(always)] + fn point_and_rate( + sys: &KTAM, + state: &S, + p: PointSafeHere, + ) -> (PointSafeHere, Rate) { + (p, sys.event_rate_at_point(state, p)) + } + + if (!self.has_duples) & (self.chunk_size == ChunkSize::Single) { + let points = [ + point_and_rate(self, state, state.move_sa_n(*p)), + point_and_rate(self, state, state.move_sa_w(*p)), + point_and_rate(self, state, PointSafeHere(p.0)), + point_and_rate(self, state, state.move_sa_e(*p)), + point_and_rate(self, state, state.move_sa_s(*p)), + ]; + state.update_multiple(&points); + } else { + let points = [ + point_and_rate(self, state, state.move_sa_n(*p)), + point_and_rate(self, state, state.move_sa_w(*p)), + point_and_rate(self, state, PointSafeHere(p.0)), + point_and_rate(self, state, state.move_sa_e(*p)), + point_and_rate(self, state, state.move_sa_s(*p)), + point_and_rate(self, state, state.move_sa_nn(*p)), + point_and_rate(self, state, state.move_sa_ne(*p)), + point_and_rate(self, state, state.move_sa_ee(*p)), + point_and_rate(self, state, state.move_sa_se(*p)), + point_and_rate(self, state, state.move_sa_ss(*p)), + point_and_rate(self, state, state.move_sa_sw(*p)), + point_and_rate(self, state, state.move_sa_ww(*p)), + point_and_rate(self, state, state.move_sa_nw(*p)), + ]; + state.update_multiple(&points); + } } - fn points_to_update_around( - &self, - state: &S, - p: &PointSafe2, - ) -> Vec { + fn points_to_update_around(&self, state: &S, p: &PointSafe2) -> Vec { match self.chunk_size { ChunkSize::Single => { let mut points = Vec::with_capacity(13); diff --git a/rgrow/src/system.rs b/rgrow/src/system.rs index 85031ba..b5613d1 100644 --- a/rgrow/src/system.rs +++ b/rgrow/src/system.rs @@ -234,7 +234,7 @@ impl TryFrom<&str> for ChunkHandling { } } -#[derive(Serialize, Deserialize, Clone, Copy, Debug)] +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "python", pyclass)] pub enum ChunkSize { #[serde(alias = "single")] From 54ca0f0040dcced525c1498063a94d8948ea8581 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 21 Jun 2024 23:47:33 +0100 Subject: [PATCH 044/117] remove debug println, add python dgds --- py-rgrow/src/lib.rs | 2 ++ rgrow/src/models/sdc1d.rs | 1 - rgrow/src/utils.rs | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/py-rgrow/src/lib.rs b/py-rgrow/src/lib.rs index 945ad30..1250a88 100644 --- a/py-rgrow/src/lib.rs +++ b/py-rgrow/src/lib.rs @@ -17,5 +17,7 @@ fn pyrgrow(m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_function(wrap_pyfunction!(rgrow::utils::string_dna_dg_ds, m)?)?; + Ok(()) } diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 8b7698d..e2eba08 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -209,7 +209,6 @@ impl SDC { /// Fill the energy_bonds array fn fill_energy_array(&mut self) { let num_of_strands = self.strand_names.len(); - println!("{:?}", self); // For each *possible* pair of strands, calculate the energy bond for strand_f in 1..(num_of_strands as usize) { // 1: no point in calculating for 0 let (f_west_glue, f_btm_glue, f_east_glue) = { diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index 7215c5e..9b1e4c0 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "python")] +use pyo3::prelude::*; + /* * A G A A A * ---------> @@ -96,6 +99,11 @@ fn dna_dg_ds(dna: impl Iterator) -> (f64, f64) { .expect("DNA must have length of at least 2") } +#[cfg_attr(feature = "python", pyfunction)] +pub fn string_dna_dg_ds(dna_sequence: &str) -> (f64, f64) { + dna_dg_ds(dna_sequence.chars().map(DnaNucleotideBase::from)) +} + /// Get delta g for some string dna sequence and its "perfect match". For example: /// /// ```rust From 417cbeda4b991c5e3c4d89be6bd043903e1b6771 Mon Sep 17 00:00:00 2001 From: angelcerveraroldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Sun, 23 Jun 2024 19:06:38 +0100 Subject: [PATCH 045/117] added missing underscore --- rgrow/src/models/ktam.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rgrow/src/models/ktam.rs b/rgrow/src/models/ktam.rs index 3c581f5..36e06d0 100644 --- a/rgrow/src/models/ktam.rs +++ b/rgrow/src/models/ktam.rs @@ -138,7 +138,7 @@ impl System for KTAM { Event::MonomerAttachment(p, _) | Event::MonomerDetachment(p) | Event::MonomerChange(p, _) => { - self.update_monomer_points(state, *p); + self._update_monomer_points(state, p); } Event::PolymerDetachment(v) => { let mut points = Vec::new(); From 50505fa654926c177e26ded8901a0e93f9c369e8 Mon Sep 17 00:00:00 2001 From: angelcerveraroldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Mon, 24 Jun 2024 10:35:48 +0100 Subject: [PATCH 046/117] easier python interface -- no need for user to keep track of strand index --- rgrow/src/models/sdc1d.rs | 249 +++++++++++++++++++++----------------- 1 file changed, 137 insertions(+), 112 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index e2eba08..6551cc8 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -210,7 +210,8 @@ impl SDC { fn fill_energy_array(&mut self) { let num_of_strands = self.strand_names.len(); // For each *possible* pair of strands, calculate the energy bond - for strand_f in 1..(num_of_strands as usize) { // 1: no point in calculating for 0 + for strand_f in 1..(num_of_strands as usize) { + // 1: no point in calculating for 0 let (f_west_glue, f_btm_glue, f_east_glue) = { let glues = self.glues.row(strand_f); ( @@ -244,8 +245,11 @@ impl SDC { continue; } - let b_inverse = if f_btm_glue % 2 == 1 { f_btm_glue + 1 } else { f_btm_glue - 1 }; - + let b_inverse = if f_btm_glue % 2 == 1 { + f_btm_glue + 1 + } else { + f_btm_glue - 1 + }; // Calculate the binding strength of the strand with the scaffold self.scaffold_energy_bonds[strand_f] = self.glue_links[(f_btm_glue, b_inverse)]; @@ -631,13 +635,24 @@ impl From>>> for SingleOrMultiScaffold { } } +#[derive(Debug)] +#[cfg_attr(feature = "python", derive(pyo3::FromPyObject))] +pub struct SDCStrand { + pub name: Option, + pub color: Option, + pub concentration: f64, + + // this may be slightly better, since this way we know that the user wont + // enter too many glues, eg an array of 5 glues + pub btm_glue: Option, + pub left_glue: Option, + pub right_glue: Option, +} + #[derive(Debug)] #[cfg_attr(feature = "python", derive(pyo3::FromPyObject))] pub struct SDCParams { - pub tile_glues: Vec>>, - pub tile_concentration: Vec, - pub tile_names: Vec>, - pub tile_colors: Vec>, + pub strands: Vec, pub scaffold: SingleOrMultiScaffold, // Pair with delta G at 37 degrees C and delta S pub glue_dg_s: HashMap, @@ -652,67 +667,122 @@ pub struct SDCParams { /// x: Original input but parsed so that there can be no errors in it (eg. No h**) /// y: From (eg. h) /// z: Inverse (eg. h*) -fn self_and_inverse(value: &String) -> (String, String, String) { +fn self_and_inverse(value: &String) -> (bool, String, String) { // Remove all the stars at the end let filtered = value.trim_end_matches("*"); let star_count = value.len() - filtered.len(); - let simplified = if star_count % 2 == 0 { - filtered.to_string() - } else { - format!("{}*", filtered.to_string()) - }; + let is_from = star_count % 2 == 0; ( - simplified, + is_from, filtered.to_string(), format!("{}*", filtered.to_string()), ) } +fn get_or_generate( + map: &mut HashMap, + count: &mut usize, + val: Option, +) -> usize { + // If the user didnt prove a glue, we assume nothign will ever stick + let str = match val { + Some(x) => x, + None => return 0, + }; + + // If we have already generated an id for this glue, then we use it + 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 SDC { pub fn from_params(params: SDCParams) -> Self { - let mut glue_name_map = BiHashMap::::new(); + let mut glue_name_map: HashMap = HashMap::new(); + + // Add one to account for the empty strand + let strand_count = params.strands.len() + 1; + + let mut strand_names: Vec = Vec::with_capacity(strand_count); + let mut strand_colors: Vec<[u8; 4]> = Vec::with_capacity(strand_count); + let mut strand_concentration = Array1::::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::::zeros((strand_count + 1, 3)); let mut gluenum = 1; - let mut tile_glues_int = Array2::::zeros((params.tile_glues.len() + 1, 3)); - - for (tgl, mut r) in std::iter::zip( - params.tile_glues.iter(), - // The firs one will just be 0 - tile_glues_int.outer_iter_mut().skip(1), - ) { - for (i, t) in tgl.iter().enumerate() { - match t { - None => r[i] = 0, - Some(s) => { - let (s, s_base, s_to) = self_and_inverse(s); - let j = glue_name_map.get_by_left(&s); - match j { - Some(j) => { - r[i] = *j; - } - None => { - glue_name_map.insert(s_base, gluenum); - glue_name_map.insert(s_to, gluenum + 1); - r[i] = gluenum; - - // The right answer here would be gluenum+1, so add one - if s.ends_with('*') { - r[i] += 1 - } - - gluenum += 2; - } - } + + for ( + id, + SDCStrand { + name, + color, + concentration, + left_glue, + btm_glue, + right_glue, + }, + ) in params.strands.into_iter().enumerate() + { + // Add the name and the color + strand_names.push(name.unwrap_or(id.to_string())); + + let color_as_str = color.as_ref().map(|x| x.as_str()); + let color_or_rand = get_color_or_random(&color_as_str).unwrap(); + strand_colors.push(color_or_rand); + + // Add the glues, note that we want to leave idnex (0, _) empty (for the empty tile) + glues[(id + 1, WEST_GLUE_INDEX)] = + get_or_generate(&mut glue_name_map, &mut gluenum, left_glue); + glues[(id + 1, BOTTOM_GLUE_INDEX)] = + get_or_generate(&mut glue_name_map, &mut gluenum, btm_glue); + glues[(id + 1, EAST_GLUE_INDEX)] = + get_or_generate(&mut glue_name_map, &mut gluenum, right_glue); + + // Add the concentrations + strand_concentration[id + 1] = concentration; + } + + let scaffold = match params.scaffold { + SingleOrMultiScaffold::Single(s) => { + let mut scaffold = Array2::::zeros((64, s.len())); + for (i, maybe_g) in s.iter().enumerate() { + if let Some(g) = maybe_g { + scaffold + .index_axis_mut(ndarray::Axis(1), i) + .fill(*glue_name_map.get(g).unwrap()); + } else { + scaffold.index_axis_mut(ndarray::Axis(1), i).fill(0); } } + scaffold } - } + SingleOrMultiScaffold::Multi(_m) => todo!(), + }; - let max_gluenum = gluenum; + let mut glue_names = vec![String::default(); gluenum]; + for (s, i) in glue_name_map.iter() { + glue_names[*i] = s.clone(); + } // Delta G at 37 degrees C - let mut glue_delta_g = Array2::::zeros((max_gluenum, max_gluenum)); - let mut glue_s = Array2::::zeros((max_gluenum, max_gluenum)); + let mut glue_delta_g = Array2::::zeros((gluenum, gluenum)); + let mut glue_s = Array2::::zeros((gluenum, gluenum)); for (k, &v) in params.glue_dg_s.iter() { let (i, j) = match k { @@ -721,20 +791,19 @@ impl SDC { (base, inverse) } RefOrPair::Pair(r1, r2) => { - let (r1, _, _) = self_and_inverse(r1); - let (r2, _, _) = self_and_inverse(r2); - (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 }) } }; + // FIXME: fails if glue not found let i = *glue_name_map - .get_by_left(&i) - // FIXME: fails if glue not found + .get(&i) .expect(format!("Glue {} not found", i).as_str()); let j = *glue_name_map - .get_by_left(&j) - // FIXME: fails if glue not found + .get(&j) .expect(format!("Glue {} not found", j).as_str()); glue_delta_g[[i, j]] = v.0; @@ -743,59 +812,15 @@ impl SDC { glue_s[[j, i]] = v.1; } - let mut glue_names = Array1::::from_elem(max_gluenum + 1, "".to_string()); - for (s, i) in glue_name_map.iter() { - glue_names[*i] = s.clone(); - } - - let scaffold = match params.scaffold { - SingleOrMultiScaffold::Single(s) => { - let mut scaffold = Array2::::zeros((64, s.len())); - for (i, maybe_g) in s.iter().enumerate() { - if let Some(g) = maybe_g { - scaffold - .index_axis_mut(ndarray::Axis(1), i) - .fill(*glue_name_map.get_by_left(g).unwrap()); - } else { - scaffold.index_axis_mut(ndarray::Axis(1), i).fill(0); - } - } - scaffold - } - SingleOrMultiScaffold::Multi(_m) => todo!(), - }; - - let mut more_colors: Vec<_> = params - .tile_colors - .iter() - .map(|c| get_color_or_random(&c.as_ref().map(|x| x.as_str())).unwrap()) - .collect(); - - // Add color for empty tile - let mut colors = vec![[0, 0, 0, 0]]; - colors.append(&mut more_colors); - - let mut input_names: Vec<_> = params - .tile_names - .into_iter() - .enumerate() - .map(|(n, os)| os.unwrap_or(n.to_string())) - .collect(); - - let mut strand_names = vec!["empty".to_string()]; - strand_names.append(&mut input_names); - - let mut c = vec![0.0]; - c.extend(params.tile_concentration); - SDC::new( - Vec::new(), + // TODO: anchor tiles + vec![], strand_names, - glue_names.into_iter().collect(), // FIXME: consider types here + glue_names, scaffold, - Array1::from(c), - tile_glues_int, - colors, + strand_concentration, + glues, + strand_colors, params.k_f, glue_delta_g, glue_s, @@ -868,16 +893,16 @@ mod test_sdc_model { let acc = input .into_iter() .map(|str| self_and_inverse(&str.to_string())) - .collect::>(); + .collect::>(); let expected = vec![ - ("some*str", "some*str", "some*str*"), - ("some*str*", "some*str", "some*str*"), - ("some*str", "some*str", "some*str*"), + (true, "some*str", "some*str*"), + (false, "some*str", "some*str*"), + (true, "some*str", "some*str*"), ] .iter() - .map(|(a, b, c)| (a.to_string(), b.to_string(), c.to_string())) - .collect::>(); + .map(|(a, b, c)| (*a, b.to_string(), c.to_string())) + .collect::>(); assert_eq!(acc, expected); } From 143eed19b374b711f1752265fc850992fc5f0c56 Mon Sep 17 00:00:00 2001 From: angelcerveraroldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:47:54 +0100 Subject: [PATCH 047/117] Allow user to set glue to its DNA sequence On the python side, allow the user to specify the DNA sequence of a glue instead of making them calculate the values for delta g and delta s --- rgrow/src/models/sdc1d.rs | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 6551cc8..ad89648 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -27,6 +27,7 @@ use crate::{ state::State, system::{Event, NeededUpdate, System, TileBondInfo}, tileset::{FromTileSet, ProcessedTileSet, Size}, + utils, }; use ndarray::prelude::{Array1, Array2}; @@ -592,8 +593,6 @@ impl FromTileSet for SDC { use std::hash::Hash; -use bimap::BiHashMap; - #[cfg(python)] use pyo3::prelude::*; @@ -649,13 +648,27 @@ pub struct SDCStrand { pub right_glue: Option, } +#[derive(Debug)] +#[cfg_attr(feature = "python", derive(pyo3::FromPyObject))] +pub enum GsOrSeq { + GS((f64, f64)), + Seq(String), +} + +fn gsorseq_to_gs(gsorseq: &GsOrSeq) -> (f64, f64) { + match gsorseq { + GsOrSeq::GS(x) => *x, + 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, pub scaffold: SingleOrMultiScaffold, // Pair with delta G at 37 degrees C and delta S - pub glue_dg_s: HashMap, + pub glue_dg_s: HashMap, pub k_f: f64, pub k_n: f64, pub k_c: f64, @@ -784,7 +797,10 @@ impl SDC { let mut glue_delta_g = Array2::::zeros((gluenum, gluenum)); let mut glue_s = Array2::::zeros((gluenum, gluenum)); - for (k, &v) in params.glue_dg_s.iter() { + for (k, gs_or_dna_sequence) in params.glue_dg_s.iter() { + // here we handle the fact that the user may have input (g, s) or TCGTA... + let gs = gsorseq_to_gs(gs_or_dna_sequence); + let (i, j) = match k { RefOrPair::Ref(r) => { let (_, base, inverse) = self_and_inverse(r); @@ -806,10 +822,10 @@ impl SDC { .get(&j) .expect(format!("Glue {} not found", j).as_str()); - glue_delta_g[[i, j]] = v.0; - glue_delta_g[[j, i]] = v.0; - glue_s[[i, j]] = v.1; - glue_s[[j, i]] = v.1; + glue_delta_g[[i, j]] = gs.0; + glue_delta_g[[j, i]] = gs.0; + glue_s[[i, j]] = gs.1; + glue_s[[j, i]] = gs.1; } SDC::new( From 7b354449038602c59746558342c751370932d59e Mon Sep 17 00:00:00 2001 From: angelcerveraroldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:17:57 +0100 Subject: [PATCH 048/117] bit copy notebook --- examples/sdc/bit_copy.ipynb | 308 ++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 examples/sdc/bit_copy.ipynb diff --git a/examples/sdc/bit_copy.ipynb b/examples/sdc/bit_copy.ipynb new file mode 100644 index 0000000..9ee5b1f --- /dev/null +++ b/examples/sdc/bit_copy.ipynb @@ -0,0 +1,308 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "9a5cb8be-399c-4ac9-bd12-b7128dcf6c11", + "metadata": {}, + "outputs": [], + "source": [ + "import rgrow as rg\n", + "from typing import List, Tuple, Optional\n", + "import numpy as np\n", + "\n", + "debug = False" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d47e2a60-fd15-4edc-a5fe-b7fe0d013ef8", + "metadata": {}, + "outputs": [], + "source": [ + "class SDCStrand:\n", + " def __init__(self, concentration, left_glue = None, btm_glue = None, right_glue = None, name = None, color = None,):\n", + " self.concentration = concentration\n", + " self.name = name\n", + " self.color = color\n", + " self.left_glue = left_glue\n", + " self.btm_glue = btm_glue\n", + " self.right_glue = right_glue\n", + "\n", + "class SDCParams:\n", + " def __init__(self, kf, kn, kc, temperature, glue_dg_s, scaffold, strands):\n", + " self.k_f = kf\n", + " self.k_n = kn\n", + " self.k_c = kc\n", + " self.glue_dg_s = glue_dg_s\n", + " self.temperature = temperature\n", + " self.scaffold = scaffold\n", + " self.strands = strands" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "id": "dea128db-8368-4581-b6f7-6432a2fe98d5", + "metadata": {}, + "outputs": [], + "source": [ + "params = SDCParams(\n", + " 1e6, 1e5, 1e4, 37.0,\n", + " { \n", + " \"0\": (-10.0, 0.0),\n", + " \"1\": (-10.0, 0.0), \n", + " \"2\": (-10.0, 0.0), \n", + " # this will get converted to (-27.299999999999994, -0.4858999999999999)\n", + " \"btm\": \"GAGGGGGATTCAATGAATATTTAT\",\n", + " },\n", + " [None, None, \"btm\", \"btm\", \"btm\", \"btm\", \"btm\", None, None],\n", + " [\n", + " SDCStrand(500e-3, \"0\", \"btm*\", \"0*\" , \"0\", \"blue\"),\n", + " SDCStrand(500e-3, \"1\" , \"btm*\", \"1*\" , \"1\", \"red\"),\n", + " SDCStrand(500e-3, \"2\" , \"btm*\", \"2*\" , \"2\", \"yellow\"),\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "e1ef23e4-d525-462c-bf64-c1619c029c80", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAoy0lEQVR4nO3de5DddX3/8ddy1mxSjMgtkMgvELHhkgUq1wmxioLYNDLSzoBanEbiOG2yEWJGq9hRkloM2GkGR5gI1AZnbASqBqwzQAMtyTCYkgTDJJayYhGsAqkdjRJ1IWfP748IspCEnJDz+R72+3jM7OzsmbP5vN97ziFPzp7s9rRarVYAAOi4/aoeAACgLoQXAEAhwgsAoBDhBQBQiPACAChEeAEAFCK8AAAKEV4AAIUILwCAQoQXAEAhwmsXrr322hx11FEZO3ZszjjjjNx///1Vj9Rxa9asyXnnnZdJkyalp6cnt956a9UjddySJUty2mmnZfz48ZkwYULOP//8PPzww1WP1XHLli3LiSeemNe97nV53etel+nTp+f222+veqyirrzyyvT09GTBggVVj9JRixYtSk9Pz4i3Y489tuqxivjxj3+cD3zgAzn44IMzbty4nHDCCVm/fn3VY3XUUUcd9ZLbu6enJwMDA1WP1jHNZjOf/vSnM2XKlIwbNy5HH310PvvZz6ZbfyOi8NqJm2++OQsXLszll1+eBx54ICeddFLe9a53ZcuWLVWP1lHbtm3LSSedlGuvvbbqUYpZvXp1BgYGsnbt2qxatSrPPvtszj333Gzbtq3q0TrqiCOOyJVXXpkNGzZk/fr1ecc73pH3vOc9+d73vlf1aEWsW7cu1113XU488cSqRyli2rRpeeKJJ55/u/fee6seqeN+9rOfZcaMGXnNa16T22+/Pf/5n/+Zv//7v8+BBx5Y9WgdtW7duhG39apVq5IkF1xwQcWTdc5VV12VZcuW5ZprrslDDz2Uq666Kp///OfzxS9+serRdq7FS5x++umtgYGB5z9uNputSZMmtZYsWVLhVGUlaa1cubLqMYrbsmVLK0lr9erVVY9S3IEHHtj6h3/4h6rH6Lhf/vKXrd///d9vrVq1qvW2t72tdemll1Y9UkddfvnlrZNOOqnqMYr7xCc+0XrLW95S9RiVu/TSS1tHH310a3h4uOpROmbWrFmtOXPmjLjsT//0T1sXXXRRRRPtnme8XuSZZ57Jhg0bcs455zx/2X777Zdzzjkn3/nOdyqcjBK2bt2aJDnooIMqnqScZrOZm266Kdu2bcv06dOrHqfjBgYGMmvWrBGP8dHu+9//fiZNmpQ3vvGNueiii/L4449XPVLHfetb38qpp56aCy64IBMmTMib3/zm3HDDDVWPVdQzzzyTr371q5kzZ056enqqHqdjzjzzzNx9990ZHBxMkjz44IO59957M3PmzIon27neqgfoNj/96U/TbDZz2GGHjbj8sMMOy3/9139VNBUlDA8PZ8GCBZkxY0b6+/urHqfjNm3alOnTp+c3v/lNXvva12blypU5/vjjqx6ro2666aY88MADWbduXdWjFHPGGWfkxhtvzDHHHJMnnngiixcvzh/+4R9m8+bNGT9+fNXjdcx///d/Z9myZVm4cGE+9alPZd26dbnkkksyZsyYzJ49u+rxirj11lvz85//PB/84AerHqWjPvnJT+YXv/hFjj322DQajTSbzVxxxRW56KKLqh5tp4QX/NbAwEA2b95ci9e/JMkxxxyTjRs3ZuvWrfn617+e2bNnZ/Xq1aM2vn70ox/l0ksvzapVqzJ27Niqxynmhf/Xf+KJJ+aMM87IkUcemVtuuSUf+tCHKpyss4aHh3Pqqafmc5/7XJLkzW9+czZv3pwvfelLtQmvL3/5y5k5c2YmTZpU9Sgddcstt+Sf/umfsmLFikybNi0bN27MggULMmnSpK68rYXXixxyyCFpNBp56qmnRlz+1FNP5fDDD69oKjpt/vz5+fa3v501a9bkiCOOqHqcIsaMGZM3velNSZJTTjkl69atyxe+8IVcd911FU/WGRs2bMiWLVty8sknP39Zs9nMmjVrcs0112RoaCiNRqPCCct4/etfn6lTp+aRRx6pepSOmjhx4kv+J+K4447LN77xjYomKuuxxx7LXXfdlW9+85tVj9JxH//4x/PJT34y73vf+5IkJ5xwQh577LEsWbKkK8PLa7xeZMyYMTnllFNy9913P3/Z8PBw7r777lq8/qVuWq1W5s+fn5UrV+bf/u3fMmXKlKpHqszw8HCGhoaqHqNjzj777GzatCkbN258/u3UU0/NRRddlI0bN9YiupLk6aefzg9+8INMnDix6lE6asaMGS/50TCDg4M58sgjK5qorOXLl2fChAmZNWtW1aN03K9+9avst9/InGk0GhkeHq5oot3zjNdOLFy4MLNnz86pp56a008/PVdffXW2bduWiy++uOrROurpp58e8X/Bjz76aDZu3JiDDjookydPrnCyzhkYGMiKFSty2223Zfz48XnyySeTJAcccEDGjRtX8XSdc9lll2XmzJmZPHlyfvnLX2bFihW55557cuedd1Y9WseMHz/+Ja/d23///XPwwQeP6tf0fexjH8t5552XI488Mj/5yU9y+eWXp9Fo5P3vf3/Vo3XURz/60Zx55pn53Oc+lwsvvDD3339/rr/++lx//fVVj9Zxw8PDWb58eWbPnp3e3tH/1/x5552XK664IpMnT860adPy3e9+N0uXLs2cOXOqHm3nqv5nld3qi1/8Ymvy5MmtMWPGtE4//fTW2rVrqx6p4/793/+9leQlb7Nnz656tI7Z2b5JWsuXL696tI6aM2dO68gjj2yNGTOmdeihh7bOPvvs1r/+679WPVZxdfhxEu9973tbEydObI0ZM6b1hje8ofXe97639cgjj1Q9VhH/8i//0urv72/19fW1jj322Nb1119f9UhF3Hnnna0krYcffrjqUYr4xS9+0br00ktbkydPbo0dO7b1xje+sfXXf/3XraGhoapH26meVqtLf7QrAMAo4zVeAACFCC8AgEKEFwBAIcILAKAQ4QUAUIjwAgAoRHjtxtDQUBYtWjSqf5r3ztRx7zrunNi7TnvXcefE3vbuPn6O12784he/yAEHHJCtW7fmda97XdXjFFPHveu4c2LvOu1dx50Te9u7+3jGCwCgEOEFAFCI8AIAKER47UZvb2/mzp1bi9/u/kJ13LuOOyf2rtPeddw5sbe9u48X1+9Gs9nMQw89lOOOOy6NRqPqcYqp49513Dmxd532ruPOib3t3X084wUAUIjwAgAoRHgBABQivAAAChFeAACFCC8AgEKEFwBAIcILAKAQ4QUAUIjwAgAoRHgBABQivAAAChFeAACFCC8AgEKEFwBAIcILAKAQ4QUAUIjwAgAoRHgBABQivAAAChFeAACFCC8AgEKEFwBAIcILAKAQ4QUAUIjwAgAoRHgBABQivAAAChFeAACFCC8AgEKEFwBAIcILAKAQ4QUAUIjwAgAoRHgBABQivAAAChFeAACF7FV4XXvttTnqqKMyduzYnHHGGbn//vv39VwAAKNO2+F18803Z+HChbn88svzwAMP5KSTTsq73vWubNmypRPzAQCMGm2H19KlS/PhD384F198cY4//vh86Utfyu/93u/lH//xHzsxHwDAqNHbzpWfeeaZbNiwIZdddtnzl+23334555xz8p3vfGennzM0NJShoaGRh/b2pq+vby/GLavZbI54Xxd13LuOOyf2rtPeddw5sbe9y2o0Gi97nZ5Wq9Xa0z/wJz/5Sd7whjfkvvvuy/Tp05+//K/+6q+yevXq/Md//MdLPmfRokVZvHjxiMvmzp2befPm7emxAABdr7+//2Wv09YzXnvjsssuy8KFC0ce+ip6xmtwcDBTp07do4odLeq4dx13Tuxdp73ruHNib3t3n7bC65BDDkmj0chTTz014vKnnnoqhx9++E4/p6+v71URWbvTaDS69gbspDruXcedE3vXSR13TuxdN928d1svrh8zZkxOOeWU3H333c9fNjw8nLvvvnvEtx4BAHiptr/VuHDhwsyePTunnnpqTj/99Fx99dXZtm1bLr744k7MBwAwarQdXu9973vzv//7v/nMZz6TJ598Mn/wB3+QO+64I4cddlgn5gMAGDX26sX18+fPz/z58/f1LAAAo5rf1QgAUIjwAgAoRHgBABQivAAAChFeAACFCC8AgEKEFwBAIcILAKAQ4QUAUIjwAgAoRHgBABQivAAAChFeAACFCC8AgEKEFwBAIcILAKAQ4QUAUIjwAgAoRHgBABQivAAAChFeAACFCC8AgEKEFwBAIcILAKAQ4QUAUIjwAgAoRHgBABQivAAAChFeAACFCC8AgEKEFwBAIcILAKAQ4QUAUIjwAgAoRHgBABQivAAAChFeAACFCC8AgEKEFwBAIcILAKAQ4QUAUIjwAgAoRHgBABQivAAAChFeAACFCC8AgEKEFwBAIcILAKAQ4QUAUIjwAgAoRHgBABTSdnitWbMm5513XiZNmpSenp7ceuutHRgLAGD0aTu8tm3blpNOOinXXnttJ+YBABi1etv9hJkzZ2bmzJmdmAUAYFRrO7zaNTQ0lKGhoZGH9vamr6+v00e/Ys1mc8T7uqjj3nXcObF3nfau486Jve1dVqPReNnr9LRardbeHtDT05OVK1fm/PPP3+V1Fi1alMWLF4+4bO7cuZk3b97eHgsA0HX6+/tf9jodD69X+zNeg4ODmTp16h5V7GhRx73r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77pauDC54YYdt3OS/OVfJrNmJXPmJFddVeloHTPzt2/QbfbqGa+nn346F110UW644YYceOCB+3omqMzWrTveH3RQtXOUVte96+A1r0lOOSW5667fXdZq7fh4+vTq5oK62qtnvAYGBjJr1qycc845+du//dvdXndoaChDQ0MjD+3tTV9f394cXVSz2Rzxvi66Ze9Go+x5w8PJggXJjBlJf3+ZM3f2NR7te1d9v3rhDNXP0vkb+5BDkt7e5KmnRl7+1FPJscd2/Pid38c7f2ylqr9fddN9vKyq927swX/A2w6vm266KQ888EDWrVu3R9dfsmRJFi9ePOKyuXPnZt68ee0eXZnBwcGqR6hE1XuXip/nDAwkmzcn995b7syHHnroJZeN9r13tnNVqr6PJ4Vv7Ars9D5ewRwluY9Xr6q9+/fgP+BthdePfvSjXHrppVm1alXGjh27R59z2WWXZeHChSMPfRU94zU4OJipU6fuUcWOFnXce/785NvfTtasSY44oty5xx13XLnDdqKKvaveOanXffynP022b08OO2zk5Ycdljz5ZOfP74bbu7Ru2LlO9/EXejXs3VZ4bdiwIVu2bMnJJ5/8/GXNZjNr1qzJNddck6GhoZcs2tfX96qIrN1pNBpdewN2Uh32brWSj3wkWbkyueeeZMqUsudX9fWtcu9uuk/V4T7+7LM7/tHE2Wcnt92247Kenh0fX3NN588f7V/fnemmnetwH9+Zbt67rfA6++yzs2nTphGXXXzxxTn22GPziU98omuXhF0ZGEhWrNjxF9L48b97BuCAA5Jx46qdrZPqunddLV2afOUryfr1yf3373hN3/77J8uXVz1Z5zyd5JEXfPxoko1JDkoyuYqB4LfaCq/x48e/5PuX+++/fw4++OA9+r4mdJtly3a8P+uskZcvX5588IOlpymnrnvX1S237PiZXX/zN8nhhycbNyZ/9EfJli1VT9Y565O8/QUfP/eCl9lJbiw+DfzOXv8cLxgNWq2qJ6hGXfeus2uv3fFWF2clcTenG73i8Lrnnnv2wRgAAKOf39UIAFCI8AIAKER4AQAUIrwAAAoRXgAAhQgvAIBChBcAQCHCCwCgEOEFAFCI8AIAKER4AQAUIrwAAAoRXgAAhQgvAIBChBcAQCHCCwCgEOEFAFCI8AIAKER4AQAUIrwAAAoRXgAAhQgvulir2FuzuT2bN29Ks7m96Ln13JvntFrl3rZvb2bTps3Zvr1Z9NyqF29u357NmzaluX17xUvDDsILAKAQ4QUAUIjwAgAoRHgBABQivAAACumtegCA+uopdlKjkfT3FzvuBfwrP3ghz3gBABQivAAAChFeAACFCC8AgEKEFwBAIcILAKAQ4QUAUIjwAgAoRHgBABQivAAAChFeAACFCC8AgEKEFwBAIcILAKAQ4QUAUIjwAgAoRHgBABQivAAAChFeAACFCC8AgEKEFwBAIcILAKAQ4QUwii1Zkpx2WjJ+fDJhQnL++cnDD1c9FdRXW+G1aNGi9PT0jHg79thjOzUbAK/Q6tXJwECydm2yalXy7LPJuecm27ZVPRnUU2+7nzBt2rTcddddv/sDetv+IwAo5I47Rn584407nvnasCF561srGQlqre1q6u3tzeGHH96JWQDosK1bd7w/6KBq54C6aju8vv/972fSpEkZO3Zspk+fniVLlmTy5Mm7vP7Q0FCGhoZGHtrbm76+vvanLazZbI54Xxd13LuOOyf2rnrvRqPsecPDyYIFyYwZSX9/mTOr/hp3y21dmr2r2buxBw/qnlar1drTP/D222/P008/nWOOOSZPPPFEFi9enB//+MfZvHlzxo8fv9PPWbRoURYvXjzisrlz52bevHl7eizAqNTff0LR8+bOTW6/Pbn33uSII8qcuXnzpjIHQRfo34P/o2krvF7s5z//eY488sgsXbo0H/rQh3Z6nVf7M16Dg4OZOnXqHlXsaFHHveu4c2LvqvduNMq9Rnb+/OS225I1a5IpU4odm2Zze7nDdnp+d9zWpdm7mr335MxX9Kh//etfn6lTp+aRRx7Z5XX6+vpeFZG1O41Go1Z33OfUce867pzYezRrtZKPfCRZuTK5556y0ZXs2V9EJdThtt4Ze3efV/RzvJ5++un84Ac/yMSJE/fVPADsQwMDyVe/mqxYseNneT355I63X/+66smgntoKr4997GNZvXp1fvjDH+a+++7Ln/zJn6TRaOT9739/p+YD4BVYtmzHv2Q866xk4sTfvd18c9WTQT219a3G//mf/8n73//+/N///V8OPfTQvOUtb8natWtz6KGHdmo+AF6BvX8VL9AJbYXXTTfd1Kk5AABGPb+rEQCgEOEFAFCI8AIAKER4AQAUIrwAAAoRXgAAhQgvAIBChBcA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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAuRUlEQVR4nO3dfZCdZX0//vd6liwpBuQpQOQbQDQ8JEDlcUJ8QEGUIiO/TkEtTgNxnJpsgJjRKnQUosWAnWZwJBOB2sSZNiJVI5YZoIGWZBhNSYJhEpuyogxYhaR2NErAhT17fn9sQRY2mA05132y9+s1c2bn3HM21+e9130277335KSr1Wq1AgBA272u6gEAAOpC8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8dqBxYsX58gjj8zee++dM844Iw8++GDVI7Xd6tWrc8EFF2TSpEnp6urKd7/73apHaruFCxfmtNNOy4QJEzJx4sRceOGFeeSRR6oeq+2WLFmSE088Mfvuu2/23XffTJ8+PXfddVfVYxV1/fXXp6urK/Pmzat6lLa69tpr09XVNex27LHHVj1WET//+c/zkY98JAceeGDGjx+fE044IevWrat6rLY68sgjX7HfXV1d6e3trXq0tmk2m/nsZz+bo446KuPHj8/RRx+dL3zhC+nU/xFR8RrBN7/5zcyfPz/XXHNNHnrooZx00kl573vfm61bt1Y9Wltt3749J510UhYvXlz1KMWsWrUqvb29WbNmTVauXJnnn38+5557brZv3171aG11+OGH5/rrr8/69euzbt26vPvd784HPvCB/OhHP6p6tCLWrl2bm2++OSeeeGLVoxQxderUPPnkky/eHnjggapHartf/epXmTFjRvbaa6/cdddd+c///M/83d/9Xfbff/+qR2urtWvXDtvrlStXJkkuuuiiiidrnxtuuCFLlizJTTfdlM2bN+eGG27Il770pXzlK1+perSRtXiF008/vdXb2/vi/Waz2Zo0aVJr4cKFFU5VVpLWihUrqh6juK1bt7aStFatWlX1KMXtv//+rb//+7+veoy2++1vf9t6y1ve0lq5cmXrne98Z+vKK6+seqS2uuaaa1onnXRS1WMU9+lPf7r1tre9reoxKnfllVe2jj766Nbg4GDVo7TN+eef35o1a9awY3/6p3/auuSSSyqa6NW54vUyzz33XNavX59zzjnnxWOve93rcs455+QHP/hBhZNRwrZt25IkBxxwQMWTlNNsNnPbbbdl+/btmT59etXjtF1vb2/OP//8Yc/xse7HP/5xJk2alDe96U255JJL8sQTT1Q9Utt973vfy6mnnpqLLrooEydOzFvf+tbceuutVY9V1HPPPZd//Md/zKxZs9LV1VX1OG1z5pln5r777ktfX1+S5OGHH84DDzyQ8847r+LJRtZd9QCd5pe//GWazWYOOeSQYccPOeSQ/Nd//VdFU1HC4OBg5s2blxkzZmTatGlVj9N2GzduzPTp0/O73/0ur3/967NixYocf/zxVY/VVrfddlseeuihrF27tupRijnjjDOybNmyHHPMMXnyySezYMGCvP3tb8+mTZsyYcKEqsdrm5/+9KdZsmRJ5s+fn6uvvjpr167NFVdckXHjxmXmzJlVj1fEd7/73fz617/OpZdeWvUobfWZz3wmv/nNb3Lsscem0Wik2WzmuuuuyyWXXFL1aCNSvOD/9Pb2ZtOmTbV4/UuSHHPMMdmwYUO2bduWb33rW5k5c2ZWrVo1ZsvXz372s1x55ZVZuXJl9t5776rHKealP/WfeOKJOeOMM3LEEUfk9ttvz0c/+tEKJ2uvwcHBnHrqqfniF7+YJHnrW9+aTZs25atf/WptitfXvva1nHfeeZk0aVLVo7TV7bffnn/6p3/K8uXLM3Xq1GzYsCHz5s3LpEmTOnKvFa+XOeigg9JoNLJly5Zhx7ds2ZJDDz20oqlot7lz5+bOO+/M6tWrc/jhh1c9ThHjxo3Lm9/85iTJKaeckrVr1+bLX/5ybr755oona4/169dn69atOfnkk1881mw2s3r16tx0003p7+9Po9GocMIy3vCGN2TKlCl59NFHqx6lrQ477LBX/BBx3HHH5dvf/nZFE5X1+OOP59577813vvOdqkdpu0996lP5zGc+kw996ENJkhNOOCGPP/54Fi5c2JHFy2u8XmbcuHE55ZRTct999714bHBwMPfdd18tXv9SN61WK3Pnzs2KFSvyb//2bznqqKOqHqkyg4OD6e/vr3qMtjn77LOzcePGbNiw4cXbqaeemksuuSQbNmyoRelKkqeffjo/+clPcthhh1U9SlvNmDHjFW8N09fXlyOOOKKiicpaunRpJk6cmPPPP7/qUdrumWeeyeteN7zONBqNDA4OVjTRq3PFawTz58/PzJkzc+qpp+b000/PjTfemO3bt+eyyy6rerS2evrpp4f9FPzYY49lw4YNOeCAAzJ58uQKJ2uf3t7eLF++PHfccUcmTJiQp556Kkmy3377Zfz48RVP1z5XXXVVzjvvvEyePDm//e1vs3z58tx///255557qh6tbSZMmPCK1+7ts88+OfDAA8f0a/o++clP5oILLsgRRxyRX/ziF7nmmmvSaDTy4Q9/uOrR2uoTn/hEzjzzzHzxi1/MxRdfnAcffDC33HJLbrnllqpHa7vBwcEsXbo0M2fOTHf32P9r/oILLsh1112XyZMnZ+rUqfnhD3+YRYsWZdasWVWPNrKq/1llp/rKV77Smjx5cmvcuHGt008/vbVmzZqqR2q7f//3f28lecVt5syZVY/WNiPlTdJaunRp1aO11axZs1pHHHFEa9y4ca2DDz64dfbZZ7f+9V//teqxiqvD20l88IMfbB122GGtcePGtd74xje2PvjBD7YeffTRqscq4l/+5V9a06ZNa/X09LSOPfbY1i233FL1SEXcc889rSStRx55pOpRivjNb37TuvLKK1uTJ09u7b333q03velNrb/+679u9ff3Vz3aiLparQ59a1cAgDHGa7wAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbxeRX9/f6699tox/W7eI6lj7jpmTuSuU+46Zk7klrvzeB+vV/Gb3/wm++23X7Zt25Z999236nGKqWPuOmZO5K5T7jpmTuSWu/O44gUAUIjiBQBQiOIFAFCI4vUquru7M3v27Fr87+4vVcfcdcycyF2n3HXMnMgtd+fx4vpX0Ww2s3nz5hx33HFpNBpVj1NMHXPXMXMid51y1zFzIrfcnccVLwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQnapeC1evDhHHnlk9t5775xxxhl58MEHd/dcAABj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rq+vr+oRKlF97vb/aPrLXyYDA8khhww/fsghyVNPtX35bN68eYSjYzv3yJmr4Rxv+/Ij7nfJq05z5yZ33pmsXp0cfniZNZ3j1asq96v1oRd0tVqt1q784bNnz85dd92VBx54IIe/ytm8p1/x6uvry5QpU3aqxY4VnZK7u7vM2mvWJA8+mFxxxdD9rq6hF+HedFNyww3tXXtg4JU/lY313CNlLs05Xu053mi8pl+27JRWK7n88mTFiuT++5O3vKXtS76o2Rwot9gOZ+iMc7y0qnO37YrX3Llzc+edd2b16tWvWrqSpKenZ48oWa+m0WjU6sR9QV1yL1qUfP3rybp1Q385zZuX7LNPsnRp+9eu8utbVe5OOqec4+1fu6qvb29vsnx5cscdQ+/l9cLVvf32S8aPb+/anXRO1eUcf7lOzj2q4tVqtXL55ZdnxYoVuf/++3PUUUe1ay4o5vbbh97P6POfTw49NNmwIXnf+5KtW6uerL3qmruO6rjXS5YMfTzrrOHHly4depsJqMqoildvb2+WL1+eO+64IxMmTMhT//cjxH777Zfx7f4RAtpo8eKhW93UNXcd1W2vd+1FNNB+o3ofryVLlmTbtm0566yzcthhh714++Y3v9mu+QAAxoxR/6oRAIBd4/9qBAAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxomO1WuVuAwPNbNy4KQMDzaLr1jE3vzfW93rH+90qdms2B7Jp08Y0mwMF14UdU7wAAApRvAAAClG8AAAKUbwAAArprnoA2LGuYis1Gsm0acWWe4lXvhC3q1zsJI0kZYN7gT1j/7ntJGfHXPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8aLWFi5MTjstmTAhmTgxufDC5JFHqp6qnDlzksceS559NlmzZuhrAWNB3Z/bdC7Fi1pbtSrp7R0qHStXJs8/n5x7brJ9e9WTtd/FFyeLFiULFiQnn5w8/HByzz3JwQdXPRm8dnV+btPZRlW8lixZkhNPPDH77rtv9t1330yfPj133XVXu2aDtrv77uTSS5OpU5OTTkqWLUueeCJZv77qydpv/vzk1luHMm/enHz848kzzySzZlU9Gbx2dX5u09lGVbwOP/zwXH/99Vm/fn3WrVuXd7/73fnABz6QH/3oR+2aD4ratm3o4wEHVDtHu+21V3LKKcm99/7+WKs1dH/69Ormgnapy3Obztc9mgdfcMEFw+5fd911WbJkSdasWZOpU6eO+Dn9/f3p7+8fvmh3d3p6ekY5annNZnPYx7rolNyNRtn1BgeTefOSGTOSadPKrDny17j9wQ86KOnuTrZsGX58y5bk2GPbu3bV59VLZ+iEWUrppMxj/bndCV/jTtrvkqrO3diJk3tUxeulms1m/vmf/znbt2/P9Ff5EXnhwoVZsGDBsGOzZ8/OnDlzdnXp4vr6+qoeoRJV5y5Vfl7Q25ts2pQ88EC5NTdv3jzC0cLBCxs5czWqPser0AmZx/pz2zlevapyT9uJk7ur1Wq1RvOHbty4MdOnT8/vfve7vP71r8/y5cvzJ3/yJzt8/J5+xauvry9TpkzZqRY7VnRK7kZjl38uGLW5c5M77khWr06OOqrYsmk2B15xrLu7/V/zvfYaej3Xn/3ZUO4XLFuWvOENQ/8CrF0GBqr/CbxTzvGSOinzWH9uj/S8Lq2T9rukqnO35YrXMccckw0bNmTbtm351re+lZkzZ2bVqlU5/vjjR3x8T0/PHlGyXk2j0ajVifuCOuRutZLLL09WrEjuv79s6Up27knaDs8/P/Qi47PP/n3x6uoaun/TTe1du5POqTqc4y9Xl8xVPrc76etbl/1+uU7OPeriNW7cuLz5zW9OkpxyyilZu3ZtvvzlL+fmm2/e7cNBu/X2JsuXD5WPCROSp54aOr7ffsn48dXO1m6LFiVf/3qybl3y4INDr4HZZ59k6dKqJ4PXrs7PbTrba77eOzg4+IpfJcKeYsmSoY9nnTX8+NKlQ/8UfSy7/fah9+z6/OeTQw9NNmxI3ve+ZOvWqieD167Oz20626iK11VXXZXzzjsvkydPzm9/+9ssX748999/f+655552zQdtNbpXOI49ixcP3WCsqftzm841quK1devW/MVf/EWefPLJ7LfffjnxxBNzzz335D3veU+75gMAGDNGVby+9rWvtWsOAIAxz//VCABQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiRQdrFbs1mwPZtGljms2BouuOmLpV7jYw0MzGjZsyMNAstiaM/ec27JjiBQBQiOIFAFCI4gUAUIjiBQBQSHfVA8COdRVbqdFIpk0rttxLjPRC3LGee4TMXeUyJ0kjSfnYr8xdNnYlqUf+BxUFg3fKXsMLXPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8aLWFi5MTjstmTAhmTgxufDC5JFHqp6q/eqae3WSC5JMStKV5LuVTlPWnDnJY48lzz6brFkztP9jWZ33ms72morX9ddfn66ursybN283jQNlrVqV9PYO/UW0cmXy/PPJuecm27dXPVl71TX39iQnJVlc9SCFXXxxsmhRsmBBcvLJycMPJ/fckxx8cNWTtU9d95rO172rn7h27drcfPPNOfHEE3fnPFDU3XcPv79s2dAVoPXrk3e8o5KRiqhr7vP+71Y38+cnt946tM9J8vGPJ+efn8yaldxwQ6WjtU1d95rOt0tXvJ5++ulccsklufXWW7P//vvv7pmgMtu2DX084IBq5yitrrnrYK+9klNOSe699/fHWq2h+9OnVzcX1NUuXfHq7e3N+eefn3POOSd/8zd/86qP7e/vT39///BFu7vT09OzK0sX1Ww2h32si07J3WiUXW9wMJk3L5kxI5k2rcyaI32Nx3ruETO3f9nKjfx8an/ygw5KuruTLVuGH9+yJTn22LYvX8v9rvp750tn6IRZSqo6d2MnvoGPunjddttteeihh7J27dqdevzChQuzYMGCYcdmz56dOXPmjHbpyvT19VU9QiWqzl2q/LygtzfZtCl54IFya27evPkVx8Z67hEzl1m6UiPlrkPyOu73yHtdjaq/j1elqtzTduIb+KiK189+9rNceeWVWblyZfbee++d+pyrrroq8+fPH77oHnTFq6+vL1OmTNmpFjtW1DH3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39U19OLjm25KbrihvWsPDIx0FWRUPw/tklYrufzyZMWK5P77k7e8pe1LvqjZHHjFsUZ3+zO/XFeSFUkuLLRec2CE3GN8r5PO2O9O2OvSOuX7eGlV597tV7wOO+ywHH/88cOOHXfccfn2t7+9w8/p6enZI0rWq2k0GrU6cV9Ql9yLFiVf/3qybt1QAZs3L9lnn+QlL2Vsm6q+vr29yfLlyR13DL2/0wtX9/bbLxk/vr1rV3lOPZ3k0ZfcfyzJhiQHJJnc5rXruNdJdbnruNcjqcv38Zfr5NyjKl4zZszIIy97572+vr4cccQRu3UoKOn224fes+vzn08OPTTZsCF53/uSrVurnqx9liwZ+njWWcOPL1069NYDY9W6JO96yf35//dxZpJlxacpw14PqcNes2cYVfH6xCc+kTPPPDNf/OIXc/HFF+fBBx/MLbfckltuuaVd80ERixcP3epi519gMLaclaRu0e01dJZRvYHqaaedlhUrVuQb3/hGpk2bli984Qu58cYbc8kll7RrPgCAMWPUr3B8//vfn/e///3tmAUAYEzzfzUCABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUonjRsVqtcreBgWY2btyUgYFm0XV3kLzYrdkcyKZNG9NsDhRct+LNbrXSHBjIpo0b0xwYqHizx/peV7/fnbPXMETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChkVMXryCOPTFdX1ytuvb297ZoPAGDM6B7Ng9euXZtms/ni/U2bNuU973lPLrroot0+GADAWDOq4nXwwQcPu3/99dfn6KOPzjvf+c4dfk5/f3/6+/uHL9rdnZ6entEsXYkXSuZLy2Yd1DF3HTMnctcpdx0zJ3LLXVaj0fiDj+lqtVqtXfnDn3vuuUyaNCnz58/P1VdfvcPHXXvttVmwYMGwY7Nnz86cOXN2ZVkAgI40bdq0P/iYXS5et99+e/78z/88TzzxRCZNmrTDx+3pV7z6+voyZcqUnWqxY0Udc9cxcyJ3nXLXMXMit9xl7cyao/pV40t97Wtfy3nnnfeqpStJenp69oiS9WoajUatTtwX1DF3HTMnctdJHTMnctdNJ+fepeL1+OOP59577813vvOd3T0PAMCYtUvv47V06dJMnDgx559//u6eBwBgzBp18RocHMzSpUszc+bMdHfv8m8qAQBqZ9TF6957780TTzyRWbNmtWMeAIAxa9SXrM4999zs4j+EBACoNf9XIwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhoypezWYzn/3sZ3PUUUdl/PjxOfroo/OFL3whrVarXfMBAIwZ3aN58A033JAlS5bk61//eqZOnZp169blsssuy3777ZcrrriiXTMCAIwJoype3//+9/OBD3wg559/fpLkyCOPzDe+8Y08+OCDbRkOAGAsGVXxOvPMM3PLLbekr68vU6ZMycMPP5wHHnggixYt2uHn9Pf3p7+/f/ii3d3p6enZtYkLajabwz7WRR1z1zFzInedctcxcyK33GU1Go0/+Jiu1iheoDU4OJirr746X/rSl9JoNNJsNnPdddflqquu2uHnXHvttVmwYMGwY7Nnz86cOXN2dlkAgI43bdq0P/iYURWv2267LZ/61Kfyt3/7t5k6dWo2bNiQefPmZdGiRZk5c+aIn7OnX/F64erezrTYsaKOueuYOZG7TrnrmDmRW+6ydmbNUf2q8VOf+lQ+85nP5EMf+lCS5IQTTsjjjz+ehQsX7rB49fT07BEl69U0Go1anbgvqGPuOmZO5K6TOmZO5K6bTs49qreTeOaZZ/K61w3/lEajkcHBwd06FADAWDSqK14XXHBBrrvuukyePDlTp07ND3/4wyxatCizZs1q13wAAGPGqIrXV77ylXz2s5/NnDlzsnXr1kyaNCl/+Zd/mc997nPtmg8AYMwYVfGaMGFCbrzxxtx4441tGgcAYOzyfzUCABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAU0tVqtVpVDwEAUAeueAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABTy/wNOAhnJKU5BTQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAu4ElEQVR4nO3df5BddX0//ud6lywprpEIASINIBp+LVD52RB/oCCawYx8Ox9QS9sIjtMmGyRmtIodJdFiwE4zOEAjWJs400akasAyBRpoSYbRND8wTGJTViyDvyCpHY0SdGHv3u8fKz9WNjEbue+z2fN4zNy53DP38n4999x797lnT+52tFqtVgAAaLuXVT0AAEBdKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF67cdNNN+Xoo4/OgQcemLPPPjvr16+veqS2W7t2bWbPnp2pU6emo6Mjt99+e9Ujtd2SJUty5plnpru7O1OmTMlFF12Uhx9+uOqx2m7ZsmU55ZRT8opXvCKveMUrMmPGjNx1111Vj1XUtddem46OjixYsKDqUdpq0aJF6ejoGHY5/vjjqx6riB/96Ef5kz/5k7zqVa/KxIkTc/LJJ2fjxo1Vj9VWRx999Iv2d0dHR3p7e6serW2azWY+8YlP5JhjjsnEiRNz7LHH5tOf/nTG6l9EVLxG8JWvfCULFy7M1VdfnQcffDCnnnpq3v72t2fHjh1Vj9ZWu3btyqmnnpqbbrqp6lGKWbNmTXp7e7Nu3bqsXr06zzzzTC644ILs2rWr6tHa6sgjj8y1116bTZs2ZePGjXnrW9+ad73rXfnOd75T9WhFbNiwITfffHNOOeWUqkcp4qSTTsrjjz/+3OWBBx6oeqS2++lPf5qZM2fmgAMOyF133ZX/+q//yt/+7d/m4IMPrnq0ttqwYcOwfb169eokycUXX1zxZO1z3XXXZdmyZbnxxhuzbdu2XHfddfnsZz+bG264oerRRtbiRc4666xWb2/vc7ebzWZr6tSprSVLllQ4VVlJWqtWrap6jOJ27NjRStJas2ZN1aMUd/DBB7f+/u//vuox2u4Xv/hF63Wve11r9erVrTe/+c2tK6+8suqR2urqq69unXrqqVWPUdxHP/rR1hve8Iaqx6jclVde2Tr22GNbg4ODVY/SNhdeeGHr8ssvH7btj/7oj1qXXnppRRPtmSNev+Hpp5/Opk2bcv755z+37WUve1nOP//8fOtb36pwMkrYuXNnkmTy5MkVT1JOs9nMrbfeml27dmXGjBlVj9N2vb29ufDCC4e9xse77373u5k6dWpe85rX5NJLL833v//9qkdqu2984xs544wzcvHFF2fKlCl5/etfny984QtVj1XU008/nX/8x3/M5Zdfno6OjqrHaZtzzjkn9913X/r6+pIkDz30UB544IHMmjWr4slG1ln1AGPNT37ykzSbzRx22GHDth922GH57//+74qmooTBwcEsWLAgM2fOTE9PT9XjtN2WLVsyY8aM/OpXv8rLX/7yrFq1KieeeGLVY7XVrbfemgcffDAbNmyoepRizj777KxYsSLHHXdcHn/88SxevDhvfOMbs3Xr1nR3d1c9Xtv8z//8T5YtW5aFCxfm4x//eDZs2JAPfvCDmTBhQubMmVP1eEXcfvvt+dnPfpb3ve99VY/SVh/72Mfy85//PMcff3wajUaazWauueaaXHrppVWPNiLFC36tt7c3W7durcX5L0ly3HHHZfPmzdm5c2e++tWvZs6cOVmzZs24LV8/+MEPcuWVV2b16tU58MADqx6nmBf+1H/KKafk7LPPzlFHHZXbbrst73//+yucrL0GBwdzxhln5DOf+UyS5PWvf322bt2az3/+87UpXl/84hcza9asTJ06tepR2uq2227LP/3TP2XlypU56aSTsnnz5ixYsCBTp04dk/ta8foNhxxySBqNRrZv3z5s+/bt23P44YdXNBXtNn/+/Nx5551Zu3ZtjjzyyKrHKWLChAl57WtfmyQ5/fTTs2HDhnzuc5/LzTffXPFk7bFp06bs2LEjp5122nPbms1m1q5dmxtvvDH9/f1pNBoVTljGK1/5ykyfPj2PPPJI1aO01RFHHPGiHyJOOOGEfO1rX6toorIee+yx3Hvvvfn6179e9Sht95GPfCQf+9jH8p73vCdJcvLJJ+exxx7LkiVLxmTxco7Xb5gwYUJOP/303Hfffc9tGxwczH333VeL81/qptVqZf78+Vm1alX+/d//Pcccc0zVI1VmcHAw/f39VY/RNuedd162bNmSzZs3P3c544wzcumll2bz5s21KF1J8uSTT+Z73/tejjjiiKpHaauZM2e+6KNh+vr6ctRRR1U0UVnLly/PlClTcuGFF1Y9Sts99dRTednLhteZRqORwcHBiibaM0e8RrBw4cLMmTMnZ5xxRs4666xcf/312bVrVy677LKqR2urJ598cthPwY8++mg2b96cyZMnZ9q0aRVO1j69vb1ZuXJl7rjjjnR3d+eJJ55IkkyaNCkTJ06seLr2ueqqqzJr1qxMmzYtv/jFL7Jy5crcf//9ueeee6oerW26u7tfdO7eQQcdlFe96lXj+py+D3/4w5k9e3aOOuqo/PjHP87VV1+dRqOR9773vVWP1lYf+tCHcs455+Qzn/lMLrnkkqxfvz633HJLbrnllqpHa7vBwcEsX748c+bMSWfn+P82P3v27FxzzTWZNm1aTjrppHz729/O0qVLc/nll1c92siq/meVY9UNN9zQmjZtWmvChAmts846q7Vu3bqqR2q7//iP/2gledFlzpw5VY/WNiPlTdJavnx51aO11eWXX9466qijWhMmTGgdeuihrfPOO6/1b//2b1WPVVwdPk7i3e9+d+uII45oTZgwofXqV7+69e53v7v1yCOPVD1WEf/yL//S6unpaXV1dbWOP/741i233FL1SEXcc889rSSthx9+uOpRivj5z3/euvLKK1vTpk1rHXjgga3XvOY1rb/6q79q9ff3Vz3aiDparTH60a4AAOOMc7wAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbz2oL+/P4sWLRrXn+Y9kjrmrmPmRO465a5j5kRuuccen+O1Bz//+c8zadKk7Ny5M694xSuqHqeYOuauY+ZE7jrlrmPmRG65xx5HvAAAClG8AAAKUbwAAApRvPags7Mzc+fOrcVfd3+hOuauY+ZE7jrlrmPmRG65xx4n1+9Bs9nMtm3bcsIJJ6TRaFQ9TjF1zF3HzIncdcpdx8yJ3HKPPY54AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUsk/F66abbsrRRx+dAw88MGeffXbWr1//Us8FADDujLp4feUrX8nChQtz9dVX58EHH8ypp56at7/97dmxY0c75gMAGDdGXbyWLl2aD3zgA7nsssty4okn5vOf/3x+7/d+L//wD//QjvkAAMaNztHc+emnn86mTZty1VVXPbftZS97Wc4///x861vfGvEx/f396e/vH75oZ2e6urr2Ydyyms3msOu6qGPuOmZO5K5T7jpmTuSWu6xGo/Fb79PRarVae/s//PGPf5xXv/rV+eY3v5kZM2Y8t/0v//Ivs2bNmvznf/7nix6zaNGiLF68eNi2uXPnZt68eXu7LADAmNfT0/Nb7zOqI1774qqrrsrChQuHL7ofHfHq6+vL9OnT96rFjhd1zF3HzIncdcpdx8yJ3HKPPaMqXoccckgajUa2b98+bPv27dtz+OGHj/iYrq6u/aJk7Umj0RizO7Cd6pi7jpkTueukjpkTuetmLOce1cn1EyZMyOmnn5777rvvuW2Dg4O57777hv3qEQCAFxv1rxoXLlyYOXPm5IwzzshZZ52V66+/Prt27cpll13WjvkAAMaNURevd7/73fnf//3ffPKTn8wTTzyRP/iDP8jdd9+dww47rB3zAQCMG/t0cv38+fMzf/78l3oWAIBxzd9qBAAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKGTUxWvt2rWZPXt2pk6dmo6Ojtx+++1tGAsAYPwZdfHatWtXTj311Nx0003tmAcAYNzqHO0DZs2alVmzZrVjFgCAcW3UxWu0+vv709/fP3zRzs50dXW1e+nfWbPZHHZdF3XMXcfMidx1yl3HzInccpfVaDR+6306Wq1Wa18X6OjoyKpVq3LRRRft9j6LFi3K4sWLh22bO3du5s2bt6/LAgCMOT09Pb/1Pm0vXvv7Ea++vr5Mnz59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XV1d+zZxQc1mc9h1XdQx91jK3GiUXa+3N9m6NXnggTLrjfQ1Lhy5EiPmHuf7Oqnn/h4L7yNj6T2tpKpzN/biRd3RarVae/s/PPDAA5MkCxcuzMUXX5wNGzbkyiuvzOc///nMmTNnxMcsWrQoixcvHrZt7ty5mTdv3t4uC7XS03NysbXmz0/uuCNZuzYpdfB669YtL9rWc3K5zM/qSLIqyUWF1tu6ZYTc43xfJ2Njf4+FfU099OzFoeRRFa8JEybkjDPOyDe/+c3ntn3wgx/Mhg0b8q1vfWvEx+zvR7z6+voyffr0vWqx40Udc4+lzI3GqP/Ny6i1WskVVySrViX335+87nVtX/I5zebAi7Y1Otuf+TeV/mbcHBgh9zjf18nY2N9jYV+XNpbe00qqOvferDmqZ/8RRxyRE088cdi2E044IV/72td2+5iurq79omTtSaPRqNUT91l1zF2XzL29ycqVQ0dAuruTJ54Y2j5pUjJxYnvXrvLr+2SSR15w+9Ekm5NMTjKtzWtXlbvKfZ1Ul7uO+3okdXlP+01jOfeoPsdr5syZefg3Pnmvr68vRx111Es6FNBey5YN/eu2c89Njjji+ctXvlL1ZO21Mcnrf31JkoW//u9PVjZR+9nXQ+qwr9k/jOqI14c+9KGcc845+cxnPpNLLrkk69evzy233JJbbrmlXfMBbbD3JxiML+cmqVt0+xrGllEd8TrzzDOzatWqfPnLX05PT08+/elP5/rrr8+ll17arvkAAMaNUZ/h+M53vjPvfOc72zELAMC45m81AgAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4wZjTKnZpNgeydeuWNJsDBdcdKXKr6KU5MJCtW7akOTBQbt1a7uvq9/fY2dcwRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKGRUxevoo49OR0fHiy69vb3tmg8AYNzoHM2dN2zYkGaz+dztrVu35m1ve1suvvjil3wwAIDxZlTF69BDDx12+9prr82xxx6bN7/5zbt9TH9/f/r7+4cv2tmZrq6u0SxdiWdL5gvLZh3UMXcdMydy1yl3HTMncstdVqPR+K336Wi1Wq19+Z8//fTTmTp1ahYuXJiPf/zju73fokWLsnjx4mHb5s6dm3nz5u3LsgAAY1JPT89vvc8+F6/bbrstf/zHf5zvf//7mTp16m7vt78f8err68v06dP3qsWOF3XMXcfMidx1yl3HzInccpe1N2uO6leNL/TFL34xs2bN2mPpSpKurq79omTtSaPRqNUT91l1zF3HzIncdVLHzIncdTOWc+9T8Xrsscdy77335utf//pLPQ8AwLi1T5/jtXz58kyZMiUXXnjhSz0PAMC4NeriNTg4mOXLl2fOnDnp7Nzn31QCANTOqIvXvffem+9///u5/PLL2zEPAMC4NepDVhdccEH28R9CAgDUmr/VCABQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFDIqIpXs9nMJz7xiRxzzDGZOHFijj322Hz6059Oq9Vq13wAAONG52jufN1112XZsmX50pe+lJNOOikbN27MZZddlkmTJuWDH/xgu2YEABgXRlW8vvnNb+Zd73pXLrzwwiTJ0UcfnS9/+ctZv359W4YDABhPRlW8zjnnnNxyyy3p6+vL9OnT89BDD+WBBx7I0qVLd/uY/v7+9Pf3D1+0szNdXV37NnFBzWZz2HVd1DF3HTMnctcpdx0zJ3LLXVaj0fit9+lojeIErcHBwXz84x/PZz/72TQajTSbzVxzzTW56qqrdvuYRYsWZfHixcO2zZ07N/PmzdvbZQEAxryenp7fep9RFa9bb701H/nIR/I3f/M3Oemkk7J58+YsWLAgS5cuzZw5c0Z8zP5+xOvZo3t702LHizrmrmPmRO465a5j5kRuucvamzVH9avGj3zkI/nYxz6W97znPUmSk08+OY899liWLFmy2+LV1dW1X5SsPWk0GrV64j6rjrnrmDmRu07qmDmRu27Gcu5RfZzEU089lZe9bPhDGo1GBgcHX9KhAADGo1Ed8Zo9e3auueaaTJs2LSeddFK+/e1vZ+nSpbn88svbNR8AwLgxquJ1ww035BOf+ETmzZuXHTt2ZOrUqfnzP//zfPKTn2zXfAAA48aoild3d3euv/76XH/99W0aBwBg/PK3GgEAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAArpaLVaraqHAACoA0e8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAACvn/AckYs9BthvMaAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAutElEQVR4nO3dfZBddX0/8Pd6lywphkiABCINIDY8JEDlcULUoiCawYy08wO12EbiODVZhJjRKnSURMWAnWZwJBOB2oSZNiJVA5YZoIGWZBhN84BhEpuyogxYhaR2NErQhb17f39seVjZYDZyv2ez5/WauXPnnrk33897z83Je8+e3O1otVqtAADQdq+pegAAgLpQvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvPZg2bJlOeaYY3LggQfm7LPPzoYNG6oeqe3WrVuX2bNnZ/Lkyeno6Mgdd9xR9Uhtt2TJkpx55pkZN25cJk6cmIsuuiiPPPJI1WO13fLly3PKKafk4IMPzsEHH5wZM2bk7rvvrnqsoq677rp0dHRkwYIFVY/SVosWLUpHR8eg2wknnFD1WEX85Cc/yQc+8IEceuihGTt2bE4++eRs2rSp6rHa6phjjnnZ/u7o6Eh3d3fVo7VNs9nMpz/96Rx77LEZO3ZsjjvuuHzuc5/LSP2NiIrXEL7+9a9n4cKFueaaa/LQQw/l1FNPzTvf+c7s3Lmz6tHaavfu3Tn11FOzbNmyqkcpZu3atenu7s769euzZs2aPPfcc7nggguye/fuqkdrq6OOOirXXXddNm/enE2bNuXtb3973vOe9+T73/9+1aMVsXHjxtx000055ZRTqh6liGnTpuXJJ5984fbggw9WPVLb/fznP8/MmTNzwAEH5O67785//ud/5u/+7u9yyCGHVD1aW23cuHHQvl6zZk2S5OKLL654sva5/vrrs3z58tx4443Zvn17rr/++nzxi1/Ml7/85apHG1qLlznrrLNa3d3dLzxuNputyZMnt5YsWVLhVGUlaa1evbrqMYrbuXNnK0lr7dq1VY9S3CGHHNL6+7//+6rHaLtf/epXrT/6oz9qrVmzpvUnf/InrSuvvLLqkdrqmmuuaZ166qlVj1HcJz/5ydab3/zmqseo3JVXXtk67rjjWv39/VWP0jYXXnhha+7cuYO2/dmf/Vnr0ksvrWiiV+aM12959tlns3nz5px//vkvbHvNa16T888/P9/97ncrnIwSdu3alSSZMGFCxZOU02w2c9ttt2X37t2ZMWNG1eO0XXd3dy688MJBf8dHux/84AeZPHly3vCGN+TSSy/NE088UfVIbfftb387Z5xxRi6++OJMnDgxb3rTm3LLLbdUPVZRzz77bP7xH/8xc+fOTUdHR9XjtM0555yT+++/Pz09PUmShx9+OA8++GBmzZpV8WRD66x6gJHmZz/7WZrNZiZNmjRo+6RJk/Jf//VfFU1FCf39/VmwYEFmzpyZ6dOnVz1O223dujUzZszIb37zm7z2ta/N6tWrc9JJJ1U9Vlvddttteeihh7Jx48aqRynm7LPPzsqVK3P88cfnySefzOLFi/OWt7wl27Zty7hx46oer21+9KMfZfny5Vm4cGGuvvrqbNy4MVdccUXGjBmTOXPmVD1eEXfccUd+8Ytf5IMf/GDVo7TVpz71qfzyl7/MCSeckEajkWazmWuvvTaXXnpp1aMNSfGC/9Pd3Z1t27bV4vqXJDn++OOzZcuW7Nq1K9/4xjcyZ86crF27dtSWrx//+Me58sors2bNmhx44IFVj1PMS7/rP+WUU3L22Wfn6KOPzu23354PfehDFU7WXv39/TnjjDPyhS98IUnypje9Kdu2bctXvvKV2hSvr371q5k1a1YmT55c9Shtdfvtt+ef/umfsmrVqkybNi1btmzJggULMnny5BG5rxWv33LYYYel0Whkx44dg7bv2LEjRxxxREVT0W6XX3557rrrrqxbty5HHXVU1eMUMWbMmLzxjW9Mkpx++unZuHFjvvSlL+Wmm26qeLL22Lx5c3bu3JnTTjvthW3NZjPr1q3LjTfemN7e3jQajQonLON1r3tdpk6dmkcffbTqUdrqyCOPfNk3ESeeeGK++c1vVjRRWY8//njuu+++fOtb36p6lLb7xCc+kU996lN53/velyQ5+eST8/jjj2fJkiUjsni5xuu3jBkzJqeffnruv//+F7b19/fn/vvvr8X1L3XTarVy+eWXZ/Xq1fm3f/u3HHvssVWPVJn+/v709vZWPUbbnHfeedm6dWu2bNnywu2MM87IpZdemi1bttSidCXJ008/nR/+8Ic58sgjqx6lrWbOnPmyj4bp6enJ0UcfXdFEZa1YsSITJ07MhRdeWPUobffMM8/kNa8ZXGcajUb6+/srmuiVOeM1hIULF2bOnDk544wzctZZZ+WGG27I7t27c9lll1U9Wls9/fTTg74Lfuyxx7Jly5ZMmDAhU6ZMqXCy9unu7s6qVaty5513Zty4cXnqqaeSJOPHj8/YsWMrnq59rrrqqsyaNStTpkzJr371q6xatSoPPPBA7r333qpHa5tx48a97Nq9gw46KIceeuiovqbv4x//eGbPnp2jjz46P/3pT3PNNdek0Wjk/e9/f9WjtdXHPvaxnHPOOfnCF76QSy65JBs2bMjNN9+cm2++uerR2q6/vz8rVqzInDlz0tk5+v+Znz17dq699tpMmTIl06ZNy/e+970sXbo0c+fOrXq0oVX93ypHqi9/+cutKVOmtMaMGdM666yzWuvXr696pLb793//91aSl93mzJlT9WhtM1TeJK0VK1ZUPVpbzZ07t3X00Ue3xowZ0zr88MNb5513Xutf//Vfqx6ruDp8nMR73/ve1pFHHtkaM2ZM6/Wvf33rve99b+vRRx+teqwi/uVf/qU1ffr0VldXV+uEE05o3XzzzVWPVMS9997bStJ65JFHqh6liF/+8petK6+8sjVlypTWgQce2HrDG97Q+pu/+ZtWb29v1aMNqaPVGqEf7QoAMMq4xgsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxesV9Pb2ZtGiRaP607yHUsfcdcycyF2n3HXMnMgt98jjc7xewS9/+cuMHz8+u3btysEHH1z1OMXUMXcdMydy1yl3HTMncss98jjjBQBQiOIFAFCI4gUAUIji9Qo6Ozszb968Wvx295eqY+46Zk7krlPuOmZO5JZ75HFx/StoNpvZvn17TjzxxDQajarHKaaOueuYOZG7TrnrmDmRW+6RxxkvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBC9ql4LVu2LMccc0wOPPDAnH322dmwYcOrPRcAwKgz7OL19a9/PQsXLsw111yThx56KKeeemre+c53ZufOne2YDwBg1Bh28Vq6dGk+/OEP57LLLstJJ52Ur3zlK/mDP/iD/MM//EM75gMAGDU6h/PkZ599Nps3b85VV131wrbXvOY1Of/88/Pd7353yNf09vamt7d38KKdnenq6tqHcctqNpuD7uuijrnrmDmRu06565g5kVvushqNxu98Tker1Wrt7R/405/+NK9//evzne98JzNmzHhh+1//9V9n7dq1+Y//+I+XvWbRokVZvHjxoG3z5s3L/Pnz93ZZAIARb/r06b/zOcM647UvrrrqqixcuHDwovvRGa+enp5MnTp1r1rsaFHH3HXMnMhdp9x1zJzILffIM6ziddhhh6XRaGTHjh2Dtu/YsSNHHHHEkK/p6uraL0rWK2k0GiN2B7ZTHXPXMXMid53UMXMid92M5NzDurh+zJgxOf3003P//fe/sK2/vz/333//oB89AgDwcsP+UePChQszZ86cnHHGGTnrrLNyww03ZPfu3bnsssvaMR8AwKgx7OL13ve+N//zP/+Tz3zmM3nqqafyx3/8x7nnnnsyadKkdswHADBq7NPF9Zdffnkuv/zyV3sWAIBRze9qBAAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKGTYxWvdunWZPXt2Jk+enI6Ojtxxxx1tGAsAYPQZdvHavXt3Tj311Cxbtqwd8wAAjFqdw33BrFmzMmvWrHbMAgAwqg27eA1Xb29vent7By/a2Zmurq52L/17azabg+7roo6565g5kbtOueuYOZFb7rIajcbvfE5Hq9Vq7esCHR0dWb16dS666KI9PmfRokVZvHjxoG3z5s3L/Pnz93VZAIARZ/r06b/zOW0vXvv7Ga+enp5MnTp1r1rs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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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TtWjtN3TTz+dvfYaXmcajUaGhoYqmujlOeI1ggULFmT27NmZNm1aTj311Fx//fXZvn17LrrooqpHa6unnnpq2HfBjz32WDZs2JD9998/U6ZMqXCy9unr68vy5ctz1113ZcKECXnyySeTJBMnTsz48eMrnq59rrjiisycOTNTpkzJr371qyxfvjz3339/7rnnnqpHa5sJEya85Ny9fffdN6997WvH9Dl9H/3oRzNr1qwcdthh+elPf5qrrroqjUYjH/jAB6oera0+8pGP5PTTT88111yTCy64IA8++GBuueWW3HLLLVWP1nZDQ0NZtmxZZs+ene7usf9lftasWbn66qszZcqUHHfccfn2t7+dJUuW5OKLL656tJFV/WuVneqGG25oTZkypTVu3LjWqaee2lqzZk3VI7Xdf/3Xf7WSvOQye/bsqkdrm5HyJmktW7as6tHa6uKLL24ddthhrXHjxrUOPPDA1plnntn6j//4j6rHKq4Obyfxvve9r3XIIYe0xo0b13rd617Xet/73td69NFHqx6riH/7t39r9fb2tnp6elpHH31065Zbbql6pCLuueeeVpLWI488UvUoRfzyl79sXXbZZa0pU6a09tlnn9brX//61t/8zd+0BgYGqh5tRF2tVoe+tSsAwBjjHC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFK+XMTAwkIULF47pd/MeSR1z1zFzInedctcxcyK33J3H+3i9jF/+8peZOHFitm3blle/+tVVj1NMHXPXMXMid51y1zFzIrfcnccRLwCAQhQvAIBCFC8AgEIUr5fR3d2dOXPm1OKvu79YHXPXMXMid51y1zFzIrfcncfJ9S+j2Wxm8+bNOeaYY9JoNKoep5g65q5j5kTuOuWuY+ZEbrk7jyNeAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACF7Fbxuummm3L44Ydnn332yWmnnZYHH3zwlZ4LAGDMGXXx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KF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhShedKxWq9xlcLCZjRs3ZXCwWXTdOuauOnMn5U5axS7N5mA2bdqYZnOw6Lr1zA07p3gBABSieAEAFKJ4AQAUongBABTSXfUAsHNdxVZqNJLe3mLLvchIJ+KO9dwjZO4qlzlJGknKx3bS9fMKPt+eazqNI14AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXtTa4sXJKackEyYkkyYl552XPPJI1VO1X11zr04yK8nkJF1J7qx0GtrJc02n+oOK17XXXpuurq7Mnz//FRoHylq1KunrS9asSVauTJ59Njn77GT79qona6+65t6e5MQkN1U9CG3nuaZTde/uA9euXZubb745J5xwwis5DxT1jW8Mv33rrTuOAK1fn7ztbZWMVERdc8/87YWxz3NNp9qtI15PPfVULrzwwnz+85/Pfvvt90rPBJXZtm3H9f77VztHaXXNDVDabh3x6uvryznnnJOzzjorf/u3f/uy9x0YGMjAwMDwRbu709PTsztLF9VsNodd10Wn5G40yq43NJTMn5/MmJH09pZZc6SP8VjPPWLm9i9buao/nzrl8zoZ+893J3yMO+n5Lqnq3I1d2IGPunjddttteeihh7J27dpduv/ixYuzaNGiYdvmzJmTuXPnjnbpyvT391c9QiWqzl2q/Dynry/ZtCl54IFya27evPkl28Z67hEzl1m6UiPlrkLVn9fJ2H++O+W5Tjrj+a5CVbl7d2EH3tVqtVq7+h/+6Ec/yrRp07Jy5crnz+0644wz8qY3vSnXX3/9iI/Z04949ff3Z+rUqbvUYseKTsndaOz2KYijNm9ectddyerVyRFHFFs2zebgS7aN9dwjZu4ul/k5XUlWJDmv0HrNwZfmLqlTPq+T8s933Z7rpLOe75Kqzv2KH/Fav359tm7dmpNOOun5bc1mM6tXr86NN96YgYGBlyza09OzR5Ssl9NoNGr1wn1OHXK3WskllyQrViT331+2dCW79knaDlXmHuuvqZ3plNx1+LyuWid9fOv6fHdy7lEVrzPPPDMbN24c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vAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAAoZVfFqNpv5xCc+kSOOOCLjx4/PkUcemU9/+tNptVrtmg8AYMzoHs2dr7vuuixdujRf/OIXc9xxx2XdunW56KKLMnHixFx66aXtmhEAYEwYVfH65je/mXPPPTfnnHNOkuTwww/Pl770pTz44INtGQ4AYCwZVfE6/fTTc8stt6S/vz9Tp07Nww8/nAceeCBLlizZ6WMGBgYyMDAwfNHu7vT09OzexAU1m81h13VRx9x1zJzIXafcdcycyC13WY1G4/fep6s1ihO0hoaGcuWVV+Yzn/lMGo1Gms1mrr766lxxxRU7fczChQuzaNGiYdvmzJmTuXPn7uqyAAAdr7e39/feZ1TF67bbbsvHPvax/N3f/V2OO+64bNiwIfPnz8+SJUsye/bsER+zpx/xeu7o3q602LGijrnrmDmRu06565g5kVvusnZlzVH9qPFjH/tYPv7xj+f9739/kuT444/P448/nsWLF++0ePX09OwRJevlNBqNWr1wn1PH3HXMnMhdJ3XMnMhdN52ce1RvJ/H0009nr72GP6TRaGRoaOgVHQoAYCwa1RGvWbNm5eqrr86UKVNy3HHH5dvf/naWLFmSiy++uF3zAQCMGaMqXjfccEM+8YlPZO7cudm6dWsmT56cv/qrv8onP/nJds0HADBmjKp4TZgwIddff32uv/76No0DADB2+VuNAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhXS1Wq1W1UMAANSBI14AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACF/H9RSFVWvFHJEgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAsU0lEQVR4nO3df5BddX038PdylyyphgiSAJEGEBsIWUEhwIT4AwXRDGZgOg+opW0Ex+mTbICY0SJ0lEQfCNhpBgeYCNYGZ9oIVI1YZ4AGWpJhNCUEwyQ2sqIMWIWkdjRK0IXcvc8fKT8WNpiNud9zs+f1mrmz3DPn8v28d2/Ovvfcs3e7Wq1WKwAAtN1+VQ8AAFAXihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhitcu3HTTTTnqqKNywAEH5LTTTsuDDz5Y9Uhtt2bNmsyePTuTJk1KV1dXvvWtb1U9UtstWbIkp5xySsaNG5eJEyfmvPPOy6OPPlr1WG23bNmynHDCCTnwwANz4IEHZsaMGbnrrruqHquoa6+9Nl1dXVmwYEHVo7TVokWL0tXVNeR23HHHVT1WET/72c/y53/+53njG9+YsWPH5q1vfWseeuihqsdqq6OOOupVX++urq709fVVPVrbNJvNfOYzn8nRRx+dsWPH5phjjsnnP//5dOpfRFS8hnH77bdn4cKFueqqq/Lwww/nxBNPzPvf//5s3bq16tHaavv27TnxxBNz0003VT1KMatXr05fX1/Wrl2bVatW5fnnn8/ZZ5+d7du3Vz1aWx1xxBG59tprs379+jz00EN573vfm3PPPTc/+MEPqh6tiHXr1uXmm2/OCSecUPUoRUybNi1PPfXUi7cHHnig6pHa7pe//GVmzpyZ/fffP3fddVf+8z//M3/3d3+Xgw46qOrR2mrdunVDvtarVq1Kkpx//vkVT9Y+1113XZYtW5Ybb7wxmzdvznXXXZcvfOELueGGG6oebXgtXuXUU09t9fX1vXi/2Wy2Jk2a1FqyZEmFU5WVpLVy5cqqxyhu69atrSSt1atXVz1KcQcddFDr7//+76seo+1+85vftP7kT/6ktWrVqta73/3u1mWXXVb1SG111VVXtU488cSqxyju8ssvb73jHe+oeozKXXbZZa1jjjmmNTg4WPUobXPOOee0Lr744iHb/vRP/7R14YUXVjTRa3PG6xWee+65rF+/PmedddaL2/bbb7+cddZZ+d73vlfhZJSwbdu2JMnBBx9c8STlNJvN3Hbbbdm+fXtmzJhR9Tht19fXl3POOWfIv/HR7kc/+lEmTZqUN7/5zbnwwgvz5JNPVj1S233729/O9OnTc/7552fixIl5+9vfni9/+ctVj1XUc889l3/8x3/MxRdfnK6urqrHaZvTTz899913X/r7+5MkjzzySB544IHMmjWr4smG1131AJ3mF7/4RZrNZg499NAh2w899ND88Ic/rGgqShgcHMyCBQsyc+bM9Pb2Vj1O223cuDEzZszI7373u7z+9a/PypUrc/zxx1c9Vlvddtttefjhh7Nu3bqqRynmtNNOy6233ppjjz02Tz31VBYvXpx3vvOd2bRpU8aNG1f1eG3zk5/8JMuWLcvChQtz5ZVXZt26dbn00kszZsyYzJkzp+rxivjWt76VX/3qV/noRz9a9Sht9elPfzq//vWvc9xxx6XRaKTZbObqq6/OhRdeWPVow1K84H/19fVl06ZNtbj+JUmOPfbYbNiwIdu2bcvXv/71zJkzJ6tXrx615eunP/1pLrvssqxatSoHHHBA1eMU8/Kf+k844YScdtppOfLII3PHHXfkYx/7WIWTtdfg4GCmT5+ea665Jkny9re/PZs2bcqXvvSl2hSvr3zlK5k1a1YmTZpU9Shtdccdd+Sf/umfsmLFikybNi0bNmzIggULMmnSpI78Witer3DIIYek0Whky5YtQ7Zv2bIlhx12WEVT0W7z58/Pd77znaxZsyZHHHFE1eMUMWbMmLzlLW9Jkpx88slZt25dvvjFL+bmm2+ueLL2WL9+fbZu3ZqTTjrpxW3NZjNr1qzJjTfemIGBgTQajQonLOMNb3hDpkyZkscee6zqUdrq8MMPf9UPEVOnTs03vvGNiiYq64knnsi9996bb37zm1WP0naf+tSn8ulPfzof/vCHkyRvfetb88QTT2TJkiUdWbxc4/UKY8aMycknn5z77rvvxW2Dg4O57777anH9S920Wq3Mnz8/K1euzL/927/l6KOPrnqkygwODmZgYKDqMdrmzDPPzMaNG7Nhw4YXb9OnT8+FF16YDRs21KJ0JckzzzyTH//4xzn88MOrHqWtZs6c+aq3hunv78+RRx5Z0URlLV++PBMnTsw555xT9Sht9+yzz2a//YbWmUajkcHBwYomem3OeA1j4cKFmTNnTqZPn55TTz01119/fbZv356LLrqo6tHa6plnnhnyU/Djjz+eDRs25OCDD87kyZMrnKx9+vr6smLFitx5550ZN25cnn766STJ+PHjM3bs2Iqna58rrrgis2bNyuTJk/Ob3/wmK1asyP3335977rmn6tHaZty4ca+6du91r3td3vjGN47qa/o++clPZvbs2TnyyCPz85//PFdddVUajUY+8pGPVD1aW33iE5/I6aefnmuuuSYXXHBBHnzwwdxyyy255ZZbqh6t7QYHB7N8+fLMmTMn3d2j/9v87Nmzc/XVV2fy5MmZNm1avv/972fp0qW5+OKLqx5teFX/WmWnuuGGG1qTJ09ujRkzpnXqqae21q5dW/VIbffv//7vrSSvus2ZM6fq0dpmuLxJWsuXL696tLa6+OKLW0ceeWRrzJgxrQkTJrTOPPPM1r/+679WPVZxdXg7iQ996EOtww8/vDVmzJjWm970ptaHPvSh1mOPPVb1WEX8y7/8S6u3t7fV09PTOu6441q33HJL1SMVcc8997SStB599NGqRyni17/+deuyyy5rTZ48uXXAAQe03vzmN7f+5m/+pjUwMFD1aMPqarU69K1dAQBGGdd4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4vYaBgYEsWrRoVL+b93DqmLuOmRO565S7jpkTueXuPN7H6zX8+te/zvjx47Nt27YceOCBVY9TTB1z1zFzInedctcxcyK33J3HGS8AgEIULwCAQhQvAIBCFK/X0N3dnblz59bir7u/XB1z1zFzInedctcxcyK33J3HxfWvodlsZvPmzZk6dWoajUbV4xRTx9x1zJzIXafcdcycyC1353HGCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgkD0qXjfddFOOOuqoHHDAATnttNPy4IMP7u25AABGnREX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HabVKnfbsaOZjRs3ZceOZrE1q87cSbmTVrFbs7kjmzZtTLO5o+i69cwNu6Z4AQAUongBABSieAEAFKJ4AQAU0l31ALBrXcVWajSS3t5iy73McBfijvbc1WZO6pnbc7wkF9iza854AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUonhRa0uWJKeckowbl0ycmJx3XvLoo1VP1X51zF3HzIncdctN5/uDite1116brq6uLFiwYC+NA2WtXp309SVr1yarViXPP5+cfXayfXvVk7VXHXPXMXMid91y0/m69/SB69aty80335wTTjhhb84DRd1999D7t96686fj9euTd72rkpGKqGPuOmZO5H5BXXLT+fbojNczzzyTCy+8MF/+8pdz0EEH7e2ZoDLbtu38ePDB1c5RWh1z1zFzInfdctN59uiMV19fX84555ycddZZ+X//7/+95r4DAwMZGBgYumh3d3p6evZk6aKazeaQj3XRKbkbjbLrDQ4mCxYkM2cmvb1l1hzuczzac9cxcyL3y4323FUfO18+QyfMUlLVuRu78eQecfG67bbb8vDDD2fdunW7tf+SJUuyePHiIdvmzp2befPmjXTpyvT391c9QiWqzl3qG8ML+vqSTZuSBx4ot+bmzZtftW20565j5kTulxvtuYfLXJWqj+NVqSp37248ubtarVZrd/+HP/3pTzN9+vSsWrXqxWu7zjjjjLztbW/L9ddfP+xj9vUzXv39/ZkyZcputdjRolNyNxp7fAniiM2fn9x5Z7JmTXL00cWWTbO541XbRnvuOmZO5H650Z57uMyldcpxvLSqc+/1M17r16/P1q1bc9JJJ724rdlsZs2aNbnxxhszMDDwqkV7enr2iZL1WhqNRq2euC+oQ+5WK7nkkmTlyuT++8t+Q0p27x9pO1SZu46ZE7lLq+NzfDh1OI4Pp5Nzj6h4nXnmmdm4ceOQbRdddFGOO+64XH755R0bEnalry9ZsWLnT8TjxiVPP71z+/jxydix1c7WTnXMXcfMidx1y03nG9FLjcP5fS817suazWY2b96cqVOn1qpUdk7urvavsIslli9PPvrRti+fZLh/fqM9dx0zJ3K/3GjP/Qd9W90rOuc4Xta+kLvcC+3Qgf6wHzv2XXXMXcfMidzQaf7g4nX//ffvhTEAAEY/f6sRAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMWLDtYqdms2d2TTpo1pNncUXbeeuavNXNfcnuNVf61hJ8ULAKAQxQsAoBDFCwCgEMULAKAQxQsAoJDuqgeAXenqKrlaI0lvyQWTJK1hfgFqtOceLnNSNHQajaS3+Jd7uODlcleTOalnbr/ZyK454wUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBUnmzUsefzz57W+TtWuTU06peqIy6pR7yZKd+caNSyZOTM47L3n00aqnaj+565Wbzjei4rVo0aJ0dXUNuR133HHtmg2KuOCCZOnSZPHi5KSTkkceSe65J5kwoerJ2qtuuVevTvr6dhbMVauS559Pzj472b696snaS+565abzdbVardbu7rxo0aJ8/etfz7333vvitu7u7hxyyCFtGa5qzWYzmzdvztSpU9NoNKoep5hOyd3VVWadtWuTdeuSSy55ad2f/jS54Ybkuuvau/Zw//pGe+7hjziFQr/Mf//3zjMhq1cn73pXiRWHCy53KWVz7/a31bbplON4aftC7hG/1Njd3Z3DDjvsxdtoLV3Uw/77JyefnLzsZ4m0Wjvvz5hR3VztVtfcL7dt286PBx9c7RylyV3tHNA90gf86Ec/yqRJk3LAAQdkxowZWbJkSSZPnrzL/QcGBjIwMDB00e7u9PT0jHzawprN5pCPddE5udv/08ohhyTd3cmWLUO3b9mSlHgVffjP8ejOPVzm0j+YDg4mCxYkM2cmvb1l1pT7JaM9d/XHzk46jpdVde7dOcs2ouJ12mmn5dZbb82xxx6bp556KosXL8473/nObNq0KePGjRv2MUuWLMnixYuHbJs7d27mzZs3kqUr1d/fX/UIlag+d6HvDBXavHnzMFtHd+7hMpcqAS/o60s2bUoeeKDcmnK/ZLTnHv7fdTWqP45Xo6rcvbvx5B7RNV6v9Ktf/SpHHnlkli5dmo997GPD7rOvn/Hq7+/PlClTOva14nbolNzd3e1fe//9k2efTf7P/0nuvPOl7bfemrzhDTt/E6qddux49U9loz33cJkbjRGffN9j8+fvzLxmTXL00cWWTbO541Xb5G6/KnIPl7m0TjmOl1Z17r1+xuuV3vCGN2TKlCl57LHHdrlPT0/PPlGyXkuj0ajVE/cFdcj9/PPJ+vXJmWe+VEC6unbev/HG9q9f1ee3ytxVZW61dv4iwcqVyf33ly0fidylVZm7k46bdTiOD6eTc/9BxeuZZ57Jj3/84/zFX/zF3poHilu6NPnqV5OHHkoefHDntSCve12yfHnVk7VX3XL39SUrVuwsmuPGJU8/vXP7+PHJ2LHVztZOctcrN51vRMXrk5/8ZGbPnp0jjzwyP//5z3PVVVel0WjkIx/5SLvmg7a7446d7131uc8lhx2WbNiQfOADydatVU/WXnXLvWzZzo9nnDF0+/LlyUc/WnqacuQeun2056bzjah4/dd//Vc+8pGP5H/+538yYcKEvOMd78jatWszYbS+4yK1cdNNO291U6fce341675NbugsIypet912W7vmAAAY9fytRgCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULzpWq1XutmNHMxs3bsqOHc2i69Yx9y5SF701mzuyadPGNJs7Cq5bbe5qMtc1N+ya4gUAUIjiBQBQiOIFAFCI4gUAUEh31QPArnUVW6nRSHp7iy33MsNdiDvacw+Tuatc5iRpJCkfu9rclWRO6pl7179FAs54AQCUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUonhRa0uWJKeckowbl0ycmJx3XvLoo1VP1X51zb0myewkk5J0JflWpdOUU8fcdczMvkHxotZWr076+pK1a5NVq5Lnn0/O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zTHHHJPPf/7zabVa7ZoPAGDU6B7Jztddd12WLVuWr371q5k2bVoeeuihXHTRRRk/fnwuvfTSds0IADAqjKh4ffe73825556bc845J0ly1FFH5Wtf+1oefPDBtgwHADCajKh4nX766bnlllvS39+fKVOm5JFHHskDDzyQpUuX7vIxAwMDGRgYGLpod3d6enr2bOKCms3mkI91UcfcdcycyF2n3HXMnMgtd1mNRuP37tPVGsEFWoODg7nyyivzhS98IY1GI81mM1dffXWuuOKKXT5m0aJFWbx48ZBtc+fOzbx583Z3WQCAjtfb2/t79xlR8brtttvyqU99Kn/7t3+badOmZcOGDVmwYEGWLl2aOXPmDPuYff2M1wtn93anxY4Wdcxdx8yJ3HXKXcfMidxyl7U7a47opcZPfepT+fSnP50Pf/jDSZK3vvWteeKJJ7JkyZJdFq+enp59omS9lkajUasn7gvqmLuOmRO566SOmRO566aTc4/o7SSeffbZ7Lff0Ic0Go0MDg7u1aEAAEajEZ3xmj17dq6++upMnjw506ZNy/e///0sXbo0F198cbvmAwAYNUZUvG644YZ85jOfybx587J169ZMmjQpf/VXf5XPfvaz7ZoPAGDUGFHxGjduXK6//vpcf/31bRoHAGD08rcaAQAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAACulqtVqtqocAAKgDZ7wAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAK+f+HI5S4NFINcAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAsQElEQVR4nO3df5DcdX0/8OexR45UYwQhQKQJiAYCBygEaIg/UBDNYEamU1BL2wiO029yAWJGi9BREi0G7DSDAzSCtcGZNgJVI9Yp0kBLMoym+YFhEptyogxYhaR2NErQg+zt94+UHycXzMXs+7O5z+Mxs7PZz3w279fzbu9zz/vs3l5Xq9VqBQCAtjug6gEAAOpC8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8dqNm2++OUcffXQOOuignHnmmVm7dm3VI7Xd6tWrM2vWrEycODFdXV35+te/XvVIbbd48eKcfvrpGTduXCZMmJALLrggDz/8cNVjtd3SpUtz8skn51WvelVe9apXZfr06bn77rurHquo6667Ll1dXZk/f37Vo7TVwoUL09XVNeRy/PHHVz1WET/+8Y/zJ3/yJ3nNa16TsWPH5qSTTsr69eurHqutjj766Jd8vru6utLX11f1aG3TbDbziU98Isccc0zGjh2bY489Np/+9KfTqX8RUfEaxh133JEFCxbkmmuuyYMPPphTTjkl73rXu7Jt27aqR2urHTt25JRTTsnNN99c9SjFrFq1Kn19fVmzZk1WrlyZZ599Nuedd1527NhR9WhtddRRR+W6667Lhg0bsn79+rzjHe/Ie9/73nzve9+rerQi1q1bl1tuuSUnn3xy1aMUceKJJ+aJJ554/vLAAw9UPVLb/exnP8uMGTNy4IEH5u67785//ud/5m/+5m9y8MEHVz1aW61bt27I53rlypVJkgsvvLDiydrn+uuvz9KlS3PTTTdly5Ytuf766/PZz342N954Y9WjDa/FS5xxxhmtvr6+5283m83WxIkTW4sXL65wqrKStFasWFH1GMVt27atlaS1atWqqkcp7uCDD2793d/9XdVjtN0vf/nL1hve8IbWypUrW29729taV1xxRdUjtdU111zTOuWUU6oeo7grr7yy9eY3v7nqMSp3xRVXtI499tjW4OBg1aO0zfnnn9+69NJLh2z7wz/8w9bFF19c0UQvzxmv3/DMM89kw4YNOffcc5/fdsABB+Tcc8/Nd77znQono4Tt27cnSQ455JCKJymn2Wzm9ttvz44dOzJ9+vSqx2m7vr6+nH/++UO+xke773//+5k4cWJe97rX5eKLL87jjz9e9Uht941vfCPTpk3LhRdemAkTJuRNb3pTvvCFL1Q9VlHPPPNM/uEf/iGXXnppurq6qh6nbc4666zcd9996e/vT5I89NBDeeCBBzJz5syKJxted9UDdJqf/vSnaTabOfzww4dsP/zww/Nf//VfFU1FCYODg5k/f35mzJiR3t7eqsdpu02bNmX69On59a9/nVe+8pVZsWJFTjjhhKrHaqvbb789Dz74YNatW1f1KMWceeaZue2223LcccfliSeeyKJFi/KWt7wlmzdvzrhx46oer21++MMfZunSpVmwYEGuvvrqrFu3LpdffnnGjBmT2bNnVz1eEV//+tfz85//PB/84AerHqWtPv7xj+cXv/hFjj/++DQajTSbzVx77bW5+OKLqx5tWIoX/J++vr5s3ry5Fq9/SZLjjjsuGzduzPbt2/OVr3wls2fPzqpVq0Zt+frRj36UK664IitXrsxBBx1U9TjFvPin/pNPPjlnnnlmJk+enDvvvDMf+tCHKpysvQYHBzNt2rR85jOfSZK86U1vyubNm/P5z3++NsXri1/8YmbOnJmJEydWPUpb3XnnnfnHf/zHLF++PCeeeGI2btyY+fPnZ+LEiR35uVa8fsOhhx6aRqORrVu3Dtm+devWHHHEERVNRbvNmzcv3/zmN7N69eocddRRVY9TxJgxY/L6178+SXLaaadl3bp1+dznPpdbbrml4snaY8OGDdm2bVtOPfXU57c1m82sXr06N910UwYGBtJoNCqcsIxXv/rVmTJlSh555JGqR2mrI4888iU/REydOjVf/epXK5qorMceeyz33ntvvva1r1U9Stt97GMfy8c//vG8//3vT5KcdNJJeeyxx7J48eKOLF5e4/UbxowZk9NOOy333Xff89sGBwdz33331eL1L3XTarUyb968rFixIv/2b/+WY445puqRKjM4OJiBgYGqx2ibc845J5s2bcrGjRufv0ybNi0XX3xxNm7cWIvSlSRPPfVUfvCDH+TII4+sepS2mjFjxkveGqa/vz+TJ0+uaKKyli1blgkTJuT888+vepS2e/rpp3PAAUPrTKPRyODgYEUTvTxnvIaxYMGCzJ49O9OmTcsZZ5yRG264ITt27Mgll1xS9Wht9dRTTw35KfjRRx/Nxo0bc8ghh2TSpEkVTtY+fX19Wb58ee66666MGzcuTz75ZJJk/PjxGTt2bMXTtc9VV12VmTNnZtKkSfnlL3+Z5cuX5/77788999xT9WhtM27cuJe8du8Vr3hFXvOa14zq1/R99KMfzaxZszJ58uT85Cc/yTXXXJNGo5EPfOADVY/WVh/5yEdy1lln5TOf+UwuuuiirF27NrfeemtuvfXWqkdru8HBwSxbtiyzZ89Od/fo/zY/a9asXHvttZk0aVJOPPHEfPe7382SJUty6aWXVj3a8Kr+tcpOdeONN7YmTZrUGjNmTOuMM85orVmzpuqR2u7f//3fW0lecpk9e3bVo7XNcHmTtJYtW1b1aG116aWXtiZPntwaM2ZM67DDDmudc845rX/913+teqzi6vB2Eu973/taRx55ZGvMmDGt1772ta33ve99rUceeaTqsYr453/+51Zvb2+rp6endfzxx7duvfXWqkcq4p577mklaT388MNVj1LEL37xi9YVV1zRmjRpUuuggw5qve51r2v95V/+ZWtgYKDq0YbV1Wp16Fu7AgCMMl7jBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIji9TIGBgaycOHCUf1u3sOpY+46Zk7krlPuOmZO5Ja783gfr5fxi1/8IuPHj8/27dvzqle9qupxiqlj7jpmTuSuU+46Zk7klrvzOOMFAFCI4gUAUIjiBQBQiOL1Mrq7uzNnzpxa/HX3F6tj7jpmTuSuU+46Zk7klrvzeHH9y2g2m9myZUumTp2aRqNR9TjF1DF3HTMnctcpdx0zJ3LL3Xmc8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKGSvitfNN9+co48+OgcddFDOPPPMrF27dl/PBQAw6oy4eN1xxx1ZsGBBrrnmmjz44IM55ZRT8q53vSvbtm1rx3wAAKPGiIvXkiVL8uEPfziXXHJJTjjhhHz+85/P7/3e7+Xv//7v2zEfAMCo0T2SnZ955pls2LAhV1111fPbDjjggJx77rn5zne+M+x9BgYGMjAwMHTR7u709PTsxbhlNZvNIdd1UcfcdcycyF2n3HXMnMgtd1mNRuO37tPVarVae/of/uQnP8lrX/vafPvb38706dOf3/4Xf/EXWbVqVf7jP/7jJfdZuHBhFi1aNGTbnDlzMnfu3D1dFgCg4/X29v7WfUZ0xmtvXHXVVVmwYMHQRfejM179/f2ZMmXKHrXY0aKOueuYOZG7TrnrmDmRW+7OM6Lideihh6bRaGTr1q1Dtm/dujVHHHHEsPfp6enZL0rWy2k0Gh37CWynOuauY+ZE7jqpY+ZE7rrp5NwjenH9mDFjctppp+W+++57ftvg4GDuu+++IU89AgDwUiN+qnHBggWZPXt2pk2bljPOOCM33HBDduzYkUsuuaQd8wEAjBojLl7ve9/78j//8z/55Cc/mSeffDJvfOMb861vfSuHH354O+YDABg19urF9fPmzcu8efP29SwAAKOav9UIAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQyIiL1+rVqzNr1qxMnDgxXV1d+frXv96GsQAARp8RF68dO3bklFNOyc0339yOeQAARq3ukd5h5syZmTlzZjtmAQAY1UZcvEZqYGAgAwMDQxft7k5PT0+7l/6dNZvNIdd1UcfcdcycyF2n3HXMnMgtd1mNRuO37tPVarVae7tAV1dXVqxYkQsuuGC3+yxcuDCLFi0asm3OnDmZO3fu3i4LANBxent7f+s+bS9e+/sZr/7+/kyZMmWPWuxoUcfcdcycyF2n3HXMnMgtd1l7smbbn2rs6enZL0rWy2k0GrV64D6njrnrmDmRu07qmDmRu246Obf38QIAKGTEZ7yeeuqpPPLII8/ffvTRR7Nx48YccsghmTRp0j4dDgBgNBlx8Vq/fn3e/va3P397wYIFSZLZs2fntttu22eDAQCMNiMuXmeffXZ+h9fjAwDUltd4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABTSXfUAsDtdXSVXayTpLblgWq3ht8tdQmfkHu2Zk91/vqGunPGCipT9ptvZfCyAulC8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8IMncucmjjya/+lWyZk1y+ulVT1RGHXPXMXNS39zQaRQvau+ii5IlS5JFi5JTT00eeii5557ksMOqnqy96pi7jpmT+uaGTjSi4rV48eKcfvrpGTduXCZMmJALLrggDz/8cLtmgyIWLEi+8IXkttuSLVuS//f/kqefTi69tOrJ2quOueuYOalvbuhEIypeq1atSl9fX9asWZOVK1fm2WefzXnnnZcdO3a0az5oqwMPTE47Lbn33he2tVq7bk+fXt1c7VbH3HXMnNQ3N3Sq7pHs/K1vfWvI7dtuuy0TJkzIhg0b8ta3vnWfDgYlHHpo0t2dbN06dPvWrcnxx1czUwl1zF3HzEl9c0OnGlHx+k3bt29PkhxyyCG73WdgYCADAwNDF+3uTk9Pz++ydBHNZnPIdV10Tu5Gxeu33/Af49Gdu46ZE7mrXr/qOUqTu5rcjcZv/5re6+I1ODiY+fPnZ8aMGent7d3tfosXL86iRYuGbJszZ07mzp27t0sX19/fX/UIlag+9+4fV/vKT3+a7NyZHH740O2HH548+WTbl8+WLVuG2Tq6c9cxcyJ31ao/nlVD7rJerg89p6vVarX25j+fM2dO7r777jzwwAM56qijdrvf/n7Gq7+/P1OmTNmjFjtadEru7u4ya69Zk6xdm1x++a7bXV3J448nN92UXH99e9feufOlP5WN9tx1zJzIXZVOOZ6VJnc1udt2xmvevHn55je/mdWrV79s6UqSnp6e/aJkvZxGo1GrB+5z6pJ7yZLkS19K1q/f9c1p/vzkFa9Ili1r/9pVfnyryl3HzIncVavL8ew3yd15RlS8Wq1WLrvssqxYsSL3339/jjnmmHbNBcXceeeu9zP61KeSI45INm5M3v3uZNu2qidrrzrmrmPmpL65oRON6KnGuXPnZvny5bnrrrty3HHHPb99/PjxGTt2bFsGrFKz2cyWLVsyderUjm3O7dApubu6Klu6mOG++kZ77jpmTuSuSqccz0qTu3Nzj+h9vJYuXZrt27fn7LPPzpFHHvn85Y477mjXfAAAo8aIn2oEAGDv+FuNAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhShedKxWq9xl585mNm3anJ07m0XXrWPuqjPXNXcnPcahzhQvAIBCFC8AgEIULwCAQhQvAIBCuqseAHanq6vkao0kvSUXTDL8i49He+7qMyedkjspF7zRSHrLP8STDBd8tOf2WwXsnjNeAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF6QZO7c5NFHk1/9KlmzJjn99KonKqOOueuWefHiXRnHjUsmTEguuCB5+OGqp2q/uuam8yle1N5FFyVLliSLFiWnnpo89FByzz3JYYdVPVl71TF3HTOvWpX09e0qmStXJs8+m5x3XrJjR9WTtVddc7MfaI3A3/7t37ZOOumk1rhx41rjxo1r/cEf/EHrX/7lX0byX+xXdu7c2dq0aVNr586dVY9SVKfkTspc1qxptW688YXbXV2t1n//d6t15ZXtX7uOueuYeXe5W60Uv2zbllaS1qpVpdasY+7qdcpxvLT9IfeIzngdddRRue6667Jhw4asX78+73jHO/Le97433/ve99rTCqHNDjwwOe205N57X9jWau26PX16dXO1Wx1z1zHzcLZv33V9yCHVzlFaXXPTebpHsvOsWbOG3L722muzdOnSrFmzJieeeOKw9xkYGMjAwMDQRbu709PTM8JRy2s2m0Ou66JzcjfavsKhhybd3cnWrUO3b92aHH9825ffzcd4dOeuY+Zk+NyN9sceYnAwmT8/mTEj6e0ts2Ydc1d/7Oyk43hZVedu7MGDe0TF68WazWb+6Z/+KTt27Mj0l/lxcfHixVm0aNGQbXPmzMncuXP3duni+vv7qx6hEtXnLvSdoUJbtmwZZuvozl3HzMnwuUuVn+f09SWbNycPPFBuzTrmHv4xXo3qj+PVqCp37x48uEdcvDZt2pTp06fn17/+dV75yldmxYoVOeGEE3a7/1VXXZUFCxYMXXQ/OuPV39+fKVOm7FGLHS3qlPunP0127kwOP3zo9sMPT558sv3rT506tf2LDKPK3HXMnFSX+znz5iXf/GayenVy1FHl1q1j7qozJ/U6jr/Y/pB7xMXruOOOy8aNG7N9+/Z85StfyezZs7Nq1ardlq+enp79omS9nEaj0bGfwHaqQ+5nn002bEjOOSe5665d27q6dt2+6ab2r1/Vx7fK3HXMnFSXu9VKLrssWbEiuf/+5Jhjyq5fx9yddNysw3F8OJ2ce8TFa8yYMXn961+fJDnttNOybt26fO5zn8stt9yyz4eDEpYsSb70pWT9+mTt2l2vBXnFK5Jly6qerL3qmLuOmfv6kuXLd5XNceNeOLs3fnwydmy1s7VTXXPT+fb6NV7PGRwcfMmL52F/cuedu97H6VOfSo44Itm4MXn3u5Nt26qerL3qmLuOmZcu3XV99tlDty9blnzwg6WnKaeuuel8IypeV111VWbOnJlJkybll7/8ZZYvX577778/99xzT7vmgyJuvnnXpW7qmLtumVutqieoRl1z0/lGVLy2bduWP/uzP8sTTzyR8ePH5+STT84999yTd77zne2aDwBg1BhR8friF7/YrjkAAEY9f6sRAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMWLjtVqlbvs3NnMpk2bs3Nns+i6dcxddeZOyp20il2azZ3ZvHlTms2dRdetZ27YPcULAKAQxQsAoBDFCwCgEMULAKCQ7qoHgN3rKrZSo5H09hZb7kWGeyHuaM9dbeaknrk9xkvyAnt2zxkvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC9qbfHi5PTTk3HjkgkTkgsuSB5+uOqp2q+OueuYOZG7brnpfL9T8bruuuvS1dWV+fPn76NxoKxVq5K+vmTNmmTlyuTZZ5Pzzkt27Kh6svaqY+46Zk7krltuOl/33t5x3bp1ueWWW3LyySfvy3mgqG99a+jt227b9dPxhg3JW99ayUhF1DF3HTMncj+nLrnpfHt1xuupp57KxRdfnC984Qs5+OCD9/VMUJnt23ddH3JItXOUVsfcdcycyF233HSevTrj1dfXl/PPPz/nnntu/uqv/upl9x0YGMjAwMDQRbu709PTszdLF9VsNodc10Wn5G40yq43OJjMn5/MmJH09pZZc7iP8WjPXcfMidwvNtpzV33sfPEMnTBLSVXnbuzBg3vExev222/Pgw8+mHXr1u3R/osXL86iRYuGbJszZ07mzp070qUr09/fX/UIlag6d6lvDM/p60s2b04eeKDcmlu2bHnJttGeu46ZE7lfbLTnHi5zVao+jlelqty9e/Dg7mq1Wq09/Q9/9KMfZdq0aVm5cuXzr+06++yz88Y3vjE33HDDsPfZ38949ff3Z8qUKXvUYkeLTsndaOz1SxBHbN685K67ktWrk2OOKbZsms2dL9k22nPXMXMi94uN9tzDZS6tU47jpVWde5+f8dqwYUO2bduWU0899fltzWYzq1evzk033ZSBgYGXLNrT07NflKyX02g0avXAfU4dcrdayWWXJStWJPffX/YbUrJnX6TtUGXuOmZO5C6tjo/x4dThOD6cTs49ouJ1zjnnZNOmTUO2XXLJJTn++ONz5ZVXdmxI2J2+vmT58l0/EY8blzz55K7t48cnY8dWO1s71TF3HTMnctctN51vRE81Due3PdW4P2s2m9myZUumTp1aq1LZObm72r/CbpZYtiz54AfbvnyS4b78RnvuOmZO5H6x0Z77d/q2uk90znG8rP0hd7kn2qED/W4/duy/6pi7jpkTuaHT/M7F6/77798HYwAAjH7+ViMAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcdrFXs0mzuzObNm9Js7iy6bj1zV5u5rrk9xqv+XMMuihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAId1VDwC709VVcrVGkt6SCyZJWsP8AtRozz1c5qRo6DQaSW/xT/dwwcvlriZzUs/cfrOR3XPGCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULksydmzz6aPKrXyVr1iSnn171RGXUKffixbvyjRuXTJiQXHBB8vDDVU/VfnLXKzedb0TFa+HChenq6hpyOf7449s1GxRx0UXJkiXJokXJqacmDz2U3HNPcthhVU/WXnXLvWpV0te3q2CuXJk8+2xy3nnJjh1VT9ZectcrN52vq9VqtfZ054ULF+YrX/lK7r333ue3dXd359BDD23LcFVrNpvZsmVLpk6dmkajUfU4xXRK7q6uMuusWZOsW5dcdtkL6/7oR8mNNybXX9/etYf76hvtuYc/4hQK/SL/8z+7zoSsWpW89a0lVhwuuNyllM29x99W26ZTjuOl7Q+5R/xUY3d3d4444ojnL6O1dFEPBx6YnHZa8qKfJdJq7bo9fXp1c7VbXXO/2Pbtu64POaTaOUqTu9o5oHukd/j+97+fiRMn5qCDDsr06dOzePHiTJo0abf7DwwMZGBgYOii3d3p6ekZ+bSFNZvNIdd10Tm52//TyqGHJt3dydatQ7dv3ZqUeBZ9+I/x6M49XObSP5gODibz5yczZiS9vWXWlPsFoz139cfOTjqOl1V17j05yzai4nXmmWfmtttuy3HHHZcnnngiixYtylve8pZs3rw548aNG/Y+ixcvzqJFi4ZsmzNnTubOnTuSpSvV399f9QiVqD53oe8MFdqyZcswW0d37uEylyoBz+nrSzZvTh54oNyacr9gtOce/uu6GtUfx6tRVe7ePXhwj+g1Xr/p5z//eSZPnpwlS5bkQx/60LD77O9nvPr7+zNlypSOfa64HTold3d3+9c+8MDk6aeTP/qj5K67Xth+223Jq1+96zeh2mnnzpf+VDbacw+XudEY8cn3vTZv3q7Mq1cnxxxTbNk0mztfsk3u9qsi93CZS+uU43hpVefe52e8ftOrX/3qTJkyJY888shu9+np6dkvStbLaTQatXrgPqcOuZ99NtmwITnnnBcKSFfXrts33dT+9av6+FaZu6rMrdauXyRYsSK5//6y5SORu7Qqc3fScbMOx/HhdHLu36l4PfXUU/nBD36QP/3TP91X80BxS5YkX/pSsn59snbtrteCvOIVybJlVU/WXnXL3deXLF++q2iOG5c8+eSu7ePHJ2PHVjtbO8ldr9x0vhEVr49+9KOZNWtWJk+enJ/85Ce55ppr0mg08oEPfKBd80Hb3Xnnrveu+tSnkiOOSDZuTN797mTbtqona6+65V66dNf12WcP3b5sWfLBD5aephy5h24f7bnpfCMqXv/93/+dD3zgA/nf//3fHHbYYXnzm9+cNWvW5LDR+o6L1MbNN++61E2dcu/9q1n3b3JDZxlR8br99tvbNQcAwKjnbzUCABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUonjRsVqtcpedO5vZtGlzdu5sFl23jrl3k7ropdncmc2bN6XZ3Flw3WpzV5O5rrlh9xQvAIBCFC8AgEIULwCAQhQvAIBCuqseAHavq9hKjUbS21tsuRcZ7oW4oz33MJm7ymVOkkaS8rGrzV1J5qSeuXf/WyTgjBcAQCmKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKF7W2eHFy+unJuHHJhAnJBRckDz9c9VTtV9fcq5PMSjIxSVeSr1c6TTl1zF3HzOwfFC9qbdWqpK8vWbMmWbkyefbZ5Lzzkh07qp6sveqae0eSU5LcXPUghdUxdx0zs3/oHukdfvzjH+fKK6/M3Xffnaeffjqvf/3rs2zZskybNq0d80FbfetbQ2/fdtuuM0AbNiRvfWslIxVR19wz/+9SN3XMXcfM7B9GVLx+9rOfZcaMGXn729+eu+++O4cddli+//3v5+CDD27XfFDU9u27rg85pNo5SqtrboDSRlS8rr/++vz+7/9+li1b9vy2Y445Zp8PBVUYHEzmz09mzEh6e6ueppy65gaowoiK1ze+8Y28613vyoUXXphVq1blta99bebOnZsPf/jDu73PwMBABgYGhi7a3Z2enp69m7igZrM55LouOiV3o1F2vb6+ZPPm5IEHyq053Md4tOceNnOZpSsl9wtGe+6qj50vnqETZimp6tyNPTiAj6h4/fCHP8zSpUuzYMGCXH311Vm3bl0uv/zyjBkzJrNnzx72PosXL86iRYuGbJszZ07mzp07kqUr1d/fX/UIlag6d8mzL/PmJd/8ZrJ6dXLUUeXW3bJly0u2jfbcw2Yus3Sl5H7BaM89XOaqVH0cr0pVuXv34ADe1Wq1Wnv6H44ZMybTpk3Lt7/97ee3XX755Vm3bl2+853vDHuf/f2MV39/f6ZMmbJHLXa06JTcjcaIf/djxFqt5LLLkhUrkvvvT97whrYvOUSzufMl20Z77mEzd7c/82/qSrIiyQWF1mvulPs5pXN3QubSOuU4XlrVuff5Ga8jjzwyJ5xwwpBtU6dOzVe/+tXd3qenp2e/KFkvp9Fo1OqB+5w65O7rS5YvT+66a9d7Wj355K7t48cnY8e2f/2qPr5V5q7yMfVUkkdedPvRJBuTHJJkUpvXlrusOmYeTh2O48Pp5Nwjeh+vGTNm5OHfeJfF/v7+TJ48eZ8OBaUsXbrrN/rOPjs58sgXLnfcUfVk7VXX3OuTvOn/Lkmy4P/+/cnKJiqjjrnrmJn9w4jOeH3kIx/JWWedlc985jO56KKLsnbt2tx666259dZb2zUftNWeP9E+utQ199lJ6hj97NQv99mpX2b2DyM643X66adnxYoV+fKXv5ze3t58+tOfzg033JCLL764XfMBAIwaI36F43ve85685z3vaccsAACjmr/VCABQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiRQdrFbs0mzuzefOmNJs7i65bz9zDRW4VvTR37szmTZvS3Lmz3LoV564kc11zw8tQvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKGVHxOvroo9PV1fWSS19fX7vmAwAYNbpHsvO6devSbDafv7158+a8853vzIUXXrjPBwMAGG1GVLwOO+ywIbevu+66HHvssXnb29622/sMDAxkYGBg6KLd3enp6RnJ0pV4rmS+uGzWQR1z1zFzInedctcxcyK33GU1Go3fuk9Xq9Vq7c1//swzz2TixIlZsGBBrr766t3ut3DhwixatGjItjlz5mTu3Ll7sywAQEfq7e39rfvsdfG6884788d//Md5/PHHM3HixN3ut7+f8erv78+UKVP2qMWOFnXMXcfMidx1yl3HzInccpe1J2uO6KnGF/viF7+YmTNnvmzpSpKenp79omS9nEajUasH7nPqmLuOmRO566SOmRO566aTc+9V8Xrsscdy77335mtf+9q+ngcAYNTaq/fxWrZsWSZMmJDzzz9/X88DADBqjbh4DQ4OZtmyZZk9e3a6u/f6mUoAgNoZcfG699578/jjj+fSSy9txzwAAKPWiE9ZnXfeednLX4QEAKg1f6sRAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoJARFa9ms5lPfOITOeaYYzJ27Ngce+yx+fSnP51Wq9Wu+QAARo3ukex8/fXXZ+nSpfnSl76UE088MevXr88ll1yS8ePH5/LLL2/XjAAAo8KIite3v/3tvPe9783555+fJDn66KPz5S9/OWvXrm3LcAAAo8mIitdZZ52VW2+9Nf39/ZkyZUoeeuihPPDAA1myZMlu7zMwMJCBgYGhi3Z3p6enZ+8mLqjZbA65ros65q5j5kTuOuWuY+ZEbrnLajQav3WfrtYIXqA1ODiYq6++Op/97GfTaDTSbDZz7bXX5qqrrtrtfRYuXJhFixYN2TZnzpzMnTt3T5cFAOh4vb29v3WfERWv22+/PR/72Mfy13/91znxxBOzcePGzJ8/P0uWLMns2bOHvc/+fsbrubN7e9JiR4s65q5j5kTuOuWuY+ZEbrnL2pM1R/RU48c+9rF8/OMfz/vf//4kyUknnZTHHnssixcv3m3x6unp2S9K1stpNBq1euA+p46565g5kbtO6pg5kbtuOjn3iN5O4umnn84BBwy9S6PRyODg4D4dCgBgNBrRGa9Zs2bl2muvzaRJk3LiiSfmu9/9bpYsWZJLL720XfMBAIwaIypeN954Yz7xiU9k7ty52bZtWyZOnJg///M/zyc/+cl2zQcAMGqMqHiNGzcuN9xwQ2644YY2jQMAMHr5W40AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFdLVarVbVQwAA1IEzXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIX8f9qUFBK2cqqlAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAsU0lEQVR4nO3df5BddX038PdylyyphgiSAJEGEBsIWUEhwIT4AwXRDGZgOg+opW0Ex+mTbICY0SJ0lEQfCNhpBgeYCNYGZ9oIVI1YZ4AGWpJhNCUEwyQ2sqIMWIWkdjRK0IXcvc8fKT8WNpiNud9zs+f1mrmz3DPn8v28d2/Ovvfcs3e7Wq1WKwAAtN1+VQ8AAFAXihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhitcu3HTTTTnqqKNywAEH5LTTTsuDDz5Y9Uhtt2bNmsyePTuTJk1KV1dXvvWtb1U9UtstWbIkp5xySsaNG5eJEyfmvPPOy6OPPlr1WG23bNmynHDCCTnwwANz4IEHZsaMGbnrrruqHquoa6+9Nl1dXVmwYEHVo7TVokWL0tXVNeR23HHHVT1WET/72c/y53/+53njG9+YsWPH5q1vfWseeuihqsdqq6OOOupVX++urq709fVVPVrbNJvNfOYzn8nRRx+dsWPH5phjjsnnP//5dOpfRFS8hnH77bdn4cKFueqqq/Lwww/nxBNPzPvf//5s3bq16tHaavv27TnxxBNz0003VT1KMatXr05fX1/Wrl2bVatW5fnnn8/ZZ5+d7du3Vz1aWx1xxBG59tprs379+jz00EN573vfm3PPPTc/+MEPqh6tiHXr1uXmm2/OCSecUPUoRUybNi1PPfXUi7cHHnig6pHa7pe//GVmzpyZ/fffP3fddVf+8z//M3/3d3+Xgw46qOrR2mrdunVDvtarVq1Kkpx//vkVT9Y+1113XZYtW5Ybb7wxmzdvznXXXZcvfOELueGGG6oebXgtXuXUU09t9fX1vXi/2Wy2Jk2a1FqyZEmFU5WVpLVy5cqqxyhu69atrSSt1atXVz1KcQcddFDr7//+76seo+1+85vftP7kT/6ktWrVqta73/3u1mWXXVb1SG111VVXtU488cSqxyju8ssvb73jHe+oeozKXXbZZa1jjjmmNTg4WPUobXPOOee0Lr744iHb/vRP/7R14YUXVjTRa3PG6xWee+65rF+/PmedddaL2/bbb7+cddZZ+d73vlfhZJSwbdu2JMnBBx9c8STlNJvN3Hbbbdm+fXtmzJhR9Tht19fXl3POOWfIv/HR7kc/+lEmTZqUN7/5zbnwwgvz5JNPVj1S233729/O9OnTc/7552fixIl5+9vfni9/+ctVj1XUc889l3/8x3/MxRdfnK6urqrHaZvTTz899913X/r7+5MkjzzySB544IHMmjWr4smG1131AJ3mF7/4RZrNZg499NAh2w899ND88Ic/rGgqShgcHMyCBQsyc+bM9Pb2Vj1O223cuDEzZszI7373u7z+9a/PypUrc/zxx1c9Vlvddtttefjhh7Nu3bqqRynmtNNOy6233ppjjz02Tz31VBYvXpx3vvOd2bRpU8aNG1f1eG3zk5/8JMuWLcvChQtz5ZVXZt26dbn00kszZsyYzJkzp+rxivjWt76VX/3qV/noRz9a9Sht9elPfzq//vWvc9xxx6XRaKTZbObqq6/OhRdeWPVow1K84H/19fVl06ZNtbj+JUmOPfbYbNiwIdu2bcvXv/71zJkzJ6tXrx615eunP/1pLrvssqxatSoHHHBA1eMU8/Kf+k844YScdtppOfLII3PHHXfkYx/7WIWTtdfg4GCmT5+ea665Jkny9re/PZs2bcqXvvSl2hSvr3zlK5k1a1YmTZpU9Shtdccdd+Sf/umfsmLFikybNi0bNmzIggULMmnSpI78Witer3DIIYek0Whky5YtQ7Zv2bIlhx12WEVT0W7z58/Pd77znaxZsyZHHHFE1eMUMWbMmLzlLW9Jkpx88slZt25dvvjFL+bmm2+ueLL2WL9+fbZu3ZqTTjrpxW3NZjNr1qzJjTfemIGBgTQajQonLOMNb3hDpkyZkscee6zqUdrq8MMPf9UPEVOnTs03vvGNiiYq64knnsi9996bb37zm1WP0naf+tSn8ulPfzof/vCHkyRvfetb88QTT2TJkiUdWbxc4/UKY8aMycknn5z77rvvxW2Dg4O57777anH9S920Wq3Mnz8/K1euzL/927/l6KOPrnqkygwODmZgYKDqMdrmzDPPzMaNG7Nhw4YXb9OnT8+FF16YDRs21KJ0JckzzzyTH//4xzn88MOrHqWtZs6c+aq3hunv78+RRx5Z0URlLV++PBMnTsw555xT9Sht9+yzz2a//YbWmUajkcHBwYomem3OeA1j4cKFmTNnTqZPn55TTz01119/fbZv356LLrqo6tHa6plnnhnyU/Djjz+eDRs25OCDD87kyZMrnKx9+vr6smLFitx5550ZN25cnn766STJ+PHjM3bs2Iqna58rrrgis2bNyuTJk/Ob3/wmK1asyP3335977rmn6tHaZty4ca+6du91r3td3vjGN47qa/o++clPZvbs2TnyyCPz85//PFdddVUajUY+8pGPVD1aW33iE5/I6aefnmuuuSYXXHBBHnzwwdxyyy255ZZbqh6t7QYHB7N8+fLMmTMn3d2j/9v87Nmzc/XVV2fy5MmZNm1avv/972fp0qW5+OKLqx5teFX/WmWnuuGGG1qTJ09ujRkzpnXqqae21q5dW/VIbffv//7vrSSvus2ZM6fq0dpmuLxJWsuXL696tLa6+OKLW0ceeWRrzJgxrQkTJrTOPPPM1r/+679WPVZxdXg7iQ996EOtww8/vDVmzJjWm970ptaHPvSh1mOPPVb1WEX8y7/8S6u3t7fV09PTOu6441q33HJL1SMVcc8997SStB599NGqRyni17/+deuyyy5rTZ48uXXAAQe03vzmN7f+5m/+pjUwMFD1aMPqarU69K1dAQBGGdd4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4vYaBgYEsWrRoVL+b93DqmLuOmRO565S7jpkTueXuPN7H6zX8+te/zvjx47Nt27YceOCBVY9TTB1z1zFzInedctcxcyK33J3HGS8AgEIULwCAQhQvAIBCFK/X0N3dnblz59bir7u/XB1z1zFzInedctcxcyK33J3HxfWvodlsZvPmzZk6dWoajUbV4xRTx9x1zJzIXafcdcycyC1353HGCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgkD0qXjfddFOOOuqoHHDAATnttNPy4IMP7u25AABGnREX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HabVKnfbsaOZjRs3ZceOZrE1q87cSbmTVrFbs7kjmzZtTLO5o+i69cwNu6Z4AQAUongBABSieAEAFKJ4AQAU0l31ALBrXcVWajSS3t5iy73McBfijvbc1WZO6pnbc7wkF9iza854AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUonhRa0uWJKeckowbl0ycmJx3XvLoo1VP1X51zF3HzIncdctN5/uDite1116brq6uLFiwYC+NA2WtXp309SVr1yarViXPP5+cfXayfXvVk7VXHXPXMXMid91y0/m69/SB69aty80335wTTjhhb84DRd1999D7t96686fj9euTd72rkpGKqGPuOmZO5H5BXXLT+fbojNczzzyTCy+8MF/+8pdz0EEH7e2ZoDLbtu38ePDB1c5RWh1z1zFzInfdctN59uiMV19fX84555ycddZZ+X//7/+95r4DAwMZGBgYumh3d3p6evZk6aKazeaQj3XRKbkbjbLrDQ4mCxYkM2cmvb1l1hzuczzac9cxcyL3y4323FUfO18+QyfMUlLVuRu78eQecfG67bbb8vDDD2fdunW7tf+SJUuyePHiIdvmzp2befPmjXTpyvT391c9QiWqzl3qG8ML+vqSTZuSBx4ot+bmzZtftW20565j5kTulxvtuYfLXJWqj+NVqSp37248ubtarVZrd/+HP/3pTzN9+vSsWrXqxWu7zjjjjLztbW/L9ddfP+xj9vUzXv39/ZkyZcputdjRolNyNxp7fAniiM2fn9x5Z7JmTXL00cWWTbO541XbRnvuOmZO5H650Z57uMyldcpxvLSqc+/1M17r16/P1q1bc9JJJ724rdlsZs2aNbnxxhszMDDwqkV7enr2iZL1WhqNRq2euC+oQ+5WK7nkkmTlyuT++8t+Q0p27x9pO1SZu46ZE7lLq+NzfDh1OI4Pp5Nzj6h4nXnmmdm4ceOQbRdddFGOO+64XH755R0bEnalry9ZsWLnT8TjxiVPP71z+/jxydix1c7WTnXMXcfMidx1y03nG9FLjcP5fS817suazWY2b96cqVOn1qpUdk7urvavsIslli9PPvrRti+fZLh/fqM9dx0zJ3K/3GjP/Qd9W90rOuc4Xta+kLvcC+3Qgf6wHzv2XXXMXcfMidzQaf7g4nX//ffvhTEAAEY/f6sRAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMWLDtYqdms2d2TTpo1pNncUXbeeuavNXNfcnuNVf61hJ8ULAKAQxQsAoBDFCwCgEMULAKAQxQsAoJDuqgeAXenqKrlaI0lvyQWTJK1hfgFqtOceLnNSNHQajaS3+Jd7uODlcleTOalnbr/ZyK454wUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBUnmzUsefzz57W+TtWuTU06peqIy6pR7yZKd+caNSyZOTM47L3n00aqnaj+565Wbzjei4rVo0aJ0dXUNuR133HHtmg2KuOCCZOnSZPHi5KSTkkceSe65J5kwoerJ2qtuuVevTvr6dhbMVauS559Pzj472b696snaS+565abzdbVardbu7rxo0aJ8/etfz7333vvitu7u7hxyyCFtGa5qzWYzmzdvztSpU9NoNKoep5hOyd3VVWadtWuTdeuSSy55ad2f/jS54Ybkuuvau/Zw//pGe+7hjziFQr/Mf//3zjMhq1cn73pXiRWHCy53KWVz7/a31bbplON4aftC7hG/1Njd3Z3DDjvsxdtoLV3Uw/77JyefnLzsZ4m0Wjvvz5hR3VztVtfcL7dt286PBx9c7RylyV3tHNA90gf86Ec/yqRJk3LAAQdkxowZWbJkSSZPnrzL/QcGBjIwMDB00e7u9PT0jHzawprN5pCPddE5udv/08ohhyTd3cmWLUO3b9mSlHgVffjP8ejOPVzm0j+YDg4mCxYkM2cmvb1l1pT7JaM9d/XHzk46jpdVde7dOcs2ouJ12mmn5dZbb82xxx6bp556KosXL8473/nObNq0KePGjRv2MUuWLMnixYuHbJs7d27mzZs3kqUr1d/fX/UIlag+d6HvDBXavHnzMFtHd+7hMpcqAS/o60s2bUoeeKDcmnK/ZLTnHv7fdTWqP45Xo6rcvbvx5B7RNV6v9Ktf/SpHHnlkli5dmo997GPD7rOvn/Hq7+/PlClTOva14nbolNzd3e1fe//9k2efTf7P/0nuvPOl7bfemrzhDTt/E6qddux49U9loz33cJkbjRGffN9j8+fvzLxmTXL00cWWTbO541Xb5G6/KnIPl7m0TjmOl1Z17r1+xuuV3vCGN2TKlCl57LHHdrlPT0/PPlGyXkuj0ajVE/cFdcj9/PPJ+vXJmWe+VEC6unbev/HG9q9f1ee3ytxVZW61dv4iwcqVyf33ly0fidylVZm7k46bdTiOD6eTc/9BxeuZZ57Jj3/84/zFX/zF3poHilu6NPnqV5OHHkoefHDntSCve12yfHnVk7VX3XL39SUrVuwsmuPGJU8/vXP7+PHJ2LHVztZOctcrN51vRMXrk5/8ZGbPnp0jjzwyP//5z3PVVVel0WjkIx/5SLvmg7a7446d7131uc8lhx2WbNiQfOADydatVU/WXnXLvWzZzo9nnDF0+/LlyUc/WnqacuQeun2056bzjah4/dd//Vc+8pGP5H/+538yYcKEvOMd78jatWszYbS+4yK1cdNNO291U6fce341675NbugsIypet912W7vmAAAY9fytRgCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULzpWq1XutmNHMxs3bsqOHc2i69Yx9y5SF701mzuyadPGNJs7Cq5bbe5qMtc1N+ya4gUAUIjiBQBQiOIFAFCI4gUAUEh31QPArnUVW6nRSHp7iy33MsNdiDvacw+Tuatc5iRpJCkfu9rclWRO6pl7179FAs54AQCUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUonhRa0uWJKeckowbl0ycmJx3XvLoo1VP1X51zb0myewkk5J0JflWpdOUU8fcdczMvkHxotZWr076+pK1a5NVq5Lnn0/O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zTHHHJPPf/7zabVa7ZoPAGDU6B7Jztddd12WLVuWr371q5k2bVoeeuihXHTRRRk/fnwuvfTSds0IADAqjKh4ffe73825556bc845J0ly1FFH5Wtf+1oefPDBtgwHADCajKh4nX766bnlllvS39+fKVOm5JFHHskDDzyQpUuX7vIxAwMDGRgYGLpod3d6enr2bOKCms3mkI91UcfcdcycyF2n3HXMnMgtd1mNRuP37tPVGsEFWoODg7nyyivzhS98IY1GI81mM1dffXWuuOKKXT5m0aJFWbx48ZBtc+fOzbx583Z3WQCAjtfb2/t79xlR8brtttvyqU99Kn/7t3+badOmZcOGDVmwYEGWLl2aOXPmDPuYff2M1wtn93anxY4Wdcxdx8yJ3HXKXcfMidxyl7U7a47opcZPfepT+fSnP50Pf/jDSZK3vvWteeKJJ7JkyZJdFq+enp59omS9lkajUasn7gvqmLuOmRO566SOmRO566aTc4/o7SSeffbZ7Lff0Ic0Go0MDg7u1aEAAEajEZ3xmj17dq6++upMnjw506ZNy/e///0sXbo0F198cbvmAwAYNUZUvG644YZ85jOfybx587J169ZMmjQpf/VXf5XPfvaz7ZoPAGDUGFHxGjduXK6//vpcf/31bRoHAGD08rcaAQAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAACulqtVqtqocAAKgDZ7wAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAK+f+HI5S4NFINcAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAArlUlEQVR4nO3dfZBddX0/8Pdy1yyphkg0ASINRGwgZAWVABPiAwpiM8hAO4MPpW0Ex+kv2QAxo0XoKEktBOw0gwNMBGuDM20EqgasM0ADLckwmJIE4yQ2sqIUrAKpHY0S6kLu3t8fkYeVTczG3O+52fN6zdzZ3DPn7vfz3r05+96zZ+92tVqtVgAAaLuDqh4AAKAuFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFK/duPHGG3P00Ufn4IMPzqmnnpqHHnqo6pHabu3atTnnnHMyefLkdHV15Y477qh6pLZbunRpTj755IwbNy6TJk3Keeedl0ceeaTqsdpu+fLlOeGEE3LIIYfkkEMOyaxZs3LXXXdVPVZR11xzTbq6urJw4cKqR2mrxYsXp6ura8jtuOOOq3qsIn784x/nT//0T/O6170uY8eOzZvf/OZs2LCh6rHa6uijj37F57urqyt9fX1Vj9Y2zWYzn/70pzN16tSMHTs2xxxzTD772c+mU/8iouI1jNtuuy2LFi3KlVdemYcffjgnnnhi3ve+92Xbtm1Vj9ZWO3bsyIknnpgbb7yx6lGKWbNmTfr6+rJu3bqsXr06zz//fM4666zs2LGj6tHa6sgjj8w111yTjRs3ZsOGDXnPe96Tc889N9/97nerHq2I9evX56abbsoJJ5xQ9ShFzJgxI08++eSLtwceeKDqkdruZz/7WWbPnp1XvepVueuuu/Kf//mf+bu/+7sceuihVY/WVuvXrx/yuV69enWS5Pzzz694sva59tprs3z58txwww3ZunVrrr322nzuc5/L9ddfX/Vow2vxCqecckqrr6/vxfvNZrM1efLk1tKlSyucqqwkrVWrVlU9RnHbtm1rJWmtWbOm6lGKO/TQQ1t///d/X/UYbffLX/6y9Qd/8Aet1atXt971rne1Lr300qpHaqsrr7yydeKJJ1Y9RnGXXXZZ6+1vf3vVY1Tu0ksvbR1zzDGtwcHBqkdpm7PPPrt10UUXDdn2x3/8x60LLrigoon2zBmv3/Dcc89l48aNOfPMM1/cdtBBB+XMM8/Mt771rQono4Tt27cnSSZMmFDxJOU0m83ceuut2bFjR2bNmlX1OG3X19eXs88+e8j/8dHu+9//fiZPnpw3vvGNueCCC/LEE09UPVLbfeMb38jMmTNz/vnnZ9KkSXnrW9+aL37xi1WPVdRzzz2Xf/zHf8xFF12Urq6uqsdpm9NOOy333Xdf+vv7kyTf+c538sADD2TOnDkVTza87qoH6DQ//elP02w2c9hhhw3Zfthhh+V73/teRVNRwuDgYBYuXJjZs2ent7e36nHabvPmzZk1a1Z+9atf5TWveU1WrVqV448/vuqx2urWW2/Nww8/nPXr11c9SjGnnnpqbrnllhx77LF58skns2TJkrzjHe/Ili1bMm7cuKrHa5sf/vCHWb58eRYtWpQrrrgi69evzyWXXJIxY8Zk7ty5VY9XxB133JGf//zn+chHPlL1KG31qU99Kr/4xS9y3HHHpdFopNls5qqrrsoFF1xQ9WjDUrzg1/r6+rJly5ZaXP+SJMcee2w2bdqU7du356tf/Wrmzp2bNWvWjNry9aMf/SiXXnppVq9enYMPPrjqcYp5+Xf9J5xwQk499dQcddRRuf322/PRj360wsnaa3BwMDNnzszVV1+dJHnrW9+aLVu25Atf+EJtiteXvvSlzJkzJ5MnT656lLa6/fbb80//9E9ZuXJlZsyYkU2bNmXhwoWZPHlyR36uFa/f8PrXvz6NRiNPP/30kO1PP/10Dj/88Iqmot0WLFiQb37zm1m7dm2OPPLIqscpYsyYMXnTm96UJDnppJOyfv36fP7zn89NN91U8WTtsXHjxmzbti1ve9vbXtzWbDazdu3a3HDDDRkYGEij0ahwwjJe+9rXZtq0aXn00UerHqWtjjjiiFd8EzF9+vR87Wtfq2iish5//PHce++9+frXv171KG33yU9+Mp/61KfyoQ99KEny5je/OY8//niWLl3akcXLNV6/YcyYMTnppJNy3333vbhtcHAw9913Xy2uf6mbVquVBQsWZNWqVfm3f/u3TJ06teqRKjM4OJiBgYGqx2ibM844I5s3b86mTZtevM2cOTMXXHBBNm3aVIvSlSTPPPNMfvCDH+SII46oepS2mj179iteGqa/vz9HHXVURROVtWLFikyaNCln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iOIFHabVKnfbubOZzZu3ZOfOZrE1q87cSbmTVrFbs7kzW7ZsTrO5s+i69cwNu6d4AQAUongBABSieAEAFKJ4AQAU0l31ALB7XcVWajSS3t5iy73McBfijvbc1WZO6pnbc7wkF9ize854AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUonhRa0uXJiefnIwbl0yalJx3XvLII1VP1X51zF3HzIncdctN5/uditc111yTrq6uLFy4cD+NA2WtWZP09SXr1iWrVyfPP5+cdVayY0fVk7VXHXPXMXMid91y0/m69/WB69evz0033ZQTTjhhf84DRd1999D7t9yy67vjjRuTd76zkpGKqGPuOmZO5H5BXXLT+fbpjNczzzyTCy64IF/84hdz6KGH7u+ZoDLbt+96O2FCtXOUVsfcdcycyF233HSefTrj1dfXl7PPPjtnnnlm/uZv/maP+w4MDGRgYGDoot3d6enp2Zeli2o2m0Pe1kWn5G40yq43OJgsXJjMnp309pZZc7iP8WjPXcfMidwvN9pzV33sfPkMnTBLSVXnbuzFk3vExevWW2/Nww8/nPXr1+/V/kuXLs2SJUuGbJs3b17mz58/0qUr09/fX/UIlag6d6kvDC/o60u2bEkeeKDcmlu3bn3FttGeu46ZE7lfbrTnHi5zVao+jlelqty9e/Hk7mq1Wq29fYc/+tGPMnPmzKxevfrFa7tOP/30vOUtb8l111037GMO9DNe/f39mTZt2l612NGiU3I3Gvt8CeKILViQ3HlnsnZtMnVqsWXTbO58xbbRnruOmRO5X2605x4uc2mdchwvrerc+/2M18aNG7Nt27a87W1ve3Fbs9nM2rVrc8MNN2RgYOAVi/b09BwQJWtPGo1GrZ64L6hD7lYrufjiZNWq5P77y35BSvbuP2k7VJm7jpkTuUur43N8OHU4jg+nk3OPqHidccYZ2bx585BtF154YY477rhcdtllHRsSdqevL1m5ctd3xOPGJU89tWv7+PHJ2LHVztZOdcxdx8yJ3HXLTecb0Y8ah/PbftR4IGs2m9m6dWumT59eq1LZObm72r/CbpZYsSL5yEfavnyS4f77jfbcdcycyP1yoz337/Rldb/onON4WQdC7nI/aIcO9Lt923HgqmPuOmZO5IZO8zsXr/vvv38/jAEAMPr5W40AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF50sFaxW7O5M1u2bE6zubPouvXMXW3muub2HK/6cw27KF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhXRXPQDsVldXsaUaSXqLrfYyrWF+A2q05x4uc8plTpJGI+ktH3yYbQU/15VkTuqZ2282snvOeAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4UWtrk5yTZHKSriR3VDpNOXXMvXRpcvLJybhxyaRJyXnnJY88UvVU7Sd3vXLT+UZUvBYvXpyurq4ht+OOO65ds0Hb7UhyYpIbqx6ksDrmXrMm6etL1q1LVq9Onn8+OeusZMeOqidrL7nrlZvO1z3SB8yYMSP33nvvS++ge8TvAjrGnF/f6qaOue++e+j9W27ZdSZk48bkne+sZKQi5N6lLrnpfCNuTd3d3Tn88MPbMQtAMdu373o7YUK1c5Qmd7VzwIiL1/e///1Mnjw5Bx98cGbNmpWlS5dmypQpu91/YGAgAwMDQxft7k5PT8/Ipy2s2WwOeVsXnZK7UenqZQz3MR7tuYfNXDj04GCycGEye3bS21tmTblfMtpzV33sfPkMnTBLSVXnbuzFk3tExevUU0/NLbfckmOPPTZPPvlklixZkne84x3ZsmVLxo0bN+xjli5dmiVLlgzZNm/evMyfP38kS1eqv7+/6hEqUXXuQl8XKrV169ZXbBvtuYfNXDh0X1+yZUvywAPl1pT7JaM993CZq1L1cbwqVeXu3Ysnd1er1Wrt6wI///nPc9RRR2XZsmX56Ec/Ouw+B/oZr/7+/kybNm2vWuxo0Sm5G4WvH+xKsirJeQXXbO7c+Yptoz33sJkb5TIvWJDceWeydm0ydWqxZdNsyv2C0Z57uMyldcpxvLSqc+/3M16/6bWvfW2mTZuWRx99dLf79PT0HBAla08ajUatnrgvqGvukur48a0qc6uVXHxxsmpVcv/9ZctHIndpVebupP/XdT2Od3Lu36l4PfPMM/nBD36QP/uzP9tf80BRzyR5+bcNjyXZlGRCkt1fuXjgq2Puvr5k5cpdZz/GjUueemrX9vHjk7Fjq52tneSuV24634hex+sTn/hE1qxZk//6r//Kgw8+mD/6oz9Ko9HIhz/84XbNB221Iclbf31LkkW//vdnKpuojDrmXr5812+2nX56csQRL91uu63qydpL7nrlpvON6IzXf//3f+fDH/5w/vd//zcTJ07M29/+9qxbty4TJ05s13zQVqcn2eeLHA9gp6d+uff9atYDm9zQWUZUvG699dZ2zQEAMOr5W40AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF50rlar2K25c2e2bN6c5s6dRdetZe7hQxe9NZs7s2XL5jSbOwuuW23uajLXNTfsnuIFAFCI4gUAUIjiBQBQiOIFAFBId9UDwO51FVup0Uh6e4st9zLDXYg72nMPk7mrXOYkaSQpH7va3JVkTuqZe7e/RALOeAEAFKN4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4UWtLlyYnn5yMG5dMmpScd17yyCNVT9V+dc29Nsk5SSYn6UpyR6XTlFPH3HXMzIFB8aLW1qxJ+vqSdeuS1auT559Pzjor2bGj6snaq665dyQ5McmNVQ9SWB1z1zEzB4bukT7gxz/+cS677LLcddddefbZZ/OmN70pK1asyMyZM9sxH7TV3XcPvX/LLbvOAG3cmLzznZWMVERdc8/59a1u6pi7jpk5MIyoeP3sZz/L7Nmz8+53vzt33XVXJk6cmO9///s59NBD2zUfFLV9+663EyZUO0dpdc0NUNqIite1116b3//938+KFSte3DZ16tT9PhRUYXAwWbgwmT076e2teppy6poboAojKl7f+MY38r73vS/nn39+1qxZkze84Q2ZP39+Pvaxj+32MQMDAxkYGBi6aHd3enp69m3igprN5pC3ddEpuRuNsuv19SVbtiQPPFBuzeE+xqM997CZyyxdKblfMtpzV33sfPkMnTBLSVXnbuzFAXxExeuHP/xhli9fnkWLFuWKK67I+vXrc8kll2TMmDGZO3fusI9ZunRplixZMmTbvHnzMn/+/JEsXan+/v6qR6hE1blLnn1ZsCD55jeTtWuTI48st+7WrVtfsW205x42c5mlKyX3S0Z77uEyV6Xq43hVqsrduxcH8K5Wq9Xa23c4ZsyYzJw5Mw8++OCL2y655JKsX78+3/rWt4Z9zIF+xqu/vz/Tpk3bqxY7WnRK7kZjxL/7MWKtVnLxxcmqVcn99yd/8AdtX3KIZnPnK7aN9tzDZu5uf+bf1JVkVZLzCq3X3Cn3C0rn7oTMpXXKcby0qnPv9zNeRxxxRI4//vgh26ZPn56vfe1ru31MT0/PAVGy9qTRaNTqifuCOuTu60tWrkzuvHPXa1o99dSu7ePHJ2PHtn/9qj6+Veau8jn1TJJHX3b/sSSbkkxIMqXNa8tdVh0zD6cOx/HhdHLuEb2O1+zZs/PIb7zKYn9/f4466qj9OhSUsnz5rt/oO/305IgjXrrddlvVk7VXXXNvSPLWX9+SZNGv//2ZyiYqo46565iZA8OIznh9/OMfz2mnnZarr746H/jAB/LQQw/l5ptvzs0339yu+aCt9v4H7aNLXXOfnqSO0U9P/XKfnvpl5sAwojNeJ598clatWpWvfOUr6e3tzWc/+9lcd911ueCCC9o1HwDAqDHiKxzf//735/3vf387ZgEAGNX8rUYAgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC86WKvYrdncmS1bNqfZ3Fl03XrmHi5yq+ituXNntmzenObOneXWrTh3JZnrmhv2QPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKGRExevoo49OV1fXK259fX3tmg8AYNToHsnO69evT7PZfPH+li1b8t73vjfnn3/+fh8MAGC0GVHxmjhx4pD711xzTY455pi8613v2u1jBgYGMjAwMHTR7u709PSMZOlKvFAyX14266COueuYOZG7TrnrmDmRW+6yGo3Gb92nq9VqtfblnT/33HOZPHlyFi1alCuuuGK3+y1evDhLliwZsm3evHmZP3/+viwLANCRent7f+s++1y8br/99vzJn/xJnnjiiUyePHm3+x3oZ7z6+/szbdq0vWqxo0Udc9cxcyJ3nXLXMXMit9xl7c2aI/pR48t96Utfypw5c/ZYupKkp6fngChZe9JoNGr1xH1BHXPXMXMid53UMXMid910cu59Kl6PP/547r333nz961/f3/MAAIxa+/Q6XitWrMikSZNy9tln7+95AABGrREXr8HBwaxYsSJz585Nd/c+/6QSAKB2Rly87r333jzxxBO56KKL2jEPAMCoNeJTVmeddVb28RchAQBqzd9qBAAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChkRMWr2Wzm05/+dKZOnZqxY8fmmGOOyWc/+9m0Wq12zQcAMGp0j2Tna6+9NsuXL8+Xv/zlzJgxIxs2bMiFF16Y8ePH55JLLmnXjAAAo8KIiteDDz6Yc889N2effXaS5Oijj85XvvKVPPTQQ20ZDgBgNBlR8TrttNNy8803p7+/P9OmTct3vvOdPPDAA1m2bNluHzMwMJCBgYGhi3Z3p6enZ98mLqjZbA55Wxd1zF3HzIncdcpdx8yJ3HKX1Wg0fus+Xa0RXKA1ODiYK664Ip/73OfSaDTSbDZz1VVX5fLLL9/tYxYvXpwlS5YM2TZv3rzMnz9/b5cFAOh4vb29v3WfERWvW2+9NZ/85Cfzt3/7t5kxY0Y2bdqUhQsXZtmyZZk7d+6wjznQz3i9cHZvb1rsaFHH3HXMnMhdp9x1zJzILXdZe7PmiH7U+MlPfjKf+tSn8qEPfShJ8uY3vzmPP/54li5dutvi1dPTc0CUrD1pNBq1euK+oI6565g5kbtO6pg5kbtuOjn3iF5O4tlnn81BBw19SKPRyODg4H4dCgBgNBrRGa9zzjknV111VaZMmZIZM2bk29/+dpYtW5aLLrqoXfMBAIwaIype119/fT796U9n/vz52bZtWyZPnpy/+Iu/yGc+85l2zQcAMGqMqHiNGzcu1113Xa677ro2jQMAMHr5W40AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFdLVarVbVQwAA1IEzXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIX8f+nsF9NnmgS8AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAArg0lEQVR4nO3dfZBddX0/8Pdy1yypxkiUAJEGIhoesoBKgIb4gILYDDLSzuBDaRvBcfpLNkDMaBE6SlKLC3aawQEawdrgTBuBqgHrFGigJRkGU5JgnMSmrCgFH4DUjkYJdSF37++PyMPKJmZj7vfc7Hm9Zu7c3DP37vfz3r05+95zz97tarVarQAA0HYHVD0AAEBdKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF67cP311+fII4/MgQcemFNPPTUPPPBA1SO13Zo1a3LOOedkypQp6erqym233Vb1SG3X39+fk08+ORMmTMjkyZNz7rnn5qGHHqp6rLZbtmxZTjjhhLzyla/MK1/5ysyaNSt33HFH1WMVddVVV6WrqysLFy6sepS2Wrx4cbq6uoZdjjnmmKrHKuJHP/pR/viP/zivfvWrM378+Bx//PFZv3591WO11ZFHHvmSr3dXV1f6+vqqHq1tms1mPvnJT2batGkZP358jjrqqHz6059Op/5FRMVrBLfccksWLVqUK664Ig8++GBOPPHEvPvd787WrVurHq2ttm/fnhNPPDHXX3991aMUs3r16vT19WXt2rVZtWpVnn322Zx11lnZvn171aO11eGHH56rrroqGzZsyPr16/POd74z733ve/Od73yn6tGKWLduXW644YaccMIJVY9SxIwZM/L4448/f7nvvvuqHqntfvrTn2b27Nl52cteljvuuCP/+Z//mb/5m7/JQQcdVPVobbVu3bphX+tVq1YlSc4777yKJ2ufq6++OsuWLct1112XLVu25Oqrr85nP/vZXHvttVWPNrIWL3HKKae0+vr6nr/dbDZbU6ZMafX391c4VVlJWitXrqx6jOK2bt3aStJavXp11aMUd9BBB7X+7u/+ruox2u4Xv/hF6w1veENr1apVrbe//e2tSy65pOqR2uqKK65onXjiiVWPUdyll17aestb3lL1GJW75JJLWkcddVRraGio6lHa5uyzz25deOGFw7b94R/+Yev888+vaKLdc8Tr1zzzzDPZsGFDzjzzzOe3HXDAATnzzDPzzW9+s8LJKGHbtm1JkkmTJlU8STnNZjM333xztm/fnlmzZlU9Ttv19fXl7LPPHvZ/fKz77ne/mylTpuR1r3tdzj///Dz22GNVj9R2X//61zNz5sycd955mTx5ct70pjflC1/4QtVjFfXMM8/kH/7hH3LhhRemq6ur6nHa5rTTTss999yTgYGBJMm3v/3t3HfffZkzZ07Fk42su+oBOs1PfvKTNJvNHHLIIcO2H3LIIfmv//qviqaihKGhoSxcuDCzZ89Ob29v1eO03aZNmzJr1qz88pe/zCte8YqsXLkyxx13XNVjtdXNN9+cBx98MOvWrat6lGJOPfXU3HTTTTn66KPz+OOPZ8mSJXnrW9+azZs3Z8KECVWP1zbf//73s2zZsixatCiXX3551q1bl4svvjjjxo3L3Llzqx6viNtuuy0/+9nP8qEPfajqUdrqE5/4RH7+85/nmGOOSaPRSLPZzJVXXpnzzz+/6tFGpHjBr/T19WXz5s21OP8lSY4++uhs3Lgx27Zty1e+8pXMnTs3q1evHrPl6wc/+EEuueSSrFq1KgceeGDV4xTz4p/6TzjhhJx66qk54ogjcuutt+bDH/5whZO119DQUGbOnJnPfOYzSZI3velN2bx5cz7/+c/Xpnh98YtfzJw5czJlypSqR2mrW2+9Nf/4j/+YFStWZMaMGdm4cWMWLlyYKVOmdOTXWvH6Na95zWvSaDTy5JNPDtv+5JNP5tBDD61oKtptwYIF+cY3vpE1a9bk8MMPr3qcIsaNG5fXv/71SZKTTjop69aty+c+97nccMMNFU/WHhs2bMjWrVvz5je/+fltzWYza9asyXXXXZfBwcE0Go0KJyzjVa96VaZPn56HH3646lHa6rDDDnvJDxHHHntsvvrVr1Y0UVmPPvpo7r777nzta1+repS2+/jHP55PfOIT+cAHPpAkOf744/Poo4+mv7+/I4uXc7x+zbhx43LSSSflnnvueX7b0NBQ7rnnnlqc/1I3rVYrCxYsyMqVK/Nv//ZvmTZtWtUjVWZoaCiDg4NVj9E2Z5xxRjZt2pSNGzc+f5k5c2bOP//8bNy4sRalK0meeuqpfO9738thhx1W9ShtNXv27Je8NczAwECOOOKIiiYqa/ny5Zk8eXLOPvvsqkdpu6effjoHHDC8zjQajQwNDVU00e454jWCRYsWZe7cuZk5c2ZOOeWUXHPNNdm+fXsuuOCCqkdrq6eeemrYT8GPPPJINm7cmEmTJmXq1KkVTtY+fX19WbFiRW6//fZMmDAhTzzxRJJk4sSJGT9+fMXTtc9ll12WOXPmZOrUqfnFL36RFStW5N57781dd91V9WhtM2HChJecu/fyl788r371q8f0OX0f+9jHcs455+SII47Ij3/841xxxRVpNBr54Ac/WPVobfXRj340p512Wj7zmc/kfe97Xx544IHceOONufHGG6sere2GhoayfPnyzJ07N93dY//b/DnnnJMrr7wyU6dOzYwZM/Ktb30rS5cuzYUXXlj1aCOr+tcqO9W1117bmjp1amvcuHGtU045pbV27dqqR2q7f//3f28lecll7ty5VY/WNiPlTdJavnx51aO11YUXXtg64ogjWuPGjWsdfPDBrTPOOKP1r//6r1WPVVwd3k7i/e9/f+uwww5rjRs3rvXa17629f73v7/18MMPVz1WEf/8z//c6u3tbfX09LSOOeaY1o033lj1SEXcddddrSSthx56qOpRivj5z3/euuSSS1pTp05tHXjgga3Xve51rb/4i79oDQ4OVj3aiLparQ59a1cAgDHGOV4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF67MTg4mMWLF4/pd/MeSR1z1zFzInedctcxcyK33J3H+3jtxs9//vNMnDgx27Ztyytf+cqqxymmjrnrmDmRu06565g5kVvuzuOIFwBAIYoXAEAhihcAQCGK1250d3dn3rx5tfjr7i9Wx9x1zJzIXafcdcycyC1353Fy/W40m81s2bIlxx57bBqNRtXjFFPH3HXMnMhdp9x1zJzILXfnccQLAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKCQvSpe119/fY488sgceOCBOfXUU/PAAw/s67kAAMacURevW265JYsWLcoVV1yRBx98MCeeeGLe/e53Z+vWre2YDwBgzBh18Vq6dGk+8pGP5IILLshxxx2Xz3/+8/md3/md/P3f/3075gMAGDO6R3PnZ555Jhs2bMhll132/LYDDjggZ555Zr75zW+O+JjBwcEMDg4OX7S7Oz09PXsxblnNZnPYdV3UMXcdMydy1yl3HTMncstdVqPR+I336Wq1Wq09/YA//vGP89rXvjb3339/Zs2a9fz2P//zP8/q1avzH//xHy95zOLFi7NkyZJh2+bNm5f58+fv6bIAAB2vt7f3N95nVEe89sZll12WRYsWDV90PzriNTAwkOnTp+9Rix0r6pi7jpkTueuUu46ZE7nl7jyjKl6vec1r0mg08uSTTw7b/uSTT+bQQw8d8TE9PT37RcnanUaj0bFfwHaqY+46Zk7krpM6Zk7krptOzj2qk+vHjRuXk046Kffcc8/z24aGhnLPPfcMe+kRAICXGvVLjYsWLcrcuXMzc+bMnHLKKbnmmmuyffv2XHDBBe2YDwBgzBh18Xr/+9+f//mf/8mnPvWpPPHEE3njG9+YO++8M4ccckg75gMAGDP26uT6BQsWZMGCBft6FgCAMc3fagQAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChk1MVrzZo1OeecczJlypR0dXXltttua8NYAABjz6iL1/bt23PiiSfm+uuvb8c8AABjVvdoHzBnzpzMmTOnHbMAAIxpoy5eozU4OJjBwcHhi3Z3p6enp91L/9aazeaw67qoY+46Zk7krlPuOmZO5Ja7rEaj8Rvv09VqtVp7u0BXV1dWrlyZc889d5f3Wbx4cZYsWTJs27x58zJ//vy9XRYAoOP09vb+xvu0vXjt70e8BgYGMn369D1q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KtOnT8/DDz+8y/v09PTsFyVrdxqNRq2euM+pa+6S6vj5rSpzq5VcdFGycmVy771ly0cid2lV5u6k/9d13Y93cu7fqng99dRT+d73vpc/+ZM/2VfzQFFPJXnxjw2PJNmYZFKSXZ+5uP+rY+6+vmTFip1HPyZMSJ54Yuf2iROT8eOrna2d5K5XbjrfqN7H62Mf+1hWr16d//7v/87999+fP/iDP0ij0cgHP/jBds0HbbU+yZt+dUmSRb/696cqm6iMOuZetmznb7adfnpy2GEvXG65perJ2kvueuWm843qiNcPf/jDfPCDH8z//u//5uCDD85b3vKWrF27NgcffHC75oO2Oj3JXp/kuB87PfXLvfdns+7f5IbOMqridfPNN7drDgCAMc/fagQAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8aJztVrFLs0dO7J506Y0d+woum4tc48cuuil2dyRzZs3pdncUXDdanNXk7muuWHXFC8AgEIULwCAQhQvAIBCFC8AgEK6qx4Adq2r2EqNRtLbW2y5FxnpRNyxnnuEzF3lMidJI0n52NXmriRzUs/cu/wlEnDECwCgGMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMWLWuvvT04+OZkwIZk8OTn33OShh6qeqv3qmntNknOSTEnSleS2Sqcpp46565iZ/YPiRa2tXp309SVr1yarViXPPpucdVayfXvVk7VXXXNvT3JikuurHqSwOuauY2b2D92jfcCPfvSjXHrppbnjjjvy9NNP5/Wvf32WL1+emTNntmM+aKs77xx++6abdh4B2rAhedvbKhmpiLrmnvOrS93UMXcdM7N/GFXx+ulPf5rZs2fnHe94R+64444cfPDB+e53v5uDDjqoXfNBUdu27byeNKnaOUqra26A0kZVvK6++ur87u/+bpYvX/78tmnTpu3zoaAKQ0PJwoXJ7NlJb2/V05RT19wAVRhV8fr617+ed7/73TnvvPOyevXqvPa1r838+fPzkY98ZJePGRwczODg4PBFu7vT09OzdxMX1Gw2h13XRafkbjTKrtfXl2zenNx3X7k1R/ocj/XcI2Yus3Sl5H7BWM9d9b7zxTN0wiwlVZ27sQc78FEVr+9///tZtmxZFi1alMsvvzzr1q3LxRdfnHHjxmXu3LkjPqa/vz9LliwZtm3evHmZP3/+aJau1MDAQNUjVKLq3CWPvixYkHzjG8maNcnhh5dbd8uWLS/ZNtZzj5i5zNKVkvsFYz33SJmrUvV+vCpV5e7dgx14V6vVau3pBxw3blxmzpyZ+++///ltF198cdatW5dvfvObIz5mfz/iNTAwkOnTp+9Rix0rOiV3ozHq3/0YtVYrueiiZOXK5N57kze8oe1LDtNs7njJtrGee8TM3e3P/Ou6kqxMcm6h9Zo75H5O6dydkLm0TtmPl1Z17n1+xOuwww7LcccdN2zbsccem69+9au7fExPT89+UbJ2p9Fo1OqJ+5w65O7rS1asSG6/fed7Wj3xxM7tEycm48e3f/2qPr9V5q7yOfVUkodfdPuRJBuTTEoytc1ry11WHTOPpA778ZF0cu5RvY/X7Nmz89CvvcviwMBAjjjiiH06FJSybNnO3+g7/fTksMNeuNxyS9WTtVddc69P8qZfXZJk0a/+/anKJiqjjrnrmJn9w6iOeH30ox/Naaedls985jN53/velwceeCA33nhjbrzxxnbNB2215y+0jy11zX16kjpGPz31y3166peZ/cOojnidfPLJWblyZb785S+nt7c3n/70p3PNNdfk/PPPb9d8AABjxqjPcHzPe96T97znPe2YBQBgTPO3GgEAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvOhgrWKXZnNHNm/elGZzR9F165l7pMitopfmjh3ZvGlTmjt2lFu34tyVZK5rbtgNxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgkFEVryOPPDJdXV0vufT19bVrPgCAMaN7NHdet25dms3m87c3b96cd73rXTnvvPP2+WAAAGPNqIrXwQcfPOz2VVddlaOOOipvf/vbd/mYwcHBDA4ODl+0uzs9PT2jWboSz5XMF5fNOqhj7jpmTuSuU+46Zk7klrusRqPxG+/T1Wq1WnvzwZ955plMmTIlixYtyuWXX77L+y1evDhLliwZtm3evHmZP3/+3iwLANCRent7f+N99rp43XrrrfmjP/qjPPbYY5kyZcou77e/H/EaGBjI9OnT96jFjhV1zF3HzIncdcpdx8yJ3HKXtSdrjuqlxhf74he/mDlz5uy2dCVJT0/PflGydqfRaNTqifucOuauY+ZE7jqpY+ZE7rrp5Nx7VbweffTR3H333fna1762r+cBABiz9up9vJYvX57Jkyfn7LPP3tfzAACMWaMuXkNDQ1m+fHnmzp2b7u69fqUSAKB2Rl287r777jz22GO58MIL2zEPAMCYNepDVmeddVb28hchAQBqzd9qBAAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChkVMWr2Wzmk5/8ZKZNm5bx48fnqKOOyqc//em0Wq12zQcAMGZ0j+bOV199dZYtW5YvfelLmTFjRtavX58LLrggEydOzMUXX9yuGQEAxoRRFa/7778/733ve3P22WcnSY488sh8+ctfzgMPPNCW4QAAxpJRFa/TTjstN954YwYGBjJ9+vR8+9vfzn333ZelS5fu8jGDg4MZHBwcvmh3d3p6evZu4oKazeaw67qoY+46Zk7krlPuOmZO5Ja7rEaj8Rvv09UaxQlaQ0NDufzyy/PZz342jUYjzWYzV155ZS677LJdPmbx4sVZsmTJsG3z5s3L/Pnz93RZAICO19vb+xvvM6ridfPNN+fjH/94/vqv/zozZszIxo0bs3DhwixdujRz584d8TH7+xGv547u7UmLHSvqmLuOmRO565S7jpkTueUua0/WHNVLjR//+MfziU98Ih/4wAeSJMcff3weffTR9Pf377J49fT07Bcla3cajUatnrjPqWPuOmZO5K6TOmZO5K6bTs49qreTePrpp3PAAcMf0mg0MjQ0tE+HAgAYi0Z1xOucc87JlVdemalTp2bGjBn51re+laVLl+bCCy9s13wAAGPGqIrXtddem09+8pOZP39+tm7dmilTpuTP/uzP8qlPfapd8wEAjBmjKl4TJkzINddck2uuuaZN4wAAjF3+ViMAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhXa1Wq1X1EAAAdeCIFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCH/Hz1slx5kEKy9AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAArnElEQVR4nO3dfZBddX0/8Pdy1yypxgiaQCINRDQQsoBKgIb4UxTEZpCBdgYfSqcRHKeTbICY0SJ0lKQWAnaawQEawdrgjI1A1YB1CjTQkgyDKSEYJ7GRFaX4BKR2NErUhb17f39EHlY2MRu533Oz5/WaObO5Z87d7+e9e3P2vWfv3u1qtVqtAADQdgdUPQAAQF0oXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXrtx/fXX54gjjsiBBx6Yk08+OQ888EDVI7Xd+vXrc9ZZZ2Xq1Knp6urKbbfdVvVIbbd8+fKceOKJmTBhQiZPnpxzzjknDz/8cNVjtd3KlStz3HHH5ZWvfGVe+cpXZs6cObnjjjuqHquoq666Kl1dXVm8eHHVo7TV0qVL09XVNWw7+uijqx6riB/96Ef58z//87z61a/O+PHjc+yxx+bBBx+seqy2OuKII170+e7q6kpfX1/Vo7VNs9nMxz/+8UyfPj3jx4/PkUcemU9+8pPp1L+IqHiN4JZbbsmSJUty+eWX56GHHsrxxx+fd73rXdm+fXvVo7XVzp07c/zxx+f666+vepRi1q1bl76+vmzYsCFr167NM888kzPOOCM7d+6serS2Ouyww3LVVVdl06ZNefDBB/OOd7wjZ599dr71rW9VPVoRGzduzA033JDjjjuu6lGKmDVrVh5//PHntvvuu6/qkdrupz/9aebOnZuXvexlueOOO/Lf//3f+fu///scdNBBVY/WVhs3bhz2uV67dm2S5Nxzz614sva5+uqrs3Llylx33XXZtm1brr766nzqU5/KtddeW/VoI2vxIieddFKrr6/vudvNZrM1derU1vLlyyucqqwkrTVr1lQ9RnHbt29vJWmtW7eu6lGKO+igg1r/+I//WPUYbfeLX/yi9YY3vKG1du3a1tve9rbWxRdfXPVIbXX55Ze3jj/++KrHKO6SSy5pveUtb6l6jMpdfPHFrSOPPLI1NDRU9Shtc+aZZ7YuuOCCYfv+9E//tHXeeedVNNGeueL1W55++uls2rQpp59++nP7DjjggJx++un5+te/XuFklLBjx44kycEHH1zxJOU0m83cfPPN2blzZ+bMmVP1OG3X19eXM888c9j/8bHuO9/5TqZOnZrXve51Oe+88/L973+/6pHa7qtf/Wpmz56dc889N5MnT86b3vSmfPazn616rKKefvrpfOELX8gFF1yQrq6uqsdpm1NOOSX33HNP+vv7kyTf/OY3c99992XevHkVTzay7qoH6DQ/+clP0mw2c8ghhwzbf8ghh+Tb3/52RVNRwtDQUBYvXpy5c+emt7e36nHabsuWLZkzZ05+/etf5xWveEXWrFmTY445puqx2urmm2/OQw89lI0bN1Y9SjEnn3xybrrpphx11FF5/PHHs2zZsvy///f/snXr1kyYMKHq8drme9/7XlauXJklS5bksssuy8aNG3PRRRdl3LhxmT9/ftXjFXHbbbflZz/7WT7wgQ9UPUpbfexjH8vPf/7zHH300Wk0Gmk2m7niiity3nnnVT3aiBQv+I2+vr5s3bq1Fs9/SZKjjjoqmzdvzo4dO/KlL30p8+fPz7p168Zs+frBD36Qiy++OGvXrs2BBx5Y9TjFvPC7/uOOOy4nn3xyDj/88Nx666354Ac/WOFk7TU0NJTZs2fnyiuvTJK86U1vytatW/OZz3ymNsXrc5/7XObNm5epU6dWPUpb3Xrrrfnnf/7nrF69OrNmzcrmzZuzePHiTJ06tSM/14rXb3nNa16TRqORJ598ctj+J598MoceemhFU9FuixYtyte+9rWsX78+hx12WNXjFDFu3Li8/vWvT5KccMIJ2bhxYz796U/nhhtuqHiy9ti0aVO2b9+eN7/5zc/tazabWb9+fa677roMDAyk0WhUOGEZr3rVqzJjxow88sgjVY/SVlOmTHnRNxEzZ87Ml7/85YomKuuxxx7L3Xffna985StVj9J2H/3oR/Oxj30s73vf+5Ikxx57bB577LEsX768I4uX53j9lnHjxuWEE07IPffc89y+oaGh3HPPPbV4/kvdtFqtLFq0KGvWrMl//Md/ZPr06VWPVJmhoaEMDAxUPUbbnHbaadmyZUs2b9783DZ79uycd9552bx5cy1KV5I89dRT+e53v5spU6ZUPUpbzZ0790UvDdPf35/DDz+8oonKWrVqVSZPnpwzzzyz6lHa7pe//GUOOGB4nWk0GhkaGqpooj1zxWsES5Ysyfz58zN79uycdNJJueaaa7Jz586cf/75VY/WVk899dSw74IfffTRbN68OQcffHCmTZtW4WTt09fXl9WrV+f222/PhAkT8sQTTyRJJk6cmPHjx1c8XftceumlmTdvXqZNm5Zf/OIXWb16de69997cddddVY/WNhMmTHjRc/de/vKX59WvfvWYfk7fRz7ykZx11lk5/PDD8+Mf/ziXX355Go1G3v/+91c9Wlt9+MMfzimnnJIrr7wy73nPe/LAAw/kxhtvzI033lj1aG03NDSUVatWZf78+enuHvtf5s8666xcccUVmTZtWmbNmpVvfOMbWbFiRS644IKqRxtZ1b9W2amuvfba1rRp01rjxo1rnXTSSa0NGzZUPVLb/ed//mcryYu2+fPnVz1a24yUN0lr1apVVY/WVhdccEHr8MMPb40bN641adKk1mmnndb693//96rHKq4OLyfx3ve+tzVlypTWuHHjWq997Wtb733ve1uPPPJI1WMV8a//+q+t3t7eVk9PT+voo49u3XjjjVWPVMRdd93VStJ6+OGHqx6liJ///Oetiy++uDVt2rTWgQce2Hrd617X+uu//uvWwMBA1aONqKvV6tCXdgUAGGM8xwsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxWsPBgYGsnTp0jH9at4jqWPuOmZO5K5T7jpmTuSWu/N4Ha89+PnPf56JEydmx44deeUrX1n1OMXUMXcdMydy1yl3HTMncsvdeVzxAgAoRPECAChE8QIAKETx2oPu7u4sWLCgFn/d/YXqmLuOmRO565S7jpkTueXuPJ5cvwfNZjPbtm3LzJkz02g0qh6nmDrmrmPmRO465a5j5kRuuTuPK14AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIXsU/G6/vrrc8QRR+TAAw/MySefnAceeOClngsAYMwZdfG65ZZbsmTJklx++eV56KGHcvzxx+dd73pXtm/f3o75AADGjFEXrxUrVuRDH/pQzj///BxzzDH5zGc+kz/4gz/IP/3TP7VjPgCAMaN7NAc//fTT2bRpUy699NLn9h1wwAE5/fTT8/Wvf33E+wwMDGRgYGD4ot3d6enp2Ydxy2o2m8Pe1kUdc9cxcyJ3nXLXMXMit9xlNRqN33lMV6vVau3tO/zxj3+c1772tbn//vszZ86c5/b/1V/9VdatW5f/+q//etF9li5dmmXLlg3bt2DBgixcuHBvlwUA6Hi9vb2/85hRXfHaF5deemmWLFkyfNH96IpXf39/ZsyYsVctdqyoY+46Zk7krlPuOmZO5Ja784yqeL3mNa9Jo9HIk08+OWz/k08+mUMPPXTE+/T09OwXJWtPGo1Gx34C26mOueuYOZG7TuqYOZG7bjo596ieXD9u3LiccMIJueeee57bNzQ0lHvuuWfYjx4BAHixUf+occmSJZk/f35mz56dk046Kddcc0127tyZ888/vx3zAQCMGaMuXu9973vzv//7v/nEJz6RJ554Im984xtz55135pBDDmnHfAAAY8Y+Pbl+0aJFWbRo0Us9CwDAmOZvNQIAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABQy6uK1fv36nHXWWZk6dWq6urpy2223tWEsAICxZ9TFa+fOnTn++ONz/fXXt2MeAIAxq3u0d5g3b17mzZvXjlkAAMa0URev0RoYGMjAwMDwRbu709PT0+6lf2/NZnPY27qoY+46Zk7krlPuOmZO5Ja7rEaj8TuP6Wq1Wq19XaCrqytr1qzJOeecs9tjli5dmmXLlg3bt2DBgixcuHBflwUA6Di9vb2/85i2F6/9/YpXf39/ZsyYsVct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cTJ3btLbW/U05dQ1N0AVRlW8vvrVr+Zd73pXzj333Kxbty6vfe1rs3DhwnzoQx/a7X0GBgYyMDAwfNHu7vT09OzbxAU1m81hb+uiU3I3GmXX6+tLtm5N7ruv3JojfYzHeu4RM5dZulJyP2+s56763PnCGTphlpKqzt3YixP4qIrX9773vaxcuTJLlizJZZddlo0bN+aiiy7KuHHjMn/+/BHvs3z58ixbtmzYvgULFmThwoWjWbpS/f39VY9Qiapzl7z6smhR8rWvJevXJ4cdVm7dbdu2vWjfWM89YuYyS1dK7ueN9dwjZa5K1efxqlSVu3cvTuBdrVartbfvcNy4cZk9e3buv//+5/ZddNFF2bhxY77+9a+PeJ/9/YpXf39/ZsyYsVctdqzolNyNxqh/92PUWq3kwguTNWuSe+9N3vCGti85TLM5+KJ9Yz33iJm725/5t3UlWZPknELrNQflflbp3J2QubROOY+XVnXul/yK15QpU3LMMccM2zdz5sx8+ctf3u19enp69ouStSeNRqNWD9xn1SF3X1+yenVy++27XtPqiSd27Z84MRk/vv3rV/XxrTJ3lY+pp5I88oLbjybZnOTgJNPavLbcZdUx80jqcB4fSSfnHtXreM2dOzcP/9arLPb39+fwww9/SYeCUlau3PUbfaeemkyZ8vx2yy1VT9Zedc39YJI3/WZLkiW/+fcnKpuojDrmrmNm9g+juuL14Q9/OKecckquvPLKvOc978kDDzyQG2+8MTfeeGO75oO22vsftI8tdc19apI6Rj819ct9auqXmf3DqK54nXjiiVmzZk2++MUvpre3N5/85CdzzTXX5LzzzmvXfAAAY8aon+H47ne/O+9+97vbMQsAwJjmbzUCABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUonjRwVrFtmZzMFu3bkmzOVh03XrmHilyq+jWHBzM1i1b0hwcLLduxbkryVzX3LAHihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIaMqXkcccUS6urpetPX19bVrPgCAMaN7NAdv3LgxzWbzudtbt27NO9/5zpx77rkv+WAAAGPNqIrXpEmTht2+6qqrcuSRR+Ztb3vbbu8zMDCQgYGB4Yt2d6enp2c0S1fi2ZL5wrJZB3XMXcfMidx1yl3HzInccpfVaDR+5zFdrVartS/v/Omnn87UqVOzZMmSXHbZZbs9bunSpVm2bNmwfQsWLMjChQv3ZVkAgI7U29v7O4/Z5+J166235s/+7M/y/e9/P1OnTt3tcfv7Fa/+/v7MmDFjr1rsWFHH3HXMnMhdp9x1zJzILXdZe7PmqH7U+EKf+9znMm/evD2WriTp6enZL0rWnjQajVo9cJ9Vx9x1zJzIXSd1zJzIXTednHufitdjjz2Wu+++O1/5ylde6nkAAMasfXodr1WrVmXy5Mk588wzX+p5AADGrFEXr6GhoaxatSrz589Pd/c+/6QSAKB2Rl287r777nz/+9/PBRdc0I55AADGrFFfsjrjjDOyj78ICQBQa/5WIwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhoypezWYzH//4xzN9+vSMHz8+Rx55ZD75yU+m1Wq1az4AgDGjezQHX3311Vm5cmU+//nPZ9asWXnwwQdz/vnnZ+LEibnooovaNSMAwJgwquJ1//335+yzz86ZZ56ZJDniiCPyxS9+MQ888EBbhgMAGEtGVbxOOeWU3Hjjjenv78+MGTPyzW9+M/fdd19WrFix2/sMDAxkYGBg+KLd3enp6dm3iQtqNpvD3tZFHXPXMXMid51y1zFzIrfcZTUajd95TFdrFE/QGhoaymWXXZZPfepTaTQaaTabueKKK3LppZfu9j5Lly7NsmXLhu1bsGBBFi5cuLfLAgB0vN7e3t95zKiK180335yPfvSj+bu/+7vMmjUrmzdvzuLFi7NixYrMnz9/xPvs71e8nr26tzctdqyoY+46Zk7krlPuOmZO5Ja7rL1Zc1Q/avzoRz+aj33sY3nf+96XJDn22GPz2GOPZfny5bstXj09PftFydqTRqNRqwfus+qYu46ZE7nrpI6ZE7nrppNzj+rlJH75y1/mgAOG36XRaGRoaOglHQoAYCwa1RWvs846K1dccUWmTZuWWbNm5Rvf+EZWrFiRCy64oF3zAQCMGaMqXtdee20+/vGPZ+HChdm+fXumTp2av/zLv8wnPvGJds0HADBmjKp4TZgwIddcc02uueaaNo0DADB2+VuNAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhXS1Wq1W1UMAANSBK14AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACF/H95x8g4iiK/eQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAArSElEQVR4nO3df5BddX0+8Ge5a5ZUYySaQCINRGwgZAGVAA3xqyiIzSAD7Qz+KJ1GcJxOsgFiRouhoyS1ELDTDA7QCNYGZ9oIVI1Yp0ADLckwmBKCcRIbWVGKv4DUjkaJupC79/tHBFnYxGzM/ZybPa/XzJ3LPXPuft7P7uXss2dP7na1Wq1WAABou0OqHgAAoC4ULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIUrz248cYbc/TRR+fQQw/NaaedlgcffLDqkdpu/fr1OffcczNlypR0dXXly1/+ctUjtd3y5ctzyimnZNy4cZk0aVLOP//8PPLII1WP1XYrV67MiSeemFe+8pV55StfmdmzZ+fOO++seqyirrnmmnR1dWXRokVVj9JWS5cuTVdX15DbcccdV/VYRfzwhz/Mn/3Zn+XVr351xo4dmxNOOCEPPfRQ1WO11dFHH/2Sr3dXV1f6+vqqHq1tms1mPvaxj2XatGkZO3ZsjjnmmHziE59Ip/5FRMVrGLfddlsWL16cK6+8Mg8//HBOOumkvPOd78z27durHq2tdu7cmZNOOik33nhj1aMUs27duvT19WXDhg1Zu3Ztnn322Zx99tnZuXNn1aO11ZFHHplrrrkmmzZtykMPPZS3v/3tOe+88/LNb36z6tGK2LhxY2666aaceOKJVY9SxMyZM/PEE088f7v//vurHqntfvKTn2TOnDl52cteljvvvDP//d//nb/7u7/LYYcdVvVobbVx48YhX+u1a9cmSS644IKKJ2ufa6+9NitXrswNN9yQbdu25dprr80nP/nJXH/99VWPNrwWL3Hqqae2+vr6nn/cbDZbU6ZMaS1fvrzCqcpK0lqzZk3VYxS3ffv2VpLWunXrqh6luMMOO6z1D//wD1WP0XY///nPW3/wB3/QWrt2beutb31r67LLLqt6pLa68sorWyeddFLVYxR3+eWXt9785jdXPUblLrvsstYxxxzTGhwcrHqUtjnnnHNaF1988ZBtf/Inf9K68MILK5po75zxepFnnnkmmzZtyllnnfX8tkMOOSRnnXVWvva1r1U4GSXs2LEjSTJhwoSKJymn2Wzm1ltvzc6dOzN79uyqx2m7vr6+nHPOOUP+Hx/tvv3tb2fKlCl53etelwsvvDDf+973qh6p7b7yla9k1qxZueCCCzJp0qS88Y1vzGc+85mqxyrqmWeeyT/90z/l4osvTldXV9XjtM3pp5+ee++9N/39/UmSb3zjG7n//vszd+7ciicbXnfVA3SaH//4x2k2mzn88MOHbD/88MPzrW99q6KpKGFwcDCLFi3KnDlz0tvbW/U4bbdly5bMnj07v/rVr/KKV7wia9asyfHHH1/1WG1166235uGHH87GjRurHqWY0047LbfcckuOPfbYPPHEE1m2bFn+3//7f9m6dWvGjRtX9Xht893vfjcrV67M4sWLc8UVV2Tjxo259NJLM2bMmMybN6/q8Yr48pe/nJ/+9Kd5//vfX/UobfXRj340P/vZz3Lcccel0Wik2WzmqquuyoUXXlj1aMNSvODX+vr6snXr1lpc/5Ikxx57bDZv3pwdO3bkC1/4QubNm5d169aN2vL1/e9/P5dddlnWrl2bQw89tOpxinnhT/0nnnhiTjvttBx11FG5/fbb84EPfKDCydprcHAws2bNytVXX50keeMb35itW7fm05/+dG2K12c/+9nMnTs3U6ZMqXqUtrr99tvzz//8z1m9enVmzpyZzZs3Z9GiRZkyZUpHfq0Vrxd5zWtek0ajkaeeemrI9qeeeipHHHFERVPRbgsXLsxXv/rVrF+/PkceeWTV4xQxZsyYvP71r0+SnHzyydm4cWM+9alP5aabbqp4svbYtGlTtm/fnje96U3Pb2s2m1m/fn1uuOGGDAwMpNFoVDhhGa961asyffr0PProo1WP0laTJ09+yQ8RM2bMyBe/+MWKJirr8ccfzz333JMvfelLVY/Sdh/5yEfy0Y9+NO9973uTJCeccEIef/zxLF++vCOLl2u8XmTMmDE5+eSTc++99z6/bXBwMPfee28trn+pm1arlYULF2bNmjX5j//4j0ybNq3qkSozODiYgYGBqsdomzPPPDNbtmzJ5s2bn7/NmjUrF154YTZv3lyL0pUkTz/9dL7zne9k8uTJVY/SVnPmzHnJW8P09/fnqKOOqmiislatWpVJkyblnHPOqXqUtvvFL36RQw4ZWmcajUYGBwcrmmjvnPEaxuLFizNv3rzMmjUrp556aq677rrs3LkzF110UdWjtdXTTz895Kfgxx57LJs3b86ECRMyderUCidrn76+vqxevTp33HFHxo0blyeffDJJMn78+IwdO7bi6dpnyZIlmTt3bqZOnZqf//znWb16de67777cfffdVY/WNuPGjXvJtXsvf/nL8+pXv3pUX9P34Q9/OOeee26OOuqo/OhHP8qVV16ZRqOR973vfVWP1lYf+tCHcvrpp+fqq6/Ou9/97jz44IO5+eabc/PNN1c9WtsNDg5m1apVmTdvXrq7R/+3+XPPPTdXXXVVpk6dmpkzZ+brX/96VqxYkYsvvrjq0YZX9T+r7FTXX399a+rUqa0xY8a0Tj311NaGDRuqHqnt/vM//7OV5CW3efPmVT1a2wyXN0lr1apVVY/WVhdffHHrqKOOao0ZM6Y1ceLE1plnntn693//96rHKq4Obyfxnve8pzV58uTWmDFjWq997Wtb73nPe1qPPvpo1WMV8a//+q+t3t7eVk9PT+u4445r3XzzzVWPVMTdd9/dStJ65JFHqh6liJ/97Getyy67rDV16tTWoYce2nrd617X+qu/+qvWwMBA1aMNq6vV6tC3dgUAGGVc4wUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4rUXAwMDWbp06ah+N+/h1DF3HTMnctcpdx0zJ3LL3Xm8j9de/OxnP8v48eOzY8eOvPKVr6x6nGLqmLuOmRO565S7jpkTueXuPM54AQAUongBABSieAEAFKJ47UV3d3fmz59fi7/u/kJ1zF3HzIncdcpdx8yJ3HJ3HhfX70Wz2cy2bdsyY8aMNBqNqscppo6565g5kbtOueuYOZFb7s7jjBcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCH7VbxuvPHGHH300Tn00ENz2mmn5cEHHzzQcwEAjDojLl633XZbFi9enCuvvDIPP/xwTjrppLzzne/M9u3b2zEfAMCoMeLitWLFinzwgx/MRRddlOOPPz6f/vSn83u/93v5x3/8x3bMBwAwanSPZOdnnnkmmzZtypIlS57fdsghh+Sss87K1772tWGfMzAwkIGBgaGLdnenp6dnP8Ytq9lsDrmvizrmrmPmRO465a5j5kRuuctqNBq/dZ+uVqvV2tcP+KMf/Sivfe1r88ADD2T27NnPb//Lv/zLrFu3Lv/1X//1kucsXbo0y5YtG7Jt/vz5WbBgwb4uCwDQ8Xp7e3/rPiM647U/lixZksWLFw9d9CA649Xf35/p06fvU4sdLeqYu46ZE7nrlLuOmRO55e48Iyper3nNa9JoNPLUU08N2f7UU0/liCOOGPY5PT09B0XJ2ptGo9GxX8B2qmPuOmZO5K6TOmZO5K6bTs49oovrx4wZk5NPPjn33nvv89sGBwdz7733DvnVIwAALzXiXzUuXrw48+bNy6xZs3Lqqafmuuuuy86dO3PRRRe1Yz4AgFFjxMXrPe95T/73f/83H//4x/Pkk0/mDW94Q+66664cfvjh7ZgPAGDU2K+L6xcuXJiFCxce6FkAAEY1f6sRAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgkBEXr/Xr1+fcc8/NlClT0tXVlS9/+cttGAsAYPQZcfHauXNnTjrppNx4443tmAcAYNTqHukT5s6dm7lz57ZjFgCAUW3ExWukBgYGMjAwMHTR7u709PS0e+nfWbPZHHJfF3XMXcfMidx1yl3HzInccpfVaDR+6z5drVartb8LdHV1Zc2aNTn//PP3uM/SpUuzbNmyIdvmz5+fBQsW7O+yAAAdp7e397fu0/bidbCf8erv78/06dP3qcWOFnXMXcfMidx1yl3HzInccpe1L2u2/VeNPT09B0XJ2ptGo1GrF+5z6pi7jpkTueukjpkTueumk3N7Hy8AgEJGfMbr6aefzqOPPvr848ceeyybN2/OhAkTMnXq1AM6HADAaDLi4vXQQw/lbW972/OPFy9enCSZN29ebrnllgM2GADAaDPi4nXGGWfkd7geHwCgtlzjBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFBId9UDwJ50dZVcrZGkt+SCabWG3y53CZ2Re7RnTuqZe0+vcUic8YLKlP3m09l8LoC6ULwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwgyYIFyWOPJb/8ZbJhQ3LKKVVPVEYdc9cxcyJ33XLTuRQvau/d705WrEiWLUve9KbkG99I7r47mTix6snaq46565g5kbtuuelwrRG4+uqrW7NmzWq94hWvaE2cOLF13nnntb71rW+N5EMcVHbt2tXasmVLa9euXVWPUlSn5E7K3DZsaLWuv/43j7u6Wq0f/KDVuvzy9q9dx9x1zCx3vXJ3gk45jpd2MOQe0RmvdevWpa+vLxs2bMjatWvz7LPP5uyzz87OnTvb1QuhrV72suTkk5N77vnNtlZr9+PZs6ubq93qmLuOmRO565abztc9kp3vuuuuIY9vueWWTJo0KZs2bcpb3vKWAzoYlPCa1yTd3clTTw3d/tRTyXHHVTNTCXXMXcfMidx1y03nG1HxerEdO3YkSSZMmLDHfQYGBjIwMDB00e7u9PT0/C5LF9FsNofc10Xn5G5UvH77Df85Ht2565g5kXuo0Z27+mNnJx3Hy6o6d6Px21/b+128BgcHs2jRosyZMye9vb173G/58uVZtmzZkG3z58/PggUL9nfp4vr7+6seoRLV597z6+pA+fGPk127ksMPH7r98MOTJ59s+/LZtm3bMFtHd+46Zk7kHmp05x4+czWqP45Xo6rce+tDz9nv4tXX15etW7fm/vvv3+t+S5YsyeLFi4cuehCd8erv78/06dP3qcWOFnXK/eyzyaZNyZlnJnfcsXtbV9fuxzfc0P71Z8yY0f5FhlFl7jpmTuQurY6v8Req03H8hQ6G3PtVvBYuXJivfvWrWb9+fY488si97tvT03NQlKy9aTQaHfsFbKe65F6xIvnc55KHHkoefDBZtCh5+cuTVavav3aVn9+qctcxcyJ3Fer4Gn+xuhzHX6yTc4+oeLVarVxyySVZs2ZN7rvvvkybNq1dc0Ext9+++319/vqvkyOOSDZvTv7oj5Lt26uerL3qmLuOmRO565abztbVarVa+7rzggULsnr16txxxx059thjn98+fvz4jB07ti0DVqnZbGbbtm2ZMWNGxzbnduiU3F1dlS1dzHD/94323HXMnMj9QqM9975/V22fTjmOl3Yw5B7R+3itXLkyO3bsyBlnnJHJkyc/f7vtttvaNR8AwKgx4l81AgCwf/ytRgCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULzpWq1XutmtXM1u2bM2uXc2i69Yxd9WZ65rba7zarzU8R/ECAChE8QIAKETxAgAoRPECACiku+oBYE+6ukqu1kjSW3LBJMNfiDvac1efOemU3Em54I1G0lv+JZ5kuOCjPbcr7NkzZ7wAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvCDJggXJY48lv/xlsmFDcsopVU9URh1z1y3z8uW7M44bl0yalJx/fvLII1VP1X51zU3nU7yovXe/O1mxIlm2LHnTm5JvfCO5++5k4sSqJ2uvOuauY+Z165K+vt0lc+3a5Nlnk7PPTnburHqy9qprbg4CrRH4+7//+9YJJ5zQGjduXGvcuHGtP/zDP2z927/920g+xEFl165drS1btrR27dpV9ShFdUrupMxtw4ZW6/rrf/O4q6vV+sEPWq3LL2//2nXMXcfMe8rdaqX4bfv2tJK01q0rtWYdc1evU47jpR0MuUd0xuvII4/MNddck02bNuWhhx7K29/+9px33nn55je/2Z5WCG32spclJ5+c3HPPb7a1Wrsfz55d3VztVsfcdcw8nB07dt9PmFDtHKXVNTedp3skO5977rlDHl911VVZuXJlNmzYkJkzZw77nIGBgQwMDAxdtLs7PT09Ixy1vGazOeS+Ljond6PtK7zmNUl3d/LUU0O3P/VUctxxbV9+D5/j0Z27jpmT4XM32h97iMHBZNGiZM6cpLe3zJp1zF39sbOTjuNlVZ27sQ8v7hEVrxdqNpv5l3/5l+zcuTOz9/Lj4vLly7Ns2bIh2+bPn58FCxbs79LF9ff3Vz1CJarPXeg7Q4W2bds2zNbRnbuOmZPhc5cqP8/p60u2bk3uv7/cmnXMPfxrvBrVH8erUVXu3n14cY+4eG3ZsiWzZ8/Or371q7ziFa/ImjVrcvzxx+9x/yVLlmTx4sVDFz2Iznj19/dn+vTp+9RiR4s65f7xj5Ndu5LDDx+6/fDDkyefbP/6M2bMaP8iw6gydx0zJ9Xlfs7ChclXv5qsX58ceWS5deuYu+rMSb2O4y90MOQecfE69thjs3nz5uzYsSNf+MIXMm/evKxbt26P5aunp+egKFl702g0OvYL2E51yP3ss8mmTcmZZyZ33LF7W1fX7sc33ND+9av6/FaZu46Zk+pyt1rJJZcka9Yk992XTJtWdv065u6k42YdjuPD6eTcIy5eY8aMyetf//okycknn5yNGzfmU5/6VG666aYDPhyUsGJF8rnPJQ89lDz44O5rQV7+8mTVqqona6865q5j5r6+ZPXq3WVz3LjfnN0bPz4ZO7ba2dqprrnpfPt9jddzBgcHX3LxPBxMbr999/s4/fVfJ0cckWzenPzRHyXbt1c9WXvVMXcdM69cufv+jDOGbl+1Knn/+0tPU05dc9P5RlS8lixZkrlz52bq1Kn5+c9/ntWrV+e+++7L3Xff3a75oIgbb9x9q5s65q5b5lar6gmqUdfcdL4RFa/t27fnz//8z/PEE09k/PjxOfHEE3P33XfnHe94R7vmAwAYNUZUvD772c+2aw4AgFHP32oEAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPGiY7Va5W67djWzZcvW7NrVLLpuHXNXnbmTcietYrdmc1e2bt2SZnNX0XXrmRv2TPECAChE8QIAKETxAgAoRPECACiku+oBYM+6iq3UaCS9vcWWe4HhLsQd7bmrzZzUM7fXeEkusGfPnPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8aLWli9PTjklGTcumTQpOf/85JFHqp6q/eqYu46ZE7nrlpvO9zsVr2uuuSZdXV1ZtGjRARoHylq3LunrSzZsSNauTZ59Njn77GTnzqona6865q5j5kTuuuWm83Xv7xM3btyYm266KSeeeOKBnAeKuuuuoY9vuWX3T8ebNiVveUslIxVRx9x1zJzI/Zy65Kbz7dcZr6effjoXXnhhPvOZz+Swww470DNBZXbs2H0/YUK1c5RWx9x1zJzIXbfcdJ79OuPV19eXc845J2eddVb+5m/+Zq/7DgwMZGBgYOii3d3p6enZn6WLajabQ+7rolNyNxpl1xscTBYtSubMSXp7y6w53Od4tOeuY+ZE7hca7bmrPna+cIZOmKWkqnM39uHFPeLideutt+bhhx/Oxo0b92n/5cuXZ9myZUO2zZ8/PwsWLBjp0pXp7++veoRKVJ271DeG5/T1JVu3JvffX27Nbdu2vWTbaM9dx8yJ3C802nMPl7kqVR/Hq1JV7t59eHF3tVqt1r5+wO9///uZNWtW1q5d+/y1XWeccUbe8IY35Lrrrhv2OQf7Ga/+/v5Mnz59n1rsaNEpuRuN/b4EccQWLkzuuCNZvz6ZNq3Ysmk2d71k22jPXcfMidwvNNpzD5e5tE45jpdWde4DfsZr06ZN2b59e970pjc9v63ZbGb9+vW54YYbMjAw8JJFe3p6DoqStTeNRqNWL9zn1CF3q5VcckmyZk1y331lvyEl+/Y/aTtUmbuOmRO5S6vja3w4dTiOD6eTc4+oeJ155pnZsmXLkG0XXXRRjjvuuFx++eUdGxL2pK8vWb1690/E48YlTz65e/v48cnYsdXO1k51zF3HzIncdctN5xvRrxqH89t+1Xgwazab2bZtW2bMmFGrUtk5ubvav8Ielli1Knn/+9u+fJLh/vcb7bnrmDmR+4VGe+7f6dvqAdE5x/GyDobc5X7RDh3od/ux4+BVx9x1zJzIDZ3mdy5e99133wEYAwBg9PO3GgEAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvOhgrWK3ZnNXtm7dkmZzV9F165m72sx1ze01XvXXGnZTvAAAClG8AAAKUbwAAApRvAAAClG8AAAK6a56AODFuoqt1Ggkvb3Flvu14f7VV7nMST1zV5M5qWdu/7KRPXPGCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULamj58uSUU5Jx45JJk5Lzz08eeaTqqdqrjpkTueuWm843ouK1dOnSdHV1Dbkdd9xx7ZoNaJN165K+vmTDhmTt2uTZZ5Ozz0527qx6svapY+ZE7rrlpvN1j/QJM2fOzD333PObD9A94g8BVOyuu4Y+vuWW3WcFNm1K3vKWSkZquzpmTuR+Tl1y0/lG3Jq6u7tzxBFHtGMWoCI7duy+nzCh2jlKqmPmRO665abzjLh4ffvb386UKVNy6KGHZvbs2Vm+fHmmTp26x/0HBgYyMDAwdNHu7vT09Ix82sKazeaQ+7qoY+5OytxolF1vcDBZtCiZMyfp7W3/esN9jkd75kTuFxrtuTvhONJJx7SSqs7d2IcXd1er1Wrt6we888478/TTT+fYY4/NE088kWXLluWHP/xhtm7dmnHjxg37nKVLl2bZsmVDts2fPz8LFizY12WhVnp7Tyi63vz5yZ13Jvffnxx5ZPvX27p1y0u2jfbMidwvNNpzD5eZeujdh2Y/ouL1Yj/96U9z1FFHZcWKFfnABz4w7D4H+xmv/v7+TJ8+fZ9a7GhRx9ydlLnRKHfd5MKFyR13JOvXJ9OmlVmz2dz1km2jPXMi9wuN9tzDZS6tk45pJVWde1/W/J1e/a961asyffr0PProo3vcp6en56AoWXvTaDRq9cJ9Th1z1yVzq5VcckmyZk1y331lvxFX9fmtMnMid2l1fI0Ppy7HtBfr5Ny/0/t4Pf300/nOd76TyZMnH6h5gAL6+pJ/+qdk9erd73P05JO7b7/8ZdWTtU8dMydy1y03nW9ExevDH/5w1q1bl//5n//JAw88kD/+4z9Oo9HI+973vnbNB7TBypW7/5XXGWckkyf/5nbbbVVP1j51zJzIXbfcdL4R/arxBz/4Qd73vvfl//7v/zJx4sS8+c1vzoYNGzJx4sR2zQe0wf5f2XnwqmPmRG7oNCMqXrfeemu75gAAGPX8rUYAgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC/oOK1it2ZzV7Zu3ZJmc1fBdavNXNfc1WSua27YM8ULAKAQxQsAoBDFCwCgEMULAKCQ7qoHgD3rKrZSo5H09hZb7gWGuxB3tOceJnNXucxJ0khSPna1uSvJnNQz93CZ4dec8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxotaWL09OOSUZNy6ZNCk5//zkkUeqnqr96pp7fZJzk0xJ0pXky5VOU04dc9cxMwcHxYtaW7cu6etLNmxI1q5Nnn02OfvsZOfOqidrr7rm3pnkpCQ3Vj1IYXXMXcfMHBy6R/qEH/7wh7n88stz55135he/+EVe//rXZ9WqVZk1a1Y75oO2uuuuoY9vuWX3GaBNm5K3vKWSkYqoa+65v77VTR1z1zEzB4cRFa+f/OQnmTNnTt72trflzjvvzMSJE/Ptb387hx12WLvmg6J27Nh9P2FCtXOUVtfcAKWNqHhde+21+f3f//2sWrXq+W3Tpk074ENBFQYHk0WLkjlzkt7eqqcpp665AaowouL1la98Je985ztzwQUXZN26dXnta1+bBQsW5IMf/OAenzMwMJCBgYGhi3Z3p6enZ/8mLqjZbA65r4tOyd1olF2vry/ZujW5//5yaw73OR7tuYfNXGbpSsn9G6M9d9XHzhfO0AmzlFR17sY+HMBHVLy++93vZuXKlVm8eHGuuOKKbNy4MZdeemnGjBmTefPmDfuc5cuXZ9myZUO2zZ8/PwsWLBjJ0pXq7++veoRKVJ275NmXhQuTr341Wb8+OfLIcutu27btJdtGe+5hM5dZulJy/8Zozz1c5qpUfRyvSlW5e/fhAN7VarVa+/oBx4wZk1mzZuWBBx54ftull16ajRs35mtf+9qwzznYz3j19/dn+vTp+9RiR4tOyd1ojPjffoxYq5VcckmyZk1y333JH/xB25ccotnc9ZJtoz33sJm725/5xbqSrElyfqH1mrvkfk7p3J2QubROOY6XVnXuA37Ga/LkyTn++OOHbJsxY0a++MUv7vE5PT09B0XJ2ptGo1GrF+5z6pC7ry9ZvTq5447d72n15JO7t48fn4wd2/71q/r8Vpm7ytfU00kefcHjx5JsTjIhydQ2ry13WXXMPJw6HMeH08m5R/Q+XnPmzMkjL3qXxf7+/hx11FEHdCgoZeXK3f+i74wzksmTf3O77baqJ2uvuuZ+KMkbf31LksW//u+PVzZRGXXMXcfMHBxGdMbrQx/6UE4//fRcffXVefe7350HH3wwN998c26++eZ2zQdtte+/aB9d6pr7jCR1jH5G6pf7jNQvMweHEZ3xOuWUU7JmzZp8/vOfT29vbz7xiU/kuuuuy4UXXtiu+QAARo0RX+H4rne9K+9617vaMQsAwKjmbzUCABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUonjRwVrFbs3mrmzduiXN5q6i69Yz93CRW0VvzV27snXLljR37Sq3bsW5K8lc19ywF4oXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCEjKl5HH310urq6XnLr6+tr13wAAKNG90h23rhxY5rN5vOPt27dmne84x254IILDvhgAACjzYiK18SJE4c8vuaaa3LMMcfkrW996x6fMzAwkIGBgaGLdnenp6dnJEtX4rmS+cKyWQd1zF3HzIncdcpdx8yJ3HKX1Wg0fus+Xa1Wq7U/H/yZZ57JlClTsnjx4lxxxRV73G/p0qVZtmzZkG3z58/PggUL9mdZAICO1Nvb+1v32e/idfvtt+dP//RP873vfS9TpkzZ434H+xmv/v7+TJ8+fZ9a7GhRx9x1zJzIXafcdcycyC13Wfuy5oh+1fhCn/3sZzN37ty9lq4k6enpOShK1t40Go1avXCfU8fcdcycyF0ndcycyF03nZx7v4rX448/nnvuuSdf+tKXDvQ8AACj1n69j9eqVasyadKknHPOOQd6HgCAUWvExWtwcDCrVq3KvHnz0t2937+pBAConREXr3vuuSff+973cvHFF7djHgCAUWvEp6zOPvvs7Oc/hAQAqDV/qxEAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgkBEVr2azmY997GOZNm1axo4dm2OOOSaf+MQn0mq12jUfAMCo0T2Sna+99tqsXLkyn/vc5zJz5sw89NBDueiiizJ+/Phceuml7ZoRAGBUGFHxeuCBB3LeeeflnHPOSZIcffTR+fznP58HH3ywLcMBAIwmIypep59+em6++eb09/dn+vTp+cY3vpH7778/K1as2ONzBgYGMjAwMHTR7u709PTs38QFNZvNIfd1UcfcdcycyF2n3HXMnMgtd1mNRuO37tPVGsEFWoODg7niiivyyU9+Mo1GI81mM1dddVWWLFmyx+csXbo0y5YtG7Jt/vz5WbBgwb4uCwDQ8Xp7e3/rPiMqXrfeems+8pGP5G//9m8zc+bMbN68OYsWLcqKFSsyb968YZ9zsJ/xeu7s3r602NGijrnrmDmRu06565g5kVvusvZlzRH9qvEjH/lIPvrRj+a9731vkuSEE07I448/nuXLl++xePX09BwUJWtvGo1GrV64z6lj7jpmTuSukzpmTuSum07OPaK3k/jFL36RQw4Z+pRGo5HBwcEDOhQAwGg0ojNe5557bq666qpMnTo1M2fOzNe//vWsWLEiF198cbvmAwAYNUZUvK6//vp87GMfy4IFC7J9+/ZMmTIlf/EXf5GPf/zj7ZoPAGDUGFHxGjduXK677rpcd911bRoHAGD08rcaAQAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAACulqtVqtqocAAKgDZ7wAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAK+f+gNbrG3IbmZQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAreElEQVR4nO3df5DcdX0/8OexZ45UYySaAJFvQkQDIQeoBGiIP1AQm8EMTGfwR2kbwHE6yQWIGS2GjkJqIWCnGRygEawNzrQRqRqxToEGWpJhNCUE4yQaOVEGfwGpHY0S60H29vtHCubkEnMx+/5s7vN4zOzs7Gc+e+/X83Zv73mf+9xeV6vVagUAgLY7rOoBAADqQvECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPHai1tuuSXHHntsDj/88Jxxxhl56KGHqh6p7davX5958+Zl8uTJ6erqype//OWqR2q75cuX57TTTsu4ceMyadKkXHDBBXn00UerHqvtVq5cmZNPPjkvf/nL8/KXvzyzZ8/O3XffXfVYRV1//fXp6urK4sWLqx6lra655pp0dXUNuZxwwglVj1XEj3/84/zpn/5pXvnKV2bs2LE56aST8vDDD1c9Vlsde+yxL3q8u7q60tfXV/VobdNsNvPRj34006ZNy9ixY3Pcccfl4x//eDr1PyIqXsP4/Oc/nyVLluTqq6/OI488klNOOSXvfOc7s3379qpHa6udO3fmlFNOyS233FL1KMWsW7cufX192bBhQ9auXZvnnnsu5557bnbu3Fn1aG11zDHH5Prrr8+mTZvy8MMP5+1vf3vOP//8fOtb36p6tCI2btyYW2+9NSeffHLVoxQxc+bMPPnkky9cHnzwwapHaruf/exnmTNnTl7ykpfk7rvvzre//e383d/9XY444oiqR2urjRs3Dnms165dmyS58MILK56sfW644YasXLkyN998c7Zt25Ybbrghn/jEJ3LTTTdVPdrwWrzI6aef3urr63vhdrPZbE2ePLm1fPnyCqcqK0lrzZo1VY9R3Pbt21tJWuvWrat6lOKOOOKI1j/8wz9UPUbb/fKXv2y97nWva61du7b11re+tXXFFVdUPVJbXX311a1TTjml6jGKu/LKK1tvetObqh6jcldccUXruOOOaw0ODlY9Stucd955rUsv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1+c///ksWbIkV199dR555JGccsopeec735nt27e3Yz4AgFFjxMVrxYoV+cAHPpBLLrkkJ554Yj71qU/lD/7gD/KP//iP7ZgPAGDU6B7Jzs8++2w2bdqUpUuXvrDtsMMOyznnnJOvf/3rw95nYGAgAwMDQxft7k5PT88BjFtWs9kccl0Xdcxdx8yJ3HXKXcfMidxyl9VoNH7nPl2tVqu1vx/wJz/5SV796lfna1/7WmbPnv3C9r/8y7/MunXr8l//9V8vus8111yTZcuWDdm2YMGCLFy4cH+XBQDoeL29vb9znxEd8ToQS5cuzZIlS4Yueggd8erv78/06dP3q8WOFnXMXcfMidx1yl3HzInccneeERWvV73qVWk0Gnn66aeHbH/66adz1FFHDXufnp6eQ6Jk7Uuj0ejYB7Cd6pi7jpkTueukjpkTueumk3OP6OT6MWPG5NRTT83999//wrbBwcHcf//9Q371CADAi434V41LlizJ/PnzM2vWrJx++um58cYbs3PnzlxyySXtmA8AYNQYcfF6z3vek//+7//Oxz72sTz11FN5/etfn3vuuSdHHnlkO+YDABg1Dujk+kWLFmXRokUHexYAgFHN/2oEAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoZMTFa/369Zk3b14mT56crq6ufPnLX27DWAAAo8+Ii9fOnTtzyimn5JZbbmnHPAAAo1b3SO8wd+7czJ07tx2zAACMaiMuXiM1MDCQgYGBoYt2d6enp6fdS//ems3mkOu6qGPuOmZO5K5T7jpmTuSWu6xGo/E79+lqtVqtA12gq6sra9asyQUXXLDXfa655posW7ZsyLYFCxZk4cKFB7osAEDH6e3t/Z37tL14HepHvPr7+zN9+vT9arGjRR1z1zFzInedctcxcyK33GXtz5pt/1VjT0/PIVGy9qXRaNTqifu8OuauY+ZE7jqpY+ZE7rrp5NzexwsAoJARH/F65pln8thjj71w+/HHH8/mzZszYcKETJky5aAOBwAwmoy4eD388MN529ve9sLtJUuWJEnmz5+f22+//aANBgAw2oy4eJ111ln5Pc7HBwCoLed4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABTSXfUAsDddXSVXayTpLblgWq3ht8tdQmfkHu2Zk3rm3ttzHBJHvKAyZb/5dDafC6AuFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC9IsnBh8vjjyf/+b7JhQ3LaaVVPVEYdc9cxcyJ33XLTuRQvau/d705WrEiWLUve+Mbkm99M7r03mTix6snaq46565g5kbtuuelwrRG47rrrWrNmzWq97GUva02cOLF1/vnnt77zne+M5EMcUnbt2tXasmVLa9euXVWPUlSn5E7KXDZsaLVuuuk3t7u6Wq0f/ajVuvLK9q9dx9x1zCx3vXJ3gk55HS/tUMg9oiNe69atS19fXzZs2JC1a9fmueeey7nnnpudO3e2qxdCW73kJcmppyb33febba3W7tuzZ1c3V7vVMXcdMydy1y03na97JDvfc889Q27ffvvtmTRpUjZt2pS3vOUtB3UwKOFVr0q6u5Onnx66/emnkxNOqGamEuqYu46ZE7nrlpvON6Li9dt27NiRJJkwYcJe9xkYGMjAwMDQRbu709PT8/ssXUSz2RxyXRedk7tR8frtN/zneHTnrmPmRO6hRnfu6l87O+l1vKyqczcav/u5fcDFa3BwMIsXL86cOXPS29u71/2WL1+eZcuWDdm2YMGCLFy48ECXLq6/v7/qESpRfe69P68Olp/+NNm1KznyyKHbjzwyeeqpti+fbdu2DbN1dOeuY+ZE7qFGd+7h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1uXVr351Fi5cmA984AN7vc/AwEAGBgaGLtrdnZ6engObuKBmsznkui46JXejUXa9vr5k69bkwQfLrVn157hjHutKVy9juM+x3KNT1V9Pe87QCbOUVHXuxn584xpR8fr+97+flStXZsmSJbnqqquycePGXH755RkzZkzmz58/7H2WL1+eZcuWDdm2YMGCLFy4cCRLV6q/v7/qESpRde6SR50WLUq++tVk/frkmGPKrbtt27Zyi+1D5Y91pauXMdxjLffo1Clf10n1X9tVqSp373584+pqtVqt/f2AY8aMyaxZs/K1r33thW2XX355Nm7cmK9//evD3udQP+LV39+f6dOn71eLHS06JXejMeK//RixViu57LJkzZrkgQeS172u7UsO0WzuKrvgi9bvkMe6u/2P9W/rSrImyQWF1mvuevFjLXcZnZC5tE752i6t6twH/YjX0UcfnRNPPHHIthkzZuSLX/ziXu/T09NzSJSsfWk0GrV64j6vDrn7+pLVq5O77tr9Xl5PPbV7+/jxydix7V+/Uz6/dXisk+SZJI/tcfvxJJuTTEgypc1rV/n5rWPuOmYeTl2+tn9bJ+ceUfGaM2dOHv2td5fs7+/P1KlTD+pQUMrKlbuvzzpr6PZVq5KLLy49De32cJK37XF7yf9dz09ye/Fpyqlj7jpm5tAwouL1wQ9+MGeeeWauu+66vPvd785DDz2U2267Lbfddlu75oO22v9ftDManJWkjg/5Walf7rNSv8wcGkb0zvWnnXZa1qxZk8997nPp7e3Nxz/+8dx444256KKL2jUfAMCoMeIzHN/1rnflXe96VztmAQAY1fyvRgCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULwCAQhQvAIBCFC8AgEIULzpYq9il2dyVrVu3pNncVXRd/k+rVfTS3LUrW7dsSXPXrnLrVpy7ksx1zQ37oHgBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFDKi4nXsscemq6vrRZe+vr52zQcAMGp0j2TnjRs3ptlsvnB769atecc73pELL7zwoA8GADDajKh4TZw4ccjt66+/Pscdd1ze+ta37vU+AwMDGRgYGLpod3d6enpGsnQlni+Ze5bNOqhj7jpmTuSuU+46Zk7klrusRqPxO/fparVarQP54M8++2wmT56cJUuW5Kqrrtrrftdcc02WLVs2ZNuCBQuycOHCA1kWAKAj9fb2/s59Drh43XnnnfmTP/mT/OAHP8jkyZP3ut+hfsSrv78/06dP368WO1rUMXcdMydy1yl3HTMncstd1v6sOaJfNe7pM5/5TObOnbvP0pUkPT09h0TJ2pdGo1GrJ+7z6pi7jpkTueukjpkTueumk3MfUPF64oknct999+VLX/rSwZ4HAGDUOqD38Vq1alUmTZqU884772DPAwAwao24eA0ODmbVqlWZP39+ursP+DeVAAC1M+Lidd999+UHP/hBLr300nbMAwAwao34kNW5556bA/xDSACAWvO/GgEAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKGVHxajab+ehHP5pp06Zl7NixOe644/Lxj388rVarXfMBAIwa3SPZ+YYbbsjKlSvz2c9+NjNnzszDDz+cSy65JOPHj8/ll1/erhkBAEaFERWvr33tazn//PNz3nnnJUmOPfbYfO5zn8tDDz3UluEAAEaTERWvM888M7fddlv6+/szffr0fPOb38yDDz6YFStW7PU+AwMDGRgYGLpod3d6enoObOKCms3mkOu6qGPuOmZO5K5T7jpmTuSWu6xGo/E79+lqjeAErcHBwVx11VX5xCc+kUajkWazmWuvvTZLly7d632uueaaLFu2bMi2BQsWZOHChfu7LABAx+vt7f2d+4yoeN1xxx358Ic/nL/927/NzJkzs3nz5ixevDgrVqzI/Pnzh73PoX7E6/mje/vTYkeLOuauY+ZE7jrlrmPmRG65y9qfNUf0q8YPf/jD+chHPpL3vve9SZKTTjopTzzxRJYvX77X4tXT03NIlKx9aTQatXriPq+OueuYOZG7TuqYOZG7bjo594jeTuJXv/pVDjts6F0ajUYGBwcP6lAAAKPRiI54zZs3L9dee22mTJmSmTNn5hvf+EZWrFiRSy+9tF3zAQCMGiMqXjfddFM++tGPZuHChdm+fXsmT56cv/iLv8jHPvaxds0HADBqjKh4jRs3LjfeeGNuvPHGNo0DADB6+V+NAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhXS1Wq1W1UMAANSBI14AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACF/H9MvcKH2OF95AAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAqoklEQVR4nO3dfZBddX0/8Pdy1yypxgiaAJFfQsQGQhZQCNAQH1AQm8EMTGfwobSN4DidZCPEjBZDR0lqIWCnGRygEawNzrQRqRqxTpEGWpJhNCUEwyQaWVEGn4DUjkaJdSF37++PCGZlE7Ix93tu9rxeM3d27plz9/t5756cfe/Zk92uVqvVCgAAbXdY1QMAANSF4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4gUAUIjiBQBQiOIFAFCI4rUXN998c4477rgcfvjhOeuss/LAAw9UPVLbrV+/PnPnzs2kSZPS1dWVL3/5y1WP1HbLly/PGWeckXHjxmXixIm56KKL8sgjj1Q9VtutXLkyp5xySl7+8pfn5S9/eWbNmpW77rqr6rGKuu6669LV1ZVFixZVPUpbLV26NF1dXUMeJ554YtVjFfHjH/84f/Znf5ZXvvKVGTt2bE4++eQ8+OCDVY/VVscdd9wLPt9dXV3p6+urerS2aTab+ehHP5qpU6dm7NixOf744/Pxj388nfoXERWvYXz+85/P4sWLc/XVV+ehhx7Kqaeemre//e3Zvn171aO11c6dO3Pqqafm5ptvrnqUYtatW5e+vr5s2LAha9euzbPPPpvzzz8/O3furHq0tjr22GNz3XXXZdOmTXnwwQfz1re+NRdeeGG+9a1vVT1aERs3bswtt9ySU045pepRipgxY0aeeOKJ5x/3339/1SO13c9+9rPMnj07L3nJS3LXXXfl29/+dv7+7/8+RxxxRNWjtdXGjRuHfK7Xrl2bJLn44osrnqx9rr/++qxcuTI33XRTtm3bluuvvz6f+MQncuONN1Y92vBavMCZZ57Z6uvre/55s9lsTZo0qbV8+fIKpyorSWvNmjVVj1Hc9u3bW0la69atq3qU4o444ojWP/7jP1Y9Rtv98pe/bP3hH/5ha+3ata03v/nNrSuuuKLqkdrq6quvbp166qlVj1HclVde2XrDG95Q9RiVu+KKK1rHH398a3BwsOpR2uaCCy5oXXbZZUO2/cmf/EnrkksuqWiifXPF63c888wz2bRpU84777zntx122GE577zz8o1vfKPCyShhx44dSZIjjzyy4knKaTabuf3227Nz587MmjWr6nHarq+vLxdccMGQf+Oj3Xe/+91MmjQpr3nNa3LJJZfkBz/4QdUjtd1XvvKVzJw5MxdffHEmTpyY17/+9fn0pz9d9VhFPfPMM/nnf/7nXHbZZenq6qp6nLY5++yzc++996a/vz9J8vDDD+f+++/PnDlzKp5seN1VD9BpfvrTn6bZbOaoo44asv2oo47Kd77znYqmooTBwcEsWrQos2fPTm9vb9XjtN2WLVsya9as/PrXv87LXvayrFmzJieddFLVY7XV7bffnoceeigbN26sepRizjrrrNx222054YQT8sQTT2TZsmV54xvfmK1bt2bcuHFVj9c23//+97Ny5cosXrw4V111VTZu3JjLL788Y8aMybx586oer4gvf/nL+fnPf573vve9VY/SVh/5yEfyi1/8IieeeGIajUaazWauueaaXHLJJVWPNizFC36jr68vW7durcX9L0lywgknZPPmzdmxY0e+8IUvZN68eVm3bt2oLV8//OEPc8UVV2Tt2rU5/PDDqx6nmD2/6z/llFNy1llnZcqUKbnjjjvyvve9r8LJ2mtwcDAzZ87MtddemyR5/etfn61bt+ZTn/pUbYrXZz7zmcyZMyeTJk2qepS2uuOOO/Iv//IvWb16dWbMmJHNmzdn0aJFmTRpUkd+rhWv3/GqV70qjUYjTz311JDtTz31VI4++uiKpqLdFi5cmK9+9atZv359jj322KrHKWLMmDF57WtfmyQ5/fTTs3Hjxnzyk5/MLbfcUvFk7bFp06Zs3749p5122vPbms1m1q9fn5tuuikDAwNpNBoVTljGK17xikybNi2PPvpo1aO01THHHPOCbyKmT5+eL37xixVNVNbjjz+ee+65J1/60peqHqXtPvzhD+cjH/lI3v3udydJTj755Dz++ONZvnx5RxYv93j9jjFjxuT000/Pvffe+/y2wcHB3HvvvbW4/6VuWq1WFi5cmDVr1uQ///M/M3Xq1KpHqszg4GAGBgaqHqNtzj333GzZsiWbN29+/jFz5sxccskl2bx5cy1KV5I8/fTT+d73vpdjjjmm6lHaavbs2S/41TD9/f2ZMmVKRROVtWrVqkycODEXXHBB1aO03a9+9ascdtjQOtNoNDI4OFjRRPvmitcwFi9enHnz5mXmzJk588wzc8MNN2Tnzp259NJLqx6trZ5++ukh3wU/9thj2bx5c4488shMnjy5wsnap6+vL6tXr86dd96ZcePG5cknn0ySjB8/PmPHjq14uvZZsmRJ5syZk8mTJ+eXv/xlVq9enfvuuy9333131aO1zbhx415w795LX/rSvPKVrxzV9/R96EMfyty5czNlypT85Cc/ydVXX51Go5H3vOc9VY/WVh/84Adz9tln59prr8073/nOPPDAA7n11ltz6623Vj1a2w0ODmbVqlWZN29eurtH/5f5uXPn5pprrsnkyZMzY8aMfPOb38yKFSty2WWXVT3a8Kr+b5Wd6sYbb2xNnjy5NWbMmNaZZ57Z2rBhQ9Ujtd1//dd/tZK84DFv3ryqR2ub4fImaa1atarq0drqsssua02ZMqU1ZsyY1oQJE1rnnntu6z/+4z+qHqu4Ovw6iXe9612tY445pjVmzJjWq1/96ta73vWu1qOPPlr1WEX827/9W6u3t7fV09PTOvHEE1u33npr1SMVcffdd7eStB555JGqRyniF7/4ReuKK65oTZ48uXX44Ye3XvOa17T++q//ujUwMFD1aMPqarU69Fe7AgCMMu7xAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETx2oeBgYEsXbp0VP827+HUMXcdMydy1yl3HTMncsvdefwer334xS9+kfHjx2fHjh15+ctfXvU4xdQxdx0zJ3LXKXcdMydyy915XPECAChE8QIAKETxAgAoRPHah+7u7syfP78Wf919T3XMXcfMidx1yl3HzInccnceN9fvQ7PZzLZt2zJ9+vQ0Go2qxymmjrnrmDmRu06565g5kVvuzuOKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIQdUvG6++eYcd9xxOfzww3PWWWflgQceONhzAQCMOiMu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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAqh0lEQVR4nO3dfZBddX0/8Pdy1yyphggaHiJNQGwgZAWVABPiAwpiM5iB6Qw+lLYRHKe/ZCPEjBahoyS1ELDTDA4wEawNzrSIVI1YZ4AGWpJhNCUE4yQaWVEGfABSOxol1IXcvb8/IpiVTcjG3O+5uef1mrmzc8+cm+/nvffs3feePbnb02q1WgEAoO0OqnoAAIC6ULwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbx248Ybb8wxxxyTgw8+OKeffnoeeOCBqkdqu7Vr12bu3LmZPHlyenp68rWvfa3qkdpu2bJlOfXUUzNhwoQcfvjhOf/88/Pwww9XPVbbrVixIieddFIOOeSQHHLIIZk1a1buvPPOqscq6pprrklPT08WLVpU9ShttWTJkvT09Iy4nXDCCVWPVcRPf/rT/MVf/EVe9apXZfz48Xn961+fBx98sOqx2uqYY4550fPd09OTgYGBqkdrm2azmU984hM59thjM378+Bx33HH51Kc+lU79i4iK1yi+9KUvZfHixbnyyivz0EMP5eSTT8673vWubN26terR2mr79u05+eSTc+ONN1Y9SjFr1qzJwMBA1q1bl9WrV+e5557LOeeck+3bt1c9WlsdffTRueaaa7Jhw4Y8+OCDecc73pHzzjsv3/3ud6serYj169fnpptuykknnVT1KEXMmDEjTzzxxAu3+++/v+qR2u4Xv/hFZs+enZe97GW58847873vfS//+I//mEMPPbTq0dpq/fr1I57r1atXJ0kuuOCCiidrn2uvvTYrVqzIDTfckC1btuTaa6/Npz/96Vx//fVVjza6Fi9y2mmntQYGBl6432w2W5MnT24tW7aswqnKStJatWpV1WMUt3Xr1laS1po1a6oepbhDDz209U//9E9Vj9F2v/71r1t/8id/0lq9enXrbW97W+vSSy+teqS2uvLKK1snn3xy1WMUd9lll7Xe/OY3Vz1G5S699NLWcccd1xoeHq56lLY599xzWxdffPGIbX/2Z3/WuvDCCyuaaM+c8fo9zz77bDZs2JCzzz77hW0HHXRQzj777HzrW9+qcDJK2LZtW5LksMMOq3iScprNZm677bZs3749s2bNqnqcthsYGMi555474mu82/3gBz/I5MmT89rXvjYXXnhhHn/88apHaruvf/3rmTlzZi644IIcfvjheeMb35jPfe5zVY9V1LPPPpt/+Zd/ycUXX5yenp6qx2mbM844I/fee28GBweTJN/5zndy//33Z86cORVPNrreqgfoND//+c/TbDZzxBFHjNh+xBFH5Pvf/35FU1HC8PBwFi1alNmzZ6e/v7/qcdpu06ZNmTVrVn7zm9/kFa94RVatWpUTTzyx6rHa6rbbbstDDz2U9evXVz1KMaeffnpuueWWHH/88XniiSeydOnSvOUtb8nmzZszYcKEqsdrmx/96EdZsWJFFi9enCuuuCLr16/PJZdcknHjxmXevHlVj1fE1772tfzyl7/MBz7wgapHaauPf/zj+dWvfpUTTjghjUYjzWYzV111VS688MKqRxuV4gW/NTAwkM2bN9fi+pckOf7447Nx48Zs27YtX/7ylzNv3rysWbOma8vXj3/841x66aVZvXp1Dj744KrHKWbXn/pPOumknH766Zk6dWpuv/32fPCDH6xwsvYaHh7OzJkzc/XVVydJ3vjGN2bz5s357Gc/W5vi9fnPfz5z5szJ5MmTqx6lrW6//fb867/+a2699dbMmDEjGzduzKJFizJ58uSOfK4Vr9/z6le/Oo1GI0899dSI7U899VSOPPLIiqai3RYuXJhvfOMbWbt2bY4++uiqxyli3Lhxed3rXpckOeWUU7J+/fp85jOfyU033VTxZO2xYcOGbN26NW9605te2NZsNrN27drccMMNGRoaSqPRqHDCMl75yldm2rRpeeSRR6oepa2OOuqoF/0QMX369HzlK1+paKKyHnvssdxzzz356le/WvUobfexj30sH//4x/O+970vSfL6178+jz32WJYtW9aRxcs1Xr9n3LhxOeWUU3Lvvfe+sG14eDj33ntvLa5/qZtWq5WFCxdm1apV+c///M8ce+yxVY9UmeHh4QwNDVU9RtucddZZ2bRpUzZu3PjCbebMmbnwwguzcePGWpSuJHn66afzwx/+MEcddVTVo7TV7NmzX/TWMIODg5k6dWpFE5W1cuXKHH74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J41Go1YH7vPqkLvVSj784WTVquS++8p+Q0r27ou0HarMXcfMidyl1fEYH00dXsdH08m5/6Di9fTTT+eHP/xh/vIv/3J/zQNFDQwkt9668yfiCROSJ5/cuX3ixGT8+Gpna6c65q5j5kTuuuWm843pV40f/ehHM3fu3EydOjU/+9nPcuWVV2bjxo353ve+l0mTJrVzzko0m81s2bIl06dP79jm3A6dk7un/SvsZomVK5MPfKDtyycZ7cuv23PXMXMi9666Pfc+X8Gz33TO63hZB0LuMZ3x+slPfpL3v//9+d///d9MmjQpb37zm7Nu3bquLF3Uw75f4Xhgq2PuOmZO5IZOM6biddttt7VrDgCArudvNQIAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieNHBWsVuzeaObN68Kc3mjqLr1jN3tZnrmtsxXvVzDTspXgAAhSheAACFKF4AAIUoXgAAhfRWPQDsVk9PsaUaSfqLrbaL1igX4nZ77oozJ/XM7RgvaLTM8FvOeAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4UWtrk8xNMjlJT5KvVTpNOXLL3e3qmJkDg+JFrW1PcnKSG6sepDC566WOueuYmQND71gf8NOf/jSXXXZZ7rzzzjzzzDN53etel5UrV2bmzJntmA/aas5vb3Ujd73UMXcdM3NgGFPx+sUvfpHZs2fn7W9/e+68885MmjQpP/jBD3LooYe2az4AgK4xpuJ17bXX5o//+I+zcuXKF7Yde+yx+30oAIBuNKbi9fWvfz3vete7csEFF2TNmjV5zWtekwULFuRDH/rQbh8zNDSUoaGhkYv29qavr2/fJi6o2WyO+FgXnZK7UenqZYz2Oe723HXMnMi9q27PXfVr564zdMIsJVWdu9F46aN7TMXrRz/6UVasWJHFixfniiuuyPr163PJJZdk3LhxmTdv3qiPWbZsWZYuXTpi2/z587NgwYKxLF2pwcHBqkeoRNW5+ytdvYwtW7a8aFu3565j5kTuXXV77tEyV6Xq1/GqVJW7v/+lj+6eVqvV2tt/cNy4cZk5c2a++c1vvrDtkksuyfr16/Otb31r1Mcc6Ge8BgcHM23atL1qsd2iU3I3esf8fz/+ID1JViU5v+CazR07XrSt23N3QuZE7vMLrdcJuTshc2md8jpeWtW59/sZr6OOOionnnjiiG3Tp0/PV77yld0+pq+v74AoWXvSaDRqdeA+rw65n07yyC73H02yMclhSaYUWL+qz2+Vuas8puTeqQ6565h5NHV4HR9NJ+ceU/GaPXt2Hn744RHbBgcHM3Xq1P06FJTyYJK373J/8W8/zktyS/FpypF7J7m7N3cdM3NgGFPx+shHPpIzzjgjV199dd7znvfkgQceyM0335ybb765XfNBW52ZZK9/195FzozcdXJm6pf7zNQvMweGMb1z/amnnppVq1bli1/8Yvr7+/OpT30q1113XS688MJ2zQcA0DXGfIXju9/97rz73e9uxywAAF3N32oEAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPGic7VaxW7NHTuyedOmNHfsKLpuLXNXnLmuuR3jFT/X8FuKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhYypexxxzTHp6el50GxgYaNd8AABdo3csO69fvz7NZvOF+5s3b8473/nOXHDBBft9MACAbjOm4jVp0qQR96+55pocd9xxedvb3rbbxwwNDWVoaGjkor296evrG8vSlXi+ZO5aNuugjrnrmDmRu06565g5kVvushqNxkvu09NqtVr78o8/++yzmTx5chYvXpwrrrhit/stWbIkS5cuHbFt/vz5WbBgwb4sCwDQkfr7+19yn30uXrfffnv+/M//PI8//ngmT5682/0O9DNeg4ODmTZt2l612G5Rx9x1zJzIXafcdcycyC13WXuz5ph+1birz3/+85kzZ84eS1eS9PX1HRAla08ajUatDtzn1TF3HTMnctdJHTMnctdNJ+fep+L12GOP5Z577slXv/rV/T0PAEDX2qf38Vq5cmUOP/zwnHvuuft7HgCArjXm4jU8PJyVK1dm3rx56e3d599UAgDUzpiL1z333JPHH388F198cTvmAQDoWmM+ZXXOOedkH/8jJABArflbjQAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFjKl4NZvNfOITn8ixxx6b8ePH57jjjsunPvWptFqtds0HANA1esey87XXXpsVK1bkC1/4QmbMmJEHH3wwF110USZOnJhLLrmkXTMCAHSFMRWvb37zmznvvPNy7rnnJkmOOeaYfPGLX8wDDzzQluEAALrJmIrXGWeckZtvvjmDg4OZNm1avvOd7+T+++/P8uXLd/uYoaGhDA0NjVy0tzd9fX37NnFBzWZzxMe6qGPuOmZO5K5T7jpmTuSWu6xGo/GS+/S0xnCB1vDwcK644op8+tOfTqPRSLPZzFVXXZXLL798t49ZsmRJli5dOmLb/Pnzs2DBgr1dFgCg4/X397/kPmMqXrfddls+9rGP5R/+4R8yY8aMbNy4MYsWLcry5cszb968UR9zoJ/xev7s3t602G5Rx9x1zJzIXafcdcycyC13WXuz5ph+1fixj30sH//4x/O+970vSfL6178+jz32WJYtW7bb4tXX13dAlKw9aTQatTpwn1fH3HXMnMhdJ3XMnMhdN52ce0xvJ/HMM8/koINGPqTRaGR4eHi/DgUA0I3GdMZr7ty5ueqqqzJlypTMmDEj3/72t7N8+fJcfPHF7ZoPAKBrjKl4XX/99fnEJz6RBQsWZOvWrZk8eXL++q//Op/85CfbNR8AQNcYU/GaMGFCrrvuulx33XVtGgcAoHv5W40AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACF9LRarVbVQwAA1IEzXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIX8f4hzaSz2L6McAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAqbElEQVR4nO3df5BddX0+8Ge5axaqEUETINJAxAZCFlAJ0BC/ioLYDDLQzuCP0mkEx+kkGyFmtBg6SlILATvN4ACNYG1wxkagasQ6RRpoSYbBlBCMk9jIilL8BaR2NErUhdy93z9SMCubkBtzP+dmz+s1c+fOPXPuft7P7snZZ8/e3O1ptVqtAADQcQdVPQAAQF0oXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXgAAhSheAACFKF4AAIUoXrtx00035dhjj83BBx+cM844Iw8++GDVI3Xc2rVrc/7552fSpEnp6enJl7/85apH6rilS5fmtNNOy/jx4zNx4sRceOGFeeSRR6oeq+OWL1+ek08+OS9/+cvz8pe/PDNnzsxdd91V9VhFXXvttenp6cmCBQuqHqWjFi9enJ6enhG3E044oeqxivjRj36UP/uzP8srX/nKHHLIITnppJPy0EMPVT1WRx177LEv+Hr39PRkYGCg6tE6ptls5qMf/WimTJmSQw45JMcdd1w+/vGPp1v/IqLiNYrbb789CxcuzFVXXZWHH344p5xySt7+9rdn69atVY/WUdu3b88pp5ySm266qepRilmzZk0GBgaybt26rF69Os8++2zOPffcbN++verROuroo4/Otddemw0bNuShhx7KW9/61lxwwQX51re+VfVoRaxfvz4333xzTj755KpHKWL69Ol54oknnr/df//9VY/UcT/96U8za9asvOQlL8ldd92V//qv/8rf/d3f5bDDDqt6tI5av379iK/16tWrkyQXXXRRxZN1znXXXZfly5fnxhtvzJYtW3LdddflE5/4RG644YaqRxtdixc4/fTTWwMDA88/bjabrUmTJrWWLl1a4VRlJWmtWrWq6jGK27p1aytJa82aNVWPUtxhhx3W+od/+Ieqx+i4X/ziF60/+IM/aK1evbr15je/uXX55ZdXPVJHXXXVVa1TTjml6jGKu+KKK1pvfOMbqx6jcpdffnnruOOOaw0PD1c9Ssecd955rUsvvXTEtj/5kz9pXXzxxRVNtGeueP2WZ555Jhs2bMg555zz/LaDDjoo55xzTr7+9a9XOBklbNu2LUly+OGHVzxJOc1mM7fddlu2b9+emTNnVj1Oxw0MDOS8884b8W98rPvOd76TSZMm5TWveU0uvvjifP/73696pI77yle+khkzZuSiiy7KxIkT8/rXvz6f/vSnqx6rqGeeeSaf+9zncumll6anp6fqcTrmzDPPzL333pvBwcEkyTe/+c3cf//9mT17dsWTja636gG6zU9+8pM0m80cccQRI7YfccQR+fa3v13RVJQwPDycBQsWZNasWenv7696nI7btGlTZs6cmV//+td52ctellWrVuXEE0+seqyOuu222/Lwww9n/fr1VY9SzBlnnJFbb701xx9/fJ544oksWbIk/+///b9s3rw548ePr3q8jvne976X5cuXZ+HChbnyyiuzfv36XHbZZRk3blzmzJlT9XhFfPnLX87PfvazvPe97616lI76yEc+kp///Oc54YQT0mg00mw2c/XVV+fiiy+uerRRKV7wfwYGBrJ58+ZavP4lSY4//vhs3Lgx27Ztyxe+8IXMmTMna9asGbPl6wc/+EEuv/zyrF69OgcffHDV4xSz60/9J598cs4444wcc8wxueOOO/K+972vwsk6a3h4ODNmzMg111yTJHn961+fzZs351Of+lRtitdnPvOZzJ49O5MmTap6lI6644478k//9E9ZuXJlpk+fno0bN2bBggWZNGlSV36tFa/f8qpXvSqNRiNPPfXUiO1PPfVUjjzyyIqmotPmz5+fr371q1m7dm2OPvroqscpYty4cXnta1+bJDn11FOzfv36fPKTn8zNN99c8WSdsWHDhmzdujVveMMbnt/WbDazdu3a3HjjjRkaGkqj0ahwwjJe8YpXZOrUqXn00UerHqWjjjrqqBf8EDFt2rR88YtfrGiish5//PHcc889+dKXvlT1KB334Q9/OB/5yEfy7ne/O0ly0kkn5fHHH8/SpUu7snh5jddvGTduXE499dTce++9z28bHh7OvffeW4vXv9RNq9XK/Pnzs2rVqvz7v/97pkyZUvVIlRkeHs7Q0FDVY3TM2WefnU2bNmXjxo3P32bMmJGLL744GzdurEXpSpKnn3463/3ud3PUUUdVPUpHzZo16wVvDTM4OJhjjjmmoonKWrFiRSZOnJjzzjuv6lE67pe//GUOOmhknWk0GhkeHq5ooj1zxWsUCxcuzJw5czJjxoycfvrpuf7667N9+/ZccsklVY/WUU8//fSIn4Ife+yxbNy4MYcffngmT55c4WSdMzAwkJUrV+bOO+/M+PHj8+STTyZJDj300BxyyCEVT9c5ixYtyuzZszN58uT84he/yMqVK3Pffffl7rvvrnq0jhk/fvwLXrv30pe+NK985SvH9Gv6PvShD+X888/PMccckx//+Me56qqr0mg08p73vKfq0Trqgx/8YM4888xcc801eec735kHH3wwt9xyS2655ZaqR+u44eHhrFixInPmzElv79j/Nn/++efn6quvzuTJkzN9+vR84xvfyLJly3LppZdWPdroqv5vld3qhhtuaE2ePLk1bty41umnn95at25d1SN13H/8x3+0krzgNmfOnKpH65jR8iZprVixourROurSSy9tHXPMMa1x48a1JkyY0Dr77LNb//Zv/1b1WMXV4e0k3vWud7WOOuqo1rhx41qvfvWrW+9617tajz76aNVjFfEv//Ivrf7+/lZfX1/rhBNOaN1yyy1Vj1TE3Xff3UrSeuSRR6oepYif//znrcsvv7w1efLk1sEHH9x6zWte0/qrv/qr1tDQUNWjjaqn1erSt3YFABhjvMYLAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMVrD4aGhrJ48eIx/W7eo6lj7jpmTuSuU+46Zk7klrv7eB+vPfj5z3+eQw89NNu2bcvLX/7yqscppo6565g5kbtOueuYOZFb7u7jihcAQCGKFwBAIYoXAEAhitce9Pb2Zu7cubX46+67qmPuOmZO5K5T7jpmTuSWu/t4cf0eNJvNbNmyJdOmTUuj0ah6nGLqmLuOmRO565S7jpkTueXuPq54AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUsk/F66abbsqxxx6bgw8+OGeccUYefPDB/T0X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c4+euRrVn8erUVXuPfWh5+xz8RoYGMjmzZtz//3373G/RYsWZeHChSMXPYCueA0ODmbq1Kl71WLHijrlfvbZZMOG5Oyzkzvv3Lmtp2fn4xtv7Pz606ZN6/wio6gydx0zJ3KXVsdjfFd1Oo/v6kDIvU/Fa/78+fnqV7+atWvX5uijj97jvn19fQdEydqTRqPRtV/ATqpL7mXLks9+NnnooeTBB5MFC5KXvjRZsaLza1f5+a0qdx0zJ3JXoY7H+G+ry3n8t3Vz7raKV6vVygc+8IGsWrUq9913X6ZMmdKpuaCYO+7Y+b4+f/3XyZFHJhs3Jn/0R8nWrVVP1ll1zF3HzIncdctNd+tptVqtvd153rx5WblyZe68884cf/zxz28/9NBDc8ghh3RkwCo1m81s2bIl06ZN69rm3Andkrunp7KlixntX99Yz13HzIncuxrruff+u2rndMt5vLQDIXdb7+O1fPnybNu2LWeddVaOOuqo52+33357p+YDABgz2v5VIwAA+8bfagQAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8aJrtVrlbjt2NLNp0+bs2NEsum4dc1edua65HePVfq3hOYoXAEAhihcAQCGKFwBAIYoXAEAhvVUPALvT01NytUaS/pILJhn9hbhjPXf1mZNuyZ2UC95oJP3lD/EkowUf67m9wp7dc8ULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMULAKAQxQuSzJuXPPZY8qtfJevWJaedVvVEZdQxd90yL126M+P48cnEicmFFyaPPFL1VJ1X19x0P8WL2nvnO5Nly5IlS5I3vCH55jeTu+9OJkyoerLOqmPuOmZesyYZGNhZMlevTp59Njn33GT79qon66y65uYA0GrD3//937dOOumk1vjx41vjx49v/eEf/mHrX//1X9v5EAeUHTt2tDZt2tTasWNH1aMU1S25kzK3detarRtu+M3jnp5W64c/bLWuuKLz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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAqgklEQVR4nO3dfZBddX0/8Pdy1yypxghKgEgTIjYQsoBKgIb4gILYDGZkOoMPpW0Ex+kv2Qgxo0XoKKQWA3aawQEawdrgTBuRqhHrFGmgJRlGU0IwTqIpK8qAD0BqR6OEupC79/dHBLOyCdmY+z0397xeM3fu3DP37vfz3j05+95zT3Z7Wq1WKwAAtN0hVQ8AAFAXihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhitce3HjjjTn22GNz6KGH5owzzsh9991X9Uhtt27dusybNy+TJ09OT09PvvKVr1Q9UtstW7Ysp512WiZMmJBJkybl/PPPz4MPPlj1WG23YsWKnHzyyXnpS1+al770pZk9e3buuOOOqscq6pprrklPT08WL15c9ShtddVVV6Wnp2fE7YQTTqh6rCJ+/OMf50//9E/z8pe/POPHj89JJ52U+++/v+qx2urYY4993te7p6cnAwMDVY/WNs1mMx/96Eczbdq0jB8/Pscdd1w+/vGPp1P/IqLiNYovfOELWbJkSa688so88MADOeWUU/K2t70t27Ztq3q0ttqxY0dOOeWU3HjjjVWPUszatWszMDCQ9evXZ82aNXnmmWdy7rnnZseOHVWP1lbHHHNMrrnmmmzcuDH3339/3vKWt+Qd73hHvvOd71Q9WhEbNmzITTfdlJNPPrnqUYqYOXNmHnvssedu9957b9Ujtd3PfvazzJkzJy960Ytyxx135Lvf/W7+7u/+LocddljVo7XVhg0bRnyt16xZkyS54IILKp6sfa699tqsWLEiN9xwQ7Zu3Zprr702n/zkJ3P99ddXPdroWjzP6aef3hoYGHjucbPZbE2ePLm1bNmyCqcqK0lr9erVVY9R3LZt21pJWmvXrq16lOIOO+yw1j/8wz9UPUbb/fKXv2z9wR/8QWvNmjWtN73pTa1LL7206pHa6sorr2ydcsopVY9R3GWXXdZ6/etfX/UYlbv00ktbxx13XGt4eLjqUdrmvPPOa1188cUjtv3xH/9x68ILL6xoor1zxuu3PP3009m4cWPOOeec57YdcsghOeecc/LNb36zwskoYfv27UmSww8/vOJJymk2m7n11luzY8eOzJ49u+px2m5gYCDnnXfeiH/j3e573/teJk+enFe96lW58MIL8+ijj1Y9Utt99atfzaxZs3LBBRdk0qRJee1rX5vPfOYzVY9V1NNPP51/+qd/ysUXX5yenp6qx2mbM888M3fffXcGBweTJN/+9rdz7733Zu7cuRVPNrreqgfoND/96U/TbDZz5JFHjth+5JFH5r//+78rmooShoeHs3jx4syZMyf9/f1Vj9N2mzdvzuzZs/OrX/0qL3nJS7J69eqceOKJVY/VVrfeemseeOCBbNiwoepRijnjjDNyyy235Pjjj89jjz2WpUuX5g1veEO2bNmSCRMmVD1e2/zgBz/IihUrsmTJklxxxRXZsGFDLrnkkowbNy7z58+verwivvKVr+TnP/953vve91Y9Slt95CMfyS9+8YuccMIJaTQaaTabufrqq3PhhRdWPdqoFC/4tYGBgWzZsqUW178kyfHHH59NmzZl+/bt+eIXv5j58+dn7dq1XVu+fvjDH+bSSy/NmjVrcuihh1Y9TjG7/9R/8skn54wzzsjUqVNz22235X3ve1+Fk7XX8PBwZs2alU984hNJkte+9rXZsmVLPv3pT9emeH32s5/N3LlzM3ny5KpHaavbbrst//zP/5xVq1Zl5syZ2bRpUxYvXpzJkyd35Nda8fotr3jFK9JoNPLEE0+M2P7EE0/kqKOOqmgq2m3RokX52te+lnXr1uWYY46pepwixo0bl1e/+tVJklNPPTUbNmzIpz71qdx0000VT9YeGzduzLZt2/K6173uuW3NZjPr1q3LDTfckKGhoTQajQonLONlL3tZpk+fnoceeqjqUdrq6KOPft4PETNmzMiXvvSliiYq65FHHsldd92VL3/5y1WP0nYf/vCH85GPfCTvfve7kyQnnXRSHnnkkSxbtqwji5drvH7LuHHjcuqpp+buu+9+btvw8HDuvvvuWlz/UjetViuLFi3K6tWr8x//8R+ZNm1a1SNVZnh4OENDQ1WP0TZnn312Nm/enE2bNj13mzVrVi688MJs2rSpFqUrSZ588sl8//vfz9FHH131KG01Z86c5/1qmMHBwUydOrWiicpauXJlJk2alPPOO6/qUdruqaeeyiGHjKwzjUYjw8PDFU20d854jWLJkiWZP39+Zs2aldNPPz3XXXddduzYkYsuuqjq0drqySefHPFT8MMPP5xNmzbl8MMPz5QpUyqcrH0GBgayatWq3H777ZkwYUIef/zxJMnEiRMzfvz4iqdrn8svvzxz587NlClT8stf/jKrVq3KPffckzvvvLPq0dpmwoQJz7t278UvfnFe/vKXd/U1fR/60Icyb968TJ06NT/5yU9y5ZVXptFo5D3veU/Vo7XVBz/4wZx55pn5xCc+kXe+85257777cvPNN+fmm2+uerS2Gx4ezsqVKzN//vz09nb/t/l58+bl6quvzpQpUzJz5sx861vfyvLly3PxxRdXPdroqv5vlZ3q+uuvb02ZMqU1bty41umnn95av3591SO13X/+53+2kjzvNn/+/KpHa5vR8iZprVy5surR2uriiy9uTZ06tTVu3LjWEUcc0Tr77LNb//7v/171WMXV4ddJvOtd72odffTRrXHjxrVe+cpXtt71rne1HnrooarHKuJf//VfW/39/a2+vr7WCSec0Lr55purHqmIO++8s5Wk9eCDD1Y9ShG/+MUvWpdeemlrypQprUMPPbT1qle9qvVXf/VXraGhoapHG1VPq9Whv9oVAKDLuMYLAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMVrL4aGhnLVVVd19W/zHk0dc9cxcyJ3nXLXMXMit9ydx+/x2otf/OIXmThxYrZv356XvvSlVY9TTB1z1zFzInedctcxcyK33J3HGS8AgEIULwCAQhQvAIBCFK+96O3tzYIFC2rx1913V8fcdcycyF2n3HXMnMgtd+dxcf1eNJvNbN26NTNmzEij0ah6nGLqmLuOmRO565S7jpkTueXuPM54AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUsl/F68Ybb8yxxx6bQw89NGeccUbuu+++Az0XAEDXGXPx+sIXvpAlS5bkyiuvzAMPPJBTTjklb3vb27Jt27Z2zAcA0DXGXLyWL1+e97///bnoooty4okn5tOf/nR+7/d+L//4j//YjvkAALpG71ie/PTTT2fjxo25/PLLn9t2yCGH5Jxzzsk3v/nNUV8zNDSUoaGhkYv29qavr28/xi2r2WyOuK+LOuauY+ZE7jrlrmPmRG65y2o0Gi/4nJ5Wq9Xa1w/4k5/8JK985SvzjW98I7Nnz35u+1/+5V9m7dq1+a//+q/nveaqq67K0qVLR2xbsGBBFi5cuK/LAgB0vP7+/hd8zpjOeO2Pyy+/PEuWLBm56EF0xmtwcDDTp0/fpxbbLeqYu46ZE7nrlLuOmRO55e48Yyper3jFK9JoNPLEE0+M2P7EE0/kqKOOGvU1fX19B0XJ2ptGo9GxX8B2qmPuOmZO5K6TOmZO5K6bTs49povrx40bl1NPPTV33333c9uGh4dz9913j3jrEQCA5xvzW41LlizJ/PnzM2vWrJx++um57rrrsmPHjlx00UXtmA8AoGuMuXi9613vyv/8z//kYx/7WB5//PG85jWvyde//vUceeSR7ZgPAKBr7NfF9YsWLcqiRYsO9CwAAF3N32oEAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoZMzFa926dZk3b14mT56cnp6efOUrX2nDWAAA3WfMxWvHjh055ZRTcuONN7ZjHgCArtU71hfMnTs3c+fObccsAABdbczFa6yGhoYyNDQ0ctHe3vT19bV76d9Zs9kccV8Xdcxdx8yJ3HXKXcfMidxyl9VoNF7wOT2tVqu1vwv09PRk9erVOf/88/f4nKuuuipLly4dsW3BggVZuHDh/i4LANBx+vv7X/A5bS9eB/sZr8HBwUyfPn2f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ueuUu46ZE7nlLmtf1hzTW427++xnP5u5c+futXQlSV9f30FRsvam0WjUasd9Vh1z1zFzIned1DFzInfddHLu/SpejzzySO666658+ctfPtDzAAB0rf36PV4rV67MpEmTct555x3oeQAAutaYi9fw8HBWrlyZ+fPnp7d3v9+pBAConTEXr7vuuiuPPvpoLr744nbMAwDQtcZ8yurcc8/Nfv5HSACAWvO3GgEAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKGVPxajab+ehHP5pp06Zl/PjxOe644/Lxj388rVarXfMBAHSN3rE8+dprr82KFSvyuc99LjNnzsz999+fiy66KBMnTswll1zSrhkBALrCmIrXN77xjbzjHe/IeeedlyQ59thj8/nPfz733XdfW4YDAOgmYypeZ555Zm6++eYMDg5m+vTp+fa3v5177703y5cv3+NrhoaGMjQ0NHLR3t709fXt38QFNZvNEfd1UcfcdcycyF2n3HXMnMgtd1mNRuMFn9PTGsMFWsPDw7niiivyyU9+Mo1GI81mM1dffXUuv/zyPb7mqquuytKlS0dsW7BgQRYuXLivywIAdLz+/v4XfM6Yitett96aD3/4w/nbv/3bzJw5M5s2bcrixYuzfPnyzJ8/f9TXHOxnvJ49u7cvLbZb1DF3HTMnctcpdx0zJ3LLXda+rDmmtxo//OEP5yMf+Uje/e53J0lOOumkPPLII1m2bNkei1dfX99BUbL2ptFo1GrHfVYdc9cxcyJ3ndQxcyJ33XRy7jH9Oomnnnoqhxwy8iWNRiPDw8MHdCgAgG40pjNe8+bNy9VXX50pU6Zk5syZ+da3vpXly5fn4osvbtd8AABdY0zF6/rrr89HP/rRLFy4MNu2bcvkyZPzF3/xF/nYxz7WrvkAALrGmIrXhAkTct111+W6665r0zgAAN3L32oEAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAopKfVarWqHgIAoA6c8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECACjk/wOsQKcOwv13vwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAqc0lEQVR4nO3df5CcdX0H8M+xZ45UYySSQCINRGxIyAEKARpiFQWxGcxAOwNq6TSC43RyFyFmtBg6Sq4WAnaawQEawdrgjI1A1YB1CjTQkgyDaX5gmMRGTpTiL0hqR6OJenB7T/+IYE4uIRuyn2ezz+s1s7Ozzzx738/77slz73tus9dRFEURAAA03RFlDwAAUBWKFwBAEsULACCJ4gUAkETxAgBIongBACRRvAAAkiheAABJFC8AgCSKFwBAEsVrH2699dY44YQT4sgjj4yzzz471q9fX/ZITbd27dqYO3duTJo0KTo6OuKee+4pe6SmW7p0aZx55pkxZsyYmDBhQlx88cXxxBNPlD1W0y1fvjxOPfXUeO1rXxuvfe1rY9asWXHfffeVPVaqG264ITo6OmLhwoVlj9JUS5YsiY6OjmG3adOmlT1Wih/96Efx53/+5/H6178+Ro8eHaecckps3Lix7LGa6oQTTnjJ17ujoyN6e3vLHq1p6vV6fOITn4gpU6bE6NGj48QTT4xPfepT0ap/EVHxGsFdd90VixYtimuvvTYee+yxOO200+Ld73537Nixo+zRmmr37t1x2mmnxa233lr2KGnWrFkTvb29sW7duli9enU8//zzccEFF8Tu3bvLHq2pjjvuuLjhhhti06ZNsXHjxnjnO98ZF110UXzrW98qe7QUGzZsiNtuuy1OPfXUskdJMWPGjHjmmWdevD3yyCNlj9R0P/3pT2P27Nnxqle9Ku6777747//+7/j7v//7OOqoo8oerak2bNgw7Gu9evXqiIi45JJLSp6seW688cZYvnx53HLLLbFt27a48cYb49Of/nTcfPPNZY82soKXOOuss4re3t4XH9fr9WLSpEnF0qVLS5wqV0QUq1atKnuMdDt27CgiolizZk3Zo6Q76qijin/8x38se4ym+8UvflH8wR/8QbF69eri7W9/e3HVVVeVPVJTXXvttcVpp51W9hjprr766uKtb31r2WOU7qqrripOPPHEYmhoqOxRmubCCy8srrjiimHb/vRP/7S47LLLSppo/1zx+h3PPfdcbNq0Kc4///wXtx1xxBFx/vnnxze+8Y0SJyPDzp07IyJi3LhxJU+Sp16vx5133hm7d++OWbNmlT1O0/X29saFF1447N94u/vOd74TkyZNije+8Y1x2WWXxfe///2yR2q6r33tazFz5sy45JJLYsKECfGWt7wlPve5z5U9VqrnnnsuvvjFL8YVV1wRHR0dZY/TNOecc0489NBD0d/fHxERjz/+eDzyyCMxZ86ckicbWWfZA7San/zkJ1Gv1+OYY44Ztv2YY46Jb3/72yVNRYahoaFYuHBhzJ49O7q7u8sep+m2bNkSs2bNil//+tfxmte8JlatWhUnn3xy2WM11Z133hmPPfZYbNiwoexR0px99tlxxx13xEknnRTPPPNM9PX1xR/90R/F1q1bY8yYMWWP1zTf+973Yvny5bFo0aK45pprYsOGDXHllVfGqFGjYt68eWWPl+Kee+6Jn/3sZ/GBD3yg7FGa6uMf/3j8/Oc/j2nTpkWtVot6vR7XXXddXHbZZWWPNiLFC36jt7c3tm7dWonXv0REnHTSSbF58+bYuXNnfPnLX4558+bFmjVr2rZ8/eAHP4irrroqVq9eHUceeWTZ46TZ+6f+U089Nc4+++w4/vjj4+67744PfvCDJU7WXENDQzFz5sy4/vrrIyLiLW95S2zdujU++9nPVqZ4ff7zn485c+bEpEmTyh6lqe6+++7453/+51i5cmXMmDEjNm/eHAsXLoxJkya15Nda8fodRx99dNRqtdi+ffuw7du3b49jjz22pKlotgULFsTXv/71WLt2bRx33HFlj5Ni1KhR8aY3vSkiIs4444zYsGFDfOYzn4nbbrut5MmaY9OmTbFjx444/fTTX9xWr9dj7dq1ccstt8TAwEDUarUSJ8zxute9LqZOnRpPPvlk2aM01cSJE1/yQ8T06dPjK1/5SkkT5Xr66afjwQcfjK9+9atlj9J0H/vYx+LjH/94vO9974uIiFNOOSWefvrpWLp0aUsWL6/x+h2jRo2KM844Ix566KEXtw0NDcVDDz1Uide/VE1RFLFgwYJYtWpV/Md//EdMmTKl7JFKMzQ0FAMDA2WP0TTnnXdebNmyJTZv3vzibebMmXHZZZfF5s2bK1G6IiJ27doV3/3ud2PixIllj9JUs2fPfslbw/T398fxxx9f0kS5VqxYERMmTIgLL7yw7FGa7pe//GUcccTwOlOr1WJoaKikifbPFa8RLFq0KObNmxczZ86Ms846K2666abYvXt3XH755WWP1lS7du0a9lPwU089FZs3b45x48bF5MmTS5yseXp7e2PlypVx7733xpgxY+LZZ5+NiIixY8fG6NGjS56ueRYvXhxz5syJyZMnxy9+8YtYuXJlPPzww/HAAw+UPVrTjBkz5iWv3Xv1q18dr3/969v6NX0f/ehHY+7cuXH88cfHj3/847j22mujVqvF+9///rJHa6qPfOQjcc4558T1118fl156aaxfvz5uv/32uP3228seremGhoZixYoVMW/evOjsbP9v83Pnzo3rrrsuJk+eHDNmzIhvfvObsWzZsrjiiivKHm1kZf+3ylZ18803F5MnTy5GjRpVnHXWWcW6devKHqnp/vM//7OIiJfc5s2bV/ZoTTNS3ogoVqxYUfZoTXXFFVcUxx9/fDFq1Khi/PjxxXnnnVf8+7//e9ljpavC20m8973vLSZOnFiMGjWqeMMb3lC8973vLZ588smyx0rxr//6r0V3d3fR1dVVTJs2rbj99tvLHinFAw88UERE8cQTT5Q9Soqf//znxVVXXVVMnjy5OPLII4s3vvGNxV//9V8XAwMDZY82oo6iaNG3dgUAaDNe4wUAkETxAgBIongBACRRvAAAkiheAABJFC8AgCSK134MDAzEkiVL2vrdvEdSxdxVzBwhd5VyVzFzhNxytx7v47UfP//5z2Ps2LGxc+fOeO1rX1v2OGmqmLuKmSPkrlLuKmaOkFvu1uOKFwBAEsULACCJ4gUAkETx2o/Ozs6YP39+Jf66+96qmLuKmSPkrlLuKmaOkFvu1uPF9ftRr9dj27ZtMX369KjVamWPk6aKuauYOULuKuWuYuYIueVuPa54AQAkUbwAAJIoXgAASRQvAIAkihcAQBLFCwAgieIFAJBE8QIASKJ4AQAkUbwAAJIoXgAASRQvAIAkihcAQBLFCwAgieIFAJBE8QIASKJ4AQAkUbwAAJIoXgAASRQvAIAkihcAQBLFCwAgieIFAJBE8QIASKJ4AQAkUbwAAJIoXgAASRQvAIAkihcAQBLFCwAgieIFAJBE8QIASKJ4AQAkUbwAAJIoXgAASRQvAIAkihcAQJKDKl633nprnHDCCXHkkUfG2WefHevXrz/UcwEAtJ2Gi9ddd90VixYtimuvvTYee+yxOO200+Ld73537NixoxnzAQC0jYaL17Jly+JDH/pQXH755XHyySfHZz/72fi93/u9+Kd/+qdmzAcA0DY6G9n5ueeei02bNsXixYtf3HbEEUfE+eefH9/4xjdGfM7AwEAMDAwMX7SzM7q6ug5i3Fz1en3YfVVUMXcVM0fIXaXcVcwcIbfcuWq12svu01EURXGgH/DHP/5xvOENb4hHH300Zs2a9eL2v/qrv4o1a9bEf/3Xf73kOUuWLIm+vr5h2+bPnx89PT0HuiwAQMvr7u5+2X0auuJ1MBYvXhyLFi0avuhhdMWrv78/pk6dekAttl1UMXcVM0fIXaXcVcwcIbfcraeh4nX00UdHrVaL7du3D9u+ffv2OPbYY0d8TldX12FRsvanVqu17BewmaqYu4qZI+SukipmjpC7alo5d0Mvrh81alScccYZ8dBDD724bWhoKB566KFhv3oEAOClGv5V46JFi2LevHkxc+bMOOuss+Kmm26K3bt3x+WXX96M+QAA2kbDxeu9731v/O///m988pOfjGeffTbe/OY3x/333x/HHHNMM+YDAGgbB/Xi+gULFsSCBQsO9SwAAG3N32oEAEiieAEAJFG8AACSKF4AAEkULwCAJIoXAEASxQsAIIniBQCQRPECAEiieAEAJFG8AACSKF4AAEkULwCAJIoXAEASxQsAIIniBQCQRPECAEiieAEAJFG8AACSKF4AAEkULwCAJIoXAEASxQsAIIniBQCQRPECAEiieAEAJFG8AACSKF4AAEkULwCAJIoXAEASxQsAIIniBQCQRPECAEiieAEAJFG8AACSKF4AAEkULwCAJIoXAEASxQsAIIniBQCQRPECAEiieAEAJFG8AACSKF4AAEkULwCAJIoXAEASxQsAIIniBQCQRPECAEiieAEAJFG8AACSNFy81q5dG3Pnzo1JkyZFR0dH3HPPPU0YCwCg/TRcvHbv3h2nnXZa3Hrrrc2YBwCgbXU2+oQ5c+bEnDlzmjELAEBba7h4NWpgYCAGBgaGL9rZGV1dXc1e+hWr1+vD7quiirmrmDlC7irlrmLmCLnlzlWr1V52n46iKIqDXaCjoyNWrVoVF1988T73WbJkSfT19Q3bNn/+/Ojp6TnYZQEAWk53d/fL7tP04nW4X/Hq7++PqVOnHlCLbRdVzF3FzBFyVyl3FTNHyC13rgNZs+m/auzq6josStb+1Gq1Sh24L6hi7ipmjpC7SqqYOULuqmnl3N7HCwAgScNXvHbt2hVPPvnki4+feuqp2Lx5c4wbNy4mT558SIcDAGgnDRevjRs3xjve8Y4XHy9atCgiIubNmxd33HHHIRsMAKDdNFy8zj333HgFr8cHAKgsr/ECAEiieAEAJFG8AACSKF4AAEkULwCAJIoXAEASxQsAIIniBQCQRPECAEiieAEAJFG8AACSKF4AAEkULwCAJIoXAEASxQsAIIniBQCQRPECAEiieAEAJFG8AACSKF4AAEkULwCAJIoXAEASxQsAIIniBQCQRPECAEiieAEAJOksewDYl46OzNVqEdGduWAUxcjb5c7QGrnbPXNENXPv6xiHCFe8oDS533xam88FUBWKFwBAEsULACCJ4gUAkETxAgBIongBACRRvAAAkiheAABJFC8AgCSKFwBAEsULACCJ4gUAkETxAgBIongBACRRvAAAkiheAABJFC8AgCSKFwBAEsULACCJ4gUAkETxAgBIongBACRRvCAienoinnoq4le/ili3LuLMM8ueKEcVc1cxc4TcVctN61K8qLxLL41Ytiyiry/i9NMjHn884oEHIsaPL3uy5qpi7ipmjpC7arlpcUUDrr/++mLmzJnFa17zmmL8+PHFRRddVHz7299u5EMcVgYHB4stW7YUg4ODZY+SqlVyR+Tc1q0riptv/u3jjo6i+OEPi+Lqq5u/dhVzVzGz3NXK3Qpa5Tye7XDI3dAVrzVr1kRvb2+sW7cuVq9eHc8//3xccMEFsXv37mb1QmiqV70q4owzIh588LfbimLP41mzypur2aqYu4qZI+SuWm5aX2cjO99///3DHt9xxx0xYcKE2LRpU7ztbW87pINBhqOPjujsjNi+ffj27dsjpk0rZ6YMVcxdxcwRclctN62voeL1u3bu3BkREePGjdvnPgMDAzEwMDB80c7O6OrqeiVLp6jX68Puq6J1ctdKXr/5Rv4ct3fuKmaOkHu49s5d/rmzlc7jucrOXau9/LF90MVraGgoFi5cGLNnz47u7u597rd06dLo6+sbtm3+/PnR09NzsEun6+/vL3uEUpSfe9/H1aHyk59EDA5GHHPM8O3HHBPx7LNNXz62bds2wtb2zl3FzBFy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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAqgklEQVR4nO3dfZBddX0/8Pdy1yypxghKgEgTIjYQsoBKgIb4gILYDGZkOoMPpW0Ex+kv2Qgxo0XoKKQWA3aawQEawdrgTBuRqhHrFGmgJRlGU0IwTqIpK8qAD0BqR6OEupC79/dHBLOyCdmY+z0397xeM3fu3DP37vfz3j05+95zT3Z7Wq1WKwAAtN0hVQ8AAFAXihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhitce3HjjjTn22GNz6KGH5owzzsh9991X9Uhtt27dusybNy+TJ09OT09PvvKVr1Q9UtstW7Ysp512WiZMmJBJkybl/PPPz4MPPlj1WG23YsWKnHzyyXnpS1+al770pZk9e3buuOOOqscq6pprrklPT08WL15c9ShtddVVV6Wnp2fE7YQTTqh6rCJ+/OMf50//9E/z8pe/POPHj89JJ52U+++/v+qx2urYY4993te7p6cnAwMDVY/WNs1mMx/96Eczbdq0jB8/Pscdd1w+/vGPp1P/IqLiNYovfOELWbJkSa688so88MADOeWUU/K2t70t27Ztq3q0ttqxY0dOOeWU3HjjjVWPUszatWszMDCQ9evXZ82aNXnmmWdy7rnnZseOHVWP1lbHHHNMrrnmmmzcuDH3339/3vKWt+Qd73hHvvOd71Q9WhEbNmzITTfdlJNPPrnqUYqYOXNmHnvssedu9957b9Ujtd3PfvazzJkzJy960Ytyxx135Lvf/W7+7u/+LocddljVo7XVhg0bRnyt16xZkyS54IILKp6sfa699tqsWLEiN9xwQ7Zu3Zprr702n/zkJ3P99ddXPdroWjzP6aef3hoYGHjucbPZbE2ePLm1bNmyCqcqK0lr9erVVY9R3LZt21pJWmvXrq16lOIOO+yw1j/8wz9UPUbb/fKXv2z9wR/8QWvNmjWtN73pTa1LL7206pHa6sorr2ydcsopVY9R3GWXXdZ6/etfX/UYlbv00ktbxx13XGt4eLjqUdrmvPPOa1188cUjtv3xH/9x68ILL6xoor1zxuu3PP3009m4cWPOOeec57YdcsghOeecc/LNb36zwskoYfv27UmSww8/vOJJymk2m7n11luzY8eOzJ49u+px2m5gYCDnnXfeiH/j3e573/teJk+enFe96lW58MIL8+ijj1Y9Utt99atfzaxZs3LBBRdk0qRJee1rX5vPfOYzVY9V1NNPP51/+qd/ysUXX5yenp6qx2mbM888M3fffXcGBweTJN/+9rdz7733Zu7cuRVPNrreqgfoND/96U/TbDZz5JFHjth+5JFH5r//+78rmooShoeHs3jx4syZMyf9/f1Vj9N2mzdvzuzZs/OrX/0qL3nJS7J69eqceOKJVY/VVrfeemseeOCBbNiwoepRijnjjDNyyy235Pjjj89jjz2WpUuX5g1veEO2bNmSCRMmVD1e2/zgBz/IihUrsmTJklxxxRXZsGFDLrnkkowbNy7z58+verwivvKVr+TnP/953vve91Y9Slt95CMfyS9+8YuccMIJaTQaaTabufrqq3PhhRdWPdqoFC/4tYGBgWzZsqUW178kyfHHH59NmzZl+/bt+eIXv5j58+dn7dq1XVu+fvjDH+bSSy/NmjVrcuihh1Y9TjG7/9R/8skn54wzzsjUqVNz22235X3ve1+Fk7XX8PBwZs2alU984hNJkte+9rXZsmVLPv3pT9emeH32s5/N3LlzM3ny5KpHaavbbrst//zP/5xVq1Zl5syZ2bRpUxYvXpzJkyd35Nda8fotr3jFK9JoNPLEE0+M2P7EE0/kqKOOqmgq2m3RokX52te+lnXr1uWYY46pepwixo0bl1e/+tVJklNPPTUbNmzIpz71qdx0000VT9YeGzduzLZt2/K6173uuW3NZjPr1q3LDTfckKGhoTQajQonLONlL3tZpk+fnoceeqjqUdrq6KOPft4PETNmzMiXvvSliiYq65FHHsldd92VL3/5y1WP0nYf/vCH85GPfCTvfve7kyQnnXRSHnnkkSxbtqwji5drvH7LuHHjcuqpp+buu+9+btvw8HDuvvvuWlz/UjetViuLFi3K6tWr8x//8R+ZNm1a1SNVZnh4OENDQ1WP0TZnn312Nm/enE2bNj13mzVrVi688MJs2rSpFqUrSZ588sl8//vfz9FHH131KG01Z86c5/1qmMHBwUydOrWiicpauXJlJk2alPPOO6/qUdruqaeeyiGHjKwzjUYjw8PDFU20d854jWLJkiWZP39+Zs2aldNPPz3XXXddduzYkYsuuqjq0drqySefHPFT8MMPP5xNmzbl8MMPz5QpUyqcrH0GBgayatWq3H777ZkwYUIef/zxJMnEiRMzfvz4iqdrn8svvzxz587NlClT8stf/jKrVq3KPffckzvvvLPq0dpmwoQJz7t278UvfnFe/vKXd/U1fR/60Icyb968TJ06NT/5yU9y5ZVXptFo5D3veU/Vo7XVBz/4wZx55pn5xCc+kXe+85257777cvPNN+fmm2+uerS2Gx4ezsqVKzN//vz09nb/t/l58+bl6quvzpQpUzJz5sx861vfyvLly3PxxRdXPdroqv5vlZ3q+uuvb02ZMqU1bty41umnn95av3591SO13X/+53+2kjzvNn/+/KpHa5vR8iZprVy5surR2uriiy9uTZ06tTVu3LjWEUcc0Tr77LNb//7v/171WMXV4ddJvOtd72odffTRrXHjxrVe+cpXtt71rne1HnrooarHKuJf//VfW/39/a2+vr7WCSec0Lr55purHqmIO++8s5Wk9eCDD1Y9ShG/+MUvWpdeemlrypQprUMPPbT1qle9qvVXf/VXraGhoapHG1VPq9Whv9oVAKDLuMYLAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMVrL4aGhnLVVVd19W/zHk0dc9cxcyJ3nXLXMXMit9ydx+/x2otf/OIXmThxYrZv356XvvSlVY9TTB1z1zFzInedctcxcyK33J3HGS8AgEIULwCAQhQvAIBCFK+96O3tzYIFC2rx1913V8fcdcycyF2n3HXMnMgtd+dxcf1eNJvNbN26NTNmzEij0ah6nGLqmLuOmRO565S7jpkTueXuPM54AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUsl/F68Ybb8yxxx6bQw89NGeccUbuu+++Az0XAEDXGXPx+sIXvpAlS5bkyiuvzAMPPJBTTjklb3vb27Jt27Z2zAcA0DXGXLyWL1+e97///bnoooty4okn5tOf/nR+7/d+L//4j//YjvkAALpG71ie/PTTT2fjxo25/PLLn9t2yCGH5Jxzzsk3v/nNUV8zNDSUoaGhkYv29qavr28/xi2r2WyOuK+LOuauY+ZE7jrlrmPmRG65y2o0Gi/4nJ5Wq9Xa1w/4k5/8JK985SvzjW98I7Nnz35u+1/+5V9m7dq1+a//+q/nveaqq67K0qVLR2xbsGBBFi5cuK/LAgB0vP7+/hd8zpjOeO2Pyy+/PEuWLBm56EF0xmtwcDDTp0/fpxbbLeqYu46ZE7nrlLuOmRO55e48Yyper3jFK9JoNPLEE0+M2P7EE0/kqKOOGvU1fX19B0XJ2ptGo9GxX8B2qmPuOmZO5K6TOmZO5K6bTs49povrx40bl1NPPTV33333c9uGh4dz9913j3jrEQCA5xvzW41LlizJ/PnzM2vWrJx++um57rrrsmPHjlx00UXtmA8AoGuMuXi9613vyv/8z//kYx/7WB5//PG85jWvyde//vUceeSR7ZgPAKBr7NfF9YsWLcqiRYsO9CwAAF3N32oEAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoZMzFa926dZk3b14mT56cnp6efOUrX2nDWAAA3WfMxWvHjh055ZRTcuONN7ZjHgCArtU71hfMnTs3c+fObccsAABdbczFa6yGhoYyNDQ0ctHe3vT19bV76d9Zs9kccV8Xdcxdx8yJ3HXKXcfMidxyl9VoNF7wOT2tVqu1vwv09PRk9erVOf/88/f4nKuuuipLly4dsW3BggVZuHDh/i4LANBx+vv7X/A5bS9eB/sZr8HBwUyfPn2f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ueuUu46ZE7nlLmtf1hzTW427++xnP5u5c+futXQlSV9f30FRsvam0WjUasd9Vh1z1zFzIned1DFzInfddHLu/SpejzzySO666658+ctfPtDzAAB0rf36PV4rV67MpEmTct555x3oeQAAutaYi9fw8HBWrlyZ+fPnp7d3v9+pBAConTEXr7vuuiuPPvpoLr744nbMAwDQtcZ8yurcc8/Nfv5HSACAWvO3GgEAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKGVPxajab+ehHP5pp06Zl/PjxOe644/Lxj388rVarXfMBAHSN3rE8+dprr82KFSvyuc99LjNnzsz999+fiy66KBMnTswll1zSrhkBALrCmIrXN77xjbzjHe/IeeedlyQ59thj8/nPfz733XdfW4YDAOgmYypeZ555Zm6++eYMDg5m+vTp+fa3v5177703y5cv3+NrhoaGMjQ0NHLR3t709fXt38QFNZvNEfd1UcfcdcycyF2n3HXMnMgtd1mNRuMFn9PTGsMFWsPDw7niiivyyU9+Mo1GI81mM1dffXUuv/zyPb7mqquuytKlS0dsW7BgQRYuXLivywIAdLz+/v4XfM6Yitett96aD3/4w/nbv/3bzJw5M5s2bcrixYuzfPnyzJ8/f9TXHOxnvJ49u7cvLbZb1DF3HTMnctcpdx0zJ3LLXda+rDmmtxo//OEP5yMf+Uje/e53J0lOOumkPPLII1m2bNkei1dfX99BUbL2ptFo1GrHfVYdc9cxcyJ3ndQxcyJ33XRy7jH9Oomnnnoqhxwy8iWNRiPDw8MHdCgAgG40pjNe8+bNy9VXX50pU6Zk5syZ+da3vpXly5fn4osvbtd8AABdY0zF6/rrr89HP/rRLFy4MNu2bcvkyZPzF3/xF/nYxz7WrvkAALrGmIrXhAkTct111+W6665r0zgAAN3L32oEAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAopKfVarWqHgIAoA6c8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECACjk/wOsQKcOwv13vwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAl4AAAJjCAYAAADdxR/1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAqgklEQVR4nO3dfZBddX0/8Pdy1yypxghKgEgTIjYQsoBKgIb4gILYDGZkOoMPpW0Ex+kv2Qgxo0XoKKQWA3aawQEawdrgTBuRqhHrFGmgJRlGU0IwTqIpK8qAD0BqR6OEupC79/dHBLOyCdmY+z0397xeM3fu3DP37vfz3j05+95zT3Z7Wq1WKwAAtN0hVQ8AAFAXihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhihcAQCGKFwBAIYoXAEAhitce3HjjjTn22GNz6KGH5owzzsh9991X9Uhtt27dusybNy+TJ09OT09PvvKVr1Q9UtstW7Ysp512WiZMmJBJkybl/PPPz4MPPlj1WG23YsWKnHzyyXnpS1+al770pZk9e3buuOOOqscq6pprrklPT08WL15c9ShtddVVV6Wnp2fE7YQTTqh6rCJ+/OMf50//9E/z8pe/POPHj89JJ52U+++/v+qx2urYY4993te7p6cnAwMDVY/WNs1mMx/96Eczbdq0jB8/Pscdd1w+/vGPp1P/IqLiNYovfOELWbJkSa688so88MADOeWUU/K2t70t27Ztq3q0ttqxY0dOOeWU3HjjjVWPUszatWszMDCQ9evXZ82aNXnmmWdy7rnnZseOHVWP1lbHHHNMrrnmmmzcuDH3339/3vKWt+Qd73hHvvOd71Q9WhEbNmzITTfdlJNPPrnqUYqYOXNmHnvssedu9957b9Ujtd3PfvazzJkzJy960Ytyxx135Lvf/W7+7u/+LocddljVo7XVhg0bRnyt16xZkyS54IILKp6sfa699tqsWLEiN9xwQ7Zu3Zprr702n/zkJ3P99ddXPdroWjzP6aef3hoYGHjucbPZbE2ePLm1bNmyCqcqK0lr9erVVY9R3LZt21pJWmvXrq16lOIOO+yw1j/8wz9UPUbb/fKXv2z9wR/8QWvNmjWtN73pTa1LL7206pHa6sorr2ydcsopVY9R3GWXXdZ6/etfX/UYlbv00ktbxx13XGt4eLjqUdrmvPPOa1188cUjtv3xH/9x68ILL6xoor1zxuu3PP3009m4cWPOOeec57YdcsghOeecc/LNb36zwskoYfv27UmSww8/vOJJymk2m7n11luzY8eOzJ49u+px2m5gYCDnnXfeiH/j3e573/teJk+enFe96lW58MIL8+ijj1Y9Utt99atfzaxZs3LBBRdk0qRJee1rX5vPfOYzVY9V1NNPP51/+qd/ysUXX5yenp6qx2mbM888M3fffXcGBweTJN/+9rdz7733Zu7cuRVPNrreqgfoND/96U/TbDZz5JFHjth+5JFH5r//+78rmooShoeHs3jx4syZMyf9/f1Vj9N2mzdvzuzZs/OrX/0qL3nJS7J69eqceOKJVY/VVrfeemseeOCBbNiwoepRijnjjDNyyy235Pjjj89jjz2WpUuX5g1veEO2bNmSCRMmVD1e2/zgBz/IihUrsmTJklxxxRXZsGFDLrnkkowbNy7z58+verwivvKVr+TnP/953vve91Y9Slt95CMfyS9+8YuccMIJaTQaaTabufrqq3PhhRdWPdqoFC/4tYGBgWzZsqUW178kyfHHH59NmzZl+/bt+eIXv5j58+dn7dq1XVu+fvjDH+bSSy/NmjVrcuihh1Y9TjG7/9R/8skn54wzzsjUqVNz22235X3ve1+Fk7XX8PBwZs2alU984hNJkte+9rXZsmVLPv3pT9emeH32s5/N3LlzM3ny5KpHaavbbrst//zP/5xVq1Zl5syZ2bRpUxYvXpzJkyd35Nda8fotr3jFK9JoNPLEE0+M2P7EE0/kqKOOqmgq2m3RokX52te+lnXr1uWYY46pepwixo0bl1e/+tVJklNPPTUbNmzIpz71qdx0000VT9YeGzduzLZt2/K6173uuW3NZjPr1q3LDTfckKGhoTQajQonLONlL3tZpk+fnoceeqjqUdrq6KOPft4PETNmzMiXvvSliiYq65FHHsldd92VL3/5y1WP0nYf/vCH85GPfCTvfve7kyQnnXRSHnnkkSxbtqwji5drvH7LuHHjcuqpp+buu+9+btvw8HDuvvvuWlz/UjetViuLFi3K6tWr8x//8R+ZNm1a1SNVZnh4OENDQ1WP0TZnn312Nm/enE2bNj13mzVrVi688MJs2rSpFqUrSZ588sl8//vfz9FHH131KG01Z86c5/1qmMHBwUydOrWiicpauXJlJk2alPPOO6/qUdruqaeeyiGHjKwzjUYjw8PDFU20d854jWLJkiWZP39+Zs2aldNPPz3XXXddduzYkYsuuqjq0drqySefHPFT8MMPP5xNmzbl8MMPz5QpUyqcrH0GBgayatWq3H777ZkwYUIef/zxJMnEiRMzfvz4iqdrn8svvzxz587NlClT8stf/jKrVq3KPffckzvvvLPq0dpmwoQJz7t278UvfnFe/vKXd/U1fR/60Icyb968TJ06NT/5yU9y5ZVXptFo5D3veU/Vo7XVBz/4wZx55pn5xCc+kXe+85257777cvPNN+fmm2+uerS2Gx4ezsqVKzN//vz09nb/t/l58+bl6quvzpQpUzJz5sx861vfyvLly3PxxRdXPdroqv5vlZ3q+uuvb02ZMqU1bty41umnn95av3591SO13X/+53+2kjzvNn/+/KpHa5vR8iZprVy5surR2uriiy9uTZ06tTVu3LjWEUcc0Tr77LNb//7v/171WMXV4ddJvOtd72odffTRrXHjxrVe+cpXtt71rne1HnrooarHKuJf//VfW/39/a2+vr7WCSec0Lr55purHqmIO++8s5Wk9eCDD1Y9ShG/+MUvWpdeemlrypQprUMPPbT1qle9qvVXf/VXraGhoapHG1VPq9Whv9oVAKDLuMYLAKAQxQsAoBDFCwCgEMULAKAQxQsAoBDFCwCgEMVrL4aGhnLVVVd19W/zHk0dc9cxcyJ3nXLXMXMit9ydx+/x2otf/OIXmThxYrZv356XvvSlVY9TTB1z1zFzInedctcxcyK33J3HGS8AgEIULwCAQhQvAIBCFK+96O3tzYIFC2rx1913V8fcdcycyF2n3HXMnMgtd+dxcf1eNJvNbN26NTNmzEij0ah6nGLqmLuOmRO565S7jpkTueXuPM54AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUongBABSieAEAFKJ4AQAUsl/F68Ybb8yxxx6bQw89NGeccUbuu+++Az0XAEDXGXPx+sIXvpAlS5bkyiuvzAMPPJBTTjklb3vb27Jt27Z2zAcA0DXGXLyWL1+e97///bnoooty4okn5tOf/nR+7/d+L//4j//YjvkAALpG71ie/PTTT2fjxo25/PLLn9t2yCGH5Jxzzsk3v/nNUV8zNDSUoaGhkYv29qavr28/xi2r2WyOuK+LOuauY+ZE7jrlrmPmRG65y2o0Gi/4nJ5Wq9Xa1w/4k5/8JK985SvzjW98I7Nnz35u+1/+5V9m7dq1+a//+q/nveaqq67K0qVLR2xbsGBBFi5cuK/LAgB0vP7+/hd8zpjOeO2Pyy+/PEuWLBm56EF0xmtwcDDTp0/fpxbbLeqYu46ZE7nrlLuOmRO55e48Yyper3jFK9JoNPLEE0+M2P7EE0/kqKOOGvU1fX19B0XJ2ptGo9GxX8B2qmPuOmZO5K6TOmZO5K6bTs49povrx40bl1NPPTV33333c9uGh4dz9913j3jrEQCA5xvzW41LlizJ/PnzM2vWrJx++um57rrrsmPHjlx00UXtmA8AoGuMuXi9613vyv/8z//kYx/7WB5//PG85jWvyde//vUceeSR7ZgPAKBr7NfF9YsWLcqiRYsO9CwAAF3N32oEAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoZMzFa926dZk3b14mT56cnp6efOUrX2nDWAAA3WfMxWvHjh055ZRTcuONN7ZjHgCArtU71hfMnTs3c+fObccsAABdbczFa6yGhoYyNDQ0ctHe3vT19bV76d9Zs9kccV8Xdcxdx8yJ3HXKXcfMidxyl9VoNF7wOT2tVqu1vwv09PRk9erVOf/88/f4nKuuuipLly4dsW3BggVZuHDh/i4LANBx+vv7X/A5bS9eB/sZr8HBwUyfPn2f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ueuUu46ZE7nlLmtf1hzTW427++xnP5u5c+futXQlSV9f30FRsvam0WjUasd9Vh1z1zFzIned1DFzInfddHLu/SpejzzySO666658+ctfPtDzAAB0rf36PV4rV67MpEmTct555x3oeQAAutaYi9fw8HBWrlyZ+fPnp7d3v9+pBAConTEXr7vuuiuPPvpoLr744nbMAwDQtcZ8yurcc8/Nfv5HSACAWvO3GgEAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKUbwAAApRvAAAClG8AAAKGVPxajab+ehHP5pp06Zl/PjxOe644/Lxj388rVarXfMBAHSN3rE8+dprr82KFSvyuc99LjNnzsz999+fiy66KBMnTswll1zSrhkBALrCmIrXN77xjbzjHe/IeeedlyQ59thj8/nPfz733XdfW4YDAOgmYypeZ555Zm6++eYMDg5m+vTp+fa3v5177703y5cv3+NrhoaGMjQ0NHLR3t709fXt38QFNZvNEfd1UcfcdcycyF2n3HXMnMgtd1mNRuMFn9PTGsMFWsPDw7niiivyyU9+Mo1GI81mM1dffXUuv/zyPb7mqquuytKlS0dsW7BgQRYuXLivywIAdLz+/v4XfM6Yitett96aD3/4w/nbv/3bzJw5M5s2bcrixYuzfPnyzJ8/f9TXHOxnvJ49u7cvLbZb1DF3HTMnctcpdx0zJ3LLXda+rDmmtxo//OEP5yMf+Uje/e53J0lOOumkPPLII1m2bNkei1dfX99BUbL2ptFo1GrHfVYdc9cxcyJ3ndQxcyJ33XRy7jH9Oomnnnoqhxwy8iWNRiPDw8MHdCgAgG40pjNe8+bNy9VXX50pU6Zk5syZ+da3vpXly5fn4osvbtd8AABdY0zF6/rrr89HP/rRLFy4MNu2bcvkyZPzF3/xF/nYxz7WrvkAALrGmIrXhAkTct111+W6665r0zgAAN3L32oEAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAopKfVarWqHgIAoA6c8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECAChE8QIAKETxAgAoRPECACjk/wOsQKcOwv13vwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "sys = rg.rgrow.System.new_sdc(params)\n", + "state = rg.State((9,9), 'square', 'none')\n", + "sys.update_all(state)\n", + "for i in range(20):\n", + " sys.evolve(state, for_events = 10)\n", + " state\n", + " sys.plot_canvas(state, annotate_tiles = True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From b7a60967d620f2194c2aadaed8c11093f147b8d3 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Mon, 24 Jun 2024 19:23:44 +0100 Subject: [PATCH 049/117] implement mismatch_locations --- rgrow/src/models/sdc1d.rs | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index ad89648..26c2fe4 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -430,11 +430,43 @@ impl System for SDC { self.anchor_tiles.clone() } - // TODO: Array containing locations to "bad connections" fn calc_mismatch_locations(&self, state: &St) -> Array2 { - todo!() + let threshold = -0.1; // Todo: fix this + let mut mismatch_locations = Array2::::zeros((state.nrows(), state.ncols())); + + // TODO: this should use an iterator from the canvas, which we should implement. + 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) as usize; + + if t == 0 { + continue; + } + + let te = state.tile_to_e(p) as usize; + let tw = state.tile_to_w(p) as usize; + + let mm_e = ((te != 0) & (self.strand_energy_bonds[(t, te)] > threshold)) as usize; + let mm_w = ((tw != 0) & (self.strand_energy_bonds[(tw, t)] > threshold)) as usize; + + // Should we repurpose one of these to represent strand-scaffold mismatches? + // These are currently impossible, but could be added in the future. + // let ts = state.tile_to_s(p); + // let mm_s = ((ts != 0) & (self.get_energy_ns(t, ts) < threshold)) as usize; + + mismatch_locations[(i, j)] = 4 * mm_e + mm_w; + } + } + + mismatch_locations } + fn set_param( &mut self, name: &str, From 7ae81b89bf1925a52a9568148605245e7f1c53a9 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Mon, 24 Jun 2024 19:28:09 +0100 Subject: [PATCH 050/117] Fix scaffold length information. --- rgrow/src/models/sdc1d.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 26c2fe4..c24b1fa 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -521,8 +521,8 @@ impl System for SDC { fn system_info(&self) -> String { format!( - "1 dimensional SDC with scaffold of len {} and {} strands", - self.scaffold.len(), + "1 dimensional SDC with scaffold of length {} and {} strands", + self.scaffold.dim().1, self.strand_names.len(), ) } From 75459699df697547690508ed044eeb94a9e1ea87 Mon Sep 17 00:00:00 2001 From: angelcerveraroldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:34:45 +0100 Subject: [PATCH 051/117] divide by R*T --- rgrow/src/models/sdc1d.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index c24b1fa..200adba 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -38,6 +38,7 @@ type_alias!( f64 => Strength, RatePerConc, Conc ); const WEST_GLUE_INDEX: usize = 0; const BOTTOM_GLUE_INDEX: usize = 1; const EAST_GLUE_INDEX: usize = 2; +const R: f64 = 8.314; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SDC { @@ -233,12 +234,12 @@ impl SDC { // Case 1: First strands is to the west of second // strand_f strand_s self.strand_energy_bonds[(strand_f, strand_s)] = - self.glue_links[(f_east_glue, s_west_glue)]; + self.glue_links[(f_east_glue, s_west_glue)] / (R * self.temperature); // Case 2: First strands is to the east of second // strand_s strand_f self.strand_energy_bonds[(strand_s, strand_f)] = - self.glue_links[(f_west_glue, s_east_glue)]; + self.glue_links[(f_west_glue, s_east_glue)] / (R * self.temperature); } // I suppose maybe we'd have weird strands with no position domain? @@ -253,7 +254,8 @@ impl SDC { }; // Calculate the binding strength of the strand with the scaffold - self.scaffold_energy_bonds[strand_f] = self.glue_links[(f_btm_glue, b_inverse)]; + self.scaffold_energy_bonds[strand_f] = + self.glue_links[(f_btm_glue, b_inverse)] / (R * self.temperature); } } @@ -466,7 +468,6 @@ impl System for SDC { mismatch_locations } - fn set_param( &mut self, name: &str, From 887d131836f6606eeb9dee609547719c048535ba Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Tue, 25 Jun 2024 22:56:16 +0100 Subject: [PATCH 052/117] RT unit fixes; use mod for scaffold dimension 0 if canvas is larger (repeat scaffolds) --- rgrow/src/models/sdc1d.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 200adba..6790f28 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -38,7 +38,7 @@ type_alias!( f64 => Strength, RatePerConc, Conc ); const WEST_GLUE_INDEX: usize = 0; const BOTTOM_GLUE_INDEX: usize = 1; const EAST_GLUE_INDEX: usize = 2; -const R: f64 = 8.314; +const R: f64 = 1.98720425864083 / 1000.0; // in kcal/mol/K #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SDC { @@ -175,11 +175,15 @@ impl SDC { self.glue_links = &self.delta_g_matrix - (self.temperature - 37.0) * &self.entropy_matrix; } - pub fn change_temperature_to(&mut self, kelvin: f64) { - self.temperature = kelvin; + pub fn change_temperature_to(&mut self, celsius: f64) { + self.temperature = celsius; self.update_system(); } + fn rtval(&self) -> f64 { + R * (self.temperature + 273.15) + } + fn polymer_update(&self, points: &Vec, state: &mut S) { let mut points_to_update = points .iter() @@ -234,12 +238,12 @@ impl SDC { // Case 1: First strands is to the west of second // strand_f strand_s self.strand_energy_bonds[(strand_f, strand_s)] = - self.glue_links[(f_east_glue, s_west_glue)] / (R * self.temperature); + self.glue_links[(f_east_glue, s_west_glue)] / self.rtval(); // Case 2: First strands is to the east of second // strand_s strand_f self.strand_energy_bonds[(strand_s, strand_f)] = - self.glue_links[(f_west_glue, s_east_glue)] / (R * self.temperature); + self.glue_links[(f_west_glue, s_east_glue)] / self.rtval(); } // I suppose maybe we'd have weird strands with no position domain? @@ -255,7 +259,7 @@ impl SDC { // Calculate the binding strength of the strand with the scaffold self.scaffold_energy_bonds[strand_f] = - self.glue_links[(f_btm_glue, b_inverse)] / (R * self.temperature); + self.glue_links[(f_btm_glue, b_inverse)] / self.rtval(); } } @@ -324,7 +328,7 @@ impl SDC { return (false, acc, Event::None); } - let scaffold_glue = self.scaffold.get(point.0).expect("Invalid Index"); + let scaffold_glue = self.scaffold.get((point.0.0.rem_euclid(self.scaffold.dim().0), point.0.1)).expect("Invalid Index"); let empty_map = HashSet::default(); let friends = self.friends_btm.get(scaffold_glue).unwrap_or(&empty_map); From bd3f29723ab90d1aa3c247ac48b8418a08a564ac Mon Sep 17 00:00:00 2001 From: angelcerveraroldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Tue, 25 Jun 2024 23:41:43 +0100 Subject: [PATCH 053/117] account for penalties --- rgrow/src/utils.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index 9b1e4c0..d7d19b0 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -1,6 +1,9 @@ #[cfg(feature = "python")] use pyo3::prelude::*; +const PENALTY_G: f64 = 1.96; +const PENALTY_S: f64 = 0.0057; + /* * A G A A A * ---------> @@ -88,7 +91,7 @@ fn dG_dS(a: &DnaNucleotideBase, b: &DnaNucleotideBase) -> (f64, f64) { /// the sum of all neighbours a, b -- dG_(37 degrees C) (a, b) - (temperature - 37) dS(a, b) fn dna_strength(dna: impl Iterator, temperature: f64) -> f64 { let (total_dg, total_ds) = dna_dg_ds(dna); - total_dg - (temperature - 37.0) * total_ds + (total_dg + PENALTY_G) - (temperature - 37.0) * (total_ds + PENALTY_S) } fn dna_dg_ds(dna: impl Iterator) -> (f64, f64) { @@ -109,7 +112,7 @@ pub fn string_dna_dg_ds(dna_sequence: &str) -> (f64, f64) { /// ```rust /// use rgrow::utils::string_dna_delta_g; /// let seq = "cgatg"; -/// assert_eq!(string_dna_delta_g(seq, 37.0), -5.8); +/// assert_eq!(string_dna_delta_g(seq, 37.0), -5.8+1.96); /// ``` /// pub fn string_dna_delta_g(dna_sequence: &str, temperature: f64) -> f64 { @@ -231,13 +234,14 @@ mod test_utils { for (&seq, &dG) in seqs.iter().zip(dG_at_37.iter()) { let result = string_dna_delta_g(seq, 37.0); println!("{}", seq); - assert_ulps_eq!(dG, result, max_ulps = 10); + // TODO: Undo dG properly + assert_ulps_eq!(dG + 1.96, result, max_ulps = 10); } for (&seq, &dG) in seqs.iter().zip(dG_at_50.iter()) { let result = string_dna_delta_g(seq, 50.0); println!("{}", seq); - assert_ulps_eq!(dG, result, max_ulps = 10); + assert_ulps_eq!(dG + 1.96 - (50.0 - 37.0) * 0.0057, result, max_ulps = 10); } } } From 0c34a90d7d439464c82e72ebc42cc8b6a068d922 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Thu, 27 Jun 2024 16:01:07 +0100 Subject: [PATCH 054/117] Fix Python mismatch display code. --- CHANGELOG.md | 4 ++++ py-rgrow/rgrow/__init__.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 158301e..ca916f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.15.1 + +- Fix Python mismatch display code (some mismatches were not shown). + # 0.15.0 - Start order tracking at 1 (0 is a site that was never filled). diff --git a/py-rgrow/rgrow/__init__.py b/py-rgrow/rgrow/__init__.py index a2634ac..bf77fbf 100644 --- a/py-rgrow/rgrow/__init__.py +++ b/py-rgrow/rgrow/__init__.py @@ -21,7 +21,7 @@ System, State, EvolveBounds, - FFSStateRef + FFSStateRef, ) import attrs import attr @@ -56,7 +56,9 @@ def _system_name_canvas(self: System, state: State | FFSStateRef) -> np.ndarray: System.name_canvas = _system_name_canvas # type: ignore -def _system_color_canvas(self: System, state: State | np.ndarray | FFSStateRef) -> np.ndarray: +def _system_color_canvas( + self: System, state: State | np.ndarray | FFSStateRef +) -> np.ndarray: """Returns the current canvas for state, as an array of tile colors.""" if isinstance(state, (State, FFSStateRef)): @@ -170,11 +172,9 @@ def _system_plot_canvas( mml = sys.calc_mismatch_locations(state) for i, j in zip(*mml.nonzero()): d = mml[i, j] - if d > 2: - # will have already been marked by the other side - # mismatches are designated by 8*N+4*E+2*S+1*W - continue - elif d == 1: # W + # We check only 0b1 (west) and 0b10 (south), as 0b100 (east) and 0b1000 (north) + # will be covered by the tile on the other side of the mismatch. + if int(d) & 1: # W ax.add_patch( plt.Rectangle( (j - 0.75, i - 0.25), @@ -186,7 +186,7 @@ def _system_plot_canvas( linewidth=0, ) ) - elif d == 2: # S + if int(d) & 2: # S ax.add_patch( plt.Rectangle( (j - 0.25, i + 0.25), From aa6a154978d4169d67b1e19662c8691f300f1869 Mon Sep 17 00:00:00 2001 From: angelcerveraroldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 27 Jun 2024 17:28:53 +0100 Subject: [PATCH 055/117] better error message (unwrap is confusing on the python side) --- rgrow/src/models/sdc1d.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 45c587a..4b83c51 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -818,7 +818,13 @@ impl SDC { if let Some(g) = maybe_g { scaffold .index_axis_mut(ndarray::Axis(1), i) - .fill(*glue_name_map.get(g).unwrap()); + .fill( + *glue_name_map + .get(g) + .expect( + format!("ERROR: Glue {} ... Perhaps it is in the glues array, but not in any of the defined strands ?", g).as_str() + ) + ); } else { scaffold.index_axis_mut(ndarray::Axis(1), i).fill(0); } From b99ee6732116c3b39f25ef9338641d206268c805 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 28 Jun 2024 00:00:02 +0100 Subject: [PATCH 056/117] Add SDCStrand, SDCParams to rgrow.sdc for Python. --- py-rgrow/rgrow/sdc.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 py-rgrow/rgrow/sdc.py diff --git a/py-rgrow/rgrow/sdc.py b/py-rgrow/rgrow/sdc.py new file mode 100644 index 0000000..deba274 --- /dev/null +++ b/py-rgrow/rgrow/sdc.py @@ -0,0 +1,27 @@ +from collections.abc import Mapping +from dataclasses import dataclass + + +@dataclass +class SDCStrand: + concentration: float + left_glue: str | None = None + btm_glue: str | None = None + right_glue: str | None = None + name: str | None = None + color: str | None = None + + +@dataclass +class SDCParams: + k_f: float + k_n: float + k_c: float + temperature: float + glue_dg_s: ( + Mapping[str | tuple[str, str], tuple[float, float] | str] + | Mapping[str, tuple[float, float] | str] + | Mapping[tuple[str, str], tuple[float, float] | str] + ) + scaffold: list[str | None] | list[list[str | None]] + strands: list[SDCStrand] From 5119aacbdd658fdf019fce6b1095a23b97193f8a Mon Sep 17 00:00:00 2001 From: angelcerveraroldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Sun, 30 Jun 2024 20:43:02 +0100 Subject: [PATCH 057/117] Fixed early panic -- This exception made implementing simulations very annoying at times --- rgrow/src/models/sdc1d.rs | 76 ++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 4b83c51..0046a36 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -811,34 +811,6 @@ impl SDC { strand_concentration[id + 1] = concentration; } - let scaffold = match params.scaffold { - SingleOrMultiScaffold::Single(s) => { - let mut scaffold = Array2::::zeros((64, s.len())); - for (i, maybe_g) in s.iter().enumerate() { - if let Some(g) = maybe_g { - scaffold - .index_axis_mut(ndarray::Axis(1), i) - .fill( - *glue_name_map - .get(g) - .expect( - format!("ERROR: Glue {} ... Perhaps it is in the glues array, but not in any of the defined strands ?", g).as_str() - ) - ); - } else { - scaffold.index_axis_mut(ndarray::Axis(1), i).fill(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(); - } - // Delta G at 37 degrees C let mut glue_delta_g = Array2::::zeros((gluenum, gluenum)); let mut glue_s = Array2::::zeros((gluenum, gluenum)); @@ -859,14 +831,22 @@ impl SDC { } }; - // FIXME: fails if glue not found - let i = *glue_name_map - .get(&i) - .expect(format!("Glue {} not found", i).as_str()); - - let j = *glue_name_map - .get(&j) - .expect(format!("Glue {} not found", j).as_str()); + // If the user defines the DNA sequence of a glue, but it is never used in any of the + // strands, then we can ignore it. Also, if the user does use the glue A, but not the + // glue B, then we can safely ignore the binding strength of A and B, thus + // + // (None, None) and (Some, None) are both fine to skip + // + // MAYBE it could be better to iterate tglue_dg_s twice, the first time, we just make + // sure that all strings are inside the glue_name_map, and if they arent, we can add + // them. The second time around we know that the glues will always be found in the map + // + // However, since you cant mutate the strands glues, it shuold be fine to just ignore + // the glues that do not exist + 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; glue_delta_g[[j, i]] = gs.0; @@ -874,6 +854,30 @@ impl SDC { glue_s[[j, i]] = gs.1; } + let scaffold = match params.scaffold { + SingleOrMultiScaffold::Single(s) => { + let mut scaffold = Array2::::zeros((64, s.len())); + for (i, maybe_g) in s.iter().enumerate() { + if let Some(g) = maybe_g { + let x = *glue_name_map + .get(g) + .expect(format!("ERROR: Glue {} in scaffold not found!", g).as_str()); + + scaffold.index_axis_mut(ndarray::Axis(1), i).fill(x); + } else { + scaffold.index_axis_mut(ndarray::Axis(1), i).fill(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(); + } + SDC::new( // TODO: anchor tiles vec![], From 7d238987858adb5be6207a3c3a891d16c6663203 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Mon, 8 Jul 2024 13:02:00 +0100 Subject: [PATCH 058/117] Calculate loop penalty --- rgrow/src/utils.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index d7d19b0..472e530 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -1,8 +1,14 @@ +use std::f64; + #[cfg(feature = "python")] use pyo3::prelude::*; const PENALTY_G: f64 = 1.96; const PENALTY_S: f64 = 0.0057; +// Gas constant in kcal / mol +// +// (same unit as delta G needed) +const R_KCAL_PER_MOL: f64 = 1.986 * 10e-6; /* * A G A A A @@ -123,6 +129,59 @@ pub fn string_dna_delta_g(dna_sequence: &str, temperature: f64) -> f64 { ) } +pub enum LoopKind { + Bulge, + Internal, + HairPin, +} + +/// # Panics +/// +/// If length is not greater or equal to 30 +fn internal_loop_penality(length: usize) -> f64 { + if length < 30 { + panic!("No loops of length under 30 are allowed yet") + } + + let g_diff_30 = 6.6; + g_diff_30 + R_KCAL_PER_MOL * (length as f64 / 30.0) * 2.44 * 310.15 +} + +/// # Panics +/// +/// If length is not greater or equal to 30 +fn hairpin_loop_penality(length: usize) -> f64 { + if length < 30 { + panic!("No loops of length under 30 are allowed yet") + } + + let g_diff_30 = 6.3; + g_diff_30 + R_KCAL_PER_MOL * (length as f64 / 30.0) * 2.44 * 310.15 +} + +/// # Panics +/// +/// If length is not greater or equal to 30 +fn bulge_loop_penality(length: usize) -> f64 { + if length < 30 { + panic!("No loops of length under 30 are allowed yet") + } + + let g_diff_30 = 5.9; + g_diff_30 + R_KCAL_PER_MOL * (length as f64 / 30.0) * 2.44 * 310.15 +} + +/// # Panics +/// +/// If length is not greater or equal to 30 +pub fn loop_penalty(length: usize, kind: LoopKind) -> f64 { + match kind { + LoopKind::Bulge => bulge_loop_penality(length), + LoopKind::HairPin => hairpin_loop_penality(length), + LoopKind::Internal => internal_loop_penality(length), + } +} + #[cfg(test)] mod test_utils { From c1f35257f709bc3d28bd3b84fdec3a728122e87c Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Mon, 8 Jul 2024 13:09:07 +0100 Subject: [PATCH 059/117] Fix R --- rgrow/src/utils.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index 472e530..fce2020 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -5,10 +5,11 @@ use pyo3::prelude::*; const PENALTY_G: f64 = 1.96; const PENALTY_S: f64 = 0.0057; -// Gas constant in kcal / mol + +// Gas constant in kcal / mol / K // // (same unit as delta G needed) -const R_KCAL_PER_MOL: f64 = 1.986 * 10e-6; +const R: f64 = 1.98720425864083 / 1000.0; /* * A G A A A @@ -144,7 +145,7 @@ fn internal_loop_penality(length: usize) -> f64 { } let g_diff_30 = 6.6; - g_diff_30 + R_KCAL_PER_MOL * (length as f64 / 30.0) * 2.44 * 310.15 + g_diff_30 + R * (length as f64 / 30.0) * 2.44 * 310.15 } /// # Panics @@ -156,7 +157,7 @@ fn hairpin_loop_penality(length: usize) -> f64 { } let g_diff_30 = 6.3; - g_diff_30 + R_KCAL_PER_MOL * (length as f64 / 30.0) * 2.44 * 310.15 + g_diff_30 + R * (length as f64 / 30.0) * 2.44 * 310.15 } /// # Panics @@ -168,7 +169,7 @@ fn bulge_loop_penality(length: usize) -> f64 { } let g_diff_30 = 5.9; - g_diff_30 + R_KCAL_PER_MOL * (length as f64 / 30.0) * 2.44 * 310.15 + g_diff_30 + R * (length as f64 / 30.0) * 2.44 * 310.15 } /// # Panics From 1c20c7786d46c5e2094a8e601df93d3e8f71b3ad Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Tue, 9 Jul 2024 12:22:17 +0100 Subject: [PATCH 060/117] cleanup loop cost helper function --- rgrow/src/utils.rs | 95 +++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index fce2020..3c948b8 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -11,6 +11,29 @@ const PENALTY_S: f64 = 0.0057; // (same unit as delta G needed) const R: f64 = 1.98720425864083 / 1000.0; +pub enum LoopKind { + Internal = 0, + Bulge = 1, + HairPin = 2, +} + +const LOOP_TABLE: [[f64; 15]; 3] = [ + // Internal Loops + [ + 3.2, 3.6, 4.0, 4.4, 4.6, 4.8, 4.9, 4.9, 5.2, 5.4, 5.6, 5.8, 5.9, 6.3, 6.6, + ], + // Bulge Loops + [ + 3.1, 3.2, 3.3, 3.5, 3.7, 3.9, 4.1, 4.3, 4.5, 4.8, 5.0, 5.2, 5.3, 5.6, 5.9, + ], + // Hairpin Loops + [ + 3.5, 3.5, 3.3, 4.0, 4.2, 4.3, 4.5, 4.6, 5.0, 5.1, 5.3, 5.5, 5.7, 6.1, 6.3, + ], +]; + +const LENGTHS: [usize; 15] = [3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20, 25, 30]; + /* * A G A A A * ---------> @@ -130,64 +153,35 @@ pub fn string_dna_delta_g(dna_sequence: &str, temperature: f64) -> f64 { ) } -pub enum LoopKind { - Bulge, - Internal, - HairPin, -} - -/// # Panics -/// -/// If length is not greater or equal to 30 -fn internal_loop_penality(length: usize) -> f64 { - if length < 30 { - panic!("No loops of length under 30 are allowed yet") - } - - let g_diff_30 = 6.6; - g_diff_30 + R * (length as f64 / 30.0) * 2.44 * 310.15 -} - -/// # Panics -/// -/// If length is not greater or equal to 30 -fn hairpin_loop_penality(length: usize) -> f64 { - if length < 30 { - panic!("No loops of length under 30 are allowed yet") - } - - let g_diff_30 = 6.3; - g_diff_30 + R * (length as f64 / 30.0) * 2.44 * 310.15 -} - -/// # Panics -/// -/// If length is not greater or equal to 30 -fn bulge_loop_penality(length: usize) -> f64 { - if length < 30 { - panic!("No loops of length under 30 are allowed yet") - } +fn _loop_penalty(length: usize, kind: LoopKind) -> f64 { + let (g_diff, len) = LOOP_TABLE[kind as usize] + .iter() + .zip(LENGTHS) + .rev() + .find(|(_, len)| len < &length) + .expect("Please enter a valid length"); - let g_diff_30 = 5.9; - g_diff_30 + R * (length as f64 / 30.0) * 2.44 * 310.15 + g_diff + R * (length as f64 / (len as f64)).ln() * 2.44 * 310.15 } -/// # Panics -/// -/// If length is not greater or equal to 30 -pub fn loop_penalty(length: usize, kind: LoopKind) -> f64 { +#[cfg_attr(feature = "python", pyfunction)] +pub fn loop_penalty(length: usize, kind: &str) -> f64 { match kind { - LoopKind::Bulge => bulge_loop_penality(length), - LoopKind::HairPin => hairpin_loop_penality(length), - LoopKind::Internal => internal_loop_penality(length), + "bulge" => _loop_penalty(length, LoopKind::Bulge), + "internal" => _loop_penalty(length, LoopKind::Internal), + "hairpin" => _loop_penalty(length, LoopKind::HairPin), + _ => panic!(), } } - #[cfg(test)] mod test_utils { + use crate::utils::LOOP_TABLE; + + use super::_loop_penalty; use super::string_dna_delta_g; use super::two_window_fold; + use approx::assert_relative_eq; use approx::assert_ulps_eq; #[test] @@ -304,4 +298,11 @@ mod test_utils { assert_ulps_eq!(dG + 1.96 - (50.0 - 37.0) * 0.0057, result, max_ulps = 10); } } + + #[test] + fn test_loops() { + let val29 = _loop_penalty(29, super::LoopKind::Internal); + assert!(val29 > LOOP_TABLE[0][13]); + assert!(val29 < LOOP_TABLE[0][14]); + } } From cf15399d561c3f3c83c03249c36d5cab33c87747 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 11 Jul 2024 11:10:18 +0100 Subject: [PATCH 061/117] expose function to python lib --- py-rgrow/src/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/py-rgrow/src/lib.rs b/py-rgrow/src/lib.rs index 76f1438..65d392e 100644 --- a/py-rgrow/src/lib.rs +++ b/py-rgrow/src/lib.rs @@ -24,9 +24,6 @@ mod rgrow { #[pymodule_export] use rgrow::system::EvolveOutcome; - #[pymodule_export] - use rgrow::utils::string_dna_dg_ds; - #[pymodule_export] use rgrow::models::atam::ATAM; #[pymodule_export] @@ -39,4 +36,8 @@ mod rgrow { use rgrow::system::DimerInfo; #[pymodule_export] use rgrow::system::NeededUpdate; + #[pymodule_export] + use rgrow::utils::loop_penalty; + #[pymodule_export] + use rgrow::utils::string_dna_dg_ds; } From 661445e7b34d2902694e9d756ceb35d0bdbb40bf Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Fri, 12 Jul 2024 19:00:31 +0100 Subject: [PATCH 062/117] boltzman distribution --- rgrow/src/models/sdc1d.rs | 288 ++++++++++++++++++++++++++++++++++++++ rgrow/src/utils.rs | 8 ++ 2 files changed, 296 insertions(+) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 46cc87e..265855a 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -38,10 +38,13 @@ use pyo3::prelude::*; type_alias!( f64 => Strength, RatePerConc, Conc ); +// This surely needs unit adjustment, it seems wayyyy too small +const BOLTZMAN_CONSTANT: f64 = 1.0; // 1.3806503e-23; 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; // in kcal/mol/K +const U0: f64 = 1e-9; #[cfg_attr(feature = "python", pyclass)] #[derive(Debug, Clone, Serialize, Deserialize)] @@ -184,6 +187,7 @@ impl SDC { self.update_system(); } + #[inline(always)] fn rtval(&self) -> f64 { R * (self.temperature + 273.15) } @@ -380,6 +384,98 @@ impl SDC { + self.strand_energy_bonds[(strand as usize, e)] + self.strand_energy_bonds[(w, strand as usize)] } + + /// Given an SDC system, and some scaffold attachments + /// + /// 0 := nothing attached to the saccffold + fn g_system(&self, attachments: Vec) -> f64 { + let mut sumg = 0.0; + + for (id, strand) in attachments.iter().enumerate() { + if strand == &0 { + continue; + } + + // Add the energy of the strand and the scaffold + sumg += self.scaffold_energy_bonds[*strand as usize]; + if let Some(s) = attachments.get(id + 1) { + // Also add the energy between the strand and the one to its right + sumg += self.strand_energy_bonds[(*strand as usize, *s as usize)] + }; + + // Take into account the penalty + let penalty = self.rtval() * (self.strand_concentration[*strand as usize] / U0).ln(); + sumg -= penalty; + } + sumg + } + + // This is quite inefficient -- and clones a lot. If the scaffold were to be + // longer than 10, this would not work + pub fn system_states(&self, scaffold: Vec) -> Vec> { + // Calculate the number of combinations ( this will i think make it a little more optimized + // since we wont need realloc ) + let mut acc = 1; + for b in &scaffold { + if let Some(x) = self.friends_btm.get(&b) { + // number of possible times + none + acc *= x.len() + 1; + } + } + + let mut possible_scaffolds: Vec> = Vec::with_capacity(acc); + possible_scaffolds.push(Vec::default()); + + for b in &scaffold { + let def = HashSet::default(); + let friends = self.friends_btm.get(b).unwrap_or(&def); + + possible_scaffolds = possible_scaffolds + .iter() + .flat_map(|scaffold_attachments| { + let mut new_combinations: Vec> = Vec::new(); + + // Each one of the friends will make one possible state + for f in friends { + let mut comb = scaffold_attachments.clone(); + comb.push(*f); + new_combinations.push(comb); + } + + // Also if nothing attached + let mut comb = scaffold_attachments.clone(); + comb.push(0); + new_combinations.push(comb); + new_combinations + }) + .collect(); + } + + possible_scaffolds + } + + #[inline(always)] + fn beta(&self) -> f64 { + 1.0 / (self.temperature * BOLTZMAN_CONSTANT) + } + + pub fn boltzman_function(&self, attachments: Vec) -> f64 { + let g_a = self.g_system(attachments); + (-self.beta() * g_a).exp() + } + + pub fn sum_systems(&self, scaffold: Vec) -> f64 { + self.system_states(scaffold) + .into_iter() + .map(|attachments| self.boltzman_function(attachments)) + .sum() + } + + pub fn probabilty(&self, scaffold: Vec, system: Vec) -> f64 { + let sum_z = self.sum_systems(scaffold); + let this_system = self.boltzman_function(system); + this_system / sum_z + } } impl System for SDC { @@ -910,7 +1006,9 @@ impl SDC { #[cfg(test)] mod test_sdc_model { + use crate::assert_all; use ndarray::array; + use num_traits::PrimInt; use super::*; #[test] @@ -985,4 +1083,194 @@ mod test_sdc_model { assert_eq!(acc, expected); } + + #[test] + fn combinations() { + let mut sdc = SDC { + anchor_tiles: Vec::new(), + strand_names: Vec::new(), + glue_names: Vec::new(), + scaffold: Array2::::zeros((5, 5)), + strand_concentration: Array1::::zeros(5), + glues: array![ + [0, 0, 0], + [1, 3, 12], + [11, 2, 12], + [29, 3, 45], + [8, 4, 2], + [11, 1, 30], + [4, 4, 1], + ], + colors: Vec::new(), + kf: 0.0, + friends_btm: HashMap::new(), + entropy_matrix: array![[1., 2., 3.], [5., 1., 8.], [5., -2., 12.]], + delta_g_matrix: array![[4., 1., -8.], [6., 1., 14.], [12., 21., -13.,]], + temperature: 50.0, + strand_energy_bonds: Array2::::zeros((5, 5)), + scaffold_energy_bonds: Array1::::zeros(5), + glue_links: Array2::::zeros((5, 5)), + }; + // We need to fill the friends map + sdc.update_system(); + + // 0 <---> Nothing + // + // 1 <---> 2 + // 3 <---> 4 + // 5 <---> 6 + let x = sdc.system_states(vec![0, 0, 1, 1, 2, 4, 0, 0]); + + assert_all!( + x.contains(&vec![0, 0, 2, 2, 5, 1, 0, 0]), + x.contains(&vec![0, 0, 2, 2, 5, 1, 0, 0]), + x.contains(&vec![0, 0, 0, 2, 5, 1, 0, 0]), + x.contains(&vec![0, 0, 2, 0, 5, 1, 0, 0]), + x.contains(&vec![0, 0, 2, 2, 0, 1, 0, 0]), + x.contains(&vec![0, 0, 2, 2, 5, 0, 0, 0]), + x.contains(&vec![0, 0, 0, 0, 5, 1, 0, 0]), + x.contains(&vec![0, 0, 0, 0, 5, 1, 0, 0]), + x.contains(&vec![0, 0, 0, 2, 0, 1, 0, 0]), + x.contains(&vec![0, 0, 0, 2, 5, 0, 0, 0]) + ); + + // Note: One is added to each since the 0 state is not in friends + // + // vvvvvv friends of 1 (squared since 1 shows up twice) + // vvvvvv vvvvvv friends of 2 + // vvvvvv vvvvvv vvvvvv friends of 4 + assert_eq!(x.len(), (1 + 1).pow(2) * (1 + 1) * (2 + 1)); + } + + #[test] + fn probablities() { + let mut strands = Vec::::new(); + + // Anchor tile + 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{}0", base); + 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{}1", base); + 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 = 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, + glue_dg_s, + k_f: 1e6, + k_n: 1e5, + k_c: 1e4, + }; + + let mut sdc = SDC::from_params(sdc_params); + sdc.update_system(); + + let scaffold = vec![0, 0, 2, 8, 16, 18, 6, 0, 0]; + let systems = sdc.system_states(scaffold.clone()); + + // A and E have only one strand possible (or empty), and BCD have 2 or empty + assert_eq!(systems.len(), 2.pow(2) * 3.pow(3)); + + let mut probs = systems + .iter() + // TODO: It wuold be better if this vvvvvvvvvvvvvvv were a pointer + .map(|s| (s.clone(), sdc.probabilty(scaffold.clone(), s.clone()))) + .collect::>(); + + probs.sort_by(|(_, p1), (_, p2)| { + p2.partial_cmp(p1) + .expect(format!("{} -- {}", p1, p2).as_str()) + }); + + // The perfect combination would be all 0's + // Lets check if that is the case + // probs.iter().for_each(|(s, p)| { + // println!("Probability of {} for {:?}", p, s); + // }); + + assert_eq!(probs[0].0, vec![0, 0, 1, 3, 5, 7, 2, 0, 0]); + } } diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index 3c948b8..1dce730 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -3,6 +3,14 @@ use std::f64; #[cfg(feature = "python")] use pyo3::prelude::*; +// For testing +#[macro_export] +macro_rules! assert_all { + ($($e:expr),*) => { + $(assert!($e);)* + }; +} + const PENALTY_G: f64 = 1.96; const PENALTY_S: f64 = 0.0057; From b893da2e0c9bb3b62750aea79d9c661acc7ac5b5 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Fri, 12 Jul 2024 19:03:46 +0100 Subject: [PATCH 063/117] temperature as kelvin --- rgrow/src/models/sdc1d.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 265855a..af1485e 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -405,6 +405,7 @@ impl SDC { // Take into account the penalty let penalty = self.rtval() * (self.strand_concentration[*strand as usize] / U0).ln(); + sumg -= penalty; } sumg @@ -456,7 +457,7 @@ impl SDC { #[inline(always)] fn beta(&self) -> f64 { - 1.0 / (self.temperature * BOLTZMAN_CONSTANT) + 1.0 / ((self.temperature + 273.15) * BOLTZMAN_CONSTANT) } pub fn boltzman_function(&self, attachments: Vec) -> f64 { From 0f5a39f9cb50ad0558e9ad24f0672633cbfc3f65 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Sat, 13 Jul 2024 11:10:21 +0100 Subject: [PATCH 064/117] Better interface Dont need to put scaffold as input (this assumes that sdc system has only one scaffold, which is true as of right now) --- rgrow/src/models/sdc1d.rs | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index af1485e..60015ee 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -385,6 +385,10 @@ impl SDC { + self.strand_energy_bonds[(w, strand as usize)] } + fn scaffold(&self) -> Vec { + self.scaffold.row(0).to_vec() + } + /// Given an SDC system, and some scaffold attachments /// /// 0 := nothing attached to the saccffold @@ -413,7 +417,9 @@ impl SDC { // This is quite inefficient -- and clones a lot. If the scaffold were to be // longer than 10, this would not work - pub fn system_states(&self, scaffold: Vec) -> Vec> { + pub fn system_states(&self) -> Vec> { + let scaffold = self.scaffold(); + // Calculate the number of combinations ( this will i think make it a little more optimized // since we wont need realloc ) let mut acc = 1; @@ -465,15 +471,15 @@ impl SDC { (-self.beta() * g_a).exp() } - pub fn sum_systems(&self, scaffold: Vec) -> f64 { - self.system_states(scaffold) + pub fn sum_systems(&self) -> f64 { + self.system_states() .into_iter() .map(|attachments| self.boltzman_function(attachments)) .sum() } - pub fn probabilty(&self, scaffold: Vec, system: Vec) -> f64 { - let sum_z = self.sum_systems(scaffold); + pub fn probabilty(&self, system: Vec) -> f64 { + let sum_z = self.sum_systems(); let this_system = self.boltzman_function(system); this_system / sum_z } @@ -1087,11 +1093,17 @@ mod test_sdc_model { #[test] fn combinations() { + let mut scaffold = Array2::::zeros((1, 8)); + scaffold[(0, 2)] = 1; + scaffold[(0, 3)] = 1; + scaffold[(0, 4)] = 2; + scaffold[(0, 5)] = 4; + let mut sdc = SDC { anchor_tiles: Vec::new(), strand_names: Vec::new(), glue_names: Vec::new(), - scaffold: Array2::::zeros((5, 5)), + scaffold, strand_concentration: Array1::::zeros(5), glues: array![ [0, 0, 0], @@ -1120,7 +1132,9 @@ mod test_sdc_model { // 1 <---> 2 // 3 <---> 4 // 5 <---> 6 - let x = sdc.system_states(vec![0, 0, 1, 1, 2, 4, 0, 0]); + + assert_eq!(sdc.scaffold(), vec![0, 0, 1, 1, 2, 4, 0, 0]); + let x = sdc.system_states(); assert_all!( x.contains(&vec![0, 0, 2, 2, 5, 1, 0, 0]), @@ -1250,15 +1264,15 @@ mod test_sdc_model { sdc.update_system(); let scaffold = vec![0, 0, 2, 8, 16, 18, 6, 0, 0]; - let systems = sdc.system_states(scaffold.clone()); + assert_eq!(sdc.scaffold(), scaffold); + let systems = sdc.system_states(); // A and E have only one strand possible (or empty), and BCD have 2 or empty assert_eq!(systems.len(), 2.pow(2) * 3.pow(3)); let mut probs = systems .iter() - // TODO: It wuold be better if this vvvvvvvvvvvvvvv were a pointer - .map(|s| (s.clone(), sdc.probabilty(scaffold.clone(), s.clone()))) + .map(|s| (s.clone(), sdc.probabilty(s.clone()))) .collect::>(); probs.sort_by(|(_, p1), (_, p2)| { From 021359ad82c9cd68fee5addcaa965082021afcc1 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:16:20 +0100 Subject: [PATCH 065/117] change 1/beta to RT --- rgrow/src/models/sdc1d.rs | 41 +++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 60015ee..7050414 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -17,7 +17,7 @@ macro_rules! type_alias { use std::{ collections::{HashMap, HashSet}, - usize, + f64, usize, }; use crate::{ @@ -38,13 +38,11 @@ use pyo3::prelude::*; type_alias!( f64 => Strength, RatePerConc, Conc ); -// This surely needs unit adjustment, it seems wayyyy too small -const BOLTZMAN_CONSTANT: f64 = 1.0; // 1.3806503e-23; 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; // in kcal/mol/K -const U0: f64 = 1e-9; +const U0: f64 = 1.0; #[cfg_attr(feature = "python", pyclass)] #[derive(Debug, Clone, Serialize, Deserialize)] @@ -392,7 +390,7 @@ impl SDC { /// Given an SDC system, and some scaffold attachments /// /// 0 := nothing attached to the saccffold - fn g_system(&self, attachments: Vec) -> f64 { + fn g_system(&self, attachments: &Vec) -> f64 { let mut sumg = 0.0; for (id, strand) in attachments.iter().enumerate() { @@ -461,24 +459,19 @@ impl SDC { possible_scaffolds } - #[inline(always)] - fn beta(&self) -> f64 { - 1.0 / ((self.temperature + 273.15) * BOLTZMAN_CONSTANT) - } - - pub fn boltzman_function(&self, attachments: Vec) -> f64 { + pub fn boltzman_function(&self, attachments: &Vec) -> f64 { let g_a = self.g_system(attachments); - (-self.beta() * g_a).exp() + (-self.rtval() * g_a).exp() } pub fn sum_systems(&self) -> f64 { self.system_states() - .into_iter() + .iter() .map(|attachments| self.boltzman_function(attachments)) .sum() } - pub fn probabilty(&self, system: Vec) -> f64 { + pub fn probabilty(&self, system: &Vec) -> f64 { let sum_z = self.sum_systems(); let this_system = self.boltzman_function(system); this_system / sum_z @@ -1009,6 +1002,24 @@ impl SDC { fn py_new(params: SDCParams) -> Self { SDC::from_params(params) } + + fn distribution(&self) -> Vec { + // Inneficient to run the same function twice, fix this + let mut probability = self + .system_states() + .iter() + .map(|sys| self.probabilty(sys)) + .collect::>(); + + probability.sort_unstable_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)); + probability + } + + /// Change temperature of the system (degrees C) and update system with that new temperature + fn set_tmp_c(&mut self, tmp: f64) { + self.temperature = tmp; + self.update_system(); + } } #[cfg(test)] @@ -1272,7 +1283,7 @@ mod test_sdc_model { let mut probs = systems .iter() - .map(|s| (s.clone(), sdc.probabilty(s.clone()))) + .map(|s| (s.clone(), sdc.probabilty(s))) .collect::>(); probs.sort_by(|(_, p1), (_, p2)| { From e2c5346689c630992d9c073350b70c3d136fbed4 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:18:46 +0100 Subject: [PATCH 066/117] Give python acess to partition function --- rgrow/src/models/sdc1d.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 7050414..08781c5 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -1003,6 +1003,10 @@ impl SDC { SDC::from_params(params) } + fn partition(&self) -> f64 { + self.sum_systems() + } + fn distribution(&self) -> Vec { // Inneficient to run the same function twice, fix this let mut probability = self From a61a2bb8ccec532db79b4c3f7ffd00109562af9c Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Mon, 22 Jul 2024 01:32:57 -0400 Subject: [PATCH 067/117] Hopefully put a tile_counts in State --- rgrow/src/models/mod.rs | 2 +- rgrow/src/python.rs | 13 ++++- rgrow/src/state.rs | 123 ++++++++++++++++++++++++++++++++++------ rgrow/src/tileset.rs | 2 +- 4 files changed, 119 insertions(+), 21 deletions(-) diff --git a/rgrow/src/models/mod.rs b/rgrow/src/models/mod.rs index c45eca0..045c040 100644 --- a/rgrow/src/models/mod.rs +++ b/rgrow/src/models/mod.rs @@ -4,5 +4,5 @@ pub mod ktam; pub mod oldktam; pub mod sdc1d; - + pub(self) mod fission_base; diff --git a/rgrow/src/python.rs b/rgrow/src/python.rs index 11621e2..367cce6 100644 --- a/rgrow/src/python.rs +++ b/rgrow/src/python.rs @@ -5,10 +5,10 @@ use std::time::Duration; use crate::base::{NumEvents, NumTiles, RgrowError, RustAny, Tile}; use crate::canvas::{Canvas, PointSafeHere}; use crate::ffs::{FFSRunConfig, FFSRunResult, FFSStateRef}; -use crate::models::sdc1d::{SDCParams, SDC}; use crate::models::atam::ATAM; use crate::models::ktam::KTAM; use crate::models::oldktam::OldKTAM; +use crate::models::sdc1d::{SDCParams, SDC}; use crate::ratestore::RateStore; use crate::state::{StateEnum, StateStatus, TrackerData}; use crate::system::{ @@ -30,11 +30,18 @@ pub struct PyState(pub(crate) StateEnum); #[pymethods] impl PyState { #[new] - pub fn empty(shape: (usize, usize), kind: &str, tracking: &str) -> PyResult { + #[pyo3(signature = (shape, kind="Square", tracking="None", n_tile_types=None))] + pub fn empty( + shape: (usize, usize), + kind: &str, + tracking: &str, + n_tile_types: Option, + ) -> PyResult { Ok(PyState(StateEnum::empty( shape, kind.try_into()?, tracking.try_into()?, + n_tile_types.unwrap_or(1), )?)) } @@ -533,4 +540,4 @@ macro_rules! create_py_system { create_py_system!(KTAM); create_py_system!(ATAM); create_py_system!(OldKTAM); -create_py_system!(SDC); \ No newline at end of file +create_py_system!(SDC); diff --git a/rgrow/src/state.rs b/rgrow/src/state.rs index c383244..fc4de27 100644 --- a/rgrow/src/state.rs +++ b/rgrow/src/state.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Debug; #[enum_dispatch] -pub trait State: RateStore + Canvas + StateStatus + Sync + Send + TrackerData { +pub trait State: RateStore + Canvas + StateStatus + Sync + Send + TrackerData + TileCounts { fn panicinfo(&self) -> String; } @@ -97,7 +97,15 @@ impl ClonableState for QuadTreeState { } } -#[enum_dispatch(State, StateStatus, Canvas, RateStore, TrackerData, CloneAsStateEnum)] +#[enum_dispatch( + State, + StateStatus, + Canvas, + RateStore, + TrackerData, + CloneAsStateEnum, + TileCounts +)] #[derive(Debug, Clone, Serialize, Deserialize)] pub enum StateEnum { SquareCanvasNullTracker(QuadTreeState), @@ -119,48 +127,95 @@ impl StateEnum { shape: (usize, usize), kind: CanvasType, tracking: TrackingType, + n_tile_types: usize, ) -> Result { Ok(match kind { CanvasType::Square => match tracking { TrackingType::None => { - QuadTreeState::::empty(shape)?.into() + QuadTreeState::::empty_with_types( + shape, + n_tile_types, + )? + .into() } TrackingType::Order => { - QuadTreeState::::empty(shape)?.into() + QuadTreeState::::empty_with_types( + shape, + n_tile_types, + )? + .into() } TrackingType::LastAttachTime => { - QuadTreeState::::empty(shape)?.into() + QuadTreeState::::empty_with_types( + shape, + n_tile_types, + )? + .into() } TrackingType::PrintEvent => { - QuadTreeState::::empty(shape)?.into() + QuadTreeState::::empty_with_types( + shape, + n_tile_types, + )? + .into() } }, CanvasType::Periodic => match tracking { TrackingType::None => { - QuadTreeState::::empty(shape)?.into() + QuadTreeState::::empty_with_types( + shape, + n_tile_types, + )? + .into() } TrackingType::Order => { - QuadTreeState::::empty(shape)?.into() + QuadTreeState::::empty_with_types( + shape, + n_tile_types, + )? + .into() } TrackingType::LastAttachTime => { - QuadTreeState::::empty(shape)?.into() + QuadTreeState::::empty_with_types( + shape, + n_tile_types, + )? + .into() } TrackingType::PrintEvent => { - QuadTreeState::::empty(shape)?.into() + QuadTreeState::::empty_with_types( + shape, + n_tile_types, + )? + .into() } }, CanvasType::Tube => match tracking { TrackingType::None => { - QuadTreeState::::empty(shape)?.into() - } - TrackingType::Order => { - QuadTreeState::::empty(shape)?.into() + QuadTreeState::::empty_with_types( + shape, + n_tile_types, + )? + .into() } + TrackingType::Order => QuadTreeState::::empty_with_types( + shape, + n_tile_types, + )? + .into(), TrackingType::LastAttachTime => { - QuadTreeState::::empty(shape)?.into() + QuadTreeState::::empty_with_types( + shape, + n_tile_types, + )? + .into() } TrackingType::PrintEvent => { - QuadTreeState::::empty(shape)?.into() + QuadTreeState::::empty_with_types( + shape, + n_tile_types, + )? + .into() } }, }) @@ -179,10 +234,17 @@ pub trait StateStatus { fn reset_tracking_assuming_empty_state(&mut self); } +#[enum_dispatch] +pub trait TileCounts { + fn tile_counts(&self) -> ArrayView1; + fn count_of_tile(&self, tile: Tile) -> NumTiles; +} + pub trait StateWithCreate: State + Sized { type Params; // fn new_raw(canvas: Self::RawCanvas) -> Result; fn empty(params: Self::Params) -> Result; + fn empty_with_types(params: Self::Params, n_tile_types: usize) -> Result; fn from_array(arr: Array2) -> Result; fn get_params(&self) -> Self::Params; fn zeroed_copy_from_state_nonzero_rate(&mut self, source: &Self) -> &mut Self; @@ -197,6 +259,7 @@ pub struct QuadTreeState { total_events: NumEvents, time: f64, pub tracker: T, + tile_counts: Array1, } impl QuadTreeState { @@ -205,6 +268,16 @@ impl QuadTreeState { } } +impl TileCounts for QuadTreeState { + fn tile_counts(&self) -> ArrayView1 { + self.tile_counts.view() + } + + fn count_of_tile(&self, tile: Tile) -> NumTiles { + self.tile_counts[tile as usize] + } +} + impl State for QuadTreeState { fn panicinfo(&self) -> String { format!( @@ -374,6 +447,23 @@ where total_events: 0, time: 0., tracker, + tile_counts: Array1::::zeros(1), + }) + } + + fn empty_with_types(params: Self::Params, n_tile_types: usize) -> Result { + let rates: QuadTreeSquareArray = + QuadTreeSquareArray::new_with_size(params.0, params.1); + let canvas = C::new_sized(params)?; + let tracker = T::default(&canvas); + Ok(QuadTreeState:: { + rates, + canvas, + ntiles: 0, + total_events: 0, + time: 0., + tracker, + tile_counts: Array1::::zeros(n_tile_types), }) } @@ -390,6 +480,7 @@ where total_events: 0, time: 0., tracker, + tile_counts: Array1::::zeros(1), }) } diff --git a/rgrow/src/tileset.rs b/rgrow/src/tileset.rs index f6d48bf..022bf82 100644 --- a/rgrow/src/tileset.rs +++ b/rgrow/src/tileset.rs @@ -668,7 +668,7 @@ impl TileSet { let kind = self.canvas_type.unwrap_or(CANVAS_TYPE_DEFAULT); let tracking = self.tracking.unwrap_or(TrackingType::None); - Ok(StateEnum::empty(shape, kind, tracking)?) + Ok(StateEnum::empty(shape, kind, tracking, 1)?) // FIXME } /// Creates an empty state, without any setup by a System. From 2b4b4b2741fb044b6aa18479905e4dbea663e2b4 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:31:23 +0100 Subject: [PATCH 068/117] mutate attachment array --- rgrow/src/state.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/rgrow/src/state.rs b/rgrow/src/state.rs index fc4de27..ce5e331 100644 --- a/rgrow/src/state.rs +++ b/rgrow/src/state.rs @@ -238,6 +238,11 @@ pub trait StateStatus { pub trait TileCounts { fn tile_counts(&self) -> ArrayView1; fn count_of_tile(&self, tile: Tile) -> NumTiles; + + /// Change the tile count based on the tile attaching + fn update_attachment(&mut self, tile: Tile); + /// Change the tile count based on the tile detaching + fn update_detachment(&mut self, tile: Tile); } pub trait StateWithCreate: State + Sized { @@ -276,6 +281,14 @@ impl TileCounts for QuadTreeState { fn count_of_tile(&self, tile: Tile) -> NumTiles { self.tile_counts[tile as usize] } + + fn update_attachment(&mut self, tile: Tile) { + self.tile_counts[tile as usize] += 1; + } + + fn update_detachment(&mut self, tile: Tile) { + self.tile_counts[tile as usize] += 1; + } } impl State for QuadTreeState { From 1a2e49ba0e6dd5d2361e8e9c36dbedb6e0925e82 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:33:26 +0100 Subject: [PATCH 069/117] probability of strand being already attached --- rgrow/src/models/sdc1d.rs | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 08781c5..555a091 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -15,10 +15,10 @@ macro_rules! type_alias { * - There are quite a few expects that need to be handled better * */ -use std::{ - collections::{HashMap, HashSet}, - f64, usize, -}; +use core::f64; +use std::collections::{HashMap, HashSet}; + +use rand::Rng; use crate::{ base::{Energy, Glue, GrowError, Rate, Tile}, @@ -63,6 +63,8 @@ pub struct SDC { /// with only glue on the south, west, and east (nothing can stuck to the top of a strand) // pub strands: Array1, pub strand_concentration: Array1, + /// The concentration of the scaffold + pub scaffold_concentration: Conc, /// Glues of a given strand by id /// /// Note that the glues will be sorted in the following manner: @@ -129,6 +131,7 @@ impl SDC { delta_g_matrix, entropy_matrix, temperature, + scaffold_concentration: todo!(), // These will be generated by the update_system function next, so just leave them // empty for now friends_btm: HashMap::new(), @@ -185,6 +188,23 @@ impl SDC { self.update_system(); } + // FIXME: + // MAKE SURE THAT THIS FUNCTION IS CORRECT + // + // It should count how many of a tile there is overall (attached or not) + // ie monomer count + // + // count_monomer = (c_monomer / c_scaffold) * count_scaffold + pub fn total_tile_count(&self, tile: Tile) -> usize { + let per = self.strand_concentration[tile as usize] / self.scaffold_concentration; + let net = per * self.scaffold().len() as f64; + net as usize + } + + pub fn attachment_probability(&self, tile: Tile) { + self.total_tile_count(tile); + } + #[inline(always)] fn rtval(&self) -> f64 { R * (self.temperature + 273.15) @@ -345,6 +365,13 @@ impl SDC { for &strand in friends { acc -= self.kf * self.strand_concentration[strand as usize]; if acc <= 0.0 && (!just_calc) { + let rand: f64 = rand::random(); + let total = self.total_tile_count(strand) as f64; + let attached = state.count_of_tile(strand) as f64; + if rand <= attached / total { + return (false, acc, Event::None); + } + return (true, acc, Event::MonomerAttachment(point, strand)); } } @@ -712,6 +739,8 @@ impl FromTileSet for SDC { glues: pc.tile_edges, anchor_tiles: Vec::new(), scaffold, + // FIXME + scaffold_concentration: 0.0, strand_concentration, kf: tileset.kf.unwrap_or(1.0e6), delta_g_matrix: todo!(), @@ -1052,6 +1081,7 @@ mod test_sdc_model { [1, 1, 78], [4, 4, 1], ], + scaffold_concentration: 0.0, colors: Vec::new(), kf: 0.0, friends_btm: HashMap::new(), @@ -1129,6 +1159,7 @@ mod test_sdc_model { [11, 1, 30], [4, 4, 1], ], + scaffold_concentration: 0.0, colors: Vec::new(), kf: 0.0, friends_btm: HashMap::new(), From 7feb8a10638a32a284954cb9517f4b4161d81beb Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:49:23 +0100 Subject: [PATCH 070/117] Update attachment/detachment --- rgrow/src/models/sdc1d.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 555a091..a797f4f 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -506,20 +506,20 @@ impl SDC { } impl System for SDC { - fn update_after_event(&self, state: &mut St, event: &crate::system::Event) { + fn update_after_event(&self, state: &mut St, event: &Event) { match event { Event::None => todo!(), - Event::MonomerAttachment(scaffold_point, _) - | Event::MonomerDetachment(scaffold_point) - | Event::MonomerChange(scaffold_point, _) => { - // TODO: Make sure that this is all that needs be done for update + Event::MonomerAttachment(scaffold_point, strand) => { + // Increment the strands attachment by one + state.update_attachment(*strand); self.update_monomer_point(state, scaffold_point) } - Event::PolymerDetachment(v) => self.polymer_update(v, state), - Event::PolymerAttachment(t) | Event::PolymerChange(t) => self.polymer_update( - &t.iter().map(|(p, _)| *p).collect::>(), - state, - ), + Event::MonomerDetachment(scaffold_point) => { + let strand = state.tile_at_point(*scaffold_point); + state.update_detachment(strand); + self.update_monomer_point(state, scaffold_point) + } + _ => panic!("This event is not supported in SDC"), } } From 8e3c12ce68ecac830178c517383ea12a3eca5a6d Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Tue, 23 Jul 2024 17:21:50 +0100 Subject: [PATCH 071/117] Better error reporting --- rgrow/src/state.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/rgrow/src/state.rs b/rgrow/src/state.rs index ce5e331..450ac6e 100644 --- a/rgrow/src/state.rs +++ b/rgrow/src/state.rs @@ -279,15 +279,28 @@ impl TileCounts for QuadTreeState { } fn count_of_tile(&self, tile: Tile) -> NumTiles { - self.tile_counts[tile as usize] + *self.tile_counts.get(tile as usize).expect( + format!( + "Count Of Tile out of bounds ({} not in arr of len {})", + tile as usize, + self.tile_counts.len() + ) + .as_str(), + ) } fn update_attachment(&mut self, tile: Tile) { - self.tile_counts[tile as usize] += 1; + *self + .tile_counts + .get_mut(tile as usize) + .expect("Out of bounds on attachment update") += 1; } fn update_detachment(&mut self, tile: Tile) { - self.tile_counts[tile as usize] += 1; + *self + .tile_counts + .get_mut(tile as usize) + .expect("Out of bounds on detachment update") -= 1; } } From 2c5f53ce17bf3d6a733187b39210824626187153 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Tue, 23 Jul 2024 17:22:16 +0100 Subject: [PATCH 072/117] When no event takes place, let time pass --- rgrow/src/system.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rgrow/src/system.rs b/rgrow/src/system.rs index 11ff317..6dda397 100644 --- a/rgrow/src/system.rs +++ b/rgrow/src/system.rs @@ -271,6 +271,7 @@ pub trait System: Debug + Sync + Send + TileBondInfo + Clone { let (point, remainder) = state.choose_point(); // todo: resultify let event = self.choose_event_at_point(state, PointSafe2(point), remainder); // FIXME if let Event::None = event { + state.add_time(time_step); return StepOutcome::DeadEventAt(time_step); } From 403c5d198f02a524bdb37c07fe34b10d7aa2ed6c Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Tue, 23 Jul 2024 17:23:00 +0100 Subject: [PATCH 073/117] scaffold concentration --- rgrow/src/models/sdc1d.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index a797f4f..702befa 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -111,6 +111,7 @@ impl SDC { glue_names: Vec, scaffold: Array2, strand_concentration: Array1, + scaffold_concentration: Conc, glues: Array2, colors: Vec<[u8; 4]>, kf: RatePerConc, @@ -131,7 +132,7 @@ impl SDC { delta_g_matrix, entropy_matrix, temperature, - scaffold_concentration: todo!(), + scaffold_concentration, // These will be generated by the update_system function next, so just leave them // empty for now friends_btm: HashMap::new(), @@ -369,7 +370,7 @@ impl SDC { let total = self.total_tile_count(strand) as f64; let attached = state.count_of_tile(strand) as f64; if rand <= attached / total { - return (false, acc, Event::None); + return (true, acc, Event::None); } return (true, acc, Event::MonomerAttachment(point, strand)); @@ -836,6 +837,7 @@ fn gsorseq_to_gs(gsorseq: &GsOrSeq) -> (f64, f64) { pub struct SDCParams { pub strands: Vec, pub scaffold: SingleOrMultiScaffold, + pub scaffold_concentration: f64, // Pair with delta G at 37 degrees C and delta S pub glue_dg_s: HashMap, pub k_f: f64, @@ -1014,6 +1016,7 @@ impl SDC { glue_names, scaffold, strand_concentration, + params.scaffold_concentration, glues, strand_colors, params.k_f, @@ -1072,6 +1075,7 @@ mod test_sdc_model { glue_names: Vec::new(), scaffold: Array2::::zeros((5, 5)), strand_concentration: Array1::::zeros(5), + scaffold_concentration: 0.0, glues: array![ [0, 0, 0], [1, 3, 12], @@ -1081,7 +1085,6 @@ mod test_sdc_model { [1, 1, 78], [4, 4, 1], ], - scaffold_concentration: 0.0, colors: Vec::new(), kf: 0.0, friends_btm: HashMap::new(), @@ -1300,6 +1303,7 @@ mod test_sdc_model { strands, scaffold, temperature: 20.0, + scaffold_concentration: 1e-100, glue_dg_s, k_f: 1e6, k_n: 1e5, From 6cd55db32bfbd9391aaa2ac171d14a61b51930de Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Tue, 23 Jul 2024 17:50:48 +0100 Subject: [PATCH 074/117] mistake when counting scaffolds --- rgrow/src/models/sdc1d.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 702befa..a508928 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -198,7 +198,7 @@ impl SDC { // count_monomer = (c_monomer / c_scaffold) * count_scaffold pub fn total_tile_count(&self, tile: Tile) -> usize { let per = self.strand_concentration[tile as usize] / self.scaffold_concentration; - let net = per * self.scaffold().len() as f64; + let net = per * self.scaffold.nrows() as f64; net as usize } From d8552349abec93f0b0e1fc2b4cea6f8e9ebf6803 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Wed, 24 Jul 2024 15:36:54 -0400 Subject: [PATCH 075/117] add an nrows/cols_usable to canvas, use this for n_scaffolds --- rgrow/src/canvas.rs | 26 ++++++++++++++++++++++++++ rgrow/src/models/sdc1d.rs | 14 +++++++------- rgrow/src/state.rs | 8 ++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/rgrow/src/canvas.rs b/rgrow/src/canvas.rs index 44d81a7..39f96ba 100644 --- a/rgrow/src/canvas.rs +++ b/rgrow/src/canvas.rs @@ -38,6 +38,8 @@ pub trait Canvas: std::fmt::Debug + Sync + Send { fn raw_array(&self) -> ArrayView2; fn nrows(&self) -> usize; fn ncols(&self) -> usize; + fn nrows_usable(&self) -> usize; + fn ncols_usable(&self) -> usize; fn set_sa_countabletilearray( &mut self, @@ -481,6 +483,14 @@ impl Canvas for CanvasSquare { self.0 .fold(0, |x, y| x + u32::from(should_be_counted[*y as usize])) } + + fn nrows_usable(&self) -> usize { + self.0.nrows() - 2 + } + + fn ncols_usable(&self) -> usize { + self.0.ncols() - 2 + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -555,6 +565,14 @@ impl Canvas for CanvasPeriodic { fn ncols(&self) -> usize { self.0.ncols() } + + fn nrows_usable(&self) -> usize { + self.0.nrows() + } + + fn ncols_usable(&self) -> usize { + self.0.ncols() + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -777,4 +795,12 @@ impl Canvas for CanvasTube { fn center(&self) -> PointSafe2 { PointSafe2((self.nrows() / 2, self.ncols() / 2)) } + + fn nrows_usable(&self) -> usize { + self.0.nrows() // FIXME: is this correct? + } + + fn ncols_usable(&self) -> usize { + self.0.ncols() // FIXME: is this correct? + } } diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index a508928..d089ede 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -189,6 +189,10 @@ impl SDC { self.update_system(); } + pub fn n_scaffolds(&self, state: &S) -> usize { + state.nrows_usable() + } + // FIXME: // MAKE SURE THAT THIS FUNCTION IS CORRECT // @@ -196,16 +200,12 @@ impl SDC { // ie monomer count // // count_monomer = (c_monomer / c_scaffold) * count_scaffold - pub fn total_tile_count(&self, tile: Tile) -> usize { + pub fn total_tile_count(&self, state: &S, tile: Tile) -> usize { let per = self.strand_concentration[tile as usize] / self.scaffold_concentration; - let net = per * self.scaffold.nrows() as f64; + let net = per * self.n_scaffolds(state) as f64; net as usize } - pub fn attachment_probability(&self, tile: Tile) { - self.total_tile_count(tile); - } - #[inline(always)] fn rtval(&self) -> f64 { R * (self.temperature + 273.15) @@ -367,7 +367,7 @@ impl SDC { acc -= self.kf * self.strand_concentration[strand as usize]; if acc <= 0.0 && (!just_calc) { let rand: f64 = rand::random(); - let total = self.total_tile_count(strand) as f64; + 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); diff --git a/rgrow/src/state.rs b/rgrow/src/state.rs index 450ac6e..53d7764 100644 --- a/rgrow/src/state.rs +++ b/rgrow/src/state.rs @@ -388,6 +388,14 @@ impl Canvas for QuadTreeState { self.canvas.ncols() } + fn nrows_usable(&self) -> usize { + self.canvas.nrows_usable() + } + + fn ncols_usable(&self) -> usize { + self.canvas.ncols_usable() + } + fn set_sa(&mut self, p: &PointSafe2, t: &Tile) { let r = unsafe { self.uvm_p(p.0) }; From b688e7580afb8d1a16e493f70f7e87c911b55fdf Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Wed, 24 Jul 2024 15:45:18 -0400 Subject: [PATCH 076/117] Move tile count updates to perform_event in sdc. By the time update_after_event is run, the canvas has already been updated, and there's no easy way to get what the tile in the point was. More generally, it might be useful to include this in the Event enum, but for now, this change fixes tile counting. --- rgrow/src/models/sdc1d.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index d089ede..9b55f3d 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -510,20 +510,34 @@ impl System for SDC { fn update_after_event(&self, state: &mut St, event: &Event) { match event { Event::None => todo!(), - Event::MonomerAttachment(scaffold_point, strand) => { + Event::MonomerAttachment(scaffold_point, _) => { // Increment the strands attachment by one - state.update_attachment(*strand); self.update_monomer_point(state, scaffold_point) } Event::MonomerDetachment(scaffold_point) => { - let strand = state.tile_at_point(*scaffold_point); - state.update_detachment(strand); self.update_monomer_point(state, scaffold_point) } _ => panic!("This event is not supported in SDC"), } } + fn perform_event(&self, state: &mut St, event: &Event) -> &Self { + 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); + } + _ => panic!("This event is not supported in SDC"), + }; + self + } + fn event_rate_at_point( &self, state: &St, From d0c75d8df1ac51241f1f3967fe3a283e02195850 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:12:48 +0100 Subject: [PATCH 077/117] reuse rand thread --- rgrow/src/models/sdc1d.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 9b55f3d..99d286a 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -362,11 +362,12 @@ impl SDC { let empty_map = HashSet::default(); let friends = self.friends_btm.get(scaffold_glue).unwrap_or(&empty_map); + let mut rand_thread = rand::thread_rng(); for &strand in friends { acc -= self.kf * self.strand_concentration[strand as usize]; if acc <= 0.0 && (!just_calc) { - let rand: f64 = rand::random(); + let rand: f64 = rand_thread.gen(); let total = self.total_tile_count(state, strand) as f64; let attached = state.count_of_tile(strand) as f64; if rand <= attached / total { From acbc811ff5af38c08e635280c2ef3efcfb666be2 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:39:31 +0100 Subject: [PATCH 078/117] remove import --- rgrow/src/models/sdc1d.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 99d286a..f2ef692 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -27,7 +27,6 @@ use crate::{ state::State, system::{Event, NeededUpdate, System, TileBondInfo}, tileset::{FromTileSet, ProcessedTileSet, Size}, - utils, }; use ndarray::prelude::{Array1, Array2}; From bf22c404c5edf6096f726c389d4ebae1f2d09d94 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:40:17 +0100 Subject: [PATCH 079/117] remove polymer update from SDC (there are no polymers) --- rgrow/src/models/sdc1d.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index f2ef692..0b1d10d 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -210,23 +210,6 @@ impl SDC { R * (self.temperature + 273.15) } - fn polymer_update(&self, points: &Vec, state: &mut S) { - let mut points_to_update = points - .iter() - .flat_map(|&point| { - [ - PointSafeHere(point.0), - state.move_sa_w(point), - state.move_sa_e(point), - ] - }) - .collect::>(); - - points_to_update.sort_unstable(); - points_to_update.dedup(); - self.update_points(state, &points_to_update) - } - fn update_monomer_point(&self, state: &mut S, scaffold_point: &PointSafe2) { let points = [ state.move_sa_w(*scaffold_point), From cf52bdbe5985948cd3f0e9ba73c7055634cc83ed Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:42:01 +0100 Subject: [PATCH 080/117] two match branches into one --- rgrow/src/models/sdc1d.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 0b1d10d..c266f44 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -493,11 +493,8 @@ impl System for SDC { fn update_after_event(&self, state: &mut St, event: &Event) { match event { Event::None => todo!(), - Event::MonomerAttachment(scaffold_point, _) => { - // Increment the strands attachment by one - self.update_monomer_point(state, scaffold_point) - } - Event::MonomerDetachment(scaffold_point) => { + Event::MonomerAttachment(scaffold_point, _) + | Event::MonomerDetachment(scaffold_point) => { self.update_monomer_point(state, scaffold_point) } _ => panic!("This event is not supported in SDC"), From 6da83aa744e4c04b56622a1a24d4a50e9654d8b3 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:57:52 +0100 Subject: [PATCH 081/117] base anneal definition --- rgrow/src/models/sdc1d.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index c266f44..a33063b 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -1021,6 +1021,39 @@ impl SDC { } } +/* +* +* EXPERIMENTAL HELPER FUNCIONS +* +* I think that this part maybe could be moved to a different file +* as to not mix implementation of the system with its use +*/ + +pub struct AnnealProtocol { + /// A tuple with initial and final temperatures (in C) + pub temperatures: (f64, f64), + /// A tuple with: + /// 1. How long to hold the initial temperature for before starting the temperature decremenet + /// 2. How long to hold the final temperature for before finishing the anneal + pub holds: (f64, f64), + /// How long to spend in the phase where the temperature is decrementing from the initial to + /// the final temp + pub anneal_time: f64, + /// TODO: Document this properly + steps_per_sec: 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, + steps_per_sec: 0.5, + } + } +} + #[cfg(feature = "python")] #[pymethods] impl SDC { From 29c6f7e9497b520a76ebe500084dac87961243c5 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:02:16 +0100 Subject: [PATCH 082/117] change to seconds per step --- rgrow/src/models/sdc1d.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index a33063b..f2549a6 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -1039,8 +1039,8 @@ pub struct AnnealProtocol { /// How long to spend in the phase where the temperature is decrementing from the initial to /// the final temp pub anneal_time: f64, - /// TODO: Document this properly - steps_per_sec: f64, + /// How long to spend at each temperature + seconds_per_step: f64, } impl Default for AnnealProtocol { @@ -1049,7 +1049,7 @@ impl Default for AnnealProtocol { temperatures: (80., 20.), holds: (10. * 60., 45. * 60.), anneal_time: 3.0 * 60.0 * 60.0, - steps_per_sec: 0.5, + seconds_per_step: 2.0, } } } From 2dc37add0cef2c951943cfaa1b62d44fea35b853 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:17:42 +0100 Subject: [PATCH 083/117] generate arrays from anneal data --- rgrow/src/models/sdc1d.rs | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index f2549a6..dfd03d0 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -18,6 +18,7 @@ macro_rules! type_alias { use core::f64; use std::collections::{HashMap, HashSet}; +use num_traits::Float; use rand::Rng; use crate::{ @@ -1054,6 +1055,73 @@ impl Default for AnnealProtocol { } } +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 + } + + /// Generates two arrays: + /// (Vec, Vec) + fn generate_arrays(&self) -> (Vec, Vec) { + // See how many steps we wil take during each of the stages + let steps_init = self.initial_steps(); + let steps_final = self.final_steps(); + let steps_delta = self.delta_steps(); + + let mut temps = Vec::::with_capacity(steps_init + steps_delta + steps_final); + let mut times = Vec::::with_capacity(steps_init + steps_delta + steps_final); + + // This assumes that the final temperature is lower + let temperature_diff = self.temperatures.0 - self.temperatures.1; + let temperature_delta = temperature_diff / (steps_delta as f64); + + // Initial time in seconds + let mut current_time = 0.0; + let mut current_temp = self.temperatures.0; + + (0..steps_init).for_each(|_step_num| { + // The temperature doesnt change + temps.push(current_temp); + // The time increments by the same delta + times.push(current_time); + + current_time += self.seconds_per_step; + }); + + (0..steps_delta).for_each(|_step_num| { + // The temperature doesnt change + temps.push(current_temp); + // The time increments by the same delta + times.push(current_time); + + current_time += self.seconds_per_step; + current_temp -= temperature_delta; + }); + + (0..steps_final).for_each(|_step_num| { + // The temperature doesnt change + temps.push(current_temp); + // The time increments by the same delta + times.push(current_time); + + current_time += self.seconds_per_step; + }); + + (temps, times) + } +} + #[cfg(feature = "python")] #[pymethods] impl SDC { From 7d1177228dbb32a6a1aa7eb1b6aa9b3d5ad69a5d Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:44:01 +0100 Subject: [PATCH 084/117] test anneal vectors --- rgrow/src/models/sdc1d.rs | 63 +++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index dfd03d0..665ee8a 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -1041,7 +1041,7 @@ pub struct AnnealProtocol { /// the final temp pub anneal_time: f64, /// How long to spend at each temperature - seconds_per_step: f64, + pub seconds_per_step: f64, } impl Default for AnnealProtocol { @@ -1073,7 +1073,7 @@ impl AnnealProtocol { /// Generates two arrays: /// (Vec, Vec) - fn generate_arrays(&self) -> (Vec, Vec) { + pub fn generate_arrays(&self) -> (Vec, Vec) { // See how many steps we wil take during each of the stages let steps_init = self.initial_steps(); let steps_final = self.final_steps(); @@ -1091,31 +1091,31 @@ impl AnnealProtocol { let mut current_temp = self.temperatures.0; (0..steps_init).for_each(|_step_num| { + current_time += self.seconds_per_step; + // The temperature doesnt change temps.push(current_temp); // The time increments by the same delta times.push(current_time); - - current_time += self.seconds_per_step; }); (0..steps_delta).for_each(|_step_num| { + current_time += self.seconds_per_step; + current_temp -= temperature_delta; + // The temperature doesnt change temps.push(current_temp); // The time increments by the same delta times.push(current_time); - - current_time += self.seconds_per_step; - current_temp -= temperature_delta; }); (0..steps_final).for_each(|_step_num| { + current_time += self.seconds_per_step; + // The temperature doesnt change temps.push(current_temp); // The time increments by the same delta times.push(current_time); - - current_time += self.seconds_per_step; }); (temps, times) @@ -1153,6 +1153,51 @@ impl SDC { } } +#[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, + }; + + #[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); + }) + } +} + #[cfg(test)] mod test_sdc_model { use crate::assert_all; From 238c4d2b293fe3a6cba43f45ebd52f88532c835c Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:18:24 +0100 Subject: [PATCH 085/117] rust anneal -- not tested --- rgrow/src/models/sdc1d.rs | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 665ee8a..c781319 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -20,13 +20,14 @@ use std::collections::{HashMap, HashSet}; use num_traits::Float; use rand::Rng; +use rayon::iter::Either; use crate::{ base::{Energy, Glue, GrowError, Rate, Tile}, canvas::{PointSafe2, PointSafeHere}, colors::get_color_or_random, state::State, - system::{Event, NeededUpdate, System, TileBondInfo}, + system::{DynSystem, Event, EvolveBounds, NeededUpdate, System, TileBondInfo}, tileset::{FromTileSet, ProcessedTileSet, Size}, }; @@ -1044,6 +1045,9 @@ pub struct AnnealProtocol { pub seconds_per_step: f64, } +/// Canvas Arrays, Times, Temperatues +type AnnealOutput = (Vec>, Vec, Vec); + impl Default for AnnealProtocol { fn default() -> Self { AnnealProtocol { @@ -1120,6 +1124,34 @@ impl AnnealProtocol { (temps, times) } + + // The reason I made this function part of the anneal struct, rather than having this function + // be part of the SDC is that it will be easier to implement "run_many_systems" and have it be + // concurrent + pub fn run_system( + &self, + mut sdc: SDC, + mut state: St, + ) -> Result { + let (tmps, times) = self.generate_arrays(); + + let bounds = EvolveBounds::default().for_time(self.seconds_per_step); + let needed = NeededUpdate::All; + let mut canvases = Vec::new(); + + for tmp in &tmps { + // Change the temperature + sdc.temperature = *tmp; + sdc.update_system(); + + crate::system::System::update_all(&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)) + } } #[cfg(feature = "python")] From 9d86e9c218fa43a375f35dd3469f4fd0995043c9 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:48:27 +0100 Subject: [PATCH 086/117] state for running anneal --- rgrow/src/models/sdc1d.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index c781319..9cebb58 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -26,7 +26,7 @@ use crate::{ base::{Energy, Glue, GrowError, Rate, Tile}, canvas::{PointSafe2, PointSafeHere}, colors::get_color_or_random, - state::State, + state::{State, StateEnum}, system::{DynSystem, Event, EvolveBounds, NeededUpdate, System, TileBondInfo}, tileset::{FromTileSet, ProcessedTileSet, Size}, }; @@ -1043,6 +1043,7 @@ pub struct AnnealProtocol { pub anneal_time: f64, /// How long to spend at each temperature pub seconds_per_step: f64, + pub scaffold_count: usize, } /// Canvas Arrays, Times, Temperatues @@ -1055,6 +1056,7 @@ impl Default for AnnealProtocol { holds: (10. * 60., 45. * 60.), anneal_time: 3.0 * 60.0 * 60.0, seconds_per_step: 2.0, + scaffold_count: 100, } } } @@ -1152,6 +1154,25 @@ impl AnnealProtocol { Ok((canvases, times, tmps)) } + + fn default_state(&self, sdc: &SDC) -> Result { + // There is a better way to do this + 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, + ) + } + + fn run_anneal_default_system(&self, sdc: SDC) -> Result { + let state = self.default_state(&sdc)?; + self.run_system(sdc, state) + } } #[cfg(feature = "python")] @@ -1194,6 +1215,7 @@ mod test_anneal { holds: (10. * 60., 45. * 60.), anneal_time: 3.0 * 60.0 * 60.0, seconds_per_step: 2.0, + scaffold_count: 100, }; #[test] From fedf2d8c64a00344c5de9fc6191e18031389c2a2 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:58:50 +0100 Subject: [PATCH 087/117] test -- check no errors are thrown --- rgrow/src/models/sdc1d.rs | 114 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 9cebb58..8838b2b 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -1218,6 +1218,114 @@ mod test_anneal { scaffold_count: 100, }; + fn gen_sdc() -> SDC { + let mut strands = Vec::::new(); + + // Anchor tile + 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{}0", base); + 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{}1", base); + 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 = 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, + }; + + 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(); @@ -1250,6 +1358,12 @@ mod test_anneal { 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)] From 162c49ba0435784c3722cbf97e34da126660530e Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:31:59 +0100 Subject: [PATCH 088/117] remove imports --- rgrow/src/models/sdc1d.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 8838b2b..d3ccd03 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -18,9 +18,7 @@ macro_rules! type_alias { use core::f64; use std::collections::{HashMap, HashSet}; -use num_traits::Float; use rand::Rng; -use rayon::iter::Either; use crate::{ base::{Energy, Glue, GrowError, Rate, Tile}, From 99386580bc32ac2914fdd4a2c34b12b56cc2ee31 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:05:42 +0100 Subject: [PATCH 089/117] add AnnealProtocol --- py-rgrow/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/py-rgrow/src/lib.rs b/py-rgrow/src/lib.rs index 65d392e..78e533d 100644 --- a/py-rgrow/src/lib.rs +++ b/py-rgrow/src/lib.rs @@ -31,6 +31,8 @@ mod rgrow { #[pymodule_export] use rgrow::models::oldktam::OldKTAM; #[pymodule_export] + use rgrow::models::sdc1d::AnnealProtocol; + #[pymodule_export] use rgrow::models::sdc1d::SDC; #[pymodule_export] use rgrow::system::DimerInfo; From e73c512e20ac5ce10bb70906d926f8532b7f0a69 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:06:43 +0100 Subject: [PATCH 090/117] Create anneal protocol from python --- rgrow/src/models/sdc1d.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index d3ccd03..07e37ac 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -1029,6 +1029,7 @@ impl SDC { * as to not mix implementation of the system with its use */ +#[cfg_attr(feature = "python", pyclass)] pub struct AnnealProtocol { /// A tuple with initial and final temperatures (in C) pub temperatures: (f64, f64), @@ -1173,6 +1174,33 @@ impl AnnealProtocol { } } +#[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 { + self.run_anneal_default_system(sdc).ok() + } +} + #[cfg(feature = "python")] #[pymethods] impl SDC { From 9b9a71c1f02038fbd5e67c10d9e325547a3d4a24 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:36:45 +0100 Subject: [PATCH 091/117] Add fixme (broken anneal) --- rgrow/src/models/sdc1d.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 07e37ac..8feff10 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -1147,6 +1147,8 @@ impl AnnealProtocol { crate::system::System::update_all(&sdc, &mut state, &needed); crate::system::System::evolve(&sdc, &mut state, bounds)?; + // FIXME: This is flattening the canvas, so it doesnt work nicely + // it should be Vec>, not Vec<_> let canvas = state.raw_array().to_slice().unwrap(); canvases.push(canvas.to_vec()) } @@ -1358,7 +1360,7 @@ mod test_anneal { let mut expected_time = vec![]; let mut ctime = 2.0; - loop { + loop {1d expected_time.push(ctime); ctime += 2.0; if ctime > 14100.0 { From 5864b2697eef48d346907712f953c12d66d66a5c Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Sat, 27 Jul 2024 17:40:20 -0400 Subject: [PATCH 092/117] =?UTF-8?q?boltzmann=20distribution=20uses=20?= =?UTF-8?q?=CE=B2=20rather=20than=20RT,=20also=20fix=20vim-introduced=20bu?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rgrow/src/models/sdc1d.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 8feff10..dd7ede1 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -472,7 +472,7 @@ impl SDC { pub fn boltzman_function(&self, attachments: &Vec) -> f64 { let g_a = self.g_system(attachments); - (-self.rtval() * g_a).exp() + (-g_a / self.rtval()).exp() } pub fn sum_systems(&self) -> f64 { @@ -1360,7 +1360,7 @@ mod test_anneal { let mut expected_time = vec![]; let mut ctime = 2.0; - loop {1d + loop { expected_time.push(ctime); ctime += 2.0; if ctime > 14100.0 { From 734d33a8202f434875937931c2072a5499f4ddc8 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Mon, 29 Jul 2024 17:12:19 +0100 Subject: [PATCH 093/117] only nonzero rate update in anneal --- rgrow/src/models/sdc1d.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index dd7ede1..caa90ce 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -1137,7 +1137,7 @@ impl AnnealProtocol { let (tmps, times) = self.generate_arrays(); let bounds = EvolveBounds::default().for_time(self.seconds_per_step); - let needed = NeededUpdate::All; + let needed = NeededUpdate::NonZero; let mut canvases = Vec::new(); for tmp in &tmps { From cef881dceff6b36785e3459d6ee85cf6751471b6 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Wed, 31 Jul 2024 12:16:49 +0100 Subject: [PATCH 094/117] fix g_system units / RT problems; add some python methods. --- rgrow/src/models/sdc1d.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index caa90ce..75a5853 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -32,6 +32,8 @@ use crate::{ use ndarray::prelude::{Array1, Array2}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "python")] +use numpy::ToPyArray; #[cfg(feature = "python")] use pyo3::prelude::*; @@ -417,11 +419,11 @@ impl SDC { }; // Take into account the penalty - let penalty = self.rtval() * (self.strand_concentration[*strand as usize] / U0).ln(); + let penalty = (self.strand_concentration[*strand as usize] / U0).ln(); sumg -= penalty; } - sumg + sumg * self.rtval() } // This is quite inefficient -- and clones a lot. If the scaffold were to be @@ -1232,6 +1234,21 @@ impl SDC { self.temperature = tmp; self.update_system(); } + + #[getter] + fn get_scaffold_energy_bonds<'py>(&self, py: Python<'py>) -> Bound<'py, numpy::PyArray1> { + self.scaffold_energy_bonds.to_pyarray_bound(py) + } + + #[getter] + fn get_strand_energy_bonds<'py>(&self, py: Python<'py>) -> Bound<'py, numpy::PyArray2> { + self.strand_energy_bonds.to_pyarray_bound(py) + } + + #[getter] + fn get_tile_concs<'py>(&self, py: Python<'py>) -> Bound<'py, numpy::PyArray1> { + self.strand_concentration.to_pyarray_bound(py) + } } #[cfg(test)] From 90bd218be0add27f1d95cc6e9dd638c300dd1d4b Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:18:53 +0100 Subject: [PATCH 095/117] Run Many Anneals -- Get probs --- rgrow/src/models/sdc1d.rs | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 8feff10..fea8b7e 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -19,13 +19,14 @@ use core::f64; use std::collections::{HashMap, HashSet}; use rand::Rng; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use crate::{ base::{Energy, Glue, GrowError, Rate, Tile}, canvas::{PointSafe2, PointSafeHere}, colors::get_color_or_random, state::{State, StateEnum}, - system::{DynSystem, Event, EvolveBounds, NeededUpdate, System, TileBondInfo}, + system::{self, DynSystem, Event, EvolveBounds, NeededUpdate, System, TileBondInfo}, tileset::{FromTileSet, ProcessedTileSet, Size}, }; @@ -1174,6 +1175,15 @@ impl AnnealProtocol { let state = self.default_state(&sdc)?; self.run_system(sdc, state) } + + fn run_many_anneals_default_system( + &self, + sdcs: Vec, + ) -> Vec> { + sdcs.par_iter() + .map(|sdc| self.run_anneal_default_system(sdc.clone())) + .collect() + } } #[cfg(feature = "python")] @@ -1201,6 +1211,13 @@ impl AnnealProtocol { fn run_one_system(&self, sdc: SDC) -> Option { self.run_anneal_default_system(sdc).ok() } + + fn run_many_systems(&self, sdcs: Vec) -> Vec> { + self.run_many_anneals_default_system(sdcs) + .into_iter() + .map(|z| z.ok()) + .collect() + } } #[cfg(feature = "python")] @@ -1232,6 +1249,21 @@ impl SDC { self.temperature = tmp; self.update_system(); } + + fn get_all_probs(&self) -> Vec<(Vec, f64, f64)> { + let systems = self.system_states(); + let mut triples = Vec::new(); + for s in systems { + let prob = self.probabilty(&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 + } } #[cfg(test)] @@ -1360,7 +1392,7 @@ mod test_anneal { let mut expected_time = vec![]; let mut ctime = 2.0; - loop {1d + loop { expected_time.push(ctime); ctime += 2.0; if ctime > 14100.0 { From 9e9ee84561a8c0ba11a70a566e0cb574127e587a Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:36:58 +0100 Subject: [PATCH 096/117] mismatches --- rgrow/src/utils.rs | 134 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 5 deletions(-) diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index 1dce730..8a985a5 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -80,12 +80,27 @@ where Some(ans) } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq)] enum DnaNucleotideBase { - A, - T, - G, - C, + A = 0, + T = 1, + G = 2, + C = 3, +} + +impl DnaNucleotideBase { + pub fn connects_to(&self) -> Self { + match self { + Self::A => Self::T, + Self::T => Self::A, + Self::G => Self::C, + Self::C => Self::G, + } + } + + pub fn ideal_sequence(v: &Vec) -> Vec { + v.iter().map(|s| s.connects_to()).collect() + } } impl From for DnaNucleotideBase { @@ -140,6 +155,94 @@ fn dna_dg_ds(dna: impl Iterator) -> (f64, f64) { .expect("DNA must have length of at least 2") } +fn good_match(a: &DnaNucleotideBase, b: &DnaNucleotideBase) -> bool { + a.connects_to() == *b +} + +/// Index this as follows: +/// +/// Given the following MISMATCH +/// PX/(P*)Y then the penalty is given +/// by index [P][X][Y] +const PENALTY_TABLE: [[[f64; 4]; 4]; 4] = [ + // AX/TY + [ + // X = A + [0.61, 0.0, 0.14, 0.88], + // X = T + [0.0, 0.69, 0.07, 0.73], + // X = G + [0.02, 0.71, -0.13, 0.0], + // X = C + [0.77, 0.64, 0.0, 1.33], + ], + // TX/AY + [ + [0.69, 0.0, 0.42, 0.92], + [0.0, 0.68, 0.34, 0.75], + [0.74, 0.43, 0.44, 0.0], + [1.33, 0.97, 0.0, 1.05], + ], + // GX/CY + [ + [0.17, 0.0, -0.25, 0.81], + [0.0, 0.45, -0.59, 0.98], + [-0.52, 0.08, -1.11, 0.0], + [0.47, 0.62, 0.0, 0.79], + ], + // CX/GY + [ + [0.43, 0.0, 0.03, 0.75], + [0.0, -0.12, -0.32, 0.40], + [0.11, -0.47, -0.11, 0.0], + [0.79, 0.62, 0.0, 0.70], + ], +]; + +/// Calculate the penalty introduced by a single mismatch +#[inline(always)] +fn calc_penalty(prior: &DnaNucleotideBase, x: &DnaNucleotideBase, y: &DnaNucleotideBase) -> f64 { + PENALTY_TABLE[*prior as usize][*x as usize][*y as usize] +} + +fn dealta_g_with_penalty( + dna_a: Vec, + dna_b: Vec, +) -> (f64, f64) { + if dna_a.len() != dna_b.len() { + panic!("Dnas must be same length to compare"); + } + + let (mut dg, mut ds) = (0.0, 0.0); + + let a_windows = dna_a.windows(2); + let b_windows = dna_b.windows(2); + + for (a, b) in std::iter::zip(a_windows, b_windows) { + let (a1, a2) = (a[0], a[1]); + let (b1, b2) = (b[0], b[1]); + + if good_match(&a2, &b2) && good_match(&a1, &b1) { + let (ndg, nds) = dG_dS(&a1, &a2); + println!("{:?}{:?}/{:?}{:?} = {ndg}", a1, a2, b1, b2); + dg += ndg; + ds += nds; + } else if good_match(&a2, &b2) { + // [0.11, 0.0, -0.11, -0.47], + + let p = PENALTY_TABLE[b2 as usize][b1 as usize][a1 as usize]; + println!("{:?}{:?}/{:?}{:?} = {p}", b2, b1, a2, a1); + dg += p; + } else if good_match(&a1, &b1) { + let p = PENALTY_TABLE[a1 as usize][a2 as usize][b2 as usize]; + println!("{:?}{:?}/{:?}{:?} = {p}", a1, a2, b1, b2); + dg += PENALTY_TABLE[a1 as usize][a2 as usize][b2 as usize]; + } + } + + (dg, ds) +} + #[cfg_attr(feature = "python", pyfunction)] pub fn string_dna_dg_ds(dna_sequence: &str) -> (f64, f64) { dna_dg_ds(dna_sequence.chars().map(DnaNucleotideBase::from)) @@ -184,8 +287,11 @@ pub fn loop_penalty(length: usize, kind: &str) -> f64 { #[cfg(test)] mod test_utils { + use crate::utils::dealta_g_with_penalty; use crate::utils::LOOP_TABLE; + use super::string_dna_dg_ds; + use super::DnaNucleotideBase; use super::_loop_penalty; use super::string_dna_delta_g; use super::two_window_fold; @@ -313,4 +419,22 @@ mod test_utils { assert!(val29 > LOOP_TABLE[0][13]); assert!(val29 < LOOP_TABLE[0][14]); } + + #[test] + fn test_mismatch_penalty() { + let dna_a = "GGACTGACG".chars().map(DnaNucleotideBase::from).collect(); + let dna_b = "CCTGGCTGC".chars().map(DnaNucleotideBase::from).collect(); + let (total, _) = dealta_g_with_penalty(dna_a, dna_b); + assert_eq!(total + 1.96, -8.32); + } + + #[test] + fn test_no_mismatches() { + let dna_a = "GGACTGAC".chars().map(DnaNucleotideBase::from).collect(); + let dna_b = DnaNucleotideBase::ideal_sequence(&dna_a); + let (g, s) = dealta_g_with_penalty(dna_a, dna_b); + let (pg, ps) = string_dna_dg_ds("GGACTGAC"); + assert_eq!(g, pg); + assert_eq!(s, ps); + } } From 65f59913df04a1a631c1f00aef8cc9c575e5c04a Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:38:30 +0100 Subject: [PATCH 097/117] typo --- rgrow/src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index 8a985a5..e09e7d2 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -55,7 +55,7 @@ const LENGTHS: [usize; 15] = [3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20, 25, 3 * + g(A, T) + (temp - 37) s(A, T) * */ -/// 2-sliding window generic implementatin for any iterator with a fold function +/// 2-sliding window generic implementation for any iterator with a fold function /// /// None will be returned if the iterator is too short fn two_window_fold(mut iter: impl Iterator, fold: F) -> Option From e4c519223eb40eba3fb1876849f9630573858b55 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:56:43 +0100 Subject: [PATCH 098/117] simplify function --- rgrow/src/utils.rs | 107 +++++++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 52 deletions(-) diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index e09e7d2..af7dd43 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -19,6 +19,46 @@ const PENALTY_S: f64 = 0.0057; // (same unit as delta G needed) const R: f64 = 1.98720425864083 / 1000.0; +/// Index this as follows: +/// +/// Given the following MISMATCH +/// PX/(P*)Y then the penalty is given +/// by index [P][X][Y] +const PENALTY_TABLE: [[[f64; 4]; 4]; 4] = [ + // AX/TY + [ + // X = A + [0.61, 0.0, 0.14, 0.88], + // X = T + [0.0, 0.69, 0.07, 0.73], + // X = G + [0.02, 0.71, -0.13, 0.0], + // X = C + [0.77, 0.64, 0.0, 1.33], + ], + // TX/AY + [ + [0.69, 0.0, 0.42, 0.92], + [0.0, 0.68, 0.34, 0.75], + [0.74, 0.43, 0.44, 0.0], + [1.33, 0.97, 0.0, 1.05], + ], + // GX/CY + [ + [0.17, 0.0, -0.25, 0.81], + [0.0, 0.45, -0.59, 0.98], + [-0.52, 0.08, -1.11, 0.0], + [0.47, 0.62, 0.0, 0.79], + ], + // CX/GY + [ + [0.43, 0.0, 0.03, 0.75], + [0.0, -0.12, -0.32, 0.40], + [0.11, -0.47, -0.11, 0.0], + [0.79, 0.62, 0.0, 0.70], + ], +]; + pub enum LoopKind { Internal = 0, Bulge = 1, @@ -159,52 +199,25 @@ fn good_match(a: &DnaNucleotideBase, b: &DnaNucleotideBase) -> bool { a.connects_to() == *b } -/// Index this as follows: -/// -/// Given the following MISMATCH -/// PX/(P*)Y then the penalty is given -/// by index [P][X][Y] -const PENALTY_TABLE: [[[f64; 4]; 4]; 4] = [ - // AX/TY - [ - // X = A - [0.61, 0.0, 0.14, 0.88], - // X = T - [0.0, 0.69, 0.07, 0.73], - // X = G - [0.02, 0.71, -0.13, 0.0], - // X = C - [0.77, 0.64, 0.0, 1.33], - ], - // TX/AY - [ - [0.69, 0.0, 0.42, 0.92], - [0.0, 0.68, 0.34, 0.75], - [0.74, 0.43, 0.44, 0.0], - [1.33, 0.97, 0.0, 1.05], - ], - // GX/CY - [ - [0.17, 0.0, -0.25, 0.81], - [0.0, 0.45, -0.59, 0.98], - [-0.52, 0.08, -1.11, 0.0], - [0.47, 0.62, 0.0, 0.79], - ], - // CX/GY - [ - [0.43, 0.0, 0.03, 0.75], - [0.0, -0.12, -0.32, 0.40], - [0.11, -0.47, -0.11, 0.0], - [0.79, 0.62, 0.0, 0.70], - ], -]; - /// Calculate the penalty introduced by a single mismatch #[inline(always)] fn calc_penalty(prior: &DnaNucleotideBase, x: &DnaNucleotideBase, y: &DnaNucleotideBase) -> f64 { PENALTY_TABLE[*prior as usize][*x as usize][*y as usize] } +/// IMPORTANT: This function assumes that there is a mismatch +fn calculate_mismatch_penalty( + (a1, a2): (&DnaNucleotideBase, &DnaNucleotideBase), + (b1, b2): (&DnaNucleotideBase, &DnaNucleotideBase), +) -> f64 { + match good_match(a1, b1) { + // Case 1: PX/(P*)Y + true => calc_penalty(a1, a2, b2), + // Case 2: XP/Y(P*) + false => calc_penalty(b2, b1, a1), + } +} + fn dealta_g_with_penalty( dna_a: Vec, dna_b: Vec, @@ -221,22 +234,12 @@ fn dealta_g_with_penalty( for (a, b) in std::iter::zip(a_windows, b_windows) { let (a1, a2) = (a[0], a[1]); let (b1, b2) = (b[0], b[1]); - if good_match(&a2, &b2) && good_match(&a1, &b1) { let (ndg, nds) = dG_dS(&a1, &a2); - println!("{:?}{:?}/{:?}{:?} = {ndg}", a1, a2, b1, b2); dg += ndg; ds += nds; - } else if good_match(&a2, &b2) { - // [0.11, 0.0, -0.11, -0.47], - - let p = PENALTY_TABLE[b2 as usize][b1 as usize][a1 as usize]; - println!("{:?}{:?}/{:?}{:?} = {p}", b2, b1, a2, a1); - dg += p; - } else if good_match(&a1, &b1) { - let p = PENALTY_TABLE[a1 as usize][a2 as usize][b2 as usize]; - println!("{:?}{:?}/{:?}{:?} = {p}", a1, a2, b1, b2); - dg += PENALTY_TABLE[a1 as usize][a2 as usize][b2 as usize]; + } else { + dg += calculate_mismatch_penalty((&a1, &a2), (&b1, &b2)); } } From c22dcedf0e0ba944a8baf034669c9ae062093f2a Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:08:52 +0100 Subject: [PATCH 099/117] Add function to handle both cases (one sequence and two) --- rgrow/src/utils.rs | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index af7dd43..be595e4 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -177,17 +177,35 @@ fn dG_dS(a: &DnaNucleotideBase, b: &DnaNucleotideBase) -> (f64, f64) { } } -/// Given some dna sequence eg TAGGCGTA, find dG +/// Get the binding strength of two sequences +/// +/// Right now this function can handle: +/// - Single Mismatches +/// +/// It can not yet handle: +/// - Many mismatches back to back +/// - Mismatches at end +/// +/// +/// If only one dna is provided, then this function will +/// use the given dna sequence eg TAGGCGTA to find dG /// of said sequence with its "perfect fit" /// (in this case ATCCGCAT) /// /// the sum of all neighbours a, b -- dG_(37 degrees C) (a, b) - (temperature - 37) dS(a, b) -fn dna_strength(dna: impl Iterator, temperature: f64) -> f64 { - let (total_dg, total_ds) = dna_dg_ds(dna); +fn sequences_strength( + dna: Vec, + other_dna: Option>, + temperature: f64, +) -> f64 { + let (total_dg, total_ds) = match other_dna { + None => single_sequence_dg_ds(dna.into_iter()), + Some(other) => sequence_pair_dg_ds(dna, other), + }; (total_dg + PENALTY_G) - (temperature - 37.0) * (total_ds + PENALTY_S) } -fn dna_dg_ds(dna: impl Iterator) -> (f64, f64) { +fn single_sequence_dg_ds(dna: impl Iterator) -> (f64, f64) { two_window_fold(dna, |(acc_dg, acc_ds), (a, b)| { let (dg, ds) = dG_dS(a, b); (dg + acc_dg, ds + acc_ds) @@ -218,10 +236,7 @@ fn calculate_mismatch_penalty( } } -fn dealta_g_with_penalty( - dna_a: Vec, - dna_b: Vec, -) -> (f64, f64) { +fn sequence_pair_dg_ds(dna_a: Vec, dna_b: Vec) -> (f64, f64) { if dna_a.len() != dna_b.len() { panic!("Dnas must be same length to compare"); } @@ -248,7 +263,7 @@ fn dealta_g_with_penalty( #[cfg_attr(feature = "python", pyfunction)] pub fn string_dna_dg_ds(dna_sequence: &str) -> (f64, f64) { - dna_dg_ds(dna_sequence.chars().map(DnaNucleotideBase::from)) + single_sequence_dg_ds(dna_sequence.chars().map(DnaNucleotideBase::from)) } /// Get delta g for some string dna sequence and its "perfect match". For example: @@ -260,9 +275,10 @@ pub fn string_dna_dg_ds(dna_sequence: &str) -> (f64, f64) { /// ``` /// pub fn string_dna_delta_g(dna_sequence: &str, temperature: f64) -> f64 { - dna_strength( + sequences_strength( // Convert dna_sequence string into an iterator of nucleotide bases - dna_sequence.chars().map(DnaNucleotideBase::from), + dna_sequence.chars().map(DnaNucleotideBase::from).collect(), + None, temperature, ) } @@ -290,7 +306,7 @@ pub fn loop_penalty(length: usize, kind: &str) -> f64 { #[cfg(test)] mod test_utils { - use crate::utils::dealta_g_with_penalty; + use crate::utils::sequence_pair_dg_ds; use crate::utils::LOOP_TABLE; use super::string_dna_dg_ds; @@ -427,7 +443,7 @@ mod test_utils { fn test_mismatch_penalty() { let dna_a = "GGACTGACG".chars().map(DnaNucleotideBase::from).collect(); let dna_b = "CCTGGCTGC".chars().map(DnaNucleotideBase::from).collect(); - let (total, _) = dealta_g_with_penalty(dna_a, dna_b); + let (total, _) = sequence_pair_dg_ds(dna_a, dna_b); assert_eq!(total + 1.96, -8.32); } @@ -435,7 +451,7 @@ mod test_utils { fn test_no_mismatches() { let dna_a = "GGACTGAC".chars().map(DnaNucleotideBase::from).collect(); let dna_b = DnaNucleotideBase::ideal_sequence(&dna_a); - let (g, s) = dealta_g_with_penalty(dna_a, dna_b); + let (g, s) = sequence_pair_dg_ds(dna_a, dna_b); let (pg, ps) = string_dna_dg_ds("GGACTGAC"); assert_eq!(g, pg); assert_eq!(s, ps); From c09bd5cc915d698da911f05a65e825761b6f9e68 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:02:22 +0100 Subject: [PATCH 100/117] DNA sequence delta g delta s with internal loop --- rgrow/src/utils.rs | 52 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index be595e4..ea7e070 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -223,16 +223,18 @@ fn calc_penalty(prior: &DnaNucleotideBase, x: &DnaNucleotideBase, y: &DnaNucleot PENALTY_TABLE[*prior as usize][*x as usize][*y as usize] } -/// IMPORTANT: This function assumes that there is a mismatch -fn calculate_mismatch_penalty( +fn calculate_single_mismatch_penalty( (a1, a2): (&DnaNucleotideBase, &DnaNucleotideBase), (b1, b2): (&DnaNucleotideBase, &DnaNucleotideBase), ) -> f64 { - match good_match(a1, b1) { + if good_match(a1, b1) { // Case 1: PX/(P*)Y - true => calc_penalty(a1, a2, b2), + calc_penalty(a1, a2, b2) + } else if good_match(a2, b2) { // Case 2: XP/Y(P*) - false => calc_penalty(b2, b1, a1), + calc_penalty(b2, b1, a1) + } else { + 0.0 } } @@ -241,8 +243,8 @@ fn sequence_pair_dg_ds(dna_a: Vec, dna_b: Vec, dna_b: Vec 2 { + dg += loop_penalty(current_loop_length - 1, "internal"); + } + current_loop_length = 0; } else { - dg += calculate_mismatch_penalty((&a1, &a2), (&b1, &b2)); + dg += calculate_single_mismatch_penalty((&a1, &a2), (&b1, &b2)); + current_loop_length += 1; } } - (dg, ds) } @@ -288,7 +296,7 @@ fn _loop_penalty(length: usize, kind: LoopKind) -> f64 { .iter() .zip(LENGTHS) .rev() - .find(|(_, len)| len < &length) + .find(|(_, len)| len <= &length) .expect("Please enter a valid length"); g_diff + R * (length as f64 / (len as f64)).ln() * 2.44 * 310.15 @@ -456,4 +464,30 @@ mod test_utils { assert_eq!(g, pg); assert_eq!(s, ps); } + + #[test] + fn internal_loop_mismatch() { + /* + * Make sure that the way I calculated -0.17 by hand is right + * + * A G C T G + * A T T | | | | | G T C + * | | | | | | + * T A A | | | | | C A G + * G A T G A + * + * Delta G = + * G(A, T) + G(T, T) + * + SingleMismatch(T A / A G) + * + InternalLoop(5) + * + SingleMismatch(G G / A C) + * + G(G, T) + G(T, C) + * = -0.17 + * */ + let (g, _) = sequence_pair_dg_ds( + "attagctggtc".chars().map(DnaNucleotideBase::from).collect(), + "taagatgacag".chars().map(DnaNucleotideBase::from).collect(), + ); + approx::assert_relative_eq!(g, -0.17); + } } From 568bff697c1360f7c38c7943e00986224ccc7d92 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:02:56 +0100 Subject: [PATCH 101/117] docs update --- rgrow/src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rgrow/src/utils.rs b/rgrow/src/utils.rs index ea7e070..0e4fe34 100644 --- a/rgrow/src/utils.rs +++ b/rgrow/src/utils.rs @@ -181,9 +181,9 @@ fn dG_dS(a: &DnaNucleotideBase, b: &DnaNucleotideBase) -> (f64, f64) { /// /// Right now this function can handle: /// - Single Mismatches +/// - Many mismatches back to back /// /// It can not yet handle: -/// - Many mismatches back to back /// - Mismatches at end /// /// From 2e9fdc7103bfe307166bd8a282bcabe8fbc62839 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Mon, 5 Aug 2024 16:28:05 +0100 Subject: [PATCH 102/117] fast partition function, some name changes --- rgrow/src/models/sdc1d.rs | 101 +++++++++++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 7 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 2dfcf9f..279c429 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -478,15 +478,92 @@ impl SDC { (-g_a / self.rtval()).exp() } - pub fn sum_systems(&self) -> f64 { + pub fn partition_function_full(&self) -> f64 { self.system_states() .iter() .map(|attachments| self.boltzman_function(attachments)) .sum() } - pub fn probabilty(&self, system: &Vec) -> f64 { - let sum_z = self.sum_systems(); + /// + /// Notes: + /// - This only works for a single scaffold type. + pub fn partition_function_fast(&self) -> f64 { + let scaffold = self.scaffold(); + + let max_competition = scaffold + .iter() + .map(|x| self.friends_btm.get(x).map(|y| y.len()).unwrap_or(0)) + .max() + .unwrap(); + + let mut z_curr = Array1::zeros(max_competition); + let mut z_prev = Array1::zeros(max_competition); + let mut z_sum = 1.0; + let mut sum_a = 0.0; + + for (i, b) in scaffold.iter().enumerate() { + // This is the partial partition function assuming that the previous site is empty: + // it sums previous, previous partition functions (location i-2). + sum_a += z_prev.sum(); + + // We now move the previous (location i-1) location partial partition functions to the previous + // array, and reset the current arry. + z_prev.assign(&z_curr); + z_curr.fill(0.); + + let friends = match self.friends_btm.get(b) { + Some(f) => f, + None => continue, + }; + + // Iterating through each possible attachment at the current location. + for (j, &f) in friends.iter().enumerate() { + let attachment_beta_dg = self.scaffold_energy_bonds[f as usize] + - (self.strand_concentration[f as usize] / U0).ln(); + + let t1 = (-attachment_beta_dg).exp(); + + if i == 0 { + // First scaffold site. + // The partition function, given f attached at j, is all we need to calculate. + // z_sum has 1 in it right now, which covers the case where nothing is attached. + // sum_a has 0, because it is not being used yet. + z_curr[j] = t1; + } else { + // Every other scaffold site + // t2 will hold the different cases where side i-1 has tile g in it. + let mut t2 = 0.; + + match self.friends_btm.get(&scaffold[i - 1]) { + Some(ff) => { + for (k, &g) in ff.iter().enumerate() { + let left_beta_dg = + self.strand_energy_bonds[(g as usize, f as usize)]; + t2 += z_prev[k] * (-left_beta_dg).exp(); + } + } + None => {} + } + + // 1.0 -> *only* tile f is attached at position i. + // sum_a -> tile f is at position i, no tile is at position i-1. + // t2 -> tile f is at position i, another tile is at position i-1. + z_curr[j] = t1 * (1.0 + t2 + sum_a); + } + z_sum += z_curr[j]; + } + } + + z_sum + } + + pub fn partition_function(&self) -> f64 { + self.partition_function_fast() + } + + pub fn probability_of_state(&self, system: &Vec) -> f64 { + let sum_z = self.partition_function_fast(); let this_system = self.boltzman_function(system); this_system / sum_z } @@ -1231,7 +1308,7 @@ impl SDC { } fn partition(&self) -> f64 { - self.sum_systems() + self.partition_function_full() } fn distribution(&self) -> Vec { @@ -1239,7 +1316,7 @@ impl SDC { let mut probability = self .system_states() .iter() - .map(|sys| self.probabilty(sys)) + .map(|sys| self.probability_of_state(sys)) .collect::>(); probability.sort_unstable_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)); @@ -1271,7 +1348,7 @@ impl SDC { let systems = self.system_states(); let mut triples = Vec::new(); for s in systems { - let prob = self.probabilty(&s); + let prob = self.probability_of_state(&s); let energy = self.boltzman_function(&s); triples.push((s, prob, energy)); } @@ -1281,6 +1358,16 @@ impl SDC { }); triples } + + #[pyo3(name = "partition_function")] + fn py_partition_function(&self) -> f64 { + self.partition_function_fast() + } + + #[pyo3(name = "partition_function_full")] + fn py_partition_function_full(&self) -> f64 { + self.partition_function_full() + } } #[cfg(test)] @@ -1707,7 +1794,7 @@ mod test_sdc_model { let mut probs = systems .iter() - .map(|s| (s.clone(), sdc.probabilty(s))) + .map(|s| (s.clone(), sdc.probability_of_state(s))) .collect::>(); probs.sort_by(|(_, p1), (_, p2)| { From 6c99af3713037ca45e8ed470c5f9bdd6720776bb Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Tue, 6 Aug 2024 11:38:20 +0100 Subject: [PATCH 103/117] sdc: add multi-precision float calculation to log for partition function. --- rgrow/Cargo.toml | 2 + rgrow/src/models/sdc1d.rs | 101 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/rgrow/Cargo.toml b/rgrow/Cargo.toml index 0720e6f..1687642 100644 --- a/rgrow/Cargo.toml +++ b/rgrow/Cargo.toml @@ -70,6 +70,8 @@ polars = { workspace = true } pyo3-polars = {workspace = true} approx = { workspace = true } bincode = "1" +rug = {version = "^1.25", features = ["num-traits"]} +az = "1.2.1" [dependencies.clap] version = "4" diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 279c429..322e293 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -21,6 +21,9 @@ use std::collections::{HashMap, HashSet}; use rand::Rng; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use az::Az; +use rug::Float; + use crate::{ base::{Energy, Glue, GrowError, Rate, Tile}, canvas::{PointSafe2, PointSafeHere}, @@ -558,6 +561,84 @@ impl SDC { z_sum } + /// + /// Notes: + /// - This only works for a single scaffold type. + pub fn log_big_partition_function_fast(&self) -> f64 { + let scaffold = self.scaffold(); + + let PREC = 64; + + let max_competition = scaffold + .iter() + .map(|x| self.friends_btm.get(x).map(|y| y.len()).unwrap_or(0)) + .max() + .unwrap(); + + let mut z_curr = Array1::from_elem(max_competition, Float::with_val(PREC, 0.)); + let mut z_prev = Array1::from_elem(max_competition, Float::with_val(PREC, 0.)); + let mut z_sum = Float::with_val(PREC, 1.0); + let mut sum_a = Float::with_val(PREC, 0.0); + + for (i, b) in scaffold.iter().enumerate() { + // This is the partial partition function assuming that the previous site is empty: + // it sums previous, previous partition functions (location i-2). + for v in z_prev.iter() { + sum_a += v; + } + + // We now move the previous (location i-1) location partial partition functions to the previous + // array, and reset the current arry. + z_prev.assign(&z_curr); + z_curr.fill(Float::with_val(PREC, 0.)); + + let friends = match self.friends_btm.get(b) { + Some(f) => f, + None => continue, + }; + + // Iterating through each possible attachment at the current location. + for (j, &f) in friends.iter().enumerate() { + let attachment_beta_dg = self.scaffold_energy_bonds[f as usize] + - (self.strand_concentration[f as usize] / U0).ln(); + + let t1 = Float::with_val(PREC, -attachment_beta_dg).exp(); + + if i == 0 { + // First scaffold site. + // The partition function, given f attached at j, is all we need to calculate. + // z_sum has 1 in it right now, which covers the case where nothing is attached. + // sum_a has 0, because it is not being used yet. + z_curr[j] = t1; + } else { + // Every other scaffold site + // t2 will hold the different cases where side i-1 has tile g in it. + let mut t2 = Float::with_val(PREC, 0.); + + match self.friends_btm.get(&scaffold[i - 1]) { + Some(ff) => { + for (k, &g) in ff.iter().enumerate() { + let left_beta_dg = + self.strand_energy_bonds[(g as usize, f as usize)]; + t2 += + z_prev[k].clone() * Float::with_val(PREC, -left_beta_dg).exp(); + } + } + None => {} + } + + // 1.0 -> *only* tile f is attached at position i. + // sum_a -> tile f is at position i, no tile is at position i-1. + // t2 -> tile f is at position i, another tile is at position i-1. + z_curr[j] = t1 * (1.0 + t2 + sum_a.clone()); + } + z_sum += z_curr[j].clone(); + } + } + + z_sum.ln().az() + } + pub fn partition_function(&self) -> f64 { self.partition_function_fast() } @@ -1368,6 +1449,26 @@ impl SDC { fn py_partition_function_full(&self) -> f64 { self.partition_function_full() } + + #[pyo3(name = "probability_of_state")] + fn py_probability_of_state(&self, state: Vec) -> f64 { + self.probability_of_state(&state) + } + + #[pyo3(name = "state_g")] + fn py_state_g(&self, state: Vec) -> f64 { + self.g_system(&state) + } + + #[pyo3(name = "rtval")] + fn py_rtval(&self) -> f64 { + self.rtval() + } + + #[pyo3(name = "log_big_partition_function")] + fn py_log_big_partition_function(&self) -> f64 { + self.log_big_partition_function_fast() + } } #[cfg(test)] From 5552e7e910679812af5f833fb5edd3d080352aec Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:16:12 +0100 Subject: [PATCH 104/117] MFE -- not yet tested --- rgrow/src/models/sdc1d.rs | 66 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 322e293..19c554d 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -16,7 +16,10 @@ macro_rules! type_alias { * */ use core::f64; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + ops::Deref, +}; use rand::Rng; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; @@ -650,6 +653,67 @@ impl SDC { } } +// MFE of system +// FIXME: Hashset needs some sort of ordering (by tile id? Will that be consistent between runs?) +impl SDC { + /// Given some set of strands xi (see the graph below), and some tile for the + /// y position, find the best match + /// + /// x2 + /// x1 + /// __ x0 y __ + /// + /// Ideal bond = x1 y + /// + /// Return energy in the ideal case + fn best_energy_for_right_strand(&self, left_possible: &Vec<(f64, Tile)>, right: &Tile) -> f64 { + let right = *right as usize; + if left_possible.is_empty() { + return self.scaffold_energy_bonds[right]; + } + + left_possible + .iter() + .fold(f64::MAX, |acc, &(lenergy, left)| { + let nenergy = lenergy + self.strand_energy_bonds[(left as usize, right as usize)]; + acc.min(nenergy) + }) + + self.scaffold_energy_bonds[right] + } + + /// This is for the standard case where the acc is not empty and the friends here hashset is + /// not empty + fn mfe_next_vector( + &self, + acc: Vec<(f64, Tile)>, + friends_here: &HashSet, + ) -> Vec<(f64, Tile)> { + friends_here + .iter() + .map(|tile| (self.best_energy_for_right_strand(&acc, tile), *tile)) + .collect() + } + + /// Next vector in the case that the accumulator is empty (meaning this is the first set of + /// strand in the system, in a system with strands everywhere, this will be the 3rd index) + fn mfe_next_vector_empty_case(&self, friends_here: &HashSet) -> Vec<(f64, Tile)> { + friends_here + .iter() + .map(|&tile| (self.scaffold_energy_bonds[tile as usize], tile)) + .collect() + } + + fn mfe(&self) { + self.scaffold() + .iter() + .fold(vec![], |acc, glue| match self.friends_btm.get(glue) { + Some(friends_here) if !acc.is_empty() => self.mfe_next_vector(acc, friends_here), + Some(friends_here) => self.mfe_next_vector_empty_case(friends_here), + None => acc.into_iter().map(|(lenergy, _)| (lenergy, 0)).collect(), + }); + } +} + impl System for SDC { fn update_after_event(&self, state: &mut St, event: &Event) { match event { From 32c1a96511c626d8e9fd8dfcd697887a71e2c871 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:58:30 +0100 Subject: [PATCH 105/117] extract sdc sys for tests --- rgrow/src/models/sdc1d.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 19c554d..f6d896f 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -1843,8 +1843,7 @@ mod test_sdc_model { assert_eq!(x.len(), (1 + 1).pow(2) * (1 + 1) * (2 + 1)); } - #[test] - fn probablities() { + fn scaffold_for_tests() -> SDC { let mut strands = Vec::::new(); // Anchor tile @@ -1949,7 +1948,12 @@ mod test_sdc_model { let mut sdc = SDC::from_params(sdc_params); sdc.update_system(); + sdc + } + #[test] + fn probablities() { + 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(); From 44e89752cb9b1d839a9c793ab724cd8098e25be7 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:40:42 +0100 Subject: [PATCH 106/117] Dont access matrix directly --- rgrow/src/models/sdc1d.rs | 51 +++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index f6d896f..bde1163 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -113,6 +113,14 @@ pub struct SDC { } impl SDC { + fn bond_between_strands(&self, x: Tile, y: Tile) -> f64 { + self.strand_energy_bonds[(x as usize, y as usize)] + } + + fn bond_with_scaffold(&self, x: Tile) -> f64 { + self.scaffold_energy_bonds[x as usize] + } + fn new( anchor_tiles: Vec<(PointSafe2, Tile)>, strand_names: Vec, @@ -398,9 +406,9 @@ impl SDC { state.tile_to_e(scaffold_point) as usize, ); - self.scaffold_energy_bonds[strand as usize] - + self.strand_energy_bonds[(strand as usize, e)] - + self.strand_energy_bonds[(w, strand as usize)] + self.bond_with_scaffold(strand) + + self.bond_between_strands(strand, e as Tile) + + self.bond_between_strands(w as Tile, strand) } fn scaffold(&self) -> Vec { @@ -419,10 +427,10 @@ impl SDC { } // Add the energy of the strand and the scaffold - sumg += self.scaffold_energy_bonds[*strand as usize]; + sumg += self.bond_with_scaffold(*strand); if let Some(s) = attachments.get(id + 1) { // Also add the energy between the strand and the one to its right - sumg += self.strand_energy_bonds[(*strand as usize, *s as usize)] + sumg += self.bond_between_strands(*strand, *s) }; // Take into account the penalty @@ -525,8 +533,8 @@ impl SDC { // Iterating through each possible attachment at the current location. for (j, &f) in friends.iter().enumerate() { - let attachment_beta_dg = self.scaffold_energy_bonds[f as usize] - - (self.strand_concentration[f as usize] / U0).ln(); + let attachment_beta_dg = + self.bond_with_scaffold(f) - (self.strand_concentration[f as usize] / U0).ln(); let t1 = (-attachment_beta_dg).exp(); @@ -544,8 +552,7 @@ impl SDC { match self.friends_btm.get(&scaffold[i - 1]) { Some(ff) => { for (k, &g) in ff.iter().enumerate() { - let left_beta_dg = - self.strand_energy_bonds[(g as usize, f as usize)]; + let left_beta_dg = self.bond_between_strands(g, f); t2 += z_prev[k] * (-left_beta_dg).exp(); } } @@ -602,8 +609,8 @@ impl SDC { // Iterating through each possible attachment at the current location. for (j, &f) in friends.iter().enumerate() { - let attachment_beta_dg = self.scaffold_energy_bonds[f as usize] - - (self.strand_concentration[f as usize] / U0).ln(); + let attachment_beta_dg = + self.bond_with_scaffold(f) - (self.strand_concentration[f as usize] / U0).ln(); let t1 = Float::with_val(PREC, -attachment_beta_dg).exp(); @@ -621,8 +628,7 @@ impl SDC { match self.friends_btm.get(&scaffold[i - 1]) { Some(ff) => { for (k, &g) in ff.iter().enumerate() { - let left_beta_dg = - self.strand_energy_bonds[(g as usize, f as usize)]; + let left_beta_dg = self.bond_between_strands(g, f); t2 += z_prev[k].clone() * Float::with_val(PREC, -left_beta_dg).exp(); } @@ -667,18 +673,17 @@ impl SDC { /// /// Return energy in the ideal case fn best_energy_for_right_strand(&self, left_possible: &Vec<(f64, Tile)>, right: &Tile) -> f64 { - let right = *right as usize; if left_possible.is_empty() { - return self.scaffold_energy_bonds[right]; + return self.bond_with_scaffold(*right); } left_possible .iter() .fold(f64::MAX, |acc, &(lenergy, left)| { - let nenergy = lenergy + self.strand_energy_bonds[(left as usize, right as usize)]; + let nenergy = lenergy + self.bond_between_strands(left, *right); acc.min(nenergy) }) - + self.scaffold_energy_bonds[right] + + self.bond_with_scaffold(*right) } /// This is for the standard case where the acc is not empty and the friends here hashset is @@ -699,7 +704,7 @@ impl SDC { fn mfe_next_vector_empty_case(&self, friends_here: &HashSet) -> Vec<(f64, Tile)> { friends_here .iter() - .map(|&tile| (self.scaffold_energy_bonds[tile as usize], tile)) + .map(|&tile| (self.bond_with_scaffold(tile), tile)) .collect() } @@ -798,17 +803,17 @@ impl System for SDC { } let p = PointSafe2((i, j)); - let t = state.tile_at_point(p) as usize; + let t = state.tile_at_point(p); if t == 0 { continue; } - let te = state.tile_to_e(p) as usize; - let tw = state.tile_to_w(p) as usize; + let te = state.tile_to_e(p); + let tw = state.tile_to_w(p); - let mm_e = ((te != 0) & (self.strand_energy_bonds[(t, te)] > threshold)) as usize; - let mm_w = ((tw != 0) & (self.strand_energy_bonds[(tw, t)] > threshold)) as usize; + 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; // Should we repurpose one of these to represent strand-scaffold mismatches? // These are currently impossible, but could be added in the future. From 9314c6a49c8d9da372b9703f8310726b75a7d71e Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:59:47 +0100 Subject: [PATCH 107/117] CachedResult type --- rgrow/src/models/sdc1d.rs | 63 +++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index bde1163..3146739 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -18,6 +18,7 @@ macro_rules! type_alias { use core::f64; use std::{ collections::{HashMap, HashSet}, + fmt::Debug, ops::Deref, }; @@ -52,6 +53,29 @@ const EAST_GLUE_INDEX: usize = 2; const R: f64 = 1.98720425864083 / 1000.0; // in kcal/mol/K const U0: f64 = 1.0; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CachedEnergy(Option); + +impl CachedEnergy { + pub fn new(cache: Option) -> Self { + Self(cache) + } + + pub fn with(cache: f64) -> Self { + CachedEnergy(Some(cache)) + } + + pub fn empty() -> Self { + CachedEnergy(None) + } +} + +impl Default for CachedEnergy { + fn default() -> Self { + Self::empty() + } +} + #[cfg_attr(feature = "python", pyclass)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SDC { @@ -105,9 +129,9 @@ pub struct SDC { /// This array is indexed as follows. Given strands x and y, where x is to the west of y /// (meaning that the east of x forms a bond with the west of y), the energy of said bond /// is given by energy_bonds[(x, y)] - strand_energy_bonds: Array2, + strand_energy_bonds: Array2, /// The energy with which a strand attached to scaffold - scaffold_energy_bonds: Array1, + scaffold_energy_bonds: Array1, /// Binding strength between two glues glue_links: Array2, } @@ -115,10 +139,12 @@ pub struct SDC { impl SDC { fn bond_between_strands(&self, x: Tile, y: Tile) -> f64 { self.strand_energy_bonds[(x as usize, y as usize)] + .0 + .unwrap_or(0.0) } fn bond_with_scaffold(&self, x: Tile) -> f64 { - self.scaffold_energy_bonds[x as usize] + self.scaffold_energy_bonds[x as usize].0.unwrap_or(0.0) } fn new( @@ -153,8 +179,8 @@ impl SDC { // empty for now friends_btm: HashMap::new(), glue_links: Array2::::zeros((strand_count, strand_count)), - strand_energy_bonds: Array2::::zeros((strand_count, strand_count)), - scaffold_energy_bonds: Array1::::zeros(strand_count), + strand_energy_bonds: Array2::::default((strand_count, strand_count)), + scaffold_energy_bonds: Array1::::default(strand_count), }; s.update_system(); s @@ -264,12 +290,12 @@ impl SDC { // Case 1: First strands is to the west of second // strand_f strand_s self.strand_energy_bonds[(strand_f, strand_s)] = - self.glue_links[(f_east_glue, s_west_glue)] / self.rtval(); + CachedEnergy::with(self.glue_links[(f_east_glue, s_west_glue)] / self.rtval()); // Case 2: First strands is to the east of second // strand_s strand_f self.strand_energy_bonds[(strand_s, strand_f)] = - self.glue_links[(f_west_glue, s_east_glue)] / self.rtval(); + CachedEnergy::with(self.glue_links[(f_west_glue, s_east_glue)] / self.rtval()); } // I suppose maybe we'd have weird strands with no position domain? @@ -285,7 +311,7 @@ impl SDC { // Calculate the binding strength of the strand with the scaffold self.scaffold_energy_bonds[strand_f] = - self.glue_links[(f_btm_glue, b_inverse)] / self.rtval(); + CachedEnergy::with(self.glue_links[(f_btm_glue, b_inverse)] / self.rtval()); } } @@ -931,7 +957,7 @@ impl FromTileSet for SDC { } // Just generate the stuff that will be filled by the model. - let energy_bonds = Array2::::zeros((pc.tile_names.len(), pc.tile_names.len())); + let energy_bonds = Array2::default((pc.tile_names.len(), pc.tile_names.len())); // We'll default to 64 scaffolds. let (n_scaffolds, scaffold_length) = match tileset.size { @@ -1479,14 +1505,21 @@ impl SDC { self.update_system(); } + // FIXME: Make sure to fill the cache array completely before running either of the following + // two functions + #[getter] fn get_scaffold_energy_bonds<'py>(&self, py: Python<'py>) -> Bound<'py, numpy::PyArray1> { - self.scaffold_energy_bonds.to_pyarray_bound(py) + self.scaffold_energy_bonds + .map(|x| x.0.unwrap()) + .to_pyarray_bound(py) } #[getter] fn get_strand_energy_bonds<'py>(&self, py: Python<'py>) -> Bound<'py, numpy::PyArray2> { - self.strand_energy_bonds.to_pyarray_bound(py) + self.strand_energy_bonds + .map(|x| x.0.unwrap()) + .to_pyarray_bound(py) } #[getter] @@ -1733,8 +1766,8 @@ mod test_sdc_model { entropy_matrix: array![[1., 2., 3.], [5., 1., 8.], [5., -2., 12.]], delta_g_matrix: array![[4., 1., -8.], [6., 1., 14.], [12., 21., -13.,]], temperature: 5., - strand_energy_bonds: Array2::::zeros((5, 5)), - scaffold_energy_bonds: Array1::::zeros(5), + strand_energy_bonds: Array2::default((5, 5)), + scaffold_energy_bonds: Array1::default(5), glue_links: Array2::::zeros((5, 5)), }; @@ -1811,8 +1844,8 @@ mod test_sdc_model { entropy_matrix: array![[1., 2., 3.], [5., 1., 8.], [5., -2., 12.]], delta_g_matrix: array![[4., 1., -8.], [6., 1., 14.], [12., 21., -13.,]], temperature: 50.0, - strand_energy_bonds: Array2::::zeros((5, 5)), - scaffold_energy_bonds: Array1::::zeros(5), + strand_energy_bonds: Array2::default((5, 5)), + scaffold_energy_bonds: Array1::default(5), glue_links: Array2::::zeros((5, 5)), }; // We need to fill the friends map From cb87255d22ef8ae84db5bcb4c8cb992e72a620b0 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 8 Aug 2024 14:17:05 +0100 Subject: [PATCH 108/117] Remove Custom Type -- Use Cell --- rgrow/src/models/sdc1d.rs | 53 ++++++++++++--------------------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 3146739..069269a 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -20,8 +20,10 @@ use std::{ collections::{HashMap, HashSet}, fmt::Debug, ops::Deref, + sync::OnceLock, }; +use cached::once_cell::unsync::OnceCell; use rand::Rng; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; @@ -53,29 +55,6 @@ const EAST_GLUE_INDEX: usize = 2; const R: f64 = 1.98720425864083 / 1000.0; // in kcal/mol/K const U0: f64 = 1.0; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CachedEnergy(Option); - -impl CachedEnergy { - pub fn new(cache: Option) -> Self { - Self(cache) - } - - pub fn with(cache: f64) -> Self { - CachedEnergy(Some(cache)) - } - - pub fn empty() -> Self { - CachedEnergy(None) - } -} - -impl Default for CachedEnergy { - fn default() -> Self { - Self::empty() - } -} - #[cfg_attr(feature = "python", pyclass)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SDC { @@ -129,22 +108,22 @@ pub struct SDC { /// This array is indexed as follows. Given strands x and y, where x is to the west of y /// (meaning that the east of x forms a bond with the west of y), the energy of said bond /// is given by energy_bonds[(x, y)] - strand_energy_bonds: Array2, + #[serde(skip)] + strand_energy_bonds: Array2>, /// The energy with which a strand attached to scaffold - scaffold_energy_bonds: Array1, + #[serde(skip)] + scaffold_energy_bonds: Array1>, /// Binding strength between two glues glue_links: Array2, } impl SDC { fn bond_between_strands(&self, x: Tile, y: Tile) -> f64 { - self.strand_energy_bonds[(x as usize, y as usize)] - .0 - .unwrap_or(0.0) + *self.strand_energy_bonds[(x as usize, y as usize)].get_or_init(|| 0.0) } fn bond_with_scaffold(&self, x: Tile) -> f64 { - self.scaffold_energy_bonds[x as usize].0.unwrap_or(0.0) + *self.scaffold_energy_bonds[x as usize].get_or_init(|| 0.0) } fn new( @@ -179,8 +158,8 @@ impl SDC { // empty for now friends_btm: HashMap::new(), glue_links: Array2::::zeros((strand_count, strand_count)), - strand_energy_bonds: Array2::::default((strand_count, strand_count)), - scaffold_energy_bonds: Array1::::default(strand_count), + strand_energy_bonds: Array2::default((strand_count, strand_count)), + scaffold_energy_bonds: Array1::default(strand_count), }; s.update_system(); s @@ -289,13 +268,13 @@ impl SDC { // Case 1: First strands is to the west of second // strand_f strand_s - self.strand_energy_bonds[(strand_f, strand_s)] = - CachedEnergy::with(self.glue_links[(f_east_glue, s_west_glue)] / self.rtval()); + self.strand_energy_bonds[(strand_f, strand_s)] + .set(self.glue_links[(f_east_glue, s_west_glue)] / self.rtval()); // Case 2: First strands is to the east of second // strand_s strand_f - self.strand_energy_bonds[(strand_s, strand_f)] = - CachedEnergy::with(self.glue_links[(f_west_glue, s_east_glue)] / self.rtval()); + self.strand_energy_bonds[(strand_s, strand_f)] + .set(self.glue_links[(f_west_glue, s_east_glue)] / self.rtval()); } // I suppose maybe we'd have weird strands with no position domain? @@ -310,8 +289,8 @@ impl SDC { }; // Calculate the binding strength of the strand with the scaffold - self.scaffold_energy_bonds[strand_f] = - CachedEnergy::with(self.glue_links[(f_btm_glue, b_inverse)] / self.rtval()); + self.scaffold_energy_bonds[strand_f] + .set(self.glue_links[(f_btm_glue, b_inverse)] / self.rtval()); } } From 09cfdc67811c72a761e24e5fd2212658dcd1e7ad Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 8 Aug 2024 14:44:21 +0100 Subject: [PATCH 109/117] Cache values --- rgrow/src/models/sdc1d.rs | 55 ++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 069269a..a913a6d 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -119,11 +119,29 @@ pub struct SDC { 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(|| 0.0) + *self.strand_energy_bonds[(x as usize, y as usize)].get_or_init(|| { + let x_east_glue = self.glues[(x as usize, EAST_GLUE_INDEX)]; + let y_west_glue = self.glues[(y as usize, WEST_GLUE_INDEX)]; + self.glue_links[(x_east_glue, y_west_glue)] / self.rtval() + }) } fn bond_with_scaffold(&self, x: Tile) -> f64 { - *self.scaffold_energy_bonds[x as usize].get_or_init(|| 0.0) + *self.scaffold_energy_bonds[x as usize].get_or_init(|| { + // TODO + let x_bottom_glue = self.glues[(x as usize, BOTTOM_GLUE_INDEX)]; + if x_bottom_glue == 0 { + return 0.0; + } + + let x_inverse = if x_bottom_glue % 2 == 1 { + x_bottom_glue + 1 + } else { + x_bottom_glue - 1 + }; + + self.glue_links[(x_bottom_glue, x_inverse)] / self.rtval() + }) } fn new( @@ -169,10 +187,16 @@ impl SDC { // Note that order is important, we need to generate the glue matrix first, then using // the data generated there, the energy array is filled, etc... self.generate_glue_matrix(); - self.fill_energy_array(); + self.empty_cache(); self.generate_friends(); } + fn empty_cache(&mut self) { + let strand_count = self.strand_names.len(); + self.strand_energy_bonds = Array2::default((strand_count, strand_count)); + self.scaffold_energy_bonds = Array1::default(strand_count); + } + fn generate_friends(&mut self) { let mut friends_btm = HashMap::new(); for (t, &b) in self @@ -268,12 +292,12 @@ impl SDC { // Case 1: First strands is to the west of second // strand_f strand_s - self.strand_energy_bonds[(strand_f, strand_s)] + let _ = self.strand_energy_bonds[(strand_f, strand_s)] .set(self.glue_links[(f_east_glue, s_west_glue)] / self.rtval()); // Case 2: First strands is to the east of second // strand_s strand_f - self.strand_energy_bonds[(strand_s, strand_f)] + let _ = self.strand_energy_bonds[(strand_s, strand_f)] .set(self.glue_links[(f_west_glue, s_east_glue)] / self.rtval()); } @@ -289,7 +313,7 @@ impl SDC { }; // Calculate the binding strength of the strand with the scaffold - self.scaffold_energy_bonds[strand_f] + let _ = self.scaffold_energy_bonds[strand_f] .set(self.glue_links[(f_btm_glue, b_inverse)] / self.rtval()); } } @@ -1484,20 +1508,25 @@ impl SDC { self.update_system(); } - // FIXME: Make sure to fill the cache array completely before running either of the following - // two functions - #[getter] - fn get_scaffold_energy_bonds<'py>(&self, py: Python<'py>) -> Bound<'py, numpy::PyArray1> { + fn get_scaffold_energy_bonds<'py>( + &mut self, + py: Python<'py>, + ) -> Bound<'py, numpy::PyArray1> { + self.fill_energy_array(); self.scaffold_energy_bonds - .map(|x| x.0.unwrap()) + .map(|x| *x.get().unwrap()) .to_pyarray_bound(py) } #[getter] - fn get_strand_energy_bonds<'py>(&self, py: Python<'py>) -> Bound<'py, numpy::PyArray2> { + fn get_strand_energy_bonds<'py>( + &mut self, + py: Python<'py>, + ) -> Bound<'py, numpy::PyArray2> { + self.fill_energy_array(); self.strand_energy_bonds - .map(|x| x.0.unwrap()) + .map(|x| *x.get().unwrap()) .to_pyarray_bound(py) } From 15ed017507c2f3290e39c1aebbb7526a0bfe1069 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:55:24 +0100 Subject: [PATCH 110/117] Remove GlueLinks --- rgrow/src/models/sdc1d.rs | 52 ++++++++++----------------------------- 1 file changed, 13 insertions(+), 39 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index a913a6d..08e19aa 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -113,8 +113,6 @@ pub struct SDC { /// The energy with which a strand attached to scaffold #[serde(skip)] scaffold_energy_bonds: Array1>, - /// Binding strength between two glues - glue_links: Array2, } impl SDC { @@ -122,25 +120,23 @@ impl SDC { *self.strand_energy_bonds[(x as usize, y as usize)].get_or_init(|| { let x_east_glue = self.glues[(x as usize, EAST_GLUE_INDEX)]; let y_west_glue = self.glues[(y as usize, WEST_GLUE_INDEX)]; - self.glue_links[(x_east_glue, y_west_glue)] / self.rtval() + let glue_value = &self.delta_g_matrix[(x_east_glue, y_west_glue)] + - (self.temperature - 37.0) * &self.entropy_matrix[(x_east_glue, y_west_glue)]; + glue_value / self.rtval() }) } fn bond_with_scaffold(&self, x: Tile) -> f64 { *self.scaffold_energy_bonds[x as usize].get_or_init(|| { - // TODO - let x_bottom_glue = self.glues[(x as usize, BOTTOM_GLUE_INDEX)]; - if x_bottom_glue == 0 { + let x_bmt = self.glues[(x as usize, BOTTOM_GLUE_INDEX)]; + if x_bmt == 0 { return 0.0; } - let x_inverse = if x_bottom_glue % 2 == 1 { - x_bottom_glue + 1 - } else { - x_bottom_glue - 1 - }; - - self.glue_links[(x_bottom_glue, x_inverse)] / self.rtval() + let x_inv = if x_bmt % 2 == 1 { x_bmt + 1 } else { x_bmt - 1 }; + let glue_value = &self.delta_g_matrix[(x_bmt, x_inv)] + - (self.temperature - 37.0) * &self.entropy_matrix[(x_bmt, x_inv)]; + glue_value / self.rtval() }) } @@ -175,7 +171,6 @@ impl SDC { // These will be generated by the update_system function next, so just leave them // empty for now friends_btm: HashMap::new(), - glue_links: Array2::::zeros((strand_count, strand_count)), strand_energy_bonds: Array2::default((strand_count, strand_count)), scaffold_energy_bonds: Array1::default(strand_count), }; @@ -184,9 +179,6 @@ impl SDC { } fn update_system(&mut self) { - // Note that order is important, we need to generate the glue matrix first, then using - // the data generated there, the energy array is filled, etc... - self.generate_glue_matrix(); self.empty_cache(); self.generate_friends(); } @@ -222,13 +214,6 @@ impl SDC { self.friends_btm = friends_btm; } - /// The strenght of glues a, b is given by: - /// - /// G(a, b) = G_(37) (a,b) - (T - 37) * S(a, b) - fn generate_glue_matrix(&mut self) { - self.glue_links = &self.delta_g_matrix - (self.temperature - 37.0) * &self.entropy_matrix; - } - pub fn change_temperature_to(&mut self, celsius: f64) { self.temperature = celsius; self.update_system(); @@ -270,6 +255,7 @@ impl SDC { /// Fill the energy_bonds array fn fill_energy_array(&mut self) { let num_of_strands = self.strand_names.len(); + let glue_links = &self.delta_g_matrix - (self.temperature - 37.0) * &self.entropy_matrix; // For each *possible* pair of strands, calculate the energy bond for strand_f in 1..(num_of_strands as usize) { // 1: no point in calculating for 0 @@ -293,12 +279,12 @@ impl SDC { // Case 1: First strands is to the west of second // strand_f strand_s let _ = self.strand_energy_bonds[(strand_f, strand_s)] - .set(self.glue_links[(f_east_glue, s_west_glue)] / self.rtval()); + .set(glue_links[(f_east_glue, s_west_glue)] / self.rtval()); // Case 2: First strands is to the east of second // strand_s strand_f let _ = self.strand_energy_bonds[(strand_s, strand_f)] - .set(self.glue_links[(f_west_glue, s_east_glue)] / self.rtval()); + .set(glue_links[(f_west_glue, s_east_glue)] / self.rtval()); } // I suppose maybe we'd have weird strands with no position domain? @@ -314,7 +300,7 @@ impl SDC { // Calculate the binding strength of the strand with the scaffold let _ = self.scaffold_energy_bonds[strand_f] - .set(self.glue_links[(f_btm_glue, b_inverse)] / self.rtval()); + .set(glue_links[(f_btm_glue, b_inverse)] / self.rtval()); } } @@ -878,14 +864,6 @@ impl System for SDC { self.update_system(); Ok(NeededUpdate::NonZero) } - "glue_links" => { - let glue_links = value - .downcast_ref::>() - .ok_or(GrowError::WrongParameterType(name.to_string()))?; - self.glue_links.clone_from(glue_links); - self.update_system(); - Ok(NeededUpdate::NonZero) - } "temperature" => { let temperature = value .downcast_ref::() @@ -901,7 +879,6 @@ impl System for SDC { match name { "kf" => Ok(Box::new(self.kf)), "strand_concentrations" => Ok(Box::new(self.strand_concentration.clone())), - "glue_links" => Ok(Box::new(self.glue_links.clone())), "energy_bonds" => Ok(Box::new(self.strand_energy_bonds.clone())), "temperature" => Ok(Box::new(self.temperature)), _ => Err(GrowError::NoParameter(name.to_string())), @@ -988,7 +965,6 @@ impl FromTileSet for SDC { let mut sys = SDC { strand_names: pc.tile_names, glue_names: pc.glue_names, - glue_links, colors: pc.tile_colors, glues: pc.tile_edges, anchor_tiles: Vec::new(), @@ -1776,7 +1752,6 @@ mod test_sdc_model { temperature: 5., strand_energy_bonds: Array2::default((5, 5)), scaffold_energy_bonds: Array1::default(5), - glue_links: Array2::::zeros((5, 5)), }; sdc.update_system(); @@ -1854,7 +1829,6 @@ mod test_sdc_model { temperature: 50.0, strand_energy_bonds: Array2::default((5, 5)), scaffold_energy_bonds: Array1::default(5), - glue_links: Array2::::zeros((5, 5)), }; // We need to fill the friends map sdc.update_system(); From 5f3e66a5eda0b93ac81dd3b9001fe09cf3648274 Mon Sep 17 00:00:00 2001 From: Angel Cervera Roldan <48255007+angelcerveraroldan@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:24:01 +0100 Subject: [PATCH 111/117] Remove line (function stopped being found) --- rgrow/src/models/sdc1d.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 08e19aa..c8752bf 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -1380,7 +1380,7 @@ impl AnnealProtocol { sdc.temperature = *tmp; sdc.update_system(); - crate::system::System::update_all(&sdc, &mut state, &needed); + // crate::system::System::update_all(&sdc, &mut state, &needed); crate::system::System::evolve(&sdc, &mut state, bounds)?; // FIXME: This is flattening the canvas, so it doesnt work nicely // it should be Vec>, not Vec<_> From 24885cba3c33a67ada95f74e8d016d57b26adf32 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 9 Aug 2024 13:27:32 +0100 Subject: [PATCH 112/117] Use astro-float instead of rug. --- Cargo.toml | 3 -- rgrow/Cargo.toml | 3 +- rgrow/src/ffs.rs | 2 +- rgrow/src/models/mod.rs | 2 +- rgrow/src/models/sdc1d.rs | 85 +++++++++++++++++++++++++++++++-------- 5 files changed, 72 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 918c1db..87c3440 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ serde = {version = "^1.0.185", features=["derive", "rc"]} pyo3 = {version = "^0.21", features = ["extension-module", "multiple-pymethods"]} rayon = { version = "1" } numpy = "^0.21" -enum_dispatch = "0.3" approx = "^0.5" pyo3-polars = "^0.15" polars = {version = "^0.41", features = ["lazy", "parquet", "product"]} @@ -22,7 +21,5 @@ edition = "2021" repository = "https://github.com/cgevans/rgrow" license = "BSD-3-Clause" categories = ["science", "simulation"] - -[profile.release] # debug = true # lto = true \ No newline at end of file diff --git a/rgrow/Cargo.toml b/rgrow/Cargo.toml index 1687642..6f74fb9 100644 --- a/rgrow/Cargo.toml +++ b/rgrow/Cargo.toml @@ -70,8 +70,7 @@ polars = { workspace = true } pyo3-polars = {workspace = true} approx = { workspace = true } bincode = "1" -rug = {version = "^1.25", features = ["num-traits"]} -az = "1.2.1" +astro-float = "0.9.4" [dependencies.clap] version = "4" diff --git a/rgrow/src/ffs.rs b/rgrow/src/ffs.rs index 7f0909b..6eb7602 100644 --- a/rgrow/src/ffs.rs +++ b/rgrow/src/ffs.rs @@ -1,6 +1,6 @@ #![allow(clippy::too_many_arguments)] -#[cfg(feature="python")] +#[cfg(feature = "python")] use std::ops::Deref; use std::sync::{Arc, Weak}; diff --git a/rgrow/src/models/mod.rs b/rgrow/src/models/mod.rs index 5f0cd8e..045c040 100644 --- a/rgrow/src/models/mod.rs +++ b/rgrow/src/models/mod.rs @@ -5,4 +5,4 @@ pub mod oldktam; pub mod sdc1d; -pub(self) mod fission_base; \ No newline at end of file +pub(self) mod fission_base; diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index c8752bf..8da7aab 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -23,13 +23,11 @@ use std::{ sync::OnceLock, }; +use astro_float::{BigFloat, RoundingMode, Sign}; use cached::once_cell::unsync::OnceCell; use rand::Rng; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; -use az::Az; -use rug::Float; - use crate::{ base::{Energy, Glue, GrowError, Rate, Tile}, canvas::{PointSafe2, PointSafeHere}, @@ -55,6 +53,47 @@ const EAST_GLUE_INDEX: usize = 2; const R: f64 = 1.98720425864083 / 1000.0; // in kcal/mol/K const U0: f64 = 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)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SDC { @@ -593,6 +632,10 @@ impl SDC { 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 ctx = astro_float::ctx::Context::new(PREC, RM, cc, -100000, 100000); let max_competition = scaffold .iter() @@ -600,22 +643,22 @@ impl SDC { .max() .unwrap(); - let mut z_curr = Array1::from_elem(max_competition, Float::with_val(PREC, 0.)); - let mut z_prev = Array1::from_elem(max_competition, Float::with_val(PREC, 0.)); - let mut z_sum = Float::with_val(PREC, 1.0); - let mut sum_a = Float::with_val(PREC, 0.0); + 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 sum_a = BigFloat::from_i64(1, PREC); for (i, b) in scaffold.iter().enumerate() { // This is the partial partition function assuming that the previous site is empty: // it sums previous, previous partition functions (location i-2). for v in z_prev.iter() { - sum_a += v; + sum_a = sum_a.add(v, PREC, RM); } // We now move the previous (location i-1) location partial partition functions to the previous // array, and reset the current arry. z_prev.assign(&z_curr); - z_curr.fill(Float::with_val(PREC, 0.)); + z_curr.fill(BigFloat::from_i32(0, PREC)); let friends = match self.friends_btm.get(b) { Some(f) => f, @@ -627,7 +670,7 @@ impl SDC { let attachment_beta_dg = self.bond_with_scaffold(f) - (self.strand_concentration[f as usize] / U0).ln(); - let t1 = Float::with_val(PREC, -attachment_beta_dg).exp(); + let t1 = BigFloat::from_f64(-attachment_beta_dg, PREC).exp(PREC, RM, &mut cc); if i == 0 { // First scaffold site. @@ -638,14 +681,19 @@ impl SDC { } else { // Every other scaffold site // t2 will hold the different cases where side i-1 has tile g in it. - let mut t2 = Float::with_val(PREC, 0.); + let mut t2 = BigFloat::from_f64(0., PREC); match self.friends_btm.get(&scaffold[i - 1]) { Some(ff) => { for (k, &g) in ff.iter().enumerate() { let left_beta_dg = self.bond_between_strands(g, f); - t2 += - z_prev[k].clone() * Float::with_val(PREC, -left_beta_dg).exp(); + t2 = t2.add( + &BigFloat::from_f64(-left_beta_dg, PREC) + .exp(PREC, RM, &mut cc) + .mul(&z_prev[k], PREC, RM), + PREC, + RM, + ); } } None => {} @@ -654,13 +702,18 @@ impl SDC { // 1.0 -> *only* tile f is attached at position i. // sum_a -> tile f is at position i, no tile is at position i-1. // t2 -> tile f is at position i, another tile is at position i-1. - z_curr[j] = t1 * (1.0 + t2 + sum_a.clone()); + z_curr[j] = t1.mul( + &t2.add(&BigFloat::from_i64(1, PREC), PREC, RM) + .add(&sum_a, PREC, RM), + PREC, + RM, + ); } - z_sum += z_curr[j].clone(); + z_sum = z_sum.add(&z_curr[j], PREC, RM); } } - z_sum.ln().az() + bigfloat_to_f64(&z_sum.ln(PREC, RM, &mut cc), RM) } pub fn partition_function(&self) -> f64 { From 20d73eca582fe14a77208f70efd5dc9006c371ea Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Fri, 9 Aug 2024 22:21:10 +0100 Subject: [PATCH 113/117] fix astro-float pfunc, use it for probability_of_state --- rgrow/src/models/sdc1d.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index 8da7aab..a0f1843 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -628,7 +628,7 @@ impl SDC { /// /// Notes: /// - This only works for a single scaffold type. - pub fn log_big_partition_function_fast(&self) -> f64 { + pub fn big_partition_function_fast(&self) -> BigFloat { let scaffold = self.scaffold(); let PREC = 64; @@ -645,8 +645,8 @@ impl SDC { 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 sum_a = BigFloat::from_i64(1, 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() { // This is the partial partition function assuming that the previous site is empty: @@ -712,19 +712,33 @@ impl SDC { z_sum = z_sum.add(&z_curr[j], PREC, RM); } } + z_sum + } - bigfloat_to_f64(&z_sum.ln(PREC, RM, &mut cc), RM) + pub fn log_big_partition_function_fast(&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"); // FIXME: don't keep making this + bigfloat_to_f64( + &self.big_partition_function_fast().ln(PREC, RM, &mut cc), + RM, + ) } pub fn partition_function(&self) -> f64 { self.partition_function_fast() } - pub fn probability_of_state(&self, system: &Vec) -> f64 { + pub fn probability_of_state_full(&self, system: &Vec) -> f64 { let sum_z = self.partition_function_fast(); let this_system = self.boltzman_function(system); this_system / sum_z } + + pub fn probability_of_state(&self, system: &Vec) -> f64 { + (-self.g_system(system) / self.rtval() - self.log_big_partition_function_fast()).exp() + } } // MFE of system From ca34b3e15a93661e33b7fb8126ebf3bf4874d47e Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Thu, 22 Aug 2024 23:21:33 +0100 Subject: [PATCH 114/117] allow python subclassing of SDC --- rgrow/src/models/sdc1d.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rgrow/src/models/sdc1d.rs b/rgrow/src/models/sdc1d.rs index a0f1843..57d2515 100644 --- a/rgrow/src/models/sdc1d.rs +++ b/rgrow/src/models/sdc1d.rs @@ -94,7 +94,7 @@ fn bigfloat_to_f64(big_float: &BigFloat, rounding_mode: RoundingMode) -> f64 { } } -#[cfg_attr(feature = "python", pyclass)] +#[cfg_attr(feature = "python", pyclass(subclass))] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SDC { /// The anchor tiles for each of the scaffolds @@ -1000,7 +1000,7 @@ impl FromTileSet for SDC { glue_links[(i, i)] = *strength; } for (i, j, strength) in pc.glue_links.iter() { - glue_links[(*i, *j)] = *strength; + glue_links[(*i, *j)] = *strength; } // Just generate the stuff that will be filled by the model. From 92288c1cf7be5eee03982511c1b41a257ed2a2fa Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Mon, 2 Sep 2024 22:51:31 +0100 Subject: [PATCH 115/117] Add sdc python code --- py-rgrow/pyproject.toml | 2 +- py-rgrow/rgrow/sdc.py | 27 ---- py-rgrow/rgrow/sdc/__init__.py | 4 + py-rgrow/rgrow/sdc/anneal.py | 130 ++++++++++++++++++ py-rgrow/rgrow/sdc/graphs.py | 125 +++++++++++++++++ py-rgrow/rgrow/sdc/reporter_methods/base.py | 11 ++ .../sdc/reporter_methods/fluorescence.py | 102 ++++++++++++++ .../sdc/reporter_methods/last_compute.py | 44 ++++++ .../reporter_methods/reporter_computation.py | 50 +++++++ py-rgrow/rgrow/sdc/reporter_methods/target.py | 42 ++++++ py-rgrow/rgrow/sdc/sdc.py | 111 +++++++++++++++ py-rgrow/rgrow/sdc/strand.py | 62 +++++++++ py-rgrow/tests/test_anneal.py | 33 +++++ 13 files changed, 715 insertions(+), 28 deletions(-) delete mode 100644 py-rgrow/rgrow/sdc.py create mode 100644 py-rgrow/rgrow/sdc/__init__.py create mode 100644 py-rgrow/rgrow/sdc/anneal.py create mode 100644 py-rgrow/rgrow/sdc/graphs.py create mode 100644 py-rgrow/rgrow/sdc/reporter_methods/base.py create mode 100644 py-rgrow/rgrow/sdc/reporter_methods/fluorescence.py create mode 100644 py-rgrow/rgrow/sdc/reporter_methods/last_compute.py create mode 100644 py-rgrow/rgrow/sdc/reporter_methods/reporter_computation.py create mode 100644 py-rgrow/rgrow/sdc/reporter_methods/target.py create mode 100644 py-rgrow/rgrow/sdc/sdc.py create mode 100644 py-rgrow/rgrow/sdc/strand.py create mode 100644 py-rgrow/tests/test_anneal.py diff --git a/py-rgrow/pyproject.toml b/py-rgrow/pyproject.toml index db07965..07e3409 100644 --- a/py-rgrow/pyproject.toml +++ b/py-rgrow/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "rgrow" -dependencies = ["numpy ~= 1.26", "attrs ~= 23.2", "matplotlib ~= 3.8.2", "typing_extensions"] +dependencies = ["numpy ~= 1.26", "attrs ~= 23.2", "matplotlib ~= 3.8.2", "typing_extensions", "tqdm"] requires-python = ">=3.9" [project.optional-dependencies] diff --git a/py-rgrow/rgrow/sdc.py b/py-rgrow/rgrow/sdc.py deleted file mode 100644 index deba274..0000000 --- a/py-rgrow/rgrow/sdc.py +++ /dev/null @@ -1,27 +0,0 @@ -from collections.abc import Mapping -from dataclasses import dataclass - - -@dataclass -class SDCStrand: - concentration: float - left_glue: str | None = None - btm_glue: str | None = None - right_glue: str | None = None - name: str | None = None - color: str | None = None - - -@dataclass -class SDCParams: - k_f: float - k_n: float - k_c: float - temperature: float - glue_dg_s: ( - Mapping[str | tuple[str, str], tuple[float, float] | str] - | Mapping[str, tuple[float, float] | str] - | Mapping[tuple[str, str], tuple[float, float] | str] - ) - scaffold: list[str | None] | list[list[str | None]] - strands: list[SDCStrand] diff --git a/py-rgrow/rgrow/sdc/__init__.py b/py-rgrow/rgrow/sdc/__init__.py new file mode 100644 index 0000000..cbce44f --- /dev/null +++ b/py-rgrow/rgrow/sdc/__init__.py @@ -0,0 +1,4 @@ +from .sdc import SDCParams, SDC +from .strand import SDCStrand + +__all__ = ["SDCParams", "SDC", "SDCStrand"] diff --git a/py-rgrow/rgrow/sdc/anneal.py b/py-rgrow/rgrow/sdc/anneal.py new file mode 100644 index 0000000..d2d1421 --- /dev/null +++ b/py-rgrow/rgrow/sdc/anneal.py @@ -0,0 +1,130 @@ +import numpy as np + +MIN = 60 +HOUR = MIN * 60 + + +class Anneal: + """ + An anneal protocol. + + Attributes: + initial_hold (float): How long to hold the system for before changing + temperature (in seconds) + final_hold (float): How long to hold the system for once the + temperature is finished changing (in seconds) + delta_time (float): The duration of time during which the temperature + will be changing (in seconds) + + initial_temperature (float): Temperature of the system before anneal + starts (in degrees C) + final_temperature (float): Target temperature, it will be reached at + the end of the anneal (in degrees C) + + scaffold_count (int): Number of scaffolds to simulate, the higher, + the more statistically significant, but the longer the anneal will + take to finish running + timestep (float): Simulated time cannot be continuous. How big do you + want each time jump to be ? The smaller, the more accurete the + system will be, but it will take longer. + """ + + def __init__( + self, + initial_hold: float, + initial_tmp: float, + delta_time: float, + final_tmp: float, + final_hold: float, + scaffold_count: int = 100, + timestep: float = 2.0, + ): + self.initial_hold = initial_hold + self.initial_tmp = initial_tmp + 8 + self.delta_time = delta_time + self.final_tmp = final_tmp + 8 + self.final_hold = final_hold + self.scaffold_count = scaffold_count + + # How many seconds to spend in each step + # + # By default, this is two. This means that if we have a 10 second anneal, + # The times array will look like: + # 2, 4, 6, 8, 10 + self.timestep = timestep + + @staticmethod + def standard_long_anneal(from_tmp=80, final_tmp=20, scaffold_count=100): + """ + Standard anneal with: + Rest for 10 minutes, then change temperatures linearly + for 3 hours, then rest for 45 minutes. + + The system will go from the initial temperature (default 80+8), + to the final temperature (default 20+8) over the 3 hours. + """ + # error_delta = 8 + return Anneal(10 * MIN, from_tmp, 3 * HOUR, final_tmp, 45 * MIN, scaffold_count) + + def gen_arrays(self): + """ + Generate the time and the temperature arrays + + Returns: + An array of times, + an array of temperatures + """ + steps_per_sec = 1 / self.timestep + number_of_steps = int(self.delta_time * steps_per_sec) + + delta_temperatures = np.linspace( + self.initial_tmp, self.final_tmp, int(number_of_steps + 1) + ) + initial_temp = np.repeat( + self.initial_tmp, int(self.initial_hold * steps_per_sec) - 1 + ) + ending_temp = np.repeat(self.final_tmp, int(self.final_hold * steps_per_sec)) + temperatures = np.concatenate([initial_temp, delta_temperatures, ending_temp]) + + total_time = self.initial_hold + self.final_hold + self.delta_time + times = np.arange(self.timestep, total_time + self.timestep, self.timestep) + + return times, temperatures + + +class AnnealOutputs: + """ + The output generated when a system runs an anneal + + Attributes: + system (SDC): The sdc system that was executed + canvas_arr list[list[list[int]]: + This is an array of snapshots, each snapshot contains information + about the state of the scaffolds at each point in time. + Take a snapshot with 4 compute domains, total length of 5. It would + look something like this: + [ + - - A B C D E - - + [0, 0, 1, 1, 2, 3, 6, 0, 0], + [0, 0, 1, 2, 8, 3, 6, 0, 0], + ... + [0, 0, 1, 3, 1, 3, 6, 0, 0], + [0, 0, 1, 1, 3, 3, 6, 0, 0], + ] + Each one of the inner-arrays represents one scaffold. The first two + elements will always be 0, as well as the last two elements (for + performance reasons), the third element would be the id of whatever + strand is attached to that scaffolds A domain. The fourth is the id + of whatever is attached to the B domain, ... If nothing is attached + then the number will be 0. + anneal (Anneal): The anneal that was executed (stored in the output + since some of its data is relevant when measuring error / graphing) + """ + + def __init__(self, system, anneal: Anneal, canvas_arr): + # The anneal that was executed + self.system = system + # Snapshots of the canvas + self.canvas_arr = canvas_arr + # The anneal that was executed + self.anneal = anneal diff --git a/py-rgrow/rgrow/sdc/graphs.py b/py-rgrow/rgrow/sdc/graphs.py new file mode 100644 index 0000000..f756ce6 --- /dev/null +++ b/py-rgrow/rgrow/sdc/graphs.py @@ -0,0 +1,125 @@ +from .reporter_methods import ReportingMethod +from .anneal import Anneal, AnnealOutputs +from .sdc import SDC + +import matplotlib.pyplot as plt + + +MIN = 60 +HOUR = MIN * 60 + + +def graph_system( + system: SDC, + anneal_output: AnnealOutputs, + method: ReportingMethod, + # TODO: Fix this for windows + path: str = "/tmp/sdc_image.png", +): + measurement = method.reporter_method(anneal_output) + + times, temps = anneal_output.anneal.gen_arrays() + times_hours = times / HOUR + + plt.clf() + + plt.plot(times_hours, measurement, label=system.name) + plt.xlabel("Time (hours)") + plt.ylabel(method.desc) + plt.ylim(0.0, 1.1) + + plt.legend() + + # Now plot the temperature + plt2 = plt.twinx() + plt2.plot(times_hours, temps, "k--", label="temperature C") + plt2.set_ylabel("temperature") + + plt.savefig(path) + + +def graph_system_with_many_reporting_methods( + system: SDC, + anneal_output: AnnealOutputs, + methods: list[ReportingMethod], + # TODO: Fix this for windows + path: str = "/tmp/sdc_image.png", +): + plt.clf() + times, temps = anneal_output.anneal.gen_arrays() + times_hours = times / HOUR + + for method in methods: + measurement = method.reporter_method(anneal_output) + plt.plot(times_hours, measurement, label=method.desc) + + plt.xlabel("Time (hours)") + plt.ylabel("Method Depended Error") + plt.ylim(0.0, 1.1) + + plt.legend() + + # Now plot the temperature + plt2 = plt.twinx() + plt2.plot(times_hours, temps, "k--", label="temperature C") + plt2.set_ylabel("temperature") + + plt.title(system.name) + + plt.savefig(path) + + +def run_and_graph_system( + system: SDC, + anneal: Anneal, + method: ReportingMethod, + # TODO: Fix this for windows + path: str = "/tmp/sdc_image.png", +): + graph_system(system, system.run_anneal(anneal), method, path) + + +def run_and_graph_system_with_many_reporting_methods( + system: SDC, + anneal: Anneal, + methods: list[ReportingMethod], + # TODO: Fix this for windows + path: str = "/tmp/sdc_image.png", +): + graph_system_with_many_reporting_methods( + system, system.run_anneal(anneal), methods, path + ) + + +def graph_many_systems_with( + systems: [SDC], + anneal: Anneal, + method: ReportingMethod, + # TODO: Fix this for windows + path: str = "/tmp/sdc_image.png", + title: str = None, +): + plt.clf() + times, temps = anneal.gen_arrays() + times_hours = times / HOUR + + for system in systems: + anneal_output = system.run_anneal(anneal) + measurement = method.reporter_method(anneal_output) + plt.plot(times_hours, measurement, label=system.name) + + plt.xlabel("Time (hours)") + plt.ylabel(method.desc) + plt.ylim(0.0, 1.1) + + plt.legend() + + # Now plot the temperature + plt2 = plt.twinx() + plt2.plot(times_hours, temps, "k--", label="temperature C") + plt2.set_ylabel("temperature") + + if title is not None: + plt.title(title) + + plt.savefig(path) diff --git a/py-rgrow/rgrow/sdc/reporter_methods/base.py b/py-rgrow/rgrow/sdc/reporter_methods/base.py new file mode 100644 index 0000000..43b4868 --- /dev/null +++ b/py-rgrow/rgrow/sdc/reporter_methods/base.py @@ -0,0 +1,11 @@ +from ..anneal import AnnealOutputs +from abc import ABCMeta, abstractmethod + + +class ReportingMethod(metaclass=ABCMeta): + @abstractmethod + @property + def desc(self) -> str: ... + + @abstractmethod + def reporter_method(self, anneal_outp: AnnealOutputs): ... diff --git a/py-rgrow/rgrow/sdc/reporter_methods/fluorescence.py b/py-rgrow/rgrow/sdc/reporter_methods/fluorescence.py new file mode 100644 index 0000000..c952f47 --- /dev/null +++ b/py-rgrow/rgrow/sdc/reporter_methods/fluorescence.py @@ -0,0 +1,102 @@ +from .base import ReportingMethod +from ..anneal import AnnealOutputs + +import rgrow as rg +import numpy as np + + +class Fluorescence(ReportingMethod): + """ + Reporting method: Mean fluorescence + """ + + desc = "Fluorescence" + _R = 1.98720425864083e-3 + _BC = 100e-9 + + @staticmethod + def calc_volume( + temperature, dgds, concentration_strand, concentration_quencher_or_fluorophore + ): + """ + Given temperature, dgds, concentration of a strand, and the concentration of + the fluororophore / quencher, find the volume of [Q - Q'] or [R - R'] + """ + beta = 1 / (Fluorescence._R * (temperature + 273.15)) + delta_g = dgds[0] - dgds[1] * (temperature - 37) + ep = np.exp(-delta_g * beta) + + minus_b = ( + ep * (concentration_strand + concentration_quencher_or_fluorophore) + 1 + ) + b_squared = ( + ep * (concentration_strand + concentration_quencher_or_fluorophore) + 1 + ) ** 2 + ac = ep * ep * concentration_quencher_or_fluorophore * concentration_strand + + return (minus_b - np.sqrt(b_squared - 4 * ac)) / (2 * ep) + + @staticmethod + def calc_percentages( + temperatures, dgds, concentration_strand, concentration_quencher_or_fluorophore + ): + """ + Given temperature, dgds, concentration of a strand, and the concentration of + the fluororophore / quencer, find the volume of [Q - Q'] or [R - R'] + """ + answer = [] + for temp in temperatures: + answer.append( + Fluorescence.calc_volume( + temp, + dgds, + concentration_strand, + concentration_quencher_or_fluorophore, + ) + / concentration_quencher_or_fluorophore + ) + return np.array(answer) + + @staticmethod + def _percentage_acc( + anneal_outp: AnnealOutputs, scaffold_position: int, expected_name: int + ): + scaffold_len = len(anneal_outp.canvas_arr[0]) - 4 + rgrows = anneal_outp.system.rgrow_system + expected_index = rgrows.tile_number_from_name(expected_name) + return (anneal_outp.canvas_arr[:, :, scaffold_position] == expected_index).sum( + axis=-1 + ) / scaffold_len + + def reporter_method(self, anneal_outp: AnnealOutputs): + times, temps = anneal_outp.anneal.gen_arrays() + + quencher_position_index = len(anneal_outp.canvas_arr[0][0]) - 4 + reporter_position_index = quencher_position_index + 1 + + # Check the percentage quencher_strand and reporter_strand that are + # attached to the scaffold + percentage_quencher = Fluorescence._percentage_acc( + anneal_outp, quencher_position_index, anneal_outp.system.quencher_name + ) + percentage_reporter = Fluorescence._percentage_acc( + anneal_outp, reporter_position_index, anneal_outp.system.reporter_name + ) + + attached_fluo = Fluorescence.calc_percentages( + temps, + rg.rgrow.string_dna_dg_ds("ACCATCCCTTCGCATCCCAA"), + 0.9 * Fluorescence._BC, + 0.8 * Fluorescence._BC, + ) + + attached_quench = Fluorescence.calc_percentages( + temps, + rg.rgrow.string_dna_dg_ds("ACCATCCCTTCGCATCCCAA"), + 12 * Fluorescence._BC, + 10 * Fluorescence._BC, + ) + + return 1 - ( + percentage_quencher * percentage_reporter * attached_quench * attached_fluo + ) diff --git a/py-rgrow/rgrow/sdc/reporter_methods/last_compute.py b/py-rgrow/rgrow/sdc/reporter_methods/last_compute.py new file mode 100644 index 0000000..fef1d00 --- /dev/null +++ b/py-rgrow/rgrow/sdc/reporter_methods/last_compute.py @@ -0,0 +1,44 @@ +from .base import ReportingMethod +from ..anneal import AnnealOutputs + + +class LastComputeDomain(ReportingMethod): + """ + Reporting method: + + Check percentage of scaffolds that contained the quenching strand + in the last computational domain. + """ + + desc = "Correct Computation" + + def __init__(self, last_strand_name=None): + self.last_strand_name = last_strand_name + + def reporter_method(self, anneal_outp: AnnealOutputs): + # This assumes that the scaffold looks like this: + # + # None, None, input, C1, C2, ..., Cn, Reporter, None, None + quencher_position_index = len(anneal_outp.canvas_arr[0][0]) - 4 + + # The length of the scaffold -- Minus four since the scaffold (under + # the hood) must start with two None positions, and end in two None + # positions + scaffold_len = len(anneal_outp.canvas_arr[0]) - 4 + + if self.last_strand_name is None: + strand_name = anneal_outp.system.quencher_name + else: + strand_name = self.last_strand_name + + rgrows = anneal_outp.system.rgrow_system + quencher_strand_index = rgrows.tile_number_from_name(strand_name) + + percentage_quencher = ( + ( + anneal_outp.canvas_arr[:, :, quencher_position_index] + == quencher_strand_index + ) + ).sum(axis=-1) / scaffold_len + + return percentage_quencher diff --git a/py-rgrow/rgrow/sdc/reporter_methods/reporter_computation.py b/py-rgrow/rgrow/sdc/reporter_methods/reporter_computation.py new file mode 100644 index 0000000..6c6a472 --- /dev/null +++ b/py-rgrow/rgrow/sdc/reporter_methods/reporter_computation.py @@ -0,0 +1,50 @@ +from .base import ReportingMethod +from ..anneal import AnnealOutputs + + +class ReporterAndComputational(ReportingMethod): + """ + Reporting method: + + Check that the reporter strand has attached and that the + last computational strand is correct + """ + + desc = "Reporter + Computation" + + def reporter_method(self, anneal_outp: AnnealOutputs): + # This assumes that the scaffold looks like this: + # + # None, None, input, C1, C2, ..., Cn, Reporter, None, None + quencher_position_index = len(anneal_outp.canvas_arr[0][0]) - 4 + reporter_position_index = quencher_position_index + 1 + + # The length of the scaffold -- Minus four since the scaffold (under + # the hood) must start with two None positions, and end in two None + # positions + scaffold_len = len(anneal_outp.canvas_arr[0]) - 4 + rgrows = anneal_outp.system.rgrow_system + + # Check the percentage quencher_strand attached + quencher_strand_index = rgrows.tile_number_from_name( + anneal_outp.system.quencher_name + ) + percentage_quencher = ( + ( + anneal_outp.canvas_arr[:, :, quencher_position_index] + == quencher_strand_index + ) + ).sum(axis=-1) / scaffold_len + + # Check the percentage reporter attached + reporter_strand_index = rgrows.tile_number_from_name( + anneal_outp.system.reporter_name + ) + percentage_reporter = ( + ( + anneal_outp.canvas_arr[:, :, reporter_position_index] + == reporter_strand_index + ) + ).sum(axis=-1) / scaffold_len + + return percentage_quencher * percentage_reporter diff --git a/py-rgrow/rgrow/sdc/reporter_methods/target.py b/py-rgrow/rgrow/sdc/reporter_methods/target.py new file mode 100644 index 0000000..592494f --- /dev/null +++ b/py-rgrow/rgrow/sdc/reporter_methods/target.py @@ -0,0 +1,42 @@ +from .base import ReportingMethod +from ..anneal import AnnealOutputs + +import numpy as np + + +class Target(ReportingMethod): + """ + Reporting method: + + Check if the scaffold matches some array exactly + """ + + desc = "Target" + + def __init__(self, target_names: list[str]): + self.target_names = target_names + + def reporter_method(self, anneal_outp: AnnealOutputs): + target_ids = np.array( + ( + [0, 0] + + [ + anneal_outp.system.rgrow_system.tile_number_from_name(name) + for name in self.target_names + ] + + [0, 0] + ) + ) + + target_percentage = np.zeros(len(anneal_outp.canvas_arr)) + i = 0 + for snapshot in anneal_outp.canvas_arr: + correct_target = 0 + for scaffold in snapshot: + if (scaffold == target_ids).all(): + correct_target += 1 + + target_percentage[i] = correct_target / len(snapshot) + i += 1 + + return target_percentage diff --git a/py-rgrow/rgrow/sdc/sdc.py b/py-rgrow/rgrow/sdc/sdc.py new file mode 100644 index 0000000..9cb1299 --- /dev/null +++ b/py-rgrow/rgrow/sdc/sdc.py @@ -0,0 +1,111 @@ +from typing import Mapping +import rgrow as rg +import numpy as np + +from .anneal import Anneal, AnnealOutputs +import tqdm +import dataclasses +import json +from .strand import SDCStrand + + +@dataclasses.dataclass +class SDCParams: + """ + Parameters used to create an SDC system + """ + + k_f: float + temperature: float + glue_dg_s: ( + Mapping[str | tuple[str, str], tuple[float, float] | str] + | Mapping[str, tuple[float, float] | str] + | Mapping[tuple[str, str], tuple[float, float] | str] + ) + scaffold: list[str | None] | list[list[str | None]] + strands: list[SDCStrand] + scaffold_concentration: float = 1e-100 + k_n: float = 0.0 + k_c: float = 0.0 + + def __post_init__(self) -> None: + self.scaffold = [None, None] + self.scaffold + [None, None] + + def __str__(self) -> str: + strands_info = "" + for strand in self.strands: + strands_info += "\n\t" + strand.__str__() + return f"Forward Rate: {self.k_f}\nStrands: {strands_info}\nScaffold: {', '.join(self.scaffold[2:-2])}" + + def to_dict(self) -> dict: + return dataclasses.asdict(self) + + def write_json(self, filename: str) -> None: + with open(filename, "w") as f: + json.dump(self.to_dict(), f) + + @classmethod + def from_dict(cls, d: dict) -> "SDCParams": + if "strands" in d: + d["strands"] = [SDCStrand(**strand) for strand in d["strands"]] + if "scaffold" in d: + if ( + d["scaffold"][0] is None + and d["scaffold"][1] is None + and d["scaffold"][-1] is None + and d["scaffold"][-2] is None + ): + d["scaffold"] = d["scaffold"][2:-2] + return cls(**d) + + @classmethod + def read_json(cls, filename: str) -> "SDCParams": + with open(filename, "r") as f: + d = json.load(f) + return cls.from_dict(d) + + +class SDC(rg.rgrow.SDC): + def __new__(cls, params, quencher_name, reporter_name, system_name): + self = super().__new__(cls, params) + self.params = params + self.quencher_name = quencher_name + self.reporter_name = reporter_name + self.name = system_name + return self + + @property + def rgrow_system(self): + return self + + def __str__(self): + header_line = f"SDC System {self.name} info:" + rep_quench = f"With reporter { + self.reporter_name} and quencher {self.quencher_name}" + strand_info = f"Parameters:\n{self.params.__str__()}" + return f"{header_line}\n{rep_quench}\n{strand_info}\n\n" + + def run_anneal(self, anneal: Anneal): + times, temperatures = anneal.gen_arrays() + scaffold_len = len(self.params.scaffold) + + # Here we will keep the state of the canvas at each point in time + canvas_arr = np.zeros( + (len(temperatures), anneal.scaffold_count, scaffold_len), dtype=int + ) + + # Now we make a state, and let the time pass ... + state = rg.State( + (anneal.scaffold_count, scaffold_len), + "square", + "none", + len(self.params.strands) + 1, + ) + + for i, t in tqdm.tqdm(enumerate(temperatures), total=len(temperatures)): + self.set_param("temperature", t) + self.update_all(state) + self.evolve(state, for_time=anneal.timestep) + canvas_arr[i, :, :] = state.canvas_view + + return AnnealOutputs(self, anneal, canvas_arr) diff --git a/py-rgrow/rgrow/sdc/strand.py b/py-rgrow/rgrow/sdc/strand.py new file mode 100644 index 0000000..3a56919 --- /dev/null +++ b/py-rgrow/rgrow/sdc/strand.py @@ -0,0 +1,62 @@ +from dataclasses import dataclass + + +@dataclass +class SDCStrand: + concentration: float = 1e-6 + left_glue: str | None = None + btm_glue: str | None = None + right_glue: str | None = None + name: str | None = None + color: str | None = None + + @staticmethod + def basic_from_string(string_representation: str): + """ + Given some simple string, generate a strand. + + For example: + - given "0A0", the strand with left glue 0*, right glue 0, and base A will be generated + - given "-B-", the strand with no glue on the left and right, and base B will be generated + """ + + return SDCStrand( + left_glue=f"{string_representation[0]}*", + btm_glue=string_representation[1], + right_glue=string_representation[2], + name=string_representation, + ) + + @staticmethod + def pair_glue_from_string(string_representation: str): + """ + Given some string, generate a strand. + + This function will only work for simple systems (that is, small systems, with not-so high complexity), the input + MUST be in the following format: f"{left_glue}{base_character}{right_glue}", where the left and right glue are + either a number, or '-' if no glue is present, and the base_character is A, or B, ..., or Z. + + Some valid strings would be "0A1", "1B1", "0K4", "-A1", "-E-" + """ + + # An even base will have an even right glue, and an odd left glue. An odd base will have an odd right glue and + # an even left glue + even_base = (ord(string_representation[1]) - ord("A")) % 2 == 0 + + l_postfix = "e" if even_base else "o" + r_postfix = "o" if even_base else "e" + + lc = string_representation[0] + rc = string_representation[2] + l_glue = None if lc == "-" else f"{lc}{l_postfix}" + r_glue = None if rc == "-" else f"{rc}{r_postfix}" + + return SDCStrand( + left_glue=l_glue, + btm_glue=string_representation[1], + right_glue=r_glue, + name=string_representation, + ) + + def __str__(self): + return f"Strand {self.name} at concentration {self.concentration}" diff --git a/py-rgrow/tests/test_anneal.py b/py-rgrow/tests/test_anneal.py new file mode 100644 index 0000000..63fe0a6 --- /dev/null +++ b/py-rgrow/tests/test_anneal.py @@ -0,0 +1,33 @@ +import numpy as np +from rgrow.sdc.anneal import Anneal + + +def test_gen_arrays(): + anneal = Anneal( + initial_hold=10, + final_hold=20, + initial_tmp=80, + delta_time=100, + final_tmp=100, + ) + + times, temps = anneal.gen_arrays() + assert len(times) == len(temps) + + +def test_times_values(): + anneal = Anneal( + initial_hold=2, + delta_time=8, + final_hold=2, + initial_tmp=100, + final_tmp=60, + ) + + times, temps = anneal.gen_arrays() + + expected_times = np.array([2, 4, 6, 8, 10, 12]) + expected_temps = np.array([100.0, 90.0, 80.0, 70.0, 60.0, 60.0]) + + assert np.array_equal(times, expected_times) + assert np.allclose(temps, expected_temps) From 7175fbea1b562be1119b84642312544dcf9fd417 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Mon, 2 Sep 2024 23:04:06 +0100 Subject: [PATCH 116/117] import fixes --- py-rgrow/rgrow/sdc/graphs.py | 2 +- py-rgrow/rgrow/sdc/reporter_methods/__init__.py | 3 +++ py-rgrow/rgrow/sdc/reporter_methods/base.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 py-rgrow/rgrow/sdc/reporter_methods/__init__.py diff --git a/py-rgrow/rgrow/sdc/graphs.py b/py-rgrow/rgrow/sdc/graphs.py index f756ce6..b8213a6 100644 --- a/py-rgrow/rgrow/sdc/graphs.py +++ b/py-rgrow/rgrow/sdc/graphs.py @@ -1,6 +1,6 @@ -from .reporter_methods import ReportingMethod from .anneal import Anneal, AnnealOutputs from .sdc import SDC +from .reporter_methods import ReportingMethod import matplotlib.pyplot as plt diff --git a/py-rgrow/rgrow/sdc/reporter_methods/__init__.py b/py-rgrow/rgrow/sdc/reporter_methods/__init__.py new file mode 100644 index 0000000..4e97435 --- /dev/null +++ b/py-rgrow/rgrow/sdc/reporter_methods/__init__.py @@ -0,0 +1,3 @@ +from .base import ReportingMethod + +__all__ = ["ReportingMethod"] diff --git a/py-rgrow/rgrow/sdc/reporter_methods/base.py b/py-rgrow/rgrow/sdc/reporter_methods/base.py index 43b4868..8bde927 100644 --- a/py-rgrow/rgrow/sdc/reporter_methods/base.py +++ b/py-rgrow/rgrow/sdc/reporter_methods/base.py @@ -3,8 +3,8 @@ class ReportingMethod(metaclass=ABCMeta): - @abstractmethod @property + @abstractmethod def desc(self) -> str: ... @abstractmethod From ae8e6f81e6d11f81c6c1a8bee6c8a597e50fc44e Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Tue, 3 Sep 2024 01:21:45 +0100 Subject: [PATCH 117/117] use Python subclass SDC, add plot_canvas to stub --- py-rgrow/rgrow/__init__.py | 20 ++++++++++---- py-rgrow/rgrow/rgrow.pyi | 54 ++++++++++++++++++++++++-------------- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/py-rgrow/rgrow/__init__.py b/py-rgrow/rgrow/__init__.py index 47c6868..95257b3 100644 --- a/py-rgrow/rgrow/__init__.py +++ b/py-rgrow/rgrow/__init__.py @@ -17,7 +17,6 @@ ATAM, KTAM, OldKTAM, - SDC, TileSet as _TileSet, EvolveOutcome, # FFSLevel, @@ -27,6 +26,7 @@ EvolveBounds, FFSStateRef, ) +from .sdc import SDC import attrs import attr @@ -68,7 +68,9 @@ def _system_name_canvas(self: "System", state: State | FFSStateRef) -> np.ndarra return a[state.canvas_view] -def _system_color_canvas(self: System, state: State | np.ndarray | FFSStateRef) -> np.ndarray: +def _system_color_canvas( + self: System, state: State | np.ndarray | FFSStateRef +) -> np.ndarray: """Returns the current canvas for state, as an array of tile colors.""" if isinstance(state, (State, FFSStateRef)): @@ -158,7 +160,11 @@ def _system_plot_canvas( tile_colors / 12.92, ((tile_colors + 0.055) / 1.055) ** 2.4, ) - lum = 0.2126 * lumcolors[:, 0] + 0.7152 * lumcolors[:, 1] + 0.0722 * lumcolors[:, 2] + lum = ( + 0.2126 * lumcolors[:, 0] + + 0.7152 * lumcolors[:, 1] + + 0.0722 * lumcolors[:, 2] + ) for i in range(i_min, i_max + 1): for j in range(j_min, j_max + 1): if cv[i, j] == 0: @@ -412,7 +418,9 @@ def ensure_state(self, n: int = 0) -> int: def check_state(self, n: int = 0) -> int: """Check that the simulation has at least n states.""" if len(self.states) < n: - raise ValueError(f"Simulation has {len(self.states)} states, but {n} were required.") + raise ValueError( + f"Simulation has {len(self.states)} states, but {n} were required." + ) return n @@ -610,7 +618,9 @@ def evolve_some( require_strong_bound=require_strong_bound, ) - def plot_state(self, state_index: int = 0, ax: "plt.Axes | None" = None) -> "plt.QuadMesh": + def plot_state( + self, state_index: int = 0, ax: "plt.Axes | None" = None + ) -> "plt.QuadMesh": """Plot a state as a pcolormesh. Returns the pcolormesh object.""" import matplotlib.pyplot as plt diff --git a/py-rgrow/rgrow/rgrow.pyi b/py-rgrow/rgrow/rgrow.pyi index 5769593..788578d 100644 --- a/py-rgrow/rgrow/rgrow.pyi +++ b/py-rgrow/rgrow/rgrow.pyi @@ -5,6 +5,7 @@ from numpy import ndarray import numpy as np import polars as pl from numpy.typing import NDArray +from matplotlib.axes import Axes class ATAM: @property @@ -249,17 +250,18 @@ class ATAM: The name of the file to write to. """ - def color_canvas(self, state: State | FFSStateRef | NDArray[np.uint]) -> NDArray[np.uint8]: ... - def name_canvas(self, state: State | FFSStateRef | NDArray[np.uint]) -> NDArray[np.str_]: ... + def color_canvas( + self, state: State | FFSStateRef | NDArray[np.uint] + ) -> NDArray[np.uint8]: ... + def name_canvas( + self, state: State | FFSStateRef | NDArray[np.uint] + ) -> NDArray[np.str_]: ... class SDC: - @property def tile_names(self) -> list[str]: ... - @property def tile_colors(self) -> NDArray[np.uint]: ... - def calc_dimers(self) -> List[DimerInfo]: """ Calculate information about the dimers the system is able to form. @@ -309,8 +311,6 @@ class SDC: Calculate the location and direction of mismatches, not jus the number. """ - - @overload def evolve( self, @@ -326,7 +326,6 @@ class SDC: show_window: bool = False, parallel: bool = True, ) -> EvolveOutcome: ... - @overload def evolve( self, @@ -342,7 +341,6 @@ class SDC: show_window: bool = False, parallel: bool = True, ) -> List[EvolveOutcome]: ... - @overload def evolve( self, @@ -399,7 +397,6 @@ class SDC: def get_param(self, param_name): ... def print_debug(self): ... - @staticmethod def read_json(filename: str) -> None: """ @@ -502,13 +499,13 @@ class SDC: filename : str The name of the file to write to. """ - - def color_canvas(self, state: State | FFSStateRef | NDArray[np.uint]) -> NDArray[np.uint8]: - ... - - def name_canvas(self, state: State | FFSStateRef | NDArray[np.uint]) -> NDArray[np.str_]: - ... + def color_canvas( + self, state: State | FFSStateRef | NDArray[np.uint] + ) -> NDArray[np.uint8]: ... + def name_canvas( + self, state: State | FFSStateRef | NDArray[np.uint] + ) -> NDArray[np.str_]: ... class EvolveBounds: def __init__(self, for_time: float | None = None): ... @@ -822,16 +819,33 @@ class KTAM: filename : str The name of the file to write to. """ - def color_canvas(self, state: State | FFSStateRef | NDArray[np.uint]) -> NDArray[np.uint8]: ... - def name_canvas(self, state: State | FFSStateRef | NDArray[np.uint]) -> NDArray[np.str_]: ... + + def color_canvas( + self, state: State | FFSStateRef | NDArray[np.uint] + ) -> NDArray[np.uint8]: ... + def name_canvas( + self, state: State | FFSStateRef | NDArray[np.uint] + ) -> NDArray[np.str_]: ... + def plot_canvas( + self, + state: State | np.ndarray | FFSStateRef, + ax: "Axes" | None = None, + annotate_tiles: bool = False, + annotate_mismatches: bool = False, + crop: bool = False, + ) -> "Axes": ... class OldKTAM: @property def tile_names(self) -> list[str]: ... @property def tile_colors(self) -> NDArray[np.uint]: ... - def color_canvas(self, state: State | FFSStateRef | NDArray[np.uint]) -> NDArray[np.uint8]: ... - def name_canvas(self, state: State | FFSStateRef | NDArray[np.uint]) -> NDArray[np.str_]: ... + def color_canvas( + self, state: State | FFSStateRef | NDArray[np.uint] + ) -> NDArray[np.uint8]: ... + def name_canvas( + self, state: State | FFSStateRef | NDArray[np.uint] + ) -> NDArray[np.str_]: ... def calc_dimers(self) -> List[DimerInfo]: """ Calculate information about the dimers the system is able to form.