Noisy circuits

In this demonstration, you’ll learn how to simulate noisy circuits using built-in functionality in PennyLane. We’ll cover the basics of noisy channels and density matrices, then use example code to simulate noisy circuits. PennyLane, the library for differentiable quantum computations, has unique features that enable us to compute gradients of noisy channels. We’ll also explore how to employ channel gradients to optimize noise parameters in a circuit.

We’re putting the N in NISQ.

../_images/N-Nisq.png

Noisy operations

Noise is any unwanted transformation that corrupts the intended output of a quantum computation. It can be separated into two categories.

  • Coherent noise is described by unitary operations that maintain the purity of the output quantum state. A common source are systematic errors originating from imperfectly-calibrated devices that do not exactly apply the desired gates, e.g., applying a rotation by an angle \(\phi+\epsilon\) instead of \(\phi\).
  • Incoherent noise is more problematic: it originates from a quantum computer becoming entangled with the environment, resulting in mixed states — probability distributions over different pure states. Incoherent noise thus leads to outputs that are always random, regardless of what basis we measure in.

Mixed states are described by density matrices. They provide a more general method of describing quantum states that elegantly encodes a distribution over pure states in a single mathematical object. Mixed states are the most general description of a quantum state, of which pure states are a special case.

The purpose of PennyLane’s default.mixed device is to provide native support for mixed states and for simulating noisy computations. Let’s use default.mixed to simulate a simple circuit for preparing the Bell state \(|\psi\rangle=\frac{1}{\sqrt{2}}(|00\rangle+|11\rangle)\). We ask the QNode to return the expectation value of \(Z_0\otimes Z_1\):

import pennylane as qml
from pennylane import numpy as np

dev = qml.device('default.mixed', wires=2)


@qml.qnode(dev)
def circuit():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))


print(f"QNode output = {circuit():.4f}")

Out:

QNode output = 1.0000

The device stores the output state as a density matrix. In this case, the density matrix is equal to \(|\psi\rangle\langle\psi|\), where \(|\psi\rangle=\frac{1}{\sqrt{2}}(|00\rangle + |11\rangle)\).

print(f"Output state is = \n{np.real(dev.state)}")

Out:

Output state is =
[[0.5 0.  0.  0.5]
 [0.  0.  0.  0. ]
 [0.  0.  0.  0. ]
 [0.5 0.  0.  0.5]]

Incoherent noise is modelled by quantum channels. Mathematically, a quantum channel is a linear, completely positive, and trace-preserving (CPTP) map. A convenient strategy for representing quantum channels is to employ Kraus operators \(\{K_i\}\) satisfying the condition \(\sum_i K_{i}^{\dagger} K_i = I\). For an initial state \(\rho\), the output state after the action of a channel \(\Phi\) is:

\[\Phi(\rho) = \sum_i K_i \rho K_{i}^{\dagger}.\]

Just like pure states are special cases of mixed states, unitary transformations are special cases of quantum channels. Unitary transformations are represented by a single Kraus operator, the unitary \(U\), and they transform a state as \(U\rho U^\dagger\).

More generally, the action of a quantum channel can be interpreted as applying a transformation corresponding to the Kraus operator \(K_i\) with some associated probability. More precisely, the channel applies the transformation \(\frac{1}{p_i}K_i\rho K_i^\dagger\) with probability \(p_i = \text{Tr}[K_i \rho K_{i}^{ \dagger}]\). Quantum channels therefore represent a probability distribution over different possible transformations on a quantum state. For example, consider the bit flip channel. It describes a transformation that flips the state of a qubit (applies an X gate) with probability \(p\) and leaves it unchanged with probability \(1-p\). Its Kraus operators are

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

This channel can be implemented in PennyLane using the qml.BitFlip operation.

Let’s see what happens when we simulate this type of noise acting on both qubits in the circuit. We’ll evaluate the QNode for different bit flip probabilities.

@qml.qnode(dev)
def bitflip_circuit(p):
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    qml.BitFlip(p, wires=0)
    qml.BitFlip(p, wires=1)
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))


ps = [0.001, 0.01, 0.1, 0.2]
for p in ps:
    print(f"QNode output for bit flip probability {p} is {bitflip_circuit(p):.4f}")

Out:

QNode output for bit flip probability 0.001 is 0.9960
QNode output for bit flip probability 0.01 is 0.9604
QNode output for bit flip probability 0.1 is 0.6400
QNode output for bit flip probability 0.2 is 0.3600

The circuit behaves quite differently in the presence of noise! This will be familiar to anyone that has run an algorithm on quantum hardware. It is also highlights why error mitigation and error correction are so important. We can use PennyLane to look under the hood and see the output state of the circuit for the largest noise parameter

print(f"Output state for bit flip probability {p} is \n{np.real(dev.state)}")

Out:

Output state for bit flip probability 0.2 is
[[0.34 0.   0.   0.34]
 [0.   0.16 0.16 0.  ]
 [0.   0.16 0.16 0.  ]
 [0.34 0.   0.   0.34]]

Besides the bit flip channel, PennyLane supports several other noisy channels that are commonly used to describe experimental imperfections: PhaseFlip, AmplitudeDamping, GeneralizedAmplitudeDamping, PhaseDamping, and the DepolarizingChannel. You can also build your own custom channel using the operation QubitChannel by specifying its Kraus operators, or even submit a pull request introducing a new channel.

