feat: add new scripts for S_m x SO(3) double collapse examples and common utilities

This commit is contained in:
Hans Aschauer 2026-08-11 06:43:18 +02:00
parent 4b7008b1df
commit 0f8e4e8f99
8 changed files with 550 additions and 3 deletions

View file

@ -0,0 +1,101 @@
"""
01_double_collapse_table.py
Reproduces and extends Example ex:dicke-network of shadow_maps_symmetric_states.tex
(sec:sm-so3-combination): for the doubly-symmetric state
|Q_m> = N * sum_{k=0}^m (-1)^k |D_m^k>_S ox |D_m^{m-k}>_S^c
(S,S^c each m qubits, S_m-symmetric on each side AND fully collectively
SO(3)-invariant), the fully active block M_{S->S^c}(|Q_m><Q_m|) collapses,
in two independent steps, from 3^m x 3^m down to O(m) scalar channels:
Step 1 (Prop. multinomial-collapse): 3^m x 3^m -> binom(m+2,2) x binom(m+2,2)
via the S_m orbit-type embedding.
Step 2 (Prop. harmonic-decomposition + Cor. projector-norm-formula):
binom(m+2,2) -> exactly floor(m/2)+1 SCALAR values A_j, since
Sym^m(R^3) decomposes multiplicity-free into spherical harmonics
H_j, j = m, m-2, ..., (0 or 1).
This script runs the check for m = 3, 4, 5 and prints a summary table.
Only the m=3 case appears in the current paper draft (with values A_1, A_3
whose SIGN should be checked against this script -- see README).
Run: python3 01_double_collapse_table.py
"""
import numpy as np
from shadow_su2_common import (
build_Q, verify_collective_invariance, reduced_correlation_matrix,
total_J2, isotypic_eigenbasis, isotypic_block_report
)
np.set_printoptions(precision=6, suppress=True)
def run_for_m(m):
print("=" * 70)
print(f"m = {m} (source/target cluster size), j0 = {m}/2 = {m/2}")
print("=" * 70)
Q = build_Q(m)
# sanity: full collective SO(3) invariance
overlaps = verify_collective_invariance(Q, n_legs=2*m, n_trials=3, seed=m)
print(f"collective invariance check |<Q|U^ox{2*m}|Q>| (should be 1.0): "
f"{[f'{o:.10f}' for o in overlaps]}")
# Step 1: multinomial collapse
Chat, types = reduced_correlation_matrix(Q, m)
nuclear_norm_reduced = np.linalg.svd(Chat, compute_uv=False).sum()
print(f"Step 1 (multinomial collapse): 3^{m}x3^{m} -> "
f"{len(types)}x{len(types)}, ||hat-C||_* = {nuclear_norm_reduced:.6f}")
# Step 2: SO(3) Casimir block-diagonalization within the reduced space
J2 = total_J2(m)
# restrict J2 (3^m x 3^m) to the binom(m+2,2)-dim symmetric subspace via
# the SAME orbit embedding used to build Chat (Sym^m(R^3) = image of that embedding)
from shadow_su2_common import build_orbit_embedding
U, _ = build_orbit_embedding(m)
J2_red = U.T @ J2 @ U
evals, evecs, groups = isotypic_eigenbasis(J2_red)
results = isotypic_block_report(Chat, groups, evecs, label=f"Step 2, m={m}")
# nuclear norm consistency check: sum_j (2j+1)|A_j| == ||hat-C||_*
total = 0.0
Aj_summary = {}
for j, (block, sv) in results.items():
Aj = np.diag(block).mean()
Aj_summary[j] = Aj
total += (2*j+1) * abs(Aj)
print(f"consistency: sum_j (2j+1)|A_j| = {total:.6f} "
f"vs ||hat-C||_* = {nuclear_norm_reduced:.6f} "
f"(match: {np.isclose(total, nuclear_norm_reduced)})")
return Aj_summary
if __name__ == "__main__":
all_results = {}
for m in (3, 4, 5):
all_results[m] = run_for_m(m)
print()
print("=" * 70)
print("SUMMARY TABLE (exact A_j values, m = 3,4,5)")
print("=" * 70)
for m, Aj in all_results.items():
j0 = m/2
items = ", ".join(f"A_{j:g}={v:+.6f}" for j, v in sorted(Aj.items(), reverse=True))
print(f"m={m} (j0={j0}): {items}")
print()
print("Pattern check across m:")
print(" sign(A_j) should be (-1)^m for every j")
print(" top ratio A_m / A_{m-2} should equal 2m exactly")
for m, Aj in all_results.items():
js = sorted(Aj.keys(), reverse=True)
if len(js) >= 2:
top, second = js[0], js[1]
ratio = Aj[top]/Aj[second]
print(f" m={m}: A_{top:g}/A_{second:g} = {ratio:.6f} (expect {2*m})")

