Repository navigation
Conversation
…t, relative hysteresis The cost function was unbounded (0.09 to 700+ across cycles depending on error and slope), which made the fixed absolute hysteresis margins meaningless and let the controller chase sub-band sensor noise. On 80 days of production data the controller changed fan mode 357 times with 148 A->B->A reversals inside an hour, flip-flopping across the full ladder while the room sat at the setpoint. Three coupled changes in _simulate_mode / _required_switch_gain: - EMPC-lite tolerance band: overshoot and floor-violation penalties now only apply *beyond* +/- deadband of the setpoint, like comfort_error already did. Previously any sub-band deviation triggered the heavy floor-violation weights (12 linear + 30 quadratic per step), which is what slammed the fan to superhigh for a 0.2C (one sensor LSB) excursion. - Per-step cost normalisation: the trajectory penalty is divided by the number of simulated steps so the magnitude is independent of the (dead-time-dependent) horizon and comparable across cycles. The one-time mode-change / rank / min- interval costs stay on that same per-step scale. - Relative hysteresis: a switch must beat the current mode's cost by a fraction of that cost on top of the absolute directional margin, so the threshold scales with the discomfort instead of being a fixed number vs an unbounded cost. Never applied to an upward switch while under-conditioned, so comfort recovery is unaffected. Also raise MODE_RANK_COST 0.05 -> 0.15 (now meaningful post-normalisation) and fix evaluate() calling get_dead_time() without hvac_mode, which mixed heat/cool dead times while the rest of the code keeps them separate. Replay on the production trace: fan changes 357 -> 130 (-64%), reversals within 60 min 148 -> 21 (-86%), prediction MAE unchanged (0.136 -> 0.140). Key behavioural guardrails (escalate below target, hold superhigh while below target, setpoint drop, long-dead-time horizon, gap model) verified unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
limit_timeout never influenced any control decision: it flowed only into get_effective_timeout(), which feeds diagnostic sensors and the CSV column but gates nothing (min_interval is the actual cooldown). Learning even auto-computed and auto-applied a limit_timeout value that then went unused. It was a third configuration knob for the user to reason about with no functional effect. Remove the parameter end to end: - const: CONF_LIMIT_TIMEOUT, DEFAULT_LIMIT_TIMEOUT, MIN_LIMIT_TIMEOUT - config_flow: the option in both the setup and options schemas - MPCController: the limit_timeout constructor arg and property; get_effective_timeout now falls back to DEFAULT_DEAD_TIME * safety factor internally - ThermalLearning.compute_optimal_parameters: stop computing/returning limit_timeout - __init__: stop applying it; seed last_change_time from min_interval instead - sensor: drop the Learned Limit Timeout sensor and the limit_timeout attributes - translations (en/fr), README The Effective Timeout sensor and the CSV effective_timeout column are kept as diagnostics (they show the learned thermal lag), now clearly documented as advisory rather than control-gating. Config now exposes deadband and min_interval only. Behaviour verified unchanged on the production replay: 130 changes, 21 reversals, identical to the previous commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lates Production trace 2026-07-05: room sat 0.4C above a 24.0C cooling setpoint for over an hour with the fan stuck at 'low', which was the model's genuine argmin (no hysteresis/min-interval hold). On a capacity-bound hot day the higher modes' learned cooling slopes were weak, so once the comfort-cost difference between modes was small the MODE_RANK_COST (energy) term tie-broke toward the cheapest mode -- the controller "gave up" and stayed quiet above the setpoint. Raising MODE_RANK_COST 0.05->0.15 in the stability work made this more likely. Gate the fan-rank (energy) cost on the comfort band: it now applies only when the room is within +/- deadband of target. When under-conditioned beyond the band (current_error > deadband), energy_weight = 0 so comfort dominates and the controller picks the mode that actually recovers the setpoint. Economy still applies in-band, where dropping to the quietest sufficient mode is what we want. Reproduced decision (24.4C vs 24.0C cool, weak higher-mode profiles, bias -0.25): now escalates to superhigh instead of holding low. Full replay: oscillation not reintroduced (reversals within 60 min 21 -> 25 over 228 h, vs 148 baseline), prediction MAE unchanged. Behavioural guardrails still pass. Kept as a hard in/out-of-band switch for now; if field data shows chatter at the band edge, soften to an energy weight that ramps with distance to the band. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…enforcement Audit of the learned effective-slope profiles on 80 days of production data surfaced two defects that corrupted the model, most visibly in heating: 1. Extrapolation blow-up. The representative slope is reported at REFERENCE_SLOPE_ERROR (1.0 C) and the sim applies the fitted gain across the whole horizon. A profile only ever sampled at/below setpoint (e.g. silent/heat, errors -1.0..0.0) still had a gain fitted and extrapolated out to +1.0 C, producing a physically impossible 2.82 C/h. Now, when a profile has never been observed at an error near the reference gap, we fall back to the constant median instead of trusting the extrapolation (silent/heat 2.82 -> 0.43). 2. Monotone enforcement corrupted by a thin low mode. build_monotone_slopes used a naive forward max(), so one bad weak-mode value dragged every stronger mode up to it: with silent/heat blown up, heat became silent=med=superhigh=2.82, erasing all fan differentiation. Replaced with sample-count-weighted isotonic regression (pool-adjacent-violators): out-of-order neighbours merge into their weighted mean, so the well-sampled modes (superhigh had 1742 cool samples vs silent's 20) dominate and a noisy 17-sample profile can't poison the rest. After both fixes, heat resolves to a sane non-decreasing 0.32/0.32/0.48. Cool profiles (already monotone, well sampled) are unchanged; full replay is unchanged (130 changes, 25 reversals, MAE 0.139) since this trace is cool-heavy and the defects hit heating. Behavioural guardrails still pass. Updated the two monotone tests that asserted the old forward-clamp values to the pooled results. Not addressed here (deeper, logged for follow-up): the effective slope still conflates fan capacity with the ambient heat load during sampling (negative silent/low cool slopes = "warms the room on a hot day"), the |slope|<0.15 stagnation filter drops 37% of samples biased toward the weak modes, and the gap-model r-squared is low (0.04-0.37) so error poorly predicts slope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…/above in heat Per the requested comfort semantics: a cooling setpoint of 24 C means "24 or less", a heating setpoint means "24 or more". The symmetric deadband let the room settle anywhere in [target-band, target+band], so in cooling it habitually sat up to a deadband ABOVE the setpoint (e.g. 24.2 with target 24.0) and the controller treated that as in-band and stayed at the lowest fan. Make the band asymmetric in the signed comfort error (positive = under- conditioned = wrong side of the setpoint for the active mode): - Wrong side (error > 0): penalised from the setpoint with no tolerance, via the FLOOR_VIOLATION terms, so the room is always driven back to the requested side. - Acceptable side (error < 0): only gently discouraged (OVERSHOOT term) so it is fine to be a little cooler than a cool setpoint / warmer than a heat setpoint without hunting. To avoid slamming the fan to maximum for a single sensor step past the setpoint, the correction is graduated: the under-side weights were retuned down (FLOOR_VIOLATION_LINEAR 12->4, QUADRATIC 30->12) and OVERSHOOT raised (3->8), and the energy (fan-rank) weight now ramps linearly from 1 at the setpoint to 0 at the deadband instead of switching off abruptly. Net decision profile in cooling: <=target -> low, +0.2 -> high, +0.4 -> superhigh; below target stays low. Behavioural guardrails still pass. IMPORTANT VALIDATION CAVEAT: this changes the closed-loop trajectory, so the open-loop CSV replay is not representative — it replays temperatures produced under the old symmetric band (room riding above the cool setpoint), so it over-counts activity (217 changes / 87 reversals vs 130/25). The real hunting behaviour can only be confirmed by deploying on the live system. If it hunts at the setpoint edge, the lever is a small wrong-side tolerance (act at +2 sensor LSB) or the deadband. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first cut of the asymmetric band softened the under-side penalty (FLOOR 12/30 -> 4/12, OVERSHOOT 3 -> 8, overshoot penalised from the setpoint) to avoid slamming the fan to maximum. On the deployed system that turned out to be fragile: at a small in-deadband excursion (room 0.2 C above a 24.0 C cool setpoint) it went straight back to holding `low` — the very stuck-above-setpoint behaviour it was meant to fix. A robustness sweep confirmed the decision at +0.2 flips with the learned dead time and the disturbance bias, because escalating on a sub-deadband error through the finite-horizon projection is inherently marginal. Restore the full under-side weights (FLOOR_VIOLATION 12/30, OVERSHOOT 3) and keep the overshoot term on the tolerated-zone form (max(-error - band, 0)), so the acceptable side stays free within the deadband while the wrong side is penalised firmly from the setpoint. At the live conditions (dead time 16 min, bias ~0) the room at 24.2 C now escalates to superhigh once the min interval clears, and steps back down to low as soon as it is at/below target; below target it stays low. Trade-off accepted for the stated requirement (never above a cool setpoint / below a heat setpoint): the correction is firm (tends to superhigh for a small excursion) rather than gently graduated — a robust graduated point does not exist across the dead-time/bias range. Behavioural guardrails pass. Replay 170 changes / 66 reversals (open-loop, still not representative for this trajectory-changing change; below the 148-reversal baseline). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.