Backends

While the circuit class samples the matrices making up a quantum circuit, the backend performs the actual matrix multiplications which appear in the statevector simulation.

Theory

Statevector simulation

The statevector simulation takes an input state psi0, and propagates it to the final state psi1 by repeated left matrix multiplications. Assuming that our circuit has k steps, then we compute psi1 = U psi0 = U_k … U_1 psi0 where U_i are matrices of dimension (2^n, 2^n) for n qubit psi0 of shape (2^n,).

Tensors

Tensors are simply generalized versions of scalars, vectors and matrices. We can imagine it to be a box with incoming and outgoing legs. While a scalar has no legs, a vector has one leg, and a matrix has one incoming and one outgoing leg. We can interpret matrix-vector multiplication as connecting the right leg of the matrix with the left leg of the vector. This will create an object with one free left leg, which represents a vector as expected. Similarly, matrix-matrix multiplication will led to an object with one incoming and one outgoing leg, so a matrix.

Optimal tensor contractions

As explained in this resource, the ordering of the tensor contractions (think matrix multiplications) can influence the computational cost dramatically. In the EfficientBackend, we leverage optimizations based on the property of the quantum circuit, as well as optimizations performed by the library opt_einsum. This library computes the contractions for us.

Classes

class quantum_gates.backends.EfficientBackend(nqubit: int, min_chunk_size: int = 3, optimal_chunk_size: int = 4)[source]

Bases: object

Evaluates the quantum circuit represented as list of list of matrices with efficient tensor contractions.

The EfficientBackend is optimized for general circuits and offers a significant speedup in the higher qubit regime, scaling to 20+ qubits.

Parameters:
  • nqubit (int) – Number of qubits in the circuit.

  • min_chunk_size (int) – The matrices are grouped in chunks of at least this size, we recommend a value of 3.

  • optimal_chunk_size (int) – The backend aims at achieving an optimal chunk size of this value, normally 4.

Note

Always use this version for the backend for simulating many qubits, it is way faster.

Example

from quantum_gates.backend import EfficientBackend

backend = EfficientBackend(nqubit=2)

H, CNOT, identity = ...
mp_list = [[H, np.eye(2)], [CNOT]]

psi0 = np.array([1, 0, 0, 0])
psi1 = backend.statevector(mp_list, psi0)  # Gives [1, 0, 0, 1] / sqrt(2)
nqubit[source]

Number of qubits in the circuit.

Type:

int

min_chunk_size[source]

The matrices are grouped in chunks of at least this size, we recommend a value of 3.

Type:

int

optimal_chunk_size[source]

The backend aims at achieving an optimal chunk size of this value, normally 4.

Type:

int

statevector(mp_list: list, psi0: array) array[source]

Propagates a statevector based on a list of matrix products.

Parameters:
  • mp_list (list[list]) – List of list that contain numpy arrays.

  • psi0 (np.array) – Statevector to be propagated.

Returns:

The propagated statevector.

Return type:

array

_statevector_low_qubit_regime(mp_list: list, psi: array)[source]

Propagator for the low qubit regime.

In the low qubit regime (nqubit < 4), we just generate the expanded matrix product and apply it to psi.

Parameters:
  • mp_list (list) –

  • psi (array) –

_statevector_medium_qubit_regime(mp_list: list, psi: array)[source]

Propagator for the medium qubit regime.

In the medium regime (4 <= nqubit < 8), we just generate the expanded matrix product and apply it to psi.

Parameters:
  • mp_list (list) –

  • psi (array) –

_statevector_high_qubit_regime(mp_list: list, psi: array)[source]

Propagator for the high qubit regime.

In the high regime (8 <= nqubit), we split the matrix product into chunks of more or less equal size.

Parameters:
  • mp_list (list) –

  • psi (array) –

_opt_einsum_many_matrices(mp: list, psi: array) array[source]

Performs the contractions of a matrix product with psi in an optimized way.

For mp = [a1,…, an], calculates (a1⊗..⊗an)(psi) einsum. a1, …, an are square matrices and psi is a vector with the same length as the dimension of a1 tensor … tensor an from mp = [a1,…, an].

Returns:

The contracted psi, representing the updated statevector.

Parameters:
  • mp (list) –

  • psi (array) –

Return type:

array

_chunk_list(l: list, min_chunk_size: int, optimal_chunk_size: int) list[source]

Converts a list into a list of list, each representing a chunk.

Assumes that we have at least enough items to create two chunks of optimal size. Joins the last chunk with the second last in case it does not reach the minimum chunk size.

Example

l = [1, 2, 3, 4, 5, 6, 7], min_chunk_size = 2, optimal_chunK_size = 3 returns [[1,2,3], [1,2,3,4]].

Returns:

The produced list of list.

Parameters:
  • l (list) –

  • min_chunk_size (int) –

  • optimal_chunk_size (int) –

Return type:

list

quantum_gates.backends.LegacyBackend[source]

alias of StandardBackend

Usage

We apply the GHZ algorithm on the two-qubit zero state as an example.

from quantum_gates.backend import EfficientBackend

backend = EfficientBackend(nqubit=2)

H, CNOT, identity = ...
mp_list = [[H, np.eye(2)], [CNOT]]

psi0 = np.array([1, 0, 0, 0])
psi1 = backend.statevector(mp_list, psi0)  # Gives [1, 0, 0, 1] / sqrt(2)

Not applying gates to each qubit will lead to errors. In each matrix product, the dimension of the combined Kronecker product has to match the dimension of psi.

from quantum_gates.backend import EfficientBackend

backend = EfficientBackend(nqubit=2)

H, CNOT = ...

mp_list1 = [[CNOT, np.eye(2)]]              # Gates for 2 + 1 = 3 qubits -> wrong.
mp_list2 = [[CNOT]]                         # Gates for 2 qubits -> fine.
mp_list3 = [[np.eye(2), np.eye(2)], [H, H]] # Gates for 1 + 1 = 2 qubits -> fine.

Possible extensions

The backend class has a simple, generic public interface. This makes implementing a custom backend easy. One can optimize the backend performance by exploiting properties of a specific quantum algorithm, use another simulation method like Matrix Product State (MPS) simulation, or use another library as backend for the computations.