Calculation Methodology

Complete reference for every algorithm, constant, and formula used across the Ener.co platform — the Estimator, the Proposer, and the Dashboard.

Contents

Part I — Financial Model (Estimator & Proposer)

These algorithms power the Refinery Estimator and Proposer tools. They use ASHRAE standards, equipment age, condition, and region to model savings before sensors are installed. Source: shared/calculations.js and estimator/index.html.

Every piece of HVAC equipment manufactured in the US must meet a minimum efficiency standard set by ASHRAE 90.1 (adopted by DOE). We use these as the nameplate kW/ton starting point for each unit based on its install year.

-- federalMinKwTon(install_year) --
-- Returns ASHRAE minimum kW/ton for the standard in effect --

2023+1.07 kW/ton   ASHRAE 90.1-2022
2016–20221.09 kW/ton   ASHRAE 90.1-2016/2019
2013–20151.11 kW/ton   ASHRAE 90.1-2013
2010–20121.19 kW/ton   ASHRAE 90.1-2010
2005–20091.26 kW/ton   ASHRAE 90.1-2004
2000–20041.30 kW/ton   ASHRAE 90.1-1999
pre-20001.41 kW/ton   ASHRAE 90.1-1989
Lower kW/ton = more efficient. A 2023 unit at 1.07 kW/ton uses 24% less energy per ton than a pre-2000 unit at 1.41 kW/ton. These are nameplate ratings — actual field performance degrades over time (see E2).

Equipment efficiency degrades over time due to coil fouling, refrigerant loss, and mechanical wear. We model this as compound annual degradation from the ASHRAE nameplate, varying by equipment condition. A hard cap at 1.5× nameplate prevents unrealistic values.

-- derivedKwTon(install_year, condition) --

nameplate = federalMinKwTon(install_year)
age = max(0, current_year install_year)

-- Condition-based annual degradation rate --
deg_rate = { poor: 3.5%, avg: 2.0%, good: 1.0% }

-- Compound degradation --
degraded = nameplate × (1 + deg_rate)age

-- Hard cap: equipment cannot degrade beyond 150% of nameplate --
cap = nameplate × 1.5

field_kwton = min(degraded, cap)
ConditionRateRationale
good1.0%/yrWell-maintained, regular coil cleaning
avg2.0%/yrTypical commercial maintenance schedule
poor3.5%/yrNeglected coils, heavy fouling, deferred maintenance
Why the 1.5× cap? A compressor drawing 50% more power than nameplate would realistically have failed or triggered fault protection before reaching that point. The cap prevents the model from projecting equipment states that wouldn't exist in the real world. Example: a 2010 unit (1.19 kW/ton) in poor condition hits the 1.785 cap at age 12 — without the cap, it would be at 2.06 kW/ton by age 16.
New unit field penalty: When modeling a replacement unit in the Buy New scenario, nameplate efficiency is derated by 10% to reflect real-world installation losses (duct leakage, refrigerant charge, controls integration). Source: PNNL field studies.
fieldKwTon(year) = federalMinKwTon(year) × 1.10

Pre-sensor estimate of EER improvement from EnerCoat treatment. Uses a logarithmic curve fit to M&V field data — older equipment has greater optimization potential but with diminishing returns. Adjusted for condition and equipment type.

-- Base M&V estimate (log-curve fit of field data) --
base_pct = 5.58 × ln(age + 1) + 2

-- Condition adjustment --
cond_mult = { poor: 1.10, avg: 1.00, good: 0.90 }
cond_adjusted = base_pct × cond_mult

-- Equipment type adjustment --
evap_only7% fixed   (evaporator-only systems)
cond_onlycond_adjusted
full (both)cond_adjusted + 7%   (evap bonus added)

-- Final: capped at max achievable savings (see E4) --
savings_pct = min(mv_estimate, max_savings_pct)
Log-curve rationale: Younger equipment has less fouling to remove, so gains flatten quickly. At age 5 the base is ~11.0%; at age 15 it's ~17.4%; at age 25 it's ~20.2%. The 7% evaporator bonus reflects the additional EER improvement from treating the indoor coil — supported by NYBC and NYT monitored data.

Savings cannot bring field efficiency below 95% of the ASHRAE nameplate minimum. This prevents claiming that treatment makes a unit better than new — we can only recapture lost efficiency, not create it.

floor = federalMinKwTon(install_year) × 0.95

-- If field kW/ton is already at or below the floor, no savings --
if kwton floor0%

-- Otherwise, max possible improvement --
max_savings_pct = (1 floor / kwton) × 100
The 95% factor gives 5% headroom below nameplate. Example: a 2016 unit (nameplate 1.09) running at 1.33 kW/ton → floor = 1.035 → max savings = 22.2%. The M&V estimate is capped at this value.

Climate and operating environment determine equipment life, cooling hours, and degradation rate. These defaults can be overridden per-unit.

RegionBase Life (yr)EFLH (hrs/yr)Deg Without (%/yr)
northeast182,5003.0
southeast164,0003.5
midwest182,5003.0
southwest204,0002.5
west222,5002.0

Condition life offsets — added to/subtracted from base life:

ConditionOffsetEffect on 18-yr Base
poor−3 years15 years
avg0 years18 years
good+2 years20 years
Condition mapping from external data: EXCELLENT/GOOD → good, FAIR → avg, POOR → poor. The mapping is case-insensitive and applied during spreadsheet import in the Proposer.

Remaining Useful Life (RUL) estimates how many years until the equipment needs replacement. Extended Useful Life (EUL) is the projected life with EnerCoat treatment — always RUL + 10 years.

condition_life = regionDefaults[region].life + conditionLifeOffset[condition]

RUL = max(3, install_year + condition_life current_year)

EUL = RUL + 10
Minimum 3-year clamp: Even end-of-life equipment gets at least 3 years of RUL. This prevents degenerate sinking fund calculations and reflects that equipment rarely fails exactly on schedule. The 10-year EUL extension is supported by 13+ years of monitored data at NYBC and 9+ years at NYT showing near-zero degradation post-treatment.

Calculates the annual deposit needed to accumulate a future replacement cost over N years at a given discount rate. Used for capital reserve comparisons across all TCO scenarios.

-- Uniform Sinking Fund --
A = FV × r / ((1 + r)N 1)

-- When r = 0 (no discount) --
A = FV / N
VariableMeaningDefault
FVFuture replacement cost (inflation-adjusted)tons × cost_per_ton × (1.03)N
rDiscount rate7%
NYears to accumulateRUL or EUL

Energy costs rise from two forces: equipment degradation (needs more kW per ton) and utility rate escalation (electricity costs more). Savings grow as the gap between treated and untreated widens each year.

-- Base annual energy cost (Year 1) --
base_energy_yr1 = tons × kwton × eflh × kwh_rate

-- For each year y = 1 to horizon: --
esc = (1 + 0.02)y−1   energy rate escalation (2%/yr)
df = min(deg_cap, (1 + deg_without)y−1)   degradation factor
deg_cap = (nameplate × 1.5) / kwton   max degradation multiplier

-- Efficiency improvement widens each year --
max_eff = max(0, 1 (nameplate × 0.95) / (kwton × df))
eff_pct = min(sav_pct + (y−1) × (deg_without deg_with), max_eff)

energy_without = base_energy_yr1 × df × esc
energy_with = energy_without × (1 eff_pct)
energy_saving = energy_without energy_with
Key insight: The savings percentage grows each year because untreated equipment degrades faster (2–3.5%/yr) than treated equipment (0.25%/yr). In Year 1 you might save 15%; by Year 10 the gap has widened and you're saving 30%+. This is the "escalating degradation gap" method.

Maintenance costs grow as equipment degrades. Treated equipment's R&M baseline is reduced by the initial savings percentage, then grows at the much slower treated degradation rate.

-- Base annual R&M (ASHRAE RP-1237) --
base_rm = tons × cost_per_ton × rm_factor   default 4%

-- For each year y: --
rm_without = base_rm × min(deg_cap, (1 + deg_without)y−1)
rm_with = (base_rm × (1 − sav_pct)) × (1 + deg_with)y−1

rm_saving = rm_without rm_with
R&M savings often exceed energy savings in later years because maintenance costs compound with equipment degradation. The default 4% R&M factor (ASHRAE RP-1237) means a 100-ton unit at $5,500/ton has a $22,000/yr maintenance baseline.

The CapEx benefit of EnerCoat is the present value of deferring a replacement from RUL to EUL. We compute that NPV once, then recognize it as a flat annual rate over the equipment's extended useful life. The result is a defensible, conservative number that respects two realities: (1) the decision genuinely creates value the moment it's made, and (2) that value is only earned over time.

-- Step 1: PV of each replacement event --
pv_without = replace_cost × ((1 + infl) / (1 + r))RUL
pv_with = replace_cost × ((1 + infl) / (1 + r))EUL

-- Step 2: NPV of deferral (the lump-sum value created today) --
npv_deferral = pv_without pv_with

-- Step 3: Recognition window --
   Forward proposals: cap at horizon so full NPV lands in the contract
recognition_yrs = min(EUL, project_horizon)
   M&V (looking back): always EUL — never claim what time hasn't earned
recognition_yrs = EUL

-- Step 4: Flat annual savings recognized in years 1..recognition_yrs --
annual_rate = npv_deferral / recognition_yrs
cap_saving(y) = annual_rate  if y ≤ recognition_yrs, else 0
Why this approach: Single-point sinking-fund deltas understate the deal because they ignore the fact that the decision shifts a real cash event from year RUL to year EUL. Multi-cycle NPV models go the other way and introduce artifacts at cycle boundaries. NPV / EUL amortization captures the full present-value benefit and spreads it linearly — the same number is shown each year, the cumulative line is straight, and total recognized savings exactly equal the deal NPV.

Buy-New (BN) drag: When BN replaces earlier than RUL (e.g., proactive turnover), the same formula yields a small negative NPV, which is amortized over the new unit's life. This shows up as a modest ongoing capital drag on the BN scenario.

Sinking-fund mode (legacy): A toggle is preserved that uses windowed sinking funds for projects/users that prefer the older display. Both methods are documented in the engine.

Year 1 snapshot comparing the full annual cost of ownership with and without treatment. Used as the headline metric in proposals.

ATCO_without = base_energy_yr1 + base_rm + sinkingFund(inflated_rul, r, RUL)
ATCO_with = (base_energy_yr1 × (1 − sav_pct)) + (base_rm × (1 − sav_pct)) + sinkingFund(inflated_eul, r, EUL)

ATCO_reduction = ATCO_without ATCO_with
co2_factor = getCO2Factor(region)   regional metric tons CO₂ per kWh (EPA eGRID 2022)

