Skip to content

Add model.time as universal source of truth for simulation time#2903

Merged
EwoutH merged 4 commits intomesa:mainfrom
EwoutH:universal_time
Dec 3, 2025
Merged

Add model.time as universal source of truth for simulation time#2903
EwoutH merged 4 commits intomesa:mainfrom
EwoutH:universal_time

Conversation

@EwoutH
Copy link
Copy Markdown
Member

@EwoutH EwoutH commented Nov 30, 2025

Summary

Introduces model.time as the single, canonical source of simulation time in Mesa. All components (DataCollector, visualization, user code, simulators) can now rely on model.time to get the current simulation time, regardless of whether the model uses simple stepping or discrete event simulation.

Motive

Previously, time in Mesa was fragmented across different locations depending on the simulation mode:

  • Simple models used model.steps as a proxy for time
  • Discrete event simulations stored time in simulator.time
  • Components needing time had to implement fallback chains like:
    def _get_time(self):
        if hasattr(model, "simulator") and hasattr(model.simulator, "time"):
            return model.simulator.time
        if hasattr(model, "time"):
            return model.time
        return float(model.steps)

This caused problems for features that need consistent time access, such as the ContinuousObservable in #2851 and the conceptual model of space discussed in #2585. The discussion in #2228 (which originated in #2223) established consensus that Mesa needs a universal truth for time.

Implementation

Model class changes:

  • Add time: float attribute initialized to 0.0
  • Add step_duration: float parameter (default 1.0) controlling time advancement per step
  • Add _simulator: Simulator | None to track if a simulator controls time
  • Modify _wrapped_step() to only auto-increment time when no simulator is attached

Simulator class changes:

  • Remove internal self.time attribute
  • Write directly to model.time during event execution
  • Add deprecated time property delegating to model.time for backward compatibility
  • Register with model via model._simulator = self in setup()
  • Reset model.time to start_time in reset()

Design kept minimal: a simple attribute rather than a property or RunControl class, following YAGNI principles while solving the immediate problem.

Usage Examples

Basic usage (time auto-increments with steps):

model = Model()
model.step()
print(model.time)  # 1.0
model.step()
print(model.time)  # 2.0

Custom step duration (useful for staged activation):

model = Model(step_duration=0.25)
model.step()
print(model.time)  # 0.25
model.step()
print(model.time)  # 0.5

With discrete event simulator (simulator controls time):

model = Model()
simulator = DEVSimulator()
simulator.setup(model)

simulator.schedule_event_absolute(some_function, 2.5)
simulator.run_until(3.0)
print(model.time)  # 3.0

Components can now simply use model.time:

class MyDataCollector:
    def collect(self, model):
        timestamp = model.time  # Always works, no fallbacks needed

Compatibility

  • simulator.time still works but emits a DeprecationWarning directing users to model.time (considering DEVS is still experimental, this is quite gentle)
  • Code directly accessing simulator.time without triggering the property (unlikely) would break (again, experimental, and all test pass)

Additional Notes

@EwoutH EwoutH requested a review from quaquel November 30, 2025 18:53
@EwoutH EwoutH added the feature Release notes label label Nov 30, 2025
@github-actions
Copy link
Copy Markdown

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 +2.9% [+1.8%, +4.1%] 🔵 -0.1% [-0.3%, +0.0%]
BoltzmannWealth large 🔵 -0.9% [-1.5%, -0.3%] 🔵 +0.1% [-1.1%, +1.6%]
Schelling small 🔵 -0.5% [-0.7%, -0.3%] 🔵 -1.1% [-1.3%, -0.9%]
Schelling large 🔵 +0.3% [-0.2%, +0.9%] 🔵 +1.2% [+0.3%, +2.3%]
WolfSheep small 🔵 +1.2% [+0.8%, +1.5%] 🔵 +0.5% [+0.3%, +0.6%]
WolfSheep large 🔵 +0.4% [-0.2%, +1.1%] 🔵 +2.5% [+1.3%, +4.2%]
BoidFlockers small 🔵 -1.4% [-2.0%, -0.9%] 🔵 -0.6% [-0.8%, -0.4%]
BoidFlockers large 🔵 -1.8% [-2.2%, -1.3%] 🔵 -0.5% [-0.8%, -0.3%]

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Dec 1, 2025

  1. I suggest making step_duration protected: _step_duration. It is good that users can control it, but we reserve the right to modify how the controlling step duration works in the future without breaking Semantic Versioning. I still would like to think through a more detailed way for users to set up time, including support for date time and calendars. This will interact with step_duration and so I don't want to be locked into this yet. model.time is generic enough that I am fine with locking this in as part of the public api.
  2. I am not sure about the API for the simulators. You now do
