rustsim-core 0.0.1

Core ABM engine: agents, models, stores, schedulers, stepping, data collection
Documentation
#![allow(clippy::type_complexity)]

use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rustsim_core::{
    event_queue::{EventContext, EventQueueModel},
    prelude::*,
};
mod support;
use support::NothingSpace;

#[derive(Debug, Clone)]
struct Counter {
    id: AgentId,
    count: u64,
}

impl Agent for Counter {
    fn id(&self) -> AgentId {
        self.id
    }
}

type CounterModel = StandardModel<NothingSpace, Counter, VecStore<Counter>, (), StdRng, ById>;

fn increment_step(
    agent: &mut Counter,
    _ctx: &mut StepContext<'_, NothingSpace, Counter, (), StdRng, ById>,
) {
    agent.count += 1;
}

#[test]
fn vec_store_iter_ids_matches_live_population_under_random_insert_remove() {
    let mut rng = StdRng::seed_from_u64(42);
    let mut store = VecStore::new();
    let mut expected = std::collections::BTreeSet::new();

    for _ in 0..1_000 {
        let id = rng.gen_range(1..=200u64);
        if rng.gen_bool(0.55) {
            store.insert(Counter { id, count: id });
            expected.insert(id);
        } else {
            let _ = store.remove(id);
            expected.remove(&id);
        }

        let ids = store.iter_ids();
        let actual: std::collections::BTreeSet<_> = ids.iter().copied().collect();
        assert_eq!(actual, expected);
        assert_eq!(store.len(), expected.len());
        assert_eq!(store.is_empty(), expected.is_empty());
    }
}

#[test]
fn standard_model_next_id_is_strictly_monotonic_after_random_insertions() {
    let mut store = VecStore::new();
    store.insert(Counter { id: 10, count: 0 });
    store.insert(Counter { id: 25, count: 0 });

    let mut model = CounterModel::new_base(
        store,
        NothingSpace,
        ById::new(),
        (),
        StdRng::seed_from_u64(7),
    )
    .with_agent_step_ctx(increment_step);

    let mut last = 25;
    for _ in 0..200 {
        let next = model.next_id();
        assert!(next > last);
        model.insert_agent(Counter { id: next, count: 0 }).unwrap();
        last = next;
    }
}

#[test]
fn by_id_scheduler_always_returns_sorted_unique_ids_under_random_population() {
    let mut rng = StdRng::seed_from_u64(9);

    for _ in 0..50 {
        let mut store = VecStore::new();
        let mut expected = Vec::new();
        for id in 1..=100u64 {
            if rng.gen_bool(0.6) {
                store.insert(Counter { id, count: 0 });
                expected.push(id);
            }
        }
        expected.sort_unstable();

        let model = CounterModel::new_base(
            store,
            NothingSpace,
            ById::new(),
            (),
            StdRng::seed_from_u64(1),
        );

        let mut scheduler = ById::new();
        let mut buf = Vec::new();
        scheduler.schedule_into(&model, &mut buf);

        assert_eq!(buf, expected);
        assert!(buf.windows(2).all(|w| w[0] < w[1]));
    }
}

#[derive(Debug, Clone)]
struct TimedAgent {
    id: AgentId,
}

impl Agent for TimedAgent {
    fn id(&self) -> AgentId {
        self.id
    }
}

type EventLog = Vec<(AgentId, u32)>;
type TimedModel = EventQueueModel<NothingSpace, TimedAgent, VecStore<TimedAgent>, EventLog, StdRng>;

fn log_action(
    agent: &mut TimedAgent,
    ctx: &mut EventContext<'_, NothingSpace, TimedAgent, EventLog, StdRng>,
) {
    let time = ctx.time() as u32;
    ctx.properties_mut().push((agent.id, time));
}

#[test]
fn event_queue_preserves_nondecreasing_time_under_random_event_sets() {
    let mut rng = StdRng::seed_from_u64(99);

    for _ in 0..25 {
        let mut store = VecStore::new();
        for id in 1..=8u64 {
            store.insert(TimedAgent { id });
        }

        let actions: Vec<
            fn(&mut TimedAgent, &mut EventContext<'_, NothingSpace, TimedAgent, EventLog, StdRng>),
        > = vec![log_action];
        let mut model = TimedModel::new(
            store,
            NothingSpace,
            Vec::new(),
            StdRng::seed_from_u64(5),
            actions,
        );

        for _ in 0..100 {
            let id = rng.gen_range(1..=8u64);
            let dt = rng.gen_range(0..=20) as f64;
            model.add_event(id, 0, dt);
        }

        model.run_events(100);
        let times: Vec<u32> = model.properties().iter().map(|(_, t)| *t).collect();
        assert!(times.windows(2).all(|w| w[0] <= w[1]));
    }
}