PMP-0005 — Flow through stopped pump
| Status | verified — engine e2ff2f8, cxf:fnv1a128:d567fd02bea2ecc336bdf44f7680de4a, 2026-08-20 |
| Severity | 2 |
| Method | rule |
| Phase | 2 |
| Category | PROTECTIVE |
| Confidence | MEDIUM |
| Estimation | QUALITATIVE_ONLY |
| G36 | — |
| Clusters | — |
| Suppresses | — |
| Suppressed by | PMP-0003 |
| Related | PMP-0001, PMP-0002, PMP-0003 |
| Playbooks | vfd-pump-faults |
| Source | DOE/Hydraulic Institute, Improving Pumping System Performance: A Sourcebook for Industry, 2nd ed., PDF p.9 / printed p.6 — discharge check valves prevent reversal while a pump is stopped; Library-authored signed branch-flow adaptation; no cited source publishes the shipped flow or persistence thresholds; Library precedents PMP-0003 (proof/status timing), SYS-0008 (mirrored signed direction outputs), and VFD-0003 (strict directional comparisons) |
| Operating states | all states in which this pump branch should be hydraulically isolated whenever pump_status is false |
Preconditions (host-enforced): pump_flow must be individual-branch flow for this exact pump; a common-header point cannot distinguish which branch is passing. Full direction semantics require signed flow with positive defined from suction to discharge. A nonnegative magnitude sensor can support yFault, but neither yForwardFlow nor yReverseFlow is then physically trustworthy and the host must mark both direction labels unavailable. The zero offset and uncertainty must be known, and stopped_flow_threshold must exceed them. pump_status must be fresh independent proof; an active same-pump PMP-0003 suppresses this verdict because the stopped/running premise is unreliable. Exclude intentional bypass/gravity paths, thermosiphon designs, maintenance flushing, free cooling, and approved parallel-pump transfer. When scope, proof, or operating intent is unknown the host reports NO_EVAL.
Points: pump_status, pump_flow
Outputs:
yFault— True while stopped-branch flow in either signed direction has persisted beyond sustained_durationyForwardFlow— Diagnostic direction flag — true while a stopped pump has signed positive suction-to-discharge flow above the allowance; unavailable on magnitude-only bindings and false is not an evaluability verdictyReverseFlow— Diagnostic direction flag — true while a stopped pump has signed negative flow beyond the allowance; unavailable on magnitude-only bindings
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
stopped_flow_threshold | 1.0 | L/s | forward.t, reverse.t | Absolute branch-flow allowance in either direction. NO_PORTABLE_DEFAULT: 1.0 L/s is an adoption-blocking placeholder that must be set above sensor noise/zero drift and against this branch’s design flow. |
sustained_duration | 300.0 | s | persist.delayTime | Continuous stopped-flow duration required before alarm. ADOPTED_TUNABLE; five minutes rejects valve-transfer and coast-down transients but is not a published universal value. |
Description
This rule detects material water flow through an individual pump branch while that pump’s independent run proof is false. Forward flow can indicate a passing or missing check valve, parallel-header pressure, thermosiphoning, or bad proof; reverse flow adds the risk of reverse rotation. The rule names the observed hydraulic signature, not which component caused it.
Detection Logic
forward = pump_flow > stopped_flow_threshold
reverse = -pump_flow > stopped_flow_threshold
yForwardFlow = NOT pump_status AND forward
yReverseFlow = NOT pump_status AND reverse
candidate = yForwardFlow OR yReverseFlow
yFault = candidate sustained for sustained_duration
Both comparisons are strict, so exactly ±1.0 L/s is clear at the defaults.
Direction outputs are raw diagnostic detail gated by stopped status; they are
not evaluability flags. TrueDelay(delayOnInit=true) applies to the OR of both
directions. A direct forward-to-reverse handoff therefore preserves the timer
because material stopped flow never ceased; an actual in-band interval resets
it, and recovery clears immediately.
Possible Diagnoses
- Passing, failed, reversed, or missing discharge check valve
- Reverse flow driven by another pump on a common header
- Thermosiphoning or a gravity path not represented in the operating gate
- Pump run-proof failure or stale false status (PMP-0003)
- Flow sensor zero error, sign inversion, or common-header misbinding
- Isolation or bypass valve left open
- Approved flushing, free-cooling, or transfer sequence not excluded
Energy Impact
PROTECTIVE, MEDIUM confidence, QUALITATIVE_ONLY. Unintended branch flow can waste active-pump head, transport unwanted heat, defeat staging, and rotate a stopped pump backward. Magnitude depends on loop pressure and temperatures that this graph does not consume.
Emissions Impact
Scope 2, qualitative. Avoided electricity and thermal conditioning are site-specific and require pressure, temperature, and active-equipment context.
Deviations
- The rule adds a signed convention without narrowing older consumers.
Positive is suction-to-discharge. Magnitude-only branch flow still supports
yFaultand PMP-0001/0002, but neither direction label; common-header flow is not a weaker proxy but the wrong measurement scope. - Both thresholds are site configured. The 1.0 L/s and 300 s defaults are adopted executable placeholders, not manufacturer or standard limits.
- Direction reversal does not reset persistence. The timer watches absolute stopped-flow candidacy; continuous material flow remains one hydraulic event even when its sign changes. Vectors pin this explicitly.
- PMP-0003 suppression is host-side and same-pump only. A proof mismatch invalidates the stopped premise; the raw graph still alarms, preserving the evidence and avoiding command/status inputs that do not belong here.
- No Pump Delivery Failure cluster is added. Running-with-no-flow, deadheading, proof mismatch, and stopped-with-flow have incompatible premises and no shared trigger whose correction reliably clears all members.
Test Vectors
21 scenarios, clock step 60 s over 2400 s.
| Scenario | Description |
|---|---|
stopped_zero_flow | A stopped branch at zero flow is healthy. |
stopped_positive_flow | Forward branch flow present at initialization exposes direction immediately and faults only after persistence. |
stopped_negative_flow | Signed reverse flow follows the independent reverse diagnostic lane. |
running_with_flow | Flow is expected while the pump is proven running, so both stopped-flow lanes are gated off. |
running_with_negative_flow | Even signed reverse flow is outside this stopped-pump rule while run proof is true; other hydraulic/proof rules own the condition. |
running_zero_flow_is_other_rule | A running pump with zero flow is silent here and belongs to PMP-0001/PMP-0002. |
positive_exact_threshold_clear | The strict comparator leaves +1.0 L/s clear. |
negative_exact_threshold_clear | The strict comparator leaves -1.0 L/s clear. |
positive_just_over_threshold | One representable test increment above the forward threshold follows the full delay. |
negative_just_over_threshold | One test increment beyond the reverse threshold follows the full delay. |
violation_ends_at_delay_boundary | Flow clears on the 300-second timer boundary before evaluation and never alarms. |
two_short_episodes_reset | Two sub-delay forward-flow episodes separated by a clear tick do not accumulate. |
short_flow_transient | A single forward-flow episode shorter than persistence exposes direction but never alarms. |
flow_recovery_resets_timer | An in-band interval resets persistence; the second stopped-flow episode serves a new full 300 seconds. |
pump_start_resets_timer | Run proof temporarily gates off both directions and resets the timer even while branch flow stays high. |
two_opposite_short_episodes | Positive and negative sub-delay episodes separated by an in-band interval do not accumulate across direction lanes. |
direction_handoff_keeps_candidate_true | A direct forward-to-reverse handoff changes diagnostic lane but not absolute-flow candidacy, so the timer continues. |
recovery_after_alarm | Falling edges pass immediately when stopped-branch flow returns inside the band. |
noise_inside_zero_band | Signed zero drift that stays inside the allowance never raises a direction flag. |
threshold_chatter_resets_delay | Flow alternating just below and above the forward threshold chatters the diagnostic but never accumulates 300 continuous seconds. |
proof_mismatch_raw_fault_host_suppresses | False status with real flow is indistinguishable from stopped-branch flow in this graph; an active same-pump PMP-0003 suppresses the raw alarm host-side. |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 60,
"horizon_s": 2400
},
"scenarios": [
{
"name": "stopped_zero_flow",
"description": "A stopped branch at zero flow is healthy.",
"inputs": {
"pump_status": false,
"pump_flow": 0.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yReverseFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "stopped_positive_flow",
"description": "Forward branch flow present at initialization exposes direction immediately and faults only after persistence.",
"inputs": {
"pump_status": false,
"pump_flow": 2.0
},
"expect": [
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 2400,
"equals": true
},
{
"output": "yReverseFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yFault",
"from_s": 360,
"to_s": 2400,
"equals": true
}
]
},
{
"name": "stopped_negative_flow",
"description": "Signed reverse flow follows the independent reverse diagnostic lane.",
"inputs": {
"pump_status": false,
"pump_flow": -2.0
},
"expect": [
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yReverseFlow",
"from_s": 0,
"to_s": 2400,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yFault",
"from_s": 360,
"to_s": 2400,
"equals": true
}
]
},
{
"name": "running_with_flow",
"description": "Flow is expected while the pump is proven running, so both stopped-flow lanes are gated off.",
"inputs": {
"pump_status": true,
"pump_flow": 2.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yReverseFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "running_with_negative_flow",
"description": "Even signed reverse flow is outside this stopped-pump rule while run proof is true; other hydraulic/proof rules own the condition.",
"inputs": {
"pump_status": true,
"pump_flow": -2.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yReverseFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "running_zero_flow_is_other_rule",
"description": "A running pump with zero flow is silent here and belongs to PMP-0001/PMP-0002.",
"inputs": {
"pump_status": true,
"pump_flow": 0.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yReverseFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "positive_exact_threshold_clear",
"description": "The strict comparator leaves +1.0 L/s clear.",
"inputs": {
"pump_status": false,
"pump_flow": 1.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yReverseFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "negative_exact_threshold_clear",
"description": "The strict comparator leaves -1.0 L/s clear.",
"inputs": {
"pump_status": false,
"pump_flow": -1.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yReverseFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "positive_just_over_threshold",
"description": "One representable test increment above the forward threshold follows the full delay.",
"inputs": {
"pump_status": false,
"pump_flow": 1.1
},
"expect": [
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 2400,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yFault",
"from_s": 360,
"to_s": 2400,
"equals": true
}
]
},
{
"name": "negative_just_over_threshold",
"description": "One test increment beyond the reverse threshold follows the full delay.",
"inputs": {
"pump_status": false,
"pump_flow": -1.1
},
"expect": [
{
"output": "yReverseFlow",
"from_s": 0,
"to_s": 2400,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yFault",
"from_s": 360,
"to_s": 2400,
"equals": true
}
]
},
{
"name": "violation_ends_at_delay_boundary",
"description": "Flow clears on the 300-second timer boundary before evaluation and never alarms.",
"inputs": {
"pump_status": false,
"pump_flow": [
{
"t": 0,
"value": 2.0
},
{
"t": 300,
"value": 0.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 240,
"equals": true
},
{
"output": "yForwardFlow",
"from_s": 360,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "two_short_episodes_reset",
"description": "Two sub-delay forward-flow episodes separated by a clear tick do not accumulate.",
"inputs": {
"pump_status": false,
"pump_flow": [
{
"t": 0,
"value": 2.0
},
{
"t": 180,
"value": 0.0
},
{
"t": 360,
"value": 2.0
},
{
"t": 600,
"value": 0.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "short_flow_transient",
"description": "A single forward-flow episode shorter than persistence exposes direction but never alarms.",
"inputs": {
"pump_status": false,
"pump_flow": [
{
"t": 0,
"value": 2.0
},
{
"t": 180,
"value": 0.0
}
]
},
"expect": [
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 120,
"equals": true
},
{
"output": "yForwardFlow",
"from_s": 240,
"to_s": 2400,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "flow_recovery_resets_timer",
"description": "An in-band interval resets persistence; the second stopped-flow episode serves a new full 300 seconds.",
"inputs": {
"pump_status": false,
"pump_flow": [
{
"t": 0,
"value": 2.0
},
{
"t": 180,
"value": 0.0
},
{
"t": 300,
"value": 2.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 540,
"equals": false
},
{
"output": "yFault",
"from_s": 660,
"to_s": 2400,
"equals": true
}
]
},
{
"name": "pump_start_resets_timer",
"description": "Run proof temporarily gates off both directions and resets the timer even while branch flow stays high.",
"inputs": {
"pump_status": [
{
"t": 0,
"value": false
},
{
"t": 180,
"value": true
},
{
"t": 360,
"value": false
}
],
"pump_flow": 2.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 600,
"equals": false
},
{
"output": "yFault",
"from_s": 720,
"to_s": 2400,
"equals": true
}
]
},
{
"name": "two_opposite_short_episodes",
"description": "Positive and negative sub-delay episodes separated by an in-band interval do not accumulate across direction lanes.",
"inputs": {
"pump_status": false,
"pump_flow": [
{
"t": 0,
"value": 2.0
},
{
"t": 180,
"value": 0.0
},
{
"t": 300,
"value": -2.0
},
{
"t": 540,
"value": 0.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "direction_handoff_keeps_candidate_true",
"description": "A direct forward-to-reverse handoff changes diagnostic lane but not absolute-flow candidacy, so the timer continues.",
"inputs": {
"pump_status": false,
"pump_flow": [
{
"t": 0,
"value": 2.0
},
{
"t": 180,
"value": -2.0
}
]
},
"expect": [
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 120,
"equals": true
},
{
"output": "yReverseFlow",
"from_s": 240,
"to_s": 2400,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yFault",
"from_s": 360,
"to_s": 2400,
"equals": true
}
]
},
{
"name": "recovery_after_alarm",
"description": "Falling edges pass immediately when stopped-branch flow returns inside the band.",
"inputs": {
"pump_status": false,
"pump_flow": [
{
"t": 0,
"value": 2.0
},
{
"t": 900,
"value": 0.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 360,
"to_s": 840,
"equals": true
},
{
"output": "yFault",
"from_s": 960,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "noise_inside_zero_band",
"description": "Signed zero drift that stays inside the allowance never raises a direction flag.",
"inputs": {
"pump_status": false,
"pump_flow": [
{
"t": 0,
"value": 0.9
},
{
"t": 300,
"value": -0.9
},
{
"t": 600,
"value": 0.5
},
{
"t": 900,
"value": -0.5
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
},
{
"output": "yReverseFlow",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "threshold_chatter_resets_delay",
"description": "Flow alternating just below and above the forward threshold chatters the diagnostic but never accumulates 300 continuous seconds.",
"inputs": {
"pump_status": false,
"pump_flow": [
{
"t": 0,
"value": 0.9
},
{
"t": 60,
"value": 1.1
},
{
"t": 120,
"value": 0.9
},
{
"t": 180,
"value": 1.1
},
{
"t": 240,
"value": 0.9
},
{
"t": 300,
"value": 1.1
},
{
"t": 360,
"value": 0.9
},
{
"t": 420,
"value": 1.1
},
{
"t": 480,
"value": 0.9
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2400,
"equals": false
}
]
},
{
"name": "proof_mismatch_raw_fault_host_suppresses",
"description": "False status with real flow is indistinguishable from stopped-branch flow in this graph; an active same-pump PMP-0003 suppresses the raw alarm host-side.",
"inputs": {
"pump_status": false,
"pump_flow": 3.0
},
"expect": [
{
"output": "yForwardFlow",
"from_s": 0,
"to_s": 2400,
"equals": true
},
{
"output": "yFault",
"from_s": 360,
"to_s": 2400,
"equals": true
}
]
}
]
}