diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 31fa08a2..1f4d229a 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -159,4 +159,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4.0.4 + uses: actions/deploy-pages@v4.0.5 diff --git a/_metadata.py b/_metadata.py index 1c493e6d..9fb38135 100644 --- a/_metadata.py +++ b/_metadata.py @@ -1,2 +1,2 @@ -__extension_version__ = "0.51.0" +__extension_version__ = "0.52.0" __extension_name__ = "pytket-qiskit" diff --git a/docs/changelog.rst b/docs/changelog.rst index a01f69d5..de2dfec4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,16 @@ Changelog ~~~~~~~~~ +0.52.0 (April 2024) +------------------- + +* Update pytket version requirement to 1.26. +* Update qiskit-aer version requirement to 0.14. +* Update conversion to qiskit to use symengine for symbolic circuits +* Add `IBMQBackend.default_compilation_pass_offline` for offline compilation given config and props objects. +* Add `DirectednessPredicate` to IBMQBackend +* Default compilation pass of IBMQBackend will keep ECR gates in the direction required by the backend. + 0.51.0 (March 2024) ------------------- diff --git a/pytket/extensions/qiskit/backends/ibm.py b/pytket/extensions/qiskit/backends/ibm.py index 1970559f..1f3b948d 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, @@ -82,9 +82,12 @@ NoFastFeedforwardPredicate, MaxNQubitsPredicate, Predicate, + DirectednessPredicate, ) -from pytket.extensions.qiskit.qiskit_convert import tk_to_qiskit, _tk_gate_set -from pytket.architecture import FullyConnected +from qiskit.providers.models import BackendProperties, QasmBackendConfiguration # type: ignore + +from ..qiskit_convert import tk_to_qiskit, _tk_gate_set +from pytket.architecture import FullyConnected, Architecture from pytket.placement import NoiseAwarePlacement from pytket.utils import prepare_circuit from pytket.utils.outcomearray import OutcomeArray @@ -190,11 +193,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 +243,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 +286,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 +326,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 @@ -328,17 +347,16 @@ def required_predicates(self) -> List[Predicate]: ) ), ] + if isinstance(self.backend_info.architecture, Architecture): + predicates.append(DirectednessPredicate(self.backend_info.architecture)) + mid_measure = self._backend_info.supports_midcircuit_measurement fast_feedforward = self._backend_info.supports_fast_feedforward if not mid_measure: - predicates = [ - NoClassicalControlPredicate(), - NoMidMeasurePredicate(), - ] + predicates + predicates.append(NoClassicalControlPredicate()) + predicates.append(NoMidMeasurePredicate()) if not fast_feedforward: - predicates = [ - NoFastFeedforwardPredicate(), - ] + predicates + predicates.append(NoFastFeedforwardPredicate()) return predicates def default_compilation_pass( @@ -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,40 +418,40 @@ 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( CXMappingPass( arch, noise_aware_placement, - directed_cx=False, + directed_cx=True, delay_measures=(not mid_measure), ) ) @@ -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 6aab6fd5..53454e08 100644 --- a/pytket/extensions/qiskit/qiskit_convert.py +++ b/pytket/extensions/qiskit/qiskit_convert.py @@ -34,6 +34,7 @@ from uuid import UUID import numpy as np +from symengine import sympify # type: ignore import sympy import qiskit.circuit.library.standard_gates as qiskit_gates # type: ignore @@ -81,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 @@ -208,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] @@ -580,7 +578,7 @@ def param_to_qiskit( if len(ppi.free_symbols) == 0: return float(ppi.evalf()) else: - return ParameterExpression(symb_map, ppi) + return ParameterExpression(symb_map, sympify(ppi)) def _get_params( @@ -724,7 +722,7 @@ def append_tk_command_to_qiskit( if optype == OpType.TK1: params = _get_params(op, symb_map) - half = ParameterExpression(symb_map, sympy.pi / 2) + half = ParameterExpression(symb_map, sympify(sympy.pi / 2)) qcirc.global_phase += -params[0] / 2 - params[2] / 2 return qcirc.append( qiskit_gates.UGate(params[1], params[0] - half, params[2] + half), @@ -749,7 +747,7 @@ def append_tk_command_to_qiskit( if type(phase) == float: qcirc.global_phase += phase * np.pi else: - qcirc.global_phase += phase * sympy.pi + qcirc.global_phase += sympify(phase * sympy.pi) return qcirc.append(g, qargs=qargs) @@ -871,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)) @@ -884,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: diff --git a/setup.py b/setup.py index e4fe4b6d..1baa5bbe 100644 --- a/setup.py +++ b/setup.py @@ -44,11 +44,11 @@ packages=find_namespace_packages(include=["pytket.*"]), include_package_data=True, install_requires=[ - "pytket ~= 1.25", + "pytket ~= 1.26", "qiskit ~= 1.0", "qiskit-algorithms ~= 0.3.0", "qiskit-ibm-runtime ~= 0.22.0", - "qiskit-aer ~= 0.13.3", + "qiskit-aer ~= 0.14.0", "qiskit-ibm-provider ~= 0.10.0", "numpy", ], diff --git a/tests/backend_test.py b/tests/backend_test.py index 88514d5f..e82de3c1 100644 --- a/tests/backend_test.py +++ b/tests/backend_test.py @@ -476,7 +476,8 @@ def test_nshots( circuit = Circuit(1).X(0) circuit.measure_all() n_shots = [1, 2, 3] - results = b.get_results(b.process_circuits([circuit] * 3, n_shots=n_shots)) + circ_comp = b.get_compiled_circuit(circuit) + results = b.get_results(b.process_circuits([circ_comp] * 3, n_shots=n_shots)) assert [sum(r.get_counts().values()) for r in results] == n_shots @@ -1330,6 +1331,21 @@ def test_crosstalk_noise_model() -> None: res.get_counts() +@pytest.mark.skipif(skip_remote_tests, reason=REASON) +def test_ecr(ibm_brisbane_backend: IBMQBackend) -> None: + ghz5 = Circuit(5) + ghz5.H(0).CX(0, 1).CX(0, 2).CX(0, 3).CX(0, 4) + ghz5.measure_all() + ibm_ghz5 = ibm_brisbane_backend.get_compiled_circuit(ghz5) + + compiled_ghz5 = ibm_brisbane_backend.get_compiled_circuit(ibm_ghz5) + + ibm_brisbane_backend.valid_circuit(compiled_ghz5) + + handle = ibm_brisbane_backend.process_circuit(compiled_ghz5, n_shots=1000) + ibm_brisbane_backend.cancel(handle) + + # helper function for testing def _get_qiskit_statevector(qc: QuantumCircuit) -> np.ndarray: """Given a QuantumCircuit, use aer_simulator_statevector to compute its diff --git a/tests/qiskit_convert_test.py b/tests/qiskit_convert_test.py index 81bf258a..f48d2d60 100644 --- a/tests/qiskit_convert_test.py +++ b/tests/qiskit_convert_test.py @@ -35,6 +35,9 @@ from qiskit.circuit.equivalence_library import StandardEquivalenceLibrary # type: ignore from qiskit_ibm_runtime.fake_provider import FakeGuadalupe # type: ignore from qiskit.circuit.parameterexpression import ParameterExpression # type: ignore +from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit +from qiskit.circuit.library import TwoLocal +from qiskit import transpile from pytket.circuit import ( Circuit, @@ -1091,3 +1094,24 @@ def test_RealAmplitudes_numeric_params() -> None: unitary2 = tkc2.get_unitary() assert compare_unitaries(qiskit_unitary, unitary1) assert compare_unitaries(unitary1, unitary2) + + +# https://github.com/CQCL/pytket-qiskit/issues/256 +def test_symbolic_param_conv() -> None: + qc = TwoLocal(1, "ry", "cz", reps=1, entanglement="linear") + qc_transpiled = transpile( + qc, basis_gates=["sx", "rz", "cx", "x"], optimization_level=3 + ) + + tket_qc = qiskit_to_tk(qc_transpiled) + CliffordSimp().apply(tket_qc) + transformed_qc = tk_to_qiskit(tket_qc) + + qc_transpiled_again = transpile(transformed_qc, basis_gates=["sx", "rz", "cx", "x"]) + + qc_transpiled_again = qc_transpiled_again.assign_parameters( + { + qc_transpiled_again.parameters[i]: 0 + for i in range(len(qc_transpiled_again.parameters)) + } + )