@operato/twin-kernel 0.7.14 → 0.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1210,8 +1210,27 @@ export interface MeterPointState {
1210
1210
  export interface DemandWindowState {
1211
1211
  startMs: number;
1212
1212
  endMs: number;
1213
- /** 그 구간의 최대 순간부하 — 표본이 없으면 `undefined`(0 이 아니다). */
1213
+ /**
1214
+ * 그 구간의 최대 순간부하 — 표본이 없으면 `undefined`(0 이 아니다).
1215
+ *
1216
+ * **계통에서 끌어온 수요**다(요금이 이것에 매겨진다). 현장에 발전·축전이 있으면 설비가 끌어당긴
1217
+ * 양과 다르다 — 그 차이는 `grossMaxKW` 와의 간격으로 읽는다.
1218
+ */
1214
1219
  maxKW?: number;
1220
+ /**
1221
+ * 설비가 **끌어당긴** 최대(발전·방전 차감 전, 충전 포함).
1222
+ *
1223
+ * ── 왜 둘을 함께 두나 (2026-08-17) ────────────────────────────────────────
1224
+ * 요금은 계통 수요(`maxKW`)로 매겨지지만, 「무엇이 그 수요를 만들었나」는 설비 부하가 답한다.
1225
+ * 하나만 두면 둘 중 하나를 잃는다: 순수요만 두면 태양광이 잠깐 가려졌을 때 왜 피크가 올랐는지 설명할
1226
+ * 수 없고, 총부하만 두면 요금이 실제보다 커 보인다.
1227
+ *
1228
+ * 그리고 **이 둘의 간격이 곧 깎인 양**이다(피크 셰이빙). 같은 표본에서 나온 두 최대값이므로 비교가
1229
+ * 성립한다 — 서로 다른 창의 값을 견주는 것이 아니다.
1230
+ *
1231
+ * 발전·축전을 선언하지 않은 현장에서는 `maxKW` 와 같다(그때는 차감할 것이 없다).
1232
+ */
1233
+ grossMaxKW?: number;
1215
1234
  /** 표본 평균 부하 — 요금 산정의 평균 수요에 대응한다. */
1216
1235
  meanKW?: number;
1217
1236
  /** **kW 를 실은** 표본 수 — 부하 판정의 근거 수다(받은 표본 전체가 아니다). */
@@ -309,6 +309,9 @@ export class EmsKernel extends FlowEngine {
309
309
  startMs: w.startMs,
310
310
  endMs: w.endMs,
311
311
  ...(w.maxKW !== undefined ? { maxKW: w.maxKW } : {}),
312
+ /* 총부하도 사실로 남긴다 — 이것이 없으면 저널을 읽는 쪽은 「무엇이 깎였나」를 되짚을 수 없다
313
+ (요금은 순수요로 매겨지지만, 그 수요를 만든 것은 설비 부하다). */
314
+ ...(w.grossMaxKW !== undefined ? { grossMaxKW: w.grossMaxKW } : {}),
312
315
  ...(w.meanKW !== undefined ? { meanKW: w.meanKW } : {}),
313
316
  samples: w.samples,
314
317
  ...(w.contractKW !== undefined ? { contractKW: w.contractKW } : {}),
@@ -450,14 +453,50 @@ export class EmsKernel extends FlowEngine {
450
453
  }
451
454
  if (!counted)
452
455
  return; // 셀 것이 하나도 없으면 구간에 값을 넣지 않는다(0 을 주장하지 않는다)
456
+ /*
457
+ * ── 계통에서 끌어오는 것은 **순수요**다 (2026-08-17) ──────────────────────
458
+ *
459
+ * 그동안 이 함수는 소비만 셌다. 그래서 태양광이 400kW 를 만들고 배터리가 300kW 를 내보내도 계통
460
+ * 수요가 한 톨도 줄지 않았다 — 「배터리로 피크를 깎는다」가 시뮬에서 성립하지 않는 상태였고,
461
+ * 그 위에 세운 요금·계약 판정도 그만큼 틀렸다.
462
+ *
463
+ * 요금이 매겨지는 것은 계량 지점을 지나는 전력이다:
464
+ *
465
+ * 순수요 = 설비 부하 + 충전 − 발전 − 방전
466
+ *
467
+ * 충전이 **더해지는** 이유: 배터리를 채우는 동안 그 전기도 계통에서 온다(피크를 만들 수 있다 —
468
+ * 잘못된 시각에 충전하면 깎으려던 피크를 오히려 키운다. 그 사실이 값에 보여야 한다).
469
+ *
470
+ * 음수는 **역송**이지 음의 수요가 아니다. 계통 수요는 0 에서 멈추고, 남는 발전은 그 설비의
471
+ * `exportKW` 가 이미 사실로 말한다(여기서 다시 만들지 않는다).
472
+ */
473
+ let generated = 0;
474
+ let discharged = 0;
475
+ let charged = 0;
476
+ for (const m of this.equipment.values()) {
477
+ const g = Number(m.generatedKW);
478
+ if (Number.isFinite(g) && g > 0)
479
+ generated += g;
480
+ const d = Number(m.dischargeKW);
481
+ if (Number.isFinite(d) && d > 0)
482
+ discharged += d;
483
+ const c = Number(m.chargeKW);
484
+ if (Number.isFinite(c) && c > 0)
485
+ charged += c;
486
+ }
487
+ const gross = total + charged;
488
+ const net = Math.max(0, gross - generated - discharged);
453
489
  this.closeDue(atMs);
454
490
  this.openWindow(atMs);
455
491
  const w = this.open;
456
492
  w.derived = true;
457
- if (w.maxKW === undefined || total > w.maxKW)
458
- w.maxKW = total;
493
+ /* 요금이 보는 수는 순수요다. 총부하는 「무엇이 그 수요를 만들었나」를 위해 함께 남긴다. */
494
+ if (w.maxKW === undefined || net > w.maxKW)
495
+ w.maxKW = net;
496
+ if (w.grossMaxKW === undefined || gross > w.grossMaxKW)
497
+ w.grossMaxKW = gross;
459
498
  w.samples++;
460
- w.meanKW = w.meanKW === undefined ? total : (w.meanKW * (w.samples - 1) + total) / w.samples;
499
+ w.meanKW = w.meanKW === undefined ? net : (w.meanKW * (w.samples - 1) + net) / w.samples;
461
500
  this.revision++;
462
501
  this.judgeOpenWindow(atMs);
463
502
  }
@@ -185,6 +185,15 @@ export interface WindowedEnergy {
185
185
  peakKW?: number;
186
186
  /** 그 최대수요가 난 구간의 시작 — 「언제였나」를 물으면 답할 수 있어야 한다. */
187
187
  peakStartMs?: number;
188
+ /**
189
+ * **그 피크 구간에서 설비가 끌어당긴 최대**(발전·방전 차감 전).
190
+ *
191
+ * `peakKW` 와의 간격이 그 순간에 깎인 양이다(태양광·배터리가 계통에서 덜어낸 몫). 다른 구간의 값과
192
+ * 견주지 않는다 — **같은 구간의 두 수**여야 그 차이가 뜻을 갖는다.
193
+ *
194
+ * 발전·축전을 선언하지 않은 현장에서는 `peakKW` 와 같다.
195
+ */
196
+ peakGrossKW?: number;
188
197
  /**
189
198
  * 어떻게 얻었나 — **파생임을 숨기지 않는다.**
190
199
  * · `mean-kw` — 구간 평균부하 × 구간 길이. 표본의 평균이므로 **추정**이다.
@@ -217,6 +226,7 @@ export declare function energyOfWindows(windows: readonly {
217
226
  endMs: number;
218
227
  meanKW?: number;
219
228
  maxKW?: number;
229
+ grossMaxKW?: number;
220
230
  }[], range?: {
221
231
  startMs: number;
222
232
  endMs: number;
@@ -205,6 +205,7 @@ export function energyOfWindows(windows, range) {
205
205
  let last;
206
206
  let peakKW;
207
207
  let peakStartMs;
208
+ let peakGrossKW;
208
209
  for (const w of windows ?? []) {
209
210
  if (!inRange(w))
210
211
  continue;
@@ -216,6 +217,9 @@ export function energyOfWindows(windows, range) {
216
217
  if (Number.isFinite(max) && (peakKW === undefined || max > peakKW)) {
217
218
  peakKW = max;
218
219
  peakStartMs = Number(w.startMs);
220
+ /* **그 구간의** 총부하를 함께 집는다 — 다른 구간의 총부하와 견주면 차이가 뜻을 잃는다. */
221
+ const gross = Number(w.grossMaxKW);
222
+ peakGrossKW = Number.isFinite(gross) ? gross : undefined;
219
223
  }
220
224
  const mean = Number(w.meanKW);
221
225
  const hours = (Number(w.endMs) - Number(w.startMs)) / 3_600_000;
@@ -233,6 +237,7 @@ export function energyOfWindows(windows, range) {
233
237
  return {
234
238
  ...(counted > 0 ? { kWh } : {}),
235
239
  ...(peakKW !== undefined ? { peakKW, peakStartMs } : {}),
240
+ ...(peakGrossKW !== undefined ? { peakGrossKW } : {}),
236
241
  basis: 'mean-kw',
237
242
  counted,
238
243
  skipped,
@@ -6019,6 +6019,9 @@ var EmsKernel = class extends FlowEngine {
6019
6019
  startMs: w.startMs,
6020
6020
  endMs: w.endMs,
6021
6021
  ...w.maxKW !== void 0 ? { maxKW: w.maxKW } : {},
6022
+ /* 총부하도 사실로 남긴다 — 이것이 없으면 저널을 읽는 쪽은 「무엇이 깎였나」를 되짚을 수 없다
6023
+ (요금은 순수요로 매겨지지만, 그 수요를 만든 것은 설비 부하다). */
6024
+ ...w.grossMaxKW !== void 0 ? { grossMaxKW: w.grossMaxKW } : {},
6022
6025
  ...w.meanKW !== void 0 ? { meanKW: w.meanKW } : {},
6023
6026
  samples: w.samples,
6024
6027
  ...w.contractKW !== void 0 ? { contractKW: w.contractKW } : {},
@@ -6144,13 +6147,27 @@ var EmsKernel = class extends FlowEngine {
6144
6147
  }
6145
6148
  }
6146
6149
  if (!counted) return;
6150
+ let generated = 0;
6151
+ let discharged = 0;
6152
+ let charged = 0;
6153
+ for (const m of this.equipment.values()) {
6154
+ const g = Number(m.generatedKW);
6155
+ if (Number.isFinite(g) && g > 0) generated += g;
6156
+ const d = Number(m.dischargeKW);
6157
+ if (Number.isFinite(d) && d > 0) discharged += d;
6158
+ const c = Number(m.chargeKW);
6159
+ if (Number.isFinite(c) && c > 0) charged += c;
6160
+ }
6161
+ const gross = total + charged;
6162
+ const net = Math.max(0, gross - generated - discharged);
6147
6163
  this.closeDue(atMs);
6148
6164
  this.openWindow(atMs);
6149
6165
  const w = this.open;
6150
6166
  w.derived = true;
6151
- if (w.maxKW === void 0 || total > w.maxKW) w.maxKW = total;
6167
+ if (w.maxKW === void 0 || net > w.maxKW) w.maxKW = net;
6168
+ if (w.grossMaxKW === void 0 || gross > w.grossMaxKW) w.grossMaxKW = gross;
6152
6169
  w.samples++;
6153
- w.meanKW = w.meanKW === void 0 ? total : (w.meanKW * (w.samples - 1) + total) / w.samples;
6170
+ w.meanKW = w.meanKW === void 0 ? net : (w.meanKW * (w.samples - 1) + net) / w.samples;
6154
6171
  this.revision++;
6155
6172
  this.judgeOpenWindow(atMs);
6156
6173
  }
@@ -6461,12 +6478,15 @@ function energyOfWindows(windows, range) {
6461
6478
  let last;
6462
6479
  let peakKW;
6463
6480
  let peakStartMs;
6481
+ let peakGrossKW;
6464
6482
  for (const w of windows ?? []) {
6465
6483
  if (!inRange(w)) continue;
6466
6484
  const max = Number(w.maxKW);
6467
6485
  if (Number.isFinite(max) && (peakKW === void 0 || max > peakKW)) {
6468
6486
  peakKW = max;
6469
6487
  peakStartMs = Number(w.startMs);
6488
+ const gross = Number(w.grossMaxKW);
6489
+ peakGrossKW = Number.isFinite(gross) ? gross : void 0;
6470
6490
  }
6471
6491
  const mean = Number(w.meanKW);
6472
6492
  const hours = (Number(w.endMs) - Number(w.startMs)) / 36e5;
@@ -6482,6 +6502,7 @@ function energyOfWindows(windows, range) {
6482
6502
  return {
6483
6503
  ...counted > 0 ? { kWh } : {},
6484
6504
  ...peakKW !== void 0 ? { peakKW, peakStartMs } : {},
6505
+ ...peakGrossKW !== void 0 ? { peakGrossKW } : {},
6485
6506
  basis: "mean-kw",
6486
6507
  counted,
6487
6508
  skipped,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.14",
3
+ "version": "0.7.15",
4
4
  "type": "module",
5
5
  "description": "Twin Domain Kernel — framework-agnostic, zero-dep (domain + sim + 3-channel contract). WMS/YMS/MES, EPCIS 2.0 · ISA-95.",
6
6
  "publishConfig": {