-
-
Couldn't load subscription status.
- Fork 1k
Add ContinuousStates and -Observables #2851
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Add ContinuousObservable class that extends mesa_signals to support agent properties that change continuously over time. This enables modeling realistic phenomena like energy depletion, resource growth, and gradual state changes without manual step-by-step updates. Key features: - Lazy evaluation: values only recalculated when accessed, improving performance - Threshold detection: automatic callbacks when values cross specified thresholds in either direction - Configurable rate functions: support linear, exponential, or custom change patterns - Full integration with existing mesa_signals reactive system: works seamlessly with Observable, Computable, and dependency tracking - Time-aware: integrates with Mesa's time system for accurate elapsed time calculations This provides a foundational building block for modeling needs-based architectures, continuous resource systems, and time-dependent agent behaviors while maintaining the composability and reactivity of mesa_signals. fix imports
Update the wolf-sheep predation example to demonstrate mesa_signals and ContinuousObservable capabilities. Energy now depletes continuously over time rather than in discrete steps, and agent behaviors react automatically to energy state changes. Changes: - Convert Animal base class to use ContinuousObservable for energy tracking with configurable metabolic rates - Add computed properties (is_hungry, can_reproduce) that automatically update based on energy levels - Implement threshold-triggered events for death (energy reaches zero) and survival mode (critical hunger) - Enhance decision logic: sheep prioritize grass when hungry, wolves hunt actively when hungry - Add activity-based metabolic rates (movement costs more energy than resting) - Improve GrassPatch to use Observable and react to state changes with signal handlers - Expand data collection to track average energy levels across populations The example now demonstrates key reactive programming patterns: observable state management, computed properties for decision making, threshold-based event triggering, and automatic dependency tracking. This makes the model more realistic (continuous dynamics) while reducing boilerplate code (declarative state management). fix agent imports
Implement flexible time retrieval in ContinuousObservable to work with different Mesa time management approaches. The _get_time() method tries multiple sources in priority order: 1. model.simulator.time (DEVS/continuous models) 2. model.time (if explicitly set) 3. model.steps (fallback for discrete models) This workaround enables ContinuousObservable to function correctly regardless of whether a model uses discrete event simulation, custom time tracking, or standard step-based progression. Obviously this should be fixed structurally. See projectmesa#2228
The ContinuousObservable descriptor was inheriting Observable's __set__ method, which stored raw numeric values instead of ContinuousState objects. This caused AttributeError when __get__ tried to access state.last_update on a float/numpy.float64. The new __set__ method creates a ContinuousState wrapper on first assignment and updates the existing state on subsequent assignments. This ensures the private attribute always contains a properly structured state object with value, last_update, rate_func, and thresholds attributes. Fixes initialization of continuous observables in agent __init__ methods where energy and other time-varying properties are set.
Changes the ContinuousObservable threshold system from storing callbacks directly to using a signal-based subscription pattern: - Change `_thresholds` from dict to set (stores threshold values only) - Update `add_threshold()` to check for existing subscriptions before subscribing to prevent duplicate callback invocations - Separate concerns: thresholds define WHICH values to watch, observers define WHO to notify when crossed - Add ValueError when attempting to add threshold to non-ContinuousObservable This fixes an issue where registering multiple thresholds with the same callback would cause it to be called multiple times per threshold crossing.
Adds test_continuous_observables.py with 19 tests covering the full functionality of continuous state management in mesa_signals: Basic functionality: - Constant and variable rate depletion over time - Manual value setting (e.g., energy boost from eating) - Time-based lazy evaluation Signal emissions: - Change notifications when values update - Threshold crossing detection (upward and downward) - Multiple threshold management - No signals when values don't change Edge cases: - Zero elapsed time (no spurious updates) - Multiple accesses at same time point (single calculation) - Exact threshold values (inclusive boundary handling) - Negative values (energy going below zero) - NumPy float compatibility (fixes AttributeError with random values) Integration: - Computed properties based on continuous observables - Batch agent creation with numpy arrays - Both DEVSimulator (float time) and ABMSimulator (integer steps) - Wolf-sheep predator-prey scenario with death thresholds All tests use proper discrete event scheduling through Mesa's simulator rather than manual time manipulation, ensuring realistic simulation behavior.
|
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:
Curious on initial thoughts! (CI test failures are unrelated, see widgetti/solara#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? |
BREAKING: Thresholds are now stored per-instance instead of per-class.
Previously, ContinuousObservable stored thresholds at the class level in a shared `_thresholds` set. This caused all instances to check the same threshold values, preventing agents from having individual thresholds based on their specific parameters (e.g., a wolf with 100 starting energy couldn't have a different critical threshold than one with 50 starting energy).
Changes:
- Move threshold storage from descriptor to ContinuousState instances
- Change threshold structure from set to dict: {threshold_value: set(callbacks)}
- Update add_threshold() to access and modify instance-level state
- Ensure threshold initialization happens before adding thresholds
- Add direction parameter to all threshold_crossed signal emissions
This allows each agent to maintain its own set of thresholds, enabling instance-specific reactive behaviors like `self.add_threshold("energy", self.starting_energy * 0.25, callback)`.
38688e2 to
b2f6d7f
Compare
|
Great question:
Lazy evaluation and threshold detection work together because thresholds are checked during value recalculation, not continuously. When someone accesses a The key is that we check the range between old and new values, not discrete sample points. With linear integration ( 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 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. |
| self.subscribers[observable][signal_type] = active_observers | ||
|
|
||
| def add_threshold(self, observable_name: str, threshold: float, callback: Callable): | ||
| """Convenience method for adding thresholds.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just nitpicking, but this docstring can use some further elaboration beyond the one line summary. For example, it might be good to give a short explanation of what a treshold is and how it only applies to continuous observables.
|
|
||
| return state.value | ||
|
|
||
| # TODO: A universal truth for time should be implemented structurally in Mesa. See https://github.com/projectmesa/mesa/discussions/2228 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes this really drives home the need for a proper solution.
Also, time should probably be observable, or easy to be made observable, in which case your ContinuousObservable becomes a subclass of Computable.
| Returns: | ||
| List of (threshold_value, direction) tuples for crossed thresholds | ||
| """ | ||
| crossed = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you indicate whether you are only interested in moving above or moving below, or do you get any crossing of the threshold?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see anything obviously wrong here. However, it's been a while since I looked at this code, so I might take another look at this PR annd the rest of signals next week. In the meantime, three clarifying questions.
|
@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: I'm not sure which direction I'm leaning personally yet. |
Summary
Adds
ContinuousObservableto 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:
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:
Computedproperties and dependency trackingAPI usage examples
Usage Examples
Basic continuous energy depletion:
Reactive behaviors with computed properties:
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
ContinuousObservableis an additive featureThis 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.