Skip to content
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

Add full support for property layers to cell spaces #2512

Merged
merged 60 commits into from
Dec 4, 2024

Conversation

quaquel
Copy link
Member

@quaquel quaquel commented Nov 18, 2024

Summary

This PR adds full support for PropertyLayers to the experimental cell spaces in Mesa. It introduces a new property_layers module and integrates it with the cell space architecture, offering an enhanced, n-dimensional implementation with attribute-like property access for cells.

Motive

While Mesa already had PropertyLayers in mesa.space, they needed adaptation to work seamlessly with the new cell spaces. This implementation:

  • Makes PropertyLayers work with n-dimensional grids
  • Provides a more intuitive attribute-like interface for accessing properties
  • Ensures proper dimension validation between layers and grids
  • Integrates better with the cell space design philosophy

Implementation

The key implementation changes include:

  1. Created a new property_layers module in mesa.experimental.cell_space containing:

    • A HasPropertyLayers mixin class for adding property layer support to grids
    • A PropertyDescriptor class enabling attribute-like access to properties
    • An enhanced n-dimensional PropertyLayer class
  2. Core improvements:

    • PropertyLayers must match grid dimensionality
    • Cells get attribute-like access to properties (e.g., cell.grass instead of cell.get_property("grass"))
    • Added support for masks and neighborhood selection
    • Implemented proper deepcopy and pickle support for dynamic properties
    • Added an automatic empty property layer to track cell occupancy
  3. Architectural changes:

    • Moved property layer functionality from DiscreteSpace to Grid
    • Enhanced error handling and validation
    • Added specialized pickle/unpickle support for GridCells with dynamic properties

Usage Examples

# Create a grid with property layers
grid = OrthogonalMooreGrid((10, 10))
grid.create_property_layer("elevation", default_value=0.0)

# Attribute-like access
cell = grid._cells[(0, 0)]
cell.elevation = 100  # Set property
print(cell.elevation)  # Get property
cell.elevation += 50   # Modify property

# Grid-level operations
grid.modify_properties("elevation", np.add, 50)
grid.set_property("elevation", 100, condition=lambda value: value > 150)

# Select cells based on properties
cells = grid.select_cells(
    conditions={"elevation": lambda x: x > 50},
    extreme_values={"elevation": "highest"},
    only_empty=True
)

Additional Notes

  • The implementation automatically handles adding/removing property descriptors
  • All property layers maintain proper sync between grid and cell levels
  • The code maintains backward compatibility while offering enhanced functionality
  • The feature is marked experimental and will benefit from community feedback
  • Future improvements could include more advanced selection methods and optimization of operations

Original description

This is a somewhat opinionated take on how to offer full support for property layers in new-style cell spaces. It draws on ideas from #2431 and #2440. In short, I have taken the property layer code from mesa.space, and created a new property_layers module in mesa.experimental.cell_space. This ensures that the existing code in mesa.space remains untouched, and I can tune the behavior to align with the design ideas of cell_space.

All relevant code for adding property layer behavior to grids is contained in a new HasPropertyLayer class. This is primarily for easy development and allowing me to understand better what is going on.

I have moved property layer functionality from the generic DiscreteSpace to Grid as discussed in #2431. Because Grid can be n-dimensional, I modified PropertyLayer and all relevant code to work for n-dimensional grids.

I enforce that the dimensionality of the property layer and the dimensionality of the grid are the same. This was left implicit before. Now, it raises errors if you try to have a layer with a different coordinate system.

Cells have attribute-like access to their properties. So cell.grass or cell.elevation. Likewise, any modification done at the cell level can just use assignment (i.e., =). This is all implemented via a PropertyDescriptor, which offers a view into the underlying numpy array defined in the property layer.

What currently does not work yet is:

  1. Masks (this is a trivial problem, just a bit more tricky because it needs to work for n-dimensional grids).
  2. The empty property layer is not yet working. I have to take a closer look at this. It involves two separate problems: (1) Where should the layer be added? It cannot be done in the __init__ of HasPropertyLayers because when this code is executed, the __init__ of Grid has not been completed. So e.g., dimensions is not yet defined. (2) It requires updating Cell to set a boolean to true or false. Since I am relying on adding descriptors dynamically, but only for grids and not for e.g., Network, I have to figure out how to make this all work correctly independently of the exact subclass of DiscreteSpace to which Cell belongs. Ideally, the Cell class should be unaware of the presence or absence of an empty property layer
  3. How the dynamic adding of descriptors interacts with deepcopy and pickle operations. Again, I have no idea here, but it should be fixable.

