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-0001 — Hydronic heat-exchanger effectiveness degradation

Statusverified — engine e2ff2f8, cxf:fnv1a128:95d59df8187626eb12cc97061e4b7f9b, 2026-08-20
Severity3
Methodstatistical
Phase2
CategoryEFFICIENCY_LOSS
ConfidenceMEDIUM
EstimationBASELINE_COMPARISON
G36
Clusters
Suppresses
Suppressed by
RelatedHX-0002, HX-0003
Playbookshydronic-heat-exchanger-faults
SourceEnergyPlus 25.1 Engineering Reference, Heat Exchangers — epsilon-NTU model using both flow-capacity rates and inlet temperatures: https://bigladdersoftware.com/epx/docs/25-1/engineering-reference/heat-exchangers.html; Guelpa and Verda, Applied Energy 258 (2020), DOI 10.1016/j.apenergy.2019.114059 — field fouling detection on 325 district-heating HX substations from primary mass flow and temperatures on both sides; DOE FEMP, Energy Management Information System Capabilities — reduced HX heat transfer from temperature sensors as a condition-based maintenance signal: https://www.energy.gov/cmei/femp/energy-management-information-system-capabilities
Operating statesOne indirect liquid-to-liquid HX exchanging heat in a settled heating or cooling state, with both branch flows established and a frozen clean/design expected-effectiveness model ready and in domain

Preconditions (host-enforced): All six physical derivation inputs must describe the same HX: primary/secondary entering/leaving temperatures plus individual branch flows, aligned in time and correctly scaled. The host computes effectiveness only after proving positive finite thermal capacity rates, sufficient entering-temperature separation, configured density/cp for each fluid (including glycol concentration), and agreement of independently calculated side heat rates within commissioned uncertainty. The expected model must be frozen, independently fitted/commissioned, ready, fresh, and in domain for the current flow-capacity ratio, entering temperatures, direction, and control state. Suspend and re-warm after starts, direction/setpoint/pump/valve/stage changes. A common-header flow, duplicated side point, same-window fitted target, imbalance, or invalid denominator means NO_EVAL, not healthy. Steam/phase-change, air/refrigerant, potable, direct-contact, and aggregate-bank service are excluded.

Points: effectiveness, effectiveness_expected

Outputs:

  • yFault — True after actual effectiveness remains more than effectiveness_allowance below the valid expected value for alarm_delay
  • yEffectivenessLow — Diagnostic sub-condition flag; true when expected minus actual effectiveness strictly exceeds the allowance. False never means NO_EVAL

Parameters:

NameDefaultUnitCXF pathDescription
effectiveness_allowance0.1251shortfallHigh.tNO_PORTABLE_DEFAULT executable placeholder: 0.125 effectiveness points is a binary-exact vector fixture, not a field recommendation. Commission from clean-model error, sensor/fluid-property uncertainty, and the minimum actionable degradation before enabling this rule.
alarm_delay900.0spersist.delayTimeADOPTED_TUNABLE 15-minute persistence after the host’s independent settling/re-warm gate. Retune to the installation time constant and data cadence; no cited source establishes a universal duration.

Description

An indirect liquid heat exchanger loses effectiveness when fouling, scale, blocked channels, internal bypass, wrong fluid properties, or hydraulic changes reduce the heat it moves for the opportunity available. This rule compares a host-validated actual thermal effectiveness with a frozen clean/design expected value for the same operating condition. It reports degradation, not a root cause and not a raw “approach” temperature.

The four-port point identity matters as much as the arithmetic. Primary and secondary are fixed topology labels; heating usually makes signed transfer positive and cooling negative. The host converts both directions to a positive effectiveness before the graph sees them.

Detection Logic

shortfall = effectiveness_expected - effectiveness
yEffectivenessLow = shortfall > effectiveness_allowance
yFault = yEffectivenessLow continuously for alarm_delay

HX-0001 block graph

The graph has no Divide. The host publishes effectiveness only after safe denominator, fluid-property, timestamp, and side-energy-balance checks. A denominator guard downstream of a division would not prevent that division from evaluating; moving the validated thermodynamic derivation to the host also supports water/glycol properties the CXF graph does not carry.

Both comparisons use finite dimensionless scalars and the threshold is strict. yEffectivenessLow is immediate diagnostic evidence; only yFault is delayed.

Possible Diagnoses

  1. Plate/tube fouling, scale, biological film, or blocked channels.
  2. Internal gasket/bypass leakage or incorrect HX piping.
  3. Insufficient or maldistributed flow not caught by the commissioned floors.
  4. Degraded or misconfigured glycol concentration/fluid properties.
  5. Temperature/flow sensor bias, time misalignment, or swapped side/location.
  6. Expected model drift, wrong domain, or baseline trained on abnormal data.

Energy Impact

