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

CHW-0004 — Chilled water low delta-T syndrome

Statusverified — engine e2ff2f8, cxf:fnv1a128:f12c5169f0aa1755a37402fe2dbfe04a, 2026-08-17
Severity3
Methodrule
Phase2
CategoryEXCESS_CONSUMPTION
ConfidenceMEDIUM
EstimationPROXY_ESTIMATION
G36
ClustersCLU-06
Suppresses
Suppressed by
RelatedCHW-0001, CHW-0003, AHU-0014, FCU-0004
Playbookslow-delta-t
SourceHVAC FDD Reference v1.0 §13 (ch. ‘Chilled Water Plants’, pdf pp. 122-123), CHW-0004; PNNL-27338 §3; PNNL-25985
Operating stateschilled water plant producing, with the chiller loaded above min_load_for_eval — the rule’s own yLoadOk is that state

Preconditions (host-enforced): chwst, chwrt and chiller_load must describe the same hydraulic loop at the same moment. On a primary/secondary plant that is the precondition most often violated: the chiller’s own entering/leaving temperatures see primary flow and read a healthy delta-T while the secondary loop that actually serves the coils is short-circuiting through the decoupler, which is the fault. Bind the temperatures where the coils are — the secondary supply and return headers on a decoupled plant, the chiller connections on a variable-primary one — and bind chiller_load from the same plant. Both temperatures must be in °C (the rule converts nothing) and design_delta_t must be this loop’s design value, not the shipped 5.6 °C, before any verdict means anything. The two sensors must also be in the right places: nothing in the rule can tell a swapped supply/return pair from a genuine low delta-T (see Deviations), and a supply sensor reading high biases delta-T low in exactly the direction that alarms. Sensor calibration is worth confirming before a first deployment, because a 0.5 K offset on a 2.8 K trip line is 18% of the decision. Evaluability is signalled in-rule by yLoadOk: when it is false the verdict is NO_EVAL, not a healthy plant.

Points: chwst, chwrt, chiller_load

Outputs:

  • yFault — True while the chilled water delta-T has stayed below design_delta_t × low_dt_fraction with the chiller loaded above min_load_for_eval, continuously for at least alarm_delay
  • yLoadOk — Evaluability signal — true when chiller_load is above min_load_for_eval, the load below which a small delta-T says nothing about the plant. False means NO_EVAL and the host must ignore yFault

Parameters:

NameDefaultUnitCXF pathDescription
design_delta_t5.6°CdesignDt.kDesign chilled water delta-T (the reference’s 5.6 °C = 10 °F). PER-LOOP SITE CONFIGURATION — read it off the plant’s design documents; a 6.7 K (12 °F) plant and a 4.4 K (8 °F) plant are both common and neither is served by the shipped value.
low_dt_fraction0.51lowDtLimit.kFraction of design delta-T below which the plant is faulted (the reference’s 50%). Kept as its own parameter rather than folded into the trip line so that a site can retune the tolerance and the design value independently — see Deviations.
min_load_for_eval40.0%loadOk.tChiller load below which delta-T is not evaluated. The reference’s min_load_for_eval: a lightly loaded plant has a small delta-T because there is little load, not because anything is wrong.
alarm_delay3600.0spersist.delayTimeContinuous low delta-T at load required before the alarm asserts (60 min). The reference’s AlarmDelay, renamed to the library’s convention

Description

A chilled water plant is sized on a temperature difference, not on a flow. Design the coils for 5.6 K between supply and return and the pumps move enough water to carry the peak load; let that difference fall to 2 K and the same load needs nearly three times the flow, so the pumps run faster, the second pump starts, and eventually a second chiller comes on to make water the first could have made if the water had come back warm enough to use. That is low delta-T syndrome, and its signature is that nothing looks broken — every zone is comfortable and the only symptom is a plant working much harder than the building it serves. It is measured at the plant because that is where the individual causes add up: a bypassing three-way valve, a fouled coil, a filter nobody changed, each too small to see from the AHU that owns it.

Detection Logic

delta_t   = chwrt − chwst
low_limit = design_delta_t × low_dt_fraction        (5.6 × 0.5 = 2.8 K)

yLoadOk = chiller_load > min_load_for_eval          (false ⇒ host reports NO_EVAL)
yFault  = delta_t < low_limit AND yLoadOk,
          sustained continuously for alarm_delay

Block graph (rule.cxf.jsonld):

CHW-0004 block graph

