Demo 3:QAOA 投资组合

第 14 章 优化与数值量子算法 · 本章演示 · 代码演示总索引

可视化:双图 —— 3 资产 Markowitz QUBO

对应教程:第 14 章 优化与数值量子算法 中相关小节 · 依赖安装:pip install "unified-quantum[visualization,simulation]"

实现要点

核心:Toy Markowitz portfolio over 3 assets A, B, C with expected returns μ = (0.10, 0.15, 0.08), covariance Σ, risk aversion λ = 4.0, and a cardinality penalty that biases toward 2-asset portfolios. The quadratic utility is encoded as a QUBO H_C (negated because uniqc's qaoa_ansatz MAXIMIZES ⟨H_C⟩) and solved by p=1 QAOA. We sweep (γ, β), overlay a classical hill-climb trajectory, then histogram the top bitstrings at the optimum — the dominant sampling peaks correspond to the asset subsets QAOA suggests holding.

完整源码

demos/ch14-optimization-numerics/03_qaoa_portfolio.py
  1"""
  2ch14-demo-3: QAOA portfolio optimization (toy 3-asset mean-variance)
  3
  4对应教程:chenzhaoyun.com/quantum-tutorial/ch14-optimization-numerics/constraint-satisfaction.html
  5核心:Toy Markowitz portfolio over 3 assets A, B, C with expected returns
  6     μ = (0.10, 0.15, 0.08), covariance Σ, risk aversion λ = 4.0, and a
  7     cardinality penalty that biases toward 2-asset portfolios. The
  8     quadratic utility is encoded as a QUBO H_C (negated because uniqc's
  9     qaoa_ansatz MAXIMIZES ⟨H_C⟩) and solved by p=1 QAOA. We sweep (γ, β),
 10     overlay a classical hill-climb trajectory, then histogram the top
 11     bitstrings at the optimum — the dominant sampling peaks correspond
 12     to the asset subsets QAOA suggests holding.
 13依赖:pip install unified-quantum[visualization]
 14运行:python demos/ch14-optimization-numerics/03_qaoa_portfolio.py
 15"""
 16from __future__ import annotations
 17
 18from pathlib import Path
 19
 20import matplotlib
 21matplotlib.use("Agg")
 22import matplotlib.pyplot as plt
 23import numpy as np
 24
 25from uniqc import qaoa_ansatz, submit_task, wait_for_result
 26
 27OUT = Path(__file__).parent
 28
 29# === 1. Toy 3-asset portfolio: expected returns & covariance ===
 30ASSETS = ["A", "B", "C"]
 31MU = np.array([0.10, 0.15, 0.08])
 32COV = np.array([
 33    [0.0100, 0.0015, -0.0008],
 34    [0.0015, 0.0200, 0.0010],
 35    [-0.0008, 0.0010, 0.0120],
 36])
 37# Risk aversion
 38LAMBDA = 4.0
 39# Cardinality constraint penalty (encourages holding ≈ 2 assets)
 40CARD_PEN = 0.30
 41CARD_TARGET = 2
 42
 43# QUBO encoding: w_i ∈ {0, 1} is whether we hold asset i. Bitstring 'qb0
 44# qb1 qb2' (MSB first) holds asset i iff qb_i = 1.
 45N_QUBITS = 3
 46
 47# Build H_C from the Markowitz objective. C(w) = w^T (λ·COV) w - μ^T w +
 48# c · (Σ w_i - 2)^2. Ising: w_i = (1 - Z_i)/2, so the per-axis Z coefficient is
 49#   α_i = -linear_i/2 - Σ_{j≠i} quadratic_ij / 4
 50# QAOA in uniqc MAXIMIZES ⟨H_C⟩, so we negate: H_C = -α_i Z_i - β_ij Z_i Z_j.
 51linear = np.array([
 52    LAMBDA * COV[i, i] - MU[i] + CARD_PEN * (1 - 2 * CARD_TARGET)
 53    for i in range(N_QUBITS)
 54])
 55quad = {
 56    (i, j): 2 * (LAMBDA * COV[i, j] + CARD_PEN)
 57    for i in range(N_QUBITS) for j in range(i + 1, N_QUBITS)
 58}
 59alpha = np.array([
 60    -linear[i] / 2 - sum(quad.get((min(i, j), max(i, j)), 0) / 4
 61                         for j in range(N_QUBITS) if j != i)
 62    for i in range(N_QUBITS)
 63])
 64beta = {ij: q / 4 for ij, q in quad.items()}
 65
 66H_C = [(f"Z{i}", -alpha[i]) for i in range(N_QUBITS)]
 67H_C += [(f"Z{i}Z{j}", -b) for (i, j), b in beta.items()]
 68
 69
 70def portfolio_cost(bs: str) -> float:
 71    w = np.array([int(c) for c in bs.zfill(N_QUBITS)])
 72    return float(w @ (LAMBDA * COV) @ w - MU @ w
 73                 + CARD_PEN * (w.sum() - CARD_TARGET) ** 2)
 74
 75
 76def portfolio_dict(bs: str) -> str:
 77    w = np.array([int(c) for c in bs.zfill(N_QUBITS)])
 78    held = [ASSETS[i] for i in range(N_QUBITS) if w[i] == 1]
 79    return ", ".join(held) if held else "(cash)"
 80
 81
 82# === 2. QAOA evaluation ===
 83SHOTS = 600
 84
 85
 86def run_qaoa(gamma: float, beta: float, shots: int = SHOTS):
 87    c = qaoa_ansatz(H_C, p=1, gammas=np.array([gamma]), betas=np.array([beta]))
 88    for q in range(N_QUBITS):
 89        c.measure(q)
 90    return wait_for_result(
 91        submit_task(circuit=c, backend="dummy:local:simulator", shots=shots)
 92    ).counts
 93
 94
 95def evaluate_qaoa(gamma: float, beta: float, shots: int = SHOTS) -> float:
 96    counts = run_qaoa(gamma, beta, shots)
 97    total = sum(counts.values())
 98    if total <= 0:
 99        return 0.0
100    return sum(portfolio_cost(bs) * cnt for bs, cnt in counts.items()) / total
101
102
103# === 3. γ × β landscape scan + hill-climb ===
104print("ch14-demo-3: QAOA portfolio optimization (3 assets)")
105print(f"  assets={ASSETS}, μ={MU.tolist()}, λ={LAMBDA}, card target={CARD_TARGET}")
106print(f"  shots per QAOA run: {SHOTS}")
107
108G = np.linspace(0.10, 1.30, 11)
109B = np.linspace(0.10, 0.90, 11)
110landscape = np.zeros((len(G), len(B)))
111for gi, g in enumerate(G):
112    for bi, b in enumerate(B):
113        landscape[gi, bi] = evaluate_qaoa(g, b)
114print(f"  grid scan: {len(G)} x {len(B)} = {len(G) * len(B)} points")
115
116gi0, bi0 = np.unravel_index(int(np.argmin(landscape)), landscape.shape)
117g_star, b_star = float(G[gi0]), float(B[bi0])
118c_star = float(landscape[gi0, bi0])
119trajectory = [(g_star, b_star, c_star)]
120rng = np.random.default_rng(0)
121for step in range(12):
122    sigma = max(0.04, 0.18 * (0.55 ** step))
123    g_try = float(np.clip(g_star + rng.normal(0, sigma), 0.05, np.pi))
124    b_try = float(np.clip(b_star + rng.normal(0, sigma), 0.05, np.pi))
125    c_try = evaluate_qaoa(g_try, b_try)
126    if c_try < c_star:
127        g_star, b_star, c_star = g_try, b_try, c_try
128    trajectory.append((g_star, b_star, c_star))
129
130counts = run_qaoa(g_star, b_star, shots=4000)
131hist = {bs.zfill(N_QUBITS): cnt for bs, cnt in counts.items()}
132total_best = sum(hist.values())
133hist_sorted = sorted(hist.items(), key=lambda kv: -kv[1])
134top_states = hist_sorted[:8]
135
136# === 5. visualisation ===
137fig = plt.figure(figsize=(12.5, 5.4))
138gs = fig.add_gridspec(1, 2, width_ratios=[1.05, 1.0])
139
140ax1 = fig.add_subplot(gs[0, 0])
141im = ax1.imshow(
142    landscape, origin="lower", aspect="auto", cmap="magma",
143    extent=[B[0] - (B[1] - B[0]) / 2, B[-1] + (B[1] - B[0]) / 2,
144            G[0] - (G[1] - G[0]) / 2, G[-1] + (G[1] - G[0]) / 2],
145)
146cs = ax1.contour(B, G, landscape, levels=4, colors="white", linewidths=0.7, alpha=0.5)
147ax1.clabel(cs, inline=True, fontsize=8, fmt="%.2f")
148ax1.plot([b_star], [g_star], "*", color="#ffd700", ms=18, mec="black", mew=0.9,
149         label=rf"opt $\gamma^*={g_star:.3f},\beta^*={b_star:.3f}$")
150ax1.plot([t[1] for t in trajectory], [t[0] for t in trajectory],
151         "-o", color="#00d4ff", ms=3, lw=1.0, alpha=0.8,
152         label="classical hill-climb")
153ax1.set_xlabel(r"mixer angle $\beta$")
154ax1.set_ylabel(r"cost angle $\gamma$")
155ax1.set_title(r"p=1 QAOA $\langle H_C\rangle$ on Markowitz QUBO (11×11 scan)")
156plt.colorbar(im, ax=ax1).set_label(r"$\langle H_C\rangle$ (lower = better)")
157ax1.legend(loc="upper right", fontsize=8)
158
159ax2 = fig.add_subplot(gs[0, 1])
160labels = [bs for bs, _ in top_states]
161probs = [cnt / total_best for _, cnt in top_states]
162costs = [portfolio_cost(bs) for bs, _ in top_states]
163best_cost = min(portfolio_cost(f"{i:03b}") for i in range(8))
164colors = ["#2ca02c" if abs(c - best_cost) < 1e-6 else
165          "#1f77b4" if c < 0 else "#d62728" for c in costs]
166ax2.bar(range(len(labels)), probs, color=colors, ec="black", lw=0.5)
167for k, (lab, p, c) in enumerate(zip(labels, probs, costs)):
168    ax2.text(k, p + 0.012, f"{lab}\n[{portfolio_dict(lab)}]\ncost={c:.3f}",
169             ha="center", va="bottom", fontsize=7)
170ax2.set_xticks(range(len(labels)))
171ax2.set_xticklabels(labels, rotation=0, fontsize=8)
172ax2.set_ylabel("sampling probability")
173ax2.set_title(rf"top bitstrings at $(\gamma^*,\beta^*)$ — $\langle H_C\rangle$={c_star:.3f}")
174ax2.set_ylim(0, max(probs) * 1.55)
175ax2.grid(alpha=0.3, axis="y")
176
177plt.suptitle("ch14 demo 3: QAOA portfolio optimization (3-asset Markowitz)", y=1.02)
178plt.tight_layout()
179out_path = OUT / "03_qaoa_portfolio.png"
180plt.savefig(out_path, dpi=120, bbox_inches="tight")
181plt.close()
182
183# === 6. stdout 关键统计 ===
184print(f"\n  total QAOA circuit evaluations: "
185      f"{len(G) * len(B) + len(trajectory) - 1 + 1}")
186print(f"  grid best      : cost={float(landscape.min()):.4f} "
187      f"at γ={G[gi0]:.3f}, β={B[bi0]:.3f}")
188print(f"  after hill-climb: γ*={g_star:.4f}, β*={b_star:.4f}")
189print(f"  mean cost       : {c_star:.4f}")
190
191print(f"  classical ground-truth costs over all 8 portfolios:")
192for i in range(8):
193    bs = f"{i:03b}"
194    print(f"    |{bs}>  [{portfolio_dict(bs)}]  cost={portfolio_cost(bs):+.4f}")
195
196print(f"  top 4 QAOA samples:")
197for bs, cnt in hist_sorted[:4]:
198    print(f"    |{bs}>  [{portfolio_dict(bs)}]  p={cnt/total_best:.4f}  "
199          f"cost={portfolio_cost(bs):+.4f}")
200print("  PNG ->", out_path)

运行结果

Demo 3:QAOA 投资组合

运行方式

# 仓库根目录执行;输出 PNG 到 demos/ch14-optimization-numerics/03_qaoa_portfolio.png,关键统计到 stdout
.venv/bin/python3 demos/ch14-optimization-numerics/03_qaoa_portfolio.py

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