Noisy Simulation

Quantum noise can be characterized 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 trace preserving maps. These maps are called Kraus operators, \(\{ K_i \}\), which satisfy the condition \(\sum_{i} K_i^\dagger K_i = \mathbb{I}\).

The bit-flip channel 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}\]

Let’s implement the bit-flip channel using CUDA-Q:

[1]:
import cudaq
from cudaq import spin

import numpy as np

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


@cudaq.kernel
def kernel(qubit_count: int):
    qvector = cudaq.qvector(qubit_count)
    x(qvector)


print(cudaq.draw(kernel, qubit_count))
     ╭───╮
q0 : ┤ x ├
     ├───┤
q1 : ┤ x ├
     ╰───╯

[3]:
# In the ideal noiseless case, we get |11> 100% of the time.

ideal_counts = cudaq.sample(kernel, qubit_count, shots_count=1000)
ideal_counts.dump()
{ 11:1000 }
[4]:
# 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)

# Other built in noise models
bit_flip = cudaq.BitFlipChannel(error_probability)
phase_flip = cudaq.PhaseFlipChannel(error_probability)
amplitude_damping = cudaq.AmplitudeDampingChannel(error_probability)

# We can also define our own, custom noise channels through
# Kraus operators. Here we will define two operators representing
# 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 noise 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,
                            qubit_count,
                            noise_model=noise_model,
                            shots_count=1000)
noisy_counts.dump()
{ 11:844 10:88 01:65 00:3 }
[5]:
# We can also use noise models with the observe function

hamiltonian = spin.z(0)

noisy_result = cudaq.observe(kernel,
                             hamiltonian,
                             qubit_count,
                             noise_model=noise_model)

noisy_result.expectation()
[5]:
-0.8666666666666666