Conceptual model of Space #2585
Replies: 16 comments 45 replies
-
|
Thanks for this. It's really helpful. My 2 cents:
|
Beta Was this translation helpful? Give feedback.
-
For me, this is a no-go. The entire point of a space is that it defines what is close and far and thus specifies neighborhood relationships. This is why I agree with you that, conceptually, an agent can be part of multiple spaces. The issue thus revolves around having agent classes that allow for multiple "locations", which is trivial to implement already on a case-by-case basis, but very hard to implement generically. That is, in my view, nothing needs to change in MESA itself because it is already possible but requires some coding by the user. On the property side of things, I am not convinced we need to change anything either. Properties are currently tied to discrete spaces only and follow the coordinate system of that space. Why would that need to change even if an agent can be part of multiple spaces? Properties are understood as being properties of that space so why decouple them. |
Beta Was this translation helpful? Give feedback.
-
|
I've been in discussion with some people (@SongshGeo @AdamZh0u @LunnyRia @peter-kinger and a few others) who are interested in working on mesa/mesa-geo#201, and we came up with a proposed design for mesa-geo v1.0 (see figure below). Not sure whether it relates to this discussion, but the new design is very similar to the experimental cell space. Linking to Mesa spaces, we can think of the VectorLayer as the ContinuousSpace, and RasterLayer as the GridSpace. This perhaps resembles the idea of having multiple spaces (i.e., layers in mesa-geo) discussed above. We may even go beyond spaces and have a ModelCollection that allows stepping several models inside a metamodel, in a hierarchical manner. I don't know whether this is what @tpike3 wants for a multi-level mesa. Nonetheless it is beyond the scope of our current discussions and thus not included in the figure. |
Beta Was this translation helpful? Give feedback.
-
|
I want to challenge one of our core assumptions: Do agents actually need a single unified position? The "one true position" idea sounds elegant, but it breaks down when we consider different space types:
These aren't interchangeable or meaningfully convertible. A social network node isn't a coordinate you can map to (x, y). An agent's position in an "economic space" based on wealth similarity has no physical location. Two Distinct ConceptsI think we're conflating:
Architectural Inconsistency in Current MesaLooking at our current implementations, we already have two different architectures: Discrete Spaces (cell_space):
Continuous Spaces (experimental):
This inconsistency suggests we need to think about architectural unification. Options for Unifying SpacesOption A: Space Owns EverythingMake discrete spaces work like continuous spaces - space owns all agent-location mappings: # All spaces own the agent→location relationship
class DiscreteSpace:
def get_cell(self, agent) -> Cell:
return self._agent_to_cell[agent]
def move_agent(self, agent, new_cell):
# Space manages the relationship
...
class ContinuousSpace:
def get_position(self, agent) -> np.ndarray:
return self._agent_positions[self._agent_to_index[agent]]
# Agents query spaces
neighbors = space.get_neighbors(agent, radius=5)Pros: Consistent architecture, space controls optimization, clear separation of concerns Option B: Protocol-BasedDefine what spaces must provide, but don't enforce how: class SpatialSpace(Protocol):
def get_location(self, agent: Agent) -> Any: ...
def move_agent(self, agent: Agent, location: Any) -> None: ...
def get_neighbors(self, agent: Agent, **kwargs) -> list[Agent]: ...
# Each space implements however makes sense internally
class DiscreteSpace: # Can keep current cell-based implementation
def get_location(self, agent):
return self._agent_to_cell[agent]
class ContinuousSpace: # Uses efficient numpy arrays
def get_location(self, agent):
return self._agent_positions[self._agent_to_index[agent]]
# Usage is explicit and clear
physical_neighbors = model.physical_space.get_neighbors(agent, radius=5)
social_contacts = model.social_network.get_neighbors(agent, hops=2)Pros: Maximum flexibility, no required inheritance, easy multiple spaces, explicit queries Option C: Hybrid ApproachSpace owns data, but agents have convenience properties: # Space is source of truth
class DiscreteSpace:
def get_cell(self, agent) -> Cell:
return self._agent_to_cell[agent]
# Agent has convenience property that delegates
class CellAgent(Agent):
@property
def cell(self):
return self._space.get_cell(self)
@cell.setter
def cell(self, new_cell):
self._space.move_agent(self, new_cell)Pros: Keeps convenient API, space still controls data, backward compatible When Spaces AlignThe powerful case is when multiple spaces DO share a coordinate system: class FireModel(Model):
def __init__(self):
# These spaces share coordinates - fire on ground affects air
self.ground = CellSpace(100, 100, coordinate_system="physical")
self.air = ContinuousSpace([[0, 100], [0, 100]], coordinate_system="physical")
class Tree(Agent):
def __init__(self, model, position):
super().__init__(model)
model.ground.add_agent(self, position)
def step(self):
# Query different space types using same coordinate system
my_cell = self.model.ground.get_cell(self)
air_temp = self.model.air.get_property("temperature", my_cell.center)ConsiderationsI'm not sure which way to go at this point. Protocol-based might be the most flexible, it has a few advantages:
This means:
Open questions:
Impact on Agent CompositionThis protocol-based approach also solves the agent composition complexity discussed in https://github.com/projectmesa/mesa/pull/2584/files#r1907499549. If agents don't need space-specific base classes, the composition problem disappears: # No more ContinuousSpaceAgent, CellAgent, etc.
# Just Agent querying whatever spaces it needs
class Boid(Agent):
def __init__(self, model):
super().__init__(model)
model.air_space.add_agent(self, position=[50, 50])
def step(self):
neighbors = self.model.air_space.get_neighbors(self, radius=5)
class HybridAgent(Agent):
def __init__(self, model):
super().__init__(model)
# Register in multiple spaces - no special inheritance needed
model.physical_space.add_agent(self, position=[30, 40])
model.social_network.add_agent(self, node=5)
def step(self):
# Query whichever space is relevant
physical_neighbors = self.model.physical_space.get_neighbors(self, radius=10)
social_contacts = self.model.social_network.get_neighbors(self, hops=2)No capability injection, no multiple inheritance, no magic - just agents querying spaces explicitly. I'm increasingly thinking the explicit space query approach is cleaner and more aligned with our "spaces own the relationship logic" philosophy. Agents don't need special capabilities - they just need to know which space(s) to query. Curious what everybody thinks! |
Beta Was this translation helpful? Give feedback.
-
|
I don't follow your claimed inconsistency. In discrete spaces, we have |
Beta Was this translation helpful? Give feedback.
-
|
I think I compiled an "ultimate" use case which you are doing about everything you can possibly want to do in an spatial-based ABM. We can use it to further develop our conceptual model of space. Note that:
Model overviewA simplified agent-based model where citizens decide whether to migrate between cities based on environmental conditions, transportation networks, and social connections. A region contains multiple cities connected by road and rail networks, where citizens live, work, and travel. Citizens make daily travel decisions (commuting, visiting friends, business trips) using either roads or rails, with road speeds affected by congestion. Over longer timescales, citizens may decide to permanently migrate to different cities based on environmental conditions (temperature, agricultural fertility), economic opportunities, social connections (where their friends live), and ideological compatibility (finding like-minded people). The atmospheric conditions provide environmental context that affects both daily comfort and long-term livability, while the fertility layer influences the economic prosperity of different regions. Spaces1. Air Space (Continuous)
2. City Space (Voronoi Cells)
3. Road Network
4. Rail Network
5. Fertility Layer
6. Social Network
7. Perception Space
Agents: CitizensPosition References
Key Behaviors
Testing RequirementsMultiple coordinate systems:
Different position types:
Cross-space queries:
|
Beta Was this translation helpful? Give feedback.
-
|
In #2875, while adding support for multiple spaces in agents, I mentioned in passing the idea of "derived" spaces. That is, an agent has a location in a primary space, and other spaces can inherit/use this location as well:
Thinking about this a bit more, I realized that something analogous already exists in matplotlib with We might. consider doing something similar in mesa by adding a space = ContinuousSpace([[0, 10], [0,10]], random=model.random)
another_space = Network(graph, share_location=space) |
Beta Was this translation helpful? Give feedback.
-
|
(I'm kicking in some open doors in this post, but better to have them explicit I think) @quaquel I was thinking about your "spaces define neighbours/distance". I think this is a powerful idea we should dive deeper in. So what if spaces are defined as query layers, not containers? An agent being "in" a space just means "include me in neighbor queries for this space." You could an API as clean as this I think: class Model:
def __init__(self):
super().__init__()
# Spaces are just query layers
self.space = ContinuousSpace([[0, 100], [0, 100]])
self.social_network = Network()
self.administrative_grid = VoronoiGrid(centroids)
class Agent:
def __init__(self, model):
super().__init__(model)
# Only an optional physical position
self.position = None
# No forced space membership!
# Agents don't need to know about spaces at all
# Agent presence is managed by the space
class MyAgent(Agent):
def __init__(self, model, physical_pos=None, social_connections=None):
super().__init__(model)
# Optional: register in spaces you care about
if physical_pos is not None:
model.space.add(self, position=physical_pos)
if social_connections is not None:
model.social_network.add(self, connections=social_connections)Agents can now also not be in any space, registration is relatively easy (and explicit!).
We could define a space as a class Space(Protocol):
"""Any space just needs membership and neighbor queries"""
def add(self, agent: Agent, position: ArrayLike) -> None:
"""Add agent at position (sets agent.position)"""
...
def remove(self, agent: Agent) -> None:
"""Remove agent from this space"""
...
def has_agent(self, agent: Agent) -> bool:
"""Check membership"""
...
def get_neighbors(self, agent: Agent, **criteria) -> AgentSet:
"""Get neighbors using agent.position"""
...
@property
def agents(self) -> AgentSet:
"""All agents in this space"""
...Then is the big question: How to make it performant?
My proposal would be to first hash out an API an a slow, but functional minimal version, and then try to make it performant (the latter might be a GSoC project). |
Beta Was this translation helpful? Give feedback.
-
|
| Concern | Where | Uses position? | Example |
|---|---|---|---|
| Coordinate system | model.world.coords |
Defines it | Bounds, validation, wrapping |
| Continuous queries | model.world or agent |
Yes (reads) | Distance, radius queries |
| Structural queries | model.world.grid |
Yes (indexes) | Cell-based neighbors |
| Spatial topology | model.world.roads |
Yes (nodes) | Graph on positions |
| Non-spatial topology | model.social_network |
No | Pure connections |
Complete API Example
class UrbanModel(Model):
def __init__(self, width=100, height=100):
super().__init__()
# Define the spatial world (shared coordinate system)
self.world = World(
coords=CoordinateSystem(x=(0, width), y=(0, height)),
torus=False
)
# Add spatial layers (all use agent.position in world.coords)
self.world.add_layer("grid", Grid(cell_size=5.0))
self.world.add_layer("districts", VoronoiGrid(centroids=[...]))
self.world.add_layer("roads", Network()) # Spatial network
# Non-spatial structures (don't use position)
self.social_network = Network() # Friendships
self.company_hierarchy = HierarchyTree() # Org chart
# Create agents
for _ in range(100):
Citizen(self)
class Citizen(Agent):
def __init__(self, model):
super().__init__(model)
# ONE position for all spatial layers
self.position = model.world.random_position()
# Register in spatial layers (all read self.position)
model.world.grid.add(self)
model.world.districts.add(self)
model.world.roads.add(self)
# Non-spatial connections (don't use position)
# (added later via social_network.add_edge)
def step(self):
# Continuous query: who's physically nearby? (uses position directly)
visible = self.world.get_neighbors_in_radius(
self,
radius=self.vision_range
)
# Grid query: same cell neighbors (position -> cell -> neighbors)
same_block = self.world.grid.get_neighbors(self, distance=0)
# Voronoi query: same district (position -> region -> neighbors)
district_members = self.world.districts.get_neighbors(self)
# Spatial network: connected by roads (follows edges between positions)
road_accessible = self.world.roads.get_neighbors(self, hops=3)
# Non-spatial network: social connections (ignores position entirely)
friends = self.model.social_network.neighbors(self)
# Move in physical space - all spatial layers see the update
target = self.find_destination()
self.world.move_toward(self, target, speed=1.0)
# Layer-specific property: what's the terrain here?
elevation = self.world.grid.get_property(self, "elevation")What Gets Reused from Current Mesa?
✅ Reuse Heavily:
- PropertyLayer system - already well-designed
- Spatial indexing from
experimental.continuous_space- the NumPy array management and distance calculations - Grid connection patterns - the offset logic for Moore, Von Neumann, Hex
- AgentSet - perfect for returning neighbor queries
⚠️ Adapt:
- Cell concept - becomes derived from position, not a container
- Movement helpers - adapt to work with
agent.position
❌ Replace:
- The "space contains agents" model
- Bidirectional cell ↔ agent relationships
- CellAgent as the primary agent type
Benefits
- Single source of truth:
agent.positionis used by all spatial layers - Clear semantics:
model.world= things sharing a coordinate system - Intuitive separation: Spatial (roads network) vs non-spatial (social network) is explicit
- Discoverable:
model.world.<tab>shows all spatial operations - Flexible: Multiple spatial views of the same geometric reality
- Performant: Each layer can optimize indexing for its structure
- No synchronization needed: All layers read the same
agent.position
Curious if this is a direction worth pursuing.
Beta Was this translation helpful? Give feedback.
-
|
I am not sure about a lot of this.
|
Beta Was this translation helpful? Give feedback.
-
|
Based on the pathfinding and discussion the last few days, I think I got a bit closer:
So that leaves you with two ways to update the agent position:
|
Beta Was this translation helpful? Give feedback.
-
|
I don't think we should overcomplicate multiple space membership. Basically:
Few example for when this would be useful:
We also shouldn't overcomplicate it. NetLogo only has continuous positions of Agents and cells (patches) for properties. Honestly, good for 90% of use cases. I would personally approach it this way:
Agents in my cell or in a next cell (concepts of neighbours) are just aggerating based on a definition of cell division and proximity according to that. So, in my visions, cells are just standardized ways to structure continuous data into more easy to handle units. A big advantage: an |
Beta Was this translation helpful? Give feedback.
-
|
Seems relevant to cross-post this comment here: #3072 (comment)
|
Beta Was this translation helpful? Give feedback.
-
|
@quaquel and I had an extensive in-person discussion. We had the following conclusions:
|
Beta Was this translation helpful? Give feedback.
-
|
I've been following this discussion with great interest, especially the recent conclusions from discussion. Let me first summarise my understanding: 1. Three-Tier Agent Hierarchy
2. Position as Single Source of Truth
3. Locatable Protocol
4. Translation Functions
5. Property Layers
I have also thoroughly reviewed #2875 and #3043. Are you open to me exploring some initial checklists for Space architecture in #3132 (especially Cell Descriptors ). If yes, do you prefer some draft PRs or detailed ideas here along with some code snippets? |
Beta Was this translation helpful? Give feedback.
-
What I Think
|
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Update: Currently aligned direction is summarized here: #2585 (comment)
Before we stabilize the cell space and new continuous space, I would like a discussion how we view spaces, properties and agents, and how they relate or interact to each other.
This is how I currently envision it for Mesa:
Mesa-Space-Conceptual-diagram.pptx
An Agent has a singular position in a single coordinate system. There can be multiple spaces (in which Agents are or are not present (see Q1). Each space can have 0, 1 or multiple properties (see Q2) which have the same "resolution" as the space (see Q3). All properties, spaces and Agents can interact with each other, because they all use a singular coordinate system.
Open questions:
Really curious on all the ideas how conceptually spaces should look in Mesa!
Beta Was this translation helpful? Give feedback.
All reactions