FA-92361 / HVAC thermostat control / Open access
Changeover delay is measured from when heating started · case 01
Cooling can start immediately after a long heating call because the lockout already expired.
ROOT CAUSE
The heating timestamp used for the changeover lockout is recorded at heat start rather than heat end.
THE FAILURE
The heating timestamp used for the changeover lockout is recorded at heat start rather than heat end.
Unsuccessful approach: Stamping the last heating minute instead of the end minute releases the lockout a minute early.
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:
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'
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'
last_heat_end = minute
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: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: 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']],
['control: 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: 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 25',
[680, 710, [674, 651, 640, 644, 663, 686, 668, 681, 706, 727, 723, 727, 746, 737, 716, 713, 725, 747]],
['idle',
'heat',
'heat',
'heat',
'heat',
'idle',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle',
'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: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: 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']],
['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']],
['control: 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']]],
[['regression: 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 14',
[680, 710, [678, 658, 672, 689, 668, 655, 667, 652, 671, 687, 710, 730, 739, 757, 736, 715]],
['idle',
'heat',
'heat',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle',
'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']],
['regression: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['control: 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 3',
[680, 710, [676, 683, 703, 725, 749, 746, 749, 735, 740, 755, 765, 742, 727, 737, 745, 751, 762]],
['idle',
'idle',
'idle',
'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']],
['regression: scenario 4',
[680,
690,
[646, 652, 631, 639, 659, 675, 688, 663, 665, 676, 683, 703, 716, 725, 740, 750, 730, 725, 738]],
['heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'cool',
'cool',
'cool']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['regression: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['control: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: 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']],
['control: 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: 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: scenario 28',
[680, 710, [657, 667, 661, 671, 681, 698, 707, 731, 740, 738, 726, 715, 719, 716, 694, 717]],
['heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'cool',
'cool',
'cool',
'cool',
'idle',
'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['regression: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['control: 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']],
['control: scenario 3',
[680, 720, [645, 664, 650, 632, 620, 628, 652, 666, 661, 660, 644]],
['heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat']]]]
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', 'cool', 'cool', 'cool', 'cool', 'idle'] | ['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle'] | Failed |
| regression: short heat cycle then hot | ['heat', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool'] | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | Failed |
| control: exact thresholds | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | Passed |
| control: deadband squeeze | ['idle', 'cool', 'cool', 'idle', 'idle'] | ['idle', 'cool', 'cool', 'idle', 'idle'] | Passed |
| control: 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 |
| control: 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 / 72f66789348c9b4c25a3ea6b433e06c3f20238ae5925b3cbd3e90c4eaa0b0770
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 = 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 - 1
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: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: 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']],
['control: 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: 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 25',
[680, 710, [674, 651, 640, 644, 663, 686, 668, 681, 706, 727, 723, 727, 746, 737, 716, 713, 725, 747]],
['idle',
'heat',
'heat',
'heat',
'heat',
'idle',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle',
'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: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: 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']],
['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']],
['control: 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']]],
[['regression: 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 14',
[680, 710, [678, 658, 672, 689, 668, 655, 667, 652, 671, 687, 710, 730, 739, 757, 736, 715]],
['idle',
'heat',
'heat',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle',
'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']],
['regression: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['control: 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 3',
[680, 710, [676, 683, 703, 725, 749, 746, 749, 735, 740, 755, 765, 742, 727, 737, 745, 751, 762]],
['idle',
'idle',
'idle',
'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']],
['regression: scenario 4',
[680,
690,
[646, 652, 631, 639, 659, 675, 688, 663, 665, 676, 683, 703, 716, 725, 740, 750, 730, 725, 738]],
['heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'cool',
'cool',
'cool']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['regression: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['control: deadband squeeze',
[700, 705, [740, 741, 735, 730, 729]],
['idle', 'cool', 'cool', 'idle', 'idle']],
['control: 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']],
['control: 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: 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: scenario 28',
[680, 710, [657, 667, 661, 671, 681, 698, 707, 731, 740, 738, 726, 715, 719, 716, 694, 717]],
['heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'cool',
'cool',
'cool',
'cool',
'idle',
'idle']],
['control: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['regression: short heat cycle then hot',
[680, 760, [660, 685, 790, 790, 790, 790, 790, 790]],
['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool']],
['control: 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']],
['control: scenario 3',
[680, 720, [645, 664, 650, 632, 620, 628, 652, 666, 661, 660, 644]],
['heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat']]]]
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', 'cool', 'cool', 'cool', 'idle'] | ['heat', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'idle'] | Failed |
| regression: short heat cycle then hot | ['heat', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool', 'cool'] | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | Failed |
| control: exact thresholds | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | Passed |
| control: deadband squeeze | ['idle', 'cool', 'cool', 'idle', 'idle'] | ['idle', 'cool', 'cool', 'idle', 'idle'] | Passed |
| control: 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 |
| control: 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 / c19c637afc17900e8c9be2a5fa67ca44d9f1185a86065ca88ff30c1a481a880b
HELD IN THE MEMBER ARCHIVE
The verified repair and its recorded checks are member-only.
This mechanism has 8 recorded checks per implementation. The open-access tier publishes the failure and the unsuccessful fix; the repaired source that passes every check, and the observations that prove it, are available to members.
Every case sharing this mechanism uses the same contract and the same repair, so this one record is held back for all of them.
Member access is invitation-based. Sign in with your invited account to inspect the repair.
Sign in to the archive ↗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.998670+00:00.
Case digest / 6b1656fb9bc34ea04d2040fd71ff7c354984535437db8b360a65afa400bf93a8