Count rejected samples and poll outcomes; pin rollback contract
Status showed accepted sample counts but not why learning was slow. Each room's model now counts samples it rejected (outliers, implausible inputs), persisted as an optional rejected_count key and shown as models.<room>.rejected_samples. Status also counts every poll outcome since start (sample_results_since_start). Add tests that a model in the exact shape 0.2.1 wrote restores with its coefficients, and that today's saved model still has the keys and types 0.2.1's restore code reads, so rolling back keeps the learned model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ace6d0061b
commit
43915ba810
4 files changed
+69
-14
No files matched your search
@@ -615,3 +615,42 @@ class BoundaryTests(unittest.IsolatedAsyncioTestCase):
|
||||
data = await self.sample()
|
||||
self.assertEqual(data['excluded_rooms'], ['sensor.bedroom'])
|
||||
self.assertEqual(data['status'], 'learning')
|
||||
|
||||
async def test_poll_outcomes_and_rejections_are_counted(self):
|
||||
await self.sample()
|
||||
self.states['sensor.room'].state = '25' # implausible 5 °C jump in five minutes
|
||||
data = await self.sample()
|
||||
self.assertEqual(data['sample_result'], 'partly_rejected')
|
||||
self.assertEqual(data['models']['sensor.room']['rejected_samples'], 1)
|
||||
self.fresh('sensor.room', 'sensor.outside')
|
||||
data = await self.sample()
|
||||
self.assertEqual(data['sample_results_since_start'],
|
||||
{'first_sample': 1, 'partly_rejected': 1, 'learned': 1})
|
||||
|
||||
async def test_restores_model_saved_by_v021(self):
|
||||
# Exact shape written by 0.2.1: no schema version, per-room counts or new keys.
|
||||
legacy = {'learners': {'sensor.room': {
|
||||
'theta': [0.004, 0.11, 0.0005], 'p': [[1e-4, 0, 0], [0, 0.01, 0], [0, 0, 1e-3]],
|
||||
'count': 900, 'error_sum': 27.0}},
|
||||
'on_samples': 300, 'off_samples': 600, 'heater_entity': 'switch.boiler',
|
||||
'outside_source': 'sensor.outside'}
|
||||
self.c._store.async_load.return_value = legacy
|
||||
await self.c.async_restore_model()
|
||||
model = self.c.learners['sensor.room']
|
||||
self.assertEqual((model.theta, model.count), ([0.004, 0.11, 0.0005], 900))
|
||||
self.assertEqual((model.on_count, model.off_count, model.rejected_count), (0, 0, 0))
|
||||
|
||||
async def test_saved_model_stays_readable_by_v021(self):
|
||||
"""Rollback contract: the keys and types 0.2.1's restore code reads."""
|
||||
await self.sample()
|
||||
await self.sample()
|
||||
payload = self.c._store.async_save.call_args.args[0]
|
||||
self.assertIsInstance(payload['heater_entity'], str)
|
||||
for key in ('on_samples', 'off_samples'):
|
||||
self.assertIs(type(payload[key]), int)
|
||||
for data in payload['learners'].values():
|
||||
self.assertEqual(len(data['theta']), 3)
|
||||
self.assertTrue(all(len(row) == 3 for row in data['p']))
|
||||
self.assertIs(type(data['count']), int)
|
||||
float(data['error_sum'])
|
||||
[float(v) for v in data['theta'] + [v for row in data['p'] for v in row]]
|
||||
Reference in new issue
Block a user