Skip to content

MultiGrid._empty_mask not updated correctly - select_cells(only_empty=True) returns occupied cells #3017

@Nithin9585

Description

@Nithin9585

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:

  1. _empty_mask[1,1] should be False (indicating the cell is occupied)
  2. select_cells(only_empty=True) should NOT include (1,1) in the results
  3. 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 block

MultiGrid (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:

  1. Inside the conditional block (only runs sometimes)
  2. Also uses True instead of False

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 empty

Impact:

  • Any simulation using MultiGrid with select_cells(only_empty=True) gets wrong results
  • Agents can be incorrectly placed on occupied cells
  • Silent data corruption (no error is raised)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugRelease notes label

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions