Skip to content

CAMBER capabilities reference

A single index of what CAMBER does, grouped by the building-analytics layers, with the key API, the option flags that tune each capability, the module, and the standard it cites. Deeper write-ups are linked where they exist.

Everything is vendor-neutral via the Role model, dependency-light (stdlib + numpy / pandas / pyarrow / matplotlib), and clean-room (every method cites a public standard; every rule ships a synthetic fixture).

flowchart TD
  src["BAS sources (CSV/SQL/Haystack/Modbus/MQTT/BACnet/OPC-UA)"] --> ingest["Ingest (SourceAdapter)"]
  ingest --> model["Semantic model (Role + MappingProvider + entities)"]
  model --> fdd["FDD (rules.Registry)"]
  model --> mv["M&V (mandv change-point / TOWT)"]
  model --> stream["Streaming (OnlineFDD / OnlineCusum / forecast)"]
  model --> domain["Domain analytics (comfort/carbon/GEB/ventilation)"]
  fdd --> triage["Triage + lifecycle (rules.triage / faultlifecycle)"]
  triage --> advisory["Advisory (aso / actionplan / scorecard)"]
  mv --> report["Reporting (AuditReport / fleet)"]
  advisory --> report
  domain --> report
  report --> platform["Platform (store / api / integrate / plugins / charts)"]

The capability families, all speaking the vendor-neutral Role model.


Ingest

Adapters normalize any source to named point series on a common time grid (SourceAdapter: point_names / load_points / units). See INGEST-PROTOCOLS.md and SECURITY.md.

  • CSV — ingest.csv_perpoint.PerPointCsvAdapter (a folder of per-point files), ingest.csv_wide.WideCsvAdapter (one wide table), and ingest.csv_long.LongCsvAdapter (timestamp,point,value historian shape). Flags: resample, profile.
  • Vendor formats — one shared multi-format timestamp parser (tsparse: ISO / US / EU-dayfirst / BAS / epoch / Excel-serial) + value/status coercion (coerce: thousands, null tokens, On/Off/Open/ Closed/Fault status) + named vendor profiles (ingest.profiles: niagara_n4/metasys/webctrl/tracer/ desigo). load_csv(..., profile=…). See INGEST-FORMATS.md.
  • Project-Haystack — ingest.haystack.HaystackAdapter over an injectable transport (http_json_transport, or client_transport to wrap a maintained client). Flags: range_str, resample.
  • SQL / historian — ingest.sql.SqlSource / read_points over any PEP-249 connection. Flags: ts_col / point_col / value_col / unit_col, where.
  • Network protocols (read-only) — Modbus ([modbus]), MQTT/Sparkplug ([mqtt]), BACnet incl. experimental BACnet/SC ([bacnet]), OPC-UA ([opcua]). Read-only by construction; historian-first posture. Per-adapter flags documented in INGEST-PROTOCOLS.md.
  • Data quality — ingest.quality.assess (coverage, gaps, flatline, outliers, duplicate timestamps, composite score) and clean. Also reads two-regime structure (n_regimes / regime_threshold / regime_outlier_frac) so a duty-cycled point — a BTU meter, a lead pump, a status — is not scored as broken for cycling; a split is claimed only on mass and separation and temporal coherence, so a randomly-railed sensor is never mistaken for a schedule. Pooled outlier_frac keeps its meaning and is never masked. Flags: expected_freq, drop_outliers, regime_aware (off by default — sensorhealth opts in per role, since scoring a duty cycle as normal is the direction that could mask a fault).
  • Time / DST — timegrid: interval_hours, regularize (sort + de-duplicate the DST fall-back hour), localize (tz-localize resolving DST ambiguous/nonexistent), dst_anomalies. load_csv de-duplicates timestamps by default. See TIME-HANDLING.md.

Semantic model

  • Roles + mapping — model.roles.Role vocabulary; model.mapping.MappingProvider (alias + pattern → role). resolve.resolve(equip, roles) assembles a role-named frame. Flags: resample.
  • Entities + completeness — model.entities (Site/Equip/Point) with equipment-template completeness validation.
  • Brick interop — interop.mapping_from_brick / roles_from_brick (import); interop.to_brick and interop.site_to_ttl / site_from_ttl (export + whole-site round-trip). Flags: backend (auto/rdflib/minimal; rdflib via the [brick] extra).
  • Project-Haystack semantics — interop.haystack_tags / equip_haystack_tags (role→tags export) and, new in 0.6, interop.role_from_tags / roles_from_haystack / mapping_from_haystack (tag→role import) — closing the round-trip to Brick-level parity (every role recovers).
  • ASHRAE 223P — interop.semantic223.site_to_223 / site_from_223 map a site (roles + equipment) to/from a 223P-shaped RDF subset (connections, medium, points; ROLE_TO_223 quantity-kinds, broadened in 0.6 to the full plant/DX/refrigerant side; every role with a physical quantity maps, and the status/command roles are documented as intentionally unmapped). Flags: profile (minimal/full), include_relations. See ONTOLOGY.md.

