Noisy Simulation

Quantum noise can be characterised into coherent and incoherent sources of errors that arise during a computation. Coherent noise is commonly due to systematic errors originating from device miscalibrations, for example, gates implementing a rotation \(\theta + \epsilon\) instead of \(\theta\).

Incoherent noise has its origins in quantum states being entangled with the environment due to decoherence. This leads to mixed states which are probability distributions over pure states and are described by employing the density matrix formalism.

We can model incoherent noise via quantum channels which are linear, completely positive, and trave preserving maps. The mathematical language used is of Kraus operators, \(\{ K_i \}\), which satisfy the condition \(\sum_{i} K_i^\dagger K_i = \mathbb{I}\).

The bit-flip operation flips the qubit with probability \(p\) and leaves it unchanged with probability \(1-p\). This can be represented by employing Kraus operators:

\[\begin{split}K_0 = \sqrt{1-p} \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}\end{split}\]
\[\begin{split}K_1 = \sqrt{p} \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}\end{split}\]

Lets implement the bit-flip channel using CUDA Quantum:

[2]:
import cudaq
import numpy as np

# To model quantum noise, we need to utilise the density matrix simulator target.
cudaq.set_target("density-matrix-cpu")
[5]:
# Let's define a simple kernel that we will add noise to.
qubit_count = 2

kernel = cudaq.make_kernel()
qubits = kernel.qalloc(qubit_count)

kernel.x(qubits[0])
kernel.x(qubits[1])
[3]:
# In the ideal noiseless case, we get |11> 100% of the time.

ideal_counts = cudaq.sample(kernel, shots_count=1000)
ideal_counts.dump()
{ 11:1000 }
[6]:
# First, we will define an out of the box noise channel. In this case,
# we choose depolarization noise. This depolarization will result in
# the qubit state decaying into a mix of the basis states, |0> and |1>,
# with our provided probability.
error_probability = 0.1
depolarization_channel = cudaq.DepolarizationChannel(error_probability)

# We can also define our own, custom noise channels through
# Kraus Operator's. Here we will define two operators repsenting
# bit flip errors.

# Define the Kraus Error Operator as a complex ndarray.
kraus_0 = np.sqrt(1 - error_probability) * np.array([[1.0, 0.0], [0.0, 1.0]],
                                                    dtype=np.complex128)
kraus_1 = np.sqrt(error_probability) * np.array([[0.0, 1.0], [1.0, 0.0]],
                                                dtype=np.complex128)

# Add the Kraus Operator to create a quantum channel.
bitflip_channel = cudaq.KrausChannel([kraus_0, kraus_1])

# Add the two channels to our Noise Model.
noise_model = cudaq.NoiseModel()

# Apply the depolarization channel to any X-gate on the 0th qubit.
noise_model.add_channel("x", [0], depolarization_channel)
# Apply the bitflip channel to any X-gate on the 1st qubit.
noise_model.add_channel("x", [1], bitflip_channel)

# Due to the impact of noise, our measurements will no longer be uniformly
# in the |11> state.
noisy_counts = cudaq.sample(kernel, noise_model=noise_model, shots_count=1000)
noisy_counts.dump()
{ 11:836 10:93 01:66 00:5 }