| import streamlit as st |
| import simpy |
| import numpy as np |
| import pandas as pd |
| import plotly.graph_objects as go |
| import json |
| import requests |
| import time |
| from dataclasses import dataclass, field |
| from typing import List, Optional |
|
|
| st.set_page_config( |
| page_title="Meridia TRS Simulator — WCO Aligned", |
| page_icon="🌐", |
| layout="wide", |
| initial_sidebar_state="expanded" |
| ) |
|
|
| |
| |
| |
| |
| st.markdown(""" |
| <style> |
| @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'); |
| |
| :root { |
| --wco-navy: #003366; |
| --wco-blue: #0066CC; |
| --wco-lblue: #3399FF; |
| --wco-gold: #F5A623; |
| --wco-white: #FFFFFF; |
| --wco-offwhite:#F0F4F8; |
| --wco-light: #E8EFF7; |
| --wco-muted: #6B8BAE; |
| --wco-border: #C5D5E8; |
| --wco-dark: #001833; |
| --wco-green: #1A8A4A; |
| --wco-red: #CC2200; |
| --wco-amber: #D4750A; |
| --mono: 'Source Code Pro', monospace; |
| --sans: 'Source Sans 3', sans-serif; |
| } |
| |
| html, body, [class*="css"] { |
| font-family: var(--sans) !important; |
| background-color: var(--wco-offwhite) !important; |
| color: var(--wco-navy) !important; |
| } |
| .stApp { background: var(--wco-offwhite) !important; } |
| |
| /* Sidebar */ |
| [data-testid="stSidebar"] { |
| background: var(--wco-navy) !important; |
| border-right: 3px solid var(--wco-blue) !important; |
| } |
| [data-testid="stSidebar"] * { color: var(--wco-white) !important; } |
| [data-testid="stSidebar"] h1, |
| [data-testid="stSidebar"] h2, |
| [data-testid="stSidebar"] h3 { color: var(--wco-gold) !important; } |
| [data-testid="stSidebar"] .stSlider > label { color: #C5D5E8 !important; } |
| [data-testid="stSidebar"] [data-testid="stSlider"] > div > div > div > div { |
| background: var(--wco-gold) !important; } |
| [data-testid="stSidebar"] .stToggle label { color: #C5D5E8 !important; } |
| |
| /* Metrics */ |
| [data-testid="stMetric"] { |
| background: var(--wco-white) !important; |
| border: 1px solid var(--wco-border) !important; |
| border-top: 3px solid var(--wco-blue) !important; |
| border-radius: 6px !important; |
| padding: 14px !important; |
| } |
| [data-testid="stMetricValue"] { |
| color: var(--wco-navy) !important; |
| font-family: var(--mono) !important; |
| font-size: 1.6rem !important; |
| font-weight: 600 !important; |
| } |
| [data-testid="stMetricLabel"] { |
| color: var(--wco-muted) !important; |
| font-size: 0.72rem !important; |
| font-weight: 600 !important; |
| letter-spacing: 0.08em !important; |
| text-transform: uppercase !important; |
| } |
| |
| /* Buttons */ |
| .stButton>button { |
| background: var(--wco-blue) !important; |
| border: none !important; |
| color: white !important; |
| font-family: var(--sans) !important; |
| font-weight: 600 !important; |
| border-radius: 4px !important; |
| letter-spacing: 0.04em !important; |
| transition: background 0.2s !important; |
| } |
| .stButton>button:hover { background: var(--wco-navy) !important; } |
| |
| /* Headings */ |
| h1,h2,h3 { font-family: var(--sans) !important; } |
| h1 { color: var(--wco-navy) !important; font-weight: 700 !important; } |
| h2 { color: var(--wco-blue) !important; font-weight: 600 !important; } |
| h3 { color: var(--wco-navy) !important; font-weight: 600 !important; } |
| |
| /* Tabs */ |
| [data-testid="stTabs"] button { |
| font-family: var(--sans) !important; |
| font-weight: 600 !important; |
| color: var(--wco-muted) !important; |
| border-radius: 0 !important; |
| } |
| [data-testid="stTabs"] button[aria-selected="true"] { |
| color: var(--wco-blue) !important; |
| border-bottom: 3px solid var(--wco-blue) !important; |
| } |
| |
| /* Expander */ |
| [data-testid="stExpander"] { |
| border: 1px solid var(--wco-border) !important; |
| border-radius: 6px !important; |
| background: var(--wco-white) !important; |
| } |
| |
| /* Info/success/warning boxes */ |
| .stAlert { border-radius: 6px !important; } |
| |
| /* Scrollbar */ |
| ::-webkit-scrollbar { width: 5px; } |
| ::-webkit-scrollbar-thumb { background: var(--wco-border); border-radius: 3px; } |
| |
| /* WCO card style */ |
| .wco-card { |
| background: white; |
| border: 1px solid var(--wco-border); |
| border-left: 4px solid var(--wco-blue); |
| border-radius: 6px; |
| padding: 16px 20px; |
| margin-bottom: 12px; |
| } |
| .wco-tag { |
| display: inline-block; |
| background: var(--wco-light); |
| color: var(--wco-blue); |
| font-size: 11px; |
| font-weight: 600; |
| padding: 2px 10px; |
| border-radius: 12px; |
| letter-spacing: 0.06em; |
| margin-right: 4px; |
| } |
| .wco-badge-green { background:#E8F5EE; color:#1A8A4A; border:1px solid #1A8A4A; } |
| .wco-badge-yellow { background:#FEF6E8; color:#D4750A; border:1px solid #D4750A; } |
| .wco-badge-red { background:#FDECEA; color:#CC2200; border:1px solid #CC2200; } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
|
|
| |
| |
| |
| COUNTRY_PRESETS = { |
| "Custom / Generic": { |
| "ports": {"Sea": "Seaport", "Air": "Airport", "Land": "Land Border"}, |
| "volumes": {"Sea": 50, "Air": 10, "Land": 25}, |
| "baseline_art": {"Sea": 48, "Air": 24, "Land": 36}, |
| "target_sea": 48, "target_air": 24, "target_land": 36, |
| "wto_tfa_cat": "A", "region": "Global", |
| "existing_sw": False, "existing_aeo": False, |
| }, |
| "India": { |
| "ports": {"Sea": "JNPT Mumbai", "Air": "Delhi IGI Airport", "Land": "Attari/Petrapole ICP"}, |
| "volumes": {"Sea": 60, "Air": 15, "Land": 30}, |
| "baseline_art": {"Sea": 85, "Air": 44, "Land": 120}, |
| "target_sea": 48, "target_air": 24, "target_land": 48, |
| "wto_tfa_cat": "A", "region": "Asia-Pacific", |
| "existing_sw": True, "existing_aeo": True, |
| }, |
| "Kenya": { |
| "ports": {"Sea": "Port of Mombasa", "Air": "JKIA Nairobi", "Land": "Malaba Border"}, |
| "volumes": {"Sea": 40, "Air": 8, "Land": 35}, |
| "baseline_art": {"Sea": 96, "Air": 48, "Land": 168}, |
| "target_sea": 72, "target_air": 48, "target_land": 72, |
| "wto_tfa_cat": "B", "region": "Eastern/Southern Africa", |
| "existing_sw": True, "existing_aeo": False, |
| }, |
| "Ghana": { |
| "ports": {"Sea": "Tema Port", "Air": "Kotoka Int'l Airport", "Land": "Aflao Border"}, |
| "volumes": {"Sea": 35, "Air": 6, "Land": 20}, |
| "baseline_art": {"Sea": 120, "Air": 72, "Land": 144}, |
| "target_sea": 96, "target_air": 48, "target_land": 96, |
| "wto_tfa_cat": "C", "region": "West Africa", |
| "existing_sw": False, "existing_aeo": False, |
| }, |
| "Vietnam": { |
| "ports": {"Sea": "Cat Lai Port HCMC", "Air": "Tan Son Nhat Airport", "Land": "Lao Cai Border"}, |
| "volumes": {"Sea": 70, "Air": 20, "Land": 40}, |
| "baseline_art": {"Sea": 60, "Air": 30, "Land": 72}, |
| "target_sea": 36, "target_air": 18, "target_land": 48, |
| "wto_tfa_cat": "A", "region": "Asia-Pacific", |
| "existing_sw": True, "existing_aeo": True, |
| }, |
| "Morocco": { |
| "ports": {"Sea": "Port of Casablanca", "Air": "Mohammed V Airport", "Land": "Bab Sebta"}, |
| "volumes": {"Sea": 45, "Air": 10, "Land": 25}, |
| "baseline_art": {"Sea": 72, "Air": 36, "Land": 96}, |
| "target_sea": 48, "target_air": 24, "target_land": 48, |
| "wto_tfa_cat": "A", "region": "North Africa/Middle East", |
| "existing_sw": True, "existing_aeo": True, |
| }, |
| "Colombia": { |
| "ports": {"Sea": "Port of Cartagena", "Air": "El Dorado Bogotá", "Land": "Ipiales Border"}, |
| "volumes": {"Sea": 40, "Air": 12, "Land": 20}, |
| "baseline_art": {"Sea": 84, "Air": 36, "Land": 120}, |
| "target_sea": 48, "target_air": 24, "target_land": 72, |
| "wto_tfa_cat": "A", "region": "Latin America/Caribbean", |
| "existing_sw": True, "existing_aeo": True, |
| }, |
| } |
|
|
| WCO_REGIONS = ["Global","Asia-Pacific","Eastern/Southern Africa","West Africa", |
| "North Africa/Middle East","Latin America/Caribbean","Europe","Gulf Region"] |
|
|
| |
| |
| |
| MODELS_TO_TRY = [ |
| ("qwen/qwen3-coder:free", "Qwen3 Coder"), |
| ("qwen/qwen3-next-80b-a3b-instruct:free", "Qwen3 Next 80B"), |
| ("openai/gpt-oss-120b:free", "GPT OSS 120B"), |
| ("google/gemma-4-26b-a4b-it:free", "Gemma 4 26B"), |
| ("nousresearch/hermes-3-llama-3.1-405b:free", "Hermes 3 Llama 405B"), |
| ("deepseek/deepseek-r1:free", "DeepSeek R1"), |
| ("google/gemini-2.0-flash-exp:free", "Gemini 2.0 Flash"), |
| ("meta-llama/llama-3.1-8b-instruct:free", "Llama 3.1 8B"), |
| ("mistralai/mistral-7b-instruct:free", "Mistral 7B"), |
| ] |
|
|
| def call_llm(prompt: str, api_key: str, system: str = "") -> tuple[str, str]: |
| """Try each free model in order; return (text, model_name_used).""" |
| headers = { |
| "Authorization": f"Bearer {api_key}", |
| "Content-Type": "application/json", |
| "HTTP-Referer": "https://meridia-trs.hf.space", |
| "X-Title": "Meridia WCO TRS Simulator", |
| } |
| messages = [] |
| if system: |
| messages.append({"role": "system", "content": system}) |
| messages.append({"role": "user", "content": prompt}) |
|
|
| for model_id, model_name in MODELS_TO_TRY: |
| try: |
| resp = requests.post( |
| "https://openrouter.ai/api/v1/chat/completions", |
| headers=headers, |
| json={"model": model_id, "messages": messages, "max_tokens": 1200}, |
| timeout=30, |
| ) |
| if resp.status_code == 200: |
| data = resp.json() |
| text = data["choices"][0]["message"]["content"].strip() |
| if text and len(text) > 50: |
| return text, model_name |
| except Exception: |
| continue |
| return "⚠ Could not reach any free LLM model. Check your API key or try again.", "None" |
|
|
|
|
| |
| |
| |
| @dataclass |
| class BoE: |
| shipment_id: str |
| port_type: str |
| filing_type: str |
| pga_involved: bool |
| aeo_status: str |
| channel: str |
| machine_release: bool = False |
| t_arrival: float = 0.0 |
| t_lodged: float = 0.0 |
| t_assessed: float = 0.0 |
| t_payment: float = 0.0 |
| t_ooc: float = 0.0 |
|
|
| @property |
| def total_hours(self): return max(self.t_ooc - self.t_arrival, 0) |
| @property |
| def seg_prearr(self): return max(self.t_lodged - self.t_arrival, 0) |
| @property |
| def seg_customs(self): return max(self.t_assessed - self.t_lodged, 0) |
| @property |
| def seg_oga_duty(self): return max(self.t_payment - self.t_assessed, 0) |
| @property |
| def seg_logistics(self): return max(self.t_ooc - self.t_payment, 0) |
|
|
|
|
| |
| |
| |
| def run_simulation(params: dict, country_cfg: dict) -> List[BoE]: |
| results: List[BoE] = [] |
| rng = np.random.default_rng(42) |
| env = simpy.Environment() |
|
|
| vols = country_cfg.get("volumes", {"Sea":50,"Air":10,"Land":25}) |
| base = country_cfg.get("baseline_art", {"Sea":48,"Air":24,"Land":36}) |
|
|
| officers_sea = simpy.Resource(env, capacity=params["officers_sea"]) |
| officers_air = simpy.Resource(env, capacity=params["officers_air"]) |
| officers_land = simpy.Resource(env, capacity=params["officers_land"]) |
|
|
| |
| PORT_CONFIG = { |
| "Sea": {"volume": vols["Sea"], "resource": officers_sea, |
| "speed": base["Sea"] / 48.0}, |
| "Air": {"volume": vols["Air"], "resource": officers_air, |
| "speed": base["Air"] / 24.0}, |
| "Land": {"volume": vols["Land"], "resource": officers_land, |
| "speed": base["Land"] / 48.0}, |
| } |
| counter = [0] |
|
|
| def process_boe(env, port_type, cfg): |
| counter[0] += 1 |
| sid = f"{port_type[0]}-{counter[0]:04d}" |
|
|
| is_advance = rng.random() < (params["advance_filing_pct"] / 100) |
| is_aeo = rng.random() < (params["aeo_enrollment_pct"] / 100) |
| is_pga = rng.random() < params.get("pga_probability", 0.35) |
| aeo_tier = rng.choice(["T1","T2","T3"]) if is_aeo else "None" |
|
|
| rms_thr = params["rms_facilitation_pct"] / 100 |
| roll = rng.random() |
| channel = "Green" if roll < rms_thr else ("Yellow" if roll < rms_thr+0.25 else "Red") |
|
|
| boe = BoE(shipment_id=sid, port_type=port_type, |
| filing_type="Advance" if is_advance else "Normal", |
| pga_involved=is_pga, aeo_status=aeo_tier, channel=channel) |
| boe.t_arrival = env.now |
|
|
| |
| spd = cfg["speed"] |
| if is_advance: |
| yield env.timeout(rng.gamma(1.2, 0.8) * spd) |
| else: |
| yield env.timeout(rng.gamma(2, 12) * spd) |
| boe.t_lodged = env.now |
|
|
| |
| if channel == "Green": |
| yield env.timeout(rng.gamma(1, 0.3) * spd) |
| else: |
| with cfg["resource"].request() as req: |
| yield req |
| if channel == "Yellow": |
| d = rng.gamma(2, 3) * spd * (0.5 if is_aeo else 1.0) |
| else: |
| d = rng.gamma(3, 8) * spd * (0.6 if is_aeo else 1.0) |
| yield env.timeout(d) |
| boe.t_assessed = env.now |
|
|
| |
| duty = 0 if (is_aeo and params["deferred_duty"]) else rng.gamma(2, 2) * spd |
| if is_pga: |
| oga = rng.gamma(2, 2 if params["pga_single_window"] else 8) * spd |
| else: |
| oga = 0 |
| yield env.timeout(max(duty, oga)) |
| boe.t_payment = env.now |
|
|
| |
| if channel == "Green" and params["auto_ooc"]: |
| boe.machine_release = True |
| yield env.timeout(0) |
| elif is_aeo: |
| yield env.timeout(rng.gamma(1.5, 0.8) * spd) |
| else: |
| yield env.timeout(rng.gamma(2, 1.5) * spd) |
| boe.t_ooc = env.now |
| results.append(boe) |
|
|
| def port_gen(env, pt, cfg): |
| for _ in range(cfg["volume"]): |
| env.process(process_boe(env, pt, cfg)) |
| yield env.timeout(rng.exponential(1.5)) |
|
|
| for pt, cfg in PORT_CONFIG.items(): |
| env.process(port_gen(env, pt, cfg)) |
| env.run(until=800) |
| return results |
|
|
|
|
| |
| |
| |
| def build_phaser_scene(results, sp, country_cfg): |
| art = round(sp.get("avg_art",0),1) |
| med = round(sp.get("median_art",0),1) |
| fac = round(sp.get("green_pct",0),1) |
| mach = round(sp.get("machine_pct",0),1) |
| t48 = round(sp.get("target48_pct",0),1) |
| aeo = round(sp.get("aeo_pct",0),1) |
|
|
| ports_cfg = country_cfg.get("ports", {"Sea":"Seaport","Air":"Airport","Land":"Land Border"}) |
| sea_label = ports_cfg.get("Sea","Seaport")[:16] |
| air_label = ports_cfg.get("Air","Airport")[:16] |
| land_label = ports_cfg.get("Land","Land Border")[:16] |
|
|
| sea_n = len([r for r in results if r.port_type=="Sea"]) |
| air_n = len([r for r in results if r.port_type=="Air"]) |
| land_n = len([r for r in results if r.port_type=="Land"]) |
| g_n = len([r for r in results if r.channel=="Green"]) |
| y_n = len([r for r in results if r.channel=="Yellow"]) |
| r_n = len([r for r in results if r.channel=="Red"]) |
|
|
| dots = [] |
| for r in results[:80]: |
| s = "fast" if r.total_hours<3 else ("ok" if r.total_hours<24 else ("slow" if r.total_hours<48 else "late")) |
| dots.append({"id":r.shipment_id,"port":r.port_type,"hours":round(r.total_hours,1), |
| "status":s,"ch":r.channel,"machine":r.machine_release}) |
|
|
| dots_j = json.dumps(dots) |
| has_data = "true" if results else "false" |
| art_col = "#1A8A4A" if art<24 else ("#D4750A" if art<48 else "#CC2200") |
| t48_col = "#1A8A4A" if t48>80 else ("#D4750A" if t48>60 else "#CC2200") |
|
|
| return f"""<!DOCTYPE html><html><head><meta charset="utf-8"> |
| <style> |
| *{{margin:0;padding:0;box-sizing:border-box;}} |
| html,body{{width:100%;height:460px;overflow:hidden;background:#EDF2F7; |
| font-family:'Source Sans 3','Segoe UI',sans-serif;}} |
| #wrap{{position:relative;width:100%;height:460px;}} |
| #phaser-canvas{{position:absolute;top:0;left:0;}} |
| #hud-top{{position:absolute;top:0;left:0;right:0;display:flex; |
| justify-content:space-between;padding:10px 14px;pointer-events:none;z-index:20;gap:8px;}} |
| #hud-bot{{position:absolute;bottom:0;left:0;right:0;display:flex;gap:8px; |
| padding:8px 14px;z-index:20;pointer-events:none;align-items:center;flex-wrap:wrap;}} |
| .hp{{background:rgba(0,30,70,0.92);border:1px solid rgba(0,102,204,0.4); |
| border-radius:5px;padding:7px 12px;}} |
| .hl{{font-size:8px;font-weight:700;letter-spacing:.14em;color:#6B8BAE; |
| text-transform:uppercase;margin-bottom:2px;}} |
| .hv{{font-family:'Source Code Pro',monospace;font-size:17px;line-height:1;font-weight:600;}} |
| .hs{{font-size:8px;color:#6B8BAE;margin-top:2px;}} |
| .hr{{display:flex;gap:8px;}} |
| .pd{{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:4px;}} |
| .pd.sea{{background:#0066CC;}}.pd.air{{background:#F5A623;}}.pd.land{{background:#1A8A4A;}} |
| .pn{{font-size:9px;font-weight:700;letter-spacing:.06em;color:#C5D5E8;}} |
| .pc{{font-size:12px;font-family:'Source Code Pro',monospace;color:#3399FF;margin-left:3px;}} |
| .pill{{font-size:8px;font-weight:700;padding:1px 7px;border-radius:8px;margin-right:3px;}} |
| .gp{{background:rgba(26,138,74,0.2);color:#4ABA7A;border:1px solid rgba(26,138,74,0.4);}} |
| .yp{{background:rgba(212,117,10,0.2);color:#F5A623;border:1px solid rgba(212,117,10,0.4);}} |
| .rp{{background:rgba(204,34,0,0.2);color:#FF5533;border:1px solid rgba(204,34,0,0.4);}} |
| #idle{{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%); |
| text-align:center;pointer-events:none;z-index:15;}} |
| #idle p{{font-size:12px;color:#6B8BAE;letter-spacing:.1em;margin-top:6px;}} |
| </style></head> |
| <body><div id="wrap"> |
| <canvas id="phaser-canvas"></canvas> |
| |
| <div id="hud-top"> |
| <div class="hp"> |
| <div class="hl">WCO TRS — Avg Release Time</div> |
| <div class="hv" style="color:{art_col}">{art}h</div> |
| <div class="hs">Median: {med}h | Target: {sp.get('target_sea',48)}h</div> |
| </div> |
| <div class="hr"> |
| <div class="hp"> |
| <div class="hl">RMS Channels</div> |
| <div style="display:flex;gap:3px;margin-top:4px;"> |
| <span class="pill gp">G {g_n}</span> |
| <span class="pill yp">Y {y_n}</span> |
| <span class="pill rp">R {r_n}</span> |
| </div> |
| <div class="hs">{fac}% facilitated</div> |
| </div> |
| <div class="hp"> |
| <div class="hl">Auto-OOC</div> |
| <div class="hv" style="color:{'#1A8A4A' if mach>40 else '#D4750A'}">{mach}%</div> |
| <div class="hs">Machine release</div> |
| </div> |
| <div class="hp"> |
| <div class="hl">WTO TFA ≤{sp.get('target_sea',48)}h</div> |
| <div class="hv" style="color:{t48_col}">{t48}%</div> |
| <div class="hs">Art.7.6.1 achievement</div> |
| </div> |
| <div class="hp"> |
| <div class="hl">AEO Enrolled</div> |
| <div class="hv" style="color:#3399FF">{aeo}%</div> |
| <div class="hs">WCO SAFE Framework</div> |
| </div> |
| </div> |
| </div> |
| |
| <div id="hud-bot"> |
| <div class="hp" style="display:flex;gap:14px;align-items:center;"> |
| <span><span class="pd sea"></span><span class="pn">{sea_label}</span><span class="pc">{sea_n}</span></span> |
| <span><span class="pd air"></span><span class="pn">{air_label}</span><span class="pc">{air_n}</span></span> |
| <span><span class="pd land"></span><span class="pn">{land_label}</span><span class="pc">{land_n}</span></span> |
| </div> |
| <div class="hp" style="margin-left:auto;display:flex;gap:8px;align-items:center;"> |
| <span style="font-size:8px;color:#6B8BAE;letter-spacing:.1em;">LEGEND</span> |
| <span style="font-size:9px;color:#1A8A4A;font-family:'Source Code Pro';">● <3h</span> |
| <span style="font-size:9px;color:#0066CC;font-family:'Source Code Pro';">● <24h</span> |
| <span style="font-size:9px;color:#D4750A;font-family:'Source Code Pro';">● <48h</span> |
| <span style="font-size:9px;color:#CC2200;font-family:'Source Code Pro';">● >48h</span> |
| </div> |
| <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> |
| </div> |
| |
| {'<div id="idle"><div style="font-size:40px;">🌐</div><p>CONFIGURE LEVERS & 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 ''} |
| |
| <script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script> |
| <script> |
| const DOTS={dots_j}, HAS_DATA={has_data}; |
| const SL="{sea_label}", AL="{air_label}", LL="{land_label}"; |
| |
| class Scene extends Phaser.Scene {{ |
| constructor(){{ super({{key:'S'}}); }} |
| create(){{ |
| const W=this.scale.width,H=this.scale.height; |
| |
| // WCO-themed background: light blue-grey |
| this.add.rectangle(W/2,H/2,W,H,0xEDF2F7); |
| |
| // Grid lines subtle |
| const grid=this.add.graphics(); |
| grid.lineStyle(0.5,0xC5D5E8,0.3); |
| for(let x=0;x<W;x+=40){{grid.moveTo(x,0);grid.lineTo(x,H);}} |
| for(let y=0;y<H;y+=40){{grid.moveTo(0,y);grid.lineTo(W,y);}} |
| grid.strokePath(); |
| |
| // Stars (subtle dots for sky) |
| const sg=this.add.graphics(); |
| for(let i=0;i<30;i++){{ |
| sg.fillStyle(0xA0B8D0,0.4); |
| sg.fillCircle(Phaser.Math.Between(0,W),Phaser.Math.Between(0,H*0.3),0.8); |
| }} |
| |
| const IW=52,IH=26; |
| const iso=(c,r)=>({{x:W/2+(c-r)*IW/2,y:150+(c+r)*IH/2}}); |
| |
| const tile=(g,c,r,fill,alpha=1,elev=0)=>{{ |
| const p=iso(c,r); |
| const top=[{{x:p.x,y:p.y-elev}},{{x:p.x+IW/2,y:p.y+IH/2-elev}}, |
| {{x:p.x,y:p.y+IH-elev}},{{x:p.x-IW/2,y:p.y+IH/2-elev}}]; |
| g.fillStyle(fill,alpha);g.fillPoints(top,true); |
| if(elev>0){{ |
| const ci=Phaser.Display.Color.IntegerToColor(fill); |
| const dk=(rv,gv,bv)=>Phaser.Display.Color.GetColor(Math.max(0,rv),Math.max(0,gv),Math.max(0,bv)); |
| g.fillStyle(dk(ci.red-40,ci.green-40,ci.blue-40),alpha); |
| g.fillPoints([{{x:p.x-IW/2,y:p.y+IH/2-elev}},{{x:p.x,y:p.y+IH-elev}}, |
| {{x:p.x,y:p.y+IH}},{{x:p.x-IW/2,y:p.y+IH/2}}],true); |
| g.fillStyle(dk(ci.red-20,ci.green-20,ci.blue-20),alpha); |
| g.fillPoints([{{x:p.x,y:p.y+IH-elev}},{{x:p.x+IW/2,y:p.y+IH/2-elev}}, |
| {{x:p.x+IW/2,y:p.y+IH/2}},{{x:p.x,y:p.y+IH}}],true); |
| }} |
| }}; |
| |
| const ter=this.add.graphics(); |
| // Water — WCO blue |
| for(let r=0;r<9;r++)for(let c=-4;c<2;c++) tile(ter,c,r,0x1A5FA0,0.7); |
| // Lighter water ripples |
| for(let r=1;r<8;r+=2)for(let c=-3;c<1;c++) tile(ter,c,r,0x3399CC,0.25); |
| // Land — light green-grey |
| 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); |
| // Roads |
| for(let c=1;c<14;c++) tile(ter,c,4,0xB8C8D8,1); |
| for(let r=0;r<9;r++) tile(ter,5,r,0xB8C8D8,1); |
| |
| // Buildings — WCO navy |
| const bl=this.add.graphics(); |
| 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);}}; |
| blk(2,1,2,2,22,0x003366); // Sea port customs |
| blk(7,1,3,2,18,0x003870); // Air terminal |
| blk(2,5,2,2,16,0x004433); // Land border |
| |
| // WCO flag colours on buildings |
| const fl=this.add.graphics(); |
| const fp1=iso(3,1); fl.fillStyle(0xF5A623,1); fl.fillRect(fp1.x-2,fp1.y-30,4,14); |
| const fp2=iso(8.5,1); fl.fillStyle(0xF5A623,1); fl.fillRect(fp2.x-2,fp2.y-26,4,12); |
| const fp3=iso(3,5); fl.fillStyle(0xF5A623,1); fl.fillRect(fp3.x-2,fp3.y-24,4,10); |
| |
| // Port labels — navy text |
| const ls={{fontFamily:'Source Sans 3,sans-serif',fontSize:'10px',fontStyle:'bold',letterSpacing:1}}; |
| const p1=iso(2.5,0); this.add.text(p1.x,p1.y-46,SL,{{...ls,color:'#003366'}}).setOrigin(0.5); |
| const p2=iso(8.5,0); this.add.text(p2.x,p2.y-34,AL,{{...ls,color:'#003366'}}).setOrigin(0.5); |
| const p3=iso(3,5.2); this.add.text(p3.x,p3.y-30,LL,{{...ls,color:'#003366'}}).setOrigin(0.5); |
| |
| // Ship (WCO blue) |
| const sh=this.add.graphics(),shp=iso(-1.5,3); |
| sh.fillStyle(0x0066CC,1);sh.fillRect(shp.x-24,shp.y-5,48,12); |
| sh.fillStyle(0x003366,1);sh.fillRect(shp.x-8,shp.y-13,18,8); |
| sh.fillStyle(0xFFFFFF,1);sh.fillRect(shp.x-6,shp.y-11,3,6); |
| this.tweens.add({{targets:sh,y:'-=4',duration:2200,yoyo:true,repeat:-1,ease:'Sine.easeInOut'}}); |
| |
| // Cargo dots |
| const COL={{fast:0x1A8A4A,ok:0x0066CC,slow:0xD4750A,late:0xCC2200}}; |
| const ORI={{Sea:{{c:0,r:3.5}},Air:{{c:9,r:3}},Land:{{c:3.5,r:6.5}}}}; |
| |
| if(HAS_DATA){{ |
| DOTS.forEach((d,i)=>{{ |
| const o=ORI[d.port]; |
| const c=o.c+(Math.random()-0.5)*2.5,r=o.r+(Math.random()-0.5)*2; |
| const pos=iso(c,r),col=COL[d.status]; |
| const g=this.add.graphics(); |
| g.fillStyle(col,0.9);g.fillCircle(pos.x,pos.y,5); |
| g.lineStyle(1,col,0.3);g.strokeCircle(pos.x,pos.y,9); |
| if(d.machine){{g.lineStyle(1.5,0xFFFFFF,0.7);g.strokeCircle(pos.x,pos.y,13);}} |
| this.tweens.add({{targets:g,alpha:{{from:0.5,to:1}},duration:800+i*35,yoyo:true,repeat:-1}}); |
| this.tweens.add({{targets:g,y:'-=3',duration:1200+i*55,yoyo:true,repeat:-1}}); |
| const hz=this.add.circle(pos.x,pos.y,14,0xffffff,0).setInteractive({{useHandCursor:true}}); |
| const tt=this.add.text(pos.x+16,pos.y-16, |
| `${{d.id}}\\n${{d.port}} | ${{d.hours}}h\\nCh:${{d.ch}} | ${{d.status.toUpperCase()}}${{d.machine?' ⚡':''}}`, |
| {{fontFamily:'Source Code Pro,monospace',fontSize:'9px',color:'#FFFFFF', |
| backgroundColor:'#003366EE',padding:{{x:6,y:4}},lineSpacing:3}} |
| ).setVisible(false).setDepth(100); |
| hz.on('pointerover',()=>tt.setVisible(true)); |
| hz.on('pointerout', ()=>tt.setVisible(false)); |
| }}); |
| }} else {{ |
| for(let i=0;i<8;i++){{ |
| const pos=iso(Math.random()*10,Math.random()*7); |
| const g=this.add.graphics(); |
| g.fillStyle(0xC5D5E8,0.5);g.fillCircle(pos.x,pos.y,4); |
| this.tweens.add({{targets:g,alpha:{{from:0.1,to:0.5}},duration:1500+i*200,yoyo:true,repeat:-1}}); |
| }} |
| }} |
| }} |
| update(){{ |
| const el=document.getElementById('sim-clock'); |
| if(el) el.textContent=new Date().toLocaleTimeString('en-GB')+' | WCO TRS LIVE'; |
| }} |
| }} |
| |
| const canvas=document.getElementById('phaser-canvas'); |
| new Phaser.Game({{ |
| type:Phaser.WEBGL, canvas:canvas, |
| width: canvas.parentElement.offsetWidth||900, |
| height:460, |
| transparent:true, |
| scale:{{mode:Phaser.Scale.RESIZE,autoCenter:Phaser.Scale.CENTER_BOTH}}, |
| scene:[Scene], |
| }}); |
| </script> |
| </div></body></html>""" |
|
|
|
|
| |
| |
| |
| def build_trs_chart(results, country_cfg, sp): |
| ports = ["Sea","Air","Land","ALL PORTS"] |
| pm = {"Sea":"Sea","Air":"Air","Land":"Land"} |
| data = {p:{"A":[],"B":[],"C":[],"D":[]} for p in ports} |
|
|
| for r in results: |
| pk = pm[r.port_type] |
| for s,v in [("A",r.seg_prearr),("B",r.seg_customs),("C",r.seg_oga_duty),("D",r.seg_logistics)]: |
| data[pk][s].append(v); data["ALL PORTS"][s].append(v) |
|
|
| avgs = {p:{s:np.mean(v) if v else 0 for s,v in sd.items()} for p,sd in data.items()} |
|
|
| port_labels = [ |
| country_cfg["ports"].get("Sea","Sea"), |
| country_cfg["ports"].get("Air","Air"), |
| country_cfg["ports"].get("Land","Land"), |
| "ALL PORTS" |
| ] |
|
|
| segs = [ |
| ("A","Seg A — Pre-arrival / Lodgement","#003366"), |
| ("B","Seg B — Customs Assessment","#0066CC"), |
| ("C","Seg C — OGA / Duty Payment","#F5A623"), |
| ("D","Seg D — Post-clearance Logistics","#6B8BAE"), |
| ] |
| fig = go.Figure() |
| for sid,sname,col in segs: |
| fig.add_trace(go.Bar( |
| name=sname, x=port_labels, |
| y=[avgs[p][sid] for p in ports], |
| marker_color=col, |
| text=[f"{avgs[p][sid]:.1f}h" for p in ports], |
| textposition="inside", |
| textfont=dict(family="Source Code Pro, monospace",size=10,color="white"), |
| )) |
|
|
| t_sea = country_cfg.get("target_sea",48) |
| t_air = country_cfg.get("target_air",24) |
| fig.add_hline(y=t_sea, line_dash="dash", line_color="#CC2200", line_width=1.5, |
| annotation_text=f"{t_sea}h — Sea/Land Target (WTO TFA)", |
| annotation_font_color="#CC2200", annotation_font_size=9) |
| if t_air != t_sea: |
| fig.add_hline(y=t_air, line_dash="dot", line_color="#D4750A", line_width=1, |
| annotation_text=f"{t_air}h — Air Target", |
| annotation_font_color="#D4750A", annotation_font_size=9) |
| fig.add_hline(y=3, line_dash="dashdot", line_color="#1A8A4A", line_width=1, |
| annotation_text="3h — Jaigaon LCS Best Practice", |
| annotation_font_color="#1A8A4A", annotation_font_size=9) |
|
|
| |
| base = country_cfg.get("baseline_art",{}) |
| base_vals = [base.get("Sea",0),base.get("Air",0),base.get("Land",0), |
| np.mean(list(base.values()))] |
| fig.add_trace(go.Scatter( |
| x=port_labels, y=base_vals, |
| mode="markers", name="Baseline ART (before reform)", |
| marker=dict(symbol="diamond",size=12,color="#CC2200", |
| line=dict(color="white",width=1)), |
| )) |
|
|
| fig.update_layout( |
| barmode="stack", |
| plot_bgcolor="white", paper_bgcolor="#F0F4F8", |
| font=dict(family="Source Sans 3, sans-serif",color="#003366",size=12), |
| title=dict( |
| text="WCO TRS — Release Time by Port Mode (Segments A–D)", |
| font=dict(size=14,color="#003366",family="Source Sans 3"),x=0.5), |
| xaxis=dict(gridcolor="#E8EFF7",linecolor="#C5D5E8",tickfont=dict(size=11)), |
| yaxis=dict(gridcolor="#E8EFF7",linecolor="#C5D5E8", |
| title=dict(text="Hours — Arrival to Physical Release", |
| font=dict(color="#6B8BAE",size=11))), |
| legend=dict(orientation="h",y=-0.28,font=dict(size=10),bgcolor="rgba(0,0,0,0)"), |
| margin=dict(t=60,b=110,l=60,r=20),height=420, |
| ) |
| return fig |
|
|
|
|
| def build_channel_chart(results): |
| ports = ["Sea","Air","Land"] |
| ch_d = {p:{"Green":0,"Yellow":0,"Red":0} for p in ports} |
| for r in results: ch_d[r.port_type][r.channel]+=1 |
| fig = go.Figure() |
| for ch,col in [("Green","#1A8A4A"),("Yellow","#D4750A"),("Red","#CC2200")]: |
| fig.add_trace(go.Bar( |
| name=ch,x=ports,y=[ch_d[p][ch] for p in ports], |
| marker_color=col,marker_opacity=0.85, |
| text=[ch_d[p][ch] for p in ports],textposition="inside", |
| textfont=dict(family="Source Code Pro",size=11,color="white"), |
| )) |
| fig.update_layout( |
| barmode="stack",plot_bgcolor="white",paper_bgcolor="#F0F4F8", |
| font=dict(family="Source Sans 3",color="#003366",size=12), |
| title=dict(text="WCO RMS Channel Distribution by Port", |
| font=dict(size=13,color="#003366"),x=0.5), |
| xaxis=dict(gridcolor="#E8EFF7"), |
| yaxis=dict(gridcolor="#E8EFF7", |
| title=dict(text="BoE Count",font=dict(color="#6B8BAE",size=11))), |
| legend=dict(orientation="h",y=-0.2,font=dict(size=10),bgcolor="rgba(0,0,0,0)"), |
| margin=dict(t=50,b=80,l=50,r=20),height=300, |
| ) |
| return fig |
|
|
|
|
| |
| |
| |
| def build_llm_prompt(results, sp, params, country_cfg): |
| ports_cfg = country_cfg.get("ports",{}) |
| base = country_cfg.get("baseline_art",{}) |
| t_sea = country_cfg.get("target_sea",48) |
|
|
| seg_means = {} |
| for pt in ["Sea","Air","Land"]: |
| sub = [r for r in results if r.port_type==pt] |
| if sub: |
| seg_means[pt] = { |
| "A": round(np.mean([r.seg_prearr for r in sub]),1), |
| "B": round(np.mean([r.seg_customs for r in sub]),1), |
| "C": round(np.mean([r.seg_oga_duty for r in sub]),1), |
| "D": round(np.mean([r.seg_logistics for r in sub]),1), |
| "total": round(np.mean([r.total_hours for r in sub]),1), |
| "baseline": base.get(pt,0), |
| "port_name": ports_cfg.get(pt,pt), |
| } |
|
|
| biggest_seg = {} |
| for pt,d in seg_means.items(): |
| segs = {"Pre-arrival (Seg A)":d["A"],"Customs Assessment (Seg B)":d["B"], |
| "OGA/Duty (Seg C)":d["C"],"Post-clearance (Seg D)":d["D"]} |
| biggest_seg[pt] = max(segs, key=segs.get) |
|
|
| policy_used = [] |
| if params["advance_filing_pct"]>50: policy_used.append(f"Advance Filing ({params['advance_filing_pct']}%)") |
| if params["rms_facilitation_pct"]>50: policy_used.append(f"RMS Facilitation ({params['rms_facilitation_pct']}%)") |
| if params["aeo_enrollment_pct"]>30: policy_used.append(f"AEO Enrollment ({params['aeo_enrollment_pct']}%)") |
| if params["pga_single_window"]: policy_used.append("PGA Single Window") |
| if params["deferred_duty"]: policy_used.append("Deferred Duty") |
| if params["auto_ooc"]: policy_used.append("Auto Out-of-Charge") |
|
|
| prompt = f"""You are a WCO (World Customs Organization) trade facilitation expert and TRS analyst. |
| |
| A Customs administration has run a Time Release Study (TRS) simulation aligned with the WCO TRS Guide v4 2025. |
| |
| COUNTRY / PORT CONFIGURATION: |
| - Country: {country_cfg.get('country_name','Generic Customs Administration')} |
| - Region: {country_cfg.get('region','Global')} |
| - WTO TFA Category: {country_cfg.get('wto_tfa_cat','A')} |
| - Sea target: {t_sea}h | Air target: {country_cfg.get('target_air',24)}h |
| |
| SIMULATION RESULTS SUMMARY: |
| - Total BoEs simulated: {len(results)} |
| - Overall Mean ART: {sp.get('avg_art',0):.1f}h | Median: {sp.get('median_art',0):.1f}h |
| - Within target (≤{t_sea}h): {sp.get('target48_pct',0):.0f}% |
| - Green channel (facilitated): {sp.get('green_pct',0):.0f}% |
| - Machine release (Auto-OOC): {sp.get('machine_pct',0):.0f}% |
| - AEO enrolled: {sp.get('aeo_pct',0):.0f}% |
| |
| SEGMENT BREAKDOWN (Mean hours per port): |
| """ |
| for pt,d in seg_means.items(): |
| improvement = round(d["baseline"]-d["total"],1) if d["baseline"]>0 else 0 |
| prompt += f""" |
| {pt} Port ({d['port_name']}): |
| Baseline ART: {d['baseline']}h → Simulated ART: {d['total']}h (improvement: {improvement}h) |
| 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 |
| Biggest bottleneck: {biggest_seg[pt]} |
| """ |
|
|
| prompt += f""" |
| POLICY LEVERS ACTIVE IN THIS SIMULATION: |
| {', '.join(policy_used) if policy_used else 'None (baseline run)'} |
| |
| Please provide: |
| 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). |
| 2. BOTTLENECK ANALYSIS — for each port, identify the biggest time segment and explain why it matters. |
| 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. |
| 4. NEXT STEPS — a concrete 3-step action plan for the Customs administration. |
| |
| Keep the total response under 500 words. Use clear headings. Be specific and reference WCO instruments by name. |
| """ |
| return prompt |
|
|
|
|
| SYSTEM_PROMPT = """You are a senior WCO (World Customs Organization) trade facilitation adviser with expertise in: |
| - WCO TRS Guide (Version 4, 2025) methodology |
| - WCO Revised Kyoto Convention (RKC) — General Annex Standards |
| - WCO SAFE Framework of Standards |
| - WTO Trade Facilitation Agreement (TFA) Article 7 |
| - WCO Single Window Compendium |
| - Risk Management Guidelines (RMS/CRA) |
| - Authorized Economic Operator (AEO) programmes |
| You give practical, evidence-based advice grounded in WCO instruments. Always cite specific WCO standards.""" |
|
|
|
|
| |
| |
| |
| def render_intro(): |
| st.markdown(""" |
| <div style="background:white;border:1px solid #C5D5E8;border-top:5px solid #003366; |
| border-radius:8px;padding:28px 32px;margin-bottom:20px;"> |
| <div style="display:flex;align-items:center;gap:16px;margin-bottom:16px;"> |
| <div style="background:#003366;color:white;font-size:28px;width:56px;height:56px; |
| border-radius:8px;display:flex;align-items:center;justify-content:center;">🌐</div> |
| <div> |
| <h2 style="margin:0;color:#003366;font-size:1.5rem;">WCO Time Release Study Simulator</h2> |
| <p style="margin:0;color:#6B8BAE;font-size:0.85rem;"> |
| Aligned with WCO TRS Guide Version 4, 2025 · WTO TFA Article 7.6.1 · SAFE Framework |
| </p> |
| </div> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| col1, col2 = st.columns([3,2]) |
|
|
| with col1: |
| st.markdown(""" |
| <div class="wco-card"> |
| <span class="wco-tag">WHAT IS THIS?</span> |
| <p style="margin-top:10px;color:#003366;font-size:0.92rem;line-height:1.7;"> |
| This simulator allows any <strong>Customs administration</strong> to model the impact of |
| trade facilitation policy reforms on cargo release times — before implementing them in the field. |
| It is built on the <strong>WCO Time Release Study (TRS) methodology</strong> (Guide v4, 2025), |
| the internationally recognised tool for measuring border clearance efficiency mandated by |
| <strong>WTO TFA Article 7.6.1</strong>. |
| </p> |
| <p style="color:#003366;font-size:0.92rem;line-height:1.7;"> |
| The simulator models three ports (Sea, Air, Land Border) and measures the four WCO TRS |
| time segments: <strong>Seg A</strong> (Pre-arrival/Lodgement) → |
| <strong>Seg B</strong> (Customs Assessment) → |
| <strong>Seg C</strong> (OGA/Duty Payment) → |
| <strong>Seg D</strong> (Post-clearance/OOC Release). |
| </p> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| st.markdown(""" |
| <div class="wco-card"> |
| <span class="wco-tag">HOW THE SIMULATION WORKS</span> |
| <p style="margin-top:10px;color:#003366;font-size:0.92rem;line-height:1.7;"> |
| The engine uses <strong>SimPy discrete-event simulation</strong> with |
| <strong>Gamma probability distributions</strong> — the same statistical approach |
| recommended in WCO TRS §2.3.1 to replicate the long-tail delay patterns seen in |
| real customs data. Each Bill of Entry (BoE) is a state machine that progresses |
| through WCO business process steps (§2.1.4 Appendix 1). |
| </p> |
| <ul style="color:#003366;font-size:0.88rem;line-height:1.9;margin-left:16px;"> |
| <li><strong>RMS Channels</strong> — Green (auto-facilitated), Yellow (documentary), Red (physical exam)</li> |
| <li><strong>AEO tiers</strong> — T1/T2/T3 per WCO SAFE Framework reduce assessment time</li> |
| <li><strong>OGA intervention</strong> — PGA delays modelled with/without Single Window</li> |
| <li><strong>Advance filing</strong> — Pre-arrival declaration eliminates Segment A entirely</li> |
| <li><strong>Deferred duty</strong> — AEO privilege removes Segment C payment wait</li> |
| <li><strong>Auto-OOC</strong> — Machine release eliminates Segment D queue for Green channel</li> |
| </ul> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| with col2: |
| st.markdown(""" |
| <div class="wco-card" style="border-left-color:#F5A623;"> |
| <span class="wco-tag" style="background:#FEF6E8;color:#D4750A;">HOW TO USE THIS APP</span> |
| <ol style="margin-top:10px;color:#003366;font-size:0.88rem;line-height:1.9;margin-left:16px;"> |
| <li>Select your <strong>country</strong> (or enter custom parameters)</li> |
| <li>Set <strong>policy levers</strong> in the sidebar</li> |
| <li>Click <strong>▶ RUN SIMULATION</strong></li> |
| <li>View the <strong>isometric port map</strong> with live cargo status</li> |
| <li>Analyse the <strong>TRS stacked bar chart</strong> (Segments A–D)</li> |
| <li>Get <strong>AI-powered analysis</strong> with WCO tool recommendations</li> |
| <li>Download the <strong>Audit Ledger CSV</strong> for your records</li> |
| </ol> |
| </div> |
| |
| <div class="wco-card" style="border-left-color:#1A8A4A;"> |
| <span class="wco-tag" style="background:#E8F5EE;color:#1A8A4A;">WHO IS THIS FOR?</span> |
| <ul style="margin-top:10px;color:#003366;font-size:0.88rem;line-height:1.9;margin-left:16px;"> |
| <li>Customs administration <strong>policy officials</strong></li> |
| <li>WCO <strong>TRS Working Group</strong> members</li> |
| <li>Trade facilitation <strong>consultants & advisers</strong></li> |
| <li>National <strong>Single Window</strong> project teams</li> |
| <li>WTO TFA <strong>Category B/C implementation</strong> teams</li> |
| <li>Regional economic community <strong>customs experts</strong></li> |
| </ul> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| |
| with st.expander("📚 WCO Instruments Referenced in This Simulator"): |
| c1,c2,c3 = st.columns(3) |
| with c1: |
| st.markdown(""" |
| **WCO TRS Guide v4 2025** |
| - §2.1.4 Business process model |
| - §2.1.6 Sampling methodology |
| - §2.3.1 Data analysis (mean/median) |
| - §2.3.3 Data visualisation |
| - §2.3.6 Final report format |
| """) |
| with c2: |
| st.markdown(""" |
| **WCO Revised Kyoto Convention** |
| - Standard 3.21 — Advance lodgement |
| - Standard 6.2 — Risk management |
| - Standard 7.2 — AEO benefits |
| - Specific Annex J — AEO |
| """) |
| with c3: |
| st.markdown(""" |
| **WTO TFA & SAFE Framework** |
| - TFA Art. 7.6.1 — ART publication |
| - TFA Art. 7.4 — Risk management |
| - SAFE Pillar 2 — AEO |
| - WCO Single Window Compendium |
| """) |
|
|
|
|
| |
| |
| |
| def main(): |
| |
| st.markdown(""" |
| <div style="background:linear-gradient(135deg,#003366,#0066CC); |
| padding:18px 28px;border-radius:8px;margin-bottom:20px; |
| display:flex;align-items:center;justify-content:space-between;"> |
| <div> |
| <h1 style="color:white;margin:0;font-size:1.7rem;letter-spacing:.04em;"> |
| 🌐 Meridia TRS Simulator |
| </h1> |
| <p style="color:#A0C4E8;margin:4px 0 0;font-size:0.8rem;letter-spacing:.12em;"> |
| WORLD CUSTOMS ORGANIZATION · TIME RELEASE STUDY · GUIDE v4 2025 |
| </p> |
| </div> |
| <div style="text-align:right;"> |
| <div style="background:rgba(255,255,255,0.12);border:1px solid rgba(255,255,255,0.25); |
| border-radius:6px;padding:6px 14px;"> |
| <div style="color:#F5A623;font-size:11px;font-weight:700;letter-spacing:.1em;">WTO TFA ART.7.6.1</div> |
| <div style="color:white;font-size:10px;margin-top:2px;">Compliant simulation methodology</div> |
| </div> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| |
| with st.sidebar: |
| st.markdown("## 🌐 WCO TRS Simulator") |
| st.markdown("<p style='color:#A0C4E8;font-size:0.72rem;letter-spacing:.1em;'>POLICY PARAMETERS</p>", |
| unsafe_allow_html=True) |
|
|
| |
| country_name = st.selectbox("Country / Administration", |
| list(COUNTRY_PRESETS.keys()), index=0) |
| country_cfg = COUNTRY_PRESETS[country_name].copy() |
| country_cfg["country_name"] = country_name |
|
|
| st.markdown("---") |
| st.markdown("**Pre-arrival & Risk**") |
| advance_pct = st.slider("Advance Filing %", 0,100,40, |
| help="WCO RKC Standard 3.21 — pre-arrival declaration") |
| rms_pct = st.slider("RMS Facilitation %", 0,100,50, |
| help="WCO RMS: sets Green channel probability") |
| aeo_pct = st.slider("AEO Enrollment %", 0,100,30, |
| help="WCO SAFE Framework trusted trader programme") |
|
|
| st.markdown("**System Enablers**") |
| pga_sw = st.toggle("PGA Single Window", value=country_cfg.get("existing_sw",False)) |
| deferred = st.toggle("Deferred Duty (AEO)", value=False) |
| auto_ooc = st.toggle("Auto Out-of-Charge", value=False) |
|
|
| st.markdown("**Officer Capacity**") |
| o_sea = st.slider("Sea Officers", 1,20,8) |
| o_air = st.slider("Air Officers", 1,10,4) |
| o_land = st.slider("Land Officers", 1,15,6) |
|
|
| |
| with st.expander("⚙ Advanced Country Config (Optional)"): |
| st.markdown("*Override country defaults below*") |
| st.markdown("**Port Names**") |
| for pt in ["Sea","Air","Land"]: |
| country_cfg["ports"][pt] = st.text_input( |
| f"{pt} Port Name", country_cfg["ports"].get(pt, pt), key=f"port_{pt}") |
|
|
| st.markdown("**Benchmark Targets (hours)**") |
| country_cfg["target_sea"] = st.number_input("Sea/Land target (h)", 12,240, |
| int(country_cfg.get("target_sea",48)), key="ts") |
| country_cfg["target_air"] = st.number_input("Air target (h)", 6,120, |
| int(country_cfg.get("target_air",24)), key="ta") |
|
|
| st.markdown("**Baseline ART (before reforms)**") |
| for pt in ["Sea","Air","Land"]: |
| country_cfg["baseline_art"][pt] = st.number_input( |
| f"{pt} baseline ART (h)", 0, 500, |
| int(country_cfg["baseline_art"].get(pt,48)), key=f"base_{pt}") |
|
|
| st.markdown("**Port Volumes (BoEs per cycle)**") |
| for pt in ["Sea","Air","Land"]: |
| country_cfg["volumes"][pt] = st.number_input( |
| f"{pt} volume", 1, 200, |
| int(country_cfg["volumes"].get(pt,50)), key=f"vol_{pt}") |
|
|
| country_cfg["region"] = st.selectbox("WCO Region", WCO_REGIONS, |
| index=WCO_REGIONS.index(country_cfg.get("region","Global"))) |
| country_cfg["wto_tfa_cat"] = st.selectbox("WTO TFA Category", |
| ["A","B","C"], index=["A","B","C"].index(country_cfg.get("wto_tfa_cat","A"))) |
| params_extra = { |
| "pga_probability": st.slider("OGA involvement %",0,100,35,key="pga_prob") / 100 |
| } |
|
|
| st.markdown("---") |
|
|
| |
| with st.expander("🤖 AI Analysis (OpenRouter API Key)"): |
| api_key = st.text_input("OpenRouter API Key", |
| value=st.session_state.get("api_key",""), |
| type="password", help="Get free key at openrouter.ai") |
| if api_key: |
| st.session_state["api_key"] = api_key |
| st.caption("Uses free-tier models. No cost to you.") |
|
|
| st.markdown("---") |
| run_btn = st.button("▶ RUN SIMULATION", use_container_width=True) |
| reset_btn = st.button("↺ RESET", use_container_width=True) |
|
|
| |
| if "results" not in st.session_state: st.session_state.results = [] |
| if "sim_params" not in st.session_state: st.session_state.sim_params = {} |
| if "country_cfg" not in st.session_state: st.session_state.country_cfg = country_cfg |
| if "llm_result" not in st.session_state: st.session_state.llm_result = "" |
| if "llm_model" not in st.session_state: st.session_state.llm_model = "" |
| if reset_btn: |
| st.session_state.results = []; st.session_state.sim_params = {} |
| st.session_state.llm_result = ""; st.session_state.llm_model = "" |
|
|
| if run_btn: |
| params = dict( |
| advance_filing_pct=advance_pct, rms_facilitation_pct=rms_pct, |
| aeo_enrollment_pct=aeo_pct, pga_single_window=pga_sw, |
| deferred_duty=deferred, auto_ooc=auto_ooc, |
| officers_sea=o_sea, officers_air=o_air, officers_land=o_land, |
| pga_probability=locals().get("params_extra",{}).get("pga_probability",0.35), |
| ) |
| with st.spinner(f"Running WCO TRS simulation for {country_name}..."): |
| results = run_simulation(params, country_cfg) |
|
|
| arts = [r.total_hours for r in results] |
| t_sea = country_cfg.get("target_sea",48) |
| sp = dict( |
| avg_art = float(np.mean(arts)) if arts else 0, |
| median_art = float(np.median(arts)) if arts else 0, |
| green_pct = len([r for r in results if r.channel=="Green"])/len(results)*100 if results else 0, |
| aeo_pct = len([r for r in results if r.aeo_status!="None"])/len(results)*100 if results else 0, |
| machine_pct = len([r for r in results if r.machine_release])/len(results)*100 if results else 0, |
| target48_pct = len([a for a in arts if a<=t_sea])/len(arts)*100 if arts else 0, |
| target24_pct = len([a for a in arts if a<=24])/len(arts)*100 if arts else 0, |
| target_sea = t_sea, |
| ) |
| st.session_state.results = results |
| st.session_state.sim_params = sp |
| st.session_state.country_cfg = country_cfg |
| st.session_state.llm_result = "" |
| st.session_state.sim_params["params"] = params |
|
|
| results = st.session_state.results |
| sim_params = st.session_state.sim_params |
| ccfg = st.session_state.get("country_cfg", country_cfg) |
|
|
| |
| tab0,tab1,tab2,tab3,tab4,tab5 = st.tabs([ |
| "📖 ABOUT", |
| "🗺 PORT VIEW", |
| "📊 TRS REPORT", |
| "📈 RMS CHANNELS", |
| "🤖 AI ANALYSIS", |
| "📋 AUDIT LEDGER", |
| ]) |
|
|
| with tab0: |
| render_intro() |
|
|
| with tab1: |
| st.components.v1.html(build_phaser_scene(results, sim_params, ccfg), |
| height=472, scrolling=False) |
| if results: |
| st.markdown("---") |
| c1,c2,c3,c4,c5 = st.columns(5) |
| c1.metric("Avg Release Time", f"{sim_params['avg_art']:.1f}h", |
| f"Median {sim_params['median_art']:.1f}h") |
| c2.metric("Green Channel", f"{sim_params['green_pct']:.0f}%","RMS Facilitated") |
| c3.metric("Machine Release", f"{sim_params['machine_pct']:.0f}%","Auto-OOC") |
| c4.metric(f"Within {sim_params.get('target_sea',48)}h Target", |
| f"{sim_params['target48_pct']:.0f}%","WTO TFA Art.7.6") |
| c5.metric("AEO Enrolled", f"{sim_params['aeo_pct']:.0f}%","SAFE Framework") |
|
|
| with tab2: |
| if not results: |
| st.info("Run the simulation to generate the WCO TRS chart.") |
| else: |
| st.plotly_chart(build_trs_chart(results, ccfg, sim_params), use_container_width=True) |
|
|
| rows = [] |
| for pt in ["Sea","Air","Land"]: |
| sub = [r for r in results if r.port_type==pt] |
| if not sub: continue |
| rows.append({ |
| "Port": ccfg["ports"].get(pt,pt), |
| "Mode": pt, "n": len(sub), |
| "Seg A (h)": round(np.mean([r.seg_prearr for r in sub]),2), |
| "Seg B (h)": round(np.mean([r.seg_customs for r in sub]),2), |
| "Seg C (h)": round(np.mean([r.seg_oga_duty for r in sub]),2), |
| "Seg D (h)": round(np.mean([r.seg_logistics for r in sub]),2), |
| "Mean ART": round(np.mean([r.total_hours for r in sub]),2), |
| "Median ART":round(np.median([r.total_hours for r in sub]),2), |
| "Baseline": ccfg["baseline_art"].get(pt,"-"), |
| }) |
| st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True) |
|
|
| art = sim_params["avg_art"] |
| t = sim_params.get("target_sea",48) |
| st.markdown("#### Policy Insights") |
| ca,cb = st.columns(2) |
| with ca: |
| if art<3: st.success("🏆 Jaigaon LCS level — ART < 3h. World-class.") |
| elif art<t/2: st.success(f"✅ ART {art:.1f}h — well within {t}h target.") |
| elif art<t: st.warning(f"⚠ ART {art:.1f}h — below {t}h target but improvable.") |
| else: st.error(f"🚨 ART {art:.1f}h — exceeds {t}h WTO TFA target.") |
| with cb: |
| if advance_pct>60 and rms_pct>60: |
| st.success("✅ Advance Filing + RMS >60% — optimal WCO pathway active.") |
| else: |
| st.info("💡 Raise both Advance Filing and RMS above 60% for Jaigaon-level ART.") |
| if not pga_sw: |
| st.warning("⚠ PGA without Single Window causes Segment C bottleneck.") |
|
|
| with tab3: |
| if not results: |
| st.info("Run simulation to see RMS channel data.") |
| else: |
| st.plotly_chart(build_channel_chart(results), use_container_width=True) |
| ch_rows = [] |
| for ch in ["Green","Yellow","Red"]: |
| sub = [r for r in results if r.channel==ch] |
| if sub: |
| ch_rows.append({ |
| "Channel":ch,"Count":len(sub), |
| "Mean ART (h)": round(np.mean([r.total_hours for r in sub]),2), |
| "Median ART (h)":round(np.median([r.total_hours for r in sub]),2), |
| "% Within target":round(len([r for r in sub if r.total_hours<=sim_params.get("target_sea",48)])/len(sub)*100,1), |
| }) |
| st.dataframe(pd.DataFrame(ch_rows), use_container_width=True, hide_index=True) |
|
|
| with tab4: |
| st.markdown("### 🤖 AI-Powered WCO TRS Analysis") |
| st.markdown( |
| "The AI adviser analyses your simulation results and recommends specific " |
| "WCO instruments, conventions, and standards to address your bottlenecks. " |
| "Uses free LLM models via OpenRouter — no cost." |
| ) |
|
|
| if not results: |
| st.info("Run the simulation first, then come back here for AI analysis.") |
| else: |
| api_key_val = st.session_state.get("api_key","") |
| if not api_key_val: |
| st.warning("Enter your OpenRouter API key in the sidebar (free at openrouter.ai) to enable AI analysis.") |
| else: |
| col_btn, col_info = st.columns([2,3]) |
| with col_btn: |
| if st.button("🤖 Generate WCO Analysis", use_container_width=True): |
| prompt = build_llm_prompt( |
| results, sim_params, |
| sim_params.get("params",{}), ccfg |
| ) |
| with st.spinner("Consulting WCO trade facilitation AI adviser..."): |
| text, model_used = call_llm(prompt, api_key_val, SYSTEM_PROMPT) |
| st.session_state.llm_result = text |
| st.session_state.llm_model = model_used |
|
|
| with col_info: |
| st.caption("AI tries 9 free models in order. Typical response: 15–30 seconds.") |
|
|
| if st.session_state.llm_result: |
| st.markdown(f""" |
| <div style="background:white;border:1px solid #C5D5E8;border-top:4px solid #003366; |
| border-radius:8px;padding:24px 28px;margin-top:16px;"> |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;"> |
| <span style="color:#003366;font-weight:700;font-size:1rem;"> |
| WCO Trade Facilitation Analysis |
| </span> |
| <span style="background:#E8EFF7;color:#003366;font-size:10px;font-weight:600; |
| padding:3px 10px;border-radius:10px;letter-spacing:.06em;"> |
| via {st.session_state.llm_model} |
| </span> |
| </div> |
| <div style="color:#003366;font-size:0.92rem;line-height:1.75;white-space:pre-wrap;">{st.session_state.llm_result}</div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| |
| st.download_button( |
| "⬇ Download AI Analysis (TXT)", |
| data=st.session_state.llm_result.encode("utf-8"), |
| file_name=f"WCO_TRS_Analysis_{ccfg.get('country_name','Generic')}.txt", |
| mime="text/plain", |
| ) |
|
|
| with tab5: |
| if not results: |
| st.info("Run simulation to populate the audit ledger.") |
| else: |
| df = pd.DataFrame([{ |
| "Shipment_ID": r.shipment_id, |
| "Port_Mode": r.port_type, |
| "Port_Name": ccfg["ports"].get(r.port_type, r.port_type), |
| "Filing_Type": r.filing_type, |
| "RMS_Channel": r.channel, |
| "OGA_Involved": r.pga_involved, |
| "AEO_Status": r.aeo_status, |
| "Machine_Release": r.machine_release, |
| "Seg_A_PreArr_h": round(r.seg_prearr, 2), |
| "Seg_B_Customs_h": round(r.seg_customs, 2), |
| "Seg_C_OGA_Duty_h": round(r.seg_oga_duty, 2), |
| "Seg_D_Logistics_h": round(r.seg_logistics,2), |
| "Total_Hours": round(r.total_hours, 2), |
| f"Within_{sim_params.get('target_sea',48)}h": r.total_hours<=sim_params.get("target_sea",48), |
| "Within_24h": r.total_hours<=24, |
| } for r in results]) |
|
|
| st.dataframe(df, use_container_width=True, height=380) |
| st.download_button( |
| "⬇ Download Meridia_TRS_Ledger.csv (WCO Format)", |
| data=df.to_csv(index=False).encode("utf-8"), |
| file_name=f"WCO_TRS_Ledger_{ccfg.get('country_name','Generic').replace(' ','_')}.csv", |
| mime="text/csv", use_container_width=True, |
| ) |
| st.markdown(""" |
| <div style="margin-top:12px;padding:10px 16px;background:#F0F4F8; |
| border:1px solid #C5D5E8;border-left:4px solid #0066CC;border-radius:6px; |
| font-size:11px;color:#6B8BAE;line-height:1.7;"> |
| <strong style="color:#003366;">WCO TRS METHODOLOGY NOTE</strong> — |
| Segments per §2.1.4: A=Arrival→Lodgement (T0→T1) · B=Customs Assessment (T1→T2) · |
| C=OGA/Duty Payment (T2→T3) · D=Post-clearance Logistics (T3→T4). |
| Mean & Median both reported per §2.3.1. RMS: Green=auto · Yellow=documentary · Red=physical. |
| WTO TFA Art.7.6.1 benchmarks apply. Data suitable for WCO TRS software import. |
| </div> |
| """, unsafe_allow_html=True) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|