rustsim-core 0.0.1

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

use rand::Rng;
use rustsim_core::{
    interaction::{PositionedAgent, SpaceInteraction},
    space::Space,
    types::AgentId,
};
use std::fmt;

#[derive(Debug, Default, Clone, Copy)]
pub struct NothingSpace;

impl Space for NothingSpace {}

impl<A> SpaceInteraction<A> for NothingSpace
where
    A: PositionedAgent<Position = ()>,
{
    type Error = std::convert::Infallible;

    fn random_position<R: rand::RngCore>(&self, _rng: &mut R) -> A::Position {}

    fn add_agent(&mut self, _agent: &A) -> Result<(), Self::Error> {
        Ok(())
    }

    fn remove_agent(&mut self, _agent: &A) -> Result<(), Self::Error> {
        Ok(())
    }

    fn nearby_ids(&self, _position: &A::Position, _radius: usize) -> Vec<AgentId> {
        Vec::new()
    }
}

pub type GridPos2 = (usize, usize);

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GridError {
    OutOfBounds(GridPos2),
}

impl fmt::Display for GridError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::OutOfBounds((x, y)) => write!(formatter, "position ({x}, {y}) is out of bounds"),
        }
    }
}

#[derive(Debug, Clone)]
pub struct Grid2D {
    width: usize,
    height: usize,
    periodic: bool,
    cells: Vec<Vec<AgentId>>,
}

impl Grid2D {
    pub fn new(width: usize, height: usize, periodic: bool) -> Self {
        assert!(width > 0 && height > 0, "grid dimensions must be positive");
        Self {
            width,
            height,
            periodic,
            cells: vec![Vec::new(); width * height],
        }
    }

    fn normalize_pos(&self, pos: GridPos2) -> Option<GridPos2> {
        if self.periodic {
            Some((pos.0 % self.width, pos.1 % self.height))
        } else if pos.0 < self.width && pos.1 < self.height {
            Some(pos)
        } else {
            None
        }
    }

    fn index(&self, pos: GridPos2) -> usize {
        pos.1 * self.width + pos.0
    }
}

impl Space for Grid2D {}

impl<A> SpaceInteraction<A> for Grid2D
where
    A: PositionedAgent<Position = GridPos2>,
{
    type Error = GridError;

    fn random_position<R: rand::RngCore>(&self, rng: &mut R) -> A::Position {
        (rng.gen_range(0..self.width), rng.gen_range(0..self.height))
    }

    fn add_agent(&mut self, agent: &A) -> Result<(), Self::Error> {
        let pos = *agent.position();
        let normalized = self.normalize_pos(pos).ok_or(GridError::OutOfBounds(pos))?;
        let index = self.index(normalized);
        self.cells[index].push(agent.id());
        Ok(())
    }

    fn remove_agent(&mut self, agent: &A) -> Result<(), Self::Error> {
        let pos = *agent.position();
        let normalized = self.normalize_pos(pos).ok_or(GridError::OutOfBounds(pos))?;
        let index = self.index(normalized);
        if let Some(cell_index) = self.cells[index]
            .iter()
            .position(|candidate| *candidate == agent.id())
        {
            self.cells[index].swap_remove(cell_index);
        }
        Ok(())
    }

    fn nearby_ids(&self, position: &A::Position, radius: usize) -> Vec<AgentId> {
        let mut ids = Vec::new();
        let radius = radius as i32;
        let (center_x, center_y) = (position.0 as i32, position.1 as i32);
        for dy in -radius..=radius {
            for dx in -radius..=radius {
                let x = center_x + dx;
                let y = center_y + dy;
                let pos = if self.periodic {
                    let wrapped_x =
                        ((x % self.width as i32) + self.width as i32) % self.width as i32;
                    let wrapped_y =
                        ((y % self.height as i32) + self.height as i32) % self.height as i32;
                    Some((wrapped_x as usize, wrapped_y as usize))
                } else if x >= 0 && y >= 0 && x < self.width as i32 && y < self.height as i32 {
                    Some((x as usize, y as usize))
                } else {
                    None
                };

                if let Some(pos) = pos {
                    ids.extend(self.cells[self.index(pos)].iter().copied());
                }
            }
        }
        ids
    }
}