From a06f81705c383c42da6807560cfd7e87807f698e Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 29 Jun 2023 01:45:50 -0400 Subject: [PATCH 01/12] boltzmann wealth model: Replace Streamlit viz with Solara --- examples/boltzmann_wealth_model/Readme.md | 6 +- examples/boltzmann_wealth_model/app.py | 126 +++------------------ experimental/__init__.py | 1 + experimental/jupyter_viz.py | 131 ++++++++++++++++++++++ setup.cfg | 1 + 5 files changed, 152 insertions(+), 113 deletions(-) create mode 100644 experimental/__init__.py create mode 100644 experimental/jupyter_viz.py diff --git a/examples/boltzmann_wealth_model/Readme.md b/examples/boltzmann_wealth_model/Readme.md index 4a6e21f1..9f952ef2 100644 --- a/examples/boltzmann_wealth_model/Readme.md +++ b/examples/boltzmann_wealth_model/Readme.md @@ -35,9 +35,9 @@ If your browser doesn't open automatically, point it to [http://127.0.0.1:8521/] ## Optional -* ``boltzmann_wealth_model/app.py``: can be used to run the simulation via the streamlit interface. -* For this some additional packages like ``streamlit`` and ``altair`` needs to be installed. -* Once installed, the app can be opened in the browser using : ``streamlit run app.py`` +* ``boltzmann_wealth_model/app.py``: can be used to run the simulation via the Solara interface. +* For this, an additional packages ``solara`` needs to be installed. +* Once installed, the app can be opened in the browser after running ``solara run app.py`` ## Further Reading diff --git a/examples/boltzmann_wealth_model/app.py b/examples/boltzmann_wealth_model/app.py index f2dd6da9..c1b5aab3 100644 --- a/examples/boltzmann_wealth_model/app.py +++ b/examples/boltzmann_wealth_model/app.py @@ -1,113 +1,19 @@ -import time - -import altair as alt -import pandas as pd -import streamlit as st +from mesa_models.experimental import JupyterViz from boltzmann_wealth_model.model import BoltzmannWealthModel -model = st.title("Boltzman Wealth Model") -num_agents = st.slider( - "Choose how many agents to include in the model", - min_value=1, - max_value=100, - value=50, -) -num_ticks = st.slider( - "Select number of Simulation Runs", min_value=1, max_value=100, value=50 +model_params = { + "N": { + "type": "SliderInt", + "value": 50, + "label": "Number of agents:", + "min": 10, + "max": 100, + "step": 1, + }, + "width": 10, + "height": 10, +} + +page = JupyterViz( + BoltzmannWealthModel, model_params, measures=["Gini"], name="Money Model" ) -height = st.slider("Select Grid Height", min_value=10, max_value=100, step=10, value=15) -width = st.slider("Select Grid Width", min_value=10, max_value=100, step=10, value=20) -model = BoltzmannWealthModel(num_agents, height, width) - - -status_text = st.empty() -run = st.button("Run Simulation") - - -if run: - tick = time.time() - step = 0 - # init grid - df_grid = pd.DataFrame() - df_gini = pd.DataFrame({"step": [0], "gini": [-1]}) - for x in range(width): - for y in range(height): - df_grid = pd.concat( - [df_grid, pd.DataFrame({"x": [x], "y": [y], "agent_count": 0})], - ignore_index=True, - ) - - heatmap = ( - alt.Chart(df_grid) - .mark_point(size=100) - .encode(x="x", y="y", color=alt.Color("agent_count")) - .interactive() - .properties(width=800, height=600) - ) - - line = ( - alt.Chart(df_gini) - .mark_line(point=True) - .encode(x="step", y="gini") - .properties(width=800, height=600) - ) - - # init progress bar - my_bar = st.progress(0, text="Simulation Progress") # progress - placeholder = st.empty() - st.subheader("Agent Grid") - chart = st.altair_chart(heatmap) - st.subheader("Gini Values") - line_chart = st.altair_chart(line) - - color_scale = alt.Scale( - domain=[0, 1, 2, 3, 4], range=["red", "cyan", "white", "white", "blue"] - ) - for i in range(num_ticks): - model.step() - my_bar.progress((i / num_ticks), text="Simulation progress") - placeholder.text("Step = %d" % i) - for cell in model.grid.coord_iter(): - cell_content, x, y = cell - agent_count = len(cell_content) - selected_row = df_grid[(df_grid["x"] == x) & (df_grid["y"] == y)] - df_grid.loc[ - selected_row.index, "agent_count" - ] = agent_count # random.choice([1,2]) - - df_gini = pd.concat( - [ - df_gini, - pd.DataFrame( - {"step": [i], "gini": [model.datacollector.model_vars["Gini"][i]]} - ), - ] - ) - # st.table(df_grid) - heatmap = ( - alt.Chart(df_grid) - .mark_circle(size=100) - .encode(x="x", y="y", color=alt.Color("agent_count", scale=color_scale)) - .interactive() - .properties(width=800, height=600) - ) - chart.altair_chart(heatmap) - - line = ( - alt.Chart(df_gini) - .mark_line(point=True) - .encode(x="step", y="gini") - .properties(width=800, height=600) - ) - line_chart.altair_chart(line) - - time.sleep(0.01) - - tock = time.time() - st.success(f"Simulation completed in {tock - tick:.2f} secs") - - # st.subheader('Agent Grid') - # fig = px.imshow(agent_counts,labels={'color':'Agent Count'}) - # st.plotly_chart(fig) - # st.subheader('Gini value over sim ticks (Plotly)') - # chart = st.line_chart(model.datacollector.model_vars['Gini']) diff --git a/experimental/__init__.py b/experimental/__init__.py new file mode 100644 index 00000000..da569911 --- /dev/null +++ b/experimental/__init__.py @@ -0,0 +1 @@ +from .jupyter_viz import JupyterViz # noqa diff --git a/experimental/jupyter_viz.py b/experimental/jupyter_viz.py new file mode 100644 index 00000000..799dc7bb --- /dev/null +++ b/experimental/jupyter_viz.py @@ -0,0 +1,131 @@ +import threading + +import solara +import matplotlib.pyplot as plt +from matplotlib.figure import Figure +from matplotlib.ticker import MaxNLocator + + +# Avoid interactive backend +plt.switch_backend("agg") + + +def agent_portrayal(agent): + if agent.wealth > 0: + return 50 + return 10 + + +class JupyterContainer: + def __init__(self, model_class, model_params, measures=None, name="Mesa Model"): + self.model_class = model_class + self.split_model_params(model_params) + self.measures = measures + self.name = name + self.thread = None + + def split_model_params(self, model_params): + self.model_params_input = {} + self.model_params_fixed = {} + for k, v in model_params.items(): + if self.check_param_is_fixed(v): + self.model_params_fixed[k] = v + else: + self.model_params_input[k] = v + + def check_param_is_fixed(self, param): + if not isinstance(param, dict): + return True + if "type" not in param: + return True + + def do_step(self): + self.model.step() + self.set_df(self.model.datacollector.get_model_vars_dataframe()) + + def do_play(self): + self.model.running = True + while self.model.running: + self.do_step() + + def threaded_do_play(self): + if self.thread is not None and self.thread.is_alive(): + return + self.thread = threading.Thread(target=self.do_play) + self.thread.start() + + def do_pause(self): + if (self.thread is None) or (not self.thread.is_alive()): + return + self.model.running = False + self.thread.join() + + @staticmethod + def portray(g): + x = [] + y = [] + s = [] + for i in range(g.width): + for j in range(g.height): + for agent in g._grid[i][j]: + _s = agent_portrayal(agent) + x.append(i) + y.append(j) + s.append(_s) + return {"x": x, "y": y, "s": s} + + +@solara.component +def MesaComponent(viz): + solara.Markdown(viz.name) + + # 1. User inputs + user_inputs = {} + for k, v in viz.model_params_input.items(): + if v["type"] == "SliderInt": + user_input = solara.use_reactive(v["value"]) + user_inputs[k] = user_input.value + solara.SliderInt( + v.get("label", "label"), + value=user_input, + min=v.get("min", 0), + max=v.get("max", 10), + step=v.get("step", 1), + ) + + # 2. Model + def make_model(): + return viz.model_class(**user_inputs, **viz.model_params_fixed) + + viz.model = solara.use_memo(make_model, dependencies=list(user_inputs.values())) + viz.df, viz.set_df = solara.use_state( + viz.model.datacollector.get_model_vars_dataframe() + ) + + # 3. Buttons + with solara.Row(): + solara.Button(label="Step", color="primary", on_click=viz.do_step) + solara.Button(label="▶", color="primary", on_click=viz.threaded_do_play) + solara.Button(label="⏸︎", color="primary", on_click=viz.do_pause) + # solara.Button(label="Reset", color="primary", on_click=do_reset) + + # 3. Space + space_fig = Figure() + space_ax = space_fig.subplots() + space_ax.scatter(**viz.portray(viz.model.grid)) + space_ax.set_axis_off() + solara.FigureMatplotlib(space_fig, dependencies=[viz.model, viz.df]) + + # 4. Plots + for i, measure in enumerate(viz.measures): + fig = Figure() + ax = fig.subplots() + ax.plot(viz.df.loc[:, measure]) + ax.set_ylabel(measure) + # Set integer x axis + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + solara.FigureMatplotlib(fig, dependencies=[viz.model, viz.df]) + + +def JupyterViz(model_class, model_params, measures=None, name="Mesa Model"): + return MesaComponent(JupyterContainer(model_class, model_params, measures, name)) diff --git a/setup.cfg b/setup.cfg index 2177d874..a478f8af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,3 +4,4 @@ package_dir = mesa_models.schelling = examples/schelling mesa_models.epstein_civil_violence = examples/epstein_civil_violence/epstein_civil_violence mesa_models.wolf_sheep = examples/wolf_sheep/wolf_sheep + mesa_models.experimental = experimental From e2e84adbf1e89d59948bdb7d3f475152514bf4e3 Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 29 Jun 2023 01:48:53 -0400 Subject: [PATCH 02/12] Simplify boltzmann_wealth_model frontend setup --- examples/boltzmann_wealth_model/Readme.md | 21 ++++------ .../{boltzmann_wealth_model => }/__init__.py | 0 examples/boltzmann_wealth_model/app.py | 4 +- .../boltzmann_wealth_model/server.py | 40 ------------------- .../{boltzmann_wealth_model => }/model.py | 0 .../boltzmann_wealth_model/requirements.txt | 1 + examples/boltzmann_wealth_model/run.py | 3 -- setup.cfg | 2 +- 8 files changed, 12 insertions(+), 59 deletions(-) rename examples/boltzmann_wealth_model/{boltzmann_wealth_model => }/__init__.py (100%) delete mode 100644 examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py rename examples/boltzmann_wealth_model/{boltzmann_wealth_model => }/model.py (100%) delete mode 100644 examples/boltzmann_wealth_model/run.py diff --git a/examples/boltzmann_wealth_model/Readme.md b/examples/boltzmann_wealth_model/Readme.md index 9f952ef2..fc27fdb2 100644 --- a/examples/boltzmann_wealth_model/Readme.md +++ b/examples/boltzmann_wealth_model/Readme.md @@ -12,32 +12,25 @@ As the model runs, the distribution of wealth among agents goes from being perfe To follow the tutorial example, launch the Jupyter Notebook and run the code in ``Introduction to Mesa Tutorial Code.ipynb`` which you can find in the main mesa repo [here](https://github.com/projectmesa/mesa/blob/main/docs/tutorials/intro_tutorial.ipynb) -To launch the interactive server, as described in the [last section of the tutorial](https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html#adding-visualization), run: +Make sure to install the requirements first: ``` - $ python server.py + pip install -r requirements.txt ``` -Make sure to install the requirements first: +To launch the interactive server, as described in the [last section of the tutorial](https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html#adding-visualization), run: ``` - pip install -r requirements.txt + $ solara run app.py ``` -If your browser doesn't open automatically, point it to [http://127.0.0.1:8521/](http://127.0.0.1:8521/). When the visualization loads, press Reset, then Run. +If your browser doesn't open automatically, point it to [http://127.0.0.1:8765/](http://127.0.0.1:8765/). When the visualization loads, click on the Play button. ## Files -* ``boltzmann_wealth_model/model.py``: Final version of the model. -* ``boltzmann_wealth_model/server.py``: Code for the interactive visualization. -* ``run.py``: Launches the server. - -## Optional - -* ``boltzmann_wealth_model/app.py``: can be used to run the simulation via the Solara interface. -* For this, an additional packages ``solara`` needs to be installed. -* Once installed, the app can be opened in the browser after running ``solara run app.py`` +* ``model.py``: Final version of the model. +* ``app.py``: Code for the interactive visualization. ## Further Reading diff --git a/examples/boltzmann_wealth_model/boltzmann_wealth_model/__init__.py b/examples/boltzmann_wealth_model/__init__.py similarity index 100% rename from examples/boltzmann_wealth_model/boltzmann_wealth_model/__init__.py rename to examples/boltzmann_wealth_model/__init__.py diff --git a/examples/boltzmann_wealth_model/app.py b/examples/boltzmann_wealth_model/app.py index c1b5aab3..b87da8fe 100644 --- a/examples/boltzmann_wealth_model/app.py +++ b/examples/boltzmann_wealth_model/app.py @@ -1,5 +1,6 @@ from mesa_models.experimental import JupyterViz -from boltzmann_wealth_model.model import BoltzmannWealthModel + +from model import BoltzmannWealthModel model_params = { "N": { @@ -17,3 +18,4 @@ page = JupyterViz( BoltzmannWealthModel, model_params, measures=["Gini"], name="Money Model" ) +page diff --git a/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py b/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py deleted file mode 100644 index a49546ce..00000000 --- a/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py +++ /dev/null @@ -1,40 +0,0 @@ -import mesa - -from .model import BoltzmannWealthModel - - -def agent_portrayal(agent): - portrayal = {"Shape": "circle", "Filled": "true", "r": 0.5} - - if agent.wealth > 0: - portrayal["Color"] = "red" - portrayal["Layer"] = 0 - else: - portrayal["Color"] = "grey" - portrayal["Layer"] = 1 - portrayal["r"] = 0.2 - return portrayal - - -grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500) -chart = mesa.visualization.ChartModule( - [{"Label": "Gini", "Color": "#0000FF"}], data_collector_name="datacollector" -) - -model_params = { - "N": mesa.visualization.Slider( - "Number of agents", - 100, - 2, - 200, - 1, - description="Choose how many agents to include in the model", - ), - "width": 10, - "height": 10, -} - -server = mesa.visualization.ModularServer( - BoltzmannWealthModel, [grid, chart], "Money Model", model_params -) -server.port = 8521 diff --git a/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py b/examples/boltzmann_wealth_model/model.py similarity index 100% rename from examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py rename to examples/boltzmann_wealth_model/model.py diff --git a/examples/boltzmann_wealth_model/requirements.txt b/examples/boltzmann_wealth_model/requirements.txt index 63b0d24e..4d93614c 100644 --- a/examples/boltzmann_wealth_model/requirements.txt +++ b/examples/boltzmann_wealth_model/requirements.txt @@ -1 +1,2 @@ mesa~=1.1 +solara diff --git a/examples/boltzmann_wealth_model/run.py b/examples/boltzmann_wealth_model/run.py deleted file mode 100644 index f1767593..00000000 --- a/examples/boltzmann_wealth_model/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from boltzmann_wealth_model.server import server - -server.launch(open_browser=True) diff --git a/setup.cfg b/setup.cfg index a478f8af..fefa5a4b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [options] package_dir = - mesa_models.boltzmann_wealth_model = examples/boltzmann_wealth_model/boltzmann_wealth_model + mesa_models.boltzmann_wealth_model = examples/boltzmann_wealth_model mesa_models.schelling = examples/schelling mesa_models.epstein_civil_violence = examples/epstein_civil_violence/epstein_civil_violence mesa_models.wolf_sheep = examples/wolf_sheep/wolf_sheep From fbbb641beece2e22c35e70589c84122e082baa01 Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 29 Jun 2023 03:13:43 -0400 Subject: [PATCH 03/12] fix: Move agent_portrayal to example-specific file --- examples/boltzmann_wealth_model/app.py | 13 ++++++++++++- experimental/jupyter_viz.py | 18 ++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/examples/boltzmann_wealth_model/app.py b/examples/boltzmann_wealth_model/app.py index b87da8fe..04f0b52c 100644 --- a/examples/boltzmann_wealth_model/app.py +++ b/examples/boltzmann_wealth_model/app.py @@ -2,6 +2,13 @@ from model import BoltzmannWealthModel + +def agent_portrayal(agent): + if agent.wealth > 0: + return 50 + return 10 + + model_params = { "N": { "type": "SliderInt", @@ -16,6 +23,10 @@ } page = JupyterViz( - BoltzmannWealthModel, model_params, measures=["Gini"], name="Money Model" + BoltzmannWealthModel, + model_params, + measures=["Gini"], + name="Money Model", + agent_portrayal=agent_portrayal, ) page diff --git a/experimental/jupyter_viz.py b/experimental/jupyter_viz.py index 799dc7bb..31d3aaef 100644 --- a/experimental/jupyter_viz.py +++ b/experimental/jupyter_viz.py @@ -10,18 +10,13 @@ plt.switch_backend("agg") -def agent_portrayal(agent): - if agent.wealth > 0: - return 50 - return 10 - - class JupyterContainer: - def __init__(self, model_class, model_params, measures=None, name="Mesa Model"): + def __init__(self, model_class, model_params, measures=None, name="Mesa Model", agent_portrayal=None): self.model_class = model_class self.split_model_params(model_params) self.measures = measures self.name = name + self.agent_portrayal = agent_portrayal self.thread = None def split_model_params(self, model_params): @@ -60,15 +55,14 @@ def do_pause(self): self.model.running = False self.thread.join() - @staticmethod - def portray(g): + def portray(self, g): x = [] y = [] s = [] for i in range(g.width): for j in range(g.height): for agent in g._grid[i][j]: - _s = agent_portrayal(agent) + _s = self.agent_portrayal(agent) x.append(i) y.append(j) s.append(_s) @@ -127,5 +121,5 @@ def make_model(): solara.FigureMatplotlib(fig, dependencies=[viz.model, viz.df]) -def JupyterViz(model_class, model_params, measures=None, name="Mesa Model"): - return MesaComponent(JupyterContainer(model_class, model_params, measures, name)) +def JupyterViz(model_class, model_params, measures=None, name="Mesa Model", agent_portrayal=None): + return MesaComponent(JupyterContainer(model_class, model_params, measures, name, agent_portrayal)) From 9244a81bf7f2d174f8606a77009e6c67a84cec04 Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 29 Jun 2023 03:21:15 -0400 Subject: [PATCH 04/12] feat: Support color in Jupyter viz --- examples/boltzmann_wealth_model/app.py | 7 +++++-- experimental/jupyter_viz.py | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/examples/boltzmann_wealth_model/app.py b/examples/boltzmann_wealth_model/app.py index 04f0b52c..0e263c92 100644 --- a/examples/boltzmann_wealth_model/app.py +++ b/examples/boltzmann_wealth_model/app.py @@ -4,9 +4,12 @@ def agent_portrayal(agent): + size = 10 + color = "tab:red" if agent.wealth > 0: - return 50 - return 10 + size = 50 + color = "tab:blue" + return {"size": size, "color": color} model_params = { diff --git a/experimental/jupyter_viz.py b/experimental/jupyter_viz.py index 31d3aaef..b4bfa8aa 100644 --- a/experimental/jupyter_viz.py +++ b/experimental/jupyter_viz.py @@ -59,14 +59,23 @@ def portray(self, g): x = [] y = [] s = [] + c = [] for i in range(g.width): for j in range(g.height): for agent in g._grid[i][j]: - _s = self.agent_portrayal(agent) + data = self.agent_portrayal(agent) x.append(i) y.append(j) - s.append(_s) - return {"x": x, "y": y, "s": s} + if "size" in data: + s.append(data["size"]) + if "color" in data: + c.append(data["color"]) + out = {"x": x, "y": y} + if len(s) > 0: + out["s"] = s + if len(c) > 0: + out["c"] = c + return out @solara.component From 02d7a3a95a989d09c4c6df8abeb089a8ea43d3ce Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 29 Jun 2023 03:45:10 -0400 Subject: [PATCH 05/12] Apply Black --- experimental/jupyter_viz.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/experimental/jupyter_viz.py b/experimental/jupyter_viz.py index b4bfa8aa..63d874f9 100644 --- a/experimental/jupyter_viz.py +++ b/experimental/jupyter_viz.py @@ -11,7 +11,14 @@ class JupyterContainer: - def __init__(self, model_class, model_params, measures=None, name="Mesa Model", agent_portrayal=None): + def __init__( + self, + model_class, + model_params, + measures=None, + name="Mesa Model", + agent_portrayal=None, + ): self.model_class = model_class self.split_model_params(model_params) self.measures = measures @@ -130,5 +137,9 @@ def make_model(): solara.FigureMatplotlib(fig, dependencies=[viz.model, viz.df]) -def JupyterViz(model_class, model_params, measures=None, name="Mesa Model", agent_portrayal=None): - return MesaComponent(JupyterContainer(model_class, model_params, measures, name, agent_portrayal)) +def JupyterViz( + model_class, model_params, measures=None, name="Mesa Model", agent_portrayal=None +): + return MesaComponent( + JupyterContainer(model_class, model_params, measures, name, agent_portrayal) + ) From 6730e523964fbefb883defdfb446c5cbc9d84d19 Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 29 Jun 2023 04:31:10 -0400 Subject: [PATCH 06/12] Update jupyter_viz.py to support Schelling --- experimental/jupyter_viz.py | 87 ++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/experimental/jupyter_viz.py b/experimental/jupyter_viz.py index 63d874f9..35133ae2 100644 --- a/experimental/jupyter_viz.py +++ b/experimental/jupyter_viz.py @@ -65,11 +65,17 @@ def do_pause(self): def portray(self, g): x = [] y = [] - s = [] - c = [] + s = [] # size + c = [] # color for i in range(g.width): for j in range(g.height): - for agent in g._grid[i][j]: + content = g._grid[i][j] + if not content: + continue + if not hasattr(content, "__iter__"): + # Is a single grid + content = [content] + for agent in content: data = self.agent_portrayal(agent) x.append(i) y.append(j) @@ -85,6 +91,43 @@ def portray(self, g): return out +def make_space(viz): + space_fig = Figure() + space_ax = space_fig.subplots() + space_ax.scatter(**viz.portray(viz.model.grid)) + space_ax.set_axis_off() + solara.FigureMatplotlib(space_fig, dependencies=[viz.model, viz.df]) + + +def make_plot(viz, measure): + fig = Figure() + ax = fig.subplots() + ax.plot(viz.df.loc[:, measure]) + ax.set_ylabel(measure) + # Set integer x axis + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + solara.FigureMatplotlib(fig, dependencies=[viz.model, viz.df]) + + +def make_user_input(user_input, k, v): + if v["type"] == "SliderInt": + solara.SliderInt( + v.get("label", "label"), + value=user_input, + min=v.get("min"), + max=v.get("max"), + step=v.get("step"), + ) + elif v["type"] == "SliderFloat": + solara.SliderFloat( + v.get("label", "label"), + value=user_input, + min=v.get("min"), + max=v.get("max"), + step=v.get("step"), + ) + + @solara.component def MesaComponent(viz): solara.Markdown(viz.name) @@ -92,16 +135,9 @@ def MesaComponent(viz): # 1. User inputs user_inputs = {} for k, v in viz.model_params_input.items(): - if v["type"] == "SliderInt": - user_input = solara.use_reactive(v["value"]) - user_inputs[k] = user_input.value - solara.SliderInt( - v.get("label", "label"), - value=user_input, - min=v.get("min", 0), - max=v.get("max", 10), - step=v.get("step", 1), - ) + user_input = solara.use_reactive(v["value"]) + user_inputs[k] = user_input.value + make_user_input(user_input, k, v) # 2. Model def make_model(): @@ -119,22 +155,15 @@ def make_model(): solara.Button(label="⏸︎", color="primary", on_click=viz.do_pause) # solara.Button(label="Reset", color="primary", on_click=do_reset) - # 3. Space - space_fig = Figure() - space_ax = space_fig.subplots() - space_ax.scatter(**viz.portray(viz.model.grid)) - space_ax.set_axis_off() - solara.FigureMatplotlib(space_fig, dependencies=[viz.model, viz.df]) - - # 4. Plots - for i, measure in enumerate(viz.measures): - fig = Figure() - ax = fig.subplots() - ax.plot(viz.df.loc[:, measure]) - ax.set_ylabel(measure) - # Set integer x axis - ax.xaxis.set_major_locator(MaxNLocator(integer=True)) - solara.FigureMatplotlib(fig, dependencies=[viz.model, viz.df]) + with solara.GridFixed(columns=2): + # 3. Space + make_space(viz) + # 4. Plots + for i, measure in enumerate(viz.measures): + if callable(measure): + solara.Markdown(measure(viz.model)) + else: + make_plot(viz, measure) def JupyterViz( From d560e8e8e5de63b5d30c52b42e6b6b2ce2209d54 Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 29 Jun 2023 04:38:09 -0400 Subject: [PATCH 07/12] schelling: Switch to Jupyter viz --- examples/schelling/README.md | 9 +++--- examples/schelling/app.py | 54 ++++++++++++++++++++++++++++++++++++ examples/schelling/run.py | 3 -- examples/schelling/server.py | 45 ------------------------------ 4 files changed, 58 insertions(+), 53 deletions(-) create mode 100644 examples/schelling/app.py delete mode 100644 examples/schelling/run.py delete mode 100644 examples/schelling/server.py diff --git a/examples/schelling/README.md b/examples/schelling/README.md index 64cc9c83..b0116b55 100644 --- a/examples/schelling/README.md +++ b/examples/schelling/README.md @@ -16,13 +16,13 @@ To install the dependencies use pip and the requirements.txt in this directory. ## How to Run -To run the model interactively, run ``mesa runserver`` in this directory. e.g. +To run the model interactively, in this directory, run the following command ``` - $ mesa runserver + $ solara run app.py ``` -Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. +Then open your browser to [http://127.0.0.1:8765/](http://127.0.0.1:8765/) and click the Play button. To view and run some example model analyses, launch the IPython Notebook and open ``analysis.ipynb``. Visualizing the analysis also requires [matplotlib](http://matplotlib.org/). @@ -32,10 +32,9 @@ To run the model with the grid displayed as an ASCII text, run `python run_ascii ## Files -* ``run.py``: Launches a model visualization server. +* ``app.py``: Code for the interactive visualization. * ``run_ascii.py``: Run the model in text mode. * ``schelling.py``: Contains the agent class, and the overall model class. -* ``server.py``: Defines classes for visualizing the model in the browser via Mesa's modular server, and instantiates a visualization server. * ``analysis.ipynb``: Notebook demonstrating how to run experiments and parameter sweeps on the model. ## Further Reading diff --git a/examples/schelling/app.py b/examples/schelling/app.py new file mode 100644 index 00000000..8f2e242a --- /dev/null +++ b/examples/schelling/app.py @@ -0,0 +1,54 @@ +from mesa_models.experimental import JupyterViz + +from model import Schelling + + +def get_happy_agents(model): + """ + Display a text count of how many happy agents there are. + """ + return f"Happy agents: {model.happy}" + + +def agent_portrayal(agent): + color = "tab:orange" if agent.type == 0 else "tab:blue" + return {"color": color} + + +model_params = { + "density": { + "type": "SliderFloat", + "value": 0.8, + "label": "Agent density", + "min": 0.1, + "max": 1.0, + "step": 0.1, + }, + "minority_pc": { + "type": "SliderFloat", + "value": 0.2, + "label": "Fraction minority", + "min": 0.0, + "max": 1.0, + "step": 0.05, + }, + "homophily": { + "type": "SliderInt", + "value": 3, + "label": "Homophily", + "min": 0, + "max": 8, + "step": 1, + }, + "width": 20, + "height": 20, +} + +page = JupyterViz( + Schelling, + model_params, + measures=["happy", get_happy_agents], + name="Schelling", + agent_portrayal=agent_portrayal, +) +page diff --git a/examples/schelling/run.py b/examples/schelling/run.py deleted file mode 100644 index f20cebcb..00000000 --- a/examples/schelling/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from server import server - -server.launch(open_browser=True) diff --git a/examples/schelling/server.py b/examples/schelling/server.py deleted file mode 100644 index 1396e9c7..00000000 --- a/examples/schelling/server.py +++ /dev/null @@ -1,45 +0,0 @@ -import mesa -from model import Schelling - - -def get_happy_agents(model): - """ - Display a text count of how many happy agents there are. - """ - return f"Happy agents: {model.happy}" - - -def schelling_draw(agent): - """ - Portrayal Method for canvas - """ - if agent is None: - return - portrayal = {"Shape": "circle", "r": 0.5, "Filled": "true", "Layer": 0} - - if agent.type == 0: - portrayal["Color"] = ["#FF0000", "#FF9999"] - portrayal["stroke_color"] = "#00FF00" - else: - portrayal["Color"] = ["#0000FF", "#9999FF"] - portrayal["stroke_color"] = "#000000" - return portrayal - - -canvas_element = mesa.visualization.CanvasGrid(schelling_draw, 20, 20, 500, 500) -happy_chart = mesa.visualization.ChartModule([{"Label": "happy", "Color": "Black"}]) - -model_params = { - "height": 20, - "width": 20, - "density": mesa.visualization.Slider("Agent density", 0.8, 0.1, 1.0, 0.1), - "minority_pc": mesa.visualization.Slider("Fraction minority", 0.2, 0.00, 1.0, 0.05), - "homophily": mesa.visualization.Slider("Homophily", 3, 0, 8, 1), -} - -server = mesa.visualization.ModularServer( - Schelling, - [canvas_element, get_happy_agents, happy_chart], - "Schelling", - model_params, -) From 16fca359e017fc335e31e45861065a1bf17d2516 Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 29 Jun 2023 06:17:03 -0400 Subject: [PATCH 08/12] Fix Ruff errors --- examples/boltzmann_wealth_model/app.py | 3 +-- examples/schelling/app.py | 3 +-- experimental/jupyter_viz.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/examples/boltzmann_wealth_model/app.py b/examples/boltzmann_wealth_model/app.py index 0e263c92..0fea7577 100644 --- a/examples/boltzmann_wealth_model/app.py +++ b/examples/boltzmann_wealth_model/app.py @@ -1,5 +1,4 @@ from mesa_models.experimental import JupyterViz - from model import BoltzmannWealthModel @@ -32,4 +31,4 @@ def agent_portrayal(agent): name="Money Model", agent_portrayal=agent_portrayal, ) -page +page # noqa diff --git a/examples/schelling/app.py b/examples/schelling/app.py index 8f2e242a..9db977a7 100644 --- a/examples/schelling/app.py +++ b/examples/schelling/app.py @@ -1,5 +1,4 @@ from mesa_models.experimental import JupyterViz - from model import Schelling @@ -51,4 +50,4 @@ def agent_portrayal(agent): name="Schelling", agent_portrayal=agent_portrayal, ) -page +page # noqa diff --git a/experimental/jupyter_viz.py b/experimental/jupyter_viz.py index 35133ae2..32352a3f 100644 --- a/experimental/jupyter_viz.py +++ b/experimental/jupyter_viz.py @@ -1,11 +1,10 @@ import threading -import solara import matplotlib.pyplot as plt +import solara from matplotlib.figure import Figure from matplotlib.ticker import MaxNLocator - # Avoid interactive backend plt.switch_backend("agg") From bd227e4a5419919130cd142259a58130f68c9746 Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 29 Jun 2023 06:21:30 -0400 Subject: [PATCH 09/12] schelling: Add solara as requirement --- examples/schelling/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/schelling/requirements.txt b/examples/schelling/requirements.txt index 19b805ac..8768b893 100644 --- a/examples/schelling/requirements.txt +++ b/examples/schelling/requirements.txt @@ -1,3 +1,4 @@ jupyter matplotlib mesa~=1.1 +solara From b9559d828afd8d9e24c3ca902f8199cb883a6a0f Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 29 Jun 2023 06:37:43 -0400 Subject: [PATCH 10/12] Add mesa_models to requirements.txt --- examples/boltzmann_wealth_model/requirements.txt | 1 + examples/schelling/requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/examples/boltzmann_wealth_model/requirements.txt b/examples/boltzmann_wealth_model/requirements.txt index 4d93614c..cd191a90 100644 --- a/examples/boltzmann_wealth_model/requirements.txt +++ b/examples/boltzmann_wealth_model/requirements.txt @@ -1,2 +1,3 @@ mesa~=1.1 solara +git+https://github.com/projectmesa/mesa-examples diff --git a/examples/schelling/requirements.txt b/examples/schelling/requirements.txt index 8768b893..2d40f057 100644 --- a/examples/schelling/requirements.txt +++ b/examples/schelling/requirements.txt @@ -2,3 +2,4 @@ jupyter matplotlib mesa~=1.1 solara +git+https://github.com/projectmesa/mesa-examples From 1d5ad3a5f449f99b1a11d480715057a894e08634 Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 29 Jun 2023 06:42:05 -0400 Subject: [PATCH 11/12] Make separate copies for experimental --- .../Readme.md | 0 .../__init__.py | 0 .../app.py | 0 .../model.py | 0 .../requirements.txt | 0 examples/{schelling => schelling_experimental}/README.md | 0 examples/{schelling => schelling_experimental}/__init__.py | 0 examples/{schelling => schelling_experimental}/analysis.ipynb | 0 examples/{schelling => schelling_experimental}/app.py | 0 examples/{schelling => schelling_experimental}/model.py | 0 examples/{schelling => schelling_experimental}/requirements.txt | 0 examples/{schelling => schelling_experimental}/run_ascii.py | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename examples/{boltzmann_wealth_model => boltzmann_wealth_model_experimental}/Readme.md (100%) rename examples/{boltzmann_wealth_model => boltzmann_wealth_model_experimental}/__init__.py (100%) rename examples/{boltzmann_wealth_model => boltzmann_wealth_model_experimental}/app.py (100%) rename examples/{boltzmann_wealth_model => boltzmann_wealth_model_experimental}/model.py (100%) rename examples/{boltzmann_wealth_model => boltzmann_wealth_model_experimental}/requirements.txt (100%) rename examples/{schelling => schelling_experimental}/README.md (100%) rename examples/{schelling => schelling_experimental}/__init__.py (100%) rename examples/{schelling => schelling_experimental}/analysis.ipynb (100%) rename examples/{schelling => schelling_experimental}/app.py (100%) rename examples/{schelling => schelling_experimental}/model.py (100%) rename examples/{schelling => schelling_experimental}/requirements.txt (100%) rename examples/{schelling => schelling_experimental}/run_ascii.py (100%) diff --git a/examples/boltzmann_wealth_model/Readme.md b/examples/boltzmann_wealth_model_experimental/Readme.md similarity index 100% rename from examples/boltzmann_wealth_model/Readme.md rename to examples/boltzmann_wealth_model_experimental/Readme.md diff --git a/examples/boltzmann_wealth_model/__init__.py b/examples/boltzmann_wealth_model_experimental/__init__.py similarity index 100% rename from examples/boltzmann_wealth_model/__init__.py rename to examples/boltzmann_wealth_model_experimental/__init__.py diff --git a/examples/boltzmann_wealth_model/app.py b/examples/boltzmann_wealth_model_experimental/app.py similarity index 100% rename from examples/boltzmann_wealth_model/app.py rename to examples/boltzmann_wealth_model_experimental/app.py diff --git a/examples/boltzmann_wealth_model/model.py b/examples/boltzmann_wealth_model_experimental/model.py similarity index 100% rename from examples/boltzmann_wealth_model/model.py rename to examples/boltzmann_wealth_model_experimental/model.py diff --git a/examples/boltzmann_wealth_model/requirements.txt b/examples/boltzmann_wealth_model_experimental/requirements.txt similarity index 100% rename from examples/boltzmann_wealth_model/requirements.txt rename to examples/boltzmann_wealth_model_experimental/requirements.txt diff --git a/examples/schelling/README.md b/examples/schelling_experimental/README.md similarity index 100% rename from examples/schelling/README.md rename to examples/schelling_experimental/README.md diff --git a/examples/schelling/__init__.py b/examples/schelling_experimental/__init__.py similarity index 100% rename from examples/schelling/__init__.py rename to examples/schelling_experimental/__init__.py diff --git a/examples/schelling/analysis.ipynb b/examples/schelling_experimental/analysis.ipynb similarity index 100% rename from examples/schelling/analysis.ipynb rename to examples/schelling_experimental/analysis.ipynb diff --git a/examples/schelling/app.py b/examples/schelling_experimental/app.py similarity index 100% rename from examples/schelling/app.py rename to examples/schelling_experimental/app.py diff --git a/examples/schelling/model.py b/examples/schelling_experimental/model.py similarity index 100% rename from examples/schelling/model.py rename to examples/schelling_experimental/model.py diff --git a/examples/schelling/requirements.txt b/examples/schelling_experimental/requirements.txt similarity index 100% rename from examples/schelling/requirements.txt rename to examples/schelling_experimental/requirements.txt diff --git a/examples/schelling/run_ascii.py b/examples/schelling_experimental/run_ascii.py similarity index 100% rename from examples/schelling/run_ascii.py rename to examples/schelling_experimental/run_ascii.py From 69a9aaf54ff02ed7ec514fe69799a27a8fd96095 Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 29 Jun 2023 06:43:15 -0400 Subject: [PATCH 12/12] Add back original boltzmann and schelling --- examples/boltzmann_wealth_model/Readme.md | 51 ++ examples/boltzmann_wealth_model/app.py | 113 +++++ .../boltzmann_wealth_model/__init__.py | 0 .../boltzmann_wealth_model/model.py | 76 +++ .../boltzmann_wealth_model/server.py | 40 ++ .../boltzmann_wealth_model/requirements.txt | 1 + examples/boltzmann_wealth_model/run.py | 3 + examples/schelling/README.md | 49 ++ examples/schelling/__init__.py | 0 examples/schelling/analysis.ipynb | 457 ++++++++++++++++++ examples/schelling/model.py | 86 ++++ examples/schelling/requirements.txt | 3 + examples/schelling/run.py | 3 + examples/schelling/run_ascii.py | 48 ++ examples/schelling/server.py | 45 ++ setup.cfg | 2 +- 16 files changed, 976 insertions(+), 1 deletion(-) create mode 100644 examples/boltzmann_wealth_model/Readme.md create mode 100644 examples/boltzmann_wealth_model/app.py create mode 100644 examples/boltzmann_wealth_model/boltzmann_wealth_model/__init__.py create mode 100644 examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py create mode 100644 examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py create mode 100644 examples/boltzmann_wealth_model/requirements.txt create mode 100644 examples/boltzmann_wealth_model/run.py create mode 100644 examples/schelling/README.md create mode 100644 examples/schelling/__init__.py create mode 100644 examples/schelling/analysis.ipynb create mode 100644 examples/schelling/model.py create mode 100644 examples/schelling/requirements.txt create mode 100644 examples/schelling/run.py create mode 100644 examples/schelling/run_ascii.py create mode 100644 examples/schelling/server.py diff --git a/examples/boltzmann_wealth_model/Readme.md b/examples/boltzmann_wealth_model/Readme.md new file mode 100644 index 00000000..4a6e21f1 --- /dev/null +++ b/examples/boltzmann_wealth_model/Readme.md @@ -0,0 +1,51 @@ +# Boltzmann Wealth Model (Tutorial) + +## Summary + +A simple model of agents exchanging wealth. All agents start with the same amount of money. Every step, each agent with one unit of money or more gives one unit of wealth to another random agent. This is the model described in the [Intro Tutorial](https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html), with the completed code. + +If you want to go over the step-by-step tutorial, please go and run the [Jupyter Notebook](https://github.com/projectmesa/mesa/blob/main/docs/tutorials/intro_tutorial.ipynb). The code here runs the finalized code in the last cells directly. + +As the model runs, the distribution of wealth among agents goes from being perfectly uniform (all agents have the same starting wealth), to highly skewed -- a small number have high wealth, more have none at all. + +## How to Run + +To follow the tutorial example, launch the Jupyter Notebook and run the code in ``Introduction to Mesa Tutorial Code.ipynb`` which you can find in the main mesa repo [here](https://github.com/projectmesa/mesa/blob/main/docs/tutorials/intro_tutorial.ipynb) + +To launch the interactive server, as described in the [last section of the tutorial](https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html#adding-visualization), run: + +``` + $ python server.py +``` + +Make sure to install the requirements first: + +``` + pip install -r requirements.txt +``` + +If your browser doesn't open automatically, point it to [http://127.0.0.1:8521/](http://127.0.0.1:8521/). When the visualization loads, press Reset, then Run. + + +## Files + +* ``boltzmann_wealth_model/model.py``: Final version of the model. +* ``boltzmann_wealth_model/server.py``: Code for the interactive visualization. +* ``run.py``: Launches the server. + +## Optional + +* ``boltzmann_wealth_model/app.py``: can be used to run the simulation via the streamlit interface. +* For this some additional packages like ``streamlit`` and ``altair`` needs to be installed. +* Once installed, the app can be opened in the browser using : ``streamlit run app.py`` + +## Further Reading + +The full tutorial describing how the model is built can be found at: +https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html + +This model is drawn from econophysics and presents a statistical mechanics approach to wealth distribution. Some examples of further reading on the topic can be found at: + +[Milakovic, M. A Statistical Equilibrium Model of Wealth Distribution. February, 2001.](https://editorialexpress.com/cgi-bin/conference/download.cgi?db_name=SCE2001&paper_id=214) + +[Dragulescu, A and Yakovenko, V. Statistical Mechanics of Money, Income, and Wealth: A Short Survey. November, 2002](http://arxiv.org/pdf/cond-mat/0211175v1.pdf) diff --git a/examples/boltzmann_wealth_model/app.py b/examples/boltzmann_wealth_model/app.py new file mode 100644 index 00000000..f2dd6da9 --- /dev/null +++ b/examples/boltzmann_wealth_model/app.py @@ -0,0 +1,113 @@ +import time + +import altair as alt +import pandas as pd +import streamlit as st +from boltzmann_wealth_model.model import BoltzmannWealthModel + +model = st.title("Boltzman Wealth Model") +num_agents = st.slider( + "Choose how many agents to include in the model", + min_value=1, + max_value=100, + value=50, +) +num_ticks = st.slider( + "Select number of Simulation Runs", min_value=1, max_value=100, value=50 +) +height = st.slider("Select Grid Height", min_value=10, max_value=100, step=10, value=15) +width = st.slider("Select Grid Width", min_value=10, max_value=100, step=10, value=20) +model = BoltzmannWealthModel(num_agents, height, width) + + +status_text = st.empty() +run = st.button("Run Simulation") + + +if run: + tick = time.time() + step = 0 + # init grid + df_grid = pd.DataFrame() + df_gini = pd.DataFrame({"step": [0], "gini": [-1]}) + for x in range(width): + for y in range(height): + df_grid = pd.concat( + [df_grid, pd.DataFrame({"x": [x], "y": [y], "agent_count": 0})], + ignore_index=True, + ) + + heatmap = ( + alt.Chart(df_grid) + .mark_point(size=100) + .encode(x="x", y="y", color=alt.Color("agent_count")) + .interactive() + .properties(width=800, height=600) + ) + + line = ( + alt.Chart(df_gini) + .mark_line(point=True) + .encode(x="step", y="gini") + .properties(width=800, height=600) + ) + + # init progress bar + my_bar = st.progress(0, text="Simulation Progress") # progress + placeholder = st.empty() + st.subheader("Agent Grid") + chart = st.altair_chart(heatmap) + st.subheader("Gini Values") + line_chart = st.altair_chart(line) + + color_scale = alt.Scale( + domain=[0, 1, 2, 3, 4], range=["red", "cyan", "white", "white", "blue"] + ) + for i in range(num_ticks): + model.step() + my_bar.progress((i / num_ticks), text="Simulation progress") + placeholder.text("Step = %d" % i) + for cell in model.grid.coord_iter(): + cell_content, x, y = cell + agent_count = len(cell_content) + selected_row = df_grid[(df_grid["x"] == x) & (df_grid["y"] == y)] + df_grid.loc[ + selected_row.index, "agent_count" + ] = agent_count # random.choice([1,2]) + + df_gini = pd.concat( + [ + df_gini, + pd.DataFrame( + {"step": [i], "gini": [model.datacollector.model_vars["Gini"][i]]} + ), + ] + ) + # st.table(df_grid) + heatmap = ( + alt.Chart(df_grid) + .mark_circle(size=100) + .encode(x="x", y="y", color=alt.Color("agent_count", scale=color_scale)) + .interactive() + .properties(width=800, height=600) + ) + chart.altair_chart(heatmap) + + line = ( + alt.Chart(df_gini) + .mark_line(point=True) + .encode(x="step", y="gini") + .properties(width=800, height=600) + ) + line_chart.altair_chart(line) + + time.sleep(0.01) + + tock = time.time() + st.success(f"Simulation completed in {tock - tick:.2f} secs") + + # st.subheader('Agent Grid') + # fig = px.imshow(agent_counts,labels={'color':'Agent Count'}) + # st.plotly_chart(fig) + # st.subheader('Gini value over sim ticks (Plotly)') + # chart = st.line_chart(model.datacollector.model_vars['Gini']) diff --git a/examples/boltzmann_wealth_model/boltzmann_wealth_model/__init__.py b/examples/boltzmann_wealth_model/boltzmann_wealth_model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py b/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py new file mode 100644 index 00000000..0f61b883 --- /dev/null +++ b/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py @@ -0,0 +1,76 @@ +import mesa + + +def compute_gini(model): + agent_wealths = [agent.wealth for agent in model.schedule.agents] + x = sorted(agent_wealths) + N = model.num_agents + B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x)) + return 1 + (1 / N) - 2 * B + + +class BoltzmannWealthModel(mesa.Model): + """A simple model of an economy where agents exchange currency at random. + + All the agents begin with one unit of currency, and each time step can give + a unit of currency to another agent. Note how, over time, this produces a + highly skewed distribution of wealth. + """ + + def __init__(self, N=100, width=10, height=10): + self.num_agents = N + self.grid = mesa.space.MultiGrid(width, height, True) + self.schedule = mesa.time.RandomActivation(self) + self.datacollector = mesa.DataCollector( + model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"} + ) + # Create agents + for i in range(self.num_agents): + a = MoneyAgent(i, self) + self.schedule.add(a) + # 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)) + + self.running = True + self.datacollector.collect(self) + + def step(self): + self.schedule.step() + # collect data + self.datacollector.collect(self) + + def run_model(self, n): + for i in range(n): + self.step() + + +class MoneyAgent(mesa.Agent): + """An agent with fixed initial wealth.""" + + def __init__(self, unique_id, model): + super().__init__(unique_id, model) + self.wealth = 1 + + def move(self): + 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): + cellmates = self.model.grid.get_cell_list_contents([self.pos]) + cellmates.pop( + cellmates.index(self) + ) # Ensure agent is not giving money to itself + if len(cellmates) > 0: + other = self.random.choice(cellmates) + other.wealth += 1 + self.wealth -= 1 + + def step(self): + self.move() + if self.wealth > 0: + self.give_money() diff --git a/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py b/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py new file mode 100644 index 00000000..a49546ce --- /dev/null +++ b/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py @@ -0,0 +1,40 @@ +import mesa + +from .model import BoltzmannWealthModel + + +def agent_portrayal(agent): + portrayal = {"Shape": "circle", "Filled": "true", "r": 0.5} + + if agent.wealth > 0: + portrayal["Color"] = "red" + portrayal["Layer"] = 0 + else: + portrayal["Color"] = "grey" + portrayal["Layer"] = 1 + portrayal["r"] = 0.2 + return portrayal + + +grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500) +chart = mesa.visualization.ChartModule( + [{"Label": "Gini", "Color": "#0000FF"}], data_collector_name="datacollector" +) + +model_params = { + "N": mesa.visualization.Slider( + "Number of agents", + 100, + 2, + 200, + 1, + description="Choose how many agents to include in the model", + ), + "width": 10, + "height": 10, +} + +server = mesa.visualization.ModularServer( + BoltzmannWealthModel, [grid, chart], "Money Model", model_params +) +server.port = 8521 diff --git a/examples/boltzmann_wealth_model/requirements.txt b/examples/boltzmann_wealth_model/requirements.txt new file mode 100644 index 00000000..63b0d24e --- /dev/null +++ b/examples/boltzmann_wealth_model/requirements.txt @@ -0,0 +1 @@ +mesa~=1.1 diff --git a/examples/boltzmann_wealth_model/run.py b/examples/boltzmann_wealth_model/run.py new file mode 100644 index 00000000..f1767593 --- /dev/null +++ b/examples/boltzmann_wealth_model/run.py @@ -0,0 +1,3 @@ +from boltzmann_wealth_model.server import server + +server.launch(open_browser=True) diff --git a/examples/schelling/README.md b/examples/schelling/README.md new file mode 100644 index 00000000..64cc9c83 --- /dev/null +++ b/examples/schelling/README.md @@ -0,0 +1,49 @@ +# Schelling Segregation Model + +## Summary + +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. + +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. + +## Installation + +To install the dependencies use pip and the requirements.txt in this directory. e.g. + +``` + $ pip install -r requirements.txt +``` + +## How to Run + +To run the model interactively, run ``mesa runserver`` in this directory. e.g. + +``` + $ mesa runserver +``` + +Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. + +To view and run some example model analyses, launch the IPython Notebook and open ``analysis.ipynb``. Visualizing the analysis also requires [matplotlib](http://matplotlib.org/). + +## How to Run without the GUI + +To run the model with the grid displayed as an ASCII text, run `python run_ascii.py` in this directory. + +## Files + +* ``run.py``: Launches a model visualization server. +* ``run_ascii.py``: Run the model in text mode. +* ``schelling.py``: Contains the agent class, and the overall model class. +* ``server.py``: Defines classes for visualizing the model in the browser via Mesa's modular server, and instantiates a visualization server. +* ``analysis.ipynb``: Notebook demonstrating how to run experiments and parameter sweeps on the model. + +## Further Reading + +Schelling's original paper describing the model: + +[Schelling, Thomas C. Dynamic Models of Segregation. Journal of Mathematical Sociology. 1971, Vol. 1, pp 143-186.](https://www.stat.berkeley.edu/~aldous/157/Papers/Schelling_Seg_Models.pdf) + +An interactive, browser-based explanation and implementation: + +[Parable of the Polygons](http://ncase.me/polygons/), by Vi Hart and Nicky Case. diff --git a/examples/schelling/__init__.py b/examples/schelling/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/schelling/analysis.ipynb b/examples/schelling/analysis.ipynb new file mode 100644 index 00000000..50f382c6 --- /dev/null +++ b/examples/schelling/analysis.ipynb @@ -0,0 +1,457 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Schelling Segregation Model\n", + "\n", + "## Background\n", + "\n", + "The Schelling (1971) segregation model is a classic of agent-based modeling, demonstrating how agents following simple rules lead to the emergence of qualitatively different macro-level outcomes. Agents are randomly placed on a grid. There are two types of agents, one constituting the majority and the other the minority. All agents want a certain number (generally, 3) of their 8 surrounding neighbors to be of the same type in order for them to be happy. Unhappy agents will move to a random available grid space. While individual agents do not have a preference for a segregated outcome (e.g. they would be happy with 3 similar neighbors and 5 different ones), the aggregate outcome is nevertheless heavily segregated.\n", + "\n", + "## Implementation\n", + "\n", + "This is a demonstration of running a Mesa model in an IPython Notebook. The actual model and agent code are implemented in Schelling.py, in the same directory as this notebook. Below, we will import the model class, instantiate it, run it, and plot the time series of the number of happy agents." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "%matplotlib inline\n", + "\n", + "from model import Schelling" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we instantiate a model instance: a 10x10 grid, with an 80% change of an agent being placed in each cell, approximately 20% of agents set as minorities, and agents wanting at least 3 similar neighbors." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "model = Schelling(10, 10, 0.8, 0.2, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We want to run the model until all the agents are happy with where they are. However, there's no guarantee that a given model instantiation will *ever* settle down. So let's run it for either 100 steps or until it stops on its own, whichever comes first:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "100\n" + ] + } + ], + "source": [ + "while model.running and model.schedule.steps < 100:\n", + " model.step()\n", + "print(model.schedule.steps) # Show how many steps have actually run" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The model has a DataCollector object, which checks and stores how many agents are happy at the end of each step. It can also generate a pandas DataFrame of the data it has collected:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "model_out = model.datacollector.get_model_vars_dataframe()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
happy
00
173
267
372
472
\n", + "
" + ], + "text/plain": [ + " happy\n", + "0 0\n", + "1 73\n", + "2 72\n", + "3 73\n", + "4 72" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model_out.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we can plot the 'happy' series:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "model_out.happy.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For testing purposes, here is a table giving each agent's x and y values at each step." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "x_positions = model.datacollector.get_agent_vars_dataframe()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
xy
StepAgentID
0(0, 0)01
(0, 1)89
(0, 2)52
(0, 3)00
(0, 4)17
\n", + "
" + ], + "text/plain": [ + " x y\n", + "Step AgentID \n", + "0 (0, 0) 0 1\n", + " (0, 1) 8 9\n", + " (0, 2) 5 2\n", + " (0, 3) 0 0\n", + " (0, 4) 1 7" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x_positions.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Effect of Homophily on segregation\n", + "\n", + "Now, we can do a parameter sweep to see how segregation changes with homophily.\n", + "\n", + "First, we create a function which takes a model instance and returns what fraction of agents are segregated -- that is, have no neighbors of the opposite type." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from mesa.batchrunner import BatchRunner" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "def get_segregation(model):\n", + " \"\"\"\n", + " Find the % of agents that only have neighbors of their same type.\n", + " \"\"\"\n", + " segregated_agents = 0\n", + " for agent in model.schedule.agents:\n", + " segregated = True\n", + " for neighbor in model.grid.iter_neighbors(agent.pos, True):\n", + " if neighbor.type != agent.type:\n", + " segregated = False\n", + " break\n", + " if segregated:\n", + " segregated_agents += 1\n", + " return segregated_agents / model.schedule.get_agent_count()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we set up the batch run, with a dictionary of fixed and changing parameters. Let's hold everything fixed except for Homophily." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "fixed_params = {\"height\": 10, \"width\": 10, \"density\": 0.8, \"minority_pc\": 0.2}\n", + "variable_parms = {\"homophily\": range(1, 9)}" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "model_reporters = {\"Segregated_Agents\": get_segregation}" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "param_sweep = BatchRunner(\n", + " Schelling,\n", + " variable_parameters=variable_parms,\n", + " fixed_parameters=fixed_params,\n", + " iterations=10,\n", + " max_steps=200,\n", + " model_reporters=model_reporters,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "80it [00:15, 3.13it/s]\n" + ] + } + ], + "source": [ + "param_sweep.run_all()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "df = param_sweep.get_model_vars_dataframe()" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.scatter(df.homophily, df.Segregated_Agents)\n", + "plt.grid(True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.9" + }, + "widgets": { + "state": {}, + "version": "1.1.2" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/examples/schelling/model.py b/examples/schelling/model.py new file mode 100644 index 00000000..ccc5699e --- /dev/null +++ b/examples/schelling/model.py @@ -0,0 +1,86 @@ +import mesa + + +class SchellingAgent(mesa.Agent): + """ + Schelling segregation agent + """ + + def __init__(self, pos, model, agent_type): + """ + Create a new Schelling agent. + + Args: + unique_id: Unique identifier for the agent. + x, y: Agent initial location. + agent_type: Indicator for the agent's type (minority=1, majority=0) + """ + super().__init__(pos, model) + self.pos = pos + self.type = agent_type + + def step(self): + similar = 0 + for neighbor in self.model.grid.iter_neighbors(self.pos, True): + if neighbor.type == self.type: + similar += 1 + + # If unhappy, move: + if similar < self.model.homophily: + self.model.grid.move_to_empty(self) + else: + self.model.happy += 1 + + +class Schelling(mesa.Model): + """ + Model class for the Schelling segregation model. + """ + + def __init__(self, width=20, height=20, density=0.8, minority_pc=0.2, homophily=3): + """ """ + + self.width = width + self.height = height + self.density = density + self.minority_pc = minority_pc + self.homophily = homophily + + self.schedule = mesa.time.RandomActivation(self) + self.grid = mesa.space.SingleGrid(width, height, torus=True) + + self.happy = 0 + self.datacollector = mesa.DataCollector( + {"happy": "happy"}, # Model-level count of happy agents + # For testing purposes, agent's individual x and y + {"x": lambda a: a.pos[0], "y": lambda a: a.pos[1]}, + ) + + # Set up agents + # We use a grid iterator that returns + # the coordinates of a cell as well as + # its contents. (coord_iter) + for cell in self.grid.coord_iter(): + x = cell[1] + y = cell[2] + if self.random.random() < self.density: + agent_type = 1 if self.random.random() < self.minority_pc else 0 + + agent = SchellingAgent((x, y), self, agent_type) + self.grid.place_agent(agent, (x, y)) + self.schedule.add(agent) + + self.running = True + self.datacollector.collect(self) + + def step(self): + """ + Run one step of the model. If All agents are happy, halt the model. + """ + self.happy = 0 # Reset counter of happy agents + self.schedule.step() + # collect data + self.datacollector.collect(self) + + if self.happy == self.schedule.get_agent_count(): + self.running = False diff --git a/examples/schelling/requirements.txt b/examples/schelling/requirements.txt new file mode 100644 index 00000000..19b805ac --- /dev/null +++ b/examples/schelling/requirements.txt @@ -0,0 +1,3 @@ +jupyter +matplotlib +mesa~=1.1 diff --git a/examples/schelling/run.py b/examples/schelling/run.py new file mode 100644 index 00000000..f20cebcb --- /dev/null +++ b/examples/schelling/run.py @@ -0,0 +1,3 @@ +from server import server + +server.launch(open_browser=True) diff --git a/examples/schelling/run_ascii.py b/examples/schelling/run_ascii.py new file mode 100644 index 00000000..460fabbb --- /dev/null +++ b/examples/schelling/run_ascii.py @@ -0,0 +1,48 @@ +import mesa +from model import Schelling + + +class SchellingTextVisualization(mesa.visualization.TextVisualization): + """ + ASCII visualization for schelling model + """ + + def __init__(self, model): + """ + Create new Schelling ASCII visualization. + """ + self.model = model + + grid_viz = mesa.visualization.TextGrid(self.model.grid, self.print_ascii_agent) + happy_viz = mesa.visualization.TextData(self.model, "happy") + self.elements = [grid_viz, happy_viz] + + @staticmethod + def print_ascii_agent(a): + """ + Minority agents are X, Majority are O. + """ + if a.type == 0: + return "O" + if a.type == 1: + return "X" + + +if __name__ == "__main__": + model_params = { + "height": 20, + "width": 20, + # Agent density, from 0.8 to 1.0 + "density": 0.8, + # Fraction minority, from 0.2 to 1.0 + "minority_pc": 0.2, + # Homophily, from 3 to 8 + "homophily": 3, + } + + model = Schelling(**model_params) + viz = SchellingTextVisualization(model) + for i in range(10): + print("Step:", i) + viz.step() + print("---") diff --git a/examples/schelling/server.py b/examples/schelling/server.py new file mode 100644 index 00000000..1396e9c7 --- /dev/null +++ b/examples/schelling/server.py @@ -0,0 +1,45 @@ +import mesa +from model import Schelling + + +def get_happy_agents(model): + """ + Display a text count of how many happy agents there are. + """ + return f"Happy agents: {model.happy}" + + +def schelling_draw(agent): + """ + Portrayal Method for canvas + """ + if agent is None: + return + portrayal = {"Shape": "circle", "r": 0.5, "Filled": "true", "Layer": 0} + + if agent.type == 0: + portrayal["Color"] = ["#FF0000", "#FF9999"] + portrayal["stroke_color"] = "#00FF00" + else: + portrayal["Color"] = ["#0000FF", "#9999FF"] + portrayal["stroke_color"] = "#000000" + return portrayal + + +canvas_element = mesa.visualization.CanvasGrid(schelling_draw, 20, 20, 500, 500) +happy_chart = mesa.visualization.ChartModule([{"Label": "happy", "Color": "Black"}]) + +model_params = { + "height": 20, + "width": 20, + "density": mesa.visualization.Slider("Agent density", 0.8, 0.1, 1.0, 0.1), + "minority_pc": mesa.visualization.Slider("Fraction minority", 0.2, 0.00, 1.0, 0.05), + "homophily": mesa.visualization.Slider("Homophily", 3, 0, 8, 1), +} + +server = mesa.visualization.ModularServer( + Schelling, + [canvas_element, get_happy_agents, happy_chart], + "Schelling", + model_params, +) diff --git a/setup.cfg b/setup.cfg index fefa5a4b..a478f8af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [options] package_dir = - mesa_models.boltzmann_wealth_model = examples/boltzmann_wealth_model + mesa_models.boltzmann_wealth_model = examples/boltzmann_wealth_model/boltzmann_wealth_model mesa_models.schelling = examples/schelling mesa_models.epstein_civil_violence = examples/epstein_civil_violence/epstein_civil_violence mesa_models.wolf_sheep = examples/wolf_sheep/wolf_sheep