Skip to content

Add thermostat house example demonstrating continuous states (#492) - #495

Open
jabrailkhalil wants to merge 1 commit into
mesa:mainfrom
jabrailkhalil:thermostat-house-v2
Open

Add thermostat house example demonstrating continuous states (#492)#495
jabrailkhalil wants to merge 1 commit into
mesa:mainfrom
jabrailkhalil:thermostat-house-v2

Conversation

@jabrailkhalil

Copy link
Copy Markdown

Closes #492 (idea 5, the thermostat house)

Fresh PR after the review of #494, which was closed by the maintainer: the first version derived the threshold limits from a setpoint by hand-wiring observe(...) and rewriting the limits manually, while its README claimed the limits were reactive. The claimed mechanism and the code contradicted each other. This version builds the example the other way around.

What changed vs #494

  • The dials are the limits: heat_on_temp / heat_off_temp are plain Observables, and each Threshold watches its limit directly. states.py subscribes the threshold to its limit observable, so moving a dial mid-run re-arms the projected crossing with no call from the agent - the README now says exactly what the code does, and the derived-setpoint machinery is gone.
  • The one genuinely non-declarative edge is kept, isolated and documented: a threshold only fires on a crossing, so a dial jump can strand the temperature outside the new deadband with no crossing back in sight. _rearm corrects that state on the same dial change (heater on below the on-limit, off above the off-limit). The README states this explicitly rather than claiming "no explicit call" anywhere.
  • app.py exposes the two dials directly as sliders.

The example

A single room: temperature as a ContinuousState (base value + rate, extrapolated analytically), two Thresholds computing the exact crossing times for heater-on/heater-off, no step logic (the only step() samples the DataCollector for the plots). Layout follows the tram model: agents.py, model.py, app.py, README.md.

What I verified

All of the following ran locally (Windows, Python 3.12, mesa main at commit a5dfcd6, 2026-09-08):

  • pytest test_examples.py -k ThermostatHouse - PASSED (the repo's auto-runner instantiates the model with no arguments and runs run_for(10)).
  • python model.py - run_for(50), temperature stays inside [heat_on_temp, heat_off_temp] after the cold-start transient (checked with assert inside the model's __main__).
  • Dial changes mid-run, checked by script: warm jump (20.5/21.5 -> 24/26), cool jump (-> 17.8/18.2) and the off-branch (heater on, dials dropped below the current temperature, heater must switch off immediately) - all hold their new deadbands in steady state. Hot-start (initial temperature above the band) starts with the heater off and cools into the band.
  • ruff check / ruff format --check and codespell clean on the example.
  • app.py imports cleanly with solara and matplotlib installed.

The behaviour checks above are the ones I personally ran; TBH on the Falsified-matrix front there is no suite in this repo to falsify against (the auto-runner only steps models), so the dial matrices in the PR are the evidence.

Mesa version

Built against Mesa main commit a5dfcd6 (2026-09-08). The example uses mesa.experimental.* APIs which may change without deprecation. Note: the CI build-pre job installs the latest published mesa (3.5.1) where mesa.experimental.states does not exist; the build-main job (mesa@main) is the one that exercises this example.

AI assistance: this contribution was authored with the help of an AI coding agent (implementation and this description); the local verification listed above was performed by the agent.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Call for examples: Mesa's new event-driven behavior APIs

1 participant