Skip to content

Move experimental cell spaces to normal #2286

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

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d90b0f7
further updates
quaquel Jan 21, 2024
9586490
Update benchmarks/WolfSheep/__init__.py
quaquel Jan 21, 2024
4aaa35d
Merge remote-tracking branch 'upstream/main'
quaquel Feb 21, 2024
d31478c
Merge remote-tracking branch 'upstream/main'
quaquel Apr 22, 2024
6e4c72e
Merge remote-tracking branch 'upstream/main'
quaquel Aug 14, 2024
70fbaf5
Merge remote-tracking branch 'upstream/main'
quaquel Aug 18, 2024
724c8db
Merge remote-tracking branch 'upstream/main'
quaquel Aug 21, 2024
45184a4
Merge remote-tracking branch 'upstream/main'
quaquel Aug 22, 2024
3d75d30
Merge remote-tracking branch 'upstream/main'
quaquel Aug 27, 2024
2759244
Update __init__.py
quaquel Aug 27, 2024
fc8aaea
Merge remote-tracking branch 'upstream/main'
quaquel Aug 28, 2024
1ba465d
Merge remote-tracking branch 'upstream/main'
quaquel Aug 30, 2024
2b5e822
Merge remote-tracking branch 'upstream/main'
quaquel Sep 2, 2024
3847799
Merge remote-tracking branch 'upstream/main'
quaquel Sep 4, 2024
301d80e
Merge remote-tracking branch 'upstream/main'
quaquel Sep 4, 2024
fe3d655
Merge remote-tracking branch 'upstream/main'
quaquel Sep 4, 2024
7d18880
Merge remote-tracking branch 'upstream/main'
quaquel Sep 5, 2024
6b49a3b
Merge remote-tracking branch 'upstream/main'
quaquel Sep 5, 2024
b9909e6
Merge remote-tracking branch 'upstream/main'
quaquel Sep 9, 2024
8ce3d83
Merge remote-tracking branch 'upstream/main'
quaquel Sep 10, 2024
88fbf74
Merge remote-tracking branch 'upstream/main'
quaquel Sep 13, 2024
077aca5
Merge remote-tracking branch 'upstream/main'
quaquel Sep 16, 2024
c771e3b
Merge remote-tracking branch 'upstream/main'
quaquel Sep 18, 2024
3fa0c27
Merge remote-tracking branch 'upstream/main'
quaquel Sep 18, 2024
e99811d
move cell_space out of experimental to spaces folder
quaquel Sep 9, 2024
06b0998
move boltzman benchmark wealth model to new spaces
quaquel Sep 9, 2024
6e53612
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 9, 2024
81f7779
Update test_solara_viz.py
quaquel Sep 9, 2024
72d32f4
fix benchmarks
quaquel Sep 9, 2024
80a56cc
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 9, 2024
6697e77
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 18, 2024
259900e
Update cell.py
quaquel Sep 19, 2024
550c123
Merge remote-tracking branch 'upstream/main' into move_spaces
quaquel Oct 7, 2024
2650ad7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 16 additions & 20 deletions benchmarks/BoltzmannWealth/boltzmann_wealth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import mesa
import mesa.spaces as spaces