kwh_saved_yr1 = tons × kwton × eflh × sav_pct
co2_total = (total_energy_savings / kwh_rate) × co2_factor
CO₂ factor is now region-specific using EPA eGRID 2022 data (see E18 constants table). Falls back to national average (0.00037 MT/kWh) if region is unknown. Total energy savings (in dollars) is converted back to kWh by dividing by the energy rate, then multiplied by the regional CO₂ factor. This captures the full horizon cumulative impact including escalation.

Models the worst-case scenario: running equipment past its useful life until emergency failure. No capital reserves — purely reactive. This is the "do nothing and hope" approach.

-- Overshoot: equipment runs 5 years past RUL --
RTF_OVERSHOOT = 5 years
rtf_replace_yr = RUL + 5

-- Post-RUL: degradation accelerates 1.5× --
rtf_deg = deg_without × 1.5

-- Post-RUL: R&M scales from 2× to 3× over 5 years --
rm_mult(overshoot_yr) = 2.0 + (overshoot_yr − 1) × (1 / 4)

-- Emergency replacement: 25% premium + 2× disruption --
emergency_cost = inflated_cost_rtf × 1.25
disruption = emergency_cost × disruption_pct × 2
Three phases:
Years 1–RUL: Normal degradation, normal R&M
Years RUL+1 to RUL+5: 1.5× degradation, R&M spikes 2→3× (ASHRAE RP-1237 end-of-life spike)
After RUL+5: Emergency replacement at 25% premium (DOE FEMP), 2× disruption (unplanned), new unit with 10% field penalty

Planned replacement at end of RUL. Capital reserves built via sinking fund from year 1. Includes disruption at replacement and a new sinking fund for the next replacement cycle. Baseline comparison scenario.

-- Phase 1: old unit (y = 1 to RUL) --
energy += base_energy_yr1 × df × esc
rm += base_rm × df
reserve += sf_bn_annual

-- Replacement event at RUL --
disruption = inflated_cost_rul × disruption_pct

-- Phase 2: new unit (y = RUL+1 to horizon) --
new_kwton = federalMinKwTon(replacement_year) × 1.10   10% field penalty
energy += new_base_energy × (1.01)ny−1 × esc
reserve += sf_new_annual   new sinking fund for next cycle
New replacement units degrade at only 1%/yr (good condition baseline) since they're brand new. The 10% field penalty (PNNL) reflects real-world installation losses vs. lab-rated nameplate efficiency.

Ener.co treats and monitors equipment. Savings are shared — client keeps their split, Ener.co's share covers ongoing service. Equipment life extends to EUL.

-- Savings split (default 75/25) --
client_pct = 75%     enerco_pct = 25%

-- For each year y: --
yr_savings = energy_sav + rm_sav + cap_sav   (streams toggleable)
enerco_payment = yr_savings × enerco_pct
monthly_payment = enerco_payment / 12

-- Client total cost = reduced energy + reduced R&M + EUL reserves + Enerco payments --
pp_total = energy_with + rm_with + ec_reserves + total_enerco_payments
Capital savings in PP compare the Buy New sinking fund vs. the EUL-based sinking fund at each year. The deferral advantage (spreading replacement cost over more years) is the primary CapEx benefit. Streams (energy, R&M, CapEx) can be individually toggled on/off for the deal structure.

One-time upfront project fee. Client keeps 100% of all savings — no ongoing payments. Same performance and life extension benefits as PP.

project_cost = tons × price_per_ton

ffs_total = energy_with + rm_with + ec_reserves + project_cost

-- Key metrics --
payback = project_cost / yr1_total_savings
ROI = (total_savings project_cost) / project_cost × 100
PricingCondenserEvapBoth
Default $/ton$750$300$1,050

Shows how key metrics change if actual EER improvement differs from the M&V estimate. Five scenarios span ±30% around baseline.

-- Scenarios --
Conservative      → baseline × 0.70   (−30%)
Slightly Below  → baseline × 0.85   (−15%)
Baseline          → baseline × 1.00
Above Baseline  → baseline × 1.15   (+15%)
Strong            → baseline × 1.30   (+30%)

-- Each scenario recalculates independently --
s_sav = sav_pct × multiplier
-- Energy & R&M recalculated at adjusted savings --
-- Capital scales proportionally (capped at 1.0× for conservative) --
ParameterValueSource / Rationale
current_year2026Updated annually
energy_escalation2%/yrEIA long-term reference case for commercial electricity
inflation_rate3.5%/yrFRED PPI HVAC Equipment (PCU3334133341); above pre-COVID trend for R-454B transition + labor
discount_rate7%DOE uses 9% (7% real + 2% inflation); we use 7% conservative
co2_factor (national)0.00037 MT/kWhEPA eGRID 2022 national average (delivered basis)
co2_factor (northeast)0.00030 MT/kWhNEWE+NYCW+NYLI+RFCE avg — hydro/nuclear/gas
co2_factor (southeast)0.00040 MT/kWhSRSO+FRCC+SRVC avg — gas + some coal
co2_factor (midwest)0.00050 MT/kWhRFCW+MROE+MROW avg — coal heavy
co2_factor (southwest)0.00035 MT/kWhERCT+AZNM avg — gas + growing renewables
co2_factor (west)0.00025 MT/kWhCAMX+NWPP avg — hydro/solar/wind
rm_factor4%ASHRAE RP-1237 baseline (% of replacement value/yr)
field_penalty10%PNNL: new unit field derate vs. nameplate
evap_fixed_pct7%Fixed evaporator-only savings component
deg_with0.25%/yrNYBC 13+ yr, NYT 9+ yr monitored data
deg_without (poor)3.5%/yrNREL/DOE Building America (2006)
deg_without (avg)2.0%/yrNREL/DOE Building America (2006)
deg_without (good)1.0%/yrNREL/DOE Building America (2006)
degradation_cap1.5× nameplateEquipment fails before exceeding 50% over nameplate draw
max_savings_floor95% nameplateCannot claim savings below near-nameplate efficiency
eul_extension+10 yearsEUL = RUL + 10; NYBC/NYT long-term data
rul_minimum3 yearsFloor on remaining useful life
rtf_overshoot5 yearsRun to Fail runs 5 years past RUL
rtf_deg_mult1.5×Degradation acceleration during overshoot
rtf_rm_range2.0–3.0×R&M spike during overshoot (ASHRAE RP-1237)
rtf_emergency_premium25%DOE FEMP unplanned replacement cost premium
rtf_disruption_multUnplanned replacement causes double disruption
mv_log_a5.58Log-curve coefficient: 5.58 × ln(age+1)
mv_log_b2Log-curve intercept (base 2% at age 0)
cond_life_poor−3 yrEquipment life offset for poor condition
cond_life_good+2 yrEquipment life offset for good condition
cond_mv_poor1.10×M&V multiplier for poor condition (more fouling)
cond_mv_good0.90×M&V multiplier for good condition (less fouling)

