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-0019 — Supply air temperature setpoint too low (over-cooling)

Statusverified — engine e2ff2f8, cxf:fnv1a128:07fe9d3b43f17351d191782e29b6577c, 2026-08-17
Severity3
Methodrule
Phase1
CategoryEXCESS_CONSUMPTION
ConfidenceHIGH
EstimationPROXY_ESTIMATION
G36
ClustersCLU-02
Suppresses
Suppressed by
RelatedVAV-0001, AHU-0022, AHU-0023
Playbooksmissing-reset
SourceHVAC FDD Reference v1.0 §9, AHU-0019; PNNL-27338; PNNL-25985 EEM-05/EEM-15
Operating statesOccupied (OS 2, 3, 4)

Preconditions (host-enforced): AHU in occupied mode and serving multiple zones; zone reheat data available and aggregated by the host into zone_reheat_fraction. When the zone data is missing, stale, or covers too few zones for the fraction to mean anything, the verdict is NO_EVAL, not healthy.

Points: sat_sp, zone_reheat_fraction

Outputs:

  • yFault — True while the SAT setpoint has stayed below sat_sp_low_limit with more than reheat_fraction_threshold of zones reheating, for at least alarm_delay

Parameters:

NameDefaultUnitCXF pathDescription
sat_sp_low_limit12.0°CspLow.tMinimum recommended SAT setpoint; below this the air is colder than any zone needs
reheat_fraction_threshold0.51rhtHigh.tFraction of served zones reheating (0-1) above which the cold air is demonstrably being reheated
alarm_delay3600.0spersist.delayTimeContinuous fault persistence required before the alarm asserts (60 min)

Description

The supply air temperature setpoint sits below the minimum recommended value while a large share of the zones served are running reheat. Air colder than any zone asked for costs chiller energy to make and then boiler or electric reheat energy to undo, so every degree of over-cooling is paid for twice — and the zones stay comfortable throughout, which is why a setpoint parked at 10 °C survives for years. SAT reset is absent in 74% of buildings (PNNL 151-building study); this is the CLU-02 member fault that shows the missing reset actually costing money.

Detection Logic

yFault = sat_sp < sat_sp_low_limit
     AND zone_reheat_fraction > reheat_fraction_threshold
     sustained continuously for alarm_delay

Block graph (rule.cxf.jsonld):

AHU-0019 block graph

Two threshold tests feed one conjunction and one timer. spLow watches the setpoint, not the measured SAT: this rule is about what the sequence asked for, while a unit that cannot hold its setpoint (AHU-0007, AHU-0013) or hunts around it (AHU-0022) is a separate finding. rhtHigh supplies the corroboration that turns “cold setpoint” into “waste” — without a reheat majority, a 10 °C setpoint may simply be serving a high-load hour. Both comparisons are strict, so a setpoint parked exactly on the 12 °C limit, or exactly half the zones reheating, does not trip the rule. persist requires 60 minutes of continuous violation — enough to ride out morning cool-down and the reheat spike after an occupied-mode transition — and delayOnInit = true holds that window across a controller restart.

Possible Diagnoses

  1. SAT setpoint too aggressive — set low at commissioning and never reset since
  2. SAT reset logic disabled or misconfigured (the CLU-02 root cause; confirm with AHU-0023)
  3. A single rogue zone dragging the AHU setpoint down through the trim-and-respond request path — one starved or mis-sensored box can hold the whole system at its minimum setpoint

Energy Impact

EXCESS_CONSUMPTION, HIGH confidence, PROXY_ESTIMATION. The waste is a reheat integral: excess_reheat_kw = Σ over reheating zones of (rht_vlv_cmd_i/100 × vav_rht_capacity_kw_i), plus the chiller energy spent making air nobody wanted. Raising the setpoint into the reset band saves 5–15% of AHU cooling plus reheat energy (PNNL-25985 EEM-05 SAT reset and EEM-15 VAV minimum flow reduction, combined — the two interact, since a high minimum flow forces reheat no setpoint change can eliminate). Heating-dominant: the reheat half of the bill grows with hours spent below balance point.

Emissions Impact

Scope 2, PROXY_EMISSIONS, HIGH confidence; typical 800–5,000 kg CO₂e/yr (excess cooling plus reheat). Sites with gas or steam reheat move that half of the inventory into scope 1; the card reports scope 2 because electric reheat and electric chilling are the common case. Avoided-emissions basis: marginal operating emissions rate (MOER).

Deviations

  • The reference’s single 50% zone-count fraction diverges from its own PNNL source: PNNL-27338 §2.2.2–2.2.3 tests two quantities — zones with reheat valves open (>10%) exceeding 25%, AND fleet-average reheat command above 50%. This card follows the reference. Sites wanting the PNNL-literal form retune reheat_fraction_threshold to 0.25; the magnitude conjunct is not expressible without a host-derived mean reheat command, which the point dictionary does not yet carry.
  • The reference counts reheating zones across a per-zone valve command array. Library v1 avoids array boundary points, so the host does the counting and feeds the scalar zone_reheat_fraction (flagged derived in the dictionary, same pattern as zone_dmpr_pos_max in AHU-0024). The reheat-active counting threshold is host configuration, not a rule parameter.
  • reheat_fraction_threshold is a fraction 0–1, not a percent: the reference states 50%, but the point compared against carries unit 1, so the parameter is 0.5. Hosts feeding a 0–100 percentage fire this rule on nearly every tick.
  • Both comparisons are strict (<, >); the reference does not specify boundary behavior, so the library’s strict convention applies.
  • The reference tags this fault for both AHU and RTU. This card is the AHU-family instance; an RTU-0004 would restate it against the RTU’s discharge setpoint and its zone group.
  • Operating-state gating (OS 2, 3, 4 — occupied) and the multi-zone precondition are declared in frontmatter for host enforcement rather than encoded in the block graph, per the library’s design stance.
  • persist.delayOnInit = true (Modelica/CDL default is false), the library’s standing choice: a violation already present at load waits out the full 60 minutes instead of alarming on the first tick after a controller restart.

