Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

VAV-0009 — Reheat coil leakage CUSUM

Statusverified — engine e2ff2f8, cxf:fnv1a128:b1d17eced97fea52d58a97a364eee7bf, 2026-08-18
Severity3
Methodstatistical
Phase3
CategoryCRITICAL_WASTE
ConfidenceMEDIUM
EstimationPROXY_ESTIMATION
G36
Clusters
Suppresses
Suppressed by
RelatedVAV-0003, VAV-0007, VAV-0008, FCU-0002, FPB-0003
Playbooksvav-min-flow-reheat
SourceBushby, Castro, Schein, House (2001), NIST/CEC PIER Project 2.3, §5.1.1 — the normalized statistic (eq. 1) and the two-sided CUSUM recursion (eqs. 2-3); Bushby, Castro, Schein, House (2001), NIST/CEC PIER Project 2.3, §5.1.3 — dTerror = discharge minus supply temperature, computed only while the reheat coil is commanded off; the leaking valve/element attribution; Bushby, Castro, Schein, House (2001), NIST/CEC PIER Project 2.3, §5.1.4 — point requirements, and the entering-air ≈ supply-air workaround this card adopts where the box has no entering-air sensor; Bushby, Castro, Schein, House (2001), NIST/CEC PIER Project 2.3, §5.1.5 — slack from normal-operation data, alarm limits from fault-injection data, collected per VAV box type; Bushby, Castro, Schein, House (2001), NIST/CEC PIER Project 2.3, §5.2 — Iowa Energy Center validation: the stuck/leaking-valve studies measured discharge rises of 3.7-11.9 °F, and Table 5 reports k = 3 with the campaign’s alarm-limit ranges; Harness calibration (committed method): tools/simharness/harness.py vavcal mode and tools/simharness/README.md — healthy reheat-off dTerror measured across the B2B OfficeMedium-4004 zones, and the supply-broadcast approximation bias validated below 0.005 °C; Library-authored: the HVAC FDD Reference v1.0 publishes no CUSUM card, so name, severity, category and the shipped defaults are argued on this card; Sibling precedent: VAV-0007/VAV-0008 (this batch’s recursion and reset topology), SYS-0006 (sub-condition flags), VAV-0003 (the valve seen from the command side); Engine pin e2ff2f8: crates/oce-blocks/src/discrete.rs (UnitDelay sample grid, loop-cut contract), crates/oce-graph/src/topo.rs (the emit-before sort that admits the feedback path)
Operating statesoccupied with the reheat coil commanded off, both gated in-graph. While the schedule is unoccupied, for exclusion_time after it goes occupied, or while rht_vlv_cmd is at/above reheat_closed_threshold, the accumulators are forced to zero. yOccupiedOk and yReheatOk publish the two gates separately.

Preconditions (host-enforced): Five host obligations. (1) This is the one VPACC channel that needs a discharge-air sensor: many-but-not-all boxes have one (points/vav.points.json vav_dat), and a box without it runs the source’s reduced two-channel VPACC (VAV-0007/VAV-0008) rather than a substitute signal. (2) sat is the serving AHU’s supply-air temperature broadcast to the box, standing in for entering-air per §5.1.4’s own workaround — bind the loop actually serving this box; the committed harness method measured the approximation bias below 0.005 °C in simulation, but a long or leaky duct run raises the healthy baseline, which is what error_mean absorbs and why it is commissioned per box. (3) rht_vlv_cmd is the COMMAND, not position feedback — a leak with the valve commanded open is invisible here by construction and belongs to VAV-0003. (4) Tick on the sample_period grid; both unit delays advance on that clock. (5) error_mean, error_sigma, slack_k and alarm_limit_h are per-box-type commissioning values (§5.1.5); the shipped set is one simulated medium-office box plus the source’s Iowa campaign. yOccupiedOk and yReheatOk are evaluability flags: either false means NO_EVAL, not healthy.

Points: vav_dat, sat, rht_vlv_cmd, occ_scheduled

Outputs:

  • yFault — True while either cumulative sum is strictly above alarm_limit_h — the discharge has run away from the supply temperature, with the coil commanded shut, for long enough that the accumulated normalized error passed the limit
  • yHigh — Sub-condition flag, undelayed — the S (upper) chart is above the limit: heat is being added across a coil commanded off, the leak direction. Not an evaluability output; a false yHigh never means NO_EVAL
  • yLow — Sub-condition flag, undelayed — the T (lower) chart is above the limit: the discharge reads persistently BELOW the supply broadcast, which no leak can produce. An instrumentation finding (wrong loop bound, sensor swap or drift), not a coil finding
  • yOccupiedOk — Evaluability flag — true only while the schedule is occupied AND exclusion_time has elapsed since it went occupied. FALSE MEANS NO_EVAL: the accumulators are held at zero by construction
  • yReheatOk — Evaluability flag — true only while rht_vlv_cmd is strictly below reheat_closed_threshold. FALSE MEANS NO_EVAL: with the coil legitimately commanded open, a discharge rise is the coil doing its job and the accumulators are held at zero

Parameters:

NameDefaultUnitCXF pathDescription
error_mean0.44°CmeanC.kExpected reheat-off rise, eq. 1’s x-bar — fan heat and duct gain make a small positive dT normal, so zero would be wrong here. 0.44 °C is the committed harness measurement (vavcal, B2B OfficeMedium-4004, occupied reheat-off ticks); commission it per box, since fan type, duct run and box size all move it.
error_sigma0.39°CsigmaC.kNormalization denominator, eq. 1’s sigma-hat. Unlike VAV-0008’s floor, this channel has a real healthy spread to measure and 0.39 °C is the same harness measurement — but simulation noise is cleaner than real sensors, so treat it as a starting point, not a statistic about your box.
slack_k3.01 (standard deviations per sample)kC.kSlack. The normalized error must exceed k before anything accumulates. 3.0 is what Table 5 reports for the source’s Iowa Energy Center campaign — measured, unlike the Figure-9 illustration the sibling cards ship — and still a commissioning parameter per §5.1.5. One constant feeds both charts.
alarm_limit_h100.01 (accumulated standard deviations)sHigh.t, tHigh.tAlarm limit, sitting inside Table 5’s published range for the campaign. At the shipped defaults a 5 °C leak rise adds about 8.7 per minute and alarms in roughly 12 minutes; the drain back through the limit after repair takes as long as the slack allows. One parameter binds both charts; split sHigh.t and tHigh.t deliberately if a site wants the source’s per-chart limits.
reheat_closed_threshold1.0%rhtOff.tThe command level below which the coil counts as off and the channel is armed (strict less-than). Set it under the controller’s minimum crack; a box that never commands fully closed never evaluates this rule, and yReheatOk says so.
sample_period60.0ssPrev.samplePeriod, tPrev.samplePeriodThe CUSUM clock — both unit delays advance on this grid, one increment per period. SET IT TO THE HOST’S TICK INTERVAL and set both paths together. 60 s matches the source’s own 1-minute data (§5.2).
exclusion_time3600.0soccGate.delayTimeHow long after each occupied-period start the accumulators stay held at zero — the source’s first hour (§5.1.3), taken literally. delayOnInit = true also covers a controller restart.

Description

A reheat valve that will not quite close is the quietest fault a VAV box can have: the zone stays comfortable, the damper compensates with more cold primary air, and the only witness is a discharge temperature a degree or two above the supply air feeding the box. That rise is real heat, paid for twice — once at the boiler and again at the chiller that removes it. No single reading is worth an alarm, because fan heat and duct gain put a small legitimate rise across every box; what convicts the coil is a rise persistently above that baseline, every minute, with the valve commanded shut. A cumulative sum chart is built for exactly that shape of evidence.

Detection Logic

dTerror  = vav_dat − sat                       rise across the box, °C

z_i      = (dTerror_i − error_mean) / error_sigma

armed_i  = occ_scheduled held true for exclusion_time
           AND rht_vlv_cmd < reheat_closed_threshold

S_i      = armed_i ? max(0,  z_i − slack_k + S_{i−1}) : 0
T_i      = armed_i ? max(0, −z_i − slack_k + T_{i−1}) : 0

yHigh    = S_i > alarm_limit_h        yLow = T_i > alarm_limit_h
yFault   = yHigh OR yLow

Block graph (rule.cxf.jsonld):

VAV-0009 block graph