Let’s take a look at another example. The depolarizing channel is a generalization of the bit flip and phase flip channels, where each of the three possible Pauli errors can be applied to a single qubit. Its Kraus operators are given by

\[\begin{split}K_0 &= \sqrt{1-p}\begin{pmatrix}1 & 0\\ 0 & 1\end{pmatrix}, \\ K_1 &= \sqrt{p/3}\begin{pmatrix}0 & 1\\ 1 & 0\end{pmatrix}, \\ K_2 &= \sqrt{p/3}\begin{pmatrix}0 & -i\\ i & 0\end{pmatrix}, \\ K_3 &= \sqrt{p/3}\begin{pmatrix}1 & 0\\ 0 & -1\end{pmatrix}.\end{split}\]

A circuit modelling the effect of depolarizing noise in preparing a Bell state is implemented below.

@qml.qnode(dev)
def depolarizing_circuit(p):
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    qml.DepolarizingChannel(p, wires=0)
    qml.DepolarizingChannel(p, wires=1)
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))


ps = [0.001, 0.01, 0.1, 0.2]
for p in ps:
    print(f"QNode output for depolarizing probability {p} is {depolarizing_circuit(p):.4f}")

Out:

QNode output for depolarizing probability 0.001 is 0.9973
QNode output for depolarizing probability 0.01 is 0.9735
QNode output for depolarizing probability 0.1 is 0.7511
QNode output for depolarizing probability 0.2 is 0.5378

As before, the output deviates from the desired value as the amount of noise increases. Modelling the noise that occurs in real experiments requires careful consideration. PennyLane offers the flexibility to experiment with different combinations of noisy channels to either mimic the performance of quantum algorithms when deployed on real devices, or to explore the effect of more general quantum transformations.

Channel gradients

The ability to compute gradients of any operation is an essential ingredient of quantum differentiable programming. In PennyLane, it is possible to compute gradients of noisy channels and optimize them inside variational circuits. PennyLane supports analytical gradients for channels whose Kraus operators are proportional to unitary matrices [1]. In other cases, gradients are evaluated using finite differences.

To illustrate this property, we’ll consider an elementary example. We aim to learn the noise parameters of a circuit in order to reproduce an observed expectation value. So suppose that we run the circuit to prepare a Bell state on a hardware device and observe that the expectation value of \(Z_0\otimes Z_1\) is not equal to 1 (as would occur with an ideal device), but instead has the value 0.7781. In the experiment, it is known that the major source of noise is amplitude damping, for example as a result of photon loss. Amplitude damping projects a state to \(|0\rangle\) with probability \(p\) and otherwise leaves it unchanged. It is described by the Kraus operators

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

What damping parameter (\(p\)) explains the experimental outcome? We can answer this question by optimizing the channel parameters to reproduce the experimental observation! 💪 Since the parameter \(p\) is a probability, we use a sigmoid function to ensure that the trainable parameters give rise to a valid channel parameter, i.e., a number between 0 and 1.

ev = np.tensor([0.7781], requires_grad=False)  # observed expectation value

def sigmoid(x):
    return 1/(1+np.exp(-x))

@qml.qnode(dev)
def damping_circuit(x):
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    qml.AmplitudeDamping(sigmoid(x), wires=0)  # p = sigmoid(x)
    qml.AmplitudeDamping(sigmoid(x), wires=1)
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))

We optimize the circuit with respect to a simple cost function that attains its minimum when the output of the QNode is equal to the experimental value:

def cost(x, target):
    return (damping_circuit(x) - target[0])**2

All that remains is to optimize the parameter. We use a straightforward gradient descent method.

opt = qml.GradientDescentOptimizer(stepsize=10)
steps = 35
x = np.tensor([0.0], requires_grad=True)

for i in range(steps):
    (x, ev), cost_val = opt.step_and_cost(cost, x, ev)
    if i % 5 == 0 or i == steps - 1:
        print(f"Step: {i}    Cost: {cost_val}")

print(f"QNode output after optimization = {damping_circuit(x):.4f}")
print(f"Experimental expectation value = {ev[0]}")
print(f"Optimized noise parameter p = {sigmoid(x.take(0)):.4f}")

Out:

Step: 0    Cost: 0.07733961000000007
Step: 5    Cost: 0.07733960999999957
Step: 10    Cost: 0.0773396099969988
Step: 15    Cost: 0.07733959171203489
Step: 20    Cost: 0.07722827121891838
Step: 25    Cost: 0.0017923029380396919
Step: 30    Cost: 3.0199179590479204e-07
Step: 34    Cost: 5.228404765345524e-10
QNode output after optimization = 0.7781
Experimental expectation value = 0.7781
Optimized noise parameter p = 0.1271

Voilà! We’ve trained the noisy channel to reproduce the experimental observation. 😎

Developing quantum algorithms that leverage the power of NISQ devices requires serious consideration of the effects of noise. With PennyLane, you have access to tools that can help you design, simulate, and optimize noisy quantum circuits. We look forward to seeing what the quantum community can achieve with them! 🚀 🎉 😸

References

[1]Johannes Jakob Meyer, Johannes Borregaard, and Jens Eisert, “A variational toolbox for quantum multi-parameter estimation.” arXiv:2006.06303 (2020).

Total running time of the script: ( 0 minutes 0.144 seconds)

Gallery generated by Sphinx-Gallery