Ecosystem — where to leverage existing OSS instead of reinventing
CAMBER deliberately ships a dependency-light, zero-config core (stdlib + numpy/pandas/pyarrow/matplotlib). For several heavier or highly-standardized pieces, mature open-source libraries already exist. The strategy is not to fork them wholesale — it is to integrate them as optional extras so the core stays light while users who need depth can opt in, and so we don't reinvent well-trodden wheels.
flowchart TD
core["CAMBER core (role model + FDD/M&V/RCx + Std-211 + ParquetStore)"]
core -- brick extra --> brick["rdflib / py-brickschema"]
core -- haystack extra --> hay["phable / pyhaystack"]
core -- pv extra --> pv["pvlib"]
core -- psychro extra --> psy["PsychroLib"]
core -- tariff extra --> pysam["NREL PySAM"]
ee["OpenEEmeter / eemeter"] -- cross-check --> core
better["LBNL BETTER"] -- cross-check --> core
openfdd["open-fdd"] -- G36 cross-validate --> core
openfdd -- shared M&V vectors --> core
openfdd -- package / historian files --> core
volttron["Eclipse VOLTTRON"] -- data source --> core
Own the distinctive core; integrate mature libraries as optional extras and cross-checks, never forks.
License notes: items marked ✓ were verified during research; items marked "confirm" are from general knowledge — check the repo's LICENSE before depending.
Candidates by area
| Area | Project | License | What it gives us | Recommendation |
|---|---|---|---|---|
| M&V | OpenEEmeter / eemeter (now opendsm) |
Apache-2.0 ✓ | CalTRACK-compliant normalized metered energy / avoided-energy at meter scale | Align + optional backend. Keep our lightweight change-point/TOWT core; offer an [eemeter] path and match CalTRACK terminology for credibility. |
| M&V weather | eeweather | Apache-2.0 (confirm) | NOAA station matching / normalization-year weather | Optional, complements our TMY/EPW loader. |
| Ontology (Brick) | py-brickschema + rdflib | BSD-style / BSD-3 (confirm) | Robust Turtle/RDF parsing, Brick reasoning/validation | Optional [brick] extra using rdflib for full models; keep our minimal zero-dep parser as the default. |
| Ontology (modeling) | BuildingMOTIF | BSD-3 (confirm) | Template-driven Brick/223P model creation + SHACL validation | Watch / optional for Phase-2 full-ontology work. |
| Haystack client | phable (modern, zero-dep) / pyhaystack | confirm / Apache-2.0 ✓ | A real hisRead/Zinc client |
Optional [haystack] extra wired as the transport our adapter already accepts — no need to hand-roll the HTTP/Zinc layer. |
| PV modeling | pvlib-python | BSD-3 ✓ | Rigorous PV performance/irradiance modeling | Optional [pv] extra for serious PV; keep our performance-ratio basics dep-free. |
| Psychrometrics | PsychroLib | MIT ✓ | ASHRAE psychrometric properties (humidity ratio, enthalpy, wet-bulb) | Optional; back any psychrometric needs (e.g. App-C solar-MRT comfort, latent loads). |
| Controls / agents | Eclipse VOLTTRON | Apache-2.0 ✓ | BACnet/Modbus drivers, historians, an agent platform | Interoperate, don't vendor — read its historian via our SQL adapter / its telemetry via MQTT; study its driver framework. Too heavy (ZMQ/gevent) to import. See note 9 below. |
Related tool: ECAM
ECAM (Energy Charting and Metrics) is an Excel-based charting and M&V tool for interval and trend data, now listed by Lattice Energy Works. PNNL's Building Re-tuning program uses it for whole-building interval-data analysis (Interval Data Analysis with ECAM, PNNL-20495). It is a related tool, not a dependency: CAMBER neither ships nor wraps it, and its own load profiles, carpet plots and change-point M&V cover the same ground in Python. See References for the Re-tuning material CAMBER links to.
The strategy: optional extras, not forks
- Core stays zero-dep. Today's
camber/runs on numpy/pandas/pyarrow/ matplotlib only. None of the above becomes a hard dependency. - Add
pyprojectextras so capability is opt-in, e.g.:pip install camber-toolkit[brick,haystack,pv]. Each extra wires a mature library behind an interface we already have (the Haystack adapter's injectable transport, a PV backend, a Brick parser swap). - Why integrate rather than fork: these projects are maintained, tested, and standards-tracking (CalTRACK, Brick, Haystack, PV). Forking would mean owning that maintenance; depending optionally gets the value without the burden, and keeps CAMBER's distinct contribution (the vendor-neutral role model + the unified FDD/M&V/RCx pipeline) clear.
- Where we keep our own: the role/mapping/entity model, the FDD rule engine and triage/lifecycle, the Std-211 reporting, and the Parquet store are CAMBER's reason to exist — no equivalent single OSS package combines them.
Near-term integration picks
- Brick via rdflib (
[brick]) — DONE.camber.interop.brickhas an rdflib backend (backend="rdflib", auto-selected when installed); the zero-dep minimal parser remains the default. Verified identical to the minimal parser on the LBNL model and able to parse models it can't (rdf:type, full IRIs).pip install camber-toolkit[brick]. - Haystack via phable/pyhaystack (
[haystack]) — DONE.camber.ingest.haystackprovidesclient_transport(his_read)to wire any maintained client into the adapter's transport seam in one line, plus a dependency-freehttp_json_transportfor token/JSON-capable servers; the decoders handle both the v3 and Hayson JSON encodings. The[haystack]extra pulls phable (Python >= 3.11) or pyhaystack (older). -
M&V: align with CalTRACK / eemeter — DONE.
mandv.caltrack_dailyandmandv.caltrack_hourlyimplement the CalTRACK 2.0 Daily, Billing and Hourly methods (0.105), matching eemeter 2.10 undereemeter_compat=True;mandv.caltrackkeeps CAMBER's own Option-C NMEC workflows (change-point daily, TOWT hourly). docs/MANDV.md maps the terminology, notes where the methods differ, and gives the eemeter cross-check recipe (no dependency added). -
Tariffs / OpenEI URDB — DONE (hybrid). A native, dependency-free engine (
camber.tariff) bills an interval load against a URDB-shaped rate (fixed, TOU energy + tiers, TOU/flat demand, ratchet) and covers the common cases;camber.interop.openeifetches + maps a URDB rate (stdliburllib). The URDB API needs a free key — get one at https://openei.org/services/ and export it asOPENEI_API_KEY(fetch_urdb_ratereads it automatically); never hard-code or commit the key. For exotic rates and cross-checking, an optional[tariff]extra bridges to NREL PySAM's battle-testedUtilityRate5(camber.interop.tariff_nrel) — BSD-3-Clause, but a ~47 MB binary, so it stays an opt-in extra, never a core dependency. Same own-it + cross-check-the-heavyweight pattern as M&V/eemeter. (NREL REopt's tariff logic is also BSD-3 but Julia-native — reachable via the REopt API, not embedded.) -
LBNL BETTER (change-point M&V + targeting) — DONE (cross-check). LBNL's BETTER analytical engine (
better-lbnl-oson PyPI; modified BSD + U.S. DOE clauses) fits change-point models to monthly energy-vs-temperature and benchmarks/targets retrofits. CAMBER already has its own change-point M&V, so the value is cross-validation:camber.interop.better.compare_changepointruns CAMBER and BETTER on the same series and reports model-order / baseload / R² agreement — two independent engines agreeing is a stronger baseline than one. Optional[better]extra, imported lazily; the core needs none of it. Same own-it-+-cross-check pattern as eemeter. -
pvlib (PV modeling) — DONE (extend + cross-check).
camber.pvmonitors a PV array against a measured plane-of-array resource with a flat performance ratio. For users who must estimate generation from weather,camber.interop.pvlib_bridge([pv]extra, BSD-3) adds what pvlib does well and CAMBER deliberately doesn't:poa_from_ghitransposes horizontal irradiance (GHI/DNI/DHI) onto the array plane, andpvwatts_expected_kwhapplies a temperature-derated PVWatts yield.compare_expectedputs CAMBER's flat-PR estimate beside pvlib's temperature-corrected one (e.g. a ~0.92 ratio at a 55 °C cell — the derate the simple model omits). Lazy import; core dep-free. -
PsychroLib (psychrometrics) — DONE (extend + cross-check). CAMBER computes wet-bulb where it needs it (cooling-tower approach) with Stull's dependency-free closed form.
camber.interop.psychro([psychro]extra, MIT) bridges to PsychroLib's exact ASHRAE-formulation properties (psychrometrics→ wet-bulb, dew point, humidity ratio, enthalpy) andcompare_wetbulbvalidates the Stull approximation against the exact value (~1 °F agreement at hot/dry CZ15 conditions). Lazy import; core dep-free. -
Network ingest protocols — DONE (optional extras). Read-only adapters for Modbus (
[modbus]/pymodbus, BSD-3), MQTT/Sparkplug ([mqtt]/paho-mqtt under EDL-1.0), BACnet incl. experimental BACnet/SC ([bacnet]/bacpypes3, MIT), and OPC-UA ([opcua]/asyncua). All lazy-imported, read-only by construction, and steered behind the historian/SQL/Haystack default posture. We depend on the permissive libraries directly and keep the LGPL ones (asyncua — and BAC0, which we don't use) as optional, dynamically-imported deps only, never vendored/bundled, so CAMBER's own code stays Apache-2.0. See SECURITY.md + INGEST-PROTOCOLS.md. -
VOLTTRON — interoperate, don't vendor. Eclipse VOLTTRON (Apache-2.0) is a full ZMQ/gevent agent platform, not a light library: its modules assume a live message bus + agent runtime (its BACnet driver even needs a separate proxy process), so it can't be imported into a dependency-light tool. CAMBER instead treats a VOLTTRON deployment as a data source — point the SQL adapter at its SQLite/PostgreSQL historian, or the MQTT adapter at its forwarded telemetry — and mines its driver framework (registry-CSV point maps, scrape scheduling, COV) as a design reference. No
volttron-*dependency; if any snippet is ever adapted, its Apache-2.0 NOTICE/attribution is retained.
For open-fdd users
If you come from open-fdd (MIT), these are the four places where the two projects meet. Every result is labelled with the engine and version that produced it, and neither project imports the other's code: the exchange is files and processes.
| What | Where | Engines and versions |
|---|---|---|
G36 cross-check: CAMBER's g36_afdd and open-fdd's two rule engines scored side by side on the same labelled open data |
examples/openfdd_crosscheck (harness, role map, tolerance profiles), results/, and the write-up below |
CAMBER g36_afdd (0.99); open-fdd pandas engine (PyPI 4.4.9) and SQL engine fdd_cli, both from commit 32a6d44 |
| Reading open-fdd data: an importer for the building package and the historian Parquet layout, a versioned role crosswalk, and a draft findings-exchange JSON that hands CAMBER's results back | INTEROP-OPENFDD.md, with the findings-exchange schema (draft 0.1) | CAMBER camber.interop.openfdd (0.99, provisional); open-fdd openfdd_package_v1 and the crosswalk's pinned docs commit |
| Shared M&V test vectors: change-point and bill cases with expected outputs, checkable without installing CAMBER | examples/mv_vectors and the section below |
CAMBER M&V (0.98); schema mv_vectors/1; any engine that reads CSV or Parquet |
| Open questions for the open-fdd author | Questions for open-fdd | — |
The questions, in short:
- Time zone and units: a stable, documented place for a building's IANA zone and unit system.
- Historian layout: whether the Parquet layout is stable enough for an external reader, and
whether
equipTypecould travel with it. - Vocabulary: whether point names seen in packages but not in the quick reference belong to the vocabulary, and which SQL roles they map to.
- Weather: whether a weather folder always carries a points map.
- Commands: a declared way to tell 0/1 commands from percentages.
Corrections and answers are welcome on #22.
Cross-validation: G36 fault conditions vs. open-fdd
Our G36 AHU fault engine (camber.fdd_g36) is a clean-room implementation of the
ASHRAE Guideline 36 §5.16.14 fault conditions. To check it independently, we
cross-validated it against open-fdd (MIT) —
a separate, independently-authored G36 FDD library — on the public LBNL simulated
single-duct AHU dataset (the CC-BY dataset the examples/lbnl_fdd
example uses). Running on a public, downloadable dataset makes this corroboration
fully reproducible and shareable, with no client data involved.
Two comparisons are described here. The first pinned open-fdd 0.1.5, the last release that still exposes the classic FC1–FC16 per-fault API; its subsections describe that comparison only. Current open-fdd is a different engine and was re-compared in 2026-09, on labelled open data and with a reusable harness: see Re-compared against open-fdd 4.4.9 below, including where the 0.1.5 comparison no longer applies.
Since 0.91
run_g36_afddalso applies the G36 §5.16.14 time filters by default: fan-on gating, ModeDelay, AlarmDelay and 5-minute averaging. The 0.1.5 comparison below compares the per-interval fault equations. To reproduce it, passmode_delay_min=0, alarm_delay_min=0, avg_window_min=0, and passfan_gate="none"when the frame has no fan signal.
What was runnable
With the signals available in this dataset, the runnable common set was FC2, FC3, FC5, FC8, FC10, FC12. The rest were unrunnable in both tools for lack of inputs, not because of any Camber limitation:
- FC7, FC9, FC11, FC13 need a supply-air-temperature setpoint trend, which this dataset didn't include.
- FC14, FC15 need coil entering/leaving temperatures, also not trended.
Neither tool can evaluate a fault whose required inputs aren't present, so these were excluded symmetrically.
Result — the equations agree
On a common denominator (the intersection of each rule's applicable rows, so both tools are scored over exactly the same hours):
- FC5, FC8, FC10, FC12 match open-fdd to 0.00 percentage points on every AHU fault scenario tested. The fault equations are equivalent; the only differences observed came from how each tool frames its denominator (see the convention below).
- FC3 had a lone residual of ≤ 2.3 pts, an immaterial mixed-air-bounds edge artifact on a fault that fires in neither tool (it is a boundary-rounding difference in the "applicable" count, not a disagreement about any flagged hour).
Conclusion: Camber's G36 implementation is independently corroborated — a second, independently-written G36 library computes the same fault equations and, on a like- for-like denominator, the same fault rates.
Convention: operating-state gating vs. single-signal gating
The one systematic difference between the two tools is which hours each fault is considered "applicable" — the denominator of the fault percentage, not whether a fault fires or where:
- Camber gates each fault by its G36 operating-state classifier. We classify
every interval into an operating state OS#1–OS#5 from the heating/cooling
valve commands plus the OA-damper position (
classify_os→ heating / free-cooling / mechanical+economizer / mechanical+min-OA / none of these), and evaluate each FC only in the operating states G36 §5.16.14.9 lists for it (OS_FAULTS). So FC10 ("OAT/MAT should track in 100% economizer"), for example, is scored only over the hours the AHU is actually in that economizer state. - open-fdd gates on a single-signal threshold. Each fault is applied over the rows selected by one signal (e.g. fan running, or a single mode flag), without the full multi-signal operating-state classification.
These two definitions select different sets of "applicable hours," which changes the reported fault magnitude (the percentage) — but not which faults fire, nor the hours at which they fire (the fault equations and their per-row results are the same). That is exactly why the cross-validation matches to 0.00 pts once both tools are put on a common denominator.
Camber's operating-state gating is the chosen convention, deliberately, because it is the more G36-faithful definition of when a fault is applicable: G36 ties each fault condition to the operating state(s) in which it is meaningful, and we honor that mapping directly. Tradeoff: operating-state gating yields a narrower applicable set than a single-signal gate (an FC is counted over fewer hours — only those in its valid operating states), so Camber's denominators are smaller and its percentages are computed over a stricter, more specific population of hours. We consider that the correct, standard-aligned behavior; the single-signal framing is broader but less precisely tied to the standard's intent.
Since 0.98 (#94) free cooling (OS#2) also needs the OA damper open beyond its minimum position. An interval with both valves shut at minimum OA is OS#5, where only FC1–FC4 apply. Before 0.98 it was OS#2. Since 0.99 (#95) heating (OS#1) likewise needs the OA damper at its minimum, judged against the same
oa_damper_minandoa_damper_tol. A heating interval with the damper open beyond it is OS#5. The 0.1.5 comparison above used the valves-only reading for both states, and no single setting reproduces it any more.oa_damper_min=-100counts every damper reading as open: it restores the valves-only free-cooling reading, but every heating interval becomes OS#5.oa_damper_min=100keeps every heating interval in OS#1, but leaves no free cooling.For cross-tool comparison,
run_g36_afdd(..., comparability=True)additionally emits a single-signal-gated (input-validity) fault % alongside the default operating-state-gated %, so a reviewer can reconcile Camber's numbers with an open-fdd-style denominator without changing Camber's default outputs. Seecamber/fdd_g36.py.
Reading open-fdd data (provisional, 0.99)
CAMBER can now run its drift, M&V and sensor-trust checks on data collected at the open-fdd edge.
camber interop openfdd ingest reads an open-fdd building package (openfdd_package_v1) or its
historian Parquet layout into a CAMBER store or portfolio workspace:
- The site time zone and the unit system are required, never guessed.
- Columns map through a versioned role crosswalk, and every unmapped column is reported.
camber interop openfdd findingshands the results back as an engine-labelled JSON document.
The boundary is files and processes only: no open-fdd code is imported or copied, and nothing is written back. See INTEROP-OPENFDD.md.
Re-compared against open-fdd 4.4.9 (2026-09)
open-fdd has since become a platform. Its fault conditions run as SQL rules in a DataFusion
engine (fdd_cli), and the pandas rule cookbook on PyPI is a second implementation. We
re-compared both engines against CAMBER's g36_afdd on labelled open data with a reusable
harness,
examples/openfdd_crosscheck
(#22, item 1). The rules we follow:
- Files and processes only. CAMBER does not import open-fdd. The pandas engine runs in its own
venv as a subprocess. The SQL engine runs as
fdd_cliin a container built from the pinned commit, with no network and read-only data mounts. No open-fdd code or SQL is copied. - Each engine speaks for itself. Every number below is labelled with its engine and tolerance profile. Verdicts are never merged.
Versions.
- open-fdd: commit
32a6d44(VERSION 3.5.58). PyPIopen-fdd4.4.9 was released from this commit, and the wheel'sopen_fdd/tree is identical to it. The SQL engine isfdd_clibuilt from the same commit. The newest repository tag, v3.2.8, is older. - CAMBER:
g36_afddon the integrated 0.101 code (with #94, #95, #97 and #105; the results files record the version string 0.100.0 and the commit), at the G36 defaults plus each dataset's run-template parameters:min_oa_pct1.6 onlbnl-sdahu, which enables FC6, and onlbnl-ddahuthe unit's seasonal minimum OA and damper position (#97, #105, below). - Role mapping and profiles: role map version 1, profiles version 2.
- Python requirement: the 4.4.9 wheel declares Python ≥ 3.10 but needs 3.11 or newer (it
imports
enum.StrEnum).
Data. lbnl-sdahu, the LBNL single-duct AHU (CC-BY-4.0, fetched from the publisher, never
redistributed), full catalog subset:
- 13 labelled faulted runs: 4 stuck OA damper, 4 supply-air-temperature bias, 4 partly stuck cooling valve and 1 valve leak;
- 1 fault-free run.
lbnl-ddahu, the dual-duct AHU, is scored separately, with its default subset: 2 stuck OA damper
runs and 1 fault-free run. lbnl-fcu is not part of the run: neither engine applies its G36 AHU
fault conditions to a fan-coil unit. CAMBER declines the equipment class, and open-fdd scopes its
FC rules to ahu.
Scoring. Each labelled run is one case. An FC fires on a run when its fault hours reach 5 % of that engine's own evaluated hours, over at least 24 evaluated hours. An FC an engine could not run is listed as not evaluated with the reason. It is left out of that FC's counts, never counted as a miss.
The engines' own alarms are scored separately, and they differ by design:
- open-fdd's status
FAULT(pandas) or non-zero fault hours (SQL) means at least one confirmed fault sample; - CAMBER flags an FC at 5 % of at least 24 applicable hours.
On annual runs both open-fdd engines raise an alarm on every run, fault-free included, in both profiles. A shared duration rule is therefore needed to compare detection at all.
Rates carry Wilson 95 % intervals. With one fault-free run per unit, every per-run FPR rests on n = 1, so read the intervals, not the point rates.
Tolerance profiles for open-fdd.
- Own defaults: ε 1.15 °F, fan heat 0.55 °F.
- G36: the Table 5.16.14.7 tolerances CAMBER uses (ε SAT/RAT 2 °F, MAT/OAT 5 °F, coil CCET/CCLT 5/2 °F, fan heat 2 °F).
- Pandas engine: passed as rule parameters.
- SQL engine: passed through its supported override,
rule_tuning/defaults.yaml. Four SQL rules (FC9, FC11, FC14, FC15) read a tolerance that file cannot set (see "SQL engine" below). They run at their defaults in this profile rather than with a partial override.
Only tolerances change. Delays and the damper and valve thresholds that select operating states stay at each engine's defaults.
lbnl-sdahu, one case per run. TPR, Wilson 95 % interval and detected/evaluated runs. Every
FPR is 0/1 except where the "not evaluated" table says otherwise.
| FC | CAMBER g36_afdd |
pandas, own defaults | pandas, G36 tol. | SQL, own defaults | SQL, G36 tol. |
|---|---|---|---|---|---|
| FC2–FC4 | 0.00 [0.00–0.23] (0/13) | 0/13 | 0/13 | 0/13 | 0/13 |
| FC6 | 0.17 [0.05–0.45] (2/12) | – | – | – | – |
| FC8 | 0.00 [0.00–0.26] (0/11) | 0.62 [0.36–0.82] (8/13) | 0.46 [0.23–0.71] (6/13) | 0.62 [0.36–0.82] (8/13) | 0.46 [0.23–0.71] (6/13) |
| FC9 | 0.00 [0.00–0.26] (0/11) | 0.08 [0.01–0.33] (1/13) | 0.08 [0.01–0.33] (1/13) | 0.08 [0.01–0.33] (1/13) | 0.08 [0.01–0.33] (1/13) |
| FC10 | 0.22 [0.06–0.55] (2/9) | 0.23 [0.08–0.50] (3/13) | 0.15 [0.04–0.42] (2/13) | 0.23 [0.08–0.50] (3/13) | 0.15 [0.04–0.42] (2/13) |
| FC11 | 0.22 [0.06–0.55] (2/9) | 0.15 [0.04–0.42] (2/13) | 0.15 [0.04–0.42] (2/13) | 0.15 [0.04–0.42] (2/13) | 0.15 [0.04–0.42] (2/13) |
| FC12 | 0.00 [0.00–0.23] (0/13) | 0/13 | 0/13 | 0/13 | 0/13 |
| FC13 | 0.25 [0.09–0.53] (3/12) | 0.08 [0.01–0.33] (1/13) | 0.08 [0.01–0.33] (1/13) | 0.00 [0.00–0.23] (0/13) | 0.00 [0.00–0.23] (0/13) |
| FC14 | 0.36 [0.15–0.65] (4/11) | 0.31 [0.13–0.58] (4/13) | 0.08 [0.01–0.33] (1/13) | 0.62 [0.36–0.82] (8/13) | 0.62 [0.36–0.82] (8/13) |
| FC15 | – | – | – | 0.62 [0.36–0.82] (8/13) | 0.62 [0.36–0.82] (8/13) |
| any FC | 0.77 [0.50–0.92] (10/13) | 0.85 [0.58–0.96] (11/13) | 0.69 [0.42–0.87] (9/13) | 0.77 [0.50–0.92] (10/13) | 0.77 [0.50–0.92] (10/13) |
| any-FC FPR | 0/1 | 0/1 | 0/1 | 0/1 | 0/1 |
Detection by fault type (any FC):
| Fault type | CAMBER | pandas, own | pandas, G36 | SQL, own | SQL, G36 |
|---|---|---|---|---|---|
| Stuck damper | 4/4 | 4/4 | 4/4 | 4/4 | 4/4 |
| Supply-air-temperature bias | 2/4 | 4/4 | 2/4 | 4/4 | 4/4 |
| Stuck valve | 4/4 | 3/4 | 3/4 | 2/4 | 2/4 |
| Valve leak | 0/1 | 0/1 | 0/1 | 0/1 | 0/1 |
The valve leak goes undetected by every engine: its 10 % leak about cancels the unit's fan heat
(see the lbnl-sdahu run template and examples/lbnl_fdd/README.md), and CAMBER's dedicated
leaking_valve rule is calibrated for it.
Month windows. --window month scores each run-month as a case: 151 faulted and 12
fault-free months on lbnl-sdahu. Months of one run are not independent, so these intervals are
narrower than the evidence supports. Any-FC results:
| Engine | TPR | FPR |
|---|---|---|
| CAMBER | 0.68 [0.60–0.75] | 0/12 |
| open-fdd pandas, own defaults | 0.71 [0.63–0.78] | 0/12 |
| open-fdd pandas, G36 tolerances | 0.48 [0.41–0.56] | 0/12 |
| open-fdd SQL, own defaults | 0.66 [0.58–0.73] | 0/12 |
| open-fdd SQL, G36 tolerances | 0.61 [0.53–0.68] | 0/12 |
lbnl-ddahu (dual duct). Any-FC results:
| Engine | One case per run | Month windows |
|---|---|---|
| CAMBER | 1/2 detected, 0/1 false alarms | TPR 0.04 [0.01–0.20], FPR 0/12 |
| open-fdd pandas, own defaults | 2/2 detected, 1/1 false alarms | TPR 0.75, FPR 9/12 |
| open-fdd pandas, G36 tolerances | 2/2 detected, 1/1 false alarms | TPR 0.62, FPR 6/12 |
| open-fdd SQL, own defaults | 2/2 detected, 1/1 false alarms | TPR 0.75, FPR 9/12 |
| open-fdd SQL, G36 tolerances | 2/2 detected, 1/1 false alarms | TPR 0.75, FPR 9/12 |
G36 §5.16.14 is written for single-duct units, and the mapping reads the cold-deck discharge as
SAT next to the hot-deck valve. On this unit, "SAT below MAT while heating" (FC5) is the design,
not a fault. Read the lbnl-ddahu numbers as a scope check, not as detection performance.
Full tables, the native-alarm scores and the probe outcomes are in
examples/openfdd_crosscheck/results/. Since 0.101 (#109) the committed JSON leaves out the
per-run verdicts with each engine's denominator; run_crosscheck.py --keep-verdicts writes them.
CAMBER since #94 (0.98). Free cooling now needs the OA damper open beyond its minimum, and
the run template enables FC6 with the unit's documented minimum OA. Per run on lbnl-sdahu,
before → after:
| FC | Before | After | Why |
|---|---|---|---|
| FC6 | not evaluated | 2/12, FPR 0/1 | Catches both stuck-open damper runs: damper_stuck_075 24.5 %, damper_stuck_100_short 25.7 % of applicable hours; fault-free 0.0 % |
| FC8 | 2/11 | 0/11 | The stuck-open runs left free cooling and are now reported by FC6 |
| FC9 | FPR 1/1 | FPR 0/1 | The false alarm on the fault-free run is gone |
| FC12 | 1/13 | 0/13 | |
| Any FC | TPR 10/13, FPR 1/1 | TPR 10/13, FPR 0/1 |
The false alarm came from unoccupied hours with the fan at 100 % and the OA damper shut; it read 18 % of the fault-free run's free-cooling hours. In month windows, CAMBER's any-FC result moves from TPR 0.60 with FPR 7/12 (all FC9) to TPR 0.68 with FPR 0/12.
On lbnl-ddahu, FC12 moves from 0/2 to 1/2, and nothing else changes.
CAMBER since #95 (0.99). Heating (OS#1) now also needs the OA damper at its minimum, judged
against the same minimum and tolerance as free cooling. The open-fdd results are unchanged
(same engines, same pin), and so is every CAMBER verdict on lbnl-sdahu, which has no heating
coil. On lbnl-ddahu the hot deck heats while the OA damper command sits well above the learned
28 % minimum (median 55 % on the fault-free run), so those hours are now OS#5. CAMBER's FC5, the
one heating-only test the mapping can run there, changes as follows:
lbnl-ddahu |
Before (0.98) | After (0.99) |
|---|---|---|
| FC5, one case per run | evaluated on all 3 runs (863–1,442 h), never fired: 0/2, FPR 0/1 | not evaluated on any run: no OS#1 hours on the two stuck-damper runs, 0.5 h on the fault-free run |
| FC5, month windows | 0/11, FPR 0/7 | not evaluated |
| Any FC | 1/2 detected, 0/1 false alarms | unchanged |
FC5 never fired on this unit before #95, so no detection changes.
CAMBER and open-fdd since #97 and #98 (0.100). Two changes reach lbnl-ddahu. The catalog
mapping now maps the published cold-deck supply-air setpoint (CSA_TEMPSPT, 55.0 °F in every
row) as the SAT setpoint (#98), for all three engines. The run template now enables CAMBER's FC6
with the unit's documented seasonal minimum OA: 31.8 %, and 11.9 % in June to August (#97). The
lbnl-sdahu results do not change. Per run on lbnl-ddahu, before → after:
| FC | Engine | Before (0.99) | After (0.100) |
|---|---|---|---|
| FC6 | CAMBER | not evaluated: no minimum OA | 0/2, FPR 0/1: fault-free 0.07 %, DMPRStuck_OA_0 1.8 %, DMPRStuck_OA_100 0 % |
| FC7 | open-fdd (all) | not evaluated: no SAT setpoint | 0/2, FPR 0/1 |
| FC9 | CAMBER | not evaluated: no SAT setpoint | fires on DMPRStuck_OA_0 (20.8 % of 126 h); under 24 h on the other two runs |
| FC9 | open-fdd (all) | not evaluated | 0/2, FPR 0/1 (0.7–1.2 % on every run) |
| FC11 | CAMBER | not evaluated | fires on DMPRStuck_OA_0 (12.3 % of 53 h); under 24 h on the other two runs |
| FC11 | open-fdd (all) | not evaluated | 1/2, FPR 0/1: DMPRStuck_OA_0 9.7–14.0 % |
| FC13 | CAMBER | not evaluated | 0/2, FPR 0/1 (0 % on every run) |
| FC13 | open-fdd pandas, own defaults | not evaluated | 1/2, FPR 0/1: DMPRStuck_OA_0 19.8 % |
| FC13 | open-fdd pandas, G36 tolerances | not evaluated | 0/2, FPR 0/1 |
| FC13 | open-fdd SQL (both profiles) | not evaluated | 1/2, FPR 0/1: DMPRStuck_OA_0 11.9 % |
| Any FC | all | as above | unchanged for every engine |
In month windows, CAMBER's any-FC result is unchanged (TPR 0.04, FPR 0/12). The open-fdd pandas engine at G36 tolerances goes from TPR 0.58 with FPR 5/12 to TPR 0.62 with FPR 6/12: its FC9 now fires on one fault-free month. The other open-fdd results are unchanged.
Re-run once on the integrated 0.100 code, with all three engines at the same pin: every verdict
above holds, on both units and in both windows. #101 changes RCx headings only, and the
free_cooling_missed entries the 0.100 templates gained do not reach g36_afdd.
FC6 does not catch the damper stuck shut. G36 evaluates FC6 only in heating and in mechanical
cooling at minimum OA. On this unit, CAMBER's learned OA damper minimum is 28 %, the summer
position, so nearly all of FC6's applicable hours fall in June to August. Against an 11.9 %
minimum, a damper stuck shut is off by less than G36's 30-point tolerance. A fixed 31.8 % would
flag DMPRStuck_OA_0 (32.1 % of its applicable hours), but only by judging those summer hours
against the other season's minimum. The fault-free run reads ok either way. The fault-free
values that #94 recorded for a single minimum (FC6 20.5 % at 31.8, 47.1 % at 11.9, re-measured on
the 0.98 code) predate #95, which moved the hot-deck heating hours with the damper open out of
OS#1: FC6's applicable hours on the fault-free run fell from 2,105 to 744 (hourly). The winter
heating hours run with the damper at or above its 45 % winter minimum, beyond the learned 28 %,
so they are now OS#5. A seasonal OA damper minimum returns them to FC6 (#105, below), but not on
DMPRStuck_OA_0. All of these setpoint tests judge the cold deck, because the mapping reads it as
SAT. The hot deck's own 90 °F setpoint is not mapped.
CAMBER since #105 (0.101). The lbnl-ddahu run template now also gives the operating states
the unit's documented seasonal minimum damper position, oa_damper_min 45 % with
oa_damper_min_by_month 28 % in June to August, where the learned minimum was the 28 % summer
position all year. The open-fdd engines do not read the template, and lbnl-sdahu does not
change. Per run on lbnl-ddahu, measured on CAMBER's side of the cross-check (15-minute data),
before → after:
| FC | Before (0.100) | After (0.101) |
|---|---|---|
| FC5, FC7 | evaluated on 0.5 h of the fault-free run only | evaluated on the fault-free run (400.5 h) and DMPRStuck_OA_100 (554.5 h); never fire. DMPRStuck_OA_0 has no OS#1 hours |
| FC6 | 0/2, FPR 0/1 over 759–777 h | 0/2, FPR 0/1 over 777–1,314 h: DMPRStuck_OA_0 1.8 %, unchanged |
| FC8 | fires on DMPRStuck_OA_0 (49.5 of 126.25 h); fault-free 0 of 4.25 h |
fires on DMPRStuck_OA_0 (49.5 of 49.75 h); not evaluated on the fault-free run |
| FC9 | fires on DMPRStuck_OA_0 (26.25 of 126.25 h); fault-free 4.25 of 4.25 h, under the 24 h floor |
0 of 49.75 h on DMPRStuck_OA_0; not evaluated on the fault-free run |
| Any FC | 1/2 detected, 0/1 false alarms | unchanged |
The FC9 hours on both runs were winter hours with the damper command at or near its 45 % minimum, read as free cooling against the learned 28 %; they are OS#5 now. In month windows CAMBER's any-FC result is unchanged (TPR 0.04, FPR 0/12). FC6 still does not catch the damper stuck shut, and no damper minimum can make it: outside June to August the faulted unit's damper command saturates (median 100 %) while the hot deck heats, or both coils run, so those hours are OS#5, where FC6 does not apply. The temperature balance there reads a median 17.7 % OA, which is within G36's 30-point tolerance of 31.8 % in any case.
Re-run once on the integrated 0.101 code, with all three engines at the same pin: CAMBER's
lbnl-ddahu verdicts are exactly those above, in both windows, and nothing else moves. Every
open-fdd verdict and every lbnl-sdahu verdict is unchanged, and so is each engine's any-FC
result on both units. In month windows on lbnl-ddahu, CAMBER's FC5 and FC7 are now evaluated in
5 faulted and 4 fault-free months and never fire, FC6 in 11 faulted and 7 fault-free months (was
6 and 3) and never fires, and FC9 no longer fires in the one DMPRStuck_OA_0 month it caught.
DMPRStuck_OA_0 stays detected through FC8, FC10, FC11 and FC12.
Not evaluated, and why
lbnl-sdahu:
| FC | Engine | Why it was not evaluated |
|---|---|---|
| FC1 | all | No run has both duct static and its setpoint. The catalog masks the faulted runs' placeholder setpoint and the fault-free run's static, which is published in Pa. |
| FC5, FC7 | all | No heating coil. |
| FC6 | open-fdd (both) | Needs a VAV total airflow and a design minimum OA flow; airflow is deliberately unmapped. CAMBER evaluates FC6 from its template's minimum OA fraction. |
| FC15 | CAMBER, pandas | No heating coil. The SQL rule requires no roles and falls back to MAT/SAT, so it is evaluated (see below). |
| FC14 | open-fdd pandas | Evaluated only through the harness's declared MAT/SAT substitution (G36 allows it on an AHU without a heating coil). CAMBER makes the same substitution internally, and the SQL rule falls back to MAT/SAT by itself. |
| FC8–FC14 | CAMBER | Not evaluated on a few runs where the FC's operating states held for under 24 h. |
lbnl-ddahu:
| FC | Engine | Why it was not evaluated |
|---|---|---|
| FC9, FC11 | CAMBER | Since #98 they are evaluated, but the fault-free run and DMPRStuck_OA_100 have under 24 h in their operating states. |
| FC14 | CAMBER | Declined: MAT/SAT span both coils. |
| FC14 | open-fdd pandas | No coil temperatures. |
| FC14 | open-fdd SQL | Evaluated: falls back to MAT/SAT. |
| FC6 | open-fdd (both) | Needs a VAV total airflow and a design minimum OA flow. CAMBER evaluates FC6 from its template's seasonal minimum OA fraction (since #97). |
| FC5 | CAMBER | Since #95 the hot deck heats with the OA damper above its learned minimum, so there were no OS#1 hours to judge. Since #105 the template's seasonal damper minimum restores them on the fault-free run and DMPRStuck_OA_100; DMPRStuck_OA_0 still has none (its damper command saturates open). |
SQL engine: the source-reading findings, tested
Before the SQL engine could run, these were read from the pinned source. Each is now confirmed on
synthetic one-day probes run through fdd_cli
(examples/openfdd_crosscheck/results/probes.md, run_crosscheck.py --probe; 20 of 20 probe
outcomes as predicted).
| Probe | What it isolates | CAMBER | pandas own / G36 | SQL own / G36 |
|---|---|---|---|---|
fc13_sat_1p5_over_sp_full_cooling |
FC13 at SAT 1.5 °F over setpoint, full cooling | not fired | fired / not fired | fired / fired |
fc9_oat_4_over_sp_free_cooling |
FC9 at OAT 4 °F over the SAT setpoint, free cooling | not fired | fired / not fired | fired / fired |
fc8_sat_3p5_over_mat_free_cooling |
FC8 at SAT 3.5 °F over MAT, free cooling (positive control) | not fired | fired / not fired | fired / not fired |
fc13_sat_3_over_sp_half_cooling |
FC13 with the cooling valve at 50 % | not fired | fired / fired | not fired / not fired |
- G36 tolerances the SQL tuning file cannot set: confirmed.
fdd_cli run-rulesitself only overridesconfirm_seconds. Its rule parameters come fromrule_tuning/defaults.yamlnext to the rules directory. Through that file: EPS_SATis always taken fromSUPPLY_TOL, so FC7 and FC13 (FC13-SAT-HIGH) keep 1.15 °F. At the "G36" profile, FC13 still fires at SAT 1.5 °F over setpoint.- FC9, FC11, FC14 and FC15 read an
EPS_MATthey do not declare, so it stays at the 1.15 °F global default. FC9 still fires at OAT 4 °F over the setpoint.
The FC8 probe is the control: FC8's tolerances are declared, the G36 profile silences it, so
the override file is read.
- Partial overrides. The first data run applied only the settable part (fan heat 2 °F) to
FC9. That made it fire on 4/13 runs against 1/13 at its defaults: the band became
OAT > SATSP + 0.3 °F instead of + 1.75 °F. Profiles version 2 therefore leaves rules whose G36
tolerances cannot all be set at their defaults. The G36 SQL column above runs FC9, FC11, FC14
and FC15 at open-fdd's defaults.
- FC13 "full cooling" threshold: confirmed. The SQL rule requires the cooling valve at 90 %
or more (clg_full_min 0.9), and the pandas rule any cooling (0.01). With the valve at 50 %,
the pandas FC13 fires and the SQL one does not. CAMBER does not either, since G36 FC13 is a
full-cooling test.
- Default ModeDelay is 10 min in the SQL rules and 0 in the pandas rules. The FC8, FC9, FC10
and FC11 results above agree between the two engines run for run (within 2 percentage points
per run on FC8, under 1 on FC9–FC11), so it does not matter on this data.
- Required roles are checked per building. fdd_cli run-rules skips a rule when a required
role is missing from the building's columns, which are the union over all its equipment.
Equipment that lacks a role its neighbour has is reported with 0 fault hours rather than
skipped. The harness therefore gives each equipment its own building. In a first run with all
units in one building, FC9, FC11 and FC13 were reported for the dual-duct unit, which then had
no SAT setpoint mapped.
What drives the differences
These are behaviour differences, each traced to a cause on public code and open data.
- Tolerances. Moving open-fdd from its defaults to the G36 tolerances lowers its FC8 rate in both engines: 8/13 → 6/13 runs. The pandas FC14 drops from 4/13 to 1/13; the SQL FC14 stays at its defaults (see above).
The SAT-bias runs show it most clearly. The −2 and −4 °C bias runs read 37–38 % FC8 and FC14 in both open-fdd engines at their defaults. At G36 tolerances the pandas FC14 reads 0–1 %. - The two open-fdd engines agree with each other where their parameters match. On every run, FC8 agrees within 2 percentage points and FC10 within 1, in both profiles; FC9 and FC11 agree within 1 at the defaults. - FC14 sign, and FC15 on a unit without a heating coil (SQL). The SQL FC14 and FC15 test the absolute MAT→SAT temperature change (either sign), falling back to MAT/SAT when there are no coil sensors. FC15 requires no roles, so it is evaluated on the cooling-only SDAHU as well, where the MAT/SAT pair spans only the cooling coil and the fan.
On SDAHU, both rules fire on the same 8 faulted runs (SAT-bias, two stuck-valve and two stuck-damper runs) and not on the fault-free run. On the dual-duct fault-free unit the SQL FC14 fires on 51 % of its hours.
The pandas FC14 tests the signed drop. CAMBER subtracts the fan-heat rise, because SAT is
downstream of the fan; on the −2/−4 °C SAT-bias runs CAMBER's FC14 reads 96–100 %.
- How the minimum-OA state is recognised. Both open-fdd engines take "minimum OA" to mean a
damper at or below 5 % (econ_min_pos, oa_damper_econ_low). This unit's minimum position is
10 %, so the open-fdd FC12 and FC13 never see the OS#4 hours in which the partly stuck valves
show up. CAMBER's FC13 reads 92/89/77 % on the 10/25/50 % stuck-valve runs; pandas reads 5/7/1 %
and SQL 3/2/0 %.
With econ_min_pos set to the unit's own minimum (0.11) and G36 tolerances, the pandas FC13
reads 68/66/48 % on those runs and under 4 % on the other runs (fault-free 1.3 %). This is a
site setting, not a tolerance, so it is not part of either profile.
- FC5 on a dual-duct unit (both open-fdd engines). The open-fdd FC5 tests "heating
commanded" without excluding simultaneous cooling. CAMBER evaluates FC5 only in OS#1, so the
dual-duct hours with both decks active are excluded. Since #95 the hot-deck hours with the OA
damper above its minimum are excluded too, which leaves CAMBER's FC5 not evaluated on this
unit. The open-fdd FC5 fires on 18–23 % of the DDAHU fault-free run's hours.
- Denominators. CAMBER divides by the hours in the FC's G36 operating states, after
ModeDelay. The pandas engine divides by the fan-proven hours after its startup delay. The SQL
FC rules report fault hours only, so the harness divides by the fan-on hours of the frame. Each
verdict in the results JSON records its denominator.
So CAMBER's percentages are larger for the same fault hours. On the 10 % and 25 % stuck-damper runs, for example, FC10 reads 100 % in CAMBER and 37–38 % in both open-fdd engines. The 0.1.5 comparison found this too.
Where the 0.1.5 comparison no longer applies
- "FC7, FC9, FC11, FC13 need a SAT setpoint, which this dataset didn't include." No longer
true for SDAHU. The dataset publishes
SA_TEMPSPT, a constant 55.2 °F, and the catalog maps it, so FC9, FC11 and FC13 are now evaluated by every engine. FC7 is still not evaluated, because the unit has no heating coil. Since 0.100 (#98) the same holds for the dual-duct unit's cold deck (CSA_TEMPSPT). - "The equations agree to 0.00 pts" and "same faults, same hours". Both describe the per-interval equations of 0.1.5 with CAMBER's time filters off. They do not carry over to fault rates now:
- CAMBER applies the G36 time filters by default (fan gate, ModeDelay, AlarmDelay, averaging), and since #94 it requires the economizer open beyond minimum for free cooling;
- current open-fdd differs from G36 in default tolerances and in how it selects operating states;
- the engines differ in the FC14/FC15 sign handling and in their denominators.
The fault conditions are still the same G36 tests, but the trip rates differ, for the reasons
above.
- "open-fdd gates on a single signal." Partly superseded. Both current open-fdd engines
select free-cooling and mechanical-cooling states from damper and valve thresholds
(econ_min_pos, econ_full_open, the cooling thresholds) on top of a fan gate and a
mode/startup delay. This is closer to an operating-state gate than 0.1.5 was, but it is not
G36's OS#1–OS#5 classifier.
- "Run open-fdd at matched tolerances." Still true, but not sufficient. The pandas engine can
match the G36 tolerances. The SQL engine cannot for FC7, FC9, FC11 and FC13–FC15. Even where
tolerances match, the remaining differences come from operating-state selection, the FC14/FC15
sign handling, the unit's minimum-OA damper position and the denominators.
Corrections from the open-fdd side are welcome on
#22, especially on the role mapping
(role_map.json) and the SQL tuning findings.
open-fdd's ECM tooling and the shared M&V vectors
open-fdd's ECM tooling (MIT) has three parts:
- Excel ECM workbooks backed by independent Python reference calculators: fan affinity, chilled-water reset, condenser water, economizer runtime, outside-air loads, kW/ton, schedule reduction and others;
- an honest comparison of each calculator's estimate against an EnergyPlus twin;
- change-point and ASHRAE Guideline 14 helpers (
fit_changepoint,select_changepoint,score_g14_monthly,option_c_savings) that credit CAMBER as their algorithm reference.
The helpers are an independent reimplementation and can fit the same data differently. They use a different breakpoint grid and a different model-selection criterion, they allow a zero-width 5P dead-band, and their heating-slope sign is the opposite of CAMBER's. Compare savings from the two tools only after checking which model each one chose.
How the two fit. open-fdd's calculators estimate a retrofit's savings before it is built, and CAMBER measures and verifies them afterwards. CAMBER's findings can replace a calculator's assumptions with measured inputs, such as:
- run hours;
- missed free-cooling hours, and what caused them;
- how a reset actually behaves;
- pump minimum-speed floors.
For M&V on monthly data, CAMBER's bill-only path covers calendarization, degree-day bases selected from the bills, and versioned billing baselines (see MANDV.md). Any integration stays at the file and process boundary, tracked in #22.
Shared vectors. The shared M&V test vectors let the two sets of helpers be cross-checked without either importing the other. They hold:
- synthetic change-point cases with known truth;
- bill cases;
- CAMBER's expected outputs and predicted series;
- expected statistics for BDG2 meters, whose daily, monthly and bill aggregates are not committed (CAMBER redistributes no datasets). A standalone script rebuilds them from the publisher's sha256-pinned files.
The vectors are engine-agnostic:
- the inputs are CSV, with optional Parquet that DataFusion reads natively;
- the expected outputs are JSON with a versioned schema (
mv_vectors/1); - the results contract is JSON, and its checker needs numpy and pandas only.
So open-fdd's pandas library or a SQL M&V twin can be checked without installing CAMBER. The vectors also show where the two G14 gates differ:
score_g14_monthlyapplies Guideline 14's calibrated-simulation tolerances: |NMBE| ≤ 5 % and CV(RMSE) ≤ 15 % monthly.- CAMBER's regression-baseline gate also requires R² ≥ 0.75 and |NMBE| ≤ 0.5 %.
- On loads with little weather signal, the first passes and the second fails.