-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
MultiGrid._empty_mask not updated correctly - select_cells(only_empty=True) returns occupied cells #3017
Description
MultiGrid.place_agent() and remove_agent() do not correctly update the _empty_mask array, which causes select_cells(only_empty=True) to incorrectly return cells that already contain agents.
The root cause is that in MultiGrid, the _empty_mask update is placed inside the if self._empties_built: conditional block, so it only executes when that flag is True. This differs from SingleGrid where the update is correctly placed outside the conditional and always executes.
As a result, when using MultiGrid with features like select_cells(only_empty=True), occupied cells are returned as "empty", which can lead to agents being placed on top of each other and incorrect simulation results.
Expected behavior
After placing an agent at position (1,1) on a MultiGrid:
_empty_mask[1,1]should beFalse(indicating the cell is occupied)select_cells(only_empty=True)should NOT include(1,1)in the results- The behavior should match
SingleGrid
Example of expected output:
MultiGrid._empty_mask[1,1] = False
select_cells(only_empty=True) returns 8 cells (out of 9)
(1,1) correctly excluded from empty cells
To Reproduce
Here is a minimal reproducible example that demonstrates the bug:
"""
Bug Reproduction: MultiGrid._empty_mask not updated correctly
This script demonstrates that MultiGrid.place_agent() does not update
the _empty_mask array, causing select_cells(only_empty=True) to return
cells that have agents in them.
"""
from mesa import Agent, Model
from mesa.space import MultiGrid, SingleGrid
class TestAgent(Agent):
"""Simple agent for testing."""
pass
# Create model and grids
model = Model()
single_grid = SingleGrid(3, 3, torus=False)
multi_grid = MultiGrid(3, 3, torus=False)
# Create agents
agent1 = TestAgent(model)
agent2 = TestAgent(model)
print("=" * 60)
print("TEST 1: Compare _empty_mask after placing agent")
print("=" * 60)
# Place agents at (1,1) on both grids
single_grid.place_agent(agent1, (1, 1))
multi_grid.place_agent(agent2, (1, 1))
print(f"\nAfter placing agent at (1,1):")
print(f" SingleGrid._empty_mask[1,1] = {single_grid._empty_mask[1,1]}")
print(f" MultiGrid._empty_mask[1,1] = {multi_grid._empty_mask[1,1]}")
print(f"\n Expected: Both should be False (cell is occupied)")
# Verify actual cell state
print(f"\nActual cell state (ground truth):")
print(f" SingleGrid.is_cell_empty((1,1)) = {single_grid.is_cell_empty((1, 1))}")
print(f" MultiGrid.is_cell_empty((1,1)) = {multi_grid.is_cell_empty((1, 1))}")
print("\n" + "=" * 60)
print("TEST 2: Impact on select_cells(only_empty=True)")
print("=" * 60)
# Create fresh MultiGrid for this test
model2 = Model()
grid = MultiGrid(3, 3, torus=False)
agent = TestAgent(model2)
grid.place_agent(agent, (1, 1))
empty_cells = grid.select_cells(only_empty=True)
print(f"\nMultiGrid 3x3 with 1 agent at (1,1):")
print(f" Total cells: 9")
print(f" Agent at: (1,1)")
print(f" select_cells(only_empty=True) returns: {len(empty_cells)} cells")
print(f" Expected: 8 cells")
print(f"\n Cells returned: {empty_cells}")
if (1, 1) in empty_cells:
print(f"\n BUG: (1,1) is in 'empty' list but has an agent!")
else:
print(f"\n Correct: (1,1) not in 'empty' list")
print("\n" + "=" * 60)
print("TEST 3: Cross-reference _empties set vs _empty_mask")
print("=" * 60)
# These two data structures should be in sync
model3 = Model()
grid3 = MultiGrid(3, 3, torus=False)
agent3 = TestAgent(model3)
# Force _empties to be built
_ = grid3.empties
print(f"\nBefore placing agent:")
print(f" (1,1) in _empties: {(1,1) in grid3._empties}")
print(f" _empty_mask[1,1]: {grid3._empty_mask[1,1]}")
grid3.place_agent(agent3, (1, 1))
print(f"\nAfter placing agent at (1,1):")
print(f" (1,1) in _empties: {(1,1) in grid3._empties}")
print(f" _empty_mask[1,1]: {grid3._empty_mask[1,1]}")
if (1,1) not in grid3._empties and grid3._empty_mask[1,1] == True:
print(f"\n INCONSISTENCY: _empties says occupied, but _empty_mask says empty!")Output showing the bug:
============================================================
TEST 1: Compare _empty_mask after placing agent
============================================================
After placing agent at (1,1):
SingleGrid._empty_mask[1,1] = False
MultiGrid._empty_mask[1,1] = True
Expected: Both should be False (cell is occupied)
Actual cell state (ground truth):
SingleGrid.is_cell_empty((1,1)) = False
MultiGrid.is_cell_empty((1,1)) = False
============================================================
TEST 2: Impact on select_cells(only_empty=True)
============================================================
MultiGrid 3x3 with 1 agent at (1,1):
Total cells: 9
Agent at: (1,1)
select_cells(only_empty=True) returns: 9 cells
Expected: 8 cells
Cells returned: [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]
BUG: (1,1) is in 'empty' list but has an agent!
============================================================
TEST 3: Cross-reference _empties set vs _empty_mask
============================================================
Before placing agent:
(1,1) in _empties: True
_empty_mask[1,1]: True
After placing agent at (1,1):
(1,1) in _empties: False
_empty_mask[1,1]: True
INCONSISTENCY: _empties says occupied, but _empty_mask says empty!
Additional context
Root Cause:
Looking at the source code in mesa/space.py:
SingleGrid (correct implementation) - lines 993-996:
if self._empties_built:
self._empties.discard(pos)
self._empty_mask[pos] = False # OUTSIDE the if blockMultiGrid (buggy implementation) - lines 1043-1045:
if self._empties_built:
self._empties.discard(pos)
self._empty_mask[agent.pos] = True # INSIDE the if block!The _empty_mask update in MultiGrid is:
- Inside the conditional block (only runs sometimes)
- Also uses
Trueinstead ofFalse
Proposed Fix:
# MultiGrid.place_agent - move _empty_mask update outside if block:
if self._empties_built:
self._empties.discard(pos)
self._empty_mask[pos] = False # Always update, use False for occupied
# MultiGrid.remove_agent - similar fix:
if self._empties_built and self.is_cell_empty(pos):
self._empties.add(pos)
if self.is_cell_empty(pos):
self._empty_mask[pos] = True # Update when cell becomes emptyImpact:
- Any simulation using
MultiGridwithselect_cells(only_empty=True)gets wrong results - Agents can be incorrectly placed on occupied cells
- Silent data corruption (no error is raised)