Files
brewpi/components/plant/pot.py
T
jensandClaude Sonnet 4.6 96fe7ce90c 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
2026-06-22 18:31:58 +02:00

103 lines
2.9 KiB
Python

from components.aplant import APlant
from components.plant import delay
class Pot(APlant):
def __init__(self, dt):
APlant.__init__(self)
self.dt = dt
self.e = 0
self.x = 0
self.p_pot = 0
# Plant specific thermal capacity [W*s/(kg*K)], mass [kg], energy
# loss coefficient [W/(kg*K)] and transport propagation delay [s]
# - all None until set_plant_params() is called; process() checks
# for that rather than silently computing on bogus values.
self.C = None
self.M = None
self.L = None
self.Td = None
self.delay = None
# Plant temperature [°C] - seeded from theta_amb the first time
# set_ambient_temperature() is called (see there), or overridden
# explicitly via initial(). None until either happens;
# process() checks for that too.
self.temp = None
# Ambient temperature [°C] - None until set_ambient_temperature()
# is called; process() checks for that as well.
self.theta_amb = None
# Set power [W]
self.p_in = 0
def set_plant_params(self, params):
self.C = params['C']
self.M = params['M']
# Negative input power as a function of plant mass and
# temperature difference T_plant and T_ambient:
# P_loss = L * Mass * (T_plant - T_ambient)
self.L = params['L']
self.Td = params['Td']
self.delay = delay.Delay(self.dt, self.Td, 0)
def set_ambient_temperature(self, theta_amb):
self.theta_amb = theta_amb
# Only seeds the plant's own starting temperature the first time
# this is called (i.e. there's no real temperature yet) - once a
# run is in progress, changing the ambient setting must not
# clobber whatever temperature has actually been simulated since.
if self.temp is None:
self.temp = theta_amb
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
def process(self):
if self.delay is None:
raise RuntimeError("Pot.process(): plant params not set - call set_plant_params() first")
if self.theta_amb is None:
raise RuntimeError("Pot.process(): ambient temperature not set - call set_ambient_temperature() first")
self.delay.put(self.p_in)
p_loss = self.L * self.M * (self.temp - self.theta_amb)
self.p_pot = self.delay.get() - p_loss
self.temp = min(100, self.temp + self.p_pot/(self.M * self.C) * self.dt)
def is_activated(self):
return True
def set_thermal_params(self, M, C):
self.M = M
self.C = C
def set_power(self, power):
self.p_in = power
def get_power(self):
return round(self.p_in, 1)
def get_temperature(self):
return self.temp
def get_p_pot(self):
return self.p_pot