Multi-factor model for total installed replacement cost. Calibrated against Google SF Bay Area facilities data and contractor quotes (2024). Includes equipment, labor, crane, piping, controls integration, startup, and commissioning.

cost_per_ton = min((base × location_index × facility_mult) + access_adder, $12,000)
total_cost = cost_per_ton × tonnage

Base Cost by Equipment Type × Tonnage Tier ($/ton)

Equipment TypeSmall (≤10T)Mid (11–25T)Large (26–50T)XL (51+T)
split$5,500$4,000
ptac$4,500
rtu$3,500$2,800$2,200$2,500
chiller_air$4,000$3,000$2,500$2,800
chiller_water$3,500$2,800$3,200
crac$8,000$7,000$6,000
ahu$2,800$2,200$1,800$2,000

Location Cost Index (national avg = 1.00)

MetroIndexMetroIndex
nyc_metro1.42dallas0.92
sf_bay1.38houston0.90
boston1.28atlanta0.92
la_metro1.22phoenix0.88
seattle1.18rural_northeast0.95
dc_metro1.15rural_southeast0.82
chicago1.12rural_midwest0.85
philadelphia1.08rural_west0.90
miami1.05

Regional fallbacks: northeast=1.12, southeast=0.92, midwest=1.05, southwest=0.90, west=1.15, tropical=1.10

Facility Type Multiplier

FacilityMultFacilityMult
office1.00healthcare1.40
retail1.00hospital1.50
warehouse0.90data_center1.70
education1.00pharma / lab1.50
hospitality1.15food_processing1.30
multifamily1.00clean_room1.60
industrial1.10mission_critical1.65

Access Complexity Adder ($/ton)

Access LevelAdderNotes
standard$0Ground level, easy access
rooftop+$250Crane required
mechanical_room+$350Interior rigging
high_rise+$500>10 floors
confined+$450Tight spaces

Equipment Cost Inflation

PeriodCAGRSource
Pre-COVID 2015–20193.1%/yrFRED PPI PCU3334133341
Post-COVID 2020–20258.0%/yrFRED PPI PCU3334133341
Recent 2024–20252.2–3.3%/yrACHR News PPI Report
Full period 2015–20255.3%/yrFRED PPI PCU3334133341
Forward rate (model)3.5%/yrAbove pre-COVID trend; R-454B transition, labor, materials

Validation — Google SF Bay Area Facilities Data

ScenarioCalculationResultGoogle Range
1T split, SF, office$5,500 × 1.38 × 1.0 + $0$7,590/ton$3K–$7.5K ✓
100T chiller_air, SF, DC, roof$2,800 × 1.38 × 1.70 + $250$6,818/ton ($682K)$250K–$600K+ ✓
100T chiller_air, SF, office$2,800 × 1.38 × 1.0 + $0$3,864/ton ($386K)
30T CRAC, NYC, data center$7,000 × 1.42 × 1.70 + $0$12,000/ton (capped)
$12,000/ton hard cap prevents unrealistic stacking of multipliers (e.g. NYC × data center × CRAC). Even the most extreme real-world scenarios rarely exceed $12K/ton fully installed. All values can be overridden per-unit in M&V project.json or per-proposal in the estimator custom $/ton field.

📊 Interactive Cost Model Charts (for sales walkthrough)

Part II — Dashboard Savings (Sensor-Based)

These formulas are used by the live dashboard once sensors are installed. They use real amperage and EER data instead of model-based estimates. Source: dashboard/calculations.py.

Measures the reduction in electrical consumption from improved EER after coating. Uses the escalating degradation gap method — untreated equipment degrades faster than treated, widening the savings gap each year.

-- Per-unit power draw (kW) — 3-phase --
multiplier = voltage × √3 × power_factor / 1,000
kW_untreated = baseline_amperage × multiplier

-- Per-unit power draw (kW) — 1-phase --
multiplier = voltage × power_factor / 1,000

-- Treated draw assumes EER returns toward SEER-10 baseline --
kW_treated = kW_untreated × 10 / baseline_EER
Only applies when baseline_EER > 10 (SEER-10 federal minimum)

-- Annual energy cost (Year 0 baseline) --
annual_energy_untreated = kW_untreated × EFLH × electricity_rate
annual_energy_treated = kW_treated × EFLH × electricity_rate

