docs: add components/sud.py step lifecycle spec
Captures the ramp/hold/user-confirm phase order and a couple of non-obvious clarifications from a design discussion (ramp is gated purely by the schedule's 'ramp' key, not actual-vs-target temp; WAIT_USER is a distinct phase, not an extended hold), plus one open gap: pure-hold steps never re-set the controller's target temperature. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhiQe64F74uHV8jzuoSa5K
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
# components/sud.py step lifecycle spec
|
||||
|
||||
Confirmed spec for a `Sud` schedule step's internal phase order
|
||||
(`components/sud.py`), from a design discussion clarifying how `ramp`/
|
||||
`hold`/user-confirm interact.
|
||||
|
||||
## Phase order
|
||||
|
||||
`ramp` -> `hold` -> user-confirm. Each step independently opts into
|
||||
`ramp` and/or `hold` by including that key (`_build_step()`,
|
||||
`sud.py:35-45`); the other phase is skipped entirely if its key is
|
||||
absent. The `'ramp'` key's presence is the *only* trigger for ramping
|
||||
(`_advance()`, `sud.py:192-210`) - there is no diff-based decision
|
||||
(`T_actual` vs. `T_target`) anywhere that decides whether to ramp, and
|
||||
no internal setpoint-trajectory limiter in the temperature controller
|
||||
(`set_theta_soll()` is an instant, unconditional assignment,
|
||||
`components/pid/temp_controller_base.py:81-82`). The `ramp` key both
|
||||
gates ramping *and* carries its settings (`rate`); it does not merely
|
||||
override settings on top of an always-on ramp.
|
||||
|
||||
## Duration `0`
|
||||
|
||||
Supported for hold: `hold_remaining = duration * 60.0` can be `0`;
|
||||
`tick()` (`sud.py:186-190`) finishes the step on the next tick once
|
||||
`hold_remaining <= 0`.
|
||||
|
||||
## End-of-step branching
|
||||
|
||||
`_finish_step()` (`sud.py:212-216`): no `user_wait_for_continue` ->
|
||||
advances immediately to the next step; `user_wait_for_continue: true`
|
||||
-> enters `WAIT_USER`, resumed only by `confirm()`.
|
||||
|
||||
`WAIT_USER` is not "hold, prolonged": `HOLDING` and `WAIT_USER` are
|
||||
distinct `SudState` values. The hold countdown runs to completion
|
||||
*before* `WAIT_USER` begins - `tick()` only decrements
|
||||
`hold_remaining` while `state == HOLDING`, so the timer is frozen
|
||||
(not extended) during the wait. It delays the next step, but it's a
|
||||
separate third phase, not a longer hold.
|
||||
|
||||
The gating field is `user_wait_for_continue`, not "presence of a user
|
||||
message": `user_message` (display text) and `user_wait_for_continue`
|
||||
(the actual block) are independent fields - a step can show a message
|
||||
without blocking, or block silently.
|
||||
|
||||
## Open items
|
||||
|
||||
- [ ] **Pure-hold steps never (re-)set the controller's target
|
||||
temperature.** For a step with only `hold` (no `ramp`),
|
||||
`tasks/sud.py`'s `on_step_changed` never calls `tc.set_theta_soll()`
|
||||
- the guard at line 73 requires `ramp is not None`. The
|
||||
controller's target temperature for a pure-hold step is whatever
|
||||
was left from the previous step's ramp; there's no explicit
|
||||
fallback to the hold step's own `temperature` field. `README.md`
|
||||
(Mash schedules section) documents this as designed behavior
|
||||
("`SudTask` drives ... `theta_soll`/`heatrate_soll` from ramp
|
||||
steps ... counts down hold steps' duration"), so it's intentional,
|
||||
not a bug - but it means a schedule can't express a temperature
|
||||
jump straight into a hold without a `ramp` block.
|
||||
Reference in New Issue
Block a user