HX-0003 — Heat transfer persists with control valve commanded closed
| Status | verified — engine e2ff2f8, cxf:fnv1a128:aff5ae932f23f7127de8a90723fadb04, 2026-08-20 |
| Severity | 3 |
| Method | rule |
| Phase | 2 |
| Category | CRITICAL_WASTE |
| Confidence | MEDIUM |
| Estimation | DIRECT_MEASUREMENT |
| G36 | — |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | HX-0001, HX-0002 |
| Playbooks | hydronic-heat-exchanger-faults |
| Source | EnergyPlus 25.1 Engineering Reference, Heat Exchangers — signed heat transfer from both capacity rates and inlet temperatures: https://bigladdersoftware.com/epx/docs/25-1/engineering-reference/heat-exchangers.html; DOE FEMP, Energy Management Information System Capabilities — monitoring reduced/changed HX heat transfer from temperature evidence for condition-based maintenance |
| Operating states | The HX’s intended isolating valve is finally commanded closed, residual transport/thermal soak has expired, and validated branch measurements can establish signed heat transfer |
Preconditions (host-enforced): The named valve must be intended to isolate the entire monitored HX exchange path; its point is the final physical output after minimum-position, exercise, local/HAND, freeze, and protective logic. control_valve_cmd and the four temperatures/two flows behind heat_transfer_rate must share one HX scope and aligned timestamps. The host publishes signed transfer only after positive finite flow/capacity, fluid-property, point-quality, and side-energy-balance checks; invalid derivation means NO_EVAL. Exclude a site-commissioned transport/thermal-soak interval after closure and after direction, setpoint, pump, or valve changes. Commission the transfer limit above the closed/no-load uncertainty envelope. Natural circulation, a manual/parallel bypass, and a failed check valve remain valid unintended paths rather than suppression reasons. Passive exchangers without an isolating valve are excluded.
Points: control_valve_cmd, heat_transfer_rate
Outputs:
yFault— True after absolute heat transfer remains above the commissioned no-load limit with the isolating valve command below its closed limit for alarm_delayyValveClosed— Immediate diagnostic sub-condition; false never means NO_EVALyTransferPresent— Immediate direction-independent diagnostic sub-condition based on absolute signed transfer; false never means NO_EVAL
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
closed_command_limit | 5.0 | % | valveClosed.t | ADOPTED_TUNABLE library valve-closed starting point. The strict comparator treats exactly 5% as not closed; align with actuator scaling and any intentional minimum position. |
unexpected_transfer_limit | 5.0 | kW | transferHigh.t | NO_PORTABLE_DEFAULT executable placeholder. Commission above reconciled closed/no-load transfer uncertainty, meter resolution, and residual-loss envelope, and below the minimum actionable unintended exchange. |
alarm_delay | 900.0 | s | persist.delayTime | ADOPTED_TUNABLE 15-minute persistence after the separate host soak/settling exclusion. It is not a substitute for determining the installation’s water transport and metal/pipe thermal time constant. |
Description
When an HX isolation/control valve is finally commanded closed, meaningful continued transfer indicates an unintended hydraulic/thermal path. The valve may be passing, never have reached its seat, leave a bypass open, or permit gravity circulation. This rule treats both heating and cooling directions the same by comparing the absolute value of a validated signed heat-transfer rate.
The observation is broader than “leaking valve.” Position feedback, branch flow, check-valve state, and a piping walk-down distinguish actuator failure, seat leakage, bypass, and thermosiphon after the alarm.
Detection Logic
yValveClosed = control_valve_cmd < closed_command_limit
yTransferPresent = abs(heat_transfer_rate) > unexpected_transfer_limit
yFault = (yValveClosed AND yTransferPresent) continuously for alarm_delay
The two sub-condition outputs are immediate diagnostics. TrueDelay uses
delayOnInit = true, but its 15 minutes do not replace the host’s independent
post-close soak exclusion. Both comparisons are strict: exactly 5% is not
closed at the shipped setting and exactly 5 kW is not transfer-present.
Possible Diagnoses
- Passing valve seat, debris, erosion, or insufficient close-off rating.
- Actuator/linkage failed or commanded scaling does not reach physical close.
- Manual bypass, three-way/parallel path, or wrong valve bound to the rule.
- Failed/missing check valve or gravity/thermosiphon circulation.
- Residual transport/metal/pipe soak not actually expired.
- Flow/temperature bias, fluid-property error, time skew, or energy imbalance.
Energy Impact
CRITICAL_WASTE with DIRECT_MEASUREMENT and MEDIUM confidence. Once the host has validated signed kW and confirmed the transfer is unwanted, thermal waste is the integral above the commissioned no-load envelope. Source energy depends on the boiler/chiller/heat-pump/district efficiency and concurrent pumping.
Emissions Impact
Scope 1+2, PROXY_EMISSIONS. Apply actual marginal source efficiency/COP and emissions factors to the validated unwanted thermal energy; direction alone does not identify the fuel/electric split.
Deviations
- The rule says transfer, not valve leakage. A closed command plus transfer cannot uniquely identify the path. Natural circulation and a bypass are real findings with different repairs, kept explicit in diagnosis.
- Heat transfer is host-derived. The graph applies
Absonly after the host validates two side estimates, safe finite capacity rates, and alignment. It does not derive kW from unguarded divisions. - 5 kW has no portable authority. It is an executable fixture. Commission a no-load uncertainty envelope before enabling the rule.
- Persistence is not soak. A host exclusion restarts after closure and every material hydraulic/thermal discontinuity; otherwise a long normal cooldown can consume the timer and manufacture a finding.
- Optional position feedback stays diagnostic. Requiring it would sharply reduce deployability, and command/position disagreement is a distinct future rule. Use it in the playbook when available.
- No cluster/suppression. HX-0001 may co-occur, but neither verdict universally invalidates or causally owns the other.
Test Vectors
14 scenarios, clock step 60 s over 3000 s.
| Scenario | Description |
|---|---|
closed_valve_no_transfer | A closed command with no measured transfer is healthy. |
open_valve_with_transfer | Expected exchange with an open valve does not accuse the valve even though transfer is present. |
closed_positive_transfer | Sustained primary-to-secondary heat transfer while closed matures a fault. |
closed_negative_transfer | Cooling-direction transfer is equally unintended; Abs makes the signature symmetric. |
command_exactly_at_closed_limit | Exactly 5% is not closed under strict Less. |
command_just_below_closed_limit | A 4.9% final command counts as closed and can mature the transfer finding. |
transfer_exactly_at_limit | Exactly 5.0 kW is clear under strict Greater. |
negative_transfer_exactly_at_limit | The mirrored -5.0 kW boundary is also clear after Abs. |
transfer_just_above_limit | 5.1 kW is one test increment beyond the executable placeholder. |
thermal_soak_ends_one_step_before_delay | A residual transfer episode that ends at 840 s never matures; the host soak exclusion remains independently required. |
mature_transfer_alarm_recovers | A mature alarm clears immediately when transfer falls below the limit. |
two_short_transfer_episodes_reset | Two short residual-transfer episodes separated by zero transfer do not accumulate. |
initial_transfer_obeys_delay | A closed-and-transferring condition present at initialization waits for delayOnInit. |
natural_circulation_is_valid_finding | Sustained thermosiphon transfer while the isolation command is closed is a real unintended path, even if the control valve itself later proves seated. |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 60,
"horizon_s": 3000
},
"scenarios": [
{
"name": "closed_valve_no_transfer",
"description": "A closed command with no measured transfer is healthy.",
"inputs": {
"control_valve_cmd": 0.0,
"heat_transfer_rate": 0.0
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 3000,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 3000,
"equals": false
}
]
},
{
"name": "open_valve_with_transfer",
"description": "Expected exchange with an open valve does not accuse the valve even though transfer is present.",
"inputs": {
"control_valve_cmd": 100.0,
"heat_transfer_rate": 20.0
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": false
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 3000,
"equals": false
}
]
},
{
"name": "closed_positive_transfer",
"description": "Sustained primary-to-secondary heat transfer while closed matures a fault.",
"inputs": {
"control_valve_cmd": 0.0,
"heat_transfer_rate": 5.1
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 3000,
"equals": true
}
]
},
{
"name": "closed_negative_transfer",
"description": "Cooling-direction transfer is equally unintended; Abs makes the signature symmetric.",
"inputs": {
"control_valve_cmd": 0.0,
"heat_transfer_rate": -5.1
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 3000,
"equals": true
}
]
},
{
"name": "command_exactly_at_closed_limit",
"description": "Exactly 5% is not closed under strict Less.",
"inputs": {
"control_valve_cmd": 5.0,
"heat_transfer_rate": 20.0
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": false
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 3000,
"equals": false
}
]
},
{
"name": "command_just_below_closed_limit",
"description": "A 4.9% final command counts as closed and can mature the transfer finding.",
"inputs": {
"control_valve_cmd": 4.9,
"heat_transfer_rate": 5.1
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 3000,
"equals": true
}
]
},
{
"name": "transfer_exactly_at_limit",
"description": "Exactly 5.0 kW is clear under strict Greater.",
"inputs": {
"control_valve_cmd": 0.0,
"heat_transfer_rate": 5.0
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 3000,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 3000,
"equals": false
}
]
},
{
"name": "negative_transfer_exactly_at_limit",
"description": "The mirrored -5.0 kW boundary is also clear after Abs.",
"inputs": {
"control_valve_cmd": 0.0,
"heat_transfer_rate": -5.0
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 3000,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 3000,
"equals": false
}
]
},
{
"name": "transfer_just_above_limit",
"description": "5.1 kW is one test increment beyond the executable placeholder.",
"inputs": {
"control_valve_cmd": 0.0,
"heat_transfer_rate": 5.1
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 3000,
"equals": true
}
]
},
{
"name": "thermal_soak_ends_one_step_before_delay",
"description": "A residual transfer episode that ends at 840 s never matures; the host soak exclusion remains independently required.",
"inputs": {
"control_valve_cmd": 0.0,
"heat_transfer_rate": [
{
"t": 0,
"value": 8.0
},
{
"t": 840,
"value": 0.0
}
]
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 780,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 840,
"to_s": 3000,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 3000,
"equals": false
}
]
},
{
"name": "mature_transfer_alarm_recovers",
"description": "A mature alarm clears immediately when transfer falls below the limit.",
"inputs": {
"control_valve_cmd": 0.0,
"heat_transfer_rate": [
{
"t": 0,
"value": 8.0
},
{
"t": 1200,
"value": 0.0
}
]
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 1140,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 1200,
"to_s": 3000,
"equals": false
},
{
"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": 3000,
"equals": false
}
]
},
{
"name": "two_short_transfer_episodes_reset",
"description": "Two short residual-transfer episodes separated by zero transfer do not accumulate.",
"inputs": {
"control_valve_cmd": 0.0,
"heat_transfer_rate": [
{
"t": 0,
"value": 8.0
},
{
"t": 600,
"value": 0.0
},
{
"t": 900,
"value": -8.0
},
{
"t": 1500,
"value": 0.0
}
]
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 540,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 600,
"to_s": 840,
"equals": false
},
{
"output": "yTransferPresent",
"from_s": 900,
"to_s": 1440,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 1500,
"to_s": 3000,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 3000,
"equals": false
}
]
},
{
"name": "initial_transfer_obeys_delay",
"description": "A closed-and-transferring condition present at initialization waits for delayOnInit.",
"inputs": {
"control_valve_cmd": 0.0,
"heat_transfer_rate": 8.0
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 3000,
"equals": true
}
]
},
{
"name": "natural_circulation_is_valid_finding",
"description": "Sustained thermosiphon transfer while the isolation command is closed is a real unintended path, even if the control valve itself later proves seated.",
"inputs": {
"control_valve_cmd": 0.0,
"heat_transfer_rate": -8.0
},
"expect": [
{
"output": "yValveClosed",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yTransferPresent",
"from_s": 0,
"to_s": 3000,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 3000,
"equals": true
}
]
}
]
}