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

HX-0003 — Heat transfer persists with control valve commanded closed

Statusverified — engine e2ff2f8, cxf:fnv1a128:aff5ae932f23f7127de8a90723fadb04, 2026-08-20
Severity3
Methodrule
Phase2
CategoryCRITICAL_WASTE
ConfidenceMEDIUM
EstimationDIRECT_MEASUREMENT
G36
Clusters
Suppresses
Suppressed by
RelatedHX-0001, HX-0002
Playbookshydronic-heat-exchanger-faults
SourceEnergyPlus 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 statesThe 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_delay
  • yValveClosed — Immediate diagnostic sub-condition; false never means NO_EVAL
  • yTransferPresent — Immediate direction-independent diagnostic sub-condition based on absolute signed transfer; false never means NO_EVAL

Parameters:

NameDefaultUnitCXF pathDescription
closed_command_limit5.0%valveClosed.tADOPTED_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_limit5.0kWtransferHigh.tNO_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_delay900.0spersist.delayTimeADOPTED_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

HX-0003 block graph

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

  1. Passing valve seat, debris, erosion, or insufficient close-off rating.
  2. Actuator/linkage failed or commanded scaling does not reach physical close.
  3. Manual bypass, three-way/parallel path, or wrong valve bound to the rule.
  4. Failed/missing check valve or gravity/thermosiphon circulation.
  5. Residual transport/metal/pipe soak not actually expired.
  6. 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 Abs only 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.

ScenarioDescription
closed_valve_no_transferA closed command with no measured transfer is healthy.
open_valve_with_transferExpected exchange with an open valve does not accuse the valve even though transfer is present.
closed_positive_transferSustained primary-to-secondary heat transfer while closed matures a fault.
closed_negative_transferCooling-direction transfer is equally unintended; Abs makes the signature symmetric.
command_exactly_at_closed_limitExactly 5% is not closed under strict Less.
command_just_below_closed_limitA 4.9% final command counts as closed and can mature the transfer finding.
transfer_exactly_at_limitExactly 5.0 kW is clear under strict Greater.
negative_transfer_exactly_at_limitThe mirrored -5.0 kW boundary is also clear after Abs.
transfer_just_above_limit5.1 kW is one test increment beyond the executable placeholder.
thermal_soak_ends_one_step_before_delayA residual transfer episode that ends at 840 s never matures; the host soak exclusion remains independently required.
mature_transfer_alarm_recoversA mature alarm clears immediately when transfer falls below the limit.
two_short_transfer_episodes_resetTwo short residual-transfer episodes separated by zero transfer do not accumulate.
initial_transfer_obeys_delayA closed-and-transferring condition present at initialization waits for delayOnInit.
natural_circulation_is_valid_findingSustained 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
        }
      ]
    }
  ]
}