PMP-0003 — Pump proof-of-operation failure
| Status | verified — engine e2ff2f8, cxf:fnv1a128:5d2c2578abc92f6bca8468f0bb076494, 2026-08-18 |
| Severity | 2 |
| Method | rule |
| Phase | 2 |
| Category | PROTECTIVE |
| Confidence | HIGH |
| Estimation | PROXY_ESTIMATION |
| G36 | — |
| Clusters | — |
| Suppresses | PMP-0005 |
| Suppressed by | — |
| Related | PMP-0001, PMP-0002, PMP-0004, PMP-0005, VFD-0001, HW-0009, HX-0002 |
| Playbooks | proof-of-operation, vfd-pump-faults |
| Source | ASHRAE Guideline 36-2021 §5.20.17.6 (chilled water plant pumps and tower fans) and §5.21.10.5 (hot water plant pumps) — the pump command/status alarm this card instantiates: commanded on with status off is Level 2 after 15 s, commanded off with status on is Level 4 after a separate 60 s window. Two directions, two windows, two severities; both proof-time defaults are the standard’s own numbers; ASHRAE Guideline 36-2021 §5.1.6 — the definition of proven: the equipment’s DI status point matches the state its DO command point was set to. This rule is that comparison, held in both directions; Clause numbers verified against the G36-2021 text by the HW-0009 agent, batch 23, and recorded in _research/local/engine-block-inventory-fdd-candidates.md — which also corrects the §5.1.5 attribution this batch’s brief carried (that clause is outdoor-air sensor selection); The gap is reserved twice inside this library: PMP-0001’s Deviations — ‘the command/status mismatch is NO_EVAL here and is nobody’s fault rule today’ — and VFD-0001’s Deviations — ‘a run-status-versus-command comparison at equipment level, which this library has no drive rule for’; Buildings.Controls.OBC.CDL.Logical.Proof (Hu & Wetter, March 2023), read at engine pin e2ff2f8 from third_party/modelica-buildings-cdl/Buildings/Controls/OBC/CDL/Logical/Proof.mo — the standardized command-vs-status idiom, and the vocabulary this card’s two directions inherit (yLocFal / yLocTru). The block is exported at the pin and is deliberately NOT used; see Deviations; HVAC FDD Reference v1.0 ch.15 ‘Pumps’ carries no command-versus-status card — its two are PMP-0001 and PMP-0002 — so name, severity and category are argued here against G36’s alarm levels rather than transcribed; points/pmp.points.json pump_cmd and pump_status — the status point’s provenance note (current switch or VFD run feedback) is what the retune guidance on both windows is argued from |
| Operating states | all — both directions are meaningful in every state, and the unoccupied hours are where the unexpected-run direction earns its keep |
Preconditions (host-enforced): Both points must belong to the same pump and both must be fresh, and the freshness obligation is sharper here than on any other pump card because the shipped windows are G36’s controller-native 15 s and 60 s. The rule is a pure agreement test between two booleans and has no way to tell a real mismatch from a stale one: a held-over last value, a lapsed COV subscription, or a poll interval longer than the proof time manufactures a fault out of nothing, and a 15 s window against a 60 s poll cycle does so on every start. Both proof times must therefore exceed the host’s worst-case delivery latency for these two points — that is the whole of the delivery-quality obligation, and it is why the rule publishes no evaluability flag (see Deviations). pump_status must prove ROTATION or WORK: a current switch set above the motor’s no-load current, a differential-pressure switch across the pump, or the drive’s own run feedback. A starter auxiliary contact wired from the same relay the command drives makes this rule structurally blind — status echoes command, the two never disagree, and neither direction can ever assert. A current switch set below no-load current is the same failure in one direction only: a sheared coupling or an uncoupled motor reads as proven running. pump_cmd must be the output actually delivered to the starter, not a scheduler’s intent upstream of BAS overrides; bound upstream, a legitimate operator override reads as an unexpected run. On a lead/lag set the changeover dwell must fit inside both proof times, and on a headered set the two points must not be crossed between pumps — the binding error that puts the lead pump’s status against the lag pump’s command produces both faults at once on two healthy machines.
Points: pump_cmd, pump_status
Outputs:
yFault— True while either direction has been proven — the OR of the two flags below, taken after their delays. The directions are mutually exclusive by construction, so a live yFault always has exactly one flag under ityFailToStart— Sub-condition flag — pump_cmd true and pump_status false continuously for start_proof_time. DIAGNOSTIC only; false never means NO_EVALyUnexpectedRun— Sub-condition flag — pump_status true and pump_cmd false continuously for stop_proof_time. Same kind as yFailToStart
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
start_proof_time | 15.0 | s | startProof.delayTime | How long the pump may be commanded on without proving running before the fault asserts. TRANSCRIBED from G36 §5.20.17.6 / §5.21.10.5, which give 15 s for the commanded-on direction. The standard assumes a controller reading a hardwired DI every scan; a host consuming polled or trended BAS points MUST raise this above its worst-case delivery latency, and a pump whose status is a DP switch downstream of a 30 s VFD ramp needs more than the ramp (vfd_ramp_alarms_at_the_shipped_default pins that case rather than hiding it) |
stop_proof_time | 60.0 | s | stopProof.delayTime | How long the pump may run against a command of off before the fault asserts. TRANSCRIBED from the same clauses, which publish a SEPARATE 60 s window for the commanded-off direction — four times the start window. The asymmetry is the standard’s and it matches the physics: a stopping pump’s status lingers while the impeller coasts and the check valve settles, and the finding this direction names, a starter left in HAND, lasts nights and weekends, so latency costs nothing while a false alarm on every normal stop costs the rule its credibility |
Description
Proof of operation is the question every other pump rule assumes an answer to: did the machine do what it was told? Two booleans settle it — the command the BAS sends the starter, and the status the starter sends back — and their disagreement, held past the window G36 publishes for its direction, is a fault in whichever direction it points. Commanded on and never proven running is a dead loop: a tripped overload, a sheared coupling, a drive locked out on a fault, a disconnect somebody opened for service and left open. Proven running while commanded off is usually a hand-off-auto switch left in HAND, and it bills for that every hour of every night. PMP-0001 and PMP-0002 both begin with a pump that is running and ask what the water is doing; this rule sits upstream of both and asks whether the run state itself is real.
Detection Logic
yFailToStart = (pump_cmd AND NOT pump_status) continuously for start_proof_time
yUnexpectedRun = (pump_status AND NOT pump_cmd) continuously for stop_proof_time
yFault = yFailToStart OR yUnexpectedRun
Block graph (rule.cxf.jsonld):
Seven blocks, two lanes, no thresholds and no reals. The conditions are mutually
exclusive at every instant — one needs the command true, the other needs it
false — and each delay passes only its own condition through, so the flags can
never be true together and yFault always has exactly one direction under it.
The Or sits after both delays rather than before, which is the one behaviour
worth knowing at deployment: a mismatch that changes direction is two faults
rather than one, and yFault drops while the new lane serves its own window
(direction_flip_reserves_the_new_window pins it). Both delays carry
delayOnInit = true, so a restart into a standing mismatch waits out the full
window instead of announcing itself on tick zero.
No evaluability flag, and none is needed: the rule is evaluable whenever both points are delivered, which is the host’s delivery-quality job. Both sub-condition flags are diagnostic — false on either means that direction is not proven, never NO_EVAL.
Possible Diagnoses
yFailToStart — commanded on, never proved:
- Tripped motor overload, or a starter that never pulled in. The first thing to look at and the one visible without tools
- VFD locked out on a fault — overcurrent, ground fault, phase loss. The drive
display carries the code, and
vfd-pump-faultsstep 1.4 already looks for it - Broken coupling or snapped belt: the motor turns, the pump does not. A current switch set above no-load current catches this; one set below it does not, and reports the pump as running
- Disconnect left open after service, or a blown control-circuit fuse — the cheapest cause, and common after a Friday afternoon
- The status device rather than the pump: a failed current switch, a DP switch out of adjustment, a broken auxiliary contact. Rule this out with a clamp meter before anyone opens anything
yUnexpectedRun — running, never commanded:
- Hand-off-auto switch left in HAND after service. The classic, and the one that shows up on the bill
- Welded contactor, or a starter holding in on a stuck relay
- BAS output miswired or left in override — a point overridden at the controller, or a command landed on the wrong starter in a pump room with several
- A second controller commanding the same pump: a boiler’s own pump control, a chiller interlock, a packaged plant sequencer. The pump is obeying somebody this rule cannot see
Energy Impact
PROTECTIVE, HIGH confidence, PROXY_ESTIMATION. Only one direction spends
electricity, and it is exactly calculable. Take a 10 kW pump — an ordinary
mid-size building’s secondary chilled-water pump — left in HAND when a service
call ends Friday afternoon and found Monday morning: 60 h x 10 kW = 600 kWh,
roughly $60-$90 at $0.10-$0.15/kWh. A year of unnoticed weekends is about 31 MWh
and $3,000-$4,700, and on a hot-water loop the pump’s own kW is the floor rather
than the total, because a circulating loop bleeds heat and the boiler answers.
yFailToStart spends no pump energy at all; its cost is the loop that did not
circulate — a chiller tripping on low evaporator flow, a heating coil with no
water in freezing weather. HIGH confidence because there is nothing to
calibrate; PROXY because the hours are measured and the kilowatts are a
nameplate.
Emissions Impact
Scope 2, PROXY_EMISSIONS. Pumps are electric everywhere, so the scope never varies with the plant the way a heating fault’s does. The weekend above is about 240 kg CO₂e on a 0.4 kg/kWh marginal operating emissions rate (MOER) and roughly 12 t/yr if it repeats — the sibling pair’s published 100-500 kg/yr band is for a fault that ends when somebody notices; this one’s is driven by hours, not by hydraulics, and has no natural end. PROXY rather than the pair’s DIRECT because this rule reads no meter and no flow.
Deviations
CDL.Logical.Proofis exported at the pin and is deliberately not used. Its two outputs map one-for-one onto this card’s two directions, but a single timing window governs both of them — checking begins at the EARLIER offeedbackDelay + debounceafter the command changes ordebounceafter the measurement settles — so G36’s published 15 s and 60 s cannot both be expressed. Composed fromLogical.Not,Logical.And,Logical.TrueDelayandLogical.Orinstead, all registry-supported classes. The HW-0009 agent’s probes reached the same verdict independently.- Three further
Proofbehaviours contradict this card, all measured against a probe document at e2ff2f8 rather than read off the engine source. Withdebounce = 60 sandfeedbackDelay = 120 s: a start into a standing mismatch raisedyLocFalat t = 0 s, because its internal delays behave asdelayOnInit = false; a stably-false status therefore gets no proof window at all, asserting on the tick the command rises; and a command held true against a status chattering on a 60 s period raisedyLocTru— the unexpected-run output — at t = 240 s, both outputs latched together. This card promises mutually exclusive flags and a full window after restart, so the last result alone settles it. Nothing surprising happened at load: the block imports, exports and runs cleanly, and the rejection is semantic. - Two Nots and two Ands rather than one Xor and two Ands.
pump_cmd XOR pump_statusconjoined with each input in turn is the same truth table in one fewer block, and draws without a wire crossing. The composed form is kept because batch 23 instantiates one template per equipment, and a family whose graphs read the same is worth more than a block. - Both proof times are transcribed from G36, not authored. §5.20.17.6 and §5.21.10.5 publish 15 s for the commanded-on direction and a separate 60 s window for the commanded-off direction on exactly this equipment, so this card ships the standard’s pair rather than the 60 s / 120 s it originally argued from VFD ramps and BAS poll cycles. The asymmetry survived the correction and its direction was right; only the magnitudes and the ratio changed, from 2x to the standard’s 4x.
- The shipped 15 s will alarm on a healthy start behind a slow ramp or a slow
poll, and the vectors pin that rather than hide it. G36’s number assumes a
controller reading a hardwired DI every scan; this library’s rules read
whatever the host delivers.
vfd_ramp_alarms_at_the_shipped_defaultruns a 30 s acceleration ramp through the default and shows the alarm asserting at 45 s and clearing itself at 60 s when the status finally makes. Retuningstart_proof_timeabove the site’s worst-case ramp and delivery latency is a precondition, not an option — the same discipline PMP-0001 applies to its placeholder threshold. delayOnInit = trueon both delays (CDL defaultfalse), the library’s standing choice, and it does real work in both directions here: a controller restart is exactly when a mismatch is most likely to be an artefact of the restart — outputs re-driven, statuses not yet polled — so serving the full window before alarming is the difference between a rule and a nuisance on every reboot.severity: 2andcategory: PROTECTIVEdescribe the fail-to-start direction only, and G36 agrees that one number cannot cover both. The standard alarms commanded-on/status-off at Level 2 and commanded-off/status-on at Level 4 — the same split this library’s 1-4 scale would make, and this card has one slot for it. Severity 2 takes it because it is the shorter fuse: a dead heating loop in freezing weather costs a coil, not a bill. The unexpected-run direction is G36’s Level 4 and EXCESS_CONSUMPTION by any reading, and is quantified as such above. Hosts routing work by severity or category should route on the sub-condition flag, not on the card.- No suppression edge is added against PMP-0001/PMP-0002, but this rule now
suppresses PMP-0005 on the same pump. PMP-0001 requires
pump_cmd AND pump_statusboth true, so either of this rule’s directions already puts it in NO_EVAL through its ownyRunOkand an edge would be redundant. PMP-0002 gates onpump_statusalone and therefore stays live duringyUnexpectedRun— and a pump running in HAND against a closed system really is deadheading, so that alarm is true and worth keeping. PMP-0005 instead assumes a false status means a stopped branch; an active proof mismatch invalidates exactly that premise. The host must scope this suppression to the same pump instance. - The rule is silent on a chattering status, deliberately. A status that will
not settle never accumulates either window
(
chattering_status_never_matures), which is precisely whereCDL.Logical.Proofraises both of its alarms. An unstable point is a delivery-quality or sensor-health finding, and SCHEMA.md’s design stance puts those outside the block graph. estimation_method: PROXY_ESTIMATIONandPROXY_EMISSIONS, against the sibling pair’s DIRECT ratings. They read a flow meter; this rule reads two booleans. The run hours are exact — the rule measures them itself — and the kilowatts are a nameplate the host supplies, which is what a proxy is.g36: nulleven though this card names two verified clauses, and the field is the reason rather than the provenance. SCHEMA.md scopesg36to “001-range rules” — the cards transcribing the reference’s G36-numbered fault conditions — and every populated value in the library today is a §5.16.14 FC#n or §5.22.6 FC#n. Widening it to a directly-instantiated sequence clause is a schema question for that field’s owner, not a decision to take inside one card, so §5.20.17.6 and §5.21.10.5 are carried insourceinstead. Batch sibling HW-0009 reaches the same null by the opposite route: G36 §5.1.15.5.b.1 forbids proving a boiler by status at all, so its card departs from the standard where this one instantiates it.- Two playbooks are bound and only one names this rule.
proof-of-operationis batch 23’s purpose-built procedure and lists PMP-0003 in its Applies-To row;vfd-pump-faultsis the pump family’s service playbook and its step 1 is where a drive lockout gets diagnosed, but its Applies-To row still names only VFD-0001/0002 and PMP-0001/0002. That row belongs to the playbook’s owner; the binding is kept because the diagnosis list points at it. relatedcarries one addition beyond the batch brief’s list. VFD-0001 is added because its own Deviations reserve exactly this check from the electrical side, and a pump whose proof fails on a drive fault trips both. HW-0009 is batch 23’s boiler instance of the same template — a sibling instantiation rather than a co-occurring fault, listed for the family rather than for the diagnosis.clustersis empty:clusters/clusters.jsondefines no cluster containing a pump rule, and this card does not edit the cluster set. Every scenario invectors.jsonis library-authored: G36 states the alarm and its two windows but publishes no test vectors, and the reference has no such card at all.
Notes
Read the flag, not the card: the two directions send different people to
different places. yFailToStart is a work order at the starter — HOA position,
overload, drive fault code, coupling — and it is urgent in proportion to what
the loop was supposed to be doing. yUnexpectedRun is usually a five-minute fix
and a conversation: put the switch back in AUTO, then find out why it was in
HAND, because somebody moved it for a reason and that reason is often another
fault this library can name. The
proof-of-operation playbook owns
both procedures and starts where it should — with which device is providing the
status. Run this card first on any pump where PMP-0001 has been quiet for a
suspiciously long time: a lead/lag standby whose yRunOk never goes true looks
exactly like a healthy idle pump until something reads the two points directly.
Test Vectors
10 scenarios, clock step 5 s over 1800 s.
| Scenario | Description |
|---|---|
agreement_running_stays_silent | Commanded on and proven running for the whole horizon. Agreement is silence: neither direction can accumulate and yFault never asserts. |
agreement_stopped_stays_silent | Commanded off and proven off — a standby pump on a lead/lag set, or any pump overnight. The other agreeing state, equally silent. |
fail_to_start_asserts_at_start_proof_time | Commanded on from init, never proves running — a tripped overload or an open disconnect. delayOnInit = true serves the full start_proof_time, so the edge lands exactly at G36’s 15 s and not on tick zero. |
unexpected_run_asserts_at_stop_proof_time | Proven running against a command of off from init — a hand-off-auto switch left in HAND. The edge lands exactly at G36’s separate 60 s window for this direction, four times the start window. |
run_proof_lost_mid_cycle | A pump running normally loses its proof at 300 s — a coupling shears, an overload trips on a hot afternoon. The window is measured from the event, not from init: the alarm lands at 315 s. |
normal_start_and_stop_lag_stay_silent | Status lags the start command by 10 s and the stop command by 45 s — both inside their published windows, which is what the windows are for. Nothing asserts. |
vfd_ramp_alarms_at_the_shipped_default | The retune obligation, pinned rather than hidden: a 30 s VFD acceleration ramp before a DP-switch status makes is LONGER than G36’s 15 s, so the shipped default alarms on a healthy start and clears itself when the status arrives. Hosts on polled points or slow ramps must raise start_proof_time (see preconditions). |
command_flip_during_start_window_resets_timer | The command drops 5 s into an unproven start and returns at 10 s. TrueDelay starts the window over, so the alarm lands at 25 s rather than at 15 s: proof time means CONTINUOUS mismatch. |
direction_flip_reserves_the_new_window | A pump running in HAND is commanded on at 600 s while the starter, now on a tripped overload, stops proving. The unexpected-run flag drops on the same tick the fail-to-start condition begins, the new direction serves its own 15 s window, and yFault is false in between. The two flags are never true together — mutual exclusivity is structural, not timed. |
chattering_status_never_matures | A marginal current switch makes and breaks every 10 s for five minutes against a steady on command, then settles made. No mismatch is ever continuous for start_proof_time, so the rule stays silent — CDL.Logical.Proof latches BOTH of its outputs on this trace (probe-verified at the pin; see Deviations). |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 5,
"horizon_s": 1800
},
"scenarios": [
{
"name": "agreement_running_stays_silent",
"description": "Commanded on and proven running for the whole horizon. Agreement is silence: neither direction can accumulate and yFault never asserts.",
"inputs": {
"pump_cmd": true,
"pump_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": "agreement_stopped_stays_silent",
"description": "Commanded off and proven off \u2014 a standby pump on a lead/lag set, or any pump overnight. The other agreeing state, equally silent.",
"inputs": {
"pump_cmd": false,
"pump_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": "fail_to_start_asserts_at_start_proof_time",
"description": "Commanded on from init, never proves running \u2014 a tripped overload or an open disconnect. delayOnInit = true serves the full start_proof_time, so the edge lands exactly at G36's 15 s and not on tick zero.",
"inputs": {
"pump_cmd": true,
"pump_status": false
},
"expect": [
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 10,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 15,
"to_s": 1800,
"equals": true
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 10,
"equals": false
},
{
"output": "yFault",
"from_s": 15,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "unexpected_run_asserts_at_stop_proof_time",
"description": "Proven running against a command of off from init \u2014 a hand-off-auto switch left in HAND. The edge lands exactly at G36's separate 60 s window for this direction, four times the start window.",
"inputs": {
"pump_cmd": false,
"pump_status": true
},
"expect": [
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 55,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 60,
"to_s": 1800,
"equals": true
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 55,
"equals": false
},
{
"output": "yFault",
"from_s": 60,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "run_proof_lost_mid_cycle",
"description": "A pump running normally loses its proof at 300 s \u2014 a coupling shears, an overload trips on a hot afternoon. The window is measured from the event, not from init: the alarm lands at 315 s.",
"inputs": {
"pump_cmd": true,
"pump_status": [
{
"t": 0,
"value": true
},
{
"t": 300,
"value": false
}
]
},
"expect": [
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 310,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 315,
"to_s": 1800,
"equals": true
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "normal_start_and_stop_lag_stay_silent",
"description": "Status lags the start command by 10 s and the stop command by 45 s \u2014 both inside their published windows, which is what the windows are for. Nothing asserts.",
"inputs": {
"pump_cmd": [
{
"t": 0,
"value": false
},
{
"t": 60,
"value": true
},
{
"t": 600,
"value": false
}
],
"pump_status": [
{
"t": 0,
"value": false
},
{
"t": 70,
"value": true
},
{
"t": 645,
"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": "vfd_ramp_alarms_at_the_shipped_default",
"description": "The retune obligation, pinned rather than hidden: a 30 s VFD acceleration ramp before a DP-switch status makes is LONGER than G36's 15 s, so the shipped default alarms on a healthy start and clears itself when the status arrives. Hosts on polled points or slow ramps must raise start_proof_time (see preconditions).",
"inputs": {
"pump_cmd": [
{
"t": 0,
"value": false
},
{
"t": 30,
"value": true
}
],
"pump_status": [
{
"t": 0,
"value": false
},
{
"t": 60,
"value": true
}
]
},
"expect": [
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 40,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 45,
"to_s": 55,
"equals": true
},
{
"output": "yFailToStart",
"from_s": 60,
"to_s": 1800,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "command_flip_during_start_window_resets_timer",
"description": "The command drops 5 s into an unproven start and returns at 10 s. TrueDelay starts the window over, so the alarm lands at 25 s rather than at 15 s: proof time means CONTINUOUS mismatch.",
"inputs": {
"pump_cmd": [
{
"t": 0,
"value": true
},
{
"t": 5,
"value": false
},
{
"t": 10,
"value": true
}
],
"pump_status": false
},
"expect": [
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 20,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 25,
"to_s": 1800,
"equals": true
},
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "direction_flip_reserves_the_new_window",
"description": "A pump running in HAND is commanded on at 600 s while the starter, now on a tripped overload, stops proving. The unexpected-run flag drops on the same tick the fail-to-start condition begins, the new direction serves its own 15 s window, and yFault is false in between. The two flags are never true together \u2014 mutual exclusivity is structural, not timed.",
"inputs": {
"pump_cmd": [
{
"t": 0,
"value": false
},
{
"t": 600,
"value": true
}
],
"pump_status": [
{
"t": 0,
"value": true
},
{
"t": 600,
"value": false
}
]
},
"expect": [
{
"output": "yUnexpectedRun",
"from_s": 0,
"to_s": 55,
"equals": false
},
{
"output": "yUnexpectedRun",
"from_s": 60,
"to_s": 595,
"equals": true
},
{
"output": "yUnexpectedRun",
"from_s": 600,
"to_s": 1800,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 0,
"to_s": 610,
"equals": false
},
{
"output": "yFailToStart",
"from_s": 615,
"to_s": 1800,
"equals": true
},
{
"output": "yFault",
"from_s": 60,
"to_s": 595,
"equals": true
},
{
"output": "yFault",
"from_s": 600,
"to_s": 610,
"equals": false
},
{
"output": "yFault",
"from_s": 615,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "chattering_status_never_matures",
"description": "A marginal current switch makes and breaks every 10 s for five minutes against a steady on command, then settles made. No mismatch is ever continuous for start_proof_time, so the rule stays silent \u2014 CDL.Logical.Proof latches BOTH of its outputs on this trace (probe-verified at the pin; see Deviations).",
"inputs": {
"pump_cmd": true,
"pump_status": [
{
"t": 0,
"value": true
},
{
"t": 10,
"value": false
},
{
"t": 20,
"value": true
},
{
"t": 30,
"value": false
},
{
"t": 40,
"value": true
},
{
"t": 50,
"value": false
},
{
"t": 60,
"value": true
},
{
"t": 70,
"value": false
},
{
"t": 80,
"value": true
},
{
"t": 90,
"value": false
},
{
"t": 100,
"value": true
},
{
"t": 110,
"value": false
},
{
"t": 120,
"value": true
},
{
"t": 130,
"value": false
},
{
"t": 140,
"value": true
},
{
"t": 150,
"value": false
},
{
"t": 160,
"value": true
},
{
"t": 170,
"value": false
},
{
"t": 180,
"value": true
},
{
"t": 190,
"value": false
},
{
"t": 200,
"value": true
},
{
"t": 210,
"value": false
},
{
"t": 220,
"value": true
},
{
"t": 230,
"value": false
},
{
"t": 240,
"value": true
},
{
"t": 250,
"value": false
},
{
"t": 260,
"value": true
},
{
"t": 270,
"value": false
},
{
"t": 280,
"value": true
},
{
"t": 290,
"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
}
]
}
]
}