Require TempController(Smith)'s model params/ambient via setters; add comprehensive process() checks

TempController(Smith).__init__ no longer takes model_params/theta_amb -
set_model_plant_params() and the existing set_ambient_temperature() must be
called instead (mirrors components/plant/pot.py's own Pot constructor
refactor). temp_controller.py's now-pointless model_params/theta_amb
constructor placeholders (kept only for "compatibility" with Smith) are
dropped too.

Both TempController variants now raise a clear RuntimeError from process()
itself if set_params() wasn't called, instead of letting it surface deep
inside Pid.process() as an opaque "'NoneType' object is not subscriptable".
Smith additionally checks its internal models via Pot.is_configured() (new)
and raises if set_model_plant_params()/set_ambient_temperature() weren't
called either.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DkkuG48uHFCGKe6dPSERFk
This commit is contained in:
2026-06-22 18:31:58 +02:00
co-authored by Claude Sonnet 4.6
parent 0a6d3e6462
commit 96fe7ce90c
8 changed files with 50 additions and 13 deletions
+3 -4
View File
@@ -2,16 +2,15 @@ from components.pid.temp_controller_base import TempControllerBase
class TempController(TempControllerBase):
def __init__(self, dt, model_params=None, theta_amb=20):
# model_params/theta_amb are accepted (but unused) for constructor
# compatibility with TempControllerSmith - PidFactory.create() calls
# either with the same arguments; "Normal" has no internal model.
def __init__(self, dt):
TempControllerBase.__init__(self, dt)
self.dt = dt
self.last_theta_ist = 20
self.heatrate_ist = 0
def process(self):
self._require_params()
# Process Kalman
self.theta_ist = self.theta_ist_set
heatrate = (self.theta_ist - self.last_theta_ist)/self.dt*60
+10
View File
@@ -65,6 +65,16 @@ class TempControllerBase(APid):
self.pid_heat.set_params(params['Heat'])
self.pid_cool.set_params(params['Cool'])
def _require_params(self):
"""Called by each subclass's process() before doing anything else -
without it, a missing set_params() call would otherwise only
surface as an unhelpful "'NoneType' object is not subscriptable"
buried inside Pid.process() (components/pid/pid.py), once
process_pid() below gets to it."""
if self.params is None:
raise RuntimeError(
"{}.process(): PID gains not set - call set_params() first".format(type(self).__name__))
def on_state_entered(self, state):
pass
+11 -5
View File
@@ -3,7 +3,7 @@ from components.pid.temp_controller_base import TempControllerBase, States
class TempController(TempControllerBase):
def __init__(self, dt, model_params, theta_amb=20):
def __init__(self, dt):
TempControllerBase.__init__(self, dt)
self.dt = dt
self.last_theta_ist = 20
@@ -12,18 +12,18 @@ class TempController(TempControllerBase):
# Fast model: same plant model but with zero transport delay, used
# to predict the current temperature without the dead time.
self.model = Pot(dt)
self.model.set_plant_params({**model_params, 'Td': 0})
self.model.set_ambient_temperature(theta_amb)
# Delayed model: keeps the plant's assumed transport delay, so it
# can be compared like-for-like against the real (delayed) measurement.
self.model_delay = Pot(dt)
self.model_delay.set_plant_params(model_params)
self.model_delay.set_ambient_temperature(theta_amb)
self.theta_ist_plant = 0
self.theta_ist_model = 0
self.theta_ist_model_delay = 0
def set_model_plant_params(self, model_params):
self.model.set_plant_params({**model_params, 'Td': 0})
self.model_delay.set_plant_params(model_params)
def set_model_power(self, power):
self.model.set_power(power)
self.model_delay.set_power(power)
@@ -46,6 +46,12 @@ class TempController(TempControllerBase):
self.model_delay.process()
def process(self):
self._require_params()
if not (self.model.is_configured() and self.model_delay.is_configured()):
raise RuntimeError(
"{}.process(): model plant params and/or ambient temperature not set - "
"call set_model_plant_params() and set_ambient_temperature() first".format(type(self).__name__))
self.theta_ist_plant = self.theta_ist_set
self.theta_ist_model = self.model.get_temperature()
self.theta_ist_model_delay = self.model_delay.get_temperature()
+8
View File
@@ -57,6 +57,14 @@ class Pot(APlant):
def initial(self, temp):
self.temp = temp
def is_configured(self):
"""Whether both set_plant_params() and set_ambient_temperature()
have been called - lets a caller driving this Pot as an internal
model (e.g. TempController(Smith)) check upfront and raise its
own comprehensive error, rather than letting process() fail on
whichever of the two happens to be missing."""
return self.delay is not None and self.theta_amb is not None
def activate(self, enable):
pass
+5 -1
View File
@@ -65,8 +65,12 @@ class SudForecastEstimator:
pot.set_plant_params(self.plant_params)
pot.set_ambient_temperature(self.theta_amb)
pot.initial(start_theta)
tc = PidFactory.create(self.pid_type, self.dt, self.plant_params, theta_amb=self.theta_amb)
tc = PidFactory.create(self.pid_type, self.dt)
tc.set_params(self.tempctrl_params)
if hasattr(tc, 'set_model_plant_params'):
tc.set_model_plant_params(self.plant_params)
if hasattr(tc, 'set_ambient_temperature'):
tc.set_ambient_temperature(self.theta_amb)
tc.set_enabled(True)
tc.set_theta_ist(pot.get_temperature())
# Seed the target at start_theta - this tc is a fresh, throwaway
@@ -43,8 +43,10 @@ if __name__ == '__main__':
temp_ist = 0
temp_soll = 20
heatrate_soll = 1.25
ctrl = TempController(dt, plant_params, theta_amb)
ctrl = TempController(dt)
ctrl.set_params(ctrl_params)
ctrl.set_model_plant_params(plant_params)
ctrl.set_ambient_temperature(theta_amb)
ctrl.set_enabled(True)
plant = Pot(dt)
plant.set_plant_params(plant_params)
+3 -1
View File
@@ -48,8 +48,10 @@ if __name__ == '__main__':
"Td" : 60
}
ctrl = TempController(dt, plant_params, theta_amb)
ctrl = TempController(dt)
ctrl.set_params(ctrl_params)
ctrl.set_model_plant_params(plant_params)
ctrl.set_ambient_temperature(theta_amb)
plant = Pot(dt)
plant.set_plant_params(plant_params)
plant.set_ambient_temperature(theta_amb)
+7 -1
View File
@@ -88,8 +88,14 @@ if __name__ == '__main__':
taskmgr.add(heater_task)
# Temperature Controller
tc = PidFactory.create(config['Controller']['pid_type'], DT, DEFAULT_PLANT_PARAMS, theta_amb=theta_amb)
tc = PidFactory.create(config['Controller']['pid_type'], DT)
tc.set_params(config['TempCtrl'])
# "Normal" has no internal model/ambient - see temp_controller.py vs.
# temp_controller_smith.py.
if hasattr(tc, 'set_model_plant_params'):
tc.set_model_plant_params(DEFAULT_PLANT_PARAMS)
if hasattr(tc, 'set_ambient_temperature'):
tc.set_ambient_temperature(theta_amb)
tc_task = TcTask(tc, DT_TASK, dispatcher.msgio_get("TempCtrl"))
taskmgr.add(tc_task)