From b6551c6433f20dd7e0aad0132c35d61ab708e675 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Fri, 19 Jun 2026 19:34:15 +0200 Subject: [PATCH] Derive Pot's M/C from pot/grain/water mass instead of hardcoding them Add Sud.derive_plant_params(), which combines pot_mass/pot_material/ grain_mass/water_mass into a single lumped (mass, specific heat) pair using approximate specific-heat constants for water, grain, and (by a small material lookup table) the pot itself. demo_sud.py now builds its plant_params from this instead of hardcoded C/M values, keeping L/Td as the only manually-tuned plant parameters. Co-Authored-By: Claude Sonnet 4.6 --- components/sud.py | 28 ++++++++++++++++++++++++++++ scripts/demos/sud/demo_sud.py | 6 +++--- sude/sud_0010.json | 6 +++--- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/components/sud.py b/components/sud.py index 6d99c9e..6dde2c2 100644 --- a/components/sud.py +++ b/components/sud.py @@ -2,6 +2,15 @@ import json from enum import Enum from utils.value import AttributeChange +# Specific heat capacities [J/(kg*K)] used to derive a single lumped +# (mass, specific heat) pair for the pot's contents from pot_mass/ +# grain_mass/water_mass. Approximate, typical values. +SPECIFIC_HEAT_WATER = 4190 +SPECIFIC_HEAT_GRAIN = 1800 +SPECIFIC_HEAT_BY_MATERIAL = { + "Edelstahl 18/10": 500, +} + class SudState(Enum): IDLE = 0 @@ -21,12 +30,31 @@ class Sud(AttributeChange): self.description = data.get('Description', '') self.schedule = data['schedule'] + self.pot_mass = data.get('pot_mass', 0) + self.pot_material = data.get('pot_material') + self.grain_mass = data.get('grain_mass', 0) + self.water_mass = data.get('water_mass', 0) + self.index = -1 self.hold_remaining = 0.0 self.state = SudState.IDLE self.step = None self.user_message = None + def derive_plant_params(self): + """Lumped (mass, specific heat) lifted from pot_mass/pot_material/ + grain_mass/water_mass, for use as Pot's "M"/"C" params.""" + c_pot = SPECIFIC_HEAT_BY_MATERIAL.get(self.pot_material, SPECIFIC_HEAT_WATER) + mass = self.water_mass + self.grain_mass + self.pot_mass + capacitance = (self.water_mass * SPECIFIC_HEAT_WATER + + self.grain_mass * SPECIFIC_HEAT_GRAIN + + self.pot_mass * c_pot) + + return { + 'M': mass, + 'C': capacitance / mass if mass > 0 else SPECIFIC_HEAT_WATER, + } + def start(self): if self.state != SudState.IDLE: return diff --git a/scripts/demos/sud/demo_sud.py b/scripts/demos/sud/demo_sud.py index 802ce15..157a8c5 100644 --- a/scripts/demos/sud/demo_sud.py +++ b/scripts/demos/sud/demo_sud.py @@ -33,14 +33,14 @@ if __name__ == '__main__': } } + sud = Sud("sude/sud_0010.json") + plant_params = { - "C" : 4190, - "M" : 20, + **sud.derive_plant_params(), "L" : 0.2, "Td" : 30 } - sud = Sud("sude/sud_0010.json") ctrl = TempController(dt, ctrl_params, plant_params, theta_amb) plant = Pot(dt, plant_params, theta_amb) stirrer = StirrerSim(dt) diff --git a/sude/sud_0010.json b/sude/sud_0010.json index fc3b0a6..7af1a18 100644 --- a/sude/sud_0010.json +++ b/sude/sud_0010.json @@ -1,10 +1,10 @@ { "Name" : "Sud-0010", "Description" : "Rotfraenkisch, Dunkles Lager", - "pot_weight_kg" : 5.960, + "pot_mass" : 5.960, "pot_material" : "Edelstahl 18/10", - "Schuettung_kg" : 5.21, - "Wasser_kg" : 22, + "grain_mass" : 5.21, + "water_mass" : 22, "schedule": [ {