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`,
`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
- a hold — `"hold": {"duration": ...}` — hold the current target for
`duration` minutes (omit/`0` for an immediate step).
A step with both ramps to `temp` and then holds there for `duration` -
useful for a mash rest ("ramp to 63°C, then hold 40 min") without needing two
separate schedule entries. `user_wait_for_continue`/`user_message` (below)
apply once the whole step is done, i.e. after the hold phase if there is one.
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 separate schedule entries. `user_wait_for_continue`/`user_message`
(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`,
`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')
if ramp is not None:
rate = ramp.get('rate', 0)
target = ramp['temp']
target = step['temperature']
if rate > 0:
t.append(t[-1] + abs(target - theta[-1]) / rate * 60.0)
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
is left out rather than synthesized from defaults."""
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))
for key in ('ramp', 'hold'):
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
# only be reached by passive cooling - force the heater off,
# 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)
if cooling:
print(" -> cooling down to {}".format(step['ramp']['temp']))
print(" -> cooling down to {}".format(step['temperature']))
print(f"Step {step['number']}: {step['descr']}")
apply_plant_params(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'])
apply_stirrer(phase)
+12 -12
View File
@@ -10,9 +10,9 @@
"user_wait_for_continue": false,
"grain_mass": 5.21,
"water_mass": 22,
"temperature": 0,
"ramp": {
"rate": 1.0,
"temp": 0,
"stirrer": {
"speed": 50,
"interval_time": 0,
@@ -32,18 +32,18 @@
"steps": [
{
"grain_mass": 0,
"descr": "Heizen f\u00fcr Einmaischen",
"descr": "Heizen für Einmaischen",
"temperature": 57,
"ramp": {
"rate": 1.8,
"temp": 57,
"stirrer": {
"speed": 75
}
}
},
{
"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": {
@@ -59,9 +59,9 @@
},
{
"descr": "Glucose-Rast",
"temperature": 63,
"ramp": {
"rate": 1.0,
"temp": 63
"rate": 1.0
},
"hold": {
"duration": 40,
@@ -74,9 +74,9 @@
},
{
"descr": "Verzuckerungs-Rast",
"temperature": 72,
"ramp": {
"rate": 1.0,
"temp": 72
"rate": 1.0
},
"hold": {
"duration": 30,
@@ -91,9 +91,9 @@
"descr": "Abmaischen",
"user_message": "Quittieren zum Abmaischen",
"user_wait_for_continue": true,
"temperature": 76,
"ramp": {
"rate": 1.0,
"temp": 76
"rate": 1.0
},
"hold": {
"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
# delay line is a numpy array internally) - the '<' comparison would
# 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)
if step is not None:
self.apply_plant_params(step)
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.apply_stirrer(phase)
@@ -83,7 +83,7 @@ class SudTask(ATask):
'Index': self.sud.index,
'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,
'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,
'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,