Measurement & Verification in CAMBER (IPMVP / ASHRAE G14 / CalTRACK)
CAMBER implements whole-building IPMVP Option-C savings — equivalently the CalTRACK normalized metered energy consumption (NMEC) workflow — from standard parts: a weather-based baseline model, goodness-of-fit statistics, and avoided energy use with uncertainty. This page maps CAMBER's API to the CalTRACK/IPMVP vocabulary and shows how to cross-check against OpenEEmeter (eemeter), the reference open-source CalTRACK implementation.
IPMVP Option-C / CalTRACK NMEC pipeline: fit a weather baseline, project it onto reporting weather, and report avoided energy with an uncertainty band.
flowchart LR
bl["Baseline (energy, temp)"] --> fit["best_model / towt"]
fit --> gof["fit_stats: CV(RMSE), NMBE"]
gof -- "G14 acceptance tier" --> avoided
rep["Reporting (energy, temp)"] --> proj["Adjusted baseline"]
fit -- "project onto reporting weather" --> proj
proj --> avoided["avoided_energy_savings"]
avoided --> out["Savings + FSU band"]
rep --> nre["detect_non_routine"]
nre -- "exclude days and refit" --> fit
avoided --> cusum["cusum tracking"]
Terminology bridge
| IPMVP / CalTRACK term | CAMBER |
|---|---|
| Baseline period / reporting period | the two (energy, temp) series you pass in |
| Baseline model | mandv.models.best_model (change-point 2P–5P) / mandv.towt (hourly) |
| Goodness of fit — CV(RMSE), NMBE | mandv.stats.fit_stats |
| Avoided energy use | mandv.stats.avoided_energy_savings |
| Fractional savings uncertainty (FSU) | G14 Annex-B, in the same call — t·1.26·CV·√((n/n′)(1+2/n)/m)/F |
| Autocorrelation correction | n′ = n(1−ρ)/(1+ρ); ρ from stats.lag1_autocorrelation, carried on FitStats.rho_lag1 |
| Normalized annual savings | drive the models with a typical year (mandv.weather TMY/EPW) |
| Live actual weather (any lat/lon) | weather_source.oat_reference — NASA POWER fetch, see WEATHER.md |
| Cumulative savings tracking | mandv.cusum |
Method correspondence
- CalTRACK Daily ↔ CAMBER daily change-point: aggregate to daily energy vs
daily-mean temperature, fit the inverse model, project onto reporting weather.
This is exactly what
mandv.caltrack.caltrack_savings()does end-to-end. - Hourly NMEC ↔
mandv.caltrack.caltrack_savings_hourly, on a TOWT baseline (mandv.towt, the LBNL Mathieu et al. time-of-week & temperature model). This is not the CalTRACK Hourly specification — that method is a different estimator:
| CalTRACK Hourly | CAMBER mandv.towt |
|---|---|
| per-calendar-month segmented models, weighted | one pooled model over the baseline |
| six fixed temperature bin edges | n_temp_segments quantile-spaced breakpoints |
| occupancy from a month × hour-of-week lookup, from a preliminary daily model's residuals | occupancy from bin-mean load vs the median of bin means |
| 365-day data sufficiency, hard limits enforced | hour count plus per-hour-of-week-bin coverage |
So these are defensible IPMVP Option-C savings on a published baseline model, not eemeter-comparable numbers. Same posture as the daily method — see the CalTRACK note below.
Expect a band comparable to the daily method, not tighter. Hourly residuals are strongly serially correlated (ρ≈0.85 is ordinary), and the effective-sample-size correction bites: measured on a 20-week synthetic, n=3360 hours becomes n_eff=282 and the band widens from 1.6% at ρ=0 to 9.7% at ρ=0.85. Finer data does not buy proportionally more certainty. - Billing/monthly ↔ change-point on monthly data (looser CV(RMSE) tier).
Quick use
from camber.mandv.caltrack import caltrack_savings
res = caltrack_savings(
baseline_energy, baseline_temp, reporting_energy, reporting_temp
) # hourly Series in
print(res.model_kind, round(res.baseline_r2, 3))
print(res.savings.savings_pct, "±", res.savings.fractional_uncertainty) # fractions
Two uncertainty kernels, and why
Which kernel applies depends on whether the savings difference contains measured energy:
measured − projected(Option C avoided energy, Option B isolation) carries the reporting period's own residual noise, which averages down over themreporting points. This is the G14 Annex-B form above.projected − projected(normalized annual savings, Option D) contains no measured energy at all — only parameter error, shared by every projected period. Its band is therefore independent of how many periods you normalize onto, and usesCV·√(p/n), the OLS average leverage.
The two have deliberately different provenance: the measured kernel is the published G14 expression,
constant and all; the projected one is textbook regression theory, cited as such rather than
borrowing G14's empirical 1.26 for a case it was never derived for.
Serial correlation widens both, via n′. Daily whole-building residuals are routinely correlated,
so an unadjusted band is optimistic — caltrack_savings estimates ρ from the baseline residuals
automatically. Strictly the reporting-period ρ is wanted, but those residuals contain the saving
itself, so the baseline fit's ρ is the standard substitution.
Acceptance thresholds — and where we differ from CalTRACK
- CAMBER uses ASHRAE Guideline 14 acceptance tiers for CV(RMSE)
(
stats.cv_rmse_max_for: ~15% monthly, ~30% daily/hourly) and reports NMBE. - CalTRACK is stricter and more prescriptive: it specifies data-sufficiency rules (coverage, minimum days), explicit model-selection criteria, and hard limits that eemeter enforces. CAMBER leaves those policy choices to the caller — so a CAMBER fit is not automatically CalTRACK-compliant. Use the thresholds and the FSU to judge whether a result is reportable, and apply CalTRACK's data rules if compliance is required.
Non-routine events (NRE)
Shutdowns, occupancy changes, or meter outages are by definition what the weather
model can't explain and will skew a baseline. mandv.nonroutine.detect_non_routine
flags days whose residual vs the baseline is a robust (MAD) outlier, and
caltrack_savings(..., exclude_non_routine=True) drops those baseline days and
refits — so a shutdown doesn't distort the savings. Point-wise today; sustained
step-change detection is on the roadmap.
Cross-checking against eemeter
eemeter pulls heavier dependencies, so install it in a separate environment rather than alongside CAMBER:
python -m venv .eemeter && . .eemeter/bin/activate
pip install eemeter eeweather
Then run the same baseline and reporting data through both and compare:
- CAMBER:
caltrack_savings(...).savings.avoided_energy. - eemeter: fit a CalTRACK daily model on the baseline and compute metered savings over the reporting period (see eemeter's docs/notebooks).
- Expect the avoided-energy figures to agree within CAMBER's reported FSU band. Differences usually trace to CalTRACK's data-sufficiency/limit rules (which eemeter applies and CAMBER leaves configurable) or to model-selection choices.
This gives a credible, standards-aligned check without making eemeter a CAMBER dependency. See docs/ECOSYSTEM.md for the broader leverage strategy.
Degree-day baseline (mandv.degreeday)
The simplest defensible weather model — a variable-base degree-day regression
E = base + a·HDD + b·CDD — for monthly-bill M&V where you have average temperature and energy per
period (a lighter cousin of the change-point models above).
from camber.mandv.degreeday import fit_degree_day
m = fit_degree_day(tavg_per_month, energy_per_month) # balance point auto-fit by min CV(RMSE)
m.balance_point, m.cooling_slope, m.heating_slope, m.fit.cv_rmse
m.predict(normal_year_tavg) # normalize / project the baseline
degree_days(tavg, balance_point) returns the (HDD, CDD) arrays. Flags: balance_point (fix it
or leave None to search), balance_range/step (search grid), kind (heating/cooling/both).
Fit statistics (R², CV(RMSE), NMBE, G14 acceptance) come from mandv.stats.fit_stats.
IPMVP Option A — key-parameter measurement (mandv.option_a)
Measure the parameter that drives the savings and stipulate the rest (the classic lighting/motor retrofit): savings = measured Δparameter × stipulated duty.
from camber.mandv.option_a import option_a_savings, stipulated_annual_hours
r = option_a_savings(
baseline_kw=100, reporting_kw=60, stipulated_factor=stipulated_annual_hours(hours_per_day=10)
) # 2600 h
r.savings, r.measured_delta, r.reduction_pct # 104000 kWh, 40 kW, 0.40
Inputs may be scalars or sampled arrays/Series (the mean is taken). The result's basis records the
measured-vs-stipulated split for audit; the stipulated portion carries uncertainty this method does
not quantify. Complements Option B (mandv.retrofit_isolation) and Option C (mandv.stats).
IPMVP Option D — calibrated simulation (mandv.rc_model)
The one remaining IPMVP boundary, now shipped: a dependency-light 1R1C grey-box building model
(RCModel.predict(oat, schedule)) calibrated to metered energy (grid tau + OLS, gated by the same
ASHRAE G14 acceptance test), then run under an as-corrected control to give a pre-implementation
modeled saving with a G14 uncertainty band — the counterfactual the ecm_savings upper bound stands
in for. Refuses to claim a saving when the calibration fails the gate. So CAMBER now covers IPMVP
Options A, B, C, and D. A 2R2C thermal-mass variant (RC2Model/calibrate2), multi-zone
stacked-OLS calibration (calibrate_zones), and an optional EnergyPlus cross-validator
(interop.energyplus) add depth on the same grid-τ/OLS/G14 footing. See OPTION-D.md.