File size: 10,400 Bytes
5e5efc0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | #!/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()
|