Skip to content

Updating Scenario in preparation for replacing batch runner#3493

Merged
quaquel merged 18 commits intomesa:mainfrom
quaquel:scenario_update
Mar 13, 2026
Merged

Updating Scenario in preparation for replacing batch runner#3493
quaquel merged 18 commits intomesa:mainfrom
quaquel:scenario_update

Conversation

@quaquel
Copy link
Copy Markdown
Member

@quaquel quaquel commented Mar 9, 2026

This PR updates the Scenario class and how it is used inside Model. It is inspired by ideas in #3483.

  1. Scenario becomes the sole place where rng and stdlib random are defined. Model just uses these.
  2. Model.__init__ is updated to take either a Scenario subclass or instance. (supersedes ENH: Accept Scenario as class or instance in Model init #3450)
  3. Scenario is now fully frozen after instantiation without a complicated ref back to model.
  4. Scenarios can spawn replications. This builds on fix-spawn-generator #3195 but updates the idea for use inside Scenario. See also the numpy docs for more details on how this all works.
  5. I added two class methods for creating scenarios from either a numpy array or a pandas dataframe.
  6. I updated all relevant examples to use the new machinery.

So what are the API implications from this?

# this now works and uses the default values from your custom scenario
model = MyModel(scenario=MyScenario)

# rng will be passed to MyScenario(rng=rng)
model = MyModel(scenario=MyScenario, rng=42) 

# running 10 replications of a given scenario.
# the rng is different for all 10 and derived via numpy.random.default_rng(42).spawn(10)
scenario = MyScenario(a=1, b=2, rng-42)
for replication in scenario.spawn_replications(n=10):
    model = MyModel(scenario=replication)

# scenario is frozen
my_scenario = MyScenario(a=1, b=2)
my_scenario.a = 4  # raises a TypeError - inline with e.g. frozen dataclasses and tuples

# just using the base scenario still works as before
# custom scenarios just exist to specify defaults
scenario = Scenario(rng=42, a=1, b=2)

quaquel added 3 commits March 9, 2026 21:37
Make Scenario the single source of truth for RNG state

Scenario now owns the canonical RNG state. Model derives self.rng and self.random from the scenario rather than maintaining parallel `_seed`/`_rng` state, eliminating the broken scenario._seed reference in `Model.__init__`.

Key changes:
- `Scenario._stdlib_seed` property derives a reproducible integer seed for
  random.Random from initial_rng_state without advancing the generator
- `Model.__init__` always creates a Scenario first (rng= is a convenience
  shorthand), raises ValueError if both rng and scenario are passed
- Removed `Model.reset_rng()`` and the now-dead `_seed`/`_rng` instance vars from Model
- replication_id defaults to None to distinguish base scenarios from
  replication 0; scenario_id uses None as sentinel so id=0 is preserved
- Fixed docstring references to removed `_seed` attribute
- Updated tests to use `scenario.initial_rng_state` instead of `model._rng`
remove none as an option because now covered by the class
@quaquel quaquel added enhancement Release notes label breaking Release notes label labels Mar 9, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 9, 2026

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 -0.8% [-1.4%, -0.1%] 🔵 +0.4% [+0.2%, +0.6%]
BoltzmannWealth large 🔵 +2.1% [+1.3%, +2.9%] 🔵 -1.1% [-2.2%, +0.0%]
Schelling small 🔵 -0.1% [-0.6%, +0.4%] 🔵 +0.4% [-0.2%, +1.1%]
Schelling large 🔵 -0.0% [-0.6%, +0.6%] 🔵 -1.1% [-2.4%, +0.4%]
WolfSheep small 🔵 -3.2% [-4.3%, -2.1%] 🔵 +8.0% [+2.2%, +14.1%]
WolfSheep large 🔵 +0.4% [-0.2%, +0.9%] 🔵 +4.0% [+2.4%, +5.8%]
SugarscapeG1mt small 🔵 -1.3% [-1.9%, -0.7%] 🔵 -0.7% [-1.8%, +0.4%]
SugarscapeG1mt large 🔵 -0.5% [-2.0%, +0.8%] 🔵 +1.4% [+0.5%, +2.5%]
BoidFlockers small 🔵 -2.2% [-3.0%, -1.6%] 🔵 -0.3% [-0.5%, -0.0%]
BoidFlockers large 🔵 -0.6% [-1.5%, +0.2%] 🔵 -0.2% [-0.5%, +0.2%]

Copy link
Copy Markdown
Member

@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 for working on this. I think you’re looking in the right direction.

scenario_id: A unique identifier for this scenario, auto-generated starting from 0
rng: Random number generator or seed value
experiment_id: Identifies the design point (e.g., row in a QMC sample matrix)
replication_id: Identifies the stochastic replication within a design point
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you elaborate on why we now need 3 different id variables? Are there other options?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Based on my experience with the workbench, there are no clean other options.

  1. We need scenario_id as a link to the original experiment matrix (e.g., from numpy.qmc, or salib)
  2. We need a replication_id for reproduction. Only by knowing this can you reproduce the exact results for a given experiment.
  3. We need a high entropy starting RNG value for high quality random number generation (and no 42 is not a high entropy starting value because this has 30 0 in front of it.

I also fail to see what the problem is. Scenario_id and replication_id are in principle generated by Mesa itself so the user need not worry about them at all.

"initial_rng_state": self.initial_rng_state,
}

def spawn_replications(self, n: int) -> list["Scenario"]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would this make sense as a Class method? Like we do with Agent.create_agents() I believe?

Copy link
Copy Markdown
Member Author

@quaquel quaquel Mar 10, 2026

Choose a reason for hiding this comment

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

No it cannot be a class method. spawn_replication takes a base scenario instance with the scenario-specific user-provided arguments and creates n versions of it, each with a unique, independent, seeded numpy generator.

@quaquel quaquel marked this pull request as ready for review March 12, 2026 10:02
@github-actions
Copy link
Copy Markdown

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🟢 -8.5% [-9.4%, -7.7%] 🔵 -0.8% [-1.0%, -0.6%]
BoltzmannWealth large 🔵 -1.6% [-3.0%, +0.0%] 🔵 -4.1% [-8.2%, -0.2%]
Schelling small 🔵 +0.5% [-0.7%, +1.6%] 🔵 +0.7% [-0.7%, +1.8%]
Schelling large 🔵 -1.2% [-2.6%, +0.1%] 🔵 -2.1% [-4.7%, +0.7%]
WolfSheep small 🔵 +1.0% [+0.1%, +1.9%] 🔵 +6.1% [+1.5%, +10.3%]
WolfSheep large 🔵 -1.8% [-3.8%, +0.1%] 🔵 -2.6% [-4.9%, -0.2%]
SugarscapeG1mt small 🔵 -2.1% [-3.2%, -1.0%] 🔵 -3.0% [-4.3%, -1.6%]
SugarscapeG1mt large 🔵 -5.0% [-7.9%, -2.2%] 🔵 -3.3% [-4.4%, -2.0%]
BoidFlockers small 🟢 -5.5% [-6.4%, -4.6%] 🔵 -1.3% [-1.7%, -0.9%]
BoidFlockers large 🔵 -3.5% [-4.6%, -2.2%] 🔵 -1.9% [-2.3%, -1.6%]

Copy link
Copy Markdown
Member

@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.

Cleans up the model nicely. Thanks.

@quaquel quaquel merged commit 166e094 into mesa:main Mar 13, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Release notes label enhancement Release notes label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants