rameshmoorthy commited on
Commit
2de7cdf
·
verified ·
1 Parent(s): 3f9d5ac

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +1088 -775
app.py CHANGED
@@ -4,986 +4,1299 @@ import numpy as np
4
  import pandas as pd
5
  import plotly.graph_objects as go
6
  import json
7
- import random
 
8
  from dataclasses import dataclass, field
9
  from typing import List, Optional
10
- import time
11
 
12
  st.set_page_config(
13
- page_title="Meridia FrontierTRS Simulator",
14
- page_icon="🚢",
15
  layout="wide",
16
  initial_sidebar_state="expanded"
17
  )
18
 
19
- # ── Inject custom CSS ──────────────────────────────────────────────────────────
 
 
 
20
  st.markdown("""
21
  <style>
22
- @import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=Share+Tech+Mono&display=swap');
23
 
24
  :root {
25
- --meridia-dark: #0a0e1a;
26
- --meridia-navy: #0d1b2a;
27
- --meridia-blue: #1a3a5c;
28
- --meridia-cyan: #00d4ff;
29
- --meridia-gold: #f5a623;
30
- --meridia-green: #00ff88;
31
- --meridia-red: #ff4757;
32
- --meridia-text: #c8d8e8;
33
- --meridia-muted: #5a7a9a;
 
 
 
 
 
 
34
  }
35
 
36
  html, body, [class*="css"] {
37
- font-family: 'Rajdhani', sans-serif;
38
- background-color: var(--meridia-dark) !important;
39
- color: var(--meridia-text) !important;
40
  }
41
-
42
- .stApp { background: var(--meridia-dark) !important; }
43
 
44
  /* Sidebar */
45
  [data-testid="stSidebar"] {
46
- background: linear-gradient(180deg, #0d1b2a 0%, #0a0e1a 100%) !important;
47
- border-right: 1px solid #1a3a5c !important;
48
  }
 
 
 
 
 
 
 
 
49
 
50
  /* Metrics */
51
  [data-testid="stMetric"] {
52
- background: rgba(0,212,255,0.05) !important;
53
- border: 1px solid rgba(0,212,255,0.2) !important;
54
- border-radius: 8px !important;
55
- padding: 12px !important;
 
56
  }
57
- [data-testid="stMetricValue"] { color: var(--meridia-cyan) !important; font-family: 'Share Tech Mono' !important; font-size: 1.8rem !important; }
58
- [data-testid="stMetricLabel"] { color: var(--meridia-muted) !important; font-size: 0.75rem !important; letter-spacing: 0.1em !important; }
59
- [data-testid="stMetricDelta"] { font-family: 'Share Tech Mono' !important; }
60
-
61
- /* Sliders */
62
- [data-testid="stSlider"] > div > div > div > div {
63
- background: var(--meridia-cyan) !important;
 
 
 
 
 
64
  }
65
 
66
  /* Buttons */
67
- .stButton > button {
68
- background: linear-gradient(135deg, #1a3a5c, #0d1b2a) !important;
69
- border: 1px solid var(--meridia-cyan) !important;
70
- color: var(--meridia-cyan) !important;
71
- font-family: 'Rajdhani', sans-serif !important;
72
- font-weight: 600 !important;
73
- letter-spacing: 0.1em !important;
74
- border-radius: 4px !important;
75
- transition: all 0.2s !important;
76
- }
77
- .stButton > button:hover {
78
- background: rgba(0,212,255,0.15) !important;
79
- box-shadow: 0 0 12px rgba(0,212,255,0.3) !important;
80
  }
 
81
 
82
- /* Section headers */
83
- h1, h2, h3 { font-family: 'Rajdhani', sans-serif !important; letter-spacing: 0.05em !important; }
84
- h1 { color: var(--meridia-cyan) !important; }
85
- h2 { color: var(--meridia-gold) !important; }
86
- h3 { color: var(--meridia-text) !important; }
87
 
88
  /* Tabs */
89
  [data-testid="stTabs"] button {
90
- font-family: 'Rajdhani', sans-serif !important;
91
- font-weight: 600 !important;
92
- letter-spacing: 0.08em !important;
93
- color: var(--meridia-muted) !important;
94
  }
95
  [data-testid="stTabs"] button[aria-selected="true"] {
96
- color: var(--meridia-cyan) !important;
97
- border-bottom-color: var(--meridia-cyan) !important;
98
  }
99
 
100
- /* Dataframe */
101
- [data-testid="stDataFrame"] { border: 1px solid #1a3a5c !important; }
 
 
 
 
 
 
 
102
 
103
  /* Scrollbar */
104
- ::-webkit-scrollbar { width: 4px; }
105
- ::-webkit-scrollbar-track { background: var(--meridia-dark); }
106
- ::-webkit-scrollbar-thumb { background: var(--meridia-blue); border-radius: 2px; }
107
-
108
- .port-badge {
109
- display: inline-block;
110
- padding: 2px 10px;
111
- border-radius: 20px;
112
- font-size: 0.7rem;
113
- font-weight: 700;
114
- letter-spacing: 0.1em;
115
- text-transform: uppercase;
116
  }
117
-
118
- .glow-text { text-shadow: 0 0 10px rgba(0,212,255,0.5); }
 
 
 
 
 
 
 
 
 
 
 
 
119
  </style>
120
  """, unsafe_allow_html=True)
121
 
122
 
123
  # ══════════════════════════════════════════════════════════════════════════════
124
- # DATA MODEL
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  # ══════════════════════════════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
 
 
 
127
  @dataclass
128
  class BoE:
129
  shipment_id: str
130
- port_type: str # Sea / Air / Land
131
- filing_type: str # Advance / Normal
132
  pga_involved: bool
133
- aeo_status: str # T1 / T2 / T3 / None
134
- machine_release: bool = False
135
- t_arrival: float = 0.0
136
- t_filed: float = 0.0
137
- t_assessed: float = 0.0
138
- t_payment: float = 0.0
139
- t_ooc: float = 0.0
 
140
 
141
  @property
142
- def total_hours(self): return max(self.t_ooc - self.t_arrival, 0)
143
  @property
144
- def seg_filing(self): return max(self.t_filed - self.t_arrival, 0)
145
  @property
146
- def seg_assessment(self): return max(self.t_assessed - self.t_filed, 0)
147
  @property
148
- def seg_payment(self): return max(self.t_payment - self.t_assessed, 0)
149
  @property
150
- def seg_logistics(self): return max(self.t_ooc - self.t_payment, 0)
151
 
152
 
153
  # ══════════════════════════════════════════════════════════════════════════════
154
- # SIMPY SIMULATION ENGINE
155
  # ══════════════════════════════════════════════════════════════════════════════
156
-
157
- def run_simulation(params: dict) -> List[BoE]:
158
- """Core SimPy discrete-event simulation of Meridia's three ports."""
159
  results: List[BoE] = []
160
  rng = np.random.default_rng(42)
161
-
162
  env = simpy.Environment()
163
 
164
- # Resources
 
 
165
  officers_sea = simpy.Resource(env, capacity=params["officers_sea"])
166
  officers_air = simpy.Resource(env, capacity=params["officers_air"])
167
  officers_land = simpy.Resource(env, capacity=params["officers_land"])
168
 
 
169
  PORT_CONFIG = {
170
- "Sea": {"volume": 50, "resource": officers_sea, "speed_mult": 1.0},
171
- "Air": {"volume": 10, "resource": officers_air, "speed_mult": 0.5},
172
- "Land": {"volume": 25, "resource": officers_land, "speed_mult": 0.75},
 
 
 
173
  }
174
-
175
  counter = [0]
176
 
177
  def process_boe(env, port_type, cfg):
178
  counter[0] += 1
179
- sid = f"BoE-{port_type[0]}{counter[0]:04d}"
180
 
181
- # Determine attributes
182
  is_advance = rng.random() < (params["advance_filing_pct"] / 100)
183
  is_aeo = rng.random() < (params["aeo_enrollment_pct"] / 100)
184
- is_pga = rng.random() < 0.35 # 35% PGA involvement
185
- aeo_status = rng.choice(["T1","T2","T3"]) if is_aeo else "None"
186
- green = rng.random() < (params["rms_facilitation_pct"] / 100)
187
-
188
- boe = BoE(
189
- shipment_id=sid,
190
- port_type=port_type,
191
- filing_type="Advance" if is_advance else "Normal",
192
- pga_involved=is_pga,
193
- aeo_status=aeo_status,
194
- )
195
 
 
 
 
196
  boe.t_arrival = env.now
197
 
198
- # ── Stage 1: Arrival → Filing ──────────────────────────────────────
 
199
  if is_advance:
200
- filing_delay = 0
201
  else:
202
- filing_delay = rng.gamma(shape=2, scale=12) * cfg["speed_mult"]
203
- yield env.timeout(filing_delay)
204
- boe.t_filed = env.now
205
 
206
- # ── Stage 2: Assessment ────────────────────────────────────────────
207
- if green:
208
- assess_delay = 0 # Green channel: skip
209
  else:
210
  with cfg["resource"].request() as req:
211
  yield req
212
- if is_aeo:
213
- assess_delay = rng.gamma(shape=2, scale=2) * cfg["speed_mult"]
214
  else:
215
- assess_delay = rng.gamma(shape=2, scale=4) * cfg["speed_mult"]
216
- # Physical exam
217
- if rng.random() < 0.15 and not green:
218
- assess_delay += rng.gamma(shape=3, scale=8) * cfg["speed_mult"]
219
- yield env.timeout(assess_delay)
220
  boe.t_assessed = env.now
221
 
222
- # ── Stage 3: Payment / PGA ────────────────────────────────────────
223
- if is_aeo and params["deferred_duty"]:
224
- payment_delay = 0
 
225
  else:
226
- base_pay = rng.gamma(shape=2, scale=3)
227
- if is_pga and not params["pga_single_window"]:
228
- base_pay *= 4.0 # Big amber expansion
229
- elif is_pga and params["pga_single_window"]:
230
- base_pay *= 1.4
231
- payment_delay = base_pay * cfg["speed_mult"]
232
- yield env.timeout(payment_delay)
233
  boe.t_payment = env.now
234
 
235
- # ── Stage 4: Out-of-Charge ────────────────────────────────────────
236
- if green and params["auto_ooc"]:
237
- logistics_delay = 0
238
  boe.machine_release = True
 
 
 
239
  else:
240
- logistics_delay = rng.gamma(shape=2, scale=1.5) * cfg["speed_mult"]
241
- yield env.timeout(logistics_delay)
242
  boe.t_ooc = env.now
243
-
244
  results.append(boe)
245
 
246
- def port_generator(env, port_type, cfg):
247
- """Generate shipments for a port with Poisson inter-arrivals."""
248
  for _ in range(cfg["volume"]):
249
- env.process(process_boe(env, port_type, cfg))
250
- interarrival = rng.exponential(scale=1.5)
251
- yield env.timeout(interarrival)
252
-
253
- for ptype, cfg in PORT_CONFIG.items():
254
- env.process(port_generator(env, ptype, cfg))
255
 
256
- env.run(until=500)
 
 
257
  return results
258
 
259
 
260
  # ══════════════════════════════════════════════════════════════════════════════
261
- # PHASER.JS ISOMETRIC GAME VIEW
262
  # ══════════════════════════════════════════════════════════════════════════════
263
-
264
- def build_phaser_scene(results: List[BoE], params: dict) -> str:
265
- """Generate the Phaser.js isometric port visualization."""
266
-
267
- art_h = round(params["avg_art"], 1) if results else 0
268
- fac_pct = round(params["green_pct"], 1) if results else 0
269
- aeo_pct = round(params["aeo_pct"], 1) if results else 0
270
- mach_pct = round(params["machine_pct"], 1) if results else 0
271
- target48 = round(params["target48_pct"], 1) if results else 0
272
- ship_count = len([r for r in results if r.port_type=="Sea"]) if results else 0
273
- air_count = len([r for r in results if r.port_type=="Air"]) if results else 0
274
- land_count = len([r for r in results if r.port_type=="Land"]) if results else 0
275
-
276
- # Shipment status data for visualization
277
- if results:
278
- statuses = []
279
- for r in results[:60]:
280
- if r.total_hours < 3: s = "fast"
281
- elif r.total_hours < 24: s = "ok"
282
- elif r.total_hours < 48: s = "slow"
283
- else: s = "delayed"
284
- statuses.append({"id": r.shipment_id, "port": r.port_type,
285
- "hours": round(r.total_hours, 1), "status": s,
286
- "machine": r.machine_release})
287
- else:
288
- statuses = []
289
-
290
- statuses_json = json.dumps(statuses)
291
-
292
- return f"""<!DOCTYPE html>
293
- <html>
294
- <head>
295
- <meta charset="utf-8">
296
  <style>
297
- * {{ margin:0; padding:0; box-sizing:border-box; }}
298
- body {{
299
- background: #0a0e1a;
300
- font-family: 'Rajdhani', 'Share Tech Mono', monospace;
301
- overflow: hidden;
302
- width: 100%;
303
- height: 520px;
304
- }}
305
- @import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@600;700&family=Share+Tech+Mono&display=swap');
306
-
307
- #game-container {{
308
- position: relative;
309
- width: 100%;
310
- height: 100%;
311
- }}
312
-
313
- #hud {{
314
- position: absolute;
315
- top: 0; left: 0; right: 0;
316
- display: flex;
317
- justify-content: space-between;
318
- align-items: flex-start;
319
- padding: 12px 16px;
320
- pointer-events: none;
321
- z-index: 10;
322
- }}
323
-
324
- .hud-panel {{
325
- background: rgba(10,14,26,0.85);
326
- border: 1px solid rgba(0,212,255,0.3);
327
- border-radius: 6px;
328
- padding: 8px 14px;
329
- backdrop-filter: blur(4px);
330
- }}
331
-
332
- .hud-title {{
333
- font-family: 'Rajdhani', sans-serif;
334
- font-size: 9px;
335
- font-weight: 700;
336
- letter-spacing: 0.15em;
337
- color: #5a7a9a;
338
- text-transform: uppercase;
339
- margin-bottom: 2px;
340
- }}
341
-
342
- .hud-value {{
343
- font-family: 'Share Tech Mono', monospace;
344
- font-size: 22px;
345
- font-weight: 400;
346
- color: #00d4ff;
347
- line-height: 1;
348
- }}
349
-
350
- .hud-value.good {{ color: #00ff88; }}
351
- .hud-value.warn {{ color: #f5a623; }}
352
- .hud-value.bad {{ color: #ff4757; }}
353
-
354
- .hud-sub {{
355
- font-size: 9px;
356
- color: #5a7a9a;
357
- margin-top: 2px;
358
- font-family: 'Rajdhani', sans-serif;
359
- letter-spacing: 0.05em;
360
- }}
361
-
362
- .hud-row {{ display: flex; gap: 10px; }}
363
-
364
- #bottom-hud {{
365
- position: absolute;
366
- bottom: 0; left: 0; right: 0;
367
- display: flex;
368
- gap: 8px;
369
- padding: 10px 16px;
370
- z-index: 10;
371
- pointer-events: none;
372
- }}
373
-
374
- .port-stat {{
375
- background: rgba(10,14,26,0.85);
376
- border: 1px solid rgba(0,212,255,0.2);
377
- border-radius: 6px;
378
- padding: 6px 12px;
379
- display: flex;
380
- align-items: center;
381
- gap: 8px;
382
- }}
383
-
384
- .port-dot {{ width: 8px; height: 8px; border-radius: 50%; }}
385
- .port-dot.sea {{ background: #00d4ff; box-shadow: 0 0 6px #00d4ff; }}
386
- .port-dot.air {{ background: #f5a623; box-shadow: 0 0 6px #f5a623; }}
387
- .port-dot.land {{ background: #00ff88; box-shadow: 0 0 6px #00ff88; }}
388
-
389
- .port-name {{
390
- font-size: 10px;
391
- font-weight: 700;
392
- letter-spacing: 0.1em;
393
- color: #c8d8e8;
394
- font-family: 'Rajdhani', sans-serif;
395
- }}
396
-
397
- .port-count {{
398
- font-size: 14px;
399
- font-family: 'Share Tech Mono', monospace;
400
- color: #00d4ff;
401
- }}
402
-
403
- #ticker {{
404
- position: absolute;
405
- top: 50%; left: 50%;
406
- transform: translate(-50%, -50%);
407
- pointer-events: none;
408
- z-index: 5;
409
- text-align: center;
410
- }}
411
-
412
- #clock {{
413
- font-family: 'Share Tech Mono', monospace;
414
- font-size: 13px;
415
- color: rgba(0,212,255,0.4);
416
- letter-spacing: 0.2em;
417
- }}
418
-
419
- canvas {{ display: block; }}
420
- #phaser-canvas {{ width: 100% !important; height: 100% !important; }}
421
- </style>
422
- </head>
423
- <body>
424
- <div id="game-container">
425
- <div id="hud">
426
- <div class="hud-panel">
427
- <div class="hud-title">Average Release Time</div>
428
- <div class="hud-value {'good' if art_h < 24 else 'warn' if art_h < 48 else 'bad'}">{art_h}h</div>
429
- <div class="hud-sub">NTFAP Target: 48h</div>
430
  </div>
431
-
432
- <div class="hud-row">
433
- <div class="hud-panel">
434
- <div class="hud-title">Green Channel</div>
435
- <div class="hud-value {'good' if fac_pct > 60 else 'warn'}">{fac_pct}%</div>
436
- <div class="hud-sub">Facilitated</div>
 
 
 
437
  </div>
438
- <div class="hud-panel">
439
- <div class="hud-title">Machine Release</div>
440
- <div class="hud-value {'good' if mach_pct > 40 else 'warn'}">{mach_pct}%</div>
441
- <div class="hud-sub">Auto-OOC</div>
442
  </div>
443
- <div class="hud-panel">
444
- <div class="hud-title">Target48h</div>
445
- <div class="hud-value {'good' if target48 > 80 else 'warn' if target48 > 60 else 'bad'}">{target48}%</div>
446
- <div class="hud-sub">Achievement</div>
 
 
 
 
 
447
  </div>
448
- </div>
449
-
450
- <div class="hud-panel">
451
- <div class="hud-title">AEO Enrolled</div>
452
- <div class="hud-value {'good' if aeo_pct > 50 else 'warn'}">{aeo_pct}%</div>
453
- <div class="hud-sub">Trusted Traders</div>
454
  </div>
455
  </div>
456
 
457
- <div id="bottom-hud">
458
- <div class="port-stat">
459
- <div class="port-dot sea"></div>
460
- <span class="port-name">AZURE PORT</span>
461
- <span class="port-count">{ship_count}</span>
462
- </div>
463
- <div class="port-stat">
464
- <div class="port-dot air"></div>
465
- <span class="port-name">VORTEX AIR</span>
466
- <span class="port-count">{air_count}</span>
467
  </div>
468
- <div class="port-stat">
469
- <div class="port-dot land"></div>
470
- <span class="port-name">STONE PASS</span>
471
- <span class="port-count">{land_count}</span>
472
- </div>
473
- <div id="clock" class="port-stat" style="margin-left:auto; border-color: rgba(0,212,255,0.15);">
474
- <span id="sim-clock" style="font-family:'Share Tech Mono',monospace; font-size:12px; color:#5a7a9a; letter-spacing:0.15em;">MERIDIA — SIM ACTIVE</span>
475
  </div>
 
476
  </div>
477
 
 
 
478
  <script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>
479
  <script>
480
- const STATUSES = {statuses_json};
481
- const RUNNING = {'true' if results else 'false'};
482
-
483
- // ── Colour palette ──────────────────────────────────────────────────────
484
- const C = {{
485
- fast: 0x00ff88,
486
- ok: 0x00d4ff,
487
- slow: 0xf5a623,
488
- delayed: 0xff4757,
489
- sea: 0x00d4ff,
490
- air: 0xf5a623,
491
- land: 0x00ff88,
492
- grid: 0x1a3a5c,
493
- dark: 0x0a0e1a,
494
- water: 0x0d2340,
495
- sand: 0x1a2e40,
496
- building:0x152840,
497
- }};
498
-
499
- class MeridiaScene extends Phaser.Scene {{
500
- constructor() {{ super({{ key: 'MeridiaScene' }}); }}
501
-
502
- preload() {{}}
503
-
504
- create() {{
505
- const W = this.scale.width;
506
- const H = this.scale.height;
507
-
508
- // ── Sky gradient background ────────────────────────────────────────
509
- const bg = this.add.graphics();
510
- bg.fillGradientStyle(0x0a0e1a, 0x0a0e1a, 0x0d2340, 0x0d2340, 1);
511
- bg.fillRect(0, 0, W, H);
512
-
513
- // ── Stars ─────────────────────────────────────────────────────────
514
- const stars = this.add.graphics();
515
- stars.fillStyle(0xffffff, 0.6);
516
- for (let i = 0; i < 80; i++) {{
517
- const x = Phaser.Math.Between(0, W);
518
- const y = Phaser.Math.Between(0, H * 0.5);
519
- const r = Math.random() < 0.1 ? 1.5 : 0.8;
520
- stars.fillCircle(x, y, r);
521
  }}
522
 
523
- // ── Isometric grid helper ─────────────────────────────────────────
524
- const ISO_W = 48, ISO_H = 24;
525
-
526
- const toIso = (col, row) => {{
527
- return {{
528
- x: W/2 + (col - row) * ISO_W / 2,
529
- y: 140 + (col + row) * ISO_H / 2
530
- }};
531
- }};
532
-
533
- const drawTile = (gfx, col, row, color, alpha=1, h=0) => {{
534
- const p = toIso(col, row);
535
- const pts = [
536
- p.x, p.y - h,
537
- p.x+ISO_W/2, p.y+ISO_H/2 - h,
538
- p.x, p.y+ISO_H - h,
539
- p.x-ISO_W/2, p.y+ISO_H/2 - h,
540
- ];
541
- gfx.fillStyle(color, alpha);
542
- gfx.fillPoints(pts.reduce((a,v,i)=> i%2===0?[...a,{{x:pts[i],y:pts[i+1]}}]:a,[]), true);
543
- // Left face (darker)
544
- if (h > 0) {{
545
- const lpts = [
546
- p.x-ISO_W/2, p.y+ISO_H/2 - h,
547
- p.x, p.y+ISO_H - h,
548
- p.x, p.y+ISO_H,
549
- p.x-ISO_W/2, p.y+ISO_H/2,
550
- ];
551
- gfx.fillStyle(Phaser.Display.Color.ValueToColor(color).darken(30).color, alpha);
552
- gfx.fillPoints(lpts.reduce((a,v,i)=> i%2===0?[...a,{{x:lpts[i],y:lpts[i+1]}}]:a,[]), true);
553
- // Right face
554
- const rpts = [
555
- p.x, p.y+ISO_H - h,
556
- p.x+ISO_W/2, p.y+ISO_H/2 - h,
557
- p.x+ISO_W/2, p.y+ISO_H/2,
558
- p.x, p.y+ISO_H,
559
- ];
560
- gfx.fillStyle(Phaser.Display.Color.ValueToColor(color).darken(15).color, alpha);
561
- gfx.fillPoints(rpts.reduce((a,v,i)=> i%2===0?[...a,{{x:rpts[i],y:rpts[i+1]}}]:a,[]), true);
562
  }}
563
  }};
564
 
565
- const terrain = this.add.graphics();
566
-
567
- // ── Water tiles (left zone — Azure Port) ──────────────────────────
568
- for (let r = 0; r < 8; r++) {{
569
- for (let c = 0; c < 5; c++) {{
570
- drawTile(terrain, c-3, r, C.water, 0.9);
571
- }}
572
- }}
573
-
574
- // ── Land tiles ────────────────────────────────────────────────────
575
- for (let r = 0; r < 8; r++) {{
576
- for (let c = 2; c < 12; c++) {{
577
- const shade = (c+r) % 2 === 0 ? 0x1a2e40 : 0x162838;
578
- drawTile(terrain, c, r, shade, 1);
579
- }}
580
- }}
581
-
582
- // ── Buildings (customs offices) ───────────────────────────────────
583
- const buildings = this.add.graphics();
584
-
585
- const drawBuilding = (col, row, w, d, h, color) => {{
586
- for (let dc=0; dc<w; dc++) {{
587
- for (let dr=0; dr<d; dr++) {{
588
- drawTile(buildings, col+dc, row+dr, color, 1, h);
589
- }}
590
- }}
591
- }};
592
-
593
- // Azure Port customs hall
594
- drawBuilding(3, 1, 2, 2, 18, 0x1a4a6c);
595
- // Vortex Air terminal
596
- drawBuilding(7, 1, 3, 2, 14, 0x2a3a5c);
597
- // Stone Pass checkpoint
598
- drawBuilding(3, 5, 2, 2, 12, 0x1a4a3c);
599
-
600
- // ── Port labels ────────────────────────────────────────────────────
601
- const labelStyle = {{
602
- fontFamily: 'Rajdhani, sans-serif',
603
- fontSize: '10px',
604
- fontStyle: 'bold',
605
- color: '#5a9abf',
606
- letterSpacing: 2,
607
- }};
608
-
609
- const p1 = toIso(3.5, 0);
610
- this.add.text(p1.x, p1.y - 30, 'AZURE PORT', {{ ...labelStyle, color: '#00d4ff' }}).setOrigin(0.5);
611
-
612
- const p2 = toIso(8.5, 0);
613
- this.add.text(p2.x, p2.y - 24, 'VORTEX AIR', {{ ...labelStyle, color: '#f5a623' }}).setOrigin(0.5);
614
-
615
- const p3 = toIso(4, 5.5);
616
- this.add.text(p3.x, p3.y - 22, 'STONE PASS', {{ ...labelStyle, color: '#00ff88' }}).setOrigin(0.5);
617
-
618
- // ── Cargo dots (animated, colour by status) ────────────────────────
619
- if (RUNNING && STATUSES.length > 0) {{
620
- const portOrigins = {{
621
- Sea: {{ col: 1.5, row: 3 }},
622
- Air: {{ col: 8.5, row: 3 }},
623
- Land: {{ col: 4, row: 6 }},
624
- }};
625
-
626
- STATUSES.forEach((s, i) => {{
627
- const orig = portOrigins[s.port];
628
- const col = orig.col + (Math.random()-0.5)*2.5;
629
- const row = orig.row + (Math.random()-0.5)*2.0;
630
- const pos = toIso(col, row);
631
-
632
- const color = C[s.status];
633
- const gfx = this.add.graphics();
634
-
635
- gfx.fillStyle(color, 0.9);
636
- gfx.fillCircle(pos.x, pos.y, 5);
637
- gfx.lineStyle(1, color, 0.4);
638
- gfx.strokeCircle(pos.x, pos.y, 9);
639
-
640
- if (s.machine) {{
641
- gfx.lineStyle(1, 0xffffff, 0.6);
642
- gfx.strokeCircle(pos.x, pos.y, 13);
643
- }}
644
-
645
- // Pulse animation
646
- this.tweens.add({{
647
- targets: gfx,
648
- alpha: {{ from: 0.5, to: 1 }},
649
- duration: 800 + i * 40,
650
- yoyo: true,
651
- repeat: -1,
652
- ease: 'Sine.easeInOut',
653
- }});
654
-
655
- // Float vertically
656
- this.tweens.add({{
657
- targets: gfx,
658
- y: gfx.y - 4,
659
- duration: 1200 + i * 60,
660
- yoyo: true,
661
- repeat: -1,
662
- ease: 'Sine.easeInOut',
663
- }});
664
-
665
- // Tooltip on hover
666
- const hitZone = this.add.circle(pos.x, pos.y, 14, 0xffffff, 0)
667
- .setInteractive({{ useHandCursor: true }});
668
-
669
- const tooltip = this.add.text(pos.x + 16, pos.y - 16,
670
- `${{s.id}}\\n${{s.port}} | ${{s.hours}}h | ${{s.status.toUpperCase()}}`,
671
- {{
672
- fontFamily: 'Share Tech Mono, monospace',
673
- fontSize: '9px',
674
- color: '#c8d8e8',
675
- backgroundColor: '#0a0e1aee',
676
- padding: {{ x:6, y:4 }},
677
- lineSpacing: 3,
678
- }}
679
  ).setVisible(false).setDepth(100);
680
-
681
- hitZone.on('pointerover', () => tooltip.setVisible(true));
682
- hitZone.on('pointerout', () => tooltip.setVisible(false));
683
  }});
684
  }} else {{
685
- // Idle state — gentle floating dots
686
- for (let i=0; i<12; i++) {{
687
- const col = Math.random()*10 - 1;
688
- const row = Math.random()*7;
689
- const pos = toIso(col, row);
690
- const gfx = this.add.graphics();
691
- gfx.fillStyle(0x1a3a5c, 0.7);
692
- gfx.fillCircle(pos.x, pos.y, 4);
693
- this.tweens.add({{
694
- targets: gfx, alpha: {{from:0.2, to:0.7}},
695
- duration: 1500+i*200, yoyo:true, repeat:-1,
696
- }});
697
  }}
698
  }}
699
-
700
- // ── Ship in water ──────────────────────────────────────────────────
701
- const shipGfx = this.add.graphics();
702
- const sp = toIso(-1, 3);
703
- shipGfx.fillStyle(0x2a5a8c, 1);
704
- shipGfx.fillRect(sp.x-20, sp.y-6, 40, 12);
705
- shipGfx.fillStyle(0x3a7abf, 1);
706
- shipGfx.fillRect(sp.x-8, sp.y-14, 16, 8);
707
-
708
- this.tweens.add({{
709
- targets: shipGfx, y: '-=3',
710
- duration: 2000, yoyo: true, repeat: -1, ease: 'Sine.easeInOut',
711
- }});
712
-
713
- // ── Legend ─────────────────────────────────────────────────────────
714
- const lg = this.add.graphics();
715
- const lx = W - 130, ly = H - 110;
716
- [[C.fast,'<3h FAST'],[C.ok,'<24h OK'],[C.slow,'<48h SLOW'],[C.delayed,'>48h LATE']].forEach(([col,lbl],i) => {{
717
- lg.fillStyle(col, 1);
718
- lg.fillCircle(lx, ly + i*20, 5);
719
- this.add.text(lx+12, ly+i*20-7, lbl, {{
720
- fontFamily:'Rajdhani,sans-serif', fontSize:'10px',
721
- fontStyle:'bold', color:'#5a7a9a', letterSpacing:1,
722
- }});
723
- }});
724
  }}
725
-
726
- update() {{
727
- // Update simulated clock
728
- const el = document.getElementById('sim-clock');
729
- if (el) {{
730
- const now = new Date();
731
- el.textContent = now.toLocaleTimeString('en-GB') + ' | MERIDIA FRONTIER';
732
- }}
733
  }}
734
  }}
735
 
 
736
  new Phaser.Game({{
737
- type: Phaser.AUTO,
738
- parent: 'game-container',
739
- width: '100%',
740
- height: 520,
741
- transparent: true,
742
- backgroundColor: '#0a0e1a',
743
- scale: {{
744
- mode: Phaser.Scale.FIT,
745
- autoCenter: Phaser.Scale.CENTER_BOTH,
746
- }},
747
- scene: [MeridiaScene],
748
- canvas: document.createElement('canvas'),
749
  }});
750
  </script>
751
- </div>
752
- </body>
753
- </html>"""
754
 
