Add ContinuousStates and -Observables
Summary
Adds ContinuousObservable to mesa_signals, enabling agent states that change continuously over time with automatic threshold detection and signal emission. Demonstrates the feature with an enhanced Wolf-Sheep predator-prey model where energy depletes continuously rather than at discrete time steps.
Motive
Agent-based models often need to represent continuously changing states like energy depletion, resource growth, or temperature changes. Currently, modelers must manually calculate state changes at each step or schedule discrete update events, leading to:
- Balancing trade-offs between update frequency and performance
- Difficulty detecting exact threshold crossings
- No standardized patterns for time-varying states
- Complex coordination of multiple changing states
This addresses discussion #2529 (Continuous States) and provides a foundational building block toward the behavioral framework outlined in discussion #2538, enabling more realistic agent behaviors driven by internal state dynamics.
Implementation
Core Components:
-
ContinuousObservabledescriptor: ExtendsObservableto track values that change over time according to a rate function. Uses lazy evaluation - values are only recalculated when accessed, making it efficient even with many agents. -
ContinuousStatehelper class: Internal state tracker storing the current value, last update time, rate function, and threshold set. Handles time-based calculation and threshold crossing detection. -
Threshold management:
HasObservables.add_threshold()provides a clean API for registering callbacks when values cross specific thresholds (upward or downward). Uses the existing signal/observer pattern - thresholds emit"threshold_crossed"signals. -
Time integration: Automatically detects time source from
model.simulator.time,model.time, or falls back tomodel.steps. Works with bothDEVSimulator(float time) andABMSimulator(integer steps).
Key design decisions:
- Lazy evaluation prevents unnecessary calculations when states aren't accessed
- Threshold storage uses a set (values only), callbacks managed through signal subscriptions to avoid duplicate invocations
- Linear integration for simplicity; extensible to more complex methods
- Fully integrated with existing
Computedproperties and dependency tracking
API usage examples
# Continuous value with rate per time unit
energy = ContinuousObservable(initial_value: float, rate_func: (value, elapsed, agent) -> float)
# Set/adjust value (emits "change" + checks thresholds)
self.energy = 120.0
self.energy += 5.0
# Thresholds (fires "threshold_crossed" with signal.threshold, signal.direction)
self.add_threshold("energy", 0.0, callback)
# Subscribe to signals
self.observe("energy", "change", on_change)
self.observe("energy", "threshold_crossed", on_threshold)
# Computed properties that depend on observables
is_hungry = Computable()
self.is_hungry = Computed(lambda: self.energy < 50.0)
Usage Examples
Basic continuous energy depletion:
class Wolf(Agent, HasObservables):
energy = ContinuousObservable(
initial_value=100.0,
rate_func=lambda value, elapsed, agent: -agent.metabolic_rate
)
def __init__(self, model):
super().__init__(model)
self.metabolic_rate = 0.5
self.energy = 100.0
# Die when energy reaches zero
self.add_threshold("energy", 0.0, self._on_death)
def _on_death(self, signal):
if signal.direction == "down":
self.remove()
Reactive behaviors with computed properties:
class Animal(Agent, HasObservables):
energy = ContinuousObservable(
initial_value=100.0,
rate_func=lambda value, elapsed, agent: -agent.metabolic_rate
)
is_hungry = Computable()
def __init__(self, model):
super().__init__(model)
self.energy = 100.0
# Computed property automatically updates when energy changes
self.is_hungry = Computed(lambda: self.energy < 50.0)
def move(self):
if self.is_hungry:
# Hunt actively when hungry
self.seek_food()
else:
# Conserve energy when not hungry
self.wander()
Enhanced Wolf-Sheep model: The updated example demonstrates continuous energy dynamics, threshold-triggered death/starvation modes, and behavior switching based on computed hunger states. Energy depletes continuously between steps rather than in discrete chunks, creating more realistic predator-prey dynamics.
Additional Notes
- Tests: Comprehensive test suite (19 tests) covering basic functionality, signals, edge cases (numpy floats, zero elapsed time, exact thresholds), and integration scenarios
-
Backward compatible: Existing mesa_signals code unchanged;
ContinuousObservableis an additive feature - Performance: Lazy evaluation ensures minimal overhead; states only recalculate when accessed
- Future extensions: Foundation for more complex behavioral patterns (needs-based architectures, homeostatic agents) discussed in #2538
- Time management: Uses TODO comment to reference #2228 regarding universal time handling in Mesa
- Documentation: Wolf-Sheep example serves as practical demonstration; could expand docs with more use cases
This provides the first of two foundational building blocks identified for Mesa's behavioral framework: reactive state management with continuous dynamics. The second block (action/task scheduling) can build naturally on this foundation.
I'm a bit mentally exhausted, was a big push to get this finished. I will write up some review points and context later this weekend.
The individual commit messages hopefully contain useful details.
For now:
- Maybe the new
ContinuousObservableandContinuousStateclasses should go into a separate file instead ofmesa_signals.py. - Yes, I know using ContiniousStates with an ABMSimulator (in the wolf_sheep example) doesn't make that much sense in the current implementation. Future work.
- We really should re-address https://github.com/projectmesa/mesa/discussions/2228 sooner than later.
model.timealways being accessible would make stuff much easier.
Curious on initial thoughts!
(CI test failures are unrelated, see https://github.com/widgetti/solara/issues/1110)
I think this idea is very cool. I hope to find time next week to take a closer look.
One quick question: how is threshold management squared with the lazy evaluation of ContinousStates/ContinuousObservables?
Great question:
how is threshold management squared with the lazy evaluation of ContinousStates/ContinuousObservables?
Lazy evaluation and threshold detection work together because thresholds are checked during value recalculation, not continuously. When someone accesses a ContinuousObservable (e.g., agent.energy), the system calculates how much time has elapsed since the last access, computes the new value, and then checks if any thresholds were crossed during that transition.
The key is that we check the range between old and new values, not discrete sample points. With linear integration (value + rate * elapsed_time), we can mathematically determine if a threshold was crossed even if we never computed the value at that exact moment. For example, if energy goes from 100 β 40 over 60 time units and there's a threshold at 50, we detect that crossing because old_value (100) > threshold (50) >= new_value (40).
Important to note: Thresholds fire when values are accessed, not at the exact simulation time they're crossed. If an agent's energy crosses zero at t=50 but nothing accesses it until t=100, the death threshold fires at t=100. This is a fundamental trade-off with lazy evaluation: efficiency in exchange for non-deterministic firing times.
In practice, this isn't an issue for most models because agents access their states during decision-making (e.g., checking is_hungry during movement) and data collectors sample states regularly. For the Wolf-Sheep example, agents effectively check their energy every step through computed properties and movement decisions.
An "eager mode" could be added in the future where registering a threshold automatically schedules a discrete event at the calculated crossing time, providing deterministic timing at the cost of additional event management overhead.
What we do need to enable is non-linear rate functions working with thresholds.
@EwoutH This is very cool! Thanks for this PR
Thanks for your early feedback! I will be in a night train Tuesday evening to Wednesday morning, I probably have some time to further look into it there.
Threshold detection for non-linear rate functions is absolutely not trivial. I explored some options here:
- https://github.com/projectmesa/mesa/discussions/2854
I'm not sure which direction I'm leaning personally yet.
@quaquel review with academic literature in mind. Also pay attention to APi and naming.
time will be property, keyword for simulator on instancing.
@coderabbitai full review
β Actions performed
Full review triggered.
Walkthrough
Introduces a reactive, continuous energy model to the wolf-sheep ecosystem simulation. ContinuousObservable enables time-based energy depletion tied to metabolic rates. Animals now use computed reactive properties responding to energy thresholds, with lifecycle callbacks triggering at critical levels. Model integrates a time-based simulator and tracks average energy per species.
Changes
| Cohort / File(s) | Summary |
|---|---|
Mesa Signals Infrastructure mesa/experimental/mesa_signals/mesa_signal.py, mesa/experimental/mesa_signals/__init__.py |
Added public ContinuousObservable class supporting time-based value evolution with threshold crossing detection and callbacks. Implemented add_threshold method on HasObservables for registering thresholds and callbacks. Extended all exports to include ContinuousObservable. |
Wolf-Sheep Agent Behavior mesa/examples/advanced/wolf_sheep/agents.py |
Refactored Animal to inherit from HasObservables with energy as ContinuousObservable decaying via metabolic_rate. Added reactive computed properties is_hungry and can_reproduce. Implemented lifecycle callbacks _on_energy_depleted and _on_critical_hunger for threshold-based state changes. Updated Sheep and Wolf movement/feeding logic to prioritize based on computed hunger state. Converted GrassPatch to HasObservables with observable fully_grown and regrowth scheduling. |
Wolf-Sheep Model Integration mesa/examples/advanced/wolf_sheep/model.py |
Integrated simulator for time-based energy dynamics. Extended data collection with "Avg Wolf Energy" and "Avg Sheep Energy" reporters. Updated agent initialization to accept explicit energy values derived from RNG. |
Continuous Observable Tests tests/test_continuous_observables.py |
Comprehensive test suite covering ContinuousObservable behavior, variable rate handling, threshold mechanics, computed properties, multi-agent scenarios, predator-prey interactions, and batch agent creation with per-agent thresholds. |
Sequence Diagram(s)
sequenceDiagram
participant Simulator
participant Animal
participant EnergyObs as ContinuousObservable<br/>(energy)
participant Computed as Computed<br/>(is_hungry)
participant Callback as Lifecycle<br/>Callback
Simulator->>Animal: step()
Note over Simulator: Time advances
Animal->>EnergyObs: get energy<br/>(queries elapsed time)
activate EnergyObs
EnergyObs->>EnergyObs: calculate new value<br/>using rate_func
EnergyObs->>EnergyObs: check_thresholds()
alt Threshold Crossed
EnergyObs->>Callback: emit threshold_crossed signal
Callback->>Callback: execute callback<br/>(e.g., _on_energy_depleted)
end
EnergyObs-->>Animal: return updated energy
deactivate EnergyObs
Animal->>Computed: read is_hungry
activate Computed
Computed->>EnergyObs: evaluate lambda<br/>(energy < threshold)
EnergyObs-->>Computed: return energy
Computed-->>Animal: return is_hungry result
deactivate Computed
alt is_hungry == true
Animal->>Animal: prioritize food-seeking<br/>movement
else
Animal->>Animal: default wandering
end
Estimated code review effort
π― 4 (Complex) | β±οΈ ~45 minutes
-
Careful attention needed: ContinuousObservable threshold detection logic and time-based value calculation in
mesa_signal.pyβ verify threshold crossing boundary conditions, time fallback logic, and signal emission correctness - Cross-system interaction: Verify simulator integration in agents.py and model.py properly initializes and accesses timing information for continuous energy depletion
- Behavioral logic: Confirm Sheep and Wolf movement/feeding decisions correctly prioritize computed properties and handle reactive state changes without unintended side effects
- Test coverage: Review test suite scope for edge cases in threshold detection, multi-agent independence, and numpy compatibility
Poem
π° Observe the energy flowing soft and slow,
Through thresholds crossed where hungry bunnies go,
With signals firing as the time moves near,
The reactive creatures nibble without fear,
Continuous depletion, renewal tooβ
Mesa's pulse now beats so steady and true! πΏ
Pre-merge checks and finishing touches
β Failed checks (1 warning, 1 inconclusive)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | β οΈ Warning | Docstring coverage is 44.17% which is insufficient. The required threshold is 80.00%. | You can run @coderabbitai generate docstrings to improve docstring coverage. |
| Title check | β Inconclusive | The title 'Add ContinuousStates and -Observables' is vague and uses non-standard phrasing with 'and -Observables' instead of 'and Observables', making it unclear and potentially misleading about the actual changes. | Use a clearer, more standard title such as 'Add ContinuousObservable for continuous agent state dynamics' to accurately reflect the main feature being introduced. |
β Passed checks (1 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description check | β Passed | The pull request provides a comprehensive description covering summary, motive, implementation details, API usage examples, and additional notes. |
β¨ Finishing touches
- [ ] π Generate docstrings
π§ͺ Generate unit tests (beta)
- [ ] Create PR with unit tests
- [ ] Post copyable unit tests in a comment
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
I'm fine with reviewers using AI tools to help them review a PR, but I'm not triaging AI comments. So @tpike3 please triage the comments and resolve the ones that are not highly relevant in this stage in het PR review process.
I'm fine with reviewers using AI tools to help them review a PR, but I'm not triaging AI comments. So @tpike3 please triage the comments and resolve the ones that are not highly relevant in this stage in het PR review process.
@tpike3 I will hide the AI-review for now. Feel free to unhide it if you find anything relevant.
I updated this PR based on the new unified model.time added in https://github.com/mesa/mesa/pull/2903.
If everyone agrees this is the direction to go, I will fix the last bugs in the example models. Please review critically.