CHW-0008 — Chiller proof-of-operation failure
| Status | verified — engine e2ff2f8, cxf:fnv1a128:aae864b287b6e0c975e77111d088cfd6, 2026-08-20 |
| Severity | 2 |
| Method | rule |
| Phase | 2 |
| Category | PROTECTIVE |
| Confidence | HIGH |
| Estimation | PROXY_ESTIMATION |
| G36 | — |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | CHW-0007, CHW-0009 |
| Playbooks | chiller-efficiency |
| Source | EPA Facilities Manual, Volume 2, ch.9 Table 9-2 — chiller BAS monitoring includes per-equipment start/stop and failure; Library proof-of-operation precedents PMP-0003 and HW-0009 — independent command/status mismatch lanes with separate initialization-safe timers; Library-authored chiller timing adaptation; no cited source publishes 300 s start proof and 120 s stop proof as portable limits |
| Operating states | all states in which the final individual-machine stage command and independent chiller run proof are authoritative |
Preconditions (host-enforced): Bind chiller_cmd to the final per-machine request issued downstream of plant enable, lead/lag selection, normal anti-recycle logic, and applicable BAS interlocks; a plant enable or cooling demand cannot say which machine was requested. Bind chiller_status to independent proof that this chiller is actually producing cooling or operating a compressor, not availability, alarm-free, enable-ready, an echoed command, or a fleet OR. For modular/multi-circuit equipment, define the rule instance boundary consistently. Both points must be fresh and time-aligned, and each proof time must exceed worst-case delivery latency plus the normal sequence for that direction. Exclude approved anti-recycle lockout, pump/valve pre-run and post-run, oil-system preparation, shutdown unloading/coast-down, demand response, emergency or manufacturer safety actions, manual/local operation, maintenance, and functional tests. When ownership, authority, or mode is unknown the host reports NO_EVAL.
Points: chiller_cmd, chiller_status
Outputs:
yFault— True while either independent command/status mismatch has matured through its direction-specific proof timeryFailToStart— True after a final per-chiller start command remains unproved for start_proof_timeyUnexpectedRun— True after proven operation continues without a final per-chiller command for stop_proof_time
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
start_proof_time | 300.0 | s | startProof.delayTime | Allowed start/permissive sequence before failure-to-start. ADOPTED_TUNABLE: set longer than normal oil, valve, pump/flow, starter, and delivery latency but shorter than operator response. |
stop_proof_time | 120.0 | s | stopProof.delayTime | Allowed unload/coast-down after final command removal. ADOPTED_TUNABLE and independent of the start window. |
Description
This rule asks whether an individual chiller did what its final stage command requested. Commanded but unproved operation can indicate a failed permissive, locked-out machine, starter/drive fault, missing flow, or bad status. Proven operation after command removal can indicate manual/local control, welded hardware, a second controller, or a command bound upstream of the real owner.
Detection Logic
fail_to_start = chiller_cmd AND NOT chiller_status
unexpected_run = NOT chiller_cmd AND chiller_status
yFailToStart = fail_to_start sustained for start_proof_time
yUnexpectedRun = unexpected_run sustained for stop_proof_time
yFault = yFailToStart OR yUnexpectedRun
The two conditions are structurally mutually exclusive and each has its own
TrueDelay(delayOnInit=true). A mismatch that reverses direction therefore
clears the old flag and serves the new direction’s complete timer; time never
accumulates across lanes.
Possible Diagnoses
yFailToStart:
- Chilled/condenser-water flow, valve, or pump permissive not established
- Active anti-recycle, oil-system, freeze, lift, current, or safety lockout
- Starter, VFD, compressor, control transformer, or disconnect failure
- Final stage command landed on the wrong machine
- Run-proof sensor, integration, or point freshness failure
yUnexpectedRun:
- Local/manual mode, service override, or second plant controller
- Welded contactor or command output stuck active
- Command bound upstream of chiller-internal logic
- Normal unload/coast-down longer than the configured stop window
Energy Impact
PROTECTIVE with a direction-dependent proxy. Failure to start threatens cooling, humidity control, and low-flow/freeze protection but is not excess chiller kW. Unexpected operation can be sized host-side from the same machine’s measured power and mismatch duration; the graph itself reads no power.
Emissions Impact
Scope 2, proxy-only for unexpected run. Do not assign avoided electricity to the fail-to-start direction.
Deviations
- The timers are adopted, not source-transcribed. Chiller permissive and coast-down sequences vary materially; 300/120 s are commissioning starts.
- No rule-wide suppression targets CHW-0007. Fail-to-start makes tracking non-evaluable, but unexpected run can still be loaded and meaningfully fail tracking. Current metadata cannot express one directional suppression safely.
- The final command is downstream of ordinary anti-recycle behavior. If a BAS request is bound upstream, correct machine protection looks like failure.
delayOnInit=trueon both lanes prevents immediate alarms when commands are re-driven and statuses repopulate after a runtime restart.- One card severity/category cannot describe both directions perfectly. Severity 2 PROTECTIVE follows failure-to-start; hosts may route unexpected run as a lower urgency energy/override finding using the direction output.
- No EnergyPlus validation is claimed. Part-load ratio or power can proxy status, but the current model exposes no independent final per-chiller BAS stage command; fabricating it from status would make proof tautological.
- CLU-06 is unchanged. A command/proof disagreement is diagnosis ordering, not a member of the existing efficiency-triggered cluster.
Test Vectors
13 scenarios, clock step 60 s over 1800 s.
| Scenario | Description |
|---|---|
agreement_stopped | Command and proof both false are healthy agreement. |
agreement_running | Command and independent run proof both true are healthy agreement. |
fail_to_start_at_initialization | A standing start mismatch serves the full 300-second initialization-safe delay. |
unexpected_run_at_initialization | A standing stop mismatch serves the independent 120-second delay. |
normal_delayed_start_and_stop | Oil-pump/permissive startup and normal unload/coast-down both complete inside their own windows. |
start_recovers_on_timer_boundary | Run proof arriving exactly at the 300-second boundary clears the candidate before evaluation and never alarms. |
stop_recovers_on_timer_boundary | Status clearing exactly at the 120-second boundary prevents an unexpected-run alarm. |
run_proof_lost_midcycle | A normally running machine that loses proof begins the start-direction timer at the loss event. |
command_chatter_resets_start_timer | A start request that drops and returns restarts the continuous 300-second proof window. |
pending_direction_reversal_uses_new_timer | A start mismatch reverses to an unexpected-run mismatch before maturity; the old lane resets and the new lane serves 120 seconds. |
mature_direction_reversal_reserves_new_window | A mature unexpected-run alarm clears when the mismatch flips; fail-to-start then waits its separate 300 seconds. |
status_chatter_never_matures | A proof signal alternating every minute never leaves either mismatch continuous for its timer. |
mismatch_recovery_clears_alarm | A matured fail-to-start output clears immediately when independent status proves run. |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 60,
"horizon_s": 1800
},
"scenarios": [
{
"name": "agreement_stopped",
"description": "Command and proof both false are healthy agreement.",
"inputs": {
"chiller_cmd": false,
"chiller_status": false
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "agreement_running",
"description": "Command and independent run proof both true are healthy agreement.",
"inputs": {
"chiller_cmd": true,
"chiller_status": true
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "fail_to_start_at_initialization",
"description": "A standing start mismatch serves the full 300-second initialization-safe delay.",
"inputs": {
"chiller_cmd": true,
"chiller_status": false
},
"expect": [
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 360,
"to_s": 1800,
"equals": true
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yFault",
"from_s": 360,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "unexpected_run_at_initialization",
"description": "A standing stop mismatch serves the independent 120-second delay.",
"inputs": {
"chiller_cmd": false,
"chiller_status": true
},
"expect": [
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 60,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 180,
"to_s": 1800,
"equals": true
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 60,
"equals": false
},
{
"output": "yFault",
"from_s": 180,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "normal_delayed_start_and_stop",
"description": "Oil-pump/permissive startup and normal unload/coast-down both complete inside their own windows.",
"inputs": {
"chiller_cmd": [
{
"t": 0,
"value": false
},
{
"t": 300,
"value": true
},
{
"t": 900,
"value": false
}
],
"chiller_status": [
{
"t": 0,
"value": false
},
{
"t": 540,
"value": true
},
{
"t": 960,
"value": false
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "start_recovers_on_timer_boundary",
"description": "Run proof arriving exactly at the 300-second boundary clears the candidate before evaluation and never alarms.",
"inputs": {
"chiller_cmd": true,
"chiller_status": [
{
"t": 0,
"value": false
},
{
"t": 300,
"value": true
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "stop_recovers_on_timer_boundary",
"description": "Status clearing exactly at the 120-second boundary prevents an unexpected-run alarm.",
"inputs": {
"chiller_cmd": false,
"chiller_status": [
{
"t": 0,
"value": true
},
{
"t": 120,
"value": false
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "run_proof_lost_midcycle",
"description": "A normally running machine that loses proof begins the start-direction timer at the loss event.",
"inputs": {
"chiller_cmd": true,
"chiller_status": [
{
"t": 0,
"value": true
},
{
"t": 600,
"value": false
}
]
},
"expect": [
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 960,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "command_chatter_resets_start_timer",
"description": "A start request that drops and returns restarts the continuous 300-second proof window.",
"inputs": {
"chiller_cmd": [
{
"t": 0,
"value": true
},
{
"t": 180,
"value": false
},
{
"t": 300,
"value": true
}
],
"chiller_status": false
},
"expect": [
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 540,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 660,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "pending_direction_reversal_uses_new_timer",
"description": "A start mismatch reverses to an unexpected-run mismatch before maturity; the old lane resets and the new lane serves 120 seconds.",
"inputs": {
"chiller_cmd": [
{
"t": 0,
"value": true
},
{
"t": 180,
"value": false
}
],
"chiller_status": [
{
"t": 0,
"value": false
},
{
"t": 180,
"value": true
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 360,
"to_s": 1800,
"equals": true
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "mature_direction_reversal_reserves_new_window",
"description": "A mature unexpected-run alarm clears when the mismatch flips; fail-to-start then waits its separate 300 seconds.",
"inputs": {
"chiller_cmd": [
{
"t": 0,
"value": false
},
{
"t": 600,
"value": true
}
],
"chiller_status": [
{
"t": 0,
"value": true
},
{
"t": 600,
"value": false
}
]
},
"expect": [
{
"output": "yUnexpectedRun",
"from_s": 180,
"to_s": 540,
"equals": true
},
{
"output": "yUnexpectedRun",
"from_s": 660,
"to_s": 1800,
"equals": false
},
{
"output": "yFault",
"from_s": 180,
"to_s": 540,
"equals": true
},
{
"output": "yFault",
"from_s": 660,
"to_s": 840,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 960,
"to_s": 1800,
"equals": true
},
{
"output": "yFault",
"from_s": 960,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "status_chatter_never_matures",
"description": "A proof signal alternating every minute never leaves either mismatch continuous for its timer.",
"inputs": {
"chiller_cmd": true,
"chiller_status": [
{
"t": 0,
"value": true
},
{
"t": 60,
"value": false
},
{
"t": 120,
"value": true
},
{
"t": 180,
"value": false
},
{
"t": 240,
"value": true
},
{
"t": 300,
"value": false
},
{
"t": 360,
"value": true
},
{
"t": 420,
"value": false
},
{
"t": 480,
"value": true
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "mismatch_recovery_clears_alarm",
"description": "A matured fail-to-start output clears immediately when independent status proves run.",
"inputs": {
"chiller_cmd": true,
"chiller_status": [
{
"t": 0,
"value": false
},
{
"t": 900,
"value": true
}
]
},
"expect": [
{
"output": "yFailToStart",
"from_s": 360,
"to_s": 840,
"equals": true
},
{
"output": "yFailToStart",
"from_s": 960,
"to_s": 1800,
"equals": false
},
{
"output": "yFault",
"from_s": 360,
"to_s": 840,
"equals": true
},
{
"output": "yFault",
"from_s": 960,
"to_s": 1800,
"equals": false
}
]
}
]
}