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 cell-centric discrete spaces (experimental) #1994

Merged
merged 74 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
dd54f21
Add draft implementation for CellSpace
Corvince Jan 19, 2024
7b58d8e
add features to cell
Corvince Jan 23, 2024
aef4a8b
remove create_neighborhood_getter
Corvince Jan 23, 2024
3240a2b
update benchmark models
Corvince Jan 23, 2024
d3df44e
try to import Grid directly from experimental
Corvince Jan 23, 2024
c80a0bd
adds a pythonic WolfSheep implementation
quaquel Jan 24, 2024
93d7db5
replace radius check with value error in neighborhood
quaquel Jan 24, 2024
be7b843
minor further code cleanup of wolfsheep
quaquel Jan 24, 2024
bd788ec
add HexGrid
quaquel Jan 24, 2024
cb0ef18
typo fix
quaquel Jan 24, 2024
94f06b1
various updates
quaquel Jan 27, 2024
be6c1af
Merge remote-tracking branch 'upstream/main' into experimental/cell-s…
quaquel Jan 27, 2024
875d0ed
add NetworkGrid
quaquel Jan 27, 2024
e4d121d
add NetworkGrid
quaquel Jan 27, 2024
5769a05
change how empties is handled
quaquel Jan 27, 2024
82a64d9
further cleanup of handling of empties
quaquel Jan 27, 2024
5fdc787
correct handling of radius in Schelling large
quaquel Jan 27, 2024
152995c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 27, 2024
c524034
Merge remote-tracking branch 'upstream/main' into experimental/cell-s…
quaquel Jan 28, 2024
972ecc4
Update cell_space.py
quaquel Jan 28, 2024
a656d93
change from Random to random
quaquel Jan 28, 2024
8df9276
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 28, 2024
f4a1089
small fixes and default capacity=None
Corvince Jan 28, 2024
1ebcf06
initial tests for cell_space
quaquel Jan 29, 2024
d362bae
some additional tests
quaquel Jan 29, 2024
c7d82d0
additional unit tests
quaquel Jan 30, 2024
b2842c0
restructure files and folders
Corvince Jan 31, 2024
740f003
various updates
quaquel Feb 2, 2024
2d8fcb5
additional tests and temporary fix for select_random_empty_cell
quaquel Feb 9, 2024
99ff668
Merge branch 'main' into experimental/cell-space
quaquel Feb 9, 2024
565ae0a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 9, 2024
ab87b70
improved annotations
quaquel Feb 9, 2024
ab13bbc
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 9, 2024
8f2e3dd
Update discrete_space.py
quaquel Feb 9, 2024
ac8ff31
Merge branch 'experimental/cell-space' of https://github.com/Corvince…
quaquel Feb 15, 2024
aa03962
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 15, 2024
9b28f9d
correct handling of seeds when running examples in issolation
quaquel Feb 16, 2024
bbca3ac
Merge branch 'experimental/cell-space' of https://github.com/Corvince…
quaquel Feb 16, 2024
fe5a93f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 16, 2024
2c80330
added docstrings
quaquel Feb 17, 2024
c79aaf0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 17, 2024
8384cf1
reformating and minor update to tests
quaquel Feb 17, 2024
253531c
Merge branch 'experimental/cell-space' of https://github.com/Corvince…
quaquel Feb 17, 2024
ebdee8e
Add optional neighborhood_func to Grid class
Corvince Feb 17, 2024
bb2fc52
allow n-dimensional grids
Corvince Feb 17, 2024
b3efdee
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 17, 2024
8c7ad05
add validation, remove neighborhood_func, add tests
Corvince Feb 18, 2024
819e903
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 18, 2024
0f49dc7
Make Cell* generic
Corvince Feb 19, 2024
da94fb1
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 19, 2024
b210686
Make Cell* generic
Corvince Feb 19, 2024
c35591c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 19, 2024
065e5ea
fixes to tests
quaquel Feb 19, 2024
1bfa009
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 19, 2024
6b1c454
type hint 3.9 fix in network.py
quaquel Feb 19, 2024
e0d5673
Merge branch 'experimental/cell-space' of https://github.com/Corvince…
quaquel Feb 19, 2024
cf789f2
fix type checking
Corvince Feb 19, 2024
50666c1
update capacity, add annotations import
Corvince Feb 19, 2024
e520d3c
fix wolf_sheep
Corvince Feb 20, 2024
85251f0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 20, 2024
3a247cb
fix wolf_sheep
Corvince Feb 20, 2024
86127f2
Merge branch 'experimental/cell-space' of https://github.com/Corvince…
quaquel Feb 20, 2024
fb19879
seperate code path for 2d and nd grids when connecting cells
quaquel Feb 21, 2024
5ea5d35
seed as kwarg
quaquel Feb 21, 2024
b7ac86c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 21, 2024
76ca4a5
code formatting fix
quaquel Feb 21, 2024
45e3e9f
testing
quaquel Feb 22, 2024
3d8cdfd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 22, 2024
ce8ca0b
Merge branch 'main' into experimental/cell-space
quaquel Feb 22, 2024
18bd2a3
Merge branch 'experimental/cell-space' of https://github.com/Corvince…
quaquel Feb 22, 2024
62c844e
minor docstring update
quaquel Feb 23, 2024
adff7aa
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 23, 2024
7d5e6b0
Merge branch 'main' into experimental/cell-space
EwoutH Feb 27, 2024
b32af9f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 27, 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
30 changes: 16 additions & 14 deletions benchmarks/Schelling/schelling.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import mesa
from mesa import Model
from mesa.experimental.cell_space import CellAgent, OrthogonalGrid
from mesa.time import RandomActivation


