rustsim-core 0.0.1

Core ABM engine: agents, models, stores, schedulers, stepping, data collection
Documentation
//! Agent containers - storage backends for the agent population.
//!
//! Two implementations are provided:
//!
//! - [`HashMapStore`] - backed by `hashbrown::HashMap`. General-purpose; supports
//!   arbitrary agent addition and removal. Best when agents are frequently
//!   created or destroyed.
//!
//! - [`VecStore`] - backed by a `Vec<Option<RefCell<A>>>` indexed by agent ID.
//!   Faster iteration for dense, contiguous ID ranges. Best when agents are
//!   never (or rarely) removed.
//!
//! Both stores use [`RefCell`] for interior mutability, allowing agent step
//! functions to borrow one agent mutably while reading others immutably.
//!
//! # Determinism note
//!
//! Store iteration order is part of simulation reproducibility whenever a
//! scheduler or caller relies on `iter_ids()` / `iter_ids_into()` without an
//! additional ordering step.
//!
//! - [`VecStore`] iterates in ascending ID order.
//! - [`HashMapStore`] iteration order is not guaranteed and must not be treated
//!   as a reproducible activation order.
//!
//! For deterministic activation order across runs, prefer [`crate::scheduler::ById`]
//! or another scheduler that imposes an explicit order.
//!
//! [`RefCell`]: std::cell::RefCell

use hashbrown::HashMap;
use std::cell::{Ref, RefCell, RefMut};

use crate::{agent::Agent, types::AgentId};

/// Trait for agent containers.
///
/// Provides CRUD operations and ID iteration. All access methods take `&self`
/// (not `&mut self`) for reads, using [`RefCell`] internally to support
/// concurrent borrows of different agents.
///
/// [`RefCell`]: std::cell::RefCell
pub trait AgentStore<A: Agent> {
    /// Borrow an agent immutably by ID.
    fn get(&self, id: AgentId) -> Option<Ref<'_, A>>;

    /// Borrow an agent mutably by ID.
    ///
    /// # Panics
    ///
    /// Panics at runtime if the agent is already borrowed (see [`RefCell`] rules).
    ///
    /// [`RefCell`]: std::cell::RefCell
    fn get_mut(&self, id: AgentId) -> Option<RefMut<'_, A>>;

    /// Insert an agent into the store.
    ///
    /// If an agent with the same ID already exists, it is silently replaced.
    fn insert(&mut self, agent: A);

    /// Remove and return an agent by ID, or `None` if not found.
    fn remove(&mut self, id: AgentId) -> Option<A>;

    /// Check whether an agent with the given ID exists.
    fn contains(&self, id: AgentId) -> bool {
        self.get(id).is_some()
    }

    /// Collect all agent IDs into a new `Vec`.
    ///
    /// Prefer [`iter_ids_into`] when a reusable buffer is available.
    ///
    /// [`iter_ids_into`]: AgentStore::iter_ids_into
    fn iter_ids(&self) -> Vec<AgentId>;

    /// Append all agent IDs into `buf` without clearing it first.
    ///
    /// This avoids allocation when `buf` is reused across calls.
    fn iter_ids_into(&self, buf: &mut Vec<AgentId>) {
        buf.extend(self.iter_ids());
    }

    /// Number of agents in the store.
    fn len(&self) -> usize;

    /// Returns `true` if the store contains no agents.
    fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

/// Hash-map-based agent store.
///
/// Backed by [`hashbrown::HashMap`] for O(1) average-case insert, remove,
/// and lookup. Iteration order is not guaranteed and must not be used as a
/// reproducibility contract.
///
/// This is the default store for most simulations.
#[derive(Debug, Default)]
pub struct HashMapStore<A: Agent> {
    agents: HashMap<AgentId, RefCell<A>>,
}

impl<A: Agent> HashMapStore<A> {
    /// Create an empty `HashMapStore`.
    pub fn new() -> Self {
        Self {
            agents: HashMap::new(),
        }
    }
}

impl<A: Agent> AgentStore<A> for HashMapStore<A> {
    fn get(&self, id: AgentId) -> Option<Ref<'_, A>> {
        self.agents.get(&id).map(|cell| cell.borrow())
    }

    fn get_mut(&self, id: AgentId) -> Option<RefMut<'_, A>> {
        self.agents.get(&id).map(|cell| cell.borrow_mut())
    }

    fn insert(&mut self, agent: A) {
        self.agents.insert(agent.id(), RefCell::new(agent));
    }

    fn remove(&mut self, id: AgentId) -> Option<A> {
        self.agents.remove(&id).map(|cell| cell.into_inner())
    }

    fn iter_ids(&self) -> Vec<AgentId> {
        self.agents.keys().copied().collect()
    }

    fn iter_ids_into(&self, buf: &mut Vec<AgentId>) {
        buf.extend(self.agents.keys().copied());
    }

    fn len(&self) -> usize {
        self.agents.len()
    }
}

/// Dense vector-based agent store.
///
/// Uses agent IDs as indices into a `Vec<Option<RefCell<A>>>`. Provides
/// faster iteration than [`HashMapStore`] when IDs form a dense, contiguous
/// range starting near 0.
///
/// Iteration order is ascending by agent ID, which makes it suitable for
/// deterministic workflows when paired with deterministic stepping logic.
///
/// Inserting an agent with a large ID will grow the internal vector to
/// accommodate it, potentially wasting memory for sparse ID spaces.
#[derive(Debug, Default)]
pub struct VecStore<A: Agent> {
    agents: Vec<Option<RefCell<A>>>,
    count: usize,
}

impl<A: Agent> VecStore<A> {
    /// Create an empty `VecStore`.
    pub fn new() -> Self {
        Self {
            agents: Vec::new(),
            count: 0,
        }
    }

    fn ensure_slot(&mut self, id: AgentId) {
        let index = id as usize;
        if index >= self.agents.len() {
            self.agents.resize_with(index + 1, || None);
        }
    }
}

impl<A: Agent> AgentStore<A> for VecStore<A> {
    fn get(&self, id: AgentId) -> Option<Ref<'_, A>> {
        self.agents
            .get(id as usize)
            .and_then(|slot| slot.as_ref().map(|cell| cell.borrow()))
    }

    fn get_mut(&self, id: AgentId) -> Option<RefMut<'_, A>> {
        self.agents
            .get(id as usize)
            .and_then(|slot| slot.as_ref().map(|cell| cell.borrow_mut()))
    }

    fn insert(&mut self, agent: A) {
        let id = agent.id();
        self.ensure_slot(id);
        let slot = &mut self.agents[id as usize];
        if slot.is_none() {
            self.count += 1;
        }
        *slot = Some(RefCell::new(agent));
    }

    fn remove(&mut self, id: AgentId) -> Option<A> {
        if (id as usize) < self.agents.len() {
            let taken = self.agents[id as usize]
                .take()
                .map(|cell| cell.into_inner());
            if taken.is_some() {
                self.count -= 1;
            }
            taken
        } else {
            None
        }
    }

    fn iter_ids(&self) -> Vec<AgentId> {
        self.agents
            .iter()
            .enumerate()
            .filter_map(|(idx, slot)| slot.as_ref().map(|_| idx as AgentId))
            .collect()
    }

    fn iter_ids_into(&self, buf: &mut Vec<AgentId>) {
        for (idx, slot) in self.agents.iter().enumerate() {
            if slot.is_some() {
                buf.push(idx as AgentId);
            }
        }
    }

    fn len(&self) -> usize {
        self.count
    }
}