@operato/twin-kernel 0.0.6 → 0.2.0
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/dist/contract.d.ts +209 -1
- package/dist/contract.js +4 -0
- package/dist/counterfactual.d.ts +2 -0
- package/dist/counterfactual.js +7 -3
- package/dist/domain-definition.d.ts +84 -1
- package/dist/domain-definition.js +11 -0
- package/dist/duration-estimator.d.ts +26 -2
- package/dist/epcis.d.ts +185 -6
- package/dist/epcis.js +175 -12
- package/dist/flow-engine.d.ts +171 -3
- package/dist/flow-engine.js +522 -21
- package/dist/forecast.d.ts +8 -0
- package/dist/forecast.js +9 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/iso-duration.d.ts +5 -0
- package/dist/iso-duration.js +43 -0
- package/dist/kernel.js +8 -2
- package/dist/mes-kernel.d.ts +2 -2
- package/dist/mes-kernel.js +18 -9
- package/dist/state-projector.d.ts +65 -2
- package/dist/state-projector.js +241 -17
- package/dist/task-fold.d.ts +56 -0
- package/dist/task-fold.js +60 -0
- package/dist/twin-observer.d.ts +2 -0
- package/dist/twin-observer.js +9 -3
- package/dist-cjs/index.cjs +993 -159
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -11,19 +11,71 @@ export type TaskStatus = 'created' | 'assigned' | 'in-progress' | 'completed';
|
|
|
11
11
|
export interface NodeState {
|
|
12
12
|
id: string;
|
|
13
13
|
type: string;
|
|
14
|
+
/**
|
|
15
|
+
* **저장 용량** — 이 자리에 동시에 놓일 수 있는 물품 수. 뜻을 바꾸지 않는다(배정 정책·포화 판정이
|
|
16
|
+
* 이 뜻으로 굳어 있다). 처리 능력은 아래 `parallelism` 이 따로 말한다.
|
|
17
|
+
*/
|
|
14
18
|
capacity?: number;
|
|
19
|
+
/**
|
|
20
|
+
* **동시 처리 수** — 이 자리에서 한 번에 진행될 수 있는 작업 수(대기 이론의 서버 수).
|
|
21
|
+
*
|
|
22
|
+
* 저장 용량과 다른 축이다: 절단 스테이션은 자재를 20개 쌓아 둘 수 있어도(`capacity`) 한 번에
|
|
23
|
+
* 한 대만 깎는다(`parallelism: 1`). 예전에는 한 필드가 둘을 겸해서 그 현장을 표현할 방법이 없었고,
|
|
24
|
+
* 자원만 놀고 있으면 같은 자리에서 작업이 무제한 동시에 진행됐다 — 대기가 생기지 않아 병목이
|
|
25
|
+
* 사라지고 예측이 낙관 쪽으로 치우쳤다.
|
|
26
|
+
*
|
|
27
|
+
* 미지정 = 제약 없음(기존 거동). 0 은 "처리하지 않는 자리"가 아니라 **선언 오류**로 보고 무시한다
|
|
28
|
+
* — 0 으로 막을 일이라면 그 자리에 작업을 보내지 않는 것이 맞다.
|
|
29
|
+
*/
|
|
30
|
+
parallelism?: number;
|
|
15
31
|
occupancy: number;
|
|
16
32
|
status?: string;
|
|
17
33
|
parentId?: string;
|
|
34
|
+
/**
|
|
35
|
+
* 이 자리를 **어떻게 알게 됐는가** — `master`(원 시스템 마스터/저작이 말해 준 자리) ·
|
|
36
|
+
* `observed`(이벤트에 등장해서 알게 된 자리). 소비처가 둘을 구별해야 한다: 관측으로 알게 된 자리는
|
|
37
|
+
* 보드에 좌표가 없고 용량이 비어 있어 **계획에 참여하지 못한다**(그 사실을 감추지 않기 위한 표시).
|
|
38
|
+
*/
|
|
39
|
+
origin?: 'master' | 'observed';
|
|
18
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* 물품 상태 — 표준이 담는 것을 담는다(EPCIS 2.0 / TDS).
|
|
43
|
+
*
|
|
44
|
+
* 세 층위가 한 모델에 들어온다: 개체(SGTIN, 일련번호까지) · **로트 클래스(LGTIN, 품번+로트)** ·
|
|
45
|
+
* 품목 클래스(GTIN). 낱개 일련번호가 없고 로트로만 관리하는 자재(원자재·화학·식품)가 현장의 다수이므로
|
|
46
|
+
* 로트와 수량·단위가 일급이어야 한다 — 없으면 회수·유통기한·품질 격리를 표현할 수 없다.
|
|
47
|
+
*/
|
|
19
48
|
export interface ItemState {
|
|
49
|
+
/** 식별자 — 개체(urn:epc:id:…) 또는 클래스(urn:epc:class:lgtin:… / urn:epc:idpat:…). */
|
|
20
50
|
epc: string;
|
|
51
|
+
/**
|
|
52
|
+
* 품목 클래스 식별자 — **URI 원문 그대로**(`urn:epc:idpat:sgtin:…` 또는 `urn:epc:class:lgtin:…`).
|
|
53
|
+
* 오더의 skuMix·할당이 이 값으로 매칭하므로 뜻을 바꾸지 않는다.
|
|
54
|
+
*/
|
|
21
55
|
gtin?: string;
|
|
56
|
+
/** 품번 키(CompanyPrefix.ItemRef) — 식별자에서 파생. 로트가 달라도 같은 품번으로 묶는 축. */
|
|
57
|
+
gtinKey?: string;
|
|
58
|
+
/** 로트·배치 번호 — LGTIN 이면 식별자에서 파생, 직렬 개체면 ilmd 에서 온다(ilmd 는 미지원). */
|
|
59
|
+
lot?: string;
|
|
22
60
|
location: string;
|
|
23
61
|
disposition?: string;
|
|
62
|
+
/** 소속 물류단위(팔레트 SSCC 등) — AggregationEvent 로 맺어진다. 3D 적재 표현의 재료. */
|
|
24
63
|
parent?: string;
|
|
64
|
+
/**
|
|
65
|
+
* 이 물류단위를 싣고 있는 **반복사용 자산**(GRAI 팔레트 등). `parent`(물류단위 소속)와 다른 축이다:
|
|
66
|
+
* `parent` 는 "무엇에 담겼나"(SSCC), 이것은 "무엇에 실렸나"(GRAI). 자산을 쓰지 않는 현장은 비어 있다.
|
|
67
|
+
*/
|
|
68
|
+
carriedBy?: string;
|
|
69
|
+
/** 수량 — 비직렬(클래스) 물품의 개수·중량. 개체 물품은 1. */
|
|
25
70
|
qty?: number;
|
|
71
|
+
/** 수량 단위(UN/ECE Rec 20 코드: EA·KGM 등). 없으면 개수로 읽는다. */
|
|
72
|
+
uom?: string;
|
|
26
73
|
expiry?: number;
|
|
74
|
+
/**
|
|
75
|
+
* 개체·로트 마스터데이터 원문 — 표준이 속성 이름을 정의하지 않으므로(상위 문서 소관) **받은 것을
|
|
76
|
+
* 그대로 들고 있는다.** 도메인이 자기 어휘로 읽을 수 있고, 우리가 모르는 속성도 잃지 않는다.
|
|
77
|
+
*/
|
|
78
|
+
ilmd?: Record<string, unknown>;
|
|
27
79
|
}
|
|
28
80
|
/**
|
|
29
81
|
* 운영·키네마틱 모션 — State 이원 모델의 "연속" 절반.
|
|
@@ -66,6 +118,59 @@ export interface MoverState {
|
|
|
66
118
|
motion?: MoverMotion;
|
|
67
119
|
oee?: OeeMetrics;
|
|
68
120
|
held?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* 교대 밖이라 지금 일하지 않는다 — 고장(down)·계획정지(held)와 **다른 이유**다.
|
|
123
|
+
* 셋을 뭉개면 "왜 안 움직이나" 에 답할 수 없다(고쳐야 하나·풀어야 하나·기다려야 하나).
|
|
124
|
+
*/
|
|
125
|
+
offShift?: boolean;
|
|
126
|
+
/** 이 자원을 어떻게 알게 됐는가 — NodeState.origin 과 같은 뜻(성장 정책을 한 규칙으로 선언). */
|
|
127
|
+
origin?: 'master' | 'observed';
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 사람 — **ISA-95 `Person`.** 설비와 다른 자원 종류다.
|
|
131
|
+
*
|
|
132
|
+
* 왜 무버(설비)로 뭉개지 않는가: 사람은 고장 나지 않고(MTBF), 설비종합효율로 평가하지 않으며,
|
|
133
|
+
* **등급(자격)과 교대로 산다.** 같은 그릇에 담으면 설비의 어휘(고장·수리·OEE)가 사람에게 붙고
|
|
134
|
+
* 사람의 어휘(등급·교대·투입 인원)가 설비에 붙는다 — 둘 다 거짓이 된다.
|
|
135
|
+
*
|
|
136
|
+
* 그리고 **인원은 현장에서 가장 자주 부족한 자원**이다. 모델에 없으면 "사람을 두 명 더 넣으면
|
|
137
|
+
* 어떻게 되나" 를 물을 수 없고, 사람이 만들어 내는 줄이 예측에서 통째로 사라진다.
|
|
138
|
+
*/
|
|
139
|
+
export interface PersonState {
|
|
140
|
+
id: string;
|
|
141
|
+
/** 소속 등급 — ISA-95 `PersonnelClassID`. 배정은 개인이 아니라 **등급으로 요구**된다. */
|
|
142
|
+
personnelClass?: string;
|
|
143
|
+
/** 'idle' | 'busy'. 고장(down)이 없다 — 사람은 그렇게 모델링하지 않는다. */
|
|
144
|
+
status: string;
|
|
145
|
+
taskId?: string;
|
|
146
|
+
/** 교대 밖 — 자원(MoverState.offShift)과 같은 뜻. */
|
|
147
|
+
offShift?: boolean;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 물리 자산 — **ISA-95 `PhysicalAsset`, GS1 `GRAI`(반복사용 자산).**
|
|
151
|
+
*
|
|
152
|
+
* 왜 물품(Material)과 따로 두는가: **SSCC 와 GRAI 는 다른 것**이다. SSCC 는 *물류단위*(그 팔레트에
|
|
153
|
+
* 실린 화물 한 덩어리)이고 GRAI 는 *돌아오는 팔레트 자체*다. 같은 GRAI 팔레트가 오늘은 SSCC 999 를,
|
|
154
|
+
* 내일은 다른 SSCC 를 싣는다. 둘을 뭉개면 **팔레트 회수·풀링을 표현할 수 없고**(팔레트가 화물과 함께
|
|
155
|
+
* 사라진다), EPCIS 조립 이벤트의 `parentID`(=물류단위)도 뜻이 흐려진다.
|
|
156
|
+
*
|
|
157
|
+
* 그리고 빈 팔레트 부족은 현장의 실제 제약이다 — 자산이 없어 작업이 못 나가는 일이 사람 부족만큼 잦다.
|
|
158
|
+
*/
|
|
159
|
+
export interface AssetState {
|
|
160
|
+
id: string;
|
|
161
|
+
/** 자산 등급 — pallet · rack · bin · trailer 등. 도메인 소유(코어는 강제하지 않는다). */
|
|
162
|
+
assetClass?: string;
|
|
163
|
+
/** 지금 있는 자리. */
|
|
164
|
+
location?: string;
|
|
165
|
+
/** 'idle' | 'in-use'. 고장·OEE 로 평가하지 않는다(설비가 아니다). */
|
|
166
|
+
status: string;
|
|
167
|
+
/** 지금 이 자산이 잡혀 있는 작업. */
|
|
168
|
+
taskId?: string;
|
|
169
|
+
/**
|
|
170
|
+
* 지금 싣고 있는 물류단위(SSCC 등) — **자산과 화물의 연결.**
|
|
171
|
+
* 비어 있으면 빈 팔레트다(회수 대상이자 다음 출고의 재료).
|
|
172
|
+
*/
|
|
173
|
+
carrying?: string;
|
|
69
174
|
}
|
|
70
175
|
export interface TaskState {
|
|
71
176
|
id: string;
|
|
@@ -77,6 +182,16 @@ export interface TaskState {
|
|
|
77
182
|
resourceRef?: string;
|
|
78
183
|
orderId?: string;
|
|
79
184
|
progress?: number;
|
|
185
|
+
/** 남은 시간·총 소요(ms) — 진행 중인 작업을 이어서 굴리는 데 필요(씨앗의 충실도). */
|
|
186
|
+
remainingMs?: number;
|
|
187
|
+
durationMs?: number;
|
|
188
|
+
/** 작업 의도 — 무자원이 설계인지(체류) 기록 누락인지 구별하는 근거. */
|
|
189
|
+
intent?: 'transport' | 'process' | 'dwell';
|
|
190
|
+
/**
|
|
191
|
+
* 이 작업에 투입된 사람들 — 설비(`resourceRef`)와 **별개 축**이다.
|
|
192
|
+
* 한 작업이 설비 하나와 사람 여럿을 동시에 잡을 수 있다(용접 로봇 1대 + 작업자 2명).
|
|
193
|
+
*/
|
|
194
|
+
personnel?: string[];
|
|
80
195
|
}
|
|
81
196
|
export interface OrderState {
|
|
82
197
|
id: string;
|
|
@@ -127,6 +242,10 @@ export interface StateSnapshot {
|
|
|
127
242
|
nodes: NodeState[];
|
|
128
243
|
items: ItemState[];
|
|
129
244
|
movers: MoverState[];
|
|
245
|
+
/** 사람 — 등급·교대·투입 상태. 인원을 선언하지 않은 트윈에서는 빈 배열. */
|
|
246
|
+
persons: PersonState[];
|
|
247
|
+
/** 물리 자산(반복사용) — 선언하지 않은 트윈에서는 빈 배열. */
|
|
248
|
+
assets: AssetState[];
|
|
130
249
|
tasks: TaskState[];
|
|
131
250
|
orders: OrderState[];
|
|
132
251
|
attentions?: Attention[];
|
|
@@ -159,9 +278,23 @@ export interface CommandAck {
|
|
|
159
278
|
export interface TwinCommandChannel {
|
|
160
279
|
dispatch(command: Pick<Command, 'commandId' | 'type' | 'args'>): Promise<CommandAck>;
|
|
161
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* 자극 발생률 — **네 분포 모두 실제로 판정된다**(예전에는 poisson 만 구현되고 나머지는 조용히 상수였다).
|
|
283
|
+
*
|
|
284
|
+
* `constant` 간격 일정
|
|
285
|
+
* `poisson` 무기억 도착(지수 간격) — 실제 도착 과정에 가장 가깝다
|
|
286
|
+
* `uniform` 0..2×평균 균등 — 평균을 유지하면서 흔들린다
|
|
287
|
+
* `profile` 시간대별 배율(`profile[시]`)로 도착률을 조절 — 하루 안의 수요 곡선
|
|
288
|
+
*/
|
|
162
289
|
export interface RateSpec {
|
|
163
290
|
distribution: 'poisson' | 'uniform' | 'constant' | 'profile';
|
|
291
|
+
/** 시간당 평균 발생 수. `profile` 이면 여기에 시간대 배율이 곱해진다. */
|
|
164
292
|
meanPerHour: number;
|
|
293
|
+
/**
|
|
294
|
+
* 시간대 배율 — `profile[시]`(0=자정). `distribution: 'profile'` 일 때만 쓰인다.
|
|
295
|
+
* 배열이 짧으면 **순환**한다(24개=하루, 8개=8시간 주기). 0 이면 그 시간대에는 발생하지 않는다.
|
|
296
|
+
* 시(hour)는 시뮬 시각 자신의 프레임 — 계약에 표준시가 없으므로 현지 시간대 해석은 하지 않는다.
|
|
297
|
+
*/
|
|
165
298
|
profile?: number[];
|
|
166
299
|
}
|
|
167
300
|
export interface ContentSpec {
|
|
@@ -188,6 +321,11 @@ export interface GeneratorSpec {
|
|
|
188
321
|
stimulus?: 'arrival' | 'order';
|
|
189
322
|
rate: RateSpec;
|
|
190
323
|
content: ContentSpec;
|
|
324
|
+
/**
|
|
325
|
+
* 운영시간 — 이 구간 밖에서는 자극이 발생하지 않는다(문 닫은 시간에 트럭이 오지 않는다).
|
|
326
|
+
* `startHour <= endHour` 면 같은 날 구간, 넘어가면 자정을 가로지르는 야간 구간(22→6).
|
|
327
|
+
* 미지정이면 24시간 가동. 시(hour) 해석은 `RateSpec.profile` 과 같다.
|
|
328
|
+
*/
|
|
191
329
|
window?: {
|
|
192
330
|
startHour: number;
|
|
193
331
|
endHour: number;
|
|
@@ -209,9 +347,30 @@ export interface ScenarioControl {
|
|
|
209
347
|
export declare const OP_EVENT: {
|
|
210
348
|
readonly task: "task.status";
|
|
211
349
|
readonly equipment: "equipment.status";
|
|
350
|
+
/** 사람 상태 전이 — 설비와 별개 채널(어휘가 다르다: 고장이 아니라 교대·투입). */
|
|
351
|
+
readonly person: "person.status";
|
|
352
|
+
/** 물리 자산 상태 전이 — 어디 있나·무엇을 싣고 있나(빈 팔레트인가). */
|
|
353
|
+
readonly asset: "asset.status";
|
|
212
354
|
readonly order: "order.status";
|
|
213
355
|
readonly quality: "quality.output";
|
|
214
356
|
};
|
|
357
|
+
/** 사람 상태 델타 — 배정·해제·교대 전이 시 방출. 미러가 인원 가용을 비추는 근거. */
|
|
358
|
+
export interface PersonStatusDelta {
|
|
359
|
+
personId: string;
|
|
360
|
+
personnelClass?: string;
|
|
361
|
+
status: string;
|
|
362
|
+
taskId?: string;
|
|
363
|
+
offShift?: boolean;
|
|
364
|
+
}
|
|
365
|
+
/** 물리 자산 상태 델타 — 이동·투입·적재/하역 시 방출. 미러가 자산 가용을 비추는 근거. */
|
|
366
|
+
export interface AssetStatusDelta {
|
|
367
|
+
assetId: string;
|
|
368
|
+
assetClass?: string;
|
|
369
|
+
status: string;
|
|
370
|
+
location?: string;
|
|
371
|
+
taskId?: string;
|
|
372
|
+
carrying?: string;
|
|
373
|
+
}
|
|
215
374
|
/** 품질 산출 델타 — recordOutput(양품/불량) 시 방출. goodCount/scrapCount 는 무버 누적값. */
|
|
216
375
|
export interface QualityDelta {
|
|
217
376
|
moverId: string;
|
|
@@ -221,6 +380,10 @@ export interface QualityDelta {
|
|
|
221
380
|
}
|
|
222
381
|
export interface TaskStatusDelta {
|
|
223
382
|
taskId: string;
|
|
383
|
+
/** 투입된 사람들 — 미러가 인원 배정을 그대로 비추려면 델타에 실려야 한다. */
|
|
384
|
+
personnel?: string[];
|
|
385
|
+
/** 투입된 물리 자산(팔레트 등). */
|
|
386
|
+
assets?: string[];
|
|
224
387
|
orderId?: string;
|
|
225
388
|
kind: string;
|
|
226
389
|
status: TaskStatus;
|
|
@@ -228,6 +391,23 @@ export interface TaskStatusDelta {
|
|
|
228
391
|
toNode?: string;
|
|
229
392
|
itemRefs?: string[];
|
|
230
393
|
resourceRef?: string;
|
|
394
|
+
/**
|
|
395
|
+
* 작업 의도 — transport(운반) · process(가공) · dwell(체류).
|
|
396
|
+
*
|
|
397
|
+
* 없으면 소비처가 "자원이 없는 것" 과 "자원을 쓰지 않는 공정" 을 **구별할 수 없다**(체류는 무자원이
|
|
398
|
+
* 정상인데 데이터 유실로 읽힌다 — 2026-07-31 에 화면이 그렇게 말할 뻔했다).
|
|
399
|
+
*/
|
|
400
|
+
intent?: 'transport' | 'process' | 'dwell';
|
|
401
|
+
/**
|
|
402
|
+
* 진척(0~1)과 남은 시간(ms) — **진행 중인 작업을 이어서 굴리려면 필요하다.**
|
|
403
|
+
*
|
|
404
|
+
* 없으면 미러 상태를 씨앗으로 한 예측이 "진행 중인 일이 하나도 없는 현장" 에서 출발한다.
|
|
405
|
+
* 완료·생성 시점에는 의미가 없어 in-progress 에만 실린다.
|
|
406
|
+
*/
|
|
407
|
+
progress?: number;
|
|
408
|
+
remainingMs?: number;
|
|
409
|
+
/** 이 작업의 총 소요 예상(ms) — 진척의 분모. */
|
|
410
|
+
durationMs?: number;
|
|
231
411
|
}
|
|
232
412
|
export interface EquipmentStatusDelta {
|
|
233
413
|
moverId: string;
|
|
@@ -267,22 +447,50 @@ export declare const CMD: {
|
|
|
267
447
|
readonly resourceResetMetrics: "resource.reset-metrics";
|
|
268
448
|
readonly resourceAdd: "resource.add";
|
|
269
449
|
};
|
|
270
|
-
export type OperationalDelta = TaskStatusDelta | EquipmentStatusDelta | OrderStatusDelta;
|
|
450
|
+
export type OperationalDelta = TaskStatusDelta | EquipmentStatusDelta | PersonStatusDelta | AssetStatusDelta | OrderStatusDelta;
|
|
271
451
|
export type EventHandler = (e: CanonicalEnvelope) => void;
|
|
272
452
|
export type Unsubscribe = () => void;
|
|
273
453
|
export interface BoardDef {
|
|
454
|
+
/** parallelism = 동시 처리 수(NodeState.parallelism 참조). capacity 는 저장 용량. */
|
|
274
455
|
nodes: {
|
|
275
456
|
id: string;
|
|
276
457
|
type: string;
|
|
277
458
|
capacity: number;
|
|
459
|
+
parallelism?: number;
|
|
278
460
|
parentId?: string;
|
|
279
461
|
}[];
|
|
462
|
+
/**
|
|
463
|
+
* 무버(설비). mtbfMs/mttrMs 지정 시 확률적 고장 모델 참여(OEE Availability 손실). 미지정=고장 없음.
|
|
464
|
+
* `window` 지정 시 그 시간대에만 일한다(교대·가동시간) — 미지정이면 24시간 가용(기존 거동).
|
|
465
|
+
*/
|
|
280
466
|
movers: {
|
|
281
467
|
id: string;
|
|
282
468
|
kind: string;
|
|
283
469
|
homeNode: string;
|
|
284
470
|
mtbfMs?: number;
|
|
285
471
|
mttrMs?: number;
|
|
472
|
+
window?: {
|
|
473
|
+
startHour: number;
|
|
474
|
+
endHour: number;
|
|
475
|
+
};
|
|
476
|
+
}[];
|
|
477
|
+
/**
|
|
478
|
+
* 사람 — ISA-95 `Person`. `personnelClass` 로 등급을 밝히고, `window` 로 교대를 선언한다.
|
|
479
|
+
* 선언하지 않으면 인원 제약이 없는 트윈이다(기존 거동).
|
|
480
|
+
*/
|
|
481
|
+
persons?: {
|
|
482
|
+
id: string;
|
|
483
|
+
personnelClass?: string;
|
|
484
|
+
window?: {
|
|
485
|
+
startHour: number;
|
|
486
|
+
endHour: number;
|
|
487
|
+
};
|
|
488
|
+
}[];
|
|
489
|
+
/** 물리 자산(반복사용) — ISA-95 `PhysicalAsset` / GS1 `GRAI`. 선언하지 않으면 자산 제약이 없다. */
|
|
490
|
+
assets?: {
|
|
491
|
+
id: string;
|
|
492
|
+
assetClass?: string;
|
|
493
|
+
homeNode?: string;
|
|
286
494
|
}[];
|
|
287
495
|
}
|
|
288
496
|
export interface TwinKernel {
|
package/dist/contract.js
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
export const OP_EVENT = {
|
|
10
10
|
task: 'task.status',
|
|
11
11
|
equipment: 'equipment.status',
|
|
12
|
+
/** 사람 상태 전이 — 설비와 별개 채널(어휘가 다르다: 고장이 아니라 교대·투입). */
|
|
13
|
+
person: 'person.status',
|
|
14
|
+
/** 물리 자산 상태 전이 — 어디 있나·무엇을 싣고 있나(빈 팔레트인가). */
|
|
15
|
+
asset: 'asset.status',
|
|
12
16
|
order: 'order.status',
|
|
13
17
|
quality: 'quality.output' // 품질 산출(양품/불량) — OEE quality 입력. live 누적기가 이걸로 good/scrap 정확 추적.
|
|
14
18
|
};
|
package/dist/counterfactual.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface CounterfactualTwin {
|
|
|
7
7
|
fork(): CounterfactualTwin;
|
|
8
8
|
dispatch(cmd: Command): CommandAck;
|
|
9
9
|
scenario: ScenarioControl;
|
|
10
|
+
/** 시뮬 시각(ms) — 구동 루프가 시각만 읽을 때 쓴다(스냅샷 재료화 회피, forecast.ts 주석 참조). */
|
|
11
|
+
clockMs?: number;
|
|
10
12
|
}
|
|
11
13
|
/** 체크포인트 이력 — 주기적으로 live 를 fork 해 저장(전체 상태 보존). 시간여행의 앵커. */
|
|
12
14
|
export declare class TwinHistory {
|
package/dist/counterfactual.js
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* 체크포인트 fork 를 정확히 T 로 재구동(RNG 연속 → 실제와 일치)한 뒤 분기 = 결정적 반사실.
|
|
8
8
|
*/
|
|
9
9
|
import { compareStates } from "./divergence.js";
|
|
10
|
+
/** 구동 루프용 시각 읽기 — 숫자 하나 때문에 전체 상태를 만들지 않는다. */
|
|
11
|
+
function clockOf(twin) {
|
|
12
|
+
return typeof twin.clockMs === 'number' ? twin.clockMs : twin.getSnapshot().simClockMs;
|
|
13
|
+
}
|
|
10
14
|
/** 체크포인트 이력 — 주기적으로 live 를 fork 해 저장(전체 상태 보존). 시간여행의 앵커. */
|
|
11
15
|
export class TwinHistory {
|
|
12
16
|
live;
|
|
@@ -18,7 +22,7 @@ export class TwinHistory {
|
|
|
18
22
|
}
|
|
19
23
|
/** 현재를 체크포인트로 저장(호스트가 주기적으로 호출). */
|
|
20
24
|
checkpoint() {
|
|
21
|
-
this.checkpoints.push({ simMs: this.live
|
|
25
|
+
this.checkpoints.push({ simMs: clockOf(this.live), twin: this.live.fork() });
|
|
22
26
|
}
|
|
23
27
|
get count() {
|
|
24
28
|
return this.checkpoints.length;
|
|
@@ -33,7 +37,7 @@ export class TwinHistory {
|
|
|
33
37
|
return undefined;
|
|
34
38
|
const t = best.twin.fork();
|
|
35
39
|
let guard = 0;
|
|
36
|
-
while (t
|
|
40
|
+
while (clockOf(t) < simMs && guard++ < 1_000_000)
|
|
37
41
|
t.tick(this.tickMs);
|
|
38
42
|
return t;
|
|
39
43
|
}
|
|
@@ -51,7 +55,7 @@ export function counterfactualAt(history, atSimMs, opts) {
|
|
|
51
55
|
const withAlt = base.fork();
|
|
52
56
|
opts.alternative(withAlt);
|
|
53
57
|
const baseline = base.fork();
|
|
54
|
-
const run = (t) => { let g = 0; while (t
|
|
58
|
+
const run = (t) => { let g = 0; while (clockOf(t) < target && g++ < 1_000_000)
|
|
55
59
|
t.tick(step); };
|
|
56
60
|
run(withAlt);
|
|
57
61
|
run(baseline);
|
|
@@ -32,7 +32,61 @@ export interface ResourceTypeDef {
|
|
|
32
32
|
}
|
|
33
33
|
/** 작업 의도 — FlowTask.intent 와 정합(ISA-95 이동 vs 변환). */
|
|
34
34
|
export type OperationIntent = 'transport' | 'process' | 'dwell';
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* ISO 8601 기간 표기(예 `PT12M`·`PT1H15M`) — **ISA-95 와 같은 표기.**
|
|
37
|
+
* 근거: B2MML `OperationsSegmentType.Duration` 의 타입 `DurationType` = `xsd:restriction base="xsd:duration"`.
|
|
38
|
+
* 밀리초 숫자로 계약하지 않는 이유: 명세는 사람과 원 시스템이 주는 값이고, 표준이 이미 표기를 정해 뒀다.
|
|
39
|
+
*/
|
|
40
|
+
export type IsoDuration = string;
|
|
41
|
+
/**
|
|
42
|
+
* 공정 모수 — **ISA-95 `OperationsSegment.ParameterSpecification`(`ParameterType`) 1:1.**
|
|
43
|
+
* 표준의 `ParameterType` = `ID` + `Value`(`ValueString` + `DataType` + `UnitOfMeasure`) 이므로 그 모양을 따른다.
|
|
44
|
+
*
|
|
45
|
+
* 표준은 **파라미터 ID 어휘를 정하지 않는다** — 상위 문서/당사자 합의 소관이다. 그래서 우리가 쓰는 ID 는
|
|
46
|
+
* `OP_PARAM` 한 곳에서만 정의한다(어휘를 코드 곳곳에서 발명하지 않기 위해 — ILMD_ATTR 과 같은 규율).
|
|
47
|
+
*/
|
|
48
|
+
export interface OperationParameter {
|
|
49
|
+
/** ISA-95 `Parameter.ID`. 우리 canonical ID 는 `OP_PARAM` 참조. */
|
|
50
|
+
id: string;
|
|
51
|
+
/** ISA-95 `Value.ValueString` — 표준이 문자열로 싣는다(숫자는 소비처가 해석). */
|
|
52
|
+
value: string;
|
|
53
|
+
/** ISA-95 `Value.UnitOfMeasure`. 무차원(비율 등)이면 생략. */
|
|
54
|
+
uom?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 우리가 소비하는 canonical 파라미터 ID — **표준이 이름을 정해 주지 않는 자리**이므로 여기서 한 번 정한다.
|
|
58
|
+
* 새 모수를 쓸 때는 반드시 여기에 등록한다(문자열 리터럴을 코드에 흩뿌리지 않는다).
|
|
59
|
+
*/
|
|
60
|
+
export declare const OP_PARAM: {
|
|
61
|
+
/** 양품률(0..1, 무차원). 없으면 커널 기본값 — 기본값을 쓴 사실은 `specCoverage()` 가 밝힌다. */
|
|
62
|
+
readonly yield: "yield";
|
|
63
|
+
/** 셋업·체인지오버 소요(ISO 8601 기간 문자열). ISA-95 는 셋업을 별도 세그먼트로도 표현하지만,
|
|
64
|
+
* 현재 커널은 작업에 붙는 셋업으로 다루므로 모수로 받는다. */
|
|
65
|
+
readonly setupDuration: "setupDuration";
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* 소요시간 변동 — **표준 밖 확장이며, 그렇게 표시한다.**
|
|
69
|
+
* ISA-95 `Duration` 은 스칼라 하나라 분포를 담지 못한다. 그런데 시뮬레이션의 신뢰도는 분산에서 나온다
|
|
70
|
+
* (평균만 맞는 상수 모델은 대기·병목을 구조적으로 과소평가한다). 그래서 평균은 표준 자리(`duration`)에
|
|
71
|
+
* 두고, 변동은 이 확장 자리에 둔다 — 섞지 않는다.
|
|
72
|
+
*
|
|
73
|
+
* 표본은 **엔진의 난수원**으로 뽑는다(추정기가 자기 난수를 쓰면 fork 결정성이 깨진다).
|
|
74
|
+
*/
|
|
75
|
+
export interface DurationVariability {
|
|
76
|
+
/** `constant`=변동 없음(기본) · `exponential`(평균=duration) · `uniform`·`triangular`(min/max 필수). */
|
|
77
|
+
distribution: 'constant' | 'exponential' | 'uniform' | 'triangular';
|
|
78
|
+
min?: IsoDuration;
|
|
79
|
+
max?: IsoDuration;
|
|
80
|
+
/** triangular 최빈값. 없으면 `duration` 을 최빈값으로 본다. */
|
|
81
|
+
mode?: IsoDuration;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 오퍼레이션 타입(공정 한 작업) — ISA-95 `OperationsSegment`.
|
|
85
|
+
*
|
|
86
|
+
* **시뮬레이션 명세가 여기 있어야 한다.** 예전에는 이 정의가 "무엇을·어디서·누가" 만 담고 "얼마나
|
|
87
|
+
* 걸리나·얼마나 성공하나" 는 커널 소스의 상수(30·20·40초, 수율 0.8)였다. 그래서 현장마다 다른 값을
|
|
88
|
+
* 데이터로 줄 방법이 없었고, 그 상수가 주목 신호(불량률 높음)까지 만들어 냈다.
|
|
89
|
+
*/
|
|
36
90
|
export interface OperationDef {
|
|
37
91
|
key: string;
|
|
38
92
|
label: string;
|
|
@@ -43,6 +97,35 @@ export interface OperationDef {
|
|
|
43
97
|
resourceType?: string;
|
|
44
98
|
/** CBV bizStep URN(방출 이벤트 어휘). */
|
|
45
99
|
bizStep?: string;
|
|
100
|
+
/**
|
|
101
|
+
* 소요시간(평균/기준) — ISA-95 `OperationsSegment.Duration` 과 1:1. 없으면 커널 기본값을 쓰고,
|
|
102
|
+
* **기본값을 썼다는 사실을 숨기지 않는다**(`specCoverage()`).
|
|
103
|
+
*/
|
|
104
|
+
duration?: IsoDuration;
|
|
105
|
+
/** 소요시간 변동(표준 밖 확장). 미지정 = 상수. */
|
|
106
|
+
variability?: DurationVariability;
|
|
107
|
+
/** 공정 모수(수율·셋업 등) — ISA-95 `ParameterSpecification` 1:1. ID 어휘는 `OP_PARAM`. */
|
|
108
|
+
parameters?: OperationParameter[];
|
|
109
|
+
/**
|
|
110
|
+
* 필요 인원 — **ISA-95 `OperationsSegment.PersonnelSpecification`**(`PersonnelClassID` + `Quantity`) 1:1.
|
|
111
|
+
*
|
|
112
|
+
* 이것이 없으면 사람은 시뮬레이션에 존재하지 않는다 — 설비만 있으면 언제나 돌아가는 공장이 된다.
|
|
113
|
+
* 현장에서 가장 자주 부족한 자원이 사람인데, 그 부족이 만드는 줄이 예측에서 통째로 사라진다.
|
|
114
|
+
* 등급(class)으로 요구한다: 특정인을 지목하는 것이 아니라 "용접 자격자 2명" 이다.
|
|
115
|
+
*/
|
|
116
|
+
personnelSpecification?: {
|
|
117
|
+
personnelClass?: string;
|
|
118
|
+
quantity: number;
|
|
119
|
+
}[];
|
|
120
|
+
/**
|
|
121
|
+
* 필요 물리 자산 — **ISA-95 `OperationsSegment.PhysicalAssetSpecification`** 과 같은 자리
|
|
122
|
+
* (`PhysicalAssetClassID` + `Quantity`). 빈 팔레트가 없어 출고가 못 나가는 일은 현장에서 잦다.
|
|
123
|
+
* 인원과 같은 규칙: 등급으로 요구하고, 모자라면 **부분 투입 없이 기다린다.**
|
|
124
|
+
*/
|
|
125
|
+
physicalAssetSpecification?: {
|
|
126
|
+
assetClass?: string;
|
|
127
|
+
quantity: number;
|
|
128
|
+
}[];
|
|
46
129
|
}
|
|
47
130
|
/** 라우트(오퍼레이션 시퀀스) — ISA-95 ProcessSegment 연결. */
|
|
48
131
|
export interface RouteDef {
|
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
* 스토어 패키지 `@operato/twin-catalog` 가 이 타입을 import 해 스토어 메타(version/supplier)를 얹어 배포한다(catalog → kernel 의존).
|
|
7
7
|
* 표준 앵커: GS1 EPCIS 2.0(bizStep) · ISA-95(WorkCenter·OperationsDefinition·BOM) · ISO 55000(Asset).
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* 우리가 소비하는 canonical 파라미터 ID — **표준이 이름을 정해 주지 않는 자리**이므로 여기서 한 번 정한다.
|
|
11
|
+
* 새 모수를 쓸 때는 반드시 여기에 등록한다(문자열 리터럴을 코드에 흩뿌리지 않는다).
|
|
12
|
+
*/
|
|
13
|
+
export const OP_PARAM = {
|
|
14
|
+
/** 양품률(0..1, 무차원). 없으면 커널 기본값 — 기본값을 쓴 사실은 `specCoverage()` 가 밝힌다. */
|
|
15
|
+
yield: 'yield',
|
|
16
|
+
/** 셋업·체인지오버 소요(ISO 8601 기간 문자열). ISA-95 는 셋업을 별도 세그먼트로도 표현하지만,
|
|
17
|
+
* 현재 커널은 작업에 붙는 셋업으로 다루므로 모수로 받는다. */
|
|
18
|
+
setupDuration: 'setupDuration'
|
|
19
|
+
};
|
|
9
20
|
const INTENTS = ['transport', 'process', 'dwell'];
|
|
10
21
|
function dupes(keys) {
|
|
11
22
|
const seen = new Set();
|
|
@@ -4,9 +4,33 @@ export interface DurationContext {
|
|
|
4
4
|
toNode: string;
|
|
5
5
|
resourceKind?: string;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* 관측된 퍼짐 — **평균만 배우면 대기와 병목을 과소평가한다.**
|
|
9
|
+
*
|
|
10
|
+
* 이력에서 배운 값이 스칼라 하나뿐이면 그 작업은 늘 정확히 같은 시간이 걸린다. 실제 현장은 그렇지
|
|
11
|
+
* 않고, 그 흔들림이 줄을 만든다(변동이 없으면 대기도 없다). 그래서 추정기가 분포까지 알면 그것을
|
|
12
|
+
* 넘긴다 — **표본은 엔진이 자기 난수로 뽑는다**(추정기가 자기 난수를 쓰면 fork 결정성이 깨진다).
|
|
13
|
+
*
|
|
14
|
+
* 단위는 ms 다(ISO 8601 은 사람이 선언하는 명세의 표기이고, 이쪽은 계측에서 나온 수다).
|
|
15
|
+
*/
|
|
16
|
+
export interface DurationSpread {
|
|
17
|
+
distribution: 'triangular' | 'uniform';
|
|
18
|
+
minMs: number;
|
|
19
|
+
maxMs: number;
|
|
20
|
+
/** triangular 최빈값. 없으면 평균을 최빈값으로 본다. */
|
|
21
|
+
modeMs?: number;
|
|
22
|
+
}
|
|
23
|
+
/** 분포까지 아는 추정치 — 평균 + 관측된 퍼짐. */
|
|
24
|
+
export interface DurationEstimate {
|
|
25
|
+
meanMs: number;
|
|
26
|
+
spread?: DurationSpread;
|
|
27
|
+
}
|
|
7
28
|
export interface DurationEstimator {
|
|
8
|
-
/**
|
|
9
|
-
|
|
29
|
+
/**
|
|
30
|
+
* 소요. **숫자**(평균만 안다) 또는 **추정치 객체**(퍼짐까지 안다)를 낼 수 있다.
|
|
31
|
+
* undefined 면 도메인 기본값(fallback) — 일부 kind 만 다루고 나머지는 defer 가능.
|
|
32
|
+
*/
|
|
33
|
+
estimate(ctx: DurationContext): number | DurationEstimate | undefined;
|
|
10
34
|
}
|
|
11
35
|
/** 균일 상수 estimator (테스트·단순 보드용). 실 duration 은 보통 씬-유도 estimator 가 제공. */
|
|
12
36
|
export declare const constantDuration: (ms: number) => DurationEstimator;
|