Skip to content

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 the m reporting 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 uses CV·√(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:

  1. CAMBER: caltrack_savings(...).savings.avoided_energy.
  2. eemeter: fit a CalTRACK daily model on the baseline and compute metered savings over the reporting period (see eemeter's docs/notebooks).
  3. 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.