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
|
### 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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user