designDt and lowDtLimit assemble the trip line inside the graph rather than shipping a pre-multiplied 2.8, so both of the reference’s numbers survive as independent set_param targets; they are retuned for different reasons.

lowDt is strict, so a plant sitting exactly on the trip line reads healthy, and the boundary is bit-exact rather than approximate: 5.6 halved is the double nearest 2.8, which a realistic temperature pair can reach exactly, so the comparison is decided by the strictness and not by rounding. loadOk is the reference’s min_load_for_eval and the whole NO_EVAL story — at 20% load a 1 K delta-T is what a healthy plant produces, and exposing the conjunct as yLoadOk lets the host tell that from a plant that is loaded and fine. persist requires 60 continuous minutes and carries delayOnInit = true; low delta-T is a plant condition, and anything shorter is a valve stroking or a coil catching up.

Possible Diagnoses

Transcribed from the reference’s CHW-0004 card:

  1. Three-way valve bypass allowing CHW to short-circuit — the classic cause, and worst as the building unloads, because that is when the bypass is widest
  2. AHU/FCU coil fouling, so the water leaves the coil colder than it should. AHU-0014 and FCU-0004 see this from the air side, one unit at a time
  3. Low airflow across cooling coils — dirty filters, a slow fan, a closed damper
  4. CHW valve leaking or stuck partially open — the same arithmetic as a bypass valve with a different part number
  5. Oversized CHW system relative to actual load — the case with no repair, where the delta-T is telling the truth

Causes 1 through 4 are local defects this plant-level rule aggregates: a building with forty coils can reach the trip line with four misbehaving and thirty-six fine, which is what makes the finding hard to chase and worth having.

Energy Impact

EXCESS_CONSUMPTION, MEDIUM confidence, PROXY_ESTIMATION. The reference’s estimator is excess_pump_kw ≈ chw_pump_kw × (design_dt − actual_dt) / design_dt — a plant at 2.8 K on a 5.6 K design spends about half its pump energy on water that comes back too cold to be worth moving — with a published range of 5–15% of pump energy. That understates the cost: the expensive consequence is staging, a second chiller serving a load the first could have carried, and the compressor energy dwarfs the pumps. The reference names “staging inefficiency” without putting a number on it and neither does this card, because the number depends on the plant’s staging logic. Confidence is MEDIUM because the finding is plant-level and the repair is not. Cooling-dominant.

Emissions Impact

Scope 2, PROXY_EMISSIONS, MEDIUM confidence; the reference’s typical range is 200–2,000 kg CO₂e/yr for pump and staging inefficiency together, on a marginal operating emissions rate basis. All of it is purchased electricity, so the scope does not vary by site the way a heating fault’s does, and the timing works against the building: low delta-T bites hardest on the hottest afternoons, which are also the hours when the marginal generator is dirtiest.

Deviations

  • The trip line is assembled in the graph rather than folded into a threshold. A single Reals.LessThreshold with t = 2.8 would compute the same verdict with one block instead of three and would lose both of the reference’s tunables — a site with a 6.7 K design could no longer change it without recomputing the product, and the 50% fraction would stop being visible. Precedent: VFD-0002’s assembled speed floor, which adds two constants where this rule multiplies, because the reference’s composition is a product.
  • Strict < at the trip line. The reference writes < too, and CDL Reals has no LessEqual in any case, so a plant at exactly 2.8 K reads healthy. The disagreement is measure-zero, and the on-the-line vector uses a 5 °C supply temperature rather than the published vectors’ 6 °C for an arithmetic reason: 8.8 − 6.0 lands one ulp above 2.8 and would have pinned the wrong side.
  • Strict > at the load floor, same treatment. The reference writes chiller_load > min_load_for_eval, so a chiller at exactly 40% is NO_EVAL.
  • yLoadOk is the library’s shape for the reference’s NO_EVAL row. The reference writes the load test as a conjunct of the fault condition and publishes a NO_EVAL vector for it; the graph computes that conjunct and additionally exposes it as a boundary output, which adds no logic and changes no verdict. It is a comparison of an input against a parameter rather than an echo of an input, which is what SCHEMA.md asks. Same stance as HP-0001’s yPowerOk.
  • Nothing guards against an inverted delta-T. A swapped supply/return pair, or sensors on the wrong side of a decoupler, produces a negative delta-T that is below any positive trip line and alarms permanently. The block set could express a guard, but suppressing negative delta-T would also suppress the genuine short-circuit case on a plant whose sensors are fine, so the honest answer is a documented blind spot. Commissioning check: swap the leads and watch the sign, once, before trusting the rule.
  • The rule is blind to which coil is responsible, and to how many. That is the reference’s design rather than a simplification — the individual coil faults are usually too small to detect one at a time, which is why the syndrome is measured in the return header. AHU-0014 and FCU-0004 are the coil-side rules worth running alongside it; neither is wired to this one.
  • Persistence stands in for averaging. The rule consumes instantaneous points and the reference specifies no averaging, so a delta-T alternating above and below the line every 20 minutes never accumulates the hour and never alarms, though a plant spending half its day low is a genuine finding. A steady syndrome — a bypassing valve or a fouled coil — reads the same either way.
  • AlarmDelay is renamed alarm_delay. The reference’s tunables table spells the fault-persistence parameter in G36’s PascalCase while spelling its three neighbours in snake_case; the library uses alarm_delay throughout and the value is unchanged at 60 min.
  • persist.delayOnInit = true (CDL default false), the library’s standing choice: a plant already below the line at controller restart waits out the full hour rather than alarming on the first tick.
  • chiller_load is per-chiller while the delta-T is per-loop. On a multi-chiller plant the load signal belongs to one machine and the header temperatures to the loop, so a plant running two chillers at 45% each is evaluated on one of them. The reference names the same single point and does not address the case; bind the lead chiller or a host-computed plant load.
  • Three published test vectors, the rest authored. The reference publishes normal delta-T, low delta-T and low load (NO_EVAL); all three are transcribed into vectors.json and pass. The remainder — the trip line and load floor boundaries, the mid-run collapse and recovery edges, the evaluability release, the intermittent case and the swapped-sensor blind spot — are library-authored.
  • The chapter’s Notes line for this fault is truncated in the source extract. It reads “One of the most common and costly CHW plant issues. Forces” and stops. Nothing here depends on the missing clause: the “forces extra pumping” reading in energy_impact.savings_range comes from the chapter’s own Savings Range row, which is complete.
  • clusters: [CLU-06] is the existing cluster set’s membership, not this card’s authorship. clusters/clusters.json already lists this fault as a member of “Chilled Water Plant Inefficiency” with CHW-0001 as the trigger, and that cluster’s playbook slug resolves to playbooks/chiller-efficiency.md.
  • playbooks cites low-delta-t, the reference’s own playbook, whose Applies-To row names this card directly.
  • Operating states and preconditions are declared in frontmatter for host enforcement rather than encoded in the block graph, per the library’s design stance. Severity 3, method: rule and the fault name are the reference’s chapter 13 card.

Notes

Source-pointer precision: the reference’s ch.13 card cites “PNNL-27338 §3”, but in PNNL-27338’s own numbering the low delta-T algorithm sits in the hot-water distribution chapter (§4.6), and that document names chilled-water diagnostics as future work. The citation is an analog/pattern source — the HW delta-T algorithm mirrored to CHW — not a CHW-specific specification.

Read yLoadOk before yFault. A plant that is off, or coasting through a mild morning at 25% load, holds yLoadOk false for hours, and every yFault = false underneath it means “not evaluated” rather than “delta-T is fine”.

Trend delta-T against plant load for a week before sending anyone: a delta-T that degrades as the building unloads points at bypass and leaking control valves, while one that is flat and low across the range points at fouling or at a plant oversized for the building. Then check the largest coils, because the syndrome is a sum. CHW-0001 shares this rule’s cluster and the two reinforce each other — extra pumping and an early-staged chiller both push kW/ton up — so treat the delta-T as the trigger and the efficiency alarm as its consequence.

Test Vectors

14 scenarios, clock step 60 s over 7200 s.