class SchellingAgent(mesa.Agent):
class SchellingAgent(CellAgent):
"""
Schelling segregation agent
"""

def __init__(self, unique_id, model, agent_type):
def __init__(self, unique_id, model, agent_type, radius):
"""
Create a new Schelling agent.
Args:
Expand All @@ -16,23 +18,22 @@ def __init__(self, unique_id, model, agent_type):
"""
super().__init__(unique_id, model)
self.type = agent_type
self.radius = radius

EwoutH marked this conversation as resolved.
Show resolved Hide resolved
def step(self):
similar = 0
for neighbor in self.model.grid.iter_neighbors(
self.pos, moore=True, radius=self.model.radius
):
for neighbor in self.cell.neighborhood(radius=self.radius).agents:
if neighbor.type == self.type:
similar += 1

# If unhappy, move:
if similar < self.model.homophily:
self.model.grid.move_to_empty(self)
self.move_to(self.model.grid.select_random_empty_cell())
else:
self.model.happy += 1


class Schelling(mesa.Model):
class Schelling(Model):
"""
Model class for the Schelling segregation model.
"""
Expand All @@ -47,22 +48,23 @@ def __init__(
self.density = density
self.minority_pc = minority_pc
self.homophily = homophily
self.radius = radius

self.schedule = mesa.time.RandomActivation(self)
self.grid = mesa.space.SingleGrid(height, width, torus=True)
self.schedule = RandomActivation(self)
self.grid = OrthogonalGrid(
height, width, torus=True, capacity=1, random=self.random
)

self.happy = 0

# Set up agents
# We use a grid iterator that returns
# the coordinates of a cell as well as
# its contents. (coord_iter)
for _cont, pos in self.grid.coord_iter():
for cell in self.grid:
if self.random.random() < self.density:
agent_type = 1 if self.random.random() < self.minority_pc else 0
agent = SchellingAgent(self.next_id(), self, agent_type)
self.grid.place_agent(agent, pos)
agent = SchellingAgent(self.next_id(), self, agent_type, radius)
agent.move_to(cell)
Copy link
Contributor

Choose a reason for hiding this comment

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

vision makes more sense for the usage of the variable radius, than radius itself here. But this is a minor concern.

Copy link
Member

Choose a reason for hiding this comment

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

For now, I preferradius as it is the same as in the current implementation of spaces.

self.schedule.add(agent)

def step(self):
Expand Down
63 changes: 32 additions & 31 deletions benchmarks/WolfSheep/wolf_sheep.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,40 @@
Center for Connected Learning and Computer-Based Modeling,
Northwestern University, Evanston, IL.
"""
import math

import mesa
from mesa import Model
from mesa.experimental.cell_space import CellAgent, OrthogonalGrid
from mesa.time import RandomActivationByType


class Animal(mesa.Agent):
def __init__(self, unique_id, model, moore, energy, p_reproduce, energy_from_food):
class Animal(CellAgent):
def __init__(self, unique_id, model, energy, p_reproduce, energy_from_food):
super().__init__(unique_id, model)
self.energy = energy
self.p_reproduce = p_reproduce
self.energy_from_food = energy_from_food
self.moore = moore

def random_move(self):
next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True)
next_move = self.random.choice(next_moves)
# Now move:
self.model.grid.move_agent(self, next_move)
self.move_to(self.cell.neighborhood().select_random_cell())

def spawn_offspring(self):
self.energy /= 2
offspring = self.__class__(
self.model.next_id(),
self.model,
self.moore,
self.energy,
self.p_reproduce,
self.energy_from_food,
)
self.model.grid.place_agent(offspring, self.pos)
offspring.move_to(self.cell)
self.model.schedule.add(offspring)

def feed(self):
...

def die(self):
self.model.grid.remove_agent(self)
self.cell.remove_agent(self)
self.remove()

def step(self):
Expand All @@ -67,8 +65,9 @@ class Sheep(Animal):

def feed(self):
# If there is grass available, eat it
agents = self.model.grid.get_cell_list_contents(self.pos)
grass_patch = next(obj for obj in agents if isinstance(obj, GrassPatch))
grass_patch = next(
obj for obj in self.cell.agents if isinstance(obj, GrassPatch)
)
if grass_patch.fully_grown:
self.energy += self.energy_from_food
grass_patch.fully_grown = False
Expand All @@ -80,8 +79,7 @@ class Wolf(Animal):
"""

def feed(self):
agents = self.model.grid.get_cell_list_contents(self.pos)
sheep = [obj for obj in agents if isinstance(obj, Sheep)]
sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)]
if len(sheep) > 0:
sheep_to_eat = self.random.choice(sheep)
self.energy += self.energy
Expand All @@ -90,7 +88,7 @@ def feed(self):
sheep_to_eat.die()


