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-0031 — Supply fan operating at excessive static pressure

Statusverified — engine e2ff2f8, cxf:fnv1a128:7137eb21f1dba596056b75ab058fd631, 2026-08-17
Severity3
Methodrule
Phase2
CategoryEXCESS_CONSUMPTION
ConfidenceHIGH
EstimationPROXY_ESTIMATION
G36
ClustersCLU-02
Suppresses
Suppressed by
RelatedAHU-0024, AHU-0001, FPB-0002
Playbooksmissing-reset
SourceHVAC FDD Reference v1.0 §9, AHU-0031; PNNL-25985 EEM-12/EEM-15; NIST TN 2024; PNNL-27338
Operating statesOccupied, fan running

Preconditions (host-enforced): AHU serves multiple zones; zone damper feedback available and aggregated by the host into zone_dmpr_pos_max, as AHU-0024. When zone damper data is missing or stale, the verdict is NO_EVAL, not healthy.

Points: dsp, dsp_sp, zone_dmpr_pos_max, sf_status

Outputs:

  • yFault — True while duct static pressure has held above high_sp_fraction of its setpoint with every zone damper below low_demand_damper_threshold and the fan running, for at least alarm_delay

Parameters:

NameDefaultUnitCXF pathDescription
high_sp_fraction0.951scaled.kFraction of the DSP setpoint above which the fan counts as running at the top of its pressure band
low_demand_damper_threshold50.0%dmprLow.tHighest zone damper position below which no zone is asking for the pressure being delivered
alarm_delay1800.0spersist.delayTimeContinuous fault persistence required before the alarm asserts (30 min)

Description

The fan holds duct static pressure at the top of its band while every zone damper sits well below fully open. Together those facts say the setpoint is higher than the system needs: the dampers are throttling away pressure the fan spent energy producing, and the excess leaves as noise, leakage, and heat. Fan power scales with the cube of pressure, so a setpoint 20% above what the worst zone needs costs roughly half again the fan energy — and trimming it back returns that energy immediately at no capital cost. DSP reset is absent in 74% of buildings (PNNL 151-building study); this is the CLU-02 member that catches the resulting operating symptom.

Detection Logic

yFault = dsp > dsp_sp × high_sp_fraction
     AND zone_dmpr_pos_max < low_demand_damper_threshold
     AND sf_status
     sustained continuously for alarm_delay

Block graph (rule.cxf.jsonld):

AHU-0031 block graph

scaled turns the live setpoint into the top-of-band pressure — a ratio rather than a fixed offset, so the test travels with a setpoint the reset is allowed to move. spHigh compares the measurement against that band and dmprLow against the host-aggregated damper maximum; demand requires both, which is the whole diagnostic content of the rule. Note what the pair excludes: high pressure with an open damper is a system doing its job, and low dampers at low pressure are a reset already working. gated adds the fan-running condition, since dsp and dsp_sp mean nothing with the fan off and a stale reading would otherwise alarm overnight. persist requires 30 minutes of continuous violation, riding out morning start-up and the settling period after a setpoint change; delayOnInit = true holds that window across a controller restart.

Possible Diagnoses

  1. DSP setpoint configured too high — a design-static value entered once and never revisited
  2. Trim-and-respond active but not aggressive enough (trim magnitude too small, trim interval too long, or a minimum setpoint floor set at the old fixed value)
  3. DSP reset responding to one rogue zone — a single box with a stuck damper or a failed flow sensor requests pressure continuously and holds the whole system up
  4. Zone damper feedback not wired back to the AHU controller, so the reset has nothing to respond to and parks at maximum

Energy Impact

EXCESS_CONSUMPTION, HIGH confidence, PROXY_ESTIMATION (EEM-12). Excess fan power follows the cube law: excess_fan_kw = ahu_fan_design_kw × [1 − (needed_dsp/actual_dsp)³], where needed_dsp is the pressure at which the most-open zone reaches its damper target. Savings run 5–30% of fan energy depending on how far above the true requirement the setpoint sits — the wide range is the cube law, not measurement uncertainty. Climate-neutral.

