├── .coderabbit.yaml ├── .codespellignore ├── .coveragerc ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ └── feature-request.md ├── PULL_REQUEST_TEMPLATE │ ├── bug.md │ └── feature.md ├── dependabot.yml ├── pull_request_template.md ├── release.yml └── workflows │ ├── benchmarks.yml │ ├── build_lint.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CITATION.cff ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── HISTORY.md ├── LICENSE ├── NOTICE ├── README.md ├── benchmarks ├── README.md ├── compare_timings.py ├── configurations.py └── global_benchmark.py ├── binder └── environment.yml ├── codecov.yaml ├── docker-compose.yml ├── docs ├── Makefile ├── README.md ├── apis │ ├── agent.md │ ├── api_main.md │ ├── batchrunner.md │ ├── datacollection.md │ ├── discrete_space.md │ ├── experimental.md │ ├── mesa_logging.md │ ├── model.md │ ├── space.md │ └── visualization.md ├── best-practices.md ├── conf.py ├── example_template.txt ├── examples_overview_template.txt ├── getting_started.md ├── images │ ├── Discrete_Space.drawio.png │ ├── mesa_logo.ico │ ├── mesa_logo.png │ └── wolf_sheep.png ├── index.md ├── make.bat ├── mesa.md ├── mesa_extension.md ├── migration_guide.md ├── overview.md └── tutorials │ ├── 0_first_model.ipynb │ ├── 1_adding_space.ipynb │ ├── 2_collecting_data.ipynb │ ├── 3_agentset.ipynb │ ├── 4_visualization_basic.ipynb │ ├── 5_visualization_dynamic_agents.ipynb │ ├── 6_visualization_custom.ipynb │ ├── 7_batch_run.ipynb │ └── 8_comparing_scenarios.ipynb ├── flake.lock ├── flake.nix ├── maintenance └── fetch_unlabeled_prs.py ├── mesa ├── __init__.py ├── agent.py ├── batchrunner.py ├── datacollection.py ├── discrete_space │ ├── __init__.py │ ├── cell.py │ ├── cell_agent.py │ ├── cell_collection.py │ ├── discrete_space.py │ ├── grid.py │ ├── network.py │ ├── property_layer.py │ └── voronoi.py ├── examples │ ├── README.md │ ├── __init__.py │ ├── advanced │ │ ├── __init__.py │ │ ├── alliance_formation │ │ │ ├── Readme.md │ │ │ ├── __init__ .py │ │ │ ├── agents.py │ │ │ ├── app.py │ │ │ └── model.py │ │ ├── epstein_civil_violence │ │ │ ├── Epstein Civil Violence.ipynb │ │ │ ├── Readme.md │ │ │ ├── __init__.py │ │ │ ├── agents.py │ │ │ ├── app.py │ │ │ └── model.py │ │ ├── pd_grid │ │ │ ├── Readme.md │ │ │ ├── __init__.py │ │ │ ├── agents.py │ │ │ ├── analysis.ipynb │ │ │ ├── app.py │ │ │ └── model.py │ │ ├── sugarscape_g1mt │ │ │ ├── Readme.md │ │ │ ├── __init__.py │ │ │ ├── agents.py │ │ │ ├── app.py │ │ │ ├── model.py │ │ │ └── sugar-map.txt │ │ └── wolf_sheep │ │ │ ├── Readme.md │ │ │ ├── __init__.py │ │ │ ├── agents.py │ │ │ ├── app.py │ │ │ └── model.py │ └── basic │ │ ├── __init__.py │ │ ├── boid_flockers │ │ ├── Readme.md │ │ ├── __init__.py │ │ ├── agents.py │ │ ├── app.py │ │ └── model.py │ │ ├── boltzmann_wealth_model │ │ ├── Readme.md │ │ ├── __init__.py │ │ ├── agents.py │ │ ├── app.py │ │ ├── model.py │ │ └── st_app.py │ │ ├── conways_game_of_life │ │ ├── Readme.md │ │ ├── __init__.py │ │ ├── agents.py │ │ ├── app.py │ │ ├── model.py │ │ └── st_app.py │ │ ├── schelling │ │ ├── Readme.md │ │ ├── __init__.py │ │ ├── agents.py │ │ ├── analysis.ipynb │ │ ├── app.py │ │ ├── model.py │ │ └── resources │ │ │ ├── blue_happy.png │ │ │ ├── blue_unhappy.png │ │ │ ├── orange_happy.png │ │ │ └── orange_unhappy.png │ │ └── virus_on_network │ │ ├── Readme.md │ │ ├── __init__.py │ │ ├── agents.py │ │ ├── app.py │ │ └── model.py ├── experimental │ ├── __init__.py │ ├── cell_space │ │ └── __init__.py │ ├── continuous_space │ │ ├── __init__.py │ │ ├── continuous_space.py │ │ └── continuous_space_agents.py │ ├── devs │ │ ├── __init__.py │ │ ├── eventlist.py │ │ └── simulator.py │ ├── mesa_signals │ │ ├── __init__.py │ │ ├── mesa_signal.py │ │ ├── observable_collections.py │ │ └── signals_util.py │ └── meta_agents │ │ ├── __init__.py │ │ └── meta_agent.py ├── mesa_logging.py ├── model.py ├── space.py └── visualization │ ├── __init__.py │ ├── command_console.py │ ├── components │ ├── __init__.py │ ├── altair_components.py │ ├── matplotlib_components.py │ └── portrayal_components.py │ ├── mpl_space_drawing.py │ ├── solara_viz.py │ ├── user_param.py │ └── utils.py ├── mypy.ini ├── pyproject.toml ├── tests ├── __init__.py ├── read_requirements.py ├── test_agent.py ├── test_batch_run.py ├── test_components_matplotlib.py ├── test_continuous_space.py ├── test_datacollector.py ├── test_devs.py ├── test_discrete_space.py ├── test_end_to_end_viz.sh ├── test_examples.py ├── test_examples_viz.py ├── test_grid.py ├── test_import_namespace.py ├── test_lifespan.py ├── test_mesa_logging.py ├── test_mesa_signals.py ├── test_meta_agents.py ├── test_model.py ├── test_portrayal_components.py ├── test_solara_viz.py └── test_space.py └── uv.lock /.coderabbit.yaml: -------------------------------------------------------------------------------- 1 | language: en-US 2 | tone_instructions: '' 3 | early_access: false 4 | enable_free_tier: true 5 | reviews: 6 | profile: chill 7 | request_changes_workflow: false 8 | high_level_summary: true 9 | high_level_summary_placeholder: '@coderabbitai summary' 10 | high_level_summary_in_walkthrough: false 11 | auto_title_placeholder: '@coderabbitai' 12 | auto_title_instructions: '' 13 | review_status: false 14 | commit_status: true 15 | fail_commit_status: false 16 | collapse_walkthrough: false 17 | changed_files_summary: true 18 | sequence_diagrams: true 19 | assess_linked_issues: true 20 | related_issues: true 21 | related_prs: true 22 | suggested_labels: true 23 | auto_apply_labels: false 24 | suggested_reviewers: true 25 | auto_assign_reviewers: false 26 | poem: true 27 | labeling_instructions: [] 28 | path_filters: [] 29 | path_instructions: [] 30 | abort_on_close: true 31 | disable_cache: false 32 | auto_review: 33 | enabled: false 34 | auto_incremental_review: false 35 | ignore_title_keywords: [] 36 | labels: [] 37 | drafts: false 38 | base_branches: [] 39 | finishing_touches: 40 | docstrings: 41 | enabled: true 42 | tools: 43 | ast-grep: 44 | rule_dirs: [] 45 | util_dirs: [] 46 | essential_rules: true 47 | packages: [] 48 | shellcheck: 49 | enabled: true 50 | ruff: 51 | enabled: true 52 | markdownlint: 53 | enabled: true 54 | github-checks: 55 | enabled: true 56 | timeout_ms: 180000 57 | languagetool: 58 | enabled: true 59 | enabled_rules: [] 60 | disabled_rules: [] 61 | enabled_categories: [] 62 | disabled_categories: [] 63 | enabled_only: false 64 | level: default 65 | biome: 66 | enabled: true 67 | hadolint: 68 | enabled: true 69 | swiftlint: 70 | enabled: true 71 | phpstan: 72 | enabled: true 73 | level: default 74 | golangci-lint: 75 | enabled: true 76 | yamllint: 77 | enabled: true 78 | gitleaks: 79 | enabled: true 80 | checkov: 81 | enabled: true 82 | detekt: 83 | enabled: true 84 | eslint: 85 | enabled: true 86 | rubocop: 87 | enabled: true 88 | buf: 89 | enabled: true 90 | regal: 91 | enabled: true 92 | actionlint: 93 | enabled: true 94 | pmd: 95 | enabled: true 96 | cppcheck: 97 | enabled: true 98 | semgrep: 99 | enabled: true 100 | circleci: 101 | enabled: true 102 | sqlfluff: 103 | enabled: true 104 | prismaLint: 105 | enabled: true 106 | oxc: 107 | enabled: true 108 | shopifyThemeCheck: 109 | enabled: true 110 | chat: 111 | auto_reply: true 112 | create_issues: true 113 | integrations: 114 | jira: 115 | usage: auto 116 | linear: 117 | usage: auto 118 | knowledge_base: 119 | opt_out: false 120 | web_search: 121 | enabled: true 122 | learnings: 123 | scope: auto 124 | issues: 125 | scope: auto 126 | jira: 127 | usage: auto 128 | project_keys: [] 129 | linear: 130 | usage: auto 131 | team_keys: [] 132 | pull_requests: 133 | scope: auto 134 | code_generation: 135 | docstrings: 136 | language: en-US 137 | path_instructions: [] 138 | -------------------------------------------------------------------------------- /.codespellignore: -------------------------------------------------------------------------------- 1 | hist 2 | hart 3 | mutch 4 | ist 5 | inactivate 6 | ue 7 | fpr 8 | falsy 9 | assertIn 10 | nD 11 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = mesa 3 | branch = True 4 | 5 | [report] 6 | omit = 7 | tests/* 8 | mesa/flat/* 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Let us know if something is broken on Mesa 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | 9 | 10 | **Expected behavior** 11 | 12 | 13 | **To Reproduce** 14 | 16 | 17 | **Additional context** 18 | 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Questions, ideas, showcases and more 4 | url: https://github.com/projectmesa/mesa/discussions 5 | about: Discuss Mesa, ask questions, share ideas, and showcase your projects 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature for Mesa 4 | 5 | --- 6 | 7 | **What's the problem this feature will solve?** 8 | 9 | 10 | **Describe the solution you'd like** 11 | 12 | 13 | **Additional context** 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | 4 | ### Bug / Issue 5 | 6 | 7 | ### Implementation 8 | 9 | 10 | ### Testing 11 | 14 | 15 | ### Additional Notes 16 | 3 | 4 | ### Motive 5 | 6 | 7 | ### Implementation 8 | 9 | 10 | ### Usage Examples 11 | 14 | 15 | ### Additional Notes 16 | 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every week 8 | interval: "weekly" 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Thanks for opening a PR! Please click the `Preview` tab and select a PR template: 2 | 3 | - [🐛 Bug fix](?expand=1&template=bug.md) 4 | - [🛠 Feature/enhancement](?expand=1&template=feature.md) 5 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # Docs: https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes 2 | changelog: 3 | exclude: 4 | labels: 5 | - ignore-for-release 6 | categories: 7 | - title: ⚠️ Breaking changes 8 | labels: 9 | - breaking 10 | - title: 🧪 Experimental features 11 | labels: 12 | - experimental 13 | - title: 🎉 New features added 14 | labels: 15 | - feature 16 | - title: 🛠 Enhancements made 17 | labels: 18 | - enhancement 19 | - title: 🐛 Bugs fixed 20 | labels: 21 | - bug 22 | - title: 🔍 Examples updated 23 | labels: 24 | - example 25 | - title: 📜 Documentation improvements 26 | labels: 27 | - docs 28 | - title: 🔧 Maintenance 29 | labels: 30 | - ci 31 | - testing 32 | - dependency 33 | - maintenance 34 | - packaging 35 | - title: Other changes 36 | labels: 37 | - "*" 38 | -------------------------------------------------------------------------------- /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: Performance benchmarks 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, ready_for_review, labeled] 6 | branches: 7 | - main 8 | paths: 9 | - '**.py' 10 | - '.github/workflows/benchmarks.yml' 11 | 12 | permissions: 13 | issues: write 14 | pull-requests: write 15 | 16 | jobs: 17 | run-benchmarks: 18 | if: > 19 | github.event.action == 'labeled' && contains(github.event.pull_request.labels.*.name, 'trigger-benchmarks') || 20 | github.event.action == 'opened' || 21 | github.event.action == 'ready_for_review' 22 | runs-on: ubuntu-latest 23 | steps: 24 | # Python and dependency setup 25 | - name: Set up Python 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: '3.13' 29 | - name: Add project directory to PYTHONPATH 30 | run: echo "PYTHONPATH=$PYTHONPATH:$(pwd)" >> $GITHUB_ENV 31 | - name: Install uv 32 | run: pip install uv 33 | - name: Install dependencies 34 | run: uv pip install --system numpy pandas tqdm tabulate matplotlib solara networkx scipy 35 | # Benchmarks on the projectmesa main branch 36 | - name: Checkout main branch 37 | uses: actions/checkout@v4 38 | with: 39 | ref: main 40 | repository: projectmesa/mesa 41 | - name: Install Mesa 42 | run: pip install --no-deps . 43 | - name: Run benchmarks on main branch 44 | working-directory: benchmarks 45 | run: python global_benchmark.py 46 | # Upload benchmarks, checkout PR branch, download benchmarks 47 | - name: Upload benchmark results 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: timings-main 51 | path: benchmarks/timings_1.pickle 52 | - name: Checkout PR branch 53 | uses: actions/checkout@v4 54 | if: github.event_name == 'pull_request_target' 55 | with: 56 | ref: ${{ github.event.pull_request.head.ref }} 57 | repository: ${{ github.event.pull_request.head.repo.full_name }} 58 | fetch-depth: 0 59 | persist-credentials: false 60 | clean: false 61 | - name: Install Mesa of the PR branch 62 | run: pip install --no-deps . 63 | - name: Download benchmark results 64 | uses: actions/download-artifact@v4 65 | with: 66 | name: timings-main 67 | path: benchmarks 68 | # Run benchmarks on the PR branch 69 | - name: Run benchmarks on PR branch 70 | working-directory: benchmarks 71 | run: python global_benchmark.py 72 | # Run compare script and create comment 73 | - name: Run compare timings and encode output 74 | working-directory: benchmarks 75 | run: | 76 | TIMING_COMPARISON=$(python compare_timings.py | base64 -w 0) # Base64 encode the output 77 | echo "TIMING_COMPARISON=$TIMING_COMPARISON" >> $GITHUB_ENV 78 | - name: Comment PR 79 | uses: actions/github-script@v7 80 | with: 81 | script: | 82 | const output = Buffer.from(process.env.TIMING_COMPARISON, 'base64').toString('utf-8'); 83 | const issue_number = context.issue.number; 84 | github.rest.issues.createComment({ 85 | issue_number: issue_number, 86 | owner: context.repo.owner, 87 | repo: context.repo.repo, 88 | body: 'Performance benchmarks:\n\n' + output 89 | }); 90 | -------------------------------------------------------------------------------- /.github/workflows/build_lint.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release** 8 | paths-ignore: 9 | - "**.md" 10 | pull_request: 11 | paths-ignore: 12 | - "**.md" 13 | workflow_dispatch: 14 | schedule: 15 | - cron: "0 6 * * 1" 16 | 17 | # This will cancel previous run if a newer job that obsoletes the said previous 18 | # run, is started. 19 | # Based on https://github.com/zulip/zulip/commit/4a11642cee3c8aec976d305d51a86e60e5d70522 20 | concurrency: 21 | group: "${{ github.workflow }}-${{ github.head_ref || github.run_id }}" 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | build: 26 | runs-on: ${{ matrix.os }}-latest 27 | # We need an explicit timeout because sometimes the batch_runner test never 28 | # completes. 29 | timeout-minutes: 6 30 | strategy: 31 | fail-fast: False 32 | matrix: 33 | os: [windows, ubuntu, macos] 34 | python-version: ["3.13"] 35 | include: 36 | - os: ubuntu 37 | python-version: "3.12" 38 | - os: ubuntu 39 | python-version: "3.11" 40 | # Disabled for now. See https://github.com/projectmesa/mesa/issues/1253 41 | #- os: ubuntu 42 | # python-version: 'pypy-3.8' 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Set up Python ${{ matrix.python-version }} 47 | uses: actions/setup-python@v5 48 | with: 49 | python-version: ${{ matrix.python-version }} 50 | allow-prereleases: true 51 | cache: "pip" 52 | - name: Install uv 53 | run: pip install uv 54 | - name: Install Mesa and dependencies 55 | run: uv pip install --system .[dev] 56 | - name: Setup Playwright 57 | run: playwright install 58 | - name: Test with pytest 59 | run: pytest --durations=10 --cov=mesa tests/ --cov-report=xml 60 | - if: matrix.os == 'ubuntu' 61 | name: Codecov 62 | uses: codecov/codecov-action@v5 63 | with: 64 | fail_ci_if_error: true 65 | token: ${{ secrets.CODECOV_TOKEN }} 66 | 67 | examples: 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v4 71 | - name: Set up Python 72 | uses: actions/setup-python@v5 73 | with: 74 | python-version: "3.13" 75 | allow-prereleases: true 76 | cache: "pip" 77 | - name: Install uv 78 | run: pip install uv 79 | - name: Install Mesa 80 | run: uv pip install --system .[examples] 81 | - name: Checkout mesa-examples 82 | uses: actions/checkout@v4 83 | with: 84 | repository: projectmesa/mesa-examples 85 | path: mesa-examples 86 | - name: Test examples 87 | run: | 88 | cd mesa-examples 89 | pytest -rA -Werror -Wdefault::FutureWarning -Wi::DeprecationWarning test_examples.py 90 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | branches: 7 | - main 8 | - release** 9 | paths-ignore: 10 | - '**.md' 11 | - '**.rst' 12 | pull_request: 13 | paths-ignore: 14 | - '**.md' 15 | - '**.rst' 16 | workflow_dispatch: 17 | schedule: 18 | - cron: '0 6 * * 1' 19 | 20 | permissions: 21 | id-token: write 22 | 23 | jobs: 24 | release: 25 | name: Deploy release to PyPI 26 | runs-on: ubuntu-latest 27 | permissions: 28 | id-token: write 29 | steps: 30 | - name: Checkout source 31 | uses: actions/checkout@v4 32 | - name: Set up Python 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: "3.13" 36 | allow-prereleases: true 37 | cache: 'pip' 38 | - name: Install dependencies 39 | run: pip install -U pip hatch 40 | - name: Build distributions 41 | run: hatch build 42 | - name: Upload package as artifact to GitHub 43 | if: github.repository == 'projectmesa/mesa' && startsWith(github.ref, 'refs/tags') 44 | uses: actions/upload-artifact@v4 45 | with: 46 | name: package 47 | path: dist/ 48 | - name: Publish package to PyPI 49 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 50 | uses: pypa/gh-action-pypi-publish@release/v1 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Benchmarking 2 | benchmarks/**/*.pickle 3 | 4 | # exampledocs 5 | docs/examples/* 6 | docs/example.md 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | 12 | *py~ 13 | *swp 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | env/ 21 | venv/ 22 | pythonenv*/ 23 | .venv/ 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .coverage 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter and iPython notebook checkpoints 71 | *.ipynb_checkpoints 72 | *.virtual_documents 73 | 74 | # Spyder app workspace config file 75 | .spyderworkspace 76 | 77 | # PyCharm environment files 78 | .idea/ 79 | *.pclprof 80 | 81 | # VSCode environment files 82 | .vscode/ 83 | *.code-workspace 84 | 85 | # Apple OSX 86 | *.DS_Store 87 | 88 | # mypy 89 | .mypy_cache/ 90 | .dmypy.json 91 | dmypy.json 92 | 93 | # JS dependencies 94 | mesa/visualization/templates/external/ 95 | mesa/visualization/templates/js/external/ 96 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autoupdate_schedule: 'monthly' 3 | 4 | repos: 5 | - repo: https://github.com/astral-sh/ruff-pre-commit 6 | # Ruff version. 7 | rev: v0.11.12 8 | hooks: 9 | # Run the linter. 10 | - id: ruff 11 | types_or: [ python, pyi, jupyter ] 12 | args: [ --fix ] 13 | # Run the formatter. 14 | - id: ruff-format 15 | types_or: [ python, pyi, jupyter ] 16 | - repo: https://github.com/asottile/pyupgrade 17 | rev: v3.20.0 18 | hooks: 19 | - id: pyupgrade 20 | args: [--py311-plus] 21 | - repo: https://github.com/pre-commit/pre-commit-hooks 22 | rev: v5.0.0 # Use the ref you want to point at 23 | hooks: 24 | - id: trailing-whitespace 25 | - id: check-toml 26 | - id: check-yaml 27 | - repo: https://github.com/codespell-project/codespell 28 | rev: v2.4.1 29 | hooks: 30 | - id: codespell 31 | args: [ 32 | "--ignore-words", 33 | ".codespellignore", 34 | ] 35 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Build documentation in the docs/ directory with Sphinx 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | # Optionally build your docs in additional formats such as PDF 12 | formats: 13 | - pdf 14 | 15 | build: 16 | os: ubuntu-lts-latest 17 | tools: 18 | python: latest 19 | 20 | # Optionally set the version of Python and requirements required to build your docs 21 | python: 22 | install: 23 | - method: pip 24 | path: . 25 | extra_requirements: 26 | - docs 27 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: "1.2.0" 2 | authors: 3 | - family-names: Hoeven 4 | given-names: Ewout 5 | name-particle: ter 6 | orcid: "https://orcid.org/0009-0002-0805-3425" 7 | - family-names: Kwakkel 8 | given-names: Jan 9 | orcid: "https://orcid.org/0000-0001-9447-2954" 10 | - family-names: Hess 11 | given-names: Vincent 12 | orcid: "https://orcid.org/0000-0002-9242-8500" 13 | - family-names: Pike 14 | given-names: Thomas 15 | orcid: "https://orcid.org/0000-0003-1576-0283" 16 | - family-names: Wang 17 | given-names: Boyu 18 | orcid: "https://orcid.org/0000-0001-9879-2138" 19 | - family-names: rht 20 | orcid: "https://orcid.org/0009-0002-6902-111X" 21 | - family-names: Kazil 22 | given-names: Jackie 23 | orcid: "https://orcid.org/0000-0002-8300-7384" 24 | doi: 10.5281/zenodo.15090710 25 | message: If you use this software, please cite our article in the 26 | Journal of Open Source Software. 27 | preferred-citation: 28 | authors: 29 | - family-names: Hoeven 30 | given-names: Ewout 31 | name-particle: ter 32 | orcid: "https://orcid.org/0009-0002-0805-3425" 33 | - family-names: Kwakkel 34 | given-names: Jan 35 | orcid: "https://orcid.org/0000-0001-9447-2954" 36 | - family-names: Hess 37 | given-names: Vincent 38 | orcid: "https://orcid.org/0000-0002-9242-8500" 39 | - family-names: Pike 40 | given-names: Thomas 41 | orcid: "https://orcid.org/0000-0003-1576-0283" 42 | - family-names: Wang 43 | given-names: Boyu 44 | orcid: "https://orcid.org/0000-0001-9879-2138" 45 | - family-names: rht 46 | orcid: "https://orcid.org/0009-0002-6902-111X" 47 | - family-names: Kazil 48 | given-names: Jackie 49 | orcid: "https://orcid.org/0000-0002-8300-7384" 50 | date-published: 2025-03-28 51 | doi: 10.21105/joss.07668 52 | issn: 2475-9066 53 | issue: 107 54 | journal: Journal of Open Source Software 55 | publisher: 56 | name: Open Journals 57 | start: 7668 58 | title: "Mesa 3: Agent-based modeling with Python in 2025" 59 | type: article 60 | url: "https://joss.theoj.org/papers/10.21105/joss.07668" 61 | volume: 10 62 | title: "Mesa 3: Agent-based modeling with Python in 2025" 63 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # We can't use slim because we need either git/wget/curl to 2 | # download mesa-examples, and so installing them requires 3 | # updating the system anyway. 4 | FROM python:bookworm 5 | LABEL maintainer="projectmesa maintainers@projectmesa.dev" 6 | 7 | # To use this Dockerfile: 8 | # 1. `docker build . -t mesa_image` 9 | # 2. `docker run --name mesa_instance -p 8765:8765 -it mesa_image` 10 | # 3. In your browser, visit http://127.0.0.1:8765 11 | # 12 | # Currently, this Dockerfile defaults to running the Schelling model, as an 13 | # illustration. If you want to run a different example, simply change the 14 | # MODEL_DIR variable below to point to another model, e.g. 15 | # /mesa-examples/examples/sugarscape_cg or path to your custom model. 16 | # You specify the MODEL_DIR (relative to this Git repo) by doing: 17 | # `docker run --name mymesa_instance -p 8765:8765 -e MODEL_DIR=/mesa-examples/examples/sugarscape_cg -it mymesa_image` 18 | # Note: the model directory MUST contain an app.py file. 19 | 20 | ENV MODEL_DIR=/opt/mesa/mesa/examples/basic/schelling 21 | 22 | # Don't buffer output: 23 | # https://docs.python.org/3.10/using/cmdline.html?highlight=pythonunbuffered#envvar-PYTHONUNBUFFERED 24 | ENV PYTHONUNBUFFERED=1 25 | 26 | WORKDIR /opt/mesa 27 | 28 | COPY . /opt/mesa 29 | 30 | EXPOSE 8765/tcp 31 | 32 | RUN pip3 install -e /opt/mesa[rec] 33 | 34 | CMD ["sh", "-c", "cd $MODEL_DIR && solara run app.py --host=0.0.0.0"] 35 | 36 | # To check file system: 37 | # docker exec -it mesa_instance bash 38 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2014-2024 Core Mesa Team and contributors 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Mesa Performance Benchmarks 2 | 3 | The `/benchmarks` directory contains tools for benchmarking the Mesa library performance on the included example models. This allows to track and compare model initialisation time and runtime between different Mesa versions. 4 | 5 | MESA uses several example base models for benchmarking performance (BoltzmannWealth, Schelling, BoidFlockers, and WolfSheep) by calculating the initialization time and run time for each of these models. These example models can be found in the `/mesa/examples` directory. 6 | 7 | ## Available Files 8 | 9 | - `configurations.py`: Contains model configurations for benchmarking 10 | - `global_benchmark.py`: Main script for running benchmarks 11 | - `compare_timings.py`: Tool to compare results between benchmark runs 12 | 13 | ## How to Use 14 | 15 | 16 | ### 1. Benchmark Configuration 17 | 18 | The `configurations.py` file defines which models to benchmark and their parameters. Each model has: 19 | 20 | - `small` and `large` configurations for testing different scales 21 | - Parameters for: 22 | - `seeds`: Number of different random seeds to use 23 | - `replications`: Number of times to repeat the benchmark for each seed 24 | - `steps`: Number of model steps to run 25 | - `parameters`: Model-specific parameters 26 | 27 | ### 2. Running Benchmarks 28 | 29 | To run the benchmarks: 30 | 31 | ```bash 32 | pip install tabulate # if you don't have it yet 33 | python global_benchmark.py 34 | ``` 35 | 36 | This will: 37 | - Run all models defined in `configurations.py` with their respective configurations 38 | - Measure initialization time and run time for each model 39 | - Save results to a pickle file named `timings_X.pickle` (where X is an incremental number) 40 | - Display a summary of results in the terminal 41 | 42 | 43 | >**Noteworthy :** the pickle file created by the benchmark is not under git control. So you can run the benchmark on the master branch first, switch to your development branch, and run the benchmarks again. 44 | 45 | #### What's being measured: 46 | 47 | - **Initialization time**: How long it takes to create model instances 48 | - **Run time**: How long it takes to run the model for the specified number of steps 49 | 50 | ### 3. Comparing Results 51 | 52 | After running benchmarks at different times (e.g., before and after code changes), you can compare the results: 53 | 54 | 1. Rename your benchmark files to distinguish them (e.g., `timings_before.pickle` and `timings_after.pickle`) 55 | 2. Edit `compare_timings.py` to use your file names: 56 | ```python 57 | filename1 = "timings_1" # Or the name of your 1st timings file 58 | filename2 = "timings_2" # Or the name of your 2nd timings file 59 | ``` 60 | 3. Run the comparison: 61 | ```bash 62 | python compare_timings.py 63 | ``` 64 | 65 | The output will show: 66 | - Percentage changes in initialization and run times 67 | - 95% confidence intervals for the changes 68 | - Emojis indicate performance changes: 69 | - 🟢 Faster performance (>3% improvement) 70 | - 🔴 Slower performance (>3% regression) 71 | - 🔵 Insignificant change (within ±3%) 72 | 73 | > Some care is required in the interpretation since it only shows percentage changes and not the absolute changes. The init times in general are tiny so slower performance here is not necissarily as much of an issue. 74 | 75 | 76 | ## Example Workflow 77 | 78 | 1. Run benchmarks on your current Mesa version: 79 | ```bash 80 | python global_benchmark.py 81 | # Results saved as timings_1.pickle 82 | ``` 83 | 84 | 2. Make code changes (add a feature in mesa, optimize a method, etc.) **OR** switch to development branch - you can do that without duplicating your pickle file in the new branch as it is not under git control 85 | 86 | 87 | 3. Run benchmarks again: 88 | ```bash 89 | python global_benchmark.py 90 | # Results saved as timings_2.pickle 91 | ``` 92 | 93 | 4. Compare results: 94 | ```bash 95 | python compare_timings.py 96 | # Shows performance change table 97 | ``` 98 | 99 | 5. Use the results to validate your optimizations or identify regressions 100 | -------------------------------------------------------------------------------- /benchmarks/compare_timings.py: -------------------------------------------------------------------------------- 1 | """compare timings across 2 benchmarks.""" 2 | 3 | import pickle 4 | 5 | import numpy as np 6 | import pandas as pd 7 | 8 | filename1 = "timings_1" 9 | filename2 = "timings_2" 10 | 11 | with open(f"{filename1}.pickle", "rb") as handle: 12 | timings_1 = pickle.load(handle) # noqa: S301 13 | 14 | with open(f"{filename2}.pickle", "rb") as handle: 15 | timings_2 = pickle.load(handle) # noqa: S301 16 | 17 | 18 | def bootstrap_percentage_change_confidence_interval(data1, data2, n=1000): 19 | """Calculate the percentage change and perform bootstrap to estimate the confidence interval. 20 | 21 | Args: 22 | data1: benchmark dataset 1 23 | data2: benchmark dataset 2 24 | n: bootstrap sample size 25 | 26 | Returns: 27 | float, mean, and lower and upper bound of confidence interval. 28 | """ 29 | change_samples = [] 30 | for _ in range(n): 31 | sampled_indices = np.random.choice( 32 | range(len(data1)), size=len(data1), replace=True 33 | ) 34 | sampled_data1 = np.array(data1)[sampled_indices] 35 | sampled_data2 = np.array(data2)[sampled_indices] 36 | change = 100 * (sampled_data2 - sampled_data1) / sampled_data1 37 | change_samples.append(np.mean(change)) 38 | lower, upper = np.percentile(change_samples, [2.5, 97.5]) 39 | return np.mean(change_samples), lower, upper 40 | 41 | 42 | # DataFrame to store the results 43 | results_df = pd.DataFrame() 44 | 45 | 46 | def performance_emoji(lower, upper): 47 | """Function to determine the emoji based on change and confidence interval.""" 48 | if upper < -3: 49 | return "🟢" # Emoji for faster performance 50 | elif lower > 3: 51 | return "🔴" # Emoji for slower performance 52 | else: 53 | return "🔵" # Emoji for insignificant change 54 | 55 | 56 | # Iterate over the models and sizes, perform analysis, and populate the DataFrame 57 | for model, size in timings_1: 58 | model_name = model.__name__ 59 | 60 | # Calculate percentage change and confidence interval for init times 61 | ( 62 | init_change, 63 | init_lower, 64 | init_upper, 65 | ) = bootstrap_percentage_change_confidence_interval( 66 | timings_1[(model, size)][0], timings_2[(model, size)][0] 67 | ) 68 | init_emoji = performance_emoji(init_lower, init_upper) 69 | init_summary = ( 70 | f"{init_emoji} {init_change:+.1f}% [{init_lower:+.1f}%, {init_upper:+.1f}%]" 71 | ) 72 | 73 | # Calculate percentage change and confidence interval for run times 74 | run_change, run_lower, run_upper = bootstrap_percentage_change_confidence_interval( 75 | timings_1[(model, size)][1], timings_2[(model, size)][1] 76 | ) 77 | run_emoji = performance_emoji(run_lower, run_upper) 78 | run_summary = ( 79 | f"{run_emoji} {run_change:+.1f}% [{run_lower:+.1f}%, {run_upper:+.1f}%]" 80 | ) 81 | 82 | # Append results to DataFrame 83 | row = pd.DataFrame( 84 | { 85 | "Model": [model_name], 86 | "Size": [size], 87 | "Init time [95% CI]": [init_summary], 88 | "Run time [95% CI]": [run_summary], 89 | } 90 | ) 91 | 92 | results_df = pd.concat([results_df, row], ignore_index=True) 93 | 94 | # Convert DataFrame to markdown with specified alignments 95 | markdown_representation = results_df.to_markdown(index=False, tablefmt="github") 96 | 97 | # Display the markdown representation 98 | print(markdown_representation) 99 | -------------------------------------------------------------------------------- /benchmarks/configurations.py: -------------------------------------------------------------------------------- 1 | """configurations for benchmarks.""" 2 | 3 | from mesa.examples import BoidFlockers, BoltzmannWealth, Schelling, WolfSheep 4 | 5 | configurations = { 6 | # Schelling Model Configurations 7 | BoltzmannWealth: { 8 | "small": { 9 | "seeds": 50, 10 | "replications": 5, 11 | "steps": 125, 12 | "parameters": { 13 | "n": 100, 14 | "width": 10, 15 | "height": 10, 16 | }, 17 | }, 18 | "large": { 19 | "seeds": 10, 20 | "replications": 3, 21 | "steps": 10, 22 | "parameters": { 23 | "n": 10000, 24 | "width": 100, 25 | "height": 100, 26 | }, 27 | }, 28 | }, 29 | # Schelling Model Configurations 30 | Schelling: { 31 | "small": { 32 | "seeds": 50, 33 | "replications": 5, 34 | "steps": 20, 35 | "parameters": { 36 | "height": 40, 37 | "width": 40, 38 | "homophily": 0.4, 39 | "radius": 1, 40 | "density": 0.625, 41 | }, 42 | }, 43 | "large": { 44 | "seeds": 10, 45 | "replications": 3, 46 | "steps": 10, 47 | "parameters": { 48 | "height": 100, 49 | "width": 100, 50 | "homophily": 1, 51 | "radius": 2, 52 | "density": 0.8, 53 | }, 54 | }, 55 | }, 56 | # WolfSheep Model Configurations 57 | WolfSheep: { 58 | "small": { 59 | "seeds": 50, 60 | "replications": 5, 61 | "steps": 80, 62 | "parameters": { 63 | "height": 25, 64 | "width": 25, 65 | "initial_sheep": 60, 66 | "initial_wolves": 40, 67 | "sheep_reproduce": 0.2, 68 | "wolf_reproduce": 0.1, 69 | "grass_regrowth_time": 20, 70 | }, 71 | }, 72 | "large": { 73 | "seeds": 10, 74 | "replications": 3, 75 | "steps": 20, 76 | "parameters": { 77 | "height": 100, 78 | "width": 100, 79 | "initial_sheep": 1000, 80 | "initial_wolves": 500, 81 | "sheep_reproduce": 0.4, 82 | "wolf_reproduce": 0.2, 83 | "grass_regrowth_time": 10, 84 | }, 85 | }, 86 | }, 87 | # BoidFlockers Model Configurations 88 | BoidFlockers: { 89 | "small": { 90 | "seeds": 25, 91 | "replications": 3, 92 | "steps": 20, 93 | "parameters": { 94 | "population_size": 200, 95 | "width": 100, 96 | "height": 100, 97 | "vision": 5, 98 | }, 99 | }, 100 | "large": { 101 | "seeds": 10, 102 | "replications": 3, 103 | "steps": 10, 104 | "parameters": { 105 | "population_size": 400, 106 | "width": 150, 107 | "height": 150, 108 | "vision": 15, 109 | }, 110 | }, 111 | }, 112 | } 113 | -------------------------------------------------------------------------------- /benchmarks/global_benchmark.py: -------------------------------------------------------------------------------- 1 | """runner for global performance benchmarks.""" 2 | 3 | import gc 4 | import os 5 | import pickle 6 | import sys 7 | import time 8 | import timeit 9 | 10 | # making sure we use this version of mesa and not one 11 | # also installed in site_packages or so. 12 | sys.path.insert(0, os.path.abspath("..")) 13 | 14 | from configurations import configurations 15 | 16 | from mesa.experimental.devs.simulator import ABMSimulator 17 | 18 | 19 | # Generic function to initialize and run a model 20 | def run_model(model_class, seed, parameters): 21 | """Run model for given seed and parameter values. 22 | 23 | Args: 24 | model_class: a model class 25 | seed: the seed 26 | parameters: parameters for the run 27 | 28 | Returns: 29 | startup time and run time 30 | """ 31 | uses_simulator = ["WolfSheep"] 32 | start_init = timeit.default_timer() 33 | if model_class.__name__ in uses_simulator: 34 | simulator = ABMSimulator() 35 | model = model_class(simulator=simulator, seed=seed, **parameters) 36 | else: 37 | model = model_class(seed=seed, **parameters) 38 | 39 | end_init_start_run = timeit.default_timer() 40 | 41 | if model_class.__name__ in uses_simulator: 42 | simulator.run_for(config["steps"]) 43 | else: 44 | for _ in range(config["steps"]): 45 | model.step() 46 | 47 | end_run = timeit.default_timer() 48 | 49 | return (end_init_start_run - start_init), (end_run - end_init_start_run) 50 | 51 | 52 | # Function to run experiments and save the fastest replication for each seed 53 | def run_experiments(model_class, config): 54 | """Run performance benchmarks. 55 | 56 | Args: 57 | model_class: the model class to use for the benchmark 58 | config: the benchmark configuration 59 | 60 | """ 61 | gc.enable() 62 | sys.path.insert(0, os.path.abspath(".")) 63 | 64 | init_times = [] 65 | run_times = [] 66 | for seed in range(1, config["seeds"] + 1): 67 | fastest_init = float("inf") 68 | fastest_run = float("inf") 69 | for _replication in range(1, config["replications"] + 1): 70 | init_time, run_time = run_model(model_class, seed, config["parameters"]) 71 | if init_time < fastest_init: 72 | fastest_init = init_time 73 | if run_time < fastest_run: 74 | fastest_run = run_time 75 | init_times.append(fastest_init) 76 | run_times.append(fastest_run) 77 | 78 | return init_times, run_times 79 | 80 | 81 | print(f"{time.strftime('%H:%M:%S', time.localtime())} starting benchmarks.") 82 | results_dict = {} 83 | for model, model_config in configurations.items(): 84 | for size, config in model_config.items(): 85 | results = run_experiments(model, config) 86 | 87 | mean_init = sum(results[0]) / len(results[0]) 88 | mean_run = sum(results[1]) / len(results[1]) 89 | 90 | print( 91 | f"{time.strftime('%H:%M:%S', time.localtime())} {model.__name__:<14} ({size}) timings: Init {mean_init:.5f} s; Run {mean_run:.4f} s" 92 | ) 93 | 94 | results_dict[model, size] = results 95 | 96 | # Change this name to anything you like 97 | save_name = "timings" 98 | 99 | i = 1 100 | while os.path.exists(f"{save_name}_{i}.pickle"): 101 | i += 1 102 | 103 | with open(f"{save_name}_{i}.pickle", "wb") as handle: 104 | pickle.dump(results_dict, handle, protocol=pickle.HIGHEST_PROTOCOL) 105 | 106 | print(f"Done benchmarking. Saved results to {save_name}_{i}.pickle.") 107 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | name: mesa-tutorials 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python 6 | - numpy 7 | - pip 8 | - pip: 9 | - nbgitpuller 10 | - pandas 11 | - matplotlib 12 | - seaborn 13 | - solara 14 | - mesa[rec] 15 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 80% 6 | threshold: 1% 7 | 8 | ignore: 9 | - "benchmarks/**" 10 | - "mesa/visualization/**" 11 | 12 | comment: off 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # To use this docker-compose.yml, run `docker compose up`. If you want to make 2 | # it run in the background, you instead run `docker compose up -d`. If you want 3 | # to run a different example, change the `MODEL_DIR` below to e.g. 4 | # "examples/sugarscape_cg". You can also specify the path to your custom model 5 | # (relative to this Git repository). 6 | # Note: the model directory MUST contain a run.py file. 7 | version: '3' 8 | services: 9 | install: 10 | build: . 11 | image: mesa:dev 12 | volumes: 13 | - .:/opt/mesa 14 | environment: 15 | # You may replace this with any model directory located 16 | # within the current directory. 17 | # E.g. if it is at my-model, then you specify it as 18 | # /opt/mesa/my-model. 19 | MODEL_DIR: /opt/mesa/mesa/examples/basic/schelling 20 | ports: 21 | - 8765:8765 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Docs for Mesa 2 | 3 | The readable version of the docs is hosted at [mesa.readthedocs.org](http://mesa.readthedocs.org/). 4 | 5 | This folder contains the docs that build the docs for the core mesa code on readthdocs. 6 | 7 | ## How to publish updates to the docs 8 | 9 | Updating docs can be confusing. Here are the basic setups. 10 | 11 | #### Submit a pull request with updates 12 | 1. Create branch (either via branching or fork of repo) -- try to use a descriptive name. 13 | * `git checkout -b doc-updates` 14 | 1. Update the docs. Save. 15 | 1. Build the docs, from the inside of the docs folder. 16 | * **Requires** sphinx: `pip install sphinx` 17 | * **Requires** nbsphinx: `pip install nbsphinx` (this will render the images from jupyter in the docs) 18 | * **Requires** sphinx-copybutton: `pip install sphinx-copybutton` (this will enable a copy option at the top of the code written in the docs) 19 | * `make html` 20 | 1. Commit the changes. If there are new files, you will have to explicit add them. 21 | * `git commit -am "Updating docs."` 22 | 1. Push the branch 23 | * `git push origin doc-updates` 24 | 1. From here you will want to submit a pull request to main. 25 | 26 | #### Update read the docs 27 | 28 | From this point, you will need to find someone that has access to readthedocs. Currently, that is [@jackiekazil](https://github.com/jackiekazil), [@rht](https://github.com/rht), and [@tpike3](https://github.com/dmasad). 29 | 30 | 1. Accept the pull request into main. 31 | 1. Log into readthedocs and launch a new build -- builds take about 10 minutes or so. 32 | 33 | ### Helpful Sphnix tips 34 | * Build html from docs: 35 | * `make html` 36 | * Autogenerate / update sphninx from docstrings (replace your name as the author): 37 | * `sphinx-apidoc -A "Jackie Kazil" -F -o docs mesa/` -------------------------------------------------------------------------------- /docs/apis/agent.md: -------------------------------------------------------------------------------- 1 | # Agent 2 | 3 | 4 | ```{eval-rst} 5 | .. automodule:: mesa.agent 6 | :members: 7 | :inherited-members: 8 | ``` 9 | -------------------------------------------------------------------------------- /docs/apis/api_main.md: -------------------------------------------------------------------------------- 1 | # APIs 2 | 3 | ```{toctree} 4 | :maxdepth: 3 5 | 6 | model 7 | agent 8 | time 9 | space 10 | discrete_space 11 | datacollection 12 | batchrunner 13 | visualization 14 | logging 15 | experimental 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/apis/batchrunner.md: -------------------------------------------------------------------------------- 1 | # Batchrunner 2 | 3 | ```{eval-rst} 4 | .. automodule:: batchrunner 5 | :members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/apis/datacollection.md: -------------------------------------------------------------------------------- 1 | # Data collection 2 | 3 | ```{eval-rst} 4 | .. automodule:: datacollection 5 | :members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/apis/discrete_space.md: -------------------------------------------------------------------------------- 1 | ## Discrete Space 2 | 3 | ```{eval-rst} 4 | .. automodule:: mesa.discrete_space.__init__ 5 | :members: 6 | ``` 7 | 8 | ```{eval-rst} 9 | .. automodule:: mesa.discrete_space.cell 10 | :members: 11 | ``` 12 | 13 | ```{eval-rst} 14 | .. automodule:: mesa.discrete_space.cell_agent 15 | :members: 16 | ``` 17 | 18 | ```{eval-rst} 19 | .. automodule:: mesa.discrete_space.cell_collection 20 | :members: 21 | ``` 22 | 23 | ```{eval-rst} 24 | .. automodule:: mesa.discrete_space.discrete_space 25 | :members: 26 | ``` 27 | 28 | ```{eval-rst} 29 | .. automodule:: mesa.discrete_space.grid 30 | :members: 31 | ``` 32 | 33 | ```{eval-rst} 34 | .. automodule:: mesa.discrete_space.network 35 | :members: 36 | ``` 37 | 38 | ```{eval-rst} 39 | .. automodule:: mesa.discrete_space.voronoi 40 | :members: 41 | ``` -------------------------------------------------------------------------------- /docs/apis/experimental.md: -------------------------------------------------------------------------------- 1 | # Experimental 2 | This namespace contains experimental features. These are under development, and their API is not necessarily stable. 3 | 4 | 5 | ## Devs 6 | 7 | ```{eval-rst} 8 | .. automodule:: experimental.devs.eventlist 9 | :members: 10 | ``` 11 | 12 | ```{eval-rst} 13 | .. automodule:: experimental.devs.simulator 14 | :members: 15 | ``` 16 | 17 | ## Continuous Space 18 | 19 | ```{eval-rst} 20 | .. automodule:: experimental.continuous_space.continuous_space 21 | :members: 22 | ``` 23 | 24 | ```{eval-rst} 25 | .. automodule:: experimental.continuous_space.continuous_space_agents 26 | :members: 27 | ``` 28 | 29 | ## Continuous Space 30 | 31 | ```{eval-rst} 32 | .. automodule:: experimental.continuous_space.continuous_space 33 | :members: 34 | ``` 35 | 36 | ```{eval-rst} 37 | .. automodule:: experimental.continuous_space.continuous_space_agents 38 | :members: 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/apis/mesa_logging.md: -------------------------------------------------------------------------------- 1 | # logging 2 | 3 | ```{eval-rst} 4 | .. automodule:: mesa.mesa_logging 5 | :members: 6 | :inherited-members: 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/apis/model.md: -------------------------------------------------------------------------------- 1 | # Model 2 | 3 | ```{eval-rst} 4 | .. automodule:: mesa.model 5 | :members: 6 | :inherited-members: 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/apis/space.md: -------------------------------------------------------------------------------- 1 | # Spaces 2 | 3 | ```{eval-rst} 4 | .. automodule:: mesa.space 5 | :members: 6 | :inherited-members: 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/apis/visualization.md: -------------------------------------------------------------------------------- 1 | # Visualization 2 | 3 | For detailed tutorials, please refer to: 4 | 5 | - [Basic Visualization](../tutorials/4_visualization_basic) 6 | - [Dynamic Agent Visualization](../tutorials/5_visualization_dynamic_agents) 7 | - [Custom Agent Visualization](../tutorials/6_visualization_custom) 8 | 9 | 10 | ## Jupyter Visualization 11 | 12 | ```{eval-rst} 13 | .. automodule:: mesa.visualization.solara_viz 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | ``` 18 | 19 | ```{eval-rst} 20 | .. automodule:: mesa.visualization.components.__init__ 21 | :members: 22 | :undoc-members: 23 | :show-inheritance: 24 | ``` 25 | 26 | ## User Parameters 27 | 28 | ```{eval-rst} 29 | .. automodule:: mesa.visualization.user_param 30 | :members: 31 | :undoc-members: 32 | :show-inheritance: 33 | ``` 34 | 35 | 36 | ## Matplotlib-based visualizations 37 | 38 | ```{eval-rst} 39 | .. automodule:: mesa.visualization.components.matplotlib_components 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | ``` 44 | 45 | ```{eval-rst} 46 | .. automodule:: mesa.visualization.mpl_space_drawing 47 | :members: 48 | :undoc-members: 49 | :show-inheritance: 50 | ``` 51 | 52 | 53 | ## Altair-based visualizations 54 | 55 | ```{eval-rst} 56 | .. automodule:: mesa.visualization.components.altair_components 57 | :members: 58 | :undoc-members: 59 | :show-inheritance: 60 | ``` 61 | 62 | ## Command Console 63 | 64 | ```{eval-rst} 65 | .. automodule:: mesa.visualization.command_console 66 | :members: 67 | :undoc-members: 68 | :show-inheritance: 69 | ``` -------------------------------------------------------------------------------- /docs/best-practices.md: -------------------------------------------------------------------------------- 1 | # Best Practices 2 | 3 | Here are some general principles that have proven helpful for developing models. 4 | 5 | ## Model Layout 6 | 7 | A model should be contained in a folder named with lower-case letters and 8 | underscores, such as `wolf_sheep`. Within that directory: 9 | 10 | - `Readme.md` describes the model, how to use it, and any other details. 11 | - `model.py` should contain the model class. 12 | - `agents.py` should contain the agent class(es). 13 | - `app.py` should contain the Solara-based visualization code (optional). 14 | 15 | You can add more files as needed, for example: 16 | - `run.py` could contain the code to run the model. 17 | - `batch_run.py` could contain the code to run the model multiple times. 18 | - `analysis.py` could contain any analysis code. 19 | 20 | Input data can be stored in a `data` directory, output data in an `output`, processed results in a `results` directory, images in an `images` directory, etc. 21 | 22 | All our [examples](examples) follow this layout. 23 | 24 | ## Randomization 25 | 26 | If your model involves some random choice, you can use the built-in `random` 27 | property that many Mesa objects have, including `Model`, `Agent`, and `AgentSet`. This works exactly 28 | like the built-in `random` library. 29 | 30 | ```python 31 | class AwesomeModel(Model): 32 | # ... 33 | 34 | def cool_method(self): 35 | interesting_number = self.random.random() 36 | print(interesting_number) 37 | 38 | class AwesomeAgent(Agent): 39 | # ... 40 | def __init__(self, unique_id, model, ...): 41 | super().__init__(unique_id, model) 42 | # ... 43 | 44 | def my_method(self): 45 | random_number = self.random.randint(0, 100) 46 | ``` 47 | 48 | `Agent.random` is just a convenient shorthand in the Agent class to `self.model.random`. If you create your own `AgentSet` 49 | instances, you have to pass `random` explicitly. Typically, you can simply do, in a Model instance, 50 | `my_agentset = AgentSet([], random=self.random)`. This ensures that `my_agentset` uses the same random 51 | number generator as the rest of the model. 52 | 53 | 54 | When a model object is created, its random property is automatically seeded 55 | with the current time. The seed determines the sequence of random numbers; if 56 | you instantiate a model with the same seed, you will get the same results. 57 | To allow you to set the seed, make sure your model has a `seed` argument in its 58 | `__init__`. 59 | 60 | ```python 61 | class AwesomeModel(Model): 62 | 63 | def __init__(self, seed=None): 64 | super().__init__(seed=seed) 65 | ... 66 | 67 | def cool_method(self): 68 | interesting_number = self.random.random() 69 | print(interesting_number) 70 | 71 | >>> model0 = AwesomeModel(seed=0) 72 | >>> model0._seed 73 | 0 74 | >>> model0.cool_method() 75 | 0.8444218515250481 76 | >>> model1 = AwesomeModel(seed=0) 77 | >>> model1.cool_method() 78 | 0.8444218515250481 79 | ``` 80 | -------------------------------------------------------------------------------- /docs/example_template.txt: -------------------------------------------------------------------------------- 1 | 2 | $readme_file 3 | 4 | ## Agents 5 | 6 | ```python 7 | $agent_file 8 | ``` 9 | 10 | 11 | ## Model 12 | 13 | ```python 14 | $model_file 15 | ``` 16 | 17 | 18 | ## App 19 | 20 | ```python 21 | $app_file 22 | ``` -------------------------------------------------------------------------------- /docs/examples_overview_template.txt: -------------------------------------------------------------------------------- 1 | 2 | $readme 3 | 4 | 5 | 6 | ```{toctree} 7 | :hidden: true 8 | :maxdepth: 2 9 | 10 | $example_paths 11 | 12 | 13 | ``` -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | Mesa is a modular framework for building, analyzing and visualizing agent-based models. 3 | 4 | **Agent-based models** are computer simulations involving multiple entities (the agents) acting and interacting with one another based on their programmed behavior. Agents can be used to represent living cells, animals, individual humans, even entire organizations or abstract entities. Sometimes, we may have an understanding of how the individual components of a system behave, and want to see what system-level behaviors and effects emerge from their interaction. Other times, we may have a good idea of how the system overall behaves, and want to figure out what individual behaviors explain it. Or we may want to see how to get agents to cooperate or compete most effectively. Or we may just want to build a cool toy with colorful little dots moving around. 5 | 6 | ## Tutorials 7 | If you want to get a quick start on how to build agent based models with MESA, check the overview and tutorials: 8 | 9 | - [Overview of the MESA library](overview): Learn about the core concepts and components of Mesa. 10 | - [Creating Your First Model](tutorials/0_first_model): Learn how to create your first Mesa model. 11 | - [Adding Space](tutorials/1_adding_space): Learn how to add space to your Mesa model and understand Mesa's space architecture. 12 | - [Collecting Data](tutorials/2_collecting_data): Learn how to collect model level and agent level data with Mesa' DataCollector. 13 | - [AgentSet](tutorials/3_agentset): Learn how to more effectively manage agents with Mesa's AgentSet. 14 | - [Basic Visualization](tutorials/4_visualization_basic): Learn how to build an interactive dashboard with Mesa's visualization module. 15 | - [Dynamic Agent Visualization](tutorials/5_visualization_dynamic_agents): Learn how to dynamically represent your agents in your interactive dashboard. 16 | - [Custom Visualization Components](tutorials/6_visualization_custom): Learn how to add custom visual components to your interactive dashboard. 17 | - [Parameter Sweeps](tutorials/7_batch_run): Learn how to conduct parameter sweeps on multiple processors with Mesa's BatchRunner. 18 | - [Comparing Scenarios](tutorials/8_comparing_scenarios): Think through how to analyze your parameter sweep results to find insight in your Mesa simulations. 19 | 20 | ## Examples 21 | Mesa ships with a collection of example models. These are classic ABMs, so if you are familiar with ABMs and want to get a quick sense of how MESA works, these examples are great place to start. You can find them [here](examples). 22 | 23 | ## Further resources 24 | To further explore Mesa and its features, we have the following resources available: 25 | 26 | ### Best practices 27 | - [Mesa best practices](best-practices): an overview of tips and guidelines for using MESA. 28 | 29 | ### API documentation 30 | - [Mesa API reference](apis): Detailed documentation of Mesa's classes and functions. 31 | 32 | ### Repository of models built using MESA 33 | - [Mesa Examples repository](https://github.com/projectmesa/mesa-examples): A collection of example models demonstrating various Mesa features and modeling techniques. 34 | 35 | ### Migration guide 36 | - [Mesa 3.0 Migration guide](migration_guide): If you're upgrading from an earlier version of Mesa, this guide will help you navigate the changes in Mesa 3.0. 37 | 38 | ### Source Ccode and development 39 | - [Mesa GitHub repository](https://github.com/projectmesa/mesa): Access the full source code of Mesa, contribute to its development, or report issues. 40 | - [Mesa release notes](https://github.com/projectmesa/mesa/releases): View the detailed changelog of Mesa, including all past releases and their features. 41 | 42 | ### Community and support 43 | - [Mesa GitHub Discussions](https://github.com/projectmesa/mesa/discussions): Join discussions, ask questions, and connect with other Mesa users. 44 | - [Matrix Chat](https://matrix.to/#/#project-mesa:matrix.org): Real-time chat for quick questions and community interaction. 45 | 46 | Enjoy modelling with Mesa, and feel free to reach out! 47 | 48 | 49 | 50 | 51 | 52 | ```{toctree} 53 | :hidden: true 54 | :maxdepth: 7 55 | 56 | Overview 57 | Creating Your First Model 58 | Adding Space 59 | Collecting Data 60 | AgentSet 61 | Basic Visualization 62 | Dynamic Agent Visualization 63 | Custom Visualization Components 64 | Parameter Sweeps 65 | Comparing Scenarios 66 | Best Practices 67 | 68 | 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/images/Discrete_Space.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/docs/images/Discrete_Space.drawio.png -------------------------------------------------------------------------------- /docs/images/mesa_logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/docs/images/mesa_logo.ico -------------------------------------------------------------------------------- /docs/images/mesa_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/docs/images/mesa_logo.png -------------------------------------------------------------------------------- /docs/images/wolf_sheep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/docs/images/wolf_sheep.png -------------------------------------------------------------------------------- /docs/mesa.md: -------------------------------------------------------------------------------- 1 | # mesa package 2 | 3 | ## Submodules 4 | 5 | ## mesa.agent module 6 | 7 | ```{eval-rst} 8 | .. automodule:: mesa.agent 9 | :members: 10 | :undoc-members: 11 | :show-inheritance: 12 | ``` 13 | 14 | ## mesa.batchrunner module 15 | 16 | ```{eval-rst} 17 | .. automodule:: mesa.batchrunner 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | ``` 22 | 23 | ## mesa.datacollection module 24 | 25 | ```{eval-rst} 26 | .. automodule:: mesa.datacollection 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | ``` 31 | 32 | ## mesa.main module 33 | 34 | ```{eval-rst} 35 | .. automodule:: mesa.main 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | ``` 40 | 41 | ## mesa.model module 42 | 43 | ```{eval-rst} 44 | .. automodule:: mesa.model 45 | :members: 46 | :undoc-members: 47 | :show-inheritance: 48 | ``` 49 | 50 | ## mesa.space module 51 | 52 | ```{eval-rst} 53 | .. automodule:: mesa.space 54 | :members: 55 | :undoc-members: 56 | :show-inheritance: 57 | ``` 58 | 59 | ## Module contents 60 | 61 | ```{eval-rst} 62 | .. automodule:: mesa 63 | :members: 64 | :undoc-members: 65 | :show-inheritance: 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/mesa_extension.md: -------------------------------------------------------------------------------- 1 | # Mesa Extensions Overview 2 | 3 | This contains an overview of Mesa Extensions. Mesa's extensibility is a key feature that allows users to enhance functionality, improve scalability, and foster innovation in agent-based modeling. 4 | 5 | 6 | ## Mesa-Geo 🌍 7 | 8 | **Field:** Geographic Information Systems (GIS) 9 | 10 | --- 11 | **Description:** 12 | Mesa-Geo is an extension of the Mesa framework designed to facilitate working with geographic data in agent-based modeling. It introduces a **GeoSpace** to host **GeoAgents**, which are enhanced agents that include a `geometry` attribute ([a Shapely object](https://shapely.readthedocs.io/en/latest/manual.html)) and a `crs` attribute (Coordinate Reference System). These attributes enable the integration of geographic and spatial data into simulations. Geometries can be defined manually using Shapely or imported from various sources, such as vector data files (e.g., shapefiles), GeoJSON objects, or GeoPandas GeoDataFrames. 13 | 14 | --- 15 | **Key Features:** 16 | - **Spatial Reference Systems Support:** Mesa-Geo handles coordinate reference systems (CRS), which is essential for working with geographic data in various projections. 17 | - **Geometric Operations Support:** Mesa-Geo utilizes Shapely, which provides robust tools for creating and manipulating geometric shapes like points, polygons, and lines. 18 | - **Topological Operations Support:** Functions for analyzing spatial relationships between geometries. 19 | 20 | --- 21 | **Author(s):** Wang Boyu 22 | 23 | --- 24 | **Additional Resources:** 25 | For more information, visit the official [Mesa-Geo repository](https://github.com/projectmesa/mesa-geo?tab=readme-ov-file). 26 | 27 | --- 28 | 29 | ## Mesa Examples 📊 30 | 31 | **Description:** 32 | Mesa Examples provide a collection of models and use cases demonstrating the features and capabilities of the Mesa framework for agent-based modeling. These examples include core and user-submitted models covering a variety of domains like grid spaces, networks, visualization, and GIS. 33 | 34 | --- 35 | 36 | **Key Features:** 37 | - **Core Examples:** Fully tested and updated models included directly with the Mesa framework. 38 | - **User Examples:** Community-contributed models showcasing advanced and diverse use cases. 39 | - **Extensive Coverage:** Examples for grid spaces, GIS integration, networks, visualization, and more. 40 | - **Easy Access:** Available directly from the Mesa package or via installation from the repository. 41 | 42 | --- 43 | 44 | **Author(s):** Contributions from the Mesa developer community. 45 | 46 | --- 47 | 48 | **Examples Include:** 49 | - **Grid Space:** Models like Bank Reserves, Conway’s Game of Life, and Forest Fire. 50 | - **GIS:** GeoSchelling Models, Urban Growth, and Population Models. 51 | - **Network:** Boltzmann Wealth Model and Ant System for the Traveling Salesman Problem. 52 | - **Visualization:** Charting tools and grid displays. 53 | 54 | --- 55 | 56 | **For More Information:** 57 | For more Detail, Visit the [Mesa Examples Repository](https://github.com/projectmesa/mesa/tree/main/mesa/examples). 58 | 59 | --- 60 | 61 | ## **Mesa-Frames** 🚀 62 | 63 | **Description:** 64 | Mesa-Frames is an extension of the Mesa framework designed to handle complex simulations with thousands of agents. By utilizing DataFrames (pandas or Polars), it enhances scalability and performance while maintaining a syntax similar to Mesa. 65 | 66 | --- 67 | 68 | **Key Features:** 69 | - **Enhanced Performance:** Uses DataFrames for SIMD processing and vectorized functions to speed up simulations. 70 | - **Backend Support:** Supports `pandas` (ease of use) and `Polars` (performance innovations with Rust-based backend). 71 | - **Seamless Integration:** Maintains a similar API and functionality as the base Mesa framework for easier adoption. 72 | - **In-Place Operations:** Functional programming and fast memory-efficient copy methods. 73 | - **Future Plans:** GPU functionality, automatic model vectorization, and backend-independent AgentSet class. 74 | 75 | --- 76 | 77 | **Usage:** 78 | - Define agents using `AgentSetPandas` or `AgentSetPolars`. 79 | - Implement models by subclassing `ModelDF`. 80 | - Perform vectorized operations to enhance simulation performance. 81 | 82 | --- 83 | 84 | **Author(s):** 85 | Developed and maintained by the Mesa development community. 86 | 87 | --- 88 | 89 | **License:** 90 | Distributed under the MIT License. 91 | 92 | --- 93 | 94 | **More Information:** 95 | Visit the [GitHub Repository](https://github.com/projectmesa/mesa-frames). 96 | 97 | --- 98 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1747542820, 24 | "narHash": "sha256-GaOZntlJ6gPPbbkTLjbd8BMWaDYafhuuYRNrxCGnPJw=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "292fa7d4f6519c074f0a50394dbbe69859bb6043", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "pyproject-build-systems": { 38 | "inputs": { 39 | "nixpkgs": [ 40 | "nixpkgs" 41 | ], 42 | "pyproject-nix": [ 43 | "pyproject-nix" 44 | ], 45 | "uv2nix": [ 46 | "uv2nix" 47 | ] 48 | }, 49 | "locked": { 50 | "lastModified": 1744599653, 51 | "narHash": "sha256-nysSwVVjG4hKoOjhjvE6U5lIKA8sEr1d1QzEfZsannU=", 52 | "owner": "pyproject-nix", 53 | "repo": "build-system-pkgs", 54 | "rev": "7dba6dbc73120e15b558754c26024f6c93015dd7", 55 | "type": "github" 56 | }, 57 | "original": { 58 | "owner": "pyproject-nix", 59 | "repo": "build-system-pkgs", 60 | "type": "github" 61 | } 62 | }, 63 | "pyproject-nix": { 64 | "inputs": { 65 | "nixpkgs": [ 66 | "nixpkgs" 67 | ] 68 | }, 69 | "locked": { 70 | "lastModified": 1746540146, 71 | "narHash": "sha256-QxdHGNpbicIrw5t6U3x+ZxeY/7IEJ6lYbvsjXmcxFIM=", 72 | "owner": "pyproject-nix", 73 | "repo": "pyproject.nix", 74 | "rev": "e09c10c24ebb955125fda449939bfba664c467fd", 75 | "type": "github" 76 | }, 77 | "original": { 78 | "owner": "pyproject-nix", 79 | "repo": "pyproject.nix", 80 | "type": "github" 81 | } 82 | }, 83 | "root": { 84 | "inputs": { 85 | "flake-utils": "flake-utils", 86 | "nixpkgs": "nixpkgs", 87 | "pyproject-build-systems": "pyproject-build-systems", 88 | "pyproject-nix": "pyproject-nix", 89 | "uv2nix": "uv2nix" 90 | } 91 | }, 92 | "systems": { 93 | "locked": { 94 | "lastModified": 1681028828, 95 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 96 | "owner": "nix-systems", 97 | "repo": "default", 98 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 99 | "type": "github" 100 | }, 101 | "original": { 102 | "owner": "nix-systems", 103 | "repo": "default", 104 | "type": "github" 105 | } 106 | }, 107 | "uv2nix": { 108 | "inputs": { 109 | "nixpkgs": [ 110 | "nixpkgs" 111 | ], 112 | "pyproject-nix": [ 113 | "pyproject-nix" 114 | ] 115 | }, 116 | "locked": { 117 | "lastModified": 1747441483, 118 | "narHash": "sha256-W8BFXk5R0TuJcjIhcGoMpSOaIufGXpizK0pm+uTqynA=", 119 | "owner": "pyproject-nix", 120 | "repo": "uv2nix", 121 | "rev": "582024dc64663e9f88d467c2f7f7b20d278349de", 122 | "type": "github" 123 | }, 124 | "original": { 125 | "owner": "pyproject-nix", 126 | "repo": "uv2nix", 127 | "type": "github" 128 | } 129 | } 130 | }, 131 | "root": "root", 132 | "version": 7 133 | } 134 | -------------------------------------------------------------------------------- /maintenance/fetch_unlabeled_prs.py: -------------------------------------------------------------------------------- 1 | # noqa: D100 2 | import os 3 | from datetime import datetime 4 | 5 | import requests 6 | 7 | # Configuration 8 | # Your GitHub Personal Access Token 9 | GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") 10 | if GITHUB_TOKEN is None: 11 | print( 12 | "Please specify your GitHub Personal Access Token as GITHUB_TOKEN in your .bashrc" 13 | ) 14 | exit() 15 | GITHUB_USERNAME = "projectmesa" 16 | GITHUB_REPO = "mesa" 17 | HEADERS = {"Authorization": f"token {GITHUB_TOKEN}"} 18 | TIMEOUT = 20 19 | 20 | 21 | def get_latest_release_date() -> str: 22 | """Fetches the latest release date from the GitHub repository.""" 23 | url = ( 24 | f"https://api.github.com/repos/{GITHUB_USERNAME}/{GITHUB_REPO}/releases/latest" 25 | ) 26 | response = requests.get(url, headers=HEADERS, timeout=TIMEOUT) 27 | response.raise_for_status() # Raises an exception for HTTP error codes 28 | return response.json()["published_at"] 29 | 30 | 31 | def get_closed_pull_requests_since_latest_release( 32 | latest_release_date, 33 | ) -> list[dict[str, any]]: 34 | """Fetches pull requests created or updated after the latest release date, then filters by merged date.""" 35 | pull_requests = [] 36 | page = 1 37 | while True: 38 | # Fetch PRs that were created or updated after the latest release date 39 | url = f"https://api.github.com/repos/{GITHUB_USERNAME}/{GITHUB_REPO}/pulls?state=closed&base=main&sort=updated&direction=desc&page={page}" 40 | response = requests.get(url, headers=HEADERS, timeout=TIMEOUT) 41 | response.raise_for_status() 42 | prs = response.json() 43 | if not prs: 44 | break 45 | 46 | # Convert latest release date to datetime for comparison 47 | latest_release_datetime = datetime.strptime( 48 | latest_release_date, "%Y-%m-%dT%H:%M:%SZ" 49 | ).astimezone() 50 | 51 | for pr in prs: 52 | # Convert PR's `updated_at` to datetime for comparison 53 | pr_updated_at = datetime.strptime( 54 | pr["updated_at"], "%Y-%m-%dT%H:%M:%SZ" 55 | ).astimezone() 56 | # Stop fetching if PR was updated before the latest release 57 | if pr_updated_at < latest_release_datetime: 58 | return pull_requests 59 | 60 | if pr["merged_at"]: 61 | pr_merged_at = datetime.strptime( 62 | pr["merged_at"], "%Y-%m-%dT%H:%M:%SZ" 63 | ).astimezone() 64 | if pr_merged_at > latest_release_datetime and not pr["labels"]: 65 | pull_requests.append(pr) 66 | page += 1 67 | return pull_requests 68 | 69 | 70 | def main() -> None: # noqa: D103 71 | # Based on https://github.com/projectmesa/mesa/pull/1917#issuecomment-1871352058 72 | latest_release_date = get_latest_release_date() 73 | pull_requests = get_closed_pull_requests_since_latest_release(latest_release_date) 74 | if len(pull_requests) <= 0: 75 | return 76 | print("These pull requests must be labeled:") 77 | for pr in pull_requests: 78 | print(f" PR #{pr['number']}: {pr['title']} - Merged at: {pr['merged_at']}") 79 | 80 | 81 | if __name__ == "__main__": 82 | main() 83 | -------------------------------------------------------------------------------- /mesa/__init__.py: -------------------------------------------------------------------------------- 1 | """Mesa Agent-Based Modeling Framework. 2 | 3 | Core Objects: Model, and Agent. 4 | """ 5 | 6 | import datetime 7 | 8 | import mesa.discrete_space as discrete_space 9 | import mesa.experimental as experimental 10 | import mesa.space as space 11 | from mesa.agent import Agent 12 | from mesa.batchrunner import batch_run 13 | from mesa.datacollection import DataCollector 14 | from mesa.model import Model 15 | 16 | __all__ = [ 17 | "Agent", 18 | "DataCollector", 19 | "Model", 20 | "batch_run", 21 | "discrete_space", 22 | "experimental", 23 | "space", 24 | ] 25 | 26 | __title__ = "mesa" 27 | __version__ = "3.2.1.dev" 28 | __license__ = "Apache 2.0" 29 | _this_year = datetime.datetime.now(tz=datetime.UTC).date().year 30 | __copyright__ = f"Copyright {_this_year} Project Mesa Team" 31 | -------------------------------------------------------------------------------- /mesa/discrete_space/__init__.py: -------------------------------------------------------------------------------- 1 | """Cell spaces for active, property-rich spatial modeling in Mesa. 2 | 3 | Cell spaces extend Mesa's spatial modeling capabilities by making the space itself active - 4 | each position (cell) can have properties and behaviors rather than just containing agents. 5 | This enables more sophisticated environmental modeling and agent-environment interactions. 6 | 7 | Key components: 8 | - Cells: Active positions that can have properties and contain agents 9 | - CellAgents: Agents that understand how to interact with cells 10 | - Spaces: Different cell organization patterns (grids, networks, etc.) 11 | - PropertyLayers: Efficient property storage and manipulation 12 | 13 | This is particularly useful for models where the environment plays an active role, 14 | like resource growth, pollution diffusion, or infrastructure networks. The cell 15 | space system is experimental and under active development. 16 | """ 17 | 18 | from mesa.discrete_space.cell import Cell 19 | from mesa.discrete_space.cell_agent import ( 20 | CellAgent, 21 | FixedAgent, 22 | Grid2DMovingAgent, 23 | ) 24 | from mesa.discrete_space.cell_collection import CellCollection 25 | from mesa.discrete_space.discrete_space import DiscreteSpace 26 | from mesa.discrete_space.grid import ( 27 | Grid, 28 | HexGrid, 29 | OrthogonalMooreGrid, 30 | OrthogonalVonNeumannGrid, 31 | ) 32 | from mesa.discrete_space.network import Network 33 | from mesa.discrete_space.property_layer import PropertyLayer 34 | from mesa.discrete_space.voronoi import VoronoiGrid 35 | 36 | __all__ = [ 37 | "Cell", 38 | "CellAgent", 39 | "CellCollection", 40 | "DiscreteSpace", 41 | "FixedAgent", 42 | "Grid", 43 | "Grid2DMovingAgent", 44 | "HexGrid", 45 | "Network", 46 | "OrthogonalMooreGrid", 47 | "OrthogonalVonNeumannGrid", 48 | "PropertyLayer", 49 | "VoronoiGrid", 50 | ] 51 | -------------------------------------------------------------------------------- /mesa/discrete_space/cell_agent.py: -------------------------------------------------------------------------------- 1 | """Agents that understand how to exist in and move through cell spaces. 2 | 3 | Provides specialized agent classes that handle cell occupation, movement, and 4 | proper registration: 5 | - CellAgent: Mobile agents that can move between cells 6 | - FixedAgent: Immobile agents permanently fixed to cells 7 | - Grid2DMovingAgent: Agents with grid-specific movement capabilities 8 | 9 | These classes ensure consistent agent-cell relationships and proper state management 10 | as agents move through the space. They can be used directly or as examples for 11 | creating custom cell-aware agents. 12 | """ 13 | 14 | from __future__ import annotations 15 | 16 | from typing import TYPE_CHECKING, Protocol 17 | 18 | from mesa.agent import Agent 19 | 20 | if TYPE_CHECKING: 21 | from mesa.discrete_space import Cell 22 | 23 | 24 | class HasCellProtocol(Protocol): 25 | """Protocol for discrete space cell holders.""" 26 | 27 | cell: Cell 28 | 29 | 30 | class HasCell: 31 | """Descriptor for cell movement behavior.""" 32 | 33 | _mesa_cell: Cell | None = None 34 | 35 | @property 36 | def cell(self) -> Cell | None: # noqa: D102 37 | return self._mesa_cell 38 | 39 | @cell.setter 40 | def cell(self, cell: Cell | None) -> None: 41 | # remove from current cell 42 | if self.cell is not None: 43 | self.cell.remove_agent(self) 44 | 45 | # update private attribute 46 | self._mesa_cell = cell 47 | 48 | # add to new cell 49 | if cell is not None: 50 | cell.add_agent(self) 51 | 52 | 53 | class BasicMovement: 54 | """Mixin for moving agents in discrete space.""" 55 | 56 | def move_to(self: HasCellProtocol, cell: Cell) -> None: 57 | """Move to a new cell.""" 58 | self.cell = cell 59 | 60 | def move_relative(self: HasCellProtocol, direction: tuple[int, ...]): 61 | """Move to a cell relative to the current cell. 62 | 63 | Args: 64 | direction: The direction to move in. 65 | """ 66 | new_cell = self.cell.connections.get(direction) 67 | if new_cell is not None: 68 | self.cell = new_cell 69 | else: 70 | raise ValueError(f"No cell in direction {direction}") 71 | 72 | 73 | class FixedCell(HasCell): 74 | """Mixin for agents that are fixed to a cell.""" 75 | 76 | @property 77 | def cell(self) -> Cell | None: # noqa: D102 78 | return self._mesa_cell 79 | 80 | @cell.setter 81 | def cell(self, cell: Cell) -> None: 82 | if self.cell is not None: 83 | raise ValueError("Cannot move agent in FixedCell") 84 | self._mesa_cell = cell 85 | 86 | cell.add_agent(self) 87 | 88 | 89 | class CellAgent(Agent, HasCell, BasicMovement): 90 | """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces. 91 | 92 | Attributes: 93 | cell (Cell): The cell the agent is currently in. 94 | """ 95 | 96 | def remove(self): 97 | """Remove the agent from the model.""" 98 | super().remove() 99 | self.cell = None # ensures that we are also removed from cell 100 | 101 | 102 | class FixedAgent(Agent, FixedCell): 103 | """A patch in a 2D grid.""" 104 | 105 | def remove(self): 106 | """Remove the agent from the model.""" 107 | super().remove() 108 | 109 | # fixme we leave self._mesa_cell on the original value 110 | # so you cannot hijack remove() to move patches 111 | self.cell.remove_agent(self) 112 | 113 | 114 | class Grid2DMovingAgent(CellAgent): 115 | """Mixin for moving agents in 2D grids.""" 116 | 117 | # fmt: off 118 | DIRECTION_MAP = { 119 | "n": (-1, 0), "north": (-1, 0), "up": (-1, 0), 120 | "s": (1, 0), "south": (1, 0), "down": (1, 0), 121 | "e": (0, 1), "east": (0, 1), "right": (0, 1), 122 | "w": (0, -1), "west": (0, -1), "left": (0, -1), 123 | "ne": (-1, 1), "northeast": (-1, 1), "upright": (-1, 1), 124 | "nw": (-1, -1), "northwest": (-1, -1), "upleft": (-1, -1), 125 | "se": (1, 1), "southeast": (1, 1), "downright": (1, 1), 126 | "sw": (1, -1), "southwest": (1, -1), "downleft": (1, -1) 127 | } 128 | # fmt: on 129 | 130 | def move(self, direction: str, distance: int = 1): 131 | """Move the agent in a cardinal direction. 132 | 133 | Args: 134 | direction: The cardinal direction to move in. 135 | distance: The distance to move. 136 | """ 137 | direction = direction.lower() # Convert direction to lowercase 138 | 139 | if direction not in self.DIRECTION_MAP: 140 | raise ValueError(f"Invalid direction: {direction}") 141 | 142 | move_vector = self.DIRECTION_MAP[direction] 143 | for _ in range(distance): 144 | self.move_relative(move_vector) 145 | -------------------------------------------------------------------------------- /mesa/discrete_space/network.py: -------------------------------------------------------------------------------- 1 | """Network-based cell space using arbitrary connection patterns. 2 | 3 | Creates spaces where cells connect based on network relationships rather than 4 | spatial proximity. Built on NetworkX graphs, this enables: 5 | - Arbitrary connectivity patterns between cells 6 | - Graph-based neighborhood definitions 7 | - Logical rather than physical distances 8 | - Dynamic connectivity changes 9 | - Integration with NetworkX's graph algorithms 10 | 11 | Useful for modeling systems like social networks, transportation systems, 12 | or any environment where connectivity matters more than physical location. 13 | """ 14 | 15 | from random import Random 16 | from typing import Any 17 | 18 | from mesa.discrete_space.cell import Cell 19 | from mesa.discrete_space.discrete_space import DiscreteSpace 20 | 21 | 22 | class Network(DiscreteSpace[Cell]): 23 | """A networked discrete space.""" 24 | 25 | def __init__( 26 | self, 27 | G: Any, # noqa: N803 28 | capacity: int | None = None, 29 | random: Random | None = None, 30 | cell_klass: type[Cell] = Cell, 31 | ) -> None: 32 | """A Networked grid. 33 | 34 | Args: 35 | G: a NetworkX Graph instance. 36 | capacity (int) : the capacity of the cell 37 | random (Random): a random number generator 38 | cell_klass (type[Cell]): The base Cell class to use in the Network 39 | 40 | """ 41 | super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) 42 | self.G = G 43 | 44 | for node_id in self.G.nodes: 45 | self._cells[node_id] = self.cell_klass( 46 | node_id, capacity, random=self.random 47 | ) 48 | 49 | self._connect_cells() 50 | 51 | def _connect_cells(self) -> None: 52 | for cell in self.all_cells: 53 | self._connect_single_cell(cell) 54 | 55 | def _connect_single_cell(self, cell: Cell): 56 | for node_id in self.G.neighbors(cell.coordinate): 57 | cell.connect(self._cells[node_id], node_id) 58 | 59 | def add_cell(self, cell: Cell): 60 | """Add a cell to the space.""" 61 | super().add_cell(cell) 62 | self.G.add_node(cell.coordinate) 63 | 64 | def remove_cell(self, cell: Cell): 65 | """Remove a cell from the space.""" 66 | super().remove_cell(cell) 67 | self.G.remove_node(cell.coordinate) 68 | 69 | def add_connection(self, cell1: Cell, cell2: Cell): 70 | """Add a connection between the two cells.""" 71 | super().add_connection(cell1, cell2) 72 | self.G.add_edge(cell1.coordinate, cell2.coordinate) 73 | 74 | def remove_connection(self, cell1: Cell, cell2: Cell): 75 | """Remove a connection between the two cells.""" 76 | super().remove_connection(cell1, cell2) 77 | self.G.remove_edge(cell1.coordinate, cell2.coordinate) 78 | -------------------------------------------------------------------------------- /mesa/examples/README.md: -------------------------------------------------------------------------------- 1 | # Mesa Core Examples 2 | This repository contains a curated set of classic agent-based models implemented using Mesa. These core examples are maintained by the Mesa development team and serve as both demonstrations of Mesa's capabilities and starting points for your own models. 3 | 4 | ## Overview 5 | The examples are categorized into two groups: 6 | 1. **Basic Examples** - Simpler models that use only stable Mesa features; ideal for beginners 7 | 2. **Advanced Examples** - More complex models that demonstrate additional concepts and may use some experimental features 8 | 9 | > **Note:** Looking for more examples? Visit the [mesa-examples](https://github.com/projectmesa/mesa-examples) repository for user-contributed models and showcases. 10 | 11 | ## Basic Examples 12 | The basic examples are relatively simple and only use stable Mesa features. They are good starting points for learning how to use Mesa. 13 | 14 | ### [Boltzmann Wealth Model](examples/basic/boltzmann_wealth_model) 15 | Completed code to go along with the [tutorial](https://mesa.readthedocs.io/latest/tutorials/0_first_model.html) on making a simple model of how a highly-skewed wealth distribution can emerge from simple rules. 16 | 17 | ### [Boids Flockers Model](examples/basic/boid_flockers) 18 | [Boids](https://en.wikipedia.org/wiki/Boids)-style flocking model, demonstrating the use of agents moving through a continuous space following direction vectors. 19 | 20 | ### [Conway's Game of Life](examples/basic/conways_game_of_life) 21 | 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. 22 | 23 | ### [Schelling Segregation Model](examples/basic/schelling) 24 | Mesa implementation of the classic [Schelling segregation](http://nifty.stanford.edu/2014/mccown-schelling-model-segregation/) model. 25 | 26 | ### [Virus on a Network Model](examples/basic/virus_on_network) 27 | This model is based on the NetLogo [Virus on a Network](https://ccl.northwestern.edu/netlogo/models/VirusonaNetwork) model. 28 | 29 | ## Advanced Examples 30 | The advanced examples are more complex and may use experimental Mesa features. They are good starting points for learning how to build more complex models. 31 | 32 | ### [Epstein Civil Violence Model](examples/advanced/epstein_civil_violence) 33 | Joshua Epstein's [model](https://www.pnas.org/doi/10.1073/pnas.092080199) of how a decentralized uprising can be suppressed or reach a critical mass of support. 34 | 35 | ### [Demographic Prisoner's Dilemma on a Grid](examples/advanced/pd_grid) 36 | 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. 37 | 38 | ### [Sugarscape Model with Traders](examples/advanced/sugarscape_g1mt) 39 | 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 how emergent price equilibrium can happen via decentralized dynamics. 40 | 41 | ### [Wolf-Sheep Predation Model](examples/advanced/wolf_sheep) 42 | Implementation of an ecological model of predation and reproduction, based on the NetLogo [Wolf Sheep Predation](http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation) model. 43 | -------------------------------------------------------------------------------- /mesa/examples/__init__.py: -------------------------------------------------------------------------------- 1 | from mesa.examples.advanced.alliance_formation.model import MultiLevelAllianceModel 2 | from mesa.examples.advanced.epstein_civil_violence.model import EpsteinCivilViolence 3 | from mesa.examples.advanced.pd_grid.model import PdGrid 4 | from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt 5 | from mesa.examples.advanced.wolf_sheep.model import WolfSheep 6 | from mesa.examples.basic.boid_flockers.model import BoidFlockers 7 | from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealth 8 | from mesa.examples.basic.conways_game_of_life.model import ConwaysGameOfLife 9 | from mesa.examples.basic.schelling.model import Schelling 10 | from mesa.examples.basic.virus_on_network.model import VirusOnNetwork 11 | 12 | __all__ = [ 13 | "BoidFlockers", 14 | "BoltzmannWealth", 15 | "ConwaysGameOfLife", 16 | "EpsteinCivilViolence", 17 | "MultiLevelAllianceModel", 18 | "PdGrid", 19 | "Schelling", 20 | "SugarscapeG1mt", 21 | "VirusOnNetwork", 22 | "WolfSheep", 23 | ] 24 | -------------------------------------------------------------------------------- /mesa/examples/advanced/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/advanced/__init__.py -------------------------------------------------------------------------------- /mesa/examples/advanced/alliance_formation/Readme.md: -------------------------------------------------------------------------------- 1 | # Alliance Formation Model (Meta-Agent Example) 2 | 3 | ## Summary 4 | 5 | This model demonstrates Mesa's meta agent capability. 6 | 7 | **Overview of meta agent:** Complex systems often have multiple levels of components. A city is not a single entity, but it is made of districts,neighborhoods, buildings, and people. A forest comprises an ecosystem of trees, plants, animals, and microorganisms. An organization is not one entity, but is made of departments, sub-departments, and people. A person is not a single entity, but it is made of micro biomes, organs and cells. 8 | 9 | This reality is the motivation for meta-agents. It allows users to represent these multiple levels, where each level can have agents with sub-agents. 10 | 11 | This model demonstrates Mesa's ability to dynamically create new classes of agents that are composed of existing agents. These meta-agents inherits functions and attributes from their sub-agents and users can specify new functionality or attributes they want the meta agent to have. For example, if a user is doing a factory simulation with autonomous systems, each major component of that system can be a sub-agent of the overall robot agent. Or, if someone is doing a simulation of an organization, individuals can be part of different organizational units that are working for some purpose. 12 | 13 | To provide a simple demonstration of this capability is an alliance formation model. 14 | 15 | In this simulation n agents are created, who have two attributes (1) power and (2) preference. Each attribute is a number between 0 and 1 over a gaussian distribution. Agents then randomly select other agents and use the [bilateral shapley value](https://en.wikipedia.org/wiki/Shapley_value) to determine if they should form an alliance. If the expected utility support an alliances, the agent creates a meta-agent. Subsequent steps may add agents to the meta-agent, create new instances of similar hierarchy, or create a new hierarchy level where meta-agents form an alliance of meta-agents. In this visualization of this model a new meta-agent hierarchy will be a larger node and a new color. 16 | 17 | In MetaAgents current configuration, agents being part of multiple meta-agents is not supported. 18 | 19 | If you would like to see an example of explicit meta-agent formation see the [warehouse model in the Mesa example's repository](https://github.com/projectmesa/mesa-examples/tree/main/examples/warehouse) 20 | 21 | 22 | ## Installation 23 | 24 | This model requires Mesa's recommended install and scipy 25 | 26 | ``` 27 | $ pip install mesa[rec] 28 | ``` 29 | 30 | ## How to Run 31 | 32 | To run the model interactively, in this directory, run the following command 33 | 34 | ``` 35 | $ solara run app.py 36 | ``` 37 | 38 | ## Files 39 | 40 | - `model.py`: Contains creation of agents, the network and management of agent execution. 41 | - `agents.py`: Contains logic for forming alliances and creation of new agents 42 | - `app.py`: Contains the code for the interactive Solara visualization. 43 | 44 | ## Further Reading 45 | 46 | The full tutorial describing how the model is built can be found at: 47 | https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html 48 | 49 | An example of the bilateral shapley value in another model: 50 | [Techno-Social Energy Infrastructure Siting: Sustainable Energy Modeling Programming (SEMPro)](https://www.jasss.org/16/3/6.html) 51 | -------------------------------------------------------------------------------- /mesa/examples/advanced/alliance_formation/__init__ .py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/advanced/alliance_formation/__init__ .py -------------------------------------------------------------------------------- /mesa/examples/advanced/alliance_formation/agents.py: -------------------------------------------------------------------------------- 1 | import mesa 2 | 3 | 4 | class AllianceAgent(mesa.Agent): 5 | """ 6 | Agent has three attributes power (float), position (float) and level (int) 7 | 8 | """ 9 | 10 | def __init__(self, model, power, position, level=0): 11 | super().__init__(model) 12 | self.power = power 13 | self.position = position 14 | self.level = level 15 | 16 | """ 17 | For this demo model agent only need attributes. 18 | 19 | More complex models could have functions that define agent behavior. 20 | """ 21 | -------------------------------------------------------------------------------- /mesa/examples/advanced/alliance_formation/app.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import networkx as nx 3 | import solara 4 | from matplotlib.figure import Figure 5 | 6 | from mesa.examples.advanced.alliance_formation.model import MultiLevelAllianceModel 7 | from mesa.visualization import SolaraViz 8 | from mesa.visualization.utils import update_counter 9 | 10 | model_params = { 11 | "seed": { 12 | "type": "InputText", 13 | "value": 42, 14 | "label": "Random Seed", 15 | }, 16 | "n": { 17 | "type": "SliderInt", 18 | "value": 50, 19 | "label": "Number of agents:", 20 | "min": 10, 21 | "max": 100, 22 | "step": 1, 23 | }, 24 | } 25 | 26 | # Create visualization elements. The visualization elements are solara components 27 | # that receive the model instance as a "prop" and display it in a certain way. 28 | # Under the hood these are just classes that receive the model instance. 29 | # You can also author your own visualization elements, which can also be functions 30 | # that receive the model instance and return a valid solara component. 31 | 32 | 33 | @solara.component 34 | def plot_network(model): 35 | update_counter.get() 36 | g = model.network 37 | pos = nx.fruchterman_reingold_layout(g) 38 | fig = Figure() 39 | ax = fig.subplots() 40 | labels = {agent.unique_id: agent.unique_id for agent in model.agents} 41 | node_sizes = [g.nodes[node]["size"] for node in g.nodes] 42 | node_colors = [g.nodes[node]["size"] for node in g.nodes()] 43 | 44 | nx.draw( 45 | g, 46 | pos, 47 | node_size=node_sizes, 48 | node_color=node_colors, 49 | cmap=plt.cm.coolwarm, 50 | labels=labels, 51 | ax=ax, 52 | ) 53 | 54 | solara.FigureMatplotlib(fig) 55 | 56 | 57 | # Create initial model instance 58 | model = MultiLevelAllianceModel(50) 59 | 60 | # Create the SolaraViz page. This will automatically create a server and display the 61 | # visualization elements in a web browser. 62 | # Display it using the following command in the example directory: 63 | # solara run app.py 64 | # It will automatically update and display any changes made to this file 65 | page = SolaraViz( 66 | model, 67 | components=[plot_network], 68 | model_params=model_params, 69 | name="Alliance Formation Model", 70 | ) 71 | page # noqa 72 | -------------------------------------------------------------------------------- /mesa/examples/advanced/epstein_civil_violence/Readme.md: -------------------------------------------------------------------------------- 1 | # Epstein Civil Violence Model 2 | 3 | ## Summary 4 | 5 | This model is based on Joshua Epstein's simulation of how civil unrest grows and is suppressed. Citizen agents wander the grid randomly, and are endowed with individual risk aversion and hardship levels; there is also a universal regime legitimacy value. There are also Cop agents, who work on behalf of the regime. Cops arrest Citizens who are actively rebelling; Citizens decide whether to rebel based on their hardship and the regime legitimacy, and their perceived probability of arrest. 6 | 7 | The model generates mass uprising as self-reinforcing processes: if enough agents are rebelling, the probability of any individual agent being arrested is reduced, making more agents more likely to join the uprising. However, the more rebelling Citizens the Cops arrest, the less likely additional agents become to join. 8 | 9 | ## How to Run 10 | 11 | To run the model interactively, in this directory, run the following command 12 | 13 | ``` 14 | $ solara run app.py 15 | ``` 16 | 17 | ## Files 18 | 19 | * ``model.py``: Core model code. 20 | * ``agent.py``: Agent classes. 21 | * ``app.py``: Sets up the interactive visualization. 22 | * ``Epstein Civil Violence.ipynb``: Jupyter notebook conducting some preliminary analysis of the model. 23 | 24 | ## Further Reading 25 | 26 | This model is based adapted from: 27 | 28 | [Epstein, J. “Modeling civil violence: An agent-based computational approach”, Proceedings of the National Academy of Sciences, Vol. 99, Suppl. 3, May 14, 2002](http://www.pnas.org/content/99/suppl.3/7243.short) 29 | 30 | A similar model is also included with NetLogo: 31 | 32 | Wilensky, U. (2004). NetLogo Rebellion model. http://ccl.northwestern.edu/netlogo/models/Rebellion. Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. 33 | -------------------------------------------------------------------------------- /mesa/examples/advanced/epstein_civil_violence/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/advanced/epstein_civil_violence/__init__.py -------------------------------------------------------------------------------- /mesa/examples/advanced/epstein_civil_violence/app.py: -------------------------------------------------------------------------------- 1 | from mesa.examples.advanced.epstein_civil_violence.agents import ( 2 | Citizen, 3 | CitizenState, 4 | Cop, 5 | ) 6 | from mesa.examples.advanced.epstein_civil_violence.model import EpsteinCivilViolence 7 | from mesa.visualization import ( 8 | Slider, 9 | SolaraViz, 10 | make_plot_component, 11 | make_space_component, 12 | ) 13 | 14 | COP_COLOR = "#000000" 15 | 16 | agent_colors = { 17 | CitizenState.ACTIVE: "#FE6100", 18 | CitizenState.QUIET: "#648FFF", 19 | CitizenState.ARRESTED: "#808080", 20 | } 21 | 22 | 23 | def citizen_cop_portrayal(agent): 24 | if agent is None: 25 | return 26 | 27 | portrayal = { 28 | "size": 50, 29 | } 30 | 31 | if isinstance(agent, Citizen): 32 | portrayal["color"] = agent_colors[agent.state] 33 | elif isinstance(agent, Cop): 34 | portrayal["color"] = COP_COLOR 35 | 36 | return portrayal 37 | 38 | 39 | def post_process(ax): 40 | ax.set_aspect("equal") 41 | ax.set_xticks([]) 42 | ax.set_yticks([]) 43 | ax.get_figure().set_size_inches(10, 10) 44 | 45 | 46 | model_params = { 47 | "seed": { 48 | "type": "InputText", 49 | "value": 42, 50 | "label": "Random Seed", 51 | }, 52 | "height": 40, 53 | "width": 40, 54 | "citizen_density": Slider("Initial Agent Density", 0.7, 0.0, 0.9, 0.1), 55 | "cop_density": Slider("Initial Cop Density", 0.04, 0.0, 0.1, 0.01), 56 | "citizen_vision": Slider("Citizen Vision", 7, 1, 10, 1), 57 | "cop_vision": Slider("Cop Vision", 7, 1, 10, 1), 58 | "legitimacy": Slider("Government Legitimacy", 0.82, 0.0, 1, 0.01), 59 | "max_jail_term": Slider("Max Jail Term", 30, 0, 50, 1), 60 | } 61 | 62 | space_component = make_space_component( 63 | citizen_cop_portrayal, post_process=post_process, draw_grid=False 64 | ) 65 | 66 | chart_component = make_plot_component( 67 | {state.name.lower(): agent_colors[state] for state in CitizenState} 68 | ) 69 | 70 | epstein_model = EpsteinCivilViolence() 71 | 72 | page = SolaraViz( 73 | epstein_model, 74 | components=[space_component, chart_component], 75 | model_params=model_params, 76 | name="Epstein Civil Violence", 77 | ) 78 | page # noqa 79 | -------------------------------------------------------------------------------- /mesa/examples/advanced/epstein_civil_violence/model.py: -------------------------------------------------------------------------------- 1 | import mesa 2 | from mesa.examples.advanced.epstein_civil_violence.agents import ( 3 | Citizen, 4 | CitizenState, 5 | Cop, 6 | ) 7 | 8 | 9 | class EpsteinCivilViolence(mesa.Model): 10 | """ 11 | Model 1 from "Modeling civil violence: An agent-based computational 12 | approach," by Joshua Epstein. 13 | http://www.pnas.org/content/99/suppl_3/7243.full 14 | 15 | Args: 16 | height: grid height 17 | width: grid width 18 | citizen_density: approximate % of cells occupied by citizens. 19 | cop_density: approximate % of cells occupied by cops. 20 | citizen_vision: number of cells in each direction (N, S, E and W) that 21 | citizen can inspect 22 | cop_vision: number of cells in each direction (N, S, E and W) that cop 23 | can inspect 24 | legitimacy: (L) citizens' perception of regime legitimacy, equal 25 | across all citizens 26 | max_jail_term: (J_max) 27 | active_threshold: if (grievance - (risk_aversion * arrest_probability)) 28 | > threshold, citizen rebels 29 | arrest_prob_constant: set to ensure agents make plausible arrest 30 | probability estimates 31 | movement: binary, whether agents try to move at step end 32 | max_iters: model may not have a natural stopping point, so we set a 33 | max. 34 | """ 35 | 36 | def __init__( 37 | self, 38 | width=40, 39 | height=40, 40 | citizen_density=0.7, 41 | cop_density=0.074, 42 | citizen_vision=7, 43 | cop_vision=7, 44 | legitimacy=0.8, 45 | max_jail_term=1000, 46 | active_threshold=0.1, 47 | arrest_prob_constant=2.3, 48 | movement=True, 49 | max_iters=1000, 50 | seed=None, 51 | ): 52 | super().__init__(seed=seed) 53 | self.movement = movement 54 | self.max_iters = max_iters 55 | 56 | self.grid = mesa.discrete_space.OrthogonalVonNeumannGrid( 57 | (width, height), capacity=1, torus=True, random=self.random 58 | ) 59 | 60 | model_reporters = { 61 | "active": CitizenState.ACTIVE.name, 62 | "quiet": CitizenState.QUIET.name, 63 | "arrested": CitizenState.ARRESTED.name, 64 | } 65 | agent_reporters = { 66 | "jail_sentence": lambda a: getattr(a, "jail_sentence", None), 67 | "arrest_probability": lambda a: getattr(a, "arrest_probability", None), 68 | } 69 | self.datacollector = mesa.DataCollector( 70 | model_reporters=model_reporters, agent_reporters=agent_reporters 71 | ) 72 | if cop_density + citizen_density > 1: 73 | raise ValueError("Cop density + citizen density must be less than 1") 74 | 75 | for cell in self.grid.all_cells: 76 | klass = self.random.choices( 77 | [Citizen, Cop, None], 78 | cum_weights=[citizen_density, citizen_density + cop_density, 1], 79 | )[0] 80 | 81 | if klass == Cop: 82 | cop = Cop(self, vision=cop_vision, max_jail_term=max_jail_term) 83 | cop.move_to(cell) 84 | elif klass == Citizen: 85 | citizen = Citizen( 86 | self, 87 | regime_legitimacy=legitimacy, 88 | threshold=active_threshold, 89 | vision=citizen_vision, 90 | arrest_prob_constant=arrest_prob_constant, 91 | ) 92 | citizen.move_to(cell) 93 | 94 | self.running = True 95 | self._update_counts() 96 | self.datacollector.collect(self) 97 | 98 | def step(self): 99 | """ 100 | Advance the model by one step and collect data. 101 | """ 102 | self.agents.shuffle_do("step") 103 | self._update_counts() 104 | self.datacollector.collect(self) 105 | 106 | if self.steps > self.max_iters: 107 | self.running = False 108 | 109 | def _update_counts(self): 110 | """Helper function for counting nr. of citizens in given state.""" 111 | counts = self.agents_by_type[Citizen].groupby("state").count() 112 | 113 | for state in CitizenState: 114 | setattr(self, state.name, counts.get(state, 0)) 115 | -------------------------------------------------------------------------------- /mesa/examples/advanced/pd_grid/Readme.md: -------------------------------------------------------------------------------- 1 | # Demographic Prisoner's Dilemma on a Grid 2 | 3 | ## Summary 4 | 5 | The Demographic Prisoner's Dilemma is a family of variants on the classic two-player [Prisoner's Dilemma]. The model consists of agents, each with a strategy of either Cooperate or Defect. Each agent's payoff is based on its strategy and the strategies of its spatial neighbors. After each step of the model, the agents adopt the strategy of their neighbor with the highest total score. 6 | 7 | The model payoff table is: 8 | 9 | | | Cooperate | Defect| 10 | |:-------------:|:---------:|:-----:| 11 | | **Cooperate** | 1, 1 | 0, D | 12 | | **Defect** | D, 0 | 0, 0 | 13 | 14 | Where *D* is the defection bonus, generally set higher than 1. In these runs, the defection bonus is set to $D=1.6$. 15 | 16 | The Demographic Prisoner's Dilemma demonstrates how simple rules can lead to the emergence of widespread cooperation, despite the Defection strategy dominating each individual interaction game. However, it is also interesting for another reason: it is known to be sensitive to the activation regime employed in it. 17 | 18 | ## How to Run 19 | 20 | To run the model interactively, in this directory, run the following command 21 | 22 | ``` 23 | $ solara run app.py 24 | ``` 25 | 26 | ## Files 27 | 28 | * ``agents.py``: contains the agent class. 29 | * ``model.py``: contains the model class; the model takes a ``activation_order`` string as an argument, which determines in which order agents are activated: Sequential, Random or Simultaneous. 30 | * ``app.py``: contains the interactive visualization server. 31 | * ``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. 32 | 33 | ## Further Reading 34 | 35 | This model is adapted from: 36 | 37 | Wilensky, U. (2002). NetLogo PD Basic Evolutionary model. http://ccl.northwestern.edu/netlogo/models/PDBasicEvolutionary. Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. 38 | 39 | The Demographic Prisoner's Dilemma originates from: 40 | 41 | [Epstein, J. Zones of Cooperation in Demographic Prisoner's Dilemma. 1998.](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.8.8629&rep=rep1&type=pdf) 42 | -------------------------------------------------------------------------------- /mesa/examples/advanced/pd_grid/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/advanced/pd_grid/__init__.py -------------------------------------------------------------------------------- /mesa/examples/advanced/pd_grid/agents.py: -------------------------------------------------------------------------------- 1 | from mesa.discrete_space import CellAgent 2 | 3 | 4 | class PDAgent(CellAgent): 5 | """Agent member of the iterated, spatial prisoner's dilemma model.""" 6 | 7 | def __init__(self, model, starting_move=None, cell=None): 8 | """ 9 | Create a new Prisoner's Dilemma agent. 10 | 11 | Args: 12 | model: model instance 13 | starting_move: If provided, determines the agent's initial state: 14 | C(ooperating) or D(efecting). Otherwise, random. 15 | """ 16 | super().__init__(model) 17 | self.score = 0 18 | self.cell = cell 19 | if starting_move: 20 | self.move = starting_move 21 | else: 22 | self.move = self.random.choice(["C", "D"]) 23 | self.next_move = None 24 | 25 | @property 26 | def is_cooroperating(self): 27 | return self.move == "C" 28 | 29 | def step(self): 30 | """Get the best neighbor's move, and change own move accordingly 31 | if better than own score.""" 32 | 33 | # neighbors = self.model.grid.get_neighbors(self.pos, True, include_center=True) 34 | neighbors = [*list(self.cell.neighborhood.agents), self] 35 | best_neighbor = max(neighbors, key=lambda a: a.score) 36 | self.next_move = best_neighbor.move 37 | 38 | if self.model.activation_order != "Simultaneous": 39 | self.advance() 40 | 41 | def advance(self): 42 | self.move = self.next_move 43 | self.score += self.increment_score() 44 | 45 | def increment_score(self): 46 | neighbors = self.cell.neighborhood.agents 47 | if self.model.activation_order == "Simultaneous": 48 | moves = [neighbor.next_move for neighbor in neighbors] 49 | else: 50 | moves = [neighbor.move for neighbor in neighbors] 51 | return sum(self.model.payoff[(self.move, move)] for move in moves) 52 | -------------------------------------------------------------------------------- /mesa/examples/advanced/pd_grid/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Solara-based visualization for the Spatial Prisoner's Dilemma Model. 3 | """ 4 | 5 | from mesa.examples.advanced.pd_grid.model import PdGrid 6 | from mesa.visualization import ( 7 | Slider, 8 | SolaraViz, 9 | make_plot_component, 10 | make_space_component, 11 | ) 12 | 13 | 14 | def pd_agent_portrayal(agent): 15 | """ 16 | Portrayal function for rendering PD agents in the visualization. 17 | """ 18 | return { 19 | "color": "blue" if agent.move == "C" else "red", 20 | "marker": "s", # square marker 21 | "size": 25, 22 | } 23 | 24 | 25 | # Model parameters 26 | model_params = { 27 | "seed": { 28 | "type": "InputText", 29 | "value": 42, 30 | "label": "Random Seed", 31 | }, 32 | "width": Slider("Grid Width", value=50, min=10, max=100, step=1), 33 | "height": Slider("Grid Height", value=50, min=10, max=100, step=1), 34 | "activation_order": { 35 | "type": "Select", 36 | "value": "Random", 37 | "values": PdGrid.activation_regimes, 38 | "label": "Activation Regime", 39 | }, 40 | } 41 | 42 | 43 | # Create grid visualization component using Altair 44 | grid_viz = make_space_component(agent_portrayal=pd_agent_portrayal) 45 | 46 | # Create plot for tracking cooperating agents over time 47 | plot_component = make_plot_component("Cooperating_Agents") 48 | 49 | # Initialize model 50 | initial_model = PdGrid() 51 | 52 | # Create visualization with all components 53 | page = SolaraViz( 54 | model=initial_model, 55 | components=[grid_viz, plot_component], 56 | model_params=model_params, 57 | name="Spatial Prisoner's Dilemma", 58 | ) 59 | page # noqa B018 60 | -------------------------------------------------------------------------------- /mesa/examples/advanced/pd_grid/model.py: -------------------------------------------------------------------------------- 1 | import mesa 2 | from mesa.discrete_space import OrthogonalMooreGrid 3 | from mesa.examples.advanced.pd_grid.agents import PDAgent 4 | 5 | 6 | class PdGrid(mesa.Model): 7 | """Model class for iterated, spatial prisoner's dilemma model.""" 8 | 9 | activation_regimes = ["Sequential", "Random", "Simultaneous"] 10 | 11 | # This dictionary holds the payoff for this agent, 12 | # keyed on: (my_move, other_move) 13 | 14 | payoff = {("C", "C"): 1, ("C", "D"): 0, ("D", "C"): 1.6, ("D", "D"): 0} 15 | 16 | def __init__( 17 | self, width=50, height=50, activation_order="Random", payoffs=None, seed=None 18 | ): 19 | """ 20 | Create a new Spatial Prisoners' Dilemma Model. 21 | 22 | Args: 23 | width, height: Grid size. There will be one agent per grid cell. 24 | activation_order: Can be "Sequential", "Random", or "Simultaneous". 25 | Determines the agent activation regime. 26 | payoffs: (optional) Dictionary of (move, neighbor_move) payoffs. 27 | """ 28 | super().__init__(seed=seed) 29 | self.activation_order = activation_order 30 | self.grid = OrthogonalMooreGrid((width, height), torus=True, random=self.random) 31 | 32 | if payoffs is not None: 33 | self.payoff = payoffs 34 | 35 | PDAgent.create_agents( 36 | self, len(self.grid.all_cells.cells), cell=self.grid.all_cells.cells 37 | ) 38 | 39 | self.datacollector = mesa.DataCollector( 40 | { 41 | "Cooperating_Agents": lambda m: len( 42 | [a for a in m.agents if a.move == "C"] 43 | ) 44 | } 45 | ) 46 | 47 | self.running = True 48 | self.datacollector.collect(self) 49 | 50 | def step(self): 51 | # Activate all agents, based on the activation regime 52 | match self.activation_order: 53 | case "Sequential": 54 | self.agents.do("step") 55 | case "Random": 56 | self.agents.shuffle_do("step") 57 | case "Simultaneous": 58 | self.agents.do("step") 59 | self.agents.do("advance") 60 | case _: 61 | raise ValueError(f"Unknown activation order: {self.activation_order}") 62 | 63 | # Collect data 64 | self.datacollector.collect(self) 65 | 66 | def run(self, n): 67 | """Run the model for n steps.""" 68 | for _ in range(n): 69 | self.step() 70 | -------------------------------------------------------------------------------- /mesa/examples/advanced/sugarscape_g1mt/Readme.md: -------------------------------------------------------------------------------- 1 | # Sugarscape Constant Growback Model with Traders 2 | 3 | ## Summary 4 | 5 | This is Epstein & Axtell's Sugarscape model with Traders, a detailed description is in Chapter four of 6 | *Growing Artificial Societies: Social Science from the Bottom Up (1996)*. The model shows an emergent price equilibrium can happen via a decentralized dynamics. 7 | 8 | This code generally matches the code in the Complexity Explorer Tutorial, but in `.py` instead of `.ipynb` format. 9 | 10 | ### Agents: 11 | 12 | - **Resource**: Resource agents grow back at one unit of sugar and spice per time step up to a specified max amount and can be harvested and traded by the trader agents. 13 | (if you do the interactive run, the color will be green if the resource agent has a bigger amount of sugar, or yellow if it has a bigger amount of spice) 14 | - **Traders**: Trader agents have the following attributes: (1) metabolism for sugar, (2) metabolism for spice, (3) vision, 15 | (4) initial sugar endowment and (5) initial spice endowment. The traverse the landscape harvesting sugar and spice and 16 | trading with other agents. If they run out of sugar or spice then they are removed from the model. (red circle if you do the interactive run) 17 | 18 | The trader agents traverse the landscape according to rule **M**: 19 | - Look out as far as vision permits in the four principal lattice directions and identify the unoccupied site(s). 20 | - Considering only unoccupied sites find the nearest position that produces the most welfare using the Cobb-Douglas function. 21 | - Move to the new position 22 | - Collect all the resources (sugar and spice) at that location 23 | (Epstein and Axtell, 1996, p. 99) 24 | 25 | The traders trade according to rule **T**: 26 | - Agents and potential trade partner compute their marginal rates of substitution (MRS), if they are equal *end*. 27 | - Exchange resources, with spice flowing from the agent with the higher MRS to the agent with the lower MRS and sugar 28 | flowing the opposite direction. 29 | - The price (p) is calculated by taking the geometric mean of the agents' MRS. 30 | - If p > 1 then p units of spice are traded for 1 unit of sugar; if p < 1 then 1/p units of sugar for 1 unit of spice 31 | - The trade occurs if it will (a) make both agent better off (increases MRS) and (b) does not cause the agents' MRS to 32 | cross over one another otherwise *end*. 33 | - This process then repeats until an *end* condition is met. 34 | (Epstein and Axtell, 1996, p. 105) 35 | 36 | The model demonstrates several Mesa concepts and features: 37 | - OrthogonalMooreGrid 38 | - Multiple agent types (traders, sugar, spice) 39 | - Dynamically removing agents from the grid and schedule when they die 40 | - Data Collection at the model and agent level 41 | - custom solara matplotlib space visualization 42 | 43 | 44 | ## How to Run 45 | 46 | To run the model interactively, in this directory, run the following command 47 | 48 | ``` 49 | $ solara run app.py 50 | ``` 51 | 52 | ## Files 53 | 54 | * `model.py`: The Sugarscape Constant Growback with Traders model. 55 | * `agents.py`: Defines the Trader agent class and the Resource agent class which contains an amount of sugar and spice. 56 | * `app.py`: Runs a visualization server via Solara (`solara run app.py`). 57 | * `sugar_map.txt`: Provides sugar and spice landscape in raster type format. 58 | * `tests.py`: Has tests to ensure that the model reproduces the results in shown in Growing Artificial Societies. 59 | 60 | ## Further Reading 61 | 62 | - [Growing Artificial Societies](https://mitpress.mit.edu/9780262550253/growing-artificial-societies/) 63 | - [Complexity Explorer Sugarscape with Traders Tutorial](https://www.complexityexplorer.org/courses/172-agent-based-models-with-python-an-introduction-to-mesa) 64 | -------------------------------------------------------------------------------- /mesa/examples/advanced/sugarscape_g1mt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/advanced/sugarscape_g1mt/__init__.py -------------------------------------------------------------------------------- /mesa/examples/advanced/sugarscape_g1mt/app.py: -------------------------------------------------------------------------------- 1 | from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt 2 | from mesa.visualization import Slider, SolaraViz, make_plot_component 3 | from mesa.visualization.components import AgentPortrayalStyle, PropertyLayerStyle 4 | from mesa.visualization.components.matplotlib_components import make_mpl_space_component 5 | 6 | 7 | def agent_portrayal(agent): 8 | return AgentPortrayalStyle( 9 | x=agent.cell.coordinate[0], 10 | y=agent.cell.coordinate[1], 11 | color="red", 12 | marker="o", 13 | size=10, 14 | zorder=1, 15 | ) 16 | 17 | 18 | def propertylayer_portrayal(layer): 19 | if layer.name == "sugar": 20 | return PropertyLayerStyle( 21 | color="blue", alpha=0.8, colorbar=True, vmin=0, vmax=10 22 | ) 23 | return PropertyLayerStyle(color="red", alpha=0.8, colorbar=True, vmin=0, vmax=10) 24 | 25 | 26 | sugarscape_space = make_mpl_space_component( 27 | agent_portrayal=agent_portrayal, 28 | propertylayer_portrayal=propertylayer_portrayal, 29 | post_process=None, 30 | draw_grid=False, 31 | ) 32 | 33 | model_params = { 34 | "seed": { 35 | "type": "InputText", 36 | "value": 42, 37 | "label": "Random Seed", 38 | }, 39 | "width": 50, 40 | "height": 50, 41 | # Population parameters 42 | "initial_population": Slider( 43 | "Initial Population", value=200, min=50, max=500, step=10 44 | ), 45 | # Agent endowment parameters 46 | "endowment_min": Slider("Min Initial Endowment", value=25, min=5, max=30, step=1), 47 | "endowment_max": Slider("Max Initial Endowment", value=50, min=30, max=100, step=1), 48 | # Metabolism parameters 49 | "metabolism_min": Slider("Min Metabolism", value=1, min=1, max=3, step=1), 50 | "metabolism_max": Slider("Max Metabolism", value=5, min=3, max=8, step=1), 51 | # Vision parameters 52 | "vision_min": Slider("Min Vision", value=1, min=1, max=3, step=1), 53 | "vision_max": Slider("Max Vision", value=5, min=3, max=8, step=1), 54 | # Trade parameter 55 | "enable_trade": {"type": "Checkbox", "value": True, "label": "Enable Trading"}, 56 | } 57 | 58 | model = SugarscapeG1mt() 59 | 60 | page = SolaraViz( 61 | model, 62 | components=[ 63 | sugarscape_space, 64 | make_plot_component("#Traders"), 65 | make_plot_component("Price"), 66 | ], 67 | model_params=model_params, 68 | name="Sugarscape {G1, M, T}", 69 | play_interval=150, 70 | ) 71 | page # noqa 72 | -------------------------------------------------------------------------------- /mesa/examples/advanced/wolf_sheep/Readme.md: -------------------------------------------------------------------------------- 1 | # Wolf-Sheep Predation Model 2 | 3 | ## Summary 4 | 5 | A simple ecological model, consisting of three agent types: wolves, sheep, and grass. The wolves and the sheep wander around the grid at random. Wolves and sheep both expend energy moving around, and replenish it by eating. Sheep eat grass, and wolves eat sheep if they end up on the same grid cell. 6 | 7 | If wolves and sheep have enough energy, they reproduce, creating a new wolf or sheep (in this simplified model, only one parent is needed for reproduction). The grass on each cell regrows at a constant rate. If any wolves and sheep run out of energy, they die. 8 | 9 | The model is tests and demonstrates several Mesa concepts and features: 10 | - MultiGrid 11 | - Multiple agent types (wolves, sheep, grass) 12 | - Overlay arbitrary text (wolf's energy) on agent's shapes while drawing on CanvasGrid 13 | - Agents inheriting a behavior (random movement) from an abstract parent 14 | - Writing a model composed of multiple files. 15 | - Dynamically adding and removing agents from the schedule 16 | 17 | ## How to Run 18 | 19 | To run the model interactively, in this directory, run the following command 20 | 21 | ``` 22 | $ solara run app.py 23 | ``` 24 | 25 | ## Files 26 | 27 | * ``wolf_sheep/random_walk.py``: This defines the ``RandomWalker`` agent, which implements the behavior of moving randomly across a grid, one cell at a time. Both the Wolf and Sheep agents will inherit from it. 28 | * ``wolf_sheep/test_random_walk.py``: Defines a simple model and a text-only visualization intended to make sure the RandomWalk class was working as expected. This doesn't actually model anything, but serves as an ad-hoc unit test. To run it, ``cd`` into the ``wolf_sheep`` directory and run ``python test_random_walk.py``. You'll see a series of ASCII grids, one per model step, with each cell showing a count of the number of agents in it. 29 | * ``wolf_sheep/agents.py``: Defines the Wolf, Sheep, and GrassPatch agent classes. 30 | * ``wolf_sheep/scheduler.py``: Defines a custom variant on the RandomActivationByType scheduler, where we can define filters for the `get_type_count` function. 31 | * ``wolf_sheep/model.py``: Defines the Wolf-Sheep Predation model itself 32 | * ``wolf_sheep/server.py``: Sets up the interactive visualization server 33 | * ``run.py``: Launches a model visualization server. 34 | 35 | ## Further Reading 36 | 37 | This model is closely based on the NetLogo Wolf-Sheep Predation Model: 38 | 39 | Wilensky, U. (1997). NetLogo Wolf Sheep Predation model. http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation. Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. 40 | 41 | See also the [Lotka–Volterra equations 42 | ](https://en.wikipedia.org/wiki/Lotka%E2%80%93Volterra_equations) for an example of a classic differential-equation model with similar dynamics. 43 | -------------------------------------------------------------------------------- /mesa/examples/advanced/wolf_sheep/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/advanced/wolf_sheep/__init__.py -------------------------------------------------------------------------------- /mesa/examples/advanced/wolf_sheep/app.py: -------------------------------------------------------------------------------- 1 | from mesa.examples.advanced.wolf_sheep.agents import GrassPatch, Sheep, Wolf 2 | from mesa.examples.advanced.wolf_sheep.model import WolfSheep 3 | from mesa.experimental.devs import ABMSimulator 4 | from mesa.visualization import ( 5 | CommandConsole, 6 | Slider, 7 | SolaraViz, 8 | make_plot_component, 9 | make_space_component, 10 | ) 11 | 12 | 13 | def wolf_sheep_portrayal(agent): 14 | if agent is None: 15 | return 16 | 17 | portrayal = { 18 | "size": 25, 19 | } 20 | 21 | if isinstance(agent, Wolf): 22 | portrayal["color"] = "tab:red" 23 | portrayal["marker"] = "o" 24 | portrayal["zorder"] = 2 25 | elif isinstance(agent, Sheep): 26 | portrayal["color"] = "tab:cyan" 27 | portrayal["marker"] = "o" 28 | portrayal["zorder"] = 2 29 | elif isinstance(agent, GrassPatch): 30 | if agent.fully_grown: 31 | portrayal["color"] = "tab:green" 32 | else: 33 | portrayal["color"] = "tab:brown" 34 | portrayal["marker"] = "s" 35 | portrayal["size"] = 75 36 | 37 | return portrayal 38 | 39 | 40 | model_params = { 41 | "seed": { 42 | "type": "InputText", 43 | "value": 42, 44 | "label": "Random Seed", 45 | }, 46 | "grass": { 47 | "type": "Select", 48 | "value": True, 49 | "values": [True, False], 50 | "label": "grass regrowth enabled?", 51 | }, 52 | "grass_regrowth_time": Slider("Grass Regrowth Time", 20, 1, 50), 53 | "initial_sheep": Slider("Initial Sheep Population", 100, 10, 300), 54 | "sheep_reproduce": Slider("Sheep Reproduction Rate", 0.04, 0.01, 1.0, 0.01), 55 | "initial_wolves": Slider("Initial Wolf Population", 10, 5, 100), 56 | "wolf_reproduce": Slider( 57 | "Wolf Reproduction Rate", 58 | 0.05, 59 | 0.01, 60 | 1.0, 61 | 0.01, 62 | ), 63 | "wolf_gain_from_food": Slider("Wolf Gain From Food Rate", 20, 1, 50), 64 | "sheep_gain_from_food": Slider("Sheep Gain From Food", 4, 1, 10), 65 | } 66 | 67 | 68 | def post_process_space(ax): 69 | ax.set_aspect("equal") 70 | ax.set_xticks([]) 71 | ax.set_yticks([]) 72 | 73 | 74 | def post_process_lines(ax): 75 | ax.legend(loc="center left", bbox_to_anchor=(1, 0.9)) 76 | 77 | 78 | space_component = make_space_component( 79 | wolf_sheep_portrayal, draw_grid=False, post_process=post_process_space 80 | ) 81 | lineplot_component = make_plot_component( 82 | {"Wolves": "tab:orange", "Sheep": "tab:cyan", "Grass": "tab:green"}, 83 | post_process=post_process_lines, 84 | ) 85 | 86 | simulator = ABMSimulator() 87 | model = WolfSheep(simulator=simulator, grass=True) 88 | 89 | page = SolaraViz( 90 | model, 91 | components=[space_component, lineplot_component, CommandConsole], 92 | model_params=model_params, 93 | name="Wolf Sheep", 94 | simulator=simulator, 95 | ) 96 | page # noqa 97 | -------------------------------------------------------------------------------- /mesa/examples/basic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/basic/__init__.py -------------------------------------------------------------------------------- /mesa/examples/basic/boid_flockers/Readme.md: -------------------------------------------------------------------------------- 1 | # Boids Flockers 2 | 3 | ## Summary 4 | 5 | An implementation of Craig Reynolds's Boids flocker model. Agents (simulated birds) try to fly towards the average position of their neighbors and in the same direction as them, while maintaining a minimum distance. This produces flocking behavior. 6 | 7 | This model tests Mesa's continuous space feature, and uses numpy arrays to represent vectors. 8 | 9 | ## How to Run 10 | 11 | To run the model interactively, in this directory, run the following command 12 | 13 | ``` 14 | $ solara run app.py 15 | ``` 16 | 17 | 18 | ## Files 19 | 20 | * [model.py](model.py): Ccntains the Boid Model 21 | * [agents.py](agents.py): Contains the Boid agent 22 | * [app.py](app.py): Solara based Visualization code. 23 | 24 | ## Further Reading 25 | 26 | The following link can be visited for more information on the boid flockers model: 27 | https://cs.stanford.edu/people/eroberts/courses/soco/projects/2008-09/modeling-natural-systems/boids.html 28 | -------------------------------------------------------------------------------- /mesa/examples/basic/boid_flockers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/basic/boid_flockers/__init__.py -------------------------------------------------------------------------------- /mesa/examples/basic/boid_flockers/agents.py: -------------------------------------------------------------------------------- 1 | """A Boid (bird-oid) agent for implementing Craig Reynolds's Boids flocking model. 2 | 3 | This implementation uses numpy arrays to represent vectors for efficient computation 4 | of flocking behavior. 5 | """ 6 | 7 | import numpy as np 8 | 9 | from mesa.experimental.continuous_space import ContinuousSpaceAgent 10 | 11 | 12 | class Boid(ContinuousSpaceAgent): 13 | """A Boid-style flocker agent. 14 | 15 | The agent follows three behaviors to flock: 16 | - Cohesion: steering towards neighboring agents 17 | - Separation: avoiding getting too close to any other agent 18 | - Alignment: trying to fly in the same direction as neighbors 19 | 20 | Boids have a vision that defines the radius in which they look for their 21 | neighbors to flock with. Their speed (a scalar) and direction (a vector) 22 | define their movement. Separation is their desired minimum distance from 23 | any other Boid. 24 | """ 25 | 26 | def __init__( 27 | self, 28 | model, 29 | space, 30 | position=(0, 0), 31 | speed=1, 32 | direction=(1, 1), 33 | vision=1, 34 | separation=1, 35 | cohere=0.03, 36 | separate=0.015, 37 | match=0.05, 38 | ): 39 | """Create a new Boid flocker agent. 40 | 41 | Args: 42 | model: Model instance the agent belongs to 43 | speed: Distance to move per step 44 | direction: numpy vector for the Boid's direction of movement 45 | vision: Radius to look around for nearby Boids 46 | separation: Minimum distance to maintain from other Boids 47 | cohere: Relative importance of matching neighbors' positions (default: 0.03) 48 | separate: Relative importance of avoiding close neighbors (default: 0.015) 49 | match: Relative importance of matching neighbors' directions (default: 0.05) 50 | """ 51 | super().__init__(space, model) 52 | self.position = position 53 | self.speed = speed 54 | self.direction = direction 55 | self.vision = vision 56 | self.separation = separation 57 | self.cohere_factor = cohere 58 | self.separate_factor = separate 59 | self.match_factor = match 60 | self.neighbors = [] 61 | self.angle = 0.0 # represents the angle at which the boid is moving 62 | 63 | def step(self): 64 | """Get the Boid's neighbors, compute the new vector, and move accordingly.""" 65 | neighbors, distances = self.get_neighbors_in_radius(radius=self.vision) 66 | self.neighbors = [n for n in neighbors if n is not self] 67 | 68 | # If no neighbors, maintain current direction 69 | if not neighbors: 70 | self.position += self.direction * self.speed 71 | return 72 | 73 | delta = self.space.calculate_difference_vector(self.position, agents=neighbors) 74 | 75 | cohere_vector = delta.sum(axis=0) * self.cohere_factor 76 | separation_vector = ( 77 | -1 * delta[distances < self.separation].sum(axis=0) * self.separate_factor 78 | ) 79 | match_vector = ( 80 | np.asarray([n.direction for n in neighbors]).sum(axis=0) * self.match_factor 81 | ) 82 | 83 | # Update direction based on the three behaviors 84 | self.direction += (cohere_vector + separation_vector + match_vector) / len( 85 | neighbors 86 | ) 87 | 88 | # Normalize direction vector 89 | self.direction /= np.linalg.norm(self.direction) 90 | 91 | # Move boid 92 | self.position += self.direction * self.speed 93 | -------------------------------------------------------------------------------- /mesa/examples/basic/boid_flockers/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from matplotlib.markers import MarkerStyle 5 | 6 | sys.path.insert(0, os.path.abspath("../../../..")) 7 | 8 | from mesa.examples.basic.boid_flockers.model import BoidFlockers 9 | from mesa.visualization import Slider, SolaraViz, make_space_component 10 | 11 | # Pre-compute markers for different angles (e.g., every 10 degrees) 12 | MARKER_CACHE = {} 13 | for angle in range(0, 360, 10): 14 | marker = MarkerStyle(10) 15 | marker._transform = marker.get_transform().rotate_deg(angle) 16 | MARKER_CACHE[angle] = marker 17 | 18 | 19 | def boid_draw(agent): 20 | neighbors = len(agent.neighbors) 21 | 22 | # Calculate the angle 23 | deg = agent.angle 24 | # Round to nearest 10 degrees 25 | rounded_deg = round(deg / 10) * 10 % 360 26 | 27 | # using cached markers to speed things up 28 | if neighbors <= 1: 29 | return {"color": "red", "size": 20, "marker": MARKER_CACHE[rounded_deg]} 30 | elif neighbors >= 2: 31 | return {"color": "green", "size": 20, "marker": MARKER_CACHE[rounded_deg]} 32 | 33 | 34 | model_params = { 35 | "seed": { 36 | "type": "InputText", 37 | "value": 42, 38 | "label": "Random Seed", 39 | }, 40 | "population_size": Slider( 41 | label="Number of boids", 42 | value=100, 43 | min=10, 44 | max=200, 45 | step=10, 46 | ), 47 | "width": 100, 48 | "height": 100, 49 | "speed": Slider( 50 | label="Speed of Boids", 51 | value=5, 52 | min=1, 53 | max=20, 54 | step=1, 55 | ), 56 | "vision": Slider( 57 | label="Vision of Bird (radius)", 58 | value=10, 59 | min=1, 60 | max=50, 61 | step=1, 62 | ), 63 | "separation": Slider( 64 | label="Minimum Separation", 65 | value=2, 66 | min=1, 67 | max=20, 68 | step=1, 69 | ), 70 | } 71 | 72 | model = BoidFlockers() 73 | 74 | page = SolaraViz( 75 | model, 76 | components=[make_space_component(agent_portrayal=boid_draw, backend="matplotlib")], 77 | model_params=model_params, 78 | name="Boid Flocking Model", 79 | ) 80 | page # noqa 81 | -------------------------------------------------------------------------------- /mesa/examples/basic/boid_flockers/model.py: -------------------------------------------------------------------------------- 1 | """ 2 | Boids Flocking Model 3 | =================== 4 | A Mesa implementation of Craig Reynolds's Boids flocker model. 5 | Uses numpy arrays to represent vectors. 6 | """ 7 | 8 | import os 9 | import sys 10 | 11 | sys.path.insert(0, os.path.abspath("../../../..")) 12 | 13 | 14 | import numpy as np 15 | 16 | from mesa import Model 17 | from mesa.examples.basic.boid_flockers.agents import Boid 18 | from mesa.experimental.continuous_space import ContinuousSpace 19 | 20 | 21 | class BoidFlockers(Model): 22 | """Flocker model class. Handles agent creation, placement and scheduling.""" 23 | 24 | def __init__( 25 | self, 26 | population_size=100, 27 | width=100, 28 | height=100, 29 | speed=1, 30 | vision=10, 31 | separation=2, 32 | cohere=0.03, 33 | separate=0.015, 34 | match=0.05, 35 | seed=None, 36 | ): 37 | """Create a new Boids Flocking model. 38 | 39 | Args: 40 | population_size: Number of Boids in the simulation (default: 100) 41 | width: Width of the space (default: 100) 42 | height: Height of the space (default: 100) 43 | speed: How fast the Boids move (default: 1) 44 | vision: How far each Boid can see (default: 10) 45 | separation: Minimum distance between Boids (default: 2) 46 | cohere: Weight of cohesion behavior (default: 0.03) 47 | separate: Weight of separation behavior (default: 0.015) 48 | match: Weight of alignment behavior (default: 0.05) 49 | seed: Random seed for reproducibility (default: None) 50 | """ 51 | super().__init__(seed=seed) 52 | self.agent_angles = np.zeros( 53 | population_size 54 | ) # holds the angle representing the direction of all agents at a given step 55 | 56 | # Set up the space 57 | self.space = ContinuousSpace( 58 | [[0, width], [0, height]], 59 | torus=True, 60 | random=self.random, 61 | n_agents=population_size, 62 | ) 63 | 64 | # Create and place the Boid agents 65 | positions = self.rng.random(size=(population_size, 2)) * self.space.size 66 | directions = self.rng.uniform(-1, 1, size=(population_size, 2)) 67 | Boid.create_agents( 68 | self, 69 | population_size, 70 | self.space, 71 | position=positions, 72 | direction=directions, 73 | cohere=cohere, 74 | separate=separate, 75 | match=match, 76 | speed=speed, 77 | vision=vision, 78 | separation=separation, 79 | ) 80 | 81 | # For tracking statistics 82 | self.average_heading = None 83 | self.update_average_heading() 84 | 85 | # vectorizing the calculation of angles for all agents 86 | def calculate_angles(self): 87 | d1 = np.array([agent.direction[0] for agent in self.agents]) 88 | d2 = np.array([agent.direction[1] for agent in self.agents]) 89 | self.agent_angles = np.degrees(np.arctan2(d1, d2)) 90 | for agent, angle in zip(self.agents, self.agent_angles): 91 | agent.angle = angle 92 | 93 | def update_average_heading(self): 94 | """Calculate the average heading (direction) of all Boids.""" 95 | if not self.agents: 96 | self.average_heading = 0 97 | return 98 | 99 | headings = np.array([agent.direction for agent in self.agents]) 100 | mean_heading = np.mean(headings, axis=0) 101 | self.average_heading = np.arctan2(mean_heading[1], mean_heading[0]) 102 | 103 | def step(self): 104 | """Run one step of the model. 105 | 106 | All agents are activated in random order using the AgentSet shuffle_do method. 107 | """ 108 | self.agents.shuffle_do("step") 109 | self.update_average_heading() 110 | self.calculate_angles() 111 | -------------------------------------------------------------------------------- /mesa/examples/basic/boltzmann_wealth_model/Readme.md: -------------------------------------------------------------------------------- 1 | # Boltzmann Wealth Model (Tutorial) 2 | 3 | ## Summary 4 | 5 | A simple model of agents exchanging wealth. All agents start with the same amount of money. Every step, each agent with one unit of money or more gives one unit of wealth to another random agent. Mesa's [Getting Started](https://mesa.readthedocs.io/latest/getting_started.html) section walks through the Boltzmann Wealth Model in a series of short introductory tutorials, starting with[Creating your First Model](https://mesa.readthedocs.io/latest/tutorials/0_first_model.html). 6 | 7 | As the model runs, the distribution of wealth among agents goes from being perfectly uniform (all agents have the same starting wealth), to highly skewed -- a small number have high wealth, more have none at all. 8 | 9 | ## How to Run 10 | 11 | To run the model interactively, in this directory, run the following command 12 | 13 | ``` 14 | $ solara run app.py 15 | ``` 16 | 17 | 18 | ## Files 19 | 20 | * ``model.py``: Final version of the model. 21 | * ``agents.py``: Final version of the agent. 22 | * ``app.py``: Code for the interactive visualization. 23 | 24 | ## Optional 25 | 26 | An optional visualization is also provided using Streamlit, which is another popular Python library for creating interactive web applications. 27 | 28 | To run the Streamlit app, you will need to install the `streamlit` and `altair` libraries: 29 | 30 | ``` 31 | $ pip install streamlit altair 32 | ``` 33 | 34 | Then, you can run the Streamlit app using the following command: 35 | 36 | ``` 37 | $ streamlit run st_app.py 38 | ``` 39 | 40 | ## Further Reading 41 | 42 | This model is drawn from econophysics and presents a statistical mechanics approach to wealth distribution. Some examples of further reading on the topic can be found at: 43 | 44 | [Milakovic, M. A Statistical Equilibrium Model of Wealth Distribution. February, 2001.](https://editorialexpress.com/cgi-bin/conference/download.cgi?db_name=SCE2001&paper_id=214) 45 | 46 | [Dragulescu, A and Yakovenko, V. Statistical Mechanics of Money, Income, and Wealth: A Short Survey. November, 2002](http://arxiv.org/pdf/cond-mat/0211175v1.pdf) 47 | -------------------------------------------------------------------------------- /mesa/examples/basic/boltzmann_wealth_model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/basic/boltzmann_wealth_model/__init__.py -------------------------------------------------------------------------------- /mesa/examples/basic/boltzmann_wealth_model/agents.py: -------------------------------------------------------------------------------- 1 | from mesa.discrete_space import CellAgent 2 | 3 | 4 | class MoneyAgent(CellAgent): 5 | """An agent with fixed initial wealth. 6 | 7 | Each agent starts with 1 unit of wealth and can give 1 unit to other agents 8 | if they occupy the same cell. 9 | 10 | Attributes: 11 | wealth (int): The agent's current wealth (starts at 1) 12 | """ 13 | 14 | def __init__(self, model, cell): 15 | """Create a new agent. 16 | 17 | Args: 18 | model (Model): The model instance that contains the agent 19 | """ 20 | super().__init__(model) 21 | self.cell = cell 22 | self.wealth = 1 23 | 24 | def move(self): 25 | """Move the agent to a random neighboring cell.""" 26 | self.cell = self.cell.neighborhood.select_random_cell() 27 | 28 | def give_money(self): 29 | """Give 1 unit of wealth to a random agent in the same cell.""" 30 | cellmates = [a for a in self.cell.agents if a is not self] 31 | 32 | if cellmates: # Only give money if there are other agents present 33 | other = self.random.choice(cellmates) 34 | other.wealth += 1 35 | self.wealth -= 1 36 | 37 | def step(self): 38 | """Execute one step for the agent: 39 | 1. Move to a neighboring cell 40 | 2. If wealth > 0, maybe give money to another agent in the same cell 41 | """ 42 | self.move() 43 | if self.wealth > 0: 44 | self.give_money() 45 | -------------------------------------------------------------------------------- /mesa/examples/basic/boltzmann_wealth_model/app.py: -------------------------------------------------------------------------------- 1 | from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealth 2 | from mesa.mesa_logging import INFO, log_to_stderr 3 | from mesa.visualization import ( 4 | SolaraViz, 5 | make_plot_component, 6 | make_space_component, 7 | ) 8 | 9 | log_to_stderr(INFO) 10 | 11 | 12 | def agent_portrayal(agent): 13 | color = agent.wealth # we are using a colormap to translate wealth to color 14 | return {"color": color} 15 | 16 | 17 | model_params = { 18 | "seed": { 19 | "type": "InputText", 20 | "value": 42, 21 | "label": "Random Seed", 22 | }, 23 | "n": { 24 | "type": "SliderInt", 25 | "value": 50, 26 | "label": "Number of agents:", 27 | "min": 10, 28 | "max": 100, 29 | "step": 1, 30 | }, 31 | "width": 10, 32 | "height": 10, 33 | } 34 | 35 | 36 | def post_process(ax): 37 | ax.get_figure().colorbar(ax.collections[0], label="wealth", ax=ax) 38 | 39 | 40 | # Create initial model instance 41 | model = BoltzmannWealth(50, 10, 10) 42 | 43 | # Create visualization elements. The visualization elements are solara components 44 | # that receive the model instance as a "prop" and display it in a certain way. 45 | # Under the hood these are just classes that receive the model instance. 46 | # You can also author your own visualization elements, which can also be functions 47 | # that receive the model instance and return a valid solara component. 48 | 49 | SpaceGraph = make_space_component( 50 | agent_portrayal, cmap="viridis", vmin=0, vmax=10, post_process=post_process 51 | ) 52 | GiniPlot = make_plot_component("Gini") 53 | 54 | # Create the SolaraViz page. This will automatically create a server and display the 55 | # visualization elements in a web browser. 56 | # Display it using the following command in the example directory: 57 | # solara run app.py 58 | # It will automatically update and display any changes made to this file 59 | page = SolaraViz( 60 | model, 61 | components=[SpaceGraph, GiniPlot], 62 | model_params=model_params, 63 | name="Boltzmann Wealth Model", 64 | ) 65 | page # noqa 66 | 67 | 68 | # In a notebook environment, we can also display the visualization elements directly 69 | # SpaceGraph(model1) 70 | # GiniPlot(model1) 71 | 72 | # The plots will be static. If you want to pick up model steps, 73 | # you have to make the model reactive first 74 | # reactive_model = solara.reactive(model1) 75 | # SpaceGraph(reactive_model) 76 | # In a different notebook block: 77 | # reactive_model.value.step() 78 | -------------------------------------------------------------------------------- /mesa/examples/basic/boltzmann_wealth_model/model.py: -------------------------------------------------------------------------------- 1 | """ 2 | Boltzmann Wealth Model 3 | ===================== 4 | 5 | A simple model of wealth distribution based on the Boltzmann-Gibbs distribution. 6 | Agents move randomly on a grid, giving one unit of wealth to a random neighbor 7 | when they occupy the same cell. 8 | """ 9 | 10 | from mesa import Model 11 | from mesa.datacollection import DataCollector 12 | from mesa.discrete_space import OrthogonalMooreGrid 13 | from mesa.examples.basic.boltzmann_wealth_model.agents import MoneyAgent 14 | 15 | 16 | class BoltzmannWealth(Model): 17 | """A simple model of an economy where agents exchange currency at random. 18 | 19 | All agents begin with one unit of currency, and each time step agents can give 20 | a unit of currency to another agent in the same cell. Over time, this produces 21 | a highly skewed distribution of wealth. 22 | 23 | Attributes: 24 | num_agents (int): Number of agents in the model 25 | grid (MultiGrid): The space in which agents move 26 | running (bool): Whether the model should continue running 27 | datacollector (DataCollector): Collects and stores model data 28 | """ 29 | 30 | def __init__(self, n=100, width=10, height=10, seed=None): 31 | """Initialize the model. 32 | 33 | Args: 34 | n (int, optional): Number of agents. Defaults to 100. 35 | width (int, optional): Grid width. Defaults to 10. 36 | height (int, optional): Grid height. Defaults to 10. 37 | seed (int, optional): Random seed. Defaults to None. 38 | """ 39 | super().__init__(seed=seed) 40 | 41 | self.num_agents = n 42 | self.grid = OrthogonalMooreGrid((width, height), random=self.random) 43 | 44 | # Set up data collection 45 | self.datacollector = DataCollector( 46 | model_reporters={"Gini": self.compute_gini}, 47 | agent_reporters={"Wealth": "wealth"}, 48 | ) 49 | MoneyAgent.create_agents( 50 | self, 51 | self.num_agents, 52 | self.random.choices(self.grid.all_cells.cells, k=self.num_agents), 53 | ) 54 | 55 | self.running = True 56 | self.datacollector.collect(self) 57 | 58 | def step(self): 59 | self.agents.shuffle_do("step") # Activate all agents in random order 60 | self.datacollector.collect(self) # Collect data 61 | 62 | def compute_gini(self): 63 | """Calculate the Gini coefficient for the model's current wealth distribution. 64 | 65 | The Gini coefficient is a measure of inequality in distributions. 66 | - A Gini of 0 represents complete equality, where all agents have equal wealth. 67 | - A Gini of 1 represents maximal inequality, where one agent has all wealth. 68 | """ 69 | agent_wealths = [agent.wealth for agent in self.agents] 70 | x = sorted(agent_wealths) 71 | n = self.num_agents 72 | # Calculate using the standard formula for Gini coefficient 73 | b = sum(xi * (n - i) for i, xi in enumerate(x)) / (n * sum(x)) 74 | return 1 + (1 / n) - 2 * b 75 | -------------------------------------------------------------------------------- /mesa/examples/basic/boltzmann_wealth_model/st_app.py: -------------------------------------------------------------------------------- 1 | # Run with streamlit run st_app.py 2 | 3 | import time 4 | 5 | import altair as alt 6 | import pandas as pd 7 | import streamlit as st 8 | from model import BoltzmannWealth 9 | 10 | model = st.title("Boltzman Wealth Model") 11 | num_agents = st.slider( 12 | "Choose how many agents to include in the model", 13 | min_value=1, 14 | max_value=100, 15 | value=50, 16 | ) 17 | num_ticks = st.slider( 18 | "Select number of Simulation Runs", min_value=1, max_value=100, value=50 19 | ) 20 | height = st.slider("Select Grid Height", min_value=10, max_value=100, step=10, value=15) 21 | width = st.slider("Select Grid Width", min_value=10, max_value=100, step=10, value=20) 22 | model = BoltzmannWealth(num_agents, height, width) 23 | 24 | 25 | status_text = st.empty() 26 | run = st.button("Run Simulation") 27 | 28 | 29 | if run: 30 | tick = time.time() 31 | step = 0 32 | # init grid 33 | df_grid = pd.DataFrame() 34 | df_gini = pd.DataFrame({"step": [0], "gini": [-1]}) 35 | for x in range(width): 36 | for y in range(height): 37 | df_grid = pd.concat( 38 | [df_grid, pd.DataFrame({"x": [x], "y": [y], "agent_count": 0})], 39 | ignore_index=True, 40 | ) 41 | 42 | heatmap = ( 43 | alt.Chart(df_grid) 44 | .mark_point(size=100) 45 | .encode(x="x", y="y", color=alt.Color("agent_count")) 46 | .interactive() 47 | .properties(width=800, height=600) 48 | ) 49 | 50 | line = ( 51 | alt.Chart(df_gini) 52 | .mark_line(point=True) 53 | .encode(x="step", y="gini") 54 | .properties(width=800, height=600) 55 | ) 56 | 57 | # init progress bar 58 | my_bar = st.progress(0, text="Simulation Progress") # progress 59 | placeholder = st.empty() 60 | st.subheader("Agent Grid") 61 | chart = st.altair_chart(heatmap) 62 | st.subheader("Gini Values") 63 | line_chart = st.altair_chart(line) 64 | 65 | color_scale = alt.Scale( 66 | domain=[0, 1, 2, 3, 4], range=["red", "cyan", "white", "white", "blue"] 67 | ) 68 | for i in range(num_ticks): 69 | model.step() 70 | my_bar.progress((i / num_ticks), text="Simulation progress") 71 | placeholder.text(f"Step = {i}") 72 | for cell in model.grid.coord_iter(): 73 | cell_content, (x, y) = cell 74 | agent_count = len(cell_content) 75 | selected_row = df_grid[(df_grid["x"] == x) & (df_grid["y"] == y)] 76 | df_grid.loc[selected_row.index, "agent_count"] = ( 77 | agent_count # random.choice([1,2]) 78 | ) 79 | 80 | df_gini = pd.concat( 81 | [ 82 | df_gini, 83 | pd.DataFrame( 84 | {"step": [i], "gini": [model.datacollector.model_vars["Gini"][i]]} 85 | ), 86 | ] 87 | ) 88 | # st.table(df_grid) 89 | heatmap = ( 90 | alt.Chart(df_grid) 91 | .mark_circle(size=100) 92 | .encode(x="x", y="y", color=alt.Color("agent_count", scale=color_scale)) 93 | .interactive() 94 | .properties(width=800, height=600) 95 | ) 96 | chart.altair_chart(heatmap) 97 | 98 | line = ( 99 | alt.Chart(df_gini) 100 | .mark_line(point=True) 101 | .encode(x="step", y="gini") 102 | .properties(width=800, height=600) 103 | ) 104 | line_chart.altair_chart(line) 105 | 106 | time.sleep(0.01) 107 | 108 | tock = time.time() 109 | st.success(f"Simulation completed in {tock - tick:.2f} secs") 110 | 111 | # st.subheader('Agent Grid') 112 | # fig = px.imshow(agent_counts,labels={'color':'Agent Count'}) 113 | # st.plotly_chart(fig) 114 | # st.subheader('Gini value over sim ticks (Plotly)') 115 | # chart = st.line_chart(model.datacollector.model_vars['Gini']) 116 | -------------------------------------------------------------------------------- /mesa/examples/basic/conways_game_of_life/Readme.md: -------------------------------------------------------------------------------- 1 | # Conway's Game Of "Life" 2 | 3 | ## Summary 4 | 5 | [The Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life), also known simply as "Life", is a cellular automaton devised by the British mathematician John Horton Conway in 1970. 6 | 7 | The "game" is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input by a human. One interacts with the Game of "Life" by creating an initial configuration and observing how it evolves, or, for advanced "players", by creating patterns with particular properties. 8 | 9 | 10 | ## How to Run 11 | 12 | To run the model interactively, in this directory, run the following command 13 | 14 | ``` 15 | $ solara run app.py 16 | ``` 17 | 18 | ## Files 19 | 20 | * ``agents.py``: Defines the behavior of an individual cell, which can be in two states: DEAD or ALIVE. 21 | * ``model.py``: Defines the model itself, initialized with a random configuration of alive and dead cells. 22 | * ``app.py``: Defines an interactive visualization using solara. 23 | * ``st_app.py``: Defines an interactive visualization using Streamlit. 24 | 25 | ## Optional 26 | 27 | * For the streamlit version, you need to have streamlit installed (can be done via pip install streamlit) 28 | 29 | 30 | ## Further Reading 31 | [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) 32 | -------------------------------------------------------------------------------- /mesa/examples/basic/conways_game_of_life/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/basic/conways_game_of_life/__init__.py -------------------------------------------------------------------------------- /mesa/examples/basic/conways_game_of_life/agents.py: -------------------------------------------------------------------------------- 1 | from mesa.discrete_space import FixedAgent 2 | 3 | 4 | class Cell(FixedAgent): 5 | """Represents a single ALIVE or DEAD cell in the simulation.""" 6 | 7 | DEAD = 0 8 | ALIVE = 1 9 | 10 | @property 11 | def x(self): 12 | return self.cell.coordinate[0] 13 | 14 | @property 15 | def y(self): 16 | return self.cell.coordinate[1] 17 | 18 | def __init__(self, model, cell, init_state=DEAD): 19 | """Create a cell, in the given state, at the given x, y position.""" 20 | super().__init__(model) 21 | self.cell = cell 22 | self.state = init_state 23 | self._next_state = None 24 | 25 | @property 26 | def is_alive(self): 27 | return self.state == self.ALIVE 28 | 29 | @property 30 | def neighbors(self): 31 | return self.cell.neighborhood.agents 32 | 33 | def determine_state(self): 34 | """Compute if the cell will be dead or alive at the next tick. This is 35 | based on the number of alive or dead neighbors. The state is not 36 | changed here, but is just computed and stored in self._nextState, 37 | because our current state may still be necessary for our neighbors 38 | to calculate their next state. 39 | """ 40 | # Get the neighbors and apply the rules on whether to be alive or dead 41 | # at the next tick. 42 | live_neighbors = sum(neighbor.is_alive for neighbor in self.neighbors) 43 | 44 | # Assume nextState is unchanged, unless changed below. 45 | self._next_state = self.state 46 | if self.is_alive: 47 | if live_neighbors < 2 or live_neighbors > 3: 48 | self._next_state = self.DEAD 49 | else: 50 | if live_neighbors == 3: 51 | self._next_state = self.ALIVE 52 | 53 | def assume_state(self): 54 | """Set the state to the new computed state -- computed in step().""" 55 | self.state = self._next_state 56 | -------------------------------------------------------------------------------- /mesa/examples/basic/conways_game_of_life/app.py: -------------------------------------------------------------------------------- 1 | from mesa.examples.basic.conways_game_of_life.model import ConwaysGameOfLife 2 | from mesa.visualization import ( 3 | SolaraViz, 4 | make_space_component, 5 | ) 6 | 7 | 8 | def agent_portrayal(agent): 9 | return { 10 | "color": "white" if agent.state == 0 else "black", 11 | "marker": "s", 12 | "size": 25, 13 | } 14 | 15 | 16 | def post_process(ax): 17 | ax.set_aspect("equal") 18 | ax.set_xticks([]) 19 | ax.set_yticks([]) 20 | 21 | 22 | model_params = { 23 | "seed": { 24 | "type": "InputText", 25 | "value": 42, 26 | "label": "Random Seed", 27 | }, 28 | "width": { 29 | "type": "SliderInt", 30 | "value": 50, 31 | "label": "Width", 32 | "min": 5, 33 | "max": 60, 34 | "step": 1, 35 | }, 36 | "height": { 37 | "type": "SliderInt", 38 | "value": 50, 39 | "label": "Height", 40 | "min": 5, 41 | "max": 60, 42 | "step": 1, 43 | }, 44 | "initial_fraction_alive": { 45 | "type": "SliderFloat", 46 | "value": 0.2, 47 | "label": "Cells initially alive", 48 | "min": 0, 49 | "max": 1, 50 | "step": 0.01, 51 | }, 52 | } 53 | 54 | # Create initial model instance 55 | model1 = ConwaysGameOfLife() 56 | 57 | # Create visualization elements. The visualization elements are solara components 58 | # that receive the model instance as a "prop" and display it in a certain way. 59 | # Under the hood these are just classes that receive the model instance. 60 | # You can also author your own visualization elements, which can also be functions 61 | # that receive the model instance and return a valid solara component. 62 | SpaceGraph = make_space_component( 63 | agent_portrayal, post_process=post_process, draw_grid=False 64 | ) 65 | 66 | 67 | # Create the SolaraViz page. This will automatically create a server and display the 68 | # visualization elements in a web browser. 69 | # Display it using the following command in the example directory: 70 | # solara run app.py 71 | # It will automatically update and display any changes made to this file 72 | page = SolaraViz( 73 | model1, 74 | components=[SpaceGraph], 75 | model_params=model_params, 76 | name="Game of Life", 77 | ) 78 | page # noqa 79 | -------------------------------------------------------------------------------- /mesa/examples/basic/conways_game_of_life/model.py: -------------------------------------------------------------------------------- 1 | from mesa import Model 2 | from mesa.discrete_space import OrthogonalMooreGrid 3 | from mesa.examples.basic.conways_game_of_life.agents import Cell 4 | 5 | 6 | class ConwaysGameOfLife(Model): 7 | """Represents the 2-dimensional array of cells in Conway's Game of Life.""" 8 | 9 | def __init__(self, width=50, height=50, initial_fraction_alive=0.2, seed=None): 10 | """Create a new playing area of (width, height) cells.""" 11 | super().__init__(seed=seed) 12 | # Use a simple grid, where edges wrap around. 13 | self.grid = OrthogonalMooreGrid((width, height), capacity=1, torus=True) 14 | 15 | # Place a cell at each location, with some initialized to 16 | # ALIVE and some to DEAD. 17 | for cell in self.grid.all_cells: 18 | Cell( 19 | self, 20 | cell, 21 | init_state=Cell.ALIVE 22 | if self.random.random() < initial_fraction_alive 23 | else Cell.DEAD, 24 | ) 25 | 26 | self.running = True 27 | 28 | def step(self): 29 | """Perform the model step in two stages: 30 | - First, all cells assume their next state (whether they will be dead or alive) 31 | - Then, all cells change state to their next state. 32 | """ 33 | self.agents.do("determine_state") 34 | self.agents.do("assume_state") 35 | -------------------------------------------------------------------------------- /mesa/examples/basic/conways_game_of_life/st_app.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import altair as alt 4 | import numpy as np 5 | import pandas as pd 6 | import streamlit as st 7 | 8 | from mesa.examples.basic.conways_game_of_life.model import ConwaysGameOfLife 9 | 10 | model = st.title("Conway's Game of Life") 11 | num_ticks = st.slider("Select number of Steps", min_value=1, max_value=100, value=50) 12 | height = st.slider("Select Grid Height", min_value=10, max_value=100, step=10, value=15) 13 | width = st.slider("Select Grid Width", min_value=10, max_value=100, step=10, value=20) 14 | model = ConwaysGameOfLife(height, width) 15 | 16 | col1, col2, col3 = st.columns(3) 17 | status_text = st.empty() 18 | # step_mode = st.checkbox('Run Step-by-Step') 19 | run = st.button("Run Simulation") 20 | 21 | 22 | if run: 23 | tick = time.time() 24 | step = 0 25 | # init grid 26 | df_grid = pd.DataFrame() 27 | agent_counts = np.zeros((model.grid.width, model.grid.height)) 28 | for x in range(width): 29 | for y in range(height): 30 | df_grid = pd.concat( 31 | [df_grid, pd.DataFrame({"x": [x], "y": [y], "state": [0]})], 32 | ignore_index=True, 33 | ) 34 | 35 | heatmap = ( 36 | alt.Chart(df_grid) 37 | .mark_point(size=100) 38 | .encode(x="x", y="y", color=alt.Color("state")) 39 | .interactive() 40 | .properties(width=800, height=600) 41 | ) 42 | 43 | # init progress bar 44 | my_bar = st.progress(0, text="Simulation Progress") # progress 45 | placeholder = st.empty() 46 | st.subheader("Agent Grid") 47 | chart = st.altair_chart(heatmap, use_container_width=True) 48 | color_scale = alt.Scale(domain=[0, 1], range=["red", "yellow"]) 49 | for i in range(num_ticks): 50 | model.step() 51 | my_bar.progress((i / num_ticks), text="Simulation progress") 52 | placeholder.text(f"Step = {i}") 53 | for contents, (x, y) in model.grid.coord_iter(): 54 | # print(f"x: {x}, y: {y}, state: {contents}") 55 | selected_row = df_grid[(df_grid["x"] == x) & (df_grid["y"] == y)] 56 | df_grid.loc[selected_row.index, "state"] = ( 57 | contents.state 58 | ) # random.choice([1,2]) 59 | 60 | heatmap = ( 61 | alt.Chart(df_grid) 62 | .mark_circle(size=100) 63 | .encode(x="x", y="y", color=alt.Color("state", scale=color_scale)) 64 | .interactive() 65 | .properties(width=800, height=600) 66 | ) 67 | chart.altair_chart(heatmap) 68 | 69 | time.sleep(0.1) 70 | 71 | tock = time.time() 72 | st.success(f"Simulation completed in {tock - tick:.2f} secs") 73 | -------------------------------------------------------------------------------- /mesa/examples/basic/schelling/Readme.md: -------------------------------------------------------------------------------- 1 | # Schelling Segregation Model 2 | 3 | ## Summary 4 | 5 | The Schelling segregation model is a classic agent-based model, demonstrating how even a mild preference for similar neighbors can lead to a much higher degree of segregation than we would intuitively expect. The model consists of agents on a square grid, where each grid cell can contain at most one agent. Agents come in two colors: orange and blue. They are happy if a certain number of their eight possible neighbors are of the same color, and unhappy otherwise. Unhappy agents will pick a random empty cell to move to each step, until they are happy. The model keeps running until there are no unhappy agents. 6 | 7 | By default, the number of similar neighbors the agents need to be happy is set to 3. That means the agents would be perfectly happy with a majority of their neighbors being of a different color (e.g. a Blue agent would be happy with five Orange neighbors and three Blue ones). Despite this, the model consistently leads to a high degree of segregation, with most agents ending up with no neighbors of a different color. 8 | 9 | ## How to Run 10 | 11 | To run the model interactively, in this directory, run the following command 12 | 13 | ``` 14 | $ solara run app.py 15 | ``` 16 | 17 | ## Files 18 | 19 | * ``model.py``: Contains the Schelling model class 20 | * ``agents.py``: Contains the Schelling agent class 21 | * ``app.py``: Code for the interactive visualization. 22 | * ``analysis.ipynb``: Notebook demonstrating how to run experiments and parameter sweeps on the model. 23 | 24 | ## Further Reading 25 | 26 | Schelling's original paper describing the model: 27 | 28 | [Schelling, Thomas C. Dynamic Models of Segregation. Journal of Mathematical Sociology. 1971, Vol. 1, pp 143-186.](https://www.stat.berkeley.edu/~aldous/157/Papers/Schelling_Seg_Models.pdf) 29 | 30 | An interactive, browser-based explanation and implementation: 31 | 32 | [Parable of the Polygons](http://ncase.me/polygons/), by Vi Hart and Nicky Case. 33 | -------------------------------------------------------------------------------- /mesa/examples/basic/schelling/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/basic/schelling/__init__.py -------------------------------------------------------------------------------- /mesa/examples/basic/schelling/agents.py: -------------------------------------------------------------------------------- 1 | from mesa.discrete_space import CellAgent 2 | 3 | 4 | class SchellingAgent(CellAgent): 5 | """Schelling segregation agent.""" 6 | 7 | def __init__( 8 | self, model, cell, agent_type: int, homophily: float = 0.4, radius: int = 1 9 | ) -> None: 10 | """Create a new Schelling agent. 11 | Args: 12 | model: The model instance the agent belongs to 13 | agent_type: Indicator for the agent's type (minority=1, majority=0) 14 | homophily: Minimum number of similar neighbors needed for happiness 15 | radius: Search radius for checking neighbor similarity 16 | """ 17 | super().__init__(model) 18 | self.cell = cell 19 | self.type = agent_type 20 | self.homophily = homophily 21 | self.radius = radius 22 | self.happy = False 23 | 24 | def assign_state(self) -> None: 25 | """Determine if agent is happy and move if necessary.""" 26 | neighbors = list(self.cell.get_neighborhood(radius=self.radius).agents) 27 | 28 | # Count similar neighbors 29 | similar_neighbors = len([n for n in neighbors if n.type == self.type]) 30 | 31 | # Calculate the fraction of similar neighbors 32 | if (valid_neighbors := len(neighbors)) > 0: 33 | similarity_fraction = similar_neighbors / valid_neighbors 34 | else: 35 | # If there are no neighbors, the similarity fraction is 0 36 | similarity_fraction = 0.0 37 | 38 | if similarity_fraction < self.homophily: 39 | self.happy = False 40 | else: 41 | self.happy = True 42 | self.model.happy += 1 43 | 44 | def step(self) -> None: 45 | # Move if unhappy 46 | if not self.happy: 47 | self.cell = self.model.grid.select_random_empty_cell() 48 | -------------------------------------------------------------------------------- /mesa/examples/basic/schelling/app.py: -------------------------------------------------------------------------------- 1 | import solara 2 | 3 | from mesa.examples.basic.schelling.model import Schelling 4 | from mesa.visualization import ( 5 | Slider, 6 | SolaraViz, 7 | make_plot_component, 8 | make_space_component, 9 | ) 10 | from mesa.visualization.components import AgentPortrayalStyle 11 | 12 | 13 | def get_happy_agents(model): 14 | """Display a text count of how many happy agents there are.""" 15 | return solara.Markdown(f"**Happy agents: {model.happy}**") 16 | 17 | 18 | def agent_portrayal(agent): 19 | style = AgentPortrayalStyle( 20 | x=agent.cell.coordinate[0], 21 | y=agent.cell.coordinate[1], 22 | marker="./resources/orange_happy.png", 23 | size=75, 24 | ) 25 | if agent.type == 0: 26 | if agent.happy: 27 | style.update( 28 | ("marker", "./resources/blue_happy.png"), 29 | ) 30 | else: 31 | style.update( 32 | ("marker", "./resources/blue_unhappy.png"), 33 | ("size", 50), 34 | ("zorder", 2), 35 | ) 36 | else: 37 | if not agent.happy: 38 | style.update( 39 | ("marker", "./resources/orange_unhappy.png"), 40 | ("size", 50), 41 | ("zorder", 2), 42 | ) 43 | 44 | return style 45 | 46 | 47 | model_params = { 48 | "seed": { 49 | "type": "InputText", 50 | "value": 42, 51 | "label": "Random Seed", 52 | }, 53 | "density": Slider("Agent density", 0.8, 0.1, 1.0, 0.1), 54 | "minority_pc": Slider("Fraction minority", 0.2, 0.0, 1.0, 0.05), 55 | "homophily": Slider("Homophily", 0.4, 0.0, 1.0, 0.125), 56 | "width": 20, 57 | "height": 20, 58 | } 59 | 60 | model1 = Schelling() 61 | 62 | HappyPlot = make_plot_component({"happy": "tab:green"}) 63 | 64 | page = SolaraViz( 65 | model1, 66 | components=[ 67 | make_space_component(agent_portrayal), 68 | HappyPlot, 69 | get_happy_agents, 70 | ], 71 | model_params=model_params, 72 | ) 73 | page # noqa 74 | -------------------------------------------------------------------------------- /mesa/examples/basic/schelling/model.py: -------------------------------------------------------------------------------- 1 | from mesa import Model 2 | from mesa.datacollection import DataCollector 3 | from mesa.discrete_space import OrthogonalMooreGrid 4 | from mesa.examples.basic.schelling.agents import SchellingAgent 5 | 6 | 7 | class Schelling(Model): 8 | """Model class for the Schelling segregation model.""" 9 | 10 | def __init__( 11 | self, 12 | height: int = 20, 13 | width: int = 20, 14 | density: float = 0.8, 15 | minority_pc: float = 0.5, 16 | homophily: float = 0.4, 17 | radius: int = 1, 18 | seed=None, 19 | ): 20 | """Create a new Schelling model. 21 | 22 | Args: 23 | width: Width of the grid 24 | height: Height of the grid 25 | density: Initial chance for a cell to be populated (0-1) 26 | minority_pc: Chance for an agent to be in minority class (0-1) 27 | homophily: Minimum number of similar neighbors needed for happiness 28 | radius: Search radius for checking neighbor similarity 29 | seed: Seed for reproducibility 30 | """ 31 | super().__init__(seed=seed) 32 | 33 | # Model parameters 34 | self.density = density 35 | self.minority_pc = minority_pc 36 | 37 | # Initialize grid 38 | self.grid = OrthogonalMooreGrid((width, height), random=self.random, capacity=1) 39 | 40 | # Track happiness 41 | self.happy = 0 42 | 43 | # Set up data collection 44 | self.datacollector = DataCollector( 45 | model_reporters={ 46 | "happy": "happy", 47 | "pct_happy": lambda m: (m.happy / len(m.agents)) * 100 48 | if len(m.agents) > 0 49 | else 0, 50 | "population": lambda m: len(m.agents), 51 | "minority_pct": lambda m: ( 52 | sum(1 for agent in m.agents if agent.type == 1) 53 | / len(m.agents) 54 | * 100 55 | if len(m.agents) > 0 56 | else 0 57 | ), 58 | }, 59 | agent_reporters={"agent_type": "type"}, 60 | ) 61 | 62 | # Create agents and place them on the grid 63 | for cell in self.grid.all_cells: 64 | if self.random.random() < self.density: 65 | agent_type = 1 if self.random.random() < minority_pc else 0 66 | SchellingAgent( 67 | self, cell, agent_type, homophily=homophily, radius=radius 68 | ) 69 | 70 | # Collect initial state 71 | self.agents.do("assign_state") 72 | self.datacollector.collect(self) 73 | 74 | def step(self): 75 | """Run one step of the model.""" 76 | self.happy = 0 # Reset counter of happy agents 77 | self.agents.shuffle_do("step") # Activate all agents in random order 78 | self.agents.do("assign_state") 79 | self.datacollector.collect(self) # Collect data 80 | self.running = self.happy < len(self.agents) # Continue until everyone is happy 81 | -------------------------------------------------------------------------------- /mesa/examples/basic/schelling/resources/blue_happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/basic/schelling/resources/blue_happy.png -------------------------------------------------------------------------------- /mesa/examples/basic/schelling/resources/blue_unhappy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/basic/schelling/resources/blue_unhappy.png -------------------------------------------------------------------------------- /mesa/examples/basic/schelling/resources/orange_happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/basic/schelling/resources/orange_happy.png -------------------------------------------------------------------------------- /mesa/examples/basic/schelling/resources/orange_unhappy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/basic/schelling/resources/orange_unhappy.png -------------------------------------------------------------------------------- /mesa/examples/basic/virus_on_network/Readme.md: -------------------------------------------------------------------------------- 1 | # Virus on a Network 2 | 3 | ## Summary 4 | 5 | This model is based on the NetLogo model "Virus on Network". It demonstrates the spread of a virus through a network and follows the SIR model, commonly seen in epidemiology. 6 | 7 | The SIR model is one of the simplest compartmental models, and many models are derivatives of this basic form. The model consists of three compartments: 8 | 9 | S: The number of susceptible individuals. When a susceptible and an infectious individual come into "infectious contact", the susceptible individual contracts the disease and transitions to the infectious compartment. 10 | I: The number of infectious individuals. These are individuals who have been infected and are capable of infecting susceptible individuals. 11 | R for the number of removed (and immune) or deceased individuals. These are individuals who have been infected and have either recovered from the disease and entered the removed compartment, or died. It is assumed that the number of deaths is negligible with respect to the total population. This compartment may also be called "recovered" or "resistant". 12 | 13 | For more information about this model, read the NetLogo's web page: http://ccl.northwestern.edu/netlogo/models/VirusonaNetwork. 14 | 15 | JavaScript library used in this example to render the network: [d3.js](https://d3js.org/). 16 | 17 | ## Installation 18 | 19 | To install the dependencies use pip and the requirements.txt in this directory. e.g. 20 | 21 | ``` 22 | $ pip install -r requirements.txt 23 | ``` 24 | 25 | ## How to Run 26 | 27 | To run the model interactively, in this directory, run the following command 28 | 29 | ``` 30 | $ solara run app.py 31 | ``` 32 | 33 | ## Files 34 | 35 | * ``model.py``: Contains the agent class, and the overall model class. 36 | * ``agents.py``: Contains the agent class. 37 | * ``app.py``: Contains the code for the interactive Solara visualization. 38 | 39 | ## Further Reading 40 | 41 | [Stonedahl, F. and Wilensky, U. (2008). NetLogo Virus on a Network model](http://ccl.northwestern.edu/netlogo/models/VirusonaNetwork). 42 | Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. 43 | 44 | 45 | [Wilensky, U. (1999). NetLogo](http://ccl.northwestern.edu/netlogo/) 46 | Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. 47 | -------------------------------------------------------------------------------- /mesa/examples/basic/virus_on_network/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectmesa/mesa/759673a981bf2af5e11ededbf81c918448d86005/mesa/examples/basic/virus_on_network/__init__.py -------------------------------------------------------------------------------- /mesa/examples/basic/virus_on_network/agents.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from mesa.discrete_space import FixedAgent 4 | 5 | 6 | class State(Enum): 7 | SUSCEPTIBLE = 0 8 | INFECTED = 1 9 | RESISTANT = 2 10 | 11 | 12 | class VirusAgent(FixedAgent): 13 | """Individual Agent definition and its properties/interaction methods.""" 14 | 15 | def __init__( 16 | self, 17 | model, 18 | initial_state, 19 | virus_spread_chance, 20 | virus_check_frequency, 21 | recovery_chance, 22 | gain_resistance_chance, 23 | cell, 24 | ): 25 | super().__init__(model) 26 | 27 | self.state = initial_state 28 | 29 | self.virus_spread_chance = virus_spread_chance 30 | self.virus_check_frequency = virus_check_frequency 31 | self.recovery_chance = recovery_chance 32 | self.gain_resistance_chance = gain_resistance_chance 33 | self.cell = cell 34 | 35 | def try_to_infect_neighbors(self): 36 | for agent in self.cell.neighborhood.agents: 37 | if (agent.state is State.SUSCEPTIBLE) and ( 38 | self.random.random() < self.virus_spread_chance 39 | ): 40 | agent.state = State.INFECTED 41 | 42 | def try_gain_resistance(self): 43 | if self.random.random() < self.gain_resistance_chance: 44 | self.state = State.RESISTANT 45 | 46 | def try_remove_infection(self): 47 | # Try to remove 48 | if self.random.random() < self.recovery_chance: 49 | # Success 50 | self.state = State.SUSCEPTIBLE 51 | self.try_gain_resistance() 52 | else: 53 | # Failed 54 | self.state = State.INFECTED 55 | 56 | def check_situation(self): 57 | if (self.state is State.INFECTED) and ( 58 | self.random.random() < self.virus_check_frequency 59 | ): 60 | self.try_remove_infection() 61 | 62 | def step(self): 63 | if self.state is State.INFECTED: 64 | self.try_to_infect_neighbors() 65 | self.check_situation() 66 | -------------------------------------------------------------------------------- /mesa/examples/basic/virus_on_network/app.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import solara 4 | 5 | from mesa.examples.basic.virus_on_network.model import ( 6 | State, 7 | VirusOnNetwork, 8 | number_infected, 9 | ) 10 | from mesa.visualization import ( 11 | Slider, 12 | SolaraViz, 13 | make_plot_component, 14 | make_space_component, 15 | ) 16 | 17 | 18 | def agent_portrayal(agent): 19 | node_color_dict = { 20 | State.INFECTED: "tab:red", 21 | State.SUSCEPTIBLE: "tab:green", 22 | State.RESISTANT: "tab:gray", 23 | } 24 | return {"color": node_color_dict[agent.state], "size": 10} 25 | 26 | 27 | def get_resistant_susceptible_ratio(model): 28 | ratio = model.resistant_susceptible_ratio() 29 | ratio_text = r"$\infty$" if ratio is math.inf else f"{ratio:.2f}" 30 | infected_text = str(number_infected(model)) 31 | 32 | return solara.Markdown( 33 | f"Resistant/Susceptible Ratio: {ratio_text}
Infected Remaining: {infected_text}" 34 | ) 35 | 36 | 37 | model_params = { 38 | "seed": { 39 | "type": "InputText", 40 | "value": 42, 41 | "label": "Random Seed", 42 | }, 43 | "num_nodes": Slider( 44 | label="Number of agents", 45 | value=10, 46 | min=10, 47 | max=100, 48 | step=1, 49 | ), 50 | "avg_node_degree": Slider( 51 | label="Avg Node Degree", 52 | value=3, 53 | min=3, 54 | max=8, 55 | step=1, 56 | ), 57 | "initial_outbreak_size": Slider( 58 | label="Initial Outbreak Size", 59 | value=1, 60 | min=1, 61 | max=10, 62 | step=1, 63 | ), 64 | "virus_spread_chance": Slider( 65 | label="Virus Spread Chance", 66 | value=0.4, 67 | min=0.0, 68 | max=1.0, 69 | step=0.1, 70 | ), 71 | "virus_check_frequency": Slider( 72 | label="Virus Check Frequency", 73 | value=0.4, 74 | min=0.0, 75 | max=1.0, 76 | step=0.1, 77 | ), 78 | "recovery_chance": Slider( 79 | label="Recovery Chance", 80 | value=0.3, 81 | min=0.0, 82 | max=1.0, 83 | step=0.1, 84 | ), 85 | "gain_resistance_chance": Slider( 86 | label="Gain Resistance Chance", 87 | value=0.5, 88 | min=0.0, 89 | max=1.0, 90 | step=0.1, 91 | ), 92 | } 93 | 94 | 95 | def post_process_lineplot(ax): 96 | ax.set_ylim(ymin=0) 97 | ax.set_ylabel("# people") 98 | ax.legend(bbox_to_anchor=(1.05, 1.0), loc="upper left") 99 | 100 | 101 | SpacePlot = make_space_component(agent_portrayal) 102 | StatePlot = make_plot_component( 103 | {"Infected": "tab:red", "Susceptible": "tab:green", "Resistant": "tab:gray"}, 104 | post_process=post_process_lineplot, 105 | ) 106 | 107 | model1 = VirusOnNetwork() 108 | 109 | page = SolaraViz( 110 | model1, 111 | components=[ 112 | SpacePlot, 113 | StatePlot, 114 | get_resistant_susceptible_ratio, 115 | ], 116 | model_params=model_params, 117 | name="Virus Model", 118 | ) 119 | page # noqa 120 | -------------------------------------------------------------------------------- /mesa/examples/basic/virus_on_network/model.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import networkx as nx 4 | 5 | import mesa 6 | from mesa import Model 7 | from mesa.discrete_space import CellCollection, Network 8 | from mesa.examples.basic.virus_on_network.agents import State, VirusAgent 9 | 10 | 11 | def number_state(model, state): 12 | return sum(1 for a in model.grid.all_cells.agents if a.state is state) 13 | 14 | 15 | def number_infected(model): 16 | return number_state(model, State.INFECTED) 17 | 18 | 19 | def number_susceptible(model): 20 | return number_state(model, State.SUSCEPTIBLE) 21 | 22 | 23 | def number_resistant(model): 24 | return number_state(model, State.RESISTANT) 25 | 26 | 27 | class VirusOnNetwork(Model): 28 | """A virus model with some number of agents.""" 29 | 30 | def __init__( 31 | self, 32 | num_nodes=10, 33 | avg_node_degree=3, 34 | initial_outbreak_size=1, 35 | virus_spread_chance=0.4, 36 | virus_check_frequency=0.4, 37 | recovery_chance=0.3, 38 | gain_resistance_chance=0.5, 39 | seed=None, 40 | ): 41 | super().__init__(seed=seed) 42 | prob = avg_node_degree / num_nodes 43 | graph = nx.erdos_renyi_graph(n=num_nodes, p=prob) 44 | self.grid = Network(graph, capacity=1, random=self.random) 45 | 46 | self.initial_outbreak_size = ( 47 | initial_outbreak_size if initial_outbreak_size <= num_nodes else num_nodes 48 | ) 49 | 50 | self.datacollector = mesa.DataCollector( 51 | { 52 | "Infected": number_infected, 53 | "Susceptible": number_susceptible, 54 | "Resistant": number_resistant, 55 | "R over S": self.resistant_susceptible_ratio, 56 | } 57 | ) 58 | 59 | VirusAgent.create_agents( 60 | self, 61 | num_nodes, 62 | State.SUSCEPTIBLE, 63 | virus_spread_chance, 64 | virus_check_frequency, 65 | recovery_chance, 66 | gain_resistance_chance, 67 | list(self.grid.all_cells), 68 | ) 69 | 70 | # Infect some nodes 71 | infected_nodes = CellCollection( 72 | self.random.sample(list(self.grid.all_cells), self.initial_outbreak_size), 73 | random=self.random, 74 | ) 75 | for a in infected_nodes.agents: 76 | a.state = State.INFECTED 77 | 78 | self.running = True 79 | self.datacollector.collect(self) 80 | 81 | def resistant_susceptible_ratio(self): 82 | try: 83 | return number_state(self, State.RESISTANT) / number_state( 84 | self, State.SUSCEPTIBLE 85 | ) 86 | except ZeroDivisionError: 87 | return math.inf 88 | 89 | def step(self): 90 | self.agents.shuffle_do("step") 91 | # collect data 92 | self.datacollector.collect(self) 93 | -------------------------------------------------------------------------------- /mesa/experimental/__init__.py: -------------------------------------------------------------------------------- 1 | """Experimental features package for Mesa. 2 | 3 | This package contains modules that are under active development and testing. These 4 | features are provided to allow early access and feedback from the Mesa community, but 5 | their APIs may change between releases without following semantic versioning. 6 | 7 | Current experimental modules: 8 | cell_space: Alternative API for discrete spaces with cell-centric functionality 9 | devs: Discrete event simulation system for scheduling events at arbitrary times 10 | mesa_signals: Reactive programming capabilities for tracking state changes 11 | 12 | Notes: 13 | - Features in this package may be changed or removed without notice 14 | - APIs are not guaranteed to be stable between releases 15 | - Features graduate from experimental status once their APIs are stabilized 16 | """ 17 | 18 | from mesa.experimental import continuous_space, devs, mesa_signals, meta_agents 19 | 20 | __all__ = ["continuous_space", "devs", "mesa_signals", "meta_agents"] 21 | -------------------------------------------------------------------------------- /mesa/experimental/cell_space/__init__.py: -------------------------------------------------------------------------------- 1 | """Cell spaces for active, property-rich spatial modeling in Mesa. 2 | 3 | Cell spaces extend Mesa's spatial modeling capabilities by making the space itself active - 4 | each position (cell) can have properties and behaviors rather than just containing agents. 5 | This enables more sophisticated environmental modeling and agent-environment interactions. 6 | 7 | Key components: 8 | - Cells: Active positions that can have properties and contain agents 9 | - CellAgents: Agents that understand how to interact with cells 10 | - Spaces: Different cell organization patterns (grids, networks, etc.) 11 | - PropertyLayers: Efficient property storage and manipulation 12 | 13 | This is particularly useful for models where the environment plays an active role, 14 | like resource growth, pollution diffusion, or infrastructure networks. The cell 15 | space system is experimental and under active development. 16 | """ 17 | 18 | import warnings 19 | 20 | from mesa.discrete_space.cell import Cell 21 | from mesa.discrete_space.cell_agent import ( 22 | CellAgent, 23 | FixedAgent, 24 | Grid2DMovingAgent, 25 | ) 26 | from mesa.discrete_space.cell_collection import CellCollection 27 | from mesa.discrete_space.discrete_space import DiscreteSpace 28 | from mesa.discrete_space.grid import ( 29 | Grid, 30 | HexGrid, 31 | OrthogonalMooreGrid, 32 | OrthogonalVonNeumannGrid, 33 | ) 34 | from mesa.discrete_space.network import Network 35 | from mesa.discrete_space.property_layer import PropertyLayer 36 | from mesa.discrete_space.voronoi import VoronoiGrid 37 | 38 | __all__ = [ 39 | "Cell", 40 | "CellAgent", 41 | "CellCollection", 42 | "DiscreteSpace", 43 | "FixedAgent", 44 | "Grid", 45 | "Grid2DMovingAgent", 46 | "HexGrid", 47 | "Network", 48 | "OrthogonalMooreGrid", 49 | "OrthogonalVonNeumannGrid", 50 | "PropertyLayer", 51 | "VoronoiGrid", 52 | ] 53 | 54 | 55 | warnings.warn( 56 | "you are importing from mesa.experimental.cell_space, " 57 | "all cell spaces have been moved to mesa.discrete_space", 58 | DeprecationWarning, 59 | stacklevel=2, 60 | ) 61 | -------------------------------------------------------------------------------- /mesa/experimental/continuous_space/__init__.py: -------------------------------------------------------------------------------- 1 | """Continuous space support.""" 2 | 3 | from mesa.experimental.continuous_space.continuous_space import ContinuousSpace 4 | from mesa.experimental.continuous_space.continuous_space_agents import ( 5 | ContinuousSpaceAgent, 6 | ) 7 | 8 | __all__ = ["ContinuousSpace", "ContinuousSpaceAgent"] 9 | -------------------------------------------------------------------------------- /mesa/experimental/continuous_space/continuous_space_agents.py: -------------------------------------------------------------------------------- 1 | """Continuous space agents.""" 2 | 3 | from __future__ import annotations 4 | 5 | from itertools import compress 6 | from typing import Protocol 7 | 8 | import numpy as np 9 | 10 | from mesa.agent import Agent 11 | from mesa.experimental.continuous_space import ContinuousSpace 12 | 13 | 14 | class HasPositionProtocol(Protocol): 15 | """Protocol for continuous space position holders.""" 16 | 17 | position: np.ndarray 18 | 19 | 20 | class ContinuousSpaceAgent(Agent): 21 | """A continuous space agent. 22 | 23 | Attributes: 24 | space (ContinuousSpace): the continuous space in which the agent is located 25 | position (np.ndarray): the position of the agent 26 | 27 | """ 28 | 29 | __slots__ = ["_mesa_index", "space"] 30 | 31 | @property 32 | def position(self) -> np.ndarray: 33 | """Position of the agent.""" 34 | return self.space.agent_positions[self.space._agent_to_index[self]] 35 | 36 | @position.setter 37 | def position(self, value: np.ndarray) -> None: 38 | if not self.space.in_bounds(value): 39 | if self.space.torus: 40 | value = self.space.torus_correct(value) 41 | else: 42 | raise ValueError(f"point {value} is outside the bounds of the space") 43 | 44 | self.space.agent_positions[self.space._agent_to_index[self]] = value 45 | 46 | @property 47 | def pos(self): # noqa: D102 48 | # just here for compatibility with solara_viz. 49 | return self.position 50 | 51 | @pos.setter 52 | def pos(self, value): 53 | # just here for compatibility solara_viz. 54 | pass 55 | 56 | def __init__(self, space: ContinuousSpace, model): 57 | """Initialize a continuous space agent. 58 | 59 | Args: 60 | space: the continuous space in which the agent is located 61 | model: the model to which the agent belongs 62 | 63 | """ 64 | super().__init__(model) 65 | self.space: ContinuousSpace = space 66 | self.space._add_agent(self) 67 | # self.position[:] = np.nan 68 | 69 | def remove(self) -> None: 70 | """Remove and delete the agent from the model and continuous space.""" 71 | super().remove() 72 | self.space._remove_agent(self) 73 | self._mesa_index = None 74 | self.space = None 75 | 76 | def get_neighbors_in_radius( 77 | self, radius: float | int = 1 78 | ) -> tuple[list, np.ndarray]: 79 | """Get neighbors within radius. 80 | 81 | Args: 82 | radius: radius within which to look for neighbors 83 | 84 | """ 85 | agents, dists = self.space.get_agents_in_radius(self.position, radius=radius) 86 | logical = np.asarray([agent is not self for agent in agents]) 87 | agents = list(compress(agents, logical)) 88 | return agents, dists[logical] 89 | 90 | def get_nearest_neighbors(self, k: int = 1) -> tuple[list, np.ndarray]: 91 | """Get neighbors within radius. 92 | 93 | Args: 94 | k: the number of nearest neighbors to return 95 | 96 | """ 97 | # return includes self, so we need to get k+1 98 | agents, dists = self.space.get_k_nearest_agents(self.position, k=k + 1) 99 | logical = np.asarray([agent is not self for agent in agents]) 100 | agents = list(compress(agents, logical)) 101 | return agents, dists[logical] 102 | -------------------------------------------------------------------------------- /mesa/experimental/devs/__init__.py: -------------------------------------------------------------------------------- 1 | """Core event management functionality for Mesa's discrete event simulation system. 2 | 3 | This module provides the foundational data structures and classes needed for event-based 4 | simulation in Mesa. The EventList class is a priority queue implementation that maintains 5 | simulation events in chronological order while respecting event priorities. Key features: 6 | 7 | - Priority-based event ordering 8 | - Weak references to prevent memory leaks from canceled events 9 | - Efficient event insertion and removal using a heap queue 10 | - Support for event cancellation without breaking the heap structure 11 | 12 | The module contains three main components: 13 | - Priority: An enumeration defining event priority levels (HIGH, DEFAULT, LOW) 14 | - SimulationEvent: A class representing individual events with timing and execution details 15 | - EventList: A heap-based priority queue managing the chronological ordering of events 16 | 17 | The implementation supports both pure discrete event simulation and hybrid approaches 18 | combining agent-based modeling with event scheduling. 19 | """ 20 | 21 | from .eventlist import Priority, SimulationEvent 22 | from .simulator import ABMSimulator, DEVSimulator 23 | 24 | __all__ = ["ABMSimulator", "DEVSimulator", "Priority", "SimulationEvent"] 25 | -------------------------------------------------------------------------------- /mesa/experimental/mesa_signals/__init__.py: -------------------------------------------------------------------------------- 1 | """Mesa Signals (Observables) package that provides reactive programming capabilities. 2 | 3 | This package enables tracking changes to properties and state in Mesa models through a 4 | reactive programming paradigm. It enables building models where components can observe 5 | and react to changes in other components' state. 6 | 7 | The package provides the core Observable classes and utilities needed to implement 8 | reactive patterns in agent-based models. This includes capabilities for watching changes 9 | to attributes, computing derived values, and managing collections that emit signals 10 | when modified. 11 | """ 12 | 13 | from .mesa_signal import All, Computable, Computed, HasObservables, Observable 14 | from .observable_collections import ObservableList 15 | 16 | __all__ = [ 17 | "All", 18 | "Computable", 19 | "Computed", 20 | "HasObservables", 21 | "Observable", 22 | "ObservableList", 23 | ] 24 | -------------------------------------------------------------------------------- /mesa/experimental/mesa_signals/observable_collections.py: -------------------------------------------------------------------------------- 1 | """Observable collection types that emit signals when modified. 2 | 3 | This module extends Mesa's reactive programming capabilities to collection types like 4 | lists. Observable collections emit signals when items are added, removed, or modified, 5 | allowing other components to react to changes in the collection's contents. 6 | 7 | The module provides: 8 | - ObservableList: A list descriptor that emits signals on modifications 9 | - SignalingList: The underlying list implementation that manages signal emission 10 | 11 | These classes enable building models where components need to track and react to 12 | changes in collections of agents, resources, or other model elements. 13 | """ 14 | 15 | from collections.abc import Iterable, MutableSequence 16 | from typing import Any 17 | 18 | from .mesa_signal import BaseObservable, HasObservables 19 | 20 | __all__ = [ 21 | "ObservableList", 22 | ] 23 | 24 | 25 | class ObservableList(BaseObservable): 26 | """An ObservableList that emits signals on changes to the underlying list.""" 27 | 28 | def __init__(self): 29 | """Initialize the ObservableList.""" 30 | super().__init__() 31 | self.signal_types: set = {"remove", "replace", "change", "insert", "append"} 32 | self.fallback_value = [] 33 | 34 | def __set__(self, instance: "HasObservables", value: Iterable): 35 | """Set the value of the descriptor attribute. 36 | 37 | Args: 38 | instance: The instance on which to set the attribute. 39 | value: The value to set the attribute to. 40 | 41 | """ 42 | super().__set__(instance, value) 43 | setattr( 44 | instance, 45 | self.private_name, 46 | SignalingList(value, instance, self.public_name), 47 | ) 48 | 49 | 50 | class SignalingList(MutableSequence[Any]): 51 | """A basic lists that emits signals on changes.""" 52 | 53 | __slots__ = ["data", "name", "owner"] 54 | 55 | def __init__(self, iterable: Iterable, owner: HasObservables, name: str): 56 | """Initialize a SignalingList. 57 | 58 | Args: 59 | iterable: initial values in the list 60 | owner: the HasObservables instance on which this list is defined 61 | name: the attribute name to which this list is assigned 62 | 63 | """ 64 | self.owner: HasObservables = owner 65 | self.name: str = name 66 | self.data = list(iterable) 67 | 68 | def __setitem__(self, index: int, value: Any) -> None: 69 | """Set item to index. 70 | 71 | Args: 72 | index: the index to set item to 73 | value: the item to set 74 | 75 | """ 76 | old_value = self.data[index] 77 | self.data[index] = value 78 | self.owner.notify(self.name, old_value, value, "replace", index=index) 79 | 80 | def __delitem__(self, index: int) -> None: 81 | """Delete item at index. 82 | 83 | Args: 84 | index: The index of the item to remove 85 | 86 | """ 87 | old_value = self.data 88 | del self.data[index] 89 | self.owner.notify(self.name, old_value, None, "remove", index=index) 90 | 91 | def __getitem__(self, index) -> Any: 92 | """Get item at index. 93 | 94 | Args: 95 | index: The index of the item to retrieve 96 | 97 | Returns: 98 | the item at index 99 | """ 100 | return self.data[index] 101 | 102 | def __len__(self) -> int: 103 | """Return the length of the list.""" 104 | return len(self.data) 105 | 106 | def insert(self, index, value): 107 | """Insert value at index. 108 | 109 | Args: 110 | index: the index to insert value into 111 | value: the value to insert 112 | 113 | """ 114 | self.data.insert(index, value) 115 | self.owner.notify(self.name, None, value, "insert", index=index) 116 | 117 | def append(self, value): 118 | """Insert value at index. 119 | 120 | Args: 121 | index: the index to insert value into 122 | value: the value to insert 123 | 124 | """ 125 | index = len(self.data) 126 | self.data.append(value) 127 | self.owner.notify(self.name, None, value, "append", index=index) 128 | 129 | def __str__(self): 130 | return self.data.__str__() 131 | 132 | def __repr__(self): 133 | return self.data.__repr__() 134 | -------------------------------------------------------------------------------- /mesa/experimental/mesa_signals/signals_util.py: -------------------------------------------------------------------------------- 1 | """Utility functions and classes for Mesa's signals implementation. 2 | 3 | This module provides helper functionality used by Mesa's reactive programming system: 4 | 5 | - AttributeDict: A dictionary subclass that allows attribute-style access to its contents 6 | - create_weakref: Helper function to properly create weak references to different types 7 | 8 | These utilities support the core signals implementation by providing reference 9 | management and convenient data structures used throughout the reactive system. 10 | """ 11 | 12 | import weakref 13 | 14 | __all__ = [ 15 | "AttributeDict", 16 | "create_weakref", 17 | ] 18 | 19 | 20 | class AttributeDict(dict): 21 | """A dict with attribute like access. 22 | 23 | Each value can be accessed as if it were an attribute with its key as attribute name 24 | 25 | """ 26 | 27 | # I want our signals to act like traitlet signals, so this is inspired by trailets Bunch 28 | # and some stack overflow posts. 29 | __setattr__ = dict.__setitem__ 30 | __delattr__ = dict.__delitem__ 31 | 32 | def __getattr__(self, key): # noqa: D105 33 | try: 34 | return self.__getitem__(key) 35 | except KeyError as e: 36 | # we need to go from key error to attribute error 37 | raise AttributeError(key) from e 38 | 39 | def __dir__(self): # noqa: D105 40 | # allows us to easily access all defined attributes 41 | names = dir({}) 42 | names.extend(self.keys()) 43 | return names 44 | 45 | 46 | def create_weakref(item, callback=None): 47 | """Helper function to create a correct weakref for any item.""" 48 | if hasattr(item, "__self__"): 49 | ref = weakref.WeakMethod(item, callback) 50 | else: 51 | ref = weakref.ref(item, callback) 52 | return ref 53 | -------------------------------------------------------------------------------- /mesa/experimental/meta_agents/__init__.py: -------------------------------------------------------------------------------- 1 | """This method is for dynamically creating new agents (meta-agents). 2 | 3 | Meta-agents are defined as agents composed of existing agents. 4 | 5 | Meta-agents are created dynamically with a pointer to the model, name of the meta-agent,, 6 | iterable of agents to belong to the new meta-agents, any new functions for the meta-agent, 7 | any new attributes for the meta-agent, whether to retain sub-agent functions, 8 | whether to retain sub-agent attributes. 9 | 10 | Examples of meta-agents: 11 | - An autonomous car where the subagents are the wheels, sensors, 12 | battery, computer etc. and the meta-agent is the car itself. 13 | - A company where the subagents are employees, departments, buildings, etc. 14 | - A city where the subagents are people, buildings, streets, etc. 15 | 16 | Currently meta-agents are restricted to one parent agent for each subagent/ 17 | one meta-agent per subagent. 18 | 19 | Goal is to assess usage and expand functionality. 20 | 21 | """ 22 | 23 | from .meta_agent import MetaAgent 24 | 25 | __all__ = ["MetaAgent"] 26 | -------------------------------------------------------------------------------- /mesa/visualization/__init__.py: -------------------------------------------------------------------------------- 1 | """Solara based visualization for Mesa models. 2 | 3 | .. note:: 4 | SolaraViz is experimental and still in active development in Mesa 3.x. While we attempt to minimize them, there might be API breaking changes in minor releases. 5 | 6 | """ 7 | 8 | from mesa.visualization.mpl_space_drawing import ( 9 | draw_space, 10 | ) 11 | 12 | from .command_console import CommandConsole 13 | from .components import make_plot_component, make_space_component 14 | from .components.altair_components import make_space_altair 15 | from .solara_viz import JupyterViz, SolaraViz 16 | from .user_param import Slider 17 | 18 | __all__ = [ 19 | "CommandConsole", 20 | "JupyterViz", 21 | "Slider", 22 | "SolaraViz", 23 | "draw_space", 24 | "make_plot_component", 25 | "make_space_altair", 26 | "make_space_component", 27 | ] 28 | -------------------------------------------------------------------------------- /mesa/visualization/components/__init__.py: -------------------------------------------------------------------------------- 1 | """Custom visualization components.""" 2 | 3 | from __future__ import annotations 4 | 5 | from collections.abc import Callable 6 | 7 | from .altair_components import SpaceAltair, make_altair_space 8 | from .matplotlib_components import ( 9 | SpaceMatplotlib, 10 | make_mpl_plot_component, 11 | make_mpl_space_component, 12 | ) 13 | from .portrayal_components import AgentPortrayalStyle, PropertyLayerStyle 14 | 15 | __all__ = [ 16 | "AgentPortrayalStyle", 17 | "PropertyLayerStyle", 18 | "SpaceAltair", 19 | "SpaceMatplotlib", 20 | "make_altair_space", 21 | "make_mpl_plot_component", 22 | "make_mpl_space_component", 23 | "make_plot_component", 24 | "make_space_component", 25 | ] 26 | 27 | 28 | def make_space_component( 29 | agent_portrayal: Callable | None = None, 30 | propertylayer_portrayal: dict | None = None, 31 | post_process: Callable | None = None, 32 | backend: str = "matplotlib", 33 | **space_drawing_kwargs, 34 | ) -> SpaceMatplotlib | SpaceAltair: 35 | """Create a Matplotlib-based space visualization component. 36 | 37 | Args: 38 | agent_portrayal: Function to portray agents. 39 | propertylayer_portrayal: Dictionary of PropertyLayer portrayal specifications 40 | post_process : a callable that will be called with the Axes instance. Allows for fine-tuning plots (e.g., control ticks) 41 | backend: the backend to use {"matplotlib", "altair"} 42 | space_drawing_kwargs : additional keyword arguments to be passed on to the underlying backend specific space drawer function. See 43 | the functions for drawing the various spaces for the appropriate backend further details. 44 | 45 | 46 | Returns: 47 | function: A function that creates a space component 48 | """ 49 | if backend == "matplotlib": 50 | return make_mpl_space_component( 51 | agent_portrayal, 52 | propertylayer_portrayal, 53 | post_process, 54 | **space_drawing_kwargs, 55 | ) 56 | elif backend == "altair": 57 | return make_altair_space( 58 | agent_portrayal, 59 | propertylayer_portrayal, 60 | post_process, 61 | **space_drawing_kwargs, 62 | ) 63 | else: 64 | raise ValueError( 65 | f"unknown backend {backend}, must be one of matplotlib, altair" 66 | ) 67 | 68 | 69 | def make_plot_component( 70 | measure: str | dict[str, str] | list[str] | tuple[str], 71 | post_process: Callable | None = None, 72 | backend: str = "matplotlib", 73 | **plot_drawing_kwargs, 74 | ): 75 | """Create a plotting function for a specified measure using the specified backend. 76 | 77 | Args: 78 | measure (str | dict[str, str] | list[str] | tuple[str]): Measure(s) to plot. 79 | post_process: a user-specified callable to do post-processing called with the Axes instance. 80 | backend: the backend to use {"matplotlib", "altair"} 81 | plot_drawing_kwargs: additional keyword arguments to pass onto the backend specific function for making a plotting component 82 | 83 | Notes: 84 | altair plotting backend is not yet implemented and planned for mesa 3.1. 85 | 86 | Returns: 87 | function: A function that creates a plot component 88 | """ 89 | if backend == "matplotlib": 90 | return make_mpl_plot_component(measure, post_process, **plot_drawing_kwargs) 91 | elif backend == "altair": 92 | raise NotImplementedError("altair line plots are not yet implemented") 93 | else: 94 | raise ValueError( 95 | f"unknown backend {backend}, must be one of matplotlib, altair" 96 | ) 97 | -------------------------------------------------------------------------------- /mesa/visualization/components/portrayal_components.py: -------------------------------------------------------------------------------- 1 | """Portrayal Components Module. 2 | 3 | This module defines data structures for styling visual elements in Mesa agent-based model visualizations. 4 | It provides user-facing classes to specify how agents and property layers should appear in the rendered space. 5 | 6 | Classes: 7 | - AgentPortrayalStyle: Controls the appearance of individual agents (e.g., color, shape, size, etc.). 8 | - PropertyLayerStyle: Controls the appearance of background property layers (e.g., color gradients or uniform fills). 9 | 10 | These components are designed to be passed into Mesa visualizations to customize and standardize how data is presented. 11 | """ 12 | 13 | from dataclasses import dataclass 14 | from typing import Any 15 | 16 | 17 | @dataclass 18 | class AgentPortrayalStyle: 19 | """Represents the visual styling options for an agent in a visualization. 20 | 21 | User facing component to control how agents are drawn. 22 | Allows specifying properties like color, size, 23 | marker shape, position, and other plot attributes. 24 | """ 25 | 26 | x: float | None = None 27 | y: float | None = None 28 | color: str | tuple | None = "tab:blue" 29 | marker: str | None = "o" 30 | size: int | float | None = 50 31 | zorder: int | None = 1 32 | alpha: float | None = 1.0 33 | edgecolors: str | tuple | None = None 34 | linewidths: float | int | None = 1.0 35 | 36 | def update(self, *updates_fields: tuple[str, Any]): 37 | """Updates attributes from variable (field_name, new_value) tuple arguments. 38 | 39 | Example: 40 | >>> def agent_portrayal(agent): 41 | >>> primary_style = AgentPortrayalStyle(color="blue", marker="^", size=10, x=agent.pos[0], y=agent.pos[1]) 42 | >>> if agent.type == 1: 43 | >>> primary_style.update(("color", "red"), ("size", 30)) 44 | >>> return primary_style 45 | """ 46 | for field_to_change, field_to_change_to in updates_fields: 47 | if hasattr(self, field_to_change): 48 | setattr(self, field_to_change, field_to_change_to) 49 | else: 50 | raise AttributeError( 51 | f"'{type(self).__name__}' object has no attribute '{field_to_change}'" 52 | ) 53 | 54 | 55 | @dataclass 56 | class PropertyLayerStyle: 57 | """Represents the visual styling options for a property layer in a visualization. 58 | 59 | User facing component to control how property layers are drawn. 60 | Allows specifying properties like colormap, single color, value limits, 61 | and colorbar visibility. 62 | 63 | Note: You can specify either a 'colormap' (for varying data) or a single 64 | 'color' (for a uniform layer appearance), but not both simultaneously. 65 | """ 66 | 67 | colormap: str | None = None 68 | color: str | None = None 69 | alpha: float = 0.8 70 | colorbar: bool = True 71 | vmin: float | None = None 72 | vmax: float | None = None 73 | 74 | def __post_init__(self): 75 | """Validate that color and colormap are not simultaneously specified.""" 76 | if self.color is not None and self.colormap is not None: 77 | raise ValueError("Specify either 'color' or 'colormap', not both.") 78 | if self.color is None and self.colormap is None: 79 | raise ValueError("Specify one of 'color' or 'colormap'") 80 | -------------------------------------------------------------------------------- /mesa/visualization/user_param.py: -------------------------------------------------------------------------------- 1 | """Solara visualization related helper classes.""" 2 | 3 | 4 | class UserParam: 5 | """UserParam.""" 6 | 7 | _ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'" 8 | 9 | def maybe_raise_error(self, param_type, valid): # noqa: D102 10 | if valid: 11 | return 12 | msg = self._ERROR_MESSAGE.format(param_type, self.label) 13 | raise ValueError(msg) 14 | 15 | 16 | class Slider(UserParam): 17 | """A number-based slider input with settable increment. 18 | 19 | Example: 20 | slider_option = Slider("My Slider", value=123, min=10, max=200, step=0.1) 21 | 22 | Args: 23 | label: The displayed label in the UI 24 | value: The initial value of the slider 25 | min: The minimum possible value of the slider 26 | max: The maximum possible value of the slider 27 | step: The step between min and max for a range of possible values 28 | dtype: either int or float 29 | """ 30 | 31 | def __init__( 32 | self, 33 | label="", 34 | value=None, 35 | min=None, 36 | max=None, 37 | step=1, 38 | dtype=None, 39 | ): 40 | """Initializes a slider. 41 | 42 | Args: 43 | label: The displayed label in the UI 44 | value: The initial value of the slider 45 | min: The minimum possible value of the slider 46 | max: The maximum possible value of the slider 47 | step: The step between min and max for a range of possible values 48 | dtype: either int or float 49 | """ 50 | self.label = label 51 | self.value = value 52 | self.min = min 53 | self.max = max 54 | self.step = step 55 | 56 | # Validate option type to make sure values are supplied properly 57 | valid = not (self.value is None or self.min is None or self.max is None) 58 | self.maybe_raise_error("slider", valid) 59 | 60 | if dtype is None: 61 | self.is_float_slider = self._check_values_are_float(value, min, max, step) 62 | else: 63 | self.is_float_slider = dtype is float 64 | 65 | def _check_values_are_float(self, value, min, max, step): # D103 66 | return any(isinstance(n, float) for n in (value, min, max, step)) 67 | 68 | def get(self, attr): # noqa: D102 69 | return getattr(self, attr) 70 | -------------------------------------------------------------------------------- /mesa/visualization/utils.py: -------------------------------------------------------------------------------- 1 | """Solara related utils.""" 2 | 3 | import solara 4 | 5 | update_counter = solara.reactive(0) 6 | 7 | 8 | def force_update(): # noqa: D103 9 | update_counter.value += 1 10 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | plugins = numpy.typing.mypy_plugin 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """init of tests.""" 2 | -------------------------------------------------------------------------------- /tests/read_requirements.py: -------------------------------------------------------------------------------- 1 | # noqa: D100 2 | import toml 3 | 4 | # This file reads the pyproject.toml and prints out the 5 | # dependencies and dev dependencies. 6 | # It is located in tests/ folder so as not to pollute the root repo. 7 | c = toml.load("pyproject.toml") 8 | print("\n".join(c["project"]["dependencies"])) 9 | print("\n".join(c["project"]["optional-dependencies"]["dev"])) 10 | -------------------------------------------------------------------------------- /tests/test_end_to_end_viz.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd examples/Flockers 4 | python run.py & 5 | PID=$! 6 | sleep 3 7 | curl localhost:8521 | grep Boids 8 | kill $PID 9 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | # noqa: D100 2 | from mesa.examples import ( 3 | BoidFlockers, 4 | BoltzmannWealth, 5 | ConwaysGameOfLife, 6 | EpsteinCivilViolence, 7 | MultiLevelAllianceModel, 8 | PdGrid, 9 | Schelling, 10 | SugarscapeG1mt, 11 | VirusOnNetwork, 12 | WolfSheep, 13 | ) 14 | 15 | 16 | def test_boltzmann_model(): # noqa: D103 17 | from mesa.examples.basic.boltzmann_wealth_model import app 18 | 19 | app.page # noqa: B018 20 | 21 | model = BoltzmannWealth(seed=42) 22 | 23 | for _i in range(10): 24 | model.step() 25 | 26 | 27 | def test_conways_game_model(): # noqa: D103 28 | from mesa.examples.basic.conways_game_of_life import app 29 | 30 | app.page # noqa: B018 31 | 32 | model = ConwaysGameOfLife(seed=42) 33 | for _i in range(10): 34 | model.step() 35 | 36 | 37 | def test_schelling_model(): # noqa: D103 38 | from mesa.examples.basic.schelling import app 39 | 40 | app.page # noqa: B018 41 | 42 | model = Schelling(seed=42) 43 | for _i in range(10): 44 | model.step() 45 | 46 | 47 | def test_virus_on_network(): # noqa: D103 48 | from mesa.examples.basic.virus_on_network import app 49 | 50 | app.page # noqa: B018 51 | 52 | model = VirusOnNetwork(seed=42) 53 | for _i in range(10): 54 | model.step() 55 | 56 | 57 | def test_boid_flockers(): # noqa: D103 58 | from mesa.examples.basic.boid_flockers import app 59 | 60 | app.page # noqa: B018 61 | 62 | model = BoidFlockers(seed=42) 63 | 64 | for _i in range(10): 65 | model.step() 66 | 67 | 68 | def test_epstein(): # noqa: D103 69 | from mesa.examples.advanced.epstein_civil_violence import app 70 | 71 | app.page # noqa: B018 72 | 73 | model = EpsteinCivilViolence(seed=42) 74 | 75 | for _i in range(10): 76 | model.step() 77 | 78 | 79 | def test_pd_grid(): # noqa: D103 80 | from mesa.examples.advanced.pd_grid import app 81 | 82 | app.page # noqa: B018 83 | 84 | model = PdGrid(seed=42) 85 | 86 | for _i in range(10): 87 | model.step() 88 | 89 | 90 | def test_sugarscape_g1mt(): # noqa: D103 91 | from mesa.examples.advanced.sugarscape_g1mt import app 92 | 93 | app.page # noqa: B018 94 | 95 | model = SugarscapeG1mt(seed=42) 96 | 97 | for _i in range(10): 98 | model.step() 99 | 100 | 101 | def test_wolf_sheep(): # noqa: D103 102 | from mesa.examples.advanced.wolf_sheep import app 103 | from mesa.experimental.devs import ABMSimulator 104 | 105 | app.page # noqa: B018 106 | 107 | simulator = ABMSimulator() 108 | WolfSheep(seed=42, simulator=simulator) 109 | simulator.run_for(10) 110 | 111 | 112 | def test_alliance_formation_model(): # noqa: D103 113 | from mesa.examples.advanced.alliance_formation import app 114 | 115 | app.page # noqa: B018 116 | 117 | model = MultiLevelAllianceModel(50, seed=42) 118 | 119 | for _i in range(10): 120 | model.step() 121 | 122 | assert len(model.agents) == len(model.network.nodes) 123 | -------------------------------------------------------------------------------- /tests/test_import_namespace.py: -------------------------------------------------------------------------------- 1 | """Test if namespsaces importing work better.""" 2 | 3 | 4 | def test_import(): 5 | """This tests the new, simpler Mesa namespace. 6 | 7 | See https://github.com/projectmesa/mesa/pull/1294. 8 | """ 9 | import mesa 10 | from mesa.space import MultiGrid 11 | 12 | _ = mesa.space.MultiGrid 13 | _ = MultiGrid 14 | 15 | from mesa.datacollection import DataCollector 16 | 17 | _ = DataCollector 18 | _ = mesa.DataCollector 19 | 20 | from mesa.batchrunner import batch_run 21 | 22 | _ = batch_run 23 | _ = mesa.batch_run 24 | -------------------------------------------------------------------------------- /tests/test_lifespan.py: -------------------------------------------------------------------------------- 1 | """Test removal of agents.""" 2 | 3 | import unittest 4 | 5 | import numpy as np 6 | 7 | from mesa import Agent, Model 8 | from mesa.datacollection import DataCollector 9 | 10 | 11 | class LifeTimeModel(Model): 12 | """Simple model for running models with a finite life.""" 13 | 14 | def __init__(self, agent_lifetime=1, n_agents=10, seed=None): # noqa: D107 15 | super().__init__(seed=seed) 16 | 17 | self.agent_lifetime = agent_lifetime 18 | self.n_agents = n_agents 19 | 20 | # keep track of the the remaining life of an agent and 21 | # how many ticks it has seen 22 | self.datacollector = DataCollector( 23 | agent_reporters={ 24 | "remaining_life": lambda a: a.remaining_life, 25 | "steps": lambda a: a.steps, 26 | } 27 | ) 28 | 29 | self.current_ID = 0 30 | 31 | for _ in range(n_agents): 32 | FiniteLifeAgent(self.agent_lifetime, self) 33 | 34 | self.datacollector.collect(self) 35 | 36 | def step(self): 37 | """Add agents back to n_agents in each step.""" 38 | self.agents.shuffle_do("step") 39 | self.datacollector.collect(self) 40 | 41 | if len(self.agents) < self.n_agents: 42 | for _ in range(self.n_agents - len(self.agents)): 43 | FiniteLifeAgent(self.agent_lifetime, self) 44 | 45 | def run_model(self, step_count=100): # noqa: D102 46 | for _ in range(step_count): 47 | self.step() 48 | 49 | 50 | class FiniteLifeAgent(Agent): 51 | """An agent that is supposed to live for a finite number of ticks. 52 | 53 | Also has a 10% chance of dying in each tick. 54 | """ 55 | 56 | def __init__(self, lifetime, model): # noqa: D107 57 | super().__init__(model) 58 | self.remaining_life = lifetime 59 | self.steps = 0 60 | self.model = model 61 | 62 | def step(self): # noqa: D102 63 | inactivated = self.inactivate() 64 | if not inactivated: 65 | self.steps += 1 # keep track of how many ticks are seen 66 | if np.random.binomial(1, 0.1) != 0: # 10% chance of dying 67 | self.remove() 68 | 69 | def inactivate(self): # noqa: D102 70 | self.remaining_life -= 1 71 | if self.remaining_life < 0: 72 | self.remove() 73 | return True 74 | return False 75 | 76 | 77 | class TestAgentLifespan(unittest.TestCase): # noqa: D101 78 | def setUp(self): # noqa: D102 79 | self.model = LifeTimeModel() 80 | self.model.run_model() 81 | self.df = self.model.datacollector.get_agent_vars_dataframe() 82 | self.df = self.df.reset_index() 83 | 84 | def test_ticks_seen(self): 85 | """Each agent should be activated no more than one time.""" 86 | assert self.df.steps.max() == 1 87 | 88 | def test_agent_lifetime(self): # noqa: D102 89 | lifetimes = self.df.groupby(["AgentID"]).agg({"Step": len}) 90 | assert lifetimes.Step.max() == 2 91 | 92 | 93 | if __name__ == "__main__": 94 | unittest.main() 95 | -------------------------------------------------------------------------------- /tests/test_mesa_logging.py: -------------------------------------------------------------------------------- 1 | """Unit tests for mesa_logging.""" 2 | 3 | import logging 4 | 5 | import pytest 6 | 7 | from mesa import mesa_logging 8 | 9 | 10 | @pytest.fixture 11 | def tear_down(): 12 | """Pytest fixture to ensure all logging is disabled after testing.""" 13 | yield None 14 | mesa_logging._logger = None 15 | ema_logger = logging.getLogger(mesa_logging.LOGGER_NAME) 16 | ema_logger.handlers = [] 17 | 18 | 19 | def test_get_logger(): 20 | """Test get_logger.""" 21 | mesa_logging._rootlogger = None 22 | logger = mesa_logging.get_rootlogger() 23 | assert logger == logging.getLogger(mesa_logging.LOGGER_NAME) 24 | assert len(logger.handlers) == 1 25 | assert isinstance(logger.handlers[0], logging.NullHandler) 26 | 27 | logger = mesa_logging.get_rootlogger() 28 | assert logger, logging.getLogger(mesa_logging.LOGGER_NAME) 29 | assert len(logger.handlers) == 1 30 | assert isinstance(logger.handlers[0], logging.NullHandler) 31 | 32 | 33 | def test_log_to_stderr(): 34 | """Test log_to_stderr.""" 35 | mesa_logging._rootlogger = None 36 | logger = mesa_logging.log_to_stderr(mesa_logging.DEBUG) 37 | assert len(logger.handlers) == 2 38 | assert logger.level == mesa_logging.DEBUG 39 | 40 | mesa_logging._rootlogger = None 41 | logger = mesa_logging.log_to_stderr() 42 | assert len(logger.handlers) == 2 43 | assert logger.level == mesa_logging.DEFAULT_LEVEL 44 | 45 | logger = mesa_logging.log_to_stderr() 46 | assert len(logger.handlers) == 2 47 | assert logger.level == mesa_logging.DEFAULT_LEVEL 48 | -------------------------------------------------------------------------------- /tests/test_model.py: -------------------------------------------------------------------------------- 1 | """Tests for model.py.""" 2 | 3 | import numpy as np 4 | 5 | from mesa.agent import Agent, AgentSet 6 | from mesa.model import Model 7 | 8 | 9 | def test_model_set_up(): 10 | """Test Model initialization.""" 11 | model = Model() 12 | assert model.running is True 13 | assert model.steps == 0 14 | model.step() 15 | assert model.steps == 1 16 | 17 | 18 | def test_running(): 19 | """Test Model is running.""" 20 | 21 | class TestModel(Model): 22 | steps = 0 23 | 24 | def step(self): 25 | """Increase steps until 10.""" 26 | if self.steps == 10: 27 | self.running = False 28 | 29 | model = TestModel() 30 | model.run_model() 31 | assert model.steps == 10 32 | 33 | 34 | def test_seed(seed=23): 35 | """Test initialization of model with specific seed.""" 36 | model = Model(seed=seed) 37 | assert model._seed == seed 38 | model2 = Model(seed=seed + 1) 39 | assert model2._seed == seed + 1 40 | assert model._seed == seed 41 | 42 | assert Model(seed=42).random.random() == Model(seed=42).random.random() 43 | assert np.all( 44 | Model(seed=42).rng.random( 45 | 10, 46 | ) 47 | == Model(seed=42).rng.random( 48 | 10, 49 | ) 50 | ) 51 | 52 | 53 | def test_reset_randomizer(newseed=42): 54 | """Test resetting the random seed on the model.""" 55 | model = Model() 56 | oldseed = model._seed 57 | model.reset_randomizer() 58 | assert model._seed == oldseed 59 | model.reset_randomizer(seed=newseed) 60 | assert model._seed == newseed 61 | 62 | 63 | def test_reset_rng(newseed=42): 64 | """Test resetting the random seed on the model.""" 65 | model = Model(rng=5) 66 | old_rng = model._rng 67 | 68 | model.reset_rng(rng=6) 69 | new_rng = model._rng 70 | 71 | assert old_rng != new_rng 72 | 73 | old_rng = new_rng 74 | model.reset_rng() 75 | new_rng = model.rng.__getstate__() 76 | 77 | assert old_rng != new_rng 78 | 79 | 80 | def test_agent_types(): 81 | """Test Mode.agent_types property.""" 82 | 83 | class TestAgent(Agent): 84 | pass 85 | 86 | model = Model() 87 | test_agent = TestAgent(model) 88 | assert test_agent in model.agents 89 | assert type(test_agent) in model.agent_types 90 | 91 | 92 | def test_agents_by_type(): 93 | """Test getting agents by type from Model.""" 94 | 95 | class Wolf(Agent): 96 | pass 97 | 98 | class Sheep(Agent): 99 | pass 100 | 101 | model = Model() 102 | wolf = Wolf(model) 103 | sheep = Sheep(model) 104 | 105 | assert model.agents_by_type[Wolf] == AgentSet([wolf], model) 106 | assert model.agents_by_type[Sheep] == AgentSet([sheep], model) 107 | assert len(model.agents_by_type) == 2 108 | 109 | 110 | def test_agent_remove(): 111 | """Test removing all agents from the model.""" 112 | 113 | class TestAgent(Agent): 114 | pass 115 | 116 | model = Model() 117 | for _ in range(100): 118 | TestAgent(model) 119 | assert len(model.agents) == 100 120 | 121 | model.remove_all_agents() 122 | assert len(model.agents) == 0 123 | --------------------------------------------------------------------------------