Notes

AHU-0023 and this rule are the two halves of the same CLU-02 story. FC-057 is the statistical trigger — it proves the setpoint never moves. This rule is the harm case: the setpoint is parked low and the zones downstream are burning fuel to undo it. Fix order is FC-057’s fix — program the reset per G36 §5.16.2 (playbook missing-reset, step 2.2) — after which this rule should clear within one occupied day. Before raising the setpoint, check the zone minimum flows: a box with a 40% minimum will reheat at any SAT and will keep this rule firing after the reset is programmed.

Test Vectors

8 scenarios, clock step 60 s over 7200 s.

ScenarioDescription
setpoint_above_limit_light_reheatReference vector: SAT setpoint 14 °C with 20% of zones reheating — setpoint above the low limit, nothing to report
setpoint_low_but_reheat_lightReference vector: SAT setpoint 10 °C with only 30% of zones reheating — a cold setpoint the zones are not fighting, so no fault
setpoint_low_with_widespread_reheatReference vector: SAT setpoint 10 °C with 65% of zones reheating — over-cooling paid for by reheat; alarms after alarm_delay (3600 s)
setpoint_exactly_at_low_limitEdge case: SAT setpoint sits exactly on sat_sp_low_limit (12 °C) with 65% of zones reheating — the strict < keeps it out of the alarm
reheat_exactly_at_thresholdEdge case: exactly half the zones reheating (0.5) against a 10 °C setpoint — the strict > means the fraction must exceed the threshold
reheat_burst_clears_before_delayTransient: a morning reheat burst at 0.65 falls back to 0.30 at t=1800 — the condition clears inside alarm_delay, so no alarm
reset_raises_setpoint_before_delayTransient: reheat stays at 0.65 but a working SAT reset walks the setpoint from 10 °C to 13.5 °C at t=1800 — the fault condition ends before it can alarm
reheat_dip_restarts_persistenceTransient: reheat dips to 0.40 at t=900 and returns to 0.65 at t=1200 against a 10 °C setpoint — persistence restarts from the return, so the alarm lands at 4800 s, not 3600 s
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 60,
    "horizon_s": 7200
  },
  "scenarios": [
    {
      "name": "setpoint_above_limit_light_reheat",
      "description": "Reference vector: SAT setpoint 14 \u00b0C with 20% of zones reheating \u2014 setpoint above the low limit, nothing to report",
      "inputs": {
        "sat_sp": 14.0,
        "zone_reheat_fraction": 0.2
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "setpoint_low_but_reheat_light",
      "description": "Reference vector: SAT setpoint 10 \u00b0C with only 30% of zones reheating \u2014 a cold setpoint the zones are not fighting, so no fault",
      "inputs": {
        "sat_sp": 10.0,
        "zone_reheat_fraction": 0.3
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "setpoint_low_with_widespread_reheat",
      "description": "Reference vector: SAT setpoint 10 \u00b0C with 65% of zones reheating \u2014 over-cooling paid for by reheat; alarms after alarm_delay (3600 s)",
      "inputs": {
        "sat_sp": 10.0,
        "zone_reheat_fraction": 0.65
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 3540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 3660,
          "to_s": 7200,
          "equals": true
        }
      ]
    },
    {
      "name": "setpoint_exactly_at_low_limit",
      "description": "Edge case: SAT setpoint sits exactly on sat_sp_low_limit (12 \u00b0C) with 65% of zones reheating \u2014 the strict `<` keeps it out of the alarm",
      "inputs": {
        "sat_sp": 12.0,
        "zone_reheat_fraction": 0.65
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "reheat_exactly_at_threshold",
      "description": "Edge case: exactly half the zones reheating (0.5) against a 10 \u00b0C setpoint \u2014 the strict `>` means the fraction must exceed the threshold",
      "inputs": {
        "sat_sp": 10.0,
        "zone_reheat_fraction": 0.5
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "reheat_burst_clears_before_delay",
      "description": "Transient: a morning reheat burst at 0.65 falls back to 0.30 at t=1800 \u2014 the condition clears inside alarm_delay, so no alarm",
      "inputs": {
        "sat_sp": 10.0,
        "zone_reheat_fraction": [
          {
            "t": 0,
            "value": 0.65
          },
          {
            "t": 1800,
            "value": 0.3
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "reset_raises_setpoint_before_delay",
      "description": "Transient: reheat stays at 0.65 but a working SAT reset walks the setpoint from 10 \u00b0C to 13.5 \u00b0C at t=1800 \u2014 the fault condition ends before it can alarm",
      "inputs": {
        "sat_sp": [
          {
            "t": 0,
            "value": 10.0
          },
          {
            "t": 1800,
            "value": 13.5
          }
        ],
        "zone_reheat_fraction": 0.65
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 7200,
          "equals": false
        }
      ]
    },
    {
      "name": "reheat_dip_restarts_persistence",
      "description": "Transient: reheat dips to 0.40 at t=900 and returns to 0.65 at t=1200 against a 10 \u00b0C setpoint \u2014 persistence restarts from the return, so the alarm lands at 4800 s, not 3600 s",
      "inputs": {
        "sat_sp": 10.0,
        "zone_reheat_fraction": [
          {
            "t": 0,
            "value": 0.65
          },
          {
            "t": 900,
            "value": 0.4
          },
          {
            "t": 1200,
            "value": 0.65
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 4740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 4860,
          "to_s": 7200,
          "equals": true
        }
      ]
    }
  ]
}