model = Model()
simulator = DEVSimulator()
simulator.setup(model)

However, in the Wolfsheep example, I have moved simulator.setup into the __init__ method of the Model class. I am inclined to make this the preferred api by adding simulator as a keyword argument to Model. and make model.simulator part of the public API of the model class if simulator is not None.

As an aside: what are people's thoughts on stabilizing the simulator stuff?

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Dec 1, 2025

Thanks for getting back.

  1. I suggest making step_duration protected: _step_duration.

Maybe we need to think this a bit further through before going one way or the other. I also want to add datetime support for example.

Do what do you think about a separate RunControl class vs integrating it in the model? In this effort, I started with it, but realized it wasn't needed and mainly added API complexity.

I am not sure about the API for the simulators.

I just copied (previous) best-practices. It can be that's now outdated. Should I update it?

As an aside: what are people's thoughts on stabilizing the simulator stuff?

Perfectly ok with it. Used it intensively without problems during my thesis.

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Dec 1, 2025

  1. I would suggest using composition for RunControl. So, a dedicated class that can be passed to Model.__init__ or something along those lines.
  2. I suggest keeping this PR focused on just making model.time part of the public API and minimizing all other changes. Having model.time as the sole truth is valuable as such. Let's worry about expanding it in separate PR's.
  3. The simulator stuff is fine as is for now. I'll make a separate PR asap as part of a move towards stabilizing it.

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Dec 1, 2025

  1. I suggest making step_duration protected: _step_duration.

Done.

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Dec 3, 2025

@quaquel can you give this a formal review?

Copy link
Copy Markdown
Member

@quaquel quaquel left a comment

Choose a reason for hiding this comment

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

1 question but fine otherwise.

This commit introduces `model.time` as the single, canonical source for simulation time in Mesa, addressing the long-standing issue of having multiple competing time sources (model.steps, simulator.time) that caused confusion and required workarounds in components like DataCollector, visualization, and the experimental ContinuousObservable.

Previously, time information was scattered: basic models only had `model.steps`, while models using simulators had to access `simulator.time`. This forced components to implement fallback chains like checking for simulator.time, then model.time, then model.steps. The new design establishes `model.time` as the ground truth that all components can rely on.

Key changes to mesa/model.py:
- Add `time: float = 0.0` attribute that tracks simulation time
- Add `step_duration: float = 1.0` parameter allowing customization of how much time passes per step (useful for staged activation where you might want 0.25 per stage)
- Add `_simulator: Simulator | None` to track whether a simulator controls time progression
- Modify `_wrapped_step()` to only auto-increment time when no simulator is attached

Key changes to mesa/experimental/devs/simulator.py:
- Remove `self.time` from Simulator, now writes directly to `model.time`
- Add deprecated `time` property that delegates to `model.time` with a DeprecationWarning for backward compatibility
- Register simulator with model via `model._simulator = self` in setup()
- Update all time reads/writes to use `model.time` instead of `self.time`
- Improve error messages to be more descriptive

The design intentionally keeps things minimal: no RunControl abstraction, no complex time management classes. Simulators simply write to `model.time` directly, and the default step wrapper increments time by `step_duration` when no simulator is attached. This provides a single way to do things while remaining backward compatible.
Replace MagicMock(spec=Model) with real Model instances since mocks lack the new time attribute. Update assertions to use model.time instead of simulator.time. Add tests for time increment, step_duration, simulator attachment, and deprecation warning. Fix AgentSet constructor calls to use random= keyword argument.
Make Model._step_duration private allows us to update it's working in the future without breaking compatibility. For example if changes are needed for variable durations or datetime support.
@EwoutH EwoutH merged commit 03d8811 into mesa:main Dec 3, 2025
4 of 11 checks passed
EwoutH added a commit that referenced this pull request Dec 4, 2025
The schedule_event_absolute method was still using self.time internally, triggering the FutureWarning added in #2903 that deprecated simulator.time in favor of model.time as the universal source of truth for simulation time. This change updates the method to use self.model.time directly, ensuring Mesa's own code doesn't trigger deprecation warnings and aligning with the established pattern where the simulator writes to and reads from model.time throughout the codebase.
@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Dec 24, 2025

Removed step_duration for now, to keep don't over invest into steps since we're moving to time.

Nithin9585 added a commit to Nithin9585/mesa that referenced this pull request Jan 3, 2026
- Updated DataCollector to use model.time in _collection_steps tracking
- Changed _record_agents and _record_agenttype to use model.time for dictionary keys
- Updated all test models to use model.time in reporters and conditions
- Ensures consistency with Mesa's deprecation plan (issue mesa#2903)
- All tests passing, ruff checks clean
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Release notes label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants