@operato/twin-kernel 0.0.1
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.
- package/README.md +80 -0
- package/dist/allocation-policy.d.ts +48 -0
- package/dist/allocation-policy.js +50 -0
- package/dist/contract.d.ts +205 -0
- package/dist/contract.js +20 -0
- package/dist/counterfactual.d.ts +40 -0
- package/dist/counterfactual.js +59 -0
- package/dist/divergence.d.ts +15 -0
- package/dist/divergence.js +28 -0
- package/dist/duration-estimator.d.ts +12 -0
- package/dist/duration-estimator.js +10 -0
- package/dist/epcis.d.ts +125 -0
- package/dist/epcis.js +172 -0
- package/dist/event-journal.d.ts +17 -0
- package/dist/event-journal.js +41 -0
- package/dist/face2-adapter.d.ts +42 -0
- package/dist/face2-adapter.js +69 -0
- package/dist/flow-engine.d.ts +224 -0
- package/dist/flow-engine.js +422 -0
- package/dist/forecast.d.ts +29 -0
- package/dist/forecast.js +34 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/kernel.d.ts +28 -0
- package/dist/kernel.js +187 -0
- package/dist/mes-kernel.d.ts +28 -0
- package/dist/mes-kernel.js +126 -0
- package/dist/mes-profile.d.ts +9 -0
- package/dist/mes-profile.js +17 -0
- package/dist/runtime.d.ts +42 -0
- package/dist/runtime.js +59 -0
- package/dist/state-projector.d.ts +37 -0
- package/dist/state-projector.js +124 -0
- package/dist/twin-observer.d.ts +28 -0
- package/dist/twin-observer.js +41 -0
- package/dist/wms-profile.d.ts +13 -0
- package/dist/wms-profile.js +20 -0
- package/dist/yms-kernel.d.ts +29 -0
- package/dist/yms-kernel.js +181 -0
- package/dist/yms-profile.d.ts +11 -0
- package/dist/yms-profile.js +22 -0
- package/dist-cjs/index.cjs +1477 -0
- package/package.json +30 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { BoardDef, Command, CommandAck, EventHandler, MoverMotion, GeneratorSpec, ScenarioControl, StateSnapshot, TwinKernel, Unsubscribe } from './contract.ts';
|
|
2
|
+
import type { EpcisEvent, BizTransactionElement } from './epcis.ts';
|
|
3
|
+
import type { AllocationPolicy, SlotView } from './allocation-policy.ts';
|
|
4
|
+
import type { DurationEstimator, DurationContext } from './duration-estimator.ts';
|
|
5
|
+
export interface FlowNode {
|
|
6
|
+
id: string;
|
|
7
|
+
type: string;
|
|
8
|
+
capacity: number;
|
|
9
|
+
occupancy: number;
|
|
10
|
+
status: string;
|
|
11
|
+
}
|
|
12
|
+
export interface FlowItem {
|
|
13
|
+
epc: string;
|
|
14
|
+
location: string;
|
|
15
|
+
disposition: string;
|
|
16
|
+
gtin?: string;
|
|
17
|
+
qty?: number;
|
|
18
|
+
expiry?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface FlowMover {
|
|
21
|
+
id: string;
|
|
22
|
+
kind: string;
|
|
23
|
+
location: string;
|
|
24
|
+
status: string;
|
|
25
|
+
taskId: string | null;
|
|
26
|
+
runMs: number;
|
|
27
|
+
setupMs: number;
|
|
28
|
+
downMs: number;
|
|
29
|
+
goodCount: number;
|
|
30
|
+
scrapCount: number;
|
|
31
|
+
lastChangeoverKey?: string;
|
|
32
|
+
mtbfMs?: number;
|
|
33
|
+
mttrMs?: number;
|
|
34
|
+
nextFailureMs?: number;
|
|
35
|
+
repairUntilMs?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface FlowTask {
|
|
38
|
+
id: string;
|
|
39
|
+
kind: string;
|
|
40
|
+
status: 'created' | 'in-progress' | 'completed';
|
|
41
|
+
itemEpc: string;
|
|
42
|
+
fromNode: string;
|
|
43
|
+
toNode: string;
|
|
44
|
+
resource: string | null;
|
|
45
|
+
remainingMs: number;
|
|
46
|
+
durationMs: number;
|
|
47
|
+
orderId?: string;
|
|
48
|
+
resourceType?: string;
|
|
49
|
+
setupMs?: number;
|
|
50
|
+
changeoverKey?: string;
|
|
51
|
+
appliedSetupMs?: number;
|
|
52
|
+
/**
|
|
53
|
+
* 작업 의도(씬 3 직교의도 정렬 · ISA-95 이동 vs 변환). 기본 'transport'.
|
|
54
|
+
* transport: mover 가 item 을 fromNode→toNode 운반(완료 시 자원 위치 이동 + 이동 모션).
|
|
55
|
+
* process: 노드에서 item 변환(자원=정지 설비 — 위치 불변, 모션 없음). 가공을 거리-0 운반으로 위장 안 함.
|
|
56
|
+
* dwell: 노드에서 시간만 소비(무자원 체류·큐어링 — 즉시 진행, 자원 불요).
|
|
57
|
+
*/
|
|
58
|
+
intent?: 'transport' | 'process' | 'dwell';
|
|
59
|
+
}
|
|
60
|
+
/** 오더 라인(멀티SKU) — 라인별 품목·잔량. 단일 SKU 오더 = 1 라인. */
|
|
61
|
+
export interface OrderLine {
|
|
62
|
+
gtin: string;
|
|
63
|
+
requested: number;
|
|
64
|
+
}
|
|
65
|
+
export interface FlowOrder {
|
|
66
|
+
id: string;
|
|
67
|
+
kind: string;
|
|
68
|
+
status: string;
|
|
69
|
+
requested: number;
|
|
70
|
+
fulfilled: number;
|
|
71
|
+
bizTransaction: string;
|
|
72
|
+
allocated: string[];
|
|
73
|
+
picked: string[];
|
|
74
|
+
gtin?: string;
|
|
75
|
+
shipmentEpc?: string | null;
|
|
76
|
+
lines?: OrderLine[];
|
|
77
|
+
held?: boolean;
|
|
78
|
+
dockDoor?: string;
|
|
79
|
+
windowStartMs?: number;
|
|
80
|
+
}
|
|
81
|
+
interface Rng {
|
|
82
|
+
(): number;
|
|
83
|
+
state: number;
|
|
84
|
+
}
|
|
85
|
+
export declare abstract class FlowEngine implements TwinKernel {
|
|
86
|
+
tenantId: string;
|
|
87
|
+
nodes: Map<string, FlowNode>;
|
|
88
|
+
items: Map<string, FlowItem>;
|
|
89
|
+
movers: Map<string, FlowMover>;
|
|
90
|
+
tasks: Map<string, FlowTask>;
|
|
91
|
+
orders: Map<string, FlowOrder>;
|
|
92
|
+
revision: number;
|
|
93
|
+
clockMs: number;
|
|
94
|
+
protected rng: Rng;
|
|
95
|
+
protected policy: AllocationPolicy;
|
|
96
|
+
/** duration 시임(선택) — 미주입 시 도메인 상수. 씬/보드 바인딩이 거리·속도 기반 estimator 주입. */
|
|
97
|
+
durationEstimator?: DurationEstimator;
|
|
98
|
+
protected epcSeq: number;
|
|
99
|
+
protected taskSeq: number;
|
|
100
|
+
protected orderSeq: number;
|
|
101
|
+
protected soSeq: number;
|
|
102
|
+
private handlers;
|
|
103
|
+
private gens;
|
|
104
|
+
private generating;
|
|
105
|
+
private speed;
|
|
106
|
+
private eventSeq;
|
|
107
|
+
constructor(tenantId: string, policy: AllocationPolicy);
|
|
108
|
+
/** 입고/도착 자극 — 아이템 생성 + EPCIS 방출 + 반입 태스크 생성. */
|
|
109
|
+
protected abstract onArrival(spec: GeneratorSpec): void;
|
|
110
|
+
/** 출고/출차 오더 자극 — 오더 생성. */
|
|
111
|
+
protected abstract onOrder(spec: GeneratorSpec): void;
|
|
112
|
+
/** created 오더 할당 — 재고 선택 + 태스크 생성(정책 위임). */
|
|
113
|
+
protected abstract allocate(order: FlowOrder): void;
|
|
114
|
+
/**
|
|
115
|
+
* 태스크 완료 시 도메인 상태 효과 전부 — occupancy·location·아이템 변환·EPCIS 방출·오더 이행.
|
|
116
|
+
* base 는 task 생명주기(진행·완료·자원해제·델타)만 소유하고 "완료가 무엇을 의미하는지"는 도메인에.
|
|
117
|
+
* 이동(WMS/YMS: from→to 이동) 이든 변환(MES: 소비→생산) 이든 여기서 결정 → base 가 이동-중립.
|
|
118
|
+
*/
|
|
119
|
+
protected abstract onTaskComplete(task: FlowTask): void;
|
|
120
|
+
loadBoard(def: BoardDef): void;
|
|
121
|
+
onEvent(handler: EventHandler): Unsubscribe;
|
|
122
|
+
/**
|
|
123
|
+
* Command 채널 — 트윈의 "행위(act)" 면. 코어 공통 커맨드(order.hold/resume)는 여기서,
|
|
124
|
+
* 도메인 커맨드(order.release 등)는 handleCommand 로 위임. 커맨드는 sim 상태를 변이하고
|
|
125
|
+
* State 델타를 유발한다(command → 행위 → 관측 폐루프).
|
|
126
|
+
*/
|
|
127
|
+
dispatch(cmd: Command): CommandAck;
|
|
128
|
+
/** 도메인 커맨드 처리(order.release 등). 기본은 거절 — 도메인이 override. */
|
|
129
|
+
protected handleCommand(cmd: Command): CommandAck;
|
|
130
|
+
readonly scenario: ScenarioControl;
|
|
131
|
+
tick(dtMs: number): void;
|
|
132
|
+
getSnapshot(): StateSnapshot;
|
|
133
|
+
/**
|
|
134
|
+
* fork — 현재 상태를 정확히 복제한 새 엔진 (디지털트윈 본연: "현재로부터 예측").
|
|
135
|
+
* 원본(live/sim)은 계속 진행, fork 는 what-if 를 앞으로 굴려 forecast·발산(predicted vs actual) 검사에 쓴다.
|
|
136
|
+
* fork 는 자기 구독자·시나리오를 갖고 원본과 격리(handlers·gens 비움, generating=false).
|
|
137
|
+
* rng 는 fork 의 시나리오 load 시 재시드(드레인 예측은 생성 없어 rng 무관·결정적).
|
|
138
|
+
*/
|
|
139
|
+
fork(tenantId?: string): this;
|
|
140
|
+
protected now(): string;
|
|
141
|
+
protected randInt(min: number, max: number): number;
|
|
142
|
+
/** task 소요 산출 — estimator 주입 시 그 값, 미주입/undefined 시 도메인 상수(fallback). "얼마"만 소비, "경로"는 씬. */
|
|
143
|
+
protected durationOf(ctx: DurationContext, fallbackMs: number): number;
|
|
144
|
+
/** skuMix 에서 weight 로 gtin 선택(rng) — 도착/오더 자극의 품목 결정. */
|
|
145
|
+
protected pickGtin(mix: {
|
|
146
|
+
gtin: string;
|
|
147
|
+
weight: number;
|
|
148
|
+
}[]): string;
|
|
149
|
+
protected nodeByType(type: string): FlowNode | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* process 변화 대수(EPCIS TransformationEvent · ISA-95 Material Consumed/Produced · 씬 Processable.transform).
|
|
152
|
+
* inputs 소비 → outputs 생산. 입출력 arity 가 곧 대수:
|
|
153
|
+
* merge N→1(조립) · split 1→N(분해) · transform 1→1(타입변경) · loss N→0(소실) · gain 0→N(부산물·생성).
|
|
154
|
+
* 아이템 상태(소비/생산)와 노드 점유를 갱신하고 계보(TransformationEvent)를 방출한다.
|
|
155
|
+
* 도메인은 이 원시만 호출 — onTaskComplete 의 소비/생산/EPCIS 손코딩을 대체.
|
|
156
|
+
*/
|
|
157
|
+
protected transform(inputs: string[], outputs: {
|
|
158
|
+
epc: string;
|
|
159
|
+
gtin?: string;
|
|
160
|
+
qty?: number;
|
|
161
|
+
location: string;
|
|
162
|
+
disposition: string;
|
|
163
|
+
}[], opts: {
|
|
164
|
+
bizStep: string;
|
|
165
|
+
disposition?: string;
|
|
166
|
+
transformationId?: string;
|
|
167
|
+
readPoint: string;
|
|
168
|
+
bizLocation?: string;
|
|
169
|
+
bizTransactionList?: BizTransactionElement[];
|
|
170
|
+
}): void;
|
|
171
|
+
/**
|
|
172
|
+
* containment 조립(EPCIS AggregationEvent ADD) — 자식들을 부모(용기)로 집約.
|
|
173
|
+
* consume 지정 시 자식이 컨테이너로 흡수되며 독립 아이템에서 이탈(dematerialize: ObjectEvent DELETE + 제거).
|
|
174
|
+
* 미지정 시 이벤트만(자식은 독립 아이템 유지 — 예: WMS 패킹, 적재된 채 도착).
|
|
175
|
+
*/
|
|
176
|
+
protected aggregate(parent: string, children: string[], opts: {
|
|
177
|
+
bizStep: string;
|
|
178
|
+
readPoint: string;
|
|
179
|
+
bizLocation?: string;
|
|
180
|
+
disposition?: string;
|
|
181
|
+
consume?: {
|
|
182
|
+
readPoint: string;
|
|
183
|
+
disposition: string;
|
|
184
|
+
};
|
|
185
|
+
}): void;
|
|
186
|
+
/**
|
|
187
|
+
* containment 분해(EPCIS AggregationEvent DELETE) — 부모(용기)에서 자식들을 풀어냄.
|
|
188
|
+
* materialize 지정 시 자식이 독립 아이템으로 등장(materialize: 노드 배치 + occupancy + ObjectEvent ADD).
|
|
189
|
+
* 미지정 시 이벤트만.
|
|
190
|
+
*/
|
|
191
|
+
protected disaggregate(parent: string, children: string[], opts: {
|
|
192
|
+
bizStep: string;
|
|
193
|
+
readPoint: string;
|
|
194
|
+
materialize?: {
|
|
195
|
+
location: string;
|
|
196
|
+
disposition: string;
|
|
197
|
+
};
|
|
198
|
+
}): void;
|
|
199
|
+
/** 품질 보고(OEE Quality) — 도메인이 완료 시 자원별 양품/불량 1건 계상(예: 용접 수율). */
|
|
200
|
+
protected recordOutput(moverId: string | null, good: boolean): void;
|
|
201
|
+
/** 무버 OEE(스냅샷 파생) — Availability×Performance×Quality. planned = 설비 존재 sim 시간(clockMs). */
|
|
202
|
+
private oeeOf;
|
|
203
|
+
/** 정책에 넘길 특정 타입 노드의 관측 뷰 — 예약(그 노드로 향하는 in-flight task) 포함. */
|
|
204
|
+
protected slotViews(nodeType: string): SlotView[];
|
|
205
|
+
protected emit(event: EpcisEvent): void;
|
|
206
|
+
protected emitOp(eventType: string, data: unknown): void;
|
|
207
|
+
protected emitTask(t: FlowTask): void;
|
|
208
|
+
protected emitMover(m: FlowMover, motion?: MoverMotion): void;
|
|
209
|
+
protected emitOrder(o: FlowOrder): void;
|
|
210
|
+
private intervalMs;
|
|
211
|
+
/** 지수분포 표본(고장/수리 간격) — mean 을 평균으로 하는 무기억 프로세스. */
|
|
212
|
+
private sampleExp;
|
|
213
|
+
/**
|
|
214
|
+
* 확률적 설비 고장(MTBF/MTTR) — mtbf 지정 무버만 참여(미지정=고장 없음, rng 무소비 → byte-identical).
|
|
215
|
+
* up: nextFailure 도래 시 down(수리까지 repairUntil). down: downMs 누적, repair 도래 시 up(다음 고장 예약).
|
|
216
|
+
* down 중 무버는 배정 불가 + 진행중 task 동결(processTasks 가 skip) → OEE Availability 손실.
|
|
217
|
+
*/
|
|
218
|
+
private processFailures;
|
|
219
|
+
protected progressOf(t: FlowTask): number;
|
|
220
|
+
private generate;
|
|
221
|
+
private processOrders;
|
|
222
|
+
private processTasks;
|
|
223
|
+
}
|
|
224
|
+
export {};
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* FlowEngine — 도메인-일반 flow 엔진 base (WMS·YMS 공유 mechanics 추출).
|
|
3
|
+
*
|
|
4
|
+
* WMS/YMS 커널의 mechanics(RNG·clock·scenario·tick 루프·무버 배정·태스크 진행·emit·snapshot·
|
|
5
|
+
* slotViews)는 동일했다 → 이 base 로 승격. 도메인 kernel 은 **flow 동사만** 구현:
|
|
6
|
+
* onArrival(자극:입고 도착) · onOrder(자극:출고 오더) · allocate(오더→태스크) · onTaskComplete(태스크 완료)
|
|
7
|
+
*
|
|
8
|
+
* TwinKernel 구현 → StateProjector·TwinRuntime 가 도메인 무관하게 소비.
|
|
9
|
+
* 통합 타입은 도메인 필드를 옵셔널로 넓혀(FlowItem.gtin?, FlowOrder.shipmentEpc? 등) 두 도메인을 담는다.
|
|
10
|
+
* (roadmap Phase5 발견 → 추출. [[project_flow_single_base_vision]] FlowNode 단일 base 방향과 정합.)
|
|
11
|
+
*/
|
|
12
|
+
import { OP_EVENT, CMD } from "./contract.js";
|
|
13
|
+
import { transformationEvent, aggregationEvent, objectEvent } from "./epcis.js";
|
|
14
|
+
const BASE_EPOCH = Date.parse('2026-01-01T00:00:00Z');
|
|
15
|
+
function mulberry32(seed) {
|
|
16
|
+
let a = seed >>> 0;
|
|
17
|
+
const fn = (() => {
|
|
18
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
19
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
20
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
21
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(fn, 'state', { get: () => a, set: (v) => { a = v >>> 0; } });
|
|
24
|
+
return fn;
|
|
25
|
+
}
|
|
26
|
+
export class FlowEngine {
|
|
27
|
+
tenantId;
|
|
28
|
+
nodes = new Map();
|
|
29
|
+
items = new Map();
|
|
30
|
+
movers = new Map();
|
|
31
|
+
tasks = new Map();
|
|
32
|
+
orders = new Map();
|
|
33
|
+
revision = 0;
|
|
34
|
+
clockMs = 0;
|
|
35
|
+
rng = mulberry32(1);
|
|
36
|
+
policy;
|
|
37
|
+
/** duration 시임(선택) — 미주입 시 도메인 상수. 씬/보드 바인딩이 거리·속도 기반 estimator 주입. */
|
|
38
|
+
durationEstimator;
|
|
39
|
+
epcSeq = 0;
|
|
40
|
+
taskSeq = 0;
|
|
41
|
+
orderSeq = 0;
|
|
42
|
+
soSeq = 0;
|
|
43
|
+
handlers = [];
|
|
44
|
+
gens = [];
|
|
45
|
+
generating = false;
|
|
46
|
+
speed = 1;
|
|
47
|
+
eventSeq = 0;
|
|
48
|
+
constructor(tenantId, policy) {
|
|
49
|
+
this.tenantId = tenantId;
|
|
50
|
+
this.policy = policy;
|
|
51
|
+
}
|
|
52
|
+
// ── TwinKernel (mechanics, 도메인 무관) ───────────────────────────────────
|
|
53
|
+
loadBoard(def) {
|
|
54
|
+
for (const n of def.nodes)
|
|
55
|
+
this.nodes.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, occupancy: 0, status: 'idle' });
|
|
56
|
+
for (const m of def.movers) {
|
|
57
|
+
const mover = { id: m.id, kind: m.kind, location: m.homeNode, status: 'idle', taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 };
|
|
58
|
+
if (m.mtbfMs !== undefined) {
|
|
59
|
+
mover.mtbfMs = m.mtbfMs;
|
|
60
|
+
mover.mttrMs = m.mttrMs;
|
|
61
|
+
mover.nextFailureMs = this.sampleExp(m.mtbfMs);
|
|
62
|
+
}
|
|
63
|
+
this.movers.set(m.id, mover);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
onEvent(handler) {
|
|
67
|
+
this.handlers.push(handler);
|
|
68
|
+
return () => { const i = this.handlers.indexOf(handler); if (i >= 0)
|
|
69
|
+
this.handlers.splice(i, 1); };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Command 채널 — 트윈의 "행위(act)" 면. 코어 공통 커맨드(order.hold/resume)는 여기서,
|
|
73
|
+
* 도메인 커맨드(order.release 등)는 handleCommand 로 위임. 커맨드는 sim 상태를 변이하고
|
|
74
|
+
* State 델타를 유발한다(command → 행위 → 관측 폐루프).
|
|
75
|
+
*/
|
|
76
|
+
dispatch(cmd) {
|
|
77
|
+
const ok = () => ({ commandId: cmd.commandId, accepted: true });
|
|
78
|
+
const fail = (error) => ({ commandId: cmd.commandId, accepted: false, error });
|
|
79
|
+
switch (cmd.type) {
|
|
80
|
+
case CMD.orderHold:
|
|
81
|
+
case CMD.orderResume: {
|
|
82
|
+
const orderId = cmd.args?.orderId;
|
|
83
|
+
const order = orderId ? this.orders.get(orderId) : undefined;
|
|
84
|
+
if (!order)
|
|
85
|
+
return fail(`order 없음: ${orderId}`);
|
|
86
|
+
order.held = cmd.type === CMD.orderHold;
|
|
87
|
+
this.emitOrder(order);
|
|
88
|
+
return ok();
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
return this.handleCommand(cmd);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** 도메인 커맨드 처리(order.release 등). 기본은 거절 — 도메인이 override. */
|
|
95
|
+
handleCommand(cmd) {
|
|
96
|
+
return { commandId: cmd.commandId, accepted: false, error: `알 수 없는 커맨드: ${cmd.type}` };
|
|
97
|
+
}
|
|
98
|
+
scenario = {
|
|
99
|
+
load: (def) => {
|
|
100
|
+
this.rng = mulberry32((def.seed ?? 1) >>> 0);
|
|
101
|
+
this.speed = def.speed ?? 1;
|
|
102
|
+
this.gens = def.generators.map(spec => ({ spec, nextMs: 0 }));
|
|
103
|
+
},
|
|
104
|
+
start: () => {
|
|
105
|
+
if (this.generating)
|
|
106
|
+
return;
|
|
107
|
+
this.generating = true;
|
|
108
|
+
for (const g of this.gens)
|
|
109
|
+
g.nextMs = this.clockMs + this.intervalMs(g.spec);
|
|
110
|
+
},
|
|
111
|
+
pause: () => { this.generating = false; },
|
|
112
|
+
reset: () => { this.generating = false; this.gens = []; },
|
|
113
|
+
setSpeed: (f) => { this.speed = f; }
|
|
114
|
+
};
|
|
115
|
+
tick(dtMs) {
|
|
116
|
+
const dt = dtMs * this.speed;
|
|
117
|
+
this.clockMs += dt;
|
|
118
|
+
if (this.generating)
|
|
119
|
+
this.generate();
|
|
120
|
+
this.processFailures(dt);
|
|
121
|
+
this.processOrders();
|
|
122
|
+
this.processTasks(dt);
|
|
123
|
+
}
|
|
124
|
+
getSnapshot() {
|
|
125
|
+
return {
|
|
126
|
+
revision: this.revision,
|
|
127
|
+
simClockMs: this.clockMs,
|
|
128
|
+
nodes: [...this.nodes.values()].map(n => ({ ...n })),
|
|
129
|
+
items: [...this.items.values()].map(i => ({ epc: i.epc, gtin: i.gtin, qty: i.qty, location: i.location, disposition: i.disposition, expiry: i.expiry })),
|
|
130
|
+
movers: [...this.movers.values()].map(m => {
|
|
131
|
+
const s = { id: m.id, kind: m.kind, location: m.location, status: m.status, taskId: m.taskId ?? undefined, oee: this.oeeOf(m) };
|
|
132
|
+
const t = m.taskId ? this.tasks.get(m.taskId) : undefined;
|
|
133
|
+
if (t && t.status === 'in-progress' && t.intent !== 'process')
|
|
134
|
+
s.motion = { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs - (t.durationMs - t.remainingMs), durationMs: t.durationMs, progress: this.progressOf(t), elapsedMs: t.durationMs - t.remainingMs };
|
|
135
|
+
return s;
|
|
136
|
+
}),
|
|
137
|
+
tasks: [...this.tasks.values()].map(t => ({ id: t.id, kind: t.kind, status: t.status, itemRefs: [t.itemEpc], fromNode: t.fromNode, toNode: t.toNode, resourceRef: t.resource ?? undefined, progress: t.status === 'in-progress' ? this.progressOf(t) : undefined })),
|
|
138
|
+
orders: [...this.orders.values()].map(o => ({ id: o.id, kind: o.kind, status: o.status, progress: o.requested ? o.fulfilled / o.requested : 0, held: o.held }))
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* fork — 현재 상태를 정확히 복제한 새 엔진 (디지털트윈 본연: "현재로부터 예측").
|
|
143
|
+
* 원본(live/sim)은 계속 진행, fork 는 what-if 를 앞으로 굴려 forecast·발산(predicted vs actual) 검사에 쓴다.
|
|
144
|
+
* fork 는 자기 구독자·시나리오를 갖고 원본과 격리(handlers·gens 비움, generating=false).
|
|
145
|
+
* rng 는 fork 의 시나리오 load 시 재시드(드레인 예측은 생성 없어 rng 무관·결정적).
|
|
146
|
+
*/
|
|
147
|
+
fork(tenantId = this.tenantId) {
|
|
148
|
+
const Ctor = this.constructor;
|
|
149
|
+
const clone = new Ctor(tenantId, this.policy);
|
|
150
|
+
// 상태·시나리오(gens/generating)·시퀀스 전부 복제 → 원본의 완전한 continuation.
|
|
151
|
+
// 구독자(handlers)만 격리(fork 는 자기 구독자), scenario 객체는 clone 에 바인딩된 것 유지.
|
|
152
|
+
const skip = new Set(['policy', 'scenario', 'tenantId', 'handlers', 'durationEstimator']);
|
|
153
|
+
for (const key of Object.keys(this)) {
|
|
154
|
+
if (skip.has(key))
|
|
155
|
+
continue;
|
|
156
|
+
const v = this[key];
|
|
157
|
+
if (typeof v === 'function')
|
|
158
|
+
continue // rng — 아래서 state 만 복제
|
|
159
|
+
;
|
|
160
|
+
clone[key] =
|
|
161
|
+
v instanceof Map ? new Map([...v.entries()].map(([k, o]) => [k, structuredClone(o)])) : structuredClone(v);
|
|
162
|
+
}
|
|
163
|
+
clone.rng.state = this.rng.state; // RNG 연속성 → 생성 포함 결정적 분기
|
|
164
|
+
clone.durationEstimator = this.durationEstimator; // 무상태 시임 — 참조 공유(policy 와 동형)
|
|
165
|
+
return clone;
|
|
166
|
+
}
|
|
167
|
+
// ── 보호 헬퍼 (도메인 hook 에서 사용) ──────────────────────────────────────
|
|
168
|
+
now() { return new Date(BASE_EPOCH + this.clockMs).toISOString(); }
|
|
169
|
+
randInt(min, max) { return max <= min ? min : min + Math.floor(this.rng() * (max - min + 1)); }
|
|
170
|
+
/** task 소요 산출 — estimator 주입 시 그 값, 미주입/undefined 시 도메인 상수(fallback). "얼마"만 소비, "경로"는 씬. */
|
|
171
|
+
durationOf(ctx, fallbackMs) {
|
|
172
|
+
return this.durationEstimator?.estimate(ctx) ?? fallbackMs;
|
|
173
|
+
}
|
|
174
|
+
/** skuMix 에서 weight 로 gtin 선택(rng) — 도착/오더 자극의 품목 결정. */
|
|
175
|
+
pickGtin(mix) {
|
|
176
|
+
const total = mix.reduce((s, m) => s + m.weight, 0);
|
|
177
|
+
let r = this.rng() * total;
|
|
178
|
+
for (const m of mix) {
|
|
179
|
+
r -= m.weight;
|
|
180
|
+
if (r <= 0)
|
|
181
|
+
return m.gtin;
|
|
182
|
+
}
|
|
183
|
+
return mix[mix.length - 1].gtin;
|
|
184
|
+
}
|
|
185
|
+
nodeByType(type) {
|
|
186
|
+
for (const n of this.nodes.values())
|
|
187
|
+
if (n.type === type)
|
|
188
|
+
return n;
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* process 변화 대수(EPCIS TransformationEvent · ISA-95 Material Consumed/Produced · 씬 Processable.transform).
|
|
193
|
+
* inputs 소비 → outputs 생산. 입출력 arity 가 곧 대수:
|
|
194
|
+
* merge N→1(조립) · split 1→N(분해) · transform 1→1(타입변경) · loss N→0(소실) · gain 0→N(부산물·생성).
|
|
195
|
+
* 아이템 상태(소비/생산)와 노드 점유를 갱신하고 계보(TransformationEvent)를 방출한다.
|
|
196
|
+
* 도메인은 이 원시만 호출 — onTaskComplete 의 소비/생산/EPCIS 손코딩을 대체.
|
|
197
|
+
*/
|
|
198
|
+
transform(inputs, outputs, opts) {
|
|
199
|
+
for (const epc of inputs) {
|
|
200
|
+
const it = this.items.get(epc);
|
|
201
|
+
if (!it)
|
|
202
|
+
continue;
|
|
203
|
+
const n = this.nodes.get(it.location);
|
|
204
|
+
if (n)
|
|
205
|
+
n.occupancy--;
|
|
206
|
+
this.items.delete(epc);
|
|
207
|
+
}
|
|
208
|
+
for (const o of outputs) {
|
|
209
|
+
this.items.set(o.epc, { epc: o.epc, gtin: o.gtin, qty: o.qty, location: o.location, disposition: o.disposition });
|
|
210
|
+
const n = this.nodes.get(o.location);
|
|
211
|
+
if (n)
|
|
212
|
+
n.occupancy++;
|
|
213
|
+
}
|
|
214
|
+
this.emit(transformationEvent({
|
|
215
|
+
eventTime: this.now(), bizStep: opts.bizStep, disposition: opts.disposition,
|
|
216
|
+
inputEPCList: inputs.length ? inputs.slice() : undefined,
|
|
217
|
+
outputEPCList: outputs.length ? outputs.map(o => o.epc) : undefined,
|
|
218
|
+
transformationID: opts.transformationId, readPoint: opts.readPoint, bizLocation: opts.bizLocation ?? opts.readPoint,
|
|
219
|
+
bizTransactionList: opts.bizTransactionList
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* containment 조립(EPCIS AggregationEvent ADD) — 자식들을 부모(용기)로 집約.
|
|
224
|
+
* consume 지정 시 자식이 컨테이너로 흡수되며 독립 아이템에서 이탈(dematerialize: ObjectEvent DELETE + 제거).
|
|
225
|
+
* 미지정 시 이벤트만(자식은 독립 아이템 유지 — 예: WMS 패킹, 적재된 채 도착).
|
|
226
|
+
*/
|
|
227
|
+
aggregate(parent, children, opts) {
|
|
228
|
+
this.emit(aggregationEvent({ eventTime: this.now(), action: 'ADD', bizStep: opts.bizStep, disposition: opts.disposition, parentID: parent, childEPCs: children.slice(), readPoint: opts.readPoint, bizLocation: opts.bizLocation }));
|
|
229
|
+
if (opts.consume) {
|
|
230
|
+
this.emit(objectEvent({ eventTime: this.now(), action: 'DELETE', bizStep: opts.bizStep, disposition: opts.consume.disposition, epcList: children.slice(), readPoint: opts.consume.readPoint }));
|
|
231
|
+
for (const c of children) {
|
|
232
|
+
const it = this.items.get(c);
|
|
233
|
+
if (it) {
|
|
234
|
+
const n = this.nodes.get(it.location);
|
|
235
|
+
if (n)
|
|
236
|
+
n.occupancy--;
|
|
237
|
+
this.items.delete(c);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* containment 분해(EPCIS AggregationEvent DELETE) — 부모(용기)에서 자식들을 풀어냄.
|
|
244
|
+
* materialize 지정 시 자식이 독립 아이템으로 등장(materialize: 노드 배치 + occupancy + ObjectEvent ADD).
|
|
245
|
+
* 미지정 시 이벤트만.
|
|
246
|
+
*/
|
|
247
|
+
disaggregate(parent, children, opts) {
|
|
248
|
+
this.emit(aggregationEvent({ eventTime: this.now(), action: 'DELETE', bizStep: opts.bizStep, parentID: parent, childEPCs: children.slice(), readPoint: opts.readPoint }));
|
|
249
|
+
if (opts.materialize) {
|
|
250
|
+
const m = opts.materialize;
|
|
251
|
+
for (const c of children) {
|
|
252
|
+
this.items.set(c, { epc: c, location: m.location, disposition: m.disposition });
|
|
253
|
+
const n = this.nodes.get(m.location);
|
|
254
|
+
if (n)
|
|
255
|
+
n.occupancy++;
|
|
256
|
+
this.emit(objectEvent({ eventTime: this.now(), action: 'ADD', bizStep: opts.bizStep, disposition: m.disposition, epcList: [c], readPoint: m.location, bizLocation: m.location }));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** 품질 보고(OEE Quality) — 도메인이 완료 시 자원별 양품/불량 1건 계상(예: 용접 수율). */
|
|
261
|
+
recordOutput(moverId, good) {
|
|
262
|
+
const m = moverId ? this.movers.get(moverId) : undefined;
|
|
263
|
+
if (!m)
|
|
264
|
+
return;
|
|
265
|
+
if (good)
|
|
266
|
+
m.goodCount++;
|
|
267
|
+
else
|
|
268
|
+
m.scrapCount++;
|
|
269
|
+
}
|
|
270
|
+
/** 무버 OEE(스냅샷 파생) — Availability×Performance×Quality. planned = 설비 존재 sim 시간(clockMs). */
|
|
271
|
+
oeeOf(m) {
|
|
272
|
+
const planned = this.clockMs;
|
|
273
|
+
const uptime = Math.max(0, planned - m.setupMs - m.downMs); // 가용시간(셋업·고장 제외)
|
|
274
|
+
const availability = planned > 0 ? uptime / planned : 1;
|
|
275
|
+
const performance = uptime > 0 ? Math.min(1, m.runMs / uptime) : (m.runMs > 0 ? 1 : 0);
|
|
276
|
+
const totalQ = m.goodCount + m.scrapCount;
|
|
277
|
+
const quality = totalQ > 0 ? m.goodCount / totalQ : 1;
|
|
278
|
+
return {
|
|
279
|
+
availability, performance, quality, overall: availability * performance * quality,
|
|
280
|
+
runMs: m.runMs, setupMs: m.setupMs, downMs: m.downMs, idleMs: Math.max(0, uptime - m.runMs), goodCount: m.goodCount, scrapCount: m.scrapCount
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/** 정책에 넘길 특정 타입 노드의 관측 뷰 — 예약(그 노드로 향하는 in-flight task) 포함. */
|
|
284
|
+
slotViews(nodeType) {
|
|
285
|
+
const reserved = new Map();
|
|
286
|
+
for (const t of this.tasks.values())
|
|
287
|
+
if (t.status !== 'completed')
|
|
288
|
+
reserved.set(t.toNode, (reserved.get(t.toNode) ?? 0) + 1);
|
|
289
|
+
const views = [];
|
|
290
|
+
for (const n of this.nodes.values())
|
|
291
|
+
if (n.type === nodeType)
|
|
292
|
+
views.push({ id: n.id, capacity: n.capacity, occupancy: n.occupancy, reserved: reserved.get(n.id) ?? 0 });
|
|
293
|
+
return views;
|
|
294
|
+
}
|
|
295
|
+
emit(event) {
|
|
296
|
+
this.revision++;
|
|
297
|
+
const e = { eventId: `${this.tenantId}-evt-${++this.eventSeq}`, eventType: `epcis.${event.type}`, eventTime: event.eventTime, tenantId: this.tenantId, data: event };
|
|
298
|
+
for (const h of this.handlers)
|
|
299
|
+
h(e);
|
|
300
|
+
}
|
|
301
|
+
emitOp(eventType, data) {
|
|
302
|
+
this.revision++;
|
|
303
|
+
const e = { eventId: `${this.tenantId}-evt-${++this.eventSeq}`, eventType, eventTime: this.now(), tenantId: this.tenantId, data };
|
|
304
|
+
for (const h of this.handlers)
|
|
305
|
+
h(e);
|
|
306
|
+
}
|
|
307
|
+
emitTask(t) { this.emitOp(OP_EVENT.task, { taskId: t.id, kind: t.kind, status: t.status, fromNode: t.fromNode, toNode: t.toNode, itemRefs: [t.itemEpc], resourceRef: t.resource ?? undefined }); }
|
|
308
|
+
emitMover(m, motion) { this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, motion }); }
|
|
309
|
+
emitOrder(o) { this.emitOp(OP_EVENT.order, { orderId: o.id, kind: o.kind, status: o.status, requested: o.requested, fulfilled: o.fulfilled, held: o.held }); }
|
|
310
|
+
// ── 내부 mechanics ─────────────────────────────────────────────────────────
|
|
311
|
+
intervalMs(spec) {
|
|
312
|
+
const base = 3_600_000 / spec.rate.meanPerHour;
|
|
313
|
+
return spec.rate.distribution === 'poisson' ? -Math.log(1 - this.rng()) * base : base;
|
|
314
|
+
}
|
|
315
|
+
/** 지수분포 표본(고장/수리 간격) — mean 을 평균으로 하는 무기억 프로세스. */
|
|
316
|
+
sampleExp(meanMs) { return -Math.log(1 - this.rng()) * meanMs; }
|
|
317
|
+
/**
|
|
318
|
+
* 확률적 설비 고장(MTBF/MTTR) — mtbf 지정 무버만 참여(미지정=고장 없음, rng 무소비 → byte-identical).
|
|
319
|
+
* up: nextFailure 도래 시 down(수리까지 repairUntil). down: downMs 누적, repair 도래 시 up(다음 고장 예약).
|
|
320
|
+
* down 중 무버는 배정 불가 + 진행중 task 동결(processTasks 가 skip) → OEE Availability 손실.
|
|
321
|
+
*/
|
|
322
|
+
processFailures(dt) {
|
|
323
|
+
for (const m of this.movers.values()) {
|
|
324
|
+
if (m.mtbfMs === undefined)
|
|
325
|
+
continue;
|
|
326
|
+
if (m.status === 'down') {
|
|
327
|
+
m.downMs += dt;
|
|
328
|
+
if (this.clockMs >= (m.repairUntilMs ?? 0)) {
|
|
329
|
+
m.status = m.taskId ? 'busy' : 'idle'; // 수리 완료 → 진행중 task 있으면 재개
|
|
330
|
+
m.repairUntilMs = undefined;
|
|
331
|
+
m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
|
|
332
|
+
this.emitMover(m);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else if (this.clockMs >= (m.nextFailureMs ?? Infinity)) {
|
|
336
|
+
m.status = 'down'; // 고장 → 수리 대기
|
|
337
|
+
m.repairUntilMs = this.clockMs + this.sampleExp(m.mttrMs ?? m.mtbfMs);
|
|
338
|
+
m.nextFailureMs = undefined;
|
|
339
|
+
this.emitMover(m);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
progressOf(t) { return t.durationMs <= 0 ? 1 : Math.min(1, Math.max(0, (t.durationMs - t.remainingMs) / t.durationMs)); }
|
|
344
|
+
generate() {
|
|
345
|
+
for (const g of this.gens) {
|
|
346
|
+
while (this.clockMs >= g.nextMs) {
|
|
347
|
+
if (g.spec.kind === 'inbound-arrival')
|
|
348
|
+
this.onArrival(g.spec);
|
|
349
|
+
else if (g.spec.kind === 'outbound-order')
|
|
350
|
+
this.onOrder(g.spec);
|
|
351
|
+
g.nextMs += this.intervalMs(g.spec);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
processOrders() {
|
|
356
|
+
for (const o of this.orders.values())
|
|
357
|
+
if (o.status === 'created' && !o.held)
|
|
358
|
+
this.allocate(o);
|
|
359
|
+
}
|
|
360
|
+
processTasks(dt) {
|
|
361
|
+
// 1) created → in-progress. dwell(무자원)은 즉시 진행, transport/process 는 가용 자원 배정.
|
|
362
|
+
for (const t of this.tasks.values()) {
|
|
363
|
+
if (t.status !== 'created')
|
|
364
|
+
continue;
|
|
365
|
+
if (t.intent === 'dwell') { // 무자원 체류 — 자원 배정 없이 즉시 진행
|
|
366
|
+
t.status = 'in-progress';
|
|
367
|
+
t.remainingMs = t.durationMs;
|
|
368
|
+
this.emitTask(t);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
// resourceType 있으면 그 kind 무버만; 없으면 아무 유휴 무버.
|
|
372
|
+
const mover = [...this.movers.values()].find(m => m.status === 'idle' && (t.resourceType === undefined || m.kind === t.resourceType));
|
|
373
|
+
if (!mover)
|
|
374
|
+
continue; // 맞는 유휴 자원 없음 → 다음 task(break 아님: 다른 타입은 가용할 수 있음)
|
|
375
|
+
// 체인지오버: task 의 changeoverKey 가 무버 직전 키와 다르면 셋업 부착(첫 작업은 셋업 없음).
|
|
376
|
+
if (t.setupMs && t.changeoverKey !== undefined && mover.lastChangeoverKey !== undefined && mover.lastChangeoverKey !== t.changeoverKey) {
|
|
377
|
+
t.appliedSetupMs = t.setupMs;
|
|
378
|
+
t.durationMs += t.setupMs; // 셋업을 가동 앞에 folded(무버 점유 = 셋업+사이클)
|
|
379
|
+
}
|
|
380
|
+
if (t.changeoverKey !== undefined)
|
|
381
|
+
mover.lastChangeoverKey = t.changeoverKey;
|
|
382
|
+
mover.status = 'busy';
|
|
383
|
+
mover.taskId = t.id;
|
|
384
|
+
t.status = 'in-progress';
|
|
385
|
+
t.resource = mover.id;
|
|
386
|
+
t.remainingMs = t.durationMs;
|
|
387
|
+
this.emitTask(t);
|
|
388
|
+
// process: 제자리 변환(모션 없음). transport: 이동 모션 방출.
|
|
389
|
+
if (t.intent === 'process')
|
|
390
|
+
this.emitMover(mover);
|
|
391
|
+
else
|
|
392
|
+
this.emitMover(mover, { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs, durationMs: t.durationMs, progress: 0, elapsedMs: 0 });
|
|
393
|
+
}
|
|
394
|
+
// 2) in-progress 진행/완료 — 상태 효과는 전부 도메인(onTaskComplete). base 는 자원 해제·델타만.
|
|
395
|
+
for (const t of this.tasks.values()) {
|
|
396
|
+
if (t.status !== 'in-progress')
|
|
397
|
+
continue;
|
|
398
|
+
if (t.resource && this.movers.get(t.resource)?.status === 'down')
|
|
399
|
+
continue; // 설비 고장 → 작업 동결
|
|
400
|
+
t.remainingMs -= dt;
|
|
401
|
+
if (t.remainingMs > 0)
|
|
402
|
+
continue;
|
|
403
|
+
this.onTaskComplete(t);
|
|
404
|
+
t.status = 'completed';
|
|
405
|
+
if (!t.resource) {
|
|
406
|
+
this.emitTask(t);
|
|
407
|
+
continue;
|
|
408
|
+
} // dwell(무자원) — 해제할 자원 없음
|
|
409
|
+
const mover = this.movers.get(t.resource);
|
|
410
|
+
// OEE 계측: 셋업/가동 누적(가동 = 총 duration − 셋업). onTaskComplete 가 recordOutput 로 품질 보고.
|
|
411
|
+
const setup = t.appliedSetupMs ?? 0;
|
|
412
|
+
mover.setupMs += setup;
|
|
413
|
+
mover.runMs += t.durationMs - setup;
|
|
414
|
+
mover.status = 'idle';
|
|
415
|
+
mover.taskId = null;
|
|
416
|
+
if (t.intent !== 'process')
|
|
417
|
+
mover.location = t.toNode; // 운반만 위치 이동; process 는 제자리
|
|
418
|
+
this.emitTask(t);
|
|
419
|
+
this.emitMover(mover);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ScenarioControl, ScenarioDef, StateSnapshot } from './contract.ts';
|
|
2
|
+
/** fork·시나리오 구동 가능한 트윈(FlowEngine 이 만족). */
|
|
3
|
+
export interface ForecastTwin {
|
|
4
|
+
getSnapshot(): StateSnapshot;
|
|
5
|
+
tick(dtMs: number): void;
|
|
6
|
+
scenario: ScenarioControl;
|
|
7
|
+
fork(): ForecastTwin;
|
|
8
|
+
}
|
|
9
|
+
export interface MonteCarloResult {
|
|
10
|
+
runs: number;
|
|
11
|
+
samples: number[];
|
|
12
|
+
min: number;
|
|
13
|
+
max: number;
|
|
14
|
+
mean: number;
|
|
15
|
+
p50: number;
|
|
16
|
+
p90: number;
|
|
17
|
+
}
|
|
18
|
+
export interface MonteCarloOptions {
|
|
19
|
+
runs: number;
|
|
20
|
+
horizonMs: number;
|
|
21
|
+
scenario: ScenarioDef;
|
|
22
|
+
tickMs?: number;
|
|
23
|
+
metric: (s: StateSnapshot) => number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 현재 상태에서 N개 확률적 미래를 표본화 → 지표 분포.
|
|
27
|
+
* run 마다 fork(현재 보존) + scenario.load(seed+i)(미래 변주) + horizon 까지 구동. 원본 무간섭.
|
|
28
|
+
*/
|
|
29
|
+
export declare function monteCarloForecast(twin: ForecastTwin, opts: MonteCarloOptions): MonteCarloResult;
|