Emissions Impact

Scope 2, PROXY_EMISSIONS, HIGH confidence; typical 300–2,000 kg CO₂e/yr (excess fan energy). Fan waste runs through the whole occupied period, so it lands across the daytime grid mix rather than concentrating in any one hour. Avoided-emissions basis: marginal operating emissions rate (MOER).

Deviations

  • The reference’s dsp >= dsp_sp × high_sp_fraction becomes a strict >: CDL has no Reals.GreaterEqual, the exact-equality case has measure zero on a measured pressure signal, and the strict form errs toward silence.
  • The reference takes max() over a per-zone damper array; library v1 avoids array boundary points, so the host aggregates and feeds the scalar zone_dmpr_pos_max — the same point AHU-0024 consumes, flagged derived in the point dictionary.
  • The fan-running condition is in the block graph rather than the preconditions: the library gates operating state host-side, but the reference states sf_status = ON as part of its detection logic and sf_status is a canonical point, so it is wired as a boundary input (as in AHU-0018). The multi-zone and damper-data preconditions stay in frontmatter.
  • high_sp_fraction is dimensionless (0–1), consistent with the reference’s 0.95; low_demand_damper_threshold stays in percent because zone_dmpr_pos_max is a percent point.
  • 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

This rule and AHU-0024 are the DSP half of CLU-02 from opposite sides. FC-058 watches the setpoint sit flat for three days and concludes the reset is absent; this rule does not care whether a reset exists, only that the fan is pushing pressure nobody is asking for. A misconfigured but active reset leaves FC-058 quiet and fires this rule — diagnosis 2. Both share the missing-reset playbook and the same $0 fix per G36 §5.16.1.

Before touching the setpoint, check for a rogue zone (playbook step 1.2). Healthy operation puts most zone dampers in the 50–75% band; all near 0% with pressure at maximum is this rule’s signature, and all near 100% is the opposite fault (AHU-0001, insufficient static pressure).

Test Vectors

9 scenarios, clock step 60 s over 5400 s.

