#![warn(missing_docs)]
use std::{
fs,
io::{BufWriter, Write},
path::Path,
};
use anyhow::Context;
use log::{debug, trace};
use rand_core::Rng;
use thiserror::Error;
pub type NbIndividuals = u64;
#[derive(Debug)]
pub struct NextReaction<Reaction>
where
Reaction: std::fmt::Debug,
{
pub time: f32,
pub event: Reaction,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum SimState {
Stop(StopReason),
Continue,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum StopReason {
NoIndividualsLeft,
MaxIndividualsReached,
MaxItersReached,
AbsorbingStateReached,
MaxTimeReached,
}
#[derive(Clone, Debug)]
pub struct IterTime {
pub iter: usize,
pub time: f32,
}
#[derive(Clone, Debug)]
pub struct Options {
pub init_iter: usize,
pub max_iter_time: IterTime,
pub max_cells: NbIndividuals,
}
pub fn simulate<P, REACTION, const NB_REACTIONS: usize>(
state: &mut CurrentState<NB_REACTIONS>,
rates: &ReactionRates<{ NB_REACTIONS }>,
possible_reactions: &[REACTION; NB_REACTIONS],
bd_process: &mut P,
options: &Options,
rng: &mut impl Rng,
) -> StopReason
where
P: AdvanceStep<NB_REACTIONS, Reaction = REACTION>,
REACTION: std::fmt::Debug,
{
let mut iter = options.init_iter;
let mut time = 0f32;
loop {
if state.population.iter().sum::<u64>() >= options.max_cells {
return StopReason::MaxIndividualsReached;
}
let (sim_state, reaction) = bd_process.next_reaction(
state,
rates,
possible_reactions,
IterTime {
iter: options.max_iter_time.iter - 1,
time: options.max_iter_time.time,
},
IterTime { iter, time },
rng,
);
debug!("time: {time} iter: {iter} and reaction: {reaction:#?}");
trace!("State: {state:#?}");
match sim_state {
SimState::Continue => {
let reaction = reaction.unwrap();
let reaction_time = reaction.time;
bd_process.advance_step(reaction, rng);
bd_process.update_state(state);
iter += 1;
time += reaction_time;
}
SimState::Stop(reason) => return reason,
}
}
}
#[derive(Debug, Clone)]
pub struct CurrentState<const NB_REACTIONS: usize> {
pub population: [NbIndividuals; NB_REACTIONS],
}
pub trait AdvanceStep<const NB_REACTIONS: usize> {
type Reaction: std::fmt::Debug + Copy;
fn next_reaction(
&self,
state: &CurrentState<NB_REACTIONS>,
rates: &ReactionRates<{ NB_REACTIONS }>,
possible_reactions: &[Self::Reaction; NB_REACTIONS],
max_iter_and_time: IterTime,
iter_and_time: IterTime,
rng: &mut impl Rng,
) -> (SimState, Option<NextReaction<Self::Reaction>>) {
if state.population.iter().sum::<u64>() == 0u64 {
return (SimState::Stop(StopReason::NoIndividualsLeft), None);
};
if iter_and_time.iter >= max_iter_and_time.iter {
return (SimState::Stop(StopReason::MaxItersReached), None);
};
if iter_and_time.time >= max_iter_and_time.time {
return (SimState::Stop(StopReason::MaxTimeReached), None);
};
let mut selected_event = 0_usize;
let times = rates.compute_times_events(&state.population, rng).unwrap();
let mut smaller_waiting_time = times[0];
for (idx, &waiting_time) in times.iter().enumerate() {
if waiting_time <= smaller_waiting_time {
smaller_waiting_time = waiting_time;
selected_event = idx;
}
}
(
SimState::Continue,
Some(NextReaction {
time: smaller_waiting_time,
event: possible_reactions[selected_event],
}),
)
}
fn advance_step(&mut self, reaction: NextReaction<Self::Reaction>, rng: &mut impl Rng);
fn update_state(&self, state: &mut CurrentState<NB_REACTIONS>);
}
#[derive(Error, Debug, PartialEq)]
enum ReactionRatesError {
#[error("no individuals left")]
NoIndividualsLeft,
#[error("the computed times are 0 or inf, probably 0 or inf rates")]
ComputeTimesAllInfinite,
}
#[derive(Debug, Clone)]
pub struct ReactionRates<const N: usize>(
pub [f32; N],
);
impl<const N: usize> ReactionRates<N> {
fn compute_times_events(
&self,
population: &[NbIndividuals; N],
rng: &mut impl Rng,
) -> Result<[f32; N], ReactionRatesError> {
if population.iter().all(|&pop| pop == 0) {
return Err(ReactionRatesError::NoIndividualsLeft);
}
let mut times = self.0;
for i in 0..N {
times[i] = exprand(times[i] * population[i] as f32, rng);
}
if times.iter().any(|time| time.is_normal()) {
return Ok(times);
}
Err(ReactionRatesError::ComputeTimesAllInfinite)
}
}
const OPEN01_PRECISION_BITS: u32 = 24;
const OPEN01_SCALE: f32 = 1.0 / (1u32 << OPEN01_PRECISION_BITS) as f32;
pub fn exprand(lambda: f32, rng: &mut impl Rng) -> f32 {
assert!(!lambda.is_sign_negative());
if lambda.is_normal() {
let bits = rng.next_u32() >> (32 - OPEN01_PRECISION_BITS);
let val = OPEN01_SCALE * (bits as f32 + 0.5);
return -val.ln() / lambda;
} else if lambda.is_infinite() {
return 0.;
}
f32::INFINITY
}
pub fn write2file<T: std::fmt::Display>(
data: &[T],
path: &Path,
header: Option<&str>,
endline: bool,
) -> anyhow::Result<()> {
fs::create_dir_all(path.parent().unwrap()).expect("Cannot create dir");
let f = fs::OpenOptions::new()
.read(true)
.append(true)
.create(true)
.open(path)
.with_context(|| "Cannot open stream")?;
let mut buffer = BufWriter::new(f);
if !data.is_empty() {
if let Some(h) = header {
writeln!(buffer, "{h}")?;
}
for ele in data.iter() {
write!(buffer, "{ele:.4},")?;
}
if endline {
writeln!(buffer)?;
}
} else {
write!(buffer, "{},", f32::NAN)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use quickcheck_macros::quickcheck;
use rand::{SeedableRng, rngs::StdRng};
use std::num::{NonZeroU8, NonZeroU16};
#[quickcheck]
fn exprand_same_seed_test(lambda: u8, seed: u64) -> bool {
let mut rng = StdRng::seed_from_u64(seed);
let lambda = lambda as f32 / 10.;
if lambda.is_normal() {
let exp1 = exprand(lambda, &mut rng);
let mut rng = StdRng::seed_from_u64(seed);
let exp2 = exprand(lambda, &mut rng);
return (exp1 - exp2).abs() < f32::EPSILON;
}
exprand(lambda, &mut rng).is_infinite()
}
#[test]
#[should_panic]
fn exprand_test_neg_lambda() {
let mut rng = StdRng::seed_from_u64(1u64);
let lambda = -0_f32;
exprand(lambda, &mut rng);
}
#[test]
fn exprand_test() {
let mut rng = StdRng::seed_from_u64(1u64);
let lambda = 0_f32;
let first = exprand(lambda, &mut rng);
assert!(first.is_infinite());
let lambda = f32::INFINITY;
let first = exprand(lambda, &mut rng);
assert!((0f32 - first).abs() < f32::EPSILON);
let lambda = 1_f32;
let first = exprand(lambda, &mut rng);
assert!(first.is_sign_positive());
}
#[test]
fn exprand_rate_one_returns_1_test() {
let mut rng = StdRng::seed_from_u64(1u64);
let lambda_one = 1f32;
let samples = 1000000;
let sum: f32 = (0..)
.map(|_| exprand(lambda_one, &mut rng))
.take(samples)
.sum();
let mean = sum / (samples as f32);
assert!((mean - 1.).abs() < 0.001)
}
#[quickcheck]
fn exprand_test_is_positive(lambda: NonZeroU8, seed: u64) -> bool {
let mut rng = StdRng::seed_from_u64(seed);
exprand(lambda.get() as f32 / 10., &mut rng).is_sign_positive()
}
struct TestNextReaction {
population: [u64; 4],
}
impl AdvanceStep<4> for TestNextReaction {
type Reaction = usize;
fn advance_step(&mut self, reaction: NextReaction<Self::Reaction>, _rng: &mut impl Rng) {
self.population[reaction.event] += 1;
}
fn update_state(&self, state: &mut CurrentState<4>) {
state.population = self.population;
}
}
#[quickcheck]
fn advance_step_trait_where_only_second_reaction_is_possible_because_state_test(
pop: NonZeroU16,
seed: u64,
) -> bool {
let mut rng = StdRng::seed_from_u64(seed);
let population = [0, pop.get() as NbIndividuals, 0, 0];
let mut expected_population = population;
expected_population[1] = population[1] + 1;
let mut state = CurrentState { population };
let rates = &ReactionRates([1., 1., 1., 1.]);
let possible_reactions = &[0usize, 1usize, 2usize, 3usize];
let mut process = TestNextReaction { population };
let (sim_state, next_reaction) = process.next_reaction(
&state,
rates,
possible_reactions,
IterTime {
iter: 10,
time: 10.,
},
IterTime { iter: 0, time: 0. },
&mut rng,
);
process.advance_step(next_reaction.unwrap(), &mut rng);
assert_eq!(state.population, population);
process.update_state(&mut state);
state.population == expected_population
&& sim_state == SimState::Continue
&& process.population == expected_population
}
#[quickcheck]
fn advance_step_trait_where_only_third_reaction_is_possible_because_rates_test(
pop: NonZeroU16,
seed: u64,
) -> bool {
let mut rng = StdRng::seed_from_u64(seed);
let population = [
pop.get() as NbIndividuals,
pop.get() as NbIndividuals,
pop.get() as NbIndividuals,
pop.get() as NbIndividuals,
];
let mut expected_population = population;
expected_population[2] = population[2] + 1;
let mut state = CurrentState { population };
let rates = &ReactionRates([0., 0., 1., 0.]);
let possible_reactions = &[0usize, 1usize, 2usize, 3usize];
let mut process = TestNextReaction { population };
let (sim_state, next_reaction) = process.next_reaction(
&state,
rates,
possible_reactions,
IterTime {
iter: 10,
time: 10.,
},
IterTime { iter: 0, time: 0. },
&mut rng,
);
process.advance_step(next_reaction.unwrap(), &mut rng);
assert_eq!(state.population, population);
process.update_state(&mut state);
state.population == expected_population
&& sim_state == SimState::Continue
&& process.population == expected_population
}
#[test]
fn reaction_rates() {
let mut rng = StdRng::seed_from_u64(1u64);
let rates = ReactionRates([0.1, 0.1]);
let population = [10, 10];
assert!(
rates
.compute_times_events(&population, &mut rng)
.unwrap()
.iter()
.all(|time| time.is_normal())
);
let rates_zeros = ReactionRates([0., 0.]);
let population = [10, 10];
assert_eq!(
rates_zeros.compute_times_events(&population, &mut rng),
Err(ReactionRatesError::ComputeTimesAllInfinite)
);
let rates = ReactionRates([0.1, 0.1]);
let population_zero = [0, 0];
assert_eq!(
rates.compute_times_events(&population_zero, &mut rng),
Err(ReactionRatesError::NoIndividualsLeft)
);
}
}