Skip to content

Add batch and suppress context managers for mesa_signals#3251

Closed
falloficarus22 wants to merge 10 commits intomesa:mainfrom
falloficarus22:mesa-signals-batch-suppress
Closed

Add batch and suppress context managers for mesa_signals#3251
falloficarus22 wants to merge 10 commits intomesa:mainfrom
falloficarus22:mesa-signals-batch-suppress

Conversation

@falloficarus22
Copy link
Copy Markdown
Contributor

@falloficarus22 falloficarus22 commented Feb 7, 2026

Summary

This PR adds two new context managers to mesa_signals via HasObservables:

  • batch_signals(): buffers signals and flushes at context exit.
  • suppress_signals(): suppresses signal emission while active.
    It is designed to reduce signal storms during bulk updates while keeping current behavior unchanged outside the contexts.

Motive

While making model behavior more reactive, frequent updates can emit many intermediate signals that are not useful to observers (especially during bulk state changes or data collection phases).
This PR introduces explicit signal-flow control so users can:

  • coalesce noisy intermediate updates into final meaningful notifications (batch_signals)
  • temporarily disable notifications during setup/mutation phases (suppress_signals)

Implementation

Changes were made in core.py and tests in test_mesa_signals.py

Key implementation details:

  • Added internal per-instance state in HasObservables:
    • _batch_depth
    • _suppress_depth
    • _signal_buffer
  • Updated notify() to route through _buffer_or_dispatch():
    • If suppress is active: drop signal.
    • Else if batch is active: buffer signal.
    • Else: dispatch immediately via _mesa_notify().
  • Added _flush_signal_buffer():
    • For ObservableSignals.CHANGED, keep only the last signal per observable name in the batch.
    • Keep non-CHANGED signals in original order.
  • Added context managers on HasObservables:
    • batch_signals() supports nesting and flushes only on outermost exit.
    • suppress_signals() supports nesting.
  • Interaction rule:
    • suppress has precedence over batch.

Usage Examples

from mesa.experimental.mesa_signals import ObservableSignals

# batch updates: only last CHANGED is emitted for each observable
agent.observe("wealth", ObservableSignals.CHANGED, handler)

with agent.batch_signals():
    agent.wealth = 10
    agent.wealth = 20
    agent.wealth = 30
# handler called once with new=30
# suppress all signals during initialization/setup
with model.suppress_signals():
    for a in model.agents:
        a.energy = 100
        a.wealth = 0
# no signals emitted inside the context
# nested behavior
with obj.batch_signals():
    obj.value = 1
    with obj.suppress_signals():
        obj.value = 2   # suppressed
    obj.value = 3
# flush emits only final non-suppressed CHANGED signal (new=3)

Additional Notes

  • Scope intentionally limited to signal flow control; no changes to computed-property dependency tracking in this PR.
  • Tests added for:
    • CHANGED deduplication in batch
    • non-CHANGED ordering
    • nested batch/suppress behavior
    • batch+suppress interaction
    • buffer cleanup after flush
  • Related Issue: Mesa Signals improvements (tracking issue) #3227

@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 7, 2026

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 -2.1% [-3.0%, -1.1%] 🔵 -1.5% [-1.7%, -1.3%]
BoltzmannWealth large 🔵 -1.0% [-1.5%, -0.5%] 🔵 +3.0% [+0.2%, +6.4%]
Schelling small 🔵 -2.8% [-3.3%, -2.3%] 🔵 -0.7% [-1.0%, -0.5%]
Schelling large 🔵 -1.0% [-1.7%, -0.1%] 🔵 -1.7% [-4.1%, +0.9%]
WolfSheep small 🔵 -1.1% [-1.6%, -0.7%] 🔵 -0.8% [-1.0%, -0.5%]
WolfSheep large 🔵 -1.0% [-2.6%, +0.4%] 🔵 +0.6% [-2.5%, +3.4%]
BoidFlockers small 🔵 -2.0% [-2.4%, -1.7%] 🔵 +0.5% [+0.2%, +0.9%]
BoidFlockers large 🔵 -1.2% [-1.8%, -0.7%] 🔵 +1.0% [+0.9%, +1.2%]

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 7, 2026

