AHU-0010 — OAT ≠ MAT in mechanical + economizer cooling
| Status | verified — engine e2ff2f8, cxf:fnv1a128:5eecdfa05c333292682916fecbc5d7f1, 2026-08-17 |
| Severity | 3 |
| Method | rule |
| Phase | 1 |
| Category | COMFORT_ENERGY |
| Confidence | LOW |
| Estimation | QUALITATIVE_ONLY |
| G36 | §5.16.14 FC#10 |
| Clusters | — |
| Suppresses | — |
| Suppressed by | AHU-0028 |
| Related | AHU-0002, AHU-0003, AHU-0008, AHU-0021, AHU-0028 |
| Playbooks | sensor-drift |
| Source | HVAC FDD Reference v1.0 §5.8.1 (index; card abbreviated); G36 §5.16.14 FC#10 (text per Addendum u public review); NISTIR 7365 (defaults provenance); PNNL-25985 (EEM-01, sensor recalibration) |
| Operating states | OS#3 (mechanical + economizer cooling, damper at 100%) — host-gated |
Preconditions (host-enforced): The unit must have a MAT sensor; G36 omits FC#10 where there is none. Evaluate only in OS#3, which G36 Table 5.16.14.2 defines by actuator signature: heating coil = 0, cooling coil > 0, OA damper at 100%. At any smaller damper position return air is supposed to be in the mixture and the equation is simply false. Supply fan running, since neither temperature describes a stream that is not moving. Per §5.16.14.11 the host suspends evaluation while the AHU is off and for ModeDelay (30 min) after a mode change in any zone group the AHU serves — a damper still stroking toward 100% leaves return air in the plenum that no fault caused. Silence this rule while AHU-0028 is active: MAT is an input here and FC-062 is its integrity gate. When any gate is unmet the verdict is NO_EVAL, not healthy.
Outputs:
yFault— True while |mat − oat| has stayed above combined_error for at least alarm_delay
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
combined_error | 3.1623 | °C | devBig.t | Largest disagreement between mixed and outdoor air that sensor accuracy alone can explain, composed as the root-sum-square of the two error bands: sqrt(eMAT² + eOAT²) = sqrt(3² + 1²) = sqrt(10) = 3.1623 °C with the G36 Table 5.16.14.5 defaults. Errors add in quadrature rather than linearly because two independent sensors are unlikely to be wrong in the same direction at once. The eOAT default is location-dependent — 1 °C for a sensor at the unit, 3 °C for a shared or global one — so a site reading campus or weather-service outdoor air recomputes sqrt(9 + 9) = 4.2426 °C. |
alarm_delay | 1800.0 | s | persist.delayTime | Continuous fault persistence required before the alarm asserts (G36 AlarmDelay, 30 min) |
Description
In OS#3 the unit has opened the outdoor air damper to 100% and closed the return damper, and is running mechanical cooling on top. Mixed air is then not a mixture at all — it is outdoor air, one plenum downstream — so the two sensors are measuring the same stream at two points and should agree to within their combined accuracy.
When they do not, either a sensor is lying or the return damper is not where the command says it is. The second case is the expensive one: return air in a building that needs cooling is warmer than the outdoor air the economizer just chose, so every degree of leak-through arrives at the coil as load the unit did not need to buy, and the damper feedback will not show it. This is the outdoor-air-side counterpart to AHU-0008, which audits the supply and mixed-air sensors in free cooling; between them the two G36 equality tests cover most of the unit’s temperature instrumentation using only operating states the sequence already visits.
Detection Logic
G36 §5.16.14 FC#10, applies to OS#3 (omitted if the unit has no MAT sensor):
| MAT_AVG − OAT_AVG | > sqrt(eMAT² + eOAT²)
with the Table 5.16.14.5 defaults and a local OAT sensor:
| mat − oat | > sqrt(3² + 1²) = 3.1623 °C
yFault = (|mat − oat| > combined_error), sustained for alarm_delay
Block graph (rule.cxf.jsonld):
There is no fan-heat term, and its absence is physics rather than omission: both sensors sit upstream of the supply fan, so whatever the fan adds is added after the comparison and cancels out of it. AHU-0008 straddles the fan and must correct for it.
The sign of dev carries the diagnosis even though the rule discards it.
Positive — mixed air warmer than outdoor — is the leaking return damper in
cooling weather. Negative is a sensor story: no amount of recirculated building
air pulls a mixture below the outdoor stream when the building is warmer than
outdoors. A host that wants the direction keeps the two temperatures alongside
the verdict.
G36’s comparison is already strict, so GreaterThreshold reproduces it exactly
and a deviation of exactly 3.1623 °C reads healthy in both. persist requires
30 minutes of continuous violation and any interruption restarts the timer.
Possible Diagnoses
Per G36 §5.16.14 Table 5.16.14.8, FC#10:
- MAT sensor error — out of calibration, or reading a stratified slice of the plenum rather than the stream (AHU-0028 catches the gross version)
- OAT sensor error — cheapest to rule out, and usually placement: a sensor on a sun-struck wall or above a condenser reads high all afternoon
- Leaking or stuck economizer damper or actuator — the return damper is not sealing, or the outdoor damper never reached the 100% it reports
Energy Impact
COMFORT_ENERGY, LOW confidence, QUALITATIVE_ONLY — the grades in the reference’s §5.8.1 index row, which maps the fault to PNNL-25985 EEM-01 (sensor recalibration) and publishes savings as sensor-dependent. Two temperatures and no airflow give no power term, and the difference is ambiguous between a lying sensor and a leaking damper. On the damper diagnosis the waste is direct and continuous: recirculated return air arrives at the coil warmer than the outdoor air the economizer selected, and the chiller pays for as long as the leak lasts. On the sensor diagnoses the cost is indirect — the same bad reading feeds AHU-0021, AHU-0030, and the changeover logic. Cooling-dominant, since OS#3 exists only when the unit is making cold air.
Emissions Impact
QUALITATIVE_EMISSIONS, LOW confidence; the block is library-assigned, as the
§5.8.1 index carries no emissions column. Scope 2: every path out of this fault
lands on the cooling plant — chiller or DX work spent on recirculated air, and
fan energy moving it. No on-site combustion is involved, since OS#3 has the
heating coil commanded shut and a heating valve leaking in this state reports
as FC#15. That is why this card is narrower than AHU-0008’s 1|2, which
cannot tell which coil is leaking. Avoided-emissions basis: N/A.
Deviations
- The reference card is an index row, so this card is built from G36. §5.8.1 gives the code, the name, and the energy grades, with no equation, vectors, or severity. The equation, OS#3 applicability, three diagnoses, and internal-variable defaults are transcribed from ASHRAE Guideline 36 §5.16.14 as it appears in Addendum u to Guideline 36-2018 (first public review, 2021).
- Severity 3 is library-assigned. The index has no severity column, and G36’s Level 3 alarm grading (§5.16.14.16) is a reporting priority rather than a ranking. The value matches every other G36 001-range card here.
- Energy profile follows the §5.8.1 index row (COMFORT_ENERGY / LOW / QUAL, EEM-01, savings “sensor-dependent”); the emissions block is library-assigned.
- Root-sum-square threshold shipped as one number. G36 writes the bound as
sqrt(eMAT² + eOAT²); the graph carries the evaluated 3.1623 °C indevBig.t, so a host retunes one parameter and no square root runs at runtime. The composition is spelled out in the parameter description because the arithmetic is not linear. - The default assumes a local OAT sensor. G36 gives eOAT as 1 °C at the
unit and 3 °C for a global one. A site feeding this rule from a campus sensor
or weather service must set
combined_errorto sqrt(9 + 9) = 4.2426 °C; leaving it at the local value makes the rule fire on disagreement G36 considers within tolerance. The two G36 equality tests do not retune together — AHU-0008’s threshold does not move at all, since it contains no OAT term. - No boundary deviation for this fault. FC#10’s comparison is already
strict (
>), unlike the≥/≤forms elsewhere in Table 5.16.14.8 (FC#5, FC#12, FC#14, FC#15), so no measure-zero rewrite is involved. Same finding as AHU-0009 and AHU-0011. - Instantaneous samples instead of averaged signals. G36 compares 5-minute
rolling averages sampled at 1-minute intervals; this rule compares raw
samples and leans on the 30-minute
persistdelay. Not equivalent — persistence resets on every compliant tick, so an oscillating difference can hide indefinitely, while a drifted sensor and a leaking damper are steady offsets and read the same either way. (Honesty note carried from AHU-0002.) - Suppression is declared, not encoded. AHU-0028 gates MAT integrity and
silences this rule while active; the engine is status-blind, so the
relationship lives in
suppressed_byfor the host to enforce. - Operating-state gating and NO_EVAL are frontmatter, not graph. G36 scopes FC#10 to OS#3 (§5.16.14.9c), suspends evaluation for ModeDelay after a mode change, and suspends it entirely when the AHU is off (§5.16.14.11). A host that evaluates at minimum outdoor air will see this rule assert on every cold morning, correctly by the equation and meaninglessly in fact, because mixing return air is what the damper is for.
persist.delayOnInit = true(Modelica/CDL default isfalse): a deviation already present at load waits out the full 30 minutes rather than alarming on the first tick after a controller restart. Library-wide choice, per AHU-0016.
Notes
This rule and AHU-0008 are the G36 pair testing whether two temperatures that ought to be equal actually are, and both are close cousins of AHU-0028. FC-062 tests containment — MAT inside the OAT–RAT envelope — a law that holds in every operating state; these two test equality, which holds in one state each. A unit whose MAT sits inside the envelope and still disagrees with outdoor air by 4 °C on full outdoor air passes the gate and fails here, which is the point of running both. The outdoor air fraction AHU-0021 and AHU-0030 compute puts MAT and OAT in numerator and denominator at once, so an error big enough to trip this rule moves that ratio further still. Run the sensor-drift playbook before anyone opens the mixing box, and check where the outdoor sensor is mounted — sun on the housing produces this signature with a calibrated element inside.
Test Vectors
9 scenarios, clock step 300 s over 7200 s.
| Scenario | Description |
|---|---|
mat_tracks_oat | Healthy OS#3: the OA damper is at 100%, the return damper is shut, and 15.5 °C outdoor air reaches the mixed-air sensor at 15.8 °C. The 0.3 °C residual is plenum warming and sensor spread, far inside the 3.1623 °C band. |
return_air_leaking_past_shut_damper | 15.0 °C outdoor air, 20.0 °C mixed air: 5 °C of warm return air is finding its way into a plenum that is supposed to be on 100% outdoor air. The return damper reads closed, so only the temperature pair can see it. Alarms after the 30 min delay. |
oat_sensor_reads_high | Mixed air 4 °C below outdoor air. Return air cannot cool a mixture below the outdoor stream, so this direction is a sensor story: an OAT sensor in afternoon sun, or a MAT sensor drifted low. Negative disagreement pins the Abs — without it the rule would be blind to half its diagnoses. |
edge_positive_dev_equals_threshold | Strict boundary, clear side: mat − oat = 3.1623 °C exactly, the full root-sum-square of eMAT and eOAT. GreaterThreshold is u > t, so a unit sitting precisely on its combined sensor budget reads healthy. |
edge_positive_dev_just_over_threshold | Strict boundary, fault side: the same outdoor air with MAT 0.04 °C warmer puts the deviation at 3.2 °C, which clears u > t and alarms after the delay. Pins the edge from the other direction. |
edge_negative_dev_equals_threshold | Same boundary on the cold side: mat − oat = −3.1623 °C exactly, |
edge_negative_dev_just_over_threshold | Cold side, fault: mat − oat = −3.2 °C. |
transient_disagreement_never_alarms | MAT rises to 20 °C for 1200 s — shorter than the 1800 s alarm delay — then settles back. A damper still stroking toward 100% leaves return air in the plenum for a few minutes; the timer resets on the first compliant tick and nothing is reported. |
fault_clears_when_damper_seats | A leaking return damper holds MAT at 20 °C against 15 °C outdoor air; the alarm asserts at 1800 s. At 3600 s the damper seats and MAT falls to 15.3 °C, and the alarm drops on that tick — assertion waits out the delay, recovery does not. |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 300,
"horizon_s": 7200
},
"scenarios": [
{
"name": "mat_tracks_oat",
"description": "Healthy OS#3: the OA damper is at 100%, the return damper is shut, and 15.5 \u00b0C outdoor air reaches the mixed-air sensor at 15.8 \u00b0C. The 0.3 \u00b0C residual is plenum warming and sensor spread, far inside the 3.1623 \u00b0C band.",
"inputs": {
"mat": 15.8,
"oat": 15.5
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 7200,
"equals": false
}
]
},
{
"name": "return_air_leaking_past_shut_damper",
"description": "15.0 \u00b0C outdoor air, 20.0 \u00b0C mixed air: 5 \u00b0C of warm return air is finding its way into a plenum that is supposed to be on 100% outdoor air. The return damper reads closed, so only the temperature pair can see it. Alarms after the 30 min delay.",
"inputs": {
"mat": 20.0,
"oat": 15.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yFault",
"from_s": 2100,
"to_s": 7200,
"equals": true
}
]
},
{
"name": "oat_sensor_reads_high",
"description": "Mixed air 4 \u00b0C below outdoor air. Return air cannot cool a mixture below the outdoor stream, so this direction is a sensor story: an OAT sensor in afternoon sun, or a MAT sensor drifted low. Negative disagreement pins the Abs \u2014 without it the rule would be blind to half its diagnoses.",
"inputs": {
"mat": 15.0,
"oat": 19.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yFault",
"from_s": 2100,
"to_s": 7200,
"equals": true
}
]
},
{
"name": "edge_positive_dev_equals_threshold",
"description": "Strict boundary, clear side: mat \u2212 oat = 3.1623 \u00b0C exactly, the full root-sum-square of eMAT and eOAT. GreaterThreshold is u > t, so a unit sitting precisely on its combined sensor budget reads healthy.",
"inputs": {
"mat": 18.5623,
"oat": 15.4
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 7200,
"equals": false
}
]
},
{
"name": "edge_positive_dev_just_over_threshold",
"description": "Strict boundary, fault side: the same outdoor air with MAT 0.04 \u00b0C warmer puts the deviation at 3.2 \u00b0C, which clears u > t and alarms after the delay. Pins the edge from the other direction.",
"inputs": {
"mat": 18.6,
"oat": 15.4
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yFault",
"from_s": 2100,
"to_s": 7200,
"equals": true
}
]
},
{
"name": "edge_negative_dev_equals_threshold",
"description": "Same boundary on the cold side: mat \u2212 oat = \u22123.1623 \u00b0C exactly, |dev| = t, clear. The band is symmetric, so both signs have to be pinned.",
"inputs": {
"mat": 14.8377,
"oat": 18.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 7200,
"equals": false
}
]
},
{
"name": "edge_negative_dev_just_over_threshold",
"description": "Cold side, fault: mat \u2212 oat = \u22123.2 \u00b0C. |dev| clears the threshold and the alarm asserts at the same magnitude the positive case does.",
"inputs": {
"mat": 14.8,
"oat": 18.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yFault",
"from_s": 2100,
"to_s": 7200,
"equals": true
}
]
},
{
"name": "transient_disagreement_never_alarms",
"description": "MAT rises to 20 \u00b0C for 1200 s \u2014 shorter than the 1800 s alarm delay \u2014 then settles back. A damper still stroking toward 100% leaves return air in the plenum for a few minutes; the timer resets on the first compliant tick and nothing is reported.",
"inputs": {
"mat": [
{
"t": 0,
"value": 15.3
},
{
"t": 600,
"value": 20.0
},
{
"t": 1800,
"value": 15.3
}
],
"oat": 15.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 7200,
"equals": false
}
]
},
{
"name": "fault_clears_when_damper_seats",
"description": "A leaking return damper holds MAT at 20 \u00b0C against 15 \u00b0C outdoor air; the alarm asserts at 1800 s. At 3600 s the damper seats and MAT falls to 15.3 \u00b0C, and the alarm drops on that tick \u2014 assertion waits out the delay, recovery does not.",
"inputs": {
"mat": [
{
"t": 0,
"value": 20.0
},
{
"t": 3600,
"value": 15.3
}
],
"oat": 15.0
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yFault",
"from_s": 2100,
"to_s": 3300,
"equals": true
},
{
"output": "yFault",
"from_s": 3900,
"to_s": 7200,
"equals": false
}
]
}
]
}