@quaquel quaquel requested a review from EwoutH November 18, 2024 14:11

This comment was marked as outdated.

@quaquel
Copy link
Member Author

quaquel commented Nov 19, 2024

The empty property layer is not yet working.

This is now working again. It required some Python tricks involving dynamically creating subclasses.

@quaquel quaquel added feature Release notes label trigger-benchmarks Special label that triggers the benchmarking CI and removed trigger-benchmarks Special label that triggers the benchmarking CI labels Nov 19, 2024
Copy link

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 -2.4% [-4.0%, -1.1%] 🔵 -1.1% [-1.3%, -0.9%]
BoltzmannWealth large 🔵 +0.5% [-0.1%, +1.0%] 🔵 +0.4% [-0.1%, +0.9%]
Schelling small 🔵 -0.5% [-0.9%, -0.1%] 🔵 +0.9% [+0.6%, +1.1%]
Schelling large 🔵 -1.0% [-1.4%, -0.6%] 🔵 -1.4% [-2.0%, -0.8%]
WolfSheep small 🔴 +10.1% [+9.8%, +10.5%] 🔴 +10.3% [+10.1%, +10.4%]
WolfSheep large 🔴 +9.5% [+9.0%, +9.8%] 🔴 +9.3% [+7.6%, +10.9%]
BoidFlockers small 🔵 +0.2% [-0.3%, +0.7%] 🔵 +1.8% [+1.2%, +2.5%]
BoidFlockers large 🔵 -0.3% [-0.9%, +0.3%] 🔵 -0.0% [-0.7%, +0.6%]

@quaquel
Copy link
Member Author

quaquel commented Nov 25, 2024

Deepcopy and pickle now also work with property layers. It took a bit longer than expected, but I learned a lot about pickle and deepcopy internals. In the end, copyreg.pickle makes it all fairly simple (and a lot easier than trying to figure out __reduce__).

one last item left: how do we want to access property layers at the grid level? There are at least 3 options

  1. attribute like access: grid.elevation
  2. dict-like access: grid.propertylayers["elevation"]
  3. method-wise access: grid.get_property_layer("elevation")

Of these, I am inclined to go with option 1. You can then do grid.elevation for the entire property layer, and cell.elevation to get the actual value for a given cell.

@EwoutH
Copy link
Member

EwoutH commented Nov 27, 2024

For completion, I think technically there's a fourth option, like Pandas does it to access series:

  1. grid["elevation"]

However, 1 feels really great once you know it, and there is precedence for this kind of attribute acces in the Python framework:

        arr = np.array([[1, 2], [3, 4]])
        arr.shape  # (2, 2)

        df = pd.DataFrame({'a': [1, 2], 'b': [3, 4]})
        df.a  # Series([1, 2])

There should be some checks on the variable names, at least they should be valid Python identifiers (similar to what we now enforce in the EMA workbench). Maybe we should also check against some other things.

Best to see is how NumPy and Pandas do it, they probably already figured this out fully. Maybe packages like nameutils could also help out here.

@quaquel
Copy link
Member Author

quaquel commented Dec 1, 2024

Best to see is how NumPy and Pandas do it, they probably already figured this out fully. Maybe packages like nameutils could also help out here.

pandas does not do a check, but if it is not a valid identifier, the attribute like access is simply not supported. So you can have column names with a space in them and pandas accepts that. I am inclined to just follow that route and document clearly that for attribute access, you need to ensure that the property layer name needs to be a valid python identifier.

Note that the package you link to does something interesting but quite different (namely handling family names and capitalization).

@EwoutH
Copy link
Member

EwoutH commented Dec 2, 2024

@quaquel Thanks for continuing working on this. How far do you expect it from being ready to be merged?

@quaquel
Copy link
Member Author

quaquel commented Dec 2, 2024

How far do you expect it from being ready to be merged?

Its basically done, but I like to give it one last look over before merging.

@quaquel quaquel merged commit 567339f into projectmesa:main Dec 4, 2024
11 checks passed
@EwoutH EwoutH added the experimental Release notes label label Dec 4, 2024
@EwoutH
Copy link
Member

EwoutH commented Dec 4, 2024

I updated the PR description based on the final implementation, could you check it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
experimental Release notes label feature Release notes label
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants