FA-92341 / HVAC thermostat control / Open access
Auto changeover squeezes the deadband by lowering the heating setpoint · case 01
Raising cooling close to heating silently drops the heating setpoint the user just chose.
ROOT CAUSE
The minimum-deadband rule moves the heating setpoint down instead of the cooling setpoint up.
VERIFIED REPAIR
Enforce the deadband by raising the cooling setpoint to heat_sp + 3.0 F.
Unsuccessful approach: Adding the deadband to the existing cooling setpoint overshoots when the gap was already partly open.
Case contract
Auto heat/cool thermostat sampled once per minute (temps in tenths F). If the cooling setpoint is less than 3.0 F above the heating setpoint it is pushed up to heat_sp + 30 (the heating setpoint wins). Heating starts below heat_sp - 1.0 F and ends at temp >= heat_sp; cooling starts above cool_sp + 1.0 F and ends at temp <= cool_sp. After a heating call ends, cooling may not start until 5 minutes later, and vice versa. Return heat, cool or idle per minute.
Why this case matters
Residential and light-commercial thermostats make these decisions sample by sample; each defect changes equipment calls in a way that shows up as short cycling, comfort complaints or equipment stress.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(heat_sp, cool_sp, temps):
diff = 10
if cool_sp - heat_sp < 30:
heat_sp = cool_sp - 30
state = 'idle'
last_heat_end = None
last_cool_end = None
out = []
for minute, temp in enumerate(temps):
if state == 'heat' and temp >= heat_sp:
state = 'idle'
last_heat_end = minute
elif state == 'cool' and temp <= cool_sp:
state = 'idle'
last_cool_end = minute
if state == 'idle':
if temp < heat_sp - diff and (last_cool_end is None or minute - last_cool_end >= 5):
state = 'heat'
elif temp > cool_sp + diff and (last_heat_end is None or minute - last_heat_end >= 5):
state = 'cool'
out.append(state)
return out
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['regression: scenario 1',
[680, 690, [693, 687, 709, 709, 685, 704, 684, 680, 686, 687, 679]],
['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle']],
['regression: scenario 2',
[680, 700, [681, 686, 698, 703, 724, 744, 752, 732, 755, 742, 762, 772]],
['idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool']],
['control: scenario 3',
[680,
740,
[649,
667,
643,
622,
637,
657,
634,
649,
674,
654,
672,
675,
698,
687,
666,
654,
670,
645,
654,
637,
651,
672]],
['heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat']],
['control: scenario 4',
[680, 720, [678, 693, 708, 706, 691, 698, 702, 725, 720, 725, 717, 703, 717, 731, 714, 728, 740, 735]],
['idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'idle',
'idle',
'cool',
'cool']]],
[['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['regression: scenario 1',
[680, 700, [687, 695, 710, 721, 712, 724, 736, 738, 737, 740, 748]],
['idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool']],
['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['control: scenario 2',
[680, 710, [674, 681, 688, 699, 718, 698, 716, 709, 691, 672, 678, 701, 724, 699, 683, 698, 691, 701]],
['idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'idle',
'idle',
'idle',
'idle',
'idle']],
['regression: scenario 3',
[680, 690, [686, 679, 704, 696, 678, 658, 635, 648, 643, 657, 650, 661, 672, 691, 687, 685, 708, 687]],
['idle',
'idle',
'idle',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle']],
['control: scenario 4',
[680, 710, [747, 771, 775, 769, 770, 766, 790, 790, 790, 789, 781]],
['cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool']]],
[['regression: scenario 6',
[680, 700, [757, 740, 724, 721, 702, 699, 724, 717, 742, 721, 723, 700]],
['cool', 'cool', 'cool', 'cool', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'idle']],
['regression: scenario 8',
[680, 690, [726, 722, 736, 726, 744, 763, 778, 789, 774, 760, 781, 786, 790, 783, 770, 770, 750]],
['cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool']],
['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: scenario 1',
[680, 710, [758, 778, 769, 788, 780, 787, 790, 773, 789, 766, 785, 779, 790, 790, 790, 777, 790]],
['cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool']],
['control: scenario 2',
[680, 740, [646, 654, 672, 672, 692, 717, 736, 759, 769, 790, 790]],
['heat', 'heat', 'heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']]],
[['regression: scenario 3',
[680, 690, [720, 710, 717, 702, 708, 715, 732, 753, 772, 753, 757]],
['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool']],
['regression: scenario 13',
[680, 700, [732, 729, 742, 749, 733, 737, 714, 714, 716, 713, 725, 733, 737, 726, 729, 723, 745]],
['cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool']],
['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['regression: scenario 1',
[680,
690,
[671, 646, 636, 635, 620, 625, 635, 653, 659, 636, 645, 633, 620, 628, 620, 638, 653, 655, 666, 678]],
['idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat']],
['control: scenario 2',
[680, 740, [655, 638, 663, 641, 622, 636, 628, 621, 621, 620, 635, 632, 644, 651, 656, 675, 687]],
['heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle']]],
[['regression: scenario 8',
[680, 690, [656, 635, 650, 663, 673, 651, 644, 644, 623, 630, 643, 655]],
['heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat']],
['regression: scenario 22',
[680, 690, [707, 687, 693, 697, 709, 698, 723, 702, 723, 731, 749, 743, 742, 754, 733, 715, 708]],
['idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'idle',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'idle']],
['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: scenario 1',
[680, 720, [774, 779, 785, 790, 790, 781, 778, 777, 770, 767, 743, 730, 708, 690]],
['cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'idle',
'idle']],
['control: scenario 2',
[680,
710,
[701,
679,
702,
721,
727,
748,
743,
720,
741,
738,
762,
769,
758,
740,
744,
725,
748,
728,
742,
755,
769]],
['idle',
'idle',
'idle',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool']]]]
for label, args, expected in fixtures[N-1]:
check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression: heat then locked cool | ['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool'] | ['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle'] | Failed |
| regression: deadband squeeze | ['cool', 'cool', 'cool', 'cool', 'cool'] | ['idle', 'cool', 'cool', 'idle', 'idle'] | Failed |
| control: exact thresholds | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | Passed |
| control: short heat cycle then hot | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | Passed |
| regression: scenario 1 | ['idle', 'idle', 'cool', 'cool', 'idle', 'cool', 'idle', 'idle', 'idle', 'idle', 'idle'] | ['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle'] | Failed |
| regression: scenario 2 | ['idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool'] | ['idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool'] | Passed |
| control: scenario 3 | ['heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'idle', 'idle', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat'] | ['heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'idle', 'idle', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat'] | Passed |
| control: scenario 4 | ['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'idle', 'idle', 'cool', 'cool'] | ['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'idle', 'idle', 'cool', 'cool'] | Passed |
SHA-256 / aef35a8798735b1b1d3c4ce048aac6a27337288f7bc5776480d63f1f8612347b
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(heat_sp, cool_sp, temps):
diff = 10
if cool_sp - heat_sp < 30:
cool_sp = cool_sp + 30
state = 'idle'
last_heat_end = None
last_cool_end = None
out = []
for minute, temp in enumerate(temps):
if state == 'heat' and temp >= heat_sp:
state = 'idle'
last_heat_end = minute
elif state == 'cool' and temp <= cool_sp:
state = 'idle'
last_cool_end = minute
if state == 'idle':
if temp < heat_sp - diff and (last_cool_end is None or minute - last_cool_end >= 5):
state = 'heat'
elif temp > cool_sp + diff and (last_heat_end is None or minute - last_heat_end >= 5):
state = 'cool'
out.append(state)
return out
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['regression: scenario 1',
[680, 690, [693, 687, 709, 709, 685, 704, 684, 680, 686, 687, 679]],
['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle']],
['regression: scenario 2',
[680, 700, [681, 686, 698, 703, 724, 744, 752, 732, 755, 742, 762, 772]],
['idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool']],
['control: scenario 3',
[680,
740,
[649,
667,
643,
622,
637,
657,
634,
649,
674,
654,
672,
675,
698,
687,
666,
654,
670,
645,
654,
637,
651,
672]],
['heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat']],
['control: scenario 4',
[680, 720, [678, 693, 708, 706, 691, 698, 702, 725, 720, 725, 717, 703, 717, 731, 714, 728, 740, 735]],
['idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'idle',
'idle',
'cool',
'cool']]],
[['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['regression: scenario 1',
[680, 700, [687, 695, 710, 721, 712, 724, 736, 738, 737, 740, 748]],
['idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool']],
['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['control: scenario 2',
[680, 710, [674, 681, 688, 699, 718, 698, 716, 709, 691, 672, 678, 701, 724, 699, 683, 698, 691, 701]],
['idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'idle',
'idle',
'idle',
'idle',
'idle']],
['regression: scenario 3',
[680, 690, [686, 679, 704, 696, 678, 658, 635, 648, 643, 657, 650, 661, 672, 691, 687, 685, 708, 687]],
['idle',
'idle',
'idle',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle']],
['control: scenario 4',
[680, 710, [747, 771, 775, 769, 770, 766, 790, 790, 790, 789, 781]],
['cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool']]],
[['regression: scenario 6',
[680, 700, [757, 740, 724, 721, 702, 699, 724, 717, 742, 721, 723, 700]],
['cool', 'cool', 'cool', 'cool', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'idle']],
['regression: scenario 8',
[680, 690, [726, 722, 736, 726, 744, 763, 778, 789, 774, 760, 781, 786, 790, 783, 770, 770, 750]],
['cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool']],
['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: scenario 1',
[680, 710, [758, 778, 769, 788, 780, 787, 790, 773, 789, 766, 785, 779, 790, 790, 790, 777, 790]],
['cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool']],
['control: scenario 2',
[680, 740, [646, 654, 672, 672, 692, 717, 736, 759, 769, 790, 790]],
['heat', 'heat', 'heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']]],
[['regression: scenario 3',
[680, 690, [720, 710, 717, 702, 708, 715, 732, 753, 772, 753, 757]],
['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool']],
['regression: scenario 13',
[680, 700, [732, 729, 742, 749, 733, 737, 714, 714, 716, 713, 725, 733, 737, 726, 729, 723, 745]],
['cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool']],
['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['regression: scenario 1',
[680,
690,
[671, 646, 636, 635, 620, 625, 635, 653, 659, 636, 645, 633, 620, 628, 620, 638, 653, 655, 666, 678]],
['idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat']],
['control: scenario 2',
[680, 740, [655, 638, 663, 641, 622, 636, 628, 621, 621, 620, 635, 632, 644, 651, 656, 675, 687]],
['heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle']]],
[['regression: scenario 8',
[680, 690, [656, 635, 650, 663, 673, 651, 644, 644, 623, 630, 643, 655]],
['heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat']],
['regression: scenario 22',
[680, 690, [707, 687, 693, 697, 709, 698, 723, 702, 723, 731, 749, 743, 742, 754, 733, 715, 708]],
['idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'idle',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'idle']],
['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: scenario 1',
[680, 720, [774, 779, 785, 790, 790, 781, 778, 777, 770, 767, 743, 730, 708, 690]],
['cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'idle',
'idle']],
['control: scenario 2',
[680,
710,
[701,
679,
702,
721,
727,
748,
743,
720,
741,
738,
762,
769,
758,
740,
744,
725,
748,
728,
742,
755,
769]],
['idle',
'idle',
'idle',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool']]]]
for label, args, expected in fixtures[N-1]:
check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression: heat then locked cool | ['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle'] | ['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle'] | Failed |
| regression: deadband squeeze | ['idle', 'idle', 'idle', 'idle', 'idle'] | ['idle', 'cool', 'cool', 'idle', 'idle'] | Failed |
| control: exact thresholds | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | Passed |
| control: short heat cycle then hot | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | Passed |
| regression: scenario 1 | ['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle'] | ['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle'] | Passed |
| regression: scenario 2 | ['idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool'] | ['idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool'] | Failed |
| control: scenario 3 | ['heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'idle', 'idle', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat'] | ['heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'idle', 'idle', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat'] | Passed |
| control: scenario 4 | ['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'idle', 'idle', 'cool', 'cool'] | ['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'idle', 'idle', 'cool', 'cool'] | Passed |
SHA-256 / 4dd2b39f0691e471b6f4df1475b25ecb50709b16f6c68ffc9aa3b0767fe05720
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(heat_sp, cool_sp, temps):
diff = 10
if cool_sp - heat_sp < 30:
cool_sp = heat_sp + 30
state = 'idle'
last_heat_end = None
last_cool_end = None
out = []
for minute, temp in enumerate(temps):
if state == 'heat' and temp >= heat_sp:
state = 'idle'
last_heat_end = minute
elif state == 'cool' and temp <= cool_sp:
state = 'idle'
last_cool_end = minute
if state == 'idle':
if temp < heat_sp - diff and (last_cool_end is None or minute - last_cool_end >= 5):
state = 'heat'
elif temp > cool_sp + diff and (last_heat_end is None or minute - last_heat_end >= 5):
state = 'cool'
out.append(state)
return out
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['regression: scenario 1',
[680, 690, [693, 687, 709, 709, 685, 704, 684, 680, 686, 687, 679]],
['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle']],
['regression: scenario 2',
[680, 700, [681, 686, 698, 703, 724, 744, 752, 732, 755, 742, 762, 772]],
['idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool']],
['control: scenario 3',
[680,
740,
[649,
667,
643,
622,
637,
657,
634,
649,
674,
654,
672,
675,
698,
687,
666,
654,
670,
645,
654,
637,
651,
672]],
['heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat']],
['control: scenario 4',
[680, 720, [678, 693, 708, 706, 691, 698, 702, 725, 720, 725, 717, 703, 717, 731, 714, 728, 740, 735]],
['idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'idle',
'idle',
'cool',
'cool']]],
[['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['regression: scenario 1',
[680, 700, [687, 695, 710, 721, 712, 724, 736, 738, 737, 740, 748]],
['idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool']],
['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['control: scenario 2',
[680, 710, [674, 681, 688, 699, 718, 698, 716, 709, 691, 672, 678, 701, 724, 699, 683, 698, 691, 701]],
['idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'idle',
'idle',
'idle',
'idle',
'idle']],
['regression: scenario 3',
[680, 690, [686, 679, 704, 696, 678, 658, 635, 648, 643, 657, 650, 661, 672, 691, 687, 685, 708, 687]],
['idle',
'idle',
'idle',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle']],
['control: scenario 4',
[680, 710, [747, 771, 775, 769, 770, 766, 790, 790, 790, 789, 781]],
['cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool']]],
[['regression: scenario 6',
[680, 700, [757, 740, 724, 721, 702, 699, 724, 717, 742, 721, 723, 700]],
['cool', 'cool', 'cool', 'cool', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'idle']],
['regression: scenario 8',
[680, 690, [726, 722, 736, 726, 744, 763, 778, 789, 774, 760, 781, 786, 790, 783, 770, 770, 750]],
['cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool']],
['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: scenario 1',
[680, 710, [758, 778, 769, 788, 780, 787, 790, 773, 789, 766, 785, 779, 790, 790, 790, 777, 790]],
['cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool']],
['control: scenario 2',
[680, 740, [646, 654, 672, 672, 692, 717, 736, 759, 769, 790, 790]],
['heat', 'heat', 'heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']]],
[['regression: scenario 3',
[680, 690, [720, 710, 717, 702, 708, 715, 732, 753, 772, 753, 757]],
['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool']],
['regression: scenario 13',
[680, 700, [732, 729, 742, 749, 733, 737, 714, 714, 716, 713, 725, 733, 737, 726, 729, 723, 745]],
['cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool']],
['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['regression: scenario 1',
[680,
690,
[671, 646, 636, 635, 620, 625, 635, 653, 659, 636, 645, 633, 620, 628, 620, 638, 653, 655, 666, 678]],
['idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat']],
['control: scenario 2',
[680, 740, [655, 638, 663, 641, 622, 636, 628, 621, 621, 620, 635, 632, 644, 651, 656, 675, 687]],
['heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle']]],
[['regression: scenario 8',
[680, 690, [656, 635, 650, 663, 673, 651, 644, 644, 623, 630, 643, 655]],
['heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat']],
['regression: scenario 22',
[680, 690, [707, 687, 693, 697, 709, 698, 723, 702, 723, 731, 749, 743, 742, 754, 733, 715, 708]],
['idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'idle',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'idle']],
['regression: heat then locked cool',
[680, 700, [650, 660, 690, 700, 712, 725, 725, 725, 715, 710]],
['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['regression: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: scenario 1',
[680, 720, [774, 779, 785, 790, 790, 781, 778, 777, 770, 767, 743, 730, 708, 690]],
['cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'idle',
'idle']],
['control: scenario 2',
[680,
710,
[701,
679,
702,
721,
727,
748,
743,
720,
741,
738,
762,
769,
758,
740,
744,
725,
748,
728,
742,
755,
769]],
['idle',
'idle',
'idle',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool',
'cool']]]]
for label, args, expected in fixtures[N-1]:
check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression: heat then locked cool | ['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle'] | ['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle'] | Passed |
| regression: deadband squeeze | ['idle', 'cool', 'cool', 'idle', 'idle'] | ['idle', 'cool', 'cool', 'idle', 'idle'] | Passed |
| control: exact thresholds | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | Passed |
| control: short heat cycle then hot | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | Passed |
| regression: scenario 1 | ['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle'] | ['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle'] | Passed |
| regression: scenario 2 | ['idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool'] | ['idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool', 'cool'] | Passed |
| control: scenario 3 | ['heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'idle', 'idle', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat'] | ['heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'idle', 'idle', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat'] | Passed |
| control: scenario 4 | ['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'idle', 'idle', 'cool', 'cool'] | ['idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'idle', 'idle', 'cool', 'cool'] | Passed |
SHA-256 / 08cef424efbeab7cd25a9356b6176c1ee9598dde826950b50fdb5aa4fc6a99e2
Verification & scope
A deterministic bounded teaching model of one thermostat or HVAC controller decision evaluated per sample. Temperatures are integer tenths of a degree Fahrenheit unless stated otherwise. The contract is a stipulated toy convention, not a claim of conformance to any manufacturer, ASHRAE guideline or code. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.
Observations recorded using Python 3.12.14 at 2026-09-29T14:51:44.901549+00:00.
Case digest / 6b56e3660143149dda6a27ee061aaf4b90f6fdbc5b80f59205b8c82b0332bcb8