Qibo 可以通过两种不同的方法执行含噪模拟:一种是多次重复电路执行并以概率方式应用噪声门,另一种是使用密度矩阵并应用噪声信道。 此外,Qibo 还提供了在模拟完成后向测量结果添加比特翻转错误的功能。这部分内容将在 “测量误差” 中进行分析。
使用密度矩阵¶
如果使用density_matrix=True标志初始化,Qibo电路可以演化密度矩阵,例如:
import qibo
qibo.set_backend("qibojit")
from qibo import Circuit, gates
# Define circuit
circuit = Circuit(2, density_matrix=True)
circuit.add(gates.H(0))
circuit.add(gates.H(1))
# execute using the default initial state |00><00|
result = circuit() # will be |++><++|
[Qibo 0.2.21|INFO|2025-10-15 15:07:09]: Using qibojit (numba) backend on /CPU:0
与状态向量电路模拟类似,用户可以在执行电路时通过传入相应的数组来指定自定义的初始密度矩阵。如果在密度矩阵电路中传入状态向量作为初始状态,它会自动转换为等效的密度矩阵。
此外,Qibo提供了几种表示信道的门,可在密度矩阵模拟过程中使用。可以使用这些信道来模拟噪声,例如
from qibo import Circuit, gates
circuit = Circuit(2, density_matrix=True) # starts with state |00><00|
circuit.add(gates.X(1))
# transforms |00><00| -> |01><01|
circuit.add(gates.PauliNoiseChannel(0, [("X", 0.3)]))
# transforms |01><01| -> (1 - px)|01><01| + px |11><11|
result = circuit()
result.state()
array([[0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j],
[0. +0.j, 0.7+0.j, 0. +0.j, 0. +0.j],
[0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j],
[0. +0.j, 0. +0.j, 0. +0.j, 0.3+0.j]])
使用重复执行¶
使用密度矩阵模拟噪声会消耗大量内存,因为它实际上将量子比特的数量翻倍。Qibo提供了一种模拟通道效应的替代方法,无需使用密度矩阵,它依赖于状态向量和重复电路执行以及采样。可以通过创建一个普通(非密度矩阵)电路并重复执行它来模拟噪声:
import numpy as np
from qibo import Circuit, gates
nqubits = 5
nshots = 1000
# Define circuit
circuit = Circuit(nqubits)
thetas = np.random.random(nqubits)
circuit.add(gates.RX(qubit, theta=phase) for qubit, phase in enumerate(thetas))
# Add noise channels to all qubits
circuit.add(
gates.PauliNoiseChannel(qubit, [("X", 0.2), ("Y", 0.0), ("Z", 0.3)])
for qubit in range(nqubits)
)
# Add measurement of all qubits
circuit.add(gates.M(*range(5)))
# Repeat execution 1000 times
result = circuit(nshots=nshots)
在这个例子中,模拟重复进行了1000次,并且qibo.gates.PauliNoiseChannel门的行为每次都不同,因为错误X、Y和Z门是按照给定的概率进行采样的。请注意,当使用通道时,c(nshots=1000)命令的行为与在《如何进行测量?》中描述的不同。通常,c(nshots=1000)会执行一次电路,然后从最终状态中采样1000个比特串。当使用通道时,因为通道的行为是概率性的,并且每次执行都不同,所以整个电路会执行1000次。请注意,现在所需的模拟时间将随着重复次数(nshots)的增加而线性增加。
请注意,仅执行一次带有通道的电路是可能的,但由于通道是概率性的,单次执行的结果是随机的,通常没有太大用处。在没有测量存在的情况下,也可以使用带有噪声通道的重复执行。如果对一个包含通道但没有测量的电路调用c(nshots=1000),则电路将执行1000次,并将最后的1000个状态向量作为形状为(nshots, 2 ^ nqubits)的张量返回。请注意,这个张量通常很大,可能会导致内存错误,因此不建议使用这种用法。
与密度矩阵方法不同,并不是每个通道都可以用于采样和重复执行。具体来说,qibo.gates.UnitaryChannel和qibo.gates.PauliNoiseChannel可以用于采样,而qibo.gates.KrausChannel则需要密度矩阵。
result.frequencies()
Counter({'00000': 260,
'00010': 115,
'00100': 80,
'10000': 67,
'00001': 64,
'01000': 59,
'00110': 38,
'10010': 35,
'01010': 31,
'00011': 27,
'01100': 24,
'00101': 20,
'10100': 19,
'10001': 18,
'01001': 17,
'11000': 17,
'11010': 14,
'00111': 12,
'01110': 12,
'10011': 12,
'10110': 9,
'01011': 8,
'01101': 8,
'10101': 8,
'01111': 5,
'10111': 5,
'11001': 4,
'11100': 4,
'11101': 4,
'11111': 3,
'11110': 1})
在每个门之后添加噪声¶
在实际应用中,噪声通常会在每个门之后出现。Qibo 提供了 qibo.models.circuit.Circuit.with_pauli_noise() 方法,该方法会自动创建一个新的电路,在每个门之后都包含一个 qibo.gates.PauliNoiseChannel。用户可以通过噪声图来控制噪声通道的概率,噪声图是一个将量子比特映射到相应概率三元组的字典。例如,下面的脚本
from qibo import Circuit, gates
# 创建一个包含2个量子比特的量子电路
circuit = Circuit(2)
# 在电路中添加量子门:对第0个量子比特应用Hadamard门,对第1个量子比特应用Hadamard门,然后应用CNOT门(控制比特为0,目标比特为1)
circuit.add([gates.H(0), gates.H(1), gates.CNOT(0, 1)])
# 定义一个噪声图,用于将量子比特ID映射到噪声概率
noise_map = {0: list(zip(["X", "Z"], [0.1, 0.2])), 1: list(zip(["Y", "Z"], [0.2, 0.1]))}
noisy_circuit = circuit.with_pauli_noise(noise_map)
print(noise_map)
{0: [('X', 0.1), ('Z', 0.2)], 1: [('Y', 0.2), ('Z', 0.1)]}
将创建一个新的电路noisy_circuit,它等价于:
noisy_circuit_2 = Circuit(2)
noisy_circuit_2.add(gates.H(0))
noisy_circuit_2.add(gates.PauliNoiseChannel(0, [("X", 0.1), ("Y", 0.0), ("Z", 0.2)]))
noisy_circuit_2.add(gates.H(1))
noisy_circuit_2.add(gates.PauliNoiseChannel(1, [("X", 0.0), ("Y", 0.2), ("Z", 0.1)]))
noisy_circuit_2.add(gates.CNOT(0, 1))
noisy_circuit_2.add(gates.PauliNoiseChannel(0, [("X", 0.1), ("Y", 0.0), ("Z", 0.2)]))
noisy_circuit_2.add(gates.PauliNoiseChannel(1, [("X", 0.0), ("Y", 0.2), ("Z", 0.1)]))
请注意,noisy_circuit使用的是原始电路circuit的门对象(并非深拷贝),而在noisy_circuit_2中,每个门都是作为新对象创建的。
用户可以使用单个元组代替字典作为噪声图。在这种情况下,相同的概率将应用于所有量子比特。也就是说,noise_map = list(zip(["X", "Z"], [0.1, 0.1])) 等价于 noise_map = {0: list(zip(["X", "Z"], [0.1, 0.1])), 1: list(zip(["X", "Z"], [0.1, 0.1])), ...}。
如前几节所述,如果在使用状态向量的电路中使用qibo.models.circuit.Circuit.with_pauli_noise(),则噪声将通过重复执行来模拟。如果用户希望改用密度矩阵,可在电路初始化期间传递density_matrix=True标志,并在新电路上调用.with_pauli_noise来实现。
使用噪声模型¶
在实际的量子电路中,一些门可能会出现严重故障并引入错误。为了模拟这种行为,Qibo 提供了 qibo.noise.NoiseModel 类,该类可以通过 qibo.noise.NoiseModel.add() 方法存储与门相关的错误,并通过 qibo.noise.NoiseModel.apply() 生成相应的含噪声电路。相应的噪声会在电路中每个门实例之后应用。也可以指定在哪些量子比特上添加噪声。
目前可用于构建自定义噪声模型的量子错误包括:qibo.noise.PauliError、qibo.noise.ThermalRelaxationError 和 qibo.noise.ResetError。
以下是使用噪声模型的示例:
import numpy as np
from qibo import Circuit, gates
from qibo.noise import NoiseModel, PauliError
# 创建一个噪声模型对象,用于模拟量子计算中的噪声效应
noise = NoiseModel()
# 向噪声模型中添加第一种错误:
# 在H门作用在第1个量子比特上时,有50%的概率发生X错误
# PauliError([("X", 0.5)])表示以50%的概率施加泡利X门(比特翻转)
noise.add(PauliError([("X", 0.5)]), gates.H, 1)
# 向噪声模型中添加第二种错误:
# 在任何CNOT门作用时,有50%的概率发生Y错误
# PauliError([("Y", 0.5)])表示以50%的概率施加泡利Y门
noise.add(PauliError([("Y", 0.5)]), gates.CNOT)
# 定义一个判断条件函数:检查RX门的旋转角度是否为π/2
# 这个函数将用于筛选特定参数的量子门
is_sqrt_x = (lambda g: np.pi / 2 in g.parameters)
# 向噪声模型中添加第三种错误:
# 当RX门作用在第0个量子比特上且旋转角度为π/2时,有50%的概率发生X错误
# conditions参数指定了额外的条件筛选
noise.add(PauliError([("X", 0.5)]), gates.RX, qubits=0, conditions=is_sqrt_x)
# 创建一个包含2个量子比特的无噪声量子电路
circuit = Circuit(2)
# 向电路中添加一系列量子门操作:
# 1. 对第0个量子比特应用Hadamard门
# 2. 对第1个量子比特应用Hadamard门
# 3. 对(0,1)量子比特对应用CNOT门
# 4. 对第0个量子比特应用RX旋转门,旋转角度为π/2
# 5. 对第0个量子比特应用RX旋转门,旋转角度为3π/2
# 6. 对第1个量子比特应用RX旋转门,旋转角度为π/2
circuit.add(
[
gates.H(0),
gates.H(1),
gates.CNOT(0, 1),
gates.RX(0, np.pi / 2),
gates.RX(0, 3 * np.pi / 2),
gates.RX(1, np.pi / 2),
]
)
# 根据之前定义的噪声模型,将噪声应用到原始电路上
# 这会生成一个新的带噪声的电路,其中包含了我们之前定义的所有噪声效应
noisy_circuit = noise.apply(circuit)
等效于
noisy_circuit_2 = Circuit(2,density_matrix=True)
noisy_circuit_2.add(gates.H(0))
noisy_circuit_2.add(gates.H(1))
noisy_circuit_2.add(gates.PauliNoiseChannel(1, [("X", 0.5)]))
noisy_circuit_2.add(gates.CNOT(0, 1))
noisy_circuit_2.add(gates.PauliNoiseChannel(0, [("Y", 0.5)]))
noisy_circuit_2.add(gates.PauliNoiseChannel(1, [("Y", 0.5)]))
noisy_circuit_2.add(gates.RX(0, np.pi / 2))
noisy_circuit_2.add(gates.PauliNoiseChannel(0, [("X", 0.5)]))
noisy_circuit_2.add(gates.RX(0, 3 * np.pi / 2))
noisy_circuit_2.add(gates.RX(1, np.pi / 2))
noisy_circuit_2().state()
array([[0.25+0.j, 0. +0.j, 0. +0.j, 0. +0.j],
[0. +0.j, 0.25+0.j, 0. +0.j, 0. +0.j],
[0. +0.j, 0. +0.j, 0.25+0.j, 0. +0.j],
[0. +0.j, 0. +0.j, 0. +0.j, 0.25+0.j]])
qibo.noise.NoiseModel类也支持密度矩阵,只需传递一个以density_matrix=True初始化的电路即可。
qibo.measurements.CircuitResult提供了qibo.measurements.CircuitResult.apply_bitflips()方法,该方法允许在无需重新执行模拟的情况下,向采样的比特串添加比特翻转错误。例如:
import numpy as np # 导入NumPy库,用于数值计算
from qibo import Circuit, gates # 从qibo库导入Circuit和gates模块,用于量子电路的构建和操作
thetas = np.random.random(4) # 生成4个随机数,作为旋转门的角度参数
circuit = Circuit(4) # 创建一个包含4个量子比特的量子电路
circuit.add(gates.RX(i, theta=t) for i, t in enumerate(thetas)) # 为每个量子比特添加RX旋转门
circuit.add((gates.M(0, 1), gates.M(2, 3))) # 添加测量门,对量子比特0,1和2,3进行测量
result = circuit(nshots=100) # 执行电路,进行100次测量,获取结果
result.apply_bitflips(0.2) # 为所有量子比特添加比特翻转错误,错误概率为0.2
error_map = {0: 0.2, 1: 0.1, 2: 0.3, 3: 0.1} # 定义每个量子比特的比特翻转错误概率
result.apply_bitflips(error_map)
array([[0, 0, 0, 0],
[1, 1, 0, 0],
[0, 0, 0, 1],
[0, 0, 0, 1],
[0, 0, 0, 0],
[0, 1, 0, 0],
[1, 0, 0, 0],
[0, 0, 1, 0],
[1, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[1, 0, 0, 0],
[0, 0, 0, 0],
[1, 1, 0, 1],
[0, 0, 0, 1],
[0, 0, 0, 0],
[0, 0, 1, 0],
[0, 1, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 1],
[0, 0, 1, 1],
[0, 0, 0, 0],
[0, 1, 1, 0],
[0, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 0],
[1, 1, 1, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 1, 1, 0],
[1, 0, 1, 0],
[0, 0, 1, 0],
[1, 0, 0, 0],
[0, 0, 0, 0],
[1, 0, 1, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 0],
[0, 0, 1, 0],
[1, 0, 1, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 1],
[1, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 1],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 1],
[0, 0, 0, 1],
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 0, 0, 0],
[0, 1, 1, 0],
[0, 0, 0, 1],
[1, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 1],
[1, 0, 1, 0],
[0, 0, 0, 0],
[0, 0, 0, 1],
[0, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 0, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 0, 0, 0],
[0, 0, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 0],
[0, 1, 0, 0],
[1, 0, 0, 1],
[1, 0, 1, 0],
[1, 0, 1, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 1, 0],
[1, 1, 0, 1],
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 0, 1, 0],
[1, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]])
然后,相应的含噪声样本和频率可以按照“如何执行测量?”示例中的描述获得。
请注意,qibo.measurements.CircuitResult.apply_bitflips() 会修改相应状态中包含的测量样本,因此原始的无噪声测量样本将不再可访问。可以通过在应用位翻转之前创建状态的副本来保留原始样本:
import numpy as np
from qibo import Circuit, gates
thetas = np.random.random(6)
circuit = Circuit(6)
circuit.add(gates.RX(qubit, theta=phase) for qubit, phase in enumerate(thetas))
circuit.add(gates.M(0, 1, p0=0.2))
circuit.add(gates.M(2, 3, p0={2: 0.1, 3: 0.0}))
circuit.add(gates.M(4, 5, p0=[0.4, 0.3]))
result = circuit(nshots=100)
result.frequencies()
Counter({'000010': 17,
'000000': 14,
'010000': 12,
'000001': 10,
'010010': 4,
'100000': 4,
'100011': 4,
'000100': 3,
'010001': 3,
'100001': 3,
'000011': 2,
'000110': 2,
'000111': 2,
'001100': 2,
'010011': 2,
'100010': 2,
'101010': 2,
'110000': 2,
'001000': 1,
'001010': 1,
'010101': 1,
'010110': 1,
'011000': 1,
'011001': 1,
'100111': 1,
'101000': 1,
'110010': 1,
'111000': 1})
在这种情况下,结果将包含根据给定的翻转概率产生的噪声样本。概率可以以字典的形式给出(必须包含所有测量的量子位作为键),一个列表(必须包含测量的量子位作为样本)或一个单独的浮点数(将用于所有测量的量子位)。请注意,与之前的代码示例不同,当翻转错误作为测量门的一部分被整合时,无法访问无噪声样本。
此外,可以使用p1参数来模拟非对称的翻转,例如使用result.apply_bitflips(p0=0.2, p1=0.1)。在这种情况下,0->1错误的概率将使用0.2,而1->0错误的概率将使用0.1。与p0类似,p1可以是一个单独的浮点数或一个字典,并且可以在qibo.measurements.CircuitResult.apply_bitflips()和测量门中使用。如果未指定p1,则将使用p0的值来处理两种错误。
在这种情况下,结果将包含根据给定的翻转概率产生的噪声样本。概率可以以字典的形式给出(必须包含所有测量的量子位作为键),一个列表(必须包含测量的量子位作为样本)或一个单独的浮点数(将用于所有测量的量子位)。请注意,与之前的代码示例不同,当翻转错误作为测量门的一部分被整合时,无法访问无噪声样本。
此外,可以使用p1参数来模拟非对称的翻转,例如使用result.apply_bitflips(p0=0.2, p1=0.1)。在这种情况下,0->1错误的概率将使用0.2,而1->0错误的概率将使用0.1。与p0类似,p1可以是一个单独的浮点数或一个字典,并且可以在qibo.measurements.CircuitResult.apply_bitflips()和测量门中使用。如果未指定p1,则将使用p0的值来处理两种错误。
# 导入必要的库
from qibo import Circuit, gates # 导入Qibo库中的Circuit和gates模块
from qibo.noise import IBMQNoiseModel # 导入IBM量子噪声模型
# 定义量子比特数量
nqubits = 2
# 创建一个密度矩阵量子电路
circuit = Circuit(2, density_matrix=True)
# 向电路中添加量子门
circuit.add(
[
gates.H(0), # 在量子比特0上添加Hadamard门
gates.X(1), # 在量子比特1上添加Pauli-X门
gates.Z(0), # 在量子比特0上添加Pauli-Z门
gates.X(0), # 在量子比特0上添加Pauli-X门
gates.CNOT(0,1), # 在量子比特0和1之间添加CNOT门,控制位为0,目标位为1
gates.CNOT(1, 0), # 在量子比特1和0之间添加CNOT门,控制位为1,目标位为0
gates.X(1), # 在量子比特1上添加Pauli-X门
gates.Z(1), # 在量子比特1上添加Pauli-Z门
gates.M(0), # 测量量子比特0
gates.M(1), # 测量量子比特1
]
)
# 打印原始电路
print("raw circuit:")
circuit.draw()
# 定义噪声参数
parameters = {
"t1": {"0": 250*1e-06, "1": 240*1e-06}, # T1弛豫时间,单位秒
"t2": {"0": 150*1e-06, "1": 160*1e-06}, # T2相干时间,单位秒
"gate_times" : (200*1e-9, 400*1e-9), # 门操作时间,单位秒
"excited_population" : 0, # 激发态布居数
"depolarizing_one_qubit" : 4.000e-4, # 单量子比特去极化误差
"depolarizing_two_qubit": 1.500e-4, # 双量子比特去极化误差
"readout_one_qubit" : {"0": (0.022, 0.034), "1": (0.015, 0.041)}, # 单量子比特测量误差
}
# 创建IBM噪声模型
noise_model = IBMQNoiseModel()
# 从字典加载噪声参数
noise_model.from_dict(parameters)
# 将噪声模型应用到电路
noisy_circuit = noise_model.apply(circuit)
# 打印带噪声的电路
print("noisy circuit:")
noisy_circuit.draw()
raw circuit: 0: ─H─Z─X─o─X─M───── 1: ─X─────X─o─X─Z─M─ noisy circuit: 0: ─H─D─TR─Z─D─TR─X─D─TR─o─D─TR─X─D─TR─RE─M──────────────── 1: ─X─D─TR───────────────X─D─TR─o─D─TR─X──D─TR─Z─D─TR─RE─M─