diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 8e554855083..00000000000 --- a/.flake8 +++ /dev/null @@ -1,8 +0,0 @@ -[flake8] -# Ignore list taken from https://github.com/psf/black/blob/master/.flake8 -# E203 Whitespace before ':' -# E266 Too many leading '#' for block comment -# E501 Line too long (82 > 79 characters) -# W503 Line break occurred before a binary operator -ignore = E203, E266, E501, W503 -exclude = docs, build diff --git a/.github/workflows/build_lint.yml b/.github/workflows/build_lint.yml index 33d311b937b..ab9f22cdade 100644 --- a/.github/workflows/build_lint.yml +++ b/.github/workflows/build_lint.yml @@ -30,8 +30,12 @@ jobs: fail-fast: False matrix: os: [windows, ubuntu, macos] - python-version: ["3.10"] + python-version: ["3.12"] include: + - os: ubuntu + python-version: "3.11" + - os: ubuntu + python-version: "3.10" - os: ubuntu python-version: "3.9" - os: ubuntu @@ -41,7 +45,7 @@ jobs: # python-version: 'pypy-3.8' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: @@ -58,27 +62,28 @@ jobs: name: Codecov uses: codecov/codecov-action@v3 - lint-flake: + lint-ruff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.10 + - uses: actions/checkout@v4 + - name: Set up Python 3.12 uses: actions/setup-python@v4 with: - python-version: "3.10" - - run: pip install flake8 - - name: Lint with flake8 - # Use settings from mesas .flake8 file - run: flake8 . --count --show-source --statistics + python-version: "3.12" + - run: pip install ruff~=0.1.1 # Update periodically + - name: Lint with ruff + # Include `--format=github` to enable automatic inline annotations. + # Use settings from pyproject.toml. + run: ruff . --output-format=github --extend-exclude 'mesa/cookiecutter-mesa/*' lint-black: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.10 + - uses: actions/checkout@v4 + - name: Set up Python 3.12 uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - run: pip install black[jupyter] - name: Lint with black run: black --check --exclude=mesa/cookiecutter-mesa/* . diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 7fc3f4f0256..119fe8904e6 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -11,7 +11,7 @@ jobs: codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: codespell-project/actions-codespell@master with: ignore_words_file: .codespellignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 65f4a4faaa3..0f35c891e85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,17 +1,20 @@ name: Release on: [push, pull_request, workflow_dispatch] +permissions: + id-token: write + jobs: release: name: Deploy release to PyPI runs-on: ubuntu-latest steps: - name: Checkout source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: "3.12" - name: Install dependencies run: pip install -U pip build wheel setuptools - name: Build distributions diff --git a/.github/workflows/update-pipfile.yml b/.github/workflows/update-pipfile.yml deleted file mode 100644 index 59a90d522e3..00000000000 --- a/.github/workflows/update-pipfile.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: "Update Pipfile.lock" -on: - schedule: - - cron: '0 6 1 * *' # 1st day of each month at 06:00 UTC - push: - paths: - - 'Pipfile' - - '.github/workflows/update-pipfile.yml' - -jobs: - piplock: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: '3.9' - - run: pip install -U pip - - run: pip install -U wheel - - run: pip install -U pipenv - - run: pipenv lock - - uses: actions/upload-artifact@v3 - with: - name: "Pipfile lock" - path: Pipfile.lock - - uses: peter-evans/create-pull-request@v4 - with: - title: "Update Pipfile.lock (dependencies)" - branch: update-pipfile - base: main - commit-message: "[Bot] Update Pipfile.lock dependencies" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 396dfc75d10..f97b4c7298d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,17 +4,17 @@ ci: repos: - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 23.9.1 hooks: - id: black-jupyter exclude: ^mesa/cookiecutter-mesa/ - repo: https://github.com/asottile/pyupgrade - rev: v3.1.0 + rev: v3.14.0 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 # Use the ref you want to point at + rev: v4.4.0 # Use the ref you want to point at hooks: - id: trailing-whitespace - id: check-toml diff --git a/.readthedocs.yml b/.readthedocs.yml index 292b2461389..02b1d4649a5 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -12,12 +12,15 @@ sphinx: formats: - pdf +build: + os: "ubuntu-22.04" + tools: + python: "3.9" + # Optionally set the version of Python and requirements required to build your docs python: - version: "3.8" install: - method: pip path: . extra_requirements: - docs - system_packages: true diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 64a29f1dca3..c901713c1c7 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -28,11 +28,12 @@ discuss via `Matrix`_ OR via `an issue`_. - `Clone your repository`_ from Github to your machine. - Create a new branch in your fork: ``git checkout -b BRANCH_NAME`` - Run ``git config pull.rebase true``. This prevents messy merge commits when updating your branch on top of Mesa main branch. -- Install an editable version with developer requirements locally: ``pip install -e .[dev]`` +- Install an editable version with developer requirements locally: ``pip install -e ".[dev]"`` - Edit the code. Save. - Git add the new files and files with changes: ``git add FILE_NAME`` - Git commit your changes with a meaningful message: ``git commit -m "Fix issue X"`` - If implementing a new feature, include some documentation in docs folder. +- Make sure that your submission works with a few of the examples in the examples repository. If adding a new feature to mesa, please illustrate usage by implementing it in an example. - Make sure that your submission passes the `GH Actions build`_. See "Testing and Standards below" to be able to run these locally. - Make sure that your code is formatted according to `the black`_ standard (you can do it via `pre-commit`_). - Push your changes to your fork on Github: ``git push origin NAME_OF_BRANCH``. @@ -67,11 +68,11 @@ If you're changing previous Mesa features, please make sure of the following: - Additional features or rewrites of current features are accompanied by tests. - New features are demonstrated in a model, so folks can understand more easily. -To ensure that your submission will not break the build, you will need to install Flake8 and pytest. +To ensure that your submission will not break the build, you will need to install Ruff and pytest. .. code-block:: bash - pip install flake8 pytest pytest-cov + pip install ruff pytest pytest-cov We test by implementing simple models and through traditional unit tests in the tests/ folder. The following only covers unit tests coverage. Ensure that your test coverage has not gone down. If it has and you need help, we will offer advice on how to structure tests for the contribution. @@ -90,7 +91,7 @@ You should no longer have to worry about code formatting. If still in doubt you .. code-block:: bash - flake8 . --ignore=F403,E501,E123,E128,W504,W503 --exclude=docs,build + ruff . .. _`PEP8` : https://www.python.org/dev/peps/pep-0008 diff --git a/Dockerfile b/Dockerfile index 4d9fed52a1b..61348e690b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,25 @@ -FROM python:3.10-slim +# We can't use slim because we need either git/wget/curl to +# download mesa-examples, and so installing them requires +# updating the system anyway. +# We can't use alpine because NumPy doesn't support musllinux yet. +# But it's in the RC version https://github.com/numpy/numpy/issues/20089. +FROM python:bookworm LABEL maintainer="rht " # To use this Dockerfile: # 1. `docker build . -t mymesa_image` -# 2. `docker run --name mymesa_instance -p 8521:8521 -it mymesa_image` -# 3. In your browser, visit http://127.0.0.1:8521 +# 2. `docker run --name mymesa_instance -p 8765:8765 -it mymesa_image` +# 3. In your browser, visit http://127.0.0.1:8765 # -# Currently, this Dockerfile defaults to running the Wolf-Sheep model, as an +# Currently, this Dockerfile defaults to running the Schelling model, as an # illustration. If you want to run a different example, simply change the # MODEL_DIR variable below to point to another model, e.g. -# examples/sugarscape_cg or path to your custom model. +# /mesa-examples/examples/sugarscape_cg or path to your custom model. # You specify the MODEL_DIR (relative to this Git repo) by doing: -# `docker run --name mymesa_instance -p 8521:8521 -e MODEL_DIR=examples/sugarscape_cg -it mymesa_image` -# Note: the model directory MUST contain a run.py file. +# `docker run --name mymesa_instance -p 8765:8765 -e MODEL_DIR=/mesa-examples/examples/sugarscape_cg -it mymesa_image` +# Note: the model directory MUST contain an app.py file. -ENV MODEL_DIR=examples/wolf_sheep +ENV MODEL_DIR=/mesa-examples/examples/schelling_experimental # Don't buffer output: # https://docs.python.org/3.10/using/cmdline.html?highlight=pythonunbuffered#envvar-PYTHONUNBUFFERED @@ -24,21 +29,10 @@ WORKDIR /opt/mesa COPY . /opt/mesa -EXPOSE 8521/tcp - -# Important: we don't install python3-dev, python3-pip and so on because doing -# so will install Python 3.9 instead of the already available Python 3.10 from -# the base image. -# The following RUN command is still provided for context. -# RUN apt-get update && apt-get upgrade -y -o Dpkg::Options::="--force-confold" \ -# && apt-get install -y --no-install-recommends \ -# build-essential \ -# python3-dev \ -# python3-pip \ -# python3-setuptools \ -# python3-wheel \ -# && rm -rf /var/lib/apt/lists/* +RUN cd / && git clone https://github.com/projectmesa/mesa-examples.git + +EXPOSE 8765/tcp RUN pip3 install -e /opt/mesa -CMD ["sh", "-c", "cd $MODEL_DIR && python3 run.py"] +CMD ["sh", "-c", "cd $MODEL_DIR && solara run app.py --host=0.0.0.0"] diff --git a/HISTORY.rst b/HISTORY.rst index f62fb695070..d0bfb96a0e5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,237 @@ Release History --------------- +2.1.2 (2023-09-23) +++++++++++++++++++ + +This release contains fixes, and several improvements and new features to +the JupyterViz/Solara frontend. It's a patch release instead of a minor release +because the JupyterViz frontend is still considered experimental. + +**Improvements** + +* perf: Access grid only once #1751 +* docs: compile notebooks at build time #1753 +* docs: Remove nbsphinx and explicit .ipynb suffix #1754 +* rtd: Use gruvbox-dark as style #1719 +* build(deps): bump actions/checkout from 3 to 4 #1790 + +**Solara/JupyterViz** + +* solara: Implement visualization for network grid #1767 +* Add support for select input type #1779 +* Add step count display to JupyterViz #1775 +* Simplify solara code #1786 +* Add docstring for jupyterviz make_user_input that documents supported inputs #1784 +* Revise, test, & document JupyterViz options for drawing agent space #1783 +* Add UserInputs component #1788 +* Fix: Remove dict merge operator, python 3.8 compat #1793 +* feat: Add reset button to JupyterViz #1795 +* Add support for solara.Checkbox user input #1798 +* viz tutorial: Update custom plot to reflect new code #1799 +* fix: Don't continue playing when a model is reset #1796 +* Docker: Update to use Solara viz #1757 + +**Refactors** + +* Move viz stuff to mesa-viz-tornado Git repo #1746 +* simplify get neighborhood #1760 +* remove attrgetter performance optimization #1809 + +**Fixes** + +* fix: Add Matplotlib as dependency #1747 +* fix install for visualization tutorial in colab #1752 +* fix: Allow multiple connections in Solara #1759 +* Revert "Ensure sphinx>=7" #1762 +* fix README pic to remove line on left side #1763 +* space: Ensure get_neighborhood output & cache are immutable #1780 +* fix: Use .pytemplate for name for cookiecutter #1785 +* HISTORY.rst: Correct neighbor_iter() replacement in 2.0.0 #1807 +* docs: Always link to stable version #1810 +* Remove exclude_none_values #1813 + +2.1.1 (2023-08-02) ++++++++++++++++++++ + +This release improves the introductory and visualization tutorial. Ensures both are Google Colab compatible with +working badges. + +Changes: + * Update `intro_tutorial` to warn users to ensure up to date version, and make colab compatible #1739, #1744 + * Improve new/experimental Solara based visualization to ensure pause button works #1745 + * Fix bug in `space.py` -> `get_heading()` #1739 + +2.1.0 (2023-07-22) Youngtown ++++++++++++++++++++++++++++++ + +This release creates `mesa.experimental` namespace, this solves the issue that PyPI release will not allow git-based install. + +**Users should read the Mesa 2.0.0 release note (directly below this), as this contains the details about the breaking +changes and other major changes that were part of Mesa 2.0 release.** + +Changes: + * Creates `mesa.experimental` namespace #1736 + * Fix Ruff lint error #1737 + * Update permissions for PyPI #1732 + +2.0.0 (2023-07-15) Wellton +++++++++++++++++++++++++++ + +**Special notes** + +Mesa 2.0 includes: + * **an experimental pure python user interface/ visualization that is also jupyter compatible please see the** `visualization tutorial`_ + * an improved `datacollector` that allows collection by agent type + * several breaking changes that provide significant improvements to Mesa. + +.. _visualization tutorial: https://mesa.readthedocs.io/en/stable/tutorials/visualization_tutorial.html +**Breaking Changes:** + +* space: change `coord_iter` to return `(content,(x,y))` instead of `(content, x,y)`; this reduces known errors of scheduler to grid mismatch #1566, #1723 +* space: change NetworkGrid `get_neighbors` to `get_neighborhood`; improves performance #1542 +* space: raise exception when pos is out of bounds in `Grid.get_neighborhood` #1524 +* space: remove deprecations (#1520, #1687, #1688): + * `find_empty()`: convert this to `move_to_empty()` + * `num_agents`: removed parameter from `move_to_empty()` + * `position_agent()`: convert this to `place_agent` + * `neighbor_iter()`: convert this to `iter_neighbors()` +* batchrunner: remove deprecations #1627 + * `class BatchRunner` and `class BatchRunnerMP`: convert these to `batch_run()` + * Please see this `batch_run() example`_ if you would like to see an an implementation. +* visualization: easier visualization creation #1693 + * `UserSettableParameter(['number', 'slider','checkbox', 'choice', 'StaticText'])`: convert to `NumberInput` , `Slider`, `CheckBox`, `Choice`, `StaticText` + * Please see this `visualization example`_ if you would like to see an implementation. + +.. _batch_run() example: https://github.com/projectmesa/mesa-examples/blob/db2ec0383eb3b1868e91c828101e84cce97bbb63/examples/bank_reserves/batch_run.py#L188-L221 +.. _visualization example: https://github.com/projectmesa/mesa-examples/blob/db2ec0383eb3b1868e91c828101e84cce97bbb63/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py#L25-L32.) + + +**New Features:** + +* datacollector: can now handle data collection by agent type #1419, #1702 +* time: allows for model level `StageActivation` #1709 +* visualization: `ChartModule` can have dynamically named properties #1685 +* visualization: improved stop server to end visualizations #1646 +* *experimental* python front end option: integrated the initial prototype of the pure python front end option #1698, #1726 + + +**Improvements** + + +* update HexGrid and create HexSingleGrid and HexMultiGrid #1581 +* correct `get_heading` for toroidal space #1686 +* update slider to start at 1FPS #1674 +* update links to examples repo due to creation of mesa_examples #1636, #1637 +* ** CI Improvements** + * update Ruff #1724 + * remove Pipfile and Pipfile.lock #1692 + * enable Codespell in Jupyter #1695 + * improve regex for better build #1669, #1671 + * exclude notebooks form linter #1670 + * updated pip for zsh #1644 + * CLI quality of life improvements #1640 +* **Docs Improvements** + * update to PyData theme #1699 + * remove .rst to create simpler build #1363, #1624 + * use seaborn in tutorials #1718 + * fix types and errors in docs #1624, #1705, #1706, #1720 + * improve tutorials #1636, #1637, #1639, #1641, #1647, #1648, #1650, #1656, #1658, #1659, #1695, #1697, + * add nbsphinx to adv_tutorial #1694 + * replace `const chart` for `var chart` in advanced tutorial #1679 +* update LICENSE to 2023 #1683 + +1.2.1 (2023-03-18) +++++++++++++++++++ + +This release fixes https://github.com/projectmesa/mesa/issues/1606, where `mesa startproject` doesn't work. + +Changes: + +* fix: Include cookiecutter folders in install content #1611 +* Fix Ruff errors and pin Ruff version #1609 +* datacollector: Add warning when returning empty dataframe with no reporters defined #1614 + +1.2.0 (2023-03-09) Taylor +++++++++++++++++++++++++++ + +**Special notes** + +New features: + +* Implement radius for NetworkGrid.get_neighbors #1564 + +Some highlights for the perf improvements: + +* Use getattr for attribute strings in model data collection #1590 this is a 2x speedup over the relevant line +* Faster is_integer function for common cases #1597 is for 1.3x speedup for grid access (grid[x, y]) +* Refactor iter/get_cell_list_contents methods #1570 at least 1.3x speedup for iter/get_cell_list_contents +* Evaluate empties set more lazily #1546 (comment) ~1.3x speedup for place_agent, remove_agent, and move_agent + +**Improvements** + +* ci: Add testing on Python 3.11 #1519 +* Remove auto-update GH Actions for Pipfile.lock #1558 +* ruff + * ruff: Add isort #1594 + * ci: Replace flake8 with Ruff #1587 + * ruff: Add more rules based on Zulip's config #1596 +* perf: faster is_integer function for common cases #1597 +* Remove _reporter_decorator #1591 +* Change index at DataFrame creation in get_agent_vars_dataframe #1586 +* Make Grid class private #1575 +* Make the internal grid and empties_built in Grid class private #1568 +* Simplify code in ContinuousSpace #1536 +* Improve docstrings of ContinuousSpace #1535 +* Simplify accept_tuple_argument decorator in space.py #1531 +* Enhance schedulers to support intra-step removal of agents #1523 +* perf: Refactor iter_cell_list_contents Performance #1527 +* Replace two loops with dictionary comprehension, list- with generator comprehension #1458 +* Make MultiGrid.place_agent faster #1508 +* Update space module-level docstring summary #1518 +* Update NetworkGrid.__init__ docstring #1514 +* Deprecate SingleGrid.position_agent #1512 +* Make swap_pos part of Grid instead of SingleGrid #1507 +* Refactor NetworkGrid docstrings and iter/get_cell_list_contents #1498 +* Hexgrid: use get_neighborhood in iter_neighbors #1504 +* Auto update year for copyright in docs #1503 +* Refactor Grid.move_to_empty #1482 +* Put "Mesa" instead of "it" in README #1490 +* Batchrunner: Remove unnecessary dict transformation, .keys() in len() #1460 +* Add Dependabot configuration for GitHub Actions update check #1480 +* Use list transformation only when shuffled is True #1478 +* Implement swap_pos #1474 +* Clean up DataCollector #1475 + + +**Fixes** + +* Update resources in README #1605 +* Fix accident from https://github.com/projectmesa/mesa/pull/1488 #1489 +* pre-commit autoupdate #1598, #1576, #1548, #1494 +* Fix docstring of DataCollector #1592 +* Update Pipfile.lock (dependencies) #1495 #1487 +* build(deps): + * build(deps): bump codecov/codecov-action from 2 to 3 dependencies Pull requests that update a dependency file #1486 + * build(deps): bump actions/upload-artifact from 2 to 3 dependencies Pull requests that update a dependency file #1485 + * build(deps): bump peter-evans/create-pull-request from 3 to 4 dependencies Pull requests that update a dependency file #1484 + * build(deps): bump actions/setup-python from 3 to 4 dependencies Pull requests that update a dependency file #1483 +* Establish reproducibility for NetworkGrid.get_neighbors when radius > 1 #1569 +* Format js code #1554 +* Add some missing const declarations #1549 +* fix tutorial url in examples #1538 +* Update cookiecutter to flat import style. #1525 +* Fix bug in Grid.get_neighborhood #1517 +* Revert changes of #1478 and #1456 #1516 +* Fix return types of some NetworkGrid methods #1505 +* Update year for copyright #1501 +* Add default_value function to NetworkGrid #1497 +* Remove extraneous spaces from docstrings in modules 2 #1496 +* Remove extraneous spaces from docstrings in modules #1493 +* SingleGrid: Remove extraneous attribute declaration (empties) #1491 + + + 1.1.1 (2022-10-21) ++++++++++++++++++ diff --git a/LICENSE b/LICENSE index 137491e71c5..b130ab1f87c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2021 Core Mesa Team and contributors +Copyright 2023 Core Mesa Team and contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/MANIFEST.in b/MANIFEST.in index 58ade830f0e..a0cea1f60a5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,10 +3,10 @@ include LICENSE include HISTORY.rst include README.rst include setup.py -include mesa/cookiecutter-mesa/* include mesa/visualization/templates/*.html include mesa/visualization/templates/css/* include mesa/visualization/templates/fonts/* +graft mesa/cookiecutter-mesa graft mesa/visualization/templates/js graft mesa/visualization/templates/external global-exclude *.py[co] diff --git a/Pipfile b/Pipfile deleted file mode 100644 index da535315557..00000000000 --- a/Pipfile +++ /dev/null @@ -1,14 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] -pytest = "*" -pytest-cov = "*" - -[packages] -mesa = "*" - -[requires] -python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 31f0fa262ec..00000000000 --- a/Pipfile.lock +++ /dev/null @@ -1,492 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "3d8caf3ab53cebc0f6b41160cf246e4fbb7332bca299c7297c66f4c72b72d979" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "arrow": { - "hashes": [ - "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1", - "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2" - ], - "markers": "python_version >= '3.6'", - "version": "==1.2.3" - }, - "binaryornot": { - "hashes": [ - "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061", - "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4" - ], - "version": "==0.4.4" - }, - "certifi": { - "hashes": [ - "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", - "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.9.24" - }, - "chardet": { - "hashes": [ - "sha256:0368df2bfd78b5fc20572bb4e9bb7fb53e2c094f60ae9993339e8671d0afb8aa", - "sha256:d3e64f022d254183001eccc5db4040520c0f23b1a3f33d6413e099eb7f126557" - ], - "markers": "python_version >= '3.6'", - "version": "==5.0.0" - }, - "charset-normalizer": { - "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==2.1.1" - }, - "click": { - "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.3" - }, - "cookiecutter": { - "hashes": [ - "sha256:9f3ab027cec4f70916e28f03470bdb41e637a3ad354b4d65c765d93aad160022", - "sha256:f3982be8d9c53dac1261864013fdec7f83afd2e42ede6f6dd069c5e149c540d5" - ], - "markers": "python_version >= '3.7'", - "version": "==2.1.1" - }, - "idna": { - "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" - ], - "markers": "python_version >= '3.5'", - "version": "==3.4" - }, - "jinja2": { - "hashes": [ - "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", - "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.2" - }, - "jinja2-time": { - "hashes": [ - "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40", - "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa" - ], - "version": "==0.2.0" - }, - "markupsafe": { - "hashes": [ - "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", - "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", - "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", - "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", - "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", - "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", - "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", - "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", - "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", - "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", - "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", - "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", - "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", - "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", - "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", - "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", - "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", - "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", - "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", - "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", - "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", - "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", - "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", - "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", - "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", - "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", - "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", - "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", - "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", - "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", - "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", - "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", - "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", - "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", - "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", - "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", - "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", - "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", - "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", - "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" - ], - "markers": "python_version >= '3.7'", - "version": "==2.1.1" - }, - "mesa": { - "hashes": [ - "sha256:14f40b3c2948f02295b1a719b380cb1676a29154b0adebd4e537d431ebd78126", - "sha256:65757c5fe189a1abd87829f1a4aed496bd08874ab77f4d326e0a83e76707d14d" - ], - "index": "pypi", - "version": "==1.1.1" - }, - "networkx": { - "hashes": [ - "sha256:15cdf7f7c157637107ea690cabbc488018f8256fa28242aed0fb24c93c03a06d", - "sha256:815383fd52ece0a7024b5fd8408cc13a389ea350cd912178b82eed8b96f82cd3" - ], - "markers": "python_version >= '3.8'", - "version": "==2.8.7" - }, - "numpy": { - "hashes": [ - "sha256:0fe563fc8ed9dc4474cbf70742673fc4391d70f4363f917599a7fa99f042d5a8", - "sha256:12ac457b63ec8ded85d85c1e17d85efd3c2b0967ca39560b307a35a6703a4735", - "sha256:2341f4ab6dba0834b685cce16dad5f9b6606ea8a00e6da154f5dbded70fdc4dd", - "sha256:296d17aed51161dbad3c67ed6d164e51fcd18dbcd5dd4f9d0a9c6055dce30810", - "sha256:488a66cb667359534bc70028d653ba1cf307bae88eab5929cd707c761ff037db", - "sha256:4d52914c88b4930dafb6c48ba5115a96cbab40f45740239d9f4159c4ba779962", - "sha256:5e13030f8793e9ee42f9c7d5777465a560eb78fa7e11b1c053427f2ccab90c79", - "sha256:61be02e3bf810b60ab74e81d6d0d36246dbfb644a462458bb53b595791251911", - "sha256:7607b598217745cc40f751da38ffd03512d33ec06f3523fb0b5f82e09f6f676d", - "sha256:7a70a7d3ce4c0e9284e92285cba91a4a3f5214d87ee0e95928f3614a256a1488", - "sha256:7ab46e4e7ec63c8a5e6dbf5c1b9e1c92ba23a7ebecc86c336cb7bf3bd2fb10e5", - "sha256:8981d9b5619569899666170c7c9748920f4a5005bf79c72c07d08c8a035757b0", - "sha256:8c053d7557a8f022ec823196d242464b6955a7e7e5015b719e76003f63f82d0f", - "sha256:926db372bc4ac1edf81cfb6c59e2a881606b409ddc0d0920b988174b2e2a767f", - "sha256:95d79ada05005f6f4f337d3bb9de8a7774f259341c70bc88047a1f7b96a4bcb2", - "sha256:95de7dc7dc47a312f6feddd3da2500826defdccbc41608d0031276a24181a2c0", - "sha256:a0882323e0ca4245eb0a3d0a74f88ce581cc33aedcfa396e415e5bba7bf05f68", - "sha256:a8365b942f9c1a7d0f0dc974747d99dd0a0cdfc5949a33119caf05cb314682d3", - "sha256:a8aae2fb3180940011b4862b2dd3756616841c53db9734b27bb93813cd79fce6", - "sha256:c237129f0e732885c9a6076a537e974160482eab8f10db6292e92154d4c67d71", - "sha256:c67b833dbccefe97cdd3f52798d430b9d3430396af7cdb2a0c32954c3ef73894", - "sha256:ce03305dd694c4873b9429274fd41fc7eb4e0e4dea07e0af97a933b079a5814f", - "sha256:d331afac87c92373826af83d2b2b435f57b17a5c74e6268b79355b970626e329", - "sha256:dada341ebb79619fe00a291185bba370c9803b1e1d7051610e01ed809ef3a4ba", - "sha256:ed2cc92af0efad20198638c69bb0fc2870a58dabfba6eb722c933b48556c686c", - "sha256:f260da502d7441a45695199b4e7fd8ca87db659ba1c78f2bbf31f934fe76ae0e", - "sha256:f2f390aa4da44454db40a1f0201401f9036e8d578a25f01a6e237cea238337ef", - "sha256:f76025acc8e2114bb664294a07ede0727aa75d63a06d2fae96bf29a81747e4a7" - ], - "markers": "python_version >= '3.8'", - "version": "==1.23.4" - }, - "pandas": { - "hashes": [ - "sha256:04e51b01d5192499390c0015630975f57836cc95c7411415b499b599b05c0c96", - "sha256:05c527c64ee02a47a24031c880ee0ded05af0623163494173204c5b72ddce658", - "sha256:0a78e05ec09731c5b3bd7a9805927ea631fe6f6cb06f0e7c63191a9a778d52b4", - "sha256:17da7035d9e6f9ea9cdc3a513161f8739b8f8489d31dc932bc5a29a27243f93d", - "sha256:249cec5f2a5b22096440bd85c33106b6102e0672204abd2d5c014106459804ee", - "sha256:2c25e5c16ee5c0feb6cf9d982b869eec94a22ddfda9aa2fbed00842cbb697624", - "sha256:32e3d9f65606b3f6e76555bfd1d0b68d94aff0929d82010b791b6254bf5a4b96", - "sha256:36aa1f8f680d7584e9b572c3203b20d22d697c31b71189322f16811d4ecfecd3", - "sha256:5b0c970e2215572197b42f1cff58a908d734503ea54b326412c70d4692256391", - "sha256:5cee0c74e93ed4f9d39007e439debcaadc519d7ea5c0afc3d590a3a7b2edf060", - "sha256:669c8605dba6c798c1863157aefde959c1796671ffb342b80fcb80a4c0bc4c26", - "sha256:66a1ad667b56e679e06ba73bb88c7309b3f48a4c279bd3afea29f65a766e9036", - "sha256:683779e5728ac9138406c59a11e09cd98c7d2c12f0a5fc2b9c5eecdbb4a00075", - "sha256:6bb391659a747cf4f181a227c3e64b6d197100d53da98dcd766cc158bdd9ec68", - "sha256:81f0674fa50b38b6793cd84fae5d67f58f74c2d974d2cb4e476d26eee33343d0", - "sha256:927e59c694e039c75d7023465d311277a1fc29ed7236b5746e9dddf180393113", - "sha256:932d2d7d3cab44cfa275601c982f30c2d874722ef6396bb539e41e4dc4618ed4", - "sha256:a52419d9ba5906db516109660b114faf791136c94c1a636ed6b29cbfff9187ee", - "sha256:b156a971bc451c68c9e1f97567c94fd44155f073e3bceb1b0d195fd98ed12048", - "sha256:bcf1a82b770b8f8c1e495b19a20d8296f875a796c4fe6e91da5ef107f18c5ecb", - "sha256:cb2a9cf1150302d69bb99861c5cddc9c25aceacb0a4ef5299785d0f5389a3209", - "sha256:d8c709f4700573deb2036d240d140934df7e852520f4a584b2a8d5443b71f54d", - "sha256:db45b94885000981522fb92349e6b76f5aee0924cc5315881239c7859883117d", - "sha256:ddf46b940ef815af4e542697eaf071f0531449407a7607dd731bf23d156e20a7", - "sha256:e675f8fe9aa6c418dc8d3aac0087b5294c1a4527f1eacf9fe5ea671685285454", - "sha256:eb7e8cf2cf11a2580088009b43de84cabbf6f5dae94ceb489f28dba01a17cb77", - "sha256:f340331a3f411910adfb4bbe46c2ed5872d9e473a783d7f14ecf49bc0869c594" - ], - "markers": "python_version >= '3.8'", - "version": "==1.5.1" - }, - "python-dateutil": { - "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" - }, - "python-slugify": { - "hashes": [ - "sha256:272d106cb31ab99b3496ba085e3fea0e9e76dcde967b5e9992500d1f785ce4e1", - "sha256:7b2c274c308b62f4269a9ba701aa69a797e9bca41aeee5b3a9e79e36b6656927" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==6.1.2" - }, - "pytz": { - "hashes": [ - "sha256:335ab46900b1465e714b4fda4963d87363264eb662aab5e65da039c25f1f5b22", - "sha256:c4d88f472f54d615e9cd582a5004d1e5f624854a6a27a6211591c251f22a6914" - ], - "version": "==2022.5" - }, - "pyyaml": { - "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0" - }, - "requests": { - "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" - ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==2.28.1" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "text-unidecode": { - "hashes": [ - "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", - "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" - ], - "version": "==1.3" - }, - "tornado": { - "hashes": [ - "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca", - "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72", - "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23", - "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8", - "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b", - "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9", - "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13", - "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75", - "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac", - "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e", - "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b" - ], - "markers": "python_version >= '3.7'", - "version": "==6.2" - }, - "tqdm": { - "hashes": [ - "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4", - "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.64.1" - }, - "urllib3": { - "hashes": [ - "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", - "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.12" - } - }, - "develop": { - "attrs": { - "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" - ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" - }, - "coverage": { - "extras": [ - "toml" - ], - "hashes": [ - "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79", - "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", - "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f", - "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a", - "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa", - "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398", - "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba", - "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d", - "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf", - "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b", - "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518", - "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d", - "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795", - "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2", - "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e", - "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32", - "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745", - "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b", - "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e", - "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d", - "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f", - "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660", - "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62", - "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6", - "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04", - "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c", - "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5", - "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef", - "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc", - "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae", - "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578", - "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466", - "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4", - "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91", - "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0", - "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4", - "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b", - "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe", - "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b", - "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75", - "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b", - "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c", - "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72", - "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b", - "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f", - "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e", - "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53", - "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3", - "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", - "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987" - ], - "markers": "python_version >= '3.7'", - "version": "==6.5.0" - }, - "exceptiongroup": { - "hashes": [ - "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41", - "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad" - ], - "markers": "python_version < '3.11'", - "version": "==1.0.0" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, - "pytest": { - "hashes": [ - "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71", - "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59" - ], - "index": "pypi", - "version": "==7.2.0" - }, - "pytest-cov": { - "hashes": [ - "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b", - "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470" - ], - "index": "pypi", - "version": "==4.0.0" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - } - } -} diff --git a/README.rst b/README.rst index aec8be7c259..b3278af6ecc 100644 --- a/README.rst +++ b/README.rst @@ -22,8 +22,7 @@ Mesa allows users to quickly create agent-based models using built-in core compo :alt: A screenshot of the Schelling Model in Mesa *Above: A Mesa implementation of the Schelling segregation model, -being visualized in a browser window and analyzed in a Jupyter -notebook.* +this can be displayed in browser windows or Jupyter.* .. _`Mesa` : https://github.com/projectmesa/mesa/ @@ -43,33 +42,35 @@ Getting started quickly: .. code-block:: bash - $ pip install mesa + pip install mesa You can also use `pip` to install the github version: .. code-block:: bash - $ pip install -U -e git+https://github.com/projectmesa/mesa@main#egg=mesa + pip install -U -e git+https://github.com/projectmesa/mesa@main#egg=mesa Or any other (development) branch on this repo or your own fork: .. code-block:: bash - $ pip install -U -e git+https://github.com/YOUR_FORK/mesa@YOUR_BRANCH#egg=mesa + pip install -U -e git+https://github.com/YOUR_FORK/mesa@YOUR_BRANCH#egg=mesa -Take a look at the `examples `_ folder for sample models demonstrating Mesa features. +For resources or help on using Mesa, check out the following: -For more help on using Mesa, check out the following resources: +* `Intro to Mesa Tutorial`_ (An introductory model, the Boltzmann Wealth Model, for beginners or those new to Mesa.) +* `Complexity Explorer Tutorial`_ (An advanced-beginner model, SugarScape with Traders, with instructional videos) +* `Mesa Examples`_ (A repository of seminal ABMs using Mesa and examples of employing specific Mesa Features) +* `Docs`_ (Mesa's documentation, API and useful snippets) +* `Discussions`_ (GitHub threaded discussions about Mesa) +* `Matrix Chat`_ (Chat Forum via Matrix to talk about Mesa) -* `Intro to Mesa Tutorial`_ -* `Docs`_ -* `Email list for users`_ -* `PyPI`_ - -.. _`Intro to Mesa Tutorial` : http://mesa.readthedocs.org/en/main/tutorials/intro_tutorial.html -.. _`Docs` : http://mesa.readthedocs.org/en/main/ -.. _`Email list for users` : https://groups.google.com/d/forum/projectmesa -.. _`PyPI` : https://pypi.python.org/pypi/Mesa/ +.. _`Intro to Mesa Tutorial` : http://mesa.readthedocs.org/en/stable/tutorials/intro_tutorial.html +.. _`Complexity Explorer Tutorial` : https://www.complexityexplorer.org/courses/172-agent-based-models-with-python-an-introduction-to-mesa +.. _`Mesa Examples` : https://github.com/projectmesa/mesa-examples/tree/main/examples +.. _`Docs` : http://mesa.readthedocs.org/ +.. _`Discussions` : https://github.com/projectmesa/mesa/discussions +.. _`Matrix Chat` : https://matrix.to/#/#project-mesa:matrix.org Running Mesa in Docker ------------------------ @@ -84,21 +85,21 @@ If you are a Mesa developer, first `install Docker Compose `_ model is +`_ model is a good example of a small well-packaged model. It's easy to create a cookiecutter mesa model by running ``mesa startproject`` diff --git a/docs/conf.py b/docs/conf.py index d682ce1a8ba..d5459150139 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,6 +14,7 @@ import sys import os +from datetime import date # If extensions (or modules to document with autodoc) are in another directory, @@ -41,6 +42,7 @@ "sphinx.ext.mathjax", "sphinx.ext.ifconfig", "sphinx.ext.viewcode", + "myst_nb", ] # Add any paths that contain templates here, relative to this directory. @@ -57,7 +59,7 @@ # General information about the project. project = "Mesa" -copyright = "2015-2021, Project Mesa Team" +copyright = f"2015-{date.today().year}, Project Mesa Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -104,7 +106,7 @@ # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" +pygments_style = "gruvbox-dark" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -112,12 +114,14 @@ # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False +nb_execution_timeout = 60 +nb_execution_mode = "cache" # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = "default" +html_theme = "pydata_sphinx_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/useful-snippets/snippets.rst b/docs/howto.rst similarity index 73% rename from docs/useful-snippets/snippets.rst rename to docs/howto.rst index 4c7e8e418f1..09792af5bbd 100644 --- a/docs/useful-snippets/snippets.rst +++ b/docs/howto.rst @@ -1,7 +1,7 @@ -Useful Snippets -=============== +How-to Guide +============ -A collection of useful code snippets. Here you can find code that allows you to get to get started on common tasks in Mesa. +Here you can find code that allows you to get to get started on common tasks in Mesa. Models with Discrete Time ------------------------- @@ -12,6 +12,16 @@ If you have `Multiple` type agents and one of them has time attribute you can st if self.model.schedule.time in self.discrete_time: self.model.space.move_agent(self, new_pos) +Implementing Model Level Functions in Staged Activation +------------------------------------------------------- +In staged activation, if you may want a function to be implemented only on the model level and not at the level of agents. +For such functions, include the prefix "model." before the model function name, when defining the function list. +For example, consider a central employment exchange which adjust the wage rate common to all laborers +in the direction of excess demand. + +.. code:: python + stage_list=[Send_Labour_Supply, Send_Labour_Demand, model.Adjust_Wage_Rate] + self.schedule = StagedActivation(self,stage_list,shuffle=True) Using ```numpy.random``` ------------- diff --git a/docs/images/Mesa_Screenshot.png b/docs/images/Mesa_Screenshot.png index ce1069a1199..5159d3501eb 100644 Binary files a/docs/images/Mesa_Screenshot.png and b/docs/images/Mesa_Screenshot.png differ diff --git a/docs/index.rst b/docs/index.rst index 94fd22f89d0..83382e1bdaa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,24 +44,24 @@ Getting started quickly: .. code-block:: bash - $ pip install mesa + pip install mesa To launch an example model, clone the `repository `_ folder and invoke ``mesa runserver`` for one of the ``examples/`` subdirectories: .. code-block:: bash - $ mesa runserver examples/wolf_sheep + mesa runserver examples/wolf_sheep For more help on using Mesa, check out the following resources: * `Mesa Introductory Tutorial`_ -* `Mesa Advanced Tutorial`_ +* `Mesa Visualization Tutorial`_ * `GitHub Issue Tracker`_ * `Email list`_ * `PyPI`_ .. _`Mesa Introductory Tutorial` : tutorials/intro_tutorial.html -.. _`Mesa Advanced Tutorial` : tutorials/adv_tutorial.html +.. _`Mesa Visualization Tutorial` : tutorials/visualization_tutorial.html .. _`GitHub Issue Tracker` : https://github.com/projectmesa/mesa/issues .. _`Email list` : https://groups.google.com/d/forum/projectmesa .. _`PyPI` : https://pypi.python.org/pypi/Mesa/ @@ -96,11 +96,12 @@ ABM features users have shared that you may want to use in your model Mesa Overview tutorials/intro_tutorial - tutorials/adv_tutorial + tutorials/visualization_tutorial Best Practices - Useful Snippets + How-to Guide API Documentation Mesa Packages + tutorials/adv_tutorial_legacy.ipynb Indices and tables ================== diff --git a/docs/overview.rst b/docs/overview.rst index 9c1e3b0c080..b2b9ddfc6e6 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -43,7 +43,7 @@ The skeleton of a model might look like this: class MyModel(mesa.Model): def __init__(self, n_agents): super().__init__() - self.schedule = mesa.timeRandomActivation(self) + self.schedule = mesa.time.RandomActivation(self) self.grid = mesa.space.MultiGrid(10, 10, torus=True) for i in range(n_agents): a = MyAgent(i, self) @@ -103,16 +103,18 @@ The data collector will collect the specified model- and agent-level data at eac agent_df = model.dc.get_agent_vars_dataframe() -To batch-run the model while varying, for example, the n_agents parameter, you'd use the batchrunner: +To batch-run the model while varying, for example, the n_agents parameter, you'd use the `batch_run` function: .. code:: python - from mesa.batchrunner import BatchRunner + import mesa parameters = {"n_agents": range(1, 20)} - batch_run = BatchRunner(MyModel, parameters, max_steps=10, - model_reporters={"n_agents": lambda m: m.schedule.get_agent_count()}) - batch_run.run_all() + mesa.batch_run( + MyModel, + parameters, + max_steps=10, + ) As with the data collector, once the runs are all over, you can extract the data as a data frame. @@ -152,5 +154,3 @@ To quickly spin up a model visualization, you might do something like: server.launch() This will launch the browser-based visualization, on the default port 8521. - - diff --git a/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py b/docs/tutorials/MoneyModel.py similarity index 76% rename from examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py rename to docs/tutorials/MoneyModel.py index 76ebc516b36..d6b8de0d66e 100644 --- a/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py +++ b/docs/tutorials/MoneyModel.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python3 + + import mesa @@ -9,43 +12,6 @@ def compute_gini(model): return 1 + (1 / N) - 2 * B -class BoltzmannWealthModel(mesa.Model): - """A simple model of an economy where agents exchange currency at random. - - All the agents begin with one unit of currency, and each time step can give - a unit of currency to another agent. Note how, over time, this produces a - highly skewed distribution of wealth. - """ - - def __init__(self, N=100, width=10, height=10): - self.num_agents = N - self.grid = mesa.space.MultiGrid(width, height, True) - self.schedule = mesa.time.RandomActivation(self) - self.datacollector = mesa.DataCollector( - model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"} - ) - # Create agents - for i in range(self.num_agents): - a = MoneyAgent(i, self) - self.schedule.add(a) - # Add the agent to a random grid cell - x = self.random.randrange(self.grid.width) - y = self.random.randrange(self.grid.height) - self.grid.place_agent(a, (x, y)) - - self.running = True - self.datacollector.collect(self) - - def step(self): - self.schedule.step() - # collect data - self.datacollector.collect(self) - - def run_model(self, n): - for i in range(n): - self.step() - - class MoneyAgent(mesa.Agent): """An agent with fixed initial wealth.""" @@ -71,3 +37,29 @@ def step(self): self.move() if self.wealth > 0: self.give_money() + + +class MoneyModel(mesa.Model): + """A model with some number of agents.""" + + def __init__(self, N, width, height): + self.num_agents = N + self.grid = mesa.space.MultiGrid(width, height, True) + self.schedule = mesa.time.RandomActivation(self) + + # Create agents + for i in range(self.num_agents): + a = MoneyAgent(i, self) + self.schedule.add(a) + # Add the agent to a random grid cell + x = self.random.randrange(self.grid.width) + y = self.random.randrange(self.grid.height) + self.grid.place_agent(a, (x, y)) + + self.datacollector = mesa.DataCollector( + model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"} + ) + + def step(self): + self.datacollector.collect(self) + self.schedule.step() diff --git a/docs/tutorials/adv_tutorial.rst b/docs/tutorials/adv_tutorial.rst deleted file mode 100644 index 5f23a0e864b..00000000000 --- a/docs/tutorials/adv_tutorial.rst +++ /dev/null @@ -1,560 +0,0 @@ -Advanced Tutorial -================= - -Adding visualization -~~~~~~~~~~~~~~~~~~~~ - -So far, we’ve built a model, run it, and analyzed some output -afterwards. However, one of the advantages of agent-based models is that -we can often watch them run step by step, potentially spotting -unexpected patterns, behaviors or bugs, or developing new intuitions, -hypotheses, or insights. Other times, watching a model run can explain -it to an unfamiliar audience better than static explanations. Like many -ABM frameworks, Mesa allows you to create an interactive visualization -of the model. In this section we’ll walk through creating a -visualization using built-in components, and (for advanced users) how to -create a new visualization element. - -**Note for Jupyter users: Due to conflicts with the tornado server Mesa -uses and Jupyter, the interactive browser of your model will load but -likely not work. This will require you to run the code from .py -files. The Mesa development team is working to develop a** `Jupyter -compatible interface `_. - -First, a quick explanation of how Mesa’s interactive visualization -works. Visualization is done in a browser window, using JavaScript to -draw the different things being visualized at each step of the model. To -do this, Mesa launches a small web server, which runs the model, turns -each step into a JSON object (essentially, structured plain text) and -sends those steps to the browser. - -A visualization is built up of a few different modules: for example, a -module for drawing agents on a grid, and another one for drawing a chart -of some variable. Each module has a Python part, which runs on the -server and turns a model state into JSON data; and a JavaScript side, -which takes that JSON data and draws it in the browser window. Mesa -comes with a few modules built in, and let you add your own as well. - - -Grid Visualization -^^^^^^^^^^^^^^^^^^ - -To start with, let’s have a visualization where we can watch the agents -moving around the grid. For this, you will need to put your model code -in a separate Python source file; for example, ``MoneyModel.py``. Next, -either in the same file or in a new one (e.g. ``MoneyModel_Viz.py``) -import the server class and the Canvas Grid class (so-called because it -uses HTML5 canvas to draw a grid). If you’re in a new file, you’ll also -need to import the actual model object. - -.. code:: ipython3 - - import mesa - - # If MoneyModel.py is where your code is: - # from MoneyModel import MoneyModel - -``CanvasGrid`` works by looping over every cell in a grid, and -generating a portrayal for every agent it finds. A portrayal is a -dictionary (which can easily be turned into a JSON object) which tells -the JavaScript side how to draw it. The only thing we need to provide is -a function which takes an agent, and returns a portrayal object. Here’s -the simplest one: it’ll draw each agent as a red, filled circle which -fills half of each cell. - -.. code:: ipython3 - - def agent_portrayal(agent): - portrayal = { - "Shape": "circle", - "Color": "red", - "Filled": "true", - "Layer": 0, - "r": 0.5, - } - return portrayal - -In addition to the portrayal method, we instantiate a canvas grid with -its width and height in cells, and in pixels. In this case, let’s create -a 10x10 grid, drawn in 500 x 500 pixels. - -.. code:: ipython3 - - grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500) - -.. code:: ipython3 - - """ - The full code should now look like: - """ - # from MoneyModel import * - import mesa - - - def agent_portrayal(agent): - portrayal = { - "Shape": "circle", - "Filled": "true", - "Layer": 0, - "Color": "red", - "r": 0.5, - } - return portrayal - - - grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500) - server = mesa.visualization.ModularServer( - MoneyModel, [grid], "Money Model", {"N": 100, "width": 10, "height": 10} - ) - server.port = 8521 # The default - server.launch() - -Now we create and launch the actual server. We do this with the -following arguments: - -- The model class we’re running and visualizing; in this case, - ``MoneyModel``. -- A list of module objects to include in the visualization; here, just - ``[grid]`` -- The title of the model: “Money Model” -- Any inputs or arguments for the model itself. In this case, 100 - agents, and height and width of 10. - -Once we create the server, we set the port for it to listen on (you can -treat this as just a piece of the URL you’ll open in the browser). -Finally, when you’re ready to run the visualization, use the server’s -``launch()`` method. - -.. code:: python - - server = ModularServer(MoneyModel, - [grid], - "Money Model", - {"N":100, "width":10, "height":10}) - server.port = 8521 # The default - server.launch() - -The full code should now look like: - -.. code:: python - - from MoneyModel import * - import mesa - - - def agent_portrayal(agent): - portrayal = {"Shape": "circle", - "Filled": "true", - "Layer": 0, - "Color": "red", - "r": 0.5} - return portrayal - - grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500) - server = mesa.visualization.ModularServer( - MoneyModel, [grid], "Money Model", {"N": 100, "width": 10, "height": 10} - )server = ModularServer(MoneyModel, - [grid], - "Money Model", - {"N":100, "width":10, "height":10}) - server.port = 8521 # The default - server.launch() - -Now run this file; this should launch the interactive visualization -server and open your web browser automatically. (If the browser doesn’t -open automatically, try pointing it at http://127.0.0.1:8521 manually. -If this doesn’t show you the visualization, something may have gone -wrong with the server launch.) - -You should see something like the figure below: the model title, an -empty space where the grid will be, and a control panel off to the -right. - -.. figure:: files/viz_empty.png - :alt: Empty Visualization - - Empty Visualization - -Click the ‘reset’ button on the control panel, and you should see the -grid fill up with red circles, representing agents. - -.. figure:: files/viz_redcircles.png - :alt: Redcircles Visualization - - Redcircles Visualization - -Click ‘step’ to advance the model by one step, and the agents will move -around. Click ‘run’ and the agents will keep moving around, at the rate -set by the ‘fps’ (frames per second) slider at the top. Try moving it -around and see how the speed of the model changes. Pressing ‘pause’ will -(as you’d expect) pause the model; presing ‘run’ again will restart it. -Finally, ‘reset’ will start a new instantiation of the model. - -To stop the visualization server, go back to the terminal where you -launched it, and press Control+c. - -Changing the agents -^^^^^^^^^^^^^^^^^^^ - -In the visualization above, all we could see is the agents moving around -– but not how much money they had, or anything else of interest. Let’s -change it so that agents who are broke (wealth 0) are drawn in grey, -smaller, and above agents who still have money. - -To do this, we go back to our ``agent_portrayal`` code and add some code -to change the portrayal based on the agent properties. - -.. code:: python - - def agent_portrayal(agent): - portrayal = {"Shape": "circle", - "Filled": "true", - "r": 0.5} - - if agent.wealth > 0: - portrayal["Color"] = "red" - portrayal["Layer"] = 0 - else: - portrayal["Color"] = "grey" - portrayal["Layer"] = 1 - portrayal["r"] = 0.2 - return portrayal - -Now launch the server again - this will open a new browser window -pointed at the updated visualization. Initially it looks the same, but -advance the model and smaller grey circles start to appear. Note that -since the zero-wealth agents have a higher layer number, they are drawn -on top of the red agents. - -.. figure:: files/viz_greycircles.png - :alt: Greycircles Visualization - - Greycircles Visualization - -Adding a chart -^^^^^^^^^^^^^^ - -Next, let’s add another element to the visualization: a chart, tracking -the model’s Gini Coefficient. This is another built-in element that Mesa -provides. - -The basic chart pulls data from the model’s DataCollector, and draws it -as a line graph using the `Charts.js `__ -JavaScript libraries. We instantiate a chart element with a list of -series for the chart to track. Each series is defined in a dictionary, -and has a ``Label`` (which must match the name of a model-level variable -collected by the DataCollector) and a ``Color`` name. We can also give -the chart the name of the DataCollector object in the model. - -Finally, we add the chart to the list of elements in the server. The -elements are added to the visualization in the order they appear, so the -chart will appear underneath the grid. - -.. code:: python - - chart = mesa.visualization.ChartModule([{"Label": "Gini", - "Color": "Black"}], - data_collector_name='datacollector') - - server = mesa.visualization.ModularServer(MoneyModel, - [grid, chart], - "Money Model", - {"N":100, "width":10, "height":10}) - -Launch the visualization and start a model run, and you’ll see a line -chart underneath the grid. Every step of the model, the line chart -updates along with the grid. Reset the model, and the chart resets too. - -.. figure:: files/viz_chart.png - :alt: Chart Visualization - - Chart Visualization - -**Note:** You might notice that the chart line only starts after a -couple of steps; this is due to a bug in Charts.js which will hopefully -be fixed soon. - -Building your own visualization component -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Note:** This section is for users who have a basic familiarity with -JavaScript. If that’s not you, don’t worry! (If you’re an advanced -JavaScript coder and find things that we’ve done wrong or inefficiently, -please `let us know `__!) - -If the visualization elements provided by Mesa aren’t enough for you, -you can build your own and plug them into the model server. - -First, you need to understand how the visualization works under the -hood. Remember that each visualization module has two sides: a Python -object that runs on the server and generates JSON data from the model -state (the server side), and a JavaScript object that runs in the -browser and turns the JSON into something it renders on the screen (the -client side). - -Obviously, the two sides of each visualization must be designed in -tandem. They result in one Python class, and one JavaScript ``.js`` -file. The path to the JavaScript file is a property of the Python class. - -For this example, let’s build a simple histogram visualization, which -can count the number of agents with each value of wealth. We’ll use the -`Charts.js `__ JavaScript library, which is -already included with Mesa. If you go and look at its documentation, -you’ll see that it had no histogram functionality, which means we have -to build our own out of a bar chart. We’ll keep the histogram as simple -as possible, giving it a fixed number of integer bins. If you were -designing a more general histogram to add to the Mesa repository for -everyone to use across different models, obviously you’d want something -more general. - -Client-Side Code -^^^^^^^^^^^^^^^^ - -In general, the server- and client-side are written in tandem. However, -if you’re like me and more comfortable with Python than JavaScript, it -makes sense to figure out how to get the JavaScript working first, and -then write the Python to be compatible with that. - -In the same directory as your model, create a new file called -``HistogramModule.js``. This will store the JavaScript code for the -client side of the new module. - -JavaScript classes can look alien to people coming from other languages -– specifically, they can look like functions. (The Mozilla `Introduction -to Object-Oriented -JavaScript `__ -is a good starting point). In ``HistogramModule.js``, start by creating -the class itself: - -.. code:: javascript - - const HistogramModule = function(bins, canvas_width, canvas_height) { - // The actual code will go here. - }; - -Note that our object is instantiated with three arguments: the number of -integer bins, and the width and height (in pixels) the chart will take -up in the visualization window. - -When the visualization object is instantiated, the first thing it needs -to do is prepare to draw on the current page. To do so, it adds a -`canvas `__ -tag to the page. It also gets the canvas' context, which is required for doing -anything with it. - -.. code:: javascript - - const HistogramModule = function(bins, canvas_width, canvas_height) { - // Create the canvas object: - const canvas = document.createElement("canvas"); - Object.assign(canvas, { - width: canvas_width, - height: canvas_height, - style: "border:1px dotted", - }); - // Append it to #elements: - const elements = document.getElementById("elements"); - elements.appendChild(canvas); - - // Create the context and the drawing controller: - const context = canvas.getContext("2d"); - }; - - -Look at the Charts.js `bar chart -documentation `__. -You’ll see some of the boilerplate needed to get a chart set up. -Especially important is the ``data`` object, which includes the -datasets, labels, and color options. In this case, we want just one -dataset (we’ll keep things simple and name it “Data”); it has ``bins`` -for categories, and the value of each category starts out at zero. -Finally, using these boilerplate objects and the canvas context we -created, we can create the chart object. - -.. code:: javascript - - const HistogramModule = function(bins, canvas_width, canvas_height) { - // Create the canvas object: - const canvas = document.createElement("canvas"); - Object.assign(canvas, { - width: canvas_width, - height: canvas_height, - style: "border:1px dotted", - }); - // Append it to #elements: - const elements = document.getElementById("elements"); - elements.appendChild(canvas); - - // Create the context and the drawing controller: - const context = canvas.getContext("2d"); - - // Prep the chart properties and series: - const datasets = [{ - label: "Data", - fillColor: "rgba(151,187,205,0.5)", - strokeColor: "rgba(151,187,205,0.8)", - highlightFill: "rgba(151,187,205,0.75)", - highlightStroke: "rgba(151,187,205,1)", - data: [] - }]; - - // Add a zero value for each bin - for (var i in bins) - datasets[0].data.push(0); - - const data = { - labels: bins, - datasets: datasets - }; - - const options = { - scaleBeginsAtZero: true - }; - - // Create the chart object - const chart = new Chart(context, {type: 'bar', data: data, options: options}); - - // Now what? - }; - -There are two methods every client-side visualization class must -implement to be able to work: ``render(data)`` to render the incoming -data, and ``reset()`` which is called to clear the visualization when -the user hits the reset button and starts a new model run. - -In this case, the easiest way to pass data to the histogram is as an -array, one value for each bin. We can then just loop over the array and -update the values in the chart’s dataset. - -There are a few ways to reset the chart, but the easiest is probably to -destroy it and create a new chart object in its place. - -With that in mind, we can add these two methods to the class: - -.. code:: javascript - - const HistogramModule = function(bins, canvas_width, canvas_height) { - // ...Everything from above... - this.render = function(data) { - datasets[0].data = data; - chart.update(); - }; - - this.reset = function() { - chart.destroy(); - chart = new Chart(context, {type: 'bar', data: data, options: options}); - }; - }; -Note the ``this``. before the method names. This makes them public and -ensures that they are accessible outside of the object itself. All the -other variables inside the class are only accessible inside the object -itself, but not outside of it. - -Server-Side Code -^^^^^^^^^^^^^^^^ - -Can we get back to Python code? Please? - -Every JavaScript visualization element has an equal and opposite -server-side Python element. The Python class needs to also have a -``render`` method, to get data out of the model object and into a -JSON-ready format. It also needs to point towards the code where the -relevant JavaScript lives, and add the JavaScript object to the model -page. - -In a Python file (either its own, or in the same file as your -visualization code), import the ``VisualizationElement`` class we’ll -inherit from, and create the new visualization class. - -.. code:: python - - from mesa.visualization.ModularVisualization import VisualizationElement, CHART_JS_FILE - - class HistogramModule(VisualizationElement): - package_includes = [CHART_JS_FILE] - local_includes = ["HistogramModule.js"] - - def __init__(self, bins, canvas_height, canvas_width): - self.canvas_height = canvas_height - self.canvas_width = canvas_width - self.bins = bins - new_element = "new HistogramModule({}, {}, {})" - new_element = new_element.format(bins, - canvas_width, - canvas_height) - self.js_code = "elements.push(" + new_element + ");" - -There are a few things going on here. ``package_includes`` is a list of -JavaScript files that are part of Mesa itself that the visualization -element relies on. You can see the included files in -`mesa/visualization/templates/ `__. -Similarly, ``local_includes`` is a list of JavaScript files in the same -directory as the class code itself. Note that both of these are class -variables, not object variables – they hold for all particular objects. - -Next, look at the ``__init__`` method. It takes three arguments: the -number of bins, and the width and height for the histogram. It then uses -these values to populate the ``js_code`` property; this is code that the -server will insert into the visualization page, which will run when the -page loads. In this case, it creates a new HistogramModule (the class we -created in JavaScript in the step above) with the desired bins, width -and height; it then appends (``push``\ es) this object to ``elements``, -the list of visualization elements that the visualization page itself -maintains. - -Now, the last thing we need is the ``render`` method. If we were making -a general-purpose visualization module we’d want this to be more -general, but in this case we can hard-code it to our model. - -.. code:: python - - import numpy as np - - class HistogramModule(VisualizationElement): - # ... Everything from above... - - def render(self, model): - wealth_vals = [agent.wealth for agent in model.schedule.agents] - hist = np.histogram(wealth_vals, bins=self.bins)[0] - return [int(x) for x in hist] - -Every time the render method is called (with a model object as the -argument) it uses numpy to generate counts of agents with each wealth -value in the bins, and then returns a list of these values. Note that -the ``render`` method doesn’t return a JSON string – just an object that -can be turned into JSON, in this case a Python list (with Python -integers as the values; the ``json`` library doesn’t like dealing with -numpy’s integer type). - -Now, you can create your new HistogramModule and add it to the server: - -.. code:: python - - histogram = mesa.visualization.HistogramModule(list(range(10)), 200, 500) - server = mesa.visualization.ModularServer(MoneyModel, - [grid, histogram, chart], - "Money Model", - {"N":100, "width":10, "height":10}) - server.launch() - -Run this code, and you should see your brand-new histogram added to the -visualization and updating along with the model! - -.. figure:: files/viz_histogram.png - :alt: Histogram Visualization - - Histogram Visualization - -If you’ve felt comfortable with this section, it might be instructive to -read the code for the -`ModularServer `__ -and the -`modular_template `__ -to get a better idea of how all the pieces fit together. - -Happy Modeling! -~~~~~~~~~~~~~~~ - -This document is a work in progress. If you see any errors, exclusions -or have any problems please contact -`us `__. diff --git a/docs/tutorials/adv_tutorial.ipynb b/docs/tutorials/adv_tutorial_legacy.ipynb similarity index 82% rename from docs/tutorials/adv_tutorial.ipynb rename to docs/tutorials/adv_tutorial_legacy.ipynb index db213e34e27..c0c26e422fb 100644 --- a/docs/tutorials/adv_tutorial.ipynb +++ b/docs/tutorials/adv_tutorial_legacy.ipynb @@ -4,7 +4,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Advanced Tutorial" + "# Advanced Tutorial\n", + "This is the legacy version of the advanced tutorial. We recommend you to read the newer (current) version because it is easier." ] }, { @@ -15,7 +16,7 @@ "\n", "So far, we've built a model, run it, and analyzed some output afterwards. However, one of the advantages of agent-based models is that we can often watch them run step by step, potentially spotting unexpected patterns, behaviors or bugs, or developing new intuitions, hypotheses, or insights. Other times, watching a model run can explain it to an unfamiliar audience better than static explanations. Like many ABM frameworks, Mesa allows you to create an interactive visualization of the model. In this section we'll walk through creating a visualization using built-in components, and (for advanced users) how to create a new visualization element.\n", "\n", - "**Note for Jupyter users: Due to conflicts with the tornado server Mesa uses and Jupyter, the interactive browser of your model will load but likely not work. This will require you to use run the code from .py files. The Mesa development team is working to develop a** [Jupyter compatible interface](https://github.com/projectmesa/mesa/issues/1363).**\n", + "**Note for Jupyter users: Due to conflicts with the tornado server Mesa uses and Jupyter, the interactive browser of your model will load but likely not work. This will require you to use run the code from .py files. The Mesa development team is working to develop a** [Jupyter compatible interface](https://github.com/projectmesa/mesa/issues/1363).\n", "\n", "First, a quick explanation of how Mesa's interactive visualization works. Visualization is done in a browser window, using JavaScript to draw the different things being visualized at each step of the model. To do this, Mesa launches a small web server, which runs the model, turns each step into a JSON object (essentially, structured plain text) and sends those steps to the browser.\n", "\n", @@ -28,33 +29,32 @@ "source": [ "#### Grid Visualization\n", "\n", - "To start with, let's have a visualization where we can watch the agents moving around the grid. For this, you will need to put your model code in a separate Python source file; for example, `MoneyModel.py`. Next, either in the same file or in a new one (e.g. `MoneyModel_Viz.py`) import the server class and the Canvas Grid class (so-called because it uses HTML5 canvas to draw a grid). If you're in a new file, you'll also need to import the actual model object." + "To start with, let's have a visualization where we can watch the agents moving around the grid. For this, you will need to put your model code in a separate Python source file. For now, let us use the `MoneyModel` created in the [Introductory Tutorial](https://mesa.readthedocs.io/en/stable/tutorials/intro_tutorial.html) saved to `MoneyModel.py` file provided.\n", + "Next, in a new source file (e.g. `MoneyModel_Viz.py`) include the code shown in the following cells to run and avoid Jupyter compatibility issue." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "tags": [] }, "outputs": [], "source": [ - "import mesa\n", - "\n", "# If MoneyModel.py is where your code is:\n", - "# from MoneyModel import MoneyModel" + "from MoneyModel import mesa, MoneyModel" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "`CanvasGrid` works by looping over every cell in a grid, and generating a portrayal for every agent it finds. A portrayal is a dictionary (which can easily be turned into a JSON object) which tells the JavaScript side how to draw it. The only thing we need to provide is a function which takes an agent, and returns a portrayal object. Here's the simplest one: it'll draw each agent as a red, filled circle which fills half of each cell." + "Mesa's `CanvasGrid` visualization class works by looping over every cell in a grid, and generating a portrayal for every agent it finds. A portrayal is a dictionary (which can easily be turned into a JSON object) which tells the JavaScript side how to draw it. The only thing we need to provide is a function which takes an agent, and returns a portrayal object. Here's the simplest one: it'll draw each agent as a red, filled circle which fills half of each cell." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": { "tags": [] }, @@ -80,7 +80,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "tags": [] }, @@ -90,63 +90,46 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "\"\"\"\n", - "The full code should now look like:\n", - "\"\"\"\n", - "# from MoneyModel import *\n", - "import mesa\n", - "\n", - "\n", - "def agent_portrayal(agent):\n", - " portrayal = {\n", - " \"Shape\": \"circle\",\n", - " \"Filled\": \"true\",\n", - " \"Layer\": 0,\n", - " \"Color\": \"red\",\n", - " \"r\": 0.5,\n", - " }\n", - " return portrayal\n", + "Now we create and launch the actual server. We do this with the following arguments:\n", "\n", + "* The model class we're running and visualizing; in this case, `MoneyModel`.\n", + "* A list of module objects to include in the visualization; here, just `[grid]`\n", + "* The title of the model: \"Money Model\"\n", + "* Any inputs or arguments for the model itself. In this case, 100 agents, and height and width of 10.\n", "\n", - "grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500)\n", + "Once we create the server, we set the port (use default 8521 here) for it to listen on (you can treat this as just a piece of the URL you’ll open in the browser). " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ "server = mesa.visualization.ModularServer(\n", " MoneyModel, [grid], \"Money Model\", {\"N\": 100, \"width\": 10, \"height\": 10}\n", ")\n", - "server.port = 8521 # The default\n", - "server.launch()" + "server.port = 8521 # the default" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now we create and launch the actual server. We do this with the following arguments:\n", - "\n", - "* The model class we're running and visualizing; in this case, `MoneyModel`.\n", - "* A list of module objects to include in the visualization; here, just `[grid]`\n", - "* The title of the model: \"Money Model\"\n", - "* Any inputs or arguments for the model itself. In this case, 100 agents, and height and width of 10.\n", - "\n", - "Once we create the server, we set the port for it to listen on (you can treat this as just a piece of the URL you'll open in the browser). Finally, when you're ready to run the visualization, use the server's `launch()` method.\n", - "\n", - "```python\n", - "server = ModularServer(MoneyModel,\n", - " [grid],\n", - " \"Money Model\",\n", - " {\"N\":100, \"width\":10, \"height\":10})\n", - "server.port = 8521 # The default\n", - "server.launch()\n", - "```\n", - "The full code should now look like:\n", + "Finally, when you’re ready to run the visualization, use the server’s launch() method." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The full code for source file `MoneyModel_Viz.py` should now look like:\n", "\n", "```python\n", - "from MoneyModel import *\n", - "import mesa\n", + "from MoneyModel import mesa, MoneyModel\n", "\n", "\n", "def agent_portrayal(agent):\n", @@ -158,9 +141,7 @@ " return portrayal\n", "\n", "grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500)\n", - "server = mesa.visualization.ModularServer(\n", - " MoneyModel, [grid], \"Money Model\", {\"N\": 100, \"width\": 10, \"height\": 10}\n", - ")server = ModularServer(MoneyModel,\n", + "server = mesa.visualization.ModularServer(MoneyModel,\n", " [grid],\n", " \"Money Model\",\n", " {\"N\":100, \"width\":10, \"height\":10})\n", @@ -173,11 +154,11 @@ "\n", "![Empty Visualization](files/viz_empty.png)\n", "\n", - "Click the 'reset' button on the control panel, and you should see the grid fill up with red circles, representing agents.\n", + "Click the `Reset` button on the control panel, and you should see the grid fill up with red circles, representing agents.\n", "\n", "![Redcircles Visualization](files/viz_redcircles.png)\n", "\n", - "Click 'step' to advance the model by one step, and the agents will move around. Click 'run' and the agents will keep moving around, at the rate set by the 'fps' (frames per second) slider at the top. Try moving it around and see how the speed of the model changes. Pressing 'pause' will (as you'd expect) pause the model; presing 'run' again will restart it. Finally, 'reset' will start a new instantiation of the model.\n", + "Click `Step` to advance the model by one step, and the agents will move around. Click `Start` and the agents will keep moving around, at the rate set by the 'fps' (frames per second) slider at the top. Try moving it around and see how the speed of the model changes. Pressing `Stop` will pause the model; presing `Start` again will restart it. Finally, `Reset` will start a new instantiation of the model.\n", "\n", "To stop the visualization server, go back to the terminal where you launched it, and press Control+c." ] @@ -190,13 +171,17 @@ "\n", "In the visualization above, all we could see is the agents moving around -- but not how much money they had, or anything else of interest. Let's change it so that agents who are broke (wealth 0) are drawn in grey, smaller, and above agents who still have money.\n", "\n", - "To do this, we go back to our `agent_portrayal` code and add some code to change the portrayal based on the agent properties.\n", - "\n", - "```python\n", + "To do this, we go back to our `agent_portrayal` code and add some code to change the portrayal based on the agent properties and launch the server again." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ "def agent_portrayal(agent):\n", - " portrayal = {\"Shape\": \"circle\",\n", - " \"Filled\": \"true\",\n", - " \"r\": 0.5}\n", + " portrayal = {\"Shape\": \"circle\", \"Filled\": \"true\", \"r\": 0.5}\n", "\n", " if agent.wealth > 0:\n", " portrayal[\"Color\"] = \"red\"\n", @@ -205,10 +190,14 @@ " portrayal[\"Color\"] = \"grey\"\n", " portrayal[\"Layer\"] = 1\n", " portrayal[\"r\"] = 0.2\n", - " return portrayal\n", - "```\n", - "\n", - "Now launch the server again - this will open a new browser window pointed at the updated visualization. Initially it looks the same, but advance the model and smaller grey circles start to appear. Note that since the zero-wealth agents have a higher layer number, they are drawn on top of the red agents.\n", + " return portrayal" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This will open a new browser window pointed at the updated visualization. Initially it looks the same, but advance the model and smaller grey circles start to appear. Note that since the zero-wealth agents have a higher layer number, they are drawn on top of the red agents.\n", "\n", "![Greycircles Visualization](files/viz_greycircles.png)" ] @@ -223,20 +212,65 @@ "\n", "The basic chart pulls data from the model's DataCollector, and draws it as a line graph using the [Charts.js](http://www.chartjs.org/) JavaScript libraries. We instantiate a chart element with a list of series for the chart to track. Each series is defined in a dictionary, and has a `Label` (which must match the name of a model-level variable collected by the DataCollector) and a `Color` name. We can also give the chart the name of the DataCollector object in the model.\n", "\n", - "Finally, we add the chart to the list of elements in the server. The elements are added to the visualization in the order they appear, so the chart will appear underneath the grid.\n", + "Finally, we add the chart to the list of elements in the server. The elements are added to the visualization in the order they appear, so the chart will appear underneath the grid." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "chart = mesa.visualization.ChartModule(\n", + " [{\"Label\": \"Gini\", \"Color\": \"Black\"}], data_collector_name=\"datacollector\"\n", + ")\n", + "\n", + "server = mesa.visualization.ModularServer(\n", + " MoneyModel, [grid, chart], \"Money Model\", {\"N\": 100, \"width\": 10, \"height\": 10}\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Launch the visualization and start a model run, either by launching the server here or through the full code for source file `MoneyModel_Viz.py`.\n", "\n", "```python\n", - "chart = mesa.visualization.ChartModule([{\"Label\": \"Gini\", \n", - " \"Color\": \"Black\"}],\n", - " data_collector_name='datacollector')\n", + "from MoneyModel import mesa, MoneyModel\n", "\n", - "server = mesa.visualization.ModularServer(MoneyModel, \n", - " [grid, chart], \n", - " \"Money Model\", \n", - " {\"N\":100, \"width\":10, \"height\":10})\n", - "```\n", "\n", - "Launch the visualization and start a model run, and you'll see a line chart underneath the grid. Every step of the model, the line chart updates along with the grid. Reset the model, and the chart resets too.\n", + "def agent_portrayal(agent):\n", + " portrayal = {\"Shape\": \"circle\", \"Filled\": \"true\", \"r\": 0.5}\n", + "\n", + " if agent.wealth > 0:\n", + " portrayal[\"Color\"] = \"red\"\n", + " portrayal[\"Layer\"] = 0\n", + " else:\n", + " portrayal[\"Color\"] = \"grey\"\n", + " portrayal[\"Layer\"] = 1\n", + " portrayal[\"r\"] = 0.2\n", + " return portrayal\n", + "\n", + "\n", + "grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500)\n", + "chart = mesa.visualization.ChartModule(\n", + " [{\"Label\": \"Gini\", \"Color\": \"Black\"}], data_collector_name=\"datacollector\"\n", + ")\n", + "\n", + "server = mesa.visualization.ModularServer(\n", + " MoneyModel, [grid, chart], \"Money Model\", {\"N\": 100, \"width\": 10, \"height\": 10}\n", + ")\n", + "server.port = 8521 # The default\n", + "server.launch()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You'll see a line chart underneath the grid. Every step of the model, the line chart updates along with the grid. Reset the model, and the chart resets too.\n", "\n", "![Chart Visualization](files/viz_chart.png)\n", "\n", @@ -342,7 +376,7 @@ " };\n", "\n", " // Create the chart object\n", - " const chart = new Chart(context, {type: 'bar', data: data, options: options});\n", + " let chart = new Chart(context, {type: 'bar', data: data, options: options});\n", "\n", " // Now what?\n", "};\n", diff --git a/docs/tutorials/files/output_19_1.png b/docs/tutorials/files/output_19_1.png deleted file mode 100644 index 010be9f0c5a..00000000000 Binary files a/docs/tutorials/files/output_19_1.png and /dev/null differ diff --git a/docs/tutorials/intro_tutorial.ipynb b/docs/tutorials/intro_tutorial.ipynb index 2fe16594e62..a77f4a13610 100644 --- a/docs/tutorials/intro_tutorial.ipynb +++ b/docs/tutorials/intro_tutorial.ipynb @@ -4,117 +4,242 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Introductory Tutorial" + "# Introductory Tutorial\n", + "\n", + "### The Boltzmann Wealth Model " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ + "**Important:** \n", + "- If you are just exploring Mesa and want the fastest way to execute the code we recommend executing this tutorial online in a Colab notebook. [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/projectmesa/mesa/blob/main/docs/tutorials/intro_tutorial.ipynb)\n", + "- If you have installed mesa and are running locally, please ensure that your [Mesa version](https://pypi.org/project/Mesa/) is up-to-date in order to run this tutorial.\n", + "\n", "## Tutorial Description\n", "\n", - "[Mesa](https://github.com/projectmesa/mesa) is a Python framework for [agent-based modeling](https://en.wikipedia.org/wiki/Agent-based_model). Getting started with Mesa is easy. In this tutorial, we will walk through creating a simple model and progressively add functionality which will illustrate Mesa's core features.\n", + "[Mesa](https://github.com/projectmesa/mesa) is a Python framework for [agent-based modeling](https://en.wikipedia.org/wiki/Agent-based_model). This tutorial will assist you in getting started. Working through the tutorial will help you discover the core features of Mesa. Through the tutorial, you are walked through creating a starter-level model. Functionality is added progressively as the process unfolds. Should anyone find any errors, bugs, have a suggestion, or just are looking for clarification, [let us know](https://github.com/projectmesa/mesa/issues)!\n", + "\n", + "The premise of this tutorial is to create a starter-level model representing agents exchanging money. This exchange of money affects wealth. \n", + "\n", + "Next, *space* is added to allow agents to move based on the change in wealth as time progresses.\n", "\n", - "**Note:** This tutorial is a work-in-progress. If you find any errors or bugs, or just find something unclear or confusing, [let us know](https://github.com/projectmesa/mesa/issues)!\n", + "Two of Mesa's analytic tools: the *data collector* and *batch runner* are then used to examine the dynamics of this simple model. \n", "\n", - "The base for this tutorial is a very simple model of agents exchanging money. Next, we add *space* to allow agents to move. Then, we'll cover two of Mesa's analytic tools: the *data collector* and *batch runner*. After that, we'll add an *interactive visualization* which lets us watch the model as it runs. Finally, we go over how to write your own visualization module, for users who are comfortable with JavaScript.\n", + "### More Tutorials: \n", "\n", - "You can also find all the code this tutorial describes in the **examples/boltzmann_wealth_model** directory of the Mesa repository." + "Visualization: There is a separate [visualization tutorial](https://mesa.readthedocs.io/en/stable/tutorials/visualization_tutorial.html) that will take users through building a visualization for this model (aka Boltzmann Wealth Model).\n", + "\n", + "Advanced Visualization (legacy): There is also an [advanced visualization tutorial](https://mesa.readthedocs.io/en/stable/tutorials/adv_tutorial_legacy.html) that will show users how to use the JavaScript based visualization option, which also uses this model as its base. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Sample Model Description\n", + "## Model Description\n", + "\n", + "This is a starter-level simulated agent-based economy. In an agent-based economy, the behavior of an individual economic agent, such as a consumer or producer, is studied in a market environment.\n", + "This model is drawn from the field econophysics, specifically a paper prepared by Drăgulescu et al. for additional information on the modeling assumptions used in this model. [Drăgulescu, 2002].\n", "\n", - "The tutorial model is a very simple simulated agent-based economy, drawn from econophysics and presenting a statistical mechanics approach to wealth distribution [Dragulescu2002]. The rules of our tutorial model:\n", + "The assumption that govern this model are:\n", "\n", "1. There are some number of agents.\n", "2. All agents begin with 1 unit of money.\n", - "3. At every step of the model, an agent gives 1 unit of money (if they have it) to some other agent.\n", + "3. At every step of the model, an agent gives 1 unit of money (if they\n", + " have it) to some other agent.\n", "\n", - "Despite its simplicity, this model yields results that are often unexpected to those not familiar with it. For our purposes, it also easily demonstrates Mesa's core features.\n", - "\n", - "Let's get started." + "Even as a starter-level model the yielded results are both interesting and unexpected to individuals unfamiliar\n", + "with it the specific topic. As such, this model is a good starting point to examine Mesa's core features." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Installation\n", + "### Tutorial Setup\n", "\n", - "To start, install Mesa. We recommend doing this in a [virtual environment](https://virtualenvwrapper.readthedocs.org/en/stable/), but make sure your environment is set up with Python 3. Mesa requires Python3 and does not work in Python 2 environments.\n", + "Create and activate a [virtual environment](http://docs.python-guide.org/en/latest/dev/virtualenvs/). *Python version 3.8 or higher is required*.\n", "\n", - "To install Mesa, simply:\n", + "Install Mesa:\n", "\n", "```bash\n", - " $ pip install mesa\n", + "pip install --upgrade mesa\n", "```\n", "\n", - "When you do that, it will install Mesa itself, as well as any dependencies that aren't in your setup yet. Additional dependencies required by this tutorial can be found in the **examples/boltzmann_wealth_model/requirements.txt** file, which can be installed directly form the github repository by running:\n", + "Install Jupyter Notebook (optional):\n", "\n", "```bash\n", - " $ pip install -r https://raw.githubusercontent.com/projectmesa/mesa/main/examples/boltzmann_wealth_model/requirements.txt\n", + "pip install jupyter\n", "```\n", "\n", - "This will install the dependencies listed in the requirements.txt file which are: \n", - "- jupyter (Ipython interactive notebook) \n", - "- matplotlib (Python's visualization library) \n", - "- mesa (this ABM library -- if not installed) \n", - "- numpy (Python's numerical python library) " + "Install Visualization Tools:\n", + "\n", + "```bash\n", + "pip install matplotlib\n", + "```\n", + "\n", + "\n", + "**If running in Google Colab run the below cell to install Mesa.** (This will also work in a locally installed version of Jupyter.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# SKIP THIS CELL unless running in colab\n", + "\n", + "%pip install --quiet mesa\n", + "# The exclamation points tell jupyter to do the command via the command line" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Building a sample model\n", + "## Building the Sample Model\n", + "\n", + "After Mesa is installed a model can be built. A jupyter notebook is recommended for this tutorial, this allows for small segments of codes to be examined one at a time. As an option this can be created using python script files.\n", "\n", - "Once Mesa is installed, you can start building our model. You can write models in two different ways:\n", + "**Good Practice:** Place a model in its own folder/directory. This is not specifically required for the starter_model, but as other models become more complicated and expand multiple python scripts, documentation, discussions and notebooks may be added.\n", "\n", - "1. Write the code in its own file with your favorite text editor, or\n", - "2. Write the model interactively in [Jupyter Notebook](http://jupyter.org/) cells.\n", + "### Create New Folder/Directory\n", "\n", - "Either way, it's good practice to put your model in its own folder -- especially if the project will end up consisting of multiple files (for example, Python files for the model and the visualization, a Notebook for analysis, and a Readme with some documentation and discussion).\n", + "- Using operating system commands create a new folder/directory named 'starter_model'.\n", "\n", - "Begin by creating a folder, and either launch a Notebook or create a new Python source file. We will use the name `money_model.py` here.\n", - "\n" + "- Change into the new folder/directory.\n", + "\n", + "\n", + "### Creating Model With Jupyter Notebook\n", + "\n", + "Write the model interactively in [Jupyter Notebook](http://jupyter.org/) cells.\n", + "\n", + "Start Jupyter Notebook:\n", + "\n", + "```bash\n", + "jupyter notebook\n", + "```\n", + "\n", + "Create a new Notebook named `money_model.ipynb` (or whatever you want to call it).\n", + "\n", + "### Creating Model With Script File (IDE, Text Editor, Colab, etc.)\n", + "\n", + "Create a new file called `money_model.py` (or whatever you want to call it)\n", + "\n", + "*Code will be added as the tutorial progresses.*\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import Dependencies\n", + "This includes importing of dependencies needed for the tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:41.694690Z", + "start_time": "2023-04-25T18:23:41.573145Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "import mesa\n", + "\n", + "# Data visualization tools.\n", + "import seaborn as sns\n", + "\n", + "# Has multi-dimensional arrays and matrices. Has a large collection of\n", + "# mathematical functions to operate on these arrays.\n", + "import numpy as np\n", + "\n", + "# Data manipulation and analysis.\n", + "import pandas as pd" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Setting up the model\n", + "### Create Agent\n", + "\n", + "First create the agent. As the tutorial progresses, more functionality will be added to the agent.\n", + "\n", + "**Background:** Agents are the individual entities that act in the model. It is a good modeling practice to make certain each Agent can be uniquely identified.\n", "\n", - "To begin writing the model code, we start with two core classes: one for the overall model, the other for the agents. The model class holds the model-level attributes, manages the agents, and generally handles the global level of our model. Each instantiation of the model class will be a specific model run. Each model will contain multiple agents, all of which are instantiations of the agent class. Both the model and agent classes are child classes of Mesa's generic `Model` and `Agent` classes. This is seen in the code with `class MoneyModel(mesa.Model)` or `class MoneyAgent(mesa.Agent)`. If you want you can specifically the class being imported by looking at the [model](https://github.com/projectmesa/mesa/blob/main/mesa/model.py) or [agent](https://github.com/projectmesa/mesa/blob/main/mesa/agent.py) code in the mesa repo.\n", + "**Model-specific information:** Agents are the individuals that exchange money, in this case the amount of money an individual agent has is represented as wealth. Additionally, agents each have a unique identifier.\n", "\n", - "Each agent has only one variable: how much wealth it currently has. (Each agent will also have a unique identifier (i.e., a name), stored in the `unique_id` variable. Giving each agent a unique id is a good practice when doing agent-based modeling.)\n", + "**Code implementation:** This is done by creating a new class (or object) that extends `mesa.Agent` creating a subclass of the `Agent` class from mesa. The new class is named `MoneyAgent`. The technical details about the agent object can be found in the [mesa repo](https://github.com/projectmesa/mesa/blob/main/mesa/agent.py).\n", "\n", - "There is only one model-level parameter: how many agents the model contains. When a new model is started, we want it to populate itself with the given number of agents.\n", "\n", - "The beginning of both classes looks like this:" + "The `MoneyAgent` class is created with the following code:\n" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:41.696198Z", + "start_time": "2023-04-25T18:23:41.693472Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ - "import mesa\n", - "\n", - "\n", "class MoneyAgent(mesa.Agent):\n", " \"\"\"An agent with fixed initial wealth.\"\"\"\n", "\n", " def __init__(self, unique_id, model):\n", + " # Pass the parameters to the parent class.\n", " super().__init__(unique_id, model)\n", - " self.wealth = 1\n", "\n", + " # Create the agent's variable and set the initial values.\n", + " self.wealth = 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create Model\n", + "\n", + "Next, create the model. Again, as the tutorial progresses, more functionality will be added to the model.\n", + "\n", + "**Background:** The model can be visualized as a grid containing all the agents. The model creates, holds and manages all the agents on the grid. The model evolves in discrete time steps.\n", "\n", + "**Model-specific information:** When a model is created the number of agents within the model is specified. The model then creates the agents and places them on the grid. The model also contains a scheduler which controls the order in which agents are activated. The scheduler is also responsible for advancing the model by one step. The model also contains a data collector which collects data from the model. These topics will be covered in more detail later in the tutorial.\n", + "\n", + "**Code implementation:** This is done by creating a new class (or object) that extends `mesa.Model` creating a subclass of the `Model` class from mesa. The new class is named `MoneyModel`. The technical details about the agent object can be found in the [mesa repo](https://github.com/projectmesa/mesa/blob/main/mesa/model.py).\n", + "\n", + "The `MoneyModel` class is created with the following code:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:41.700374Z", + "start_time": "2023-04-25T18:23:41.698198Z" + } + }, + "outputs": [], + "source": [ "class MoneyModel(mesa.Model):\n", " \"\"\"A model with some number of agents.\"\"\"\n", "\n", @@ -129,39 +254,43 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Adding the scheduler\n", + "### Adding the Scheduler\n", + "Now the model will be modified to add a scheduler.\n", "\n", - "Time in most agent-based models moves in steps, sometimes also called **ticks**. At each step of the model, one or more of the agents -- usually all of them -- are activated and take their own step, changing internally and/or interacting with one another or the environment.\n", + "**Background:** The scheduler controls the order in which agents are activated, causing the agent to take their defined action. The scheduler is also responsible for advancing the model by one step. A step is the smallest unit of time in the model, and is often referred to as a tick. The scheduler can be configured to activate agents in different orders. This can be important as the order in which agents are activated can impact the results of the model [Comer2014]. At each step of the model, one or more of the agents -- usually all of them -- are activated and take their own step, changing internally and/or interacting with one another or the environment.\n", "\n", - "The **scheduler** is a special model component which controls the order in which agents are activated. For example, all the agents may activate in the same order every step; their order might be shuffled; we may try to simulate all the agents acting at the same time; and more. Mesa offers a few different built-in scheduler classes, with a common interface. That makes it easy to change the activation regime a given model uses, and see whether it changes the model behavior. This may not seem important, but scheduling patterns can have an impact on your results [Comer2014].\n", + "**Model-specific information:** A new class is named `RandomActivationByAgent` is created which extends `mesa.time.RandomActivation` creating a subclass of the `RandomActivation` class from Mesa. This class activates all the agents once per step, in random order. Every agent is expected to have a ``step`` method. The step method is the action the agent takes when it is activated by the model schedule. We add an agent to the schedule using the `add` method; when we call the schedule's `step` method, the model shuffles the order of the agents, then activates and executes each agent's ```step``` method. The scheduler is then added to the model.\n", "\n", - "For now, let's use one of the simplest ones: `RandomActivation`*, which activates all the agents once per step, in random order. Every agent is expected to have a ``step`` method. The step method is the action the agent takes when it is activated by the model schedule. We add an agent to the schedule using the `add` method; when we call the schedule's `step` method, the model shuffles the order of the agents, then activates and executes each agent's ```step``` method.\n", + "**Code implementation:** The technical details about the timer object can be found in the [mesa repo](https://github.com/projectmesa/mesa/blob/main/mesa/time.py). Mesa offers a few different built-in scheduler classes, with a common interface. That makes it easy to change the activation regime a given model uses, and see whether it changes the model behavior. The details pertaining to the scheduler interface can be located the same [mesa repo](https://github.com/projectmesa/mesa/blob/main/mesa/time.py).\n", "\n", - "*Unlike `mesa.model` or `mesa.agent`, `mesa.time` has multiple classes (e.g. `RandomActivation`, `StagedActivation` etc). To ensure context, time is used in the import as evidenced below with `mesa.time.Randomactivation`. You can see the different time classes as [mesa.time](https://github.com/projectmesa/mesa/blob/main/mesa/time.py). \n", - "\n", - "With that in mind, the model code with the scheduler added looks like this:" + "With that in mind, the `MoneyAgent` code is modified below to visually show when a new agent is created. The MoneyModel code is modified by adding the RandomActivation method to the model. with the scheduler added looks like this:" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:41.705824Z", + "start_time": "2023-04-25T18:23:41.703377Z" + } + }, "outputs": [], "source": [ - "import mesa\n", - "\n", - "\n", "class MoneyAgent(mesa.Agent):\n", " \"\"\"An agent with fixed initial wealth.\"\"\"\n", "\n", " def __init__(self, unique_id, model):\n", + " # Pass the parameters to the parent class.\n", " super().__init__(unique_id, model)\n", + "\n", + " # Create the agent's attribute and set the initial values.\n", " self.wealth = 1\n", "\n", " def step(self):\n", " # The agent's step will go here.\n", " # For demonstration purposes we will print the agent's unique_id\n", - " print(\"Hi, I am agent \" + str(self.unique_id) + \".\")\n", + " print(f\"Hi, I am an agent, you can call me {str(self.unique_id)}.\")\n", "\n", "\n", "class MoneyModel(mesa.Model):\n", @@ -169,14 +298,19 @@ "\n", " def __init__(self, N):\n", " self.num_agents = N\n", + " # Create scheduler and assign it to the model\n", " self.schedule = mesa.time.RandomActivation(self)\n", + "\n", " # Create agents\n", " for i in range(self.num_agents):\n", " a = MoneyAgent(i, self)\n", + " # Add the agent to the scheduler\n", " self.schedule.add(a)\n", "\n", " def step(self):\n", " \"\"\"Advance the model by one step.\"\"\"\n", + "\n", + " # The model's step will go here for now this will call the step method of each agent and print the agent's unique_id\n", " self.schedule.step()" ] }, @@ -184,22 +318,54 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "At this point, we have a model which runs -- it just doesn't do anything. You can see for yourself with a few easy lines. If you've been working in an interactive session, you can create a model object directly. Otherwise, you need to open an interactive session in the same directory as your source code file, and import the classes. For example, if your code is in `money_model.py`:\n", + "### Running the Model\n", + "A basic model has now been created. The model can be run by creating a model object and calling the step method. The model will run for one step and print the unique_id of each agent. You may run the model for multiple steps by calling the step method multiple times.\n", + "\n", + "Note: If you are using `.py` (script) files instead of `.ipynb` (Jupyter), the common convention is\n", + "to have a `run.py` in the same directory as your model code. You then (1) import the ``MoneyModel`` class,\n", + "(2) create a model object and (3) run it for a few steps. As shown below:\n", "\n", "```python\n", "from money_model import MoneyModel\n", + "\n", + "starter_model = MoneyModel(10)\n", + "starter_model.step()\n", "```\n", - "Then create the model object, and run it for one step:" + "Create the model object, and run it for one step:" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:41.711368Z", + "start_time": "2023-04-25T18:23:41.706825Z" + } + }, + "outputs": [], + "source": [ + "starter_model = MoneyModel(10)\n", + "starter_model.step()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:41.713554Z", + "start_time": "2023-04-25T18:23:41.711368Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ - "empty_model = MoneyModel(10)\n", - "empty_model.step()" + "# Run this step overnight and see what happens! Notice the order of the agents changes each time.\n", + "starter_model.step()" ] }, { @@ -207,8 +373,63 @@ "metadata": {}, "source": [ "#### Exercise\n", - " \n", - "Try modifying the code above to have every agent print out its `wealth` when it is activated. Run a few steps of the model to see how the agent activation order is shuffled each step." + "Modifying the code below to have every agent print out its `wealth` when it is activated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:41.716975Z", + "start_time": "2023-04-25T18:23:41.714555Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "class MoneyAgent(mesa.Agent):\n", + " \"\"\"An agent with fixed initial wealth.\"\"\"\n", + "\n", + " def __init__(self, unique_id, model):\n", + " # Pass the parameters to the parent class.\n", + " super().__init__(unique_id, model)\n", + "\n", + " # Create the agent's variable and set the initial values.\n", + " self.wealth = 1\n", + "\n", + " def step(self):\n", + " # The agent's step will go here.\n", + " # FIXME: Need to print the agent's wealth\n", + " print(f\"Hi, I am an agent and I am broke!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a model for 12 Agents, and run it for a few steps to see the output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:41.719722Z", + "start_time": "2023-04-25T18:23:41.716975Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# Fixme: Create the model object, and run it" ] }, { @@ -217,7 +438,13 @@ "source": [ "### Agent Step\n", "\n", - "Now we just need to have the agents do what we intend for them to do: check their wealth, and if they have the money, give one unit of it away to another random agent. To allow the agent to choose another agent at random, we use the ``model.random`` random-number generator. This works just like Python's ``random`` module, but with a fixed seed set when the model is instantiated, that can be used to replicate a specific model run later." + "Returning back to the MoneyAgent the actual step process is now going to be created.\n", + "\n", + "**Background:** This is where the agent's behavior as it relates to each step or tick of the model is defined.\n", + "\n", + "**Model-specific information:** In this case, the agent will check its wealth, and if it has money, give one unit of it away to another random agent.\n", + "\n", + "**Code implementation:** The agent's step method is called by the scheduler during each step of the model. To allow the agent to choose another agent at random, we use the `model.random` random-number generator. This works just like Python's `random` module, but with a fixed seed set when the model is instantiated, that can be used to replicate a specific model run later." ] }, { @@ -226,28 +453,40 @@ "source": [ "To pick an agent at random, we need a list of all agents. Notice that there isn't such a list explicitly in the model. The scheduler, however, does have an internal list of all the agents it is scheduled to activate.\n", "\n", - "With that in mind, we rewrite the agent `step` method, like this:" + "With that in mind, we rewrite the agent `step` method as shown below:" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:41.723390Z", + "start_time": "2023-04-25T18:23:41.721719Z" + } + }, "outputs": [], "source": [ + "import copy\n", + "\n", + "\n", "class MoneyAgent(mesa.Agent):\n", " \"\"\"An agent with fixed initial wealth.\"\"\"\n", "\n", " def __init__(self, unique_id, model):\n", + " # Pass the parameters to the parent class.\n", " super().__init__(unique_id, model)\n", + "\n", + " # Create the agent's variable and set the initial values.\n", " self.wealth = 1\n", "\n", " def step(self):\n", - " if self.wealth == 0:\n", - " return\n", - " other_agent = self.random.choice(self.model.schedule.agents)\n", - " other_agent.wealth += 1\n", - " self.wealth -= 1" + " # Verify agent has some wealth\n", + " if self.wealth > 0:\n", + " other_agent = self.random.choice(self.model.schedule.agents)\n", + " if other_agent is not None:\n", + " other_agent.wealth += 1\n", + " self.wealth -= 1" ] }, { @@ -258,10 +497,10 @@ "\n", "With that last piece in hand, it's time for the first rudimentary run of the model.\n", "\n", - "If you've written the code in its own file (`money_model.py` or a different name), launch an interpreter in the same directory as the file (either the plain Python command-line interpreter, or the IPython interpreter), or launch a Jupyter Notebook there. Then import the classes you created. (If you wrote the code in a Notebook, obviously this step isn't necessary).\n", + "If you've written the code in its own script file (`money_model.py` or a different name) you can now modify your ``run.py`` or even launch a Jupyter Notebook. You then just follow the same three steps of (1) import your model class ``MoneyModel``, (2) create the model object and (3) run it for a few steps. If you wrote the code in one Notebook then step 1, importing, is not necessary.\n", "\n", "```python\n", - "from money_model import *\n", + "from money_model import MoneyModel\n", "```\n", "\n", "Now let's create a model with 10 agents, and run it for 10 steps." @@ -270,7 +509,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:41.790021Z", + "start_time": "2023-04-25T18:23:41.724975Z" + } + }, "outputs": [], "source": [ "model = MoneyModel(10)\n", @@ -292,6 +536,7 @@ "If you are running from a text editor or IDE, you'll also need to add this line, to make the graph appear.\n", "\n", "```python\n", + "import matplotlib.pyplot as plt\n", "plt.show()\n", "```" ] @@ -299,7 +544,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:41.882917Z", + "start_time": "2023-04-25T18:23:41.727492Z" + } + }, "outputs": [], "source": [ "# For a jupyter notebook add the following line:\n", @@ -309,7 +559,11 @@ "import matplotlib.pyplot as plt\n", "\n", "agent_wealth = [a.wealth for a in model.schedule.agents]\n", - "plt.hist(agent_wealth)" + "# Create a histogram with seaborn\n", + "g = sns.histplot(agent_wealth, discrete=True)\n", + "g.set(\n", + " title=\"Wealth distribution\", xlabel=\"Wealth\", ylabel=\"Number of agents\"\n", + "); # The semicolon is just to avoid printing the object representation" ] }, { @@ -329,7 +583,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:42.027373Z", + "start_time": "2023-04-25T18:23:41.886918Z" + } + }, "outputs": [], "source": [ "all_wealth = []\n", @@ -344,7 +603,9 @@ " for agent in model.schedule.agents:\n", " all_wealth.append(agent.wealth)\n", "\n", - "plt.hist(all_wealth, bins=range(max(all_wealth) + 1))" + "# Use seaborn\n", + "g = sns.histplot(all_wealth, discrete=True)\n", + "g.set(title=\"Wealth distribution\", xlabel=\"Wealth\", ylabel=\"Number of agents\");" ] }, { @@ -381,7 +642,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:42.030776Z", + "start_time": "2023-04-25T18:23:42.028374Z" + } + }, "outputs": [], "source": [ "class MoneyModel(mesa.Model):\n", @@ -467,7 +733,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:42.036559Z", + "start_time": "2023-04-25T18:23:42.033158Z" + } + }, "outputs": [], "source": [ "class MoneyAgent(mesa.Agent):\n", @@ -521,16 +792,21 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let's create a model with 50 agents on a 10x10 grid, and run it for 20 steps." + "Let's create a model with 100 agents on a 10x10 grid, and run it for 20 steps." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:42.042251Z", + "start_time": "2023-04-25T18:23:42.040741Z" + } + }, "outputs": [], "source": [ - "model = MoneyModel(50, 10, 10)\n", + "model = MoneyModel(100, 10, 10)\n", "for i in range(20):\n", " model.step()" ] @@ -539,27 +815,28 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now let's use matplotlib and numpy to visualize the number of agents residing in each cell. To do that, we create a numpy array of the same size as the grid, filled with zeros. Then we use the grid object's `coord_iter()` feature, which lets us loop over every cell in the grid, giving us each cell's coordinates and contents in turn." + "Now let's use matplotlib and numpy to visualize the number of agents residing in each cell. To do that, we create a numpy array of the same size as the grid, filled with zeros. Then we use the grid object's `coord_iter()` feature, which lets us loop over every cell in the grid, giving us each cell's positions and contents in turn." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:42.207690Z", + "start_time": "2023-04-25T18:23:42.043252Z" + } + }, "outputs": [], "source": [ - "import numpy as np\n", - "\n", "agent_counts = np.zeros((model.grid.width, model.grid.height))\n", - "for cell in model.grid.coord_iter():\n", - " cell_content, x, y = cell\n", + "for cell_content, (x, y) in model.grid.coord_iter():\n", " agent_count = len(cell_content)\n", " agent_counts[x][y] = agent_count\n", - "plt.imshow(agent_counts, interpolation=\"nearest\")\n", - "plt.colorbar()\n", - "\n", - "# If running from a text editor or IDE, remember you'll need the following:\n", - "# plt.show()" + "# Plot using seaborn, with a size of 5x5\n", + "g = sns.heatmap(agent_counts, cmap=\"viridis\", annot=True, cbar=False, square=True)\n", + "g.figure.set_size_inches(4, 4)\n", + "g.set(title=\"Number of agents on each cell of the grid\");" ] }, { @@ -580,7 +857,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:42.215443Z", + "start_time": "2023-04-25T18:23:42.214438Z" + } + }, "outputs": [], "source": [ "def compute_gini(model):\n", @@ -607,10 +889,15 @@ "\n", " def give_money(self):\n", " cellmates = self.model.grid.get_cell_list_contents([self.pos])\n", + " cellmates.pop(\n", + " cellmates.index(self)\n", + " ) # Ensure agent is not giving money to itself\n", " if len(cellmates) > 1:\n", " other = self.random.choice(cellmates)\n", " other.wealth += 1\n", " self.wealth -= 1\n", + " if other == self:\n", + " print(\"I JUST GAVE MONEY TO MYSELF HEHEHE!\")\n", "\n", " def step(self):\n", " self.move()\n", @@ -658,10 +945,15 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:42.232493Z", + "start_time": "2023-04-25T18:23:42.216442Z" + } + }, "outputs": [], "source": [ - "model = MoneyModel(50, 10, 10)\n", + "model = MoneyModel(100, 10, 10)\n", "for i in range(100):\n", " model.step()" ] @@ -676,11 +968,18 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:42.372795Z", + "start_time": "2023-04-25T18:23:42.233495Z" + } + }, "outputs": [], "source": [ "gini = model.datacollector.get_model_vars_dataframe()\n", - "gini.plot()" + "# Plot the Gini coefficient over time\n", + "g = sns.lineplot(data=gini)\n", + "g.set(title=\"Gini Coefficient over Time\", ylabel=\"Gini Coefficient\");" ] }, { @@ -693,7 +992,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:42.385021Z", + "start_time": "2023-04-25T18:23:42.372795Z" + } + }, "outputs": [], "source": [ "agent_wealth = model.datacollector.get_agent_vars_dataframe()\n", @@ -704,17 +1008,29 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "You'll see that the DataFrame's index is pairings of model step and agent ID. You can analyze it the way you would any other DataFrame. For example, to get a histogram of agent wealth at the model's end:" + "You'll see that the DataFrame's index is pairings of model step and agent ID. This is because the data collector stores the data in a dictionary, with the step number as the key, and a dictionary of agent ID and variable value pairs as the value. The data collector then converts this dictionary into a DataFrame, which is why the index is a pair of (model step, agent ID). You can analyze it the way you would any other DataFrame. For example, to get a histogram of agent wealth at the model's end:" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:42.558338Z", + "start_time": "2023-04-25T18:23:42.383031Z" + } + }, "outputs": [], "source": [ - "end_wealth = agent_wealth.xs(99, level=\"Step\")[\"Wealth\"]\n", - "end_wealth.hist(bins=range(agent_wealth.Wealth.max() + 1))" + "last_step = agent_wealth.index.get_level_values(\"Step\").max()\n", + "end_wealth = agent_wealth.xs(last_step, level=\"Step\")[\"Wealth\"]\n", + "# Create a histogram of wealth at the last step\n", + "g = sns.histplot(end_wealth, discrete=True)\n", + "g.set(\n", + " title=\"Distribution of wealth at the end of simulation\",\n", + " xlabel=\"Wealth\",\n", + " ylabel=\"Number of agents\",\n", + ");" ] }, { @@ -727,11 +1043,74 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:42.702477Z", + "start_time": "2023-04-25T18:23:42.520333Z" + } + }, "outputs": [], "source": [ + "# Get the wealth of agent 14 over time\n", "one_agent_wealth = agent_wealth.xs(14, level=\"AgentID\")\n", - "one_agent_wealth.Wealth.plot()" + "\n", + "# Plot the wealth of agent 14 over time\n", + "g = sns.lineplot(data=one_agent_wealth, x=\"Step\", y=\"Wealth\")\n", + "g.set(title=\"Wealth of agent 14 over time\");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also plot a reporter of multiple agents over time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agent_list = [3, 14, 25]\n", + "\n", + "# Get the wealth of multiple agents over time\n", + "multiple_agents_wealth = agent_wealth[\n", + " agent_wealth.index.get_level_values(\"AgentID\").isin(agent_list)\n", + "]\n", + "# Plot the wealth of multiple agents over time\n", + "g = sns.lineplot(data=multiple_agents_wealth, x=\"Step\", y=\"Wealth\", hue=\"AgentID\")\n", + "g.set(title=\"Wealth of agents 3, 14 and 25 over time\");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also plot the average of all agents, with a 95% confidence interval for that average." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Transform the data to a long format\n", + "agent_wealth_long = agent_wealth.T.unstack().reset_index()\n", + "agent_wealth_long.columns = [\"Step\", \"AgentID\", \"Variable\", \"Value\"]\n", + "agent_wealth_long.head(3)\n", + "\n", + "# Plot the average wealth over time\n", + "g = sns.lineplot(data=agent_wealth_long, x=\"Step\", y=\"Value\", errorbar=(\"ci\", 95))\n", + "g.set(title=\"Average wealth over time\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Which is exactly 1, as expected in this model, since each agent starts with one wealth unit, and each agent gives one wealth unit to another agent at each step." ] }, { @@ -746,7 +1125,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:42.711736Z", + "start_time": "2023-04-25T18:23:42.702477Z" + } + }, "outputs": [], "source": [ "# save the model data (stored in the pandas gini object) to CSV\n", @@ -775,7 +1159,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:23:42.716831Z", + "start_time": "2023-04-25T18:23:42.714736Z" + } + }, "outputs": [], "source": [ "def compute_gini(model):\n", @@ -874,13 +1263,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**Note for Windows OS users:** If you are running this tutorial in Jupyter, make sure that you set `number_processes = 1` (single process). If `number_processes` is greater than 1, it is less straightforward to set up. You can read [Mesa's collection of useful snippets](https://github.com/projectmesa/mesa/blob/main/docs/useful-snippets/snippets.rst), in 'Using multi-process `batch_run` on Windows' section for how to do it." + "**Note for Windows OS users:** If you are running this tutorial in Jupyter, make sure that you set `number_processes = 1` (single process). If `number_processes` is greater than 1, it is less straightforward to set up. You can read [Mesa's how-to guide](https://github.com/projectmesa/mesa/blob/main/docs/howto.rst), in 'Using multi-process `batch_run` on Windows' section for how to do it." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:24:02.422337Z", + "start_time": "2023-04-25T18:23:42.717833Z" + } + }, "outputs": [], "source": [ "params = {\"width\": 10, \"height\": 10, \"N\": range(10, 500, 10)}\n", @@ -906,11 +1300,14 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:24:10.090556Z", + "start_time": "2023-04-25T18:24:02.423340Z" + } + }, "outputs": [], "source": [ - "import pandas as pd\n", - "\n", "results_df = pd.DataFrame(results)\n", "print(results_df.keys())" ] @@ -925,13 +1322,25 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:24:10.237362Z", + "start_time": "2023-04-25T18:24:10.090556Z" + } + }, "outputs": [], "source": [ + "# Filter the results to only contain the data of one agent (the Gini coefficient will be the same for the entire population at any time) at the 100th step of each episode\n", "results_filtered = results_df[(results_df.AgentID == 0) & (results_df.Step == 100)]\n", - "N_values = results_filtered.N.values\n", - "gini_values = results_filtered.Gini.values\n", - "plt.scatter(N_values, gini_values)" + "results_filtered[[\"iteration\", \"N\", \"Gini\"]].reset_index(\n", + " drop=True\n", + ").head() # Create a scatter plot\n", + "g = sns.scatterplot(data=results_filtered, x=\"N\", y=\"Gini\")\n", + "g.set(\n", + " xlabel=\"Number of agents\",\n", + " ylabel=\"Gini coefficient\",\n", + " title=\"Gini coefficient vs. number of agents\",\n", + ");" ] }, { @@ -947,7 +1356,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:24:10.257241Z", + "start_time": "2023-04-25T18:24:10.239363Z" + } + }, "outputs": [], "source": [ "# First, we filter the results\n", @@ -973,7 +1387,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-25T18:24:10.314122Z", + "start_time": "2023-04-25T18:24:10.258232Z" + } + }, "outputs": [], "source": [ "results_one_episode = results_df[\n", @@ -995,8 +1414,6 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "`virtual environment`: http://docs.python-guide.org/en/latest/dev/virtualenvs/\n", - "\n", "[Comer2014] Comer, Kenneth W. “Who Goes First? An Examination of the Impact of Activation on Outcome Behavior in AgentBased Models.” George Mason University, 2014. http://mars.gmu.edu/bitstream/handle/1920/9070/Comer_gmu_0883E_10539.pdf\n", "\n", "[Dragulescu2002] Drăgulescu, Adrian A., and Victor M. Yakovenko. “Statistical Mechanics of Money, Income, and Wealth: A Short Survey.” arXiv Preprint Cond-mat/0211175, 2002. http://arxiv.org/abs/cond-mat/0211175." @@ -1006,7 +1423,7 @@ "metadata": { "anaconda-cloud": {}, "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1020,7 +1437,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.6" + "version": "3.10.12" }, "widgets": { "state": {}, diff --git a/docs/tutorials/intro_tutorial.rst b/docs/tutorials/intro_tutorial.rst deleted file mode 100644 index 1b13daee0be..00000000000 --- a/docs/tutorials/intro_tutorial.rst +++ /dev/null @@ -1,1186 +0,0 @@ -Introductory Tutorial -===================== - -Tutorial Description --------------------- - -`Mesa `__ is a Python framework for -`agent-based -modeling `__. Getting -started with Mesa is easy. In this tutorial, we will walk through -creating a simple model and progressively add functionality which will -illustrate Mesa’s core features. - -**Note:** This tutorial is a work-in-progress. If you find any errors or -bugs, or just find something unclear or confusing, `let us -know `__! - -The base for this tutorial is a very simple model of agents exchanging -money. Next, we add *space* to allow agents to move. Then, we’ll cover -two of Mesa’s analytic tools: the *data collector* and *batch runner*. -After that, we’ll add an *interactive visualization* which lets us watch -the model as it runs. Finally, we go over how to write your own -visualization module, for users who are comfortable with JavaScript. - -You can also find all the code this tutorial describes in the -**examples/boltzmann_wealth_model** directory of the Mesa repository. - -Sample Model Description ------------------------- - -The tutorial model is a very simple simulated agent-based economy, drawn -from econophysics and presenting a statistical mechanics approach to -wealth distribution [Dragulescu2002]. The rules of our tutorial model: - -1. There are some number of agents. -2. All agents begin with 1 unit of money. -3. At every step of the model, an agent gives 1 unit of money (if they - have it) to some other agent. - -Despite its simplicity, this model yields results that are often -unexpected to those not familiar with it. For our purposes, it also -easily demonstrates Mesa’s core features. - -Let’s get started. - -Installation -~~~~~~~~~~~~ - -To start, install Mesa. We recommend doing this in a `virtual -environment `__, -but make sure your environment is set up with Python 3. Mesa requires -Python3 and does not work in Python 2 environments. - -To install Mesa, simply: - -.. code:: bash - - $ pip install mesa - -When you do that, it will install Mesa itself, as well as any -dependencies that aren’t in your setup yet. Additional dependencies -required by this tutorial can be found in the -**examples/boltzmann_wealth_model/requirements.txt** file, which can be -installed directly form the github repository by running: - -.. code:: bash - - $ pip install -r https://raw.githubusercontent.com/projectmesa/mesa/main/examples/boltzmann_wealth_model/requirements.txt - -| This will install the dependencies listed in the requirements.txt file - which are: -| - jupyter (Ipython interactive notebook) -| - matplotlib (Python’s visualization library) -| - mesa (this ABM library – if not installed) -| - numpy (Python’s numerical python library) - -Building a sample model ------------------------ - -Once Mesa is installed, you can start building our model. You can write -models in two different ways: - -1. Write the code in its own file with your favorite text editor, or -2. Write the model interactively in `Jupyter - Notebook `__ cells. - -Either way, it’s good practice to put your model in its own folder – -especially if the project will end up consisting of multiple files (for -example, Python files for the model and the visualization, a Notebook -for analysis, and a Readme with some documentation and discussion). - -Begin by creating a folder, and either launch a Notebook or create a new -Python source file. We will use the name ``money_model.py`` here. - -Setting up the model -~~~~~~~~~~~~~~~~~~~~ - -To begin writing the model code, we start with two core classes: one for -the overall model, the other for the agents. The model class holds the -model-level attributes, manages the agents, and generally handles the -global level of our model. Each instantiation of the model class will be -a specific model run. Each model will contain multiple agents, all of -which are instantiations of the agent class. Both the model and agent -classes are child classes of Mesa’s generic ``Model`` and ``Agent`` -classes. This is seen in the code with ``class MoneyModel(mesa.Model)`` -or ``class MoneyAgent(mesa.Agent)``. If you want you can specifically -the class being imported by looking at the -`model `__ -or -`agent `__ -code in the mesa repo. - -Each agent has only one variable: how much wealth it currently has. -(Each agent will also have a unique identifier (i.e., a name), stored in -the ``unique_id`` variable. Giving each agent a unique id is a good -practice when doing agent-based modeling.) - -There is only one model-level parameter: how many agents the model -contains. When a new model is started, we want it to populate itself -with the given number of agents. - -The beginning of both classes looks like this: - -.. code:: ipython3 - - import mesa - - - class MoneyAgent(mesa.Agent): - """An agent with fixed initial wealth.""" - - def __init__(self, unique_id, model): - super().__init__(unique_id, model) - self.wealth = 1 - - - class MoneyModel(mesa.Model): - """A model with some number of agents.""" - - def __init__(self, N): - self.num_agents = N - # Create agents - for i in range(self.num_agents): - a = MoneyAgent(i, self) - -Adding the scheduler -~~~~~~~~~~~~~~~~~~~~ - -Time in most agent-based models moves in steps, sometimes also called -**ticks**. At each step of the model, one or more of the agents – -usually all of them – are activated and take their own step, changing -internally and/or interacting with one another or the environment. - -The **scheduler** is a special model component which controls the order -in which agents are activated. For example, all the agents may activate -in the same order every step; their order might be shuffled; we may try -to simulate all the agents acting at the same time; and more. Mesa -offers a few different built-in scheduler classes, with a common -interface. That makes it easy to change the activation regime a given -model uses, and see whether it changes the model behavior. This may not -seem important, but scheduling patterns can have an impact on your -results [Comer2014]. - -For now, let’s use one of the simplest ones: ``RandomActivation``\ \*, -which activates all the agents once per step, in random order. Every -agent is expected to have a ``step`` method. The step method is the -action the agent takes when it is activated by the model schedule. We -add an agent to the schedule using the ``add`` method; when we call the -schedule’s ``step`` method, the model shuffles the order of the agents, -then activates and executes each agent’s ``step`` method. - -\*Unlike ``mesa.model`` or ``mesa.agent``, ``mesa.time`` has multiple -classes (e.g. ``RandomActivation``, ``StagedActivation`` etc). To ensure -context, time is used in the import as evidenced below with -``mesa.time.Randomactivation``. You can see the different time classes -as -`mesa.time `__. - -With that in mind, the model code with the scheduler added looks like -this: - -.. code:: ipython3 - - import mesa - - - class MoneyAgent(mesa.Agent): - """An agent with fixed initial wealth.""" - - def __init__(self, unique_id, model): - super().__init__(unique_id, model) - self.wealth = 1 - - def step(self): - # The agent's step will go here. - # For demonstration purposes we will print the agent's unique_id - print("Hi, I am agent " + str(self.unique_id) + ".") - - - class MoneyModel(mesa.Model): - """A model with some number of agents.""" - - def __init__(self, N): - self.num_agents = N - self.schedule = mesa.time.RandomActivation(self) - # Create agents - for i in range(self.num_agents): - a = MoneyAgent(i, self) - self.schedule.add(a) - - def step(self): - """Advance the model by one step.""" - self.schedule.step() - -At this point, we have a model which runs – it just doesn’t do anything. -You can see for yourself with a few easy lines. If you’ve been working -in an interactive session, you can create a model object directly. -Otherwise, you need to open an interactive session in the same directory -as your source code file, and import the classes. For example, if your -code is in ``money_model.py``: - -.. code:: python - - from money_model import MoneyModel - -Then create the model object, and run it for one step: - -.. code:: ipython3 - - empty_model = MoneyModel(10) - empty_model.step() - - -.. parsed-literal:: - - Hi, I am agent 6. - Hi, I am agent 2. - Hi, I am agent 1. - Hi, I am agent 0. - Hi, I am agent 4. - Hi, I am agent 5. - Hi, I am agent 3. - Hi, I am agent 9. - Hi, I am agent 8. - Hi, I am agent 7. - - -Exercise -^^^^^^^^ - -Try modifying the code above to have every agent print out its -``wealth`` when it is activated. Run a few steps of the model to see how -the agent activation order is shuffled each step. - -Agent Step -~~~~~~~~~~ - -Now we just need to have the agents do what we intend for them to do: -check their wealth, and if they have the money, give one unit of it away -to another random agent. To allow the agent to choose another agent at -random, we use the ``model.random`` random-number generator. This works -just like Python’s ``random`` module, but with a fixed seed set when the -model is instantiated, that can be used to replicate a specific model -run later. - -To pick an agent at random, we need a list of all agents. Notice that -there isn’t such a list explicitly in the model. The scheduler, however, -does have an internal list of all the agents it is scheduled to -activate. - -With that in mind, we rewrite the agent ``step`` method, like this: - -.. code:: ipython3 - - class MoneyAgent(mesa.Agent): - """An agent with fixed initial wealth.""" - - def __init__(self, unique_id, model): - super().__init__(unique_id, model) - self.wealth = 1 - - def step(self): - if self.wealth == 0: - return - other_agent = self.random.choice(self.model.schedule.agents) - other_agent.wealth += 1 - self.wealth -= 1 - -Running your first model -~~~~~~~~~~~~~~~~~~~~~~~~ - -With that last piece in hand, it’s time for the first rudimentary run of -the model. - -If you’ve written the code in its own file (``money_model.py`` or a -different name), launch an interpreter in the same directory as the file -(either the plain Python command-line interpreter, or the IPython -interpreter), or launch a Jupyter Notebook there. Then import the -classes you created. (If you wrote the code in a Notebook, obviously -this step isn’t necessary). - -.. code:: python - - from money_model import * - -Now let’s create a model with 10 agents, and run it for 10 steps. - -.. code:: ipython3 - - model = MoneyModel(10) - for i in range(10): - model.step() - -Next, we need to get some data out of the model. Specifically, we want -to see the distribution of the agent’s wealth. We can get the wealth -values with list comprehension, and then use matplotlib (or another -graphics library) to visualize the data in a histogram. - -If you are running from a text editor or IDE, you’ll also need to add -this line, to make the graph appear. - -.. code:: python - - plt.show() - -.. code:: ipython3 - - # For a jupyter notebook add the following line: - %matplotlib inline - - # The below is needed for both notebooks and scripts - import matplotlib.pyplot as plt - - agent_wealth = [a.wealth for a in model.schedule.agents] - plt.hist(agent_wealth) - - - - -.. parsed-literal:: - - (array([2., 0., 0., 0., 0., 6., 0., 0., 0., 2.]), - array([0. , 0.2, 0.4, 0.6, 0.8, 1. , 1.2, 1.4, 1.6, 1.8, 2. ]), - ) - - - - -.. image:: intro_tutorial_files/output_19_1.png - - -You’ll should see something like the distribution above. Yours will -almost certainly look at least slightly different, since each run of the -model is random, after all. - -To get a better idea of how a model behaves, we can create multiple -model runs and see the distribution that emerges from all of them. We -can do this with a nested for loop: - -.. code:: ipython3 - - all_wealth = [] - # This runs the model 100 times, each model executing 10 steps. - for j in range(100): - # Run the model - model = MoneyModel(10) - for i in range(10): - model.step() - - # Store the results - for agent in model.schedule.agents: - all_wealth.append(agent.wealth) - - plt.hist(all_wealth, bins=range(max(all_wealth) + 1)) - - - - -.. parsed-literal:: - - (array([433., 304., 150., 71., 29., 13.]), - array([0, 1, 2, 3, 4, 5, 6]), - ) - - - - -.. image:: intro_tutorial_files/output_22_1.png - - -This runs 100 instantiations of the model, and runs each for 10 steps. -(Notice that we set the histogram bins to be integers, since agents can -only have whole numbers of wealth). This distribution looks a lot -smoother. By running the model 100 times, we smooth out some of the -‘noise’ of randomness, and get to the model’s overall expected behavior. - -This outcome might be surprising. Despite the fact that all agents, on -average, give and receive one unit of money every step, the model -converges to a state where most agents have a small amount of money and -a small number have a lot of money. - -Adding space -~~~~~~~~~~~~ - -Many ABMs have a spatial element, with agents moving around and -interacting with nearby neighbors. Mesa currently supports two overall -kinds of spaces: grid, and continuous. Grids are divided into cells, and -agents can only be on a particular cell, like pieces on a chess board. -Continuous space, in contrast, allows agents to have any arbitrary -position. Both grids and continuous spaces are frequently -`toroidal `__, meaning -that the edges wrap around, with cells on the right edge connected to -those on the left edge, and the top to the bottom. This prevents some -cells having fewer neighbors than others, or agents being able to go off -the edge of the environment. - -Let’s add a simple spatial element to our model by putting our agents on -a grid and make them walk around at random. Instead of giving their unit -of money to any random agent, they’ll give it to an agent on the same -cell. - -Mesa has two main types of grids: ``SingleGrid`` and ``MultiGrid``\ \*. -``SingleGrid`` enforces at most one agent per cell; ``MultiGrid`` allows -multiple agents to be in the same cell. Since we want agents to be able -to share a cell, we use ``MultiGrid``. - -\*However there are more types of space to include ``HexGrid``, -``NetworkGrid``, and the previously mentioned ``ContinuousSpace``. -Similar to ``mesa.time`` context is retained with -``mesa.space.[enter class]``. You can see the different classes as -`mesa.space `__ - -We instantiate a grid with width and height parameters, and a boolean as -to whether the grid is toroidal. Let’s make width and height model -parameters, in addition to the number of agents, and have the grid -always be toroidal. We can place agents on a grid with the grid’s -``place_agent`` method, which takes an agent and an (x, y) tuple of the -coordinates to place the agent. - -.. code:: ipython3 - - class MoneyModel(mesa.Model): - """A model with some number of agents.""" - - def __init__(self, N, width, height): - self.num_agents = N - self.grid = mesa.space.MultiGrid(width, height, True) - self.schedule = mesa.time.RandomActivation(self) - - # Create agents - for i in range(self.num_agents): - a = MoneyAgent(i, self) - self.schedule.add(a) - - # Add the agent to a random grid cell - x = self.random.randrange(self.grid.width) - y = self.random.randrange(self.grid.height) - self.grid.place_agent(a, (x, y)) - -Under the hood, each agent’s position is stored in two ways: the agent -is contained in the grid in the cell it is currently in, and the agent -has a ``pos`` variable with an (x, y) coordinate tuple. The -``place_agent`` method adds the coordinate to the agent automatically. - -Now we need to add to the agents’ behaviors, letting them move around -and only give money to other agents in the same cell. - -First let’s handle movement, and have the agents move to a neighboring -cell. The grid object provides a ``move_agent`` method, which like you’d -imagine, moves an agent to a given cell. That still leaves us to get the -possible neighboring cells to move to. There are a couple ways to do -this. One is to use the current coordinates, and loop over all -coordinates +/- 1 away from it. For example: - -.. code:: python - - neighbors = [] - x, y = self.pos - for dx in [-1, 0, 1]: - for dy in [-1, 0, 1]: - neighbors.append((x+dx, y+dy)) - -But there’s an even simpler way, using the grid’s built-in -``get_neighborhood`` method, which returns all the neighbors of a given -cell. This method can get two types of cell neighborhoods: -`Moore `__ (includes -all 8 surrounding squares), and `Von -Neumann `__\ (only -up/down/left/right). It also needs an argument as to whether to include -the center cell itself as one of the neighbors. - -With that in mind, the agent’s ``move`` method looks like this: - -.. code:: python - - class MoneyAgent(mesa.Agent): - #... - 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) - -Next, we need to get all the other agents present in a cell, and give -one of them some money. We can get the contents of one or more cells -using the grid’s ``get_cell_list_contents`` method, or by accessing a -cell directly. The method accepts a list of cell coordinate tuples, or a -single tuple if we only care about one cell. - -.. code:: python - - class MoneyAgent(mesa.Agent): - #... - def give_money(self): - cellmates = self.model.grid.get_cell_list_contents([self.pos]) - if len(cellmates) > 1: - other = self.random.choice(cellmates) - other.wealth += 1 - self.wealth -= 1 - -And with those two methods, the agent’s ``step`` method becomes: - -.. code:: python - - class MoneyAgent(mesa.Agent): - # ... - def step(self): - self.move() - if self.wealth > 0: - self.give_money() - -Now, putting that all together should look like this: - -.. code:: ipython3 - - class MoneyAgent(mesa.Agent): - """An agent with fixed initial wealth.""" - - def __init__(self, unique_id, model): - super().__init__(unique_id, model) - self.wealth = 1 - - def move(self): - possible_steps = self.model.grid.get_neighborhood( - self.pos, moore=True, include_center=False - ) - new_position = self.random.choice(possible_steps) - self.model.grid.move_agent(self, new_position) - - def give_money(self): - cellmates = self.model.grid.get_cell_list_contents([self.pos]) - if len(cellmates) > 1: - other_agent = self.random.choice(cellmates) - other_agent.wealth += 1 - self.wealth -= 1 - - def step(self): - self.move() - if self.wealth > 0: - self.give_money() - - - class MoneyModel(mesa.Model): - """A model with some number of agents.""" - - def __init__(self, N, width, height): - self.num_agents = N - self.grid = mesa.space.MultiGrid(width, height, True) - self.schedule = mesa.time.RandomActivation(self) - # Create agents - for i in range(self.num_agents): - a = MoneyAgent(i, self) - self.schedule.add(a) - # Add the agent to a random grid cell - x = self.random.randrange(self.grid.width) - y = self.random.randrange(self.grid.height) - self.grid.place_agent(a, (x, y)) - - def step(self): - self.schedule.step() - -Let’s create a model with 50 agents on a 10x10 grid, and run it for 20 -steps. - -.. code:: ipython3 - - model = MoneyModel(50, 10, 10) - for i in range(20): - model.step() - -Now let’s use matplotlib and numpy to visualize the number of agents -residing in each cell. To do that, we create a numpy array of the same -size as the grid, filled with zeros. Then we use the grid object’s -``coord_iter()`` feature, which lets us loop over every cell in the -grid, giving us each cell’s coordinates and contents in turn. - -.. code:: ipython3 - - import numpy as np - - agent_counts = np.zeros((model.grid.width, model.grid.height)) - for cell in model.grid.coord_iter(): - cell_content, x, y = cell - agent_count = len(cell_content) - agent_counts[x][y] = agent_count - plt.imshow(agent_counts, interpolation="nearest") - plt.colorbar() - - # If running from a text editor or IDE, remember you'll need the following: - # plt.show() - - - - -.. parsed-literal:: - - - - - - -.. image:: intro_tutorial_files/output_32_1.png - - -Collecting Data -~~~~~~~~~~~~~~~ - -So far, at the end of every model run, we’ve had to go and write our own -code to get the data out of the model. This has two problems: it isn’t -very efficient, and it only gives us end results. If we wanted to know -the wealth of each agent at each step, we’d have to add that to the loop -of executing steps, and figure out some way to store the data. - -Since one of the main goals of agent-based modeling is generating data -for analysis, Mesa provides a class which can handle data collection and -storage for us and make it easier to analyze. - -The data collector stores three categories of data: model-level -variables, agent-level variables, and tables (which are a catch-all for -everything else). Model- and agent-level variables are added to the data -collector along with a function for collecting them. Model-level -collection functions take a model object as an input, while agent-level -collection functions take an agent object as an input. Both then return -a value computed from the model or each agent at their current state. -When the data collector’s ``collect`` method is called, with a model -object as its argument, it applies each model-level collection function -to the model, and stores the results in a dictionary, associating the -current value with the current step of the model. Similarly, the method -applies each agent-level collection function to each agent currently in -the schedule, associating the resulting value with the step of the -model, and the agent’s ``unique_id``. - -Let’s add a DataCollector to the model with -`mesa.DataCollector `__, -and collect two variables. At the agent level, we want to collect every -agent’s wealth at every step. At the model level, let’s measure the -model’s `Gini -Coefficient `__, a -measure of wealth inequality. - -.. code:: ipython3 - - def compute_gini(model): - agent_wealths = [agent.wealth for agent in model.schedule.agents] - x = sorted(agent_wealths) - N = model.num_agents - B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x)) - return 1 + (1 / N) - 2 * B - - - class MoneyAgent(mesa.Agent): - """An agent with fixed initial wealth.""" - - def __init__(self, unique_id, model): - super().__init__(unique_id, model) - self.wealth = 1 - - def move(self): - possible_steps = self.model.grid.get_neighborhood( - self.pos, moore=True, include_center=False - ) - new_position = self.random.choice(possible_steps) - self.model.grid.move_agent(self, new_position) - - def give_money(self): - cellmates = self.model.grid.get_cell_list_contents([self.pos]) - if len(cellmates) > 1: - other = self.random.choice(cellmates) - other.wealth += 1 - self.wealth -= 1 - - def step(self): - self.move() - if self.wealth > 0: - self.give_money() - - - class MoneyModel(mesa.Model): - """A model with some number of agents.""" - - def __init__(self, N, width, height): - self.num_agents = N - self.grid = mesa.space.MultiGrid(width, height, True) - self.schedule = mesa.time.RandomActivation(self) - - # Create agents - for i in range(self.num_agents): - a = MoneyAgent(i, self) - self.schedule.add(a) - # Add the agent to a random grid cell - x = self.random.randrange(self.grid.width) - y = self.random.randrange(self.grid.height) - self.grid.place_agent(a, (x, y)) - - self.datacollector = mesa.DataCollector( - model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"} - ) - - def step(self): - self.datacollector.collect(self) - self.schedule.step() - -At every step of the model, the datacollector will collect and store the -model-level current Gini coefficient, as well as each agent’s wealth, -associating each with the current step. - -We run the model just as we did above. Now is when an interactive -session, especially via a Notebook, comes in handy: the DataCollector -can export the data its collected as a pandas\* DataFrame, for easy -interactive analysis. - -\*If you are new to Python, please be aware that pandas is already -installed as a dependency of Mesa and that -`pandas `__ is a “fast, powerful, -flexible and easy to use open source data analysis and manipulation -tool”. pandas is great resource to help analyze the data collected in -your models - -.. code:: ipython3 - - model = MoneyModel(50, 10, 10) - for i in range(100): - model.step() - -To get the series of Gini coefficients as a pandas DataFrame: - -.. code:: ipython3 - - gini = model.datacollector.get_model_vars_dataframe() - gini.plot() - - - - -.. parsed-literal:: - - - - - - -.. image:: intro_tutorial_files/output_38_1.png - - -Similarly, we can get the agent-wealth data: - -.. code:: ipython3 - - agent_wealth = model.datacollector.get_agent_vars_dataframe() - agent_wealth.head() - - - - -.. raw:: html - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Wealth
StepAgentID
001
11
21
31
41
-
- - - -You’ll see that the DataFrame’s index is pairings of model step and -agent ID. You can analyze it the way you would any other DataFrame. For -example, to get a histogram of agent wealth at the model’s end: - -.. code:: ipython3 - - end_wealth = agent_wealth.xs(99, level="Step")["Wealth"] - end_wealth.hist(bins=range(agent_wealth.Wealth.max() + 1)) - - - - -.. parsed-literal:: - - - - - - -.. image:: intro_tutorial_files/output_42_1.png - - -Or to plot the wealth of a given agent (in this example, agent 14): - -.. code:: ipython3 - - one_agent_wealth = agent_wealth.xs(14, level="AgentID") - one_agent_wealth.Wealth.plot() - - - - -.. parsed-literal:: - - - - - - -.. image:: intro_tutorial_files/output_44_1.png - - -You can also use pandas to export the data to a CSV (comma separated -value), which can be opened by any common spreadsheet application or -opened by pandas. - -If you do not specify a file path, the file will be saved in the local -directory. After you run the code below you will see two files appear -(*model_data.csv* and *agent_data.csv*) - -.. code:: ipython3 - - # save the model data (stored in the pandas gini object) to CSV - gini.to_csv("model_data.csv") - - # save the agent data (stored in the pandas agent_wealth object) to CSV - agent_wealth.to_csv("agent_data.csv") - -Batch Run -~~~~~~~~~ - -Like we mentioned above, you usually won’t run a model only once, but -multiple times, with fixed parameters to find the overall distributions -the model generates, and with varying parameters to analyze how they -drive the model’s outputs and behaviors. Instead of needing to write -nested for-loops for each model, Mesa provides a `batch_run `__ -function which automates it for you. - -The batch runner also requires an additional variable ``self.running`` -for the MoneyModel class. This variable enables conditional shut off of -the model once a condition is met. In this example it will be set as -True indefinitely. - -.. code:: ipython3 - - def compute_gini(model): - agent_wealths = [agent.wealth for agent in model.schedule.agents] - x = sorted(agent_wealths) - N = model.num_agents - B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x)) - return 1 + (1 / N) - 2 * B - - - class MoneyModel(mesa.Model): - """A model with some number of agents.""" - - def __init__(self, N, width, height): - self.num_agents = N - self.grid = mesa.space.MultiGrid(width, height, True) - self.schedule = mesa.time.RandomActivation(self) - self.running = True - - # Create agents - for i in range(self.num_agents): - a = MoneyAgent(i, self) - self.schedule.add(a) - # Add the agent to a random grid cell - x = self.random.randrange(self.grid.width) - y = self.random.randrange(self.grid.height) - self.grid.place_agent(a, (x, y)) - - self.datacollector = mesa.DataCollector( - model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"} - ) - - def step(self): - self.datacollector.collect(self) - self.schedule.step() - -We call ``batch_run`` with the following arguments: - -- ``model_cls`` The model class that is used for the batch run. - -- ``parameters`` A dictionary containing all the parameters of the - model class and desired values to use for the batch run as key-value - pairs. Each value can either be fixed ( - e.g. ``{"height": 10, "width": 10}``) or an iterable - (e.g. ``{"N": range(10, 500, 10)}``). ``batch_run`` will then - generate all possible parameter combinations based on this dictionary - and run the model ``iterations`` times for each combination. - -- ``number_processes`` If not specified, defaults to 1. Set it to - ``None`` to use all the available processors. Note: Multiprocessing - does make debugging challenging. If your parameter sweeps are - resulting in unexpected errors set ``number_processes = 1``. - -- ``iterations`` The number of iterations to run each parameter - combination for. Optional. If not specified, defaults to 1. - -- ``data_collection_period`` The length of the period (number of steps) - after which the model and agent reporters collect data. Optional. If - not specified, defaults to -1, i.e. only at the end of each episode. - -- ``max_steps`` The maximum number of time steps after which the model - halts. An episode does either end when ``self.running`` of the model - class is set to ``False`` or when - ``model.schedule.steps == max_steps`` is reached. Optional. If not - specified, defaults to 1000. - -- ``display_progress`` Display the batch run progress. Optional. If not - specified, defaults to ``True``. - -In the following example, we hold the height and width fixed, and vary -the number of agents. We tell the batch runner to run 5 instantiations -of the model with each number of agents, and to run each for 100 steps. - -We want to keep track of - -1. the Gini coefficient value and -2. the individual agent’s wealth development. - -Since for the latter changes at each time step might be interesting, we -set ``data_collection_period = 1``. - -Note: The total number of runs is 245 (= 49 different populations \* 5 -iterations per population). However, the resulting list of dictionaries -will be of length 6186250 (= 250 average agents per population \* 49 -different populations \* 5 iterations per population \* 101 steps per -iteration). - -**Note for Windows OS users:** If you are running this tutorial in -Jupyter, make sure that you set ``number_processes = 1`` (single -process). If ``number_processes`` is greater than 1, it is less -straightforward to set up. You can read `Mesa’s collection of useful -snippets `__, -in ‘Using multi-process ``batch_run`` on Windows’ section for how to do -it. - -.. code:: ipython3 - - params = {"width": 10, "height": 10, "N": range(10, 500, 10)} - - results = mesa.batch_run( - MoneyModel, - parameters=params, - iterations=5, - max_steps=100, - number_processes=1, - data_collection_period=1, - display_progress=True, - ) - - -.. parsed-literal:: - - 245it [00:34, 7.02it/s] - - -To further analyze the return of the ``batch_run`` function, we convert -the list of dictionaries to a Pandas DataFrame and print its keys. - -.. code:: ipython3 - - import pandas as pd - - results_df = pd.DataFrame(results) - print(results_df.keys()) - - -.. parsed-literal:: - - Index(['RunId', 'iteration', 'Step', 'width', 'height', 'N', 'Gini', 'AgentID', - 'Wealth'], - dtype='object') - - -First, we want to take a closer look at how the Gini coefficient at the -end of each episode changes as we increase the size of the population. -For this, we filter our results to only contain the data of one agent -(the Gini coefficient will be the same for the entire population at any -time) at the 100th step of each episode and then scatter-plot the values -for the Gini coefficient over the the number of agents. Notice there are -five values for each population size since we set ``iterations=5`` when -calling the batch run. - -.. code:: ipython3 - - results_filtered = results_df[(results_df.AgentID == 0) & (results_df.Step == 100)] - N_values = results_filtered.N.values - gini_values = results_filtered.Gini.values - plt.scatter(N_values, gini_values) - - - - -.. parsed-literal:: - - - - - - -.. image:: intro_tutorial_files/output_57_1.png - - -Second, we want to display the agent’s wealth at each time step of one -specific episode. To do this, we again filter our large data frame, this -time with a fixed number of agents and only for a specific iteration of -that population. To print the results, we convert the filtered data -frame to a string specifying the desired columns to print. - -Pandas has built-in functions to convert to a lot of different data -formats. For example, to display as a table in a Jupyter Notebook, we -can use the ``to_html()`` function which takes the same arguments as -``to_string()`` (see commented lines). - -.. code:: ipython3 - - # First, we filter the results - one_episode_wealth = results_df[(results_df.N == 10) & (results_df.iteration == 2)] - # Then, print the columns of interest of the filtered data frame - print( - one_episode_wealth.to_string( - index=False, columns=["Step", "AgentID", "Wealth"], max_rows=25 - ) - ) - # For a prettier display we can also convert the data frame to html, uncomment to test in a Jupyter Notebook - # from IPython.display import display, HTML - # display(HTML(one_episode_wealth.to_html(index=False, columns=['Step', 'AgentID', 'Wealth'], max_rows=25))) - - -.. parsed-literal:: - - Step AgentID Wealth - 0 0 1 - 0 1 1 - 0 2 1 - 0 3 1 - 0 4 1 - 0 5 1 - 0 6 1 - 0 7 1 - 0 8 1 - 0 9 1 - 1 0 2 - 1 1 1 - ... ... ... - 99 8 4 - 99 9 1 - 100 0 0 - 100 1 0 - 100 2 1 - 100 3 0 - 100 4 1 - 100 5 1 - 100 6 0 - 100 7 2 - 100 8 4 - 100 9 1 - - -Lastly, we want to take a look at the development of the Gini -coefficient over the course of one iteration. Filtering and printing -looks almost the same as above, only this time we choose a different -episode. - -.. code:: ipython3 - - results_one_episode = results_df[ - (results_df.N == 10) & (results_df.iteration == 1) & (results_df.AgentID == 0) - ] - print(results_one_episode.to_string(index=False, columns=["Step", "Gini"], max_rows=25)) - - -.. parsed-literal:: - - Step Gini - 0 0.00 - 1 0.18 - 2 0.18 - 3 0.18 - 4 0.18 - 5 0.32 - 6 0.32 - 7 0.32 - 8 0.42 - 9 0.42 - 10 0.42 - 11 0.42 - ... ... - 89 0.66 - 90 0.66 - 91 0.66 - 92 0.66 - 93 0.56 - 94 0.56 - 95 0.56 - 96 0.56 - 97 0.56 - 98 0.56 - 99 0.56 - 100 0.56 - - -Happy Modeling! -~~~~~~~~~~~~~~~ - -This document is a work in progress. If you see any errors, exclusions -or have any problems please contact -`us `__. - -``virtual environment``: -http://docs.python-guide.org/en/latest/dev/virtualenvs/ - -[Comer2014] Comer, Kenneth W. “Who Goes First? An Examination of the -Impact of Activation on Outcome Behavior in AgentBased Models.” George -Mason University, 2014. -http://mars.gmu.edu/bitstream/handle/1920/9070/Comer_gmu_0883E_10539.pdf - -[Dragulescu2002] Drăgulescu, Adrian A., and Victor M. Yakovenko. -“Statistical Mechanics of Money, Income, and Wealth: A Short Survey.” -arXiv Preprint Cond-mat/0211175, 2002. -http://arxiv.org/abs/cond-mat/0211175. diff --git a/docs/tutorials/intro_tutorial_files/output_19_1.png b/docs/tutorials/intro_tutorial_files/output_19_1.png deleted file mode 100644 index 67febb9ea3a..00000000000 Binary files a/docs/tutorials/intro_tutorial_files/output_19_1.png and /dev/null differ diff --git a/docs/tutorials/intro_tutorial_files/output_22_1.png b/docs/tutorials/intro_tutorial_files/output_22_1.png deleted file mode 100644 index cf21c1a8282..00000000000 Binary files a/docs/tutorials/intro_tutorial_files/output_22_1.png and /dev/null differ diff --git a/docs/tutorials/intro_tutorial_files/output_32_1.png b/docs/tutorials/intro_tutorial_files/output_32_1.png deleted file mode 100644 index 0a776f4cbe1..00000000000 Binary files a/docs/tutorials/intro_tutorial_files/output_32_1.png and /dev/null differ diff --git a/docs/tutorials/intro_tutorial_files/output_38_1.png b/docs/tutorials/intro_tutorial_files/output_38_1.png deleted file mode 100644 index 6186006826e..00000000000 Binary files a/docs/tutorials/intro_tutorial_files/output_38_1.png and /dev/null differ diff --git a/docs/tutorials/intro_tutorial_files/output_42_1.png b/docs/tutorials/intro_tutorial_files/output_42_1.png deleted file mode 100644 index cdab93196b7..00000000000 Binary files a/docs/tutorials/intro_tutorial_files/output_42_1.png and /dev/null differ diff --git a/docs/tutorials/intro_tutorial_files/output_44_1.png b/docs/tutorials/intro_tutorial_files/output_44_1.png deleted file mode 100644 index c6f98a951ca..00000000000 Binary files a/docs/tutorials/intro_tutorial_files/output_44_1.png and /dev/null differ diff --git a/docs/tutorials/intro_tutorial_files/output_57_1.png b/docs/tutorials/intro_tutorial_files/output_57_1.png deleted file mode 100644 index 47bb0094f40..00000000000 Binary files a/docs/tutorials/intro_tutorial_files/output_57_1.png and /dev/null differ diff --git a/docs/tutorials/visualization_tutorial.ipynb b/docs/tutorials/visualization_tutorial.ipynb new file mode 100644 index 00000000000..f19b21421ef --- /dev/null +++ b/docs/tutorials/visualization_tutorial.ipynb @@ -0,0 +1,271 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Visualization Tutorial" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Important:** \n", + "- If you are just exploring Mesa and want the fastest way to execute the code we recommend executing this tutorial online in a Colab notebook. [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/projectmesa/mesa/blob/main/docs/tutorials/visualization_tutorial.ipynb)\n", + "- If you have installed mesa and are running locally, please ensure that your [Mesa version](https://pypi.org/project/Mesa/) is up-to-date in order to run this tutorial.\n", + "\n", + "### Adding visualization\n", + "\n", + "So far, we've built a model, run it, and analyzed some output afterwards. However, one of the advantages of agent-based models is that we can often watch them run step by step, potentially spotting unexpected patterns, behaviors or bugs, or developing new intuitions, hypotheses, or insights. Other times, watching a model run can explain it to an unfamiliar audience better than static explanations. Like many ABM frameworks, Mesa allows you to create an interactive visualization of the model. In this section we'll walk through creating a visualization using built-in components, and (for advanced users) how to create a new visualization element.\n", + "\n", + "First, a quick explanation of how Mesa's interactive visualization works. The visualization is done in a browser window, using the [Solara](https://solara.dev/) framework, a pure Python, React-style web framework. Running `solara run app.py` will launch a web server, which runs the model, and displays model detail at each step via the Matplotlib plotting library. Alternatively, you can execute everything inside a Jupyter environment." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Grid Visualization\n", + "\n", + "To start with, let's have a visualization where we can watch the agents moving around the grid. Let us use the same `MoneyModel` created in the [Introductory Tutorial](https://mesa.readthedocs.io/en/stable/tutorials/intro_tutorial.html).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%pip install --quiet mesa\n", + "import mesa\n", + "\n", + "# You can either define the BoltzmannWealthModel (aka MoneyModel) or install mesa-models:\n", + "%pip install --quiet -U git+https://github.com/projectmesa/mesa-examples#egg=mesa-models\n", + "\n", + "from mesa_models.boltzmann_wealth_model.model import BoltzmannWealthModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Mesa's grid visualizer works by looping over every cell in a grid, and generating a portrayal for every agent it finds. A portrayal is a dictionary (which can easily be turned into a JSON object) which tells Matplotlib the color and size of the scatterplot markers (each signifying an agent). The only thing we need to provide is a function which takes an agent, and returns a portrayal dictionary. Here's the simplest one: it'll draw each agent as a blue, filled circle, with a radius size of 50." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def agent_portrayal(agent):\n", + " return {\n", + " \"color\": \"tab:blue\",\n", + " \"size\": 50,\n", + " }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In addition to the portrayal method, we instantiate the model parameters, some of which are modifiable by user inputs. In this case, the number of agents, N, is specified as a slider of integers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model_params = {\n", + " \"N\": {\n", + " \"type\": \"SliderInt\",\n", + " \"value\": 50,\n", + " \"label\": \"Number of agents:\",\n", + " \"min\": 10,\n", + " \"max\": 100,\n", + " \"step\": 1,\n", + " },\n", + " \"width\": 10,\n", + " \"height\": 10,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we instantiate the visualization object which (by default) displays the grid containing the agents, and timeseries of of values computed by the model's data collector. In this example, we specify the Gini coefficient.\n", + "\n", + "There are 3 buttons:\n", + "- the step button, which advances the model by 1 step\n", + "- the play button, which advances the model indefinitely until it is paused, or until `model.running` is False (you may specify the stopping condition)\n", + "- the pause button, which pauses the model\n", + "\n", + "To reset the model, simply change the model parameter from the user input (e.g. the \"Number of agents\" slider)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from mesa.experimental import JupyterViz\n", + "\n", + "page = JupyterViz(\n", + " BoltzmannWealthModel,\n", + " model_params,\n", + " measures=[\"Gini\"],\n", + " name=\"Money Model\",\n", + " agent_portrayal=agent_portrayal,\n", + ")\n", + "# This is required to render the visualization in the Jupyter notebook\n", + "page" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Changing the agents\n", + "\n", + "In the visualization above, all we could see is the agents moving around -- but not how much money they had, or anything else of interest. Let's change it so that agents who are broke (wealth 0) are drawn in red, smaller. (TODO: currently, we can't predict the drawing order of the circles, so a broke agent may be overshadowed by a wealthy agent. We should fix this by doing a hollow circle instead)\n", + "\n", + "To do this, we go back to our `agent_portrayal` code and add some code to change the portrayal based on the agent properties and launch the server again." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def agent_portrayal(agent):\n", + " size = 10\n", + " color = \"tab:red\"\n", + " if agent.wealth > 0:\n", + " size = 50\n", + " color = \"tab:blue\"\n", + " return {\"size\": size, \"color\": color}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "page = JupyterViz(\n", + " BoltzmannWealthModel,\n", + " model_params,\n", + " measures=[\"Gini\"],\n", + " name=\"Money Model\",\n", + " agent_portrayal=agent_portrayal,\n", + ")\n", + "# This is required to render the visualization in the Jupyter notebook\n", + "page" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Building your own visualization component\n", + "\n", + "**Note:** This section is for users who have a basic familiarity with Python's Matplotlib plotting library.\n", + "\n", + "If the visualization elements provided by Mesa aren't enough for you, you can build your own and plug them into the model server.\n", + "\n", + "For this example, let's build a simple histogram visualization, which can count the number of agents with each value of wealth." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import solara\n", + "from matplotlib.figure import Figure\n", + "\n", + "\n", + "def make_histogram(model):\n", + " # Note: you must initialize a figure using this method instead of\n", + " # plt.figure(), for thread safety purpose\n", + " fig = Figure()\n", + " ax = fig.subplots()\n", + " wealth_vals = [agent.wealth for agent in model.schedule.agents]\n", + " # Note: you have to use Matplotlib's OOP API instead of plt.hist\n", + " # because plt.hist is not thread-safe.\n", + " ax.hist(wealth_vals, bins=10)\n", + " solara.FigureMatplotlib(fig)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we reinitialize the visualization object, but this time with the histogram (see the measures argument)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "page = JupyterViz(\n", + " BoltzmannWealthModel,\n", + " model_params,\n", + " measures=[\"Gini\", make_histogram],\n", + " name=\"Money Model\",\n", + " agent_portrayal=agent_portrayal,\n", + ")\n", + "# This is required to render the visualization in the Jupyter notebook\n", + "page" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Happy Modeling!\n", + "\n", + "This document is a work in progress. If you see any errors, exclusions or have any problems please contact [us](https://github.com/projectmesa/mesa/issues)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + }, + "nbsphinx": { + "execute": "never" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/Readme.md b/examples/Readme.md deleted file mode 100644 index dac81ed341e..00000000000 --- a/examples/Readme.md +++ /dev/null @@ -1,57 +0,0 @@ -# Example Code -This directory contains example models meant to test and demonstrate Mesa's features, and provide demonstrations for how to build and analyze agent-based models. For more information on each model, see its own Readme and documentation. - -## Models - -Classic models, some of which can be found in NetLogo's/MASON's example models. - -### bank_reserves -A highly abstracted, simplified model of an economy, with only one type of agent and a single bank representing all banks in an economy. - -### color_patches -A cellular automaton model where agents opinions are influenced by that of their neighbors. As the model evolves, color patches representing the prevailing opinion in a given area expand, contract, and sometimes disappear. - -### conways_game_of_life -Implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life), a cellular automata where simple rules can give rise to complex patterns. - -### epstein_civil_violence -Joshua Epstein's [model](http://www.uvm.edu/~pdodds/files/papers/others/2002/epstein2002a.pdf) of how a decentralized uprising can be suppressed or reach a critical mass of support. - -### boid_flockers -[Boids](https://en.wikipedia.org/wiki/Boids)-style flocking model, demonstrating the use of agents moving through a continuous space following direction vectors. - -### forest_fire -Simple cellular automata of a fire spreading through a forest of cells on a grid, based on the NetLogo [Fire model](http://ccl.northwestern.edu/netlogo/models/Fire). - -### hex_snowflake -Conway's game of life on a hexagonal grid. - -### pd_grid -Grid-based demographic prisoner's dilemma model, demonstrating how simple rules can lead to the emergence of widespread cooperation -- and how a model activation regime can change its outcome. - -### schelling (GUI and Text) -Mesa implementation of the classic [Schelling segregation model](http://nifty.stanford.edu/2014/mccown-schelling-model-segregation/). - -### boltzmann_wealth_model -Completed code to go along with the [tutorial]() on making a simple model of how a highly-skewed wealth distribution can emerge from simple rules. - -### wolf_sheep -Implementation of an ecological model of predation and reproduction, based on the NetLogo [Wolf Sheep Predation model](http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation). - -### sugarscape_cg -Implementation of Sugarscape 2 Constant Growback model, based on the Netlogo -[Sugarscape 2 Constant Growback](http://ccl.northwestern.edu/netlogo/models/Sugarscape2ConstantGrowback) - -### virus_on_network -This model is based on the NetLogo model "Virus on Network". - -## Feature examples - -Example models specifically for demonstrating Mesa's features. - -### charts - -A modified version of the "bank_reserves" example made to provide examples of mesa's charting tools. - -### Shape Example -Example of grid display and direction showing agents in the form of arrow-head shape. diff --git a/examples/bank_reserves/Readme.md b/examples/bank_reserves/Readme.md deleted file mode 100644 index 27570d209b0..00000000000 --- a/examples/bank_reserves/Readme.md +++ /dev/null @@ -1,58 +0,0 @@ -# Bank Reserves Model - -## Summary - -A highly abstracted, simplified model of an economy, with only one type of agent and a single bank representing all banks in an economy. People (represented by circles) move randomly within the grid. If two or more people are on the same grid location, there is a 50% chance that they will trade with each other. If they trade, there is an equal chance of giving the other agent $5 or $2. A positive trade balance will be deposited in the bank as savings. If trading results in a negative balance, the agent will try to withdraw from its savings to cover the balance. If it does not have enough savings to cover the negative balance, it will take out a loan from the bank to cover the difference. The bank is required to keep a certain percentage of deposits as reserves. If run.py is used to run the model, then the percent of deposits the bank is required to retain is a user settable parameter. The amount the bank is able to loan at any given time is a function of the amount of deposits, its reserves, and its current total outstanding loan amount. - -The model demonstrates the following Mesa features: - - MultiGrid for creating shareable space for agents - - DataCollector for collecting data on individual model runs - - Slider for adjusting initial model parameters - - ModularServer for visualization of agent interaction - - Agent object inheritance - - Using a BatchRunner to collect data on multiple combinations of model parameters - -## Installation - -To install the dependencies use pip and the requirements.txt in this directory. e.g. - -``` - $ pip install -r requirements.txt -``` - -## Interactive Model Run - -To run the model interactively, use `mesa runserver` in this directory: - -``` - $ mesa runserver -``` - -Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/), select the model parameters, press Reset, then Start. - -## Batch Run - -To run the model as a batch run to collect data on multiple combinations of model parameters, run "batch_run.py" in this directory. - -``` - $ python batch_run.py -``` -A progress status bar will display. - -To update the parameters to test other parameter sweeps, edit the list of parameters in the dictionary named "br_params" in "batch_run.py". - -## Files - -* ``bank_reserves/random_walker.py``: This defines a class that inherits from the Mesa Agent class. The main purpose is to provide a method for agents to move randomly one cell at a time. -* ``bank_reserves/agents.py``: Defines the People and Bank classes. -* ``bank_reserves/model.py``: Defines the Bank Reserves model and the DataCollector functions. -* ``bank_reserves/server.py``: Sets up the interactive visualization server. -* ``run.py``: Launches a model visualization server. -* ``batch_run.py``: Basically the same as model.py, but includes a Mesa BatchRunner. The result of the batch run will be a .csv file with the data from every step of every run. - -## Further Reading - -This model is a Mesa implementation of the Bank Reserves model from NetLogo: - -Wilensky, U. (1998). NetLogo Bank Reserves model. http://ccl.northwestern.edu/netlogo/models/BankReserves. Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. - diff --git a/examples/bank_reserves/bank_reserves/agents.py b/examples/bank_reserves/bank_reserves/agents.py deleted file mode 100644 index d59390df084..00000000000 --- a/examples/bank_reserves/bank_reserves/agents.py +++ /dev/null @@ -1,182 +0,0 @@ -""" -The following code was adapted from the Bank Reserves model included in Netlogo -Model information can be found at: http://ccl.northwestern.edu/netlogo/models/BankReserves -Accessed on: November 2, 2017 -Author of NetLogo code: - Wilensky, U. (1998). NetLogo Bank Reserves model. - http://ccl.northwestern.edu/netlogo/models/BankReserves. - Center for Connected Learning and Computer-Based Modeling, - Northwestern University, Evanston, IL. -""" - -import mesa - -from bank_reserves.random_walk import RandomWalker - - -class Bank(mesa.Agent): - def __init__(self, unique_id, model, reserve_percent=50): - # initialize the parent class with required parameters - super().__init__(unique_id, model) - # for tracking total value of loans outstanding - self.bank_loans = 0 - """percent of deposits the bank must keep in reserves - this is set via - Slider in server.py""" - self.reserve_percent = reserve_percent - # for tracking total value of deposits - self.deposits = 0 - # total amount of deposits in reserve - self.reserves = (self.reserve_percent / 100) * self.deposits - # amount the bank is currently able to loan - self.bank_to_loan = 0 - - """update the bank's reserves and amount it can loan; - this is called every time a person balances their books - see below for Person.balance_books()""" - - def bank_balance(self): - self.reserves = (self.reserve_percent / 100) * self.deposits - self.bank_to_loan = self.deposits - (self.reserves + self.bank_loans) - - -# subclass of RandomWalker, which is subclass to Mesa Agent -class Person(RandomWalker): - def __init__(self, unique_id, pos, model, moore, bank, rich_threshold): - # init parent class with required parameters - super().__init__(unique_id, pos, model, moore=moore) - # the amount each person has in savings - self.savings = 0 - # total loan amount person has outstanding - self.loans = 0 - """start everyone off with a random amount in their wallet from 1 to a - user settable rich threshold amount""" - self.wallet = self.random.randint(1, rich_threshold + 1) - # savings minus loans, see balance_books() below - self.wealth = 0 - # person to trade with, see do_business() below - self.customer = 0 - # person's bank, set at __init__, all people have the same bank in this model - self.bank = bank - - def do_business(self): - """check if person has any savings, any money in wallet, or if the - bank can loan them any money""" - if self.savings > 0 or self.wallet > 0 or self.bank.bank_to_loan > 0: - # create list of people at my location (includes self) - my_cell = self.model.grid.get_cell_list_contents([self.pos]) - # check if other people are at my location - if len(my_cell) > 1: - # set customer to self for while loop condition - customer = self - while customer == self: - """select a random person from the people at my location - to trade with""" - customer = self.random.choice(my_cell) - # 50% chance of trading with customer - if self.random.randint(0, 1) == 0: - # 50% chance of trading $5 - if self.random.randint(0, 1) == 0: - # give customer $5 from my wallet (may result in negative wallet) - customer.wallet += 5 - self.wallet -= 5 - # 50% chance of trading $2 - else: - # give customer $2 from my wallet (may result in negative wallet) - customer.wallet += 2 - self.wallet -= 2 - - def balance_books(self): - # check if wallet is negative from trading with customer - if self.wallet < 0: - # if negative money in wallet, check if my savings can cover the balance - if self.savings >= (self.wallet * -1): - """if my savings can cover the balance, withdraw enough - money from my savings so that my wallet has a 0 balance""" - self.withdraw_from_savings(self.wallet * -1) - # if my savings cannot cover the negative balance of my wallet - else: - # check if i have any savings - if self.savings > 0: - """if i have savings, withdraw all of it to reduce my - negative balance in my wallet""" - self.withdraw_from_savings(self.savings) - # record how much money the bank can loan out right now - temp_loan = self.bank.bank_to_loan - """check if the bank can loan enough money to cover the - remaining negative balance in my wallet""" - if temp_loan >= (self.wallet * -1): - """if the bank can loan me enough money to cover - the remaining negative balance in my wallet, take out a - loan for the remaining negative balance""" - self.take_out_loan(self.wallet * -1) - else: - """if the bank cannot loan enough money to cover the negative - balance of my wallet, then take out a loan for the - total amount the bank can loan right now""" - self.take_out_loan(temp_loan) - else: - """if i have money in my wallet from trading with customer, deposit - it to my savings in the bank""" - self.deposit_to_savings(self.wallet) - # check if i have any outstanding loans, and if i have savings - if self.loans > 0 and self.savings > 0: - # check if my savings can cover my outstanding loans - if self.savings >= self.loans: - # payoff my loans with my savings - self.withdraw_from_savings(self.loans) - self.repay_a_loan(self.loans) - # if my savings won't cover my loans - else: - # pay off part of my loans with my savings - self.withdraw_from_savings(self.savings) - self.repay_a_loan(self.wallet) - # calculate my wealth - self.wealth = self.savings - self.loans - - # part of balance_books() - def deposit_to_savings(self, amount): - # take money from my wallet and put it in savings - self.wallet -= amount - self.savings += amount - # increase bank deposits - self.bank.deposits += amount - - # part of balance_books() - def withdraw_from_savings(self, amount): - # put money in my wallet from savings - self.wallet += amount - self.savings -= amount - # decrease bank deposits - self.bank.deposits -= amount - - # part of balance_books() - def repay_a_loan(self, amount): - # take money from my wallet to pay off all or part of a loan - self.loans -= amount - self.wallet -= amount - # increase the amount the bank can loan right now - self.bank.bank_to_loan += amount - # decrease the bank's outstanding loans - self.bank.bank_loans -= amount - - # part of balance_books() - def take_out_loan(self, amount): - """borrow from the bank to put money in my wallet, and increase my - outstanding loans""" - self.loans += amount - self.wallet += amount - # decresae the amount the bank can loan right now - self.bank.bank_to_loan -= amount - # increase the bank's outstanding loans - self.bank.bank_loans += amount - - # step is called for each agent in model.BankReservesModel.schedule.step() - def step(self): - # move to a cell in my Moore neighborhood - self.random_move() - # trade - self.do_business() - # deposit money or take out a loan - self.balance_books() - # update the bank's reserves and the amount it can loan right now - self.bank.bank_balance() diff --git a/examples/bank_reserves/bank_reserves/model.py b/examples/bank_reserves/bank_reserves/model.py deleted file mode 100644 index a27b1bdace6..00000000000 --- a/examples/bank_reserves/bank_reserves/model.py +++ /dev/null @@ -1,163 +0,0 @@ -""" -The following code was adapted from the Bank Reserves model included in Netlogo -Model information can be found at: http://ccl.northwestern.edu/netlogo/models/BankReserves -Accessed on: November 2, 2017 -Author of NetLogo code: - Wilensky, U. (1998). NetLogo Bank Reserves model. - http://ccl.northwestern.edu/netlogo/models/BankReserves. - Center for Connected Learning and Computer-Based Modeling, - Northwestern University, Evanston, IL. -""" - -import mesa -import numpy as np - -from bank_reserves.agents import Bank, Person - -""" -If you want to perform a parameter sweep, call batch_run.py instead of run.py. -For details see batch_run.py in the same directory as run.py. -""" - -# Start of datacollector functions - - -def get_num_rich_agents(model): - """return number of rich agents""" - - rich_agents = [a for a in model.schedule.agents if a.savings > model.rich_threshold] - return len(rich_agents) - - -def get_num_poor_agents(model): - """return number of poor agents""" - - poor_agents = [a for a in model.schedule.agents if a.loans > 10] - return len(poor_agents) - - -def get_num_mid_agents(model): - """return number of middle class agents""" - - mid_agents = [ - a - for a in model.schedule.agents - if a.loans < 10 and a.savings < model.rich_threshold - ] - return len(mid_agents) - - -def get_total_savings(model): - """sum of all agents' savings""" - - agent_savings = [a.savings for a in model.schedule.agents] - # return the sum of agents' savings - return np.sum(agent_savings) - - -def get_total_wallets(model): - """sum of amounts of all agents' wallets""" - - agent_wallets = [a.wallet for a in model.schedule.agents] - # return the sum of all agents' wallets - return np.sum(agent_wallets) - - -def get_total_money(model): - # sum of all agents' wallets - wallet_money = get_total_wallets(model) - # sum of all agents' savings - savings_money = get_total_savings(model) - # return sum of agents' wallets and savings for total money - return wallet_money + savings_money - - -def get_total_loans(model): - # list of amounts of all agents' loans - agent_loans = [a.loans for a in model.schedule.agents] - # return sum of all agents' loans - return np.sum(agent_loans) - - -class BankReserves(mesa.Model): - """ - This model is a Mesa implementation of the Bank Reserves model from NetLogo. - It is a highly abstracted, simplified model of an economy, with only one - type of agent and a single bank representing all banks in an economy. People - (represented by circles) move randomly within the grid. If two or more people - are on the same grid location, there is a 50% chance that they will trade with - each other. If they trade, there is an equal chance of giving the other agent - $5 or $2. A positive trade balance will be deposited in the bank as savings. - If trading results in a negative balance, the agent will try to withdraw from - its savings to cover the balance. If it does not have enough savings to cover - the negative balance, it will take out a loan from the bank to cover the - difference. The bank is required to keep a certain percentage of deposits as - reserves and the bank's ability to loan at any given time is a function of - the amount of deposits, its reserves, and its current total outstanding loan - amount. - """ - - # grid height - grid_h = 20 - # grid width - grid_w = 20 - - """init parameters "init_people", "rich_threshold", and "reserve_percent" - are all set via Slider""" - - def __init__( - self, - height=grid_h, - width=grid_w, - init_people=2, - rich_threshold=10, - reserve_percent=50, - ): - self.height = height - self.width = width - self.init_people = init_people - self.schedule = mesa.time.RandomActivation(self) - self.grid = mesa.space.MultiGrid(self.width, self.height, torus=True) - # rich_threshold is the amount of savings a person needs to be considered "rich" - self.rich_threshold = rich_threshold - self.reserve_percent = reserve_percent - # see datacollector functions above - self.datacollector = mesa.DataCollector( - model_reporters={ - "Rich": get_num_rich_agents, - "Poor": get_num_poor_agents, - "Middle Class": get_num_mid_agents, - "Savings": get_total_savings, - "Wallets": get_total_wallets, - "Money": get_total_money, - "Loans": get_total_loans, - }, - agent_reporters={"Wealth": lambda x: x.wealth}, - ) - - # create a single bank for the model - self.bank = Bank(1, self, self.reserve_percent) - - # create people for the model according to number of people set by user - for i in range(self.init_people): - # set x, y coords randomly within the grid - x = self.random.randrange(self.width) - y = self.random.randrange(self.height) - p = Person(i, (x, y), self, True, self.bank, self.rich_threshold) - # place the Person object on the grid at coordinates (x, y) - self.grid.place_agent(p, (x, y)) - # add the Person object to the model schedule - self.schedule.add(p) - - self.running = True - self.datacollector.collect(self) - - def step(self): - # tell all the agents in the model to run their step function - self.schedule.step() - # collect data - self.datacollector.collect(self) - - def run_model(self): - for i in range(self.run_time): - self.step() diff --git a/examples/bank_reserves/bank_reserves/random_walk.py b/examples/bank_reserves/bank_reserves/random_walk.py deleted file mode 100644 index 7e067881e4e..00000000000 --- a/examples/bank_reserves/bank_reserves/random_walk.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Citation: -The following code is a copy from random_walk.py at -https://github.com/projectmesa/mesa/blob/main/examples/wolf_sheep/wolf_sheep/random_walk.py -Accessed on: November 2, 2017 -Original Author: Jackie Kazil - -Generalized behavior for random walking, one grid cell at a time. -""" - -import mesa - - -class RandomWalker(mesa.Agent): - """ - Class implementing random walker methods in a generalized manner. - Not intended to be used on its own, but to inherit its methods to multiple - other agents. - """ - - grid = None - x = None - y = None - # use a Moore neighborhood - moore = True - - def __init__(self, unique_id, pos, model, moore=True): - """ - grid: The MultiGrid object in which the agent lives. - x: The agent's current x coordinate - y: The agent's current y coordinate - moore: If True, may move in all 8 directions. - Otherwise, only up, down, left, right. - """ - super().__init__(unique_id, model) - self.pos = pos - self.moore = moore - - def random_move(self): - """ - Step one cell in any allowable direction. - """ - # Pick the next cell from the adjacent cells. - next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True) - next_move = self.random.choice(next_moves) - # Now move: - self.model.grid.move_agent(self, next_move) diff --git a/examples/bank_reserves/bank_reserves/server.py b/examples/bank_reserves/bank_reserves/server.py deleted file mode 100644 index c66cd920b9c..00000000000 --- a/examples/bank_reserves/bank_reserves/server.py +++ /dev/null @@ -1,90 +0,0 @@ -import mesa - -from bank_reserves.agents import Person -from bank_reserves.model import BankReserves - -""" -Citation: -The following code was adapted from server.py at -https://github.com/projectmesa/mesa/blob/main/examples/wolf_sheep/wolf_sheep/server.py -Accessed on: November 2, 2017 -Author of original code: Taylor Mutch -""" - -# The colors here are taken from Matplotlib's tab10 palette -# Green -RICH_COLOR = "#2ca02c" -# Red -POOR_COLOR = "#d62728" -# Blue -MID_COLOR = "#1f77b4" - - -def person_portrayal(agent): - if agent is None: - return - - portrayal = {} - - # update portrayal characteristics for each Person object - if isinstance(agent, Person): - portrayal["Shape"] = "circle" - portrayal["r"] = 0.5 - portrayal["Layer"] = 0 - portrayal["Filled"] = "true" - - color = MID_COLOR - - # set agent color based on savings and loans - if agent.savings > agent.model.rich_threshold: - color = RICH_COLOR - if agent.savings < 10 and agent.loans < 10: - color = MID_COLOR - if agent.loans > 10: - color = POOR_COLOR - - portrayal["Color"] = color - - return portrayal - - -# dictionary of user settable parameters - these map to the model __init__ parameters -model_params = { - "init_people": mesa.visualization.Slider( - "People", 25, 1, 200, description="Initial Number of People" - ), - "rich_threshold": mesa.visualization.Slider( - "Rich Threshold", - 10, - 1, - 20, - description="Upper End of Random Initial Wallet Amount", - ), - "reserve_percent": mesa.visualization.Slider( - "Reserves", - 50, - 1, - 100, - description="Percent of deposits the bank has to hold in reserve", - ), -} - -# set the portrayal function and size of the canvas for visualization -canvas_element = mesa.visualization.CanvasGrid(person_portrayal, 20, 20, 500, 500) - -# map data to chart in the ChartModule -chart_element = mesa.visualization.ChartModule( - [ - {"Label": "Rich", "Color": RICH_COLOR}, - {"Label": "Poor", "Color": POOR_COLOR}, - {"Label": "Middle Class", "Color": MID_COLOR}, - ] -) - -# create instance of Mesa ModularServer -server = mesa.visualization.ModularServer( - BankReserves, - [canvas_element, chart_element], - "Bank Reserves Model", - model_params=model_params, -) diff --git a/examples/bank_reserves/batch_run.py b/examples/bank_reserves/batch_run.py deleted file mode 100644 index 768fe1876b8..00000000000 --- a/examples/bank_reserves/batch_run.py +++ /dev/null @@ -1,221 +0,0 @@ -""" -The following code was adapted from the Bank Reserves model included in Netlogo -Model information can be found at: http://ccl.northwestern.edu/netlogo/models/BankReserves -Accessed on: November 2, 2017 -Author of NetLogo code: - Wilensky, U. (1998). NetLogo Bank Reserves model. - http://ccl.northwestern.edu/netlogo/models/BankReserves. - Center for Connected Learning and Computer-Based Modeling, - Northwestern University, Evanston, IL. - -This version of the model has a BatchRunner at the bottom. This -is for collecting data on parameter sweeps. It is not meant to -be run with run.py, since run.py starts up a server for visualization, which -isn't necessary for the BatchRunner. To run a parameter sweep, call -batch_run.py in the command line. - -The BatchRunner is set up to collect step by step data of the model. It does -this by collecting the DataCollector object in a model_reporter (i.e. the -DataCollector is collecting itself every step). - -The end result of the batch run will be a CSV file created in the same -directory from which Python was run. The CSV file will contain the data from -every step of every run. -""" - -import itertools - -import mesa -import numpy as np -import pandas as pd - -from bank_reserves.agents import Bank, Person - -# Start of datacollector functions - - -def get_num_rich_agents(model): - """list of rich agents""" - - rich_agents = [a for a in model.schedule.agents if a.savings > model.rich_threshold] - # return number of rich agents - return len(rich_agents) - - -def get_num_poor_agents(model): - """list of poor agents""" - - poor_agents = [a for a in model.schedule.agents if a.loans > 10] - # return number of poor agents - return len(poor_agents) - - -def get_num_mid_agents(model): - """list of middle class agents""" - - mid_agents = [ - a - for a in model.schedule.agents - if a.loans < 10 and a.savings < model.rich_threshold - ] - # return number of middle class agents - return len(mid_agents) - - -def get_total_savings(model): - """list of amounts of all agents' savings""" - - agent_savings = [a.savings for a in model.schedule.agents] - # return the sum of agents' savings - return np.sum(agent_savings) - - -def get_total_wallets(model): - """list of amounts of all agents' wallets""" - - agent_wallets = [a.wallet for a in model.schedule.agents] - # return the sum of all agents' wallets - return np.sum(agent_wallets) - - -def get_total_money(model): - """sum of all agents' wallets""" - - wallet_money = get_total_wallets(model) - # sum of all agents' savings - savings_money = get_total_savings(model) - # return sum of agents' wallets and savings for total money - return wallet_money + savings_money - - -def get_total_loans(model): - """list of amounts of all agents' loans""" - - agent_loans = [a.loans for a in model.schedule.agents] - # return sum of all agents' loans - return np.sum(agent_loans) - - -def track_params(model): - return (model.init_people, model.rich_threshold, model.reserve_percent) - - -def track_run(model): - return model.uid - - -class BankReservesModel(mesa.Model): - # id generator to track run number in batch run data - id_gen = itertools.count(1) - - # grid height - grid_h = 20 - # grid width - grid_w = 20 - - """init parameters "init_people", "rich_threshold", and "reserve_percent" - are all set via Slider""" - - def __init__( - self, - height=grid_h, - width=grid_w, - init_people=2, - rich_threshold=10, - reserve_percent=50, - ): - self.uid = next(self.id_gen) - self.height = height - self.width = width - self.init_people = init_people - self.schedule = mesa.time.RandomActivation(self) - self.grid = mesa.space.MultiGrid(self.width, self.height, torus=True) - # rich_threshold is the amount of savings a person needs to be considered "rich" - self.rich_threshold = rich_threshold - self.reserve_percent = reserve_percent - # see datacollector functions above - self.datacollector = mesa.DataCollector( - model_reporters={ - "Rich": get_num_rich_agents, - "Poor": get_num_poor_agents, - "Middle Class": get_num_mid_agents, - "Savings": get_total_savings, - "Wallets": get_total_wallets, - "Money": get_total_money, - "Loans": get_total_loans, - "Model Params": track_params, - "Run": track_run, - }, - agent_reporters={"Wealth": "wealth"}, - ) - - # create a single bank for the model - self.bank = Bank(1, self, self.reserve_percent) - - # create people for the model according to number of people set by user - for i in range(self.init_people): - # set x coordinate as a random number within the width of the grid - x = self.random.randrange(self.width) - # set y coordinate as a random number within the height of the grid - y = self.random.randrange(self.height) - p = Person(i, (x, y), self, True, self.bank, self.rich_threshold) - # place the Person object on the grid at coordinates (x, y) - self.grid.place_agent(p, (x, y)) - # add the Person object to the model schedule - self.schedule.add(p) - - self.running = True - - def step(self): - # collect data - self.datacollector.collect(self) - # tell all the agents in the model to run their step function - self.schedule.step() - - def run_model(self): - for i in range(self.run_time): - self.step() - - -# parameter lists for each parameter to be tested in batch run -br_params = { - "init_people": [25, 100], - "rich_threshold": [5, 10], - "reserve_percent": 5, -} - -if __name__ == "__main__": - data = mesa.batch_run( - BankReservesModel, - br_params, - ) - br_df = pd.DataFrame(data) - br_df.to_csv("BankReservesModel_Data.csv") - - # The commented out code below is the equivalent code as above, but done - # via the legacy BatchRunner class. This is a good example to look at if - # you want to migrate your code to use `batch_run()` from `BatchRunner`. - # Things to note: - # - You have to set "reserve_percent" in br_params to `[5]`, because the - # legacy BatchRunner doesn't auto-detect that it is single-valued. - # - The model reporters need to be explicitly specified in the legacy - # BatchRunner - """ - from mesa.batchrunner import BatchRunnerMP - br = BatchRunnerMP( - BankReservesModel, - nr_processes=2, - variable_parameters=br_params, - iterations=2, - max_steps=1000, - model_reporters={"Data Collector": lambda m: m.datacollector}, - ) - br.run_all() - br_df = br.get_model_vars_dataframe() - br_step_data = pd.DataFrame() - for i in range(len(br_df["Data Collector"])): - if isinstance(br_df["Data Collector"][i], DataCollector): - i_run_data = br_df["Data Collector"][i].get_model_vars_dataframe() - br_step_data = br_step_data.append(i_run_data, ignore_index=True) - br_step_data.to_csv("BankReservesModel_Step_Data.csv") - """ diff --git a/examples/bank_reserves/requirements.txt b/examples/bank_reserves/requirements.txt deleted file mode 100644 index 90169c50035..00000000000 --- a/examples/bank_reserves/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -itertools -mesa -numpy -pandas diff --git a/examples/bank_reserves/run.py b/examples/bank_reserves/run.py deleted file mode 100644 index d6cf2ec0268..00000000000 --- a/examples/bank_reserves/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from bank_reserves.server import server - -server.launch() diff --git a/examples/boid_flockers/Flocker Test.ipynb b/examples/boid_flockers/Flocker Test.ipynb deleted file mode 100644 index 664019e51fc..00000000000 --- a/examples/boid_flockers/Flocker Test.ipynb +++ /dev/null @@ -1,114 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "from boid_flockers.model import BoidFlockers\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "%matplotlib inline" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "def draw_boids(model):\n", - " x_vals = []\n", - " y_vals = []\n", - " for boid in model.schedule.agents:\n", - " x, y = boid.pos\n", - " x_vals.append(x)\n", - " y_vals.append(y)\n", - " fig = plt.figure(figsize=(10, 10))\n", - " ax = fig.add_subplot(111)\n", - " ax.scatter(x_vals, y_vals)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "model = BoidFlockers(100, 100, 100, speed=5, vision=5, separation=1)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "for i in range(50):\n", - " model.step()" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlwAAAJPCAYAAACpXgqFAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3W+snNd9H/jvT1LMUnEVmQwg+Y9iB22MxEbqVt1N04Kt\nuGtLVI3WirCA0wAu1LTJInAXN1rSrSUnqPUi68ZuyPVqF4bRJnaJoPZWTaPYKdwV2TRMs9ggzsZx\n7Ur22img1rIhuiHtMHEU1TbPvpih7tXVveS9d+bcZ56ZzwcYaJ5n5rlz9PDOne+c8zvnqdZaAADo\n57qhGwAAsOwELgCAzgQuAIDOBC4AgM4ELgCAzgQuAIDOdhS4quoDVXW+qj69Yd8/qqrPVNW/r6pf\nrKpv2/DYg1X1+ar6bFXd1aPhAABjsdMerg8muXvTvjNJXttae12SzyV5MEmq6jVJfjDJa6bHvK+q\n9KQBACtrR0GotfbrSb6yad/Z1trl6eZvJnnF9P49ST7cWvt6a+3JJL+b5Pvm01wAgPGZV8/T307y\nsen9lyV5asNjTyV5+ZxeBwBgdGYOXFX1E0n+a2vtQ1d5musHAQAr64ZZDq6qv5XkjUlev2H3F5Pc\ntmH7FdN9m48VwgCA0Wit1V6P3XPgqqq7k/y9JHe01v54w0MfTfKhqjqVyVDidyX5+FY/Y5aGr7qq\neqi19tDQ7Rgr5282zt/eOXezcf5m4/zt3awdRTsKXFX14SR3JPn2qvpCkndmMivxRUnOVlWS/EZr\n7a2ttSeq6pEkTyT5RpK3ttb0ZgEAK2tHgau19kNb7P7AVZ7/riTv2mujAACWifWxxuvc0A0YuXND\nN2Dkzg3dgBE7N3QDRu7c0A0YuXNDN2BV1VCjfVXV1HABAGMwa27RwwUA0JnABQDQmcAFANCZwAUA\n0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZ\nwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAF\nANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQ\nmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnA\nBQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA\n0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANDZjgJXVX2gqs5X1ac37DtU\nVWer6nNVdaaqbt7w2INV9fmq+mxV3dWj4QAAY7HTHq4PJrl7074Hkpxtrb06ya9Mt1NVr0nyg0le\nMz3mfVWlJw0AWFk7CkKttV9P8pVNu9+U5PT0/ukkPzC9f0+SD7fWvt5aezLJ7yb5vtmbCgAwTrP0\nPN3SWjs/vX8+yS3T+y9L8tSG5z2V5OUzvA4AwKjNZaivtdaStKs9ZR6vAwAwRjfMcOz5qrq1tfZ0\nVb00yZen+7+Y5LYNz3vFdN8LVNVDGzbPtdbOzdAedqiqjiWHTky2Lp5srT02bIsAYLFU1dEkR+f2\n8yadUzt64Vcl+eXW2vdOt9+T5EJr7d1V9UCSm1trD0yL5j+USd3Wy5P8myR/um16oapqrbWa1/8I\nOzMJWzc9mjx8cLJn7XLyzU8mX3uH4AUAW5s1t+yoh6uqPpzkjiTfXlVfSPIPkvx0kkeq6u8keTLJ\nm5OktfZEVT2S5Ikk30jy1s1hiyEdOpGcOpjcd2XHdcn7b08+9ZGqlzyeXHdBrxcAzNeOAldr7Ye2\neegN2zz/XUnetddGsd+uT3LjgeRnbp9srx2pqnuFLgCYj1lquBiliyeTtSNJpkOKb0/y3Ul+Jht6\nvQ4mx08kEbgAYA4sSLpiJr1Wl+5N7v9Ecv/l5C1Jnh26WQCw1HZcND/3F1Y0P7j12YrPHk6uf23y\n8IHJI2vPJJcMKQLA1Ky5ReAiiaUiAObJ39TlI3CxLW94gP23xfI7Rg2WwL4sC8H4rL/hT115w5t5\nCLAvXrD8jolICFzLyxseABaFwAUAc7V5+Z21Z5JLJwdtEoNTw7WkJkOKN34k+TPTmYefejb5o3sM\nKQL0p4Z2+ajh4ipuSPJj0/trQzYEYKVMA5aQxXMEriXy/G9UNx9O3ntgQw3XATVcADAMgWtJvHBW\n4v2Xh20RAHCFwLU0Ns9K/PR1ydrlPHf5JkWbADAUgWtpfW+Sb34yOX5hsn1J0SYADMQsxSWx15WN\nJ8fd/K7kulcmz/6n5GvvmDxidg0AXOHSPjxnt9OQpyHtI+sXrX5bkj/4enLgsgtZA8A6y0LwnN1P\nQz50Ijm1cSZjkp/8luSnYoV6AJif64ZuAADAstPDtdIunkzW/kqSTUOKa5fX95ndCACzUsO1gjbV\nep1Lbv4fFM0DwPYUzbMre53NCACrbNbcooZrhKrqWNXhM5NbHdvd0YdOTMLWfZncHj643psFAPSg\nhmtkXngJn7UjVbWLHqrLh/u1DgDYisA1Opsv4bPzZRsmYe3G106K469Ye1ZRPAD0JXCtlCvrbt2a\n5B8n+VKSbz6ufgsA+hK4RufiyWTtSJKNRe+77KE6Nr2dzvq1FgGAXsxSHKHdXsLn+ceZoQgAu2VZ\nCHZlr2ENAFaZwAUA0Jl1uAAAFpzAtQJmWygVAJiVIcUlp1AeAGY3a26xLMTS2/tCqQDAfBhSBADo\nTA/X0pvHQqkAwCzUcK0Aa28BwGyswwUA0Jl1uAAAFpzABQDQmcAFANCZwAUA0JnABQDQmcAFANCZ\nwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAF\nANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0JnABQDQ\nmcAFANCZwAUA0JnABQDQmcAFANCZwAUA0NnMgauqHqyqx6vq01X1oao6UFWHqupsVX2uqs5U1c3z\naCwAzENVHas6fGZyq2NDt4flV621vR9c9aok/zbJ97TWnq2qf57kY0lem+T3Wmvvqaq3J3lJa+2B\nTce21lrt+cUBWGmToHToxGTr4snW2mM7P+6mR5OHD072rD2TXLp3p8ezmmbNLTfM+PqXknw9yY1V\n9c0kNyb5UpIHk9wxfc7pJOeSPLDVDwCA3VoPTaeuhKYjVbXD0HToxOS4+67sOJgcP5FE4KKbmYYU\nW2sXk5xM8p8zCVpfba2dTXJLa+389Gnnk9wyUysB4HkOnZj0UN2Xye3hg+u9XbB4Zurhqqo/leT+\nJK9K8vtJ/kVVvWXjc1prraq2HLesqoc2bJ5rrZ2bpT0AcG0XTyZrR5JsHFI8OWiTWDhVdTTJ0bn9\nvBlruH4wyZ2ttR+Zbv/NJN+f5L9P8t+11p6uqpcm+dXW2ndvOlYNFwB7Mmsd1l7rv1hds+aWWQPX\n65L8syT/bZI/TvJPk3w8ySuTXGitvbuqHkhys6J5AOZJaGI/DRq4pg34+5kMoF9O8okkP5LkTyZ5\n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\nMinr9tba9ya5PsnfiHN3NR/M5LNhoy3Pl8/cLW11/uaWWfb95FowdWbfl+R3W2tPtta+nuT/zOTc\nsYXW2tOttU9O7/9hks9kMpnjTUlOT592OskPDNPCxVdVr0jyxiQ/m+TKDB3nbwem34b/cmvtA0nS\nWvtGa+334/ztxKVMvjDdWFU3JLkxyZfi3G2rtfbrSb6yafd258tn7iZbnb95Zpah06wFU3fv5Um+\nsGHbedqh6TfmP5fJm+aW1tr56UPnk9wyULPG4H9N8veSXN6wz/nbme9M8l+q6oNV9Ymq+idV9a1x\n/q6ptXYxk5nw/zmToPXV1trZOHe7td358pm7ezNlli6Bazpe/Oktbn99w3MsmLo3zskeVNWLk/zL\nJD/eWvuDjY9NZ9E6r1uoqr+W5Muttd/Jeu/W8zh/V3VDktuTvK+1dnuSr2XTEJjzt7Wq+lNJ7k/y\nqkw+3F5cVW/Z+Bznbnd2cL6cy23MI7PMsizE9q/Y2p1Xe3y6YOobk7x+w+4vJrltw/Yrpvt4vs3n\n6bY8P2WzSVV9SyZh6+dba7803X2+qm5trT1dVS9N8uXhWrjQ/lKSN1XVG5P8iSQ3VdXPx/nbqaeS\nPNVa+63p9i9kUgPytPN3Tf9Nkv+ntXYhSarqF5P8xTh3u7Xde9Vn7g7NK7MMMUvx7kyGJ+65sjr9\n1EeT/I2qelFVfWeS70ry8f1u3wj8v0m+q6peVVUvyqRo76MDt2lhVVUl+bkkT7TW3rvhoY8muW96\n/74kv7T5WJLW2jtaa7e11r4zk4Llf9ta+5tx/naktfZ0ki9U1aunu96Q5PEkvxzn71o+m+T7q+rg\n9H38hkwmbjh3u7Pde9Vn7g7MM7Ps+zpcVfX5TBZMvTjd9RuttbdOH3tHJmOk38hk6OexfW3cSFTV\nX03y3kxm7fxca+0fDtykhVVVR5L8uySfynp374OZvDEeSfIdSZ5M8ubW2leHaONYVNUdSU601t5U\nVYfi/O1IVb0ukwkHL0ryH5P8cCbvXefvGqrq72cSEi4n+USSH0nyJ+PcbamqPpzkjiTfnkm91j9I\n8pFsc7585j7fFufvnZl8Xswls1j4FACgs6FnKQIALD2BCwCgM4ELAKAzgQsAoDOBCwCgM4ELAKAz\ngQsAoDOBCwCgs/8fICoqGcqtXKgAAAAASUVORK5CYII=\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "draw_boids(model)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.4.2" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/boid_flockers/Readme.md b/examples/boid_flockers/Readme.md deleted file mode 100644 index cb3292b4f68..00000000000 --- a/examples/boid_flockers/Readme.md +++ /dev/null @@ -1,34 +0,0 @@ -# Flockers - -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. - -## How to Run - -Launch the model: -``` - $ python Flocker_Server.py -``` - -Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. - -## Files - -* [flockers/model.py](flockers/model.py): Core model file; contains the BoidModel class. -* [flockers/boid.py](flockers/boid.py): The Boid agent class. -* [flockers/SimpleContinuousModule.py](flockers/SimpleContinuousModule.py): Defines ``SimpleCanvas``, the Python side of a custom visualization module for drawing agents with continuous positions. -* [flockers/simple_continuous_canvas.js](flockers/simple_continuous_canvas.js): JavaScript side of the ``SimpleCanvas`` visualization module; takes the output generated by the Python ``SimpleCanvas`` element and draws it in the browser window via HTML5 canvas. -* [flockers/server.py](flockers/server.py): Sets up the visualization; uses the SimpleCanvas element defined above -* [run.py](run.py) Launches the visualization. -* [Flocker Test.ipynb](Flocker Test.ipynb): Tests the model in a Jupyter notebook. - -## Further Reading - -======= -* Launch the visualization -``` -$ mesa runserver -``` -* Visit your browser: http://127.0.0.1:8521/ -* In your browser hit *run* diff --git a/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py b/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py deleted file mode 100644 index 3f3da5dd01e..00000000000 --- a/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py +++ /dev/null @@ -1,32 +0,0 @@ -import mesa - - -class SimpleCanvas(mesa.visualization.VisualizationElement): - local_includes = ["boid_flockers/simple_continuous_canvas.js"] - portrayal_method = None - canvas_height = 500 - canvas_width = 500 - - def __init__(self, portrayal_method, canvas_height=500, canvas_width=500): - """ - Instantiate a new SimpleCanvas - """ - self.portrayal_method = portrayal_method - self.canvas_height = canvas_height - self.canvas_width = canvas_width - new_element = "new Simple_Continuous_Module({}, {})".format( - self.canvas_width, self.canvas_height - ) - self.js_code = "elements.push(" + new_element + ");" - - def render(self, model): - space_state = [] - for obj in model.schedule.agents: - portrayal = self.portrayal_method(obj) - x, y = obj.pos - x = (x - model.space.x_min) / (model.space.x_max - model.space.x_min) - y = (y - model.space.y_min) / (model.space.y_max - model.space.y_min) - portrayal["x"] = x - portrayal["y"] = y - space_state.append(portrayal) - return space_state diff --git a/examples/boid_flockers/boid_flockers/boid.py b/examples/boid_flockers/boid_flockers/boid.py deleted file mode 100644 index d8f45222650..00000000000 --- a/examples/boid_flockers/boid_flockers/boid.py +++ /dev/null @@ -1,105 +0,0 @@ -import mesa -import numpy as np - - -class Boid(mesa.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 velocity (a vector) - define their movement. Separation is their desired minimum distance from - any other Boid. - """ - - def __init__( - self, - unique_id, - model, - pos, - speed, - velocity, - vision, - separation, - cohere=0.025, - separate=0.25, - match=0.04, - ): - """ - Create a new Boid flocker agent. - - Args: - unique_id: Unique agent identifyer. - pos: Starting position - speed: Distance to move per step. - heading: 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__(unique_id, model) - self.pos = np.array(pos) - self.speed = speed - self.velocity = velocity - self.vision = vision - self.separation = separation - self.cohere_factor = cohere - self.separate_factor = separate - self.match_factor = match - - def cohere(self, neighbors): - """ - Return the vector toward the center of mass of the local neighbors. - """ - cohere = np.zeros(2) - if neighbors: - for neighbor in neighbors: - cohere += self.model.space.get_heading(self.pos, neighbor.pos) - cohere /= len(neighbors) - return cohere - - def separate(self, neighbors): - """ - Return a vector away from any neighbors closer than separation dist. - """ - me = self.pos - them = (n.pos for n in neighbors) - separation_vector = np.zeros(2) - for other in them: - if self.model.space.get_distance(me, other) < self.separation: - separation_vector -= self.model.space.get_heading(me, other) - return separation_vector - - def match_heading(self, neighbors): - """ - Return a vector of the neighbors' average heading. - """ - match_vector = np.zeros(2) - if neighbors: - for neighbor in neighbors: - match_vector += neighbor.velocity - match_vector /= len(neighbors) - return match_vector - - def step(self): - """ - Get the Boid's neighbors, compute the new vector, and move accordingly. - """ - - neighbors = self.model.space.get_neighbors(self.pos, self.vision, False) - self.velocity += ( - self.cohere(neighbors) * self.cohere_factor - + self.separate(neighbors) * self.separate_factor - + self.match_heading(neighbors) * self.match_factor - ) / 2 - self.velocity /= np.linalg.norm(self.velocity) - new_pos = self.pos + self.velocity * self.speed - self.model.space.move_agent(self, new_pos) diff --git a/examples/boid_flockers/boid_flockers/model.py b/examples/boid_flockers/boid_flockers/model.py deleted file mode 100644 index 00a08d765d5..00000000000 --- a/examples/boid_flockers/boid_flockers/model.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Flockers -============================================================= -A Mesa implementation of Craig Reynolds's Boids flocker model. -Uses numpy arrays to represent vectors. -""" - -import mesa -import numpy as np - -from .boid import Boid - - -class BoidFlockers(mesa.Model): - """ - Flocker model class. Handles agent creation, placement and scheduling. - """ - - def __init__( - self, - population=100, - width=100, - height=100, - speed=1, - vision=10, - separation=2, - cohere=0.025, - separate=0.25, - match=0.04, - ): - """ - 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.""" - self.population = population - self.vision = vision - self.speed = speed - self.separation = separation - self.schedule = mesa.time.RandomActivation(self) - self.space = mesa.space.ContinuousSpace(width, height, True) - self.factors = dict(cohere=cohere, separate=separate, match=match) - self.make_agents() - self.running = True - - def make_agents(self): - """ - Create self.population agents, with random positions and starting headings. - """ - for i 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)) - velocity = np.random.random(2) * 2 - 1 - boid = Boid( - i, - self, - pos, - self.speed, - velocity, - self.vision, - self.separation, - **self.factors - ) - self.space.place_agent(boid, pos) - self.schedule.add(boid) - - def step(self): - self.schedule.step() diff --git a/examples/boid_flockers/boid_flockers/server.py b/examples/boid_flockers/boid_flockers/server.py deleted file mode 100644 index 4906df699c7..00000000000 --- a/examples/boid_flockers/boid_flockers/server.py +++ /dev/null @@ -1,23 +0,0 @@ -import mesa - -from .model import BoidFlockers -from .SimpleContinuousModule import SimpleCanvas - - -def boid_draw(agent): - return {"Shape": "circle", "r": 2, "Filled": "true", "Color": "Red"} - - -boid_canvas = SimpleCanvas(boid_draw, 500, 500) -model_params = { - "population": 100, - "width": 100, - "height": 100, - "speed": 5, - "vision": 10, - "separation": 2, -} - -server = mesa.visualization.ModularServer( - BoidFlockers, [boid_canvas], "Boids", model_params -) diff --git a/examples/boid_flockers/boid_flockers/simple_continuous_canvas.js b/examples/boid_flockers/boid_flockers/simple_continuous_canvas.js deleted file mode 100644 index 20c0ded8732..00000000000 --- a/examples/boid_flockers/boid_flockers/simple_continuous_canvas.js +++ /dev/null @@ -1,79 +0,0 @@ -const ContinuousVisualization = function(width, height, context) { - this.draw = function(objects) { - for (const p of objects) { - if (p.Shape == "rect") - this.drawRectange(p.x, p.y, p.w, p.h, p.Color, p.Filled); - if (p.Shape == "circle") - this.drawCircle(p.x, p.y, p.r, p.Color, p.Filled); - }; - - }; - - this.drawCircle = function(x, y, radius, color, fill) { - const cx = x * width; - const cy = y * height; - const r = radius; - - context.beginPath(); - context.arc(cx, cy, r, 0, Math.PI * 2, false); - context.closePath(); - - context.strokeStyle = color; - context.stroke(); - - if (fill) { - context.fillStyle = color; - context.fill(); - } - - }; - - this.drawRectange = function(x, y, w, h, color, fill) { - context.beginPath(); - const dx = w * width; - const dy = h * height; - - // Keep the drawing centered: - const x0 = (x*width) - 0.5*dx; - const y0 = (y*height) - 0.5*dy; - - context.strokeStyle = color; - context.fillStyle = color; - if (fill) - context.fillRect(x0, y0, dx, dy); - else - context.strokeRect(x0, y0, dx, dy); - }; - - this.resetCanvas = function() { - context.clearRect(0, 0, width, height); - context.beginPath(); - }; -}; - -const Simple_Continuous_Module = function(canvas_width, canvas_height) { - // Create the element - // ------------------ - - const canvas = document.createElement("canvas"); - Object.assign(canvas, { - width: canvas_width, - height: canvas_height, - style: 'border:1px dotted' - }); - // Append it to body: - document.getElementById("elements").appendChild(canvas); - - // Create the context and the drawing controller: - const context = canvas.getContext("2d"); - const canvasDraw = new ContinuousVisualization(canvas_width, canvas_height, context); - - this.render = function(data) { - canvasDraw.resetCanvas(); - canvasDraw.draw(data); - }; - - this.reset = function() { - canvasDraw.resetCanvas(); - }; -}; diff --git a/examples/boid_flockers/requirements.txt b/examples/boid_flockers/requirements.txt deleted file mode 100644 index bcbfbbe220b..00000000000 --- a/examples/boid_flockers/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -jupyter -matplotlib -mesa diff --git a/examples/boid_flockers/run.py b/examples/boid_flockers/run.py deleted file mode 100644 index be0c1c75c58..00000000000 --- a/examples/boid_flockers/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from boid_flockers.server import server - -server.launch() diff --git a/examples/boltzmann_wealth_model/Readme.md b/examples/boltzmann_wealth_model/Readme.md deleted file mode 100644 index d8f96dcabd4..00000000000 --- a/examples/boltzmann_wealth_model/Readme.md +++ /dev/null @@ -1,39 +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](http://mesa.readthedocs.io/en/latest/intro-tutorial.html). - -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 examples, launch the Jupyter Notebook and run the code in ``Introduction to Mesa Tutorial Code.ipynb``. - -To launch the interactive server, as described in the [last section of the tutorial](http://mesa.readthedocs.io/en/latest/intro-tutorial.html#adding-visualization), run: - -``` - $ python viz_money_model.py -``` - -If your browser doesn't open automatically, point it to [http://127.0.0.1:8521/](http://127.0.0.1:8521/). When the visualization loads, press Reset, then Run. - - -## Files - -* ``Introduction to Mesa Tutorial Code.ipynb``: Jupyter Notebook with all the steps as described in the tutorial. -* ``money_model.py``: Final version of the model. -* ``viz_money_model.py``: Creates and launches interactive visualization. - -## Further Reading - -The full tutorial describing how the model is built can be found at: -http://mesa.readthedocs.io/en/latest/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) -____ -You will need to open the file as a Jupyter (aka iPython) notebook with an iPython 3 kernel. Required dependencies are listed in the provided `requirements.txt` file which can be installed by running `pip install -r requirements.txt` diff --git a/examples/boltzmann_wealth_model/boltzmann_wealth_model/__init__.py b/examples/boltzmann_wealth_model/boltzmann_wealth_model/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py b/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py deleted file mode 100644 index a49546ce741..00000000000 --- a/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py +++ /dev/null @@ -1,40 +0,0 @@ -import mesa - -from .model import BoltzmannWealthModel - - -def agent_portrayal(agent): - portrayal = {"Shape": "circle", "Filled": "true", "r": 0.5} - - if agent.wealth > 0: - portrayal["Color"] = "red" - portrayal["Layer"] = 0 - else: - portrayal["Color"] = "grey" - portrayal["Layer"] = 1 - portrayal["r"] = 0.2 - return portrayal - - -grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500) -chart = mesa.visualization.ChartModule( - [{"Label": "Gini", "Color": "#0000FF"}], data_collector_name="datacollector" -) - -model_params = { - "N": mesa.visualization.Slider( - "Number of agents", - 100, - 2, - 200, - 1, - description="Choose how many agents to include in the model", - ), - "width": 10, - "height": 10, -} - -server = mesa.visualization.ModularServer( - BoltzmannWealthModel, [grid, chart], "Money Model", model_params -) -server.port = 8521 diff --git a/examples/boltzmann_wealth_model/requirements.txt b/examples/boltzmann_wealth_model/requirements.txt deleted file mode 100644 index 23603b7348c..00000000000 --- a/examples/boltzmann_wealth_model/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -jupyter -matplotlib -mesa -numpy diff --git a/examples/boltzmann_wealth_model/run.py b/examples/boltzmann_wealth_model/run.py deleted file mode 100644 index ea57809eb0a..00000000000 --- a/examples/boltzmann_wealth_model/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from boltzmann_wealth_model.server import server - -server.launch() diff --git a/examples/boltzmann_wealth_model_network/README.md b/examples/boltzmann_wealth_model_network/README.md deleted file mode 100644 index 8a33d096e06..00000000000 --- a/examples/boltzmann_wealth_model_network/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Boltzmann Wealth Model with Network - -## Summary - -This is the same Boltzmann Wealth Model, but with a network grid implementation. - -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](http://mesa.readthedocs.io/en/latest/intro-tutorial.html). - -In this network implementation, agents must be located on a node, with a limit of one agent per node. In order to give or receive the unit of money, the agent must be directly connected to the other agent (there must be a direct link between the nodes). - -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. - -JavaScript library used in this example to render the network: [sigma.js](http://sigmajs.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. - -## Files - -* ``run.py``: Launches a model visualization server. -* ``model.py``: Contains the agent class, and the overall model class. -* ``server.py``: Defines classes for visualizing the model (network layout) in the browser via Mesa's modular server, and instantiates a visualization server. - -## Further Reading - -The full tutorial describing how the model is built can be found at: -http://mesa.readthedocs.io/en/master/tutorials/intro_tutorial.html - -This model is drawn from econophysics and presents a statistical mechanics approach to wealth distribution. Some examples of further reading on the topic can be found at: - -[Milakovic, M. A Statistical Equilibrium Model of Wealth Distribution. February, 2001.](https://editorialexpress.com/cgi-bin/conference/download.cgi?db_name=SCE2001&paper_id=214) - -[Dragulescu, A and Yakovenko, V. Statistical Mechanics of Money, Income, and Wealth: A Short Survey. November, 2002](http://arxiv.org/pdf/cond-mat/0211175v1.pdf) diff --git a/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/__init__.py b/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/model.py b/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/model.py deleted file mode 100644 index 181daec4e45..00000000000 --- a/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/model.py +++ /dev/null @@ -1,79 +0,0 @@ -import mesa -import networkx as nx - - -def compute_gini(model): - agent_wealths = [agent.wealth for agent in model.schedule.agents] - x = sorted(agent_wealths) - N = model.num_agents - B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x)) - return 1 + (1 / N) - 2 * B - - -class BoltzmannWealthModelNetwork(mesa.Model): - """A model with some number of agents.""" - - def __init__(self, num_agents=7, num_nodes=10): - - self.num_agents = num_agents - self.num_nodes = num_nodes if num_nodes >= self.num_agents else self.num_agents - self.G = nx.erdos_renyi_graph(n=self.num_nodes, p=0.5) - self.grid = mesa.space.NetworkGrid(self.G) - self.schedule = mesa.time.RandomActivation(self) - self.datacollector = mesa.DataCollector( - model_reporters={"Gini": compute_gini}, - agent_reporters={"Wealth": lambda _: _.wealth}, - ) - - list_of_random_nodes = self.random.sample(list(self.G), self.num_agents) - - # Create agents - for i in range(self.num_agents): - a = MoneyAgent(i, self) - self.schedule.add(a) - # Add the agent to a random node - self.grid.place_agent(a, list_of_random_nodes[i]) - - self.running = True - self.datacollector.collect(self) - - def step(self): - self.schedule.step() - # collect data - self.datacollector.collect(self) - - def run_model(self, n): - for i in range(n): - self.step() - - -class MoneyAgent(mesa.Agent): - """An agent with fixed initial wealth.""" - - def __init__(self, unique_id, model): - super().__init__(unique_id, model) - self.wealth = 1 - - def move(self): - possible_steps = [ - node - for node in self.model.grid.get_neighbors(self.pos, include_center=False) - if self.model.grid.is_cell_empty(node) - ] - if len(possible_steps) > 0: - new_position = self.random.choice(possible_steps) - self.model.grid.move_agent(self, new_position) - - def give_money(self): - - neighbors_nodes = self.model.grid.get_neighbors(self.pos, include_center=False) - neighbors = self.model.grid.get_cell_list_contents(neighbors_nodes) - if len(neighbors) > 0: - other = self.random.choice(neighbors) - other.wealth += 1 - self.wealth -= 1 - - def step(self): - self.move() - if self.wealth > 0: - self.give_money() diff --git a/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/server.py b/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/server.py deleted file mode 100644 index abc493a3e5b..00000000000 --- a/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/server.py +++ /dev/null @@ -1,58 +0,0 @@ -import mesa - -from .model import BoltzmannWealthModelNetwork - - -def network_portrayal(G): - # The model ensures there is 0 or 1 agent per node - - portrayal = dict() - portrayal["nodes"] = [ - { - "id": node_id, - "size": 3 if agents else 1, - "color": "#CC0000" if not agents or agents[0].wealth == 0 else "#007959", - "label": None - if not agents - else f"Agent:{agents[0].unique_id} Wealth:{agents[0].wealth}", - } - for (node_id, agents) in G.nodes.data("agent") - ] - - portrayal["edges"] = [ - {"id": edge_id, "source": source, "target": target, "color": "#000000"} - for edge_id, (source, target) in enumerate(G.edges) - ] - - return portrayal - - -grid = mesa.visualization.NetworkModule(network_portrayal, 500, 500) -chart = mesa.visualization.ChartModule( - [{"Label": "Gini", "Color": "Black"}], data_collector_name="datacollector" -) - -model_params = { - "num_agents": mesa.visualization.Slider( - "Number of agents", - 7, - 2, - 10, - 1, - description="Choose how many agents to include in the model", - ), - "num_nodes": mesa.visualization.Slider( - "Number of nodes", - 10, - 3, - 12, - 1, - description="Choose how many nodes to include in the model, with at " - "least the same number of agents", - ), -} - -server = mesa.visualization.ModularServer( - BoltzmannWealthModelNetwork, [grid, chart], "Money Model", model_params -) -server.port = 8521 diff --git a/examples/boltzmann_wealth_model_network/requirements.txt b/examples/boltzmann_wealth_model_network/requirements.txt deleted file mode 100644 index f3aa7ff7a50..00000000000 --- a/examples/boltzmann_wealth_model_network/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -jupyter -matplotlib -mesa -numpy -networkx diff --git a/examples/boltzmann_wealth_model_network/run.py b/examples/boltzmann_wealth_model_network/run.py deleted file mode 100644 index 34a388a484c..00000000000 --- a/examples/boltzmann_wealth_model_network/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from boltzmann_wealth_model_network.server import server - -server.launch() diff --git a/examples/charts/Readme.md b/examples/charts/Readme.md deleted file mode 100644 index c3145d91ddf..00000000000 --- a/examples/charts/Readme.md +++ /dev/null @@ -1,40 +0,0 @@ -# Mesa Charts Example - -## Summary - -A modified version of the "bank_reserves" example made to provide examples of mesa's charting tools. - -The chart types included in this example are: -- Line Charts for time-series data of multiple model parameters -- Pie Charts for model parameters -- Bar charts for both model and agent-level parameters - -## Installation - -To install the dependencies use pip and the requirements.txt in this directory. e.g. - -``` - $ pip install -r requirements.txt -``` - -## Interactive Model Run - -To run the model interactively, use `mesa runserver` in this directory: - -``` - $ mesa runserver -``` - -Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/), select the model parameters, press Reset, then Start. - -## Files - -* ``bank_reserves/random_walker.py``: This defines a class that inherits from the Mesa Agent class. The main purpose is to provide a method for agents to move randomly one cell at a time. -* ``bank_reserves/agents.py``: Defines the People and Bank classes. -* ``bank_reserves/model.py``: Defines the Bank Reserves model and the DataCollector functions. -* ``bank_reserves/server.py``: Sets up the interactive visualization server. -* ``run.py``: Launches a model visualization server. - -## Further Reading - -See the "bank_reserves" model for more information. diff --git a/examples/charts/charts/agents.py b/examples/charts/charts/agents.py deleted file mode 100644 index 0d18c453e5f..00000000000 --- a/examples/charts/charts/agents.py +++ /dev/null @@ -1,182 +0,0 @@ -""" -The following code was adapted from the Bank Reserves model included in Netlogo -Model information can be found at: http://ccl.northwestern.edu/netlogo/models/BankReserves -Accessed on: November 2, 2017 -Author of NetLogo code: - Wilensky, U. (1998). NetLogo Bank Reserves model. - http://ccl.northwestern.edu/netlogo/models/BankReserves. - Center for Connected Learning and Computer-Based Modeling, - Northwestern University, Evanston, IL. -""" - -import mesa - -from charts.random_walk import RandomWalker - - -class Bank(mesa.Agent): - def __init__(self, unique_id, model, reserve_percent=50): - # initialize the parent class with required parameters - super().__init__(unique_id, model) - # for tracking total value of loans outstanding - self.bank_loans = 0 - """percent of deposits the bank must keep in reserves - this is set via - Slider in server.py""" - self.reserve_percent = reserve_percent - # for tracking total value of deposits - self.deposits = 0 - # total amount of deposits in reserve - self.reserves = (self.reserve_percent / 100) * self.deposits - # amount the bank is currently able to loan - self.bank_to_loan = 0 - - """update the bank's reserves and amount it can loan; - this is called every time a person balances their books - see below for Person.balance_books()""" - - def bank_balance(self): - self.reserves = (self.reserve_percent / 100) * self.deposits - self.bank_to_loan = self.deposits - (self.reserves + self.bank_loans) - - -# subclass of RandomWalker, which is subclass to Mesa Agent -class Person(RandomWalker): - def __init__(self, unique_id, pos, model, moore, bank, rich_threshold): - # init parent class with required parameters - super().__init__(unique_id, pos, model, moore=moore) - # the amount each person has in savings - self.savings = 0 - # total loan amount person has outstanding - self.loans = 0 - """start everyone off with a random amount in their wallet from 1 to a - user settable rich threshold amount""" - self.wallet = self.random.randint(1, rich_threshold + 1) - # savings minus loans, see balance_books() below - self.wealth = 0 - # person to trade with, see do_business() below - self.customer = 0 - # person's bank, set at __init__, all people have the same bank in this model - self.bank = bank - - def do_business(self): - """check if person has any savings, any money in wallet, or if the - bank can loan them any money""" - if self.savings > 0 or self.wallet > 0 or self.bank.bank_to_loan > 0: - # create list of people at my location (includes self) - my_cell = self.model.grid.get_cell_list_contents([self.pos]) - # check if other people are at my location - if len(my_cell) > 1: - # set customer to self for while loop condition - customer = self - while customer == self: - """select a random person from the people at my location - to trade with""" - customer = self.random.choice(my_cell) - # 50% chance of trading with customer - if self.random.randint(0, 1) == 0: - # 50% chance of trading $5 - if self.random.randint(0, 1) == 0: - # give customer $5 from my wallet (may result in negative wallet) - customer.wallet += 5 - self.wallet -= 5 - # 50% chance of trading $2 - else: - # give customer $2 from my wallet (may result in negative wallet) - customer.wallet += 2 - self.wallet -= 2 - - def balance_books(self): - # check if wallet is negative from trading with customer - if self.wallet < 0: - # if negative money in wallet, check if my savings can cover the balance - if self.savings >= (self.wallet * -1): - """if my savings can cover the balance, withdraw enough - money from my savings so that my wallet has a 0 balance""" - self.withdraw_from_savings(self.wallet * -1) - # if my savings cannot cover the negative balance of my wallet - else: - # check if i have any savings - if self.savings > 0: - """if i have savings, withdraw all of it to reduce my - negative balance in my wallet""" - self.withdraw_from_savings(self.savings) - # record how much money the bank can loan out right now - temp_loan = self.bank.bank_to_loan - """check if the bank can loan enough money to cover the - remaining negative balance in my wallet""" - if temp_loan >= (self.wallet * -1): - """if the bank can loan me enough money to cover - the remaining negative balance in my wallet, take out a - loan for the remaining negative balance""" - self.take_out_loan(self.wallet * -1) - else: - """if the bank cannot loan enough money to cover the negative - balance of my wallet, then take out a loan for the - total amount the bank can loan right now""" - self.take_out_loan(temp_loan) - else: - """if i have money in my wallet from trading with customer, deposit - it to my savings in the bank""" - self.deposit_to_savings(self.wallet) - # check if i have any outstanding loans, and if i have savings - if self.loans > 0 and self.savings > 0: - # check if my savings can cover my outstanding loans - if self.savings >= self.loans: - # payoff my loans with my savings - self.withdraw_from_savings(self.loans) - self.repay_a_loan(self.loans) - # if my savings won't cover my loans - else: - # pay off part of my loans with my savings - self.withdraw_from_savings(self.savings) - self.repay_a_loan(self.wallet) - # calculate my wealth - self.wealth = self.savings - self.loans - - # part of balance_books() - def deposit_to_savings(self, amount): - # take money from my wallet and put it in savings - self.wallet -= amount - self.savings += amount - # increase bank deposits - self.bank.deposits += amount - - # part of balance_books() - def withdraw_from_savings(self, amount): - # put money in my wallet from savings - self.wallet += amount - self.savings -= amount - # decrease bank deposits - self.bank.deposits -= amount - - # part of balance_books() - def repay_a_loan(self, amount): - # take money from my wallet to pay off all or part of a loan - self.loans -= amount - self.wallet -= amount - # increase the amount the bank can loan right now - self.bank.bank_to_loan += amount - # decrease the bank's outstanding loans - self.bank.bank_loans -= amount - - # part of balance_books() - def take_out_loan(self, amount): - """borrow from the bank to put money in my wallet, and increase my - outstanding loans""" - self.loans += amount - self.wallet += amount - # decresae the amount the bank can loan right now - self.bank.bank_to_loan -= amount - # increase the bank's outstanding loans - self.bank.bank_loans += amount - - # step is called for each agent in model.BankReservesModel.schedule.step() - def step(self): - # move to a cell in my Moore neighborhood - self.random_move() - # trade - self.do_business() - # deposit money or take out a loan - self.balance_books() - # update the bank's reserves and the amount it can loan right now - self.bank.bank_balance() diff --git a/examples/charts/charts/model.py b/examples/charts/charts/model.py deleted file mode 100644 index 295dfc27665..00000000000 --- a/examples/charts/charts/model.py +++ /dev/null @@ -1,147 +0,0 @@ -""" -The following code was adapted from the Bank Reserves model included in Netlogo -Model information can be found at: http://ccl.northwestern.edu/netlogo/models/BankReserves -Accessed on: November 2, 2017 -Author of NetLogo code: - Wilensky, U. (1998). NetLogo Bank Reserves model. - http://ccl.northwestern.edu/netlogo/models/BankReserves. - Center for Connected Learning and Computer-Based Modeling, - Northwestern University, Evanston, IL. -""" - -import mesa -import numpy as np - -from charts.agents import Bank, Person - -""" -If you want to perform a parameter sweep, call batch_run.py instead of run.py. -For details see batch_run.py in the same directory as run.py. -""" - -# Start of datacollector functions - - -def get_num_rich_agents(model): - """return number of rich agents""" - - rich_agents = [a for a in model.schedule.agents if a.savings > model.rich_threshold] - return len(rich_agents) - - -def get_num_poor_agents(model): - """return number of poor agents""" - - poor_agents = [a for a in model.schedule.agents if a.loans > 10] - return len(poor_agents) - - -def get_num_mid_agents(model): - """return number of middle class agents""" - - mid_agents = [ - a - for a in model.schedule.agents - if a.loans < 10 and a.savings < model.rich_threshold - ] - return len(mid_agents) - - -def get_total_savings(model): - """sum of all agents' savings""" - - agent_savings = [a.savings for a in model.schedule.agents] - # return the sum of agents' savings - return np.sum(agent_savings) - - -def get_total_wallets(model): - """sum of amounts of all agents' wallets""" - - agent_wallets = [a.wallet for a in model.schedule.agents] - # return the sum of all agents' wallets - return np.sum(agent_wallets) - - -def get_total_money(model): - # sum of all agents' wallets - wallet_money = get_total_wallets(model) - # sum of all agents' savings - savings_money = get_total_savings(model) - # return sum of agents' wallets and savings for total money - return wallet_money + savings_money - - -def get_total_loans(model): - # list of amounts of all agents' loans - agent_loans = [a.loans for a in model.schedule.agents] - # return sum of all agents' loans - return np.sum(agent_loans) - - -class Charts(mesa.Model): - - # grid height - grid_h = 20 - # grid width - grid_w = 20 - - """init parameters "init_people", "rich_threshold", and "reserve_percent" - are all set via Slider""" - - def __init__( - self, - height=grid_h, - width=grid_w, - init_people=2, - rich_threshold=10, - reserve_percent=50, - ): - self.height = height - self.width = width - self.init_people = init_people - self.schedule = mesa.time.RandomActivation(self) - self.grid = mesa.space.MultiGrid(self.width, self.height, torus=True) - # rich_threshold is the amount of savings a person needs to be considered "rich" - self.rich_threshold = rich_threshold - self.reserve_percent = reserve_percent - # see datacollector functions above - self.datacollector = mesa.DataCollector( - model_reporters={ - "Rich": get_num_rich_agents, - "Poor": get_num_poor_agents, - "Middle Class": get_num_mid_agents, - "Savings": get_total_savings, - "Wallets": get_total_wallets, - "Money": get_total_money, - "Loans": get_total_loans, - }, - agent_reporters={"Wealth": lambda x: x.wealth}, - ) - - # create a single bank for the model - self.bank = Bank(1, self, self.reserve_percent) - - # create people for the model according to number of people set by user - for i in range(self.init_people): - # set x, y coords randomly within the grid - x = self.random.randrange(self.width) - y = self.random.randrange(self.height) - p = Person(i, (x, y), self, True, self.bank, self.rich_threshold) - # place the Person object on the grid at coordinates (x, y) - self.grid.place_agent(p, (x, y)) - # add the Person object to the model schedule - self.schedule.add(p) - - self.running = True - self.datacollector.collect(self) - - def step(self): - # tell all the agents in the model to run their step function - self.schedule.step() - # collect data - self.datacollector.collect(self) - - def run_model(self): - for i in range(self.run_time): - self.step() diff --git a/examples/charts/charts/random_walk.py b/examples/charts/charts/random_walk.py deleted file mode 100644 index 7e067881e4e..00000000000 --- a/examples/charts/charts/random_walk.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Citation: -The following code is a copy from random_walk.py at -https://github.com/projectmesa/mesa/blob/main/examples/wolf_sheep/wolf_sheep/random_walk.py -Accessed on: November 2, 2017 -Original Author: Jackie Kazil - -Generalized behavior for random walking, one grid cell at a time. -""" - -import mesa - - -class RandomWalker(mesa.Agent): - """ - Class implementing random walker methods in a generalized manner. - Not intended to be used on its own, but to inherit its methods to multiple - other agents. - """ - - grid = None - x = None - y = None - # use a Moore neighborhood - moore = True - - def __init__(self, unique_id, pos, model, moore=True): - """ - grid: The MultiGrid object in which the agent lives. - x: The agent's current x coordinate - y: The agent's current y coordinate - moore: If True, may move in all 8 directions. - Otherwise, only up, down, left, right. - """ - super().__init__(unique_id, model) - self.pos = pos - self.moore = moore - - def random_move(self): - """ - Step one cell in any allowable direction. - """ - # Pick the next cell from the adjacent cells. - next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True) - next_move = self.random.choice(next_moves) - # Now move: - self.model.grid.move_agent(self, next_move) diff --git a/examples/charts/charts/server.py b/examples/charts/charts/server.py deleted file mode 100644 index ba7bfbdfd51..00000000000 --- a/examples/charts/charts/server.py +++ /dev/null @@ -1,113 +0,0 @@ -import mesa - -from charts.agents import Person -from charts.model import Charts - -""" -Citation: -The following code was adapted from server.py at -https://github.com/projectmesa/mesa/blob/main/examples/wolf_sheep/wolf_sheep/server.py -Accessed on: November 2, 2017 -Author of original code: Taylor Mutch -""" - -# The colors here are taken from Matplotlib's tab10 palette -# Green -RICH_COLOR = "#2ca02c" -# Red -POOR_COLOR = "#d62728" -# Blue -MID_COLOR = "#1f77b4" - - -def person_portrayal(agent): - if agent is None: - return - - portrayal = {} - - # update portrayal characteristics for each Person object - if isinstance(agent, Person): - portrayal["Shape"] = "circle" - portrayal["r"] = 0.5 - portrayal["Layer"] = 0 - portrayal["Filled"] = "true" - - color = MID_COLOR - - # set agent color based on savings and loans - if agent.savings > agent.model.rich_threshold: - color = RICH_COLOR - if agent.savings < 10 and agent.loans < 10: - color = MID_COLOR - if agent.loans > 10: - color = POOR_COLOR - - portrayal["Color"] = color - - return portrayal - - -# dictionary of user settable parameters - these map to the model __init__ parameters -model_params = { - "init_people": mesa.visualization.Slider( - "People", 25, 1, 200, description="Initial Number of People" - ), - "rich_threshold": mesa.visualization.Slider( - "Rich Threshold", - 10, - 1, - 20, - description="Upper End of Random Initial Wallet Amount", - ), - "reserve_percent": mesa.visualization.Slider( - "Reserves", - 50, - 1, - 100, - description="Percent of deposits the bank has to hold in reserve", - ), -} - -# set the portrayal function and size of the canvas for visualization -canvas_element = mesa.visualization.CanvasGrid(person_portrayal, 20, 20, 500, 500) - -# map data to chart in the ChartModule -line_chart = mesa.visualization.ChartModule( - [ - {"Label": "Rich", "Color": RICH_COLOR}, - {"Label": "Poor", "Color": POOR_COLOR}, - {"Label": "Middle Class", "Color": MID_COLOR}, - ] -) - -model_bar = mesa.visualization.BarChartModule( - [ - {"Label": "Rich", "Color": RICH_COLOR}, - {"Label": "Poor", "Color": POOR_COLOR}, - {"Label": "Middle Class", "Color": MID_COLOR}, - ] -) - -agent_bar = mesa.visualization.BarChartModule( - [{"Label": "Wealth", "Color": MID_COLOR}], - scope="agent", - sorting="ascending", - sort_by="Wealth", -) - -pie_chart = mesa.visualization.PieChartModule( - [ - {"Label": "Rich", "Color": RICH_COLOR}, - {"Label": "Middle Class", "Color": MID_COLOR}, - {"Label": "Poor", "Color": POOR_COLOR}, - ] -) - -# create instance of Mesa ModularServer -server = mesa.visualization.ModularServer( - Charts, - [canvas_element, line_chart, model_bar, agent_bar, pie_chart], - "Mesa Charts", - model_params=model_params, -) diff --git a/examples/charts/requirements.txt b/examples/charts/requirements.txt deleted file mode 100644 index 90169c50035..00000000000 --- a/examples/charts/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -itertools -mesa -numpy -pandas diff --git a/examples/charts/run.py b/examples/charts/run.py deleted file mode 100644 index ec56c635b58..00000000000 --- a/examples/charts/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from charts.server import server - -server.launch() diff --git a/examples/color_patches/Readme.md b/examples/color_patches/Readme.md deleted file mode 100644 index 5b722bcea04..00000000000 --- a/examples/color_patches/Readme.md +++ /dev/null @@ -1,38 +0,0 @@ -# Color Patches - - -This is a cellular automaton model where each agent lives in a cell on a 2D grid, and never moves. - -An agent's state represents its "opinion" and is shown by the color of the cell the agent lives in. Each color represents an opinion - there are 16 of them. At each time step, an agent's opinion is influenced by that of its neighbors, and changes to the most common one found; ties are randomly arbitrated. As an agent adapts its thinking to that of its neighbors, the cell color changes. - -### Parameters you can play with: -(you must change the code to alter the parameters at this stage) -* Vary the number of opinions. -* Vary the size of the grid -* Change the grid from fixed borders to a torus continuum - -### Observe -* how groups of like minded agents form and evolve -* how sometimes a single opinion prevails -* how some minority or fragmented opinions rapidly disappear - -## 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. - -## Files - -* ``color_patches/model.py``: Defines the cell and model classes. The cell class governs each cell's behavior. The model class itself controls the lattice on which the cells live and interact. -* ``color_patches/server.py``: Defines an interactive visualization. -* ``run.py``: Launches an interactive visualization - -## Further Reading - -Inspired from [this model](http://www.cs.sjsu.edu/~pearce/modules/lectures/abs/as/ca.htm) from San Jose University
-Other similar models: [Schelling Segregation Model](https://github.com/projectmesa/mesa/tree/main/examples/schelling) diff --git a/examples/color_patches/color_patches/__init__.py b/examples/color_patches/color_patches/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/color_patches/color_patches/model.py b/examples/color_patches/color_patches/model.py deleted file mode 100644 index fb15a221430..00000000000 --- a/examples/color_patches/color_patches/model.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -The model - a 2D lattice where agents live and have an opinion -""" - -from collections import Counter - -import mesa - - -class ColorCell(mesa.Agent): - """ - Represents a cell's opinion (visualized by a color) - """ - - OPINIONS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] - - def __init__(self, pos, model, initial_state): - """ - Create a cell, in the given state, at the given row, col position. - """ - super().__init__(pos, model) - self._row = pos[0] - self._col = pos[1] - self._state = initial_state - self._next_state = None - - def get_col(self): - """Return the col location of this cell.""" - return self._col - - def get_row(self): - """Return the row location of this cell.""" - return self._row - - def get_state(self): - """Return the current state (OPINION) of this cell.""" - return self._state - - def step(self): - """ - Determines the agent opinion for the next step by polling its neighbors - The opinion is determined by the majority of the 8 neighbors' opinion - A choice is made at random in case of a tie - The next state is stored until all cells have been polled - """ - _neighbor_iter = self.model.grid.iter_neighbors((self._row, self._col), True) - neighbors_opinion = Counter(n.get_state() for n in _neighbor_iter) - # Following is a a tuple (attribute, occurrences) - polled_opinions = neighbors_opinion.most_common() - tied_opinions = [] - for neighbor in polled_opinions: - if neighbor[1] == polled_opinions[0][1]: - tied_opinions.append(neighbor) - - self._next_state = self.random.choice(tied_opinions)[0] - - def advance(self): - """ - Set the state of the agent to the next state - """ - self._state = self._next_state - - -class ColorPatches(mesa.Model): - """ - represents a 2D lattice where agents live - """ - - def __init__(self, width=20, height=20): - """ - Create a 2D lattice with strict borders where agents live - The agents next state is first determined before updating the grid - """ - - self._grid = mesa.space.Grid(width, height, torus=False) - self._schedule = mesa.time.SimultaneousActivation(self) - - # self._grid.coord_iter() - # --> should really not return content + col + row - # -->but only col & row - # for (contents, col, row) in self._grid.coord_iter(): - # replaced content with _ to appease linter - for (_, row, col) in self._grid.coord_iter(): - cell = ColorCell( - (row, col), self, ColorCell.OPINIONS[self.random.randrange(0, 16)] - ) - self._grid.place_agent(cell, (row, col)) - self._schedule.add(cell) - - self.running = True - - def step(self): - """ - Advance the model one step. - """ - self._schedule.step() - - # the following is a temporary fix for the framework classes accessing - # model attributes directly - # I don't think it should - # --> it imposes upon the model builder to use the attributes names that - # the framework expects. - # - # Traceback included in docstrings - - @property - def grid(self): - """ - /mesa/visualization/modules/CanvasGridVisualization.py - is directly accessing Model.grid - 76 def render(self, model): - 77 grid_state = defaultdict(list) - ---> 78 for y in range(model.grid.height): - 79 for x in range(model.grid.width): - 80 cell_objects = model.grid.get_cell_list_contents([(x, y)]) - - AttributeError: 'ColorPatches' object has no attribute 'grid' - """ - return self._grid - - @property - def schedule(self): - """ - mesa_ABM/examples_ABM/color_patches/mesa/visualization/ModularVisualization.py", - line 278, in run_model - while self.model.schedule.steps < self.max_steps and self.model.running: - AttributeError: 'NoneType' object has no attribute 'steps' - """ - return self._schedule diff --git a/examples/color_patches/color_patches/server.py b/examples/color_patches/color_patches/server.py deleted file mode 100644 index 44c4624ebb3..00000000000 --- a/examples/color_patches/color_patches/server.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -handles the definition of the canvas parameters and -the drawing of the model representation on the canvas -""" -# import webbrowser - -import mesa - -from .model import ColorPatches - -_COLORS = [ - "Aqua", - "Blue", - "Fuchsia", - "Gray", - "Green", - "Lime", - "Maroon", - "Navy", - "Olive", - "Orange", - "Purple", - "Red", - "Silver", - "Teal", - "White", - "Yellow", -] - - -grid_rows = 50 -grid_cols = 25 -cell_size = 10 -canvas_width = grid_rows * cell_size -canvas_height = grid_cols * cell_size - - -def color_patch_draw(cell): - """ - This function is registered with the visualization server to be called - each tick to indicate how to draw the cell in its current state. - - :param cell: the cell in the simulation - - :return: the portrayal dictionary. - - """ - if cell is None: - raise AssertionError - portrayal = {"Shape": "rect", "w": 1, "h": 1, "Filled": "true", "Layer": 0} - portrayal["x"] = cell.get_row() - portrayal["y"] = cell.get_col() - portrayal["Color"] = _COLORS[cell.get_state()] - return portrayal - - -canvas_element = mesa.visualization.CanvasGrid( - color_patch_draw, grid_rows, grid_cols, canvas_width, canvas_height -) - -server = mesa.visualization.ModularServer( - ColorPatches, - [canvas_element], - "Color Patches", - {"width": grid_rows, "height": grid_cols}, -) - -# webbrowser.open('http://127.0.0.1:8521') # TODO: make this configurable diff --git a/examples/color_patches/requirements.txt b/examples/color_patches/requirements.txt deleted file mode 100644 index 1ad1bbec7ab..00000000000 --- a/examples/color_patches/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -mesa \ No newline at end of file diff --git a/examples/color_patches/run.py b/examples/color_patches/run.py deleted file mode 100644 index afe422d45d9..00000000000 --- a/examples/color_patches/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from color_patches.server import server - -server.launch() diff --git a/examples/conways_game_of_life/Readme.md b/examples/conways_game_of_life/Readme.md deleted file mode 100644 index 686afb4065a..00000000000 --- a/examples/conways_game_of_life/Readme.md +++ /dev/null @@ -1,30 +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 - -* ``game_of_life/cell.py``: Defines the behavior of an individual cell, which can be in two states: DEAD or ALIVE. -* ``game_of_life/model.py``: Defines the model itself, initialized with a random configuration of alive and dead cells. -* ``game_of_life/portrayal.py``: Describes for the front end how to render a cell. -* ``game_of_live/server.py``: Defines an interactive visualization. -* ``run.py``: Launches the visualization - -## Further Reading -[Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) - diff --git a/examples/conways_game_of_life/conways_game_of_life/cell.py b/examples/conways_game_of_life/conways_game_of_life/cell.py deleted file mode 100644 index 8639288d4ca..00000000000 --- a/examples/conways_game_of_life/conways_game_of_life/cell.py +++ /dev/null @@ -1,53 +0,0 @@ -import mesa - - -class Cell(mesa.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__(pos, 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 step(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 advance(self): - """ - Set the state to the new computed state -- computed in step(). - """ - self.state = self._nextState diff --git a/examples/conways_game_of_life/conways_game_of_life/model.py b/examples/conways_game_of_life/conways_game_of_life/model.py deleted file mode 100644 index 635ccaa959d..00000000000 --- a/examples/conways_game_of_life/conways_game_of_life/model.py +++ /dev/null @@ -1,43 +0,0 @@ -import mesa - -from .cell import Cell - - -class ConwaysGameOfLife(mesa.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. - """ - - # Set up the grid and schedule. - - # Use SimultaneousActivation which simulates all the cells - # computing their next state simultaneously. This needs to - # be done because each cell's next state depends on the current - # state of all its neighbors -- before they've changed. - self.schedule = mesa.time.SimultaneousActivation(self) - - # Use a simple grid, where edges wrap around. - self.grid = mesa.space.Grid(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.schedule.add(cell) - - self.running = True - - def step(self): - """ - Have the scheduler advance each cell by one step - """ - self.schedule.step() diff --git a/examples/conways_game_of_life/conways_game_of_life/portrayal.py b/examples/conways_game_of_life/conways_game_of_life/portrayal.py deleted file mode 100644 index 4f68468d857..00000000000 --- a/examples/conways_game_of_life/conways_game_of_life/portrayal.py +++ /dev/null @@ -1,19 +0,0 @@ -def portrayCell(cell): - """ - This function is registered with the visualization server to be called - each tick to indicate how to draw the cell in its current state. - :param cell: the cell in the simulation - :return: the portrayal dictionary. - """ - if cell is None: - raise AssertionError - return { - "Shape": "rect", - "w": 1, - "h": 1, - "Filled": "true", - "Layer": 0, - "x": cell.x, - "y": cell.y, - "Color": "black" if cell.isAlive else "white", - } diff --git a/examples/conways_game_of_life/conways_game_of_life/server.py b/examples/conways_game_of_life/conways_game_of_life/server.py deleted file mode 100644 index 4167b3d01bd..00000000000 --- a/examples/conways_game_of_life/conways_game_of_life/server.py +++ /dev/null @@ -1,12 +0,0 @@ -import mesa - -from .portrayal import portrayCell -from .model import ConwaysGameOfLife - - -# Make a world that is 50x50, on a 250x250 display. -canvas_element = mesa.visualization.CanvasGrid(portrayCell, 50, 50, 250, 250) - -server = mesa.visualization.ModularServer( - ConwaysGameOfLife, [canvas_element], "Game of Life", {"height": 50, "width": 50} -) diff --git a/examples/conways_game_of_life/requirements.txt b/examples/conways_game_of_life/requirements.txt deleted file mode 100644 index 1ad1bbec7ab..00000000000 --- a/examples/conways_game_of_life/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -mesa \ No newline at end of file diff --git a/examples/conways_game_of_life/run.py b/examples/conways_game_of_life/run.py deleted file mode 100644 index 2854fdee59d..00000000000 --- a/examples/conways_game_of_life/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from conways_game_of_life.server import server - -server.launch() diff --git a/examples/epstein_civil_violence/Epstein Civil Violence.ipynb b/examples/epstein_civil_violence/Epstein Civil Violence.ipynb deleted file mode 100644 index 2fe5ed25879..00000000000 --- a/examples/epstein_civil_violence/Epstein Civil Violence.ipynb +++ /dev/null @@ -1,119 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This example implements the first model from \"Modeling civil violence: An agent-based computational approach,\" by Joshua Epstein. The paper (pdf) can be found [here](http://www.uvm.edu/~pdodds/files/papers/others/2002/epstein2002a.pdf).\n", - "\n", - "The model consists of two types of agents: \"Citizens\" (called \"Agents\" in the paper) and \"Cops.\" Agents decide whether or not to rebel by weighing their unhappiness ('grievance') against the risk of rebelling, which they estimate by comparing the local ratio of rebels to cops. \n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "%matplotlib inline\n", - "\n", - "from epstein_civil_violence.agent import Citizen, Cop\n", - "from epstein_civil_violence.model import EpsteinCivilViolence" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "model = EpsteinCivilViolence(\n", - " height=40,\n", - " width=40,\n", - " citizen_density=0.7,\n", - " cop_density=0.074,\n", - " citizen_vision=7,\n", - " cop_vision=7,\n", - " legitimacy=0.8,\n", - " max_jail_term=1000,\n", - " max_iters=1000,\n", - ") # cap the number of steps the model takes\n", - "model.run_model()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The model's data collector counts the number of citizens who are Active (in rebellion), Jailed, or Quiescent after each step." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "model_out = model.datacollector.get_model_vars_dataframe()" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAfsAAAEWCAYAAABhUT6OAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+17YcXAAAgAElEQVR4nOzdd3gc1bn48e+7Vb3L6rLce8E2xmC68Q2mh5oQWoAQbiAhJL8ASUglySUJKeTSk5Bg4NJLKAYChBIMJsg2uMuWZVm9d620q909vz9mJcu2mm0Ve3k/z7OPd8+cmXlnLemdc+bMHDHGoJRSSqnwZRvrAJRSSik1sjTZK6WUUmFOk71SSikV5jTZK6WUUmFOk71SSikV5jTZK6WUUmFOk706ZCLyAxH5ywDLvyIi/xzNmEaTiFwlIh/0+twmIhMHqL9ZRE4eleAOIyLygIj8aKzjUOrzSJO9GhIRuVRE8kOJrFJEXhOR4wGMMb8yxlwbqpcnIkZEHN3rGmMeN8b81+EU80gyxsQYY4pCMfxdRH6xz/JZxph3R2LfInKWiPxHRNpFpF5EHheR7JHY1z77zQ19z90vE4qh+/MJxpjrjTF3jHQsSqn9abJXgxKR7wB/BH4FpAG5wH3AuWMZ10COxJgPlYhcCPwfcDeQAswCvMAHIpI4zPty9P5sjCkJneTEGGNiQsXzepX9ezj3r5Q6QMYYfemr3xcQD7QBFw1Q56fAY6H3JYAJrdMGHAtcBXwQWn5Lr2VtQBfw9177+itQCZQDvwDsoWVXAR8AdwGNwC5gxSHE7MY6GagIvf4IuEPLTgbKgO8CNaF4vtpr3WTgJaAF+A9wR/fxhZYbYDJwXej4fKF4Xg4tLwZOO9Q49jkeAXYDt+xTbgM2AT8P7asJmN1reSrQAYwLfT4L+DRU70Ngbq+6xcCtwAaskwjHAN+vASbvU/Z34Bf7HNstvY7tPOAMYDvQAPxgn+O4DdgJ1ANPA0lj/fuhL30dKS9t2avBHAtEAC8Msf6JoX8TjNWi+6j3QmPMb8ye1t8MoBbrDzfAI4AfK1EeBfwXcG2v1Y8BCrBarb8B/ioicpAx/xBYAswH5gGLgdt7LU/HOmnIAq4B7u3VOr4X6AQygKtDr/0YYx4CHge6j/nsYY6jt2lYvRfP7BNDEHgOWG6M8QLPA1/uVeVi4D1jTI2ILAAeBr6OdULzIPCSiLh71f8ycCbW/6+/r+M+AOlY/09ZwI+BPwOXAQuBE4Af9xr78C2sk4GTgEysE757D3H/Sn1uaLJXg0kG6obhD/teRCQSeBG42xizSkTSgBXAt40x7caYGuAPwJd6rbbbGPNnY0wA68QgA6uL/mBi/grwc2NMjTGmFvgZcHmv5V2h5V3GmFVYLfNpImIHLgB+HIpzUyiWg3VQcfSxnZTQv5V9LKvstfz/2DvZXxoqA/ga8KAx5mNjTMAY8whWC35Jr/p/MsaUGmM6hn6I/eoCfmmM6QKeDMV4tzGm1RizGdgMzA3V/TrwQ2NMWeik5afAhfteTlBK9U1/UdRg6oEUEXEMc8L/K1BgjPl16PN4wAlU9mqs24DSXutUdb8xxnhC9WLY31BizsTq9u62O1TWs4191vWE9pWK9XtTus+6B+tg49hXXejfDKxLHL1l9Fr+LyBSRI7B+j7ns6cHZDxwpYh8s9e6rn3i6X3ch6o+dOIG1qUEgOpeyzvYc6zjgRdEJNhreQDrZK98GGNSKixpy14N5iOsLuvzhlh/0GkUReQ2rNbpNb2KS7FakSnGmITQK84YM+tAA2ZoMVdgJZBuuaGywdRiXWrI2Wfd/gz2fRxsHPsqwLoGflHvQhGxYfVEvA093fpPY7XuLwVeMca0hqqXYrW0E3q9oowxTxzA8YyUUqwxGr1jizDGaKJXagg02asBGWOasa6n3isi54lIlIg4RWSFiPymj1VqgSDQ533mIrKC0PXX3l3BxphK4J/A70QkTkRsIjJJRE4aoZifAG4XkVQRSQnVf2wI2w5gXff+aWi7M4ErB1ilmn6+i0OJo4+4DPD/Qtu6VEQiRSQd+AsQh3VJpNv/AZdgXUL4v17lfwauF5FjxBItImeKSOyBxjMCHgB+KSLjAULfV9jeWaHUcNNkrwZljPk98B2sgWO1WK2sG7Guue9b1wP8ElgtIk0ismSfKpdgdYVv7XUP9gOhZVdgdRtvwRqA9SxWF/RIxPwLIB9rZPlGYF2obChuxOpersIaYf63Aer+FZgZ+i72+74OMY69GGOewrrefzNWt/0WIBJYaoyp71XvY6Adq3v+tV7l+VjX7e/B+v4Lse6COBzcjXUHxD9FpBVYgzVgUyk1BGI1CJRSSikVrrRlr5RSSoU5TfZKKaVUmNNkr5RSSoU5TfZKKaVUmAvLh+qkpKSYvLy8sQ5DKaWOKGvXrq0zxqSOwn7GORyOvwCz0UbncAgCm/x+/7ULFy6s6atCWCb7vLw88vPzxzoMpZQ6oojIoTwNcsgcDsdf0tPTZ6SmpjbabDa9JewQBYNBqa2tnVlVVfUX4Jy+6ozYGZWIPCwiNSKyqVfZb0Vkm4hsEJEXRCSh17Lvi0ihiBSIyBd6lZ8eKisMPXlNKaXUkW12ampqiyb64WGz2UxqamozVk9J33VGcP9/B07fp+xNrOk152JNY/l9gNBTyL6ENf/26cB9ImIPTTpyL9YEKTOBL4fqKqWUOnLZNNEPr9D32W9OH7Fkb4x5H2tO6t5l/+w1qccaIDv0/lzgSWOM1xizC+vJXYtDr0JjTJExxoc1M5Y+IlMppZQ6AGM5MOJq9jyqM4u9Z9MqC5X1V74fEblORPJFJL+2tnYEwlVKKRVOdu7c6Vy2bNmk8ePHz87Ozp5zxRVX5HZ0dMhA65x00kmT6+rq7KMV477+9Kc/JRcXFzsPdL0xSfYi8kOsmcMe7y7qo5oZoHz/QmMeMsYsMsYsSk0d8cGkSimljmDBYJDzzjtv8jnnnNO0e/fuTcXFxRs7OzvlG9/4RvZA67333nuFKSkpgYHqjKTHHnsspaSk5ICT/aiPxheRK4GzgGVmz4P5y9h7ytBs9kzz2V+5UkqpI9z3nv0sZ3tVa9RwbnNqeqzntxfOKx2ozssvvxzrdruDN910Uz2Aw+HggQceKM3Ly5s7ZcqUzm3btkWuXLmyBOCUU06Z/N3vfrf6rLPOas3KypqTn5+/NSMjw3/fffcl3X///WldXV2yYMGC9pUrV+4GuOSSS/I2bNgQLSLmK1/5St1PfvKTmk2bNrmvu+668fX19Q673W6eeeaZolmzZnl/9KMfpb3wwgtJPp9PzjzzzKY//OEPFQUFBa4VK1ZMWbx4cVt+fn5MWlqa74033ih85plnEjZt2hR1xRVXTIyIiAjm5+dvjYmJGdLYh1Ft2YvI6cCtwDmh2dG6vQR8SUTcIjIBmAL8B/gEmCIiE0TEhTWI76XRjFkppVT42bhxY+S8efN65yGSkpKCWVlZPr/fP2BXPsC6desinn322aT8/Pxt27Zt22Kz2cwDDzyQ/NFHH0VVVlY6d+zYsXn79u1bbrjhhnqASy+9dML1119fU1BQsCU/P39bbm5u1/PPPx9XWFgYsWHDhq1bt27d8umnn0a99tprMQAlJSUR3/rWt2oKCws3x8fHB1auXJn41a9+tXH27NmelStXFm3btm3LUBM9jGDLXkSeAE4GUkSkDPgJ1uh7N/CmiACsMcZcb4zZLCJPY03J6QduCM0bjojcCLwB2IGHjTGbB9t3dUsn1S2dpMVFjMCRKaWUGi6DtcBHijEGEdkvWQ51JtjXX389dtOmTVHz5s2bAdDZ2WkbN26c/5JLLmkqLS11X3nllTlnn3128xe/+MWWxsZGW3V1teuKK65oAoiKijKAef311+Pef//9uJkzZ84E8Hg8tm3btkVMnDjRl5WV5T3uuOM6AI466ihPcXGx+1COd8SSvTHmy30U/3WA+r/Emgd93/JVwKoD2XdNq5eaFq8me6WUUn2aM2dOxz/+8Y/E3mUNDQ22+vp6R3Jysn/79u095V6vd79ecGOMXHTRRfX33ntv+b7LNm3atOWFF16Iu++++8Y99dRTSQ8++GBJXzEYY/j2t79d+b3vfa+ud3lBQYHL5XL1nHXY7XbT0dFxSD3xYfuYQtP3OD6llFKKc845p7Wzs9N2zz33JAP4/X6+8Y1v5Fx99dU1kydP9m3evDkqEAhQWFjo3LBhQ/S+659++uktr7zySmJ5ebkDoLq62r59+3ZXZWWlIxAIcNVVVzX94he/KN+4cWNUUlJSMD093ffoo48mAHR0dEhra6ttxYoVLY8++mhKc3OzDWDXrl3O7u31JyYmJtDc3HzAdwOEbbIPaq5XSinVD5vNxosvvlj4/PPPJ44fP352YmLifJvNxq9//euq5cuXt+Xk5HinTZs266abbsqZOXOmZ9/1Fy5c2Hn77beXL1u2bOrUqVNnnnrqqVNLS0udxcXFzuOPP37a9OnTZ1599dUTfv7zn5cBPPbYY7vuvffecVOnTp25aNGi6aWlpY7zzz+/5aKLLmo4+uijp0+dOnXmF7/4xUlNTU0DJvIrrrii7pvf/Ob46dOnz2xraxt0bEE3Ger1iSOJO2OKWfPxfzgqN3HwykoppQAQkbXGmEUjvZ/PPvuseN68eXWD1xw9b775ZvSVV1458amnntp5wgkn7JfcjwSfffZZyrx58/L6WhaWE+FAPzfjK6WUUn1Yvnx5e0VFxcaxjmOkhG03fhh2WCillFIHJWyTvbbtlVJKKUvYJnsdoKeUUkpZwjbZaze+UkopZQnjZK/ZXimllIJwTvZjHYBSSqnD2sqVKxNEZOH69esHfNzqvtPKXnLJJePXrl17RD2iNWyTfVBb9koppQbw5JNPJi1YsKDt0UcfTRqo3r7Tyj711FO7Fy5c2DnyEQ6fsL3PXpv2Sil1BHjxhhxqtgzrFLeMm+nhvHsHnGCnubnZlp+fH/PWW28VnHvuuZN///vfVwDcfvvtaU8//XSyiLBs2bLmo48+2rPvtLKnnnrq1Lvuuqv0o48+it61a5f7gQceKAOrB2Dt2rVRjzzySGlf0986HGOXcsM22WuuV0op1Z/HH3884eSTT26eO3euNyEhIfDBBx9EVVRUOF599dXEtWvXbouNjQ1WV1fb09LSAvfff/+4u+66q/TEE0/c68l6l19+eeOSJUumA2UAzz77bNIPf/jDyt7T37rdbnPZZZflPvDAA8k33nhj/ZgcLOGc7DXbK6XU4W+QFvhIefrpp5NuuummGoALLrig4dFHH00KBoNcdtlldbGxsUGAtLS0wEDbyMzM9Ofk5Hjffvvt6FmzZnUWFRVFLF++vO3OO+9M7Wv625E/qv6Fb7LXtr1SSqk+VFVV2desWRO3ffv2yBtvvJFAICAiYs4444wmkSHPLQPAhRde2PjEE08kTp8+vXPFihWNNpttwOlvx0oYD9Ab6wiUUkodjh599NHE888/v76iomJjeXn5xqqqqg3Z2dm+pKQk/6OPPprS2tpqA2vaWhh4WtnLLrus8fXXX0985plnki699NIG6H/629E6vr6EbbLX++yVUkr15Zlnnkk+//zzG3uXnXvuuY0VFRXOFStWNM2fP3/G9OnTZ95xxx3pMPC0sqmpqYEpU6Z0lJeXu0855RQP9D/97egd4f7Cdorb199dzSnTxo11KEopdcT4PE9xGw4GmuI2bFv2esleKaWUsoRtsteH6iillFKWsE32muuVUkopS/gm+7EOQCmllDpMhG+y16a9UkopBYRzsh/rAJRSSqnDRPgme23ZK6WUGkBUVNRRAy0/6qijpgMUFBS4pkyZMutAtn3BBRfk/e1vf0s8lPiG04glexF5WERqRGRTr7IkEXlTRHaE/k0MlYuI/ElECkVkg4gs6LXOlaH6O0TkyqHuX3O9UkqpQ7F+/fptYx3DcBnJZ+P/HbgHWNmr7DbgbWPMnSJyW+jzrcAKYErodQxwP3CMiCQBPwEWYfXMrxWRl4wxez35qC+a65VS6vD3o9U/yilsLBzWKW4nJ0723LH0jiFNsNPc3Gw7/fTTJzc3N9v9fr/8+Mc/rrjsssuawGr5ezye9b3r+/1+brjhhuzVq1fH+nw++drXvlbzve99ry4YDHLVVVflrl69OjYnJ8d7uPUuj1iyN8a8LyJ5+xSfC5wcev8I8C5Wsj8XWGmsb2eNiCSISEao7pvGmAYAEXkTOB14YvD9H/IhKKWUCnNRUVHBV199tTApKSlYWVnpOOaYY6ZfeumlTTZb3x3ff/zjH1Pi4+MDmzZt2trR0SFHH3309LPPPrvl448/jiosLHQXFBRsLisrc86ZM2fWVVddNWZT2u5rtGe9SzPGVAIYYypFpPt5tllA77OwslBZf+X7EZHrgOsAXOmT9aE6Sil1BBhqC3ykBINB+fa3v529Zs2aGJvNRk1NjausrMyRm5vb55S0b731Vty2bduiXnrppUSA1tZW+5YtWyLee++92IsvvrjB4XCQl5fXdeyxx7aO7pEM7HCZ4ravOQXNAOX7FxrzEPAQWM/G11SvlFJqMA8++GBSfX29Y+PGjVvdbrfJysqa09HR0e94NmOM/O53vyu54IILWnqXv/LKK/EHOj3uaBrt0fjVoe55Qv/WhMrLgJxe9bKBigHKB3W4XS9RSil1+GlubranpKR0ud1u8/LLL8dWVFQMOBXt8uXLm++///5Ur9crABs2bHC3tLTYTjrppNZnnnkmye/3s3v3bueaNWtiR+cIhma0W/YvAVcCd4b+/Uev8htF5EmsAXrNoW7+N4BfdY/aB/4L+P4ox6yUUirMdHV14XK5zLXXXtuwYsWKybNnz54xa9Ysz4QJEzoHWu/mm2+uKy4uds+ZM2eGMUaSkpK6Vq1atfPyyy9vevvtt+OmTZs2a8KECZ2LFy/+fHTji8gTWAPsUkSkDGtU/Z3A0yJyDVACXBSqvgo4AygEPMBXAYwxDSJyB/BJqN7PuwfrDUYb9koppfqTn58fmZOT483IyPB/+umnfd5i1z0Sf9q0ab4dO3ZsBrDb7dxzzz3lQPm+9VeuXFkyokEfgpEcjf/lfhYt66OuAW7oZzsPAw8f6P51gJ5SSqm+/OY3v0l98MEHx/32t78d08GBo+lwGaA37DTXK6WU6sstt9xSe8stt9SOdRyjKXwflzvWASillOpPMBgMHr5D149Aoe8z2N/y8E322rRXSqnD1aba2tp4TfjDIxgMSm1tbTywqb862o2vlFJqVPn9/murqqr+UlVVNZswbnSOoiCwye/3X9tfhfBN9tqRr5RSh6WFCxfWAOeMdRyfJ2F7RqUte6WUUsoSvsl+rANQSimlDhPhm+w12yullFJAGCd7faiOUkopZQnbZK+pXimllLKEbbLXfnyllFLKErbJXlO9UkopZQnbZB8MarpXSimlIIyTvaZ6pZRSyhK+yV6zvVJKKQWEc7If6wCUUkqpw0T4Jntt2iullFJAWCf7sY5AKaWUOjyEb7LXjnyllFIKGEKyF5GLRCQ29P52EXleRBaMfGiHRlv2SimllGUoLfsfGWNaReR44AvAI8D9IxvWodNcr5RSSlmGkuwDoX/PBO43xvwDcI1cSMNDJ8JRSimlLENJ9uUi8iBwMbBKRNxDXG9Maa5XSimlLENJ2hcDbwCnG2OagCTgeyMalVJKKaWGzaDJ3hjjAf4BtItILuAEth3KTkXkZhHZLCKbROQJEYkQkQki8rGI7BCRp0TEFarrDn0uDC3PG8o+9D57pZRSyjKU0fjfBKqBN4FXQ69XDnaHIpIFfAtYZIyZDdiBLwG/Bv5gjJkCNALXhFa5Bmg0xkwG/hCqNyjN9UoppZRlKN34NwHTjDGzjDFzQq+5h7hfBxApIg4gCqgETgWeDS1/BDgv9P7c0GdCy5eJiAy2A530TimllLIMJdmXAs3DtUNjTDlwF1CCleSbgbVAkzHGH6pWBmSF3meFYiC0vBlIHnQ/evOdUkopBVgt7MEUAe+KyKuAt7vQGPP7g9mhiCRitdYnAE3AM8CKPqp2Z+u+WvH7ZXIRuQ64DsCVPlm78ZVSSqmQobTsS7Cu17uA2F6vg3UasMsYU2uM6QKeB44DEkLd+gDZQEXofRmQAxBaHg807LtRY8xDxphFxphFoA/VUUoppboN2rI3xvwMQESijTHtw7DPEmCJiEQBHcAyIB94B7gQeBK4EusOAICXQp8/Ci3/lxnCUHsdja+UUkpZhjIa/1gR2QJsDX2eJyL3HewOjTEfYw20WwdsDMXwEHAr8B0RKcS6Jv/X0Cp/BZJD5d8Bbhvafg42QqWUUiq8DOWa/R+xnon/EoAx5jMROfFQdmqM+Qnwk32Ki4DFfdTtBC464H1oR75SSikFDPGxt8aY0n2KAn1WPEwI2rJXSimlug2lZV8qIscBJvRUu28R6tI/nGmuV0oppSxDadlfD9yAdb97GTA/9Pmw1uUPjnUISiml1GFhKC37SGPMV3oXiEj6CMUzLCKcdj4p3u/uPKWUUupzaSgt+12hyWoie5WtGqmAhkOUy05R7XDcJaiUUkod+YaS7DcC/wY+EJFJobJBn00/lpx2G61eP+1e/+CVlVJKqTA3lGRvjDH3YQ3Me1lEzuYwH//mtFvnIlUtnWMciVJKKTX2hnLNXgCMMatFZBnwFDB9RKM6RA6bDT/Q2O6D1LGORimllBpbQ0n2Z3S/McZUisipWM+yP2zZbFbLvlW78ZVSSqn+k72IXGaMeQz4cj/Tx78/YlEdInt3su/UZK+UUkoN1LKPDv3b1wx3h/U1e1vo5KRNk71SSinVf7I3xjwYevuWMWZ172UisnREozpE9tCww9bOrrENRCmllDoMDGU0/v8OseywYRNBBFo02SullFIDXrM/FmsgXqqIfKfXojjAPtKBHaop42L4z64GjDH0M+ZAKaWU+lwYqGXvAmKwTghie71agAtHPrRDs3RyCp8UN/KbNwrGOhSllFJqTA10zf494D0R+bsxZvcoxjQsJqRY4wvvf3cnt55+WD8WQCmllBpRA3Xj/9EY823gHhHZb/S9MeacEY3sEF26OJdfv7aNmIihPEpAKaWUCl8DZcJHQ//eNRqBDDeH3ca3T5vKL1dtpaHdR1K0a6xDUkoppcbEQMm+RERmhrrze4jILKBmZMMaHjMz4wDYWtnC0skpYxyNUkopNTYGGqD3v/T9ZPls4O6RCWd4zciwkv1bW6vHOBKllFJq7AyU7Ofs26oHMMa8AcwduZCGT1K0i0XjE3n84xI8Pn2anlJKqc+ngZK98yCXHVa+fdpUfP4gD7xXNNahKKWUUmNioGS/Q0TO2LdQRFYAR0zmPH5KCjMy4nhhfRk7qlspa/SMdUhKKaXUqBpogN7NwCsicjGwNlS2CDgWOGukAxtOly8Zzw9e2MjyP1gT9d14ymSaOnxMTo3h8mPzembJU0oppcLRQA/V2S4ic4BLgdmh4veArxtjOkcjuOFy6TG5BIJB/ue1bXh8Ae55p7Bn2U9f3kJqrJuvHJOLzx9kRkYca3c3YrcJHV0BlkxM5ozZ6dhtMuTH7rZ7/WytbCHCaWdGRhwN7T5+8tImLjk6l3nZ8bR5/UQ67by9tabnOQAf7axnYmo0Xzo6lz/9awcCnL8gi5217ZwwJYUolwNjDP6gwRma6acrEGTt7kbWlzQxZVwMeSlRfLSznppWL8nRLuKjnJw3P+uAHhfsDwRx2IcyZYKlpbOLDl+AhCgnNpGe2A4X/kDwgP7v1Ojp/lnrCgSxh+azCATNgD9/gaDhs7ImSuo9TM+IZXp63ID7+LionormDpbPTMcYQ9DA6sI6Jo+LYWpaXxN6Hrw2r5/3CmqZmBrNhJRo/EFDjHvPn1h/IEhRXTuRTjs5SVEA+PxBXI7h+Z0xxlBY00aE005Du4/EKBcfFNYxPyeBuEgHaXERff5+dvgCtHn9/M+qrcMShzo8iTGjP1utiCQAf8E6iTDA1UAB8BSQBxQDFxtjGsX6K303cAbgAa4yxqwbaPuLFi0y+fn5fS5bX9LIO9tqWL2znnGxbsoaO9hY3jxozC67jSWTkllf0khchDVkISnaxcWLsimsaeO97bVMHhfDW1tH5q7EaWmxuBw2NpY3E+2yk50YRUF166DrnTQ1lfk5CWQlRnL3Wzu4cGE2OUlRrN3dyIrZ6Wwoa8Jpt3HspGR++tJm1pU0MSszDpsIXn+Aby2bQn2bj43lzczOjGNOdjzlTZ20e/3c9UYBLZ1ddAX2/AzdfNpUzpiTzri4CF5cX05ClJMTp6TyyoYKNle0kJcSzZ/e3oHHF+DPVyzi+XVlJEW7+NFZMwkaQ5TLQWtnF43tXWQkRFDd0kmk005yjJtA0GC3CV2BII98WMyrGytZX9LEF4/K4toTJjAzI46yxg7e31GLxxvA4wvw8oYKshIiiY908m5BDfNyEiiqbSfKZWdqeiydvgBzsuOZkBLNzIw4cpKiiHBaUz90JyNjDG9vrWHp5BQiXf1PC2GMwesPUtPiJTc5qqd8W1ULqwvrWTIxiaRoF26HnfhIJ1sqWshNjmLd7kZOnpbac0ISDBp8gWBPHN38gSBtXj/tvgBpsW42lDeztriRorp2vnR0DvNyEthU3szfVhdz5tx0cpOisIkQG+Fkc0UzNS1ePiiso6q5k8yECL68OJekaBedXUH+U9zA+KQoTp0+jvd31FLa2MFlx+QiIgSChu6/EyKCTeiJdVN5M6UNHtxOG1PGxdLa6ScrMRJjDMZAc0cXBdWtvFtQw5qiBnbVtXP10gmsKapnW1ULs7PiKWnw0OTZM2nV/JwEOnwBEqOdZMZHEhfpZGN5MxvLmvEFgnt9JwtyE5g8LoYfnDGDCKed0gYP7++oo7MrwEc76/mgsK7f/687zpvNCZNT2FHTRkZ8BCUNHuZkxfPRznrm5SQwISUau02w24QtFS1srWwhLyWKo3ISaff5ebeglh3VrVS1dPJZaXOfv4/zsuNZNiONLx2dw81Pf8rqwvr96lx/0iRuW2E95bOyuYM/vLmdpGg3mQkR/HNzNfNy4pmeHkd1SydtXhfMhG8AACAASURBVH/Pz0VRbRt2m42i2jbKGjto9Pjw+AL9Hm+36emxjIuLoN3rxxjDupKmnmW7f33WWmPMokE3oo44Y5XsHwH+bYz5i4i4gCjgB0CDMeZOEbkNSDTG3BoaN/BNrGR/DHC3MeaYgbY/ULLvS35xAxFOO3ab8HR+KaUNHSydnMxJU1P587+LeOI/pQA47VYL0ecPMjMjjppWL3Vt3v22lxztYvnMNP65pZqGdh8AE1OiqW31khjtoqTBGjdw+5kz+KS4gYz4SC5bkst97+zktU1V3Lx8Cksnp3DuPavxB/f8/0Q4bXR2Bffb376+v2I6bV4/nxQ3sKaoYcjfQ28Om+y17/6Mi3UzeVwMAB/u3P8P2YGyCThstv3+qGfGR9Do6cLlsNHccfCzGYrAYD/y3XVE4KicBGwi5O9uJDnaxe1nzWBbVSuvbazC4wtQ1+Yl1u0gNdZNUV17zzbGxbqZmBpNZnwkL2+o2OuEyGkXMuIje34OwEoKsRFO1u5uJGgMInDKtHGUNnqwidDm9VNU277XNnpvE6yTz+6ft4PV+2csJcaFMVDfxzaH+vMxVKmxbmLcDnaFvsPcpCh8/iBVLXt3Is7PSeDT0iayEiIpb+oYcJsi8MX5WXxhdjo/fGFTz+9q9zaGU0Z8BJ1dARo9XRwzIYnSBg8Vzft3gNoE+vraYt2O0EydB3fXUFZCJFPTYjhmYjIby5rZWtXCjHTr5HxNUT3vFtQC1vfsstto7ezq2ZfTLhw7KYWcxEh+df5cTfZhatSTvYjEAZ8BE02vnYtIAXCyMaZSRDKAd40x00TkwdD7J/at198+DjTZD0V3S8VmEzq7AkQ47XT4AqwvbSQjPpLkGBcNbT68/iBT02IQkZ4u5Davn2iXA1tobEBhTSuTUmP67Fru3Y1e09qJ02YjLtLZM67AHwhS3epld317KNHG9rlut21VLazaUElucjR5yVHsrvdgtwlNHh+ZCZF4/UF217fz8meVLBifwLeWTcEY64/C21tr2FHdyoyMONp9fpKiXazaWEmHL8CyGWkcPzmFhCjnXi3SD3fWs7G8mc9Kmyiub2dKWiw1LZ0smzGOtLgIvF1BTpuZxv/+aweVTVa5xxfgl6u2ctbcDDAQG+HAbrPx+qZKAsZw6vRxbChrZnNFCwlRTpKiXHztxIlMTYslEDQ8t7aMmtZOot0OjIEF4xOxC6TFRXDytHF0BYMYA/GRTkobPKwurOP02ekEgoZ3Cmp5fVMVC8cn0trZxYvry6lv9xEb4aQrENzrxMLlsOHzB3tOBhKjnDR6upibHU+Tp4uSBg9RLjvnzMvkyU9Ke9ZLjnZx5XF5fLSzntgIBxXNHXi7grR7/WQnRuENBKlr9RLpsjM+KYrEaBefFDfQ5OkiIz6CCKcdfzDIpvIWjp2YzNF5iawtaeQLs9KJj3QSF+Hkbx8W09BuJbOZGXFkJUThdtqobOqgvKmD3KRoVsxJJ9rloKiujefXlfd0IVc0dXDMhCQinHY+3FlPU4eP02els66kiY3lzfj8e068upN8Sowbt8NGeVMHd5w3m896JeCESCepsW4aPD4a231MSo3htJlpZCVE9lweK2voIDHauvQTH+nsaa2WNnjISojs+V3x+YNUNHVQXN/O9upWvnbCxJ5kWd3SyQeFdXj9QTaWNVFY08Zxk1JIinaxZGIyCVFOMhMiAXp6J7p/P97fXsvO2jbW7m6krs1LZ1eQzIQIfP4gCVEunl1bttfv0emz0pmYGk1lcycvrC8nPtLJZUtyWTIxmeMmpezV29H793FHTRvPryvD4wswPjmKa46fiE2goLoVh81GjNvB3W9vp6XDT1cgSGyEk+zESJbPTKO21cuivET+ta2G97fXkRLj4rIl46lv9+Hx+kmPjyA3KQp/0OzXCzQU3X/HehMRTfZhqt9kLyJvG2OWicivjTG3DtsOReYDDwFbgHlYg/9uAsqNMQm96jUaYxJF5BXgTmPMB91xAbcaY/L32e51wHUAubm5C3fvPuLm7lGHGWMMmytamDwuhginnd317Wwqb2Hp5GQSogZ+/HJrp5X8jYGpoUsw4agrEDzsxmkMJ38guF8y9QeCBA1h+X+qyT58DTQaP0NETgLOEZEngb1OWwe7bj7IPhcA3zTGfCwidwO3DVC/r5FVfU3M8xDWSQSLFi0a/WsTKuyICLOz4ns+j0+OZnxy9JDWjY1wMiszfvCKR7hwTvRgzbHhsO9fptSRZqBk/2OsJJwN/H6fZQY49SD3WQaUGWM+Dn1+NrSfahHJ6NWNX9Orfk6v9bOBioPct1JKKfW50+8pqjHmWWPMCuA3xphT9nkdbKLHGFMFlIrItFDRMqwu/ZeAK0NlVwL/CL1/CbhCLEuA5oGu1yullFJqb4NO9m6MuUNEzgFODBW9a4x55RD3+03g8dBI/CLgq1gnHk+LyDVACXBRqO4qrJH4hVi33n31EPetlFJKfa4MmuxF5H+AxcDjoaKbRGSpMeb7B7tTY8ynWE/j29eyPuoa4IaD3ZdSSin1eTdosgfOBOYbY4LQc4/8euCgk71SSimlRs9Qh5Um9Hof/kOMlVJKqTAylJb9/wDrReQdrNvgTkRb9UoppdQRYygD9J4QkXeBo7GS/a2hEfVKKaWUOgIMpWVP6Fa3l0Y4FqWUUkqNAH0UlFJKKRXmNNkrpZRSYW7AZC8iNhHZNFrBKKWUUmr4DZjsQ/fWfyYiuaMUj1JKKaWG2VAG6GUAm0XkP0B7d6Ex5pwRi0oppZRSw2Yoyf5nIx6FUkoppUbMUO6zf09ExgNTjDFviUgUYB9sPaWUUkodHgYdjS8iX8Oac/7BUFEW8OJIBqWUUkqp4TOUW+9uAJYCLQDGmB3AuJEMSimllFLDZyjJ3muM8XV/EBEHYEYuJKWUUkoNp6Ek+/dE5AdApIgsB54BXh7ZsJRSSik1XIaS7G8DaoGNwNeBVcDtIxmUUkoppYbPUEbjB0XkEeBjrO77AmOMduMrpZRSR4hBk72InAk8AOzEmuJ2goh83Rjz2kgHp5RSSqlDN5SH6vwOOMUYUwggIpOAVwFN9koppdQRYCjX7Gu6E31IEVAzQvEopZRSapj127IXkfNDbzeLyCrgaaxr9hcBn4xCbEoppZQaBgN145/d6301cFLofS2QOGIRKaWUUmpY9ZvsjTFfHc1AlFJKKTUyhjIafwLwTSCvd32d4lYppZQ6MgxlNP6LwF+xnpoXHK4di4gdyAfKjTFnhU4qngSSgHXA5cYYn4i4gZXAQqAeuMQYUzxccSillFLhbiij8TuNMX8yxrxjjHmv+zUM+74J2Nrr86+BPxhjpgCNwDWh8muARmPMZOAPoXpKKaWUGqKhJPu7ReQnInKsiCzofh3KTkUkGzgT+EvoswCnYk2lC/AIcF7o/bmhz4SWLwvVV0oppdQQDKUbfw5wOVYy7u7GN6HPB+uPwC1AbOhzMtBkjPGHPpcBWaH3WUApgDHGLyLNofp1vTcoItcB1wHk5uYeQmhKKaVUeBlKsv8iMLH3NLeHQkTOwnpQz1oRObm7uI+qZgjL9hQY8xDwEMCiRYv02f1HKF/AR1lrmfVBYHzseOw2O+Vt5cS6YolzxY1tgEopdQQaSrL/DEhg+J6atxQ4R0TOACKAOKyWfoKIOEKt+2ygIlS/DMgBykTEAcQDDQPuoWk3bFsF088YppBHXiAYYGPdRj6p+oSACeAL+PAGvFS2V1LWWkaHvwOn3UmMM4b5qfNx2BzkxOZwau6prKteR1tXG21dbTR7m7GLndy4XOJccWyo3cDult2cNfEsytrKiHBEsDh9MQnuBGo9tWTFZmGToVzN2Z+ny0N9Zz2J7kRiXDEHvH5lWyW+oI9aTy0b6zZS3FLMOyXv0Oht7KkT54rDG/DiDXixiY2J8RNZmrmUsrYydjbtpKq9iuMyj8Mb9LKueh1zU+eSFpVGlCOKC6deyKSESVS1V9HQ2UBtRy3p0ekUNRXR4mvBJjYS3Yn8u/zfxLniWJy+GLvNTpuvDV/QR1ZMFvNT59MV7KK2oxaA1MhU6jvqCRIkxhlDcmTyQX13Sik1mmSwCexE5F1gLtZT87zd5cNx612oZf//QqPxnwGeM8Y8KSIPABuMMfeJyA3AHGPM9SLyJeB8Y8zFA213Uabd5F8XAz9tPtQQ+/RB+QckRSTxTuk7bKvfRnJkMjaxsbZ6LVGOKJZkLqGxs5H6jnqqPdUAOGwOHDYHdrEzO2U2J+ecTFJEEqWtpdR31PP3zX+nqLlor/0IQlZMFgZDSmQKyRHJlLWVsb1x+wHFaxc7ARPoc1l6dDqJ7kQiHZHYxMbc1LnUd9TjsrtYkLaAnNgcHDYHmdGZPLb1MSraKohwRLC+ej07m3futZ0EdwLbGrYxOWEyE+In0BXoorilGLvYCRIkMzqTyQmTmZo0lVW7VrG6fPVesThsDk7MOpGTc04m0hFJVXsVn9V+Rrw7nuzYbLbUb+HN3W/27G9SwiSAnu1MSZxCs7cZT5eHtq62nu/Q7N8RNCxsYmNSwiQcYp0zp0WlEeOKwRvwEu+Oxxfw9fRGGAwT4iaQFJmEL+AjaIJUt1cTMAGyY7OZnjSdcVHjRiTOoarvqCfeHU/QBLGLHbvNPqbxqNEnImuNMYvGOg41/IaS7E/qq3w4RuTvk+wnsufWu/XAZcYYr4hEAI8CR2G16L9kjCnqb5swvMneH/TzUcVHvFv6LoVNhXQGOtlSv6VneWpkKnUddRgMbrubeFc8NR01PclzZvJMoh3RNHmb2Nm8k1Zfa5/7mZwwma/M+ArLxy/HbXfj8XuId8X3+Qc3EAzQ6G1kTeUadjXvYnH6YuLd8djFTnZsNjWeGtp8bZS1lTE9aToJ7gQ+rPiQWFcsMc4Y/l3+bzxdHjKiM1hXsw5fwEd5WznegJfytnLiXHH4Aj46A5377bv7eGenzGZ2ymzy4vKo76ynqKkIX9DHjsYdpEalsr1hO13BLqYmTiUlMoVmbzMtvhZ2t+zGYHDYHFw39zpyYnNw293MS51HckTyoAnGGGMlo171giaIMQa7zU73z3N9Zz3PbX+OzkAn8a54EiMSGR83ns31m5mWOI3EiETsYqcz0EmCOwGA9TXrsYud+s56JsRP4O3db+OwOfAH/cxJnUPQBClpKbF6DpxR7Gjcwa7mXQB0mS7KWsvwB/00e5vp8Hf0e4LVF0E4LvM4qj3VTEmYQltXGzWeGqYlTeP6udeTFJmE0+bEZXftt+7Opp1sa9iGXeysqVyDp8tDnDuOmckzOTbjWNKj0+k9ptUf9PNe2Xu8U/KO9Z1i2NW8i411G3GIA7/xMy5qHBdMuYAoRxQJEQl4ujykRaXRZbr4V8m/2Fq/lar2Kpx2J2lRaTR7m0mOTMYu9p6fo+SIZM6ceCbtXe1sqd9CZ6ATp81JnCuOCEcEy3KXcXLOydR31FPaWkpyZDIlLSU4bU4WpC0g2hmNIHrSMYo02YevQZP9kehgk33QBNneuJ1Paz7FJjby4vL4bf5v2dawjUhHJBnRGVS2V3JS9knkxOawIG0Bx2cdj6fLQ2egk0R3IiKCL+Drs2XU/V2XtZaxpWELnf5OMmMyiXPFMSVxykF3pw8XYwy1HbUkuhNB4J2SdyhqLiItKo31Nes5bfxpnJh9Iv6gH4dt4CtAHf4OPF2e/bq5W3wtbKjdwNTEqWPekh1J/qAfX8BHfnU+s1NmEzRBChoK6PR34vF7yInN6TlBq2yv5IPyD3ih8AWavc247W4cNgcR9giavE17nTSkR6czJWEKn9Z+ijGGWFcsle2V++2/O2mDdSnEYXOQHJlMRVsFvoCPrmAXEfYIEiMSey6RLM1cSlJEEp2BTtZVr6OgsaDf45ubOpejUo+i0dtIaWsphU2FZMdkkxKZAkBmTCZrKtewu2U3ANMSpyEi7GjcgV3s+IKDDwGyi50oZxRdgS6yY7Op66hjUsIkTsk5hRpPDU3eJhLdidhsNpakLyHCEYGI8O+yf1PRXkGcK44rZ11JVkzWoPtSFk324WsoLftW9gyIcwFOoN0Yc9iOlDqQZN/ia+GpbU/x8KaH+2yNxbpiufXoWzlt/GlEO6NHKmSlCJogNZ4a0qPTMcZgMOxo3MEnVZ/wdsnb5FfnkxKZgjGGeHc8ubG5NPuaiXXFct7k80iOSKatq43js44naII8t/05iluKWVu9lsyYTDr9nWTHZuO2u5mSOIUv5H2BSEdkn7EYY9jeuJ3UqFRqPDU4xEGjt5EaTw2n5JxClDNq0OMJBAO0+lrpCnaRGpXac4w2sbGtYRuFTYW8X/Y+s5JnMTF+InUddaRFpeE3fj6r/YwWbwulraWsq1lHTmwOca44ytvKqWyv7OlxGYzL5uKE7BPY2bSTifETsdvsZEZn4rA5iHZG09rVSou3paeXp8PfQYwzhoAJUNleSYQ9glNyTyE9yrpM1X2SNtDdv3Uddfyn8j8cn338kAaUegNeWn2t+IN+0qPTB60/kjTZh68DbtmLyHnAYmPMD0YmpEM3lGTvC/i499N7eXjTw4DVjXpKzikszVrKvNR51HhqeKP4Df57/n9ry0Cpw4Qxhh1NO0iNTCUxIpEOfwfN3mY+KP+AaGc0QRMkwZ3A0qylbK7fzONbHmdN5RpqO2qxix2X3UWHv6PPbXef+HT4O4iwR5AXn0dDZwM1nr3HJp+UfRLJkcmUt5VT1lpGbmwu4+PGU9tRS42nhi31W3oaDRnRGUyMn0hihDUuZlriNGw2Gy6bi6r2KlbtWtUzVkcQ5qTO4ei0o3E73CS5kxARUiNTcdvdxLpiiXZFkxyRTJwrbsATjoOlyT58HVQ3voisMcYsGYF4hsVgyd4X8HHzuzfzftn7RDmi+NXxv+LU3FNH5JdHKXV42VC7AX/Qj9PmJC8+j/U168mIzmBK4pT96gaCAQoaCyhrLcMX9LG6fDWvFL3SszzaGU1SRBItvhbcNjcAx2Qcw6yUWdR31LOzaSeb6zdT31Hfc1llX+dMOoeM6IyeS1yb6zcPegwOcTA5cTIzkmaQEZ3Bl6d/mXh3PKWtpWTHZh/0JUFN9uFrKN345/f6aAMWAScZY44dycAOxUDJ3hfwce0/r2V9zXpuOfoWLp52MW67ewyiVEodaYImyAs7XsBld7Esdxkuu2vQ8Svdl2S8AS/tXe1UtlVS2FRITmwOdpudo8YdtVf9Zq/1d6sr2EWnv5Oi5iJ2Ne8iOTK553bXlwpfwml39gwQ3dfUxKmckHUCtR217GjcwfFZx7MwbSHHZR43YKNGk334Gkqy/1uvj36gGPizMWa47rsfdgMl+0c2P8Jd+Xfxs+N+xvlTzu9jbaWUOjJUtVfxatGrtPpaqWivoL6jnsKmQho69zyKJNIR2XPpIi8uj/TodOaPm09GdAbHZR7XM07AGIPNZtNkH6YGfajOET2vfTAItj3dWS2+Fv688c8szVyqiV4pdcRLj07nmjnX7FUWNEHautoQhE5/J4kRiTR5m3i16FVe2/UaO5t2sqZyDQCxzlimJ0/vubtBha9+k72I/HiA9Ywx5o4RiGd4eeohJrXn432f3keLt4WbFtw0hkEppdTIsYmt5y6AWJc1/UhKZApXzrqSK2ddCUB5WzkFDQU8t+M5ChoKSI5MZk7KHFazut/tqiPbQC379j7KorGmnE0GDv9kv+k5WHI9ANsatvHEtie4eNrFzEieMcaBKaXU2MmKySIrJotTc/eez+xO7hyjiNRI6zfZG2N+1/1eRGKx5p//KtZT7n7X33qHldptPW8f3vQwsa5YvnnUN8cwIKWUUmr0DXjNXkSSgO8AX8GaU36BMaZxoHUOKx3WIBVjDJ9UfcLSzKXEu+PHOCillFJqdA10zf63wPlY08bOMca0jVpUw8VjJfui5iLqOupYnL54jANSSimlRt9AT174LpAJ3A5UiEhL6NUqIi2jE94h6rBGl3aPPF2Sedg+B0gppZQaMQNdsx/bWVmGQ/0O8DSwqmgVeXF5+thbpZRSn0tHfkLvz6Rl4O+ktTyfDXUbOHvS2WMdkVJKKTUmwjfZx2YA8FntpwBMT5o+ltEopZRSYyZ8k320Na/2S9Ufk+BOYGHawjEOSCmllBobYZ/sCzxVHDXuKJ2LXiml1OdW+CZ7m5OutFns7mpmclT6WEejlFJKjZnwTfZio+KU2wiIML6+ZKyjUUoppcZMGCd7oSQmEYDcDc9B/c4xDkgppZQaG+Gb7IGSVqtFn9Plh/9dMMbRKKWUUmMjfJO92ChtLSXKEUVyMGiVNRaPaUhKKaXUWAjjZC+UtJSQG5eLXP1Pq2zjM2Mbk1JKKTUGBpz17sgmlLSWMDVxKuQeAxNOhHWPwvHfBVv4nuMopdSAjIGyfNj6ElR+an1OmQLTzxrryNQICttkHwDK28pZlrvMKlh4FTx7NXx0Dyz91liGppRSI8fvhY5GiO11y3EwCO/+CvL/Bp66/dcp/jfkPzx6MapRN+rJXkRygJVAOhAEHjLG3C0iScBTQB5QDFxsjGkUEQHuBs4APMBVxph1g+2nNuDBH/TvmfxmxrmQNAne/BHEZcKcC4f/4JRS4e/930J7Pay4c/T26WmAyEQQgYAfit6F1GlQvQmqNkFrpfWI8OlnwCvfgdI1kDTRSvp+H3S179lWwniYvMyaP2TcDEjIhZot8OI3gA9H75jUqBqLlr0f+K4xZp2IxAJrReRN4CrgbWPMnSJyG3AbcCuwApgSeh0D3B/6d0AVPmsW3syYTKvA7oAvPwH3LoaXvw3TzgBX1HAfm1LqSBMMWklUZP9lLRVWa3jBFRCfDZueg3/9wlrWVg15x8OMs8EdB86I/ddvLoP374LWKshaCMfdaCXnVf8P7C6YdZ7VAjcGujqg5CNo3A3ps0FsVsPkrZ9BsMvaXt4J1kDj5tK+j+WdX+x531C097LcY+HCv0Fcxv7rZcyD/14N3+jjO1BhQYwxYxuAyD+Ae0Kvk40xlSKSAbxrjJkmIg+G3j8Rql/QXa+/bS7KtJufPnYr3y99mX+c9w8mxk/cs7DoPVh5jvX+6jcgV+e4V+qI11AEnc2QMb/vpO3zWMnT4baWf/oEbHkRGnZBXYFVxx0PE06w/kb4Wg9s/84o6PJY77/8pNVafuVmKP340I6rL0kTwea0Givjl1onGzFp0LATCl6DycutkxBXNJSvg4APMueDM3LQTYvIWmPMouEPWo21Mb1mLyJ5wFHAx0BadwIPJfxxoWpZQO/T2LJQ2V7JXkSuA64DWJhho6KrGYCM6H3OYieeBCf/wLp+9fAXICIBpiyHxV+HnKOhswUi4ob3QJU6UD4PmCB4W6BkDez+EHa8AYl51h/7xt2w4HKr6zb3WPC1gbcVmsutn2Owkt/uD63WYtYiKwG4Y63yjkZrUNZAmsuthFJXaHUZN+22klrypBE//L2011vJ3B0L46ZD8WorcXnqYd0jVsKu3rSnftocqwWdMN5K8K2V8PK3rOMGK1F2t5R78zbDtlf2L4/Lso6/+AOYeS781y+tk4a6HZD/1z3fZ8lHVv0nvrT3+ifeAktvsq6Jv/kjcETCefdZJwIfPwBTvgCLrrb+DyeeDOVrrZ6AiSdZJx7Tz+yZ62NASRNg8ml7l2XrBGDKMmYtexGJAd4DfmmMeV5EmowxCb2WNxpjEkXkVeB/jDEfhMrfBm4xxqztb9uLMu3mrAcv5R1PCe9d8l7fld78Caz+48BBTjgJco6xfgHTZll/OD5vJwLeVnDFQEs5xGYO/U6GYBAC3iG1Jo4YxkBnk3XtdDAlH0P1RqtldfS1kD4HKj+DzAVQ9gkE/Va3rs1uJYp/3g4Vn1qtzOQpUL/DSkoiVsvsQESnwriZsKufn/3eco+zuponngiLroGCVeBts/a75r7+10ufa+2juRSW32ElsvgssDmspJy9GDY9C1tfhuTJsOI3VvdxU6nV4mwogg1PWycQOcdYJ9yx6bDmASuht9VYCW/3h3ta3gCTToWd/+o7nqQJ0FIJZf8Z/LhTpsKX/s86IajaEDqB2gUJedBUDIkTrHoOtxXvUHS2QMV62P4GxKSC3Q2Lr7MuIfano3FoP0+jRFv24WtMkr2IOIFXgDeMMb8PlfV0zw9HN/4J953PLrp48bwX+w8kGOD/t3f/QVaV9x3H3193l91l2WX5oQgsAupKRKyRIOAPRI0/EDMh6diJNNNYY8Z2aqaa/khM40xsO52ME0cTq0lqTPzRdMSEOA1JLYYoThJjFKQWsPxGRBQDCCggAst++8f3ud67uAus7nL2nvt5zdy5nHPPvTznuc+e73m+z3PuYfV82LMtek9rftn5TNX3Cn4c1A+Gtndh+ldiCGD3ljgw1tRHT2z42dAwJLY/8G4czKtqip/Rtg/mfzXeO+Hq7C4D3L8nxgx3b4EtK+Cpf44ynXhmHPiGTYA5fxqzdAtOPBMGnBi9zXNvhNM/2TFl2t4Or/wW3lgGLzwI21bDBV+CnRvjvU0j4+C+eWnU4fk3RY+zujbqpbMxz67KfrQH4O5a+V+wZ2sEsp0b4afXQ00DzPhGBJkV8+J7GzQmDtSLvg/jPwVbV8YDYp9eW9zxc2sHRs/xcMZM61jfLedEevacL0TPsqpf1Nv+d6Ku/ndO9FpX/DzKfM4XYNH9HT9z7IUw8dq41MqOg6WPHr6Nlxo6LrIEyx+LFPUffSYyYRt/F99xT6iqjZPCrtQNjIlkaxZ0nlq/7J+iPUz/SvytAbz8mzhRMIvgO3IijL4ggqq3H307q0AK9vl1zIN9ml3/ELDd3W8uWf9N4M2SCXqD3f3LZnYV8EViNv4U4G53n3y4/2PSiCo/+56r2FvXxI9m/qh7BWxvjwNudW2M/725BpY8DCsfh+ZRxVTdkQyb0DG1OGpKpO1Kx/bqmqOnCHDdfBh91UZ+8QAAC+lJREFUbvyG/6CxcRKwe2ucKFTXFQ9Q+3bDsh/D1tWRhq2qic8cNzPSvCedB43DYtu9O6Lc656K3s7wsyIlu3VFvF7bFIH7SJpGRs++MzPviMC3bG705Nrbjq5+Co6rLr5n7HQ449ORihwwDJbOiV7nkNY4cXj+34qp2EtviyDmHpOddm6MHm3DkEhtL34ggmP9IBhwQmRkzvjj+GGlDb+FqX8FbXvjxOPA3ggMrzxz5PKeNiNmQre923F9Vb+OPfDm0bEfo8+Dn91Y3P4jn4iTqdpGWPgvsa7heJh1L5x2RfH97e3dy6IUJpjt+gOs/VV81ydO6Po9bfvh7U2RUm5vi+/uzXUR0J+9J06+Pju3617pO9vjsWJeBNkDe6ON798T392ODXGCcuKZsHVVzGBf/UQE7Cl/Gd/Lwf1w8a2RTXjhgXQivRfO/rP4+6lvjvIV6sE9HmbxN9o8GgaNPro6kqOiYJ9fWQT7C4DfAMuIS+8A/oEYt/8xcBKwEfgTd9+eTg7uAWYQl95d5+6L3/fBJSaNqPLWb1/KwOaxfO+y7/XsDrhHOvbt1+K5aWQctF7+daQm92yD3W8c+XMmfi6yCdtWv/+1kR+Ds2ZHECtomRwH0i0vHfmz6wfH+F+H9K8B6bseMy1ORFrSOVPdQJh0HfzqtjghOfWyCEatl0U5zGK/d70RvcKD+2OC06Lvd/J/D4re1riZcZDftCjGHp/9DuzdDjNujwzKkofjRGdXSYKmq7HUY6WpJSZWDT01hi9GTYGzromTprmfj0zEGZ+OwLbuqej5DjklMkSFXmV7W/Sym0YUP3f7+qi7lnM6ZnkKCgFMJGMK9vmV+Wz83jBpRJUPv3Ma44ZP4o7pd2RTiNdfjNR+w/Fxic7Pb4Jpfxe99wN7o7cJ0Svbvi5m7m5f33UPuqklguWBd2K8ccpfxInF09/ougz1g2PM8ITTY2JRYQz35Is+/P65x2VJq+fHmOcplxSHALpj58ZIkzcMSb/stSh6pm+9FunXhqGRzTiuGkZNjiBqVbDkQXjuvhjPvfDvY4LT1pUxO/mVZyID0Hp51Gldc9Tr8sfiJGvb6jjZGXxKBPS6pvjMwvCLSIVSsM+v3Ab7xm9OZvrYK7jtvNuyLk73uEcAHdgSQX390zHuWlMfcwDe2RavFbQfjKDbryF6h5teiJTxaTMOPzFIROQQCvb5ldtosPvgPgbUDMi6GN1nBuOuLC6XjuPW1HUM9BDp4+ZRxeXWQy69ERGRipfLYO/APj/AgH5lGOxFRER6WC5v/9aeJjs19mvMuCQiIiLZy2WwP5ieG2p66VpsERGRMpLLYF+4nq+xRj17ERGRXAb7gymNrzF7ERGRnAb7Qs++LGfji4iI9LBcB/v6PN2ERURE5APKZ7BPafz+1f0zLomIiEj28hns03N9tXr2IiIiuQ726tmLiIjkNdgbVFsVNZ3dYUxERKTC5DPYY9Qf1y/rYoiIiPQJuQz2DvSvqs26GCIiIn1CLoN9O1BfpZ69iIgI5DXYm6lnLyIikuQz2AP1CvYiIiJAjoN9/6q6rIshIiLSJ+Qz2JvG7EVERAryGewx9exFRESSnAZ7jdmLiIgU5DPYm8bsRURECnIZ7B1orNHv4ouIiEBOgz1AY3VD1kUQERHpE8om2JvZDDNbZWZrzeyWI23fqDveiYiIAGUS7M2sCrgXuBIYD8w2s/GHe88ApfFFRESAMgn2wGRgrbuvd/f9wBxg1uHe0FijNL6IiAiUT7AfCbxasrwprXuPmd1gZovNbPGAduP4ppOOaQFFRET6qnIJ9tbJOu+w4H6fu09y90mjjx/PyJapx6hoIiIifVu5BPtNwKiS5Rbg9YzKIiIiUlbKJdgvAlrNbKyZ9QOuAeZlXCYREZGyUJ11AY6Gu7eZ2ReBJ4Aq4Ifu/lLGxRIRESkLZRHsAdz9ceDxrMshIiJSbsoljS8iIiIfkIK9iIhIzinYi4iI5JyCvYiISM6Zux95qzJjZruAVVmXo48YCmzLuhB9hOqiSHVRpLooGufujVkXQnpe2czG76ZV7j4p60L0BWa2WHURVBdFqosi1UWRmS3OugzSO5TGFxERyTkFexERkZzLa7C/L+sC9CGqiyLVRZHqokh1UaS6yKlcTtATERGRorz27EVERCRRsBcREcm53AV7M5thZqvMbK2Z3ZJ1eXqbmY0ys4VmtsLMXjKzm9L6wWa2wMzWpOdBab2Z2d2pfpaa2cRs96BnmVmVmf2Pmf0iLY81s+dSPTyabpGMmdWm5bXp9TFZlrunmVmzmc01s5WpbZxbwW3iS+lvY7mZPWJmdZXSLszsh2a2xcyWl6zrdjsws2vT9mvM7Nos9kU+nFwFezOrAu4FrgTGA7PNbHy2pep1bcDfuvvpwFTgxrTPtwBPunsr8GRahqib1vS4AfjusS9yr7oJWFGyfDtwV6qHHcD1af31wA53PxW4K22XJ98G5rv7R4CziDqpuDZhZiOBvwYmufsE4hbZ11A57eJBYMYh67rVDsxsMPB1YAowGfh64QRBykeugj3RENe6+3p33w/MAWZlXKZe5e6b3X1J+vcu4qA+ktjvh9JmDwGfSv+eBTzs4fdAs5kNP8bF7hVm1gJcBdyflg24BJibNjm0Hgr1Mxf4eNq+7JlZE3Ah8AMAd9/v7jupwDaRVAP1ZlYN9Ac2UyHtwt1/DWw/ZHV328EVwAJ33+7uO4AFvP8EQvq4vAX7kcCrJcub0rqKkFKOZwPPAcPcfTPECQFwQtosz3X0LeDLQHtaHgLsdPe2tFy6r+/VQ3r9rbR9HpwMbAUeSEMa95tZAxXYJtz9NeAOYCMR5N8CXqAy20VBd9tBbttHJclbsO/sDLwiri00swHAT4Gb3f3tw23aybqyryMz+wSwxd1fKF3dyaZ+FK+Vu2pgIvBddz8b2EMxVduZ3NZFSjfPAsYCI4AGIl19qEpoF0fS1b5Xcp3kRt6C/SZgVMlyC/B6RmU5Zsyshgj0/+Huj6XVfyikYtPzlrQ+r3V0PvBJM9tADN9cQvT0m1P6Fjru63v1kF4fyPvTneVqE7DJ3Z9Ly3OJ4F9pbQLgUuBld9/q7geAx4DzqMx2UdDddpDn9lEx8hbsFwGtaaZtP2IizryMy9Sr0njiD4AV7n5nyUvzgMKs2WuBn5Ws/1yaeTsVeKuQ0itn7v5Vd29x9zHE9/6Uu38WWAhcnTY7tB4K9XN12j4XvRV3fwN41czGpVUfB/6PCmsTyUZgqpn1T38rhbqouHZRorvt4AngcjMblDIll6d1Uk7cPVcPYCawGlgHfC3r8hyD/b2ASKktBV5Mj5nEOOOTwJr0PDhtb8QVC+uAZcQs5cz3o4fr5CLgF+nfJwPPA2uBnwC1aX1dWl6bXj8563L3cB18FFic2sV/AoMqtU0A/wisBJYD/w7UVkq7AB4h5iocIHro13+QdgB8PtXJWuC6rPdLj+4/9HO5IiIiOZe3NL6IiIgcQsFeREQk5xTsRUREck7BXkREJOcU7EVERHJOwV6kh5jZ19Ld1Zaa2YtmNsXMbjaz/lmXTUQqmy69E+kBZnYucCdwkbvvM7OhQD/gd8T1ytsyLaCIVDT17EV6xnBgm7vvA0jB/Wri99gXmtlCADO73MyeNbMlZvaTdE8DzGyDmd1uZs+nx6lZ7YiI5I+CvUjP+CUwysxWm9l3zGy6u99N/Ib4xe5+cert3wpc6u4TiV+4+5uSz3jb3ScD9xC/6y8i0iOqj7yJiByJu+82s48B04CLgUfN7NA7zU0FxgPPpFuk9wOeLXn9kZLnu3q3xCJSSRTsRXqIux8EngaeNrNlFG82UmDAAnef3dVHdPFvEZEPRWl8kR5gZuPMrLVk1UeBV4BdQGNa93vg/MJ4fLoT22kl7/lMyXNpj19E5ENRz16kZwwA/tXMmoE24u5gNwCzgf82s81p3P7PgUfMrDa971biLo0AtWb2HHES3lXvX0Sk23TpnUgfYGYb0CV6ItJLlMYXERHJOfXsRUREck49exERkZxTsBcREck5BXsREZGcU7AXERHJOQV7ERGRnPt/5PtiRD7CCSQAAAAASUVORK5CYII=\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "ax = model_out.plot()\n", - "ax.set_title(\"Citizen Condition Over Time\")\n", - "ax.set_xlabel(\"Step\")\n", - "ax.set_ylabel(\"Number of Citizens\")\n", - "_ = ax.legend(bbox_to_anchor=(1.35, 1.025))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/examples/epstein_civil_violence/Readme.md b/examples/epstein_civil_violence/Readme.md deleted file mode 100644 index 2e715b33b99..00000000000 --- a/examples/epstein_civil_violence/Readme.md +++ /dev/null @@ -1,33 +0,0 @@ -# Epstein Civil Violence Model - -## Summary - -This model is based on Joshua Epstein's simulation of how civil unrest grows and is suppressed. Citizen agents wander the grid randomly, and are endowed with individual risk aversion and hardship levels; there is also a universal regime legitimacy value. There are also Cop agents, who work on behalf of the regime. Cops arrest Citizens who are actively rebelling; Citizens decide whether to rebel based on their hardship and the regime legitimacy, and their perceived probability of arrest. - -The model generates mass uprising as self-reinforcing processes: if enough agents are rebelling, the probability of any individual agent being arrested is reduced, making more agents more likely to join the uprising. However, the more rebelling Citizens the Cops arrest, the less likely additional agents become to join. - -## How to Run - -To run the model interactively, run ``EpsteinCivilViolenceServer.py`` in this directory. e.g. - -``` - $ python EpsteinCivilViolenceServer.py -``` - -Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. - -## Files - -* ``EpsteinCivilViolence.py``: Core model and agent code. -* ``EpsteinCivilViolenceServer.py``: Sets up the interactive visualization. -* ``Epstein Civil Violence.ipynb``: Jupyter notebook conducting some preliminary analysis of the model. - -## Further Reading - -This model is based adapted from: - -[Epstein, J. “Modeling civil violence: An agent-based computational approach”, Proceedings of the National Academy of Sciences, Vol. 99, Suppl. 3, May 14, 2002](http://www.pnas.org/content/99/suppl.3/7243.short) - -A similar model is also included with NetLogo: - -Wilensky, U. (2004). NetLogo Rebellion model. http://ccl.northwestern.edu/netlogo/models/Rebellion. Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. diff --git a/examples/epstein_civil_violence/epstein_civil_violence/agent.py b/examples/epstein_civil_violence/epstein_civil_violence/agent.py deleted file mode 100644 index c270a9700ed..00000000000 --- a/examples/epstein_civil_violence/epstein_civil_violence/agent.py +++ /dev/null @@ -1,186 +0,0 @@ -import math - -import mesa - - -class Citizen(mesa.Agent): - """ - A member of the general population, may or may not be in active rebellion. - Summary of rule: If grievance - risk > threshold, rebel. - - Attributes: - unique_id: unique int - x, y: Grid coordinates - hardship: Agent's 'perceived hardship (i.e., physical or economic - privation).' Exogenous, drawn from U(0,1). - regime_legitimacy: Agent's perception of regime legitimacy, equal - across agents. Exogenous. - risk_aversion: Exogenous, drawn from U(0,1). - threshold: if (grievance - (risk_aversion * arrest_probability)) > - threshold, go/remain Active - vision: number of cells in each direction (N, S, E and W) that agent - can inspect - condition: Can be "Quiescent" or "Active;" deterministic function of - greivance, perceived risk, and - grievance: deterministic function of hardship and regime_legitimacy; - how aggrieved is agent at the regime? - arrest_probability: agent's assessment of arrest probability, given - rebellion - - """ - - def __init__( - self, - unique_id, - model, - pos, - hardship, - regime_legitimacy, - risk_aversion, - threshold, - vision, - ): - """ - Create a new Citizen. - Args: - unique_id: unique int - x, y: Grid coordinates - hardship: Agent's 'perceived hardship (i.e., physical or economic - privation).' Exogenous, drawn from U(0,1). - regime_legitimacy: Agent's perception of regime legitimacy, equal - across agents. Exogenous. - risk_aversion: Exogenous, drawn from U(0,1). - threshold: if (grievance - (risk_aversion * arrest_probability)) > - threshold, go/remain Active - vision: number of cells in each direction (N, S, E and W) that - agent can inspect. Exogenous. - model: model instance - """ - super().__init__(unique_id, model) - self.breed = "citizen" - self.pos = pos - self.hardship = hardship - self.regime_legitimacy = regime_legitimacy - self.risk_aversion = risk_aversion - self.threshold = threshold - self.condition = "Quiescent" - self.vision = vision - self.jail_sentence = 0 - self.grievance = self.hardship * (1 - self.regime_legitimacy) - self.arrest_probability = None - - def step(self): - """ - Decide whether to activate, then move if applicable. - """ - if self.jail_sentence: - self.jail_sentence -= 1 - return # no other changes or movements if agent is in jail. - self.update_neighbors() - self.update_estimated_arrest_probability() - net_risk = self.risk_aversion * self.arrest_probability - if ( - self.condition == "Quiescent" - and (self.grievance - net_risk) > self.threshold - ): - self.condition = "Active" - elif ( - self.condition == "Active" and (self.grievance - net_risk) <= self.threshold - ): - self.condition = "Quiescent" - if self.model.movement and self.empty_neighbors: - new_pos = self.random.choice(self.empty_neighbors) - self.model.grid.move_agent(self, new_pos) - - def update_neighbors(self): - """ - Look around and see who my neighbors are - """ - self.neighborhood = self.model.grid.get_neighborhood( - self.pos, moore=False, radius=1 - ) - self.neighbors = self.model.grid.get_cell_list_contents(self.neighborhood) - self.empty_neighbors = [ - c for c in self.neighborhood if self.model.grid.is_cell_empty(c) - ] - - def update_estimated_arrest_probability(self): - """ - Based on the ratio of cops to actives in my neighborhood, estimate the - p(Arrest | I go active). - - """ - cops_in_vision = len([c for c in self.neighbors if c.breed == "cop"]) - actives_in_vision = 1.0 # citizen counts herself - for c in self.neighbors: - if ( - c.breed == "citizen" - and c.condition == "Active" - and c.jail_sentence == 0 - ): - actives_in_vision += 1 - self.arrest_probability = 1 - math.exp( - -1 * self.model.arrest_prob_constant * (cops_in_vision / actives_in_vision) - ) - - -class Cop(mesa.Agent): - """ - A cop for life. No defection. - Summary of rule: Inspect local vision and arrest a random active agent. - - Attributes: - unique_id: unique int - x, y: Grid coordinates - vision: number of cells in each direction (N, S, E and W) that cop is - able to inspect - """ - - def __init__(self, unique_id, model, pos, vision): - """ - Create a new Cop. - Args: - unique_id: unique int - x, y: Grid coordinates - vision: number of cells in each direction (N, S, E and W) that - agent can inspect. Exogenous. - model: model instance - """ - super().__init__(unique_id, model) - self.breed = "cop" - self.pos = pos - self.vision = vision - - def step(self): - """ - Inspect local vision and arrest a random active agent. Move if - applicable. - """ - self.update_neighbors() - active_neighbors = [] - for agent in self.neighbors: - if ( - agent.breed == "citizen" - and agent.condition == "Active" - and agent.jail_sentence == 0 - ): - active_neighbors.append(agent) - if active_neighbors: - arrestee = self.random.choice(active_neighbors) - sentence = self.random.randint(0, self.model.max_jail_term) - arrestee.jail_sentence = sentence - if self.model.movement and self.empty_neighbors: - new_pos = self.random.choice(self.empty_neighbors) - self.model.grid.move_agent(self, new_pos) - - def update_neighbors(self): - """ - Look around and see who my neighbors are. - """ - self.neighborhood = self.model.grid.get_neighborhood( - self.pos, moore=False, radius=1 - ) - self.neighbors = self.model.grid.get_cell_list_contents(self.neighborhood) - self.empty_neighbors = [ - c for c in self.neighborhood if self.model.grid.is_cell_empty(c) - ] diff --git a/examples/epstein_civil_violence/epstein_civil_violence/model.py b/examples/epstein_civil_violence/epstein_civil_violence/model.py deleted file mode 100644 index 02d92c39318..00000000000 --- a/examples/epstein_civil_violence/epstein_civil_violence/model.py +++ /dev/null @@ -1,142 +0,0 @@ -import mesa - -from .agent import Cop, Citizen - - -class EpsteinCivilViolence(mesa.Model): - """ - Model 1 from "Modeling civil violence: An agent-based computational - approach," by Joshua Epstein. - http://www.pnas.org/content/99/suppl_3/7243.full - Attributes: - height: grid height - width: grid width - citizen_density: approximate % of cells occupied by citizens. - cop_density: approximate % of cells occupied by cops. - citizen_vision: number of cells in each direction (N, S, E and W) that - citizen can inspect - cop_vision: number of cells in each direction (N, S, E and W) that cop - can inspect - legitimacy: (L) citizens' perception of regime legitimacy, equal - across all citizens - max_jail_term: (J_max) - active_threshold: if (grievance - (risk_aversion * arrest_probability)) - > threshold, citizen rebels - arrest_prob_constant: set to ensure agents make plausible arrest - probability estimates - movement: binary, whether agents try to move at step end - max_iters: model may not have a natural stopping point, so we set a - max. - - """ - - def __init__( - self, - width=40, - height=40, - citizen_density=0.7, - cop_density=0.074, - citizen_vision=7, - cop_vision=7, - legitimacy=0.8, - max_jail_term=1000, - active_threshold=0.1, - arrest_prob_constant=2.3, - movement=True, - max_iters=1000, - ): - super().__init__() - self.width = width - self.height = height - self.citizen_density = citizen_density - self.cop_density = cop_density - self.citizen_vision = citizen_vision - self.cop_vision = cop_vision - self.legitimacy = legitimacy - self.max_jail_term = max_jail_term - self.active_threshold = active_threshold - self.arrest_prob_constant = arrest_prob_constant - self.movement = movement - self.max_iters = max_iters - self.iteration = 0 - self.schedule = mesa.time.RandomActivation(self) - self.grid = mesa.space.Grid(width, height, torus=True) - model_reporters = { - "Quiescent": lambda m: self.count_type_citizens(m, "Quiescent"), - "Active": lambda m: self.count_type_citizens(m, "Active"), - "Jailed": self.count_jailed, - } - agent_reporters = { - "x": lambda a: a.pos[0], - "y": lambda a: a.pos[1], - "breed": lambda a: a.breed, - "jail_sentence": lambda a: getattr(a, "jail_sentence", None), - "condition": lambda a: getattr(a, "condition", None), - "arrest_probability": lambda a: getattr(a, "arrest_probability", None), - } - self.datacollector = mesa.DataCollector( - model_reporters=model_reporters, agent_reporters=agent_reporters - ) - unique_id = 0 - if self.cop_density + self.citizen_density > 1: - raise ValueError("Cop density + citizen density must be less than 1") - for (contents, x, y) in self.grid.coord_iter(): - if self.random.random() < self.cop_density: - cop = Cop(unique_id, self, (x, y), vision=self.cop_vision) - unique_id += 1 - self.grid[x][y] = cop - self.schedule.add(cop) - elif self.random.random() < (self.cop_density + self.citizen_density): - citizen = Citizen( - unique_id, - self, - (x, y), - hardship=self.random.random(), - regime_legitimacy=self.legitimacy, - risk_aversion=self.random.random(), - threshold=self.active_threshold, - vision=self.citizen_vision, - ) - unique_id += 1 - self.grid[x][y] = citizen - self.schedule.add(citizen) - - self.running = True - self.datacollector.collect(self) - - def step(self): - """ - Advance the model by one step and collect data. - """ - self.schedule.step() - # collect data - self.datacollector.collect(self) - self.iteration += 1 - if self.iteration > self.max_iters: - self.running = False - - @staticmethod - def count_type_citizens(model, condition, exclude_jailed=True): - """ - Helper method to count agents by Quiescent/Active. - """ - count = 0 - for agent in model.schedule.agents: - if agent.breed == "cop": - continue - if exclude_jailed and agent.jail_sentence: - continue - if agent.condition == condition: - count += 1 - return count - - @staticmethod - def count_jailed(model): - """ - Helper method to count jailed agents. - """ - count = 0 - for agent in model.schedule.agents: - if agent.breed == "citizen" and agent.jail_sentence: - count += 1 - return count diff --git a/examples/epstein_civil_violence/epstein_civil_violence/portrayal.py b/examples/epstein_civil_violence/epstein_civil_violence/portrayal.py deleted file mode 100644 index 80134adcc79..00000000000 --- a/examples/epstein_civil_violence/epstein_civil_violence/portrayal.py +++ /dev/null @@ -1,33 +0,0 @@ -from .agent import Citizen, Cop - -COP_COLOR = "#000000" -AGENT_QUIET_COLOR = "#0066CC" -AGENT_REBEL_COLOR = "#CC0000" -JAIL_COLOR = "#757575" - - -def citizen_cop_portrayal(agent): - if agent is None: - return - - portrayal = { - "Shape": "circle", - "x": agent.pos[0], - "y": agent.pos[1], - "Filled": "true", - } - - if isinstance(agent, Citizen): - color = ( - AGENT_QUIET_COLOR if agent.condition == "Quiescent" else AGENT_REBEL_COLOR - ) - color = JAIL_COLOR if agent.jail_sentence else color - portrayal["Color"] = color - portrayal["r"] = 0.8 - portrayal["Layer"] = 0 - - elif isinstance(agent, Cop): - portrayal["Color"] = COP_COLOR - portrayal["r"] = 0.5 - portrayal["Layer"] = 1 - return portrayal diff --git a/examples/epstein_civil_violence/epstein_civil_violence/server.py b/examples/epstein_civil_violence/epstein_civil_violence/server.py deleted file mode 100644 index 6b835bd2b14..00000000000 --- a/examples/epstein_civil_violence/epstein_civil_violence/server.py +++ /dev/null @@ -1,54 +0,0 @@ -import mesa - -from .model import EpsteinCivilViolence -from .agent import Citizen, Cop - - -COP_COLOR = "#000000" -AGENT_QUIET_COLOR = "#0066CC" -AGENT_REBEL_COLOR = "#CC0000" -JAIL_COLOR = "#757575" - - -def citizen_cop_portrayal(agent): - if agent is None: - return - - portrayal = { - "Shape": "circle", - "x": agent.pos[0], - "y": agent.pos[1], - "Filled": "true", - } - - if type(agent) is Citizen: - color = ( - AGENT_QUIET_COLOR if agent.condition == "Quiescent" else AGENT_REBEL_COLOR - ) - color = JAIL_COLOR if agent.jail_sentence else color - portrayal["Color"] = color - portrayal["r"] = 0.8 - portrayal["Layer"] = 0 - - elif type(agent) is Cop: - portrayal["Color"] = COP_COLOR - portrayal["r"] = 0.5 - portrayal["Layer"] = 1 - return portrayal - - -model_params = dict( - height=40, - width=40, - citizen_density=0.7, - cop_density=0.074, - citizen_vision=7, - cop_vision=7, - legitimacy=0.8, - max_jail_term=1000, -) - -canvas_element = mesa.visualization.CanvasGrid(citizen_cop_portrayal, 40, 40, 480, 480) -server = mesa.visualization.ModularServer( - EpsteinCivilViolence, [canvas_element], "Epstein Civil Violence", model_params -) diff --git a/examples/epstein_civil_violence/requirements.txt b/examples/epstein_civil_violence/requirements.txt deleted file mode 100644 index bcbfbbe220b..00000000000 --- a/examples/epstein_civil_violence/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -jupyter -matplotlib -mesa diff --git a/examples/epstein_civil_violence/run.py b/examples/epstein_civil_violence/run.py deleted file mode 100644 index 5aa2644ac3d..00000000000 --- a/examples/epstein_civil_violence/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from epstein_civil_violence.server import server - -server.launch() diff --git a/examples/forest_fire/Forest Fire Model.ipynb b/examples/forest_fire/Forest Fire Model.ipynb deleted file mode 100644 index db9be7203e0..00000000000 --- a/examples/forest_fire/Forest Fire Model.ipynb +++ /dev/null @@ -1,623 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# The Forest Fire Model\n", - "## A rapid introduction to Mesa\n", - "\n", - "The [Forest Fire Model](http://en.wikipedia.org/wiki/Forest-fire_model) is one of the simplest examples of a model that exhibits self-organized criticality.\n", - "\n", - "Mesa is a new, Pythonic agent-based modeling framework. A big advantage of using Python is that it a great language for interactive data analysis. Unlike some other ABM frameworks, with Mesa you can write a model, run it, and analyze it all in the same environment. (You don't have to, of course. But you can).\n", - "\n", - "In this notebook, we'll go over a rapid-fire (pun intended, sorry) introduction to building and analyzing a model with Mesa." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "First, some imports. We'll go over what all the Mesa ones mean just below." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "import matplotlib.pyplot as plt\n", - "\n", - "%matplotlib inline\n", - "\n", - "from mesa import Model, Agent\n", - "from mesa.time import RandomActivation\n", - "from mesa.space import Grid\n", - "from mesa.datacollection import DataCollector\n", - "from mesa.batchrunner import BatchRunner" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Building the model\n", - "\n", - "Most models consist of basically two things: agents, and an world for the agents to be in. The Forest Fire model has only one kind of agent: a tree. A tree can either be unburned, on fire, or already burned. The environment is a grid, where each cell can either be empty or contain a tree.\n", - "\n", - "First, let's define our tree agent. The agent needs to be assigned **x** and **y** coordinates on the grid, and that's about it. We could assign agents a condition to be in, but for now let's have them all start as being 'Fine'. Since the agent doesn't move, and there is only at most one tree per cell, we can use a tuple of its coordinates as a unique identifier.\n", - "\n", - "Next, we define the agent's **step** method. This gets called whenever the agent needs to act in the world and takes the *model* object to which it belongs as an input. The tree's behavior is simple: If it is currently on fire, it spreads the fire to any trees above, below, to the left and the right of it that are not themselves burned out or on fire; then it burns itself out. " - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "class TreeCell(Agent):\n", - " \"\"\"\n", - " A tree cell.\n", - "\n", - " Attributes:\n", - " x, y: Grid coordinates\n", - " condition: Can be \"Fine\", \"On Fire\", or \"Burned Out\"\n", - " unique_id: (x,y) tuple.\n", - "\n", - " unique_id isn't strictly necessary here, but it's good practice to give one to each\n", - " agent anyway.\n", - " \"\"\"\n", - "\n", - " def __init__(self, model, pos):\n", - " \"\"\"\n", - " Create a new tree.\n", - " Args:\n", - " pos: The tree's coordinates on the grid. Used as the unique_id\n", - " \"\"\"\n", - " super().__init__(pos, model)\n", - " self.pos = pos\n", - " self.unique_id = pos\n", - " self.condition = \"Fine\"\n", - "\n", - " def step(self):\n", - " \"\"\"\n", - " If the tree is on fire, spread it to fine trees nearby.\n", - " \"\"\"\n", - " if self.condition == \"On Fire\":\n", - " neighbors = self.model.grid.get_neighbors(self.pos, moore=False)\n", - " for neighbor in neighbors:\n", - " if neighbor.condition == \"Fine\":\n", - " neighbor.condition = \"On Fire\"\n", - " self.condition = \"Burned Out\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we need to define the model object itself. The main thing the model needs is the grid, which the trees are placed on. But since the model is dynamic, it also needs to include time -- it needs a schedule, to manage the trees activation as they spread the fire from one to the other.\n", - "\n", - "The model also needs a few parameters: how large the grid is and what the density of trees on it will be. Density will be the key parameter we'll explore below.\n", - "\n", - "Finally, we'll give the model a data collector. This is a Mesa object which collects and stores data on the model as it runs for later analysis.\n", - "\n", - "The constructor needs to do a few things. It instantiates all the model-level variables and objects; it randomly places trees on the grid, based on the density parameter; and it starts the fire by setting all the trees on one edge of the grid (x=0) as being On \"Fire\".\n", - "\n", - "Next, the model needs a **step** method. Like at the agent level, this method defines what happens every step of the model. We want to activate all the trees, one at a time; then we run the data collector, to count how many trees are currently on fire, burned out, or still fine. If there are no trees left on fire, we stop the model by setting its **running** property to False." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "class ForestFire(Model):\n", - " \"\"\"\n", - " Simple Forest Fire model.\n", - " \"\"\"\n", - "\n", - " def __init__(self, width, height, density):\n", - " \"\"\"\n", - " Create a new forest fire model.\n", - "\n", - " Args:\n", - " width, height: The size of the grid to model\n", - " density: What fraction of grid cells have a tree in them.\n", - " \"\"\"\n", - " # Set up model objects\n", - " self.schedule = RandomActivation(self)\n", - " self.grid = Grid(width, height, torus=False)\n", - " self.dc = DataCollector(\n", - " {\n", - " \"Fine\": lambda m: self.count_type(m, \"Fine\"),\n", - " \"On Fire\": lambda m: self.count_type(m, \"On Fire\"),\n", - " \"Burned Out\": lambda m: self.count_type(m, \"Burned Out\"),\n", - " }\n", - " )\n", - "\n", - " # Place a tree in each cell with Prob = density\n", - " for x in range(self.width):\n", - " for y in range(self.height):\n", - " if self.random.random() < density:\n", - " # Create a tree\n", - " new_tree = TreeCell(self, (x, y))\n", - " # Set all trees in the first column on fire.\n", - " if x == 0:\n", - " new_tree.condition = \"On Fire\"\n", - " self.grid[x][y] = new_tree\n", - " self.schedule.add(new_tree)\n", - " self.running = True\n", - "\n", - " def step(self):\n", - " \"\"\"\n", - " Advance the model by one step.\n", - " \"\"\"\n", - " self.schedule.step()\n", - " self.dc.collect(self)\n", - " # Halt if no more fire\n", - " if self.count_type(self, \"On Fire\") == 0:\n", - " self.running = False\n", - "\n", - " @staticmethod\n", - " def count_type(model, tree_condition):\n", - " \"\"\"\n", - " Helper method to count trees in a given condition in a given model.\n", - " \"\"\"\n", - " count = 0\n", - " for tree in model.schedule.agents:\n", - " if tree.condition == tree_condition:\n", - " count += 1\n", - " return count" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Running the model\n", - "\n", - "Let's create a model with a 100 x 100 grid, and a tree density of 0.6. Remember, ForestFire takes the arguments *height*, *width*, *density*." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "fire = ForestFire(100, 100, 0.6)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To run the model until it's done (that is, until it sets its **running** property to False) just use the **run_model()** method. This is implemented in the Model parent object, so we didn't need to implement it above." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "fire.run_model()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "That's all there is to it!\n", - "\n", - "But... so what? This code doesn't include a visualization, after all. \n", - "\n", - "Remember the data collector? Now we can put the data it collected into a pandas DataFrame:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "results = fire.dc.get_model_vars_dataframe()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And chart it, to see the dynamics." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "results.plot()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this case, the fire burned itself out after about 90 steps, with many trees left unburned. \n", - "\n", - "You can try changing the density parameter and rerunning the code above, to see how different densities yield different dynamics. For example:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYAAAAD8CAYAAAB+UHOxAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMi4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvIxREBQAAIABJREFUeJzs3XdcldUfwPHP4bKRIQg4QHHgwK24F2qhlrlKc+UsKxuWZmr92mbTNMvM0hxpWVmpleXeG/fAPXGBIoiy7z2/P+6DYjlQgcv4vl8vXve55z7je0Gf7/Occ55zlNYaIYQQhY+drQMQQghhG5IAhBCikJIEIIQQhZQkACGEKKQkAQghRCElCUAIIQopSQBCCFFISQIQQohCShKAEEIUUva2DuB2ihUrpoOCgmwdhhBC5Ctbt269oLX2vdN6eToBBAUFERERYeswhBAiX1FKncjKelIFJIQQhZQkACGEKKQkAQghRCElCUAIIQopSQBCCFFIZSkBKKVeVkrtVUrtUUr9qJRyVkqVVUptUkodUkr9pJRyNNZ1Mt4fNj4PyrSfUUb5AaVUm5z5SkIIIbLijglAKVUKeBEI1VpXA0xAd+AjYJzWOhi4BAw0NhkIXNJaVwDGGeuhlAoxtqsKtAW+UkqZsvfrCCGEyKqsPgdgD7gopdIAV+As0AroaXw+A3gbmAR0NJYB5gJfKqWUUT5Ha50CHFNKHQbqAxvu/2sIIUTepLUmKT2Jy6mXSUhNuPaasZycnoxGY9EWLNqC1hoLFsp7ladtUNscje2OCUBrfVop9SlwEkgCFgNbgTitdbqxWhRQylguBZwytk1XSsUDPkb5xky7zrzNNUqpQcAggNKlS9/DVxJCiJyVYk7hUvIlYhJjiE6M5nzi+WuvF5Iu3HCCT0hNwKzNd32MtkFtbZ8AlFJFsV69lwXigF+AdjdZNWN2eXWLz25VfmOB1t8A3wCEhobKjPVCiFwRlxzHucRzxCTGcCHpAheSLhCTZF2OT4knLiWO+JR4LqdeJik96T/b2yt7fF198XXxpahzUUp7lMbD0QMPRw/cHd1xd3S/tpy5zMnkhMnOhJ2yww477JQd1kqTnJeVKqAHgGNa6xgApdRvQGPASyllb9wFBABnjPWjgEAgSillD3gCsZnKM2TeRgghckVcchyH4g5xOO4wR+KOcDT+KEfijhCbHPufdd0d3PFx8cHb2ZuSRUoS4hOCp6MnXs5eeDh64Ofqd+3H29kbO5W/OlZmJQGcBBoqpVyxVgG1BiKAFcBjwBygLzDfWH+B8X6D8flyrbVWSi0AflBKfQaUBIKBzdn4XYQQ4gZmi5nDcYfZGbOTnTE72RWzi+OXj1/7vIhDEcp7lScsMIxynuUoWaQkvi6+FHMpRjGXYjjbO9su+FyQlTaATUqpucA2IB3YjrWK5i9gjlJqtFE21dhkKvC90cgbi7XnD1rrvUqpn4F9xn6e0/oeKsaEEOIWUs2p7Lmwh63ntxJxPoKdMTu5mnYVAG9nb2r41qBjhY6EeIdQ3qs8fq5+uVbdkhcprfNuNXtoaKiW0UCFELeSmJbIrgu72HZ+G1vPb2VnzE5SzCkAVPCqQF3/utT0rUkt31oEuAcUmpO9Umqr1jr0Tuvl6eGg70dKuhkne3nMQIiCJCYxhm3R29gRvYNt0ds4EHsAszajUFT2rkzXil0JLR5KHb86FHUuautw87wCmQD2nI7nqZkRfNmzNnXLeNs6HCHEfTh/9Tz/HP+Hf479w56LewBwNjlT3bc6A6oNoI5/HWr41sDD0cPGkeY/BTIBlPRywcnejqdmbuX3wY0p4+Nm65CEEHchNjmWpSeWsvDYQrad34ZGU8W7Ci/VeYn6xetT2acyDnYOtg4z3yuwbQDHLlyl81fr8HZ15NdnG1PUzTGboxNCZJd0Szp7Luxhw5kNrD+znt0XdmPWZsp6lqVd2Xa0C2pHkGeQrcPMN7LaBlBgEwDAluOx9Pp2E7UCvfj+yfrSJiBEHnL+6nlWn17N+tPr2XR2EwlpCSgU1YpVo1HJRoSXCadi0YqFpuE2OxX6RmCAekHefNqtJi/+uJ1X5+5i/OO15B+TEDZi0RYiL0ayMmolq06tIjI2EoDibsUJDwqnUclGNCzREE8nTxtHWngU6AQA0KFmSU7FJvLJogOU9nZlWHglW4ckRKGRbklny7ktLD6xmFWnVhGTFIOdsqOmb01eqvMSLQJaUN6rvFyY2UiBTwAAg8PKc/JiIl8sP0xgUVe61Qu880ZCiHtitpjZFr2NRccXseTEEmKTY3G1d6VpqaaEBYbRtFRT6aKZRxSKBKCUYnTnapyJT+K133fj5+FEWCU/W4clRIFh0Ra2R2+/dtK/kHQBF3sXmgc0p21QW5qWalrgh1XIjwpFAgBwMNnxVa86dP9mI8/O2sasJxtQt4xchQhxryzawq6YXSw6vojFxxcTnRSNk8mJ5gHNCQ8Kp3mp5rg6uNo6THEbBboX0M3EJKTw2NfriUtM45dnGlHR3z1b9y9EQZZuSWd79HZWnFrBkhNLOHf1HI52jjQp1YS2QW1pEdgCNwd57sbWpBvobZy8mMijX6/HpBS/Dm5MKS+XbD+GEAVFUnoSG85sYPnJ5ayKWkVcShyOdo40KtmINkFtaBnYkiKORWwdpshEEsAdRJ69TLfJG/B1d2LuM43xlgfFhLgmLjmOVVGrWH5yOevPrCfZnIy7ozstAlrQqnQrmpRsItU7eZgkgCzYfCyWJ6ZuonJxd354qiFuToWmSUSI/zh95TTLTy5n+cnlbIvehkVb8Hf1p2VgS1qXaU1d/7oy/EI+IQkgi5buO8/Ts7bSuLwP0/rVw96Uv2b0EeJ+JKQmsOj4IuYfns+OmB2AdRjlVqVb0ap0K0K8Q6SPfj6UbU8CK6UqAT9lKioHvAnMNMqDgONAN631JWX91/I58BCQCPTTWm8z9tUX+J+xn9Fa6xlZ/UI55YEQf8Z0rsaIX3czdslBRrStbOuQhMhRZouZzec2M+/wPJadXEaKOYVynuUYUmcI4WXCKe1R2tYhilySlRnBDgC1AJRSJuA08DswElimtf5QKTXSeD8C64TxwcZPA2AS0EAp5Q28BYRinQx+q1Jqgdb6UrZ/q7v0eL3S7DgVz6SVR6hbuigPhPjbOiQhst3l1MvMPTiXH/f/yLmr53B3dKdThU50LN+RasWqyZV+IXS3ld6tgSNa6xNKqY5AmFE+A1iJNQF0BGZqa93SRqWUl1KqhLHuEq11LIBSagnQFvjxfr9EdnjrkRB2n45j6M87+OvFZgR6SwOXKBiiEqKYFTmL3w79RlJ6Eg2KN2BY6DBaBrbEyeRk6/CEDd1tAujO9RO2v9b6LIDW+qxSKuPR2lLAqUzbRBlltyrPE5wdTEzqVZeHJ6zh2dlbmftMY5wdZPRQkT9prdkZs5OZ+2ay7OQy7JQdD5V9iD4hfajkLeNhCassJwCllCPQARh1p1VvUqZvU/7v4wwCBgGULp27dZGB3q6M7VaLp2ZG8O6f+xjTuXquHl+I+5WYlsjCYwv5+cDPRMZG4uHowYBqA+hRuQd+rjL8ibjR3dwBtAO2aa3PG+/PK6VKGFf/JYBoozwKyDzaWgBwxigP+1f5yn8fRGv9DfANWHsB3UV82eLBEH+eaVGer1cdIbRMUbrUCcjtEIS4a0fjj/LzgZ9ZcHgBCWkJVCxakTcavkH7cu2lv764pbtJAD24sb5+AdAX+NB4nZ+p/Hml1BysjcDxRpJYBIxRSmUMwBPOne8mbOKV8IpsO3mJ13/fQ9WSnlQqLsNFiLxHa82GMxuYtncaG89uxMHOgfCgcLpX6k5N35rSqCvuKEvPASilXLHW35fTWscbZT7Az0Bp4CTQVWsda3QD/RJrA28i0F9rHWFsMwB4zdjt+1rrabc7bm48B3Ar0ZeTeWjCWtyd7Zk3uAmervIAjMgb0i3pLD6+mGl7p7E/dj9+Ln70qNKDzhU64+PiY+vwRB4gD4Jlgy3HY+n57Ubql/Vmev/6OMhDYsKGktKTmHd4HjP2zuD0ldOU9SxL/6r9aV+uPQ4muUAR18mUkNmgXpA3YzpXZ/jcXbz7xz7e61TN1iGJQshsMTP/yHy+3P4lMUkx1PStyav1XiUsMAw7JRcl4t5JAriDrqGBHI6+wuTVRwn2L0KfRkG2DkkUElpr1p1Zx9iIsRyOO0xN35p83Pxj6vrXlfp9kS0kAWTBq20rczj6Cu/8sY+yxdxoFuxr65BEAbc/dj9jI8ay8exGAt0DGdtiLA+WeVBO/CJbyf1jFpjsFJ/3qE0F3yIMnr2NIzFXbB2SKKBiEmN4Y90bdPujG5GxkYyoN4L5HecTHhQuJ3+R7SQBZFERJ3um9A3F0WTHkzMiiEtMtXVIogBJMacwZfcU2v/enj+P/knfqn1Z2GUhvUN6SwOvyDGSAO5CoLcrXz9Rl9OXknjhx+2YLXm3B5XIH7TWLDmxhI7zOvL5ts9pUKIB8zvOZ1joMDwcPWwdnijgJAHcpXpB3rzTsSprDl3gy+WHbR2OyMciL0YyYNEAhq4ciou9C9+Gf8uEVhNkOGaRa6QR+B50rxfIlmOxjF92kLplitI0uJitQxL5yPmr5/li+xcsOLIATydP/tfgfzxa8VHs7eS/o8hd8i/uHiilGN25GrtPxzNkznYWDmmGv4ezrcMSeVxiWiLT905n+t7ppFvS6Ve1H0/WeFKqeoTNSBXQPXJ1tGdS7zokpZl54YftpJsttg5J5FFmi5l5h+fxyO+PMGnnJJqVasb8TvMZGjpUTv7CpiQB3IcKfu580KU6m4/H8unig7YOR+QxWmuWnljKY388xhvr3sDfzZ+Z7WYyNmwsge6Bd96BEDlMqoDuU8dapdh0LJavVx2hXlBRWleR6SQLu4wneL/Y/gX7Lu4jyCOIT1p8QniZcBm6QeQpkgCywZvtQ9h5Ko6hP+/kzxeaynSShVjEuQi+2P4F26K3UapIKd5r8h7ty7WXBl6RJ8nlSDZwdjDxVa86WLRmwPQt8pBYIRRxLoInFz1J/0X9OZVwiv81+B9/dPqDThU6yclf5FmSALJJGR83vnkilBOxifSbtoWrKem2Dknkgi3ntjBw0UD6L+rP4bjDDA8dzsIuC3m88uPyBK/I8+TSJBs1Ku/Dlz1q88ysrTwzaytT+obiZC8TyxdEW85tYdLOSWw5t4ViLsV4td6rPFbxMVzsXWwdmhBZlqU7AKWUl1JqrlJqv1IqUinVSCnlrZRaopQ6ZLwWNdZVSqkJSqnDSqldSqk6mfbT11j/kFKqb059KVsKr1qcDx+twZpDFxj6804ZLqKAORp3lOeXPc+ARQM4Hn+cEfVG8HeXv3ki5Ak5+Yt8J6t3AJ8D/2itH1NKOQKuWKd2XKa1/lApNRIYCYzAOnl8sPHTAJgENFBKeQNvAaGABrYqpRZorS9l6zfKA7qFBhKfmMb7CyPxdHHg/U7VZCTHfO5i0kUm7ZzE3INzcbF34aU6L9GrSi+c7eUBQJF/3TEBKKU8gOZAPwCtdSqQqpTqCIQZq80AVmJNAB2Bmdo61+RG4+6hhLHuEq11rLHfJVjnDc480XyB8VTzcsQmpjJp5RF83BwZFl7J1iGJe5CcnsysyFlM2T2F5PRkulbsyrO1nsXb2dvWoQlx37JyB1AOiAGmKaVqAluBIYC/1vosgNb6rFLKz1i/FNYJ5DNEGWW3Ki+wXm1TiUtXU/li+WF83Bzp16SsrUMSd2H96fW8u/FdTl85TVhgGEPrDqWsp/wNRcGRlQRgD9QBXtBab1JKfY61uudWblbXoW9TfuPGSg0CBgGULp2/R0VUSvF+5+rEXk3lnT/3UdzTmbbVStg6LHEH8SnxfLzlYxYcWUCQRxBTwqfQoEQDW4clRLbLSiNwFBCltd5kvJ+LNSGcN6p2MF6jM62f+Tn3AODMbcpvoLX+RmsdqrUO9fXN/1MvmuwUE3rUpnagF0Pm7CDieKytQxK3oLVm0fFFdJjXgb+O/sVT1Z9iboe5cvIXBdYdE4DW+hxwSimVUYndGtgHLAAyevL0BeYbywuAPkZvoIZAvFFVtAgIV0oVNXoMhRtlBZ6zg4kpfetR0suFJ2dGcDhappTMa6ITo3lpxUu8suoV/F39mdN+Di/WeREnk5OtQxMix2T1QbAXgNlKqV1ALWAM8CHwoFLqEPCg8R5gIXAUOAx8CwwGMBp/3wO2GD/vZjQIFwbebo7M6F8feztFv2mbiU5ItnVIwrD0xFI6z+/MujPrGFp3KD88/AOVvSvbOiwhcpyydtbJm0JDQ3VERIStw8hWu6LieHzyRsr7uTFnUCOKOMmzeLaSmJbIR1s+4rdDv1HVpyofNvuQIM8gW4clxH1TSm3VWofeaT0ZCiKX1Qjw4qtedYg8m8Dg2dtIk3kEbGLvhb10+7Mbvx/6nSerP8n3D30vJ39R6EgCsIGWlf14v1M1Vh+M4f2/Im0dTqFitpiZsnsKvRf2Jjk9maltpjKkzhAc7GTcHlH4SP2DjXSvX5pD0VeYuvYYVUt60DVUJgjJaTGJMYxaM4pN5zYRXiacNxu9iaeTp63DEsJmJAHY0Kh2lYk8e5nX5+2hor87NQO9bB1SgbX+zHpGrRlFYloi7zZ+l04VOsnwHKLQkyogG7I32fFlzzr4FnHi6e+3EpOQYuuQCpx0SzoTtk3gmSXP4O3szZz2c+gc3FlO/kIgCcDmvN0c+aZPXeKSUhk8eyup6dIonF3OXT3HgEUD+Hb3t3QJ7sIPD/9Aea/ytg5LiDxDEkAeULWkJx8/VpMtxy/x7p97bR1OgbDq1Coe++MxDsQe4MNmH/J247dluGYh/kXaAPKIDjVLsvd0PJNXH6VaSU+618/f4yDZSqo5lXFbxzErchaVvSvzaYtPKeNRxtZhCZEnSQLIQ15tW5l9Zy/z5vy9VCruTu3SRW0dUr5y4vIJhq8aTmRsJL2q9GJo3aE4mhxtHZYQeZZUAeUhJjvFFz1q4+fhxODZ27h4RRqFs+qPI3/Q7Y9unLl6hgktJzCy/kg5+QtxB5IA8hgvV0e+7l2X2KupvPDjdtLlSeHbSkxL5PW1r/Pa2teo7F2ZuY/MpWXplrYOS4h8QRJAHlStlCejO1Vj/ZGLfLr4oK3DybP2XNhD1z+68seRP3im5jNMbTOV4m7FbR2WEPmGtAHkUV1DA9l+Ko6vVx2hVqCnTCSTidliZtreaUzcPpFirsX4rs13hBa/47hXQoh/kQSQh731SAh7T8fzyi+7CPZ3p7xvEVuHZHPnrp5j1JpRRJyPoE1QG95o+IYM5yDEPZIqoDzMyd7EpN51cbS345nvt3I1Jd3WIdnUouOL6LKgC/su7mN0k9F80vwTOfkLcR8kAeRxJb1c+KJHbY7EXOHVubuwWPLu/A05JcWcwrsb3uWVVa8Q5BHEL4/8QscKHWU4ByHuU5YSgFLquFJqt1Jqh1IqwijzVkotUUodMl6LGuVKKTVBKXVYKbVLKVUn0376GusfUkr1vdXxxI2aVCjGqHZV+Gv3WYbP3YW5ECWBk5dP0nthb345+Av9q/VnRrsZlPaQh+SEyA530wbQUmt9IdP7kcAyrfWHSqmRxvsRQDsg2PhpAEwCGiilvIG3gFBAA1uVUgu01pey4XsUeE81L0diqplxSw+iteaTrjUx2RXsK+DFxxfz5vo3MSkTX7b6khaBLWwdkhAFyv00AncEwozlGcBKrAmgIzBTW+ea3KiU8lJKlTDWXZIxD7BSagnQFvjxPmIoVIY8EIydgrFLDmLRmrHdahXIJJBqTuXTiE/5cf+P1PCtwafNP6VEEekFJUR2y2oC0MBipZQGJmutvwH8tdZnAbTWZ5VSfsa6pYBTmbaNMspuVX4DpdQgYBBA6dJyq/9vL7QOxs5O8cmiA2hgbNea2JsKTlNOfEo8g5cOZteFXTwR8gQv13kZB5PM1iVETshqAmiitT5jnOSXKKX232bdm12S6tuU31hgTS7fgHVS+CzGV6g817ICSsHH/xzAomFct4KRBBJSE3hmyTMcuHSAz8I+48EyD9o6JHGX0tLSiIqKIjk52dahFArOzs4EBATg4HBvF0lZSgBa6zPGa7RS6negPnBeKVXCuPovAUQbq0cBmec3DADOGOVh/ypfeU9RCwaHVcBOKT78ez9aaz7vXjtfVwddTbvK4KWD2R+7n3EtxxEWGGbrkMQ9iIqKwt3dnaCgIOmllcO01ly8eJGoqCjKli17T/u442WjUspNKeWesQyEA3uABUBGT56+wHxjeQHQx+gN1BCIN6qKFgHhSqmiRo+hcKNM3KNnWpRnVLvK/LnrLKN+y79dRJPSk3h+2fPsvrCbj1t8LCf/fCw5ORkfHx85+ecCpRQ+Pj73dbeVlTsAf+B34w9qD/ygtf5HKbUF+FkpNRA4CXQ11l8IPAQcBhKB/gBa61il1HvAFmO9dzMahMW9e7pFea6mmpmw7BBuTva82T4kX/3nSzGnMGT5ELZFb+ODph9ItU8BkJ/+/eV39/u7vmMC0FofBWrepPwi0Pom5Rp47hb7+g747u7DFLfz8gPBJCSnMW3dcdydHRj6YEVbh5QlaeY0Xl7xMhvObmB0k9E8VO4hW4ckCgCTyUT16tWvvZ83bx4XLlxg5syZTJgwwYaR5T0yFlABoJTijYdDuJqSzoRlh3B3suep5uVsHdZtXU27yojVI1hzeg1vNnqTjhU62jokUUC4uLiwY8eOG8qCgoIIDZUBA/8t/3cdEQDY2Sk+6FKDh6uX4P2Fkfyw6aStQ7ql4/HH6flXT9aeXssbDd+ga8Wud95IiPuwcuVK2rdvD8Dbb7/NgAEDCAsLo1y5cjfcFcyaNYv69etTq1Ytnn76acxms61CzhWSAAoQk51i3OO1CKvky+vzdjNv+2lbh/Qfq06tosdfPbiUfInJD06mW6Vutg5JFDBJSUnUqlWLWrVq0blz55uus3//fhYtWsTmzZt55513SEtLIzIykp9++ol169axY8cOTCYTs2fPzuXoc5dUARUwjvZ2fN27Lv2mbebln3eQmGqmZwPbP1Bn0RYm75rMVzu+oop3Fca3HE/JIiVtHZbIQe/8sZd9Zy5n6z5DSnrw1iNVb7vOzaqA/u3hhx/GyckJJycn/Pz8OH/+PMuWLWPr1q3Uq1cPsCYSPz+/2+4nv5MEUAA5O5iY3r8+z87aymu/7yYhOY2nW5S3WTxXUq/w2trXWHFqBY+Ue4Q3G72Js72zzeIRwsnJ6dqyyWQiPT0drTV9+/blgw8+sGFkuUsSQAHl7GBi8hOhDP15Bx/8vZ/LyWm8El4p17voHY07ypAVQziVcIqR9UfSs3JP6SZYSNzpSj2vad26NR07duTll1/Gz8+P2NhYEhISKFOmjK1DyzGSAAowR3s7Pu9eG3dnByauOMLlpHTe6VAVu1x6YnjpiaW8vvZ1nO2d+Tb8W+oVr5crxxXiXoSEhDB69GjCw8OxWCw4ODgwceLEAp0AlLXbft4UGhqqIyIibB1Gvqe15sO/9zN59VE61SrJJ11r4pCDYweZLWa+2P4FU/dMpUaxGowNGyuTtRcSkZGRVKlSxdZhFCo3+50rpbZqre/Y71XuAAoBpRQj21XGw8WBTxYd4EqKmS971sbZwZTtx7qUfIlXV7/KxrMb6VqxKyPrj8TR5JjtxxFC3D9JAIWEUornWlbAw9meN+bvZcD0LXzbJxQ3p+z7J7A/dj9Dlg/hQtIF3m38Lp2Db94FTwiRN8hzAIXME42CGPd4TTYdi6XXlE3EJaZmy37Xn15P37/7YtZmZrSbISd/IfIBSQCFUOfaAXzVqw77zlym+zcbiU64v7Hb/zjyB88te44A9wB+ePgHqhWrlk2RCiFykiSAQqpN1eJ8168eJy4m0u3rDURdSrzrfWitmbJ7Cq+tfY26/nWZ3nY6fq4F+8EZIQoSSQCFWNPgYsx6sgGxV1Pp+vUGjsRcyfK2ZouZMZvG8Pm2z3mo7ENMemAS7o7uORitECK7SQIo5OqWKcqcQY1IM1vo9vUG9p6Jv+M2yenJDFs1jDkH5tC/an8+aPaBzNsr8oyoqCg6duxIcHAw5cuXZ8iQIaSm3l1bV1hYGJUqVbo2ptDcuXMBaNy4cU6EbDNZTgBKKZNSartS6k/jfVml1Cal1CGl1E9KKUej3Ml4f9j4PCjTPkYZ5QeUUm2y+8uIexNS0oOfn26Ek70d3b/ZyNYTt56nJyE1gWeWPsPyk8sZUW8EQ0OHYqfkOkLkDVprunTpQqdOnTh06BAHDx7kypUrvP7663e9r9mzZ7Njxw527NjBY489BsD69ev/s15+HjH0bv7nDgEiM73/CBintQ4GLgEDjfKBwCWtdQVgnLEeSqkQoDtQFWgLfKWUyv6O6OKelPMtwi/PNqZYESd6T9nMmkMx/1nnYtJFBi4ayM7onXzU/CN6h/S2QaRC3Nry5ctxdnamf//+gHWcn3HjxvHdd9+RmJjI9OnT6dKlC23btiU4OJhXX331rvZfpEgRwDq8dMuWLenZs+e1yWfy41DSWUoASqkA4GFgivFeAa2AucYqM4BOxnJH4z3G562N9TsCc7TWKVrrY1injKyfHV9CZI9SXi78/HQjyvi4MnB6BP/sOXfts7NXztLvn34ciz/GhFYTaFe2nQ0jFeLm9u7dS926dW8o8/DwoHTp0hw+fBiAHTt28NNPP7F7925++uknTp06ddN99erV61oV0MWLF//z+ebNm3n//ffZt29fvh1KOqtPAY0HXgUyWvl8gDitdbrxPgooZSyXAk4BaK3TlVLxxvqlgI2Z9pl5G5FH+Lo78dOgRvSbvpnnftjGx4/WoHaFVAYtHkRiWiKTH5xMHf86tg5T5Ad/j4Rzu7N3n8WrQ7sPb/mx1vqmgw1mLm/dujWenp6AdfyfEydOEBgY+J9tZs+efdtZxOrXr0/ZsmUB8u1Q0ndMAEqp9kC01nqrUioso/gmq+o7fHa7bTIfbxAwCKB0aduPY18Yebo6MGtgA56aGcHwP/6iWIWZuDjYM63tNCp5V7J1eELcUtWqVfn1119vKLt8+TKnTp1iMqvSAAAgAElEQVSifPnybN269aZDQd8LNze3a8v5dSjprNwBNAE6KKUeApwBD6x3BF5KKXvjLiAAOGOsHwUEAlFKKXvAE4jNVJ4h8zbXaK2/Ab4B62Bw9/KlxP1zc7LnuXaKfcumcDXJmUeKvUvFovljsnmRR9zmSj2ntG7dmpEjRzJz5kz69OmD2Wxm2LBh9OvXD1dX1xw9bn4cSvqObQBa61Fa6wCtdRDWRtzlWutewArgMWO1vsB8Y3mB8R7j8+XaOuToAqC70UuoLBAMbM62byKy1eqo1QxZ+RylPUvSwuMdpq68wui/IrFYJCeLvEspxe+//84vv/xCcHAwFStWxNnZmTFjxuTocTMPJV2jRg0efPBBzp49m6PHzA53NRy0UQX0ita6vVKqHDAH8Aa2A7211ilKKWfge6A21iv/7lrro8b2rwMDgHTgJa3137c7ngwHbRsLjy7k9bWvU9G7Il8/8DWejl68++c+pq8/zmN1A/iwS3Xsc3A4aZF/yXDQuS/XhoPWWq8EVhrLR7lJLx6tdTLQ9Rbbvw+8fzfHFLnr5wM/M3rjaOr41+HLVl9SxNHa7e2tR0LwcnVg/NJDJCSnMaFHbZzspRevEPmZXMaJa6bunsp7G9+jWUAzvn7g62snf7DeWr/0QEXebB/Cor3nGTB9C1dS7q3xTAiRN0gCEGitGb91POO3jadd2XaMbzn+lpO2D2halk+71mTj0Vi6fb2B6Mv3N5KoEMJ2JAEUclprxkaMZeqeqXSt2JUPmn6Ag93tx/V5rG4AU/qGcvziVTp/tZ7D0Qm5FK0QIjtJAijEMk7+M/bNoEflHrzR8A1Mdlmr129ZyY85gxqSkm7m0Ukb2HL81uMHCSHyJkkAhdS/T/6j6o+66ROUt1MjwIvfnm2Ct5sjvaZs4u/deb/bmxDiOkkAhVB2nPwzlPZx5ddnG1O1pAeDf9jG1LXHuJuuxUJkN5PJRK1atahZsyZ16tS56QieOa1fv37XhpDOTGvN6NGjrz2j0LJlS/bu3XvH/c2bN499+/Zle5ySAAqZ7Dz5Z/B2c+SHJxvyQBV/3vtzH8//uJ2E5LRsiliIu+Pi4sKOHTvYuXMnH3zwAaNGjbqr7XNyFM+JEyeyfv16du7cycGDBxk1ahQdOnQgOfn2nSkkAYj7prXm04hPs/Xkn8HF0cTk3nV5tW0l/tlzjvZfrGV31J0nlxEiJ12+fJmiRYsC1iGc27dvf+2z559/nunTpwMQFBTEu+++S9OmTfnll18ICwtjxIgR1K9fn4oVK7JmzRrAmhyGDx9OvXr1qFGjBpMnTwas/7eef/55QkJCePjhh4mOjr5pPB999BFffPHFtWEpwsPDady48bWRQzOGmwaYO3cu/fr1Y/369SxYsIDhw4dTq1Ytjhw5km2/n7t6EEzkXxZtYcymMfx04Cd6Vu7JyPojs+3kn8HOTjE4rAL1g7x54cftdJm0jtceqkK/xkHZfiwhbiUpKYlatWqRnJzM2bNnWb58eZa2c3Z2Zu3atQB8/fXXpKens3nzZhYuXMg777zD0qVLmTp1Kp6enmzZsoWUlBSaNGlCeHg427dv58CBA+zevZvz588TEhLCgAEDbtj/5cuXuXr1KuXLl7+hPDQ09LbVQI0bN6ZDhw60b9/+2sQ02UUSQCFgtph5a/1bzD8yn/5V+/Ny3Zdz9IQcGuTNwheb8covO3nnj31sOHKRjx+rgZerY44dU+Q9H23+iP2x+7N1n5W9KzOi/ojbrpNRBQSwYcMG+vTpw549e+6478cff/yG9126dAGgbt26HD9+HIDFixeza9eua/X78fHxHDp0iNWrV9OjRw9MJhMlS5akVatWWf5OtxrCOjdIFVABl2ZJY+Sakcw/Mp/BNQfn+Mk/Q1E3R6b0DeV/D1dhxYFoHvlyrTwvIHJdo0aNuHDhAjExMdjb22OxWK599u9698zDOwPXho3OPGS01povvvji2lSRx44dIzw8HOCO/688PDxwc3Pj6NGjN5Rv27aNkJCQ/+zjTu0C2UHuAAqwFHMKr6x6hZWnVjKs7jD6VeuXq8dXSvFks3LUKVOUQTO30vmr9UzqVZemwcVyNQ5hG3e6Us8N+/fvx2w24+PjQ5kyZdi3bx8pKSkkJyezbNkymjZtelf7a9OmDZMmTaJVq1Y4ODhw8OBBSpUqRfPmzZk8eTJ9+vQhOjqaFStW0LNnz/9sP3z4cF588UV++eUXXFxcWLp0KWvXrr3WluDv709kZCSVKlXi999/x93dOgeXu7s7CQnZfwElCaCASkpPYsjyIWw4u4HXG7xO98rdbRZLndJFmfdcYwZOj6DvtM2827EqvRrk7XHSRf6V0QYA1iv2GTNmYDKZCAwMpFu3btSoUYPg4GBq16591/t+8sknOX78OHXq1EFrja+vL/PmzaNz584sX76c6tWrU7FiRVq0aHHT7V944QUuXbpE9erVMZlMFC9enPnz5+Pi4gLAhx9+SPv27QkMDKRatWpcuXIFgO7du/PUU08xYcIE5s6d+592hHt1V8NB5zYZDvreJKcnM3jZYLae38rbjd6mc3BnW4cEQEJyGi/8uJ2VB2IY2LQsrz1UBZOdNA4XJDIcdO67n+GgpQ2ggEkzpzF05VAizkUwpumYPHPyB3B3dmBKn1D6NQ5i6tpjPP19BFdlRFEhbEYSQAFitpgZtXYUa06v4Y1Gb/BwuYdtHdJ/2JvseLtDVd7tWJXl+6PpPXWTJAEhbOSOCUAp5ayU2qyU2qmU2quUescoL6uU2qSUOqSU+kkp5WiUOxnvDxufB2Xa1yij/IBSqk1OfanCSGvNexvfY9HxRQyrO4yuFW86J0+e0adREF/1qsuuqHienBFBclrOPX0phLi5rNwBpACttNY1gVpAW6VUQ+AjYJzWOhi4BAw01h8IXNJaVwDGGeuhlArBOqdwVaAt8JVSSqaUygYZwzv8euhXnqr+VK739rlXbasV57NuNdl47CLPzNpKarrlzhuJPC8vtysWNPf7u87KpPBaa33FeOtg/GigFZAx2tEMoJOx3NF4j/F5a2Xt3NoRmKO1TtFaHwMOc5MpJcXdm7xrMjP2zaBn5Z68UPsFW4dzVzrWKsUHnauz8kAMQ+ZsJ90sSSA/c3Z25uLFi5IEcoHWmosXL+LsfPPJm7IiS91AjSv1rUAFYCJwBIjTWmdU3kYBpYzlUsApI8B0pVQ84GOUb8y028zbiHs0O3I2E3dMpEP5DoyoPyJfDrnQvX5pElPNvPvnPobP3cXYrjWxk95B+VJAQABRUVHExMTYOpRCwdnZmYCAgHvePksJQGttBmoppbyA34Gb9fPKSPk3+5+rb1N+A6XUIGAQQOnSpbMSXqH1+6Hf+XDzh7Qu3Zp3Gr+Dncq/bfoDmpYlKc3MJ4sO4OxgYkznavkymRV2Dg4OlC1b1tZhiCy6qzOG1joOWAk0BLyUUhkJJAA4YyxHAYEAxueeQGzm8ptsk/kY32itQ7XWob6+vncTXqGy6Pgi3t7wNo1KNOLj5h9jb5f/n+l7rmUFnmtZnh83n2TQ91s5E5dk65CEKNCy0gvI17jyRynlAjwARAIrgIyh6foC843lBcZ7jM+Xa2uF4AKgu9FLqCwQDGzOri9SmKyJWsPINSOp6VuT8S3H42gqOIOsvRJeidcfqsKaQzE88NkqJq86Qpq0CwiRI7JyB1ACWKGU2gVsAZZorf8ERgBDlVKHsdbxTzXWnwr4GOVDgZEAWuu9wM/APuAf4DmjaknchS3ntvDyypcJ9gpmYuuJuDq42jqkbKWU4qnm5Vg6tAWNyxfjg7/38/CENWw+JnMOC5HdZCiIfGTvhb0MXDwQP1c/predjrezt61DynFL9p3n7QV7OR2XxKN1AnijfRUZVlqIO5ChIAqYI3FHeHrp03g5efHtg98WipM/wIMh/iwZ2pxnw8ozf8dp2n0udwNCZBdJAPlAQmoCLy5/EQc7B74N/xZ/N39bh5SrXB3tGdG2Mr8NboyjvR3dv9nA+KUHMVvy7t2rEPmBJIA8TmvNW+vf4vSV03za4lMC3QPvvFEBVSPAiz9faEqHmiUZv/QQPb7dyNl46SkkxL2SBJDHzY6czZITS3ipzkvU9a9r63Bszt3ZgfHdazO2a032nI6n3edrWLz3nK3DEiJfkgSQh+2I3sHYiLG0DGxJ36p977xBIfJo3QD+fKEppbxcGPT9Vt5esJeUdOlUJsTdkASQR11KvsQrq17B382f0U1Hy1OxN1HOtwi/DW5M/yZBTF9/nEcnrefYhau2DkuIfEMSQB5ktpgZuWYkl5Iv8VnYZ3g4etg6pDzLyd7EW49U5ds+oZyKTaL9hDXM33Ha1mEJkS9IAsiDvtn9DevPrGdkg5GE+ITYOpx84cEQf/4e0owqJTwYMmcHr87dSWKqTDQjxO1IAshj1p1ex6Qdk3ik3CM8FvzYnTcQ15T0cmHOoIY837ICv2yNosOX64g8e9nWYQmRZ0kCyEOOxR9j+KrhVChagf81/J/U+98De5Mdr7SpxPcDGhCflEbHieuYvu6YjE8vxE1IAsgj4lPirQ97mRz4stWXBW6Mn9zWNLgY/wxpRtMKxXj7j30MnBHBxSsptg5LiDxFEkAekG5JZ/iq4URdiWJc2DhKFilp65AKBJ8iTkztG8rbj4Sw9vAF2n6+hjWHZKISITJIAsgDxkaMZcPZDbzR8A3q+NexdTgFilKKfk3KMv+5Jni5OPDE1M28/9c+mYReCCQB2Nxvh35jVuQselfpTZfgLrYOp8CqUsKDBc83pVeD0ny75hgdvlzLntPxtg5LCJuSBGBDW89v5b2N79GkZBOGhQ6zdTgFnoujifc7V2dav3rEJabRaeI6vlh2SCaiF4WWJAAbOZVwipdXvExAkQA+blEwpnTML1pW9mPxy815qHoJxi45yKOT1nM4+oqtwxIi12VlSshApdQKpVSkUmqvUmqIUe6tlFqilDpkvBY1ypVSaoJS6rBSapdSqk6mffU11j+klCq0g9tcTLrIM0uewYKFL1p9IU/62oCXqyMTetTmy561ORGbyMMT1jBlzVEZYloUKlm5A0gHhmmtq2CdDP45pVQI1qkel2mtg4FlxnuAdljn+w0GBgGTwJowgLeABkB94K2MpFGYXE27yuBlg4lOjGZi64kEeQbZOqRCrX2Nkix+qTlNKxRj9F+RdJq4TtoGRKFxxwSgtT6rtd5mLCdgnRC+FNARmGGsNgPoZCx3BGZqq42Al1KqBNAG63zCsVrrS8ASoG22fps8Ls2cxksrXuJA7AHGho2lpm9NW4ckAD8PZ6b0DeXLnrU5dzmZDl+u5b0/93E1RYaSEAXbXbUBKKWCgNrAJsBfa30WrEkC8DNWKwWcyrRZlFF2q/JCwaItvL7udTae3cg7jd+heUBzW4ckMlFK0b5GSZYObUH3+qWZuvYY4eNWsyzyvK1DEyLHZDkBKKWKAL8CL2mtbzfAys3GL9C3Kf/3cQYppSKUUhExMQXjoR2tNZ9s+YS/j/3Ny3VfpmOFjrYOSdyCp4sDYzpX59dnG+HmZGLgjAienLGFg+cTbB2aENkuSwlAKeWA9eQ/W2v9m1F83qjawXiNNsqjgMzzFgYAZ25TfgOt9Tda61Ctdaivr+/dfJc867s9313r69+/an9bhyOyoG4Zb/58oRkj2lZm09FY2o5fzSu/7OR0nExBKQqOrPQCUsBUIFJr/VmmjxYAGT15+gLzM5X3MXoDNQTijSqiRUC4Uqqo0fgbbpQVaH8d/Yvx28bTrmw7htcbLgO85SOO9nY8G1ae1a+2ZGDTsizYcYaWn65k9J/7uHQ11dbhCXHf1J1GSVRKNQXWALuBjCdmXsPaDvAzUBo4CXTVWscaCeNLrA28iUB/rXWEsa8BxrYA72utp93u2KGhoToiIuJevleeEHEugkFLBlHLrxaTH5iMg8nB1iGJ+3A6LolxSw7y27Yo3BztGdC0LP2bBOHl6mjr0IS4gVJqq9Y69I7r5eVhcvNzAjgWf4zeC3vj4+LD9+2+x9PJ09YhiWxy8HwCny46wOJ953FzNNG7YRkGNiuLn7uzrUMTApAEYFOxybH0+qsXiemJzH5oNgHuAbYOSeSA/ecu89WKI/y56wwOJjserxfI0y3KU8rLxdahiUJOEoCNJKcnM3DxQA7EHuC7Nt9Rw7eGrUMSOezYhatMWnmY37ZZ5yLuVi+QF1pVoISnJAJhG5IAbMCiLbyy6hWWnljKZ2Gf8UCZB2wdkshFp+OSmLTyMD9tOYVSil4NSjM4rAK+7k62Dk0UMllNADIYXDYav3U8S04sYVjoMDn5F0KlvFwY3ak6y4eF0blWKWZuOEHzj1fw4d/7pdeQyJMkAWSThUcXMm3vNB6v9Dh9QvrYOhxhQ4Hernz0WA2WDm1Bm6r+TF59hOYfr2DaumMy2JzIU6QKKBscvHSQ3gt7U8W7ClPaTMHBTrp7iusOnk9g9F+RrD4YQ/VSnozpXJ3qAdIrTOQcqQLKJZdTL/Pyipdxc3Dj0xafyslf/EdFf3dm9K93bbC5jhPX8vaCvSQkp9k6NFHISQK4DxZt4fU1r3Pmyhk+C/sMX9eCMXSFyH4Zg80tG9aC3g3LMGPDcR74bBV/7TpLXr4LFwWbJID78O2ub1kZtZJX6r1Cbb/atg5H5AMezg6827Eavw9ugo+bE8/9sI1HJ61ny/FYW4cmCiFJAPdo7em1TNwxkYfLPUzPyj1tHY7IZ2oFerHg+SZ89Gh1Tscl0fXrDTLqqMh10gh8D6ISonj8z8cp7lacWQ/NwsVeHvgR9y4p1cy09ceYtOIIV1PTeaxuAC89UJGS8kSxuEfyIFgOSUxLpO8/fTl95TQ/PfwTgR6Bd95IiCy4dDWViSsOM3PDCVDQt1EZng2rgLebDDYn7o70AsoBFm3hf+v+x8FLB/mo2Udy8hfZqqibI/9rH8KyYS14pEZJpq49RvOPV/D50kNckekpRQ6QBHAXJu+azJITSxhadyjNAprZOhxRQAV6uzK2W00WvdScJhV8GLf0IC0+XsHUtcdITjPbOjxRgEgVUBYtObGEoSuH0qF8B0Y3GS0Tu4hcs+NUHJ8s2s+6wxcpVsSRrqGB9KhXmtI+rrYOTeRR0gaQjQ7EHuCJv58guGgw37X5DieTDO4lct/6wxeYtv44yyLPo4Fmwb70alCa1pX9sDfJzby4LtsSgFLqO6A9EK21rmaUeQM/AUHAcaCb1vqSMRvY58BDWGcD66e13mZs0xf4n7Hb0VrrGXcKLi8kgItJF+nxVw/M2sych+fIw17C5s7GJ/HTllPM2XyKc5eT8fdwoleDMvRuWEYajAWQvQmgOXAFmJkpAXwMxGqtP1RKjQSKaq1HKKUeAl7AmgAaAJ9rrRsYCSMCCAU0sBWoq7W+dLtj2zoBpJnTeHLxk+y9uJcZ7WZQ1aeqzWIR4t/SzRaW74/m+40nWHPoAs4OdnStG8jApmUJKuZm6/CEDWU1AdjfaQWt9WqlVNC/ijsCYcbyDGAlMMIon6mtWWWjUspLKVXCWHeJ1jrWCG4J1jmDf8zCd7GZMZvHsC16G580/0RO/iLPsTfZEV61OOFVi3PgXAJT1hzlpy2nmLXpBOEh/jzVrBx1yxSV9ipxS3dMALfgr7U+C6C1PquU8jPKSwGnMq0XZZTdqvw/lFKDgEEApUuXvsfw7t/cg3OZe3AuA6sNpG3ZtjaLQ4isqFTcnU+61mR4m0rM2HCcWRtPsmjveaqU8ODx0AA61ipFUakeEv+S3S1HN7vU0Lcp/2+h1t9orUO11qG+vrapb98ds5sxm8bQuGRjXqj9gk1iEOJe+Hk4M7xNZTaMasV7naphb6d4+499NBizjOd+2MbqgzEyJ4G45l7vAM4rpUoYV/8lgGijPArI/HRUAHDGKA/7V/nKezx2jrqYdJGXV76Mn6sfHzX7CJOdydYhCXHXXB3teaJhGZ5oWIZ9Zy7zc8Qp5u04zV+7zlLS05lOtUvRqXYpKvq72zpUYUP3egewAOhrLPcF5mcq76OsGgLxRlXRIiBcKVVUKVUUCDfK8pR0SzqvrHqFuJQ4xoWNw8vZy9YhCXHfQkp68HaHqmx6rTUTe9ahYnF3Jq8+Svi41Tz0+Rq+XX2U85eTbR2msIGs9AL6EevVezHgPPAWMA/4GSgNnAS6aq1jjW6gX2Jt4E0E+mutI4z9DABeM3b7vtZ62p2Cy+1eQJ9s+YSZ+2YypukYHin/SK4dV4jcFpOQwp+7zjBv+2l2RsWjFDQu78ODVfxpXcWfQG95yCw/kwfB7tLfx/7m1dWv0rNyT0Y1GJUrxxQiLzgac4V5O87w564zHI25CkAlf3daV/GjdRV/agV6YbKTnkT5iSSAu5DxpG8V7ypMCZ+Cg0mmdRSF07ELV1kWeZ5lkdFsPh6L2aLxcXOkVWU/Hgzxp1mwLy6O0i6W10kCyKKohCj6/N0HhWJOe3nSV4gM8UlprDoYw7LI8yzfH01CcjrODnY0reDLgyF+PFDFH58iMixKXpRtD4IVZBeSLjBoySBSzClMbztdTv5CZOLp4kCHmiXpULMkaWYLm4/FsmTfeZbsO8/SyPOY7PYQVtGXLnUCaF3FD2cHuTPIbwrtHcDl1MsM+GcAJxNO8m34t9T0rZkjxxGioNFas+/sZf7YeZZ5209z7nIy7s72tK9RkkfrlJKnj/MAqQK6jaT0JJ5e8jS7L+xmYuuJNC7ZONuPIURhYLZoNhy5yG/bovh7zzmS0syU8nKhXbXitKtenNqBRbGTBuRcJwngFtIsaQxZPoS1p9fySYtPaBPUJlv3L0RhdTUlnX/2nOOv3WdZe+gCqWYL/h5OtKlanLbVilM/yFuGrc4lkgBuwqItjFozioXHFvJWo7d4rOJj2bZvIcR1l5PTWLE/mr93n2PlwWiS0yy4O9nToJw3jcsXo3EFHyr5u0tVUQ6RRuCbGL91PAuPLWRInSFy8hciB3k4O9CxVik61ipFYmo6qw/GsPrQBdYfvsDSSOvIMcWKONKofDFaVvKlZSU/GazOBgpNAvjt0G9M2zuNxys9zsBqA20djhCFhqujPW2rlaBttRIARF1KZP2Ri6w/fIG1hy/yx84z2CkIDfLmwSr+PBDiT1mZzyBXFIoqoC3ntjBo8SDql6jPxNYTsbcrNHlPiDzNYtHsPh3P0khr99L95xIAKOfrRqtKfrSq7EdokDeO9tJ2cDekDcBw4vIJei3sRTHnYnz/0Pe4O8roh0LkVVGXElkWGc3SyPNsOhpLqtlCESd7mlYoRsvKvoRV8sPfw9nWYeZ5kgCA+JR4ei/sTXxKPLMfnk2ge+CdNxJC5AlXU9JZd/gCKw7EsGJ/NOeMEUtLeblQM9CTWoFe1AzwolopT9yc5K4+s0LfCJxmSWPYymGcvnKaKeFT5OQvRD7j5mR/bcpLrTX7zyWw7vAFdpyKY2dUHAt3nwPATkGl4h40Cy5G82BfQoOKylPJWVQgE4DWmjGbxrDp3CbGNB1DHf86tg5JCHEflFJUKeFBlRIe18ouXklhV1Q8O07FsflYLNPXHeeb1UdxsrejQTkfmgcXo0FZH4L9i0hCuIUCWQW0/sA8nt74Bk+5lOVFj6pg7wQmJ+PVAbQFLOlgsYA2g8UM5lRIT4a0REjLeE2yrmdyADv7G38s6dfXSb1qLCeCvTO4FAUXb+urqzc4ewEa0lOsx8n4SU+F9CRreVqS9fjpydbPTE7Wfdk7Gq9O4OBq3ZeL1/VXl6Lg4AIoUAqUXaZlkzV2kwOYHK1xmxzB0RUc3cFOGtZEwZGYms6mo7GsPhTDmkMXOBx9BbDeIZQt5kbl4h5ULu5OpeLuVPArQkBR1wLbuJxn2wCUUm2BzwETMEVr/eGt1r3XBKDP7GDlr71okZSMXcaJ3Zx6+43sHKwnWAcXcHC2Lts7Xz/ZW9KticKSDpY0a7mDq/XH0fX6cnoSJF2CxEvW16RLkHbV+PJ21hO7ydF6Yjc5Wk/s9sYx7Y0fk4ORIFIy/RhJKSnu+v7uiwInD3D2sL46uRvJQ4PWWXjN2I3iWsJBWWN3dDN+L0Wsy45Gl77UK5ByxfqasWxOAXP6f3/HRXzBuxx4lwef8tbXokHG7yYt0/rGz81iVMr6dzUZic8uUzK0d7L+DeVBpALrdFwSO07GceDcZSLPJXDgXAInYxOvfW6noKSXC0E+bpTxcaWMjyv+Hs4UK+KEt5sjPkUc8XZ1zJdPL+fJBKCUMgEHgQexzhO8Beihtd53s/Wz9Ulgi+X6lbedyXp1fO01h//A6cYxs2t+4fRUSI6H5DgjISRy7aSnLcYy1rsbc5r1O1vSr3//1KuQfBlSLl9/Tbl8/cT+75P6rV5vlhzMadZ4Uq8aP1esr1qDUxHrnYdTkevJwcHF+N3YW0/QdvbWRJRwFmKPQNxJ4zvlgIyEbJ9xd+j43zs9k32mxOHw3+Vr65qu3yn+53dlSLls/N0y/e7TU4y7PBfjLs/l+h2fyfHGiwWT8bmz5/XE7ewBTp7W/WfckaYnWV/Tkoy7Rs/rPy5e1u1MjtZ4lV32JkGLxfr3sjPlyeR6JSWdg+cTOBZzlRMXr3IiNpHjFxM5cfEqcYlpN93Gy9UBHzdHfNyc8CliJAY3J4oVsZYVK+JIMXcnirk54eFinyeebs6rjcD1gcNa66MASqk5QEfgpgkgW9nZgZ2z9Uo7t9ln8xOO9o7WK+QihWD46vRUaxKIPQKXTgDaSBj/OvneLElpff0OwZx6/c4hPcV65/HvO6wb7izSjKpBo8ycZj2hmuOtdywZSfXfP+Z0bpoYUda7rGsnbk/wDLSe0DOOn55srX5MvHj9rjWjqjBjOS0x+xPiDUnv3xdHRkJW3OT3azGqMZOvV6Fa0jPtWN24z4xqTAdn407buMu2mI2/R8QmV0IAAAYpSURBVPL1/VnSrZ87uWf68bBeNJgc/3VXZ8T471jSU6xhZLoTLeJYhDqObtRxcoZARwgyqoXti3AlzY5LyWYuJWviks38v737iXGqiuI4/v21084MYMI/MYRxBP8sZKG4UQwukBiDf6IuNNFowsKEjSaYaAy6MZq4cKNu3BA0slAjUVFiTJSgRlcoCAYIGjQBNRAHw18ZZ6btHBf31unUgSFaXtv7zidp+t7j0Tmn8+add+997T3xV40TIzWOD9c4NXyK08OjnD46ysG/Rjg7MorMqFGgSvGfZxVKzJrRS2+5TH9vmb5yib7eXmb0hudSuY/+cg/95SK9PQX6y0VKhfr7O9nlc2Zw81XzWvu7bpJ1AVgE/Nqw/htwU8YxuG7SU4b5V4eHCwVl7M9/t+DQ5BNrvSVRGY6tjlOhtVhfrhe4enGrd79ZrWk5jpNN2cVWmGg99fRNdK2p0PD/4+vZ+MRYV3NLpVAKJ/iZl8bXiF2vlWEYPRPyO3ssLp+ZiLk2FmJpVOhpiKUv/NzKcHjPpimcs+Ljgu4XPN81XSU+ztFTO2ZFxihRoYcxeqhSxBA1KzCOGKeAIQ7PWwHrNl5INP9Z1gVgqrbRpN+gpLXAWoDBwcEsYnKue0gTV8Qsanc07Ve/gcPG4/jaOU5pZqFVMHY2FJHmGzLqLa3xalMRHA/PKsSWTKGhVVNoGBdsbAVWJhfPxn1qFUrVUYrVMUqVEfoqo4xXK0DoOpON/9OVu3Dguov+9mVdAH5jcoEdAI407mBmG4ANEMYAsgvNOdd1CkUo9E+/nxRbR/0wc/7Fj+t8oRDugOmEG1OzHt7+FrhG0hJJZeBBYGvGMTjnnCPjFoCZVSU9DnxKKIBvmNn+LGNwzjkXZP5JYDP7BPgk65/rnHNusu77hINzzrmW8ALgnHM55QXAOedyyguAc87llBcA55zLqY7+OmhJx4DD/+Ml5gN/tCicTpaXPCE/ueYlT8hPrlnmeYWZTftlYR1dAP4vSTsv5Bvxul1e8oT85JqXPCE/uXZint4F5JxzOeUFwDnncir1ArCh3QFkJC95Qn5yzUuekJ9cOy7PpMcAnHPOnVvqLQDnnHPnkGQBkLRa0o+SfpK0vt3xtJKkNyQNSdrXsG2upG2SDsbnOe2MsRUkXS7pC0kHJO2XtC5uTzHXPknfSPo+5vp83L5E0o6Y67vxK9S7nqSipN2SPo7rqeZ5SNJeSXsk7YzbOur4Ta4AxInnXwPuAJYCD0la2t6oWupNYHXTtvXAdjO7Btge17tdFXjSzK4FlgOPxd9jirmOAqvM7HpgGbBa0nLgJeCVmOsJ4NE2xthK64ADDeup5glwq5kta7j9s6OO3+QKAA0Tz5vZGFCfeD4JZvYVcLxp873Apri8Cbgv06AuAjM7ambfxeUzhBPGItLM1czsz7haig8DVgHvxe1J5CppALgL2BjXRYJ5nkdHHb8pFoCpJp5PffLUy8zsKIQTJ7CgzfG0lKTFwA3ADhLNNXaL7AGGgG3Az8BJM6vGXVI5jl8FngbqM7TPI808IRTxzyTtinOdQ4cdv5lPCJOBaSeed91D0izgfeAJMzsdLhjTY2Y1YJmk2cAW4Nqpdss2qtaSdDcwZGa7JK2sb55i167Os8EKMzsiaQGwTdIP7Q6oWYotgGknnk/Q75IWAsTnoTbH0xKSSoST/1tm9kHcnGSudWZ2EviSMO4xW1L9Ii2F43gFcI+kQ4Su2VWEFkFqeQJgZkfi8xChqN9Ihx2/KRaAPE48vxVYE5fXAB+1MZaWiH3DrwMHzOzlhn9KMddL45U/kvqB2whjHl8A98fduj5XM3vGzAbMbDHh7/JzM3uYxPIEkDRT0iX1ZeB2YB8ddvwm+UEwSXcSrizqE8+/2OaQWkbSO8BKwjcL/g48B3wIbAYGgV+AB8yseaC4q0i6Bfga2MtEf/GzhHGA1HK9jjAgWCRclG02sxckXUm4Up4L7AYeMbPR9kXaOrEL6CkzuzvFPGNOW+JqD/C2mb0oaR4ddPwmWQCcc85NL8UuIOeccxfAC4BzzuWUFwDnnMspLwDOOZdTXgCccy6nvAA451xOeQFwzrmc8gLgnHM59Tcnq+40vZg4eQAAAABJRU5ErkJggg==\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fire = ForestFire(100, 100, 0.8)\n", - "fire.run_model()\n", - "results = fire.dc.get_model_vars_dataframe()\n", - "results.plot()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "... But to really understand how the final outcome varies with density, we can't just tweak the parameter by hand over and over again. We need to do a batch run. \n", - "\n", - "## Batch runs\n", - "\n", - "Batch runs, also called parameter sweeps, allow use to systemically vary the density parameter, run the model, and check the output. Mesa provides a BatchRunner object which takes a model class, a dictionary of parameters and the range of values they can take and runs the model at each combination of these values. We can also give it reporters, which collect some data on the model at the end of each run and store it, associated with the parameters that produced it.\n", - "\n", - "For ease of typing and reading, we'll first create the parameters to vary and the reporter, and then assign them to a new BatchRunner." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "fixed_params = dict(height=50, width=50) # Height and width are constant\n", - "# Vary density from 0.01 to 1, in 0.01 increments:\n", - "variable_params = dict(density=np.linspace(0, 1, 101)[1:])" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "# At the end of each model run, calculate the fraction of trees which are Burned Out\n", - "model_reporter = {\n", - " \"BurnedOut\": lambda m: (\n", - " ForestFire.count_type(m, \"Burned Out\") / m.schedule.get_agent_count()\n", - " )\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "# Create the batch runner\n", - "param_run = BatchRunner(\n", - " ForestFire,\n", - " variable_parameters=variable_params,\n", - " fixed_parameters=fixed_params,\n", - " model_reporters=model_reporter,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now the BatchRunner, which we've named param_run, is ready to go. To run the model at every combination of parameters (in this case, every density value), just use the **run_all()** method." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100it [00:04, 11.23it/s]\n" - ] - } - ], - "source": [ - "param_run.run_all()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Like with the data collector, we can extract the data the batch runner collected into a dataframe:" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "df = param_run.get_model_vars_dataframe()" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
densityRunBurnedOutheightwidth
00.0100.0250005050
720.7300.9899835050
710.7200.9928965050
700.7100.9810695050
690.7000.9800575050
\n", - "
" - ], - "text/plain": [ - " density Run BurnedOut height width\n", - "0 0.01 0 0.025000 50 50\n", - "72 0.73 0 0.989983 50 50\n", - "71 0.72 0 0.992896 50 50\n", - "70 0.71 0 0.981069 50 50\n", - "69 0.70 0 0.980057 50 50" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df.head()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As you can see, each row here is a run of the model, identified by its parameter values (and given a unique index by the Run column). To view how the BurnedOut fraction varies with density, we can easily just plot them:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(0, 1)" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.scatter(df.density, df.BurnedOut)\n", - "plt.xlim(0, 1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And we see the very clear emergence of a critical value around 0.5, where the model quickly shifts from almost no trees being burned, to almost all of them.\n", - "\n", - "In this case we ran the model only once at each value. However, it's easy to have the BatchRunner execute multiple runs at each parameter combination, in order to generate more statistically reliable results. We do this using the *iteration* argument.\n", - "\n", - "Let's run the model 5 times at each parameter point, and export and plot the results as above." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "500it [00:22, 11.33it/s] \n" - ] - }, - { - "data": { - "text/plain": [ - "(0, 1)" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "param_run = BatchRunner(\n", - " ForestFire,\n", - " variable_params,\n", - " fixed_params,\n", - " iterations=5,\n", - " model_reporters=model_reporter,\n", - ")\n", - "param_run.run_all()\n", - "df = param_run.get_model_vars_dataframe()\n", - "plt.scatter(df.density, df.BurnedOut)\n", - "plt.xlim(0, 1)" - ] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.6" - }, - "widgets": { - "state": {}, - "version": "1.1.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/forest_fire/forest_fire/__init__.py b/examples/forest_fire/forest_fire/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/forest_fire/forest_fire/agent.py b/examples/forest_fire/forest_fire/agent.py deleted file mode 100644 index 34ff2aa24ad..00000000000 --- a/examples/forest_fire/forest_fire/agent.py +++ /dev/null @@ -1,36 +0,0 @@ -import mesa - - -class TreeCell(mesa.Agent): - """ - A tree cell. - - Attributes: - x, y: Grid coordinates - condition: Can be "Fine", "On Fire", or "Burned Out" - unique_id: (x,y) tuple. - - unique_id isn't strictly necessary here, but it's good - practice to give one to each agent anyway. - """ - - def __init__(self, pos, model): - """ - Create a new tree. - Args: - pos: The tree's coordinates on the grid. - model: standard model reference for agent. - """ - super().__init__(pos, model) - self.pos = pos - self.condition = "Fine" - - def step(self): - """ - If the tree is on fire, spread it to fine trees nearby. - """ - if self.condition == "On Fire": - for neighbor in self.model.grid.iter_neighbors(self.pos, True): - if neighbor.condition == "Fine": - neighbor.condition = "On Fire" - self.condition = "Burned Out" diff --git a/examples/forest_fire/forest_fire/model.py b/examples/forest_fire/forest_fire/model.py deleted file mode 100644 index de74118f26e..00000000000 --- a/examples/forest_fire/forest_fire/model.py +++ /dev/null @@ -1,66 +0,0 @@ -import mesa - -from .agent import TreeCell - - -class ForestFire(mesa.Model): - """ - Simple Forest Fire model. - """ - - def __init__(self, width=100, height=100, density=0.65): - """ - Create a new forest fire model. - - Args: - width, height: The size of the grid to model - density: What fraction of grid cells have a tree in them. - """ - # Set up model objects - self.schedule = mesa.time.RandomActivation(self) - self.grid = mesa.space.Grid(width, height, torus=False) - - self.datacollector = mesa.DataCollector( - { - "Fine": lambda m: self.count_type(m, "Fine"), - "On Fire": lambda m: self.count_type(m, "On Fire"), - "Burned Out": lambda m: self.count_type(m, "Burned Out"), - } - ) - - # Place a tree in each cell with Prob = density - for (contents, x, y) in self.grid.coord_iter(): - if self.random.random() < density: - # Create a tree - new_tree = TreeCell((x, y), self) - # Set all trees in the first column on fire. - if x == 0: - new_tree.condition = "On Fire" - self.grid.place_agent(new_tree, (x, y)) - self.schedule.add(new_tree) - - self.running = True - self.datacollector.collect(self) - - def step(self): - """ - Advance the model by one step. - """ - self.schedule.step() - # collect data - self.datacollector.collect(self) - - # Halt if no more fire - if self.count_type(self, "On Fire") == 0: - self.running = False - - @staticmethod - def count_type(model, tree_condition): - """ - Helper method to count trees in a given condition in a given model. - """ - count = 0 - for tree in model.schedule.agents: - if tree.condition == tree_condition: - count += 1 - return count diff --git a/examples/forest_fire/forest_fire/server.py b/examples/forest_fire/forest_fire/server.py deleted file mode 100644 index 6d8f9fd31cf..00000000000 --- a/examples/forest_fire/forest_fire/server.py +++ /dev/null @@ -1,36 +0,0 @@ -import mesa - -from .model import ForestFire - -COLORS = {"Fine": "#00AA00", "On Fire": "#880000", "Burned Out": "#000000"} - - -def forest_fire_portrayal(tree): - if tree is None: - return - portrayal = {"Shape": "rect", "w": 1, "h": 1, "Filled": "true", "Layer": 0} - (x, y) = tree.pos - portrayal["x"] = x - portrayal["y"] = y - portrayal["Color"] = COLORS[tree.condition] - return portrayal - - -canvas_element = mesa.visualization.CanvasGrid( - forest_fire_portrayal, 100, 100, 500, 500 -) -tree_chart = mesa.visualization.ChartModule( - [{"Label": label, "Color": color} for (label, color) in COLORS.items()] -) -pie_chart = mesa.visualization.PieChartModule( - [{"Label": label, "Color": color} for (label, color) in COLORS.items()] -) - -model_params = { - "height": 100, - "width": 100, - "density": mesa.visualization.Slider("Tree density", 0.65, 0.01, 1.0, 0.01), -} -server = mesa.visualization.ModularServer( - ForestFire, [canvas_element, tree_chart, pie_chart], "Forest Fire", model_params -) diff --git a/examples/forest_fire/readme.md b/examples/forest_fire/readme.md deleted file mode 100644 index 6a16f976d02..00000000000 --- a/examples/forest_fire/readme.md +++ /dev/null @@ -1,41 +0,0 @@ -# Forest Fire Model - -## Summary - -The [forest fire model](http://en.wikipedia.org/wiki/Forest-fire_model) is a simple, cellular automaton simulation of a fire spreading through a forest. The forest is a grid of cells, each of which can either be empty or contain a tree. Trees can be unburned, on fire, or burned. The fire spreads from every on-fire tree to unburned neighbors; the on-fire tree then becomes burned. This continues until the fire dies out. - -## How to Run - -To run the model interactively, run ``mesa runserver`` in this directory. e.g. - -``` - $ mesa runserver -``` - -Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. - -To view and run the model analyses, use the ``Forest Fire Model`` Notebook. - -## Files - -### ``forest_fire/model.py`` - -This defines the model. There is one agent class, **TreeCell**. Each TreeCell object which has (x, y) coordinates on the grid, and its condition is *Fine* by default. Every step, if the tree's condition is *On Fire*, it spreads the fire to any *Fine* trees in its [Von Neumann neighborhood](http://en.wikipedia.org/wiki/Von_Neumann_neighborhood) before changing its own condition to *Burned Out*. - -The **ForestFire** class is the model container. It is instantiated with width and height parameters which define the grid size, and density, which is the probability of any given cell having a tree in it. When a new model is instantiated, cells are randomly filled with trees with probability equal to density. All the trees in the left-hand column (x=0) are set to *On Fire*. - -Each step of the model, trees are activated in random order, spreading the fire and burning out. This continues until there are no more trees on fire -- the fire has completely burned out. - - -### ``forest_fire/server.py`` - -This code defines and launches the in-browser visualization for the ForestFire model. It includes the **forest_fire_draw** method, which takes a TreeCell object as an argument and turns it into a portrayal to be drawn in the browser. Each tree is drawn as a rectangle filling the entire cell, with a color based on its condition. *Fine* trees are green, *On Fire* trees red, and *Burned Out* trees are black. - -## Further Reading - -Read about the Forest Fire model on Wikipedia: http://en.wikipedia.org/wiki/Forest-fire_model - -This is directly based on the comparable NetLogo model: - -Wilensky, U. (1997). NetLogo Fire model. http://ccl.northwestern.edu/netlogo/models/Fire. Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. - diff --git a/examples/forest_fire/requirements.txt b/examples/forest_fire/requirements.txt deleted file mode 100644 index bcbfbbe220b..00000000000 --- a/examples/forest_fire/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -jupyter -matplotlib -mesa diff --git a/examples/forest_fire/run.py b/examples/forest_fire/run.py deleted file mode 100644 index 98c1adf25f2..00000000000 --- a/examples/forest_fire/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from forest_fire.server import server - -server.launch() diff --git a/examples/hex_snowflake/Readme.md b/examples/hex_snowflake/Readme.md deleted file mode 100644 index 990e1dea994..00000000000 --- a/examples/hex_snowflake/Readme.md +++ /dev/null @@ -1,27 +0,0 @@ -# Conway's Game Of "Life" on a hexagonal grid - -## Summary - -In this model, each dead cell will become alive if it has exactly one neighbor. Alive cells stay alive forever. - - -## 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 - -* ``hex_snowflake/cell.py``: Defines the behavior of an individual cell, which can be in two states: DEAD or ALIVE. -* ``hex_snowflake/model.py``: Defines the model itself, initialized with one alive cell at the center. -* ``hex_snowflake/portrayal.py``: Describes for the front end how to render a cell. -* ``hex_snowflake/server.py``: Defines an interactive visualization. -* ``run.py``: Launches the visualization - -## Further Reading -[Explanation of how hexagon neighbors are calculated. (The method is slightly different for Cartesian coordinates)](http://www.redblobgames.com/grids/hexagons/#neighbors-offset) diff --git a/examples/hex_snowflake/hex_snowflake/cell.py b/examples/hex_snowflake/hex_snowflake/cell.py deleted file mode 100644 index 32656c53027..00000000000 --- a/examples/hex_snowflake/hex_snowflake/cell.py +++ /dev/null @@ -1,58 +0,0 @@ -import mesa - - -class Cell(mesa.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__(pos, model) - self.x, self.y = pos - self.state = init_state - self._nextState = None - self.isConsidered = False - - @property - def isAlive(self): - return self.state == self.ALIVE - - @property - def neighbors(self): - return self.model.grid.iter_neighbors((self.x, self.y)) - - @property - def considered(self): - return self.isConsidered is True - - def step(self): - """ - Compute if the cell will be dead or alive at the next tick. A dead - cell will become alive if it has only one neighbor. 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. - When a cell is made alive, its neighbors are able to be considered in the next step. Only cells that are considered check their neighbors for performance reasons. - """ - # assume no state change - self._nextState = self.state - - if not self.isAlive and self.isConsidered: - # 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) - - if live_neighbors == 1: - self._nextState = self.ALIVE - for a in self.neighbors: - a.isConsidered = True - - def advance(self): - """ - Set the state to the new computed state -- computed in step(). - """ - self.state = self._nextState diff --git a/examples/hex_snowflake/hex_snowflake/model.py b/examples/hex_snowflake/hex_snowflake/model.py deleted file mode 100644 index 6a932907cd4..00000000000 --- a/examples/hex_snowflake/hex_snowflake/model.py +++ /dev/null @@ -1,46 +0,0 @@ -import mesa - -from hex_snowflake.cell import Cell - - -class HexSnowflake(mesa.Model): - """ - Represents the hex grid of cells. The grid is represented by a 2-dimensional array of cells with adjacency rules specific to hexagons. - """ - - def __init__(self, width=50, height=50): - """ - Create a new playing area of (width, height) cells. - """ - - # Set up the grid and schedule. - - # Use SimultaneousActivation which simulates all the cells - # computing their next state simultaneously. This needs to - # be done because each cell's next state depends on the current - # state of all its neighbors -- before they've changed. - self.schedule = mesa.time.SimultaneousActivation(self) - - # Use a hexagonal grid, where edges wrap around. - self.grid = mesa.space.HexGrid(width, height, torus=True) - - # Place a dead cell at each location. - for (contents, x, y) in self.grid.coord_iter(): - cell = Cell((x, y), self) - self.grid.place_agent(cell, (x, y)) - self.schedule.add(cell) - - # activate the center(ish) cell. - centerishCell = self.grid[width // 2][height // 2] - - centerishCell.state = 1 - for a in centerishCell.neighbors: - a.isConsidered = True - - self.running = True - - def step(self): - """ - Have the scheduler advance each cell by one step - """ - self.schedule.step() diff --git a/examples/hex_snowflake/hex_snowflake/portrayal.py b/examples/hex_snowflake/hex_snowflake/portrayal.py deleted file mode 100644 index a0a4020e896..00000000000 --- a/examples/hex_snowflake/hex_snowflake/portrayal.py +++ /dev/null @@ -1,18 +0,0 @@ -def portrayCell(cell): - """ - This function is registered with the visualization server to be called - each tick to indicate how to draw the cell in its current state. - :param cell: the cell in the simulation - :return: the portrayal dictionary. - """ - if cell is None: - raise AssertionError - return { - "Shape": "hex", - "r": 1, - "Filled": "true", - "Layer": 0, - "x": cell.x, - "y": cell.y, - "Color": "black" if cell.isAlive else "white", - } diff --git a/examples/hex_snowflake/hex_snowflake/server.py b/examples/hex_snowflake/hex_snowflake/server.py deleted file mode 100644 index 3095bd5c582..00000000000 --- a/examples/hex_snowflake/hex_snowflake/server.py +++ /dev/null @@ -1,13 +0,0 @@ -import mesa - -from hex_snowflake.portrayal import portrayCell -from hex_snowflake.model import HexSnowflake - -width, height = 50, 50 - -# Make a world that is 50x50, on a 500x500 display. -canvas_element = mesa.visualization.CanvasHexGrid(portrayCell, width, height, 500, 500) - -server = mesa.visualization.ModularServer( - HexSnowflake, [canvas_element], "Hex Snowflake", {"height": height, "width": width} -) diff --git a/examples/hex_snowflake/requirements.txt b/examples/hex_snowflake/requirements.txt deleted file mode 100644 index 1ad1bbec7ab..00000000000 --- a/examples/hex_snowflake/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -mesa \ No newline at end of file diff --git a/examples/hex_snowflake/run.py b/examples/hex_snowflake/run.py deleted file mode 100644 index 8bc7c1469df..00000000000 --- a/examples/hex_snowflake/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from hex_snowflake.server import server - -server.launch() diff --git a/examples/pd_grid/analysis.ipynb b/examples/pd_grid/analysis.ipynb deleted file mode 100644 index 53a63345884..00000000000 --- a/examples/pd_grid/analysis.ipynb +++ /dev/null @@ -1,231 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Demographic Prisoner's Dilemma\n", - "\n", - "The Demographic Prisoner's Dilemma is a family of variants on the classic two-player [Prisoner's Dilemma](https://en.wikipedia.org/wiki/Prisoner's_dilemma), first developed by [Joshua Epstein](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.8.8629&rep=rep1&type=pdf). The model consists of agents, each with a strategy of either Cooperate or Defect. Each agent's payoff is based on its strategy and the strategies of its spatial neighbors. After each step of the model, the agents adopt the strategy of their neighbor with the highest total score. \n", - "\n", - "The specific variant presented here is adapted from the [Evolutionary Prisoner's Dilemma](http://ccl.northwestern.edu/netlogo/models/PDBasicEvolutionary) model included with NetLogo. Its payoff table is a slight variant of the traditional PD payoff table:\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "
**Cooperate****Defect**
**Cooperate**1, 10, *D*
**Defect***D*, 00, 0
\n", - "\n", - "Where *D* is the defection bonus, generally set higher than 1. In these runs, the defection bonus is set to $D=1.6$.\n", - "\n", - "The Demographic Prisoner's Dilemma demonstrates how simple rules can lead to the emergence of widespread cooperation, despite the Defection strategy dominiating each individual interaction game. However, it is also interesting for another reason: it is known to be sensitive to the activation regime employed in it.\n", - "\n", - "Below, we demonstrate this by instantiating the same model (with the same random seed) three times, with three different activation regimes: \n", - "\n", - "* Sequential activation, where agents are activated in the order they were added to the model;\n", - "* Random activation, where they are activated in random order every step;\n", - "* Simultaneous activation, simulating them all being activated simultaneously.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from pd_grid.model import PdGrid\n", - "\n", - "import numpy as np\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import matplotlib.gridspec\n", - "\n", - "%matplotlib inline" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Helper functions" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "bwr = plt.get_cmap(\"bwr\")\n", - "\n", - "\n", - "def draw_grid(model, ax=None):\n", - " \"\"\"\n", - " Draw the current state of the grid, with Defecting agents in red\n", - " and Cooperating agents in blue.\n", - " \"\"\"\n", - " if not ax:\n", - " fig, ax = plt.subplots(figsize=(6, 6))\n", - " grid = np.zeros((model.grid.width, model.grid.height))\n", - " for agent, x, y in model.grid.coord_iter():\n", - " if agent.move == \"D\":\n", - " grid[y][x] = 1\n", - " else:\n", - " grid[y][x] = 0\n", - " ax.pcolormesh(grid, cmap=bwr, vmin=0, vmax=1)\n", - " ax.axis(\"off\")\n", - " ax.set_title(\"Steps: {}\".format(model.schedule.steps))" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "def run_model(model):\n", - " \"\"\"\n", - " Run an experiment with a given model, and plot the results.\n", - " \"\"\"\n", - " fig = plt.figure(figsize=(12, 8))\n", - "\n", - " ax1 = fig.add_subplot(231)\n", - " ax2 = fig.add_subplot(232)\n", - " ax3 = fig.add_subplot(233)\n", - " ax4 = fig.add_subplot(212)\n", - "\n", - " draw_grid(model, ax1)\n", - " model.run(10)\n", - " draw_grid(model, ax2)\n", - " model.run(10)\n", - " draw_grid(model, ax3)\n", - " model.datacollector.get_model_vars_dataframe().plot(ax=ax4)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "# Set the random seed\n", - "seed = 21" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Sequential Activation" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAtIAAAHiCAYAAADF+CuaAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMi4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvIxREBQAAIABJREFUeJzt3Xm4XXWd5/v3NzPkHCBk2AGCJOg5UUDBEBGKFCKIAqKIU4HeFoenkNKyy7baBlsLEK265XDVri7Lvig0eFtjFAFpGwdA0UJECYgKwQwgQ4QMhCkhZP7dP/Y68XByprXWns/79Tz7OXuv8bdX9jr5nLW/+7sjpYQkSZKkfMY1ewCSJElSOzJIS5IkSQUYpCVJkqQCDNKSJElSAQZpSZIkqQCDtCRJklSAQVqSJEkqwCDdISJiUUTcFhFPR8QTEfGLiHhFNu/dEXFrs8cIEBHviIiHIuLZiLguIvZv9pikRmiHczQiDoiI6yPi0YhIETF3wPzJEXFFRDwTEWsi4iPNGalUX21yvr4+Im6NiKey8/GrEdHdb77nawMYpDtAROwDfB/478D+wEHAJ4GtzRzXQBFxOPD/Av8BqACbgX9r6qCkBmiXcxTYBfwQeMsQ8y8BeoBDgFcD/yUiTm3M0KTGaKPzdV/g08CBwEuAOcDn+s2/BM/X+kspeWvzG7AQeGqIeS8BtgA7gU19ywGTgc8DDwNrgf8B7JXNOxFYDfxX4HHgQeCd/bZ5OrAM2Aj8CfjPoxznPwHf7Pf4hcA2oLvZx9Cbt3re2uUc7bf+BCABcwdM/xPw2n6PPwV8q9nH15u3Wt7a7Xztt503A7/v99jztQE3r0h3hhXAzoi4KiJOi4hpfTNSSvcB5wO/TCl1pZT2y2Z9BugFjgJeRPUv7ov6bXM2MCObfi5wWUTMz+ZdDrw/pdQNHAH8pG+l7C2mRUOM83Dgt/3Gdj/VIN1b7GlLbaNdztEhZWM+kH7ncHb/8Lzbklpcu56vJwD3Zut5vjaIQboDpJSeARZRvYL0VWB9VudYGWz5iAjgr4H/lFJ6IqW0kerV4rMHLPoPKaWtKaWfAf8HeHs2fTtwWETsk1J6MqV0V7+x7JdSGqp2rAt4esC0p4HuQZaVOkYbnaPD6cp+9j+HPX/VcdrxfI2IU6gG9L7w7vnaIAbpDpFSui+l9O6U0hyqf9EeCHxpiMVnAnsDd2Z/7T5FtS5yZr9lnkwpPdvv8UPZNqFaP3k68FBE/CwijhvlMDcB+wyYtg/Vt7OkjtYm5+hwNmU/+5/Dnr/qSO10vkbEscA3gbemlFZkkz1fG8Qg3YFSSn8ArqR68kP1r+r+HgeeAw7P/trdL6W0b0qpq98y0yJiar/HLwAezbZ/R0rpTGAWcB3w7VEO7V7gyL4HEXEo1bqyFUOuIXWgFj5Hhxvzk8Bj9DuHs/v3lt221Mpa+XyNiJcD1wPvTSnd3G/Mnq8NYpDuABHx4oj4+4iYkz0+GDgHuD1bZC0wJyImAaSUdlF9u+qLETErW+egiHjdgE1/MiImRcRfAmcA38kevzMi9k0pbQeeofqhi9H4BvCGiPjL7BfKpcA12dtgUsdqo3OUiJhC9Q9cgMnZ4z5fBz4REdMi4sVU386+crTbltpBu5yvEXEE1SvfH0op/e9BFvF8bQCDdGfYCLwS+FVEPEv1ZL8H+Pts/k+o/hW6JiIez6ZdAKwCbo+IZ4CbgPn9trkGeJLqX8zfAM7P/iqHavu6B7P1zgf+r76VImJT9ktiDymle7PlvwGso1qr9YESz1tqF21xjmae489vC/8he9znYuB+qm9L/wz4XErph6M6AlL7aJfz9e+plo9cni23KSL6X3H2fG2ASGngOxQa6yLiROB/ZbVhklqM56jUPjxfO5tXpCVJkqQCDNKSJElSAZZ2SJIkSQV4RVqSJEkqwCAtSZIkFTCh2QMYVsTz6k5ijx7o9ZOIXMuPNLaRtpf3uQ3c3sD1R5o/3PbKHue8+y67/YHKHos8+671v9tIy5NSvhdmow04Z6Uxr5XPWc9X6fkKnK9ekZYkSZIKMEhLkiRJBRikJUmSpAJaukZ6pFrXsttr1LqjWT9vzXPZ9YcbT9mx1HpsZWui846n6LK1MPKxkCRJrcIr0pIkSVIBBmlJkiSpAIO0JEmSVEBL10iX7b08cP0y26t1H+i865ftU11GrXtWl91fXiONd7j9la3nbua/myRJqi+vSEuSJEkFGKQlSZKkAgzSkiRJUgEtXSNda3lrZfNsq6yyvZLzjqdML+Vaq3dNdd75/R+X7f89UK17o0uSpObxirQkSZJUgEFakiRJKsAgLUmSJBXQ0jXS9e4fPNzyIy1b6x7XI6n18sMdm3rXf5f5dxnN8nnnD7f9Wo9NkiR1Dq9IS5IkSQUYpCVJkqQCDNKSJElSAS1dIz2SsnXKtawFrnfdb1l5eivXeiz1fq71PPZltz0Sa6olSWpfXpGWJEmSCjBIS5IkSQUYpCVJkqQC2rpGutb1q3l6Kde7j3TZ3sllanUb3Ud6pOXzrl/r8efZdq17ZkuSpNblFWlJkiSpAIO0JEmSVIBBWpIkSSqgrWukBypbvzrcsiPJW/ta7xrqvOMfro90nh7UtVDP5zbY+mX6XJc9FvlfB9ZQS5LUKrwiLUmSJBVgkJYkSZIKMEhLkiRJBbRVjXS9e/TmqW+tdw11XmXqfMuOpWyf6LL/jmV7bNezJrrWrxNJktQ6vCItSZIkFWCQliRJkgowSEuSJEkFtFWN9EjK1s7m2Va963pr2RN7NPPzbDtvn+eyy+dV62PXSCOPXZIktQqvSEuSJEkFGKQlSZKkAgzSkiRJUgEtXSNdpsYZ8te+9l++0fseSa1romtZH15W2R7YjaxxbnQ99Z7HwippSZJahVekJUmSpAIM0pIkSVIBBmlJkiSpgJaukS7bD7hMbW+j64TzyltHXEaZWvNazM/7XMv2wa7nv22Z2nVJktRavCItSZIkFWCQliRJkgowSEuSJEkFtHSNdFl5a2FrWZ+at+62bJ1u2drbWtYF17N2fTTqeaxr3c877/J2kZYkqXV4RVqSJEkqwCAtSZIkFWCQliRJkgpoqxrpRtbe5l231jXO9a6xHm5/ecfS6J7aZfeX51jmfa556+zzb88qaUmSWoVXpCVJkqQCDNKSJElSAQZpSZIkqYCWrpEu2/e5TF/oWtYcj2b5Wo8n7/aGU3asI22v3vXgeccz3PbqXwM9/PpWSEuS1Dq8Ii1JkiQVYJCWJEmSCjBIS5IkSQW0dI10reWpzc1bC1vrut1a72+gMjXWta7XzltfXuvtD7e/keqn89bp5621lyRJrcsr0pIkSVIBBmlJkiSpAIO0JEmSVEBb10jXsw653jXHtR57LXs3l61RrnXNc73rzfOMN29v89r3PreGWpKkVuEVaUmSJKkAg7QkSZJUgEFakiRJKqCla6Tz1p+W1X/7ZWtfmz1/oDzHLm9N8kjbLls/3sy+1Y3uA13vntqSJKl2vCItSZIkFWCQliRJkgowSEuSJEkFtHSNdPmeu8MvP1w9a61rU2tdO1vP/ZV97o2u6611rfxw6t2z2ppoSZLah1ekJUmSpAIM0pIkSVIBBmlJkiSpgJaukc5b+1q213KZfZcda975eXs95+nNXLaXca2PxUjL17N3c62Pc/l/V0mS1Cq8Ii1JkiQVYJCWJEmSCmjp0o6y8pYA1LL0I+9b+I38+vPR7D/PsmWfS9nnWrakJ08bxJG+/rzW7ev23J7FHZIktQqvSEuSJEkFGKQlSZKkAgzSkiRJUgFtXSNdy3Z3g61fZix5t93orxAfbv1af2112frvWn8td5nlyz6XRn6duSRJqi+vSEuSJEkFGKQlSZKkAgzSkiRJUgFtXSNd617MZb4aeqSxlVXr/sR5xlfPr+BuxP4HKvM6qXetuyRJah9ekZYkSZIKMEhLkiRJBRikJUmSpALaukY6bx/pWta35q2hLts7udH7q+W2B6p1H+m8NdhlXie1HstI6++5PUmS1Cq8Ii1JkiQVYJCWJEmSCjBIS5IkSQW0dI10rXsn13J7eets89bW5t1+LcdT777RZeu9692Hutavu+Hk71dulbQkSa3CK9KSJElSAQZpSZIkqQCDtCRJklRAS9dI562NrWWdctk62bw10WW3P1LN9Ejy9E6ud/123n7fZV8XjewbXb4+XJIktQqvSEuSJEkFGKQlSZKkAgzSkiRJUgEtXSNd6zrlkepZm9lbOe/+yvZGHm48ta5Zzru9WteXlzlWtR5r+eWtkpYkqVV4RVqSJEkqwCAtSZIkFWCQliRJkgpo6RrpvMr36B19bexI+85Tfz2a9QcqWz+eZ9u1PK6jmT9Q3uVHGl+ZdetZmz7YY0mS1Lq8Ii1JkiQVYJCWJEmSCjBIS5IkSQW0dI102d7NI8lT/5q3VrZsnXC964iH21/eOt28zyWvssdmoDK9oevd43rk5ypJklqFV6QlSZKkAgzSkiRJUgEGaUmSJKmAlq6RrnXd8EjzG9lvuGy/4Eb2Yi5b351X/euMR//c8/471rsntiRJah1ekZYkSZIKMEhLkiRJBRikJUmSpAJaukZ6JGXrkIebX+8e1o2u985TN1zrsQ1U5t9lsP2V7fGdZ/+1rgcfqN6vO0mSVDtekZYkSZIKMEhLkiRJBRikJUmSpALaqka6lv2BB9N/e2XrbmvdH7jetbl59lXvntm17iNddvlarTsaI2/PGmpJklqFV6QlSZKkAgzSkiRJUgEGaUmSJKmAlq6RrnXtbT3lrSsuW4dctmZ6uPVHGmuebY1mbGX7So+kbF/peo4lfz23JElqFV6RliRJkgowSEuSJEkFGKQlSZKkAlq6RnokeWtvy/QALtubuNY102UNd+zK7jvvsSrbE7uex6rWPbPLb88qaUmSWoVXpCVJkqQCDNKSJElSAQZpSZIkqYC2qpEuW3ubpw65bN3uQGVrputtuP3Vusd1Kz03qG3tfLOfqyRJahyvSEuSJEkFGKQlSZKkAgzSkiRJUgFtVSOdt245b31q/+3Xu290rbeXtz/xcPvPO9a8yvZibmRN9khjrfW/syRJah9ekZYkSZIKMEhLkiRJBRikJUmSpALaqka6bO1umdrcvLWtta7brff2htt23rHk2Vcj9l/L8VkDLUmS+nhFWpIkSSrAIC1JkiQVYJCWJEmSCmjpGul61+KW7b1cZl95lR1bnhrrWvakLqLZdcjD9dSuZ236SGOpri9JklqFV6QlSZKkAgzSkiRJUgEGaUmSJKmAlq6Rzitv/Wqe5cvWCZettR1p/bLbG25beeeXPe71rsnOc2zKjrXWz1WSJLUOr0hLkiRJBRikJUmSpAIM0pIkSVIBLV0jnbcWN2/d8HDbr3e/4Lx1xGX3l2d7te55XXZ7ZevLy7wuytaul7Xn/q2pliSpVXhFWpIkSSrAIC1JkiQVYJCWJEmSCmjpGumBmtmDt2y9dtka5rL14HnG18z67MG2V7ZXc5ljV/Y1VvZ1IUmSWpdXpCVJkqQCDNKSJElSAQZpSZIkqYC2qpEeKG/9aS37B+etaa71+rWuS87z3EfaV6PrxUdSZnt5x1rr+m5JktS6vCItSZIkFWCQliRJkgpo69KOWr+tXmbZsmPJK29JQJnx1Pvr0kdafqR2dWXVs7zC9neSJHUur0hLkiRJBRikJUmSpAIM0pIkSVIBLV0jXev60VrWq9b7K7/L7j/v9of7Wuxat5srq2xLujxq+dXrkiSps3hFWpIkSSrAIC1JkiQVYJCWJEmSCmjpGum89alltzdcbW+jeyeXVcvn2uyvQ2/09vqvX/a5lZ1vjbUkSa3LK9KSJElSAQZpSZIkqQCDtCRJklRAS9dI17pfcC3rTcvUIBdZvt7r13PdRvZ9LrL94Y5lrft1S5KkzuEVaUmSJKkAg7QkSZJUgEFakiRJKqCla6RHMlJ9atkewHnk3VYzx5p3+/Wux250z+089eu1rl23plqSpM7hFWlJkiSpAIO0JEmSVIBBWpIkSSqgrWukB6pnv+K8ta617oVc7zri4Zav93Ov9bHL28M7j7K17NZES5LUObwiLUmSJBVgkJYkSZIKMEhLkiRJBbR1jXTeWtg8PXzLrFtkbCOpd9/p4Xon5+0DXevey3nV8tjWuu6+3rX0kiSpcbwiLUmSJBVgkJYkSZIKMEhLkiRJBbR0jXTeOuO8dct5eiePpN69lUean3f/zexn3Owe23mee61r3cvXb0uSpFbhFWlJkiSpAIO0JEmSVIBBWpIkSSogUrLqUpIkScrLK9KSJElSAQZpSZIkqQCDtCRJklSAQVqSJEkqwCAtSZIkFWCQliRJkgowSEuSJEkFGKQlSZKkAgzSkiRJUgEGaUmSJKkAg7QkSZJUgEFakiRJKsAgLUmSJBVgkJYkSZIKMEhLkiRJBRikJUmSpAIM0pIkSVIBBmlJkiSpAIO0JEmSVIBBWpIkSSrAIC1JkiQVYJCWJEmSCjBIS5IkSQUYpCVJkqQCDNKSJElSAQZpSZIkqQCDtCRJklSAQVqSJEkqwCAtSZIkFWCQliRJkgowSEuSJEkFGKQlSZKkAgzSkiRJUgEGaUmSJKkAg7QkSZJUgEFakiRJKsAgLUmSJBVgkJYkSZIKmNDsAQxnxowZae7cuc0ehiRJkjrcnXfe+XhKaWaedVo6SM+dO5elS5c2exiSJEnqcBHxUN51LO2QJEmSCjBIS5IkSQUYpCVJkqQCWrpGWpIkKa/t27ezevVqtmzZ0uyhqAVNmTKFOXPmMHHixNLbMkhLkqSOsnr1arq7u5k7dy4R0ezhqIWklNiwYQOrV69m3rx5pbdnaYckSeooW7ZsYfr06YZo7SEimD59es3erTBID+LT31/G/7o9dwcUSZLUIgzRGkotXxsG6UHcdv8GbrpvbbOHIUmSpBZmkB5Eb6WLFWs2NnsYkiSpja1Zs4azzz6bF77whRx22GGcfvrprFixotnD2u26665j2bJlux9fdNFF3HTTTTXfz7XXXktE8Ic//KHm2wa4++67ueGGG+qy7ZEYpAfRU+nm0ae3sHHL9mYPRZIktaGUEmeddRYnnngi999/P8uWLeOf/umfWLu2se9479y5c8h5A4P0pZdeymte85qaj2Hx4sUsWrSIb33rWzXfNhikW878SjcAK9dtavJIJElSO/rpT3/KxIkTOf/883dPO+qoo1i0aBEf/ehHOeKII3jpS1/KkiVLgGrwHmz6LbfcwgknnMBZZ53FYYcdxvnnn8+uXbsA+PGPf8xxxx3HggULeNvb3samTdXcMnfuXC699FIWLVrEd77zHb761a/yile8giOPPJK3vOUtbN68mdtuu43rr7+ej370oxx11FHcf//9vPvd7+bqq6/evY2LL76YBQsW8NKXvnT31eT169dzyimnsGDBAt7//vdzyCGH8Pjjjw95HDZt2sQvfvELLr/88ucF6V27dvGBD3yAww8/nDPOOIPTTz99977vvPNOXvWqV3H00Ufzute9jsceewyAE088kQsuuIBjjjmG3t5e/v3f/51t27Zx0UUXsWTJEo466iiWLFnCz372M4466iiOOuooXv7yl7NxY/2qDEZsfxcRVwBnAOtSSkdk0z4HvAHYBtwPvCel9FQ272PA+4CdwH9MKf0om34q8N+A8cDXUkr/XPunUxu9WZBesWYjC14wrcmjkSRJRX3yf9/Lskefqek2DztwHy5+w+HDLnPPPfdw9NFH7zH9mmuu4e677+a3v/0tjz/+OK94xSs44YQTuO222wadDvDrX/+aZcuWccghh3DqqadyzTXXcOKJJ/LpT3+am266ialTp/KZz3yGL3zhC1x00UVAtVfyrbfeCsCGDRv467/+awA+8YlPcPnll/OhD32IN77xjZxxxhm89a1vHfQ5zJgxg7vuuot/+7d/4/Of/zxf+9rX+OQnP8lJJ53Exz72MX74wx9y2WWXDXscrrvuOk499VR6e3vZf//9ueuuu1iwYAHXXHMNDz74IL///e9Zt24dL3nJS3jve9/L9u3b+dCHPsT3vvc9Zs6cyZIlS/j4xz/OFVdcAcCOHTv49a9/zQ033MAnP/lJbrrpJi699FKWLl3Kv/7rvwLwhje8gS9/+cscf/zxbNq0iSlTpgw7xjJG00f6SuBfga/3m3Yj8LGU0o6I+AzwMeCCiDgMOBs4HDgQuCkierN1vgycAqwG7oiI61NKy2hBc6btxV4Tx7NirVekJUlS7dx6662cc845jB8/nkqlwqte9SruuOOOIafvs88+HHPMMRx66KEAnHPOOdx6661MmTKFZcuWcfzxxwOwbds2jjvuuN37+au/+qvd9++55x4+8YlP8NRTT7Fp0yZe97rXjWqsb37zmwE4+uijueaaa3aP/9prrwXg1FNPZdq04S84Ll68mA9/+MMAnH322SxevJgFCxZw66238ra3vY1x48Yxe/ZsXv3qVwOwfPly7rnnHk455RSgWppywAEHDDqmBx98cNB9Hn/88XzkIx/hne98J29+85uZM2fOqJ5vESMG6ZTSzyNi7oBpP+738Hag70+ZM4FvpZS2An+MiFXAMdm8VSmlBwAi4lvZsi0ZpMeNC3oqXaxc5wcOJUlqZyNdOa6Xww8/fHepQn8ppUGXH2o67NmuLSJIKXHKKaewePHiQdeZOnXq7vvvfve7ue666zjyyCO58sorueWWW0bxDGDy5MkAjB8/nh07dow4zoE2bNjAT37yE+655x4igp07dxIRfPaznx32OBx++OH88pe/HPWYBrrwwgt5/etfzw033MCxxx7LTTfdxItf/OJRjzuPWtRIvxf4QXb/IOCRfvNWZ9OGmt6yemZ1s9zOHZIkqYCTTjqJrVu38tWvfnX3tDvuuINp06axZMkSdu7cyfr16/n5z3/OMcccwwknnDDodKiWdvzxj39k165dLFmyhEWLFnHsscfyi1/8glWrVgGwefPmITuCbNy4kQMOOIDt27fzjW98Y/f07u7u3PXDixYt4tvf/jZQrdF+8sknh1z26quv5l3vehcPPfQQDz74II888gjz5s3j1ltvZdGiRXz3u99l165drF27dne4nz9/PuvXr98dpLdv386999477JgGPo/777+fl770pVxwwQUsXLiwbt1CoGSQjoiPAzuAvn+VwTpcp2GmD7bN8yJiaUQsXb9+fZnhldJb6WLdxq08vdnOHZIkKZ+I4Nprr+XGG2/khS98IYcffjiXXHIJ73jHO3jZy17GkUceyUknncRnP/tZZs+ezVlnnTXodIDjjjuOCy+8kCOOOIJ58+Zx1llnMXPmTK688krOOeccXvayl3HssccOGRg/9alP8cpXvpJTTjnleVdmzz77bD73uc/x8pe/nPvvv39Uz+viiy/mxz/+MQsWLOAHP/gBBxxwAN3d3YMuu3jxYs4666znTXvLW97CN7/5Td7ylrcwZ84cjjjiCN7//vfzyle+kn333ZdJkyZx9dVXc8EFF3DkkUdy1FFHcdtttw07ple/+tUsW7Zs94cNv/SlL3HEEUdw5JFHstde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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "m = PdGrid(50, 50, \"Sequential\", seed=seed)\n", - "run_model(m)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Random Activation" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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/oQQJsUYy9PZpuhnpC9/eBrT56/M6j5H7dSdS48fvc3rp06dyr777vuZ5ffffz+TJ0/m7bff5pNPPmG//fZjzJgxvPzyyw0uB3j99deZPn06u+yyC+PHj+f+++9n7Nix/OIXv+Dpp5+mS5cuXH311VxzzTVccsklQF2v5AkTJgBQWVnJt771LQAuvvhibrrpJs477zxOOOGEzYVzQ3r27Mmbb77J9ddfz29+8xv+8pe/cPnll3P44Ydz0UUX8fjjj3PjjTc2+jg9+OCDjB8/nmHDhrHjjjvy5ptvss8++3D//fczd+5cpkyZwuLFixk5ciRnn302Gzdu5LzzzuMf//gHvXr14q677uJnP/sZN998MwDV1dW8/vrrPProo1x++eU8/fTTXHHFFUycOJE//vGPABx//PFcd911HHLIIaxevZpOnTo1OkZJUm7U1kYq12yoK47rFcVb/7xy/Wdnkbt2bEef7h3p26MTB+1WtrlQ3lQ09+vRibKuHSktsThWYSv6QrqQTJgwgTPOOIPS0lL69OnDF77wBd54441tLu/evTv7778/u+66KwBnnHEGEyZMoFOnTkyfPp1DDjkEgA0bNnDQQQdtvp3TTjtt889Tp07l4osvZvny5axevZqjjz66WWM9+eSTAdh33325//77N4//gQceAGD8+PHssMMOje7jjjvu4IILLgDg9NNP54477mCfffZhwoQJfPnLX6akpIS+ffty2GGHAfDuu+8ydepUxo0bB9RFU/r169fgmObOndvgbR5yyCH84Ac/4Gtf+xonn3wyAwYMaNb9lSQls2LtRmYuXMm7i1YxY8EqZi9exfzl61m8aj0ba7acRS4J0LNrXYE8qKwLB+5atsWMcp/U964dLT/UOhT9K7mxmeNcGT16NPfee+9nlm8rJtNYfGbrt6JCCMQYGTduHHfccUeD23Tp0mXzz9/4xjd48MEH2XPPPbnlllt4/vnnm3EPoGPHjgCUlpZSXV3d5Di3VllZybPPPsvUqVMJIVBTU0MIgf/+7/9u9HEYPXo0r7zySrPHtLULL7yQY489lkcffZQDDzyQp59+OicfcpSktmZDdS3vf7KamQtWMXPhqrrieeEqFqz4tDd/j+3aM7xPN/YfvGOqQO64RYHcq2tH2pU26xQVUqvgqz2Bww8/nKqqKv785z9vXvbGG2+www47cNdd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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "m = PdGrid(50, 50, \"Random\", seed=seed)\n", - "run_model(m)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": true - }, - "source": [ - "## Simultaneous Activation" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "m = PdGrid(50, 50, \"Simultaneous\", seed=seed)\n", - "run_model(m)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:mesa]", - "language": "python", - "name": "conda-env-mesa-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.6" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/examples/pd_grid/pd_grid/__init__.py b/examples/pd_grid/pd_grid/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/pd_grid/pd_grid/agent.py b/examples/pd_grid/pd_grid/agent.py deleted file mode 100644 index 57e247240ea..00000000000 --- a/examples/pd_grid/pd_grid/agent.py +++ /dev/null @@ -1,49 +0,0 @@ -import mesa - - -class PDAgent(mesa.Agent): - """Agent member of the iterated, spatial prisoner's dilemma model.""" - - def __init__(self, pos, model, starting_move=None): - """ - Create a new Prisoner's Dilemma agent. - - Args: - pos: (x, y) tuple of the agent's position. - model: model instance - starting_move: If provided, determines the agent's initial state: - C(ooperating) or D(efecting). Otherwise, random. - """ - super().__init__(pos, model) - self.pos = pos - self.score = 0 - if starting_move: - self.move = starting_move - else: - self.move = self.random.choice(["C", "D"]) - self.next_move = None - - @property - def isCooroperating(self): - return self.move == "C" - - def step(self): - """Get the best neighbor's move, and change own move accordingly if better than own score.""" - neighbors = self.model.grid.get_neighbors(self.pos, True, include_center=True) - best_neighbor = max(neighbors, key=lambda a: a.score) - self.next_move = best_neighbor.move - - if self.model.schedule_type != "Simultaneous": - self.advance() - - def advance(self): - self.move = self.next_move - self.score += self.increment_score() - - def increment_score(self): - neighbors = self.model.grid.get_neighbors(self.pos, True) - if self.model.schedule_type == "Simultaneous": - moves = [neighbor.next_move for neighbor in neighbors] - else: - moves = [neighbor.move for neighbor in neighbors] - return sum(self.model.payoff[(self.move, move)] for move in moves) diff --git a/examples/pd_grid/pd_grid/model.py b/examples/pd_grid/pd_grid/model.py deleted file mode 100644 index d2445c88d61..00000000000 --- a/examples/pd_grid/pd_grid/model.py +++ /dev/null @@ -1,62 +0,0 @@ -import mesa - -from .agent import PDAgent - - -class PdGrid(mesa.Model): - """Model class for iterated, spatial prisoner's dilemma model.""" - - schedule_types = { - "Sequential": mesa.time.BaseScheduler, - "Random": mesa.time.RandomActivation, - "Simultaneous": mesa.time.SimultaneousActivation, - } - - # This dictionary holds the payoff for this agent, - # keyed on: (my_move, other_move) - - payoff = {("C", "C"): 1, ("C", "D"): 0, ("D", "C"): 1.6, ("D", "D"): 0} - - def __init__( - self, width=50, height=50, schedule_type="Random", payoffs=None, seed=None - ): - """ - Create a new Spatial Prisoners' Dilemma Model. - - Args: - width, height: Grid size. There will be one agent per grid cell. - schedule_type: Can be "Sequential", "Random", or "Simultaneous". - Determines the agent activation regime. - payoffs: (optional) Dictionary of (move, neighbor_move) payoffs. - """ - self.grid = mesa.space.SingleGrid(width, height, torus=True) - self.schedule_type = schedule_type - self.schedule = self.schedule_types[self.schedule_type](self) - - # Create agents - for x in range(width): - for y in range(height): - agent = PDAgent((x, y), self) - self.grid.place_agent(agent, (x, y)) - self.schedule.add(agent) - - self.datacollector = mesa.DataCollector( - { - "Cooperating_Agents": lambda m: len( - [a for a in m.schedule.agents if a.move == "C"] - ) - } - ) - - self.running = True - self.datacollector.collect(self) - - def step(self): - self.schedule.step() - # collect data - self.datacollector.collect(self) - - def run(self, n): - """Run the model for n steps.""" - for _ in range(n): - self.step() diff --git a/examples/pd_grid/pd_grid/portrayal.py b/examples/pd_grid/pd_grid/portrayal.py deleted file mode 100644 index a7df44a439f..00000000000 --- a/examples/pd_grid/pd_grid/portrayal.py +++ /dev/null @@ -1,19 +0,0 @@ -def portrayPDAgent(agent): - """ - This function is registered with the visualization server to be called - each tick to indicate how to draw the agent in its current state. - :param agent: the agent in the simulation - :return: the portrayal dictionary - """ - if agent is None: - raise AssertionError - return { - "Shape": "rect", - "w": 1, - "h": 1, - "Filled": "true", - "Layer": 0, - "x": agent.pos[0], - "y": agent.pos[1], - "Color": "blue" if agent.isCooroperating else "red", - } diff --git a/examples/pd_grid/pd_grid/server.py b/examples/pd_grid/pd_grid/server.py deleted file mode 100644 index 50095311ac5..00000000000 --- a/examples/pd_grid/pd_grid/server.py +++ /dev/null @@ -1,22 +0,0 @@ -import mesa - -from .portrayal import portrayPDAgent -from .model import PdGrid - - -# Make a world that is 50x50, on a 500x500 display. -canvas_element = mesa.visualization.CanvasGrid(portrayPDAgent, 50, 50, 500, 500) - -model_params = { - "height": 50, - "width": 50, - "schedule_type": mesa.visualization.Choice( - "Scheduler type", - value="Random", - choices=list(PdGrid.schedule_types.keys()), - ), -} - -server = mesa.visualization.ModularServer( - PdGrid, [canvas_element], "Prisoner's Dilemma", model_params -) diff --git a/examples/pd_grid/readme.md b/examples/pd_grid/readme.md deleted file mode 100644 index 8b4bc40c88f..00000000000 --- a/examples/pd_grid/readme.md +++ /dev/null @@ -1,42 +0,0 @@ -# Demographic Prisoner's Dilemma on a Grid - -## Summary - -The Demographic Prisoner's Dilemma is a family of variants on the classic two-player [Prisoner's Dilemma]. The model consists of agents, each with a strategy of either Cooperate or Defect. Each agent's payoff is based on its strategy and the strategies of its spatial neighbors. After each step of the model, the agents adopt the strategy of their neighbor with the highest total score. - -The model payoff table is: - -| | Cooperate | Defect| -|:-------------:|:---------:|:-----:| -| **Cooperate** | 1, 1 | 0, D | -| **Defect** | D, 0 | 0, 0 | - -Where *D* is the defection bonus, generally set higher than 1. In these runs, the defection bonus is set to $D=1.6$. - -The Demographic Prisoner's Dilemma demonstrates how simple rules can lead to the emergence of widespread cooperation, despite the Defection strategy dominating each individual interaction game. However, it is also interesting for another reason: it is known to be sensitive to the activation regime employed in it. - -## How to Run - -##### Web based model simulation - -To run the model interactively, run ``mesa runserver`` in this directory. - -##### Jupyter Notebook - -Launch the ``Demographic Prisoner's Dilemma Activation Schedule.ipynb`` notebook and run the code. - -## Files - -* ``run.py`` is the entry point for the font-end simulations. -* ``pd_grid/``: contains the model and agent classes; the model takes a ``schedule_type`` string as an argument, which determines what schedule type the model uses: Sequential, Random or Simultaneous. -* ``Demographic Prisoner's Dilemma Activation Schedule.ipynb``: Jupyter Notebook for running the scheduling experiment. This runs the model three times, one for each activation type, and demonstrates how the activation regime drives the model to different outcomes. - -## Further Reading - -This model is adapted from: - -Wilensky, U. (2002). NetLogo PD Basic Evolutionary model. http://ccl.northwestern.edu/netlogo/models/PDBasicEvolutionary. Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. - -The Demographic Prisoner's Dilemma originates from: - -[Epstein, J. Zones of Cooperation in Demographic Prisoner's Dilemma. 1998.](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.8.8629&rep=rep1&type=pdf) diff --git a/examples/pd_grid/requirements.txt b/examples/pd_grid/requirements.txt deleted file mode 100644 index bcbfbbe220b..00000000000 --- a/examples/pd_grid/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -jupyter -matplotlib -mesa diff --git a/examples/pd_grid/run.py b/examples/pd_grid/run.py deleted file mode 100644 index ec7d04bebfa..00000000000 --- a/examples/pd_grid/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from pd_grid.server import server - -server.launch() diff --git a/examples/schelling/README.md b/examples/schelling/README.md deleted file mode 100644 index 64cc9c83295..00000000000 --- a/examples/schelling/README.md +++ /dev/null @@ -1,49 +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, run ``mesa runserver`` in this directory. e.g. - -``` - $ mesa runserver -``` - -Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. - -To view and run some example model analyses, launch the IPython Notebook and open ``analysis.ipynb``. Visualizing the analysis also requires [matplotlib](http://matplotlib.org/). - -## How to Run without the GUI - -To run the model with the grid displayed as an ASCII text, run `python run_ascii.py` in this directory. - -## Files - -* ``run.py``: Launches a model visualization server. -* ``run_ascii.py``: Run the model in text mode. -* ``schelling.py``: Contains the agent class, and the overall model class. -* ``server.py``: Defines classes for visualizing the model in the browser via Mesa's modular server, and instantiates a visualization server. -* ``analysis.ipynb``: Notebook demonstrating how to run experiments and parameter sweeps on the model. - -## Further Reading - -Schelling's original paper describing the model: - -[Schelling, Thomas C. Dynamic Models of Segregation. Journal of Mathematical Sociology. 1971, Vol. 1, pp 143-186.](https://www.stat.berkeley.edu/~aldous/157/Papers/Schelling_Seg_Models.pdf) - -An interactive, browser-based explanation and implementation: - -[Parable of the Polygons](http://ncase.me/polygons/), by Vi Hart and Nicky Case. diff --git a/examples/schelling/analysis.ipynb b/examples/schelling/analysis.ipynb deleted file mode 100644 index 64a57e23f2f..00000000000 --- a/examples/schelling/analysis.ipynb +++ /dev/null @@ -1,457 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Schelling Segregation Model\n", - "\n", - "## Background\n", - "\n", - "The Schelling (1971) segregation model is a classic of agent-based modeling, demonstrating how agents following simple rules lead to the emergence of qualitatively different macro-level outcomes. Agents are randomly placed on a grid. There are two types of agents, one constituting the majority and the other the minority. All agents want a certain number (generally, 3) of their 8 surrounding neighbors to be of the same type in order for them to be happy. Unhappy agents will move to a random available grid space. While individual agents do not have a preference for a segregated outcome (e.g. they would be happy with 3 similar neighbors and 5 different ones), the aggregate outcome is nevertheless heavily segregated.\n", - "\n", - "## Implementation\n", - "\n", - "This is a demonstration of running a Mesa model in an IPython Notebook. The actual model and agent code are implemented in Schelling.py, in the same directory as this notebook. Below, we will import the model class, instantiate it, run it, and plot the time series of the number of happy agents." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "%matplotlib inline\n", - "\n", - "from model import Schelling" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we instantiate a model instance: a 10x10 grid, with an 80% change of an agent being placed in each cell, approximately 20% of agents set as minorities, and agents wanting at least 3 similar neighbors." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "model = Schelling(10, 10, 0.8, 0.2, 3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We want to run the model until all the agents are happy with where they are. However, there's no guarantee that a given model instantiation will *ever* settle down. So let's run it for either 100 steps or until it stops on its own, whichever comes first:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "100\n" - ] - } - ], - "source": [ - "while model.running and model.schedule.steps < 100:\n", - " model.step()\n", - "print(model.schedule.steps) # Show how many steps have actually run" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The model has a DataCollector object, which checks and stores how many agents are happy at the end of each step. It can also generate a pandas DataFrame of the data it has collected:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "model_out = model.datacollector.get_model_vars_dataframe()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
happy
00
173
267
372
472
\n", - "
" - ], - "text/plain": [ - " happy\n", - "0 0\n", - "1 73\n", - "2 72\n", - "3 73\n", - "4 72" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "model_out.head()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we can plot the 'happy' series:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "model_out.happy.plot()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For testing purposes, here is a table giving each agent's x and y values at each step." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "x_positions = model.datacollector.get_agent_vars_dataframe()" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
xy
StepAgentID
0(0, 0)01
(0, 1)89
(0, 2)52
(0, 3)00
(0, 4)17
\n", - "
" - ], - "text/plain": [ - " x y\n", - "Step AgentID \n", - "0 (0, 0) 0 1\n", - " (0, 1) 8 9\n", - " (0, 2) 5 2\n", - " (0, 3) 0 0\n", - " (0, 4) 1 7" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "x_positions.head()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Effect of Homophily on segregation\n", - "\n", - "Now, we can do a parameter sweep to see how segregation changes with homophily.\n", - "\n", - "First, we create a function which takes a model instance and returns what fraction of agents are segregated -- that is, have no neighbors of the opposite type." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "from mesa.batchrunner import BatchRunner" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "def get_segregation(model):\n", - " \"\"\"\n", - " Find the % of agents that only have neighbors of their same type.\n", - " \"\"\"\n", - " segregated_agents = 0\n", - " for agent in model.schedule.agents:\n", - " segregated = True\n", - " for neighbor in model.grid.iter_neighbors(agent.pos, True):\n", - " if neighbor.type != agent.type:\n", - " segregated = False\n", - " break\n", - " if segregated:\n", - " segregated_agents += 1\n", - " return segregated_agents / model.schedule.get_agent_count()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, we set up the batch run, with a dictionary of fixed and changing parameters. Let's hold everything fixed except for Homophily." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "fixed_params = {\"height\": 10, \"width\": 10, \"density\": 0.8, \"minority_pc\": 0.2}\n", - "variable_parms = {\"homophily\": range(1, 9)}" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "model_reporters = {\"Segregated_Agents\": get_segregation}" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "param_sweep = BatchRunner(\n", - " Schelling,\n", - " variable_parameters=variable_parms,\n", - " fixed_parameters=fixed_params,\n", - " iterations=10,\n", - " max_steps=200,\n", - " model_reporters=model_reporters,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "80it [00:15, 3.13it/s]\n" - ] - } - ], - "source": [ - "param_sweep.run_all()" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "df = param_sweep.get_model_vars_dataframe()" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.scatter(df.homophily, df.Segregated_Agents)\n", - "plt.grid(True)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:mesa]", - "language": "python", - "name": "conda-env-mesa-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.6" - }, - "widgets": { - "state": {}, - "version": "1.1.2" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/examples/schelling/model.py b/examples/schelling/model.py deleted file mode 100644 index 52cfafa2c57..00000000000 --- a/examples/schelling/model.py +++ /dev/null @@ -1,89 +0,0 @@ -import mesa - - -class SchellingAgent(mesa.Agent): - """ - Schelling segregation agent - """ - - def __init__(self, pos, model, agent_type): - """ - Create a new Schelling agent. - - Args: - unique_id: Unique identifier for the agent. - x, y: Agent initial location. - agent_type: Indicator for the agent's type (minority=1, majority=0) - """ - super().__init__(pos, model) - self.pos = pos - self.type = agent_type - - def step(self): - similar = 0 - for neighbor in self.model.grid.iter_neighbors(self.pos, True): - if neighbor.type == self.type: - similar += 1 - - # If unhappy, move: - if similar < self.model.homophily: - self.model.grid.move_to_empty(self) - else: - self.model.happy += 1 - - -class Schelling(mesa.Model): - """ - Model class for the Schelling segregation model. - """ - - def __init__(self, width=20, height=20, density=0.8, minority_pc=0.2, homophily=3): - """ """ - - self.width = width - self.height = height - self.density = density - self.minority_pc = minority_pc - self.homophily = homophily - - self.schedule = mesa.time.RandomActivation(self) - self.grid = mesa.space.SingleGrid(width, height, torus=True) - - self.happy = 0 - self.datacollector = mesa.DataCollector( - {"happy": "happy"}, # Model-level count of happy agents - # For testing purposes, agent's individual x and y - {"x": lambda a: a.pos[0], "y": lambda a: a.pos[1]}, - ) - - # Set up agents - # We use a grid iterator that returns - # the coordinates of a cell as well as - # its contents. (coord_iter) - for cell in self.grid.coord_iter(): - x = cell[1] - y = cell[2] - if self.random.random() < self.density: - if self.random.random() < self.minority_pc: - agent_type = 1 - else: - agent_type = 0 - - agent = SchellingAgent((x, y), self, agent_type) - self.grid.position_agent(agent, (x, y)) - self.schedule.add(agent) - - self.running = True - self.datacollector.collect(self) - - def step(self): - """ - Run one step of the model. If All agents are happy, halt the model. - """ - self.happy = 0 # Reset counter of happy agents - self.schedule.step() - # collect data - self.datacollector.collect(self) - - if self.happy == self.schedule.get_agent_count(): - self.running = False diff --git a/examples/schelling/requirements.txt b/examples/schelling/requirements.txt deleted file mode 100644 index bcbfbbe220b..00000000000 --- a/examples/schelling/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -jupyter -matplotlib -mesa diff --git a/examples/schelling/run.py b/examples/schelling/run.py deleted file mode 100644 index a25f3b1294e..00000000000 --- a/examples/schelling/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from server import server - -server.launch() diff --git a/examples/schelling/run_ascii.py b/examples/schelling/run_ascii.py deleted file mode 100644 index 8d70c39756e..00000000000 --- a/examples/schelling/run_ascii.py +++ /dev/null @@ -1,49 +0,0 @@ -import mesa - -from model import Schelling - - -class SchellingTextVisualization(mesa.visualization.TextVisualization): - """ - ASCII visualization for schelling model - """ - - def __init__(self, model): - """ - Create new Schelling ASCII visualization. - """ - self.model = model - - grid_viz = mesa.visualization.TextGrid(self.model.grid, self.print_ascii_agent) - happy_viz = mesa.visualization.TextData(self.model, "happy") - self.elements = [grid_viz, happy_viz] - - @staticmethod - def print_ascii_agent(a): - """ - Minority agents are X, Majority are O. - """ - if a.type == 0: - return "O" - if a.type == 1: - return "X" - - -if __name__ == "__main__": - model_params = { - "height": 20, - "width": 20, - # Agent density, from 0.8 to 1.0 - "density": 0.8, - # Fraction minority, from 0.2 to 1.0 - "minority_pc": 0.2, - # Homophily, from 3 to 8 - "homophily": 3, - } - - model = Schelling(**model_params) - viz = SchellingTextVisualization(model) - for i in range(10): - print("Step:", i) - viz.step() - print("---") diff --git a/examples/schelling/server.py b/examples/schelling/server.py deleted file mode 100644 index fd643096db3..00000000000 --- a/examples/schelling/server.py +++ /dev/null @@ -1,46 +0,0 @@ -import mesa - -from model import Schelling - - -def get_happy_agents(model): - """ - Display a text count of how many happy agents there are. - """ - return f"Happy agents: {model.happy}" - - -def schelling_draw(agent): - """ - Portrayal Method for canvas - """ - if agent is None: - return - portrayal = {"Shape": "circle", "r": 0.5, "Filled": "true", "Layer": 0} - - if agent.type == 0: - portrayal["Color"] = ["#FF0000", "#FF9999"] - portrayal["stroke_color"] = "#00FF00" - else: - portrayal["Color"] = ["#0000FF", "#9999FF"] - portrayal["stroke_color"] = "#000000" - return portrayal - - -canvas_element = mesa.visualization.CanvasGrid(schelling_draw, 20, 20, 500, 500) -happy_chart = mesa.visualization.ChartModule([{"Label": "happy", "Color": "Black"}]) - -model_params = { - "height": 20, - "width": 20, - "density": mesa.visualization.Slider("Agent density", 0.8, 0.1, 1.0, 0.1), - "minority_pc": mesa.visualization.Slider("Fraction minority", 0.2, 0.00, 1.0, 0.05), - "homophily": mesa.visualization.Slider("Homophily", 3, 0, 8, 1), -} - -server = mesa.visualization.ModularServer( - Schelling, - [canvas_element, get_happy_agents, happy_chart], - "Schelling", - model_params, -) diff --git a/examples/shape_example/Readme.md b/examples/shape_example/Readme.md deleted file mode 100644 index fb0bae3f3f1..00000000000 --- a/examples/shape_example/Readme.md +++ /dev/null @@ -1,41 +0,0 @@ -# Shape Model -- Basic Grid with two agents - -## Summary - -A very basic example model to showcase the visualization on web browser. - -A simple grid is displayed on browser with two agents. The example does not -have any agent motion involved. This example does not have any movement of -agents so as to keep it to the simplest of level possible. - -This model showcases following features: - -* A rectangular grid -* Text Overlay on the agent's shape on CanvasGrid -* ArrowHead shaped agent for displaying heading of the agent on CanvasGrid - -## 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. - -## Files - -* ``shape_model/model.py``: Defines the basic shape model and agents. -* ``shape_model/server.py``: Sets up the interactive visualization server. -* ``run.py``: Launches a model visualization server. diff --git a/examples/shape_example/requirements.txt b/examples/shape_example/requirements.txt deleted file mode 100644 index da0b5b956fd..00000000000 --- a/examples/shape_example/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -mesa diff --git a/examples/shape_example/run.py b/examples/shape_example/run.py deleted file mode 100644 index c9f32698864..00000000000 --- a/examples/shape_example/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from shape_example.server import server - -server.launch() diff --git a/examples/shape_example/shape_example/model.py b/examples/shape_example/shape_example/model.py deleted file mode 100644 index 5ffe114af68..00000000000 --- a/examples/shape_example/shape_example/model.py +++ /dev/null @@ -1,39 +0,0 @@ -import mesa - - -class Walker(mesa.Agent): - def __init__(self, unique_id, model, pos, heading=(1, 0)): - super().__init__(unique_id, model) - self.pos = pos - self.heading = heading - self.headings = {(1, 0), (0, 1), (-1, 0), (0, -1)} - - -class ShapeExample(mesa.Model): - def __init__(self, N=2, width=20, height=10): - self.N = N # num of agents - self.headings = ((1, 0), (0, 1), (-1, 0), (0, -1)) # tuples are fast - self.grid = mesa.space.SingleGrid(width, height, torus=False) - self.schedule = mesa.time.RandomActivation(self) - self.make_walker_agents() - self.running = True - - def make_walker_agents(self): - unique_id = 0 - while True: - if unique_id == self.N: - break - x = self.random.randrange(self.grid.width) - y = self.random.randrange(self.grid.height) - pos = (x, y) - heading = self.random.choice(self.headings) - # heading = (1, 0) - if self.grid.is_cell_empty(pos): - print(f"Creating agent {unique_id} at ({x}, {y})") - a = Walker(unique_id, self, pos, heading) - self.schedule.add(a) - self.grid.place_agent(a, pos) - unique_id += 1 - - def step(self): - self.schedule.step() diff --git a/examples/shape_example/shape_example/server.py b/examples/shape_example/shape_example/server.py deleted file mode 100644 index bec3a68d5f1..00000000000 --- a/examples/shape_example/shape_example/server.py +++ /dev/null @@ -1,44 +0,0 @@ -import mesa - -from .model import Walker, ShapeExample - - -def agent_draw(agent): - portrayal = None - if agent is None: - # Actually this if part is unnecessary, but still keeping it for - # aesthetics - pass - elif isinstance(agent, Walker): - print(f"Uid: {agent.unique_id}, Heading: {agent.heading}") - portrayal = { - "Shape": "arrowHead", - "Filled": "true", - "Layer": 2, - "Color": ["#00FF00", "#99FF99"], - "stroke_color": "#666666", - "Filled": "true", - "heading_x": agent.heading[0], - "heading_y": agent.heading[1], - "text": agent.unique_id, - "text_color": "white", - "scale": 0.8, - } - return portrayal - - -width = 15 -height = 10 -num_agents = 2 -pixel_ratio = 50 -grid = mesa.visualization.CanvasGrid( - agent_draw, width, height, width * pixel_ratio, height * pixel_ratio -) -server = mesa.visualization.ModularServer( - ShapeExample, - [grid], - "Shape Model Example", - {"N": num_agents, "width": width, "height": height}, -) -server.max_steps = 0 -server.port = 8521 diff --git a/examples/sugarscape_cg/Readme.md b/examples/sugarscape_cg/Readme.md deleted file mode 100644 index 948272b68a8..00000000000 --- a/examples/sugarscape_cg/Readme.md +++ /dev/null @@ -1,61 +0,0 @@ -# Sugarscape Constant Growback model - -## Summary - -This is Epstein & Axtell's Sugarscape Constant Growback model, with a detailed -description in the chapter 2 of Growing Artificial Societies: Social Science from the Bottom Up - -A simple ecological model, consisting of two agent types: ants, and sugar -patches. - -The ants wander around according to Epstein's rule M: -- Look out as far as vision pennies in the four principal lattice directions and identify the unoccupied site(s) having the most sugar. The order in which each agent search es the four directions is random. -- If the greatest sugar value appears on multiple sites then select the nearest one. That is, if the largest sugar within an agent s vision is four, but the value occurs twice, once at a lattice position two units away and again at a site three units away, the former is chosen. If it appears at multiple sites the same distance away, the first site encountered is selected (the site search order being random). -- Move to this site. Notice that there is no distinction between how far an agent can move and how far it can see. So, if vision equals 5, the agent can move up to 5 lattice positions north , south, east, or west. -- Collect all the sugar at this new position. - -The sugar patches grow at a constant rate of 1 until it reaches maximum capacity. If ant metabolizes to the point it has zero or negative sugar, it dies. - - -The model is tests and demonstrates several Mesa concepts and features: - - MultiGrid - - Multiple agent types (ants, sugar patches) - - Overlay arbitrary text (wolf's energy) on agent's shapes while drawing on CanvasGrid - - Dynamically removing agents from the grid and schedule when they die - -## 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. - -## Files - -* ``sugarscape/agents.py``: Defines the SsAgent, and Sugar agent classes. -* ``sugarscape/schedule.py``: This is exactly based on wolf_sheep/schedule.py. -* ``sugarscape/model.py``: Defines the Sugarscape Constant Growback model itself -* ``sugarscape/server.py``: Sets up the interactive visualization server -* ``run.py``: Launches a model visualization server. - -## Further Reading - -This model is based on the Netlogo Sugarscape 2 Constant Growback: - -Li, J. and Wilensky, U. (2009). NetLogo Sugarscape 2 Constant Growback model. -http://ccl.northwestern.edu/netlogo/models/Sugarscape2ConstantGrowback. -Center for Connected Learning and Computer-Based Modeling, -Northwestern University, Evanston, IL. - -The ant sprite is taken from https://openclipart.org/detail/229519/ant-silhouette, with CC0 1.0 license. diff --git a/examples/sugarscape_cg/requirements.txt b/examples/sugarscape_cg/requirements.txt deleted file mode 100644 index 4baebfe444d..00000000000 --- a/examples/sugarscape_cg/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -jupyter -mesa diff --git a/examples/sugarscape_cg/run.py b/examples/sugarscape_cg/run.py deleted file mode 100644 index 6098cec7be4..00000000000 --- a/examples/sugarscape_cg/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from sugarscape_cg.server import server - -server.launch() diff --git a/examples/sugarscape_cg/sugarscape_cg/__init__.py b/examples/sugarscape_cg/sugarscape_cg/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/sugarscape_cg/sugarscape_cg/agents.py b/examples/sugarscape_cg/sugarscape_cg/agents.py deleted file mode 100644 index 28a99095120..00000000000 --- a/examples/sugarscape_cg/sugarscape_cg/agents.py +++ /dev/null @@ -1,84 +0,0 @@ -import math - -import mesa - - -def get_distance(pos_1, pos_2): - """Get the distance between two point - - Args: - pos_1, pos_2: Coordinate tuples for both points. - - """ - x1, y1 = pos_1 - x2, y2 = pos_2 - dx = x1 - x2 - dy = y1 - y2 - return math.sqrt(dx**2 + dy**2) - - -class SsAgent(mesa.Agent): - def __init__( - self, unique_id, pos, model, moore=False, sugar=0, metabolism=0, vision=0 - ): - super().__init__(unique_id, model) - self.pos = pos - self.moore = moore - self.sugar = sugar - self.metabolism = metabolism - self.vision = vision - - def get_sugar(self, pos): - this_cell = self.model.grid.get_cell_list_contents([pos]) - for agent in this_cell: - if type(agent) is Sugar: - return agent - - def is_occupied(self, pos): - this_cell = self.model.grid.get_cell_list_contents([pos]) - return any(isinstance(agent, SsAgent) for agent in this_cell) - - def move(self): - # Get neighborhood within vision - neighbors = [ - i - for i in self.model.grid.get_neighborhood( - self.pos, self.moore, False, radius=self.vision - ) - if not self.is_occupied(i) - ] - neighbors.append(self.pos) - # Look for location with the most sugar - max_sugar = max(self.get_sugar(pos).amount for pos in neighbors) - candidates = [ - pos for pos in neighbors if self.get_sugar(pos).amount == max_sugar - ] - # Narrow down to the nearest ones - min_dist = min(get_distance(self.pos, pos) for pos in candidates) - final_candidates = [ - pos for pos in candidates if get_distance(self.pos, pos) == min_dist - ] - self.random.shuffle(final_candidates) - self.model.grid.move_agent(self, final_candidates[0]) - - def eat(self): - sugar_patch = self.get_sugar(self.pos) - self.sugar = self.sugar - self.metabolism + sugar_patch.amount - sugar_patch.amount = 0 - - def step(self): - self.move() - self.eat() - if self.sugar <= 0: - self.model.grid.remove_agent(self) - self.model.schedule.remove(self) - - -class Sugar(mesa.Agent): - def __init__(self, unique_id, pos, model, max_sugar): - super().__init__(unique_id, model) - self.amount = max_sugar - self.max_sugar = max_sugar - - def step(self): - self.amount = min([self.max_sugar, self.amount + 1]) diff --git a/examples/sugarscape_cg/sugarscape_cg/model.py b/examples/sugarscape_cg/sugarscape_cg/model.py deleted file mode 100644 index b27566e96f2..00000000000 --- a/examples/sugarscape_cg/sugarscape_cg/model.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -Sugarscape Constant Growback Model -================================ - -Replication of the model found in Netlogo: -Li, J. and Wilensky, U. (2009). NetLogo Sugarscape 2 Constant Growback model. -http://ccl.northwestern.edu/netlogo/models/Sugarscape2ConstantGrowback. -Center for Connected Learning and Computer-Based Modeling, -Northwestern University, Evanston, IL. -""" - -import mesa - -from .agents import SsAgent, Sugar - - -class SugarscapeCg(mesa.Model): - """ - Sugarscape 2 Constant Growback - """ - - verbose = True # Print-monitoring - - def __init__(self, width=50, height=50, initial_population=100): - """ - Create a new Constant Growback model with the given parameters. - - Args: - initial_population: Number of population to start with - """ - - # Set parameters - self.width = width - self.height = height - self.initial_population = initial_population - - self.schedule = mesa.time.RandomActivationByType(self) - self.grid = mesa.space.MultiGrid(self.width, self.height, torus=False) - self.datacollector = mesa.DataCollector( - {"SsAgent": lambda m: m.schedule.get_type_count(SsAgent)} - ) - - # Create sugar - import numpy as np - - sugar_distribution = np.genfromtxt("sugarscape_cg/sugar-map.txt") - agent_id = 0 - for _, x, y in self.grid.coord_iter(): - max_sugar = sugar_distribution[x, y] - sugar = Sugar(agent_id, (x, y), self, max_sugar) - agent_id += 1 - self.grid.place_agent(sugar, (x, y)) - self.schedule.add(sugar) - - # Create agent: - for i in range(self.initial_population): - x = self.random.randrange(self.width) - y = self.random.randrange(self.height) - sugar = self.random.randrange(6, 25) - metabolism = self.random.randrange(2, 4) - vision = self.random.randrange(1, 6) - ssa = SsAgent(agent_id, (x, y), self, False, sugar, metabolism, vision) - agent_id += 1 - self.grid.place_agent(ssa, (x, y)) - self.schedule.add(ssa) - - self.running = True - self.datacollector.collect(self) - - def step(self): - self.schedule.step() - # collect data - self.datacollector.collect(self) - if self.verbose: - print([self.schedule.time, self.schedule.get_type_count(SsAgent)]) - - def run_model(self, step_count=200): - - if self.verbose: - print( - "Initial number Sugarscape Agent: ", - self.schedule.get_type_count(SsAgent), - ) - - for i in range(step_count): - self.step() - - if self.verbose: - print("") - print( - "Final number Sugarscape Agent: ", - self.schedule.get_type_count(SsAgent), - ) diff --git a/examples/sugarscape_cg/sugarscape_cg/resources/ant.png b/examples/sugarscape_cg/sugarscape_cg/resources/ant.png deleted file mode 100644 index f2c858251f8..00000000000 Binary files a/examples/sugarscape_cg/sugarscape_cg/resources/ant.png and /dev/null differ diff --git a/examples/sugarscape_cg/sugarscape_cg/server.py b/examples/sugarscape_cg/sugarscape_cg/server.py deleted file mode 100644 index 54a3e857750..00000000000 --- a/examples/sugarscape_cg/sugarscape_cg/server.py +++ /dev/null @@ -1,41 +0,0 @@ -import mesa - -from .agents import SsAgent, Sugar -from .model import SugarscapeCg - -color_dic = {4: "#005C00", 3: "#008300", 2: "#00AA00", 1: "#00F800"} - - -def SsAgent_portrayal(agent): - if agent is None: - return - - if type(agent) is SsAgent: - return {"Shape": "sugarscape_cg/resources/ant.png", "scale": 0.9, "Layer": 1} - - elif type(agent) is Sugar: - if agent.amount != 0: - color = color_dic[agent.amount] - else: - color = "#D6F5D6" - return { - "Color": color, - "Shape": "rect", - "Filled": "true", - "Layer": 0, - "w": 1, - "h": 1, - } - - return {} - - -canvas_element = mesa.visualization.CanvasGrid(SsAgent_portrayal, 50, 50, 500, 500) -chart_element = mesa.visualization.ChartModule( - [{"Label": "SsAgent", "Color": "#AA0000"}] -) - -server = mesa.visualization.ModularServer( - SugarscapeCg, [canvas_element, chart_element], "Sugarscape 2 Constant Growback" -) -# server.launch() diff --git a/examples/sugarscape_cg/sugarscape_cg/sugar-map.txt b/examples/sugarscape_cg/sugarscape_cg/sugar-map.txt deleted file mode 100644 index 1357a6676b4..00000000000 --- a/examples/sugarscape_cg/sugarscape_cg/sugar-map.txt +++ /dev/null @@ -1,50 +0,0 @@ -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 2 2 2 2 2 2 2 2 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 2 2 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 -0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 -0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 -0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 -0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 -0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 -0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 -0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 2 2 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 -1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 -1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 -1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 -1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 -1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 -1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 -1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 -1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 -2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 -2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 -2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 -2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 -2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 -2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 -2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 -2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 -2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 -2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 -2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 -2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 -2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 -2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 -2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/examples/virus_on_network/README.md b/examples/virus_on_network/README.md deleted file mode 100644 index b6e989217e0..00000000000 --- a/examples/virus_on_network/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Virus on a Network - -## Summary - -This model is based on the NetLogo model "Virus on Network". - -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. - -## Files - -* ``run.py``: Launches a model visualization server. -* ``model.py``: Contains the agent class, and the overall model class. -* ``server.py``: Defines classes for visualizing the model (network layout) in the browser via Mesa's modular server, and instantiates a visualization server. - -## Further Reading - -The full tutorial describing how the model is built can be found at: -http://mesa.readthedocs.io/en/master/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. diff --git a/examples/virus_on_network/requirements.txt b/examples/virus_on_network/requirements.txt deleted file mode 100644 index f8a0e4475ca..00000000000 --- a/examples/virus_on_network/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -networkx>=2.0 diff --git a/examples/virus_on_network/run.py b/examples/virus_on_network/run.py deleted file mode 100644 index 9f1ef7292ae..00000000000 --- a/examples/virus_on_network/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from virus_on_network.server import server - -server.launch() diff --git a/examples/virus_on_network/virus_on_network/__init__.py b/examples/virus_on_network/virus_on_network/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/virus_on_network/virus_on_network/model.py b/examples/virus_on_network/virus_on_network/model.py deleted file mode 100644 index 7d1e68f7f15..00000000000 --- a/examples/virus_on_network/virus_on_network/model.py +++ /dev/null @@ -1,160 +0,0 @@ -import math -from enum import Enum -import networkx as nx - -import mesa - - -class State(Enum): - SUSCEPTIBLE = 0 - INFECTED = 1 - RESISTANT = 2 - - -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(mesa.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, - ): - - 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.schedule = mesa.time.RandomActivation(self) - 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 i, node in enumerate(self.G.nodes()): - a = VirusAgent( - i, - self, - State.SUSCEPTIBLE, - self.virus_spread_chance, - self.virus_check_frequency, - self.recovery_chance, - self.gain_resistance_chance, - ) - self.schedule.add(a) - # 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.schedule.step() - # collect data - self.datacollector.collect(self) - - def run_model(self, n): - for i in range(n): - self.step() - - -class VirusAgent(mesa.Agent): - def __init__( - self, - unique_id, - model, - initial_state, - virus_spread_chance, - virus_check_frequency, - recovery_chance, - gain_resistance_chance, - ): - super().__init__(unique_id, 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_neighbors(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: - # Checking... - if 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() diff --git a/examples/virus_on_network/virus_on_network/server.py b/examples/virus_on_network/virus_on_network/server.py deleted file mode 100644 index a8f47c61e6b..00000000000 --- a/examples/virus_on_network/virus_on_network/server.py +++ /dev/null @@ -1,133 +0,0 @@ -import math - -import mesa - -from .model import VirusOnNetwork, State, number_infected - - -def network_portrayal(G): - # The model ensures there is always 1 agent per node - - def node_color(agent): - return {State.INFECTED: "#FF0000", State.SUSCEPTIBLE: "#008000"}.get( - agent.state, "#808080" - ) - - def edge_color(agent1, agent2): - if State.RESISTANT in (agent1.state, agent2.state): - return "#000000" - return "#e8e8e8" - - def edge_width(agent1, agent2): - if State.RESISTANT in (agent1.state, agent2.state): - return 3 - return 2 - - def get_agents(source, target): - return G.nodes[source]["agent"][0], G.nodes[target]["agent"][0] - - portrayal = dict() - portrayal["nodes"] = [ - { - "size": 6, - "color": node_color(agents[0]), - "tooltip": f"id: {agents[0].unique_id}
state: {agents[0].state.name}", - } - for (_, agents) in G.nodes.data("agent") - ] - - portrayal["edges"] = [ - { - "source": source, - "target": target, - "color": edge_color(*get_agents(source, target)), - "width": edge_width(*get_agents(source, target)), - } - for (source, target) in G.edges - ] - - return portrayal - - -network = mesa.visualization.NetworkModule(network_portrayal, 500, 500) -chart = mesa.visualization.ChartModule( - [ - {"Label": "Infected", "Color": "#FF0000"}, - {"Label": "Susceptible", "Color": "#008000"}, - {"Label": "Resistant", "Color": "#808080"}, - ] -) - - -def get_resistant_susceptible_ratio(model): - ratio = model.resistant_susceptible_ratio() - ratio_text = "∞" if ratio is math.inf else f"{ratio:.2f}" - infected_text = str(number_infected(model)) - - return "Resistant/Susceptible Ratio: {}
Infected Remaining: {}".format( - ratio_text, infected_text - ) - - -model_params = { - "num_nodes": mesa.visualization.Slider( - "Number of agents", - 10, - 10, - 100, - 1, - description="Choose how many agents to include in the model", - ), - "avg_node_degree": mesa.visualization.Slider( - "Avg Node Degree", 3, 3, 8, 1, description="Avg Node Degree" - ), - "initial_outbreak_size": mesa.visualization.Slider( - "Initial Outbreak Size", - 1, - 1, - 10, - 1, - description="Initial Outbreak Size", - ), - "virus_spread_chance": mesa.visualization.Slider( - "Virus Spread Chance", - 0.4, - 0.0, - 1.0, - 0.1, - description="Probability that susceptible neighbor will be infected", - ), - "virus_check_frequency": mesa.visualization.Slider( - "Virus Check Frequency", - 0.4, - 0.0, - 1.0, - 0.1, - description="Frequency the nodes check whether they are infected by " "a virus", - ), - "recovery_chance": mesa.visualization.Slider( - "Recovery Chance", - 0.3, - 0.0, - 1.0, - 0.1, - description="Probability that the virus will be removed", - ), - "gain_resistance_chance": mesa.visualization.Slider( - "Gain Resistance Chance", - 0.5, - 0.0, - 1.0, - 0.1, - description="Probability that a recovered agent will become " - "resistant to this virus in the future", - ), -} - -server = mesa.visualization.ModularServer( - VirusOnNetwork, - [network, get_resistant_susceptible_ratio, chart], - "Virus Model", - model_params, -) -server.port = 8521 diff --git a/examples/wolf_sheep/Readme.md b/examples/wolf_sheep/Readme.md deleted file mode 100644 index 30794a6ee67..00000000000 --- a/examples/wolf_sheep/Readme.md +++ /dev/null @@ -1,57 +0,0 @@ -# Wolf-Sheep Predation Model - -## Summary - -A simple ecological model, consisting of three agent types: wolves, sheep, and grass. The wolves and the sheep wander around the grid at random. Wolves and sheep both expend energy moving around, and replenish it by eating. Sheep eat grass, and wolves eat sheep if they end up on the same grid cell. - -If wolves and sheep have enough energy, they reproduce, creating a new wolf or sheep (in this simplified model, only one parent is needed for reproduction). The grass on each cell regrows at a constant rate. If any wolves and sheep run out of energy, they die. - -The model is tests and demonstrates several Mesa concepts and features: - - MultiGrid - - Multiple agent types (wolves, sheep, grass) - - Overlay arbitrary text (wolf's energy) on agent's shapes while drawing on CanvasGrid - - Agents inheriting a behavior (random movement) from an abstract parent - - Writing a model composed of multiple files. - - Dynamically adding and removing agents from the schedule - -## Installation - -To install the dependencies use pip and the requirements.txt in this directory. e.g. - -``` - # First, we clone the Mesa repo - $ git clone https://github.com/projectmesa/mesa.git - $ cd mesa - # Then we cd to the example directory - $ cd examples/wolf_sheep - $ 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. - -## Files - -* ``wolf_sheep/random_walk.py``: This defines the ``RandomWalker`` agent, which implements the behavior of moving randomly across a grid, one cell at a time. Both the Wolf and Sheep agents will inherit from it. -* ``wolf_sheep/test_random_walk.py``: Defines a simple model and a text-only visualization intended to make sure the RandomWalk class was working as expected. This doesn't actually model anything, but serves as an ad-hoc unit test. To run it, ``cd`` into the ``wolf_sheep`` directory and run ``python test_random_walk.py``. You'll see a series of ASCII grids, one per model step, with each cell showing a count of the number of agents in it. -* ``wolf_sheep/agents.py``: Defines the Wolf, Sheep, and GrassPatch agent classes. -* ``wolf_sheep/scheduler.py``: Defines a custom variant on the RandomActivationByType scheduler, where we can define filters for the `get_type_count` function. -* ``wolf_sheep/model.py``: Defines the Wolf-Sheep Predation model itself -* ``wolf_sheep/server.py``: Sets up the interactive visualization server -* ``run.py``: Launches a model visualization server. - -## Further Reading - -This model is closely based on the NetLogo Wolf-Sheep Predation Model: - -Wilensky, U. (1997). NetLogo Wolf Sheep Predation model. http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation. Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. - -See also the [Lotka–Volterra equations -](https://en.wikipedia.org/wiki/Lotka%E2%80%93Volterra_equations) for an example of a classic differential-equation model with similar dynamics. diff --git a/examples/wolf_sheep/requirements.txt b/examples/wolf_sheep/requirements.txt deleted file mode 100644 index da0b5b956fd..00000000000 --- a/examples/wolf_sheep/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -mesa diff --git a/examples/wolf_sheep/run.py b/examples/wolf_sheep/run.py deleted file mode 100644 index dc5d367e89d..00000000000 --- a/examples/wolf_sheep/run.py +++ /dev/null @@ -1,3 +0,0 @@ -from wolf_sheep.server import server - -server.launch() diff --git a/examples/wolf_sheep/wolf_sheep/__init__.py b/examples/wolf_sheep/wolf_sheep/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/wolf_sheep/wolf_sheep/agents.py b/examples/wolf_sheep/wolf_sheep/agents.py deleted file mode 100644 index fe62192bf60..00000000000 --- a/examples/wolf_sheep/wolf_sheep/agents.py +++ /dev/null @@ -1,120 +0,0 @@ -import mesa -from wolf_sheep.random_walk import RandomWalker - - -class Sheep(RandomWalker): - """ - A sheep that walks around, reproduces (asexually) and gets eaten. - - The init is the same as the RandomWalker. - """ - - energy = None - - def __init__(self, unique_id, pos, model, moore, energy=None): - super().__init__(unique_id, pos, model, moore=moore) - self.energy = energy - - def step(self): - """ - A model step. Move, then eat grass and reproduce. - """ - self.random_move() - living = True - - if self.model.grass: - # Reduce energy - self.energy -= 1 - - # If there is grass available, eat it - this_cell = self.model.grid.get_cell_list_contents([self.pos]) - grass_patch = [obj for obj in this_cell if isinstance(obj, GrassPatch)][0] - if grass_patch.fully_grown: - self.energy += self.model.sheep_gain_from_food - grass_patch.fully_grown = False - - # Death - if self.energy < 0: - self.model.grid.remove_agent(self) - self.model.schedule.remove(self) - living = False - - if living and self.random.random() < self.model.sheep_reproduce: - # Create a new sheep: - if self.model.grass: - self.energy /= 2 - lamb = Sheep( - self.model.next_id(), self.pos, self.model, self.moore, self.energy - ) - self.model.grid.place_agent(lamb, self.pos) - self.model.schedule.add(lamb) - - -class Wolf(RandomWalker): - """ - A wolf that walks around, reproduces (asexually) and eats sheep. - """ - - energy = None - - def __init__(self, unique_id, pos, model, moore, energy=None): - super().__init__(unique_id, pos, model, moore=moore) - self.energy = energy - - def step(self): - self.random_move() - self.energy -= 1 - - # If there are sheep present, eat one - x, y = self.pos - this_cell = self.model.grid.get_cell_list_contents([self.pos]) - sheep = [obj for obj in this_cell if isinstance(obj, Sheep)] - if len(sheep) > 0: - sheep_to_eat = self.random.choice(sheep) - self.energy += self.model.wolf_gain_from_food - - # Kill the sheep - self.model.grid.remove_agent(sheep_to_eat) - self.model.schedule.remove(sheep_to_eat) - - # Death or reproduction - if self.energy < 0: - self.model.grid.remove_agent(self) - self.model.schedule.remove(self) - else: - if self.random.random() < self.model.wolf_reproduce: - # Create a new wolf cub - self.energy /= 2 - cub = Wolf( - self.model.next_id(), self.pos, self.model, self.moore, self.energy - ) - self.model.grid.place_agent(cub, cub.pos) - self.model.schedule.add(cub) - - -class GrassPatch(mesa.Agent): - """ - A patch of grass that grows at a fixed rate and it is eaten by sheep - """ - - def __init__(self, unique_id, pos, model, fully_grown, countdown): - """ - Creates a new patch of grass - - Args: - grown: (boolean) Whether the patch of grass is fully grown or not - countdown: Time for the patch of grass to be fully grown again - """ - super().__init__(unique_id, model) - self.fully_grown = fully_grown - self.countdown = countdown - self.pos = pos - - def step(self): - if not self.fully_grown: - if self.countdown <= 0: - # Set as fully grown - self.fully_grown = True - self.countdown = self.model.grass_regrowth_time - else: - self.countdown -= 1 diff --git a/examples/wolf_sheep/wolf_sheep/model.py b/examples/wolf_sheep/wolf_sheep/model.py deleted file mode 100644 index 2b8fdbdeed1..00000000000 --- a/examples/wolf_sheep/wolf_sheep/model.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -Wolf-Sheep Predation Model -================================ - -Replication of the model found in NetLogo: - Wilensky, U. (1997). NetLogo Wolf Sheep Predation model. - http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation. - Center for Connected Learning and Computer-Based Modeling, - Northwestern University, Evanston, IL. -""" - -import mesa - -from wolf_sheep.scheduler import RandomActivationByTypeFiltered -from wolf_sheep.agents import Sheep, Wolf, GrassPatch - - -class WolfSheep(mesa.Model): - """ - Wolf-Sheep Predation Model - """ - - height = 20 - width = 20 - - initial_sheep = 100 - initial_wolves = 50 - - sheep_reproduce = 0.04 - wolf_reproduce = 0.05 - - wolf_gain_from_food = 20 - - grass = False - grass_regrowth_time = 30 - sheep_gain_from_food = 4 - - verbose = False # Print-monitoring - - description = ( - "A model for simulating wolf and sheep (predator-prey) ecosystem modelling." - ) - - def __init__( - self, - width=20, - height=20, - initial_sheep=100, - initial_wolves=50, - sheep_reproduce=0.04, - wolf_reproduce=0.05, - wolf_gain_from_food=20, - grass=False, - grass_regrowth_time=30, - sheep_gain_from_food=4, - ): - """ - Create a new Wolf-Sheep model with the given parameters. - - Args: - initial_sheep: Number of sheep to start with - initial_wolves: Number of wolves to start with - sheep_reproduce: Probability of each sheep reproducing each step - wolf_reproduce: Probability of each wolf reproducing each step - wolf_gain_from_food: Energy a wolf gains from eating a sheep - grass: Whether to have the sheep eat grass for energy - grass_regrowth_time: How long it takes for a grass patch to regrow - once it is eaten - sheep_gain_from_food: Energy sheep gain from grass, if enabled. - """ - super().__init__() - # Set parameters - self.width = width - self.height = height - self.initial_sheep = initial_sheep - self.initial_wolves = initial_wolves - self.sheep_reproduce = sheep_reproduce - self.wolf_reproduce = wolf_reproduce - self.wolf_gain_from_food = wolf_gain_from_food - self.grass = grass - self.grass_regrowth_time = grass_regrowth_time - self.sheep_gain_from_food = sheep_gain_from_food - - self.schedule = RandomActivationByTypeFiltered(self) - self.grid = mesa.space.MultiGrid(self.width, self.height, torus=True) - self.datacollector = mesa.DataCollector( - { - "Wolves": lambda m: m.schedule.get_type_count(Wolf), - "Sheep": lambda m: m.schedule.get_type_count(Sheep), - "Grass": lambda m: m.schedule.get_type_count( - GrassPatch, lambda x: x.fully_grown - ), - } - ) - - # Create sheep: - for i in range(self.initial_sheep): - x = self.random.randrange(self.width) - y = self.random.randrange(self.height) - energy = self.random.randrange(2 * self.sheep_gain_from_food) - sheep = Sheep(self.next_id(), (x, y), self, True, energy) - self.grid.place_agent(sheep, (x, y)) - self.schedule.add(sheep) - - # Create wolves - for i in range(self.initial_wolves): - x = self.random.randrange(self.width) - y = self.random.randrange(self.height) - energy = self.random.randrange(2 * self.wolf_gain_from_food) - wolf = Wolf(self.next_id(), (x, y), self, True, energy) - self.grid.place_agent(wolf, (x, y)) - self.schedule.add(wolf) - - # Create grass patches - if self.grass: - for agent, x, y in self.grid.coord_iter(): - - fully_grown = self.random.choice([True, False]) - - if fully_grown: - countdown = self.grass_regrowth_time - else: - countdown = self.random.randrange(self.grass_regrowth_time) - - patch = GrassPatch(self.next_id(), (x, y), self, fully_grown, countdown) - self.grid.place_agent(patch, (x, y)) - self.schedule.add(patch) - - self.running = True - self.datacollector.collect(self) - - def step(self): - self.schedule.step() - # collect data - self.datacollector.collect(self) - if self.verbose: - print( - [ - self.schedule.time, - self.schedule.get_type_count(Wolf), - self.schedule.get_type_count(Sheep), - self.schedule.get_type_count(GrassPatch, lambda x: x.fully_grown), - ] - ) - - def run_model(self, step_count=200): - - if self.verbose: - print("Initial number wolves: ", self.schedule.get_type_count(Wolf)) - print("Initial number sheep: ", self.schedule.get_type_count(Sheep)) - print( - "Initial number grass: ", - self.schedule.get_type_count(GrassPatch, lambda x: x.fully_grown), - ) - - for i in range(step_count): - self.step() - - if self.verbose: - print("") - print("Final number wolves: ", self.schedule.get_type_count(Wolf)) - print("Final number sheep: ", self.schedule.get_type_count(Sheep)) - print( - "Final number grass: ", - self.schedule.get_type_count(GrassPatch, lambda x: x.fully_grown), - ) diff --git a/examples/wolf_sheep/wolf_sheep/random_walk.py b/examples/wolf_sheep/wolf_sheep/random_walk.py deleted file mode 100644 index 1125589b1e9..00000000000 --- a/examples/wolf_sheep/wolf_sheep/random_walk.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Generalized behavior for random walking, one grid cell at a time. -""" - -import mesa - - -class RandomWalker(mesa.Agent): - """ - Class implementing random walker methods in a generalized manner. - - Not intended to be used on its own, but to inherit its methods to multiple - other agents. - - """ - - grid = None - x = None - y = None - moore = True - - def __init__(self, unique_id, pos, model, moore=True): - """ - grid: The MultiGrid object in which the agent lives. - x: The agent's current x coordinate - y: The agent's current y coordinate - moore: If True, may move in all 8 directions. - Otherwise, only up, down, left, right. - """ - super().__init__(unique_id, model) - self.pos = pos - self.moore = moore - - def random_move(self): - """ - Step one cell in any allowable direction. - """ - # Pick the next cell from the adjacent cells. - next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True) - next_move = self.random.choice(next_moves) - # Now move: - self.model.grid.move_agent(self, next_move) diff --git a/examples/wolf_sheep/wolf_sheep/resources/sheep.png b/examples/wolf_sheep/wolf_sheep/resources/sheep.png deleted file mode 100644 index dfb81b0e5d7..00000000000 Binary files a/examples/wolf_sheep/wolf_sheep/resources/sheep.png and /dev/null differ diff --git a/examples/wolf_sheep/wolf_sheep/resources/wolf.png b/examples/wolf_sheep/wolf_sheep/resources/wolf.png deleted file mode 100644 index 5357b855197..00000000000 Binary files a/examples/wolf_sheep/wolf_sheep/resources/wolf.png and /dev/null differ diff --git a/examples/wolf_sheep/wolf_sheep/scheduler.py b/examples/wolf_sheep/wolf_sheep/scheduler.py deleted file mode 100644 index e2a59fc7c80..00000000000 --- a/examples/wolf_sheep/wolf_sheep/scheduler.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Type, Callable - -import mesa - - -class RandomActivationByTypeFiltered(mesa.time.RandomActivationByType): - """ - A scheduler that overrides the get_type_count method to allow for filtering - of agents by a function before counting. - - Example: - >>> scheduler = RandomActivationByTypeFiltered(model) - >>> scheduler.get_type_count(AgentA, lambda agent: agent.some_attribute > 10) - """ - - def get_type_count( - self, - type_class: Type[mesa.Agent], - filter_func: Callable[[mesa.Agent], bool] = None, - ) -> int: - """ - Returns the current number of agents of certain type in the queue that satisfy the filter function. - """ - count = 0 - for agent in self.agents_by_type[type_class].values(): - if filter_func is None or filter_func(agent): - count += 1 - return count diff --git a/examples/wolf_sheep/wolf_sheep/server.py b/examples/wolf_sheep/wolf_sheep/server.py deleted file mode 100644 index bccf4ec849d..00000000000 --- a/examples/wolf_sheep/wolf_sheep/server.py +++ /dev/null @@ -1,79 +0,0 @@ -import mesa - -from wolf_sheep.agents import Wolf, Sheep, GrassPatch -from wolf_sheep.model import WolfSheep - - -def wolf_sheep_portrayal(agent): - if agent is None: - return - - portrayal = {} - - if type(agent) is Sheep: - portrayal["Shape"] = "wolf_sheep/resources/sheep.png" - # https://icons8.com/web-app/433/sheep - portrayal["scale"] = 0.9 - portrayal["Layer"] = 1 - - elif type(agent) is Wolf: - portrayal["Shape"] = "wolf_sheep/resources/wolf.png" - # https://icons8.com/web-app/36821/German-Shepherd - portrayal["scale"] = 0.9 - portrayal["Layer"] = 2 - portrayal["text"] = round(agent.energy, 1) - portrayal["text_color"] = "White" - - elif type(agent) is GrassPatch: - if agent.fully_grown: - portrayal["Color"] = ["#00FF00", "#00CC00", "#009900"] - else: - portrayal["Color"] = ["#84e184", "#adebad", "#d6f5d6"] - portrayal["Shape"] = "rect" - portrayal["Filled"] = "true" - portrayal["Layer"] = 0 - portrayal["w"] = 1 - portrayal["h"] = 1 - - return portrayal - - -canvas_element = mesa.visualization.CanvasGrid(wolf_sheep_portrayal, 20, 20, 500, 500) -chart_element = mesa.visualization.ChartModule( - [ - {"Label": "Wolves", "Color": "#AA0000"}, - {"Label": "Sheep", "Color": "#666666"}, - {"Label": "Grass", "Color": "#00AA00"}, - ] -) - -model_params = { - # The following line is an example to showcase StaticText. - "title": mesa.visualization.StaticText("Parameters:"), - "grass": mesa.visualization.Checkbox("Grass Enabled", True), - "grass_regrowth_time": mesa.visualization.Slider("Grass Regrowth Time", 20, 1, 50), - "initial_sheep": mesa.visualization.Slider( - "Initial Sheep Population", 100, 10, 300 - ), - "sheep_reproduce": mesa.visualization.Slider( - "Sheep Reproduction Rate", 0.04, 0.01, 1.0, 0.01 - ), - "initial_wolves": mesa.visualization.Slider("Initial Wolf Population", 50, 10, 300), - "wolf_reproduce": mesa.visualization.Slider( - "Wolf Reproduction Rate", - 0.05, - 0.01, - 1.0, - 0.01, - description="The rate at which wolf agents reproduce.", - ), - "wolf_gain_from_food": mesa.visualization.Slider( - "Wolf Gain From Food Rate", 20, 1, 50 - ), - "sheep_gain_from_food": mesa.visualization.Slider("Sheep Gain From Food", 4, 1, 10), -} - -server = mesa.visualization.ModularServer( - WolfSheep, [canvas_element, chart_element], "Wolf Sheep Predation", model_params -) -server.port = 8521 diff --git a/examples/wolf_sheep/wolf_sheep/test_random_walk.py b/examples/wolf_sheep/wolf_sheep/test_random_walk.py deleted file mode 100644 index ab3b044ab1e..00000000000 --- a/examples/wolf_sheep/wolf_sheep/test_random_walk.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -Testing the RandomWalker by having an ABM composed only of random walker -agents. -""" - -from mesa import Model -from mesa.space import MultiGrid -from mesa.time import RandomActivation -from mesa.visualization.TextVisualization import TextVisualization, TextGrid - -from wolf_sheep.random_walk import RandomWalker - - -class WalkerAgent(RandomWalker): - """ - Agent which only walks around. - """ - - def step(self): - self.random_move() - - -class WalkerWorld(Model): - """ - Random walker world. - """ - - height = 10 - width = 10 - - def __init__(self, width, height, agent_count): - """ - Create a new WalkerWorld. - - Args: - width, height: World size. - agent_count: How many agents to create. - """ - self.height = height - self.width = width - self.grid = MultiGrid(self.width, self.height, torus=True) - self.agent_count = agent_count - - self.schedule = RandomActivation(self) - # Create agents - for i in range(self.agent_count): - x = self.random.randrange(self.width) - y = self.random.randrange(self.height) - a = WalkerAgent(i, (x, y), self, True) - self.schedule.add(a) - self.grid.place_agent(a, (x, y)) - - def step(self): - self.schedule.step() - - -class WalkerWorldViz(TextVisualization): - """ - ASCII Visualization for a WalkerWorld agent. - Each cell is displayed as the number of agents currently in that cell. - """ - - def __init__(self, model): - """ - Create a new visualization for a WalkerWorld instance. - - args: - model: An instance of a WalkerWorld model. - """ - self.model = model - grid_viz = TextGrid(self.model.grid, None) - grid_viz.converter = lambda x: str(len(x)) - self.elements = [grid_viz] - - -if __name__ == "__main__": - print("Testing 10x10 world, with 50 random walkers, for 10 steps.") - model = WalkerWorld(10, 10, 50) - viz = WalkerWorldViz(model) - for i in range(10): - print("Step:", str(i)) - viz.step() diff --git a/mesa/__init__.py b/mesa/__init__.py index 1371a90032b..3cfaaff8709 100644 --- a/mesa/__init__.py +++ b/mesa/__init__.py @@ -2,18 +2,16 @@ Mesa Agent-Based Modeling Framework Core Objects: Model, and Agent. - """ import datetime -from mesa.model import Model -from mesa.agent import Agent - -import mesa.time as time import mesa.space as space -import mesa.flat.visualization as visualization +import mesa.time as time +import mesa.visualization as visualization +from mesa.agent import Agent +from mesa.batchrunner import batch_run from mesa.datacollection import DataCollector -from mesa.batchrunner import batch_run # noqa +from mesa.model import Model __all__ = [ "Model", @@ -23,9 +21,11 @@ "visualization", "DataCollector", "batch_run", + "experimental", ] __title__ = "mesa" -__version__ = "1.1.1" +__version__ = "2.1.2" __license__ = "Apache 2.0" -__copyright__ = f"Copyright {datetime.date.today().year} Project Mesa Team" +_this_year = datetime.datetime.now(tz=datetime.timezone.utc).date().year +__copyright__ = f"Copyright {_this_year} Project Mesa Team" diff --git a/mesa/agent.py b/mesa/agent.py index a3daf7cf612..5a8966cfd3b 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -2,15 +2,15 @@ The agent class for Mesa framework. Core Objects: Agent - """ # Mypy; for the `|` operator purpose # Remove this __future__ import once the oldest supported Python is 3.10 from __future__ import annotations +from random import Random + # mypy from typing import TYPE_CHECKING -from random import Random if TYPE_CHECKING: # We ensure that these are not imported during runtime to prevent cyclic @@ -35,7 +35,6 @@ def __init__(self, unique_id: int, model: Model) -> None: def step(self) -> None: """A single step of the agent.""" - pass def advance(self) -> None: pass diff --git a/mesa/batchrunner.py b/mesa/batchrunner.py index 0f9d31c68a9..c9c528470a3 100644 --- a/mesa/batchrunner.py +++ b/mesa/batchrunner.py @@ -1,17 +1,6 @@ -""" -Batchrunner -=========== - -A single class to manage a batch run or parameter sweep of a given model. - -""" -import copy import itertools -import random from functools import partial -from itertools import count, product -from multiprocessing import Pool, cpu_count -from warnings import warn +from multiprocessing import Pool from typing import ( Any, Dict, @@ -24,8 +13,7 @@ Union, ) -import pandas as pd -from tqdm import tqdm +from tqdm.auto import tqdm from mesa.model import Model @@ -210,567 +198,3 @@ def _collect_data( agent_dict.update(zip(dc.agent_reporters, data[2:])) all_agents_data.append(agent_dict) return model_data, all_agents_data - - -class ParameterError(TypeError): - MESSAGE = ( - "Parameters must map a name to a value. " - "These names did not match parameters: {}" - ) - - def __init__(self, bad_names): - self.bad_names = bad_names - - def __str__(self): - return self.MESSAGE.format(self.bad_names) - - -class VariableParameterError(ParameterError): - MESSAGE = ( - "Variable_parameters must map a name to a sequence of values. " - "These parameters were given with non-sequence values: {}" - ) - - -class FixedBatchRunner: - """This class is instantiated with a model class, and model parameters - associated with one or more values. It is also instantiated with model and - agent-level reporters, dictionaries mapping a variable name to a function - which collects some data from the model or its agents at the end of the run - and stores it. - - Note that by default, the reporters only collect data at the *end* of the - run. To get step by step data, simply have a reporter store the model's - entire DataCollector object. - """ - - def __init__( - self, - model_cls, - parameters_list=None, - fixed_parameters=None, - iterations=1, - max_steps=1000, - model_reporters=None, - agent_reporters=None, - display_progress=True, - ): - """Create a new BatchRunner for a given model with the given - parameters. - - Args: - model_cls: The class of model to batch-run. - parameters_list: A list of dictionaries of parameter sets. - The model will be run with dictionary of parameters. - For example, given parameters_list of - [{"homophily": 3, "density": 0.8, "minority_pc": 0.2}, - {"homophily": 2, "density": 0.9, "minority_pc": 0.1}, - {"homophily": 4, "density": 0.6, "minority_pc": 0.5}] - 3 models will be run, one for each provided set of parameters. - fixed_parameters: Dictionary of parameters that stay same through - all batch runs. For example, given fixed_parameters of - {"constant_parameter": 3}, - every instantiated model will be passed constant_parameter=3 - as a kwarg. - iterations: The total number of times to run the model for each set - of parameters. - max_steps: Upper limit of steps above which each run will be halted - if it hasn't halted on its own. - model_reporters: The dictionary of variables to collect on each run - at the end, with variable names mapped to a function to collect - them. For example: - {"agent_count": lambda m: m.schedule.get_agent_count()} - agent_reporters: Like model_reporters, but each variable is now - collected at the level of each agent present in the model at - the end of the run. - display_progress: Display progress bar with time estimation? - - """ - self.model_cls = model_cls - if parameters_list is None: - parameters_list = [] - self.parameters_list = list(parameters_list) - self.fixed_parameters = fixed_parameters or {} - self._include_fixed = len(self.fixed_parameters) > 0 - self.iterations = iterations - self.max_steps = max_steps - - for params in self.parameters_list: - if list(params) != list(self.parameters_list[0]): - msg = "parameter names in parameters_list are not equal across the list" - raise ValueError(msg) - - self.model_reporters = model_reporters - self.agent_reporters = agent_reporters - - if self.model_reporters: - self.model_vars = {} - - if self.agent_reporters: - self.agent_vars = {} - - self.datacollector_model_reporters = {} - self.datacollector_agent_reporters = {} - - self.display_progress = display_progress - - def _make_model_args(self): - """Prepare all combinations of parameter values for `run_all` - - Returns: - Tuple with the form: - (total_iterations, all_kwargs, all_param_values) - """ - total_iterations = self.iterations - all_kwargs = [] - all_param_values = [] - - count = len(self.parameters_list) - if count: - for params in self.parameters_list: - kwargs = params.copy() - kwargs.update(self.fixed_parameters) - all_kwargs.append(kwargs) - all_param_values.append(list(params.values())) - - elif len(self.fixed_parameters): - count = 1 - kwargs = self.fixed_parameters.copy() - all_kwargs.append(kwargs) - all_param_values.append(list(kwargs.values())) - - total_iterations *= count - - return total_iterations, all_kwargs, all_param_values - - def run_all(self): - """Run the model at all parameter combinations and store results.""" - run_count = count() - total_iterations, all_kwargs, all_param_values = self._make_model_args() - - with tqdm(total_iterations, disable=not self.display_progress) as pbar: - for i, kwargs in enumerate(all_kwargs): - param_values = all_param_values[i] - for _ in range(self.iterations): - self.run_iteration(kwargs, param_values, next(run_count)) - pbar.update() - - def run_iteration(self, kwargs, param_values, run_count): - model = self.model_cls(**kwargs) - results = self.run_model(model) - if param_values is not None: - model_key = tuple(param_values) + (run_count,) - else: - model_key = (run_count,) - - if self.model_reporters: - self.model_vars[model_key] = self.collect_model_vars(model) - if self.agent_reporters: - agent_vars = self.collect_agent_vars(model) - for agent_id, reports in agent_vars.items(): - agent_key = model_key + (agent_id,) - self.agent_vars[agent_key] = reports - # Collects data from datacollector object in model - if results is not None: - if results.model_reporters is not None: - self.datacollector_model_reporters[ - model_key - ] = results.get_model_vars_dataframe() - if results.agent_reporters is not None: - self.datacollector_agent_reporters[ - model_key - ] = results.get_agent_vars_dataframe() - - return ( - getattr(self, "model_vars", None), - getattr(self, "agent_vars", None), - getattr(self, "datacollector_model_reporters", None), - getattr(self, "datacollector_agent_reporters", None), - ) - - def run_model(self, model): - """Run a model object to completion, or until reaching max steps. - - If your model runs in a non-standard way, this is the method to modify - in your subclass. - - """ - while model.running and model.schedule.steps < self.max_steps: - model.step() - - if hasattr(model, "datacollector"): - return model.datacollector - else: - return None - - def collect_model_vars(self, model): - """Run reporters and collect model-level variables.""" - model_vars = {} - for var, reporter in self.model_reporters.items(): - model_vars[var] = reporter(model) - - return model_vars - - def collect_agent_vars(self, model): - """Run reporters and collect agent-level variables.""" - agent_vars = {} - for agent in model.schedule._agents.values(): - agent_record = {} - for var, reporter in self.agent_reporters.items(): - agent_record[var] = getattr(agent, reporter) - agent_vars[agent.unique_id] = agent_record - return agent_vars - - def get_model_vars_dataframe(self): - """Generate a pandas DataFrame from the model-level variables - collected. - """ - - return self._prepare_report_table(self.model_vars) - - def get_agent_vars_dataframe(self): - """Generate a pandas DataFrame from the agent-level variables - collected. - """ - - return self._prepare_report_table(self.agent_vars, extra_cols=["AgentId"]) - - def get_collector_model(self): - """ - Passes pandas dataframes from datacollector module in dictionary format of model reporters - :return: dict {(Param1, Param2,...,iteration): } - """ - - return self.datacollector_model_reporters - - def get_collector_agents(self): - """ - Passes pandas dataframes from datacollector module in dictionary format of agent reporters - :return: dict {(Param1, Param2,...,iteration): } - """ - return self.datacollector_agent_reporters - - def _prepare_report_table(self, vars_dict, extra_cols=None): - """ - Creates a dataframe from collected records and sorts it using 'Run' - column as a key. - """ - extra_cols = ["Run"] + (extra_cols or []) - index_cols = [] - if self.parameters_list: - index_cols = list(self.parameters_list[0].keys()) - index_cols += extra_cols - - records = [] - for param_key, values in vars_dict.items(): - record = dict(zip(index_cols, param_key)) - record.update(values) - records.append(record) - - df = pd.DataFrame(records) - rest_cols = set(df.columns) - set(index_cols) - ordered = df[index_cols + list(sorted(rest_cols))] - ordered.sort_values(by="Run", inplace=True) - if self._include_fixed: - for param, val in self.fixed_parameters.items(): - - # avoid error when val is an iterable - vallist = [val for i in range(ordered.shape[0])] - ordered[param] = vallist - return ordered - - -class ParameterProduct: - def __init__(self, variable_parameters): - self.param_names, self.param_lists = zip( - *(copy.deepcopy(variable_parameters)).items() - ) - self._product = product(*self.param_lists) - - def __iter__(self): - return self - - def __next__(self): - return dict(zip(self.param_names, next(self._product))) - - -# Roughly inspired by sklearn.model_selection.ParameterSampler. Does not handle -# distributions, only lists. -class ParameterSampler: - def __init__(self, parameter_lists, n, random_state=None): - self.param_names, self.param_lists = zip( - *(copy.deepcopy(parameter_lists)).items() - ) - self.n = n - if random_state is None: - self.random_state = random.Random() - elif isinstance(random_state, int): - self.random_state = random.Random(random_state) - else: - self.random_state = random_state - self.count = 0 - - def __iter__(self): - return self - - def __next__(self): - self.count += 1 - if self.count <= self.n: - return dict( - zip( - self.param_names, - [self.random_state.choice(p_list) for p_list in self.param_lists], - ) - ) - raise StopIteration() - - -class BatchRunner(FixedBatchRunner): - """DEPRECATION WARNING: BatchRunner Class has been replaced batch_run function - This class is instantiated with a model class, and model parameters - associated with one or more values. It is also instantiated with model and - agent-level reporters, dictionaries mapping a variable name to a function - which collects some data from the model or its agents at the end of the run - and stores it. - - Note that by default, the reporters only collect data at the *end* of the - run. To get step by step data, simply have a reporter store the model's - entire DataCollector object. - - """ - - def __init__( - self, - model_cls, - variable_parameters=None, - fixed_parameters=None, - iterations=1, - max_steps=1000, - model_reporters=None, - agent_reporters=None, - display_progress=True, - ): - """Create a new BatchRunner for a given model with the given - parameters. - - Args: - model_cls: The class of model to batch-run. - variable_parameters: Dictionary of parameters to lists of values. - The model will be run with every combo of these parameters. - For example, given variable_parameters of - {"param_1": range(5), - "param_2": [1, 5, 10]} - models will be run with {param_1=1, param_2=1}, - {param_1=2, param_2=1}, ..., {param_1=4, param_2=10}. - fixed_parameters: Dictionary of parameters that stay same through - all batch runs. For example, given fixed_parameters of - {"constant_parameter": 3}, - every instantiated model will be passed constant_parameter=3 - as a kwarg. - iterations: The total number of times to run the model for each - combination of parameters. - max_steps: Upper limit of steps above which each run will be halted - if it hasn't halted on its own. - model_reporters: The dictionary of variables to collect on each run - at the end, with variable names mapped to a function to collect - them. For example: - {"agent_count": lambda m: m.schedule.get_agent_count()} - agent_reporters: Like model_reporters, but each variable is now - collected at the level of each agent present in the model at - the end of the run. - display_progress: Display progress bar with time estimation? - - """ - warn( - "BatchRunner class has been replaced by batch_run function. Please see documentation.", - DeprecationWarning, - 2, - ) - if variable_parameters is None: - super().__init__( - model_cls, - variable_parameters, - fixed_parameters, - iterations, - max_steps, - model_reporters, - agent_reporters, - display_progress, - ) - else: - super().__init__( - model_cls, - ParameterProduct(variable_parameters), - fixed_parameters, - iterations, - max_steps, - model_reporters, - agent_reporters, - display_progress, - ) - - -class BatchRunnerMP(BatchRunner): # pragma: no cover - """DEPRECATION WARNING: BatchRunner class has been replaced by batch_run - Child class of BatchRunner, extended with multiprocessing support.""" - - def __init__(self, model_cls, nr_processes=None, **kwargs): - """Create a new BatchRunnerMP for a given model with the given - parameters. - - model_cls: The class of model to batch-run. - nr_processes: int - the number of separate processes the BatchRunner - should start, all running in parallel. - kwargs: the kwargs required for the parent BatchRunner class - """ - warn( - "BatchRunnerMP class has been replaced by batch_run function. Please see documentation.", - DeprecationWarning, - 2, - ) - if nr_processes is None: - # identify the number of processors available on users machine - available_processors = cpu_count() - self.processes = available_processors - print(f"BatchRunner MP will use {self.processes} processors.") - else: - self.processes = nr_processes - - super().__init__(model_cls, **kwargs) - self.pool = Pool(self.processes) - - def _make_model_args_mp(self): - """Prepare all combinations of parameter values for `run_all` - Due to multiprocessing requirements of @StaticMethod takes different input, hence the similar function - Returns: - List of list with the form: - [[model_object, dictionary_of_kwargs, max_steps, iterations]] - """ - total_iterations = self.iterations - all_kwargs = [] - - count = len(self.parameters_list) - if count: - for params in self.parameters_list: - kwargs = params.copy() - kwargs.update(self.fixed_parameters) - # run each iterations specific number of times - for iter in range(self.iterations): - kwargs_repeated = kwargs.copy() - all_kwargs.append( - [self.model_cls, kwargs_repeated, self.max_steps, iter] - ) - - elif len(self.fixed_parameters): - count = 1 - kwargs = self.fixed_parameters.copy() - all_kwargs.append(kwargs) - - total_iterations *= count - - return all_kwargs, total_iterations - - @staticmethod - def _run_wrappermp(iter_args): - """ - Based on requirement of Python multiprocessing requires @staticmethod decorator; - this is primarily to ensure functionality on Windows OS and does not impact MAC or Linux distros - - :param iter_args: List of arguments for model run - iter_args[0] = model object - iter_args[1] = key word arguments needed for model object - iter_args[2] = maximum number of steps for model - iter_args[3] = number of time to run model for stochastic/random variation with same parameters - :return: - tuple of param values which serves as a unique key for model results - model object - """ - - model_i = iter_args[0] - kwargs = iter_args[1] - max_steps = iter_args[2] - iteration = iter_args[3] - - # instantiate version of model with correct parameters - model = model_i(**kwargs) - while model.running and model.schedule.steps < max_steps: - model.step() - - # add iteration number to dictionary to make unique_key - kwargs["iteration"] = iteration - - # convert kwargs dict to tuple to make consistent - param_values = tuple(kwargs.values()) - - return param_values, model - - def _result_prep_mp(self, results): - """ - Helper Function - :param results: Takes results dictionary from Processpool and single processor debug run and fixes format to - make compatible with BatchRunner Output - :updates model_vars and agents_vars so consistent across all batchrunner - """ - # Take results and convert to dictionary so dataframe can be called - for model_key, model in results.items(): - if self.model_reporters: - self.model_vars[model_key] = self.collect_model_vars(model) - if self.agent_reporters: - agent_vars = self.collect_agent_vars(model) - for agent_id, reports in agent_vars.items(): - agent_key = model_key + (agent_id,) - self.agent_vars[agent_key] = reports - if hasattr(model, "datacollector"): - if model.datacollector.model_reporters is not None: - self.datacollector_model_reporters[ - model_key - ] = model.datacollector.get_model_vars_dataframe() - if model.datacollector.agent_reporters is not None: - self.datacollector_agent_reporters[ - model_key - ] = model.datacollector.get_agent_vars_dataframe() - - # Make results consistent - if len(self.datacollector_model_reporters) == 0: - self.datacollector_model_reporters = None - if len(self.datacollector_agent_reporters) == 0: - self.datacollector_agent_reporters = None - - def run_all(self): - """ - Run the model at all parameter combinations and store results, - overrides run_all from BatchRunner. - """ - - run_iter_args, total_iterations = self._make_model_args_mp() - # register the process pool and init a queue - # store results in ordered dictionary - results = {} - - if self.processes > 1: - with tqdm(total_iterations, disable=not self.display_progress) as pbar: - for params, model in self.pool.imap_unordered( - self._run_wrappermp, run_iter_args - ): - results[params] = model - pbar.update() - - self._result_prep_mp(results) - # For debugging model due to difficulty of getting errors during multiprocessing - else: - for run in run_iter_args: - params, model_data = self._run_wrappermp(run) - results[params] = model_data - - self._result_prep_mp(results) - - # Close multi-processing - self.pool.close() - - return ( - getattr(self, "model_vars", None), - getattr(self, "agent_vars", None), - getattr(self, "datacollector_model_reporters", None), - getattr(self, "datacollector_agent_reporters", None), - ) diff --git a/mesa/cookiecutter-mesa/hooks/post_gen_project.py b/mesa/cookiecutter-mesa/hooks/post_gen_project.py new file mode 100644 index 00000000000..24c615d517d --- /dev/null +++ b/mesa/cookiecutter-mesa/hooks/post_gen_project.py @@ -0,0 +1,11 @@ +import glob +import os + +file_list = glob.glob("**/*.pytemplate", recursive=True) + +for file_path in file_list: + # Check if the file is a regular file + if not os.path.isfile(file_path): + continue + # Rename the file + os.rename(file_path, file_path.replace(".pytemplate", ".py")) diff --git a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.py b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.pytemplate similarity index 100% rename from mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.py rename to mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.pytemplate diff --git a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.py b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate similarity index 79% rename from mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.py rename to mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate index 64023f8cb18..a72014b21de 100644 --- a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.py +++ b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate @@ -1,5 +1,5 @@ #!/usr/bin/env python -from setuptools import setup, find_packages +from setuptools import find_packages, setup requires = ["mesa"] diff --git a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.py b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate similarity index 79% rename from mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.py rename to mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate index ef59ed81d24..eedca040807 100644 --- a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.py +++ b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate @@ -1,10 +1,7 @@ -from mesa import Agent, Model -from mesa.time import RandomActivation -from mesa.space import MultiGrid -from mesa.datacollection import DataCollector +import mesa -class {{cookiecutter.agent}}(Agent): # noqa +class {{cookiecutter.agent}}(mesa.Agent): # noqa """ An agent """ @@ -24,7 +21,7 @@ def step(self): pass -class {{cookiecutter.model}}(Model): +class {{cookiecutter.model}}(mesa.Model): """ The model class holds the model-level attributes, manages the agents, and generally handles the global level of our model. @@ -38,8 +35,8 @@ class {{cookiecutter.model}}(Model): def __init__(self, num_agents, width, height): super().__init__() self.num_agents = num_agents - self.schedule = RandomActivation(self) - self.grid = MultiGrid(width=width, height=height, torus=True) + self.schedule = mesa.time.RandomActivation(self) + self.grid = mesa.space.MultiGrid(width=width, height=height, torus=True) for i in range(self.num_agents): agent = {{cookiecutter.agent}}(i, self) @@ -50,7 +47,7 @@ def __init__(self, num_agents, width, height): self.grid.place_agent(agent, (x, y)) # example data collector - self.datacollector = DataCollector() + self.datacollector = mesa.datacollection.DataCollector() self.running = True self.datacollector.collect(self) diff --git a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.py b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate similarity index 64% rename from mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.py rename to mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate index aa8e8a9fe83..832f2df7db4 100644 --- a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.py +++ b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate @@ -4,8 +4,7 @@ from .model import {{ cookiecutter.model }}, {{ cookiecutter.agent }} # noqa -from mesa.visualization.ModularVisualization import ModularServer -from mesa.visualization.modules import CanvasGrid, ChartModule +import mesa def circle_portrayal_example(agent): @@ -22,12 +21,14 @@ def circle_portrayal_example(agent): return portrayal -canvas_element = CanvasGrid(circle_portrayal_example, 20, 20, 500, 500) -chart_element = ChartModule([{"Label": "{{ cookiecutter.camel }}", "Color": "Pink"}]) +canvas_element = mesa.visualization.CanvasGrid( + circle_portrayal_example, 20, 20, 500, 500 +) +chart_element = mesa.visualization.ChartModule([{"Label": "{{ cookiecutter.camel }}", "Color": "Pink"}]) model_kwargs = {"num_agents": 10, "width": 10, "height": 10} -server = ModularServer( +server = mesa.visualization.ModularServer( {{cookiecutter.model}}, [canvas_element, chart_element], "{{ cookiecutter.camel }}", diff --git a/mesa/datacollection.py b/mesa/datacollection.py index 695742ad33b..8bddfa23da2 100644 --- a/mesa/datacollection.py +++ b/mesa/datacollection.py @@ -33,14 +33,12 @@ * The model has a schedule object called 'schedule' * The schedule has an agent list called agents * For collecting agent-level variables, agents must have a unique_id - """ -from functools import partial import itertools -from operator import attrgetter -import pandas as pd import types +import pandas as pd + class DataCollector: """Class for collecting data generated by a Mesa model. @@ -50,48 +48,57 @@ class DataCollector: functions which actually collect them. When the collect(...) method is called, it collects these attributes and executes these functions one by one and stores the results. - """ - def __init__(self, model_reporters=None, agent_reporters=None, tables=None): - """Instantiate a DataCollector with lists of model and agent reporters. + def __init__( + self, + model_reporters=None, + agent_reporters=None, + tables=None, + ): + """ + Instantiate a DataCollector with lists of model and agent reporters. Both model_reporters and agent_reporters accept a dictionary mapping a - variable name to either an attribute name, or a method. - For example, if there was only one model-level reporter for number of - agents, it might look like: - {"agent_count": lambda m: m.schedule.get_agent_count() } - If there was only one agent-level reporter (e.g. the agent's energy), - it might look like this: - {"energy": "energy"} - or like this: - {"energy": lambda a: a.energy} + variable name to either an attribute name, a function, a method of a class/instance, + or a function with parameters placed in a list. + + Model reporters can take four types of arguments: + 1. Lambda function: + {"agent_count": lambda m: m.schedule.get_agent_count()} + 2. Method of a class/instance: + {"agent_count": self.get_agent_count} # self here is a class instance + {"agent_count": Model.get_agent_count} # Model here is a class + 3. Class attributes of a model: + {"model_attribute": "model_attribute"} + 4. Functions with parameters that have been placed in a list: + {"Model_Function": [function, [param_1, param_2]]} + + Agent reporters can similarly take: + 1. Attribute name (string) referring to agent's attribute: + {"energy": "energy"} + 2. Lambda function: + {"energy": lambda a: a.energy} + 3. Method of an agent class/instance: + {"agent_action": self.do_action} # self here is an agent class instance + {"agent_action": Agent.do_action} # Agent here is a class + 4. Functions with parameters placed in a list: + {"Agent_Function": [function, [param_1, param_2]]} The tables arg accepts a dictionary mapping names of tables to lists of columns. For example, if we want to allow agents to write their age when they are destroyed (to keep track of lifespans), it might look like: - {"Lifespan": ["unique_id", "age"]} + {"Lifespan": ["unique_id", "age"]} Args: - model_reporters: Dictionary of reporter names and attributes/funcs - agent_reporters: Dictionary of reporter names and attributes/funcs. + model_reporters: Dictionary of reporter names and attributes/funcs/methods. + agent_reporters: Dictionary of reporter names and attributes/funcs/methods. tables: Dictionary of table names to lists of column names. Notes: - If you want to pickle your model you must not use lambda functions. - If your model includes a large number of agents, you should *only* - use attribute names for the agent reporter, it will be much faster. - - Model reporters can take four types of arguments: - lambda like above: - {"agent_count": lambda m: m.schedule.get_agent_count() } - method with @property decorators - {"agent_count": schedule.get_agent_count() - class attributes of model - {"model_attribute": "model_attribute"} - functions with parameters that have placed in a list - {"Model_Function":[function, [param_1, param_2]]} - + - If you want to pickle your model you must not use lambda functions. + - If your model includes a large number of agents, it is recommended to + use attribute names for the agent reporter, as it will be faster. """ self.model_reporters = {} self.agent_reporters = {} @@ -120,8 +127,6 @@ def _new_model_reporter(self, name, reporter): reporter: Attribute string, or function object that returns the variable when given a model instance. """ - if type(reporter) is str: - reporter = partial(self._getattr, reporter) self.model_reporters[name] = reporter self.model_vars[name] = [] @@ -130,14 +135,31 @@ def _new_agent_reporter(self, name, reporter): Args: name: Name of the agent-level variable to collect. - reporter: Attribute string, or function object that returns the - variable when given a model instance. - + reporter: Attribute string, function object, method of a class/instance, or + function with parameters placed in a list that returns the + variable when given an agent instance. """ - if type(reporter) is str: + # Check if the reporter is an attribute string + if isinstance(reporter, str): attribute_name = reporter - reporter = partial(self._getattr, reporter) - reporter.attribute_name = attribute_name + + def attr_reporter(agent): + return getattr(agent, attribute_name, None) + + reporter = attr_reporter + + # Check if the reporter is a function with arguments placed in a list + elif isinstance(reporter, list): + func, params = reporter[0], reporter[1] + + def func_with_params(agent): + return func(agent, *params) + + reporter = func_with_params + + # For other types (like lambda functions, method of a class/instance), + # it's already suitable to be used as a reporter directly. + self.agent_reporters[name] = reporter def _new_table(self, table_name, table_columns): @@ -146,7 +168,6 @@ def _new_table(self, table_name, table_columns): Args: table_name: Name of the new table. table_columns: List of columns to add to the table. - """ new_table = {column: [] for column in table_columns} self.tables[table_name] = new_table @@ -154,39 +175,32 @@ def _new_table(self, table_name, table_columns): def _record_agents(self, model): """Record agents data in a mapping of functions and agents.""" rep_funcs = self.agent_reporters.values() - if all(hasattr(rep, "attribute_name") for rep in rep_funcs): - prefix = ["model.schedule.steps", "unique_id"] - attributes = [func.attribute_name for func in rep_funcs] - get_reports = attrgetter(*prefix + attributes) - else: - def get_reports(agent): - _prefix = (agent.model.schedule.steps, agent.unique_id) - reports = tuple(rep(agent) for rep in rep_funcs) - return _prefix + reports + def get_reports(agent): + _prefix = (agent.model.schedule.steps, agent.unique_id) + reports = tuple(rep(agent) for rep in rep_funcs) + return _prefix + reports agent_records = map(get_reports, model.schedule.agents) return agent_records - def _reporter_decorator(self, reporter): - return reporter() - def collect(self, model): """Collect all the data for the given model object.""" if self.model_reporters: - for var, reporter in self.model_reporters.items(): # Check if Lambda operator if isinstance(reporter, types.LambdaType): self.model_vars[var].append(reporter(model)) # Check if model attribute - elif isinstance(reporter, partial): - self.model_vars[var].append(reporter(model)) + elif isinstance(reporter, str): + self.model_vars[var].append(getattr(model, reporter, None)) # Check if function with arguments elif isinstance(reporter, list): self.model_vars[var].append(reporter[0](*reporter[1])) + # TODO: Check if method of a class, as of now it is assumed + # implicitly if the other checks fail. else: - self.model_vars[var].append(self._reporter_decorator(reporter)) + self.model_vars[var].append(reporter()) if self.agent_reporters: agent_records = self._record_agents(model) @@ -200,7 +214,6 @@ def add_table_row(self, table_name, row, ignore_missing=False): row: A dictionary of the form {column_name: value...} ignore_missing: If True, fill any missing columns with Nones; if False, throw an error if any columns are missing - """ if table_name not in self.tables: raise Exception("Table does not exist.") @@ -213,18 +226,18 @@ def add_table_row(self, table_name, row, ignore_missing=False): else: raise Exception("Could not insert row with missing column") - @staticmethod - def _getattr(name, _object): - """Turn around arguments of getattr to make it partially callable.""" - return getattr(_object, name, None) - def get_model_vars_dataframe(self): """Create a pandas DataFrame from the model variables. The DataFrame has one column for each model variable, and the index is (implicitly) the model tick. - """ + # Check if self.model_reporters dictionary is empty, if so raise warning + if not self.model_reporters: + raise UserWarning( + "No model reporters have been defined in the DataCollector, returning empty DataFrame." + ) + return pd.DataFrame(self.model_vars) def get_agent_vars_dataframe(self): @@ -232,16 +245,21 @@ def get_agent_vars_dataframe(self): The DataFrame has one column for each variable, with two additional columns for tick and agent_id. - """ + # Check if self.agent_reporters dictionary is empty, if so raise warning + if not self.agent_reporters: + raise UserWarning( + "No agent reporters have been defined in the DataCollector, returning empty DataFrame." + ) + all_records = itertools.chain.from_iterable(self._agent_records.values()) rep_names = list(self.agent_reporters) df = pd.DataFrame.from_records( data=all_records, - columns=["Step", "AgentID"] + rep_names, + columns=["Step", "AgentID", *rep_names], + index=["Step", "AgentID"], ) - df = df.set_index(["Step", "AgentID"]) return df def get_table_dataframe(self, table_name): @@ -249,7 +267,6 @@ def get_table_dataframe(self, table_name): Args: table_name: The name of the table to convert. - """ if table_name not in self.tables: raise Exception("No such table.") diff --git a/mesa/experimental/__init__.py b/mesa/experimental/__init__.py new file mode 100644 index 00000000000..964dc5d19a3 --- /dev/null +++ b/mesa/experimental/__init__.py @@ -0,0 +1 @@ +from .jupyter_viz import JupyterViz, make_text # noqa diff --git a/mesa/experimental/jupyter_viz.py b/mesa/experimental/jupyter_viz.py new file mode 100644 index 00000000000..de207bf2926 --- /dev/null +++ b/mesa/experimental/jupyter_viz.py @@ -0,0 +1,338 @@ +import threading + +import matplotlib.pyplot as plt +import networkx as nx +import reacton.ipywidgets as widgets +import solara +from matplotlib.figure import Figure +from matplotlib.ticker import MaxNLocator + +import mesa + +# Avoid interactive backend +plt.switch_backend("agg") + + +@solara.component +def JupyterViz( + model_class, + model_params, + measures=None, + name="Mesa Model", + agent_portrayal=None, + space_drawer="default", + play_interval=150, +): + """Initialize a component to visualize a model. + Args: + model_class: class of the model to instantiate + model_params: parameters for initializing the model + measures: list of callables or data attributes to plot + name: name for display + agent_portrayal: options for rendering agents (dictionary) + space_drawer: method to render the agent space for + the model; default implementation is :meth:`make_space`; + simulations with no space to visualize should + specify `space_drawer=False` + play_interval: play interval (default: 150) + """ + + current_step, set_current_step = solara.use_state(0) + + # 1. Set up model parameters + user_params, fixed_params = split_model_params(model_params) + model_parameters, set_model_parameters = solara.use_state( + {**fixed_params, **{k: v["value"] for k, v in user_params.items()}} + ) + + # 2. Set up Model + def make_model(): + model = model_class(**model_parameters) + set_current_step(0) + return model + + reset_counter = solara.use_reactive(0) + model = solara.use_memo( + make_model, dependencies=[*list(model_parameters.values()), reset_counter.value] + ) + + def handle_change_model_params(name: str, value: any): + set_model_parameters({**model_parameters, name: value}) + + # 3. Set up UI + solara.Markdown(name) + UserInputs(user_params, on_change=handle_change_model_params) + ModelController(model, play_interval, current_step, set_current_step, reset_counter) + + with solara.GridFixed(columns=2): + # 4. Space + if space_drawer == "default": + # draw with the default implementation + make_space(model, agent_portrayal) + elif space_drawer: + # if specified, draw agent space with an alternate renderer + space_drawer(model, agent_portrayal) + # otherwise, do nothing (do not draw space) + + # 5. Plots + for measure in measures: + if callable(measure): + # Is a custom object + measure(model) + else: + make_plot(model, measure) + + +@solara.component +def ModelController( + model, play_interval, current_step, set_current_step, reset_counter +): + playing = solara.use_reactive(False) + thread = solara.use_reactive(None) + # We track the previous step to detect if user resets the model via + # clicking the reset button or changing the parameters. If previous_step > + # current_step, it means a model reset happens while the simulation is + # still playing. + previous_step = solara.use_reactive(0) + + def on_value_play(change): + if previous_step.value > current_step and current_step == 0: + # We add extra checks for current_step == 0, just to be sure. + # We automatically stop the playing if a model is reset. + playing.value = False + elif model.running: + do_step() + else: + playing.value = False + + def do_step(): + model.step() + previous_step.value = current_step + set_current_step(model.schedule.steps) + + def do_play(): + model.running = True + while model.running: + do_step() + + def threaded_do_play(): + if thread is not None and thread.is_alive(): + return + thread.value = threading.Thread(target=do_play) + thread.start() + + def do_pause(): + if (thread is None) or (not thread.is_alive()): + return + model.running = False + thread.join() + + def do_reset(): + reset_counter.value += 1 + + with solara.Row(): + solara.Button(label="Step", color="primary", on_click=do_step) + # This style is necessary so that the play widget has almost the same + # height as typical Solara buttons. + solara.Style( + """ + .widget-play { + height: 30px; + } + """ + ) + widgets.Play( + value=0, + interval=play_interval, + repeat=True, + show_repeat=False, + on_value=on_value_play, + playing=playing.value, + on_playing=playing.set, + ) + solara.Button(label="Reset", color="primary", on_click=do_reset) + solara.Markdown(md_text=f"**Step:** {current_step}") + # threaded_do_play is not used for now because it + # doesn't work in Google colab. We use + # ipywidgets.Play until it is fixed. The threading + # version is definite a much better implementation, + # if it works. + # solara.Button(label="▶", color="primary", on_click=viz.threaded_do_play) + # solara.Button(label="⏸︎", color="primary", on_click=viz.do_pause) + # solara.Button(label="Reset", color="primary", on_click=do_reset) + + +def split_model_params(model_params): + model_params_input = {} + model_params_fixed = {} + for k, v in model_params.items(): + if check_param_is_fixed(v): + model_params_fixed[k] = v + else: + model_params_input[k] = v + return model_params_input, model_params_fixed + + +def check_param_is_fixed(param): + if not isinstance(param, dict): + return True + if "type" not in param: + return True + + +@solara.component +def UserInputs(user_params, on_change=None): + """Initialize user inputs for configurable model parameters. + Currently supports :class:`solara.SliderInt`, :class:`solara.SliderFloat`, + :class:`solara.Select`, and :class:`solara.Checkbox`. + + Props: + user_params: dictionary with options for the input, including label, + min and max values, and other fields specific to the input type. + on_change: function to be called with (name, value) when the value of an input changes. + """ + + for name, options in user_params.items(): + # label for the input is "label" from options or name + label = options.get("label", name) + input_type = options.get("type") + + def change_handler(value, name=name): + on_change(name, value) + + if input_type == "SliderInt": + solara.SliderInt( + label, + value=options.get("value"), + on_value=change_handler, + min=options.get("min"), + max=options.get("max"), + step=options.get("step"), + ) + elif input_type == "SliderFloat": + solara.SliderFloat( + label, + value=options.get("value"), + on_value=change_handler, + min=options.get("min"), + max=options.get("max"), + step=options.get("step"), + ) + elif input_type == "Select": + solara.Select( + label, + value=options.get("value"), + on_value=change_handler, + values=options.get("values"), + ) + elif input_type == "Checkbox": + solara.Checkbox( + label=label, + on_value=change_handler, + value=options.get("value"), + ) + else: + raise ValueError(f"{input_type} is not a supported input type") + + +def make_space(model, agent_portrayal): + space_fig = Figure() + space_ax = space_fig.subplots() + space = getattr(model, "grid", None) + if space is None: + # Sometimes the space is defined as model.space instead of model.grid + space = model.space + if isinstance(space, mesa.space.NetworkGrid): + _draw_network_grid(space, space_ax, agent_portrayal) + elif isinstance(space, mesa.space.ContinuousSpace): + _draw_continuous_space(space, space_ax, agent_portrayal) + else: + _draw_grid(space, space_ax, agent_portrayal) + space_ax.set_axis_off() + solara.FigureMatplotlib(space_fig, format="png") + + +def _draw_grid(space, space_ax, agent_portrayal): + def portray(g): + x = [] + y = [] + s = [] # size + c = [] # color + for i in range(g.width): + for j in range(g.height): + content = g._grid[i][j] + if not content: + continue + if not hasattr(content, "__iter__"): + # Is a single grid + content = [content] + for agent in content: + data = agent_portrayal(agent) + x.append(i) + y.append(j) + if "size" in data: + s.append(data["size"]) + if "color" in data: + c.append(data["color"]) + out = {"x": x, "y": y} + if len(s) > 0: + out["s"] = s + if len(c) > 0: + out["c"] = c + return out + + space_ax.scatter(**portray(space)) + + +def _draw_network_grid(space, space_ax, agent_portrayal): + graph = space.G + pos = nx.spring_layout(graph, seed=0) + nx.draw( + graph, + ax=space_ax, + pos=pos, + **agent_portrayal(graph), + ) + + +def _draw_continuous_space(space, space_ax, agent_portrayal): + def portray(space): + x = [] + y = [] + s = [] # size + c = [] # color + for agent in space._agent_to_index: + data = agent_portrayal(agent) + _x, _y = agent.pos + x.append(_x) + y.append(_y) + if "size" in data: + s.append(data["size"]) + if "color" in data: + c.append(data["color"]) + out = {"x": x, "y": y} + if len(s) > 0: + out["s"] = s + if len(c) > 0: + out["c"] = c + return out + + space_ax.scatter(**portray(space)) + + +def make_plot(model, measure): + fig = Figure() + ax = fig.subplots() + df = model.datacollector.get_model_vars_dataframe() + ax.plot(df.loc[:, measure]) + ax.set_ylabel(measure) + # Set integer x axis + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + solara.FigureMatplotlib(fig) + + +def make_text(renderer): + def function(model): + solara.Markdown(renderer(model)) + + return function diff --git a/mesa/flat/visualization.py b/mesa/flat/visualization.py index d5222109121..2eb7b802c5e 100644 --- a/mesa/flat/visualization.py +++ b/mesa/flat/visualization.py @@ -1,5 +1,5 @@ # This collects all of Mesa visualization components under a flat namespace. -from mesa.visualization.ModularVisualization import * # noqa -from mesa.visualization.modules import * # noqa -from mesa.visualization.UserParam import * # noqa -from mesa.visualization.TextVisualization import * # noqa +from mesa_viz_tornado.ModularVisualization import * # noqa +from mesa_viz_tornado.modules import * # noqa +from mesa_viz_tornado.UserParam import * # noqa +from mesa_viz_tornado.TextVisualization import * # noqa diff --git a/mesa/main.py b/mesa/main.py index 51f0639ac16..197a36b9274 100644 --- a/mesa/main.py +++ b/mesa/main.py @@ -1,8 +1,12 @@ -import sys import os -import click +import sys +from pathlib import Path from subprocess import call +import click + +from mesa import __version__ + PROJECT_PATH = click.Path( exists=True, file_okay=False, dir_okay=True, resolve_path=True ) @@ -10,11 +14,12 @@ SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__)) COOKIECUTTER_PATH = os.path.join(os.path.dirname(SCRIPTS_DIR), COOKIECUTTER_DIR) +CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]} -@click.group() + +@click.group(context_settings=CONTEXT_SETTINGS) def cli(): "Manage Mesa projects" - pass @cli.command() @@ -25,12 +30,11 @@ def runserver(project): PROJECT is the path to the directory containing `run.py`, or the current directory if not specified. """ - sys.path.insert(0, project) - os.chdir(project) - - with open("run.py") as f: - code = compile(f.read(), "run.py", "exec") - exec(code, {}, {}) + run_path = Path(project) / "run.py" + if not run_path.exists(): + sys.exit(f"ERROR: file {run_path} does not exist") + args = [sys.executable, str(run_path)] + call(args) @click.command() @@ -38,11 +42,19 @@ def runserver(project): "--no-input", is_flag=True, help="Do not prompt user for custom mesa model input." ) def startproject(no_input): + """Create a new mesa project""" args = ["cookiecutter", COOKIECUTTER_PATH] if no_input: args.append("--no-input") call(args) +@click.command() +def version(): + """Show the version of mesa""" + print(f"mesa {__version__}") + + cli.add_command(runserver) cli.add_command(startproject) +cli.add_command(version) diff --git a/mesa/model.py b/mesa/model.py index aba1db9a741..1ac0edd6460 100644 --- a/mesa/model.py +++ b/mesa/model.py @@ -2,7 +2,6 @@ The model class for Mesa framework. Core Objects: Model - """ # Mypy; for the `|` operator purpose # Remove this __future__ import once the oldest supported Python is 3.10 @@ -10,11 +9,11 @@ import random -from mesa.datacollection import DataCollector - # mypy from typing import Any +from mesa.datacollection import DataCollector + class Model: """Base class for models.""" @@ -22,7 +21,11 @@ class Model: def __new__(cls, *args: Any, **kwargs: Any) -> Any: """Create a new model object and instantiate its RNG automatically.""" obj = object.__new__(cls) - obj._seed = kwargs.get("seed", None) + obj._seed = kwargs.get("seed") + if obj._seed is None: + # We explicitly specify the seed here so that we know its value in + # advance. + obj._seed = random.random() # noqa: S311 obj.random = random.Random(obj._seed) return obj @@ -33,7 +36,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: Attributes: schedule: schedule object running: a bool indicating if the model should continue running - """ self.running = True @@ -43,14 +45,12 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def run_model(self) -> None: """Run the model until the end condition is reached. Overload as needed. - """ while self.running: self.step() def step(self) -> None: """A single step. Fill in here.""" - pass def next_id(self) -> int: """Return the next unique ID for agents, increment current_id""" @@ -70,7 +70,10 @@ def reset_randomizer(self, seed: int | None = None) -> None: self._seed = seed def initialize_data_collector( - self, model_reporters=None, agent_reporters=None, tables=None + self, + model_reporters=None, + agent_reporters=None, + tables=None, ) -> None: if not hasattr(self, "schedule") or self.schedule is None: raise RuntimeError( diff --git a/mesa/space.py b/mesa/space.py index 362099671a2..6ed7c09697e 100644 --- a/mesa/space.py +++ b/mesa/space.py @@ -4,10 +4,13 @@ Objects used to add a spatial component to a model. -Grid: base grid, a simple list-of-lists. -SingleGrid: grid which strictly enforces one object per cell. -MultiGrid: extension to Grid where each cell is a set of objects. - +Grid: base grid, which creates a rectangular grid. +SingleGrid: extension to Grid which strictly enforces one agent per cell. +MultiGrid: extension to Grid where each cell can contain a set of agents. +HexGrid: extension to Grid to handle hexagonal neighbors. +ContinuousSpace: a two-dimensional space where each agent has an arbitrary + position of `float`'s. +NetworkGrid: a network where each node contains zero or more agents. """ # Instruction for PyLint to suppress variable name errors, since we have a # good reason to use one-character variable names for x and y. @@ -17,19 +20,16 @@ # Remove this __future__ import once the oldest supported Python is 3.10 from __future__ import annotations -import itertools import collections +import itertools import math -from warnings import warn - -import numpy as np - +from numbers import Real from typing import ( Any, Callable, - List, Iterable, Iterator, + List, Sequence, Tuple, TypeVar, @@ -37,11 +37,17 @@ cast, overload, ) +from warnings import warn + +import networkx as nx +import numpy as np +import numpy.typing as npt # For Mypy from .agent import Agent -from numbers import Real -import numpy.typing as npt + +# for better performance, we calculate the tuple to use in the is_integer function +_types_integer = (int, np.integer) Coordinate = Tuple[int, int] # used in ContinuousSpace @@ -56,41 +62,34 @@ F = TypeVar("F", bound=Callable[..., Any]) -def clamp(x: float, lowest: float, highest: float) -> float: - # much faster than np.clip for a scalar x. - return lowest if x <= lowest else (highest if x >= highest else x) - - def accept_tuple_argument(wrapped_function: F) -> F: """Decorator to allow grid methods that take a list of (x, y) coord tuples to also handle a single position, by automatically wrapping tuple in single-item list rather than forcing user to do it.""" - def wrapper(*args: Any) -> Any: - if isinstance(args[1], tuple) and len(args[1]) == 2: - return wrapped_function(args[0], [args[1]]) - else: - return wrapped_function(*args) + def wrapper(grid_instance, positions) -> Any: + if len(positions) == 2 and not isinstance(positions[0], tuple): + positions = [positions] + return wrapped_function(grid_instance, positions) return cast(F, wrapper) def is_integer(x: Real) -> bool: # Check if x is either a CPython integer or Numpy integer. - return isinstance(x, (int, np.integer)) + return isinstance(x, _types_integer) -class Grid: - """Base class for a square grid. +class _Grid: + """Base class for a rectangular grid. - Grid cells are indexed by [x][y], where [0][0] is assumed to be the - bottom-left and [width-1][height-1] is the top-right. If a grid is + Grid cells are indexed by [x, y], where [0, 0] is assumed to be the + bottom-left and [width-1, height-1] is the top-right. If a grid is toroidal, the top and bottom, and left and right, edges wrap to each other Properties: width, height: The grid's width and height. torus: Boolean which determines whether to treat the grid as a torus. - grid: Internal list-of-lists which holds the grid cells themselves. """ def __init__(self, width: int, height: int, torus: bool) -> None: @@ -103,25 +102,49 @@ def __init__(self, width: int, height: int, torus: bool) -> None: self.height = height self.width = width self.torus = torus + self.num_cells = height * width - self.grid: list[list[GridContent]] - self.grid = [ + # Internal list-of-lists which holds the grid cells themselves + self._grid: list[list[GridContent]] + self._grid = [ [self.default_val() for _ in range(self.height)] for _ in range(self.width) ] - # Add all cells to the empties list. - self.empties = set(itertools.product(range(self.width), range(self.height))) + # Flag to check if the empties set has been created. Better than initializing + # _empties as set() because in this case it would become impossible to discern + # if the set hasn't still being built or if it has become empty after creation. + self._empties_built = False # Neighborhood Cache - self._neighborhood_cache: dict[Any, list[Coordinate]] = dict() + self._neighborhood_cache: dict[Any, Sequence[Coordinate]] = {} + + # Cutoff used inside self.move_to_empty. The parameters are fitted on Python + # 3.11 and it was verified that they are roughly the same for 3.10. Refer to + # the code in PR#1565 to check for their stability when a new release gets out. + self.cutoff_empties = 7.953 * self.num_cells**0.384 @staticmethod def default_val() -> None: """Default value for new cell elements.""" return None + @property + def empties(self) -> set: + if not self._empties_built: + self.build_empties() + return self._empties + + def build_empties(self) -> None: + self._empties = set( + filter( + self.is_cell_empty, + itertools.product(range(self.width), range(self.height)), + ) + ) + self._empties_built = True + @overload - def __getitem__(self, index: int) -> list[GridContent]: + def __getitem__(self, index: int | Sequence[Coordinate]) -> list[GridContent]: ... @overload @@ -130,23 +153,16 @@ def __getitem__( ) -> GridContent | list[GridContent]: ... - @overload - def __getitem__(self, index: Sequence[Coordinate]) -> list[GridContent]: - ... - - def __getitem__( - self, - index: int | Sequence[Coordinate] | tuple[int | slice, int | slice], - ) -> GridContent | list[GridContent]: + def __getitem__(self, index): """Access contents from the grid.""" if isinstance(index, int): # grid[x] - return self.grid[index] + return self._grid[index] elif isinstance(index[0], tuple): # grid[(x1, y1), (x2, y2), ...] index = cast(Sequence[Coordinate], index) - return [self.grid[x][y] for x, y in map(self.torus_adj, index)] + return [self._grid[x][y] for x, y in map(self.torus_adj, index)] x, y = index x_int, y_int = is_integer(x), is_integer(y) @@ -155,47 +171,32 @@ def __getitem__( # grid[x, y] index = cast(Coordinate, index) x, y = self.torus_adj(index) - return self.grid[x][y] + return self._grid[x][y] elif x_int: # grid[x, :] x, _ = self.torus_adj((x, 0)) y = cast(slice, y) - return self.grid[x][y] + return self._grid[x][y] elif y_int: # grid[:, y] _, y = self.torus_adj((0, y)) x = cast(slice, x) - return [rows[y] for rows in self.grid[x]] + return [rows[y] for rows in self._grid[x]] else: # grid[:, :] x, y = (cast(slice, x), cast(slice, y)) - return [cell for rows in self.grid[x] for cell in rows[y]] + return [cell for rows in self._grid[x] for cell in rows[y]] def __iter__(self) -> Iterator[GridContent]: """Create an iterator that chains the rows of the grid together as if it is one list:""" - return itertools.chain(*self.grid) + return itertools.chain(*self._grid) - def coord_iter(self) -> Iterator[tuple[GridContent, int, int]]: - """An iterator that returns coordinates as well as cell contents.""" + def coord_iter(self) -> Iterator[tuple[GridContent, Coordinate]]: + """An iterator that returns positions as well as cell contents.""" for row in range(self.width): for col in range(self.height): - yield self.grid[row][col], row, col # agent, x, y - - def neighbor_iter(self, pos: Coordinate, moore: bool = True) -> Iterator[Agent]: - """Iterate over position neighbors. - - Args: - pos: (x,y) coords tuple for the position to get the neighbors of. - moore: Boolean for whether to use Moore neighborhood (including - diagonals) or Von Neumann (only up/down/left/right). - """ - - warn( - "`neighbor_iter` is deprecated in favor of `iter_neighbors` " - "and will be removed in the subsequent version." - ) - return self.iter_neighbors(pos, moore) + yield self._grid[row][col], (row, col) # agent, position def iter_neighborhood( self, @@ -231,7 +232,7 @@ def get_neighborhood( moore: bool, include_center: bool = False, radius: int = 1, - ) -> list[Coordinate]: + ) -> Sequence[Coordinate]: """Return a list of cells that are in the neighborhood of a certain point. @@ -256,32 +257,58 @@ def get_neighborhood( if neighborhood is not None: return neighborhood - coordinates: set[Coordinate] = set() + if self.out_of_bounds(pos): + raise Exception("The `pos` tuple passed is out of bounds.") + + # we use a dict to keep insertion order + neighborhood = {} x, y = pos - for dy in range(-radius, radius + 1): - for dx in range(-radius, radius + 1): - # Skip coordinates that are outside manhattan distance - if not moore and abs(dx) + abs(dy) > radius: - continue - coord = (x + dx, y + dy) + # First we check if the neighborhood is inside the grid + if ( + x >= radius + and self.width - x > radius + and y >= radius + and self.height - y > radius + ): + # If the radius is smaller than the distance from the borders, we + # can skip boundary checks. + x_range = range(x - radius, x + radius + 1) + y_range = range(y - radius, y + radius + 1) + + for new_x in x_range: + for new_y in y_range: + if not moore and abs(new_x - x) + abs(new_y - y) > radius: + continue + + neighborhood[(new_x, new_y)] = True - if self.out_of_bounds(coord): - # Skip if not a torus and new coords out of bounds. - if not self.torus: + else: + # If the radius is larger than the distance from the borders, we + # must use a slower method, that takes into account the borders + # and the torus property. + for dx in range(-radius, radius + 1): + for dy in range(-radius, radius + 1): + if not moore and abs(dx) + abs(dy) > radius: continue - coord = self.torus_adj(coord) - coordinates.add(coord) + new_x = x + dx + new_y = y + dy + + if self.torus: + new_x %= self.width + new_y %= self.height + + if not self.out_of_bounds((new_x, new_y)): + neighborhood[(new_x, new_y)] = True if not include_center: - coordinates.discard(pos) + neighborhood.pop(pos, None) - neighborhood = sorted(coordinates) - self._neighborhood_cache[cache_key] = neighborhood + self._neighborhood_cache[cache_key] = tuple(neighborhood.keys()) - return neighborhood + return tuple(neighborhood.keys()) def iter_neighbors( self, @@ -357,34 +384,41 @@ def out_of_bounds(self, pos: Coordinate) -> bool: def iter_cell_list_contents( self, cell_list: Iterable[Coordinate] ) -> Iterator[Agent]: - """Returns an iterator of the contents of the cells - identified in cell_list. + """Returns an iterator of the agents contained in the cells identified + in `cell_list`; cells with empty content are excluded. Args: cell_list: Array-like of (x, y) tuples, or single tuple. Returns: - An iterator of the contents of the cells identified in cell_list + An iterator of the agents contained in the cells identified in `cell_list`. """ - # Note: filter(None, iterator) filters away an element of iterator that - # is falsy. Hence, iter_cell_list_contents returns only non-empty - # contents. - return filter(None, (self.grid[x][y] for x, y in cell_list)) + # iter_cell_list_contents returns only non-empty contents. + return ( + cell + for x, y in cell_list + if (cell := self._grid[x][y]) != self.default_val() + ) @accept_tuple_argument def get_cell_list_contents(self, cell_list: Iterable[Coordinate]) -> list[Agent]: - """Returns a list of the contents of the cells - identified in cell_list. - Note: this method returns a list of `Agent`'s; `None` contents are excluded. + """Returns an iterator of the agents contained in the cells identified + in `cell_list`; cells with empty content are excluded. Args: cell_list: Array-like of (x, y) tuples, or single tuple. Returns: - A list of the contents of the cells identified in cell_list + A list of the agents contained in the cells identified in `cell_list`. """ return list(self.iter_cell_list_contents(cell_list)) + def place_agent(self, agent: Agent, pos: Coordinate) -> None: + ... + + def remove_agent(self, agent: Agent) -> None: + ... + def move_agent(self, agent: Agent, pos: Coordinate) -> None: """Move an agent from its current position to a new position. @@ -397,57 +431,43 @@ def move_agent(self, agent: Agent, pos: Coordinate) -> None: self.remove_agent(agent) self.place_agent(agent, pos) - def place_agent(self, agent: Agent, pos: Coordinate) -> None: - """Place the agent at the specified location, and set its pos variable.""" - x, y = pos - self.grid[x][y] = agent - self.empties.discard(pos) - agent.pos = pos + def swap_pos(self, agent_a: Agent, agent_b: Agent) -> None: + """Swap agents positions""" + agents_no_pos = [] + if (pos_a := agent_a.pos) is None: + agents_no_pos.append(agent_a) + if (pos_b := agent_b.pos) is None: + agents_no_pos.append(agent_b) + if agents_no_pos: + agents_no_pos = [f"" for a in agents_no_pos] + raise Exception(f"{', '.join(agents_no_pos)} - not on the grid") - def remove_agent(self, agent: Agent) -> None: - """Remove the agent from the grid and set its pos attribute to None.""" - if (pos := agent.pos) is None: + if pos_a == pos_b: return - x, y = pos - self.grid[x][y] = self.default_val() - self.empties.add(pos) - agent.pos = None + + self.remove_agent(agent_a) + self.remove_agent(agent_b) + + self.place_agent(agent_a, pos_b) + self.place_agent(agent_b, pos_a) def is_cell_empty(self, pos: Coordinate) -> bool: """Returns a bool of the contents of a cell.""" x, y = pos - return self.grid[x][y] == self.default_val() + return self._grid[x][y] == self.default_val() - def move_to_empty( - self, agent: Agent, cutoff: float = 0.998, num_agents: int | None = None - ) -> None: + def move_to_empty(self, agent: Agent) -> None: """Moves agent to a random empty cell, vacating agent's old cell.""" - if len(self.empties) == 0: + num_empty_cells = len(self.empties) + if num_empty_cells == 0: raise Exception("ERROR: No empty cells") - if num_agents is None: - try: - num_agents = agent.model.schedule.get_agent_count() - except AttributeError: - raise Exception( - "Your agent is not attached to a model, and so Mesa is unable\n" - "to figure out the total number of agents you have created.\n" - "This number is required in order to calculate the threshold\n" - "for using a much faster algorithm to find an empty cell.\n" - "In this case, you must specify `num_agents`." - ) - new_pos = (0, 0) # Initialize it with a starting value. - # This method is based on Agents.jl's random_empty() implementation. - # See https://github.com/JuliaDynamics/Agents.jl/pull/541. - # For the discussion, see - # https://github.com/projectmesa/mesa/issues/1052. - # This switch assumes the worst case (for this algorithm) of one - # agent per position, which is not true in general but is appropriate - # here. - if clamp(num_agents / (self.width * self.height), 0.0, 1.0) < cutoff: - # The default cutoff value provided is the break-even comparison - # with the time taken in the else branching point. - # The number is measured to be 0.998 in Agents.jl, but since Mesa - # run under different environment, the number is different here. + + # This method is based on Agents.jl's random_empty() implementation. See + # https://github.com/JuliaDynamics/Agents.jl/pull/541. For the discussion, see + # https://github.com/projectmesa/mesa/issues/1052 and + # https://github.com/projectmesa/mesa/pull/1565. The cutoff value provided + # is the break-even comparison with the time taken in the else branching point. + if num_empty_cells > self.cutoff_empties: while True: new_pos = ( agent.random.randrange(self.width), @@ -460,98 +480,55 @@ def move_to_empty( self.remove_agent(agent) self.place_agent(agent, new_pos) - def find_empty(self) -> Coordinate | None: - """Pick a random empty cell.""" - import random - - warn( - ( - "`find_empty` is being phased out since it uses the global " - "`random` instead of the model-level random-number generator. " - "Consider replacing it with having a model or agent object " - "explicitly pick one of the grid's list of empty cells." - ), - DeprecationWarning, - ) - - if self.exists_empty_cells(): - pos = random.choice(sorted(self.empties)) - return pos - else: - return None - def exists_empty_cells(self) -> bool: """Return True if any cells empty else False.""" return len(self.empties) > 0 -class SingleGrid(Grid): - """Grid where each cell contains exactly at most one object.""" - - empties: set[Coordinate] = set() - - def position_agent( - self, agent: Agent, x: int | str = "random", y: int | str = "random" - ) -> None: - """Position an agent on the grid. - This is used when first placing agents! Setting either x or y to "random" - gives the same behavior as 'move_to_empty()' to get a random position. - If x or y are positive, they are used. - Use 'swap_pos()' to swap agents positions. - """ - if x == "random" or y == "random": - if len(self.empties) == 0: - raise Exception("ERROR: Grid full") - self.move_to_empty(agent) - else: - coords = (x, y) - self.place_agent(agent, coords) +class SingleGrid(_Grid): + """Rectangular grid where each cell contains exactly at most one agent. - def swap_pos(self, agent_a: Agent, agent_b: Agent) -> None: - """Swap agents positions""" - agents_no_pos = [] - if (pos_a := agent_a.pos) is None: - agents_no_pos.append(agent_a) - if (pos_b := agent_b.pos) is None: - agents_no_pos.append(agent_b) - if agents_no_pos: - agents_no_pos = [f"" for a in agents_no_pos] - raise Exception(f"{', '.join(agents_no_pos)} - not on the grid") - - if pos_a == pos_b: - return - - self.remove_agent(agent_a) - self.remove_agent(agent_b) + Grid cells are indexed by [x, y], where [0, 0] is assumed to be the + bottom-left and [width-1, height-1] is the top-right. If a grid is + toroidal, the top and bottom, and left and right, edges wrap to each other. - self.place_agent(agent_a, pos_b) - self.place_agent(agent_b, pos_a) + Properties: + width, height: The grid's width and height. + torus: Boolean which determines whether to treat the grid as a torus. + """ def place_agent(self, agent: Agent, pos: Coordinate) -> None: + """Place the agent at the specified location, and set its pos variable.""" if self.is_cell_empty(pos): - super().place_agent(agent, pos) + x, y = pos + self._grid[x][y] = agent + if self._empties_built: + self._empties.discard(pos) + agent.pos = pos else: raise Exception("Cell not empty") + def remove_agent(self, agent: Agent) -> None: + """Remove the agent from the grid and set its pos attribute to None.""" + if (pos := agent.pos) is None: + return + x, y = pos + self._grid[x][y] = self.default_val() + if self._empties_built: + self._empties.add(pos) + agent.pos = None -class MultiGrid(Grid): - """Grid where each cell can contain more than one object. - Grid cells are indexed by [x][y], where [0][0] is assumed to be at - bottom-left and [width-1][height-1] is the top-right. If a grid is - toroidal, the top and bottom, and left and right, edges wrap to each other. +class MultiGrid(_Grid): + """Rectangular grid where each cell can contain more than one agent. - Each grid cell holds a set object. + Grid cells are indexed by [x, y], where [0, 0] is assumed to be at + bottom-left and [width-1, height-1] is the top-right. If a grid is + toroidal, the top and bottom, and left and right, edges wrap to each other. Properties: width, height: The grid's width and height. - torus: Boolean which determines whether to treat the grid as a torus. - - grid: Internal list-of-lists which holds the grid cells themselves. - - Methods: - get_neighbors: Returns the objects surrounding a given cell. """ grid: list[list[MultiGridContent]] @@ -564,41 +541,43 @@ def default_val() -> MultiGridContent: def place_agent(self, agent: Agent, pos: Coordinate) -> None: """Place the agent at the specified location, and set its pos variable.""" x, y = pos - if agent not in self.grid[x][y]: - self.grid[x][y].append(agent) - self.empties.discard(pos) - agent.pos = pos + if agent.pos is None or agent not in self._grid[x][y]: + self._grid[x][y].append(agent) + agent.pos = pos + if self._empties_built: + self._empties.discard(pos) def remove_agent(self, agent: Agent) -> None: """Remove the agent from the given location and set its pos attribute to None.""" pos = agent.pos x, y = pos - self.grid[x][y].remove(agent) - if self.is_cell_empty(pos): - self.empties.add(pos) + self._grid[x][y].remove(agent) + if self._empties_built and self.is_cell_empty(pos): + self._empties.add(pos) agent.pos = None @accept_tuple_argument def iter_cell_list_contents( self, cell_list: Iterable[Coordinate] - ) -> Iterator[MultiGridContent]: - """Returns an iterator of the contents of the - cells identified in cell_list. + ) -> Iterator[Agent]: + """Returns an iterator of the agents contained in the cells identified + in `cell_list`; cells with empty content are excluded. Args: cell_list: Array-like of (x, y) tuples, or single tuple. Returns: - A iterator of the contents of the cells identified in cell_list - + An iterator of the agents contained in the cells identified in `cell_list`. """ return itertools.chain.from_iterable( - self[x][y] for x, y in cell_list if not self.is_cell_empty((x, y)) + cell + for x, y in cell_list + if (cell := self._grid[x][y]) != self.default_val() ) -class HexGrid(Grid): - """Hexagonal Grid: Extends Grid to handle hexagonal neighbors. +class _HexGrid: + """Hexagonal Grid which handles hexagonal neighbors. Functions according to odd-q rules. See http://www.redblobgames.com/grids/hexagons/#coordinates for more. @@ -653,12 +632,10 @@ def get_neighborhood( coordinates = set() while radius > 0: - level_size = len(queue) radius -= 1 - for i in range(level_size): - + for _i in range(level_size): x, y = queue.pop() if x % 2 == 0: @@ -703,24 +680,11 @@ def get_neighborhood( else: coordinates.discard(pos) - neighborhood = sorted(coordinates) + neighborhood = tuple(sorted(coordinates)) self._neighborhood_cache[cache_key] = neighborhood return neighborhood - def neighbor_iter(self, pos: Coordinate) -> Iterator[Agent]: - """Iterate over position neighbors. - - Args: - pos: (x,y) coords tuple for the position to get the neighbors of. - """ - - warn( - "`neighbor_iter` is deprecated in favor of `iter_neighbors` " - "and will be removed in the subsequent version." - ) - return self.iter_neighbors(pos) - def iter_neighborhood( self, pos: Coordinate, include_center: bool = False, radius: int = 1 ) -> Iterator[Coordinate]: @@ -753,7 +717,7 @@ def iter_neighbors( Returns: An iterator of non-None objects in the given neighborhood """ - neighborhood = self.iter_neighborhood(pos, include_center, radius) + neighborhood = self.get_neighborhood(pos, include_center, radius) return self.iter_cell_list_contents(neighborhood) def get_neighbors( @@ -774,19 +738,67 @@ def get_neighbors( return list(self.iter_neighbors(pos, include_center, radius)) +class HexSingleGrid(_HexGrid, SingleGrid): + """Hexagonal SingleGrid: a SingleGrid where neighbors are computed + according to a hexagonal tiling of the grid. + + Functions according to odd-q rules. + See http://www.redblobgames.com/grids/hexagons/#coordinates for more. + + Properties: + width, height: The grid's width and height. + torus: Boolean which determines whether to treat the grid as a torus. + """ + + +class HexMultiGrid(_HexGrid, MultiGrid): + """Hexagonal MultiGrid: a MultiGrid where neighbors are computed + according to a hexagonal tiling of the grid. + + Functions according to odd-q rules. + See http://www.redblobgames.com/grids/hexagons/#coordinates for more. + + Properties: + width, height: The grid's width and height. + torus: Boolean which determines whether to treat the grid as a torus. + """ + + +class HexGrid(HexSingleGrid): + """Hexagonal Grid: a Grid where neighbors are computed + according to a hexagonal tiling of the grid. + + Functions according to odd-q rules. + See http://www.redblobgames.com/grids/hexagons/#coordinates for more. + + Properties: + width, height: The grid's width and height. + torus: Boolean which determines whether to treat the grid as a torus. + """ + + def __init__(self, width: int, height: int, torus: bool) -> None: + super().__init__(width, height, torus) + warn( + ( + "HexGrid is being deprecated; use instead HexSingleGrid or HexMultiGrid " + "depending on your use case." + ), + DeprecationWarning, + stacklevel=2, + ) + + class ContinuousSpace: """Continuous space where each agent can have an arbitrary position. - Assumes that all agents are point objects, and have a pos property storing - their position as an (x, y) tuple. + Assumes that all agents have a pos property storing their position as + an (x, y) tuple. - This class uses a numpy array internally to store agent objects, to speed + This class uses a numpy array internally to store agents in order to speed up neighborhood lookups. This array is calculated on the first neighborhood - lookup, and is reused (and updated) until agents are added or removed. + lookup, and is updated if agents are added or removed. """ - _grid = None - def __init__( self, x_max: float, @@ -819,18 +831,16 @@ def __init__( self._agent_to_index: dict[Agent, int | None] = {} def _build_agent_cache(self): - """Cache Agent positions to speed up neighbors calculations.""" + """Cache agents positions to speed up neighbors calculations.""" self._index_to_agent = {} - agents = self._agent_to_index.keys() - for idx, agent in enumerate(agents): + for idx, agent in enumerate(self._agent_to_index): self._agent_to_index[agent] = idx self._index_to_agent[idx] = agent - self._agent_points = np.array( - [self._index_to_agent[idx].pos for idx in range(len(agents))] - ) + # Since dicts are ordered by insertion, we can iterate through agents keys + self._agent_points = np.array([agent.pos for agent in self._agent_to_index]) def _invalidate_agent_cache(self): - """Clear cached data of Agents and positions in the space.""" + """Clear cached data of agents and positions in the space.""" self._agent_points = None self._index_to_agent = {} @@ -860,18 +870,17 @@ def move_agent(self, agent: Agent, pos: FloatCoordinate) -> None: # instead of invalidating the full cache, # apply the move to the cached values idx = self._agent_to_index[agent] - self._agent_points[idx, 0] = pos[0] - self._agent_points[idx, 1] = pos[1] + self._agent_points[idx] = pos def remove_agent(self, agent: Agent) -> None: - """Remove an agent from the simulation. + """Remove an agent from the space. Args: agent: The agent object to remove """ if agent not in self._agent_to_index: raise Exception("Agent does not exist in the space") - self._agent_to_index.pop(agent) + del self._agent_to_index[agent] self._invalidate_agent_cache() agent.pos = None @@ -879,7 +888,7 @@ def remove_agent(self, agent: Agent) -> None: def get_neighbors( self, pos: FloatCoordinate, radius: float, include_center: bool = True ) -> list[Agent]: - """Get all objects within a certain radius. + """Get all agents within a certain radius. Args: pos: (x,y) coordinate tuple to center the search at. @@ -906,18 +915,30 @@ def get_neighbors( def get_heading( self, pos_1: FloatCoordinate, pos_2: FloatCoordinate ) -> FloatCoordinate: - """Get the heading angle between two points, accounting for toroidal space. + """Get the heading vector between two points, accounting for toroidal space. + It is possible to calculate the heading angle by applying the atan2 function to the + result. Args: pos_1, pos_2: Coordinate tuples for both points. """ one = np.array(pos_1) two = np.array(pos_2) - if self.torus: - one = (one - self.center) % self.size - two = (two - self.center) % self.size heading = two - one - if isinstance(pos_1, tuple): + if self.torus: + inverse_heading = heading - np.sign(heading) * self.size + + def get_min_abs(x, y): + return x if abs(x) < abs(y) else y + + # Choose the smaller heading based on their absolute value for + # each dimension independently. + heading = tuple( + get_min_abs(heading[i], inverse_heading[i]) for i in range(2) + ) + if isinstance(pos_1, np.ndarray): + heading = np.asarray(heading) + else: heading = tuple(heading) return heading @@ -968,29 +989,50 @@ def out_of_bounds(self, pos: FloatCoordinate) -> bool: class NetworkGrid: """Network Grid where each node contains zero or more agents.""" - def __init__(self, G: Any) -> None: - self.G = G + def __init__(self, g: Any) -> None: + """Create a new network. + + Args: + G: a NetworkX graph instance. + """ + self.G = g for node_id in self.G.nodes: - G.nodes[node_id]["agent"] = list() + g.nodes[node_id]["agent"] = self.default_val() - def place_agent(self, agent: Agent, node_id: int) -> None: - """Place a agent in a node.""" + @staticmethod + def default_val() -> list: + """Default value for a new node.""" + return [] + def place_agent(self, agent: Agent, node_id: int) -> None: + """Place an agent in a node.""" self.G.nodes[node_id]["agent"].append(agent) agent.pos = node_id - def get_neighbors(self, node_id: int, include_center: bool = False) -> list[int]: - """Get all adjacent nodes""" + def get_neighborhood( + self, node_id: int, include_center: bool = False, radius: int = 1 + ) -> list[int]: + """Get all adjacent nodes within a certain radius""" + if radius == 1: + neighborhood = list(self.G.neighbors(node_id)) + if include_center: + neighborhood.append(node_id) + else: + neighbors_with_distance = nx.single_source_shortest_path_length( + self.G, node_id, radius + ) + if not include_center: + del neighbors_with_distance[node_id] + neighborhood = sorted(neighbors_with_distance.keys()) + return neighborhood - neighbors = list(self.G.neighbors(node_id)) - if include_center: - neighbors.append(node_id) - - return neighbors + def get_neighbors(self, node_id: int, include_center: bool = False) -> list[Agent]: + """Get all agents in adjacent nodes.""" + neighborhood = self.get_neighborhood(node_id, include_center) + return self.get_cell_list_contents(neighborhood) def move_agent(self, agent: Agent, node_id: int) -> None: """Move an agent from its current node to a new node.""" - self.remove_agent(agent) self.place_agent(agent, node_id) @@ -1002,25 +1044,23 @@ def remove_agent(self, agent: Agent) -> None: def is_cell_empty(self, node_id: int) -> bool: """Returns a bool of the contents of a cell.""" - return not self.G.nodes[node_id]["agent"] + return self.G.nodes[node_id]["agent"] == self.default_val() - def get_cell_list_contents(self, cell_list: list[int]) -> list[GridContent]: - """Returns the contents of a list of cells ((x,y) tuples) - Note: this method returns a list of `Agent`'s; `None` contents are excluded. + def get_cell_list_contents(self, cell_list: list[int]) -> list[Agent]: + """Returns a list of the agents contained in the nodes identified + in `cell_list`; nodes with empty content are excluded. """ return list(self.iter_cell_list_contents(cell_list)) - def get_all_cell_contents(self) -> list[GridContent]: - """Returns a list of the contents of the cells - identified in cell_list.""" - return list(self.iter_cell_list_contents(self.G)) + def get_all_cell_contents(self) -> list[Agent]: + """Returns a list of all the agents in the network.""" + return self.get_cell_list_contents(self.G) - def iter_cell_list_contents(self, cell_list: list[int]) -> list[GridContent]: - """Returns an iterator of the contents of the cells - identified in cell_list.""" - list_of_lists = [ + def iter_cell_list_contents(self, cell_list: list[int]) -> Iterator[Agent]: + """Returns an iterator of the agents contained in the nodes identified + in `cell_list`; nodes with empty content are excluded. + """ + return itertools.chain.from_iterable( self.G.nodes[node_id]["agent"] - for node_id in cell_list - if not self.is_cell_empty(node_id) - ] - return [item for sublist in list_of_lists for item in sublist] + for node_id in itertools.filterfalse(self.is_cell_empty, cell_list) + ) diff --git a/mesa/time.py b/mesa/time.py index 34fc8705fad..f0816cd68dc 100644 --- a/mesa/time.py +++ b/mesa/time.py @@ -28,11 +28,11 @@ from collections import defaultdict # mypy -from typing import Iterator, Union, Iterable +from typing import Union + from mesa.agent import Agent from mesa.model import Model - # BaseScheduler has a self.time of int, while # StagedActivation has a self.time of float TimeT = Union[float, int] @@ -45,7 +45,6 @@ class BaseScheduler: Assumes that each agent added has a *step* method which takes no arguments. (This is explicitly meant to replicate the scheduler in MASON). - """ def __init__(self, model: Model) -> None: @@ -61,12 +60,10 @@ def add(self, agent: Agent) -> None: Args: agent: An Agent to be added to the schedule. NOTE: The agent must have a step() method. - """ - if agent.unique_id in self._agents: raise Exception( - f"Agent with unique id {repr(agent.unique_id)} already added to scheduler" + f"Agent with unique id {agent.unique_id!r} already added to scheduler" ) self._agents[agent.unique_id] = agent @@ -76,14 +73,14 @@ def remove(self, agent: Agent) -> None: Args: agent: An agent object. - """ del self._agents[agent.unique_id] def step(self) -> None: """Execute the step of all the agents, one at a time.""" - for agent in self.agent_buffer(shuffled=False): - agent.step() + # To be able to remove and/or add agents during stepping + # it's necessary for the keys view to be a list. + self.do_each("step") self.steps += 1 self.time += 1 @@ -95,19 +92,22 @@ def get_agent_count(self) -> int: def agents(self) -> list[Agent]: return list(self._agents.values()) - def agent_buffer(self, shuffled: bool = False) -> Iterator[Agent]: - """Simple generator that yields the agents while letting the user - remove and/or add agents during stepping. - - """ - agent_keys = self._agents.keys() - if shuffled: - agent_keys = list(agent_keys) + def get_agent_keys(self, shuffle: bool = False) -> list[int]: + # To be able to remove and/or add agents during stepping + # it's necessary to cast the keys view to a list. + agent_keys = list(self._agents.keys()) + if shuffle: self.model.random.shuffle(agent_keys) + return agent_keys - for key in agent_keys: - if key in self._agents: - yield self._agents[key] + def do_each(self, method, agent_keys=None, shuffle=False): + if agent_keys is None: + agent_keys = self.get_agent_keys() + if shuffle: + self.model.random.shuffle(agent_keys) + for agent_key in agent_keys: + if agent_key in self._agents: + getattr(self._agents[agent_key], method)() class RandomActivation(BaseScheduler): @@ -118,7 +118,6 @@ class RandomActivation(BaseScheduler): default behavior for an ABM. Assumes that all agents have a step(model) method. - """ def step(self) -> None: @@ -126,8 +125,7 @@ def step(self) -> None: random order. """ - for agent in self.agent_buffer(shuffled=True): - agent.step() + self.do_each("step", shuffle=True) self.steps += 1 self.time += 1 @@ -138,17 +136,15 @@ class SimultaneousActivation(BaseScheduler): This scheduler requires that each agent have two methods: step and advance. step() activates the agent and stages any necessary changes, but does not apply them yet. advance() then applies the changes. - """ def step(self) -> None: """Step all agents, then advance them.""" - for agent in self._agents.values(): - agent.step() - # the previous steps might remove some agents, but - # this loop will go over the remaining existing agents - for agent in self._agents.values(): - agent.advance() + self.do_each("step") + # do_each recomputes the agent_keys from scratch whenever it is called. + # It can handle the case when some agents might have been removed in + # the previous loop. + self.do_each("advance") self.steps += 1 self.time += 1 @@ -163,7 +159,6 @@ class StagedActivation(BaseScheduler): This schedule tracks steps and time separately. Time advances in fractional increments of 1 / (# of stages), meaning that 1 step = 1 unit of time. - """ def __init__( @@ -183,29 +178,26 @@ def __init__( shuffle_between_stages: If True, shuffle the agents after each stage; otherwise, only shuffle at the start of each step. - """ super().__init__(model) - self.stage_list = ["step"] if not stage_list else stage_list + self.stage_list = stage_list if stage_list else ["step"] self.shuffle = shuffle self.shuffle_between_stages = shuffle_between_stages self.stage_time = 1 / len(self.stage_list) def step(self) -> None: """Executes all the stages for all agents.""" - agent_keys = self._agents.keys() - if self.shuffle: - agent_keys = list(agent_keys) - self.model.random.shuffle(agent_keys) + # To be able to remove and/or add agents during stepping + # it's necessary for the keys view to be a list. + agent_keys = self.get_agent_keys(self.shuffle) for stage in self.stage_list: - for agent_key in agent_keys: - getattr(self._agents[agent_key], stage)() # Run stage + if stage.startswith("model."): + getattr(self.model, stage[6:])() + else: + self.do_each(stage, agent_keys=agent_keys) # We recompute the keys because some agents might have been removed # in the previous loop. - agent_keys = self._agents.keys() - if self.shuffle_between_stages: - agent_keys = list(agent_keys) - self.model.random.shuffle(agent_keys) + agent_keys = self.get_agent_keys(self.shuffle_between_stages) self.time += self.stage_time self.steps += 1 @@ -242,7 +234,6 @@ def add(self, agent: Agent) -> None: Args: agent: An Agent to be added to the schedule. """ - super().add(agent) agent_class: type[Agent] = type(agent) self.agents_by_type[agent_class][agent.unique_id] = agent @@ -251,7 +242,6 @@ def remove(self, agent: Agent) -> None: """ Remove all instances of a given agent from the schedule. """ - del self._agents[agent.unique_id] agent_class: type[Agent] = type(agent) @@ -267,9 +257,10 @@ def step(self, shuffle_types: bool = True, shuffle_agents: bool = True) -> None: shuffle_agents: If True, the order of execution of each agents in a type group is shuffled. """ - type_keys: Iterable[type[Agent]] = self.agents_by_type.keys() + # To be able to remove and/or add agents during stepping + # it's necessary to cast the keys view to a list. + type_keys: list[type[Agent]] = list(self.agents_by_type.keys()) if shuffle_types: - type_keys = list(type_keys) self.model.random.shuffle(type_keys) for agent_class in type_keys: self.step_type(agent_class, shuffle_agents=shuffle_agents) @@ -284,12 +275,12 @@ def step_type(self, type_class: type[Agent], shuffle_agents: bool = True) -> Non Args: type_class: Class object of the type to run. """ - agent_keys: Iterable[int] = self.agents_by_type[type_class].keys() + agent_keys: list[int] = list(self.agents_by_type[type_class].keys()) if shuffle_agents: - agent_keys = list(agent_keys) self.model.random.shuffle(agent_keys) for agent_key in agent_keys: - self.agents_by_type[type_class][agent_key].step() + if agent_key in self.agents_by_type[type_class]: + self.agents_by_type[type_class][agent_key].step() def get_type_count(self, type_class: type[Agent]) -> int: """ diff --git a/mesa/visualization/ModularVisualization.py b/mesa/visualization/ModularVisualization.py index 0b49b3f6931..dbdbf3b3f9f 100644 --- a/mesa/visualization/ModularVisualization.py +++ b/mesa/visualization/ModularVisualization.py @@ -1,437 +1 @@ -""" -ModularServer -============= - -A visualization server which renders a model via one or more elements. - -The concept for the modular visualization server as follows: -A visualization is composed of VisualizationElements, each of which defines how -to generate some visualization from a model instance and render it on the -client. VisualizationElements may be anything from a simple text display to -a multilayered HTML5 canvas. - -The actual server is launched with one or more VisualizationElements; -it runs the model object through each of them, generating data to be sent to -the client. The client page is also generated based on the JavaScript code -provided by each element. - -This file consists of the following classes: - -VisualizationElement: Parent class for all other visualization elements, with - the minimal necessary options. -PageHandler: The handler for the visualization page, generated from a template - and built from the various visualization elements. -SocketHandler: Handles the websocket connection between the client page and - the server. -ModularServer: The overall visualization application class which stores and - controls the model and visualization instance. - - -ModularServer should *not* need to be subclassed on a model-by-model basis; it -should be primarily a pass-through for VisualizationElement subclasses, which -define the actual visualization specifics. - -For example, suppose we have created two visualization elements for our model, -called canvasvis and graphvis; we would launch a server with: - - server = ModularServer(MyModel, [canvasvis, graphvis], name="My Model") - server.launch() - -The client keeps track of what step it is showing. Clicking the Step button in -the browser sends a message requesting the viz_state corresponding to the next -step position, which is then sent back to the client via the websocket. - -The websocket protocol is as follows: -Each message is a JSON object, with a "type" property which defines the rest of -the structure. - -Server -> Client: - Send over the model state to visualize. - Model state is a list, with each element corresponding to a div; each div - is expected to have a render function associated with it, which knows how - to render that particular data. The example below includes two elements: - the first is data for a CanvasGrid, the second for a raw text display. - - { - "type": "viz_state", - "data": [{0:[ {"Shape": "circle", "x": 0, "y": 0, "r": 0.5, - "Color": "#AAAAAA", "Filled": "true", "Layer": 0, - "text": 'A', "text_color": "white" }]}, - "Shape Count: 1"] - } - - Informs the client that the model is over. - {"type": "end"} - - Informs the client of the current model's parameters - { - "type": "model_params", - "params": 'dict' of model params, (i.e. {arg_1: val_1, ...}) - } - -Client -> Server: - Reset the model. - TODO: Allow this to come with parameters - { - "type": "reset" - } - - Get a given state. - { - "type": "get_step", - "step:" index of the step to get. - } - - Submit model parameter updates - { - "type": "submit_params", - "param": name of model parameter - "value": new value for 'param' - } - - Get the model's parameters - { - "type": "get_params" - } - -""" -import asyncio -import os -import platform -import tornado.autoreload -import tornado.ioloop -import tornado.web -import tornado.websocket -import tornado.escape -import tornado.gen -import webbrowser - -from mesa.visualization.UserParam import UserSettableParameter, UserParam - -# Suppress several pylint warnings for this file. -# Attributes being defined outside of init is a Tornado feature. -# pylint: disable=attribute-defined-outside-init - -# Change the event loop policy for windows -if platform.system() == "Windows" and platform.python_version_tuple() >= ("3", "7"): - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - -D3_JS_FILE = "external/d3-7.4.3.min.js" -CHART_JS_FILE = "external/chart-3.6.1.min.js" - - -def is_user_param(val): - return isinstance(val, UserSettableParameter) or issubclass( - val.__class__, UserParam - ) - - -class VisualizationElement: - """ - Defines an element of the visualization. - - Attributes: - package_includes: A list of external JavaScript and CSS files to - include that are part of the Mesa packages. - local_includes: A list of JavaScript and CSS files that are local to - the directory that the server is being run in. - js_code: A JavaScript code string to instantiate the element. - local_dir: A full path to the directory containing the local includes. - If a relative path is given, it is relative to the working - directory where the server is being run. If an absolute path - is given, it is used as-is. Default is the current working - directory. - - Methods: - render: Takes a model object, and produces JSON data which can be sent - to the client. - - """ - - package_includes = [] - local_includes = [] - js_code = "" - render_args = {} - local_dir = "" - - def __init__(self): - pass - - def render(self, model): - """Build visualization data from a model object. - - Args: - model: A model object - - Returns: - A JSON-ready object. - - """ - return "VisualizationElement goes here." - - -class TextElement(VisualizationElement): - """ - Module for drawing live-updating text. - """ - - package_includes = ["TextModule.js"] - js_code = "elements.push(new TextModule());" - - -# ============================================================================= -# Actual Tornado code starts here: - - -class PageHandler(tornado.web.RequestHandler): - """Handler for the HTML template which holds the visualization.""" - - def get(self): - elements = self.application.visualization_elements - for i, element in enumerate(elements): - element.index = i - self.render( - "modular_template.html", - port=self.application.port, - model_name=self.application.model_name, - description=self.application.description, - package_js_includes=self.application.package_js_includes, - package_css_includes=self.application.package_css_includes, - local_js_includes=self.application.local_js_includes, - local_css_includes=self.application.local_css_includes, - scripts=self.application.js_code, - ) - - -class SocketHandler(tornado.websocket.WebSocketHandler): - """Handler for websocket.""" - - def open(self): - if self.application.verbose: - print("Socket opened!") - self.write_message( - {"type": "model_params", "params": self.application.user_params} - ) - - def check_origin(self, origin): - return True - - @property - def viz_state_message(self): - return {"type": "viz_state", "data": self.application.render_model()} - - def on_message(self, message): - """Receiving a message from the websocket, parse, and act accordingly.""" - if self.application.verbose: - print(message) - msg = tornado.escape.json_decode(message) - - if msg["type"] == "get_step": - if not self.application.model.running: - self.write_message({"type": "end"}) - else: - self.application.model.step() - self.write_message(self.viz_state_message) - - elif msg["type"] == "reset": - self.application.reset_model() - self.write_message(self.viz_state_message) - - elif msg["type"] == "submit_params": - param = msg["param"] - value = msg["value"] - - # Is the param editable? - if param in self.application.user_params: - if is_user_param(self.application.model_kwargs[param]): - self.application.model_kwargs[param].value = value - else: - self.application.model_kwargs[param] = value - - else: - if self.application.verbose: - print("Unexpected message!") - - -class ModularServer(tornado.web.Application): - """Main visualization application.""" - - EXCLUDE_LIST = ("width", "height") - - def __init__( - self, - model_cls, - visualization_elements, - name="Mesa Model", - model_params=None, - port=None, - ): - """ - Args: - model_cls: Mesa model class - visualization_elements: visualisation elements - name: A String for the model name - port: Port the webserver listens to (int) - Order of configuration: - 1. Parameter to ModularServer.launch - 2. Parameter to ModularServer() - 3. Environment var PORT - 4. Default value (8521) - model_params: A dict of model parameters - """ - - self.verbose = True - self.max_steps = 100000 - - if port is not None: - self.port = port - else: - # Default port to listen on - self.port = int(os.getenv("PORT", 8521)) - - # Handlers and other globals: - page_handler = (r"/", PageHandler) - socket_handler = (r"/ws", SocketHandler) - static_handler = ( - r"/static/(.*)", - tornado.web.StaticFileHandler, - {"path": os.path.dirname(__file__) + "/templates"}, - ) - custom_handler = ( - r"/local/custom/(.*)", - tornado.web.StaticFileHandler, - {"path": ""}, - ) - self.handlers = [page_handler, socket_handler, static_handler, custom_handler] - - self.settings = { - "debug": True, - "autoreload": False, - "template_path": os.path.dirname(__file__) + "/templates", - } - - """Create a new visualization server with the given elements.""" - if model_params is None: - model_params = {} - # Prep visualization elements: - self.visualization_elements = self._auto_convert_functions_to_TextElements( - visualization_elements - ) - self.package_js_includes = set() - self.package_css_includes = set() - self.local_js_includes = set() - self.local_css_includes = set() - self.js_code = [] - for element in self.visualization_elements: - for include_file in element.package_includes: - if self._is_stylesheet(include_file): - self.package_css_includes.add(include_file) - else: - self.package_js_includes.add(include_file) - if element.local_includes: - mapped_local_dir = element.__class__.__name__ - element_file_handler = ( - rf"/local/{mapped_local_dir}/(.*)", - tornado.web.StaticFileHandler, - {"path": element.local_dir}, - ) - self.handlers.append(element_file_handler) - for include_file in element.local_includes: - include_file_path = f"{mapped_local_dir}/{include_file}" - if self._is_stylesheet(include_file): - self.local_css_includes.add(include_file_path) - else: - self.local_js_includes.add(include_file_path) - self.js_code.append(element.js_code) - - # Initializing the model - self.model_name = name - self.model_cls = model_cls - self.description = "No description available" - if hasattr(model_cls, "description"): - self.description = model_cls.description - elif model_cls.__doc__ is not None: - self.description = model_cls.__doc__ - - self.model_kwargs = model_params - self.reset_model() - - # Initializing the application itself: - super().__init__(self.handlers, **self.settings) - - @property - def user_params(self): - result = {} - for param, val in self.model_kwargs.items(): - if is_user_param(val): - result[param] = val.json - - return result - - def reset_model(self): - """Reinstantiate the model object, using the current parameters.""" - - model_params = {} - for key, val in self.model_kwargs.items(): - if is_user_param(val): - if val.param_type == "static_text": - # static_text is never used for setting params - continue - model_params[key] = val.value - else: - model_params[key] = val - - self.model = self.model_cls(**model_params) - # We specify the `running` attribute here so that the user doesn't have - # to define it explicitly in their model's __init__. - self.model.running = True - - def render_model(self): - """Turn the current state of the model into a dictionary of - visualizations - - """ - visualization_state = [] - for element in self.visualization_elements: - element_state = element.render(self.model) - visualization_state.append(element_state) - return visualization_state - - def launch(self, port=None, open_browser=True): - """Run the app.""" - if port is not None: - self.port = port - url = f"http://127.0.0.1:{self.port}" - print(f"Interface starting at {url}") - self.listen(self.port) - if open_browser: - webbrowser.open(url) - tornado.autoreload.start() - tornado.ioloop.IOLoop.current().start() - - @staticmethod - def _is_stylesheet(filename): - return filename.lower().endswith(".css") - - def _auto_convert_fn_to_TextElement(self, x): - """ - Automatically convert a function to a TextElement object. - See https://github.com/projectmesa/mesa/issues/1233. - """ - - # Note: a class constructor is also a callable. - if not callable(x): - # i.e. not a function - return x - - class MyTextElement(TextElement): - def render(self, model): - return x(model) - - return MyTextElement() - - def _auto_convert_functions_to_TextElements(self, visualization_elements): - out_elements = [ - self._auto_convert_fn_to_TextElement(e) for e in visualization_elements - ] - return out_elements +from mesa_viz_tornado.ModularVisualization import * # noqa diff --git a/mesa/visualization/TextVisualization.py b/mesa/visualization/TextVisualization.py index 548ceb6d51b..f41c90ea595 100644 --- a/mesa/visualization/TextVisualization.py +++ b/mesa/visualization/TextVisualization.py @@ -1,128 +1 @@ -""" -Text Visualization -================== - -Base classes for ASCII-only visualizations of a model. -These are useful for quick debugging, and can readily be rendered in an IPython -Notebook or via text alone in a browser window. - -Classes: - -TextVisualization: Class meant to wrap around a Model object and render it -in some way using Elements, which are stored in a list and rendered in that -order. Each element, in turn, renders a particular piece of information as -text. - -ASCIIElement: Parent class for all other ASCII elements. render() returns its -representative string, which can be printed via the overloaded __str__ method. - -TextData: Uses getattr to get the value of a particular property of a model -and prints it, along with its name. - -TextGrid: Prints a grid, assuming that the value of each cell maps to exactly -one ASCII character via a converter method. This (as opposed to a dictionary) -is used so as to allow the method to access Agent internals, as well as to -potentially render a cell based on several values (e.g. an Agent grid and a -Patch value grid). - -""" -# Pylint instructions: allow single-character variable names. -# pylint: disable=invalid-name - - -class TextVisualization: - """ASCII-Only visualization of a model. - - Properties: - - model: The underlying model object to be visualized. - elements: List of visualization elements, which will be rendered - in the order they are added. - - """ - - def __init__(self, model): - """Create a new Text Visualization object.""" - self.model = model - self.elements = [] - - def render(self): - """Render all the text elements, in order.""" - for element in self.elements: - print(element) - - def step(self): - """Advance the model by a step and print the results.""" - self.model.step() - self.render() - - -class ASCIIElement: - """Base class for all TextElements to render. - - Methods: - render: 'Renders' some data into ASCII and returns. - __str__: Displays render() by default. - """ - - def __init__(self): - pass - - def render(self): - """Render the element as text.""" - return "Placeholder!" - - def __str__(self): - return self.render() - - -class TextData(ASCIIElement): - """Prints the value of one particular variable from the base model.""" - - def __init__(self, model, var_name): - """Create a new data renderer.""" - self.model = model - self.var_name = var_name - - def render(self): - return self.var_name + ": " + str(getattr(self.model, self.var_name)) - - -class TextGrid(ASCIIElement): - """Class for creating an ASCII visualization of a basic grid object. - - By default, assume that each cell is represented by one character, and - that empty cells are rendered as ' ' characters. When printed, the TextGrid - results in a width x height grid of ascii characters. - - Properties: - grid: The underlying grid object. - - """ - - grid = None - - def __init__(self, grid, converter): - """Create a new ASCII grid visualization. - - Args: - grid: The underlying Grid object. - converter: function for converting the content of each cell - to ascii. Takes the contents of a cell, and returns - a single character. - """ - self.grid = grid - self.converter = converter - - def render(self): - """What to show when printed.""" - viz = "" - for y in range(self.grid.height): - for x in range(self.grid.width): - c = self.grid[y][x] - if c is None: - viz += " " - else: - viz += self.converter(c) - viz += "\n" - return viz +from mesa_viz_tornado.TextVisualization import * # noqa diff --git a/mesa/visualization/UserParam.py b/mesa/visualization/UserParam.py index 0a1c6ac7560..d41362389f4 100644 --- a/mesa/visualization/UserParam.py +++ b/mesa/visualization/UserParam.py @@ -1,297 +1 @@ -from warnings import warn -import numbers - - -NUMBER = "number" -CHECKBOX = "checkbox" -CHOICE = "choice" -SLIDER = "slider" -STATIC_TEXT = "static_text" - - -class UserSettableParameter: - """A class for providing options to a visualization for a given parameter. - - UserSettableParameter can be used instead of keyword arguments when specifying model parameters in an - instance of a `ModularServer` so that the parameter can be adjusted in the UI without restarting the server. - - Validation of correctly-specified params happens on startup of a `ModularServer`. Each param is handled - individually in the UI and sends callback events to the server when an option is updated. That option is then - re-validated, in the `value.setter` property method to ensure input is correct from the UI to `reset_model` - callback. - - Parameter types include: - - 'number' - a simple numerical input - - 'checkbox' - boolean checkbox - - 'choice' - String-based dropdown input, for selecting choices within a model - - 'slider' - A number-based slider input with settable increment - - 'static_text' - A non-input textbox for displaying model info. - - Examples: - - # Simple number input - number_option = UserSettableParameter('number', 'My Number', value=123) - - # Checkbox input - boolean_option = UserSettableParameter('checkbox', 'My Boolean', value=True) - - # Choice input - choice_option = UserSettableParameter('choice', 'My Choice', value='Default choice', - choices=['Default Choice', 'Alternate Choice']) - - # Slider input - slider_option = UserSettableParameter('slider', 'My Slider', value=123, min_value=10, max_value=200, step=0.1) - - # Static text - static_text = UserSettableParameter('static_text', value="This is a descriptive textbox") - """ - - NUMBER = NUMBER - CHECKBOX = CHECKBOX - CHOICE = CHOICE - SLIDER = SLIDER - STATIC_TEXT = STATIC_TEXT - - TYPES = (NUMBER, CHECKBOX, CHOICE, SLIDER, STATIC_TEXT) - - _ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'" - - def __init__( - self, - param_type=None, - name="", - value=None, - min_value=None, - max_value=None, - step=1, - choices=None, - description=None, - ): - - warn( - "UserSettableParameter is deprecated in favor of UserParam objects " - "such as Slider, Checkbox, Choice, StaticText, NumberInput. " - "See the examples folder for how to use them. " - "UserSettableParameter will be removed in the next major release." - ) - if choices is None: - choices = list() - if param_type not in self.TYPES: - raise ValueError(f"{param_type} is not a valid Option type") - self.param_type = param_type - self.name = name - self._value = value - self.min_value = min_value - self.max_value = max_value - self.step = step - self.choices = choices - self.description = description - - # Validate option types to make sure values are supplied properly - msg = self._ERROR_MESSAGE.format(self.param_type, name) - valid = True - - if self.param_type == self.NUMBER: - valid = not (self.value is None) - - elif self.param_type == self.SLIDER: - valid = not ( - self.value is None or self.min_value is None or self.max_value is None - ) - - elif self.param_type == self.CHOICE: - valid = not (self.value is None or len(self.choices) == 0) - - elif self.param_type == self.CHECKBOX: - valid = isinstance(self.value, bool) - - elif self.param_type == self.STATIC_TEXT: - valid = isinstance(self.value, str) - - if not valid: - raise ValueError(msg) - - @property - def value(self): - return self._value - - @value.setter - def value(self, value): - self._value = value - if self.param_type == self.SLIDER: - if self._value < self.min_value: - self._value = self.min_value - elif self._value > self.max_value: - self._value = self.max_value - elif self.param_type == self.CHOICE: - if self._value not in self.choices: - print( - "Selected choice value not in available choices, selected first choice from 'choices' list" - ) - self._value = self.choices[0] - - @property - def json(self): - result = self.__dict__.copy() - result["value"] = result.pop( - "_value" - ) # Return _value as value, value is the same - return result - - -class UserParam: - _ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'" - - @property - def json(self): - result = self.__dict__.copy() - result["value"] = result.pop( - "_value" - ) # Return _value as value, value is the same - return result - - def maybe_raise_error(self, valid): - if not valid: - msg = self._ERROR_MESSAGE.format(self.param_type, self.name) - raise ValueError(msg) - - @property - def value(self): - return self._value - - @value.setter - def value(self, value): - self._value = value - - -class Slider(UserParam): - """ - A number-based slider input with settable increment. - - Example: - - slider_option = Slider("My Slider", value=123, min_value=10, max_value=200, step=0.1) - """ - - def __init__( - self, - name="", - value=None, - min_value=None, - max_value=None, - step=1, - description=None, - ): - self.param_type = SLIDER - self.name = name - self._value = value - self.min_value = min_value - self.max_value = max_value - self.step = step - self.description = description - - # Validate option type to make sure values are supplied properly - valid = not ( - self.value is None or self.min_value is None or self.max_value is None - ) - self.maybe_raise_error(valid) - - @property - def value(self): - return self._value - - @value.setter - def value(self, value): - self._value = value - if self._value < self.min_value: - self._value = self.min_value - elif self._value > self.max_value: - self._value = self.max_value - - -class Checkbox(UserParam): - """ - Boolean checkbox. - - Example: - - boolean_option = Checkbox('My Boolean', True) - """ - - def __init__(self, name="", value=None, description=None): - self.param_type = CHECKBOX - self.name = name - self._value = value - self.description = description - - # Validate option type to make sure values are supplied properly - valid = isinstance(self.value, bool) - self.maybe_raise_error(valid) - - -class Choice(UserParam): - """ - String-based dropdown input, for selecting choices within a model - - Example: - choice_option = Choice( - 'My Choice', - value='Default choice', - choices=['Default Choice', 'Alternate Choice'] - ) - """ - - def __init__(self, name="", value=None, choices=None, description=None): - self.param_type = CHOICE - self.name = name - self._value = value - self.choices = choices - self.description = description - - # Validate option type to make sure values are supplied properly - valid = not (self.value is None or len(self.choices) == 0) - self.maybe_raise_error(valid) - - @property - def value(self): - return self._value - - @value.setter - def value(self, value): - self._value = value - if self._value not in self.choices: - print( - "Selected choice value not in available choices, selected first choice from 'choices' list" - ) - self._value = self.choices[0] - - -class StaticText(UserParam): - """ - A non-input textbox for displaying model info. - - Example: - static_text = StaticText("This is a descriptive textbox") - """ - - def __init__(self, value=None): - self.param_type = STATIC_TEXT - self._value = value - valid = isinstance(self.value, str) - self.maybe_raise_error(valid) - - -class NumberInput(UserParam): - """ - a simple numerical input - - Example: - number_option = NumberInput("My Number", value=123) - """ - - def __init__(self, name="", value=None, description=None): - self.param_type = NUMBER - self.name = name - self._value = value - valid = isinstance(self.value, numbers.Number) - self.maybe_raise_error(valid) +from mesa_viz_tornado.UserParam import * # noqa diff --git a/mesa/visualization/__init__.py b/mesa/visualization/__init__.py index ae62921653c..754dca50064 100644 --- a/mesa/visualization/__init__.py +++ b/mesa/visualization/__init__.py @@ -1,10 +1,4 @@ -""" -Mesa Visualization Module -------------------------- - -TextVisualization: Base class for writing ASCII visualizations of model state. - -TextServer: Class which takes a TextVisualization child class as an input, and -renders it in-browser, along with an interface. - -""" +from mesa_viz_tornado.ModularVisualization import * # noqa +from mesa_viz_tornado.modules import * # noqa +from mesa_viz_tornado.UserParam import * # noqa +from mesa_viz_tornado.TextVisualization import * # noqa diff --git a/mesa/visualization/modules.py b/mesa/visualization/modules.py new file mode 100644 index 00000000000..40fc193a522 --- /dev/null +++ b/mesa/visualization/modules.py @@ -0,0 +1 @@ +from mesa_viz_tornado.modules import * # noqa diff --git a/mesa/visualization/modules/BarChartVisualization.py b/mesa/visualization/modules/BarChartVisualization.py deleted file mode 100644 index afe1558bca1..00000000000 --- a/mesa/visualization/modules/BarChartVisualization.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -Pie Chart Module -============ - -Module for drawing live-updating bar charts using d3.js - -""" -import json -from mesa.visualization.ModularVisualization import VisualizationElement, D3_JS_FILE - - -class BarChartModule(VisualizationElement): - """Each bar chart can either visualize model-level or agent-level fields from a datcollector - with a bar chart. - - Attributes: - scope: whether to visualize agent-level or model-level fields - fields: A List of Dictionaries containing information about each field to be charted, - including the name of the datacollector field and the desired color of the - corresponding bar. - Ex: [{"Label":"", "Color":""}] - sorting: Whether to sort ascending, descending, or neither when charting agent fields - sort_by: The agent field to sort by - canvas_height, canvas_width: The width and height to draw the chart on the page, in pixels. - Default to 800 x 400 - data_collector_name: Name of the DataCollector object in the model to retrieve data from. - - """ - - package_includes = [D3_JS_FILE, "BarChartModule.js"] - - def __init__( - self, - fields, - scope="model", - sorting="none", - sort_by="none", - canvas_height=400, - canvas_width=800, - data_collector_name="datacollector", - ): - """ - Create a new bar chart visualization. - - Args: - scope: "model" if visualizing model-level fields, "agent" if visualizing agent-level - fields. - fields: A List of Dictionaries containing information about each field to be charted, - including the name of the datacollector field and the desired color of the - corresponding bar. - Ex: [{"Label":"", "Color":""}] - sorting: "ascending", "descending", or "none" - sort_by: The agent field to sort by - canvas_height, canvas_width: Size in pixels of the chart to draw. - data_collector_name: Name of the DataCollector to use. - """ - - self.scope = scope - self.fields = fields - self.sorting = sorting - self.canvas_height = canvas_height - self.canvas_width = canvas_width - self.data_collector_name = data_collector_name - - fields_json = json.dumps(self.fields) - new_element = "new BarChartModule({}, {}, {}, '{}', '{}')" - new_element = new_element.format( - fields_json, canvas_width, canvas_height, sorting, sort_by - ) - self.js_code = "elements.push(" + new_element + ")" - - def render(self, model): - current_values = [] - data_collector = getattr(model, self.data_collector_name) - - if self.scope == "agent": - df = data_collector.get_agent_vars_dataframe().astype("float") - latest_step = df.index.levels[0][-1] - labelStrings = [f["Label"] for f in self.fields] - dict = df.loc[latest_step].T.loc[labelStrings].to_dict() - current_values = list(dict.values()) - - elif self.scope == "model": - outDict = {} - for s in self.fields: - name = s["Label"] - try: - val = data_collector.model_vars[name][-1] - except (IndexError, KeyError): - val = 0 - outDict[name] = val - current_values.append(outDict) - else: - raise ValueError("scope must be 'agent' or 'model'") - return current_values diff --git a/mesa/visualization/modules/CanvasGridVisualization.py b/mesa/visualization/modules/CanvasGridVisualization.py deleted file mode 100644 index 49e85196135..00000000000 --- a/mesa/visualization/modules/CanvasGridVisualization.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -Modular Canvas Rendering -======================== - -Module for visualizing model objects in grid cells. - -""" -from collections import defaultdict -from mesa.visualization.ModularVisualization import VisualizationElement - - -class CanvasGrid(VisualizationElement): - """A CanvasGrid object uses a user-provided portrayal method to generate a - portrayal for each object. A portrayal is a JSON-ready dictionary which - tells the relevant JavaScript code (GridDraw.js) where to draw what shape. - - The render method returns a dictionary, keyed on layers, with values as - lists of portrayals to draw. Portrayals themselves are generated by the - user-provided portrayal_method, which accepts an object as an input and - produces a portrayal of it. - - A portrayal as a dictionary with the following structure: - "x", "y": Coordinates for the cell in which the object is placed. - "Shape": Can be either "circle", "rect", "arrowHead" or a custom image. - For Circles: - "r": The radius, defined as a fraction of cell size. r=1 will - fill the entire cell. - "xAlign", "yAlign": Alignment of the circle within the cell. - Defaults to 0.5 (center). - For Rectangles: - "w", "h": The width and height of the rectangle, which are in - fractions of cell width and height. - "xAlign", "yAlign": Alignment of the rectangle within the - cell. Defaults to 0.5 (center). - For arrowHead: - "scale": Proportion scaling as a fraction of cell size. - "heading_x": represents x direction unit vector. - "heading_y": represents y direction unit vector. - For an image: - The image must be placed in the same directory from which the - server is launched. An image has the attributes "x", "y", - "scale", "text" and "text_color". - "Color": The color to draw the shape in; needs to be a valid HTML - color, e.g."Red" or "#AA08F8" - "Filled": either "true" or "false", and determines whether the shape is - filled or not. - "Layer": Layer number of 0 or above; higher-numbered layers are drawn - above lower-numbered layers. - "text": The text to be inscribed inside the Shape. Normally useful for - showing the unique_id of the agent. - "text_color": The color to draw the inscribed text. Should be given in - conjunction of "text" property. - - - Attributes: - portrayal_method: Function which generates portrayals from objects, as - described above. - grid_height, grid_width: Size of the grid to visualize, in cells. - canvas_height, canvas_width: Size, in pixels, of the grid visualization - to draw on the client. - template: "canvas_module.html" stores the module's HTML template. - - """ - - package_includes = ["GridDraw.js", "CanvasModule.js", "InteractionHandler.js"] - - def __init__( - self, - portrayal_method, - grid_width, - grid_height, - canvas_width=500, - canvas_height=500, - ): - """Instantiate a new CanvasGrid. - - Args: - portrayal_method: function to convert each object on the grid to - a portrayal, as described above. - grid_width, grid_height: Size of the grid, in cells. - canvas_height, canvas_width: Size of the canvas to draw in the - client, in pixels. (default: 500x500) - - """ - self.portrayal_method = portrayal_method - self.grid_width = grid_width - self.grid_height = grid_height - self.canvas_width = canvas_width - self.canvas_height = canvas_height - - new_element = "new CanvasModule({}, {}, {}, {})".format( - self.canvas_width, self.canvas_height, self.grid_width, self.grid_height - ) - - self.js_code = "elements.push(" + new_element + ");" - - def render(self, model): - grid_state = defaultdict(list) - for x in range(model.grid.width): - for y in range(model.grid.height): - cell_objects = model.grid.get_cell_list_contents([(x, y)]) - for obj in cell_objects: - portrayal = self.portrayal_method(obj) - if portrayal: - portrayal["x"] = x - portrayal["y"] = y - grid_state[portrayal["Layer"]].append(portrayal) - - return grid_state diff --git a/mesa/visualization/modules/ChartVisualization.py b/mesa/visualization/modules/ChartVisualization.py deleted file mode 100644 index 8fc51a3450e..00000000000 --- a/mesa/visualization/modules/ChartVisualization.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -Chart Module -============ - -Module for drawing live-updating line charts using Charts.js - -""" -import json -from mesa.visualization.ModularVisualization import VisualizationElement, CHART_JS_FILE - - -class ChartModule(VisualizationElement): - """Each chart can visualize one or more model-level series as lines - with the data value on the Y axis and the step number as the X axis. - - At the moment, each call to the render method returns a list of the most - recent values of each series. - - Attributes: - series: A list of dictionaries containing information on series to - plot. Each dictionary must contain (at least) the "Label" and - "Color" keys. The "Label" value must correspond to a - model-level series collected by the model's DataCollector, and - "Color" must have a valid HTML color. - canvas_height, canvas_width: The width and height to draw the chart on - the page, in pixels. Default to 200 x 500 - data_collector_name: Name of the DataCollector object in the model to - retrieve data from. - template: "chart_module.html" stores the HTML template for the module. - - - Example: - schelling_chart = ChartModule([{"Label": "happy", "Color": "Black"}], - data_collector_name="datacollector") - - TODO: - Have it be able to handle agent-level variables as well. - - More Pythonic customization; in particular, have both series-level and - chart-level options settable in Python, and passed to the front-end - the same way that "Color" is currently. - - """ - - package_includes = [CHART_JS_FILE, "ChartModule.js"] - - def __init__( - self, - series, - canvas_height=200, - canvas_width=500, - data_collector_name="datacollector", - ): - """ - Create a new line chart visualization. - - Args: - series: A list of dictionaries containing series names and - HTML colors to chart them in, e.g. - [{"Label": "happy", "Color": "Black"},] - canvas_height, canvas_width: Size in pixels of the chart to draw. - data_collector_name: Name of the DataCollector to use. - """ - - self.series = series - self.canvas_height = canvas_height - self.canvas_width = canvas_width - self.data_collector_name = data_collector_name - - series_json = json.dumps(self.series) - new_element = "new ChartModule({}, {}, {})" - new_element = new_element.format(series_json, canvas_width, canvas_height) - self.js_code = "elements.push(" + new_element + ");" - - def render(self, model): - current_values = [] - data_collector = getattr(model, self.data_collector_name) - - for s in self.series: - name = s["Label"] - try: - val = data_collector.model_vars[name][-1] # Latest value - except (IndexError, KeyError): - val = 0 - current_values.append(val) - return current_values diff --git a/mesa/visualization/modules/HexGridVisualization.py b/mesa/visualization/modules/HexGridVisualization.py deleted file mode 100644 index ebe2781d798..00000000000 --- a/mesa/visualization/modules/HexGridVisualization.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Modular Canvas Rendering -======================== - -Module for visualizing model objects in hexagonal grid cells. - -""" -from collections import defaultdict -from mesa.visualization.ModularVisualization import VisualizationElement - - -class CanvasHexGrid(VisualizationElement): - """A CanvasHexGrid object functions similarly to a CanvasGrid object. It takes a portrayal dictionary and talks to HexDraw.js to draw that shape. - - A portrayal as a dictionary with the following structure: - "x", "y": Coordinates for the cell in which the object is placed. - "Shape": Can be either "hex" or "circle" - "r": The radius, defined as a fraction of cell size. r=1 will - fill the entire cell. - "Color": The color to draw the shape in; needs to be a valid HTML - color, e.g."Red" or "#AA08F8" - "Filled": either "true" or "false", and determines whether the shape is - filled or not. - "Layer": Layer number of 0 or above; higher-numbered layers are drawn - above lower-numbered layers. - "text": The text to be inscribed inside the Shape. Normally useful for - showing the unique_id of the agent. - "text_color": The color to draw the inscribed text. Should be given in - conjunction of "text" property. - - - Attributes: - portrayal_method: Function which generates portrayals from objects, as - described above. - grid_height, grid_width: Size of the grid to visualize, in cells. - canvas_height, canvas_width: Size, in pixels, of the grid visualization - to draw on the client. - template: "canvas_module.html" stores the module's HTML template. - - """ - - package_includes = ["HexDraw.js", "CanvasHexModule.js", "InteractionHandler.js"] - portrayal_method = None # Portrayal function - canvas_width = 500 - canvas_height = 500 - - def __init__( - self, - portrayal_method, - grid_width, - grid_height, - canvas_width=500, - canvas_height=500, - ): - """Instantiate a new CanvasGrid. - - Args: - portrayal_method: function to convert each object on the grid to - a portrayal, as described above. - grid_width, grid_height: Size of the grid, in cells. - canvas_height, canvas_width: Size of the canvas to draw in the - client, in pixels. (default: 500x500) - - """ - self.portrayal_method = portrayal_method - self.grid_width = grid_width - self.grid_height = grid_height - self.canvas_width = canvas_width - self.canvas_height = canvas_height - - new_element = "new CanvasHexModule({}, {}, {}, {})".format( - self.canvas_width, self.canvas_height, self.grid_width, self.grid_height - ) - - self.js_code = "elements.push(" + new_element + ");" - - def render(self, model): - grid_state = defaultdict(list) - for x in range(model.grid.width): - for y in range(model.grid.height): - cell_objects = model.grid.get_cell_list_contents([(x, y)]) - for obj in cell_objects: - portrayal = self.portrayal_method(obj) - if portrayal: - portrayal["x"] = x - portrayal["y"] = y - grid_state[portrayal["Layer"]].append(portrayal) - - return grid_state diff --git a/mesa/visualization/modules/NetworkVisualization.py b/mesa/visualization/modules/NetworkVisualization.py deleted file mode 100644 index a50e92090f7..00000000000 --- a/mesa/visualization/modules/NetworkVisualization.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Network Visualization Module -============ - -Module for rendering the network, using [d3.js](https://d3js.org/) framework. - -""" -from mesa.visualization.ModularVisualization import VisualizationElement, D3_JS_FILE - - -class NetworkModule(VisualizationElement): - package_includes = [] - - def __init__( - self, - portrayal_method, - canvas_height=500, - canvas_width=500, - ): - NetworkModule.package_includes = ["NetworkModule_d3.js", D3_JS_FILE] - - self.portrayal_method = portrayal_method - self.canvas_height = canvas_height - self.canvas_width = canvas_width - new_element = f"new NetworkModule({self.canvas_width}, {self.canvas_height})" - self.js_code = "elements.push(" + new_element + ");" - - def render(self, model): - return self.portrayal_method(model.G) diff --git a/mesa/visualization/modules/PieChartVisualization.py b/mesa/visualization/modules/PieChartVisualization.py deleted file mode 100644 index 470e38fc9f7..00000000000 --- a/mesa/visualization/modules/PieChartVisualization.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -Pie Chart Module -============ - -Module for drawing live-updating pie charts using d3.js - -""" -import json -from mesa.visualization.ModularVisualization import VisualizationElement, D3_JS_FILE - - -class PieChartModule(VisualizationElement): - """Each chart can visualize one set of fields from a datacollector as a - pie chart. - - - Attributes: - fields: A list of dictionaries containing information on fields to - plot. Each dictionary must contain (at least) the "Label" and - "Color" keys. The "Label" value must correspond to a - model-level field collected by the model's DataCollector, and - "Color" must have a valid HTML color. - canvas_height, canvas_width: The width and height to draw the chart on - the page, in pixels. Default to 500 x 500 - data_collector_name: Name of the DataCollector object in the model to - retrieve data from. - - - - """ - - package_includes = [D3_JS_FILE, "PieChartModule.js"] - - def __init__( - self, - fields, - canvas_height=500, - canvas_width=500, - data_collector_name="datacollector", - ): - """ - Create a new line chart visualization. - - Args: - fields: A list of dictionaries containing fields names and - HTML colors to chart them in, e.g. - [{"Label": "happy", "Color": "Black"},] - canvas_height, canvas_width: Size in pixels of the chart to draw. - data_collector_name: Name of the DataCollector to use. - """ - - self.fields = fields - self.canvas_height = canvas_height - self.canvas_width = canvas_width - self.data_collector_name = data_collector_name - - fields_json = json.dumps(self.fields) - new_element = "new PieChartModule({}, {}, {})" - new_element = new_element.format(fields_json, canvas_width, canvas_height) - self.js_code = "elements.push(" + new_element + ");" - - def render(self, model): - current_values = [] - data_collector = getattr(model, self.data_collector_name) - - for s in self.fields: - name = s["Label"] - try: - val = data_collector.model_vars[name][-1] # Latest value - except (IndexError, KeyError): - val = 0 - current_values.append(val) - return current_values diff --git a/mesa/visualization/modules/__init__.py b/mesa/visualization/modules/__init__.py deleted file mode 100644 index 8cc89e23f9f..00000000000 --- a/mesa/visualization/modules/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Container for all built-in visualization modules. -""" - -from mesa.visualization.modules.CanvasGridVisualization import CanvasGrid # noqa -from mesa.visualization.modules.ChartVisualization import ChartModule # noqa -from mesa.visualization.modules.PieChartVisualization import PieChartModule # noqa -from mesa.visualization.modules.BarChartVisualization import BarChartModule # noqa -from mesa.visualization.modules.HexGridVisualization import CanvasHexGrid # noqa -from mesa.visualization.modules.NetworkVisualization import NetworkModule # noqa - -# Delete this line in the next major release, once the simpler namespace has -# become widely adopted. -from mesa.visualization.ModularVisualization import TextElement # noqa diff --git a/mesa/visualization/templates/css/visualization.css b/mesa/visualization/templates/css/visualization.css deleted file mode 100644 index 9ae0af8a3b8..00000000000 --- a/mesa/visualization/templates/css/visualization.css +++ /dev/null @@ -1,20 +0,0 @@ -.model-parameter { - margin-bottom: 15px; -} - -/* This is specific to the Network visualization */ -div.d3tooltip { - position: absolute; - text-align: center; - padding: 1px; - font: 20px sans-serif; - background: lightsteelblue; - border: 3px; - border-radius: 8px; - pointer-events: none; -} - -canvas.world-grid { - position:absolute; - border:1px dotted -} diff --git a/mesa/visualization/templates/js/BarChartModule.js b/mesa/visualization/templates/js/BarChartModule.js deleted file mode 100644 index c5cb763ca46..00000000000 --- a/mesa/visualization/templates/js/BarChartModule.js +++ /dev/null @@ -1,182 +0,0 @@ -"use strict"; -// Note: This grouped bar chart is based off the example found here: -// https://bl.ocks.org/mbostock/3887051 -const BarChartModule = function ( - fields, - canvas_width, - canvas_height, - sorting, - sortingKey -) { - // Create the overall chart div - const chartDiv = document.createElement("div"); - chartDiv.className = "bar chart"; - chartDiv.setAttribute("width", canvas_width); - const elements = document.getElementById("elements"); - elements.appendChild(chartDiv); - - // Create the svg element: - const svg = d3.create("svg"); - svg - .attr("width", canvas_width) - .attr("height", canvas_height) - .style("border", "1px dotted"); - chartDiv.appendChild(svg.node()); - - //create the legend - const legend = d3.create("div"); - legend - .attr("class", "legend") - .attr( - "style", - `display:block;width:${canvas_width}px;text-align:center` - ); - - chartDiv.appendChild(legend.node()); - - legend - .selectAll("span") - .data(fields) - .enter() - .append("span") - .html(function (d) { - return ( - "" + - " " + - d["Label"].replace(" ", " ") - ); - }) - .attr("style", "padding-left:10px;padding-right:10px;"); - - // setup the d3 svg - const margin = { top: 20, right: 20, bottom: 30, left: 40 }; - const width = +svg.attr("width") - margin.left - margin.right; - const height = +svg.attr("height") - margin.top - margin.bottom; - const g = svg - .append("g") - .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); - - // Setup the bar chart - const x0 = d3.scaleBand().rangeRound([0, width]).paddingInner(0.1); - const x1 = d3.scaleBand().padding(0.05); - const y = d3.scaleLinear().rangeRound([height, 0]); - const colorScale = d3.scaleOrdinal(fields.map((field) => field["Color"])); - const keys = fields.map((f) => f["Label"]); - const chart = g.append("g"); - const axisBottom = g.append("g"); - const axisLeft = g.append("g"); - - axisBottom - .attr("class", "axis") - .attr("transform", "translate(0," + height + ")") - .call(d3.axisBottom(x0)); - - axisLeft.attr("class", "axis").call(d3.axisLeft(y).ticks(null, "s")); - - //Render step - this.render = function (data) { - //Axes - let minY = d3.min(data, function (d) { - return d3.min(keys, function (key) { - return d[key]; - }); - }); - if (minY > 0) { - minY = 0; - } - const maxY = d3.max(data, function (d) { - return d3.max(keys, function (key) { - return d[key]; - }); - }); - - x0.domain( - data.map(function (d, i) { - return i; - }) - ); - x1.domain(keys).rangeRound([0, x0.bandwidth()]); - y.domain([minY, maxY]).nice(); - - if (data.length > 1) { - axisBottom - .attr("transform", "translate(0," + y(0) + ")") - .call(d3.axisBottom(x0)); - } - - axisLeft.call(d3.axisLeft(y).ticks(null, "s")); - - //Sorting - if (sorting != "none") { - if (sorting == "ascending") { - data.sort((a, b) => b[sortingKey] - a[sortingKey]); - } else if (sorting == "descending") { - data.sort((a, b) => a[sortingKey] - b[sortingKey]); - } - } - - //Draw Chart - const rects = chart - .selectAll("g") - .data(data) - .enter() - .append("g") - .attr("transform", function (d, i) { - return "translate(" + x0(i) + ",0)"; - }) - .selectAll("rect"); - - rects - .data(function (d) { - return keys.map(function (key) { - return { key: key, value: d[key] }; - }); - }) - .enter() - .append("rect") - .attr("x", function (d) { - return x1(d.key); - }) - .attr("width", x1.bandwidth()) - .attr("fill", function (d) { - return colorScale(d.key); - }) - .attr("y", function (d) { - return Math.min(y(d.value), y(0)); - }) - .attr("height", function (d) { - return Math.abs(y(d.value) - y(0)); - }) - .append("title") - .text(function (d) { - return d.value; - }); - - //Update chart - chart - .selectAll("g") - .data(data) - .selectAll("rect") - .data(function (d) { - return keys.map(function (key) { - return { key: key, value: d[key] }; - }); - }) - .attr("y", function (d) { - return Math.min(y(d.value), y(0)); - }) - .attr("height", function (d) { - return Math.abs(y(d.value) - y(0)); - }) - .select("title") - .text(function (d) { - return d.value; - }); - }; - - this.reset = function () { - chart.selectAll("g").data([]).exit().remove(); - }; -}; diff --git a/mesa/visualization/templates/js/CanvasHexModule.js b/mesa/visualization/templates/js/CanvasHexModule.js deleted file mode 100644 index 0823462b1a6..00000000000 --- a/mesa/visualization/templates/js/CanvasHexModule.js +++ /dev/null @@ -1,69 +0,0 @@ -const CanvasHexModule = function ( - canvas_width, - canvas_height, - grid_width, - grid_height -) { - const createElement = (tagName, attrs) => { - const element = document.createElement(tagName); - Object.assign(element, attrs); - return element; - }; - - // Create the element - // ------------------ - const parent = createElement("div", { - style: `height:${canvas_height}px;`, - className: "world-grid-parent", - }); - - // Create the tag with absolute positioning : - const createCanvas = () => { - const el = createElement("canvas", { - width: canvas_width, - height: canvas_height, - className: "world-grid", - }); - return el; - }; - const canvas = createCanvas(); - const interaction_canvas = createCanvas(); - - // Append it to parent: - parent.appendChild(canvas); - parent.appendChild(interaction_canvas); - - // Append it to #elements - const elements = document.getElementById("elements"); - elements.appendChild(parent); - - // Create the context and the drawing controller: - const context = canvas.getContext("2d"); - - const interactionHandler = new InteractionHandler( - canvas_width, - canvas_height, - grid_width, - grid_height, - interaction_canvas.getContext("2d") - ); - - const canvasDraw = new HexVisualization( - canvas_width, - canvas_height, - grid_width, - grid_height, - context, - interactionHandler - ); - - this.render = (data) => { - canvasDraw.resetCanvas(); - for (const layer in data) canvasDraw.drawLayer(data[layer]); - canvasDraw.drawGridLines("#eee"); - }; - - this.reset = () => { - canvasDraw.resetCanvas(); - }; -}; diff --git a/mesa/visualization/templates/js/CanvasModule.js b/mesa/visualization/templates/js/CanvasModule.js deleted file mode 100644 index 69b9db7d9e2..00000000000 --- a/mesa/visualization/templates/js/CanvasModule.js +++ /dev/null @@ -1,70 +0,0 @@ -const CanvasModule = function ( - canvas_width, - canvas_height, - grid_width, - grid_height -) { - const createElement = (tagName, attrs) => { - const element = document.createElement(tagName); - Object.assign(element, attrs); - return element; - }; - - // Create the element - // ------------------ - // - const parent = createElement("div", { - style: `height:${canvas_height}px;`, - className: "world-grid-parent", - }); - - // Create the tag with absolute positioning : - const createCanvas = () => { - const el = createElement("canvas", { - width: canvas_width, - height: canvas_height, - className: "world-grid", - }); - return el; - }; - const canvas = createCanvas(); - const interaction_canvas = createCanvas(); - - // Append it to parent: - parent.appendChild(canvas); - parent.appendChild(interaction_canvas); - - // Append it to #elements - const elements = document.getElementById("elements"); - elements.appendChild(parent); - - // Create the context for the agents and interactions and the drawing controller: - const context = canvas.getContext("2d"); - - // Create an interaction handler using the - const interactionHandler = new InteractionHandler( - canvas_width, - canvas_height, - grid_width, - grid_height, - interaction_canvas.getContext("2d") - ); - const canvasDraw = new GridVisualization( - canvas_width, - canvas_height, - grid_width, - grid_height, - context, - interactionHandler - ); - - this.render = (data) => { - canvasDraw.resetCanvas(); - for (const layer in data) canvasDraw.drawLayer(data[layer]); - canvasDraw.drawGridLines("#eee"); - }; - - this.reset = () => { - canvasDraw.resetCanvas(); - }; -}; diff --git a/mesa/visualization/templates/js/ChartModule.js b/mesa/visualization/templates/js/ChartModule.js deleted file mode 100644 index 78680258c1e..00000000000 --- a/mesa/visualization/templates/js/ChartModule.js +++ /dev/null @@ -1,98 +0,0 @@ -const ChartModule = function (series, canvas_width, canvas_height) { - const canvas = document.createElement("canvas"); - Object.assign(canvas, { - width: canvas_width, - height: canvas_height, - style: "border:1px dotted", - }); - // Append it to #elements - const elements = document.getElementById("elements"); - elements.appendChild(canvas); - // Create the context and the drawing controller: - const context = canvas.getContext("2d"); - - const convertColorOpacity = (hex) => { - if (hex.indexOf("#") != 0) { - return "rgba(0,0,0,0.1)"; - } - - hex = hex.replace("#", ""); - const r = parseInt(hex.substring(0, 2), 16); - const g = parseInt(hex.substring(2, 4), 16); - const b = parseInt(hex.substring(4, 6), 16); - return `rgba(${r},${g},${b},0.1)`; - }; - - // Prep the chart properties and series: - const datasets = []; - for (const i in series) { - const s = series[i]; - const new_series = { - label: s.Label, - borderColor: s.Color, - backgroundColor: convertColorOpacity(s.Color), - data: [], - }; - datasets.push(new_series); - } - - const chartData = { - labels: [], - datasets: datasets, - }; - - const chartOptions = { - responsive: true, - tooltips: { - mode: "index", - intersect: false, - }, - hover: { - mode: "nearest", - intersect: true, - }, - scales: { - x: { - display: true, - title: { - display: true, - }, - ticks: { - maxTicksLimit: 11, - }, - }, - y: { - display: true, - title: { - display: true, - }, - }, - }, - }; - - const chart = new Chart(context, { - type: "line", - data: chartData, - options: chartOptions, - }); - - this.render = (data) => { - chart.data.labels.push(control.tick); - for (let i = 0; i < data.length; i++) { - chart.data.datasets[i].data.push(data[i]); - } - chart.update(); - }; - - this.reset = () => { - while (chart.data.labels.length) { - chart.data.labels.pop(); - } - chart.data.datasets.forEach((dataset) => { - while (dataset.data.length) { - dataset.data.pop(); - } - }); - chart.update(); - }; -}; diff --git a/mesa/visualization/templates/js/GridDraw.js b/mesa/visualization/templates/js/GridDraw.js deleted file mode 100644 index b24e529d0cc..00000000000 --- a/mesa/visualization/templates/js/GridDraw.js +++ /dev/null @@ -1,421 +0,0 @@ -/** -Mesa Canvas Grid Visualization -==================================================================== - -This is JavaScript code to visualize a Mesa Grid or MultiGrid state using the -HTML5 Canvas. Here's how it works: - -On the server side, the model developer will have assigned a portrayal to each -agent type. The visualization then loops through the grid, for each object adds -a JSON object to an inner list (keyed on layer) of lists to be sent to the -browser. - -Each JSON object to be drawn contains the following fields: Shape (currently -only rectanges and circles are supported), x, y, Color, Filled (boolean), -Layer; circles also get a Radius, while rectangles get x and y sizes. The -latter values are all between [0, 1] and get scaled to the grid cell. - -The browser (this code, in fact) then iteratively draws them in, one layer at a -time. Thus, it should be possible to turn different layers on and off. - -Here's a sample input, for a 2x2 grid with one layer being cell colors and the -other agent locations, represented by circles: - -{"Shape": "rect", "x": 0, "y": 0, "Color": ["#00aa00", "#aa00aa"], "stroke_color": "red", "Filled": "true", "Layer": 0} - -{0:[ - {"Shape": "rect", "x": 0, "y": 0, "w": 1, "h": 1,"Color": ["#00aa00", "#aa00aa"], "stroke_color": "red", "Filled": "true", "Layer": 0}, - {"Shape": "rect", "x": 0, "y": 1, "w": 1, "h": 1, "Color": ["#00aa00", "#aa00aa"], "stroke_color": "red", "Filled": "true", "Layer": 0}, - {"Shape": "rect", "x": 1, "y": 0, "w": 1, "h": 1, "Color": ["#00aa00", "#aa00aa"], "stroke_color": "red", "Filled": "true", "Layer": 0}, - {"Shape": "rect", "x": 1, "y": 1, "w": 1, "h": 1, "Color": ["#00aa00", "#aa00aa"], "stroke_color": "red", "Filled": "true", "Layer": 0} - ], - 1:[ - {"Shape": "circle", "x": 0, "y": 0, "r": 0.5, "Color": ["#00aa00", "#aa00aa"], "stroke_color": "red", "Filled": "true", "Layer": 1, "text": 'A', "text_color": "white"}, - {"Shape": "circle", "x": 1, "y": 1, "r": 0.5, "Color": ["#00aa00", "#aa00aa"], "stroke_color": "red", "Filled": "true", "Layer": 1, "text": 'B', "text_color": "white"} - {"Shape": "arrowHead", "x": 1, "y": 0, "heading_x": -1, heading_y: 0, "scale": 0.5, "Color": ["#00aa00", "#aa00aa"], "stroke_color": "red", "Filled": "true", "Layer": 1, "text": 'C', "text_color": "white"} - ] -} - -*/ - -const GridVisualization = function ( - width, - height, - gridWidth, - gridHeight, - context, - interactionHandler -) { - // Find cell size: - const cellWidth = Math.floor(width / gridWidth); - const cellHeight = Math.floor(height / gridHeight); - - // Find max radius of the circle that can be inscribed (fit) into the - // cell of the grid. - const maxR = Math.min(cellHeight, cellWidth) / 2 - 1; - - // Calls the appropriate shape(agent) - this.drawLayer = function (portrayalLayer) { - // Re-initialize the lookup table - interactionHandler ? interactionHandler.mouseoverLookupTable.init() : null; - - for (const i in portrayalLayer) { - const p = portrayalLayer[i]; - - // If p.Color is a string scalar, cast it to an array. - // This is done to maintain backwards compatibility - if (!Array.isArray(p.Color)) p.Color = [p.Color]; - - // Does the inversion of y positioning because of html5 - // canvas y direction is from top to bottom. But we - // normally keep y-axis in plots from bottom to top. - p.y = gridHeight - p.y - 1; - - // if a handler exists, add coordinates for the portrayalLayer index - interactionHandler - ? interactionHandler.mouseoverLookupTable.set(p.x, p.y, i) - : null; - - // If the stroke color is not defined, then the first color in the colors array is the stroke color. - if (!p.stroke_color) p.stroke_color = p.Color[0]; - - // Default alignments to 0.5 (center of a cell) - p.xAlign ??= 0.5; - p.yAlign ??= 0.5; - - if (p.Shape == "rect") - this.drawRectangle( - p.x, - p.y, - p.xAlign, - p.yAlign, - p.w, - p.h, - p.Color, - p.stroke_color, - p.Filled, - p.text, - p.text_color - ); - else if (p.Shape == "circle") - this.drawCircle( - p.x, - p.y, - p.xAlign, - p.yAlign, - p.r, - p.Color, - p.stroke_color, - p.Filled, - p.text, - p.text_color - ); - else if (p.Shape == "arrowHead") - this.drawArrowHead( - p.x, - p.y, - p.heading_x, - p.heading_y, - p.scale, - p.Color, - p.stroke_color, - p.Filled, - p.text, - p.text_color - ); - else - this.drawCustomImage(p.Shape, p.x, p.y, p.scale, p.text, p.text_color); - } - // if a handler exists, update its mouse listeners with the new data - interactionHandler - ? interactionHandler.updateMouseListeners(portrayalLayer) - : null; - }; - - // DRAWING METHODS - // ===================================================================== - - /** - Draw a circle in the specified grid cell. - x, y: Grid coords - xAlign, yAlign: Alignment within the cell, defaults to 0.5 (center) - r: Radius, as a multiple of cell size - colors: List of colors for the gradient. Providing only one color will fill the shape with only that color, not gradient. - stroke_color: Color to stroke the shape - fill: Boolean for whether or not to fill the circle. - text: Inscribed text in rectangle. - text_color: Color of the inscribed text. - */ - this.drawCircle = function ( - x, - y, - xAlign, - yAlign, - radius, - colors, - stroke_color, - fill, - text, - text_color - ) { - // Prevent circle from being drawn outside cell bounds. - // Since a radius of 1 corresponds to a circle that fills - // the entire cell, it is necessary to divide radius by 2. - xAlign = clamp(xAlign, radius / 2, 1 - radius / 2); - yAlign = clamp(yAlign, radius / 2, 1 - radius / 2); - - const cx = (x + xAlign) * cellWidth; - const cy = (y + yAlign) * cellHeight; - const r = radius * maxR; - - context.beginPath(); - context.arc(cx, cy, r, 0, Math.PI * 2, false); - context.closePath(); - - context.strokeStyle = stroke_color; - context.stroke(); - - if (fill) { - const gradient = context.createRadialGradient(cx, cy, r, cx, cy, 0); - - for (let i = 0; i < colors.length; i++) { - gradient.addColorStop(i / colors.length, colors[i]); - } - - context.fillStyle = gradient; - context.fill(); - } - - // This part draws the text inside the Circle - if (text !== undefined) { - context.fillStyle = text_color; - context.textAlign = "center"; - context.textBaseline = "middle"; - context.fillText(text, cx, cy); - } - }; - - /** - Draw a rectangle in the specified grid cell. - x, y: Grid coords - xAlign, yAlign: Alignment within the cell, defaults to 0.5 (center) - w, h: Width and height, [0, 1] - colors: List of colors for the gradient. Providing only one color will fill the shape with only that color, not gradient. - stroke_color: Color to stroke the shape - fill: Boolean, whether to fill or not. - text: Inscribed text in rectangle. - text_color: Color of the inscribed text. - */ - this.drawRectangle = function ( - x, - y, - xAlign, - yAlign, - w, - h, - colors, - stroke_color, - fill, - text, - text_color - ) { - context.beginPath(); - const dx = w * cellWidth; - const dy = h * cellHeight; - - // Prevent rect from being drawn outside cell bounds. - xAlign = clamp(xAlign, w / 2, 1 - w / 2); - yAlign = clamp(yAlign, h / 2, 1 - h / 2); - - const x0 = (x + xAlign) * cellWidth - dx / 2; - const y0 = (y + yAlign) * cellHeight - dy / 2; - - context.strokeStyle = stroke_color; - context.strokeRect(x0, y0, dx, dy); - - if (fill) { - const gradient = context.createLinearGradient( - x0, - y0, - x0 + cellWidth, - y0 + cellHeight - ); - - for (let i = 0; i < colors.length; i++) { - gradient.addColorStop(i / colors.length, colors[i]); - } - - // Fill with gradient - context.fillStyle = gradient; - context.fillRect(x0, y0, dx, dy); - } else { - context.fillStyle = color; - context.strokeRect(x0, y0, dx, dy); - } - // This part draws the text inside the Rectangle - if (text !== undefined) { - const cx = (x + 0.5) * cellWidth; - const cy = (y + 0.5) * cellHeight; - context.fillStyle = text_color; - context.textAlign = "center"; - context.textBaseline = "middle"; - context.fillText(text, cx, cy); - } - }; - - /** - Draw an arrow head in the specified grid cell. - x, y: Grid coords - s: Scaling of the arrowHead with respect to cell size[0, 1] - colors: List of colors for the gradient. Providing only one color will fill the shape with only that color, not gradient. - stroke_color: Color to stroke the shape - fill: Boolean, whether to fill or not. - text: Inscribed text in shape. - text_color: Color of the inscribed text. - */ - this.drawArrowHead = function ( - x, - y, - heading_x, - heading_y, - scale, - colors, - stroke_color, - fill, - text, - text_color - ) { - const arrowR = maxR * scale; - const cx = (x + 0.5) * cellWidth; - const cy = (y + 0.5) * cellHeight; - if (heading_x === 0 && heading_y === 1) { - p1_x = cx; - p1_y = cy - arrowR; - p2_x = cx - arrowR; - p2_y = cy + arrowR; - p3_x = cx; - p3_y = cy + 0.8 * arrowR; - p4_x = cx + arrowR; - p4_y = cy + arrowR; - } else if (heading_x === 1 && heading_y === 0) { - p1_x = cx + arrowR; - p1_y = cy; - p2_x = cx - arrowR; - p2_y = cy - arrowR; - p3_x = cx - 0.8 * arrowR; - p3_y = cy; - p4_x = cx - arrowR; - p4_y = cy + arrowR; - } else if (heading_x === 0 && heading_y === -1) { - p1_x = cx; - p1_y = cy + arrowR; - p2_x = cx - arrowR; - p2_y = cy - arrowR; - p3_x = cx; - p3_y = cy - 0.8 * arrowR; - p4_x = cx + arrowR; - p4_y = cy - arrowR; - } else if (heading_x === -1 && heading_y === 0) { - p1_x = cx - arrowR; - p1_y = cy; - p2_x = cx + arrowR; - p2_y = cy - arrowR; - p3_x = cx + 0.8 * arrowR; - p3_y = cy; - p4_x = cx + arrowR; - p4_y = cy + arrowR; - } - - context.beginPath(); - context.moveTo(p1_x, p1_y); - context.lineTo(p2_x, p2_y); - context.lineTo(p3_x, p3_y); - context.lineTo(p4_x, p4_y); - context.closePath(); - - context.strokeStyle = stroke_color; - context.stroke(); - - if (fill) { - const gradient = context.createLinearGradient(p1_x, p1_y, p3_x, p3_y); - - for (let i = 0; i < colors.length; i++) { - gradient.addColorStop(i / colors.length, colors[i]); - } - - // Fill with gradient - context.fillStyle = gradient; - context.fill(); - } - - // This part draws the text inside the ArrowHead - if (text !== undefined) { - context.fillStyle = text_color; - context.textAlign = "center"; - context.textBaseline = "middle"; - context.fillText(text, cx, cy); - } - }; - - this.drawCustomImage = function (shape, x, y, scale, text, text_color_) { - const img = new Image(); - img.src = "local/custom/".concat(shape); - if (scale === undefined) { - scale = 1; - } - // Calculate coordinates so the image is always centered - const dWidth = cellWidth * scale; - const dHeight = cellHeight * scale; - const cx = x * cellWidth + cellWidth / 2 - dWidth / 2; - const cy = y * cellHeight + cellHeight / 2 - dHeight / 2; - - // Coordinates for the text - const tx = (x + 0.5) * cellWidth; - const ty = (y + 0.5) * cellHeight; - - img.onload = function () { - context.drawImage(img, cx, cy, dWidth, dHeight); - // This part draws the text on the image - if (text !== undefined) { - // ToDo: Fix fillStyle - // context.fillStyle = text_color; - context.textAlign = "center"; - context.textBaseline = "middle"; - context.fillText(text, tx, ty); - } - }; - }; - - /** - Draw Grid lines in the full gird - */ - - this.drawGridLines = function () { - context.beginPath(); - context.strokeStyle = "#eee"; - maxX = cellWidth * gridWidth; - maxY = cellHeight * gridHeight; - - // Draw horizontal grid lines: - for (let y = 0; y <= maxY; y += cellHeight) { - context.moveTo(0, y + 0.5); - context.lineTo(maxX, y + 0.5); - } - - for (let x = 0; x <= maxX; x += cellWidth) { - context.moveTo(x + 0.5, 0); - context.lineTo(x + 0.5, maxY); - } - - context.stroke(); - }; - - this.resetCanvas = function () { - context.clearRect(0, 0, width, height); - context.beginPath(); - }; -}; - -const clamp = function (val, min, max) { - return Math.min(Math.max(min, val), max); -} diff --git a/mesa/visualization/templates/js/HexDraw.js b/mesa/visualization/templates/js/HexDraw.js deleted file mode 100644 index a09bbbff9bc..00000000000 --- a/mesa/visualization/templates/js/HexDraw.js +++ /dev/null @@ -1,266 +0,0 @@ -/** -Mesa Canvas Grid Visualization -==================================================================== - -This is JavaScript code to visualize a Mesa Grid or MultiGrid state using the -HTML5 Canvas. Here's how it works: - -On the server side, the model developer will have assigned a portrayal to each -agent type. The visualization then loops through the grid, for each object adds -a JSON object to an inner list (keyed on layer) of lists to be sent to the -browser. - -Each JSON object to be drawn contains the following fields: Shape (currently -only rectanges and circles are supported), x, y, Color, Filled (boolean), -Layer; circles also get a Radius, while rectangles get x and y sizes. The -latter values are all between [0, 1] and get scaled to the grid cell. - -The browser (this code, in fact) then iteratively draws them in, one layer at a -time. Thus, it should be possible to turn different layers on and off. - -Here's a sample input, for a 2x2 grid with one layer being cell colors and the -other agent locations, represented by circles: - -{"Shape": "rect", "x": 0, "y": 0, "Color": "#00aa00", "Filled": "true", "Layer": 0} - -{0:[ - {"Shape": "rect", "x": 0, "y": 0, "w": 1, "h": 1, "Color": "#00aa00", "Filled": "true", "Layer": 0}, - {"Shape": "rect", "x": 0, "y": 1, "w": 1, "h": 1, "Color": "#00aa00", "Filled": "true", "Layer": 0}, - {"Shape": "rect", "x": 1, "y": 0, "w": 1, "h": 1, "Color": "#00aa00", "Filled": "true", "Layer": 0}, - {"Shape": "rect", "x": 1, "y": 1, "w": 1, "h": 1, "Color": "#00aa00", "Filled": "true", "Layer": 0} - ], - 1:[ - {"Shape": "circle", "x": 0, "y": 0, "r": 0.5, "Color": "#AAAAAA", "Filled": "true", "Layer": 1, "text": 'A', "text_color": "white"}, - {"Shape": "circle", "x": 1, "y": 1, "r": 0.5, "Color": "#AAAAAA", "Filled": "true", "Layer": 1, "text": 'B', "text_color": "white"} - {"Shape": "arrowHead", "x": 1, "y": 0, "heading_x": -1, heading_y: 0, "scale": 0.5, "Color": "green", "Filled": "true", "Layer": 1, "text": 'C', "text_color": "white"} - ] -} - -*/ - -const HexVisualization = function ( - width, - height, - gridWidth, - gridHeight, - context, - interactionHandler -) { - // Find cell size: - const cellWidth = Math.floor(width / gridWidth); - const cellHeight = Math.floor(height / gridHeight); - - // Find max radius of the circle that can be inscribed (fit) into the - // cell of the grid. - const maxR = Math.min(cellHeight, cellWidth) / 2 - 1; - - // Configure the interaction handler to use a hex coordinate mapper - interactionHandler ? interactionHandler.setCoordinateMapper("hex") : null; - - // Calls the appropriate shape(agent) - this.drawLayer = function (portrayalLayer) { - // Re-initialize the lookup table - interactionHandler ? interactionHandler.mouseoverLookupTable.init() : null; - for (const i in portrayalLayer) { - const p = portrayalLayer[i]; - // Does the inversion of y positioning because of html5 - // canvas y direction is from top to bottom. But we - // normally keep y-axis in plots from bottom to top. - p.y = gridHeight - p.y - 1; - - // if a handler exists, add coordinates for the portrayalLayer index - interactionHandler - ? interactionHandler.mouseoverLookupTable.set(p.x, p.y, i) - : null; - - if (p.Shape == "hex") - this.drawHex(p.x, p.y, p.r, p.Color, p.Filled, p.text, p.text_color); - else if (p.Shape == "circle") - this.drawCircle(p.x, p.y, p.r, p.Color, p.Filled, p.text, p.text_color); - else if (p.Shape == "arrowHead") - this.drawArrowHead( - p.x, - p.y, - p.heading_x, - p.heading_y, - p.scale, - p.Color, - p.Filled, - p.text, - p.text_color - ); - else - this.drawCustomImage(p.Shape, p.x, p.y, p.scale, p.text, p.text_color); - } - // if a handler exists, update its mouse listeners with the new data - interactionHandler - ? interactionHandler.updateMouseListeners(portrayalLayer) - : null; - }; - - // DRAWING METHODS - // ===================================================================== - - /** - Draw a circle in the specified grid cell. - x, y: Grid coords - r: Radius, as a multiple of cell size - color: Code for the fill color - fill: Boolean for whether or not to fill the circle. - text: Inscribed text in rectangle. - text_color: Color of the inscribed text. - */ - this.drawCircle = function (x, y, radius, color, fill, text, text_color) { - const cx = (x + 0.5) * cellWidth; - let cy; - if (x % 2 == 0) { - cy = (y + 0.5) * cellHeight; - } else { - cy = (y + 0.5) * cellHeight + cellHeight / 2; - } - const r = radius * maxR; - - context.beginPath(); - context.arc(cx, cy, r, 0, Math.PI * 2, false); - context.closePath(); - - context.strokeStyle = color; - context.stroke(); - - if (fill) { - context.fillStyle = color; - context.fill(); - } - - // This part draws the text inside the Circle - if (text !== undefined) { - context.fillStyle = text_color; - context.textAlign = "center"; - context.textBaseline = "middle"; - context.fillText(text, cx, cy); - } - }; - - /** - Draw a hexagon in the specified grid cell. - x, y: Grid coords - r: Radius, as a multiple of cell size - color: Code for the fill color - fill: Boolean for whether or not to fill the circle. - text: Inscribed text in rectangle. - text_color: Color of the inscribed text. - */ - this.drawHex = function (x, y, radius, color, fill, text, text_color) { - const cx = (x + 0.5) * cellWidth; - let cy; - if (x % 2 == 0) { - cy = (y + 0.5) * cellHeight; - } else { - cy = (y + 0.5) * cellHeight + cellHeight / 2; - } - maxHexRadius = cellHeight / Math.sqrt(3); - const r = radius * maxHexRadius; - - function hex_corner(x, y, size, i) { - const angle_deg = 60 * i; - const angle_rad = (Math.PI / 180) * angle_deg; - return [ - x + size * Math.cos(angle_rad) * 1.2, - y + size * Math.sin(angle_rad), - ]; - } - - context.beginPath(); - let [px, py] = hex_corner(cx, cy, r, 1); - // console.log(px,py) - context.moveTo(px, py); - //for i in range(5): - Array.from(new Array(5), (n, i) => { - [px, py] = hex_corner(cx, cy, r, i + 2); - // console.log(px,py) - context.lineTo(px, py); - }); - context.closePath(); - - context.strokeStyle = color; - context.stroke(); - - if (fill) { - context.fillStyle = color; - context.fill(); - } - // This part draws the text inside the Circle - if (text !== undefined) { - context.fillStyle = text_color; - context.textAlign = "center"; - context.textBaseline = "middle"; - context.fillText(text, cx, cy); - } - }; - - this.drawCustomImage = function (shape, x, y, scale, text, text_color_) { - const img = new Image(); - img.src = "local/".concat(shape); - if (scale === undefined) { - scale = 1; - } - // Calculate coordinates so the image is always centered - const dWidth = cellWidth * scale; - const dHeight = cellHeight * scale; - const cx = x * cellWidth + cellWidth / 2 - dWidth / 2; - const cy = y * cellHeight + cellHeight / 2 - dHeight / 2; - - // Coordinates for the text - const tx = (x + 0.5) * cellWidth; - const ty = (y + 0.5) * cellHeight; - - img.onload = function () { - context.drawImage(img, cx, cy, dWidth, dHeight); - // This part draws the text on the image - if (text !== undefined) { - // ToDo: Fix fillStyle - // context.fillStyle = text_color; - context.textAlign = "center"; - context.textBaseline = "middle"; - context.fillText(text, tx, ty); - } - }; - }; - - /** - Draw Grid lines in the full gird - */ - - this.drawGridLines = function (strokeColor) { - context.beginPath(); - context.strokeStyle = strokeColor || "#eee"; - const maxX = cellWidth * gridWidth; - const maxY = cellHeight * gridHeight; - - const xStep = cellWidth * 0.33; - const yStep = cellHeight * 0.5; - - let yStart = yStep; - for (let x = cellWidth / 2; x <= maxX; x += cellWidth) { - for (let y = yStart; y <= maxY; y += cellHeight) { - context.moveTo(x - 2 * xStep, y); - - context.lineTo(x - xStep, y - yStep); - context.lineTo(x + xStep, y - yStep); - context.lineTo(x + 2 * xStep, y); - - context.lineTo(x + xStep, y + yStep); - context.lineTo(x - xStep, y + yStep); - context.lineTo(x - 2 * xStep, y); - } - yStart = yStart === 0 ? yStep : 0; - } - - context.stroke(); - }; - - this.resetCanvas = function () { - context.clearRect(0, 0, width, height); - context.beginPath(); - }; -}; diff --git a/mesa/visualization/templates/js/InteractionHandler.js b/mesa/visualization/templates/js/InteractionHandler.js deleted file mode 100644 index 93f1a6b6371..00000000000 --- a/mesa/visualization/templates/js/InteractionHandler.js +++ /dev/null @@ -1,186 +0,0 @@ -/** -Mesa Visualization InteractionHandler -==================================================================== - -This uses the context of an additional canvas laid overtop of another canvas -visualization and maps mouse movements to agent position, displaying any agent -attributes included in the portrayal that are not listed in the ignoredFeatures. - -The following portrayal will yield tooltips with wealth, id, and pos: - -portrayal = { - "Shape": "circle", - "Filled": "true", - "Layer": 0, - "Color": colors[agent.wealth] if agent.wealth < len(colors) else '#a0a', - "r": 0.3 + 0.1 * agent.wealth, - "wealth": agent.wealth, - "id": agent.unique_id, - 'pos': agent.pos -} - -**/ - -const InteractionHandler = function (width, height, gridWidth, gridHeight, ctx) { - // Find cell size: - const cellWidth = Math.floor(width / gridWidth); - const cellHeight = Math.floor(height / gridHeight); - - const lineHeight = 10; - - // list of standard rendering features to ignore (and key-values in the portrayal will be added ) - const ignoredFeatures = [ - "Shape", - "Filled", - "Color", - "r", - "x", - "y", - "xAlign", - "yAlign", - "w", - "h", - "width", - "height", - "heading_x", - "heading_y", - "stroke_color", - "text_color", - ]; - - // Set a variable to hold the lookup table and make it accessible to draw scripts - const mouseoverLookupTable = (this.mouseoverLookupTable = buildLookupTable( - gridWidth, - gridHeight - )); - function buildLookupTable(gridWidth, gridHeight) { - let lookupTable; - this.init = function () { - lookupTable = [...Array(gridHeight).keys()].map((i) => Array(gridWidth)); - }; - - this.set = function (x, y, value) { - if (lookupTable[y][x]) lookupTable[y][x].push(value); - else lookupTable[y][x] = [value]; - }; - - this.get = function (x, y) { - if (lookupTable[y]) return lookupTable[y][x] || []; - return []; - }; - - return this; - } - - let coordinateMapper; - this.setCoordinateMapper = function (mapperName) { - if (mapperName === "hex") { - coordinateMapper = function (event) { - const x = Math.floor(event.offsetX / cellWidth); - const y = - x % 2 === 0 - ? Math.floor(event.offsetY / cellHeight) - : Math.floor((event.offsetY - cellHeight / 2) / cellHeight); - return { x: x, y: y }; - }; - return; - } - - // default coordinate mapper for grids - coordinateMapper = function (event) { - return { - x: Math.floor(event.offsetX / cellWidth), - y: Math.floor(event.offsetY / cellHeight), - }; - }; - }; - - this.setCoordinateMapper("grid"); - - // wrap the rect styling in a function - function drawTooltipBox(ctx, x, y, width, height) { - ctx.fillStyle = "#F0F0F0"; - ctx.beginPath(); - ctx.shadowOffsetX = -3; - ctx.shadowOffsetY = 2; - ctx.shadowBlur = 6; - ctx.shadowColor = "#33333377"; - ctx.rect(x, y, width, height); - ctx.fill(); - ctx.shadowColor = "transparent"; - } - - let listener; - let tmp; - this.updateMouseListeners = function (portrayalLayer) { - tmp = portrayalLayer; - - // Remove the prior event listener to avoid creating a new one every step - ctx.canvas.removeEventListener("mousemove", listener); - - // define the event litser for this step - listener = function (event) { - // clear the previous interaction - ctx.clearRect(0, 0, width, height); - - // map the event to x,y coordinates - const position = coordinateMapper(event); - const yPosition = Math.floor(event.offsetY / cellHeight); - const xPosition = Math.floor(event.offsetX / cellWidth); - - // look up the portrayal items the coordinates refer to and draw a tooltip - mouseoverLookupTable - .get(position.x, position.y) - .forEach((portrayalIndex, nthAgent) => { - const agent = portrayalLayer[portrayalIndex]; - const features = Object.keys(agent).filter( - (k) => ignoredFeatures.indexOf(k) < 0 - ); - const textWidth = Math.max.apply( - null, - features.map((k) => ctx.measureText(`${k}: ${agent[k]}`).width) - ); - const textHeight = features.length * lineHeight; - const y = Math.max( - lineHeight * 2, - Math.min(height - textHeight, event.offsetY - textHeight / 2) - ); - const rectMargin = 2 * lineHeight; - let x = 0; - let rectX = 0; - - if (event.offsetX < width / 2) { - x = - event.offsetX + rectMargin + nthAgent * (textWidth + rectMargin); - ctx.textAlign = "left"; - rectX = x - rectMargin / 2; - } else { - x = - event.offsetX - - rectMargin - - nthAgent * (textWidth + rectMargin + lineHeight); - ctx.textAlign = "right"; - rectX = x - textWidth - rectMargin / 2; - } - - // draw a background box - drawTooltipBox( - ctx, - rectX, - y - rectMargin, - textWidth + rectMargin, - textHeight + rectMargin - ); - - // set the color and draw the text - ctx.fillStyle = "black"; - features.forEach((k, i) => { - ctx.fillText(`${k}: ${agent[k]}`, x, y + i * lineHeight); - }); - }); - }; - ctx.canvas.addEventListener("mousemove", listener); - }; - - return this; -}; diff --git a/mesa/visualization/templates/js/NetworkModule_d3.js b/mesa/visualization/templates/js/NetworkModule_d3.js deleted file mode 100644 index 83032083224..00000000000 --- a/mesa/visualization/templates/js/NetworkModule_d3.js +++ /dev/null @@ -1,126 +0,0 @@ -const NetworkModule = function (svg_width, svg_height) { - // Create the svg element: - const svg = d3.create("svg"); - svg - .attr("class", "NetworkModule_d3") - .attr("width", svg_width) - .attr("height", svg_height) - .style("border", "1px dotted"); - - // Append svg to #elements: - document.getElementById("elements").appendChild(svg.node()); - - const width = +svg.attr("width"); - const height = +svg.attr("height"); - const g = svg - .append("g") - .classed("network_root", true); - - const tooltip = d3 - .select("body") - .append("div") - .attr("class", "d3tooltip") - .style("opacity", 0); - - const zoom = d3.zoom() - .on("zoom", (event) => { - g.attr("transform", event.transform); - }); - - svg.call(zoom); - - svg.call( - zoom.transform, - d3.zoomIdentity.translate(width / 2, height / 2) - ); - - const links = g.append("g").attr("class", "links"); - - const nodes = g.append("g").attr("class", "nodes"); - - this.render = (data) => { - const graph = JSON.parse(JSON.stringify(data)); - - const simulation = d3 - .forceSimulation() - .nodes(graph.nodes) - .force("charge", d3.forceManyBody().strength(-80).distanceMin(2)) - .force("link", d3.forceLink(graph.edges)) - .force("center", d3.forceCenter()) - .stop(); - - for ( - let i = 0, - n = Math.ceil( - Math.log(simulation.alphaMin()) / - Math.log(1 - simulation.alphaDecay()) - ); - i < n; - ++i - ) { - simulation.tick(); - } - - links.selectAll("line").data(graph.edges).enter().append("line"); - - links - .selectAll("line") - .data(graph.edges) - .attr("x1", function (d) { - return d.source.x; - }) - .attr("y1", function (d) { - return d.source.y; - }) - .attr("x2", function (d) { - return d.target.x; - }) - .attr("y2", function (d) { - return d.target.y; - }) - .attr("stroke-width", function (d) { - return d.width; - }) - .attr("stroke", function (d) { - return d.color; - }); - - links.selectAll("line").data(graph.edges).exit().remove(); - - nodes - .selectAll("circle") - .data(graph.nodes) - .enter() - .append("circle") - .on("mouseover", function (event, d) { - tooltip.transition().duration(200).style("opacity", 0.9); - tooltip - .html(d.tooltip) - .style("left", event.pageX + "px") - .style("top", event.pageY + "px"); - }) - .on("mouseout", function () { - tooltip.transition().duration(500).style("opacity", 0); - }); - - nodes - .selectAll("circle") - .data(graph.nodes) - .attr("cx", function (d) { - return d.x; - }) - .attr("cy", function (d) { - return d.y; - }) - .attr("r", function (d) { - return d.size; - }) - .attr("fill", function (d) { - return d.color; - }); - - nodes.selectAll("circle").data(graph.nodes).exit().remove(); - }; - - this.reset = () => {}; -}; diff --git a/mesa/visualization/templates/js/PieChartModule.js b/mesa/visualization/templates/js/PieChartModule.js deleted file mode 100644 index 16237d31f7f..00000000000 --- a/mesa/visualization/templates/js/PieChartModule.js +++ /dev/null @@ -1,111 +0,0 @@ -"use strict"; -//Note: This pie chart is based off the example found here: -//https://bl.ocks.org/mbostock/3887235 - -const PieChartModule = function (fields, canvas_width, canvas_height) { - const createElement = (tagName, attrs) => { - const element = document.createElement(tagName); - Object.assign(element, attrs); - return element; - }; - - // Create the overall chart div - const chartDiv = createElement("div", { - className: "pie chart", - width: canvas_width, - }); - document.getElementById("elements").appendChild(chartDiv); - - // Create the svg element: - const svg = d3.create("svg"); - svg - .attr("width", canvas_width) - .attr("height", canvas_height) - .style("border", "1px dotted"); - chartDiv.appendChild(svg.node()); - - //create the legend - const legend = d3.create("div"); - legend - .attr("class", "legend") - .attr("style", `display:block;width:${canvas_width}px;text-align:center`); - chartDiv.appendChild(legend.node()); - - legend - .selectAll("span") - .data(fields) - .enter() - .append("span") - .html(function (d) { - return ( - "" + - " " + - d["Label"].replace(" ", " ") - ); - }) - .attr("style", "padding-left:10px;padding-right:10px;"); - - // setup the d3 svg selection - const width = +svg.attr("width"); - const height = +svg.attr("height"); - const maxRadius = Math.min(width, height) / 2; - const g = svg - .append("g") - .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); - - // Create the base chart and helper methods - const color = d3.scaleOrdinal(fields.map((field) => field["Color"])); - const pie = d3 - .pie() - .sort(null) - .value(function (d) { - return d; - }); - const path = d3.arc().outerRadius(maxRadius).innerRadius(0); - const arc = g - .selectAll(".arc") - .data(pie(fields.map((field) => 0))) //Initialize the pie chart with dummy data - .enter() - .append("g") - .attr("class", "arc"); - - arc - .append("path") - .attr("d", path) - .style("fill", function (d, i) { - return color(i); - }) - .append("title") - .text(function (d) { - return d.value; - }); - - this.render = function (data) { - //Update the pie chart each time new data comes in - arc - .data(pie(data)) - .select("path") - .attr("d", path) - .select("title") - .text(function (d) { - return ( - d.value + - " : " + - (((d.endAngle - d.startAngle) * 100.0) / (Math.PI * 2)).toFixed(2) + - "%" - ); - }); - }; - - this.reset = function () { - //Reset the chart by setting each field to 0 - arc - .data(pie(fields.map((field) => 0))) - .enter() - .select("g"); - - arc.select("path").attr("d", path); - }; -}; diff --git a/mesa/visualization/templates/js/TextModule.js b/mesa/visualization/templates/js/TextModule.js deleted file mode 100644 index c06888517f7..00000000000 --- a/mesa/visualization/templates/js/TextModule.js +++ /dev/null @@ -1,15 +0,0 @@ -const TextModule = function () { - const text = document.createElement("p"); - text.className = "lead"; - - // Append text tag to #elements: - document.getElementById("elements").appendChild(text); - - this.render = function (data) { - text.innerHTML = data; - }; - - this.reset = function () { - text.innerHTML = ""; - }; -}; diff --git a/mesa/visualization/templates/js/runcontrol.js b/mesa/visualization/templates/js/runcontrol.js deleted file mode 100644 index 8cc7176f94d..00000000000 --- a/mesa/visualization/templates/js/runcontrol.js +++ /dev/null @@ -1,360 +0,0 @@ -/* runcontrol.js - Users can reset() the model, advance it by one step(), or start() it. reset() and - step() send a message to the server, which then sends back the appropriate data. - start() just calls the step() method at fixed intervals. - - The model parameters are controlled via the ModelController object. -*/ - -/* - * Variable definitions - */ -const controller = new ModelController(); -const vizElements = []; -const startModelButton = document.getElementById("play-pause"); -const stepModelButton = document.getElementById("step"); -const resetModelButton = document.getElementById("reset"); -const stepDisplay = document.getElementById("currentStep"); - -/** - * A ModelController that defines the model state. - * @param {number} tick=0 - Initial step of the model - * @param {number} fps=3 - Run the model with this number of frames per second - * @param {boolean} running=false - Initialize the model in a running state? - * @param {boolean} finished=false - Initialize the model in a finished state? - */ -function ModelController(tick = 0, fps = 3, running = false, finished = false) { - this.tick = tick; - this.fps = fps; - this.running = running; - this.finished = finished; - - /** Start the model and keep it running until stopped */ - this.start = function start() { - this.running = true; - this.step(); - startModelButton.firstElementChild.innerText = "Stop"; - }; - - /** Stop the model */ - this.stop = function stop() { - this.running = false; - startModelButton.firstElementChild.innerText = "Start"; - }; - - /** - * Step the model one step ahead. - * - * If the model is in a running state this function will be called repeatedly - * after the visualization elements are rendered. */ - this.step = function step() { - this.tick += 1; - stepDisplay.innerText = this.tick; - send({ type: "get_step", step: this.tick }); - }; - - /** Reset the model and visualization state but keep its running state */ - this.reset = function reset() { - this.tick = 0; - stepDisplay.innerText = this.tick; - // Reset all the visualizations - vizElements.forEach((element) => element.reset()); - if (this.finished) { - this.finished = false; - startModelButton.firstElementChild.innerText = "Start"; - } - clearTimeout(this.timeout); - send({ type: "reset" }); - }; - - /** Stops the model and put it into a finished state */ - this.done = function done() { - this.stop(); - this.finished = true; - startModelButton.firstElementChild.innerText = "Done"; - }; - - /** - * Render visualisation elements with new data. - * @param {any[]} data Model state data passed to the visualization elements - */ - this.render = function render(data) { - vizElements.forEach((element, index) => element.render(data[index])); - if (this.running) { - this.timeout = setTimeout(() => this.step(), 1000 / this.fps); - } - }; - - /** - * Update the frames per second - * @param {number} val - The new value of frames per second - */ - this.updateFPS = function (val) { - this.fps = Number(val); - }; -} - -/* - * Set up the the FPS control - */ -const fpsControl = new Slider("#fps", { - max: 20, - min: 0, - value: controller.fps, - ticks: [0, 20], - ticks_labels: [0, 20], - ticks_position: [0, 100], -}); -fpsControl.on("change", () => controller.updateFPS(fpsControl.getValue())); - -/* - * Button logic for start, stop and reset buttons - */ -startModelButton.onclick = () => { - if (controller.running) { - controller.stop(); - } else if (!controller.finished) { - controller.start(); - } -}; -stepModelButton.onclick = () => { - if (!controller.running & !controller.finished) { - controller.step(); - } -}; -resetModelButton.onclick = () => controller.reset(); - -/* - * Websocket opening and message handling - */ - -/** Open the websocket connection; support TLS-specific URLs when appropriate */ -const ws = new WebSocket( - (window.location.protocol === "https:" ? "wss://" : "ws://") + - location.host + - "/ws" -); - -/** - * Parse and handle an incoming message on the WebSocket connection. - * @param {string} message - the message received from the WebSocket - */ -ws.onmessage = function (message) { - const msg = JSON.parse(message.data); - switch (msg["type"]) { - case "viz_state": - // Update visualization state - controller.render(msg["data"]); - break; - case "end": - // We have reached the end of the model - controller.done(); - break; - case "model_params": - // Create GUI elements for each model parameter and reset everything - initGUI(msg["params"]); - controller.reset(); - break; - default: - // There shouldn't be any other message - console.log("Unexpected message."); - console.log(msg); - } -}; - -/** - * Turn an object into a string to send to the server, and send it. - * @param {string} message - The message to send to the Python server - */ -const send = function (message) { - const msg = JSON.stringify(message); - ws.send(msg); -}; - -/* - * GUI initialization (for input parameters) - */ - -/** - * Create the GUI with user-settable parameters - * @param {object} model_params - Create the GUI from these model parameters - */ -const initGUI = function (model_params) { - const sidebar = document.getElementById("sidebar"); - - const onSubmitCallback = function (param_name, value) { - send({ type: "submit_params", param: param_name, value: value }); - }; - - const addBooleanInput = function (param, obj) { - const domID = param + "_id"; - const _switch = document.createElement("div"); - _switch.className = "form-check form-switch"; - - const label = ` - - `; - _switch.innerHTML += label; - - const input = document.createElement("input"); - Object.assign(input, { - className: "form-check-input model-parameter", - type: "checkbox", - id: domID, - checked: obj.value, - }); - input.setAttribute("role", "switch"); - input.addEventListener("change", (event) => - onSubmitCallback(param, event.currentTarget.checked) - ); - _switch.appendChild(input); - - sidebar.appendChild(_switch); - }; - - const addNumberInput = function (param, obj) { - const domID = param + "_id"; - const div = document.createElement("div"); - div.innerHTML = ` -

- -

- - `; - sidebar.appendChild(div); - const numberInput = document.getElementById(domID); - numberInput.value = obj.value; - numberInput.onchange = () => { - onSubmitCallback(param, Number(numberInput.value)); - }; - }; - - const addSliderInput = function (param, obj) { - const domID = param + "_id"; - const tooltipID = domID + "_tooltip"; - let tooltip = ""; - // Enable tooltip label - if (obj.description !== null) { - tooltip = `title='${obj.description}'`; - } - - const div = document.createElement("div"); - div.innerHTML = ` -

- - ${obj.name} - -

- - `; - sidebar.appendChild(div); - - // Setup slider - const sliderInput = new Slider("#" + domID, { - min: obj.min_value, - max: obj.max_value, - value: obj.value, - step: obj.step, - ticks: [obj.min_value, obj.max_value], - ticks_labels: [obj.min_value, obj.max_value], - ticks_positions: [0, 100], - }); - sliderInput.on("change", () => { - onSubmitCallback(param, Number(sliderInput.getValue())); - }); - }; - - const addChoiceInput = function (param, obj) { - const domID = param + "_id"; - const template = [ - `

- -

`, - `"); - - // Finally render the dropdown and activate choice listeners - const div = document.createElement("div"); - div.innerHTML = template.join(""); - sidebar.appendChild(div); - - const select = document.getElementById(domID); - select.onchange = () => onSubmitCallback(param, obj.choices[select.value]); - }; - - const addTextBox = function (param, obj) { - const well = document.createElement("div"); - well.className = "well"; - well.innerHTML = obj.value; - sidebar.appendChild(well); - }; - - const addParamInput = function (param, option) { - switch (option["param_type"]) { - case "checkbox": - addBooleanInput(param, option); - break; - - case "slider": - addSliderInput(param, option); - break; - - case "choice": - addChoiceInput(param, option); - break; - - case "number": - addNumberInput(param, option); // Behaves the same as just a simple number - break; - - case "static_text": - addTextBox(param, option); - break; - } - }; - - for (const option in model_params) { - const type = typeof model_params[option]; - const param_str = String(option); - - switch (type) { - case "boolean": - addBooleanInput(param_str, { - value: model_params[option], - name: param_str, - }); - break; - case "number": - addNumberInput(param_str, { - value: model_params[option], - name: param_str, - }); - break; - case "object": - addParamInput(param_str, model_params[option]); // catch-all for params that use Option class - break; - } - } -}; - -// Backward-Compatibility aliases -const control = controller; -const elements = vizElements; diff --git a/mesa/visualization/templates/modular_template.html b/mesa/visualization/templates/modular_template.html deleted file mode 100644 index 7d6e56fd0fc..00000000000 --- a/mesa/visualization/templates/modular_template.html +++ /dev/null @@ -1,106 +0,0 @@ - - - {{ model_name }} (Mesa visualization) - - - - - - {% for file_name in package_css_includes %} - - {% end %} - {% for file_name in local_css_includes %} - - {% end %} - - - - - - - -
- -
-
-
- - -
-

Current Step: 0 -

-
-
-
- - - - - - - - - - {% for file_name in package_js_includes %} - - {% end %} - {% for file_name in local_js_includes %} - - {% end %} - - - - - - - - diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..7b7b1d6d026 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,51 @@ +[tool.ruff] +# See https://github.com/charliermarsh/ruff#rules for error code definitions. +select = [ + # "ANN", # annotations TODO + "B", # bugbear + "C4", # comprehensions + "DTZ", # naive datetime + "E", # style errors + "F", # flakes + "I", # import sorting + "ISC", # string concatenation + "N", # naming + "PGH", # pygrep-hooks + "PIE", # miscellaneous + "PLC", # pylint convention + "PLE", # pylint error + # "PLR", # pylint refactor TODO + "PLW", # pylint warning + "Q", # quotes + "RUF", # Ruff + "S", # security + "SIM", # simplify + "T10", # debugger + "UP", # upgrade + "W", # style warnings + "YTT", # sys.version +] +# Ignore list taken from https://github.com/psf/black/blob/master/.flake8 +# E203 Whitespace before ':' +# E266 Too many leading '#' for block comment +# E501 Line too long (82 > 79 characters) +# W503 Line break occurred before a binary operator +# But we don't specify them because ruff's Black already +# checks for it. +# See https://github.com/charliermarsh/ruff/issues/1842#issuecomment-1381210185 +extend-ignore = [ + "E501", + "S101", # Use of `assert` detected + "B017", # `assertRaises(Exception)` should be considered evil TODO + "PGH004", # Use specific rule codes when using `noqa` TODO + "B905", # `zip()` without an explicit `strict=` parameter + "N802", # Function name should be lowercase + "N999", # Invalid module name. We should revisit this in the future, TODO + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` TODO + "S310", # Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. + "S603", # `subprocess` call: check for execution of untrusted input +] +extend-exclude = ["docs", "build"] +# Hardcode to Python 3.8. +# Reminder to update mesa-examples if the value below is changed. +target-version = "py38" diff --git a/setup.py b/setup.py index 8a59a0f83f2..6024165dc50 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,41 @@ #!/usr/bin/env python import re -import os -import urllib.request -import zipfile -import shutil - -from setuptools import setup, find_packages from codecs import open -requires = ["click", "cookiecutter", "networkx", "numpy", "pandas", "tornado", "tqdm"] +from setuptools import find_packages, setup + +requires = [ + "click", + "cookiecutter", + "matplotlib", + "mesa_viz_tornado", + "networkx", + "numpy", + "pandas", + "solara", + "tqdm", +] extras_require = { - "dev": ["black", "coverage", "flake8", "pytest >= 4.6", "pytest-cov", "sphinx"], - "docs": ["sphinx", "ipython"], + "dev": [ + "black", + "ruff~=0.1.1", # Update periodically + "coverage", + "pytest >= 4.6", + "pytest-cov", + "sphinx", + ], + # Explicitly install ipykernel for Python 3.8. + # See https://stackoverflow.com/questions/28831854/how-do-i-add-python3-kernel-to-jupyter-ipython + # Could be removed in the future + "docs": [ + "sphinx", + "ipython", + "ipykernel", + "pydata_sphinx_theme", + "seaborn", + "myst-nb", + ], } version = "" @@ -24,76 +47,6 @@ with open("README.rst", "rb", encoding="utf-8") as f: readme = f.read() -# Ensure JS dependencies are downloaded -external_dir = "mesa/visualization/templates/external" -# We use a different path for single-file JS because some of them are loaded -# the same way as Mesa JS files -external_dir_single = "mesa/visualization/templates/js/external" -# First, ensure that the external directories exists -os.makedirs(external_dir, exist_ok=True) -os.makedirs(external_dir_single, exist_ok=True) - - -def ensure_JS_dep(dirname, url): - dst_path = os.path.join(external_dir, dirname) - if os.path.isdir(dst_path): - # Do nothing if already downloaded - return - print(f"Downloading the {dirname} dependency from the internet...") - zip_file = dirname + ".zip" - urllib.request.urlretrieve(url, zip_file) - with zipfile.ZipFile(zip_file, "r") as zip_ref: - zip_ref.extractall() - shutil.move(dirname, dst_path) - # Cleanup - os.remove(zip_file) - print("Done") - - -def ensure_JS_dep_single(url, out_name=None): - # Used for downloading e.g. D3.js single file - if out_name is None: - out_name = url.split("/")[-1] - dst_path = os.path.join(external_dir_single, out_name) - if os.path.isfile(dst_path): - return - print(f"Downloading the {out_name} dependency from the internet...") - urllib.request.urlretrieve(url, out_name) - shutil.move(out_name, dst_path) - - -# Important: when you update JS dependency version, make sure to also update the -# hardcoded included files and versions in: mesa/visualization/templates/modular_template.html - -# Ensure Bootstrap -bootstrap_version = "5.1.3" -ensure_JS_dep( - f"bootstrap-{bootstrap_version}-dist", - f"https://github.com/twbs/bootstrap/releases/download/v{bootstrap_version}/bootstrap-{bootstrap_version}-dist.zip", -) - -# Ensure Bootstrap Slider -bootstrap_slider_version = "11.0.2" -ensure_JS_dep( - f"bootstrap-slider-{bootstrap_slider_version}", - f"https://github.com/seiyria/bootstrap-slider/archive/refs/tags/v{bootstrap_slider_version}.zip", -) - -# Important: when updating the D3 version, make sure to update the constant -# D3_JS_FILE in mesa/visualization/ModularVisualization.py. -d3_version = "7.4.3" -ensure_JS_dep_single( - f"https://cdnjs.cloudflare.com/ajax/libs/d3/{d3_version}/d3.min.js", - out_name=f"d3-{d3_version}.min.js", -) -# Important: Make sure to update CHART_JS_FILE in -# mesa/visualization/ModularVisualization.py. -chartjs_version = "3.6.1" -ensure_JS_dep_single( - f"https://cdn.jsdelivr.net/npm/chart.js@{chartjs_version}/dist/chart.min.js", - out_name=f"chart-{chartjs_version}.min.js", -) - setup( name="Mesa", @@ -105,12 +58,6 @@ def ensure_JS_dep_single(url, out_name=None): url="https://github.com/projectmesa/mesa", packages=find_packages(), package_data={ - "mesa": [ - "visualization/templates/*.html", - "visualization/templates/css/*", - "visualization/templates/js/*", - "visualization/templates/external/**/*", - ], "cookiecutter-mesa": ["cookiecutter-mesa/*"], }, include_package_data=True, diff --git a/tests/test_batch_run.py b/tests/test_batch_run.py index fffd422d077..31efb87dd2d 100644 --- a/tests/test_batch_run.py +++ b/tests/test_batch_run.py @@ -1,5 +1,6 @@ +import mesa from mesa.agent import Agent -from mesa.batchrunner import _make_model_kwargs, batch_run +from mesa.batchrunner import _make_model_kwargs from mesa.datacollection import DataCollector from mesa.model import Model from mesa.time import BaseScheduler @@ -87,7 +88,7 @@ def step(self): def test_batch_run(): - result = batch_run(MockModel, {}, number_processes=2) + result = mesa.batch_run(MockModel, {}, number_processes=2) assert result == [ { "RunId": 0, @@ -120,7 +121,7 @@ def test_batch_run(): def test_batch_run_with_params(): - batch_run( + mesa.batch_run( MockModel, { "variable_model_params": range(5), @@ -131,7 +132,9 @@ def test_batch_run_with_params(): def test_batch_run_no_agent_reporters(): - result = batch_run(MockModel, {"enable_agent_reporters": False}, number_processes=2) + result = mesa.batch_run( + MockModel, {"enable_agent_reporters": False}, number_processes=2 + ) print(result) assert result == [ { @@ -145,11 +148,11 @@ def test_batch_run_no_agent_reporters(): def test_batch_run_single_core(): - batch_run(MockModel, {}, number_processes=1, iterations=10) + mesa.batch_run(MockModel, {}, number_processes=1, iterations=10) def test_batch_run_unhashable_param(): - result = batch_run( + result = mesa.batch_run( MockModel, { "n_agents": 2, diff --git a/tests/test_batchrunner.py b/tests/test_batchrunner.py deleted file mode 100644 index 2bc7dc6f9d6..00000000000 --- a/tests/test_batchrunner.py +++ /dev/null @@ -1,329 +0,0 @@ -""" -Test the BatchRunner -""" -from functools import reduce -from operator import mul -import unittest - -from mesa import Agent, Model -from mesa.time import BaseScheduler -from mesa.datacollection import DataCollector -from mesa.batchrunner import ( - BatchRunner, - FixedBatchRunner, - ParameterProduct, - ParameterSampler, -) - - -NUM_AGENTS = 7 - - -class MockAgent(Agent): - """ - Minimalistic agent implementation for testing purposes - """ - - def __init__(self, unique_id, model, val): - super().__init__(unique_id, model) - self.unique_id = unique_id - self.val = val - self.local = 0 - - def step(self): - self.val += 1 - self.local += 0.25 - - -class MockModel(Model): - """ - Minimalistic model for testing purposes - """ - - def __init__( - self, - variable_model_param=None, - variable_agent_param=None, - fixed_model_param=None, - schedule=None, - **kwargs - ): - super().__init__() - self.schedule = BaseScheduler(None) if schedule is None else schedule - self.variable_model_param = variable_model_param - self.variable_agent_param = variable_agent_param - self.fixed_model_param = fixed_model_param - self.n_agents = kwargs.get("n_agents", NUM_AGENTS) - self.datacollector = DataCollector( - model_reporters={"reported_model_param": self.get_local_model_param}, - agent_reporters={"agent_id": "unique_id", "agent_local": "local"}, - ) - self.running = True - self.init_agents() - - def init_agents(self): - if self.variable_agent_param is None: - agent_val = 1 - else: - agent_val = self.variable_agent_param - for i in range(self.n_agents): - self.schedule.add(MockAgent(i, self, agent_val)) - - def get_local_model_param(self): - return 42 - - def step(self): - self.datacollector.collect(self) - self.schedule.step() - - -class MockMixedModel(Model): - def __init__(self, **other_params): - super().__init__() - self.variable_name = other_params.get("variable_name", 42) - self.fixed_name = other_params.get("fixed_name") - self.running = True - self.schedule = BaseScheduler(None) - self.schedule.add(MockAgent(1, self, 0)) - - def step(self): - self.schedule.step() - - -class TestBatchRunner(unittest.TestCase): - """ - Test that BatchRunner is running batches - """ - - def setUp(self): - self.mock_model = MockModel - self.model_reporters = { - "reported_variable_value": lambda m: m.variable_model_param, - "reported_fixed_value": lambda m: m.fixed_model_param, - } - self.agent_reporters = {"agent_id": "unique_id", "agent_val": "val"} - self.variable_params = { - "variable_model_param": range(3), - "variable_agent_param": [1, 8], - } - self.fixed_params = None - self.iterations = 17 - self.max_steps = 3 - - def launch_batch_processing(self): - batch = BatchRunner( - self.mock_model, - variable_parameters=self.variable_params, - fixed_parameters=self.fixed_params, - iterations=self.iterations, - max_steps=self.max_steps, - model_reporters=self.model_reporters, - agent_reporters=self.agent_reporters, - ) - batch.run_all() - return batch - - def launch_batch_processing_fixed(self): - # Adding second batchrun to test fixed params increase coverage - batch = BatchRunner( - self.mock_model, - fixed_parameters={"fixed": "happy"}, - iterations=4, - max_steps=self.max_steps, - model_reporters=self.model_reporters, - agent_reporters=None, - ) - - batch.run_all() - return batch - - def launch_batch_processing_fixed_list(self): - batch = FixedBatchRunner( - self.mock_model, - parameters_list=self.variable_params, - fixed_parameters=self.fixed_params, - iterations=self.iterations, - max_steps=self.max_steps, - model_reporters=self.model_reporters, - agent_reporters=self.agent_reporters, - ) - batch.run_all() - return batch - - @property - def model_runs(self): - """ - Returns total number of batch runner's iterations. - """ - if isinstance(self.variable_params, list): - return len(self.variable_params) * self.iterations - else: - return ( - reduce(mul, map(len, self.variable_params.values())) * self.iterations - ) - - def test_model_level_vars(self): - """ - Test that model-level variable collection is of the correct size - """ - batch = self.launch_batch_processing() - model_vars = batch.get_model_vars_dataframe() - model_collector = batch.get_collector_model() - expected_cols = ( - len(self.variable_params) + len(self.model_reporters) + 1 - ) # extra column with run index - self.assertEqual(model_vars.shape, (self.model_runs, expected_cols)) - self.assertEqual(len(model_collector.keys()), self.model_runs) - for var, values in self.variable_params.items(): - self.assertEqual(set(model_vars[var].unique()), set(values)) - if self.fixed_params: - for var, values in self.fixed_params.items(): - self.assertEqual(set(model_vars[var].unique()), set(values)) - - def test_agent_level_vars(self): - """ - Test that agent-level variable collection is of the correct size - """ - batch = self.launch_batch_processing() - agent_vars = batch.get_agent_vars_dataframe() - agent_collector = batch.get_collector_agents() - # extra columns with run index and agentId - expected_cols = len(self.variable_params) + len(self.agent_reporters) + 2 - self.assertEqual( - agent_vars.shape, (self.model_runs * NUM_AGENTS, expected_cols) - ) - assert "agent_val" in list(agent_vars.columns) - assert "val_non_existent" not in list(agent_vars.columns) - assert "agent_id" in list(agent_collector[(0, 1, 1)].columns) - assert "Step" in list(agent_collector[(0, 1, 5)].index.names) - assert "nose" not in list(agent_collector[(0, 1, 1)].columns) - for var, values in self.variable_params.items(): - self.assertEqual(set(agent_vars[var].unique()), set(values)) - - self.assertEqual( - agent_collector[(0, 1, 0)].shape, (NUM_AGENTS * self.max_steps, 2) - ) - - with self.assertRaises(KeyError): - agent_collector[(900, "k", 3)] - - def test_model_with_fixed_parameters_as_kwargs(self): - """ - Test that model with fixed parameters passed like kwargs is - properly handled - """ - self.fixed_params = {"fixed_model_param": "Fixed", "n_agents": 1} - batch = self.launch_batch_processing() - model_vars = batch.get_model_vars_dataframe() - agent_vars = batch.get_agent_vars_dataframe() - self.assertEqual(len(model_vars), len(agent_vars)) - self.assertEqual(len(model_vars), self.model_runs) - self.assertEqual(model_vars["reported_fixed_value"].unique(), ["Fixed"]) - - def test_model_with_only_fixed_parameters(self): - """ - Test that model with only fixed parameters and multiple iterations is - properly handled - """ - batch = self.launch_batch_processing_fixed() - model_vars = batch.get_model_vars_dataframe() - self.assertEqual(len(model_vars), 4) - self.assertEqual(model_vars["fixed"].unique(), ["happy"]) - - with self.assertRaises(AttributeError): - batch.get_agent_vars_dataframe() - - def test_model_with_variable_and_fixed_kwargs(self): - self.mock_model = MockMixedModel - self.model_reporters = { - "reported_fixed_param": lambda m: m.fixed_name, - "reported_variable_param": lambda m: m.variable_name, - } - self.fixed_params = {"fixed_name": "Fixed"} - self.variable_params = {"variable_name": [1, 2, 3]} - batch = self.launch_batch_processing() - model_vars = batch.get_model_vars_dataframe() - expected_cols = ( - len(self.variable_params) - + len(self.fixed_params) - + len(self.model_reporters) - + 1 - ) - self.assertEqual(model_vars.shape, (self.model_runs, expected_cols)) - self.assertEqual( - model_vars["reported_fixed_param"].iloc[0], self.fixed_params["fixed_name"] - ) - - def test_model_with_variable_kwargs_list(self): - self.variable_params = [ - {"variable_model_param": 1, "variable_agent_param": 1}, - {"variable_model_param": 2, "variable_agent_param": 1}, - {"variable_model_param": 2, "variable_agent_param": 8}, - {"variable_model_param": 3, "variable_agent_param": 8}, - ] - n_params = len(self.variable_params[0]) - batch = self.launch_batch_processing_fixed_list() - - model_vars = batch.get_model_vars_dataframe() - expected_cols = n_params + len(self.model_reporters) + 1 - self.assertEqual(model_vars.shape, (self.model_runs, expected_cols)) - - agent_vars = batch.get_agent_vars_dataframe() - expected_cols = n_params + len(self.agent_reporters) + 2 - self.assertEqual( - agent_vars.shape, (self.model_runs * NUM_AGENTS, expected_cols) - ) - - def test_model_with_variable_kwargs_list_mixed_length(self): - self.variable_params = [ - {"variable_model_param": 1}, - {"variable_model_param": 2}, - {"variable_model_param": 2, "variable_agent_param": 8}, - {"variable_model_param": 3, "variable_agent_param": 8}, - {"variable_agent_param": 1}, - ] - # This is currently not supported. Check that it raises the correct error. - msg = "parameter names in parameters_list are not equal across the list" - with self.assertRaises(ValueError, msg=msg): - self.launch_batch_processing_fixed_list() - - -class TestParameters(unittest.TestCase): - def test_product(self): - params = ParameterProduct({"var_alpha": ["a", "b", "c"], "var_num": [10, 20]}) - - lp = list(params) - self.assertCountEqual( - lp, - [ - {"var_alpha": "a", "var_num": 10}, - {"var_alpha": "a", "var_num": 20}, - {"var_alpha": "b", "var_num": 10}, - {"var_alpha": "b", "var_num": 20}, - {"var_alpha": "c", "var_num": 10}, - {"var_alpha": "c", "var_num": 20}, - ], - ) - - def test_sampler(self): - params1 = ParameterSampler( - { - "var_alpha": ["a", "b", "c", "d", "e"], - "var_num": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - }, - n=10, - random_state=1, - ) - params2 = ParameterSampler( - {"var_alpha": ["a", "b", "c", "d", "e"], "var_num": range(16)}, - n=10, - random_state=1, - ) - - lp = list(params1) - self.assertEqual(10, len(lp)) - self.assertEqual(lp, list(params2)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_batchrunnerMP.py b/tests/test_batchrunnerMP.py deleted file mode 100644 index d6ff3001eae..00000000000 --- a/tests/test_batchrunnerMP.py +++ /dev/null @@ -1,271 +0,0 @@ -""" -Test the BatchRunner -""" -from functools import reduce -from operator import mul -import unittest - -from mesa import Agent, Model -from mesa.time import BaseScheduler -from mesa.datacollection import DataCollector -from mesa.batchrunner import BatchRunnerMP, ParameterProduct, ParameterSampler -from multiprocessing import freeze_support, cpu_count - -NUM_AGENTS = 7 - - -class MockAgent(Agent): - """ - Minimalistic agent implementation for testing purposes - """ - - def __init__(self, unique_id, model, val): - super().__init__(unique_id, model) - self.unique_id = unique_id - self.val = val - self.local = 0 - - def step(self): - self.val += 1 - self.local += 0.25 - - -class MockModel(Model): - """ - Minimalistic model for testing purposes - """ - - def __init__( - self, - variable_model_param, - variable_agent_param, - fixed_model_param=None, - schedule=None, - **kwargs - ): - super().__init__() - self.schedule = BaseScheduler(None) if schedule is None else schedule - self.variable_model_param = variable_model_param - self.variable_agent_param = variable_agent_param - self.fixed_model_param = fixed_model_param - self.n_agents = kwargs.get("n_agents", NUM_AGENTS) - self.datacollector = DataCollector( - model_reporters={"reported_model_param": self.get_local_model_param}, - agent_reporters={"agent_id": "unique_id", "agent_local": "local"}, - ) - self.running = True - self.init_agents() - - def get_local_model_param(self): - return 42 - - def init_agents(self): - for i in range(self.n_agents): - self.schedule.add(MockAgent(i, self, self.variable_agent_param)) - - def step(self): - self.datacollector.collect(self) - self.schedule.step() - - -class MockMixedModel(Model): - def __init__(self, **other_params): - super().__init__() - self.variable_name = other_params.get("variable_name", 42) - self.fixed_name = other_params.get("fixed_name") - self.running = True - self.schedule = BaseScheduler(None) - self.schedule.add(MockAgent(1, self, 0)) - - def step(self): - self.schedule.step() - - -class TestBatchRunnerMP(unittest.TestCase): - """ - Test that BatchRunner is running batches - """ - - def setUp(self): - self.skipTest("Disabled due to consistent hangs") - self.mock_model = MockModel - self.model_reporters = { - "reported_variable_value": lambda m: m.variable_model_param, - "reported_fixed_value": lambda m: m.fixed_model_param, - } - self.agent_reporters = {"agent_id": "unique_id", "agent_val": "val"} - self.variable_params = { - "variable_model_param": range(3), - "variable_agent_param": [1, 8], - } - self.fixed_params = None - self.iterations = 17 - self.max_steps = 3 - - def launch_batch_processing(self): - batch = BatchRunnerMP( - self.mock_model, - nr_processes=None, - variable_parameters=self.variable_params, - fixed_parameters=self.fixed_params, - iterations=self.iterations, - max_steps=self.max_steps, - model_reporters=self.model_reporters, - agent_reporters=self.agent_reporters, - ) - - batch.run_all() - return batch - - def launch_batch_processing_debug(self): - """ - Tests with one processor for debugging purposes - """ - - batch = BatchRunnerMP( - self.mock_model, - nr_processes=1, - variable_parameters=self.variable_params, - fixed_parameters=self.fixed_params, - iterations=self.iterations, - max_steps=self.max_steps, - model_reporters=self.model_reporters, - agent_reporters=self.agent_reporters, - ) - - batch.run_all() - return batch - - @property - def model_runs(self): - """ - Returns total number of batch runner's iterations. - """ - return reduce(mul, map(len, self.variable_params.values())) * self.iterations - - def batch_model_vars(self, results): - model_vars = results.get_model_vars_dataframe() - model_collector = results.get_collector_model() - expected_cols = ( - len(self.variable_params) + len(self.model_reporters) + 1 - ) # extra column with run index - self.assertEqual(model_vars.shape, (self.model_runs, expected_cols)) - self.assertEqual(len(model_collector.keys()), self.model_runs) - - def test_model_level_vars(self): - """ - Test that model-level variable collection is of the correct size - """ - batch = self.launch_batch_processing() - assert batch.processes == cpu_count() - assert batch.processes != 1 - self.batch_model_vars(batch) - - batch2 = self.launch_batch_processing_debug() - self.batch_model_vars(batch2) - - def batch_agent_vars(self, result): - agent_vars = result.get_agent_vars_dataframe() - agent_collector = result.get_collector_agents() - # extra columns with run index and agentId - expected_cols = len(self.variable_params) + len(self.agent_reporters) + 2 - assert "agent_val" in list(agent_vars.columns) - assert "val_non_existent" not in list(agent_vars.columns) - assert "agent_id" in list(agent_collector[(0, 1, 1)].columns) - assert "Step" in list(agent_collector[(0, 1, 5)].index.names) - assert "nose" not in list(agent_collector[(0, 1, 1)].columns) - - self.assertEqual( - agent_vars.shape, (self.model_runs * NUM_AGENTS, expected_cols) - ) - - self.assertEqual( - agent_collector[(0, 1, 0)].shape, (NUM_AGENTS * self.max_steps, 2) - ) - - def test_agent_level_vars(self): - """ - Test that agent-level variable collection is of the correct size - """ - batch = self.launch_batch_processing() - self.batch_agent_vars(batch) - - batch2 = self.launch_batch_processing_debug() - self.batch_agent_vars(batch2) - - def test_model_with_fixed_parameters_as_kwargs(self): - """ - Test that model with fixed parameters passed like kwargs is - properly handled - """ - self.fixed_params = {"fixed_model_param": "Fixed", "n_agents": 1} - batch = self.launch_batch_processing() - model_vars = batch.get_model_vars_dataframe() - agent_vars = batch.get_agent_vars_dataframe() - - self.assertEqual(len(model_vars), len(agent_vars)) - self.assertEqual(len(model_vars), self.model_runs) - self.assertEqual(model_vars["reported_fixed_value"].unique(), ["Fixed"]) - - def test_model_with_variable_and_fixed_kwargs(self): - self.mock_model = MockMixedModel - self.model_reporters = { - "reported_fixed_param": lambda m: m.fixed_name, - "reported_variable_param": lambda m: m.variable_name, - } - self.fixed_params = {"fixed_name": "Fixed"} - self.variable_params = {"variable_name": [1, 2, 3]} - batch = self.launch_batch_processing() - model_vars = batch.get_model_vars_dataframe() - expected_cols = ( - len(self.variable_params) - + len(self.fixed_params) - + len(self.model_reporters) - + 1 - ) - self.assertEqual(model_vars.shape, (self.model_runs, expected_cols)) - self.assertEqual( - model_vars["reported_fixed_param"].iloc[0], self.fixed_params["fixed_name"] - ) - - -class TestParameters(unittest.TestCase): - def test_product(self): - params = ParameterProduct({"var_alpha": ["a", "b", "c"], "var_num": [10, 20]}) - - lp = list(params) - self.assertCountEqual( - lp, - [ - {"var_alpha": "a", "var_num": 10}, - {"var_alpha": "a", "var_num": 20}, - {"var_alpha": "b", "var_num": 10}, - {"var_alpha": "b", "var_num": 20}, - {"var_alpha": "c", "var_num": 10}, - {"var_alpha": "c", "var_num": 20}, - ], - ) - - def test_sampler(self): - params1 = ParameterSampler( - { - "var_alpha": ["a", "b", "c", "d", "e"], - "var_num": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - }, - n=10, - random_state=1, - ) - params2 = ParameterSampler( - {"var_alpha": ["a", "b", "c", "d", "e"], "var_num": range(16)}, - n=10, - random_state=1, - ) - - lp = list(params1) - self.assertEqual(10, len(lp)) - self.assertEqual(lp, list(params2)) - - -if __name__ == "__main__": - freeze_support() - unittest.main() diff --git a/tests/test_datacollector.py b/tests/test_datacollector.py index 0f2bd4c0833..a45d8891971 100644 --- a/tests/test_datacollector.py +++ b/tests/test_datacollector.py @@ -3,7 +3,7 @@ """ import unittest -from mesa import Model, Agent +from mesa import Agent, Model from mesa.time import BaseScheduler @@ -24,6 +24,9 @@ def step(self): self.val += 1 self.val2 += 1 + def double_val(self): + return self.val * 2 + def write_final_values(self): """ Write the final value to the appropriate table. @@ -32,6 +35,18 @@ def write_final_values(self): self.model.datacollector.add_table_row("Final_Values", row) +def agent_function_with_params(agent, multiplier, offset): + return (agent.val * multiplier) + offset + + +class DifferentMockAgent(MockAgent): + # We define a different MockAgent to test for attributes that are present + # only in 1 type of agent, but not the other. + def __init__(self, unique_id, model, val=0): + super().__init__(unique_id, model, val=val) + self.val3 = val + 42 + + class MockModel(Model): """ Minimalistic model for testing purposes. @@ -43,19 +58,24 @@ def __init__(self): self.schedule = BaseScheduler(self) self.model_val = 100 - for i in range(10): - a = MockAgent(i, self, val=i) - self.schedule.add(a) + self.n = 10 + for i in range(self.n): + self.schedule.add(MockAgent(i, self, val=i)) self.initialize_data_collector( - { + model_reporters={ "total_agents": lambda m: m.schedule.get_agent_count(), "model_value": "model_val", "model_calc": self.schedule.get_agent_count, "model_calc_comp": [self.test_model_calc_comp, [3, 4]], "model_calc_fail": [self.test_model_calc_comp, [12, 0]], }, - {"value": lambda a: a.val, "value2": "val2"}, - {"Final_Values": ["agent_id", "final_value"]}, + agent_reporters={ + "value": lambda a: a.val, + "value2": "val2", + "double_value": MockAgent.double_val, + "value_with_params": [agent_function_with_params, [2, 3]], + }, + tables={"Final_Values": ["agent_id", "final_value"]}, ) def test_model_calc_comp(self, input1, input2): @@ -123,6 +143,19 @@ def test_agent_records(self): data_collector = self.model.datacollector agent_table = data_collector.get_agent_vars_dataframe() + assert "double_value" in list(agent_table.columns) + assert "value_with_params" in list(agent_table.columns) + + # Check the double_value column + for (step, agent_id), value in agent_table["double_value"].items(): + expected_value = (step + agent_id) * 2 + self.assertEqual(value, expected_value) + + # Check the value_with_params column + for (step, agent_id), value in agent_table["value_with_params"].items(): + expected_value = ((step + agent_id) * 2) + 3 + self.assertEqual(value, expected_value) + assert len(data_collector._agent_records) == 8 for step, records in data_collector._agent_records.items(): if step < 5: @@ -131,7 +164,7 @@ def test_agent_records(self): assert len(records) == 9 for values in records: - assert len(values) == 4 + assert len(values) == 6 assert "value" in list(agent_table.columns) assert "value2" in list(agent_table.columns) @@ -148,7 +181,7 @@ def test_table_rows(self): assert len(data_collector.tables["Final_Values"]) == 2 assert "agent_id" in data_collector.tables["Final_Values"] assert "final_value" in data_collector.tables["Final_Values"] - for key, data in data_collector.tables["Final_Values"].items(): + for _key, data in data_collector.tables["Final_Values"].items(): assert len(data) == 9 with self.assertRaises(Exception): @@ -166,7 +199,7 @@ def test_exports(self): agent_vars = data_collector.get_agent_vars_dataframe() table_df = data_collector.get_table_dataframe("Final_Values") assert model_vars.shape == (8, 5) - assert agent_vars.shape == (77, 2) + assert agent_vars.shape == (77, 4) assert table_df.shape == (9, 2) with self.assertRaises(Exception): diff --git a/tests/test_examples.py b/tests/test_examples.py index 9baa31c8b55..1c149da4b75 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,14 +1,17 @@ -import sys -import os.path -import unittest import contextlib import importlib +import os.path +import sys +import unittest def classcase(name): return "".join(x.capitalize() for x in name.replace("-", "_").split("_")) +@unittest.skip( + "Skipping TextExamples, because examples folder was moved. More discussion needed." +) class TestExamples(unittest.TestCase): """ Test examples' models. This creates a model object and iterates it through @@ -59,7 +62,7 @@ def test_examples(self): f"{example.replace('-', '_')}.server" ) server.server.render_model() - Model = getattr(mod, classcase(example)) - model = Model() + model_class = getattr(mod, classcase(example)) + model = model_class() for _ in range(10): model.step() diff --git a/tests/test_grid.py b/tests/test_grid.py index 3104ba2e68e..686d8e78b59 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -3,8 +3,9 @@ """ import random import unittest -from unittest.mock import patch, Mock -from mesa.space import Grid, SingleGrid, MultiGrid, HexGrid +from unittest.mock import Mock, patch + +from mesa.space import HexSingleGrid, MultiGrid, SingleGrid # Initial agent positions for testing # @@ -15,7 +16,7 @@ # 1 0 1 # 0 0 1 # ------------------- -TEST_GRID = [[0, 1, 0, 1, 0], [0, 0, 1, 1, 0], [1, 1, 0, 0, 0]] +TEST_GRID = [[0, 1, 0, 1, 0, 0], [0, 0, 1, 1, 0, 1], [1, 1, 0, 0, 0, 1]] class MockAgent: @@ -29,9 +30,9 @@ def __init__(self, unique_id, pos): self.pos = pos -class TestBaseGrid(unittest.TestCase): +class TestSingleGrid(unittest.TestCase): """ - Testing a non-toroidal grid. + Testing a non-toroidal singlegrid. """ torus = False @@ -40,9 +41,10 @@ def setUp(self): """ Create a test non-toroidal grid and populate it with Mock Agents """ + # The height needs to be even to test the edge case described in PR #1517 + height = 6 # height of grid width = 3 # width of grid - height = 5 # height of grid - self.grid = Grid(width, height, self.torus) + self.grid = SingleGrid(width, height, self.torus) self.agents = [] counter = 0 for x in range(width): @@ -109,25 +111,22 @@ def test_neighbors(self): assert len(neighborhood) == 8 neighborhood = self.grid.get_neighborhood((1, 4), moore=False) - assert len(neighborhood) == 3 + assert len(neighborhood) == 4 neighborhood = self.grid.get_neighborhood((1, 4), moore=True) - assert len(neighborhood) == 5 + assert len(neighborhood) == 8 neighborhood = self.grid.get_neighborhood((0, 0), moore=False) assert len(neighborhood) == 2 - neighbors = self.grid.get_neighbors((4, 1), moore=False) - assert len(neighbors) == 0 - - neighbors = self.grid.get_neighbors((4, 1), moore=True) - assert len(neighbors) == 0 + with self.assertRaises(Exception): + neighbors = self.grid.get_neighbors((4, 1), moore=False) neighbors = self.grid.get_neighbors((1, 1), moore=False, include_center=True) assert len(neighbors) == 3 neighbors = self.grid.get_neighbors((1, 3), moore=False, radius=2) - assert len(neighbors) == 2 + assert len(neighbors) == 3 def test_coord_iter(self): ci = self.grid.coord_iter() @@ -135,44 +134,71 @@ def test_coord_iter(self): # no agent in first space first = next(ci) assert first[0] is None - assert first[1] == 0 - assert first[2] == 0 + assert first[1] == (0, 0) # first agent in the second space second = next(ci) assert second[0].unique_id == 1 assert second[0].pos == (0, 1) - assert second[1] == 0 - assert second[2] == 1 + assert second[1] == (0, 1) def test_agent_move(self): # get the agent at [0, 1] agent = self.agents[0] - self.grid.move_agent(agent, (1, 1)) - assert agent.pos == (1, 1) + self.grid.move_agent(agent, (1, 0)) + assert agent.pos == (1, 0) # move it off the torus and check for the exception - if not self.torus: + if not self.grid.torus: with self.assertRaises(Exception): self.grid.move_agent(agent, [-1, 1]) with self.assertRaises(Exception): self.grid.move_agent(agent, [1, self.grid.height + 1]) else: - self.grid.move_agent(agent, [-1, 1]) - assert agent.pos == (self.grid.width - 1, 1) - self.grid.move_agent(agent, [1, self.grid.height + 1]) - assert agent.pos == (1, 1) + self.grid.move_agent(agent, [0, -1]) + assert agent.pos == (0, self.grid.height - 1) + self.grid.move_agent(agent, [1, self.grid.height]) + assert agent.pos == (1, 0) def test_agent_remove(self): agent = self.agents[0] x, y = agent.pos self.grid.remove_agent(agent) assert agent.pos is None - assert self.grid.grid[x][y] is None + assert self.grid[x][y] is None + + def test_swap_pos(self): + # Swap agents positions + agent_a, agent_b = list(filter(None, self.grid))[:2] + pos_a = agent_a.pos + pos_b = agent_b.pos + + self.grid.swap_pos(agent_a, agent_b) + + assert agent_a.pos == pos_b + assert agent_b.pos == pos_a + assert self.grid[pos_a] == agent_b + assert self.grid[pos_b] == agent_a + + # Swap the same agents + self.grid.swap_pos(agent_a, agent_a) + + assert agent_a.pos == pos_b + assert self.grid[pos_b] == agent_a + + # Raise for agents not on the grid + self.grid.remove_agent(agent_a) + self.grid.remove_agent(agent_b) + + id_a = agent_a.unique_id + id_b = agent_b.unique_id + e_message = f", - not on the grid" + with self.assertRaisesRegex(Exception, e_message): + self.grid.swap_pos(agent_a, agent_b) -class TestBaseGridTorus(TestBaseGrid): +class TestSingleGridTorus(TestSingleGrid): """ - Testing the toroidal base grid. + Testing the toroidal singlegrid. """ torus = True @@ -191,25 +217,30 @@ def test_neighbors(self): neighborhood = self.grid.get_neighborhood((0, 0), moore=False) assert len(neighborhood) == 4 + # here we test the edge case described in PR #1517 using a radius + # measuring half of the grid height + neighborhood = self.grid.get_neighborhood((0, 0), moore=True, radius=3) + assert len(neighborhood) == 17 + + neighborhood = self.grid.get_neighborhood((1, 1), moore=False, radius=3) + assert len(neighborhood) == 15 + neighbors = self.grid.get_neighbors((1, 4), moore=False) - assert len(neighbors) == 1 + assert len(neighbors) == 2 neighbors = self.grid.get_neighbors((1, 4), moore=True) - assert len(neighbors) == 3 + assert len(neighbors) == 4 neighbors = self.grid.get_neighbors((1, 1), moore=False, include_center=True) assert len(neighbors) == 3 neighbors = self.grid.get_neighbors((1, 3), moore=False, radius=2) - assert len(neighbors) == 2 + assert len(neighbors) == 3 -class TestSingleGrid(unittest.TestCase): +class TestSingleGridEnforcement(unittest.TestCase): """ - Test the SingleGrid object. - - Since it inherits from Grid, all the functionality tested above should - work here too. Instead, this tests the enforcement. + Test the enforcement in SingleGrid. """ def setUp(self): @@ -245,58 +276,28 @@ def test_enforcement(self, mock_model): # Place the agent in an empty cell mock_model.schedule.get_agent_count = Mock(side_effect=lambda: len(self.agents)) - self.grid.position_agent(a) + self.grid.move_to_empty(a) self.num_agents += 1 # Test whether after placing, the empty cells are reduced by 1 assert a.pos not in self.grid.empties assert len(self.grid.empties) == 8 - for i in range(10): - # Since the agents and the grid are not associated with a model, we - # must explicitly tell move_to_empty the number of agents. - self.grid.move_to_empty(a, num_agents=self.num_agents) + for _i in range(10): + self.grid.move_to_empty(a) assert len(self.grid.empties) == 8 # Place agents until the grid is full empty_cells = len(self.grid.empties) for i in range(empty_cells): a = MockAgent(101 + i, None) - self.grid.position_agent(a) + self.grid.move_to_empty(a) self.num_agents += 1 assert len(self.grid.empties) == 0 a = MockAgent(110, None) with self.assertRaises(Exception): - self.grid.position_agent(a) + self.grid.move_to_empty(a) with self.assertRaises(Exception): - self.move_to_empty(self.agents[0], num_agents=self.num_agents) - - # Swap agents positions - agent_a, agent_b = random.sample(list(self.grid), k=2) - pos_a = agent_a.pos - pos_b = agent_b.pos - - self.grid.swap_pos(agent_a, agent_b) - - assert agent_a.pos == pos_b - assert agent_b.pos == pos_a - assert self.grid[pos_a] == agent_b - assert self.grid[pos_b] == agent_a - - # Swap the same agents - self.grid.swap_pos(agent_a, agent_a) - - assert agent_a.pos == pos_b - assert self.grid[pos_b] == agent_a - - # Raise for agents not on the grid - self.grid.remove_agent(agent_a) - self.grid.remove_agent(agent_b) - - id_a = agent_a.unique_id - id_b = agent_b.unique_id - e_message = f", - not on the grid" - with self.assertRaisesRegex(Exception, e_message): - self.grid.swap_pos(agent_a, agent_b) + self.move_to_empty(self.agents[0]) # Number of agents at each position for testing @@ -330,7 +331,7 @@ def setUp(self): counter = 0 for x in range(width): for y in range(height): - for i in range(TEST_MULTIGRID[x][y]): + for _i in range(TEST_MULTIGRID[x][y]): counter += 1 # Create and place the mock agent a = MockAgent(counter, None) @@ -372,9 +373,9 @@ def test_neighbors(self): assert len(neighbors) == 11 -class TestHexGrid(unittest.TestCase): +class TestHexSingleGrid(unittest.TestCase): """ - Testing a hexagonal grid. + Testing a hexagonal singlegrid. """ def setUp(self): @@ -383,7 +384,7 @@ def setUp(self): """ width = 3 height = 5 - self.grid = HexGrid(width, height, torus=False) + self.grid = HexSingleGrid(width, height, torus=False) self.agents = [] counter = 0 for x in range(width): @@ -400,7 +401,6 @@ def test_neighbors(self): """ Test the hexagonal neighborhood methods on the non-toroid. """ - neighborhood = self.grid.get_neighborhood((1, 1)) assert len(neighborhood) == 6 @@ -427,20 +427,18 @@ def test_neighbors(self): assert sum(x + y for x, y in neighborhood) == 39 -class TestHexGridTorus(TestBaseGrid): +class TestHexSingleGridTorus(TestSingleGrid): """ - Testing a hexagonal toroidal grid. + Testing a hexagonal toroidal singlegrid. """ - torus = True - def setUp(self): """ Create a test non-toroidal grid and populate it with Mock Agents """ width = 3 height = 5 - self.grid = HexGrid(width, height, torus=True) + self.grid = HexSingleGrid(width, height, torus=True) self.agents = [] counter = 0 for x in range(width): @@ -457,7 +455,6 @@ def test_neighbors(self): """ Test the hexagonal neighborhood methods on the toroid. """ - neighborhood = self.grid.get_neighborhood((1, 1)) assert len(neighborhood) == 6 @@ -480,9 +477,10 @@ def test_neighbors(self): class TestIndexing: # Create a grid where the content of each coordinate is a tuple of its coordinates - grid = Grid(3, 5, True) - for _, x, y in grid.coord_iter(): - grid.grid[x][y] = (x, y) + grid = SingleGrid(3, 5, True) + for _, pos in grid.coord_iter(): + x, y = pos + grid._grid[x][y] = pos def test_int(self): assert self.grid[0][0] == (0, 0) diff --git a/tests/test_import_namespace.py b/tests/test_import_namespace.py index e405dc8f710..f9489711004 100644 --- a/tests/test_import_namespace.py +++ b/tests/test_import_namespace.py @@ -3,32 +3,31 @@ def test_import(): # https://github.com/projectmesa/mesa/pull/1294. import mesa import mesa.flat as mf - from mesa.time import RandomActivation - mesa.time.RandomActivation - RandomActivation - mf.RandomActivation + _ = mesa.time.RandomActivation + _ = RandomActivation + _ = mf.RandomActivation from mesa.space import MultiGrid - mesa.space.MultiGrid - MultiGrid - mf.MultiGrid + _ = mesa.space.MultiGrid + _ = MultiGrid + _ = mf.MultiGrid from mesa.visualization.ModularVisualization import ModularServer - mesa.visualization.ModularServer - ModularServer - mf.ModularServer + _ = mesa.visualization.ModularServer + _ = ModularServer + _ = mf.ModularServer from mesa.datacollection import DataCollector - DataCollector - mesa.DataCollector - mf.DataCollector + _ = DataCollector + _ = mesa.DataCollector + _ = mf.DataCollector from mesa.batchrunner import batch_run - batch_run - mesa.batch_run + _ = batch_run + _ = mesa.batch_run diff --git a/tests/test_jupyter_viz.py b/tests/test_jupyter_viz.py new file mode 100644 index 00000000000..9702125229f --- /dev/null +++ b/tests/test_jupyter_viz.py @@ -0,0 +1,132 @@ +import unittest +from unittest.mock import Mock, patch + +import ipyvuetify as vw +import solara + +from mesa.experimental.jupyter_viz import JupyterViz, UserInputs + + +class TestMakeUserInput(unittest.TestCase): + def test_unsupported_type(self): + @solara.component + def Test(user_params): + UserInputs(user_params) + + """unsupported input type should raise ValueError""" + # bogus type + with self.assertRaisesRegex(ValueError, "not a supported input type"): + solara.render(Test({"mock": {"type": "bogus"}}), handle_error=False) + + # no type is specified + with self.assertRaisesRegex(ValueError, "not a supported input type"): + solara.render(Test({"mock": {}}), handle_error=False) + + def test_slider_int(self): + @solara.component + def Test(user_params): + UserInputs(user_params) + + options = { + "type": "SliderInt", + "value": 10, + "label": "number of agents", + "min": 10, + "max": 20, + "step": 1, + } + user_params = {"num_agents": options} + _, rc = solara.render(Test(user_params), handle_error=False) + slider_int = rc.find(vw.Slider).widget + + assert slider_int.v_model == options["value"] + assert slider_int.label == options["label"] + assert slider_int.min == options["min"] + assert slider_int.max == options["max"] + assert slider_int.step == options["step"] + + def test_checkbox(self): + @solara.component + def Test(user_params): + UserInputs(user_params) + + options = {"type": "Checkbox", "value": True, "label": "On"} + user_params = {"num_agents": options} + _, rc = solara.render(Test(user_params), handle_error=False) + checkbox = rc.find(vw.Checkbox).widget + + assert checkbox.v_model == options["value"] + assert checkbox.label == options["label"] + + def test_label_fallback(self): + """name should be used as fallback label""" + + @solara.component + def Test(user_params): + UserInputs(user_params) + + options = { + "type": "SliderInt", + "value": 10, + } + + user_params = {"num_agents": options} + _, rc = solara.render(Test(user_params), handle_error=False) + slider_int = rc.find(vw.Slider).widget + + assert slider_int.v_model == options["value"] + assert slider_int.label == "num_agents" + assert slider_int.min is None + assert slider_int.max is None + assert slider_int.step is None + + +class TestJupyterViz(unittest.TestCase): + @patch("mesa.experimental.jupyter_viz.make_space") + def test_call_space_drawer(self, mock_make_space): + mock_model_class = Mock() + agent_portrayal = { + "Shape": "circle", + "color": "gray", + } + # initialize with space drawer unspecified (use default) + # component must be rendered for code to run + solara.render( + JupyterViz( + model_class=mock_model_class, + model_params={}, + agent_portrayal=agent_portrayal, + ) + ) + # should call default method with class instance and agent portrayal + mock_make_space.assert_called_with( + mock_model_class.return_value, agent_portrayal + ) + + # specify no space should be drawn; any false value should work + for falsy_value in [None, False, 0]: + mock_make_space.reset_mock() + solara.render( + JupyterViz( + model_class=mock_model_class, + model_params={}, + agent_portrayal=agent_portrayal, + space_drawer=falsy_value, + ) + ) + # should call default method with class instance and agent portrayal + assert mock_make_space.call_count == 0 + + # specify a custom space method + altspace_drawer = Mock() + solara.render( + JupyterViz( + model_class=mock_model_class, + model_params={}, + agent_portrayal=agent_portrayal, + space_drawer=altspace_drawer, + ) + ) + altspace_drawer.assert_called_with( + mock_model_class.return_value, agent_portrayal + ) diff --git a/tests/test_lifespan.py b/tests/test_lifespan.py index 280b8beeb83..cfd60cdeb74 100644 --- a/tests/test_lifespan.py +++ b/tests/test_lifespan.py @@ -1,10 +1,11 @@ import unittest -from mesa.time import RandomActivation -from mesa.datacollection import DataCollector -from mesa import Model, Agent import numpy as np +from mesa import Agent, Model +from mesa.datacollection import DataCollector +from mesa.time import RandomActivation + class LifeTimeModel(Model): """Simple model for running models with a finite life""" diff --git a/tests/test_main.py b/tests/test_main.py index 2d6f0cac5ac..9f7f9834626 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,6 +2,7 @@ import sys import unittest from unittest.mock import patch + from click.testing import CliRunner from mesa.main import cli @@ -19,8 +20,11 @@ def setUp(self): def tearDown(self): sys.path[:] = self.old_sys_path + @unittest.skip( + "Skipping test_run, because examples folder was moved. More discussion needed." + ) def test_run(self): - with patch("mesa.visualization.ModularServer") as ModularServer: + with patch("mesa.visualization.ModularServer") as ModularServer: # noqa: N806 example_dir = os.path.abspath( os.path.join(os.path.dirname(__file__), "../examples/wolf_sheep") ) diff --git a/tests/test_scaffold.py b/tests/test_scaffold.py index b2ed1130552..627552eb0eb 100644 --- a/tests/test_scaffold.py +++ b/tests/test_scaffold.py @@ -1,5 +1,6 @@ -import unittest import os +import unittest + from click.testing import CliRunner from mesa.main import cli diff --git a/tests/test_space.py b/tests/test_space.py index 769214cf0c5..f8f2cc9440c 100644 --- a/tests/test_space.py +++ b/tests/test_space.py @@ -4,12 +4,9 @@ import numpy as np import pytest -from mesa.space import ContinuousSpace -from mesa.space import SingleGrid -from mesa.space import NetworkGrid +from mesa.space import ContinuousSpace, NetworkGrid, SingleGrid from tests.test_grid import MockAgent - TEST_AGENTS = [(-20, -20), (-20, -20.05), (65, 18)] TEST_AGENTS_GRID = [(1, 1), (10, 0), (10, 10)] TEST_AGENTS_NETWORK_SINGLE = [0, 1, 5] @@ -302,18 +299,16 @@ def test_remove_agent(self): for i, pos in enumerate(TEST_AGENTS_GRID): a = self.agents[i] assert a.pos == pos - assert self.space.grid[pos[0]][pos[1]] == a + assert self.space[pos[0]][pos[1]] == a self.space.remove_agent(a) assert a.pos is None - assert self.space.grid[pos[0]][pos[1]] is None + assert self.space[pos[0]][pos[1]] is None def test_empty_cells(self): if self.space.exists_empty_cells(): - pytest.deprecated_call(self.space.find_empty) for i, pos in enumerate(list(self.space.empties)): a = MockAgent(-i, pos) - self.space.position_agent(a, x=pos[0], y=pos[1]) - assert self.space.find_empty() is None + self.space.place_agent(a, pos) with self.assertRaises(Exception): self.space.move_to_empty(a) @@ -325,12 +320,34 @@ def move_agent(self): _agent = self.agents[agent_number] assert _agent.pos == initial_pos - assert self.space.grid[initial_pos[0]][initial_pos[1]] == _agent - assert self.space.grid[final_pos[0]][final_pos[1]] is None + assert self.space[initial_pos[0]][initial_pos[1]] == _agent + assert self.space[final_pos[0]][final_pos[1]] is None self.space.move_agent(_agent, final_pos) assert _agent.pos == final_pos - assert self.space.grid[initial_pos[0]][initial_pos[1]] is None - assert self.space.grid[final_pos[0]][final_pos[1]] == _agent + assert self.space[initial_pos[0]][initial_pos[1]] is None + assert self.space[final_pos[0]][final_pos[1]] == _agent + + def test_iter_cell_list_contents(self): + """ + Test neighborhood retrieval + """ + cell_list_1 = list(self.space.iter_cell_list_contents(TEST_AGENTS_GRID[0])) + assert len(cell_list_1) == 1 + + cell_list_2 = list( + self.space.iter_cell_list_contents( + (TEST_AGENTS_GRID[0], TEST_AGENTS_GRID[1]) + ) + ) + assert len(cell_list_2) == 2 + + cell_list_3 = list(self.space.iter_cell_list_contents(tuple(TEST_AGENTS_GRID))) + assert len(cell_list_3) == 3 + + cell_list_4 = list( + self.space.iter_cell_list_contents((TEST_AGENTS_GRID[0], (0, 0))) + ) + assert len(cell_list_4) == 1 class TestSingleNetworkGrid(unittest.TestCase): @@ -340,7 +357,7 @@ def setUp(self): """ Create a test network grid and populate with Mock Agents. """ - G = nx.complete_graph(TestSingleNetworkGrid.GRAPH_SIZE) + G = nx.cycle_graph(TestSingleNetworkGrid.GRAPH_SIZE) # noqa: N806 self.space = NetworkGrid(G) self.agents = [] for i, pos in enumerate(TEST_AGENTS_NETWORK_SINGLE): @@ -357,14 +374,10 @@ def test_agent_positions(self): assert a.pos == pos def test_get_neighbors(self): - assert ( - len(self.space.get_neighbors(0, include_center=True)) - == TestSingleNetworkGrid.GRAPH_SIZE - ) - assert ( - len(self.space.get_neighbors(0, include_center=False)) - == TestSingleNetworkGrid.GRAPH_SIZE - 1 - ) + assert len(self.space.get_neighborhood(0, include_center=True)) == 3 + assert len(self.space.get_neighborhood(0, include_center=False)) == 2 + assert len(self.space.get_neighborhood(2, include_center=True, radius=3)) == 7 + assert len(self.space.get_neighborhood(2, include_center=False, radius=3)) == 6 def test_move_agent(self): initial_pos = 1 @@ -415,7 +428,7 @@ def setUp(self): """ Create a test network grid and populate with Mock Agents. """ - G = nx.complete_graph(TestMultipleNetworkGrid.GRAPH_SIZE) + G = nx.complete_graph(TestMultipleNetworkGrid.GRAPH_SIZE) # noqa: N806 self.space = NetworkGrid(G) self.agents = [] for i, pos in enumerate(TEST_AGENTS_NETWORK_MULTIPLE): @@ -433,11 +446,11 @@ def test_agent_positions(self): def test_get_neighbors(self): assert ( - len(self.space.get_neighbors(0, include_center=True)) + len(self.space.get_neighborhood(0, include_center=True)) == TestMultipleNetworkGrid.GRAPH_SIZE ) assert ( - len(self.space.get_neighbors(0, include_center=False)) + len(self.space.get_neighborhood(0, include_center=False)) == TestMultipleNetworkGrid.GRAPH_SIZE - 1 ) diff --git a/tests/test_time.py b/tests/test_time.py index d92ae04c37c..60dbf506658 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -4,13 +4,14 @@ import unittest from unittest import TestCase, mock -from mesa import Model, Agent + +from mesa import Agent, Model from mesa.time import ( BaseScheduler, - StagedActivation, RandomActivation, - SimultaneousActivation, RandomActivationByType, + SimultaneousActivation, + StagedActivation, ) RANDOM = "random" @@ -29,7 +30,15 @@ def __init__(self, unique_id, model): self.steps = 0 self.advances = 0 + def kill_other_agent(self): + for agent in self.model.schedule.agents: + if agent is not self: + self.model.schedule.remove(agent) + break + def stage_one(self): + if self.model.enable_kill_other_agent: + self.kill_other_agent() self.model.log.append(self.unique_id + "_1") def stage_two(self): @@ -39,11 +48,14 @@ def advance(self): self.advances += 1 def step(self): + if self.model.enable_kill_other_agent: + self.kill_other_agent() self.steps += 1 + self.model.log.append(self.unique_id) class MockModel(Model): - def __init__(self, shuffle=False, activation=STAGED): + def __init__(self, shuffle=False, activation=STAGED, enable_kill_other_agent=False): """ Creates a Model instance with a schedule @@ -59,10 +71,11 @@ def __init__(self, shuffle=False, activation=STAGED): The default scheduler is a BaseScheduler. """ self.log = [] + self.enable_kill_other_agent = enable_kill_other_agent # Make scheduler if activation == STAGED: - model_stages = ["stage_one", "stage_two"] + model_stages = ["stage_one", "model.model_stage", "stage_two"] self.schedule = StagedActivation(self, model_stages, shuffle=shuffle) elif activation == RANDOM: self.schedule = RandomActivation(self) @@ -81,33 +94,37 @@ def __init__(self, shuffle=False, activation=STAGED): def step(self): self.schedule.step() + def model_stage(self): + self.log.append("model_stage") + class TestStagedActivation(TestCase): """ Test the staged activation. """ - expected_output = ["A_1", "B_1", "A_2", "B_2"] + expected_output = ["A_1", "B_1", "model_stage", "A_2", "B_2"] def test_no_shuffle(self): """ - Testing staged activation without shuffling. + Testing the staged activation without shuffling. """ model = MockModel(shuffle=False) model.step() model.step() - assert all([i == j for i, j in zip(model.log[:4], model.log[4:])]) + assert all(i == j for i, j in zip(model.log[:5], model.log[5:])) def test_shuffle(self): """ - Test staged activation with shuffling + Test the staged activation with shuffling """ model = MockModel(shuffle=True) model.step() for output in self.expected_output[:2]: assert output in model.log[:2] - for output in self.expected_output[2:]: - assert output in model.log[2:] + for output in self.expected_output[3:]: + assert output in model.log[3:] + assert self.expected_output[2] == model.log[2] def test_shuffle_shuffles_agents(self): model = MockModel(shuffle=True) @@ -118,7 +135,7 @@ def test_shuffle_shuffles_agents(self): def test_remove(self): """ - Test staged activation can remove an agent + Test the staged activation can remove an agent """ model = MockModel(shuffle=True) agent_keys = list(model.schedule._agents.keys()) @@ -126,6 +143,15 @@ def test_remove(self): model.schedule.remove(agent) assert agent not in model.schedule.agents + def test_intrastep_remove(self): + """ + Test the staged activation can remove an agent in a + step of another agent so that the one removed doesn't step. + """ + model = MockModel(shuffle=True, enable_kill_other_agent=True) + model.step() + assert len(model.log) == 3 + def test_add_existing_agent(self): model = MockModel() agent = model.schedule.agents[0] @@ -162,12 +188,20 @@ def test_random_activation_step_steps_each_agent(self): """ Test the random activation step causes each agent to step """ - model = MockModel(activation=RANDOM) model.step() agent_steps = [i.steps for i in model.schedule.agents] # one step for each of 2 agents - assert all(map(lambda x: x == 1, agent_steps)) + assert all(x == 1 for x in agent_steps) + + def test_intrastep_remove(self): + """ + Test the random activation can remove an agent in a + step of another agent so that the one removed doesn't step. + """ + model = MockModel(activation=RANDOM, enable_kill_other_agent=True) + model.step() + assert len(model.log) == 1 class TestSimultaneousActivation(TestCase): @@ -184,8 +218,8 @@ def test_simultaneous_activation_step_steps_and_advances_each_agent(self): # one step for each of 2 agents agent_steps = [i.steps for i in model.schedule.agents] agent_advances = [i.advances for i in model.schedule.agents] - assert all(map(lambda x: x == 1, agent_steps)) - assert all(map(lambda x: x == 1, agent_advances)) + assert all(x == 1 for x in agent_steps) + assert all(x == 1 for x in agent_advances) class TestRandomActivationByType(TestCase): @@ -224,7 +258,7 @@ def test_random_activation_step_steps_each_agent(self): model.step() agent_steps = [i.steps for i in model.schedule.agents] # one step for each of 2 agents - assert all(map(lambda x: x == 1, agent_steps)) + assert all(x == 1 for x in agent_steps) def test_add_non_unique_ids(self): """ @@ -233,8 +267,8 @@ def test_add_non_unique_ids(self): RandomActivationByType. """ model = MockModel(activation=RANDOM_BY_TYPE) - a = MockAgent(0, None) - b = MockAgent(0, None) + a = MockAgent(0, model) + b = MockAgent(0, model) model.schedule.add(a) with self.assertRaises(Exception): model.schedule.add(b) diff --git a/tests/test_tornado.py b/tests/test_tornado.py index e0f50cf52f3..24025a586be 100644 --- a/tests/test_tornado.py +++ b/tests/test_tornado.py @@ -1,8 +1,10 @@ -from tornado.testing import AsyncHTTPTestCase +import json + import tornado +from tornado.testing import AsyncHTTPTestCase + from mesa import Model from mesa.visualization.ModularVisualization import ModularServer -import json class TestServer(AsyncHTTPTestCase): diff --git a/tests/test_usersettableparam.py b/tests/test_usersettableparam.py index 6dc52774438..8f02fa4b0b6 100644 --- a/tests/test_usersettableparam.py +++ b/tests/test_usersettableparam.py @@ -1,65 +1,55 @@ from unittest import TestCase + from mesa.visualization.UserParam import ( - UserSettableParameter, - Slider, Checkbox, Choice, - StaticText, NumberInput, + Slider, + StaticText, ) class TestOption(TestCase): def setUp(self): - self.number_option = UserSettableParameter("number", value=123) - self.number_option_standalone = NumberInput("number", value=123) - self.checkbox_option = UserSettableParameter("checkbox", value=True) - self.checkbox_option_standalone = Checkbox(value=True) - self.choice_option = UserSettableParameter( - "choice", + self.number_option = NumberInput("number", value=123) + self.checkbox_option = Checkbox(value=True) + self.choice_option = Choice( value="I am your default choice", choices=["I am your default choice", "I am your other choice"], ) - self.choice_option_standalone = Choice( - value="I am your default choice", - choices=["I am your default choice", "I am your other choice"], - ) - self.slider_option = UserSettableParameter( - "slider", value=123, min_value=100, max_value=200 - ) - self.slider_option_standalone = Slider(value=123, min_value=100, max_value=200) + self.slider_option = Slider(value=123, min_value=100, max_value=200) self.static_text_option = StaticText("Hurr, Durr Im'a Sheep") def test_number(self): - for option in [self.number_option, self.number_option_standalone]: - assert option.value == 123 - option.value = 321 - assert option.value == 321 + option = self.number_option + assert option.value == 123 + option.value = 321 + assert option.value == 321 def test_checkbox(self): - for option in [self.checkbox_option, self.checkbox_option_standalone]: - assert option.value - option.value = False - assert not option.value + option = self.checkbox_option + assert option.value + option.value = False + assert not option.value def test_choice(self): - for option in [self.choice_option, self.choice_option_standalone]: - assert option.value == "I am your default choice" - option.value = "I am your other choice" - assert option.value == "I am your other choice" - option.value = "I am not an available choice" - assert option.value == "I am your default choice" + option = self.choice_option + assert option.value == "I am your default choice" + option.value = "I am your other choice" + assert option.value == "I am your other choice" + option.value = "I am not an available choice" + assert option.value == "I am your default choice" def test_slider(self): - for option in [self.slider_option, self.slider_option_standalone]: - assert option.value == 123 - option.value = 150 - assert option.value == 150 - option.value = 0 - assert option.value == 100 - option.value = 300 - assert option.value == 200 - assert option.json["value"] == 200 + option = self.slider_option + assert option.value == 123 + option.value = 150 + assert option.value == 150 + option.value = 0 + assert option.value == 100 + option.value = 300 + assert option.value == 200 + assert option.json["value"] == 200 with self.assertRaises(ValueError): Slider() diff --git a/tests/test_visualization.py b/tests/test_visualization.py index 3f33bee92fe..55e53cdb7a0 100644 --- a/tests/test_visualization.py +++ b/tests/test_visualization.py @@ -1,29 +1,47 @@ -from unittest import TestCase from collections import defaultdict +from unittest import TestCase +import mesa from mesa.model import Model -from mesa.space import Grid +from mesa.space import MultiGrid from mesa.time import SimultaneousActivation from mesa.visualization.ModularVisualization import ModularServer from mesa.visualization.modules import CanvasGrid, TextElement -from mesa.visualization.UserParam import UserSettableParameter +from mesa.visualization.UserParam import ( + NumberInput, + Slider, +) + -from tests.test_batchrunner import MockAgent +class MockAgent(mesa.Agent): + """ + Minimalistic agent implementation for testing purposes + """ + + def __init__(self, unique_id, model, val): + super().__init__(unique_id, model) + self.unique_id = unique_id + self.val = val + self.local = 0 + + def step(self): + self.val += 1 + self.local += 0.25 class MockModel(Model): """Test model for testing""" def __init__(self, width, height, key1=103, key2=104): - self.width = width self.height = height self.key1 = (key1,) self.key2 = key2 self.schedule = SimultaneousActivation(self) - self.grid = Grid(width, height, torus=True) + self.grid = MultiGrid(width, height, torus=True) - for (c, x, y) in self.grid.coord_iter(): + for _c, pos in self.grid.coord_iter(): + x, y = pos a = MockAgent(x + y * 100, self, x * y * 3) self.grid.place_agent(a, (x, y)) self.schedule.add(a) @@ -48,12 +66,11 @@ def portrayal(self, cell): } def setUp(self): - self.user_params = { "width": 1, "height": 1, - "key1": UserSettableParameter("number", "Test Parameter", 101), - "key2": UserSettableParameter("slider", "Test Parameter", 200, 0, 300, 10), + "key1": NumberInput("Test Parameter", 101), + "key2": Slider("Test Parameter", 200, 0, 300, 10), } self.viz_elements = [ @@ -68,7 +85,6 @@ def setUp(self): ) def test_canvas_render_model_state(self): - test_portrayal = self.portrayal(None) test_grid_state = defaultdict(list) test_grid_state[test_portrayal["Layer"]].append(test_portrayal) @@ -83,8 +99,6 @@ def test_text_render_model_state(self): def test_user_params(self): print(self.server.user_params) assert self.server.user_params == { - "key1": UserSettableParameter("number", "Test Parameter", 101).json, - "key2": UserSettableParameter( - "slider", "Test Parameter", 200, 0, 300, 10 - ).json, + "key1": NumberInput("Test Parameter", 101).json, + "key2": Slider("Test Parameter", 200, 0, 300, 10).json, }