From c9d38e50dbad6fd29ba61bbad15a1b900fd4d78e Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Sun, 21 Jun 2026 16:47:35 +0200 Subject: [PATCH] docs: update Forecast vs. actual duration for the new server-side simulated estimate --- README.md | 51 ++++++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 9c268f6..6eca2b7 100644 --- a/README.md +++ b/README.md @@ -191,32 +191,37 @@ ever. ### Forecast vs. actual duration -The GUI's Automatic tab shows two different time estimates, and they can -diverge substantially: +The GUI's Automatic tab shows up to three time estimates for a schedule: -- Before a run starts, it shows a *static* estimate: walk the schedule - assuming every ramp instantly achieves and holds its declared `rate` +- 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`. -- Once running, it switches to a *dynamic* one: the already-elapsed part is - the actual measured trace, and only the remaining steps are re-projected - from the live temperature/step/`hold_remaining` each tick. + 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 + `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, and only the remaining steps are re-projected from the + live temperature/step/`hold_remaining` each tick. -In practice the dynamic total tends to run well above the static one - a -schedule estimated at ~160 min commonly finishes closer to ~250-270 min. -The static model assumes the plant instantly tracks the declared rate and -that "reached" is instant once the math says so; the real PID cascade -(`theta_err -> pid_hold -> heatrate_soll -> pid_heat -> heater power -> -actual heat rate`) needs real time to spin up (constrained by the plant's -thermal mass and the Smith predictor's transport-delay compensation) and -then more time to settle within the tight 0.2°C "reached" tolerance -(`tasks/sud.py`'s `TEMP_REACHED_TOLERANCE`) once nominally at target. That -settling lag is roughly constant per step rather than proportional to the -step's nominal duration, so it eats a larger fraction of short ramps and -keeps accumulating across every ramp/hold transition in the schedule - -many small (~20-90%) per-step overruns compounding into one large gap by -the end, rather than one big error anywhere in particular. The dynamic -re-anchoring is what corrects for this as a brew actually progresses; the -upfront static number has no settling-time data to work with yet. +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 within the tight 0.2°C "reached" tolerance +(`tasks/sud.py`'s `TEMP_REACHED_TOLERANCE`). 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 +`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). ## Logging & analysis