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:
buzeraandClaude Opus 5.5 committed 2026-10-27 04:46:21 +02:00
1 parent 13cb972566
commit 2cfba024ff
6 files changed
+274 -34

No files matched your search

+23
View File
@@ -392,3 +392,26 @@ class StartupAndRestartTests(unittest.TestCase):
self.assertFalse(tracker.changed) # nothing new
self.poll(tracker, 'unavailable')
self.assertTrue(tracker.changed)
class StrictInputTests(unittest.TestCase):
"""Weather inputs of an open window are required, never assumed, outside forecasts."""
def test_unknown_direction_or_other_zone_while_open(self):
windows = wm.build_windows([(window('a'), None), (window('c', zone='living'), None)], ['bed', 'living'])
tracker = wm.VentilationTracker(windows)
calm = {**WEATHER, 'wind_direction': None}
for t, a, c in ((300.0, 'off', 'off'), (600.0, 'on', 'unavailable'), (900.0, 'on', 'unavailable')):
tracker.update(t, {'a': (a, 450.0), 'c': (c, 450.0)}, {'bed': 20.0, 'living': 21.0}, calm)
bed = tracker.zone_inputs('bed', calm)
self.assertEqual((bed['opening'], bed['facing'], bed['other_opening']), (1.0, None, None))
features = ['vent_natural', 'vent_wind', 'vent_wind_dir', 'vent_cross']
self.assertFalse(wm.ventilation_known(features, bed))
self.assertTrue(wm.ventilation_known(['vent_natural', 'vent_wind'], bed))
self.assertIsNone(wm.ventilation_vector(features, 20.0, 2.0, bed)) # learning refuses
self.assertIsNotNone(wm.ventilation_vector_for_prediction(features, 20.0, 2.0, bed))
# Closed, every opening term is 0 whatever the weather: nothing is missing.
closed = {**bed, 'opening': 0.0, 'facing': 0.0, 'wind': None, 'other_opening': None}
self.assertEqual(wm.ventilation_vector(features, 20.0, 2.0, closed), [0.0, 0.0, 0.0, 0.0])
self.assertTrue(wm.ventilation_known(features, closed))
self.assertTrue(wm.ventilation_known(features, None)) # no windows