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

AHU-0015 — Inactive heating coil temperature rise

Statusverified — engine e2ff2f8, cxf:fnv1a128:2e4a701f33e4a7b3245b96c9e5b6922a, 2026-08-17
Severity2
Methodrule
Phase1
CategoryCRITICAL_WASTE
ConfidenceHIGH
EstimationDIRECT_MEASUREMENT
G36§5.16.14 FC#15
Clusters
Suppresses
Suppressed byAHU-0028
RelatedAHU-0016, AHU-0020, AHU-0014, AHU-0012, AHU-0028
Playbooksstuck-actuator
SourceHVAC FDD Reference v1.0 §5.8.1 (index; card abbreviated); G36 §5.16.14 FC#15 (text per Addendum u public review); NISTIR 7365 (defaults provenance); PNNL EEM-03 (leaking coil valves; the §5.8.1 index row’s EEM mapping)
Operating statesOS#2-#4 (heating coil commanded off) — host-gated

Preconditions (host-enforced): Supply fan running, and the unit in one of the three states where G36 requires the heating coil to be off (Table 5.16.14.2): OS#2 free cooling (HC = 0, CC = 0, minimum < OA damper < 100%), OS#3 mechanical plus economizer cooling (HC = 0, CC > 0, OA damper = 100%), or OS#4 mechanical cooling on minimum OA (HC = 0, CC > 0, OA damper at minimum). In OS#1 the coil is commanded to heat and a rise across it is the intended result, not a fault. Suspend evaluation for ModeDelay (30 min) after any mode or operating-state change in a zone group the AHU serves, and whenever the AHU is not operating (G36 §5.16.14.11) — a coil coasting down still shows its rise, and a preheat coil in freeze protection shows one on purpose. This binding reads the coil through MAT and SAT, so a unit with no MAT sensor cannot run the rule as shipped: install dedicated coil sensors and rebind, or omit. Silence the rule while AHU-0028 is active: a MAT outside the OAT/RAT envelope is not a coil entering temperature. When any gate is unmet the verdict is NO_EVAL, not healthy.

Points: sat, mat

Outputs:

  • yFault — True while sat has stayed more than coil_rise_threshold above mat for at least alarm_delay

Parameters:

NameDefaultUnitCXF pathDescription
coil_rise_threshold4.1623°CriseBig.tTemperature rise across the heating coil that stops being fan heat and sensor noise and starts being heat. Composed from the G36 §5.16.14 internal variables as sqrt(eHCET² + eHCLT²) + dTSF = sqrt(3² + 1²) + 1 = 4.1623, using the proxied epsilons Table 5.16.14.5 prescribes when the coil is read through MAT and SAT (eHCET = eMAT = 3 °C, eHCLT = eSAT = 1 °C) and the fan-heat term dTSF = 1 °C, which belongs here because the supply fan sits between the two sensors and its rise would otherwise be charged to the coil. Retunes: dedicated sensors bracketing the coil with the fan outside the pair drop the dTSF term and their own epsilons, giving sqrt(2)·1 ≈ 1.41 for a matched ±1 °C pair or 3.1623 if the entering sensor keeps a 3 °C band; a site that measures a fan rise other than 1 °C substitutes it directly
alarm_delay1800.0spersist.delayTimeContinuous fault persistence required before the alarm asserts (G36 AlarmDelay, 30 min)

Description

In OS#2, OS#3, and OS#4 the heating coil is closed by definition — G36 identifies all three states partly by HC = 0. Air crossing the unit should pick up the supply fan’s degree of shaft work and nothing more. When SAT reads several degrees above MAT anyway, hot water is moving through a valve that reports itself shut, or an electric or gas stage is energized that nothing called for.

