From df91ec61f3e4bfb9873107bed66f747adbd0c5b3 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Mon, 22 Jun 2026 22:44:19 +0200 Subject: [PATCH] docs: refresh Forecast vs. actual duration for the real-temperature anchoring fixes Documents the recent SudTask.send_forecast()/recv() changes (anchored at get_theta_ist_set(), re-anchored on every fresh Start) and the two spurious-divergence fixes: client/brewpi_gui.py's forecast_history t=0 seeding, and the hardcoded last_theta_ist=20 bug in both TempController variants. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01DkkuG48uHFCGKe6dPSERFk --- README.md | 46 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 28a6702..4c3402d 100644 --- a/README.md +++ b/README.md @@ -308,11 +308,20 @@ server uses - plant params (`M`/`C`/`L`/`Td`) aren't a constructor argument at all, since they come from the doc being forecast itself (`Sud.derive_plant_params()`, same as the real run), not from any generic default. A multi-hour brew simulates in well under a second since it's -pure CPU-bound iteration, no real time/IO involved. This -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. +pure CPU-bound iteration, no real time/IO involved. + +It's anchored to the real current temperature +(`TempControllerBase.get_theta_ist_set()` - the raw sensor reading, valid +even before this controller's first `process()` tick, unlike +`get_theta_ist()`), not a cold start at ambient - `SudTask.send_forecast()` +computes it once on Load (a preview of what's coming), and again on every +fresh `Start` (not a Pause→resume, which keeps whatever forecast a run +already established rather than discarding it mid-run), since the real +temperature can have drifted in the meantime +(time passing, manual heating via the Manual tab in between). Once a run +is actually progressing, though, it's deliberately *not* continuously +re-anchored to match whatever's happening: a forecast that keeps chasing +reality would no longer be a meaningful baseline to compare it against. `SudTask._send_forecast()` thins the curve to at most `MAX_FORECAST_POINTS` (1000) before broadcasting it - a fine enough `dt` over a multi-hour brew @@ -366,6 +375,33 @@ 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. +Two other sources of spurious (non-control-related) divergence, both +fixed: + +- `client/brewpi_gui.py`'s actual-trace history used to only get its first + sample at `on_plot_timer()`'s next 1-second (real time) tick - under a + real `sim_warp_factor`, simulated time has already moved measurably past + `t=0` by then, leaving a small visible gap between the forecast's exact + `t=0` anchor and where the actual line first appears. `on_sud_changed()` + now seeds `forecast_history` with the real `(elapsed, plot_temp_ist)` + point the moment the first `Elapsed` push of a *fresh* run arrives + (gated on elapsed still being small, `FRESH_RUN_ELAPSED_THRESHOLD_S`, so + reconnecting mid-brew doesn't get a bogus point plotted at `x=0`). +- Both `TempController` variants used to hardcode `last_theta_ist = 20` at + construction, used to compute the very first `process()` tick's + `heatrate_ist` - a wildly bogus rate spike whenever the real starting + temperature wasn't actually 20 (invisible until a forecast actually got + anchored to something other than 20, per the fix above), which then took + several exponentially-smoothed ticks to decay out of the rate-PID loop, + visibly distorting the trajectory during exactly that window. It now + starts `None` and treats the first tick's rate as `0` (no real history + yet) instead of computing it against a hardcoded, usually-wrong + baseline - `SudForecastEstimator` is the case that actually surfaced + this: it builds a fresh `TempController` on every call, so its first + tick always hit this fresh, unlike the real controller, which only ever + goes through it once near server startup and has long since + self-corrected by the time any forecast comparison matters. + ## Logging & analysis `tasks/sud_log.py`'s `SudLogTask` records every Sud run's measured data -