4. 模拟环境搭建与工具链

在进入纠错阈值模拟之前,先把“算得动、跑得对、结果可复现”的环境搭好,是后续所有实验的前提。本节围绕两条主线展开:一是以 NumPy、SciPy 为核心的 Python 科学计算栈,负责矩阵运算、稀疏线性代数与随机数控制;二是以 Stim、PyMatching 为代表的纠错模拟库,负责稳定子电路的采样与解码。理解它们各自的分工,比记住安装命令更重要。

一、工具链的分工与数据流

纠错阈值模拟的典型链路是:先构造一个带噪声的稳定子电路(例如旋转表面码的记忆实验),再由采样器生成大量含测量错误的探测事件(detector events),然后由解码器根据探测事件推断最可能的错误链,最后统计逻辑错误率。Stim 承担“电路构造 + 快速采样 + 探测事件生成”,PyMatching 承担“基于匹配图的最小权完美匹配解码”,NumPy/SciPy 则承担参数扫描、稀疏矩阵组装、统计分析与绘图。下图给出这条链路的对象与数据流向。

NumPy / SciPy 参数网格 p、d 扫描 稀疏矩阵 / 随机种子 Stim 电路 surface_code:rotated 噪声通道 + 探测器 采样器 compile_detector_sampler shots 次探测事件 PyMatching 匹配图解码 预测逻辑翻转 逻辑错误率统计 比较预测值与真实逻辑观测值 → 每点错误数 / shots 阈值判读:不同 d 曲线交点附近即阈值 p_th p < p_th 时增大 d 降低错误率;p > p_th 时相反

二、科学计算栈的安装与版本约束

建议使用独立虚拟环境,避免与系统 Python 冲突。NumPy 与 SciPy 是 Stim、PyMatching 的底层依赖,版本过旧可能导致 wheel 不匹配。一个可用的安装流程如下(示例命令,实际版本以官方发布为准):

python -m venv qec_env
source qec_env/bin/activate        # Windows: qec_env\Scripts\activate
python -m pip install --upgrade pip
pip install numpy scipy
pip install stim pymatching
python -c "import stim, pymatching, numpy, scipy; print(stim.__version__, pymatching.__version__)"

最后一行用于验证导入是否成功并打印版本号。若在无网络或受限环境,可提前下载对应平台的 wheel 文件离线安装。NumPy 的随机数生成器建议显式指定种子(如 np.random.default_rng(2024)),以保证参数扫描结果可复现;Stim 采样器本身也支持传入 seed 参数。

三、用 Stim 构造电路并采样探测事件

Stim 提供内置的电路生成器,可直接生成旋转表面码的记忆实验电路。下面以距离 d=3、物理错误率 p=0.001 为例,构造电路并采样 10000 次:

import stim

circuit = stim.Circuit.generated(
    "surface_code:rotated_memory_z",
    distance=3,
    rounds=3,
    after_clifford_depolarization=0.001,
    before_measure_flip_probability=0.001,
    after_reset_flip_probability=0.001,
)
sampler = circuit.compile_detector_sampler(seed=2024)
dets, obs = sampler.sample(shots=10000, separate_observables=True)
print(dets.shape, obs.shape)   # (10000, 探测器数), (10000, 1)

这里 dets 是布尔型探测事件矩阵,每一行对应一次采样,列对应一个探测器是否被触发;obs 是真实逻辑观测值。三个噪声参数分别刻画 Clifford 门后的去极化噪声、测量前翻转与复位后翻转,是阈值模拟中最常用的噪声模型组合。注意 rounds 与 distance 共同决定探测器数量,d 增大时探测器数近似按 d² 增长,采样成本随之上升。

四、用 PyMatching 解码并统计逻辑错误率

PyMatching 可直接从 Stim 电路提取解码图,无需手工构造匹配图:

import pymatching
import numpy as np

matching = pymatching.Matching.from_detector_error_model(
    circuit.detector_error_model(decompose_errors=True)
)
predictions = matching.decode_batch(dets)
logical_errors = np.sum(np.any(predictions != obs, axis=1))
print("逻辑错误率:", logical_errors / 10000)

decode_batch 返回预测的逻辑翻转,与真实 obs 逐行比较,任一位不一致即记为一次逻辑错误。这个“错误数 / shots”就是该参数点下的逻辑错误率估计。由于是统计量,shots 较小时波动明显,通常每个参数点取 10⁴~10⁶ 次采样,并在报告中给出置信区间或标准误(如 sqrt(p(1-p)/N))。

阈值扫描时,建议固定随机种子、固定 shots,并让 d 与 p 的网格在代码中显式列出,便于复现与横向比较。若某点错误数为 0,说明 shots 不足或 p 远低于阈值,应增大采样量而非直接记为 0 错误率。

五、参数扫描的组织方式

把上述流程封装成函数,用 NumPy 生成参数网格,即可得到阈值曲线所需的数据。示例结构如下:

def logical_error_rate(d, p, shots=20000, seed=2024):
    c = stim.Circuit.generated(
        "surface_code:rotated_memory_z",
        distance=d, rounds=d,
        after_clifford_depolarization=p,
        before_measure_flip_probability=p,
        after_reset_flip_probability=p,
    )
    dets, obs = c.compile_detector_sampler(seed=seed).sample(
        shots=shots, separate_observables=True)
    m = pymatching.Matching.from_detector_error_model(
        c.detector_error_model(decompose_errors=True))
    pred = m.decode_batch(dets)
    return float(np.mean(np.any(pred != obs, axis=1)))

for d in [3, 5, 7]:
    for p in np.logspace(-3.5, -2, 8):
        print(d, p, logical_error_rate(d, p))

运行后把 (p, 错误率) 按 d 分组绘制在半对数坐标上,不同 d 的曲线会在阈值附近交叉:p 低于阈值时,d 越大错误率越低;p 高于阈值时,d 越大错误率反而越高。这正是后续章节判读阈值位置的依据。环境搭建阶段不必追求大规模数据,先用小 d、少 shots 跑通全链路,确认形状、导入与统计逻辑无误,再扩展规模。

常见问题:一是 Stim 与 PyMatching 版本不匹配导致 from_detector_error_model 报错,应升级到相互兼容的版本;二是忘记 separate_observables=True,导致观测值与探测事件混在同一数组;三是 shots 过小使低错误率点全为 0,误判为“无错误”。

完成本节后,你应当拥有一条可复现的最小链路:虚拟环境中装好 NumPy、SciPy、Stim、PyMatching,能用内置生成器构造带噪声的表面码电路,采样探测事件,用匹配解码得到逻辑错误率,并以函数形式组织参数扫描。这条链路是后续阈值模拟与物理错误率扫描实验的直接基础。