AHU-0019 — Supply air temperature setpoint too low (over-cooling)
| Status | verified — engine e2ff2f8, cxf:fnv1a128:07fe9d3b43f17351d191782e29b6577c, 2026-08-17 |
| Severity | 3 |
| Method | rule |
| Phase | 1 |
| Category | EXCESS_CONSUMPTION |
| Confidence | HIGH |
| Estimation | PROXY_ESTIMATION |
| G36 | — |
| Clusters | CLU-02 |
| Suppresses | — |
| Suppressed by | — |
| Related | VAV-0001, AHU-0022, AHU-0023 |
| Playbooks | missing-reset |
| Source | HVAC FDD Reference v1.0 §9, AHU-0019; PNNL-27338; PNNL-25985 EEM-05/EEM-15 |
| Operating states | Occupied (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:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
sat_sp_low_limit | 12.0 | °C | spLow.t | Minimum recommended SAT setpoint; below this the air is colder than any zone needs |
reheat_fraction_threshold | 0.5 | 1 | rhtHigh.t | Fraction of served zones reheating (0-1) above which the cold air is demonstrably being reheated |
alarm_delay | 3600.0 | s | persist.delayTime | Continuous 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):
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
- SAT setpoint too aggressive — set low at commissioning and never reset since
- SAT reset logic disabled or misconfigured (the CLU-02 root cause; confirm with AHU-0023)
- 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_thresholdto 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(flaggedderivedin the dictionary, same pattern aszone_dmpr_pos_maxin AHU-0024). The reheat-active counting threshold is host configuration, not a rule parameter. reheat_fraction_thresholdis a fraction 0–1, not a percent: the reference states 50%, but the point compared against carries unit1, 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 isfalse), 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.
| Scenario | Description |
|---|---|
setpoint_above_limit_light_reheat | Reference vector: SAT setpoint 14 °C with 20% of zones reheating — setpoint above the low limit, nothing to report |
setpoint_low_but_reheat_light | Reference 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_reheat | Reference 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_limit | Edge 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_threshold | Edge 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_delay | Transient: 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_delay | Transient: 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_persistence | Transient: 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
}
]
}
]
}