|
| 1 | +""" |
| 2 | +One-layer QAOA for a QUBO Hamiltonian — closed-form symbolic expectation. |
| 3 | +
|
| 4 | +This example uses ``tc.SymbolCircuit`` to derive the QAOA cost-function |
| 5 | +expectation |
| 6 | +
|
| 7 | + F(gamma, beta) = <psi(gamma, beta)| H_C |psi(gamma, beta)> |
| 8 | +
|
| 9 | +as a closed-form sympy expression in the two variational parameters gamma |
| 10 | +(phase separator) and beta (mixer), then cross-validates against a numeric |
| 11 | +tc.Circuit evaluation and plots the energy landscape. |
| 12 | +
|
| 13 | +Problem: Max-Cut on the 3-node path graph P3 (0 -- 1 -- 2) |
| 14 | +--------------------------------------------------------- |
| 15 | +QUBO (minimisation form, variables x_i in {0, 1}): |
| 16 | +
|
| 17 | + min f(x) = Q_00 x_0 + Q_11 x_1 + Q_22 x_2 |
| 18 | + + Q_01 x_0 x_1 + Q_12 x_1 x_2 |
| 19 | +
|
| 20 | + Q_diag = {0: -1, 1: -2, 2: -1} |
| 21 | + Q_off = {(0,1): +2, (1,2): +2} |
| 22 | +
|
| 23 | + (f(x) = -MaxCut(x), so minimising f maximises the cut.) |
| 24 | +
|
| 25 | +Ising mapping (x_i = (1 - Z_i) / 2, H_C = -f(x)): |
| 26 | + H_C = 1 - (Z_0 Z_1 + Z_1 Z_2) / 2 (to maximise) |
| 27 | +
|
| 28 | +QAOA ansatz (1 layer): |
| 29 | + |psi(gamma, beta)> = U_M(beta) U_C(gamma) |+>^3 |
| 30 | +
|
| 31 | + U_C(gamma) = exp(-i gamma H_C) |
| 32 | + x RZZ(-gamma) on (0,1) and RZZ(-gamma) on (1,2) |
| 33 | +
|
| 34 | + U_M(beta) = prod_i RX(2 beta) |
| 35 | +
|
| 36 | +Run: |
| 37 | + conda run -n 2602 python examples/qaoa_symbolic.py |
| 38 | +""" |
| 39 | + |
| 40 | +import sympy |
| 41 | +import numpy as np |
| 42 | +import matplotlib.pyplot as plt |
| 43 | +import tensorcircuit as tc |
| 44 | + |
| 45 | +# ── QUBO → Ising helpers ────────────────────────────────────────────────────── |
| 46 | + |
| 47 | + |
| 48 | +def qubo_to_ising(Q_diag: dict, Q_off: dict): |
| 49 | + """Convert QUBO coefficients to Ising (const, h, J) form. |
| 50 | +
|
| 51 | + QUBO: minimise sum_i Q_diag[i] x_i + sum_{i<j} Q_off[(i,j)] x_i x_j |
| 52 | + Ising: maximise const + sum_i h[i] Z_i + sum_{i<j} J[(i,j)] Z_i Z_j |
| 53 | +
|
| 54 | + The sign flip converts min→max. |
| 55 | + """ |
| 56 | + const = 0.0 |
| 57 | + h: dict = {} |
| 58 | + J: dict = {} |
| 59 | + for i, q in Q_diag.items(): |
| 60 | + # -q * x_i = -q * (1 - Z_i)/2 → const term -q/2, linear term +q/2 Z_i |
| 61 | + const -= q / 2 |
| 62 | + h[i] = h.get(i, 0) + q / 2 |
| 63 | + for (i, j), q in Q_off.items(): |
| 64 | + # -q * x_i x_j = -q (1-Z_i)(1-Z_j)/4 |
| 65 | + # constant: -q/4, linear: +q/4 Z_i + q/4 Z_j, quadratic: -q/4 Z_i Z_j |
| 66 | + const -= q / 4 |
| 67 | + h[i] = h.get(i, 0) + q / 4 |
| 68 | + h[j] = h.get(j, 0) + q / 4 |
| 69 | + J[(i, j)] = J.get((i, j), 0) - q / 4 |
| 70 | + return const, h, J |
| 71 | + |
| 72 | + |
| 73 | +# ── QAOA circuit builder ────────────────────────────────────────────────────── |
| 74 | + |
| 75 | + |
| 76 | +def build_qaoa(n, const, h, J, gamma, beta): |
| 77 | + """Return a SymbolCircuit for 1-layer QAOA with symbolic (gamma, beta).""" |
| 78 | + tc.set_backend("numpy") |
| 79 | + sc = tc.SymbolCircuit(n) |
| 80 | + |
| 81 | + # Initial state |+>^n = H^n |0>^n |
| 82 | + for i in range(n): |
| 83 | + sc.h(i) |
| 84 | + |
| 85 | + # Cost unitary: exp(-i gamma H_C) |
| 86 | + # exp(-i gamma h_i Z_i) = RZ(2 gamma h_i) |
| 87 | + # exp(-i gamma J_ij Z_i Z_j) = RZZ(2 gamma J_ij) |
| 88 | + for i, hi in sorted(h.items()): |
| 89 | + if hi: |
| 90 | + sc.rz(i, theta=2 * hi * gamma) |
| 91 | + for (i, j), Jij in sorted(J.items()): |
| 92 | + if Jij: |
| 93 | + sc.rzz(i, j, theta=2 * Jij * gamma) |
| 94 | + |
| 95 | + # Mixer unitary: exp(-i beta sum X_i) = prod RX(2 beta) |
| 96 | + for i in range(n): |
| 97 | + sc.rx(i, theta=2 * beta) |
| 98 | + |
| 99 | + return sc |
| 100 | + |
| 101 | + |
| 102 | +# ── main ────────────────────────────────────────────────────────────────────── |
| 103 | + |
| 104 | + |
| 105 | +def main(): |
| 106 | + # ── Problem ─────────────────────────────────────────────────────────────── |
| 107 | + n = 3 |
| 108 | + # MaxCut QUBO (minimisation): f(x) = -MaxCut(x), P3 path graph (0--1--2) |
| 109 | + # Q_diag[i] = -degree(i), Q_off[(i,j)] = +2 per edge |
| 110 | + Q_diag = {0: -1, 1: -2, 2: -1} |
| 111 | + Q_off = {(0, 1): 2, (1, 2): 2} |
| 112 | + |
| 113 | + print("=" * 60) |
| 114 | + print("QUBO: minimise f(x) = x^T Q x (= -MaxCut(x))") |
| 115 | + print("Q_diag =", Q_diag) |
| 116 | + print("Q_off =", Q_off) |
| 117 | + print("(Minimising f = Maximising MaxCut; optimal cut = 2)") |
| 118 | + print() |
| 119 | + |
| 120 | + # ── Ising Hamiltonian ───────────────────────────────────────────────────── |
| 121 | + const, h, J = qubo_to_ising(Q_diag, Q_off) |
| 122 | + print("Ising Hamiltonian H_C (to maximise):") |
| 123 | + terms = [f"{const:+.4f}"] |
| 124 | + for i, hi in sorted(h.items()): |
| 125 | + if hi: |
| 126 | + terms.append(f"{hi:+.4f} Z_{i}") |
| 127 | + for (i, j), Jij in sorted(J.items()): |
| 128 | + if Jij: |
| 129 | + terms.append(f"{Jij:+.4f} Z_{i}Z_{j}") |
| 130 | + print(" H_C =", " ".join(terms)) |
| 131 | + print() |
| 132 | + |
| 133 | + # ── Symbolic parameters ─────────────────────────────────────────────────── |
| 134 | + gamma = sympy.Symbol("gamma", real=True) |
| 135 | + beta = sympy.Symbol("beta", real=True) |
| 136 | + |
| 137 | + # ── Build circuit ───────────────────────────────────────────────────────── |
| 138 | + sc = build_qaoa(n, const, h, J, gamma, beta) |
| 139 | + print( |
| 140 | + f"QAOA circuit: {sc.gate_count()} gates, " f"free symbols: {sc.free_symbols()}" |
| 141 | + ) |
| 142 | + print() |
| 143 | + |
| 144 | + # ── Symbolic expectation F(gamma, beta) = <H_C> ────────────────────────── |
| 145 | + F = sympy.Integer(0) + const |
| 146 | + for i, hi in h.items(): |
| 147 | + if hi: |
| 148 | + F = F + hi * sc.expectation_ps(z=[i]) |
| 149 | + for (i, j), Jij in J.items(): |
| 150 | + if Jij: |
| 151 | + F = F + Jij * sc.expectation_ps(z=[i, j]) |
| 152 | + |
| 153 | + F = sympy.trigsimp(F.rewrite(sympy.cos)) |
| 154 | + print("Closed-form expectation F(gamma, beta) = <H_C>:") |
| 155 | + print(" ", F) |
| 156 | + print() |
| 157 | + |
| 158 | + # ── Symbolic gradient ───────────────────────────────────────────────────── |
| 159 | + dF_dgamma = sympy.trigsimp(sympy.diff(F, gamma)) |
| 160 | + dF_dbeta = sympy.trigsimp(sympy.diff(F, beta)) |
| 161 | + print("dF/dgamma =", dF_dgamma) |
| 162 | + print("dF/dbeta =", dF_dbeta) |
| 163 | + print() |
| 164 | + |
| 165 | + # ── Numerical cross-validation ──────────────────────────────────────────── |
| 166 | + print("Numerical cross-validation (symbolic vs tc.Circuit):") |
| 167 | + test_points = [(0.5, 0.3), (np.pi / 3, np.pi / 8), (1.0, 0.7)] |
| 168 | + for gv, bv in test_points: |
| 169 | + sym_val = float(F.subs({gamma: gv, beta: bv})) |
| 170 | + c = sc.to_circuit({gamma: gv, beta: bv}) |
| 171 | + ref_val = float(const) |
| 172 | + for i, hi in h.items(): |
| 173 | + if hi: |
| 174 | + ref_val += float(hi) * float(c.expectation_ps(z=[i]).real) |
| 175 | + for (i, j), Jij in J.items(): |
| 176 | + if Jij: |
| 177 | + ref_val += float(Jij) * float(c.expectation_ps(z=[i, j]).real) |
| 178 | + print( |
| 179 | + f" gamma={gv:.4f}, beta={bv:.4f}: " |
| 180 | + f"sym={sym_val:.6f} ref={ref_val:.6f} " |
| 181 | + f"ok={abs(sym_val - ref_val) < 1e-6}" |
| 182 | + ) |
| 183 | + print() |
| 184 | + |
| 185 | + # ── Landscape: lambdify for fast numpy evaluation ───────────────────────── |
| 186 | + F_func = sympy.lambdify([gamma, beta], F, "numpy") |
| 187 | + |
| 188 | + gammas = np.linspace(0, np.pi, 200) |
| 189 | + betas = np.linspace(0, np.pi / 2, 200) |
| 190 | + GG, BB = np.meshgrid(gammas, betas) |
| 191 | + FF = F_func(GG, BB) |
| 192 | + |
| 193 | + # QUBO cost landscape: <f(x)> = -<H_C> (minimisation framing) |
| 194 | + FF_loss = -FF |
| 195 | + |
| 196 | + # Optimal parameters (grid-search on QUBO cost → argmin) |
| 197 | + idx = np.unravel_index(np.argmin(FF_loss), FF_loss.shape) |
| 198 | + g_opt, b_opt, F_opt = GG[idx], BB[idx], FF_loss[idx] |
| 199 | + print( |
| 200 | + f"Grid-search optimum: gamma*={g_opt:.4f} beta*={b_opt:.4f} " |
| 201 | + f"QUBO cost*={F_opt:.4f} (ideal = {-2:.4f})" |
| 202 | + ) |
| 203 | + print() |
| 204 | + |
| 205 | + # ── Plot 2-D energy landscape ───────────────────────────────────────────── |
| 206 | + fig, ax = plt.subplots(figsize=(7, 5)) |
| 207 | + im = ax.pcolormesh(GG, BB, FF_loss, shading="auto", cmap="RdYlGn_r") |
| 208 | + cbar = fig.colorbar(im, ax=ax) |
| 209 | + cbar.set_label(r"$\langle f(x)\rangle(\gamma,\beta)$ (QUBO cost)", fontsize=12) |
| 210 | + |
| 211 | + ax.scatter( |
| 212 | + g_opt, |
| 213 | + b_opt, |
| 214 | + marker="*", |
| 215 | + s=200, |
| 216 | + color="white", |
| 217 | + zorder=5, |
| 218 | + label=f"minimum ({g_opt:.2f}, {b_opt:.2f})", |
| 219 | + ) |
| 220 | + ax.set_xlabel(r"$\gamma$", fontsize=13) |
| 221 | + ax.set_ylabel(r"$\beta$", fontsize=13) |
| 222 | + ax.set_title( |
| 223 | + "1-layer QAOA energy landscape\n" |
| 224 | + r"$F(\gamma,\beta)$ from symbolic expression (Max-Cut P3)", |
| 225 | + fontsize=12, |
| 226 | + ) |
| 227 | + ax.legend(fontsize=11) |
| 228 | + plt.tight_layout() |
| 229 | + |
| 230 | + out = "examples/qaoa_symbolic_landscape.png" |
| 231 | + plt.savefig(out, dpi=150) |
| 232 | + print(f"Landscape plot saved to {out}") |
| 233 | + |
| 234 | + |
| 235 | +if __name__ == "__main__": |
| 236 | + main() |
0 commit comments