ScenarioDescription
pressure_below_high_bandReference vector: 350 Pa against a 400 Pa setpoint (scaled band 380 Pa) with the highest damper at 85% — pressure is not pinned high, nothing to report
high_pressure_low_demandReference vector: 390 Pa against a 400 Pa setpoint with the highest damper at 35% — pressure held at the top of the band while no zone needs air; alarms after alarm_delay (1800 s)
high_pressure_high_demandReference vector: 390 Pa against a 400 Pa setpoint with the highest damper at 95% — the pressure is being asked for, so no fault
pressure_exactly_at_scaled_setpointEdge case: dsp sits exactly on dsp_sp × high_sp_fraction (380 Pa) with dampers at 35% — the strict > keeps the boundary out of the alarm
damper_exactly_at_thresholdEdge case: the highest damper sits exactly on low_demand_damper_threshold (50%) with pressure pinned at 390 Pa — the strict < means demand must be below the threshold, not at it
fan_off_stale_pressureGate: the fan is off while the pressure and damper readings still show the fault pattern — sf_status suppresses the rule outright
fan_cycle_restarts_persistenceTransient: the fan stops at t=600 and restarts at t=900 with the pressure pattern otherwise unchanged — persistence restarts from the restart, so the alarm lands at 2700 s, not 1800 s
reset_trims_pressure_before_delayTransient: trim-and-respond drops the setpoint to 300 Pa at t=1200 and the fan follows to 280 Pa — the condition clears inside alarm_delay, so a working reset never alarms
dampers_open_before_delayTransient: a zone opens up at t=1200 and takes the highest damper to 80% while pressure stays pinned — demand appears before the timer expires, so no alarm
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 60,
    "horizon_s": 5400
  },
  "scenarios": [
    {
      "name": "pressure_below_high_band",
      "description": "Reference vector: 350 Pa against a 400 Pa setpoint (scaled band 380 Pa) with the highest damper at 85% \u2014 pressure is not pinned high, nothing to report",
      "inputs": {
        "dsp": 350.0,
        "dsp_sp": 400.0,
        "zone_dmpr_pos_max": 85.0,
        "sf_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "high_pressure_low_demand",
      "description": "Reference vector: 390 Pa against a 400 Pa setpoint with the highest damper at 35% \u2014 pressure held at the top of the band while no zone needs air; alarms after alarm_delay (1800 s)",
      "inputs": {
        "dsp": 390.0,
        "dsp_sp": 400.0,
        "zone_dmpr_pos_max": 35.0,
        "sf_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1740,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1860,
          "to_s": 5400,
          "equals": true
        }
      ]
    },
    {
      "name": "high_pressure_high_demand",
      "description": "Reference vector: 390 Pa against a 400 Pa setpoint with the highest damper at 95% \u2014 the pressure is being asked for, so no fault",
      "inputs": {
        "dsp": 390.0,
        "dsp_sp": 400.0,
        "zone_dmpr_pos_max": 95.0,
        "sf_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "pressure_exactly_at_scaled_setpoint",
      "description": "Edge case: dsp sits exactly on dsp_sp \u00d7 high_sp_fraction (380 Pa) with dampers at 35% \u2014 the strict `>` keeps the boundary out of the alarm",
      "inputs": {
        "dsp": 380.0,
        "dsp_sp": 400.0,
        "zone_dmpr_pos_max": 35.0,
        "sf_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "damper_exactly_at_threshold",
      "description": "Edge case: the highest damper sits exactly on low_demand_damper_threshold (50%) with pressure pinned at 390 Pa \u2014 the strict `<` means demand must be below the threshold, not at it",
      "inputs": {
        "dsp": 390.0,
        "dsp_sp": 400.0,
        "zone_dmpr_pos_max": 50.0,
        "sf_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "fan_off_stale_pressure",
      "description": "Gate: the fan is off while the pressure and damper readings still show the fault pattern \u2014 sf_status suppresses the rule outright",
      "inputs": {
        "dsp": 390.0,
        "dsp_sp": 400.0,
        "zone_dmpr_pos_max": 35.0,
        "sf_status": false
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "fan_cycle_restarts_persistence",
      "description": "Transient: the fan stops at t=600 and restarts at t=900 with the pressure pattern otherwise unchanged \u2014 persistence restarts from the restart, so the alarm lands at 2700 s, not 1800 s",
      "inputs": {
        "dsp": 390.0,
        "dsp_sp": 400.0,
        "zone_dmpr_pos_max": 35.0,
        "sf_status": [
          {
            "t": 0,
            "value": true
          },
          {
            "t": 600,
            "value": false
          },
          {
            "t": 900,
            "value": true
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2640,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2760,
          "to_s": 5400,
          "equals": true
        }
      ]
    },
    {
      "name": "reset_trims_pressure_before_delay",
      "description": "Transient: trim-and-respond drops the setpoint to 300 Pa at t=1200 and the fan follows to 280 Pa \u2014 the condition clears inside alarm_delay, so a working reset never alarms",
      "inputs": {
        "dsp": [
          {
            "t": 0,
            "value": 390.0
          },
          {
            "t": 1200,
            "value": 280.0
          }
        ],
        "dsp_sp": [
          {
            "t": 0,
            "value": 400.0
          },
          {
            "t": 1200,
            "value": 300.0
          }
        ],
        "zone_dmpr_pos_max": 35.0,
        "sf_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "dampers_open_before_delay",
      "description": "Transient: a zone opens up at t=1200 and takes the highest damper to 80% while pressure stays pinned \u2014 demand appears before the timer expires, so no alarm",
      "inputs": {
        "dsp": 390.0,
        "dsp_sp": 400.0,
        "zone_dmpr_pos_max": [
          {
            "t": 0,
            "value": 35.0
          },
          {
            "t": 1200,
            "value": 80.0
          }
        ],
        "sf_status": true
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    }
  ]
}