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-0001 — Duct static pressure too low at full fan speed

Statusverified — engine e2ff2f8, cxf:fnv1a128:e621fd6c71d0e16c2eefa5e05edad265, 2026-08-17
Severity3
Methodrule
Phase1
CategoryPROTECTIVE
ConfidenceLOW
EstimationQUALITATIVE_ONLY
G36§5.16.14 FC#1
Clusters
Suppresses
Suppressed by
RelatedAHU-0024, AHU-0031, FPB-0002
Playbooks
SourceHVAC FDD Reference v1.0 §9, AHU-0001; G36 §5.16.14 FC#1
Operating statesOS 1–5 (all)

Preconditions (host-enforced): Supply fan running — with the fan off both the pressure reading and the speed feedback are meaningless. The unit must be a multi-zone VAV AHU under duct static pressure control; a constant-volume unit has no dsp_sp to compare against. The speed feedback must be a real VFD readback rather than the commanded speed echoed back, since a defeated or bypassed drive reports 100% while the fan turns at line speed or not at all. When any gate is unmet the verdict is NO_EVAL, not healthy.

Points: dsp, dsp_sp, sf_speed

Outputs:

  • yFault — True while duct static pressure has stayed more than dsp_error_threshold below its setpoint with the fan above speed_full_threshold, for at least alarm_delay

Parameters:

NameDefaultUnitCXF pathDescription
dsp_error_threshold25.0PagapBig.tShortfall below the duct static pressure setpoint that counts as a real deficit rather than control error (0.1 inWC)
speed_full_threshold99.0%spdFull.tSupply fan speed above which the fan is treated as having no reserve left
alarm_delay1800.0spersist.delayTimeContinuous fault persistence required before the alarm asserts (30 min)

Description

The fan is at the stop and the duct is still short of pressure. VAV boxes throttle against that pressure to hold zone airflow; without it they run wide open and still starve. The control loop has already asked for everything and the measurement has not moved, so the deficit is mechanical rather than a tuning problem — a belt slipping on its sheaves, a fire/smoke damper that closed and never reopened, a filter bank past its change-out point, or a VFD that has silently derated all read the same way from the outside. This is a protective fault: it catches the mechanical failure early and explains complaints that would otherwise be chased zone by zone. The energy story is real but indirect — a fan pinned at 100% against a leaking or obstructed duct burns full fan power to deliver less than design air.

Detection Logic

press_gap = dsp_sp − dsp
yFault    = (press_gap > dsp_error_threshold)      pressure short of setpoint by more than tolerance
        AND (sf_speed  > speed_full_threshold)     fan has no reserve left
            sustained continuously for alarm_delay

Block graph (rule.cxf.jsonld):

AHU-0001 block graph

The gap form is the reference’s dsp < dsp_sp − eps_dsp rearranged so one positive number carries the tolerance (see Deviations). The speed conjunct is what makes the rule mean anything: pressure below setpoint at part speed is an ordinary control loop working, and only a loop that has run out of fan is evidence of a defect. Both comparisons are strict, so a deficit sitting exactly on 25 Pa and a speed feedback parked exactly on 99.0% both read healthy. persist requires 30 minutes of continuous violation — enough to separate a mechanical fault from a morning start-up, a damper stroke, or the pressure dip after a bank of boxes opens at once — and any interruption restarts the timer.

Possible Diagnoses

  1. Ductwork obstruction or collapse — a closed fire/smoke damper, a dropped internal liner, or debris left after a renovation
  2. Fan belt slipping or broken
  3. Fan motor or VFD fault — a drive derating on a thermal or current limit reports full speed command while delivering less
  4. Excessive duct leakage — a disconnected branch or a failed flex connection downstream of the sensor
  5. DSP sensor fault — a plugged or disconnected sensing tube reads low with a perfectly healthy duct behind it

Energy Impact

PROTECTIVE, LOW confidence, QUALITATIVE_ONLY. No waste term is computable and none is published: the alarm is about equipment damage and undeliverable airflow, not kilowatt-hours. What energy there is depends on the cause — duct leakage means the fan produces air that never reaches a zone, an obstruction means the fan spends pressure across the blockage, and a slipping belt or derated drive costs comfort and a repair bill instead. No PNNL measure covers this fault, hence LOW confidence and no range. Climate-neutral. If a leakage or obstruction cause is confirmed, size the recovered fan energy per Energy Impact Reference §4.4 from fan hours and the pressure spent against the defect.

Emissions Impact

QUALITATIVE_EMISSIONS, LOW confidence; negligible direct emissions. A fan already at 100% draws what it draws whether or not the duct is short of pressure, so detecting this fault avoids no emissions on its own. Scope is N/A — there is no emitting stream to attribute, the general shape of a PROTECTIVE fault. Any credit belongs to the repair that follows and to the DSP reset it enables, which is AHU-0031’s to claim. Avoided-emissions basis: N/A.

Deviations

  • The fan-speed conjunct uses the reference’s bare sf_speed ≥ 99%. G36-2018 §5.16.14 FC#1 subtracts a VFD speed error allowance (> 99% − eps_VFDSPD, 94% at the Table 5.16.14.5 default); the reference ch.9 card simplifies to a bare 99% and this card follows its primary source. Drives that plateau just under 99% read NO-fault under the shipped default — retune speed_full_threshold to 94.0 for the G36 form.
  • dsp < dsp_sp − eps_dsp rewritten in gap form. Subtracting first and testing dsp_sp − dsp > eps_dsp keeps eps_dsp the positive number the reference publishes, retunable at one CXF path, instead of negating it into an AddParameter. Same rearrangement as AHU-0028; the one-ulp difference at the threshold is not observable at 1 Pa sensor resolution.
  • sf_speed >= 99% → strict >. CDL Reals offers only strict comparisons, so a feedback parked at exactly 99.000% reads as not-at-full- speed and the rule stays silent. A host binding a coarsely quantized speed point (integer percent) should retune speed_full_threshold to 98.9 rather than rely on the drive overshooting.
  • The fan-running condition is a precondition, not a wire. The reference lists it under Preconditions rather than in its Logic row, so it stays in frontmatter for the host. This is the opposite choice from AHU-0031, where the reference puts sf_status = ON in the logic and the graph carries it.
  • Operating states OS 1–5 are declared, not gated. The reference marks the fault applicable in every operating state, so the frontmatter records applicability and the graph carries no state logic.
  • First PROTECTIVE fault in the library, and the first with no emissions scope. emissions.scope is the literal string "N/A"; the schema types the field as free text, so the convention is set here.
  • persist.delayOnInit = true (Modelica/CDL default is false), the library’s standing choice: a deficit already present at load waits out the full 30 minutes instead of alarming on the first tick after a controller restart.

Notes

Rule out diagnosis 5 first: a plugged sensing tube reads low forever and the fan chases it to 100%, which is the same trace as a collapsed duct. Two tells separate them — a genuine deficit moves when zone demand moves, and a healthy duct with a bad sensor still delivers zone airflow. This fault is the low-side mirror of AHU-0031 (excessive static pressure): same two signals, opposite failure, and they cannot be true at once. If the DSP setpoint has never been reset from its design value — the case AHU-0024 detects — a fan at 100% against an unrealistic setpoint is a tuning artifact, not a broken belt.

Test Vectors

10 scenarios, clock step 300 s over 5400 s.

