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-0007 — Chilled-water supply temperature tracking failure

Statusverified — engine e2ff2f8, cxf:fnv1a128:c2159704c8d4e994bfed070006ff5d4e, 2026-08-20
Severity3
Methodrule
Phase2
CategoryCOMFORT_ENERGY
ConfidenceMEDIUM
EstimationQUALITATIVE_ONLY
G36
Clusters
Suppresses
Suppressed by
RelatedCHW-0001, CHW-0002, CHW-0005, CHW-0006, CHW-0008, CHW-0009
Playbookschiller-efficiency
SourcePNNL-29078, Building Re-Tuning Training Guide: Central Utility Plant Heating and Cooling Control Guide, PDF pp.91 and 96 (§10) — the plant maintains a chilled-water supply target and should trend supply temperature during operation; EPA Facilities Manual, Volume 2, ch.9 Table 9-2 — chiller BAS monitoring includes leaving-water setpoint, start/stop, failure, and chilled-water temperature above setpoint; Library-authored executable adaptation of AHU-0033’s strict mirrored tracking-error topology; no source publishes the shipped 1 K / 20% / 900 s combination as portable
Operating statesnormal automatic chiller operation after startup, with the individual machine proven running and loaded above minimum_load

Preconditions (host-enforced): chwst and chwst_sp must describe the same controllable leaving-water target. Prefer this chiller’s evaporator outlet temperature and the final active setpoint used by its controller. A common-header measurement/setpoint is acceptable only when the deployment proves the staged machine(s) truly control that same mixed-header target; it is not interchangeable with an individual barrel outlet by assumption. chiller_status and chiller_load must belong to the same machine, not a fleet OR/max. Flow and minimum-flow permissives must be established. Exclude startup pull-down, setpoint/reset ramps, staging transfers, pump/valve transients, ice-making, and manufacturer current/lift/surge/freeze/demand limits. Temperature points must be healthy, fresh, aligned, and in degC. yLoadOk is numerical evaluability only: false means NO_EVAL; true does not prove any of the host obligations. A stopped chiller is also NO_EVAL even though yLoadOk can remain true.

Points: chwst, chwst_sp, chiller_status, chiller_load

Outputs:

  • yFault — True while a running, meaningfully loaded chiller has remained outside either side of the active CHWST band for sustained_duration
  • yTooWarm — Diagnostic direction flag — true when the evaluable leaving-water temperature is more than tracking_error above setpoint
  • yTooCold — Diagnostic direction flag — true when the evaluable leaving-water temperature is more than tracking_error below setpoint
  • yLoadOk — Evaluability flag — true only when chiller_load is strictly above minimum_load; false means NO_EVAL

Parameters:

NameDefaultUnitCXF pathDescription
tracking_error1.0Kwarm.t, cold.tSymmetric leaving-water tracking allowance. ADOPTED_TUNABLE: commission above combined sensor uncertainty and the settled control deadband; equality is clear.
minimum_load20.0%loadOk.tPer-machine load floor. ADOPTED_TUNABLE: excludes low-load cycling and unloading behavior; equality is not evaluable.
sustained_duration900.0spersist.delayTimeContinuous out-of-band duration. LIBRARY_PRECEDENT informed by hydronic/SAT tracking timescales; verify against this plant’s pull-down, reset, and staging dynamics.

Description

This rule reports a chiller or genuinely representative plant leaving-water temperature that cannot hold its active target under meaningful load. A warm direction can mean insufficient capacity, lost flow, fouling, refrigerant or sensor trouble, or a current/lift limit. A cold direction can mean aggressive staging, a misapplied setpoint, or a control loop driving below target. The direction narrows investigation; it does not identify the failed component.

Detection Logic

error          = chwst - chwst_sp
load_ok        = chiller_load > minimum_load
running_loaded = chiller_status AND load_ok
warm           = error > tracking_error
cold           = -error > tracking_error

yLoadOk  = load_ok
yTooWarm = running_loaded AND warm
yTooCold = running_loaded AND cold
yFault   = running_loaded AND (warm OR cold), sustained for sustained_duration

CHW-0007 block graph

Both directional comparisons are strict. The diagnostic flags are gated by machine status and load but are not persistence outputs. The single TrueDelay(delayOnInit=true) follows their OR, so a sampled direct handoff from too warm to too cold preserves timing: the machine never re-entered its band. Any in-band, stopped, or low-load tick resets the timer, and recovery clears immediately.

Possible Diagnoses

  1. Insufficient evaporator flow, closed isolation valve, or failed pump proof
  2. Fouled evaporator tubes or low refrigerant charge
  3. Compressor current, demand, surge, lift, or freeze-limit operation
  4. Incorrect staging or a machine too small for the present load
  5. Leaving-water sensor bias, stale value, or machine/header misbinding
  6. Active setpoint not delivered to the chiller controller
  7. Over-responsive local control, bad tuning, or unexcluded setpoint reset
  8. Intentional ice-making or other non-comfort operating mode not host-gated

Energy Impact

COMFORT_ENERGY and QUALITATIVE_ONLY. Overcooling normally increases compressor lift; water that is too warm can force more air/water flow and can miss space or humidity targets. This graph has no causal baseline or flow/power measurement, so it does not turn temperature error into claimed savings.

Emissions Impact

Scope 2, qualitative. Any avoided electrical energy is established only after the host quantifies the chiller, pump, and air-side response.

Deviations

  • All three defaults require commissioning. PNNL supports the control and low-load context, not a universal 1 K error, 20% load floor, or 900 s delay.
  • Confidence is MEDIUM rather than the brief’s proposed HIGH. The signature is direct, but operating limits, setpoint transitions, and machine/header topology can produce the same evidence until binding and host gates are commissioned.
  • Common-header binding is conditional. A mixed-header temperature can be a real plant target, but cannot validate an individual barrel by name alone.
  • Direction handoff preserves persistence. One timer after the OR measures continuous out-of-band operation; an actual in-band sample resets it.
  • No automatic suppression is added. CHW-0008 cannot suppress only its fail-to-start direction in current rule-wide metadata, and tracking during an unexpected run remains useful evidence. Hosts order diagnosis by direction.
  • CLU-06 is unchanged. This control-capability signature is related to efficiency and approach findings, but the existing cluster is not broadened into a mixed staging/proof cluster in this slice.

Test Vectors

20 scenarios, clock step 60 s over 3600 s.

