{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Visualization - Advanced Space Rendering\n", "\n", "### The Boltzmann Wealth Model " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you want to get straight to the tutorial checkout these environment providers:
\n", "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/projectmesa/mesa/main?labpath=docs%2Ftutorials%2F6_visualization_rendering_with_space_renderer.ipynb) (This can take 30 seconds to 5 minutes to load)\n", "\n", "Due to conflict with Colab and Solara there are no colab links for this tutorial\n", "\n", "*If you are running locally, please ensure you have the latest Mesa version installed.*\n", "\n", "## Tutorial Description\n", "This tutorial builds upon the [Dynamic Agent Representation](https://mesa.readthedocs.io/latest/tutorials/5_visualization_dynamic_agents.html) tutorial. We will explore more advanced features of the SpaceRenderer to create more informative and visually appealing spatial visualizations.\n", "\n", "Specifically, we'll learn how to style the grid lines.\n", "\n", "*If you are starting here please see the [Running Your First Model tutorial](https://mesa.readthedocs.io/latest/tutorials/0_first_model.html) for dependency and start-up instructions*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Import Dependencies\n", "This includes importing of dependencies needed for the tutorial." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [], "source": [ "# Has multi-dimensional arrays and matrices.\n", "# Has a large collection of mathematical functions to operate on these arrays.\n", "import numpy as np\n", "\n", "# Data manipulation and analysis.\n", "import pandas as pd\n", "\n", "# Data visualization tools.\n", "import seaborn as sns\n", "\n", "import mesa\n", "from mesa.discrete_space import CellAgent, OrthogonalMooreGrid\n", "from mesa.visualization import SolaraViz, SpaceRenderer, make_plot_component\n", "from mesa.visualization.components import AgentPortrayalStyle" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Basic Model\n", "\n", "The following is the basic model we will be using to build the dashboard. This is the same model seen in tutorials 0-3. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def compute_gini(model):\n", " agent_wealths = [agent.wealth for agent in model.agents]\n", " x = sorted(agent_wealths)\n", " N = model.num_agents\n", " B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x))\n", " return 1 + (1 / N) - 2 * B\n", "\n", "\n", "class MoneyAgent(CellAgent):\n", " \"\"\"An agent with fixed initial wealth.\"\"\"\n", "\n", " def __init__(self, model, cell):\n", " \"\"\"initialize a MoneyAgent instance.\n", "\n", " Args:\n", " model: A model instance\n", " \"\"\"\n", " super().__init__(model)\n", " self.cell = cell\n", " self.wealth = 1\n", "\n", " def move(self):\n", " \"\"\"Move the agent to a random neighboring cell.\"\"\"\n", " self.cell = self.cell.neighborhood.select_random_cell()\n", "\n", " def give_money(self):\n", " \"\"\"Give 1 unit of wealth to a random agent in the same cell.\"\"\"\n", " cellmates = [a for a in self.cell.agents if a is not self]\n", "\n", " if cellmates: # Only give money if there are other agents present\n", " other = self.random.choice(cellmates)\n", " other.wealth += 1\n", " self.wealth -= 1\n", "\n", " def step(self):\n", " \"\"\"do one step of the agent.\"\"\"\n", " self.move()\n", " if self.wealth > 0:\n", " self.give_money()\n", "\n", "\n", "class MoneyModel(mesa.Model):\n", " \"\"\"A model with some number of agents.\"\"\"\n", "\n", " def __init__(self, n=10, width=10, height=10, seed=None):\n", " \"\"\"Initialize a MoneyModel instance.\n", "\n", " Args:\n", " N: The number of agents.\n", " width: width of the grid.\n", " height: Height of the grid.\n", " \"\"\"\n", " super().__init__(seed=seed)\n", " self.num_agents = n\n", " self.grid = OrthogonalMooreGrid((width, height), random=self.random)\n", "\n", " # Create agents\n", " MoneyAgent.create_agents(\n", " self,\n", " self.num_agents,\n", " self.random.choices(self.grid.all_cells.cells, k=self.num_agents),\n", " )\n", "\n", " self.datacollector = mesa.DataCollector(\n", " model_reporters={\"Gini\": compute_gini}, agent_reporters={\"Wealth\": \"wealth\"}\n", " )\n", " self.datacollector.collect(self)\n", "\n", " def step(self):\n", " \"\"\"do one step of the model\"\"\"\n", " self.agents.shuffle_do(\"step\")\n", " self.datacollector.collect(self)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Lets make sure the model works\n", "model = MoneyModel(100, 10, 10)\n", "for _ in range(20):\n", " model.step()\n", "\n", "\n", "data = model.datacollector.get_agent_vars_dataframe()\n", "# Use seaborn\n", "g = sns.histplot(data[\"Wealth\"], discrete=True)\n", "g.set(title=\"Wealth distribution\", xlabel=\"Wealth\", ylabel=\"number of agents\");" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Adding visualization\n", "\n", "So far, we've built a model, run it, and analyzed some output afterwards. However, one of the advantages of agent-based models is that we can often watch them run step by step, potentially spotting unexpected patterns, behaviors or bugs, or developing new intuitions, hypotheses, or insights. Other times, watching a model run can explain it to an unfamiliar audience better than static explanations. Like many ABM frameworks, Mesa allows you to create an interactive visualization of the model. In this section we'll walk through creating a visualization using built-in components, and (for advanced users) how to create a new visualization element.\n", "\n", "First, a quick explanation of how Mesa's interactive visualization works. The visualization is done in a browser window or Jupyter instance, using the [Solara](https://solara.dev/) framework, a pure Python, React-style web framework. Running `solara run app.py` will launch a web server, which runs the model, and displays model detail at each step via a plotting library. Alternatively, you can execute everything inside a Jupyter instance and display it inline." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As like last time we then instantiate the model parameters, some of which are modifiable by user inputs. In this case, the number of agents, N, is specified as a slider of integers." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "model_params = {\n", " \"n\": {\n", " \"type\": \"SliderInt\",\n", " \"value\": 50,\n", " \"label\": \"Number of agents:\",\n", " \"min\": 10,\n", " \"max\": 100,\n", " \"step\": 1,\n", " },\n", " \"width\": 10,\n", " \"height\": 10,\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Then just like last time we instantiate the visualization object which (by default) displays the grid containing the agents, and timeseries of values computed by the model's data collector. In this example, we specify the Gini coefficient.\n", "\n", "There are 3 buttons:\n", "- the step button, which advances the model by 1 step\n", "- the play button, which advances the model indefinitely until it is paused\n", "- the pause button, which pauses the model\n", "\n", "To reset the model, the order of operations are important\n", "1. Stop the model\n", "2. Update the parameters (e.g. move the sliders)\n", "3. Press reset \n", "\n", "**Additional Interactive Controls**\n", "\n", "In addition to the basic controls (Play, Pause, Step), there are three extra interactive UI elements that give you more control over the simulation and visualization performance:\n", "\n", "1. **Play Interval Slider**\n", " This slider controls the time delay (in milliseconds) between each step of the simulation when it is playing.\n", "\n", " * **Lower values** = faster simulation updates\n", " * **Higher values** = slower, more observable step-by-step updates\n", "\n", "2. **Render Interval Slider**\n", " This slider determines how frequently the visualization updates, based on the number of steps.\n", "\n", " * For example, if set to `5`, the visualization will update only **after every 5 steps** of the model.\n", " * ⚠️ Note: This interval is **step-based**, not time-based.\n", "\n", "3. **Use Threads Checkbox**\n", " This checkbox enables threaded execution of the model.\n", "\n", " * When enabled, the visualization runs on a separate thread, allowing the UI to remain responsive even during heavy computations.\n", " * It also ensures the visualization only updates at fixed intervals, improving performance and responsiveness during rapid simulations." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Page Tab View\n", "\n", "#### **Plot Components**\n", "You can place different components (except the renderer) on separate pages according to your preference. There are no restrictions on page numbering — pages do not need to be sequential or positive. Each page acts as an independent window where components may or may not exist.\n", "\n", "The default page is `page=0`. If pages are not sequential (e.g., `page=1` and `page=10`), the system will automatically create the 8 empty pages in between to maintain consistent indexing. To avoid empty pages in your dashboard, use sequential page numbers.\n", "\n", "To assign a plot component to a specific page, pass the `page` keyword argument to `make_plot_component`. For example, the following will display the plot component on page 1:\n", "\n", "```python\n", "plot_comp = make_plot_component(\"encoding\", page=1)\n", "```\n", "\n", "#### **Custom Components**\n", "In tutorial 8, you will learn how to create custom components for the Solara dashboard. If you want a custom component to appear on a specific page, you must pass it as a tuple containing the component and the page number.\n", "\n", "```python\n", "@solara.component\n", "def CustomComponent():\n", " ...\n", "\n", "page = SolaraViz(\n", " model,\n", " renderer,\n", " components=[(CustomComponent, 1)] # Custom component will appear on page 1\n", ")\n", "```\n", "\n", "> ⚠️ **Warning**\n", "> Running the model can be performance-intensive. It is strongly recommended to pause the model in the dashboard before switching pages." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Advanced Rendering with `SpaceRenderer`\n", "\n", "> **⚠️ Important:**\n", "> Mesa supports both `matplotlib` and `altair` backends for rendering, but **they are not interchangeable** when it comes to visualization methods and `post_process` functions.\n", ">\n", "> * `matplotlib`-specific functions like `ax.set_title()` will not work with `altair`, and vice versa.\n", "> * **Be sure to run only the code blocks corresponding to the backend you are using**.\n", "> * Mixing backend-specific calls will lead to runtime errors.\n", "\n", "`SpaceRenderer` is a powerful tool in Mesa for visualizing spatial grids. It goes beyond simply drawing agents — it allows detailed customization of the grid, dynamic styling of agents, and the ability to overlay calculated values as *property layers*. Property layers effectively act as heatmaps, coloring each grid cell based on model-defined data (more on this in the next tutorial).\n", "\n", "In this section, we’ll demonstrate:\n", "\n", "* Styling the grid using `draw_structure()`\n", "* Customizing agent appearance with `draw_agents()`\n", "* Enhancing the final visualization using the `post_process` function\n", "* Applying these techniques to both `matplotlib` and `altair` backends\n", "\n", "Before we begin, we’ll reuse the `agent_portrayal` function and `money_model` defined in the previous tutorial." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def agent_portrayal(agent):\n", " portrayal = AgentPortrayalStyle(size=50, color=\"orange\")\n", " if agent.wealth > 0:\n", " portrayal.update((\"color\", \"blue\"), (\"size\", 100))\n", " return portrayal\n", "\n", "\n", "# Create initial model instance\n", "money_model = MoneyModel(n=50, width=10, height=10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### Drawing the Grid and Agents\n", "We’ll now create a renderer and draw both the grid structure and the agents using the `matplotlib` backend." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%capture\n", "\n", "renderer = SpaceRenderer(model=money_model, backend=\"matplotlib\")\n", "renderer.draw_structure(lw=2, ls=\"solid\", color=\"black\", alpha=0.1)\n", "renderer.draw_agents(agent_portrayal)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can pass drawing keyword arguments (kwargs) directly to draw_structure() to customize the grid appearance. See the full list of accepted arguments in the [SpaceDrawer documentation](https://mesa.readthedocs.io/latest/apis/visualization.html#module-mesa.visualization.space_drawers)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### Using Altair Backend\n", "The same can be achieved with the `altair` backend:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%capture\n", "\n", "renderer = SpaceRenderer(model=money_model, backend=\"altair\")\n", "renderer.draw_structure(\n", " xlabel=\"x\",\n", " ylabel=\"y\",\n", " grid_width=2,\n", " grid_dash=[1],\n", " grid_color=\"black\",\n", " grid_opacity=0.1,\n", " title=\"Boltzmann Wealth Model\",\n", ")\n", "renderer.draw_agents(agent_portrayal)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### Customizing the Final Output with `post_process`\n", "You can use a post_process function to make high-level visual tweaks after the rendering is complete. This is especially useful for setting axis labels, titles, aspect ratios, and more.\n", "\n", "The post_process can be passed into either the render() function or set differently as a property of SpaceRenderer." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**With `matplotlib`:**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def post_process(ax):\n", " \"\"\"Customize the matplotlib axes after rendering.\"\"\"\n", " ax.set_title(\"Boltzmann Wealth Model\")\n", " ax.set_xlabel(\"x\")\n", " ax.set_ylabel(\"y\")\n", " ax.grid(True, which=\"both\", linestyle=\"--\", linewidth=0.5, alpha=0.5)\n", " ax.set_aspect(\"equal\", adjustable=\"box\")\n", "\n", "\n", "renderer.post_process = post_process" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**With `Altair`:**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def post_process(chart):\n", " \"\"\"Customize the Altair chart after rendering.\"\"\"\n", " chart = (\n", " chart.properties(\n", " title=\"Boltzmann Wealth Model\",\n", " width=600,\n", " height=400,\n", " )\n", " .configure_axis(\n", " labelFontSize=12,\n", " titleFontSize=14,\n", " )\n", " .configure_title(fontSize=16)\n", " )\n", " return chart\n", "\n", "\n", "renderer.post_process = post_process" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can also use `post_process` with plots or other components." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### Post-Processing Line Plots\n", "> **NOTE**:\n", "> The backend of plot components can be the same as, or different from, that of the SpaceRenderer, depending on your preference.\n", "\n", "Let’s apply `post_process` to a line chart of the Gini coefficient over time:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def post_process_lines(ax):\n", " \"\"\"Customize the matplotlib axes for the Gini line plot.\"\"\"\n", " ax.set_title(\"Gini Coefficient Over Time\")\n", " ax.set_xlabel(\"Time Step\")\n", " ax.set_ylabel(\"Gini Coefficient\")\n", " ax.grid(True, which=\"both\", linestyle=\"--\", linewidth=0.5, alpha=0.5)\n", " ax.set_aspect(\"auto\")\n", "\n", "\n", "GiniPlot = make_plot_component(\"Gini\", post_process=post_process_lines)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### Launching the Full Visualization\n", "Now that we have the model, visual renderer, and plot components defined, we can bring everything together using `SolaraViz`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "page = SolaraViz(\n", " money_model,\n", " renderer,\n", " components=[GiniPlot],\n", " model_params=model_params,\n", " name=\"Boltzmann Wealth Model\",\n", ")\n", "\n", "# This is required to render the visualization in a Jupyter notebook\n", "page" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise\n", "\n", "- Try different agent shapes, colors, and grid styles.\n", "- Experiment with post_process to improve the look and feel of the final output.\n", "- Add additional time-series plots." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Next Steps\n", "\n", "Checkout [mesa examples](https://github.com/projectmesa/mesa/tree/main/mesa/examples) to further explore the capabilities of the visualization stack.\n", "Check out the next [property layer visualization](https://mesa.readthedocs.io/latest/tutorials/7_visualization_propertylayer_visualization.html) on how to further enhance your interactive dashboard." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[Comer2014] Comer, Kenneth W. “Who Goes First? An Examination of the Impact of Activation on Outcome Behavior in AgentBased Models.” George Mason University, 2014. http://mars.gmu.edu/bitstream/handle/1920/9070/Comer_gmu_0883E_10539.pdf\n", "\n", "[Dragulescu2002] Drăgulescu, Adrian A., and Victor M. Yakovenko. “Statistical Mechanics of Money, Income, and Wealth: A Short Survey.” arXiv Preprint Cond-mat/0211175, 2002. http://arxiv.org/abs/cond-mat/0211175." ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "env", "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.13.1" }, "widgets": { "state": {}, "version": "1.1.2" } }, "nbformat": 4, "nbformat_minor": 4 }