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-0013 — SAT too high at full cooling

Statusverified — engine e2ff2f8, cxf:fnv1a128:52a1f486bef61ee338ec4a5ec1338a05, 2026-08-17
Severity3
Methodrule
Phase1
CategoryEXCESS_CONSUMPTION
ConfidenceMEDIUM
EstimationPROXY_ESTIMATION
G36§5.16.14 FC#13
Clusters
Suppresses
Suppressed by
RelatedAHU-0012, AHU-0007, AHU-0023, AHU-0032, AHU-0033
Playbooks
SourceHVAC FDD Reference v1.0 §5.8.1 (index; card abbreviated); G36 §5.16.14 FC#13 (text per Addendum u public review); NISTIR 7365 (defaults provenance)
Operating statesOS#3-#4 (mechanical cooling) — host-gated

Preconditions (host-enforced): Supply fan running, and the unit in one of the two mechanical-cooling operating states G36 defines by actuator signature: OS#3 (HC = 0, CC > 0, OA damper = 100%) or OS#4 (HC = 0, CC > 0, OA damper at minimum). Suspend evaluation for ModeDelay (30 min) after any mode or operating-state change in a zone group the AHU serves, while actuators are still stroking and the coil has not caught up. clg_vlv_cmd must be the command the AHU controller is issuing, not a position feedback: this rule asks whether the loop has run out of capacity to ask for, and a feedback that disagrees with its command is a stuck-actuator finding (AHU-0020), not this one. sat_sp must be the active setpoint, including any reset — comparing against a design value the sequence is no longer holding produces a fault every mild afternoon. When any gate is unmet the verdict is NO_EVAL, not healthy.

Points: sat, sat_sp, clg_vlv_cmd

Outputs:

  • yFault — True while SAT has stayed more than sat_error_threshold above its setpoint with the cooling valve commanded above cc_full_threshold, for at least alarm_delay

Parameters:

NameDefaultUnitCXF pathDescription
sat_error_threshold1.0°CspMiss.tAmount by which SAT may exceed its setpoint before the miss is real rather than sensor error. Default 1.0 °C is G36’s eSAT, the supply-air sensor accuracy allowance (NISTIR 7365). A site with a calibrated SAT sensor may lower it; raising it to quiet a hunting loop hides AHU-0022 instead of fixing it
cc_full_threshold99.0%clgFull.tCooling coil command above which the loop is treated as having no capacity left to ask for (G36 CC >= 99%)
alarm_delay1800.0spersist.delayTimeContinuous fault persistence required before the alarm asserts (G36 AlarmDelay, 30 min)

Description

The cooling valve is wide open and the supply air is still above setpoint. The control loop has already asked for everything it has, so whatever is wrong is not tuning: either the coil cannot deliver, the chilled water or refrigerant behind it cannot deliver, or the sensor reporting the miss is wrong. Downstream the effect is the ordinary one — zones that cannot get cold enough, VAV boxes opening toward maximum flow, and a fan pushing more air to make up the degrees the coil failed to remove.

This is G36 §5.16.14 FC#13, the exact mirror of AHU-0007 on the heating side. Both rules make the same statement: an actuator at its stop with the controlled variable still on the wrong side of its target is evidence of a defect, and until the actuator saturates it is evidence of nothing.

Detection Logic

sp_gap = sat − sat_sp
yFault = (sp_gap      > sat_error_threshold)   SAT above setpoint by more than sensor accuracy
     AND (clg_vlv_cmd > cc_full_threshold)     cooling loop has nothing left to ask for
         sustained continuously for alarm_delay

Block graph (rule.cxf.jsonld):

AHU-0013 block graph

The gap form is G36’s SAT_AVG > SATSP + eSAT rearranged so the allowance stays a single positive number at one CXF path. clgFull is the half that gives the miss its meaning: SAT above setpoint at a part-open valve is a control loop doing its job, and only a loop that has run out of coil is evidence of a defect. Both comparisons are strict, so a miss sitting exactly on 1.0 °C and a command parked exactly on 99.0% both read healthy. persist requires 30 minutes of continuous violation and any interruption restarts the timer, which separates a failed coil from a morning pulldown or the minutes after a large block of zones opens at once.

