fix(mpc): pre-cool before a scheduled comfort window (cooling lookahead) - #404
fix(mpc): pre-cool before a scheduled comfort window (cooling lookahead)#404andgian wants to merge 3 commits into
Conversation
…indow Cool-capable rooms kept LOOKAHEAD_BASE_BLOCKS (6 blocks / 30 min), so the optimizer could not see an upcoming comfort-window setpoint drop far enough ahead to start cooling early. Heating (UFH) already pre-heats via an extended, tau-scaled lookahead; cooling had no equivalent, so an AC only reacted once the window opened - behaving like a plain scheduled thermostat. Add LOOKAHEAD_COOLING_BLOCKS (18 / 90 min) and take the lookahead as the max of the heating tau horizon and the cooling horizon. No afterglow synthesis is added for cooling: unlike a UFH slab, an AC has negligible stored-emission afterglow, so the RC model's post-run warm-back (block_Q=0) already models decay correctly, avoiding the over-cooling that motivated keeping cooling at the base lookahead. Hybrid UFH+AC rooms now get both (max): winter pre-heating is preserved and summer pre-cooling is enabled.
The three lookahead tests encoded the old rule that cool-capable rooms are
pinned to LOOKAHEAD_BASE_BLOCKS. Update them to the new rule - the lookahead
is the max of the heating tau horizon and the cooling horizon - rather than
loosening or removing the assertions:
- test_unknown_system_no_regression now asserts both halves of the rule: a
pure-heating room (can_cool=False) still keeps the base lookahead, while a
cool-capable room with no heating profile extends to LOOKAHEAD_COOLING_BLOCKS.
- test_lookahead_blocks_attribute_exposed gets the recomputed expectations for
all six setups, with the arithmetic behind each spelled out.
- test_hybrid_ufh_ac_cooling_balance_preserved asserted exactly the balance this
fix deliberately changes, so it is rewritten (and renamed) as
test_hybrid_ufh_ac_gets_max_of_both_horizons: a hybrid room keeps the full UFH
horizon and also clears the cooling horizon.
Also refresh the comments that described the old behaviour ("cooling stays at
base=6", "can_cool=False keeps the extension active") so they stop lying.
No production code is touched.
…okahead The synthesis gate keyed off self._lookahead_blocks > LOOKAHEAD_BASE_BLOCKS. Once the cooling horizon can raise that lookahead, a room whose own heating tau does not warrant synthesis gets it anyway just by being cool-capable: a radiator room (tau=10min, horizon 2+2=4, below base) paired with an AC reaches lookahead 18 and silently switches afterglow synthesis on for its HEATING hypothesis, making heating look cheaper than before. That is a heating-side behaviour change this fix never intended. Track the heating horizon separately from the combined lookahead and gate synthesis on the heating horizon alone. The combined lookahead is unchanged - still max(heating tau horizon, cooling horizon) - so pre-cooling is unaffected; only the synthesis eligibility is now decided by the heating system's own tau. Tests: test_ufh_afterglow_visible_in_cost forces the lookahead by hand to isolate synthesis, so it now sets the heating horizon too. New regression test test_cooling_extension_does_not_enable_heating_synthesis pins the rule: a radiator+AC room's heating cost must equal an unprofiled+AC room's at the same horizon, while UFH stays synthesis-eligible.
7e3006c to
7787fa3
Compare
| heating hypothesis: a radiator + AC room keeps the same heating cost as an | ||
| unprofiled room at the same horizon. | ||
| """ | ||
| lookahead = min(self._lookahead_blocks, len(future_T_outdoor)) |
There was a problem hiding this comment.
_evaluate_action() still uses the combined self._lookahead_blocks for the actual cost window (lookahead = min(self._lookahead_blocks, len(future_T_outdoor))); only the synthesis gate moved to _heating_lookahead_blocks.
So a radiator (or unprofiled) room with an AC now integrates HEATING/IDLE cost over 18 blocks instead of 6, even though this PR is framed as cooling-only. That shifts the DP action selection for heating too.
test_cooling_extension_does_not_enable_heating_synthesis only compares two rooms that both already have lookahead=18; nothing checks against the pre-fix baseline (radiator, can_cool=False, lookahead=6), so this cost shift isn't caught by any test.
Might be cleaner to use _heating_lookahead_blocks for HEATING/IDLE cost and reserve the combined lookahead for COOLING.
|
This guard window got a lot wider for any AC-equipped room, and I don't think that's intentional.
No test covers |
Problem
Cool-capable rooms use
LOOKAHEAD_BASE_BLOCKS(6 blocks / 30 min) for the MPC decision lookahead. That horizon is too short to see an upcoming comfort-window setpoint drop (e.g. a schedule lowering the cool target from an eco value to a comfort value), so the optimizer only starts cooling once the window opens — the room reaches comfort well after you need it, behaving like a plain scheduled thermostat.Heating already solves the symmetric problem: slow systems (UFH) get an extended, tau-scaled lookahead so the cost function values pre-heating before the window. Cooling had no equivalent.
The existing code deliberately kept cooling at the base lookahead, with the comment that extending it "without a matching cooling-side synthesis would shift the energy/comfort ratio for COOLING and cause more aggressive AC use."
Fix
LOOKAHEAD_COOLING_BLOCKS = 18(90 min) and take the per-plan lookahead as the max of the heating tau horizon (when a heating profile exists) and the cooling horizon (whencan_cool).block_Q = 0post-run) does not capture. An AC has negligible stored-emission afterglow: when it stops, cooling stops, and the room warms back per its RC time constant, which the model already captures. So the over-cooling the original comment worried about does not arise — the missing piece was the horizon, not a synthesis.max): winter pre-heating is preserved and summer pre-cooling is enabled. (Previouslynot self.can_coolexcluded hybrid rooms from the heating lookahead extension too; themaxkeeps their winter preheat.)self._lookahead_blocks > LOOKAHEAD_BASE_BLOCKS, so once cooling can raise that lookahead, a room whose own heating tau does not warrant synthesis would get it just by being cool-capable — a radiator room (tau=10 min, horizon2+2=4, below base) paired with an AC would reach lookahead 18 and silently enable synthesis on its HEATING hypothesis. The heating horizon is now tracked separately (_heating_lookahead_blocks) and gates synthesis on its own; the combined lookahead is unchanged, so pre-cooling is unaffected.Behaviour
can_cool=False: byte-identical (cooling branch skipped, heating branch unchanged in effect). This is narrower than "pure-heating rooms":MPCOptimizerdeclarescan_cool: bool = Trueas its dataclass default, so a room only takes the unchanged path whencan_cool=Falseis passed explicitly.max(base, heating tau horizon, cooling horizon).Testing
Deployed on a live Airstage (AC) + underfloor (UFH) hybrid setup; RoomMind loads cleanly and steady-state cooling control is unchanged. Pre-cool-before-window behaviour validated against a daily comfort schedule.
Full suite green with the CI command (
pytest tests/ -v --cov=custom_components/roommind --cov-report=term --cov-fail-under=95): 2018 passed, total coverage 95.83%,mpc_optimizer.pyat 100%.ruff check,ruff format --checkandmypyclean.The three lookahead tests that encoded the old "cool-capable rooms stay at base" rule are updated in a separate commit to assert the new rule (the max of the two horizons) rather than being loosened or removed. A regression test (
test_cooling_extension_does_not_enable_heating_synthesis) pins the synthesis gate: a radiator+AC room's heating cost must equal an unprofiled+AC room's at the same horizon, while UFH stays synthesis-eligible.