EFFICIENCY_LOSS with BASELINE_COMPARISON and MEDIUM confidence. Lost transfer must be replaced by upstream boilers, chillers, heat pumps, district energy, or longer pumping. The estimator uses the same validated available-rate basis as the effectiveness calculation; this two-point graph alone cannot produce kW. Guelpa and Verda’s 1.6% is a network-wide expected benefit from a cleaning program across 325 substations, not a savings range to assign to one alarm.

Emissions Impact

Scope 1+2, PROXY_EMISSIONS. Apply the marginal emissions rate of the actual replacement heat source and electricity used while the fault is active. Do not infer fuel/electric split from transfer direction alone.

Deviations

  • The thermodynamic ratio is host-derived. EnergyPlus documents the epsilon-NTU physics, but the repository graph intentionally compares two safe scalars instead of dividing inside CXF. This is a safety and fluid-property adaptation, not a claim that the host model is standardized.
  • effectiveness_allowance = 0.125 is not portable. No source supplies a universal threshold. The exact binary value makes strict-boundary vectors unambiguous; deployment must replace it before enabling evaluation.
  • The field method is precedent, not a transcribed algorithm. Guelpa and Verda use a calibrated fouling workflow under variable district-heating conditions. This card keeps the baseline/error-domain obligation but does not claim to reproduce their full method.
  • No in-graph readiness flag. Baseline/domain, denominator, and balance validity depend on provenance and configuration beyond two boundary points; they are mandatory host NO_EVAL gates.
  • No suppression. HX-0002 may explain why HX-0001 is unevaluable, but rule IDs are not equipment-instance scoped. A host gates the same instance rather than globally suppressing every HX-0001 when any HX-0002 is active.
  • Initial scope excludes steam. Phase change needs a different capacity and topology contract even though some trade usage calls it hydronic.

Test Vectors

16 scenarios, clock step 60 s over 3000 s.