Possible Diagnoses

Transcribed from G36 §5.16.14 FC#13:

  1. SAT sensor error
  2. Cooling coil valve stuck closed or actuator failure
  3. Fouled or undersized cooling coil
  4. CHW temperature too high or CHW unavailable
  5. DX cooling unavailable
  6. Gas or electric heat stuck on
  7. Heating coil valve leaking or stuck open

The list is FC#12’s minus the MAT sensor, which this rule does not read. Entries 6 and 7 are heat sources fighting the coil, so a unit that trips this rule and AHU-0012 together is pointing at those two rather than at the coil.

Energy Impact

EXCESS_CONSUMPTION, MEDIUM confidence, PROXY_ESTIMATION, savings 2–5% of AHU energy — the §5.8.1 index row, the only energy statement the reference makes here (no EEM mapped). The estimate is branched because the rule does not say which branch you are in. If a heat source is fighting the coil, the waste is immediate and AHU-0016’s term sizes it. If the coil, the chilled water, or the compressor is simply not delivering, the AHU wastes nothing at the coil and the cost lands downstream: shortfall_kw = supply_airflow_m3s × 1.2 × 1.005 × (sat − sat_sp), made up by extra airflow at fan power if it is made up at all. Design airflow substituting for a measured one keeps it a proxy. Cooling-dominant, since the rule is evaluated only in mechanical-cooling states.

Emissions Impact

PROXY_EMISSIONS, MEDIUM confidence. Scope 2: everything this fault spends is purchased electricity — the chiller or DX compressor running longer, and the fan power moving extra air. Diagnoses 6 and 7 can put a combustion stream behind the fault, but that is heat this rule cannot see; AHU-0012 measures it and carries the Scope 1 half. Emissions can rise rather than fall when the fault is fixed, since a coil restored to capacity finally delivers the cooling the building has been asking for — this rule buys comfort and diagnosis, and the avoided-emissions claim belongs to whatever waste the repair uncovers. Avoided-emissions basis: marginal operating emissions rate (MOER), applicable only to the fighting-heat-source branch.

Deviations

  • The reference card is abbreviated; G36 is the normative text. The HVAC FDD Reference carries AHU-0013 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#13 as it appears in Addendum u to Guideline 36-2018 (First Public Review, 2021).
  • Setpoint comparison rewritten as gap comparison. Subtracting first and testing sat − sat_sp > eSAT keeps the allowance the positive number G36 publishes at one CXF path, instead of an offset added to the setpoint ahead of a two-signal comparison. Same rearrangement as AHU-0001 and AHU-0028. The threshold is a single G36 constant rather than a composition — eSAT = 1 °C, the NISTIR 7365 supply-air sensor accuracy — so recalibrating that sensor changes sat_error_threshold directly, with no arithmetic.
  • CC >= 99% becomes a strict > 99.0. CDL Reals offers only strict comparisons, so a command parked at exactly 99.000% reads as not-saturated and the rule stays silent where G36 would evaluate it. Same deviation and retune hint as AHU-0001 and AHU-0007: a host binding a coarsely quantized command should set cc_full_threshold to 98.9 rather than rely on the signal overshooting.
  • 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 signal can hide indefinitely; the realistic instance here is a hunting SAT loop, which is AHU-0022’s fault to report. A steady miss against a saturated valve reads the same either way. (Honesty note from AHU-0002.)
  • Operating states and ModeDelay are host-side preconditions. G36 scopes FC#13 to OS#3–#4, suspends evaluation for ModeDelay (30 min) after a mode change in a served zone group, and suspends it entirely while the AHU is off; none of it is in the graph, per the library’s stance (precedent AHU-0029). CC > 0 is part of both applicable state definitions, so the host’s gate already implies a cooling call — the graph’s clgFull test is the stronger statement that the call has saturated.
  • Severity 3 is the library’s. No chapter card states one and the §5.8.1 index carries no severity column. G36’s Level 3 alarm grading is a reporting priority rather than this library’s 1–4 scale, so it corroborates without supplying.
  • The energy profile is the index row’s; the runtime formula and scope are the library’s. category, confidence, estimation_method, and savings_range are copied from §5.8.1 — the identical row the reference gives AHU-0007, this rule’s heating-side mirror. The branched formula is mirrored from FC-007’s, but the scope departs from it: FC-007 records 1|2 because the heat making up its deficit may be gas or electric, while nothing on the cooling side burns fuel, so this card records 2 and leaves the Scope 1 half of the shared diagnoses to AHU-0012.
  • 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

