Adding gate noise¶

In this notebook, we show how an example of noise model where the gates are perfect, but are followed by a user-defined noisy channel.

Simulation with depolarizing gate noise¶

Here, the noisy channel will be a depolarizing channel (whether one-qbit or two-qbit).

In [1]:
import numpy as np
from itertools import product
from qat.lang.AQASM import Program, H, PH, CNOT, SWAP, RX
from qat.hardware.default import DefaultGatesSpecification, HardwareModel
from qat.quops import QuantumChannelKraus

prog = Program()
reg = prog.qalloc(2)
prog.apply(H, reg[0])
prog.apply(CNOT, reg)
prog.apply(RX(0.3), reg[0])
prog.apply(RX(0.5), reg[1])
circ = prog.to_circ()

circ.display()

gates_spec = DefaultGatesSpecification()

# instantiating a noise model representing a depolarizing noise
# with probability 5%
prob = 0.05
Xmat = np.array([[0, 1], [1, 0]])
Ymat = np.array([[0, -1j], [1j, 0]])
Zmat = np.array([[1, 0], [0, -1]])
kraus_ops = [np.sqrt(1-prob)*np.identity(2),
             np.sqrt(prob/3)*Xmat, np.sqrt(prob/3)*Ymat, np.sqrt(prob/3)*Zmat]
noise = QuantumChannelKraus(kraus_ops)

# two-qbit version
noise2 = QuantumChannelKraus([np.kron(K1, K2) for K1, K2 in product(noise.kraus_operators, 
                                                                    noise.kraus_operators)])
# for each gate, we specify the noise
# note that the values in this dictionary are lambda functions
# with as many arguments as the gate's number of arguments
gates_noise = {"H": lambda: noise,
               "CNOT": lambda: noise2,
               "RX": lambda _: noise}

hw_model = HardwareModel(gates_spec, gates_noise, idle_noise=None)
No description has been provided for this image

Noisy simulation¶

Let us now run the circuit with a noisy simulator (QPU):

In [2]:
from qat.qpus import NoisyQProc
qpu = NoisyQProc(hardware_model=hw_model)

results = qpu.submit(circ.to_job())
for sample in results:
    print(sample.state, sample.probability)
|00> 0.38396314421341365
|01> 0.11603685578658596
|10> 0.11603685578658596
|11> 0.38396314421341365

Perfect simulation¶

Let us compare to a perfect simulation:

In [3]:
from qat.qpus import MPS

qpu_mps = MPS()
results = qpu_mps.submit(circ.to_job())
for sample in results:
    print(sample.state, sample.probability)
|00> 0.4241766773367914
|01> 0.07582332266320863
|10> 0.0758233226632086
|11> 0.4241766773367911
/var/lib/jenkins/workspace/notebooks_release_qaptiva-1.13.1/venvs/notebook314/lib/python3.14/site-packages/IPython/core/interactiveshell.py:3701: UserWarning: No maximum bond dimension or fidelity has been set. The computation will be exact, but defeats the purpose of using MPS. Please input a bond dimension or a fidelity to ensure truncations.
  exec(code_obj, self.user_global_ns, self.user_ns)

Using predefined quantum channels¶

Instead of the custom depolarizing channel defined above, you may want to use predefined quantum channels such as amplitude damping (AD) or pure dephasing (PD):

In [4]:
from qat.quops import ParametricAmplitudeDamping, ParametricPureDephasing
from qat.quops import QuantumChannelKraus

T1, T2 = 44000, 38000 #nanosecs

#Amplitude Damping
amp_damp = ParametricAmplitudeDamping(T_1=T1)

#Pure Dephasing 
pure_deph = ParametricPureDephasing(T_phi=1/(1/T2 - 1/(2*T1)))

# for the CNOT gate (whose duration is assumed to be 200 ns)
# we choose a noise model with AD on the first (control) qubit,
# and PD on the second (target) qubit
two_qb_noise = QuantumChannelKraus([np.kron(k1, k2)
                                    for k1, k2 in product(amp_damp(tau=200).kraus_operators,
                                                          pure_deph(tau=200).kraus_operators)])

# we assume that the gate is followed by AD and/or PD noise equivalent to its duration
# i.e 40 ns for the Hadamard gate, 5.0 * theta ns for the RX(theta) gate
gates_noise_predef = {"H": lambda : amp_damp(tau=40) * pure_deph(tau=40), #H will be followed by AD+PD
                      "CNOT": lambda: two_qb_noise,
                      "RX": lambda theta: amp_damp(tau=5.0*theta) # RX will be followed by AD only
                      }

hw_model = HardwareModel(gates_spec, gates_noise_predef, idle_noise=None)
qpu_predef = NoisyQProc(hardware_model=hw_model)

results = qpu_predef.submit(circ.to_job())
for sample in results:
    print(sample.state, sample.probability)
|00> 0.4249555209792799
|01> 0.0776598058510446
|10> 0.07547158455138861
|11> 0.4219130886182865

Qubit-dependent gate noise¶

One can also specify gate noise on a qubit-specific level. Similarly as for idle noise, one just has to give a dictionary whose keys are the qubit indices and values the corresponding noise channels. In the case of gates acting on several qubits, the indices must be given in a tuple and the order matters (a CNOT from qubits 0 to 1 can have different noise than a CNOT from 1 to 0)

In [5]:
prob_qb2 = 0.10
kraus_ops_qb2 = [np.sqrt(1-prob_qb2)*np.identity(2),
                 np.sqrt(prob_qb2/3)*Xmat,
                 np.sqrt(prob_qb2/3)*Ymat,
                 np.sqrt(prob_qb2/3)*Zmat]
noise_qb2 = QuantumChannelKraus(kraus_ops_qb2)

# for each gate, we specify the noise
# note that the values in this dictionary are lambda functions
# with as many arguments as the gate's number of arguments
gates_noise_2 = {"H": lambda: noise,
                 "CNOT": lambda: noise2,
                 "RX": {0: (lambda _: noise), 1: (lambda _: noise_qb2)}}

hw_model_2 = HardwareModel(gates_spec, gates_noise_2, idle_noise=None)
In [6]:
qpu_2 = NoisyQProc(hardware_model=hw_model_2)

results = qpu_2.submit(circ.to_job())
for sample in results:
    print(sample.state, sample.probability)
|00> 0.3743943481981697
|01> 0.12560565180182978
|10> 0.12560565180182978
|11> 0.3743943481981697
In [ ]: