-
Notifications
You must be signed in to change notification settings - Fork 874
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Adding Cell Properties to Grids in Mesa #1889
Comments
To provide my take on these questions:
|
The current method is to use patch agents, e.g. in the Sugarscape example, https://github.com/projectmesa/mesa-examples/blob/10985d44091b9ba1ecebd013d2d2252e2116649b/examples/sugarscape_g1mt/sugarscape_g1mt/model.py#L92-L105. Patch agents are more general than cell properties in that they are FSM, and they have simple abstraction over how they work, without the need of additional documentation. However, they don't scale to a cell containing lots of patch types. To get the sugar agent in a cell, a loop over the cell content is needed: https://github.com/projectmesa/mesa-examples/blob/10985d44091b9ba1ecebd013d2d2252e2116649b/examples/sugarscape_g1mt/sugarscape_g1mt/trader_agents.py#L53-L62. But then again, this is solvable by combining the |
Thanks for your reply, and the additional context. I think one nut NetLogo cracked very successfully, is how easy and intuitive working with a spatial environment is. One of those aspects is how easy patches can be used and modified. In Mesa I haven't seen that replicated. Especially just being to able to ask something like |
I can see this implemented as combining the existing
One option to be in line with NetLogo would be to define a class class Patch:
def __init__(self, ...):
self.agents = []
self.pcolor = ...
self.sugar = ...
def step(self):
... With this, you can reuse the edit1: rename |
The main point I'm thinking about is that it would be nice to be able to add as much built-in functionality to patches as possible. It should be really intuitive to ask thinks to patches, move to an empty one, get their neighbours, see how many neighbours have some characteristics, etc. Don't know yet how we get there do. How would a Patch as an object fit in that? It should also still be somewhat performant, especially searching for one/all empties or with some characteristic. |
I agree that NetLogo is very expressive, with code that reads like an VSO natural language, while still being OOP (but OOP in the Smalltalk message-passing sense, which is different than most OOP languages). Implementing the
NetLogo is actually often faster than Mesa, in this benchmark. Edit: Replace SVO with VSO |
Thanks for this interesting proposal! Having cell properties would be greatly useful for gis models. Our rainfall and urban_growth models are two examples using cell properties. Currently mesa-geo has classes for raster layers, which contains cells: https://github.com/projectmesa/mesa-geo/blob/e10761e30bef509ea270a96e39decc0d97fc1318/mesa_geo/raster_layers.py#L165. Here cells are essentially just agents. So it is a bit different from your proposed implementation. If Mesa has support for cell properties, then Mesa-Geo could probably have a simpler implementation by inheriting directly from Mesa. |
Thanks for taking a interest @wang-boyu! Nice to hear that more people would find something like this useful. I just had an idea: Why not just add a NumPy 2D array as a property layer? Could be something like this: from mesa.space import MultiGrid
import numpy as np
class MultiGridWithArrayProperties(MultiGrid):
def __init__(self, width, height, torus):
super().__init__(width, height, torus)
self.properties = {}
def add_new_property(self, property_name, default_value=None):
"""Add a new property with a default value."""
self.properties[property_name] = np.full((self.width, self.height), default_value)
def remove_property(self, property_name):
"""Remove a property."""
del self.properties[property_name]
def set_property_cell(self, x, y, property_name, value):
"""Set the value of a specific property for a cell."""
self.properties[property_name][x, y] = value
def get_property_cell(self, x, y, property_name):
"""Get the value of a specific property for a cell."""
return self.properties[property_name][x, y]
def set_property_all_cells(self, property_name, value):
"""Set a property for all cells to a new value."""
self.properties[property_name][:] = value
def modify_property_all_cells(self, property_name, operation):
"""Modify a property for all cells with a Python or Numpy operation."""
self.properties[property_name] = operation(self.properties[property_name])
def set_property_conditional(self, property_name, value, condition):
"""Set a property for all cells that meet a certain condition."""
self.properties[property_name][condition(self.properties)] = value
def modify_property_conditional(self, property_name, operation, condition):
"""Modify a property for all cells that meet a certain condition with a Python or Numpy operation."""
condition_met = condition(self.properties)
self.properties[property_name][condition_met] = operation(self.properties[property_name][condition_met])
def get_property_value_all_cells(self, property_name):
"""Get all values for a property.""")
return self.properties[property_name]
def get_cells_with_multiple_properties(self, conditions):
"""Get positions of cells that meet multiple property conditions."""
# Initialize with the condition of the first property
first_property, first_value = next(iter(conditions.items()))
combined_condition = self.properties[first_property] == first_value
# Apply logical AND for each subsequent condition
for property_name, value in conditions.items():
if property_name != first_property:
combined_condition &= self.properties[property_name] == value
return list(zip(*np.where(combined_condition)))
def aggregate_property(self, property_name, operation):
"""Perform an aggregate operation (e.g., sum, mean) on a property across all cells."""
return operation(self.properties[property_name]) Then you can do: conditions = {
"color": "red",
"empty": True
}
red_and_empty_cells = grid.get_cells_with_multiple_properties(conditions) and a lot more. Advantage is that everything is NumPy, nothing is looped, and everything is vectorized. Should work fast for all size grids. Disadvantage would be that it only works on rectangular grids, so not on the Hex grids. |
Yes having values as numpy arrays would be helpful. In fact raster layers have functions to extract cell values as numpy array, and apply numpy array to cell values: https://github.com/projectmesa/mesa-geo/blob/e10761e30bef509ea270a96e39decc0d97fc1318/mesa_geo/raster_layers.py#L324-L372. The function name But these numpy arrays are constructed only when the functions are called. It would be more efficient to store everything as numpy arrays as you mentioned above. I'm wondering how this links to the |
Thanks for your take! I thought about it some more, and I think we need two types of patches:
|
One thing I'm struggling with a bit is a strict definition of how agents fill a space. Earlier, I imagined grid cells having a capacity, of how many agents it would hold. With one agent type, that's a very easy and straightforward model: a capacity of n holds n agents. However, as you get multiple agent types, that get's complicated. Is there a capacity for each agent type, of one total capacity? If the latter, does each agent take up the same space? The conceptual model I now have in my heads says you have two components, of which you need either one of or both:
But maybe there might even be an interaction effect between agents (of different types): If some agent type is there, I might want to really be there, but if there is another type, no way I'm going there. Especially with biology and social models that could be the case. (to be fair, I think this is definitely to complicated for a native mesa implementation) So why is this capacity important? Primarily, because it is needed to get the options to which agents can move. However, if you have to check a total capacity and a capacity for that agent type every time, it could complicate both the code (and thus maintenance, scalability, etc.) and could reduce performance. I'm trying some solutions and implementations, but might be overthinking this. If anyone can help me simplify this or narrow it down, that would be very helpful. Edit: To formulate the concrete situation:
|
Practically there are now three problems, that can probably be best solved in this order:
I did some benchmarks, for empty and capacity cells it isn't faster most of the time to use a 2d array, because many calls are individual writes anyway. For the properties this will be different, since you could ask all cells to increase their values. So these two problems can be split. |
Awesome discussion here. Cells and properties and how to implement them is something I have been thinking about a lot over the last couple of years. It really is a hard problem and I haven't found a good solution yet. There has been a discussion about a layered architecture here: #643 (comment) , which I think is another interesting way to look at it. That could be a way to combine a fast numpy properties layer with a traditional agents layer. RE capacity: I wouldn't overthink things. In my mind MultiGrid (with unlimited capacity) is always the "default mode" and SingleGrid is a special (but common) case. Conceptually, that is, I know its implemented differently. |
Something thats vaguely related to this thread and the one about data collection: We currently expose and make strong use of the "unique_id" of agents. However, we have no control about the actual uniqueness of ids and how they are used (integers, strings or something completely different). We could maybe circumvent this by using |
Thanks for your insights Corvince!
It’s insane how there are so much insightful discussions hidden all over this repository. This is another treasure of one. I have been thinking about layers, height/elevation and 2.5D spaces as well. On practical example case would be that you can have a soil layer, a plant layer and an air layer. To solve this problem properly we might need a good conceptual model of what layers there can or should be in an ABM.
Thanks. capacity=n would probably be the most logical approach, where 1 and inf are special cases. I think one agent of each type per cell is also still a common case. But maybe we can use layers for that.
Was indeed tinkering with this. Using ints is indeed faster and more memory efficient for manipulating NumPy arrays, but you need a translation step to get the agent again (dict, func, whatever) which makes it slower again in most cases. Thanks for your insights, especially multiple layers of grids could help with the capacity problem! |
Okay, the next issue I’m contemplating is how properties and cell layers should link together. Should they be linked one-to-one (each layers has its own properties)? If so, how do those layers communicate though them. |
Think I'm getting there:
Edit: Nice thing is that this approach also completely separates the three problems (multiple-agents, flexible capacity and cell properties). |
I have an initial implementation of a PropertyLayer class with all the functionality and a _PropertyGrid class which SingleGrid and MultiGrid will use. Still work in progress, but check it out in: #1898 |
+1 to this being a great discussion. A few thoughts...
|
Agreed on the PEP. We should make some template / guidelines for that. Edit: Also, discussion/issue --> MEP --> implementation is probably the right order. I think sometimes you need an implementation to see how it works, but in many cases something like a formal MEP does help with that. |
I like that order. |
I'm going to close this issue as completed!
|
This post is now slightly out of date, but preserved for historical context. See #1898 for the proposed implementation.
This proposal suggests two potential methods for introducing cell properties to grids in Mesa: a base class extension and a Mixin class. These enhancements will enable users to assign and manage custom properties (such as color or type) to grid cells, similar to patches in NetLogo. This functionality can be beneficial for a wide range of simulations where the environment's characteristics are as crucial as the agents' behaviors.
Motivation
Introducing properties to grid cells in agent-based models adds significant depth and realism to simulations. These properties can represent various environmental factors like terrain type, resources, or pollution levels, impacting how agents interact and behave. For example, in ecological simulations, cell properties might dictate animal movement based on land fertility or water availability. In urban models, properties could represent different neighborhood characteristics, influencing decisions made by agents representing residents or developers. This enhancement not only allows for more detailed and realistic modeling scenarios but also makes simulations more versatile and applicable to a wide range of fields, from ecology and urban planning to social sciences.
Why not just a layer of agents (in a MultiGrid)?
Implementation
Here are two high-level options how to integrate grid cell properties to the Mesa Space module.
Option 1: Base Class Extension
Implementation:
Extend the existing grid classes (
SingleGrid
,MultiGrid
,HexSingleGrid
,HexMultiGrid
) to include a dictionary for cell properties.Advantages:
Disadvantages:
Usage Example:
Option 2: Mixin Class
Implementation:
Create a Mixin class that can be combined with any existing grid class to add cell properties functionality.
Advantages:
Disadvantages:
Usage Example:
Discussion
I would like feedback on this proposal! I have a few main discussion points:
ask patches
. This proposal doesn't go this far, as they only have properties and no functions. Should we consider allowing cells to have functions or behaviors, similar to NetLogo's approach?capacity - len(agents)
.(This approach aligns with Python's interpretation of numeric values in boolean contexts, where 0 is False (equivalent to 'empty') and any non-zero value is True (indicating 'places left'. So you can just do
if places_left:
)color == "red"
)outside == True
)empty == False
)Do you think all functions should be able to filter on a property value?
ContinuousSpace
class, because there are no discrete grid cells. In NetLogo however, patches have a discrete (square) size, but space is still continuous for agents. Should we offer some hybrid concept?@jackiekazil @tpike3 @Corvince @rht very curious what you think!
The text was updated successfully, but these errors were encountered: