|
| 1 | +"""Helper functions for the MPS simulator notebook.""" |
| 2 | + |
| 3 | +import math |
| 4 | +import numpy as np |
| 5 | +import quimb |
| 6 | +import quimb.tensor as qtn |
| 7 | +import pepsy as py |
| 8 | + |
| 9 | + |
| 10 | +def build_lattice(Lx, Ly, coupling_j, field_h, cyclic=True, lattice="square", mode="hilbert"): |
| 11 | + """Build the 2D ITF lattice: mapper, Hamiltonian MPO, edges, sites. |
| 12 | +
|
| 13 | + Returns |
| 14 | + ------- |
| 15 | + dict with keys: mpo_H, edges_1d, sites, mapper, res (full build_itf_lattice output). |
| 16 | + """ |
| 17 | + mapper = py.core.OneDMap(Lx, Ly, mode="hilbert") |
| 18 | + res = py.ham_tn.build_itf_lattice( |
| 19 | + L_x=Lx, L_y=Ly, lattice=lattice, cyclic=cyclic, |
| 20 | + J=coupling_j, field=field_h, |
| 21 | + mapper=mapper, |
| 22 | + ) |
| 23 | + mpo_H = res["mpo"] |
| 24 | + edges_1d = res["edges_1d"] |
| 25 | + sites = sorted({(site,) for edge in edges_1d for site in edge}) |
| 26 | + return { |
| 27 | + "mpo_H": mpo_H, |
| 28 | + "edges_1d": edges_1d, |
| 29 | + "sites": sites, |
| 30 | + "mapper": mapper, |
| 31 | + "res": res, |
| 32 | + } |
| 33 | + |
| 34 | + |
| 35 | +def build_initial_state(L, coupling_j, field_h, z_coord=4, theta_offset=None): |
| 36 | + """Build the intermediate-temperature product state (arXiv:2503.20870). |
| 37 | +
|
| 38 | + Parameters |
| 39 | + ---------- |
| 40 | + theta_offset : float or None |
| 41 | + Offset added to arcsin(h/(J·z)). Default is 2π/9 (paper value). |
| 42 | +
|
| 43 | + Returns |
| 44 | + ------- |
| 45 | + psi0 : quimb.tensor.MatrixProductState |
| 46 | + theta_paper : float |
| 47 | + """ |
| 48 | + if theta_offset is None: |
| 49 | + theta_offset = 2 * math.pi / 9 |
| 50 | + ratio = field_h / (coupling_j * z_coord) |
| 51 | + sin2theta = np.clip(ratio, -1.0, 1.0) |
| 52 | + theta_min = np.arcsin(sin2theta) |
| 53 | + theta_paper = theta_min + theta_offset |
| 54 | + psi0 = py.ps_to_mps(L, theta=0.5 * theta_paper) |
| 55 | + return psi0, theta_paper |
| 56 | + |
| 57 | + |
| 58 | +def build_trotter_gates(sites, edges_1d, field_h, coupling_j, dt, to_backend): |
| 59 | + """Build 2nd-order Strang-splitting Trotter gate stream. |
| 60 | +
|
| 61 | + RX(h·dt) → RZZ(2J·dt) → RX(h·dt) |
| 62 | +
|
| 63 | + Convention: py.rx(θ) = exp(-i θ X / 2), so the half-step |
| 64 | + exp(-i h·dt/2 · X) requires rx(h·dt). |
| 65 | +
|
| 66 | + Returns |
| 67 | + ------- |
| 68 | + list of (gate, where) tuples. |
| 69 | + """ |
| 70 | + rx_half = to_backend(py.rx(field_h * dt)) |
| 71 | + rzz_full = to_backend(py.rzz(2 * coupling_j * dt)) |
| 72 | + |
| 73 | + gates = [] |
| 74 | + for site in sites: |
| 75 | + gates.append((rx_half, site)) |
| 76 | + for edge in edges_1d: |
| 77 | + gates.append((rzz_full, edge)) |
| 78 | + for site in sites: |
| 79 | + gates.append((rx_half, site)) |
| 80 | + return gates |
| 81 | + |
| 82 | + |
| 83 | +def build_mpo_z_sq(Lx, Ly, mapper): |
| 84 | + """Build the (ΣZ_i/L)² MPO for measuring magnetization squared. |
| 85 | +
|
| 86 | + Returns |
| 87 | + ------- |
| 88 | + mpo_z_sq_offdiag : quimb.tensor.MatrixProductOperator |
| 89 | + Off-diagonal part: (1/L²) Σ_{i≠j} Z_i Z_j |
| 90 | + diagonal_shift : float |
| 91 | + Constant 1/L to add (from diagonal Z_i²=I terms). |
| 92 | + mpo_z : quimb.tensor.MatrixProductOperator |
| 93 | + The magnetization MPO M = (1/L) Σ Z_i (useful on its own). |
| 94 | + """ |
| 95 | + L = Lx * Ly |
| 96 | + Z_op = quimb.pauli("Z", dtype="complex128") |
| 97 | + |
| 98 | + builder = py.ham_tn(Lx=Lx, Ly=Ly, mapper=mapper, data_type="complex128") |
| 99 | + |
| 100 | + # Sites in 2D coordinates (what build_mpo expects) |
| 101 | + res = py.ham_tn.build_itf_lattice( |
| 102 | + L_x=Lx, L_y=Ly, lattice="square", cyclic=True, |
| 103 | + J=-1, field=1, mapper=mapper, |
| 104 | + ) |
| 105 | + all_sites_2d = list(res["one_d_to_two_d"].values()) |
| 106 | + |
| 107 | + # M = (1/L) Σ_i Z_i |
| 108 | + z_terms = [((Z_op,), (site,), 1.0 / L) for site in all_sites_2d] |
| 109 | + mpo_z = builder.build_mpo(z_terms, compress_each=True) |
| 110 | + |
| 111 | + # M² off-diagonal = (1/L²) Σ_{i≠j} Z_i Z_j |
| 112 | + z_sq_terms = [] |
| 113 | + for i, site_i in enumerate(all_sites_2d): |
| 114 | + for j, site_j in enumerate(all_sites_2d): |
| 115 | + if i != j: |
| 116 | + z_sq_terms.append( |
| 117 | + ((Z_op, Z_op), (site_i, site_j), 1.0 / (L * L)) |
| 118 | + ) |
| 119 | + |
| 120 | + mpo_z_sq_offdiag = builder.build_mpo(z_sq_terms, compress_each=True) |
| 121 | + diagonal_shift = 1.0 / L # from Z_i² = I |
| 122 | + |
| 123 | + return mpo_z_sq_offdiag, diagonal_shift, mpo_z |
| 124 | + |
| 125 | + |
| 126 | +def measure_energy(psi, mpo_H, L, optimizer): |
| 127 | + """<H>/L via MPO.""" |
| 128 | + e = py.core.expec_mpo(mpo_H, psi, contraction_opt=optimizer) |
| 129 | + return float(np.real(e)) / L |
| 130 | + |
| 131 | + |
| 132 | +def measure_z(psi, mpo_z, optimizer): |
| 133 | + """<ΣZ/L> = <M> via MPO.""" |
| 134 | + m = py.core.expec_mpo(mpo_z, psi, contraction_opt=optimizer) |
| 135 | + return float(np.real(m)) |
| 136 | + |
| 137 | + |
| 138 | +def measure_z_sq(psi, mpo_z_sq_offdiag, diagonal_shift, optimizer): |
| 139 | + """<(ΣZ/L)²> = <M²> via MPO + diagonal shift.""" |
| 140 | + off_diag = py.core.expec_mpo(mpo_z_sq_offdiag, psi, contraction_opt=optimizer) |
| 141 | + return float(np.real(off_diag)) + diagonal_shift |
0 commit comments