Refactor sud JSON pot params under a nested 'pot' key

Top-level pot_mass/pot_material/L/Td/grain_mass/water_mass and their
per-step overrides are now grouped under a 'pot' sub-object, matching
the structure already applied to sud_0010.json's parent level. Updated
all sude/*.json files and the parsing/consuming code in components/sud.py,
tasks/sud.py, components/sud_forecast.py, and scripts/demos/sud/demo_sud.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T52JH848ojhXXHn1bAzdC3
This commit is contained in:
2026-06-26 07:31:27 +02:00
co-authored by Claude Sonnet 4.6
parent 7dd0400278
commit e2e3baab08
9 changed files with 128 additions and 84 deletions
+26 -22
View File
@@ -47,8 +47,9 @@ def _build_step(number, defaults, raw_step):
its own, so it's left None, telling SudTask not to push a new its own, so it's left None, telling SudTask not to push a new
theta_soll and just keep whatever the previous step left running.""" theta_soll and just keep whatever the previous step left running."""
step = {'number': number} step = {'number': number}
for key in ('descr', 'user_message', 'user_wait_for_continue', 'grain_mass', 'water_mass'): for key in ('descr', 'user_message', 'user_wait_for_continue'):
step[key] = raw_step.get(key, defaults.get(key)) step[key] = raw_step.get(key, defaults.get(key))
step['pot'] = _merge_defaults(defaults.get('pot', {}), raw_step.get('pot', {}))
step['temperature'] = raw_step.get('temperature') step['temperature'] = raw_step.get('temperature')
step['ramp'] = _merge_defaults(defaults.get('ramp', {}), raw_step.get('ramp', {})) step['ramp'] = _merge_defaults(defaults.get('ramp', {}), raw_step.get('ramp', {}))
if 'hold' in raw_step: if 'hold' in raw_step:
@@ -59,21 +60,23 @@ def _build_step(number, defaults, raw_step):
EMPTY_SUD = { EMPTY_SUD = {
'Name': '', 'Name': '',
'Description': '', 'Description': '',
'pot_mass': 0, 'pot': {
'pot_material': None, 'mass': 0,
# Pot energy loss coefficient [W/(kg*K)] and transport propagation 'material': None,
# delay [s] - see derive_plant_params(). Defaults match the generic # Pot energy loss coefficient [W/(kg*K)] and transport propagation
# values brewpi.py used to hardcode for every brew alike. # delay [s] - see derive_plant_params(). Defaults match the generic
'L': 0.2, # values brewpi.py used to hardcode for every brew alike.
'Td': 30, 'L': 0.2,
# Initial grain_mass/water_mass - what's actually in the pot before 'Td': 30,
# the brew starts (e.g. water added but no malt yet), as opposed to # Initial grain_mass/water_mass - what's actually in the pot before
# default.step's grain_mass/water_mass, which is just inert template # the brew starts (e.g. water added but no malt yet), as opposed to
# filler for steps that don't override it. Lets a caller derive # default.step's pot.grain_mass/pot.water_mass, which is just inert
# starting plant params directly (see SudTask.recv()'s Load handler) # template filler for steps that don't override it. Lets a caller
# without having to parse the first step out of the schedule. # derive starting plant params directly (see SudTask.recv()'s Load
'grain_mass': 0, # handler) without having to parse the first step out of the schedule.
'water_mass': 0, 'grain_mass': 0,
'water_mass': 0,
},
'steps': [], 'steps': [],
} }
@@ -107,12 +110,13 @@ class Sud(AttributeChange):
step_defaults = data.get('default', {}).get('step', {}) step_defaults = data.get('default', {}).get('step', {})
schedule = [_build_step(i + 1, step_defaults, raw) for i, raw in enumerate(data['steps'])] schedule = [_build_step(i + 1, step_defaults, raw) for i, raw in enumerate(data['steps'])]
pot_mass = data.get('pot_mass', 0) pot_data = data.get('pot', {})
pot_material = data.get('pot_material') pot_mass = pot_data.get('mass', 0)
L = data.get('L', EMPTY_SUD['L']) pot_material = pot_data.get('material')
Td = data.get('Td', EMPTY_SUD['Td']) L = pot_data.get('L', EMPTY_SUD['pot']['L'])
grain_mass = data.get('grain_mass', EMPTY_SUD['grain_mass']) Td = pot_data.get('Td', EMPTY_SUD['pot']['Td'])
water_mass = data.get('water_mass', EMPTY_SUD['water_mass']) grain_mass = pot_data.get('grain_mass', EMPTY_SUD['pot']['grain_mass'])
water_mass = pot_data.get('water_mass', EMPTY_SUD['pot']['water_mass'])
return name, description, schedule, pot_mass, pot_material, L, Td, grain_mass, water_mass return name, description, schedule, pot_mass, pot_material, L, Td, grain_mass, water_mass
+2 -1
View File
@@ -156,7 +156,8 @@ class SudForecastEstimator:
# re-assigns self.step to retrigger it) - only the *first* call # re-assigns self.step to retrigger it) - only the *first* call
# for a given index is its actual start. # for a given index is its actual start.
step_starts.setdefault(sud.index, t[-1]) step_starts.setdefault(sud.index, t[-1])
params = sud.derive_plant_params(step.get('grain_mass', 0), step.get('water_mass', 0)) pot = step.get('pot', {})
params = sud.derive_plant_params(pot.get('grain_mass', 0), pot.get('water_mass', 0))
pot.set_plant_params(params) pot.set_plant_params(params)
tc.set_model_plant_params(params) tc.set_model_plant_params(params)
if sud.state == SudState.RAMPING and step['temperature'] is not None: if sud.state == SudState.RAMPING and step['temperature'] is not None:
+4 -2
View File
@@ -42,7 +42,8 @@ if __name__ == '__main__':
sud.load(json.load(f)) sud.load(json.load(f))
first_step = sud.schedule[0] first_step = sud.schedule[0]
plant_params = sud.derive_plant_params(first_step['grain_mass'], first_step['water_mass']) first_pot = first_step.get('pot', {})
plant_params = sud.derive_plant_params(first_pot.get('grain_mass', 0), first_pot.get('water_mass', 0))
ctrl = TempController(dt) ctrl = TempController(dt)
ctrl.set_params(ctrl_params) ctrl.set_params(ctrl_params)
@@ -74,7 +75,8 @@ if __name__ == '__main__':
stirrer.set_speed(speed) stirrer.set_speed(speed)
def apply_plant_params(step): def apply_plant_params(step):
params = sud.derive_plant_params(step['grain_mass'], step['water_mass']) pot = step.get('pot', {})
params = sud.derive_plant_params(pot.get('grain_mass', 0), pot.get('water_mass', 0))
plant.set_plant_params(params) plant.set_plant_params(params)
ctrl.set_model_plant_params(params) ctrl.set_model_plant_params(params)
+12 -8
View File
@@ -1,19 +1,23 @@
{ {
"Name": "GuiTest", "Name": "GuiTest",
"Description": "Small, fast schedule for exercising the GUI (load/start/pause/stop/restart) without waiting through a real brew", "Description": "Small, fast schedule for exercising the GUI (load/start/pause/stop/restart) without waiting through a real brew",
"pot_mass": 5.96, "pot": {
"pot_material": "Edelstahl 18/10", "mass": 5.96,
"L": 0.2, "material": "Edelstahl 18/10",
"Td": 30, "L": 0.2,
"grain_mass": 5.21, "Td": 30,
"water_mass": 22, "grain_mass": 5.21,
"water_mass": 22
},
"default": { "default": {
"step": { "step": {
"descr": "Put description here", "descr": "Put description here",
"user_message": "Put user message here", "user_message": "Put user message here",
"user_wait_for_continue": false, "user_wait_for_continue": false,
"grain_mass": 5.21, "pot": {
"water_mass": 22, "grain_mass": 5.21,
"water_mass": 22
},
"temperature": 0, "temperature": 0,
"ramp": { "ramp": {
"rate": 5.0, "rate": 5.0,
+12 -8
View File
@@ -1,19 +1,23 @@
{ {
"Name": "GuiTest - No User", "Name": "GuiTest - No User",
"Description": "Small, fast schedule for exercising the GUI (load/start/pause/stop/restart) without waiting through a real brew", "Description": "Small, fast schedule for exercising the GUI (load/start/pause/stop/restart) without waiting through a real brew",
"pot_mass": 5.96, "pot": {
"pot_material": "Edelstahl 18/10", "mass": 5.96,
"L": 0.2, "material": "Edelstahl 18/10",
"Td": 30, "L": 0.2,
"grain_mass": 5.21, "Td": 30,
"water_mass": 22, "grain_mass": 5.21,
"water_mass": 22
},
"default": { "default": {
"step": { "step": {
"descr": "Put description here", "descr": "Put description here",
"user_message": "Put user message here", "user_message": "Put user message here",
"user_wait_for_continue": false, "user_wait_for_continue": false,
"grain_mass": 5.21, "pot": {
"water_mass": 22, "grain_mass": 5.21,
"water_mass": 22
},
"temperature": 0, "temperature": 0,
"ramp": { "ramp": {
"rate": 5.0, "rate": 5.0,
+18 -10
View File
@@ -1,19 +1,23 @@
{ {
"Name": "Sud-0010", "Name": "Sud-0010",
"Description": "Rotfraenkisch, Dunkles Lager", "Description": "Rotfraenkisch, Dunkles Lager",
"pot_mass": 5.96, "pot": {
"pot_material": "Edelstahl 18/10", "mass": 5.96,
"L": 0.2, "material": "Edelstahl 18/10",
"Td": 30, "L": 0.2,
"grain_mass": 0, "Td": 30,
"water_mass": 22, "grain_mass": 0,
"water_mass": 22
},
"default": { "default": {
"step": { "step": {
"descr": "Put description here", "descr": "Put description here",
"user_message": "Put user message here", "user_message": "Put user message here",
"user_wait_for_continue": false, "user_wait_for_continue": false,
"grain_mass": 5.21, "pot": {
"water_mass": 22, "grain_mass": 5.21,
"water_mass": 22
},
"temperature": 0, "temperature": 0,
"ramp": { "ramp": {
"rate": 1.0, "rate": 1.0,
@@ -35,7 +39,9 @@
}, },
"steps": [ "steps": [
{ {
"grain_mass": 0, "pot": {
"grain_mass": 0
},
"descr": "Heizen für Einmaischen", "descr": "Heizen für Einmaischen",
"temperature": 57, "temperature": 57,
"ramp": { "ramp": {
@@ -91,7 +97,9 @@
} }
}, },
{ {
"water_mass": 20, "pot": {
"water_mass": 20
},
"descr": "Abmaischen", "descr": "Abmaischen",
"user_message": "Quittieren zum Abmaischen", "user_message": "Quittieren zum Abmaischen",
"user_wait_for_continue": true, "user_wait_for_continue": true,
+24 -14
View File
@@ -1,19 +1,23 @@
{ {
"Name": "Sud-0010", "Name": "Sud-0010",
"Description": "Rotfraenkisch, Dunkles Lager", "Description": "Rotfraenkisch, Dunkles Lager",
"pot_mass": 5.96, "pot": {
"pot_material": "Edelstahl 18/10", "mass": 5.96,
"L": 0.2, "material": "Edelstahl 18/10",
"Td": 30, "L": 0.2,
"grain_mass": 0, "Td": 30,
"water_mass": 22, "grain_mass": 0,
"water_mass": 22
},
"default": { "default": {
"step": { "step": {
"descr": "Put description here", "descr": "Put description here",
"user_message": "Put user message here", "user_message": "Put user message here",
"user_wait_for_continue": false, "user_wait_for_continue": false,
"grain_mass": 5.21, "pot": {
"water_mass": 22, "grain_mass": 5.21,
"water_mass": 22
},
"ramp": { "ramp": {
"rate": 1.0, "rate": 1.0,
"temp": 0, "temp": 0,
@@ -35,8 +39,10 @@
}, },
"steps": [ "steps": [
{ {
"grain_mass": 0, "pot": {
"descr": "Heizen f\u00fcr Einmaischen", "grain_mass": 0
},
"descr": "Heizen für Einmaischen",
"ramp": { "ramp": {
"rate": 1.8, "rate": 1.8,
"temp": 57, "temp": 57,
@@ -46,8 +52,8 @@
} }
}, },
{ {
"descr": "Einmaischen einf\u00fcllen", "descr": "Einmaischen einfüllen",
"user_message": "Bitte Malz einf\u00fcllen und best\u00e4tigen", "user_message": "Bitte Malz einfüllen und bestätigen",
"user_wait_for_continue": true, "user_wait_for_continue": true,
"hold": { "hold": {
"stirrer": { "stirrer": {
@@ -97,7 +103,9 @@
} }
}, },
{ {
"water_mass": 20, "pot": {
"water_mass": 20
},
"descr": "Heizen Abmaischen", "descr": "Heizen Abmaischen",
"ramp": { "ramp": {
"rate": 1.0, "rate": 1.0,
@@ -105,7 +113,9 @@
} }
}, },
{ {
"water_mass": 20, "pot": {
"water_mass": 20
},
"descr": "Halten Abmaischen", "descr": "Halten Abmaischen",
"user_message": "Quittieren zum Abmaischen", "user_message": "Quittieren zum Abmaischen",
"user_wait_for_continue": true, "user_wait_for_continue": true,
+19 -11
View File
@@ -1,19 +1,23 @@
{ {
"Name": "Sud-0020", "Name": "Sud-0020",
"Description": "Bavarian, Dunkles (Todo: Pot temperature drop at malt fill in)", "Description": "Bavarian, Dunkles (Todo: Pot temperature drop at malt fill in)",
"pot_mass": 5.96, "pot": {
"pot_material": "Edelstahl 18/10", "mass": 5.96,
"L": 0.2, "material": "Edelstahl 18/10",
"Td": 30, "L": 0.2,
"grain_mass": 0, "Td": 30,
"water_mass": 10, "grain_mass": 0,
"water_mass": 10
},
"default": { "default": {
"step": { "step": {
"descr": "Put description here", "descr": "Put description here",
"user_message": "Put user message here", "user_message": "Put user message here",
"user_wait_for_continue": false, "user_wait_for_continue": false,
"grain_mass": 2.51, "pot": {
"water_mass": 10, "grain_mass": 2.51,
"water_mass": 10
},
"temperature": 0, "temperature": 0,
"ramp": { "ramp": {
"rate": 1.0, "rate": 1.0,
@@ -35,7 +39,9 @@
}, },
"steps": [ "steps": [
{ {
"grain_mass": 0, "pot": {
"grain_mass": 0
},
"descr": "Heizen für Einmaischen", "descr": "Heizen für Einmaischen",
"temperature": 57, "temperature": 57,
"ramp": { "ramp": {
@@ -57,7 +63,7 @@
}, },
{ {
"descr": "Einmaischen Rast", "descr": "Einmaischen Rast",
"temperature": 55 , "temperature": 55,
"hold": { "hold": {
"duration": 20 "duration": 20
} }
@@ -92,7 +98,9 @@
} }
}, },
{ {
"water_mass": 20, "pot": {
"water_mass": 20
},
"descr": "Abmaischen", "descr": "Abmaischen",
"user_message": "Quittieren zum Abmaischen", "user_message": "Quittieren zum Abmaischen",
"user_wait_for_continue": true, "user_wait_for_continue": true,
+10 -7
View File
@@ -164,7 +164,8 @@ class SudTask(ATask):
phase = ramp if ramping else hold phase = ramp if ramping else hold
if step is not None: if step is not None:
self.apply_plant_params(step.get('grain_mass', 0), step.get('water_mass', 0)) pot = step.get('pot', {})
self.apply_plant_params(pot.get('grain_mass', 0), pot.get('water_mass', 0))
if ramping and step['temperature'] is not None: if ramping and step['temperature'] is not None:
self.tc.set_theta_soll(step['temperature']) self.tc.set_theta_soll(step['temperature'])
self.tc.set_heatrate_soll(ramp['rate']) self.tc.set_heatrate_soll(ramp['rate'])
@@ -383,12 +384,14 @@ class SudTask(ATask):
doc = { doc = {
'Name': self.sud.name, 'Name': self.sud.name,
'Description': self.sud.description, 'Description': self.sud.description,
'pot_mass': self.sud.pot_mass, 'pot': {
'pot_material': self.sud.pot_material, 'mass': self.sud.pot_mass,
'L': self.sud.L, 'material': self.sud.pot_material,
'Td': self.sud.Td, 'L': self.sud.L,
'grain_mass': self.sud.grain_mass, 'Td': self.sud.Td,
'water_mass': self.sud.water_mass, 'grain_mass': self.sud.grain_mass,
'water_mass': self.sud.water_mass,
},
'steps': schedule[index:], 'steps': schedule[index:],
} }
start_theta = self.tc.get_theta_ist() start_theta = self.tc.get_theta_ist()