Jinxious commited on
Commit
89f4f05
·
verified ·
1 Parent(s): e9bb0fe

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +18 -0
  2. app.py +563 -0
  3. packages.txt +3 -0
  4. requirements.txt +9 -0
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ ENV MPLBACKEND=Agg \
4
+ PYTHONDONTWRITEBYTECODE=1 \
5
+ PYTHONUNBUFFERED=1
6
+
7
+ RUN apt-get update \
8
+ && apt-get install -y --no-install-recommends libgfortran5 gfortran libgl1 \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ WORKDIR /app
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ COPY . .
16
+
17
+ EXPOSE 8501
18
+ CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
app.py ADDED
@@ -0,0 +1,563 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime as dt
2
+ import io
3
+ import logging
4
+ import random
5
+ import wave
6
+
7
+ import folium
8
+ import networkx as nx
9
+ import numpy as np
10
+ import pandas as pd
11
+ import streamlit as st
12
+ from folium.plugins import AntPath
13
+ from geopy.distance import geodesic
14
+ from streamlit_folium import st_folium
15
+
16
+
17
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(message)s")
18
+
19
+ SANTA = "\U0001F385"
20
+ SNOWFLAKE = "\u2744\ufe0f"
21
+ SPARKLES = "\u2728"
22
+ GIFT = "\U0001F381"
23
+
24
+ st.set_page_config(
25
+ page_title="Quantum Santa's Path Optimizer",
26
+ page_icon=SANTA,
27
+ layout="wide",
28
+ initial_sidebar_state="expanded",
29
+ )
30
+
31
+
32
+ CITY_DATA = [
33
+ {"name": "North Pole", "lat": 90.0, "lon": 0.0, "tz": 0.0, "icon": SNOWFLAKE, "base_risk": 0.05},
34
+ {"name": "New York", "lat": 40.7128, "lon": -74.0060, "tz": -5.0, "icon": "\U0001F5FD", "base_risk": 0.30},
35
+ {"name": "London", "lat": 51.5074, "lon": -0.1278, "tz": 0.0, "icon": "\U0001F3F0", "base_risk": 0.25},
36
+ {"name": "Tokyo", "lat": 35.6762, "lon": 139.6503, "tz": 9.0, "icon": "\U0001F5FC", "base_risk": 0.35},
37
+ {"name": "Sydney", "lat": -33.8688, "lon": 151.2093, "tz": 10.0, "icon": "\U0001F998", "base_risk": 0.20},
38
+ {"name": "Paris", "lat": 48.8566, "lon": 2.3522, "tz": 1.0, "icon": "\U0001F950", "base_risk": 0.28},
39
+ {"name": "Cairo", "lat": 30.0444, "lon": 31.2357, "tz": 2.0, "icon": "\U0001F9FF", "base_risk": 0.32},
40
+ {"name": "Rio de Janeiro", "lat": -22.9068, "lon": -43.1729, "tz": -3.0, "icon": "\U0001F334", "base_risk": 0.33},
41
+ {"name": "Cape Town", "lat": -33.9249, "lon": 18.4241, "tz": 2.0, "icon": "\U0001F427", "base_risk": 0.22},
42
+ {"name": "Moscow", "lat": 55.7558, "lon": 37.6176, "tz": 3.0, "icon": "\U0001F9CA", "base_risk": 0.27},
43
+ {"name": "Mumbai", "lat": 19.0760, "lon": 72.8777, "tz": 5.5, "icon": "\U0001F54C", "base_risk": 0.38},
44
+ {"name": "Singapore", "lat": 1.3521, "lon": 103.8198, "tz": 8.0, "icon": "\U0001F981", "base_risk": 0.31},
45
+ {"name": "Los Angeles", "lat": 34.0522, "lon": -118.2437, "tz": -8.0, "icon": "\U0001F3AC", "base_risk": 0.29},
46
+ {"name": "Mexico City", "lat": 19.4326, "lon": -99.1332, "tz": -6.0, "icon": "\U0001F32E", "base_risk": 0.34},
47
+ {"name": "Toronto", "lat": 43.6532, "lon": -79.3832, "tz": -5.0, "icon": "\U0001F341", "base_risk": 0.26},
48
+ ]
49
+
50
+
51
+ def build_city_df():
52
+ return pd.DataFrame(CITY_DATA).set_index("name")
53
+
54
+
55
+ def distance_km(city_a, city_b):
56
+ return geodesic((city_a["lat"], city_a["lon"]), (city_b["lat"], city_b["lon"])).km
57
+
58
+
59
+ def build_distance_matrix(city_df):
60
+ names = list(city_df.index)
61
+ n = len(names)
62
+ dist = np.zeros((n, n))
63
+ for i in range(n):
64
+ for j in range(i + 1, n):
65
+ d = distance_km(city_df.loc[names[i]], city_df.loc[names[j]])
66
+ dist[i, j] = d
67
+ dist[j, i] = d
68
+ return dist, names
69
+
70
+
71
+ def route_length(route, dist):
72
+ total = 0.0
73
+ for i in range(len(route) - 1):
74
+ total += dist[route[i], route[i + 1]]
75
+ return total
76
+
77
+
78
+ def nearest_neighbor_route(dist):
79
+ n = dist.shape[0]
80
+ unvisited = set(range(1, n))
81
+ route = [0]
82
+ while unvisited:
83
+ last = route[-1]
84
+ next_city = min(unvisited, key=lambda x: dist[last, x])
85
+ route.append(next_city)
86
+ unvisited.remove(next_city)
87
+ route.append(0)
88
+ return route
89
+
90
+
91
+ def christofides_route(dist):
92
+ n = dist.shape[0]
93
+ g = nx.Graph()
94
+ for i in range(n):
95
+ for j in range(i + 1, n):
96
+ g.add_edge(i, j, weight=dist[i, j])
97
+ cycle = nx.algorithms.approximation.traveling_salesman_problem(
98
+ g,
99
+ weight="weight",
100
+ cycle=True,
101
+ method=nx.algorithms.approximation.christofides,
102
+ )
103
+ return rotate_cycle_start(cycle, 0)
104
+
105
+
106
+ def rotate_cycle_start(route, start_index):
107
+ if route[0] == start_index and route[-1] == start_index:
108
+ return route
109
+ if start_index in route:
110
+ idx = route.index(start_index)
111
+ rotated = route[idx:] + route[1:idx + 1]
112
+ if rotated[0] != start_index:
113
+ rotated = [start_index] + rotated
114
+ if rotated[-1] != start_index:
115
+ rotated.append(start_index)
116
+ return rotated
117
+ return [start_index] + route + [start_index]
118
+
119
+
120
+ def compute_arrivals(route, city_df, dist, start_dt, speed_kmph):
121
+ arrivals = []
122
+ elapsed_hours = 0.0
123
+ names = list(city_df.index)
124
+ for idx, city_idx in enumerate(route):
125
+ city_name = names[city_idx]
126
+ city = city_df.loc[city_name]
127
+ arrival_utc = start_dt + dt.timedelta(hours=elapsed_hours)
128
+ local_time = arrival_utc + dt.timedelta(hours=city["tz"])
129
+ arrivals.append(
130
+ {
131
+ "city": city_name,
132
+ "order": idx + 1,
133
+ "arrival_utc": arrival_utc,
134
+ "local_time": local_time,
135
+ }
136
+ )
137
+ if idx < len(route) - 1:
138
+ leg_km = dist[route[idx], route[idx + 1]]
139
+ elapsed_hours += leg_km / speed_kmph
140
+ return arrivals
141
+
142
+
143
+ def predict_awake_probability(base_prob, hour):
144
+ if 23 <= hour or hour < 5:
145
+ time_factor = 0.3
146
+ elif 5 <= hour < 8:
147
+ time_factor = 0.7
148
+ elif 8 <= hour < 18:
149
+ time_factor = 1.2
150
+ else:
151
+ time_factor = 0.8
152
+ return min(0.95, base_prob * time_factor)
153
+
154
+
155
+ def compute_route_metrics(route, city_df, dist, start_dt, speed_kmph):
156
+ arrivals = compute_arrivals(route, city_df, dist, start_dt, speed_kmph)
157
+ risks = []
158
+ for item in arrivals:
159
+ city = city_df.loc[item["city"]]
160
+ hour = item["local_time"].hour
161
+ risk = predict_awake_probability(city["base_risk"], hour)
162
+ item["risk"] = risk
163
+ risks.append(risk)
164
+ total_distance = route_length(route, dist)
165
+ avg_risk = float(np.mean(risks)) if risks else 0.0
166
+ return total_distance, avg_risk, arrivals
167
+
168
+
169
+ def route_edge_similarity(route_a, route_b):
170
+ edges_a = {(route_a[i], route_a[i + 1]) for i in range(len(route_a) - 1)}
171
+ edges_b = {(route_b[i], route_b[i + 1]) for i in range(len(route_b) - 1)}
172
+ if not edges_a:
173
+ return 0.0
174
+ return len(edges_a & edges_b) / len(edges_a)
175
+
176
+
177
+ def quantum_inspired_tsp(dist, start_dt, strength, city_df, speed_kmph):
178
+ base_route = nearest_neighbor_route(dist)
179
+ candidates = [base_route]
180
+ rng = np.random.default_rng()
181
+ num_candidates = int(10 + 20 * strength)
182
+ for _ in range(num_candidates):
183
+ perm = base_route[1:-1]
184
+ perm = perm.copy()
185
+ swaps = max(1, int(strength * len(perm)))
186
+ for _ in range(swaps):
187
+ i, j = rng.integers(0, len(perm), size=2)
188
+ perm[i], perm[j] = perm[j], perm[i]
189
+ candidate = [0] + perm + [0]
190
+ candidates.append(candidate)
191
+
192
+ costs = []
193
+ for route in candidates:
194
+ dist_km, avg_risk, _ = compute_route_metrics(route, city_df, dist, start_dt, speed_kmph)
195
+ costs.append(dist_km * (1.0 + avg_risk))
196
+
197
+ best_idx = int(np.argmin(costs))
198
+ worst_idx = int(np.argmax(costs))
199
+ best_route = candidates[best_idx]
200
+ worst_route = candidates[worst_idx]
201
+
202
+ amplitudes = []
203
+ for route, cost in zip(candidates, costs):
204
+ weight = 1.0 / (1.0 + cost)
205
+ sim_best = route_edge_similarity(route, best_route)
206
+ sim_worst = route_edge_similarity(route, worst_route)
207
+ interference = (1.0 + 0.5 * sim_best) * (1.0 - 0.3 * sim_worst * strength)
208
+ amplitudes.append(max(1e-6, weight * interference))
209
+
210
+ amplitudes = np.array(amplitudes)
211
+ amplitudes = amplitudes / amplitudes.sum()
212
+
213
+ if random.random() < 0.25 * strength:
214
+ worse_pool = np.argsort(costs)[-max(2, len(candidates) // 4):]
215
+ pick = int(rng.choice(worse_pool))
216
+ return candidates[pick]
217
+
218
+ pick = int(rng.choice(len(candidates), p=amplitudes))
219
+ return candidates[pick]
220
+
221
+
222
+ def build_map(city_df, route, arrivals):
223
+ names = list(city_df.index)
224
+ map_center = [city_df["lat"].mean(), city_df["lon"].mean()]
225
+ fmap = folium.Map(location=map_center, zoom_start=1, tiles="CartoDB dark_matter")
226
+
227
+ risk_lookup = {item["city"]: item["risk"] for item in arrivals}
228
+ coords = []
229
+ for order, city_idx in enumerate(route):
230
+ city_name = names[city_idx]
231
+ city = city_df.loc[city_name]
232
+ coords.append((city["lat"], city["lon"]))
233
+ risk = risk_lookup.get(city_name, 0.0)
234
+ color = "#2ecc71" if risk < 0.2 else "#f1c40f" if risk < 0.5 else "#e74c3c"
235
+ popup = (
236
+ f"<b>{order + 1}. {city_name}</b><br>"
237
+ f"Local time: {arrivals[order]['local_time'].strftime('%H:%M')}<br>"
238
+ f"Awake risk: {risk:.0%}"
239
+ )
240
+ folium.CircleMarker(
241
+ location=(city["lat"], city["lon"]),
242
+ radius=7,
243
+ color=color,
244
+ fill=True,
245
+ fill_color=color,
246
+ popup=popup,
247
+ ).add_to(fmap)
248
+
249
+ folium.PolyLine(coords, weight=3, color="#d4af37", opacity=0.9).add_to(fmap)
250
+ AntPath(coords, color="#e6f1ff", weight=2, delay=800).add_to(fmap)
251
+ return fmap
252
+
253
+
254
+ def santa_summary(route, city_df, total_distance, avg_risk):
255
+ names = list(city_df.index)
256
+ path = " -> ".join(names[i] for i in route)
257
+ return f"Route: {path}\nDistance: {total_distance:.1f} km\nAvg awake risk: {avg_risk:.0%}"
258
+
259
+
260
+ def render_quantum_cards():
261
+ cards = [
262
+ ("Superposition", "Explore many candidate routes at once to mimic quantum states."),
263
+ ("Tunneling", "Occasionally accept worse routes to escape local minima."),
264
+ ("Interference", "Reinforce good paths and dampen weak ones via similarity weighting."),
265
+ ]
266
+ cols = st.columns(3)
267
+ for col, (title, body) in zip(cols, cards):
268
+ with col:
269
+ st.markdown(
270
+ f"""
271
+ <div class="quantum-card">
272
+ <h4>{title}</h4>
273
+ <p>{body}</p>
274
+ </div>
275
+ """,
276
+ unsafe_allow_html=True,
277
+ )
278
+
279
+
280
+ def render_css(christmas_mode):
281
+ glow = "glowBorder 4s ease-in-out infinite alternate" if christmas_mode else "none"
282
+ st.markdown(
283
+ f"""
284
+ <style>
285
+ :root {{
286
+ --red: #8b0000;
287
+ --gold: #d4af37;
288
+ --blue1: #0a192f;
289
+ --blue2: #1a2a6c;
290
+ --ice: #e6f1ff;
291
+ }}
292
+ html, body, [class*="css"] {{
293
+ font-family: "Cinzel", "Georgia", serif;
294
+ }}
295
+ .stApp {{
296
+ background: linear-gradient(135deg, var(--blue1), var(--blue2));
297
+ color: var(--ice);
298
+ }}
299
+ section.main > div {{
300
+ animation: {glow};
301
+ border: 1px solid rgba(212, 175, 55, 0.2);
302
+ border-radius: 16px;
303
+ padding: 1.25rem;
304
+ background: rgba(10, 25, 47, 0.45);
305
+ backdrop-filter: blur(6px);
306
+ }}
307
+ h1, h2, h3 {{
308
+ color: var(--ice);
309
+ }}
310
+ .metric-card {{
311
+ background: rgba(255, 255, 255, 0.08);
312
+ border: 1px solid rgba(212, 175, 55, 0.35);
313
+ border-radius: 14px;
314
+ padding: 1rem;
315
+ text-align: center;
316
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
317
+ }}
318
+ .metric-card h3 {{
319
+ margin-bottom: 0.25rem;
320
+ color: var(--gold);
321
+ }}
322
+ .metric-card p {{
323
+ margin: 0;
324
+ font-size: 1.3rem;
325
+ color: var(--ice);
326
+ }}
327
+ .quantum-card {{
328
+ background: rgba(255, 255, 255, 0.08);
329
+ border: 1px solid rgba(212, 175, 55, 0.35);
330
+ border-radius: 12px;
331
+ padding: 0.75rem 1rem;
332
+ height: 100%;
333
+ }}
334
+ .quantum-card h4 {{
335
+ margin: 0 0 0.5rem 0;
336
+ color: var(--gold);
337
+ }}
338
+ div.stButton > button {{
339
+ background: linear-gradient(120deg, #d4af37, #f7d774);
340
+ color: #2b1b00;
341
+ font-weight: 700;
342
+ border: none;
343
+ padding: 0.6rem 1.4rem;
344
+ border-radius: 999px;
345
+ font-size: 1.1rem;
346
+ }}
347
+ div.stButton > button:hover {{
348
+ filter: brightness(1.05);
349
+ }}
350
+ @keyframes glowBorder {{
351
+ from {{ box-shadow: 0 0 12px rgba(212, 175, 55, 0.2); }}
352
+ to {{ box-shadow: 0 0 24px rgba(212, 175, 55, 0.45); }}
353
+ }}
354
+ .snowflake {{
355
+ position: fixed;
356
+ top: -10px;
357
+ color: rgba(230, 241, 255, 0.8);
358
+ user-select: none;
359
+ z-index: 9999;
360
+ animation: fall 10s linear infinite;
361
+ }}
362
+ @keyframes fall {{
363
+ 0% {{ transform: translateY(-10px); opacity: 0; }}
364
+ 10% {{ opacity: 1; }}
365
+ 100% {{ transform: translateY(110vh); opacity: 0; }}
366
+ }}
367
+ </style>
368
+ """,
369
+ unsafe_allow_html=True,
370
+ )
371
+
372
+ if christmas_mode:
373
+ flakes = "".join(
374
+ f'<div class="snowflake" style="left:{i * 8}%; animation-delay:{i * 0.6}s;">{SNOWFLAKE}</div>'
375
+ for i in range(12)
376
+ )
377
+ st.markdown(flakes, unsafe_allow_html=True)
378
+
379
+
380
+ def render_share_button(text):
381
+ escaped = text.replace("\\", "\\\\").replace("\n", "\\n").replace("'", "\\'").replace('"', '\\"')
382
+ st.components.v1.html(
383
+ f"""
384
+ <div style="display:flex;gap:8px;align-items:center;">
385
+ <button onclick="navigator.clipboard.writeText('{escaped}')"
386
+ style="background:#d4af37;color:#2b1b00;font-weight:700;border:none;padding:10px 16px;border-radius:999px;">
387
+ Copy Route Summary
388
+ </button>
389
+ <span style="color:#e6f1ff;font-size:0.9rem;">Ready to share {GIFT}</span>
390
+ </div>
391
+ """,
392
+ height=60,
393
+ )
394
+
395
+
396
+ @st.cache_data(show_spinner=False)
397
+ def generate_bell_audio():
398
+ sample_rate = 22050
399
+ duration = 1.0
400
+ t = np.linspace(0, duration, int(sample_rate * duration), False)
401
+ tone = 0.45 * np.sin(2 * np.pi * 880 * t) * np.exp(-3 * t)
402
+ audio = np.int16(tone * 32767)
403
+ buffer = io.BytesIO()
404
+ with wave.open(buffer, "wb") as wav_file:
405
+ wav_file.setnchannels(1)
406
+ wav_file.setsampwidth(2)
407
+ wav_file.setframerate(sample_rate)
408
+ wav_file.writeframes(audio.tobytes())
409
+ return buffer.getvalue()
410
+
411
+
412
+ def main():
413
+ today = dt.datetime.utcnow().date()
414
+ christmas_mode = today.month == 12 and today.day in (24, 25)
415
+ render_css(christmas_mode)
416
+
417
+ st.title(f"{SANTA} Quantum Santa's Path Optimizer")
418
+ st.write("Plan Santa's global gift route with quantum-inspired optimization and risk awareness.")
419
+
420
+ city_df = build_city_df()
421
+ dist_check, _ = build_distance_matrix(city_df)
422
+ health_ok = len(city_df.index) >= 15 and np.isfinite(dist_check).all()
423
+
424
+ left, right = st.columns([0.3, 0.7], gap="large")
425
+
426
+ with left:
427
+ st.subheader("Control Deck")
428
+ city_names = list(city_df.index)
429
+ city_labels = {name: f"{city_df.loc[name]['icon']} {name}" for name in city_names}
430
+ selected = st.multiselect(
431
+ "Select 3-15 cities (North Pole required)",
432
+ options=city_names,
433
+ default=["North Pole", "New York", "London", "Tokyo", "Sydney"],
434
+ format_func=lambda x: city_labels[x],
435
+ )
436
+ algo = st.selectbox(
437
+ "Optimization mode",
438
+ ["Quantum-Inspired", "Classic (Christofides)", "Classic (Greedy)"],
439
+ )
440
+ strength = st.slider("Quantum strength", min_value=0.0, max_value=1.0, value=0.7, step=0.05)
441
+ start_time = st.time_input("Departure time (UTC)", value=dt.time(22, 30))
442
+ speed = st.slider("Santa speed (km/h)", 300.0, 1500.0, 900.0, step=50.0)
443
+
444
+ st.markdown("### Quantum Concepts")
445
+ render_quantum_cards()
446
+
447
+ enable_sound = st.checkbox("Enable bell sound (Christmas mode)", value=False)
448
+ optimize_clicked = st.button(f"{SPARKLES} OPTIMIZE ROUTE", use_container_width=True, key="optimize")
449
+
450
+ st.caption(f"Health check: {'OK' if health_ok else 'Issues detected'}")
451
+
452
+ if len(selected) < 3:
453
+ st.error("Pick at least 3 cities to begin the optimization.")
454
+ return
455
+ if len(selected) > 15:
456
+ st.error("Please select 15 cities or fewer for a responsive experience.")
457
+ return
458
+ if "North Pole" not in selected:
459
+ st.error("North Pole must be included as the starting point.")
460
+ return
461
+
462
+ selected = ["North Pole"] + [city for city in selected if city != "North Pole"]
463
+
464
+ if optimize_clicked:
465
+ with st.spinner("Optimizing across quantum states..."):
466
+ progress = st.progress(0)
467
+ for pct in range(0, 90, 15):
468
+ progress.progress(pct)
469
+
470
+ chosen_df = city_df.loc[selected]
471
+ dist, _ = build_distance_matrix(chosen_df)
472
+ start_dt = dt.datetime.combine(today, start_time)
473
+ start_perf = dt.datetime.utcnow()
474
+
475
+ try:
476
+ if algo == "Quantum-Inspired":
477
+ route = quantum_inspired_tsp(dist, start_dt, strength, chosen_df, speed)
478
+ elif algo == "Classic (Christofides)":
479
+ route = christofides_route(dist)
480
+ else:
481
+ route = nearest_neighbor_route(dist)
482
+ except Exception as exc:
483
+ logging.exception("Optimization failed: %s", exc)
484
+ st.error("Optimization failed. Please try a different city set or algorithm.")
485
+ return
486
+
487
+ total_distance, avg_risk, arrivals = compute_route_metrics(
488
+ route, chosen_df, dist, start_dt, speed
489
+ )
490
+ elapsed = (dt.datetime.utcnow() - start_perf).total_seconds()
491
+ progress.progress(100)
492
+
493
+ st.balloons()
494
+ st.session_state["last_result"] = {
495
+ "cities": selected,
496
+ "route": route,
497
+ "dist": dist,
498
+ "arrivals": arrivals,
499
+ "total_distance": total_distance,
500
+ "avg_risk": avg_risk,
501
+ "elapsed": elapsed,
502
+ }
503
+
504
+ if "last_result" in st.session_state:
505
+ result = st.session_state["last_result"]
506
+ chosen_df = city_df.loc[result["cities"]]
507
+ route = result["route"]
508
+ dist = result["dist"]
509
+ arrivals = result["arrivals"]
510
+ total_distance = result["total_distance"]
511
+ avg_risk = result["avg_risk"]
512
+ elapsed = result["elapsed"]
513
+ with right:
514
+ st.subheader("Route Visualization")
515
+ fmap = build_map(chosen_df, route, arrivals)
516
+ st_folium(fmap, width=700, height=520)
517
+
518
+ metrics = st.columns(3)
519
+ metrics[0].markdown(
520
+ f"<div class='metric-card'><h3>Total Distance</h3><p>{total_distance:.0f} km</p></div>",
521
+ unsafe_allow_html=True,
522
+ )
523
+ metrics[1].markdown(
524
+ f"<div class='metric-card'><h3>Average Risk</h3><p>{avg_risk:.0%}</p></div>",
525
+ unsafe_allow_html=True,
526
+ )
527
+ metrics[2].markdown(
528
+ f"<div class='metric-card'><h3>Optimization Time</h3><p>{elapsed:.2f} s</p></div>",
529
+ unsafe_allow_html=True,
530
+ )
531
+
532
+ st.markdown("### Route Details")
533
+ for idx, stop in enumerate(arrivals):
534
+ leg_distance = ""
535
+ if idx < len(route) - 1:
536
+ leg_km = dist[route[idx], route[idx + 1]]
537
+ leg_distance = f" - {leg_km:.0f} km to next"
538
+ st.write(
539
+ f"{stop['order']:02d}. {stop['city']} - "
540
+ f"{stop['local_time'].strftime('%H:%M')} local - "
541
+ f"risk {stop['risk']:.0%}{leg_distance}"
542
+ )
543
+
544
+ summary = santa_summary(route, chosen_df, total_distance, avg_risk)
545
+ render_share_button(summary)
546
+
547
+ if avg_risk > 0.5:
548
+ st.warning("Santa is departing too early. Many kids are still awake.")
549
+ elif avg_risk > 0.3:
550
+ st.info("Consider delaying departure to reduce awake risk.")
551
+ else:
552
+ st.success("Great timing! Most kids are asleep.")
553
+ else:
554
+ with right:
555
+ st.subheader("Route Visualization")
556
+ st.info("Select cities and click OPTIMIZE ROUTE to see the magic.")
557
+
558
+ if christmas_mode and enable_sound:
559
+ st.audio(generate_bell_audio(), format="audio/wav")
560
+
561
+
562
+ if __name__ == "__main__":
563
+ main()
packages.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ libgfortran5
2
+ gfortran
3
+ libgl1
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ streamlit==1.32.0
2
+ numpy==1.26.4
3
+ networkx==3.2.1
4
+ folium==0.17.0
5
+ streamlit-folium==0.18.0
6
+ pandas==2.2.1
7
+ geopy==2.4.1
8
+ matplotlib==3.8.3
9
+ scipy==1.11.4