Trim-and-Respond / G36 reset compliance
The drift families ask "is this slowly getting worse?"; this family asks a control-sequence question: "is the plant's setpoint-reset logic doing what ASHRAE Guideline-36 intends?" G36 replaces fixed supply-air-temperature and duct-static setpoints with reset schedules — an OAT-driven SAT target, and Trim-and-Respond (T&R) loops driven by per-zone requests. When that reset logic is missing, mis-scheduled, or dominated by one rogue zone, the plant burns avoidable reheat and fan energy while every instantaneous check still reads "in range."
flowchart TD
zones["per-zone temps"] --> req["requests (cooling_sat_requests / static_pressure_requests)"]
req --> tr["Trim-and-Respond (tr_simulate)"]
oat[OAT] --> map["OAT to SAT target (oat_sat_setpoint)"]
tr --> sp["reset setpoint (SAT / duct static)"]
map --> comp["supply_air_reset_compliance (right target?)"]
sp --> eff["reset_effectiveness (trimming and responding?)"]
req --> rogue["rogue_zone_census (one zone dragging?)"]
req --> cohort["cohort_starvation (whole AHU starved?)"]
Zone requests drive Trim-and-Respond to a setpoint; the four detector questions read compliance, effectiveness, rogue zones, and cohort starvation off that pipeline.
CAMBER already ships the G36 primitives in camber.g36_reset (the OAT→SAT map oat_sat_setpoint,
the T&R loop tr_step/tr_simulate with the SAT_TR/STATIC_TR parameter presets, and the per-zone
request generators cooling_sat_requests/static_pressure_requests). This family wires them into
rules.
The detectors
| Rule | Signal | Sided | Catches |
|---|---|---|---|
supply_air_reset_compliance |
supply-air temp vs the G36 OAT-reset target | up (too cold) | supply air held colder than the G36 §5.16.2.2.b OAT→SAT target — an avoidable reheat/energy opportunity |
sat_reset_effectiveness |
supply-air-temp setpoint vs the request-implied T&R trajectory | two-sided | a SAT reset that is stuck, not responding, not trimming, or diverging vs its own zone requests |
static_reset_effectiveness |
duct-static setpoint vs the request-implied T&R trajectory | two-sided | a duct-static reset that is stuck, not responding, not trimming, or diverging vs its own zone requests |
sat_rogue_zone_census |
per-zone SAT reset-request rate across the zone fleet | one-sided (monopolizer) | one chronically over-demanding zone monopolizing the SAT requests and dragging the reset colder than the fleet needs |
static_rogue_zone_census |
per-zone duct-static reset-request rate across the zone fleet | one-sided (monopolizer) | one chronically over-demanding zone monopolizing the duct-static requests and dragging the reset higher than the fleet needs |
static_cohort_starvation |
per-zone request rate, cohort-wide simultaneity per AHU | cohort common-mode | most/all of an AHU's zones requesting the duct-static reset at once → one upstream fault (fan/static SP/upstream damper), not N zone faults |
sat_cohort_starvation |
per-zone request rate, cohort-wide simultaneity per AHU | cohort common-mode | most/all of an AHU's zones requesting the SAT reset at once → cooling/reset maxed (caveated as a possible design-day) |
SAT-reset compliance vs the existing supply_air_reset. These are complementary detectors on the
same signal. supply_air_reset (camber.satreset) asks the shape question — does supply air get
reset upward at all as cooling load drops (a negative slope against OAT)? supply_air_reset_compliance asks the
target question — is supply air held colder than the specific G36 OAT→SAT map would command? A
plant can have a healthy reset slope yet still sit below the G36 target, and vice-versa, so both are
worth running.
The G36 map (oat_sat_setpoint, §5.16.2.2.b) holds SAT at min_clg_sat (55 °F default) when it is
hot out (OAT ≥ oat_max, 70 °F) and resets it up toward t_max (65 °F) when it is cool (OAT ≤
oat_min, 60 °F). The rule compares actual SAT to that OAT-computed target — not to a mapped
SUPPLY_AIR_TEMP_SP — so it works on a typical trend export carrying only SAT and OAT. It is
one-sided (only SAT colder than the target is an opportunity; running warmer than min_clg_sat
is the energy-saving direction) and warn-level (an opportunity, not a hard fault), warning when SAT
runs below target warn_pct % of the hours and the mean gap clears warn_gap_f (so a trivially
small persistent gap does not flag). The four G36 map parameters are constructor arguments, so a site
can match its own reset schedule (make_rule("supply_air_reset_compliance", oat_min=…, min_clg_sat=…)).
Thresholds are screening / opportunity-grade (provisional-untuned). OAT is building-level and arrives
via the runner's shared channel; the rule declines loudly when it is unmapped.
Tracking error (0.90.1). Running warmer is the energy-saving direction, but supply air held far
above the target is not "tracking" it either -- a cooling shortfall or a fixed, high setpoint. So
the rule also reports the mean absolute error (mean_abs_error_f, tracks_target) and, over the
warm-weather samples where the cooling reset applies (OAT at or above oat_min, so a heating mode
can't explain it), how often and how far supply air runs above the target (pct_above_target,
mean_above_gap_f, n_warm). When supply air runs more than track_gap_f (5 °F) above the target on
average for warn_pct % of those samples, the rule warns "NOT tracking" instead of saying "ok";
an ok finding says "tracks the G36 reset target" (or the configured or site-sequence one: the
finding names its target by its source, target_source, 0.108, #269) only when the mean absolute error is below
track_gap_f.
Air handlers only (0.90.1). Rules are gated by roles, so a heat pump's or a terminal unit's
discharge air would otherwise be read as an air handler's reset supply air. The rule declares
the air-handler classes (provisional; AHU, RTU, DOAS, MAU and their spellings), and the runner
declines it with an info finding and a caveat on equipment of any other recognised class.
Equipment with no recorded class still runs, and an unrecognised class runs with a caveat. Since
0.91 the rule's equip_classes attribute is its entry in the central table
camber.rules.applicability.RULE_EQUIP_CLASSES (("air_handler",)), the one place every built-in
rule's classes are declared.
Operating-state gates. By default, the rule judges only samples where the supply fan runs (fan
status, else fan speed, else airflow) and the space is occupied (a trended occupancy point, else the
assumed weekday 07-18 schedule). A unit parked overnight with SAT drifting is therefore not scored
against a reset it was never running. The metrics record fan_gate, occupancy_gate, n_gated
and reset_source (g36_default, configured, or a label passed as reset_source=). Pass
fan_gate=False, occupied_only=False for the historical, ungated read. The gates left the
synthetic, fleet and LBNL benchmark metrics unchanged. In the RCx report, a
finding judged against the G36 defaults with no site sequence is shown as a reference only and is
not priced.
The shape rule is fan-gated too. supply_air_reset fits its SAT-vs-OAT slope on occupied,
cooling, fan-on samples (the same fan-signal precedence) and records fan_gate; its evidence
draws exactly those samples. With the fan off the "supply" sensor reads unconditioned plenum air,
which bends the slope and widens the spread. fan_gate=False restores the ungated read.
The reset direction follows the driver (#65, 0.92). A G36 cooling SAT reset lowers supply air as
OAT rises (§5.16.2.2.b) and lowers it again for each cooling SAT reset request beyond the ignored
ones (the trim-and-respond response is negative, Table 5.16.2.2; camber.g36_reset.SAT_TR). So
supply_air_reset reads a reset from a negative SAT-vs-OAT slope, and a trended setpoint must
move against its driver (Spearman rho < 0 against OAT or against requests). Supply air that rises
with OAT over cooling hours is supply air rising with load: it is reported as "SAT RISING WITH LOAD
(possible capacity shortfall, not a reset)" with reset_direction: "rising_with_load" (info, or
warn when cold supply air dominates), and a setpoint that rises with its driver is "moves with …
in the wrong direction" (sp_wrong_direction, never ok). Until 0.92 the rule read the positive
slope as the reset. static_pressure_reset is unchanged: a static reset raises its setpoint with
requests.
A trended setpoint beats the SAT shape (#63). Supply air that looks reset can still be a
setpoint that never moved. When SUPPLY_AIR_TEMP_SP is mapped, supply_air_reset asks the
setpoint (camber.setpoint_reset.classify_setpoint_reset, fan-on samples): a reset must move on at
least 3 days and 10 % of the days judged, with its driver (SAT_RESET_REQUESTS, else OAT;
|Spearman rho| >= 0.3, in the reset direction). A flat setpoint reads "NOT RESET (setpoint flat …);
SAT deviates …", a setpoint that only stepped reads as a one-time step or manual change, and one that
moves but not with its driver leaves an apparent reset unconfirmed (info). static_pressure_reset
applies the same test with static-pressure requests, else supply airflow, as the driver.
Reset effectiveness — is the reset actually trimming-and-responding?
supply_air_reset_compliance above asks whether SAT sits at the right target; reset
effectiveness asks whether the reset mechanism works at all. Given the plant's own aggregated
per-cycle reset-request count (SAT_RESET_REQUESTS / STATIC_PRESSURE_REQUESTS) alongside the
actual reset setpoint (SUPPLY_AIR_TEMP_SP / DUCT_STATIC_SP), the rule reconstructs the
setpoint that Trim-and-Respond should have produced (tr_simulate, G36 §5.1.14) and compares it to
the trended setpoint. It flags four modes:
- stuck — the setpoint barely moves while the request pattern would have moved it (a frozen or overridden reset);
- not responding — under sustained demand the setpoint sits at the energy-saving (trim) end instead of responding toward demand (zones starve);
- not trimming — while zones are idle the setpoint stays parked at the demand end (energy wasted);
- diverges — the setpoint moves the opposite way to the T&R command on most cycles (an inverted or mis-wired reset).
It ships as two instances — sat_reset_effectiveness (supply-air-temp setpoint, °F, SAT_TR preset)
and static_reset_effectiveness (duct-static setpoint, in. w.c., STATIC_TR preset) — sharing one
reset-agnostic camber.g36_reset.reset_effectiveness analyzer. It is two-sided (both starving and
wasting are faults) and warn-level (an operational opportunity, not a hard equipment fault).
Unlike the compliance rule it needs the reset setpoint and the request count both mapped, so it
declines loudly on the common trend export that carries no request point. The reconstructed-trajectory
error (mean_abs_error_sp) is informational only — a coarser trend cadence than the controller
inflates it, so the verdict rests on the cadence-robust mode detectors above, not the raw error.
Thresholds are screening / opportunity-grade (provisional-untuned).
Since 0.106 (#186) "not responding" and "not trimming" are judged only on the cycles where the
trajectory the requests imply had itself left that end of the band (n_demand_judged,
n_idle_judged). Under modest demand a correct Trim-and-Respond creeps off its trim end slowly,
and it is no longer called unresponsive for doing what its requests ask. reason is the first
failure mode and failure_modes lists every one that holds (a reset that is both stuck and not
responding says so).
Rogue-zone census — which zone is dragging the reset?
The two rules above look at the reset setpoint; this one looks at the demand side that drives it. In G36 the SAT / duct-static reset responds to the high-percentile of per-zone requests (§5.14.8), so one chronically over-demanding zone can monopolize the requests and drag the whole reset — forcing a colder supply-air temperature or higher duct static than the rest of the fleet needs. The plant then serves one bad box at everyone else's energy expense.
sat_rogue_zone_census and static_rogue_zone_census are fleet rules (many zones in, one
aggregate Finding out). Across the zone fleet each computes every zone's per-cycle request series — a
vectorized form of cooling_sat_requests / static_pressure_requests — then scores each zone by its
share of the group's total requests and the fraction of active cycles on which it holds the
binding (maximum) request. A zone is flagged as a rogue when it both holds the binding request a
dominant fraction of the time and commands a disproportionate share of the requests. Requiring both
is what keeps a uniformly-busy fleet quiet: when every zone is equally hot they all tie at the binding
max, but their shares are equal, so none is singled out.
Topology honesty (auto-scoping). Deciding that a zone drags AHU-1's reset requires knowing which zones AHU-1 serves. The census now scopes per air handler automatically whenever a served-by topology is available, and the caveat it attaches reflects how trustworthy that grouping is:
- Semantic topology (from a Brick
feeds/ HaystackahuRefmodel, passed asrun_fleet(..., topology=site.topology)) — the census scopes per air handler (a rogue is compared only to its siblings, inrogue_by_group) and the confound caveat drops. - Naming-heuristic topology — when no semantic model is supplied,
Registry.run_fleetauto-builds a grouping from the equipment ids themselves (AHU_1_VAV_3underAHU_1), so the census scopes per-AHU even with no model — but keeps a softened screening caveat because the grouping is inferred, not verified. - Partial coverage — zones the topology does not cover are pooled together (building-wide fallback) and that remainder is caveated; the covered zones are still scoped.
- No topology — the original building-wide pool with the full confound caveat (a zone flagged in the single pool may simply serve a hotter air handler, so it is a screening signal only).
- Zero coverage — a topology (e.g. the auto-built naming one) that matches none of the zones
applies no grouping:
grouping_provenanceisNoneand the caveat names the topology's provenance and the 0-of-N coverage, rather than labelling an ungrouped census "heuristic".
System-on gating. A zone's request cycles count only while its system is on: when the zone frames
carry SUPPLY_FAN_STATUS and/or OCCUPANCY (per zone, or merged in as a building-level shared
series), cycles with the fan off or the space unoccupied are dropped — a free-floating building has
every zone warm at once, which otherwise reads as a starved cohort or a rogue zone. With neither
trended the census runs ungated and says so in a caveat. Both census families
(*_rogue_zone_census, *_cohort_starvation) share the gate.
The grouping and its provenance are recorded in the finding's metrics (grouped,
grouping_provenance, n_zones_ungrouped). The census also declines loudly — demoting a clean "ok"
to info — when zones were unevaluable or no zone generated any request (the reset is not
demand-bound, so nothing can be dragging it). Thresholds are screening / opportunity-grade
(provisional-untuned).
Overloaded is not starved (0.106, #186). A zone no box can cool sits at its cooling maximum
with its damper wide open, so it asks for more duct static as well as for colder air. When the
zones trend their space temperature and cooling setpoint, the static census checks each rogue's
static-request cycles: a zone at least 3 °F above its cooling setpoint on half of them or more is
listed in overloaded_zones (its share in overloaded_share) instead of rogues. The finding
still warns (the zone does hold the duct static up), and points at the SAT census's rogue.
Tier-1 SAT requests (0.106, #186). G36's lowest SAT request tier is a zone cooling loop above
95 %. The SAT censuses count it on the zones that trend their cooling loop (zone_cool_loop); on
the others the caveat says how many zones it was not evaluated on.
Cohort starvation — the common-mode twin
The rogue-zone census finds an outlier — one zone dominating the requests. Its mirror image is a
common-mode pattern: when most or all of an air handler's zones raise reset requests at the same
time, that is not many independent zone faults, it is one upstream fault the reset cannot fix — a
duct-static setpoint capped too low, a supply fan maxed out or failing, or a restricted upstream
damper (SAT side: cooling capacity or the reset maxed). static_cohort_starvation and
sat_cohort_starvation group each AHU's zones (per the same topology machinery) and
flag a group when a high fraction of its zones request simultaneously on a sustained fraction of the
active cycles. The finding names the air handler and says look upstream, not at individual zones.
The two twins are provably distinct shapes on the same signal: a lone rogue never reaches the cohort-wide fraction, and a starved cohort shares its requests too evenly to concentrate in one zone — so a rogue never trips starvation and a starved cohort never trips the rogue census. Both scope per air handler with the same provenance/coverage-aware caveat (semantic drops it, heuristic softens it, partial pools the remainder). The SAT variant carries an extra caveat: a whole cohort running warm can be a genuinely hot design-day rather than a capacity fault, so corroborate with OAT before acting.
Dollar impact
camber.fault_economics prices the reset family's findings so an operator can rank the backlog by
money, not just severity (triage-grade, not audit-grade):
supply_air_reset_compliance→ avoidable terminal reheat: reheat-coil capacity × below-target hours × diversity, scaled by the below-target gap (mean_gap_f) so a bigger gap costs more. Needs the reheat-coil sizing (EquipmentLoad.heating_capacity_kbtuh), else uncosted.static_reset_effectiveness(not_trimming) → wasted fan energy (static SP parked high while zones are idle):fan_kw× idle-untrimmed fraction × excess.sat_reset_effectiveness(not_trimming) → avoidable reheat (SAT parked cold).-
The other reset-effectiveness modes are uncosted by design —
not_responding(zones starve) is a comfort/capacity risk, andstuck/divergeshave an indeterminate energy sign; the estimator returns an honest basis rather than fabricating a dollar figure. -
sat/static_rogue_zone_census→ the whole air handler over-services (colder SAT / higher static) because one zone drags the reset: reheat / fan waste scaled by the rogue's request share (worst_zone_share). Uncosted when the census is ungrouped (a building-wide screening signal, not a per-AHU verdict). static_cohort_starvation→ sustained fan over-pressure from the upstream fault, scaled by the cohort's sustained fraction;sat_cohort_starvationis unmet-load/comfort → uncosted.
Because these findings are fleet-level (equip="<fleet>"), the estimator attributes sizing to the
offending air handler named in the metrics (worst_group, or the group holding worst_zone), so
a {equip: EquipmentLoad} map keyed by AHU costs each starved/dragged handler on its own fan/coil.
Family complete
The Trim-and-Respond / G36-reset family now spans seven detectors on the shared camber.g36_reset
engine (oat_sat_setpoint, tr_step / tr_simulate, cooling_sat_requests /
static_pressure_requests): SAT-reset compliance (does the reset sit at the right target?),
reset effectiveness for SAT and static (does the reset actually trim and respond?), the
rogue-zone census for SAT and static (is one zone dragging the reset?), and its common-mode twin
cohort starvation for SAT and static (is a whole AHU cohort starved by an upstream fault?). The
census family scopes per air handler automatically via the served-by topology —
semantic (from a Brick/Haystack model) or a naming heuristic run_fleet auto-builds.
Further reading
The PNNL Building Re-tuning guides behind these checks (links only; see References):
AHU Static Pressure Control (static_pressure_reset),
AHU Discharge-Air Temperature Control (supply_air_reset,
supply_air_reset_compliance, supply_air_control),
Central Utility Plant Cooling Control and
Central Utility Plant Heating Control (the CHW / HW loop and pump
differential-pressure resets), and training chapters 5 and
8.