def compute_gini(model):
Expand Down Expand Up @@ -42,17 +43,18 @@ def __init__(self, seed=None, n=100, width=10, height=10):
"""
super().__init__(seed=seed)
self.num_agents = n
self.grid = mesa.space.MultiGrid(width, height, True)
self.grid = spaces.OrthogonalMooreGrid((width, height))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't have a solution just yet, bit on first glance I don't directly like OrthogonalMooreGrid and OrthogonalVonNeumannGrid.

Maybe OrthogonalGrid with an required (so no default) diagonal_connections boolean keyword argument might work. That would also make it easier to experiment with it: Instead of needing to import another space, you switch a keyword argument.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We debated this endlessly with the original PR. I don't want to redo that debate here. Let's first try and move all this code from experimental and take it from there in future PRs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that once it's stable we can't as easily do things like this.

One part of stabilizing an API is re-evaluating properties of it. I know that will add some work, but if we're not doing that, then why did we make it experimental at all?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still you should start by revisiting the original PR.

Regarding experimentation, I don't see a lot of problem changing spaces.OrthogonalMooreGrid to spaces.OrthogonalVonNeumannGrid If you import the spaces, no need to change imports.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, I am happy to debate the API, but there is enough stuff going on in this PR that follows from resolving all the implications that follow from moving this from experimental to stable. I feel that adding the actual API to that long list adds further confusion.

Moreover, we had good reasons in the original PR for the split and like @Corvince, I don't see a major practical difference between changing imports or a boolean.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Moore is used as a label in the current version of the space but with a boolean. So we don't add new concepts to MESA.
  2. Knowing Moore vs von Neumann neighborhoods in my view is basic ABM knowledge that a user should just have. It sits in the same category of knowing what a torus is.
  3. Explaining the difference between Moore and von Neumann is presently already covered in the docstring. We might expand this a bit more so a user does not need to google it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moore is used as a label in the current version of the space but with a boolean.

So why then at the least not keep it this way? Would make migrating also easier.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You normally subclass when you have either:

  • Different (conflicting) attributes
  • Different methods, with some absent in other

With diagonal/notdiagonal this is not the case, as far as I understand. They have the same methods, return the same outputs (a collection of cells) and have the same attributes (neighbours, etc.). The only thing that's different is a single function implementation (afaik), which can easily be covered by an if-else.

So why do I like keyword arguments (in this case)? Because they are more scalable. If you add another two booleans (on how many agents are allowed, another neighbour configuration, etc.), you can have 8 configuration options. With subclasses, you need to have 8 subclasses.

Here's another idea: Why not have a keyword argument neighbour_definition. Can be "Moore", "VonNeuman", but also something else, like a custom map. Could help a lot when we want to implement:

It just feels like subclassing limits us significantly for future extension, and isn't the best practice in this case.


I'm not going to die on this hill (in this PR). Move forward with it without this if you want, I can follow up.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am happy to explore this further, but indeed, preferably separately.

When to subclass is a deeply contested topic, judging by the various StackOverflow questions about this that have been closed as being too opinionated. We indeed use it here for different implementations of how cells are wired up inside the grid.

But I would not say that the fact that the only difference is the wiring up method implies we should (note not could, because that is indeed possible) reduce this all to a list of options for some neighbour_definition keyword argument and have a long case match statement to cover all possible neighborhood definitions. From a classic OO point of view, a Moore grid, a von Neuman grid, a network, a hexgrid, or a voroinoi grid, are all discrete grids. Subclassing them while abiding by a common interface is, in fact, good design. The motivation for subclassing in this reasoning is that their behavior is different.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note on the API (again, outside the scope of this PR): The use of subclassed to explicitly donate which space is used does not prohibit a possibly more user-friendly API. We could also think about a create_space() function with completely different arguments that return our spaces, without users needing to instantiate them themselves.

self.schedule = mesa.time.RandomActivation(self)
self.datacollector = mesa.DataCollector(
model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"}
)
# Create agents
for _ in range(self.num_agents):
a = MoneyAgent(self)
agent = MoneyAgent(self)
# Add the agent to a random grid cell
x = self.random.randrange(self.grid.width)
y = self.random.randrange(self.grid.height)
self.grid.place_agent(a, (x, y))
x = self.random.randrange(width)
y = self.random.randrange(height)
agent.move_to(self.grid[(x, y)])

self.running = True
self.datacollector.collect(self)
Expand All @@ -74,7 +76,7 @@ def run_model(self, n):
self.step()


class MoneyAgent(mesa.Agent):
class MoneyAgent(mesa.spaces.CellAgent):
"""An agent with fixed initial wealth."""

def __init__(self, model):
Expand All @@ -86,27 +88,21 @@ def __init__(self, model):
super().__init__(model)
self.wealth = 1

def move(self):
"""Move the agent to a random neighboring cell."""
possible_steps = self.model.grid.get_neighborhood(
self.pos, moore=True, include_center=False
)
new_position = self.random.choice(possible_steps)
self.model.grid.move_agent(self, new_position)

def give_money(self):
"""Give money to a random cell mate."""
cellmates = self.model.grid.get_cell_list_contents([self.pos])
cellmates.pop(
cellmates.index(self)
) # Ensure agent is not giving money to itself
cellmates = [agent for agent in self.cell.agents if agent is not self]
if len(cellmates) > 0:
other = self.random.choice(cellmates)
other.wealth += 1
self.wealth -= 1

def step(self):
"""Run the agent for 1 step."""
self.move()
self.move_to(self.cell.neighborhood().select_random_cell())
if self.wealth > 0:
self.give_money()


if __name__ == "__main__":
model = BoltzmannWealth()
for _ in range(10):
model.step()
2 changes: 1 addition & 1 deletion benchmarks/Schelling/schelling.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

from mesa import Model
from mesa.experimental.cell_space import Cell, CellAgent, OrthogonalMooreGrid
from mesa.spaces import CellAgent, OrthogonalMooreGrid


class SchellingAgent(CellAgent):
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/WolfSheep/wolf_sheep.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import math

from mesa import Model
from mesa.experimental.cell_space import CellAgent, FixedAgent, OrthogonalVonNeumannGrid
from mesa.experimental.devs import ABMSimulator
from mesa.spaces import CellAgent, OrthogonalVonNeumannGrid


class Animal(CellAgent):
Expand Down
7 changes: 5 additions & 2 deletions mesa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

import datetime

import mesa.space as space
import mesa.space as old_space

# import mesa.spaces as spaces
import mesa.time as time
from mesa.agent import Agent
from mesa.batchrunner import batch_run
Expand All @@ -16,7 +18,8 @@
"Model",
"Agent",
"time",
"space",
"old_space",
"spaces",
"DataCollector",
"batch_run",
"experimental",
Expand Down
4 changes: 1 addition & 3 deletions mesa/experimental/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Experimental init."""

