本地模拟与含噪模拟¶
上一章我们用 Simulator 跑过一次无噪声采样;本章把它讲透:Simulator 的三种输出各回答什么问题、态矢量与密度矩阵两类后端如何取舍、怎样用错误加载器与读出错误手工构造噪声,以及 dummy 后端家族——从无约束模拟器、虚拟拓扑、真实芯片标定噪声到完全由一份 YAML 声明的含噪虚拟机。
态矢量、测量统计与纠缠的理论背景见量子计算算法教程·量子计算基础与量子力学的基本概念。本页示例基于 unified-quantum 0.1.0,所有输出均为实际运行结果;凡涉及随机采样的输出每次运行略有不同。
本课知识点
三种本地模拟输出——能写出
simulate_pmeasure/simulate_statevector/simulate_shots的调用方式,并区分它们各自回答的问题。模拟后端:态矢量与密度矩阵——能比较 statevector 与 density_matrix 两类后端的能力与开销,并解释噪声模拟为何必须用密度矩阵。
错误模型与错误加载器——能用三层
ErrorLoader构造从全局到逐门实例的噪声配置,并预测噪声如何让 GHZ 态"泄漏"到其他基矢。读出错误——能写出
readout_error二元组的含义,并手算它对测量概率的作用。dummy 后端家族——能比较
dummy:local:*各成员的拓扑约束与模拟引擎,并解释统一入口如何呈现拓扑不兼容错误。声明式含噪虚拟机——能用一份 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 次样并计数,模拟真实实验"重复测量"的过程,键是十进制整数(7即111)。
概率列表的位序约定:下标 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']
后端( |
噪声支持 |
内存开销 |
适用场景 |
|---|---|---|---|
|
❌ |
\(O(2^n)\) |
无噪声快速模拟,默认值 |
|
✅ |
\(O(4^n)\) |
含噪声模拟( |
|
✅ |
\(O(4^n)\) |
复杂噪声模型,需额外安装 |
噪声为什么必须用密度矩阵?态矢量只能描述纯态,而任何噪声通道都会把纯态变成混合态——只能用密度矩阵 \(\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 里:
错误模型 |
物理含义 |
参数 |
|---|---|---|
|
位翻转 / 相位翻转 |
|
|
单比特去极化 |
|
|
双比特去极化 |
|
|
振幅阻尼(能量弛豫) |
|
|
一般单比特泡利噪声 |
|
错误加载器分三层,逐层收窄作用范围:
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,差额摊到其余六个基矢上。去极化噪声以单比特/双比特泡利错误为主,因此与 000、111 汉明距离为 1 的态(001、110 等)泄漏最明显。两组含噪结果都是确定性的(密度矩阵演化不采样),只有 simulate_shots 才有随机涨落。
4. 读出错误:测量端的混淆矩阵¶
门噪声之外,真实设备还有读出错误:测 \(|0\rangle\) 读成 1、测 \(|1\rangle\) 读成 0。NoisySimulator 的 readout_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 |
拓扑 |
噪声 |
说明 |
|---|---|---|---|
|
全连接 |
无 |
无约束虚拟机,调试首选 |
|
\(N\) 比特链 |
无 |
虚拟链式拓扑 |
|
\(R\times C\) 网格 |
无 |
虚拟网格拓扑 |
|
\(N\) 比特链 |
无 |
MPS 引擎,大比特数低纠缠可扩展 |
|
真实芯片耦合图 |
芯片标定噪声 |
先按真芯片编译,再本地注入噪声 |
|
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_max 与 seed。counts 的键是二进制串(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_relaxation 按 gate_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 节即可得到全连接机器。
下一步¶
噪声让
01/10出现了——如何测量并缓解这些误差,见第 6 章:芯片校准与误差缓解把模拟结果画成图(计数直方图、线路图),见第 4 章:可视化
准备把线路提交到真实云平台,见第 5 章:云端执行
混合态与密度矩阵的测量理论(量子层析),见姊妹站量子态层析
练习题¶
练习 1【三种本地模拟输出】(→ 第 1 节)
把 GHZ 线路中的
h(0)改成x(0),先笔算simulate_pmeasure与simulate_shots的结果,再运行验证。解释输出中幅值
0.7071与概率0.5000的关系:statevector 与 pmeasure 各自存的是什么量?构造一个线路,使
|001>与|110>的概率各为 0.5(其余为 0),用simulate_statevector验证幅值。
提示:在 GHZ 线路末尾再加一个
x(0),同时翻转两个分量的最右比特(qubit 0)。
练习 2【模拟后端:态矢量与密度矩阵】(→ 第 2 节)
分别用
statevector与density_matrix后端跑 GHZ 线路的simulate_pmeasure,确认结果一致。把
NoisySimulator的backend_type故意设为statevector并调用simulate_pmeasure,记录报错信息。解释为什么噪声通道作用后的态无法再用态矢量表示,而密度矩阵始终可以。
提示:噪声带来经典不确定性——以概率 \(p\) 处于不同纯态的混合,对应 \(\rho=\sum_i p_i|\psi_i\rangle\langle\psi_i|\),一般不再是秩 1 的投影算符。
练习 3【错误模型与错误加载器】(→ 第 3 节)
把分层配置中 CNOT 的噪声从 5% 改成 20%,先预测
|000>概率的移动方向和泄漏态的分布,再运行验证。用
AmplitudeDamping(0.1)替换Depolarizing作为通用噪声,观察 GHZ 分布的泄漏模式有何不同(哪个方向偏得多)。改用
ErrorLoader_GateSpecificError,只给第二个 CNOT(比特 1、2)加 10% 去极化,预测哪些错误态概率升高,再验证。
提示:与施加噪声的门相邻的比特最容易被翻转,看泄漏态与
000/111的汉明距离。
练习 4【读出错误】(→ 第 4 节)
对 GHZ 线路把读出错误改为
{0: [0.0, 0.3]},先手算四个非零概率再运行验证。给三个比特同时配
{i: [0.1, 0.1]}的对称读出错误,预测000与111的概率各是多少(其余态不必逐个算)。试着对
NoisySimulator调用simulate_shots,记录报错,并改用第 6 节的dummy:virtual虚拟机完成同样的含噪采样。
提示:对称错误下每个比特读对的概率是 0.9,三个比特独立读对的概率是 \(0.9^3\)。
练习 5【dummy 后端家族】(→ 第 5 节)
用
resolve_dummy_backend查看dummy:local:virtual-grid-2x2的拓扑,注意标准 GHZ 线路的cnot(1, 2)并不是网格上的边;改写线路(仍制备 GHZ 态)后提交并验证计数。解释
simulate_shots返回{7: 965, 0: 1035}而wait_for_result返回{'000': ..., '111': ...}:两套键格式分别是什么,为什么要统一后者?把
bad线路提交到dummy:local:simulator,验证全连接后端不报拓扑错误;再对比第 5 节的失败信息,总结"拓扑约束由 backend id 声明"这句话的含义。
提示:网格上 qubit 0 与 1、2 都相邻,用
cnot(0, 1)与cnot(0, 2)的"星形"布局同样能制备 GHZ 态。
练习 6【声明式含噪虚拟机】(→ 第 6 节)
用
uniqc backend virtual init生成模板,把 qubit 0 的读出错误改成[0.2, 0.2],预测 GHZ 泄漏如何加剧,再提交验证。把 YAML 中
t2_us改成大于2 * t1_us的值,运行uniqc backend virtual validate,观察并解释报错。删除 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)})\) 会变成负数,物理上不允许。