FCU-0004 — Inactive cooling coil temperature drop (leak)
| Status | verified — engine e2ff2f8, cxf:fnv1a128:40d3e69b8b4c5f2aa7e5f24c9981671b, 2026-08-17 |
| Severity | 3 |
| Method | rule |
| Phase | 1 |
| Category | CRITICAL_WASTE |
| Confidence | HIGH |
| Estimation | DIRECT_MEASUREMENT |
| G36 | §5.22.6 FC#4 |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | FCU-0005, FCU-0002, FCU-0006, FCU-0007 |
| Playbooks | fcu-faults |
| Source | HVAC FDD Reference v1.0 §12, FCU-0004; G36 §5.22.6 FC#4 (the chapter’s cited source; clause text not available — see Deviations); G36 Addendum u, Table 5.16.14.5 (εRAT, εSAT, ΔTSF — the threshold composition) and §5.17.4.5 FC#3 (the RAT/SAT-proxied form of the same test); PNNL EEM-03 (fix leaking valves) |
| Operating states | OS#2 (no active coils) — host-gated |
Preconditions (host-enforced): The fan must be running.
ratandsatare a coil entering and leaving temperature only while air is crossing the coil; on a cycling-fan FCU the discharge sensor sits in stagnant air over a coil full of chilled water between cycles and reads several degrees cold, which is this fault’s exact signature and none of its meaning. This graph does not consume the dictionary’s canonicalfan_cmdorfan_status, so the host must gate it on trusted fan proof; an FCU-0006 fail-to-start makes this verdict NO_EVAL (fan_off_standing_water_reads_as_a_leakpins the consequence). Suspend evaluation for a settling window after the cooling valve closes — a coil surrendering the chilled water standing in it shows the same drop for several minutes. The heating coil should also be off: a heating call warms the discharge and can only hide this fault, never fabricate it, so the masking is a miss rather than a false alarm, but a host that wants OS#2 as the reference scopes it should gate onhtg_vlv_cmdtoo, which this rule does not read. Both sensors must be in the airstream and trustworthy: aratbound to a wall-mounted space sensor, a discharge sensor in a plenum shared with another unit, or a cabinet drawing ducted outdoor air upstream of the coil all break the binding with no other symptom, and the outdoor-air case biases this rule toward false alarms in winter. Nothing in this rule cross-checks either sensor. When any gate is unmet the verdict is NO_EVAL, not healthy — as it is whenever the in-rule output yCmdOk reads false.
Points: clg_vlv_cmd, sat, rat
Outputs:
yFault— True while the valve has been commanded shut and sat has stayed more than inactive_coil_threshold below rat, continuously for at least alarm_delayyCmdOk— True while clg_vlv_cmd is below cmd_closed_threshold — the coil is commanded shut and the drop across it is therefore interpretable. False means the coil is allowed to be cooling and this rule has no verdict; the host reports NO_EVAL, not healthy
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
inactive_coil_threshold | 3.0 | °C | dropBig.t | Drop from entering to leaving air that stops being sensor error and starts being a leak. ADOPTED — the reference names the parameter in the equation and publishes no value for it (see Deviations). 3.0 °C is the rounded G36-style composition for FCU-grade instrumentation, sqrt(e_ret² + e_sup²) + dTSF, and it is deliberately the same number FCU-0005 ships because the reference names one parameter for both equations. Note the direction: on this cooling-side rule the fan’s rise works against the measured drop, so the shipped value fires at about 4 °C of true coil work |
cmd_closed_threshold | 1.0 | % | vlvShut.t | Command below which the cooling valve counts as commanded shut. ADOPTED — the reference writes the test as clg_vlv_cmd = 0%, which is not a comparison a real-valued signal supports (see Deviations). 1.0% is deliberately tighter than AHU-0016’s 5% open threshold: this rule needs the valve to be at rest, not merely nearly closed |
alarm_delay | 1800.0 | s | persist.delayTime | Continuous violation required before the alarm asserts (30 min). ADOPTED — the reference publishes no tunables line for this card (see Deviations); the value is the AHU twin’s G36 AlarmDelay |
Description
A fan coil’s chilled-water valve is small, and when its seat wears the leak is correspondingly small: a few degrees of cooling on air the sequence believes is passing an inert coil. In the heating season that is the expensive version — the heating coil warms the air, the leaking cooling coil takes part of it back, the zone holds setpoint, and the only evidence is a boiler and a chiller both working slightly harder than the room requires. Nobody complains, so nobody looks, which is why the fault is worth a rule rather than a walkthrough: fan coils are deployed by the hundred, and one leaking seat wastes an amount too small to see on a bill and too tedious to find by hand. The chapter puts one leaking valve at 3–10% of zone cooling energy and 100–800 kg CO₂e a year, mapped to PNNL EEM-03. Because the rule reads temperatures rather than flows it cannot separate a worn seat from an actuator short of its close position from a three-way bypass that is not sealing; all three are on the diagnosis list.
Detection Logic
yCmdOk = clg_vlv_cmd < cmd_closed_threshold (false ⇒ host reports NO_EVAL)
drop = rat − sat
yFault = (drop > inactive_coil_threshold) AND yCmdOk,
sustained continuously for alarm_delay
Block graph (rule.cxf.jsonld):
drop subtracts in the reference’s own order (entering_temp − leaving_temp),
which makes the sign of this rule the opposite of FCU-0005’s. vlvShut feeds
gate and also leaves the block as yCmdOk, so a host can tell the two
silences apart: quiet with yCmdOk true is a coil shut and behaving, quiet with
it false is a coil that was asked to cool, about which this rule has no opinion.
On this side of the pair the fan works against the signal — with both valves
shut a healthy unit reads rat − sat slightly negative, because the fan puts
its shaft work into the air — so a leak must overcome that rise and then clear
the sensor allowance on top, and the shipped 3.0 °C takes roughly 4 °C of real
coil work to trip where the same number on the heating-side twin trips at about
2 °C. Both comparisons are strict. persist requires 30 continuous minutes,
separating a leaking seat from a coil giving up the chilled water standing in it
after a call ends; recovery is immediate, and delayOnInit = true holds the
window across a restart.
Possible Diagnoses
Transcribed from the reference’s FCU-0004 card:
- Cooling coil valve leaking through — the worn or eroded seat; the common case, priced by the playbook at $150–$600 to replace
- Valve not fully closing (mechanical) — the actuator has lost its close position or binds short of the seat, distinguishable on site by stroking it against feedback
- Three-way valve bypass not sealing — nothing about the valve’s travel looks wrong, which is why this one survives an actuator check
A fourth belongs in the operator’s head though the reference does not list it: either sensor being wrong produces this trace with a perfectly good valve. A return sensor reading high and a discharge sensor reading low are indistinguishable here and both are cheap to check against a portable reference, which is why G36 §5.16.14 puts the two sensor errors ahead of the valve in its own diagnosis order.
Energy Impact
CRITICAL_WASTE, HIGH confidence, DIRECT_MEASUREMENT, 3–10% of zone cooling energy, mapped by the reference to PNNL EEM-03 (fix leaking valves). DIRECT_MEASUREMENT is honest here: the two temperatures the rule reads are the measurement, and the runtime formula converts them to thermal power with one substitution — design airflow for measured, since an FCU has no flow station. HIGH confidence because a sustained drop across a coil commanded shut has no benign explanation other than a sensor or a stopped fan. Heating-dominant per the chapter, which reads oddly for a cooling fault until the operating state is taken into account: the hours a leaking chilled-water valve does the most damage are the hours a heating coil is fighting it, and those are the hours the cooling valve is commanded shut for weeks at a time. In deep cooling weather the same leak hides behind legitimate calls for cooling.
Emissions Impact
Scope 2, DIRECT_EMISSIONS, HIGH confidence; typically 100–800 kg CO₂e/yr per unit of parasitic cooling, MOER basis. The unwanted cooling is purchased electricity at the chiller and pumps, so the cooling half of the exchange is Scope 2 on every site. The heating that cancels it follows whatever the plant burns and is FCU-0005’s half to claim. Where the cause turns out to be a sensor or a stopped fan there is nothing to attribute at all.
Deviations
inactive_coil_thresholdis adopted, not transcribed. The reference names the parameter and publishes no tunables line for this card. 3.0 °C is G36’s composition for the same fault on an air handler —sqrt(εSAT² + εRAT²) + ΔTSF, the form Addendum u §5.17.4.5 FC#3 uses on the same sensor pair — which gives 2.41 at that clause’s 1 °C values and 2.98 with the ±1.4 °C-class sensors an FCU typically carries. It is also the value FCU-0005 ships, because the reference names one parameter for both equations. (Only FC#3’s right-hand side is borrowed; its operands read as a drop under a description that says rise, so this card’s sign comes from the reference’sentering_temp − leaving_temp.)- Fan heat is inside the measurement, and here it works against the signal.
Neither the chapter nor the playbook mentions fan heat; the term comes from
G36’s ΔTSF via AHU-0014. The fan raises the discharge and the leaking coil
lowers it, so
rat − satunder-reports the true coil drop by one fan rise: 3.0 °C measured is about 4 °C of coil work, making this rule about half as sensitive in coil terms as its twin. The asymmetry is real, not an authoring slip; a host wanting matched sensitivity lowers this threshold by one fan-heat term. clg_vlv_cmd = 0%becomes< 1.0%. CDLRealshas no equality block, and equality on a float from a BAS would be the wrong test anyway — controllers write 0.0001% and round-tripped analog values land near but not on zero. 1.0% is tighter than AHU-0016’s 5% “open” threshold because that rule needs the valve to be doing something and this one needs it to be doing nothing. On a two-position solenoid the command is 0 or 100 and the test is exact.alarm_delayis adopted at 30 minutes. The reference publishes no delay here; FCU-0001’s 60 min belongs to a transition counter and says nothing about a coil. 1800 s is the AHU twin’s G36 AlarmDelay and it does identifiable work — riding out the residual cold a just-closed coil gives up. A site whose coils purge in five minutes can cut it and detect leaks sooner.- Strict comparisons at both boundaries. A drop sitting exactly on 3.0 °C is
not a fault and a command sitting exactly on 1.0% is not shut. The reference
writes
>for the temperature test; the command side is the adopted test above. Both err toward silence, and a host binding coarsely quantized temperatures should retune down. - The G36 clause is the chapter’s citation, carried forward unverified. The
library’s G36 material is Addendum u, which carries no fan coil section, so
everything claimed as transcribed comes from the reference’s ch.12 card and the
g36field is provenance the reference asserts. If §5.22.6 states averaging windows, epsilons or an alarm delay, they will correct the adopted values above. ratandsatstand in for the coil entering and leaving temperatures, as the FCU point dictionary directs, so the rule sees the whole air path: the fan is inside the measurement (handled by the fan-heat term) and so is any duct after the coil (not handled, and it biases this rule quieter). One configuration breaks the binding in the dangerous direction — a cabinet drawing outdoor air upstream of the coil presents a mixture colder than the return sensor reads, sorat − satshows a winter drop with no leak at all.- The fan-running gate remains host-side. The dictionary now defines
fan_cmdandfan_statusfor FCU-0006, but this graph does not consume them. A cycling-fan FCU evaluated between cycles is the realistic way to get a false alarm out of this rule (fan_off_standing_water_reads_as_a_leakpins it). Gate on trusted fan proof and treat FCU-0006 fail-to-start as NO_EVAL here. yCmdOkis the library’s, not the reference’s. Exposing the command conjunct as a boundary output adds no logic and changes no verdict; it lets the host distinguish “the coil is shut and quiet” from “the coil is cooling, ask me later”, which are the sameyFault = falseand mean opposite things. Same wiring as FCU-0005’syCmdOkand RTU-0002’syStageOk.- Instantaneous samples instead of rolling averages. G36’s AHU set computes every signal as a 5-minute rolling average; whether §5.22.6 does is unknown here. Persistence is not equivalent — averaging tolerates a signal whose mean sits outside the bound while it keeps crossing back, so a leak modulated by riser pressure or a valve hunting around its seat can hide indefinitely. A steady leak reads the same either way.
- A leak is only visible between calls for cooling. The rule is silent whenever the valve is open, so a valve that leaks all summer is detected in autumn and one on a unit in continuous cooling is never detected at all. That is inherent in the reference’s equation, and it is the structural reason the chapter calls the fault heating-dominant.
- Operating state OS#2 is host-enforced, and only half of it is testable
here. The graph’s command test covers the cooling half of “no active coils”;
the heating half is not tested, and that gap is benign — an active heating coil
raises
satand can only silence this rule, never trip it. Masking costs detections, not credibility. - The runtime formula is extended. To the chapter’s
(entering_temp − leaving_temp) × fcu_airflow × cp_airthis card adds air density (the product needs mass flow) and adds the fan’s rise back, since the measured drop is the coil’s work reduced by it — a quarter of the answer at the shipped threshold. - Severity 3 is the reference’s, and it disagrees with the AHU twin. AHU-0014/AHU-0015 carry severity 2, assigned by this library because the AHU reference has no card to state one. The difference is defensible on scale and is recorded rather than smoothed, because a host ranking a mixed fleet by severity will see the same physics at two levels.
clustersis empty. The chapter README calls FCU-0004/FCU-0005 the zone-scale members of the simultaneous-conditioning family, butclusters/clusters.jsonlists only AHU rules there and this card does not edit the cluster set. The relationship is carried byrelatedand the playbook.persist.delayOnInit = true(Modelica/CDL default isfalse), the library’s standing choice: a leak already present when the controller restarts waits out the full 30 minutes instead of alarming on the first tick.- The reference publishes no vectors for this card, so
vectors.jsonis authored from the equation.
Notes
Read yCmdOk before reading yFault. On a unit in cooling season it will be
false most of the day, and every yFault = false under it means “not
evaluated”, not “no leak”.
This card is one half of a pair the reference states symmetrically, and the
asymmetries are what to hold in mind: the sign of the subtraction is reversed
(rat − sat here, sat − rat in FCU-0005), fan heat pushes the two
measurements in opposite directions so the shared 3.0 °C is not a shared
sensitivity, and the emissions scope differs because the plants differ.
Everything else is identical, because nothing in the chapter distinguishes them.
The fcu-faults playbook orders the service.
Step 1.3 is the manual version of this rule — command the valve to 0% and
measure across the coil, where any measurable change confirms the leak, a
sharper test than this rule ships because a technician knows the fan is running
and can put a calibrated probe on both sides. Step 2.3 is the remote workaround:
lock the cooling valve out for the heating season while the unit waits for
parts. Step 3.4 ranks a fleet by |temp_change| × airflow × cp_air, which is
this card’s runtime estimator. Expect FCU-0002 on the same unit in heating
weather — if both are active, this one is the cause and that one the
consequence.
Test Vectors
15 scenarios, clock step 60 s over 5400 s.
| Scenario | Description |
|---|---|
healthy_unit_shows_fan_heat_only | Both valves shut and the fan running: 22.0 °C room air arrives at the discharge sensor at 22.5 °C. The only thing the air crossed was the fan, which put its shaft work into the stream, and the cooling coil took nothing back. drop = rat − sat = −0.5 °C, the healthy signature of a tight chilled-water valve |
drop_just_below_threshold | One tenth inside the allowance: rat − sat = 2.9 °C at a valve commanded shut. Silent — and worth reading with the fan-heat direction in mind, because the fan warms the air between the two sensors, so a measured 2.9 °C is a true coil drop of nearly 4 °C that this rule does not report (see Deviations) |
drop_exactly_at_threshold | Threshold edge: rat − sat = 3.0 °C exactly. dropBig is a strict >, which is the comparison the reference writes, so a drop sitting precisely on inactive_coil_threshold reads healthy |
drop_just_over_threshold | Threshold edge, other side: rat − sat = 3.1 °C clears the strict comparison on the first tick, and with delayOnInit the alarm asserts at exactly one alarm_delay (1800 s) |
cmd_just_below_closed_threshold | Command edge: a 6 °C drop with clg_vlv_cmd at 0.9%. Below cmd_closed_threshold the valve counts as shut, so yCmdOk holds and the alarm lands after alarm_delay. Between 0% and 1% a leak and a trickle of commanded flow are indistinguishable to this rule |
cmd_exactly_at_closed_threshold | Command edge: the same 6 °C drop with the command sitting on exactly 1.0%. vlvShut is a strict <, so the valve does not count as shut, yCmdOk goes false, and the rule has no verdict — the host reports NO_EVAL rather than health |
cmd_just_above_closed_threshold | Command edge, third side: 1.1% with the same drop. Silent for the same reason, and pinned so a future retune of cmd_closed_threshold cannot move the boundary unnoticed |
valve_open_and_cooling_normally | A fan coil doing its job: the valve is commanded 60% open and pulls 24 °C room air down to 14 °C. The largest drop in the set raises nothing, because yCmdOk is false and this rule has nothing to say about a coil that was asked to cool |
chilled_water_valve_leaking_through | The motivating case: the valve is commanded 0% and 22 °C room air still arrives at 16 °C. Correcting for the fan’s own rise, the coil is pulling something over 6 °C out of air nobody asked to cool — a worn seat, an actuator short of its close position, or a three-way bypass that is not sealing |
leak_starts_mid_run | A rising edge mid-run rather than at load: the unit runs healthy on fan heat alone and the riser is pressurized at t = 600 s, after which the drop holds at 6 °C. The alarm lands at exactly 600 + 1800 = 2400 s |
transient_clears_before_alarm_delay | Transient: a 6 °C drop persists for 1200 s while the chilled water standing in a just-closed coil gives up its cold, then the discharge returns to room air plus fan heat. Anything shorter than alarm_delay never reaches the operator, which is the delay’s main job on this fault |
fault_clears_on_recovery | A sustained 6 °C drop alarms at 1800 s; the coil is isolated at t = 3000 s and the discharge climbs back to room air plus fan heat. TrueDelay has no off-delay, so the alarm drops on that tick and the accumulated time is discarded |
zone_calls_for_cooling_before_alarm | Recovery through the command term: the drop never changes, but the zone calls for cooling at t = 1200 s and the valve opens to 40%. yCmdOk goes false, persistence restarts, and the leak that was 10 minutes from being reported is not reported. On a unit cycling in and out of cooling, this rule only ever sees leaks that outlast a full closed period |
fan_off_standing_water_reads_as_a_leak | The known hole, pinned so it cannot change silently: with the fan stopped, the discharge sensor sits in still air over a coil full of chilled water and reads 15 °C while the return sensor reads the room at 22 °C. No air is moving and nothing is being wasted, but the rule alarms. Canonical fan status now exists for FCU-0006, but this graph does not consume it, so this gate remains the host’s — see preconditions |
oscillating_drop_never_alarms | A leak modulated by riser pressure swings the drop between 6 °C and −0.5 °C on a 20-minute period. Its mean is well outside the allowance and a 5-minute rolling average would report the fault, but persistence restarts on every compliant tick, so this rule stays silent — the limit of substituting TrueDelay for averaging (see Deviations) |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 60,
"horizon_s": 5400
},
"scenarios": [
{
"name": "healthy_unit_shows_fan_heat_only",
"description": "Both valves shut and the fan running: 22.0 \u00b0C room air arrives at the discharge sensor at 22.5 \u00b0C. The only thing the air crossed was the fan, which put its shaft work into the stream, and the cooling coil took nothing back. `drop` = rat \u2212 sat = \u22120.5 \u00b0C, the healthy signature of a tight chilled-water valve",
"inputs": {
"clg_vlv_cmd": 0.0,
"sat": 22.5,
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "drop_just_below_threshold",
"description": "One tenth inside the allowance: rat \u2212 sat = 2.9 \u00b0C at a valve commanded shut. Silent \u2014 and worth reading with the fan-heat direction in mind, because the fan warms the air between the two sensors, so a measured 2.9 \u00b0C is a true coil drop of nearly 4 \u00b0C that this rule does not report (see Deviations)",
"inputs": {
"clg_vlv_cmd": 0.0,
"sat": 19.1,
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "drop_exactly_at_threshold",
"description": "Threshold edge: rat \u2212 sat = 3.0 \u00b0C exactly. `dropBig` is a strict `>`, which is the comparison the reference writes, so a drop sitting precisely on inactive_coil_threshold reads healthy",
"inputs": {
"clg_vlv_cmd": 0.0,
"sat": 19.0,
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "drop_just_over_threshold",
"description": "Threshold edge, other side: rat \u2212 sat = 3.1 \u00b0C clears the strict comparison on the first tick, and with delayOnInit the alarm asserts at exactly one alarm_delay (1800 s)",
"inputs": {
"clg_vlv_cmd": 0.0,
"sat": 18.9,
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1740,
"equals": false
},
{
"output": "yFault",
"from_s": 1800,
"to_s": 5400,
"equals": true
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "cmd_just_below_closed_threshold",
"description": "Command edge: a 6 \u00b0C drop with clg_vlv_cmd at 0.9%. Below cmd_closed_threshold the valve counts as shut, so yCmdOk holds and the alarm lands after alarm_delay. Between 0% and 1% a leak and a trickle of commanded flow are indistinguishable to this rule",
"inputs": {
"clg_vlv_cmd": 0.9,
"sat": 16.0,
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1740,
"equals": false
},
{
"output": "yFault",
"from_s": 1800,
"to_s": 5400,
"equals": true
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "cmd_exactly_at_closed_threshold",
"description": "Command edge: the same 6 \u00b0C drop with the command sitting on exactly 1.0%. `vlvShut` is a strict `<`, so the valve does not count as shut, yCmdOk goes false, and the rule has no verdict \u2014 the host reports NO_EVAL rather than health",
"inputs": {
"clg_vlv_cmd": 1.0,
"sat": 16.0,
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": false
}
]
},
{
"name": "cmd_just_above_closed_threshold",
"description": "Command edge, third side: 1.1% with the same drop. Silent for the same reason, and pinned so a future retune of cmd_closed_threshold cannot move the boundary unnoticed",
"inputs": {
"clg_vlv_cmd": 1.1,
"sat": 16.0,
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": false
}
]
},
{
"name": "valve_open_and_cooling_normally",
"description": "A fan coil doing its job: the valve is commanded 60% open and pulls 24 \u00b0C room air down to 14 \u00b0C. The largest drop in the set raises nothing, because yCmdOk is false and this rule has nothing to say about a coil that was asked to cool",
"inputs": {
"clg_vlv_cmd": 60.0,
"sat": 14.0,
"rat": 24.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": false
}
]
},
{
"name": "chilled_water_valve_leaking_through",
"description": "The motivating case: the valve is commanded 0% and 22 \u00b0C room air still arrives at 16 \u00b0C. Correcting for the fan's own rise, the coil is pulling something over 6 \u00b0C out of air nobody asked to cool \u2014 a worn seat, an actuator short of its close position, or a three-way bypass that is not sealing",
"inputs": {
"clg_vlv_cmd": 0.0,
"sat": 16.0,
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1740,
"equals": false
},
{
"output": "yFault",
"from_s": 1800,
"to_s": 5400,
"equals": true
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "leak_starts_mid_run",
"description": "A rising edge mid-run rather than at load: the unit runs healthy on fan heat alone and the riser is pressurized at t = 600 s, after which the drop holds at 6 \u00b0C. The alarm lands at exactly 600 + 1800 = 2400 s",
"inputs": {
"clg_vlv_cmd": 0.0,
"sat": [
{
"t": 0,
"value": 22.5
},
{
"t": 600,
"value": 16.0
}
],
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2340,
"equals": false
},
{
"output": "yFault",
"from_s": 2400,
"to_s": 5400,
"equals": true
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "transient_clears_before_alarm_delay",
"description": "Transient: a 6 \u00b0C drop persists for 1200 s while the chilled water standing in a just-closed coil gives up its cold, then the discharge returns to room air plus fan heat. Anything shorter than alarm_delay never reaches the operator, which is the delay's main job on this fault",
"inputs": {
"clg_vlv_cmd": 0.0,
"sat": [
{
"t": 0,
"value": 16.0
},
{
"t": 1200,
"value": 22.5
}
],
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "fault_clears_on_recovery",
"description": "A sustained 6 \u00b0C drop alarms at 1800 s; the coil is isolated at t = 3000 s and the discharge climbs back to room air plus fan heat. `TrueDelay` has no off-delay, so the alarm drops on that tick and the accumulated time is discarded",
"inputs": {
"clg_vlv_cmd": 0.0,
"sat": [
{
"t": 0,
"value": 16.0
},
{
"t": 3000,
"value": 22.5
}
],
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1740,
"equals": false
},
{
"output": "yFault",
"from_s": 1800,
"to_s": 2940,
"equals": true
},
{
"output": "yFault",
"from_s": 3000,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "zone_calls_for_cooling_before_alarm",
"description": "Recovery through the command term: the drop never changes, but the zone calls for cooling at t = 1200 s and the valve opens to 40%. yCmdOk goes false, persistence restarts, and the leak that was 10 minutes from being reported is not reported. On a unit cycling in and out of cooling, this rule only ever sees leaks that outlast a full closed period",
"inputs": {
"clg_vlv_cmd": [
{
"t": 0,
"value": 0.0
},
{
"t": 1200,
"value": 40.0
}
],
"sat": 16.0,
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 1140,
"equals": true
},
{
"output": "yCmdOk",
"from_s": 1200,
"to_s": 5400,
"equals": false
}
]
},
{
"name": "fan_off_standing_water_reads_as_a_leak",
"description": "The known hole, pinned so it cannot change silently: with the fan stopped, the discharge sensor sits in still air over a coil full of chilled water and reads 15 \u00b0C while the return sensor reads the room at 22 \u00b0C. No air is moving and nothing is being wasted, but the rule alarms. Canonical fan status now exists for FCU-0006, but this graph does not consume it, so this gate remains the host's \u2014 see preconditions",
"inputs": {
"clg_vlv_cmd": 0.0,
"sat": 15.0,
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1740,
"equals": false
},
{
"output": "yFault",
"from_s": 1800,
"to_s": 5400,
"equals": true
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
},
{
"name": "oscillating_drop_never_alarms",
"description": "A leak modulated by riser pressure swings the drop between 6 \u00b0C and \u22120.5 \u00b0C on a 20-minute period. Its mean is well outside the allowance and a 5-minute rolling average would report the fault, but persistence restarts on every compliant tick, so this rule stays silent \u2014 the limit of substituting TrueDelay for averaging (see Deviations)",
"inputs": {
"clg_vlv_cmd": 0.0,
"sat": [
{
"t": 0,
"value": 16.0
},
{
"t": 600,
"value": 22.5
},
{
"t": 1200,
"value": 16.0
},
{
"t": 1800,
"value": 22.5
},
{
"t": 2400,
"value": 16.0
},
{
"t": 3000,
"value": 22.5
},
{
"t": 3600,
"value": 16.0
},
{
"t": 4200,
"value": 22.5
},
{
"t": 4800,
"value": 16.0
}
],
"rat": 22.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 5400,
"equals": false
},
{
"output": "yCmdOk",
"from_s": 0,
"to_s": 5400,
"equals": true
}
]
}
]
}