Mesa 3.2.0__py3-none-any.whl → 3.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of Mesa might be problematic. Click here for more details.

Files changed (40) hide show
  1. mesa/__init__.py +1 -1
  2. mesa/agent.py +3 -3
  3. mesa/datacollection.py +1 -1
  4. mesa/examples/advanced/epstein_civil_violence/app.py +11 -11
  5. mesa/examples/advanced/pd_grid/app.py +10 -11
  6. mesa/examples/advanced/sugarscape_g1mt/app.py +34 -16
  7. mesa/examples/advanced/wolf_sheep/app.py +21 -18
  8. mesa/examples/basic/boid_flockers/app.py +15 -11
  9. mesa/examples/basic/boltzmann_wealth_model/app.py +39 -32
  10. mesa/examples/basic/conways_game_of_life/app.py +13 -16
  11. mesa/examples/basic/schelling/Readme.md +2 -2
  12. mesa/examples/basic/schelling/agents.py +9 -3
  13. mesa/examples/basic/schelling/app.py +50 -3
  14. mesa/examples/basic/schelling/model.py +2 -0
  15. mesa/examples/basic/schelling/resources/blue_happy.png +0 -0
  16. mesa/examples/basic/schelling/resources/blue_unhappy.png +0 -0
  17. mesa/examples/basic/schelling/resources/orange_happy.png +0 -0
  18. mesa/examples/basic/schelling/resources/orange_unhappy.png +0 -0
  19. mesa/examples/basic/virus_on_network/app.py +31 -14
  20. mesa/experimental/continuous_space/continuous_space.py +1 -1
  21. mesa/space.py +4 -1
  22. mesa/visualization/__init__.py +2 -0
  23. mesa/visualization/backends/__init__.py +23 -0
  24. mesa/visualization/backends/abstract_renderer.py +97 -0
  25. mesa/visualization/backends/altair_backend.py +440 -0
  26. mesa/visualization/backends/matplotlib_backend.py +419 -0
  27. mesa/visualization/components/__init__.py +28 -8
  28. mesa/visualization/components/altair_components.py +86 -0
  29. mesa/visualization/components/matplotlib_components.py +4 -2
  30. mesa/visualization/components/portrayal_components.py +120 -0
  31. mesa/visualization/mpl_space_drawing.py +292 -129
  32. mesa/visualization/solara_viz.py +274 -32
  33. mesa/visualization/space_drawers.py +797 -0
  34. mesa/visualization/space_renderer.py +399 -0
  35. {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/METADATA +13 -4
  36. {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/RECORD +39 -29
  37. mesa/examples/advanced/sugarscape_g1mt/tests.py +0 -69
  38. {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/WHEEL +0 -0
  39. {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/licenses/LICENSE +0 -0
  40. {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/licenses/NOTICE +0 -0
mesa/__init__.py CHANGED
@@ -24,7 +24,7 @@ __all__ = [
24
24
  ]
25
25
 
26
26
  __title__ = "mesa"
27
- __version__ = "3.2.0"
27
+ __version__ = "3.3.0"
28
28
  __license__ = "Apache 2.0"
29
29
  _this_year = datetime.datetime.now(tz=datetime.UTC).date().year
30
30
  __copyright__ = f"Copyright {_this_year} Project Mesa Team"
mesa/agent.py CHANGED
@@ -185,7 +185,7 @@ class AgentSet(MutableSet, Sequence):
185
185
  Random()
186
186
  ) # FIXME see issue 1981, how to get the central rng from model
187
187
  self.random = random
188
- self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
188
+ self._agents = weakref.WeakKeyDictionary(dict.fromkeys(agents))
189
189
 
190
190
  def __len__(self) -> int:
191
191
  """Return the number of agents in the AgentSet."""
@@ -264,7 +264,7 @@ class AgentSet(MutableSet, Sequence):
264
264
  self.random.shuffle(weakrefs)
265
265
 
266
266
  if inplace:
267
- self._agents.data = {entry: None for entry in weakrefs}
267
+ self._agents.data = dict.fromkeys(weakrefs)
268
268
  return self
269
269
  else:
270
270
  return AgentSet(
@@ -303,7 +303,7 @@ class AgentSet(MutableSet, Sequence):
303
303
 
304
304
  This is a private method primarily used internally by other methods like select, shuffle, and sort.
305
305
  """
306
- self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
306
+ self._agents = weakref.WeakKeyDictionary(dict.fromkeys(agents))
307
307
  return self
308
308
 
309
309
  def do(self, method: str | Callable, *args, **kwargs) -> AgentSet:
mesa/datacollection.py CHANGED
@@ -300,7 +300,7 @@ class DataCollector:
300
300
  if agent_type in agent_types:
301
301
  agents = model.agents_by_type[agent_type]
302
302
  else:
303
- from mesa import Agent
303
+ from mesa import Agent # noqa: PLC0415
304
304
 
305
305
  if issubclass(agent_type, Agent):
306
306
  agents = [
@@ -7,9 +7,10 @@ from mesa.examples.advanced.epstein_civil_violence.model import EpsteinCivilViol
7
7
  from mesa.visualization import (
8
8
  Slider,
9
9
  SolaraViz,
10
+ SpaceRenderer,
10
11
  make_plot_component,
11
- make_space_component,
12
12
  )
13
+ from mesa.visualization.components import AgentPortrayalStyle
13
14
 
14
15
  COP_COLOR = "#000000"
15
16
 
@@ -24,14 +25,12 @@ def citizen_cop_portrayal(agent):
24
25
  if agent is None:
25
26
  return
26
27
 
27
- portrayal = {
28
- "size": 50,
29
- }
28
+ portrayal = AgentPortrayalStyle(size=200)
30
29
 
31
30
  if isinstance(agent, Citizen):
32
- portrayal["color"] = agent_colors[agent.state]
31
+ portrayal.update(("color", agent_colors[agent.state]))
33
32
  elif isinstance(agent, Cop):
34
- portrayal["color"] = COP_COLOR
33
+ portrayal.update(("color", COP_COLOR))
35
34
 
36
35
  return portrayal
37
36
 
@@ -51,7 +50,7 @@ model_params = {
51
50
  },
52
51
  "height": 40,
53
52
  "width": 40,
54
- "citizen_density": Slider("Initial Agent Density", 0.7, 0.0, 0.9, 0.1),
53
+ "citizen_density": Slider("Initial Agent Density", 0.7, 0.1, 0.9, 0.1),
55
54
  "cop_density": Slider("Initial Cop Density", 0.04, 0.0, 0.1, 0.01),
56
55
  "citizen_vision": Slider("Citizen Vision", 7, 1, 10, 1),
57
56
  "cop_vision": Slider("Cop Vision", 7, 1, 10, 1),
@@ -59,19 +58,20 @@ model_params = {
59
58
  "max_jail_term": Slider("Max Jail Term", 30, 0, 50, 1),
60
59
  }
61
60
 
62
- space_component = make_space_component(
63
- citizen_cop_portrayal, post_process=post_process, draw_grid=False
64
- )
65
61
 
66
62
  chart_component = make_plot_component(
67
63
  {state.name.lower(): agent_colors[state] for state in CitizenState}
68
64
  )
69
65
 
70
66
  epstein_model = EpsteinCivilViolence()
67
+ renderer = SpaceRenderer(epstein_model, backend="matplotlib")
68
+ renderer.draw_agents(citizen_cop_portrayal)
69
+ renderer.post_process = post_process
71
70
 
72
71
  page = SolaraViz(
73
72
  epstein_model,
74
- components=[space_component, chart_component],
73
+ renderer,
74
+ components=[chart_component],
75
75
  model_params=model_params,
76
76
  name="Epstein Civil Violence",
77
77
  )
@@ -6,20 +6,19 @@ from mesa.examples.advanced.pd_grid.model import PdGrid
6
6
  from mesa.visualization import (
7
7
  Slider,
8
8
  SolaraViz,
9
+ SpaceRenderer,
9
10
  make_plot_component,
10
- make_space_component,
11
11
  )
12
+ from mesa.visualization.components import AgentPortrayalStyle
12
13
 
13
14
 
14
15
  def pd_agent_portrayal(agent):
15
16
  """
16
17
  Portrayal function for rendering PD agents in the visualization.
17
18
  """
18
- return {
19
- "color": "blue" if agent.move == "C" else "red",
20
- "marker": "s", # square marker
21
- "size": 25,
22
- }
19
+ return AgentPortrayalStyle(
20
+ color="blue" if agent.move == "C" else "red", marker="s", size=25
21
+ )
23
22
 
24
23
 
25
24
  # Model parameters
@@ -40,19 +39,19 @@ model_params = {
40
39
  }
41
40
 
42
41
 
43
- # Create grid visualization component using Altair
44
- grid_viz = make_space_component(agent_portrayal=pd_agent_portrayal)
45
-
46
42
  # Create plot for tracking cooperating agents over time
47
- plot_component = make_plot_component("Cooperating_Agents")
43
+ plot_component = make_plot_component("Cooperating_Agents", backend="altair", grid=True)
48
44
 
49
45
  # Initialize model
50
46
  initial_model = PdGrid()
47
+ # Create grid and agent visualization component using Altair
48
+ renderer = SpaceRenderer(initial_model, backend="altair").render(pd_agent_portrayal)
51
49
 
52
50
  # Create visualization with all components
53
51
  page = SolaraViz(
54
52
  model=initial_model,
55
- components=[grid_viz, plot_component],
53
+ renderer=renderer,
54
+ components=[plot_component],
56
55
  model_params=model_params,
57
56
  name="Spatial Prisoner's Dilemma",
58
57
  )
@@ -1,24 +1,31 @@
1
1
  from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt
2
- from mesa.visualization import Slider, SolaraViz, make_plot_component
3
- from mesa.visualization.components.matplotlib_components import make_mpl_space_component
2
+ from mesa.visualization import Slider, SolaraViz, SpaceRenderer, make_plot_component
3
+ from mesa.visualization.components import AgentPortrayalStyle, PropertyLayerStyle
4
4
 
5
5
 
6
6
  def agent_portrayal(agent):
7
- return {"marker": "o", "color": "red", "size": 10}
7
+ return AgentPortrayalStyle(
8
+ x=agent.cell.coordinate[0],
9
+ y=agent.cell.coordinate[1],
10
+ color="red",
11
+ marker="o",
12
+ size=10,
13
+ zorder=1,
14
+ )
8
15
 
9
16
 
10
- propertylayer_portrayal = {
11
- "sugar": {"color": "blue", "alpha": 0.8, "colorbar": True, "vmin": 0, "vmax": 10},
12
- "spice": {"color": "red", "alpha": 0.8, "colorbar": True, "vmin": 0, "vmax": 10},
13
- }
17
+ def propertylayer_portrayal(layer):
18
+ if layer.name == "sugar":
19
+ return PropertyLayerStyle(
20
+ color="blue", alpha=0.8, colorbar=True, vmin=0, vmax=10
21
+ )
22
+ return PropertyLayerStyle(color="red", alpha=0.8, colorbar=True, vmin=0, vmax=10)
14
23
 
15
24
 
16
- sugarscape_space = make_mpl_space_component(
17
- agent_portrayal=agent_portrayal,
18
- propertylayer_portrayal=propertylayer_portrayal,
19
- post_process=None,
20
- draw_grid=False,
21
- )
25
+ def post_process(chart):
26
+ chart = chart.properties(width=400, height=400)
27
+ return chart
28
+
22
29
 
23
30
  model_params = {
24
31
  "seed": {
@@ -47,12 +54,23 @@ model_params = {
47
54
 
48
55
  model = SugarscapeG1mt()
49
56
 
57
+ # Here, the renderer uses the Altair backend, while the plot components
58
+ # use the Matplotlib backend.
59
+ # Both can be mixed and matched to enhance the visuals of your model.
60
+ renderer = SpaceRenderer(model, backend="altair").render(
61
+ agent_portrayal=agent_portrayal,
62
+ propertylayer_portrayal=propertylayer_portrayal,
63
+ post_process=post_process,
64
+ )
65
+
66
+ # Note: It is advised to switch the pages after pausing the model
67
+ # on the Solara dashboard.
50
68
  page = SolaraViz(
51
69
  model,
70
+ renderer,
52
71
  components=[
53
- sugarscape_space,
54
- make_plot_component("#Traders"),
55
- make_plot_component("Price"),
72
+ make_plot_component("#Traders", page=1),
73
+ make_plot_component("Price", page=1),
56
74
  ],
57
75
  model_params=model_params,
58
76
  name="Sugarscape {G1, M, T}",
@@ -5,34 +5,32 @@ from mesa.visualization import (
5
5
  CommandConsole,
6
6
  Slider,
7
7
  SolaraViz,
8
+ SpaceRenderer,
8
9
  make_plot_component,
9
- make_space_component,
10
10
  )
11
+ from mesa.visualization.components import AgentPortrayalStyle
11
12
 
12
13
 
13
14
  def wolf_sheep_portrayal(agent):
14
15
  if agent is None:
15
16
  return
16
17
 
17
- portrayal = {
18
- "size": 25,
19
- }
18
+ portrayal = AgentPortrayalStyle(
19
+ size=50,
20
+ marker="o",
21
+ zorder=2,
22
+ )
20
23
 
21
24
  if isinstance(agent, Wolf):
22
- portrayal["color"] = "tab:red"
23
- portrayal["marker"] = "o"
24
- portrayal["zorder"] = 2
25
+ portrayal.update(("color", "red"))
25
26
  elif isinstance(agent, Sheep):
26
- portrayal["color"] = "tab:cyan"
27
- portrayal["marker"] = "o"
28
- portrayal["zorder"] = 2
27
+ portrayal.update(("color", "cyan"))
29
28
  elif isinstance(agent, GrassPatch):
30
29
  if agent.fully_grown:
31
- portrayal["color"] = "tab:green"
30
+ portrayal.update(("color", "tab:green"))
32
31
  else:
33
- portrayal["color"] = "tab:brown"
34
- portrayal["marker"] = "s"
35
- portrayal["size"] = 75
32
+ portrayal.update(("color", "tab:brown"))
33
+ portrayal.update(("marker", "s"), ("size", 125), ("zorder", 1))
36
34
 
37
35
  return portrayal
38
36
 
@@ -75,9 +73,6 @@ def post_process_lines(ax):
75
73
  ax.legend(loc="center left", bbox_to_anchor=(1, 0.9))
76
74
 
77
75
 
78
- space_component = make_space_component(
79
- wolf_sheep_portrayal, draw_grid=False, post_process=post_process_space
80
- )
81
76
  lineplot_component = make_plot_component(
82
77
  {"Wolves": "tab:orange", "Sheep": "tab:cyan", "Grass": "tab:green"},
83
78
  post_process=post_process_lines,
@@ -86,9 +81,17 @@ lineplot_component = make_plot_component(
86
81
  simulator = ABMSimulator()
87
82
  model = WolfSheep(simulator=simulator, grass=True)
88
83
 
84
+ renderer = SpaceRenderer(
85
+ model,
86
+ backend="matplotlib",
87
+ )
88
+ renderer.draw_agents(wolf_sheep_portrayal)
89
+ renderer.post_process = post_process_space
90
+
89
91
  page = SolaraViz(
90
92
  model,
91
- components=[space_component, lineplot_component, CommandConsole],
93
+ renderer,
94
+ components=[lineplot_component, CommandConsole],
92
95
  model_params=model_params,
93
96
  name="Wolf Sheep",
94
97
  simulator=simulator,
@@ -1,12 +1,8 @@
1
- import os
2
- import sys
3
-
4
1
  from matplotlib.markers import MarkerStyle
5
2
 
6
- sys.path.insert(0, os.path.abspath("../../../.."))
7
-
8
3
  from mesa.examples.basic.boid_flockers.model import BoidFlockers
9
- from mesa.visualization import Slider, SolaraViz, make_space_component
4
+ from mesa.visualization import Slider, SolaraViz, SpaceRenderer
5
+ from mesa.visualization.components import AgentPortrayalStyle
10
6
 
11
7
  # Pre-compute markers for different angles (e.g., every 10 degrees)
12
8
  MARKER_CACHE = {}
@@ -25,10 +21,12 @@ def boid_draw(agent):
25
21
  rounded_deg = round(deg / 10) * 10 % 360
26
22
 
27
23
  # using cached markers to speed things up
28
- if neighbors <= 1:
29
- return {"color": "red", "size": 20, "marker": MARKER_CACHE[rounded_deg]}
30
- elif neighbors >= 2:
31
- return {"color": "green", "size": 20, "marker": MARKER_CACHE[rounded_deg]}
24
+ boid_style = AgentPortrayalStyle(
25
+ color="red", size=20, marker=MARKER_CACHE[rounded_deg]
26
+ )
27
+ if neighbors >= 2:
28
+ boid_style.update(("color", "green"), ("marker", MARKER_CACHE[rounded_deg]))
29
+ return boid_style
32
30
 
33
31
 
34
32
  model_params = {
@@ -71,9 +69,15 @@ model_params = {
71
69
 
72
70
  model = BoidFlockers()
73
71
 
72
+ # Quickest way to visualize grid along with agents or property layers.
73
+ renderer = SpaceRenderer(
74
+ model,
75
+ backend="matplotlib",
76
+ ).render(agent_portrayal=boid_draw)
77
+
74
78
  page = SolaraViz(
75
79
  model,
76
- components=[make_space_component(agent_portrayal=boid_draw, backend="matplotlib")],
80
+ renderer,
77
81
  model_params=model_params,
78
82
  name="Boid Flocking Model",
79
83
  )
@@ -1,17 +1,21 @@
1
+ import altair as alt
2
+
1
3
  from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealth
2
4
  from mesa.mesa_logging import INFO, log_to_stderr
3
5
  from mesa.visualization import (
4
6
  SolaraViz,
7
+ SpaceRenderer,
5
8
  make_plot_component,
6
- make_space_component,
7
9
  )
10
+ from mesa.visualization.components import AgentPortrayalStyle
8
11
 
9
12
  log_to_stderr(INFO)
10
13
 
11
14
 
12
15
  def agent_portrayal(agent):
13
- color = agent.wealth # we are using a colormap to translate wealth to color
14
- return {"color": color}
16
+ return AgentPortrayalStyle(
17
+ color=agent.wealth
18
+ ) # we are using a colormap to translate wealth to color
15
19
 
16
20
 
17
21
  model_params = {
@@ -33,45 +37,48 @@ model_params = {
33
37
  }
34
38
 
35
39
 
36
- def post_process(ax):
37
- ax.get_figure().colorbar(ax.collections[0], label="wealth", ax=ax)
40
+ def post_process(chart):
41
+ """Post-process the Altair chart to add a colorbar legend."""
42
+ chart = chart.encode(
43
+ color=alt.Color(
44
+ "color:N",
45
+ scale=alt.Scale(scheme="viridis", domain=[0, 10]),
46
+ legend=alt.Legend(
47
+ title="Wealth",
48
+ orient="right",
49
+ type="gradient",
50
+ gradientLength=200,
51
+ ),
52
+ ),
53
+ )
54
+ return chart
38
55
 
39
56
 
40
- # Create initial model instance
41
57
  model = BoltzmannWealth(50, 10, 10)
42
58
 
43
- # Create visualization elements. The visualization elements are solara components
44
- # that receive the model instance as a "prop" and display it in a certain way.
45
- # Under the hood these are just classes that receive the model instance.
46
- # You can also author your own visualization elements, which can also be functions
47
- # that receive the model instance and return a valid solara component.
59
+ # The SpaceRenderer is responsible for drawing the model's space and agents.
60
+ # It builds the visualization in layers, first drawing the grid structure,
61
+ # and then drawing the agents on top. It uses a specified backend
62
+ # (like "altair" or "matplotlib") for creating the plots.
63
+ renderer = SpaceRenderer(model, backend="altair")
64
+ # Can customize the grid appearance.
65
+ renderer.draw_structure(grid_color="black", grid_dash=[6, 2], grid_opacity=0.3)
66
+ renderer.draw_agents(agent_portrayal=agent_portrayal, cmap="viridis", vmin=0, vmax=10)
48
67
 
49
- SpaceGraph = make_space_component(
50
- agent_portrayal, cmap="viridis", vmin=0, vmax=10, post_process=post_process
51
- )
68
+ # The post_process function is used to modify the Altair chart after it has been created.
69
+ # It can be used to add legends, colorbars, or other visual elements.
70
+ renderer.post_process = post_process
71
+
72
+ # Creates a line plot component from the model's "Gini" datacollector.
52
73
  GiniPlot = make_plot_component("Gini")
53
74
 
54
- # Create the SolaraViz page. This will automatically create a server and display the
55
- # visualization elements in a web browser.
56
- # Display it using the following command in the example directory:
57
- # solara run app.py
58
- # It will automatically update and display any changes made to this file
75
+ # The SolaraViz page combines the model, renderer, and components into a web interface.
76
+ # To run the visualization, save this code as app.py and run `solara run app.py`
59
77
  page = SolaraViz(
60
78
  model,
61
- components=[SpaceGraph, GiniPlot],
79
+ renderer,
80
+ components=[GiniPlot],
62
81
  model_params=model_params,
63
82
  name="Boltzmann Wealth Model",
64
83
  )
65
84
  page # noqa
66
-
67
-
68
- # In a notebook environment, we can also display the visualization elements directly
69
- # SpaceGraph(model1)
70
- # GiniPlot(model1)
71
-
72
- # The plots will be static. If you want to pick up model steps,
73
- # you have to make the model reactive first
74
- # reactive_model = solara.reactive(model1)
75
- # SpaceGraph(reactive_model)
76
- # In a different notebook block:
77
- # reactive_model.value.step()
@@ -1,16 +1,17 @@
1
1
  from mesa.examples.basic.conways_game_of_life.model import ConwaysGameOfLife
2
2
  from mesa.visualization import (
3
3
  SolaraViz,
4
- make_space_component,
4
+ SpaceRenderer,
5
5
  )
6
+ from mesa.visualization.components import AgentPortrayalStyle
6
7
 
7
8
 
8
9
  def agent_portrayal(agent):
9
- return {
10
- "color": "white" if agent.state == 0 else "black",
11
- "marker": "s",
12
- "size": 25,
13
- }
10
+ return AgentPortrayalStyle(
11
+ color="white" if agent.state == 0 else "black",
12
+ marker="s",
13
+ size=30,
14
+ )
14
15
 
15
16
 
16
17
  def post_process(ax):
@@ -54,15 +55,11 @@ model_params = {
54
55
  # Create initial model instance
55
56
  model1 = ConwaysGameOfLife()
56
57
 
57
- # Create visualization elements. The visualization elements are solara components
58
- # that receive the model instance as a "prop" and display it in a certain way.
59
- # Under the hood these are just classes that receive the model instance.
60
- # You can also author your own visualization elements, which can also be functions
61
- # that receive the model instance and return a valid solara component.
62
- SpaceGraph = make_space_component(
63
- agent_portrayal, post_process=post_process, draw_grid=False
64
- )
65
-
58
+ renderer = SpaceRenderer(model1, backend="matplotlib")
59
+ # In this case the renderer only draws the agents because we just want to observe
60
+ # the state of the agents, not the structure of the grid.
61
+ renderer.draw_agents(agent_portrayal=agent_portrayal)
62
+ renderer.post_process = post_process
66
63
 
67
64
  # Create the SolaraViz page. This will automatically create a server and display the
68
65
  # visualization elements in a web browser.
@@ -71,7 +68,7 @@ SpaceGraph = make_space_component(
71
68
  # It will automatically update and display any changes made to this file
72
69
  page = SolaraViz(
73
70
  model1,
74
- components=[SpaceGraph],
71
+ renderer,
75
72
  model_params=model_params,
76
73
  name="Game of Life",
77
74
  )
@@ -2,9 +2,9 @@
2
2
 
3
3
  ## Summary
4
4
 
5
- The Schelling segregation model is a classic agent-based model, demonstrating how even a mild preference for similar neighbors can lead to a much higher degree of segregation than we would intuitively expect. The model consists of agents on a square grid, where each grid cell can contain at most one agent. Agents come in two colors: red and blue. They are happy if a certain number of their eight possible neighbors are of the same color, and unhappy otherwise. Unhappy agents will pick a random empty cell to move to each step, until they are happy. The model keeps running until there are no unhappy agents.
5
+ The Schelling segregation model is a classic agent-based model, demonstrating how even a mild preference for similar neighbors can lead to a much higher degree of segregation than we would intuitively expect. The model consists of agents on a square grid, where each grid cell can contain at most one agent. Agents come in two colors: orange and blue. They are happy if a certain number of their eight possible neighbors are of the same color, and unhappy otherwise. Unhappy agents will pick a random empty cell to move to each step, until they are happy. The model keeps running until there are no unhappy agents.
6
6
 
7
- By default, the number of similar neighbors the agents need to be happy is set to 3. That means the agents would be perfectly happy with a majority of their neighbors being of a different color (e.g. a Blue agent would be happy with five Red neighbors and three Blue ones). Despite this, the model consistently leads to a high degree of segregation, with most agents ending up with no neighbors of a different color.
7
+ By default, the number of similar neighbors the agents need to be happy is set to 3. That means the agents would be perfectly happy with a majority of their neighbors being of a different color (e.g. a Blue agent would be happy with five Orange neighbors and three Blue ones). Despite this, the model consistently leads to a high degree of segregation, with most agents ending up with no neighbors of a different color.
8
8
 
9
9
  ## How to Run
10
10
 
@@ -19,8 +19,9 @@ class SchellingAgent(CellAgent):
19
19
  self.type = agent_type
20
20
  self.homophily = homophily
21
21
  self.radius = radius
22
+ self.happy = False
22
23
 
23
- def step(self) -> None:
24
+ def assign_state(self) -> None:
24
25
  """Determine if agent is happy and move if necessary."""
25
26
  neighbors = list(self.cell.get_neighborhood(radius=self.radius).agents)
26
27
 
@@ -34,8 +35,13 @@ class SchellingAgent(CellAgent):
34
35
  # If there are no neighbors, the similarity fraction is 0
35
36
  similarity_fraction = 0.0
36
37
 
37
- # Move if unhappy
38
38
  if similarity_fraction < self.homophily:
39
- self.cell = self.model.grid.select_random_empty_cell()
39
+ self.happy = False
40
40
  else:
41
+ self.happy = True
41
42
  self.model.happy += 1
43
+
44
+ def step(self) -> None:
45
+ # Move if unhappy
46
+ if not self.happy:
47
+ self.cell = self.model.grid.select_random_empty_cell()
@@ -1,12 +1,15 @@
1
+ import os
2
+
1
3
  import solara
2
4
 
3
5
  from mesa.examples.basic.schelling.model import Schelling
4
6
  from mesa.visualization import (
5
7
  Slider,
6
8
  SolaraViz,
9
+ SpaceRenderer,
7
10
  make_plot_component,
8
- make_space_component,
9
11
  )
12
+ from mesa.visualization.components import AgentPortrayalStyle
10
13
 
11
14
 
12
15
  def get_happy_agents(model):
@@ -14,8 +17,45 @@ def get_happy_agents(model):
14
17
  return solara.Markdown(f"**Happy agents: {model.happy}**")
15
18
 
16
19
 
20
+ path = os.path.dirname(os.path.abspath(__file__))
21
+
22
+
17
23
  def agent_portrayal(agent):
18
- return {"color": "tab:orange" if agent.type == 0 else "tab:blue"}
24
+ style = AgentPortrayalStyle(
25
+ x=agent.cell.coordinate[0],
26
+ y=agent.cell.coordinate[1],
27
+ marker=os.path.join(path, "resources", "orange_happy.png"),
28
+ size=75,
29
+ )
30
+ if agent.type == 0:
31
+ if agent.happy:
32
+ style.update(
33
+ (
34
+ "marker",
35
+ os.path.join(path, "resources", "blue_happy.png"),
36
+ ),
37
+ )
38
+ else:
39
+ style.update(
40
+ (
41
+ "marker",
42
+ os.path.join(path, "resources", "blue_unhappy.png"),
43
+ ),
44
+ ("size", 50),
45
+ ("zorder", 2),
46
+ )
47
+ else:
48
+ if not agent.happy:
49
+ style.update(
50
+ (
51
+ "marker",
52
+ os.path.join(path, "resources", "orange_unhappy.png"),
53
+ ),
54
+ ("size", 50),
55
+ ("zorder", 2),
56
+ )
57
+
58
+ return style
19
59
 
20
60
 
21
61
  model_params = {
@@ -31,14 +71,21 @@ model_params = {
31
71
  "height": 20,
32
72
  }
33
73
 
74
+ # Note: Models with images as markers are very performance intensive.
34
75
  model1 = Schelling()
76
+ renderer = SpaceRenderer(model1, backend="matplotlib")
77
+ # Here we use renderer.render() to render the agents and grid in one go.
78
+ # This function always renders the grid and then renders the agents or
79
+ # property layers on top of it if specified. It also supports passing the
80
+ # post_process function to fine-tune the plot after rendering in itself.
81
+ renderer.render(agent_portrayal=agent_portrayal)
35
82
 
36
83
  HappyPlot = make_plot_component({"happy": "tab:green"})
37
84
 
38
85
  page = SolaraViz(
39
86
  model1,
87
+ renderer,
40
88
  components=[
41
- make_space_component(agent_portrayal),
42
89
  HappyPlot,
43
90
  get_happy_agents,
44
91
  ],
@@ -68,11 +68,13 @@ class Schelling(Model):
68
68
  )
69
69
 
70
70
  # Collect initial state
71
+ self.agents.do("assign_state")
71
72
  self.datacollector.collect(self)
72
73
 
73
74
  def step(self):
74
75
  """Run one step of the model."""
75
76
  self.happy = 0 # Reset counter of happy agents
76
77
  self.agents.shuffle_do("step") # Activate all agents in random order
78
+ self.agents.do("assign_state")
77
79
  self.datacollector.collect(self) # Collect data
78
80
  self.running = self.happy < len(self.agents) # Continue until everyone is happy