FPB-0003 — Reheat valve closed with unintended temperature rise
| Status | verified — engine e2ff2f8, cxf:fnv1a128:4d6cece087e01b0d5e88794f4e8f2d30, 2026-08-20 |
| Severity | 3 |
| Method | rule |
| Phase | 2 |
| Category | CRITICAL_WASTE |
| Confidence | MEDIUM |
| Estimation | PROXY_ESTIMATION |
| G36 | — |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | FPB-0001, FPB-0002, FPB-0006, FCU-0005, VAV-0009 |
| Playbooks | fan-powered-terminal-faults |
| Source | Library leak-mechanism precedents FCU-0005 and VAV-0009 — air temperature rise across reheat while the hydronic valve is commanded shut; LBNL FDD simulated FPU dataset, DOI 10.25984/1881324 — documented PFPU fan/reheat branch and SFPU downstream-fan reheat topology; future validation source only; Library-authored instantaneous coil-local adaptation; no cited source publishes 5%, 3 K, or 600 s as portable FPB limits |
| Operating states | hydronic-reheat FPB states with proven airflow through the reheat coil and no legitimate reheat request |
Preconditions (host-enforced): Apply only to hydronic reheat with available hot water at meaningful temperature and pressure. fan_status must prove airflow through the evaluated coil path. Bind physical coil-local temperatures: SFPU downstream of the series fan/immediately upstream of the coil and immediately at coil outlet; PFPU immediately around the fan/reheat branch before primary/branch mixing. A mixed zone-discharge proxy is invalid unless the host supplies a validated derived coil-leaving estimate. Exclude freeze/exercise/commissioning, intentional minimum valve position, sensor faults, and warm-soak after a prior heating call. Inputs must be fresh and aligned; unmet obligations are NO_EVAL, not healthy.
Points: rht_vlv_cmd, rht_coil_entering_temp, rht_coil_leaving_temp, fan_status
Outputs:
yFault— True after proven coil airflow, a closed valve command, and material coil-local temperature rise persist for sustained_durationyValveClosed— Diagnostic sub-condition flag; true when rht_vlv_cmd is strictly below valve_closed_threshold. False never means NO_EVALyTemperatureRise— Diagnostic sub-condition flag; true when coil leaving minus entering temperature is strictly above temperature_rise_threshold. False never means NO_EVAL
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
valve_closed_threshold | 5.0 | % | valveClosed.t | ADOPTED_TUNABLE command below which the reheat valve counts as closed; equality is not closed. |
temperature_rise_threshold | 3.0 | K | riseHigh.t | ADOPTED_TUNABLE coil-local rise allowance above sensor error, fan heat, piping migration, and normal residual heat; equality is clear. |
sustained_duration | 600.0 | s | persist.delayTime | ADOPTED_TUNABLE continuous signature duration; no existing 600 s leakage rule or cited source establishes a portable precedent. |
Description
This rule identifies heat added across a hydronic FPB reheat coil while its valve is commanded shut and airflow through that coil is proven. Coil-local measurement is essential, especially before PFPU branch air mixes with primary air.
Detection Logic
valve_closed = rht_vlv_cmd < valve_closed_threshold
rise = rht_coil_leaving_temp - rht_coil_entering_temp
rise_high = rise > temperature_rise_threshold
yValveClosed = valve_closed
yTemperatureRise = rise_high
yFault = fan_status AND valve_closed AND rise_high,
sustained for sustained_duration
Both thresholds are strict. The complete three-part candidate feeds one
TrueDelay(delayOnInit=true); loss of any premise clears a mature alarm immediately.
Possible Diagnoses
- Valve seat passing from wear, debris, fouling, or unsuitable close-off pressure.
- Actuator/linkage not reaching the seat despite a closed command.
- Manual bypass, three-way piping, or unintended gravity circulation.
- Residual hot-water availability or warm-soak not excluded by the host.
- Entering/leaving sensor bias, swap, or PFPU mixed-discharge misbinding.
Energy Impact
Leaked reheat can be paid for twice when primary cooling removes it again. Temperature rise is only a proxy; PFPU primary airflow is not automatically the branch coil airflow required to turn that rise into thermal power.
Emissions Impact
Scope 1 and/or 2 depends on heating and cooling sources. Use validated coil airflow, rise, runtime, and source-specific factors before quantifying savings.
Deviations
- All three defaults are adopted. The brief labeled 600 s as library precedent, but no existing 600 s leakage rule supports that claim; it is recorded as
ADOPTED_TUNABLEinstead. - The rule is hydronic-only. Electric reheat needs proof/status logic and safety treatment rather than a fictitious valve command.
- Temperatures are coil-local. This is stricter than common VAV/FCU proxies and prevents PFPU branch mixing from erasing the signature.
- Fan proof is in-graph, but other gates remain host-side. Hot-water availability, warm-soak, freeze/exercise, and point quality still determine evaluability.
- The rule is related to leak siblings but not placed in CLU-01. A single AHU simultaneous-command repair cannot reliably clear a physically passing terminal valve.
- No empirical FPR or TPR is claimed. LBNL replay and mapping are deferred to PR11.
Test Vectors
16 scenarios, clock step 60 s over 1800 s.
| Scenario | Description |
|---|---|
closed_valve_no_rise | A shut valve with equal coil inlet/outlet temperature is healthy. |
open_valve_with_rise | Heating with an open valve is legitimate and cannot enter the leak candidate. |
fan_off_with_rise | A temperature difference without proven branch airflow is not evaluated by the fault output. |
sustained_closed_valve_rise | Closed valve, proven fan, and a 3.1 K rise mature after the 600 s persistence margin. |
valve_exactly_at_closed_threshold | Exactly 5% is not closed under the strict less-than comparison. |
valve_just_below_closed_threshold | At 4.9%, the valve is classified closed and a sustained 3.1 K rise alarms. |
rise_exactly_at_threshold | Exactly 3.0 K is clear under the strict greater-than comparison. |
rise_just_above_threshold | One tenth kelvin above the rise threshold exercises the positive edge. |
negative_temperature_rise | Leaving air colder than entering air cannot satisfy the unintended-heating direction. |
violation_ends_one_step_before_persistence | Residual rise clears at 540 s, one evaluator step before persistence, so no alarm matures. |
mature_fault_recovers_with_temperature | A mature alarm clears immediately when the coil rise disappears. |
mature_fault_recovers_when_valve_opens | A legitimate call for heat clears the mature leak verdict on the same tick. |
mature_fault_recovers_when_fan_stops | Loss of airflow proof removes evaluability from the verdict and clears a mature alarm. |
two_short_rise_episodes_reset | Two short rises separated by a no-rise interval cannot accumulate timer age. |
normal_residual_heat_raw_transient | A four-minute post-call warm-coil transient is rejected by persistence; hosts should still exclude known warm-soak windows. |
pfpu_mixed_discharge_is_invalid_binding | A diluted mixed discharge can hide a real branch-local coil rise; this raw clear result pins why PFPU sensors must remain immediately around the reheat coil. |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 60,
"horizon_s": 1800
},
"scenarios": [
{
"name": "closed_valve_no_rise",
"description": "A shut valve with equal coil inlet/outlet temperature is healthy.",
"inputs": {
"rht_vlv_cmd": 0.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 20.0,
"fan_status": true
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 1800,
"equals": true
},
{
"output": "yTemperatureRise",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "open_valve_with_rise",
"description": "Heating with an open valve is legitimate and cannot enter the leak candidate.",
"inputs": {
"rht_vlv_cmd": 50.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 25.0,
"fan_status": true
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yTemperatureRise",
"from_s": 0,
"to_s": 1800,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "fan_off_with_rise",
"description": "A temperature difference without proven branch airflow is not evaluated by the fault output.",
"inputs": {
"rht_vlv_cmd": 0.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 25.0,
"fan_status": false
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 1800,
"equals": true
},
{
"output": "yTemperatureRise",
"from_s": 0,
"to_s": 1800,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "sustained_closed_valve_rise",
"description": "Closed valve, proven fan, and a 3.1 K rise mature after the 600 s persistence margin.",
"inputs": {
"rht_vlv_cmd": 0.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 23.1,
"fan_status": true
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 1800,
"equals": true
},
{
"output": "yTemperatureRise",
"from_s": 0,
"to_s": 1800,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 540,
"equals": false
},
{
"output": "yFault",
"from_s": 660,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "valve_exactly_at_closed_threshold",
"description": "Exactly 5% is not closed under the strict less-than comparison.",
"inputs": {
"rht_vlv_cmd": 5.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 25.0,
"fan_status": true
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yTemperatureRise",
"from_s": 0,
"to_s": 1800,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "valve_just_below_closed_threshold",
"description": "At 4.9%, the valve is classified closed and a sustained 3.1 K rise alarms.",
"inputs": {
"rht_vlv_cmd": 4.9,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 23.1,
"fan_status": true
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 1800,
"equals": true
},
{
"output": "yTemperatureRise",
"from_s": 0,
"to_s": 1800,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 540,
"equals": false
},
{
"output": "yFault",
"from_s": 660,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "rise_exactly_at_threshold",
"description": "Exactly 3.0 K is clear under the strict greater-than comparison.",
"inputs": {
"rht_vlv_cmd": 0.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 23.0,
"fan_status": true
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 1800,
"equals": true
},
{
"output": "yTemperatureRise",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "rise_just_above_threshold",
"description": "One tenth kelvin above the rise threshold exercises the positive edge.",
"inputs": {
"rht_vlv_cmd": 0.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 23.1,
"fan_status": true
},
"expect": [
{
"output": "yTemperatureRise",
"from_s": 0,
"to_s": 1800,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 540,
"equals": false
},
{
"output": "yFault",
"from_s": 660,
"to_s": 1800,
"equals": true
}
]
},
{
"name": "negative_temperature_rise",
"description": "Leaving air colder than entering air cannot satisfy the unintended-heating direction.",
"inputs": {
"rht_vlv_cmd": 0.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 18.0,
"fan_status": true
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 1800,
"equals": true
},
{
"output": "yTemperatureRise",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "violation_ends_one_step_before_persistence",
"description": "Residual rise clears at 540 s, one evaluator step before persistence, so no alarm matures.",
"inputs": {
"rht_vlv_cmd": 0.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": [
{
"t": 0,
"value": 24.0
},
{
"t": 540,
"value": 20.0
}
],
"fan_status": true
},
"expect": [
{
"output": "yTemperatureRise",
"from_s": 0,
"to_s": 480,
"equals": true
},
{
"output": "yTemperatureRise",
"from_s": 540,
"to_s": 1800,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "mature_fault_recovers_with_temperature",
"description": "A mature alarm clears immediately when the coil rise disappears.",
"inputs": {
"rht_vlv_cmd": 0.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": [
{
"t": 0,
"value": 24.0
},
{
"t": 900,
"value": 20.0
}
],
"fan_status": true
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 540,
"equals": false
},
{
"output": "yFault",
"from_s": 660,
"to_s": 840,
"equals": true
},
{
"output": "yFault",
"from_s": 900,
"to_s": 1800,
"equals": false
},
{
"output": "yTemperatureRise",
"from_s": 0,
"to_s": 840,
"equals": true
},
{
"output": "yTemperatureRise",
"from_s": 900,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "mature_fault_recovers_when_valve_opens",
"description": "A legitimate call for heat clears the mature leak verdict on the same tick.",
"inputs": {
"rht_vlv_cmd": [
{
"t": 0,
"value": 0.0
},
{
"t": 900,
"value": 50.0
}
],
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 24.0,
"fan_status": true
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 540,
"equals": false
},
{
"output": "yFault",
"from_s": 660,
"to_s": 840,
"equals": true
},
{
"output": "yFault",
"from_s": 900,
"to_s": 1800,
"equals": false
},
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 840,
"equals": true
},
{
"output": "yValveClosed",
"from_s": 900,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "mature_fault_recovers_when_fan_stops",
"description": "Loss of airflow proof removes evaluability from the verdict and clears a mature alarm.",
"inputs": {
"rht_vlv_cmd": 0.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 24.0,
"fan_status": [
{
"t": 0,
"value": true
},
{
"t": 900,
"value": false
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 540,
"equals": false
},
{
"output": "yFault",
"from_s": 660,
"to_s": 840,
"equals": true
},
{
"output": "yFault",
"from_s": 900,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "two_short_rise_episodes_reset",
"description": "Two short rises separated by a no-rise interval cannot accumulate timer age.",
"inputs": {
"rht_vlv_cmd": 0.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": [
{
"t": 0,
"value": 24.0
},
{
"t": 240,
"value": 20.0
},
{
"t": 480,
"value": 24.0
},
{
"t": 720,
"value": 20.0
}
],
"fan_status": true
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "normal_residual_heat_raw_transient",
"description": "A four-minute post-call warm-coil transient is rejected by persistence; hosts should still exclude known warm-soak windows.",
"inputs": {
"rht_vlv_cmd": 0.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": [
{
"t": 0,
"value": 24.0
},
{
"t": 240,
"value": 20.0
}
],
"fan_status": true
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
},
{
"name": "pfpu_mixed_discharge_is_invalid_binding",
"description": "A diluted mixed discharge can hide a real branch-local coil rise; this raw clear result pins why PFPU sensors must remain immediately around the reheat coil.",
"inputs": {
"rht_vlv_cmd": 0.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 22.0,
"fan_status": true
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 1800,
"equals": true
},
{
"output": "yTemperatureRise",
"from_s": 0,
"to_s": 1800,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 1800,
"equals": false
}
]
}
]
}