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

PMP-0005 — Flow through stopped pump

Statusverified — engine e2ff2f8, cxf:fnv1a128:d567fd02bea2ecc336bdf44f7680de4a, 2026-08-20
Severity2
Methodrule
Phase2
CategoryPROTECTIVE
ConfidenceMEDIUM
EstimationQUALITATIVE_ONLY
G36
Clusters
Suppresses
Suppressed byPMP-0003
RelatedPMP-0001, PMP-0002, PMP-0003
Playbooksvfd-pump-faults
SourceDOE/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 statesall 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_duration
  • yForwardFlow — 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 verdict
  • yReverseFlow — Diagnostic direction flag — true while a stopped pump has signed negative flow beyond the allowance; unavailable on magnitude-only bindings

Parameters:

NameDefaultUnitCXF pathDescription
stopped_flow_threshold1.0L/sforward.t, reverse.tAbsolute 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_duration300.0spersist.delayTimeContinuous 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

PMP-0005 block graph

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

  1. Passing, failed, reversed, or missing discharge check valve
  2. Reverse flow driven by another pump on a common header
  3. Thermosiphoning or a gravity path not represented in the operating gate
  4. Pump run-proof failure or stale false status (PMP-0003)
  5. Flow sensor zero error, sign inversion, or common-header misbinding
  6. Isolation or bypass valve left open
  7. 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 yFault and 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.

ScenarioDescription
stopped_zero_flowA stopped branch at zero flow is healthy.
stopped_positive_flowForward branch flow present at initialization exposes direction immediately and faults only after persistence.
stopped_negative_flowSigned reverse flow follows the independent reverse diagnostic lane.
running_with_flowFlow is expected while the pump is proven running, so both stopped-flow lanes are gated off.
running_with_negative_flowEven 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_ruleA running pump with zero flow is silent here and belongs to PMP-0001/PMP-0002.
positive_exact_threshold_clearThe strict comparator leaves +1.0 L/s clear.
negative_exact_threshold_clearThe strict comparator leaves -1.0 L/s clear.
positive_just_over_thresholdOne representable test increment above the forward threshold follows the full delay.
negative_just_over_thresholdOne test increment beyond the reverse threshold follows the full delay.
violation_ends_at_delay_boundaryFlow clears on the 300-second timer boundary before evaluation and never alarms.
two_short_episodes_resetTwo sub-delay forward-flow episodes separated by a clear tick do not accumulate.
short_flow_transientA single forward-flow episode shorter than persistence exposes direction but never alarms.
flow_recovery_resets_timerAn in-band interval resets persistence; the second stopped-flow episode serves a new full 300 seconds.
pump_start_resets_timerRun proof temporarily gates off both directions and resets the timer even while branch flow stays high.
two_opposite_short_episodesPositive and negative sub-delay episodes separated by an in-band interval do not accumulate across direction lanes.
direction_handoff_keeps_candidate_trueA direct forward-to-reverse handoff changes diagnostic lane but not absolute-flow candidacy, so the timer continues.
recovery_after_alarmFalling edges pass immediately when stopped-branch flow returns inside the band.
noise_inside_zero_bandSigned zero drift that stays inside the allowance never raises a direction flag.
threshold_chatter_resets_delayFlow alternating just below and above the forward threshold chatters the diagnostic but never accumulates 300 continuous seconds.
proof_mismatch_raw_fault_host_suppressesFalse 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
        }
      ]
    }
  ]
}