# 校准与误差缓解:从含噪计数到可信结果 真实量子硬件的测量结果从来不是理想概率分布:读出时比特可能被读反(读出误差),门执行时会偏离幺正(门误差)。UnifiedQuantum 用两层架构对付它们:**校准层**(`uniqc.calibration`)负责跑表征实验、把结果连同时间戳写入缓存 `~/.uniqc/calibration_cache/`;**缓解层**(`uniqc.qem`)负责读缓存、修正测量结果,并强制校准数据的新鲜度(TTL)。本章把这两层完整走一遍——全部在本地 dummy 后端上实际运行,不需要任何 token。 想深入了解"表征测量结果"的量子力学背景,见姊妹站[量子态层析教程](https://chenzhaoyun.com/quantum-tutorial/ch07-quantum-info/quantum-state-tomography-tutorial.html);误差缓解最大的受益者是变分算法,见[VQE 教程](https://chenzhaoyun.com/quantum-tutorial/ch08-qml/vqe-tutorial.html)。本页示例基于 `unified-quantum` **0.1.0**,所有输出均为实际运行结果。 :::{admonition} 本课知识点 :class: tip 1. **[构造含噪 dummy 后端](#uqt-qem-noisy-backend)**——能用 `DummyAdapter` 的 `noise_model` 注入读出翻转与去极化噪声,并解释 `[p(0→1), p(1→0)]` 各分量的含义。 2. **[读出校准与混淆矩阵](#uqt-qem-readout-calibration)**——能写出混淆矩阵 $C[\text{meas}][\text{prep}]=P(\text{meas}\,|\,\text{prep})$ 的定义,用 `ReadoutCalibrator` 测出它并计算 assignment fidelity。 3. **[M3 线性反演缓解](#uqt-qem-m3-mitigation)**——能手算 $n_{\text{corr}}=C^{-1}n_{\text{obs}}$ 的读出修正,并用 `M3Mitigator`/`ReadoutEM` 在单比特与双比特计数上验证。 4. **[校准缓存与 TTL 新鲜度](#uqt-qem-cache-ttl)**——能解释 `calibrated_at` + `max_age_hours` 的缓存设计,并主动触发 `StaleCalibrationError`。 5. **[XEB 交叉熵基准测试](#uqt-qem-xeb-benchmark)**——能解释随机线路与 normalized linear XEB 估计器如何随深度指数衰减,写出拟合模型 $F(m)=A\,r^m+B$ 中各参数的含义。 6. **[一键校准与基准工作流](#uqt-qem-xeb-workflow)**——能用 `run_1q_xeb_workflow` 串联"校准→读出缓解→XEB→拟合",比较不同噪声水平与开关缓解时的拟合结果。 ::: (uqt-qem-noisy-backend)= ## 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): ```python 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"]) ``` ```text {'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` 四舍五入成计数,**不做随机采样**,所以本章所有输出都是确定性的、可逐字复现;真实硬件上的计数才有二项涨落。 (uqt-qem-readout-calibration)= ## 2. 读出校准:测出混淆矩阵 修正读出误差的前提是先**测量**它。`ReadoutCalibrator` 的做法很直接:对每个比特逐一制备基础态 $|0\rangle$、$|1\rangle$,反复测量并统计误读概率,得到 2×2 **混淆矩阵**(confusion matrix)$C$,其中 $C[\text{meas}][\text{prep}]=P(\text{meas}\,|\,\text{prep})$;对角元均值即 **assignment fidelity**: ```python 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) ``` ```text ((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 是最低位): ```python 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)) ``` ```text [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)$ 等恰好是两比特独立翻转的乘积——两个比特都读对的概率必然低于单个比特。校准也可以走命令行(写入默认缓存目录,无法指定临时目录): ```bash uniqc calibrate readout --qubits 0 --type 1q --shots 400 --backend dummy:local:simulator ``` ```text ℹ 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 章](../ch05-cloud/index.md))。 (uqt-qem-m3-mitigation)= ## 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}`): ```python 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) ``` ```text [-1.77635684e-15 4.00000000e+02] ``` 期望值正是 $[0, 400]$——完美还原"全部制备在 $|1\rangle$"的真相。$-10^{-15}$ 量级的负数是浮点误差;真实数据上线性反演还可能给出本应为 0 的分量变成小负数,因此 `uniqc.qem` 的 M3(Matrix Mitigation of Measurement errors)实现会先把负值截断到 0、再按总 shots 归一。库的两种用法: ```python 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])) ``` ```text {0: 0.0, 1: 400.0} {0: 0.0, 1: 400.0} ``` `ReadoutEM` 是推荐入口:它按 `measured_qubits` 的个数自动分派——1 比特用 1q 校准、2 比特用 2q 联合校准、3 比特以上用逐比特张量积近似,并自动从缓存加载校准数据。来一个更有说服力的双比特例子——Bell 态在含读出噪声的后端上会出现本不该存在的 `01`/`10`: ```python 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()}) ``` ```text 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` 供下游工作流继续使用。 (uqt-qem-cache-ttl)= ## 4. 校准缓存与 TTL:过期数据不可用 真实芯片的噪声会随时间漂移,昨天的混淆矩阵今天未必还成立。因此 UnifiedQuantum 约定:**校准层只写缓存、从不删除;缓解层读取时强制检查新鲜度**——每份结果都带 ISO-8601 的 `calibrated_at` 时间戳,QEM 侧按 `max_age_hours`(默认 24)检查,超龄即抛 `StaleCalibrationError`。我们把校准结果的时钟拨回 48 小时前,主动触发一次: ```python 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}") ``` ```text 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`,按"后端 + 结果类型 + 新鲜度"筛选: ```python from uniqc.calibration.results import find_cached_results print(find_cached_results("dummy", "readout_1q", max_age_hours=24.0, cache_dir=cache_dir)) ``` ```text [PosixPath('/tmp/uniqc-cal-rvif7co3/readout_1q_dummy_q0_20260905T033125.620127Z.json')] ``` 这套设计意味着典型工作循环是:早上对芯片跑一轮校准 → 一整天内的实验都由 QEM 自动取用新数据 → 隔天(或 TTL 过期)自动报错提醒你重新校准,而不是拿着陈旧的修正矩阵得出系统性错误的结论。 (uqt-qem-xeb-benchmark)= ## 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]$ 就是**每层门保真度**——本章最关心的数字。先看一条随机线路长什么样: ```python 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) ``` ```text 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 后端算出两个分布,逐深度打分再拟合: ```python 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()}) ``` ```text 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`,库内部同样会过滤后再平均。 (uqt-qem-xeb-workflow)= ## 6. 一键工作流:校准 + 缓解 + 基准 前两节的步骤——校准读出、构造 `ReadoutEM`、生成随机线路、拟合——被 `run_1q_xeb_workflow` 打包成一次调用,且对任意后端通用: ```python 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}") ``` ```text 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$ 干净地反映了门噪声。扫一遍噪声强度: ```python 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}") ``` ```text 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`: ```python 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) ``` ```text 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,本章未运行**: ```bash # 需要先配置 token:uniqc config set originq.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 章:本地与含噪模拟](../ch03-simulation/index.md) - 想把校准跑在真机上:token 配置与任务提交见[第 5 章:云端执行](../ch05-cloud/index.md) - 想看误差缓解如何改善一个完整算法的能量估计,去[第 7 章:VQE 实验](../ch07-labs/index.md) - 变分算法与噪声的理论背景见姊妹站 [VQE 教程](https://chenzhaoyun.com/quantum-tutorial/ch08-qml/vqe-tutorial.html) ## 练习题 **练习 1【构造含噪 dummy 后端】**(→ [第 1 节](#uqt-qem-noisy-backend)) 1. 把读出噪声改成 `[0.0, 0.20]`,先笔算 X 门线路在 `shots=400` 下的计数,再运行验证。 2. 再给 `noise_model` 加上 `"depol": 0.05`,预测新的计数分布并运行对比;解释哪部分偏离来自读出、哪部分来自去极化。 > 提示:把 `depol` 单独设为 0 各跑一次,对两次结果作差。 **练习 2【读出校准与混淆矩阵】**(→ [第 2 节](#uqt-qem-readout-calibration)) 1. 对 `readout=[0.02, 0.06]` 的后端做 `calibrate_1q`,先写出你预期的混淆矩阵再运行对比。 2. 把校准的 `shots` 从 400 改成 100,解释为什么 dummy 后端上矩阵纹丝不动,而真实芯片上小 shots 会引入估计误差。 > 提示:回顾第 1 节——dummy 按精确概率换算计数,没有采样涨落。 **练习 3【M3 线性反演缓解】**(→ [第 3 节](#uqt-qem-m3-mitigation)) 1. 构造制备 $|0\rangle$ 的空线路,在 `readout=[0.07, 0.10]` 后端上取 400 shots 计数,手算 $C^{-1}n_{\text{obs}}$ 预测缓解结果,再用 `M3Mitigator` 验证。 2. 把 Bell 演示的 `shots` 改成 100,先预测 `corrected` 中 `01`/`10` 分量的大小再运行;解释线性反演为什么会给出需要截断的小负值。 > 提示:$C$ 的每列是"一个制备态被读成各结果的概率",反演就是把混叠按列解开。 **练习 4【校准缓存与 TTL 新鲜度】**(→ [第 4 节](#uqt-qem-cache-ttl)) 1. 把 `timedelta(hours=48)` 改成 `hours=20`,预测 `try` 块是否还抛异常,运行验证你的判断。 2. 把 `find_cached_results` 的 `max_age_hours` 改成 `0.0`,预测返回值再运行验证。 > 提示:刚写入的文件"年龄"也有几秒;阈值取 0 时,任何正的年龄都会被排除。 **练习 5【XEB 交叉熵基准测试】**(→ [第 5 节](#uqt-qem-xeb-benchmark)) 1. 把 `depol` 从 0.02 改成 0.04,先按 `0.02 → r=0.9733` 的比例外推预测 $r$,再运行验证拟合输出。 2. 把 `depths` 改成 `[1, 2, 3, 4]` 重跑,观察拟合是否仍稳定,并解释为什么深度跨度大更有利于分辨 $r$。 > 提示:$r$ 的信息藏在衰减"斜率"里——点又少又挤在一起时,斜率很难分辨。 **练习 6【一键校准与基准工作流】**(→ [第 6 节](#uqt-qem-xeb-workflow)) 1. 在对比循环中追加 `depol=0.05`,先按 `0.01→0.9809`、`0.03→0.9392` 的趋势预测 $r$,再运行验证。 2. 把 `readout` 从 0.04 提高到 0.15,对比开关 `use_readout_em` 时的 `A` 与 `r`,指出哪个拟合参数受读出误差影响最直接。 > 提示:回顾第 6 节的对比——读出误差首先压低的是振幅 $A$。