The setpoint this rule compares against is the one the sequence is actually holding, which makes it quietly dependent on the reset strategy. Where SAT reset has been disabled or never commissioned — the condition AHU-0023 detects — the active setpoint may be a design-day value, and a coil that cannot reach it in August is being asked for capacity nobody budgeted. Read this rule together with AHU-0012: every diagnosis here appears there, and what differs is that FC#12 finds heat entering the stream while FC#13 finds heat failing to leave it. A coil that has lost capacity trips this rule alone; a heat source fighting the coil trips both, which is the cheapest discriminator available before anyone opens an access panel.

Test Vectors

10 scenarios, clock step 300 s over 5400 s.

ScenarioDescription
coil_modulating_on_setpointOrdinary mechanical cooling: SAT is on its 13 °C setpoint with the valve at 65%. The loop has reserve in both directions, which is what a healthy cooling coil looks like
valve_wide_open_holding_setpointThe valve is at 100% and SAT is 0.5 °C high — a coil sized close to its design point on a design day. Half a degree is inside eSAT, so this is a unit at capacity, not a unit that has lost capacity
sat_error_exactly_at_thresholdThreshold edge: SAT − SATSP = 1.0 °C exactly, the whole eSAT allowance, with the valve at 100%. spMiss is a strict >, so an error sitting precisely on the sensor allowance reads healthy
sat_error_just_over_thresholdThreshold edge, other side: 1.1 °C above setpoint at 100% valve clears the strict comparison, and the alarm asserts one alarm_delay (1800 s) later
valve_exactly_at_full_thresholdThreshold edge: a 5 °C setpoint miss with the valve parked on exactly 99.0%. clgFull is a strict >, so a command sitting on cc_full_threshold does not count as saturated and the rule stays silent
valve_just_over_full_thresholdThreshold edge, other side: the same 5 °C miss with the valve at 99.5% clears the strict comparison and alarms after alarm_delay
coil_saturated_and_missing_setpointThe valve is wide open and SAT is stuck 5 °C above setpoint — the control loop has asked for everything and the temperature has not moved. Chilled water too warm, a fouled or undersized coil, or a valve that reports open and is not: capacity, supply, or sensor, never tuning
morning_pulldown_clears_before_delayTransient: the valve pins at 100% and SAT runs 5 °C high while the coil pulls the building down after a warm night, reaching setpoint at t = 1200. A pulldown is shorter than alarm_delay, so nothing alarms
fault_clears_when_chilled_water_returnsA saturated valve missing setpoint alarms at 1800 s; the chiller comes back at t = 3000 and SAT reaches setpoint. TrueDelay has no off-delay, so the alarm drops on that tick
valve_backs_off_before_delayTransient: SAT stays 5 °C high but the valve modulates back to 85% at t = 1200. A loop with reserve left is a loop still working, so persistence restarts and no alarm follows — the saturation half is what makes the setpoint miss evidence of a defect
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 300,
    "horizon_s": 5400
  },
  "scenarios": [
    {
      "name": "coil_modulating_on_setpoint",
      "description": "Ordinary mechanical cooling: SAT is on its 13 \u00b0C setpoint with the valve at 65%. The loop has reserve in both directions, which is what a healthy cooling coil looks like",
      "inputs": {
        "sat": 13.0,
        "sat_sp": 13.0,
        "clg_vlv_cmd": 65.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "valve_wide_open_holding_setpoint",
      "description": "The valve is at 100% and SAT is 0.5 \u00b0C high \u2014 a coil sized close to its design point on a design day. Half a degree is inside eSAT, so this is a unit at capacity, not a unit that has lost capacity",
      "inputs": {
        "sat": 13.5,
        "sat_sp": 13.0,
        "clg_vlv_cmd": 100.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "sat_error_exactly_at_threshold",
      "description": "Threshold edge: SAT \u2212 SATSP = 1.0 \u00b0C exactly, the whole eSAT allowance, with the valve at 100%. `spMiss` is a strict `>`, so an error sitting precisely on the sensor allowance reads healthy",
      "inputs": {
        "sat": 14.0,
        "sat_sp": 13.0,
        "clg_vlv_cmd": 100.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "sat_error_just_over_threshold",
      "description": "Threshold edge, other side: 1.1 \u00b0C above setpoint at 100% valve clears the strict comparison, and the alarm asserts one alarm_delay (1800 s) later",
      "inputs": {
        "sat": 14.1,
        "sat_sp": 13.0,
        "clg_vlv_cmd": 100.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 5400,
          "equals": true
        }
      ]
    },
    {
      "name": "valve_exactly_at_full_threshold",
      "description": "Threshold edge: a 5 \u00b0C setpoint miss with the valve parked on exactly 99.0%. `clgFull` is a strict `>`, so a command sitting on cc_full_threshold does not count as saturated and the rule stays silent",
      "inputs": {
        "sat": 18.0,
        "sat_sp": 13.0,
        "clg_vlv_cmd": 99.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "valve_just_over_full_threshold",
      "description": "Threshold edge, other side: the same 5 \u00b0C miss with the valve at 99.5% clears the strict comparison and alarms after alarm_delay",
      "inputs": {
        "sat": 18.0,
        "sat_sp": 13.0,
        "clg_vlv_cmd": 99.5
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 5400,
          "equals": true
        }
      ]
    },
    {
      "name": "coil_saturated_and_missing_setpoint",
      "description": "The valve is wide open and SAT is stuck 5 \u00b0C above setpoint \u2014 the control loop has asked for everything and the temperature has not moved. Chilled water too warm, a fouled or undersized coil, or a valve that reports open and is not: capacity, supply, or sensor, never tuning",
      "inputs": {
        "sat": 18.0,
        "sat_sp": 13.0,
        "clg_vlv_cmd": 100.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 5400,
          "equals": true
        }
      ]
    },
    {
      "name": "morning_pulldown_clears_before_delay",
      "description": "Transient: the valve pins at 100% and SAT runs 5 \u00b0C high while the coil pulls the building down after a warm night, reaching setpoint at t = 1200. A pulldown is shorter than alarm_delay, so nothing alarms",
      "inputs": {
        "sat": [
          {
            "t": 0,
            "value": 18.0
          },
          {
            "t": 1200,
            "value": 13.2
          }
        ],
        "sat_sp": 13.0,
        "clg_vlv_cmd": 100.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "fault_clears_when_chilled_water_returns",
      "description": "A saturated valve missing setpoint alarms at 1800 s; the chiller comes back at t = 3000 and SAT reaches setpoint. `TrueDelay` has no off-delay, so the alarm drops on that tick",
      "inputs": {
        "sat": [
          {
            "t": 0,
            "value": 18.0
          },
          {
            "t": 3000,
            "value": 13.0
          }
        ],
        "sat_sp": 13.0,
        "clg_vlv_cmd": 100.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": "valve_backs_off_before_delay",
      "description": "Transient: SAT stays 5 \u00b0C high but the valve modulates back to 85% at t = 1200. A loop with reserve left is a loop still working, so persistence restarts and no alarm follows \u2014 the saturation half is what makes the setpoint miss evidence of a defect",
      "inputs": {
        "sat": 18.0,
        "sat_sp": 13.0,
        "clg_vlv_cmd": [
          {
            "t": 0,
            "value": 100.0
          },
          {
            "t": 1200,
            "value": 85.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    }
  ]
}