docs: refresh README for piecewise Sud forecasting

Rewrites the "Forecast vs. actual duration" section to match the
previous commit: no more naive abs(delta)/rate placeholder, no more
per-tick re-anchoring of the projected remainder - the forecast is
computed once, piecewise per segment, deliberately left alone as a
fixed comparison baseline. Documents the Finished flag, the
real-confirm-anchored segment stitching for steps requiring user
confirmation, and extend_forecast_while_waiting()'s flat-repeat
display in the meantime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhiQe64F74uHV8jzuoSa5K
This commit is contained in:
2026-06-22 16:37:38 +02:00
co-authored by Claude Sonnet 4.6
parent 57b86bc075
commit 6e5fd23ccb
+40 -37
View File
@@ -278,47 +278,50 @@ disabled, its temperature/heat-rate setpoint controls are inactive.
### Forecast vs. actual duration ### Forecast vs. actual duration
The GUI's Automatic tab shows up to three time estimates for a schedule: The GUI's Automatic tab plots two things against the same time axis: a
*forecast* (dashed) and, once a run starts, the *actual* measured trace
(solid) overlaid on top of it - a comparison between predicted and real
control behavior, not just a progress display.
- Immediately on load, a quick *naive* preview: walk the schedule assuming The forecast is computed server-side (`components/sud_forecast.py`'s
every ramp instantly achieves and holds its declared `rate` `SudForecastEstimator`) by actually running the schedule through a
(`abs(delta)/rate`) and every hold lasts exactly its declared `duration`. throwaway `Sud`/`Pot`/temperature-controller trio built from the same
Deliberately approximate - it's just a placeholder until the next one params (`pid_type`, `TempCtrl` gains, plant params, ambient, heater max
arrives a moment later. power) the real server uses - a multi-hour brew simulates in well under a
- A *simulated* estimate, computed server-side (`components/sud_forecast.py`'s second since it's pure CPU-bound iteration, no real time/IO involved. This
`SudForecastEstimator`) by actually running the schedule through a is deliberately *not* recomputed as a run progresses: a forecast that keeps
throwaway `Sud`/`Pot`/temperature-controller trio built from the same re-anchoring itself to match whatever's actually happening would no longer
params (`pid_type`, `TempCtrl` gains, plant params, ambient, heater max be a meaningful baseline to compare reality against. It's computed once,
power) the real server uses - a multi-hour brew simulates in well under a up front, and left alone.
second since it's pure CPU-bound iteration, no real time/IO involved. This
replaces the naive preview a moment after a schedule is loaded.
- Once running, a *dynamic* one: the already-elapsed part is the actual
measured trace, plotted against `Sud.elapsed` (see "State replay on
connect" above) rather than wall-clock time, and only the remaining steps
are re-projected from the live temperature/step/`hold_remaining` each
tick.
The naive estimate is structurally optimistic - it assumes the plant The one exception is a step with `user_wait_for_continue`: a human's
instantly tracks the declared rate and that "reached" is instant once the response time genuinely can't be forecast, so `SudForecastEstimator.
math says so, when the real PID cascade (`theta_err -> pid_hold -> estimate()` simply stops simulating there instead of assuming a delay (the
heatrate_soll -> pid_heat -> heater power -> actual heat rate`) needs real old behavior - zero delay - quietly understated the schedule). The
time to spin up and settle into the controller's own `HOLD` state forecast is therefore piecewise: `tasks/sud.py`'s `SudTask` sends the first
(`TempControllerBase.is_holding()`, gated by the `HoldHeat`/`HoldCool` segment (up to the first such step, or the end of the schedule if there is
thresholds). The simulated estimate accounts for that (it's driven by the none) on Load, marking it `Finished: false` if it stopped early. The *next*
real controller dynamics), so it's normally segment is only computed once the real confirmation actually happens,
within a few percent of how the dynamic one settles once a run actually anchored at the real elapsed time and real current temperature at that
finishes - if the two diverge by far more than that, suspect a real control moment - so the unforecastable wait shows up honestly as a gap in the
issue rather than a forecasting one (see the `HoldCool` threshold note in timeline rather than as a guess - and appended to what's already shown.
While waiting, the GUI just repeats the forecast's last value out to "now"
(`SudForecastPlot.extend_forecast_while_waiting()`) rather than leaving a
visual gap, then snaps to the new segment the moment it arrives.
Comparing the two lines: real-world divergence from the forecast - more
than a transient blip during a ramp's settling - is worth investigating as
an actual control issue (see the `HoldCool` threshold note in
`components/pid/temp_controller_base.py` - too tight a threshold there once `components/pid/temp_controller_base.py` - too tight a threshold there once
caused exactly this kind of large, otherwise-unexplained gap, by making the caused exactly this kind of large, otherwise-unexplained gap, by making the
controller chatter in and out of `COOL` and never actually converge). Rule controller chatter in and out of `COOL` and never actually converge). The
out a *display* artifact first, though: the dynamic trace used to position GUI plots the actual trace using `Sud.elapsed` (tick-counted simulated
itself using wall-clock time times the configured `sim_warp_factor`, but seconds, see "State replay on connect" above) rather than wall-clock time
that factor is only nominal - real asyncio/Python scheduling overhead means times the configured `sim_warp_factor` - the latter is only nominal, and
the actually-achieved speedup runs measurably below it (confirmed: ~80x real asyncio/Python scheduling overhead means the actually-achieved
instead of a configured 100x), which alone produced a divergence large speedup runs measurably below it (confirmed: ~80x instead of a configured
enough to look like a control problem. `Sud.elapsed` (tick-counted, immune 100x), which on its own used to produce a divergence large enough to look
to wall-clock pacing) replaced that reconstruction for exactly this reason. like a control problem.
## Logging & analysis ## Logging & analysis