-- Year N savings with degradation escalation --
energy_savings(N) = annual_energy_untreated × (1 + deg_without)N
                        annual_energy_treated × (1 + deg_with)N
VariableSourceDefault
baseline_amperageUnit baseline (sensor or manual)
voltageUnit equipment spec208V
phaseUnit equipment spec3-phase
power_factorUnit equipment spec0.85
baseline_EERUnit baseline (sensor or manual)
electricity_rateSite setting$0.20/kWh
EFLHUnit override > site default2,000 hrs
deg_withoutUnit: coil degradation without coating2%/yr
deg_withUnit: coil degradation with coating1%/yr

Baseline annual R&M cost is a percentage of total replacement cost. Coating reduces R&M proportionally to the EER efficiency improvement. Both untreated and treated R&M costs escalate annually, but at different rates.

-- Replacement cost --
replacement_cost = tonnage × replacement_cost_per_ton

-- Baseline annual R&M --
baseline_rm = rm_pct × replacement_cost

-- EER improvement factor --
eer_improvement = (baseline_EER 10) / baseline_EER

-- Treated R&M (reduced by EER improvement) --
treated_rm = baseline_rm × (1 eer_improvement)

-- Year N savings with escalation --
rm_savings(N) = baseline_rm × (1 + rm_esc_untreated)N
                  treated_rm × (1 + rm_esc_treated)N
VariableSourceDefault
tonnageUnit equipment spec
replacement_cost_per_tonUnit setting$3,000 (>15T) / $2,200 (≤15T)
rm_pctSite: baseline R&M %4%
baseline_EERUnit baseline
rm_esc_untreatedSite: R&M escalation (untreated)2%/yr
rm_esc_treatedSite: R&M escalation (treated)1%/yr

M&V CapEx savings use NPV / EUL Linear Amortization. For each unit we compute the present value of deferring its replacement from RUL to EUL, then recognize that NPV linearly over the unit's extended useful life. This is purposely conservative for backward-looking M&V — we never claim savings that elapsed time hasn't yet earned.

-- Per unit --
RUL = max(3, install_year + useful_life current_year)
EUL = RUL + extension_yrs
replace_cost = tonnage × cost_per_ton

-- Present value of replacement events --
pv_without = replace_cost × ((1 + infl) / (1 + r))RUL
pv_with = replace_cost × ((1 + infl) / (1 + r))EUL
npv_unit = pv_without pv_with

-- Recognition: M&V always uses EUL (cap_at_horizon = false) --
annual_rate_unit = npv_unit / EUL   years 1..EUL, then 0

-- Fleet aggregation --
capex_yearly(y) = Σunits annual_rate_unit if y ≤ EULunit
npv_deferral_total = Σunits npv_unit
VariableSourceDefault
replace_costtonnage × replacement_cost_per_ton
r (discount_rate)project.json7%
infl (rm_pct as proxy)project.json3%
useful_life_yrsproject.json18 yrs
extension_yrsproject.json10 yrs
RULinstall_year + useful_life − current_year (floor 3)
EULRUL + extension_yrs
Why amortize over EUL (not horizon)? M&V reports actuals. Recognizing the entire NPV in year 1 would overstate realized savings; spreading it over EUL matches the period during which the deferred replacement remains in service.

Forward proposals (Estimator/Proposer) use the same formula but cap recognition at min(EUL, project_horizon) so the full deal NPV is shown within the contract term.

Sinking fund mode remains available as a toggle for backward-compatible display.

The dashboard counters show cumulative savings accruing in real time since each site's coating date.

-- Elapsed time --
elapsed = now coating_date
elapsed_yrs = elapsed / seconds_per_year

-- Cumulative savings = sum of each full year + partial current year --
cumulative(T) = Σy=0..floor(T)-1 annual_savings(y) + annual_savings(floor(T)) × frac(T)

-- Current accrual rate (for counter animation) --
rate_per_sec = annual_savings(current_year) / seconds_per_year
The SSE stream pushes updated cumulative values every 1 second. The browser uses requestAnimationFrame to smoothly interpolate between ticks.
ATCO_without = annual_energy_untreated + baseline_rm + reserve_without
ATCO_with = annual_energy_treated + treated_rm + reserve_with

total_savings = ATCO_without ATCO_with
savings_pct = total_savings / ATCO_without × 100

Part III — Sensor & Live Reading Calculations

Monnit current sensors report raw amperage. We convert to kW using per-unit voltage, phase configuration, and power factor, then to kWh by multiplying by the time interval between readings.

-- Instantaneous power (3-phase) --
kW = amperage × voltage × √3 × power_factor / 1,000

-- Instantaneous power (1-phase) --
kW = amperage × voltage × power_factor / 1,000

-- Energy consumed between two readings --
dt_hrs = (t₂ t₁) / 3,600
kWh = kW × dt_hrs

-- Annual projection --
annual_kWh = kW × EFLH
annual_cost = annual_kWh × electricity_rate
VariableSourceNotes
amperageMonnit current sensor reading20A or 150A sensor variants
voltageUnit equipment specDefault 208V
phaseUnit equipment spec3-phase uses √3 multiplier
power_factorUnit equipment specDefault 0.85
EFLHEstimated from sensors or unit/site overrideEquivalent full-load hours/yr
electricity_rateSite setting$/kWh

Measures the heat energy removed by the evaporator coil using moist-air enthalpy from dry-bulb temperature and relative humidity at inlet and outlet.

