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" ) # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # WCO COLOUR THEME (WCO brand: dark navy #003366, accent blue #0066CC, # gold/amber #F5A623, white #FFFFFF, light grey #F0F4F8) # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ st.markdown(""" """, unsafe_allow_html=True) # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # COUNTRY BENCHMARK DATABASE (WCO Member data + WTO TFA commitments) # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 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"] # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # OPENROUTER LLM β free tier fallback chain # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 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" # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # DATA MODEL β WCO TRS Guide v4 2025 Β§2.1.4 timestamps # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ @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) # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # SIMULATION ENGINE # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 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"]) # Speed multiplier derived from baseline ART (normalised to 48h sea) 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 # Seg A: Arrival β Lodgement 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 # Seg B: Lodgement β Assessment 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 # Seg C: Assessment β Duty + OGA 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 # Seg D: Payment β OOC 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 # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # PHASER.JS MAP (WCO colour theme: navy + white + blue) # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 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"""
CONFIGURE LEVERS & RUN SIMULATION
WCO TIME RELEASE STUDY SIMULATOR READY
Aligned with WCO TRS Guide Version 4, 2025 Β· WTO TFA Article 7.6.1 Β· SAFE Framework
This simulator allows any Customs administration to model the impact of trade facilitation policy reforms on cargo release times β before implementing them in the field. It is built on the WCO Time Release Study (TRS) methodology (Guide v4, 2025), the internationally recognised tool for measuring border clearance efficiency mandated by WTO TFA Article 7.6.1.
The simulator models three ports (Sea, Air, Land Border) and measures the four WCO TRS time segments: Seg A (Pre-arrival/Lodgement) β Seg B (Customs Assessment) β Seg C (OGA/Duty Payment) β Seg D (Post-clearance/OOC Release).
The engine uses SimPy discrete-event simulation with Gamma probability distributions β 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).
WORLD CUSTOMS ORGANIZATION Β· TIME RELEASE STUDY Β· GUIDE v4 2025
POLICY PARAMETERS
", unsafe_allow_html=True) # Country selector 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) # Advanced β collapsed 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("---") # OpenRouter API key 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) # ββ Session state ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 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) # ββ Tabs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 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