校准与误差缓解:从含噪计数到可信结果

真实量子硬件的测量结果从来不是理想概率分布:读出时比特可能被读反(读出误差),门执行时会偏离幺正(门误差)。UnifiedQuantum 用两层架构对付它们:校准层uniqc.calibration)负责跑表征实验、把结果连同时间戳写入缓存 ~/.uniqc/calibration_cache/缓解层uniqc.qem)负责读缓存、修正测量结果,并强制校准数据的新鲜度(TTL)。本章把这两层完整走一遍——全部在本地 dummy 后端上实际运行,不需要任何 token。

想深入了解"表征测量结果"的量子力学背景,见姊妹站量子态层析教程;误差缓解最大的受益者是变分算法,见VQE 教程。本页示例基于 unified-quantum 0.1.0,所有输出均为实际运行结果。

本课知识点

  1. 构造含噪 dummy 后端——能用 DummyAdapternoise_model 注入读出翻转与去极化噪声,并解释 [p(0→1), p(1→0)] 各分量的含义。

  2. 读出校准与混淆矩阵——能写出混淆矩阵 \(C[\text{meas}][\text{prep}]=P(\text{meas}\,|\,\text{prep})\) 的定义,用 ReadoutCalibrator 测出它并计算 assignment fidelity。

  3. M3 线性反演缓解——能手算 \(n_{\text{corr}}=C^{-1}n_{\text{obs}}\) 的读出修正,并用 M3Mitigator/ReadoutEM 在单比特与双比特计数上验证。

  4. 校准缓存与 TTL 新鲜度——能解释 calibrated_at + max_age_hours 的缓存设计,并主动触发 StaleCalibrationError

  5. XEB 交叉熵基准测试——能解释随机线路与 normalized linear XEB 估计器如何随深度指数衰减,写出拟合模型 \(F(m)=A\,r^m+B\) 中各参数的含义。

  6. 一键校准与基准工作流——能用 run_1q_xeb_workflow 串联"校准→读出缓解→XEB→拟合",比较不同噪声水平与开关缓解时的拟合结果。

1. 构造含噪 dummy 后端:先看见误差

读出误差是最便宜、也最容易修正的系统误差:制备 \(|1\rangle\) 的比特有一定概率被读成 0,反之亦然。DummyAdapternoise_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}}\)),因此

\[n_{\text{corr}} = C^{-1}\,n_{\text{obs}}.\]

先用第 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=001=012=103=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 估计器打分:

\[F_{\text{XEB}}=\frac{\langle p_{\text{obs}},p_{\text{ideal}}\rangle-1/N}{\langle p_{\text{ideal}},p_{\text{ideal}}\rangle-1/N},\qquad N=2^n .\]

\(p_{\text{obs}}\) 与理想完全一致时 \(F=1\),退化为均匀分布时 \(F=0\)。随着深度 \(m\) 增大噪声不断累积,\(F\) 按指数衰减,拟合模型为

\[F(m)=A\,r^m+B,\]

其中 \(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

下一步

练习题

练习 1【构造含噪 dummy 后端】(→ 第 1 节

  1. 把读出噪声改成 [0.0, 0.20],先笔算 X 门线路在 shots=400 下的计数,再运行验证。

  2. 再给 noise_model 加上 "depol": 0.05,预测新的计数分布并运行对比;解释哪部分偏离来自读出、哪部分来自去极化。

提示:把 depol 单独设为 0 各跑一次,对两次结果作差。

练习 2【读出校准与混淆矩阵】(→ 第 2 节

  1. readout=[0.02, 0.06] 的后端做 calibrate_1q,先写出你预期的混淆矩阵再运行对比。

  2. 把校准的 shots 从 400 改成 100,解释为什么 dummy 后端上矩阵纹丝不动,而真实芯片上小 shots 会引入估计误差。

提示:回顾第 1 节——dummy 按精确概率换算计数,没有采样涨落。

练习 3【M3 线性反演缓解】(→ 第 3 节

  1. 构造制备 \(|0\rangle\) 的空线路,在 readout=[0.07, 0.10] 后端上取 400 shots 计数,手算 \(C^{-1}n_{\text{obs}}\) 预测缓解结果,再用 M3Mitigator 验证。

  2. 把 Bell 演示的 shots 改成 100,先预测 corrected01/10 分量的大小再运行;解释线性反演为什么会给出需要截断的小负值。

提示:\(C\) 的每列是"一个制备态被读成各结果的概率",反演就是把混叠按列解开。

练习 4【校准缓存与 TTL 新鲜度】(→ 第 4 节

  1. timedelta(hours=48) 改成 hours=20,预测 try 块是否还抛异常,运行验证你的判断。

  2. find_cached_resultsmax_age_hours 改成 0.0,预测返回值再运行验证。

提示:刚写入的文件"年龄"也有几秒;阈值取 0 时,任何正的年龄都会被排除。

练习 5【XEB 交叉熵基准测试】(→ 第 5 节

  1. depol 从 0.02 改成 0.04,先按 0.02 r=0.9733 的比例外推预测 \(r\),再运行验证拟合输出。

  2. depths 改成 [1, 2, 3, 4] 重跑,观察拟合是否仍稳定,并解释为什么深度跨度大更有利于分辨 \(r\)

提示:\(r\) 的信息藏在衰减"斜率"里——点又少又挤在一起时,斜率很难分辨。

练习 6【一键校准与基准工作流】(→ 第 6 节

  1. 在对比循环中追加 depol=0.05,先按 0.01→0.98090.03→0.9392 的趋势预测 \(r\),再运行验证。

  2. readout 从 0.04 提高到 0.15,对比开关 use_readout_em 时的 Ar,指出哪个拟合参数受读出误差影响最直接。

提示:回顾第 6 节的对比——读出误差首先压低的是振幅 \(A\)