use rgrow::base::GrowError;
use rgrow::canvas::{Canvas, PointSafe2, PointSafeHere};
use rgrow::models::sdc1d::{GsOrSeq, RefOrPair, SDCParams, SDCStrand, SingleOrMultiScaffold};
use rgrow::models::sdc1d_bindreplace::SDC1DBindReplace;
use rgrow::state::StateEnum;
use rgrow::system::{Event, EvolveBounds, NeededUpdate, System, TileBondInfo};
use rgrow::tileset::CanvasType::SquareCompact;
use rgrow::tileset::TrackingConfig;
use rgrow::units::PerSecond;
use std::collections::HashMap;
fn make_bitcopy(n: usize, input_bit: u32) -> SDC1DBindReplace {
assert!(n >= 2);
assert!(input_bit <= 1);
let mut strands = Vec::new();
strands.push(SDCStrand {
name: Some(format!("input_{input_bit}")),
color: None,
concentration: 1e6,
btm_glue: Some("sc0".to_string()),
left_glue: None,
right_glue: Some(format!("c{input_bit}*")),
});
for i in 1..n {
for bit in 0..=1u32 {
strands.push(SDCStrand {
name: Some(format!("pos{i}_bit{bit}")),
color: None,
concentration: 1e6,
btm_glue: Some(format!("sc{i}")),
left_glue: Some(format!("c{bit}")),
right_glue: Some(format!("c{bit}*")),
});
}
}
let scaffold = (0..n).map(|i| Some(format!("sc{i}*"))).collect::<Vec<_>>();
let params = SDCParams {
strands,
quencher_name: None,
quencher_concentration: 0.0,
reporter_name: None,
fluorophore_concentration: 0.0,
scaffold: SingleOrMultiScaffold::Single(scaffold),
scaffold_concentration: 1e-100,
glue_dg_s: HashMap::new(),
k_f: 1e6,
k_n: 0.0,
k_c: 0.0,
temperature: 37.0,
junction_penalty_dg: None,
junction_penalty_ds: None,
};
SDC1DBindReplace::from_params(params)
}
fn correct_tile(pos: usize, input_bit: u32) -> u32 {
if pos == 0 {
1 } else {
let base = 2 + 2 * (pos - 1) as u32;
base + input_bit
}
}
fn wrong_tile(pos: usize, input_bit: u32) -> u32 {
correct_tile(pos, 1 - input_bit)
}
#[test]
fn test_bitcopy_empty_state_rates() {
let n = 5;
let sys = make_bitcopy(n, 0);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
for col in 0..n {
let rate = sys.event_rate_at_point(&state, PointSafeHere((0, col)));
assert_eq!(
f64::from(rate),
1.0,
"empty position {col} should have rate 1"
);
}
}
#[test]
fn test_bitcopy_perfect_filled_state_rates() {
let n = 5;
let input_bit = 0;
let sys = make_bitcopy(n, input_bit);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
for col in 0..n {
let tile = correct_tile(col, input_bit);
sys.place_tile(&mut state, PointSafe2((0, col)), tile, false)
.unwrap();
}
for col in 0..n {
let rate = sys.event_rate_at_point(&state, PointSafeHere((0, col)));
assert_eq!(
f64::from(rate),
0.0,
"position {col} should have rate 0 when perfectly filled"
);
}
}
#[test]
fn test_bitcopy_mismatched_filled_state_rates() {
let n = 5;
let input_bit = 0;
let mismatch_pos = 2;
let sys = make_bitcopy(n, input_bit);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
for col in 0..n {
let tile = if col == mismatch_pos {
wrong_tile(col, input_bit)
} else {
correct_tile(col, input_bit)
};
sys.place_tile(&mut state, PointSafe2((0, col)), tile, false)
.unwrap();
}
for col in 0..n {
let rate = sys.event_rate_at_point(&state, PointSafeHere((0, col)));
let expected = if col == mismatch_pos {
1.0
} else if col == mismatch_pos - 1 || col == mismatch_pos + 1 {
1.0
} else {
0.0
};
assert_eq!(
f64::from(rate),
expected,
"position {col} rate mismatch (mismatch at {mismatch_pos})"
);
}
}
#[test]
fn test_bitcopy_reaches_perfect_copy() {
for input_bit in [0u32, 1] {
let n = 5;
let sys = make_bitcopy(n, input_bit);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
let bounds = EvolveBounds::default().for_events(100_000);
System::evolve(&sys, &mut state, bounds).unwrap();
for col in 0..n {
let tile = state.tile_at_point(PointSafe2((0, col)));
let expected = correct_tile(col, input_bit);
assert_eq!(
tile, expected,
"input_bit={input_bit}, position {col}: expected tile {expected}, got {tile}"
);
}
}
}
fn make_bitcopy_with_energy(n: usize, input_bit: u32) -> SDC1DBindReplace {
assert!(n >= 2);
assert!(input_bit <= 1);
let mut strands = Vec::new();
strands.push(SDCStrand {
name: Some(format!("input_{input_bit}")),
color: None,
concentration: 1e6,
btm_glue: Some("sc0".to_string()),
left_glue: None,
right_glue: Some(format!("c{input_bit}*")),
});
for i in 1..n {
for bit in 0..=1u32 {
strands.push(SDCStrand {
name: Some(format!("pos{i}_bit{bit}")),
color: None,
concentration: 1e6,
btm_glue: Some(format!("sc{i}")),
left_glue: Some(format!("c{bit}")),
right_glue: Some(format!("c{bit}*")),
});
}
}
let scaffold = (0..n).map(|i| Some(format!("sc{i}*"))).collect::<Vec<_>>();
let mut glue_dg_s: HashMap<RefOrPair, GsOrSeq> = HashMap::new();
for i in 0..n {
glue_dg_s.insert(RefOrPair::Ref(format!("sc{i}")), GsOrSeq::GS((-10.0, 0.0)));
}
for bit in 0..=1u32 {
glue_dg_s.insert(RefOrPair::Ref(format!("c{bit}")), GsOrSeq::GS((-10.0, 0.0)));
}
let params = SDCParams {
strands,
quencher_name: None,
quencher_concentration: 0.0,
reporter_name: None,
fluorophore_concentration: 0.0,
scaffold: SingleOrMultiScaffold::Single(scaffold),
scaffold_concentration: 1e-100,
glue_dg_s,
k_f: 1e6,
k_n: 0.0,
k_c: 0.0,
temperature: 37.0,
junction_penalty_dg: None,
junction_penalty_ds: None,
};
let mut sys = SDC1DBindReplace::from_params(params);
sys.account_for_energy = true;
sys.physical_attachment_rate = true;
sys.update();
sys
}
#[test]
fn test_energy_empty_state_rates() {
let n = 5;
let sys = make_bitcopy_with_energy(n, 0);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
let kf = 1e6_f64;
let conc = 1e6_f64;
for col in 0..n {
let rate = f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, col))));
let expected = if col == 0 { kf * conc } else { 2.0 * kf * conc };
assert!(
(rate - expected).abs() < 1e-6 * expected,
"empty position {col}: expected {expected}, got {rate}"
);
}
}
#[test]
fn test_energy_perfect_filled_rates() {
let n = 5;
let input_bit = 0;
let sys = make_bitcopy_with_energy(n, input_bit);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
for col in 0..n {
let tile = correct_tile(col, input_bit);
sys.place_tile(&mut state, PointSafe2((0, col)), tile, false)
.unwrap();
}
for col in 0..n {
let rate = sys.event_rate_at_point(&state, PointSafeHere((0, col)));
assert_eq!(
f64::from(rate),
0.0,
"position {col} should have rate 0 when perfectly filled"
);
}
}
#[test]
fn test_energy_mismatched_rates() {
let n = 5;
let input_bit = 0;
let mismatch_pos = 2;
let sys = make_bitcopy_with_energy(n, input_bit);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
for col in 0..n {
let tile = if col == mismatch_pos {
wrong_tile(col, input_bit)
} else {
correct_tile(col, input_bit)
};
sys.place_tile(&mut state, PointSafe2((0, col)), tile, false)
.unwrap();
}
for col in 0..n {
let rate = f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, col))));
if col == mismatch_pos || col == mismatch_pos - 1 || col == mismatch_pos + 1 {
assert!(
rate > 0.0,
"position {col} should have positive rate with energy"
);
assert!(
rate < 1.0,
"position {col} rate {rate} should be much less than 1.0 due to negative ΔG"
);
} else {
assert_eq!(rate, 0.0, "position {col} should have rate 0");
}
}
}
#[test]
fn test_energy_reaches_perfect_copy() {
for input_bit in [0u32, 1] {
let n = 5;
let sys = make_bitcopy_with_energy(n, input_bit);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
let bounds = EvolveBounds::default().for_events(1_000_000);
System::evolve(&sys, &mut state, bounds).unwrap();
for col in 0..n {
let tile = state.tile_at_point(PointSafe2((0, col)));
let expected = correct_tile(col, input_bit);
assert_eq!(
tile, expected,
"energy: input_bit={input_bit}, position {col}: expected tile {expected}, got {tile}"
);
}
}
}
#[test]
fn test_no_energy_backward_compat() {
let n = 5;
let input_bit = 0;
let mismatch_pos = 2;
let sys_no_energy = make_bitcopy(n, input_bit);
let mut sys_with_data = make_bitcopy_with_energy(n, input_bit);
sys_with_data.account_for_energy = false;
sys_with_data.physical_attachment_rate = false;
let mut state1 = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys_no_energy.tile_names().len(),
)
.unwrap();
let mut state2 = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys_with_data.tile_names().len(),
)
.unwrap();
sys_no_energy.update_state(&mut state1, &NeededUpdate::All);
sys_with_data.update_state(&mut state2, &NeededUpdate::All);
for col in 0..n {
let tile = if col == mismatch_pos {
wrong_tile(col, input_bit)
} else {
correct_tile(col, input_bit)
};
sys_no_energy
.place_tile(&mut state1, PointSafe2((0, col)), tile, false)
.unwrap();
sys_with_data
.place_tile(&mut state2, PointSafe2((0, col)), tile, false)
.unwrap();
}
for col in 0..n {
let rate1 = f64::from(sys_no_energy.event_rate_at_point(&state1, PointSafeHere((0, col))));
let rate2 = f64::from(sys_with_data.event_rate_at_point(&state2, PointSafeHere((0, col))));
assert_eq!(
rate1, rate2,
"position {col}: account_for_energy=false should give same rates as no-energy system"
);
}
}
#[test]
fn test_weak_replacement_allows_less_matching() {
let n = 5;
let input_bit = 0;
let mut sys = make_bitcopy_with_energy(n, input_bit);
sys.allow_weak_replacement = true;
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
for col in 0..n {
let tile = correct_tile(col, input_bit);
sys.place_tile(&mut state, PointSafe2((0, col)), tile, false)
.unwrap();
}
for col in 0..n {
let rate = f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, col))));
if col == 0 {
assert_eq!(
rate, 0.0,
"position 0 has only one matching strand, rate should be 0"
);
} else {
assert!(
rate > 0.0,
"position {col} should have nonzero rate with allow_weak_replacement"
);
assert!(
rate < 1.0,
"position {col} rate {rate} should be small due to strong binding energy"
);
}
}
}
fn make_bitcopy_weak_replacement(n: usize, input_bit: u32, dg: f64) -> SDC1DBindReplace {
assert!(n >= 2);
assert!(input_bit <= 1);
let mut strands = Vec::new();
strands.push(SDCStrand {
name: Some(format!("input_{input_bit}")),
color: None,
concentration: 1e6,
btm_glue: Some("sc0".to_string()),
left_glue: None,
right_glue: Some(format!("c{input_bit}*")),
});
for i in 1..n {
for bit in 0..=1u32 {
strands.push(SDCStrand {
name: Some(format!("pos{i}_bit{bit}")),
color: None,
concentration: 1e6,
btm_glue: Some(format!("sc{i}")),
left_glue: Some(format!("c{bit}")),
right_glue: Some(format!("c{bit}*")),
});
}
}
let scaffold = (0..n).map(|i| Some(format!("sc{i}*"))).collect::<Vec<_>>();
let mut glue_dg_s: HashMap<RefOrPair, GsOrSeq> = HashMap::new();
for i in 0..n {
glue_dg_s.insert(RefOrPair::Ref(format!("sc{i}")), GsOrSeq::GS((dg, 0.0)));
}
for bit in 0..=1u32 {
glue_dg_s.insert(RefOrPair::Ref(format!("c{bit}")), GsOrSeq::GS((dg, 0.0)));
}
let params = SDCParams {
strands,
quencher_name: None,
quencher_concentration: 0.0,
reporter_name: None,
fluorophore_concentration: 0.0,
scaffold: SingleOrMultiScaffold::Single(scaffold),
scaffold_concentration: 1e-100,
glue_dg_s,
k_f: 1e6,
k_n: 0.0,
k_c: 0.0,
temperature: 37.0,
junction_penalty_dg: None,
junction_penalty_ds: None,
};
let mut sys = SDC1DBindReplace::from_params(params);
sys.account_for_energy = true;
sys.allow_weak_replacement = true;
sys.physical_attachment_rate = true;
sys.update();
sys
}
#[test]
fn test_weak_replacement_fills_and_evolves() {
for input_bit in [0u32, 1] {
let n = 5;
let sys = make_bitcopy_weak_replacement(n, input_bit, -5.0);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
let bounds = EvolveBounds::default().for_events(1_000);
System::evolve(&sys, &mut state, bounds).unwrap();
for col in 0..n {
let tile = state.tile_at_point(PointSafe2((0, col)));
assert_ne!(
tile, 0,
"weak_replacement: input_bit={input_bit}, position {col} should be filled"
);
}
for col in 1..n {
let rate = sys.event_rate_at_point(&state, PointSafeHere((0, col)));
assert!(
rate.0 > 0.0,
"weak_replacement: input_bit={input_bit}, position {col} should have nonzero rate"
);
}
}
}
fn make_bitcopy_with_entropy(n: usize, input_bit: u32, dg: f64, ds: f64) -> SDC1DBindReplace {
assert!(n >= 2);
assert!(input_bit <= 1);
let mut strands = Vec::new();
strands.push(SDCStrand {
name: Some(format!("input_{input_bit}")),
color: None,
concentration: 1e6,
btm_glue: Some("sc0".to_string()),
left_glue: None,
right_glue: Some(format!("c{input_bit}*")),
});
for i in 1..n {
for bit in 0..=1u32 {
strands.push(SDCStrand {
name: Some(format!("pos{i}_bit{bit}")),
color: None,
concentration: 1e6,
btm_glue: Some(format!("sc{i}")),
left_glue: Some(format!("c{bit}")),
right_glue: Some(format!("c{bit}*")),
});
}
}
let scaffold = (0..n).map(|i| Some(format!("sc{i}*"))).collect::<Vec<_>>();
let mut glue_dg_s: HashMap<RefOrPair, GsOrSeq> = HashMap::new();
for i in 0..n {
glue_dg_s.insert(RefOrPair::Ref(format!("sc{i}")), GsOrSeq::GS((dg, ds)));
}
for bit in 0..=1u32 {
glue_dg_s.insert(RefOrPair::Ref(format!("c{bit}")), GsOrSeq::GS((dg, ds)));
}
let params = SDCParams {
strands,
quencher_name: None,
quencher_concentration: 0.0,
reporter_name: None,
fluorophore_concentration: 0.0,
scaffold: SingleOrMultiScaffold::Single(scaffold),
scaffold_concentration: 1e-100,
glue_dg_s,
k_f: 1e6,
k_n: 0.0,
k_c: 0.0,
temperature: 37.0,
junction_penalty_dg: None,
junction_penalty_ds: None,
};
let mut sys = SDC1DBindReplace::from_params(params);
sys.account_for_energy = true;
sys.allow_weak_replacement = true;
sys.physical_attachment_rate = true;
sys.update();
sys
}
#[test]
fn test_bindunbind_replacement_rate() {
let n = 5;
let mut sys = make_bitcopy_with_energy(n, 0);
sys.account_for_energy = true;
sys.physical_attachment_rate = true;
sys.allow_weak_replacement = true;
sys.bindunbind_replacement_rate = false;
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
for col in 0..n {
sys.place_tile(
&mut state,
PointSafe2((0, col)),
correct_tile(col, 0),
false,
)
.unwrap();
}
sys.update_state(&mut state, &NeededUpdate::All);
let rates_detach: Vec<f64> = (0..n)
.map(|c| f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, c)))))
.collect();
sys.bindunbind_replacement_rate = true;
sys.update_state(&mut state, &NeededUpdate::All);
let rates_combined: Vec<f64> = (0..n)
.map(|c| f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, c)))))
.collect();
let kf = 1e6_f64;
let conc = 1e6_f64;
let r_attach_cascade = kf * conc;
for col in 0..n {
let r_d = rates_detach[col];
let r_c = rates_combined[col];
if col == 0 {
assert_eq!(r_d, 0.0, "pos 0: no replacer, detach rate should be 0");
assert_eq!(r_c, 0.0, "pos 0: no replacer, combined rate should be 0");
} else {
let expected = (r_d * r_attach_cascade) / (r_d + r_attach_cascade);
assert!(
(r_c - expected).abs() < 1e-10 * expected.abs(),
"pos {col}: combined={r_c}, expected={expected}, detach={r_d}"
);
assert!(
r_c <= r_d,
"pos {col}: combined rate should be <= detach rate"
);
}
}
}
#[test]
fn test_physical_attachment_rate_empty_sites() {
let n = 5;
let mut sys = make_bitcopy(n, 0);
sys.physical_attachment_rate = true;
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
let kf = 1e6_f64;
let conc = 1e6_f64;
let rate0 = f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, 0))));
assert!(
(rate0 - kf * conc).abs() < 1e-6 * (kf * conc),
"position 0: expected {}, got {rate0}",
kf * conc
);
for col in 1..n {
let rate = f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, col))));
assert!(
(rate - 2.0 * kf * conc).abs() < 1e-6 * (2.0 * kf * conc),
"position {col}: expected {}, got {rate}",
2.0 * kf * conc
);
}
}
#[test]
fn test_allow_same_replacement_increases_rate() {
let n = 5;
let input_bit = 0;
let mut sys = make_bitcopy_with_energy(n, input_bit);
sys.allow_weak_replacement = true;
sys.bindunbind_replacement_rate = true;
sys.physical_attachment_rate = true;
sys.allow_same_replacement = false;
sys.update();
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
for col in 0..n {
sys.place_tile(
&mut state,
PointSafe2((0, col)),
correct_tile(col, input_bit),
false,
)
.unwrap();
}
sys.update_state(&mut state, &NeededUpdate::All);
let rates_no_same: Vec<f64> = (0..n)
.map(|c| f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, c)))))
.collect();
assert_eq!(
rates_no_same[0], 0.0,
"pos 0: only one candidate (itself), should be filtered"
);
sys.allow_same_replacement = true;
sys.update_state(&mut state, &NeededUpdate::All);
let rates_with_same: Vec<f64> = (0..n)
.map(|c| f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, c)))))
.collect();
assert!(
rates_with_same[0] > 0.0,
"pos 0: self-replacement allowed, rate should be > 0"
);
for col in 1..n {
assert!(
rates_with_same[col] >= rates_no_same[col],
"pos {col}: rate with same ({}) should be >= without ({})",
rates_with_same[col],
rates_no_same[col]
);
}
}
#[test]
fn test_mismatch_locations_all_correct() {
let n = 5;
let input_bit = 0;
let sys = make_bitcopy(n, input_bit);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
for col in 0..n {
sys.place_tile(
&mut state,
PointSafe2((0, col)),
correct_tile(col, input_bit),
false,
)
.unwrap();
}
let mm = sys.calc_mismatch_locations(&state);
for col in 0..n {
assert_eq!(
mm[(0, col)],
0,
"position {col} should have no mismatches when all correct"
);
}
}
#[test]
fn test_mismatch_locations_single_mismatch() {
let n = 5;
let input_bit = 0;
let mismatch_pos = 2;
let sys = make_bitcopy(n, input_bit);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
for col in 0..n {
let tile = if col == mismatch_pos {
wrong_tile(col, input_bit)
} else {
correct_tile(col, input_bit)
};
sys.place_tile(&mut state, PointSafe2((0, col)), tile, false)
.unwrap();
}
let mm = sys.calc_mismatch_locations(&state);
assert_eq!(mm[(0, 0)], 0, "position 0 should have no mismatches");
assert_eq!(
mm[(0, 1)],
4,
"position 1: east mismatch with wrong tile at pos 2"
);
assert_eq!(mm[(0, 2)], 5, "position 2: both east and west mismatches");
assert_eq!(
mm[(0, 3)],
1,
"position 3: west mismatch with wrong tile at pos 2"
);
assert_eq!(mm[(0, 4)], 0, "position 4 should have no mismatches");
}
#[test]
fn test_mismatch_locations_empty_neighbors() {
let n = 5;
let input_bit = 0;
let sys = make_bitcopy(n, input_bit);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
sys.place_tile(
&mut state,
PointSafe2((0, 0)),
correct_tile(0, input_bit),
false,
)
.unwrap();
sys.place_tile(
&mut state,
PointSafe2((0, 2)),
correct_tile(2, input_bit),
false,
)
.unwrap();
let mm = sys.calc_mismatch_locations(&state);
assert_eq!(mm[(0, 1)], 0, "empty position 1 should be 0");
assert_eq!(mm[(0, 3)], 0, "empty position 3 should be 0");
assert_eq!(mm[(0, 4)], 0, "empty position 4 should be 0");
assert_eq!(
mm[(0, 0)],
0,
"position 0: east neighbor empty → no mismatch counted"
);
assert_eq!(
mm[(0, 2)],
0,
"position 2: both neighbors empty → no mismatch counted"
);
}
#[test]
fn test_set_get_param_roundtrip() {
let mut sys = make_bitcopy(5, 0);
let kf_val: f64 = *sys.get_param("kf").unwrap().downcast::<f64>().unwrap();
assert!((kf_val - 1e6).abs() < 1.0, "initial kf should be 1e6");
sys.set_param("kf", Box::new(2e6_f64)).unwrap();
let kf_val: f64 = *sys.get_param("kf").unwrap().downcast::<f64>().unwrap();
assert!((kf_val - 2e6).abs() < 1.0, "kf should be 2e6 after set");
let temp: f64 = *sys
.get_param("temperature")
.unwrap()
.downcast::<f64>()
.unwrap();
assert!(
(temp - 37.0).abs() < 0.01,
"initial temperature should be 37.0"
);
sys.set_param("temperature", Box::new(50.0_f64)).unwrap();
let temp: f64 = *sys
.get_param("temperature")
.unwrap()
.downcast::<f64>()
.unwrap();
assert!(
(temp - 50.0).abs() < 0.01,
"temperature should be 50.0 after set"
);
}
#[test]
fn test_set_param_unknown_returns_error() {
let mut sys = make_bitcopy(5, 0);
let result = sys.set_param("nonexistent", Box::new(1.0_f64));
assert!(result.is_err(), "unknown param should return error");
match result.unwrap_err() {
GrowError::NoParameter(name) => assert_eq!(name, "nonexistent"),
other => panic!("expected NoParameter, got: {other:?}"),
}
}
#[test]
fn test_set_param_kf_changes_rates() {
let n = 5;
let mut sys = make_bitcopy(n, 0);
sys.physical_attachment_rate = true;
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
let rate_before = f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, 0))));
let needed = sys.set_param("kf", Box::new(2e6_f64)).unwrap();
sys.update_state(&mut state, &needed);
let rate_after = f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, 0))));
assert!(
(rate_after - 2.0 * rate_before).abs() < 1e-6 * rate_before,
"doubling kf should double rate: before={rate_before}, after={rate_after}"
);
}
#[test]
fn test_choose_event_empty_site_returns_attachment() {
let n = 5;
let sys = make_bitcopy(n, 0);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
let (event, _rate) = sys.choose_event_at_point(&state, PointSafe2((0, 0)), PerSecond(0.0));
match event {
Event::MonomerAttachment(p, tile) => {
assert_eq!(p, PointSafe2((0, 0)));
assert_eq!(tile, 1, "should attach the input strand (tile 1)");
}
other => panic!("expected MonomerAttachment, got: {other:?}"),
}
}
#[test]
fn test_choose_event_filled_site_returns_change() {
let n = 5;
let input_bit = 0;
let mismatch_pos = 2;
let sys = make_bitcopy(n, input_bit);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
for col in 0..n {
let tile = if col == mismatch_pos {
wrong_tile(col, input_bit)
} else {
correct_tile(col, input_bit)
};
sys.place_tile(&mut state, PointSafe2((0, col)), tile, false)
.unwrap();
}
let (event, _rate) =
sys.choose_event_at_point(&state, PointSafe2((0, mismatch_pos)), PerSecond(0.0));
match event {
Event::MonomerChange(p, tile) => {
assert_eq!(p, PointSafe2((0, mismatch_pos)));
assert_eq!(
tile,
correct_tile(mismatch_pos, input_bit),
"should change to the correct tile"
);
}
other => panic!("expected MonomerChange, got: {other:?}"),
}
}
#[test]
fn test_n2_empty_rates_and_evolve() {
for input_bit in [0u32, 1] {
let n = 2;
let sys = make_bitcopy(n, input_bit);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
for col in 0..n {
let rate = f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, col))));
assert_eq!(
rate, 1.0,
"n=2 input_bit={input_bit}: empty position {col} rate should be 1.0"
);
}
let bounds = EvolveBounds::default().for_events(1_000);
System::evolve(&sys, &mut state, bounds).unwrap();
for col in 0..n {
let tile = state.tile_at_point(PointSafe2((0, col)));
let expected = correct_tile(col, input_bit);
assert_eq!(
tile, expected,
"n=2 input_bit={input_bit}, position {col}: expected {expected}, got {tile}"
);
}
}
}
#[test]
fn test_entropy_temperature_dependence() {
let n = 5;
let input_bit = 0;
let dg = -10.0;
let ds = -0.03;
let mut sys = make_bitcopy_with_entropy(n, input_bit, dg, ds);
let mut state = StateEnum::empty(
(1, n),
SquareCompact,
&TrackingConfig::default(),
sys.tile_names().len(),
)
.unwrap();
sys.update_state(&mut state, &NeededUpdate::All);
for col in 0..n {
sys.place_tile(
&mut state,
PointSafe2((0, col)),
correct_tile(col, input_bit),
false,
)
.unwrap();
}
sys.update_state(&mut state, &NeededUpdate::All);
let rates_37: Vec<f64> = (1..n)
.map(|c| f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, c)))))
.collect();
let needed = sys.set_param("temperature", Box::new(50.0_f64)).unwrap();
sys.update_state(&mut state, &needed);
let rates_50: Vec<f64> = (1..n)
.map(|c| f64::from(sys.event_rate_at_point(&state, PointSafeHere((0, c)))))
.collect();
for (i, col) in (1..n).enumerate() {
assert!(
rates_50[i] > rates_37[i],
"position {col}: rate at T=50 ({}) should exceed rate at T=37 ({})",
rates_50[i],
rates_37[i]
);
}
}