rustsim 0.0.1

High-performance agent-based modelling engine - top-level orchestration crate
Documentation
//! Demonstrates using `rustsim` as an external library dependency.
//! An external Rust application would add `rustsim = { path = "..." }` (or a registry version)
//! to its `Cargo.toml` and then write code exactly like this.

use rand::rngs::StdRng;
use rand::SeedableRng;
use rustsim::prelude::*;

// ?? 1. Define an agent ??????????????????????????????????????????????
#[derive(Debug, Clone)]
struct Boid {
    id: AgentId,
    pos: rustsim_spaces::continuous::ContinuousPos,
    speed: f64,
}

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

impl PositionedAgent for Boid {
    type Position = rustsim_spaces::continuous::ContinuousPos;
    fn position(&self) -> &Self::Position {
        &self.pos
    }
    fn set_position(&mut self, p: Self::Position) {
        self.pos = p;
    }
}

// ?? 2. Define model types ???????????????????????????????????????????
type BoidModel = StandardModel<
    rustsim_spaces::continuous::ContinuousSpace2D,
    Boid,
    HashMapStore<Boid>,
    (), // no extra properties
    StdRng,
    Fastest,
>;

fn boid_step(
    boid: &mut Boid,
    _ctx: &mut StepContext<
        '_,
        rustsim_spaces::continuous::ContinuousSpace2D,
        Boid,
        (),
        StdRng,
        Fastest,
    >,
) {
    let new_x = boid.pos.x + boid.speed;
    let new_y = boid.pos.y;
    // clamp inside extent
    boid.pos = rustsim_spaces::continuous::ContinuousPos::new(new_x.min(99.9), new_y);
}

// ?? 3. Build, populate, step, collect ???????????????????????????????
#[test]
fn external_consumer_full_workflow() {
    let space =
        rustsim_spaces::continuous::ContinuousSpace2D::new(100.0, 100.0, false, 5.0).unwrap();
    let store = HashMapStore::new();

    let mut model = BoidModel::new_base(store, space, Fastest::new(), (), StdRng::seed_from_u64(7))
        .with_agent_step_ctx(boid_step);

    // add agents via the interaction API
    for i in 1..=10 {
        let boid = Boid {
            id: model.next_id(),
            pos: rustsim_spaces::continuous::ContinuousPos::new(i as f64, 50.0),
            speed: i as f64 * 0.5,
        };
        add_agent(&mut model, boid).unwrap();
    }

    // step the model 5 times
    model.step_n(5);
    assert_eq!(model.time(), Time::Discrete(5));

    // collect data using collect_step
    let ids: Vec<AgentId> = model.agents().map(|a| a.id()).collect();
    let mut snapshots: Vec<(AgentId, f64)> = Vec::new();
    let mut model_snaps: Vec<Time> = Vec::new();

    collect_step(
        &model,
        &ids,
        Some(&|boid: &Boid, _m: &BoidModel| (boid.id, boid.pos.x)),
        Some(&|m: &BoidModel| m.time()),
        &mut snapshots,
        &mut model_snaps,
    );

    assert_eq!(snapshots.len(), 10);
    assert_eq!(model_snaps, vec![Time::Discrete(5)]);

    // every boid moved right: boid i started at x=i, moved 5*speed per step
    for (id, x) in &snapshots {
        let expected = (*id as f64) + 5.0 * (*id as f64 * 0.5);
        let expected = expected.min(99.9);
        assert!(
            (x - expected).abs() < 1e-9,
            "boid {id}: expected x={expected}, got x={x}"
        );
    }

    // pathfinding works from the same prelude
    let path = astar(
        (0usize, 0usize),
        (3, 3),
        |a, b| ((a.0 as f64 - b.0 as f64).powi(2) + (a.1 as f64 - b.1 as f64).powi(2)).sqrt(),
        |n| {
            let (x, y) = *n;
            let mut neighbors = Vec::new();
            if x + 1 < 5 {
                neighbors.push(((x + 1, y), 1.0));
            }
            if y + 1 < 5 {
                neighbors.push(((x, y + 1), 1.0));
            }
            neighbors
        },
    );
    assert!(path.is_some());

    // compute backend detection doesn't panic
    let _backend = detect_backend();
}