From 728456c14949ce3b29550e16fbc831c96af5cd02 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven <15776622+EwoutH@users.noreply.github.com> Date: Tue, 7 Oct 2025 20:12:02 +0200 Subject: [PATCH 1/6] Fix: Auto-increment seed across batch_run iterations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using batch_run() with a single seed value and multiple iterations, all iterations were using the same seed, producing identical results instead of independent replications. This defeats the purpose of running multiple iterations. This commit modifies _model_run_func to automatically increment the seed for each iteration (seed, seed+1, seed+2, ...) when a numeric seed is provided. This ensures: - Each iteration produces different random outcomes - Results remain reproducible (same base seed → same sequence) - Backward compatibility with seed arrays (no modification if seed is already an iterable passed via parameters) - Unchanged behavior when no seed is specified (each iteration gets random seed from OS) The fix only applies when: 1. A 'seed' parameter exists in kwargs 2. The seed value is not None 3. The iteration number is > 0 4. The seed is a single numeric value (int/float, not bool) --- mesa/batchrunner.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mesa/batchrunner.py b/mesa/batchrunner.py index 83bc79061b6..4eb801cf967 100644 --- a/mesa/batchrunner.py +++ b/mesa/batchrunner.py @@ -170,6 +170,14 @@ def _model_run_func( Return model_data, agent_data from the reporters """ run_id, iteration, kwargs = run + + # Handle seed uniqueness across iterations + if 'seed' in kwargs and kwargs['seed'] is not None and iteration > 0: + seed_value = kwargs['seed'] + if isinstance(seed_value, (int, float)) and not isinstance(seed_value, bool): + kwargs = kwargs.copy() + kwargs['seed'] = int(seed_value) + iteration + model = model_cls(**kwargs) while model.running and model.steps <= max_steps: model.step() From c03c6fa7a3e62bb49adb1e7d5e2225163289093e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:18:10 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/batchrunner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mesa/batchrunner.py b/mesa/batchrunner.py index 4eb801cf967..5499aa2d5f9 100644 --- a/mesa/batchrunner.py +++ b/mesa/batchrunner.py @@ -172,11 +172,11 @@ def _model_run_func( run_id, iteration, kwargs = run # Handle seed uniqueness across iterations - if 'seed' in kwargs and kwargs['seed'] is not None and iteration > 0: - seed_value = kwargs['seed'] + if "seed" in kwargs and kwargs["seed"] is not None and iteration > 0: + seed_value = kwargs["seed"] if isinstance(seed_value, (int, float)) and not isinstance(seed_value, bool): kwargs = kwargs.copy() - kwargs['seed'] = int(seed_value) + iteration + kwargs["seed"] = int(seed_value) + iteration model = model_cls(**kwargs) while model.running and model.steps <= max_steps: From 7f456af959eb67a4ba575e6374e27b63a889dea2 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 12 Nov 2025 10:38:36 +0100 Subject: [PATCH 3/6] add rng as kwarg and deprecate iterations --- mesa/batchrunner.py | 30 +++++++---- tests/test_batch_run.py | 112 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 12 deletions(-) diff --git a/mesa/batchrunner.py b/mesa/batchrunner.py index 5499aa2d5f9..9b53458c385 100644 --- a/mesa/batchrunner.py +++ b/mesa/batchrunner.py @@ -30,27 +30,32 @@ 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. @@ -62,6 +67,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]] @@ -70,11 +76,20 @@ 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 cannont 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 rgn instead", DeprecationWarning, stacklevel=2) + rng = [None,] * iterations + if not isinstance(rng, Iterable): + rng = [rng] + 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"] = rng_i + runs_list.append((run_id, i, kwargs)) run_id += 1 process_func = partial( @@ -171,13 +186,6 @@ def _model_run_func( """ run_id, iteration, kwargs = run - # Handle seed uniqueness across iterations - if "seed" in kwargs and kwargs["seed"] is not None and iteration > 0: - seed_value = kwargs["seed"] - if isinstance(seed_value, (int, float)) and not isinstance(seed_value, bool): - kwargs = kwargs.copy() - kwargs["seed"] = int(seed_value) + iteration - model = model_cls(**kwargs) while model.running and model.steps <= max_steps: model.step() diff --git a/tests/test_batch_run.py b/tests/test_batch_run.py index aec13bc4ccc..6e4fe831897 100644 --- a/tests/test_batch_run.py +++ b/tests/test_batch_run.py @@ -1,4 +1,5 @@ """Test Batchrunner.""" +import pytest import mesa from mesa.agent import Agent @@ -130,7 +131,7 @@ def step(self): # noqa: D102 def test_batch_run(): # noqa: D103 - result = mesa.batch_run(MockModel, {}, number_processes=2) + result = mesa.batch_run(MockModel, {}, number_processes=2, rng=42) assert result == [ { "RunId": 0, @@ -140,6 +141,7 @@ def test_batch_run(): # noqa: D103 "AgentID": 1, "agent_id": 1, "agent_local": 250.0, + "rng":42, }, { "RunId": 0, @@ -149,6 +151,7 @@ def test_batch_run(): # noqa: D103 "AgentID": 2, "agent_id": 2, "agent_local": 250.0, + "rng": 42, }, { "RunId": 0, @@ -158,9 +161,114 @@ def test_batch_run(): # noqa: D103 "AgentID": 3, "agent_id": 3, "agent_local": 250.0, + "rng": 42, }, ] + result = mesa.batch_run(MockModel, {}, number_processes=2, iterations=1) + assert result == [ + { + "RunId": 0, + "iteration": 0, + "Step": 1000, + "reported_model_param": 42, + "AgentID": 1, + "agent_id": 1, + "agent_local": 250.0, + "rng":None, + }, + { + "RunId": 0, + "iteration": 0, + "Step": 1000, + "reported_model_param": 42, + "AgentID": 2, + "agent_id": 2, + "agent_local": 250.0, + "rng": None, + }, + { + "RunId": 0, + "iteration": 0, + "Step": 1000, + "reported_model_param": 42, + "AgentID": 3, + "agent_id": 3, + "agent_local": 250.0, + "rng": None, + }, + ] + + + result = mesa.batch_run(MockModel, {}, number_processes=2, rng=[42, 31415]) + assert result == [ + { + "RunId": 0, + "iteration": 0, + "Step": 1000, + "reported_model_param": 42, + "AgentID": 1, + "agent_id": 1, + "agent_local": 250.0, + "rng":42, + }, + { + "RunId": 0, + "iteration": 0, + "Step": 1000, + "reported_model_param": 42, + "AgentID": 2, + "agent_id": 2, + "agent_local": 250.0, + "rng": 42, + }, + { + "RunId": 0, + "iteration": 0, + "Step": 1000, + "reported_model_param": 42, + "AgentID": 3, + "agent_id": 3, + "agent_local": 250.0, + "rng": 42, + }, + { + "RunId": 1, + "iteration": 1, + "Step": 1000, + "reported_model_param": 42, + "AgentID": 1, + "agent_id": 1, + "agent_local": 250.0, + "rng": 31415, + }, + { + "RunId": 1, + "iteration": 1, + "Step": 1000, + "reported_model_param": 42, + "AgentID": 2, + "agent_id": 2, + "agent_local": 250.0, + "rng": 31415, + }, + { + "RunId": 1, + "iteration": 1, + "Step": 1000, + "reported_model_param": 42, + "AgentID": 3, + "agent_id": 3, + "agent_local": 250.0, + "rng": 31415, + }, + + ] + + with pytest.raises(ValueError): + mesa.batch_run(MockModel, {}, number_processes=2, rng=42, iterations=1) + + def test_batch_run_with_params(): # noqa: D103 mesa.batch_run( @@ -185,6 +293,7 @@ def test_batch_run_no_agent_reporters(): # noqa: D103 "Step": 1000, "enable_agent_reporters": False, "reported_model_param": 42, + "rng": None, } ] @@ -208,6 +317,7 @@ def test_batch_run_unhashable_param(): # noqa: D103 "agent_local": 250.0, "n_agents": 2, "variable_model_params": {"key": "value"}, + "rng": None, } assert result == [ From 10136f97a30ae95c69f7df6a3bbb3000d733d6dc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:39:04 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/batchrunner.py | 16 ++++++++++++---- tests/test_batch_run.py | 10 ++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/mesa/batchrunner.py b/mesa/batchrunner.py index 9b53458c385..6d9fcde5c9c 100644 --- a/mesa/batchrunner.py +++ b/mesa/batchrunner.py @@ -51,7 +51,7 @@ def batch_run( 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|None = None, + iterations: int | None = None, data_collection_period: int = -1, max_steps: int = 1000, display_progress: bool = True, @@ -77,10 +77,18 @@ def batch_run( """ if iterations is not None and rng is not None: - raise ValueError("you cannont use both iterations and rng at the same time. Please only use rng.") + raise ValueError( + "you cannont 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 rgn instead", DeprecationWarning, stacklevel=2) - rng = [None,] * iterations + warnings.warn( + "iterations is deprecated, please use rgn instead", + DeprecationWarning, + stacklevel=2, + ) + rng = [ + None, + ] * iterations if not isinstance(rng, Iterable): rng = [rng] diff --git a/tests/test_batch_run.py b/tests/test_batch_run.py index 6e4fe831897..dcbc63079c2 100644 --- a/tests/test_batch_run.py +++ b/tests/test_batch_run.py @@ -1,4 +1,5 @@ """Test Batchrunner.""" + import pytest import mesa @@ -141,7 +142,7 @@ def test_batch_run(): # noqa: D103 "AgentID": 1, "agent_id": 1, "agent_local": 250.0, - "rng":42, + "rng": 42, }, { "RunId": 0, @@ -175,7 +176,7 @@ def test_batch_run(): # noqa: D103 "AgentID": 1, "agent_id": 1, "agent_local": 250.0, - "rng":None, + "rng": None, }, { "RunId": 0, @@ -199,7 +200,6 @@ def test_batch_run(): # noqa: D103 }, ] - result = mesa.batch_run(MockModel, {}, number_processes=2, rng=[42, 31415]) assert result == [ { @@ -210,7 +210,7 @@ def test_batch_run(): # noqa: D103 "AgentID": 1, "agent_id": 1, "agent_local": 250.0, - "rng":42, + "rng": 42, }, { "RunId": 0, @@ -262,14 +262,12 @@ def test_batch_run(): # noqa: D103 "agent_local": 250.0, "rng": 31415, }, - ] with pytest.raises(ValueError): mesa.batch_run(MockModel, {}, number_processes=2, rng=42, iterations=1) - def test_batch_run_with_params(): # noqa: D103 mesa.batch_run( MockModel, From 904e796f5a469729e78380b782b723da6dba1f9e Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 12 Nov 2025 10:41:37 +0100 Subject: [PATCH 5/6] fix for typo in value error message --- mesa/batchrunner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/batchrunner.py b/mesa/batchrunner.py index 6d9fcde5c9c..094361c826f 100644 --- a/mesa/batchrunner.py +++ b/mesa/batchrunner.py @@ -78,7 +78,7 @@ def batch_run( """ if iterations is not None and rng is not None: raise ValueError( - "you cannont use both iterations and rng at the same time. Please only use rng." + "you cannot use both iterations and rng at the same time. Please only use rng." ) if iterations is not None: warnings.warn( From 797263818150b7584bae19d5c5f0e13e0777c2ef Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 12 Nov 2025 10:42:36 +0100 Subject: [PATCH 6/6] Update batchrunner.py --- mesa/batchrunner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/batchrunner.py b/mesa/batchrunner.py index 094361c826f..38c847c8882 100644 --- a/mesa/batchrunner.py +++ b/mesa/batchrunner.py @@ -82,7 +82,7 @@ def batch_run( ) if iterations is not None: warnings.warn( - "iterations is deprecated, please use rgn instead", + "iterations is deprecated, please use rng instead", DeprecationWarning, stacklevel=2, )