实操实验室:用 VQE 求解 H₂ 基态能量¶
前面六章的能力——线路构建、模拟执行、测量统计——在这一章串成第一个完整的算法:变分量子本征求解器(VQE)。我们从零走完全流程:写出分子哈密顿量 → 构造参数化试探线路 → 测量能量期望值 → 经典优化器迭代更新参数,最终把 H₂ 简化模型的基态能量"压"到精确值,并逐行解读收敛过程。
变分法的理论推导(变分原理、拟设选取、优化器比较)见姊妹站量子计算算法教程·VQE 教程。本页示例基于 unified-quantum 0.1.0,所有输出均为实际运行结果;带随机性的部分(第 6 节)已单独标注。
本课知识点
变分原理与混合迭代——解释变分原理为什么保证 VQE 给出的能量是基态能量的上界,写出量子-经典交替的迭代流程。
H₂ 简化模型与精确参照值——写出两比特 H₂ 简化模型的泡利串哈密顿量,并用精确对角化计算基态能量作为参照。
构造试探态:HEA 与 UCCSD——能用
hea与uccsd_ansatz构造参数化试探线路,解释参数个数与线路结构的对应。测量能量期望值——能用
pauli_expectation计算泡利串期望值,解释基变换线路如何把 X/Y 测量转成 Z 测量。端到端求解:run_vqe_workflow——能用
run_vqe_workflow端到端求解基态能量,解读收敛历史并与精确值比较。有限采样与统计噪声——比较 statevector 精确能量与有限 shots 估计,解释 \(1/\sqrt N\) 噪声标度及其对优化的影响。
1. 变分原理:为什么这个循环能工作¶
对任何含参数的试探态 \(|\psi(\boldsymbol{\theta})\rangle\),变分原理(Rayleigh–Ritz)给出
其中 \(E_0\) 是哈密顿量 \(H\) 的真实基态能量。也就是说:无论参数取什么值,测得的能量都不会低于基态能量;于是"求基态能量"变成了一个经典优化问题——调 \(\boldsymbol{\theta}\),把 \(E(\boldsymbol{\theta})\) 往下压:
┌────────────┐ ┌──────────────┐ ┌──────────────┐
│ 制备 |ψ(θ)⟩ │──▶│ 测量 ⟨ψ|H|ψ⟩ │──▶│ 经典优化器 │
│ (拟设线路)│ │ (期望值估计)│ │ (更新 θ) │
└────────────┘ └──────────────┘ └──────┬───────┘
▲ │
└───────────────── 循环直至收敛 ◀─────┘
量子部分只负责"给定 θ,估计能量"这件经典计算机不擅长的事,且每次评估只需浅线路、少量测量——这正是 VQE 对 NISQ 设备友好的原因;而"往哪个方向调 θ"交给经典优化器。想深入变分原理的证明与拟设理论,见姊妹站 VQE 教程。
2. 目标体系:两比特 H₂ 简化模型¶
要测量 \(\langle H\rangle\),先把 \(H\) 分解成泡利串之和(量子化学程序做完选定基组、电子积分与映射后输出的标准形式):
本页用 uniqc 的 VQE 工作流自带的"H₂-like"两比特教学模型(系数单位 Hartree)——真实 H₂ 的四比特版本(13 项泡利串 + 核排斥能)见包仓库 examples/2_advanced/algorithms/vqe.py,流程完全相同。紧凑字符串的约定是:第 \(i\) 个字符作用在第 \(i\) 个量子比特上,例如 "ZI" 表示 \(Z_0 \otimes I_1\)。
动手前先做实验物理的好习惯:用精确对角化算出基态能量当参照值。
import numpy as np
H2_LIKE = [
("ZZ", -1.0523),
("ZI", 0.39793),
("IZ", -0.39793),
("XX", 0.18093),
]
# 把每个泡利串变成 2^n × 2^n 矩阵再求和(约定:第 i 个字符 ↔ 第 i 个比特)
I = np.eye(2)
X = np.array([[0, 1], [1, 0]])
Z = np.diag([1, -1])
PAULI = {"I": I, "X": X, "Z": Z}
def to_matrix(pauli_str):
m = PAULI[pauli_str[0]]
for ch in pauli_str[1:]:
m = np.kron(m, PAULI[ch])
return m
H = sum(coeff * to_matrix(p) for p, coeff in H2_LIKE)
print(np.round(np.linalg.eigvalsh(H), 6))
[-1.23323 -0.87137 0.236133 1.868467]
能谱从小到大排好了:基态 \(E_0 = -1.23323\) Ha,第一激发态 \(-0.87137\) Ha,能隙约 0.36 Ha。接下来 VQE 的任务就是:从一组随机的 \(\boldsymbol{\theta}\) 出发,把 \(E(\boldsymbol{\theta})\) 从初始值一路压到 \(-1.23323\)。
3. 构造试探态:HEA 与 UCCSD¶
拟设(ansatz)就是"参数 → 线路"的模板。uniqc 内置多种拟设,本页用最常用的两种。
硬件高效拟设 HEA(hea):每层对每个比特放旋转门、再按拓扑放纠缠门,结构浅、适合真机:
from uniqc import hea, hea_param_count
import numpy as np
print(hea_param_count(n_qubits=2, depth=2)) # 参数量 = 2 个旋转门 × 2 比特 × 2 层
theta_demo = np.array([0.4, -1.2, 2.1, 0.5, 0.3, -0.6, -0.7, 1.5])
demo = hea(n_qubits=2, depth=2, params=theta_demo)
demo.measure(0, 1) # 末尾补测量指令(第 6 节的 shots 模式需要)
print(demo.originir)
8
QINIT 2
CREG 2
RZ q[0], (0.4)
RY q[0], (-1.2)
RZ q[1], (2.1)
RY q[1], (0.5)
CNOT q[0], q[1]
CNOT q[1], q[0]
RZ q[0], (0.3)
RY q[0], (-0.6)
RZ q[1], (-0.7)
RY q[1], (1.5)
CNOT q[0], q[1]
CNOT q[1], q[0]
MEASURE q[0], c[0]
MEASURE q[1], c[1]
读一遍线路:每层先放 RZ+RY(默认旋转门组合),2 比特的 RING 拓扑每层发射 CNOT(0→1) 与 CNOT(1→0) 两个纠缠门;CNOT 没有参数,所以参数量是 \(2 \times 2 \times 2 = 8\)。注意两个细节:角度恰好为 0 的旋转门不会发射到线路里;Circuit 按实际用到的比特自动确定寄存器大小。
化学启发拟设 UCCSD(uccsd_ansatz):以 Hartree–Fock 参考态为起点,叠加单/双激发算符。对 4 比特、2 电子的 H₂:单激发 \(2\times 2 = 4\) 个、双激发 \(\binom{2}{2}\binom{2}{2} = 1\) 个,共 5 个参数:
from uniqc import uccsd_ansatz
theta_u = np.array([0.0, 0.0, 0.0, 0.0, 0.1]) # 只有双激发参数非零
chem = uccsd_ansatz(n_qubits=4, n_electrons=2, params=theta_u)
print(chem.originir)
QINIT 4
CREG 0
X q[0]
X q[1]
CNOT q[0], q[1]
CNOT q[1], q[2]
CNOT q[2], q[3]
RY q[3], (0.05)
CNOT q[2], q[3]
RY q[3], (-0.05)
CNOT q[1], q[3]
RY q[3], (0.05)
CNOT q[2], q[3]
RY q[3], (-0.05)
CNOT q[1], q[3]
CNOT q[0], q[3]
CNOT q[0], q[1]
开头的 X q[0]、X q[1] 把前 2 个自旋轨道置占据,这就是 HF 参考态;后面 8 个 CNOT 与 4 个 RY(角度 \(\theta/2\))构成双激发模块,把电子对在占据轨道与虚轨道之间"转动"。
版本提示(0.1.0)
实测 0.1.0 的 uccsd_ansatz 双激发分解存在缺陷:对 HF 初态,任意非零的双激发参数都会把线路变成固定输出,能量不再随参数变化,VQE 优化也因此几乎停在 HF 能量。在修复之前,端到端实验建议使用 HEA——本页即采用这一方案。
4. 测量能量期望值:pauli_expectation¶
有了 \(\boldsymbol{\theta}\to\) 线路,能量评估只剩一件事:对每个泡利串 \(P_i\) 测期望 \(\langle P_i\rangle\),再加权求和。pauli_expectation 负责单串期望,原理是基变换:Z 和 I 不用动,X 基测量前加一个 H 门,Y 基测量前加 \(S^\dagger\) 再加 H——全部转成 Z 测量后,按"偶宇称 − 奇宇称"统计。先用 Bell 态做个热身:
from uniqc import Circuit
from uniqc.algorithms.core.measurement import pauli_expectation
bell = Circuit()
bell.h(0)
bell.cx(0, 1)
bell.measure(0, 1)
for p in ("ZZ", "XX", "YY"):
print(f"<{p}> = {pauli_expectation(bell, p):+.6f}")
<ZZ> = +1.000000
<XX> = +1.000000
<YY> = -1.000000
\(|\Phi^+\rangle=\frac{1}{\sqrt2}(|00\rangle+|11\rangle)\) 在 Z 基与 X 基下都是完全关联的(\(\langle ZZ\rangle=\langle XX\rangle=+1\)),而在 Y 基下完全反关联(\(\langle YY\rangle=-1\))。除了紧凑字符串,函数还接受带下标与元组列表两种写法,三者等价:
print(pauli_expectation(bell, "Z0Z1"))
print(pauli_expectation(bell, [("Z", 0), ("Z", 1)]))
0.9999999999999998
0.9999999999999998
现在把四个期望值加权求和,就得到完整的能量函数:
from uniqc import hea
def build_ansatz(params):
c = hea(n_qubits=2, depth=2, params=params)
c.measure(0, 1) # 末尾测量指令:shots 模式必需(第 6 节)
return c
def energy(params, shots=None):
c = build_ansatz(params)
return sum(coeff * pauli_expectation(c, p, shots=shots) for p, coeff in H2_LIKE)
init = np.random.default_rng(0).uniform(-np.pi / 8, np.pi / 8, size=8)
c = build_ansatz(init)
for p, coeff in H2_LIKE:
print(f"<{p}> = {pauli_expectation(c, p):+.6f} 系数 {coeff:+.5f}")
print(f"E(初始参数) = {energy(init):+.6f} Ha")
<ZZ> = +0.859801 系数 -1.05230
<ZI> = +0.930952 系数 +0.39793
<IZ> = +0.925604 系数 -0.39793
<XX> = -0.012504 系数 +0.18093
E(初始参数) = -0.904902 Ha
初始参数离基态还差得远:\(E = -0.905\) Ha,比 \(E_0 = -1.233\) Ha 高出 0.33 Ha。注意这组初值用的随机种子正是工作流的默认初始化(default_rng(0)、范围 \([-\pi/8, \pi/8]\)),所以这里的 \(-0.904902\) 会与下一节收敛历史的第 0 次求值严格一致。
5. 端到端求解:run_vqe_workflow¶
run_vqe_workflow 把"拟设 → 逐项期望 → 经典优化"整条流水线打包:默认 HEA 拟设、pauli_expectation 精确评估(shots=None)、scipy 的 COBYLA 无梯度优化器(默认 maxiter=200、初始步长 rhobeg=0.1)。传入 §2 的哈密顿量与 §4 同款的初始化:
from uniqc.algorithms.workflows.vqe_workflow import run_vqe_workflow
result = run_vqe_workflow(H2_LIKE, n_qubits=2, depth=2)
exact = np.linalg.eigvalsh(H)[0]
for i, e in enumerate(result.history):
if i % 10 == 0 or i == len(result.history) - 1:
print(f"求值 {i:3d}: E = {e:+.6f} Ha")
print(f"最终能量 E = {result.energy:.6f} Ha(精确值 {exact:.6f},偏差 {abs(result.energy - exact):.2e} Ha)")
print(np.round(result.params, 3))
print(f"求值次数 {result.n_iter},成功 {result.success}:{result.message}")
求值 0: E = -0.904902 Ha
求值 10: E = -1.062113 Ha
求值 20: E = -1.164986 Ha
求值 30: E = -1.230096 Ha
求值 40: E = -1.231649 Ha
求值 50: E = -1.232433 Ha
求值 60: E = -1.232949 Ha
求值 70: E = -1.233146 Ha
求值 80: E = -1.233199 Ha
求值 90: E = -1.233214 Ha
求值 100: E = -1.233224 Ha
求值 110: E = -1.233228 Ha
求值 120: E = -1.233229 Ha
求值 130: E = -1.233229 Ha
求值 140: E = -1.233230 Ha
求值 150: E = -1.233230 Ha
求值 154: E = -1.233230 Ha
最终能量 E = -1.233230 Ha(精确值 -1.233230,偏差 7.55e-10 Ha)
[ 0.182 -0.926 -0.381 -0. 0.464 -0. 0. -0.645]
求值次数 155,成功 True:Return from COBYLA because the trust region radius reaches its lower bound.
这就是一次端到端的 VQE:155 次能量求值,从 -0.905 Ha 一路压到 -1.233230 Ha,与精确对角化偏差 \(7.55\times10^{-10}\) Ha——远小于化学精度(\(1.6\times10^{-3}\) Ha)。整个流程完全确定(初值种子、优化器都无随机源),本页输出可逐位复现。返回的 VQEResult 常用字段:energy(最优能量)、params(最优参数,可看到几个旋转角被优化到 \(\approx 0\))、history(每次求值的能量,本节用它打印收敛过程)、n_iter、success、message。
再看一眼"VQE 到底找到了什么态"——在最优参数处逐项复测:
c_opt = build_ansatz(result.params)
for p, coeff in H2_LIKE:
print(f"<{p}> = {pauli_expectation(c_opt, p):+.6f} 系数 {coeff:+.5f}")
<ZZ> = +1.000000 系数 -1.05230
<ZI> = -0.000030 系数 +0.39793
<IZ> = -0.000030 系数 -0.39793
<XX> = -1.000000 系数 +0.18093
\(\langle ZZ\rangle=+1\)、\(\langle ZI\rangle=\langle IZ\rangle\approx 0\)、\(\langle XX\rangle=-1\)——这正是基态 \(\frac{1}{\sqrt2}(|00\rangle-|11\rangle)\) 的指纹:两个比特完全关联、单个比特完全无偏、反 \(X\) 关联。能量 \(-1.0523\times 1+0.18093\times(-1)=-1.23323\) Ha,与理论一致。
6. 有限采样与统计噪声¶
到目前为止能量来自 statevector 精确概率。真实设备(以及 pauli_expectation(..., shots=N))只能做有限次测量:期望值是"偶宇称 − 奇宇称"的频率估计,天然带统计噪声。在最优参数处重复估计能量(以下结果每次运行略有不同,采样随机数未播种):
opt = result.params
for shots in (1000, 10000):
vals = [energy(opt, shots=shots) for _ in range(5)]
print(f"shots={shots:5d}: 均值 {np.mean(vals):+.6f} 标准差 {np.std(vals):.6f}")
shots= 1000: 均值 -1.228614 标准差 0.012547
shots=10000: 均值 -1.239676 标准差 0.004532
均值在精确值附近摆动,标准差从 0.0125 降到 0.0045,比值约 2.8(5 次重复的样本标准差本身也有涨落)——与 \(1/\sqrt N\) 标度预言的 \(\sqrt{10}\approx 3.2\) 倍一致。把噪声直接放进优化循环,会发生什么?
for k in range(2):
r = run_vqe_workflow(H2_LIKE, n_qubits=2, depth=2, shots=4096)
print(f"第 {k} 次运行: E = {r.energy:+.6f} Ha(求值 {r.n_iter} 次)")
第 0 次运行: E = -1.208294 Ha(求值 70 次)
第 1 次运行: E = -1.244179 Ha(求值 84 次)
两个现象都值得注意:其一,每次运行结果不同,且常常在离精确值还差 \(10^{-2}\) Ha 量级时就提前收敛(求值次数从 155 降到七八十)——噪声把能量地形"抹平"了,无梯度优化器难以分辨小改进;其二,收敛值可能低于精确值(如 -1.244179 Ha),这并不违反变分原理——变分原理约束的是真实期望值,而 shots 给出的是带 \(\sim0.005\) 统计误差的估计量。真机上还有器件噪声叠加在采样噪声之上,需要第 6 章的校准与误差缓解工具来收拾。
下一步¶
回到理论:变分原理证明、拟设比较(UCCSD vs HEA)、优化器与梯度方法(参数移位),见姊妹站 VQE 教程;同一套"拟设 + 期望 + 优化"框架换个哈密顿量就是 QAOA(姊妹站 QAOA 教程,包内对应
qaoa_workflow)。把本页实验放进带噪声的 dummy 虚拟机重跑,观察收敛变差的程度:第 3 章:本地与含噪模拟。
采样噪声与器件噪声的缓解手段:第 6 章:校准与误差缓解;把最优线路提交到(虚拟)真机:第 5 章:云端执行。
练习题¶
练习 1【变分原理与混合迭代】(→ 第 1 节)
打印
result.history[:10],找出比上一次求值更高的能量值,解释为什么 COBYLA 允许这类试探性求值、它们为什么不违反变分原理。用 §4 的
energy评估全零参数:先笔算(提示:全零参数下 HEA 只剩 CNOT,作用在 \(|00\rangle\) 上仍是 \(|00\rangle\)),再运行验证。
提示:\(|00\rangle\) 处 \(\langle ZZ\rangle=\langle ZI\rangle=\langle IZ\rangle=+1\)、\(\langle XX\rangle=0\)。
练习 2【H₂ 简化模型与精确参照值】(→ 第 2 节)
从
H2_LIKE中删掉("XX", 0.18093)项,先笔算新基态能量(此时 \(H\) 是对角的,基态能量 = 最小对角元),再用 §2 的对角化代码与run_vqe_workflow分别验证。给
H2_LIKE增加恒等项("II", 0.5),先预测 VQE 最终能量与优化后参数各自如何变化,再运行验证。
提示:恒等项对每个态的贡献都是同一个常数——它平移整个能量地形,但不改变任何梯度。
练习 3【构造试探态:HEA 与 UCCSD】(→ 第 3 节)
笔算
hea_param_count(n_qubits=4, depth=3),再运行验证,并解释为什么 CNOT 不贡献参数。把 §3 演示参数中的
2.1改成0.0,先预测 OriginIR 会少哪几行,再打印验证。笔算
uccsd_ansatz(n_qubits=6, n_electrons=3)的参数个数(单激发数 + 双激发数),再构造一次验证。
提示:单激发数 = 占据轨道数 × 虚轨道数;双激发数 = \(\binom{3}{2}\times\binom{3}{2}\)。
练习 4【测量能量期望值】(→ 第 4 节)
对 Bell 态先笔算 \(\langle ZX\rangle\) 与 \(\langle XZ\rangle\),再用
pauli_expectation验证。用
x(0)、x(1)制备 \(|11\rangle\) 并测量两个比特,先笔算 \(\langle II\rangle\)、\(\langle ZI\rangle\)、\(\langle IZ\rangle\)、\(\langle ZZ\rangle\) 四个值再验证;注意紧凑字符串第 \(i\) 个字符对应第 \(i\) 个比特,\(\langle II\rangle\) 恒为 \(+1\)。对同一参数用
shots=1000重复估计 \(\langle XX\rangle\) 20 次,统计标准差;换成shots=100000再来一轮,比较标准差缩小的倍数。
提示:单次频率估计的标准差 \(\sigma \leq 1/(2\sqrt{N})\),与 \(1/\sqrt N\) 同标度。
练习 5【端到端求解:run_vqe_workflow】(→ 第 5 节)
把
depth从 2 改成 1,先预测参数个数与能否收敛到 -1.233230 Ha,再运行并比较求值次数。写一个两参数自定义拟设:
ry(0, θ0)、ry(1, θ1)、cx(0, 1)、测量两个比特。先证明它能精确表示基态 \(\frac{1}{\sqrt2}(|00\rangle-|11\rangle)\)(解出解析的 \(\theta_0,\theta_1\)),再用run_vqe_workflow(H2_LIKE, n_qubits=2, ansatz=..., init_params=...)收敛,检查最优参数是否落在解析值附近。
提示:基态对应 \(\theta_0=-\pi/2\)、\(\theta_1=0\);自定义拟设必须显式传
init_params(工作流无法推断参数个数)。
练习 6【有限采样与统计噪声】(→ 第 6 节)
把 §6 第一段的 shots 改成 2000 与 20000 各重复 5 次,比较两组标准差之比是否接近 \(\sqrt{10}\)。
解释为什么 shots-VQE 的收敛值可以低于精确基态能量(例如本页的 -1.244179 Ha),而这并不违反变分原理。
提示:变分原理约束真实期望值 \(\langle\psi(\boldsymbol{\theta})|H|\psi(\boldsymbol{\theta})\rangle\),而优化器看到的是它的含噪估计量。