Skip to content

Thermostat

A room heated by a furnace under a thermostat while heat leaks to the outside.

  • Name: thermostat
  • Field: engineering/control
  • Formalism: ordinary differential equations
  • References: Meadows 2008

Factory

@register_model(
    field='engineering/control',
    tags=('control', 'feedback', 'stocks and flows', 'ode'),
    references=('Meadows 2008',),
    defaults={'until': 24.0, 'dt': 0.25},
)
def thermostat(
    T: float = 10.0,
    *,
    setting: float = 18.0,
    furnace: float = 5.0,
    leak: float = 0.1,
    outside: float = 10.0,
    swing: float = 0.0,
) -> System:
    """Return a room heated by a furnace under a thermostat while heat leaks to the outside.

    The furnace adds the gap between the setting and the room temperature, at most `furnace` degrees per hour, and
    nothing when the room is warmer than the setting. The room loses `leak` of its difference to the outside
    temperature per hour, so it settles below the setting, at `(setting + leak * outside) / (1 + leak)` for a
    constant outside. The outside temperature swings by `swing` around `outside` over a day, warmest at noon.

    Args:
        T:
            The initial room temperature.

        setting:
            The thermostat setting.

        furnace:
            The largest heating rate of the furnace, in degrees per hour.

        leak:
            The fraction of the difference to the outside lost per hour.

        outside:
            The mean outside temperature.

        swing:
            The amplitude of the daily swing of the outside temperature.

    Returns:
        system:
            The model, with the room temperature `room.T` in hours.

    Examples:
        >>> from skcomplex.models.engineering.control import thermostat
        >>> thermostat(leak=0.3).elements[0].leak
        0.3
    """
    room = RoomElement('room', T=T, setting=setting, furnace=furnace, leak=leak, outside=outside, swing=swing)
    return System(elements=[room])

Components

class RoomElement(Element):
    """Room heated by a furnace towards a setting and leaking heat to an outside temperature with a daily swing."""

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        T: Annotated[object, StateSpec()] = 10.0,
        setting: Annotated[object, ParameterSpec()] = 18.0,
        furnace: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 5.0,
        leak: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
        outside: Annotated[object, ParameterSpec()] = 10.0,
        swing: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.T = T
        self.setting = setting
        self.furnace = furnace
        self.leak = leak
        self.outside = outside
        self.swing = swing

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the saturated heating and the leak of the temperature `T`."""
        return [
            RateLaw(
                s.T,
                clip(s.setting - s.T, 0, s.furnace)
                - s.leak * (s.T - s.outside - s.swing * cos(2 * pi * (s.t - HOURS_PER_DAY / 2) / HOURS_PER_DAY)),
            ),
        ]

Equations

room.T: diff(T_room(t), t) == (swing_room*cos(-1.0*pi + 0.08333333333333333*pi*t) + outside_room - T_room(t))*leak_room + where(setting_room - T_room(t) < 0, 0, where(setting_room - T_room(t) > furnace_room, furnace_room, setting_room - T_room(t)))