Source code for qibo.backends.clifford

"""Module defining the Clifford backend."""

import collections
from functools import reduce
from importlib.util import find_spec, module_from_spec
from typing import Union

import numpy as np

from qibo import gates
from qibo.backends.numpy import NumpyBackend
from qibo.config import raise_error


def _get_engine_name(backend):
    return backend.platform if backend.platform is not None else backend.name


[docs]class CliffordBackend(NumpyBackend): """Backend for the simulation of Clifford circuits following `Aaronson & Gottesman (2004) <https://arxiv.org/abs/quant-ph/0406196>`_. Args: :class:`qibo.backends.abstract.Backend`: Backend used for the calculation. """ def __init__(self, engine=None): super().__init__() if engine == "stim": import stim # pylint: disable=C0415 engine = "numpy" self.platform = "stim" self._stim = stim else: if engine is None: from qibo.backends import _check_backend # pylint: disable=C0415 engine = _get_engine_name(_check_backend(engine)) self.platform = engine spec = find_spec("qibo.backends._clifford_operations") self.engine = module_from_spec(spec) spec.loader.exec_module(self.engine) if engine == "numpy": pass elif engine == "numba": from qibojit.backends import ( # pylint: disable=C0415 clifford_operations_cpu, ) for method in dir(clifford_operations_cpu): setattr(self.engine, method, getattr(clifford_operations_cpu, method)) elif engine == "cupy": # pragma: no cover from qibojit.backends import ( # pylint: disable=C0415 clifford_operations_gpu, ) for method in dir(clifford_operations_gpu): setattr(self.engine, method, getattr(clifford_operations_gpu, method)) else: raise_error( NotImplementedError, f"Backend `{engine}` is not supported for Clifford Simulation.", ) self.np = self.engine.np self.name = "clifford"
[docs] def cast(self, x, dtype=None, copy: bool = False): """Cast an object as the array type of the current backend. Args: x: Object to cast to array. copy (bool, optional): If ``True`` a copy of the object is created in memory. Defaults to ``False``. """ return self.engine.cast(x, dtype=dtype, copy=copy)
[docs] def calculate_frequencies(self, samples): res, counts = self.engine.np.unique(samples, return_counts=True) # The next two lines are necessary for the GPU backends res = [int(r) if not isinstance(r, str) else r for r in res] counts = [int(v) for v in counts] return collections.Counter(dict(zip(res, counts)))
[docs] def zero_state(self, nqubits: int): """Construct the zero state |00...00>. Args: nqubits (int): Number of qubits. Returns: (ndarray): Symplectic matrix for the zero state. """ identity = self.np.eye(nqubits) symplectic_matrix = self.np.zeros( (2 * nqubits + 1, 2 * nqubits + 1), dtype=bool ) symplectic_matrix[:nqubits, :nqubits] = self.np.copy(identity) symplectic_matrix[nqubits:-1, nqubits : 2 * nqubits] = self.np.copy(identity) return symplectic_matrix
def _clifford_pre_execution_reshape(self, state): """Reshape the symplectic matrix to the shape needed by the engine before circuit execution. Args: state (ndarray): Input state. Returns: ndarray: Reshaped state. """ return self.engine._clifford_pre_execution_reshape( # pylint: disable=protected-access state ) def _clifford_post_execution_reshape(self, state, nqubits: int): """Reshape the symplectic matrix to the shape needed by the engine after circuit execution. Args: state (ndarray): Input state. nqubits (int): Number of qubits. Returns: ndarray: Reshaped state. """ return self.engine._clifford_post_execution_reshape( # pylint: disable=protected-access state, nqubits )
[docs] def apply_gate_clifford(self, gate, symplectic_matrix, nqubits): """Apply a gate to a symplectic matrix.""" operation = getattr(self.engine, gate.__class__.__name__) kwargs = ( {"theta": gate.init_kwargs["theta"]} if "theta" in gate.init_kwargs else {} ) return operation(symplectic_matrix, *gate.init_args, nqubits, **kwargs)
[docs] def apply_channel(self, channel, state, nqubits): probabilities = channel.coefficients + (1 - np.sum(channel.coefficients),) index = self.np.random.choice( range(len(probabilities)), size=1, p=probabilities )[0] if index != len(channel.gates): gate = channel.gates[index] state = gate.apply_clifford(self, state, nqubits) return state
[docs] def execute_circuit( # pylint: disable=R1710 self, circuit, initial_state=None, nshots: int = 1000 ): """Execute a Clifford circuits. Args: circuit (:class:`qibo.models.circuit.Circuit`): Input circuit. initial_state (ndarray, optional): The ``symplectic_matrix`` of the initial state. If ``None``, defaults to the zero state. Defaults to ``None``. nshots (int, optional): Number of shots to perform if ``circuit`` has measurements. Defaults to :math:`10^{3}`. Returns: :class:`qibo.quantum_info.clifford.Clifford`: Object storing to the final results. """ from qibo.quantum_info.clifford import Clifford # pylint: disable=C0415 if self.platform == "stim": circuit_stim = self._stim.Circuit() # pylint: disable=E1101 for gate in circuit.queue: circuit_stim.append(gate.__class__.__name__, list(gate.qubits)) x_destab, z_destab, x_stab, z_stab, x_phases, z_phases = ( self._stim.Tableau.from_circuit( # pylint: disable=no-member circuit_stim ).to_numpy() ) symplectic_matrix = np.block([[x_destab, z_destab], [x_stab, z_stab]]) symplectic_matrix = np.c_[symplectic_matrix, np.r_[x_phases, z_phases]] return Clifford( symplectic_matrix, measurements=circuit.measurements, nshots=nshots, _backend=self, ) for gate in circuit.queue: if ( not gate.clifford and not gate.__class__.__name__ == "M" and not isinstance(gate, gates.PauliNoiseChannel) ): raise_error(RuntimeError, "Circuit contains non-Clifford gates.") if circuit.repeated_execution and nshots != 1: return self.execute_circuit_repeated(circuit, nshots, initial_state) try: nqubits = circuit.nqubits state = self.zero_state(nqubits) if initial_state is None else initial_state state = self._clifford_pre_execution_reshape(state) for gate in circuit.queue: gate.apply_clifford(self, state, nqubits) state = self._clifford_post_execution_reshape(state, nqubits) clifford = Clifford( state, measurements=circuit.measurements, nshots=nshots, _backend=self, ) return clifford except self.oom_error: # pragma: no cover raise_error( RuntimeError, f"State does not fit in {self.device} memory." "Please switch the execution device to a " "different one using ``qibo.set_device``.", )
[docs] def execute_circuit_repeated(self, circuit, nshots: int = 1000, initial_state=None): """Execute a Clifford circuits ``nshots`` times. This is used for all the simulations that involve repeated execution. For instance when collapsing measurement or noise channels are present. Args: circuit (:class:`qibo.models.circuit.Circuit`): input circuit. initial_state (ndarray, optional): Symplectic_matrix of the initial state. If ``None``, defaults to :meth:`qibo.backends.clifford.CliffordBackend.zero_state`. Defaults to ``None``. nshots (int, optional): Number of times to repeat the execution. Defaults to :math:`1000`. Returns: :class:`qibo.quantum_info.clifford.Clifford`: Object storing to the final results. """ from qibo.quantum_info.clifford import Clifford # pylint: disable=C0415 circuit_copy = circuit.copy() samples = [] for _ in range(nshots): res = self.execute_circuit(circuit_copy, initial_state, nshots=1) for measurement in circuit_copy.measurements: measurement.result.reset() samples.append(res.samples()) samples = self.np.vstack(samples) for meas in circuit.measurements: meas.result.register_samples(samples[:, meas.target_qubits], self) result = Clifford( self.zero_state(circuit.nqubits), measurements=circuit.measurements, nshots=nshots, _backend=self, ) result.symplectic_matrix, result._samples = None, None return result
[docs] def sample_shots( self, state, qubits: Union[tuple, list], nqubits: int, nshots: int, collapse: bool = False, ): # pylint: disable=W0221 """Sample shots by measuring selected ``qubits`` in symplectic matrix of a ``state``. Args: state (ndarray): symplectic matrix from which to sample shots from. qubits: (tuple or list): qubits to measure. nqubits (int): total number of qubits of the state. nshots (int): number of shots to sample. collapse (bool, optional): If ``True`` the input state is going to be collapsed with the last shot. Defaults to ``False``. Returns: (ndarray): Samples shots. """ if isinstance(qubits, list): qubits = tuple(qubits) if collapse: samples = [self.engine.M(state, qubits, nqubits) for _ in range(nshots - 1)] samples.append(self.engine.M(state, qubits, nqubits, collapse)) else: samples = [self.engine.M(state, qubits, nqubits) for _ in range(nshots)] return self.engine.cast(samples, dtype=int)
[docs] def symplectic_matrix_to_generators( self, symplectic_matrix, return_array: bool = False ): """Extract the stabilizers and destabilizers generators from symplectic matrix. Args: symplectic_matrix (ndarray): The input symplectic_matrix. return_array (bool, optional): If ``True`` returns the generators as ``ndarrays``. If ``False``, generators are returned as strings. Defaults to ``False``. Returns: (list, list): Extracted generators and their corresponding phases, respectively. """ bits_to_gate = {"00": "I", "01": "X", "10": "Z", "11": "Y"} nqubits = int((symplectic_matrix.shape[1] - 1) / 2) phases = (-1) ** symplectic_matrix[:-1, -1] tmp = 1 * symplectic_matrix[:-1, :-1] X, Z = tmp[:, :nqubits], tmp[:, nqubits:] generators = [] for x, z in zip(X, Z): paulis = [bits_to_gate[f"{zz}{xx}"] for xx, zz in zip(x, z)] if return_array: from qibo import matrices # pylint: disable=C0415 paulis = [self.cast(getattr(matrices, p)) for p in paulis] matrix = reduce(self.np.kron, paulis) generators.append(matrix) else: generators.append("".join(paulis)) if return_array: generators = self.cast(generators) return generators, phases