本地模拟与含噪模拟

上一章我们用 Simulator 跑过一次无噪声采样;本章把它讲透:Simulator 的三种输出各回答什么问题、态矢量与密度矩阵两类后端如何取舍、怎样用错误加载器与读出错误手工构造噪声,以及 dummy 后端家族——从无约束模拟器、虚拟拓扑、真实芯片标定噪声到完全由一份 YAML 声明的含噪虚拟机。

态矢量、测量统计与纠缠的理论背景见量子计算算法教程·量子计算基础量子力学的基本概念。本页示例基于 unified-quantum 0.1.0,所有输出均为实际运行结果;凡涉及随机采样的输出每次运行略有不同

本课知识点

  1. 三种本地模拟输出——能写出 simulate_pmeasure / simulate_statevector / simulate_shots 的调用方式,并区分它们各自回答的问题。

  2. 模拟后端:态矢量与密度矩阵——能比较 statevector 与 density_matrix 两类后端的能力与开销,并解释噪声模拟为何必须用密度矩阵。

  3. 错误模型与错误加载器——能用三层 ErrorLoader 构造从全局到逐门实例的噪声配置,并预测噪声如何让 GHZ 态"泄漏"到其他基矢。

  4. 读出错误——能写出 readout_error 二元组的含义,并手算它对测量概率的作用。

  5. dummy 后端家族——能比较 dummy:local:* 各成员的拓扑约束与模拟引擎,并解释统一入口如何呈现拓扑不兼容错误。

  6. 声明式含噪虚拟机——能用一份 YAML 声明拓扑、门噪声、热弛豫与读出错误,构造 dummy:virtual:<name> 并运行任务。

1. 三种本地模拟输出

以 GHZ 线路为例——h(0) 制造叠加,两级 CNOT 把纠缠传播到全部三个比特:

from uniqc import Circuit
from uniqc.simulator import Simulator

c = Circuit()
c.h(0)
c.cnot(0, 1)
c.cnot(1, 2)
c.measure(0, 1, 2)

sim = Simulator()

probs = sim.simulate_pmeasure(c.originir)
print("== simulate_pmeasure ==")
for state, prob in enumerate(probs):
    if prob > 1e-9:
        print(f"  |{state:03b}>: {prob:.4f}")

sv = sim.simulate_statevector(c.originir)
print("== simulate_statevector ==")
for state, amp in enumerate(sv):
    if abs(amp) > 1e-9:
        print(f"  |{state:03b}>: {amp:.4f}")

counts = sim.simulate_shots(c.originir, shots=2000)
print("== simulate_shots ==")
print(counts)
== simulate_pmeasure ==
  |000>: 0.5000
  |111>: 0.5000
== simulate_statevector ==
  |000>: 0.7071+0.0000j
  |111>: 0.7071+0.0000j
== simulate_shots ==
{7: 965, 0: 1035}

三个方法回答三个不同的问题:

  • simulate_pmeasure理想概率分布——测量前先算出每个基矢的概率,返回长度 \(2^n\) 的列表,下标就是测量结果的十进制值;

  • simulate_statevector末态幅值——拿到态矢量本身(复数振幅),适合检查干涉与相位;GHZ 态幅值 \(\tfrac{1}{\sqrt 2}\approx 0.7071\),模平方才是概率 \(0.5\)

  • simulate_shots采样统计——按概率分布抽 shots 次样并计数,模拟真实实验"重复测量"的过程,键是十进制整数(7111)。

概率列表的位序约定:下标 i 的二进制表示 f"{i:03b}" 中,qubit 0 在最右(LSB),与全站测量结果的位串约定一致。Simulator 的输入既可以是 originir 字符串(如上),也可以直接传 Circuit 对象或 OpenQASM 2.0 字符串,格式自动检测。

2. 模拟后端:态矢量与密度矩阵

Simulator 底层有两类引擎,由 backend_type 选择。同一个无噪声 Bell 线路在两个后端下给出完全一致的概率:

from uniqc import Circuit
from uniqc.simulator import Simulator

bell = Circuit()
bell.h(0)
bell.cnot(0, 1)
bell.measure(0, 1)

