Optitransfer commited on
Commit
ca3826f
·
verified ·
1 Parent(s): 6c75125

Fix Play: Gradio Plot drops Plotly frames; drive playback server-side with gr.Timer

Browse files
Files changed (1) hide show
  1. app.py +88 -93
app.py CHANGED
@@ -69,7 +69,7 @@ and with lost messages. The merge rules guarantee that everyone who has heard th
69
  evidence every honest drone can check for itself, then **dropped automatically**. The rest keep going, and each round
70
  ends with a **receipt** that a quorum of drones co-signs and anyone can check afterwards.
71
 
72
- **Watch the mission.** Press **Run mission**, then hit **Play** on the animation. Left = **WITH** Trusted Swarms (converges, attackers dropped). Right = **WITHOUT** — leader-based (poisoned picture, then blank when the leader is lost).
73
  """
74
 
75
  S4 = f"""
@@ -291,9 +291,11 @@ def _frame_traces(sw, upto):
291
  return left, right, left_note, right_note
292
 
293
 
294
- def _anim_compare(sw):
295
- """Gold-standard side-by-side animated mission: WITH trusted vs WITHOUT leader-based."""
 
296
  n_rounds = len(sw.rounds)
 
297
  xr, yr = _axis_range(sw)
298
  fig = make_subplots(
299
  rows=1, cols=2, shared_xaxes=False, shared_yaxes=True,
@@ -303,121 +305,101 @@ def _anim_compare(sw):
303
  ),
304
  horizontal_spacing=0.06,
305
  )
306
-
307
- # Seed with final frame so static view is complete; frames animate from round 1
308
- left, right, _, _ = _frame_traces(sw, n_rounds)
309
  for tr in left:
310
  fig.add_trace(tr, row=1, col=1)
311
  for tr in right:
312
  fig.add_trace(tr, row=1, col=2)
313
-
314
- frames = []
315
- for upto in range(1, n_rounds + 1):
316
- left, right, ln, rn = _frame_traces(sw, upto)
317
- data = left + right
318
- frames.append(go.Frame(
319
- data=data, name=str(upto),
320
- layout=go.Layout(title_text=f"Mission animation · round {upto} of {n_rounds}"),
321
- ))
322
- fig.frames = frames
323
-
324
- sliders = [dict(
325
- active=n_rounds - 1,
326
- currentvalue=dict(prefix="Round ", font=dict(size=14)),
327
- pad=dict(t=40, b=10),
328
- steps=[dict(method="animate",
329
- args=[[str(i)], dict(mode="immediate", frame=dict(duration=0, redraw=True), transition=dict(duration=0))],
330
- label=str(i)) for i in range(1, n_rounds + 1)],
331
- )]
332
  fig.update_layout(
333
- title=dict(text="Side-by-side mission: press ▶ Play to watch WITH vs WITHOUT", x=0.02, xanchor="left"),
334
- height=560, margin=dict(l=40, r=20, t=80, b=80),
335
  template="plotly_white",
336
  legend=dict(orientation="h", y=1.12, x=0),
337
- updatemenus=[dict(
338
- type="buttons", showactive=False, direction="left",
339
- x=1, xanchor="right", y=1.14, yanchor="top",
340
- buttons=[
341
- dict(label="▶ Play", method="animate",
342
- args=[None, dict(frame=dict(duration=700, redraw=True), fromcurrent=True,
343
- transition=dict(duration=400, easing="cubic-in-out"),
344
- mode="immediate")]),
345
- dict(label="⏸ Pause", method="animate",
346
- args=[[None], dict(frame=dict(duration=0, redraw=False), mode="immediate",
347
- transition=dict(duration=0))]),
348
- dict(label="⟲ Restart", method="animate",
349
- args=[["1"], dict(frame=dict(duration=0, redraw=True), mode="immediate",
350
- transition=dict(duration=0))]),
351
- ],
352
- )],
353
- sliders=sliders,
354
  )
355
  fig.update_xaxes(title_text="metres east", range=xr, row=1, col=1)
356
  fig.update_xaxes(title_text="metres east", range=xr, row=1, col=2)
357
  fig.update_yaxes(title_text="metres north", range=yr, scaleanchor="x", scaleratio=1, row=1, col=1)
358
  fig.update_yaxes(range=yr, scaleanchor="x2", scaleratio=1, row=1, col=2)
359
- # Stronger subplot title styling
360
  for ann in fig.layout.annotations:
361
- if "WITH" in (ann.text or ""):
362
- ann.font = dict(size=13, color="#1f6feb")
363
- elif "WITHOUT" in (ann.text or ""):
364
  ann.font = dict(size=13, color="#d62828")
 
 
365
  return fig
366
 
367
 