ScenarioDescription
normal_delta_tThe reference’s published vector 1: CHWST 6 °C, CHWRT 12 °C, load 60% — expected NO_FAULT. A 6 K delta-T is above design, let alone above the 2.8 K trip line, so the rule is evaluated and silent.
low_delta_tThe reference’s published vector 2: CHWST 6 °C, CHWRT 8 °C, load 60% — expected FAULT. Delta-T is 2 K against a 2.8 K trip line; the alarm lands at exactly alarm_delay because delayOnInit holds the condition from the first tick.
low_loadThe reference’s published vector 3: CHWST 6 °C, CHWRT 7 °C, load 20% — expected NO_EVAL. The 1 K delta-T is well below the trip line and means nothing at 20% load, which is what yLoadOk = false tells the host.
delta_t_exactly_at_the_thresholdBoundary, bit-exact: 7.8 − 5.0 is the double nearest 2.8, and 5.6 × 0.5 is the same double (halving is exact), so delta-T sits exactly on the trip line. Reals.Less is strict, so exactly-at-the-line is not a fault.
delta_t_just_below_the_thresholdBoundary from below: 7.79 − 5.0 = 2.79 K clears the strict comparison by 10 mK and alarms on the normal schedule.
delta_t_just_above_the_thresholdBoundary from above: 7.81 − 5.0 = 2.81 K is above the trip line and never alarms, however marginal the plant’s performance is.
load_exactly_at_the_evaluability_floorBoundary on the other conjunct: load is exactly min_load_for_eval (40%) with a collapsed 2 K delta-T. Reals.GreaterThreshold is strict, so exactly-at-the-floor is NO_EVAL and the rule stays silent.
load_just_below_the_evaluability_floor39.9% load with the same collapsed delta-T: still NO_EVAL.
load_just_above_the_evaluability_floor40.1% load with the same collapsed delta-T: evaluable, and the alarm lands at exactly alarm_delay. One tenth of a percent of chiller load is the whole difference between this scenario and the previous one.
delta_t_collapses_mid_runA plant running at a healthy 6 K delta-T loses it at t = 1800 s — a three-way valve opening its bypass, or a coil starving. The alarm lands at exactly 5400 s: the mid-run rising edge carries the same T + delayTime arithmetic as the init case.
delta_t_recovers_after_alarmRecovery: the alarm asserts at 3600 s and the return temperature comes back at t = 5400 s. TrueDelay passes the falling edge with no delay, so yFault drops on that tick.
load_drops_after_alarmThe evaluability release: an alarming plant unloads to 20% at t = 5400 s with the low delta-T unchanged. yFault and yLoadOk fall on the same tick, and only the pair tells the host that the chiller unloaded rather than that the delta-T recovered.
intermittent_low_delta_never_alarmsDelta-T alternating between 2 K and 6 K every 1200 s — a hunting control valve, or a plant cycling between two coils. No single episode reaches alarm_delay, so nothing fires. Persistence is not averaging: a loop spending half its day at 2 K delta-T is a real finding this rule cannot make.
supply_and_return_sensors_swappedBlind spot, pinned: the two temperature sensors are wired backwards (CHWST 12 °C, CHWRT 6 °C), so delta-T reads −6 K. Nothing in the graph knows a negative delta-T is impossible, and the rule reports the strongest possible low-delta-T fault on a plant that may be running perfectly.
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 60,
    "horizon_s": 7200
  },
  "scenarios": [
    {
      "name": "normal_delta_t",
      "description": "The reference's published vector 1: CHWST 6 \u00b0C, CHWRT 12 \u00b0C, load 60% \u2014 expected NO_FAULT. A 6 K delta-T is above design, let alone above the 2.8 K trip line, so the rule is evaluated and silent.",
      "inputs": {
        "chwst": 6.0,
        "chwrt": 12.0,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "low_delta_t",
      "description": "The reference's published vector 2: CHWST 6 \u00b0C, CHWRT 8 \u00b0C, load 60% \u2014 expected FAULT. Delta-T is 2 K against a 2.8 K trip line; the alarm lands at exactly alarm_delay because delayOnInit holds the condition from the first tick.",
      "inputs": {
        "chwst": 6.0,
        "chwrt": 8.0,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 3600,
          "to_s": 7200,
          "equals": true
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "low_load",
      "description": "The reference's published vector 3: CHWST 6 \u00b0C, CHWRT 7 \u00b0C, load 20% \u2014 expected NO_EVAL. The 1 K delta-T is well below the trip line and means nothing at 20% load, which is what yLoadOk = false tells the host.",
      "inputs": {
        "chwst": 6.0,
        "chwrt": 7.0,
        "chiller_load": 20.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "delta_t_exactly_at_the_threshold",
      "description": "Boundary, bit-exact: 7.8 \u2212 5.0 is the double nearest 2.8, and 5.6 \u00d7 0.5 is the same double (halving is exact), so delta-T sits exactly on the trip line. Reals.Less is strict, so exactly-at-the-line is not a fault.",
      "inputs": {
        "chwst": 5.0,
        "chwrt": 7.8,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "delta_t_just_below_the_threshold",
      "description": "Boundary from below: 7.79 \u2212 5.0 = 2.79 K clears the strict comparison by 10 mK and alarms on the normal schedule.",
      "inputs": {
        "chwst": 5.0,
        "chwrt": 7.79,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 3600,
          "to_s": 7200,
          "equals": true
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "delta_t_just_above_the_threshold",
      "description": "Boundary from above: 7.81 \u2212 5.0 = 2.81 K is above the trip line and never alarms, however marginal the plant's performance is.",
      "inputs": {
        "chwst": 5.0,
        "chwrt": 7.81,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "load_exactly_at_the_evaluability_floor",
      "description": "Boundary on the other conjunct: load is exactly min_load_for_eval (40%) with a collapsed 2 K delta-T. Reals.GreaterThreshold is strict, so exactly-at-the-floor is NO_EVAL and the rule stays silent.",
      "inputs": {
        "chwst": 6.0,
        "chwrt": 8.0,
        "chiller_load": 40.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "load_just_below_the_evaluability_floor",
      "description": "39.9% load with the same collapsed delta-T: still NO_EVAL.",
      "inputs": {
        "chwst": 6.0,
        "chwrt": 8.0,
        "chiller_load": 39.9
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "load_just_above_the_evaluability_floor",
      "description": "40.1% load with the same collapsed delta-T: evaluable, and the alarm lands at exactly alarm_delay. One tenth of a percent of chiller load is the whole difference between this scenario and the previous one.",
      "inputs": {
        "chwst": 6.0,
        "chwrt": 8.0,
        "chiller_load": 40.1
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 3600,
          "to_s": 7200,
          "equals": true
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "delta_t_collapses_mid_run",
      "description": "A plant running at a healthy 6 K delta-T loses it at t = 1800 s \u2014 a three-way valve opening its bypass, or a coil starving. The alarm lands at exactly 5400 s: the mid-run rising edge carries the same T + delayTime arithmetic as the init case.",
      "inputs": {
        "chwst": 6.0,
        "chwrt": [
          {
            "t": 0,
            "value": 12.0
          },
          {
            "t": 1800,
            "value": 8.0
          }
        ],
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5340,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 5400,
          "to_s": 7200,
          "equals": true
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "delta_t_recovers_after_alarm",
      "description": "Recovery: the alarm asserts at 3600 s and the return temperature comes back at t = 5400 s. TrueDelay passes the falling edge with no delay, so yFault drops on that tick.",
      "inputs": {
        "chwst": 6.0,
        "chwrt": [
          {
            "t": 0,
            "value": 8.0
          },
          {
            "t": 5400,
            "value": 12.0
          }
        ],
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 3600,
          "to_s": 5340,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 5400,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "load_drops_after_alarm",
      "description": "The evaluability release: an alarming plant unloads to 20% at t = 5400 s with the low delta-T unchanged. yFault and yLoadOk fall on the same tick, and only the pair tells the host that the chiller unloaded rather than that the delta-T recovered.",
      "inputs": {
        "chwst": 6.0,
        "chwrt": 8.0,
        "chiller_load": [
          {
            "t": 0,
            "value": 60.0
          },
          {
            "t": 5400,
            "value": 20.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 3600,
          "to_s": 5340,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 5400,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 5340,
          "equals": true
        },
        {
          "output": "yLoadOk",
          "from_s": 5400,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "intermittent_low_delta_never_alarms",
      "description": "Delta-T alternating between 2 K and 6 K every 1200 s \u2014 a hunting control valve, or a plant cycling between two coils. No single episode reaches alarm_delay, so nothing fires. Persistence is not averaging: a loop spending half its day at 2 K delta-T is a real finding this rule cannot make.",
      "inputs": {
        "chwst": 6.0,
        "chwrt": [
          {
            "t": 0,
            "value": 8.0
          },
          {
            "t": 1200,
            "value": 12.0
          },
          {
            "t": 2400,
            "value": 8.0
          },
          {
            "t": 3600,
            "value": 12.0
          },
          {
            "t": 4800,
            "value": 8.0
          },
          {
            "t": 6000,
            "value": 12.0
          }
        ],
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "supply_and_return_sensors_swapped",
      "description": "Blind spot, pinned: the two temperature sensors are wired backwards (CHWST 12 \u00b0C, CHWRT 6 \u00b0C), so delta-T reads \u22126 K. Nothing in the graph knows a negative delta-T is impossible, and the rule reports the strongest possible low-delta-T fault on a plant that may be running perfectly.",
      "inputs": {
        "chwst": 12.0,
        "chwrt": 6.0,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 3600,
          "to_s": 7200,
          "equals": true
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 7200,
          "equals": true
        }
      ]
    }
  ]
}