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::SeedableRng;
use rustsim_core::{
    event_queue::{EventContext, EventQueueModel},
    prelude::*,
};
mod support;
use std::time::Instant;
use support::NothingSpace;

#[derive(Debug, Clone)]
struct Particle {
    id: AgentId,
    x: f32,
    vx: f32,
}

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

type ParticleModel = StandardModel<NothingSpace, Particle, VecStore<Particle>, (), StdRng, ById>;

fn particle_step(
    agent: &mut Particle,
    _ctx: &mut StepContext<'_, NothingSpace, Particle, (), StdRng, ById>,
) {
    agent.x += agent.vx;
}

#[test]
fn sustained_standard_model_soak_preserves_population_and_values() {
    const AGENT_COUNT: u64 = 20_000;
    const STEP_COUNT: usize = 200;

    let mut store = VecStore::new();
    for id in 1..=AGENT_COUNT {
        store.insert(Particle {
            id,
            x: 0.0,
            vx: 0.01 + (id % 7) as f32 * 0.001,
        });
    }

    let mut model = ParticleModel::new(
        store,
        NothingSpace,
        ById::new(),
        (),
        StdRng::seed_from_u64(42),
        Some(Box::new(particle_step)),
        None,
        true,
    );

    let t0 = Instant::now();
    for step in 0..STEP_COUNT {
        model.step();
        if (step + 1) % 50 == 0 {
            assert_eq!(model.agents_len(), AGENT_COUNT as usize);
        }
    }
    let elapsed_ms = t0.elapsed().as_millis();

    assert_eq!(model.time(), Time::Discrete(STEP_COUNT as u64));
    assert_eq!(model.agents_len(), AGENT_COUNT as usize);

    let a1 = model.agent(1).unwrap();
    let expected_a1 = STEP_COUNT as f32 * 0.011;
    assert!((a1.x - expected_a1).abs() < 1e-4);
    drop(a1);

    let checksum: f64 = model.agents().map(|a| a.x as f64).sum();
    assert!(checksum.is_finite());
    assert!(checksum > 0.0);

    eprintln!(
        "[core soak] {} agents x {} steps completed in {} ms",
        AGENT_COUNT, STEP_COUNT, elapsed_ms
    );
}

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

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

type TimedModel = EventQueueModel<NothingSpace, TimedAgent, VecStore<TimedAgent>, (), StdRng>;

fn timed_action(
    agent: &mut TimedAgent,
    ctx: &mut EventContext<'_, NothingSpace, TimedAgent, (), StdRng>,
) {
    agent.handled += 1;
    agent.remaining -= 1;
    if agent.remaining > 0 {
        ctx.add_event(agent.id, 0, 1.0);
    }
}

#[test]
fn event_queue_soak_handles_large_rescheduling_volume() {
    const AGENT_COUNT: u64 = 5_000;
    const EVENTS_PER_AGENT: u32 = 8;

    let mut store = VecStore::new();
    for id in 1..=AGENT_COUNT {
        store.insert(TimedAgent {
            id,
            remaining: EVENTS_PER_AGENT,
            handled: 0,
        });
    }

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

    for id in 1..=AGENT_COUNT {
        model.add_event(id, 0, 1.0);
    }

    let t0 = Instant::now();
    model.run_events((AGENT_COUNT as usize) * (EVENTS_PER_AGENT as usize));
    let elapsed_ms = t0.elapsed().as_millis();

    assert!(model.queue_is_empty());
    assert!((model.time_f64() - EVENTS_PER_AGENT as f64).abs() < 1e-10);

    for id in 1..=AGENT_COUNT {
        let agent = model.agent(id).unwrap();
        assert_eq!(agent.handled, EVENTS_PER_AGENT);
        assert_eq!(agent.remaining, 0);
    }

    eprintln!(
        "[event soak] {} agents x {} events completed in {} ms",
        AGENT_COUNT, EVENTS_PER_AGENT, elapsed_ms
    );
}

#[test]
#[ignore]
fn scale_profile_standard_model_large_population() {
    const AGENT_COUNT: u64 = 250_000;
    const STEP_COUNT: usize = 50;

    let mut store = VecStore::new();
    for id in 1..=AGENT_COUNT {
        store.insert(Particle {
            id,
            x: 0.0,
            vx: 0.001,
        });
    }

    let mut model = ParticleModel::new(
        store,
        NothingSpace,
        ById::new(),
        (),
        StdRng::seed_from_u64(42),
        Some(Box::new(particle_step)),
        None,
        true,
    );

    let t0 = Instant::now();
    model.step_n(STEP_COUNT);
    let elapsed_ms = t0.elapsed().as_millis();

    assert_eq!(model.agents_len(), AGENT_COUNT as usize);
    assert_eq!(model.time(), Time::Discrete(STEP_COUNT as u64));
    assert!((model.agent(1).unwrap().x - (STEP_COUNT as f32 * 0.001)).abs() < 1e-5);

    eprintln!(
        "[core scale] {} agents x {} steps completed in {} ms",
        AGENT_COUNT, STEP_COUNT, elapsed_ms
    );
}