4. 仿真环境搭建
BB84仿真的第一步不是写协议,而是把“能稳定复现实验”的环境搭起来。量子密钥分发仿真涉及三类计算:经典概率与随机抽样(密钥比特、基选择、误码注入)、量子态的制备与测量(光子偏振态、测量算符)、以及统计后处理(筛选、误码率估计、窃听检测)。因此环境需要同时具备数值计算、量子态演化和可复现的随机数控制能力。本节以 Python 3.10~3.12 为基准,给出可直接落地的安装路径、最小验证示例和项目目录规范。
一、科学计算栈与量子仿真库的选型
基础栈建议固定为 numpy、scipy、pandas、matplotlib,分别承担数组运算、统计与优化、结果表格化、误码率曲线绘制。量子层有两个主流选择:QuTiP 面向开放量子系统,用 Qobj 表示态与算符,适合把 BB84 的偏振态写成密度矩阵、把测量写成投影算符,便于后续扩展到噪声信道与退相干;Qiskit 面向量子线路,适合用 QuantumCircuit 表达“制备—传输—测量”的线路结构,并可直接对接模拟器后端。对 BB84 仿真而言,两者都能胜任,区别在于抽象层次:QuTiP 更贴近“态与算符”的物理视角,Qiskit 更贴近“线路与测量”的工程视角。本课程主线采用 QuTiP 做态演化,Qiskit 作为对照实现,二者共存于同一环境并不冲突。
安装时建议使用虚拟环境隔离,避免与系统 Python 冲突。以 conda 为例,创建环境并一次性装齐依赖:
conda create -n bb84sim python=3.11 -y
conda activate bb84sim
pip install numpy scipy pandas matplotlib
pip install qutip qiskit qiskit-aer
若使用纯 venv,则把前两行替换为 python -m venv .venv 与激活命令即可。安装完成后必须做版本核对,因为 QuTiP 4 与 5 的 API 存在差异(例如 qutip.sesolve 的参数组织),Qiskit 1.x 也调整了 Aer 的导入路径。建议在项目里锁定版本,而不是“装最新”。
二、最小示例:用 QuTiP 表达 BB84 的单光子偏振态
BB84 使用两组共轭基:直角基(Z 基)取水平 |H⟩ 与垂直 |V⟩,对角基(X 基)取 |+⟩=(|H⟩+|V⟩)/√2 与 |−⟩=(|H⟩−|V⟩)/√2。在 QuTiP 中把 |H⟩ 记为基矢 basis(2,0),|V⟩ 记为 basis(2,1),则四个态可直接构造,测量算符用外积表示。下面这段代码验证了“同基测量必得原比特、异基测量结果随机”这一 BB84 的核心性质:
import numpy as np
import qutip as qt
H, V = qt.basis(2, 0), qt.basis(2, 1)
P = [(H + V).unit(), (H - V).unit()] # |+>, |->
states = {'Z0': H, 'Z1': V, 'X0': P[0], 'X1': P[1]}
proj = {'Z': [H * H.dag(), V * V.dag()],
'X': [P[0] * P[0].dag(), P[1] * P[1].dag()]}
def measure(state, basis):
p = [qt.expect(m, state) for m in proj[basis]]
return int(np.random.choice([0, 1], p=np.array(p) / sum(p)))
np.random.seed(2024)
for name, st in states.items():
prep_basis, bit = name[0], int(name[1])
same = measure(st, prep_basis)
cross = measure(st, 'X' if prep_basis == 'Z' else 'Z')
print(f"{name}: 同基测得 {same}(应等于 {bit}),异基测得 {cross}")
预期输出中,同基测量结果恒等于制备比特,异基测量在 0/1 间近似等概率出现。这正是后续“基比对”步骤能筛出正确密钥、而窃听者用错基会引入约 25% 误码的物理根源。示例中的 seed 是刻意设置的:仿真必须可复现,所有随机过程都应通过统一的随机数生成器控制,而不是散落在各处的 np.random 全局调用。
三、最小示例:用 Qiskit 搭建一条 BB84 单轮线路
Qiskit 视角下,一次“制备—测量”可写成两量子比特线路:一个比特承载信号,另一个比特仅用于演示测量基切换。用 H 门制备 |+⟩,用 measure 指定测量基,通过 AerSimulator 取 counts:
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
qc = QuantumCircuit(1, 1)
qc.h(0) # 制备 |+>
qc.measure(0, 0) # Z 基测量
sim = AerSimulator(seed_simulator=7)
result = sim.run(transpile(qc, sim), shots=1000).result()
print(result.get_counts()) # 约各 50% 的 {'0':..., '1':...}
把 h 换成 x 即制备 |1⟩,在测量前再加一个 h 即切换到 X 基测量。Qiskit 的价值在于线路结构一目了然,便于把 BB84 扩展成含窃听者 Eve 的三方线路;QuTiP 的价值在于能直接给出密度矩阵与保真度。建议在项目中让两者共享同一套“比特序列、基序列、误码注入”的经典层,量子层只负责态与测量,这样两种实现可以交叉验证。
四、项目目录与依赖管理
仿真代码一旦超过两百行,就必须把“协议逻辑”和“量子实现”分开,否则更换后端时会把协议层一起改乱。推荐的最小目录结构如下:src/bb84/protocol.py 只放经典层逻辑——生成比特与基、基比对、筛选密钥、误码率计算、窃听判定;src/bb84/quantum.py 定义统一接口 prepare(bit, basis) 与 measure(state, basis),内部可切换 QuTiP 或 Qiskit 实现;tests/ 放性质测试,例如“同基测量 1000 次全部正确”“异基测量卡方检验不拒绝 50/50”;results/ 存放 qber.csv 与图表,且不纳入版本控制。
依赖管理的关键是锁定版本并记录环境。使用 pyproject.toml 声明依赖区间,再用 pip freeze > requirements.lock 固化实际安装版本;若用 conda,则导出 environment.yml。一个可直接参考的依赖声明如下:
[project]
name = "bb84-sim"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"numpy>=1.26,<2.1",
"scipy>=1.11",
"pandas>=2.1",
"matplotlib>=3.8",
"qutip>=5.0,<6",
"qiskit>=1.0,<2",
"qiskit-aer>=0.14",
]
需要特别提醒两个常见坑。其一是 随机性来源不统一:协议层用 np.random、量子层用 Qiskit 的 seed_simulator,会导致同一 seed 下两次运行结果不同,复现失败。正确做法是创建一个 np.random.Generator 实例贯穿全流程,并把它的种子写入结果文件的元数据。其二是 QuTiP 与 Qiskit 的比特约定不一致:QuTiP 的 basis(2,0) 对应 |0⟩,Qiskit 的测量比特 0 也对应 |0⟩,但若在 Qiskit 中把 |H⟩ 映射为 |1⟩,两套实现就会在“比特—态”映射上错位,表现为交叉验证时误码率异常偏高。建议在 quantum.py 顶部用常量显式声明映射,并在测试中断言两套后端对同一 (bit, basis) 输入返回相同测量分布。
完成本节后,你应当拥有一个可激活的独立环境、一份锁定版本的依赖清单、一个能打印出“同基测量恒正确、异基测量近似随机”的最小脚本,以及一个协议层与量子层分离的目录骨架。这套骨架将直接承载后续的密钥筛选、窃听检测与误码率曲线绘制,无需再返工重构。