Thermostat Simulation

A thermostat simulation showing balancing feedback and time delays.

Level:Beginner

controltime-delaybalancing-loop

  • Stocks:indoor_temp
  • Flows:heat_gain, heat_loss
  • Feedback Loops:controller chasing set-point (balancing), high gain overshoot (reinforcing)
  • Probes:indoor_temp, heater_on

Delays

Learn about delays in systems, how they create oscillations, and their impact on system behavior and decision-making.

Explore Delays

Feedback Loops

Understand the balancing and reinforcing feedback loops that drive system behavior and create complex dynamics in systems thinking.

Explore Feedback Loops
simulation.py

Thermostat control with delayed sensing

Here a basic on/off controller tries to keep the room comfortable, but the temperature sensor lags behind reality. That delay causes overshoot and a gentle oscillation around the setpoint.


from tys import probe, progress

Simulate a thermostat with delayed temperature sensing.

def simulate(cfg: dict):

    import simpy
    env = simpy.Environment()

Parameters

    indoor_temp     = cfg["initial_temp"]       # indoor temp (°C)
    outside_temp    = cfg["outside_temp"]       # fixed outside temp
    setpoint        = cfg["setpoint"]           # desired indoor temp
    band            = cfg["comfort_band"]       # ±°C accepted dead-band
    loss_rate       = cfg["loss_coeff"]         # heat-loss constant
    heater_kw       = cfg["heater_power"]       # °C per tick when on
    control_gain    = cfg["gain"]               # controller aggressiveness
    sensor_delay    = cfg["sensor_delay"]       # perception delay (ticks)
    sim_time        = cfg["sim_time"]
    sensor_history  = [indoor_temp] * sensor_delay    # delay line for sensed temp
    heater_on       = False

Recorder Log the indoor temperature and heater state.

    def recorder():
        while True:
            probe("indoor_temp", env.now, indoor_temp)
            probe("heater_on", env.now, 1 if heater_on else 0)
            yield env.timeout(0.5)
    env.process(recorder())

    done = env.event()

Dynamics Update temperature and controller every tick.

    def dynamics():
        nonlocal indoor_temp, heater_on
        for t in range(sim_time):
            sensor_T     = sensor_history[0]
            error        = setpoint - sensor_T
            heater_on    = error > control_gain * band        # bang-bang with gain

            heat_gain    = heater_kw if heater_on else 0
            heat_loss    = loss_rate * (indoor_temp - outside_temp)
            indoor_temp += heat_gain - heat_loss

            sensor_history.append(indoor_temp)
            sensor_history.pop(0)

            if abs(error) < band / 2 and t % 5 == 0:
                progress(int(100 * t / sim_time), "😌 In comfort zone")
            if abs(error) > 3 * band and heater_on:
                progress(int(100 * t / sim_time), "⚠︎ Big overshoot, heater still ON")

            yield env.timeout(1)

        progress(100)
        done.succeed({
            "final_temp": indoor_temp,
            "overshoot": max(sensor_history) - setpoint,
        })

    env.process(dynamics())
    env.run(until=done)
    return done.value


def requirements():
    return {
        "builtin": ["micropip", "pyyaml"],
        "external": ["simpy==4.1.1"],
    }
Default.yaml
initial_temp: 18
outside_temp: 5
setpoint: 21
comfort_band: 0.5
loss_coeff: 0.05
heater_power: 0.8
gain: 1.0
sensor_delay: 3
sim_time: 120
Charts (Default)

indoor_temp

indoor_temp chartCSV
Samples241 @ 0.00–120.00
Valuesmin 18.00, mean 19.66, median 19.78, max 20.56, σ 0.68

heater_on

heater_on chartCSV
Samples241 @ 0.00–120.00
Valuesmin 0.00, mean 0.92, median 1.00, max 1.00, σ 0.27
Final Results (Default)
MetricValue
final_temp18.83
overshoot-2.17
FAQ
Why does the system overshoot the setpoint?
The heater uses a delayed sensor reading, so decisions lag behind the true temperature and overshoot occurs.
How is the heater turned on or off?
The controller compares the sensed error against gain * band and toggles the heater accordingly.
What does sensor_delay represent?
It is the number of ticks between the actual temperature and the value the controller reads.