Skip to content

Conversation

@EwoutH
Copy link
Member

@EwoutH EwoutH commented Oct 7, 2025

Problem

When using batch_run() with a single seed value and multiple iterations, all iterations use the same seed, producing identical results instead of independent replications.

parameters = {'seed': 42}
batch_run(MyModel, parameters, iterations=10)
# All 10 iterations use seed=42 → identical results

See #2835.

Solution

Modify _model_run_func to automatically increment the seed for each iteration: 42, 43, 44, etc.

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

Behavior changes

  • seed=42, iterations=3: currently all use 42, now uses 42, 43, 44
  • seed=[42, 43, 44], iterations=1: unchanged
  • No seed specified: unchanged (random)

Code that passes a single seed with multiple iterations will get different results. The current behavior seems like a bug (why run multiple identical iterations?), but this technically breaks existing code.

Review

I'm in doubt about this. What if users change have other random elements in their model? Do we do good obscuring this?

Secondly, is this a bugfix or a breaking change? Should we treat it as a fix and merge, or wait for a major version?

Might close #2835. @dylan-munson curious what you think.

@EwoutH EwoutH added bug Release notes label breaking Release notes label labels Oct 7, 2025
@github-actions
Copy link

github-actions bot commented Oct 7, 2025

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 +0.5% [-0.2%, +1.2%] 🔵 -0.1% [-0.2%, +0.1%]
BoltzmannWealth large 🔵 -0.3% [-1.4%, +0.8%] 🔵 +0.1% [-3.2%, +3.4%]
Schelling small 🔵 -0.9% [-1.2%, -0.6%] 🔵 -2.1% [-2.5%, -1.6%]
Schelling large 🔵 -1.5% [-2.2%, -0.6%] 🔵 -2.6% [-6.8%, +1.4%]
WolfSheep small 🔵 -0.9% [-1.3%, -0.6%] 🔵 +1.0% [+0.7%, +1.2%]
WolfSheep large 🔵 +2.1% [+1.1%, +3.0%] 🔴 +5.4% [+4.1%, +6.7%]
BoidFlockers small 🔵 -1.5% [-2.3%, -0.5%] 🔵 -0.7% [-1.0%, -0.4%]
BoidFlockers large 🔵 -1.3% [-2.2%, -0.4%] 🔵 -0.9% [-1.6%, -0.1%]

@EwoutH EwoutH requested a review from quaquel October 7, 2025 19:08
@quaquel
Copy link
Member

quaquel commented Oct 7, 2025

I agree that this needs to be fixed. However, using subsequent integers with Mersenne Twister, Python's default RNG, is a bad idea.

From wikipedia: "A consequence of poor diffusion is that two instances of the generator, started with initial states that are almost the same, will usually output nearly the same sequence for many iterations". Also, using a seed with many zeros (like 42) is actually bad as well. One option is to just use time.time() every single time and return this seed value for reproducibility.

As an aside, numpy's rng is much better and I believe we should move all mesa code over to using this while deprecating the use of python's stdlib random library.

@tpike3
Copy link
Member

tpike3 commented Oct 8, 2025

Considering how important this is, maybe we should just go all in and do the switch to numpy and its rng and then have seed options like system time and hierarchical seeding?

Does it have to be a breaking change? Could we keep the old behavior and just add a warning?

@quaquel
Copy link
Member

quaquel commented Oct 8, 2025

Does it have to be a breaking change? Could we keep the old behavior and just add a warning?

Moving the internals over should be possible as a non-breaking change.

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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
kwargs["seed"] = int(seed_value) + iteration
kwargs["seed"] = seed_value + time.time()

This is all that is needed to ensure a much better spread of seeding values and thus better randomness.

@EwoutH
Copy link
Member Author

EwoutH commented Oct 28, 2025

As an aside, numpy's rng is much better and I believe we should move all mesa code over to using this while deprecating the use of python's stdlib random library.

Considering how important this is, maybe we should just go all in and do the switch to numpy and its rng and then have seed options like system time and hierarchical seeding?

Considering this, do we want to move forward with this PR?

return this seed value for reproducibility.

Where/how should we do this (without breaking API)?

EwoutH and others added 2 commits October 28, 2025 19:03
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)
@quaquel
Copy link
Member

quaquel commented Oct 28, 2025

Considering this, do we want to move forward with this PR?

Shifting to numpy rng requires changes in e.g., CellCollection and AgentSet, it's independent from this PR.

Where/how should we do this (without breaking API)?

The return is List[Dict[str, Any]], so in prinicple you could just insert a seed kwarg into the dict.

Alternatively, you can keep stuff as is and just document the behavior.

Or, perhaps even better: raise a ValueError if seed and iterations don't match. So, if you do iterations=10 and seed=[5,] you raise a ValueError because the number of iterations and the number of seeds don't match.

In my view, we might even consider deprecating iterations in favor of only seed, where seed is either a single SeedLike or a list of SeedLike.

@quaquel quaquel mentioned this pull request Nov 10, 2025
4 tasks
@quaquel
Copy link
Member

quaquel commented Nov 12, 2025

@EwoutH, I have updated this PR as discussed yesterday. I added a new kewword argument rng and deprecated iterations. I have also updated the tests accordingly.

While reviewing the code, I noticed the current use of run_id and iteration in the return value from batch_run. run_id is just the sum total of runs, while iteration gives the iteration-id of particular experiment. I am inclined to modify this (probably in a separate PR). In my mind, it makes sense to have 3 pieces of information: an identifier for the experiment (i.e., the exact parameter settings), an identifier for the iteration/replication (this might also just be the seed value instead of an integer starting from 0), and possibly an identifier for which run in total it is. Currently, batch_run only gives you the second and third, so grouping by experiment can be a bit tricky, while this is critical when calculating, e.g., the average across replications.

@EwoutH EwoutH added the deprecation When a new deprecation is introduced label Nov 12, 2025
Copy link
Member Author

@EwoutH EwoutH left a comment

Choose a reason for hiding this comment

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

Thanks!

Since this is now a new, breaking feature, we should do it the proper way.

Could you add a section in the migration guide and link to it in the deprecationwarning? See #2872 for a recent example.

We also have to be careful about the order of our arguments. Some people use, positional arguments (stupid, I know), which we’re changing once we remove “iterations”.

Also we should properly explain this in the tutorial (can be a separate PR).

@quaquel
Copy link
Member

quaquel commented Nov 12, 2025

  1. It's technically not breaking because iterations will continue to work. It just issues a deprecation warning.
  2. Yes, I'll take a look at adding it to the migration guide.
  3. If you use arguments instead of keyword arguments, you deserve what you get :). Also, iterations is still there and still functions, but with a warning. So there is no problem.
  4. Yes, I was planning to update the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Release notes label bug Release notes label deprecation When a new deprecation is introduced

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants