Skip to content

Noisy simulation

Digital Noise

In the description of closed quantum systems, a pure state vector is used to represent the complete quantum state. Thus, pure quantum states are represented by state vectors $|\psi \rangle $.

However, this description is not sufficient to study open quantum systems. When the system interacts with its environment, quantum systems can be in a mixed state, where quantum information is no longer entirely contained in a single state vector but is distributed probabilistically.

To address these more general cases, we consider a probabilistic combination \(p_i\) of possible pure states \(|\psi_i \rangle\). Thus, the system is described by a density matrix \(\rho\) defined as follows:

\[ \rho = \sum_i p_i |\psi_i\rangle \langle \psi_i| \]

The transformations of the density operator of an open quantum system interacting with its environment (noise) are represented by the super-operator \(S: \rho \rightarrow S(\rho)\), often referred to as a quantum channel. Quantum channels, due to the conservation of the probability distribution, must be CPTP (Completely Positive and Trace Preserving). Any CPTP super-operator can be written in the following form:

\[ S(\rho) = \sum_i K_i \rho K^{\dagger}_i \]

Where \(K_i\) are the Kraus operators, and satisfy the property \(\sum_i K_i K^{\dagger}_i = \mathbb{I}\). As noise is the result of system interactions with its environment, it is therefore possible to simulate noisy quantum circuit with noise type gates.

Thus, pyqtorch implements a large selection of single qubit noise gates such as:

  • The bit flip channel defined as: \(\textbf{BitFlip}(\rho) =(1-p) \rho + p X \rho X^{\dagger}\)
  • The phase flip channel defined as: \(\textbf{PhaseFlip}(\rho) = (1-p) \rho + p Z \rho Z^{\dagger}\)
  • The depolarizing channel defined as: \(\textbf{Depolarizing}(\rho) = (1-p) \rho + \frac{p}{3} (X \rho X^{\dagger} + Y \rho Y^{\dagger} + Z \rho Z^{\dagger})\)
  • The pauli channel defined as: \(\textbf{PauliChannel}(\rho) = (1-p_x-p_y-p_z) \rho + p_x X \rho X^{\dagger} + p_y Y \rho Y^{\dagger} + p_z Z \rho Z^{\dagger}\)
  • The amplitude damping channel defined as: \(\textbf{AmplitudeDamping}(\rho) = K_0 \rho K_0^{\dagger} + K_1 \rho K_1^{\dagger}\) with: \(\begin{equation*} K_{0} \ =\begin{pmatrix} 1 & 0\\ 0 & \sqrt{1-\ \gamma } \end{pmatrix} ,\ K_{1} \ =\begin{pmatrix} 0 & \sqrt{\ \gamma }\\ 0 & 0 \end{pmatrix} \end{equation*}\)
  • The phase damping channel defined as: \(\textbf{PhaseDamping}(\rho) = K_0 \rho K_0^{\dagger} + K_1 \rho K_1^{\dagger}\) with: \(\begin{equation*} K_{0} \ =\begin{pmatrix} 1 & 0\\ 0 & \sqrt{1-\ \gamma } \end{pmatrix}, \ K_{1} \ =\begin{pmatrix} 0 & 0\\ 0 & \sqrt{\ \gamma } \end{pmatrix} \end{equation*}\)
  • The generalize amplitude damping channel is defined as: \(\textbf{GeneralizedAmplitudeDamping}(\rho) = K_0 \rho K_0^{\dagger} + K_1 \rho K_1^{\dagger} + K_2 \rho K_2^{\dagger} + K_3 \rho K_3^{\dagger}\) with: \(\begin{cases} K_{0} \ =\sqrt{p} \ \begin{pmatrix} 1 & 0\\ 0 & \sqrt{1-\ \gamma } \end{pmatrix} ,\ K_{1} \ =\sqrt{p} \ \begin{pmatrix} 0 & 0\\ 0 & \sqrt{\ \gamma } \end{pmatrix} \\ K_{2} \ =\sqrt{1\ -p} \ \begin{pmatrix} \sqrt{1-\ \gamma } & 0\\ 0 & 1 \end{pmatrix} ,\ K_{3} \ =\sqrt{1-p} \ \begin{pmatrix} 0 & 0\\ \sqrt{\ \gamma } & 0 \end{pmatrix} \end{cases}\)

Noise gates are Primitive types, but they also request a probability argument to represent the noise affecting the system. And either a vector or a density matrix can be used as an input, but the output will always be a density matrix.

import torch
from pyqtorch.noise import AmplitudeDamping, PhaseFlip
from pyqtorch.utils import random_state

