From 6ce0b3c43457e3582e14300290edfde19a0de667 Mon Sep 17 00:00:00 2001 From: cqc-melf <70640934+cqc-melf@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:52:24 +0100 Subject: [PATCH] Internal/narrow types required for compilation (#310) * Narrow args for _tk_gate_set * Narrow types for _get_backend_info args * Shadow methods for default_compilation_pass and rebase_pass * Use config for backend_name * Update changelog.rst --------- Co-authored-by: Matthew Burke --- docs/changelog.rst | 5 ++ pytket/extensions/qiskit/backends/ibm.py | 97 +++++++++++++++------- pytket/extensions/qiskit/qiskit_convert.py | 29 +++++-- 3 files changed, 94 insertions(+), 37 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 52238387..1dd4c385 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,11 @@ Changelog ~~~~~~~~~ +Unreleased +---------- + +* Add `IBMQBackend.default_compilation_pass_static` for offline compilation given config and props objects. + 0.51.1rc0 (April 2024) ---------------------- diff --git a/pytket/extensions/qiskit/backends/ibm.py b/pytket/extensions/qiskit/backends/ibm.py index 1970559f..96dd44ac 100644 --- a/pytket/extensions/qiskit/backends/ibm.py +++ b/pytket/extensions/qiskit/backends/ibm.py @@ -55,11 +55,11 @@ from pytket.backends.backendinfo import BackendInfo from pytket.backends.backendresult import BackendResult from pytket.backends.resulthandle import _ResultIdTuple -from pytket.extensions.qiskit.qiskit_convert import ( - process_characterisation, +from ..qiskit_convert import ( get_avg_characterisation, + process_characterisation_from_config, ) -from pytket.extensions.qiskit._metadata import __extension_version__ +from .._metadata import __extension_version__ from pytket.passes import ( BasePass, auto_rebase_pass, @@ -83,7 +83,9 @@ MaxNQubitsPredicate, Predicate, ) -from pytket.extensions.qiskit.qiskit_convert import tk_to_qiskit, _tk_gate_set +from qiskit.providers.models import BackendProperties, QasmBackendConfiguration # type: ignore + +from ..qiskit_convert import tk_to_qiskit, _tk_gate_set from pytket.architecture import FullyConnected from pytket.placement import NoiseAwarePlacement from pytket.utils import prepare_circuit @@ -190,11 +192,12 @@ def __init__( else provider ) self._backend: "_QiskIBMBackend" = self._provider.get_backend(backend_name) - config = self._backend.configuration() + config: QasmBackendConfiguration = self._backend.configuration() self._max_per_job = getattr(config, "max_experiments", 1) - gate_set = _tk_gate_set(self._backend) - self._backend_info = self._get_backend_info(self._backend) + gate_set = _tk_gate_set(config) + props: Optional[BackendProperties] = self._backend.properties() + self._backend_info = self._get_backend_info(config, props) self._service = QiskitRuntimeService( channel="ibm_quantum", token=token, instance=instance @@ -239,9 +242,21 @@ def backend_info(self) -> BackendInfo: return self._backend_info @classmethod - def _get_backend_info(cls, backend: "_QiskIBMBackend") -> BackendInfo: - config = backend.configuration() - characterisation = process_characterisation(backend) + def _get_backend_info( + cls, + config: QasmBackendConfiguration, + props: Optional[BackendProperties], + ) -> BackendInfo: + """Construct a BackendInfo from data returned by the IBMQ API. + + :param config: The configuration of this backend. + :type config: QasmBackendConfiguration + :param props: The measured properties of this backend (not required). + :type props: Optional[BackendProperties] + :return: Information about the backend. + :rtype: BackendInfo + """ + characterisation = process_characterisation_from_config(config, props) averaged_errors = get_avg_characterisation(characterisation) characterisation_keys = [ "t1times", @@ -270,10 +285,10 @@ def _get_backend_info(cls, backend: "_QiskIBMBackend") -> BackendInfo: hasattr(config, "supported_instructions") and "reset" in config.supported_instructions ) - gate_set = _tk_gate_set(backend) + gate_set = _tk_gate_set(config) backend_info = BackendInfo( cls.__name__, - backend.name, + config.backend_name, __extension_version__, arch, ( @@ -310,9 +325,12 @@ def available_devices(cls, **kwargs: Any) -> List[BackendInfo]: else: provider = IBMProvider() - backend_info_list = [ - cls._get_backend_info(backend) for backend in provider.backends() - ] + backend_info_list = [] + for backend in provider.backends(): + config = backend.configuration() + props = backend.properties() + backend_info_list.append(cls._get_backend_info(config, props)) + return backend_info_list @property @@ -376,6 +394,23 @@ def default_compilation_pass( :return: Compilation pass guaranteeing required predicates. :rtype: BasePass """ + config: QasmBackendConfiguration = self._backend.configuration() + props: Optional[BackendProperties] = self._backend.properties() + return IBMQBackend.default_compilation_pass_offline( + config, props, optimisation_level, placement_options + ) + + @staticmethod + def default_compilation_pass_offline( + config: QasmBackendConfiguration, + props: Optional[BackendProperties], + optimisation_level: int = 2, + placement_options: Optional[Dict] = None, + ) -> BasePass: + backend_info = IBMQBackend._get_backend_info(config, props) + primitive_gates = _get_primitive_gates(_tk_gate_set(config)) + supports_rz = OpType.Rz in primitive_gates + assert optimisation_level in range(3) passlist = [DecomposeBoxes()] # If you make changes to the default_compilation_pass, @@ -383,33 +418,33 @@ def default_compilation_pass( # https://tket.quantinuum.com/extensions/pytket-qiskit/index.html#default-compilation # Edit this docs source file -> pytket-qiskit/docs/intro.txt if optimisation_level == 0: - if self._supports_rz: + if supports_rz: # If the Rz gate is unsupported then the rebase should be skipped # This prevents an error when compiling to the stabilizer backend # where no TK1 replacement can be found for the rebase. - passlist.append(self.rebase_pass()) + passlist.append(IBMQBackend.rebase_pass_offline(primitive_gates)) elif optimisation_level == 1: passlist.append(SynthesiseTket()) elif optimisation_level == 2: passlist.append(FullPeepholeOptimise()) - mid_measure = self._backend_info.supports_midcircuit_measurement - arch = self._backend_info.architecture + mid_measure = backend_info.supports_midcircuit_measurement + arch = backend_info.architecture assert arch is not None if not isinstance(arch, FullyConnected): if placement_options is not None: noise_aware_placement = NoiseAwarePlacement( arch, - self._backend_info.averaged_node_gate_errors, # type: ignore - self._backend_info.averaged_edge_gate_errors, # type: ignore - self._backend_info.averaged_readout_errors, # type: ignore + backend_info.averaged_node_gate_errors, # type: ignore + backend_info.averaged_edge_gate_errors, # type: ignore + backend_info.averaged_readout_errors, # type: ignore **placement_options, ) else: noise_aware_placement = NoiseAwarePlacement( arch, - self._backend_info.averaged_node_gate_errors, # type: ignore - self._backend_info.averaged_edge_gate_errors, # type: ignore - self._backend_info.averaged_readout_errors, # type: ignore + backend_info.averaged_node_gate_errors, # type: ignore + backend_info.averaged_edge_gate_errors, # type: ignore + backend_info.averaged_readout_errors, # type: ignore ) passlist.append( @@ -432,8 +467,10 @@ def default_compilation_pass( ] ) - if self._supports_rz: - passlist.extend([self.rebase_pass(), RemoveRedundancies()]) + if supports_rz: + passlist.extend( + [IBMQBackend.rebase_pass_offline(primitive_gates), RemoveRedundancies()] + ) return SequencePass(passlist) @property @@ -442,7 +479,11 @@ def _result_id_type(self) -> _ResultIdTuple: return (str, int, int, str) def rebase_pass(self) -> BasePass: - return auto_rebase_pass(self._primitive_gates) + return IBMQBackend.rebase_pass_offline(self._primitive_gates) + + @staticmethod + def rebase_pass_offline(primitive_gates: set[OpType]) -> BasePass: + return auto_rebase_pass(primitive_gates) def process_circuits( self, diff --git a/pytket/extensions/qiskit/qiskit_convert.py b/pytket/extensions/qiskit/qiskit_convert.py index f773de8d..53454e08 100644 --- a/pytket/extensions/qiskit/qiskit_convert.py +++ b/pytket/extensions/qiskit/qiskit_convert.py @@ -82,15 +82,13 @@ from pytket.pauli import Pauli, QubitPauliString from pytket.architecture import Architecture, FullyConnected from pytket.utils import QubitPauliOperator, gen_term_sequence_circuit +from qiskit.providers.models import BackendProperties, QasmBackendConfiguration # type: ignore from pytket.passes import RebaseCustom if TYPE_CHECKING: from qiskit.providers.backend import BackendV1 as QiskitBackend # type: ignore - from qiskit.providers.models.backendproperties import ( # type: ignore - BackendProperties, - Nduv, - ) + from qiskit.providers.models.backendproperties import Nduv # type: ignore from qiskit.circuit.quantumcircuitdata import QuantumCircuitData # type: ignore from pytket.circuit import Op, UnitID @@ -209,9 +207,8 @@ _gate_str_2_optype_rev[OpType.Unitary1qBox] = "unitary" -def _tk_gate_set(backend: "QiskitBackend") -> Set[OpType]: +def _tk_gate_set(config: QasmBackendConfiguration) -> Set[OpType]: """Set of tket gate types supported by the qiskit backend""" - config = backend.configuration() if config.simulator: gate_set = { _gate_str_2_optype[gate_str] @@ -872,10 +869,25 @@ def process_characterisation(backend: "QiskitBackend") -> Dict[str, Any]: :return: A dictionary containing device characteristics :rtype: dict """ + config = backend.configuration() + props = backend.properties() + return process_characterisation_from_config(config, props) - # TODO explicitly check for and separate 1 and 2 qubit gates - properties = cast("BackendProperties", backend.properties()) +def process_characterisation_from_config( + config: QasmBackendConfiguration, properties: Optional[BackendProperties] +) -> Dict[str, Any]: + """Obtain a dictionary containing device Characteristics given config and props. + + :param config: A IBMQ configuration object + :type config: QasmBackendConfiguration + :param properties: An optional IBMQ properties object + :type properties: Optional[BackendProperties] + :return: A dictionary containing device characteristics + :rtype: dict + """ + + # TODO explicitly check for and separate 1 and 2 qubit gates def return_value_if_found(iterator: Iterable["Nduv"], name: str) -> Optional[Any]: try: first_found = next(filter(lambda item: item.name == name, iterator)) @@ -885,7 +897,6 @@ def return_value_if_found(iterator: Iterable["Nduv"], name: str) -> Optional[Any return first_found.value return None - config = backend.configuration() coupling_map = config.coupling_map n_qubits = config.n_qubits if coupling_map is None: