Skip to content

Idle-offset calibration is one install's numbers — make it per-install (auto-calibrate with a sensor; carry the uncertainty without one) #24

Description

@zebraengine

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)

  1. Sensor present (LAN or car) → calibrate per install, automatically. Move the estimator core from contrib/calibrate_idle_offset.py into thermal.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 to settings; idle_offset_c() prefers the install's own numbers with the built-ins as fallback. /api/thermal's model object reports idle_offset_source: "built-in" | "calibrated", segment count, and the range covered. The shipped constants become the seed everyone starts from, not the calibration.
  2. 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_c when 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.
  3. 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 an idle_offset_calibrated event 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_c land on the seeded truth.
  • Sensor-less synthetic install: forecast steady_state_se_c widens 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions