这两年量子计算的热度大家都看到了,但真正动手写过量子程序的人其实没那么多。我一直觉得,要理解量子算法,最好的方式不是直接调Qiskit或Cirq这种框架的API,而是自己从零写一个能在经典计算机上运行的量子计算模拟器。这个想法在去年终于落地了,我把教学用的模拟器开源了出来,项目代号就叫DREAMVFIA,整个实现只依赖NumPy,目标是让任何有Python基础的人都能看懂量子电路在底层到底是怎么算的。
这篇是上篇,先把地基打牢:量子比特的数学表示、单比特门、双比特门、测量机制,最后用不到五十行代码跑通一个真正的Bell态电路——也就是那个著名的“纠缠态”。如果你之前对量子计算的理解只停留在“叠加”“坍缩”这些名词上,这篇文章就是来把这些名词变成你能摸到的数据结构和矩阵运算的。
1. 为什么要在Python里写一个量子模拟器(以及DREAMVFIA的起点)
1.1 模拟器补上的认知断层
说实话,现在的量子计算学习路径有点尴尬。直接上真机吧,排队几小时是家常便饭,而且真机的噪声、退相干、校准问题会把一个刚入门的初学者劝退。直接学算法吧,市面上大多数教程又默认你已经理解了量子门的物理意义。我当初就是卡在这个位置:知道X门是比特翻转,知道H门产生叠加,但这些门组合起来为什么能加速搜索?为什么纠缠态是“纠缠”的?没人告诉我。
模拟器恰好补上了这块认知断层。只要把量子态当成一个复数向量,把量子门当成矩阵,把测量当成概率采样,所有抽象概念都会变成屏幕上实实在在的数字。这一点是真实量子硬件做不到的——真机你只能拿到测量结果,但模拟器可以把每一步运算结束后的中间状态完整打印出来,这对于理解算法内部的运作机制简直是降维帮助。
DREAMVFIA这个项目的定位就是教学型模拟器。它刻意不做成黑盒,所有门的矩阵、所有采样逻辑、所有张量积的顺序都摊开放在源码里。你甚至可以给它加调试打印,看看每个门作用前后量子态发生了什么样的变化。这种“全透明”的体验,是Qiskit这类工业级框架给不了的。
1.2 为什么是Python而不是C++/Rust
这是个好问题。C++或Rust在性能上远超Python,真要模拟大规模量子电路,Python确实不是对手。但DREAMVFIA的首要目标不是性能,而是教学和正确性。Python配上NumPy,底层是BLAS/LAPACK这些经过几十年优化的线性代数库,矩阵乘法效率并不差,而且代码可读性非常高。
另一个现实的理由是:NumPy的np.kron(张量积)、np.dot、复数数组这些特性,天然和量子计算的数学语言对齐。你不需要处理指针、内存管理、模板元编程,就能把量子态的演化写得和教科书公式一一对应。这一点在项目早期尤其重要,因为量子计算里最容易犯错的就是索引顺序和符号约定问题,Python的交互式环境让你可以随时验证中间结果,减少了很多无谓的debug时间。
如果你之后对性能有刚需,可以把核心的矩阵乘法部分切到numba或用C扩展。但DREAMVFIA设计上就把这两层拆开了,你完全可以在保持API不变的前提下替换底层实现。先跑对,再跑快,这是我做开源项目一贯的顺序。
1.3 模拟器的天花板:2^N维度内存爆炸
在进入代码之前,必须先搞清楚一个物理现实:经典计算机模拟量子计算的代价是指数级的。一个n量子比特的系统,其量子态是一个包含2^n个复数的向量。每个复数如果按两个float64存储,就是16字节,那么30个量子比特的状态向量大约是16GB,32个比特直接飚到64GB,普通笔记本电脑已经扛不住了。
这个内存上限也是推动真实量子计算机发展的核心理由之一——因为真机不存在这种指数级的内存膨胀。DREAMVFIA在项目文档里明确写了适用边界:算法演示、教学验证、中小规模电路,基本在20个量子比特以内可以跑得很舒服;超过这个规模,就该去考虑真实硬件或者更高级的张量网络模拟方法了。
理解这个边界很重要。它不是缺陷,而是模拟器这种工具的天然属性。你用它学原理、跑算法、验证想法,而不是拿它挑战世界纪录。后面我所有代码示例都控制在几个比特的规模,就是为了让你在笔记本上也能立刻跑起来。
2. 量子比特的数学底座:状态向量与叠加态
2.1 经典比特到量子比特:不只是一个0/1开关
经典比特很简单,要么是0,要么是1,一根电线缆要么通电要么不通电。但量子比特没法用这种“开关”直觉去套。一个量子比特的状态可以写成:
|ψ⟩ = α|0⟩ + β|1⟩
这里的希腊字母α和β是复数,描述的是量子比特处于|0⟩和|1⟩的“概率幅”。注意,这里不是经典概率混合,不是说有65%概率是0、35%概率是1。叠加态是一种更微妙的属性:量子比特“同时”处于两种计算基态上,而这个“同时”只有在测量时才会被打破。
我一开始也老犯迷糊,后来用一句话提醒自己:经典是“要么/要么”,量子是“既/又”。你在计算机里存储的是α和β这两个复数,它们本身才是量子比特状态的真实表示。
2.2 Dirac记号与基底向量
Dirac记号(狄拉克记号)是量子计算的语言基础。|0⟩读作“ket 0”,它就是一个列向量[1, 0]的转置;|1⟩是列向量[0, 1]。在n量子比特系统里,计算基态的数量是2^n,每个基态都可以用一串0和1来表示,比如|00⟩、|01⟩、|10⟩、|11⟩。
代码里,我直接用NumPy的复数数组来表示这些向量。初始化一个n比特系统,就是创建一个长度为2^n的全零复数数组,然后把第0号元素设为1。为什么是第0号?因为数组下标0对应二进制全零的基态,也就是所有比特都处于|0⟩的状态。这是整个模拟器的最初始状态,后续所有门的操作都从这个基准出发。
2.3 叠加态的归一化条件与Born规则
既然α和β是概率幅,那么它们和概率之间的关系由Born规则给出:测量得到|0⟩的概率是|α|²,测量得到|1⟩的概率是|β|²。这两项加起来必须等于1,也就是归一化条件:
|α|² + |β|² = 1
为什么要归一化?因为量子门在数学上是幺正矩阵(unitary matrix),它作用在量子态上会保持向量的长度不变。如果初始状态是归一化的,经过任何合法量子门演化后依然归一化,这样测量概率永远是合法的概率分布。
构建模拟器时,我一直保持着两个习惯。第一,每次应用门之后检查np.sum(np.abs(state) ** 2)是否约等于1,一旦偏差超过1e-10,说明某个门的矩阵构造有问题。第二,测量采样时,我会把概率向量再除以总和做一次软归一化,因为浮点计算累积的小误差可能让np.random.choice的p参数总和差一点点。
2.4 用NumPy把状态向量落地
说了这么多抽象概念,是时候看实际代码了。初始化一个单比特系统和多比特系统的代码非常直接:
import numpy as np # 单比特基态 state_0 = np.array([1, 0], dtype=complex) state_1 = np.array([0, 1], dtype=complex) # n比特系统:初始化为 |00...0> def zero_state(n_qubits): dim = 2 ** n_qubits state = np.zeros(dim, dtype=complex) state[0] = 1 return state state = zero_state(2) print(state) # [1.+0.j 0.+0.j 0.+0.j 0.+0.j]这段代码虽然简单,但它定义了整个模拟器的核心数据结构。往后每一步量子门操作,本质上都是对这样一个长度为2^n的复数数组做线性变换。QuantumCircuit类到时候就是围绕这个数组做封装,提供更友好的门调用接口。
3. 量子门就是矩阵:单比特门的核心实现
3.1 幺正矩阵:为什么量子门必须是它
现在到了理解量子计算最关键的一步:量子门在数学上是一个幺正矩阵。所谓幺正,就是满足U†U = UU† = I,其中U†是U的共轭转置。这个性质保证了量子门的可逆性,也保证了归一化条件永远成立。
为什么必须可逆?因为量子演化在理想情况下是信息无损的。经典计算机里的AND门是信息丢失的,两个输入比特变成一个输出比特,丢失的信息永远消失了,所以没有逆运算。但量子门不能这样,它必须把输入的量子态一一映射到输出量子态,而且这个映射必须是双射,否则概率无法保持。
开发DREAMVFIA时,我写了一个单元测试函数,专门检查每个门的矩阵是不是幺正的:计算U的共轭转置乘以U,看看结果是否接近单位矩阵。这个测试看起来多余,但它帮我抓过不少低级错误,比如矩阵元素抄错正负号,或者忘记除以√2。
3.2 常用单比特门的矩阵
DREAMVFIA里首先要实现五个基础单比特门。它们的矩阵形式如下:
| 门 | 矩阵 | 作用 |
|---|---|---|
| I | [[1, 0], [0, 1]] | 什么都不做 |
| X | [[0, 1], [1, 0]] | 比特翻转: |
| Y | [[0, -i], [i, 0]] | 比特翻转+相位 |
| Z | [[1, 0], [0, -1]] | 相位翻转: |
| H | (1/√2)[[1, 1], [1, -1]] | 产生叠加: |
其中H门也叫Hadamard门,它是整个量子算法里出场频率最高的角色。在代码里定义这些门非常直观:
I = np.array([[1, 0], [0, 1]], dtype=complex) X = np.array([[0, 1], [1, 0]], dtype=complex) Y = np.array([[0, -1j], [1j, 0]], dtype=complex) Z = np.array([[1, 0], [0, -1]], dtype=complex) H = np.array([[1, 1], [1, -1]], dtype=complex) / np.sqrt(2)你可能会问,为什么X门就是经典非门?因为把X矩阵作用到|0⟩上,得到[0, 1],也就是|1⟩;作用到|1⟩上,得到[1, 0],也就是|0⟩。确实翻转了。但X门比经典非门强的地方在于,它还能作用于叠加态:α|0⟩ + β|1⟩经过X门会变成β|0⟩ + α|1⟩,两个概率幅对调,这是经典逻辑门做不到的。
3.3 应用单比特门:kron构造完整矩阵
在单比特系统上应用门很简单,直接做矩阵乘法就行。但量子电路通常有多个比特,每个门只会作用在其中某一个比特上。比如有两个量子比特,我想在第一个比特上做H门,第二个比特保持不动,那么实际作用的矩阵是H与I的张量积:H⊗I。
DREAMVFIA里的apply_single_qubit_gate函数就是这样构造完整矩阵的。这里要特别强调量子比特顺序约定。我采用大端序(big-endian):数组下标的二进制表示最左边是第0号量子比特,最右边是第n-1号量子比特。也就是说,对于2个比特,下标2的二进制是10,代表|10⟩,也就是第0号比特为1、第1号比特为0。
def apply_single_qubit_gate(state, gate, target_qubit, n_qubits): """ 在target_qubit上应用单比特门。 约定:q0是最高位(大端序)。 """ full_matrix = np.array([[1]], dtype=complex) for q in range(n_qubits): if q == target_qubit: full_matrix = np.kron(full_matrix, gate) else: full_matrix = np.kron(full_matrix, I) return full_matrix @ state这个实现方式不是性能最优,但对于教学目的来说,它的每一步都对应了张量积的概念,非常直观。如果以后要模拟20个以上的量子比特,这种构造完整矩阵再点乘的方法会非常慢,因为需要构造一个4^n大小的矩阵。到时候可以改用reshape操作直接对状态向量做张量收缩,性能会有质的飞跃。
3.4 H门的直觉:量子版公平硬币
H门在量子计算里作用太大了,值得单独聊一聊。把它作用在|0⟩上,结果是(|0⟩ + |1⟩)/√2。这个状态测量后,得到0和1的概率各是50%,看起来就像抛一枚公平硬币。
但H门和公平硬币有一个根本区别:把两个H门连续作用在|0⟩上,结果是|0⟩,概率是100%。如果它是公平硬币,两次抛掷的结果应该会出现“正反组合”的概率分布,而不是确定回到起点。这个特性叫干涉(interference),是量子计算能超越经典计算的秘密武器之一。记住这个特性,后面在Deutsch-Jozsa算法、Grover搜索算法里会反复见到它。
3.5 浮点精度和归一化的坑
写模拟器绕不开浮点误差,这里分享一个我踩过好几次的坑。H门的矩阵含有1/√2这个无理数,连续作用两次H门后,理想结果应该精确等于单位矩阵,但浮点运算会得到类似0.9999999999999的对角线元素。如果你直接拿它和单位矩阵比较,结果会是False。
解决办法很简单:用np.allclose而不是==来判断矩阵相等,另外在测量前对概率做一次归一化。我还在DREAMVFIA里封装了一个is_normalized函数,每次门操作后检查归一化误差,一旦超限就抛警告。这个习惯帮我避开了好几个因为矩阵构造错误导致概率分布完全乱掉的情况。
4. 从单比特到双比特:张量积、CNOT门与纠缠
4.1 复合系统为什么用张量积
单比特系统的状态是二维复数向量,那两个比特的组合空间是几维?答案是4维,而不是2+2=4这么简单。更准确地说,是2×2=4维。三个比特就是2³=8维,n个比特就是2ⁿ维。这个指数增长的维度,正是量子计算能力的来源,也是经典模拟器的噩梦。
张量积是组合量子系统的数学工具。在NumPy里,np.kron就是做这个的。两个单比特门组合时,比如第一个比特做H、第二个比特做X,那么完整门矩阵就是np.kron(H, X)。如果你构造的矩阵维度不对,大概率是张量积的顺序写反了,导致控制比特和目标比特被调换。
4.2 从kron到多比特量子门
DREAMVFIA里构建多比特门的基础就是kron,但它有个更灵活的需求:CNOT门这种双比特门的两个控制目标比特并不总是相邻的。处理这种情况,我用了一个比较朴素但非常清晰的方法——遍历所有2ⁿ个基态,对每个基态判断:如果控制位是1,就把目标位翻转。然后构造出对应的置换矩阵。
以CNOT矩阵为例。用咱的大端序约定,控制位是0号比特、目标位是1号比特时,量子电路的行为是:|00⟩→|00⟩,|01⟩→|01⟩,|10⟩→|11⟩,|11⟩→|10⟩。对应的4×4矩阵是:
# CNOT矩阵 (ctrl=0, targ=1, 大端序) cnot_0_1 = np.array([ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0] ], dtype=complex)当然,写模拟器不可能每个门都从查矩阵开始,那太不工程了。所以我用循环构造了一种通用做法:
def cnot_matrix(ctrl, targ, n_qubits): dim = 2 ** n_qubits U = np.zeros((dim, dim), dtype=complex) for i in range(dim): # 将下标转成比特列表(大端序) bits = [(i >> (n_qubits - 1 - k)) & 1 for k in range(n_qubits)] if bits[ctrl] == 1: bits[targ] ^= 1 # 翻转目标比特 j = 0 for b in bits: j = (j << 1) | b U[j, i] = 1 return U这个函数的时间复杂度是O(2ⁿ),每次调用都全量重算矩阵,效率不高,但代码和数学定义完全对标,调试时可以直接对照你手算的基态映射。性能优化的事后面再说,教学代码优先保证正确性。
4.3 CNOT门:量子世界的if语句
CNOT(受控非门)是量子计算里非常重要的双比特门,因为它能制造纠缠。它的逻辑很直白:如果控制比特是|1⟩,就对目标比特做X翻转;如果控制比特是|0⟩,目标比特保持不变。
这不就是if语句吗?对,它就是量子世界的if语句。但它的强悍之处在于,如果控制比特处于叠加态,CNOT门会把这个叠加“传染”到目标比特上,从而创造出两个比特都处于叠加相关态的情况——这就是纠缠。经典if语句做不了这件事,因为经典程序的条件必须是确定的0或1。
4.4 Bell态:纠缠从哪冒出来
现在到了这篇文章最激动人心的部分。我们用现有的模拟器组件搭建第一个真正的量子电路,制造Bell态。Bell态是最简单也是最典型的纠缠态,它的形式是(|00⟩ + |11⟩)/√2。
搭建这个电路只需要两步:
# 初始化两个比特 state = zero_state(2) # 第一步:在第0号比特上施加H门 state = apply_single_qubit_gate(state, H, 0, 2) # 此时状态变为 (|00> + |10>) / √2 # 第二步:以第0号比特为控制位,第1号比特为目标位,施加CNOT门 state = cnot_matrix(0, 1, 2) @ state print(state) # 输出: [0.70710678+0.j, 0.+0.j, 0.+0.j, 0.70710678+0.j]输出里的第0号元素是0.707,第3号元素也是0.707。按照大端序映射,第0号下标对应|00⟩,第3号下标对应|11⟩,中间两个下标对应的|01⟩和|10⟩是零。也就是说,这个双比特系统的量子态确实变成了(|00⟩ + |11⟩)/√2。
为什么说它纠缠?因为如果你测量第0号比特,得到的结果有50%是0、50%是1;但一旦你知道了第0号比特的结果,第1号比特的结果就完全确定了。它们像一个不可分割的整体,各自的“个体状态”在纠缠面前失去了意义。用经典概率来描述这种相关性是不可能的——经典的“两个比特要么是00要么是11”和这里的叠加态有本质区别,区别就在于叠加态下还存在干涉的可能性,而经典概率没有。
4.5 测量的实现:按Born规则抽样
模拟器的最后一块拼图是测量。量子力学告诉我们,测量会让量子态坍缩到某个计算基态,结果带有概率性,概率由Born规则给出。在经典模拟器里,“测量”这个动作其实分两步走:
第一步,根据当前量子态计算每个基态的概率:probs = np.abs(state) ** 2。第二步,根据这个概率分布做随机抽样,得到最终的经典结果。
下面是我在DREAMVFIA里使用的测量函数核心部分:
def measure_state(state, shots=1024): probs = np.abs(state) ** 2 probs = probs / np.sum(probs) # 浮点误差的软归一化 dim = len(state) outcomes = np.random.choice(dim, size=shots, p=probs) return outcomes注意这里有个重要的细节:真正的量子硬件上,测量之后量子态就坍缩了,你无法再看到坍缩前的完整信息。但模拟器可以提前访问整个概率分布,甚至可以在不“坍缩”的情况下把量子态原样保留下来。这是一个只有模拟器才有的温柔待遇,我经常利用它来观察中间态的振幅,而不是傻傻地只盯着采样结果。
5. 电路级封装:QuantumCircuit类与测量逻辑
5.1 最小API设计
散装的矩阵乘法函数演示原理很合适,但真要写算法试验,需要更友好的接口。DREAMVFIA从设计之初就决定模仿主流框架的写电路风格,但把依赖降到最低。它提供的核心方法只有四个:
- 构造一个n比特电路,初始状态为|0...0⟩
- 在指定比特上施加单比特门(h、x、z等)
- 在指定比特对上施加CNOT门(cx)
- 对整个电路进行多次测量并返回统计结果
这看起来很简单,但恰恰是这套简单API可以跑通几乎所有经典量子算法,包括Deutsch-Jozsa、Grover搜索这些教科书级别的例子。少即是多,在这个项目里尤其明显。
5.2 即时执行与延迟执行的选择
市面上的主流模拟器通常会先把所有门保存成一个电路图,再统一编译执行。这种方式叫延迟执行,优点是能做门融合、线路优化,真实硬件上也必须这么做。但DREAMVFIA选择了更朴素的即时执行:每调用一个门方法,立刻在状态向量上进行矩阵乘法。
即时执行的好处是调试直观。你在每个门之后打印状态向量,立刻能看到这个门对量子态的影响,不用去解析复杂的数据结构。代价是少了线路优化的可能性,但对于教学场景这不是问题。当你理解了量子计算的本质之后再去看Qiskit那套编译管线,会觉得豁然开朗,但对初学者来说,延迟执行的多层抽象反而会掩盖量子原理。
5.3 QuantumCircuit核心代码
这是我写这个模拟器时最满意的一部分,因为代码可以足够短但功能足够全:
class QuantumCircuit: def __init__(self, n_qubits): self.n = n_qubits self.state = np.zeros(2 ** n_qubits, dtype=complex) self.state[0] = 1 def _apply_single(self, gate, target): self.state = apply_single_qubit_gate(self.state, gate, target, self.n) def h(self, target): self._apply_single(H, target) return self def x(self, target): self._apply_single(X, target) return self def z(self, target): self._apply_single(Z, target) return self def cx(self, ctrl, targ): self.state = cnot_matrix(ctrl, targ, self.n) @ self.state return self def measure(self, shots=1024): probs = np.abs(self.state) ** 2 probs = probs / np.sum(probs) return np.random.choice(2 ** self.n, size=shots, p=probs)每个门的return self让电路构建可以链式调用,写起来很顺手。你也可以看到measure函数没有修改self.state,这意味着同一个量子电路可以反复测量很多次,每次都得到新的采样结果。这对于调试算法特别有用——你可以先把电路构建好,反复采样看统计规律,而不用每次重头搭建。
5.4 用已知结果做回归测试
作为一个正经的开源项目,DREAMVFIA里必须有测试,不然代码改着改着就会出幺蛾子。我总结了三类最基本的回归测试:
第一类:X门作用在|0⟩上必须得到|1⟩。这验证了最基本的比特翻转逻辑正确。第二类:连续两次H门作用在任何基态上,结果必须回到原始状态(在浮点误差范围内)。这验证了H门的矩阵归一化没有问题。第三类:Bell态电路的测量结果中,00和11的出现次数大致相等,01和10几乎不出现。这验证了张量积顺序和CNOT逻辑都正确。
这些测试看起来简单,但任何一个失败都意味着你之前写的某个门矩阵有问题。我把它们写成了一个脚本,每次提交代码时自动跑一遍。维护这个测试集花不了多少功夫,却能让后续迭代安心很多。
6. 上篇验收:跑通Bell态电路,看到“量子”真的坍缩成经典结果
6.1 完整代码一览
到这里,我们已经把所有零件都准备好了。把散落的函数和类拼在一起,一个完整的Bell态制备与测量程序就长这样:
import numpy as np from collections import Counter # ---- 基础门 ---- I = np.array([[1, 0], [0, 1]], dtype=complex) X = np.array([[0, 1], [1, 0]], dtype=complex) H = np.array([[1, 1], [1, -1]], dtype=complex) / np.sqrt(2) # ---- 单比特门应用 ---- def apply_single_qubit_gate(state, gate, target, n_qubits): full = np.array([[1]], dtype=complex) for q in range(n_qubits): full = np.kron(full, gate if q == target else I) return full @ state # ---- CNOT矩阵 ---- def cnot_matrix(ctrl, targ, n_qubits): dim = 2 ** n_qubits U = np.zeros((dim, dim), dtype=complex) for i in range(dim): bits = [(i >> (n_qubits - 1 - k)) & 1 for k in range(n_qubits)] if bits[ctrl] == 1: bits[targ] ^= 1 j = 0 for b in bits: j = (j << 1) | b U[j, i] = 1 return U # ---- 量子电路类 ---- class QuantumCircuit: def __init__(self, n_qubits): self.n = n_qubits self.state = np.zeros(2 ** n_qubits, dtype=complex) self.state[0] = 1 def h(self, target): self.state = apply_single_qubit_gate(self.state, H, target, self.n) return self def cx(self, ctrl, targ): self.state = cnot_matrix(ctrl, targ, self.n) @ self.state return self def measure(self, shots=1024): probs = np.abs(self.state) ** 2 probs = probs / np.sum(probs) return np.random.choice(2 ** self.n, size=shots, p=probs) # ---- 构建Bell态 ---- qc = QuantumCircuit(2) qc.h(0) qc.cx(0, 1) outcomes = qc.measure(shots=2048) # ---- 统计结果 ---- counts = Counter(outcomes) for key in sorted(counts.keys()): binary = format(key, '02b') # 大端序下,下标0对应|00> print(f"|{binary}>: {counts[key]}")这段代码完全可以用在课堂演示里。把它保存成bell.py,在终端执行,你会看到类似下面的输出:
|00>: 1041 |11>: 100701和10完全消失了。这就是纠缠的直接证据:两个量子比特的测量结果要么都是0,要么都是1,永远不会出现一个0一个1的独立情况。
6.2 输出结果解读
为什么结果不是严格的各50%?因为2048次采样是有限样本,会有统计涨落。你可以试着把shots调到100000,会发现00和11的比例越来越接近50:50,这正是大数定律在起作用。
有些读者可能会疑惑:这个输出看起来就像两个经典比特被初始化为相等的0或1,然后同时输出,有什么神奇的呢?神奇之处在于,在测量之前,这个量子系统处于(|00⟩ + |11⟩)/√2的叠加态,它不是“有50%概率是00、50%概率是11”的经典混合态。虽然单次测量的统计结果相同,但如果你在这个Bell态上再做某些干涉操作,比如再给第0号比特施加一次H门,经典混合态和量子纠缠态会给出完全不同的结果。这个实验以后在中篇里我会仔细讲,这里先留个引子。
6.3 常见问题排查表
写模拟器的过程中,我积累了一些高频问题。按我的经验,90%的错误都出在下面几个地方:
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 测量概率和不为1 | 门矩阵不幺正,或复数输入错 | 检查矩阵元素,用U†U=I做验证 |
| 测量结果全是00 | 忘了加H门,或CNOT控制目标配反 | 检查电路门顺序和比特索引 |
| 结果出现01和10 | CNOT方向配反,或索引顺序理解错误 | 先手写一个2比特基态映射,再对照矩阵 |
| 矩阵维度不匹配 | 张量积kron的顺序或次数不对 | 打印每一步的中时期望矩阵尺寸 |
| H门两次后不还原 | 浮点误差 | 用np.allclose判断,别用== |
这里特别提一下,量子比特顺序约定不同会导致完全不同的结果。Qiskit默认使用小端序,而DREAMVFIA使用大端序。如果你在两个框架之间切换,务必要先搞清楚下标和比特的映射关系,这是新手最容易掉进去的坑。
6.4 下篇预告
到这,上篇的骨架已经搭起来了:状态向量、单比特门、CNOT门、测量,这些东西已经能支撑你去捣鼓很多有趣的实验了。但模拟器还缺很多实用功能:可视化的布洛赫球、更多受控门(比如受控Z门、受控相位门),以及直接在模拟器上跑通一个完整算法。
下篇我会把重点放在算法层面:在DREAMVFIA上实现Deutsch-Jozsa算法和Grover搜索算法,这两者会彻底改变你对量子计算“不过是概率模型”的错觉——干涉效应会让你看到叠加态的真正威力。我会继续本着“一切从零手写”的原则,连Oracle电路都用基础门搭出来。到时候这套模拟器就不再只是玩具,而是一个能真正帮你理解量子算法精髓的教学工具。
我自己在实际做这个项目的过程中,最大的体会是:写量子模拟器这件事本身比结果更有价值。每实现一个门、每跑通一个电路,你对“叠加”“纠缠”“干涉”的理解就会深一层。建议你下一步也动手把代码敲一遍,尤其是那个用于构造CNOT的循环,一句话一句话地读,用笔在纸上画两遍how the bits mapping works。等这些基础都扎实了,下篇的算法解读你看起来会轻松很多。