10.0 KiB · bytes match pinned hash
Text · original UTF-8 line locators · 10.0 KiB
"""Domain-wall melting in the clean open XXZ chain.
​
Method: exact propagation of the initial product state inside its conserved
total-Z sector (sparse Hamiltonian built from the instance's Pauli terms,
Chebyshev expansion of exp(-iHt) between requested times, spectrum bounded by
Gershgorin discs). When the sector is too large
for the budget (the 32-site runtime/sanity run), a second-order sweep-TEBD
matrix-product-state integrator with bounded bond dimension is used instead.
​
No offline training, fitting, preprocessing or reference lookup: the method
only reads the instance it is given.
"""
import time as _time
from itertools import combinations
​
import numpy as np
​
PRIORS = {
"family": "exact-sector-chebyshev-with-tebd-fallback",
"parameters": {
"exact_max_sector_dim": 2_500_000,
"chebyshev_coef_cutoff": 1e-16,
"chebyshev_max_scaled_step": 60.0,
"tebd_dt_max": 0.05,
"tebd_trotter_order": 2,
"tebd_max_bond_dim": 64,
"tebd_svd_cutoff": 1e-10,
"tebd_wall_clock_guard_s": 150.0,
},
"notes": (
"Physics-only method; no training, fitting or use of reference traces. "
"Hamiltonian is parsed from instance['terms'] (1-body Z, 2-body XX/YY/ZZ). "
"If the initial product state's fixed-magnetisation sector has dimension <= "
"exact_max_sector_dim, the state is propagated exactly in that sector with a "
"Chebyshev expansion of exp(-iHt) (Bessel coefficients kept down to 1e-16; "
"measured agreement with the public references ~1e-14). Otherwise (only the "
"large runtime/sanity run) a symmetric sweep-TEBD MPS integrator is used with the "
"stated step, bond-dimension cap and SVD cutoff; it is approximate at late times "
"once entanglement exceeds the cap. Hybrid rule: the switch depends only on sector "
"size. If TEBD exceeds the wall-clock guard, the bond cap is halved for the "
"remaining steps (adaptive, declared)."
),
}
​
​
def _parse(instance):
n = int(instance["n_sites"])
two = {} # (i, j) -> {"XX": a, "YY": b, "ZZ": c}
field = np.zeros(n)
const = 0.0
for coef, ops in instance["terms"]:
coef = float(coef)
items = sorted((int(k), str(v).upper()) for k, v in ops.items())
items = [(s, p) for s, p in items if p != "I"]
if not items:
const += coef
elif len(items) == 1 and items[0][1] == "Z":
field[items[0][0]] += coef
elif len(items) == 2 and items[0][1] == items[1][1] and items[0][1] in ("X", "Y", "Z"):
key = (items[0][0], items[1][0])
d = two.setdefault(key, {"XX": 0.0, "YY": 0.0, "ZZ": 0.0})
d[items[0][1] * 2] += coef
else:
raise ValueError("unsupported term %r" % (ops,))
return n, two, field
​
​
def _initial_bits(instance, n):
st = instance["initial_state"]
if "bits" in st:
return [int(b) for b in st["bits"]]
if st.get("type") == "domain_wall":
return [0] * (n // 2) + [1] * (n - n // 2)
raise ValueError("unsupported initial state %r" % (st.get("type"),))
​
​
def _sector_dim(n, k):
from math import comb
return comb(n, k)
​
​
# ---------------------------------------------------------------- exact ----
def _exact(n, two, field, bits, times):
import scipy.sparse as sp
​
for d in two.values():
if abs(d["XX"] - d["YY"]) > 1e-14:
raise ValueError("XX != YY breaks total-Z conservation")
k = sum(bits)
states = np.zeros(_sector_dim(n, k), dtype=np.int64)
for idx, occ in enumerate(combinations(range(n), k)):
v = 0
for s in occ:
v |= 1 << s
states[idx] = v
states.sort()
dim = states.size
zsite = 1 - 2 * ((states[:, None] >> np.arange(n)[None, :]) & 1) # dim x n, int
diag = zsite.astype(float) @ field
rows, cols, vals = [], [], []
for (i, j), d in two.items():
zi, zj = zsite[:, i], zsite[:, j]
diag += d["ZZ"] * zi * zj
hop = d["XX"] + d["YY"]
if hop != 0.0:
anti = np.nonzero(zi != zj)[0]
flipped = states[anti] ^ ((1 << i) | (1 << j))
tgt = np.searchsorted(states, flipped)
rows.append(tgt)
cols.append(anti)
vals.append(np.full(anti.size, hop))
rows.append(np.arange(dim))
cols.append(np.arange(dim))
vals.append(diag)
H = sp.csr_matrix(
(np.concatenate(vals), (np.concatenate(rows), np.concatenate(cols))), shape=(dim, dim)
)
v0 = 0
for s, b in enumerate(bits):
if b:
v0 |= 1 << s
psi = np.zeros(dim, dtype=np.complex128)
psi[np.searchsorted(states, v0)] = 1.0
zf = zsite.astype(float)
​
# Chebyshev propagator; spectrum bounded by Gershgorin discs of real H.
absH = abs(H)
rad = np.asarray(absH.sum(axis=1)).ravel() - np.abs(diag)
emin, emax = float(np.min(diag - rad)), float(np.max(diag + rad))
a = 0.5 * (emax - emin) * (1 + 1e-9) + 1e-12
b = 0.5 * (emax + emin)
Hs = ((H - b * sp.identity(dim, format="csr")) / a).tocsr()
​
def cheb_step(v, dt):
from scipy.special import jv
x = a * dt
kmax = int(x + 10 * x ** (1 / 3) + 30)
c = jv(np.arange(kmax + 1), x)
# keep terms until coefficients are negligible
tail = np.nonzero(np.abs(c) > 1e-16)[0]
kmax = int(tail[-1]) + 2 if tail.size else 2
V = np.stack([v.real, v.imag], axis=1)
t0v, t1v = V, Hs @ V
res = c[0] * (t0v[:, 0] + 1j * t0v[:, 1])
ph = -1j
res = res + 2 * ph * c[1] * (t1v[:, 0] + 1j * t1v[:, 1])
for k in range(2, min(kmax, c.size - 1) + 1):
t2v = 2 * (Hs @ t1v) - t0v
ph *= -1j
res = res + 2 * ph * c[k] * (t2v[:, 0] + 1j * t2v[:, 1])
t0v, t1v = t1v, t2v
return np.exp(-1j * b * dt) * res
​
times = np.asarray(times, dtype=float)
out = np.zeros((times.size, n))
order = np.argsort(times, kind="stable")
t_cur = 0.0
for idx in order:
t = times[idx]
if t != t_cur:
dt = t - t_cur
nsub = max(1, int(np.ceil(abs(dt) * a / 60.0)))
for _ in range(nsub):
psi = cheb_step(psi, dt / nsub)
psi /= np.linalg.norm(psi)
t_cur = t
out[idx] = (np.abs(psi) ** 2) @ zf
return out
​
​
# ----------------------------------------------------------------- TEBD ----
_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)
​
​
def _bond_h(n, two, field, i):
d = two.get((i, i + 1), {"XX": 0.0, "YY": 0.0, "ZZ": 0.0})
h = d["XX"] * np.kron(_X, _X) + d["YY"] * np.kron(_Y, _Y) + d["ZZ"] * np.kron(_Z, _Z)
I2 = np.eye(2)
# split on-site fields between neighbouring bonds
wl = 1.0 if i == 0 else 0.5
wr = 1.0 if i + 1 == n - 1 else 0.5
h = h + wl * field[i] * np.kron(_Z, I2) + wr * field[i + 1] * np.kron(I2, _Z)
return h
​
​
def _gate(h, dt):
w, v = np.linalg.eigh(h)
return (v * np.exp(-1j * w * dt)) @ v.conj().T
​
​
def _apply(M, i, g, chi, cutoff, absorb_right):
a, b = M[i], M[i + 1]
l, r = a.shape[0], b.shape[2]
th = (a.reshape(l * 2, -1) @ b.reshape(b.shape[0], 2 * r)).reshape(l, 4, r)
th = (g @ th).reshape(l * 2, 2 * r) # g: (4, 4) acting on the physical pair
u, s, vh = np.linalg.svd(th, full_matrices=False)
keep = max(1, min(chi, int(np.sum(s > cutoff * s[0]))))
u, s, vh = u[:, :keep], s[:keep], vh[:keep]