Skip to content

Bound the MILP FiveTuple rescaling by the encoded depth (#65) - #106

Closed
jkhav wants to merge 4 commits into
fix/65-rescaling-boundfrom
fix/65-milp-rescaling
Closed

jkhav wants to merge 4 commits into
fix/65-rescaling-boundfrom
fix/65-milp-rescaling

Conversation

@jkhav

@jkhav jkhav commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Stacked on #88: it reuses pair_k and the per-depth gap bound. Together with #88 this fixes #65 (F1 there, F15 here). F58 continues in #104.

Problem

add_milp_rescaling took its k bound and both big-Ms from Synthesizer.max_k, which is fixed from the input. Past the input depth, and with custom pairs whose k is not reduced, the correct circuit was infeasible. CBC never returned a wrong circuit here; it missed valid ones:

  • X from {sx, z}: no sx;sx
  • Z from {h, sx}: no h;sx;sx;h
  • custom pair |0⟩ → |1⟩ with the target written as a=4, k=4: no x
  • custom input with k=6 (a=1): no x

Fix

  • Same bound as the SMT side. max_gap = max(k_in + inc·depth − k_target, k_target − k_in) bounds |k1 − k2|. The rescaling k is in [0, max_gap // 2], the parity big-M is 2·max_gap + 1, and the value big-M is B + R, with B the vectors' entry bound and R = 2B·2^(max_gap // 2) the bound of the Rescaled vectors (no longer 2**max_k).
  • Stale PuLP bound. In PuLP the bound belongs to the variable. Incremental search redeclares k{pair} at each depth and gets back the first depth's variable, so its ub stayed stale even after pop(). k has no ub now; the one-hot bounds it.
  • Intermediate k bound. Inter's k bound was 2·(d+1), ignoring the input's own k. It is k_in + inc·d now. This is the k half of MILP/Gurobi state bounds ignore the input state #69 F4.
  • Per-gate increments. inc is the largest k increment in the gate set, from a k_increments table in gates.py. encode_layer reads the same table, so a new gate that raises k must be added there (KeyError otherwise) instead of silently breaking a hardcoded 2. Gate sets without h/sx get a gap of 0 and smaller big-Ms.

Why it is sound

With Δ = k1 − k2, K = ⌊|Δ|/2⌋ and q = |Δ| mod 2, the equality is v1·2^K·√2^q = v2 (or swapped), and √2·(a,b,c,d) = (b−d, a+c, b+d, c−a), as encoded.

  • Each layer holds exactly one gate (ExactlyOne), so |Δ| ≤ max_gap and K ≤ max_gap // 2. A larger gap is infeasible, never unconstrained.
  • Parity rows: the relaxed side reaches 2|Δ| ≤ 2·max_gap. The active side forces 2k + q = |Δ|; lb=0 rules out the wrong sleq.
  • Value rows: an inactive row is at most c·|expr| + R, with c ≤ 2^(max_gap // 2) and |expr| ≤ 2B. The row multipliers in constrained_rescaling (2·tmp, 2c·tmp, 3c·tmp, with tmp = B + R) cover each case.

This holds in exact arithmetic. MILP solvers accept values within a tolerance, so very large big-Ms are a numerical risk; that is #104.

Testing

  • New tests in tests/test_rescaling_bound.py: the four MILP cases above (all fail on Bound the FiveTuple rescaling by the encoded depth, in SMT and MILP (#65) #88's branch), and max_k_increment for four gate sets.
  • Full suite: 484 passed.
  • The rescaling on its own vs exact values: a script builds only add_milp_rescaling with fixed vectors and compares CBC's feasibility with complex-float ground truth from the definition. 1,848 solves (630 equal pairs, 870 not), with tight, off-by-one-parity and loose max_gap, plus too-small bounds that must be infeasible: 0 mismatches. As a check on the checker, a parity big-M of 2·max_gap − 1 gives 58 mismatches and a value big-M of (B+R)/4 gives 11.
  • still needs benchmarking to compare the times after changing big M

Not in this PR

ondrik and others added 4 commits October 14, 2026 16:02
…ax_k

add_rescaling covered k = floor(|k_out - k_target| / 2) only below
Synthesizer.max_k, which is computed once from the input. Past the input
depth (incremental search, -d, restricted gate sets, custom vector pairs)
the rescaled vectors were left unconstrained, so any circuit passed the
equivalence check: t;x;t;x with -d 1 gave h;h;h;h.

encode_equivalence now covers every gap the encoded depth can produce
(each layer raises k by at most 2), and add_rescaling asserts k < d, so a
gap outside the case split is UNSAT instead of unconstrained.

Part of #65 (the MILP counterpart, F15, is not addressed here).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177AhE9SXm3iJ1KP8GTSk6o
The rescaling bound is computed from the pairs' k, so a custom pair with
k=4.0 raised a TypeError in range(). Integral floats are now accepted
and anything else is rejected with a ValueError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177AhE9SXm3iJ1KP8GTSk6o
add_milp_rescaling took its k bound and both big-Ms from Synthesizer.max_k,
which is fixed from the input. Past the input depth, or with custom pairs
whose k is not reduced, the correct circuit was infeasible: CBC found no
sx;sx for X from {sx, z}, nor h;sx;sx;h for Z from {h, sx}.

- The MILP rescaling uses the same per-depth gap bound as the SMT one, with
  a parity big-M of 2*max_gap + 1 and a value big-M from the vectors' bounds.
- k of the rescaling has no ub: incremental search gets back the first
  depth's PuLP variable with its stale bound, the one-hot bounds k anyway.
- The intermediate states' k bound includes the input's k.
- The per-layer increase of k comes from a k_increments table in gates.py,
  used by encode_layer and by the bounds, instead of a constant 2.
@codecov

codecov Bot commented Oct 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@ondrik

ondrik commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Consolidated into #88 at @ondrik's request: this PR's commit is now on #88's branch, rebased onto master, and #88's description includes this one. While rebasing, the MILP test for Z from {h, sx} failed again, because since #67 the target's Rescaled2 vector was first declared at depth 0, with the smallest bound; #88's last commit names it per depth. Closing this one in favour of #88.

🤖 Generated with Claude Code

https://claude.ai/code/session_0177AhE9SXm3iJ1KP8GTSk6o

@ondrik ondrik closed this Oct 22, 2026
@ondrik
ondrik deleted the fix/65-milp-rescaling branch October 27, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants