AHU-0001 — Duct static pressure too low at full fan speed
| Status | verified — engine e2ff2f8, cxf:fnv1a128:e621fd6c71d0e16c2eefa5e05edad265, 2026-08-17 |
| Severity | 3 |
| Method | rule |
| Phase | 1 |
| Category | PROTECTIVE |
| Confidence | LOW |
| Estimation | QUALITATIVE_ONLY |
| G36 | §5.16.14 FC#1 |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | AHU-0024, AHU-0031, FPB-0002 |
| Playbooks | — |
| Source | HVAC FDD Reference v1.0 §9, AHU-0001; G36 §5.16.14 FC#1 |
| Operating states | OS 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.
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:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
dsp_error_threshold | 25.0 | Pa | gapBig.t | Shortfall below the duct static pressure setpoint that counts as a real deficit rather than control error (0.1 inWC) |
speed_full_threshold | 99.0 | % | spdFull.t | Supply fan speed above which the fan is treated as having no reserve left |
alarm_delay | 1800.0 | s | persist.delayTime | Continuous 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):
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
- Ductwork obstruction or collapse — a closed fire/smoke damper, a dropped internal liner, or debris left after a renovation
- Fan belt slipping or broken
- Fan motor or VFD fault — a drive derating on a thermal or current limit reports full speed command while delivering less
- Excessive duct leakage — a disconnected branch or a failed flex connection downstream of the sensor
- 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 — retunespeed_full_thresholdto 94.0 for the G36 form. dsp < dsp_sp − eps_dsprewritten in gap form. Subtracting first and testingdsp_sp − dsp > eps_dspkeepseps_dspthe positive number the reference publishes, retunable at one CXF path, instead of negating it into anAddParameter. Same rearrangement as AHU-0028; the one-ulp difference at the threshold is not observable at 1 Pa sensor resolution.sf_speed >= 99%→ strict>. CDLRealsoffers 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 retunespeed_full_thresholdto 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 = ONin 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
PROTECTIVEfault in the library, and the first with no emissions scope.emissions.scopeis 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 isfalse), 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.
| Scenario | Description |
|---|---|
normal_part_speed_operation | Reference 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_setpoint | Reference 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_speed | Reference 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_threshold | Threshold 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_threshold | Threshold 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_threshold | Threshold 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_threshold | Threshold edge, other side: 99.5% clears the strict comparison, so the same 75 Pa deficit alarms after alarm_delay |
startup_ramp_clears_before_delay | 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 — a morning start-up is shorter than alarm_delay, so nothing alarms |
fault_clears_on_recovery | 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 — TrueDelay has no off-delay |
speed_backs_off_before_delay | Transient: 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
}
]
}
]
}