From e2e3baab08f0f7dd8f4490590e7b566e898e598c Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Fri, 26 Jun 2026 07:31:27 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01T52JH848ojhXXHn1bAzdC3 --- components/sud.py | 48 +++++++++++++++++++---------------- components/sud_forecast.py | 3 ++- scripts/demos/sud/demo_sud.py | 6 +++-- sude/GuiTest.json | 20 +++++++++------ sude/GuiTest_no_user.json | 20 +++++++++------ sude/sud_0010.json | 28 ++++++++++++-------- sude/sud_0010_downloaded.json | 40 ++++++++++++++++++----------- sude/sud_0020.json | 30 ++++++++++++++-------- tasks/sud.py | 17 ++++++++----- 9 files changed, 128 insertions(+), 84 deletions(-) diff --git a/components/sud.py b/components/sud.py index 968036a..2bc4ef3 100644 --- a/components/sud.py +++ b/components/sud.py @@ -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 theta_soll and just keep whatever the previous step left running.""" 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['pot'] = _merge_defaults(defaults.get('pot', {}), raw_step.get('pot', {})) step['temperature'] = raw_step.get('temperature') step['ramp'] = _merge_defaults(defaults.get('ramp', {}), raw_step.get('ramp', {})) if 'hold' in raw_step: @@ -59,21 +60,23 @@ def _build_step(number, defaults, raw_step): EMPTY_SUD = { 'Name': '', '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, - # Initial grain_mass/water_mass - what's actually in the pot before - # the brew starts (e.g. water added but no malt yet), as opposed to - # default.step's grain_mass/water_mass, which is just inert template - # filler for steps that don't override it. Lets a caller derive - # starting plant params directly (see SudTask.recv()'s Load handler) - # without having to parse the first step out of the schedule. - 'grain_mass': 0, - 'water_mass': 0, + 'pot': { + 'mass': 0, + '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, + # Initial grain_mass/water_mass - what's actually in the pot before + # the brew starts (e.g. water added but no malt yet), as opposed to + # default.step's pot.grain_mass/pot.water_mass, which is just inert + # template filler for steps that don't override it. Lets a caller + # derive starting plant params directly (see SudTask.recv()'s Load + # handler) without having to parse the first step out of the schedule. + 'grain_mass': 0, + 'water_mass': 0, + }, 'steps': [], } @@ -107,12 +110,13 @@ class Sud(AttributeChange): step_defaults = data.get('default', {}).get('step', {}) schedule = [_build_step(i + 1, step_defaults, raw) for i, raw in enumerate(data['steps'])] - 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']) - grain_mass = data.get('grain_mass', EMPTY_SUD['grain_mass']) - water_mass = data.get('water_mass', EMPTY_SUD['water_mass']) + pot_data = data.get('pot', {}) + pot_mass = pot_data.get('mass', 0) + pot_material = pot_data.get('material') + L = pot_data.get('L', EMPTY_SUD['pot']['L']) + Td = pot_data.get('Td', EMPTY_SUD['pot']['Td']) + 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 diff --git a/components/sud_forecast.py b/components/sud_forecast.py index cdb3666..cd8238d 100644 --- a/components/sud_forecast.py +++ b/components/sud_forecast.py @@ -156,7 +156,8 @@ class SudForecastEstimator: # re-assigns self.step to retrigger it) - only the *first* call # for a given index is its actual start. 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) tc.set_model_plant_params(params) if sud.state == SudState.RAMPING and step['temperature'] is not None: diff --git a/scripts/demos/sud/demo_sud.py b/scripts/demos/sud/demo_sud.py index db192ee..77a8189 100644 --- a/scripts/demos/sud/demo_sud.py +++ b/scripts/demos/sud/demo_sud.py @@ -42,7 +42,8 @@ if __name__ == '__main__': sud.load(json.load(f)) 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.set_params(ctrl_params) @@ -74,7 +75,8 @@ if __name__ == '__main__': stirrer.set_speed(speed) 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) ctrl.set_model_plant_params(params) diff --git a/sude/GuiTest.json b/sude/GuiTest.json index bb5753c..c006116 100644 --- a/sude/GuiTest.json +++ b/sude/GuiTest.json @@ -1,19 +1,23 @@ { "Name": "GuiTest", "Description": "Small, fast schedule for exercising the GUI (load/start/pause/stop/restart) without waiting through a real brew", - "pot_mass": 5.96, - "pot_material": "Edelstahl 18/10", - "L": 0.2, - "Td": 30, - "grain_mass": 5.21, - "water_mass": 22, + "pot": { + "mass": 5.96, + "material": "Edelstahl 18/10", + "L": 0.2, + "Td": 30, + "grain_mass": 5.21, + "water_mass": 22 + }, "default": { "step": { "descr": "Put description here", "user_message": "Put user message here", "user_wait_for_continue": false, - "grain_mass": 5.21, - "water_mass": 22, + "pot": { + "grain_mass": 5.21, + "water_mass": 22 + }, "temperature": 0, "ramp": { "rate": 5.0, diff --git a/sude/GuiTest_no_user.json b/sude/GuiTest_no_user.json index 89be929..ced0796 100644 --- a/sude/GuiTest_no_user.json +++ b/sude/GuiTest_no_user.json @@ -1,19 +1,23 @@ { "Name": "GuiTest - No User", "Description": "Small, fast schedule for exercising the GUI (load/start/pause/stop/restart) without waiting through a real brew", - "pot_mass": 5.96, - "pot_material": "Edelstahl 18/10", - "L": 0.2, - "Td": 30, - "grain_mass": 5.21, - "water_mass": 22, + "pot": { + "mass": 5.96, + "material": "Edelstahl 18/10", + "L": 0.2, + "Td": 30, + "grain_mass": 5.21, + "water_mass": 22 + }, "default": { "step": { "descr": "Put description here", "user_message": "Put user message here", "user_wait_for_continue": false, - "grain_mass": 5.21, - "water_mass": 22, + "pot": { + "grain_mass": 5.21, + "water_mass": 22 + }, "temperature": 0, "ramp": { "rate": 5.0, diff --git a/sude/sud_0010.json b/sude/sud_0010.json index 9cca489..a11ec22 100644 --- a/sude/sud_0010.json +++ b/sude/sud_0010.json @@ -1,19 +1,23 @@ { "Name": "Sud-0010", "Description": "Rotfraenkisch, Dunkles Lager", - "pot_mass": 5.96, - "pot_material": "Edelstahl 18/10", - "L": 0.2, - "Td": 30, - "grain_mass": 0, - "water_mass": 22, + "pot": { + "mass": 5.96, + "material": "Edelstahl 18/10", + "L": 0.2, + "Td": 30, + "grain_mass": 0, + "water_mass": 22 + }, "default": { "step": { "descr": "Put description here", "user_message": "Put user message here", "user_wait_for_continue": false, - "grain_mass": 5.21, - "water_mass": 22, + "pot": { + "grain_mass": 5.21, + "water_mass": 22 + }, "temperature": 0, "ramp": { "rate": 1.0, @@ -35,7 +39,9 @@ }, "steps": [ { - "grain_mass": 0, + "pot": { + "grain_mass": 0 + }, "descr": "Heizen für Einmaischen", "temperature": 57, "ramp": { @@ -91,7 +97,9 @@ } }, { - "water_mass": 20, + "pot": { + "water_mass": 20 + }, "descr": "Abmaischen", "user_message": "Quittieren zum Abmaischen", "user_wait_for_continue": true, diff --git a/sude/sud_0010_downloaded.json b/sude/sud_0010_downloaded.json index 8dd7a7b..7797480 100644 --- a/sude/sud_0010_downloaded.json +++ b/sude/sud_0010_downloaded.json @@ -1,19 +1,23 @@ { "Name": "Sud-0010", "Description": "Rotfraenkisch, Dunkles Lager", - "pot_mass": 5.96, - "pot_material": "Edelstahl 18/10", - "L": 0.2, - "Td": 30, - "grain_mass": 0, - "water_mass": 22, + "pot": { + "mass": 5.96, + "material": "Edelstahl 18/10", + "L": 0.2, + "Td": 30, + "grain_mass": 0, + "water_mass": 22 + }, "default": { "step": { "descr": "Put description here", "user_message": "Put user message here", "user_wait_for_continue": false, - "grain_mass": 5.21, - "water_mass": 22, + "pot": { + "grain_mass": 5.21, + "water_mass": 22 + }, "ramp": { "rate": 1.0, "temp": 0, @@ -35,8 +39,10 @@ }, "steps": [ { - "grain_mass": 0, - "descr": "Heizen f\u00fcr Einmaischen", + "pot": { + "grain_mass": 0 + }, + "descr": "Heizen für Einmaischen", "ramp": { "rate": 1.8, "temp": 57, @@ -46,8 +52,8 @@ } }, { - "descr": "Einmaischen einf\u00fcllen", - "user_message": "Bitte Malz einf\u00fcllen und best\u00e4tigen", + "descr": "Einmaischen einfüllen", + "user_message": "Bitte Malz einfüllen und bestätigen", "user_wait_for_continue": true, "hold": { "stirrer": { @@ -97,7 +103,9 @@ } }, { - "water_mass": 20, + "pot": { + "water_mass": 20 + }, "descr": "Heizen Abmaischen", "ramp": { "rate": 1.0, @@ -105,7 +113,9 @@ } }, { - "water_mass": 20, + "pot": { + "water_mass": 20 + }, "descr": "Halten Abmaischen", "user_message": "Quittieren zum Abmaischen", "user_wait_for_continue": true, @@ -117,4 +127,4 @@ } } ] -} \ No newline at end of file +} diff --git a/sude/sud_0020.json b/sude/sud_0020.json index 2bde624..836bd6d 100644 --- a/sude/sud_0020.json +++ b/sude/sud_0020.json @@ -1,19 +1,23 @@ { "Name": "Sud-0020", "Description": "Bavarian, Dunkles (Todo: Pot temperature drop at malt fill in)", - "pot_mass": 5.96, - "pot_material": "Edelstahl 18/10", - "L": 0.2, - "Td": 30, - "grain_mass": 0, - "water_mass": 10, + "pot": { + "mass": 5.96, + "material": "Edelstahl 18/10", + "L": 0.2, + "Td": 30, + "grain_mass": 0, + "water_mass": 10 + }, "default": { "step": { "descr": "Put description here", "user_message": "Put user message here", "user_wait_for_continue": false, - "grain_mass": 2.51, - "water_mass": 10, + "pot": { + "grain_mass": 2.51, + "water_mass": 10 + }, "temperature": 0, "ramp": { "rate": 1.0, @@ -35,7 +39,9 @@ }, "steps": [ { - "grain_mass": 0, + "pot": { + "grain_mass": 0 + }, "descr": "Heizen für Einmaischen", "temperature": 57, "ramp": { @@ -57,7 +63,7 @@ }, { "descr": "Einmaischen Rast", - "temperature": 55 , + "temperature": 55, "hold": { "duration": 20 } @@ -92,7 +98,9 @@ } }, { - "water_mass": 20, + "pot": { + "water_mass": 20 + }, "descr": "Abmaischen", "user_message": "Quittieren zum Abmaischen", "user_wait_for_continue": true, diff --git a/tasks/sud.py b/tasks/sud.py index 3e43aca..29ba7a2 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -164,7 +164,8 @@ class SudTask(ATask): phase = ramp if ramping else hold 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: self.tc.set_theta_soll(step['temperature']) self.tc.set_heatrate_soll(ramp['rate']) @@ -383,12 +384,14 @@ class SudTask(ATask): doc = { 'Name': self.sud.name, 'Description': self.sud.description, - 'pot_mass': self.sud.pot_mass, - 'pot_material': self.sud.pot_material, - 'L': self.sud.L, - 'Td': self.sud.Td, - 'grain_mass': self.sud.grain_mass, - 'water_mass': self.sud.water_mass, + 'pot': { + 'mass': self.sud.pot_mass, + 'material': self.sud.pot_material, + 'L': self.sud.L, + 'Td': self.sud.Td, + 'grain_mass': self.sud.grain_mass, + 'water_mass': self.sud.water_mass, + }, 'steps': schedule[index:], } start_theta = self.tc.get_theta_ist()