Source code for qibo.transpiler.unroller

from enum import EnumMeta, Flag, auto
from functools import reduce
from operator import or_
from typing import List, Optional, Union

from qibo import gates
from qibo.backends import Backend, _check_backend
from qibo.config import raise_error
from qibo.gates import Gate
from qibo.models import Circuit
from qibo.transpiler._exceptions import DecompositionError
from qibo.transpiler.decompositions import (
    cz_dec,
    gpi2_dec,
    iswap_dec,
    opt_dec,
    standard_decompositions,
    u3_dec,
)


class FlagMeta(EnumMeta):
    """Metaclass for :class:`qibo.transpiler.unroller.NativeGates`
    that allows initialization with a list of gate name strings."""

    def __getitem__(cls, keys: Union[str, List[str]]):
        if isinstance(keys, str):
            try:
                return super().__getitem__(keys)
            except KeyError:
                return super().__getitem__("NONE")
        return reduce(or_, [cls[key] for key in keys])  # pylint: disable=E1136


[docs]class NativeGates(Flag, metaclass=FlagMeta): """Define native gates supported by the unroller. A native gate set should contain at least one two-qubit gate (:class:`qibo.gates.gates.CZ` or :class:`qibo.gates.gates.iSWAP`), and at least one single-qubit gate (:class:`qibo.gates.gates.GPI2` or :class:`qibo.gates.gates.U3`). Possible gates are: - :class:`qibo.gates.gates.I` - :class:`qibo.gates.gates.Z` - :class:`qibo.gates.gates.RZ` - :class:`qibo.gates.gates.M` - :class:`qibo.gates.gates.GPI2` - :class:`qibo.gates.gates.U3` - :class:`qibo.gates.gates.CZ` - :class:`qibo.gates.gates.iSWAP` - :class:`qibo.gates.gates.CNOT` """ NONE = 0 I = auto() Z = auto() RZ = auto() M = auto() GPI2 = auto() U3 = auto() CZ = auto() iSWAP = auto() CNOT = auto() # For testing purposes
[docs] @classmethod def default(cls): """Return default native gates set.""" return cls.CZ | cls.GPI2 | cls.I | cls.Z | cls.RZ | cls.M
[docs] @classmethod def from_gatelist(cls, gatelist: List[Gate]): """Create a NativeGates object containing all gates from a ``gatelist``.""" natives = cls(0) for gate in gatelist: natives |= cls.from_gate(gate) return natives
[docs] @classmethod def from_gate(cls, gate: Gate): # pylint: disable=R1710 """Create a :class:`qibo.transpiler.unroller.NativeGates` object from a :class:`qibo.gates.gates.Gate`.""" if isinstance(gate, Gate): return cls.from_gate(gate.__class__) try: return getattr(cls, gate.__name__) except AttributeError: raise_error(ValueError, f"Gate {gate} cannot be used as native.")
# TODO: Make setting single-qubit native gates more flexible
[docs]class Unroller: """Decomposes a circuit to native gates.""" def __init__( self, native_gates: NativeGates, backend: Optional[Backend] = None, ): self.native_gates = native_gates self.backend = backend """Initializes the unroller. Args: native_gates (:class:`qibo.transpiler.unroller.NativeGates`): Native gates to use in the transpiled circuit. backend (:class:`qibo.backends.Backend`): Backend to use for gate matrix. """ def __call__(self, circuit: Circuit) -> Circuit: """Decomposes a circuit to native gates. Args: circuit (:class:`qibo.models.circuit.Circuit`): Circuit to be decomposed. Returns: (:class:`qibo.models.circuit.Circuit`): Decomposed circuit. """ translated_circuit = Circuit(**circuit.init_kwargs) for gate in circuit.queue: translated_circuit.add( translate_gate( gate, self.native_gates, backend=self.backend, ) ) return translated_circuit
[docs]def translate_gate( gate, native_gates: NativeGates, backend: Optional[Backend] = None, ) -> List[Gate]: """Maps gates to a hardware-native implementation. Args: gate (:class:`qibo.gates.abstract.Gate`): Gate to be decomposed. native_gates (:class:`qibo.transpiler.unroller.NativeGates`): Native gates supported by the hardware. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to use for gate matrix. If ``None``, defaults to the global backend. Defaults to ``None``. Returns: list[:class:`qibo.gates.abstract.Gate`]: Native gates that decompose the input gate. """ backend = _check_backend(backend) if isinstance(gate, (gates.I, gates.Align)): return gate if isinstance(gate, gates.M): gate.basis_gates = len(gate.basis_gates) * [gates.Z] gate.basis = [] return gate if len(gate.qubits) == 1: return _translate_single_qubit_gates(gate, native_gates, backend) decomposition_2q = _translate_two_qubit_gates(gate, native_gates, backend) final_decomposition = [] for decomposed_2q_gate in decomposition_2q: if len(decomposed_2q_gate.qubits) == 1: final_decomposition += _translate_single_qubit_gates( decomposed_2q_gate, native_gates, backend ) else: final_decomposition.append(decomposed_2q_gate) return final_decomposition
def _translate_single_qubit_gates( gate: Gate, single_qubit_natives: NativeGates, backend: Backend ) -> List[Gate]: """Helper method for :meth:`translate_gate`. Maps single-qubit gates to a hardware-native implementation. Args: gate (:class:`qibo.gates.abstract.Gate`): Gate to be decomposed. single_qubit_natives (:class:`qibo.transpiler.unroller.NativeGates`): Single qubit native gates supported by the hardware. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to use for gate matrix. If ``None``, defaults to the global backend. Returns: list[:class:`qibo.gates.abstract.Gate`]: Native gates that decompose the input gate. """ if not (NativeGates.U3 & single_qubit_natives) and not ( NativeGates.GPI2 & single_qubit_natives ): raise_error(DecompositionError, "Use U3 or GPI2 as single qubit native gates") if NativeGates.GPI2 & single_qubit_natives: return gpi2_dec(gate, backend) return u3_dec(gate, backend) def _translate_two_qubit_gates( # pylint: disable=R1710 gate: Gate, native_gates: NativeGates, backend: Backend ) -> List[Gate]: """Helper method for :meth:`translate_gate`. Maps two-qubit gates to a hardware-native implementation. Args: gate (:class:`qibo.gates.abstract.Gate`): Gate to be decomposed. native_gates (:class:`qibo.transpiler.unroller.NativeGates`): Native gates supported by the hardware. backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to use for gate matrix. If ``None``, defaults to the global backend. Defaults to ``None``. Returns: list[:class:`qibo.gates.abstract.Gate`]: Native gates that decompose the input gate. """ if ( native_gates & (NativeGates.CZ | NativeGates.iSWAP) ) is NativeGates.CZ | NativeGates.iSWAP: # Check for a special optimized decomposition. if gate.__class__ in opt_dec.decompositions: return opt_dec(gate, backend) # Check if the gate has a CZ decomposition if gate.__class__ not in iswap_dec.decompositions: return cz_dec(gate, backend) # Check the decomposition with less 2 qubit gates. if cz_dec.count_2q(gate, backend) < iswap_dec.count_2q(gate, backend): return cz_dec(gate) if cz_dec.count_2q(gate, backend) > iswap_dec.count_2q(gate, backend): return iswap_dec(gate, backend) # If equal check the decomposition with less 1 qubit gates. # This is never used for now but may be useful for future generalization if cz_dec.count_1q(gate, backend) < iswap_dec.count_1q( gate, backend ): # pragma: no cover return cz_dec(gate, backend) return iswap_dec(gate, backend) # pragma: no cover if native_gates & NativeGates.CZ: return cz_dec(gate, backend) if native_gates & NativeGates.iSWAP: if gate.__class__ in iswap_dec.decompositions: return iswap_dec(gate, backend) # First decompose into CZ cz_decomposed = cz_dec(gate, backend) # Then CZ are decomposed into iSWAP iswap_decomposed = [] for g in cz_decomposed: # Need recursive function as gates.Unitary is not in iswap_dec for g_translated in translate_gate( g, native_gates=native_gates, backend=backend ): iswap_decomposed.append(g_translated) return iswap_decomposed # For testing purposes # No CZ, iSWAP gates in the native gate set # Decompose CNOT, CZ, SWAP gates into CNOT gates if native_gates & NativeGates.CNOT: return standard_decompositions(gate, backend) raise_error( DecompositionError, "Use only CZ and/or iSWAP as native gates. CNOT is allowed in circuits" + "where the two-qubit gates are limited to CZ, CNOT, and SWAP.", ) # pragma: no cover