Skip to content

Commit d1659a8

Browse files
committed
fix(app): custom problem preview was calling Relaxation as a callable
The Custom-problem Preview panel raised "'SpinRelaxation' object is not callable" because it did `problem.loss_fn(problem.relaxation(x))` — but `relaxation` is a Relaxation *instance*. Build the latent of the right shape, push it through `relax.forward`, then call `loss_fn` directly. Also report both the relaxed and the discrete sample value, type-check the return, and add an AppTest assertion that the Preview panel renders no st.error.
1 parent 5323c57 commit d1659a8

2 files changed

Lines changed: 42 additions & 7 deletions

File tree

app/_common.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -978,16 +978,36 @@ def preview_problem(problem: Any, cfg: dict) -> None:
978978
)
979979
with st.spinner("Evaluating loss on a random sample..."):
980980
try:
981+
# Build a latent of the same shape ``qqa.anneal`` would allocate,
982+
# then push it through the relaxation's ``forward`` so the value
983+
# we hand to ``loss_fn`` matches what the optimiser will actually
984+
# see (continuous spins in [-1, +1] / one-hot simplex / etc.).
985+
# ``problem.relaxation`` is a *Relaxation instance* — calling it
986+
# directly raised "'SpinRelaxation' object is not callable".
987+
relax = problem.relaxation
981988
if problem.variable_kind == "categorical":
982-
x = torch.randn(1, problem.num_vars, problem.num_category).softmax(dim=-1)
983-
elif problem.variable_kind == "spin":
984-
x = torch.rand(1, problem.num_vars)
989+
latent = torch.rand(1, problem.num_vars, problem.num_category)
985990
else:
986-
x = torch.rand(1, problem.num_vars)
987-
val = problem.loss_fn(problem.relaxation(x))
991+
latent = torch.rand(1, problem.num_vars)
992+
x = relax.forward(latent)
993+
val = problem.loss_fn(x)
994+
if not torch.is_tensor(val):
995+
raise TypeError(
996+
f"loss_fn must return a torch.Tensor, got {type(val).__name__}."
997+
)
998+
if val.shape[0] != 1:
999+
raise ValueError(
1000+
f"loss_fn returned shape {tuple(val.shape)} — expected a "
1001+
"leading batch axis matching the input (B=1 here)."
1002+
)
1003+
# Also evaluate the discrete projection so users see the kind of
1004+
# number QQA tracks as ``best_obj``.
1005+
with torch.no_grad():
1006+
val_disc = problem.loss_fn(relax.project(latent))
9881007
st.success(
989-
f"loss_fn returns tensor shape {tuple(val.shape)}; "
990-
f"sample value = {val.item():.4f}"
1008+
f"loss_fn output shape {tuple(val.shape)} ✓ — relaxed sample "
1009+
f"= {float(val.flatten()[0]):.4f}, discrete sample "
1010+
f"= {float(val_disc.flatten()[0]):.4f}."
9911011
)
9921012
except Exception as e: # pragma: no cover - surfaced in UI
9931013
st.error(f"loss_fn raised: {e}")

tests/test_gui_apptest.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,21 @@ def test_home_page_custom_problem_flow():
5959
assert "source" in cfg["extra"]
6060
# The snippet executes without raising.
6161

62+
# The preview panel must succeed end-to-end. Regression: previously the
63+
# preview called ``problem.relaxation(x)`` which raised
64+
# "'SpinRelaxation' object is not callable" and rendered as an st.error
65+
# under the Preview heading. AppTest does not raise on st.error, so we
66+
# have to inspect the rendered error elements explicitly.
67+
error_bodies = [getattr(e, "body", "") or getattr(e, "value", "") for e in at.error]
68+
assert not any("loss_fn raised" in (b or "") for b in error_bodies), (
69+
f"Custom problem preview surfaced an error: {error_bodies!r}"
70+
)
71+
success_bodies = [getattr(s, "body", "") or getattr(s, "value", "") for s in at.success]
72+
assert any("loss_fn output shape" in (b or "") for b in success_bodies), (
73+
"Custom problem preview did not display the expected success banner; "
74+
f"got success bodies={success_bodies!r}"
75+
)
76+
6277
# Fallback: import via the same mechanism streamlit_app uses.
6378
import sys
6479

0 commit comments

Comments
 (0)