Demo 3:QAOA 投资组合¶
可视化:双图 —— 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)
运行结果¶

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