FAILURE MAP
← Case archive

FA-92531 / HVAC thermostat control / Open access

Economizer compares outdoor air above return air plus the differential · case 01

Outdoor air warmer than the return air is brought in for cooling.

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

ROOT CAUSE

The differential is added to the return temperature instead of subtracted.

VERIFIED REPAIR

Enable only when outdoor < return - diff.

Unsuccessful approach: Allowing outdoor equal to return - diff still enables one step too early.

Case contract

Rooftop economizer per sample; samples are [outdoor, return_air, cooling_call, mixed_air, occupied] in tenths F. Economizing is enabled when outdoor <= high_limit - 2.0 F and outdoor < return - diff, and disabled when outdoor > high_limit or outdoor >= return. Unoccupied: damper 0%. Occupied, enabled and cooling: if mixed air < mat_low the damper steps down 10% but not below min_pos, otherwise it steps up 20% from at least min_pos, capped at 100%. Otherwise the damper sits at min_pos. Return [enabled, damper%] per sample.

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(settings, samples):
    enabled = False
    pos = 0
    out = []
    for outdoor, ret, cooling, mixed, occupied in samples:
        if enabled and (outdoor > settings['high_limit'] or outdoor >= ret):
            enabled = False
        elif not enabled and outdoor <= settings['high_limit'] - 20 and outdoor < ret + settings['diff']:
            enabled = True
        if not occupied:
            pos = 0
        elif enabled and cooling:
            if mixed < settings['mat_low']:
                pos = max(settings['min_pos'], pos - 10)
            else:
                pos = min(100, max(settings['min_pos'], pos) + 20)
        else:
            pos = settings['min_pos']
        out.append([enabled, pos])
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 750, 'diff': 0, 'min_pos': 20, 'mat_low': 500},
    [[648, 701, True, 604, True],
     [640, 708, True, 484, True],
     [671, 716, True, 536, True],
     [649, 721, True, 616, False],
     [640, 749, True, 619, True],
     [651, 751, True, 599, True],
     [669, 718, False, 504, True],
     [638, 704, False, 542, True]]],
   [[True, 40], [True, 30], [True, 50], [True, 0], [True, 40], [True, 60], [True, 20], [True, 20]]],
  ['control: scenario 2',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[460, 757, True, 619, True],
     [462, 720, False, 618, True],
     [502, 734, True, 534, True],
     [488, 707, False, 516, True],
     [450, 735, False, 496, True],
     [472, 754, True, 606, True],
     [477, 741, False, 575, True],
     [452, 710, True, 531, True],
     [431, 759, False, 576, True]]],
   [[True, 40],
    [True, 20],
    [True, 40],
    [True, 20],
    [True, 20],
    [True, 40],
    [True, 20],
    [True, 40],
    [True, 20]]],
  ['control: scenario 3',
   [{'high_limit': 700, 'diff': 40, 'min_pos': 30, 'mat_low': 530},
    [[608, 734, True, 493, True],
     [609, 741, True, 531, True],
     [587, 758, True, 553, True],
     [561, 759, False, 498, False],
     [530, 741, False, 509, True],
     [544, 760, True, 493, True],
     [548, 751, True, 583, True],
     [512, 705, False, 540, False],
     [490, 711, True, 562, True]]],
   [[True, 30],
    [True, 50],
    [True, 70],
    [True, 0],
    [True, 30],
    [True, 30],
    [True, 50],
    [True, 0],
    [True, 50]]]],
 [['regression: scenario 3',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[715, 711, True, 506, True],
     [744, 713, True, 551, True],
     [732, 724, True, 568, True],
     [758, 725, False, 532, True],
     [795, 734, True, 552, True],
     [788, 742, True, 497, True],
     [760, 709, True, 515, False],
     [769, 759, False, 564, True],
     [795, 721, True, 546, True]]],
   [[False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 0],
    [False, 20],
    [False, 20]]],
  ['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 650, 'diff': 0, 'min_pos': 30, 'mat_low': 500},
    [[754, 754, False, 483, False],
     [716, 749, True, 499, True],
     [699, 701, False, 573, True],
     [694, 750, True, 534, False],
     [704, 716, False, 517, True],
     [708, 701, False, 567, True],
     [743, 735, True, 609, True],
     [775, 747, False, 554, True],
     [743, 731, False, 583, False],
     [764, 708, False, 507, True],
     [795, 743, False, 528, True],
     [770, 706, True, 501, True],
     [764, 752, False, 578, True]]],
   [[False, 0],
    [False, 30],
    [False, 30],
    [False, 0],
    [False, 30],
    [False, 30],
    [False, 30],
    [False, 30],
    [False, 0],
    [False, 30],
    [False, 30],
    [False, 30],
    [False, 30]]],
  ['control: scenario 2',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 30, 'mat_low': 500},
    [[492, 750, False, 518, True],
     [495, 710, True, 564, True],
     [507, 701, True, 603, True],
     [545, 705, False, 490, True],
     [529, 724, True, 489, False],
     [508, 721, True, 529, False],
     [482, 717, True, 619, True],
     [464, 753, False, 608, True],
     [478, 719, False, 559, False],
     [481, 730, False, 494, True],
     [511, 719, False, 613, True],
     [479, 743, False, 542, True],
     [477, 728, True, 597, True],
     [493, 756, False, 531, True]]],
   [[True, 30],
    [True, 50],
    [True, 70],
    [True, 30],
    [True, 0],
    [True, 0],
    [True, 50],
    [True, 30],
    [True, 0],
    [True, 30],
    [True, 30],
    [True, 30],
    [True, 50],
    [True, 30]]]],
 [['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 650, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[678, 721, True, 495, True],
     [646, 726, True, 616, True],
     [669, 714, True, 600, True],
     [630, 728, True, 542, True],
     [626, 751, True, 530, False],
     [638, 723, False, 607, True],
     [654, 706, True, 557, True],
     [625, 704, False, 618, True]]],
   [[False, 20], [False, 20], [False, 20], [True, 40], [True, 0], [True, 20], [False, 20], [True, 20]]],
  ['control: scenario 2',
   [{'high_limit': 650, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[599, 747, False, 570, True],
     [625, 701, False, 603, True],
     [623, 732, False, 565, False],
     [624, 737, True, 482, True],
     [637, 740, False, 485, True],
     [676, 702, True, 590, True],
     [678, 755, True, 522, True],
     [670, 733, True, 571, True],
     [684, 712, True, 616, False],
     [687, 736, True, 607, False],
     [712, 739, False, 524, True]]],
   [[True, 30],
    [True, 30],
    [True, 0],
    [True, 30],
    [True, 30],
    [False, 30],
    [False, 30],
    [False, 30],
    [False, 0],
    [False, 0],
    [False, 30]]],
  ['control: scenario 3',
   [{'high_limit': 650, 'diff': 0, 'min_pos': 20, 'mat_low': 500},
    [[761, 714, True, 573, False],
     [780, 758, False, 588, True],
     [761, 703, True, 548, True],
     [746, 756, True, 495, True],
     [714, 752, True, 488, True],
     [754, 748, True, 509, True],
     [719, 713, True, 561, True],
     [725, 724, False, 616, True],
     [720, 740, False, 513, False],
     [719, 725, True, 487, True],
     [756, 716, False, 534, True],
     [795, 715, True, 615, False]]],
   [[False, 0],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 0],
    [False, 20],
    [False, 20],
    [False, 0]]]],
 [['regression: scenario 25',
   [{'high_limit': 750, 'diff': 40, 'min_pos': 20, 'mat_low': 530},
    [[760, 736, True, 594, True],
     [745, 759, True, 518, False],
     [715, 703, True, 494, True],
     [724, 736, True, 521, True],
     [739, 728, True, 586, True],
     [739, 717, True, 609, True],
     [774, 731, True, 575, True],
     [755, 733, True, 581, False],
     [732, 715, True, 558, True],
     [712, 732, True, 614, True]]],
   [[False, 20],
    [False, 0],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 0],
    [False, 20],
    [False, 20]]],
  ['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 30, 'mat_low': 500},
    [[505, 738, False, 612, True],
     [535, 706, True, 498, True],
     [549, 734, False, 604, True],
     [558, 729, True, 521, True],
     [540, 724, True, 571, True],
     [530, 749, True, 592, True],
     [507, 715, True, 533, False],
     [468, 703, False, 516, True],
     [457, 715, False, 563, True],
     [444, 708, False, 513, True]]],
   [[True, 30],
    [True, 30],
    [True, 30],
    [True, 50],
    [True, 70],
    [True, 90],
    [True, 0],
    [True, 30],
    [True, 30],
    [True, 30]]],
  ['control: scenario 2',
   [{'high_limit': 650, 'diff': 20, 'min_pos': 15, 'mat_low': 530},
    [[741, 731, False, 559, True],
     [755, 719, True, 563, False],
     [721, 727, False, 535, True],
     [760, 746, True, 518, False],
     [752, 759, True, 565, True],
     [721, 749, True, 610, True],
     [697, 734, True, 612, True],
     [689, 709, True, 487, True],
     [680, 737, True, 520, True],
     [641, 709, True, 611, True],
     [637, 721, True, 540, False],
     [603, 735, False, 541, True],
     [586, 719, True, 618, True],
     [618, 703, True, 613, True]]],
   [[False, 15],
    [False, 0],
    [False, 15],
    [False, 0],
    [False, 15],
    [False, 15],
    [False, 15],
    [False, 15],
    [False, 15],
    [False, 15],
    [False, 0],
    [True, 15],
    [True, 35],
    [True, 55]]]],
 [['regression: scenario 51',
   [{'high_limit': 750, 'diff': 40, 'min_pos': 20, 'mat_low': 500},
    [[690, 736, True, 548, True],
     [672, 704, False, 523, True],
     [705, 722, True, 586, True],
     [724, 748, True, 489, False],
     [757, 743, True, 569, True],
     [757, 733, True, 557, True],
     [721, 748, False, 595, True],
     [711, 726, False, 514, True]]],
   [[True, 40], [True, 20], [True, 40], [True, 0], [False, 20], [False, 20], [False, 20], [False, 20]]],
  ['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 750, 'diff': 0, 'min_pos': 20, 'mat_low': 500},
    [[684, 714, False, 579, True],
     [715, 719, False, 481, True],
     [737, 759, True, 615, False],
     [715, 756, True, 609, False],
     [743, 742, True, 491, True],
     [776, 713, False, 491, True],
     [798, 748, False, 566, True],
     [800, 751, True, 547, True],
     [800, 736, True, 486, True],
     [760, 727, False, 570, True]]],
   [[True, 20],
    [True, 20],
    [True, 0],
    [True, 0],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20]]],
  ['control: scenario 2',
   [{'high_limit': 650, 'diff': 40, 'min_pos': 30, 'mat_low': 500},
    [[786, 737, False, 497, True],
     [800, 759, True, 556, False],
     [785, 714, True, 618, False],
     [793, 713, True, 542, False],
     [780, 729, True, 610, True],
     [743, 742, True, 575, True],
     [717, 752, False, 602, True],
     [746, 757, True, 482, True]]],
   [[False, 30], [False, 0], [False, 0], [False, 0], [False, 30], [False, 30], [False, 30], [False, 30]]]]]
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: enable band edges[[False, 20], [True, 40], [True, 60], [True, 80], [False, 20]][[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]Failed
control: economizer cycle[[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]][[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]Passed
control: cold mixed air[[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]][[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]Passed
control: night cooling call[[True, 0], [True, 40]][[True, 0], [True, 40]]Passed
control: near return disable[[True, 35], [True, 55], [True, 75], [False, 15]][[True, 35], [True, 55], [True, 75], [False, 15]]Passed
control: scenario 1[[True, 40], [True, 30], [True, 50], [True, 0], [True, 40], [True, 60], [True, 20], [True, 20]][[True, 40], [True, 30], [True, 50], [True, 0], [True, 40], [True, 60], [True, 20], [True, 20]]Passed
control: scenario 2[[True, 40], [True, 20], [True, 40], [True, 20], [True, 20], [True, 40], [True, 20], [True, 40], [True, 20]][[True, 40], [True, 20], [True, 40], [True, 20], [True, 20], [True, 40], [True, 20], [True, 40], [True, 20]]Passed
control: scenario 3[[True, 30], [True, 50], [True, 70], [True, 0], [True, 30], [True, 30], [True, 50], [True, 0], [True, 50]][[True, 30], [True, 50], [True, 70], [True, 0], [True, 30], [True, 30], [True, 50], [True, 0], [True, 50]]Passed

SHA-256 / f0ebc143c6ab39592426cc5065c96c8661c6d1e824b74ab6c4f9485ae2b134c8

2 / The unsuccessful fix

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

N = 1
observations = []
def solve(settings, samples):
    enabled = False
    pos = 0
    out = []
    for outdoor, ret, cooling, mixed, occupied in samples:
        if enabled and (outdoor > settings['high_limit'] or outdoor >= ret):
            enabled = False
        elif not enabled and outdoor <= settings['high_limit'] - 20 and outdoor <= ret - settings['diff']:
            enabled = True
        if not occupied:
            pos = 0
        elif enabled and cooling:
            if mixed < settings['mat_low']:
                pos = max(settings['min_pos'], pos - 10)
            else:
                pos = min(100, max(settings['min_pos'], pos) + 20)
        else:
            pos = settings['min_pos']
        out.append([enabled, pos])
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 750, 'diff': 0, 'min_pos': 20, 'mat_low': 500},
    [[648, 701, True, 604, True],
     [640, 708, True, 484, True],
     [671, 716, True, 536, True],
     [649, 721, True, 616, False],
     [640, 749, True, 619, True],
     [651, 751, True, 599, True],
     [669, 718, False, 504, True],
     [638, 704, False, 542, True]]],
   [[True, 40], [True, 30], [True, 50], [True, 0], [True, 40], [True, 60], [True, 20], [True, 20]]],
  ['control: scenario 2',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[460, 757, True, 619, True],
     [462, 720, False, 618, True],
     [502, 734, True, 534, True],
     [488, 707, False, 516, True],
     [450, 735, False, 496, True],
     [472, 754, True, 606, True],
     [477, 741, False, 575, True],
     [452, 710, True, 531, True],
     [431, 759, False, 576, True]]],
   [[True, 40],
    [True, 20],
    [True, 40],
    [True, 20],
    [True, 20],
    [True, 40],
    [True, 20],
    [True, 40],
    [True, 20]]],
  ['control: scenario 3',
   [{'high_limit': 700, 'diff': 40, 'min_pos': 30, 'mat_low': 530},
    [[608, 734, True, 493, True],
     [609, 741, True, 531, True],
     [587, 758, True, 553, True],
     [561, 759, False, 498, False],
     [530, 741, False, 509, True],
     [544, 760, True, 493, True],
     [548, 751, True, 583, True],
     [512, 705, False, 540, False],
     [490, 711, True, 562, True]]],
   [[True, 30],
    [True, 50],
    [True, 70],
    [True, 0],
    [True, 30],
    [True, 30],
    [True, 50],
    [True, 0],
    [True, 50]]]],
 [['regression: scenario 3',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[715, 711, True, 506, True],
     [744, 713, True, 551, True],
     [732, 724, True, 568, True],
     [758, 725, False, 532, True],
     [795, 734, True, 552, True],
     [788, 742, True, 497, True],
     [760, 709, True, 515, False],
     [769, 759, False, 564, True],
     [795, 721, True, 546, True]]],
   [[False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 0],
    [False, 20],
    [False, 20]]],
  ['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 650, 'diff': 0, 'min_pos': 30, 'mat_low': 500},
    [[754, 754, False, 483, False],
     [716, 749, True, 499, True],
     [699, 701, False, 573, True],
     [694, 750, True, 534, False],
     [704, 716, False, 517, True],
     [708, 701, False, 567, True],
     [743, 735, True, 609, True],
     [775, 747, False, 554, True],
     [743, 731, False, 583, False],
     [764, 708, False, 507, True],
     [795, 743, False, 528, True],
     [770, 706, True, 501, True],
     [764, 752, False, 578, True]]],
   [[False, 0],
    [False, 30],
    [False, 30],
    [False, 0],
    [False, 30],
    [False, 30],
    [False, 30],
    [False, 30],
    [False, 0],
    [False, 30],
    [False, 30],
    [False, 30],
    [False, 30]]],
  ['control: scenario 2',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 30, 'mat_low': 500},
    [[492, 750, False, 518, True],
     [495, 710, True, 564, True],
     [507, 701, True, 603, True],
     [545, 705, False, 490, True],
     [529, 724, True, 489, False],
     [508, 721, True, 529, False],
     [482, 717, True, 619, True],
     [464, 753, False, 608, True],
     [478, 719, False, 559, False],
     [481, 730, False, 494, True],
     [511, 719, False, 613, True],
     [479, 743, False, 542, True],
     [477, 728, True, 597, True],
     [493, 756, False, 531, True]]],
   [[True, 30],
    [True, 50],
    [True, 70],
    [True, 30],
    [True, 0],
    [True, 0],
    [True, 50],
    [True, 30],
    [True, 0],
    [True, 30],
    [True, 30],
    [True, 30],
    [True, 50],
    [True, 30]]]],
 [['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 650, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[678, 721, True, 495, True],
     [646, 726, True, 616, True],
     [669, 714, True, 600, True],
     [630, 728, True, 542, True],
     [626, 751, True, 530, False],
     [638, 723, False, 607, True],
     [654, 706, True, 557, True],
     [625, 704, False, 618, True]]],
   [[False, 20], [False, 20], [False, 20], [True, 40], [True, 0], [True, 20], [False, 20], [True, 20]]],
  ['control: scenario 2',
   [{'high_limit': 650, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[599, 747, False, 570, True],
     [625, 701, False, 603, True],
     [623, 732, False, 565, False],
     [624, 737, True, 482, True],
     [637, 740, False, 485, True],
     [676, 702, True, 590, True],
     [678, 755, True, 522, True],
     [670, 733, True, 571, True],
     [684, 712, True, 616, False],
     [687, 736, True, 607, False],
     [712, 739, False, 524, True]]],
   [[True, 30],
    [True, 30],
    [True, 0],
    [True, 30],
    [True, 30],
    [False, 30],
    [False, 30],
    [False, 30],
    [False, 0],
    [False, 0],
    [False, 30]]],
  ['control: scenario 3',
   [{'high_limit': 650, 'diff': 0, 'min_pos': 20, 'mat_low': 500},
    [[761, 714, True, 573, False],
     [780, 758, False, 588, True],
     [761, 703, True, 548, True],
     [746, 756, True, 495, True],
     [714, 752, True, 488, True],
     [754, 748, True, 509, True],
     [719, 713, True, 561, True],
     [725, 724, False, 616, True],
     [720, 740, False, 513, False],
     [719, 725, True, 487, True],
     [756, 716, False, 534, True],
     [795, 715, True, 615, False]]],
   [[False, 0],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 0],
    [False, 20],
    [False, 20],
    [False, 0]]]],
 [['regression: scenario 25',
   [{'high_limit': 750, 'diff': 40, 'min_pos': 20, 'mat_low': 530},
    [[760, 736, True, 594, True],
     [745, 759, True, 518, False],
     [715, 703, True, 494, True],
     [724, 736, True, 521, True],
     [739, 728, True, 586, True],
     [739, 717, True, 609, True],
     [774, 731, True, 575, True],
     [755, 733, True, 581, False],
     [732, 715, True, 558, True],
     [712, 732, True, 614, True]]],
   [[False, 20],
    [False, 0],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 0],
    [False, 20],
    [False, 20]]],
  ['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 30, 'mat_low': 500},
    [[505, 738, False, 612, True],
     [535, 706, True, 498, True],
     [549, 734, False, 604, True],
     [558, 729, True, 521, True],
     [540, 724, True, 571, True],
     [530, 749, True, 592, True],
     [507, 715, True, 533, False],
     [468, 703, False, 516, True],
     [457, 715, False, 563, True],
     [444, 708, False, 513, True]]],
   [[True, 30],
    [True, 30],
    [True, 30],
    [True, 50],
    [True, 70],
    [True, 90],
    [True, 0],
    [True, 30],
    [True, 30],
    [True, 30]]],
  ['control: scenario 2',
   [{'high_limit': 650, 'diff': 20, 'min_pos': 15, 'mat_low': 530},
    [[741, 731, False, 559, True],
     [755, 719, True, 563, False],
     [721, 727, False, 535, True],
     [760, 746, True, 518, False],
     [752, 759, True, 565, True],
     [721, 749, True, 610, True],
     [697, 734, True, 612, True],
     [689, 709, True, 487, True],
     [680, 737, True, 520, True],
     [641, 709, True, 611, True],
     [637, 721, True, 540, False],
     [603, 735, False, 541, True],
     [586, 719, True, 618, True],
     [618, 703, True, 613, True]]],
   [[False, 15],
    [False, 0],
    [False, 15],
    [False, 0],
    [False, 15],
    [False, 15],
    [False, 15],
    [False, 15],
    [False, 15],
    [False, 15],
    [False, 0],
    [True, 15],
    [True, 35],
    [True, 55]]]],
 [['regression: scenario 51',
   [{'high_limit': 750, 'diff': 40, 'min_pos': 20, 'mat_low': 500},
    [[690, 736, True, 548, True],
     [672, 704, False, 523, True],
     [705, 722, True, 586, True],
     [724, 748, True, 489, False],
     [757, 743, True, 569, True],
     [757, 733, True, 557, True],
     [721, 748, False, 595, True],
     [711, 726, False, 514, True]]],
   [[True, 40], [True, 20], [True, 40], [True, 0], [False, 20], [False, 20], [False, 20], [False, 20]]],
  ['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 750, 'diff': 0, 'min_pos': 20, 'mat_low': 500},
    [[684, 714, False, 579, True],
     [715, 719, False, 481, True],
     [737, 759, True, 615, False],
     [715, 756, True, 609, False],
     [743, 742, True, 491, True],
     [776, 713, False, 491, True],
     [798, 748, False, 566, True],
     [800, 751, True, 547, True],
     [800, 736, True, 486, True],
     [760, 727, False, 570, True]]],
   [[True, 20],
    [True, 20],
    [True, 0],
    [True, 0],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20]]],
  ['control: scenario 2',
   [{'high_limit': 650, 'diff': 40, 'min_pos': 30, 'mat_low': 500},
    [[786, 737, False, 497, True],
     [800, 759, True, 556, False],
     [785, 714, True, 618, False],
     [793, 713, True, 542, False],
     [780, 729, True, 610, True],
     [743, 742, True, 575, True],
     [717, 752, False, 602, True],
     [746, 757, True, 482, True]]],
   [[False, 30], [False, 0], [False, 0], [False, 0], [False, 30], [False, 30], [False, 30], [False, 30]]]]]
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: enable band edges[[False, 20], [True, 40], [True, 60], [True, 80], [False, 20]][[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]Failed
control: economizer cycle[[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]][[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]Passed
control: cold mixed air[[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]][[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]Passed
control: night cooling call[[True, 0], [True, 40]][[True, 0], [True, 40]]Passed
control: near return disable[[True, 35], [True, 55], [True, 75], [False, 15]][[True, 35], [True, 55], [True, 75], [False, 15]]Passed
control: scenario 1[[True, 40], [True, 30], [True, 50], [True, 0], [True, 40], [True, 60], [True, 20], [True, 20]][[True, 40], [True, 30], [True, 50], [True, 0], [True, 40], [True, 60], [True, 20], [True, 20]]Passed
control: scenario 2[[True, 40], [True, 20], [True, 40], [True, 20], [True, 20], [True, 40], [True, 20], [True, 40], [True, 20]][[True, 40], [True, 20], [True, 40], [True, 20], [True, 20], [True, 40], [True, 20], [True, 40], [True, 20]]Passed
control: scenario 3[[True, 30], [True, 50], [True, 70], [True, 0], [True, 30], [True, 30], [True, 50], [True, 0], [True, 50]][[True, 30], [True, 50], [True, 70], [True, 0], [True, 30], [True, 30], [True, 50], [True, 0], [True, 50]]Passed

SHA-256 / 10338bc9d0c212717f4f29b275016f142fb8d47f240c50fb42a4a6de005d8f81

3 / The verified repair

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

N = 1
observations = []
def solve(settings, samples):
    enabled = False
    pos = 0
    out = []
    for outdoor, ret, cooling, mixed, occupied in samples:
        if enabled and (outdoor > settings['high_limit'] or outdoor >= ret):
            enabled = False
        elif not enabled and outdoor <= settings['high_limit'] - 20 and outdoor < ret - settings['diff']:
            enabled = True
        if not occupied:
            pos = 0
        elif enabled and cooling:
            if mixed < settings['mat_low']:
                pos = max(settings['min_pos'], pos - 10)
            else:
                pos = min(100, max(settings['min_pos'], pos) + 20)
        else:
            pos = settings['min_pos']
        out.append([enabled, pos])
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 750, 'diff': 0, 'min_pos': 20, 'mat_low': 500},
    [[648, 701, True, 604, True],
     [640, 708, True, 484, True],
     [671, 716, True, 536, True],
     [649, 721, True, 616, False],
     [640, 749, True, 619, True],
     [651, 751, True, 599, True],
     [669, 718, False, 504, True],
     [638, 704, False, 542, True]]],
   [[True, 40], [True, 30], [True, 50], [True, 0], [True, 40], [True, 60], [True, 20], [True, 20]]],
  ['control: scenario 2',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[460, 757, True, 619, True],
     [462, 720, False, 618, True],
     [502, 734, True, 534, True],
     [488, 707, False, 516, True],
     [450, 735, False, 496, True],
     [472, 754, True, 606, True],
     [477, 741, False, 575, True],
     [452, 710, True, 531, True],
     [431, 759, False, 576, True]]],
   [[True, 40],
    [True, 20],
    [True, 40],
    [True, 20],
    [True, 20],
    [True, 40],
    [True, 20],
    [True, 40],
    [True, 20]]],
  ['control: scenario 3',
   [{'high_limit': 700, 'diff': 40, 'min_pos': 30, 'mat_low': 530},
    [[608, 734, True, 493, True],
     [609, 741, True, 531, True],
     [587, 758, True, 553, True],
     [561, 759, False, 498, False],
     [530, 741, False, 509, True],
     [544, 760, True, 493, True],
     [548, 751, True, 583, True],
     [512, 705, False, 540, False],
     [490, 711, True, 562, True]]],
   [[True, 30],
    [True, 50],
    [True, 70],
    [True, 0],
    [True, 30],
    [True, 30],
    [True, 50],
    [True, 0],
    [True, 50]]]],
 [['regression: scenario 3',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[715, 711, True, 506, True],
     [744, 713, True, 551, True],
     [732, 724, True, 568, True],
     [758, 725, False, 532, True],
     [795, 734, True, 552, True],
     [788, 742, True, 497, True],
     [760, 709, True, 515, False],
     [769, 759, False, 564, True],
     [795, 721, True, 546, True]]],
   [[False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 0],
    [False, 20],
    [False, 20]]],
  ['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 650, 'diff': 0, 'min_pos': 30, 'mat_low': 500},
    [[754, 754, False, 483, False],
     [716, 749, True, 499, True],
     [699, 701, False, 573, True],
     [694, 750, True, 534, False],
     [704, 716, False, 517, True],
     [708, 701, False, 567, True],
     [743, 735, True, 609, True],
     [775, 747, False, 554, True],
     [743, 731, False, 583, False],
     [764, 708, False, 507, True],
     [795, 743, False, 528, True],
     [770, 706, True, 501, True],
     [764, 752, False, 578, True]]],
   [[False, 0],
    [False, 30],
    [False, 30],
    [False, 0],
    [False, 30],
    [False, 30],
    [False, 30],
    [False, 30],
    [False, 0],
    [False, 30],
    [False, 30],
    [False, 30],
    [False, 30]]],
  ['control: scenario 2',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 30, 'mat_low': 500},
    [[492, 750, False, 518, True],
     [495, 710, True, 564, True],
     [507, 701, True, 603, True],
     [545, 705, False, 490, True],
     [529, 724, True, 489, False],
     [508, 721, True, 529, False],
     [482, 717, True, 619, True],
     [464, 753, False, 608, True],
     [478, 719, False, 559, False],
     [481, 730, False, 494, True],
     [511, 719, False, 613, True],
     [479, 743, False, 542, True],
     [477, 728, True, 597, True],
     [493, 756, False, 531, True]]],
   [[True, 30],
    [True, 50],
    [True, 70],
    [True, 30],
    [True, 0],
    [True, 0],
    [True, 50],
    [True, 30],
    [True, 0],
    [True, 30],
    [True, 30],
    [True, 30],
    [True, 50],
    [True, 30]]]],
 [['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 650, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[678, 721, True, 495, True],
     [646, 726, True, 616, True],
     [669, 714, True, 600, True],
     [630, 728, True, 542, True],
     [626, 751, True, 530, False],
     [638, 723, False, 607, True],
     [654, 706, True, 557, True],
     [625, 704, False, 618, True]]],
   [[False, 20], [False, 20], [False, 20], [True, 40], [True, 0], [True, 20], [False, 20], [True, 20]]],
  ['control: scenario 2',
   [{'high_limit': 650, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[599, 747, False, 570, True],
     [625, 701, False, 603, True],
     [623, 732, False, 565, False],
     [624, 737, True, 482, True],
     [637, 740, False, 485, True],
     [676, 702, True, 590, True],
     [678, 755, True, 522, True],
     [670, 733, True, 571, True],
     [684, 712, True, 616, False],
     [687, 736, True, 607, False],
     [712, 739, False, 524, True]]],
   [[True, 30],
    [True, 30],
    [True, 0],
    [True, 30],
    [True, 30],
    [False, 30],
    [False, 30],
    [False, 30],
    [False, 0],
    [False, 0],
    [False, 30]]],
  ['control: scenario 3',
   [{'high_limit': 650, 'diff': 0, 'min_pos': 20, 'mat_low': 500},
    [[761, 714, True, 573, False],
     [780, 758, False, 588, True],
     [761, 703, True, 548, True],
     [746, 756, True, 495, True],
     [714, 752, True, 488, True],
     [754, 748, True, 509, True],
     [719, 713, True, 561, True],
     [725, 724, False, 616, True],
     [720, 740, False, 513, False],
     [719, 725, True, 487, True],
     [756, 716, False, 534, True],
     [795, 715, True, 615, False]]],
   [[False, 0],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 0],
    [False, 20],
    [False, 20],
    [False, 0]]]],
 [['regression: scenario 25',
   [{'high_limit': 750, 'diff': 40, 'min_pos': 20, 'mat_low': 530},
    [[760, 736, True, 594, True],
     [745, 759, True, 518, False],
     [715, 703, True, 494, True],
     [724, 736, True, 521, True],
     [739, 728, True, 586, True],
     [739, 717, True, 609, True],
     [774, 731, True, 575, True],
     [755, 733, True, 581, False],
     [732, 715, True, 558, True],
     [712, 732, True, 614, True]]],
   [[False, 20],
    [False, 0],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 0],
    [False, 20],
    [False, 20]]],
  ['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 30, 'mat_low': 500},
    [[505, 738, False, 612, True],
     [535, 706, True, 498, True],
     [549, 734, False, 604, True],
     [558, 729, True, 521, True],
     [540, 724, True, 571, True],
     [530, 749, True, 592, True],
     [507, 715, True, 533, False],
     [468, 703, False, 516, True],
     [457, 715, False, 563, True],
     [444, 708, False, 513, True]]],
   [[True, 30],
    [True, 30],
    [True, 30],
    [True, 50],
    [True, 70],
    [True, 90],
    [True, 0],
    [True, 30],
    [True, 30],
    [True, 30]]],
  ['control: scenario 2',
   [{'high_limit': 650, 'diff': 20, 'min_pos': 15, 'mat_low': 530},
    [[741, 731, False, 559, True],
     [755, 719, True, 563, False],
     [721, 727, False, 535, True],
     [760, 746, True, 518, False],
     [752, 759, True, 565, True],
     [721, 749, True, 610, True],
     [697, 734, True, 612, True],
     [689, 709, True, 487, True],
     [680, 737, True, 520, True],
     [641, 709, True, 611, True],
     [637, 721, True, 540, False],
     [603, 735, False, 541, True],
     [586, 719, True, 618, True],
     [618, 703, True, 613, True]]],
   [[False, 15],
    [False, 0],
    [False, 15],
    [False, 0],
    [False, 15],
    [False, 15],
    [False, 15],
    [False, 15],
    [False, 15],
    [False, 15],
    [False, 0],
    [True, 15],
    [True, 35],
    [True, 55]]]],
 [['regression: scenario 51',
   [{'high_limit': 750, 'diff': 40, 'min_pos': 20, 'mat_low': 500},
    [[690, 736, True, 548, True],
     [672, 704, False, 523, True],
     [705, 722, True, 586, True],
     [724, 748, True, 489, False],
     [757, 743, True, 569, True],
     [757, 733, True, 557, True],
     [721, 748, False, 595, True],
     [711, 726, False, 514, True]]],
   [[True, 40], [True, 20], [True, 40], [True, 0], [False, 20], [False, 20], [False, 20], [False, 20]]],
  ['regression: enable band edges',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[681, 740, True, 560, True],
     [680, 700, True, 560, True],
     [680, 701, True, 560, True],
     [700, 750, True, 560, True],
     [710, 750, True, 560, True]]],
   [[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]],
  ['control: economizer cycle',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[600, 740, True, 560, True],
     [600, 740, True, 560, True],
     [600, 740, True, 520, True],
     [690, 740, True, 560, True],
     [705, 740, True, 560, True],
     [600, 740, False, 560, True],
     [600, 740, True, 560, False]]],
   [[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]],
  ['control: cold mixed air',
   [{'high_limit': 700, 'diff': 0, 'min_pos': 30, 'mat_low': 530},
    [[450, 720, True, 600, True],
     [450, 720, True, 600, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True],
     [450, 720, True, 500, True]]],
   [[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]],
  ['control: night cooling call',
   [{'high_limit': 700, 'diff': 20, 'min_pos': 20, 'mat_low': 530},
    [[550, 740, True, 560, False], [550, 740, True, 560, True]]],
   [[True, 0], [True, 40]]],
  ['control: near return disable',
   [{'high_limit': 750, 'diff': 20, 'min_pos': 15, 'mat_low': 500},
    [[700, 730, True, 560, True],
     [715, 730, True, 560, True],
     [725, 730, True, 560, True],
     [730, 730, True, 560, True]]],
   [[True, 35], [True, 55], [True, 75], [False, 15]]],
  ['control: scenario 1',
   [{'high_limit': 750, 'diff': 0, 'min_pos': 20, 'mat_low': 500},
    [[684, 714, False, 579, True],
     [715, 719, False, 481, True],
     [737, 759, True, 615, False],
     [715, 756, True, 609, False],
     [743, 742, True, 491, True],
     [776, 713, False, 491, True],
     [798, 748, False, 566, True],
     [800, 751, True, 547, True],
     [800, 736, True, 486, True],
     [760, 727, False, 570, True]]],
   [[True, 20],
    [True, 20],
    [True, 0],
    [True, 0],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20],
    [False, 20]]],
  ['control: scenario 2',
   [{'high_limit': 650, 'diff': 40, 'min_pos': 30, 'mat_low': 500},
    [[786, 737, False, 497, True],
     [800, 759, True, 556, False],
     [785, 714, True, 618, False],
     [793, 713, True, 542, False],
     [780, 729, True, 610, True],
     [743, 742, True, 575, True],
     [717, 752, False, 602, True],
     [746, 757, True, 482, True]]],
   [[False, 30], [False, 0], [False, 0], [False, 0], [False, 30], [False, 30], [False, 30], [False, 30]]]]]
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: enable band edges[[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]][[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]Passed
control: economizer cycle[[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]][[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]Passed
control: cold mixed air[[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]][[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]Passed
control: night cooling call[[True, 0], [True, 40]][[True, 0], [True, 40]]Passed
control: near return disable[[True, 35], [True, 55], [True, 75], [False, 15]][[True, 35], [True, 55], [True, 75], [False, 15]]Passed
control: scenario 1[[True, 40], [True, 30], [True, 50], [True, 0], [True, 40], [True, 60], [True, 20], [True, 20]][[True, 40], [True, 30], [True, 50], [True, 0], [True, 40], [True, 60], [True, 20], [True, 20]]Passed
control: scenario 2[[True, 40], [True, 20], [True, 40], [True, 20], [True, 20], [True, 40], [True, 20], [True, 40], [True, 20]][[True, 40], [True, 20], [True, 40], [True, 20], [True, 20], [True, 40], [True, 20], [True, 40], [True, 20]]Passed
control: scenario 3[[True, 30], [True, 50], [True, 70], [True, 0], [True, 30], [True, 30], [True, 50], [True, 0], [True, 50]][[True, 30], [True, 50], [True, 70], [True, 0], [True, 30], [True, 30], [True, 50], [True, 0], [True, 50]]Passed

SHA-256 / ad09160ade292c18e28a583c0427ba301fe8dd9d867793a6d0496a529fac87b4

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

Case digest / daec5ae3d23bff9c3697b084a82811cc41442f37ebd7fd0934515f0e91d34dad