Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| """ | |
| Live Supply Chain Map — visual dashboard for judges. | |
| Shows a world map with: | |
| - Ports (green=open, red=closed, yellow=reduced) | |
| - Shipping routes (blue=open, red=blocked) | |
| - Shipments moving along routes | |
| - Disruptions highlighted | |
| - Real-time stats panel | |
| Generates an HTML file that opens in browser. | |
| Run: python3 visualize.py | |
| """ | |
| import json | |
| import webbrowser | |
| import os | |
| from world import SupplyChainWorld | |
| from real_data import REAL_PORTS, REAL_ROUTES | |
| def generate_map_html(world: SupplyChainWorld, filename: str = "supply_chain_map.html") -> str: | |
| """Generate an interactive HTML map of the supply chain.""" | |
| # Port data with status colors | |
| ports_js = [] | |
| for p in REAL_PORTS: | |
| status = world.ports.get(p["id"], {}).get("status", "open") | |
| color = {"open": "#22c55e", "closed": "#ef4444", "reduced": "#eab308"}.get(status, "#666") | |
| throughput = p.get("throughput_teu", 0) | |
| ports_js.append({ | |
| "id": p["id"], "name": p["name"], "lat": p["lat"], "lon": p["lon"], | |
| "status": status, "color": color, "throughput": throughput, | |
| "country": p.get("country", ""), | |
| }) | |
| # Route data with status | |
| routes_js = [] | |
| for r in REAL_ROUTES: | |
| src_port = next((p for p in REAL_PORTS if p["id"] == r["from"]), None) | |
| dst_port = next((p for p in REAL_PORTS if p["id"] == r["to"]), None) | |
| if not src_port or not dst_port: | |
| continue | |
| route_info = world.routes.get(r["from"], {}).get(r["to"], {}) | |
| status = route_info.get("status", "open") | |
| color = "#3b82f6" if status == "open" else "#ef4444" | |
| routes_js.append({ | |
| "from_lat": src_port["lat"], "from_lon": src_port["lon"], | |
| "to_lat": dst_port["lat"], "to_lon": dst_port["lon"], | |
| "cost": r["cost_per_teu"], "days": r["transit_days"], | |
| "mode": r.get("mode", "ocean"), "status": status, "color": color, | |
| "carrier": r.get("carrier", ""), | |
| }) | |
| # Shipment positions | |
| shipments_js = [] | |
| for s in world.shipments.values(): | |
| port = world.ports.get(s.current_location, {}) | |
| p_data = next((p for p in REAL_PORTS if p["id"] == s.current_location), None) | |
| if p_data: | |
| color = {"pending": "#fbbf24", "in_transit": "#3b82f6", "delivered": "#22c55e", "lost": "#ef4444"}.get(s.status, "#666") | |
| shipments_js.append({ | |
| "id": s.id, "product": s.product, "value": s.value_usd, | |
| "lat": p_data["lat"], "lon": p_data["lon"], | |
| "status": s.status, "color": color, | |
| }) | |
| # Disruptions | |
| disruptions_js = [ | |
| {"type": d.type, "severity": d.severity, "desc": d.description, | |
| "active": d.active, "start": d.start_day, "end": d.end_day} | |
| for d in world.disruptions | |
| ] | |
| # Stats | |
| total_value = sum(s.value_usd for s in world.shipments.values()) | |
| stats = { | |
| "day": world.day, "total_days": world.total_days, | |
| "pending": sum(1 for s in world.shipments.values() if s.status == "pending"), | |
| "in_transit": sum(1 for s in world.shipments.values() if s.status == "in_transit"), | |
| "delivered": sum(1 for s in world.shipments.values() if s.status == "delivered"), | |
| "lost": sum(1 for s in world.shipments.values() if s.status == "lost"), | |
| "delivered_value": world.delivered_value, | |
| "lost_value": world.lost_value, | |
| "shipping_cost": world.total_shipping_cost, | |
| "total_value": total_value, | |
| } | |
| html = f"""<!DOCTYPE html> | |
| <html><head> | |
| <meta charset="utf-8"> | |
| <title>SupplyChainEnv — Live Map</title> | |
| <style> | |
| * {{ margin:0; padding:0; box-sizing:border-box; }} | |
| body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; }} | |
| .header {{ padding: 16px 24px; background: #1e293b; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; align-items: center; }} | |
| .header h1 {{ font-size: 20px; color: #f8fafc; }} | |
| .header .badge {{ background: #22c55e; color: #000; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }} | |
| .container {{ display: flex; height: calc(100vh - 56px); }} | |
| .map {{ flex: 1; position: relative; background: #1e293b; overflow: hidden; }} | |
| .sidebar {{ width: 360px; background: #0f172a; border-left: 1px solid #334155; overflow-y: auto; padding: 16px; }} | |
| svg {{ width: 100%; height: 100%; }} | |
| .stat-card {{ background: #1e293b; border-radius: 8px; padding: 12px 16px; margin-bottom: 8px; }} | |
| .stat-label {{ font-size: 11px; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; }} | |
| .stat-value {{ font-size: 24px; font-weight: 700; margin-top: 2px; }} | |
| .stat-value.green {{ color: #22c55e; }} | |
| .stat-value.red {{ color: #ef4444; }} | |
| .stat-value.blue {{ color: #3b82f6; }} | |
| .stat-value.yellow {{ color: #eab308; }} | |
| .disruption {{ background: #1e293b; border-left: 3px solid #ef4444; border-radius: 4px; padding: 8px 12px; margin-bottom: 6px; font-size: 13px; }} | |
| .disruption.active {{ border-color: #ef4444; background: #291515; }} | |
| .disruption.upcoming {{ border-color: #eab308; }} | |
| .section-title {{ font-size: 13px; font-weight: 600; color: #94a3b8; margin: 16px 0 8px; text-transform: uppercase; letter-spacing: 1px; }} | |
| .port-label {{ font-size: 10px; fill: #cbd5e1; text-anchor: middle; pointer-events: none; }} | |
| .tooltip {{ position: absolute; background: #1e293b; border: 1px solid #475569; border-radius: 6px; padding: 8px 12px; font-size: 12px; pointer-events: none; display: none; z-index: 10; }} | |
| </style> | |
| </head><body> | |
| <div class="header"> | |
| <h1>SupplyChainEnv — Global Trade Network</h1> | |
| <span class="badge">Day {stats['day']}/{stats['total_days']} | {world.difficulty.upper()}</span> | |
| </div> | |
| <div class="container"> | |
| <div class="map"> | |
| <svg viewBox="-180 -90 360 180" preserveAspectRatio="xMidYMid meet"> | |
| <rect x="-180" y="-90" width="360" height="180" fill="#0c1222"/> | |
| <!-- Simplified world outline --> | |
| <ellipse cx="0" cy="0" rx="170" ry="80" fill="none" stroke="#1e293b" stroke-width="0.5"/> | |
| <!-- Routes --> | |
| {"".join(f'<line x1="{r["from_lon"]}" y1="{-r["from_lat"]}" x2="{r["to_lon"]}" y2="{-r["to_lat"]}" stroke="{r["color"]}" stroke-width="{"0.8" if r["status"]=="open" else "1.2"}" opacity="{"0.4" if r["status"]=="open" else "0.8"}" stroke-dasharray="{"" if r["status"]=="open" else "2,2"}"/>' for r in routes_js)} | |
| <!-- Ports --> | |
| {"".join(f'<circle cx="{p["lon"]}" cy="{-p["lat"]}" r="2.5" fill="{p["color"]}" stroke="#fff" stroke-width="0.3"/><text x="{p["lon"]}" y="{-p["lat"]-4}" class="port-label">{p["name"]}</text>' for p in ports_js)} | |
| <!-- Shipments --> | |
| {"".join(f'<circle cx="{s["lon"]}" cy="{-s["lat"]}" r="1.5" fill="{s["color"]}" opacity="0.9"><animate attributeName="r" values="1;2.5;1" dur="2s" repeatCount="indefinite"/></circle>' for s in shipments_js)} | |
| </svg> | |
| </div> | |
| <div class="sidebar"> | |
| <div class="stat-card"><div class="stat-label">Shipments Pending</div><div class="stat-value yellow">{stats['pending']}</div></div> | |
| <div class="stat-card"><div class="stat-label">In Transit</div><div class="stat-value blue">{stats['in_transit']}</div></div> | |
| <div class="stat-card"><div class="stat-label">Delivered</div><div class="stat-value green">{stats['delivered']} (${stats['delivered_value']:,.0f})</div></div> | |
| <div class="stat-card"><div class="stat-label">Lost</div><div class="stat-value red">{stats['lost']} (${stats['lost_value']:,.0f})</div></div> | |
| <div class="stat-card"><div class="stat-label">Shipping Cost</div><div class="stat-value">${stats['shipping_cost']:,.0f}</div></div> | |
| <div class="stat-card"><div class="stat-label">Total Cargo Value</div><div class="stat-value">${stats['total_value']:,.0f}</div></div> | |
| <div class="section-title">Active Disruptions</div> | |
| {"".join(f'<div class="disruption {"active" if d["active"] else "upcoming"}"><strong>{d["type"].upper()}</strong> [{d["severity"]}]<br/>{d["desc"][:80]}<br/>Day {d["start"]}-{d["end"]}</div>' for d in disruptions_js)} | |
| <div class="section-title">Legend</div> | |
| <div style="font-size:12px; line-height:1.8;"> | |
| <span style="color:#22c55e;">●</span> Port Open | |
| <span style="color:#ef4444;">●</span> Port Closed | |
| <span style="color:#eab308;">●</span> Reduced<br/> | |
| <span style="color:#3b82f6;">▬</span> Route Open | |
| <span style="color:#ef4444;">▬</span> Route Blocked<br/> | |
| <span style="color:#fbbf24;">●</span> Pending | |
| <span style="color:#3b82f6;">●</span> In Transit | |
| <span style="color:#22c55e;">●</span> Delivered | |
| </div> | |
| <div class="section-title" style="margin-top:20px;">Data Sources</div> | |
| <div style="font-size:11px; color:#64748b; line-height:1.6;"> | |
| Port throughput: UNCTAD 2023<br/> | |
| Shipping rates: Freightos Baltic Index Q1 2024<br/> | |
| Disruptions: Lloyd's List, WHO, USGS 2017-2024<br/> | |
| LPI scores: World Bank 2023 | |
| </div> | |
| </div> | |
| </div> | |
| </body></html>""" | |
| with open(filename, "w") as f: | |
| f.write(html) | |
| return os.path.abspath(filename) | |
| def main(): | |
| """Run the demo agent and generate map at each stage.""" | |
| from server.supply_chain_environment import SupplyChainEnvironment | |
| from models import SupplyChainAction | |
| def tool(name, args=None): | |
| return SupplyChainAction(action_type="ToolCallAction", tool_name=name, arguments=args or {}) | |
| env = SupplyChainEnvironment() | |
| env.reset(seed=42, difficulty="hard") | |
| # Route shipments | |
| obs = env.step(tool("view_shipments")) | |
| for s in obs.tool_result["shipments"]: | |
| if s["status"] == "pending": | |
| path_obs = env.step(tool("find_path", {"from_port": s["current_location"], "to_warehouse": s["destination"]})) | |
| path = path_obs.tool_result.get("path") | |
| if path: | |
| env.step(tool("route_shipment", {"shipment_id": s["id"], "route": path})) | |
| # Advance 15 days for interesting state | |
| for _ in range(15): | |
| obs = env.step(tool("advance_day")) | |
| if obs.done: | |
| break | |
| # Generate map | |
| path = generate_map_html(env._world) | |
| print(f"Map generated: {path}") | |
| webbrowser.open(f"file://{path}") | |
| if __name__ == "__main__": | |
| main() | |