173 lines
5.1 KiB
Rust
173 lines
5.1 KiB
Rust
//! Integration tests for symclaw-bio.
|
||
|
||
use std::collections::HashMap;
|
||
use symclaw_bio::genome::{SignedPerm, genetic_code};
|
||
use symclaw_bio::ode_model::OdeModel;
|
||
use symclaw_bio::population::{HardyWeinberg, JC69};
|
||
use symclaw_bio::reactions::{Reaction, ReactionNetwork};
|
||
|
||
// ── ODE Model ────────────────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn ode_model_state_count() {
|
||
let m = OdeModel::new(vec!["x", "y"], vec!["k"]);
|
||
assert_eq!(m.n_states(), 2);
|
||
}
|
||
|
||
#[test]
|
||
fn ode_model_parameter_count() {
|
||
let m = OdeModel::new(vec!["x"], vec!["k1", "k2", "k3"]);
|
||
assert_eq!(m.n_params(), 3);
|
||
}
|
||
|
||
#[test]
|
||
fn ode_model_add_ode_and_output() {
|
||
let mut m = OdeModel::new(vec!["x"], vec!["k"]);
|
||
m.add_ode("x", "k * x");
|
||
m.add_output("obs", "x");
|
||
assert_eq!(m.n_outputs(), 1);
|
||
assert!(m.ode_expr("x").is_ok(), "ODE expression for x should parse");
|
||
}
|
||
|
||
// ── Reaction Network ─────────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn reaction_network_stoichiometry_shape() {
|
||
// A → B: stoichiometry matrix should be 2 rows × 1 column
|
||
let mut net = ReactionNetwork::new();
|
||
net.add_species("A");
|
||
net.add_species("B");
|
||
let mut reactants = HashMap::new();
|
||
reactants.insert("A".to_owned(), 1.0);
|
||
let mut products = HashMap::new();
|
||
products.insert("B".to_owned(), 1.0);
|
||
net.add_reaction(Reaction {
|
||
name: "r1".into(),
|
||
reactants,
|
||
products,
|
||
rate: "k1".into(),
|
||
});
|
||
let s = net.stoichiometry_matrix();
|
||
assert_eq!(s.len(), 2, "2 rows (species)");
|
||
assert_eq!(s[0].len(), 1, "1 column (reaction)");
|
||
}
|
||
|
||
#[test]
|
||
fn reaction_network_deficiency_reversible() {
|
||
// A ⇌ B: deficiency = 0
|
||
let mut net = ReactionNetwork::new();
|
||
net.add_species("A");
|
||
net.add_species("B");
|
||
let mut r1 = HashMap::new();
|
||
r1.insert("A".to_owned(), 1.0);
|
||
let mut p1 = HashMap::new();
|
||
p1.insert("B".to_owned(), 1.0);
|
||
let mut r2 = HashMap::new();
|
||
r2.insert("B".to_owned(), 1.0);
|
||
let mut p2 = HashMap::new();
|
||
p2.insert("A".to_owned(), 1.0);
|
||
net.add_reaction(Reaction {
|
||
name: "r1".into(),
|
||
reactants: r1,
|
||
products: p1,
|
||
rate: "k1".into(),
|
||
});
|
||
net.add_reaction(Reaction {
|
||
name: "r2".into(),
|
||
reactants: r2,
|
||
products: p2,
|
||
rate: "k2".into(),
|
||
});
|
||
let d = net.deficiency();
|
||
assert_eq!(d, 0, "A⇌B deficiency = 0, got {d}");
|
||
}
|
||
|
||
// ── Population Genetics ───────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn hardy_weinberg_frequencies_sum_to_one() {
|
||
let hw = HardyWeinberg::new(0.3);
|
||
let (aa, ab, bb) = hw.genotype_freqs();
|
||
let total = aa + ab + bb;
|
||
assert!((total - 1.0).abs() < 1e-10, "HW freqs sum to 1: {total}");
|
||
}
|
||
|
||
#[test]
|
||
fn hardy_weinberg_max_het_at_p_half() {
|
||
let hw = HardyWeinberg::new(0.5);
|
||
let (_, ab, _) = hw.genotype_freqs();
|
||
assert!((ab - 0.5).abs() < 1e-10, "2pq at p=0.5 = 0.5, got {ab}");
|
||
}
|
||
|
||
#[test]
|
||
fn jc69_rate_matrix_symmetry() {
|
||
let jc = JC69::new(0.01);
|
||
let r = jc.rate_matrix();
|
||
for (i, row_i) in r.iter().enumerate() {
|
||
for (j, &val) in row_i.iter().enumerate() {
|
||
if i != j {
|
||
assert!(
|
||
(val - r[j][i]).abs() < 1e-12,
|
||
"JC69 rate[{i}][{j}]={} ≠ [{j}][{i}]={}",
|
||
val,
|
||
r[j][i]
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn jc69_rate_rows_sum_to_zero() {
|
||
let jc = JC69::new(0.02);
|
||
let r = jc.rate_matrix();
|
||
for (i, row) in r.iter().enumerate() {
|
||
let sum: f64 = row.iter().sum();
|
||
assert!(sum.abs() < 1e-10, "JC69 row {i} = {sum}, expected 0");
|
||
}
|
||
}
|
||
|
||
// ── Genome Algebra ────────────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn genetic_code_has_64_codons() {
|
||
assert_eq!(genetic_code().len(), 64);
|
||
}
|
||
|
||
#[test]
|
||
fn genetic_code_atg_is_met() {
|
||
// ATG → Met (either "M" or "Met" depending on encoding)
|
||
let aa = genetic_code().get("ATG").copied().unwrap_or("");
|
||
assert!(aa == "M" || aa == "Met", "ATG should be Met/M, got: {aa}");
|
||
}
|
||
|
||
#[test]
|
||
fn genetic_code_stop_codons() {
|
||
let code = genetic_code();
|
||
for stop in &["TAA", "TAG", "TGA"] {
|
||
let aa = code.get(stop).copied().unwrap_or("");
|
||
assert!(
|
||
aa == "Stop" || aa == "*" || aa == "Ter",
|
||
"{stop} should be a stop codon, got: {aa}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn signed_perm_double_reversal_identity() {
|
||
let p = SignedPerm::new(vec![1, 2, 3, 4]);
|
||
let rev = p.reverse_segment(1, 3);
|
||
let back = rev.reverse_segment(1, 3);
|
||
assert_eq!(back.to_string(), p.to_string(), "rev(rev(p)) = p");
|
||
}
|
||
|
||
#[test]
|
||
fn signed_perm_identity_is_identity() {
|
||
assert!(SignedPerm::identity(4).is_identity());
|
||
}
|
||
|
||
#[test]
|
||
fn signed_perm_breakpoints_sorted() {
|
||
assert_eq!(SignedPerm::identity(4).breakpoints(), 0);
|
||
}
|