FA-92351 / HVAC thermostat control / Open access
Heating call ends at the start threshold instead of at setpoint · case 01
Rooms never reach the heating setpoint and the call chatters at the bottom of the band.
ROOT CAUSE
The heat end test uses heat_sp - differential.
VERIFIED REPAIR
End the heating call when temperature reaches heat_sp.
Unsuccessful approach: Ending only above setpoint overshoots by one tenth on every cycle.
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 - diff:
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: 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']],
['regression: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: 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: 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, [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 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: scenario 6',
[680,
690,
[694, 696, 693, 697, 678, 655, 668, 684, 659, 671, 693, 680, 655, 660, 640, 655, 657, 634, 622, 620]],
['idle',
'idle',
'idle',
'idle',
'idle',
'heat',
'heat',
'idle',
'heat',
'heat',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat']],
['regression: scenario 29',
[680, 700, [694, 696, 676, 693, 679, 684, 666, 676, 671, 659, 680, 680, 658, 650, 662, 677]],
['idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat']],
['control: 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: 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: 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']]],
[['regression: scenario 10',
[680, 700, [693, 681, 656, 676, 697, 710, 694, 681, 682, 707, 717, 711, 736, 731, 735]],
['idle',
'idle',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'cool',
'cool']],
['regression: scenario 52',
[680, 700, [670, 669, 652, 663, 640, 620, 634, 649, 660, 680]],
['idle', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'idle']],
['control: 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: 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: 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']],
['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 9',
[680, 690, [659, 637, 655, 663, 655, 654, 639, 635, 631, 656, 667, 672, 683, 704, 683, 696, 718]],
['heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle']],
['regression: scenario 35',
[680,
690,
[712,
690,
682,
662,
680,
655,
665,
643,
622,
636,
657,
681,
658,
663,
656,
663,
649,
661,
657,
643,
667]],
['idle',
'idle',
'idle',
'heat',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat']],
['control: 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: 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: 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']],
['regression: 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 19',
[680,
690,
[644, 668, 674, 684, 659, 674, 661, 653, 633, 642, 656, 675, 656, 679, 675, 677, 700, 714, 720]],
['heat',
'heat',
'heat',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle']],
['regression: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: 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: 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: scenario 3 | ['heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'heat', 'heat', 'idle', 'heat', 'heat', 'heat', 'heat', 'idle'] | ['heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'idle', 'idle', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat'] | Failed |
| regression: exact thresholds | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | Passed |
| control: 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 |
| control: short heat cycle then hot | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | 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 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 / 05efc62baa5ff4c86924658e92c95eaa41df8ace2653b21233890fc9cb59d290
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
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: 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']],
['regression: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: 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: 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, [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 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: scenario 6',
[680,
690,
[694, 696, 693, 697, 678, 655, 668, 684, 659, 671, 693, 680, 655, 660, 640, 655, 657, 634, 622, 620]],
['idle',
'idle',
'idle',
'idle',
'idle',
'heat',
'heat',
'idle',
'heat',
'heat',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat']],
['regression: scenario 29',
[680, 700, [694, 696, 676, 693, 679, 684, 666, 676, 671, 659, 680, 680, 658, 650, 662, 677]],
['idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat']],
['control: 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: 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: 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']]],
[['regression: scenario 10',
[680, 700, [693, 681, 656, 676, 697, 710, 694, 681, 682, 707, 717, 711, 736, 731, 735]],
['idle',
'idle',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'cool',
'cool']],
['regression: scenario 52',
[680, 700, [670, 669, 652, 663, 640, 620, 634, 649, 660, 680]],
['idle', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'idle']],
['control: 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: 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: 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']],
['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 9',
[680, 690, [659, 637, 655, 663, 655, 654, 639, 635, 631, 656, 667, 672, 683, 704, 683, 696, 718]],
['heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle']],
['regression: scenario 35',
[680,
690,
[712,
690,
682,
662,
680,
655,
665,
643,
622,
636,
657,
681,
658,
663,
656,
663,
649,
661,
657,
643,
667]],
['idle',
'idle',
'idle',
'heat',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat']],
['control: 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: 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: 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']],
['regression: 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 19',
[680,
690,
[644, 668, 674, 684, 659, 674, 661, 653, 633, 642, 656, 675, 656, 679, 675, 677, 700, 714, 720]],
['heat',
'heat',
'heat',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle']],
['regression: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: 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: 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: 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 |
| regression: exact thresholds | ['idle', 'heat', 'heat', 'idle', 'idle', 'idle', 'idle'] | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | Failed |
| control: 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 |
| control: short heat cycle then hot | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | 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 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 / 764dd9351ca2dab7fcfc59a8d06f3d0fa5a38e30c9812623f5a50dff1101b856
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: 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']],
['regression: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: 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: 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, [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 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: scenario 6',
[680,
690,
[694, 696, 693, 697, 678, 655, 668, 684, 659, 671, 693, 680, 655, 660, 640, 655, 657, 634, 622, 620]],
['idle',
'idle',
'idle',
'idle',
'idle',
'heat',
'heat',
'idle',
'heat',
'heat',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat']],
['regression: scenario 29',
[680, 700, [694, 696, 676, 693, 679, 684, 666, 676, 671, 659, 680, 680, 658, 650, 662, 677]],
['idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'heat',
'heat',
'heat',
'heat']],
['control: 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: 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: 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']]],
[['regression: scenario 10',
[680, 700, [693, 681, 656, 676, 697, 710, 694, 681, 682, 707, 717, 711, 736, 731, 735]],
['idle',
'idle',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'idle',
'cool',
'cool',
'cool']],
['regression: scenario 52',
[680, 700, [670, 669, 652, 663, 640, 620, 634, 649, 660, 680]],
['idle', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'heat', 'idle']],
['control: 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: 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: 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']],
['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 9',
[680, 690, [659, 637, 655, 663, 655, 654, 639, 635, 631, 656, 667, 672, 683, 704, 683, 696, 718]],
['heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle',
'idle',
'idle']],
['regression: scenario 35',
[680,
690,
[712,
690,
682,
662,
680,
655,
665,
643,
622,
636,
657,
681,
658,
663,
656,
663,
649,
661,
657,
643,
667]],
['idle',
'idle',
'idle',
'heat',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat']],
['control: 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: 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: 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']],
['regression: 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 19',
[680,
690,
[644, 668, 674, 684, 659, 674, 661, 653, 633, 642, 656, 675, 656, 679, 675, 677, 700, 714, 720]],
['heat',
'heat',
'heat',
'idle',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'heat',
'idle',
'idle',
'idle']],
['regression: exact thresholds',
[680, 720, [670, 669, 680, 731, 730, 720, 721]],
['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle']],
['control: 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: 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: 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 |
| regression: exact thresholds | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | ['idle', 'heat', 'idle', 'idle', 'idle', 'idle', 'idle'] | Passed |
| control: 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 |
| control: short heat cycle then hot | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | ['heat', 'idle', 'idle', 'idle', 'idle', 'idle', 'cool', 'cool'] | 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 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 / cd356e0c7a5db4aab8bd9ebf549e12c431be5f53be131e3ad936a90a0d1d1e2b
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.989970+00:00.
Case digest / cbc6a46fbee2fcde8348a55e8bffe114cfa2ee1a7a5a325801b46673839e98fe