ScenarioDescription
running_loaded_on_setpointA loaded running chiller exactly on its active leaving-water target is evaluable and clear.
too_warm_at_initializationInsufficient cooling is exposed immediately but delayOnInit serves the complete 900-second persistence window.
too_cold_at_initializationOvercooling follows the mirrored diagnostic lane and the same persistence window.
warm_exact_boundary_clearExactly +1.0 K is clear under the strict comparator.
cold_exact_boundary_clearExactly -1.0 K is clear under the mirrored strict comparator.
warm_just_beyond_boundaryOne test increment beyond the warm allowance asserts direction and then the fault.
cold_just_beyond_boundaryOne test increment beyond the cold allowance follows the independent cold lane.
load_below_floor_not_evaluableA large temperature error below the load floor produces neither direction nor fault.
load_exact_floor_not_evaluableExactly minimum_load is not evaluable because the load comparator is strict.
load_just_above_floor_evaluableOne increment above the floor opens the load gate while an on-target machine remains clear.
chiller_off_large_errorA stopped chiller has no tracking verdict even if a warm header and stale setpoint create a large raw error.
startup_transient_shorter_than_delayA loaded startup pull-down reaches the band before 900 seconds and never alarms.
violation_ends_one_step_before_persistenceA warm violation clearing one evaluator step before 900 seconds never matures.
mature_fault_clears_on_recoveryA matured tracking alarm clears immediately after leaving-water temperature recovers into band.
setpoint_step_then_recoveryA setpoint reset step creates a short raw error that recovers before persistence; hosts should also exclude unsettled ramps.
intermittent_error_resets_timerA compliant interval resets persistence; the second warm episode must serve a fresh 900 seconds.
load_gate_drop_resets_timerDropping below the evaluability floor clears direction and resets persistence even while temperature remains warm.
status_gate_drop_resets_timerA stopped interval resets timing; restart into the same error requires the full duration again.
direct_direction_handoff_keeps_timerA sampled warm-to-cold handoff changes diagnostic direction but the out-of-band OR never falls, so persistence continues.
threshold_chatter_never_persistsAlternating inside and outside the warm allowance exposes chatter but never accumulates 900 continuous seconds.
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 60,
    "horizon_s": 3600
  },
  "scenarios": [
    {
      "name": "running_loaded_on_setpoint",
      "description": "A loaded running chiller exactly on its active leaving-water target is evaluable and clear.",
      "inputs": {
        "chwst": 6.0,
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "too_warm_at_initialization",
      "description": "Insufficient cooling is exposed immediately but delayOnInit serves the complete 900-second persistence window.",
      "inputs": {
        "chwst": 7.1,
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        },
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 840,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "too_cold_at_initialization",
      "description": "Overcooling follows the mirrored diagnostic lane and the same persistence window.",
      "inputs": {
        "chwst": 4.9,
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        },
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 840,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "warm_exact_boundary_clear",
      "description": "Exactly +1.0 K is clear under the strict comparator.",
      "inputs": {
        "chwst": 7.0,
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "cold_exact_boundary_clear",
      "description": "Exactly -1.0 K is clear under the mirrored strict comparator.",
      "inputs": {
        "chwst": 5.0,
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "warm_just_beyond_boundary",
      "description": "One test increment beyond the warm allowance asserts direction and then the fault.",
      "inputs": {
        "chwst": 7.01,
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 840,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "cold_just_beyond_boundary",
      "description": "One test increment beyond the cold allowance follows the independent cold lane.",
      "inputs": {
        "chwst": 4.99,
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 840,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "load_below_floor_not_evaluable",
      "description": "A large temperature error below the load floor produces neither direction nor fault.",
      "inputs": {
        "chwst": 9.0,
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 19.9
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "load_exact_floor_not_evaluable",
      "description": "Exactly minimum_load is not evaluable because the load comparator is strict.",
      "inputs": {
        "chwst": 9.0,
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 20.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "load_just_above_floor_evaluable",
      "description": "One increment above the floor opens the load gate while an on-target machine remains clear.",
      "inputs": {
        "chwst": 6.0,
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 20.1
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "chiller_off_large_error",
      "description": "A stopped chiller has no tracking verdict even if a warm header and stale setpoint create a large raw error.",
      "inputs": {
        "chwst": 12.0,
        "chwst_sp": 6.0,
        "chiller_status": false,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooCold",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yLoadOk",
          "from_s": 0,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "startup_transient_shorter_than_delay",
      "description": "A loaded startup pull-down reaches the band before 900 seconds and never alarms.",
      "inputs": {
        "chwst": [
          {
            "t": 0,
            "value": 9.0
          },
          {
            "t": 600,
            "value": 6.0
          }
        ],
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "violation_ends_one_step_before_persistence",
      "description": "A warm violation clearing one evaluator step before 900 seconds never matures.",
      "inputs": {
        "chwst": [
          {
            "t": 0,
            "value": 8.0
          },
          {
            "t": 840,
            "value": 6.0
          }
        ],
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 780,
          "equals": true
        },
        {
          "output": "yTooWarm",
          "from_s": 900,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "mature_fault_clears_on_recovery",
      "description": "A matured tracking alarm clears immediately after leaving-water temperature recovers into band.",
      "inputs": {
        "chwst": [
          {
            "t": 0,
            "value": 8.0
          },
          {
            "t": 1200,
            "value": 6.0
          }
        ],
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 840,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 1140,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 1260,
          "to_s": 3600,
          "equals": false
        },
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 1140,
          "equals": true
        },
        {
          "output": "yTooWarm",
          "from_s": 1260,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "setpoint_step_then_recovery",
      "description": "A setpoint reset step creates a short raw error that recovers before persistence; hosts should also exclude unsettled ramps.",
      "inputs": {
        "chwst": 6.0,
        "chwst_sp": [
          {
            "t": 0,
            "value": 6.0
          },
          {
            "t": 600,
            "value": 4.5
          },
          {
            "t": 1200,
            "value": 6.0
          }
        ],
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    },
    {
      "name": "intermittent_error_resets_timer",
      "description": "A compliant interval resets persistence; the second warm episode must serve a fresh 900 seconds.",
      "inputs": {
        "chwst": [
          {
            "t": 0,
            "value": 8.0
          },
          {
            "t": 600,
            "value": 6.0
          },
          {
            "t": 900,
            "value": 8.0
          }
        ],
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "load_gate_drop_resets_timer",
      "description": "Dropping below the evaluability floor clears direction and resets persistence even while temperature remains warm.",
      "inputs": {
        "chwst": 8.0,
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": [
          {
            "t": 0,
            "value": 60.0
          },
          {
            "t": 600,
            "value": 10.0
          },
          {
            "t": 900,
            "value": 60.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "status_gate_drop_resets_timer",
      "description": "A stopped interval resets timing; restart into the same error requires the full duration again.",
      "inputs": {
        "chwst": 8.0,
        "chwst_sp": 6.0,
        "chiller_status": [
          {
            "t": 0,
            "value": true
          },
          {
            "t": 600,
            "value": false
          },
          {
            "t": 900,
            "value": true
          }
        ],
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "direct_direction_handoff_keeps_timer",
      "description": "A sampled warm-to-cold handoff changes diagnostic direction but the out-of-band OR never falls, so persistence continues.",
      "inputs": {
        "chwst": [
          {
            "t": 0,
            "value": 8.0
          },
          {
            "t": 600,
            "value": 4.0
          }
        ],
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yTooWarm",
          "from_s": 0,
          "to_s": 540,
          "equals": true
        },
        {
          "output": "yTooCold",
          "from_s": 660,
          "to_s": 3600,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 840,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 3600,
          "equals": true
        }
      ]
    },
    {
      "name": "threshold_chatter_never_persists",
      "description": "Alternating inside and outside the warm allowance exposes chatter but never accumulates 900 continuous seconds.",
      "inputs": {
        "chwst": [
          {
            "t": 0,
            "value": 6.9
          },
          {
            "t": 120,
            "value": 7.1
          },
          {
            "t": 240,
            "value": 6.9
          },
          {
            "t": 360,
            "value": 7.1
          },
          {
            "t": 480,
            "value": 6.9
          },
          {
            "t": 600,
            "value": 7.1
          },
          {
            "t": 720,
            "value": 6.9
          }
        ],
        "chwst_sp": 6.0,
        "chiller_status": true,
        "chiller_load": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3600,
          "equals": false
        }
      ]
    }
  ]
}