Demo 2:QAOA MaxCut 4-node

第 8 章 量子机器学习 · 本章演示 · 代码演示总索引

可视化:双图(热图+top bitstring) —— 14×14 网格+爬山轨迹

对应教程:第 8 章 量子机器学习 中相关小节 · 依赖安装:pip install "unified-quantum[visualization,simulation]"

实现要点

核心:K4 是 4 节点完全图,3-正则,含 6 条边;3-正则图上 QAOA p=1 的 最坏情形近似比下界为 r_1 = 0.6924(Farhi et al., 2014)。 这里在 (γ, β) 网格上扫描 p=1 QAOA 期望成本 ⟨H_C⟩,画出等高线 + 经典爬山优化轨迹;K4 这个特定实例上 QAOA p=1 能达到 ~0.92 的 实际近似比(远好于最坏情形保证)。

完整源码

demos/ch08-qml/02_qaoa_maxcut_4node.py
  1"""
  2ch08-qml demo 2: QAOA on the 4-node MaxCut (K4)
  3
  4对应教程:chenzhaoyun.com/quantum-tutorial/ch08-qml/qaoa-tutorial.html
  5核心:K4 是 4 节点完全图,3-正则,含 6 条边;3-正则图上 QAOA p=1 的
  6     最坏情形近似比下界为 r_1 = 0.6924(Farhi et al., 2014)。
  7     这里在 (γ, β) 网格上扫描 p=1 QAOA 期望成本 ⟨H_C⟩,画出等高线 +
  8     经典爬山优化轨迹;K4 这个特定实例上 QAOA p=1 能达到 ~0.92 的
  9     实际近似比(远好于最坏情形保证)。
 10
 11依赖:pip install unified-quantum[visualization]
 12运行:python demos/ch08-qml/02_qaoa_maxcut_4node.py
 13"""
 14from __future__ import annotations
 15
 16from pathlib import Path
 17
 18import matplotlib
 19matplotlib.use("Agg")
 20import matplotlib.pyplot as plt
 21import numpy as np
 22
 23from uniqc import qaoa_ansatz, submit_task, wait_for_result
 24
 25OUT = Path(__file__).parent
 26
 27# === 1. K4 graph and problem Hamiltonian ===
 28# Edges of K4: every pair of the 4 nodes
 29EDGES = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
 30N_QUBITS = 4
 31N_EDGES = len(EDGES)
 32OPT_CUT = 4  # K4 MaxCut optimal: partition {0,1}|{2,3} → 4 cut edges
 33
 34# Cost Hamiltonian H_C = sum_{(i,j)∈E} (1 - Z_i Z_j)/2
 35# (1 - Z_i Z_j)/2 contributes +1 iff cut. The constant 1/2 per edge shifts
 36# energy by |E|/2; we drop the constant and encode with negative Z_i Z_j
 37# coefficients so that QAOA maximises <H_C>.
 38H_C = [(f"Z{i}Z{j}", -0.5) for i, j in EDGES]
 39
 40SHOTS = 1500
 41
 42
 43def evaluate_qaoa(gamma: float, beta: float, shots: int = SHOTS) -> tuple[float, dict]:
 44    """Build the p=1 QAOA circuit, measure, return (mean_cut_count, raw_counts)."""
 45    c = qaoa_ansatz(H_C, p=1, gammas=np.array([gamma]), betas=np.array([beta]))
 46    for q in range(N_QUBITS):
 47        c.measure(q)
 48    r = wait_for_result(submit_task(circuit=c, backend="dummy:local:simulator", shots=shots))
 49    counts = r.counts
 50    total = sum(counts.values())
 51    if total <= 0:
 52        return 0.0, counts
 53    cut_count = 0.0
 54    for i, j in EDGES:
 55        p_same = 0.0
 56        for bs, cnt in counts.items():
 57            bs_str = bs.zfill(N_QUBITS)
 58            qi = int(bs_str[N_QUBITS - 1 - i])
 59            qj = int(bs_str[N_QUBITS - 1 - j])
 60            if qi == qj:
 61                p_same += cnt
 62        p_same /= total
 63        cut_count += 1.0 - p_same
 64    return cut_count, counts
 65
 66
 67# === 2. γ × β grid scan (one quantum submission per cell) ===
 68print("ch08-qml-demo-2: QAOA p=1 on K4 MaxCut")
 69print(f"  nodes={N_QUBITS}, edges={N_EDGES}, optimal cut={OPT_CUT}")
 70print(f"  shots per QAOA run: {SHOTS}")
 71
 72G = np.linspace(0.05, 1.4, 14)  # γ
 73B = np.linspace(0.05, 0.95, 14)  # β
 74cut_grid = np.full((len(G), len(B)), np.nan)
 75total_evals = 0
 76for gi, g in enumerate(G):
 77    for bi, b in enumerate(B):
 78        c, _ = evaluate_qaoa(g, b)
 79        cut_grid[gi, bi] = c
 80        total_evals += 1
 81
 82# === 3. classical hill-climb starting from the grid optimum ===
 83gi_best, bi_best = np.unravel_index(int(np.nanargmax(cut_grid)), cut_grid.shape)
 84g_best, b_best = float(G[gi_best]), float(B[bi_best])
 85c_best, best_counts = evaluate_qaoa(g_best, b_best, shots=8000)
 86
 87# A short COBYLA-like refinement: gradient-free random walk
 88rng = np.random.default_rng(0)
 89trajectory = [(g_best, b_best, c_best)]
 90for step in range(20):
 91    sigma = max(0.03, 0.12 * (0.6 ** step))
 92    g_try = g_best + rng.normal(0, sigma)
 93    b_try = b_best + rng.normal(0, sigma)
 94    g_try = max(0.01, min(np.pi, g_try))
 95    b_try = max(0.01, min(np.pi, b_try))
 96    c_try, _ = evaluate_qaoa(g_try, b_try, shots=4000)
 97    if c_try > c_best:
 98        g_best, b_best, c_best = g_try, b_try, c_try
 99        best_counts = _
