Source code for qibo.quantum_info.clifford

"""Module definig the Clifford object, which allows phase-space representation of Clifford circuits and stabilizer states."""

from dataclasses import dataclass, field
from functools import reduce
from itertools import product
from typing import Optional, Union

import numpy as np

from qibo import Circuit
from qibo.backends import CliffordBackend
from qibo.config import raise_error
from qibo.gates import M
from qibo.measurements import frequencies_to_binary

from ._clifford_utils import _decomposition_AG04, _decomposition_BM20, _string_product


[docs]@dataclass class Clifford: """Object storing the results of a circuit execution with the :class:`qibo.backends.clifford.CliffordBackend`. Args: data (ndarray or :class:`qibo.models.circuit.Circuit`): If ``ndarray``, it is the symplectic matrix of the stabilizer state in phase-space representation. If :class:`qibo.models.circuit.Circuit`, it is a circuit composed only of Clifford gates and computational-basis measurements. nqubits (int, optional): number of qubits of the state. measurements (list, optional): list of measurements gates :class:`qibo.gates.M`. Defaults to ``None``. nshots (int, optional): number of shots used for sampling the measurements. Defaults to :math:`1000`. engine (str, optional): engine to use in the execution of the :class:`qibo.backends.CliffordBackend`. It accepts ``"numpy"``, ``"numba"``, ``"cupy"``, and ``"stim"`` (see `stim <https://github.com/quantumlib/Stim>`_). If ``None``, defaults to the corresponding engine from :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. """ symplectic_matrix: np.ndarray = field(init=False) data: Union[np.ndarray, Circuit] = field(repr=False) nqubits: Optional[int] = None measurements: Optional[list] = None nshots: int = 1000 engine: Optional[str] = None _backend: Optional[CliffordBackend] = None _measurement_gate = None _samples: Optional[int] = None def __post_init__(self): if isinstance(self.data, Circuit): clifford = self.from_circuit(self.data, engine=self.engine) self.symplectic_matrix = clifford.symplectic_matrix self.nqubits = clifford.nqubits self.measurements = clifford.measurements self._samples = clifford._samples self._measurement_gate = clifford._measurement_gate else: # adding the scratch row if not provided self.symplectic_matrix = self.data if self.symplectic_matrix.shape[0] % 2 == 0: self.symplectic_matrix = np.vstack( ( self.symplectic_matrix, np.zeros(self.symplectic_matrix.shape[1], dtype=np.uint8), ) ) self.nqubits = int((self.symplectic_matrix.shape[1] - 1) / 2) if self._backend is None: self._backend = CliffordBackend(self.engine) self.engine = self._backend.engine
[docs] @classmethod def from_circuit( cls, circuit: Circuit, initial_state: Optional[np.ndarray] = None, nshots: int = 1000, engine: Optional[str] = None, ): """Allows to create a :class:`qibo.quantum_info.clifford.Clifford` object by executing the input circuit. Args: circuit (:class:`qibo.models.circuit.Circuit`): Clifford circuit to run. initial_state (ndarray, optional): symplectic matrix of the initial state. If ``None``, defaults to the symplectic matrix of the zero state. Defaults to ``None``. nshots (int, optional): number of measurement shots to perform if ``circuit`` has measurement gates. Defaults to :math:`10^{3}`. engine (str, optional): engine to use in the execution of the :class:`qibo.backends.CliffordBackend`. It accepts ``"numpy"``, ``"numba"``, ``"cupy"``, and ``"stim"`` (see `stim <https://github.com/quantumlib/Stim>`_). If ``None``, defaults to the corresponding engine from :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. Returns: (:class:`qibo.quantum_info.clifford.Clifford`): Object storing the result of the circuit execution. """ cls._backend = CliffordBackend(engine) return cls._backend.execute_circuit(circuit, initial_state, nshots)
[docs] def to_circuit(self, algorithm: Optional[str] = "AG04"): """Converts symplectic matrix into a Clifford circuit. Args: algorithm (str, optional): If ``AG04``, uses the decomposition algorithm from `Aaronson & Gottesman (2004) <https://arxiv.org/abs/quant-ph/0406196>`_. If ``BM20`` and ``Clifford.nqubits <= 3``, uses the decomposition algorithm from `Bravyi & Maslov (2020) <https://arxiv.org/abs/2003.09412>`_. Defaults to ``AG04``. Returns: :class:`qibo.models.circuit.Circuit`: circuit composed of Clifford gates. """ if not isinstance(algorithm, str): raise_error( TypeError, f"``algorithm`` must be type str, but it is type {type(algorithm)}", ) if algorithm not in ["AG04", "BM20"]: raise_error(ValueError, f"``algorithm`` {algorithm} not found.") if algorithm == "BM20": return _decomposition_BM20(self) return _decomposition_AG04(self)
[docs] def generators(self, return_array: bool = False): """Extracts the generators of stabilizers and destabilizers. Args: return_array (bool, optional): If ``True`` returns the generators as ``ndarray``. If ``False``, their representation as strings is returned. Defaults to ``False``. Returns: (list, list): Generators and their corresponding phases, respectively. """ return self._backend.symplectic_matrix_to_generators( self.symplectic_matrix, return_array )
[docs] def stabilizers(self, symplectic: bool = False, return_array: bool = False): """Extracts the stabilizers of the state. Args: symplectic (bool, optional): If ``True``, returns the rows of the symplectic matrix that correspond to the :math:`n` generators of the :math:`2^{n}` total stabilizers, independently of ``return_array``. return_array (bool, optional): To be used when ``symplectic = False``. If ``True`` returns the stabilizers as ``ndarray``. If ``False``, returns stabilizers as strings. Defaults to ``False``. Returns: (ndarray or list): Stabilizers of the state. """ if not symplectic: generators, phases = self.generators(return_array) return self._construct_operators( generators[self.nqubits :], phases[self.nqubits :], ) return self.symplectic_matrix[self.nqubits : -1, :]
[docs] def destabilizers(self, symplectic: bool = False, return_array: bool = False): """Extracts the destabilizers of the state. Args: symplectic (bool, optional): If ``True``, returns the rows of the symplectic matrix that correspond to the :math:`n` generators of the :math:`2^{n}` total destabilizers, independently of ``return_array``. return_array (bool, optional): To be used when ``symplectic = False``. If ``True`` returns the destabilizers as ``ndarray``. If ``False``, their representation as strings is returned. Defaults to ``False``. Returns: (ndarray or list): Destabilizers of the state. """ if not symplectic: generators, phases = self.generators(return_array) return self._construct_operators( generators[: self.nqubits], phases[: self.nqubits] ) return self.symplectic_matrix[: self.nqubits, :]
[docs] def state(self): """Builds the density matrix representation of the state. .. note:: This method is inefficient in runtime and memory for a large number of qubits. Returns: (ndarray): Density matrix of the state. """ stabilizers = self.stabilizers(return_array=True) return self.engine.np.sum(stabilizers, axis=0) / len(stabilizers)
@property def measurement_gate(self): """Single measurement gate containing all measured qubits. Useful for sampling all measured qubits at once when simulating. """ if self._measurement_gate is None: for gate in self.measurements: if self._measurement_gate is None: self._measurement_gate = M(*gate.init_args, **gate.init_kwargs) else: self._measurement_gate.add(gate) return self._measurement_gate
[docs] def samples(self, binary: bool = True, registers: bool = False): """Returns raw measurement samples. Args: binary (bool, optional): If ``False``, return samples in binary form. If ``True``, returns samples in decimal form. Defalts to ``True``. registers (bool, optional): If ``True``, groups samples according to registers. Defaults to ``False``. Returns: If ``binary`` is ``True`` samples are returned in binary form as a tensor of shape ``(nshots, n_measured_qubits)``. If ``binary`` is ``False`` samples are returned in decimal form as a tensor of shape ``(nshots,)``. If ``registers`` is ``True`` samples are returned in a ``dict`` where the keys are the register names and the values are the samples tensors for each register. If ``registers`` is ``False`` a single tensor is returned which contains samples from all the measured qubits, independently of their registers. """ if not self.measurements: raise_error(RuntimeError, "No measurement provided.") measured_qubits = self.measurement_gate.qubits if self._samples is None: if self.measurements[0].result.has_samples(): samples = np.concatenate( [gate.result.samples() for gate in self.measurements], axis=1 ) else: samples = self._backend.sample_shots( self.symplectic_matrix, measured_qubits, self.nqubits, self.nshots ) if self.measurement_gate.has_bitflip_noise(): p0, p1 = self.measurement_gate.bitflip_map bitflip_probabilities = self._backend.cast( [ [p0.get(q) for q in measured_qubits], [p1.get(q) for q in measured_qubits], ] ) samples = self._backend.cast(samples, dtype="int32") samples = self._backend.apply_bitflips(samples, bitflip_probabilities) # register samples to individual gate ``MeasurementResult`` qubit_map = { q: i for i, q in enumerate(self.measurement_gate.target_qubits) } self._samples = self._backend.cast(samples, dtype="int32") for gate in self.measurements: rqubits = tuple(qubit_map.get(q) for q in gate.target_qubits) gate.result.register_samples(self._samples[:, rqubits]) if registers: return { gate.register_name: gate.result.samples(binary) for gate in self.measurements } if binary: return self._samples return self._backend.samples_to_decimal(self._samples, len(measured_qubits))
[docs] def frequencies(self, binary: bool = True, registers: bool = False): """Returns the frequencies of measured samples. Args: binary (bool, optional): If ``True``, return frequency keys in binary form. If ``False``, return frequency keys in decimal form. Defaults to ``True``. registers (bool, optional): If ``True``, groups frequencies according to registers. Defaults to ``False``. Returns: A `collections.Counter` where the keys are the observed values and the values the corresponding frequencies, that is the number of times each measured value/bitstring appears. If ``binary`` is ``True`` the keys of the :class:`collections.Counter` are in binary form, as strings of :math:`0` and :math`1`. If ``binary`` is ``False`` the keys of the :class:`collections.Counter` are integers. If ``registers`` is ``True`` a `dict` of :class:`collections.Counter` is returned where keys are the name of each register. If ``registers`` is ``False`` a single :class:`collections.Counter` is returned which contains samples from all the measured qubits, independently of their registers. """ measured_qubits = self.measurement_gate.target_qubits freq = self._backend.calculate_frequencies(self.samples(False)) if registers: if binary: return { gate.register_name: frequencies_to_binary( self._backend.calculate_frequencies(gate.result.samples(False)), len(gate.target_qubits), ) for gate in self.measurements } return { gate.register_name: self._backend.calculate_frequencies( gate.result.samples(False) ) for gate in self.measurements } if binary: return frequencies_to_binary(freq, len(measured_qubits)) return freq
[docs] def probabilities(self, qubits: Optional[Union[tuple, list]] = None): """Computes the probabilities of the selected qubits from the measured samples. Args: qubits (tuple or list, optional): Qubits for which to compute the probabilities. Returns: (ndarray): Measured probabilities. """ if isinstance(qubits, list): qubits = tuple(qubits) measured_qubits = self.measurement_gate.qubits if qubits is not None: if not set(qubits).issubset(set(measured_qubits)): raise_error( RuntimeError, f"Asking probabilities for qubits {qubits}, but only qubits {measured_qubits} were measured.", ) qubits = [measured_qubits.index(q) for q in qubits] else: qubits = range(len(measured_qubits)) probs = [0 for _ in range(2 ** len(measured_qubits))] samples = self.samples(binary=False) for s in samples: probs[int(s)] += 1 probs = self.engine.cast(probs, float) / len(samples) return self._backend.calculate_probabilities( self.engine.np.sqrt(probs), qubits, len(measured_qubits) )
[docs] def copy(self, deep: bool = False): """Returns copy of :class:`qibo.quantum_info.clifford.Clifford` object. Args: deep (bool, optional): If ``True``, creates another copy in memory. Defaults to ``False``. Returns: :class:`qibo.quantum_info.clifford.Clifford`: copy of original ``Clifford`` object. """ if not isinstance(deep, bool): raise_error( TypeError, f"``deep`` must be type bool, but it is type {type(deep)}." ) symplectic_matrix = ( self.engine.np.copy(self.symplectic_matrix) if deep else self.symplectic_matrix ) return self.__class__( symplectic_matrix, self.nqubits, self.measurements, self.nshots, _backend=self._backend, )
def _construct_operators(self, generators: list, phases: list): """Helper function to construct all the operators from their generators. Args: generators (list or ndarray): generators. phases (list or ndarray): phases of the generators. Returns: (list): All operators generated by the generators of the stabilizer group. """ if not isinstance(generators[0], str): generators = self._backend.cast(generators) phases = self._backend.cast(phases) operators = generators * phases.reshape(-1, 1, 1) identity = self.engine.identity_density_matrix( self.nqubits, normalize=False ) operators = self._backend.cast([(g, identity) for g in operators]) return self._backend.cast( [reduce(self.engine.np.matmul, ops) for ops in product(*operators)] ) operators = list(np.copy(generators)) for i in (phases == -1).nonzero()[0]: i = int(i) operators[i] = f"-{operators[i]}" identity = "".join(["I" for _ in range(self.nqubits)]) operators = [(g, identity) for g in operators] return [_string_product(ops) for ops in product(*operators)]