sd4m commited on
Commit
7000fd5
·
verified ·
1 Parent(s): ced1355

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +422 -0
app.py ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # MARS-LOOP Interactive Demo (Gradio + Plotly)
3
+ # --------------------------------------------------
4
+ # Features:
5
+ # - Hero section with big rotating Mars + "Iniciemos simulación" button
6
+ # - Slide 1: Jezero-like crater with blue trash points and an orange bot
7
+ # The bot collects points following an AI-optimized route (k-means sorting + 2-opt path)
8
+ # - Slide 2: "Inside the robot" process animation (Sort -> Shred+Wash -> Pelletize -> Mix -> Form/Print)
9
+ # with live KPIs (mass recovery, water recovery, crew time)
10
+ # - Slide 3: Clean crater "after" view
11
+ #
12
+ # Dependencies: gradio, plotly, numpy, pandas
13
+ # Run: pip install gradio plotly numpy pandas
14
+ # python app.py
15
+ #
16
+ # This file is self-contained (no external images needed).
17
+
18
+ import math
19
+ import numpy as np
20
+ import pandas as pd
21
+ import plotly.graph_objects as go
22
+ import gradio as gr
23
+ from dataclasses import dataclass
24
+
25
+ # --------------------------
26
+ # Helpers: simple k-means
27
+ # --------------------------
28
+ def kmeans(X, k=3, iters=15, seed=42):
29
+ rng = np.random.RandomState(seed)
30
+ # choose random points as initial centroids
31
+ idx = rng.choice(len(X), size=k, replace=False)
32
+ C = X[idx].copy()
33
+ for _ in range(iters):
34
+ # assign
35
+ d = ((X[:, None, :] - C[None, :, :]) ** 2).sum(axis=2) # (n,k)
36
+ labels = d.argmin(axis=1)
37
+ # update
38
+ for j in range(k):
39
+ pts = X[labels == j]
40
+ if len(pts) > 0:
41
+ C[j] = pts.mean(axis=0)
42
+ return labels, C
43
+
44
+ # --------------------------
45
+ # Helpers: route optimization
46
+ # --------------------------
47
+ def nn_route(points):
48
+ """Nearest-neighbor heuristic route through all points (returns indices order)."""
49
+ n = len(points)
50
+ if n == 0:
51
+ return []
52
+ remaining = set(range(n))
53
+ order = [0]
54
+ remaining.remove(0)
55
+ while remaining:
56
+ last = order[-1]
57
+ # choose nearest
58
+ best = min(remaining, key=lambda j: np.linalg.norm(points[j] - points[last]))
59
+ order.append(best)
60
+ remaining.remove(best)
61
+ return order
62
+
63
+ def two_opt(points, order, iters=100):
64
+ """2-opt improvement on an existing route order."""
65
+ n = len(order)
66
+ if n < 4:
67
+ return order
68
+ def route_len(ordr):
69
+ return sum(np.linalg.norm(points[ordr[i]] - points[ordr[(i+1) % n]]) for i in range(n-1))
70
+ best = order[:]
71
+ best_len = route_len(best)
72
+ improved = True
73
+ loops = 0
74
+ while improved and loops < iters:
75
+ improved = False
76
+ loops += 1
77
+ for i in range(1, n-2):
78
+ for k in range(i+1, n-1):
79
+ new_order = best[:i] + best[i:k+1][::-1] + best[k+1:]
80
+ new_len = route_len(new_order)
81
+ if new_len < best_len:
82
+ best, best_len = new_order, new_len
83
+ improved = True
84
+ if not improved:
85
+ break
86
+ return best
87
+
88
+ # --------------------------
89
+ # Data classes
90
+ # --------------------------
91
+ @dataclass
92
+ class Scenario:
93
+ seed: int
94
+ points: np.ndarray # (n,2) trash points
95
+ labels: np.ndarray # material classes per point
96
+ order: list # route visiting order
97
+ unit_mass: float # kg per trash item
98
+ # KPIs
99
+ batch_mass: float
100
+ water_used: float
101
+ water_recov: float
102
+ water_loss: float
103
+ useful_mass: float
104
+ metals_reuse: float
105
+ mass_recovery_pct: float
106
+ crew_time_min: float
107
+
108
+ # --------------------------
109
+ # Scenario generation
110
+ # --------------------------
111
+ def generate_scenario(n_points=60, seed=42, crater_radius=20.0):
112
+ rng = np.random.RandomState(seed)
113
+ # Sample points in an ellipse/valley to evoke crater
114
+ theta = rng.uniform(0, 2*np.pi, size=n_points)
115
+ r = crater_radius * np.sqrt(rng.uniform(0, 1, size=n_points)) # denser center
116
+ # elliptical distortion
117
+ a, b = 1.0, 0.65
118
+ x = a * r * np.cos(theta)
119
+ y = b * r * np.sin(theta) - 2.5 # slight offset
120
+ P = np.stack([x, y], axis=1)
121
+
122
+ # Fake spectral features by mixing position + random
123
+ f1 = (x - x.min()) / (x.max() - x.min() + 1e-6)
124
+ f2 = (y - y.min()) / (y.max() - y.min() + 1e-6)
125
+ f3 = rng.beta(2, 5, size=n_points) # polymer-likeness
126
+ X = np.stack([f1, f2, f3], axis=1)
127
+
128
+ labels, _ = kmeans(X, k=3, iters=20, seed=seed)
129
+
130
+ # Route planning on points (AI optimizer)
131
+ order = two_opt(P, nn_route(P))
132
+
133
+ # KPI model (toy but consistent)
134
+ unit_mass = 1.8 # kg per trash item (avg packaging/textile piece)
135
+ batch_mass = n_points * unit_mass
136
+ # water use per kg + recovery
137
+ water_perkg = 0.7
138
+ water_used = water_perkg * batch_mass
139
+ water_recovery = 0.93
140
+ water_recov = water_used * water_recovery
141
+ water_loss = water_used - water_recov
142
+
143
+ # mass conversion to useful composite including regolith (r=0.3), efficiencies product
144
+ eff = 0.95 * 0.97 * 0.96 * 0.95 * 0.95
145
+ polymer_mass_after = batch_mass * eff
146
+ # regolith fraction in final (r)
147
+ rfrac = 0.30
148
+ final_mass = polymer_mass_after / (1 - rfrac) * 0.98 # 2% trim loss fudge
149
+ metals_reuse = 12.0 # kg available as frames reuse, constant per batch
150
+ useful_mass = final_mass
151
+ mass_recovery_pct = (useful_mass + metals_reuse) / batch_mass * 100.0
152
+
153
+ # crew time estimation
154
+ crew_time = max(6.0, 8.0 + 0.04 * batch_mass)
155
+
156
+ return Scenario(
157
+ seed=seed, points=P, labels=labels, order=order, unit_mass=unit_mass,
158
+ batch_mass=batch_mass, water_used=water_used, water_recov=water_recov, water_loss=water_loss,
159
+ useful_mass=useful_mass, metals_reuse=metals_reuse, mass_recovery_pct=mass_recovery_pct,
160
+ crew_time_min=crew_time
161
+ )
162
+
163
+ # --------------------------
164
+ # Figures
165
+ # --------------------------
166
+ def hero_html():
167
+ # Rotating planet built with pure CSS (no external image)
168
+ # Large, soft gradients evoke the Red Planet.
169
+ return gr.HTML(
170
+ '''
171
+ <style>
172
+ .hero-wrap{height:78vh;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#12090a;}
173
+ .mars{
174
+ width: 420px; height: 420px; border-radius:50%;
175
+ background: radial-gradient( circle at 35% 30%,
176
+ #ffb199 0%, #e06045 35%, #b63a27 55%, #5a1e16 75%, #2a1010 100% );
177
+ box-shadow: 0 0 80px rgba(255,90,50,0.35), inset -30px -40px 80px rgba(0,0,0,0.6);
178
+ position: relative; animation: spin 18s linear infinite;
179
+ }
180
+ .mars:before{
181
+ content:""; position:absolute; inset:0; border-radius:50%;
182
+ background: radial-gradient(circle at 70% 65%, rgba(255,255,255,0.12), rgba(0,0,0,0) 40%);
183
+ filter: blur(1px);
184
+ }
185
+ @keyframes spin{ from{transform: rotate(0deg)} to{transform: rotate(360deg)} }
186
+ h1{font-size:48px; letter-spacing:10px; color:#ffe6df; margin:28px 0 6px; font-weight:300;}
187
+ .sub{color:#ffb8a9; letter-spacing:4px; margin-bottom:22px;}
188
+ .btn-start{
189
+ background:#e06045; color:#170d0d; border:none; padding:14px 26px; border-radius:999px;
190
+ font-weight:700; letter-spacing:1px; cursor:pointer; transition: all .2s ease;
191
+ }
192
+ .btn-start:hover{ transform: translateY(-1px); filter: brightness(1.05); }
193
+ </style>
194
+ <div class="hero-wrap">
195
+ <div class="mars"></div>
196
+ <h1>MARTE</h1>
197
+ <div class="sub">PLANETA ROJO</div>
198
+ <button id="start-sim" class="btn-start">Iniciemos simulación</button>
199
+ <script>
200
+ // Bridge click to Gradio event by triggering a hidden button if exists
201
+ setTimeout(()=>{
202
+ const btn = document.querySelector('button.sr-only-start');
203
+ const real = document.getElementById('start-sim');
204
+ if(btn && real){ real.onclick = ()=> btn.click(); }
205
+ }, 500);
206
+ </script>
207
+ </div>
208
+ '''
209
+ )
210
+
211
+ def crater_animation(scn: Scenario, show_classes=True):
212
+ P = scn.points
213
+ labels = scn.labels
214
+ order = scn.order
215
+
216
+ # Colors by class
217
+ palette = np.array(["deepskyblue", "dodgerblue", "lightskyblue"]) if show_classes else np.array(["deepskyblue"]*3)
218
+ colors = palette[labels]
219
+
220
+ # Robot initial pos at first point
221
+ fig = go.Figure()
222
+
223
+ # crater / landscape (stylized)
224
+ t = np.linspace(0, 2*np.pi, 200)
225
+ a, b = 22, 14
226
+ fig.add_trace(go.Scatter(x=a*np.cos(t), y=b*np.sin(t)-2.5, mode="lines", line=dict(width=2), name="Crater Edge", opacity=0.25))
227
+
228
+ # trash points initial
229
+ fig.add_trace(go.Scatter(x=P[:,0], y=P[:,1], mode="markers",
230
+ marker=dict(size=9, color=colors, opacity=0.95, line=dict(width=0)),
231
+ name="Trash"))
232
+
233
+ # robot
234
+ start = P[order[0]]
235
+ fig.add_trace(go.Scatter(x=[start[0]], y=[start[1]], mode="markers",
236
+ marker=dict(size=18, color="orange", symbol="triangle-up"), name="Bot"))
237
+
238
+ frames = []
239
+ # Animate visiting each point (3 substeps per edge)
240
+ sub = 3
241
+ trash_opacity = np.ones(len(P)) * 0.95
242
+ bx, by = start[0], start[1]
243
+
244
+ for idx in range(1, len(order)):
245
+ p0 = P[order[idx-1]]
246
+ p1 = P[order[idx]]
247
+ for s in range(sub):
248
+ tfrac = (s+1)/sub
249
+ x = p0[0]*(1-tfrac) + p1[0]*tfrac
250
+ y = p0[1]*(1-tfrac) + p1[1]*tfrac
251
+ frames.append(go.Frame(data=[
252
+ # crater edge again
253
+ go.Scatter(x=a*np.cos(t), y=b*np.sin(t)-2.5, mode="lines", line=dict(width=2), opacity=0.25, showlegend=False),
254
+ # trash with current opacity
255
+ go.Scatter(x=P[:,0], y=P[:,1], mode="markers",
256
+ marker=dict(size=9, color=colors, opacity=trash_opacity, line=dict(width=0)), showlegend=False),
257
+ # bot position
258
+ go.Scatter(x=[x], y=[y], mode="markers", marker=dict(size=18, color="orange", symbol="triangle-up"), showlegend=False)
259
+ ]))
260
+
261
+ # when arriving, mark this point as collected (fade it)
262
+ trash_opacity[order[idx]] = 0.1
263
+
264
+ fig.update(frames=frames)
265
+ fig.update_layout(
266
+ title="Jezero Crater • Trash Collection",
267
+ xaxis=dict(visible=False), yaxis=dict(visible=False, scaleanchor="x", scaleratio=1),
268
+ height=520, plot_bgcolor="rgba(18,9,10,1)", paper_bgcolor="rgba(18,9,10,1)",
269
+ font=dict(color="#ffe6df"),
270
+ updatemenus=[dict(type="buttons", x=0.02, y=0.96, buttons=[
271
+ dict(label="Play", method="animate", args=[None]),
272
+ dict(label="Pause", method="animate",
273
+ args=[[None], {"mode":"immediate","frame":{"duration":0,"redraw":False},
274
+ "transition":{"duration":0}}])
275
+ ])],
276
+ showlegend=False
277
+ )
278
+ return fig
279
+
280
+ def process_animation(scn: Scenario):
281
+ stages = ["SORT", "SHRED+WASH", "PELLETIZE", "MIX (REGOLITH)", "FORM/PRINT"]
282
+ x = [0, 1, 2, 3, 4]
283
+ y = [0]*5
284
+
285
+ # Base layout
286
+ fig = go.Figure()
287
+ # boxes
288
+ for i, s in enumerate(stages):
289
+ fig.add_trace(go.Scatter(
290
+ x=[i], y=[0], mode="markers+text",
291
+ marker=dict(size=140, symbol="square", color="#2b1a1a", line=dict(color="#623a35", width=2)),
292
+ text=[s], textfont=dict(color="#ffb8a9"), textposition="middle center", showlegend=False
293
+ ))
294
+ # progress dot (animated)
295
+ frames = []
296
+ nframes = 60
297
+ for f in range(nframes):
298
+ idx = int((f / nframes) * len(stages))
299
+ idx = min(idx, len(stages)-1)
300
+ frames.append(go.Frame(data=[
301
+ go.Scatter(x=[i for i in x], y=[0]*5, mode="markers+text",
302
+ marker=dict(size=140, symbol="square", color=["#3e2321" if j<=idx else "#2b1a1a" for j in range(5)],
303
+ line=dict(color="#623a35", width=2)),
304
+ text=stages, textfont=dict(color="#ffb8a9"), textposition="middle center", showlegend=False),
305
+ go.Scatter(x=[idx], y=[0.55], mode="markers", marker=dict(size=18, color="orange", symbol="triangle-up"), showlegend=False)
306
+ ]))
307
+
308
+ # KPIs as annotations
309
+ kp = scn
310
+ ann = [
311
+ dict(x=0, y=-0.9, text=f"Batch waste: {kp.batch_mass:.1f} kg", showarrow=False, font=dict(color="#ffe6df")),
312
+ dict(x=1, y=-0.9, text=f"Water used: {kp.water_used:.1f} L • Recovered: {kp.water_recov:.1f} L", showarrow=False, font=dict(color="#ffe6df")),
313
+ dict(x=2, y=-0.9, text=f"Useful composite: {kp.useful_mass:.1f} kg", showarrow=False, font=dict(color="#ffe6df")),
314
+ dict(x=3, y=-0.9, text=f"Metals → reuse: {kp.metals_reuse:.1f} kg", showarrow=False, font=dict(color="#ffe6df")),
315
+ dict(x=4, y=-0.9, text=f"Mass recovery: {kp.mass_recovery_pct:.1f}% • Crew time ≤ {kp.crew_time_min:.0f} min", showarrow=False, font=dict(color="#ffe6df")),
316
+ ]
317
+
318
+ fig.update(frames=frames)
319
+ fig.update_layout(
320
+ title="Inside the Bot • MARS-LOOP Process",
321
+ xaxis=dict(visible=False, range=[-0.5, 4.5]),
322
+ yaxis=dict(visible=False, range=[-1.2, 1.2]),
323
+ height=420, plot_bgcolor="rgba(18,9,10,1)", paper_bgcolor="rgba(18,9,10,1)",
324
+ font=dict(color="#ffe6df"),
325
+ updatemenus=[dict(type="buttons", x=0.02, y=0.96, buttons=[
326
+ dict(label="Play", method="animate", args=[None]),
327
+ dict(label="Pause", method="animate",
328
+ args=[[None], {"mode":"immediate","frame":{"duration":0,"redraw":False},
329
+ "transition":{"duration":0}}])
330
+ ])],
331
+ annotations=ann
332
+ )
333
+ return fig
334
+
335
+ def clean_crater(P):
336
+ # stylized empty crater
337
+ fig = go.Figure()
338
+ t = np.linspace(0, 2*np.pi, 200)
339
+ a, b = 22, 14
340
+ fig.add_trace(go.Scatter(x=a*np.cos(t), y=b*np.sin(t)-2.5, mode="lines",
341
+ line=dict(width=2), opacity=0.28, name="Crater"))
342
+ fig.add_annotation(x=0, y=0, text="Área limpia ✅", showarrow=False, font=dict(size=28, color="#b4ffb4"))
343
+ fig.update_layout(
344
+ title="Jezero Crater • After Cleaning",
345
+ xaxis=dict(visible=False), yaxis=dict(visible=False, scaleanchor="x", scaleratio=1),
346
+ height=520, plot_bgcolor="rgba(18,9,10,1)", paper_bgcolor="rgba(18,9,10,1)",
347
+ font=dict(color="#ffe6df"), showlegend=False
348
+ )
349
+ return fig
350
+
351
+ # --------------------------
352
+ # Gradio App
353
+ # --------------------------
354
+ with gr.Blocks(title="MARS-LOOP Interactive", theme=gr.themes.Soft(primary_hue="red")) as demo:
355
+ gr.HTML("<style> .gradio-container {max-width: 980px !important;} </style>")
356
+ state_scn = gr.State() # Scenario object
357
+ state_slide = gr.State(0)
358
+
359
+ # HERO
360
+ hero = hero_html()
361
+ hidden_start_bridge = gr.Button("Iniciar", visible=False, elem_classes=["sr-only-start"])
362
+
363
+ # SLIDE 1
364
+ with gr.Group(visible=False) as slide1:
365
+ gr.Markdown("### 🏜️ Simulación: Jezero Crater (detección y recolección de desechos)")
366
+ with gr.Row():
367
+ show_classes = gr.Checkbox(value=True, label="Mostrar clasificación automática de materiales (IA)")
368
+ seed_in = gr.Slider(1, 9999, value=42, step=1, label="Semilla")
369
+ regen = gr.Button("🔄 Regenerar escenario")
370
+ crater_plot = gr.Plot()
371
+ next1 = gr.Button("Siguiente ➜ Proceso interno")
372
+
373
+ # SLIDE 2
374
+ with gr.Group(visible=False) as slide2:
375
+ gr.Markdown("### ⚙️ Proceso dentro del robot (MARS-LOOP)")
376
+ process_plot = gr.Plot()
377
+ next2 = gr.Button("Siguiente ➜ Cráter limpio")
378
+
379
+ # SLIDE 3
380
+ with gr.Group(visible=False) as slide3:
381
+ gr.Markdown("### ✅ Resultado: Cráter limpio")
382
+ clean_plot = gr.Plot()
383
+ reset = gr.Button("Reiniciar")
384
+
385
+ # ------------------
386
+ # Callbacks
387
+ # ------------------
388
+ def on_start():
389
+ scn = generate_scenario(seed=42)
390
+ fig = crater_animation(scn, show_classes=True)
391
+ return (
392
+ gr.update(visible=False), # hide hero
393
+ gr.update(visible=True), # show slide1
394
+ scn, 1, # state scenario + slide
395
+ gr.update(value=fig) # crater plot
396
+ )
397
+
398
+ def on_regen(seed, show):
399
+ scn = generate_scenario(seed=int(seed))
400
+ fig = crater_animation(scn, show_classes=bool(show))
401
+ return scn, gr.update(value=fig)
402
+
403
+ def go_next1(scn: Scenario):
404
+ fig = process_animation(scn)
405
+ return gr.update(visible=False), gr.update(visible=True), gr.update(value=fig), 2
406
+
407
+ def go_next2(scn: Scenario):
408
+ fig = clean_crater(scn.points)
409
+ return gr.update(visible=False), gr.update(visible=True), gr.update(value=fig), 3
410
+
411
+ def on_reset():
412
+ return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), 0
413
+
414
+ # wire
415
+ hidden_start_bridge.click(on_start, outputs=[hero, slide1, state_scn, state_slide, crater_plot])
416
+ regen.click(on_regen, inputs=[seed_in, show_classes], outputs=[state_scn, crater_plot])
417
+ next1.click(go_next1, inputs=[state_scn], outputs=[slide1, slide2, process_plot, state_slide])
418
+ next2.click(go_next2, inputs=[state_scn], outputs=[slide2, slide3, clean_plot, state_slide])
419
+ reset.click(on_reset, outputs=[slide3, slide2, hero, state_slide])
420
+
421
+ if __name__ == "__main__":
422
+ demo.launch()