ScenarioDescription
healthy_heating_effectivenessA commissioned heating point inside its expected-effectiveness allowance is healthy; direction is resolved before this sign-independent graph.
healthy_cooling_effectivenessCooling service uses the same effectiveness comparison after the host reconciles negative signed heat transfer into a positive effectiveness.
sustained_effectiveness_degradationA 0.15 shortfall exceeds the 0.125 executable placeholder and matures with a one-step assertion margin.
shortfall_exactly_at_thresholdBinary-exact 0.75 - 0.625 equals 0.125 and remains clear under strict Greater.
shortfall_just_below_thresholdA binary-exact 0.1240234375 shortfall is just inside the strict 0.125 allowance and remains clear.
shortfall_just_above_thresholdA 0.126 shortfall is just beyond the executable threshold.
actual_above_expectedA negative shortfall is not degradation.
violation_ends_one_step_before_delayThe candidate clears at 840 s, one evaluator step before 900 s, and never alarms.
mature_alarm_recoversA mature degradation clears immediately when effectiveness recovers.
two_short_violations_resetTwo sub-delay degradations separated by a healthy interval cannot accumulate.
initialization_violation_obeys_delayA violation present on the first tick is held by delayOnInit.
zero_expected_is_raw_host_no_evalA zero expected value is invalid host evidence; the raw graph stays clear and does not divide by it.
out_of_domain_expected_is_host_gateThe raw graph can compare any finite scalars; an out-of-domain baseline must be NO_EVAL at the host, not silently normalized in CXF.
expected_model_not_ready_is_host_gateThe raw graph has no readiness input and would alarm on these scalars; a baseline that is not frozen, fresh, and ready must be NO_EVAL at the host.
unbalanced_side_heat_rates_are_host_gateThe raw graph cannot inspect the two side heat rates and would alarm on these derived scalars; a failed side-energy-balance check must prevent publication and force host NO_EVAL.
invalid_thermal_denominator_is_host_gateZero flow or insufficient entering-temperature separation is not represented by these raw scalar ports; the host must withhold effectiveness and report NO_EVAL before the graph, which never divides.
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 60,
    "horizon_s": 3000
  },
  "scenarios": [
    {
      "name": "healthy_heating_effectiveness",
      "description": "A commissioned heating point inside its expected-effectiveness allowance is healthy; direction is resolved before this sign-independent graph.",
      "inputs": {
        "effectiveness": 0.75,
        "effectiveness_expected": 0.8
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        }
      ]
    },
    {
      "name": "healthy_cooling_effectiveness",
      "description": "Cooling service uses the same effectiveness comparison after the host reconciles negative signed heat transfer into a positive effectiveness.",
      "inputs": {
        "effectiveness": 0.72,
        "effectiveness_expected": 0.8
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        }
      ]
    },
    {
      "name": "sustained_effectiveness_degradation",
      "description": "A 0.15 shortfall exceeds the 0.125 executable placeholder and matures with a one-step assertion margin.",
      "inputs": {
        "effectiveness": 0.65,
        "effectiveness_expected": 0.8
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "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": "shortfall_exactly_at_threshold",
      "description": "Binary-exact 0.75 - 0.625 equals 0.125 and remains clear under strict Greater.",
      "inputs": {
        "effectiveness": 0.625,
        "effectiveness_expected": 0.75
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        }
      ]
    },
    {
      "name": "shortfall_just_below_threshold",
      "description": "A binary-exact 0.1240234375 shortfall is just inside the strict 0.125 allowance and remains clear.",
      "inputs": {
        "effectiveness": 0.6259765625,
        "effectiveness_expected": 0.75
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        }
      ]
    },
    {
      "name": "shortfall_just_above_threshold",
      "description": "A 0.126 shortfall is just beyond the executable threshold.",
      "inputs": {
        "effectiveness": 0.624,
        "effectiveness_expected": 0.75
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "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": "actual_above_expected",
      "description": "A negative shortfall is not degradation.",
      "inputs": {
        "effectiveness": 0.85,
        "effectiveness_expected": 0.8
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        }
      ]
    },
    {
      "name": "violation_ends_one_step_before_delay",
      "description": "The candidate clears at 840 s, one evaluator step before 900 s, and never alarms.",
      "inputs": {
        "effectiveness": [
          {
            "t": 0,
            "value": 0.6
          },
          {
            "t": 840,
            "value": 0.8
          }
        ],
        "effectiveness_expected": 0.8
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "from_s": 0,
          "to_s": 780,
          "equals": true
        },
        {
          "output": "yEffectivenessLow",
          "from_s": 840,
          "to_s": 3000,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        }
      ]
    },
    {
      "name": "mature_alarm_recovers",
      "description": "A mature degradation clears immediately when effectiveness recovers.",
      "inputs": {
        "effectiveness": [
          {
            "t": 0,
            "value": 0.6
          },
          {
            "t": 1200,
            "value": 0.8
          }
        ],
        "effectiveness_expected": 0.8
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "from_s": 0,
          "to_s": 1140,
          "equals": true
        },
        {
          "output": "yEffectivenessLow",
          "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_violations_reset",
      "description": "Two sub-delay degradations separated by a healthy interval cannot accumulate.",
      "inputs": {
        "effectiveness": [
          {
            "t": 0,
            "value": 0.6
          },
          {
            "t": 600,
            "value": 0.8
          },
          {
            "t": 900,
            "value": 0.6
          },
          {
            "t": 1500,
            "value": 0.8
          }
        ],
        "effectiveness_expected": 0.8
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "from_s": 0,
          "to_s": 540,
          "equals": true
        },
        {
          "output": "yEffectivenessLow",
          "from_s": 600,
          "to_s": 840,
          "equals": false
        },
        {
          "output": "yEffectivenessLow",
          "from_s": 900,
          "to_s": 1440,
          "equals": true
        },
        {
          "output": "yEffectivenessLow",
          "from_s": 1500,
          "to_s": 3000,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        }
      ]
    },
    {
      "name": "initialization_violation_obeys_delay",
      "description": "A violation present on the first tick is held by delayOnInit.",
      "inputs": {
        "effectiveness": 0.5,
        "effectiveness_expected": 0.8
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "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": "zero_expected_is_raw_host_no_eval",
      "description": "A zero expected value is invalid host evidence; the raw graph stays clear and does not divide by it.",
      "inputs": {
        "effectiveness": 0.0,
        "effectiveness_expected": 0.0
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3000,
          "equals": false
        }
      ]
    },
    {
      "name": "out_of_domain_expected_is_host_gate",
      "description": "The raw graph can compare any finite scalars; an out-of-domain baseline must be NO_EVAL at the host, not silently normalized in CXF.",
      "inputs": {
        "effectiveness": 0.4,
        "effectiveness_expected": 0.8
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "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": "expected_model_not_ready_is_host_gate",
      "description": "The raw graph has no readiness input and would alarm on these scalars; a baseline that is not frozen, fresh, and ready must be NO_EVAL at the host.",
      "inputs": {
        "effectiveness": 0.4,
        "effectiveness_expected": 0.8
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "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": "unbalanced_side_heat_rates_are_host_gate",
      "description": "The raw graph cannot inspect the two side heat rates and would alarm on these derived scalars; a failed side-energy-balance check must prevent publication and force host NO_EVAL.",
      "inputs": {
        "effectiveness": 0.4,
        "effectiveness_expected": 0.8
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "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": "invalid_thermal_denominator_is_host_gate",
      "description": "Zero flow or insufficient entering-temperature separation is not represented by these raw scalar ports; the host must withhold effectiveness and report NO_EVAL before the graph, which never divides.",
      "inputs": {
        "effectiveness": 0.4,
        "effectiveness_expected": 0.8
      },
      "expect": [
        {
          "output": "yEffectivenessLow",
          "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
        }
      ]
    }
  ]
}