Thanks for this PR. I am not sure about this PR. It makes a number of choices that might have to be discussed first. For example, the context managers now work at the instance level rather than being global. What are the pro's and con's of this design? Also, I want to see a more elegant design for how signals are aggregated in the batch context manager. Most importantly, this must be easily user extendable for custom signals that a user might define.

@falloficarus22
Copy link
Copy Markdown
Contributor Author

Thanks for this PR. I am not sure about this PR. It makes a number of choices that might have to be discussed first. For example, the context managers now work at the instance level rather than being global. What are the pro's and con's of this design? Also, I want to see a more elegant design for how signals are aggregated in the batch context manager. Most importantly, this must be easily user extendable for custom signals that a user might define.

I think the key design tradeoff is instance-local vs global batching/suppression.

Instance-local context managers

Pros:

  • Strong isolation: one object’s batching does not silently affect others.
  • Better determinism in multi-model/experiment settings.
  • Cleaner lifecycle/GC boundaries (state stays with the instance).
  • Easier debugging because behavior is scoped and explicit.

Cons:

  • Less convenient when users want to batch a whole graph of objects at once.
  • Requires explicit coordination if multiple related objects should flush together.

Global context managers

Pros:

  • Very convenient “single switch” to batch/suppress everything.
  • Easy for broad bulk operations.

Cons:

  • Hidden coupling across otherwise unrelated objects.
  • Higher risk of surprising behavior and order sensitivity.
  • Harder to reason about with concurrent/parallel runs.
  • More complex lifecycle management and leak risk.

My preference is instance-local as the safe default, and if needed we can add an explicit opt-in “global coordinator” later rather than making global behavior implicit by default.

I can make current signal aggregation more extensible by replacing hardcoded CHANGED handling with a pluggable aggregation policy API so custom/user-defined signals are first-class.

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 8, 2026

Thanks for the overview of pro's and cons.

Better determinism in multi-model/experiment settings.

This one makes no sense to me. When done well signals should always stay within a single model unless the user does something very strange

I can make current signal aggregation more extensible by replacing hardcoded CHANGED handling with a pluggable aggregation policy API so custom/user-defined signals are first-class.

Too me this is critical, so I would like to see this developed at least at the API level before moving forward with this PR. For the record, I have been playing around with this and have some directions I might explore myself.

@falloficarus22 falloficarus22 force-pushed the mesa-signals-batch-suppress branch from e78fe1d to bdc943f Compare February 8, 2026 07:53
@falloficarus22
Copy link
Copy Markdown
Contributor Author

falloficarus22 commented Feb 8, 2026

  1. Made batch aggregation policy-driven and user-extensible in core.py:
  • Added BatchSignalAggregator type alias.
  • Added instance policy list: self._batch_aggregators.
  • Reworked _flush_signal_buffer() to run configured aggregators in sequence.
  • Added default built-in policy as API method: HasObservables.aggregate_last_changed(...)
  • Added API methods:
    • add_batch_aggregator(...)
    • clear_batch_aggregators(...)
  • Kept existing behavior by default via self._batch_aggregators = [self.aggregate_last_changed].
  1. Added @emit dependency tracking for @computed_property in core.py:
  • Introduced _EMIT_DEPENDENCY sentinel.
  • In emit(...) wrappers, when a computed is currently evaluating:
    • register dependency via CURRENT_COMPUTED._add_parent(self, observable_name, _EMIT_DEPENDENCY)
    • register processing signal.
  • Updated computed change detection to treat _EMIT_DEPENDENCY as a signal-driven dependency that forces recomputation when dirty.
  1. Added tests in test_mesa_signals.py:
  • test_custom_batch_aggregator_for_user_defined_signal
  • test_computed_property_tracks_emit_dependency

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 8, 2026

Thanks for the update.

  1. I am going to take a look at this myself as well. I am not yet convinced by the resulting API and want to explore some other ideas.
  2. Please revert the emit stuff to keep this PR focussed on just the context managers. Also, we are still discussing the dependency problem in the issue so putting in a PR for this is premature.

@falloficarus22 falloficarus22 force-pushed the mesa-signals-batch-suppress branch from 96d7695 to 6dc9901 Compare February 8, 2026 13:13
@falloficarus22 falloficarus22 force-pushed the mesa-signals-batch-suppress branch from 0e66e20 to 74591fb Compare February 8, 2026 13:26
@falloficarus22
Copy link
Copy Markdown
Contributor Author

Closed this in favour of #3261

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants