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-0008 — Chiller proof-of-operation failure

Statusverified — engine e2ff2f8, cxf:fnv1a128:aae864b287b6e0c975e77111d088cfd6, 2026-08-20
Severity2
Methodrule
Phase2
CategoryPROTECTIVE
ConfidenceHIGH
EstimationPROXY_ESTIMATION
G36
Clusters
Suppresses
Suppressed by
RelatedCHW-0007, CHW-0009
Playbookschiller-efficiency
SourceEPA 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 statesall 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 timer
  • yFailToStart — True after a final per-chiller start command remains unproved for start_proof_time
  • yUnexpectedRun — True after proven operation continues without a final per-chiller command for stop_proof_time

Parameters:

NameDefaultUnitCXF pathDescription
start_proof_time300.0sstartProof.delayTimeAllowed 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_time120.0sstopProof.delayTimeAllowed 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

CHW-0008 block graph

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:

  1. Chilled/condenser-water flow, valve, or pump permissive not established
  2. Active anti-recycle, oil-system, freeze, lift, current, or safety lockout
  3. Starter, VFD, compressor, control transformer, or disconnect failure
  4. Final stage command landed on the wrong machine
  5. Run-proof sensor, integration, or point freshness failure

yUnexpectedRun:

  1. Local/manual mode, service override, or second plant controller
  2. Welded contactor or command output stuck active
  3. Command bound upstream of chiller-internal logic
  4. 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=true on 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.

ScenarioDescription
agreement_stoppedCommand and proof both false are healthy agreement.
agreement_runningCommand and independent run proof both true are healthy agreement.
fail_to_start_at_initializationA standing start mismatch serves the full 300-second initialization-safe delay.
unexpected_run_at_initializationA standing stop mismatch serves the independent 120-second delay.
normal_delayed_start_and_stopOil-pump/permissive startup and normal unload/coast-down both complete inside their own windows.
start_recovers_on_timer_boundaryRun proof arriving exactly at the 300-second boundary clears the candidate before evaluation and never alarms.
stop_recovers_on_timer_boundaryStatus clearing exactly at the 120-second boundary prevents an unexpected-run alarm.
run_proof_lost_midcycleA normally running machine that loses proof begins the start-direction timer at the loss event.
command_chatter_resets_start_timerA start request that drops and returns restarts the continuous 300-second proof window.
pending_direction_reversal_uses_new_timerA 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_windowA mature unexpected-run alarm clears when the mismatch flips; fail-to-start then waits its separate 300 seconds.
status_chatter_never_maturesA proof signal alternating every minute never leaves either mismatch continuous for its timer.
mismatch_recovery_clears_alarmA 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
        }
      ]
    }
  ]
}