diff --git a/.github/workflows/build_lint.yml b/.github/workflows/build_lint.yml deleted file mode 100644 index 3d720f95..00000000 --- a/.github/workflows/build_lint.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: build - -on: - push: - branches: - - main - - release** - paths-ignore: - - '**.md' - - '**.rst' - pull_request: - paths-ignore: - - '**.md' - - '**.rst' - workflow_dispatch: - schedule: - - cron: '0 6 * * 1' # Monday at 6:00 UTC - -# This will cancel previous run if a newer job that obsoletes the said previous -# run, is started. -# Based on https://github.com/zulip/zulip/commit/4a11642cee3c8aec976d305d51a86e60e5d70522 -concurrency: - group: "${{ github.workflow }}-${{ github.head_ref || github.run_id }}" - cancel-in-progress: true - -jobs: - build-stable: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install dependencies - run: pip install mesa pytest - - name: Test with pytest - run: pytest test_examples.py - - build-pre: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install dependencies - run: | - pip install mesa --pre - pip install pytest - - name: Test with pytest - run: pytest test_examples.py - - build-main: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install dependencies - run: | - pip install pytest - pip install -U git+https://github.com/projectmesa/mesa@main#egg=mesa - - name: Test with pytest - run: pytest test_examples.py diff --git a/.github/workflows/test_examples.yml b/.github/workflows/test_examples.yml new file mode 100644 index 00000000..d257eef2 --- /dev/null +++ b/.github/workflows/test_examples.yml @@ -0,0 +1,60 @@ +name: Test example models + +on: + push: + paths: + - 'examples/**/*.py' # If an example model is modified + - 'test_examples.py' # If the test script is modified + - '.github/workflows/test_examples.yml' # If this workflow is modified + pull_request: + paths: + - 'examples/**/*.py' + - 'test_examples.py' + - '.github/workflows/test_examples.yml' + workflow_dispatch: + schedule: + - cron: '0 6 * * 1' # Monday at 6:00 UTC + +jobs: + # build-stable: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - name: Set up Python + # uses: actions/setup-python@v5 + # with: + # python-version: "3.12" + # - name: Install dependencies + # run: pip install mesa pytest + # - name: Test with pytest + # run: pytest -rA -Werror test_examples.py + + build-pre: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: | + pip install mesa --pre + pip install .[test] + - name: Test with pytest + run: pytest -rA -Werror -Wdefault::FutureWarning test_examples.py + + build-main: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: | + pip install .[test] + pip install -U git+https://github.com/projectmesa/mesa@main#egg=mesa + - name: Test with pytest + run: pytest -rA -Werror -Wdefault::FutureWarning test_examples.py diff --git a/.github/workflows/test_gis_examples.yml b/.github/workflows/test_gis_examples.yml new file mode 100644 index 00000000..f52986cf --- /dev/null +++ b/.github/workflows/test_gis_examples.yml @@ -0,0 +1,60 @@ +name: Test GIS models + +on: + push: + paths: + - 'gis/**/*.py' # If a gis model is modified + - 'test_gis_examples.py' # If the gis test script is modified + - '.github/workflows/test_gis_examples.yml' # If this workflow is modified + pull_request: + paths: + - 'gis/**/*.py' + - 'test_gis_examples.py' + - '.github/workflows/test_gis_examples.yml' + workflow_dispatch: + schedule: + - cron: '0 6 * * 1' # Monday at 6:00 UTC + +jobs: + # build-stable: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - name: Set up Python + # uses: actions/setup-python@v5 + # with: + # python-version: "3.12" + # - name: Install dependencies + # run: pip install mesa pytest + # - name: Test with pytest + # run: pytest -rA -Werror test_examples.py + + build-pre: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: | + pip install mesa-geo --pre + pip install .[test_gis] + - name: Test with pytest + run: pytest -rA -Werror test_gis_examples.py + + build-main: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: | + pip install -U git+https://github.com/projectmesa/mesa-geo@main#egg=mesa-geo + pip install .[test_gis] + - name: Test with pytest + run: pytest -rA -Werror test_gis_examples.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4b7d1a8a..c6d54026 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.5.0 + rev: v0.6.3 hooks: # Run the linter. # TODO fix the lint issues for the Jupyter notebooks @@ -14,7 +14,7 @@ repos: - id: ruff-format types_or: [ python, pyi, jupyter ] - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 + rev: v3.17.0 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/README.md b/README.md index c7750a3e..34f2647c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ -# mesa-examples +# Mesa Examples -This repository contains examples that work with Mesa and illustrate different features of Mesa. +This repository contains examples that work with Mesa and illustrate different features of Mesa. For more information on each model, see its own Readme and documentation. + +- Mesa examples that work on the Mesa and Mesa-Geo main development branches are available here on the [`main`](https://github.com/projectmesa/mesa-examples) branch. +- Mesa examples that work with Mesa 2.x releases and Mesa-Geo 0.8.x releases are available here on the [`mesa-2.x`](https://github.com/projectmesa/mesa-examples/tree/mesa-2.x) branch. To contribute to this repository, see [CONTRIBUTING.rst](https://github.com/projectmesa/mesa-examples/blob/main/CONTRIBUTING.rst). @@ -9,7 +12,148 @@ This repo also contains a package that readily lets you import and run some of t $ # This will install the "mesa_models" package $ pip install -U -e git+https://github.com/projectmesa/mesa-examples#egg=mesa-models ``` +For Mesa 2.x examples, install: +```console +$ # This will install the "mesa_models" package +$ pip install -U -e git+https://github.com/projectmesa/mesa-examples@mesa-2.x#egg=mesa-models +``` ```python from mesa_models.boltzmann_wealth_model.model import BoltzmannWealthModel + ``` You can see the available models at [setup.cfg](https://github.com/projectmesa/mesa-examples/blob/main/setup.cfg). + +Table of Contents +================= + +* [Grid Spacce Examples](#grid-space-examples) +* [Continuous Space Examples](#continuous-space-examples) +* [Network Examples](#network-examples) +* [Visualization Examples](#visualization-examples) +* [GIS Examples](#gis-examples) +* [Other Examples](#other-examples) + +## Grid Space Examples + +### [Bank Reserves Model](https://github.com/projectmesa/mesa-examples/blob/main/examples/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. + +### [Boltzmann Wealth Model](https://github.com/projectmesa/mesa-examples/tree/main/examples/boltzmann_wealth_model) + +Completed code to go along with the [tutorial](https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html) on making a simple model of how a highly-skewed wealth distribution can emerge from simple rules. + +### [Color Patches Model](https://github.com/projectmesa/mesa-examples/tree/main/examples/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. + +### [Conway's Game Of "Life" Model](https://github.com/projectmesa/mesa-examples/tree/main/examples/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. + +### [Conway's Game Of "Life" Model (Fast)](https://github.com/projectmesa/mesa-examples/tree/main/examples/conways_game_of_life_fast) + +A very fast performance optimized version of Conway's Game of Life using the Mesa [`PropertyLayer`](https://github.com/projectmesa/mesa/pull/1898). About 100x as fast as the regular versions, but limited visualisation (for [now](https://github.com/projectmesa/mesa/issues/2138)). + +### [Conway's Game Of "Life" Model on a Hexagonal Grid](https://github.com/projectmesa/mesa-examples/tree/main/examples/hex_snowflake) + +Conway's game of life on a hexagonal grid. + +### [Demographic Prisoner's Dilemma on a Grid](https://github.com/projectmesa/mesa-examples/tree/main/examples/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. + +### [Epstein Civil Violence Model](https://github.com/projectmesa/mesa-examples/tree/main/examples/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. + +### [Forest Fire Model](https://github.com/projectmesa/mesa-examples/tree/main/examples/forest_fire) + +Simple cellular automata of a fire spreading through a forest of cells on a grid, based on the NetLogo [Fire](http://ccl.northwestern.edu/netlogo/models/Fire) model. + +### [Hotelling's Law Model](https://github.com/projectmesa/mesa-examples/tree/main/examples/hotelling_law) + +This project is an agent-based model implemented using the Mesa framework in Python. It simulates market dynamics based on Hotelling's Law, exploring the behavior of stores in a competitive market environment. Stores adjust their prices and locations if it's increases market share to maximize revenue, providing insights into the effects of competition and customer behavior on market outcomes. + +### [Schelling Segregation Model](https://github.com/projectmesa/mesa-examples/tree/main/examples/schelling) + +Mesa implementation of the classic [Schelling segregation](http://nifty.stanford.edu/2014/mccown-schelling-model-segregation/) model. + +### [Sugarscape Constant Growback Model](https://github.com/projectmesa/mesa-examples/tree/main/examples/sugarscape_cg) + +This is Epstein & Axtell's Sugarscape Constant Growback model, with a detailed description in the Chapter Two of *Growing Artificial Societies: Social Science from the Bottom Up*. It is based on the Netlogo +[Sugarscape 2 Constant Growback](http://ccl.northwestern.edu/netlogo/models/Sugarscape2ConstantGrowback) model. + +### [Sugarscape Constant Growback Model with Traders](https://github.com/projectmesa/mesa-examples/tree/main/examples/sugarscape_g1mt) + +This is Epstein & Axtell's Sugarscape model with Traders, a detailed description is in Chapter four of *Growing Artificial Societies: Social Science from the Bottom Up (1996)*. The model shows an emergent price equilibrium can happen via a decentralized dynamics. + +### [Wolf-Sheep Predation Model](https://github.com/projectmesa/mesa-examples/tree/main/examples/wolf_sheep) + +Implementation of an ecological model of predation and reproduction, based on the NetLogo [Wolf Sheep Predation](http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation) model. + +## Continuous Space Examples + +### [Boids Flockers Model](https://github.com/projectmesa/mesa-examples/tree/main/examples/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. + +## Network Examples + +### [Boltzmann Wealth Model with Network](https://github.com/projectmesa/mesa-examples/tree/main/examples/boltzmann_wealth_model_network) + +This is the same [Boltzmann Wealth](https://github.com/projectmesa/mesa-examples/tree/main/examples/boltzmann_wealth_model) Model, but with a network grid implementation. + +### [Virus on a Network Model](https://github.com/projectmesa/mesa-examples/tree/main/examples/virus_on_network) + +This model is based on the NetLogo [Virus on a Network](https://ccl.northwestern.edu/netlogo/models/VirusonaNetwork) model. + +### [Ant System for Traveling Salesman Problem](https://github.com/projectmesa/mesa-examples/tree/main/examples/aco_tsp) + +This is based on Dorigo's Ant System "Swarm Intelligence" algorithm for generating solutions for the Traveling Salesman Problem. + +## Visualization Examples + +### [Boltzmann Wealth Model (Experimental)](https://github.com/projectmesa/mesa-examples/tree/main/examples/boltzmann_wealth_model_experimental) + +Boltzmann Wealth model with the experimental Juptyer notebook visualization feature. + +### [Charts Example](https://github.com/projectmesa/mesa-examples/tree/main/examples/charts) + +A modified version of the [Bank Reserves](https://github.com/projectmesa/mesa-examples/tree/main/examples/bank_reserves) example made to provide examples of Mesa's charting tools. + +### [Schelling Segregation Model (Experimental)](https://github.com/projectmesa/mesa-examples/tree/main/examples/schelling_experimental) + +Schelling segregation model with the experimental Juptyer notebook visualization feature. + +### [Shape Example](https://github.com/projectmesa/mesa-examples/tree/main/examples/shape_example) + +Example of grid display and direction showing agents in the form of arrow-head shape. + +## GIS Examples + +### Vector Data + +- [GeoSchelling Model (Polygons)](https://github.com/projectmesa/mesa-examples/tree/main/gis/geo_schelling) +- [GeoSchelling Model (Points & Polygons)](https://github.com/projectmesa/mesa-examples/tree/main/gis/geo_schelling_points) +- [GeoSIR Epidemics Model](https://github.com/projectmesa/mesa-examples/tree/main/gis/geo_sir) +- [Agents and Networks Model](https://github.com/projectmesa/mesa-examples/tree/main/gis/agents_and_networks) + +### Raster Data + +- [Rainfall Model](https://github.com/projectmesa/mesa-examples/tree/main/gis/rainfall) +- [Urban Growth Model](https://github.com/projectmesa/mesa-examples/tree/main/gis/urban_growth) + +### Raster and Vector Data Overlay + +- [Population Model](https://github.com/projectmesa/mesa-examples/tree/main/gis/population) + +## Other Examples + +### [El Farol Model](https://github.com/projectmesa/mesa-examples/tree/main/examples/el_farol) + +This folder contains an implementation of El Farol restaurant model. Agents (restaurant customers) decide whether to go to the restaurant or not based on their memory and reward from previous trials. Implications from the model have been used to explain how individual decision-making affects overall performance and fluctuation. + +### [Schelling Model with Caching and Replay](https://github.com/projectmesa/mesa-examples/tree/main/examples/caching_and_replay) + +This example applies caching on the Mesa [Schelling](https://github.com/projectmesa/mesa-examples/tree/main/examples/schelling) example. It enables a simulation run to be "cached" or in other words recorded. The recorded simulation run is persisted on the local file system and can be replayed at any later point. diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index dac81ed3..00000000 --- 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/aco_tsp/README.md b/examples/aco_tsp/README.md new file mode 100644 index 00000000..a810738d --- /dev/null +++ b/examples/aco_tsp/README.md @@ -0,0 +1,55 @@ +Ant System for the Traveling Salesman Problem +======================== + +This is an implementation of the Ant System (AS) algorithm for solving the Traveling Salesman Problem (TSP). This example uses Mesa's Network Grid to model a TSP by representing cities as nodes and the possible paths between them as edges. Ants are then modeled as Mesa Agents that generate solutions by traversing the network using a "swarm intelligence" algorithm. + +When an ant is choosing its next city, it consider both a pheromone trail laid down by previous ants and a greedy heuristic based on city proximity. Pheromone evaporates over time and the strength of new pheromone trail laid by an ant is proportional to the quality of its TSP solution. This produces an emergent solution as the pheromone trail is continually updated and guides ants to high quality solutions as they are discovered. + +As this model runs, more pheromone will be laid on better solutions, and less traveled paths will have their pheromone evaporate. Ants will therefore reinforce good paths and abandon bad ones. Since decisions are ultimately samples from a weighted probability distribution, ants will sometimes explore unlikely paths, which might lead to new strong solutions that will be reflected in the updated pheromone levels. + +Here, we plot the best solution per iteration, the best solution so far in all iterations, and a graph representation where the edge width is proportional to the pheromone quantity. You will quickly see most of the edges in the fully connected graph disappear and a subset of the paths emerge as reasonable candidates in the final TSP solution. + +## How to run +To launch the interactive visualization, run `solara run app.py` in this directory. Tune the $\alpha$ and $\beta$ parameters to modify how much the pheromone and city proximity influence the ants' decisions, respectively. See the Algorithm details section for more. + +Alternatively, to run for a fixed number of iterations, run `python run_tsp.py` from this directory (and update that file with the parameters you want). + +## Algorithm details +Each agent/ant is initialized to a random city and constructs a solution by choosing a sequence of cities until all are visited, but none are visited more than once. Ants then deposit a "pheromone" signal on each path in their solution that is proportional to 1/d, where d is the final distance of the solution. This means shorter paths are given more pheromone. + +When an ant is on city $i$ and deciding which city to choose next, it samples randomly using the following probabilities of transition from city $i$ to $j$: + +$$ +p_{ij}^k = \frac{\tau_{ij}^\alpha \eta_{ij}^\beta}{\sum_{l \in J_i^k} \tau_{il}^\alpha \eta_{il}^\beta} +$$ + +where: +- $\tau_{ij}$ is the amount of path pheromone +- $\eta_{ij}$ the a greedy heuristic of desireability + - In this case, $\eta_{ij} = 1/d_{ij}$, where $d_{ij}$ is the distance between + cities +- $\alpha$ is a hyperparameter setting the importance of the pheromone +- $\beta$ a hyperparameter for setting the importance of the greedy heuristic +- And the denominator sum is over $J_i^k$, which is the set of cities not yet + visited by ant $k$. + +In other words, $\alpha$ and $\beta$ are tuned to set the relative importance of the phermone trail left by prior ants, and the greedy heuristic of 1-over-distance. + +## Data collection +The following data is collected and can be used for further analysis: +- Agent-level (individual ants, reset after each iteration) + - `tsp_distance`: TSP solution distance + - `tsp_solution`: TSP solution path +- Model-level (collection of ants over many iterations) + - `num_steps`: number of algorithm iterations, where one step means each ant generates a full TSP solution and the pheromone trail is updated + - `best_distance`: the distance of the best path found in all iterations + - This is the best solution yet and can only stay flat or improve over time + - `best_distance_iter`: the distance of the best path of all ants in a single iteration + - This changes over time as the ant colony explores different solutions and can be used to understand the explore/exploit trade-off. E.g., if the colony quickly finds a good solution, but then this value trends upward and stays high, then this suggests the ants are stuck re-inforcing a suboptimal solution. + - `best_path`: the best path found in all iterations + +## References +- Original paper: Dorigo, M., Maniezzo, V., & Colorni, A. (1996). Ant system: optimization by a +colony of cooperating agents. IEEE transactions on systems, man, and cybernetics, +part b (cybernetics), 26(1), 29-41. +- Video series of this code being implemented: https://www.youtube.com/playlist?list=PLSgGvve8UweGk2TLSO-q5OSH59Q00ZxCQ \ No newline at end of file diff --git a/examples/aco_tsp/aco_tsp/__init__.py b/examples/aco_tsp/aco_tsp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/aco_tsp/aco_tsp/data/kroA100.tsp b/examples/aco_tsp/aco_tsp/data/kroA100.tsp new file mode 100644 index 00000000..b9b48209 --- /dev/null +++ b/examples/aco_tsp/aco_tsp/data/kroA100.tsp @@ -0,0 +1,107 @@ +NAME: kroA100 +TYPE: TSP +COMMENT: 100-city problem A (Krolak/Felts/Nelson) +DIMENSION: 100 +EDGE_WEIGHT_TYPE : EUC_2D +NODE_COORD_SECTION +1 1380 939 +2 2848 96 +3 3510 1671 +4 457 334 +5 3888 666 +6 984 965 +7 2721 1482 +8 1286 525 +9 2716 1432 +10 738 1325 +11 1251 1832 +12 2728 1698 +13 3815 169 +14 3683 1533 +15 1247 1945 +16 123 862 +17 1234 1946 +18 252 1240 +19 611 673 +20 2576 1676 +21 928 1700 +22 53 857 +23 1807 1711 +24 274 1420 +25 2574 946 +26 178 24 +27 2678 1825 +28 1795 962 +29 3384 1498 +30 3520 1079 +31 1256 61 +32 1424 1728 +33 3913 192 +34 3085 1528 +35 2573 1969 +36 463 1670 +37 3875 598 +38 298 1513 +39 3479 821 +40 2542 236 +41 3955 1743 +42 1323 280 +43 3447 1830 +44 2936 337 +45 1621 1830 +46 3373 1646 +47 1393 1368 +48 3874 1318 +49 938 955 +50 3022 474 +51 2482 1183 +52 3854 923 +53 376 825 +54 2519 135 +55 2945 1622 +56 953 268 +57 2628 1479 +58 2097 981 +59 890 1846 +60 2139 1806 +61 2421 1007 +62 2290 1810 +63 1115 1052 +64 2588 302 +65 327 265 +66 241 341 +67 1917 687 +68 2991 792 +69 2573 599 +70 19 674 +71 3911 1673 +72 872 1559 +73 2863 558 +74 929 1766 +75 839 620 +76 3893 102 +77 2178 1619 +78 3822 899 +79 378 1048 +80 1178 100 +81 2599 901 +82 3416 143 +83 2961 1605 +84 611 1384 +85 3113 885 +86 2597 1830 +87 2586 1286 +88 161 906 +89 1429 134 +90 742 1025 +91 1625 1651 +92 1187 706 +93 1787 1009 +94 22 987 +95 3640 43 +96 3756 882 +97 776 392 +98 1724 1642 +99 198 1810 +100 3950 1558 +EOF \ No newline at end of file diff --git a/examples/aco_tsp/aco_tsp/model.py b/examples/aco_tsp/aco_tsp/model.py new file mode 100644 index 00000000..a7b44f74 --- /dev/null +++ b/examples/aco_tsp/aco_tsp/model.py @@ -0,0 +1,247 @@ +from dataclasses import dataclass + +import mesa +import networkx as nx +import numpy as np + + +@dataclass +class NodeCoordinates: + city: int + x: float + y: float + + @classmethod + def from_line(cls, line: str): + city, x, y = line.split() + return cls(int(city), float(x), float(y)) + + +class TSPGraph: + def __init__(self, g: nx.Graph, pheromone_init: float = 1e-6): + self.g = g + self.pheromone_init = pheromone_init + self._add_edge_properties() + + @property + def pos(self): + return {k: v["pos"] for k, v in dict(self.g.nodes.data()).items()} + + @property + def cities(self): + return list(self.g.nodes) + + @property + def num_cities(self): + return len(self.g.nodes) + + def _add_edge_properties(self): + for u, v in self.g.edges(): + u_x, u_y = self.g.nodes[u]["pos"] + v_x, v_y = self.g.nodes[v]["pos"] + self.g[u][v]["distance"] = ((u_x - v_x) ** 2 + (u_y - v_y) ** 2) ** 0.5 + self.g[u][v]["visibility"] = 1 / self.g[u][v]["distance"] + self.g[u][v]["pheromone"] = self.pheromone_init + + @classmethod + def from_random(cls, num_cities: int, seed: int = 0) -> "TSPGraph": + g = nx.random_geometric_graph(num_cities, 2.0, seed=seed).to_directed() + + return cls(g) + + @classmethod + def from_tsp_file(cls, file_path: str) -> "TSPGraph": + with open(file_path) as f: + lines = f.readlines() + # Skip lines until reach the text "NODE_COORD_SECTION" + while lines.pop(0).strip() != "NODE_COORD_SECTION": + pass + + g = nx.Graph() + for line in lines: + if line.strip() == "EOF": + break + node_coordinate = NodeCoordinates.from_line(line) + + g.add_node( + node_coordinate.city, pos=(node_coordinate.x, node_coordinate.y) + ) + + # Add edges between all nodes to make a complete graph + for u in g.nodes(): + for v in g.nodes(): + if u == v: + continue + g.add_edge(u, v) + + return cls(g) + + +class AntTSP(mesa.Agent): + """ + An agent + """ + + def __init__(self, model, alpha: float = 1.0, beta: float = 5.0): + """ + Customize the agent + """ + super().__init__(model) + self.alpha = alpha + self.beta = beta + self._cities_visited = [] + self._traveled_distance = 0 + self.tsp_solution = [] + self.tsp_distance = 0 + + def calculate_pheromone_delta(self, q: float = 100): + results = {} + for idx, start_city in enumerate(self.tsp_solution[:-1]): + end_city = self.tsp_solution[idx + 1] + results[(start_city, end_city)] = q / self.tsp_distance + + return results + + def decide_next_city(self): + # Random + # new_city = self.random.choice(list(self.model.all_cities - set(self.cities_visited))) + # Choose closest city not yet visited + g = self.model.grid.G + current_city = self.pos + neighbors = list(g.neighbors(current_city)) + candidates = [n for n in neighbors if n not in self._cities_visited] + if len(candidates) == 0: + return current_city + + # p_ij(t) = 1/Z*[(tau_ij)**alpha * (1/distance)**beta] + results = [] + for city in candidates: + val = ( + (g[current_city][city]["pheromone"]) ** self.alpha + * (g[current_city][city]["visibility"]) ** self.beta + ) + results.append(val) + + results = np.array(results) + norm = results.sum() + results /= norm + + new_city = self.model.random.choices(candidates, weights=results)[0] + + return new_city + + def step(self): + """ + Modify this method to change what an individual agent will do during each step. + Can include logic based on neighbors states. + """ + g = self.model.grid.G + for idx in range(self.model.num_cities - 1): + # Pick a random city that isn't in the list of cities visited + current_city = self.pos + new_city = self.decide_next_city() + self._cities_visited.append(new_city) + self.model.grid.move_agent(self, new_city) + self._traveled_distance += g[current_city][new_city]["distance"] + + self.tsp_solution = self._cities_visited.copy() + self.tsp_distance = self._traveled_distance + self._cities_visited = [] + self._traveled_distance = 0 + + +class AcoTspModel(mesa.Model): + """ + The model class holds the model-level attributes, manages the agents, and generally handles + the global level of our model. + + 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. + """ + + def __init__( + self, + num_agents: int = 20, + tsp_graph: TSPGraph = TSPGraph.from_random(20), + max_steps: int = int(1e6), + ant_alpha: float = 1.0, + ant_beta: float = 5.0, + ): + super().__init__() + self.num_agents = num_agents + self.tsp_graph = tsp_graph + self.num_cities = tsp_graph.num_cities + self.all_cities = set(range(self.num_cities)) + self.max_steps = max_steps + self.grid = mesa.space.NetworkGrid(tsp_graph.g) + + for _ in range(self.num_agents): + agent = AntTSP(model=self, alpha=ant_alpha, beta=ant_beta) + + city = tsp_graph.cities[self.random.randrange(self.num_cities)] + self.grid.place_agent(agent, city) + agent._cities_visited.append(city) + + self.num_steps = 0 + self.best_path = None + self.best_distance = float("inf") + self.best_distance_iter = float("inf") + # Re-initialize pheromone levels + tsp_graph._add_edge_properties() + + self.datacollector = mesa.datacollection.DataCollector( + model_reporters={ + "num_steps": "num_steps", + "best_distance": "best_distance", + "best_distance_iter": "best_distance_iter", + "best_path": "best_path", + }, + agent_reporters={ + "tsp_distance": "tsp_distance", + "tsp_solution": "tsp_solution", + }, + ) + self.datacollector.collect(self) # Collect initial state at steps=0 + + self.running = True + + def update_pheromone(self, q: float = 100, ro: float = 0.5): + # tau_ij(t+1) = (1-ro)*tau_ij(t) + delta_tau_ij(t) + # delta_tau_ij(t) = sum_k^M {Q/L^k} * I[i,j \in T^k] + delta_tau_ij = {} + for k, agent in enumerate(self.agents): + delta_tau_ij[k] = agent.calculate_pheromone_delta(q) + + for i, j in self.grid.G.edges(): + # Evaporate + tau_ij = (1 - ro) * self.grid.G[i][j]["pheromone"] + # Add ant's contribution + for k, delta_tau_ij_k in delta_tau_ij.items(): + tau_ij += delta_tau_ij_k.get((i, j), 0.0) + + self.grid.G[i][j]["pheromone"] = tau_ij + + def step(self): + """ + A model step. Used for activating the agents and collecting data. + """ + self.agents.shuffle_do("step") + self.update_pheromone() + + # Check len of cities visited by an agent + best_instance_iter = float("inf") + for agent in self.agents: + # Check for best path + if agent.tsp_distance < self.best_distance: + self.best_distance = agent.tsp_distance + self.best_path = agent.tsp_solution + + if agent.tsp_distance < best_instance_iter: + best_instance_iter = agent.tsp_distance + + self.best_distance_iter = best_instance_iter + + if self.num_steps >= self.max_steps: + self.running = False + + self.datacollector.collect(self) diff --git a/examples/aco_tsp/app.py b/examples/aco_tsp/app.py new file mode 100644 index 00000000..00fa52e0 --- /dev/null +++ b/examples/aco_tsp/app.py @@ -0,0 +1,76 @@ +""" +Configure visualization elements and instantiate a server +""" + +import networkx as nx +import solara +from aco_tsp.model import AcoTspModel, TSPGraph +from matplotlib.figure import Figure +from mesa.visualization import SolaraViz, make_plot_measure + + +def circle_portrayal_example(agent): + return {"node_size": 20, "width": 0.1} + + +tsp_graph = TSPGraph.from_tsp_file("aco_tsp/data/kroA100.tsp") +model_params = { + "num_agents": tsp_graph.num_cities, + "tsp_graph": tsp_graph, + "ant_alpha": { + "type": "SliderFloat", + "value": 1.0, + "label": "Alpha: pheromone exponent", + "min": 0.0, + "max": 10.0, + "step": 0.1, + }, + "ant_beta": { + "type": "SliderFloat", + "value": 5.0, + "label": "Beta: heuristic exponent", + "min": 0.0, + "max": 10.0, + "step": 0.1, + }, +} + +model = AcoTspModel() + + +def make_graph(model): + fig = Figure() + ax = fig.subplots() + ax.set_title("Cities and pheromone trails") + graph = model.grid.G + pos = model.tsp_graph.pos + weights = [graph[u][v]["pheromone"] for u, v in graph.edges()] + # normalize the weights + weights = [w / max(weights) for w in weights] + + nx.draw( + graph, + ax=ax, + pos=pos, + node_size=10, + width=weights, + edge_color="gray", + ) + + return solara.FigureMatplotlib(fig) + + +def ant_level_distances(model): + # ant_distances = model.datacollector.get_agent_vars_dataframe() + # Plot so that the step index is the x-axis, there's a line for each agent, + # and the y-axis is the distance traveled + # ant_distances['tsp_distance'].unstack(level=1).plot(ax=ax) + pass + + +page = SolaraViz( + model, + components=[make_plot_measure(["best_distance_iter", "best_distance"]), make_graph], + model_params=model_params, + play_interval=1, +) diff --git a/examples/aco_tsp/run_tsp.py b/examples/aco_tsp/run_tsp.py new file mode 100644 index 00000000..8b33e5ce --- /dev/null +++ b/examples/aco_tsp/run_tsp.py @@ -0,0 +1,47 @@ +from collections import defaultdict + +import matplotlib.pyplot as plt +from aco_tsp.model import AcoTspModel, TSPGraph + + +def main(): + # tsp_graph = TSPGraph.from_random(num_cities=20, seed=1) + tsp_graph = TSPGraph.from_tsp_file("aco_tsp/data/kroA100.tsp") + model_params = { + "num_agents": tsp_graph.num_cities, + "tsp_graph": tsp_graph, + } + number_of_episodes = 50 + + results = defaultdict(list) + + best_path = None + best_distance = float("inf") + + model = AcoTspModel(**model_params) + + for e in range(number_of_episodes): + # model = AcoTspModel(**model_params) + model.step() + results["best_distance"].append(model.best_distance) + results["best_path"].append(model.best_path) + print( + f"Episode={e + 1}; Min. distance={model.best_distance:.2f}; pheromone_1_8={model.grid.G[17][15]['pheromone']:.4f}" + ) + if model.best_distance < best_distance: + best_distance = model.best_distance + best_path = model.best_path + print(f"New best distance: distance={best_distance:.2f}") + + print(f"Best distance: {best_distance:.2f}") + print(f"Best path: {best_path}") + # print(model.datacollector.get_model_vars_dataframe()) + + _, ax = plt.subplots() + ax.plot(results["best_distance"]) + ax.set(xlabel="Episode", ylabel="Best distance", title="Best distance per episode") + plt.show() + + +if __name__ == "__main__": + main() diff --git a/examples/bank_reserves/bank_reserves/agents.py b/examples/bank_reserves/bank_reserves/agents.py index 2863df99..11b52563 100644 --- a/examples/bank_reserves/bank_reserves/agents.py +++ b/examples/bank_reserves/bank_reserves/agents.py @@ -10,15 +10,18 @@ Northwestern University, Evanston, IL. """ -import mesa - from .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) +class Bank: + """Note that the Bank class is not a Mesa Agent, but just a regular Python + class. This is because there is only one bank in this model, and it does not + use any Mesa-specific features like the scheduler or the grid, and doesn't + have a step method. It is just used to keep track of the bank's reserves and + the amount it can loan out, for Person agents to interact with.""" + + def __init__(self, model, reserve_percent=50): + self.model = 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 @@ -42,9 +45,9 @@ def bank_balance(self): # subclass of RandomWalker, which is subclass to Mesa Agent class Person(RandomWalker): - def __init__(self, unique_id, pos, model, moore, bank, rich_threshold): + def __init__(self, model, moore, bank, rich_threshold): # init parent class with required parameters - super().__init__(unique_id, pos, model, moore=moore) + super().__init__(model, moore=moore) # the amount each person has in savings self.savings = 0 # total loan amount person has outstanding @@ -173,7 +176,6 @@ def take_out_loan(self, 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() diff --git a/examples/bank_reserves/bank_reserves/model.py b/examples/bank_reserves/bank_reserves/model.py index f0f4ba77..ae6b0a0f 100644 --- a/examples/bank_reserves/bank_reserves/model.py +++ b/examples/bank_reserves/bank_reserves/model.py @@ -26,14 +26,14 @@ 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] + rich_agents = [a for a in model.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] + poor_agents = [a for a in model.agents if a.loans > 10] return len(poor_agents) @@ -41,9 +41,7 @@ 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 + a for a in model.agents if a.loans < 10 and a.savings < model.rich_threshold ] return len(mid_agents) @@ -51,7 +49,7 @@ def get_num_mid_agents(model): def get_total_savings(model): """sum of all agents' savings""" - agent_savings = [a.savings for a in model.schedule.agents] + agent_savings = [a.savings for a in model.agents] # return the sum of agents' savings return np.sum(agent_savings) @@ -59,7 +57,7 @@ def get_total_savings(model): def get_total_wallets(model): """sum of amounts of all agents' wallets""" - agent_wallets = [a.wallet for a in model.schedule.agents] + agent_wallets = [a.wallet for a in model.agents] # return the sum of all agents' wallets return np.sum(agent_wallets) @@ -75,7 +73,7 @@ def get_total_money(model): def get_total_loans(model): # list of amounts of all agents' loans - agent_loans = [a.loans for a in model.schedule.agents] + agent_loans = [a.loans for a in model.agents] # return sum of all agents' loans return np.sum(agent_loans) @@ -118,7 +116,7 @@ def __init__( 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 @@ -138,25 +136,23 @@ def __init__( ) # create a single bank for the model - self.bank = Bank(1, self, self.reserve_percent) + self.bank = Bank(self, self.reserve_percent) # create people for the model according to number of people set by user - for i in range(self.init_people): + for _ 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) + p = Person(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() + self.agents.shuffle_do("step") # collect data self.datacollector.collect(self) diff --git a/examples/bank_reserves/bank_reserves/random_walk.py b/examples/bank_reserves/bank_reserves/random_walk.py index 7e067881..0d0258ab 100644 --- a/examples/bank_reserves/bank_reserves/random_walk.py +++ b/examples/bank_reserves/bank_reserves/random_walk.py @@ -24,7 +24,7 @@ class RandomWalker(mesa.Agent): # use a Moore neighborhood moore = True - def __init__(self, unique_id, pos, model, moore=True): + def __init__(self, model, moore=True): """ grid: The MultiGrid object in which the agent lives. x: The agent's current x coordinate @@ -32,8 +32,7 @@ def __init__(self, unique_id, pos, model, moore=True): moore: If True, may move in all 8 directions. Otherwise, only up, down, left, right. """ - super().__init__(unique_id, model) - self.pos = pos + super().__init__(model) self.moore = moore def random_move(self): diff --git a/examples/bank_reserves/batch_run.py b/examples/bank_reserves/batch_run.py index fae38433..00b08a63 100644 --- a/examples/bank_reserves/batch_run.py +++ b/examples/bank_reserves/batch_run.py @@ -37,7 +37,7 @@ 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] + rich_agents = [a for a in model.agents if a.savings > model.rich_threshold] # return number of rich agents return len(rich_agents) @@ -45,7 +45,7 @@ def get_num_rich_agents(model): def get_num_poor_agents(model): """list of poor agents""" - poor_agents = [a for a in model.schedule.agents if a.loans > 10] + poor_agents = [a for a in model.agents if a.loans > 10] # return number of poor agents return len(poor_agents) @@ -54,9 +54,7 @@ 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 + a for a in model.agents if a.loans < 10 and a.savings < model.rich_threshold ] # return number of middle class agents return len(mid_agents) @@ -65,7 +63,7 @@ def get_num_mid_agents(model): def get_total_savings(model): """list of amounts of all agents' savings""" - agent_savings = [a.savings for a in model.schedule.agents] + agent_savings = [a.savings for a in model.agents] # return the sum of agents' savings return np.sum(agent_savings) @@ -73,7 +71,7 @@ def get_total_savings(model): def get_total_wallets(model): """list of amounts of all agents' wallets""" - agent_wallets = [a.wallet for a in model.schedule.agents] + agent_wallets = [a.wallet for a in model.agents] # return the sum of all agents' wallets return np.sum(agent_wallets) @@ -91,7 +89,7 @@ def get_total_money(model): def get_total_loans(model): """list of amounts of all agents' loans""" - agent_loans = [a.loans for a in model.schedule.agents] + agent_loans = [a.loans for a in model.agents] # return sum of all agents' loans return np.sum(agent_loans) @@ -129,7 +127,7 @@ def __init__( 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 @@ -150,8 +148,8 @@ def __init__( agent_reporters={"Wealth": "wealth"}, ) - # create a single bank for the model - self.bank = Bank(1, self, self.reserve_percent) + # create a single bank object for the model + self.bank = Bank(self, self.reserve_percent) # create people for the model according to number of people set by user for i in range(self.init_people): @@ -162,8 +160,6 @@ def __init__( 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 @@ -171,7 +167,7 @@ def step(self): # collect data self.datacollector.collect(self) # tell all the agents in the model to run their step function - self.schedule.step() + self.agents.shuffle_do("step") def run_model(self): for i in range(self.run_time): @@ -185,7 +181,9 @@ def run_model(self): "reserve_percent": 5, } -if __name__ == "__main__": + +def main(): + # The existing batch run logic here data = mesa.batch_run( BankReservesModel, br_params, @@ -193,30 +191,6 @@ def run_model(self): 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") - """ + +if __name__ == "__main__": + main() diff --git a/examples/boid_flockers/Flocker Test.ipynb b/examples/boid_flockers/Flocker Test.ipynb index 82ecc47b..c757f3a8 100644 --- a/examples/boid_flockers/Flocker Test.ipynb +++ b/examples/boid_flockers/Flocker Test.ipynb @@ -25,7 +25,7 @@ "def draw_boids(model):\n", " x_vals = []\n", " y_vals = []\n", - " for boid in model.schedule.agents:\n", + " for boid in model.agents:\n", " x, y = boid.pos\n", " x_vals.append(x)\n", " y_vals.append(y)\n", diff --git a/examples/boid_flockers/app.py b/examples/boid_flockers/app.py index 5de317fe..205cb218 100644 --- a/examples/boid_flockers/app.py +++ b/examples/boid_flockers/app.py @@ -1,5 +1,5 @@ from boid_flockers.model import BoidFlockers -from mesa.experimental import JupyterViz +from mesa.visualization import SolaraViz, make_space_matplotlib def boid_draw(agent): @@ -15,11 +15,12 @@ def boid_draw(agent): "separation": 2, } -page = JupyterViz( - model_class=BoidFlockers, +model = BoidFlockers(100, 100, 100, 5, 10, 2) + +page = SolaraViz( + model, + [make_space_matplotlib(agent_portrayal=boid_draw)], model_params=model_params, - measures=[], name="BoidFlockers", - agent_portrayal=boid_draw, ) page # noqa diff --git a/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py b/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py index 42b3e9dd..ec670d7a 100644 --- a/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py +++ b/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py @@ -18,7 +18,7 @@ def __init__(self, portrayal_method=None, canvas_height=500, canvas_width=500): def render(self, model): space_state = [] - for obj in model.schedule.agents: + for obj in model.agents: portrayal = self.portrayal_method(obj) x, y = obj.pos x = (x - model.space.x_min) / (model.space.x_max - model.space.x_min) diff --git a/examples/boid_flockers/boid_flockers/model.py b/examples/boid_flockers/boid_flockers/model.py index 6ebbf8aa..ae3099f3 100644 --- a/examples/boid_flockers/boid_flockers/model.py +++ b/examples/boid_flockers/boid_flockers/model.py @@ -26,9 +26,7 @@ class Boid(mesa.Agent): def __init__( self, - unique_id, model, - pos, speed, direction, vision, @@ -41,8 +39,6 @@ def __init__( Create a new Boid flocker agent. Args: - unique_id: Unique agent identifier. - pos: Starting position speed: Distance to move per step. direction: numpy vector for the Boid's direction of movement. vision: Radius to look around for nearby Boids. @@ -51,8 +47,7 @@ def __init__( 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) + super().__init__(model) self.speed = speed self.direction = direction self.vision = vision @@ -123,7 +118,7 @@ def __init__( 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 = {"cohere": cohere, "separate": separate, "match": match} self.make_agents() @@ -132,15 +127,13 @@ def make_agents(self): """ Create self.population agents, with random positions and starting headings. """ - for i in range(self.population): + for _ in range(self.population): x = self.random.random() * self.space.x_max y = self.random.random() * self.space.y_max pos = np.array((x, y)) direction = np.random.random(2) * 2 - 1 boid = Boid( - unique_id=i, model=self, - pos=pos, speed=self.speed, direction=direction, vision=self.vision, @@ -148,7 +141,6 @@ def make_agents(self): **self.factors, ) self.space.place_agent(boid, pos) - self.schedule.add(boid) def step(self): - self.schedule.step() + self.agents.shuffle_do("step") diff --git a/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py b/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py index 11a3e958..ac091a6c 100644 --- a/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py +++ b/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py @@ -2,7 +2,7 @@ def compute_gini(model): - agent_wealths = [agent.wealth for agent in model.schedule.agents] + agent_wealths = [agent.wealth for agent in model.agents] x = sorted(agent_wealths) N = model.num_agents B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x)) @@ -21,14 +21,14 @@ def __init__(self, N=100, width=10, height=10): super().__init__() 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) + for _ in range(self.num_agents): + a = MoneyAgent(self) + # Add the agent to a random grid cell x = self.random.randrange(self.grid.width) y = self.random.randrange(self.grid.height) @@ -38,7 +38,7 @@ def __init__(self, N=100, width=10, height=10): self.datacollector.collect(self) def step(self): - self.schedule.step() + self.agents.shuffle_do("step") # collect data self.datacollector.collect(self) @@ -50,8 +50,8 @@ def run_model(self, n): class MoneyAgent(mesa.Agent): """An agent with fixed initial wealth.""" - def __init__(self, unique_id, model): - super().__init__(unique_id, model) + def __init__(self, model): + super().__init__(model) self.wealth = 1 def move(self): diff --git a/examples/boltzmann_wealth_model_experimental/app.py b/examples/boltzmann_wealth_model_experimental/app.py index dc1213bb..199b3a1a 100644 --- a/examples/boltzmann_wealth_model_experimental/app.py +++ b/examples/boltzmann_wealth_model_experimental/app.py @@ -1,4 +1,8 @@ -from mesa.experimental import JupyterViz +from mesa.visualization import ( + SolaraViz, + make_plot_measure, + make_space_matplotlib, +) from model import BoltzmannWealthModel @@ -24,11 +28,38 @@ def agent_portrayal(agent): "height": 10, } -page = JupyterViz( - BoltzmannWealthModel, - model_params, - measures=["Gini"], - name="Money Model", - agent_portrayal=agent_portrayal, +# Create initial model instance +model1 = BoltzmannWealthModel(50, 10, 10) + +# Create visualization elements. The visualization elements are solara components +# that receive the model instance as a "prop" and display it in a certain way. +# Under the hood these are just classes that receive the model instance. +# You can also author your own visualization elements, which can also be functions +# that receive the model instance and return a valid solara component. +SpaceGraph = make_space_matplotlib(agent_portrayal) +GiniPlot = make_plot_measure("Gini") + +# Create the SolaraViz page. This will automatically create a server and display the +# visualization elements in a web browser. +# Display it using the following command in the example directory: +# solara run app.py +# It will automatically update and display any changes made to this file +page = SolaraViz( + model1, + components=[SpaceGraph, GiniPlot], + model_params=model_params, + name="Boltzmann Wealth Model", ) page # noqa + + +# In a notebook environment, we can also display the visualization elements directly +# SpaceGraph(model1) +# GiniPlot(model1) + +# The plots will be static. If you want to pick up model steps, +# you have to make the model reactive first +# reactive_model = solara.reactive(model1) +# SpaceGraph(reactive_model) +# In a different notebook block: +# reactive_model.value.step() diff --git a/examples/boltzmann_wealth_model_experimental/model.py b/examples/boltzmann_wealth_model_experimental/model.py index 11a3e958..ac091a6c 100644 --- a/examples/boltzmann_wealth_model_experimental/model.py +++ b/examples/boltzmann_wealth_model_experimental/model.py @@ -2,7 +2,7 @@ def compute_gini(model): - agent_wealths = [agent.wealth for agent in model.schedule.agents] + agent_wealths = [agent.wealth for agent in model.agents] x = sorted(agent_wealths) N = model.num_agents B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x)) @@ -21,14 +21,14 @@ def __init__(self, N=100, width=10, height=10): super().__init__() 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) + for _ in range(self.num_agents): + a = MoneyAgent(self) + # Add the agent to a random grid cell x = self.random.randrange(self.grid.width) y = self.random.randrange(self.grid.height) @@ -38,7 +38,7 @@ def __init__(self, N=100, width=10, height=10): self.datacollector.collect(self) def step(self): - self.schedule.step() + self.agents.shuffle_do("step") # collect data self.datacollector.collect(self) @@ -50,8 +50,8 @@ def run_model(self, n): class MoneyAgent(mesa.Agent): """An agent with fixed initial wealth.""" - def __init__(self, unique_id, model): - super().__init__(unique_id, model) + def __init__(self, model): + super().__init__(model) self.wealth = 1 def move(self): 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 index d7b43374..a61cf4bf 100644 --- a/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/model.py +++ b/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/model.py @@ -3,7 +3,7 @@ def compute_gini(model): - agent_wealths = [agent.wealth for agent in model.schedule.agents] + agent_wealths = [agent.wealth for agent in model.agents] x = sorted(agent_wealths) N = model.num_agents B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x)) @@ -19,7 +19,7 @@ def __init__(self, num_agents=7, num_nodes=10): 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}, @@ -29,8 +29,8 @@ def __init__(self, num_agents=7, num_nodes=10): # Create agents for i in range(self.num_agents): - a = MoneyAgent(i, self) - self.schedule.add(a) + a = MoneyAgent(self) + # Add the agent to a random node self.grid.place_agent(a, list_of_random_nodes[i]) @@ -38,7 +38,7 @@ def __init__(self, num_agents=7, num_nodes=10): self.datacollector.collect(self) def step(self): - self.schedule.step() + self.agents.shuffle_do("step") # collect data self.datacollector.collect(self) @@ -50,8 +50,8 @@ def run_model(self, n): class MoneyAgent(mesa.Agent): """An agent with fixed initial wealth.""" - def __init__(self, unique_id, model): - super().__init__(unique_id, model) + def __init__(self, model): + super().__init__(model) self.wealth = 1 def move(self): 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 index fce6b4cd..50a019ce 100644 --- a/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/server.py +++ b/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/server.py @@ -12,9 +12,11 @@ def network_portrayal(G): "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}", + "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") ] diff --git a/examples/caching_and_replay/model.py b/examples/caching_and_replay/model.py index 8b04368b..e4bc23c7 100644 --- a/examples/caching_and_replay/model.py +++ b/examples/caching_and_replay/model.py @@ -8,16 +8,15 @@ class SchellingAgent(mesa.Agent): Schelling segregation agent """ - def __init__(self, unique_id, model, agent_type): + def __init__(self, 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__(unique_id, model) + super().__init__(model) self.type = agent_type def step(self): @@ -70,7 +69,6 @@ def __init__( self.homophily = homophily self.radius = radius - self.schedule = mesa.time.RandomActivation(self) self.grid = mesa.space.SingleGrid(width, height, torus=True) self.happy = 0 @@ -85,9 +83,8 @@ def __init__( for _, pos in self.grid.coord_iter(): if self.random.random() < self.density: agent_type = 1 if self.random.random() < self.minority_pc else 0 - agent = SchellingAgent(self.next_id(), self, agent_type) + agent = SchellingAgent(self, agent_type) self.grid.place_agent(agent, pos) - self.schedule.add(agent) self.datacollector.collect(self) @@ -96,9 +93,9 @@ def step(self): Run one step of the model. """ self.happy = 0 # Reset counter of happy agents - self.schedule.step() + self.agents.shuffle_do("step") self.datacollector.collect(self) - if self.happy == self.schedule.get_agent_count(): + if self.happy == len(self.agents): self.running = False diff --git a/examples/charts/charts/agents.py b/examples/charts/charts/agents.py index 2863df99..11b52563 100644 --- a/examples/charts/charts/agents.py +++ b/examples/charts/charts/agents.py @@ -10,15 +10,18 @@ Northwestern University, Evanston, IL. """ -import mesa - from .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) +class Bank: + """Note that the Bank class is not a Mesa Agent, but just a regular Python + class. This is because there is only one bank in this model, and it does not + use any Mesa-specific features like the scheduler or the grid, and doesn't + have a step method. It is just used to keep track of the bank's reserves and + the amount it can loan out, for Person agents to interact with.""" + + def __init__(self, model, reserve_percent=50): + self.model = 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 @@ -42,9 +45,9 @@ def bank_balance(self): # subclass of RandomWalker, which is subclass to Mesa Agent class Person(RandomWalker): - def __init__(self, unique_id, pos, model, moore, bank, rich_threshold): + def __init__(self, model, moore, bank, rich_threshold): # init parent class with required parameters - super().__init__(unique_id, pos, model, moore=moore) + super().__init__(model, moore=moore) # the amount each person has in savings self.savings = 0 # total loan amount person has outstanding @@ -173,7 +176,6 @@ def take_out_loan(self, 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() diff --git a/examples/charts/charts/model.py b/examples/charts/charts/model.py index 71e984a1..679f3176 100644 --- a/examples/charts/charts/model.py +++ b/examples/charts/charts/model.py @@ -26,14 +26,14 @@ 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] + rich_agents = [a for a in model.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] + poor_agents = [a for a in model.agents if a.loans > 10] return len(poor_agents) @@ -41,9 +41,7 @@ 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 + a for a in model.agents if a.loans < 10 and a.savings < model.rich_threshold ] return len(mid_agents) @@ -51,7 +49,7 @@ def get_num_mid_agents(model): def get_total_savings(model): """sum of all agents' savings""" - agent_savings = [a.savings for a in model.schedule.agents] + agent_savings = [a.savings for a in model.agents] # return the sum of agents' savings return np.sum(agent_savings) @@ -59,7 +57,7 @@ def get_total_savings(model): def get_total_wallets(model): """sum of amounts of all agents' wallets""" - agent_wallets = [a.wallet for a in model.schedule.agents] + agent_wallets = [a.wallet for a in model.agents] # return the sum of all agents' wallets return np.sum(agent_wallets) @@ -75,7 +73,7 @@ def get_total_money(model): def get_total_loans(model): # list of amounts of all agents' loans - agent_loans = [a.loans for a in model.schedule.agents] + agent_loans = [a.loans for a in model.agents] # return sum of all agents' loans return np.sum(agent_loans) @@ -101,7 +99,7 @@ def __init__( 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 @@ -121,25 +119,23 @@ def __init__( ) # create a single bank for the model - self.bank = Bank(1, self, self.reserve_percent) + self.bank = Bank(self, self.reserve_percent) # create people for the model according to number of people set by user - for i in range(self.init_people): + for _ 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) + p = Person(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() + self.agents.shuffle_do("step") # collect data self.datacollector.collect(self) diff --git a/examples/charts/charts/random_walk.py b/examples/charts/charts/random_walk.py index 7e067881..0d0258ab 100644 --- a/examples/charts/charts/random_walk.py +++ b/examples/charts/charts/random_walk.py @@ -24,7 +24,7 @@ class RandomWalker(mesa.Agent): # use a Moore neighborhood moore = True - def __init__(self, unique_id, pos, model, moore=True): + def __init__(self, model, moore=True): """ grid: The MultiGrid object in which the agent lives. x: The agent's current x coordinate @@ -32,8 +32,7 @@ def __init__(self, unique_id, pos, model, moore=True): moore: If True, may move in all 8 directions. Otherwise, only up, down, left, right. """ - super().__init__(unique_id, model) - self.pos = pos + super().__init__(model) self.moore = moore def random_move(self): diff --git a/examples/color_patches/color_patches/model.py b/examples/color_patches/color_patches/model.py index 4e69b965..3cd08a6c 100644 --- a/examples/color_patches/color_patches/model.py +++ b/examples/color_patches/color_patches/model.py @@ -18,7 +18,7 @@ def __init__(self, pos, model, initial_state): """ Create a cell, in the given state, at the given row, col position. """ - super().__init__(pos, model) + super().__init__(model) self._row = pos[0] self._col = pos[1] self._state = initial_state @@ -36,7 +36,7 @@ def get_state(self): """Return the current state (OPINION) of this cell.""" return self._state - def step(self): + def determine_opinion(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 @@ -54,7 +54,7 @@ def step(self): self._next_state = self.random.choice(tied_opinions)[0] - def advance(self): + def assume_opinion(self): """ Set the state of the agent to the next state """ @@ -73,7 +73,6 @@ def __init__(self, width=20, height=20): """ super().__init__() self._grid = mesa.space.SingleGrid(width, height, torus=False) - self.schedule = mesa.time.SimultaneousActivation(self) # self._grid.coord_iter() # --> should really not return content + col + row @@ -85,23 +84,17 @@ def __init__(self, width=20, height=20): (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. + Perform the model step in two stages: + - First, all agents determine their next opinion based on their neighbors current opinions + - Then, all agents update their opinion to the next opinion """ - 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 + self.agents.do("determine_opinion") + self.agents.do("assume_opinion") @property def grid(self): diff --git a/examples/color_patches/color_patches/server.py b/examples/color_patches/color_patches/server.py index aa52332e..34d0d744 100644 --- a/examples/color_patches/color_patches/server.py +++ b/examples/color_patches/color_patches/server.py @@ -2,6 +2,7 @@ handles the definition of the canvas parameters and the drawing of the model representation on the canvas """ + # import webbrowser import mesa 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 index 8639288d..35c8d3f2 100644 --- a/examples/conways_game_of_life/conways_game_of_life/cell.py +++ b/examples/conways_game_of_life/conways_game_of_life/cell.py @@ -11,7 +11,7 @@ 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) + super().__init__(model) self.x, self.y = pos self.state = init_state self._nextState = None @@ -24,7 +24,7 @@ def isAlive(self): def neighbors(self): return self.model.grid.iter_neighbors((self.x, self.y), True) - def step(self): + def determine_state(self): """ Compute if the cell will be dead or alive at the next tick. This is based on the number of alive or dead neighbors. The state is not @@ -46,7 +46,7 @@ def step(self): if live_neighbors == 3: self._nextState = self.ALIVE - def advance(self): + def assume_state(self): """ Set the state to the new computed state -- computed in step(). """ 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 index f6c9637a..76d9ca9f 100644 --- a/examples/conways_game_of_life/conways_game_of_life/model.py +++ b/examples/conways_game_of_life/conways_game_of_life/model.py @@ -14,15 +14,6 @@ def __init__(self, width=50, height=50): Create a new playing area of (width, height) cells. """ super().__init__() - - # 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.SingleGrid(width, height, torus=True) @@ -33,12 +24,14 @@ def __init__(self, width=50, height=50): 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 + Perform the model step in two stages: + - First, all cells assume their next state (whether they will be dead or alive) + - Then, all cells change state to their next state """ - self.schedule.step() + self.agents.do("determine_state") + self.agents.do("assume_state") diff --git a/examples/conways_game_of_life_fast/Readme.md b/examples/conways_game_of_life_fast/Readme.md new file mode 100644 index 00000000..5aaf14b7 --- /dev/null +++ b/examples/conways_game_of_life_fast/Readme.md @@ -0,0 +1,54 @@ +## Conway's Game of Life (Fast) +This example demonstrates a fast and efficient implementation of Conway's Game of Life using the [`PropertyLayer`](https://github.com/projectmesa/mesa/pull/1898) from the Mesa framework. + +### Overview +Conway's [Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) is a classic cellular automaton where each cell on a grid can either be alive or dead. The state of each cell changes over time based on a set of simple rules that depend on the number of alive neighbors. + +#### Key features: +- **No grid or agents:** This implementation uses the `PropertyLayer` to manage the state of cells, eliminating the need for traditional grids or agents. +- **Fast:** By using 2D convolution to count neighbors, the model efficiently applies the rules of the Game of Life across the entire grid. +- **Toroidal:** The grid wraps around at the edges, creating a seamless, continuous surface. + +#### Performance +The model is benchmarked in https://github.com/projectmesa/mesa/pull/1898#issuecomment-1849000346 to be about 100x faster over a traditional implementation. + + + +- Benchmark code: [benchmark_gol.zip](https://github.com/projectmesa/mesa/files/13628343/benchmark_gol.zip) + +### Getting Started +#### Prerequisites +- Python 3.9 or higher +- Mesa 2.3 or higher +- NumPy and SciPy + +#### Running the Model +To run the model, open a new file or notebook and add: + +```Python +from model import GameOfLifeModel +model = GameOfLifeModel(width=10, height=10, alive_fraction=0.2) +for i in range(10): + model.step() +``` +Or to run visualized with Solara, run in your terminal: + +```bash +solara run app.py +``` + +### Understanding the Code +- **Model initialization:** The grid is represented by a `PropertyLayer` where each cell is randomly initialized as alive or dead based on a given probability. +- **`PropertyLayer`:** In the `cell_layer` (which is a `PropertyLayer`), each cell has either a value of 1 (alive) or 0 (dead). +- **Step function:** Each simulation step calculates the number of alive neighbors for each cell and applies the Game of Life rules. +- **Data collection:** The model tracks and reports the number of alive cells and the fraction of the grid that is alive. + +### Customization +You can easily modify the model parameters such as grid size and initial alive fraction to explore different scenarios. You can also add more metrics or visualisations. + +### Summary +This example provides a fast approach to modeling cellular automata using Mesa's `PropertyLayer`. + +### Future work +Add visualisation of the `PropertyLayer` in SolaraViz. See: +- https://github.com/projectmesa/mesa/issues/2138 diff --git a/examples/conways_game_of_life_fast/app.py b/examples/conways_game_of_life_fast/app.py new file mode 100644 index 00000000..568f0641 --- /dev/null +++ b/examples/conways_game_of_life_fast/app.py @@ -0,0 +1,35 @@ +from mesa.visualization import SolaraViz, make_plot_measure +from model import GameOfLifeModel + +model_params = { + "width": { + "type": "SliderInt", + "value": 10, + "label": "Width", + "min": 5, + "max": 25, + "step": 1, + }, + "height": { + "type": "SliderInt", + "value": 10, + "label": "Height", + "min": 5, + "max": 25, + "step": 1, + }, +} + +gol = GameOfLifeModel(10, 10) + +TotalAlivePlot = make_plot_measure("Cells alive") +FractionAlivePlot = make_plot_measure("Fraction alive") + + +page = SolaraViz( + gol, + components=[TotalAlivePlot, FractionAlivePlot], + model_params=model_params, + name="Game of Life Model", +) +page # noqa diff --git a/examples/conways_game_of_life_fast/model.py b/examples/conways_game_of_life_fast/model.py new file mode 100644 index 00000000..40499b64 --- /dev/null +++ b/examples/conways_game_of_life_fast/model.py @@ -0,0 +1,52 @@ +import numpy as np +from mesa import Model +from mesa.datacollection import DataCollector +from mesa.space import PropertyLayer +from scipy.signal import convolve2d + + +# fmt: off +class GameOfLifeModel(Model): + def __init__(self, width=10, height=10, alive_fraction=0.2): + super().__init__() + # Initialize the property layer for cell states + self.cell_layer = PropertyLayer("cells", width, height, False, dtype=bool) + # Randomly set cells to alive + self.cell_layer.data = np.random.choice([True, False], size=(width, height), p=[alive_fraction, 1 - alive_fraction]) + + # Metrics and datacollector + self.cells = width * height + self.alive_count = 0 + self.alive_fraction = 0 + self.datacollector = DataCollector( + model_reporters={"Cells alive": "alive_count", + "Fraction alive": "alive_fraction"} + ) + self.datacollector.collect(self) + + def step(self): + # Define a kernel for counting neighbors. The kernel has 1s around the center cell (which is 0). + # This setup allows us to count the live neighbors of each cell when we apply convolution. + kernel = np.array([[1, 1, 1], + [1, 0, 1], + [1, 1, 1]]) + + # Count neighbors using convolution. + # convolve2d applies the kernel to each cell of the grid, summing up the values of neighbors. + # boundary="wrap" ensures that the grid wraps around, simulating a toroidal surface. + neighbor_count = convolve2d(self.cell_layer.data, kernel, mode="same", boundary="wrap") + + # Apply Game of Life rules: + # 1. A live cell with 2 or 3 live neighbors survives, otherwise it dies. + # 2. A dead cell with exactly 3 live neighbors becomes alive. + # These rules are implemented using logical operations on the grid. + self.cell_layer.data = np.logical_or( + np.logical_and(self.cell_layer.data, np.logical_or(neighbor_count == 2, neighbor_count == 3)), + # Rule for live cells + np.logical_and(~self.cell_layer.data, neighbor_count == 3) # Rule for dead cells + ) + + # Metrics + self.alive_count = np.sum(self.cell_layer.data) + self.alive_fraction = self.alive_count / self.cells + self.datacollector.collect(self) diff --git a/examples/el_farol/el_farol/agents.py b/examples/el_farol/el_farol/agents.py index 638269d7..edfe9a63 100644 --- a/examples/el_farol/el_farol/agents.py +++ b/examples/el_farol/el_farol/agents.py @@ -3,8 +3,8 @@ class BarCustomer(mesa.Agent): - def __init__(self, unique_id, model, memory_size, crowd_threshold, num_strategies): - super().__init__(unique_id, model) + def __init__(self, model, memory_size, crowd_threshold, num_strategies): + super().__init__(model) # Random values from -1.0 to 1.0 self.strategies = np.random.rand(num_strategies, memory_size + 1) * 2 - 1 self.best_strategy = self.strategies[0] @@ -14,7 +14,7 @@ def __init__(self, unique_id, model, memory_size, crowd_threshold, num_strategie self.utility = 0 self.update_strategies() - def step(self): + def update_attendance(self): prediction = self.predict_attendance( self.best_strategy, self.model.history[-self.memory_size :] ) diff --git a/examples/el_farol/el_farol/model.py b/examples/el_farol/el_farol/model.py index c9a3d6c0..a57d5de5 100644 --- a/examples/el_farol/el_farol/model.py +++ b/examples/el_farol/el_farol/model.py @@ -15,7 +15,6 @@ def __init__( super().__init__() self.running = True self.num_agents = N - self.schedule = mesa.time.RandomActivation(self) # Initialize the previous attendance randomly so the agents have a history # to work with from the start. @@ -24,9 +23,9 @@ def __init__( # strategies would have worked. self.history = np.random.randint(0, 100, size=memory_size * 2).tolist() self.attendance = self.history[-1] - for i in range(self.num_agents): - a = BarCustomer(i, self, memory_size, crowd_threshold, num_strategies) - self.schedule.add(a) + for _ in range(self.num_agents): + BarCustomer(self, memory_size, crowd_threshold, num_strategies) + self.datacollector = mesa.DataCollector( model_reporters={"Customers": "attendance"}, agent_reporters={"Utility": "utility", "Attendance": "attend"}, @@ -35,10 +34,9 @@ def __init__( def step(self): self.datacollector.collect(self) self.attendance = 0 - self.schedule.step() + self.agents.shuffle_do("update_attendance") # We ensure that the length of history is constant # after each step. self.history.pop(0) self.history.append(self.attendance) - for agent in self.schedule.agents: - agent.update_strategies() + self.agents.shuffle_do("update_strategies") diff --git a/examples/epstein_civil_violence/epstein_civil_violence/agent.py b/examples/epstein_civil_violence/epstein_civil_violence/agent.py index ea108faa..b746a5a4 100644 --- a/examples/epstein_civil_violence/epstein_civil_violence/agent.py +++ b/examples/epstein_civil_violence/epstein_civil_violence/agent.py @@ -9,7 +9,6 @@ class Citizen(mesa.Agent): 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). @@ -30,7 +29,6 @@ class Citizen(mesa.Agent): def __init__( self, - unique_id, model, pos, hardship, @@ -42,7 +40,6 @@ def __init__( """ 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). @@ -55,7 +52,7 @@ def __init__( agent can inspect. Exogenous. model: model instance """ - super().__init__(unique_id, model) + super().__init__(model) self.breed = "citizen" self.pos = pos self.hardship = hardship @@ -129,17 +126,16 @@ class Cop(mesa.Agent): able to inspect """ - def __init__(self, unique_id, model, pos, vision): + def __init__(self, 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) + super().__init__(model) self.breed = "cop" self.pos = pos self.vision = vision diff --git a/examples/epstein_civil_violence/epstein_civil_violence/model.py b/examples/epstein_civil_violence/epstein_civil_violence/model.py index 6bce24eb..b278f23e 100644 --- a/examples/epstein_civil_violence/epstein_civil_violence/model.py +++ b/examples/epstein_civil_violence/epstein_civil_violence/model.py @@ -58,7 +58,7 @@ def __init__( self.movement = movement self.max_iters = max_iters self.iteration = 0 - self.schedule = mesa.time.RandomActivation(self) + self.grid = mesa.space.SingleGrid(width, height, torus=True) model_reporters = { @@ -78,18 +78,16 @@ def __init__( 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 + cop = Cop(self, (x, y), vision=self.cop_vision) 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(), @@ -98,9 +96,7 @@ def __init__( 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) @@ -109,7 +105,7 @@ def step(self): """ Advance the model by one step and collect data. """ - self.schedule.step() + self.agents.shuffle_do("step") # collect data self.datacollector.collect(self) self.iteration += 1 @@ -122,7 +118,7 @@ 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: + for agent in model.agents: if agent.breed == "cop": continue if exclude_jailed and agent.jail_sentence > 0: @@ -137,7 +133,7 @@ def count_jailed(model): Helper method to count jailed agents. """ count = 0 - for agent in model.schedule.agents: + for agent in model.agents: if agent.breed == "citizen" and agent.jail_sentence > 0: count += 1 return count @@ -148,7 +144,7 @@ def count_cops(model): Helper method to count jailed agents. """ count = 0 - for agent in model.schedule.agents: + for agent in model.agents: if agent.breed == "cop": count += 1 return count diff --git a/examples/forest_fire/Forest Fire Model.ipynb b/examples/forest_fire/Forest Fire Model.ipynb index afe4142a..189672ed 100644 --- a/examples/forest_fire/Forest Fire Model.ipynb +++ b/examples/forest_fire/Forest Fire Model.ipynb @@ -35,8 +35,7 @@ "from mesa import Agent, Model\n", "from mesa.batchrunner import BatchRunner\n", "from mesa.datacollection import DataCollector\n", - "from mesa.space import Grid\n", - "from mesa.time import RandomActivation" + "from mesa.space import Grid" ] }, { @@ -65,21 +64,17 @@ " 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", + " unique_id: int\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", + " pos: The tree's coordinates on the grid.\n", " \"\"\"\n", - " super().__init__(pos, model)\n", + " super().__init__(model)\n", " self.pos = pos\n", - " self.unique_id = pos\n", " self.condition = \"Fine\"\n", "\n", " def step(self):\n", @@ -129,7 +124,6 @@ " 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", @@ -149,14 +143,13 @@ " 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.agents.shuffle_do(\"step\")\n", " self.dc.collect(self)\n", " # Halt if no more fire\n", " if self.count_type(self, \"On Fire\") == 0:\n", @@ -168,7 +161,7 @@ " 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", + " for tree in model.agents:\n", " if tree.condition == tree_condition:\n", " count += 1\n", " return count" @@ -339,9 +332,7 @@ "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", + " \"BurnedOut\": lambda m: (ForestFire.count_type(m, \"Burned Out\") / len(m.agents))\n", "}" ] }, diff --git a/examples/forest_fire/forest_fire/agent.py b/examples/forest_fire/forest_fire/agent.py index 34ff2aa2..c9b4e975 100644 --- a/examples/forest_fire/forest_fire/agent.py +++ b/examples/forest_fire/forest_fire/agent.py @@ -8,21 +8,17 @@ class TreeCell(mesa.Agent): Attributes: x, y: Grid coordinates condition: Can be "Fine", "On Fire", or "Burned Out" - unique_id: (x,y) tuple. + unique_id: int - unique_id isn't strictly necessary here, but it's good - practice to give one to each agent anyway. """ - def __init__(self, pos, model): + def __init__(self, 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 + super().__init__(model) self.condition = "Fine" def step(self): diff --git a/examples/forest_fire/forest_fire/model.py b/examples/forest_fire/forest_fire/model.py index 843176b7..e23a9e9e 100644 --- a/examples/forest_fire/forest_fire/model.py +++ b/examples/forest_fire/forest_fire/model.py @@ -18,7 +18,7 @@ def __init__(self, width=100, height=100, density=0.65): """ super().__init__() # Set up model objects - self.schedule = mesa.time.RandomActivation(self) + self.grid = mesa.space.SingleGrid(width, height, torus=False) self.datacollector = mesa.DataCollector( @@ -33,12 +33,11 @@ def __init__(self, width=100, height=100, density=0.65): for contents, (x, y) in self.grid.coord_iter(): if self.random.random() < density: # Create a tree - new_tree = TreeCell((x, y), self) + new_tree = TreeCell(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) @@ -47,7 +46,7 @@ def step(self): """ Advance the model by one step. """ - self.schedule.step() + self.agents.shuffle_do("step") # collect data self.datacollector.collect(self) @@ -61,7 +60,7 @@ 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: + for tree in model.agents: if tree.condition == tree_condition: count += 1 return count diff --git a/examples/hex_snowflake/hex_snowflake/cell.py b/examples/hex_snowflake/hex_snowflake/cell.py index a9fd64ec..7e2367c5 100644 --- a/examples/hex_snowflake/hex_snowflake/cell.py +++ b/examples/hex_snowflake/hex_snowflake/cell.py @@ -11,7 +11,7 @@ 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) + super().__init__(model) self.x, self.y = pos self.state = init_state self._nextState = None @@ -29,7 +29,7 @@ def neighbors(self): def considered(self): return self.isConsidered is True - def step(self): + def determine_state(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 @@ -53,8 +53,8 @@ def step(self): for a in self.neighbors: a.isConsidered = True - def advance(self): + def assume_state(self): """ - Set the state to the new computed state -- computed in step(). + Set the state to the new computed state """ self.state = self._nextState diff --git a/examples/hex_snowflake/hex_snowflake/model.py b/examples/hex_snowflake/hex_snowflake/model.py index 329bfe11..349d41b0 100644 --- a/examples/hex_snowflake/hex_snowflake/model.py +++ b/examples/hex_snowflake/hex_snowflake/model.py @@ -14,14 +14,6 @@ def __init__(self, width=50, height=50): Create a new playing area of (width, height) cells. """ super().__init__() - # 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.HexSingleGrid(width, height, torus=True) @@ -29,7 +21,6 @@ def __init__(self, width=50, height=50): for contents, pos in self.grid.coord_iter(): cell = Cell(pos, self) self.grid.place_agent(cell, pos) - self.schedule.add(cell) # activate the center(ish) cell. centerishCell = self.grid[width // 2][height // 2] @@ -42,6 +33,9 @@ def __init__(self, width=50, height=50): def step(self): """ - Have the scheduler advance each cell by one step + Perform the model step in two stages: + - First, all cells assume their next state (whether they will be dead or alive) + - Then, all cells change state to their next state """ - self.schedule.step() + self.agents.do("determine_state") + self.agents.do("assume_state") diff --git a/examples/hotelling_law/app.py b/examples/hotelling_law/app.py index 6ab325e5..e1aaaf2a 100644 --- a/examples/hotelling_law/app.py +++ b/examples/hotelling_law/app.py @@ -5,7 +5,7 @@ from hotelling_law.agents import ConsumerAgent, StoreAgent from hotelling_law.model import HotellingModel from matplotlib.figure import Figure -from mesa.experimental import JupyterViz +from mesa.visualization import SolaraViz, make_plot_measure model_params = { "N_stores": { @@ -108,7 +108,8 @@ def agent_portrayal(agent): return portrayal -def space_drawer(model, agent_portrayal): +@solara.component +def SpaceDrawer(model): fig = Figure(figsize=(8, 5), dpi=100) ax = fig.subplots() @@ -121,7 +122,7 @@ def space_drawer(model, agent_portrayal): cell_store_contents = {} # Track store agents in each cell jitter_amount = 0.3 # Jitter for visual separation - for agent in model.schedule.agents: + for agent in model.agents: portrayal = agent_portrayal(agent) # Track store agents for cell coloring @@ -150,7 +151,7 @@ def space_drawer(model, agent_portrayal): ax.add_patch(rect) # Jittered scatter plot for all agents - for agent in model.schedule.agents: + for agent in model.agents: portrayal = agent_portrayal(agent) jitter_x = np.random.uniform(-jitter_amount, jitter_amount) + agent.pos[0] + 0.5 jitter_y = np.random.uniform(-jitter_amount, jitter_amount) + agent.pos[1] + 0.5 @@ -177,9 +178,7 @@ def make_market_share_and_price_chart(model): # Get store agents and sort them by their unique_id # to ensure consistent order - store_agents = [ - agent for agent in model.schedule.agents if isinstance(agent, StoreAgent) - ] + store_agents = [agent for agent in model.agents if isinstance(agent, StoreAgent)] store_agents_sorted = sorted(store_agents, key=lambda agent: agent.unique_id) # Now gather market shares, prices, and labels using the sorted list @@ -241,7 +240,7 @@ def make_price_changes_line_chart(model): # Retrieve agent colors based on their portrayal agent_colors = { f"Store_{agent.unique_id}_Price": agent_portrayal(agent)["color"] - for agent in model.schedule.agents + for agent in model.agents if isinstance(agent, StoreAgent) } @@ -277,7 +276,7 @@ def make_market_share_line_chart(model): # Retrieve agent colors based on their portrayal agent_colors = { f"Store_{agent.unique_id}_Market Share": agent_portrayal(agent)["color"] - for agent in model.schedule.agents + for agent in model.agents if isinstance(agent, StoreAgent) } @@ -313,7 +312,7 @@ def make_revenue_line_chart(model): # Retrieve agent colors based on their portrayal agent_colors = { f"Store_{agent.unique_id}_Revenue": agent_portrayal(agent)["color"] - for agent in model.schedule.agents + for agent in model.agents if isinstance(agent, StoreAgent) } @@ -340,20 +339,20 @@ def make_revenue_line_chart(model): return solara.FigureMatplotlib(fig) -# Instantiate the JupyterViz component with your model -page = JupyterViz( - model_class=HotellingModel, - model_params=model_params, - measures=[ +model1 = HotellingModel(20, 20) + +# Instantiate the SolaraViz component with your model +page = SolaraViz( + model1, + components=[ + SpaceDrawer, make_price_changes_line_chart, make_market_share_and_price_chart, make_market_share_line_chart, - "Price Variance", + make_plot_measure("Price Variance"), make_revenue_line_chart, ], name="Hotelling's Law Model", - agent_portrayal=agent_portrayal, - space_drawer=space_drawer, play_interval=150, ) diff --git a/examples/hotelling_law/hotelling_law/agents.py b/examples/hotelling_law/hotelling_law/agents.py index 62d8cc8d..deaec596 100644 --- a/examples/hotelling_law/hotelling_law/agents.py +++ b/examples/hotelling_law/hotelling_law/agents.py @@ -9,11 +9,11 @@ class StoreAgent(Agent): """An agent representing a store with a price and ability to move and adjust prices.""" - def __init__(self, unique_id, model, price=10, can_move=True, strategy="Budget"): + def __init__(self, model, price=10, can_move=True, strategy="Budget"): # Initializes the store agent with a unique ID, # the model it belongs to,its initial price, # and whether it can move. - super().__init__(unique_id, model) + super().__init__(model) self.price = price # Initial price of the store. self.can_move = can_move # Indicates if the agent can move. self.market_share = 0 # Initialize market share @@ -119,7 +119,7 @@ def adjust_price(self): def identify_competitors(self): competitors = [] - for agent in self.model.schedule.agents: + for agent in self.model.agents: if isinstance(agent, StoreAgent) and agent.unique_id != self.unique_id: # Estimate market overlap as a measure of competition overlap = self.estimate_market_overlap(agent) @@ -160,8 +160,8 @@ class ConsumerAgent(Agent): """A consumer agent that chooses a store based on price and distance.""" - def __init__(self, unique_id, model): - super().__init__(unique_id, model) + def __init__(self, model): + super().__init__(model) self.preferred_store = None def determine_preferred_store(self): diff --git a/examples/hotelling_law/hotelling_law/model.py b/examples/hotelling_law/hotelling_law/model.py index 40c19b81..d2bc19bc 100644 --- a/examples/hotelling_law/hotelling_law/model.py +++ b/examples/hotelling_law/hotelling_law/model.py @@ -5,7 +5,6 @@ from mesa.agent import AgentSet from mesa.datacollection import DataCollector from mesa.space import MultiGrid -from mesa.time import RandomActivation from .agents import ConsumerAgent, StoreAgent @@ -94,8 +93,6 @@ def __init__( self.consumer_preferences = consumer_preferences # Type of environment ('grid' or 'line'). self.environment_type = environment_type - # Scheduler to activate agents one at a time, in random order. - self.schedule = RandomActivation(self) # Initialize AgentSets for store and consumer agents self.store_agents = AgentSet([], self) self.consumer_agents = AgentSet([], self) @@ -181,14 +178,14 @@ def _initialize_agents(self): ) # Calculate number of mobile agents. mobile_agents_assigned = 0 - for unique_id in range(self.num_agents): + for _ in range(self.num_agents): strategy = random.choices(["Budget", "Premium"], weights=[70, 30], k=1)[0] can_move = mobile_agents_assigned < num_mobile_agents if can_move: mobile_agents_assigned += 1 - agent = StoreAgent(unique_id, self, can_move=can_move, strategy=strategy) - self.schedule.add(agent) + agent = StoreAgent(self, can_move=can_move, strategy=strategy) + self.store_agents.add(agent) # Randomly place agents on the grid for a grid environment. @@ -197,10 +194,10 @@ def _initialize_agents(self): self.grid.place_agent(agent, (x, y)) # Place consumer agents - for i in range(self.num_consumers): + for _ in range(self.num_consumers): # Ensure unique ID across all agents - consumer = ConsumerAgent(self.num_agents + i, self) - self.schedule.add(consumer) + consumer = ConsumerAgent(self) + self.consumer_agents.add(consumer) # Place consumer randomly on the grid x = self.random.randrange(self.grid.width) @@ -218,8 +215,8 @@ def step(self): """Advance the model by one step.""" # Collect data for the current step. self.datacollector.collect(self) - # Activate the next agent in the schedule. - self.schedule.step() + # Activate all agents in random order + self.agents.shuffle_do("step") # Update market dynamics based on the latest actions self.recalculate_market_share() diff --git a/examples/ising/README.md b/examples/ising/README.md new file mode 100644 index 00000000..6b8b02a1 --- /dev/null +++ b/examples/ising/README.md @@ -0,0 +1,25 @@ +# Ising Model + +## Summary + +The Ising model (or Lenz–Ising model), named after the physicists Ernst Ising and Wilhelm Lenz, is a mathematical model of ferromagnetism in statistical mechanics. The model consists of discrete variables that represent magnetic dipole moments of atomic "spins" that can be in one of two states (+1 or −1). The spins are arranged in a graph, usually a lattice (where the local structure repeats periodically in all directions), allowing each spin to interact with its neighbors. Neighboring spins that agree have a lower energy than those that disagree; the system tends to the lowest energy but heat disturbs this tendency, thus creating the possibility of different structural phases. The model allows the identification of phase transitions as a simplified model of reality. The two-dimensional square-lattice Ising model is one of the simplest statistical models to show a phase transition. + +## How to Run + +To run the model interactively, run ``solara run run.py`` in this directory. e.g. + +Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press ``run``. + +## Things to Try + +What happens when the temperature slider is very high? (This is called the "paramagnetic" state.) Try this with different **Spin Up Probability** values. + +What happens when the temperature slider is set very low? (This is called the "ferromagnetic" state.) Again, try this with different **Spin Up Probability** values. + +Between these two very different behaviors is a transition point. On an infinite grid, the transition point can be proved to be $2 / ln (1 + sqrt 2)$, which is about 2.27. On a large enough finite toroidal grid, the transition point is near this number. + +## References + +(NetLogo Ising)[https://ccl.northwestern.edu/netlogo/models/Ising] + +(Introduction to Monte Carlo methods for an Ising Model of a Ferromagnet)[https://arxiv.org/pdf/0803.0217] diff --git a/examples/ising/app.py b/examples/ising/app.py new file mode 100644 index 00000000..d3c322ac --- /dev/null +++ b/examples/ising/app.py @@ -0,0 +1,39 @@ +import solara +from ising.model import IsingModel +from ising.portrayal import portray_spin +from mesa.visualization import ( + SolaraViz, + make_space_matplotlib, +) + +model_params = { + "temperature": { + "type": "SliderFloat", + "label": "Temperature", + "value": 2, + "min": 0.01, + "max": 10, + "step": 0.06, + }, + "spin_up_probability": { + "type": "SliderFloat", + "label": "Spin Up Probability", + "value": 0.5, + "min": 0, + "max": 1, + "step": 0.05, + }, +} + +SpaceGraph = make_space_matplotlib(portray_spin) +model = IsingModel() + + +@solara.component +def Page(): + SolaraViz( + model, + components=[SpaceGraph], + model_params=model_params, + name="Ising Model", + ) diff --git a/examples/ising/ising/model.py b/examples/ising/ising/model.py new file mode 100644 index 00000000..09fa3e85 --- /dev/null +++ b/examples/ising/ising/model.py @@ -0,0 +1,43 @@ +import mesa +import numpy as np + +from .spin import Spin + + +class IsingModel(mesa.Model): + def __init__( + self, + width=50, + height=50, + spin_up_probability: float = 0.7, + temperature: float = 1, + ): + super().__init__() + self.temperature = temperature + self.grid = mesa.space.SingleGrid(width, height, torus=True) + + for contents, (x, y) in self.grid.coord_iter(): + cell = Spin((x, y), self, Spin.DOWN) + if self.random.random() < spin_up_probability: + cell.state = cell.UP + self.grid.place_agent(cell, (x, y)) + + self.running = True + + def step(self): + agents_list = list(self.agents) + for i in range(1000): + random_spin = self.random.choice(agents_list) + dE = self.get_energy_change(random_spin) + if dE < 0 or self.random.random() < self.boltzmann_factor(dE): + random_spin.state *= -1 + + def get_energy_change(self, spin: Spin): + neighbors = spin.neighbors() + sum_over_neighbors = 0 + for neighbor in neighbors: + sum_over_neighbors += neighbor.state + return sum_over_neighbors * 2 * spin.state + + def boltzmann_factor(self, dE): + return np.exp(-dE / self.temperature) diff --git a/examples/ising/ising/portrayal.py b/examples/ising/ising/portrayal.py new file mode 100644 index 00000000..b4aad837 --- /dev/null +++ b/examples/ising/ising/portrayal.py @@ -0,0 +1,15 @@ +def portray_spin(spin): + """ + 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 spin is None: + raise AssertionError + return { + "shape": "s", + "x": spin.x, + "y": spin.y, + "color": "grey" if spin.state is spin.UP else "black", + } diff --git a/examples/ising/ising/spin.py b/examples/ising/ising/spin.py new file mode 100644 index 00000000..cb06a04d --- /dev/null +++ b/examples/ising/ising/spin.py @@ -0,0 +1,19 @@ +import mesa + + +class Spin(mesa.Agent): + """Represents a single ALIVE or DEAD cell in the simulation.""" + + UP = 1 + DOWN = -1 + + def __init__(self, position, model, init_state): + """ + Create a cell, in the given state, at the given x, y position. + """ + super().__init__(model) + self.x, self.y = position + self.state = init_state + + def neighbors(self): + return self.model.grid.iter_neighbors((self.x, self.y), moore=True) diff --git a/examples/pd_grid/analysis.ipynb b/examples/pd_grid/analysis.ipynb index 1fe69759..e3f52170 100644 --- a/examples/pd_grid/analysis.ipynb +++ b/examples/pd_grid/analysis.ipynb @@ -72,7 +72,7 @@ " grid[y][x] = 0\n", " ax.pcolormesh(grid, cmap=bwr, vmin=0, vmax=1)\n", " ax.axis(\"off\")\n", - " ax.set_title(f\"Steps: {model.schedule.steps}\")" + " ax.set_title(f\"Steps: {model.steps}\")" ] }, { diff --git a/examples/pd_grid/pd_grid/agent.py b/examples/pd_grid/pd_grid/agent.py index e289169f..85121327 100644 --- a/examples/pd_grid/pd_grid/agent.py +++ b/examples/pd_grid/pd_grid/agent.py @@ -4,18 +4,16 @@ class PDAgent(mesa.Agent): """Agent member of the iterated, spatial prisoner's dilemma model.""" - def __init__(self, pos, model, starting_move=None): + def __init__(self, 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 + super().__init__(model) self.score = 0 if starting_move: self.move = starting_move @@ -35,7 +33,7 @@ def step(self): best_neighbor = max(neighbors, key=lambda a: a.score) self.next_move = best_neighbor.move - if self.model.schedule_type != "Simultaneous": + if self.model.activation_order != "Simultaneous": self.advance() def advance(self): @@ -44,7 +42,7 @@ def advance(self): def increment_score(self): neighbors = self.model.grid.get_neighbors(self.pos, True) - if self.model.schedule_type == "Simultaneous": + if self.model.activation_order == "Simultaneous": moves = [neighbor.next_move for neighbor in neighbors] else: moves = [neighbor.move for neighbor in neighbors] diff --git a/examples/pd_grid/pd_grid/model.py b/examples/pd_grid/pd_grid/model.py index b970c0f4..b1c2a05c 100644 --- a/examples/pd_grid/pd_grid/model.py +++ b/examples/pd_grid/pd_grid/model.py @@ -6,11 +6,7 @@ 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, - } + activation_regimes = ["Sequential", "Random", "Simultaneous"] # This dictionary holds the payoff for this agent, # keyed on: (my_move, other_move) @@ -18,33 +14,31 @@ class PdGrid(mesa.Model): 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 + self, width=50, height=50, activation_order="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". + activation_order: Can be "Sequential", "Random", or "Simultaneous". Determines the agent activation regime. payoffs: (optional) Dictionary of (move, neighbor_move) payoffs. """ super().__init__() + self.activation_order = activation_order 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) + agent = PDAgent(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"] + [a for a in m.agents if a.move == "C"] ) } ) @@ -53,8 +47,19 @@ def __init__( self.datacollector.collect(self) def step(self): - self.schedule.step() - # collect data + # Activate all agents, based on the activation regime + match self.activation_order: + case "Sequential": + self.agents.do("step") + case "Random": + self.agents.shuffle_do("step") + case "Simultaneous": + self.agents.do("step") + self.agents.do("advance") + case _: + raise ValueError(f"Unknown activation order: {self.activation_order}") + + # Collect data self.datacollector.collect(self) def run(self, n): diff --git a/examples/pd_grid/pd_grid/server.py b/examples/pd_grid/pd_grid/server.py index f2447da3..57785acc 100644 --- a/examples/pd_grid/pd_grid/server.py +++ b/examples/pd_grid/pd_grid/server.py @@ -9,10 +9,10 @@ model_params = { "height": 50, "width": 50, - "schedule_type": mesa.visualization.Choice( - "Scheduler type", + "activation_order": mesa.visualization.Choice( + "Activation regime", value="Random", - choices=list(PdGrid.schedule_types.keys()), + choices=PdGrid.activation_regimes, ), } diff --git a/examples/pd_grid/readme.md b/examples/pd_grid/readme.md index 8b4bc40c..51b91fd4 100644 --- a/examples/pd_grid/readme.md +++ b/examples/pd_grid/readme.md @@ -28,7 +28,7 @@ Launch the ``Demographic Prisoner's Dilemma Activation Schedule.ipynb`` notebook ## 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. +* ``pd_grid/``: contains the model and agent classes; the model takes a ``activation_order`` string as an argument, which determines in which order agents are activated: 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 diff --git a/examples/schelling/analysis.ipynb b/examples/schelling/analysis.ipynb index 50f382c6..71d925c1 100644 --- a/examples/schelling/analysis.ipynb +++ b/examples/schelling/analysis.ipynb @@ -65,9 +65,9 @@ } ], "source": [ - "while model.running and model.schedule.steps < 100:\n", + "while model.running and model.steps < 100:\n", " model.step()\n", - "print(model.schedule.steps) # Show how many steps have actually run" + "print(model.steps) # Show how many steps have actually run" ] }, { @@ -328,7 +328,7 @@ " 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", + " for agent in model.agents:\n", " segregated = True\n", " for neighbor in model.grid.iter_neighbors(agent.pos, True):\n", " if neighbor.type != agent.type:\n", @@ -336,7 +336,7 @@ " break\n", " if segregated:\n", " segregated_agents += 1\n", - " return segregated_agents / model.schedule.get_agent_count()" + " return segregated_agents / len(model.agents)" ] }, { diff --git a/examples/schelling/model.py b/examples/schelling/model.py index dfba4efb..e995f31e 100644 --- a/examples/schelling/model.py +++ b/examples/schelling/model.py @@ -6,16 +6,15 @@ class SchellingAgent(mesa.Agent): Schelling segregation agent """ - def __init__(self, unique_id, model, agent_type): + def __init__(self, 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__(unique_id, model) + super().__init__(model) self.type = agent_type def step(self): @@ -68,7 +67,6 @@ def __init__( self.homophily = homophily self.radius = radius - self.schedule = mesa.time.RandomActivation(self) self.grid = mesa.space.SingleGrid(width, height, torus=True) self.happy = 0 @@ -83,9 +81,8 @@ def __init__( for _, pos in self.grid.coord_iter(): if self.random.random() < self.density: agent_type = 1 if self.random.random() < self.minority_pc else 0 - agent = SchellingAgent(self.next_id(), self, agent_type) + agent = SchellingAgent(self, agent_type) self.grid.place_agent(agent, pos) - self.schedule.add(agent) self.datacollector.collect(self) @@ -94,9 +91,9 @@ def step(self): Run one step of the model. """ self.happy = 0 # Reset counter of happy agents - self.schedule.step() + self.agents.shuffle_do("step") self.datacollector.collect(self) - if self.happy == self.schedule.get_agent_count(): + if self.happy == len(self.agents): self.running = False diff --git a/examples/schelling_experimental/analysis.ipynb b/examples/schelling_experimental/analysis.ipynb index 50f382c6..71d925c1 100644 --- a/examples/schelling_experimental/analysis.ipynb +++ b/examples/schelling_experimental/analysis.ipynb @@ -65,9 +65,9 @@ } ], "source": [ - "while model.running and model.schedule.steps < 100:\n", + "while model.running and model.steps < 100:\n", " model.step()\n", - "print(model.schedule.steps) # Show how many steps have actually run" + "print(model.steps) # Show how many steps have actually run" ] }, { @@ -328,7 +328,7 @@ " 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", + " for agent in model.agents:\n", " segregated = True\n", " for neighbor in model.grid.iter_neighbors(agent.pos, True):\n", " if neighbor.type != agent.type:\n", @@ -336,7 +336,7 @@ " break\n", " if segregated:\n", " segregated_agents += 1\n", - " return segregated_agents / model.schedule.get_agent_count()" + " return segregated_agents / len(model.agents)" ] }, { diff --git a/examples/schelling_experimental/app.py b/examples/schelling_experimental/app.py index 634e0f99..e2b45583 100644 --- a/examples/schelling_experimental/app.py +++ b/examples/schelling_experimental/app.py @@ -1,4 +1,4 @@ -from mesa.visualization.jupyter_viz import JupyterViz, Slider, make_text +from mesa.visualization import Slider, SolaraViz, make_plot_measure from model import Schelling @@ -21,10 +21,13 @@ def agent_portrayal(agent): "height": 20, } -page = JupyterViz( - Schelling, - model_params, - measures=["happy", make_text(get_happy_agents)], - agent_portrayal=agent_portrayal, +model1 = Schelling(20, 20, 0.8, 0.2, 3) + +HappyPlot = make_plot_measure("happy") + +page = SolaraViz( + model1, + components=[HappyPlot, get_happy_agents], + model_params=model_params, ) page # noqa diff --git a/examples/schelling_experimental/model.py b/examples/schelling_experimental/model.py index c91b034a..d7239a97 100644 --- a/examples/schelling_experimental/model.py +++ b/examples/schelling_experimental/model.py @@ -6,17 +6,14 @@ class SchellingAgent(mesa.Agent): Schelling segregation agent """ - def __init__(self, pos, model, agent_type): + def __init__(self, model, agent_type): """ Create a new Schelling agent. Args: - unique_id: Unique identifier for the agent. - pos: Agent initial location. agent_type: Indicator for the agent's type (minority=1, majority=0) """ - super().__init__(pos, model) - self.pos = pos + super().__init__(model) self.type = agent_type def step(self): @@ -57,7 +54,7 @@ def __init__(self, width=20, height=20, density=0.8, minority_pc=0.2, homophily= for _, pos in self.grid.coord_iter(): if self.random.random() < density: agent_type = 1 if self.random.random() < minority_pc else 0 - agent = SchellingAgent(pos, self, agent_type) + agent = SchellingAgent(self, agent_type) self.grid.place_agent(agent, pos) self.datacollector.collect(self) @@ -67,9 +64,7 @@ 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.agents.shuffle().do("step") - # Must be before data collection. - self._advance_time() # Temporary API; will be finalized by Mesa 3.0 release + self.agents.shuffle_do("step") # collect data self.datacollector.collect(self) diff --git a/examples/shape_example/shape_example/model.py b/examples/shape_example/shape_example/model.py index 23f47a6f..2fa82fd7 100644 --- a/examples/shape_example/shape_example/model.py +++ b/examples/shape_example/shape_example/model.py @@ -2,9 +2,8 @@ class Walker(mesa.Agent): - def __init__(self, unique_id, model, pos, heading=(1, 0)): - super().__init__(unique_id, model) - self.pos = pos + def __init__(self, model, heading=(1, 0)): + super().__init__(model) self.heading = heading self.headings = {(1, 0), (0, 1), (-1, 0), (0, -1)} @@ -15,7 +14,7 @@ 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 @@ -31,10 +30,10 @@ def make_walker_agents(self): # 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) + a = Walker(self, heading) + self.grid.place_agent(a, pos) unique_id += 1 def step(self): - self.schedule.step() + self.agents.shuffle_do("step") diff --git a/examples/sugarscape_cg/Readme.md b/examples/sugarscape_cg/Readme.md index 948272b6..0334a628 100644 --- a/examples/sugarscape_cg/Readme.md +++ b/examples/sugarscape_cg/Readme.md @@ -21,7 +21,7 @@ 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 + - Dynamically removing agents from the grid and model when they die ## Installation @@ -44,7 +44,6 @@ Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and p ## 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. diff --git a/examples/sugarscape_cg/sugarscape_cg/agents.py b/examples/sugarscape_cg/sugarscape_cg/agents.py index 6e245eaa..a75f07c2 100644 --- a/examples/sugarscape_cg/sugarscape_cg/agents.py +++ b/examples/sugarscape_cg/sugarscape_cg/agents.py @@ -17,11 +17,8 @@ def get_distance(pos_1, pos_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 + def __init__(self, model, moore=False, sugar=0, metabolism=0, vision=0): + super().__init__(model) self.moore = moore self.sugar = sugar self.metabolism = metabolism @@ -70,12 +67,12 @@ def step(self): self.eat() if self.sugar <= 0: self.model.grid.remove_agent(self) - self.model.schedule.remove(self) + self.remove() class Sugar(mesa.Agent): - def __init__(self, unique_id, pos, model, max_sugar): - super().__init__(unique_id, model) + def __init__(self, model, max_sugar): + super().__init__(model) self.amount = max_sugar self.max_sugar = max_sugar diff --git a/examples/sugarscape_cg/sugarscape_cg/model.py b/examples/sugarscape_cg/sugarscape_cg/model.py index e1857cb0..667e9b37 100644 --- a/examples/sugarscape_cg/sugarscape_cg/model.py +++ b/examples/sugarscape_cg/sugarscape_cg/model.py @@ -37,23 +37,19 @@ def __init__(self, width=50, height=50, initial_population=100): 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)} + {"SsAgent": lambda m: len(m.agents_by_type[SsAgent])} ) # Create sugar import numpy as np sugar_distribution = np.genfromtxt(Path(__file__).parent / "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 + sugar = Sugar(self, max_sugar) self.grid.place_agent(sugar, (x, y)) - self.schedule.add(sugar) # Create agent: for i in range(self.initial_population): @@ -62,34 +58,31 @@ def __init__(self, width=50, height=50, initial_population=100): 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 + ssa = SsAgent(self, False, sugar, metabolism, vision) self.grid.place_agent(ssa, (x, y)) - self.schedule.add(ssa) self.running = True self.datacollector.collect(self) def step(self): - self.schedule.step() + # Step suger and agents + self.agents_by_type[Sugar].do("step") + self.agents_by_type[SsAgent].shuffle_do("step") # collect data self.datacollector.collect(self) if self.verbose: - print([self.schedule.time, self.schedule.get_type_count(SsAgent)]) + print(f"Step: {self.steps}, SsAgents: {len(self.agents_by_type[SsAgent])}") def run_model(self, step_count=200): if self.verbose: print( - "Initial number Sugarscape Agent: ", - self.schedule.get_type_count(SsAgent), + f"Initial number Sugarscape Agents: {len(self.agents_by_type[SsAgent])}" ) for i in range(step_count): self.step() if self.verbose: - print("") print( - "Final number Sugarscape Agent: ", - self.schedule.get_type_count(SsAgent), + f"\nFinal number Sugarscape Agents: {len(self.agents_by_type[SsAgent])}" ) diff --git a/examples/sugarscape_g1mt/app.py b/examples/sugarscape_g1mt/app.py index 2196995d..146d3d5c 100644 --- a/examples/sugarscape_g1mt/app.py +++ b/examples/sugarscape_g1mt/app.py @@ -1,12 +1,12 @@ import numpy as np import solara from matplotlib.figure import Figure -from mesa.experimental import JupyterViz +from mesa.visualization import SolaraViz, make_plot_measure from sugarscape_g1mt.model import SugarscapeG1mt from sugarscape_g1mt.trader_agents import Trader -def space_drawer(model, agent_portrayal): +def SpaceDrawer(model): def portray(g): layers = { "sugar": [[np.nan for j in range(g.height)] for i in range(g.width)], @@ -42,7 +42,7 @@ def portray(g): # Trader ax.scatter(**out["trader"]) ax.set_axis_off() - solara.FigureMatplotlib(fig) + return solara.FigureMatplotlib(fig) model_params = { @@ -50,12 +50,12 @@ def portray(g): "height": 50, } -page = JupyterViz( - SugarscapeG1mt, - model_params, - measures=["Trader", "Price"], +model1 = SugarscapeG1mt(50, 50) + +page = SolaraViz( + model1, + components=[SpaceDrawer, make_plot_measure(["Trader", "Price"])], name="Sugarscape {G1, M, T}", - space_drawer=space_drawer, play_interval=1500, ) page # noqa diff --git a/examples/sugarscape_g1mt/run.py b/examples/sugarscape_g1mt/run.py index 1522adb3..f1056fa4 100644 --- a/examples/sugarscape_g1mt/run.py +++ b/examples/sugarscape_g1mt/run.py @@ -61,47 +61,45 @@ def assess_results(results, single_agent): # Run the model +def main(): + args = sys.argv[1:] + + if len(args) == 0: + server.launch() + + elif args[0] == "-s": + print("Running Single Model") + model = SugarscapeG1mt() + model.run_model() + model_results = model.datacollector.get_model_vars_dataframe() + model_results["Step"] = model_results.index + agent_results = model.datacollector.get_agent_vars_dataframe() + agent_results = agent_results.reset_index() + assess_results(model_results, agent_results) + + elif args[0] == "-b": + print("Conducting a Batch Run") + params = { + "width": 50, + "height": 50, + "vision_min": range(1, 4), + "metabolism_max": [2, 3, 4, 5], + } + + results_batch = mesa.batch_run( + SugarscapeG1mt, + parameters=params, + iterations=1, + number_processes=1, + data_collection_period=1, + display_progress=True, + ) + + assess_results(results_batch, None) -args = sys.argv[1:] - -if len(args) == 0: - server.launch() - -elif args[0] == "-s": - print("Running Single Model") - # instantiate the model - model = SugarscapeG1mt() - # run the model - model.run_model() - # Get results - model_results = model.datacollector.get_model_vars_dataframe() - # Convert to make similar to batch_run_results - model_results["Step"] = model_results.index - agent_results = model.datacollector.get_agent_vars_dataframe() - agent_results = agent_results.reset_index() - # assess the results - assess_results(model_results, agent_results) - -elif args[0] == "-b": - print("Conducting a Batch Run") - # Batch Run - params = { - "width": 50, - "height": 50, - "vision_min": range(1, 4), - "metabolism_max": [2, 3, 4, 5], - } - - results_batch = mesa.batch_run( - SugarscapeG1mt, - parameters=params, - iterations=1, - number_processes=1, - data_collection_period=1, - display_progress=True, - ) - - assess_results(results_batch, None) - -else: - raise Exception("Option not found") + else: + raise Exception("Option not found") + + +if __name__ == "__main__": + main() diff --git a/examples/sugarscape_g1mt/sugarscape_g1mt/model.py b/examples/sugarscape_g1mt/sugarscape_g1mt/model.py index f88f1c2c..91bb001d 100644 --- a/examples/sugarscape_g1mt/sugarscape_g1mt/model.py +++ b/examples/sugarscape_g1mt/sugarscape_g1mt/model.py @@ -68,22 +68,17 @@ def __init__( self.enable_trade = enable_trade self.running = True - # initiate activation schedule - self.schedule = mesa.time.RandomActivationByType(self) # initiate mesa grid class self.grid = mesa.space.MultiGrid(self.width, self.height, torus=False) # initiate datacollector self.datacollector = mesa.DataCollector( model_reporters={ - "Trader": lambda m: m.schedule.get_type_count(Trader), + "Trader": lambda m: len(m.agents_by_type[Trader]), "Trade Volume": lambda m: sum( - len(a.trade_partners) - for a in m.schedule.agents_by_type[Trader].values() + len(a.trade_partners) for a in m.agents_by_type[Trader] ), "Price": lambda m: geometric_mean( - flatten( - [a.prices for a in m.schedule.agents_by_type[Trader].values()] - ) + flatten([a.prices for a in m.agents_by_type[Trader]]) ), }, agent_reporters={"Trade Network": lambda a: get_trade(a)}, @@ -93,16 +88,13 @@ def __init__( sugar_distribution = np.genfromtxt(Path(__file__).parent / "sugar-map.txt") spice_distribution = np.flip(sugar_distribution, 1) - agent_id = 0 for _, (x, y) in self.grid.coord_iter(): max_sugar = sugar_distribution[x, y] max_spice = spice_distribution[x, y] - resource = Resource(agent_id, self, (x, y), max_sugar, max_spice) - self.schedule.add(resource) + resource = Resource(self, max_sugar, max_spice) self.grid.place_agent(resource, (x, y)) - agent_id += 1 - for i in range(self.initial_population): + for _ in range(self.initial_population): # get agent position x = self.random.randrange(self.width) y = self.random.randrange(self.height) @@ -121,9 +113,7 @@ def __init__( vision = int(self.random.uniform(self.vision_min, self.vision_max + 1)) # create Trader object trader = Trader( - agent_id, self, - (x, y), moore=False, sugar=sugar, spice=spice, @@ -133,20 +123,6 @@ def __init__( ) # place agent self.grid.place_agent(trader, (x, y)) - self.schedule.add(trader) - agent_id += 1 - - def randomize_traders(self): - """ - helper function for self.step() - - puts traders in randomized list for step function - """ - - traders_shuffle = list(self.schedule.agents_by_type[Trader].values()) - self.random.shuffle(traders_shuffle) - - return traders_shuffle def step(self): """ @@ -154,13 +130,12 @@ def step(self): and then randomly activates traders """ # step Resource agents - for resource in self.schedule.agents_by_type[Resource].values(): - resource.step() + self.agents_by_type[Resource].do("step") # step trader agents # to account for agent death and removal we need a seperate data strcuture to # iterate - trader_shuffle = self.randomize_traders() + trader_shuffle = self.agents_by_type[Trader].shuffle() for agent in trader_shuffle: agent.prices = [] @@ -171,16 +146,14 @@ def step(self): if not self.enable_trade: # If trade is not enabled, return early - self._steps += 1 self.datacollector.collect(self) return - trader_shuffle = self.randomize_traders() + trader_shuffle = self.agents_by_type[Trader].shuffle() for agent in trader_shuffle: agent.trade_with_neighbors() - self._steps += 1 # collect model level data self.datacollector.collect(self) """ @@ -196,11 +169,11 @@ def step(self): """ # Need to remove excess data # Create local variable to store trade data - agent_trades = self.datacollector._agent_records[self._steps] + agent_trades = self.datacollector._agent_records[self.steps] # Get rid of all None to reduce data storage needs agent_trades = [agent for agent in agent_trades if agent[2] is not None] # Reassign the dictionary value with lean trade data - self.datacollector._agent_records[self._steps] = agent_trades + self.datacollector._agent_records[self.steps] = agent_trades def run_model(self, step_count=1000): for i in range(step_count): diff --git a/examples/sugarscape_g1mt/sugarscape_g1mt/resource_agents.py b/examples/sugarscape_g1mt/sugarscape_g1mt/resource_agents.py index 18d11cd6..042f1672 100644 --- a/examples/sugarscape_g1mt/sugarscape_g1mt/resource_agents.py +++ b/examples/sugarscape_g1mt/sugarscape_g1mt/resource_agents.py @@ -9,9 +9,8 @@ class Resource(mesa.Agent): - grows 1 amount of spice at each turn """ - def __init__(self, unique_id, model, pos, max_sugar, max_spice): - super().__init__(unique_id, model) - self.pos = pos + def __init__(self, model, max_sugar, max_spice): + super().__init__(model) self.sugar_amount = max_sugar self.max_sugar = max_sugar self.spice_amount = max_spice diff --git a/examples/sugarscape_g1mt/sugarscape_g1mt/trader_agents.py b/examples/sugarscape_g1mt/sugarscape_g1mt/trader_agents.py index 96bc8c5b..2c63a8a1 100644 --- a/examples/sugarscape_g1mt/sugarscape_g1mt/trader_agents.py +++ b/examples/sugarscape_g1mt/sugarscape_g1mt/trader_agents.py @@ -29,9 +29,7 @@ class Trader(mesa.Agent): def __init__( self, - unique_id, model, - pos, moore=False, sugar=0, spice=0, @@ -39,8 +37,7 @@ def __init__( metabolism_spice=0, vision=0, ): - super().__init__(unique_id, model) - self.pos = pos + super().__init__(model) self.moore = moore self.sugar = sugar self.spice = spice @@ -306,7 +303,7 @@ def maybe_die(self): if self.is_starved(): self.model.grid.remove_agent(self) - self.model.schedule.remove(self) + self.remove() def trade_with_neighbors(self): """ diff --git a/examples/sugarscape_g1mt/tests.py b/examples/sugarscape_g1mt/tests.py index bcfcf739..274afa6b 100644 --- a/examples/sugarscape_g1mt/tests.py +++ b/examples/sugarscape_g1mt/tests.py @@ -23,7 +23,7 @@ def test_decreasing_price_variance(): model.datacollector._new_model_reporter( "price_variance", lambda m: np.var( - flatten([a.prices for a in m.schedule.agents_by_type[Trader].values()]) + flatten([a.prices for a in m.agents_by_type[Trader].values()]) ), ) model.run_model(step_count=50) @@ -40,7 +40,7 @@ def calculate_carrying_capacities(enable_trade): for vision_max in visions: model = SugarscapeG1mt(vision_max=vision_max, enable_trade=enable_trade) model.run_model(step_count=50) - carrying_capacities.append(len(model.schedule.agents_by_type[Trader])) + carrying_capacities.append(len(model.agents_by_type[Trader])) return carrying_capacities # Carrying capacity should increase over mean vision (figure IV-6). diff --git a/examples/virus_on_network/app.py b/examples/virus_on_network/app.py index a9661a4d..caa1360f 100644 --- a/examples/virus_on_network/app.py +++ b/examples/virus_on_network/app.py @@ -3,7 +3,7 @@ import solara from matplotlib.figure import Figure from matplotlib.ticker import MaxNLocator -from mesa.experimental import JupyterViz, make_text +from mesa.visualization import SolaraViz, make_space_matplotlib from virus_on_network.model import State, VirusOnNetwork, number_infected @@ -57,7 +57,7 @@ def make_plot(model): fig.legend() # Set integer x axis ax.xaxis.set_major_locator(MaxNLocator(integer=True)) - solara.FigureMatplotlib(fig) + return solara.FigureMatplotlib(fig) model_params = { @@ -119,14 +119,17 @@ def make_plot(model): }, } -page = JupyterViz( - VirusOnNetwork, - model_params, - measures=[ +SpacePlot = make_space_matplotlib(agent_portrayal) + +model1 = VirusOnNetwork() + +page = SolaraViz( + model1, + [ + SpacePlot, make_plot, - make_text(get_resistant_susceptible_ratio), + get_resistant_susceptible_ratio, ], name="Virus Model", - agent_portrayal=agent_portrayal, ) page # noqa diff --git a/examples/virus_on_network/virus_on_network/model.py b/examples/virus_on_network/virus_on_network/model.py index a33e7545..d892a0c4 100644 --- a/examples/virus_on_network/virus_on_network/model.py +++ b/examples/virus_on_network/virus_on_network/model.py @@ -47,7 +47,7 @@ def __init__( 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 ) @@ -65,9 +65,8 @@ def __init__( ) # Create agents - for i, node in enumerate(self.G.nodes()): + for node in self.G.nodes(): a = VirusAgent( - i, self, State.SUSCEPTIBLE, self.virus_spread_chance, @@ -75,7 +74,7 @@ def __init__( self.recovery_chance, self.gain_resistance_chance, ) - self.schedule.add(a) + # Add the agent to the node self.grid.place_agent(a, node) @@ -96,7 +95,7 @@ def resistant_susceptible_ratio(self): return math.inf def step(self): - self.schedule.step() + self.agents.shuffle_do("step") # collect data self.datacollector.collect(self) @@ -112,7 +111,6 @@ class VirusAgent(mesa.Agent): def __init__( self, - unique_id, model, initial_state, virus_spread_chance, @@ -120,7 +118,7 @@ def __init__( recovery_chance, gain_resistance_chance, ): - super().__init__(unique_id, model) + super().__init__(model) self.state = initial_state diff --git a/examples/wolf_sheep/wolf_sheep/agents.py b/examples/wolf_sheep/wolf_sheep/agents.py index 460c4abb..c0b06f3a 100644 --- a/examples/wolf_sheep/wolf_sheep/agents.py +++ b/examples/wolf_sheep/wolf_sheep/agents.py @@ -12,8 +12,8 @@ class Sheep(RandomWalker): energy = None - def __init__(self, unique_id, pos, model, moore, energy=None): - super().__init__(unique_id, pos, model, moore=moore) + def __init__(self, model, moore, energy=None): + super().__init__(model, moore=moore) self.energy = energy def step(self): @@ -37,18 +37,15 @@ def step(self): # Death if self.energy < 0: self.model.grid.remove_agent(self) - self.model.schedule.remove(self) + self.remove() 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 - ) + lamb = Sheep(self.model, self.moore, self.energy) self.model.grid.place_agent(lamb, self.pos) - self.model.schedule.add(lamb) class Wolf(RandomWalker): @@ -58,8 +55,8 @@ class Wolf(RandomWalker): energy = None - def __init__(self, unique_id, pos, model, moore, energy=None): - super().__init__(unique_id, pos, model, moore=moore) + def __init__(self, model, moore, energy=None): + super().__init__(model, moore=moore) self.energy = energy def step(self): @@ -76,21 +73,18 @@ def step(self): # Kill the sheep self.model.grid.remove_agent(sheep_to_eat) - self.model.schedule.remove(sheep_to_eat) + sheep_to_eat.remove() # Death or reproduction if self.energy < 0: self.model.grid.remove_agent(self) - self.model.schedule.remove(self) + self.remove() 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) + cub = Wolf(self.model, self.moore, self.energy) + self.model.grid.place_agent(cub, self.pos) class GrassPatch(mesa.Agent): @@ -98,7 +92,7 @@ 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): + def __init__(self, model, fully_grown, countdown): """ Creates a new patch of grass @@ -106,10 +100,9 @@ def __init__(self, unique_id, pos, model, fully_grown, countdown): 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) + super().__init__(model) self.fully_grown = fully_grown self.countdown = countdown - self.pos = pos def step(self): if not self.fully_grown: diff --git a/examples/wolf_sheep/wolf_sheep/model.py b/examples/wolf_sheep/wolf_sheep/model.py index 2626f958..85b60b73 100644 --- a/examples/wolf_sheep/wolf_sheep/model.py +++ b/examples/wolf_sheep/wolf_sheep/model.py @@ -12,7 +12,6 @@ import mesa from .agents import GrassPatch, Sheep, Wolf -from .scheduler import RandomActivationByTypeFiltered class WolfSheep(mesa.Model): @@ -35,8 +34,6 @@ class WolfSheep(mesa.Model): 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." ) @@ -81,35 +78,33 @@ def __init__( 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 - ), - } - ) + + collectors = { + "Wolves": lambda m: len(m.agents_by_type[Wolf]), + "Sheep": lambda m: len(m.agents_by_type[Sheep]), + } + + if grass: + collectors["Grass"] = lambda m: len(m.agents_by_type[GrassPatch]) + + self.datacollector = mesa.DataCollector(collectors) # 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) + sheep = Sheep(self, True, energy) self.grid.place_agent(sheep, (x, y)) - self.schedule.add(sheep) # Create wolves - for i in range(self.initial_wolves): + for _ 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) + wolf = Wolf(self, True, energy) self.grid.place_agent(wolf, (x, y)) - self.schedule.add(wolf) # Create grass patches if self.grass: @@ -121,44 +116,23 @@ def __init__( else: countdown = self.random.randrange(self.grass_regrowth_time) - patch = GrassPatch(self.next_id(), (x, y), self, fully_grown, countdown) + patch = GrassPatch(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() + # This replicated the behavior of the old RandomActivationByType scheduler + # when using step(shuffle_types=True, shuffle_agents=True). + # Conceptually, it can be argued that this should be modelled differently. + self.random.shuffle(self.agent_types) + for agent_type in self.agent_types: + self.agents_by_type[agent_type].do("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 index 49219fa7..a204f9cc 100644 --- a/examples/wolf_sheep/wolf_sheep/random_walk.py +++ b/examples/wolf_sheep/wolf_sheep/random_walk.py @@ -18,7 +18,7 @@ class RandomWalker(mesa.Agent): y = None moore = True - def __init__(self, unique_id, pos, model, moore=True): + def __init__(self, model, moore=True): """ grid: The MultiGrid object in which the agent lives. x: The agent's current x coordinate @@ -26,8 +26,7 @@ def __init__(self, unique_id, pos, model, moore=True): moore: If True, may move in all 8 directions. Otherwise, only up, down, left, right. """ - super().__init__(unique_id, model) - self.pos = pos + super().__init__(model) self.moore = moore def random_move(self): diff --git a/examples/wolf_sheep/wolf_sheep/scheduler.py b/examples/wolf_sheep/wolf_sheep/scheduler.py deleted file mode 100644 index 97424a55..00000000 --- a/examples/wolf_sheep/wolf_sheep/scheduler.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Callable, Optional, Type - -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: Optional[Callable[[mesa.Agent], bool]] = None, - ) -> int: - """ - Returns the current number of agents of certain type in the queue - that satisfy the filter function. - """ - if type_class not in self.agents_by_type: - return 0 - 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/test_random_walk.py b/examples/wolf_sheep/wolf_sheep/test_random_walk.py index d2340fed..393a46b1 100644 --- a/examples/wolf_sheep/wolf_sheep/test_random_walk.py +++ b/examples/wolf_sheep/wolf_sheep/test_random_walk.py @@ -5,7 +5,6 @@ from mesa import Model from mesa.space import MultiGrid -from mesa.time import RandomActivation from mesa.visualization.TextVisualization import TextGrid, TextVisualization from wolf_sheep.random_walk import RandomWalker @@ -40,17 +39,15 @@ def __init__(self, width, height, agent_count): 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() + self.agents.shuffle_do("step") class WalkerWorldViz(TextVisualization): diff --git a/gis/README.md b/gis/README.md deleted file mode 100644 index 0f6fe9f1..00000000 --- a/gis/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# GIS Examples - -## Vector Data - -- [GeoSchelling Model (Polygons)](https://github.com/projectmesa/mesa-examples/tree/main/gis/geo_schelling) -- [GeoSchelling Model (Points & Polygons)](https://github.com/projectmesa/mesa-examples/tree/main/gis/geo_schelling_points) -- [GeoSIR Epidemics Model](https://github.com/projectmesa/mesa-examples/tree/main/gis/geo_sir) -- [Agents and Networks Model](https://github.com/projectmesa/mesa-examples/tree/main/gis/agents_and_networks) - -## Raster Data - -- [Rainfall Model](https://github.com/projectmesa/mesa-examples/tree/main/gis/rainfall) -- [Urban Growth Model](https://github.com/projectmesa/mesa-examples/tree/main/gis/urban_growth) - -## Raster and Vector Data Overlay - -- [Population Model](https://github.com/projectmesa/mesa-examples/tree/main/gis/population) diff --git a/gis/agents_and_networks/scripts/run.py b/gis/agents_and_networks/scripts/run.py index 1a4a71a3..8aa0712d 100644 --- a/gis/agents_and_networks/scripts/run.py +++ b/gis/agents_and_networks/scripts/run.py @@ -39,6 +39,7 @@ def make_parser(): "lakes_file": f"data/{args.campus}/hydrop.zip", "rivers_file": f"data/{args.campus}/hydrol.zip", "driveway_file": f"data/{args.campus}/{data_file_prefix}_Rds.zip", + "output_dir": "outputs", "show_walkway": True, "show_lakes_and_rivers": True, "show_driveway": True, diff --git a/gis/agents_and_networks/src/agent/building.py b/gis/agents_and_networks/src/agent/building.py index cc260a3b..7a18bddc 100644 --- a/gis/agents_and_networks/src/agent/building.py +++ b/gis/agents_and_networks/src/agent/building.py @@ -35,3 +35,6 @@ def __eq__(self, other): if isinstance(other, Building): return self.unique_id == other.unique_id return False + + def __hash__(self) -> int: + return hash(self.unique_id) diff --git a/gis/agents_and_networks/src/agent/commuter.py b/gis/agents_and_networks/src/agent/commuter.py index 71c59f1e..85a928bb 100644 --- a/gis/agents_and_networks/src/agent/commuter.py +++ b/gis/agents_and_networks/src/agent/commuter.py @@ -8,8 +8,8 @@ import pyproj from shapely.geometry import LineString, Point -from src.agent.building import Building -from src.space.utils import UnitTransformer, redistribute_vertices +from ..space.utils import UnitTransformer, redistribute_vertices +from .building import Building class Commuter(mg.GeoAgent): diff --git a/gis/agents_and_networks/src/model/model.py b/gis/agents_and_networks/src/model/model.py index 9e4ca37c..4fad3fe0 100644 --- a/gis/agents_and_networks/src/model/model.py +++ b/gis/agents_and_networks/src/model/model.py @@ -1,5 +1,6 @@ import uuid from functools import partial +from pathlib import Path import geopandas as gpd import mesa @@ -7,11 +8,13 @@ import pandas as pd from shapely.geometry import Point -from src.agent.building import Building -from src.agent.commuter import Commuter -from src.agent.geo_agents import Driveway, LakeAndRiver, Walkway -from src.space.campus import Campus -from src.space.road_network import CampusWalkway +from ..agent.building import Building +from ..agent.commuter import Commuter +from ..agent.geo_agents import Driveway, LakeAndRiver, Walkway +from ..space.campus import Campus +from ..space.road_network import CampusWalkway + +script_directory = Path(__file__).resolve().parent def get_time(model) -> pd.Timedelta: @@ -59,14 +62,15 @@ class AgentsAndNetworks(mesa.Model): def __init__( self, - campus: str, - data_crs: str, - buildings_file: str, - walkway_file: str, - lakes_file: str, - rivers_file: str, - driveway_file: str, - num_commuters, + campus="ub", + data_crs="epsg:4326", + buildings_file=script_directory / "../../data/ub/UB_bld.zip", + walkway_file=script_directory / "../../data/ub/UB_walkway_line.zip", + lakes_file=script_directory / "../../data/ub/hydrop.zip", + rivers_file=script_directory / "../../data/ub/hydrol.zip", + driveway_file=script_directory / "../../data/data/ub/UB_Rds.zip", + output_dir=script_directory / "../../outputs", + num_commuters=50, commuter_min_friends=5, commuter_max_friends=10, commuter_happiness_increase=0.5, @@ -94,7 +98,9 @@ def __init__( Commuter.CHANCE_NEW_FRIEND = chance_new_friend self._load_buildings_from_file(buildings_file, crs=model_crs, campus=campus) - self._load_road_vertices_from_file(walkway_file, crs=model_crs, campus=campus) + self._load_road_vertices_from_file( + walkway_file, crs=model_crs, campus=campus, output_dir=output_dir + ) self._set_building_entrance() self.got_to_destination = 0 self._create_commuters() @@ -164,14 +170,16 @@ def _load_buildings_from_file( self.space.add_buildings(buildings) def _load_road_vertices_from_file( - self, walkway_file: str, crs: str, campus: str + self, walkway_file: str, crs: str, campus: str, output_dir: str ) -> None: walkway_df = ( gpd.read_file(walkway_file) .set_crs(self.data_crs, allow_override=True) .to_crs(crs) ) - self.walkway = CampusWalkway(campus=campus, lines=walkway_df["geometry"]) + self.walkway = CampusWalkway( + campus=campus, lines=walkway_df["geometry"], output_dir=output_dir + ) if self.show_walkway: walkway_creator = mg.AgentCreator(Walkway, model=self) walkway = walkway_creator.from_GeoDataFrame(walkway_df) diff --git a/gis/agents_and_networks/src/space/campus.py b/gis/agents_and_networks/src/space/campus.py index 0d415538..5b3ebb35 100644 --- a/gis/agents_and_networks/src/space/campus.py +++ b/gis/agents_and_networks/src/space/campus.py @@ -6,8 +6,8 @@ import mesa_geo as mg from shapely.geometry import Point -from src.agent.building import Building -from src.agent.commuter import Commuter +from ..agent.building import Building +from ..agent.commuter import Commuter class Campus(mg.GeoSpace): diff --git a/gis/agents_and_networks/src/space/road_network.py b/gis/agents_and_networks/src/space/road_network.py index 9f4d1940..4c260afa 100644 --- a/gis/agents_and_networks/src/space/road_network.py +++ b/gis/agents_and_networks/src/space/road_network.py @@ -9,7 +9,7 @@ import pyproj from sklearn.neighbors import KDTree -from src.space.utils import segmented +from .utils import segmented class RoadNetwork: @@ -64,10 +64,10 @@ class CampusWalkway(RoadNetwork): list[mesa.space.FloatCoordinate], ] - def __init__(self, campus, lines) -> None: + def __init__(self, campus, lines, output_dir) -> None: super().__init__(lines) self.campus = campus - self._path_cache_result = f"outputs/{campus}_path_cache_result.pkl" + self._path_cache_result = f"{output_dir}/{campus}_path_cache_result.pkl" try: with open(self._path_cache_result, "rb") as cached_result: self._path_select_cache = pickle.load(cached_result) diff --git a/gis/agents_and_networks/src/visualization/server.py b/gis/agents_and_networks/src/visualization/server.py index 64c43235..c3c04e17 100644 --- a/gis/agents_and_networks/src/visualization/server.py +++ b/gis/agents_and_networks/src/visualization/server.py @@ -1,8 +1,8 @@ import mesa -from src.agent.building import Building -from src.agent.commuter import Commuter -from src.agent.geo_agents import Driveway, LakeAndRiver, Walkway +from ..agent.building import Building +from ..agent.commuter import Commuter +from ..agent.geo_agents import Driveway, LakeAndRiver, Walkway class ClockElement(mesa.visualization.TextElement): diff --git a/gis/geo_schelling/model.py b/gis/geo_schelling/model.py index 0f2d9a7b..613a7e59 100644 --- a/gis/geo_schelling/model.py +++ b/gis/geo_schelling/model.py @@ -1,8 +1,28 @@ import random +from pathlib import Path +import geopandas as gpd +import libpysal import mesa import mesa_geo as mg +script_directory = Path(__file__).resolve().parent + + +def get_largest_connected_components(gdf): + """Get the largest connected component of a GeoDataFrame.""" + # create spatial weights matrix + W = libpysal.weights.Queen.from_dataframe( + gdf, use_index=True, silence_warnings=True + ) + # get component labels + gdf["component"] = W.component_labels + # get the largest component + largest_component = gdf["component"].value_counts().idxmax() + # subset the GeoDataFrame + gdf = gdf[gdf["component"] == largest_component] + return gdf + class SchellingAgent(mg.GeoAgent): """Schelling segregation agent.""" @@ -52,6 +72,7 @@ class GeoSchelling(mesa.Model): """Model class for the Schelling segregation model.""" def __init__(self, density=0.6, minority_pc=0.2, export_data=False): + super().__init__() self.density = density self.minority_pc = minority_pc self.export_data = export_data @@ -66,7 +87,10 @@ def __init__(self, density=0.6, minority_pc=0.2, export_data=False): # Set up the grid with patches for every NUTS region ac = mg.AgentCreator(SchellingAgent, model=self) - agents = ac.from_file("data/nuts_rg_60M_2013_lvl_2.geojson") + data_path = script_directory / "data/nuts_rg_60M_2013_lvl_2.geojson" + agents_gdf = gpd.read_file(data_path) + agents_gdf = get_largest_connected_components(agents_gdf) + agents = ac.from_GeoDataFrame(agents_gdf, unique_id="index") self.space.add_agents(agents) # Set up agents diff --git a/gis/geo_schelling/server.py b/gis/geo_schelling/server.py index 0871e219..c8e85d09 100644 --- a/gis/geo_schelling/server.py +++ b/gis/geo_schelling/server.py @@ -38,9 +38,7 @@ def schelling_draw(agent): happy_element = HappyElement() -map_element = mg.visualization.MapModule( - schelling_draw, [52, 12], 4, tiles=xyz.CartoDB.Positron -) +map_element = mg.visualization.MapModule(schelling_draw, tiles=xyz.CartoDB.Positron) happy_chart = mesa.visualization.ChartModule([{"Label": "happy", "Color": "Black"}]) server = mesa.visualization.ModularServer( GeoSchelling, [map_element, happy_element, happy_chart], "Schelling", model_params diff --git a/gis/geo_schelling_points/geo_schelling_points/model.py b/gis/geo_schelling_points/geo_schelling_points/model.py index b8544934..5031eb1e 100644 --- a/gis/geo_schelling_points/geo_schelling_points/model.py +++ b/gis/geo_schelling_points/geo_schelling_points/model.py @@ -1,5 +1,6 @@ import random import uuid +from pathlib import Path import mesa import mesa_geo as mg @@ -7,6 +8,8 @@ from .agents import PersonAgent, RegionAgent from .space import Nuts2Eu +script_directory = Path(__file__).resolve().parent + class GeoSchellingPoints(mesa.Model): def __init__(self, red_percentage=0.5, similarity_threshold=0.5): @@ -24,9 +27,8 @@ def __init__(self, red_percentage=0.5, similarity_threshold=0.5): # Set up the grid with patches for every NUTS region ac = mg.AgentCreator(RegionAgent, model=self) - regions = ac.from_file( - "data/nuts_rg_60M_2013_lvl_2.geojson", unique_id="NUTS_ID" - ) + data_path = script_directory / "../data/nuts_rg_60M_2013_lvl_2.geojson" + regions = ac.from_file(data_path, unique_id="NUTS_ID") self.space.add_regions(regions) for region in regions: diff --git a/gis/geo_sir/model.py b/gis/geo_sir/model.py index d9d49585..093cd236 100644 --- a/gis/geo_sir/model.py +++ b/gis/geo_sir/model.py @@ -1,14 +1,19 @@ +from pathlib import Path + import mesa import mesa_geo as mg -from agents import NeighbourhoodAgent, PersonAgent from shapely.geometry import Point +from .agents import NeighbourhoodAgent, PersonAgent + +script_directory = Path(__file__).resolve().parent + class GeoSir(mesa.Model): """Model class for a simplistic infection model.""" # Geographical parameters for desired map - geojson_regions = "data/TorontoNeighbourhoods.geojson" + geojson_regions = script_directory / "data/TorontoNeighbourhoods.geojson" unique_id = "HOODNUM" def __init__( @@ -23,6 +28,7 @@ def __init__( :param infection_risk: Probability of agent to become infected, if it has been exposed to another infected """ + super().__init__() self.schedule = mesa.time.BaseScheduler(self) self.space = mg.GeoSpace(warn_crs_conversion=False) self.steps = 0 diff --git a/gis/population/population/model.py b/gis/population/population/model.py index 6c1065ad..2bd77439 100644 --- a/gis/population/population/model.py +++ b/gis/population/population/model.py @@ -1,6 +1,7 @@ import math import random import uuid +from pathlib import Path import mesa import mesa_geo as mg @@ -9,6 +10,8 @@ from .space import UgandaArea +script_directory = Path(__file__).resolve().parent + class Person(mg.GeoAgent): MOBILITY_RANGE_X = 0.0 @@ -52,13 +55,18 @@ def step(self): class Population(mesa.Model): def __init__( self, - population_gzip_file="data/popu.asc.gz", - lake_zip_file="data/lake.zip", - world_zip_file="data/clip.zip", + population_gzip_file="../data/popu.asc.gz", + lake_zip_file="../data/lake.zip", + world_zip_file="../data/clip.zip", ): super().__init__() self.space = UgandaArea(crs="epsg:4326") - self.space.load_data(population_gzip_file, lake_zip_file, world_zip_file) + self.space.load_data( + script_directory / population_gzip_file, + script_directory / lake_zip_file, + script_directory / world_zip_file, + model=self, + ) pixel_size_x, pixel_size_y = self.space.population_layer.resolution Person.MOBILITY_RANGE_X = pixel_size_x / 2.0 Person.MOBILITY_RANGE_Y = pixel_size_y / 2.0 diff --git a/gis/population/population/space.py b/gis/population/population/space.py index 73bfe0ad..e2747e19 100644 --- a/gis/population/population/space.py +++ b/gis/population/population/space.py @@ -1,5 +1,6 @@ from __future__ import annotations +import gzip import uuid import geopandas as gpd @@ -14,10 +15,11 @@ class UgandaCell(Cell): def __init__( self, + model, pos: mesa.space.Coordinate | None = None, indices: mesa.space.Coordinate | None = None, ): - super().__init__(pos, indices) + super().__init__(model, pos, indices) self.population = None def step(self): @@ -32,18 +34,20 @@ class UgandaArea(GeoSpace): def __init__(self, crs): super().__init__(crs=crs) - def load_data(self, population_gzip_file, lake_zip_file, world_zip_file): + def load_data(self, population_gzip_file, lake_zip_file, world_zip_file, model): world_size = gpd.GeoDataFrame.from_file(world_zip_file) raster_layer = RasterLayer.from_file( - f"/vsigzip/{population_gzip_file}", + population_gzip_file, + model=model, cell_cls=UgandaCell, attr_name="population", + rio_opener=gzip.open, ) raster_layer.crs = world_size.crs raster_layer.total_bounds = world_size.total_bounds self.add_layer(raster_layer) self.lake = gpd.GeoDataFrame.from_file(lake_zip_file).geometry[0] - self.add_agents(GeoAgent(uuid.uuid4().int, None, self.lake, self.crs)) + self.add_agents(GeoAgent(uuid.uuid4().int, model, self.lake, self.crs)) @property def population_layer(self): diff --git a/gis/rainfall/rainfall/model.py b/gis/rainfall/rainfall/model.py index 380ff743..36d363ab 100644 --- a/gis/rainfall/rainfall/model.py +++ b/gis/rainfall/rainfall/model.py @@ -1,4 +1,5 @@ import uuid +from pathlib import Path import mesa import mesa_geo as mg @@ -7,6 +8,8 @@ from .space import CraterLake +script_directory = Path(__file__).resolve().parent + class RaindropAgent(mg.GeoAgent): def __init__(self, unique_id, model, pos): @@ -63,7 +66,7 @@ def __init__(self, rain_rate=500, water_height=5, export_data=False, num_steps=2 self.export_data = export_data self.num_steps = num_steps - self.space = CraterLake(crs="epsg:4326", water_height=water_height) + self.space = CraterLake(crs="epsg:4326", water_height=water_height, model=self) self.schedule = mesa.time.RandomActivation(self) self.datacollector = mesa.DataCollector( { @@ -73,7 +76,8 @@ def __init__(self, rain_rate=500, water_height=5, export_data=False, num_steps=2 } ) - self.space.set_elevation_layer("data/elevation.asc.gz", crs="epsg:4326") + data_path = script_directory / "../data/elevation.asc.gz" + self.space.set_elevation_layer(data_path, crs="epsg:4326") @property def contained(self): diff --git a/gis/rainfall/rainfall/space.py b/gis/rainfall/rainfall/space.py index 61c49d7c..3ae8ab83 100644 --- a/gis/rainfall/rainfall/space.py +++ b/gis/rainfall/rainfall/space.py @@ -1,5 +1,7 @@ from __future__ import annotations +import gzip + import mesa import mesa_geo as mg import numpy as np @@ -12,10 +14,11 @@ class LakeCell(mg.Cell): def __init__( self, + model, pos: mesa.space.Coordinate | None = None, indices: mesa.space.Coordinate | None = None, ): - super().__init__(pos, indices) + super().__init__(model, pos, indices) self.elevation = None self.water_level = None self.water_level_normalized = None @@ -25,14 +28,19 @@ def step(self): class CraterLake(mg.GeoSpace): - def __init__(self, crs, water_height): + def __init__(self, crs, water_height, model): super().__init__(crs=crs) + self.model = model self.water_height = water_height self.outflow = 0 def set_elevation_layer(self, elevation_gzip_file, crs): raster_layer = mg.RasterLayer.from_file( - f"/vsigzip/{elevation_gzip_file}", cell_cls=LakeCell, attr_name="elevation" + elevation_gzip_file, + model=self.model, + cell_cls=LakeCell, + attr_name="elevation", + rio_opener=gzip.open, ) raster_layer.crs = crs raster_layer.apply_raster( diff --git a/gis/urban_growth/urban_growth/model.py b/gis/urban_growth/urban_growth/model.py index b3f2b3b3..98a530b4 100644 --- a/gis/urban_growth/urban_growth/model.py +++ b/gis/urban_growth/urban_growth/model.py @@ -1,8 +1,12 @@ +from pathlib import Path + import mesa import numpy as np from .space import City +script_directory = Path(__file__).resolve().parent + class UrbanGrowth(mesa.Model): def __init__( @@ -64,14 +68,14 @@ def _load_data(self) -> None: width=self.world_width, height=self.world_height, crs="epsg:3857", + model=self, total_bounds=[-901575.0, 1442925.0, -885645.0, 1454745.0], ) + + labels = ["urban", "slope", "road1", "excluded", "landuse"] + self.space.load_datasets( - urban_data="data/urban_santafe.asc.gz", - slope_data="data/slope_santafe.asc.gz", - road_data="data/road1_santafe.asc.gz", - excluded_data="data/excluded_santafe.asc.gz", - land_use_data="data/landuse_santafe.asc.gz", + *(script_directory / f"../data/{label}_santafe.asc.gz" for label in labels) ) def _check_suitability(self) -> None: diff --git a/gis/urban_growth/urban_growth/space.py b/gis/urban_growth/urban_growth/space.py index 207068c1..7e132d79 100644 --- a/gis/urban_growth/urban_growth/space.py +++ b/gis/urban_growth/urban_growth/space.py @@ -1,5 +1,6 @@ from __future__ import annotations +import gzip import random import mesa @@ -26,10 +27,11 @@ class UrbanCell(mg.Cell): def __init__( self, + model: mesa.Model | None = None, pos: mesa.space.Coordinate | None = None, indices: mesa.space.Coordinate | None = None, ): - super().__init__(pos, indices) + super().__init__(model, pos, indices) self.urban = None self.slope = None self.road_1 = None @@ -78,10 +80,10 @@ def _edge_growth(self) -> None: class City(mg.GeoSpace): - def __init__(self, width, height, crs, total_bounds): + def __init__(self, width, height, crs, total_bounds, model): super().__init__(crs=crs) self.add_layer( - mg.RasterLayer(width, height, crs, total_bounds, cell_cls=UrbanCell) + mg.RasterLayer(width, height, crs, total_bounds, model, cell_cls=UrbanCell) ) def load_datasets( @@ -95,7 +97,7 @@ def load_datasets( "land_use": land_use_data, } for attribute_name, data_file in data.items(): - with rio.open(f"/vsigzip/{data_file}", "r") as dataset: + with rio.open(data_file, "r", opener=gzip.open) as dataset: values = dataset.read() self.raster_layer.apply_raster(values, attr_name=attribute_name) diff --git a/pyproject.toml b/pyproject.toml index d82e0f3b..574f2f5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,22 @@ license = {file = "LICENSE"} readme = "README.rst" requires-python = ">=3.8" +[project.optional-dependencies] +test = [ + "pytest", + "scipy", +] +test_gis = [ + "pytest", + "momepy", +] +rl_example = [ + "stable-baselines3", + "seaborn", + "mesa", + "tensorboard" +] + [build-system] requires = [ "setuptools", diff --git a/rl/.gitignore b/rl/.gitignore new file mode 100644 index 00000000..ba0430d2 --- /dev/null +++ b/rl/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/rl/README.md b/rl/README.md new file mode 100644 index 00000000..edb45617 --- /dev/null +++ b/rl/README.md @@ -0,0 +1,66 @@ +# Reinforcement Learning Implementations with Mesa + +This repository demonstrates various applications of reinforcement learning (RL) using the Mesa agent-based modeling framework. + +
+
+
+
+