The recursion, the occupancy arming and the feedback-through-UnitDelay topology are VAV-0008’s, documented there; this card adds one gate. A single Logical.And of the occupancy arm and the reheat-off comparison drives both reset switches, so the accumulators hold state only while the coil is commanded off — the one condition under which a discharge rise means anything. When the coil is commanded open, the state is cleared, not frozen (see Deviations). Both threshold comparisons are strict, and there is no alarm delay: the accumulation is the persistence test.

Possible Diagnoses

§5.1.3 attributes this channel to a leaking valve or element; the gate and the direction flags narrow it further.

  1. Reheat valve passing — worn seat, debris, or actuator not driving fully closed. The classic yHigh finding: rise within the source’s measured 3.7–11.9 °F leak band, coil commanded shut. Verify at the coil: pipe surface temperature downstream of the valve tells the truth in one visit.
  2. Electric reheat element held on — welded contactor or failed SCR, the electric-box equivalent of a passing valve, same signature and a bigger safety question. FCU-0002 is the fan-coil sibling.
  3. Valve driven open by override or bad sequence — the command reads closed at the BAS but a local override or miswired output holds it open. The rule cannot tell this from a leak; the work order can.
  4. Instrumentation, not the coil (yLow, or a yHigh that survives valve isolation) — wrong AHU loop bound as sat, discharge sensor drifted or swapped with another box. A persistent negative rise is physically impossible from a leak, which is why the T chart’s finding is named as instrumentation rather than folded into the coil story.

Energy Impact

CRITICAL_WASTE, MEDIUM confidence, PROXY_ESTIMATION. Leak heat is bought at the plant and then removed by cooling the same air back down — the same double-payment as VAV-0003, seen from the temperature side instead of the command side. The rule deliberately consumes no airflow point, so it prices nothing in-rule; the proxy is (dTerror − error_mean) × airflow for a host that wants numbers, and the leak-hours count alone ranks boxes for a valve walk-down. Confidence is MEDIUM on the sibling cards’ terms: the method is validated on physically injected faults (§5.2), the shipped parameters are one simulated box plus the source’s campaign. Heating-season biased, since the leak needs a live heat source behind the valve.

Emissions Impact

Scope 1 or 2 by heat source, QUALITATIVE_EMISSIONS. A passing hydronic valve spends boiler fuel (Scope 1 where gas) plus the chiller electricity that removes it (Scope 2); an electric element is Scope 2 twice. The double-payment structure means abating this fault removes emissions on both sides at once, which is why it ranks above its comfort impact — the zone usually feels fine.

Deviations

  • Reheat-on clears the accumulators rather than holding them. The source computes dTerror only while the coil is off and says nothing about what the sums do meanwhile. Holding would carry a stale sum across a multi-hour heating call and alarm on the first armed tick after it; clearing costs the case where a slow leak is interleaved with regular legitimate reheat — but a coil regularly commanded open is VAV-0003’s jurisdiction, not a coil passing unseen. The vectors pin the choice (reheat_command_resets_accumulator).
  • One Logical.And gates both charts through the shared reset switches, collapsing the source’s occupied-only rule and reheat-off rule into a single arm condition. The two evaluability flags stay separate outputs precisely because the host cannot otherwise tell which gate silenced the rule — a box heating all day publishes yReheatOk = false for hours and that is normal.
  • error_mean/error_sigma are measured, not floored — the contrast with VAV-0008. Fan heat and duct gain give this channel a real healthy baseline; the committed vavcal method measured 0.44/0.39 °C across the OfficeMedium-4004 zones on reheat-off occupied ticks. They remain per-box commissioning values (§5.1.5): a long duct run or a series fan moves the baseline, and error_mean is where that lives.
  • slack_k = 3.0 and alarm_limit_h = 100 come from Table 5’s Iowa campaign, not Figure 9’s illustration — the one channel in the trio whose defaults are measured. Still commissioning parameters; the campaign’s boxes are not your boxes.
  • The T chart is kept and its finding renamed. A leak cannot produce a negative rise, so yLow names instrumentation (wrong sat binding, sensor drift or swap) instead of pretending to be a coil finding. Dropping the chart would have been cheaper in blocks; keeping the source’s two-sided recursion buys a free wrong-loop detector on the channel most exposed to a binding mistake.
  • sat stands in for entering-air temperature per §5.1.4’s own workaround. The bias of that approximation was validated below 0.005 °C by the committed harness method — in simulation, on one building; the real-world residual is duct gain, which error_mean absorbs by design.
  • rht_vlv_cmd is compared strictly below 1 % (Reals.LessThreshold) — command, not position, because position feedback is rare on terminal units and a lying command is diagnosis 3. The threshold is a parameter so sites whose controllers never write a clean zero can still arm the rule.
  • The shared house choices are inherited, not re-argued: in-graph reset driving the published sum and the delay input together, delayOnInit = true serving a fresh exclusion hour on restart, UnitDelay y_start seeding costless at zero, strict comparisons, no alarm delay, constants over parameterized gains. VAV-0008’s Deviations carry the full arguments.
  • Severity 3, category: CRITICAL_WASTE, confidence: MEDIUM and the name are library-authored. CRITICAL_WASTE follows VAV-0003 — same waste, same double payment — rather than the trio’s COMFORT_ENERGY, because the zone is typically comfortable while this fault runs. g36: null on the same grounds as the siblings.

