RTU-0011 — RTU supply-air temperature tracking failure
| Status | verified — engine e2ff2f8, cxf:fnv1a128:0ae66df187840b9fde91f9796ee3082a, 2026-08-20 |
| Severity | 3 |
| Method | rule |
| Phase | 2 |
| Category | COMFORT_ENERGY |
| Confidence | MEDIUM |
| Estimation | QUALITATIVE_ONLY |
| G36 | — |
| Clusters | — |
| Suppresses | — |
| Suppressed by | — |
| Related | RTU-0001, RTU-0002, RTU-0003, RTU-0004, RTU-0007, RTU-0008, RTU-0009, RTU-0010 |
| Playbooks | rtu-compressor-refrigerant |
| Source | Veronica, Automatically Detecting Faulty Regulation in HVAC Controls, HVAC&R Research 19(4), 2013, pp.412-422, DOI 10.1080/10789669.2013.789369 — regulated variables are checked against user allowance bands; semantic evidence, not this graph’s thresholds; Library CHW-0007 and HW-0010 — strict mirrored tracking-error topology with operating-premise gating and persistent alarm; Library-authored packaged-DX adaptation; no cited source publishes the shipped 2 K and 900 s combination as portable |
| Operating states | normal automatic RTU heating or cooling after airflow and active mechanical delivery are established and the active SAT target has settled |
Preconditions (host-enforced): sat and sat_sp must be the same RTU’s discharge temperature and final active mode-specific target, not an occupied default or cooling-only constant during heating. sf_status must prove stable supply airflow; comp_status or htg_status must represent active mechanical conditioning. Exclude startup, defrost, post-heat fan delay, mode/setpoint changes, demand response, low-ambient protection, and OEM capacity or safety limits. Suspend or smooth evaluation across normal staged/cycling-DX off intervals. Points must be healthy, fresh, aligned, and in degC. yConditioningActive false means NO_EVAL; true does not prove fan status or the other host obligations.
Points: sat, sat_sp, sf_status, comp_status, htg_status
Outputs:
yFault— True after proven fan operation and mechanical conditioning remain outside either side of the active SAT band for sustained_durationyTooWarm— Diagnostic direction flag; true while evaluable SAT is strictly more than tracking_error above setpoint. False never means NO_EVALyTooCold— Diagnostic direction flag; true while evaluable SAT is strictly more than tracking_error below setpoint. False never means NO_EVALyConditioningActive— Evaluability subcondition; true when compressor or heating proof is active. False means NO_EVAL; true does not establish every host precondition
Parameters:
| Name | Default | Unit | CXF path | Description |
|---|---|---|---|---|
tracking_error | 2.0 | K | tooWarmRaw.t, tooColdRaw.t | ADOPTED_TUNABLE symmetric settled SAT allowance; equality is clear. Commission above combined sensor uncertainty, cycling ripple, and normal deadband. |
sustained_duration | 900.0 | s | persist.delayTime | LIBRARY_PRECEDENT continuous out-of-band duration; commission above normal mode, staging, and setpoint-settling dynamics. |
Description
This rule reports an RTU whose discharge temperature remains materially outside its final active target while the supply fan and mechanical heating or cooling are proven active. Warm/cold outputs identify the observed direction, not the failed component or commanded mode.
Detection Logic
error = sat - sat_sp
conditioning_active = comp_status OR htg_status
running_conditioning = sf_status AND conditioning_active
too_warm = error > tracking_error
too_cold = -error > tracking_error
yConditioningActive = conditioning_active
yTooWarm = running_conditioning AND too_warm
yTooCold = running_conditioning AND too_cold
yFault = running_conditioning AND (too_warm OR too_cold),
sustained for sustained_duration
Comparisons are strict and direction flags are immediate. One
TrueDelay(delayOnInit=true) follows the warm/cold OR, so a direct sampled
direction handoff preserves age while any in-band or non-running tick resets it.
Possible Diagnoses
- Active setpoint misbound, stale, overridden, or not delivered locally
- Insufficient heating/cooling capacity, failed stage, or abnormal cycling
- Low supply airflow, dirty filter/coil, or failed fan proof
- Economizer or outdoor-air damper introducing the wrong air condition
- Refrigerant charge, condenser airflow, compressor, or heat-section fault
- Sensor bias or normal OEM limit/transition omitted from host gating
Energy Impact
A tracking fault may increase compressor, heating, or fan runtime and can miss temperature or humidity targets. The sign alone does not establish waste; no energy is inferred from temperature error without a causal model.
Emissions Impact
Scope 1 and/or 2, qualitative. Quantify only after isolating the cause and measuring affected fuel or electrical input against a defensible baseline.
Deviations
- The 2 K / 900 s pair is adopted library logic. The sources support SAT tracking as a diagnostic, not portable packaged-unit thresholds.
- Confidence is MEDIUM rather than the brief’s proposed HIGH. Cycling DX, active-setpoint semantics, airflow, OEM limits, and transitions require commissioned host gates before the same signature is causal.
- Mechanical status is an OR, not a mode table. Heating-only, cooling-only, or both-active raw states are detectable; the host validates the actual mode.
- Direction handoff preserves persistence. A sampled warm-to-cold transition remains continuously out of band; an actual in-band sample resets the timer.
- Proof faults remain related, not suppressors. Current metadata cannot suppress tracking only for RTU-0010’s fail-to-start direction.
- No empirical FPR or TPR is claimed. The current harness lacks a defensible final active RTU SAT setpoint plus aligned mechanical-status mapping.
Test Vectors
15 scenarios, clock step 60 s over 1500 s.
| Scenario | Description |
|---|---|
fan_off_with_large_error | Temperature error without proven supply airflow is not evaluable. |
fan_on_without_mechanical_conditioning | A running fan alone does not establish the mechanical-delivery premise. |
active_on_setpoint | Stable mechanical operation at the active target is healthy. |
cooling_status_too_warm | Cooling proof with SAT above its target drives the warm diagnostic and matures the alarm. |
heating_status_too_cold | Heating-only proof with SAT below target exercises the independent cold direction. |
exact_warm_error_is_clear | The +2 K boundary is clear under the strict comparison. |
exact_cold_error_is_clear | The -2 K boundary is clear under the mirrored strict comparison. |
both_conditioning_statuses_active_raw_case | The graph intentionally treats either or both mechanical statuses as active; host mode validation remains external. |
normal_dx_off_cycle_resets_timer | A normal compressor-off interval removes evaluability and resets an immature error episode. |
violation_ends_one_step_before_persistence | An error ending one evaluator step before 900 s never matures. |
mature_warm_fault_recovers | A mature alarm and its direction clear immediately when SAT returns to band. |
two_short_error_episodes_reset | Two sub-duration violations separated by an in-band interval cannot accumulate persistence. |
initialization_violation_serves_full_delay | delayOnInit requires the full tracking interval after evaluator startup. |
sampled_direction_handoff_preserves_persistence | A direct sampled warm-to-cold handoff stays continuously outside the symmetric band and preserves timer age. |
setpoint_step_raw_transient | An unexcluded active-setpoint step can create raw error, but this short case resets before alarm maturity. |
vectors.json
{
"schema": "cxf-library/vectors/v1",
"clock": {
"step_s": 60,
"horizon_s": 1500
},
"scenarios": [
{
"name": "fan_off_with_large_error",
"description": "Temperature error without proven supply airflow is not evaluable.",
"inputs": {
"sat": 20.0,
"sat_sp": 15.0,
"sf_status": false,
"comp_status": true,
"htg_status": false
},
"expect": [
{
"output": "yConditioningActive",
"from_s": 0,
"to_s": 1500,
"equals": true
},
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yTooCold",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
}
]
},
{
"name": "fan_on_without_mechanical_conditioning",
"description": "A running fan alone does not establish the mechanical-delivery premise.",
"inputs": {
"sat": 20.0,
"sat_sp": 15.0,
"sf_status": true,
"comp_status": false,
"htg_status": false
},
"expect": [
{
"output": "yConditioningActive",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yTooCold",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
}
]
},
{
"name": "active_on_setpoint",
"description": "Stable mechanical operation at the active target is healthy.",
"inputs": {
"sat": 15.0,
"sat_sp": 15.0,
"sf_status": true,
"comp_status": true,
"htg_status": false
},
"expect": [
{
"output": "yConditioningActive",
"from_s": 0,
"to_s": 1500,
"equals": true
},
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yTooCold",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
}
]
},
{
"name": "cooling_status_too_warm",
"description": "Cooling proof with SAT above its target drives the warm diagnostic and matures the alarm.",
"inputs": {
"sat": 17.1,
"sat_sp": 15.0,
"sf_status": true,
"comp_status": true,
"htg_status": false
},
"expect": [
{
"output": "yConditioningActive",
"from_s": 0,
"to_s": 1500,
"equals": true
},
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 1500,
"equals": true
},
{
"output": "yTooCold",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 1500,
"equals": true
}
]
},
{
"name": "heating_status_too_cold",
"description": "Heating-only proof with SAT below target exercises the independent cold direction.",
"inputs": {
"sat": 12.9,
"sat_sp": 15.0,
"sf_status": true,
"comp_status": false,
"htg_status": true
},
"expect": [
{
"output": "yConditioningActive",
"from_s": 0,
"to_s": 1500,
"equals": true
},
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yTooCold",
"from_s": 0,
"to_s": 1500,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 1500,
"equals": true
}
]
},
{
"name": "exact_warm_error_is_clear",
"description": "The +2 K boundary is clear under the strict comparison.",
"inputs": {
"sat": 17.0,
"sat_sp": 15.0,
"sf_status": true,
"comp_status": true,
"htg_status": false
},
"expect": [
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yTooCold",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
}
]
},
{
"name": "exact_cold_error_is_clear",
"description": "The -2 K boundary is clear under the mirrored strict comparison.",
"inputs": {
"sat": 13.0,
"sat_sp": 15.0,
"sf_status": true,
"comp_status": true,
"htg_status": false
},
"expect": [
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yTooCold",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
}
]
},
{
"name": "both_conditioning_statuses_active_raw_case",
"description": "The graph intentionally treats either or both mechanical statuses as active; host mode validation remains external.",
"inputs": {
"sat": 17.1,
"sat_sp": 15.0,
"sf_status": true,
"comp_status": true,
"htg_status": true
},
"expect": [
{
"output": "yConditioningActive",
"from_s": 0,
"to_s": 1500,
"equals": true
},
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 1500,
"equals": true
},
{
"output": "yTooCold",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 1500,
"equals": true
}
]
},
{
"name": "normal_dx_off_cycle_resets_timer",
"description": "A normal compressor-off interval removes evaluability and resets an immature error episode.",
"inputs": {
"sat": 18.0,
"sat_sp": 15.0,
"sf_status": true,
"comp_status": [
{
"t": 0,
"value": true
},
{
"t": 600,
"value": false
},
{
"t": 900,
"value": true
}
],
"htg_status": false
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 540,
"equals": true
},
{
"output": "yTooWarm",
"from_s": 600,
"to_s": 840,
"equals": false
},
{
"output": "yTooWarm",
"from_s": 900,
"to_s": 1500,
"equals": true
}
]
},
{
"name": "violation_ends_one_step_before_persistence",
"description": "An error ending one evaluator step before 900 s never matures.",
"inputs": {
"sat": [
{
"t": 0,
"value": 18.0
},
{
"t": 840,
"value": 15.0
}
],
"sat_sp": 15.0,
"sf_status": true,
"comp_status": true,
"htg_status": false
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 780,
"equals": true
},
{
"output": "yTooWarm",
"from_s": 840,
"to_s": 1500,
"equals": false
},
{
"output": "yTooCold",
"from_s": 0,
"to_s": 1500,
"equals": false
}
]
},
{
"name": "mature_warm_fault_recovers",
"description": "A mature alarm and its direction clear immediately when SAT returns to band.",
"inputs": {
"sat": [
{
"t": 0,
"value": 18.0
},
{
"t": 1080,
"value": 15.0
}
],
"sat_sp": 15.0,
"sf_status": true,
"comp_status": true,
"htg_status": false
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 1020,
"equals": true
},
{
"output": "yFault",
"from_s": 1080,
"to_s": 1500,
"equals": false
},
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 1020,
"equals": true
},
{
"output": "yTooWarm",
"from_s": 1080,
"to_s": 1500,
"equals": false
},
{
"output": "yTooCold",
"from_s": 0,
"to_s": 1500,
"equals": false
}
]
},
{
"name": "two_short_error_episodes_reset",
"description": "Two sub-duration violations separated by an in-band interval cannot accumulate persistence.",
"inputs": {
"sat": [
{
"t": 0,
"value": 18.0
},
{
"t": 480,
"value": 15.0
},
{
"t": 720,
"value": 18.0
},
{
"t": 1200,
"value": 15.0
}
],
"sat_sp": 15.0,
"sf_status": true,
"comp_status": true,
"htg_status": false
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
}
]
},
{
"name": "initialization_violation_serves_full_delay",
"description": "delayOnInit requires the full tracking interval after evaluator startup.",
"inputs": {
"sat": 18.0,
"sat_sp": 15.0,
"sf_status": true,
"comp_status": true,
"htg_status": false
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 1500,
"equals": true
},
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 1500,
"equals": true
},
{
"output": "yTooCold",
"from_s": 0,
"to_s": 1500,
"equals": false
}
]
},
{
"name": "sampled_direction_handoff_preserves_persistence",
"description": "A direct sampled warm-to-cold handoff stays continuously outside the symmetric band and preserves timer age.",
"inputs": {
"sat": [
{
"t": 0,
"value": 18.0
},
{
"t": 480,
"value": 12.0
}
],
"sat_sp": 15.0,
"sf_status": true,
"comp_status": true,
"htg_status": false
},
"expect": [
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 420,
"equals": true
},
{
"output": "yTooWarm",
"from_s": 480,
"to_s": 1500,
"equals": false
},
{
"output": "yTooCold",
"from_s": 0,
"to_s": 420,
"equals": false
},
{
"output": "yTooCold",
"from_s": 480,
"to_s": 1500,
"equals": true
},
{
"output": "yFault",
"from_s": 0,
"to_s": 840,
"equals": false
},
{
"output": "yFault",
"from_s": 960,
"to_s": 1500,
"equals": true
}
]
},
{
"name": "setpoint_step_raw_transient",
"description": "An unexcluded active-setpoint step can create raw error, but this short case resets before alarm maturity.",
"inputs": {
"sat": 15.0,
"sat_sp": [
{
"t": 0,
"value": 15.0
},
{
"t": 300,
"value": 12.0
},
{
"t": 720,
"value": 15.0
}
],
"sf_status": true,
"comp_status": true,
"htg_status": false
},
"expect": [
{
"output": "yFault",
"from_s": 0,
"to_s": 1500,
"equals": false
},
{
"output": "yTooWarm",
"from_s": 0,
"to_s": 240,
"equals": false
},
{
"output": "yTooWarm",
"from_s": 300,
"to_s": 660,
"equals": true
},
{
"output": "yTooWarm",
"from_s": 720,
"to_s": 1500,
"equals": false
},
{
"output": "yTooCold",
"from_s": 0,
"to_s": 1500,
"equals": false
}
]
}
]
}