input_state = random_state(n_qubits=2)
noise_prob = 0.3
AmpD = AmplitudeDamping(0,noise_prob)
output_state = AmpD(input_state) #It's a density matrix
pf = PhaseFlip(1,0.7)
output_state = pf(output_state)

Noisy circuit initialization is the same as noiseless ones and the output will always be a density matrix. Let’s show its usage through the simulation of a realistic \(X\) gate.

We know that an \(X\) gate flips the state of the qubit, for instance \(X|0\rangle = |1\rangle\). In practice, it's common for the target qubit to stay in its original state after applying \(X\) due to the interactions between it and its environment. The possibility of failure can be represented by a BitFlip gate, which flips the state again after the application of the \(X\) gate, returning it to its original state with a probability 1 - gate_fidelity.

import matplotlib.pyplot as plt
import torch

from pyqtorch.circuit import QuantumCircuit
from pyqtorch.noise import BitFlip
from pyqtorch.primitives import X
from pyqtorch.utils import product_state


input_state = product_state('00')
x = X(0)
gate_fidelity = 0.9
bf = BitFlip(0,1.-gate_fidelity)
circ = QuantumCircuit(2,[x,bf])
output_state = circ(input_state)
output_state_diag = output_state.diagonal(dim1=0).real

plt.figure()
diag_values = output_state_diag.squeeze().numpy()
plt.bar(range(len(diag_values)), diag_values, color='blue', alpha=0.7)
custom_labels = ['00', '01', '10', '11']
plt.xticks(range(len(diag_values)), custom_labels)
plt.title("Probability of state occurrence")
plt.xlabel('Possible States')
plt.ylabel('Probability')
2024-11-20T16:13:53.502007 image/svg+xml Matplotlib v3.9.2, https://matplotlib.org/

Analog Noise

Analog noise is made possible by specifying noise_operators in HamiltonianEvolution:

import torch
from pyqtorch import uniform_state, HamiltonianEvolution
from pyqtorch.matrices import DEFAULT_MATRIX_DTYPE
from pyqtorch.noise import Depolarizing
from pyqtorch.utils import  SolverType

n_qubits = 2
qubit_targets = list(range(n_qubits))

# Random hermitian hamiltonian
matrix = torch.rand(2**n_qubits, 2**n_qubits, dtype=DEFAULT_MATRIX_DTYPE)
hermitian_matrix = matrix + matrix.T.conj()

time = torch.tensor([1.0])
time_symbol = "t"
dur_val = torch.rand(1)
list_ops = Depolarizing(0, error_probability=0.1).tensor(2)
list_ops = [op.squeeze() for op in list_ops]
solver = SolverType.DP5_ME
n_steps = 5

hamiltonian_evolution = HamiltonianEvolution(hermitian_matrix, time_symbol, qubit_targets,
        duration=dur_val, steps=n_steps,
        solver=solver, noise_operators=list_ops,)

# Starting from a uniform state
psi_start = uniform_state(n_qubits)

# Returns the evolved state
psi_end = hamiltonian_evolution(state = psi_start, values={time_symbol: time})

print(psi_end)
tensor([[[ 0.0789+0.1845j], [ 0.0216+0.5226j]], [[ 0.2567+0.5124j], [-0.0153+0.5979j]]], dtype=torch.complex128)

Readout errors

Another source of noise can be added when performing measurements. This is typically described using confusion matrices of the form:

\[ T(x|x')=\delta_{xx'} \]

where \(x\) represent a bitstring. We provide two ways to define readout errors: - ReadoutNoise : where each bit can be corrupted independently given an error probability or a 1D tensor of errors. - CorrelatedReadoutNoise : where we provide the full confusion matrix for all possible bitstrings.

import torch
import pyqtorch as pyq
from pyqtorch.noise.readout import ReadoutNoise

rx = pyq.RX(0, param_name="theta")
y = pyq.Y(0)
cnot = pyq.CNOT(0, 1)
ops = [rx, y, cnot]
n_qubits = 2
circ = pyq.QuantumCircuit(n_qubits, ops)
state = pyq.random_state(n_qubits)
theta = torch.rand(1, requires_grad=True)
obs = pyq.Observable(pyq.Z(0))

noiseless_expectation = pyq.expectation(circ, state, {"theta": theta}, observable=obs)
readobj = ReadoutNoise(n_qubits, seed=0)
noisycirc = pyq.QuantumCircuit(n_qubits, ops, readobj)
noisy_expectation = pyq.expectation(noisycirc, state, {"theta": theta}, observable=obs, n_shots=1000)
print("Noiseless expectation ", noiseless_expectation.item())
print("Noisy expectation ", noisy_expectation.item())
Noiseless expectation 0.7339597762005495 Noisy expectation 0.71