In OS#3 and OS#4 the cooling coil is running at the same time, so every kilowatt the leaking coil adds is a kilowatt the chiller is paid to remove — the AHU-0016 failure arriving through a different door. In OS#2 the leak spends the economizer’s savings by warming the outdoor air that was supposed to do the cooling, often pushing the unit into mechanical cooling it did not need. This rule and AHU-0014 are AHU-0016’s silent siblings: that rule reads the two valve commands and needs both past 5% open, so a valve reporting 0% and flowing anyway is invisible to it.

Detection Logic

rise   = sat − mat
yFault = rise > coil_rise_threshold,
         sustained continuously for alarm_delay

Block graph (rule.cxf.jsonld):

AHU-0015 block graph

G36 writes the test as HCLT_AVG − HCET_AVG ≥ sqrt(eHCET² + eHCLT²) + ΔTSF*, footnoting the fan-heat factor as included or not depending on where the coil sensors sit. This library binds HCET := mat and HCLT := sat — the instrumentation most air handlers actually have — which brings the proxied epsilons (3 °C mixed-air, 1 °C supply-air, root-sum-square 3.1623 °C) and puts the supply fan inside the measurement, so the ΔTSF term applies: 3.1623 + 1 = 4.1623 °C.

Here the term is doing physical work rather than bookkeeping. A healthy draw-through unit with both coils shut already reads sat − mat = +1.0 °C. Drop the term and the test charges that degree to the coil: the threshold falls to 3.1623 °C, a true coil rise of 2.17 °C reports as a leak, and on a unit whose fan adds more than 3.16 °C the alarm never clears. With the term the shipped test fires precisely when the coil’s own contribution exceeds the 3.1623 °C sensor floor.

The comparison is strict, so a rise sitting exactly on 4.1623 °C reads healthy where G36 would report the fault. persist requires 30 continuous minutes and any interruption restarts the timer, which separates a leaking valve from a coil giving up the hot water still standing in it after a state change.

Possible Diagnoses

Transcribed from G36 §5.16.14 FC#15:

  1. HCET sensor error
  2. HCLT sensor error
  3. Heating coil valve stuck open or leaking

The single-zone version of the same row (§5.18.14, the SZVAV table in Addendum u) adds “gas or electric heat stuck on”, which applies to any unit with a non-hydronic heat source even though the VAV row omits it.

Under this library’s binding, diagnoses 1 and 2 read as MAT and SAT sensor error, and they are the cheap ones to eliminate first. Diagnosis 3 dominates in the field and is why the card carries the stuck-actuator playbook: a two-way valve whose seat has eroded, or an actuator that has lost its close position, passes water at a command of 0% and no command-based rule will ever see it.

Energy Impact

CRITICAL_WASTE, HIGH confidence, DIRECT_MEASUREMENT, savings 0.5–5% of site energy mapped to PNNL EEM-03 (leaking coil valves) — the §5.8.1 index row, the only energy statement the reference makes here. DIRECT_MEASUREMENT is honest in a way it is not for the abbreviated comparison rules: the two temperatures the rule already reads are the measurement.

waste_kw = supply_airflow_m3s × 1.2 × 1.005 × ((sat − mat) − dTSF)

Subtracting the fan rise is not a rounding detail — at the threshold it is 1.0 of a 4.16 °C measured rise, so crediting it to the coil would overstate the waste by about a third, and more on any smaller leak. Design airflow is the one substitution. HIGH confidence because a sustained rise across a coil commanded shut has no benign explanation other than a sensor, and the sensor case shows up as a rise that does not move with load. Cooling-dominant, following the operating states: all three are cooling-side, and in OS#3–#4 the leak is paid for twice, once at the boiler and once at the chiller.

Emissions Impact

PROXY_EMISSIONS, scope 1+2, both library-assigned since the §5.8.1 index publishes no emissions column. The leaked heat is Scope 1 or Scope 2 depending on the plant (gas boiler versus electric resistance or a heat pump), and the cooling that removes it again in OS#3–#4 is purchased electricity, Scope 2. On an all-electric site the exchange collapses to Scope 2, and when the cause is a sensor there is nothing to attribute. Avoided-emissions basis: marginal operating emissions rate (MOER) for the electric half, static combustion factor for the fuel half.

