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/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/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: diff --git a/docs/conf.py b/docs/conf.py index 161edf5ff15..65cfbbdc5ad 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 pathlib 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,69 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} + + + +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 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() 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 + + 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)) + + 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(base_name) + + md_filepath = os.path.join(HERE, "examples", md_filename) + 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: + template = string.Template(fh.read()) + + 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]), + ) + ) + fh.write(content) + +def setup(app): + 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..1cbb67015e9 --- /dev/null +++ b/docs/example_template.txt @@ -0,0 +1,22 @@ + +$readme_file + +## Agents + +```python +$agent_file +``` + + +## Model + +```python +$model_file +``` + + +## App + +```python +$app_file +``` \ No newline at end of file diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 00000000000..11c450385bc --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,14 @@ + +# Examples + + +```{toctree} +:maxdepth: 1 + +boid flockers +virus on network +conways game of life +schelling +boltzmann wealth model + +``` \ 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..6ec38f12362 --- /dev/null +++ b/docs/examples_overview_template.txt @@ -0,0 +1,10 @@ + +# Examples + + +```{toctree} +:maxdepth: 1 + +$examples + +``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index eceefe6c4d2..b866ffbc913 100644 --- a/docs/index.md +++ b/docs/index.md @@ -89,6 +89,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/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`. > diff --git a/mesa/examples/basic/conways_game_of_life/agents.py b/mesa/examples/basic/conways_game_of_life/agents.py index 63017ff8118..17af7502572 100644 --- a/mesa/examples/basic/conways_game_of_life/agents.py +++ b/mesa/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 diff --git a/mesa/examples/basic/conways_game_of_life/app.py b/mesa/examples/basic/conways_game_of_life/app.py new file mode 100644 index 00000000000..bff865a8d12 --- /dev/null +++ b/mesa/examples/basic/conways_game_of_life/app.py @@ -0,0 +1,71 @@ +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") diff --git a/mesa/examples/basic/schelling/Readme.md b/mesa/examples/basic/schelling/Readme.md new file mode 100644 index 00000000000..af3f01b3563 --- /dev/null +++ b/mesa/examples/basic/schelling/Readme.md @@ -0,0 +1,47 @@ +# 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. diff --git a/mesa/examples/basic/virus_on_network/Readme.md b/mesa/examples/basic/virus_on_network/Readme.md new file mode 100644 index 00000000000..484a4c6ffdf --- /dev/null +++ b/mesa/examples/basic/virus_on_network/Readme.md @@ -0,0 +1,61 @@ +# 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.