File size: 13,361 Bytes
802f0fa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
/** OEH teaching curves — same model as backend/services/oeh_guide_physics.py. Always SIMULATED. */
(function (g) {
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
  const avg = (arr) => (arr.length ? arr.reduce((s, n) => s + n, 0) / arr.length : 0);
  const oat = (hour, mean, amp) => mean + amp * Math.cos((hour - 15) * (Math.PI / 12));
  const loadCurve = (hour) => clamp(Math.sin(((hour - 6) / 14) * Math.PI), 0.05, 1);
  const occupancyCurve = (hour) => clamp(Math.sin(((hour - 8) / 10) * Math.PI), 0.03, 1);
  const f = (v, key, d) => {
    const n = Number(v[key]);
    return Number.isFinite(n) ? n : d;
  };
  const pct = (n) => `${n.toFixed(0)}%`;

  function pointAt(oid, x, v) {
    if (oid === "O1") {
      const occ = f(v, "occStart", 8.5);
      const sev = f(v, "severity", 50);
      const baseStart = occ - 2.5, baseStop = 17.5;
      const optStart = occ - (0.5 + (sev / 100) * 2);
      const optStop = 17.5 - (0.1 + ((100 - sev) / 100) * 0.4);
      return [baseStart <= x && x < baseStop ? 100 : 0, optStart <= x && x < optStop ? 100 : 0];
    }
    if (oid === "O2") {
      const db = f(v, "deadBand", 1);
      const pb = f(v, "propBand", 1);
      const dev = clamp(3 * Math.sin(((x - 9) / 14) * Math.PI), -3, 3);
      const out = (d, band, p) => {
        const half = band / 2;
        if (Math.abs(d) <= half) return 0;
        return clamp(((Math.abs(d) - half) / Math.max(p, 0.01)) * 100, 0, 100);
      };
      return [out(dev, 1, 1), out(dev, db, pb)];
    }
    if (oid === "O3") {
      const phases = [0, 1.2, 2.4, 3.6, 4.8];
      const devs = phases.map((ph) => Math.max(0, 2.5 * Math.sin(((x - 8 + ph) / 12) * Math.PI)));
      devs[4] += f(v, "faultBias", 1.5);
      const baseline = Math.max(...devs);
      const n = Math.min(Math.trunc(f(v, "numAvg", 3)), 5);
      const sorted = [...devs].sort((a, b) => b - a);
      const optimized = sorted.slice(0, n).reduce((s, n) => s + n, 0) / n;
      return [baseline, optimized];
    }
    if (oid === "O4") {
      const load = (f(v, "peakLoad", 75) / 100) * loadCurve(x);
      const delay = f(v, "stageDelay", 10);
      const baseline = clamp(Math.ceil(load * 3 + 0.45), 0, 3);
      const optimized = clamp(Math.ceil(load * 3 - ((delay - 5) / 15) * 0.3), 0, 3);
      return [baseline, optimized];
    }
    if (oid === "O5") {
      const demand = (f(v, "demandAmp", 80) / 100) * loadCurve(x);
      const openNeeded = 40 + demand * 60;
      const optSpeed = clamp(100 * (openNeeded / Math.max(f(v, "targetOpen", 92), 1)), 25, 100);
      return [100, (optSpeed / 100) ** 3 * 100];
    }
    if (oid === "O6") {
      const demand = (f(v, "heatSeverity", 60) / 100) * (1 - loadCurve(x));
      const floor = f(v, "boilerType", 1) ? 40 : 55;
      return [82, floor + demand * (82 - floor)];
    }
    if (oid === "O7") {
      const load = (f(v, "loadSeverity", 80) / 100) * loadCurve(x);
      return [6.5, 6.5 + (1 - load) * 5.5];
    }
    if (oid === "O8") {
      const wb = oat(x, f(v, "wetBulbMean", 20), 4);
      return [f(v, "wetBulbMean", 20) + 11.5, wb + f(v, "approach", 3.5)];
    }
    if (oid === "O9") {
      const load = 100 * loadCurve(x) * (f(v, "loadVar", 60) / 100) + (1 - f(v, "loadVar", 60) / 100) * 70;
      const baseline = 5 + (100 - load) * 0.08;
      const optimized = f(v, "valveType", 0) ? 2 + (100 - load) * 0.02 : baseline;
      return [baseline, optimized];
    }
    if (oid === "O10") {
      const temp = oat(x, f(v, "oatMean", 18), 6);
      const active = temp < 21 && f(v, "dewPoint", 10) < 12;
      const demand = 100 * loadCurve(x);
      return [demand, active ? demand * 0.15 : demand];
    }
    if (oid === "O11") {
      const residual = f(v, "residual", 27);
      const low = f(v, "overnightLow", 16);
      if (x < 4) return [residual - 1, residual - 1];
      if (x < 7.5) {
        const pf = clamp((x - 4) / 2.5, 0, 1);
        return [residual, residual - pf * Math.max(0, residual - 2 - low)];
      }
      return [23, 23];
    }
    if (oid === "O12") {
      const occ = f(v, "peakOcc", 80) * occupancyCurve(x);
      return [100, clamp(30 + occ * 0.7, 30, 100)];
    }
    if (oid === "O13") {
      const density = f(v, "peakDensity", 55) * occupancyCurve(x);
      const speed = clamp(25 + density * 0.75, 25, 100);
      return [100, (speed / 100) ** 3 * 100];
    }
    if (oid === "O14") {
      const load = (f(v, "loadAmp", 75) / 100) * loadCurve(x);
      const speed = clamp(100 * ((40 + load * 60) / 95), 30, 100);
      return [100, (speed / 100) ** 3 * 100];
    }
    if (oid === "O15") return [95, clamp((oat(x, f(v, "ambientMean", 20), 6) - 2) * 2.8, 25, 100)];
    if (oid === "O16") {
      const load = (f(v, "loadAmp", 75) / 100) * loadCurve(x);
      return [100, clamp(100 * (1 - f(v, "idlePct", 20) / 100) * (0.4 + 0.6 * load), 20, 100)];
    }
    if (oid === "O17") return [100 + x * 1.6, 100 - (f(v, "coordScore", 55) / 100) * x * 3.2];
    if (oid === "O18") return [100 + x * 0.3, 100 - (f(v, "coverage", 50) / 100) * x * 0.9];
    if (oid === "O19") {
      const base = 100 + x * 1.2;
      return [base, base * (1 - clamp(f(v, "freq", 4) / 12, 0, 1))];
    }
    if (oid === "O20") {
      const baseline = clamp(100 - x * 4, 40, 100);
      const factor = f(v, "accessCtrl", 1) * 0.5 + clamp(f(v, "backupFreq", 4) / 12, 0, 1) * 0.5;
      return [baseline, clamp(100 - x * 4 * (1 - factor), 40, 100)];
    }
    return [100, 100];
  }

  function seriesFor(oid, sliders, xType) {
    const n = xType === "month" ? 12 : 24;
    const out = [];
    for (let x = 0; x < n; x++) {
      const [b, o] = pointAt(oid, x, sliders);
      out.push({ x, baseline: Math.round(b * 1000) / 1000, optimized: Math.round(o * 1000) / 1000 });
    }
    return out;
  }

  function metricsFor(oid, pts, v) {
    const ab = avg(pts.map((p) => p.baseline));
    const ao = avg(pts.map((p) => p.optimized));
    const last = pts[pts.length - 1] || { baseline: 0, optimized: 0 };
    if (oid === "O1") {
      const bh = pts.filter((p) => p.baseline > 0).length;
      const oh = pts.filter((p) => p.optimized > 0).length;
      const red = bh ? ((bh - oh) / bh) * 100 : 0;
      return [
        { label: "Baseline runtime", value: `${bh} h/day` },
        { label: "Optimized runtime", value: `${oh} h/day` },
        { label: "Operating hours cut", value: pct(red) },
      ];
    }
    if (oid === "O2") {
      const red = ab ? ((ab - ao) / ab) * 100 : 0;
      return [
        { label: "Baseline avg output", value: `${ab.toFixed(0)}%` },
        { label: "Optimized avg output", value: `${ao.toFixed(0)}%` },
        { label: "Est. HVAC energy cut", value: pct(red) },
      ];
    }
    if (oid === "O3") {
      const red = ab ? ((ab - ao) / ab) * 100 : 0;
      return [
        { label: "Baseline signal (high-select)", value: `${ab.toFixed(1)}°C` },
        { label: "Optimized signal (weighted)", value: `${ao.toFixed(1)}°C` },
        { label: "Over-cooling reduced", value: pct(red) },
      ];
    }
    if (oid === "O4") {
      const red = ab ? clamp(((ab - ao) / ab) * 100, 0, 10) : 0;
      return [
        { label: "Avg stages — baseline", value: ab.toFixed(1) },
        { label: "Avg stages — optimized", value: ao.toFixed(1) },
        { label: "Chiller-hours saved", value: pct(red) },
      ];
    }
    if (oid === "O5")
      return [
        { label: "Baseline fan power", value: "100%" },
        { label: "Optimized fan power", value: `${ao.toFixed(0)}%` },
        { label: "Fan energy saved", value: pct(clamp(100 - ao, 0, 30)) },
      ];
    if (oid === "O6")
      return [
        { label: "Baseline flow temp", value: "82°C" },
        { label: "Optimized avg flow temp", value: `${ao.toFixed(0)}°C` },
        { label: "Boiler efficiency gain", value: `${clamp((82 - ao) * 0.15, 0, 5).toFixed(1)}%` },
      ];
    if (oid === "O7") {
      const per = f(v, "compType", 0) ? 4.5 : 2.5;
      return [
        { label: "Baseline CHW temp", value: "6.5°C" },
        { label: "Optimized avg CHW temp", value: `${ao.toFixed(1)}°C` },
        { label: "Chiller energy saved", value: pct(clamp((ao - 6.5) * per, 0, 15)) },
      ];
    }
    if (oid === "O8") {
      const sav = clamp((ab - ao) * 2.5, 0, 15);
      return [
        { label: "Baseline CW temp", value: `${ab.toFixed(1)}°C` },
        { label: "Optimized avg CW temp", value: `${ao.toFixed(1)}°C` },
        { label: "Chiller energy saved", value: pct(sav) },
      ];
    }
    if (oid === "O9") {
      const sav = clamp((ab - ao) * 3, 0, 15);
      return [
        { label: "Baseline efficiency loss", value: `${ab.toFixed(1)}%` },
        { label: "Optimized efficiency loss", value: `${ao.toFixed(1)}%` },
        { label: "Compressor energy saved", value: pct(sav) },
      ];
    }
    if (oid === "O10") {
      const hours = pts.filter((p) => p.optimized < p.baseline * 0.9).length;
      const sav = ab ? clamp(((ab - ao) / ab) * 100, 0, 20) : 0;
      return [
        { label: "Economy cycle active", value: `${hours} h/day` },
        { label: "Optimized avg compressor load", value: `${ao.toFixed(0)}%` },
        { label: "Compressor energy saved", value: pct(sav) },
      ];
    }
    if (oid === "O11") {
      const window = pts.filter((p) => p.x >= 4 && p.x < 7.5);
      const minOpt = window.length ? Math.min(...window.map((p) => p.optimized)) : f(v, "residual", 27);
      const residual = f(v, "residual", 27);
      return [
        { label: "Residual temp", value: `${residual.toFixed(1)}°C` },
        { label: "Purged down to", value: `${minOpt.toFixed(1)}°C` },
        { label: "Start-up energy saved", value: pct(clamp((residual - minOpt) * 4, 0, 20)) },
      ];
    }
    if (oid === "O12") {
      const est = Math.round(400 + (f(v, "peakOcc", 80) / 100) * (f(v, "co2SP", 800) - 400));
      return [
        { label: "Optimized avg OA flow", value: `${ao.toFixed(0)}%` },
        { label: "Peak CO₂ estimate", value: `${est} ppm` },
        { label: "Ventilation energy saved", value: pct(clamp(100 - ao, 0, 20)) },
      ];
    }
    if (oid === "O13")
      return [
        { label: "Baseline fan power", value: "100%" },
        { label: "Optimized avg fan power", value: `${ao.toFixed(0)}%` },
        { label: "Fan energy saved", value: pct(clamp(100 - ao, 0, 80)) },
      ];
    if (oid === "O14")
      return [
        { label: "Baseline pump power", value: "100%" },
        { label: "Optimized avg pump power", value: `${ao.toFixed(0)}%` },
        { label: "Pumping energy saved", value: pct(clamp(100 - ao, 0, 30)) },
      ];
    if (oid === "O15") {
      const sav = ab ? clamp(((ab - ao) / ab) * 100, 0, 30) : 0;
      return [
        { label: "Baseline fan power", value: `${ab.toFixed(0)}%` },
        { label: "Optimized avg fan power", value: `${ao.toFixed(0)}%` },
        { label: "Condenser fan energy saved", value: pct(sav) },
      ];
    }
    if (oid === "O16")
      return [
        { label: "Baseline pump power", value: "100%" },
        { label: "Optimized avg pump power", value: `${ao.toFixed(0)}%` },
        { label: "CW pump energy saved", value: pct(clamp(100 - ao, 0, 30)) },
      ];
    if (oid === "O17") {
      const sav = last.baseline ? clamp(((last.baseline - last.optimized) / last.baseline) * 100, 0, 50) : 0;
      return [
        { label: "Year-end index — no plan", value: last.baseline.toFixed(0) },
        { label: "Year-end index — with plan", value: last.optimized.toFixed(0) },
        { label: "Total energy saved", value: pct(sav) },
      ];
    }
    if (oid === "O18") {
      const cov = f(v, "coverage", 50);
      const sav = last.baseline ? clamp(((last.baseline - last.optimized) / last.baseline) * 100, 0, 10) : 0;
      return [
        { label: "Total energy saved", value: pct(sav) },
        { label: "Est. NABERS star gain", value: `+${((cov / 100) * 0.5).toFixed(1)} ★` },
        { label: "Training coverage", value: `${cov.toFixed(0)}%` },
      ];
    }
    if (oid === "O19") {
      const sav = last.baseline ? clamp(((last.baseline - last.optimized) / last.baseline) * 100, 0, 20) : 0;
      return [
        { label: "Year-end index — reactive", value: last.baseline.toFixed(0) },
        { label: "Year-end index — maintained", value: last.optimized.toFixed(0) },
        { label: "HVAC energy saved", value: pct(sav) },
      ];
    }
    if (oid === "O20") {
      const sav = clamp((last.optimized - last.baseline) * 0.2, 0, 10);
      return [
        { label: "Settings retained — baseline", value: `${last.baseline.toFixed(0)}%` },
        { label: "Settings retained — managed", value: `${last.optimized.toFixed(0)}%` },
        { label: "HVAC energy saved", value: pct(sav) },
      ];
    }
    const sav = ab ? clamp(((ab - ao) / ab) * 100, 0, 80) : 0;
    return [
      { label: "Baseline avg", value: ab.toFixed(1) },
      { label: "Optimized avg", value: ao.toFixed(1) },
      { label: "Guide reduction", value: pct(sav) },
    ];
  }

  g.OehPhysics = { seriesFor, metricsFor, clamp };
})(window);