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)))