kinaar111 commited on
Commit
59bb2ab
·
verified ·
1 Parent(s): 9d595d2

WebGL-safe 2D trajectory fallback + enriched loss plot

Browse files
Files changed (3) hide show
  1. app.py +124 -26
  2. build_info.py +1 -1
  3. demo_core.py +245 -33
app.py CHANGED
@@ -55,6 +55,10 @@ from demo_core import (
55
  LOCAL_RUN_MD,
56
  PlotStreamConfig,
57
  RUN_MODES,
 
 
 
 
58
  get_overview_composite_path,
59
  get_preset_params,
60
  get_sample_guitar_path,
@@ -914,29 +918,46 @@ def _has_opt_history(history: dict | None) -> bool:
914
  return bool(history and history.get("step"))
915
 
916
 
917
- def _plot_3d_on_run_start(mode: str) -> tuple:
918
- """Hide 3D column during Quick Demo runs."""
919
- if mode == "quick":
920
- return gr.update(visible=False), gr.update(visible=True)
 
 
 
 
 
 
 
 
 
921
  return gr.update(visible=True), gr.update(visible=False)
922
 
923
 
924
- def _plot_3d_on_run_finish(mode: str, history: dict | None) -> tuple:
925
- """Reveal full trajectory after Quick Demo completes."""
926
- if mode == "quick":
927
- if _has_opt_history(history):
928
- return gr.update(visible=True), gr.update(visible=False)
929
- return gr.update(visible=False), gr.update(visible=True)
930
- return gr.update(visible=True), gr.update(visible=False)
 
 
 
 
931
 
 
 
932
 
933
- def _plot_3d_for_mode(mode: str, history: dict | None) -> tuple:
934
- """Idle visibility when switching run mode or resetting."""
935
- return _plot_3d_on_run_finish(mode, history)
936
 
 
 
 
 
937
 
938
- def _apply_run_mode_with_3d(mode: str, history: dict | None) -> tuple:
939
- return _apply_run_mode(mode) + _plot_3d_for_mode(mode, history)
 
940
 
941
 
942
  def _nav_to_page(page: str) -> tuple:
@@ -1130,6 +1151,32 @@ WALLPAPER_HEAD = f"""
1130
  }}
1131
  }})();
1132
  </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1133
  {KATEX_CDN}
1134
  """
1135
 
@@ -1359,6 +1406,21 @@ footer {{
1359
  .gradio-container .vqc-run-mode-radio label {{
1360
  line-height: 1.35 !important;
1361
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1362
  .gradio-container .vqc-panel-header-row {{
1363
  display: flex !important;
1364
  align-items: center !important;
@@ -2166,9 +2228,13 @@ def _run_optimize(
2166
  use_fisher_modulation: bool,
2167
  compare_adam: bool,
2168
  run_mode: str,
 
 
2169
  progress: gr.Progress = gr.Progress(track_tqdm=False),
2170
  ):
2171
  plot_cfg = PlotStreamConfig.for_run_mode(run_mode or "quick")
 
 
2172
  stream_fn = run_comparison_stream if compare_adam else run_optimization_stream
2173
  try:
2174
  yield from stream_fn(
@@ -2178,6 +2244,8 @@ def _run_optimize(
2178
  burst_factor_max=float(burst_factor_max),
2179
  use_fisher_modulation=bool(use_fisher_modulation),
2180
  plot_cfg=plot_cfg,
 
 
2181
  progress_cb=progress,
2182
  )
2183
  except Exception as exc:
@@ -2579,9 +2647,23 @@ def build_app() -> gr.Blocks:
2579
  )
2580
 
2581
  opt_history_state = gr.State(None)
 
2582
 
2583
  with gr.Tabs():
2584
  with gr.Tab("OPTIMIZE"):
 
 
 
 
 
 
 
 
 
 
 
 
 
2585
  with gr.Row():
2586
  with gr.Column(
2587
  visible=False,
@@ -2598,9 +2680,9 @@ def build_app() -> gr.Blocks:
2598
  elem_classes=["vqc-plot3d-hint-col"],
2599
  ) as plot_3d_hint_col:
2600
  plot_3d_hint = gr.Markdown(
2601
- "**S³ trajectory (Quick Demo)** — hidden during the run to cut Plotly "
2602
- "payload size. The full stereographic path appears here when optimization "
2603
- "completes.",
2604
  elem_classes=["vqc-plot3d-hint"],
2605
  )
2606
  plot_loss = gr.Plot(label="Loss envelope")
@@ -2717,10 +2799,10 @@ def build_app() -> gr.Blocks:
2717
  logger.exception("Modal synthesis failed")
2718
  return None, None, go.Figure(), friendly_error("synthesis", exc)
2719
 
2720
- def _reset_workspace():
2721
  p = get_run_mode_params("quick")
2722
  spec = RUN_MODES["quick"]
2723
- plot_col, hint_col = _plot_3d_for_mode("quick", None)
2724
  return (
2725
  p["total_steps"],
2726
  p["steps_per_update"],
@@ -2750,14 +2832,14 @@ def build_app() -> gr.Blocks:
2750
  opt_event = (
2751
  run_opt_btn.click(
2752
  _plot_3d_on_run_start,
2753
- [run_mode],
2754
  [plot_3d_col, plot_3d_hint_col],
2755
  )
2756
  .then(
2757
  _run_optimize,
2758
  [
2759
  total_steps, steps_per_update, learning_rate, burst_factor_max,
2760
- use_fisher, compare_adam, run_mode,
2761
  ],
2762
  [plot_3d, plot_loss, plot_vu, plot_twist, opt_status, opt_history_state],
2763
  )
@@ -2768,7 +2850,7 @@ def build_app() -> gr.Blocks:
2768
  )
2769
  .then(
2770
  _plot_3d_on_run_finish,
2771
- [run_mode, opt_history_state],
2772
  [plot_3d_col, plot_3d_hint_col],
2773
  )
2774
  )
@@ -2800,10 +2882,16 @@ def build_app() -> gr.Blocks:
2800
  ]
2801
  run_mode.change(
2802
  _apply_run_mode_with_3d,
2803
- [run_mode, opt_history_state],
2804
  run_mode_outputs,
2805
  )
2806
 
 
 
 
 
 
 
2807
  def _hint_on_dial_change(ts, spu, cmp):
2808
  return _runtime_hint(ts, spu, cmp)
2809
 
@@ -2816,7 +2904,11 @@ def build_app() -> gr.Blocks:
2816
  use_fisher, compare_adam, audio_duration, run_mode, preset_note,
2817
  runtime_hint, opt_status, opt_history_state, plot_3d_col, plot_3d_hint_col,
2818
  ]
2819
- reset_btn.click(_reset_workspace, outputs=reset_outputs)
 
 
 
 
2820
 
2821
  demo_guitar_btn.click(
2822
  _run_demo_guitar,
@@ -2918,6 +3010,12 @@ def build_app() -> gr.Blocks:
2918
  newhere_minimize_btn.click(_minimize_newhere, outputs=newhere_outputs[:3])
2919
  claims_minimize_btn.click(_minimize_claims, outputs=claims_outputs[:3])
2920
  demo.load(_stream_term_boot, outputs=term_keypad_outputs)
 
 
 
 
 
 
2921
 
2922
  gr.Markdown(
2923
  "6-String Optimizer — mystery shell + physics-audio · "
 
55
  LOCAL_RUN_MD,
56
  PlotStreamConfig,
57
  RUN_MODES,
58
+ TRAJECTORY_VIEW_CHOICES,
59
+ WEBGL_PROBE_JS,
60
+ resolve_trajectory_mode,
61
+ webgl_status_markdown,
62
  get_overview_composite_path,
63
  get_preset_params,
64
  get_sample_guitar_path,
 
918
  return bool(history and history.get("step"))
919
 
920
 
921
+ def _trajectory_panel_visibility(
922
+ run_mode: str,
923
+ history: dict | None,
924
+ traj_view: str,
925
+ webgl_ok: bool,
926
+ *,
927
+ running: bool = False,
928
+ ) -> tuple:
929
+ """Hide WebGL 3D column during Quick runs; 2D stays visible live."""
930
+ resolved = resolve_trajectory_mode(traj_view, webgl_ok)
931
+ if run_mode == "quick" and resolved == "3d":
932
+ if running or not _has_opt_history(history):
933
+ return gr.update(visible=False), gr.update(visible=True)
934
  return gr.update(visible=True), gr.update(visible=False)
935
 
936
 
937
+ def _plot_3d_on_run_start(run_mode: str, traj_view: str, webgl_ok: bool) -> tuple:
938
+ return _trajectory_panel_visibility(run_mode, None, traj_view, webgl_ok, running=True)
939
+
940
+
941
+ def _plot_3d_on_run_finish(run_mode: str, history: dict | None, traj_view: str, webgl_ok: bool) -> tuple:
942
+ return _trajectory_panel_visibility(run_mode, history, traj_view, webgl_ok, running=False)
943
+
944
+
945
+ def _plot_3d_for_mode(run_mode: str, history: dict | None, traj_view: str, webgl_ok: bool) -> tuple:
946
+ return _trajectory_panel_visibility(run_mode, history, traj_view, webgl_ok, running=False)
947
+
948
 
949
+ def _apply_run_mode_with_3d(mode: str, history: dict | None, traj_view: str, webgl_ok: bool) -> tuple:
950
+ return _apply_run_mode(mode) + _plot_3d_for_mode(mode, history, traj_view, webgl_ok)
951
 
 
 
 
952
 
953
+ def _on_webgl_probe(probe: str | None = None) -> tuple:
954
+ ok = str(probe if probe is not None else "1") != "0"
955
+ view_up = gr.update(value="2d") if not ok else gr.update()
956
+ return ok, view_up, webgl_status_markdown(ok, "2d" if not ok else "auto")
957
 
958
+
959
+ def _on_trajectory_view_change(view: str, webgl_ok: bool) -> str:
960
+ return webgl_status_markdown(webgl_ok, view)
961
 
962
 
963
  def _nav_to_page(page: str) -> tuple:
 
1151
  }}
1152
  }})();
1153
  </script>
1154
+ <script>
1155
+ (function() {{
1156
+ function markWebGLFailed() {{
1157
+ try {{ localStorage.setItem('6string-webgl-failed', '1'); }} catch (e) {{}}
1158
+ var status = document.querySelector('.vqc-webgl-status');
1159
+ if (status) {{
1160
+ status.innerHTML = '⚠ <strong>WebGL error detected</strong> — switch <strong>Trajectory view</strong> to <strong>2D (WebGL-safe)</strong>.';
1161
+ }}
1162
+ }}
1163
+ function scanPlotlyWebGLErrors() {{
1164
+ var plots = document.querySelectorAll('.vqc-plot3d-panel, .js-plotly-plot');
1165
+ plots.forEach(function(plot) {{
1166
+ var text = plot.innerText || '';
1167
+ if (text.indexOf('WebGL is not supported') >= 0) markWebGLFailed();
1168
+ }});
1169
+ }}
1170
+ setInterval(scanPlotlyWebGLErrors, 2500);
1171
+ var obs = new MutationObserver(scanPlotlyWebGLErrors);
1172
+ function start() {{
1173
+ scanPlotlyWebGLErrors();
1174
+ if (document.body) obs.observe(document.body, {{ childList: true, subtree: true }});
1175
+ }}
1176
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
1177
+ else start();
1178
+ }})();
1179
+ </script>
1180
  {KATEX_CDN}
1181
  """
1182
 
 
1406
  .gradio-container .vqc-run-mode-radio label {{
1407
  line-height: 1.35 !important;
1408
  }}
1409
+ .gradio-container .vqc-webgl-status {{
1410
+ margin: 0.2rem 0 0.45rem 0 !important;
1411
+ padding: 0.45rem 0.65rem !important;
1412
+ border-radius: 8px !important;
1413
+ border: 1px solid rgba(255, 176, 0, 0.25) !important;
1414
+ background: rgba(10, 10, 10, 0.4) !important;
1415
+ font-size: 0.84rem !important;
1416
+ color: #c9b8ff !important;
1417
+ }}
1418
+ .gradio-container .vqc-webgl-status:empty {{
1419
+ display: none !important;
1420
+ }}
1421
+ .gradio-container .vqc-trajectory-view-radio label {{
1422
+ line-height: 1.3 !important;
1423
+ }}
1424
  .gradio-container .vqc-panel-header-row {{
1425
  display: flex !important;
1426
  align-items: center !important;
 
2228
  use_fisher_modulation: bool,
2229
  compare_adam: bool,
2230
  run_mode: str,
2231
+ trajectory_view: str,
2232
+ webgl_ok: bool,
2233
  progress: gr.Progress = gr.Progress(track_tqdm=False),
2234
  ):
2235
  plot_cfg = PlotStreamConfig.for_run_mode(run_mode or "quick")
2236
+ traj_mode = resolve_trajectory_mode(trajectory_view, webgl_ok)
2237
+ webgl_fallback = trajectory_view == "3d" and not webgl_ok
2238
  stream_fn = run_comparison_stream if compare_adam else run_optimization_stream
2239
  try:
2240
  yield from stream_fn(
 
2244
  burst_factor_max=float(burst_factor_max),
2245
  use_fisher_modulation=bool(use_fisher_modulation),
2246
  plot_cfg=plot_cfg,
2247
+ trajectory_mode=traj_mode,
2248
+ trajectory_webgl_fallback=webgl_fallback,
2249
  progress_cb=progress,
2250
  )
2251
  except Exception as exc:
 
2647
  )
2648
 
2649
  opt_history_state = gr.State(None)
2650
+ webgl_ok_state = gr.State(True)
2651
 
2652
  with gr.Tabs():
2653
  with gr.Tab("OPTIMIZE"):
2654
+ gr.Markdown(
2655
+ "Watch the **stereographic Rosenbrock path** toward [1,1,1] — "
2656
+ "colored by loss. **Burst events** (vertical ticks on loss plot) fire when "
2657
+ "a virtual string's stagnation tension triggers a plateau escape."
2658
+ )
2659
+ trajectory_view = gr.Radio(
2660
+ choices=TRAJECTORY_VIEW_CHOICES,
2661
+ value="auto",
2662
+ label="Trajectory view",
2663
+ info="Choose 2D if you see a WebGL error (common in embedded or privacy browsers).",
2664
+ elem_classes=["vqc-trajectory-view-radio"],
2665
+ )
2666
+ webgl_status = gr.Markdown("", elem_classes=["vqc-webgl-status"])
2667
  with gr.Row():
2668
  with gr.Column(
2669
  visible=False,
 
2680
  elem_classes=["vqc-plot3d-hint-col"],
2681
  ) as plot_3d_hint_col:
2682
  plot_3d_hint = gr.Markdown(
2683
+ "**S³ trajectory (Quick Demo + 3D)** — hidden during the run to cut "
2684
+ "Plotly payload. **2D (WebGL-safe)** mode streams live projections here. "
2685
+ "Full 3D path unlocks when optimization completes.",
2686
  elem_classes=["vqc-plot3d-hint"],
2687
  )
2688
  plot_loss = gr.Plot(label="Loss envelope")
 
2799
  logger.exception("Modal synthesis failed")
2800
  return None, None, go.Figure(), friendly_error("synthesis", exc)
2801
 
2802
+ def _reset_workspace(traj_view, webgl_ok):
2803
  p = get_run_mode_params("quick")
2804
  spec = RUN_MODES["quick"]
2805
+ plot_col, hint_col = _plot_3d_for_mode("quick", None, traj_view, webgl_ok)
2806
  return (
2807
  p["total_steps"],
2808
  p["steps_per_update"],
 
2832
  opt_event = (
2833
  run_opt_btn.click(
2834
  _plot_3d_on_run_start,
2835
+ [run_mode, trajectory_view, webgl_ok_state],
2836
  [plot_3d_col, plot_3d_hint_col],
2837
  )
2838
  .then(
2839
  _run_optimize,
2840
  [
2841
  total_steps, steps_per_update, learning_rate, burst_factor_max,
2842
+ use_fisher, compare_adam, run_mode, trajectory_view, webgl_ok_state,
2843
  ],
2844
  [plot_3d, plot_loss, plot_vu, plot_twist, opt_status, opt_history_state],
2845
  )
 
2850
  )
2851
  .then(
2852
  _plot_3d_on_run_finish,
2853
+ [run_mode, opt_history_state, trajectory_view, webgl_ok_state],
2854
  [plot_3d_col, plot_3d_hint_col],
2855
  )
2856
  )
 
2882
  ]
2883
  run_mode.change(
2884
  _apply_run_mode_with_3d,
2885
+ [run_mode, opt_history_state, trajectory_view, webgl_ok_state],
2886
  run_mode_outputs,
2887
  )
2888
 
2889
+ trajectory_view.change(
2890
+ _on_trajectory_view_change,
2891
+ [trajectory_view, webgl_ok_state],
2892
+ [webgl_status],
2893
+ )
2894
+
2895
  def _hint_on_dial_change(ts, spu, cmp):
2896
  return _runtime_hint(ts, spu, cmp)
2897
 
 
2904
  use_fisher, compare_adam, audio_duration, run_mode, preset_note,
2905
  runtime_hint, opt_status, opt_history_state, plot_3d_col, plot_3d_hint_col,
2906
  ]
2907
+ reset_btn.click(
2908
+ _reset_workspace,
2909
+ [trajectory_view, webgl_ok_state],
2910
+ outputs=reset_outputs,
2911
+ )
2912
 
2913
  demo_guitar_btn.click(
2914
  _run_demo_guitar,
 
3010
  newhere_minimize_btn.click(_minimize_newhere, outputs=newhere_outputs[:3])
3011
  claims_minimize_btn.click(_minimize_claims, outputs=claims_outputs[:3])
3012
  demo.load(_stream_term_boot, outputs=term_keypad_outputs)
3013
+ demo.load(
3014
+ _on_webgl_probe,
3015
+ inputs=None,
3016
+ outputs=[webgl_ok_state, trajectory_view, webgl_status],
3017
+ js=WEBGL_PROBE_JS,
3018
+ )
3019
 
3020
  gr.Markdown(
3021
  "6-String Optimizer — mystery shell + physics-audio · "
build_info.py CHANGED
@@ -1,4 +1,4 @@
1
  """Auto-generated by scripts/deploy_hf_space.sh — do not edit by hand."""
2
 
3
  BUILD_COMMIT = "97b4a81"
4
- BUILD_UPDATED_UTC = "2026-06-28T17:08:06Z"
 
1
  """Auto-generated by scripts/deploy_hf_space.sh — do not edit by hand."""
2
 
3
  BUILD_COMMIT = "97b4a81"
4
+ BUILD_UPDATED_UTC = "2026-06-28T17:15:51Z"
demo_core.py CHANGED
@@ -12,6 +12,7 @@ from typing import Any, Generator
12
  import numpy as np
13
  import plotly.express as px
14
  import plotly.graph_objects as go
 
15
  import pandas as pd
16
  import torch
17
 
@@ -51,6 +52,7 @@ SIMULATION_BANNER_MD = """
51
 
52
  KNOWN_LIMITATIONS_BODY = """
53
  - **CPU only** — optimization + Plotly streaming can feel slow; use **Quick Demo** first.
 
54
  - **Cold starts** — first load after sleep may take 30–60s to build.
55
  - **ETA is approximate** — actual time varies with server load and concurrent users.
56
  - **Compare vs RiemannianAdam** doubles wall time; capped at 3000 steps on HF.
@@ -58,6 +60,24 @@ KNOWN_LIMITATIONS_BODY = """
58
  - **Best experience:** desktop Chrome/Firefox · let Quick Demo finish before switching tabs.
59
  """.strip()
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  KNOWN_LIMITATIONS_MD = f"### Known limitations (HF free tier)\n{KNOWN_LIMITATIONS_BODY}"
62
 
63
  LOCAL_RUN_MD = """
@@ -419,6 +439,30 @@ class PlotStreamConfig:
419
  return cls(trajectory_every=1, trajectory_max_points=400, loss_max_points=1500, live_3d=True)
420
 
421
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  def _subsample_df(df: pd.DataFrame, max_points: int) -> pd.DataFrame:
423
  if len(df) <= max_points:
424
  return df
@@ -445,6 +489,10 @@ def friendly_error(context: str, exc: Exception) -> str:
445
  "asset": (
446
  f"Missing bundled asset ({msg}). Re-deploy the Space or use the GitHub repo copy."
447
  ),
 
 
 
 
448
  }
449
  lead = hints.get(context, "Something went wrong.")
450
  return f"⚠ {lead}\n\nDetails: {msg[:300]}"
@@ -521,6 +569,73 @@ def _build_hierarchical_optimizer(
521
  return opt
522
 
523
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  def _make_trajectory_figure(
525
  df: pd.DataFrame,
526
  *,
@@ -572,18 +687,95 @@ def _make_trajectory_figure(
572
  return fig
573
 
574
 
575
- def _make_loss_figure(df: pd.DataFrame, *, max_points: int | None = None) -> go.Figure:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  plot_df = _subsample_df(df, max_points) if max_points else df
577
- fig = px.line(plot_df, x="step", y="loss", title="Loss Envelope (log scale)", log_y=True)
 
 
 
 
 
 
 
 
578
  fig.update_traces(line=dict(color="#FFB000", width=2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
579
  fig.update_layout(
580
  paper_bgcolor="#0a0a0a",
581
  plot_bgcolor="#111318",
582
  font=dict(color="#FFB000"),
583
- xaxis=dict(gridcolor="#333"),
584
- yaxis=dict(gridcolor="#333"),
585
  height=280,
586
- margin=dict(l=40, r=20, t=40, b=30),
587
  )
588
  return fig
589
 
@@ -657,26 +849,42 @@ def _build_live_figures(
657
  total_steps: int,
658
  last_traj: go.Figure | None,
659
  loss_builder,
 
 
 
660
  ) -> tuple[go.Figure, go.Figure, go.Figure, go.Figure]:
661
  """Throttle expensive 3D redraws; subsample points for 2D plots."""
662
  final = step >= total_steps
663
  lite = not final
664
- loss_fig = _make_loss_figure(df, max_points=None if final else plot_cfg.loss_max_points)
 
 
 
 
 
 
665
  vu_fig = _make_string_vu_figure(monitor)
666
  twist_fig = _make_twist_figure(df, max_points=None if final else plot_cfg.loss_max_points)
667
 
668
- refresh_3d = final or (update_idx % plot_cfg.trajectory_every == 0)
669
- if not plot_cfg.live_3d and not final:
 
670
  traj_fig = last_traj or _trajectory_placeholder()
671
- elif refresh_3d:
672
- traj_fig = _make_trajectory_figure(
673
  df,
 
674
  max_points=None if final else plot_cfg.trajectory_max_points,
675
  lite=lite and not final,
 
676
  )
677
  else:
678
- traj_fig = last_traj or _make_trajectory_figure(
679
- df, max_points=plot_cfg.trajectory_max_points, lite=True,
 
 
 
 
680
  )
681
 
682
  if loss_builder is not None:
@@ -693,6 +901,8 @@ def run_optimization_stream(
693
  burst_factor_max: float,
694
  use_fisher_modulation: bool,
695
  plot_cfg: PlotStreamConfig | None = None,
 
 
696
  progress_cb=None,
697
  ) -> Generator[tuple, None, None]:
698
  import time
@@ -768,6 +978,9 @@ def run_optimization_stream(
768
  total_steps=total_steps,
769
  last_traj=last_traj,
770
  loss_builder=None,
 
 
 
771
  )
772
  last_traj = traj_fig
773
  yield (traj_fig, loss_fig, vu_fig, twist_fig, status, dict(monitor.history))
@@ -833,6 +1046,8 @@ def run_comparison_stream(
833
  burst_factor_max: float,
834
  use_fisher_modulation: bool,
835
  plot_cfg: PlotStreamConfig | None = None,
 
 
836
  progress_cb=None,
837
  ) -> Generator[tuple, None, None]:
838
  """Side-by-side burst optimizer vs geoopt RiemannianAdam on identical Rosenbrock S³ init."""
@@ -914,26 +1129,23 @@ def run_comparison_stream(
914
  f"ADAM {loss_a:.6f} (best {best_a:.6f}) │ Δ {loss_a - loss_b:+.4f} │ {eta}"
915
  )
916
  update_idx += 1
917
- final = step >= cap
918
- lite = not final
919
- loss_max = None if final else plot_cfg.loss_max_points
920
- loss_fig = _make_dual_loss_figure(hist_burst, hist_adam, max_points=loss_max)
921
- vu_fig = _make_string_vu_figure(monitor)
922
- twist_fig = _make_twist_figure(df, max_points=loss_max)
923
-
924
- refresh_3d = final or (update_idx % plot_cfg.trajectory_every == 0)
925
- if not plot_cfg.live_3d and not final:
926
- traj_fig = last_traj or _trajectory_placeholder("3D paused in Quick mode — dual loss & VU update live")
927
- elif refresh_3d:
928
- traj_fig = _make_trajectory_figure(
929
- df,
930
- max_points=None if final else plot_cfg.trajectory_max_points,
931
- lite=lite,
932
- )
933
- else:
934
- traj_fig = last_traj or _make_trajectory_figure(
935
- df, max_points=plot_cfg.trajectory_max_points, lite=True,
936
- )
937
  last_traj = traj_fig
938
  yield (traj_fig, loss_fig, vu_fig, twist_fig, status, dict(monitor.history))
939
 
@@ -965,7 +1177,7 @@ def export_optimization_plots(history: dict | None) -> tuple[str | None, str | N
965
  u = np.array([history["u_x"][i], history["u_y"][i], history["u_z"][i]])
966
  monitor.record_step(int(step), history["loss"][i], u, twist=history.get("twist", [0])[i])
967
  return (
968
- save_plotly_png(_make_trajectory_figure(df), "trajectory.png"),
969
  save_plotly_png(_make_loss_figure(df), "loss.png"),
970
  save_plotly_png(_make_string_vu_figure(monitor), "vu.png"),
971
  )
 
12
  import numpy as np
13
  import plotly.express as px
14
  import plotly.graph_objects as go
15
+ from plotly.subplots import make_subplots
16
  import pandas as pd
17
  import torch
18
 
 
52
 
53
  KNOWN_LIMITATIONS_BODY = """
54
  - **CPU only** — optimization + Plotly streaming can feel slow; use **Quick Demo** first.
55
+ - **WebGL required for 3D** — embedded views, privacy browsers, or older GPUs may block WebGL; use **2D (WebGL-safe)** trajectory view.
56
  - **Cold starts** — first load after sleep may take 30–60s to build.
57
  - **ETA is approximate** — actual time varies with server load and concurrent users.
58
  - **Compare vs RiemannianAdam** doubles wall time; capped at 3000 steps on HF.
 
60
  - **Best experience:** desktop Chrome/Firefox · let Quick Demo finish before switching tabs.
61
  """.strip()
62
 
63
+ WEBGL_PROBE_JS = """() => {
64
+ let ok = false;
65
+ try {
66
+ if (localStorage.getItem('6string-webgl-failed') === '1') ok = false;
67
+ else {
68
+ const c = document.createElement('canvas');
69
+ ok = !!(window.WebGLRenderingContext && (c.getContext('webgl') || c.getContext('experimental-webgl')));
70
+ }
71
+ } catch (e) { ok = false; }
72
+ return ok ? "1" : "0";
73
+ }"""
74
+
75
+ TRAJECTORY_VIEW_CHOICES: list[tuple[str, str]] = [
76
+ ("Auto (3D if WebGL available)", "auto"),
77
+ ("3D interactive", "3d"),
78
+ ("2D projection (WebGL-safe)", "2d"),
79
+ ]
80
+
81
  KNOWN_LIMITATIONS_MD = f"### Known limitations (HF free tier)\n{KNOWN_LIMITATIONS_BODY}"
82
 
83
  LOCAL_RUN_MD = """
 
439
  return cls(trajectory_every=1, trajectory_max_points=400, loss_max_points=1500, live_3d=True)
440
 
441
 
442
+ def resolve_trajectory_mode(view_pref: str, webgl_ok: bool) -> str:
443
+ """Map UI preference + client WebGL probe to '3d' or '2d'."""
444
+ pref = (view_pref or "auto").strip().lower()
445
+ if pref == "2d":
446
+ return "2d"
447
+ if pref == "3d":
448
+ return "3d" if webgl_ok else "2d"
449
+ return "2d" if not webgl_ok else "3d"
450
+
451
+
452
+ def webgl_status_markdown(webgl_ok: bool, view_pref: str) -> str:
453
+ resolved = resolve_trajectory_mode(view_pref, webgl_ok)
454
+ if not webgl_ok:
455
+ return (
456
+ "⚠ **WebGL unavailable** — trajectory uses **2D stereographic projections**. "
457
+ "Select **2D (WebGL-safe)** below, or try desktop Chrome/Firefox."
458
+ )
459
+ if view_pref == "2d":
460
+ return "Showing **2D projections** (WebGL-safe mode)."
461
+ if view_pref == "3d":
462
+ return "✓ WebGL detected — **interactive 3D** trajectory."
463
+ return "✓ WebGL detected — **Auto** will use 3D trajectory."
464
+
465
+
466
  def _subsample_df(df: pd.DataFrame, max_points: int) -> pd.DataFrame:
467
  if len(df) <= max_points:
468
  return df
 
489
  "asset": (
490
  f"Missing bundled asset ({msg}). Re-deploy the Space or use the GitHub repo copy."
491
  ),
492
+ "webgl": (
493
+ "3D trajectory needs WebGL. Switch **Trajectory view** to **2D (WebGL-safe)** "
494
+ "in the OPTIMIZE tab, or open in desktop Chrome/Firefox."
495
+ ),
496
  }
497
  lead = hints.get(context, "Something went wrong.")
498
  return f"⚠ {lead}\n\nDetails: {msg[:300]}"
 
569
  return opt
570
 
571
 
572
+ def _make_trajectory_2d_figure(
573
+ df: pd.DataFrame,
574
+ *,
575
+ max_points: int | None = None,
576
+ lite: bool = False,
577
+ webgl_fallback: bool = False,
578
+ ) -> go.Figure:
579
+ """Canvas/SVG 2D projections — no WebGL required."""
580
+ if df.empty or not {"u_x", "u_y", "u_z"}.issubset(df.columns):
581
+ return _trajectory_placeholder("Waiting for trajectory data…")
582
+
583
+ plot_df = _subsample_df(df, max_points) if max_points else df
584
+ marker_size = 3 if lite else 4
585
+ fallback_note = " · 2D fallback (WebGL unavailable)" if webgl_fallback else ""
586
+ fig = make_subplots(
587
+ rows=1,
588
+ cols=2,
589
+ subplot_titles=("u_x vs u_y", "u_y vs u_z"),
590
+ horizontal_spacing=0.1,
591
+ )
592
+ marker_kw = dict(
593
+ size=marker_size,
594
+ color=plot_df["loss"],
595
+ colorscale="Viridis",
596
+ showscale=not lite,
597
+ colorbar=dict(title="loss", len=0.55, y=0.5) if not lite else None,
598
+ )
599
+ line_kw = dict(width=1 if lite else 2, color="rgba(255,170,0,0.65)")
600
+ for col, xcol, ycol in ((1, "u_x", "u_y"), (2, "u_y", "u_z")):
601
+ fig.add_trace(
602
+ go.Scatter(
603
+ x=plot_df[xcol],
604
+ y=plot_df[ycol],
605
+ mode="lines+markers",
606
+ marker=marker_kw,
607
+ line=line_kw,
608
+ name=f"{xcol}/{ycol}",
609
+ showlegend=False,
610
+ ),
611
+ row=1,
612
+ col=col,
613
+ )
614
+ fig.add_trace(
615
+ go.Scatter(
616
+ x=[1],
617
+ y=[1],
618
+ mode="markers",
619
+ marker=dict(size=9, color="#FF4444", symbol="diamond"),
620
+ name="min [1,1,1]",
621
+ showlegend=False,
622
+ ),
623
+ row=1,
624
+ col=col,
625
+ )
626
+ fig.update_layout(
627
+ title=f"S³ → R³ Stereographic — 2D projections{fallback_note}",
628
+ paper_bgcolor="#0a0a0a",
629
+ font=dict(color="#FFB000"),
630
+ height=380,
631
+ margin=dict(l=40, r=20, t=50, b=30),
632
+ )
633
+ for col, xlab, ylab in ((1, "u_x", "u_y"), (2, "u_y", "u_z")):
634
+ fig.update_xaxes(title_text=xlab, gridcolor="#333", row=1, col=col)
635
+ fig.update_yaxes(title_text=ylab, gridcolor="#333", row=1, col=col)
636
+ return fig
637
+
638
+
639
  def _make_trajectory_figure(
640
  df: pd.DataFrame,
641
  *,
 
687
  return fig
688
 
689
 
690
+ def make_trajectory_figure(
691
+ df: pd.DataFrame,
692
+ *,
693
+ mode: str = "3d",
694
+ max_points: int | None = None,
695
+ lite: bool = False,
696
+ webgl_fallback: bool = False,
697
+ ) -> go.Figure:
698
+ """Dispatch 3D WebGL or 2D canvas trajectory."""
699
+ if mode == "2d":
700
+ return _make_trajectory_2d_figure(
701
+ df,
702
+ max_points=max_points,
703
+ lite=lite,
704
+ webgl_fallback=webgl_fallback,
705
+ )
706
+ if df.empty:
707
+ return _trajectory_placeholder("Waiting for trajectory data…")
708
+ return _make_trajectory_figure(df, max_points=max_points, lite=lite)
709
+
710
+
711
+ def _make_loss_figure(
712
+ df: pd.DataFrame,
713
+ *,
714
+ max_points: int | None = None,
715
+ monitor: OptimizerMonitor | None = None,
716
+ step: int | None = None,
717
+ best_loss: float | None = None,
718
+ ) -> go.Figure:
719
+ if df.empty or "loss" not in df.columns:
720
+ fig = go.Figure()
721
+ fig.add_annotation(
722
+ text="Loss curve will appear after the first optimizer update",
723
+ xref="paper", yref="paper", x=0.5, y=0.5,
724
+ showarrow=False, font=dict(size=13, color="#FFB000"),
725
+ )
726
+ fig.update_layout(
727
+ title="Loss Envelope (log scale)",
728
+ paper_bgcolor="#0a0a0a",
729
+ plot_bgcolor="#111318",
730
+ font=dict(color="#FFB000"),
731
+ height=280,
732
+ xaxis=dict(visible=False),
733
+ yaxis=dict(visible=False),
734
+ )
735
+ return fig
736
+
737
  plot_df = _subsample_df(df, max_points) if max_points else df
738
+ current_loss = float(plot_df["loss"].iloc[-1])
739
+ best = best_loss if best_loss is not None else float(plot_df["loss"].min())
740
+ cur_step = step if step is not None else int(plot_df["step"].iloc[-1])
741
+ burst_n = len(monitor.burst_events) if monitor else 0
742
+ title = (
743
+ f"Loss Envelope — step {cur_step:,} · current {current_loss:.4g} · "
744
+ f"best {best:.4g} · {burst_n} burst{'s' if burst_n != 1 else ''}"
745
+ )
746
+ fig = px.line(plot_df, x="step", y="loss", title=title, log_y=True)
747
  fig.update_traces(line=dict(color="#FFB000", width=2))
748
+ if monitor:
749
+ color_cycle = STRING_COLORS
750
+ for i, (burst_step, layer, _) in enumerate(monitor.burst_events[-12:]):
751
+ color = color_cycle[i % len(color_cycle)]
752
+ fig.add_vline(
753
+ x=burst_step,
754
+ line_width=1,
755
+ line_dash="dot",
756
+ line_color=color,
757
+ opacity=0.55,
758
+ )
759
+ if i == len(monitor.burst_events[-12:]) - 1:
760
+ fig.add_annotation(
761
+ x=burst_step,
762
+ y=current_loss,
763
+ text=f"{layer} burst",
764
+ showarrow=False,
765
+ font=dict(size=9, color=color),
766
+ yshift=12,
767
+ )
768
+ losses = plot_df["loss"].astype(float)
769
+ y_min = max(losses.min() * 0.5, 1e-12)
770
+ y_max = max(losses.max() * 2.0, y_min * 10)
771
  fig.update_layout(
772
  paper_bgcolor="#0a0a0a",
773
  plot_bgcolor="#111318",
774
  font=dict(color="#FFB000"),
775
+ xaxis=dict(gridcolor="#333", title="Step"),
776
+ yaxis=dict(gridcolor="#333", title="Loss", type="log", range=[np.log10(y_min), np.log10(y_max)]),
777
  height=280,
778
+ margin=dict(l=40, r=20, t=50, b=30),
779
  )
780
  return fig
781
 
 
849
  total_steps: int,
850
  last_traj: go.Figure | None,
851
  loss_builder,
852
+ trajectory_mode: str = "3d",
853
+ trajectory_webgl_fallback: bool = False,
854
+ best_loss: float | None = None,
855
  ) -> tuple[go.Figure, go.Figure, go.Figure, go.Figure]:
856
  """Throttle expensive 3D redraws; subsample points for 2D plots."""
857
  final = step >= total_steps
858
  lite = not final
859
+ loss_fig = _make_loss_figure(
860
+ df,
861
+ max_points=None if final else plot_cfg.loss_max_points,
862
+ monitor=monitor,
863
+ step=step,
864
+ best_loss=best_loss,
865
+ )
866
  vu_fig = _make_string_vu_figure(monitor)
867
  twist_fig = _make_twist_figure(df, max_points=None if final else plot_cfg.loss_max_points)
868
 
869
+ use_2d = trajectory_mode == "2d"
870
+ refresh_traj = final or (update_idx % plot_cfg.trajectory_every == 0) or use_2d
871
+ if not plot_cfg.live_3d and not final and not use_2d:
872
  traj_fig = last_traj or _trajectory_placeholder()
873
+ elif refresh_traj:
874
+ traj_fig = make_trajectory_figure(
875
  df,
876
+ mode=trajectory_mode,
877
  max_points=None if final else plot_cfg.trajectory_max_points,
878
  lite=lite and not final,
879
+ webgl_fallback=trajectory_webgl_fallback,
880
  )
881
  else:
882
+ traj_fig = last_traj or make_trajectory_figure(
883
+ df,
884
+ mode=trajectory_mode,
885
+ max_points=plot_cfg.trajectory_max_points,
886
+ lite=True,
887
+ webgl_fallback=trajectory_webgl_fallback,
888
  )
889
 
890
  if loss_builder is not None:
 
901
  burst_factor_max: float,
902
  use_fisher_modulation: bool,
903
  plot_cfg: PlotStreamConfig | None = None,
904
+ trajectory_mode: str = "3d",
905
+ trajectory_webgl_fallback: bool = False,
906
  progress_cb=None,
907
  ) -> Generator[tuple, None, None]:
908
  import time
 
978
  total_steps=total_steps,
979
  last_traj=last_traj,
980
  loss_builder=None,
981
+ trajectory_mode=trajectory_mode,
982
+ trajectory_webgl_fallback=trajectory_webgl_fallback,
983
+ best_loss=best_loss,
984
  )
985
  last_traj = traj_fig
986
  yield (traj_fig, loss_fig, vu_fig, twist_fig, status, dict(monitor.history))
 
1046
  burst_factor_max: float,
1047
  use_fisher_modulation: bool,
1048
  plot_cfg: PlotStreamConfig | None = None,
1049
+ trajectory_mode: str = "3d",
1050
+ trajectory_webgl_fallback: bool = False,
1051
  progress_cb=None,
1052
  ) -> Generator[tuple, None, None]:
1053
  """Side-by-side burst optimizer vs geoopt RiemannianAdam on identical Rosenbrock S³ init."""
 
1129
  f"ADAM {loss_a:.6f} (best {best_a:.6f}) │ Δ {loss_a - loss_b:+.4f} │ {eta}"
1130
  )
1131
  update_idx += 1
1132
+ traj_fig, loss_fig, vu_fig, twist_fig = _build_live_figures(
1133
+ df,
1134
+ monitor,
1135
+ plot_cfg=plot_cfg,
1136
+ update_idx=update_idx,
1137
+ step=step,
1138
+ total_steps=cap,
1139
+ last_traj=last_traj,
1140
+ loss_builder=lambda d: _make_dual_loss_figure(
1141
+ hist_burst,
1142
+ hist_adam,
1143
+ max_points=None if step >= cap else plot_cfg.loss_max_points,
1144
+ ),
1145
+ trajectory_mode=trajectory_mode,
1146
+ trajectory_webgl_fallback=trajectory_webgl_fallback,
1147
+ best_loss=best_b,
1148
+ )
 
 
 
1149
  last_traj = traj_fig
1150
  yield (traj_fig, loss_fig, vu_fig, twist_fig, status, dict(monitor.history))
1151
 
 
1177
  u = np.array([history["u_x"][i], history["u_y"][i], history["u_z"][i]])
1178
  monitor.record_step(int(step), history["loss"][i], u, twist=history.get("twist", [0])[i])
1179
  return (
1180
+ save_plotly_png(make_trajectory_figure(df, mode="2d"), "trajectory.png"),
1181
  save_plotly_png(_make_loss_figure(df), "loss.png"),
1182
  save_plotly_png(_make_string_vu_figure(monitor), "vu.png"),
1183
  )