Deviations

  • The reference card is abbreviated; G36 is the normative text. The HVAC FDD Reference carries AHU-0015 only as a §5.8.1 index row — no equation, internal variables, vectors, severity, diagnoses, or preconditions. Detection logic and the diagnosis list are transcribed from ASHRAE Guideline 36 §5.16.14 FC#15 as it appears in Addendum u to Guideline 36-2018 (First Public Review, 2021); the fourth diagnosis comes from the addendum’s single-zone table (§5.18.14) and is marked as such.
  • HCET and HCLT are bound to MAT and SAT. G36 leaves the instrumentation open (§5.16.14.5) and Table 5.16.14.5 supplies the proxied epsilons. The consequence is that the rule sees the whole air path from the mixing box to the supply sensor: the fan is inside the measurement (handled by dTSF) and so is any duct heat gain between coil and sensor (not handled — on this fault it biases the rise upward and makes the rule slightly louder, the opposite of its effect on AHU-0014). A site with dedicated coil sensors rebinds the two boundary inputs at deployment and retunes coil_rise_threshold with its own sensor errors; because the fan is then outside the pair, that retune also drops the dTSF term.
  • The fan-heat term is included, and here it is what keeps healthy units quiet. Fan heat and coil heat both raise SAT, so the measured rise is the true coil rise plus one dTSF and the threshold must discount the fan’s own contribution before charging anything to the coil. Unlike its cooling-side twin this rule is not additionally desensitized by the binding, so there is no sharper retune of the same kind to offer; sites with a measured fan rise other than 1 °C substitute it in the sum directly.
  • G36’s becomes a strict >. CDL Reals offers only strict comparisons, so a rise of exactly 4.1623 °C reads healthy where G36 reports the fault. Measure zero on a real temperature signal, and it errs toward silence. A host binding coarsely quantized temperatures should retune the threshold down rather than rely on the signal overshooting.
  • The threshold is a rounded constant, not a root-sum-square computed in the graph. sqrt(3² + 1²) + 1 = 4.16227766…, shipped as 4.1623 — high by 2.2 × 10⁻⁵ °C, four orders of magnitude below the resolution of the sensors feeding it, and one number to retune instead of three.
  • Instantaneous samples instead of 5-minute rolling averages. G36 computes every §5.16.14 signal as a 5-minute rolling average with 1-minute sampling; this library consumes instantaneous points and lets the 30-minute AlarmDelay stand in. Not equivalent — persistence resets on every compliant tick, so an oscillating rise (a short-cycling electric stage) can hide indefinitely, while the steady leak of a failed valve seat reads the same either way. (Honesty note from AHU-0002.)
  • Operating states, ModeDelay, and the not-operating suspension are host-side preconditions. G36 scopes FC#15 to OS#2–#4 and suspends evaluation after a mode change in a served zone group and whenever the AHU is off; none of it is in the graph, per the library’s stance. Freeze protection deserves its own mention there: a preheat coil driven open to protect itself produces this exact signature while doing its job. G36 attaches no “omit if no MAT sensor” qualifier to FC#15, but this library’s binding needs MAT, so the qualifier applies to the shipped rule and lives in preconditions.
  • Severity 2 is the library’s. The §5.8.1 index carries no severity column. Severity 2 puts this fault with AHU-0016 and AHU-0020 rather than the 001-range comparison rules at 3, which is where CRITICAL_WASTE and HIGH confidence point. G36’s Level 3 alarm grading is a priority scheme, not this library’s 1–4 scale.
  • The energy profile is the index row’s; the runtime formula, climate sensitivity, and emissions block are the library’s, reasoned from the operating states the fault is evaluated in.
  • persist.delayOnInit = true (Modelica/CDL default is false), the library’s standing choice: a violation already present at load waits out the full 30 minutes instead of alarming on the first tick after a controller restart.

Notes

This rule and AHU-0012 test the same sign, on the same two sensors, in overlapping states, and both exist on purpose. AHU-0012 thresholds at eSAT + eMAT + ΔTSF = 5.0 °C — bands added linearly, worst case — while this one adds them in quadrature to 4.1623 °C, the sharper composition when the errors are independent, so this rule alarms first by 0.84 °C of rise. FC#12’s diagnosis list is broad and includes cooling-side capacity failures; FC#15 names the heating coil and its sensors and nothing else, so a host that wants one alarm should keep this one. Start at the sensors, since a MAT reading low or a SAT reading high produces this trace with nothing wrong in the mechanical room; an active AHU-0028 should already be suppressing the rule. Then isolate the coil and watch the rise collapse to fan heat — or, on electric or gas heat, check the stage’s contactor or safety interlock.

Test Vectors

8 scenarios, clock step 300 s over 5400 s.

ScenarioDescription
healthy_unit_shows_fan_heat_onlyThe pin this rule exists to keep clear: a draw-through unit with the heating coil shut still warms its air by dTSF = 1 °C crossing the supply fan, so sat − mat = +1.0 °C in every healthy OS#2-#4 hour. The 4.1623 °C threshold carries the fan-heat term precisely so this reads healthy forever; without it every healthy AHU in the fleet would alarm
rise_exactly_at_thresholdThreshold edge: sat − mat = 4.1623 °C exactly, the composed sqrt(eHCET² + eHCLT²) + dTSF allowance. riseBig is a strict >, so a rise sitting precisely on the allowance reads healthy — G36’s would report it (see Deviations)
rise_just_over_thresholdThreshold edge, other side: sat − mat = 4.2623 °C clears the strict comparison on the first tick and the alarm asserts one alarm_delay (1800 s) later
heating_valve_leaking_during_free_coolingOS#2 on a mild morning: the economizer mixes to 16 °C and the air reaches the supply sensor at 23 °C with the heating valve commanded 0%. Take the fan’s 1 °C back out and the coil is adding about 6 °C to air the economizer was brought in to cool — hot water flowing through a valve that reports itself shut, which is exactly the leak AHU-0016 is structurally blind to
mat_sensor_reading_lowSame 6 °C signature in a season when the hot-water plant is locked out and the valve has been verified shut: no coil can be adding heat, and the mixed-air sensor is what is wrong. The rule reports the contradiction; G36 lists both sensor errors ahead of the valve in its diagnosis order for this reason
transient_clears_before_alarm_delayTransient: a 7 °C rise persists for 1200 s while hot water standing in a just-closed coil gives up its residual heat, then SAT settles back to mat + fan heat. Anything shorter than alarm_delay never reaches the operator
fault_clears_on_recoveryA sustained 7 °C rise alarms at 1800 s; the leaking valve is isolated at t = 3000 and SAT falls back to mat + fan heat. TrueDelay has no off-delay, so the alarm drops on that tick
oscillating_rise_never_alarmsAn electric heating stage short-cycling on a 10-minute period swings the rise between 7 °C and 1 °C. Its mean is well outside the allowance and G36’s 5-minute rolling average would report the fault, but persistence restarts on every compliant tick, so this rule stays silent — the limit of substituting TrueDelay for averaging (see Deviations)
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 300,
    "horizon_s": 5400
  },
  "scenarios": [
    {
      "name": "healthy_unit_shows_fan_heat_only",
      "description": "The pin this rule exists to keep clear: a draw-through unit with the heating coil shut still warms its air by dTSF = 1 \u00b0C crossing the supply fan, so sat \u2212 mat = +1.0 \u00b0C in every healthy OS#2-#4 hour. The 4.1623 \u00b0C threshold carries the fan-heat term precisely so this reads healthy forever; without it every healthy AHU in the fleet would alarm",
      "inputs": {
        "mat": 18.0,
        "sat": 19.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "rise_exactly_at_threshold",
      "description": "Threshold edge: sat \u2212 mat = 4.1623 \u00b0C exactly, the composed sqrt(eHCET\u00b2 + eHCLT\u00b2) + dTSF allowance. `riseBig` is a strict `>`, so a rise sitting precisely on the allowance reads healthy \u2014 G36's `\u2265` would report it (see Deviations)",
      "inputs": {
        "mat": 15.8377,
        "sat": 20.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "rise_just_over_threshold",
      "description": "Threshold edge, other side: sat \u2212 mat = 4.2623 \u00b0C clears the strict comparison on the first tick and the alarm asserts one alarm_delay (1800 s) later",
      "inputs": {
        "mat": 15.7377,
        "sat": 20.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 5400,
          "equals": true
        }
      ]
    },
    {
      "name": "heating_valve_leaking_during_free_cooling",
      "description": "OS#2 on a mild morning: the economizer mixes to 16 \u00b0C and the air reaches the supply sensor at 23 \u00b0C with the heating valve commanded 0%. Take the fan's 1 \u00b0C back out and the coil is adding about 6 \u00b0C to air the economizer was brought in to cool \u2014 hot water flowing through a valve that reports itself shut, which is exactly the leak AHU-0016 is structurally blind to",
      "inputs": {
        "mat": 16.0,
        "sat": 23.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 5400,
          "equals": true
        }
      ]
    },
    {
      "name": "mat_sensor_reading_low",
      "description": "Same 6 \u00b0C signature in a season when the hot-water plant is locked out and the valve has been verified shut: no coil can be adding heat, and the mixed-air sensor is what is wrong. The rule reports the contradiction; G36 lists both sensor errors ahead of the valve in its diagnosis order for this reason",
      "inputs": {
        "mat": 14.0,
        "sat": 20.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 5400,
          "equals": true
        }
      ]
    },
    {
      "name": "transient_clears_before_alarm_delay",
      "description": "Transient: a 7 \u00b0C rise persists for 1200 s while hot water standing in a just-closed coil gives up its residual heat, then SAT settles back to mat + fan heat. Anything shorter than alarm_delay never reaches the operator",
      "inputs": {
        "mat": 16.0,
        "sat": [
          {
            "t": 0,
            "value": 23.0
          },
          {
            "t": 1200,
            "value": 17.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "fault_clears_on_recovery",
      "description": "A sustained 7 \u00b0C rise alarms at 1800 s; the leaking valve is isolated at t = 3000 and SAT falls back to mat + fan heat. `TrueDelay` has no off-delay, so the alarm drops on that tick",
      "inputs": {
        "mat": 16.0,
        "sat": [
          {
            "t": 0,
            "value": 23.0
          },
          {
            "t": 3000,
            "value": 17.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 2700,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 3300,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "oscillating_rise_never_alarms",
      "description": "An electric heating stage short-cycling on a 10-minute period swings the rise between 7 \u00b0C and 1 \u00b0C. Its mean is well outside the allowance and G36's 5-minute rolling average would report the fault, but persistence restarts on every compliant tick, so this rule stays silent \u2014 the limit of substituting TrueDelay for averaging (see Deviations)",
      "inputs": {
        "mat": 16.0,
        "sat": [
          {
            "t": 0,
            "value": 23.0
          },
          {
            "t": 600,
            "value": 17.0
          },
          {
            "t": 1200,
            "value": 23.0
          },
          {
            "t": 1800,
            "value": 17.0
          },
          {
            "t": 2400,
            "value": 23.0
          },
          {
            "t": 3000,
            "value": 17.0
          },
          {
            "t": 3600,
            "value": 23.0
          },
          {
            "t": 4200,
            "value": 17.0
          },
          {
            "t": 4800,
            "value": 23.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    }
  ]
}