Notes

The trio splits by error signal: VAV-0007 accumulates airflow tracking error, VAV-0008 zone temperature error, this card the reheat-off discharge rise. This is the only channel that needs vav_dat; a box without it runs the other two as the source’s reduced VPACC and loses only this coil’s coverage.

VAV-0003 and this card meet at the same valve from opposite sides: 052 convicts a valve commanded open while the zone is satisfied, this card a valve passing while commanded shut. Neither suppresses the other — a failed actuator can produce both in one afternoon, and seeing both is the diagnosis.

Test Vectors

8 scenarios, clock step 60 s over 10800 s.

ScenarioDescription
healthy_variation_below_slackdTerror wanders over 0.05, 0.83 and 1.60 degC — plus and minus one sigma about the commissioned 0.44 degC, then out to just inside 3 sigma.
leak_accumulates_to_alarmA 5.0 degC rise across a coil commanded shut — inside the 3.7 to 11.9 degF band the source measured on its stuck valve (Table 3). z = 11.7, so each sample banks 8.69 and the twelfth accumulating sample carries S past h = 100 at t = 4260 s.
negative_bias_accumulates_on_low_sideThe mirror direction: the discharge reads 5.0 degC BELOW the broadcast supply temperature, which no leak can produce. T accumulates, yLow rises and yHigh stays down — the direction flags are what separate a leaking coil from a broken dT channel.
unoccupied_resets_mid_accumulationS reaches 86.9 and the schedule drops at t = 4140 s. The accumulator goes to zero rather than freezing, so the return to occupancy at 4380 s costs a fresh warmup hour and a fresh climb: the alarm lands at 8640 s instead of 4260 s.
warmup_hour_runs_from_the_occupied_transitionOccupancy starts at t = 1800 s, so the excluded hour ends at 5400 s and not at 3600 s: the source’s first-hour rule is measured from the transition, not from controller start. The alarm follows twelve samples later.
reheat_command_resets_accumulatorS reaches 78.2, the coil is commanded open from 4140 s to 4440 s, and the accumulator is back at zero when the valve shuts again — the source’s own reading of its Figure 13 (the dT CUSUM is reset to zero when the valve is not full close). The climb restarts from nothing and the alarm lands at 5160 s, not the 4620 s a held accumulator would give.
alarm_clears_as_accumulator_drains_through_hAfter the alarm at 4260 s the rise falls back to 1.415 degC, inside the slack band, and S drains at 0.5 per sample from 104.3. Both sides of h at the accumulator level: still true at 4680 s (S = 100.8), false from 4860 s (S = 99.3). The comparison is strict and there is no persistence timer to unwind.
reheat_open_throughout_channel_silentA 5.0 degC rise across a coil commanded 50% open is the coil doing its job. No reheat-off sample exists all run, both accumulators stay at zero, and yReheatOk stays false so the host reports NO_EVAL rather than a healthy box.
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 60,
    "horizon_s": 10800
  },
  "scenarios": [
    {
      "name": "healthy_variation_below_slack",
      "description": "dTerror wanders over 0.05, 0.83 and 1.60 degC \u2014 plus and minus one sigma about the commissioned 0.44 degC, then out to just inside 3 sigma. |z| never clears slack_k, so neither accumulator ever leaves zero. This is what the slack parameter is for.",
      "inputs": {
        "vav_dat": [
          {
            "t": 0,
            "value": 13.83
          },
          {
            "t": 600,
            "value": 13.05
          },
          {
            "t": 1200,
            "value": 14.6
          },
          {
            "t": 1800,
            "value": 13.05
          },
          {
            "t": 2400,
            "value": 13.83
          },
          {
            "t": 3000,
            "value": 14.6
          },
          {
            "t": 3600,
            "value": 13.05
          },
          {
            "t": 4200,
            "value": 13.83
          },
          {
            "t": 4800,
            "value": 14.6
          },
          {
            "t": 5400,
            "value": 13.05
          },
          {
            "t": 6000,
            "value": 13.83
          },
          {
            "t": 6600,
            "value": 14.6
          },
          {
            "t": 7200,
            "value": 13.05
          },
          {
            "t": 7800,
            "value": 13.83
          },
          {
            "t": 8400,
            "value": 14.6
          },
          {
            "t": 9000,
            "value": 13.05
          },
          {
            "t": 9600,
            "value": 13.83
          },
          {
            "t": 10200,
            "value": 14.6
          }
        ],
        "sat": 13.0,
        "rht_vlv_cmd": 0.0,
        "occ_scheduled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 10800,
          "equals": false
        },
        {
          "output": "yHigh",
          "from_s": 0,
          "to_s": 10800,
          "equals": false
        },
        {
          "output": "yLow",
          "from_s": 0,
          "to_s": 10800,
          "equals": false
        },
        {
          "output": "yReheatOk",
          "from_s": 0,
          "to_s": 10800,
          "equals": true
        },
        {
          "output": "yOccupiedOk",
          "from_s": 0,
          "to_s": 3540,
          "equals": false
        },
        {
          "output": "yOccupiedOk",
          "from_s": 3660,
          "to_s": 10800,
          "equals": true
        }
      ]
    },
    {
      "name": "leak_accumulates_to_alarm",
      "description": "A 5.0 degC rise across a coil commanded shut \u2014 inside the 3.7 to 11.9 degF band the source measured on its stuck valve (Table 3). z = 11.7, so each sample banks 8.69 and the twelfth accumulating sample carries S past h = 100 at t = 4260 s.",
      "inputs": {
        "vav_dat": 18.0,
        "sat": 13.0,
        "rht_vlv_cmd": 0.0,
        "occ_scheduled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4140,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 4320,
          "to_s": 10800,
          "equals": true
        },
        {
          "output": "yHigh",
          "from_s": 0,
          "to_s": 4140,
          "equals": false
        },
        {
          "output": "yHigh",
          "from_s": 4320,
          "to_s": 10800,
          "equals": true
        },
        {
          "output": "yLow",
          "from_s": 0,
          "to_s": 10800,
          "equals": false
        }
      ]
    },
    {
      "name": "negative_bias_accumulates_on_low_side",
      "description": "The mirror direction: the discharge reads 5.0 degC BELOW the broadcast supply temperature, which no leak can produce. T accumulates, yLow rises and yHigh stays down \u2014 the direction flags are what separate a leaking coil from a broken dT channel.",
      "inputs": {
        "vav_dat": 8.0,
        "sat": 13.0,
        "rht_vlv_cmd": 0.0,
        "occ_scheduled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4020,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 4200,
          "to_s": 10800,
          "equals": true
        },
        {
          "output": "yLow",
          "from_s": 0,
          "to_s": 4020,
          "equals": false
        },
        {
          "output": "yLow",
          "from_s": 4200,
          "to_s": 10800,
          "equals": true
        },
        {
          "output": "yHigh",
          "from_s": 0,
          "to_s": 10800,
          "equals": false
        }
      ]
    },
    {
      "name": "unoccupied_resets_mid_accumulation",
      "description": "S reaches 86.9 and the schedule drops at t = 4140 s. The accumulator goes to zero rather than freezing, so the return to occupancy at 4380 s costs a fresh warmup hour and a fresh climb: the alarm lands at 8640 s instead of 4260 s.",
      "inputs": {
        "vav_dat": 18.0,
        "sat": 13.0,
        "rht_vlv_cmd": 0.0,
        "occ_scheduled": [
          {
            "t": 0,
            "value": true
          },
          {
            "t": 4140,
            "value": false
          },
          {
            "t": 4380,
            "value": true
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 8520,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 8700,
          "to_s": 10800,
          "equals": true
        },
        {
          "output": "yOccupiedOk",
          "from_s": 0,
          "to_s": 3540,
          "equals": false
        },
        {
          "output": "yOccupiedOk",
          "from_s": 3660,
          "to_s": 4080,
          "equals": true
        },
        {
          "output": "yOccupiedOk",
          "from_s": 4140,
          "to_s": 7920,
          "equals": false
        },
        {
          "output": "yOccupiedOk",
          "from_s": 8040,
          "to_s": 10800,
          "equals": true
        }
      ]
    },
    {
      "name": "warmup_hour_runs_from_the_occupied_transition",
      "description": "Occupancy starts at t = 1800 s, so the excluded hour ends at 5400 s and not at 3600 s: the source's first-hour rule is measured from the transition, not from controller start. The alarm follows twelve samples later.",
      "inputs": {
        "vav_dat": 18.0,
        "sat": 13.0,
        "rht_vlv_cmd": 0.0,
        "occ_scheduled": [
          {
            "t": 0,
            "value": false
          },
          {
            "t": 1800,
            "value": true
          }
        ]
      },
      "expect": [
        {
          "output": "yOccupiedOk",
          "from_s": 0,
          "to_s": 5340,
          "equals": false
        },
        {
          "output": "yOccupiedOk",
          "from_s": 5460,
          "to_s": 10800,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5940,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 6120,
          "to_s": 10800,
          "equals": true
        }
      ]
    },
    {
      "name": "reheat_command_resets_accumulator",
      "description": "S reaches 78.2, the coil is commanded open from 4140 s to 4440 s, and the accumulator is back at zero when the valve shuts again \u2014 the source's own reading of its Figure 13 (the dT CUSUM is reset to zero when the valve is not full close). The climb restarts from nothing and the alarm lands at 5160 s, not the 4620 s a held accumulator would give.",
      "inputs": {
        "vav_dat": 18.0,
        "sat": 13.0,
        "rht_vlv_cmd": [
          {
            "t": 0,
            "value": 0.0
          },
          {
            "t": 4140,
            "value": 50.0
          },
          {
            "t": 4500,
            "value": 0.0
          }
        ],
        "occ_scheduled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5040,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 5220,
          "to_s": 10800,
          "equals": true
        },
        {
          "output": "yReheatOk",
          "from_s": 0,
          "to_s": 4080,
          "equals": true
        },
        {
          "output": "yReheatOk",
          "from_s": 4140,
          "to_s": 4440,
          "equals": false
        },
        {
          "output": "yReheatOk",
          "from_s": 4500,
          "to_s": 10800,
          "equals": true
        }
      ]
    },
    {
      "name": "alarm_clears_as_accumulator_drains_through_h",
      "description": "After the alarm at 4260 s the rise falls back to 1.415 degC, inside the slack band, and S drains at 0.5 per sample from 104.3. Both sides of h at the accumulator level: still true at 4680 s (S = 100.8), false from 4860 s (S = 99.3). The comparison is strict and there is no persistence timer to unwind.",
      "inputs": {
        "vav_dat": [
          {
            "t": 0,
            "value": 18.0
          },
          {
            "t": 4320,
            "value": 14.415
          }
        ],
        "sat": 13.0,
        "rht_vlv_cmd": 0.0,
        "occ_scheduled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4140,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 4320,
          "to_s": 4680,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 4860,
          "to_s": 10800,
          "equals": false
        }
      ]
    },
    {
      "name": "reheat_open_throughout_channel_silent",
      "description": "A 5.0 degC rise across a coil commanded 50% open is the coil doing its job. No reheat-off sample exists all run, both accumulators stay at zero, and yReheatOk stays false so the host reports NO_EVAL rather than a healthy box.",
      "inputs": {
        "vav_dat": 18.0,
        "sat": 13.0,
        "rht_vlv_cmd": 50.0,
        "occ_scheduled": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 10800,
          "equals": false
        },
        {
          "output": "yHigh",
          "from_s": 0,
          "to_s": 10800,
          "equals": false
        },
        {
          "output": "yLow",
          "from_s": 0,
          "to_s": 10800,
          "equals": false
        },
        {
          "output": "yReheatOk",
          "from_s": 0,
          "to_s": 10800,
          "equals": false
        }
      ]
    }
  ]
}