from mesa.experimental import cell_space

from .solara_viz import JupyterViz, Slider, SolaraViz, make_text

__all__ = ["cell_space", "JupyterViz", "SolaraViz", "make_text", "Slider"]
__all__ = ["JupyterViz", "SolaraViz", "make_text", "Slider"]
38 changes: 0 additions & 38 deletions mesa/experimental/cell_space/__init__.py

This file was deleted.

1 change: 0 additions & 1 deletion mesa/experimental/components/matplotlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from matplotlib.ticker import MaxNLocator

import mesa
from mesa.experimental.cell_space import VoronoiGrid


@solara.component
Expand Down
25 changes: 25 additions & 0 deletions mesa/spaces/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from mesa.spaces.cell import Cell
from mesa.spaces.cell_agent import CellAgent
from mesa.spaces.cell_collection import CellCollection
from mesa.spaces.discrete_space import DiscreteSpace
from mesa.spaces.grid import (
Grid,
HexGrid,
OrthogonalMooreGrid,
OrthogonalVonNeumannGrid,
)
from mesa.spaces.network import Network
from mesa.spaces.voronoi import VoronoiGrid

__all__ = [
"CellCollection",
"Cell",
"CellAgent",
"DiscreteSpace",
"Grid",
"HexGrid",
"OrthogonalMooreGrid",
"OrthogonalVonNeumannGrid",
"Network",
"VoronoiGrid",
]
7 changes: 5 additions & 2 deletions mesa/experimental/cell_space/cell.py → mesa/spaces/cell.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
from random import Random
from typing import TYPE_CHECKING, Any

from mesa.experimental.cell_space.cell_agent import CellAgent
from mesa.experimental.cell_space.cell_collection import CellCollection
from mesa.spaces.cell_collection import CellCollection

if TYPE_CHECKING:
from mesa.spaces.cell_agent import CellAgent

from mesa.space import PropertyLayer

if TYPE_CHECKING:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from mesa.agent import Agent

if TYPE_CHECKING:
from mesa.experimental.cell_space import Cell
from mesa.spaces.cell import Cell


class HasCellProtocol(Protocol):
Expand Down Expand Up @@ -127,7 +127,3 @@ def move(self, direction: str, distance: int = 1):

if direction not in self.DIRECTION_MAP:
raise ValueError(f"Invalid direction: {direction}")

move_vector = self.DIRECTION_MAP[direction]
for _ in range(distance):
self.move_relative(move_vector)
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from typing import TYPE_CHECKING, Generic, TypeVar

if TYPE_CHECKING:
from mesa.experimental.cell_space.cell import Cell
from mesa.experimental.cell_space.cell_agent import CellAgent
from mesa.spaces.cell import Cell
from mesa.spaces.cell_agent import CellAgent

T = TypeVar("T", bound="Cell")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from random import Random
from typing import Any, Generic, TypeVar

from mesa.experimental.cell_space.cell import Cell
from mesa.experimental.cell_space.cell_collection import CellCollection
from mesa.space import PropertyLayer
from mesa.spaces.cell import Cell
from mesa.spaces.cell_collection import CellCollection

T = TypeVar("T", bound=Cell)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from random import Random
from typing import Generic, TypeVar

from mesa.experimental.cell_space import Cell, DiscreteSpace
from mesa.spaces import Cell, DiscreteSpace

T = TypeVar("T", bound=Cell)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from random import Random
from typing import Any

from mesa.experimental.cell_space.cell import Cell
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
from mesa.spaces.cell import Cell
from mesa.spaces.discrete_space import DiscreteSpace


class Network(DiscreteSpace[Cell]):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

import numpy as np

from mesa.experimental.cell_space.cell import Cell
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
from mesa.spaces.cell import Cell
from mesa.spaces.discrete_space import DiscreteSpace


class Delaunay:
Expand Down
2 changes: 1 addition & 1 deletion mesa/visualization/components/matplotlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from matplotlib.figure import Figure

import mesa
from mesa.experimental.cell_space import VoronoiGrid
from mesa.space import PropertyLayer
from mesa.spaces import VoronoiGrid
from mesa.visualization.utils import update_counter


Expand Down
4 changes: 2 additions & 2 deletions tests/test_cell_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import pytest

from mesa import Model
from mesa.experimental.cell_space import (
from mesa.space import PropertyLayer
from mesa.spaces import (
Cell,
CellAgent,
CellCollection,
Expand All @@ -18,7 +19,6 @@
OrthogonalVonNeumannGrid,
VoronoiGrid,
)
from mesa.space import PropertyLayer


def test_orthogonal_grid_neumann():
Expand Down
2 changes: 1 addition & 1 deletion tests/test_solara_viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def drawer(model):

# check voronoi space drawer
voronoi_model = mesa.Model()
voronoi_model.grid = mesa.experimental.cell_space.VoronoiGrid(
voronoi_model.grid = mesa.spaces.VoronoiGrid(
centroids_coordinates=[(0, 1), (0, 0), (1, 0)],
)
solara.render(
Expand Down
Loading