-- Step 1: Convert dry-bulb °F → °C --
T_c = (T_f 32) / 1.8

-- Step 2: Saturation vapor pressure (Buck equation, kPa) --
Pws = 0.61121 × exp( (18.678 T_c / 234.5) × (T_c / (257.14 + T_c)) )

-- Step 3: Actual vapor pressure --
Pw = (RH / 100) × Pws

-- Step 4: Humidity ratio (lb water / lb dry air) --
Pw_psi = Pw × 0.14504  kPa → psi
W = 0.62198 × Pw_psi / (14.696 Pw_psi)

-- Step 5: Moist air enthalpy (BTU/lb) --
h = 0.240 × T_f + W × (1061 + 0.444 × T_f)

-- Enthalpy delta (coil heat extraction) --
Δh = h_inlet h_outlet
Inlet and outlet readings are matched by nearest timestamp within a 5-minute window. The chart shows Δh over time — a declining trend may indicate coil fouling.

Temperature spread between compressor discharge and condenser outlet. Not true subcooling (requires pressure), but a practical proxy.

ΔT_condenser = T_discharge T_condenser_outlet
Low (< 10°F): possible low charge or poor airflow. OK (10–20°F): healthy heat rejection. High (> 20°F): possible liquid line restriction or overcharge.

Real-time EER from live sensor data. Combines enthalpy delta (cooling output) with amperage (power input).

CFM = tonnage × cfm_per_ton  (default 400 CFM/ton)

BTU/hr = Δh × CFM × 4.5  (4.5 = 60 min × 0.075 lb/ft³)

Watts = Amps × Voltage

EER = BTU/hr / Watts
Reference lines at EER 10 (minimum), 12 (baseline), 14 (good). Points with EER < 6 or > 25 flagged as suspect.
is_ON = amperage amp_on_threshold  (default 6.0 A)

runtime_hours = Σ interval_hours where is_ON

PLF = avg_amps_when_on / max_amps_in_window

EFLH_runtime = runtime_hours × PLF
BTU_interval = Δh × CFM × 4.5 × interval_hours
full_load_BTU_hr = tonnage × 12,000

EFLH_energy = Σ BTU_interval / full_load_BTU_hr
Requires evap inlet/outlet temp/RH sensors. Returns null if not mapped — graceful degradation to Method A only.

Scales measured EFLH to full-year estimate using Cooling Degree Hours (CDH, base 65°F) from Open-Meteo weather data.

CDH = Σ max(0, temp_f 65)   per hour from weather data
CDH_scale = CDH_annual / CDH_current_period

EFLH_annual = min(EFLH_runtime × CDH_scale, EFLH_energy × CDH_scale)
Confidence scoring: < 2 weeks → Low • 2–6 weeks → Moderate • > 6 weeks → Good • Full season → High • CDH < 5% annual → Low (winter penalty)
Q_total = h_inlet h_outlet
Q_sensible = 0.240 × (T_inlet T_outlet)
Q_latent = Q_total Q_sensible
Latent % = Q_latent / Q_total × 100
Typical commercial HVAC: 20–40% latent fraction. Higher outdoor humidity → higher latent fraction.

Plots instantaneous EER vs outdoor temperature. Linear regression quantifies efficiency degradation at higher ambient temps.

slope = (n·Σxy Σx·Σy) / (n·Σx² (Σx)²)
intercept = (Σy slope·Σx) / n

