Recalibrate only the model parts whose configuration changed

Confirmed: learned models were matched only by zone key, so replacing a
zone's sensors, changing sensor weights, moving or replacing a heat
source or turning a window kept coefficients learned for a different
configuration, silently.

compatibility.py fingerprints exactly what each learned component
depends on: the zone measurement (sensors and relative weights; equal
scaling changes nothing), each zone's heat sources, the neighbour's
measurement, the zone's windows, window orientation (direction term
only), the recovery time (recovery only) and other zones' openings
(cross term only). Comfort weight, target offset and names are not
included. Heat-source baselines have their own fingerprint (sensor and
included sources).

On restore, a changed measurement recalibrates the zone: base and
extended loss/heat/bias keep their values as a warm start, but their
uncertainty, counts and readiness start over, other terms return to the
prior, and the zone's forecast statistics are dropped. Otherwise only
the changed terms are reset; everything else, including other zones,
is kept. Each recalibration is logged (Status model_changes, info log)
and saved at once. Data from before fingerprints (0.4.0) is assumed to
match its current configuration and is fingerprinted at the first
start, so the next edit is detected even before a learned sample.

Tests cover comfort changes, sensor replacement and weights, window
orientation and size, moved and replaced heat sources, re-added zones,
0.4.0 data and a second restart, plus an HA Core test through real
subentry updates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
buzeraandClaude Opus 5.5 committed 2026-10-27 03:35:11 +02:00
1 parent 9bb64e79ca
commit 099c63cd02
10 files changed
+475 -25

No files matched your search

+27 -1
View File
@@ -15,7 +15,7 @@ displayed standard deviation uses a running estimate of the measurement noise.
from __future__ import annotations
from math import isfinite, sqrt
from typing import Any, NamedTuple
from typing import Any, Iterable, NamedTuple
FORGETTING = 0.999 # about 3.5 days of 5-minute samples
MAX_STEP_CHANGE = 0.8 # °C per 5 minutes: larger is a glitch, not physics
@@ -195,6 +195,32 @@ class FeatureLearner:
bounded[i] = min(limit, max(-limit, bounded[i]))
return bounded
def reset(self, features: Iterable[str], keep_values: Iterable[str] = ()) -> None:
"""Forget what was learned for `features` (after a configuration change).
Their variance returns to the prior and their correlations with the other
coefficients are cleared; values go back to the prior unless listed in
`keep_values` (used as a warm start). Other coefficients are untouched.
"""
keep = set(keep_values)
for name in features:
if name not in self.features:
continue
i = self.features.index(name)
spec = self._specs[i]
for j in range(len(self.features)):
self.p[i][j] = self.p[j][i] = 0.0
self.p[i][i] = spec.variance
if name not in keep:
self.theta[i] = spec.prior
self.active[i] = 0
self.theta = self._bounded(self.theta)
def reset_counts(self) -> None:
"""Readiness and error statistics start over (the measured quantity changed)."""
self.count = self.rejected_count = self.on_count = self.off_count = 0
self.error_sum = self._residual_sq = self._residual_weight = 0.0
@property
def noise_var(self) -> float:
"""Estimated variance of the unexplained 5-minute change (°C²)."""