Spatial Environments
3.1 Spatial Environments
Agent-based models often require agents to be embedded in a spatial structure --- a grid representing a city, a network representing interbank lending, or a continuous plane representing a physical environment. The SDK provides three spatial backends, each with different trade-offs between structural simplicity and geometric expressiveness.
GridSpace
GridSpace embeds agents on a discrete 2D grid. Each cell can hold one agent (default) or multiple agents (multi_agent=True). Neighbourhoods are either Von Neumann (4-connected: north, south, east, west) or Moore (8-connected: including diagonals). The grid can optionally wrap toroidally, where the left edge connects to the right and the top connects to the bottom, eliminating boundary effects.
class GridSpace(Space):
def __init__(
self,
model: ABMModel,
rows: int, # Number of rows
cols: int, # Number of columns
wrap: bool = False, # Toroidal wrapping
link_type: str = "grid", # Link type for spatial links
multi_agent: bool = False, # Allow multiple agents per cell
neighborhood: Union[Neighborhood, str] = Neighborhood.VON_NEUMANN,
) -> None
Key methods:
| Method | Signature | Description |
|---|---|---|
place_agent | (agent, position: Tuple[int, int]) -> None | Place an agent at a grid cell |
move_agent | (agent, new_position: Tuple[int, int]) -> None | Move agent to a new cell |
get_neighbors | (agent, radius: int = 1) -> List[Agent] | Agents in neighbourhood |
get_agents_at | (position: Tuple[int, int]) -> List[Agent] | Agents at a specific cell |
get_neighborhood | (row, col, radius=1, include_center=False) -> List[Tuple[int,int]] | Cell positions in neighbourhood |
get_cell_property | (position, name: str) -> Any | Read cell-level property |
set_cell_property | (position, name: str, value: Any) -> None | Write cell-level property |
distance | (pos1, pos2) -> float | Manhattan distance (respects wrapping) |
GridSpace is used by spatial models such as forest fire (fire spreading between adjacent cells), Schelling segregation (agents relocating based on neighbourhood composition), SIR on a grid (local disease transmission), and Game of Life (cellular automaton rules).
For frontend grid visualisation, agents can declare a __color_map__ class attribute that maps state values to CSS colour strings. The frontend's Canvas-based grid renderer uses this mapping to colour cells by state (e.g., {"TREE": "#228B22", "BURNING": "#FF4500", "ASH": "#808080"}).
from simudyne.engine.space import GridSpace, Neighborhood
# 30x30 toroidal grid with Moore (8-connected) neighbourhood
grid = GridSpace(model, rows=30, cols=30, wrap=True, neighborhood=Neighborhood.MOORE)
grid.place_agent(tree_agent, (15, 15))
neighbours = grid.get_neighbors(tree_agent, radius=1) # Up to 8 agents
NetworkSpace
NetworkSpace embeds agents on an arbitrary graph where edges represent relationships (interbank lending, social contacts, supply chain links). Unlike GridSpace, there is no fixed dimensionality --- the topology is entirely determined by the edges. Neighbourhood queries use breadth-first search up to the specified radius.
class NetworkSpace(Space):
def __init__(
self,
model: ABMModel,
link_type: str = "network",
) -> None
Key methods:
| Method | Signature | Description |
|---|---|---|
add_node | (node_id: str, **properties) -> None | Add a node to the graph |
add_edge | (node_a: str, node_b: str, weight: float = 1.0) -> None | Add an edge |
remove_edge | (node_a: str, node_b: str) -> None | Remove an edge |
place_agent | (agent, position: str) -> None | Associate agent with a node |
get_neighbors | (agent, radius: int = 1) -> List[Agent] | BFS neighbours up to radius |
distance | (pos1: str, pos2: str) -> float | Shortest-path distance |
NetworkSpace is typically used with the topology generators (Section 3.2) to create standard graph structures. It is used by models such as Gai-Kapadia (scale-free interbank network), SIR on networks (small-world contact network), and counterparty credit (bilateral exposure graph).
from simudyne.engine.space import NetworkSpace
net = NetworkSpace(model, link_type="interbank")
# Topology is built separately via generators (see Section 3.2)
Space2D (Continuous 2D)
Space2D provides a continuous 2D spatial environment where agents have real-valued (x, y) coordinates rather than discrete grid cells. It uses a spatial hash grid for O(1) amortised radius queries: the continuous plane is subdivided into coarse cells, and agents are indexed by cell. Radius queries check only cells that overlap the query circle, avoiding full-population scans.
class Space2D:
def __init__(
self,
model: ABMModel,
width: float, # Width of the 2D plane
height: float, # Height of the 2D plane
wrap: bool = False, # Toroidal wrapping
cell_size: Optional[float] = None, # Hash grid cell size (auto-tuned if None)
) -> None
Key methods:
| Method | Signature | Description |
|---|---|---|
place_agent | (agent, x: float, y: float, visibility_radius=inf) -> None | Place at continuous coordinates |
move_agent | (agent, new_x: float, new_y: float) -> None | Move to new coordinates |
remove_agent | (agent) -> None | Remove from space |
get_agents_in_radius | (x, y, radius, exclude=None) -> List[Tuple] | Agents within radius (id, x, y, dist) |
distance | (agent_a, agent_b) -> float | Euclidean distance |
add_layer | (name, resolution_x=200, resolution_y=200) -> FieldLayer | Add a scalar field |
get_layer | (name) -> FieldLayer | Retrieve a field layer |
has_vicinity_changed | (agent) -> bool | Dirty-cell check for delta observations |
Space2D also supports field layers --- 2D NumPy arrays that represent continuous scalar fields (pheromone concentrations, nutrient gradients, temperature maps). Field layers support bilinear interpolation for smooth sampling, vectorised evaporation and diffusion, and dirty-cell tracking so that agents only receive updated observations when their vicinity has actually changed.
class FieldLayer:
def __init__(self, name: str, width: float, height: float,
resolution_x: int = 200, resolution_y: int = 200,
wrap: bool = False) -> None
def deposit(self, x: float, y: float, amount: float) -> None
def sample(self, x: float, y: float) -> float
def gradient(self, x: float, y: float) -> Tuple[float, float]
def evaporate(self, factor: float) -> None
def diffuse(self, rate: float) -> None
The Space2DEnvironment extends Environment with push-based observations: instead of agents pulling their neighbourhood state, the environment pushes an Observation object to each agent only when its vicinity has changed (detected via dirty-cell tracking in the spatial hash grid and field layers). This dramatically reduces redundant computation in models with localised interactions.
Space2D is used by the ant colony model (pheromone-based foraging with two field layers on a toroidal plane) and the global supply chain model (supply_chain_v3), which places 50 agents across a geographic coordinate system with tiered hub routing and GeoMap visualisation in the frontend.
class Space2DEnvironment(Environment):
def __init__(self, width=100.0, height=100.0, wrap=False,
cell_size=None, visibility=None, **kwargs) -> None
The VisibilityConfig controls what agents can see about their neighbours:
| Level | Agents See | Use Case |
|---|---|---|
ANONYMOUS | Count of neighbours only | Pure density-dependent behaviour |
IDS_ONLY | Agent IDs and distances | Targeted messaging without state leakage |
PARTIAL_STATE | Selected state fields (configurable) | Controlled information sharing |
FULL_STATE | All state fields | Full observability (development/debugging) |