diff --git a/.gitignore b/.gitignore index 98823b67..e545a0a4 100644 --- a/.gitignore +++ b/.gitignore @@ -156,8 +156,10 @@ llm_rules.md .python-version benchmarks/results/* -docs/api/_build/* -docs/api/reference/* +docs/api/_build +docs/api/reference/**/mesa_frames.*.rst examples/**/results/* -docs/general/**/data_* -docs/site/* \ No newline at end of file +docs/general/tutorials/data_csv +docs/general/tutorials/data_parquet +docs/general/tutorials/*.ipynb +docs/site \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..b7595854 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,40 @@ +## Version 0.1.0-alpha โ€” 2024-08-28 + +## What's Changed + +* Refactoring mesa.Agent, mesa.AgentSet, mesa.Model -> AgentSetDF, AgentsDF, ModelDF by @adamamer20 in +* setup: Migrate from setup.py to pyproject.toml by @rht in +* ci: Add pre-commit configuration by @rht in +* Merge requirements.txt into pyproject.toml by @rht in +* ci: Add GA for tests by @rht in +* Changes to AgentSetDF and AgentsDF before time.py -> CopyMixin by @adamamer20 in +* benchmark: Split Polars agent into native and concise by @rht in +* benchmark: Split pandas agent into native and concise by @rht in +* speed up mesa readme_plot script by @adamamer20 in +* Adding DataFrameMixin for improved reusability/encapsulation by @adamamer20 in +* Abstract SpaceDF by @adamamer20 in +* Adding Abstract DiscreteSpaceDF by @adamamer20 in +* Adding abstract GridDF by @adamamer20 in +* Additional methods and fixes to DataFrameMixin by @adamamer20 in +* Concrete GridPandas by @adamamer20 in +* [pre-commit.ci] pre-commit autoupdate by @pre-commit-ci in +* Fixes and Tests for PolarsMixin by @adamamer20 in +* Adding Comparison and Indexing methods to DataFrameMixin by @adamamer20 in +* Concrete GridPolars by @adamamer20 in +* Sugarscape Instantaneous Growback (Pandas-with-loop implementation) by @adamamer20 in +* Adding pydoclint and properly format docstring by @adamamer20 in +* Docs with material-from-mkdocs by @adamamer20 in +* Enforce correct numpy docstring formatting with ruff.pydocstyle by @adamamer20 in +* API Documentation with Sphinx by @adamamer20 in +* Move images from docs to docs/general to make it available for mkdocs by @adamamer20 in +* Adding user guide by @adamamer20 in +* Adding SugarScape IG (polars with loops) by @adamamer20 in +* Automatic publishing on PyPI on new release by @adamamer20 in + +## New Contributors + +* @adamamer20 made their first contribution in +* @rht made their first contribution in +* @pre-commit-ci made their first contribution in + +**Full Changelog**: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 147b84d3..82a340c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,16 +15,22 @@ Before contributing, we recommend reviewing our [roadmap](https://projectmesa.gi Before you begin contributing, ensure that you have the necessary tools installed: - **Install Python** (at least the version specified in `requires-python` of `pyproject.toml`). ๐Ÿ -- We recommend using a virtual environment manager like: - - [Astral's UV](https://docs.astral.sh/uv/#installation) ๐ŸŒŸ - - [Hatch](https://hatch.pypa.io/latest/install/) ๐Ÿ—๏ธ + +-- We recommend using a virtual environment manager like: + + - [Astral's UV](https://docs.astral.sh/uv/#installation) ๐ŸŒŸ + - [Hatch](https://hatch.pypa.io/latest/install/) ๐Ÿ—๏ธ + - Install **pre-commit** to enforce code quality standards before pushing changes: + - [Pre-commit installation guide](https://pre-commit.com/#install) โœ… - [More about pre-commit hooks](https://stackoverflow.com/collectives/articles/71270196/how-to-use-pre-commit-to-automatically-correct-commits-and-merge-requests-with-g) -- If using **VS Code**, consider installing these extensions to automatically enforce formatting: - - [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) โ€“ Python linting & formatting ๐Ÿพ - - [Markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint) โ€“ Markdown linting (for documentation) โœ๏ธ - - [Git Hooks](https://marketplace.visualstudio.com/items?itemName=lakshmikanthayyadevara.githooks) โ€“ Automatically runs & visualizes pre-commit hooks ๐Ÿ”— + +-- If using **VS Code**, consider installing these extensions to automatically enforce formatting: + + - [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) โ€“ Python linting & formatting ๐Ÿพ + - [Markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint) โ€“ Markdown linting (for documentation) โœ๏ธ + - [Git Hooks](https://marketplace.visualstudio.com/items?itemName=lakshmikanthayyadevara.githooks) โ€“ Automatically runs & visualizes pre-commit hooks ๐Ÿ”— --- @@ -58,28 +64,13 @@ Before you begin contributing, ensure that you have the necessary tools installe #### **Step 3: Install Dependencies** ๐Ÿ“ฆ -It is recommended to set up a virtual environment before installing dependencies. - -- **Using UV**: - - ```sh - uv add --dev .[dev] - ``` - -- **Using Hatch**: +We manage the development environment with [uv](https://docs.astral.sh/uv/): - ```sh - hatch env create dev - ``` +```sh +uv sync --all-extras +``` -- **Using Standard Python**: - - ```sh - python3 -m venv myenv - source myenv/bin/activate # macOS/Linux - myenv\Scripts\activate # Windows - pip install -e ".[dev]" - ``` +This creates `.venv/` and installs mesa-frames with the development extras. #### **Step 4: Make and Commit Changes** โœจ @@ -99,33 +90,35 @@ It is recommended to set up a virtual environment before installing dependencies - **Run pre-commit hooks** to enforce code quality standards: ```sh - pre-commit run + uv run pre-commit run -a ``` - **Run tests** to ensure your contribution does not break functionality: ```sh - pytest --cov + uv run pytest -q --cov=mesa_frames --cov-report=term-missing ``` - - If using UV: `uv run pytest --cov` +-- **Optional: Runtime Type Checking (beartype)** ๐Ÿ” -- **Optional: Enable runtime type checking** during development for enhanced type safety: + You can enable stricter runtime validation of function arguments/returns with `beartype` during local development: ```sh - MESA_FRAMES_RUNTIME_TYPECHECKING=1 uv run pytest --cov + MESA_FRAMES_RUNTIME_TYPECHECKING=1 uv run pytest -q --cov=mesa_frames --cov-report=term-missing ``` - !!! tip "Automatically Enabled" - Runtime type checking is automatically enabled in these scenarios: + Quick facts: - - **Hatch development environment** (`hatch shell dev`) - - **VS Code debugging** (when using the debugger) - - **VS Code testing** (when running tests through VS Code's testing interface) +- Automatically enabled in: Hatch dev env (`hatch shell dev`), VS Code debugger, and VS Code test runs. +- Enable manually by exporting `MESA_FRAMES_RUNTIME_TYPECHECKING=1` (any of 1/true/yes). +- Use only for development/debugging; adds overheadโ€”disable for performance measurements or large simulations. +- Unset with your shell (e.g. `unset`/`Remove-Item Env:` depending on shell) to turn it off. - No manual setup needed in these environments! + Example for a one-off test run: - For more details on runtime type checking, see the [Development Guidelines](https://projectmesa.github.io/mesa-frames/development/). + ```sh + MESA_FRAMES_RUNTIME_TYPECHECKING=1 uv run pytest -q + ``` #### **Step 6: Documentation Updates (If Needed)** ๐Ÿ“– @@ -135,8 +128,7 @@ It is recommended to set up a virtual environment before installing dependencies - Preview your changes by running: ```sh - mkdocs serve - uv run mkdocs serve #If using uv + uv run mkdocs serve ``` - Open `http://127.0.0.1:8000` in your browser to verify documentation updates. diff --git a/README.md b/README.md index 6a16baad..b46acc8e 100644 --- a/README.md +++ b/README.md @@ -1,173 +1,140 @@ -# mesa-frames ๐Ÿš€ + +

+ Mesa logo +

-mesa-frames is an extension of the [mesa](https://github.com/projectmesa/mesa) framework, designed for complex simulations with thousands of agents. By storing agents in a DataFrame, mesa-frames significantly enhances the performance and scalability of mesa, while maintaining a similar syntax. mesa-frames allows for the use of [vectorized functions](https://stackoverflow.com/a/1422198) which significantly speeds up operations whenever simultaneous activation of agents is possible. +

mesa-frames

+ -## Why DataFrames? ๐Ÿ“Š +| | | +| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| CI/CD | [![CI Checks](https://github.com/projectmesa/mesa-frames/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/projectmesa/mesa-frames/actions/workflows/build.yml) [![codecov](https://codecov.io/gh/projectmesa/mesa-frames/branch/main/graph/badge.svg)](https://app.codecov.io/gh/projectmesa/mesa-frames) | +| Package | [![PyPI - Version](https://img.shields.io/pypi/v/mesa-frames.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/mesa-frames/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/mesa-frames.svg?color=blue&label=Downloads&logo=pypi&logoColor=gold)](https://pypi.org/project/mesa-frames/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mesa-frames.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/mesa-frames/) | +| Meta | [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://docs.astral.sh/ruff/) [![formatter - Ruff](https://img.shields.io/badge/formatter-Ruff-0f172a?logo=ruff&logoColor=white)](https://docs.astral.sh/ruff/formatter/) [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch) [![Managed with uv](https://img.shields.io/badge/managed%20with-uv-5a4fcf?logo=uv&logoColor=white)](https://github.com/astral-sh/uv) | +| Chat | [![chat](https://img.shields.io/matrix/project-mesa:matrix.org?label=chat&logo=Matrix)](https://matrix.to/#/#project-mesa:matrix.org) | -DataFrames are optimized for simultaneous operations through [SIMD processing](https://en.wikipedia.org/wiki/Single_instruction,_multiple_data). At the moment, mesa-frames supports the use of Polars library. +--- -- [Polars](https://pola.rs/) is a new DataFrame library with a syntax similar to pandas but with several innovations, including a backend implemented in Rust, the Apache Arrow memory format, query optimization, and support for larger-than-memory DataFrames. +## Scale Mesa beyond its limits -The following is a performance graph showing execution time using mesa and mesa-frames for the [Boltzmann Wealth model](https://mesa.readthedocs.io/en/stable/tutorials/intro_tutorial.html). +Classic [Mesa](https://github.com/projectmesa/mesa) stores each agent as a Python object, which quickly becomes a bottleneck at scale. +**mesa-frames** reimagines agent storage using **Polars DataFrames**, so agents live in a columnar store rather than the Python heap. -![Performance Graph with Mesa](https://github.com/projectmesa/mesa-frames/blob/main/examples/boltzmann_wealth/boltzmann_with_mesa.png) +You keep the Mesa-style `Model` / `AgentSet` structure, but updates are vectorized and memory-efficient. -![Performance Graph without Mesa](https://github.com/projectmesa/mesa-frames/blob/main/examples/boltzmann_wealth/boltzmann_no_mesa.png) +### Why it matters -([You can check the script used to generate the graph here](https://github.com/projectmesa/mesa-frames/blob/main/examples/boltzmann_wealth/performance_plot.py), but if you want to additionally compare vs Mesa, you have to uncomment `mesa_implementation` and its label) +- โšก **10ร— faster** bulk updates on 10k+ agents ([see Benchmarks](#benchmarks)) +- ๐Ÿ“Š **Columnar execution** via [Polars](https://docs.pola.rs/): [SIMD](https://en.wikipedia.org/wiki/Single_instruction,_multiple_data) ops, multi-core support +- ๐Ÿ”„ **Declarative logic**: agent rules as transformations, not Python loops +- ๐Ÿš€ **Roadmap**: Lazy queries and GPU support for even faster models -## Installation +--- -### Install from PyPI +## Who is it for? -```bash -pip install mesa-frames -``` +- Researchers needing to scale to **tens or hundreds of thousands of agents** +- Users whose agent logic can be written as **vectorized, set-based operations** -### Install from Source +โŒ **Not a good fit if:** your model depends on strict per-agent sequencing, complex non-vectorizable methods, or fine-grained identity tracking. -To install the most updated version of mesa-frames, you can clone the repository and install the package in editable mode. +--- -#### Cloning the Repository +## Why DataFrames? -To get started with mesa-frames, first clone the repository from GitHub: +DataFrames enable SIMD and columnar operations that are far more efficient than Python loops. +mesa-frames currently uses **Polars** as its backend. -```bash -git clone https://github.com/projectmesa/mesa-frames.git -cd mesa_frames -``` +| Feature | mesa (classic) | mesa-frames | +| ---------------------- | -------------- | ----------- | +| Storage | Python objects | Polars DataFrame | +| Updates | Loops | Vectorized ops | +| Memory overhead | High | Low | +| Max agents (practical) | ~10^3 | ~10^6+ | -#### Installing in a Conda Environment +--- -If you want to install it into a new environment: +## Benchmarks -```bash -conda create -n myenv -``` +[![Reproduce Benchmarks](https://img.shields.io/badge/Reproduce%20Benchmarks-๐Ÿ“Š-orange?style=for-the-badge)](https://github.com/projectmesa/mesa-frames/blob/main/benchmarks/README.md) -If you want to install it into an existing environment: +**mesa-frames consistently outperforms classic Mesa across both toy and canonical ABMs.** -```bash -conda activate myenv -``` +In the Boltzmann model, it maintains near-constant runtimes even as agent count rises, achieving **up to 10ร— faster execution** at scale. -Then, to install mesa-frames itself: +In the more computation-intensive Sugarscape model, **mesa-frames roughly halves total runtime**. -```bash -pip install -e . -``` +We still have room to optimize performance further (see [Roadmap](#roadmap)). -#### Installing in a Python Virtual Environment +![Benchmark: Boltzmann Wealth](docs/general/plots/boltzmann.svg) -If you want to install it into a new environment: +![Benchmark: Sugarscape IG](docs/general/plots/sugarscape.svg) -```bash -python3 -m venv myenv -source myenv/bin/activate # On Windows, use `myenv\Scripts\activate` -``` +--- -If you want to install it into an existing environment: +## Quick Start -```bash -source myenv/bin/activate # On Windows, use `myenv\Scripts\activate` -``` +[![Explore the Tutorials](https://img.shields.io/badge/Explore%20the%20Tutorials-๐Ÿ“š-blue?style=for-the-badge)](/mesa-frames/tutorials/2_introductory_tutorial/) -Then, to install mesa-frames itself: +1. **Install** ```bash -pip install -e . + pip install mesa-frames ``` -## Usage - -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/projectmesa/mesa-frames/blob/main/docs/general/user-guide/2_introductory-tutorial.ipynb) +Or for development: -**Note:** mesa-frames is currently in its early stages of development. As such, the usage patterns and API are subject to change. Breaking changes may be introduced. Reports of feedback and issues are encouraged. - -[You can find the API documentation here](https://projectmesa.github.io/mesa-frames/api). - -### Creation of an Agent - -The agent implementation differs from base mesa. Agents are only defined at the AgentSet level. You can import `AgentSet`. As in mesa, you subclass and make sure to call `super().__init__(model)`. You can use the `add` method or the `+=` operator to add agents to the AgentSet. Most methods mirror the functionality of `mesa.AgentSet`. Additionally, `mesa-frames.AgentSet` implements many dunder methods such as `AgentSet[mask, attr]` to get and set items intuitively. All operations are by default inplace, but if you'd like to use functional programming, mesa-frames implements a fast copy method which aims to reduce memory usage, relying on reference-only and native copy methods. - -```python -from mesa-frames import AgentSet - -class MoneyAgents(AgentSet): - def __init__(self, n: int, model: Model): - super().__init__(model) - # Adding the agents to the agent set - self += pl.DataFrame( - {"wealth": pl.ones(n, eager=True)} - ) - - def step(self) -> None: - # The give_money method is called - self.do("give_money") - - def give_money(self): - # Active agents are changed to wealthy agents - self.select(self.wealth > 0) +```bash +git clone https://github.com/projectmesa/mesa-frames.git +cd mesa-frames +uv sync --all-extras +``` - # Receiving agents are sampled (only native expressions currently supported) - other_agents = self.df.sample( - n=len(self.active_agents), with_replacement=True - ) +1. **Create a model** - # Wealth of wealthy is decreased by 1 - self["active", "wealth"] -= 1 + ```python + from mesa_frames import AgentSet, Model + import polars as pl - # Compute the income of the other agents (only native expressions currently supported) - new_wealth = other_agents.group_by("unique_id").len() + class MoneyAgents(AgentSet): + def __init__(self, n: int, model: Model): + super().__init__(model) + self += pl.DataFrame({"wealth": pl.ones(n, eager=True)}) - # Add the income to the other agents - self[new_wealth, "wealth"] += new_wealth["len"] -``` + def give_money(self): + self.select(self.wealth > 0) + other_agents = self.df.sample(n=len(self.active_agents), with_replacement=True) + self["active", "wealth"] -= 1 + new_wealth = other_agents.group_by("unique_id").len() + self[new_wealth, "wealth"] += new_wealth["len"] -### Creation of the Model + def step(self): + self.do("give_money") -Creation of the model is fairly similar to the process in mesa. You subclass `Model` and call `super().__init__()`. The `model.sets` attribute has the same interface as `mesa-frames.AgentSet`. You can use `+=` or `self.sets.add` with a `mesa-frames.AgentSet` (or a list of `AgentSet`) to add agents to the model. + class MoneyModelDF(Model): + def __init__(self, N: int): + super().__init__() + self.sets += MoneyAgents(N, self) -```python -from mesa-frames import Model + def step(self): + self.sets.do("step") + ``` -class MoneyModelDF(Model): - def __init__(self, N: int, agents_cls): - super().__init__() - self.n_agents = N - self.sets += MoneyAgents(N, self) +--- - def step(self): - # Executes the step method for every agentset in self.sets - self.sets.do("step") +## Roadmap - def run_model(self, n): - for _ in range(n): - self.step() -``` +> Community contributions welcome โ€” see the [full roadmap](mesa-frames/roadmap) -## What's Next? ๐Ÿ”ฎ +- Transition to LazyFrames for optimization and GPU support +- Auto-vectorize existing Mesa models via decorator +- Increase possible Spaces (Network, Continuous...) +- Refine the API to align to Mesa -- Refine the API to make it more understandable for someone who is already familiar with the mesa package. The goal is to provide a seamless experience for users transitioning to or incorporating mesa-frames. -- Adding support for default mesa functions to ensure that the standard mesa functionality is preserved. -- Adding GPU functionality (cuDF and Dask-cuDF). -- Creating a decorator that will automatically vectorize an existing mesa model. This feature will allow users to easily tap into the performance enhancements that mesa-frames offers without significant code alterations. -- Creating a unique class for AgentSet, independent of the backend implementation. +--- ## License -Copyright 2024 Adam Amer, Project Mesa team and contributors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +Copyright ยฉ 2025 Adam Amer, Project Mesa team and contributors -For the full license text, see the [LICENSE](https://github.com/projectmesa/mesa-frames/blob/main/LICENSE) file in the GitHub repository. +Licensed under the [Apache License, Version 2.0](https://raw.githubusercontent.com/projectmesa/mesa-frames/refs/heads/main/LICENSE). diff --git a/ROADMAP.md b/ROADMAP.md index 7dd953f5..e70292cf 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,67 +1,63 @@ -# Roadmap ๐Ÿ—บ๏ธ +# Roadmap -This document outlines the development roadmap for the mesa-frames project. It provides insights into our current priorities, upcoming features, and long-term vision. +This document outlines the near-term roadmap for mesa-frames as of October 2025. -## 0.1.0 Stable Release Goals ๐ŸŽฏ +### 1) LazyFrames for Polars + GPU -### 1. Transitioning polars implementation from eager API to lazy API +Switch Polars usage from eager to `LazyFrame` to enable better query optimization and GPU acceleration. -One of our major priorities was to move from pandas to polars as the primary dataframe backend. This transition was motivated by performance considerations. -Now we should transition to using the lazily evaluated version of polars. +Related issues: -**Related issues:** [#10: GPU integration: Dask, cuda (cudf) and RAPIDS (Polars)](https://github.com/projectmesa/mesa-frames/issues/10), [#89: Investigate using Ibis for the common interface library to any DF backend](https://github.com/projectmesa/mesa-frames/issues/89), [#52: Use of LazyFrames for Polars implementation](https://github.com/projectmesa/mesa-frames/issues/52) +- [#52: Use of LazyFrames for Polars implementation](https://github.com/projectmesa/mesa-frames/issues/52) -#### Progress and Next Steps +- [#144: Switch to LazyFrame for Polars implementation (PR)](https://github.com/projectmesa/mesa-frames/pull/144) -- We are exploring [Ibis](https://ibis-project.org/) or [narwhals](https://github.com/narwhals-dev/narwhals) as a common interface library that could support multiple backends (Polars, DuckDB, Spark etc.), but since most of the development is currently in polars, we will currently continue using Polars. -- We're transitioning to the lazy API, mainly in order to use GPU acceleration +- [#89: Investigate Ibis or Narwhals for backend flexibility](https://github.com/projectmesa/mesa-frames/issues/89) -### 2. Handling Concurrency Management +- [#122: Deprecate DataFrameMixin (remove during LazyFrames refactor)](https://github.com/projectmesa/mesa-frames/issues/122) -A critical aspect of agent-based models is efficiently managing concurrent agent movements, especially when multiple agents attempt to move to the same location simultaneously. We aim to implement abstractions that handle these concurrency conditions automatically. +Progress and next steps: -**Related issues:** [#108: Adding abstraction of optimal agent movement](https://github.com/projectmesa/mesa-frames/issues/108), [#48: Emulate RandomActivation with DataFrame.rolling](https://github.com/projectmesa/mesa-frames/issues/48) +- Land [#144](https://github.com/projectmesa/mesa-frames/pull/144) and convert remaining eager paths to lazy. -#### Sugarscape Example of Concurrency Issues +- Validate GPU execution paths and benchmark improvements. -Testing with many potential collisions revealed a specific issue: +- Revisit Ibis/Narwhals after LazyFrame stabilization. -**Problem scenario:** +- Fold DataFrameMixin removal into the LazyFrames transition ([#122](https://github.com/projectmesa/mesa-frames/issues/122)). -- Consider two agents targeting the same cell: - - A mid-priority agent (higher in the agent order) - - A low-priority agent (lower in the agent order) -- The mid-priority agent has low preference for the cell -- The low-priority agent has high preference for the cell -- Without accounting for priority: - - The mid-priority agent's best moves kept getting "stolen" by higher priority agents - - This forced it to resort to lower preference target cells - - However, these lower preference cells were often already taken by lower priority agents in previous iterations +--- -**Solution approach:** +### 2) AgentSet Enhancements -- Implement a "priority" count to ensure that each action is "legal" -- This prevents race conditions but requires recomputing the priority at each iteration -- Current implementation may be slower than Numba due to this overhead -- After the Ibis refactoring, we can investigate if lazy evaluation can help mitigate this performance issue +Expose movement methods from `AgentContainer` and provide optimized utilities for "move to optimal" workflows. -The Sugarscape example demonstrates the need for this abstraction, as multiple agents often attempt to move to the same cell simultaneously. By generalizing this functionality, we can eliminate the need for users to implement complex conflict resolution logic repeatedly. +Related issues: -#### Progress and Next Steps +- [#108: Adding abstraction of optimal agent movement](https://github.com/projectmesa/mesa-frames/issues/108) -- Create utility functions in `DiscreteSpace` and `AgentSetRegistry` to move agents optimally based on specified attributes -- Provide built-in resolution strategies for common concurrency scenarios -- Ensure the implementation works efficiently with the vectorized approach of mesa-frames +- [#118: Adds move_to_optimal in DiscreteSpaceDF (PR)](https://github.com/projectmesa/mesa-frames/pull/118) -### Additional 0.1.0 Goals +- [#82: Add movement methods to AgentContainer](https://github.com/projectmesa/mesa-frames/issues/82) -- Complete core API stabilization -- Completely mirror mesa's functionality -- Improve documentation and examples -- Address outstanding bugs and performance issues +Next steps: -## Beyond 0.1.0 +- Consolidate movement APIs under `AgentContainer`. -Future roadmap items will be added as the project evolves and new priorities emerge. +- Keep conflict resolution simple, vectorized, and well-documented. -We welcome community feedback on our roadmap! Please open an issue if you have suggestions or would like to contribute to any of these initiatives. +--- + +### 3) Research & Publication + +JOSS paper preparation and submission. + +Related items: + +- [#90: JOSS paper for the package](https://github.com/projectmesa/mesa-frames/issues/90) + +- [#107: paper - Adding Statement of Need (PR)](https://github.com/projectmesa/mesa-frames/pull/107) + +--- + +See [our contribution guide](/mesa-frames/contributing/) and browse all open items at diff --git a/docs/api/_static/brand-core.css b/docs/api/_static/brand-core.css new file mode 100644 index 00000000..e69de29b diff --git a/docs/api/_static/brand-pydata.css b/docs/api/_static/brand-pydata.css new file mode 100644 index 00000000..e69de29b diff --git a/docs/api/conf.py b/docs/api/conf.py index 43098ec2..4998fbc2 100644 --- a/docs/api/conf.py +++ b/docs/api/conf.py @@ -31,9 +31,26 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- +# Hide objects (classes/methods) from the page Table of Contents +toc_object_entries = False # NEW: stop adding class/method entries to the TOC + + html_theme = "pydata_sphinx_theme" html_static_path = ["_static"] html_show_sourcelink = False +html_logo = ( + "https://raw.githubusercontent.com/projectmesa/mesa/main/docs/images/mesa_logo.png" +) +html_favicon = ( + "https://raw.githubusercontent.com/projectmesa/mesa/main/docs/images/mesa_logo.ico" +) + +# Add custom branding CSS/JS (mesa_brand) to static files +html_css_files = [ + # Shared brand variables then theme adapter for pydata + "brand-core.css", + "brand-pydata.css", +] # -- Extension settings ------------------------------------------------------ # intersphinx mapping @@ -52,9 +69,20 @@ copybutton_prompt_is_regexp = True # -- Custom configurations --------------------------------------------------- +add_module_names = False autoclass_content = "class" autodoc_member_order = "bysource" -autodoc_default_options = {"special-members": True, "exclude-members": "__weakref__"} +autodoc_default_options = { + "members": True, + "inherited-members": True, + "undoc-members": True, + "member-order": "bysource", + "special-members": True, + "exclude-members": "__weakref__,__dict__,__module__,__annotations__,__firstlineno__,__static_attributes__,__abstractmethods__,__slots__", +} + +autosummary_generate_overwrite = True + # -- GitHub link and user guide settings ------------------------------------- github_root = "https://github.com/projectmesa/mesa-frames" @@ -64,7 +92,7 @@ "external_links": [ { "name": "User guide", - "url": f"{web_root}/user-guide/", + "url": f"{web_root}/user-guide/0_getting-started/", }, ], "icon_links": [ @@ -73,6 +101,12 @@ "url": github_root, "icon": "fa-brands fa-github", }, + { + "name": "Matrix", + "url": "https://matrix.to/#/#project-mesa:matrix.org", + "icon": "fa-solid fa-comments", + }, ], - "navbar_end": ["navbar-icon-links"], + "navbar_start": ["navbar-logo"], + "navbar_end": ["theme-switcher", "navbar-icon-links"], } diff --git a/docs/api/index.rst b/docs/api/index.rst index 936350d6..a7c2ab4c 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -1,34 +1,55 @@ mesa-frames API =============== -This page provides a high-level overview of all public mesa-frames objects, functions, and methods. All classes and functions exposed in the ``mesa_frames.*`` namespace are public. +.. toctree:: + :caption: Shortcuts + :maxdepth: 1 + :hidden: + + reference/agents/index + reference/model + reference/space/index + reference/datacollector + -.. grid:: - .. grid-item-card:: +Overview +-------- - .. toctree:: - :maxdepth: 2 +mesa-frames provides a DataFrame-first API for agent-based models. Instead of representing each agent as a distinct Python object, agents are stored in AgentSets (backed by DataFrames) and manipulated via vectorised operations. This leads to much lower memory overhead and faster bulk updates while keeping an object-oriented feel for model structure and lifecycle management. - reference/agents/index - .. grid-item-card:: +Mini usage flow +--------------- + +1. Create a Model and register AgentSets on ``model.sets``. +2. Populate AgentSets with agents (rows) and attributes (columns) via adding a DataFrame to the AgentSet. +3. Implement AgentSet methods that operate on DataFrames +4. Use ``model.sets.do("step")`` from the model loop to advance the simulation; datacollectors and reporters can sample model- and agent-level columns at each step. + +.. grid:: + :gutter: 2 - .. toctree:: - :maxdepth: 2 + .. grid-item-card:: Manage agent collections + :link: reference/agents/index + :link-type: doc - reference/model + Create and operate on ``AgentSets`` and ``AgentSetRegisties``: add/remove agents. - .. grid-item-card:: + .. grid-item-card:: Model orchestration + :link: reference/model + :link-type: doc - .. toctree:: - :maxdepth: 2 + ``Model`` API for registering sets, stepping the simulation, and integrating with datacollectors/reporters. - reference/space/index + .. grid-item-card:: Spatial support + :link: reference/space/index + :link-type: doc - .. grid-item-card:: + Placement and neighbourhood utilities for ``Grid`` and space - .. toctree:: - :maxdepth: 2 + .. grid-item-card:: Collect simulation data + :link: reference/datacollector + :link-type: doc - reference/datacollector \ No newline at end of file + Record model- and agent-level metrics over time with ``DataCollector``. Sample columns, run aggregations, and export cleaned frames for analysis. \ No newline at end of file diff --git a/docs/api/reference/agents/index.rst b/docs/api/reference/agents/index.rst index a1c03126..4576a204 100644 --- a/docs/api/reference/agents/index.rst +++ b/docs/api/reference/agents/index.rst @@ -3,15 +3,187 @@ Agents .. currentmodule:: mesa_frames +Quick intro +----------- -.. autoclass:: AgentSet - :members: - :inherited-members: - :autosummary: - :autosummary-nosignatures: - -.. autoclass:: AgentSetRegistry - :members: - :inherited-members: - :autosummary: - :autosummary-nosignatures: +- ``AgentSet`` stores agents as rows in a Polars-backed table and provides vectorised operations for high-performance updates. + +- ``AgentSetRegistry`` (available at ``model.sets``) is the container that holds all ``AgentSet`` instances for a model and provides convenience operations (add/remove sets, step all sets, rename). + +- Keep agent logic column-oriented and prefer Polars expressions for updates. + +Minimal example +--------------- + +.. code-block:: python + + from mesa_frames import Model, AgentSet + import polars as pl + + class MySet(AgentSet): + def __init__(self, model): + super().__init__(model) + self.add(pl.DataFrame({"age": [0, 5, 10]})) + + def step(self): + # vectorised update: increase age for all agents + self.df = self.df.with_columns((pl.col("age") + 1).alias("age")) + + class MyModel(Model): + def __init__(self): + super().__init__() + # register an AgentSet on the model's registry + self.sets += MySet(self) + + m = MyModel() + # step all registered sets (delegates to each AgentSet.step) + m.sets.do("step") + +API reference +--------------------------------- + +.. tab-set:: + + .. tab-item:: AgentSet + + .. tab-set:: + + .. tab-item:: Overview + + .. rubric:: Lifecycle / Core + + .. autosummary:: + :nosignatures: + :toctree: + + AgentSet.__init__ + AgentSet.step + AgentSet.rename + AgentSet.copy + + .. rubric:: Accessors & Views + + .. autosummary:: + :nosignatures: + :toctree: + + AgentSet.df + AgentSet.model + AgentSet.random + AgentSet.space + AgentSet.active_agents + AgentSet.inactive_agents + AgentSet.index + AgentSet.pos + AgentSet.name + AgentSet.get + AgentSet.contains + AgentSet.__len__ + AgentSet.__iter__ + AgentSet.__getitem__ + AgentSet.__contains__ + + .. rubric:: Mutators + + .. autosummary:: + :nosignatures: + :toctree: + + AgentSet.add + AgentSet.remove + AgentSet.discard + AgentSet.set + AgentSet.select + AgentSet.shuffle + AgentSet.sort + AgentSet.do + + .. rubric:: Operators / Internal helpers + + .. autosummary:: + :nosignatures: + :toctree: + + AgentSet.__add__ + AgentSet.__iadd__ + AgentSet.__sub__ + AgentSet.__isub__ + AgentSet.__repr__ + AgentSet.__reversed__ + + .. tab-item:: Full API + + .. autoclass:: AgentSet + :autosummary: + :autosummary-nosignatures: + + .. tab-item:: AgentSetRegistry + + .. tab-set:: + + .. tab-item:: Overview + + .. rubric:: Lifecycle / Core + + .. autosummary:: + :nosignatures: + :toctree: + + AgentSetRegistry.__init__ + AgentSetRegistry.copy + AgentSetRegistry.rename + + .. rubric:: Accessors & Queries + + .. autosummary:: + :nosignatures: + :toctree: + + AgentSetRegistry.get + AgentSetRegistry.contains + AgentSetRegistry.ids + AgentSetRegistry.keys + AgentSetRegistry.items + AgentSetRegistry.values + AgentSetRegistry.model + AgentSetRegistry.random + AgentSetRegistry.space + AgentSetRegistry.__len__ + AgentSetRegistry.__iter__ + AgentSetRegistry.__getitem__ + AgentSetRegistry.__contains__ + + .. rubric:: Mutators / Coordination + + .. autosummary:: + :nosignatures: + :toctree: + + AgentSetRegistry.add + AgentSetRegistry.remove + AgentSetRegistry.discard + AgentSetRegistry.replace + AgentSetRegistry.shuffle + AgentSetRegistry.sort + AgentSetRegistry.do + AgentSetRegistry.__setitem__ + AgentSetRegistry.__add__ + AgentSetRegistry.__iadd__ + AgentSetRegistry.__sub__ + AgentSetRegistry.__isub__ + + .. rubric:: Representation + + .. autosummary:: + :nosignatures: + :toctree: + + AgentSetRegistry.__repr__ + AgentSetRegistry.__str__ + AgentSetRegistry.__reversed__ + + .. tab-item:: Full API + + .. autoclass:: AgentSetRegistry + :autosummary: + :autosummary-nosignatures: diff --git a/docs/api/reference/datacollector.rst b/docs/api/reference/datacollector.rst index bdf38cfd..e95e2672 100644 --- a/docs/api/reference/datacollector.rst +++ b/docs/api/reference/datacollector.rst @@ -1,10 +1,68 @@ Data Collection -===== +=============== .. currentmodule:: mesa_frames -.. autoclass:: DataCollector - :members: - :inherited-members: - :autosummary: - :autosummary-nosignatures: \ No newline at end of file +Quick intro +----------- + +``DataCollector`` samples model- and agent-level columns over time and returns cleaned DataFrames suitable for analysis. Typical patterns: + +- Provide ``model_reporters`` (callables producing scalars) and ``agent_reporters`` (column selectors or callables that operate on an AgentSet). +- Call ``collector.collect(model)`` inside the model step or use built-in integration if the model calls the collector automatically. + +Minimal example +--------------- + +.. code-block:: python + + from mesa_frames import DataCollector, Model, AgentSet + import polars as pl + + class P(AgentSet): + def __init__(self, model): + super().__init__(model) + self.add(pl.DataFrame({'x': [1,2]})) + + class M(Model): + def __init__(self): + super().__init__() + self.sets += P(self) + self.dc = DataCollector(model_reporters={'count': lambda m: len(m.sets['P'])}, + agent_reporters='x') + + m = M() + m.dc.collect() + +API reference +------------- + +.. tab-set:: + + .. tab-item:: Overview + + .. rubric:: Lifecycle / Core + + .. autosummary:: + :nosignatures: + :toctree: + + DataCollector.__init__ + DataCollector.collect + DataCollector.conditional_collect + DataCollector.flush + DataCollector.data + + .. rubric:: Reporting / Internals + + .. autosummary:: + :nosignatures: + :toctree: + + DataCollector.seed + + .. tab-item:: Full API + + .. autoclass:: DataCollector + :autosummary: + :autosummary-nosignatures: diff --git a/docs/api/reference/model.rst b/docs/api/reference/model.rst index 099e601b..0fb12b55 100644 --- a/docs/api/reference/model.rst +++ b/docs/api/reference/model.rst @@ -3,8 +3,67 @@ Model .. currentmodule:: mesa_frames -.. autoclass:: Model - :members: - :inherited-members: - :autosummary: - :autosummary-nosignatures: \ No newline at end of file +Quick intro +----------- + +The ``Model`` orchestrates the simulation lifecycle: creating and registering ``AgentSet``s, stepping the simulation, and integrating with ``DataCollector`` and spatial ``Grid``s. Typical usage: + +- Instantiate ``Model``, add ``AgentSet`` instances to ``model.sets``. +- Call ``model.sets.do('step')`` inside your model loop to trigger set-level updates. +- Use ``DataCollector`` to sample model- and agent-level columns each step. + +Minimal example +--------------- + +.. code-block:: python + + from mesa_frames import Model, AgentSet, DataCollector + import polars as pl + + class People(AgentSet): + def step(self): + self.add(pl.DataFrame({'wealth': [1, 2, 3]})) + + class MyModel(Model): + def __init__(self): + super().__init__() + self.sets += People(self) + self.dc = DataCollector(model_reporters={'avg_wealth': lambda m: m.sets['People'].df['wealth'].mean()}) + + m = MyModel() + m.step() + +API reference +------------- + +.. tab-set:: + + .. tab-item:: Overview + + .. rubric:: Lifecycle / Core + + .. autosummary:: + :nosignatures: + :toctree: + + Model.__init__ + Model.step + Model.run_model + Model.reset_randomizer + + .. rubric:: Accessors / Properties + + .. autosummary:: + :nosignatures: + :toctree: + + Model.steps + Model.sets + Model.space + Model.seed + + .. tab-item:: Full API + + .. autoclass:: Model + :autosummary: + :autosummary-nosignatures: diff --git a/docs/api/reference/space/index.rst b/docs/api/reference/space/index.rst index 8741b6b6..aa8692f0 100644 --- a/docs/api/reference/space/index.rst +++ b/docs/api/reference/space/index.rst @@ -4,8 +4,98 @@ This page provides a high-level overview of possible space objects for mesa-fram .. currentmodule:: mesa_frames -.. autoclass:: Grid - :members: - :inherited-members: - :autosummary: - :autosummary-nosignatures: \ No newline at end of file +Quick intro +----------- + + + +Currently we only support the ``Grid``. Typical usage: + +- Construct ``Grid(model, (width, height))`` and use ``place``/ ``move`` helpers to update agent positional columns. +- Use neighbourhood queries to produce masks or index lists and then apply vectorised updates to selected rows. + +Minimal example +--------------- + +.. code-block:: python + + from mesa_frames import Model, Grid, AgentSet + import polars as pl + + class P(AgentSet): + pass + + class M(Model): + def __init__(self): + super().__init__() + self.space = Grid(self, (10, 10)) + self.sets += P(self) + self.space.place_to_empty(self.sets) + + m = M() + m.space.move_to_available(m.sets) + + +API reference +------------- + +.. tab-set:: + + .. tab-item:: Overview + + .. rubric:: Lifecycle / Core + + .. autosummary:: + :nosignatures: + :toctree: + + Grid.__init__ + Grid.copy + + .. rubric:: Placement & Movement + + .. autosummary:: + :nosignatures: + :toctree: + + Grid.place_agents + Grid.move_agents + Grid.place_to_empty + Grid.place_to_available + Grid.move_to_empty + Grid.move_to_available + + .. rubric:: Sampling & Queries + + .. autosummary:: + :nosignatures: + :toctree: + + Grid.get_neighbors + Grid.get_directions + Grid.get_distances + Grid.sample_cells + Grid.random_pos + Grid.is_empty + Grid.is_available + Grid.is_full + + .. rubric:: Accessors & Metadata + + .. autosummary:: + :nosignatures: + :toctree: + + Grid.dimensions + Grid.neighborhood_type + Grid.torus + Grid.remaining_capacity + Grid.agents + Grid.model + Grid.random + + .. tab-item:: Full API + + .. autoclass:: Grid + :autosummary: + :autosummary-nosignatures: diff --git a/docs/general/changelog.md b/docs/general/changelog.md new file mode 100644 index 00000000..33ece5d4 --- /dev/null +++ b/docs/general/changelog.md @@ -0,0 +1 @@ +{% include-markdown "../../CHANGELOG.md" %} diff --git a/docs/general/index.md b/docs/general/index.md index 9859d2ee..cee3f109 100644 --- a/docs/general/index.md +++ b/docs/general/index.md @@ -1,89 +1 @@ -# Welcome to mesa-frames ๐Ÿš€ - -mesa-frames is an extension of the [mesa](https://github.com/projectmesa/mesa) framework, designed for complex simulations with thousands of agents. By storing agents in a DataFrame, mesa-frames significantly enhances the performance and scalability of mesa, while maintaining a similar syntax. - -You can get a model which is multiple orders of magnitude faster based on the number of agents - the more agents, the faster the relative performance. - -## Why DataFrames? ๐Ÿ“Š - -DataFrames are optimized for simultaneous operations through [SIMD processing](https://en.wikipedia.org/wiki/Single_instruction,_multiple_data). Currently, mesa-frames supports the library: - -- [Polars](https://pola.rs/): A new DataFrame library with a Rust backend, offering innovations like Apache Arrow memory format and support for larger-than-memory DataFrames. - -## Performance Boost ๐ŸŽ๏ธ - -Check out our performance graphs comparing mesa and mesa-frames for the [Boltzmann Wealth model](https://mesa.readthedocs.io/en/stable/tutorials/intro_tutorial.html): - -![Performance Graph with Mesa](https://github.com/projectmesa/mesa-frames/raw/main/examples/boltzmann_wealth/boltzmann_with_mesa.png) - -![Performance Graph without Mesa](https://github.com/projectmesa/mesa-frames/raw/main/examples/boltzmann_wealth/boltzmann_no_mesa.png) - -## Quick Start ๐Ÿš€ - -### Installation - -#### Installing from PyPI - -```bash -pip install mesa-frames -``` - -#### Installing from Source - -```bash -git clone https://github.com/projectmesa/mesa-frames.git -cd mesa_frames -pip install -e . -``` - -### Basic Usage - -Here's a quick example of how to create a model using mesa-frames: - -```python -from mesa_frames import AgentSet, Model -import polars as pl - -class MoneyAgents(AgentSet): - def __init__(self, n: int, model: Model): - super().__init__(model) - self += pl.DataFrame( - {"wealth": pl.ones(n, eager=True)} - ) - - def step(self) -> None: - self.do("give_money") - - def give_money(self): - # ... (implementation details) - -class MoneyModel(Model): - def __init__(self, N: int): - super().__init__() - self.sets += MoneyAgents(N, self) - - def step(self): - self.sets.do("step") - - def run_model(self, n): - for _ in range(n): - self.step() -``` - -## What's Next? ๐Ÿ”ฎ - -- API refinement for seamless transition from mesa -- Support for mesa functions -- Multiple other spaces: GeoGrid, ContinuousSpace, Network... -- Additional backends: Dask, cuDF (GPU), Dask-cuDF (GPU)... -- More examples: Schelling model, ... -- Automatic vectorization of existing mesa models -- Backend-agnostic AgentSet class - -## Get Involved! ๐Ÿค - -mesa-frames is in its early stages, and we welcome your feedback and contributions! Check out our [GitHub repository](https://github.com/projectmesa/mesa-frames) to get started. - -## License - -mesa-frames is available under the MIT License. See the [LICENSE](https://github.com/projectmesa/mesa-frames/blob/main/LICENSE) file for full details. +{% include-markdown "../../README.md" %} diff --git a/docs/general/plots/boltzmann.svg b/docs/general/plots/boltzmann.svg new file mode 100644 index 00000000..f21ca936 --- /dev/null +++ b/docs/general/plots/boltzmann.svg @@ -0,0 +1,1083 @@ + + + + + + + + 2025-10-16T19:57:07.933517 + image/svg+xml + + + Matplotlib v3.10.5, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/general/plots/sugarscape.svg b/docs/general/plots/sugarscape.svg new file mode 100644 index 00000000..679002c9 --- /dev/null +++ b/docs/general/plots/sugarscape.svg @@ -0,0 +1,1091 @@ + + + + + + + + 2025-10-16T19:57:08.355947 + image/svg+xml + + + Matplotlib v3.10.5, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/general/tutorials/2_introductory_tutorial.py b/docs/general/tutorials/2_introductory_tutorial.py new file mode 100644 index 00000000..92f6f1f9 --- /dev/null +++ b/docs/general/tutorials/2_introductory_tutorial.py @@ -0,0 +1,277 @@ +from __future__ import annotations + +# %% [markdown] +"""[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/projectmesa/mesa-frames/blob/main/docs/general/user-guide/2_introductory-tutorial.ipynb)""" + +# %% [markdown] +"""## Installation (if running in Colab) + +Run the following cell to install `mesa-frames` if you are using Google Colab.""" + +# %% +# #!pip install git+https://github.com/projectmesa/mesa-frames mesa + +# %% [markdown] +""" # Introductory Tutorial: Boltzmann Wealth Model with mesa-frames ๐Ÿ’ฐ๐Ÿš€ + +In this tutorial, we'll implement the Boltzmann Wealth Model using mesa-frames. This model simulates the distribution of wealth among agents, where agents randomly give money to each other. + +## Setting Up the Model ๐Ÿ—๏ธ + +First, let's import the necessary modules and set up our model class:""" + +# %% +from mesa_frames import Model, AgentSet, DataCollector + + +class MoneyModel(Model): + def __init__(self, N: int, agents_cls): + super().__init__() + self.n_agents = N + self.sets += agents_cls(N, self) + self.datacollector = DataCollector( + model=self, + model_reporters={ + "total_wealth": lambda m: m.sets["MoneyAgents"].df["wealth"].sum() + }, + agent_reporters={"wealth": "wealth"}, + storage="csv", + storage_uri="./data", + trigger=lambda m: m.schedule.steps % 2 == 0, + ) + + def step(self): + # Executes the step method for every agentset in self.sets + self.sets.do("step") + + def run_model(self, n): + for _ in range(n): + self.step() + self.datacollector.conditional_collect + self.datacollector.flush() + + +# %% [markdown] +"""## Implementing the AgentSet ๐Ÿ‘ฅ + +Now, let's implement our `MoneyAgents` using polars backends.""" + +# %% +import polars as pl + + +class MoneyAgents(AgentSet): + def __init__(self, n: int, model: Model): + super().__init__(model) + self += pl.DataFrame({"wealth": pl.ones(n, eager=True)}) + + def step(self) -> None: + self.do("give_money") + + def give_money(self): + self.select(self.wealth > 0) + other_agents = self.df.sample(n=len(self.active_agents), with_replacement=True) + self["active", "wealth"] -= 1 + new_wealth = other_agents.group_by("unique_id").len() + self[new_wealth["unique_id"], "wealth"] += new_wealth["len"] + + +# %% [markdown] +""" +## Running the Model โ–ถ๏ธ + +Now that we have our model and agent set defined, let's run a simulation:""" + +# %% +# Create and run the model +model = MoneyModel(1000, MoneyAgents) +model.run_model(100) + +wealth_dist = list(model.sets.df.values())[0] + +# Print the final wealth distribution +print(wealth_dist.select(pl.col("wealth")).describe()) + +# %% [markdown] +""" +This output shows the statistical summary of the wealth distribution after 100 steps of the simulation with 1000 agents. + +## Performance Comparison ๐ŸŽ๏ธ๐Ÿ’จ + +One of the key advantages of mesa-frames is its performance with large numbers of agents. Let's compare the performance of mesa and polars:""" + + +# %% +class MoneyAgentsConcise(AgentSet): + def __init__(self, n: int, model: Model): + super().__init__(model) + ## Adding the agents to the agent set + # 1. Changing the df attribute directly (not recommended, if other agents were added before, they will be lost) + """self.df = pl.DataFrame( + {"wealth": pl.ones(n, eager=True)} + )""" + # 2. Adding the dataframe with add + """self.add( + pl.DataFrame( + { + "wealth": pl.ones(n, eager=True), + } + ) + )""" + # 3. Adding the dataframe with __iadd__ + self += pl.DataFrame({"wealth": pl.ones(n, eager=True)}) + + def step(self) -> None: + # The give_money method is called + # self.give_money() + self.do("give_money") + + def give_money(self): + ## Active agents are changed to wealthy agents + # 1. Using the __getitem__ method + # self.select(self["wealth"] > 0) + # 2. Using the fallback __getattr__ method + self.select(self.wealth > 0) + + # Receiving agents are sampled (only native expressions currently supported) + other_agents = self.df.sample(n=len(self.active_agents), with_replacement=True) + + # Wealth of wealthy is decreased by 1 + # 1. Using the __setitem__ method with self.active_agents mask + # self[self.active_agents, "wealth"] -= 1 + # 2. Using the __setitem__ method with "active" mask + self["active", "wealth"] -= 1 + + # Compute the income of the other agents (only native expressions currently supported) + new_wealth = other_agents.group_by("unique_id").len() + + # Add the income to the other agents + # 1. Using the set method + """self.set( + attr_names="wealth", + values=pl.col("wealth") + new_wealth["len"], + mask=new_wealth, + )""" + + # 2. Using the __setitem__ method + self[new_wealth, "wealth"] += new_wealth["len"] + + +class MoneyAgentsNative(AgentSet): + def __init__(self, n: int, model: Model): + super().__init__(model) + self += pl.DataFrame({"wealth": pl.ones(n, eager=True)}) + + def step(self) -> None: + self.do("give_money") + + def give_money(self): + ## Active agents are changed to wealthy agents + self.select(pl.col("wealth") > 0) + + other_agents = self.df.sample(n=len(self.active_agents), with_replacement=True) + + # Wealth of wealthy is decreased by 1 + self.df = self.df.with_columns( + wealth=pl.when( + pl.col("unique_id").is_in(self.active_agents["unique_id"].implode()) + ) + .then(pl.col("wealth") - 1) + .otherwise(pl.col("wealth")) + ) + + new_wealth = other_agents.group_by("unique_id").len() + + # Add the income to the other agents + self.df = ( + self.df.join(new_wealth, on="unique_id", how="left") + .fill_null(0) + .with_columns(wealth=pl.col("wealth") + pl.col("len")) + .drop("len") + ) + + +# %% [markdown] +"""Add Mesa implementation of MoneyAgent and MoneyModel classes to test Mesa performance""" + +# %% +import mesa + + +class MesaMoneyAgent(mesa.Agent): + """An agent with fixed initial wealth.""" + + def __init__(self, model): + # Pass the parameters to the parent class. + super().__init__(model) + + # Create the agent's variable and set the initial values. + self.wealth = 1 + + def step(self): + # Verify agent has some wealth + if self.wealth > 0: + other_agent: MesaMoneyAgent = self.model.random.choice(self.model.agents) + if other_agent is not None: + other_agent.wealth += 1 + self.wealth -= 1 + + +class MesaMoneyModel(mesa.Model): + """A model with some number of agents.""" + + def __init__(self, N: int): + super().__init__() + self.num_agents = N + for _ in range(N): + self.agents.add(MesaMoneyAgent(self)) + + def step(self): + """Advance the model by one step.""" + self.agents.shuffle_do("step") + + def run_model(self, n_steps) -> None: + for _ in range(n_steps): + self.step() + + +# %% +import time + + +def run_simulation(model: MesaMoneyModel | MoneyModel, n_steps: int): + start_time = time.time() + model.run_model(n_steps) + end_time = time.time() + return end_time - start_time + + +# Compare mesa and mesa-frames implementations +n_agents_list = [10**2, 10**3 + 1, 2 * 10**3] +n_steps = 100 +print("Execution times:") +for implementation in [ + "mesa", + "mesa-frames (pl concise)", + "mesa-frames (pl native)", +]: + print(f"---------------\n{implementation}:") + for n_agents in n_agents_list: + if implementation == "mesa": + ntime = run_simulation(MesaMoneyModel(n_agents), n_steps) + elif implementation == "mesa-frames (pl concise)": + ntime = run_simulation(MoneyModel(n_agents, MoneyAgentsConcise), n_steps) + elif implementation == "mesa-frames (pl native)": + ntime = run_simulation(MoneyModel(n_agents, MoneyAgentsNative), n_steps) + + print(f" Number of agents: {n_agents}, Time: {ntime:.2f} seconds") + print("---------------") + +# %% [markdown] +""" +## Conclusion ๐ŸŽ‰ + +- All mesa-frames implementations significantly outperform the original mesa implementation. ๐Ÿ† +- The native implementation for Polars shows better performance than their concise counterparts. ๐Ÿ’ช +- The Polars native implementation shows the most impressive speed-up, ranging from 10.86x to 17.60x faster than mesa! ๐Ÿš€๐Ÿš€๐Ÿš€ +- The performance advantage of mesa-frames becomes more pronounced as the number of agents increases. ๐Ÿ“ˆ""" diff --git a/docs/general/tutorials/4_datacollector.py b/docs/general/tutorials/4_datacollector.py new file mode 100644 index 00000000..16d9837b --- /dev/null +++ b/docs/general/tutorials/4_datacollector.py @@ -0,0 +1,229 @@ +from __future__ import annotations + +# %% [markdown] +"""# Data Collector Tutorial + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/projectmesa/mesa-frames/blob/main/docs/general/user-guide/4_datacollector.ipynb) + +This notebook walks you through using the concrete `DataCollector` in `mesa-frames` to collect model- and agent-level data and write it to different storage backends: **memory, CSV, Parquet, S3, and PostgreSQL**. + +It also shows how to use **conditional triggers** and how the **schema validation** behaves for PostgreSQL.""" + +# %% [markdown] +"""## Installation (Colab or fresh env) + +Uncomment and run the next cell if you're in Colab or a clean environment.""" + +# %% +# !pip install git+https://github.com/projectmesa/mesa-frames mesa + +# %% [markdown] +"""## Minimal Example Model + +We create a tiny model using the `Model` and an `AgentSet`-style agent container. This is just to demonstrate collection APIs.""" + +# %% +from mesa_frames import Model, AgentSet, DataCollector +import polars as pl + + +class MoneyAgents(AgentSet): + def __init__(self, n: int, model: Model): + super().__init__(model) + # one column, one unit of wealth each + self += pl.DataFrame({"wealth": pl.ones(n, eager=True)}) + + def step(self) -> None: + self.select(self.wealth > 0) + receivers = self.df.sample(n=len(self.active_agents), with_replacement=True) + self["active", "wealth"] -= 1 + income = receivers.group_by("unique_id").len() + self[income["unique_id"], "wealth"] += income["len"] + + +class MoneyModel(Model): + def __init__(self, n: int): + super().__init__() + self.sets.add(MoneyAgents(n, self)) + self.dc = DataCollector( + model=self, + model_reporters={ + "total_wealth": lambda m: m.sets["MoneyAgents"].df["wealth"].sum(), + "n_agents": lambda m: len(m.sets["MoneyAgents"]), + }, + agent_reporters={ + "wealth": "wealth", # pull existing column + }, + storage="memory", # we'll switch this per example + storage_uri=None, + trigger=lambda m: m.steps % 2 + == 0, # collect every 2 steps via conditional_collect + reset_memory=True, + ) + + def step(self): + self.sets.do("step") + + def run(self, steps: int, conditional: bool = True): + for _ in range(steps): + self.step() + self.dc.conditional_collect() # or .collect if you want to collect every step regardless of trigger + + +model = MoneyModel(1000) +model.run(10) +model.dc.data # peek in-memory dataframes + +# %% [markdown] +"""## Saving the data for later use + +`DataCollector` supports multiple storage backends. +Files are saved with **step number** and **batch number** (e.g., `model_step10_batch2.csv`) so multiple collects at the same step donโ€™t overwrite. + +- **CSV:** `storage="csv"` โ†’ writes `model_step{n}_batch{k}.csv`, easy to open anywhere. +- **Parquet:** `storage="parquet"` โ†’ compressed, efficient for large datasets. +- **S3:** `storage="S3-csv"`/`storage="S3-parquet"` โ†’ saves CSV/Parquet directly to Amazon S3. +- **PostgreSQL:** `storage="postgresql"` โ†’ inserts results into `model_data` and `agent_data` tables for querying.""" + +# %% [markdown] +"""## Writing to Local CSV + +Switch the storage to `csv` and provide a folder path. Files are written as `model_step{n}.csv` and `agent_step{n}.csv`.""" + +# %% +import os + +os.makedirs("./data_csv", exist_ok=True) +model_csv = MoneyModel(1000) +model_csv.dc = DataCollector( + model=model_csv, + model_reporters={ + "total_wealth": lambda m: m.sets["MoneyAgents"].df["wealth"].sum(), + "n_agents": lambda m: len(m.sets["MoneyAgents"]), + }, + agent_reporters={ + "wealth": "wealth", + }, + storage="csv", # saving as csv + storage_uri="./data_csv", + trigger=lambda m: m._steps % 2 == 0, + reset_memory=True, +) +model_csv.run(10) +model_csv.dc.flush() +os.listdir("./data_csv") + +# %% [markdown] +"""## Writing to Local Parquet + +Use `parquet` for columnar output.""" + +# %% +os.makedirs("./data_parquet", exist_ok=True) +model_parq = MoneyModel(1000) +model_parq.dc = DataCollector( + model=model_parq, + model_reporters={ + "total_wealth": lambda m: m.sets["MoneyAgents"].df["wealth"].sum(), + "n_agents": lambda m: len(m.sets["MoneyAgents"]), + }, + agent_reporters={ + "wealth": "wealth", + }, + storage="parquet", # save as parquet + storage_uri="data_parquet", + trigger=lambda m: m._steps % 2 == 0, + reset_memory=True, +) +model_parq.run(10) +model_parq.dc.flush() +os.listdir("./data_parquet") + +# %% [markdown] +"""## Writing to Amazon S3 (CSV or Parquet) + +Set AWS credentials via environment variables or your usual config. Then choose `S3-csv` or `S3-parquet` and pass an S3 URI (e.g., `s3://my-bucket/experiments/run-1`). + +> **Note:** This cell requires network access & credentials when actually run.""" + +# %% +model_s3 = MoneyModel(1000) +model_s3.dc = DataCollector( + model=model_s3, + model_reporters={ + "total_wealth": lambda m: m.sets["MoneyAgents"].df["wealth"].sum(), + "n_agents": lambda m: len(m.sets["MoneyAgents"]), + }, + agent_reporters={ + "wealth": "wealth", + }, + storage="S3-csv", # save as csv in S3 + storage_uri="s3://my-bucket/experiments/run-1", # change it to required path + trigger=lambda m: m._steps % 2 == 0, + reset_memory=True, +) +model_s3.run(10) +model_s3.dc.flush() + +# %% [markdown] +"""## Writing to PostgreSQL + +PostgreSQL requires that the target tables exist and that the expected reporter columns are present. The collector will validate tables/columns up front and raise descriptive errors if something is missing. + +Below is a minimal schema example. Adjust columns to your configured reporters.""" + +# %% +DDL_MODEL = r""" +CREATE SCHEMA IF NOT EXISTS public; +CREATE TABLE IF NOT EXISTS public.model_data ( + step INTEGER, + seed VARCHAR, + total_wealth BIGINT, + n_agents INTEGER +); +""" +DDL_AGENT = r""" +CREATE TABLE IF NOT EXISTS public.agent_data ( + step INTEGER, + seed VARCHAR, + unique_id BIGINT, + wealth BIGINT +); +""" +print(DDL_MODEL) +print(DDL_AGENT) + +# %% [markdown] +"""After creating the tables (outside this notebook or via a DB connection cell), configure and flush:""" + +# %% +POSTGRES_URI = "postgresql://user:pass@localhost:5432/mydb" +m_pg = MoneyModel(300) +m_pg.dc._storage = "postgresql" +m_pg.dc._storage_uri = POSTGRES_URI +m_pg.run(6) +m_pg.dc.flush() + +# %% [markdown] +"""## Triggers & Conditional Collection + +The collector accepts a `trigger: Callable[[Model], bool]`. When using `conditional_collect()`, the collector checks the trigger and collects only if it returns `True`. + +You can always call `collect()` to gather data unconditionally.""" + +# %% +m = MoneyModel(100) +m.dc.trigger = lambda model: model._steps % 3 == 0 # every 3rd step +m.run(10, conditional=True) +m.dc.data["model"].head() + +# %% [markdown] +"""## Troubleshooting + +- **ValueError: Please define a storage_uri** โ€” for non-memory backends you must set `_storage_uri`. +- **Missing columns in table** โ€” check the PostgreSQL error text; create/alter the table to include the columns for your configured `model_reporters` and `agent_reporters`, plus required `step` and `seed`. +- **Permissions/credentials errors** (S3/PostgreSQL) โ€” ensure correct IAM/credentials or database permissions.""" + +# %% [markdown] +"""--- +*Generated on 2025-08-30.*""" diff --git a/docs/general/user-guide/0_getting-started.md b/docs/general/user-guide/0_getting-started.md index 1edc1587..caebc1c9 100644 --- a/docs/general/user-guide/0_getting-started.md +++ b/docs/general/user-guide/0_getting-started.md @@ -15,20 +15,20 @@ Objects can be easily subclassed to respect mesa's object-oriented philosophy. ### Vectorized Operations โšก -mesa-frames leverages the power of vectorized operations provided by DataFrame libraries: +`mesa-frames` leverages **Polars** to replace Python loops with **column-wise expressions** executed in native Rust. +This allows you to update all agents simultaneously, the main source of `mesa-frames`' performance advantage. -- Operations are performed on entire columns of data at once -- This approach is significantly faster than iterating over individual agents -- Complex behaviors can be expressed in fewer lines of code +Unlike traditional `mesa` models, where the **activation order** of agents can affect results (see [Comer, 2014](http://mars.gmu.edu/bitstream/handle/1920/9070/Comer_gmu_0883E_10539.pdf)), +`mesa-frames` processes all agents **in parallel by default**. +This removes order-dependent effects, though you should handle conflicts explicitly when sequential logic is required. -You should never use loops to iterate through your agents. Instead, use vectorized operations and implemented methods. If you need to loop, loop through vectorized operations (see the advanced tutorial SugarScape IG for more information). +!!! tip "Best practice" + Always start by expressing agent logic in a vectorized form. + Fall back to loops only when ordering or conflict resolution is essential. -It's important to note that in traditional `mesa` models, the order in which agents are activated can significantly impact the results of the model (see [Comer, 2014](http://mars.gmu.edu/bitstream/handle/1920/9070/Comer_gmu_0883E_10539.pdf)). `mesa-frames`, by default, doesn't have this issue as all agents are processed simultaneously. However, this comes with the trade-off of needing to carefully implement conflict resolution mechanisms when sequential processing is required. We'll discuss how to handle these situations later in this guide. +For a deeper understanding of vectorization and why it accelerates computation, see: -Check out these resources to understand vectorization and why it speeds up the code: - -- [What is vectorization?](https://stackoverflow.com/a/1422181) -- [Vectorization Explained, Step by Step](https://machinelearningcompass.com/machine_learning_math/vectorization/) +- [How vectorization speeds up your Python code โ€” PythonSpeed](https://pythonspeed.com/articles/vectorization-python) Here's a comparison between mesa-frames and mesa: @@ -42,7 +42,7 @@ Here's a comparison between mesa-frames and mesa: self.select(self.wealth > 0) # Receiving agents are sampled (only native expressions currently supported) - other_agents = self.sets.sample( + other_agents = self.df.sample( n=len(self.active_agents), with_replacement=True ) @@ -92,10 +92,10 @@ If you're familiar with mesa, this guide will help you understand the key differ }) def step(self): givers = self.wealth > 0 - receivers = self.sets.sample(n=len(self.active_agents)) + receivers = self.df.sample(n=len(self.active_agents), with_replacement=True) self[givers, "wealth"] -= 1 - new_wealth = receivers.groupby("unique_id").count() - self[new_wealth["unique_id"], "wealth"] += new_wealth["count"] + new_wealth = receivers.group_by("unique_id").len() + self[new_wealth["unique_id"], "wealth"] += new_wealth["len"] ``` === "mesa" @@ -146,12 +146,85 @@ If you're familiar with mesa, this guide will help you understand the key differ self.schedule.step() ``` -### Transition Tips ๐Ÿ’ก +### From Imperative Code to Behavioral Rules ๐Ÿ’ญ + +When scientists describe an ABM-like process they typically write a **system of state-transition functions**: + +$$ +x_i(t+1) = f_i\big(x_i(t),\; \mathcal{N}(i,t),\; E(t)\big) +$$ + +Here, $x_i(t)$ is the agentโ€™s state, $\mathcal{N}(i,t)$ its neighborhood or local environment, and $E(t)$ a global environment; $f_i$ is the behavioral law. + +In classic `mesa`, agent behavior is implemented through explicit loops: each agent individually gathers information from its neighbors, computes its next state, and often stores this in a buffer to ensure synchronous updates. The behavioral law $f_i$ is distributed across multiple steps: neighbor iteration, temporary buffers, and scheduling logic, resulting in procedural, step-by-step control flow. + +In `mesa-frames`, these stages are unified into a single vectorized transformation. Agent interactions, state transitions, and updates are expressed as DataFrame operations (such as joins, group-bys, and column expressions) allowing all agents to process perceptions and commit actions simultaneously. This approach centralizes the behavioral law $f_i$ into concise, declarative rules, improving clarity and performance. + +#### Example: Network contagion (Linear Threshold) + +Behavioral rule: a node activates if the number of active neighbors โ‰ฅ its threshold. + +=== "mesa-frames" + + Single vectorized transformation. A join brings in source activity, a group-by aggregates exposures per destination, and a column expression applies the activation equation and commits in one pass, no explicit loops or staging structure needed. + + ```python + class Nodes(AgentSet): + # self.df columns: agent_id, active (bool), theta (int) + # self.model.space.edges: DataFrame[src, dst] + def step(self): + E = self.model.space.edges # [src, dst] + # Exposure: active neighbors per dst (vectorized join + groupby) + exposures = ( + E.join( + self.df.select(pl.col("agent_id").alias("src"), + pl.col("active").alias("src_active")), + on="src", how="left" + ) + .with_columns(pl.col("src_active").fill_null(False)) + .group_by("dst") + .agg(pl.col("src_active").sum().alias("k_active")) + ) + # Behavioral equation applied to all agents, committed in-place + self.df = ( + self.df + .join(exposures, left_on="agent_id", right_on="dst", how="left") + .with_columns(pl.col("k_active").fill_null(0)) + .with_columns( + (pl.col("active") | (pl.col("k_active") >= pl.col("theta"))) + .alias("active") + ) + .drop(["k_active", "dst"]) + ) + ``` + +=== "mesa" + + Two-phase imperative procedure. Each agent loops over its neighbors to count active ones (exposure), stores a provisional next state to avoid premature mutation, then a separate pass commits all buffered states for synchronicity. + + ```python + class Node(mesa.Agent): + def step(self): + # (1) Gather exposure: count active neighbors right now + k_active = sum( + 1 for j in self.model.G.neighbors(self.unique_id) + if self.model.id2agent[j].active + ) + # (2) Compute next state (don't mutate yet to stay synchronous) + self.next_active = self.active or (k_active >= self.theta) + + # Second pass (outside the agent method) performs the commit: + for a in model.agents: + a.active = a.next_active + ``` -1. **Think in Sets ๐ŸŽญ**: Instead of individual agents, think about operations on groups of agents. -2. **Leverage DataFrame Operations ๐Ÿ› ๏ธ**: Familiarize yourself with Polars operations for efficient agent manipulation. -3. **Vectorize Logic ๐Ÿš…**: Convert loops and conditionals to vectorized operations where possible. -4. **Use AgentSets ๐Ÿ“ฆ**: Group similar agents into AgentSets instead of creating many individual agent classes. +!!! tip "Transition tips โ€” quick summary" + 1. Think in sets: operate on AgentSets/DataFrames, not per-agent objects. + 2. Write transitions as Polars column expressions; avoid Python loops. + 3. Use joins + group-bys to compute interactions/exposure across relations. + 4. Commit state synchronously in one vectorized pass. + 5. Group similar agents into one AgentSet with typed columns. + 6. Use UDFs or staged/iterative patterns only for true race/conflict cases. ### Handling Race Conditions ๐Ÿ @@ -163,4 +236,4 @@ When simultaneous activation is not possible, you need to handle race conditions 2. **Looping Mechanism ๐Ÿ”**: Implement a looping mechanism on vectorized operations. -For a more detailed implementation of handling race conditions, please refer to the `examples/sugarscape-ig` in the mesa-frames repository. This example demonstrates how to implement the Sugarscape model with instantaneous growback, which requires careful handling of sequential agent actions. +For a more detailed implementation of handling race conditions, see the [Advanced Tutorial](../tutorials/3_advanced_tutorial.ipynb). It walks through the Sugarscape model with instantaneous growback and shows practical patterns for staged vectorization and conflict resolution. diff --git a/mkdocs.yml b/mkdocs.yml index 8a462881..40afb584 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,9 @@ # Project information -site_name: mesa-frames +site_name: mesa-frames documentation site_url: https://projectmesa.github.io/mesa-frames repo_url: https://github.com/projectmesa/mesa-frames repo_name: projectmesa/mesa-frames +edit_uri: edit/main/docs/general/ docs_dir: docs/general # Theme configuration @@ -40,12 +41,17 @@ theme: code: Roboto Mono icon: repo: fontawesome/brands/github + # Logo (PNG) + logo: https://raw.githubusercontent.com/projectmesa/mesa/main/docs/images/mesa_logo.png + # Favicon (ICO) + favicon: https://raw.githubusercontent.com/projectmesa/mesa/main/docs/images/mesa_logo.ico # Plugins plugins: - search - mkdocs-jupyter: - execute: true # Ensures the notebooks run and generate output + execute: false # Ensures the notebooks run and generate output + include: ["*.ipynb"] # Restrict processing to notebooks only (avoid executing raw .py tutorial files) - git-revision-date-localized: enable_creation_date: true - minify: @@ -92,10 +98,9 @@ markdown_extensions: # Extra JavaScript and CSS for rendering extra_javascript: - - javascripts/mathjax.js - - https://polyfill.io/v3/polyfill.min.js?features=es6 - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js +# Custom CSS for branding (brand-core then material adapter) # Customization extra: social: @@ -110,12 +115,12 @@ nav: - User Guide: - Getting Started: user-guide/0_getting-started.md - Classes: user-guide/1_classes.md - - Introductory Tutorial: user-guide/2_introductory-tutorial.ipynb - - Data Collector Tutorial: user-guide/4_datacollector.ipynb - - Advanced Tutorial: user-guide/3_advanced-tutorial.md - - Benchmarks: user-guide/4_benchmarks.md + - Tutorials: + - Introductory Tutorial: tutorials/2_introductory_tutorial.ipynb + - Advanced Tutorial: tutorials/3_advanced_tutorial.ipynb + - Data Collector Tutorial: tutorials/4_datacollector.ipynb - API Reference: api/index.html - Contributing: - Contribution Guide: contributing.md - - Development Guidelines: development/index.md - Roadmap: roadmap.md + - Changelog: changelog.md diff --git a/pyproject.toml b/pyproject.toml index 5bca2fcf..7f63a94c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ test = [ docs = [ { include-group = "typechecking" }, + "jupytext>=1.17.3", "mkdocs-material>=9.6.14", "mkdocs-jupyter>=0.25.1", "mkdocs-git-revision-date-localized-plugin>=1.4.7", @@ -77,7 +78,6 @@ docs = [ "sphinx-copybutton>=0.5.2", "sphinx-design>=0.6.1", "autodocsumm>=0.2.14", - "perfplot>=0.10.2", "seaborn>=0.13.2", "sphinx-autobuild>=2025.8.25", "mesa>=3.2.0", diff --git a/uv.lock b/uv.lock index ee2f031c..4db6f107 100644 --- a/uv.lock +++ b/uv.lock @@ -1161,19 +1161,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, ] -[[package]] -name = "matplotx" -version = "0.3.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "matplotlib" }, - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/01/0e6938bb717fa7722d6d81336c62de71b815ce73e382aa1873a1e68ccc93/matplotx-0.3.10.tar.gz", hash = "sha256:b6926ce5274cf5da966cb46b90a8c7fefb761478c6c85c8f7ed3ee8ec90e86e5", size = 24041, upload-time = "2022-08-22T14:22:56.374Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/ef/e8a30503ae0c26681a9610c7f0be58646bea8119b98cc65c47661abc27a3/matplotx-0.3.10-py3-none-any.whl", hash = "sha256:4d7adafdb001c771d66d9362bb8ca99fcaed15319259223a714f36793dfabbb8", size = 25099, upload-time = "2022-08-22T14:22:54.733Z" }, -] - [[package]] name = "mdit-py-plugins" version = "0.5.0" @@ -1235,6 +1222,7 @@ dependencies = [ dev = [ { name = "autodocsumm" }, { name = "beartype" }, + { name = "jupytext" }, { name = "mesa" }, { name = "mkdocs-git-revision-date-localized-plugin" }, { name = "mkdocs-include-markdown-plugin" }, @@ -1243,7 +1231,6 @@ dev = [ { name = "mkdocs-minify-plugin" }, { name = "numba" }, { name = "numpydoc" }, - { name = "perfplot" }, { name = "pre-commit" }, { name = "pydata-sphinx-theme" }, { name = "pytest" }, @@ -1259,6 +1246,7 @@ dev = [ docs = [ { name = "autodocsumm" }, { name = "beartype" }, + { name = "jupytext" }, { name = "mesa" }, { name = "mkdocs-git-revision-date-localized-plugin" }, { name = "mkdocs-include-markdown-plugin" }, @@ -1266,7 +1254,6 @@ docs = [ { name = "mkdocs-material" }, { name = "mkdocs-minify-plugin" }, { name = "numpydoc" }, - { name = "perfplot" }, { name = "pydata-sphinx-theme" }, { name = "seaborn" }, { name = "sphinx" }, @@ -1298,6 +1285,7 @@ requires-dist = [ dev = [ { name = "autodocsumm", specifier = ">=0.2.14" }, { name = "beartype", specifier = ">=0.21.0" }, + { name = "jupytext", specifier = ">=1.17.3" }, { name = "mesa", specifier = ">=3.2.0" }, { name = "mkdocs-git-revision-date-localized-plugin", specifier = ">=1.4.7" }, { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.5" }, @@ -1306,7 +1294,6 @@ dev = [ { name = "mkdocs-minify-plugin", specifier = ">=0.8.0" }, { name = "numba", specifier = ">=0.60.0" }, { name = "numpydoc", specifier = ">=1.8.0" }, - { name = "perfplot", specifier = ">=0.10.2" }, { name = "pre-commit", specifier = ">=4.2.0" }, { name = "pydata-sphinx-theme", specifier = ">=0.16.1" }, { name = "pytest", specifier = ">=8.3.5" }, @@ -1322,6 +1309,7 @@ dev = [ docs = [ { name = "autodocsumm", specifier = ">=0.2.14" }, { name = "beartype", specifier = ">=0.21.0" }, + { name = "jupytext", specifier = ">=1.17.3" }, { name = "mesa", specifier = ">=3.2.0" }, { name = "mkdocs-git-revision-date-localized-plugin", specifier = ">=1.4.7" }, { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.5" }, @@ -1329,7 +1317,6 @@ docs = [ { name = "mkdocs-material", specifier = ">=9.6.14" }, { name = "mkdocs-minify-plugin", specifier = ">=0.8.0" }, { name = "numpydoc", specifier = ">=1.8.0" }, - { name = "perfplot", specifier = ">=0.10.2" }, { name = "pydata-sphinx-theme", specifier = ">=0.16.1" }, { name = "seaborn", specifier = ">=0.13.2" }, { name = "sphinx", specifier = ">=7.4.7" }, @@ -1730,21 +1717,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] -[[package]] -name = "perfplot" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "matplotlib" }, - { name = "matplotx" }, - { name = "numpy" }, - { name = "rich" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/97/41/51d8b9caa150a050de16a229f627e4b37515dbff0075259e4e75aff7218b/perfplot-0.10.2.tar.gz", hash = "sha256:d76daa72334564b5c8825663f24d15db55ea33e938b34595a146e5e44ed87e41", size = 25044, upload-time = "2022-03-03T15:56:37.392Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/85/ffaf2c1f92d17916c089a5c860d23b3117398f19f467fd1de1026d03aebc/perfplot-0.10.2-py3-none-any.whl", hash = "sha256:545ce0f7f22509ad00092d79a794cdc6e9805383e6cedab2bfed3519a7ef4e19", size = 21198, upload-time = "2022-03-03T15:56:35.388Z" }, -] - [[package]] name = "pexpect" version = "4.9.0" @@ -2275,19 +2247,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] -[[package]] -name = "rich" -version = "14.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, -] - [[package]] name = "roman-numerals-py" version = "3.1.0"