FAILURE MAP
← Case archive

FA-92551 / HVAC thermostat control / Open access

Economizer ramp starts from the closed position · case 01

After occupancy begins the damper opens to less than minimum ventilation on the first economizer step.

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

ROOT CAUSE

The ramp adds 20% to the current position without first raising it to min_pos.

VERIFIED REPAIR

Ramp up from at least min_pos.

Unsuccessful approach: Clamping after the addition still takes one step less than the contract.

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, 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: 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]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['control: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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: 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]]],
  ['regression: 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: 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: 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]]],
  ['regression: scenario 3',
   [{'high_limit': 700, 'diff': 40, 'min_pos': 15, 'mat_low': 500},
    [[648, 702, True, 512, True],
     [638, 725, True, 584, True],
     [603, 726, True, 591, True],
     [610, 705, True, 566, False],
     [612, 706, False, 595, True],
     [587, 717, True, 552, True],
     [623, 754, True, 482, True],
     [595, 710, True, 491, True],
     [615, 710, False, 485, True]]],
   [[True, 35],
    [True, 55],
    [True, 75],
    [True, 0],
    [True, 15],
    [True, 35],
    [True, 25],
    [True, 15],
    [True, 15]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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: 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 6',
   [{'high_limit': 650, 'diff': 40, 'min_pos': 30, 'mat_low': 530},
    [[450, 740, True, 605, True],
     [424, 725, True, 523, False],
     [400, 741, True, 543, True],
     [409, 711, True, 502, True],
     [439, 732, False, 568, True],
     [438, 760, True, 580, False],
     [467, 735, True, 574, False],
     [492, 716, False, 501, False],
     [529, 729, True, 518, True]]],
   [[True, 50], [True, 0], [True, 50], [True, 40], [True, 30], [True, 0], [True, 0], [True, 0], [True, 30]]],
  ['regression: scenario 10',
   [{'high_limit': 750, 'diff': 40, 'min_pos': 15, 'mat_low': 500},
    [[545, 708, True, 597, True],
     [541, 746, False, 504, True],
     [502, 702, True, 493, True],
     [512, 727, True, 496, True],
     [509, 707, False, 598, True],
     [482, 711, True, 599, True],
     [489, 748, False, 608, True],
     [500, 716, True, 502, True],
     [477, 718, True, 516, False]]],
   [[True, 35],
    [True, 15],
    [True, 15],
    [True, 15],
    [True, 15],
    [True, 35],
    [True, 15],
    [True, 35],
    [True, 0]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['regression: 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]]]]]
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: economizer cycle[[True, 20], [True, 40], [True, 30], [True, 50], [False, 20], [True, 20], [True, 0]][[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]Failed
regression: cold mixed air[[True, 20], [True, 40], [True, 30], [True, 30], [True, 30], [True, 30]][[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]Failed
control: enable band edges[[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]][[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]Passed
regression: night cooling call[[True, 0], [True, 20]][[True, 0], [True, 40]]Failed
regression: near return disable[[True, 20], [True, 40], [True, 60], [False, 15]][[True, 35], [True, 55], [True, 75], [False, 15]]Failed
regression: scenario 1[[True, 20], [True, 20], [True, 40], [True, 0], [True, 20], [True, 40], [True, 20], [True, 20]][[True, 40], [True, 30], [True, 50], [True, 0], [True, 40], [True, 60], [True, 20], [True, 20]]Failed
regression: scenario 2[[True, 20], [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]]Failed
regression: scenario 3[[True, 30], [True, 50], [True, 70], [True, 0], [True, 30], [True, 30], [True, 50], [True, 0], [True, 20]][[True, 30], [True, 50], [True, 70], [True, 0], [True, 30], [True, 30], [True, 50], [True, 0], [True, 50]]Failed

SHA-256 / ec6ac20175d864efbcb26169578ceb72fc631bbedabd934a837b195da018a5a8

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: 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]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['control: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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: 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]]],
  ['regression: 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: 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: 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]]],
  ['regression: scenario 3',
   [{'high_limit': 700, 'diff': 40, 'min_pos': 15, 'mat_low': 500},
    [[648, 702, True, 512, True],
     [638, 725, True, 584, True],
     [603, 726, True, 591, True],
     [610, 705, True, 566, False],
     [612, 706, False, 595, True],
     [587, 717, True, 552, True],
     [623, 754, True, 482, True],
     [595, 710, True, 491, True],
     [615, 710, False, 485, True]]],
   [[True, 35],
    [True, 55],
    [True, 75],
    [True, 0],
    [True, 15],
    [True, 35],
    [True, 25],
    [True, 15],
    [True, 15]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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: 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 6',
   [{'high_limit': 650, 'diff': 40, 'min_pos': 30, 'mat_low': 530},
    [[450, 740, True, 605, True],
     [424, 725, True, 523, False],
     [400, 741, True, 543, True],
     [409, 711, True, 502, True],
     [439, 732, False, 568, True],
     [438, 760, True, 580, False],
     [467, 735, True, 574, False],
     [492, 716, False, 501, False],
     [529, 729, True, 518, True]]],
   [[True, 50], [True, 0], [True, 50], [True, 40], [True, 30], [True, 0], [True, 0], [True, 0], [True, 30]]],
  ['regression: scenario 10',
   [{'high_limit': 750, 'diff': 40, 'min_pos': 15, 'mat_low': 500},
    [[545, 708, True, 597, True],
     [541, 746, False, 504, True],
     [502, 702, True, 493, True],
     [512, 727, True, 496, True],
     [509, 707, False, 598, True],
     [482, 711, True, 599, True],
     [489, 748, False, 608, True],
     [500, 716, True, 502, True],
     [477, 718, True, 516, False]]],
   [[True, 35],
    [True, 15],
    [True, 15],
    [True, 15],
    [True, 15],
    [True, 35],
    [True, 15],
    [True, 35],
    [True, 0]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['regression: 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]]]]]
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: economizer cycle[[True, 20], [True, 40], [True, 30], [True, 50], [False, 20], [True, 20], [True, 0]][[True, 40], [True, 60], [True, 50], [True, 70], [False, 20], [True, 20], [True, 0]]Failed
regression: cold mixed air[[True, 30], [True, 50], [True, 40], [True, 30], [True, 30], [True, 30]][[True, 50], [True, 70], [True, 60], [True, 50], [True, 40], [True, 30]]Failed
control: enable band edges[[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]][[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]Passed
regression: night cooling call[[True, 0], [True, 20]][[True, 0], [True, 40]]Failed
regression: near return disable[[True, 20], [True, 40], [True, 60], [False, 15]][[True, 35], [True, 55], [True, 75], [False, 15]]Failed
regression: scenario 1[[True, 20], [True, 20], [True, 40], [True, 0], [True, 20], [True, 40], [True, 20], [True, 20]][[True, 40], [True, 30], [True, 50], [True, 0], [True, 40], [True, 60], [True, 20], [True, 20]]Failed
regression: scenario 2[[True, 20], [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]]Failed
regression: scenario 3[[True, 30], [True, 50], [True, 70], [True, 0], [True, 30], [True, 30], [True, 50], [True, 0], [True, 30]][[True, 30], [True, 50], [True, 70], [True, 0], [True, 30], [True, 30], [True, 50], [True, 0], [True, 50]]Failed

SHA-256 / dcdfefee4d8dbbf2d569b75ded8589d713a97ed7dc543220891d95a90e7ad469

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: 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]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['control: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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: 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]]],
  ['regression: 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: 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: 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]]],
  ['regression: scenario 3',
   [{'high_limit': 700, 'diff': 40, 'min_pos': 15, 'mat_low': 500},
    [[648, 702, True, 512, True],
     [638, 725, True, 584, True],
     [603, 726, True, 591, True],
     [610, 705, True, 566, False],
     [612, 706, False, 595, True],
     [587, 717, True, 552, True],
     [623, 754, True, 482, True],
     [595, 710, True, 491, True],
     [615, 710, False, 485, True]]],
   [[True, 35],
    [True, 55],
    [True, 75],
    [True, 0],
    [True, 15],
    [True, 35],
    [True, 25],
    [True, 15],
    [True, 15]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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: 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 6',
   [{'high_limit': 650, 'diff': 40, 'min_pos': 30, 'mat_low': 530},
    [[450, 740, True, 605, True],
     [424, 725, True, 523, False],
     [400, 741, True, 543, True],
     [409, 711, True, 502, True],
     [439, 732, False, 568, True],
     [438, 760, True, 580, False],
     [467, 735, True, 574, False],
     [492, 716, False, 501, False],
     [529, 729, True, 518, True]]],
   [[True, 50], [True, 0], [True, 50], [True, 40], [True, 30], [True, 0], [True, 0], [True, 0], [True, 30]]],
  ['regression: scenario 10',
   [{'high_limit': 750, 'diff': 40, 'min_pos': 15, 'mat_low': 500},
    [[545, 708, True, 597, True],
     [541, 746, False, 504, True],
     [502, 702, True, 493, True],
     [512, 727, True, 496, True],
     [509, 707, False, 598, True],
     [482, 711, True, 599, True],
     [489, 748, False, 608, True],
     [500, 716, True, 502, True],
     [477, 718, True, 516, False]]],
   [[True, 35],
    [True, 15],
    [True, 15],
    [True, 15],
    [True, 15],
    [True, 35],
    [True, 15],
    [True, 35],
    [True, 0]]],
  ['regression: 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: 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]]],
  ['regression: 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]]],
  ['regression: 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]]],
  ['regression: 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]]]]]
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: 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
regression: 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: enable band edges[[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]][[False, 20], [False, 20], [True, 40], [True, 60], [False, 20]]Passed
regression: night cooling call[[True, 0], [True, 40]][[True, 0], [True, 40]]Passed
regression: near return disable[[True, 35], [True, 55], [True, 75], [False, 15]][[True, 35], [True, 55], [True, 75], [False, 15]]Passed
regression: 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
regression: 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
regression: 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 / 60bd842c3a6bcca0f0e172a5f50aebeaca3786590665da1e67af7ffde10779b4

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

Case digest / 601390b3f57a285bcecc1a8a4003ba9656cc9ac29534961a49529c6c1711f3cf