Source code for qibo.gates.abstract

"""Module defines the abstract gate classes Gate, SpecialGate, ParametrizedGate."""

import json
from abc import abstractmethod
from collections.abc import Iterable
from math import pi
from typing import List, Sequence, Tuple

import sympy
from numpy.typing import ArrayLike

from qibo import config
from qibo.config import raise_error

REQUIRED_FIELDS = [
    "name",
    "init_args",
    "init_kwargs",
    "_target_qubits",
    "_control_qubits",
]
REQUIRED_FIELDS_INIT_KWARGS = [
    "theta",
    "phi",
    "lam",
    "phi0",
    "phi1",
    "register_name",
    "collapse",
    "basis",
    "p0",
    "p1",
]

GATES_CONTROLLED_BY_DEFAULT = [
    "cx",
    "cy",
    "cz",
    "csx",
    "csxdg",
    "crx",
    "cry",
    "crz",
    "cu1",
    "cu2",
    "cu3",
    "ccx",
    "ccz",
    "fanout",
]


[docs]class Gate: """The base class for gate implementation. All base gates should inherit this class. Attributes: name (str): Name of the gate. draw_label (str): Optional label for drawing the gate in a circuit with :meth:`qibo.models.Circuit.draw`. is_controlled_by (bool): ``True`` if the gate was created using the :meth:`qibo.gates.abstract.Gate.controlled_by` method, otherwise ``False``. init_args (list): Arguments used to initialize the gate. init_kwargs (dict): Arguments used to initialize the gate. target_qubits (tuple): Tuple with ids of target qubits. control_qubits (tuple): Tuple with ids of control qubits sorted in increasing order. """ def __init__(self): self.name = None self.draw_label = None self.is_controlled_by = False # args for creating gate self.init_args = [] self.init_kwargs = {} self.unitary = False self._target_qubits = () self._control_qubits = () self._parameters = () config.ALLOW_SWITCHERS = False self.symbolic_parameters = {} # for distributed circuits self.device_gates = set() self.original_gate = None self._clifford = False def apply(self, backend, state: ArrayLike, nqubits: int) -> ArrayLike: return backend.apply_gate(self, state, nqubits) def apply_clifford(self, backend, state: ArrayLike, nqubits: int) -> ArrayLike: return backend.apply_gate_clifford(self, state, nqubits)
[docs] def basis_rotation(self): """Transformation required to rotate the basis for measuring the gate.""" raise_error( NotImplementedError, f"Basis rotation is not implemented for {self.__class__.__name__}", )
def check_controls(func): # pylint: disable=E0213 def wrapper(self, *args): if self.control_qubits: raise_error( RuntimeError, "Cannot use `controlled_by` method " + f"on gate {self} because it is already " + f"controlled by {self.control_qubits}.", ) return func(self, *args) # pylint: disable=E1102 return wrapper @property def clifford(self) -> bool: """Return boolean value representing if a Gate is Clifford or not.""" return False
[docs] def commutes(self, gate) -> bool: """Checks if two gates commute. Args: gate: Gate to check if it commutes with the current gate. Returns: bool: ``True`` if the gates commute, ``False`` otherwise. """ if isinstance(gate, SpecialGate): # pragma: no cover return False t1 = set(self.target_qubits) t2 = set(gate.target_qubits) a = self.__class__ == gate.__class__ and t1 == t2 b = not (t1 & set(gate.qubits) or t2 & set(self.qubits)) return a or b
@property def control_qubits(self) -> Tuple[int, ...]: """Tuple with ids of control qubits sorted in increasing order.""" return tuple(sorted(self._control_qubits)) @control_qubits.setter def control_qubits(self, qubits: Sequence[int]) -> None: """Sets control qubits set.""" self._set_control_qubits(qubits) self._check_control_target_overlap() @check_controls def controlled_by(self, *qubits: int): """Controls the gate on (arbitrarily many) qubits. To see how this method affects the underlying matrix representation of a gate, please see the documentation of :meth:`qibo.gates.Gate.matrix`. .. note:: Some gate classes default to another gate class depending on the number of controls present. For instance, an :math:`1`-controlled :class:`qibo.gates.X` gate will default to a :class:`qibo.gates.CNOT` gate, while a :math:`2`-controlled :class:`qibo.gates.X` gate defaults to a :class:`qibo.gates.TOFFOLI` gate. Other gates affected by this method are: :class:`qibo.gates.Y`, :class:`qibo.gates.Z`, :class:`qibo.gates.RX`, :class:`qibo.gates.RY`, :class:`qibo.gates.RZ`, :class:`qibo.gates.U1`, :class:`qibo.gates.U2`, and :class:`qibo.gates.U3`. Args: *qubits (int): Ids of the qubits that the gate will be controlled on. Returns: :class:`qibo.gates.Gate`: object in with the corresponding gate being controlled in the given qubits. """ if qubits: self.is_controlled_by = True self.control_qubits = qubits return self
[docs] def dagger(self): """Returns the dagger (conjugate transpose) of the gate. Note that dagger is not persistent for parametrized gates. For example, applying a dagger to an :class:`qibo.gates.gates.RX` gate will change the sign of its parameter at the time of application. However, if the parameter is updated after that, for example using :meth:`qibo.models.circuit.Circuit.set_parameters`, then the action of dagger will be lost. Returns: :class:`qibo.gates.Gate`: object representing the dagger of the original gate. """ new_gate = self._dagger() new_gate.is_controlled_by = self.is_controlled_by new_gate.control_qubits = self.control_qubits if hasattr(self, "_clifford"): new_gate._clifford = self._clifford return new_gate
[docs] def decompose( self, *free, use_toffolis: bool = True, method: str = "standard", **kwargs ): """Decomposes multi-control gates to gates supported by OpenQASM. Decompositions are based on `arXiv:9503016 <https://arxiv.org/abs/quant-ph/9503016>`_. If the gate is already controlled, it recursively decomposes the base gate and updates the control qubits accordingly. Args: free (int): Ids of free qubits to use for the gate decomposition. use_toffolis: If ``True`` the decomposition contains only ``TOFFOLI`` gates. If ``False`` a congruent representation is used for ``TOFFOLI`` gates. See :class:`qibo.gates.TOFFOLI` for more details on this representation. method (str, optional): Choice of gate set for the decomposition. If ``"standard"``, decomposes circuit into :class:`qibo.gates.gates.CNOT`, :class:`qibo.gates.gates.RX`, :class:`qibo.gates.gates.RY`, :class:`qibo.gates.gates.RZ`, :class:`qibo.gates.gates.U1`, :class:`qibo.gates.gates.U2`, :class:`qibo.gates.gates.U3`, and Clifford gates. If ``"clifford_plus_t"``, decomposes the circuit into :class:`qibo.gates.gates.CNOT`, :class:`qibo.gates.gates.H`, :class:`qibo.gates.gates.S`, :class:`qibo.gates.gates.X`, :class:`qibo.gates.gates.Y`, :class:`qibo.gates.gates.Z`, and :class:`qibo.gates.gates.T`. Defaults to ``"standard"``. kwargs (dict, optional): Additional arguments. When ``method = "clifford_plus_t"``, one can set ``epsilon`` (:math:`\\epsilon`) precision for the transpilation of each gate into the Clifford + :class:`qibo.gates.gates.T` gate set. This precision defaults to :math:`\\epsilon = 10^{-16}`. Another possible keyword argument is ``mpmath_dps``, which defines the number of decimal places used by the ``mpmath`` package. ``mpmmath_dps`` defaults to :math:`256`. Returns: List[:class:`qibo.gates.abstract.Gate`]: Gates that have the same effect as applying the original gate. """ if self.is_controlled_by: # Step 1: Error check with all controls/targets if set(free) & set(self.qubits): # pragma: no cover raise_error( ValueError, "Cannot decompose multi-controlled ``X`` gate if free " "qubits coincide with target or controls.", ) ncontrols = len(self.control_qubits) # Step 2: Decompose base gate without controls base_gate = self.__class__(*self.init_args, **self.init_kwargs) decomposed = base_gate._base_decompose( *free, use_toffolis=use_toffolis, ncontrols=ncontrols, method=method, **kwargs, ) mask = self._control_mask_after_stripping(decomposed) for bool_value, gate in zip(mask, decomposed): if bool_value: gate.is_controlled_by = True gate.control_qubits += self.control_qubits return decomposed return self._base_decompose(*free, use_toffolis=use_toffolis, method=method)
[docs] @staticmethod def from_dict(raw: dict): """Load from serialization. Essentially the counter-part of :meth:`raw`. """ from qibo.gates import ( # pylint: disable=import-outside-toplevel gates, measurements, ) for mod in (gates, measurements): try: cls = getattr(mod, raw["_class"]) break except AttributeError: # gate not found in given module, try next pass else: raise_error(ValueError, f"Unknown gate {raw['_class']}") gate = cls(*raw["init_args"], **raw["init_kwargs"]) if raw["_class"] == "M": if raw["measurement_result"]["samples"] is not None: gate.result.register_samples(raw["measurement_result"]["samples"]) return gate if gate.name not in GATES_CONTROLLED_BY_DEFAULT: try: return gate.controlled_by(*raw["_control_qubits"]) except RuntimeError as e: # pragma: no cover if "controlled" in e.args[0]: return gate raise e return gate
[docs] @abstractmethod def generator(self, backend): """This function returns the gate's generator. Returns: ArrayLike: generator. """
[docs] def generator_eigenvalue(self): """This function returns the eigenvalues of the gate's generator. Returns: float: eigenvalue of the generator. """ raise_error( NotImplementedError, f"Generator eigenvalue is not implemented for {self.__class__.__name__}", )
@property def hamming_weight(self) -> bool: """Return boolean value representing if a Gate is Hamming-weight-preserving or not.""" return False
[docs] def matrix(self, backend=None) -> ArrayLike: """Returns the matrix representation of the gate. If gate has controlled qubits inserted by :meth:`qibo.gates.Gate.controlled_by`, then :meth:`qibo.gates.Gate.matrix` returns the matrix of the original gate. .. code-block:: python from qibo import gates gate = gates.SWAP(3, 4).controlled_by(0, 1, 2) print(gate.matrix()) To return the full matrix that takes the control qubits into account, one should use :meth:`qibo.models.Circuit.unitary`, e.g. .. code-block:: python from qibo import Circuit, gates nqubits = 5 circuit = Circuit(nqubits) circuit.add(gates.SWAP(3, 4).controlled_by(0, 1, 2)) print(circuit.unitary()) Args: backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses the current backend. Defaults to ``None``. Returns: ndarray: Matrix representation of gate. """ from qibo.backends import _check_backend # pylint: disable=C0415 backend = _check_backend(backend) return backend.matrix(self)
[docs] def on_qubits(self, qubit_map: dict): """Creates the same gate targeting different qubits. Args: qubit_map (int): Dictionary mapping original qubit indices to new ones. Returns: A :class:`qibo.gates.Gate` object of the original gate type targeting the given qubits. Example: .. testcode:: from qibo import Circuit, gates circuit = Circuit(4) # Add some CNOT gates # equivalent to gates.CNOT(2, 3) circuit.add(gates.CNOT(2, 3).on_qubits({2: 2, 3: 3})) # equivalent to gates.CNOT(3, 0) circuit.add(gates.CNOT(2, 3).on_qubits({2: 3, 3: 0})) # equivalent to gates.CNOT(1, 3) circuit.add(gates.CNOT(2, 3).on_qubits({2: 1, 3: 3})) # equivalent to gates.CNOT(2, 1) circuit.add(gates.CNOT(2, 3).on_qubits({2: 2, 3: 1})) circuit.draw() .. testoutput:: 0: ───X───── 1: ───|─o─X─ 2: ─o─|─|─o─ 3: ─X─o─X─── """ # Explicit mention of gates where the method # `.controlled_by` fail. This should be fixed separatedly. qubits = ( self.qubits if self.name in GATES_CONTROLLED_BY_DEFAULT else self.target_qubits ) qubits = tuple(qubit_map.get(q) for q in qubits) gate = self.__class__(*qubits, **self.init_kwargs) if self.is_controlled_by: controls = (qubit_map.get(q) for q in self.control_qubits) gate = gate.controlled_by(*controls) return gate
@property def parameters(self) -> Tuple[float, ...]: """Returns a tuple containing the current value of gate's parameters.""" return self._parameters @property def qasm_label(self) -> str | Tuple[str, str]: """String corresponding to OpenQASM operation of the gate. For more exotic gates, both the internal qibo name of the gate and the custom gate QASM definition are returned as a tuple for broader compatibility. """ raise_error( NotImplementedError, f"{self.__class__.__name__} is not supported by OpenQASM", ) @property def qubits(self) -> Tuple[int, ...]: """Tuple with ids of all qubits (control and target) that the gate acts.""" return self.control_qubits + self.target_qubits @property def raw(self) -> dict: """Serialize to dictionary. The values used in the serialization should be compatible with a JSON dump (or any other one supporting a minimal set of scalar types). Though the specific implementation is up to the specific gate. """ encoded = self.__dict__ encoded_simple = { key: value for key, value in encoded.items() if key in REQUIRED_FIELDS } encoded_simple["init_kwargs"] = { key: value for key, value in encoded_simple["init_kwargs"].items() if key in REQUIRED_FIELDS_INIT_KWARGS } encoded_simple["_class"] = type(self).__name__ return encoded_simple @property def target_qubits(self) -> Tuple[int, ...]: """Tuple with ids of target qubits.""" return self._target_qubits @target_qubits.setter def target_qubits(self, qubits: Sequence[int]) -> None: """Sets target qubits tuple.""" self._set_target_qubits(qubits) self._check_control_target_overlap()
[docs] def to_json(self): """Dump gate to JSON. Note: Consider using :meth:`raw` directly. """ return json.dumps(self.raw)
def _base_decompose( self, *free, use_toffolis: bool = True, method: str = "standard", **kwargs ): """Base decomposition for gates. Returns a list containing the gate itself. Should be overridden by subclasses that support decomposition to simpler gates. Args: free: Ids of free qubits to use for the gate decomposition. use_toffolis: If ``True`` the decomposition contains only ``TOFFOLI`` gates. If ``False`` a congruent representation is used for ``TOFFOLI`` gates. See :class:`qibo.gates.TOFFOLI` for more details on this representation. kwargs (dict, optional): Additional arguments. When ``method = "clifford_plus_t"``, one can set ``epsilon`` (:math:`\\epsilon`) precision for the transpilation of each gate into the Clifford + :class:`qibo.gates.gates.T` gate set. This precision defaults to :math:`\\epsilon = 10^{-16}`. Another possible keyword argument is ``mpmath_dps``, which defines the number of decimal places used by the ``mpmath`` package. ``mpmmath_dps`` defaults to :math:`256`. Returns: List[:class:`qibo.gates.abstract.Gate`]: Synthesis of the original gate in another gate set. """ try: if method == "clifford_plus_t": from qibo.transpiler.decompositions import ( # pylint: disable=C0415 clifford_plus_t, ) mpmath_dps = kwargs.get("mpmath_dps", 256) epsilon = kwargs.get("epsilon", 1e-16) func = clifford_plus_t func._set_precision_cliff_plus_t(epsilon, mpmath_dps) else: from qibo.transpiler.decompositions import ( # pylint: disable=C0415 standard_decompositions, ) func = standard_decompositions return func(self) except KeyError: return [self.__class__(*self.init_args, **self.init_kwargs)] def _check_control_target_overlap(self) -> None: """Checks that there are no qubits that are both target and controls.""" control_and_target = self._control_qubits + self._target_qubits common = len(set(control_and_target)) != len(control_and_target) if common: raise_error( ValueError, f"{set(self._target_qubits) & set(self._control_qubits)}" + "qubits are both targets and controls " + f"for gate {self.__class__.__name__}.", ) def _control_mask_after_stripping(self, gates) -> List[bool]: """Returns a mask indicating which gates should be controlled.""" left = 0 right = len(gates) - 1 mask = [True] * len(gates) while left < right: g1, g2 = gates[left], gates[right] if self._gates_cancel(g1, g2): mask[left] = False mask[right] = False left += 1 right -= 1 return mask def _dagger(self): """Helper method for :meth:`qibo.gates.Gate.dagger`.""" # By default the ``_dagger`` method creates an equivalent gate, assuming # that the gate is Hermitian (true for common gates like H or Paulis). # If the gate is not Hermitian the ``_dagger`` method should be modified. return self.__class__(*self.init_args, **self.init_kwargs) @staticmethod def _find_repeated(qubits: Sequence[int]) -> int: """Finds the first qubit id that is repeated in a sequence of qubit ids.""" temp_set = set() for qubit in qubits: if qubit in temp_set: return qubit temp_set.add(qubit) @staticmethod def _gates_cancel(g1, g2) -> bool: """Determines if two gates cancel each other. Two gates are considered to cancel if: - They are of the same type (class). - They act on the same target and control qubits. - For fixed gates (like H, CX, X, Y, Z, SWAP), they always cancel in pairs. - For parametrized rotation gates (subclasses of _Rn_), their parameters sum to a multiple of 2π. Note: Multi-parameter gates are not currently supported by this check. Args: g1, g2: Gate instances to compare. Returns: bool: True if the gates cancel each other, False otherwise. """ if g1.__class__ != g2.__class__: return False if g1.target_qubits != g2.target_qubits: return False if g1.control_qubits != g2.control_qubits: return False # Identity conditions for fixed gates name = g1.name if name in ("h", "cx", "x", "y", "z", "swap", "ecr", "ccx", "ccz", "fanout"): return True # Check for parametrized rotation gates if "_Rn_" in [base.__name__ for base in g1.__class__.__bases__]: theta1 = g1.parameters[0] theta2 = g2.parameters[0] # Check if theta1 + theta2 is a multiple of 2π return bool((theta1 + theta2) % (2 * pi) < 1e-8) return False def _set_control_qubits(self, qubits: Sequence[int]) -> None: """Helper method for setting control qubits.""" if len(set(qubits)) != len(qubits): repeated = self._find_repeated(qubits) raise_error( ValueError, f"Control qubit {repeated} was given twice for gate {self.__class__.__name__}.", ) self._control_qubits = qubits def _set_target_qubits(self, qubits: Sequence[int]) -> None: """Helper method for setting target qubits.""" self._target_qubits = tuple(qubits) if len(self._target_qubits) != len(set(qubits)): repeated = self._find_repeated(qubits) raise_error( ValueError, f"Target qubit {repeated} was given twice for gate {self.__class__.__name__}.", ) def _set_targets_and_controls( self, target_qubits: Sequence[int], control_qubits: Sequence[int] ) -> None: """Sets target and control qubits simultaneously. This is used for the reduced qubit updates in the distributed circuits because using the individual setters may raise errors due to temporary overlap of control and target qubits. """ self._set_target_qubits(target_qubits) self._set_control_qubits(control_qubits) self._check_control_target_overlap()
class SpecialGate(Gate): """Abstract class for special gates.""" def commutes(self, gate: Gate) -> bool: return False def matrix(self, backend=None): # pragma: no cover raise_error( NotImplementedError, "Special gates do not have matrix representation." ) def on_qubits(self, qubit_map: dict): raise_error(NotImplementedError, "Cannot use special gates on subroutines.") class ParametrizedGate(Gate): """Base class for parametrized gates. Implements the basic functionality of parameter setters and getters. """ def __init__(self, trainable: bool = True): super().__init__() self.parameter_names = "theta" self.nparams = 1 self.trainable = trainable @abstractmethod def gradient(self, backend=None) -> Gate: """Returns Unitary with gate's gradient. Only gates with a single parameter are currently supported. """ def matrix(self, backend=None) -> ArrayLike: from qibo.backends import _check_backend # pylint: disable=C0415 backend = _check_backend(backend) return backend.matrix_parametrized(self) def on_qubits(self, qubit_map: dict) -> Gate: gate = super().on_qubits(qubit_map) gate.parameters = self.parameters return gate @Gate.parameters.setter def parameters(self, x: ArrayLike) -> None: """Updates the values of gate's parameters.""" if isinstance(self.parameter_names, str): nparams = 1 names = [self.parameter_names] if not isinstance(x, Iterable): x = [x] else: # Captures the ``Unitary`` gate case where the given parameter # can be an array try: if len(x) != 1: # pragma: no cover x = [x] except TypeError: # tf.Variable case s = tuple(x.shape) if not s or s[0] != 1: x = [x] else: nparams = len(self.parameter_names) names = self.parameter_names if not self._parameters: params = nparams * [None] else: params = list(self._parameters) if len(x) != nparams: raise_error( ValueError, f"Parametrized gate has {nparams} parameters " + f"but {len(x)} update values were given.", ) for i, v in enumerate(x): if isinstance(v, sympy.Expr): self.symbolic_parameters[i] = v params[i] = v self._parameters = tuple(params) self.init_kwargs.update( {n: v for n, v in zip(names, self._parameters) if n in self.init_kwargs} ) # set parameters in device gates for gate in self.device_gates: # pragma: no cover gate.parameters = x def substitute_symbols(self): params = list(self._parameters) for i, param in self.symbolic_parameters.items(): for symbol in param.free_symbols: param = symbol.evaluate(param) params[i] = float(param) self.parameters = tuple(params)