sv_sim = Simulator(backend_type="statevector")
dm_sim = Simulator(backend_type="density_matrix")
print("statevector: ", [f"{p:.4f}" for p in sv_sim.simulate_pmeasure(bell.originir)])
print("density_matrix:", [f"{p:.4f}" for p in dm_sim.simulate_pmeasure(bell.originir)])
statevector:  ['0.5000', '0.0000', '0.0000', '0.5000']
density_matrix: ['0.5000', '0.0000', '0.0000', '0.5000']

后端(backend_type

噪声支持

内存开销

适用场景

statevector

\(O(2^n)\)

无噪声快速模拟,默认值

density_matrix

\(O(4^n)\)

含噪声模拟(NoisySimulator 的标配)

density_operator_qutip

\(O(4^n)\)

复杂噪声模型,需额外安装 unified-quantum[simulation]

噪声为什么必须用密度矩阵?态矢量只能描述纯态,而任何噪声通道都会把纯态变成混合态——只能用密度矩阵 \(\rho\) 编码。NoisySimulator 强制检查这一点,在默认 statevector 后端上调用会直接报错:

from uniqc.simulator import NoisySimulator
from uniqc.simulator.error_model import ErrorLoader_GenericError, Depolarizing

noisy_sv = NoisySimulator(
    backend_type="statevector",  # 态矢量后端不支持噪声通道
    error_loader=ErrorLoader_GenericError([Depolarizing(0.01)]),
)
try:
    noisy_sv.simulate_pmeasure(bell.originir)
except ValueError as e:
    print("ValueError:", e)
ValueError: simulate_pmeasure is only available for density_operator type OpcodeSimulator backend.

两类后端都接受若干别名(如 density_matrix / density_operator / CLI 的 density 是同一个东西),也可以用统一工厂 create_simulator(backend="density_matrix") 构造。无噪声时选默认 statevector 即可;本章其余含噪示例一律 density_matrix

3. 错误模型与错误加载器

NoisySimulator 的门噪声由错误模型(描述一种物理噪声通道)与错误加载器(决定噪声加在哪些门上)组合而成。常用错误模型都在 uniqc.simulator.error_model 里:

错误模型

物理含义

参数

BitFlip / PhaseFlip

位翻转 / 相位翻转

p

Depolarizing

单比特去极化

p

TwoQubitDepolarizing

双比特去极化

p

AmplitudeDamping

振幅阻尼(能量弛豫)

gamma

PauliError1Q

一般单比特泡利噪声

p_x, p_y, p_z

错误加载器分三层,逐层收窄作用范围:

  • ErrorLoader_GenericError([...])——所有门加同一组噪声;

  • ErrorLoader_GateTypeError(generic_error=..., gatetype_error={...})——按门类型('H''CNOT'…)额外加噪;

  • ErrorLoader_GateSpecificError(..., gate_specific_error={('CNOT', (0, 1)): [...]})——精确到"某个门 + 某组比特"。

对 GHZ 线路做三组对比——无噪声、全局 2% 去极化、以及"单比特门 0.5% + CNOT 5%"的分层配置:

from uniqc import Circuit
from uniqc.simulator import Simulator, NoisySimulator
from uniqc.simulator.error_model import (
    Depolarizing,
    TwoQubitDepolarizing,
    ErrorLoader_GenericError,
    ErrorLoader_GateTypeError,
)

ghz = Circuit()
ghz.h(0)
ghz.cnot(0, 1)
ghz.cnot(1, 2)
ghz.measure(0, 1, 2)
code = ghz.originir


def show(title, probs):
    print(f"== {title} ==")
    for state, prob in enumerate(probs):
        if prob > 1e-9:
            print(f"  |{state:03b}>: {prob:.4f}")


show("无噪声", Simulator().simulate_pmeasure(code))

generic = NoisySimulator(
    backend_type="density_matrix",
    error_loader=ErrorLoader_GenericError([Depolarizing(0.02)]),
)
show("所有门 2% 去极化", generic.simulate_pmeasure(code))

gatetype = NoisySimulator(
    backend_type="density_matrix",
    error_loader=ErrorLoader_GateTypeError(
        generic_error=[Depolarizing(0.005)],
        gatetype_error={"CNOT": [TwoQubitDepolarizing(0.05)]},
    ),
)
show("1q 0.5% + CNOT 5% 去极化", gatetype.simulate_pmeasure(code))
== 无噪声 ==
  |000>: 0.5000
  |111>: 0.5000
== 所有门 2% 去极化 ==
  |000>: 0.4740
  |001>: 0.0129
  |010>: 0.0066
  |011>: 0.0066
  |100>: 0.0066
  |101>: 0.0066
  |110>: 0.0129
  |111>: 0.4740
== 1q 0.5% + CNOT 5% 去极化 ==
  |000>: 0.4614
  |001>: 0.0222
  |010>: 0.0082
  |011>: 0.0082
  |100>: 0.0082
  |101>: 0.0082
  |110>: 0.0222
  |111>: 0.4614

噪声的效果是"泄漏":000/111 的概率跌破 0.5,差额摊到其余六个基矢上。去极化噪声以单比特/双比特泡利错误为主,因此与 000111 汉明距离为 1 的态(001110 等)泄漏最明显。两组含噪结果都是确定性的(密度矩阵演化不采样),只有 simulate_shots 才有随机涨落。

4. 读出错误:测量端的混淆矩阵

门噪声之外,真实设备还有读出错误:测 \(|0\rangle\) 读成 1、测 \(|1\rangle\) 读成 0。NoisySimulatorreadout_error 按比特配置一个二元组:

readout_error = {q: [p(0→1), p(1→0)]}
# p(0→1):真实为 0 被读成 1 的概率;p(1→0):真实为 1 被读成 0 的概率

只给 qubit 0 配一组非对称读出错误,其余比特保持理想:

from uniqc import Circuit
from uniqc.simulator import NoisySimulator

ghz = Circuit()
ghz.h(0)
ghz.cnot(0, 1)
ghz.cnot(1, 2)
ghz.measure(0, 1, 2)

sim = NoisySimulator(
    backend_type="density_matrix",
    readout_error={0: [0.10, 0.02]},
)
probs = sim.simulate_pmeasure(ghz.originir)
for state, prob in enumerate(probs):
    if prob > 1e-9:
        print(f"  |{state:03b}>: {prob:.4f}")
  |000>: 0.4500
  |001>: 0.0500
  |110>: 0.0100
  |111>: 0.4900

这组数字可以完全手算。理想分布是 \(P(000)=P(111)=0.5\),读出错误只作用在 \(q_0\)(位串最右位)上:

  • \(P(000) = 0.5 \times (1-0.10) = 0.45\)\(P(001) = 0.5 \times 0.10 = 0.05\)(真实 0 被读成 1);

  • \(P(111) = 0.5 \times (1-0.02) = 0.49\)\(P(110) = 0.5 \times 0.02 = 0.01\)(真实 1 被读成 0)。

实现上就是把每个含错误比特的边缘概率乘上一个 \(2\times 2\) 混淆矩阵,因此 simulate_pmeasure 的结果依然是确定性的。注意一个已知限制:密度矩阵路径不支持 simulate_shots(会抛 NotImplementedError: Density matrix is not supported for shot simulation.)——想对含噪线路做端到端采样,用下面两节的 dummy 后端家族。

5. dummy 后端家族:统一入口的本地成员

第 1 章我们用 submit_task + wait_for_result 走过统一执行管道;dummy 家族就是这条管道的本地成员,backend id 的语法本身描述了机器的约束与噪声:

backend id

拓扑

噪声

说明

dummy:local:simulator

全连接

无约束虚拟机,调试首选

dummy:local:virtual-line-N

\(N\) 比特链

虚拟链式拓扑

dummy:local:virtual-grid-RxC

\(R\times C\) 网格

虚拟网格拓扑

dummy:local:mps-linear-N:chi=K[:seed=S]

\(N\) 比特链

MPS 引擎,大比特数低纠缠可扩展

dummy:<platform>:<chip>

真实芯片耦合图

芯片标定噪声

先按真芯片编译,再本地注入噪声

dummy:virtual:<name>

YAML 声明

YAML 声明

用户自定义含噪虚拟机(第 6 节)

resolve_dummy_backend 可以先"拆开"一个 backend id 看它的规格,再提交 GHZ 线路:

from uniqc import Circuit, submit_task, wait_for_result, TaskFailedError
from uniqc.backend_adapter.dummy_backend import resolve_dummy_backend

ghz = Circuit(3)
ghz.h(0)
ghz.cnot(0, 1)
ghz.cnot(1, 2)
ghz.measure(0, 1, 2)

for backend in (
    "dummy:local:simulator",
    "dummy:local:virtual-line-3",
    "dummy:local:mps-linear-3:chi=8:seed=2024",
):
    spec = resolve_dummy_backend(backend)
    print(f"== {backend} ==")
    print("  description:    ", spec.description)
    print("  topology:       ", spec.available_topology)
    print("  simulator_kwargs:", spec.simulator_kwargs)
    result = wait_for_result(submit_task(ghz, backend=backend, shots=512), timeout=30)
    print("  counts:         ", result.counts)
== dummy:local:simulator ==
  description:     Unconstrained noiseless local simulator
  topology:        None
  simulator_kwargs: None
  counts:          {'000': 256, '111': 256}
== dummy:local:virtual-line-3 ==
  description:     Noiseless virtual 3-qubit line topology
  topology:        [[0, 1], [1, 2]]
  simulator_kwargs: None
  counts:          {'000': 256, '111': 256}
== dummy:local:mps-linear-3:chi=8:seed=2024 ==
  description:     Noiseless MPS simulator on a 3-qubit linear chain, chi=8
  topology:        [[0, 1], [1, 2]]
  simulator_kwargs: {'chi_max': 8, 'seed': 2024}
  counts:          {'111': 250, '000': 262}

三个成员的差别一目了然:simulator 无拓扑(None 即全连接);virtual-line-3 声明了链 0-1-2;MPS 成员额外携带引擎参数 chi_maxseedcounts 的键是二进制串(UnifiedResult 的统一格式),采样计数每次运行略有不同。

拓扑约束不是摆设。链 0-1-2 上没有 0-2 边,直接发一个 cnot(0, 2) 的线路,任务会在取结果时失败——与真实云后端"提交成功、执行报错"的形态完全一致:

bad = Circuit(3)
bad.h(0)
bad.cnot(0, 2)  # 0-2 不是链式拓扑 0-1-2 上的相邻边
bad.measure(0, 1, 2)
try:
    wait_for_result(submit_task(bad, backend="dummy:local:virtual-line-3", shots=512), timeout=30)
except TaskFailedError as e:
    print("TaskFailedError:", str(e)[:88], "...")
TaskFailedError: Task 'uqt_e412d78f19d741ba9ca4345b639c2cce' failed on backend 'dummy:local:virtual-line- ...

(完整错误信息里带有 Unsupported topology. 与该线路的 OriginIR;task_id 每次运行不同。)

最有意思的成员是 dummy:<platform>:<chip>:它按真实芯片编译线路,再用本地密度矩阵模拟器注入该芯片标定数据换算出的噪声——全程离线,不需要 token。前提是本地已有芯片缓存(由 uniqc backend update --platform originq 拉取,那一步需要平台 token,见第 5 章);下面的例子直接使用已缓存的 OriginQ WK_C180:

from uniqc import Circuit, submit_task, wait_for_result

bell = Circuit()
bell.h(0)
bell.cnot(0, 9)  # WK_C180 耦合图上的一条真实相邻边
bell.measure(0, 9)

result = wait_for_result(submit_task(bell, backend="dummy:originq:WK_C180", shots=512), timeout=60)
print(result)
UnifiedResult(counts={'00': 235, '01': 21, '10': 21, '11': 235}, probabilities={'00': 0.458984375, '01': 0.041015625, '10': 0.041015625, '11': 0.458984375}, shots=512, platform='dummy', task_id='uqt_0f08ed240386468282555e1d1305f78d', backend_name='dummy:originq:WK_C180', execution_time=None, error_message=None)

与理想 Bell 态(只剩 00/11)相比,真实标定噪声带来了约 4% 的 01/10 泄漏——在不上真机、不花 token 的情况下,你已经在模拟"这台机器会给出什么样的结果"。

6. 声明式含噪虚拟机:dummy:virtual

手写 ErrorLoader 适合程序化构造;如果只想用一份配置文件描述整台含噪机器(拓扑、分层门噪声、T1/T2 热弛豫、逐比特读出错误),写一个 YAML 到 ~/.uniqc/backend/virtual/<name>.yaml 即可(uniqc backend virtual init <name> 可生成带完整注释的模板)。虚拟机名就是文件名,之后任何接受 backend 的地方都能用 dummy:virtual:<name>

# ~/.uniqc/backend/virtual/uqt-tutorial.yaml
description: 教学用 3 比特含噪虚拟机
num_qubits: 3
topology:
  - [0, 1]
  - [1, 2]
gate_times_ns:
  default_1q: 30
  default_2q: 80
noise:
  depolarizing:
    1q: 0.001
    2q: 0.02
  gate_type:
    CZ: {depolarizing: 0.03}
  thermal_relaxation:
    default: {t1_us: 50, t2_us: 40}
    qubits:
      2: {t1_us: 30, t2_us: 25}
  readout:
    default: [0.02, 0.02]
    qubits:
      2: [0.05, 0.08]

配置各节读法:topology 省略则全连接;noise.depolarizing 按门元数(1q/2q)给均匀去极化,gate_type 按门名覆盖;thermal_relaxationgate_times_ns 的门时长把 T1/T2 换算成振幅阻尼 + 退相位(要求 \(T_2 \le 2T_1\));readout 逐比特给出 [p(0→1), p(1→0)]。各层噪声叠加生效

写好后先校验,再看解析结果:

$ uniqc backend virtual validate uqt-tutorial
✓ /home/agony/.uniqc/backend/virtual/uqt-tutorial.yaml: valid
  Qubits: 3, topology edges: 2
  Noise: depol, gate-type(1), T1/T2, readout
  Use: uniqc submit circuit.qasm --backend dummy:virtual:uqt-tutorial
$ uniqc backend virtual show uqt-tutorial
───────────────────────────── virtual:uqt-tutorial ─────────────────────────────
╭────────────────────────────────── Overview ──────────────────────────────────╮
│ File:        /home/agony/.uniqc/backend/virtual/uqt-tutorial.yaml            │
│ Description: 教学用 3 比特含噪虚拟机                                         │
│ Qubits:      3 (0, 1, 2)                                                     │
│ Topology:    2 edges                                                         │
│ Reference:   dummy:virtual:uqt-tutorial                                      │
╰──────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────── Gate Error Model ──────────────────────────────╮
│ Uniform depolarizing: 1q=0.001, 2q=0.02                                      │
│ Gate type CZ: depolarizing 0.03                                              │
╰──────────────────────────────────────────────────────────────────────────────╯
Gate times: default_1q=30.0ns, default_2q=80.0ns
                          Per-Qubit Noise
╭──────────┬────────────┬────────────┬──────────────┬──────────────╮
│    Qubit │    T1 (us) │    T2 (us) │  Readout p01 │  Readout p10 │
├──────────┼────────────┼────────────┼──────────────┼──────────────┤
│        0 │       50.0 │       40.0 │       0.0200 │       0.0200 │
│        1 │       50.0 │       40.0 │       0.0200 │       0.0200 │
│        2 │       30.0 │       25.0 │       0.0500 │       0.0800 │
╰──────────────────────────────────────────────────────────────────────────────╯

最后用统一入口把 GHZ 线路分别跑在无噪声与这台虚拟机上:

from uniqc import Circuit, submit_task, wait_for_result

ghz = Circuit(3)
ghz.h(0)
ghz.cnot(0, 1)
ghz.cnot(1, 2)
ghz.measure(0, 1, 2)

for backend in ("dummy:local:simulator", "dummy:virtual:uqt-tutorial"):
    result = wait_for_result(submit_task(ghz, backend=backend, shots=2048), timeout=60)
    print(f"== {backend} ==")
    print("  counts:", result.counts)
== dummy:local:simulator ==
  counts: {'000': 1024, '111': 1024}
== dummy:virtual:uqt-tutorial ==
  counts: {'000': 910, '001': 37, '010': 27, '011': 84, '100': 54, '101': 26, '110': 35, '111': 875}

同一份代码,只换 backend 字符串,就从理想世界搬进了"有 2% 双比特去极化、qubit 2 更差"的机器。与 dummy:<platform>:<chip> 的差别在噪声来源:后者从芯片标定数据换算,前者完全由你的 YAML 声明(且支持真双比特退极化通道与 T1/T2 热弛豫)。虚拟机同样受拓扑约束——YAML 里删掉 topology 节即可得到全连接机器。

下一步

练习题

练习 1【三种本地模拟输出】(→ 第 1 节

  1. 把 GHZ 线路中的 h(0) 改成 x(0),先笔算 simulate_pmeasuresimulate_shots 的结果,再运行验证。

  2. 解释输出中幅值 0.7071 与概率 0.5000 的关系:statevector 与 pmeasure 各自存的是什么量?

  3. 构造一个线路,使 |001>|110> 的概率各为 0.5(其余为 0),用 simulate_statevector 验证幅值。

提示:在 GHZ 线路末尾再加一个 x(0),同时翻转两个分量的最右比特(qubit 0)。

练习 2【模拟后端:态矢量与密度矩阵】(→ 第 2 节

  1. 分别用 statevectordensity_matrix 后端跑 GHZ 线路的 simulate_pmeasure,确认结果一致。

  2. NoisySimulatorbackend_type 故意设为 statevector 并调用 simulate_pmeasure,记录报错信息。

  3. 解释为什么噪声通道作用后的态无法再用态矢量表示,而密度矩阵始终可以。

提示:噪声带来经典不确定性——以概率 \(p\) 处于不同纯态的混合,对应 \(\rho=\sum_i p_i|\psi_i\rangle\langle\psi_i|\),一般不再是秩 1 的投影算符。

练习 3【错误模型与错误加载器】(→ 第 3 节

  1. 把分层配置中 CNOT 的噪声从 5% 改成 20%,先预测 |000> 概率的移动方向和泄漏态的分布,再运行验证。

  2. AmplitudeDamping(0.1) 替换 Depolarizing 作为通用噪声,观察 GHZ 分布的泄漏模式有何不同(哪个方向偏得多)。

  3. 改用 ErrorLoader_GateSpecificError,只给第二个 CNOT(比特 1、2)加 10% 去极化,预测哪些错误态概率升高,再验证。

提示:与施加噪声的门相邻的比特最容易被翻转,看泄漏态与 000/111 的汉明距离。

练习 4【读出错误】(→ 第 4 节

  1. 对 GHZ 线路把读出错误改为 {0: [0.0, 0.3]},先手算四个非零概率再运行验证。

  2. 给三个比特同时配 {i: [0.1, 0.1]} 的对称读出错误,预测 000111 的概率各是多少(其余态不必逐个算)。

  3. 试着对 NoisySimulator 调用 simulate_shots,记录报错,并改用第 6 节的 dummy:virtual 虚拟机完成同样的含噪采样。

提示:对称错误下每个比特读对的概率是 0.9,三个比特独立读对的概率是 \(0.9^3\)

练习 5【dummy 后端家族】(→ 第 5 节

  1. resolve_dummy_backend 查看 dummy:local:virtual-grid-2x2 的拓扑,注意标准 GHZ 线路的 cnot(1, 2) 并不是网格上的边;改写线路(仍制备 GHZ 态)后提交并验证计数。

  2. 解释 simulate_shots 返回 {7: 965, 0: 1035}wait_for_result 返回 {'000': ..., '111': ...}:两套键格式分别是什么,为什么要统一后者?

  3. bad 线路提交到 dummy:local:simulator,验证全连接后端不报拓扑错误;再对比第 5 节的失败信息,总结"拓扑约束由 backend id 声明"这句话的含义。

提示:网格上 qubit 0 与 1、2 都相邻,用 cnot(0, 1)cnot(0, 2) 的"星形"布局同样能制备 GHZ 态。

练习 6【声明式含噪虚拟机】(→ 第 6 节

  1. uniqc backend virtual init 生成模板,把 qubit 0 的读出错误改成 [0.2, 0.2],预测 GHZ 泄漏如何加剧,再提交验证。

  2. 把 YAML 中 t2_us 改成大于 2 * t1_us 的值,运行 uniqc backend virtual validate,观察并解释报错。

  3. 删除 YAML 的 topology 节使虚拟机全连接,重新提交第 5 节的 bad 线路(含 cnot(0, 2)),验证它现在能执行。

提示:\(T_2 > 2T_1\) 时退相位概率 \(p_\varphi = 0.5(1-\mathrm{e}^{-t(1/T_2-1/2T_1)})\) 会变成负数,物理上不允许。