VAV-0001 — VAV minimum airflow setpoint too high
| Status | verified — engine e2ff2f8, cxf:fnv1a128:e251b00bd4acce37ca4d81521c086a37, 2026-08-17 |
| Severity | 3 |
| Method | rule |
| Phase | 2 |
| Category | EXCESS_CONSUMPTION |
| Confidence | HIGH |
| Estimation | PROXY_ESTIMATION |
| G36 | — |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | AHU-0019, VAV-0003, VAV-0006 |
| Playbooks | vav-min-flow-reheat |
| Source | HVAC FDD Reference v1.0 §10, VAV-0001; PNNL-25985 EEM-15 (minimum VAV flow reductions); PNNL retuning |
| Operating states | heating and deadband (host-gated) |
Preconditions (host-enforced): The AHU serving this VAV box is running and the zone is in occupied mode. Both are host-enforced: an unoccupied box sitting at a high programmed minimum costs nothing while no air is moving, and the ventilation requirement this rule compares against is an occupied-mode quantity. The host must also confirm that zone_airflow_sp_min carries the box’s configured minimum in L/s — the number the damper loop clamps to, not the active setpoint and not a design value in CFM — and that ventilation_requirement has been set for this specific zone (see Deviations; the shipped default is a placeholder from the reference’s own test vectors, not a site value).
Points: zone_airflow_sp_min, rht_vlv_cmd
Outputs:
yFault— True while the programmed minimum exceeds the scaled ventilation requirement with reheat active, continuously for at least alarm_delay
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
ventilation_requirement | 70.0 | L/s | ventReq.k | The zone’s design minimum outdoor-air requirement per ASHRAE 62.1. PER-ZONE SITE CONFIGURATION — the reference gives no default and the shipped 70.0 is a placeholder; a host that leaves it unset is comparing against an arbitrary number. |
min_flow_multiplier | 1.5 | 1 | scaled.k | How far above the ventilation requirement a programmed minimum may sit before it counts as oversized |
reheat_active_threshold | 10.0 | % | rhtOn.t | Reheat valve command above which the coil counts as actively heating |
alarm_delay | 3600.0 | s | persist.delayTime | Continuous fault persistence required before the alarm asserts (60 min) |
Description
The box is configured never to deliver less than some floor of airflow, and that floor sits far above what the zone needs for ventilation. Every hour the zone is not calling for cooling, the box pushes cold supply air it does not need into the space and the reheat coil pays to warm it back up — the fan on one side, the boiler on the other, with the zone comfortable throughout, so nothing about the symptom points at the cause. The fault lives in a number a commissioning technician typed rather than in any equipment behavior, which is why the second term matters: an oversized minimum only costs money while the reheat coil is running against it. The setpoint test finds the defect, the reheat test proves it is being paid for now. The economics are multiplicative — the same default-minimum habit repeats across every box in a building — and PNNL-25985 ranks minimum VAV flow reduction (EEM-15) as the highest-impact individual retuning measure for offices, at 5–16% of site energy.
Detection Logic
sp_high = zone_airflow_sp_min > (ventilation_requirement × min_flow_multiplier)
rht_on = rht_vlv_cmd > reheat_active_threshold
yFault = (sp_high AND rht_on) sustained continuously for alarm_delay
Block graph (rule.cxf.jsonld):
At the shipped defaults the trip point is 70 × 1.5 = 105 L/s (about 222 cfm), so
a box configured at the reference’s 235 L/s is flagged and one at 95 L/s is not
— but the 70 L/s is a placeholder, and the rule is only as meaningful as the
per-zone ventilation_requirement a host binds in its place (see Deviations).
Both comparisons are strict, as the reference writes them: a box configured at
exactly the allowance and a valve reported at exactly 10% both clear. The
60-minute persist delay matters more here than the block count suggests.
Neither input is noisy — a configured minimum does not jitter — so the delay is
not filtering measurement noise; it is filtering morning warm-up, when a box
legitimately runs reheat at whatever minimum it has while the space recovers
from setback. An hour of continuous reheat against an oversized minimum is no
longer warm-up. delayOnInit = true holds that window across a restart.
Possible Diagnoses
- Minimum flow setpoint set too high during commissioning — the common case, usually a default the technician never revisited
- Minimum flow reset logic disabled: the box supports a dynamic minimum (dual-maximum, or a ventilation reset driven by occupancy) and it was switched off or never enabled
- Code-required minimum genuinely higher than necessary — overdesign in the original ventilation calculation, a design-review item rather than a BAS fix
Energy Impact
EXCESS_CONSUMPTION, HIGH confidence, PROXY_ESTIMATION. Two subsystems pay at
once: the reheat coil warms air the zone never needed and the supply fan moves
it. The reference’s 10–20% excess reheat energy is the per-box figure;
PNNL-25985’s EEM-15 gives the building-scale one at 5–16% of site energy, and
for a 50,000 ft² office at $2/ft² the playbook puts annual recovery at
$5,000–16,000. Estimation is PROXY because the rule sees a setpoint and a valve
command, not thermal flow: the waste share
(zone_airflow_sp_min − ventilation_requirement) / zone_airflow_sp_min leans
entirely on ventilation_requirement being right. Heating-dominant, though the
fan share runs whenever the box does.
Emissions Impact
Scope 1 + 2, PROXY_EMISSIONS, HIGH confidence; typically 200–1,500 kg CO₂e/yr per zone. The split follows the two subsystems — gas at the boiler serving the reheat coil is scope 1, fan electricity is scope 2 — and hydronic reheat off an electric boiler or heat pump moves the whole thing into scope 2. Avoided-emissions basis: marginal operating emissions rate (MOER).
Deviations
ventilation_requirementships with a placeholder default. The reference gives it as “Config” — no number, because the value comes from that zone’s ASHRAE 62.1 calculation. This card ships 70.0 L/s (the reference’s own test value, roughly an 1,800 ft² office at 8 occupants) so the document is runnable as delivered. It is not a site value; hosts MUST setventReq.kper box. Set it 2× too high and the rule never fires on a genuinely oversized box, 2× too low and every box in the building alarms. It is the only parameter in the rule whose default carries no authority.reheat_active_thresholdis adopted, not transcribed. It appears in the reference’s equation but not its tunables table. This card adopts 10.0%, the value the same chapter gives VAV-0006’s identically-named parameter, which asks the same question of the same point. Sites whose valve commands park at a nonzero rest position should retune above that position.zone_airflowis dropped from the points list. The reference’s points table lists it but its equation never uses it — the test compares a configured setpoint against a ventilation requirement. It is verification context, not a rule input, and carrying it would force every host to bind a point the graph ignores. Precedent: AHU-0029 dropsoatfor the same reason.- A configuration value is consumed as a live point. Hosts whose BAS does
not expose the configured minimum as readable cannot run this rule from a
trend archive alone; they need a config export bound as a point
(
Min_Air_Flow_Setpoint_Limitin the dictionary). Binding the active airflow setpoint instead breaks the rule — it rises above the minimum whenever the zone calls for cooling, producing alarms during normal cooling. AlarmDelay = 60 minbecomespersist.delayTime = 3600 swithdelayOnInit = true(Modelica/CDL default isfalse), the library’s standing choice: a box already faulted at load waits out the full hour rather than alarming on the first tick after a restart.- Operating states (heating, deadband) and preconditions (AHU running, zone occupied) are declared in frontmatter for host enforcement rather than encoded in the block graph, per the library’s design stance.
- Frontmatter
clustersis empty. CLU-05 covers this fault’s neighbours (VAV-0003, VAV-0006, SYS-0007) but chapter 7 does not list VAV-0001 among its members, and this card does not edit the cluster definition. The relationship is carried by the shared playbook and byrelated; worth revisiting when the cluster set is next reviewed, since an oversized minimum is diagnosis 1 for VAV-0006. g36: null. This is a research-backed 050-range rule; G36’s own guidance on VAV minimums (20% of design airflow or the ventilation minimum, whichever is greater) informs the playbook’s remediation but is not the source of the detection logic.
Notes
Check the air handler before touching individual boxes: if this rule fires on more than half the boxes on one AHU, the boxes are probably fine and the supply air is too cold (AHU-0019). PNNL-27338’s AIRCx check is the sharper version — more than 25% of zones with reheat valves above 50% means look at the SAT reset first.
The remote fix is the vav-min-flow-reheat playbook’s step 2.1: reduce the minimum to the calculated ventilation requirement, reviewing the heating maximum on dual-maximum boxes at the same time. Clearing this fault should also quiet VAV-0006 on the same box, since an oversized minimum is its diagnosis 1; it will not quiet VAV-0003, which sees a valve open with the zone already satisfied.
Test Vectors
9 scenarios, clock step 300 s over 9000 s.
| Scenario | Description |
|---|---|
normal_minimum | Reference vector: minimum airflow setpoint 95 L/s against a 70 L/s ventilation requirement, reheat valve at 40%. 95 sits below the 105 L/s trip point (70 × 1.5), so the reheat is heating air the zone is required to receive and nothing fires. |
excessive_minimum | Reference vector: minimum airflow setpoint 235 L/s — 3.4× the ventilation requirement — with the reheat valve at 40%. The box is pushing cold air the zone does not need and paying the boiler to warm it back up. Both terms hold from t=0, so delayOnInit makes the alarm land one alarm_delay in. |
high_min_no_reheat | Reference vector: the same oversized 235 L/s minimum with the reheat valve shut. The setpoint is still wrong, but no heat is being spent against it — this rule reports live energy waste, not a configuration audit, so it stays silent. |
min_sp_exactly_at_multiple | Boundary: the minimum setpoint sits exactly on ventilation_requirement × min_flow_multiplier (70.0 × 1.5 = 105.0) with reheat well open. The comparison is strict >, so a box configured precisely at the allowance clears. |
min_sp_just_above_multiple | Boundary from the other side: 105.1 L/s, one tenth of a litre per second over the allowance, with the same reheat. The setpoint test now passes and the alarm lands after alarm_delay. |
reheat_exactly_at_threshold | Boundary on the reheat term: an oversized minimum with the valve reported at exactly reheat_active_threshold (10.0%). Strict > again, so a valve parked on the threshold — typical of a leak-by allowance or a quantized command — does not count as active. |
reheat_just_above_threshold | Boundary from the other side: the same oversized minimum with the valve at 10.1%. Both terms hold and the alarm lands after alarm_delay. |
transient_reheat_pulse | The oversized minimum is present all along, but reheat only runs for the first 3000 s — a morning warm-up burst that ends 600 s short of the 3600 s alarm_delay. The timer resets when the valve shuts and no alarm is ever raised. |
alarm_clears_after_minimum_lowered | Recovery: the alarm asserts at 3600 s, then the technician retunes the box minimum from 235 L/s down to 100 L/s at t=5400 s (playbook step 2.1). yFault drops on that same tick — TrueDelay only delays the rising edge — even though the reheat valve is still modulating. |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 300,
"horizon_s": 9000
},
"scenarios": [
{
"name": "normal_minimum",
"description": "Reference vector: minimum airflow setpoint 95 L/s against a 70 L/s ventilation requirement, reheat valve at 40%. 95 sits below the 105 L/s trip point (70 \u00d7 1.5), so the reheat is heating air the zone is required to receive and nothing fires.",
"inputs": {
"zone_airflow_sp_min": 95.0,
"rht_vlv_cmd": 40.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 9000,
"equals": false
}
]
},
{
"name": "excessive_minimum",
"description": "Reference vector: minimum airflow setpoint 235 L/s \u2014 3.4\u00d7 the ventilation requirement \u2014 with the reheat valve at 40%. The box is pushing cold air the zone does not need and paying the boiler to warm it back up. Both terms hold from t=0, so delayOnInit makes the alarm land one alarm_delay in.",
"inputs": {
"zone_airflow_sp_min": 235.0,
"rht_vlv_cmd": 40.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 3300,
"equals": false
},
{
"output": "yFault",
"from_s": 3900,
"to_s": 9000,
"equals": true
}
]
},
{
"name": "high_min_no_reheat",
"description": "Reference vector: the same oversized 235 L/s minimum with the reheat valve shut. The setpoint is still wrong, but no heat is being spent against it \u2014 this rule reports live energy waste, not a configuration audit, so it stays silent.",
"inputs": {
"zone_airflow_sp_min": 235.0,
"rht_vlv_cmd": 0.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 9000,
"equals": false
}
]
},
{
"name": "min_sp_exactly_at_multiple",
"description": "Boundary: the minimum setpoint sits exactly on ventilation_requirement \u00d7 min_flow_multiplier (70.0 \u00d7 1.5 = 105.0) with reheat well open. The comparison is strict `>`, so a box configured precisely at the allowance clears.",
"inputs": {
"zone_airflow_sp_min": 105.0,
"rht_vlv_cmd": 40.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 9000,
"equals": false
}
]
},
{
"name": "min_sp_just_above_multiple",
"description": "Boundary from the other side: 105.1 L/s, one tenth of a litre per second over the allowance, with the same reheat. The setpoint test now passes and the alarm lands after alarm_delay.",
"inputs": {
"zone_airflow_sp_min": 105.1,
"rht_vlv_cmd": 40.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 3300,
"equals": false
},
{
"output": "yFault",
"from_s": 3900,
"to_s": 9000,
"equals": true
}
]
},
{
"name": "reheat_exactly_at_threshold",
"description": "Boundary on the reheat term: an oversized minimum with the valve reported at exactly reheat_active_threshold (10.0%). Strict `>` again, so a valve parked on the threshold \u2014 typical of a leak-by allowance or a quantized command \u2014 does not count as active.",
"inputs": {
"zone_airflow_sp_min": 235.0,
"rht_vlv_cmd": 10.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 9000,
"equals": false
}
]
},
{
"name": "reheat_just_above_threshold",
"description": "Boundary from the other side: the same oversized minimum with the valve at 10.1%. Both terms hold and the alarm lands after alarm_delay.",
"inputs": {
"zone_airflow_sp_min": 235.0,
"rht_vlv_cmd": 10.1
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 3300,
"equals": false
},
{
"output": "yFault",
"from_s": 3900,
"to_s": 9000,
"equals": true
}
]
},
{
"name": "transient_reheat_pulse",
"description": "The oversized minimum is present all along, but reheat only runs for the first 3000 s \u2014 a morning warm-up burst that ends 600 s short of the 3600 s alarm_delay. The timer resets when the valve shuts and no alarm is ever raised.",
"inputs": {
"zone_airflow_sp_min": 235.0,
"rht_vlv_cmd": [
{
"t": 0,
"value": 40.0
},
{
"t": 3000,
"value": 0.0
}
]
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 9000,
"equals": false
}
]
},
{
"name": "alarm_clears_after_minimum_lowered",
"description": "Recovery: the alarm asserts at 3600 s, then the technician retunes the box minimum from 235 L/s down to 100 L/s at t=5400 s (playbook step 2.1). yFault drops on that same tick \u2014 TrueDelay only delays the rising edge \u2014 even though the reheat valve is still modulating.",
"inputs": {
"zone_airflow_sp_min": [
{
"t": 0,
"value": 235.0
},
{
"t": 5400,
"value": 100.0
}
],
"rht_vlv_cmd": 40.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 3300,
"equals": false
},
{
"output": "yFault",
"from_s": 3900,
"to_s": 5100,
"equals": true
},
{
"output": "yFault",
"from_s": 5700,
"to_s": 9000,
"equals": false
}
]
}
]
}