Move Sud step's target temperature from ramp.temp to step.temperature

A step's target temperature applies to the whole step (it's what a
ramp ramps to and what a hold then holds at), not just its ramp phase
- promote it from a nested "ramp": {"temp": ...} to a step-level
"temperature" field, defaulted like descr/grain_mass/etc. Updates all
consumers (Sud._build_step, SudTask, the GUI's forecast estimator,
demo_sud.py) and migrates sude/sud_0010.json and the README.
This commit is contained in:
2026-06-21 09:47:45 +02:00
parent e62863ca93
commit 194156f244
6 changed files with 28 additions and 26 deletions
+8 -6
View File
@@ -130,17 +130,19 @@ environment (`$WORKON_HOME`/`$BREWPI_HOME`) on a Raspberry Pi-style deployment.
Files under `sude/` describe a brew's mash schedule ("Sud"): `pot_mass`, Files under `sude/` describe a brew's mash schedule ("Sud"): `pot_mass`,
`pot_material` (fixed for the whole brew), and a `steps` list, each a ramp, a `pot_material` (fixed for the whole brew), and a `steps` list, each a ramp, a
hold, or both: hold, or both, plus a step-level `temperature` (the target a ramp step ramps
to, and what a hold step holds at):
- a ramp — `"ramp": {"rate": ..., "temp": ...}` — ramp to `temp` at `rate` - a ramp — `"ramp": {"rate": ...}` — ramp to `temperature` at `rate`
(°C/min), and/or (°C/min), and/or
- a hold — `"hold": {"duration": ...}` — hold the current target for - a hold — `"hold": {"duration": ...}` — hold the current target for
`duration` minutes (omit/`0` for an immediate step). `duration` minutes (omit/`0` for an immediate step).
A step with both ramps to `temp` and then holds there for `duration` - A step with both ramps to `temperature` and then holds there for `duration`
useful for a mash rest ("ramp to 63°C, then hold 40 min") without needing two - useful for a mash rest ("ramp to 63°C, then hold 40 min") without needing
separate schedule entries. `user_wait_for_continue`/`user_message` (below) two separate schedule entries. `user_wait_for_continue`/`user_message`
apply once the whole step is done, i.e. after the hold phase if there is one. (below) apply once the whole step is done, i.e. after the hold phase if
there is one.
Both `ramp` and `hold` carry a `stirrer` block (`speed`, `interval_time`, Both `ramp` and `hold` carry a `stirrer` block (`speed`, `interval_time`,
`on_ratio`): `interval_time: 0` runs the stirrer continuously at `speed`; `on_ratio`): `interval_time: 0` runs the stirrer continuously at `speed`;
+1 -1
View File
@@ -139,7 +139,7 @@ class SudForecastPlot(FigureCanvasQTAgg):
hold = step.get('hold') hold = step.get('hold')
if ramp is not None: if ramp is not None:
rate = ramp.get('rate', 0) rate = ramp.get('rate', 0)
target = ramp['temp'] target = step['temperature']
if rate > 0: if rate > 0:
t.append(t[-1] + abs(target - theta[-1]) / rate * 60.0) t.append(t[-1] + abs(target - theta[-1]) / rate * 60.0)
theta.append(target) theta.append(target)
+1 -1
View File
@@ -38,7 +38,7 @@ def _build_step(number, defaults, raw_step):
ramp or a hold depending on which of those keys it specifies; the other ramp or a hold depending on which of those keys it specifies; the other
is left out rather than synthesized from defaults.""" is left out rather than synthesized from defaults."""
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', 'grain_mass', 'water_mass', 'temperature'):
step[key] = raw_step.get(key, defaults.get(key)) step[key] = raw_step.get(key, defaults.get(key))
for key in ('ramp', 'hold'): for key in ('ramp', 'hold'):
if key in raw_step: if key in raw_step:
+3 -3
View File
@@ -81,15 +81,15 @@ if __name__ == '__main__':
# A ramp step whose target is below the current temperature can # A ramp step whose target is below the current temperature can
# only be reached by passive cooling - force the heater off, # only be reached by passive cooling - force the heater off,
# mirroring tasks/sud.py's SudTask. # mirroring tasks/sud.py's SudTask.
cooling = ramping and 'ramp' in step and step['ramp']['temp'] < ctrl.get_theta_ist() - TEMP_REACHED_TOLERANCE cooling = bool(ramping and 'ramp' in step and step['temperature'] < ctrl.get_theta_ist() - TEMP_REACHED_TOLERANCE)
ctrl.set_cooling(cooling) ctrl.set_cooling(cooling)
if cooling: if cooling:
print(" -> cooling down to {}".format(step['ramp']['temp'])) print(" -> cooling down to {}".format(step['temperature']))
print(f"Step {step['number']}: {step['descr']}") print(f"Step {step['number']}: {step['descr']}")
apply_plant_params(step) apply_plant_params(step)
if ramping and 'ramp' in step: if ramping and 'ramp' in step:
ctrl.set_theta_soll(step['ramp']['temp']) ctrl.set_theta_soll(step['temperature'])
ctrl.set_heatrate_soll(step['ramp']['rate']) ctrl.set_heatrate_soll(step['ramp']['rate'])
apply_stirrer(phase) apply_stirrer(phase)
+12 -12
View File
@@ -10,9 +10,9 @@
"user_wait_for_continue": false, "user_wait_for_continue": false,
"grain_mass": 5.21, "grain_mass": 5.21,
"water_mass": 22, "water_mass": 22,
"temperature": 0,
"ramp": { "ramp": {
"rate": 1.0, "rate": 1.0,
"temp": 0,
"stirrer": { "stirrer": {
"speed": 50, "speed": 50,
"interval_time": 0, "interval_time": 0,
@@ -32,18 +32,18 @@
"steps": [ "steps": [
{ {
"grain_mass": 0, "grain_mass": 0,
"descr": "Heizen f\u00fcr Einmaischen", "descr": "Heizen für Einmaischen",
"temperature": 57,
"ramp": { "ramp": {
"rate": 1.8, "rate": 1.8,
"temp": 57,
"stirrer": { "stirrer": {
"speed": 75 "speed": 75
} }
} }
}, },
{ {
"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": {
@@ -59,9 +59,9 @@
}, },
{ {
"descr": "Glucose-Rast", "descr": "Glucose-Rast",
"temperature": 63,
"ramp": { "ramp": {
"rate": 1.0, "rate": 1.0
"temp": 63
}, },
"hold": { "hold": {
"duration": 40, "duration": 40,
@@ -74,9 +74,9 @@
}, },
{ {
"descr": "Verzuckerungs-Rast", "descr": "Verzuckerungs-Rast",
"temperature": 72,
"ramp": { "ramp": {
"rate": 1.0, "rate": 1.0
"temp": 72
}, },
"hold": { "hold": {
"duration": 30, "duration": 30,
@@ -91,9 +91,9 @@
"descr": "Abmaischen", "descr": "Abmaischen",
"user_message": "Quittieren zum Abmaischen", "user_message": "Quittieren zum Abmaischen",
"user_wait_for_continue": true, "user_wait_for_continue": true,
"temperature": 76,
"ramp": { "ramp": {
"rate": 1.0, "rate": 1.0
"temp": 76
}, },
"hold": { "hold": {
"duration": 30, "duration": 30,
@@ -103,4 +103,4 @@
} }
} }
] ]
} }
+3 -3
View File
@@ -69,13 +69,13 @@ class SudTask(ATask):
# bool(...): get_theta_ist() can be a numpy.float64 (Pot's transport # bool(...): get_theta_ist() can be a numpy.float64 (Pot's transport
# delay line is a numpy array internally) - the '<' comparison would # delay line is a numpy array internally) - the '<' comparison would
# then yield numpy.bool_, which json.dumps() can't serialize. # then yield numpy.bool_, which json.dumps() can't serialize.
cooling = bool(ramping and ramp is not None and ramp['temp'] < self.tc.get_theta_ist() - TEMP_REACHED_TOLERANCE) cooling = bool(ramping and ramp is not None and step['temperature'] < self.tc.get_theta_ist() - TEMP_REACHED_TOLERANCE)
self.tc.set_cooling(cooling) self.tc.set_cooling(cooling)
if step is not None: if step is not None:
self.apply_plant_params(step) self.apply_plant_params(step)
if ramping and ramp is not None: if ramping and ramp is not None:
self.tc.set_theta_soll(ramp['temp']) self.tc.set_theta_soll(step['temperature'])
self.tc.set_heatrate_soll(ramp['rate']) self.tc.set_heatrate_soll(ramp['rate'])
self.apply_stirrer(phase) self.apply_stirrer(phase)
@@ -83,7 +83,7 @@ class SudTask(ATask):
'Index': self.sud.index, 'Index': self.sud.index,
'Type': 'ramp' if ramping and ramp is not None else 'hold' if hold is not None else None, 'Type': 'ramp' if ramping and ramp is not None else 'hold' if hold is not None else None,
'Descr': step.get('descr') if step else None, 'Descr': step.get('descr') if step else None,
'Temp': ramp.get('temp') if ramp else None, 'Temp': step.get('temperature') if ramp is not None else None,
'Rate': ramp.get('rate') if ramp else None, 'Rate': ramp.get('rate') if ramp else None,
'Duration': hold.get('duration') if (hold is not None and not ramping) else None, 'Duration': hold.get('duration') if (hold is not None and not ramping) else None,
'WaitForUser': step.get('user_wait_for_continue', False) if step else None, 'WaitForUser': step.get('user_wait_for_continue', False) if step else None,