From d90b0f72cd65d5875eb9e4012990699cfe886c5c Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 18:57:57 +0100 Subject: [PATCH 01/34] further updates --- benchmarks/WolfSheep/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/benchmarks/WolfSheep/__init__.py b/benchmarks/WolfSheep/__init__.py index e69de29bb2d..18b86ab19ba 100644 --- a/benchmarks/WolfSheep/__init__.py +++ b/benchmarks/WolfSheep/__init__.py @@ -0,0 +1,14 @@ +from wolf_sheep import WolfSheep + +if __name__ == "__main__": + # for profiling this benchmark model + import time + + # model = WolfSheep(15, 25, 25, 60, 40, 0.2, 0.1, 20) + model = WolfSheep(15, 100, 100, 1000, 500, 0.4, 0.2, 20) + + start_time = time.perf_counter() + for _ in range(100): + model.step() + + print(time.perf_counter() - start_time) From 95864900d9e7981c8ea7240e34672f3fe792d681 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 21 Jan 2024 19:13:00 +0100 Subject: [PATCH 02/34] Update benchmarks/WolfSheep/__init__.py --- benchmarks/WolfSheep/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/WolfSheep/__init__.py b/benchmarks/WolfSheep/__init__.py index 18b86ab19ba..98b1e9fdfed 100644 --- a/benchmarks/WolfSheep/__init__.py +++ b/benchmarks/WolfSheep/__init__.py @@ -1,4 +1,4 @@ -from wolf_sheep import WolfSheep +from .wolf_sheep import WolfSheep if __name__ == "__main__": # for profiling this benchmark model From 2759244eb350ce8e38b31b08cbea46e40998893a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 27 Aug 2024 14:01:46 +0200 Subject: [PATCH 03/34] Update __init__.py --- benchmarks/WolfSheep/__init__.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/benchmarks/WolfSheep/__init__.py b/benchmarks/WolfSheep/__init__.py index 98b1e9fdfed..e69de29bb2d 100644 --- a/benchmarks/WolfSheep/__init__.py +++ b/benchmarks/WolfSheep/__init__.py @@ -1,14 +0,0 @@ -from .wolf_sheep import WolfSheep - -if __name__ == "__main__": - # for profiling this benchmark model - import time - - # model = WolfSheep(15, 25, 25, 60, 40, 0.2, 0.1, 20) - model = WolfSheep(15, 100, 100, 1000, 500, 0.4, 0.2, 20) - - start_time = time.perf_counter() - for _ in range(100): - model.step() - - print(time.perf_counter() - start_time) From 2d1d391208bfd7173075ff6c2a6ede55aebb5114 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 17 Oct 2024 16:07:41 +0200 Subject: [PATCH 04/34] first test of examples in docs --- docs/conf.py | 87 +++++++++ docs/example_template.txt | 15 ++ docs/examples.md | 14 ++ docs/examples/boid_flockers.md | 0 docs/examples/boltzmann_wealth_model.md | 150 +++++++++++++++ docs/examples/conways_game_of_life.md | 129 +++++++++++++ docs/examples/schelling.md | 148 +++++++++++++++ docs/examples/virus_on_network.md | 243 ++++++++++++++++++++++++ docs/examples_overview_template.txt | 10 + docs/index.md | 2 +- pyproject.toml | 1 - 11 files changed, 797 insertions(+), 2 deletions(-) create mode 100644 docs/example_template.txt create mode 100644 docs/examples.md create mode 100644 docs/examples/boid_flockers.md create mode 100644 docs/examples/boltzmann_wealth_model.md create mode 100644 docs/examples/conways_game_of_life.md create mode 100644 docs/examples/schelling.md create mode 100644 docs/examples/virus_on_network.md create mode 100644 docs/examples_overview_template.txt diff --git a/docs/conf.py b/docs/conf.py index 161edf5ff15..798764848cb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,12 +14,16 @@ # serve to show the default. import os +import os.path as osp +import glob import sys +import string from datetime import date # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. +HERE = osp.abspath(osp.dirname(__file__)) sys.path.insert(0, os.path.abspath(".")) sys.path.insert(0, "../examples") sys.path.insert(0, "../mesa") @@ -289,3 +293,86 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} + + + +def setup_examples_pages(): + # create rst files for all examples + # check what examples exist + examples_folder = osp.abspath(osp.join(HERE, "..", "examples")) + + # fixme shift to walkdir + # we should have a single rst page for each subdirectory + + basic_examples = [f.path for f in os.scandir(osp.join(examples_folder, "basic")) if f.is_dir() ] + advanced_examples = [] + # advanced_examples = [f.path for f in os.scandir(osp.join(examples_folder, "advanced")) if f.is_dir()] + examples = basic_examples + advanced_examples + + # get all existing rst files + md_files = glob.glob(os.path.join(HERE, "examples", "*.md")) + md_files = {os.path.basename(os.path.normpath(entry)) for entry in md_files} + + # check which rst files exist + with open(os.path.join(HERE, "example_template.txt")) as fh: + template = string.Template(fh.read()) + + # TODO:: at the moment no idea what happens if example is updated. Does this trigger a rebuild of the html page? + # probably not... because file is not new. So we need some timestamp trick? + examples_md = [] + for example in examples: + # fixme we have the directories + # from the directory, get 3 file names + base_name = os.path.basename(os.path.normpath(example)) + + agent_filename = os.path.join(example, "agents.py") + model_filename = os.path.join(example, "model.py") + readme_filename = os.path.join(example, "README.md") + + md_filename = f"{base_name}.md" + examples_md.append(f"./examples/{base_name}") + + with open(agent_filename, 'r') as content_file: + agent_file = content_file.read() + with open(model_filename, 'r') as content_file: + model_file = content_file.read() + with open(readme_filename, 'r') as content_file: + readme_file = content_file.read() + + if md_filename not in md_files: + with open(os.path.join(HERE, "examples", md_filename), "w") as fh: + # fixme we need to read in the entire file here + content = template.substitute( + dict(agent_file=agent_file, model_file=model_file, + readme_file=readme_file) + ) + fh.write(content) + else: + md_files.remove(md_filename) + + # these md files are outdated because the example has been removed + for entry in md_files: + fn = os.path.join(HERE, "examples", entry) + os.remove(fn) + + # creeate overview of examples.md + with open(os.path.join(HERE, "examples_overview_template.txt")) as fh: + template = string.Template(fh.read()) + + with open(os.path.join(HERE, "examples.md"), "w") as fh: + content = template.substitute( + dict( + examples="\n".join([f"<{entry[2::]}>" for entry in examples_md]), + ) + ) + fh.write(content) + +def setup(app): + # copy changelog into source folder for documentation + # dest = osp.join(HERE, "./getting_started/changelog.md") + # shutil.copy(osp.join(HERE, "..", "..", "CHANGELOG.md"), dest) + setup_examples_pages() + + +if __name__ == "__main__": + setup_examples_pages() \ No newline at end of file diff --git a/docs/example_template.txt b/docs/example_template.txt new file mode 100644 index 00000000000..6fcc5c06a0a --- /dev/null +++ b/docs/example_template.txt @@ -0,0 +1,15 @@ + +$readme_file + +# Agents + +```python +$agent_file +``` + + +# Model + +```python +$model_file +``` \ No newline at end of file diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 00000000000..1ee87de8fe4 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,14 @@ + +# Examples + + +```{toctree} +:maxdepth: 2 + + + + + + + +``` \ No newline at end of file diff --git a/docs/examples/boid_flockers.md b/docs/examples/boid_flockers.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/examples/boltzmann_wealth_model.md b/docs/examples/boltzmann_wealth_model.md new file mode 100644 index 00000000000..e112aa5ec66 --- /dev/null +++ b/docs/examples/boltzmann_wealth_model.md @@ -0,0 +1,150 @@ + +# 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) + +Make sure to install the requirements first: + +``` + $ pip install -r requirements.txt +``` + +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: + +``` + $ solara run app.py +``` + +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 + +* ``model.py``: Final version of the model. +* ``app.py``: Code for the interactive visualization. + +## Optional + +An optional visualization is also provided using Streamlit, which is another popular Python library for creating interactive web applications. + +To run the Streamlit app, you will need to install the `streamlit` and `altair` libraries: + +``` + $ pip install streamlit altair +``` + +Then, you can run the Streamlit app using the following command: + +``` + $ streamlit run st_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) + + +# Agents + +```python +from mesa import Agent + + +class MoneyAgent(Agent): + """An agent with fixed initial wealth.""" + + def __init__(self, model): + super().__init__(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() + +``` + + +# Model + +```python +from agents import MoneyAgent + +import mesa + + +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): + super().__init__() + self.num_agents = n + self.grid = mesa.space.MultiGrid(width, height, True) + + self.datacollector = mesa.DataCollector( + model_reporters={"Gini": self.compute_gini}, + agent_reporters={"Wealth": "wealth"}, + ) + # Create agents + for _ in range(self.num_agents): + a = MoneyAgent(self) + + # Add the agent to a random grid cell + x = self.random.randrange(self.grid.width) + y = self.random.randrange(self.grid.height) + self.grid.place_agent(a, (x, y)) + + self.running = True + self.datacollector.collect(self) + + def step(self): + self.agents.shuffle_do("step") + self.datacollector.collect(self) + + def compute_gini(self): + agent_wealths = [agent.wealth for agent in self.agents] + x = sorted(agent_wealths) + n = self.num_agents + b = sum(xi * (n - i) for i, xi in enumerate(x)) / (n * sum(x)) + return 1 + (1 / n) - 2 * b + +``` \ No newline at end of file diff --git a/docs/examples/conways_game_of_life.md b/docs/examples/conways_game_of_life.md new file mode 100644 index 00000000000..ff490a6c3d4 --- /dev/null +++ b/docs/examples/conways_game_of_life.md @@ -0,0 +1,129 @@ + +# Conway's Game Of "Life" + +## Summary + +[The Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life), also known simply as "Life", is a cellular automaton devised by the British mathematician John Horton Conway in 1970. + +The "game" is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input by a human. One interacts with the Game of "Life" by creating an initial configuration and observing how it evolves, or, for advanced "players", by creating patterns with particular properties. + + +## 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 ``run``. + +## Files + +* ``agents.py``: Defines the behavior of an individual cell, which can be in two states: DEAD or ALIVE. +* ``model.py``: Defines the model itself, initialized with a random configuration of alive and dead cells. +* ``portrayal.py``: Describes for the front end how to render a cell. +* ``st_app.py``: Defines an interactive visualization using Streamlit. + +## Optional + +* ``conways_game_of_life/st_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 st_app.py`` + + +## Further Reading +[Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) + + +# Agents + +```python +from mesa import Agent + + +class Cell(Agent): + """Represents a single ALIVE or DEAD cell in the simulation.""" + + DEAD = 0 + ALIVE = 1 + + def __init__(self, pos, model, init_state=DEAD): + """Create a cell, in the given state, at the given x, y position.""" + super().__init__(model) + self.x, self.y = pos + self.state = init_state + self._nextState = None + + @property + def isAlive(self): + return self.state == self.ALIVE + + @property + def neighbors(self): + return self.model.grid.iter_neighbors((self.x, self.y), True) + + def determine_state(self): + """Compute if the cell will be dead or alive at the next tick. This is + based on the number of alive or dead neighbors. The state is not + changed here, but is just computed and stored in self._nextState, + because our current state may still be necessary for our neighbors + to calculate their next state. + """ + # Get the neighbors and apply the rules on whether to be alive or dead + # at the next tick. + live_neighbors = sum(neighbor.isAlive for neighbor in self.neighbors) + + # Assume nextState is unchanged, unless changed below. + self._nextState = self.state + if self.isAlive: + if live_neighbors < 2 or live_neighbors > 3: + self._nextState = self.DEAD + else: + if live_neighbors == 3: + self._nextState = self.ALIVE + + def assume_state(self): + """Set the state to the new computed state -- computed in step().""" + self.state = self._nextState + +``` + + +# Model + +```python +from agents import Cell + +from mesa import Model +from mesa.space import SingleGrid + + +class ConwaysGameOfLife(Model): + """Represents the 2-dimensional array of cells in Conway's Game of Life.""" + + def __init__(self, width=50, height=50): + """Create a new playing area of (width, height) cells.""" + super().__init__() + # Use a simple grid, where edges wrap around. + self.grid = SingleGrid(width, height, torus=True) + + # Place a cell at each location, with some initialized to + # ALIVE and some to DEAD. + for _contents, (x, y) in self.grid.coord_iter(): + cell = Cell((x, y), self) + if self.random.random() < 0.1: + cell.state = cell.ALIVE + self.grid.place_agent(cell, (x, y)) + + self.running = True + + def step(self): + """Perform the model step in two stages: + - First, all cells assume their next state (whether they will be dead or alive) + - Then, all cells change state to their next state. + """ + self.agents.do("determine_state") + self.agents.do("assume_state") + +``` \ No newline at end of file diff --git a/docs/examples/schelling.md b/docs/examples/schelling.md new file mode 100644 index 00000000000..44413fdb329 --- /dev/null +++ b/docs/examples/schelling.md @@ -0,0 +1,148 @@ + +# 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, in this directory, run the following command + +``` + $ solara run app.py +``` + +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/). + +## 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 + +* ``app.py``: Code for the interactive visualization. +* ``schelling.py``: Contains the agent class, and the overall model class. +* ``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. + + +# Agents + +```python +from mesa import Agent, Model + + +class SchellingAgent(Agent): + """Schelling segregation agent.""" + + def __init__(self, model: Model, agent_type: int) -> None: + """Create a new Schelling agent. + + Args: + agent_type: Indicator for the agent's type (minority=1, majority=0) + """ + super().__init__(model) + self.type = agent_type + + def step(self) -> None: + neighbors = self.model.grid.iter_neighbors( + self.pos, moore=True, radius=self.model.radius + ) + similar = sum(1 for neighbor in neighbors if neighbor.type == self.type) + + # If unhappy, move: + if similar < self.model.homophily: + self.model.grid.move_to_empty(self) + else: + self.model.happy += 1 + +``` + + +# Model + +```python +from agents import SchellingAgent + +import mesa +from mesa import Model + + +class Schelling(Model): + """Model class for the Schelling segregation model.""" + + def __init__( + self, + height=20, + width=20, + homophily=3, + radius=1, + density=0.8, + minority_pc=0.2, + seed=None, + ): + """Create a new Schelling model. + + Args: + width, height: Size of the space. + density: Initial Chance for a cell to populated + minority_pc: Chances for an agent to be in minority class + homophily: Minimum number of agents of same class needed to be happy + radius: Search radius for checking similarity + seed: Seed for Reproducibility + """ + super().__init__(seed=seed) + self.homophily = homophily + self.radius = radius + + self.grid = mesa.space.SingleGrid(width, height, torus=True) + + self.happy = 0 + self.datacollector = mesa.DataCollector( + model_reporters={"happy": "happy"}, # Model-level count of happy agents + ) + + # Set up agents + # We use a grid iterator that returns + # the coordinates of a cell as well as + # its contents. (coord_iter) + for _, pos in self.grid.coord_iter(): + if self.random.random() < density: + agent_type = 1 if self.random.random() < minority_pc else 0 + agent = SchellingAgent(self, agent_type) + self.grid.place_agent(agent, pos) + + self.datacollector.collect(self) + + def step(self): + """Run one step of the model.""" + self.happy = 0 # Reset counter of happy agents + self.agents.shuffle_do("step") + + self.datacollector.collect(self) + + self.running = self.happy != len(self.agents) + +``` \ No newline at end of file diff --git a/docs/examples/virus_on_network.md b/docs/examples/virus_on_network.md new file mode 100644 index 00000000000..7d338a3a41f --- /dev/null +++ b/docs/examples/virus_on_network.md @@ -0,0 +1,243 @@ + +# Virus on a Network + +## Summary + +This model is based on the NetLogo model "Virus on Network". It demonstrates the spread of a virus through a network and follows the SIR model, commonly seen in epidemiology. + +The SIR model is one of the simplest compartmental models, and many models are derivatives of this basic form. The model consists of three compartments: + +S: The number of susceptible individuals. When a susceptible and an infectious individual come into "infectious contact", the susceptible individual contracts the disease and transitions to the infectious compartment. +I: The number of infectious individuals. These are individuals who have been infected and are capable of infecting susceptible individuals. +R for the number of removed (and immune) or deceased individuals. These are individuals who have been infected and have either recovered from the disease and entered the removed compartment, or died. It is assumed that the number of deaths is negligible with respect to the total population. This compartment may also be called "recovered" or "resistant". + +For more information about this model, read the NetLogo's web page: http://ccl.northwestern.edu/netlogo/models/VirusonaNetwork. + +JavaScript library used in this example to render the network: [d3.js](https://d3js.org/). + +## 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. + +or + +Directly run the file ``run.py`` in the terminal. e.g. + +``` + $ python run.py +``` + + +## Files + +* ``model.py``: Contains the agent class, and the overall model class. +* ``agents.py``: Contains the agent class. +* ``app.py``: Contains the code for the interactive Solara visualization. + +## 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 + + +[Stonedahl, F. and Wilensky, U. (2008). NetLogo Virus on a Network model](http://ccl.northwestern.edu/netlogo/models/VirusonaNetwork). +Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. + + +[Wilensky, U. (1999). NetLogo](http://ccl.northwestern.edu/netlogo/) +Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. + + +# Agents + +```python +from enum import Enum + +from mesa import Agent + + +class State(Enum): + SUSCEPTIBLE = 0 + INFECTED = 1 + RESISTANT = 2 + + +class VirusAgent(Agent): + """Individual Agent definition and its properties/interaction methods.""" + + def __init__( + self, + model, + initial_state, + virus_spread_chance, + virus_check_frequency, + recovery_chance, + gain_resistance_chance, + ): + super().__init__(model) + + self.state = initial_state + + self.virus_spread_chance = virus_spread_chance + self.virus_check_frequency = virus_check_frequency + self.recovery_chance = recovery_chance + self.gain_resistance_chance = gain_resistance_chance + + def try_to_infect_neighbors(self): + neighbors_nodes = self.model.grid.get_neighborhood( + self.pos, include_center=False + ) + susceptible_neighbors = [ + agent + for agent in self.model.grid.get_cell_list_contents(neighbors_nodes) + if agent.state is State.SUSCEPTIBLE + ] + for a in susceptible_neighbors: + if self.random.random() < self.virus_spread_chance: + a.state = State.INFECTED + + def try_gain_resistance(self): + if self.random.random() < self.gain_resistance_chance: + self.state = State.RESISTANT + + def try_remove_infection(self): + # Try to remove + if self.random.random() < self.recovery_chance: + # Success + self.state = State.SUSCEPTIBLE + self.try_gain_resistance() + else: + # Failed + self.state = State.INFECTED + + def try_check_situation(self): + if (self.random.random() < self.virus_check_frequency) and ( + self.state is State.INFECTED + ): + self.try_remove_infection() + + def step(self): + if self.state is State.INFECTED: + self.try_to_infect_neighbors() + self.try_check_situation() + +``` + + +# Model + +```python +import math + +import networkx as nx +from agents import State, VirusAgent + +import mesa +from mesa import Model + + +def number_state(model, state): + return sum(1 for a in model.grid.get_all_cell_contents() if a.state is state) + + +def number_infected(model): + return number_state(model, State.INFECTED) + + +def number_susceptible(model): + return number_state(model, State.SUSCEPTIBLE) + + +def number_resistant(model): + return number_state(model, State.RESISTANT) + + +class VirusOnNetwork(Model): + """A virus model with some number of agents.""" + + def __init__( + self, + num_nodes=10, + avg_node_degree=3, + initial_outbreak_size=1, + virus_spread_chance=0.4, + virus_check_frequency=0.4, + recovery_chance=0.3, + gain_resistance_chance=0.5, + ): + super().__init__() + self.num_nodes = num_nodes + prob = avg_node_degree / self.num_nodes + self.G = nx.erdos_renyi_graph(n=self.num_nodes, p=prob) + self.grid = mesa.space.NetworkGrid(self.G) + + self.initial_outbreak_size = ( + initial_outbreak_size if initial_outbreak_size <= num_nodes else num_nodes + ) + self.virus_spread_chance = virus_spread_chance + self.virus_check_frequency = virus_check_frequency + self.recovery_chance = recovery_chance + self.gain_resistance_chance = gain_resistance_chance + + self.datacollector = mesa.DataCollector( + { + "Infected": number_infected, + "Susceptible": number_susceptible, + "Resistant": number_resistant, + } + ) + + # Create agents + for node in self.G.nodes(): + a = VirusAgent( + self, + State.SUSCEPTIBLE, + self.virus_spread_chance, + self.virus_check_frequency, + self.recovery_chance, + self.gain_resistance_chance, + ) + + # Add the agent to the node + self.grid.place_agent(a, node) + + # Infect some nodes + infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size) + for a in self.grid.get_cell_list_contents(infected_nodes): + a.state = State.INFECTED + + self.running = True + self.datacollector.collect(self) + + def resistant_susceptible_ratio(self): + try: + return number_state(self, State.RESISTANT) / number_state( + self, State.SUSCEPTIBLE + ) + except ZeroDivisionError: + return math.inf + + def step(self): + self.agents.shuffle_do("step") + # collect data + self.datacollector.collect(self) + + def run_model(self, n): + for _ in range(n): + self.step() + +``` \ No newline at end of file diff --git a/docs/examples_overview_template.txt b/docs/examples_overview_template.txt new file mode 100644 index 00000000000..611959e91a5 --- /dev/null +++ b/docs/examples_overview_template.txt @@ -0,0 +1,10 @@ + +# Examples + + +```{toctree} +:maxdepth: 2 + +$examples + +``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index eceefe6c4d2..77e40cfd900 100644 --- a/docs/index.md +++ b/docs/index.md @@ -88,7 +88,7 @@ ABM features users have shared that you may want to use in your model Mesa Overview tutorials/intro_tutorial -tutorials/visualization_tutorial +examples Migration guide Best Practices How-to Guide diff --git a/pyproject.toml b/pyproject.toml index b046b797f66..fc8c56672bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,3 @@ -[build-system] requires = ["hatchling"] build-backend = "hatchling.build" From dc84b97f87176569f1fbd06a743f56ccfedbf037 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:09:29 +0000 Subject: [PATCH 05/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 798764848cb..4185d6c20a7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -332,11 +332,11 @@ def setup_examples_pages(): md_filename = f"{base_name}.md" examples_md.append(f"./examples/{base_name}") - with open(agent_filename, 'r') as content_file: + with open(agent_filename) as content_file: agent_file = content_file.read() - with open(model_filename, 'r') as content_file: + with open(model_filename) as content_file: model_file = content_file.read() - with open(readme_filename, 'r') as content_file: + with open(readme_filename) as content_file: readme_file = content_file.read() if md_filename not in md_files: From 25837e25955590d6aa67eab94a1e74f745e44cf8 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 17 Oct 2024 16:15:01 +0200 Subject: [PATCH 06/34] Update conf.py --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 4185d6c20a7..76745cd17e8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -355,7 +355,7 @@ def setup_examples_pages(): fn = os.path.join(HERE, "examples", entry) os.remove(fn) - # creeate overview of examples.md + # create overview of examples.md with open(os.path.join(HERE, "examples_overview_template.txt")) as fh: template = string.Template(fh.read()) From 6c132d1d0becfd0c51d39c4775cedffda08e8d4b Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 17 Oct 2024 16:17:23 +0200 Subject: [PATCH 07/34] Update pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index fc8c56672bc..b046b797f66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,4 @@ +[build-system] requires = ["hatchling"] build-backend = "hatchling.build" From 12512972bda051e12acb84035226cef47314391e Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 17 Oct 2024 16:21:17 +0200 Subject: [PATCH 08/34] Update conf.py --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 76745cd17e8..0bcc20cd874 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -327,7 +327,7 @@ def setup_examples_pages(): agent_filename = os.path.join(example, "agents.py") model_filename = os.path.join(example, "model.py") - readme_filename = os.path.join(example, "README.md") + readme_filename = os.path.join(example, "Readme.md") md_filename = f"{base_name}.md" examples_md.append(f"./examples/{base_name}") From 6cb510da2c18c17424271ef2a46cb2db410212b5 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 17 Oct 2024 16:23:57 +0200 Subject: [PATCH 09/34] filename updates --- examples/basic/schelling/{README.md => Readme.md} | 0 examples/basic/virus_on_network/{README.md => Readme..md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename examples/basic/schelling/{README.md => Readme.md} (100%) rename examples/basic/virus_on_network/{README.md => Readme..md} (100%) diff --git a/examples/basic/schelling/README.md b/examples/basic/schelling/Readme.md similarity index 100% rename from examples/basic/schelling/README.md rename to examples/basic/schelling/Readme.md diff --git a/examples/basic/virus_on_network/README.md b/examples/basic/virus_on_network/Readme..md similarity index 100% rename from examples/basic/virus_on_network/README.md rename to examples/basic/virus_on_network/Readme..md From 8f41c33697af0a07fb429198d41cc807f29b380a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 17 Oct 2024 16:27:48 +0200 Subject: [PATCH 10/34] Update conf.py --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0bcc20cd874..1fcba77f86a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -374,5 +374,5 @@ def setup(app): setup_examples_pages() -if __name__ == "__main__": - setup_examples_pages() \ No newline at end of file +# if __name__ == "__main__": +# setup_examples_pages() \ No newline at end of file From 8bbdb1d8048d5a458d0f4531044c808ce7d5f65e Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 17 Oct 2024 16:30:51 +0200 Subject: [PATCH 11/34] fixes --- docs/conf.py | 3 +- docs/examples/boid_flockers.md | 198 ++++++++++++++ docs/examples/boltzmann_wealth_model.md | 150 ----------- docs/examples/conways_game_of_life.md | 129 ---------- docs/examples/schelling.md | 148 ----------- docs/examples/virus_on_network.md | 243 ------------------ .../{Readme..md => Readme.md} | 0 7 files changed, 199 insertions(+), 672 deletions(-) delete mode 100644 docs/examples/boltzmann_wealth_model.md delete mode 100644 docs/examples/conways_game_of_life.md delete mode 100644 docs/examples/schelling.md delete mode 100644 docs/examples/virus_on_network.md rename examples/basic/virus_on_network/{Readme..md => Readme.md} (100%) diff --git a/docs/conf.py b/docs/conf.py index 1fcba77f86a..9a1055f54d9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -341,7 +341,6 @@ def setup_examples_pages(): if md_filename not in md_files: with open(os.path.join(HERE, "examples", md_filename), "w") as fh: - # fixme we need to read in the entire file here content = template.substitute( dict(agent_file=agent_file, model_file=model_file, readme_file=readme_file) @@ -373,6 +372,6 @@ def setup(app): # shutil.copy(osp.join(HERE, "..", "..", "CHANGELOG.md"), dest) setup_examples_pages() - +# # if __name__ == "__main__": # setup_examples_pages() \ No newline at end of file diff --git a/docs/examples/boid_flockers.md b/docs/examples/boid_flockers.md index e69de29bb2d..52f08995e9f 100644 --- a/docs/examples/boid_flockers.md +++ b/docs/examples/boid_flockers.md @@ -0,0 +1,198 @@ + +# Boids Flockers + +## Summary + +An implementation of Craig Reynolds's Boids flocker model. Agents (simulated birds) try to fly towards the average position of their neighbors and in the same direction as them, while maintaining a minimum distance. This produces flocking behavior. + +This model tests Mesa's continuous space feature, and uses numpy arrays to represent vectors. It also demonstrates how to create custom visualization components. + +## 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 launch the visualization interactively, run ``mesa runserver`` in this directory. e.g. + +``` +$ mesa runserver +``` + +or + +Directly run the file ``run.py`` in the terminal. e.g. + +``` + $ python run.py +``` + +* Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. + +## Files + +* [model.py](model.py): Core model file; contains the Boid Model and Boid Agent class. +* [app.py](app.py): Visualization code. + +## Further Reading + +The following link can be visited for more information on the boid flockers model: +https://cs.stanford.edu/people/eroberts/courses/soco/projects/2008-09/modeling-natural-systems/boids.html + + +# Agents + +```python +import numpy as np + +from mesa import Agent + + +class Boid(Agent): + """A Boid-style flocker agent. + + The agent follows three behaviors to flock: + - Cohesion: steering towards neighboring agents. + - Separation: avoiding getting too close to any other agent. + - Alignment: try to fly in the same direction as the neighbors. + + Boids have a vision that defines the radius in which they look for their + neighbors to flock with. Their speed (a scalar) and direction (a vector) + define their movement. Separation is their desired minimum distance from + any other Boid. + """ + + def __init__( + self, + model, + speed, + direction, + vision, + separation, + cohere=0.03, + separate=0.015, + match=0.05, + ): + """Create a new Boid flocker agent. + + Args: + speed: Distance to move per step. + direction: numpy vector for the Boid's direction of movement. + vision: Radius to look around for nearby Boids. + separation: Minimum distance to maintain from other Boids. + cohere: the relative importance of matching neighbors' positions + separate: the relative importance of avoiding close neighbors + match: the relative importance of matching neighbors' headings + """ + super().__init__(model) + self.speed = speed + self.direction = direction + self.vision = vision + self.separation = separation + self.cohere_factor = cohere + self.separate_factor = separate + self.match_factor = match + self.neighbors = None + + def step(self): + """Get the Boid's neighbors, compute the new vector, and move accordingly.""" + self.neighbors = self.model.space.get_neighbors(self.pos, self.vision, False) + n = 0 + match_vector, separation_vector, cohere = np.zeros((3, 2)) + for neighbor in self.neighbors: + n += 1 + heading = self.model.space.get_heading(self.pos, neighbor.pos) + cohere += heading + if self.model.space.get_distance(self.pos, neighbor.pos) < self.separation: + separation_vector -= heading + match_vector += neighbor.direction + n = max(n, 1) + cohere = cohere * self.cohere_factor + separation_vector = separation_vector * self.separate_factor + match_vector = match_vector * self.match_factor + self.direction += (cohere + separation_vector + match_vector) / n + self.direction /= np.linalg.norm(self.direction) + new_pos = self.pos + self.direction * self.speed + self.model.space.move_agent(self, new_pos) + +``` + + +# Model + +```python +"""Flockers. +============================================================= +A Mesa implementation of Craig Reynolds's Boids flocker model. +Uses numpy arrays to represent vectors. +""" + +import numpy as np +from agents import Boid + +import mesa + + +class BoidFlockers(mesa.Model): + """Flocker model class. Handles agent creation, placement and scheduling.""" + + def __init__( + self, + seed=None, + population=100, + width=100, + height=100, + vision=10, + speed=1, + separation=1, + cohere=0.03, + separate=0.015, + match=0.05, + ): + """Create a new Flockers model. + + Args: + population: Number of Boids + width, height: Size of the space. + speed: How fast should the Boids move. + vision: How far around should each Boid look for its neighbors + separation: What's the minimum distance each Boid will attempt to + keep from any other + cohere, separate, match: factors for the relative importance of + the three drives. + """ + super().__init__(seed=seed) + self.population = population + self.vision = vision + self.speed = speed + self.separation = separation + + self.space = mesa.space.ContinuousSpace(width, height, True) + self.factors = {"cohere": cohere, "separate": separate, "match": match} + self.make_agents() + + def make_agents(self): + """Create self.population agents, with random positions and starting headings.""" + for _ in range(self.population): + x = self.random.random() * self.space.x_max + y = self.random.random() * self.space.y_max + pos = np.array((x, y)) + direction = np.random.random(2) * 2 - 1 + boid = Boid( + model=self, + speed=self.speed, + direction=direction, + vision=self.vision, + separation=self.separation, + **self.factors, + ) + self.space.place_agent(boid, pos) + + def step(self): + self.agents.shuffle_do("step") + +``` \ No newline at end of file diff --git a/docs/examples/boltzmann_wealth_model.md b/docs/examples/boltzmann_wealth_model.md deleted file mode 100644 index e112aa5ec66..00000000000 --- a/docs/examples/boltzmann_wealth_model.md +++ /dev/null @@ -1,150 +0,0 @@ - -# 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) - -Make sure to install the requirements first: - -``` - $ pip install -r requirements.txt -``` - -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: - -``` - $ solara run app.py -``` - -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 - -* ``model.py``: Final version of the model. -* ``app.py``: Code for the interactive visualization. - -## Optional - -An optional visualization is also provided using Streamlit, which is another popular Python library for creating interactive web applications. - -To run the Streamlit app, you will need to install the `streamlit` and `altair` libraries: - -``` - $ pip install streamlit altair -``` - -Then, you can run the Streamlit app using the following command: - -``` - $ streamlit run st_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) - - -# Agents - -```python -from mesa import Agent - - -class MoneyAgent(Agent): - """An agent with fixed initial wealth.""" - - def __init__(self, model): - super().__init__(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() - -``` - - -# Model - -```python -from agents import MoneyAgent - -import mesa - - -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): - super().__init__() - self.num_agents = n - self.grid = mesa.space.MultiGrid(width, height, True) - - self.datacollector = mesa.DataCollector( - model_reporters={"Gini": self.compute_gini}, - agent_reporters={"Wealth": "wealth"}, - ) - # Create agents - for _ in range(self.num_agents): - a = MoneyAgent(self) - - # Add the agent to a random grid cell - x = self.random.randrange(self.grid.width) - y = self.random.randrange(self.grid.height) - self.grid.place_agent(a, (x, y)) - - self.running = True - self.datacollector.collect(self) - - def step(self): - self.agents.shuffle_do("step") - self.datacollector.collect(self) - - def compute_gini(self): - agent_wealths = [agent.wealth for agent in self.agents] - x = sorted(agent_wealths) - n = self.num_agents - b = sum(xi * (n - i) for i, xi in enumerate(x)) / (n * sum(x)) - return 1 + (1 / n) - 2 * b - -``` \ No newline at end of file diff --git a/docs/examples/conways_game_of_life.md b/docs/examples/conways_game_of_life.md deleted file mode 100644 index ff490a6c3d4..00000000000 --- a/docs/examples/conways_game_of_life.md +++ /dev/null @@ -1,129 +0,0 @@ - -# Conway's Game Of "Life" - -## Summary - -[The Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life), also known simply as "Life", is a cellular automaton devised by the British mathematician John Horton Conway in 1970. - -The "game" is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input by a human. One interacts with the Game of "Life" by creating an initial configuration and observing how it evolves, or, for advanced "players", by creating patterns with particular properties. - - -## 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 ``run``. - -## Files - -* ``agents.py``: Defines the behavior of an individual cell, which can be in two states: DEAD or ALIVE. -* ``model.py``: Defines the model itself, initialized with a random configuration of alive and dead cells. -* ``portrayal.py``: Describes for the front end how to render a cell. -* ``st_app.py``: Defines an interactive visualization using Streamlit. - -## Optional - -* ``conways_game_of_life/st_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 st_app.py`` - - -## Further Reading -[Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) - - -# Agents - -```python -from mesa import Agent - - -class Cell(Agent): - """Represents a single ALIVE or DEAD cell in the simulation.""" - - DEAD = 0 - ALIVE = 1 - - def __init__(self, pos, model, init_state=DEAD): - """Create a cell, in the given state, at the given x, y position.""" - super().__init__(model) - self.x, self.y = pos - self.state = init_state - self._nextState = None - - @property - def isAlive(self): - return self.state == self.ALIVE - - @property - def neighbors(self): - return self.model.grid.iter_neighbors((self.x, self.y), True) - - def determine_state(self): - """Compute if the cell will be dead or alive at the next tick. This is - based on the number of alive or dead neighbors. The state is not - changed here, but is just computed and stored in self._nextState, - because our current state may still be necessary for our neighbors - to calculate their next state. - """ - # Get the neighbors and apply the rules on whether to be alive or dead - # at the next tick. - live_neighbors = sum(neighbor.isAlive for neighbor in self.neighbors) - - # Assume nextState is unchanged, unless changed below. - self._nextState = self.state - if self.isAlive: - if live_neighbors < 2 or live_neighbors > 3: - self._nextState = self.DEAD - else: - if live_neighbors == 3: - self._nextState = self.ALIVE - - def assume_state(self): - """Set the state to the new computed state -- computed in step().""" - self.state = self._nextState - -``` - - -# Model - -```python -from agents import Cell - -from mesa import Model -from mesa.space import SingleGrid - - -class ConwaysGameOfLife(Model): - """Represents the 2-dimensional array of cells in Conway's Game of Life.""" - - def __init__(self, width=50, height=50): - """Create a new playing area of (width, height) cells.""" - super().__init__() - # Use a simple grid, where edges wrap around. - self.grid = SingleGrid(width, height, torus=True) - - # Place a cell at each location, with some initialized to - # ALIVE and some to DEAD. - for _contents, (x, y) in self.grid.coord_iter(): - cell = Cell((x, y), self) - if self.random.random() < 0.1: - cell.state = cell.ALIVE - self.grid.place_agent(cell, (x, y)) - - self.running = True - - def step(self): - """Perform the model step in two stages: - - First, all cells assume their next state (whether they will be dead or alive) - - Then, all cells change state to their next state. - """ - self.agents.do("determine_state") - self.agents.do("assume_state") - -``` \ No newline at end of file diff --git a/docs/examples/schelling.md b/docs/examples/schelling.md deleted file mode 100644 index 44413fdb329..00000000000 --- a/docs/examples/schelling.md +++ /dev/null @@ -1,148 +0,0 @@ - -# 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, in this directory, run the following command - -``` - $ solara run app.py -``` - -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/). - -## 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 - -* ``app.py``: Code for the interactive visualization. -* ``schelling.py``: Contains the agent class, and the overall model class. -* ``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. - - -# Agents - -```python -from mesa import Agent, Model - - -class SchellingAgent(Agent): - """Schelling segregation agent.""" - - def __init__(self, model: Model, agent_type: int) -> None: - """Create a new Schelling agent. - - Args: - agent_type: Indicator for the agent's type (minority=1, majority=0) - """ - super().__init__(model) - self.type = agent_type - - def step(self) -> None: - neighbors = self.model.grid.iter_neighbors( - self.pos, moore=True, radius=self.model.radius - ) - similar = sum(1 for neighbor in neighbors if neighbor.type == self.type) - - # If unhappy, move: - if similar < self.model.homophily: - self.model.grid.move_to_empty(self) - else: - self.model.happy += 1 - -``` - - -# Model - -```python -from agents import SchellingAgent - -import mesa -from mesa import Model - - -class Schelling(Model): - """Model class for the Schelling segregation model.""" - - def __init__( - self, - height=20, - width=20, - homophily=3, - radius=1, - density=0.8, - minority_pc=0.2, - seed=None, - ): - """Create a new Schelling model. - - Args: - width, height: Size of the space. - density: Initial Chance for a cell to populated - minority_pc: Chances for an agent to be in minority class - homophily: Minimum number of agents of same class needed to be happy - radius: Search radius for checking similarity - seed: Seed for Reproducibility - """ - super().__init__(seed=seed) - self.homophily = homophily - self.radius = radius - - self.grid = mesa.space.SingleGrid(width, height, torus=True) - - self.happy = 0 - self.datacollector = mesa.DataCollector( - model_reporters={"happy": "happy"}, # Model-level count of happy agents - ) - - # Set up agents - # We use a grid iterator that returns - # the coordinates of a cell as well as - # its contents. (coord_iter) - for _, pos in self.grid.coord_iter(): - if self.random.random() < density: - agent_type = 1 if self.random.random() < minority_pc else 0 - agent = SchellingAgent(self, agent_type) - self.grid.place_agent(agent, pos) - - self.datacollector.collect(self) - - def step(self): - """Run one step of the model.""" - self.happy = 0 # Reset counter of happy agents - self.agents.shuffle_do("step") - - self.datacollector.collect(self) - - self.running = self.happy != len(self.agents) - -``` \ No newline at end of file diff --git a/docs/examples/virus_on_network.md b/docs/examples/virus_on_network.md deleted file mode 100644 index 7d338a3a41f..00000000000 --- a/docs/examples/virus_on_network.md +++ /dev/null @@ -1,243 +0,0 @@ - -# Virus on a Network - -## Summary - -This model is based on the NetLogo model "Virus on Network". It demonstrates the spread of a virus through a network and follows the SIR model, commonly seen in epidemiology. - -The SIR model is one of the simplest compartmental models, and many models are derivatives of this basic form. The model consists of three compartments: - -S: The number of susceptible individuals. When a susceptible and an infectious individual come into "infectious contact", the susceptible individual contracts the disease and transitions to the infectious compartment. -I: The number of infectious individuals. These are individuals who have been infected and are capable of infecting susceptible individuals. -R for the number of removed (and immune) or deceased individuals. These are individuals who have been infected and have either recovered from the disease and entered the removed compartment, or died. It is assumed that the number of deaths is negligible with respect to the total population. This compartment may also be called "recovered" or "resistant". - -For more information about this model, read the NetLogo's web page: http://ccl.northwestern.edu/netlogo/models/VirusonaNetwork. - -JavaScript library used in this example to render the network: [d3.js](https://d3js.org/). - -## 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. - -or - -Directly run the file ``run.py`` in the terminal. e.g. - -``` - $ python run.py -``` - - -## Files - -* ``model.py``: Contains the agent class, and the overall model class. -* ``agents.py``: Contains the agent class. -* ``app.py``: Contains the code for the interactive Solara visualization. - -## 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 - - -[Stonedahl, F. and Wilensky, U. (2008). NetLogo Virus on a Network model](http://ccl.northwestern.edu/netlogo/models/VirusonaNetwork). -Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. - - -[Wilensky, U. (1999). NetLogo](http://ccl.northwestern.edu/netlogo/) -Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. - - -# Agents - -```python -from enum import Enum - -from mesa import Agent - - -class State(Enum): - SUSCEPTIBLE = 0 - INFECTED = 1 - RESISTANT = 2 - - -class VirusAgent(Agent): - """Individual Agent definition and its properties/interaction methods.""" - - def __init__( - self, - model, - initial_state, - virus_spread_chance, - virus_check_frequency, - recovery_chance, - gain_resistance_chance, - ): - super().__init__(model) - - self.state = initial_state - - self.virus_spread_chance = virus_spread_chance - self.virus_check_frequency = virus_check_frequency - self.recovery_chance = recovery_chance - self.gain_resistance_chance = gain_resistance_chance - - def try_to_infect_neighbors(self): - neighbors_nodes = self.model.grid.get_neighborhood( - self.pos, include_center=False - ) - susceptible_neighbors = [ - agent - for agent in self.model.grid.get_cell_list_contents(neighbors_nodes) - if agent.state is State.SUSCEPTIBLE - ] - for a in susceptible_neighbors: - if self.random.random() < self.virus_spread_chance: - a.state = State.INFECTED - - def try_gain_resistance(self): - if self.random.random() < self.gain_resistance_chance: - self.state = State.RESISTANT - - def try_remove_infection(self): - # Try to remove - if self.random.random() < self.recovery_chance: - # Success - self.state = State.SUSCEPTIBLE - self.try_gain_resistance() - else: - # Failed - self.state = State.INFECTED - - def try_check_situation(self): - if (self.random.random() < self.virus_check_frequency) and ( - self.state is State.INFECTED - ): - self.try_remove_infection() - - def step(self): - if self.state is State.INFECTED: - self.try_to_infect_neighbors() - self.try_check_situation() - -``` - - -# Model - -```python -import math - -import networkx as nx -from agents import State, VirusAgent - -import mesa -from mesa import Model - - -def number_state(model, state): - return sum(1 for a in model.grid.get_all_cell_contents() if a.state is state) - - -def number_infected(model): - return number_state(model, State.INFECTED) - - -def number_susceptible(model): - return number_state(model, State.SUSCEPTIBLE) - - -def number_resistant(model): - return number_state(model, State.RESISTANT) - - -class VirusOnNetwork(Model): - """A virus model with some number of agents.""" - - def __init__( - self, - num_nodes=10, - avg_node_degree=3, - initial_outbreak_size=1, - virus_spread_chance=0.4, - virus_check_frequency=0.4, - recovery_chance=0.3, - gain_resistance_chance=0.5, - ): - super().__init__() - self.num_nodes = num_nodes - prob = avg_node_degree / self.num_nodes - self.G = nx.erdos_renyi_graph(n=self.num_nodes, p=prob) - self.grid = mesa.space.NetworkGrid(self.G) - - self.initial_outbreak_size = ( - initial_outbreak_size if initial_outbreak_size <= num_nodes else num_nodes - ) - self.virus_spread_chance = virus_spread_chance - self.virus_check_frequency = virus_check_frequency - self.recovery_chance = recovery_chance - self.gain_resistance_chance = gain_resistance_chance - - self.datacollector = mesa.DataCollector( - { - "Infected": number_infected, - "Susceptible": number_susceptible, - "Resistant": number_resistant, - } - ) - - # Create agents - for node in self.G.nodes(): - a = VirusAgent( - self, - State.SUSCEPTIBLE, - self.virus_spread_chance, - self.virus_check_frequency, - self.recovery_chance, - self.gain_resistance_chance, - ) - - # Add the agent to the node - self.grid.place_agent(a, node) - - # Infect some nodes - infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size) - for a in self.grid.get_cell_list_contents(infected_nodes): - a.state = State.INFECTED - - self.running = True - self.datacollector.collect(self) - - def resistant_susceptible_ratio(self): - try: - return number_state(self, State.RESISTANT) / number_state( - self, State.SUSCEPTIBLE - ) - except ZeroDivisionError: - return math.inf - - def step(self): - self.agents.shuffle_do("step") - # collect data - self.datacollector.collect(self) - - def run_model(self, n): - for _ in range(n): - self.step() - -``` \ No newline at end of file diff --git a/examples/basic/virus_on_network/Readme..md b/examples/basic/virus_on_network/Readme.md similarity index 100% rename from examples/basic/virus_on_network/Readme..md rename to examples/basic/virus_on_network/Readme.md From 962e794b651b56afdeb8861e9ed694c49b6ac306 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 17 Oct 2024 16:40:21 +0200 Subject: [PATCH 12/34] further updates --- docs/conf.py | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9a1055f54d9..aa9dbef8411 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -361,7 +361,7 @@ def setup_examples_pages(): with open(os.path.join(HERE, "examples.md"), "w") as fh: content = template.substitute( dict( - examples="\n".join([f"<{entry[2::]}>" for entry in examples_md]), + examples="\n".join([f"{entry} <{entry[2::]}>" for entry in examples_md]), ) ) fh.write(content) diff --git a/docs/index.md b/docs/index.md index 77e40cfd900..fd3d2d936e8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -88,7 +88,7 @@ ABM features users have shared that you may want to use in your model Mesa Overview tutorials/intro_tutorial -examples +Examples Migration guide Best Practices How-to Guide From ee662b1507d2d8682b6e378f7ee12604637db77a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 17 Oct 2024 17:55:39 +0200 Subject: [PATCH 13/34] Update examples_overview_template.txt --- docs/examples_overview_template.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples_overview_template.txt b/docs/examples_overview_template.txt index 611959e91a5..6ec38f12362 100644 --- a/docs/examples_overview_template.txt +++ b/docs/examples_overview_template.txt @@ -3,7 +3,7 @@ ```{toctree} -:maxdepth: 2 +:maxdepth: 1 $examples From fd4bfaad483c5d44fd586516cc8fc7765fc9c386 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 18 Oct 2024 07:50:40 +0200 Subject: [PATCH 14/34] Update example_template.txt --- docs/example_template.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/example_template.txt b/docs/example_template.txt index 6fcc5c06a0a..33103502640 100644 --- a/docs/example_template.txt +++ b/docs/example_template.txt @@ -1,14 +1,15 @@ +## Description $readme_file -# Agents +## Agents ```python $agent_file ``` -# Model +## Model ```python $model_file From 3f0c3e600062162ad3e54f092cc91d0bc80ee031 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 18 Oct 2024 07:54:29 +0200 Subject: [PATCH 15/34] add app.py into docs --- docs/conf.py | 25 +++-- docs/example_template.txt | 7 ++ docs/examples/boid_flockers.md | 198 --------------------------------- 3 files changed, 20 insertions(+), 210 deletions(-) delete mode 100644 docs/examples/boid_flockers.md diff --git a/docs/conf.py b/docs/conf.py index aa9dbef8411..99d9b3f64f3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -317,33 +317,34 @@ def setup_examples_pages(): with open(os.path.join(HERE, "example_template.txt")) as fh: template = string.Template(fh.read()) - # TODO:: at the moment no idea what happens if example is updated. Does this trigger a rebuild of the html page? - # probably not... because file is not new. So we need some timestamp trick? examples_md = [] for example in examples: - # fixme we have the directories - # from the directory, get 3 file names base_name = os.path.basename(os.path.normpath(example)) agent_filename = os.path.join(example, "agents.py") model_filename = os.path.join(example, "model.py") readme_filename = os.path.join(example, "Readme.md") + app_filename = os.path.join(example, "app.py") md_filename = f"{base_name}.md" examples_md.append(f"./examples/{base_name}") - with open(agent_filename) as content_file: - agent_file = content_file.read() - with open(model_filename) as content_file: - model_file = content_file.read() - with open(readme_filename) as content_file: - readme_file = content_file.read() - + # fixme should be replaced with something based on timestep + # so if any(mymodelfiles) is newer then existing_md_filename if md_filename not in md_files: + with open(agent_filename) as content_file: + agent_file = content_file.read() + with open(model_filename) as content_file: + model_file = content_file.read() + with open(readme_filename) as content_file: + readme_file = content_file.read() + with open(app_filename) as content_file: + app_file = content_file.read() + with open(os.path.join(HERE, "examples", md_filename), "w") as fh: content = template.substitute( dict(agent_file=agent_file, model_file=model_file, - readme_file=readme_file) + readme_file=readme_file, app_file=app_file) ) fh.write(content) else: diff --git a/docs/example_template.txt b/docs/example_template.txt index 33103502640..0c4687da658 100644 --- a/docs/example_template.txt +++ b/docs/example_template.txt @@ -13,4 +13,11 @@ $agent_file ```python $model_file +``` + + +## App + +```python +$app_file ``` \ No newline at end of file diff --git a/docs/examples/boid_flockers.md b/docs/examples/boid_flockers.md deleted file mode 100644 index 52f08995e9f..00000000000 --- a/docs/examples/boid_flockers.md +++ /dev/null @@ -1,198 +0,0 @@ - -# Boids Flockers - -## Summary - -An implementation of Craig Reynolds's Boids flocker model. Agents (simulated birds) try to fly towards the average position of their neighbors and in the same direction as them, while maintaining a minimum distance. This produces flocking behavior. - -This model tests Mesa's continuous space feature, and uses numpy arrays to represent vectors. It also demonstrates how to create custom visualization components. - -## 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 launch the visualization interactively, run ``mesa runserver`` in this directory. e.g. - -``` -$ mesa runserver -``` - -or - -Directly run the file ``run.py`` in the terminal. e.g. - -``` - $ python run.py -``` - -* Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. - -## Files - -* [model.py](model.py): Core model file; contains the Boid Model and Boid Agent class. -* [app.py](app.py): Visualization code. - -## Further Reading - -The following link can be visited for more information on the boid flockers model: -https://cs.stanford.edu/people/eroberts/courses/soco/projects/2008-09/modeling-natural-systems/boids.html - - -# Agents - -```python -import numpy as np - -from mesa import Agent - - -class Boid(Agent): - """A Boid-style flocker agent. - - The agent follows three behaviors to flock: - - Cohesion: steering towards neighboring agents. - - Separation: avoiding getting too close to any other agent. - - Alignment: try to fly in the same direction as the neighbors. - - Boids have a vision that defines the radius in which they look for their - neighbors to flock with. Their speed (a scalar) and direction (a vector) - define their movement. Separation is their desired minimum distance from - any other Boid. - """ - - def __init__( - self, - model, - speed, - direction, - vision, - separation, - cohere=0.03, - separate=0.015, - match=0.05, - ): - """Create a new Boid flocker agent. - - Args: - speed: Distance to move per step. - direction: numpy vector for the Boid's direction of movement. - vision: Radius to look around for nearby Boids. - separation: Minimum distance to maintain from other Boids. - cohere: the relative importance of matching neighbors' positions - separate: the relative importance of avoiding close neighbors - match: the relative importance of matching neighbors' headings - """ - super().__init__(model) - self.speed = speed - self.direction = direction - self.vision = vision - self.separation = separation - self.cohere_factor = cohere - self.separate_factor = separate - self.match_factor = match - self.neighbors = None - - def step(self): - """Get the Boid's neighbors, compute the new vector, and move accordingly.""" - self.neighbors = self.model.space.get_neighbors(self.pos, self.vision, False) - n = 0 - match_vector, separation_vector, cohere = np.zeros((3, 2)) - for neighbor in self.neighbors: - n += 1 - heading = self.model.space.get_heading(self.pos, neighbor.pos) - cohere += heading - if self.model.space.get_distance(self.pos, neighbor.pos) < self.separation: - separation_vector -= heading - match_vector += neighbor.direction - n = max(n, 1) - cohere = cohere * self.cohere_factor - separation_vector = separation_vector * self.separate_factor - match_vector = match_vector * self.match_factor - self.direction += (cohere + separation_vector + match_vector) / n - self.direction /= np.linalg.norm(self.direction) - new_pos = self.pos + self.direction * self.speed - self.model.space.move_agent(self, new_pos) - -``` - - -# Model - -```python -"""Flockers. -============================================================= -A Mesa implementation of Craig Reynolds's Boids flocker model. -Uses numpy arrays to represent vectors. -""" - -import numpy as np -from agents import Boid - -import mesa - - -class BoidFlockers(mesa.Model): - """Flocker model class. Handles agent creation, placement and scheduling.""" - - def __init__( - self, - seed=None, - population=100, - width=100, - height=100, - vision=10, - speed=1, - separation=1, - cohere=0.03, - separate=0.015, - match=0.05, - ): - """Create a new Flockers model. - - Args: - population: Number of Boids - width, height: Size of the space. - speed: How fast should the Boids move. - vision: How far around should each Boid look for its neighbors - separation: What's the minimum distance each Boid will attempt to - keep from any other - cohere, separate, match: factors for the relative importance of - the three drives. - """ - super().__init__(seed=seed) - self.population = population - self.vision = vision - self.speed = speed - self.separation = separation - - self.space = mesa.space.ContinuousSpace(width, height, True) - self.factors = {"cohere": cohere, "separate": separate, "match": match} - self.make_agents() - - def make_agents(self): - """Create self.population agents, with random positions and starting headings.""" - for _ in range(self.population): - x = self.random.random() * self.space.x_max - y = self.random.random() * self.space.y_max - pos = np.array((x, y)) - direction = np.random.random(2) * 2 - 1 - boid = Boid( - model=self, - speed=self.speed, - direction=direction, - vision=self.vision, - separation=self.separation, - **self.factors, - ) - self.space.place_agent(boid, pos) - - def step(self): - self.agents.shuffle_do("step") - -``` \ No newline at end of file From 1515f2994df58292169801c4b0d5dc7da2d43be9 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 18 Oct 2024 08:03:17 +0200 Subject: [PATCH 16/34] Update app.py --- examples/basic/conways_game_of_life/{st_app.py => app.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/basic/conways_game_of_life/{st_app.py => app.py} (100%) diff --git a/examples/basic/conways_game_of_life/st_app.py b/examples/basic/conways_game_of_life/app.py similarity index 100% rename from examples/basic/conways_game_of_life/st_app.py rename to examples/basic/conways_game_of_life/app.py From 5759cb33035b3d366287ae35c03c4ed79fdf628b Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 18 Oct 2024 08:37:15 +0200 Subject: [PATCH 17/34] remove inits from api docs --- docs/apis/experimental.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/apis/experimental.md b/docs/apis/experimental.md index d5f662c0620..c74b4fe43a4 100644 --- a/docs/apis/experimental.md +++ b/docs/apis/experimental.md @@ -3,11 +3,6 @@ This namespace contains experimental features. These are under development, and ## Cell Space -```{eval-rst} -.. automodule:: experimental.cell_space.__init__ - :members: -``` - ```{eval-rst} .. automodule:: experimental.cell_space.cell :members: @@ -40,11 +35,6 @@ This namespace contains experimental features. These are under development, and ## Devs -```{eval-rst} -.. automodule:: experimental.devs.__init__ - :members: -``` - ```{eval-rst} .. automodule:: experimental.devs.eventlist :members: From f5070b701e752409ecb68f9941a7a2698b44309d Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 18 Oct 2024 08:44:00 +0200 Subject: [PATCH 18/34] updates --- docs/examples.md | 12 +- docs/examples/boid_flockers.md | 266 ++++++++++++++++ docs/examples/boltzmann_wealth_model.md | 224 ++++++++++++++ docs/examples/conways_game_of_life.md | 208 +++++++++++++ docs/examples/schelling.md | 199 ++++++++++++ docs/examples/virus_on_network.md | 385 ++++++++++++++++++++++++ 6 files changed, 1288 insertions(+), 6 deletions(-) create mode 100644 docs/examples/boid_flockers.md create mode 100644 docs/examples/boltzmann_wealth_model.md create mode 100644 docs/examples/conways_game_of_life.md create mode 100644 docs/examples/schelling.md create mode 100644 docs/examples/virus_on_network.md diff --git a/docs/examples.md b/docs/examples.md index 1ee87de8fe4..75b4b14b932 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -3,12 +3,12 @@ ```{toctree} -:maxdepth: 2 +:maxdepth: 1 - - - - - +./examples/boid_flockers +./examples/virus_on_network +./examples/conways_game_of_life +./examples/schelling +./examples/boltzmann_wealth_model ``` \ No newline at end of file diff --git a/docs/examples/boid_flockers.md b/docs/examples/boid_flockers.md new file mode 100644 index 00000000000..bb285177ddc --- /dev/null +++ b/docs/examples/boid_flockers.md @@ -0,0 +1,266 @@ + +## Description +# Boids Flockers + +## Summary + +An implementation of Craig Reynolds's Boids flocker model. Agents (simulated birds) try to fly towards the average position of their neighbors and in the same direction as them, while maintaining a minimum distance. This produces flocking behavior. + +This model tests Mesa's continuous space feature, and uses numpy arrays to represent vectors. It also demonstrates how to create custom visualization components. + +## 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 launch the visualization interactively, run ``mesa runserver`` in this directory. e.g. + +``` +$ mesa runserver +``` + +or + +Directly run the file ``run.py`` in the terminal. e.g. + +``` + $ python run.py +``` + +* Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. + +## Files + +* [model.py](model.py): Core model file; contains the Boid Model and Boid Agent class. +* [app.py](app.py): Visualization code. + +## Further Reading + +The following link can be visited for more information on the boid flockers model: +https://cs.stanford.edu/people/eroberts/courses/soco/projects/2008-09/modeling-natural-systems/boids.html + + +## Agents + +```python +import numpy as np + +from mesa import Agent + + +class Boid(Agent): + """A Boid-style flocker agent. + + The agent follows three behaviors to flock: + - Cohesion: steering towards neighboring agents. + - Separation: avoiding getting too close to any other agent. + - Alignment: try to fly in the same direction as the neighbors. + + Boids have a vision that defines the radius in which they look for their + neighbors to flock with. Their speed (a scalar) and direction (a vector) + define their movement. Separation is their desired minimum distance from + any other Boid. + """ + + def __init__( + self, + model, + speed, + direction, + vision, + separation, + cohere=0.03, + separate=0.015, + match=0.05, + ): + """Create a new Boid flocker agent. + + Args: + speed: Distance to move per step. + direction: numpy vector for the Boid's direction of movement. + vision: Radius to look around for nearby Boids. + separation: Minimum distance to maintain from other Boids. + cohere: the relative importance of matching neighbors' positions + separate: the relative importance of avoiding close neighbors + match: the relative importance of matching neighbors' headings + """ + super().__init__(model) + self.speed = speed + self.direction = direction + self.vision = vision + self.separation = separation + self.cohere_factor = cohere + self.separate_factor = separate + self.match_factor = match + self.neighbors = None + + def step(self): + """Get the Boid's neighbors, compute the new vector, and move accordingly.""" + self.neighbors = self.model.space.get_neighbors(self.pos, self.vision, False) + n = 0 + match_vector, separation_vector, cohere = np.zeros((3, 2)) + for neighbor in self.neighbors: + n += 1 + heading = self.model.space.get_heading(self.pos, neighbor.pos) + cohere += heading + if self.model.space.get_distance(self.pos, neighbor.pos) < self.separation: + separation_vector -= heading + match_vector += neighbor.direction + n = max(n, 1) + cohere = cohere * self.cohere_factor + separation_vector = separation_vector * self.separate_factor + match_vector = match_vector * self.match_factor + self.direction += (cohere + separation_vector + match_vector) / n + self.direction /= np.linalg.norm(self.direction) + new_pos = self.pos + self.direction * self.speed + self.model.space.move_agent(self, new_pos) + +``` + + +## Model + +```python +"""Flockers. +============================================================= +A Mesa implementation of Craig Reynolds's Boids flocker model. +Uses numpy arrays to represent vectors. +""" + +import numpy as np + +import mesa + +from .agents import Boid + + +class BoidFlockers(mesa.Model): + """Flocker model class. Handles agent creation, placement and scheduling.""" + + def __init__( + self, + seed=None, + population=100, + width=100, + height=100, + vision=10, + speed=1, + separation=1, + cohere=0.03, + separate=0.015, + match=0.05, + ): + """Create a new Flockers model. + + Args: + population: Number of Boids + width, height: Size of the space. + speed: How fast should the Boids move. + vision: How far around should each Boid look for its neighbors + separation: What's the minimum distance each Boid will attempt to + keep from any other + cohere, separate, match: factors for the relative importance of + the three drives. + """ + super().__init__(seed=seed) + self.population = population + self.vision = vision + self.speed = speed + self.separation = separation + + self.space = mesa.space.ContinuousSpace(width, height, True) + self.factors = {"cohere": cohere, "separate": separate, "match": match} + self.make_agents() + + def make_agents(self): + """Create self.population agents, with random positions and starting headings.""" + for _ in range(self.population): + x = self.random.random() * self.space.x_max + y = self.random.random() * self.space.y_max + pos = np.array((x, y)) + direction = np.random.random(2) * 2 - 1 + boid = Boid( + model=self, + speed=self.speed, + direction=direction, + vision=self.vision, + separation=self.separation, + **self.factors, + ) + self.space.place_agent(boid, pos) + + def step(self): + self.agents.shuffle_do("step") + +``` + + +## App + +```python +from mesa.visualization import Slider, SolaraViz, make_space_matplotlib + +from .model import BoidFlockers + + +def boid_draw(agent): + if not agent.neighbors: # Only for the first Frame + neighbors = len(agent.model.space.get_neighbors(agent.pos, agent.vision, False)) + else: + neighbors = len(agent.neighbors) + + if neighbors <= 1: + return {"color": "red", "size": 20} + elif neighbors >= 2: + return {"color": "green", "size": 20} + + +model_params = { + "population": Slider( + label="Number of boids", + value=100, + min=10, + max=200, + step=10, + ), + "width": 100, + "height": 100, + "speed": Slider( + label="Speed of Boids", + value=5, + min=1, + max=20, + step=1, + ), + "vision": Slider( + label="Vision of Bird (radius)", + value=10, + min=1, + max=50, + step=1, + ), + "separation": Slider( + label="Minimum Separation", + value=2, + min=1, + max=20, + step=1, + ), +} + +model = BoidFlockers() + +page = SolaraViz( + model, + [make_space_matplotlib(agent_portrayal=boid_draw)], + model_params=model_params, + name="Boid Flocking Model", +) +page # noqa + +``` \ No newline at end of file diff --git a/docs/examples/boltzmann_wealth_model.md b/docs/examples/boltzmann_wealth_model.md new file mode 100644 index 00000000000..df17f9eee08 --- /dev/null +++ b/docs/examples/boltzmann_wealth_model.md @@ -0,0 +1,224 @@ + +## Description +# 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) + +Make sure to install the requirements first: + +``` + $ pip install -r requirements.txt +``` + +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: + +``` + $ solara run app.py +``` + +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 + +* ``model.py``: Final version of the model. +* ``app.py``: Code for the interactive visualization. + +## Optional + +An optional visualization is also provided using Streamlit, which is another popular Python library for creating interactive web applications. + +To run the Streamlit app, you will need to install the `streamlit` and `altair` libraries: + +``` + $ pip install streamlit altair +``` + +Then, you can run the Streamlit app using the following command: + +``` + $ streamlit run st_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) + + +## Agents + +```python +from mesa import Agent + + +class MoneyAgent(Agent): + """An agent with fixed initial wealth.""" + + def __init__(self, model): + super().__init__(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() + +``` + + +## Model + +```python +import mesa + +from .agents import MoneyAgent + + +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): + super().__init__() + self.num_agents = n + self.grid = mesa.space.MultiGrid(width, height, True) + + self.datacollector = mesa.DataCollector( + model_reporters={"Gini": self.compute_gini}, + agent_reporters={"Wealth": "wealth"}, + ) + # Create agents + for _ in range(self.num_agents): + a = MoneyAgent(self) + + # Add the agent to a random grid cell + x = self.random.randrange(self.grid.width) + y = self.random.randrange(self.grid.height) + self.grid.place_agent(a, (x, y)) + + self.running = True + self.datacollector.collect(self) + + def step(self): + self.agents.shuffle_do("step") + self.datacollector.collect(self) + + def compute_gini(self): + agent_wealths = [agent.wealth for agent in self.agents] + x = sorted(agent_wealths) + n = self.num_agents + b = sum(xi * (n - i) for i, xi in enumerate(x)) / (n * sum(x)) + return 1 + (1 / n) - 2 * b + +``` + + +## App + +```python +from mesa.visualization import ( + SolaraViz, + make_plot_measure, + make_space_matplotlib, +) + +from .model import BoltzmannWealthModel + + +def agent_portrayal(agent): + size = 10 + color = "tab:red" + if agent.wealth > 0: + size = 50 + color = "tab:blue" + return {"size": size, "color": color} + + +model_params = { + "n": { + "type": "SliderInt", + "value": 50, + "label": "Number of agents:", + "min": 10, + "max": 100, + "step": 1, + }, + "width": 10, + "height": 10, +} + +# Create initial model instance +model1 = BoltzmannWealthModel(50, 10, 10) + +# Create visualization elements. The visualization elements are solara components +# that receive the model instance as a "prop" and display it in a certain way. +# Under the hood these are just classes that receive the model instance. +# You can also author your own visualization elements, which can also be functions +# that receive the model instance and return a valid solara component. +SpaceGraph = make_space_matplotlib(agent_portrayal) +GiniPlot = make_plot_measure("Gini") + +# Create the SolaraViz page. This will automatically create a server and display the +# visualization elements in a web browser. +# Display it using the following command in the example directory: +# solara run app.py +# It will automatically update and display any changes made to this file +page = SolaraViz( + model1, + components=[SpaceGraph, GiniPlot], + model_params=model_params, + name="Boltzmann Wealth Model", +) +page # noqa + + +# In a notebook environment, we can also display the visualization elements directly +# SpaceGraph(model1) +# GiniPlot(model1) + +# The plots will be static. If you want to pick up model steps, +# you have to make the model reactive first +# reactive_model = solara.reactive(model1) +# SpaceGraph(reactive_model) +# In a different notebook block: +# reactive_model.value.step() + +``` \ No newline at end of file diff --git a/docs/examples/conways_game_of_life.md b/docs/examples/conways_game_of_life.md new file mode 100644 index 00000000000..d0b1758e2ab --- /dev/null +++ b/docs/examples/conways_game_of_life.md @@ -0,0 +1,208 @@ + +## Description +# Conway's Game Of "Life" + +## Summary + +[The Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life), also known simply as "Life", is a cellular automaton devised by the British mathematician John Horton Conway in 1970. + +The "game" is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input by a human. One interacts with the Game of "Life" by creating an initial configuration and observing how it evolves, or, for advanced "players", by creating patterns with particular properties. + + +## 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 ``run``. + +## Files + +* ``agents.py``: Defines the behavior of an individual cell, which can be in two states: DEAD or ALIVE. +* ``model.py``: Defines the model itself, initialized with a random configuration of alive and dead cells. +* ``portrayal.py``: Describes for the front end how to render a cell. +* ``st_app.py``: Defines an interactive visualization using Streamlit. + +## Optional + +* ``conways_game_of_life/st_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 st_app.py`` + + +## Further Reading +[Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) + + +## Agents + +```python +from mesa import Agent + + +class Cell(Agent): + """Represents a single ALIVE or DEAD cell in the simulation.""" + + DEAD = 0 + ALIVE = 1 + + def __init__(self, pos, model, init_state=DEAD): + """Create a cell, in the given state, at the given x, y position.""" + super().__init__(model) + self.x, self.y = pos + self.state = init_state + self._nextState = None + + @property + def isAlive(self): + return self.state == self.ALIVE + + @property + def neighbors(self): + return self.model.grid.iter_neighbors((self.x, self.y), True) + + def determine_state(self): + """Compute if the cell will be dead or alive at the next tick. This is + based on the number of alive or dead neighbors. The state is not + changed here, but is just computed and stored in self._nextState, + because our current state may still be necessary for our neighbors + to calculate their next state. + """ + # Get the neighbors and apply the rules on whether to be alive or dead + # at the next tick. + live_neighbors = sum(neighbor.isAlive for neighbor in self.neighbors) + + # Assume nextState is unchanged, unless changed below. + self._nextState = self.state + if self.isAlive: + if live_neighbors < 2 or live_neighbors > 3: + self._nextState = self.DEAD + else: + if live_neighbors == 3: + self._nextState = self.ALIVE + + def assume_state(self): + """Set the state to the new computed state -- computed in step().""" + self.state = self._nextState + +``` + + +## Model + +```python +from mesa import Model +from mesa.space import SingleGrid + +from .agents import Cell + + +class ConwaysGameOfLife(Model): + """Represents the 2-dimensional array of cells in Conway's Game of Life.""" + + def __init__(self, width=50, height=50): + """Create a new playing area of (width, height) cells.""" + super().__init__() + # Use a simple grid, where edges wrap around. + self.grid = SingleGrid(width, height, torus=True) + + # Place a cell at each location, with some initialized to + # ALIVE and some to DEAD. + for _contents, (x, y) in self.grid.coord_iter(): + cell = Cell((x, y), self) + if self.random.random() < 0.1: + cell.state = cell.ALIVE + self.grid.place_agent(cell, (x, y)) + + self.running = True + + def step(self): + """Perform the model step in two stages: + - First, all cells assume their next state (whether they will be dead or alive) + - Then, all cells change state to their next state. + """ + self.agents.do("determine_state") + self.agents.do("assume_state") + +``` + + +## App + +```python +import time + +import altair as alt +import numpy as np +import pandas as pd +import streamlit as st +from model import ConwaysGameOfLife + +model = st.title("Conway's Game of Life") +num_ticks = st.slider("Select number of Steps", 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 = ConwaysGameOfLife(height, width) + +col1, col2, col3 = st.columns(3) +status_text = st.empty() +# step_mode = st.checkbox('Run Step-by-Step') +run = st.button("Run Simulation") + + +if run: + tick = time.time() + step = 0 + # init grid + df_grid = pd.DataFrame() + agent_counts = np.zeros((model.grid.width, model.grid.height)) + for x in range(width): + for y in range(height): + df_grid = pd.concat( + [df_grid, pd.DataFrame({"x": [x], "y": [y], "state": [0]})], + ignore_index=True, + ) + + heatmap = ( + alt.Chart(df_grid) + .mark_point(size=100) + .encode(x="x", y="y", color=alt.Color("state")) + .interactive() + .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, use_container_width=True) + color_scale = alt.Scale(domain=[0, 1], range=["red", "yellow"]) + for i in range(num_ticks): + model.step() + my_bar.progress((i / num_ticks), text="Simulation progress") + placeholder.text("Step = %d" % i) + for contents, (x, y) in model.grid.coord_iter(): + # print('x:',x,'y:',y, 'state:',contents) + selected_row = df_grid[(df_grid["x"] == x) & (df_grid["y"] == y)] + df_grid.loc[selected_row.index, "state"] = ( + contents.state + ) # random.choice([1,2]) + + heatmap = ( + alt.Chart(df_grid) + .mark_circle(size=100) + .encode(x="x", y="y", color=alt.Color("state", scale=color_scale)) + .interactive() + .properties(width=800, height=600) + ) + chart.altair_chart(heatmap) + + time.sleep(0.1) + + tock = time.time() + st.success(f"Simulation completed in {tock - tick:.2f} secs") + +``` \ No newline at end of file diff --git a/docs/examples/schelling.md b/docs/examples/schelling.md new file mode 100644 index 00000000000..3c879a31f7a --- /dev/null +++ b/docs/examples/schelling.md @@ -0,0 +1,199 @@ + +## Description +# 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, in this directory, run the following command + +``` + $ solara run app.py +``` + +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/). + +## 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 + +* ``app.py``: Code for the interactive visualization. +* ``schelling.py``: Contains the agent class, and the overall model class. +* ``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. + + +## Agents + +```python +from mesa import Agent, Model + + +class SchellingAgent(Agent): + """Schelling segregation agent.""" + + def __init__(self, model: Model, agent_type: int) -> None: + """Create a new Schelling agent. + + Args: + agent_type: Indicator for the agent's type (minority=1, majority=0) + """ + super().__init__(model) + self.type = agent_type + + def step(self) -> None: + neighbors = self.model.grid.iter_neighbors( + self.pos, moore=True, radius=self.model.radius + ) + similar = sum(1 for neighbor in neighbors if neighbor.type == self.type) + + # If unhappy, move: + if similar < self.model.homophily: + self.model.grid.move_to_empty(self) + else: + self.model.happy += 1 + +``` + + +## Model + +```python +import mesa +from mesa import Model + +from .agents import SchellingAgent + + +class Schelling(Model): + """Model class for the Schelling segregation model.""" + + def __init__( + self, + height=20, + width=20, + homophily=3, + radius=1, + density=0.8, + minority_pc=0.2, + seed=None, + ): + """Create a new Schelling model. + + Args: + width, height: Size of the space. + density: Initial Chance for a cell to populated + minority_pc: Chances for an agent to be in minority class + homophily: Minimum number of agents of same class needed to be happy + radius: Search radius for checking similarity + seed: Seed for Reproducibility + """ + super().__init__(seed=seed) + self.homophily = homophily + self.radius = radius + + self.grid = mesa.space.SingleGrid(width, height, torus=True) + + self.happy = 0 + self.datacollector = mesa.DataCollector( + model_reporters={"happy": "happy"}, # Model-level count of happy agents + ) + + # Set up agents + # We use a grid iterator that returns + # the coordinates of a cell as well as + # its contents. (coord_iter) + for _, pos in self.grid.coord_iter(): + if self.random.random() < density: + agent_type = 1 if self.random.random() < minority_pc else 0 + agent = SchellingAgent(self, agent_type) + self.grid.place_agent(agent, pos) + + self.datacollector.collect(self) + + def step(self): + """Run one step of the model.""" + self.happy = 0 # Reset counter of happy agents + self.agents.shuffle_do("step") + + self.datacollector.collect(self) + + self.running = self.happy != len(self.agents) + +``` + + +## App + +```python +import solara + +from mesa.visualization import ( + Slider, + SolaraViz, + make_plot_measure, + make_space_matplotlib, +) + +from .model import Schelling + + +def get_happy_agents(model): + """Display a text count of how many happy agents there are.""" + return solara.Markdown(f"**Happy agents: {model.happy}**") + + +def agent_portrayal(agent): + return {"color": "tab:orange" if agent.type == 0 else "tab:blue"} + + +model_params = { + "density": Slider("Agent density", 0.8, 0.1, 1.0, 0.1), + "minority_pc": Slider("Fraction minority", 0.2, 0.0, 1.0, 0.05), + "homophily": Slider("Homophily", 3, 0, 8, 1), + "width": 20, + "height": 20, +} + +model1 = Schelling(20, 20, 0.8, 0.2, 3) + +HappyPlot = make_plot_measure("happy") + +page = SolaraViz( + model1, + components=[ + make_space_matplotlib(agent_portrayal), + make_plot_measure("happy"), + get_happy_agents, + ], + model_params=model_params, +) +page # noqa + +``` \ No newline at end of file diff --git a/docs/examples/virus_on_network.md b/docs/examples/virus_on_network.md new file mode 100644 index 00000000000..88c9fd470eb --- /dev/null +++ b/docs/examples/virus_on_network.md @@ -0,0 +1,385 @@ + +## Description +# Virus on a Network + +## Summary + +This model is based on the NetLogo model "Virus on Network". It demonstrates the spread of a virus through a network and follows the SIR model, commonly seen in epidemiology. + +The SIR model is one of the simplest compartmental models, and many models are derivatives of this basic form. The model consists of three compartments: + +S: The number of susceptible individuals. When a susceptible and an infectious individual come into "infectious contact", the susceptible individual contracts the disease and transitions to the infectious compartment. +I: The number of infectious individuals. These are individuals who have been infected and are capable of infecting susceptible individuals. +R for the number of removed (and immune) or deceased individuals. These are individuals who have been infected and have either recovered from the disease and entered the removed compartment, or died. It is assumed that the number of deaths is negligible with respect to the total population. This compartment may also be called "recovered" or "resistant". + +For more information about this model, read the NetLogo's web page: http://ccl.northwestern.edu/netlogo/models/VirusonaNetwork. + +JavaScript library used in this example to render the network: [d3.js](https://d3js.org/). + +## 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. + +or + +Directly run the file ``run.py`` in the terminal. e.g. + +``` + $ python run.py +``` + + +## Files + +* ``model.py``: Contains the agent class, and the overall model class. +* ``agents.py``: Contains the agent class. +* ``app.py``: Contains the code for the interactive Solara visualization. + +## 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 + + +[Stonedahl, F. and Wilensky, U. (2008). NetLogo Virus on a Network model](http://ccl.northwestern.edu/netlogo/models/VirusonaNetwork). +Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. + + +[Wilensky, U. (1999). NetLogo](http://ccl.northwestern.edu/netlogo/) +Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. + + +## Agents + +```python +from enum import Enum + +from mesa import Agent + + +class State(Enum): + SUSCEPTIBLE = 0 + INFECTED = 1 + RESISTANT = 2 + + +class VirusAgent(Agent): + """Individual Agent definition and its properties/interaction methods.""" + + def __init__( + self, + model, + initial_state, + virus_spread_chance, + virus_check_frequency, + recovery_chance, + gain_resistance_chance, + ): + super().__init__(model) + + self.state = initial_state + + self.virus_spread_chance = virus_spread_chance + self.virus_check_frequency = virus_check_frequency + self.recovery_chance = recovery_chance + self.gain_resistance_chance = gain_resistance_chance + + def try_to_infect_neighbors(self): + neighbors_nodes = self.model.grid.get_neighborhood( + self.pos, include_center=False + ) + susceptible_neighbors = [ + agent + for agent in self.model.grid.get_cell_list_contents(neighbors_nodes) + if agent.state is State.SUSCEPTIBLE + ] + for a in susceptible_neighbors: + if self.random.random() < self.virus_spread_chance: + a.state = State.INFECTED + + def try_gain_resistance(self): + if self.random.random() < self.gain_resistance_chance: + self.state = State.RESISTANT + + def try_remove_infection(self): + # Try to remove + if self.random.random() < self.recovery_chance: + # Success + self.state = State.SUSCEPTIBLE + self.try_gain_resistance() + else: + # Failed + self.state = State.INFECTED + + def try_check_situation(self): + if (self.random.random() < self.virus_check_frequency) and ( + self.state is State.INFECTED + ): + self.try_remove_infection() + + def step(self): + if self.state is State.INFECTED: + self.try_to_infect_neighbors() + self.try_check_situation() + +``` + + +## Model + +```python +import math + +import networkx as nx + +import mesa +from mesa import Model + +from .agents import State, VirusAgent + + +def number_state(model, state): + return sum(1 for a in model.grid.get_all_cell_contents() if a.state is state) + + +def number_infected(model): + return number_state(model, State.INFECTED) + + +def number_susceptible(model): + return number_state(model, State.SUSCEPTIBLE) + + +def number_resistant(model): + return number_state(model, State.RESISTANT) + + +class VirusOnNetwork(Model): + """A virus model with some number of agents.""" + + def __init__( + self, + num_nodes=10, + avg_node_degree=3, + initial_outbreak_size=1, + virus_spread_chance=0.4, + virus_check_frequency=0.4, + recovery_chance=0.3, + gain_resistance_chance=0.5, + ): + super().__init__() + self.num_nodes = num_nodes + prob = avg_node_degree / self.num_nodes + self.G = nx.erdos_renyi_graph(n=self.num_nodes, p=prob) + self.grid = mesa.space.NetworkGrid(self.G) + + self.initial_outbreak_size = ( + initial_outbreak_size if initial_outbreak_size <= num_nodes else num_nodes + ) + self.virus_spread_chance = virus_spread_chance + self.virus_check_frequency = virus_check_frequency + self.recovery_chance = recovery_chance + self.gain_resistance_chance = gain_resistance_chance + + self.datacollector = mesa.DataCollector( + { + "Infected": number_infected, + "Susceptible": number_susceptible, + "Resistant": number_resistant, + } + ) + + # Create agents + for node in self.G.nodes(): + a = VirusAgent( + self, + State.SUSCEPTIBLE, + self.virus_spread_chance, + self.virus_check_frequency, + self.recovery_chance, + self.gain_resistance_chance, + ) + + # Add the agent to the node + self.grid.place_agent(a, node) + + # Infect some nodes + infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size) + for a in self.grid.get_cell_list_contents(infected_nodes): + a.state = State.INFECTED + + self.running = True + self.datacollector.collect(self) + + def resistant_susceptible_ratio(self): + try: + return number_state(self, State.RESISTANT) / number_state( + self, State.SUSCEPTIBLE + ) + except ZeroDivisionError: + return math.inf + + def step(self): + self.agents.shuffle_do("step") + # collect data + self.datacollector.collect(self) + + def run_model(self, n): + for _ in range(n): + self.step() + +``` + + +## App + +```python +import math + +import solara +from matplotlib.figure import Figure +from matplotlib.ticker import MaxNLocator + +from mesa.visualization import Slider, SolaraViz, make_space_matplotlib + +from .model import State, VirusOnNetwork, number_infected + + +def agent_portrayal(graph): + def get_agent(node): + return graph.nodes[node]["agent"][0] + + edge_width = [] + edge_color = [] + for u, v in graph.edges(): + agent1 = get_agent(u) + agent2 = get_agent(v) + w = 2 + ec = "#e8e8e8" + if State.RESISTANT in (agent1.state, agent2.state): + w = 3 + ec = "black" + edge_width.append(w) + edge_color.append(ec) + node_color_dict = { + State.INFECTED: "tab:red", + State.SUSCEPTIBLE: "tab:green", + State.RESISTANT: "tab:gray", + } + node_color = [node_color_dict[get_agent(node).state] for node in graph.nodes()] + return { + "width": edge_width, + "edge_color": edge_color, + "node_color": node_color, + } + + +def get_resistant_susceptible_ratio(model): + ratio = model.resistant_susceptible_ratio() + ratio_text = r"$\infty$" if ratio is math.inf else f"{ratio:.2f}" + infected_text = str(number_infected(model)) + + return f"Resistant/Susceptible Ratio: {ratio_text}
Infected Remaining: {infected_text}" + + +def make_plot(model): + # This is for the case when we want to plot multiple measures in 1 figure. + fig = Figure() + ax = fig.subplots() + measures = ["Infected", "Susceptible", "Resistant"] + colors = ["tab:red", "tab:green", "tab:gray"] + for i, m in enumerate(measures): + color = colors[i] + df = model.datacollector.get_model_vars_dataframe() + ax.plot(df.loc[:, m], label=m, color=color) + fig.legend() + # Set integer x axis + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + ax.set_xlabel("Step") + ax.set_ylabel("Number of Agents") + return solara.FigureMatplotlib(fig) + + +model_params = { + "num_nodes": Slider( + label="Number of agents", + value=10, + min=10, + max=100, + step=1, + ), + "avg_node_degree": Slider( + label="Avg Node Degree", + value=3, + min=3, + max=8, + step=1, + ), + "initial_outbreak_size": Slider( + label="Initial Outbreak Size", + value=1, + min=1, + max=10, + step=1, + ), + "virus_spread_chance": Slider( + label="Virus Spread Chance", + value=0.4, + min=0.0, + max=1.0, + step=0.1, + ), + "virus_check_frequency": Slider( + label="Virus Check Frequency", + value=0.4, + min=0.0, + max=1.0, + step=0.1, + ), + "recovery_chance": Slider( + label="Recovery Chance", + value=0.3, + min=0.0, + max=1.0, + step=0.1, + ), + "gain_resistance_chance": Slider( + label="Gain Resistance Chance", + value=0.5, + min=0.0, + max=1.0, + step=0.1, + ), +} + +SpacePlot = make_space_matplotlib(agent_portrayal) + +model1 = VirusOnNetwork() + +page = SolaraViz( + model1, + [ + SpacePlot, + make_plot, + # get_resistant_susceptible_ratio, # TODO: Fix and uncomment + ], + model_params=model_params, + name="Virus Model", +) +page # noqa + +``` \ No newline at end of file From b65fa0a80583a76e6910f434f4c36724b126ac1f Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 18 Oct 2024 09:18:27 +0200 Subject: [PATCH 19/34] hacky fix --- HISTORY.md | 2 +- docs/README.md | 9 ++-- docs/tutorials/visualization_tutorial.ipynb | 57 +++++++++++++++++---- examples/__init__.py | 1 + 4 files changed, 53 insertions(+), 16 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index f2e55b1a7ab..e08bd7098a5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,7 +5,7 @@ title: Release History # 3.0.0b1 (2024-10-17) ## Highlights -Basic exmaples are now importable in Mesa, includes boid_flockers, boltzmann_wealth_model, conways_game_of_life, schelling, and virus_on_network models. With this change they are also integrated into the Mesa tutorial in the docs. +Basic examples are now importable in Mesa, includes boid_flockers, boltzmann_wealth_model, conways_game_of_life, schelling, and virus_on_network models. With this change they are also integrated into the Mesa tutorial in the docs. Also, in this release, visualizations are improved by making visualization elements scalable and more clearly labeling the plots. diff --git a/docs/README.md b/docs/README.md index 0a85e858703..edd25eadaf4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,15 +1,14 @@ -Docs for Mesa -============= +# Docs for Mesa The readable version of the docs is hosted at [mesa.readthedocs.org](http://mesa.readthedocs.org/). This folder contains the docs that build the docs for the core mesa code on readthdocs. -### How to publish updates to the docs +## How to publish updates to the docs Updating docs can be confusing. Here are the basic setups. -##### Submit a pull request with updates +#### Submit a pull request with updates 1. Create branch (either via branching or fork of repo) -- try to use a descriptive name. * `git checkout -b doc-updates` 1. Update the docs. Save. @@ -23,7 +22,7 @@ Updating docs can be confusing. Here are the basic setups. * `git push origin doc-updates` 1. From here you will want to submit a pull request to main. -##### Update read the docs +#### Update read the docs From this point, you will need to find someone that has access to readthedocs. Currently, that is [@jackiekazil](https://github.com/jackiekazil), [@rht](https://github.com/rht), and [@tpike3](https://github.com/dmasad). diff --git a/docs/tutorials/visualization_tutorial.ipynb b/docs/tutorials/visualization_tutorial.ipynb index c4bfda6eb74..15cdbcd6f17 100644 --- a/docs/tutorials/visualization_tutorial.ipynb +++ b/docs/tutorials/visualization_tutorial.ipynb @@ -35,21 +35,58 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { - "tags": [] + "tags": [], + "ExecuteTime": { + "end_time": "2024-10-18T07:17:09.866711Z", + "start_time": "2024-10-18T07:17:09.429185Z" + } }, - "outputs": [], "source": [ "# Install and import the latest Mesa pre-release version\n", - "%pip install --quiet --upgrade --pre mesa\n", + "# %pip install --quiet --upgrade --pre mesa\n", "import mesa\n", - "print(f\"Mesa version: {mesa.__version__}\")\n", - "\n", - "# You can either define the BoltzmannWealthModel (aka MoneyModel) or install it\n", - "\n", - "from mesa.examples.basic import BoltzmannWealthModel" - ] + "print(f\"Mesa version: {mesa.__version__}\")" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mesa version: 3.0.0b1\n" + ] + } + ], + "execution_count": 1 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-18T07:17:11.592085Z", + "start_time": "2024-10-18T07:17:11.293216Z" + } + }, + "cell_type": "code", + "source": [ + "try:\n", + " from mesa.examples.basic import BoltzmannWealthModel\n", + "except ImportError:\n", + " from MoneyModel import MoneyModel as BoltzmannWealthModel\n" + ], + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'mesa.examples.basic'", + "output_type": "error", + "traceback": [ + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mModuleNotFoundError\u001B[0m Traceback (most recent call last)", + "Cell \u001B[0;32mIn[2], line 1\u001B[0m\n\u001B[0;32m----> 1\u001B[0m \u001B[38;5;28;01mfrom\u001B[39;00m \u001B[38;5;21;01mmesa\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01mexamples\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01mbasic\u001B[39;00m \u001B[38;5;28;01mimport\u001B[39;00m BoltzmannWealthModel\n", + "\u001B[0;31mModuleNotFoundError\u001B[0m: No module named 'mesa.examples.basic'" + ] + } + ], + "execution_count": 2 }, { "cell_type": "markdown", diff --git a/examples/__init__.py b/examples/__init__.py index e69de29bb2d..d97882b4077 100644 --- a/examples/__init__.py +++ b/examples/__init__.py @@ -0,0 +1 @@ +import basic From 6ec96871e05e732bfcffbb7f60b47d1bd86db6f0 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 18 Oct 2024 09:25:40 +0200 Subject: [PATCH 20/34] Update visualization_tutorial.ipynb --- docs/tutorials/visualization_tutorial.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/visualization_tutorial.ipynb b/docs/tutorials/visualization_tutorial.ipynb index 15cdbcd6f17..8a3c3bcb241 100644 --- a/docs/tutorials/visualization_tutorial.ipynb +++ b/docs/tutorials/visualization_tutorial.ipynb @@ -70,7 +70,7 @@ "source": [ "try:\n", " from mesa.examples.basic import BoltzmannWealthModel\n", - "except ImportError:\n", + "except ModuleNotFoundError:\n", " from MoneyModel import MoneyModel as BoltzmannWealthModel\n" ], "outputs": [ From 4bbf000bd1b8d59a372a14fcf23732971f2cadee Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 18 Oct 2024 09:31:28 +0200 Subject: [PATCH 21/34] cleanup --- docs/example_template.txt | 1 - examples/__init__.py | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/example_template.txt b/docs/example_template.txt index 0c4687da658..1cbb67015e9 100644 --- a/docs/example_template.txt +++ b/docs/example_template.txt @@ -1,5 +1,4 @@ -## Description $readme_file ## Agents diff --git a/examples/__init__.py b/examples/__init__.py index d97882b4077..e69de29bb2d 100644 --- a/examples/__init__.py +++ b/examples/__init__.py @@ -1 +0,0 @@ -import basic From 962c0cc9ca3b037cc6cdedf9d9f704942677f2f7 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 18 Oct 2024 09:54:14 +0200 Subject: [PATCH 22/34] updated files --- docs/examples/boid_flockers.md | 1 - docs/examples/boltzmann_wealth_model.md | 1 - docs/examples/conways_game_of_life.md | 1 - docs/examples/schelling.md | 1 - docs/examples/virus_on_network.md | 1 - 5 files changed, 5 deletions(-) diff --git a/docs/examples/boid_flockers.md b/docs/examples/boid_flockers.md index bb285177ddc..4d463f98da7 100644 --- a/docs/examples/boid_flockers.md +++ b/docs/examples/boid_flockers.md @@ -1,5 +1,4 @@ -## Description # Boids Flockers ## Summary diff --git a/docs/examples/boltzmann_wealth_model.md b/docs/examples/boltzmann_wealth_model.md index df17f9eee08..d54801afd0f 100644 --- a/docs/examples/boltzmann_wealth_model.md +++ b/docs/examples/boltzmann_wealth_model.md @@ -1,5 +1,4 @@ -## Description # Boltzmann Wealth Model (Tutorial) ## Summary diff --git a/docs/examples/conways_game_of_life.md b/docs/examples/conways_game_of_life.md index d0b1758e2ab..289aed7cd9f 100644 --- a/docs/examples/conways_game_of_life.md +++ b/docs/examples/conways_game_of_life.md @@ -1,5 +1,4 @@ -## Description # Conway's Game Of "Life" ## Summary diff --git a/docs/examples/schelling.md b/docs/examples/schelling.md index 3c879a31f7a..24f6e3c2aeb 100644 --- a/docs/examples/schelling.md +++ b/docs/examples/schelling.md @@ -1,5 +1,4 @@ -## Description # Schelling Segregation Model ## Summary diff --git a/docs/examples/virus_on_network.md b/docs/examples/virus_on_network.md index 88c9fd470eb..0c37df4148b 100644 --- a/docs/examples/virus_on_network.md +++ b/docs/examples/virus_on_network.md @@ -1,5 +1,4 @@ -## Description # Virus on a Network ## Summary From 6ce1e0a4ffcde522e087227582dc5bbbe3e6801a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 18 Oct 2024 10:13:48 +0200 Subject: [PATCH 23/34] updates --- docs/conf.py | 10 +++++----- docs/examples.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 99d9b3f64f3..3c40e49cf9d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -327,7 +327,7 @@ def setup_examples_pages(): app_filename = os.path.join(example, "app.py") md_filename = f"{base_name}.md" - examples_md.append(f"./examples/{base_name}") + examples_md.append((base_name, f"./examples/{base_name}")) # fixme should be replaced with something based on timestep # so if any(mymodelfiles) is newer then existing_md_filename @@ -362,7 +362,7 @@ def setup_examples_pages(): with open(os.path.join(HERE, "examples.md"), "w") as fh: content = template.substitute( dict( - examples="\n".join([f"{entry} <{entry[2::]}>" for entry in examples_md]), + examples="\n".join([f"{' '.join(base_name.split('_'))} <{file_path[2::]}>" for base_name, file_path in examples_md]), ) ) fh.write(content) @@ -373,6 +373,6 @@ def setup(app): # shutil.copy(osp.join(HERE, "..", "..", "CHANGELOG.md"), dest) setup_examples_pages() -# -# if __name__ == "__main__": -# setup_examples_pages() \ No newline at end of file + +if __name__ == "__main__": + setup_examples_pages() \ No newline at end of file diff --git a/docs/examples.md b/docs/examples.md index 75b4b14b932..f2089e385ed 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -5,10 +5,10 @@ ```{toctree} :maxdepth: 1 -./examples/boid_flockers -./examples/virus_on_network -./examples/conways_game_of_life -./examples/schelling -./examples/boltzmann_wealth_model +boid flockers +virus on network +conways game of life +schelling +boltzmann wealth model ``` \ No newline at end of file From 14f71042878d39d483f4b5248443bc02b8422a14 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 18 Oct 2024 14:14:47 +0200 Subject: [PATCH 24/34] automated detection of updates to example files --- docs/conf.py | 71 ++++++++++--------- docs/examples.md | 10 +-- docs/examples/conways_game_of_life.md | 16 ++--- examples/basic/conways_game_of_life/agents.py | 16 ++--- 4 files changed, 58 insertions(+), 55 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 3c40e49cf9d..bac6aaed10b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -296,20 +296,33 @@ +def write_example_md_file(agent_filename, model_filename, readme_filename, app_filename, md_filepath, template): + with open(agent_filename) as content_file: + agent_file = content_file.read() + with open(model_filename) as content_file: + model_file = content_file.read() + with open(readme_filename) as content_file: + readme_file = content_file.read() + with open(app_filename) as content_file: + app_file = content_file.read() + + with open(md_filepath, "w") as fh: + content = template.substitute( + dict(agent_file=agent_file, model_file=model_file, + readme_file=readme_file, app_file=app_file) + ) + fh.write(content) + def setup_examples_pages(): - # create rst files for all examples + # create md files for all examples # check what examples exist examples_folder = osp.abspath(osp.join(HERE, "..", "examples")) - - # fixme shift to walkdir - # we should have a single rst page for each subdirectory - basic_examples = [f.path for f in os.scandir(osp.join(examples_folder, "basic")) if f.is_dir() ] advanced_examples = [] # advanced_examples = [f.path for f in os.scandir(osp.join(examples_folder, "advanced")) if f.is_dir()] examples = basic_examples + advanced_examples - # get all existing rst files + # get all existing md files md_files = glob.glob(os.path.join(HERE, "examples", "*.md")) md_files = {os.path.basename(os.path.normpath(entry)) for entry in md_files} @@ -327,33 +340,23 @@ def setup_examples_pages(): app_filename = os.path.join(example, "app.py") md_filename = f"{base_name}.md" - examples_md.append((base_name, f"./examples/{base_name}")) - - # fixme should be replaced with something based on timestep - # so if any(mymodelfiles) is newer then existing_md_filename - if md_filename not in md_files: - with open(agent_filename) as content_file: - agent_file = content_file.read() - with open(model_filename) as content_file: - model_file = content_file.read() - with open(readme_filename) as content_file: - readme_file = content_file.read() - with open(app_filename) as content_file: - app_file = content_file.read() - - with open(os.path.join(HERE, "examples", md_filename), "w") as fh: - content = template.substitute( - dict(agent_file=agent_file, model_file=model_file, - readme_file=readme_file, app_file=app_file) - ) - fh.write(content) - else: - md_files.remove(md_filename) - - # these md files are outdated because the example has been removed - for entry in md_files: - fn = os.path.join(HERE, "examples", entry) - os.remove(fn) + examples_md.append(base_name) + + # let's establish the latest update to the example files + timestamps = [osp.getmtime(fh) for fh in [agent_filename, model_filename, readme_filename, app_filename]] + latest_edit = max(timestamps) + + md_filepath = os.path.join(HERE, "examples", md_filename) + + # if the example is new or the existing example md file is older than the latest update, create a new file + if md_filename not in md_files or latest_edit > osp.getmtime(md_filepath): + write_example_md_file(agent_filename, model_filename, readme_filename, app_filename, md_filepath, template) + + + # check if any md files should be removed because the example is removed + outdated_md_files = md_files - {f"{entry}.md" for entry in examples_md} + for entry in outdated_md_files: + os.remove(os.path.join(HERE, "examples", entry) ) # create overview of examples.md with open(os.path.join(HERE, "examples_overview_template.txt")) as fh: @@ -362,7 +365,7 @@ def setup_examples_pages(): with open(os.path.join(HERE, "examples.md"), "w") as fh: content = template.substitute( dict( - examples="\n".join([f"{' '.join(base_name.split('_'))} <{file_path[2::]}>" for base_name, file_path in examples_md]), + examples="\n".join([f"{' '.join(base_name.split('_'))} >" for base_name in examples_md]), ) ) fh.write(content) diff --git a/docs/examples.md b/docs/examples.md index f2089e385ed..23630b65253 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -5,10 +5,10 @@ ```{toctree} :maxdepth: 1 -boid flockers -virus on network -conways game of life -schelling -boltzmann wealth model +boid flockers > +virus on network > +conways game of life > +schelling > +boltzmann wealth model > ``` \ No newline at end of file diff --git a/docs/examples/conways_game_of_life.md b/docs/examples/conways_game_of_life.md index 289aed7cd9f..b60a002dbbd 100644 --- a/docs/examples/conways_game_of_life.md +++ b/docs/examples/conways_game_of_life.md @@ -53,10 +53,10 @@ class Cell(Agent): super().__init__(model) self.x, self.y = pos self.state = init_state - self._nextState = None + self._next_state = None @property - def isAlive(self): + def is_alive(self): return self.state == self.ALIVE @property @@ -72,20 +72,20 @@ class Cell(Agent): """ # Get the neighbors and apply the rules on whether to be alive or dead # at the next tick. - live_neighbors = sum(neighbor.isAlive for neighbor in self.neighbors) + live_neighbors = sum(neighbor.is_alive for neighbor in self.neighbors) # Assume nextState is unchanged, unless changed below. - self._nextState = self.state - if self.isAlive: + self._next_state = self.state + if self.is_alive: if live_neighbors < 2 or live_neighbors > 3: - self._nextState = self.DEAD + self._next_state = self.DEAD else: if live_neighbors == 3: - self._nextState = self.ALIVE + self._next_state = self.ALIVE def assume_state(self): """Set the state to the new computed state -- computed in step().""" - self.state = self._nextState + self.state = self._next_state ``` diff --git a/examples/basic/conways_game_of_life/agents.py b/examples/basic/conways_game_of_life/agents.py index 63017ff8118..17af7502572 100644 --- a/examples/basic/conways_game_of_life/agents.py +++ b/examples/basic/conways_game_of_life/agents.py @@ -12,10 +12,10 @@ def __init__(self, pos, model, init_state=DEAD): super().__init__(model) self.x, self.y = pos self.state = init_state - self._nextState = None + self._next_state = None @property - def isAlive(self): + def is_alive(self): return self.state == self.ALIVE @property @@ -31,17 +31,17 @@ def determine_state(self): """ # Get the neighbors and apply the rules on whether to be alive or dead # at the next tick. - live_neighbors = sum(neighbor.isAlive for neighbor in self.neighbors) + live_neighbors = sum(neighbor.is_alive for neighbor in self.neighbors) # Assume nextState is unchanged, unless changed below. - self._nextState = self.state - if self.isAlive: + self._next_state = self.state + if self.is_alive: if live_neighbors < 2 or live_neighbors > 3: - self._nextState = self.DEAD + self._next_state = self.DEAD else: if live_neighbors == 3: - self._nextState = self.ALIVE + self._next_state = self.ALIVE def assume_state(self): """Set the state to the new computed state -- computed in step().""" - self.state = self._nextState + self.state = self._next_state From 2ef80f863807508647cd4cca936e870f2eb91b62 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 18 Oct 2024 14:18:55 +0200 Subject: [PATCH 25/34] Update conf.py --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index bac6aaed10b..86b1e8a85d1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -365,7 +365,7 @@ def setup_examples_pages(): with open(os.path.join(HERE, "examples.md"), "w") as fh: content = template.substitute( dict( - examples="\n".join([f"{' '.join(base_name.split('_'))} >" for base_name in examples_md]), + examples="\n".join([f"{' '.join(base_name.split('_'))} " for base_name in examples_md]), ) ) fh.write(content) From ed764f8129b7adc71cb47b6fbed475bbb2ec465d Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Fri, 18 Oct 2024 16:09:43 +0200 Subject: [PATCH 26/34] Improve Mesa 3.0 beta 1 release notes (#2384) Add some more information about the examples and mention the SPEC 7 random number generator. --- HISTORY.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index e08bd7098a5..6d3a3903ef8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,13 +3,17 @@ title: Release History --- # 3.0.0b1 (2024-10-17) +Mesa 3.0 beta 1 is our last beta release before the Mesa 3.0 stable release. We are restructuring our examples and have move 9 core examples from [mesa-examples](https://github.com/projectmesa/mesa-examples) to mesa itself ([#2358](https://github.com/projectmesa/mesa/pull/2358)). The 5 basic examples are now directly importable ([#2381](https://github.com/projectmesa/mesa/pull/2381)): -## Highlights -Basic examples are now importable in Mesa, includes boid_flockers, boltzmann_wealth_model, conways_game_of_life, schelling, and virus_on_network models. With this change they are also integrated into the Mesa tutorial in the docs. +```Python +from mesa.examples.basic import BoidFlockers, BoltzmannWealthModel, ConwaysGameOfLife, Schelling, VirusOnNetwork +``` + +The 5 basic examples will always use stable Mesa features, we are also working on 4 more advanced example which can also include experimental features. -Also, in this release, visualizations are improved by making visualization elements scalable and more clearly labeling the plots. +All our core examples can now be viewed in the [`examples`](https://github.com/projectmesa/mesa/tree/main/examples) folder. [mesa-examples](https://github.com/projectmesa/mesa-examples) will continue to exists for user showcases. We're also working on making the examples visible in the ReadtheDocs ([#2382](https://github.com/projectmesa/mesa/pull/2382)) and on an website ([mesa-examples#139](https://github.com/projectmesa/mesa-examples/issues/139)). Follow all our work on the examples in this tracking issue [#2364](https://github.com/projectmesa/mesa/issues/2364). - +Furthermore, the visualizations are improved by making visualization elements scalable and more clearly labeling the plots, and the Model now has an `rng` argument for an [SPEC 7](https://scientific-python.org/specs/spec-0007/) compliant NumPy random number generator ([#2352](https://github.com/projectmesa/mesa/pull/2352)). Following SPEC 7, you have to pass either `seed` or `rng`. Whichever one you pass will be used to seed both `random.Random`, and `numpy.random.Generator.` ## What's Changed ### ⚠️ Breaking changes From ed43d5cbfc63812d93d763ecc5fa318b6e55edc8 Mon Sep 17 00:00:00 2001 From: rht Date: Fri, 18 Oct 2024 10:19:02 -0400 Subject: [PATCH 27/34] refactor: Simplify Schelling code (#2353) * refactor: Simplify Schelling code Ported from https://github.com/projectmesa/mesa-examples/pull/222. * Update benchmarks/Schelling/schelling.py Co-authored-by: Ewout ter Hoeven * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Jan Kwakkel Co-authored-by: Ewout ter Hoeven Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- benchmarks/Schelling/schelling.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 2b6614daac7..f4543cb4312 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -34,11 +34,10 @@ def __init__( def step(self): """Run one step of the agent.""" - similar = 0 - neighborhood = self.cell.get_neighborhood(radius=self.radius) - for neighbor in neighborhood.agents: - if neighbor.type == self.type: - similar += 1 + neighbors = self.cell.get_neighborhood(radius=self.radius).agents + similar = len( + [neighbor for neighbor in neighbors if neighbor.type == self.type] + ) # If unhappy, move: if similar < self.homophily: @@ -76,7 +75,6 @@ def __init__( """ super().__init__(seed=seed) self.simulator = simulator - self.minority_pc = minority_pc self.happy = 0 self.grid = OrthogonalMooreGrid( @@ -92,7 +90,7 @@ def __init__( # its contents. (coord_iter) for cell in self.grid: if self.random.random() < density: - agent_type = 1 if self.random.random() < self.minority_pc else 0 + agent_type = 1 if self.random.random() < minority_pc else 0 SchellingAgent(self, agent_type, radius, homophily, cell) def step(self): From 824459fbddd03c14ccdf29255dcafa0fd090782c Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 18 Oct 2024 22:25:23 +0200 Subject: [PATCH 28/34] trying to merge --- docs/conf.py | 5 +---- docs/examples.md | 10 +++++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 86b1e8a85d1..1be59c735a8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -316,7 +316,7 @@ def write_example_md_file(agent_filename, model_filename, readme_filename, app_f def setup_examples_pages(): # create md files for all examples # check what examples exist - examples_folder = osp.abspath(osp.join(HERE, "..", "examples")) + examples_folder = osp.abspath(osp.join(HERE, "..", "mesa", "examples")) basic_examples = [f.path for f in os.scandir(osp.join(examples_folder, "basic")) if f.is_dir() ] advanced_examples = [] # advanced_examples = [f.path for f in os.scandir(osp.join(examples_folder, "advanced")) if f.is_dir()] @@ -371,9 +371,6 @@ def setup_examples_pages(): fh.write(content) def setup(app): - # copy changelog into source folder for documentation - # dest = osp.join(HERE, "./getting_started/changelog.md") - # shutil.copy(osp.join(HERE, "..", "..", "CHANGELOG.md"), dest) setup_examples_pages() diff --git a/docs/examples.md b/docs/examples.md index 23630b65253..11c450385bc 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -5,10 +5,10 @@ ```{toctree} :maxdepth: 1 -boid flockers > -virus on network > -conways game of life > -schelling > -boltzmann wealth model > +boid flockers +virus on network +conways game of life
+schelling
+boltzmann wealth model
``` \ No newline at end of file From e0ac4ba7f9c6c08176a684748fba77513553d20d Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 19 Oct 2024 20:21:17 +0200 Subject: [PATCH 29/34] Update index.md --- docs/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.md b/docs/index.md index fd3d2d936e8..b866ffbc913 100644 --- a/docs/index.md +++ b/docs/index.md @@ -88,6 +88,7 @@ ABM features users have shared that you may want to use in your model Mesa Overview tutorials/intro_tutorial +tutorials/visualization_tutorial Examples Migration guide Best Practices From 8fa9f537bc741ae5a4704b2c04dd701274e16611 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 19 Oct 2024 21:09:25 +0200 Subject: [PATCH 30/34] some typo fixes --- docs/overview.md | 3 ++- docs/packages.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index cdadafd10de..17ef67302fe 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -15,7 +15,8 @@ Mesa is modular, meaning that its modeling, analysis and visualization component Most models consist of one class to represent the model itself and one or more classes for agents. Mesa provides built-in functionality for managing agents and their interactions. These are implemented in Mesa's modeling modules: -- `mesa.Model`, `mesa.Agent` +- [mesa.model](apis/model) +- [mesa.agent](apis/agent) - [mesa.space](apis/space) The skeleton of a model might look like this: diff --git a/docs/packages.md b/docs/packages.md index 75dab0951d9..1ad6e10d906 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -60,7 +60,7 @@ The commands above should also work with Anaconda, just replace the `pip` with ` ## Package Development: A "How-to Guide" -The purpose of this section is help you understand, setup, and distribute your Mesa package as quickly as possible. A Mesa package is just a Python package or repo. We just call it a Mesa package, because we are talking about a Python package in the context of Mesa. These instructions assume that you are a little familiar with development, but that you have little knowledge of the packaging process. +The purpose of this section is to help you understand, setup, and distribute your Mesa package as quickly as possible. A Mesa package is just a Python package or repo. We just call it a Mesa package, because we are talking about a Python package in the context of Mesa. These instructions assume that you are a little familiar with development, but that you have little knowledge of the packaging process. There are two ways to share a package: @@ -85,7 +85,7 @@ Most likely you created an ABM that has the code that you want to share in it, w > > 4. [Clone the repo to your computer](https://help.github.com/articles/cloning-a-repository/#platform-linux). > -> 5. Copy your code directory into the repo that you cloned one your computer. +> 5. Copy your code directory into the repo that you cloned on your computer. > > 6. Add a requirements.txt file, which lets people know which external Python packages are needed to run the code in your repo. To create a file, run: `pip freeze > requirements.txt`. Note, if you are running Anaconda, you will need to install pip first: `conda install pip`. > From 4be7dd2ef7e91760399d1580a9330dbb5af488bb Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 20 Oct 2024 21:24:12 +0200 Subject: [PATCH 31/34] allways create md files as requested by ewout --- .gitignore | 3 + docs/conf.py | 23 +- docs/examples/boid_flockers.md | 265 ---------------- docs/examples/boltzmann_wealth_model.md | 223 -------------- docs/examples/conways_game_of_life.md | 207 ------------- docs/examples/schelling.md | 198 ------------ docs/examples/virus_on_network.md | 384 ------------------------ 7 files changed, 5 insertions(+), 1298 deletions(-) delete mode 100644 docs/examples/boid_flockers.md delete mode 100644 docs/examples/boltzmann_wealth_model.md delete mode 100644 docs/examples/conways_game_of_life.md delete mode 100644 docs/examples/schelling.md delete mode 100644 docs/examples/virus_on_network.md diff --git a/.gitignore b/.gitignore index 78b8b417a8f..90567e718ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Benchmarking benchmarks/**/*.pickle +# exampledocs +docs/examples/*.md + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/docs/conf.py b/docs/conf.py index 1be59c735a8..3af97a11b7a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -318,15 +318,9 @@ def setup_examples_pages(): # check what examples exist examples_folder = osp.abspath(osp.join(HERE, "..", "mesa", "examples")) basic_examples = [f.path for f in os.scandir(osp.join(examples_folder, "basic")) if f.is_dir() ] - advanced_examples = [] - # advanced_examples = [f.path for f in os.scandir(osp.join(examples_folder, "advanced")) if f.is_dir()] + advanced_examples = [] # fixme [f.path for f in os.scandir(osp.join(examples_folder, "advanced")) if f.is_dir()] examples = basic_examples + advanced_examples - # get all existing md files - md_files = glob.glob(os.path.join(HERE, "examples", "*.md")) - md_files = {os.path.basename(os.path.normpath(entry)) for entry in md_files} - - # check which rst files exist with open(os.path.join(HERE, "example_template.txt")) as fh: template = string.Template(fh.read()) @@ -342,21 +336,8 @@ def setup_examples_pages(): md_filename = f"{base_name}.md" examples_md.append(base_name) - # let's establish the latest update to the example files - timestamps = [osp.getmtime(fh) for fh in [agent_filename, model_filename, readme_filename, app_filename]] - latest_edit = max(timestamps) - md_filepath = os.path.join(HERE, "examples", md_filename) - - # if the example is new or the existing example md file is older than the latest update, create a new file - if md_filename not in md_files or latest_edit > osp.getmtime(md_filepath): - write_example_md_file(agent_filename, model_filename, readme_filename, app_filename, md_filepath, template) - - - # check if any md files should be removed because the example is removed - outdated_md_files = md_files - {f"{entry}.md" for entry in examples_md} - for entry in outdated_md_files: - os.remove(os.path.join(HERE, "examples", entry) ) + write_example_md_file(agent_filename, model_filename, readme_filename, app_filename, md_filepath, template) # create overview of examples.md with open(os.path.join(HERE, "examples_overview_template.txt")) as fh: diff --git a/docs/examples/boid_flockers.md b/docs/examples/boid_flockers.md deleted file mode 100644 index 4d463f98da7..00000000000 --- a/docs/examples/boid_flockers.md +++ /dev/null @@ -1,265 +0,0 @@ - -# Boids Flockers - -## Summary - -An implementation of Craig Reynolds's Boids flocker model. Agents (simulated birds) try to fly towards the average position of their neighbors and in the same direction as them, while maintaining a minimum distance. This produces flocking behavior. - -This model tests Mesa's continuous space feature, and uses numpy arrays to represent vectors. It also demonstrates how to create custom visualization components. - -## 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 launch the visualization interactively, run ``mesa runserver`` in this directory. e.g. - -``` -$ mesa runserver -``` - -or - -Directly run the file ``run.py`` in the terminal. e.g. - -``` - $ python run.py -``` - -* Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. - -## Files - -* [model.py](model.py): Core model file; contains the Boid Model and Boid Agent class. -* [app.py](app.py): Visualization code. - -## Further Reading - -The following link can be visited for more information on the boid flockers model: -https://cs.stanford.edu/people/eroberts/courses/soco/projects/2008-09/modeling-natural-systems/boids.html - - -## Agents - -```python -import numpy as np - -from mesa import Agent - - -class Boid(Agent): - """A Boid-style flocker agent. - - The agent follows three behaviors to flock: - - Cohesion: steering towards neighboring agents. - - Separation: avoiding getting too close to any other agent. - - Alignment: try to fly in the same direction as the neighbors. - - Boids have a vision that defines the radius in which they look for their - neighbors to flock with. Their speed (a scalar) and direction (a vector) - define their movement. Separation is their desired minimum distance from - any other Boid. - """ - - def __init__( - self, - model, - speed, - direction, - vision, - separation, - cohere=0.03, - separate=0.015, - match=0.05, - ): - """Create a new Boid flocker agent. - - Args: - speed: Distance to move per step. - direction: numpy vector for the Boid's direction of movement. - vision: Radius to look around for nearby Boids. - separation: Minimum distance to maintain from other Boids. - cohere: the relative importance of matching neighbors' positions - separate: the relative importance of avoiding close neighbors - match: the relative importance of matching neighbors' headings - """ - super().__init__(model) - self.speed = speed - self.direction = direction - self.vision = vision - self.separation = separation - self.cohere_factor = cohere - self.separate_factor = separate - self.match_factor = match - self.neighbors = None - - def step(self): - """Get the Boid's neighbors, compute the new vector, and move accordingly.""" - self.neighbors = self.model.space.get_neighbors(self.pos, self.vision, False) - n = 0 - match_vector, separation_vector, cohere = np.zeros((3, 2)) - for neighbor in self.neighbors: - n += 1 - heading = self.model.space.get_heading(self.pos, neighbor.pos) - cohere += heading - if self.model.space.get_distance(self.pos, neighbor.pos) < self.separation: - separation_vector -= heading - match_vector += neighbor.direction - n = max(n, 1) - cohere = cohere * self.cohere_factor - separation_vector = separation_vector * self.separate_factor - match_vector = match_vector * self.match_factor - self.direction += (cohere + separation_vector + match_vector) / n - self.direction /= np.linalg.norm(self.direction) - new_pos = self.pos + self.direction * self.speed - self.model.space.move_agent(self, new_pos) - -``` - - -## Model - -```python -"""Flockers. -============================================================= -A Mesa implementation of Craig Reynolds's Boids flocker model. -Uses numpy arrays to represent vectors. -""" - -import numpy as np - -import mesa - -from .agents import Boid - - -class BoidFlockers(mesa.Model): - """Flocker model class. Handles agent creation, placement and scheduling.""" - - def __init__( - self, - seed=None, - population=100, - width=100, - height=100, - vision=10, - speed=1, - separation=1, - cohere=0.03, - separate=0.015, - match=0.05, - ): - """Create a new Flockers model. - - Args: - population: Number of Boids - width, height: Size of the space. - speed: How fast should the Boids move. - vision: How far around should each Boid look for its neighbors - separation: What's the minimum distance each Boid will attempt to - keep from any other - cohere, separate, match: factors for the relative importance of - the three drives. - """ - super().__init__(seed=seed) - self.population = population - self.vision = vision - self.speed = speed - self.separation = separation - - self.space = mesa.space.ContinuousSpace(width, height, True) - self.factors = {"cohere": cohere, "separate": separate, "match": match} - self.make_agents() - - def make_agents(self): - """Create self.population agents, with random positions and starting headings.""" - for _ in range(self.population): - x = self.random.random() * self.space.x_max - y = self.random.random() * self.space.y_max - pos = np.array((x, y)) - direction = np.random.random(2) * 2 - 1 - boid = Boid( - model=self, - speed=self.speed, - direction=direction, - vision=self.vision, - separation=self.separation, - **self.factors, - ) - self.space.place_agent(boid, pos) - - def step(self): - self.agents.shuffle_do("step") - -``` - - -## App - -```python -from mesa.visualization import Slider, SolaraViz, make_space_matplotlib - -from .model import BoidFlockers - - -def boid_draw(agent): - if not agent.neighbors: # Only for the first Frame - neighbors = len(agent.model.space.get_neighbors(agent.pos, agent.vision, False)) - else: - neighbors = len(agent.neighbors) - - if neighbors <= 1: - return {"color": "red", "size": 20} - elif neighbors >= 2: - return {"color": "green", "size": 20} - - -model_params = { - "population": Slider( - label="Number of boids", - value=100, - min=10, - max=200, - step=10, - ), - "width": 100, - "height": 100, - "speed": Slider( - label="Speed of Boids", - value=5, - min=1, - max=20, - step=1, - ), - "vision": Slider( - label="Vision of Bird (radius)", - value=10, - min=1, - max=50, - step=1, - ), - "separation": Slider( - label="Minimum Separation", - value=2, - min=1, - max=20, - step=1, - ), -} - -model = BoidFlockers() - -page = SolaraViz( - model, - [make_space_matplotlib(agent_portrayal=boid_draw)], - model_params=model_params, - name="Boid Flocking Model", -) -page # noqa - -``` \ No newline at end of file diff --git a/docs/examples/boltzmann_wealth_model.md b/docs/examples/boltzmann_wealth_model.md deleted file mode 100644 index d54801afd0f..00000000000 --- a/docs/examples/boltzmann_wealth_model.md +++ /dev/null @@ -1,223 +0,0 @@ - -# 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) - -Make sure to install the requirements first: - -``` - $ pip install -r requirements.txt -``` - -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: - -``` - $ solara run app.py -``` - -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 - -* ``model.py``: Final version of the model. -* ``app.py``: Code for the interactive visualization. - -## Optional - -An optional visualization is also provided using Streamlit, which is another popular Python library for creating interactive web applications. - -To run the Streamlit app, you will need to install the `streamlit` and `altair` libraries: - -``` - $ pip install streamlit altair -``` - -Then, you can run the Streamlit app using the following command: - -``` - $ streamlit run st_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) - - -## Agents - -```python -from mesa import Agent - - -class MoneyAgent(Agent): - """An agent with fixed initial wealth.""" - - def __init__(self, model): - super().__init__(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() - -``` - - -## Model - -```python -import mesa - -from .agents import MoneyAgent - - -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): - super().__init__() - self.num_agents = n - self.grid = mesa.space.MultiGrid(width, height, True) - - self.datacollector = mesa.DataCollector( - model_reporters={"Gini": self.compute_gini}, - agent_reporters={"Wealth": "wealth"}, - ) - # Create agents - for _ in range(self.num_agents): - a = MoneyAgent(self) - - # Add the agent to a random grid cell - x = self.random.randrange(self.grid.width) - y = self.random.randrange(self.grid.height) - self.grid.place_agent(a, (x, y)) - - self.running = True - self.datacollector.collect(self) - - def step(self): - self.agents.shuffle_do("step") - self.datacollector.collect(self) - - def compute_gini(self): - agent_wealths = [agent.wealth for agent in self.agents] - x = sorted(agent_wealths) - n = self.num_agents - b = sum(xi * (n - i) for i, xi in enumerate(x)) / (n * sum(x)) - return 1 + (1 / n) - 2 * b - -``` - - -## App - -```python -from mesa.visualization import ( - SolaraViz, - make_plot_measure, - make_space_matplotlib, -) - -from .model import BoltzmannWealthModel - - -def agent_portrayal(agent): - size = 10 - color = "tab:red" - if agent.wealth > 0: - size = 50 - color = "tab:blue" - return {"size": size, "color": color} - - -model_params = { - "n": { - "type": "SliderInt", - "value": 50, - "label": "Number of agents:", - "min": 10, - "max": 100, - "step": 1, - }, - "width": 10, - "height": 10, -} - -# Create initial model instance -model1 = BoltzmannWealthModel(50, 10, 10) - -# Create visualization elements. The visualization elements are solara components -# that receive the model instance as a "prop" and display it in a certain way. -# Under the hood these are just classes that receive the model instance. -# You can also author your own visualization elements, which can also be functions -# that receive the model instance and return a valid solara component. -SpaceGraph = make_space_matplotlib(agent_portrayal) -GiniPlot = make_plot_measure("Gini") - -# Create the SolaraViz page. This will automatically create a server and display the -# visualization elements in a web browser. -# Display it using the following command in the example directory: -# solara run app.py -# It will automatically update and display any changes made to this file -page = SolaraViz( - model1, - components=[SpaceGraph, GiniPlot], - model_params=model_params, - name="Boltzmann Wealth Model", -) -page # noqa - - -# In a notebook environment, we can also display the visualization elements directly -# SpaceGraph(model1) -# GiniPlot(model1) - -# The plots will be static. If you want to pick up model steps, -# you have to make the model reactive first -# reactive_model = solara.reactive(model1) -# SpaceGraph(reactive_model) -# In a different notebook block: -# reactive_model.value.step() - -``` \ No newline at end of file diff --git a/docs/examples/conways_game_of_life.md b/docs/examples/conways_game_of_life.md deleted file mode 100644 index b60a002dbbd..00000000000 --- a/docs/examples/conways_game_of_life.md +++ /dev/null @@ -1,207 +0,0 @@ - -# Conway's Game Of "Life" - -## Summary - -[The Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life), also known simply as "Life", is a cellular automaton devised by the British mathematician John Horton Conway in 1970. - -The "game" is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input by a human. One interacts with the Game of "Life" by creating an initial configuration and observing how it evolves, or, for advanced "players", by creating patterns with particular properties. - - -## 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 ``run``. - -## Files - -* ``agents.py``: Defines the behavior of an individual cell, which can be in two states: DEAD or ALIVE. -* ``model.py``: Defines the model itself, initialized with a random configuration of alive and dead cells. -* ``portrayal.py``: Describes for the front end how to render a cell. -* ``st_app.py``: Defines an interactive visualization using Streamlit. - -## Optional - -* ``conways_game_of_life/st_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 st_app.py`` - - -## Further Reading -[Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) - - -## Agents - -```python -from mesa import Agent - - -class Cell(Agent): - """Represents a single ALIVE or DEAD cell in the simulation.""" - - DEAD = 0 - ALIVE = 1 - - def __init__(self, pos, model, init_state=DEAD): - """Create a cell, in the given state, at the given x, y position.""" - super().__init__(model) - self.x, self.y = pos - self.state = init_state - self._next_state = None - - @property - def is_alive(self): - return self.state == self.ALIVE - - @property - def neighbors(self): - return self.model.grid.iter_neighbors((self.x, self.y), True) - - def determine_state(self): - """Compute if the cell will be dead or alive at the next tick. This is - based on the number of alive or dead neighbors. The state is not - changed here, but is just computed and stored in self._nextState, - because our current state may still be necessary for our neighbors - to calculate their next state. - """ - # Get the neighbors and apply the rules on whether to be alive or dead - # at the next tick. - live_neighbors = sum(neighbor.is_alive for neighbor in self.neighbors) - - # Assume nextState is unchanged, unless changed below. - self._next_state = self.state - if self.is_alive: - if live_neighbors < 2 or live_neighbors > 3: - self._next_state = self.DEAD - else: - if live_neighbors == 3: - self._next_state = self.ALIVE - - def assume_state(self): - """Set the state to the new computed state -- computed in step().""" - self.state = self._next_state - -``` - - -## Model - -```python -from mesa import Model -from mesa.space import SingleGrid - -from .agents import Cell - - -class ConwaysGameOfLife(Model): - """Represents the 2-dimensional array of cells in Conway's Game of Life.""" - - def __init__(self, width=50, height=50): - """Create a new playing area of (width, height) cells.""" - super().__init__() - # Use a simple grid, where edges wrap around. - self.grid = SingleGrid(width, height, torus=True) - - # Place a cell at each location, with some initialized to - # ALIVE and some to DEAD. - for _contents, (x, y) in self.grid.coord_iter(): - cell = Cell((x, y), self) - if self.random.random() < 0.1: - cell.state = cell.ALIVE - self.grid.place_agent(cell, (x, y)) - - self.running = True - - def step(self): - """Perform the model step in two stages: - - First, all cells assume their next state (whether they will be dead or alive) - - Then, all cells change state to their next state. - """ - self.agents.do("determine_state") - self.agents.do("assume_state") - -``` - - -## App - -```python -import time - -import altair as alt -import numpy as np -import pandas as pd -import streamlit as st -from model import ConwaysGameOfLife - -model = st.title("Conway's Game of Life") -num_ticks = st.slider("Select number of Steps", 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 = ConwaysGameOfLife(height, width) - -col1, col2, col3 = st.columns(3) -status_text = st.empty() -# step_mode = st.checkbox('Run Step-by-Step') -run = st.button("Run Simulation") - - -if run: - tick = time.time() - step = 0 - # init grid - df_grid = pd.DataFrame() - agent_counts = np.zeros((model.grid.width, model.grid.height)) - for x in range(width): - for y in range(height): - df_grid = pd.concat( - [df_grid, pd.DataFrame({"x": [x], "y": [y], "state": [0]})], - ignore_index=True, - ) - - heatmap = ( - alt.Chart(df_grid) - .mark_point(size=100) - .encode(x="x", y="y", color=alt.Color("state")) - .interactive() - .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, use_container_width=True) - color_scale = alt.Scale(domain=[0, 1], range=["red", "yellow"]) - for i in range(num_ticks): - model.step() - my_bar.progress((i / num_ticks), text="Simulation progress") - placeholder.text("Step = %d" % i) - for contents, (x, y) in model.grid.coord_iter(): - # print('x:',x,'y:',y, 'state:',contents) - selected_row = df_grid[(df_grid["x"] == x) & (df_grid["y"] == y)] - df_grid.loc[selected_row.index, "state"] = ( - contents.state - ) # random.choice([1,2]) - - heatmap = ( - alt.Chart(df_grid) - .mark_circle(size=100) - .encode(x="x", y="y", color=alt.Color("state", scale=color_scale)) - .interactive() - .properties(width=800, height=600) - ) - chart.altair_chart(heatmap) - - time.sleep(0.1) - - tock = time.time() - st.success(f"Simulation completed in {tock - tick:.2f} secs") - -``` \ No newline at end of file diff --git a/docs/examples/schelling.md b/docs/examples/schelling.md deleted file mode 100644 index 24f6e3c2aeb..00000000000 --- a/docs/examples/schelling.md +++ /dev/null @@ -1,198 +0,0 @@ - -# 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, in this directory, run the following command - -``` - $ solara run app.py -``` - -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/). - -## 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 - -* ``app.py``: Code for the interactive visualization. -* ``schelling.py``: Contains the agent class, and the overall model class. -* ``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. - - -## Agents - -```python -from mesa import Agent, Model - - -class SchellingAgent(Agent): - """Schelling segregation agent.""" - - def __init__(self, model: Model, agent_type: int) -> None: - """Create a new Schelling agent. - - Args: - agent_type: Indicator for the agent's type (minority=1, majority=0) - """ - super().__init__(model) - self.type = agent_type - - def step(self) -> None: - neighbors = self.model.grid.iter_neighbors( - self.pos, moore=True, radius=self.model.radius - ) - similar = sum(1 for neighbor in neighbors if neighbor.type == self.type) - - # If unhappy, move: - if similar < self.model.homophily: - self.model.grid.move_to_empty(self) - else: - self.model.happy += 1 - -``` - - -## Model - -```python -import mesa -from mesa import Model - -from .agents import SchellingAgent - - -class Schelling(Model): - """Model class for the Schelling segregation model.""" - - def __init__( - self, - height=20, - width=20, - homophily=3, - radius=1, - density=0.8, - minority_pc=0.2, - seed=None, - ): - """Create a new Schelling model. - - Args: - width, height: Size of the space. - density: Initial Chance for a cell to populated - minority_pc: Chances for an agent to be in minority class - homophily: Minimum number of agents of same class needed to be happy - radius: Search radius for checking similarity - seed: Seed for Reproducibility - """ - super().__init__(seed=seed) - self.homophily = homophily - self.radius = radius - - self.grid = mesa.space.SingleGrid(width, height, torus=True) - - self.happy = 0 - self.datacollector = mesa.DataCollector( - model_reporters={"happy": "happy"}, # Model-level count of happy agents - ) - - # Set up agents - # We use a grid iterator that returns - # the coordinates of a cell as well as - # its contents. (coord_iter) - for _, pos in self.grid.coord_iter(): - if self.random.random() < density: - agent_type = 1 if self.random.random() < minority_pc else 0 - agent = SchellingAgent(self, agent_type) - self.grid.place_agent(agent, pos) - - self.datacollector.collect(self) - - def step(self): - """Run one step of the model.""" - self.happy = 0 # Reset counter of happy agents - self.agents.shuffle_do("step") - - self.datacollector.collect(self) - - self.running = self.happy != len(self.agents) - -``` - - -## App - -```python -import solara - -from mesa.visualization import ( - Slider, - SolaraViz, - make_plot_measure, - make_space_matplotlib, -) - -from .model import Schelling - - -def get_happy_agents(model): - """Display a text count of how many happy agents there are.""" - return solara.Markdown(f"**Happy agents: {model.happy}**") - - -def agent_portrayal(agent): - return {"color": "tab:orange" if agent.type == 0 else "tab:blue"} - - -model_params = { - "density": Slider("Agent density", 0.8, 0.1, 1.0, 0.1), - "minority_pc": Slider("Fraction minority", 0.2, 0.0, 1.0, 0.05), - "homophily": Slider("Homophily", 3, 0, 8, 1), - "width": 20, - "height": 20, -} - -model1 = Schelling(20, 20, 0.8, 0.2, 3) - -HappyPlot = make_plot_measure("happy") - -page = SolaraViz( - model1, - components=[ - make_space_matplotlib(agent_portrayal), - make_plot_measure("happy"), - get_happy_agents, - ], - model_params=model_params, -) -page # noqa - -``` \ No newline at end of file diff --git a/docs/examples/virus_on_network.md b/docs/examples/virus_on_network.md deleted file mode 100644 index 0c37df4148b..00000000000 --- a/docs/examples/virus_on_network.md +++ /dev/null @@ -1,384 +0,0 @@ - -# Virus on a Network - -## Summary - -This model is based on the NetLogo model "Virus on Network". It demonstrates the spread of a virus through a network and follows the SIR model, commonly seen in epidemiology. - -The SIR model is one of the simplest compartmental models, and many models are derivatives of this basic form. The model consists of three compartments: - -S: The number of susceptible individuals. When a susceptible and an infectious individual come into "infectious contact", the susceptible individual contracts the disease and transitions to the infectious compartment. -I: The number of infectious individuals. These are individuals who have been infected and are capable of infecting susceptible individuals. -R for the number of removed (and immune) or deceased individuals. These are individuals who have been infected and have either recovered from the disease and entered the removed compartment, or died. It is assumed that the number of deaths is negligible with respect to the total population. This compartment may also be called "recovered" or "resistant". - -For more information about this model, read the NetLogo's web page: http://ccl.northwestern.edu/netlogo/models/VirusonaNetwork. - -JavaScript library used in this example to render the network: [d3.js](https://d3js.org/). - -## 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. - -or - -Directly run the file ``run.py`` in the terminal. e.g. - -``` - $ python run.py -``` - - -## Files - -* ``model.py``: Contains the agent class, and the overall model class. -* ``agents.py``: Contains the agent class. -* ``app.py``: Contains the code for the interactive Solara visualization. - -## 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 - - -[Stonedahl, F. and Wilensky, U. (2008). NetLogo Virus on a Network model](http://ccl.northwestern.edu/netlogo/models/VirusonaNetwork). -Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. - - -[Wilensky, U. (1999). NetLogo](http://ccl.northwestern.edu/netlogo/) -Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. - - -## Agents - -```python -from enum import Enum - -from mesa import Agent - - -class State(Enum): - SUSCEPTIBLE = 0 - INFECTED = 1 - RESISTANT = 2 - - -class VirusAgent(Agent): - """Individual Agent definition and its properties/interaction methods.""" - - def __init__( - self, - model, - initial_state, - virus_spread_chance, - virus_check_frequency, - recovery_chance, - gain_resistance_chance, - ): - super().__init__(model) - - self.state = initial_state - - self.virus_spread_chance = virus_spread_chance - self.virus_check_frequency = virus_check_frequency - self.recovery_chance = recovery_chance - self.gain_resistance_chance = gain_resistance_chance - - def try_to_infect_neighbors(self): - neighbors_nodes = self.model.grid.get_neighborhood( - self.pos, include_center=False - ) - susceptible_neighbors = [ - agent - for agent in self.model.grid.get_cell_list_contents(neighbors_nodes) - if agent.state is State.SUSCEPTIBLE - ] - for a in susceptible_neighbors: - if self.random.random() < self.virus_spread_chance: - a.state = State.INFECTED - - def try_gain_resistance(self): - if self.random.random() < self.gain_resistance_chance: - self.state = State.RESISTANT - - def try_remove_infection(self): - # Try to remove - if self.random.random() < self.recovery_chance: - # Success - self.state = State.SUSCEPTIBLE - self.try_gain_resistance() - else: - # Failed - self.state = State.INFECTED - - def try_check_situation(self): - if (self.random.random() < self.virus_check_frequency) and ( - self.state is State.INFECTED - ): - self.try_remove_infection() - - def step(self): - if self.state is State.INFECTED: - self.try_to_infect_neighbors() - self.try_check_situation() - -``` - - -## Model - -```python -import math - -import networkx as nx - -import mesa -from mesa import Model - -from .agents import State, VirusAgent - - -def number_state(model, state): - return sum(1 for a in model.grid.get_all_cell_contents() if a.state is state) - - -def number_infected(model): - return number_state(model, State.INFECTED) - - -def number_susceptible(model): - return number_state(model, State.SUSCEPTIBLE) - - -def number_resistant(model): - return number_state(model, State.RESISTANT) - - -class VirusOnNetwork(Model): - """A virus model with some number of agents.""" - - def __init__( - self, - num_nodes=10, - avg_node_degree=3, - initial_outbreak_size=1, - virus_spread_chance=0.4, - virus_check_frequency=0.4, - recovery_chance=0.3, - gain_resistance_chance=0.5, - ): - super().__init__() - self.num_nodes = num_nodes - prob = avg_node_degree / self.num_nodes - self.G = nx.erdos_renyi_graph(n=self.num_nodes, p=prob) - self.grid = mesa.space.NetworkGrid(self.G) - - self.initial_outbreak_size = ( - initial_outbreak_size if initial_outbreak_size <= num_nodes else num_nodes - ) - self.virus_spread_chance = virus_spread_chance - self.virus_check_frequency = virus_check_frequency - self.recovery_chance = recovery_chance - self.gain_resistance_chance = gain_resistance_chance - - self.datacollector = mesa.DataCollector( - { - "Infected": number_infected, - "Susceptible": number_susceptible, - "Resistant": number_resistant, - } - ) - - # Create agents - for node in self.G.nodes(): - a = VirusAgent( - self, - State.SUSCEPTIBLE, - self.virus_spread_chance, - self.virus_check_frequency, - self.recovery_chance, - self.gain_resistance_chance, - ) - - # Add the agent to the node - self.grid.place_agent(a, node) - - # Infect some nodes - infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size) - for a in self.grid.get_cell_list_contents(infected_nodes): - a.state = State.INFECTED - - self.running = True - self.datacollector.collect(self) - - def resistant_susceptible_ratio(self): - try: - return number_state(self, State.RESISTANT) / number_state( - self, State.SUSCEPTIBLE - ) - except ZeroDivisionError: - return math.inf - - def step(self): - self.agents.shuffle_do("step") - # collect data - self.datacollector.collect(self) - - def run_model(self, n): - for _ in range(n): - self.step() - -``` - - -## App - -```python -import math - -import solara -from matplotlib.figure import Figure -from matplotlib.ticker import MaxNLocator - -from mesa.visualization import Slider, SolaraViz, make_space_matplotlib - -from .model import State, VirusOnNetwork, number_infected - - -def agent_portrayal(graph): - def get_agent(node): - return graph.nodes[node]["agent"][0] - - edge_width = [] - edge_color = [] - for u, v in graph.edges(): - agent1 = get_agent(u) - agent2 = get_agent(v) - w = 2 - ec = "#e8e8e8" - if State.RESISTANT in (agent1.state, agent2.state): - w = 3 - ec = "black" - edge_width.append(w) - edge_color.append(ec) - node_color_dict = { - State.INFECTED: "tab:red", - State.SUSCEPTIBLE: "tab:green", - State.RESISTANT: "tab:gray", - } - node_color = [node_color_dict[get_agent(node).state] for node in graph.nodes()] - return { - "width": edge_width, - "edge_color": edge_color, - "node_color": node_color, - } - - -def get_resistant_susceptible_ratio(model): - ratio = model.resistant_susceptible_ratio() - ratio_text = r"$\infty$" if ratio is math.inf else f"{ratio:.2f}" - infected_text = str(number_infected(model)) - - return f"Resistant/Susceptible Ratio: {ratio_text}
Infected Remaining: {infected_text}" - - -def make_plot(model): - # This is for the case when we want to plot multiple measures in 1 figure. - fig = Figure() - ax = fig.subplots() - measures = ["Infected", "Susceptible", "Resistant"] - colors = ["tab:red", "tab:green", "tab:gray"] - for i, m in enumerate(measures): - color = colors[i] - df = model.datacollector.get_model_vars_dataframe() - ax.plot(df.loc[:, m], label=m, color=color) - fig.legend() - # Set integer x axis - ax.xaxis.set_major_locator(MaxNLocator(integer=True)) - ax.set_xlabel("Step") - ax.set_ylabel("Number of Agents") - return solara.FigureMatplotlib(fig) - - -model_params = { - "num_nodes": Slider( - label="Number of agents", - value=10, - min=10, - max=100, - step=1, - ), - "avg_node_degree": Slider( - label="Avg Node Degree", - value=3, - min=3, - max=8, - step=1, - ), - "initial_outbreak_size": Slider( - label="Initial Outbreak Size", - value=1, - min=1, - max=10, - step=1, - ), - "virus_spread_chance": Slider( - label="Virus Spread Chance", - value=0.4, - min=0.0, - max=1.0, - step=0.1, - ), - "virus_check_frequency": Slider( - label="Virus Check Frequency", - value=0.4, - min=0.0, - max=1.0, - step=0.1, - ), - "recovery_chance": Slider( - label="Recovery Chance", - value=0.3, - min=0.0, - max=1.0, - step=0.1, - ), - "gain_resistance_chance": Slider( - label="Gain Resistance Chance", - value=0.5, - min=0.0, - max=1.0, - step=0.1, - ), -} - -SpacePlot = make_space_matplotlib(agent_portrayal) - -model1 = VirusOnNetwork() - -page = SolaraViz( - model1, - [ - SpacePlot, - make_plot, - # get_resistant_susceptible_ratio, # TODO: Fix and uncomment - ], - model_params=model_params, - name="Virus Model", -) -page # noqa - -``` \ No newline at end of file From f92b37cd882a59a90a1b8b479299fbb6f5572004 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 20 Oct 2024 21:27:40 +0200 Subject: [PATCH 32/34] Update conf.py --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 3af97a11b7a..1c131286eb7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -317,7 +317,7 @@ def setup_examples_pages(): # create md files for all examples # check what examples exist examples_folder = osp.abspath(osp.join(HERE, "..", "mesa", "examples")) - basic_examples = [f.path for f in os.scandir(osp.join(examples_folder, "basic")) if f.is_dir() ] + basic_examples = [f.path for f in os.scandir(osp.join(examples_folder, "basic")) if f.is_dir() and not f.name.startswith("__") ] advanced_examples = [] # fixme [f.path for f in os.scandir(osp.join(examples_folder, "advanced")) if f.is_dir()] examples = basic_examples + advanced_examples From 51633526728611284376ee5c9fb5dc2fbf4852d5 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 20 Oct 2024 21:27:50 +0200 Subject: [PATCH 33/34] Update conf.py --- docs/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1c131286eb7..31ed5611e7c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -354,6 +354,6 @@ def setup_examples_pages(): def setup(app): setup_examples_pages() - -if __name__ == "__main__": - setup_examples_pages() \ No newline at end of file +# +# if __name__ == "__main__": +# setup_examples_pages() \ No newline at end of file From 04e8763281c12925803ceafc8ae769cf07f9a1ea Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 20 Oct 2024 21:38:12 +0200 Subject: [PATCH 34/34] Update conf.py --- docs/conf.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 31ed5611e7c..65cfbbdc5ad 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,7 @@ import os import os.path as osp -import glob +import pathlib import sys import string from datetime import date @@ -324,6 +324,8 @@ def setup_examples_pages(): with open(os.path.join(HERE, "example_template.txt")) as fh: template = string.Template(fh.read()) + pathlib.Path(os.path.join(HERE, "examples")).mkdir(parents=True, exist_ok=True) + examples_md = [] for example in examples: base_name = os.path.basename(os.path.normpath(example)) @@ -355,5 +357,5 @@ def setup(app): setup_examples_pages() # -# if __name__ == "__main__": -# setup_examples_pages() \ No newline at end of file +if __name__ == "__main__": + setup_examples_pages() \ No newline at end of file