755
 
756
  # ══════════════════════════════════════════════════════════════════════════════
757
- # TRS STACKED BAR CHART
758
  # ══════════════════════════════════════════════════════════════════════════════
 
 
 
 
759
 
760
- def build_trs_chart(results: List[BoE]) -> go.Figure:
761
- ports = ["Sea (Azure Port)", "Air (Vortex)", "Land (Stone Pass)", "ALL PORTS"]
762
- port_map = {"Sea": "Sea (Azure Port)", "Air": "Air (Vortex)", "Land": "Land (Stone Pass)"}
 
763
 
764
- data = {p: {"filing": [], "assessment": [], "payment": [], "logistics": []} for p in ports}
765
 
766
- for r in results:
767
- pk = port_map[r.port_type]
768
- data[pk]["filing"].append(r.seg_filing)
769
- data[pk]["assessment"].append(r.seg_assessment)
770
- data[pk]["payment"].append(r.seg_payment)
771
- data[pk]["logistics"].append(r.seg_logistics)
772
- data["ALL PORTS"]["filing"].append(r.seg_filing)
773
- data["ALL PORTS"]["assessment"].append(r.seg_assessment)
774
- data["ALL PORTS"]["payment"].append(r.seg_payment)
775
- data["ALL PORTS"]["logistics"].append(r.seg_logistics)
776
-
777
- avgs = {p: {seg: np.mean(v) if v else 0 for seg, v in segs.items()} for p, segs in data.items()}
778
 
779
  segs = [
780
- ("filing", "Pre-arrival / Filing", "#1a4a8c"),
781
- ("assessment", "Customs Assessment", "#2a7abf"),
782
- ("payment", "Trade Action / PGA / Duty","#f5a623"),
783
- ("logistics", "Post-clearance Logistics", "#3a5a7a"),
784
  ]
785
-
786
  fig = go.Figure()
787
- for seg_key, seg_name, color in segs:
788
  fig.add_trace(go.Bar(
789
- name=seg_name,
790
- x=ports,
791
- y=[avgs[p][seg_key] for p in ports],
792
- marker_color=color,
793
- text=[f"{avgs[p][seg_key]:.1f}h" for p in ports],
794
  textposition="inside",
795
- textfont=dict(family="Share Tech Mono, monospace", size=10, color="white"),
796
  ))
797
 
798
- # 48h target line
799
- fig.add_hline(y=48, line_dash="dash", line_color="#ff4757", line_width=1.5,
800
- annotation_text="48h NTFAP Target", annotation_font_color="#ff4757",
801
- annotation_font_size=10)
802
- fig.add_hline(y=24, line_dash="dot", line_color="#f5a623", line_width=1,
803
- annotation_text="24h Air Target", annotation_font_color="#f5a623",
804
- annotation_font_size=10)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
805
 
806
  fig.update_layout(
807
  barmode="stack",
808
- plot_bgcolor="#0d1b2a",
809
- paper_bgcolor="#0a0e1a",
810
- font=dict(family="Rajdhani, sans-serif", color="#c8d8e8"),
811
- title=dict(text="MERIDIA TRS — RELEASE TIME BREAKDOWN BY PORT",
812
- font=dict(size=14, color="#00d4ff", family="Rajdhani"),
813
- x=0.5),
814
- xaxis=dict(gridcolor="#1a3a5c", linecolor="#1a3a5c"),
815
- yaxis=dict(gridcolor="#1a3a5c", linecolor="#1a3a5c",
816
- title="Hours", titlefont=dict(color="#5a7a9a")),
817
- legend=dict(orientation="h", y=-0.2, font=dict(size=10)),
818
- margin=dict(t=50, b=80, l=50, r=20),
819
- height=380,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
820
  )
821
  return fig
822
 
823
 
824
  # ══════════════════════════════════════════════════════════════════════════════
825
- # STREAMLIT UI
826
  # ══════════════════════════════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
827
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
828
  def main():
829
- # ── Header ─────────────────────────────────────────────────────────────
830
  st.markdown("""
831
- <div style='text-align:center; padding: 1rem 0 0.5rem;'>
832
- <h1 style='font-family:Rajdhani,sans-serif; font-size:2.2rem; letter-spacing:0.15em;
833
- color:#00d4ff; text-shadow: 0 0 20px rgba(0,212,255,0.4); margin:0;'>
834
- ◈ THE MERIDIA FRONTIER
835
- </h1>
836
- <p style='color:#5a7a9a; font-size:0.8rem; letter-spacing:0.2em; margin:4px 0 0;
837
- font-family:Rajdhani,sans-serif;'>
838
- TIME RELEASE STUDY — GAMIFIED CUSTOMS SIMULATOR
839
- </p>
 
 
 
 
 
 
 
 
 
840
  </div>
841
- <hr style='border:none; border-top: 1px solid #1a3a5c; margin: 0.75rem 0;'>
842
  """, unsafe_allow_html=True)
843
 
844
- # ── Sidebar Controls ────────────────────────────────────────────────────
845
  with st.sidebar:
846
- st.markdown("### POLICY LEVERS")
847
- st.markdown("<p style='color:#5a7a9a;font-size:0.72rem;letter-spacing:0.1em;'>ADJUST PARAMETERS & RUN SIM</p>", unsafe_allow_html=True)
 
848
 
849
- st.markdown("**Filing & Risk**")
850
- advance_pct = st.slider("Advance Filing %", 0, 100, 40, key="adv")
851
- rms_pct = st.slider("RMS Facilitation %", 0, 100, 50, key="rms")
852
- aeo_pct = st.slider("AEO Enrollment %", 0, 100, 30, key="aeo")
 
 
 
 
 
 
 
 
 
 
853
 
854
  st.markdown("**System Enablers**")
855
- pga_sw = st.toggle("PGA Single Window", value=False, key="pga")
856
- deferred = st.toggle("Deferred Duty (AEO)", value=False, key="def")
857
- auto_ooc = st.toggle("Auto Out-of-Charge", value=False, key="ooc")
858
 
859
  st.markdown("**Officer Capacity**")
860
- officers_sea = st.slider("Sea Officers", 1, 20, 8, key="osea")
861
- officers_air = st.slider("Air Officers", 1, 10, 4, key="oair")
862
- officers_land = st.slider("Land Officers", 1, 15, 6, key="oland")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
863
 
864
  st.markdown("---")