FDD — fault detection & diagnostics

Rule engine (rules.base.Registry, rules.builtin.builtin_registry); each rule consumes a role-frame and returns a Finding. Run with registry.run(name, equip_refs, mapping, min_trust=…).

  • Air-side (G36 + PNNL Re-tuning) — simultaneous heat/cool, reheat (penalty + G36 minimization), SAT reset, overcooling (min-flow + severity), economizer / OA-fraction (incl. under-ventilation), night/weekend setback, duct-static, zone census, and unmet-setpoint hours (unmet_setpoint_hours — occupied space temp outside the heating/cooling band, the operator-facing comfort/capacity metric). Per-rule flags (e.g. threshold, min_oa_pct, occupied_only, tol_F).
  • Occupancy. The schedule-based rules (night_weekend_setback, overcooling_min_flow, overcooling_severity, reheat_penalty, zones_heat_cool_census, outdoor_air_fraction, reheat_minimization_g36 since 0.106, the DCV rules) take a trended OCCUPANCY point as the truth — it replaces the schedule — and otherwise use start_hour / end_hour / occupied_days (default Mon–Fri 07–18, a generic office assumption; camber.schedules.effective_occupied_mask). Since 0.98 damper_census reads each box's trended occupancy the same way (schedule fallback), and supply_air_control gates its fan-on hours on a trended occupancy point when one exists, with no schedule fallback (occupancy_gate); both report which gate applied in an occupancy_gate metric.
  • Fan-on only. outdoor_air_fraction judges fan-on samples by default (fan_gate=True: fan status, else fan speed, else airflow — camber.schedules.fan_on_mask): with the fan stopped the mixing-box temperatures read still air, not a mix. A unit with no fan signal is judged ungated and the finding says so. Its min_oa_pct is the unit's own design minimum (confirm it against the sequence — a 10 % minimum damper position can be a 1.6 % OA fraction), and min_oa_pct_by_month sets a seasonal one.
  • Terminal air temperatures. At a VAV/CAV box SUPPLY_AIR_TEMP is the box discharge (downstream of its reheat coil) and the entering primary air is mapped to MIXED_AIR_TEMP. reheat_penalty judges "reheat into cold supply" on the entering air; with only the discharge mapped the count is a lower bound (reheat warms it), so it is caveated and never reported as a confident ok. It also cross-checks the valve against the discharge-air rise (#63): a valve at ≥ 90 % with a median rise under 5 °F is declined (nothing counted or costed), and a shut valve with a ≥ 10 °F rise on a quarter of its samples is caveated. Where a box trends both the reheat demand and the measured position (0.98, #85), map the position as heat_valve_position: reheat_penalty and overcooling_min_flow then judge the heat delivered from the position (a valve stuck shut is 0 % open, not a reheat penalty), record it in valve_signal, and caveat a demand ≥ 90 % while the position reads ≤ 5 % on a quarter of the full-demand samples as a stuck or failed valve. On fan-powered boxes fan_heat_f (°F, or "auto": the median closed-valve lift, clipped to 0–8 °F) raises both valve-vs-discharge bounds by the box fan's own heat. box_type (0.100, #99: "single_duct", "parallel", "series") sets the caps on that estimate. A series box's fan always runs and mixes in plenum air, so its no-rise allowance is capped at 10 °F, while the big-rise allowance stays at 8 °F. The estimate is learned from the closed-valve samples, and a higher cap there would absorb a passing valve's heat.
  • Not overcooling. overcooling_severity excludes morning recovery (WARMUP, else the first recovery_hours of each occupied block) and fan-off free-floating, and reports a space below setpoint with its reheat ≥ reheat_saturated_pct open as a heating shortfall. The shortfall grade is the lesser of its depth tier and its share tier (0.98, #85: shortfall_share_pct, warn from 5 %, fault from 20 % of occupied samples at least warn deep), so a few deep cold hours a year grade info; share_pct applies the same gate to the overcooling tiers (off by default). overcooling_min_flow declines without AIRFLOW + AIRFLOW_SP (it can't test "at minimum").
  • Minimum vs active airflow setpoint (0.106, #186). airflow_sp is the box's active airflow setpoint (what the static-pressure census and the cohort rules read); its minimum has its own role, airflow_min_sp, which reheat_minimization_g36, zones_heat_cool_census, reheat_penalty and overcooling_min_flow prefer. With only airflow_sp mapped they read it as the minimum, as before, unless it swings with load while the airflow tracks it: then reheat_minimization_g36 and the census infer the minimum (its 5th percentile), say so, and the per-box rule rates no higher than warn. reheat_minimization_g36 does not count G36's high heating loop (the zone heating loop, zone_heat_loop, above 50 %; without it the valve at ≥ 90 % or the discharge ≥ 15 °F above the space) as a violation: a dual-maximum box raises its airflow there by design. zones_heat_cool_census rates the share of occupied hours with one zone heating while another cools (pct_hours_mixed); one box heating above its minimum is the per-box rule's.
  • Held setback (0.93, #43). A unit scheduled off at night still cycles on to hold its zones at the setback temperature, so night_weekend_setback no longer reads runtime alone: when the runtime test fails, a fan that cycles (mean duty below max_hold_duty_pct, 90 %, in the unoccupied hours it runs) while the zone sits at setback reads "effective (fan cycling to hold)". The zone is space_temp, else the return air while the fan runs; the setback is the trended heat_sp / cool_sp in unoccupied hours, else the configured unoccupied_heat_sp_f / unoccupied_cool_sp_f, else (heating only) a zone min_setback_depth_f (3 °F) below its occupied temperature. A fan running every unoccupied hour stays "MISSING", and the 5 % runtime floor (#57) still decides first.
  • Leaking valves and fan heat (0.93, #42). leaking_valve allows for the supply fan's heat (fan_heat_f, default 2 °F, G36's ΔT_SF) before calling a heating leak, gives a cooling leak no credit for it, judges fan-on samples only when a fan signal is mapped, and judges each coil on its own leaving-air sensor (heat_coil_leaving_temp / cool_coil_leaving_temp) when one is trended rather than on the supply air downstream of the fan (coil_sensor_fan_heat=True on a blow-through unit).
  • Out of reheat (0.93, #44). reheat_capacity_shortfall (terminal boxes only) flags a zone more than tol_f (1.5 °F) below its heating setpoint while its reheat valve is at or above reheat_saturated_pct (90 %) -- a capacity or airflow problem, not tuning. Occupied samples, less morning recovery, warm-up and fan-off samples; warn at 5 %, fault at 20 % of them, after 10 h. The setpoint is heat_sp, else the config's heat_sp_f (one value, or per box); with airflow + airflow_sp the finding says whether the box was short of air or of heat.
  • Dehumidification with reheat (0.93, #41). simultaneous_heat_cool sets apart the both-open hours that read as dehumidification with reheat: the fan running, the air leaving the cooling coil (COOL_COIL_LEAVING_TEMP) at least 2 °F below the supply air (the heat is added after the coil) and at or within 2 °F of the entering dew point (from OAT + OUTDOOR_RH and RETURN_AIR_TEMP + RETURN_AIR_HUMIDITY, the lower of the two; else a return humidity of 55 % or more). A coil leaving above the entering dew point is dry, so reheat after it still counts. Partial evidence (reheat after the coil but no humidity, or high humidity but no coil-leaving temperature) is a caveat and caps the finding at warn, never a fault. The rule parameter dehumidification declares the sequence (true accepts reheat after the coil unless a dew point shows it dry; false counts every both-open hour). A unit with none of these signals is judged as before, with a caveat.
  • Status duty. Event-logged status points are resampled to their time-weighted duty (camber.realio.load_status(how="duty")), so a runtime verdict such as setback does not change with the resample interval; how="any" keeps the old "on at any moment" bins. Ingest and a run's read from a store resample status the same way, and the start-counting rules count the fewest starts the duties allow, declining a below-limit verdict on a coarse trend that hid starts (0.104, #143; TIME-HANDLING.md).
  • Central plant & hydronic — chiller kW/ton efficiency, chiller staging + multi-chiller fleet over-staging, cooling-tower approach, condenser-water reset, CHW/HW pump (riding-curve + VFD-min), CHW reset + low-ΔT, boiler summer-lockout + short-cycle, hot-water pump warm-weather lockout (hw_pump_summer_lockout, 0.103). Flags include design targets (design_kw_per_ton, max_starts_per_day, …).
  • Control stability — control_hunting: flags a modulating output (valve/damper) that reverses direction excessively (unstable loop) by counting reversals per observed hour beyond a deadband (percent of stroke; data gaps are not counted as calm time). A trend too coarse to show the warn rate (15-min data can show at most 4 reversals/hr) is declined with a caveat, never called stable. Flags: warn_per_hr, fault_per_hr, deadband, gap_factor. supply_air_control: flags supply-air temperature that fails to track its setpoint (control/capacity fault; fan-on hours only, and since 0.98 only occupied ones when the unit trends occupancy). Flags: tol_F, warn_pct, fault_pct, occupancy_gate ("trended" default, "schedule", "off"). airflow_tracking: flags measured VAV airflow that fails to track its setpoint (stuck/undersized damper, failed actuator, starvation, bad flow sensor); since 0.107 fan-on hours only when a fan status or speed is mapped, and occupied ones when occupancy is trended. Flags: tol_frac, abs_tol, warn_pct, fault_pct.
  • Stuck actuator — actuator_stuck (0.98, #85; terminal boxes and fan coils): a damper or valve (roles: damper, heating-valve position else demand, cooling valve) that holds one position over occupied, fan-on hours is judged against what the zone asked for. A run of at least min_flat_hours is contradicted (can fault) when the damper is shut through occupied hours with no airflow (against AIRFLOW_SP or min_airflow; with neither, the occupied mode alone, caveated), part open while the zone runs warm_margin_f over its cooling setpoint, fully open while the zone sits satisfied_margin_f below it, or flat while its demand twin moves; it is unexplained (warn at most) when one value holds whole_day_share of a day while the demand, airflow setpoint, a setpoint or the zone temperature moves; anything else is consistent (a box at its minimum in a mild zone, a damper saturated open on a hot day). The ornl-frp-vav dataset declares it as its detector.
  • Peer/cohort — cohort.CohortDeviation (fleet rule): flags a unit running unlike its peers on a role (robust z of a mean/peak/load-shape summary). Shipped instances cohort_airflow, cohort_space_temp; construct your own for any role. Flags: k, summary, min_cohort, and since 0.98 (#85, opt-in): group_by_topology (compare only units behind the same air handler), normalise ("design_max": by design airflow; "reference": by each unit's declared known-good twin or period), summary="variability" and tail ("low" / "high"). Size normalisation alone cannot isolate a stuck box: on ornl-frp-vav only the box's own fault-free reference, or its damper's variability on the low tail, separated it. Since 0.106 (#186) min_abs_dev (1 °F for cohort_space_temp) keeps a tight cohort from flagging a small real difference, and a cohort_airflow cohort whose spread passes max_spread (0.3: box sizes or loads differ) reads info, not ok, when nothing stands out.
  • Economizer / free cooling — economizer_high_limit (excess OA above the high limit — not locked out; judged on measured OA/supply airflow when trended, else the MAT/RAT balance, else the damper; samples still below return air are left to a differential changeover; with no configured min_oa_pct only excess above a generous 50 % bound is scored and OA that is excess only against a generic 20 % minimum is declined, since it depends on the unknown design minimum), free_cooling_missed (mechanical cooling — cooling valve above active %, default 5 % — ran while OAT was cool enough for free; durations in hours; hours already on ~100 % outside air are an integrated economizer, not missed — the RCx economizer page's test, shared as camber.freecooling.integrated_economizer_mask; 0.98 records why: missed_cause tells a damper commanded open that did not deliver outside air from an economizer never commanded open; 0.102 judges fan-on hours only, from fan status, else speed, else airflow, and runs ungated, saying so, on a unit with none of them), static_pressure_reset (duct-static setpoint that doesn't trim with demand; a one-time step is reported as a step, not a reset — see camber.setpoint_reset). Flags: high_limit_f, min_oa_pct, min_damper, differential, active, min_range_inwc.
  • Packaged / DX & refrigerant-side (docs/FDD-DX.md) — compressor_short_cycle and compressor_staging (RTU/DX cycling + staging), heatpump_defrost (excess reversing-valve cycling), filter_fouling (filter ΔP at/above change-out), and chiller_approach_fouling (condenser/evaporator approach-temperature degradation — refrigerant-side fouling without refrigerant-pressure sensors). New equipment templates: RTU, HeatPump/VRF, DOAS, FCU.
  • Drift-detection families — catches equipment degrading over time against its own frozen, load-normalized baseline (the "is this slowly getting worse than it used to be, at matched load?" question), all on one shared engine (camber.chillerbaseline / camber.chillerdrift), each pairing a period statistic with a streaming sustained-shift CUSUM alarm and honest threshold labels (screening-grade magnitude floors vs. provisional-untuned CUSUM timing):
  • Chiller (CHILLER-DRIFT.md) — condenser/evaporator approach, liquid-line subcooling (charge), suction superheat (evaporator feed), condenser-water range.
  • Pump / hydronic (PUMP-DRIFT.md) — the distribution side at matched duty.
  • AHU / air-side (AHU-DRIFT.md) — supply fans, coils, filters, duct-static control, with a per-AHU co-movement roll-up (diagnose_ahu_drift) that names the locus.
  • VAV / zone-terminal (VAV-DRIFT.md) — the box's damper, airflow tracking, and reheat coil, with a per-box roll-up (diagnose_vav_drift) disambiguating box vs upstream starvation. Each family ships a physics simulator (ahusim / condensersim / evaporatorsim / vavsim) for ROC validation; camber.driftvalidation calibrates thresholds against labelled data. Period rules, run via Registry.run_periods with a frozen baseline store. Driven from a config's drift section or camber drift run|report|freeze|list (camber.driftrun) — one page per run via report.drift_report_html. Scoring is read-only toward the store; only freeze creates a reference and only the attributed accept_new_normal moves one, and an equipment nothing could evaluate is listed as not evaluated rather than diagnosed steady. See CLI.md.
  • Trim-and-Respond / G36 reset analytics (TR-RESET.md) — asks whether the plant's setpoint-reset logic does what ASHRAE Guideline 36 intends (camber.g36_reset engine). Five detectors: supply_air_reset_compliance (SAT held colder than the §5.16.2.2 OAT→SAT target — a reheat opportunity), sat/static_reset_effectiveness (does the reset setpoint actually trim and respond to its zone requests, or is it stuck / not-responding / not-trimming / diverging?), and sat/static_rogue_zone_census (fleet rules finding the one zone monopolizing the requests and dragging the whole reset). Screening / opportunity-grade. Since 0.106 (#186) a slow but correct reset under modest demand is not called unresponsive, the finding lists every failure mode, the static census lists a zone that is short of cooling as overloaded_zones rather than a rogue, the SAT censuses count G36's tier-1 request when a zone trends zone_cool_loop, and the cohort-starvation summary names every starved air handler.
  • G36 §5.16.14 engine — fdd_g36.run_g36_afdd scores AHU fault conditions FC1–FC15 with operating-state gating (an interval with a missing valve command is unclassified — no FC is scored there — rather than read as free cooling; unsorted/duplicate timestamps are sorted and de-duplicated first); cross-validated vs open-fdd 0.1.5, re-compared with open-fdd 4.4.9 on labelled LBNL runs (ECOSYSTEM.md), and accuracy-scored in the synthetic harness (VALIDATION.md). Since 0.91 it applies the G36 time filters: evaluation only while the supply fan runs (a run with no fan signal is declined), ModeDelay (30 min) after a fan start or zone-group mode change, AlarmDelay (30 min) persistence, and 5-minute averaging. It also scores cooling-only AHUs, with the heating-coil tests omitted. The registered rule g36_afdd runs it from a config (rules: ["g36_afdd"]). It gives one finding per AHU with every FC's hours and rate, and those findings flow into camber run and the RCx report. MAT and SAT stand in for the cooling-coil entering and leaving temperatures only on an AHU without a heating coil. Otherwise FC14 and FC15 are declined. FC8/FC9 hours that coincide with a confirmed FC14 are attributed to FC14, so a passing valve is reported as a leak, not under the free-cooling labels. Since 0.98 (#94) free cooling (OS#2) needs the OA damper open beyond its minimum position, not just both valves shut: a fan-on hour at minimum OA with both coils idle (a deadband hour, an unoccupied recirculation run) is OS#5, where only FC1-FC4 apply. The minimum is oa_damper_min, learned by default from the unit's mechanical-cooling hours at minimum OA. Since 0.99 (#95) heating (OS#1) likewise needs the OA damper at that minimum: a heating hour with the damper open beyond it is OS#5. Since 0.101 (#105) oa_damper_min_by_month gives a sequence with a seasonal minimum position its own minimum per month. Unoccupied operation is evaluated, as in G36; occupancy_gate="trended" limits the evaluation to a trended occupancy point's occupied hours. Parameters: heating_coil, min_oa_pct (enables FC6, the test for a damper stuck open at minimum OA), oa_damper_min, oa_damper_min_by_month, oa_damper_tol, occupancy_gate, mode_delay_min, alarm_delay_min, and the screening-grade warn_pct / fault_pct. Like every air-handler rule it declines a VAV box, heat pump, fan coil or plant (camber.rules.applicability), and an FC13 it reports is linked to a chilled-water plant short of setpoint in the same hours (chw_supply_tracking) as the likely upstream cause.
  • Sensor health / data trust — sensorhealth (physical bounds, cross-sensor consistency, per-role trust roll-up + trusted_roles gate), sensordrift (bias / drift / tracking vs a reference — fetch one with weather_source.oat_reference (NASA POWER by lat/lon), oat_reference_for (by address, via a keyless geocoder), or oat_reference_isd (station-precise, NOAA/ISD); see WEATHER.md), mapping_confidence. The runner's min_trust flag makes a rule decline when its inputs aren't trusted. Cross-sensor and provenance checks: flow-based mixing balance, copied points, gap-fill signatures, cross-unit identity, zone-vs-outdoor CO2, percent-mapped-as-airflow; see SENSOR-HEALTH.md.
  • Prioritization & lifecycle — rules.triage: rank_findings (severity, or a magnitude/cost key), group_findings (root-cause grouping), FaultRegister (new/ongoing/resolved across runs). Persistent, cross-process: faultlifecycle.FaultLifecycle — a fingerprint-keyed fault store with an assignment/status workflow, SLA/aging tracking, and atomic JSON persistence. Flags: magnitude_key, actionable_only, reopen_on_recurrence, auto_resolve_absent.
  • Fault economics — fault_economics: per-fault annual $ impact → rank by money. Flags: params (assumptions), models, min_severity (via rank_by_cost). Triage-grade DEFAULT_MODELS cover simultaneous heat/cool, reheat, chiller/tower/pump efficiency, duct-static, boiler cycling, and the Trim-and-Respond reset family (SAT-below-target and static/SAT reset-not-trimming → reheat/fan waste; not_responding/stuck/diverges are comfort/indeterminate → uncosted by design). Drift findings are also uncosted by design — a drift is a leading recommission indicator, not a spend (its magnitude is a condition-space residual, not a priceable energy quantity). Every estimate carries its basis + assumptions and returns uncosted (never a fabricated figure) when the sizing it needs is missing.
  • Ventilation (ASHRAE 62.1) — ventilation.assess_62_1 (Ventilation Rate Procedure: required vs delivered OA, deficit) and assess_dcv (is OA raised when CO₂/occupancy is high, judged on occupied, non-economizing samples via economizer_active_mask; the CO₂ lift taken within the hour of day since 0.93), with the VentilationRateProcedure / DemandControlledVentilation rules, the DcvSystemVerification fleet rule (zone CO₂ joined to the serving air handler's OA via the served-by topology) and Role.OA_AIRFLOW (the OA damper judges where the flow is missing; a declared 100 % outdoor-air unit falls back to supply airflow or fan speed). CO₂ adequacy (co2_ventilation, and the fleet twin co2_ventilation_system) leaves economizer-mode hours out of its over-ventilation verdict (iaq.economizer_mode_mask) and, since 0.107, judges a trended occupancy point's occupied hours in place of the assumed schedule. Flags: space_type vs rp/ra, ez, aggregate, co2_setpoint, oa_floor, min_modulation, min_lift_ppm. See VENTILATION.md.
  • Accuracy + CI gating — eval.benchmark + validation.metrics_with_ci (Wilson CIs), and eval.check_against_baseline to gate accuracy (TPR/FPR/diagnosis) against a committed baseline in CI (--json / --gate / --tol / --update-baseline). See VALIDATION.md.
  • Validation dossier — camber.dossier / camber validate aggregates all four validation tracks (synthetic, generated-fleet, real-data FDD, real-data M&V) into one text/HTML/JSON credibility artifact: live-recomputed pure tracks + cited real-data results with Wilson CIs and honest boundaries. See VALIDATION.md.

Sequence-of-Operations conformance

soo — a declarative clause engine (gated predicates over roles, JSON-authorable) measuring operated-vs-designed behavior as a conformance %, with soo_library (packaged ASHRAE G36 clauses). Flags: persistence window, per-class spec.

M&V — measurement & verification

See MANDV.md. Change-point models (mandv.models, 2P–5P + zero variants), a TOWT variant (mandv.towt, after LBNL's Mathieu et al. 2011), fit statistics + G14 fractional savings uncertainty (mandv.stats), CUSUM (mandv.cusum), weather normalization (mandv.weather), normalized annual savings (mandv.normalized), non-routine adjustment (mandv.nonroutine), Option-B retrofit isolation (mandv.retrofit_isolation), the CalTRACK 2.0 Daily, Billing and Hourly methods (mandv.caltrack_daily, mandv.caltrack_hourly), a degree-day baseline on period-mean temperature (mandv.degreeday, HDD/CDD regression with an auto-fit balance point; the variable-base form on bills is mandv.basetemp), IPMVP Option A (mandv.option_a, measured Δparameter × stipulated duty), and Option D-style modeled savings (mandv.rc_model, a 1R1C or 2R2C grey-box calibrated to metered energy — grid-τ + OLS, G14-gated — run as-found vs as-corrected for a modeled pre-implementation saving, with multi-zone stacked-OLS calibration and an optional EnergyPlus cross-validator (interop.energyplus); see OPTION-D.md). CAMBER now covers IPMVP Options A/B/C/D. Flags: confidence, exclude_non_routine, model kinds, aggregate, balance_point, interval.

Streaming / online

Incremental monitors for a live BAS feed — bounded state, O(1)–O(window) per sample. See STREAMING.md and FORECAST.md.

  • Online M&V — mandv.online.OnlineCusum (incremental tabular CUSUM of savings/waste vs a baseline model → savings-erosion alarm) and RollingAnomaly (rolling median/MAD residual z-score). Flags: limit, slack, window, k, min_samples.
  • Online FDD — rules.online.OnlineFDD: sliding trailing-window rule evaluation emitting a Transition only on a verdict change (no per-sample re-alert), per-equipment isolation. Flags: window, eval_every, min_samples, emit_ok.
  • Forecasting + learned-normal anomalies — forecast.seasonal_forecast (time-of-week shape + additive drift, no ML dep), backtest (MAE / MAPE / CV(RMSE) honesty check), forecast_anomalies (robust residual band → FDD signal). Flags: drift_window, k, test_frac.

Commissioning (RCx / MBCx)

rcx: functional_test (FPT pass-rate), before_after (MBCx persistence across an intervention date), track_measures (measure register → verified/regressed/inconclusive/insufficient).

Money & compliance

  • Tariffs — tariff (URDB-shaped: TOU energy + tiers, TOU/flat demand, ratchet, fixed → monthly + annual bill), tariff.validate_bill (vs actual invoices, MAPE + per-month status), interop.openei (URDB fetch), [tariff] PySAM bridge. Flags: tol_pct.
  • ECM finance — finance: payback, NPV, IRR, SIR with escalation / O&M / salvage.
  • Demand & peak — demand: peak + drivers, load factor, baseload, night/weekend baseload anomaly, peak-shave $ value. Flags: near_peak_frac, start_hour/end_hour, target_kw.
  • BPS compliance — bps: site_eui, emissions_intensity, assess_bps / assess_eui (compliant?, margin, penalty exposure). Limits are caller-supplied (no hard-coded legal values).

Grid-interactive (GEB) & carbon timing

Beyond using less energy — quantify shifting and shedding load, and the carbon cost of when power is used. Advisory analytics (read-only toward the BAS). See GEB.md and CARBON.md.

  • Demand response & flexibility — geb.demand_response (shed kW/kWh/% + rebound vs a baseline), geb.flexibility (sheddable load above baseload, peak-to-average headroom). Flags: rebound_hours, baseload_pct.
  • Load timing — geb.carbon_aware_shift (CO₂ saved shifting load dirty→clean hours) and geb.operation_score (load timing vs a price/carbon signal, rearrangement-inequality bounds).
  • Hourly / marginal Scope-2 — carbon_hourly.hourly_emissions (time-varying factor → co2e, effective factor, timing premium) and marginal_vs_average (load-shift value uses marginal; reporting uses average). Flags: unit_kg_per_kwh.
  • OpenADR export — interop.openadr.to_openadr_report: map a demand_response result to an OpenADR-3.0-shaped report payload for a DR program.

Domain analytics

comfort (Std-55 PMV/PPD), iaq (CO₂ ventilation adequacy), cost, carbon, water (irrigation / cooling-tower / leak), loadprofile, pv (+ interop.pvlib_bridge, [pv]), interop.psychro (PsychroLib, [psychro]), lighting. Plus: - Schedule inference — schedule.detect_schedule / compare_schedule: the actual weekly operating schedule from interval load vs a stated one (setback opportunity). SCHEDULE.md. - Change-point detection — changedetect.detect_level_shifts: when a signal's mean shifts (MBCx persistence/regression). CHANGEDETECT.md. - Free-cooling opportunity — freecooling.free_cooling_opportunity: missed economizer hours → recoverable kWh/$. FREECOOLING.md. - Load disaggregation — disaggregate.disaggregate_load: baseload / weather / other split. DISAGGREGATE.md.

Advisory & synthesis

Read-only, human-in-the-loop layers on top of the findings: - ASO — aso.recommend / recommend_findings: an actionable finding → a suggested setpoint/ sequence change, grounded (cites the rule + G36/PNNL), never a BAS command. ASO.md. - Action plan — actionplan.build_action_plan: findings + fault_economics ($/yr) + aso, ranked worst-dollars-first; embeds in the audit report + config runs. ACTIONPLAN.md. - Health scorecard — scorecard.build_scorecard: per-category scores + an overall A–F grade. SCORECARD.md.

AI-assist (advisory, provider-agnostic)

Dependency-light, advisory-only, read-only toward the BAS. The LLM path is fully provider-agnostic — no vendor named, no SDK, no network (an AST guard enforces it) — and everything works with no LLM wired via deterministic fallbacks.

  • Assisted point mapping — mapping_assist.suggest_roles / review_unmapped: suggest roles for unmapped tags (a human-confirmed review list; never mutates a MappingProvider). FeatureSuggester (numpy baseline: string + unit + physical-range fit), optional MLSuggester ([ml] extra, scikit-learn, no pretrained weights), and LLMSuggester (agent seam; proposals validated + re-scored deterministically). See MAPPING-ASSIST.md.
  • Grounded explanation & Q&A — agent.explain / agent.ask: cited, plain-language explanations and NL Q&A over the findings/costs/recommendations/scorecard/completeness/history/mapping. A Context of citable Facts (order-stable ids), number-traceability verification (agent.check), a deterministic template fallback, and an injected complete(prompt, **opts) seam (client_from_callable). See AGENT.md.
  • Portfolio triage — agent.facts_from_fleet builds grounded facts from a report.FleetReport (per-building EUI / faults / recoverable $), and build_context(fleet=…) makes a multi-site context, so ask/explain answer portfolio-wide ("which building is worst?").

Storage

store.ParquetStore — entity-keyed, hive-partitioned (site/year) Parquet with tag-filtered reads, rollups, retention pruning, year-partition pruning + column projection + cached catalog. See SCALE.md. Flags: read_long(columns=…, start/end), rollup(freq, agg), prune(before_year).

Reporting, integration & API

  • Audit — report.AuditReport (ASHRAE/ACCA Standard 211, text/HTML) with prioritized findings.
  • Portfolio rollup — report.build_fleet_report (cross-sectional EUI benchmark + fault rollup, ranked by recoverable $). Flags: price, loads, peer_median_eui, top_n.
  • Outbound — integrate: CMMS work-orders (finding_to_ticket / findings_to_tickets → neutral dict), notifiers (dispatch_findings over webhook_transport / email_transport, with slack_payload / teams_payload formatters; severity filter + fingerprint dedupe), and findings/ metrics export (export_findings: CSV / Parquet / JSON). All opt-in, from the findings layer — never writing to the BAS. Flags: channel, min_severity, dedupe, dry_run, format, flatten_metrics. See INTEGRATIONS.md.
  • Charts + dashboard — the full visualization pattern catalog A–J: readiness ribbon (A), fault-annotated multi-trend (B), load carpet (E), data-quality dashboard (I), OAT cloud-shape scatter (D, oat_scatter), templated diagnostic scatters (G, diagnostic), rules as a chart engine (J, evidence — every rule renders its own proof), cohort small-multiples (C, cohort), M&V savings with uncertainty (H, savings), load profiles / load-duration curves (F, loadprofile_chart), plus the legacy scatters/CUSUM/energy-signature. report.build_dashboard assembles them into one self-contained HTML (matplotlib inlined, no web framework), embeds each finding's evidence (rules=), and offers a brush-able inline-SVG scatter (interactive=True). Flags: sections, rank_by, top_n, normalize, rules, evidence, interactive. See VISUALIZATION.md.
  • Read-only API + live web UI — api.server (camber serve <store> or python -m camber.api.server <store> [port]): GET /about /health /facilities /points /history, plus a live vanilla-JS dashboard at /ui (facility/equip/role selectors + a synchronized multitrend, brush-linked via window.CAMBER, polling for fresh data; a date range and brush-to-zoom over a min/max-thinned whole span, 0.103). Read-only, localhost-bound, CSP-locked, no framework; a Host allowlist against DNS rebinding and token auth (--auth token, 0.103; the loopback default since 0.107, --auth none opts out). Env: CAMBER_STORE / CAMBER_API_HOST / CAMBER_API_PORT / CAMBER_API_ALLOWED_HOSTS / CAMBER_API_AUTH / CAMBER_API_TOKEN. See VISUALIZATION.md.

Orchestration & distribution

  • Config-driven runs — config: one JSON config (source → mapping → equipment → rules → report) runs a whole analysis: python -m camber.config run.json.
  • CLI — the camber console script: run / report / explain / ask / fleet / charts / validate / serve / drift / edge subcommands, with a vendor-neutral --llm-cmd seam for the agent. See CLI.md.
  • Plugins — plugins: third-party rules / ingest adapters / report formats discovered via Python entry points (camber.rules / camber.adapters / camber.reports) or registered in-process, duck-typed against the existing protocols with per-plugin error isolation. See PLUGINS.md.
  • Distribution & deployment — slim multi-stage Docker image + compose bundle (DOCKER.md), PyPI (camber-toolkit) + GHCR via the tag-driven release workflow, CI on 3.10/3.11, and reference Kubernetes / conda-recipe manifests (deploy/). See DEPLOY.md.

See also: ARCHITECTURE.md, ECOSYSTEM.md (fork-vs-depend analysis), and the ROADMAP.