FAILURE MAP
← Case archive

FA-92501 / HVAC thermostat control / Open access

Celsius arrow press steps from a truncated half degree · case 01

The first press after switching to C sometimes leaves the displayed value unchanged or jumps the wrong way.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

The starting half-degree for a C step truncates the stored value instead of rounding half up.

VERIFIED REPAIR

Round the stored value half up to 0.5 C before stepping.

Unsuccessful approach: Rounding to the nearest whole degree C before doubling loses the half-degree position.

Case contract

Thermostat setpoint keypad. The setpoint is stored in tenths F. In C mode the display shows the stored value converted to C and rounded half up to 0.5 C; a press moves the displayed value by 0.5 C and stores exactly 320 + half_degrees*9 tenths F. In F mode the display rounds half up to whole F and a press moves the whole-degree value by 1 F. After each press the stored value is clamped to 45.0-90.0 F. Return [display, stored_tenths_F] per press.

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(unit, presses, start_f10):
    f10 = start_f10
    out = []
    for p in presses:
        step = 1 if p == 'up' else -1
        if unit == 'C':
            c_half = (f10 - 320) // 9
            f10 = 320 + (c_half + step) * 9
        else:
            f_whole = (f10 + 5) // 10
            f10 = (f_whole + step) * 10
        f10 = max(450, min(900, f10))
        if unit == 'C':
            c_half = (2 * (f10 - 320) + 9) // 18
            out.append(['%d.%d' % (c_half // 2, 5 * (c_half % 2)), f10])
        else:
            out.append([str((f10 + 5) // 10), f10])
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: scenario 19',
   ['C', ['up', 'up', 'up', 'up', 'up', 'up'], 659],
   [['19.5', 671], ['20.0', 680], ['20.5', 689], ['21.0', 698], ['21.5', 707], ['22.0', 716]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]],
  ['control: scenario 1',
   ['F', ['down', 'down', 'up', 'up', 'up', 'down', 'down', 'up'], 716],
   [['71', 710], ['70', 700], ['71', 710], ['72', 720], ['73', 730], ['72', 720], ['71', 710], ['72', 720]]]],
 [['regression: scenario 46',
   ['C', ['up', 'down', 'up', 'up', 'up', 'up', 'up'], 445],
   [['7.5', 455], ['7.0', 450], ['7.5', 455], ['8.0', 464], ['8.5', 473], ['9.0', 482], ['9.5', 491]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]],
  ['control: scenario 1',
   ['F', ['down', 'down', 'up', 'up', 'down', 'down', 'down', 'up'], 652],
   [['64', 640], ['63', 630], ['64', 640], ['65', 650], ['64', 640], ['63', 630], ['62', 620], ['63', 630]]]],
 [['regression: scenario 56',
   ['C', ['up', 'down', 'up', 'down', 'up'], 445],
   [['7.5', 455], ['7.0', 450], ['7.5', 455], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: scenario 1',
   ['C', ['up', 'up', 'down', 'up', 'down'], 707],
   [['22.0', 716], ['22.5', 725], ['22.0', 716], ['22.5', 725], ['22.0', 716]]]],
 [['regression: scenario 35',
   ['C', ['up', 'down', 'up', 'down', 'down', 'up', 'up', 'down'], 776],
   [['26.0', 788],
    ['25.5', 779],
    ['26.0', 788],
    ['25.5', 779],
    ['25.0', 770],
    ['25.5', 779],
    ['26.0', 788],
    ['25.5', 779]]],
  ['regression: scenario 5',
   ['C', ['down', 'down', 'up', 'up', 'up'], 445],
   [['7.0', 450], ['7.0', 450], ['7.5', 455], ['8.0', 464], ['8.5', 473]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]]],
 [['regression: scenario 32',
   ['C', ['down', 'down', 'up', 'down', 'down'], 652],
   [['18.0', 644], ['17.5', 635], ['18.0', 644], ['17.5', 635], ['17.0', 626]]],
  ['regression: scenario 6',
   ['C', ['down', 'down', 'down', 'up', 'down', 'down'], 725],
   [['22.0', 716], ['21.5', 707], ['21.0', 698], ['21.5', 707], ['21.0', 698], ['20.5', 689]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]]]]
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 fixtureActualExpectedOutcome
regression: scenario 19[['19.0', 662], ['19.5', 671], ['20.0', 680], ['20.5', 689], ['21.0', 698], ['21.5', 707]][['19.5', 671], ['20.0', 680], ['20.5', 689], ['21.0', 698], ['21.5', 707], ['22.0', 716]]Failed
regression: celsius steps[['22.5', 725], ['23.0', 734], ['22.5', 725]][['22.5', 725], ['23.0', 734], ['22.5', 725]]Passed
control: fahrenheit half value[['74', 740], ['73', 730], ['72', 720]][['74', 740], ['73', 730], ['72', 720]]Passed
control: upper clamp[['90', 900]][['90', 900]]Passed
regression: celsius off-grid start[['21.5', 707], ['22.0', 716]][['21.5', 707], ['22.0', 716]]Passed
control: celsius lower clamp[['7.0', 450], ['7.0', 450], ['7.5', 455]][['7.0', 450], ['7.0', 450], ['7.5', 455]]Passed
regression: celsius half-step display[['22.0', 716]][['22.0', 716]]Passed
control: scenario 1[['71', 710], ['70', 700], ['71', 710], ['72', 720], ['73', 730], ['72', 720], ['71', 710], ['72', 720]][['71', 710], ['70', 700], ['71', 710], ['72', 720], ['73', 730], ['72', 720], ['71', 710], ['72', 720]]Passed

SHA-256 / 2fcd500e73cb6783aaa593f16d0a274532fb547a161b587090b9b842d858c5ba

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(unit, presses, start_f10):
    f10 = start_f10
    out = []
    for p in presses:
        step = 1 if p == 'up' else -1
        if unit == 'C':
            c_half = 2 * ((f10 - 320 + 9) // 18)
            f10 = 320 + (c_half + step) * 9
        else:
            f_whole = (f10 + 5) // 10
            f10 = (f_whole + step) * 10
        f10 = max(450, min(900, f10))
        if unit == 'C':
            c_half = (2 * (f10 - 320) + 9) // 18
            out.append(['%d.%d' % (c_half // 2, 5 * (c_half % 2)), f10])
        else:
            out.append([str((f10 + 5) // 10), f10])
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: scenario 19',
   ['C', ['up', 'up', 'up', 'up', 'up', 'up'], 659],
   [['19.5', 671], ['20.0', 680], ['20.5', 689], ['21.0', 698], ['21.5', 707], ['22.0', 716]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]],
  ['control: scenario 1',
   ['F', ['down', 'down', 'up', 'up', 'up', 'down', 'down', 'up'], 716],
   [['71', 710], ['70', 700], ['71', 710], ['72', 720], ['73', 730], ['72', 720], ['71', 710], ['72', 720]]]],
 [['regression: scenario 46',
   ['C', ['up', 'down', 'up', 'up', 'up', 'up', 'up'], 445],
   [['7.5', 455], ['7.0', 450], ['7.5', 455], ['8.0', 464], ['8.5', 473], ['9.0', 482], ['9.5', 491]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]],
  ['control: scenario 1',
   ['F', ['down', 'down', 'up', 'up', 'down', 'down', 'down', 'up'], 652],
   [['64', 640], ['63', 630], ['64', 640], ['65', 650], ['64', 640], ['63', 630], ['62', 620], ['63', 630]]]],
 [['regression: scenario 56',
   ['C', ['up', 'down', 'up', 'down', 'up'], 445],
   [['7.5', 455], ['7.0', 450], ['7.5', 455], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: scenario 1',
   ['C', ['up', 'up', 'down', 'up', 'down'], 707],
   [['22.0', 716], ['22.5', 725], ['22.0', 716], ['22.5', 725], ['22.0', 716]]]],
 [['regression: scenario 35',
   ['C', ['up', 'down', 'up', 'down', 'down', 'up', 'up', 'down'], 776],
   [['26.0', 788],
    ['25.5', 779],
    ['26.0', 788],
    ['25.5', 779],
    ['25.0', 770],
    ['25.5', 779],
    ['26.0', 788],
    ['25.5', 779]]],
  ['regression: scenario 5',
   ['C', ['down', 'down', 'up', 'up', 'up'], 445],
   [['7.0', 450], ['7.0', 450], ['7.5', 455], ['8.0', 464], ['8.5', 473]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]]],
 [['regression: scenario 32',
   ['C', ['down', 'down', 'up', 'down', 'down'], 652],
   [['18.0', 644], ['17.5', 635], ['18.0', 644], ['17.5', 635], ['17.0', 626]]],
  ['regression: scenario 6',
   ['C', ['down', 'down', 'down', 'up', 'down', 'down'], 725],
   [['22.0', 716], ['21.5', 707], ['21.0', 698], ['21.5', 707], ['21.0', 698], ['20.5', 689]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]]]]
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 fixtureActualExpectedOutcome
regression: scenario 19[['19.5', 671], ['20.5', 689], ['21.5', 707], ['22.5', 725], ['23.5', 743], ['24.5', 761]][['19.5', 671], ['20.0', 680], ['20.5', 689], ['21.0', 698], ['21.5', 707], ['22.0', 716]]Failed
regression: celsius steps[['22.5', 725], ['23.5', 743], ['23.5', 743]][['22.5', 725], ['23.0', 734], ['22.5', 725]]Failed
control: fahrenheit half value[['74', 740], ['73', 730], ['72', 720]][['74', 740], ['73', 730], ['72', 720]]Passed
control: upper clamp[['90', 900]][['90', 900]]Passed
regression: celsius off-grid start[['21.5', 707], ['22.5', 725]][['21.5', 707], ['22.0', 716]]Failed
control: celsius lower clamp[['7.0', 450], ['7.0', 450], ['7.5', 455]][['7.0', 450], ['7.0', 450], ['7.5', 455]]Passed
regression: celsius half-step display[['22.5', 725]][['22.0', 716]]Failed
control: scenario 1[['71', 710], ['70', 700], ['71', 710], ['72', 720], ['73', 730], ['72', 720], ['71', 710], ['72', 720]][['71', 710], ['70', 700], ['71', 710], ['72', 720], ['73', 730], ['72', 720], ['71', 710], ['72', 720]]Passed

SHA-256 / 2487354ca993cce26b301b3189334c8ddbc5929681efeaeebb3b7eb856094794

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(unit, presses, start_f10):
    f10 = start_f10
    out = []
    for p in presses:
        step = 1 if p == 'up' else -1
        if unit == 'C':
            c_half = (2 * (f10 - 320) + 9) // 18
            f10 = 320 + (c_half + step) * 9
        else:
            f_whole = (f10 + 5) // 10
            f10 = (f_whole + step) * 10
        f10 = max(450, min(900, f10))
        if unit == 'C':
            c_half = (2 * (f10 - 320) + 9) // 18
            out.append(['%d.%d' % (c_half // 2, 5 * (c_half % 2)), f10])
        else:
            out.append([str((f10 + 5) // 10), f10])
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: scenario 19',
   ['C', ['up', 'up', 'up', 'up', 'up', 'up'], 659],
   [['19.5', 671], ['20.0', 680], ['20.5', 689], ['21.0', 698], ['21.5', 707], ['22.0', 716]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]],
  ['control: scenario 1',
   ['F', ['down', 'down', 'up', 'up', 'up', 'down', 'down', 'up'], 716],
   [['71', 710], ['70', 700], ['71', 710], ['72', 720], ['73', 730], ['72', 720], ['71', 710], ['72', 720]]]],
 [['regression: scenario 46',
   ['C', ['up', 'down', 'up', 'up', 'up', 'up', 'up'], 445],
   [['7.5', 455], ['7.0', 450], ['7.5', 455], ['8.0', 464], ['8.5', 473], ['9.0', 482], ['9.5', 491]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]],
  ['control: scenario 1',
   ['F', ['down', 'down', 'up', 'up', 'down', 'down', 'down', 'up'], 652],
   [['64', 640], ['63', 630], ['64', 640], ['65', 650], ['64', 640], ['63', 630], ['62', 620], ['63', 630]]]],
 [['regression: scenario 56',
   ['C', ['up', 'down', 'up', 'down', 'up'], 445],
   [['7.5', 455], ['7.0', 450], ['7.5', 455], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: scenario 1',
   ['C', ['up', 'up', 'down', 'up', 'down'], 707],
   [['22.0', 716], ['22.5', 725], ['22.0', 716], ['22.5', 725], ['22.0', 716]]]],
 [['regression: scenario 35',
   ['C', ['up', 'down', 'up', 'down', 'down', 'up', 'up', 'down'], 776],
   [['26.0', 788],
    ['25.5', 779],
    ['26.0', 788],
    ['25.5', 779],
    ['25.0', 770],
    ['25.5', 779],
    ['26.0', 788],
    ['25.5', 779]]],
  ['regression: scenario 5',
   ['C', ['down', 'down', 'up', 'up', 'up'], 445],
   [['7.0', 450], ['7.0', 450], ['7.5', 455], ['8.0', 464], ['8.5', 473]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]]],
 [['regression: scenario 32',
   ['C', ['down', 'down', 'up', 'down', 'down'], 652],
   [['18.0', 644], ['17.5', 635], ['18.0', 644], ['17.5', 635], ['17.0', 626]]],
  ['regression: scenario 6',
   ['C', ['down', 'down', 'down', 'up', 'down', 'down'], 725],
   [['22.0', 716], ['21.5', 707], ['21.0', 698], ['21.5', 707], ['21.0', 698], ['20.5', 689]]],
  ['regression: celsius steps',
   ['C', ['up', 'up', 'down'], 720],
   [['22.5', 725], ['23.0', 734], ['22.5', 725]]],
  ['control: fahrenheit half value',
   ['F', ['up', 'down', 'down'], 725],
   [['74', 740], ['73', 730], ['72', 720]]],
  ['control: upper clamp', ['F', ['up'], 895], [['90', 900]]],
  ['regression: celsius off-grid start', ['C', ['down', 'up'], 716], [['21.5', 707], ['22.0', 716]]],
  ['control: celsius lower clamp',
   ['C', ['down', 'down', 'up'], 452],
   [['7.0', 450], ['7.0', 450], ['7.5', 455]]],
  ['regression: celsius half-step display', ['C', ['up'], 707], [['22.0', 716]]]]]
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 fixtureActualExpectedOutcome
regression: scenario 19[['19.5', 671], ['20.0', 680], ['20.5', 689], ['21.0', 698], ['21.5', 707], ['22.0', 716]][['19.5', 671], ['20.0', 680], ['20.5', 689], ['21.0', 698], ['21.5', 707], ['22.0', 716]]Passed
regression: celsius steps[['22.5', 725], ['23.0', 734], ['22.5', 725]][['22.5', 725], ['23.0', 734], ['22.5', 725]]Passed
control: fahrenheit half value[['74', 740], ['73', 730], ['72', 720]][['74', 740], ['73', 730], ['72', 720]]Passed
control: upper clamp[['90', 900]][['90', 900]]Passed
regression: celsius off-grid start[['21.5', 707], ['22.0', 716]][['21.5', 707], ['22.0', 716]]Passed
control: celsius lower clamp[['7.0', 450], ['7.0', 450], ['7.5', 455]][['7.0', 450], ['7.0', 450], ['7.5', 455]]Passed
regression: celsius half-step display[['22.0', 716]][['22.0', 716]]Passed
control: scenario 1[['71', 710], ['70', 700], ['71', 710], ['72', 720], ['73', 730], ['72', 720], ['71', 710], ['72', 720]][['71', 710], ['70', 700], ['71', 710], ['72', 720], ['73', 730], ['72', 720], ['71', 710], ['72', 720]]Passed

SHA-256 / 39cbf77bf1a56e152aafe53f74935b37ba7ac6279fb5fc998d83f3f82e6ae5cd

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:46.315757+00:00.

Case digest / 66a3f755805e4edb38bc84961757572a49e966f81c4cf5eb4be1cb3cf7f76146