100    trajectory.append((g_best, b_best, c_best))
101
102# === 4. Build bitstring histogram for the best run ===
103hist = {bs.zfill(N_QUBITS): cnt for bs, cnt in best_counts.items()}
104total_best = sum(hist.values())
105hist_sorted = sorted(hist.items(), key=lambda kv: -kv[1])
106top_states = hist_sorted[:8]
107
108# best observed single sample's cut
109def cut_of(bs: str) -> int:
110    bs = bs.zfill(N_QUBITS)
111    return sum(1 for i, j in EDGES if int(bs[N_QUBITS - 1 - i]) != int(bs[N_QUBITS - 1 - j]))
112
113best_sample_cut = max(cut_of(bs) for bs, _ in top_states)
114
115# === 5. matplotlib 可视化 ===
116fig = plt.figure(figsize=(12.5, 5.4))
117gs = fig.add_gridspec(1, 2, width_ratios=[1.05, 1.0])
118
119# Left: heatmap of ⟨cuts⟩ with optimal marker
120ax1 = fig.add_subplot(gs[0, 0])
121im = ax1.imshow(
122    cut_grid, origin="lower", aspect="auto", cmap="viridis",
123    extent=[B[0] - (B[1] - B[0]) / 2, B[-1] + (B[1] - B[0]) / 2,
124            G[0] - (G[1] - G[0]) / 2, G[-1] + (G[1] - G[0]) / 2],
125)
126ax1.plot([b_best], [g_best], "*", color="#ffd700", ms=18, mec="black", mew=0.9,
127         label=fr"optimum $\gamma^*={g_best:.3f}$, $\beta^*={b_best:.3f}$")
128cs = ax1.contour(B, G, cut_grid, levels=[1.0, 2.0, 3.0, 3.5], colors="white",
129                 linewidths=0.8, alpha=0.55)
130ax1.clabel(cs, inline=True, fontsize=8, fmt="%.1f")
131# trajectory line
132traj_g = [t[0] for t in trajectory]
133traj_b = [t[1] for t in trajectory]
134ax1.plot(traj_b, traj_g, "-o", color="#ff6f61", ms=3, lw=1.0, alpha=0.75,
135         label="classical hill-climb trajectory")
136ax1.set_xlabel(r"mixer angle $\beta$")
137ax1.set_ylabel(r"cost angle $\gamma$")
138ax1.set_title(r"p=1 QAOA $\langle H_C\rangle$ landscape on K4")
139cbar = plt.colorbar(im, ax=ax1)
140cbar.set_label(r"mean # cuts (out of 4)")
141ax1.legend(loc="lower left", fontsize=8)
142
143# Right: histogram of top bitstrings at (γ*, β*)
144ax2 = fig.add_subplot(gs[0, 1])
145labels = [bs for bs, _ in top_states]
146probs = [cnt / total_best for _, cnt in top_states]
147cuts = [cut_of(bs) for bs, _ in top_states]
148colors = ["#2ca02c" if c == OPT_CUT else "#1f77b4" if c == OPT_CUT - 1 else "#d62728" for c in cuts]
149bars = ax2.bar(range(len(labels)), probs, color=colors, ec="black", lw=0.5)
150for k, (lab, p, c) in enumerate(zip(labels, probs, cuts)):
151    ax2.text(k, p + 0.012, f"{lab}\ncut={c}", ha="center", va="bottom",
152             fontsize=7.5)
153ax2.set_xticks(range(len(labels)))
154ax2.set_xticklabels(labels, rotation=0, fontsize=8)
155ax2.set_ylabel("sampling probability")
156ax2.set_title(fr"top bitstrings at $(\gamma^*,\beta^*)$ — best cut in samples = {best_sample_cut}/{OPT_CUT}")
157ax2.set_ylim(0, max(probs) * 1.45)
158ax2.grid(alpha=0.3, axis="y")
159
160plt.suptitle("ch08-qml demo 2: QAOA p=1 on K4 MaxCut", y=1.02)
161plt.tight_layout()
162out_path = OUT / "02_qaoa_maxcut_4node.png"
163plt.savefig(out_path, dpi=120, bbox_inches="tight")
164plt.close()
165
166# === 6. stdout 关键统计 ===
167print(f"  total QAOA circuit evaluations: {total_evals + 1 + (len(trajectory) - 1)}")
168print(f"  grid scan size: {len(G)}x{len(B)} = {len(G)*len(B)} cells")
169print(f"  grid best      : cuts={float(cut_grid.max()):.3f}/{OPT_CUT} "
170      f"at γ={G[gi_best]:.3f}, β={B[bi_best]:.3f}")
171print(f"  after hill-climb: γ*={g_best:.4f}, β*={b_best:.4f}")
172print(f"  mean cut count  : {c_best:.4f} / {OPT_CUT}")
173print(f"  approximation r : {c_best/OPT_CUT:.4f}  (3-regular worst-case guarantee = 0.6924)")
174print(f"  best single sample cut: {best_sample_cut} / {OPT_CUT}")
175print(f"  top 3 bitstrings:")
176for bs, cnt in hist_sorted[:3]:
177    print(f"    |{bs}>  → cut={cut_of(bs)}, prob={cnt/total_best:.4f}")
178print(f"  PNG -> {out_path}")

运行结果

Demo 2:QAOA MaxCut 4-node

运行方式

# 仓库根目录执行;输出 PNG 到 demos/ch08-qml/02_qaoa_maxcut_4node.png,关键统计到 stdout
.venv/bin/python3 demos/ch08-qml/02_qaoa_maxcut_4node.py

← 本章演示 · ↑ 代码演示总索引 · ⌂ 教程门户