AHU-0023 — Supply air temperature reset not functioning
| Status | verified — engine e2ff2f8, cxf:fnv1a128:16397a4af5f0c3617c2f555a71820b83, 2026-08-17 |
| Severity | 3 |
| Method | statistical |
| Phase | 2 |
| Category | EXCESS_CONSUMPTION |
| Confidence | HIGH |
| Estimation | PROXY_ESTIMATION |
| G36 | — |
| Clusters | CLU-02 |
| Suppresses | — |
| Suppressed by | — |
| Related | AHU-0019, AHU-0024 |
| Playbooks | missing-reset |
| Source | HVAC FDD Reference v1.0 §9, AHU-0023; PNNL RetuningOpps A03; PNNL-25985 EEM-05 |
| Operating states | Occupied cooling modes (OS 2, 3, 4) |
Preconditions (host-enforced): AHU running for the bulk of the evaluation window. Sufficient outdoor-air variation is signaled in-rule via the yOatVaried output: when it is false, the host must report NO_EVAL, not healthy.
Outputs:
yFault— True while the SAT setpoint has stayed flat over the evaluation window despite sufficient OAT variation, for at least alarm_delayyOatVaried— Evaluability signal — true when OAT has varied enough within the evaluation window for the rule to be meaningful; false means NO_EVAL
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
evaluation_window | 604800.0 | s | spRef.samplePeriod, oatRef.samplePeriod, spFlatHeld.delayTime, oatFlatHeld.delayTime | Window over which setpoint flatness and OAT variation are assessed (7 days); drives both baseline sample periods and both dwell timers |
sp_flat_tolerance | 1.0 | °C | spFlat.t | Max deviation of SAT_SP from its sampled baseline to count as flat (half the reference’s 2 °C min expected range) |
oat_variation_tolerance | 4.0 | °C | oatFlat.t | Max deviation of OAT from its sampled baseline to still count as flat (half the reference’s 8 °C min OAT range) |
alarm_delay | 86400.0 | s | persist.delayTime | Fault persistence before alarm (24 h) |
Description
The supply air temperature setpoint remains fixed over an extended evaluation window despite varying outdoor-air and load conditions. A functioning SAT reset (G36 §5.16.2 trim-and-respond) modulates the setpoint warmer as loads decrease, cutting mechanical cooling and downstream VAV reheat. PNNL’s 151-building field study found SAT reset absent in 74% of buildings — this rule and its twin AHU-0024 are the “74% problem,” the trigger pair for cluster CLU-02, and the fix is a $0 desk-only sequence change worth ~2.5% of site energy on its own.
Detection Logic
baseline(x) = x sampled and held every evaluation_window (7 days)
sp_flat = |sat_sp − baseline(sat_sp)| < sp_flat_tolerance,
continuously for evaluation_window
oat_flat = |oat − baseline(oat)| < oat_variation_tolerance,
continuously for evaluation_window
yOatVaried = NOT oat_flat (false ⇒ host reports NO_EVAL)
yFault = sp_flat AND yOatVaried, sustained for alarm_delay
Block graph (rule.cxf.jsonld):
Two symmetric chains compare each signal against a weekly sample-and-hold
baseline (Discrete.Sampler, which emits the live input on its first tick —
no startup artifact). spFlatHeld asserts only after the setpoint has stayed
within tolerance of its baseline continuously for a full window; any reset
activity ≥ 1 °C resets it. oatFlatHeld does the same for OAT, and its
negation is yOatVaried: “not varied” is defined as a full window of
continuous flatness. In the synchronous dataflow both dwell timers fire on
the same tick in the all-flat case, so the fault conjunction is false by
construction — no boundary race — and persist (24 h) filters any residual
transient. Worst-case time to alarm from cold start:
evaluation_window + alarm_delay (8 days).
Possible Diagnoses
- SAT reset sequence never programmed in the BAS
- SAT reset sequence disabled by an operator
- SAT setpoint overridden to a fixed value
- Trim-and-respond parameters misconfigured
- Zone request signals not reaching the AHU controller
Energy Impact
EXCESS_CONSUMPTION, HIGH confidence, PROXY_ESTIMATION (EEM-05). A fixed-low SAT over-cools the air stream and drives downstream VAV reheat; savings from enabling reset are 1–4.4% of site energy (2.5% national weighted median), heating-dominant by climate. Prevalence: 74% of buildings.
Emissions Impact
Scope 2, PROXY_EMISSIONS, HIGH confidence; typical 800–5,000 kg CO₂e/yr (excess cooling and reheat). Avoided-emissions basis: MOER.
Deviations
- Windowed range → deviation from a weekly sampled baseline. The reference
computes
max − minover the window and CDL has no windowed min/max, so each signal is compared against aDiscrete.Samplerhold refreshed once per window, with tolerances at half the reference ranges. Equivalent detection for signals that move and return, slightly conservative for monotonic drift within one window.Reals.MovingAveragewas rejected: the engine implements it with a 64-checkpoint ring, so a 7-day window silently degrades at BAS tick rates, while the sampler is exact at any tick rate. - NO_EVAL is surfaced as the second output
yOatVaried, since boolean block logic cannot express a tri-state; false means NO_EVAL, never healthy. Its inverted-flat semantics make it optimistically true during the first window after startup, whenyFaultcannot fire anyway becausespFlatHeldneeds the same full window. AlarmDelay= 24 h is aTrueDelayon the fault conjunction; the evaluation window itself is enforced by the flatness dwell timers.delayOnInit = trueon allTrueDelays (startup conservatism per AHU-0016).
Notes
Fastest payback in the catalog together with AHU-0024: remote fix ~90%, $0, programmed per G36 §5.16.2 (start 18 °C; trim +0.2 °F when satisfied; respond −0.5 °F per cooling request; range 13–18 °C). See the missing-reset playbook.
Test Vectors
4 scenarios, clock step 300 s over 777600 s.
| Scenario | Description |
|---|---|
reset_working | Reference vector: SAT_SP modulates ~4degC with OAT swings; no fault |
fixed_setpoint | Reference vector: SAT_SP pinned at 13degC despite 15degC OAT swings; fault after evaluation_window (7d) + alarm_delay (24h) |
insufficient_oat_variation | Reference vector (NO_EVAL): flat SAT_SP but OAT never varies; yOatVaried drops false once OAT has been flat for a full window, so the host reports NO_EVAL; yFault must never fire |
reset_resumes | Setpoint flat for 4 days, then reset activity resumes before the window completes; flatness timer resets, never alarms |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 300,
"horizon_s": 777600
},
"scenarios": [
{
"name": "reset_working",
"description": "Reference vector: SAT_SP modulates ~4degC with OAT swings; no fault",
"inputs": {
"sat_sp": [
{
"t": 0,
"value": 13.0
},
{
"t": 43200,
"value": 17.0
},
{
"t": 86400,
"value": 13.0
},
{
"t": 129600,
"value": 17.0
},
{
"t": 172800,
"value": 13.0
},
{
"t": 216000,
"value": 17.0
},
{
"t": 259200,
"value": 13.0
},
{
"t": 302400,
"value": 17.0
},
{
"t": 345600,
"value": 13.0
},
{
"t": 388800,
"value": 17.0
},
{
"t": 432000,
"value": 13.0
},
{
"t": 475200,
"value": 17.0
},
{
"t": 518400,
"value": 13.0
},
{
"t": 561600,
"value": 17.0
},
{
"t": 604800,
"value": 13.0
},
{
"t": 648000,
"value": 17.0
},
{
"t": 691200,
"value": 13.0
},
{
"t": 734400,
"value": 17.0
},
{
"t": 777600,
"value": 13.0
}
],
"oat": [
{
"t": 0,
"value": 5.0
},
{
"t": 43200,
"value": 20.0
},
{
"t": 86400,
"value": 5.0
},
{
"t": 129600,
"value": 20.0
},
{
"t": 172800,
"value": 5.0
},
{
"t": 216000,
"value": 20.0
},
{
"t": 259200,
"value": 5.0
},
{
"t": 302400,
"value": 20.0
},
{
"t": 345600,
"value": 5.0
},
{
"t": 388800,
"value": 20.0
},
{
"t": 432000,
"value": 5.0
},
{
"t": 475200,
"value": 20.0
},
{
"t": 518400,
"value": 5.0
},
{
"t": 561600,
"value": 20.0
},
{
"t": 604800,
"value": 5.0
},
{
"t": 648000,
"value": 20.0
},
{
"t": 691200,
"value": 5.0
},
{
"t": 734400,
"value": 20.0
},
{
"t": 777600,
"value": 5.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 777600,
"equals": false
},
{
"output": "yOatVaried",
"from_s": 0,
"to_s": 777600,
"equals": true
}
]
},
{
"name": "fixed_setpoint",
"description": "Reference vector: SAT_SP pinned at 13degC despite 15degC OAT swings; fault after evaluation_window (7d) + alarm_delay (24h)",
"inputs": {
"sat_sp": 13.0,
"oat": [
{
"t": 0,
"value": 5.0
},
{
"t": 43200,
"value": 20.0
},
{
"t": 86400,
"value": 5.0
},
{
"t": 129600,
"value": 20.0
},
{
"t": 172800,
"value": 5.0
},
{
"t": 216000,
"value": 20.0
},
{
"t": 259200,
"value": 5.0
},
{
"t": 302400,
"value": 20.0
},
{
"t": 345600,
"value": 5.0
},
{
"t": 388800,
"value": 20.0
},
{
"t": 432000,
"value": 5.0
},
{
"t": 475200,
"value": 20.0
},
{
"t": 518400,
"value": 5.0
},
{
"t": 561600,
"value": 20.0
},
{
"t": 604800,
"value": 5.0
},
{
"t": 648000,
"value": 20.0
},
{
"t": 691200,
"value": 5.0
},
{
"t": 734400,
"value": 20.0
},
{
"t": 777600,
"value": 5.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 688800,
"equals": false
},
{
"output": "yFault",
"from_s": 694800,
"to_s": 777600,
"equals": true
},
{
"output": "yOatVaried",
"from_s": 0,
"to_s": 777600,
"equals": true
}
]
},
{
"name": "insufficient_oat_variation",
"description": "Reference vector (NO_EVAL): flat SAT_SP but OAT never varies; yOatVaried drops false once OAT has been flat for a full window, so the host reports NO_EVAL; yFault must never fire",
"inputs": {
"sat_sp": 13.0,
"oat": 10.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 777600,
"equals": false
},
{
"output": "yOatVaried",
"from_s": 0,
"to_s": 602400,
"equals": true
},
{
"output": "yOatVaried",
"from_s": 608400,
"to_s": 777600,
"equals": false
}
]
},
{
"name": "reset_resumes",
"description": "Setpoint flat for 4 days, then reset activity resumes before the window completes; flatness timer resets, never alarms",
"inputs": {
"sat_sp": [
{
"t": 0,
"value": 13.0
},
{
"t": 345600,
"value": 17.0
},
{
"t": 388800,
"value": 13.0
},
{
"t": 432000,
"value": 17.0
},
{
"t": 475200,
"value": 13.0
},
{
"t": 518400,
"value": 17.0
},
{
"t": 561600,
"value": 13.0
},
{
"t": 604800,
"value": 17.0
},
{
"t": 648000,
"value": 13.0
},
{
"t": 691200,
"value": 17.0
},
{
"t": 734400,
"value": 13.0
},
{
"t": 777600,
"value": 17.0
}
],
"oat": [
{
"t": 0,
"value": 5.0
},
{
"t": 43200,
"value": 20.0
},
{
"t": 86400,
"value": 5.0
},
{
"t": 129600,
"value": 20.0
},
{
"t": 172800,
"value": 5.0
},
{
"t": 216000,
"value": 20.0
},
{
"t": 259200,
"value": 5.0
},
{
"t": 302400,
"value": 20.0
},
{
"t": 345600,
"value": 5.0
},
{
"t": 388800,
"value": 20.0
},
{
"t": 432000,
"value": 5.0
},
{
"t": 475200,
"value": 20.0
},
{
"t": 518400,
"value": 5.0
},
{
"t": 561600,
"value": 20.0
},
{
"t": 604800,
"value": 5.0
},
{
"t": 648000,
"value": 20.0
},
{
"t": 691200,
"value": 5.0
},
{
"t": 734400,
"value": 20.0
},
{
"t": 777600,
"value": 5.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 777600,
"equals": false
}
]
}
]
}