Keep spans with unknown inputs out of Model Error
Reproduced: a window unknown for a whole 15-minute span was replayed as closed and scored as a Model Error sample (MAE 0.76 °C on one sample). - A zone's replay is complete only if every step's model inputs were observed: window openings (and a confirmed recovery), the wind, direction and other zones' windows that an open window's terms use, and, for the extended layer, electrical heat and neighbours, whose own replay must be complete too. - Incomplete spans are counted, not scored; the base model's error on them is kept separately (it needs only the relay and outdoor temperature). Spans that could not be replayed (uncertain radiator lag, no reading at the end) are counted as skipped. Model Error sensors show incomplete, skipped and incomplete_base_model_mae; stored data gains an "excluded" section and older data loads unchanged. - Window inputs no longer stand in for unknown values: the facing term is unknown while a window is open and the wind direction is not, and the cross-ventilation input is unknown while another zone's window is. With every window closed, opening terms are 0 and need no weather input. Learning refuses such samples; forecasts still assume calm air. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
13cb972566
commit
2cfba024ff
6 files changed
+274
-34
No files matched your search
@@ -150,17 +150,22 @@ def ventilation_vector(features: Iterable[str], temp: float, outside: float,
|
||||
inputs: Mapping[str, Any]) -> list[float] | None:
|
||||
"""Ventilation feature values for one zone and step; None if an input is unknown.
|
||||
|
||||
`inputs`: opening, facing (sum of opening x cos), other_opening, recovery,
|
||||
t_ref (pre-ventilation temperature), wind, gust.
|
||||
`inputs`: opening, facing (sum of opening x cos; None when the wind direction is
|
||||
unknown), other_opening (None when another zone's windows are unknown),
|
||||
recovery, t_ref (pre-ventilation temperature), wind, gust. With every window
|
||||
closed the opening terms are 0 whatever the weather, so only the opening and,
|
||||
while recovering, the reference temperature must be known.
|
||||
"""
|
||||
difference = outside - temp
|
||||
opening = inputs.get("opening")
|
||||
if opening is None:
|
||||
return None
|
||||
values = []
|
||||
for name in features:
|
||||
opening = inputs.get("opening")
|
||||
if opening is None:
|
||||
return None
|
||||
if name == "vent_natural":
|
||||
values.append(opening * difference)
|
||||
elif name in ("vent_wind", "vent_wind_dir", "vent_gust", "vent_cross") and opening == 0:
|
||||
values.append(0.0)
|
||||
elif name in ("vent_wind", "vent_wind_dir", "vent_gust"):
|
||||
wind = inputs.get("wind")
|
||||
if wind is None:
|
||||
@@ -178,7 +183,10 @@ def ventilation_vector(features: Iterable[str], temp: float, outside: float,
|
||||
return None
|
||||
values.append(opening * max(0.0, gust - wind) * difference)
|
||||
elif name == "vent_cross":
|
||||
values.append(opening * (inputs.get("other_opening") or 0.0) * difference)
|
||||
other = inputs.get("other_opening")
|
||||
if other is None:
|
||||
return None
|
||||
values.append(opening * other * difference)
|
||||
elif name == "recovery":
|
||||
recovery = inputs.get("recovery") or 0.0
|
||||
t_ref = inputs.get("t_ref")
|
||||
@@ -203,10 +211,22 @@ def ventilation_vector_for_prediction(features: Iterable[str], temp: float, outs
|
||||
if x is None and inputs.get("opening") is not None:
|
||||
x = ventilation_vector(features, temp, outside, {
|
||||
**inputs, "wind": inputs.get("wind") or 0.0, "gust": inputs.get("gust") or inputs.get("wind") or 0.0,
|
||||
"facing": inputs.get("facing") or 0.0})
|
||||
"facing": inputs.get("facing") or 0.0, "other_opening": inputs.get("other_opening") or 0.0})
|
||||
return x
|
||||
|
||||
|
||||
def ventilation_known(features: Iterable[str], inputs: Mapping[str, Any] | None) -> bool:
|
||||
"""True when a zone's ventilation term follows from observed inputs alone.
|
||||
|
||||
False while a window state is unknown or a recovery's start is unconfirmed,
|
||||
or while a window is open and a term it activates lacks its weather input.
|
||||
Zones without windows (`inputs` None or empty) need nothing.
|
||||
"""
|
||||
if not inputs:
|
||||
return True
|
||||
return ventilation_vector(list(features), 0.0, 0.0, inputs) is not None
|
||||
|
||||
|
||||
def _iso(timestamp: float) -> str:
|
||||
return datetime.fromtimestamp(timestamp, timezone.utc).isoformat(timespec="seconds")
|
||||
|
||||
@@ -266,7 +286,7 @@ class VentilationTracker:
|
||||
zone = zones.setdefault(window.zone, {
|
||||
"opening": 0.0, "facing_unit": 0.0, "known": True, "open_now": False, "possibly_open": False,
|
||||
"open_windows": [], "last_close": None, "close_confirmed": True, "first_open": None,
|
||||
"open_confirmed": True, "unknown_windows": []})
|
||||
"open_confirmed": True, "unknown_windows": [], "facing_known": True})
|
||||
if fraction is None:
|
||||
zone["known"] = False
|
||||
else:
|
||||
@@ -274,6 +294,8 @@ class VentilationTracker:
|
||||
zone["facing_unit"] += window.opening * fraction * (
|
||||
facing_cos(weather["wind_direction"], window.azimuth)
|
||||
if weather.get("wind_direction") is not None else 0.0)
|
||||
if fraction > 0 and weather.get("wind_direction") is None:
|
||||
zone["facing_known"] = False
|
||||
if is_open is None:
|
||||
if memory["unknown_since"] is None:
|
||||
memory["unknown_since"] = memory["seen"] if memory["seen"] is not None else now
|
||||
@@ -382,7 +404,8 @@ class VentilationTracker:
|
||||
recovery = 0.0
|
||||
zone.update(recovery=recovery, t_ref=self._t_ref.get(key),
|
||||
recovery_confirmed=self._recovery_confirmed.get(key, True),
|
||||
other_opening=sum(z["opening"] for k, z in zones.items() if k != key and z["known"]))
|
||||
other_opening=sum(z["opening"] for k, z in zones.items() if k != key and z["known"]),
|
||||
others_known=all(z["known"] for k, z in zones.items() if k != key))
|
||||
# Learning of the closed-window model is unsafe while anything ventilation-related goes on.
|
||||
zone["affected"] = (not zone["known"]) or zone["opening"] > 0 or ventilating or recovery > 0
|
||||
|
||||
@@ -396,8 +419,10 @@ class VentilationTracker:
|
||||
if zone is None:
|
||||
return None
|
||||
known = zone["known"] and (zone["recovery"] == 0 or zone["recovery_confirmed"])
|
||||
return {"opening": zone["opening"] if known else None, "facing": zone["facing_unit"],
|
||||
"other_opening": zone["other_opening"], "recovery": zone["recovery"],
|
||||
return {"opening": zone["opening"] if known else None,
|
||||
"facing": zone["facing_unit"] if zone["facing_known"] else None,
|
||||
"other_opening": zone["other_opening"] if zone["others_known"] else None,
|
||||
"recovery": zone["recovery"],
|
||||
"t_ref": zone["t_ref"], "wind": weather.get("wind"), "gust": weather.get("wind_gust"),
|
||||
"affected": zone["affected"], "known": known,
|
||||
"unknown_windows": list(zone["unknown_windows"])}
|
||||
|
||||
Reference in new issue
Block a user