Files
jensandClaude Sonnet 4.6 f1688d3c71 Derive Pot's L/Td from the Sud doc instead of a fixed server default
Sud now parses top-level L/Td fields from sude/*.json (defaulting to 0.2/30,
matching the old hardcoded server constants), and derive_plant_params()
returns them alongside M/C - constant for the whole brew, unlike M/C which
vary per step with grain/water mass. All sude/*.json docs gain explicit
L/Td fields.

Callers (tasks/sud.py, SudForecastEstimator, demo_sud.py) switch from the
narrow set_thermal_params(M, C)/set_model_params(M, C) to the full
set_plant_params(params)/set_model_plant_params(params), since
derive_plant_params() now always returns all four keys; both narrow setters
are removed as dead code.

Caught along the way: Pot.set_plant_params() unconditionally rebuilt the
Delay ring buffer, which was harmless when only ever called once at
construction - but now running on every Sud step change, it was wiping
in-flight delayed power at each step boundary. Fixed to only rebuild when
Td actually changes; verified this restores the exact original forecast
result for sud_0010.json.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DkkuG48uHFCGKe6dPSERFk
2026-06-22 18:45:07 +02:00

106 lines
3.2 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']
# Only rebuilds the delay line if Td actually changed - this now
# gets called on every Sud step change (M/C vary with grain/water
# mass), not just once at construction, and rebuilding unconditionally
# would zero out whatever power was already in flight through the
# transport delay at every single step transition.
Td = params['Td']
if Td != self.Td:
self.Td = 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_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