class GrassPatch(mesa.Agent):
class GrassPatch(CellAgent):
"""
A patch of grass that grows at a fixed rate and it is eaten by sheep
"""
Expand All @@ -100,7 +98,7 @@ def __init__(self, unique_id, model, fully_grown, countdown):
Creates a new patch of grass

Args:
grown: (boolean) Whether the patch of grass is fully grown or not
fully_grown: (boolean) Whether the patch of grass is fully grown or not
countdown: Time for the patch of grass to be fully grown again
"""
super().__init__(unique_id, model)
Expand All @@ -117,7 +115,7 @@ def step(self):
self.countdown -= 1


class WolfSheep(mesa.Model):
class WolfSheep(Model):
"""
Wolf-Sheep Predation Model

Expand Down Expand Up @@ -161,24 +159,27 @@ def __init__(
self.initial_wolves = initial_wolves
self.grass_regrowth_time = grass_regrowth_time

self.schedule = mesa.time.RandomActivationByType(self)
self.grid = mesa.space.MultiGrid(self.height, self.width, torus=False)
self.schedule = RandomActivationByType(self)
self.grid = OrthogonalGrid(
self.height,
self.width,
moore=moore,
torus=False,
capacity=math.inf,
random=self.random,
)

# Create sheep:
for _ in range(self.initial_sheep):
pos = (
self.random.randrange(self.width),
self.random.randrange(self.height),
)
energy = self.random.randrange(2 * sheep_gain_from_food)
sheep = Sheep(
self.next_id(),
self,
moore,
energy,
sheep_reproduce,
sheep_gain_from_food,
self.next_id(), self, energy, sheep_reproduce, sheep_gain_from_food
)
self.grid.place_agent(sheep, pos)
sheep.move_to(self.grid.cells[pos])
self.schedule.add(sheep)

# Create wolves
Expand All @@ -189,21 +190,21 @@ def __init__(
)
energy = self.random.randrange(2 * wolf_gain_from_food)
wolf = Wolf(
self.next_id(), self, moore, energy, wolf_reproduce, wolf_gain_from_food
self.next_id(), self, energy, wolf_reproduce, wolf_gain_from_food
)
self.grid.place_agent(wolf, pos)
wolf.move_to(self.grid.cells[pos])
Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you for agreeing to #1953 (comment) & to be as intuitive as NetLogo.

Copy link
Member

Choose a reason for hiding this comment

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

Once we added a CellAgent, adding a dedicated move method was a small step. Random walks are now just a one-liner as well.

self.schedule.add(wolf)

# Create grass patches
possibly_fully_grown = [True, False]
for _agent, pos in self.grid.coord_iter():
for cell in self.grid:
fully_grown = self.random.choice(possibly_fully_grown)
if fully_grown:
countdown = self.grass_regrowth_time
else:
countdown = self.random.randrange(self.grass_regrowth_time)
patch = GrassPatch(self.next_id(), self, fully_grown, countdown)
self.grid.place_agent(patch, pos)
patch.move_to(cell)
self.schedule.add(patch)

def step(self):
Expand Down
4 changes: 4 additions & 0 deletions mesa/experimental/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
from .jupyter_viz import JupyterViz, make_text, Slider # noqa
from mesa.experimental import cell_space


__all__ = ["JupyterViz", "make_text", "Slider", "cell_space"]
17 changes: 17 additions & 0 deletions mesa/experimental/cell_space/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from mesa.experimental.cell_space.cell import Cell
from mesa.experimental.cell_space.cell_agent import CellAgent
from mesa.experimental.cell_space.cell_collection import CellCollection
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
from mesa.experimental.cell_space.grid import Grid, HexGrid, OrthogonalGrid
from mesa.experimental.cell_space.network import Network

__all__ = [
"CellCollection",
"Cell",
"CellAgent",
"DiscreteSpace",
"Grid",
"HexGrid",
"OrthogonalGrid",
"Network",
]
127 changes: 127 additions & 0 deletions mesa/experimental/cell_space/cell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
from __future__ import annotations

from functools import cache
from random import Random
from typing import TYPE_CHECKING, Any

from mesa.experimental.cell_space.cell_collection import CellCollection

if TYPE_CHECKING:
from mesa.experimental.cell_space.cell_agent import CellAgent


class Cell:
EwoutH marked this conversation as resolved.
Show resolved Hide resolved
__slots__ = [
"coordinate",
"_connections",
"agents",
"capacity",
"properties",
"random",
]

def __init__(
self, coordinate: Any, capacity: int | None = None, random: Random | None = None
) -> None:
""" "

Args:
coordinate:
capacity (int) : the capacity of the cell. If None, the capacity is infinite
random (Random) : the random number generator to use

"""
super().__init__()
self.coordinate = coordinate
self._connections: list[Cell] = [] # TODO: change to CellCollection?
self.agents = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, )
self.capacity = capacity
self.properties: dict[str, object] = {}
self.random = random