368
- def _err_anim(sw):
369
- """Animated error bars building round by round."""
370
  n_rounds = len(sw.rounds)
 
371
  xs = [r["round"] for r in sw.rounds]
372
  te = [r["trusted_err"] if r["trusted_err"] is not None else 0 for r in sw.rounds]
373
  le = [r["lead_err"] if r["lead_err"] is not None else 0 for r in sw.rounds]
374
  lead_miss = [r["lead_err"] is None for r in sw.rounds]
375
-
376
- def bar_frame(upto):
377
- te_v = te[:upto] + [0] * (n_rounds - upto)
378
- le_v = le[:upto] + [0] * (n_rounds - upto)
379
- texts = []
380
- for i in range(n_rounds):
381
- if i >= upto:
382
- texts.append("")
383
- elif lead_miss[i]:
384
- texts.append("leader lost")
385
- else:
386
- texts.append("")
387
- return [
388
- go.Bar(x=xs, y=te_v, name="Trusted (WITH)", marker_color="#1f6feb", legendgroup="t"),
389
- go.Bar(x=xs, y=le_v, name="Leader-based (WITHOUT)", marker_color="#d62828", legendgroup="l",
390
- text=texts, textposition="outside"),
391
- ]
392
-
393
- fig = go.Figure(data=bar_frame(n_rounds))
394
- fig.frames = [go.Frame(data=bar_frame(u), name=str(u)) for u in range(1, n_rounds + 1)]
395
  fig.update_layout(
396
  barmode="group",
397
- title="Error from truth over rounds (metres; lower is better) — ▶ Play",
398
  height=340, margin=dict(l=40, r=20, t=60, b=40), template="plotly_white",
399
- xaxis=dict(title="round", dtick=1), yaxis=dict(title="error (m)"),
 
400
  legend=dict(orientation="h", y=-0.22),
401
- updatemenus=[dict(
402
- type="buttons", showactive=False, direction="left", x=1, xanchor="right", y=1.15, yanchor="top",
403
- buttons=[
404
- dict(label="▶ Play", method="animate",
405
- args=[None, dict(frame=dict(duration=550, redraw=True), fromcurrent=True,
406
- transition=dict(duration=300), mode="immediate")]),
407
- dict(label="⏸ Pause", method="animate",
408
- args=[[None], dict(frame=dict(duration=0), mode="immediate", transition=dict(duration=0))]),
409
- ],
410
- )],
411
- sliders=[dict(
412
- active=n_rounds - 1, currentvalue=dict(prefix="Round "),
413
- steps=[dict(method="animate",
414
- args=[[str(i)], dict(mode="immediate", frame=dict(duration=0, redraw=True), transition=dict(duration=0))],
415
- label=str(i)) for i in range(1, n_rounds + 1)],
416
- )],
417
  )
418
  return fig
419
 
420
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  def _kpis(sw, n, att):
422
  rs = sw.rounds; acc = sum(r["accepted"] for r in rs)
423
  te = [r["trusted_err"] for r in rs if r["trusted_err"] is not None]
@@ -459,8 +441,10 @@ def run(n, poisoner, equivocator, impersonator, loss, lose_lead, rounds, seed):
459
  "cosigners": last["cosigners"], "cosignatures": last["cosignatures"], "public_keys": sw.pki}
460
  path = tempfile.NamedTemporaryFile("w", suffix="-receipt.json", delete=False).name
461
  json.dump(rec, open(path, "w"), indent=2)
462
- return (_story(sw), _kpis(sw, n, att), _anim_compare(sw), _err_anim(sw), sw.trust_table(), log, rec, path, sw,
463
- "Receipt not yet checked. Press **Verify the last receipt**.")
 
 
464
 
465
 
466
  def verify(sw):
