Repository navigation
Idle-offset calibration is one install's numbers — make it per-install (auto-calibrate with a sensor; carry the uncertainty without one) #24
Description
Context
#5 established that the handle-proxy ambient (ambient ≈ idle handle − offset) needs a calibrated, ambient-dependent offset rather than a constant, and was closed when idle_offset_c() landed: a linear model (1.4 °C at 30 °C, slope −0.124 °C/°C, clamped to 23–38.5 °C) fitted from 42 settled-idle segments over 8 days on one install, with contrib/calibrate_idle_offset.py to re-derive the constants by hand.
That resolved the value half of #5. Its "possible fix 1" — refit against accumulated measured-ambient data automatically — is the half that didn't ship: the constants are still hardcoded in thermal.py, so every other install starts from (and, without a sensor, stays on) numbers that describe one garage in one August. This is the largest remaining place where a single-install prior governs behaviour; #21–#23 pulled the fit gate, the drift threshold, and the τ/rise priors back to per-install, and this is the next one.
What the offset governs
Every proxy-tier fit (ambient_source pre_idle / cooldown_tail), the idle ambient tile, and the forecast's ambient whenever no sensor reports. The error is a constant bias per install, not noise — so it largely cancels in the drift watch (a difference of same-tier fits) but lands 1:1 on the derate forecast's plateau, against a SUGGEST_MARGIN_C of 2.0 °C. That is the real exposure, and it is a sensor-less user's exposure.
Proposal (one PR, one concern: the idle-offset prior becomes per-install)
- Sensor present (LAN or car) → calibrate per install, automatically. Move the estimator core from
contrib/calibrate_idle_offset.pyintothermal.py(settled-idle gating, per-segment means, day-jackknifed slope). Run it at session end alongside the drift recheck; persist ref / slope / covered ambient range tosettings;idle_offset_c()prefers the install's own numbers with the built-ins as fallback./api/thermal's model object reportsidle_offset_source: "built-in" | "calibrated", segment count, and the range covered. The shipped constants become the seed everyone starts from, not the calibration. - No sensor → carry the uncertainty rather than pretend. No ground truth means no fit. Attach a ±1.5 °C proxy uncertainty to the forecast's
steady_state_se_cwhen ambient is proxy-sourced — the amp controller's confidence guard already consumes that field, so it turns conservative on sensor-less installs without touching the margin constant. Plus a one-line dashboard note: ambient is inferred from the idle handle using a one-install calibration; a LAN sensor (POST /api/ambient) replaces it. - Recalibration hygiene. The comment block above
IDLE_OFFSET_*already warns that a recalibration shifts every proxy-tier fit and reads as a drift step. When the auto-calibration first lands or moves materially, record anidle_offset_calibratedevent and mark it on the rise-vs-date chart so a step there carries its explanation.
Verification to bring
- The in-process calibrator must reproduce the contrib script's result on the same data (2026-08 calibration: mean 1.45 °C, 95% CI [1.13, 1.78], slope −0.124) — the proof it is the same estimator.
- Synthetic install with a true offset of 3 °C seeded against the 1.4 built-in: the calibrated path recovers ~3, and proxy-tier fits'
rise_ref_cland on the seeded truth. - Sensor-less synthetic install: forecast
steady_state_se_cwidens by the proxy term and the confidence guard's behaviour follows.
Out of scope, stated
A per-install offset for a sensor-less user is impossible without ground truth; the honest answer there is the uncertainty term and the note, not a cleverer guess. Sensor placement contamination is #3's concern and stays there.
Related: #5 (the value; closed), #3 (sensor placement), #1 (roadmap).
Activity
zebraengine commentedon Aug 27, 2026
Correction to part 2 before building it: the amp controller's confidence guard reads
steady_state_se_conly onbasis == "trajectory"forecasts, and the trajectory projection is ambient-free — it extrapolates the handle curve itself. Proxy-ambient error therefore never reaches the controller; it reaches only the model-basis forecast (the first couple of minutes of a session, before a trajectory exists) and the idle tile's "a full-rate charge started now" estimate. That is a smaller exposure than this issue claimed. The PR will reportambient_se_con model-basis forecasts for the API and UI, and will not touch the controller; the "confidence guard follows" verification line is withdrawn.Two further deviations, for the record: calibration runs in the daily maintenance pass rather than at session end (it scans a month of raw rows, and unplugs are frequent), and the "reproduce the 2026-08 result" bar is replaced by "the contrib script and the in-process estimator are the same code run on the same window, today" — the 30-day window has moved past the August data.
zebraengine commentedon Aug 27, 2026
Shipped in #27 and live on the reference install: first maintenance pass after restart adopted the install's own model (1.15 °C at 30 °C, slope −0.159, 128 segments over 24 days, residual sd 0.89), recorded the
idle_offset_calibratedevent, and/api/thermalnow reportsmodel.idle_offset.source: "calibrated". Sensor-less installs keep the labelled seed with a stated ±1.5 °C. Part 2's controller coupling was withdrawn per the comment above.