use rand::Rng;
use rand::distributions::OpenClosed01;
use smallvec::SmallVec;
use super::entity::{LefKey, LegKey, Mobility};
use super::lattice::{Direction, Position, SiteKind};
use super::spawn::Spawn;
use super::world::World;
use super::{Schedule, SimRng, Time};
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub struct Rates {
pub step: f64,
pub respawn: f64,
pub respawn_blocked: f64,
pub respawn_captured: f64,
}
impl Default for Rates {
fn default() -> Self {
Rates {
step: 1.0,
respawn: 1e-3,
respawn_blocked: 1e-2,
respawn_captured: 1e-3,
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum Action {
Step { leg: LegKey },
Respawn { lef: LefKey },
Evict { leg: LegKey },
}
impl Action {
pub fn code(self) -> u8 {
match self {
Action::Step { .. } => 0,
Action::Respawn { .. } => 1,
Action::Evict { .. } => 3,
}
}
pub fn schedule(&self, world: &World, rates: &Rates, now: Time, rng: &mut SimRng) -> Schedule {
match *self {
Action::Step { leg } => schedule_step(leg, world, rates, now, rng),
Action::Respawn { lef } => schedule_respawn(lef, world, rates, now, rng),
Action::Evict { leg } => schedule_evict(leg, world, rates, now, rng),
}
}
pub(crate) fn fire(
&self,
world: &mut World,
spawn: &dyn Spawn,
_now: Time,
rng: &mut SimRng,
touched: &mut Touched,
) {
match *self {
Action::Step { leg } => fire_step(leg, world, rng, touched),
Action::Respawn { lef } => fire_respawn(lef, world, spawn, rng, touched),
Action::Evict { leg } => fire_evict(leg, world, spawn, rng, touched),
}
}
}
#[derive(Clone, Default, Debug)]
pub(crate) struct Touched {
pub(crate) legs: SmallVec<[LegKey; 8]>,
pub(crate) lefs: SmallVec<[LefKey; 4]>,
}
impl Touched {
pub(crate) fn clear(&mut self) {
self.legs.clear();
self.lefs.clear();
}
pub(crate) fn dependent_actions(&self, world: &World, fired: Action) -> SmallVec<[Action; 32]> {
let from_legs = self.legs.iter().flat_map(|&leg_k| {
let lef_k = world.legs[leg_k].lef;
[
Action::Step { leg: leg_k },
Action::Evict { leg: leg_k },
Action::Respawn { lef: lef_k },
]
});
let from_lefs = self
.lefs
.iter()
.map(|&lef_k| Action::Respawn { lef: lef_k });
let mut actions: SmallVec<[Action; 32]> = SmallVec::new();
actions.push(fired);
for action in from_legs.chain(from_lefs) {
if !actions.contains(&action) {
actions.push(action);
}
}
actions
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
enum Ahead {
Free,
CtcfBack(f64),
CtcfBackHeld,
Blocked,
}
fn ahead(world: &World, leg_k: LegKey) -> Ahead {
let leg = &world.legs[leg_k];
let Some(target) = leg
.facing
.shift(leg.pos)
.filter(|&t| (t as usize) < world.lattice.len())
else {
return Ahead::Blocked;
};
let back = match world.lattice.sites[target as usize].kind {
SiteKind::Ctcf {
captures_from,
strength,
} if captures_from == leg.facing => Some(strength),
_ => None,
};
let occupants = world.legs_at(target);
match back {
Some(strength) if occupants.is_empty() => Ahead::CtcfBack(strength),
Some(_)
if occupants
.iter()
.any(|&o| world.legs[o].mobility == Mobility::Captured) =>
{
Ahead::CtcfBackHeld
}
_ if occupants.is_empty() => Ahead::Free,
_ => Ahead::Blocked,
}
}
fn protected(world: &World, leg_k: LegKey) -> bool {
let lef = &world.lefs[world.legs[leg_k].lef];
lef.legs
.iter()
.any(|&l| l != leg_k && world.legs[l].mobility == Mobility::Captured)
}
fn blocked(world: &World, leg_k: LegKey) -> bool {
world.legs[leg_k].mobility == Mobility::Moving
&& match ahead(world, leg_k) {
Ahead::Blocked => true,
Ahead::CtcfBackHeld => protected(world, leg_k),
Ahead::Free | Ahead::CtcfBack(_) => false,
}
}
fn at_rate(rate: f64, now: Time, rng: &mut SimRng) -> Schedule {
if rate > 0.0 {
Schedule::At(now + sample_exponential(rng, rate))
} else {
Schedule::Never
}
}
fn schedule_step(
leg_k: LegKey,
world: &World,
rates: &Rates,
now: Time,
rng: &mut SimRng,
) -> Schedule {
if world.legs[leg_k].mobility != Mobility::Moving {
return Schedule::Never;
}
match ahead(world, leg_k) {
Ahead::Free => at_rate(rates.step, now, rng),
Ahead::CtcfBack(_) if protected(world, leg_k) => at_rate(rates.step, now, rng),
Ahead::CtcfBack(strength) => at_rate((1.0 - strength) * rates.step, now, rng),
Ahead::CtcfBackHeld | Ahead::Blocked => Schedule::Never,
}
}
fn schedule_evict(
leg_k: LegKey,
world: &World,
rates: &Rates,
now: Time,
rng: &mut SimRng,
) -> Schedule {
if world.legs[leg_k].mobility != Mobility::Moving || protected(world, leg_k) {
return Schedule::Never;
}
match ahead(world, leg_k) {
Ahead::CtcfBack(strength) => at_rate(strength * rates.step, now, rng),
Ahead::CtcfBackHeld => at_rate(rates.step, now, rng),
Ahead::Free | Ahead::Blocked => Schedule::Never,
}
}
fn schedule_respawn(
lef_k: LefKey,
world: &World,
rates: &Rates,
now: Time,
rng: &mut SimRng,
) -> Schedule {
at_rate(respawn_rate(lef_k, world, rates), now, rng)
}
fn respawn_rate(lef_k: LefKey, world: &World, rates: &Rates) -> f64 {
let legs = world.lefs[lef_k].legs;
if legs
.iter()
.any(|&l| world.legs[l].mobility == Mobility::Captured)
{
rates.respawn_captured
} else if legs.iter().any(|&l| blocked(world, l)) {
rates.respawn_blocked
} else {
rates.respawn
}
}
fn fire_step(leg_k: LegKey, world: &mut World, rng: &mut SimRng, touched: &mut Touched) {
let (old_pos, lef_k, facing) = {
let leg = &world.legs[leg_k];
(leg.pos, leg.lef, leg.facing)
};
let new_pos = facing
.shift(old_pos)
.expect("step scheduled at lattice boundary");
let neighbors_at = |p: Option<Position>| -> SmallVec<[LegKey; 2]> {
p.filter(|p| (*p as usize) < world.lattice.len())
.map(|p| world.legs_at(p).iter().copied().collect())
.unwrap_or_default()
};
let rear_neighbors = neighbors_at(facing.opposite().shift(old_pos));
let post_neighbors = neighbors_at(facing.shift(new_pos));
world.remove_leg(leg_k, old_pos);
world.insert_leg(leg_k, new_pos);
let kind = world.lattice.sites[new_pos as usize].kind;
let arrived_from = facing.opposite();
let leg = &mut world.legs[leg_k];
leg.pos = new_pos;
leg.mobility = resolve_mobility(kind, arrived_from, rng);
let captured = leg.mobility == Mobility::Captured;
touched.legs.push(leg_k);
touched.lefs.push(lef_k);
if captured {
touched
.legs
.extend(world.lefs[lef_k].legs.into_iter().filter(|&l| l != leg_k));
}
touched.legs.extend(
rear_neighbors
.into_iter()
.chain(post_neighbors)
.filter(|&n| n != leg_k),
);
}
fn fire_evict(
leg_k: LegKey,
world: &mut World,
spawn: &dyn Spawn,
rng: &mut SimRng,
touched: &mut Touched,
) {
debug_assert!(
!protected(world, leg_k),
"eviction scheduled for a protected leg"
);
let lef_k = world.legs[leg_k].lef;
fire_respawn(lef_k, world, spawn, rng, touched);
}
fn fire_respawn(
lef_k: LefKey,
world: &mut World,
spawn: &dyn Spawn,
rng: &mut SimRng,
touched: &mut Touched,
) {
let [l1, l2] = world.lefs[lef_k].legs;
let p1 = world.legs[l1].pos;
let p2 = world.legs[l2].pos;
for p in [p1, p2] {
for d in [Direction::Left, Direction::Right] {
if let Some(np) = d.shift(p)
&& (np as usize) < world.lattice.len()
{
for &n in world.legs_at(np) {
if n != l1 && n != l2 {
touched.legs.push(n);
}
}
}
}
}
world.remove_leg(l1, p1);
world.remove_leg(l2, p2);
let new_pos = spawn.sample(world, rng);
for d in [Direction::Left, Direction::Right] {
if let Some(np) = d.shift(new_pos)
&& (np as usize) < world.lattice.len()
{
for &n in world.legs_at(np) {
touched.legs.push(n);
}
}
}
let kind = world.lattice.sites[new_pos as usize].kind;
let mobility = land_lef(kind, rng);
for (l, m) in [l1, l2].into_iter().zip(mobility) {
let leg = &mut world.legs[l];
leg.pos = new_pos;
leg.mobility = m;
}
world.insert_leg(l1, new_pos);
world.insert_leg(l2, new_pos);
touched.legs.extend([l1, l2]);
touched.lefs.push(lef_k);
}
fn sample_exponential(rng: &mut SimRng, rate: f64) -> Time {
let u: f64 = rng.sample(OpenClosed01);
-libm::log(u) / rate
}
pub(crate) fn land_lef(kind: SiteKind, rng: &mut SimRng) -> [Mobility; 2] {
[
resolve_mobility(kind, Direction::Left.opposite(), rng),
resolve_mobility(kind, Direction::Right.opposite(), rng),
]
}
pub(crate) fn resolve_mobility(
site: SiteKind,
arrived_from: Direction,
rng: &mut SimRng,
) -> Mobility {
match site {
SiteKind::Plain => Mobility::Moving,
SiteKind::Ctcf {
captures_from,
strength,
} => {
if arrived_from == captures_from && rng.gen_bool(strength) {
Mobility::Captured
} else {
Mobility::Moving
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Lattice, SpawnRule};
use Mobility::Moving;
use rand::SeedableRng;
#[test]
fn dependent_actions_start_with_the_fired_action_and_have_no_duplicates() {
let mut rng = SimRng::seed_from_u64(0);
let mut world = World::new(Lattice::plain(20).unwrap());
let (lef, [left, right]) = world.spawn_lef(10, &mut rng);
let mut touched = Touched::default();
touched.legs.extend([left, right, left]); touched.lefs.push(lef);
let fired = Action::Respawn { lef };
let deps = touched.dependent_actions(&world, fired);
assert_eq!(
deps.as_slice(),
&[
fired,
Action::Step { leg: left },
Action::Evict { leg: left },
Action::Step { leg: right },
Action::Evict { leg: right }
]
);
}
const RATES: Rates = Rates {
step: 1.0,
respawn: 1.0,
respawn_blocked: 2.0,
respawn_captured: 3.0,
};
type LefSpec = ((u32, Mobility), (u32, Mobility));
fn lattice(strength: f64) -> Lattice {
let ctcf = |site| crate::model::Ctcf {
site,
captures_from: Direction::Left,
strength,
};
Lattice::with_ctcfs(40, &[ctcf(20), ctcf(25)]).unwrap()
}
fn world_on(lattice: Lattice, lefs: &[LefSpec]) -> (World, Vec<LefKey>) {
let mut world = World::new(lattice);
let keys = lefs
.iter()
.map(|&(l, r)| world.add_lef(l, r).unwrap())
.collect();
(world, keys)
}
fn world_with(lefs: &[LefSpec]) -> (World, Vec<LefKey>) {
world_on(lattice(1.0), lefs)
}
fn scheduled(world: &World, action: Action) -> bool {
let mut rng = SimRng::seed_from_u64(0);
matches!(
action.schedule(world, &RATES, 0.0, &mut rng),
Schedule::At(_)
)
}
fn clocks(world: &World, leg: LegKey) -> (bool, bool) {
(
scheduled(world, Action::Step { leg }),
scheduled(world, Action::Evict { leg }),
)
}
#[test]
fn respawn_rate_takes_the_first_rule_that_applies() {
use Mobility::Captured;
let rate = |lefs: &[LefSpec], which: usize| {
let (world, keys) = world_with(lefs);
respawn_rate(keys[which], &world, &RATES)
};
assert_eq!(rate(&[((5, Moving), (8, Moving))], 0), RATES.respawn);
assert_eq!(
rate(
&[((5, Moving), (8, Moving)), ((10, Moving), (12, Moving))],
1
),
RATES.respawn
);
assert_eq!(
rate(
&[((5, Moving), (8, Moving)), ((9, Moving), (12, Moving))],
0
),
RATES.respawn_blocked
);
assert_eq!(
rate(&[((0, Moving), (8, Moving))], 0),
RATES.respawn_blocked
);
let rosette = [
((2, Moving), (5, Moving)),
((6, Moving), (9, Moving)),
((10, Moving), (13, Moving)),
];
for lef in 0..3 {
assert_eq!(rate(&rosette, lef), RATES.respawn_blocked, "LEF {lef}");
}
assert_eq!(rate(&[((26, Moving), (30, Moving))], 0), RATES.respawn);
assert_eq!(
rate(&[((0, Moving), (20, Captured))], 0),
RATES.respawn_captured
);
}
#[test]
fn step_and_eviction_split_the_encounter_with_a_ctcf_from_behind() {
use Mobility::Captured;
let (world, keys) = world_with(&[((5, Moving), (8, Moving))]);
assert_eq!(clocks(&world, world.lefs[keys[0]].legs[0]), (true, false));
for (strength, expected) in [
(0.5, (true, true)),
(1.0, (false, true)),
(0.0, (true, false)),
] {
let (world, keys) = world_on(lattice(strength), &[((26, Moving), (30, Moving))]);
assert_eq!(
clocks(&world, world.lefs[keys[0]].legs[0]),
expected,
"strength {strength}"
);
}
let (world, keys) = world_on(
lattice(0.0),
&[((10, Moving), (25, Captured)), ((26, Moving), (30, Moving))],
);
assert_eq!(clocks(&world, world.lefs[keys[1]].legs[0]), (false, true));
let (world, keys) =
world_with(&[((25, Moving), (27, Moving)), ((26, Moving), (26, Moving))]);
let behind = world.lefs[keys[1]].legs[0];
assert_eq!(clocks(&world, behind), (false, false));
assert!(blocked(&world, behind));
let (world, keys) = world_with(&[((30, Moving), (39, Moving))]);
assert_eq!(clocks(&world, world.lefs[keys[0]].legs[1]), (false, false));
}
#[test]
fn a_captured_partner_makes_a_leg_immune_to_eviction() {
use Mobility::Captured;
let (world, keys) = world_with(&[((21, Moving), (25, Captured))]);
let leg = world.lefs[keys[0]].legs[0];
assert_eq!(clocks(&world, leg), (true, false));
let mut rng = SimRng::seed_from_u64(0);
let full = |action: Action, world: &World, rng: &mut SimRng| match action
.schedule(world, &RATES, 0.0, rng)
{
Schedule::At(t) => t,
Schedule::Never => f64::INFINITY,
};
let (free_world, free_keys) = world_with(&[((5, Moving), (8, Moving))]);
let free_leg = free_world.lefs[free_keys[0]].legs[0];
let t_protected = full(Action::Step { leg }, &world, &mut rng);
let mut rng = SimRng::seed_from_u64(0);
assert_eq!(
t_protected,
full(Action::Step { leg: free_leg }, &free_world, &mut rng)
);
let (world, keys) = world_with(&[
((10, Moving), (20, Captured)),
((21, Moving), (25, Captured)),
]);
let leg = world.lefs[keys[1]].legs[0];
assert_eq!(clocks(&world, leg), (false, false));
assert!(blocked(&world, leg));
}
#[test]
fn a_capture_reschedules_the_partner() {
let (mut world, keys) = world_with(&[((21, Moving), (24, Moving))]);
let [left, right] = world.lefs[keys[0]].legs;
let mut touched = Touched::default();
fire_step(
right,
&mut world,
&mut SimRng::seed_from_u64(0),
&mut touched,
);
assert_eq!(world.legs[right].mobility, Mobility::Captured);
assert!(touched.legs.contains(&left), "the partner's clocks changed");
}
#[test]
fn eviction_wins_the_race_with_probability_strength() {
let p = 0.3;
let (world, keys) = world_on(lattice(p), &[((26, Moving), (30, Moving))]);
let leg = world.lefs[keys[0]].legs[0];
let mut rng = SimRng::seed_from_u64(9);
let n = 20_000;
let mut evicted_first = 0;
for _ in 0..n {
let time = |action: Action, rng: &mut SimRng| match action
.schedule(&world, &RATES, 0.0, rng)
{
Schedule::At(t) => t,
Schedule::Never => f64::INFINITY,
};
if time(Action::Evict { leg }, &mut rng) < time(Action::Step { leg }, &mut rng) {
evicted_first += 1;
}
}
let frac = evicted_first as f64 / n as f64;
let sigma = (p * (1.0 - p) / n as f64).sqrt();
assert!(
(frac - p).abs() < 4.0 * sigma,
"eviction first in {frac}, want {p} (σ {sigma:.4})"
);
}
fn evict(world: &mut World, leg: LegKey) {
let mut rng = SimRng::seed_from_u64(1);
fire_evict(
leg,
world,
&SpawnRule::UniformNonCtcf,
&mut rng,
&mut Touched::default(),
);
}
#[test]
fn an_eviction_unloads_the_lef_at_once() {
let (mut world, keys) = world_with(&[((26, Moving), (30, Moving))]);
let [left, right] = world.lefs[keys[0]].legs;
evict(&mut world, left);
let (l, r) = (world.legs[left], world.legs[right]);
assert_eq!(l.pos, r.pos);
assert_ne!(l.pos, 26);
assert_eq!(
(l.mobility, r.mobility),
(Mobility::Moving, Mobility::Moving)
);
assert!(!world.occupied(30));
}
#[test]
fn stepping_onto_a_ctcf_from_behind_passes_it() {
let (mut world, keys) = world_on(lattice(0.0), &[((26, Moving), (30, Moving))]);
let leg = world.lefs[keys[0]].legs[0];
let mut rng = SimRng::seed_from_u64(1);
fire_step(leg, &mut world, &mut rng, &mut Touched::default());
assert_eq!(
(world.legs[leg].pos, world.legs[leg].mobility),
(25, Mobility::Moving)
);
}
#[test]
fn plain_site_always_moving() {
let mut rng = SimRng::seed_from_u64(0);
assert_eq!(
resolve_mobility(SiteKind::Plain, Direction::Right, &mut rng),
Mobility::Moving
);
assert_eq!(
resolve_mobility(SiteKind::Plain, Direction::Left, &mut rng),
Mobility::Moving
);
}
#[test]
fn landing_captures_only_from_the_capture_side() {
let mut rng = SimRng::seed_from_u64(0);
let site = SiteKind::Ctcf {
captures_from: Direction::Right,
strength: 1.0,
};
assert_eq!(
resolve_mobility(site, Direction::Right, &mut rng),
Mobility::Captured
);
assert_eq!(
resolve_mobility(site, Direction::Left, &mut rng),
Mobility::Moving
);
}
#[test]
fn strength_0_always_passes_through() {
let mut rng = SimRng::seed_from_u64(0);
let site = SiteKind::Ctcf {
captures_from: Direction::Right,
strength: 0.0,
};
for _ in 0..50 {
assert_eq!(
resolve_mobility(site, Direction::Right, &mut rng),
Mobility::Moving
);
assert_eq!(
resolve_mobility(site, Direction::Left, &mut rng),
Mobility::Moving
);
}
}
#[test]
fn capture_fraction_tracks_strength() {
let mut rng = SimRng::seed_from_u64(42);
let p = 0.3;
let site = SiteKind::Ctcf {
captures_from: Direction::Right,
strength: p,
};
let n = 50_000;
let mut captured = 0usize;
for _ in 0..n {
if resolve_mobility(site, Direction::Right, &mut rng) == Mobility::Captured {
captured += 1;
}
}
let frac = captured as f64 / n as f64;
let sigma = (p * (1.0 - p) / n as f64).sqrt();
assert!(
(frac - p).abs() < 4.0 * sigma,
"capture fraction {frac} differs from p={p} by more than 4σ ({sigma:.4})"
);
}
}