Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,39 @@ This guide contains breaking changes between major Mesa versions and how to reso

Non-breaking changes aren't included, for those see our [Release history](https://github.com/projectmesa/mesa/releases).

## Mesa 3.4.0

### batch run
`batch_run` has been updated to offer explicit control over the random seeds that are used to run
multiple replications of a given experiment. For this a new keyword argument, `rng` has been
added and `iterations` will issue a `DeprecationWarning`. The new `rng` keyword argument
takes a valid value for seeding or a list of valid values. If you want to run multiple iterations/replications
of a given experiment, you need to pass the required seeds explicitly.

Below is a simple example of the new recommended usage of `batch_run`. Note how we first
create 5 random integers which we then use as seed values for the new `rng` keyword argument.

```python
import numpy as np
import sys

# let's create 5 random integers
rng = np.random.default_rng(42)
rng_values = rng.integers(0, sys.maxsize, size=(5,))

results = mesa.batch_run(
MoneyModel,
parameters=params,
rng=rng_values.tolist(), # we pass the 5 seed values to rng
max_steps=100,
number_processes=1,
data_collection_period=1,
display_progress=True,
)


```

## Mesa 3.3.0

Mesa 3.3.0 is a visualization upgrade introducing a new and improved API, full support for both `altair` and `matplotlib` backends, and resolving several recurring issues from previous versions.
Expand Down
113 changes: 97 additions & 16 deletions docs/tutorials/9_batch_run.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 2,
"metadata": {
"collapsed": false,
"jupyter": {
Expand Down Expand Up @@ -94,7 +94,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -162,7 +162,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -199,8 +199,8 @@
"* `number_processes`\n",
" If not specified, defaults to 1. Set it to `None` to use all the available processors.\n",
" Note: Multiprocessing does make debugging challenging. If your parameter sweeps are resulting in unexpected errors set `number_processes=1`.\n",
"* `iterations`\n",
" The number of iterations to run each parameter combination for. Optional. If not specified, defaults to 1.\n",
"* `rng`\n",
" a valid value or iterable of values for seeding the random number generator in the model. Defaults to a single None value meaning the model is ran once.\n",
"* `data_collection_period`\n",
" The length of the period (number of steps) after which the model and agent reporters collect data. Optional. If not specified, defaults to -1, i.e. only at the end of each episode.\n",
"* `max_steps`\n",
Expand All @@ -227,16 +227,43 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"\n",
"rng = np.random.default_rng(42)\n",
"seed_values = rng.integers(0, sys.maxsize, size=(5,))"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "2a208c5972c84a9ebc77fbc828763c94",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
" 0%| | 0/100 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"params = {\"width\": 10, \"height\": 10, \"n\": range(5, 105, 5)}\n",
"\n",
"results = mesa.batch_run(\n",
" MoneyModel,\n",
" parameters=params,\n",
" iterations=5,\n",
" rng=seed_values.tolist(),\n",
" max_steps=100,\n",
" number_processes=1,\n",
" data_collection_period=1,\n",
Expand All @@ -260,9 +287,18 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 14,
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"The results have 525100 rows.\n",
"The columns of the data frame are ['RunId', 'iteration', 'Step', 'width', 'height', 'n', 'seed', 'Gini', 'AgentID', 'Wealth', 'Steps_not_given'].\n"
]
}
],
"source": [
"results_df = pd.DataFrame(results)\n",
"print(f\"The results have {len(results)} rows.\")\n",
Expand All @@ -278,7 +314,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 15,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -305,7 +341,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 16,
"metadata": {
"collapsed": false,
"jupyter": {
Expand Down Expand Up @@ -336,9 +372,28 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 17,
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" Step AgentID Wealth\n",
" 0 NaN NaN\n",
" 1 1.0 1.0\n",
" 1 2.0 1.0\n",
" 1 3.0 1.0\n",
" 1 4.0 1.0\n",
" ... ... ...\n",
" 100 6.0 1.0\n",
" 100 7.0 1.0\n",
" 100 8.0 1.0\n",
" 100 9.0 2.0\n",
" 100 10.0 0.0\n"
]
}
],
"source": [
"# First, we filter the results\n",
"one_episode_wealth = results_df[(results_df.n == 10) & (results_df.iteration == 2)]\n",
Expand All @@ -364,9 +419,28 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 18,
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" Step Gini\n",
" 1 0.00\n",
" 2 0.00\n",
" 3 0.00\n",
" 4 0.00\n",
" 5 0.00\n",
" ... ...\n",
" 96 0.18\n",
" 97 0.18\n",
" 98 0.18\n",
" 99 0.18\n",
" 100 0.18\n"
]
}
],
"source": [
"results_one_episode = results_df[\n",
" (results_df.n == 10) & (results_df.iteration == 1) & (results_df.AgentID == 1)\n",
Expand All @@ -391,6 +465,13 @@
"\n",
"[Dragulescu2002] Drăgulescu, Adrian A., and Victor M. Yakovenko. “Statistical Mechanics of Money, Income, and Wealth: A Short Survey.” arXiv Preprint Cond-mat/0211175, 2002. http://arxiv.org/abs/cond-mat/0211175."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
Expand All @@ -410,7 +491,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.5"
"version": "3.12.2"
},
"widgets": {
"state": {},
Expand Down
39 changes: 35 additions & 4 deletions mesa/batchrunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,35 @@

"""

import inspect
import itertools
import multiprocessing
from collections.abc import Iterable, Mapping
import warnings
from collections.abc import Iterable, Mapping, Sequence
from functools import partial
from multiprocessing import Pool
from typing import Any

import numpy as np
from tqdm.auto import tqdm

from mesa.model import Model

multiprocessing.set_start_method("spawn", force=True)

SeedLike = int | np.integer | Sequence[int] | np.random.SeedSequence


def batch_run(
model_cls: type[Model],
parameters: Mapping[str, Any | Iterable[Any]],
# We still retain the Optional[int] because users may set it to None (i.e. use all CPUs)
number_processes: int | None = 1,
iterations: int = 1,
iterations: int | None = None,
data_collection_period: int = -1,
max_steps: int = 1000,
display_progress: bool = True,
rng: SeedLike | Iterable[SeedLike] | None = None,
) -> list[dict[str, Any]]:
"""Batch run a mesa model with a set of parameter values.

Expand All @@ -62,6 +68,7 @@ def batch_run(
data_collection_period (int, optional): Number of steps after which data gets collected, by default -1 (end of episode)
max_steps (int, optional): Maximum number of model steps after which the model halts, by default 1000
display_progress (bool, optional): Display batch run process, by default True
rng : a valid value or iterable of values for seeding the random number generator in the model

Returns:
List[Dict[str, Any]]
Expand All @@ -70,11 +77,34 @@ def batch_run(
batch_run assumes the model has a `datacollector` attribute that has a DataCollector object initialized.

"""
if iterations is not None and rng is not None:
raise ValueError(
"you cannot use both iterations and rng at the same time. Please only use rng."
)
if iterations is not None:
warnings.warn(
"iterations is deprecated, please use rng instead",
DeprecationWarning,
stacklevel=2,
)
rng = [
None,
] * iterations
if not isinstance(rng, Iterable):
rng = [rng]

# establish to use seed or rng as name for parameter
model_parameters = inspect.signature(Model).parameters
rng_kwarg_name = "rng"
if "seed" in model_parameters:
rng_kwarg_name = "seed"
Comment on lines +95 to +99
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior should be concisely explained in the batch_run docstring and tutorial

Copy link
Member

@quaquel quaquel Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Using both seed and rng on a model already gives an error. This just ensures that the seed is set to the keyword argument specified by the user in their model class, whether it is seed or rng.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, that's true. Give me a moment to get up to speed with it myself.


runs_list = []
run_id = 0
for iteration in range(iterations):
for i, rng_i in enumerate(rng):
for kwargs in _make_model_kwargs(parameters):
runs_list.append((run_id, iteration, kwargs))
kwargs[rng_kwarg_name] = rng_i
runs_list.append((run_id, i, kwargs))
run_id += 1

process_func = partial(
Expand Down Expand Up @@ -170,6 +200,7 @@ def _model_run_func(
Return model_data, agent_data from the reporters
"""
run_id, iteration, kwargs = run

model = model_cls(**kwargs)
while model.running and model.steps <= max_steps:
model.step()
Expand Down
Loading