PMP-0002 — Pump deadheading (high DP, low/no flow)
| Status | verified — engine e2ff2f8, cxf:fnv1a128:cdf8042c408d62c7d323457a8f2103ac, 2026-08-17 |
| Severity | 2 |
| Method | rule |
| Phase | 2 |
| Category | PROTECTIVE |
| Confidence | HIGH |
| Estimation | DIRECT_MEASUREMENT |
| G36 | — |
| Clusters | — |
| Suppresses | PMP-0001 |
| Suppressed by | — |
| Related | PMP-0001, PMP-0005, PMP-0006, VFD-0002 |
| Playbooks | vfd-pump-faults |
| Source | HVAC FDD Reference v1.0 §15 (ch. ‘Pumps’, pdf pp. 134-135), PMP-0002; Engineering best practice |
| Operating states | pump proven running |
Preconditions (host-enforced): pump_dp must be the differential pressure ACROSS THIS PUMP — discharge minus suction — and the point dictionary marks it provisional for exactly this reason. A loop or decoupler DP bound here breaks the rule rather than degrading it: depending on where the taps sit, a deadhead can read high, unchanged, or low, and in the last case the rule is silent forever on the fault it exists to find. Confirm the tap location at binding review. deadhead_dp_threshold must then be set from this pump’s curve and deadhead_flow_threshold from this loop’s design flow; both shipped values are placeholders (see Deviations), and the DP one is the more dangerous of the two because the reference’s 150%-of-design multiplier is above the shutoff head of many pumps. All three points must belong to the same pump; on a headered set a common loop flow meter or a header DP tap bound to each pump makes the rule read one machine’s hydraulics onto another. The pump must be proven running by rotation — a current switch or drive feedback — and when pump_status is false this rule has no verdict at all: the host reports NO_EVAL, not healthy. There is no in-rule evaluability output (see Deviations), so that gate is the host’s to enforce.
Points: pump_status, pump_dp, pump_flow
Outputs:
yFault— True while the pump has been proven running with its differential pressure above deadhead_dp_threshold and its flow below deadhead_flow_threshold, continuously for at least alarm_delay
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
deadhead_dp_threshold | 300.0 | kPa | dpHigh.t | Differential pressure across the pump above which it is working against a closed system. PER-PUMP SITE CONFIGURATION — the reference states 150% of design head and the rule carries absolute units, so the shipped 300.0 kPa is 150% of a 200 kPa (≈20 m) design head and means nothing on any other pump. Set it from the pump curve, not from the multiplier (see Deviations) |
deadhead_flow_threshold | 2.0 | L/s | lowFlow.t | Flow below which the pump is delivering nothing useful. PER-LOOP SITE CONFIGURATION — the reference states 10% of design flow; the shipped 2.0 L/s is 10% of the same 20 L/s design flow PMP-0001’s placeholder assumes, which keeps the pair’s 5%/10% relationship intact at the defaults |
alarm_delay | 300.0 | s | persist.delayTime | Continuous violation required before the alarm asserts (5 min). ADOPTED — the reference’s tunables line for this card truncates mid-sentence and its equation states no persistence at all; 300 s is the sibling PMP-0001’s published AlarmDelay (see Deviations) |
Description
A deadheaded pump is running against a closed system. The discharge valve, or every terminal valve on the loop, or a check valve someone installed backwards, leaves the water nowhere to go, so the pump rides up its curve to shutoff head and recirculates the same volute of water until that water boils. It is the fault on this family’s list that damages hardware fastest — the mechanical seal loses the flow that cools it, the bearings take the radial thrust a pump far off its best efficiency point produces — and the reference prices the outcome at $5,000 to $20,000. The signature is the pair of readings, not either alone: high DP by itself is a loop at high head, low flow by itself is PMP-0001’s condition. Together they mean downstream resistance went up, which is the definition of deadheading and why this card carries HIGH confidence where its sibling carries MEDIUM.
Detection Logic
dp_high = pump_dp > deadhead_dp_threshold
low_flow = pump_flow < deadhead_flow_threshold
yFault = (pump_status AND dp_high AND low_flow)
sustained continuously for alarm_delay
Block graph (rule.cxf.jsonld):
dpHigh and lowFlow are the two comparisons, hydraulic conjoins them into
the deadhead signature, gate adds the reference’s pump_status conjunct, and
persist measures the duration. The nesting is only structural — CDL’s
Logical.And takes two inputs — but it groups the graph the way the physics
does, hydraulic evidence on one side and run proof on the other.
Both comparisons are strict, so a DP sitting exactly on its threshold is not
high and a flow sitting exactly on its threshold is not low; each boundary is
pinned from three sides. persist requires five continuous minutes, which is
what separates a deadheading pump from the ordinary event that produces the
identical trace for half that: every zone valve on a loop closing together at
the end of a setback, or a two-way control valve stroking shut while its
neighbour has not yet opened. Any moment where the flow returns or the DP falls
back drops the timer, so the alarm always describes one continuous episode.
There is no evaluability output. Both hydraulic terms are direct comparisons on
bound inputs and the run term is a bound input — exposing pump_status as
yStatusOk would echo a point the host already has, which SCHEMA.md’s
boundary-output convention exists to prevent. The NO_EVAL cases therefore live
entirely in preconditions: a stopped pump, a pump in hand, and any binding
where pump_dp is not measured across the pump.
Possible Diagnoses
Transcribed from the reference’s PMP-0002 card:
- Downstream isolation valve closed — the single-valve case, usually left shut after service on a branch, and the cheapest of the four to fix
- Severe system blockage — a plugged strainer basket after a piping repair, or debris carried into a reducer; the playbook’s step 2.4 checks the strainer before anything is disassembled
- Check valve installed backwards — a commissioning error rather than a failure, worth suspecting on a pump that has never made flow since it was installed or repaired
- All terminal unit valves closed — nothing is broken at all. The loop is at no load with no minimum-flow path, which is a control or design finding: a DP reset that never trims, a missing bypass, or a lead pump that should have staged off. The common one on a variable-primary chilled water plant
The first three are field failures on one pump; the fourth is a plant-sequencing problem that will recur on every pump in the building, and the distinction is available before anyone goes to site by asking whether the loop was at genuine no load when the alarm started. The DP term is also what makes this rule diagnostic rather than merely detective: a failed impeller or a sheared coupling makes no head while making no flow, and a deadheaded pump makes its maximum.
Energy Impact
PROTECTIVE, HIGH confidence, DIRECT_MEASUREMENT. The energy line is the sibling’s — 100% of the pump’s draw while the condition lasts, since none of it is moving water anywhere — but the number that matters to an owner is the $5K–$20K of seal and bearing damage the reference attaches to this card and not to PMP-0001: a pump can deadhead for an afternoon and cost a few dollars of electricity and a mechanical seal. HIGH confidence is the reference’s rating and is earned by the second term, two independent measurements agreeing on one hydraulic story, though it survives only one direction of sensor failure (see Deviations). Climate sensitivity is neutral.
Emissions Impact
Scope 2, DIRECT_EMISSIONS, HIGH confidence; the reference’s typical range is 100–500 kg CO₂e/yr for pump energy plus the equipment damage risk, on a marginal operating emissions rate (MOER) basis. Pump motors are electric, so the scope assignment does not vary with the plant the way a heating fault’s does. The embodied emissions of a replaced pump end are outside the range and outside this rule’s reach, and are plausibly the larger number over a decade of a recurring deadhead nobody diagnosed.
Deviations
- The reference’s 150%-of-design-head multiplier is above the shutoff head of many pumps, and this card says so rather than endorsing it. Shutoff head is typically only 110–130% of head at the design point, so a threshold set at 150% exceeds the highest pressure the pump can produce and the rule is silent by construction. Retune to the curve’s shutoff head less a margin (commonly 105–120% of design), or above a reset sequence’s maximum setpoint.
deadhead_dp_thresholdships an absolute placeholder. The reference states a percentage and the rule carries kPa, because CDL parameters carry units and this library does no unit conversion in v1; the point dictionary is canonical on it. The shipped 300.0 kPa is 150% of a 200 kPa (≈20 m) design head and is arbitrary on any other pump. Hosts MUST set it per pump, and per the bullet above, from the curve rather than from the multiplier.deadhead_flow_thresholdships an absolute placeholder too, at twice PMP-0001’s. 2.0 L/s is 10% of the same notional 20 L/s design flow that card’s 1.0 L/s assumes, so the pair’s published 5%/10% relationship survives at the defaults and there is a band — 5% to 10% of design — where a high-DP pump trips this rule and not that one. Hosts retuning one threshold should retune both from the same design flow.alarm_delayhas no published value, and no published existence. The reference’s equation for this card is the bare three-term conjunction with no “sustained for” clause, and its tunables line ends mid-sentence — the same truncation artifact VFD-0002’s line carries. Both the persistence and its value are therefore ADOPTED, at PMP-0001’s published 300 s: same chapter, same family, same physical event from a second angle. VFD-0002’s 900 s was rejected — fifteen minutes is a control-loop response window, and a pump running its seal dry does not have fifteen minutes. Hosts should shorten rather than lengthen it.- The DP tap location is an assumption, and binding the wrong point inverts the
rule. The dictionary marks
pump_dpprovisional for this reason: across the pump, a deadhead reads high; across the loop or a decoupler it may read high or low depending on where the taps sit. This card assumes the across-the-pump reading. A loop-DP binding fails silently — the rule simply never fires — and a header-DP binding can be loud in the wrong direction, as a stopped standby pump whose tap sees the running pump’s header pressure has high DP and no flow, with only thepump_statusconjunct keeping it quiet. Binding review owns this; no logic can detect it. - No evaluability output, deliberately.
pump_statusis a boundary input, so exposing it would echo a point the host already reads — the case SCHEMA.md’s convention rules out — and neither hydraulic term is a derived quantity a host could not compute for itself. Contrast PMP-0001’syRunOk, a conjunction held for a delay, and VFD-0001’syCmdOk. suppresses: [PMP-0001]is an authored relationship, not the reference’s — neither card declares suppression. Both fire on one physical event and this one is the specific diagnosis where PMP-0001 is the general condition. The direction matters more than usual because the general card’s diagnosis list is wrong for this fault: impeller failure, air lock and a broken coupling all produce low DP, so leaving both alarms up sends a technician to open a volute on a pump whose isolation valve is shut. Precedent for authoring the edge: the VFD-0001/VFD-0002 pair.- A DP transmitter reading high and a flow meter reading zero produce this fault exactly. Two failed sensors are less likely than one, which is why this card outranks its sibling on confidence, but the rule has no third measurement and the failure is not exotic: a plugged DP tap reads whatever pressure it last saw, and several flow meter types read zero when they lose signal. Motor current settles it in a minute — a deadheaded centrifugal pump draws noticeably less than at design, not more.
- A constant-speed pump deadheads differently from a variable-speed one. On
DP control, closing valves drives the measured pressure up and the drive slows
to its minimum, so the DP this rule finally sees is shutoff head at minimum
speed, which can sit well below a threshold derived from a full-speed curve. A
second, quieter reason the shipped multiplier can leave the rule silent, and an
argument for deriving the threshold from the DP setpoint’s maximum on any
drive-controlled loop. VFD-0002 is
relatedfor this reason. - Strict comparisons at both thresholds. The reference writes
>and<too, and CDLRealshas neitherGreaterEqualnorLessEqualin any case. A DP of exactly 300.0 kPa is not high and a flow of exactly 2.0 L/s is not low; both disagreements have measure zero and both err toward silence. - Persistence stands in for averaging. The rule consumes instantaneous points
and the reference specifies no averaging. A loop cycling in and out of a
deadhead faster than
alarm_delay— a hunting control valve, a pump staging against a badly tuned bypass — is a real finding this rule cannot make, the same blind spot PMP-0001 documents. - The energy formula’s inputs are not this rule’s inputs.
waste_kw = pump_rated_kw × (pump_speed/100)³needs nameplate power and drive speed and the pump dictionary carries neither, so the host supplies both. Transcribed unchanged otherwise, including its identity with PMP-0001’s. - The chapter number is uncertain. The reference’s page headers label Energy
Recovery, Pumps and Variable Frequency Drives all as “Ch. 15”, which cannot be
right for all three.
sourcefollows the VFD cards’ §15 and names the chapter title and page range so the citation resolves regardless. g36is null and no G36 provenance is claimed. The reference sources this card to engineering best practice alone — unlike PMP-0001, which cites G36 alarm patterns — sosourcesays only what the reference says.persist.delayOnInit = true(CDL default isfalse), the library’s standing choice: a pump already deadheading at controller restart waits out the full five minutes rather than alarming on the first tick.- The reference publishes a vector table for PMP-0001 and none for this card,
so every scenario in
vectors.jsonis library-authored.clustersis empty:clusters/clusters.jsondefines no cluster containing a pump rule, and this card does not edit the cluster set.
Notes
Treat this card as the pair’s head. When both pump rules are firing, this is the
finding and PMP-0001 is its shadow — the two diagnosis lists point in opposite
physical directions and only one of them fits a pump making full head. Before
deploying on a fleet, do two things that cost one trend each: confirm where the
DP transmitter is tapped, and compare deadhead_dp_threshold against the pump
curve’s shutoff head. The
vfd-pump-faults playbook’s step 2 is the
service order, and its first two entries are remote — the DP setpoint may simply
be too high, and a loop with no DP reset sequence pumps against closed valves by
design (EEM-10, 0.5–2% of site energy). That playbook’s header and the chapter
README both still list the pump family as future work; both belong to other
owners to correct.
Test Vectors
15 scenarios, clock step 60 s over 1800 s.
| Scenario | Description |
|---|---|
normal_operation | Pump running, 200 kPa across it, 8.0 L/s moving. Below the DP line and well above the flow line — the ordinary state of a healthy loop. |
deadheading | The fault: pump running, 350 kPa across it, zero flow. Every terminal valve is shut and the pump is churning water into heat. The alarm lands at exactly 300 s. |
dp_exactly_at_threshold | Boundary: DP sits exactly on deadhead_dp_threshold (300.0 kPa) with no flow. CDL Reals has no GreaterEqual, so the comparison is strict and exactly 300.0 is not high DP. |
dp_just_above_threshold | Boundary from above: 300.1 kPa clears the strict comparison and alarms on the ordinary 300 s schedule. |
dp_just_below_threshold | Boundary from below: 299.9 kPa with zero flow is silent here. A pump that has lost its impeller sits in this region, and PMP-0001 is the rule that reports it. |
flow_exactly_at_threshold | Boundary: flow sits exactly on deadhead_flow_threshold (2.0 L/s) against high DP. Strict LessThreshold, so exactly 2.0 is not low flow. |
flow_just_below_threshold | Boundary from below: 1.9 L/s against high DP is a partial deadhead — a couple of bypass paths still open — and it alarms. |
flow_just_above_threshold | Boundary from above: 2.1 L/s against the same high DP never alarms. |
stopped_pump_reading_header_dp | A standby pump, stopped, with its DP tap reading 350 kPa of header pressure the running pump is making, and no flow of its own. The status conjunct is the only thing standing between that geometry and a permanent false alarm — which is exactly why the point dictionary marks pump_dp provisional. |
impeller_failure_signature | Pump running, no flow, and only 40 kPa across it: the pump is turning and making no head. Broken coupling, failed impeller, or an air lock. Silent here and loud on PMP-0001 — the DP term is what separates the two diagnoses, and this is the vector that proves it. |
high_dp_at_normal_flow | 350 kPa with 8.0 L/s moving: a loop simply running at high head, which is a DP-reset opportunity and not a fault. Both conjuncts are required and only one holds. |
valve_closure_transient | A four-minute deadhead at t=300 s — every zone valve closing together on a morning setback, then reopening. Shorter than alarm_delay, so nothing fires and the accumulated time is discarded on the release. |
deadhead_starts_mid_run | A healthy loop deadheads at t=600 s. The alarm lands at exactly 900 s — alarm_delay measured from the rising edge, not from init. |
valves_open_after_alarm | Recovery: the alarm asserts at 300 s and someone opens the isolation valve at t=900 s. DP falls, flow returns, and yFault drops on that tick. |
status_drops_after_alarm | A deadheading pump trips out at t=900 s — which is what a deadheaded pump eventually does. yFault goes false with the status and the host must read that as NO_EVAL, not as a resolved fault; the DP and flow signature has not changed at all. |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 60,
"horizon_s": 1800
},
"scenarios": [
{
"name": "normal_operation",
"description": "Pump running, 200 kPa across it, 8.0 L/s moving. Below the DP line and well above the flow line \u2014 the ordinary state of a healthy loop.",
"inputs": {
"pump_status": true,
"pump_dp": 200.0,
"pump_flow": 8.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "deadheading",
"description": "The fault: pump running, 350 kPa across it, zero flow. Every terminal valve is shut and the pump is churning water into heat. The alarm lands at exactly 300 s.",
"inputs": {
"pump_status": true,
"pump_dp": 350.0,
"pump_flow": 0.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yFault",
"from_s": 300,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "dp_exactly_at_threshold",
"description": "Boundary: DP sits exactly on deadhead_dp_threshold (300.0 kPa) with no flow. CDL Reals has no GreaterEqual, so the comparison is strict and exactly 300.0 is not high DP.",
"inputs": {
"pump_status": true,
"pump_dp": 300.0,
"pump_flow": 0.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "dp_just_above_threshold",
"description": "Boundary from above: 300.1 kPa clears the strict comparison and alarms on the ordinary 300 s schedule.",
"inputs": {
"pump_status": true,
"pump_dp": 300.1,
"pump_flow": 0.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yFault",
"from_s": 300,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "dp_just_below_threshold",
"description": "Boundary from below: 299.9 kPa with zero flow is silent here. A pump that has lost its impeller sits in this region, and PMP-0001 is the rule that reports it.",
"inputs": {
"pump_status": true,
"pump_dp": 299.9,
"pump_flow": 0.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "flow_exactly_at_threshold",
"description": "Boundary: flow sits exactly on deadhead_flow_threshold (2.0 L/s) against high DP. Strict LessThreshold, so exactly 2.0 is not low flow.",
"inputs": {
"pump_status": true,
"pump_dp": 350.0,
"pump_flow": 2.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "flow_just_below_threshold",
"description": "Boundary from below: 1.9 L/s against high DP is a partial deadhead \u2014 a couple of bypass paths still open \u2014 and it alarms.",
"inputs": {
"pump_status": true,
"pump_dp": 350.0,
"pump_flow": 1.9
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yFault",
"from_s": 300,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "flow_just_above_threshold",
"description": "Boundary from above: 2.1 L/s against the same high DP never alarms.",
"inputs": {
"pump_status": true,
"pump_dp": 350.0,
"pump_flow": 2.1
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "stopped_pump_reading_header_dp",
"description": "A standby pump, stopped, with its DP tap reading 350 kPa of header pressure the running pump is making, and no flow of its own. The status conjunct is the only thing standing between that geometry and a permanent false alarm \u2014 which is exactly why the point dictionary marks pump_dp provisional.",
"inputs": {
"pump_status": false,
"pump_dp": 350.0,
"pump_flow": 0.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "impeller_failure_signature",
"description": "Pump running, no flow, and only 40 kPa across it: the pump is turning and making no head. Broken coupling, failed impeller, or an air lock. Silent here and loud on PMP-0001 \u2014 the DP term is what separates the two diagnoses, and this is the vector that proves it.",
"inputs": {
"pump_status": true,
"pump_dp": 40.0,
"pump_flow": 0.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "high_dp_at_normal_flow",
"description": "350 kPa with 8.0 L/s moving: a loop simply running at high head, which is a DP-reset opportunity and not a fault. Both conjuncts are required and only one holds.",
"inputs": {
"pump_status": true,
"pump_dp": 350.0,
"pump_flow": 8.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "valve_closure_transient",
"description": "A four-minute deadhead at t=300 s \u2014 every zone valve closing together on a morning setback, then reopening. Shorter than alarm_delay, so nothing fires and the accumulated time is discarded on the release.",
"inputs": {
"pump_status": true,
"pump_dp": [
{
"t": 0,
"value": 200.0
},
{
"t": 300,
"value": 350.0
},
{
"t": 540,
"value": 200.0
}
],
"pump_flow": [
{
"t": 0,
"value": 8.0
},
{
"t": 300,
"value": 0.5
},
{
"t": 540,
"value": 8.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "deadhead_starts_mid_run",
"description": "A healthy loop deadheads at t=600 s. The alarm lands at exactly 900 s \u2014 alarm_delay measured from the rising edge, not from init.",
"inputs": {
"pump_status": true,
"pump_dp": [
{
"t": 0,
"value": 200.0
},
{
"t": 600,
"value": 350.0
}
],
"pump_flow": [
{
"t": 0,
"value": 8.0
},
{
"t": 600,
"value": 0.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 900,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "valves_open_after_alarm",
"description": "Recovery: the alarm asserts at 300 s and someone opens the isolation valve at t=900 s. DP falls, flow returns, and yFault drops on that tick.",
"inputs": {
"pump_status": true,
"pump_dp": [
{
"t": 0,
"value": 350.0
},
{
"t": 900,
"value": 180.0
}
],
"pump_flow": [
{
"t": 0,
"value": 0.0
},
{
"t": 900,
"value": 8.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yFault",
"from_s": 300,
"to_s": 840,
"equals": true
},
{
"output": "yFault",
"from_s": 900,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "status_drops_after_alarm",
"description": "A deadheading pump trips out at t=900 s \u2014 which is what a deadheaded pump eventually does. yFault goes false with the status and the host must read that as NO_EVAL, not as a resolved fault; the DP and flow signature has not changed at all.",
"inputs": {
"pump_status": [
{
"t": 0,
"value": true
},
{
"t": 900,
"value": false
}
],
"pump_dp": 350.0,
"pump_flow": 0.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yFault",
"from_s": 300,
"to_s": 840,
"equals": true
},
{
"output": "yFault",
"from_s": 900,
"to_s": 1800,
"equals": false
}
]
}
]
}