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:
@@ -278,47 +278,50 @@ disabled, its temperature/heat-rate setpoint controls are inactive.
|
||||
|
||||
### 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
|
||||
every ramp instantly achieves and holds its declared `rate`
|
||||
(`abs(delta)/rate`) and every hold lasts exactly its declared `duration`.
|
||||
Deliberately approximate - it's just a placeholder until the next one
|
||||
arrives a moment later.
|
||||
- A *simulated* estimate, computed server-side (`components/sud_forecast.py`'s
|
||||
The forecast is computed server-side (`components/sud_forecast.py`'s
|
||||
`SudForecastEstimator`) by actually running the schedule through a
|
||||
throwaway `Sud`/`Pot`/temperature-controller trio built from the same
|
||||
params (`pid_type`, `TempCtrl` gains, plant params, ambient, heater max
|
||||
power) the real server uses - a multi-hour brew simulates in well under a
|
||||
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.
|
||||
is deliberately *not* recomputed as a run progresses: a forecast that keeps
|
||||
re-anchoring itself to match whatever's actually happening would no longer
|
||||
be a meaningful baseline to compare reality against. It's computed once,
|
||||
up front, and left alone.
|
||||
|
||||
The naive estimate is structurally optimistic - it assumes the plant
|
||||
instantly tracks the declared rate and that "reached" is instant once the
|
||||
math says so, when the real PID cascade (`theta_err -> pid_hold ->
|
||||
heatrate_soll -> pid_heat -> heater power -> actual heat rate`) needs real
|
||||
time to spin up and settle into the controller's own `HOLD` state
|
||||
(`TempControllerBase.is_holding()`, gated by the `HoldHeat`/`HoldCool`
|
||||
thresholds). The simulated estimate accounts for that (it's driven by the
|
||||
real controller dynamics), so it's normally
|
||||
within a few percent of how the dynamic one settles once a run actually
|
||||
finishes - if the two diverge by far more than that, suspect a real control
|
||||
issue rather than a forecasting one (see the `HoldCool` threshold note in
|
||||
The one exception is a step with `user_wait_for_continue`: a human's
|
||||
response time genuinely can't be forecast, so `SudForecastEstimator.
|
||||
estimate()` simply stops simulating there instead of assuming a delay (the
|
||||
old behavior - zero delay - quietly understated the schedule). The
|
||||
forecast is therefore piecewise: `tasks/sud.py`'s `SudTask` sends the first
|
||||
segment (up to the first such step, or the end of the schedule if there is
|
||||
none) on Load, marking it `Finished: false` if it stopped early. The *next*
|
||||
segment is only computed once the real confirmation actually happens,
|
||||
anchored at the real elapsed time and real current temperature at that
|
||||
moment - so the unforecastable wait shows up honestly as a gap in the
|
||||
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
|
||||
caused exactly this kind of large, otherwise-unexplained gap, by making the
|
||||
controller chatter in and out of `COOL` and never actually converge). Rule
|
||||
out a *display* artifact first, though: the dynamic trace used to position
|
||||
itself using wall-clock time times the configured `sim_warp_factor`, but
|
||||
that factor is only nominal - real asyncio/Python scheduling overhead means
|
||||
the actually-achieved speedup runs measurably below it (confirmed: ~80x
|
||||
instead of a configured 100x), which alone produced a divergence large
|
||||
enough to look like a control problem. `Sud.elapsed` (tick-counted, immune
|
||||
to wall-clock pacing) replaced that reconstruction for exactly this reason.
|
||||
controller chatter in and out of `COOL` and never actually converge). The
|
||||
GUI plots the actual trace using `Sud.elapsed` (tick-counted simulated
|
||||
seconds, see "State replay on connect" above) rather than wall-clock time
|
||||
times the configured `sim_warp_factor` - the latter is only nominal, and
|
||||
real asyncio/Python scheduling overhead means the actually-achieved
|
||||
speedup runs measurably below it (confirmed: ~80x instead of a configured
|
||||
100x), which on its own used to produce a divergence large enough to look
|
||||
like a control problem.
|
||||
|
||||
## Logging & analysis
|
||||
|
||||
|
||||
Reference in New Issue
Block a user