View file

@ -0,0 +1,80 @@
"""
02_subsector_multiplicity_example.py
Extends Example ex:dicke-network beyond the FULLY ACTIVE block M_{S->S^c}.
For m=4 (S = {A,B,C,D}, S^c = {E,F,G,H}), this looks at the SUB-sector block
M_{V->T} with V = {A,B,C} subsetneq S (qubit D a "spectator") and
T = {E,F,G} subsetneq S^c (qubit H a spectator).
Unlike the fully active case, V (and T) here live in the FULL (unsymmetrized)
(R^3)^{ox 3}, which has multiplicities m_j = 1,3,2,1 for j=0,1,2,3
(Prop. branching-su2). In particular m_1 = 3, so the reduced block A_1 need
NOT be a scalar -- this is the first concrete state in the note where a
genuinely non-trivial (non-identity) reduced matrix A_j appears, rather than
the always-scalar case forced by the multiplicity-free Sym^m(R^3) of
Example ex:dicke-network.
Finding (see README): A_3 is scalar (mult 1, as expected), A_1 is a genuine
RANK-1 3x3 matrix (multiplicity 3, one nonzero singular value repeated 3x),
and A_2, A_0 vanish identically for this particular state/cut -- an
unexplained selection rule, flagged as an open question, not yet understood.
Run: python3 02_subsector_multiplicity_example.py
"""
import numpy as np
from shadow_su2_common import build_Q, apply_leg, PAULIS, total_J2, isotypic_eigenbasis, isotypic_block_report
from itertools import product
np.set_printoptions(precision=4, suppress=True, linewidth=140)
def main():
m = 4
Q = build_Q(m) # legs 0,1,2,3 = A,B,C,D (source S); legs 4,5,6,7 = E,F,G,H (target S^c)
V_legs = [0, 1, 2] # {A,B,C} subsetneq S; D = leg 3 left inactive (spectator)
T_legs = [4, 5, 6] # {E,F,G} subsetneq S^c; H = leg 7 left inactive (spectator)
# raw (unreduced) 27x27 block M_{V->T} -- no multinomial collapse here,
# since V,T are not the full active sector and the S_m symmetry does not
# act on a fixed 3-out-of-4 subset the same simple way (cf. Remark
# permutation-separating-example: only the residual S_3-within-V x S_3-within-T
# symmetry survives, and we do not exploit it here -- this is the raw block).
c = np.zeros((3, 3, 3, 3, 3, 3), dtype=complex)
for iA, iB, iC, iE, iF, iG in product(range(3), repeat=6):
ket = Q
for leg, ip in zip(V_legs, [iA, iB, iC]):
ket = apply_leg(ket, leg, PAULIS[ip])
for leg, ip in zip(T_legs, [iE, iF, iG]):
ket = apply_leg(ket, leg, PAULIS[ip])
c[iA, iB, iC, iE, iF, iG] = np.vdot(Q, ket).real
M = c.reshape(27, 27)
nuc = np.linalg.svd(M, compute_uv=False).sum()
print(f"V = {{A,B,C}} subsetneq S = {{A,B,C,D}}, T = {{E,F,G}} subsetneq S^c = {{E,F,G,H}}")
print(f"||M_{{V->T}}||_* = {nuc:.6f}\n")
# Casimir decomposition of the FULL (unsymmetrized) (R^3)^{ox3} on each side
J2 = total_J2(3)
evals, evecs, groups = isotypic_eigenbasis(J2)
print("multiplicities on full (R^3)^{ox3} (no S_3 symmetrization):",
{j: len(idx) for j, idx in groups.items()}, " (expect {3:1, 2:2, 1:3, 0:1})\n")
results = isotypic_block_report(M, groups, evecs, label="isotypic blocks of M_{V->T}")
print("\nFull reduced matrices (rounded) and their singular values:")
total_check = 0.0
for j in sorted(results, reverse=True):
block, sv = results[j]
print(f"\nj={j}: block =\n{block}")
print(f" singular values: {sv}")
total_check += (2*j+1) * 0 # placeholder, real check below using sv directly if block scalar
# nuclear-norm consistency: sum over ALL singular values of all blocks == ||M||_*
all_sv = np.concatenate([sv for (_, sv) in results.values()])
print(f"\nsum of all block singular values = {all_sv.sum():.6f} vs ||M||_* = {nuc:.6f} "
f"(match: {np.isclose(all_sv.sum(), nuc)})")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,72 @@
# Scripts: S_m × SO(3) double collapse (fehlende Resultate im Entwurf)
Diese Skripte reproduzieren und erweitern `Example ex:dicke-network` /
`sec:sm-so3-combination` aus `shadow_maps_symmetric_states.tex`. Sie decken
genau die Ergebnisse ab, die in der Team-Notiz vom [Datum der Sitzung] als
"im Entwurf fehlend" markiert wurden.
## Dateien
- **`shadow_su2_common.py`** — gemeinsames Modul: Dicke-Zustände, der
doppelt-symmetrische Zustand `|Q_m>`, Pauli-Operatoren, die
Multinomial-Kollaps-Einbettung (Prop. `multinomial-collapse`), und die
exakten Casimir-Projektoren + Blockstruktur-Check (Remark
`casimir-projectors`, Cor. `projector-norm-formula`). Keine externen
Abhängigkeiten außer NumPy.
- **`01_double_collapse_table.py`** — der volle aktive Block $M_{S\to S^c}$
für $m=3,4,5$. Reproduziert das bestehende $m=3$-Beispiel im Paper und
erweitert es auf $m=4,5$. Erzeugt die Tabelle der exakten $A_j$-Werte und
prüft zwei Muster:
- Vorzeichen $=(-1)^m$
- Top-Verhältnis $A_m/A_{m-2}=2m$ exakt
**Achtung:** Die Werte hier sind $A_1=-1/3$, $A_3=-2$ für $m=3$ —
**negativ**. Der aktuelle Paper-Entwurf gibt $A_1=+1/3$, $A_3=+2$ an
(positiv). Für dieselbe Zustandsdefinition sollte das Vorzeichen aber
eindeutig sein (kein Freiheitsgrad einer globalen Phase, da es sich um
einen Erwartungswert handelt). Bitte vor Übernahme ins Paper gegenprüfen —
betrifft nicht die im Text gezogene Konsequenz $3A_1+7A_3=15$, die nur
$|A_j|$ benutzt.
- **`02_subsector_multiplicity_example.py`** — der Teilsektor-Fall
$V=\{A,B,C\}\subsetneq S=\{A,B,C,D\}$ (ein "Zuschauer"-Qubit $D$), analog
auf der Zielseite. Im Unterschied zu `01` lebt $V$ hier in der vollen,
nicht symmetrisierten $(\mathbb R^3)^{\otimes3}$ mit Multiplizitäten
$m_j=1,3,2,1$ für $j=0,1,2,3$. Ergebnis:
- $j=3$: skalar, $A_3=-0.8$ (Multiplizität 1, wie erwartet)
- $j=2$: **verschwindet identisch** (nicht nur klein — exakt Null,
unerklärt, siehe unten)
- $j=1$: **echte, nicht-skalare $3\times3$-Matrix**, Rang 1 (ein
Singulärwert $0{,}3$, dreifach über die Multiplizität)
- $j=0$: exakt Null
Das ist im aktuellen Entwurf komplett unbehandelt — bisher zeigt das
Paper nur den multiplizitätsfreien Fall $V=S$, bei dem $A_j$ zwangsläufig
skalar ist. Dieses Beispiel ist der erste konkrete Beleg für echte
Multiplizitätsraum-Struktur am doppelt-symmetrischen Zustand.
## Offene Punkte (nicht in den Skripten gelöst)
1. **Geschlossene Form für $A_j(m)$**: Nur numerisch gemustert (Tabelle in
`01`), nicht hergeleitet. Nächster Schritt wäre eine saubere
Clebsch-Gordan-Herleitung via `sympy` (z. B. über den "gestreckten
Zustand"-Trick), nicht durch Raten aus den drei Datenpunkten.
2. **Warum verschwinden $j=2,0$ im Teilsektor-Beispiel (`02`)?** Exakt
Null, nicht nur klein — deutet auf eine zusätzliche Auswahlregel hin
(Parität? Eigenschaft der spezifischen $Q_4$-Konstruktion?). Nicht
untersucht.
3. Das Vorzeichen-Problem oben (Punkt zu `01`) sollte geklärt werden, bevor
das Beispiel im Paper erweitert wird.
## Ausführen
```bash
pip install -r requirements.txt
python3 01_double_collapse_table.py
python3 02_subsector_multiplicity_example.py
```
Beide Skripte sind eigenständig lauffähig (importieren nur
`shadow_su2_common.py` aus demselben Verzeichnis) und laufen jeweils in
wenigen Sekunden.

View file

@ -0,0 +1 @@
numpy>=1.20

View file

@ -0,0 +1,241 @@
"""
shadow_su2_common.py
Shared building blocks for the S_m x SO(3) double-collapse numerics
(Section "Combining with permutation symmetry: multiplicity-free channels",
sec:sm-so3-combination, in shadow_maps_symmetric_states.tex).
Provides:
- Dicke states and the doubly-symmetric ("singlet-of-two-multiplets") state
|Q_m> = sum_k (-1)^k |D_m^k>_S ox |D_m^{m-k}>_S^c
- Pauli matrices and leg-wise operator application on a rank-2m tensor
- The S_m orbit-type embedding (multinomial collapse, Prop. multinomial-collapse)
- Exact SO(3) Casimir projectors via J_x,J_y,J_z on (R^3)^{ox k}
(Remark casimir-projectors), and the resulting isotypic block check
(Corollary projector-norm-formula)
No external dependencies beyond numpy.
"""
import numpy as np
from itertools import product
from math import factorial
# ---------------------------------------------------------------------
# 1. Dicke states and the doubly-symmetric invariant state |Q_m>
# ---------------------------------------------------------------------
def dicke(m, k):
"""|D_m^k>: equal superposition of all weight-k bitstrings on m qubits."""
dim = 2**m
psi = np.zeros(dim, dtype=complex)
n_terms = 0
for bits in range(dim):
if bin(bits).count("1") == k:
psi[bits] = 1.0
n_terms += 1
psi /= np.sqrt(n_terms)
return psi
def build_Q(m):
"""
|Q_m> = N * sum_{k=0}^m (-1)^k |D_m^k>_S ox |D_m^{m-k}>_S^c , S,S^c each m qubits.
This is (up to overall phase) the unique SO(3) singlet formed by coupling
the two spin-j0=m/2 permutation-symmetric multiplets on S and S^c to J=0
(Example ex:dicke-network for m=3; here for general m).
Returns the state reshaped as a rank-(2m) tensor: legs 0..m-1 are the
source cluster S, legs m..2m-1 are the target cluster S^c.
"""
dimS = 2**m
Q = np.zeros(dimS*dimS, dtype=complex)
for k in range(m+1):
Dk = dicke(m, k)
Dmk = dicke(m, m-k)
Q += ((-1)**k) * np.kron(Dk, Dmk)
Q /= np.linalg.norm(Q)
return Q.reshape([2]*(2*m))
def verify_collective_invariance(Q_flat_dim_legs, n_legs, n_trials=5, seed=0):
"""
Sanity check: |<Q|U^{ox n_legs}|Q>| == 1 for random single-qubit U in SU(2),
confirming full collective SO(3) invariance of the state.
Q_flat_dim_legs: the state as a rank-n_legs tensor.
"""
rng = np.random.default_rng(seed)
Q = Q_flat_dim_legs
results = []
for _ in range(n_trials):
v = rng.standard_normal(4)
v /= np.linalg.norm(v)
a, b, c, d = v
U = np.array([[a+1j*b, c+1j*d], [-c+1j*d, a-1j*b]], dtype=complex)
Qrot = Q
for leg in range(n_legs):
Qrot = apply_leg(Qrot, leg, U)
overlap = np.vdot(Q, Qrot)
results.append(abs(overlap))
return results
# ---------------------------------------------------------------------
# 2. Pauli matrices and leg-wise application
# ---------------------------------------------------------------------
X = np.array([[0, 1], [1, 0]], dtype=complex)
Y = np.array([[0, -1j], [1j, 0]], dtype=complex)
Z = np.array([[1, 0], [0, -1]], dtype=complex)
PAULIS = [X, Y, Z] # index 0,1,2 <-> x,y,z
def apply_leg(psi, axis, P):
"""Apply a 2x2 (or dxd) operator P on the given tensor leg of psi."""
out = np.tensordot(P, psi, axes=([1], [axis]))
return np.moveaxis(out, 0, axis)
# ---------------------------------------------------------------------
# 3. Multinomial (S_m orbit-type) collapse -- Prop. multinomial-collapse
# ---------------------------------------------------------------------
def orbit_types(m):
"""All (a_x,a_y,a_z) with a_x+a_y+a_z=m -- the |T_m| = binom(m+2,2) orbit types."""
return [(a, b, m-a-b) for a in range(m+1) for b in range(m+1-a)]
def _type_of(idx_tuple):
cnt = [0, 0, 0]
for i in idx_tuple:
cnt[i] += 1
return tuple(cnt)
def multinomial_coeff(alpha):
m = sum(alpha)
denom = 1
for a in alpha:
denom *= factorial(a)
return factorial(m) // denom
def build_orbit_embedding(m):
"""
Orthonormal embedding U: 3^m -> binom(m+2,2), columns u_alpha
(normalized indicator vectors of each S_m orbit), as in the proof of
Prop. multinomial-collapse.
"""
types = orbit_types(m)
idx_m = list(product(range(3), repeat=m))
U = np.zeros((3**m, len(types)))
for col, alpha in enumerate(types):
mask = np.array([1.0 if _type_of(t) == alpha else 0.0 for t in idx_m])
U[:, col] = mask / np.sqrt(mask.sum())
return U, types
def reduced_correlation_matrix(Q, m):
"""
Build the multinomial-collapsed reduced matrix hat-C (Eq. reduced-dicke-matrix)
for the FULLY ACTIVE block M_{S->S^c}(rho) of the m+m cluster state Q,
without ever materializing the full 3^m x 3^m matrix explicitly (uses one
orbit representative per row/column instead of a brute-force loop).
"""
types = orbit_types(m)
ntypes = len(types)
Chat = np.zeros((ntypes, ntypes))
for a_col, alpha in enumerate(types):
rep_src = []
for i, cnt in enumerate(alpha):
rep_src += [i]*cnt
ket = Q
for leg in range(m):
ket = apply_leg(ket, leg, PAULIS[rep_src[leg]])
for b_col, beta in enumerate(types):
rep_tgt = []
for i, cnt in enumerate(beta):
rep_tgt += [i]*cnt
ket2 = ket
for leg in range(m):
ket2 = apply_leg(ket2, m+leg, PAULIS[rep_tgt[leg]])
val = np.vdot(Q, ket2).real
Chat[a_col, b_col] = val * np.sqrt(multinomial_coeff(alpha) * multinomial_coeff(beta))
return Chat, types
# ---------------------------------------------------------------------
# 4. Exact SO(3) Casimir projectors (Remark casimir-projectors) and the
# isotypic block check (Corollary projector-norm-formula)
# ---------------------------------------------------------------------
_EPS = np.zeros((3, 3, 3))
_EPS[0, 1, 2] = _EPS[1, 2, 0] = _EPS[2, 0, 1] = 1
_EPS[0, 2, 1] = _EPS[2, 1, 0] = _EPS[1, 0, 2] = -1
J1_CARTESIAN = [-1j*_EPS[a] for a in range(3)] # spin-1 generator, single leg
def _kron_n(mats):
out = mats[0]
for M in mats[1:]:
out = np.kron(out, M)
return out
def total_J2(k):
"""J^2_tot = sum_a (sum_l J_a on leg l)^2 acting on (R^3)^{ox k}, as a 3^k x 3^k matrix."""
I3 = np.eye(3)
dim = 3**k
Jtot = [np.zeros((dim, dim), dtype=complex) for _ in range(3)]
for a in range(3):
for leg in range(k):
mats = [I3]*k
mats[leg] = J1_CARTESIAN[a]
Jtot[a] += _kron_n(mats)
return sum(Ja @ Ja for Ja in Jtot)
def isotypic_eigenbasis(J2_matrix):
"""
Diagonalize J^2 (real part), return (evals, evecs, groups) where groups
maps j -> list of eigenvector indices spanning that isotypic component.
Eigenvalues are j(j+1); j is recovered via j=(-1+sqrt(1+4*ev))/2.
"""
J2_real = J2_matrix.real
evals, evecs = np.linalg.eigh(J2_real)
groups = {}
for i, ev in enumerate(evals):
j = round((-1 + np.sqrt(1 + 4*max(ev, 0))) / 2, 3)
groups.setdefault(j, []).append(i)
return evals, evecs, groups
def isotypic_block_report(M, groups, evecs, label=""):
"""
Rotate M into the J^2 eigenbasis and report, per isotype j:
- block dimension
- whether it is a scalar multiple of the identity (mean diag, spread, off-diag)
- max leakage into other isotypes (should vanish -- Cor. projector-norm-formula)
Returns dict j -> (block matrix, singular values).
"""
M_rot = evecs.T @ M @ evecs
results = {}
if label:
print(f"--- {label} ---")
for j in sorted(groups, reverse=True):
idxs = groups[j]
block = M_rot[np.ix_(idxs, idxs)]
diag = np.diag(block)
offdiag_within = block - np.diag(diag)
offblock_max = 0.0
for j2 in groups:
if j2 == j:
continue
offblock_max = max(offblock_max, np.abs(M_rot[np.ix_(idxs, groups[j2])]).max())
sv = np.linalg.svd(block, compute_uv=False) if len(idxs) > 1 else np.abs(diag)
print(f" j={j:.1f} dim={len(idxs):2d} mean(diag)={diag.mean(): .8f} "
f"spread(diag)={diag.std():.2e} |offdiag|max={np.abs(offdiag_within).max():.2e} "
f"|leak to other j|max={offblock_max:.2e}")
results[j] = (block, sv)
return results