FPB-0006 — Reheat-coil heat-transfer degradation
| Status | verified — engine e2ff2f8, cxf:fnv1a128:573e7d690f4b0f4e699a19df07db6869, 2026-08-20 |
| Severity | 3 |
| Method | statistical |
| Phase | 3 |
| Category | COMFORT_ENERGY |
| Confidence | MEDIUM |
| Estimation | PROXY_ESTIMATION |
| G36 | — |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | FPB-0003, FPB-0004, HW-0010 |
| Playbooks | fan-powered-terminal-faults |
| Source | LBNL FDD simulated FPU dataset, DOI 10.25984/1881324 — PFPU/SFPU reheat topology and coil-fouling fault classes; future empirical replay source, not threshold evidence; Library host-baseline precedent PMP-0006 and coil-performance precedents AHU-0038/CHW-0005/CHW-0006 — positive baseline, in-domain gate, and non-causal degradation naming; Library-authored thresholds and persistence; no cited source publishes 90%, 3 K, 30%, or 900 s as portable FPB limits |
| Operating states | hydronic-reheat FPB at stable near-full heat with proven coil-path airflow and a ready expected-delta-T model |
Preconditions (host-enforced): Apply only to hydronic reheat. Hot-water supply must be available at adequate temperature, flow, and pressure; plant tracking faults make this verdict NO_EVAL. rht_delta_t_expected must be trained on known-good operation, frozen/versioned, fresh, positive, and in-domain for current fan/airflow, entering temperature, valve state, and hot-water condition. Bind the same coil-local temperatures required by FPB-0003, including PFPU branch-local sensors before mixing. fan_status must prove airflow through that coil path. Exclude startup warm-up, valve exercise, freeze protection, maintenance, unstable fan/airflow, sensor faults, and plant transitions. minimum_expected_delta_t is adoption-blocking; yBaselineOk proves only numerical positivity.
Points: rht_vlv_cmd, rht_coil_entering_temp, rht_coil_leaving_temp, rht_delta_t_expected, fan_status
Outputs:
yFault— True after proven fan operation, near-full heat command, a valid expected rise, and material actual-rise deficit persist for sustained_durationyBaselineOk— Evaluability flag — true only when rht_delta_t_expected is strictly above minimum_expected_delta_t. FALSE MEANS NO_EVALyFullHeatCommand— Diagnostic sub-condition flag; true when rht_vlv_cmd is strictly above full_command_threshold. False never means NO_EVALyTemperatureRiseLow— Diagnostic sub-condition flag; true when a numerically valid expected rise exceeds actual coil rise by more than the allowed fraction. False never means NO_EVAL
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
full_command_threshold | 90.0 | % | fullHeat.t | ADOPTED_TUNABLE command above which the valve counts as near-full heat; equality is clear. |
minimum_expected_delta_t | 3.0 | K | baselineOk.t | NO_PORTABLE_DEFAULT and adoption-blocking expected-rise floor; equality is NO_EVAL. |
max_delta_t_drop_fraction | 0.3 | 1 | allowance.k | ADOPTED_TUNABLE maximum actual-rise deficit as a fraction of positive expected rise; strict comparison. |
sustained_duration | 900.0 | s | persist.delayTime | ADOPTED_TUNABLE continuous low-transfer duration; commission against coil, valve, fan, and hot-water response. |
Description
This rule detects actual reheat-coil air temperature rise materially below a known-good current-condition expectation at proven airflow and near-full valve command. It reports heat-transfer degradation without overclaiming fouling.
Detection Logic
actual_rise = rht_coil_leaving_temp - rht_coil_entering_temp
baseline_ok = rht_delta_t_expected > minimum_expected_delta_t
full_heat = rht_vlv_cmd > full_command_threshold
allowance = rht_delta_t_expected * max_delta_t_drop_fraction
rise_low = baseline_ok AND (rht_delta_t_expected - actual_rise) > allowance
yBaselineOk = baseline_ok
yFullHeatCommand = full_heat
yTemperatureRiseLow = rise_low
yFault = fan_status AND full_heat AND rise_low, sustained for sustained_duration
The relative deficit is cross-multiplied in the positive-baseline domain;
there is no Divide. Persistence uses delayOnInit=true.
Possible Diagnoses
- Air- or water-side fouling, coil air bypass, or obstructed flow path.
- Low hot-water temperature/flow/pressure or serving-plant tracking failure.
- Valve/actuator/linkage not delivering the commanded full stroke.
- Excessive or mis-modeled airflow/fan condition.
- Sensor bias/location, actual/expected scope mismatch, or bad baseline model.
Energy Impact
Poor transfer can extend heating runtime or miss comfort. The temperature-rise deficit is a performance proxy, not automatically avoidable thermal energy.
Emissions Impact
Scope 1/2 qualitative by the hot-water source and compensating equipment.
Deviations
- The rule names observable degradation, not fouling. Hydraulic, actuator, airflow, plant, sensor, and model causes share the signature.
- No Divide block is used. Cross-multiplication is equivalent only in the positive expected-rise domain enforced by yBaselineOk.
- The expected point is site-fitted. Known-good training, inputs, model readiness, and hot-water condition stay host-side.
- All defaults require commissioning. 3 K is adoption-blocking; 90%, 30%, and 900 s are adopted tunables.
- No empirical FPR/TPR is claimed. LBNL mapping/replay is deferred to PR11.
Test Vectors
18 scenarios, clock step 60 s over 2100 s.
| Scenario | Description |
|---|---|
full_heat_meets_expected_rise | Full heat with actual coil rise equal to its valid expected baseline is healthy. |
baseline_zero_is_no_eval | A zero expected delta-T is numerically invalid and safe without Divide. |
baseline_just_below_minimum | A 2.9 K expected delta-T is just below the adoption-blocking floor and remains NO_EVAL. |
baseline_exactly_at_minimum | Exactly 3 K is NO_EVAL under strict Greater. |
baseline_just_above_minimum | 3.1 K is evaluable when actual rise matches it. |
partial_command_low_rise | Low rise at partial valve command remains diagnostically visible but cannot mature a full-heat degradation fault. |
command_exactly_at_full_threshold | Exactly 90% is not full heat under the strict comparison. |
drop_exactly_at_threshold | Actual 7 K against expected 10 K makes residual equal the 3 K allowance and remains clear. |
both_just_beyond_thresholds | Command 90.1% and actual rise 6.9 K put both strict comparisons just beyond their boundaries. |
zero_actual_rise | Zero actual rise is a strong low-transfer signature under an otherwise valid full-heat baseline. |
negative_actual_rise | A negative measured rise also trips the one-sided low-transfer residual; sensor binding is among the diagnoses. |
fan_off_low_rise | The diagnostic remains visible, but no fan proof means the final verdict stays clear/NO_EVAL. |
hot_water_unavailable_raw_signature | Full command with no hot-water availability produces the raw fault; the host must report NO_EVAL rather than diagnose coil degradation. |
violation_ends_one_step_before_persistence | Low transfer clears at 840 s, one step before persistence, and never alarms. |
mature_fault_recovers | A mature degradation alarm clears when coil rise recovers to expected. |
command_release_resets_timer | Valve command backs below full heat before persistence and resets the candidate. |
two_short_low_transfer_episodes | Separated short degradation episodes cannot accumulate timer age. |
expected_baseline_change_resets_candidate | A valid expected-model change removes the residual and resets persistence. |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 60,
"horizon_s": 2100
},
"scenarios": [
{
"name": "full_heat_meets_expected_rise",
"description": "Full heat with actual coil rise equal to its valid expected baseline is healthy.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 30.0,
"rht_delta_t_expected": 10.0,
"fan_status": true
},
"expect": [
{
"output": "yBaselineOk",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yFullHeatCommand",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 2100,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 2100,
"equals": false
}
]
},
{
"name": "baseline_zero_is_no_eval",
"description": "A zero expected delta-T is numerically invalid and safe without Divide.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 20.0,
"rht_delta_t_expected": 0.0,
"fan_status": true
},
"expect": [
{
"output": "yBaselineOk",
"from_s": 0,
"to_s": 2100,
"equals": false
},
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 2100,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 2100,
"equals": false
}
]
},
{
"name": "baseline_just_below_minimum",
"description": "A 2.9 K expected delta-T is just below the adoption-blocking floor and remains NO_EVAL.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 20.0,
"rht_delta_t_expected": 2.9,
"fan_status": true
},
"expect": [
{
"output": "yBaselineOk",
"from_s": 0,
"to_s": 2100,
"equals": false
},
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 2100,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 2100,
"equals": false
}
]
},
{
"name": "baseline_exactly_at_minimum",
"description": "Exactly 3 K is NO_EVAL under strict Greater.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 20.0,
"rht_delta_t_expected": 3.0,
"fan_status": true
},
"expect": [
{
"output": "yBaselineOk",
"from_s": 0,
"to_s": 2100,
"equals": false
},
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 2100,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 2100,
"equals": false
}
]
},
{
"name": "baseline_just_above_minimum",
"description": "3.1 K is evaluable when actual rise matches it.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 23.1,
"rht_delta_t_expected": 3.1,
"fan_status": true
},
"expect": [
{
"output": "yBaselineOk",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 2100,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 2100,
"equals": false
}
]
},
{
"name": "partial_command_low_rise",
"description": "Low rise at partial valve command remains diagnostically visible but cannot mature a full-heat degradation fault.",
"inputs": {
"rht_vlv_cmd": 60.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 25.0,
"rht_delta_t_expected": 10.0,
"fan_status": true
},
"expect": [
{
"output": "yBaselineOk",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yFullHeatCommand",
"from_s": 0,
"to_s": 2100,
"equals": false
},
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 2100,
"equals": false
}
]
},
{
"name": "command_exactly_at_full_threshold",
"description": "Exactly 90% is not full heat under the strict comparison.",
"inputs": {
"rht_vlv_cmd": 90.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 25.0,
"rht_delta_t_expected": 10.0,
"fan_status": true
},
"expect": [
{
"output": "yFullHeatCommand",
"from_s": 0,
"to_s": 2100,
"equals": false
},
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 2100,
"equals": false
}
]
},
{
"name": "drop_exactly_at_threshold",
"description": "Actual 7 K against expected 10 K makes residual equal the 3 K allowance and remains clear.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 27.0,
"rht_delta_t_expected": 10.0,
"fan_status": true
},
"expect": [
{
"output": "yBaselineOk",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yFullHeatCommand",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 2100,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 2100,
"equals": false
}
]
},
{
"name": "both_just_beyond_thresholds",
"description": "Command 90.1% and actual rise 6.9 K put both strict comparisons just beyond their boundaries.",
"inputs": {
"rht_vlv_cmd": 90.1,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 26.9,
"rht_delta_t_expected": 10.0,
"fan_status": true
},
"expect": [
{
"output": "yBaselineOk",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yFullHeatCommand",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 2100,
"equals": true
}
]
},
{
"name": "zero_actual_rise",
"description": "Zero actual rise is a strong low-transfer signature under an otherwise valid full-heat baseline.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 20.0,
"rht_delta_t_expected": 10.0,
"fan_status": true
},
"expect": [
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 2100,
"equals": true
}
]
},
{
"name": "negative_actual_rise",
"description": "A negative measured rise also trips the one-sided low-transfer residual; sensor binding is among the diagnoses.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 18.0,
"rht_delta_t_expected": 10.0,
"fan_status": true
},
"expect": [
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 2100,
"equals": true
}
]
},
{
"name": "fan_off_low_rise",
"description": "The diagnostic remains visible, but no fan proof means the final verdict stays clear/NO_EVAL.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 20.0,
"rht_delta_t_expected": 10.0,
"fan_status": false
},
"expect": [
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 2100,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 2100,
"equals": false
}
]
},
{
"name": "hot_water_unavailable_raw_signature",
"description": "Full command with no hot-water availability produces the raw fault; the host must report NO_EVAL rather than diagnose coil degradation.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 20.0,
"rht_delta_t_expected": 10.0,
"fan_status": true
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 2100,
"equals": true
}
]
},
{
"name": "violation_ends_one_step_before_persistence",
"description": "Low transfer clears at 840 s, one step before persistence, and never alarms.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": [
{
"t": 0,
"value": 24.0
},
{
"t": 840,
"value": 30.0
}
],
"rht_delta_t_expected": 10.0,
"fan_status": true
},
"expect": [
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 780,
"equals": true
},
{
"output": "yTemperatureRiseLow",
"from_s": 840,
"to_s": 2100,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 2100,
"equals": false
}
]
},
{
"name": "mature_fault_recovers",
"description": "A mature degradation alarm clears when coil rise recovers to expected.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": [
{
"t": 0,
"value": 24.0
},
{
"t": 1200,
"value": 30.0
}
],
"rht_delta_t_expected": 10.0,
"fan_status": true
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 1140,
"equals": true
},
{
"output": "yFault",
"from_s": 1200,
"to_s": 2100,
"equals": false
},
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 1140,
"equals": true
},
{
"output": "yTemperatureRiseLow",
"from_s": 1200,
"to_s": 2100,
"equals": false
}
]
},
{
"name": "command_release_resets_timer",
"description": "Valve command backs below full heat before persistence and resets the candidate.",
"inputs": {
"rht_vlv_cmd": [
{
"t": 0,
"value": 100.0
},
{
"t": 600,
"value": 80.0
}
],
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 24.0,
"rht_delta_t_expected": 10.0,
"fan_status": true
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2100,
"equals": false
},
{
"output": "yFullHeatCommand",
"from_s": 0,
"to_s": 540,
"equals": true
},
{
"output": "yFullHeatCommand",
"from_s": 600,
"to_s": 2100,
"equals": false
}
]
},
{
"name": "two_short_low_transfer_episodes",
"description": "Separated short degradation episodes cannot accumulate timer age.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": [
{
"t": 0,
"value": 24.0
},
{
"t": 360,
"value": 30.0
},
{
"t": 600,
"value": 24.0
},
{
"t": 960,
"value": 30.0
}
],
"rht_delta_t_expected": 10.0,
"fan_status": true
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2100,
"equals": false
}
]
},
{
"name": "expected_baseline_change_resets_candidate",
"description": "A valid expected-model change removes the residual and resets persistence.",
"inputs": {
"rht_vlv_cmd": 100.0,
"rht_coil_entering_temp": 20.0,
"rht_coil_leaving_temp": 26.9,
"rht_delta_t_expected": [
{
"t": 0,
"value": 10.0
},
{
"t": 600,
"value": 6.0
}
],
"fan_status": true
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 2100,
"equals": false
},
{
"output": "yTemperatureRiseLow",
"from_s": 0,
"to_s": 540,
"equals": true
},
{
"output": "yTemperatureRiseLow",
"from_s": 600,
"to_s": 2100,
"equals": false
}
]
}
]
}