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-0023 — Supply air temperature reset not functioning

Statusverified — engine e2ff2f8, cxf:fnv1a128:16397a4af5f0c3617c2f555a71820b83, 2026-08-17
Severity3
Methodstatistical
Phase2
CategoryEXCESS_CONSUMPTION
ConfidenceHIGH
EstimationPROXY_ESTIMATION
G36
ClustersCLU-02
Suppresses
Suppressed by
RelatedAHU-0019, AHU-0024
Playbooksmissing-reset
SourceHVAC FDD Reference v1.0 §9, AHU-0023; PNNL RetuningOpps A03; PNNL-25985 EEM-05
Operating statesOccupied 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.

Points: sat_sp, oat

Outputs:

  • yFault — True while the SAT setpoint has stayed flat over the evaluation window despite sufficient OAT variation, for at least alarm_delay
  • yOatVaried — Evaluability signal — true when OAT has varied enough within the evaluation window for the rule to be meaningful; false means NO_EVAL

Parameters:

NameDefaultUnitCXF pathDescription
evaluation_window604800.0sspRef.samplePeriod, oatRef.samplePeriod, spFlatHeld.delayTime, oatFlatHeld.delayTimeWindow over which setpoint flatness and OAT variation are assessed (7 days); drives both baseline sample periods and both dwell timers
sp_flat_tolerance1.0°CspFlat.tMax deviation of SAT_SP from its sampled baseline to count as flat (half the reference’s 2 °C min expected range)
oat_variation_tolerance4.0°CoatFlat.tMax deviation of OAT from its sampled baseline to still count as flat (half the reference’s 8 °C min OAT range)
alarm_delay86400.0spersist.delayTimeFault 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):

AHU-0023 block graph

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

  1. SAT reset sequence never programmed in the BAS
  2. SAT reset sequence disabled by an operator
  3. SAT setpoint overridden to a fixed value
  4. Trim-and-respond parameters misconfigured
  5. 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 − min over the window and CDL has no windowed min/max, so each signal is compared against a Discrete.Sampler hold 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.MovingAverage was 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, when yFault cannot fire anyway because spFlatHeld needs the same full window.
  • AlarmDelay = 24 h is a TrueDelay on the fault conjunction; the evaluation window itself is enforced by the flatness dwell timers.
  • delayOnInit = true on all TrueDelays (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.

ScenarioDescription
reset_workingReference vector: SAT_SP modulates ~4degC with OAT swings; no fault
fixed_setpointReference vector: SAT_SP pinned at 13degC despite 15degC OAT swings; fault after evaluation_window (7d) + alarm_delay (24h)
insufficient_oat_variationReference 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_resumesSetpoint 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
        }
      ]
    }
  ]
}