Skip to content

Commit 795450d

Browse files
committed
feat(app,qqa): EA preview safety, TSP penalty method, spin arrows on topology
Summary ------- * `app/_common.py` — stop building O(N^2) Plotly heatmaps in the problem preview. For coupling matrices above ~600 spins fall back to a sparse non-zero scatter (O(nnz) markers). The default Edwards-Anderson selection (`L=32, dim=3`, N=32 768) used to OOM the browser tab; it now renders a structural spy plot instead. Default EA lattice side dropped from 32 to 6 so the first interaction also converges in <1 s. * `src/qqa/problems/extras.py` — TSP rewritten as a true penalty method following Lucas (2014). `BinaryRelaxation(shape_fn=...)` lifts the latent into `(B, N, N)`; `loss_fn` returns `tour + lambda_r * row_pen + lambda_c * col_pen` so the optimiser sees both permutation constraints as gradients (the previous `CategoricalRelaxation` formulation hid the row constraint inside the relaxation's softmax). `score_summary` runs Hungarian assignment on every replica's projected matrix, mutates `best_sol` to the cleaned permutation, and surfaces `extra.raw_feasible` / `extra.snapped` so the dashboard can distinguish optimiser-converged tours from post-hoc-snapped ones. * `app/streamlit_app.py` — sidebar replaces the single `column_penalty` slider with `row_penalty` + `col_penalty`, plus an explanatory caption. Per-problem `size_default` / `size_max` updated to keep EA, TSP usable out-of-the-box. * `app/_solution_viz.py` — spin-system renderers now place up / down triangle markers on the actual topology of the problem: - 1D Ising -> ring with periodic bonds - SK -> circle (no spatial structure) + complementary local-energy bar (positive = frustrated) - 2D EA -> lattice grid with bond ribbons - 3D EA -> side-by-side z-slices (capped at six) Title carries colour-coded up / down counts. The TSP renderer adds segment direction arrows, an optional pairwise-distance backdrop for small N, and a "raw permutation" / "Hungarian-snapped" badge. * `tests/test_extra_problems.py` — three new TSP regression tests (BinaryRelaxation + two penalties; per-term decomposition; Hungarian snap always returns a valid permutation) and one EA preview test ensuring the sparse scatter fallback fires for `L=16, dim=3`. ruff format && ruff check clean. pytest: 91 passed, 1 skipped.
1 parent efd6e4b commit 795450d

5 files changed

Lines changed: 646 additions & 118 deletions

File tree

app/_common.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,8 @@ def build_problem(cfg: dict) -> Any:
946946
if kind == "tsp":
947947
return qqa.TSP(
948948
N=size,
949-
column_penalty=float(extra.get("column_penalty", 3.0)),
949+
row_penalty=float(extra.get("row_penalty", 5.0)),
950+
col_penalty=float(extra.get("col_penalty", 5.0)),
950951
seed=cfg["seed"],
951952
device=device,
952953
)
@@ -1001,14 +1002,71 @@ def _graph_preview(g: nx.Graph, title: str) -> None:
10011002

10021003

10031004
def _coupling_preview(J: np.ndarray, title: str) -> None:
1004-
fig = go.Figure(data=go.Heatmap(z=J, colorscale="RdBu", zmid=0, colorbar={"title": "J_ij"}))
1005+
"""Show the coupling matrix without melting the browser.
1006+
1007+
Plotly's ``Heatmap`` is dense — every cell becomes a SVG rect. For
1008+
``N ≳ 1000`` the trace alone is hundreds of MB; for the default EA
1009+
setting (``L=32, dim=3 ⇒ N=32 768``) it instantly OOMs the tab.
1010+
Past ``N_show=256`` we fall back to a *sparse* spy plot of the
1011+
non-zero couplings (still ``O(nnz)`` markers, not ``O(N²)`` rects).
1012+
Spin glasses on a hyper-cubic lattice have ``≈ d·N`` non-zeros, so
1013+
this stays well-behaved even at 32k spins.
1014+
"""
1015+
N = J.shape[0]
1016+
# 600^2 ≈ 360k cells — Plotly handles that comfortably; 1k^2 = 1M is
1017+
# already laggy. Anything bigger ⇒ fall back to a sparse view.
1018+
N_show = 600
1019+
if N_show >= N:
1020+
fig = go.Figure(data=go.Heatmap(z=J, colorscale="RdBu", zmid=0, colorbar={"title": "J_ij"}))
1021+
fig.update_layout(
1022+
title={"text": title, "x": 0.5, "font": {"color": "#f8fafc"}},
1023+
paper_bgcolor="rgba(0,0,0,0)",
1024+
plot_bgcolor="rgba(0,0,0,0)",
1025+
height=400,
1026+
)
1027+
st.plotly_chart(fig, width="stretch")
1028+
return
1029+
1030+
rows, cols = np.nonzero(J)
1031+
if rows.size == 0:
1032+
st.info(f"{title}: coupling matrix is all-zero (N={N}).")
1033+
return
1034+
vals = J[rows, cols]
1035+
vmax = float(np.abs(vals).max())
1036+
fig = go.Figure(
1037+
data=go.Scatter(
1038+
x=cols,
1039+
y=rows,
1040+
mode="markers",
1041+
marker={
1042+
"size": 4,
1043+
"color": vals,
1044+
"colorscale": "RdBu",
1045+
"cmin": -vmax,
1046+
"cmax": vmax,
1047+
"colorbar": {"title": "J_ij", "thickness": 12},
1048+
"line": {"width": 0},
1049+
},
1050+
hovertemplate="i=%{y}, j=%{x}<br>J=%{marker.color:.3f}<extra></extra>",
1051+
)
1052+
)
10051053
fig.update_layout(
1006-
title={"text": title, "x": 0.5, "font": {"color": "#f8fafc"}},
1054+
title={
1055+
"text": f"{title} — sparse view ({rows.size} non-zero entries)",
1056+
"x": 0.5,
1057+
"font": {"color": "#f8fafc"},
1058+
},
10071059
paper_bgcolor="rgba(0,0,0,0)",
10081060
plot_bgcolor="rgba(0,0,0,0)",
1009-
height=400,
1061+
height=440,
1062+
xaxis={"title": "j", "scaleanchor": "y", "scaleratio": 1, "autorange": True},
1063+
yaxis={"title": "i", "autorange": "reversed"},
10101064
)
10111065
st.plotly_chart(fig, width="stretch")
1066+
st.caption(
1067+
f"Showing {rows.size} non-zero couplings out of {N * N:,} matrix entries. "
1068+
"A dense heatmap at this size would crash the browser."
1069+
)
10121070

10131071

10141072
def preview_problem(problem: Any, cfg: dict) -> None:

0 commit comments

Comments
 (0)