Files
heating-mpc-ha/tests/test_physics.py
T

115 lines
5.4 KiB
Python

"""Offline tests: run python -m unittest discover -s tests -v."""
import importlib.util
import unittest
from pathlib import Path
path = Path(__file__).resolve().parents[1] / 'custom_components' / 'heating_mpc' / 'physics.py'
spec = importlib.util.spec_from_file_location('heating_mpc_physics', path)
m = importlib.util.module_from_spec(spec)
import sys
sys.modules[spec.name] = m
spec.loader.exec_module(m)
class ModelTests(unittest.TestCase):
def test_fitting_and_restore(self):
learn = m.Learner()
t = 20.0
lag = 0.0
for step in range(1000):
heater = int(step % 96 < 35)
lag = lag + m.HEAT_LAG * (heater - lag)
value = t + 0.003 * (8 - t) + 0.055 * lag + 0.001
learn.learn(t, value, 8, lag)
t = value
self.assertGreaterEqual(learn.count, 950)
self.assertAlmostEqual(learn.theta[0], 0.003, delta=0.003)
self.assertAlmostEqual(learn.theta[1], 0.055, delta=0.03)
restored = m.Learner.from_dict(learn.as_dict())
self.assertEqual(restored.count, learn.count)
self.assertEqual(restored.theta, learn.theta)
def test_optimize_warm_room_prefers_off(self):
zones = {'living': 23.5, 'bedroom': 23.4}
learners = {'living': m.Learner(), 'bedroom': m.Learner()}
out = m.optimize(zones, 12, 0, 22, {'living': 2, 'bedroom': 1},
learners, False, 3600, horizon_minutes=60)
self.assertFalse(out['proposal'])
self.assertEqual(len(out['plan_15min']), 4)
def test_optimize_cold_room_prefers_heat(self):
zones = {'living': 19.5, 'bedroom': 19.9}
learners = {'living': m.Learner(), 'bedroom': m.Learner()}
out = m.optimize(zones, 4, 0, 22, {'living': 2, 'bedroom': 1},
learners, False, 3600, horizon_minutes=60)
self.assertTrue(out['proposal'])
def test_minimum_duration_on_and_off(self):
learners = {'r': m.Learner()}
# Could not switch OFF during first 15 minutes if just turned ON.
out = m.optimize({'r': 24}, 12, 1, 22, {'r': 1}, learners, True, 120,
minimum_on_minutes=15, horizon_minutes=60)
self.assertTrue(out['proposal'])
# Could not switch ON during first 10 minutes if just turned OFF.
out = m.optimize({'r': 18}, 0, 0, 22, {'r': 1}, learners, False, 120,
minimum_off_minutes=10, horizon_minutes=60)
self.assertFalse(out['proposal'])
def test_invalid_room_weights_rejected(self):
with self.assertRaises(ValueError):
m.optimize({'r': 20}, 10, 0, 22, {'r': 0}, {'r': m.Learner()},
False, 1000, horizon_minutes=60)
def test_short_horizon_does_not_claim_60_minute_prediction(self):
out = m.optimize({'r': 20}, 5, 0, 22, {'r': 1}, {'r': m.Learner()},
False, 3600, horizon_minutes=30)
self.assertEqual(out['predicted_60m'], {})
self.assertIn('r', out['predicted_end'])
def test_prediction_matches_replay_at_60_minutes(self):
learners = {'r': m.Learner()}
out = m.optimize({'r': 20}, 5, .4, 22, {'r': 1}, learners, False, 3600,
horizon_minutes=90)
temps, lag = {'r': 20}, .4
for index, bit in enumerate(out['plan_15min']):
for _ in range(3):
temps, lag = m.simulate_step(temps, 5, lag, bit, learners)
if index == 3:
self.assertEqual(out['predicted_60m']['r'], round(temps['r'], 3))
self.assertEqual(out['predicted_end']['r'], round(temps['r'], 3))
def test_jitter_normalized_and_rejection_counted(self):
nominal, delayed = m.Learner(), m.Learner()
self.assertTrue(nominal.learn(20, 20.1, 5, .5, command=True))
self.assertTrue(delayed.learn(20, 20.11, 5, .5, 330, True))
for a, b in zip(nominal.theta, delayed.theta):
self.assertAlmostEqual(a, b)
self.assertFalse(delayed.learn(20, 25, 5, .5, command=False))
self.assertEqual((delayed.count, delayed.on_count, delayed.off_count), (1, 1, 0))
def test_corrupt_model_restore_is_atomic(self):
for changes in ({'theta': [-1, .1, 0]}, {'p': [[-1, 0, 0], [0, 1, 0], [0, 0, 1]]},
{'count': float('inf')}, {'count': '5'}, {'error_sum': float('nan')},
{'off_count': 1}, {'theta': [float('nan'), 0, 0]}, {'p': None}):
with self.subTest(changes=changes):
data = m.Learner().as_dict()
data.update(changes)
self.assertEqual(m.Learner.from_dict(data), m.Learner())
for bad in (None, [], 'broken'):
self.assertEqual(m.Learner.from_dict(bad), m.Learner())
def test_nonfinite_optimizer_inputs_rejected(self):
for key, value in (('weights', {'r': float('nan')}), ('effective_heat', float('nan')),
('switching_cost', float('inf')), ('seconds_since_change', -1),
('minimum_on_minutes', -1), ('comfort_band', float('nan'))):
args = dict(temps={'r': 20}, outside=5, effective_heat=0, target=22,
weights={'r': 1}, learners={'r': m.Learner()}, is_on=False,
seconds_since_change=3600, horizon_minutes=60)
args[key] = value
with self.subTest(key=key), self.assertRaises(ValueError):
m.optimize(**args)
if __name__ == '__main__':
unittest.main()