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
This commit is contained in:
2026-06-22 18:45:07 +02:00
co-authored by Claude Sonnet 4.6
parent 96fe7ce90c
commit f1688d3c71
12 changed files with 59 additions and 41 deletions
+23 -12
View File
@@ -61,6 +61,11 @@ EMPTY_SUD = {
'Description': '',
'pot_mass': 0,
'pot_material': None,
# Pot energy loss coefficient [W/(kg*K)] and transport propagation
# delay [s] - see derive_plant_params(). Defaults match the generic
# values brewpi.py used to hardcode for every brew alike.
'L': 0.2,
'Td': 30,
'steps': [],
}
@@ -74,8 +79,8 @@ class Sud(AttributeChange):
AttributeChange.__init__(self)
self._data = EMPTY_SUD
(self.name, self.description, self.schedule,
self.pot_mass, self.pot_material) = self._parse_data(self._data)
(self.name, self.description, self.schedule, self.pot_mass,
self.pot_material, self.L, self.Td) = self._parse_data(self._data)
self._paused_from = None
self._reset_run_state()
@@ -83,9 +88,9 @@ class Sud(AttributeChange):
@staticmethod
def _parse_data(data):
"""Parses a sud.json document into the (name, description, schedule,
pot_mass, pot_material) tuple Sud needs. Computed up front rather
than assigned straight onto self, so a malformed load() can't
leave a half-applied schedule in place."""
pot_mass, pot_material, L, Td) tuple Sud needs. Computed up front
rather than assigned straight onto self, so a malformed load()
can't leave a half-applied schedule in place."""
name = data.get('Name', '')
description = data.get('Description', '')
@@ -94,8 +99,10 @@ class Sud(AttributeChange):
pot_mass = data.get('pot_mass', 0)
pot_material = data.get('pot_material')
L = data.get('L', EMPTY_SUD['L'])
Td = data.get('Td', EMPTY_SUD['Td'])
return name, description, schedule, pot_mass, pot_material
return name, description, schedule, pot_mass, pot_material, L, Td
def _reset_run_state(self):
"""Resets run-time progress back to a freshly-loaded, not-yet-started
@@ -128,18 +135,20 @@ class Sud(AttributeChange):
except (KeyError, TypeError):
return False
self.name, self.description, self.schedule, self.pot_mass, self.pot_material = parsed
(self.name, self.description, self.schedule, self.pot_mass,
self.pot_material, self.L, self.Td) = parsed
self._data = data
self._reset_run_state()
return True
def derive_plant_params(self, grain_mass, water_mass):
"""Lumped (mass, specific heat) lifted from pot_mass/pot_material and
the current step's grain_mass/water_mass, for use as Pot's "M"/"C"
params. grain_mass/water_mass vary per step (e.g. malt going in,
water boiling off), so this is recomputed on every step change
rather than once at startup."""
"""Full Pot plant params - "M"/"C" lumped (mass, specific heat)
from pot_mass/pot_material and the current step's grain_mass/
water_mass (these vary per step, e.g. malt going in, water
boiling off, so this is recomputed on every step change rather
than once at startup), plus "L"/"Td" straight from this Sud's
own doc (constant for the whole brew - see Sud.load())."""
c_pot = SPECIFIC_HEAT_BY_MATERIAL.get(self.pot_material, SPECIFIC_HEAT_WATER)
mass = water_mass + grain_mass + self.pot_mass
capacitance = (water_mass * SPECIFIC_HEAT_WATER
@@ -149,6 +158,8 @@ class Sud(AttributeChange):
return {
'M': mass,
'C': capacitance / mass if mass > 0 else SPECIFIC_HEAT_WATER,
'L': self.L,
'Td': self.Td,
}
def start(self):