def connect(self, other) -> None:
"""Connects this cell to another cell.

Args:
other (Cell): other cell to connect to

"""
self._connections.append(other)

def disconnect(self, other) -> None:
"""Disconnects this cell from another cell.

Args:
other (Cell): other cell to remove from connections

"""
self._connections.remove(other)

def add_agent(self, agent: CellAgent) -> None:
"""Adds an agent to the cell.

Args:
agent (CellAgent): agent to add to this Cell

"""
n = len(self.agents)

if self.capacity and n >= self.capacity:
raise Exception(
"ERROR: Cell is full"
) # FIXME we need MESA errors or a proper error

self.agents.append(agent)

def remove_agent(self, agent: CellAgent) -> None:
"""Removes an agent from the cell.

Args:
agent (CellAgent): agent to remove from this cell

"""
self.agents.remove(agent)
agent.cell = None

@property
def is_empty(self) -> bool:
"""Returns a bool of the contents of a cell."""
return len(self.agents) == 0

@property
def is_full(self) -> bool:
"""Returns a bool of the contents of a cell."""
return len(self.agents) == self.capacity

def __repr__(self):
return f"Cell({self.coordinate}, {self.agents})"

@cache
def neighborhood(self, radius=1, include_center=False):
return CellCollection(
self._neighborhood(radius=radius, include_center=include_center),
random=self.random,
)

@cache
def _neighborhood(self, radius=1, include_center=False):
# if radius == 0:
# return {self: self.agents}
if radius < 1:
raise ValueError("radius must be larger than one")
if radius == 1:
neighborhood = {neighbor: neighbor.agents for neighbor in self._connections}
if not include_center:
return neighborhood
else:
neighborhood[self] = self.agents
return neighborhood
else:
neighborhood = {}
for neighbor in self._connections:
neighborhood.update(
neighbor._neighborhood(radius - 1, include_center=True)
)
if not include_center:
neighborhood.pop(self, None)
return neighborhood
Loading
Loading