校准与误差缓解:从含噪计数到可信结果¶
真实量子硬件的测量结果从来不是理想概率分布:读出时比特可能被读反(读出误差),门执行时会偏离幺正(门误差)。UnifiedQuantum 用两层架构对付它们:校准层(uniqc.calibration)负责跑表征实验、把结果连同时间戳写入缓存 ~/.uniqc/calibration_cache/;缓解层(uniqc.qem)负责读缓存、修正测量结果,并强制校准数据的新鲜度(TTL)。本章把这两层完整走一遍——全部在本地 dummy 后端上实际运行,不需要任何 token。
想深入了解"表征测量结果"的量子力学背景,见姊妹站量子态层析教程;误差缓解最大的受益者是变分算法,见VQE 教程。本页示例基于 unified-quantum 0.1.0,所有输出均为实际运行结果。
本课知识点
构造含噪 dummy 后端——能用
DummyAdapter的noise_model注入读出翻转与去极化噪声,并解释[p(0→1), p(1→0)]各分量的含义。读出校准与混淆矩阵——能写出混淆矩阵 \(C[\text{meas}][\text{prep}]=P(\text{meas}\,|\,\text{prep})\) 的定义,用
ReadoutCalibrator测出它并计算 assignment fidelity。M3 线性反演缓解——能手算 \(n_{\text{corr}}=C^{-1}n_{\text{obs}}\) 的读出修正,并用
M3Mitigator/ReadoutEM在单比特与双比特计数上验证。校准缓存与 TTL 新鲜度——能解释
calibrated_at+max_age_hours的缓存设计,并主动触发StaleCalibrationError。XEB 交叉熵基准测试——能解释随机线路与 normalized linear XEB 估计器如何随深度指数衰减,写出拟合模型 \(F(m)=A\,r^m+B\) 中各参数的含义。
一键校准与基准工作流——能用
run_1q_xeb_workflow串联"校准→读出缓解→XEB→拟合",比较不同噪声水平与开关缓解时的拟合结果。
1. 构造含噪 dummy 后端:先看见误差¶
读出误差是最便宜、也最容易修正的系统误差:制备 \(|1\rangle\) 的比特有一定概率被读成 0,反之亦然。DummyAdapter 的 noise_model 让我们在本地精确复现这类误差——"readout": [0.07, 0.10] 的含义是 \(P(0\to1)=0.07\)(制备 \(|0\rangle\) 被误读为 1)、\(P(1\to0)=0.10\)(制备 \(|1\rangle\) 被误读为 0):
from uniqc import Circuit
from uniqc.backend_adapter.task.adapters import DummyAdapter
adapter = DummyAdapter(noise_model={"readout": [0.07, 0.10]})
circuit = Circuit(1)
circuit.x(0)
circuit.measure(0)
task_id = adapter.submit(circuit.originir, shots=400)
print(adapter.query(task_id)["result"])
{'0': 40, '1': 360}
线路制备的是确定的 \(|1\rangle\),理想计数应为 {0: 0, 1: 400};实际却有整整 10% 落在 '0' 上——正是 \(P(1\to0)=0.10\) 的直接体现。noise_model 还支持 "depol_1q"/"depol_2q"(门去极化噪声)和 "readout" 的标量、逐比特字典(如 {0: [0.06, 0.08], 1: [0.04, 0.05]})写法。
一个对教学很友好的细节:dummy 后端把精确概率按 shots 四舍五入成计数,不做随机采样,所以本章所有输出都是确定性的、可逐字复现;真实硬件上的计数才有二项涨落。
2. 读出校准:测出混淆矩阵¶
修正读出误差的前提是先测量它。ReadoutCalibrator 的做法很直接:对每个比特逐一制备基础态 \(|0\rangle\)、\(|1\rangle\),反复测量并统计误读概率,得到 2×2 混淆矩阵(confusion matrix)\(C\),其中 \(C[\text{meas}][\text{prep}]=P(\text{meas}\,|\,\text{prep})\);对角元均值即 assignment fidelity:
import tempfile
from pathlib import Path
from uniqc.calibration.readout import ReadoutCalibrator
cache_dir = tempfile.mkdtemp(prefix="uniqc-cal-")
calibrator = ReadoutCalibrator(adapter=adapter, shots=400, cache_dir=cache_dir)
cal = calibrator.calibrate_1q(0)
print(cal.confusion_matrix)
print(cal.assignment_fidelity)
for p in sorted(Path(cache_dir).glob("*.json")):
print(p.name)
((0.93, 0.1), (0.07, 0.9))
0.915
readout_1q_dummy_q0_20260905T033125.620127Z.json
对照注入的噪声:第 0 行(测得 0)是 \([P(0|0)=0.93,\ P(0|1)=0.10]\),第 1 行(测得 1)是 \([0.07, 0.90]\)——校准实验精确还原了 noise_model。assignment fidelity \(=(0.93+0.90)/2=0.915\)。校准结果同时被写入缓存目录,文件名格式为 {type}_{backend}_{qubit}_{timestamp}.json(时间戳随运行时间变化;默认缓存目录是 ~/.uniqc/calibration_cache/,这里用临时目录是为了不污染全局状态)。
两比特的联合读出校准用 calibrate_2q:对四个基础态 \(|00\rangle,|01\rangle,|10\rangle,|11\rangle\) 各跑一轮,得到 4×4 矩阵(行=测量结果、列=制备态,比特 0 是最低位):
adapter2 = DummyAdapter(noise_model={"readout": {0: [0.06, 0.08], 1: [0.04, 0.05]}})
cal2 = ReadoutCalibrator(adapter=adapter2, shots=400, cache_dir=cache_dir).calibrate_2q(0, 1)
for row in cal2.confusion_matrix:
print([round(v, 3) for v in row])
print(round(cal2.assignment_fidelity, 4))
[0.902, 0.077, 0.077, 0.005]
[0.058, 0.882, 0.003, 0.045]
[0.037, 0.003, 0.882, 0.075]
[0.003, 0.037, 0.037, 0.875]
0.8856
对角元 \(0.902\approx(1-0.06)(1-0.04)\) 等恰好是两比特独立翻转的乘积——两个比特都读对的概率必然低于单个比特。校准也可以走命令行(写入默认缓存目录,无法指定临时目录):
uniqc calibrate readout --qubits 0 --type 1q --shots 400 --backend dummy:local:simulator
ℹ Readout calibration on backend=dummy:local:simulator
ℹ Calibrating 1q readout...
Qubit 0: assignment fidelity = 1.00000
✓ Readout calibration complete!
dummy:local:simulator 本身无噪声(CLI 不接受 noise_model,那是 Python API 的测试便利),所以校准出单位矩阵、保真度 1.0;真实芯片上把 --backend 换成真机名即可(需要 token,见第 5 章)。
3. M3 缓解:线性反演修正计数¶
有了混淆矩阵,修正就是解一个线性方程:观测计数向量 \(n_{\text{obs}}\) 是真实计数被 \(C\) "弄脏"的结果(\(n_{\text{obs}}\approx C\,n_{\text{true}}\)),因此
先用第 2 节校准出的矩阵手算一遍(观测计数取自第 1 节的 {0: 40, 1: 360}):
import numpy as np
C = np.array(cal.confusion_matrix) # 校准得到的 2×2 混淆矩阵
n_obs = np.array([40, 360])
print(np.linalg.inv(C) @ n_obs)
[-1.77635684e-15 4.00000000e+02]
期望值正是 \([0, 400]\)——完美还原"全部制备在 \(|1\rangle\)"的真相。\(-10^{-15}\) 量级的负数是浮点误差;真实数据上线性反演还可能给出本应为 0 的分量变成小负数,因此 uniqc.qem 的 M3(Matrix Mitigation of Measurement errors)实现会先把负值截断到 0、再按总 shots 归一。库的两种用法:
from uniqc.qem import M3Mitigator, ReadoutEM
m3 = M3Mitigator(calibration_result=cal) # 直接传入校准结果
print(m3.mitigate_counts({0: 40, 1: 360}))
em = ReadoutEM(adapter=adapter, shots=400, cache_dir=cache_dir) # 从缓存自动查找
print(em.mitigate_counts({0: 40, 1: 360}, measured_qubits=[0]))
{0: 0.0, 1: 400.0}
{0: 0.0, 1: 400.0}
ReadoutEM 是推荐入口:它按 measured_qubits 的个数自动分派——1 比特用 1q 校准、2 比特用 2q 联合校准、3 比特以上用逐比特张量积近似,并自动从缓存加载校准数据。来一个更有说服力的双比特例子——Bell 态在含读出噪声的后端上会出现本不该存在的 01/10:
bell = Circuit(2)
bell.h(0)
bell.cnot(0, 1)
bell.measure(0)
bell.measure(1)
task_id = adapter2.submit(bell.originir, shots=1000)
observed = {int(k, 2): v for k, v in adapter2.query(task_id)["result"].items()}
print("observed: ", observed)
em2 = ReadoutEM(adapter=adapter2, shots=400, cache_dir=cache_dir)
corrected = em2.mitigate_counts(observed, measured_qubits=[0, 1])
print("corrected:", {k: round(v, 1) for k, v in corrected.items()})
observed: {0: 453, 1: 52, 2: 57, 3: 438}
corrected: {0: 499.0, 1: 1.0, 2: 1.0, 3: 499.1}
键是十进制整数:0=00、1=01、2=10、3=11(比特 0 为最低位)。理想 Bell 态只有 00/11 各 500,观测里却混进了约 100 个 01/10;4×4 矩阵反演后它们几乎被完全清除——这就是误差缓解"把可信度买回来"的过程。缓解结果保持总 shots 不变(可能带小数)。此外 M3Mitigator(...).apply(result) / ReadoutEM.apply(result) 可以直接作用于 UnifiedResult,返回一个新的 UnifiedResult 供下游工作流继续使用。
4. 校准缓存与 TTL:过期数据不可用¶
真实芯片的噪声会随时间漂移,昨天的混淆矩阵今天未必还成立。因此 UnifiedQuantum 约定:校准层只写缓存、从不删除;缓解层读取时强制检查新鲜度——每份结果都带 ISO-8601 的 calibrated_at 时间戳,QEM 侧按 max_age_hours(默认 24)检查,超龄即抛 StaleCalibrationError。我们把校准结果的时钟拨回 48 小时前,主动触发一次:
import dataclasses
from datetime import datetime, timedelta, timezone
from uniqc.qem import M3Mitigator, StaleCalibrationError
stale = dataclasses.replace(
cal,
calibrated_at=(datetime.now(timezone.utc) - timedelta(hours=48)).isoformat(),
)
try:
M3Mitigator(calibration_result=stale, max_age_hours=24.0)
except StaleCalibrationError as err:
print(f"{type(err).__name__}: {err}")
StaleCalibrationError: Calibration data is 48.0 hours old (max_age_hours=24.0). Calibrated at: 2026-09-03T03:31:25.624720+00:00
(完整异常消息还附带文档链接与 uniqc calibrate 排障提示,为节省篇幅从略;时间戳随运行时间变化。)配套的缓存查询接口是 find_cached_results,按"后端 + 结果类型 + 新鲜度"筛选:
from uniqc.calibration.results import find_cached_results
print(find_cached_results("dummy", "readout_1q", max_age_hours=24.0, cache_dir=cache_dir))
[PosixPath('/tmp/uniqc-cal-rvif7co3/readout_1q_dummy_q0_20260905T033125.620127Z.json')]
这套设计意味着典型工作循环是:早上对芯片跑一轮校准 → 一整天内的实验都由 QEM 自动取用新数据 → 隔天(或 TTL 过期)自动报错提醒你重新校准,而不是拿着陈旧的修正矩阵得出系统性错误的结论。
5. XEB:随机线路测每层保真度¶
读出校准修正的是"测"的误差;门误差("算"的误差)要用 **XEB(cross-entropy benchmarking)**来度量。思路:对目标比特跑若干层随机单比特门,得到理想概率分布 \(p_{\text{ideal}}\)(无噪模拟)与实际观测分布 \(p_{\text{obs}}\),用 normalized linear XEB 估计器打分:
\(p_{\text{obs}}\) 与理想完全一致时 \(F=1\),退化为均匀分布时 \(F=0\)。随着深度 \(m\) 增大噪声不断累积,\(F\) 按指数衰减,拟合模型为
其中 \(r\in(0,1]\) 就是每层门保真度——本章最关心的数字。先看一条随机线路长什么样:
from uniqc.calibration.xeb.circuits import generate_1q_xeb_circuits
sample = generate_1q_xeb_circuits(qubit=0, depths=[3], n_circuits=1, seed=7)
print(sample[0].originir)
QINIT 1
CREG 1
RZ q[0], (5.637360571650786)
T q[0]
T q[0]
MEASURE q[0], c[0]
每层从 {H, X, Y, Z, S, T, RX, RY, RZ} 中随机取一个门;seed 固定时线路序列完全可复现。下面手工走一遍完整的 XEB 流程——同一批线路分别用无噪 Simulator 和含 depol=0.02 去极化噪声的 dummy 后端算出两个分布,逐深度打分再拟合:
import numpy as np
from uniqc.simulator import Simulator
from uniqc.calibration.xeb.circuits import generate_1q_xeb_circuits
from uniqc.calibration.xeb.fitter import compute_linear_xeb, fit_exponential
noisy = DummyAdapter(noise_model={"depol": 0.02})
ideal = Simulator()
depths = [1, 2, 4, 8, 16]
fidelities = []
for depth in depths:
values = []
for circuit in generate_1q_xeb_circuits(0, [depth], n_circuits=6, seed=1):
p_ideal = np.asarray(ideal.simulate_pmeasure(circuit.originir))
p_noisy = np.asarray(noisy.simulate_pmeasure(circuit.originir))
values.append(compute_linear_xeb(p_ideal, p_noisy))
values = [v for v in values if np.isfinite(v)]
fidelities.append(float(np.mean(values)))
print(f"depth={depth:2d} XEB fidelity={fidelities[-1]:.4f}")
print({k: round(v, 4) if isinstance(v, float) else v for k, v in fit_exponential(depths, fidelities).items()})
depth= 1 XEB fidelity=0.9733
depth= 2 XEB fidelity=0.9474
depth= 4 XEB fidelity=0.8975
depth= 8 XEB fidelity=0.8056
depth=16 XEB fidelity=0.6489
{'r': 0.9733, 'A': 1.0, 'B': 0.0, 'r_stderr': 0.0, 'n_points': 5, 'method': 'scipy_curve_fit'}
教科书式的指数衰减:拟合给出 \(r=0.9733\)(\(A=1\)、\(B=0\)),即每个随机门执行后"保真度乘以 0.9733";验证一下 \(0.9733^{16}\approx 0.65\),正对应 depth=16 处的 0.6489。代码里那行 isfinite 过滤不是装饰:单比特线路若恰好以等幅叠加态收尾(如 H 门后接 RZ),理想分布恰为均匀、估计器分母为 0,该线路返回 nan,库内部同样会过滤后再平均。
6. 一键工作流:校准 + 缓解 + 基准¶
前两节的步骤——校准读出、构造 ReadoutEM、生成随机线路、拟合——被 run_1q_xeb_workflow 打包成一次调用,且对任意后端通用:
import tempfile
from uniqc import xeb_workflow
cache_dir = tempfile.mkdtemp(prefix="uniqc-xeb-")
results = xeb_workflow.run_1q_xeb_workflow(
backend="dummy:local:simulator",
qubits=[0],
depths=[2, 4, 8, 16],
n_circuits=5,
shots=256,
use_readout_em=True,
noise_model={"depol": 0.01, "readout": 0.04},
seed=11,
cache_dir=cache_dir,
)
r = results[0]
print(f"qubit: {r.qubit}")
print(f"depths: {list(r.depths)}")
print(f"fidelity/layer: {r.fidelity_per_layer:.6f}")
print(f"fit A={r.fit_a:.4f} r={r.fit_r:.4f} B={r.fit_b:.4f}")
qubit: 0
depths: [2, 4, 8, 16]
fidelity/layer: 0.980861
fit A=1.0271 r=0.9809 B=0.0000
这次同时注入了门噪声(depol=0.01)与读出噪声(readout=0.04),但因为 use_readout_em=True,工作流先自动跑读出校准、再在计算 XEB 前修正读出误差,所以 \(r=0.9809\) 干净地反映了门噪声。扫一遍噪声强度:
for depol in [0.0, 0.01, 0.03]:
results = xeb_workflow.run_1q_xeb_workflow(
backend="dummy:local:simulator",
qubits=[0],
depths=[2, 4, 8, 16],
n_circuits=5,
shots=256,
use_readout_em=True,
noise_model={"depol": depol, "readout": 0.04},
seed=11,
cache_dir=cache_dir,
)
print(f"depol={depol:.2f} fidelity_per_layer={results[0].fidelity_per_layer:.4f}")
depol=0.00 fidelity_per_layer=1.0000
depol=0.01 fidelity_per_layer=0.9809
depol=0.03 fidelity_per_layer=0.9392
\(r\) 随门噪声单调下降,无门噪声时精确回到 1。最后验证"校准 + 缓解"确实在起作用——同样的噪声下对比开关 use_readout_em:
cache_dir = tempfile.mkdtemp(prefix="uniqc-xeb-em-")
for em in [True, False]:
results = xeb_workflow.run_1q_xeb_workflow(
backend="dummy:local:simulator",
qubits=[0],
depths=[2, 4, 8, 16],
n_circuits=5,
shots=256,
use_readout_em=em,
noise_model={"depol": 0.01, "readout": 0.04},
seed=11,
cache_dir=cache_dir,
)
r = results[0]
print(f"use_readout_em={em} r={r.fidelity_per_layer:.4f} A={r.fit_a:.4f}")
for p in sorted(Path(cache_dir).glob("*.json")):
print(p.name)
use_readout_em=True r=0.9809 A=1.0271
use_readout_em=False r=0.9867 A=0.9200
readout_1q_dummy_q0_20260905T033125.851132Z.json
xeb_1q_dummy_q0_20260905T033125.891050Z.json
xeb_1q_dummy_q0_20260905T033125.928793Z.json
不缓解时读出误差把整条衰减曲线往下压:振幅 \(A\) 从约 1 跌到 \(0.9200\approx 1-0.04\times2\),同时 \(r\) 被高估为 0.9867(常数压低与指数衰减在拟合里互相纠缠);开了缓解后 \(A\) 恢复到约 1、\(r\) 回到 0.9809——与上面噪声扫描中 depol=0.01 一行完全一致。缓存目录里能看到工作流留下的两类文件:readout_1q_*(自动读出校准)与两条 xeb_1q_*(两次基准结果)。
上真实芯片时把 backend 换成 dummy:originq:WK_C180 这类规则型 id,即可先按真实芯片做编译/转译、再用该芯片的标定数据本地含噪执行(需要本地芯片缓存);真机校准则走命令行——以下为模板,需要 token,本章未运行:
# 需要先配置 token:uniqc config set originq.token <TOKEN>(见第 5 章)
uniqc calibrate xeb --qubits 0 1 2 --type 1q --depths 5 10 20 --n-circuits 50 \
--backend originq:WK_C180
下一步¶
想系统了解 dummy 噪声家族(
chip_characterization、虚拟拓扑、含噪本地模拟),继续第 3 章:本地与含噪模拟想把校准跑在真机上:token 配置与任务提交见第 5 章:云端执行
想看误差缓解如何改善一个完整算法的能量估计,去第 7 章:VQE 实验
变分算法与噪声的理论背景见姊妹站 VQE 教程
练习题¶
练习 1【构造含噪 dummy 后端】(→ 第 1 节)
把读出噪声改成
[0.0, 0.20],先笔算 X 门线路在shots=400下的计数,再运行验证。再给
noise_model加上"depol": 0.05,预测新的计数分布并运行对比;解释哪部分偏离来自读出、哪部分来自去极化。
提示:把
depol单独设为 0 各跑一次,对两次结果作差。
练习 2【读出校准与混淆矩阵】(→ 第 2 节)
对
readout=[0.02, 0.06]的后端做calibrate_1q,先写出你预期的混淆矩阵再运行对比。把校准的
shots从 400 改成 100,解释为什么 dummy 后端上矩阵纹丝不动,而真实芯片上小 shots 会引入估计误差。
提示:回顾第 1 节——dummy 按精确概率换算计数,没有采样涨落。
练习 3【M3 线性反演缓解】(→ 第 3 节)
构造制备 \(|0\rangle\) 的空线路,在
readout=[0.07, 0.10]后端上取 400 shots 计数,手算 \(C^{-1}n_{\text{obs}}\) 预测缓解结果,再用M3Mitigator验证。把 Bell 演示的
shots改成 100,先预测corrected中01/10分量的大小再运行;解释线性反演为什么会给出需要截断的小负值。
提示:\(C\) 的每列是"一个制备态被读成各结果的概率",反演就是把混叠按列解开。
练习 4【校准缓存与 TTL 新鲜度】(→ 第 4 节)
把
timedelta(hours=48)改成hours=20,预测try块是否还抛异常,运行验证你的判断。把
find_cached_results的max_age_hours改成0.0,预测返回值再运行验证。
提示:刚写入的文件"年龄"也有几秒;阈值取 0 时,任何正的年龄都会被排除。
练习 5【XEB 交叉熵基准测试】(→ 第 5 节)
把
depol从 0.02 改成 0.04,先按0.02 → r=0.9733的比例外推预测 \(r\),再运行验证拟合输出。把
depths改成[1, 2, 3, 4]重跑,观察拟合是否仍稳定,并解释为什么深度跨度大更有利于分辨 \(r\)。
提示:\(r\) 的信息藏在衰减"斜率"里——点又少又挤在一起时,斜率很难分辨。
练习 6【一键校准与基准工作流】(→ 第 6 节)
在对比循环中追加
depol=0.05,先按0.01→0.9809、0.03→0.9392的趋势预测 \(r\),再运行验证。把
readout从 0.04 提高到 0.15,对比开关use_readout_em时的A与r,指出哪个拟合参数受读出误差影响最直接。
提示:回顾第 6 节的对比——读出误差首先压低的是振幅 \(A\)。