@@ -488,10 +472,16 @@ with gr.Blocks(title="Trusted Swarms: leaderless, poison-proof shared mission st
488
  with gr.Column(scale=3):
489
  story = gr.Markdown()
490
  kpis = gr.Markdown()
 
 
 
 
 
 
491
  with gr.Row():
492
- anim = gr.Plot(label="Side-by-side mission animation")
493
  with gr.Row():
494
- er = gr.Plot(label="Error over rounds")
495
  with gr.Row():
496
  with gr.Column():
497
  gr.Markdown("**Trust in each drone** (E4 trust layer; ≤ 0.1 in any dimension = dropped by every honest drone)")
@@ -510,8 +500,13 @@ with gr.Blocks(title="Trusted Swarms: leaderless, poison-proof shared mission st
510
  with gr.Column(elem_classes="sec"):
511
  gr.Markdown(S4); gr.Markdown(S5); gr.Markdown(S6); gr.Markdown(FOOT)
512
  ins = [n, p, e, i, loss, ll, rounds, seed]
513
- outs = [story, kpis, anim, er, tt, lg, rj, fl, state, vo]
514
  go_btn.click(run, ins, outs)
 
 
 
 
 
515
  vb.click(verify, state, vo)
516
  demo.load(run, ins, outs)
517
 
 
69
  evidence every honest drone can check for itself, then **dropped automatically**. The rest keep going, and each round
70
  ends with a **receipt** that a quorum of drones co-signs and anyone can check afterwards.
71
 
72
+ **Watch the mission.** Press **Run mission**, then hit **▶ Play** above the animation. Left = **WITH** Trusted Swarms (converges, attackers dropped). Right = **WITHOUT** — leader-based (poisoned picture, then blank when the leader is lost).
73
  """
74
 
75
  S4 = f"""
 
291
  return left, right, left_note, right_note
292
 
293
 
294
+ def _anim_compare(sw, upto=None):
295
+ """Side-by-side mission at round `upto` (1..N): WITH trusted vs WITHOUT leader-based.
296
+ Gradio's Plot component drops Plotly `frames`, so playback is driven server-side by gr.Timer."""
297
  n_rounds = len(sw.rounds)
298
+ upto = n_rounds if upto is None else max(1, min(int(upto), n_rounds))
299
  xr, yr = _axis_range(sw)
300
  fig = make_subplots(
301
  rows=1, cols=2, shared_xaxes=False, shared_yaxes=True,
 
305
  ),
306
  horizontal_spacing=0.06,
307
  )
308
+ left, right, _, _ = _frame_traces(sw, upto)
 
 
309
  for tr in left:
310
  fig.add_trace(tr, row=1, col=1)
311
  for tr in right:
312
  fig.add_trace(tr, row=1, col=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  fig.update_layout(
314
+ title=dict(text=f"Mission · round {upto} of {n_rounds}", x=0.02, xanchor="left"),
315
+ height=560, margin=dict(l=40, r=20, t=90, b=40),
316
  template="plotly_white",
317
  legend=dict(orientation="h", y=1.12, x=0),
318
+ uirevision="mission",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  )
320
  fig.update_xaxes(title_text="metres east", range=xr, row=1, col=1)
321
  fig.update_xaxes(title_text="metres east", range=xr, row=1, col=2)
322
  fig.update_yaxes(title_text="metres north", range=yr, scaleanchor="x", scaleratio=1, row=1, col=1)
323
  fig.update_yaxes(range=yr, scaleanchor="x2", scaleratio=1, row=1, col=2)
 
324
  for ann in fig.layout.annotations:
325
+ if "WITHOUT" in (ann.text or ""):
 
 
326
  ann.font = dict(size=13, color="#d62828")
327
+ elif "WITH" in (ann.text or ""):
328
+ ann.font = dict(size=13, color="#1f6feb")
329
  return fig
330
 
331
 
332
+ def _err_anim(sw, upto=None):
333
+ """Error bars built up to round `upto`."""
334
  n_rounds = len(sw.rounds)
335
+ upto = n_rounds if upto is None else max(1, min(int(upto), n_rounds))
336
  xs = [r["round"] for r in sw.rounds]
337
  te = [r["trusted_err"] if r["trusted_err"] is not None else 0 for r in sw.rounds]
338
  le = [r["lead_err"] if r["lead_err"] is not None else 0 for r in sw.rounds]
339
  lead_miss = [r["lead_err"] is None for r in sw.rounds]
340
+ te_v = te[:upto] + [0] * (n_rounds - upto)
341
+ le_v = le[:upto] + [0] * (n_rounds - upto)
342
+ texts = ["leader lost" if (i < upto and lead_miss[i]) else "" for i in range(n_rounds)]
343
+ ymax = max(te + le + [1]) * 1.2
344
+ fig = go.Figure(data=[
345
+ go.Bar(x=xs, y=te_v, name="Trusted (WITH)", marker_color="#1f6feb"),
346
+ go.Bar(x=xs, y=le_v, name="Leader-based (WITHOUT)", marker_color="#d62828",
347
+ text=texts, textposition="outside"),
348
+ ])
 
 
 
 
 
 
 
 
 
 
 
349
  fig.update_layout(
350
  barmode="group",
351
+ title=f"Error from truth, rounds 1–{upto} (metres; lower is better)",
352
  height=340, margin=dict(l=40, r=20, t=60, b=40), template="plotly_white",
353
+ xaxis=dict(title="round", dtick=1, range=[0.5, n_rounds + 0.5]),
354
+ yaxis=dict(title="error (m)", range=[0, ymax]),
355
  legend=dict(orientation="h", y=-0.22),
356
+ uirevision="err",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  )
358
  return fig
359
 
360
 
361
+ def _render(sw, rnd):
362
+ if sw is None:
363
+ return gr.update(), gr.update()
364
+ return _anim_compare(sw, rnd), _err_anim(sw, rnd)
365
+
366
+
367
+ def play(sw, rnd):
368
+ if sw is None:
369
+ return gr.update(), gr.update(), gr.update(), gr.Timer(active=False)
370
+ n_rounds = len(sw.rounds)
371
+ rnd = int(rnd or 1)
372
+ if rnd >= n_rounds: # at the end: start over
373
+ rnd = 1
374
+ a, e = _render(sw, rnd)
375
+ return a, e, gr.update(value=rnd, maximum=n_rounds), gr.Timer(active=True)
376
+
377
+
378
+ def tick(sw, rnd):
379
+ if sw is None:
380
+ return gr.update(), gr.update(), gr.update(), gr.Timer(active=False)
381
+ n_rounds = len(sw.rounds)
382
+ rnd = min(int(rnd or 0) + 1, n_rounds)
383
+ a, e = _render(sw, rnd)
384
+ return a, e, gr.update(value=rnd), gr.Timer(active=rnd < n_rounds)
385
+
386
+
387
+ def pause():
388
+ return gr.Timer(active=False)
389
+
390
+
391
+ def restart(sw):
392
+ if sw is None:
393
+ return gr.update(), gr.update(), gr.update(), gr.Timer(active=False)
394
+ a, e = _render(sw, 1)
395
+ return a, e, gr.update(value=1), gr.Timer(active=False)
396
+
397
+
398
+ def scrub(sw, rnd):
399
+ a, e = _render(sw, rnd)
400
+ return a, e, gr.Timer(active=False)
401
+
402
+
403
  def _kpis(sw, n, att):
404
  rs = sw.rounds; acc = sum(r["accepted"] for r in rs)
405
  te = [r["trusted_err"] for r in rs if r["trusted_err"] is not None]
 
441
  "cosigners": last["cosigners"], "cosignatures": last["cosignatures"], "public_keys": sw.pki}
442
  path = tempfile.NamedTemporaryFile("w", suffix="-receipt.json", delete=False).name
443
  json.dump(rec, open(path, "w"), indent=2)
444
+ nr = len(sw.rounds)
445
+ return (_story(sw), _kpis(sw, n, att), _anim_compare(sw, nr), _err_anim(sw, nr), sw.trust_table(), log, rec, path, sw,
446
+ "Receipt not yet checked. Press **Verify the last receipt**.",
447
+ gr.update(value=nr, maximum=nr), gr.Timer(active=False))
448
 
449
 
450
  def verify(sw):
 
472
  with gr.Column(scale=3):
473
  story = gr.Markdown()
474
  kpis = gr.Markdown()
475
+ with gr.Row(equal_height=True):
476
+ play_btn = gr.Button("▶ Play", variant="primary", scale=1, min_width=100)
477
+ pause_btn = gr.Button("⏸ Pause", scale=1, min_width=100)
478
+ restart_btn = gr.Button("⟲ Restart", scale=1, min_width=100)
479
+ rnd = gr.Slider(1, 8, value=8, step=1, label="Round (drag to scrub)", scale=4)
480
+ timer = gr.Timer(0.9, active=False)
481
  with gr.Row():
482
+ anim = gr.Plot(label="Side-by-side mission animation", show_label=False)
483
  with gr.Row():
484
+ er = gr.Plot(label="Error over rounds", show_label=False)
485
  with gr.Row():
486
  with gr.Column():
487
  gr.Markdown("**Trust in each drone** (E4 trust layer; ≤ 0.1 in any dimension = dropped by every honest drone)")
 
500
  with gr.Column(elem_classes="sec"):
501
  gr.Markdown(S4); gr.Markdown(S5); gr.Markdown(S6); gr.Markdown(FOOT)
502
  ins = [n, p, e, i, loss, ll, rounds, seed]
503
+ outs = [story, kpis, anim, er, tt, lg, rj, fl, state, vo, rnd, timer]
504
  go_btn.click(run, ins, outs)
505
+ play_btn.click(play, [state, rnd], [anim, er, rnd, timer], show_progress="hidden")
506
+ timer.tick(tick, [state, rnd], [anim, er, rnd, timer], show_progress="hidden")
507
+ pause_btn.click(pause, None, timer, show_progress="hidden")
508
+ restart_btn.click(restart, state, [anim, er, rnd, timer], show_progress="hidden")
509
+ rnd.release(scrub, [state, rnd], [anim, er, timer], show_progress="hidden")
510
  vb.click(verify, state, vo)
511
  demo.load(run, ins, outs)
512