Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FPB-0006 — Reheat-coil heat-transfer degradation

Statusverified — engine e2ff2f8, cxf:fnv1a128:573e7d690f4b0f4e699a19df07db6869, 2026-08-20
Severity3
Methodstatistical
Phase3
CategoryCOMFORT_ENERGY
ConfidenceMEDIUM
EstimationPROXY_ESTIMATION
G36
Clusters
Suppresses
Suppressed by
RelatedFPB-0003, FPB-0004, HW-0010
Playbooksfan-powered-terminal-faults
SourceLBNL 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 stateshydronic-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_duration
  • yBaselineOk — Evaluability flag — true only when rht_delta_t_expected is strictly above minimum_expected_delta_t. FALSE MEANS NO_EVAL
  • yFullHeatCommand — Diagnostic sub-condition flag; true when rht_vlv_cmd is strictly above full_command_threshold. False never means NO_EVAL
  • yTemperatureRiseLow — 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:

NameDefaultUnitCXF pathDescription
full_command_threshold90.0%fullHeat.tADOPTED_TUNABLE command above which the valve counts as near-full heat; equality is clear.
minimum_expected_delta_t3.0KbaselineOk.tNO_PORTABLE_DEFAULT and adoption-blocking expected-rise floor; equality is NO_EVAL.
max_delta_t_drop_fraction0.31allowance.kADOPTED_TUNABLE maximum actual-rise deficit as a fraction of positive expected rise; strict comparison.
sustained_duration900.0spersist.delayTimeADOPTED_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

FPB-0006 block graph

The relative deficit is cross-multiplied in the positive-baseline domain; there is no Divide. Persistence uses delayOnInit=true.

Possible Diagnoses

  1. Air- or water-side fouling, coil air bypass, or obstructed flow path.
  2. Low hot-water temperature/flow/pressure or serving-plant tracking failure.
  3. Valve/actuator/linkage not delivering the commanded full stroke.
  4. Excessive or mis-modeled airflow/fan condition.
  5. 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.

ScenarioDescription
full_heat_meets_expected_riseFull heat with actual coil rise equal to its valid expected baseline is healthy.
baseline_zero_is_no_evalA zero expected delta-T is numerically invalid and safe without Divide.
baseline_just_below_minimumA 2.9 K expected delta-T is just below the adoption-blocking floor and remains NO_EVAL.
baseline_exactly_at_minimumExactly 3 K is NO_EVAL under strict Greater.
baseline_just_above_minimum3.1 K is evaluable when actual rise matches it.
partial_command_low_riseLow rise at partial valve command remains diagnostically visible but cannot mature a full-heat degradation fault.
command_exactly_at_full_thresholdExactly 90% is not full heat under the strict comparison.
drop_exactly_at_thresholdActual 7 K against expected 10 K makes residual equal the 3 K allowance and remains clear.
both_just_beyond_thresholdsCommand 90.1% and actual rise 6.9 K put both strict comparisons just beyond their boundaries.
zero_actual_riseZero actual rise is a strong low-transfer signature under an otherwise valid full-heat baseline.
negative_actual_riseA negative measured rise also trips the one-sided low-transfer residual; sensor binding is among the diagnoses.
fan_off_low_riseThe diagnostic remains visible, but no fan proof means the final verdict stays clear/NO_EVAL.
hot_water_unavailable_raw_signatureFull 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_persistenceLow transfer clears at 840 s, one step before persistence, and never alarms.
mature_fault_recoversA mature degradation alarm clears when coil rise recovers to expected.
command_release_resets_timerValve command backs below full heat before persistence and resets the candidate.
two_short_low_transfer_episodesSeparated short degradation episodes cannot accumulate timer age.
expected_baseline_change_resets_candidateA 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
        }
      ]
    }
  ]
}