865
- run_btn = st.button("▶ RUN SIMULATION", use_container_width=True)
866
- reset_btn = st.button("↺ RESET", use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
867
 
868
  # ── Session state ────────────────────────────────────────────────────────
869
- if "results" not in st.session_state:
870
- st.session_state.results = []
871
- if "sim_params" not in st.session_state:
872
- st.session_state.sim_params = {}
 
873
  if reset_btn:
874
- st.session_state.results = []
875
- st.session_state.sim_params = {}
876
 
877
  if run_btn:
878
  params = dict(
879
- advance_filing_pct=advance_pct,
880
- rms_facilitation_pct=rms_pct,
881
- aeo_enrollment_pct=aeo_pct,
882
- pga_single_window=pga_sw,
883
- deferred_duty=deferred,
884
- auto_ooc=auto_ooc,
885
- officers_sea=officers_sea,
886
- officers_air=officers_air,
887
- officers_land=officers_land,
888
  )
889
- with st.spinner("Simulating Meridia port activity..."):
890
- results = run_simulation(params)
891
 
892
- # Compute summary stats
893
  arts = [r.total_hours for r in results]
894
- green_results = [r for r in results if r.seg_assessment == 0 and r.seg_filing == 0]
895
- aeo_results = [r for r in results if r.aeo_status != "None"]
896
- machine_results = [r for r in results if r.machine_release]
897
-
898
- params["avg_art"] = np.mean(arts) if arts else 0
899
- params["green_pct"] = len(green_results) / len(results) * 100 if results else 0
900
- params["aeo_pct"] = len(aeo_results) / len(results) * 100 if results else 0
901
- params["machine_pct"] = len(machine_results)/ len(results) * 100 if results else 0
902
- params["target48_pct"] = len([a for a in arts if a <= 48]) / len(arts) * 100 if arts else 0
903
-
904
- st.session_state.results = results
905
- st.session_state.sim_params = params
 
 
 
 
906
 
907
  results = st.session_state.results
908
  sim_params = st.session_state.sim_params
 
909
 
910
- # ── Main content tabs ───────────────────────────────────────────────────
911
- tab1, tab2, tab3 = st.tabs(["🗺 PORT VIEW", "📊 TRS REPORT", "📋 AUDIT LEDGER"])
 
 
 
 
 
 
 
912
 
913
- with tab1:
914
- # Phaser.js game
915
- html_src = build_phaser_scene(results, sim_params)
916
- st.components.v1.html(html_src, height=530, scrolling=False)
917
 
918
- if not results:
919
- st.info("Configure policy levers in the sidebar and click **RUN SIMULATION** to activate the port.")
920
- else:
 
921
  st.markdown("---")
922
- col1, col2, col3, col4 = st.columns(4)
923
- col1.metric("Avg Release Time", f"{sim_params['avg_art']:.1f}h",
924
- f"{48 - sim_params['avg_art']:.1f}h vs 48h target")
925
- col2.metric("Green Channel", f"{sim_params['green_pct']:.0f}%", "Facilitated")
926
- col3.metric("Machine Release", f"{sim_params['machine_pct']:.0f}%","Auto-OOC")
927
- col4.metric("Target ≤48h", f"{sim_params['target48_pct']:.0f}%","Achieved")
 
 
928
 
929
  with tab2:
930
  if not results:
931
- st.info("Run the simulation first to generate the TRS report.")
932
  else:
933
- fig = build_trs_chart(results)
934
- st.plotly_chart(fig, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
935
 
936
- # Insights
937
  art = sim_params["avg_art"]
938
- st.markdown("### 🔍 POLICY INSIGHTS")
939
- col_a, col_b = st.columns(2)
940
- with col_a:
941
- if art < 3:
942
- st.success("🏆 **Jaigaon LCS Efficiency Achieved!** ART < 3h — world-class performance.")
943
- elif art < 24:
944
- st.success(f" ART {art:.1f}h — Air terminal target achieved.")
945
- elif art < 48:
946
- st.warning(f"⚠ ART {art:.1f}h — Below 48h target but can improve.")
 
 
947
  else:
948
- st.error(f"🚨 ART {art:.1f}h Exceeds NTFAP 48h target. Enable Advance Filing + Facilitation.")
949
-
950
- with col_b:
951
- if sim_params.get("aeo_pct", 0) == 0:
952
- st.warning("PGA without Single Window detected" if not pga_sw else "")
953
- pga_on = advance_pct > 60 and rms_pct > 60
954
- if pga_on:
955
- st.success("✅ Advance Filing + Facilitation combo active — optimal path.")
956
- else:
957
- st.info("💡 Tip: Set Advance Filing + RMS Facilitation both above 60% to reach Jaigaon-level ART.")
958
 
959
  with tab3:
960
  if not results:
961
- st.info("Run the simulation first to populate the audit ledger.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
962
  else:
963
  df = pd.DataFrame([{
964
- "Shipment_ID": r.shipment_id,
965
- "Port_Type": r.port_type,
966
- "Filing_Type": r.filing_type,
967
- "PGA_Involved": r.pga_involved,
968
- "AEO_Status": r.aeo_status,
969
- "Machine_Release":r.machine_release,
970
- "Filing_h": round(r.seg_filing, 2),
971
- "Assessment_h": round(r.seg_assessment, 2),
972
- "Payment_h": round(r.seg_payment, 2),
973
- "Logistics_h": round(r.seg_logistics, 2),
974
- "Total_Hours": round(r.total_hours, 2),
 
 
 
 
975
  } for r in results])
976
 
977
- st.dataframe(df, use_container_width=True, height=400)
978
-
979
- csv = df.to_csv(index=False).encode("utf-8")
980
  st.download_button(
981
- "⬇ Download Meridia_TRS_Ledger.csv",
982
- data=csv,
983
- file_name="Meridia_TRS_Ledger.csv",
984
- mime="text/csv",
985
- use_container_width=True,
986
  )
 
 
 
 
 
 
 
 
 
 
 
987
 
988
 
989
  if __name__ == "__main__":
 
4
  import pandas as pd
5
  import plotly.graph_objects as go
6
  import json
7
+ import requests
8
+ import time
9
  from dataclasses import dataclass, field
10
  from typing import List, Optional
 
11
 
12
  st.set_page_config(
13
+ page_title="Meridia TRS Simulator WCO Aligned",
14
+ page_icon="🌐",
15
  layout="wide",
16
  initial_sidebar_state="expanded"
17
  )
18
 
19
+ # ══════════════════════════════════════════════════════════════════════════════
20
+ # WCO COLOUR THEME (WCO brand: dark navy #003366, accent blue #0066CC,
21
+ # gold/amber #F5A623, white #FFFFFF, light grey #F0F4F8)
22
+ # ══════════════════════════════════════════════════════════════════════════════
23
  st.markdown("""
24
  <style>
25
+ @import url('https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@300;400;600;700&family=Source+Code+Pro:wght@400;600&display=swap');
26
 
27
  :root {
28
+ --wco-navy: #003366;
29
+ --wco-blue: #0066CC;
30
+ --wco-lblue: #3399FF;
31
+ --wco-gold: #F5A623;
32
+ --wco-white: #FFFFFF;
33
+ --wco-offwhite:#F0F4F8;
34
+ --wco-light: #E8EFF7;
35
+ --wco-muted: #6B8BAE;
36
+ --wco-border: #C5D5E8;
37
+ --wco-dark: #001833;
38
+ --wco-green: #1A8A4A;
39
+ --wco-red: #CC2200;
40
+ --wco-amber: #D4750A;
41
+ --mono: 'Source Code Pro', monospace;
42
+ --sans: 'Source Sans 3', sans-serif;
43
  }
44
 
45
  html, body, [class*="css"] {
46
+ font-family: var(--sans) !important;
47
+ background-color: var(--wco-offwhite) !important;
48
+ color: var(--wco-navy) !important;
49
  }
50
+ .stApp { background: var(--wco-offwhite) !important; }
 
51
 
52
  /* Sidebar */
53
  [data-testid="stSidebar"] {
54
+ background: var(--wco-navy) !important;
55
+ border-right: 3px solid var(--wco-blue) !important;
56
  }
57
+ [data-testid="stSidebar"] * { color: var(--wco-white) !important; }
58
+ [data-testid="stSidebar"] h1,
59
+ [data-testid="stSidebar"] h2,
60
+ [data-testid="stSidebar"] h3 { color: var(--wco-gold) !important; }
61
+ [data-testid="stSidebar"] .stSlider > label { color: #C5D5E8 !important; }
62
+ [data-testid="stSidebar"] [data-testid="stSlider"] > div > div > div > div {
63
+ background: var(--wco-gold) !important; }
64
+ [data-testid="stSidebar"] .stToggle label { color: #C5D5E8 !important; }
65
 
66
  /* Metrics */
67
  [data-testid="stMetric"] {
68
+ background: var(--wco-white) !important;
69
+ border: 1px solid var(--wco-border) !important;
70
+ border-top: 3px solid var(--wco-blue) !important;
71
+ border-radius: 6px !important;
72
+ padding: 14px !important;
73
  }
74
+ [data-testid="stMetricValue"] {
75
+ color: var(--wco-navy) !important;
76
+ font-family: var(--mono) !important;
77
+ font-size: 1.6rem !important;
78
+ font-weight: 600 !important;
79
+ }
80
+ [data-testid="stMetricLabel"] {
81
+ color: var(--wco-muted) !important;
82
+ font-size: 0.72rem !important;
83
+ font-weight: 600 !important;
84
+ letter-spacing: 0.08em !important;
85
+ text-transform: uppercase !important;
86
  }
87
 
88
  /* Buttons */
89
+ .stButton>button {
90
+ background: var(--wco-blue) !important;
91
+ border: none !important;
92
+ color: white !important;
93
+ font-family: var(--sans) !important;
94
+ font-weight: 600 !important;
95
+ border-radius: 4px !important;
96
+ letter-spacing: 0.04em !important;
97
+ transition: background 0.2s !important;
 
 
 
 
98
  }
99
+ .stButton>button:hover { background: var(--wco-navy) !important; }
100
 
101
+ /* Headings */
102
+ h1,h2,h3 { font-family: var(--sans) !important; }
103
+ h1 { color: var(--wco-navy) !important; font-weight: 700 !important; }
104
+ h2 { color: var(--wco-blue) !important; font-weight: 600 !important; }
105
+ h3 { color: var(--wco-navy) !important; font-weight: 600 !important; }
106
 
107
  /* Tabs */
108
  [data-testid="stTabs"] button {
109
+ font-family: var(--sans) !important;
110
+ font-weight: 600 !important;
111
+ color: var(--wco-muted) !important;
112
+ border-radius: 0 !important;
113
  }
114
  [data-testid="stTabs"] button[aria-selected="true"] {
115
+ color: var(--wco-blue) !important;
116
+ border-bottom: 3px solid var(--wco-blue) !important;
117
  }
118
 
119
+ /* Expander */
120
+ [data-testid="stExpander"] {
121
+ border: 1px solid var(--wco-border) !important;
122
+ border-radius: 6px !important;
123
+ background: var(--wco-white) !important;
124
+ }
125
+
126
+ /* Info/success/warning boxes */
127
+ .stAlert { border-radius: 6px !important; }
128
 
129
  /* Scrollbar */
130
+ ::-webkit-scrollbar { width: 5px; }
131
+ ::-webkit-scrollbar-thumb { background: var(--wco-border); border-radius: 3px; }
132
+
133
+ /* WCO card style */
134
+ .wco-card {
135
+ background: white;
136
+ border: 1px solid var(--wco-border);
137
+ border-left: 4px solid var(--wco-blue);
138
+ border-radius: 6px;
139
+ padding: 16px 20px;
140
+ margin-bottom: 12px;
 
141
  }
142
+ .wco-tag {
143
+ display: inline-block;
144
+ background: var(--wco-light);
145
+ color: var(--wco-blue);
146
+ font-size: 11px;
147
+ font-weight: 600;
148
+ padding: 2px 10px;
149
+ border-radius: 12px;
150
+ letter-spacing: 0.06em;
151
+ margin-right: 4px;
152
+ }
153
+ .wco-badge-green { background:#E8F5EE; color:#1A8A4A; border:1px solid #1A8A4A; }
154
+ .wco-badge-yellow { background:#FEF6E8; color:#D4750A; border:1px solid #D4750A; }
155
+ .wco-badge-red { background:#FDECEA; color:#CC2200; border:1px solid #CC2200; }
156
  </style>
157
  """, unsafe_allow_html=True)
158
 
159
 
160
  # ══════════════════════════════════════════════════════════════════════════════
161
+ # COUNTRY BENCHMARK DATABASE (WCO Member data + WTO TFA commitments)
162
+ # ══════════════════════════════════════════════════════════════════════════════
163
+ COUNTRY_PRESETS = {
164
+ "Custom / Generic": {
165
+ "ports": {"Sea": "Seaport", "Air": "Airport", "Land": "Land Border"},
166
+ "volumes": {"Sea": 50, "Air": 10, "Land": 25},
167
+ "baseline_art": {"Sea": 48, "Air": 24, "Land": 36},
168
+ "target_sea": 48, "target_air": 24, "target_land": 36,
169
+ "wto_tfa_cat": "A", "region": "Global",
170
+ "existing_sw": False, "existing_aeo": False,
171
+ },
172
+ "India": {
173
+ "ports": {"Sea": "JNPT Mumbai", "Air": "Delhi IGI Airport", "Land": "Attari/Petrapole ICP"},
174
+ "volumes": {"Sea": 60, "Air": 15, "Land": 30},
175
+ "baseline_art": {"Sea": 85, "Air": 44, "Land": 120},
176
+ "target_sea": 48, "target_air": 24, "target_land": 48,
177
+ "wto_tfa_cat": "A", "region": "Asia-Pacific",
178
+ "existing_sw": True, "existing_aeo": True,
179
+ },
180
+ "Kenya": {
181
+ "ports": {"Sea": "Port of Mombasa", "Air": "JKIA Nairobi", "Land": "Malaba Border"},
182
+ "volumes": {"Sea": 40, "Air": 8, "Land": 35},
183
+ "baseline_art": {"Sea": 96, "Air": 48, "Land": 168},
184
+ "target_sea": 72, "target_air": 48, "target_land": 72,
185
+ "wto_tfa_cat": "B", "region": "Eastern/Southern Africa",
186
+ "existing_sw": True, "existing_aeo": False,
187
+ },
188
+ "Ghana": {
189
+ "ports": {"Sea": "Tema Port", "Air": "Kotoka Int'l Airport", "Land": "Aflao Border"},
190
+ "volumes": {"Sea": 35, "Air": 6, "Land": 20},
191
+ "baseline_art": {"Sea": 120, "Air": 72, "Land": 144},
192
+ "target_sea": 96, "target_air": 48, "target_land": 96,
193
+ "wto_tfa_cat": "C", "region": "West Africa",
194
+ "existing_sw": False, "existing_aeo": False,
195
+ },
196
+ "Vietnam": {
197
+ "ports": {"Sea": "Cat Lai Port HCMC", "Air": "Tan Son Nhat Airport", "Land": "Lao Cai Border"},
198
+ "volumes": {"Sea": 70, "Air": 20, "Land": 40},
199
+ "baseline_art": {"Sea": 60, "Air": 30, "Land": 72},
200
+ "target_sea": 36, "target_air": 18, "target_land": 48,
201
+ "wto_tfa_cat": "A", "region": "Asia-Pacific",
202
+ "existing_sw": True, "existing_aeo": True,
203
+ },
204
+ "Morocco": {
205
+ "ports": {"Sea": "Port of Casablanca", "Air": "Mohammed V Airport", "Land": "Bab Sebta"},
206
+ "volumes": {"Sea": 45, "Air": 10, "Land": 25},
207
+ "baseline_art": {"Sea": 72, "Air": 36, "Land": 96},
208
+ "target_sea": 48, "target_air": 24, "target_land": 48,
209
+ "wto_tfa_cat": "A", "region": "North Africa/Middle East",
210
+ "existing_sw": True, "existing_aeo": True,
211
+ },
212
+ "Colombia": {
213
+ "ports": {"Sea": "Port of Cartagena", "Air": "El Dorado Bogotá", "Land": "Ipiales Border"},
214
+ "volumes": {"Sea": 40, "Air": 12, "Land": 20},
215
+ "baseline_art": {"Sea": 84, "Air": 36, "Land": 120},
216
+ "target_sea": 48, "target_air": 24, "target_land": 72,
217
+ "wto_tfa_cat": "A", "region": "Latin America/Caribbean",
218
+ "existing_sw": True, "existing_aeo": True,
219
+ },
220
+ }
221
+
222
+ WCO_REGIONS = ["Global","Asia-Pacific","Eastern/Southern Africa","West Africa",
223
+ "North Africa/Middle East","Latin America/Caribbean","Europe","Gulf Region"]
224
+
225
+ # ══════════════════════════════════════════════════════════════════════════════
226
+ # OPENROUTER LLM — free tier fallback chain
227
  # ══════════════════════════════════════════════════════════════════════════════
228
+ MODELS_TO_TRY = [
229
+ ("qwen/qwen3-coder:free", "Qwen3 Coder"),
230
+ ("qwen/qwen3-next-80b-a3b-instruct:free", "Qwen3 Next 80B"),
231
+ ("openai/gpt-oss-120b:free", "GPT OSS 120B"),
232
+ ("google/gemma-4-26b-a4b-it:free", "Gemma 4 26B"),
233
+ ("nousresearch/hermes-3-llama-3.1-405b:free", "Hermes 3 Llama 405B"),
234
+ ("deepseek/deepseek-r1:free", "DeepSeek R1"),
235
+ ("google/gemini-2.0-flash-exp:free", "Gemini 2.0 Flash"),
236
+ ("meta-llama/llama-3.1-8b-instruct:free", "Llama 3.1 8B"),
237
+ ("mistralai/mistral-7b-instruct:free", "Mistral 7B"),
238
+ ]
239
+
240
+ def call_llm(prompt: str, api_key: str, system: str = "") -> tuple[str, str]:
241
+ """Try each free model in order; return (text, model_name_used)."""
242
+ headers = {
243
+ "Authorization": f"Bearer {api_key}",
244
+ "Content-Type": "application/json",
245
+ "HTTP-Referer": "https://meridia-trs.hf.space",
246
+ "X-Title": "Meridia WCO TRS Simulator",
247
+ }
248
+ messages = []
249
+ if system:
250
+ messages.append({"role": "system", "content": system})
251
+ messages.append({"role": "user", "content": prompt})
252
+
253
+ for model_id, model_name in MODELS_TO_TRY:
254
+ try:
255
+ resp = requests.post(
256
+ "https://openrouter.ai/api/v1/chat/completions",
257
+ headers=headers,
258
+ json={"model": model_id, "messages": messages, "max_tokens": 1200},
259
+ timeout=30,
260
+ )
261
+ if resp.status_code == 200:
262
+ data = resp.json()
263
+ text = data["choices"][0]["message"]["content"].strip()
264
+ if text and len(text) > 50:
265
+ return text, model_name
266
+ except Exception:
267
+ continue
268
+ return "⚠ Could not reach any free LLM model. Check your API key or try again.", "None"
269
+
270
 
271
+ # ══════════════════════════════════════════════════════════════════════════════
272
+ # DATA MODEL — WCO TRS Guide v4 2025 §2.1.4 timestamps
273
+ # ══════════════════════════════════════════════════════════════════════════════
274
  @dataclass
275
  class BoE:
276
  shipment_id: str
277
+ port_type: str
278
+ filing_type: str
279
  pga_involved: bool
280
+ aeo_status: str
281
+ channel: str
282
+ machine_release: bool = False
283
+ t_arrival: float = 0.0
284
+ t_lodged: float = 0.0
285
+ t_assessed: float = 0.0
286
+ t_payment: float = 0.0
287
+ t_ooc: float = 0.0
288
 
289
  @property
290
+ def total_hours(self): return max(self.t_ooc - self.t_arrival, 0)
291
  @property
292
+ def seg_prearr(self): return max(self.t_lodged - self.t_arrival, 0)
293
  @property
294
+ def seg_customs(self): return max(self.t_assessed - self.t_lodged, 0)
295
  @property
296
+ def seg_oga_duty(self): return max(self.t_payment - self.t_assessed, 0)
297
  @property
298
+ def seg_logistics(self): return max(self.t_ooc - self.t_payment, 0)
299
 
300
 
301
  # ══════════════════════════════════════════════════════════════════════════════
302
+ # SIMULATION ENGINE
303
  # ══════════════════════════════════════════════════════════════════════════════
304
+ def run_simulation(params: dict, country_cfg: dict) -> List[BoE]:
 
 
305
  results: List[BoE] = []
306
  rng = np.random.default_rng(42)
 
307
  env = simpy.Environment()
308
 
309
+ vols = country_cfg.get("volumes", {"Sea":50,"Air":10,"Land":25})
310
+ base = country_cfg.get("baseline_art", {"Sea":48,"Air":24,"Land":36})
311
+
312
  officers_sea = simpy.Resource(env, capacity=params["officers_sea"])
313
  officers_air = simpy.Resource(env, capacity=params["officers_air"])
314
  officers_land = simpy.Resource(env, capacity=params["officers_land"])
315
 
316
+ # Speed multiplier derived from baseline ART (normalised to 48h sea)
317
  PORT_CONFIG = {
318
+ "Sea": {"volume": vols["Sea"], "resource": officers_sea,
319
+ "speed": base["Sea"] / 48.0},
320
+ "Air": {"volume": vols["Air"], "resource": officers_air,
321
+ "speed": base["Air"] / 24.0},
322
+ "Land": {"volume": vols["Land"], "resource": officers_land,
323
+ "speed": base["Land"] / 48.0},
324
  }
 
325
  counter = [0]
326
 
327
  def process_boe(env, port_type, cfg):
328
  counter[0] += 1
329
+ sid = f"{port_type[0]}-{counter[0]:04d}"
330
 
 
331
  is_advance = rng.random() < (params["advance_filing_pct"] / 100)
332
  is_aeo = rng.random() < (params["aeo_enrollment_pct"] / 100)
333
+ is_pga = rng.random() < params.get("pga_probability", 0.35)
334
+ aeo_tier = rng.choice(["T1","T2","T3"]) if is_aeo else "None"
335
+
336
+ rms_thr = params["rms_facilitation_pct"] / 100
337
+ roll = rng.random()
338
+ channel = "Green" if roll < rms_thr else ("Yellow" if roll < rms_thr+0.25 else "Red")
 
 
 
 
 
339
 
340
+ boe = BoE(shipment_id=sid, port_type=port_type,
341
+ filing_type="Advance" if is_advance else "Normal",
342
+ pga_involved=is_pga, aeo_status=aeo_tier, channel=channel)
343
  boe.t_arrival = env.now
344
 
345
+ # Seg A: Arrival → Lodgement
346
+ spd = cfg["speed"]
347
  if is_advance:
348
+ yield env.timeout(rng.gamma(1.2, 0.8) * spd)
349
  else:
350
+ yield env.timeout(rng.gamma(2, 12) * spd)
351
+ boe.t_lodged = env.now
 
352
 
353
+ # Seg B: Lodgement → Assessment
354
+ if channel == "Green":
355
+ yield env.timeout(rng.gamma(1, 0.3) * spd)
356
  else:
357
  with cfg["resource"].request() as req:
358
  yield req
359
+ if channel == "Yellow":
360
+ d = rng.gamma(2, 3) * spd * (0.5 if is_aeo else 1.0)
361
  else:
362
+ d = rng.gamma(3, 8) * spd * (0.6 if is_aeo else 1.0)
363
+ yield env.timeout(d)
 
 
 
364
  boe.t_assessed = env.now
365
 
366
+ # Seg C: Assessment Duty + OGA
367
+ duty = 0 if (is_aeo and params["deferred_duty"]) else rng.gamma(2, 2) * spd
368
+ if is_pga:
369
+ oga = rng.gamma(2, 2 if params["pga_single_window"] else 8) * spd
370
  else:
371
+ oga = 0
372
+ yield env.timeout(max(duty, oga))
 
 
 
 
 
373
  boe.t_payment = env.now
374
 
375
+ # Seg D: Payment → OOC
376
+ if channel == "Green" and params["auto_ooc"]:
 
377
  boe.machine_release = True
378
+ yield env.timeout(0)
379
+ elif is_aeo:
380
+ yield env.timeout(rng.gamma(1.5, 0.8) * spd)
381
  else:
382
+ yield env.timeout(rng.gamma(2, 1.5) * spd)
 
383
  boe.t_ooc = env.now
 
384
  results.append(boe)
385
 
386
+ def port_gen(env, pt, cfg):
 
387
  for _ in range(cfg["volume"]):
388
+ env.process(process_boe(env, pt, cfg))
389
+ yield env.timeout(rng.exponential(1.5))
 
 
 
 
390
 
391
+ for pt, cfg in PORT_CONFIG.items():
392
+ env.process(port_gen(env, pt, cfg))
393
+ env.run(until=800)
394
  return results
395
 
396
 
397
  # ══════════════════════════════════════════════════════════════════════════════
398
+ # PHASER.JS MAP (WCO colour theme: navy + white + blue)
399
  # ══════════════════════════════════════════════════════════════════════════════
400
+ def build_phaser_scene(results, sp, country_cfg):
401
+ art = round(sp.get("avg_art",0),1)
402
+ med = round(sp.get("median_art",0),1)
403
+ fac = round(sp.get("green_pct",0),1)
404
+ mach = round(sp.get("machine_pct",0),1)
405
+ t48 = round(sp.get("target48_pct",0),1)
406
+ aeo = round(sp.get("aeo_pct",0),1)
407
+
408
+ ports_cfg = country_cfg.get("ports", {"Sea":"Seaport","Air":"Airport","Land":"Land Border"})
409
+ sea_label = ports_cfg.get("Sea","Seaport")[:16]
410
+ air_label = ports_cfg.get("Air","Airport")[:16]
411
+ land_label = ports_cfg.get("Land","Land Border")[:16]
412
+
413
+ sea_n = len([r for r in results if r.port_type=="Sea"])
414
+ air_n = len([r for r in results if r.port_type=="Air"])
415
+ land_n = len([r for r in results if r.port_type=="Land"])
416
+ g_n = len([r for r in results if r.channel=="Green"])
417
+ y_n = len([r for r in results if r.channel=="Yellow"])
418
+ r_n = len([r for r in results if r.channel=="Red"])
419
+
420
+ dots = []
421
+ for r in results[:80]:
422
+ s = "fast" if r.total_hours<3 else ("ok" if r.total_hours<24 else ("slow" if r.total_hours<48 else "late"))
423
+ dots.append({"id":r.shipment_id,"port":r.port_type,"hours":round(r.total_hours,1),
424
+ "status":s,"ch":r.channel,"machine":r.machine_release})
425
+
426
+ dots_j = json.dumps(dots)
427
+ has_data = "true" if results else "false"
428
+ art_col = "#1A8A4A" if art<24 else ("#D4750A" if art<48 else "#CC2200")
429
+ t48_col = "#1A8A4A" if t48>80 else ("#D4750A" if t48>60 else "#CC2200")
430
+
431
+ return f"""<!DOCTYPE html><html><head><meta charset="utf-8">
 
432
  <style>
433
+ *{{margin:0;padding:0;box-sizing:border-box;}}
434
+ html,body{{width:100%;height:460px;overflow:hidden;background:#EDF2F7;
435
+ font-family:'Source Sans 3','Segoe UI',sans-serif;}}
436
+ #wrap{{position:relative;width:100%;height:460px;}}
437
+ #phaser-canvas{{position:absolute;top:0;left:0;}}
438
+ #hud-top{{position:absolute;top:0;left:0;right:0;display:flex;
439
+ justify-content:space-between;padding:10px 14px;pointer-events:none;z-index:20;gap:8px;}}
440
+ #hud-bot{{position:absolute;bottom:0;left:0;right:0;display:flex;gap:8px;
441
+ padding:8px 14px;z-index:20;pointer-events:none;align-items:center;flex-wrap:wrap;}}
442
+ .hp{{background:rgba(0,30,70,0.92);border:1px solid rgba(0,102,204,0.4);
443
+ border-radius:5px;padding:7px 12px;}}
444
+ .hl{{font-size:8px;font-weight:700;letter-spacing:.14em;color:#6B8BAE;
445
+ text-transform:uppercase;margin-bottom:2px;}}
446
+ .hv{{font-family:'Source Code Pro',monospace;font-size:17px;line-height:1;font-weight:600;}}
447
+ .hs{{font-size:8px;color:#6B8BAE;margin-top:2px;}}
448
+ .hr{{display:flex;gap:8px;}}
449
+ .pd{{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:4px;}}
450
+ .pd.sea{{background:#0066CC;}}.pd.air{{background:#F5A623;}}.pd.land{{background:#1A8A4A;}}
451
+ .pn{{font-size:9px;font-weight:700;letter-spacing:.06em;color:#C5D5E8;}}
452
+ .pc{{font-size:12px;font-family:'Source Code Pro',monospace;color:#3399FF;margin-left:3px;}}
453
+ .pill{{font-size:8px;font-weight:700;padding:1px 7px;border-radius:8px;margin-right:3px;}}
454
+ .gp{{background:rgba(26,138,74,0.2);color:#4ABA7A;border:1px solid rgba(26,138,74,0.4);}}
455
+ .yp{{background:rgba(212,117,10,0.2);color:#F5A623;border:1px solid rgba(212,117,10,0.4);}}
456
+ .rp{{background:rgba(204,34,0,0.2);color:#FF5533;border:1px solid rgba(204,34,0,0.4);}}
457
+ #idle{{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
458
+ text-align:center;pointer-events:none;z-index:15;}}
459
+ #idle p{{font-size:12px;color:#6B8BAE;letter-spacing:.1em;margin-top:6px;}}
460
+ </style></head>
461
+ <body><div id="wrap">
462
+ <canvas id="phaser-canvas"></canvas>
463
+
464
+ <div id="hud-top">
465
+ <div class="hp">
466
+ <div class="hl">WCO TRS — Avg Release Time</div>
467
+ <div class="hv" style="color:{art_col}">{art}h</div>
468
+ <div class="hs">Median: {med}h &nbsp;|&nbsp; Target: {sp.get('target_sea',48)}h</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  </div>
470
+ <div class="hr">
471
+ <div class="hp">
472
+ <div class="hl">RMS Channels</div>
473
+ <div style="display:flex;gap:3px;margin-top:4px;">
474
+ <span class="pill gp">G {g_n}</span>
475
+ <span class="pill yp">Y {y_n}</span>
476
+ <span class="pill rp">R {r_n}</span>
477
+ </div>
478
+ <div class="hs">{fac}% facilitated</div>
479
  </div>
480
+ <div class="hp">
481
+ <div class="hl">Auto-OOC</div>
482
+ <div class="hv" style="color:{'#1A8A4A' if mach>40 else '#D4750A'}">{mach}%</div>
483
+ <div class="hs">Machine release</div>
484
  </div>
485
+ <div class="hp">
486
+ <div class="hl">WTO TFA {sp.get('target_sea',48)}h</div>
487
+ <div class="hv" style="color:{t48_col}">{t48}%</div>
488
+ <div class="hs">Art.7.6.1 achievement</div>
489
+ </div>
490
+ <div class="hp">
491
+ <div class="hl">AEO Enrolled</div>
492
+ <div class="hv" style="color:#3399FF">{aeo}%</div>
493
+ <div class="hs">WCO SAFE Framework</div>
494
  </div>
 
 
 
 
 
 
495
  </div>
496
  </div>
497
 
498
+ <div id="hud-bot">
499
+ <div class="hp" style="display:flex;gap:14px;align-items:center;">
500
+ <span><span class="pd sea"></span><span class="pn">{sea_label}</span><span class="pc">{sea_n}</span></span>
501
+ <span><span class="pd air"></span><span class="pn">{air_label}</span><span class="pc">{air_n}</span></span>
502
+ <span><span class="pd land"></span><span class="pn">{land_label}</span><span class="pc">{land_n}</span></span>
 
 
 
 
 
503
  </div>
504
+ <div class="hp" style="margin-left:auto;display:flex;gap:8px;align-items:center;">
505
+ <span style="font-size:8px;color:#6B8BAE;letter-spacing:.1em;">LEGEND</span>
506
+ <span style="font-size:9px;color:#1A8A4A;font-family:'Source Code Pro';"> &lt;3h</span>
507
+ <span style="font-size:9px;color:#0066CC;font-family:'Source Code Pro';">● &lt;24h</span>
508
+ <span style="font-size:9px;color:#D4750A;font-family:'Source Code Pro';">● &lt;48h</span>
509
+ <span style="font-size:9px;color:#CC2200;font-family:'Source Code Pro';">● &gt;48h</span>
 
510
  </div>
511
+ <div class="hp"><span id="sim-clock" style="font-family:'Source Code Pro',monospace;font-size:10px;color:#6B8BAE;letter-spacing:.08em;">WCO TRS SIMULATOR</span></div>
512
  </div>
513
 
514
+ {'<div id="idle"><div style="font-size:40px;">🌐</div><p>CONFIGURE LEVERS &amp; RUN SIMULATION</p><p style="font-size:10px;margin-top:2px;color:#003366;">WCO TIME RELEASE STUDY SIMULATOR READY</p></div>' if not results else ''}
515
+
516
  <script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>
517
  <script>
518
+ const DOTS={dots_j}, HAS_DATA={has_data};
519
+ const SL="{sea_label}", AL="{air_label}", LL="{land_label}";
520
+
521
+ class Scene extends Phaser.Scene {{
522
+ constructor(){{ super({{key:'S'}}); }}
523
+ create(){{
524
+ const W=this.scale.width,H=this.scale.height;
525
+
526
+ // WCO-themed background: light blue-grey
527
+ this.add.rectangle(W/2,H/2,W,H,0xEDF2F7);
528
+
529
+ // Grid lines subtle
530
+ const grid=this.add.graphics();
531
+ grid.lineStyle(0.5,0xC5D5E8,0.3);
532
+ for(let x=0;x<W;x+=40){{grid.moveTo(x,0);grid.lineTo(x,H);}}
533
+ for(let y=0;y<H;y+=40){{grid.moveTo(0,y);grid.lineTo(W,y);}}
534
+ grid.strokePath();
535
+
536
+ // Stars (subtle dots for sky)
537
+ const sg=this.add.graphics();
538
+ for(let i=0;i<30;i++){{
539
+ sg.fillStyle(0xA0B8D0,0.4);
540
+ sg.fillCircle(Phaser.Math.Between(0,W),Phaser.Math.Between(0,H*0.3),0.8);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
  }}
542
 
543
+ const IW=52,IH=26;
544
+ const iso=(c,r)=>({{x:W/2+(c-r)*IW/2,y:150+(c+r)*IH/2}});
545
+
546
+ const tile=(g,c,r,fill,alpha=1,elev=0)=>{{
547
+ const p=iso(c,r);
548
+ const top=[{{x:p.x,y:p.y-elev}},{{x:p.x+IW/2,y:p.y+IH/2-elev}},
549
+ {{x:p.x,y:p.y+IH-elev}},{{x:p.x-IW/2,y:p.y+IH/2-elev}}];
550
+ g.fillStyle(fill,alpha);g.fillPoints(top,true);
551
+ if(elev>0){{
552
+ const ci=Phaser.Display.Color.IntegerToColor(fill);
553
+ const dk=(rv,gv,bv)=>Phaser.Display.Color.GetColor(Math.max(0,rv),Math.max(0,gv),Math.max(0,bv));
554
+ g.fillStyle(dk(ci.red-40,ci.green-40,ci.blue-40),alpha);
555
+ g.fillPoints([{{x:p.x-IW/2,y:p.y+IH/2-elev}},{{x:p.x,y:p.y+IH-elev}},
556
+ {{x:p.x,y:p.y+IH}},{{x:p.x-IW/2,y:p.y+IH/2}}],true);
557
+ g.fillStyle(dk(ci.red-20,ci.green-20,ci.blue-20),alpha);
558
+ g.fillPoints([{{x:p.x,y:p.y+IH-elev}},{{x:p.x+IW/2,y:p.y+IH/2-elev}},
559
+ {{x:p.x+IW/2,y:p.y+IH/2}},{{x:p.x,y:p.y+IH}}],true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  }}
561
  }};
562
 
563
+ const ter=this.add.graphics();
564
+ // Water — WCO blue
565
+ for(let r=0;r<9;r++)for(let c=-4;c<2;c++) tile(ter,c,r,0x1A5FA0,0.7);
566
+ // Lighter water ripples
567
+ for(let r=1;r<8;r+=2)for(let c=-3;c<1;c++) tile(ter,c,r,0x3399CC,0.25);
568
+ // Land light green-grey
569
+ for(let r=0;r<9;r++)for(let c=1;c<14;c++) tile(ter,c,r,(c+r)%2===0?0xD4E0EC:0xC8D8E8,1);
570
+ // Roads
571
+ for(let c=1;c<14;c++) tile(ter,c,4,0xB8C8D8,1);
572
+ for(let r=0;r<9;r++) tile(ter,5,r,0xB8C8D8,1);
573
+
574
+ // Buildings WCO navy
575
+ const bl=this.add.graphics();
576
+ const blk=(c,r,w,d,h,col)=>{{for(let dc=0;dc<w;dc++)for(let dr=0;dr<d;dr++)tile(bl,c+dc,r+dr,col,1,h);}};
577
+ blk(2,1,2,2,22,0x003366); // Sea port customs
578
+ blk(7,1,3,2,18,0x003870); // Air terminal
579
+ blk(2,5,2,2,16,0x004433); // Land border
580
+
581
+ // WCO flag colours on buildings
582
+ const fl=this.add.graphics();
583
+ const fp1=iso(3,1); fl.fillStyle(0xF5A623,1); fl.fillRect(fp1.x-2,fp1.y-30,4,14);
584
+ const fp2=iso(8.5,1); fl.fillStyle(0xF5A623,1); fl.fillRect(fp2.x-2,fp2.y-26,4,12);
585
+ const fp3=iso(3,5); fl.fillStyle(0xF5A623,1); fl.fillRect(fp3.x-2,fp3.y-24,4,10);
586
+
587
+ // Port labels — navy text
588
+ const ls={{fontFamily:'Source Sans 3,sans-serif',fontSize:'10px',fontStyle:'bold',letterSpacing:1}};
589
+ const p1=iso(2.5,0); this.add.text(p1.x,p1.y-46,SL,{{...ls,color:'#003366'}}).setOrigin(0.5);
590
+ const p2=iso(8.5,0); this.add.text(p2.x,p2.y-34,AL,{{...ls,color:'#003366'}}).setOrigin(0.5);
591
+ const p3=iso(3,5.2); this.add.text(p3.x,p3.y-30,LL,{{...ls,color:'#003366'}}).setOrigin(0.5);
592
+
593
+ // Ship (WCO blue)
594
+ const sh=this.add.graphics(),shp=iso(-1.5,3);
595
+ sh.fillStyle(0x0066CC,1);sh.fillRect(shp.x-24,shp.y-5,48,12);
596
+ sh.fillStyle(0x003366,1);sh.fillRect(shp.x-8,shp.y-13,18,8);
597
+ sh.fillStyle(0xFFFFFF,1);sh.fillRect(shp.x-6,shp.y-11,3,6);
598
+ this.tweens.add({{targets:sh,y:'-=4',duration:2200,yoyo:true,repeat:-1,ease:'Sine.easeInOut'}});
599
+
600
+ // Cargo dots
601
+ const COL={{fast:0x1A8A4A,ok:0x0066CC,slow:0xD4750A,late:0xCC2200}};
602
+ const ORI={{Sea:{{c:0,r:3.5}},Air:{{c:9,r:3}},Land:{{c:3.5,r:6.5}}}};
603
+
604
+ if(HAS_DATA){{
605
+ DOTS.forEach((d,i)=>{{
606
+ const o=ORI[d.port];
607
+ const c=o.c+(Math.random()-0.5)*2.5,r=o.r+(Math.random()-0.5)*2;
608
+ const pos=iso(c,r),col=COL[d.status];
609
+ const g=this.add.graphics();
610
+ g.fillStyle(col,0.9);g.fillCircle(pos.x,pos.y,5);
611
+ g.lineStyle(1,col,0.3);g.strokeCircle(pos.x,pos.y,9);
612
+ if(d.machine){{g.lineStyle(1.5,0xFFFFFF,0.7);g.strokeCircle(pos.x,pos.y,13);}}
613
+ this.tweens.add({{targets:g,alpha:{{from:0.5,to:1}},duration:800+i*35,yoyo:true,repeat:-1}});
614
+ this.tweens.add({{targets:g,y:'-=3',duration:1200+i*55,yoyo:true,repeat:-1}});
615
+ const hz=this.add.circle(pos.x,pos.y,14,0xffffff,0).setInteractive({{useHandCursor:true}});
616
+ const tt=this.add.text(pos.x+16,pos.y-16,
617
+ `${{d.id}}\\n${{d.port}} | ${{d.hours}}h\\nCh:${{d.ch}} | ${{d.status.toUpperCase()}}${{d.machine?' ⚡':''}}`,
618
+ {{fontFamily:'Source Code Pro,monospace',fontSize:'9px',color:'#FFFFFF',
619
+ backgroundColor:'#003366EE',padding:{{x:6,y:4}},lineSpacing:3}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
620
  ).setVisible(false).setDepth(100);
621
+ hz.on('pointerover',()=>tt.setVisible(true));
622
+ hz.on('pointerout', ()=>tt.setVisible(false));
 
623
  }});
624
  }} else {{
625
+ for(let i=0;i<8;i++){{
626
+ const pos=iso(Math.random()*10,Math.random()*7);
627
+ const g=this.add.graphics();
628
+ g.fillStyle(0xC5D5E8,0.5);g.fillCircle(pos.x,pos.y,4);
629
+ this.tweens.add({{targets:g,alpha:{{from:0.1,to:0.5}},duration:1500+i*200,yoyo:true,repeat:-1}});
 
 
 
 
 
 
 
630
  }}
631
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  }}
633
+ update(){{
634
+ const el=document.getElementById('sim-clock');
635
+ if(el) el.textContent=new Date().toLocaleTimeString('en-GB')+' | WCO TRS LIVE';
 
 
 
 
 
636
  }}
637
  }}
638
 
639
+ const canvas=document.getElementById('phaser-canvas');
640
  new Phaser.Game({{
641
+ type:Phaser.WEBGL, canvas:canvas,
642
+ width: canvas.parentElement.offsetWidth||900,
643
+ height:460,
644
+ transparent:true,
645
+ scale:{{mode:Phaser.Scale.RESIZE,autoCenter:Phaser.Scale.CENTER_BOTH}},
646
+ scene:[Scene],
 
 
 
 
 
 
647
  }});
648
  </script>
649
+ </div></body></html>"""
 
 
650
 
651
 
652
  # ══════════════════════════════════════════════════════════════════════════════
653
+ # TRS CHART WCO colours
654
  # ══════════════════════════════════════════════════════════════════════════════
655
+ def build_trs_chart(results, country_cfg, sp):
656
+ ports = ["Sea","Air","Land","ALL PORTS"]
657
+ pm = {"Sea":"Sea","Air":"Air","Land":"Land"}
658
+ data = {p:{"A":[],"B":[],"C":[],"D":[]} for p in ports}
659
 
660
+ for r in results:
661
+ pk = pm[r.port_type]
662
+ for s,v in [("A",r.seg_prearr),("B",r.seg_customs),("C",r.seg_oga_duty),("D",r.seg_logistics)]:
663
+ data[pk][s].append(v); data["ALL PORTS"][s].append(v)
664
 
665
+ avgs = {p:{s:np.mean(v) if v else 0 for s,v in sd.items()} for p,sd in data.items()}
666
 
667
+ port_labels = [
668
+ country_cfg["ports"].get("Sea","Sea"),
669
+ country_cfg["ports"].get("Air","Air"),
670
+ country_cfg["ports"].get("Land","Land"),
671
+ "ALL PORTS"
672
+ ]
 
 
 
 
 
 
673
 
674
  segs = [
675
+ ("A","Seg A — Pre-arrival / Lodgement","#003366"),
676
+ ("B","Seg B — Customs Assessment","#0066CC"),
677
+ ("C","Seg C OGA / Duty Payment","#F5A623"),
678
+ ("D","Seg D — Post-clearance Logistics","#6B8BAE"),
679
  ]
 
680
  fig = go.Figure()
681
+ for sid,sname,col in segs:
682
  fig.add_trace(go.Bar(
683
+ name=sname, x=port_labels,
684
+ y=[avgs[p][sid] for p in ports],
685
+ marker_color=col,
686
+ text=[f"{avgs[p][sid]:.1f}h" for p in ports],
 
687
  textposition="inside",
688
+ textfont=dict(family="Source Code Pro, monospace",size=10,color="white"),
689
  ))
690
 
691
+ t_sea = country_cfg.get("target_sea",48)
692
+ t_air = country_cfg.get("target_air",24)
693
+ fig.add_hline(y=t_sea, line_dash="dash", line_color="#CC2200", line_width=1.5,
694
+ annotation_text=f"{t_sea}h — Sea/Land Target (WTO TFA)",
695
+ annotation_font_color="#CC2200", annotation_font_size=9)
696
+ if t_air != t_sea:
697
+ fig.add_hline(y=t_air, line_dash="dot", line_color="#D4750A", line_width=1,
698
+ annotation_text=f"{t_air}h — Air Target",
699
+ annotation_font_color="#D4750A", annotation_font_size=9)
700
+ fig.add_hline(y=3, line_dash="dashdot", line_color="#1A8A4A", line_width=1,
701
+ annotation_text="3h — Jaigaon LCS Best Practice",
702
+ annotation_font_color="#1A8A4A", annotation_font_size=9)
703
+
704
+ # Baseline bars (ghost)
705
+ base = country_cfg.get("baseline_art",{})
706
+ base_vals = [base.get("Sea",0),base.get("Air",0),base.get("Land",0),
707
+ np.mean(list(base.values()))]
708
+ fig.add_trace(go.Scatter(
709
+ x=port_labels, y=base_vals,
710
+ mode="markers", name="Baseline ART (before reform)",
711
+ marker=dict(symbol="diamond",size=12,color="#CC2200",
712
+ line=dict(color="white",width=1)),
713
+ ))
714
 
715
  fig.update_layout(
716
  barmode="stack",
717
+ plot_bgcolor="white", paper_bgcolor="#F0F4F8",
718
+ font=dict(family="Source Sans 3, sans-serif",color="#003366",size=12),
719
+ title=dict(
720
+ text="WCO TRS — Release Time by Port Mode (Segments A–D)",
721
+ font=dict(size=14,color="#003366",family="Source Sans 3"),x=0.5),
722
+ xaxis=dict(gridcolor="#E8EFF7",linecolor="#C5D5E8",tickfont=dict(size=11)),
723
+ yaxis=dict(gridcolor="#E8EFF7",linecolor="#C5D5E8",
724
+ title=dict(text="Hours — Arrival to Physical Release",
725
+ font=dict(color="#6B8BAE",size=11))),
726
+ legend=dict(orientation="h",y=-0.28,font=dict(size=10),bgcolor="rgba(0,0,0,0)"),
727
+ margin=dict(t=60,b=110,l=60,r=20),height=420,
728
+ )
729
+ return fig
730
+
731
+
732
+ def build_channel_chart(results):
733
+ ports = ["Sea","Air","Land"]
734
+ ch_d = {p:{"Green":0,"Yellow":0,"Red":0} for p in ports}
735
+ for r in results: ch_d[r.port_type][r.channel]+=1
736
+ fig = go.Figure()
737
+ for ch,col in [("Green","#1A8A4A"),("Yellow","#D4750A"),("Red","#CC2200")]:
738
+ fig.add_trace(go.Bar(
739
+ name=ch,x=ports,y=[ch_d[p][ch] for p in ports],
740
+ marker_color=col,marker_opacity=0.85,
741
+ text=[ch_d[p][ch] for p in ports],textposition="inside",
742
+ textfont=dict(family="Source Code Pro",size=11,color="white"),
743
+ ))
744
+ fig.update_layout(
745
+ barmode="stack",plot_bgcolor="white",paper_bgcolor="#F0F4F8",
746
+ font=dict(family="Source Sans 3",color="#003366",size=12),
747
+ title=dict(text="WCO RMS Channel Distribution by Port",
748
+ font=dict(size=13,color="#003366"),x=0.5),
749
+ xaxis=dict(gridcolor="#E8EFF7"),
750
+ yaxis=dict(gridcolor="#E8EFF7",
751
+ title=dict(text="BoE Count",font=dict(color="#6B8BAE",size=11))),
752
+ legend=dict(orientation="h",y=-0.2,font=dict(size=10),bgcolor="rgba(0,0,0,0)"),
753
+ margin=dict(t=50,b=80,l=50,r=20),height=300,
754
  )
755
  return fig
756
 
757
 
758
  # ══════════════════════════════════════════════════════════════════════════════
759
+ # LLM ANALYSIS — builds a rich prompt from simulation results
760
  # ══════════════════════════════════════════════════════════════════════════════
761
+ def build_llm_prompt(results, sp, params, country_cfg):
762
+ ports_cfg = country_cfg.get("ports",{})
763
+ base = country_cfg.get("baseline_art",{})
764
+ t_sea = country_cfg.get("target_sea",48)
765
+
766
+ seg_means = {}
767
+ for pt in ["Sea","Air","Land"]:
768
+ sub = [r for r in results if r.port_type==pt]
769
+ if sub:
770
+ seg_means[pt] = {
771
+ "A": round(np.mean([r.seg_prearr for r in sub]),1),
772
+ "B": round(np.mean([r.seg_customs for r in sub]),1),
773
+ "C": round(np.mean([r.seg_oga_duty for r in sub]),1),
774
+ "D": round(np.mean([r.seg_logistics for r in sub]),1),
775
+ "total": round(np.mean([r.total_hours for r in sub]),1),
776
+ "baseline": base.get(pt,0),
777
+ "port_name": ports_cfg.get(pt,pt),
778
+ }
779
+
780
+ biggest_seg = {}
781
+ for pt,d in seg_means.items():
782
+ segs = {"Pre-arrival (Seg A)":d["A"],"Customs Assessment (Seg B)":d["B"],
783
+ "OGA/Duty (Seg C)":d["C"],"Post-clearance (Seg D)":d["D"]}
784
+ biggest_seg[pt] = max(segs, key=segs.get)
785
+
786
+ policy_used = []
787
+ if params["advance_filing_pct"]>50: policy_used.append(f"Advance Filing ({params['advance_filing_pct']}%)")
788
+ if params["rms_facilitation_pct"]>50: policy_used.append(f"RMS Facilitation ({params['rms_facilitation_pct']}%)")
789
+ if params["aeo_enrollment_pct"]>30: policy_used.append(f"AEO Enrollment ({params['aeo_enrollment_pct']}%)")
790
+ if params["pga_single_window"]: policy_used.append("PGA Single Window")
791
+ if params["deferred_duty"]: policy_used.append("Deferred Duty")
792
+ if params["auto_ooc"]: policy_used.append("Auto Out-of-Charge")
793
+
794
+ prompt = f"""You are a WCO (World Customs Organization) trade facilitation expert and TRS analyst.
795
+
796
+ A Customs administration has run a Time Release Study (TRS) simulation aligned with the WCO TRS Guide v4 2025.
797
+
798
+ COUNTRY / PORT CONFIGURATION:
799
+ - Country: {country_cfg.get('country_name','Generic Customs Administration')}
800
+ - Region: {country_cfg.get('region','Global')}
801
+ - WTO TFA Category: {country_cfg.get('wto_tfa_cat','A')}
802
+ - Sea target: {t_sea}h | Air target: {country_cfg.get('target_air',24)}h
803
+
804
+ SIMULATION RESULTS SUMMARY:
805
+ - Total BoEs simulated: {len(results)}
806
+ - Overall Mean ART: {sp.get('avg_art',0):.1f}h | Median: {sp.get('median_art',0):.1f}h
807
+ - Within target (≤{t_sea}h): {sp.get('target48_pct',0):.0f}%
808
+ - Green channel (facilitated): {sp.get('green_pct',0):.0f}%
809
+ - Machine release (Auto-OOC): {sp.get('machine_pct',0):.0f}%
810
+ - AEO enrolled: {sp.get('aeo_pct',0):.0f}%
811
+
812
+ SEGMENT BREAKDOWN (Mean hours per port):
813
+ """
814
+ for pt,d in seg_means.items():
815
+ improvement = round(d["baseline"]-d["total"],1) if d["baseline"]>0 else 0
816
+ prompt += f"""
817
+ {pt} Port ({d['port_name']}):
818
+ Baseline ART: {d['baseline']}h → Simulated ART: {d['total']}h (improvement: {improvement}h)
819
+ Seg A Pre-arrival: {d['A']}h | Seg B Customs: {d['B']}h | Seg C OGA/Duty: {d['C']}h | Seg D Logistics: {d['D']}h
820
+ Biggest bottleneck: {biggest_seg[pt]}
821
+ """
822
+
823
+ prompt += f"""
824
+ POLICY LEVERS ACTIVE IN THIS SIMULATION:
825
+ {', '.join(policy_used) if policy_used else 'None (baseline run)'}
826
+
827
+ Please provide:
828
+ 1. A 3-4 sentence EXECUTIVE SUMMARY of what the simulation shows, as you would write in a WCO TRS Final Report (§2.3.6).
829
+ 2. BOTTLENECK ANALYSIS — for each port, identify the biggest time segment and explain why it matters.
830
+ 3. TOP 3 WCO TOOL RECOMMENDATIONS — specific WCO instruments, conventions, or frameworks (e.g. Revised Kyoto Convention Standard 3.21, SAFE Framework AEO, Single Window Recommendation, TFA Article 7.6) that would address the identified bottlenecks. Be specific about which standard or instrument.
831
+ 4. NEXT STEPS — a concrete 3-step action plan for the Customs administration.
832
+
833
+ Keep the total response under 500 words. Use clear headings. Be specific and reference WCO instruments by name.
834
+ """
835
+ return prompt
836
+
837
+
838
+ SYSTEM_PROMPT = """You are a senior WCO (World Customs Organization) trade facilitation adviser with expertise in:
839
+ - WCO TRS Guide (Version 4, 2025) methodology
840
+ - WCO Revised Kyoto Convention (RKC) — General Annex Standards
841
+ - WCO SAFE Framework of Standards
842
+ - WTO Trade Facilitation Agreement (TFA) Article 7
843
+ - WCO Single Window Compendium
844
+ - Risk Management Guidelines (RMS/CRA)
845
+ - Authorized Economic Operator (AEO) programmes
846
+ You give practical, evidence-based advice grounded in WCO instruments. Always cite specific WCO standards."""
847
+
848
 
849
+ # ══════════════════════════════════════════════════════════════════════════════
850
+ # INTRO PAGE COMPONENT
851
+ # ══════════════════════════════════════════════════════════════════════════════
852
+ def render_intro():
853
+ st.markdown("""
854
+ <div style="background:white;border:1px solid #C5D5E8;border-top:5px solid #003366;
855
+ border-radius:8px;padding:28px 32px;margin-bottom:20px;">
856
+ <div style="display:flex;align-items:center;gap:16px;margin-bottom:16px;">
857
+ <div style="background:#003366;color:white;font-size:28px;width:56px;height:56px;
858
+ border-radius:8px;display:flex;align-items:center;justify-content:center;">🌐</div>
859
+ <div>
860
+ <h2 style="margin:0;color:#003366;font-size:1.5rem;">WCO Time Release Study Simulator</h2>
861
+ <p style="margin:0;color:#6B8BAE;font-size:0.85rem;">
862
+ Aligned with WCO TRS Guide Version 4, 2025 · WTO TFA Article 7.6.1 · SAFE Framework
863
+ </p>
864
+ </div>
865
+ </div>
866
+ </div>
867
+ """, unsafe_allow_html=True)
868
+
869
+ col1, col2 = st.columns([3,2])
870
+
871
+ with col1:
872
+ st.markdown("""
873
+ <div class="wco-card">
874
+ <span class="wco-tag">WHAT IS THIS?</span>
875
+ <p style="margin-top:10px;color:#003366;font-size:0.92rem;line-height:1.7;">
876
+ This simulator allows any <strong>Customs administration</strong> to model the impact of
877
+ trade facilitation policy reforms on cargo release times — before implementing them in the field.
878
+ It is built on the <strong>WCO Time Release Study (TRS) methodology</strong> (Guide v4, 2025),
879
+ the internationally recognised tool for measuring border clearance efficiency mandated by
880
+ <strong>WTO TFA Article 7.6.1</strong>.
881
+ </p>
882
+ <p style="color:#003366;font-size:0.92rem;line-height:1.7;">
883
+ The simulator models three ports (Sea, Air, Land Border) and measures the four WCO TRS
884
+ time segments: <strong>Seg A</strong> (Pre-arrival/Lodgement) →
885
+ <strong>Seg B</strong> (Customs Assessment) →
886
+ <strong>Seg C</strong> (OGA/Duty Payment) →
887
+ <strong>Seg D</strong> (Post-clearance/OOC Release).
888
+ </p>
889
+ </div>
890
+ """, unsafe_allow_html=True)
891
+
892
+ st.markdown("""
893
+ <div class="wco-card">
894
+ <span class="wco-tag">HOW THE SIMULATION WORKS</span>
895
+ <p style="margin-top:10px;color:#003366;font-size:0.92rem;line-height:1.7;">
896
+ The engine uses <strong>SimPy discrete-event simulation</strong> with
897
+ <strong>Gamma probability distributions</strong> — the same statistical approach
898
+ recommended in WCO TRS §2.3.1 to replicate the long-tail delay patterns seen in
899
+ real customs data. Each Bill of Entry (BoE) is a state machine that progresses
900
+ through WCO business process steps (§2.1.4 Appendix 1).
901
+ </p>
902
+ <ul style="color:#003366;font-size:0.88rem;line-height:1.9;margin-left:16px;">
903
+ <li><strong>RMS Channels</strong> — Green (auto-facilitated), Yellow (documentary), Red (physical exam)</li>
904
+ <li><strong>AEO tiers</strong> — T1/T2/T3 per WCO SAFE Framework reduce assessment time</li>
905
+ <li><strong>OGA intervention</strong> — PGA delays modelled with/without Single Window</li>
906
+ <li><strong>Advance filing</strong> — Pre-arrival declaration eliminates Segment A entirely</li>
907
+ <li><strong>Deferred duty</strong> — AEO privilege removes Segment C payment wait</li>
908
+ <li><strong>Auto-OOC</strong> — Machine release eliminates Segment D queue for Green channel</li>
909
+ </ul>
910
+ </div>
911
+ """, unsafe_allow_html=True)
912
+
913
+ with col2:
914
+ st.markdown("""
915
+ <div class="wco-card" style="border-left-color:#F5A623;">
916
+ <span class="wco-tag" style="background:#FEF6E8;color:#D4750A;">HOW TO USE THIS APP</span>
917
+ <ol style="margin-top:10px;color:#003366;font-size:0.88rem;line-height:1.9;margin-left:16px;">
918
+ <li>Select your <strong>country</strong> (or enter custom parameters)</li>
919
+ <li>Set <strong>policy levers</strong> in the sidebar</li>
920
+ <li>Click <strong>▶ RUN SIMULATION</strong></li>
921
+ <li>View the <strong>isometric port map</strong> with live cargo status</li>
922
+ <li>Analyse the <strong>TRS stacked bar chart</strong> (Segments A–D)</li>
923
+ <li>Get <strong>AI-powered analysis</strong> with WCO tool recommendations</li>
924
+ <li>Download the <strong>Audit Ledger CSV</strong> for your records</li>
925
+ </ol>
926
+ </div>
927
+
928
+ <div class="wco-card" style="border-left-color:#1A8A4A;">
929
+ <span class="wco-tag" style="background:#E8F5EE;color:#1A8A4A;">WHO IS THIS FOR?</span>
930
+ <ul style="margin-top:10px;color:#003366;font-size:0.88rem;line-height:1.9;margin-left:16px;">
931
+ <li>Customs administration <strong>policy officials</strong></li>
932
+ <li>WCO <strong>TRS Working Group</strong> members</li>
933
+ <li>Trade facilitation <strong>consultants & advisers</strong></li>
934
+ <li>National <strong>Single Window</strong> project teams</li>
935
+ <li>WTO TFA <strong>Category B/C implementation</strong> teams</li>
936
+ <li>Regional economic community <strong>customs experts</strong></li>
937
+ </ul>
938
+ </div>
939
+ """, unsafe_allow_html=True)
940
+
941
+ # WCO instrument reference
942
+ with st.expander("📚 WCO Instruments Referenced in This Simulator"):
943
+ c1,c2,c3 = st.columns(3)
944
+ with c1:
945
+ st.markdown("""
946
+ **WCO TRS Guide v4 2025**
947
+ - §2.1.4 Business process model
948
+ - §2.1.6 Sampling methodology
949
+ - §2.3.1 Data analysis (mean/median)
950
+ - §2.3.3 Data visualisation
951
+ - §2.3.6 Final report format
952
+ """)
953
+ with c2:
954
+ st.markdown("""
955
+ **WCO Revised Kyoto Convention**
956
+ - Standard 3.21 — Advance lodgement
957
+ - Standard 6.2 — Risk management
958
+ - Standard 7.2 — AEO benefits
959
+ - Specific Annex J — AEO
960
+ """)
961
+ with c3:
962
+ st.markdown("""
963
+ **WTO TFA & SAFE Framework**
964
+ - TFA Art. 7.6.1 — ART publication
965
+ - TFA Art. 7.4 — Risk management
966
+ - SAFE Pillar 2 — AEO
967
+ - WCO Single Window Compendium
968
+ """)
969
+
970
+
971
+ # ══════════════════════════════════════════════════════════════════════════════
972
+ # MAIN APP
973
+ # ══════════════════════════════════════════════════════════════════════════════
974
  def main():
975
+ # ── Header banner ───────────────────────────────────────────────────────
976
  st.markdown("""
977
+ <div style="background:linear-gradient(135deg,#003366,#0066CC);
978
+ padding:18px 28px;border-radius:8px;margin-bottom:20px;
979
+ display:flex;align-items:center;justify-content:space-between;">
980
+ <div>
981
+ <h1 style="color:white;margin:0;font-size:1.7rem;letter-spacing:.04em;">
982
+ 🌐 Meridia TRS Simulator
983
+ </h1>
984
+ <p style="color:#A0C4E8;margin:4px 0 0;font-size:0.8rem;letter-spacing:.12em;">
985
+ WORLD CUSTOMS ORGANIZATION · TIME RELEASE STUDY · GUIDE v4 2025
986
+ </p>
987
+ </div>
988
+ <div style="text-align:right;">
989
+ <div style="background:rgba(255,255,255,0.12);border:1px solid rgba(255,255,255,0.25);
990
+ border-radius:6px;padding:6px 14px;">
991
+ <div style="color:#F5A623;font-size:11px;font-weight:700;letter-spacing:.1em;">WTO TFA ART.7.6.1</div>
992
+ <div style="color:white;font-size:10px;margin-top:2px;">Compliant simulation methodology</div>
993
+ </div>
994
+ </div>
995
  </div>
 
996
  """, unsafe_allow_html=True)
997
 
998
+ # ── Sidebar ──────────────────────────────────────────────────────────────
999
  with st.sidebar:
1000
+ st.markdown("## 🌐 WCO TRS Simulator")
1001
+ st.markdown("<p style='color:#A0C4E8;font-size:0.72rem;letter-spacing:.1em;'>POLICY PARAMETERS</p>",
1002
+ unsafe_allow_html=True)
1003
 
1004
+ # Country selector
1005
+ country_name = st.selectbox("Country / Administration",
1006
+ list(COUNTRY_PRESETS.keys()), index=0)
1007
+ country_cfg = COUNTRY_PRESETS[country_name].copy()
1008
+ country_cfg["country_name"] = country_name
1009
+
1010
+ st.markdown("---")
1011
+ st.markdown("**Pre-arrival & Risk**")
1012
+ advance_pct = st.slider("Advance Filing %", 0,100,40,
1013
+ help="WCO RKC Standard 3.21 — pre-arrival declaration")
1014
+ rms_pct = st.slider("RMS Facilitation %", 0,100,50,
1015
+ help="WCO RMS: sets Green channel probability")
1016
+ aeo_pct = st.slider("AEO Enrollment %", 0,100,30,
1017
+ help="WCO SAFE Framework trusted trader programme")
1018
 
1019
  st.markdown("**System Enablers**")
1020
+ pga_sw = st.toggle("PGA Single Window", value=country_cfg.get("existing_sw",False))
1021
+ deferred = st.toggle("Deferred Duty (AEO)", value=False)
1022
+ auto_ooc = st.toggle("Auto Out-of-Charge", value=False)
1023
 
1024
  st.markdown("**Officer Capacity**")
1025
+ o_sea = st.slider("Sea Officers", 1,20,8)
1026
+ o_air = st.slider("Air Officers", 1,10,4)
1027
+ o_land = st.slider("Land Officers", 1,15,6)
1028
+
1029
+ # Advanced — collapsed
1030
+ with st.expander("⚙ Advanced Country Config (Optional)"):
1031
+ st.markdown("*Override country defaults below*")
1032
+ st.markdown("**Port Names**")
1033
+ for pt in ["Sea","Air","Land"]:
1034
+ country_cfg["ports"][pt] = st.text_input(
1035
+ f"{pt} Port Name", country_cfg["ports"].get(pt, pt), key=f"port_{pt}")
1036
+
1037
+ st.markdown("**Benchmark Targets (hours)**")
1038
+ country_cfg["target_sea"] = st.number_input("Sea/Land target (h)", 12,240,
1039
+ int(country_cfg.get("target_sea",48)), key="ts")
1040
+ country_cfg["target_air"] = st.number_input("Air target (h)", 6,120,
1041
+ int(country_cfg.get("target_air",24)), key="ta")
1042
+
1043
+ st.markdown("**Baseline ART (before reforms)**")
1044
+ for pt in ["Sea","Air","Land"]:
1045
+ country_cfg["baseline_art"][pt] = st.number_input(
1046
+ f"{pt} baseline ART (h)", 0, 500,
1047
+ int(country_cfg["baseline_art"].get(pt,48)), key=f"base_{pt}")
1048
+
1049
+ st.markdown("**Port Volumes (BoEs per cycle)**")
1050
+ for pt in ["Sea","Air","Land"]:
1051
+ country_cfg["volumes"][pt] = st.number_input(
1052
+ f"{pt} volume", 1, 200,
1053
+ int(country_cfg["volumes"].get(pt,50)), key=f"vol_{pt}")
1054
+
1055
+ country_cfg["region"] = st.selectbox("WCO Region", WCO_REGIONS,
1056
+ index=WCO_REGIONS.index(country_cfg.get("region","Global")))
1057
+ country_cfg["wto_tfa_cat"] = st.selectbox("WTO TFA Category",
1058
+ ["A","B","C"], index=["A","B","C"].index(country_cfg.get("wto_tfa_cat","A")))
1059
+ params_extra = {
1060
+ "pga_probability": st.slider("OGA involvement %",0,100,35,key="pga_prob") / 100
1061
+ }
1062
 
1063
  st.markdown("---")
1064
+
1065
+ # OpenRouter API key
1066
+ with st.expander("🤖 AI Analysis (OpenRouter API Key)"):
1067
+ api_key = st.text_input("OpenRouter API Key",
1068
+ value=st.session_state.get("api_key",""),
1069
+ type="password", help="Get free key at openrouter.ai")
1070
+ if api_key:
1071
+ st.session_state["api_key"] = api_key
1072
+ st.caption("Uses free-tier models. No cost to you.")
1073
+
1074
+ st.markdown("---")
1075
+ run_btn = st.button("▶ RUN SIMULATION", use_container_width=True)
1076
+ reset_btn = st.button("↺ RESET", use_container_width=True)
1077
 
1078
  # ── Session state ────────────────────────────────────────────────────────
1079
+ if "results" not in st.session_state: st.session_state.results = []
1080
+ if "sim_params" not in st.session_state: st.session_state.sim_params = {}
1081
+ if "country_cfg" not in st.session_state: st.session_state.country_cfg = country_cfg
1082
+ if "llm_result" not in st.session_state: st.session_state.llm_result = ""
1083
+ if "llm_model" not in st.session_state: st.session_state.llm_model = ""
1084
  if reset_btn:
1085
+ st.session_state.results = []; st.session_state.sim_params = {}
1086
+ st.session_state.llm_result = ""; st.session_state.llm_model = ""
1087
 
1088
  if run_btn:
1089
  params = dict(
1090
+ advance_filing_pct=advance_pct, rms_facilitation_pct=rms_pct,
1091
+ aeo_enrollment_pct=aeo_pct, pga_single_window=pga_sw,
1092
+ deferred_duty=deferred, auto_ooc=auto_ooc,
1093
+ officers_sea=o_sea, officers_air=o_air, officers_land=o_land,
1094
+ pga_probability=locals().get("params_extra",{}).get("pga_probability",0.35),
 
 
 
 
1095
  )
1096
+ with st.spinner(f"Running WCO TRS simulation for {country_name}..."):
1097
+ results = run_simulation(params, country_cfg)
1098
 
 
1099
  arts = [r.total_hours for r in results]
1100
+ t_sea = country_cfg.get("target_sea",48)
1101
+ sp = dict(
1102
+ avg_art = float(np.mean(arts)) if arts else 0,
1103
+ median_art = float(np.median(arts)) if arts else 0,
1104
+ green_pct = len([r for r in results if r.channel=="Green"])/len(results)*100 if results else 0,
1105
+ aeo_pct = len([r for r in results if r.aeo_status!="None"])/len(results)*100 if results else 0,
1106
+ machine_pct = len([r for r in results if r.machine_release])/len(results)*100 if results else 0,
1107
+ target48_pct = len([a for a in arts if a<=t_sea])/len(arts)*100 if arts else 0,
1108
+ target24_pct = len([a for a in arts if a<=24])/len(arts)*100 if arts else 0,
1109
+ target_sea = t_sea,
1110
+ )
1111
+ st.session_state.results = results
1112
+ st.session_state.sim_params = sp
1113
+ st.session_state.country_cfg = country_cfg
1114
+ st.session_state.llm_result = ""
1115
+ st.session_state.sim_params["params"] = params
1116
 
1117
  results = st.session_state.results
1118
  sim_params = st.session_state.sim_params
1119
+ ccfg = st.session_state.get("country_cfg", country_cfg)
1120
 
1121
+ # ── Tabs ─────────────────────────────────────────────────────────────────
1122
+ tab0,tab1,tab2,tab3,tab4,tab5 = st.tabs([
1123
+ "📖 ABOUT",
1124
+ "🗺 PORT VIEW",
1125
+ "📊 TRS REPORT",
1126
+ "📈 RMS CHANNELS",
1127
+ "🤖 AI ANALYSIS",
1128
+ "📋 AUDIT LEDGER",
1129
+ ])
1130
 
1131
+ with tab0:
1132
+ render_intro()
 
 
1133
 
1134
+ with tab1:
1135
+ st.components.v1.html(build_phaser_scene(results, sim_params, ccfg),
1136
+ height=472, scrolling=False)
1137
+ if results:
1138
  st.markdown("---")
1139
+ c1,c2,c3,c4,c5 = st.columns(5)
1140
+ c1.metric("Avg Release Time", f"{sim_params['avg_art']:.1f}h",
1141
+ f"Median {sim_params['median_art']:.1f}h")
1142
+ c2.metric("Green Channel", f"{sim_params['green_pct']:.0f}%","RMS Facilitated")
1143
+ c3.metric("Machine Release", f"{sim_params['machine_pct']:.0f}%","Auto-OOC")
1144
+ c4.metric(f"Within {sim_params.get('target_sea',48)}h Target",
1145
+ f"{sim_params['target48_pct']:.0f}%","WTO TFA Art.7.6")
1146
+ c5.metric("AEO Enrolled", f"{sim_params['aeo_pct']:.0f}%","SAFE Framework")
1147
 
1148
  with tab2:
1149
  if not results:
1150
+ st.info("Run the simulation to generate the WCO TRS chart.")
1151
  else:
1152
+ st.plotly_chart(build_trs_chart(results, ccfg, sim_params), use_container_width=True)
1153
+
1154
+ rows = []
1155
+ for pt in ["Sea","Air","Land"]:
1156
+ sub = [r for r in results if r.port_type==pt]
1157
+ if not sub: continue
1158
+ rows.append({
1159
+ "Port": ccfg["ports"].get(pt,pt),
1160
+ "Mode": pt, "n": len(sub),
1161
+ "Seg A (h)": round(np.mean([r.seg_prearr for r in sub]),2),
1162
+ "Seg B (h)": round(np.mean([r.seg_customs for r in sub]),2),
1163
+ "Seg C (h)": round(np.mean([r.seg_oga_duty for r in sub]),2),
1164
+ "Seg D (h)": round(np.mean([r.seg_logistics for r in sub]),2),
1165
+ "Mean ART": round(np.mean([r.total_hours for r in sub]),2),
1166
+ "Median ART":round(np.median([r.total_hours for r in sub]),2),
1167
+ "Baseline": ccfg["baseline_art"].get(pt,"-"),
1168
+ })
1169
+ st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)
1170
 
 
1171
  art = sim_params["avg_art"]
1172
+ t = sim_params.get("target_sea",48)
1173
+ st.markdown("#### Policy Insights")
1174
+ ca,cb = st.columns(2)
1175
+ with ca:
1176
+ if art<3: st.success("🏆 Jaigaon LCS level ART < 3h. World-class.")
1177
+ elif art<t/2: st.success(f"✅ ART {art:.1f}h — well within {t}h target.")
1178
+ elif art<t: st.warning(f" ART {art:.1f}h — below {t}h target but improvable.")
1179
+ else: st.error(f"🚨 ART {art:.1f}h exceeds {t}h WTO TFA target.")
1180
+ with cb:
1181
+ if advance_pct>60 and rms_pct>60:
1182
+ st.success("✅ Advance Filing + RMS >60% — optimal WCO pathway active.")
1183
  else:
1184
+ st.info("💡 Raise both Advance Filing and RMS above 60% for Jaigaon-level ART.")
1185
+ if not pga_sw:
1186
+ st.warning("⚠ PGA without Single Window causes Segment C bottleneck.")
 
 
 
 
 
 
 
1187
 
1188
  with tab3:
1189
  if not results:
1190
+ st.info("Run simulation to see RMS channel data.")
1191
+ else:
1192
+ st.plotly_chart(build_channel_chart(results), use_container_width=True)
1193
+ ch_rows = []
1194
+ for ch in ["Green","Yellow","Red"]:
1195
+ sub = [r for r in results if r.channel==ch]
1196
+ if sub:
1197
+ ch_rows.append({
1198
+ "Channel":ch,"Count":len(sub),
1199
+ "Mean ART (h)": round(np.mean([r.total_hours for r in sub]),2),
1200
+ "Median ART (h)":round(np.median([r.total_hours for r in sub]),2),
1201
+ "% Within target":round(len([r for r in sub if r.total_hours<=sim_params.get("target_sea",48)])/len(sub)*100,1),
1202
+ })
1203
+ st.dataframe(pd.DataFrame(ch_rows), use_container_width=True, hide_index=True)
1204
+
1205
+ with tab4:
1206
+ st.markdown("### 🤖 AI-Powered WCO TRS Analysis")
1207
+ st.markdown(
1208
+ "The AI adviser analyses your simulation results and recommends specific "
1209
+ "WCO instruments, conventions, and standards to address your bottlenecks. "
1210
+ "Uses free LLM models via OpenRouter — no cost."
1211
+ )
1212
+
1213
+ if not results:
1214
+ st.info("Run the simulation first, then come back here for AI analysis.")
1215
+ else:
1216
+ api_key_val = st.session_state.get("api_key","")
1217
+ if not api_key_val:
1218
+ st.warning("Enter your OpenRouter API key in the sidebar (free at openrouter.ai) to enable AI analysis.")
1219
+ else:
1220
+ col_btn, col_info = st.columns([2,3])
1221
+ with col_btn:
1222
+ if st.button("🤖 Generate WCO Analysis", use_container_width=True):
1223
+ prompt = build_llm_prompt(
1224
+ results, sim_params,
1225
+ sim_params.get("params",{}), ccfg
1226
+ )
1227
+ with st.spinner("Consulting WCO trade facilitation AI adviser..."):
1228
+ text, model_used = call_llm(prompt, api_key_val, SYSTEM_PROMPT)
1229
+ st.session_state.llm_result = text
1230
+ st.session_state.llm_model = model_used
1231
+
1232
+ with col_info:
1233
+ st.caption("AI tries 9 free models in order. Typical response: 15–30 seconds.")
1234
+
1235
+ if st.session_state.llm_result:
1236
+ st.markdown(f"""
1237
+ <div style="background:white;border:1px solid #C5D5E8;border-top:4px solid #003366;
1238
+ border-radius:8px;padding:24px 28px;margin-top:16px;">
1239
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
1240
+ <span style="color:#003366;font-weight:700;font-size:1rem;">
1241
+ WCO Trade Facilitation Analysis
1242
+ </span>
1243
+ <span style="background:#E8EFF7;color:#003366;font-size:10px;font-weight:600;
1244
+ padding:3px 10px;border-radius:10px;letter-spacing:.06em;">
1245
+ via {st.session_state.llm_model}
1246
+ </span>
1247
+ </div>
1248
+ <div style="color:#003366;font-size:0.92rem;line-height:1.75;white-space:pre-wrap;">{st.session_state.llm_result}</div>
1249
+ </div>
1250
+ """, unsafe_allow_html=True)
1251
+
1252
+ # Download analysis
1253
+ st.download_button(
1254
+ "⬇ Download AI Analysis (TXT)",
1255
+ data=st.session_state.llm_result.encode("utf-8"),
1256
+ file_name=f"WCO_TRS_Analysis_{ccfg.get('country_name','Generic')}.txt",
1257
+ mime="text/plain",
1258
+ )
1259
+
1260
+ with tab5:
1261
+ if not results:
1262
+ st.info("Run simulation to populate the audit ledger.")
1263
  else:
1264
  df = pd.DataFrame([{
1265
+ "Shipment_ID": r.shipment_id,
1266
+ "Port_Mode": r.port_type,
1267
+ "Port_Name": ccfg["ports"].get(r.port_type, r.port_type),
1268
+ "Filing_Type": r.filing_type,
1269
+ "RMS_Channel": r.channel,
1270
+ "OGA_Involved": r.pga_involved,
1271
+ "AEO_Status": r.aeo_status,
1272
+ "Machine_Release": r.machine_release,
1273
+ "Seg_A_PreArr_h": round(r.seg_prearr, 2),
1274
+ "Seg_B_Customs_h": round(r.seg_customs, 2),
1275
+ "Seg_C_OGA_Duty_h": round(r.seg_oga_duty, 2),
1276
+ "Seg_D_Logistics_h": round(r.seg_logistics,2),
1277
+ "Total_Hours": round(r.total_hours, 2),
1278
+ f"Within_{sim_params.get('target_sea',48)}h": r.total_hours<=sim_params.get("target_sea",48),
1279
+ "Within_24h": r.total_hours<=24,
1280
  } for r in results])
1281
 
1282
+ st.dataframe(df, use_container_width=True, height=380)
 
 
1283
  st.download_button(
1284
+ "⬇ Download Meridia_TRS_Ledger.csv (WCO Format)",
1285
+ data=df.to_csv(index=False).encode("utf-8"),
1286
+ file_name=f"WCO_TRS_Ledger_{ccfg.get('country_name','Generic').replace(' ','_')}.csv",
1287
+ mime="text/csv", use_container_width=True,
 
1288
  )
1289
+ st.markdown("""
1290
+ <div style="margin-top:12px;padding:10px 16px;background:#F0F4F8;
1291
+ border:1px solid #C5D5E8;border-left:4px solid #0066CC;border-radius:6px;
1292
+ font-size:11px;color:#6B8BAE;line-height:1.7;">
1293
+ <strong style="color:#003366;">WCO TRS METHODOLOGY NOTE</strong> —
1294
+ Segments per §2.1.4: A=Arrival→Lodgement (T0→T1) · B=Customs Assessment (T1→T2) ·
1295
+ C=OGA/Duty Payment (T2→T3) · D=Post-clearance Logistics (T3→T4).
1296
+ Mean & Median both reported per §2.3.1. RMS: Green=auto · Yellow=documentary · Red=physical.
1297
+ WTO TFA Art.7.6.1 benchmarks apply. Data suitable for WCO TRS software import.
1298
+ </div>
1299
+ """, unsafe_allow_html=True)
1300
 
1301
 
1302
  if __name__ == "__main__":