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-0003 — Reheat valve closed with unintended temperature rise

Statusverified — engine e2ff2f8, cxf:fnv1a128:4d6cece087e01b0d5e88794f4e8f2d30, 2026-08-20
Severity3
Methodrule
Phase2
CategoryCRITICAL_WASTE
ConfidenceMEDIUM
EstimationPROXY_ESTIMATION
G36
Clusters
Suppresses
Suppressed by
RelatedFPB-0001, FPB-0002, FPB-0006, FCU-0005, VAV-0009
Playbooksfan-powered-terminal-faults
SourceLibrary 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 stateshydronic-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_duration
  • yValveClosed — Diagnostic sub-condition flag; true when rht_vlv_cmd is strictly below valve_closed_threshold. False never means NO_EVAL
  • yTemperatureRise — Diagnostic sub-condition flag; true when coil leaving minus entering temperature is strictly above temperature_rise_threshold. False never means NO_EVAL

Parameters:

NameDefaultUnitCXF pathDescription
valve_closed_threshold5.0%valveClosed.tADOPTED_TUNABLE command below which the reheat valve counts as closed; equality is not closed.
temperature_rise_threshold3.0KriseHigh.tADOPTED_TUNABLE coil-local rise allowance above sensor error, fan heat, piping migration, and normal residual heat; equality is clear.
sustained_duration600.0spersist.delayTimeADOPTED_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

FPB-0003 block graph

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

  1. Valve seat passing from wear, debris, fouling, or unsuitable close-off pressure.
  2. Actuator/linkage not reaching the seat despite a closed command.
  3. Manual bypass, three-way piping, or unintended gravity circulation.
  4. Residual hot-water availability or warm-soak not excluded by the host.
  5. 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_TUNABLE instead.
  • 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.

ScenarioDescription
closed_valve_no_riseA shut valve with equal coil inlet/outlet temperature is healthy.
open_valve_with_riseHeating with an open valve is legitimate and cannot enter the leak candidate.
fan_off_with_riseA temperature difference without proven branch airflow is not evaluated by the fault output.
sustained_closed_valve_riseClosed valve, proven fan, and a 3.1 K rise mature after the 600 s persistence margin.
valve_exactly_at_closed_thresholdExactly 5% is not closed under the strict less-than comparison.
valve_just_below_closed_thresholdAt 4.9%, the valve is classified closed and a sustained 3.1 K rise alarms.
rise_exactly_at_thresholdExactly 3.0 K is clear under the strict greater-than comparison.
rise_just_above_thresholdOne tenth kelvin above the rise threshold exercises the positive edge.
negative_temperature_riseLeaving air colder than entering air cannot satisfy the unintended-heating direction.
violation_ends_one_step_before_persistenceResidual rise clears at 540 s, one evaluator step before persistence, so no alarm matures.
mature_fault_recovers_with_temperatureA mature alarm clears immediately when the coil rise disappears.
mature_fault_recovers_when_valve_opensA legitimate call for heat clears the mature leak verdict on the same tick.
mature_fault_recovers_when_fan_stopsLoss of airflow proof removes evaluability from the verdict and clears a mature alarm.
two_short_rise_episodes_resetTwo short rises separated by a no-rise interval cannot accumulate timer age.
normal_residual_heat_raw_transientA four-minute post-call warm-coil transient is rejected by persistence; hosts should still exclude known warm-soak windows.
pfpu_mixed_discharge_is_invalid_bindingA 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
        }
      ]
    }
  ]
}