ScenarioDescription
normal_part_speed_operationReference vector: 350 Pa against a 375 Pa setpoint with the fan at 60% — the fan still has headroom, and the 25 Pa gap does not clear the strict threshold either; nothing to report
at_full_speed_holding_setpointReference vector: 370 Pa against a 375 Pa setpoint with the fan at 100% — the fan is maxed out but the duct is only 5 Pa short, well inside dsp_error_threshold, so the system is simply sized close to its design point
underpressure_at_full_speedReference vector: 300 Pa against a 375 Pa setpoint with the fan at 100% — a 75 Pa deficit with no speed left to give; the alarm asserts one alarm_delay (1800 s) after the condition appears
gap_exactly_at_thresholdThreshold edge: dsp_sp − dsp = 25.0 Pa exactly with the fan at 100% — gapBig is a strict >, so a deficit sitting precisely on dsp_error_threshold reads healthy
gap_just_over_thresholdThreshold edge, other side: dsp_sp − dsp = 25.1 Pa with the fan at 100% clears the strict comparison and alarms after alarm_delay
speed_exactly_at_thresholdThreshold edge: the fan sits exactly on speed_full_threshold (99.0%) with a 75 Pa deficit — spdFull is a strict >, so a feedback parked on 99.000% does not count as full speed and the rule stays silent
speed_just_over_thresholdThreshold edge, other side: 99.5% clears the strict comparison, so the same 75 Pa deficit alarms after alarm_delay
startup_ramp_clears_before_delayTransient: the fan pins at 100% and runs 75 Pa short while the system comes up, then pressure recovers to 370 Pa at t=1200 — a morning start-up is shorter than alarm_delay, so nothing alarms
fault_clears_on_recoveryA sustained deficit alarms at 1800 s; a closed fire/smoke damper reopens at t=3000, pressure recovers to 370 Pa, and the alarm drops on that tick — TrueDelay has no off-delay
speed_backs_off_before_delayTransient: the deficit holds but the fan drops to 92% at t=1200 as zone demand falls — the rule needs both halves at once, so persistence restarts and no alarm follows
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 300,
    "horizon_s": 5400
  },
  "scenarios": [
    {
      "name": "normal_part_speed_operation",
      "description": "Reference vector: 350 Pa against a 375 Pa setpoint with the fan at 60% \u2014 the fan still has headroom, and the 25 Pa gap does not clear the strict threshold either; nothing to report",
      "inputs": {
        "dsp": 350.0,
        "dsp_sp": 375.0,
        "sf_speed": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "at_full_speed_holding_setpoint",
      "description": "Reference vector: 370 Pa against a 375 Pa setpoint with the fan at 100% \u2014 the fan is maxed out but the duct is only 5 Pa short, well inside dsp_error_threshold, so the system is simply sized close to its design point",
      "inputs": {
        "dsp": 370.0,
        "dsp_sp": 375.0,
        "sf_speed": 100.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "underpressure_at_full_speed",
      "description": "Reference vector: 300 Pa against a 375 Pa setpoint with the fan at 100% \u2014 a 75 Pa deficit with no speed left to give; the alarm asserts one alarm_delay (1800 s) after the condition appears",
      "inputs": {
        "dsp": 300.0,
        "dsp_sp": 375.0,
        "sf_speed": 100.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 5400,
          "equals": true
        }
      ]
    },
    {
      "name": "gap_exactly_at_threshold",
      "description": "Threshold edge: dsp_sp \u2212 dsp = 25.0 Pa exactly with the fan at 100% \u2014 gapBig is a strict `>`, so a deficit sitting precisely on dsp_error_threshold reads healthy",
      "inputs": {
        "dsp": 350.0,
        "dsp_sp": 375.0,
        "sf_speed": 100.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "gap_just_over_threshold",
      "description": "Threshold edge, other side: dsp_sp \u2212 dsp = 25.1 Pa with the fan at 100% clears the strict comparison and alarms after alarm_delay",
      "inputs": {
        "dsp": 349.9,
        "dsp_sp": 375.0,
        "sf_speed": 100.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 5400,
          "equals": true
        }
      ]
    },
    {
      "name": "speed_exactly_at_threshold",
      "description": "Threshold edge: the fan sits exactly on speed_full_threshold (99.0%) with a 75 Pa deficit \u2014 spdFull is a strict `>`, so a feedback parked on 99.000% does not count as full speed and the rule stays silent",
      "inputs": {
        "dsp": 300.0,
        "dsp_sp": 375.0,
        "sf_speed": 99.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "speed_just_over_threshold",
      "description": "Threshold edge, other side: 99.5% clears the strict comparison, so the same 75 Pa deficit alarms after alarm_delay",
      "inputs": {
        "dsp": 300.0,
        "dsp_sp": 375.0,
        "sf_speed": 99.5
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 5400,
          "equals": true
        }
      ]
    },
    {
      "name": "startup_ramp_clears_before_delay",
      "description": "Transient: the fan pins at 100% and runs 75 Pa short while the system comes up, then pressure recovers to 370 Pa at t=1200 \u2014 a morning start-up is shorter than alarm_delay, so nothing alarms",
      "inputs": {
        "dsp": [
          {
            "t": 0,
            "value": 300.0
          },
          {
            "t": 1200,
            "value": 370.0
          }
        ],
        "dsp_sp": 375.0,
        "sf_speed": 100.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "fault_clears_on_recovery",
      "description": "A sustained deficit alarms at 1800 s; a closed fire/smoke damper reopens at t=3000, pressure recovers to 370 Pa, and the alarm drops on that tick \u2014 TrueDelay has no off-delay",
      "inputs": {
        "dsp": [
          {
            "t": 0,
            "value": 300.0
          },
          {
            "t": 3000,
            "value": 370.0
          }
        ],
        "dsp_sp": 375.0,
        "sf_speed": 100.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1500,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 2100,
          "to_s": 2700,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 3300,
          "to_s": 5400,
          "equals": false
        }
      ]
    },
    {
      "name": "speed_backs_off_before_delay",
      "description": "Transient: the deficit holds but the fan drops to 92% at t=1200 as zone demand falls \u2014 the rule needs both halves at once, so persistence restarts and no alarm follows",
      "inputs": {
        "dsp": 300.0,
        "dsp_sp": 375.0,
        "sf_speed": [
          {
            "t": 0,
            "value": 100.0
          },
          {
            "t": 1200,
            "value": 92.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 5400,
          "equals": false
        }
      ]
    }
  ]
}