SS_res = Σ (y_i (slope·x_i + intercept))²
SS_tot = Σ (y_i ȳy
= 1 SS_res / SS_tot
Higher R² means CDH-based annualization is more trustworthy. Points filtered: 6 ≤ EER ≤ 25. Fixed x-axis: 20–100°F.
avg_amps_hour = mean(amps in hour)   when amps ≥ threshold
max_amps_unit = max(amps) across all data
load_factor = avg_amps_hour / max_amps_unit
Per-unit regression with R² validates CDH annualization. Fixed axes: x = 20–100°F, y = 0–1.0.
avg_EER = mean(EER_i)   filtered 6 ≤ EER ≤ 25

avg_kW = avg_amps_on × voltage × phase_mult × power_factor / 1,000

subcool_ΔT = mean(T_discharge T_condenser_outlet)  matched ± 5 min
MetricColor CodingThresholds
Avg EERGreen / Yellow / Red≥ 14 / ≥ 10 / < 10
Subcool ΔTGreen / Yellow / Red10–20°F / < 10°F / > 20°F

Part IV — IPMVP Option B Baseline Readiness

These metrics assess whether a unit’s baseline data collection is sufficient for IPMVP Option B (retrofit isolation with metered data) M&V analysis. Computed in dashboard/baseline.py and displayed in the Site Workshop.

IPMVP Option B isolates the retrofit measure (coil coating) by metering the individual HVAC unit before and after treatment. The full IPMVP spec has ~15 criteria covering whole-building Option C and multi-system retrofits. For individual unit isolation with a passive coating ECM, 7 metrics are the ones that actually gate baseline readiness.

What doesn’t apply (and why):

CriterionWhy Excluded
CV-RMSE / NMBEModel-fit outputs, not baseline prerequisites. Shown as informational only.
FSU (Fractional Savings Uncertainty)Requires post-treatment data. Irrelevant during baseline collection.
12-month ruleFormal spec says 12 months for weather-sensitive measures. Direct metering + OAT range coverage across the cooling season is a defensible short-term Option B. OAT range matters, not calendar time.
Non-routine adjustmentsApply at reporting/analysis time, not during collection.
Meter certifications, t-statsApply at reporting/analysis time.

The entire M&V analysis is kW-vs-OAT. Gaps in temperature bins = gaps in the savings story. We bin outdoor air temperature into 5°F buckets across the cooling range.

-- 5°F bins from 60°F to 100°F --
bins = [60–65), [65–70), [70–75), [75–80), [80–85), [85–90), [90–95), [95–100)

filled_bins = count(bins with ≥ 1 compressor-on reading)

-- Pass criterion --
Target: filled_bins 5
8 possible bins. Requiring 5 ensures coverage across most of the cooling range without penalizing sites that never see extreme temperatures. Only compressor-on readings (kW ≥ threshold) are counted.

Statistical power per temperature bin. Sparse bins produce noisy averages that weaken the regression.

thinnest_bin = min(count per filled bin)

Target: thinnest_bin 10 readings
Only filled bins are considered (empty bins are an OAT coverage issue, not a per-bin density issue). 10 readings per bin is the minimum for a defensible bin average.

Overall dataset size for regression confidence. More points = tighter confidence intervals on the baseline model.

total_on = count(readings where kW min_kw AND OAT is not null)

-- kW derivation --
kW = amperage × voltage × phase_mult × PF / 1,000

Target: total_on 500
VariableSourceDefault
voltageunits.voltage208
phase_mult√3 if 3-phase, 1 if single√3
PFunits.power_factor0.85
min_kwunits.amp_on_threshold × V × √3 × PF / 1000~1.8 kW

Missing intervals create a biased sample. Completeness is estimated from the median reporting interval vs actual reading count.

median_gap = median(consecutive timestamp differences)
expected = total_span / median_gap
completeness = min(actual_count / expected, 1.0)

Target: completeness 90%
Using median gap (not mean) avoids being skewed by a single long outage. Completeness is capped at 100%.

Calendar days from first to last compressor-on reading. Short windows risk capturing only one weather pattern.

mon_days = (last_timestamp first_timestamp) / 86,400

Target: mon_days 14
14 days captures at least two weekday/weekend cycles and likely some weather variation. The formal IPMVP 12-month rule is relaxed because direct metering + OAT bin coverage provides equivalent statistical power for a passive ECM.

Does electrical power actually correlate with outdoor air temperature? Low R² indicates an oversized unit, variable internal loads, or staging behavior that masks the OAT relationship.

-- scipy.stats.linregress(OAT, kW) --
slope, intercept, r_value, p, stderr = linregress(oat[], kw[])
= r_value²

Target: 0.75
R² ≥ 0.75 means OAT explains at least 75% of kW variance. Values below this still allow baseline collection to continue but flag that the OAT-based savings model may have limited explanatory power for this unit. Minimum 10 data points required for regression.

All configured sensor channels (source_mappings) must have at least one reading. kW + OAT are the minimum for Option B; supply/return temp+RH enable EER calculation.

total_channels = count(source_mappings for unit)
reporting = count(channels with ≥ 1 raw_reading)

Target: reporting = total_channels  (100%)
A channel that was mapped but never reported data indicates a configuration error, dead sensor, or connectivity problem that must be resolved before baseline can be considered complete.

Composite score combining all 7 metrics. Simple average of individual percentages (each capped at 100%).

metric_pcti = min(valuei / targeti × 100, 100)
overall_pct = mean(metric_pct1..7)
StatusColorCondition
ReadyGreenoverall ≥ 90% AND no metric below 60%
PartialAmberoverall ≥ 50% (but not meeting Ready criteria)
CollectingRedoverall < 50%
The “no metric below 60%” gate prevents a unit from being marked Ready when it has excellent coverage in most areas but a critical gap in one (e.g. only 2 OAT bins filled, or no sensor channels reporting).

These are computed and displayed for reference but do not gate baseline readiness. They become gating criteria at the analysis/reporting stage.

-- CV-RMSE (Coefficient of Variation of RMSE) --
predictedi = slope × OATi + intercept
RMSE = √(mean((predicted actual)²))
CV-RMSE = (RMSE / mean(kW)) × 100%
MetricTypical RangeWhen It Matters
CV-RMSE< 25% good, < 15% excellentModel fit quality — assessed at analysis time
NMBE± 5%Model bias — assessed at analysis time
FSUVariesRequires post-treatment data
ASHRAE Guideline 14 thresholds for hourly data: CV-RMSE ≤ 30%, NMBE ≤ 10%. For our sub-hourly metered data with direct measurement (Option B), these are typically well within bounds. They’re shown in the Site Workshop IPMVP card for early visibility.

After computing the efficiency improvement (Methods A–D in the M&V skill), translate to annual kWh and $/yr savings. Always compute multiple approaches and present as a comparison table. Each M&V library entry shows all applicable approaches with the primary highlighted.

CodeMethodFormulaBest When
A kW-only kW_reduction% × avg_baseline_kW × EFLH kW/CT-only data, no coil-face RH sensors
B EER-based (humidity-normalized) EER_improvement% × avg_baseline_kW × EFLH Full sensor suite with humidity normalization. Default when data supports it.
C Decomposed (kW + sensible) (kW_share + sensible_share) × EER% × kW × EFLH When latent component is controversial or audience demands conservatism
E Regression × TMY ∑(kW_pre(OAT) − kW_post(OAT)) over TMY hours Gold standard IPMVP. Accounts for actual temp distribution.
F Vendor’s method, corrected inputs savings_frac × avg_baseline_kW × EFLH Apples-to-apples with vendor; or third-party validated (e.g. NYSERDA)
Primary Approach Selection
Data AvailablePrimaryRationale
Full sensors + humidity-normalized EERBFull thermodynamic picture including latent capacity
Third-party validated (NYSERDA, utility regression)FDon’t override independently validated savings
OAT-dependent improvement (oversized unit)EImprovement concentrated at high OAT; uniform % overstates
kW-only / CT dataAOnly honest option without psychrometric data
Mixed fleet, no common EERA per-unitCan’t apply a single EER% to heterogeneous equipment
EER Improvement Decomposition

Required on every entry with evap inlet/outlet T+RH data. Decompose total EER improvement at matched OAT × humidity conditions into three components:

ComponentMechanismWeather Sensitivity
kW reduction Cleaner condenser → lower head pressure → less compressor work Low — direct mechanical improvement
Sensible capacity Colder supply air from improved heat transfer Low — stable across conditions
Latent capacity Lower evap temp → more moisture condensation High — varies with ambient humidity
-- Decomposition at matched (OAT, h_return) bins --
sensible_tons = 1.08 × CFM × (T_return T_supply) / 12,000
latent_tons = total_tons sensible_tons
kW_component = tons_pre × 12 / kW_post tons_pre × 12 / kW_pre
latent_component = total_ΔEER kW_component sensible_component
Every M&V library entry includes: (1) a justification paragraph explaining why the primary approach was chosen, (2) the decomposition table + stacked bar chart, and (3) the full approach comparison table. The primary approach is agreed upon with the analyst before finalizing the entry. See SKILL.md §9 for the complete methodology and audience-based recommendations.

A first-principles diagnostic that estimates what fraction of the air entering the evaporator coil is outdoor air vs. recirculated return air. This is a heuristic sanity check, not an IPMVP method or a correction factor for savings calculations. It exists to explain unusual EER-vs-OAT patterns (e.g. positive slope) so engineers don’t mistakenly flag them as data quality issues.

Mixed Air Temperature Equation

For a unit with a fixed outdoor air fraction, the temperature of the mixed air entering the evap coil is:

Tmixed = OA% × Toutdoor + (1 OA%) × Treturn

If Treturn is roughly constant (thermostatically controlled zone), then a linear regression of evap inlet temperature against outdoor air temperature yields OA% as the slope. A slope of 0.72 means ~72% outdoor air, 28% recirculated.

Method
-- Data: evap_inlet_rh sensor temp paired with weather OAT at same hour --
-- Requires ≥30 matched readings, sampled up to 2,000 --

slope, intercept, r, _, _ = linregress(weather_OAT, inlet_temp)
= r²
estimated_OA% = clamp(slope × 100, 0, 100)   -- only if R² ≥ 0.4
What R² Means Here
Interpretation
≥ 0.7Consistent OA fraction — fixed dampers or code-mandated minimum OA. Reliable estimate.
0.4 – 0.7Moderate correlation — could be variable dampers (economizer), occupancy-driven return air swings, or inconsistent fan scheduling. Estimate is approximate.
< 0.4OAT does not drive inlet temp — mostly recirculated air, or too much noise. No OA% reported.
Why High-OA Units Can Show Positive EER-vs-OAT Slope

For a standard recirculated-air RTU, the evap load is roughly constant (thermostat-controlled zone). Higher OAT means higher condensing temperature → compressor works harder → EER drops.

For a high-OA unit, the mechanism is part-load efficiency: at low OAT the evap load is very small (outdoor air barely needs cooling), so the compressor runs at 20–30% capacity where DX part-load curves are steep and inefficient. As OAT rises, load increases toward the 60–80% capacity sweet spot where most compressors peak in COP. This can produce a positive EER-vs-OAT slope up to the part-load peak.

Above the part-load peak (often around 80–85°F OAT depending on the unit’s specific performance curves), condensing pressure effects dominate and EER should begin to decline — the familiar negative slope returns. The full curve may therefore be inverted-U shaped: rising EER through part-load, peaking, then falling at full load. More data at higher OATs is needed to confirm where the inflection occurs for any given unit.

Variable airflow rate may also contribute: if the unit modulates fan speed with load, the changing CFM affects the enthalpy calculation and can steepen the positive slope in the part-load region.

Common High-OA Applications
ApplicationTypical OA%Reference
MRI / imaging suites70–100%ASHRAE 170 Table 7-1
Procedure rooms, ORs100% (code)ASHRAE 170
Labs / clean rooms100%Pressurization requirement
Data center economizersVariable (0–100%)ASHRAE TC 9.9
Printing / manufacturing30–80%VOC / particulate exhaust makeup
Does OA% affect savings calculations? No. Our bin-matched kW and EER comparisons (pre vs post at same OAT) are valid regardless of OA fraction because the OA% is a constant physical characteristic of the unit — it doesn’t change pre-to-post treatment. TMY3-weighted annual projections are also unaffected. The OA% is purely a pattern-explanation diagnostic that prevents engineers from misinterpreting EER chart shapes. It is not an IPMVP method, not a correction factor, and not an input to any savings formula.
Limitation — economizer units: If a unit has a variable-OA damper (economizer), the OA fraction changes with OAT. Below the switchover (~55°F) it may be 100% OA; above, it drops to minimum code ventilation. The linear regression gives a misleading “average” in this case. A nonlinear inlet-vs-OAT relationship is the telltale sign. This does not affect savings calculations but means the reported OA% should be interpreted as approximate.