diff --git a/docs/changelog.md b/docs/changelog.md index 3ea66662..e6529ed1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,7 +6,9 @@ ## Unreleased -- Fix handling of non-default registers when selecting bits in results. +- Fix handling of non-default registers when selecting bits in results. +- Update default compilation to use `Qiskit` `SabreLayoutPassManager` as a `CustomPass`. +- Fix handling of non-default registers when selecting bits in results. - The {py:func}`tk_to_qiskit` converter gives a warning if the input {py:class}`~pytket.circuit.Circuit` contains [implicit qubit permutations](https://docs.quantinuum.com/tket/user-guide/manual/manual_circuit.html#implicit-qubit-permutations). ## 0.58.0 (October 2024) diff --git a/docs/index.md b/docs/index.md index 8911076e..846f6fb6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -205,12 +205,9 @@ Every {py:class}`~pytket.backends.backend.Backend` in pytket has its own {py:met * - [AutoRebase [2]](inv:#*.AutoRebase) - [SynthesiseTket](inv:#*.SynthesiseTket) - [FullPeepholeOptimise](inv:#*.passes.FullPeepholeOptimise) -* - [CXMappingPass [3]](inv:#*.passes.CXMappingPass) - - [CXMappingPass [3]](inv:#*.passes.CXMappingPass) - - [CXMappingPass [3]](inv:#*.passes.CXMappingPass) -* - [NaivePlacementPass](inv:#*.passes.NaivePlacementPass) - - [NaivePlacementPass](inv:#*.passes.NaivePlacementPass) - - [NaivePlacementPass](inv:#*.passes.NaivePlacementPass) +* - LightSabre [3] + - LightSabre [3] + - LightSabre [3] * - [AutoRebase [2]](inv:#*.AutoRebase) - [SynthesiseTket](inv:#*.SynthesiseTket) - [KAKDecomposition(allow_swaps=False)](inv:#*.passes.KAKDecomposition) @@ -231,7 +228,7 @@ Every {py:class}`~pytket.backends.backend.Backend` in pytket has its own {py:met - \[1\] If no value is specified then `optimisation_level` defaults to a value of 2. - \[2\] {py:class}`~pytket._tket.passes.AutoRebase` is a conversion to the gateset supported by the backend. For IBM quantum devices and emulators the supported gate set is either $\{X, SX, Rz, CX\}$, $\{X, SX, Rz, ECR\}$, or $\{X, SX, Rz, CZ\}$. The more idealised Aer simulators have a much broader range of supported gates. -- \[3\] Here [CXMappingPass](inv:#*.passes.CXMappingPass) maps program qubits to the architecture using a [NoiseAwarePlacement](inv:#*.NoiseAwarePlacement) +- \[3\] This is imported from qiskit and corresponds to the method in "LightSABRE: A Lightweight and Enhanced SABRE Algorithm", Henry Zou, Matthew Treinish, Kevin Hartman, Alexander Ivrii, Jake Lishman, arXiv:2409.08368. **Note:** The {py:meth}`~AerBackend.default_compilation_pass` for {py:class}`AerBackend` is the same as above. diff --git a/docs/pytket-docs-theming b/docs/pytket-docs-theming index 45cc4e49..cfbe34c4 160000 --- a/docs/pytket-docs-theming +++ b/docs/pytket-docs-theming @@ -1 +1 @@ -Subproject commit 45cc4e49f473905984b99077e8739fe18e69595e +Subproject commit cfbe34c48f88c56085b8ef65f640d0108b8a9fa6 diff --git a/pytket/extensions/qiskit/backends/aer.py b/pytket/extensions/qiskit/backends/aer.py index 09535720..68807957 100644 --- a/pytket/extensions/qiskit/backends/aer.py +++ b/pytket/extensions/qiskit/backends/aer.py @@ -35,7 +35,7 @@ AutoRebase, BasePass, CliffordSimp, - CXMappingPass, + CustomPass, DecomposeBoxes, FullPeepholeOptimise, NaivePlacementPass, @@ -43,7 +43,6 @@ SynthesiseTket, ) from pytket.pauli import Pauli, QubitPauliString -from pytket.placement import NoiseAwarePlacement from pytket.predicates import ( ConnectivityPredicate, DefaultRegisterPredicate, @@ -73,7 +72,7 @@ CrosstalkParams, NoisyCircuitBuilder, ) -from .ibm_utils import _STATUS_MAP, _batch_circuits +from .ibm_utils import _STATUS_MAP, _batch_circuits, _gen_lightsabre_transformation if TYPE_CHECKING: from qiskit_aer import AerJob @@ -164,32 +163,11 @@ def _arch_dependent_default_compilation_pass( self, arch: Architecture, optimisation_level: int = 2, - placement_options: Optional[dict[str, Any]] = None, ) -> BasePass: assert optimisation_level in range(3) - 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 - **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 - ) - arch_specific_passes = [ - CXMappingPass( - arch, - noise_aware_placement, - directed_cx=True, - delay_measures=False, - ), + AutoRebase({OpType.CX, OpType.TK1}), + CustomPass(_gen_lightsabre_transformation(arch, optimisation_level)), NaivePlacementPass(arch), ] if optimisation_level == 0: @@ -199,7 +177,8 @@ def _arch_dependent_default_compilation_pass( self.rebase_pass(), *arch_specific_passes, self.rebase_pass(), - ] + ], + False, ) if optimisation_level == 1: return SequencePass( @@ -208,7 +187,8 @@ def _arch_dependent_default_compilation_pass( SynthesiseTket(), *arch_specific_passes, SynthesiseTket(), - ] + ], + False, ) return SequencePass( [ @@ -217,7 +197,8 @@ def _arch_dependent_default_compilation_pass( *arch_specific_passes, CliffordSimp(False), SynthesiseTket(), - ] + ], + False, ) def _arch_independent_default_compilation_pass( @@ -233,7 +214,6 @@ def _arch_independent_default_compilation_pass( def default_compilation_pass( self, optimisation_level: int = 2, - placement_options: Optional[dict[str, Any]] = None, ) -> BasePass: """ See documentation for :py:meth:`IBMQBackend.default_compilation_pass`. @@ -245,7 +225,8 @@ def default_compilation_pass( and self._backend_info.get_misc("characterisation") ): return self._arch_dependent_default_compilation_pass( - arch, optimisation_level, placement_options=placement_options # type: ignore + arch, # type: ignore + optimisation_level, ) return self._arch_independent_default_compilation_pass(optimisation_level) diff --git a/pytket/extensions/qiskit/backends/ibm.py b/pytket/extensions/qiskit/backends/ibm.py index fa48f400..83b8f5e9 100644 --- a/pytket/extensions/qiskit/backends/ibm.py +++ b/pytket/extensions/qiskit/backends/ibm.py @@ -51,7 +51,7 @@ AutoRebase, BasePass, CliffordSimp, - CXMappingPass, + CustomPass, DecomposeBoxes, FullPeepholeOptimise, KAKDecomposition, @@ -61,7 +61,6 @@ SimplifyInitial, SynthesiseTket, ) -from pytket.placement import NoiseAwarePlacement from pytket.predicates import ( DirectednessPredicate, GateSetPredicate, @@ -94,7 +93,7 @@ tk_to_qiskit, ) from .config import QiskitConfig -from .ibm_utils import _STATUS_MAP, _batch_circuits +from .ibm_utils import _STATUS_MAP, _batch_circuits, _gen_lightsabre_transformation if TYPE_CHECKING: from qiskit_ibm_runtime.ibm_backend import IBMBackend # type: ignore @@ -345,7 +344,6 @@ def required_predicates(self) -> list[Predicate]: def default_compilation_pass( self, optimisation_level: int = 2, - placement_options: Optional[dict[str, Any]] = None, ) -> BasePass: """ A suggested compilation pass that will will, if possible, produce an equivalent @@ -359,8 +357,6 @@ def default_compilation_pass( is tailored to the backend's requirements. The default compilation passes for the :py:class:`IBMQBackend` and the - Aer simulators support an optional ``placement_options`` dictionary containing - arguments to override the default settings in :py:class:`NoiseAwarePlacement`. :param optimisation_level: The level of optimisation to perform during compilation. @@ -372,14 +368,12 @@ def default_compilation_pass( that should give the best results from execution. - :param placement_options: Optional argument allowing the user to override - the default settings in :py:class:`NoiseAwarePlacement`. :return: Compilation pass guaranteeing required predicates. """ config: PulseBackendConfiguration = self._backend.configuration() props: Optional[BackendProperties] = self._backend.properties() return IBMQBackend.default_compilation_pass_offline( - config, props, optimisation_level, placement_options + config, props, optimisation_level ) @staticmethod @@ -387,7 +381,6 @@ def default_compilation_pass_offline( config: PulseBackendConfiguration, props: Optional[BackendProperties], optimisation_level: int = 2, - placement_options: Optional[dict[str, Any]] = None, ) -> BasePass: backend_info = IBMQBackend._get_backend_info(config, props) primitive_gates = _get_primitive_gates(_tk_gate_set(config)) @@ -409,33 +402,12 @@ def default_compilation_pass_offline( passlist.append(SynthesiseTket()) elif optimisation_level == 2: passlist.append(FullPeepholeOptimise()) - 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, - 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, - 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(AutoRebase(primitive_gates)) passlist.append( - CXMappingPass( - arch, - noise_aware_placement, - directed_cx=True, - delay_measures=(not mid_measure), - ) + CustomPass(_gen_lightsabre_transformation(arch, optimisation_level)) ) passlist.append(NaivePlacementPass(arch)) if optimisation_level == 1: @@ -449,11 +421,10 @@ def default_compilation_pass_offline( ] ) - if supports_rz: - passlist.extend( - [IBMQBackend.rebase_pass_offline(primitive_gates), RemoveRedundancies()] - ) - return SequencePass(passlist) + passlist.extend( + [IBMQBackend.rebase_pass_offline(primitive_gates), RemoveRedundancies()] + ) + return SequencePass(passlist, False) @property def _result_id_type(self) -> _ResultIdTuple: diff --git a/pytket/extensions/qiskit/backends/ibm_utils.py b/pytket/extensions/qiskit/backends/ibm_utils.py index 08055c42..5d2850d8 100644 --- a/pytket/extensions/qiskit/backends/ibm_utils.py +++ b/pytket/extensions/qiskit/backends/ibm_utils.py @@ -12,20 +12,27 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Shared utility methods for ibm backends. -""" +"""Shared utility methods for ibm backends.""" import itertools from collections.abc import Collection, Sequence -from typing import TYPE_CHECKING, Optional +from typing import Callable, Optional import numpy as np +from pytket.architecture import Architecture from pytket.backends.status import StatusEnum +from pytket.circuit import Circuit, Node +from pytket.passes import RebaseTket +from pytket.transform import Transform +from qiskit.passmanager.flow_controllers import ConditionalController # type: ignore from qiskit.providers import JobStatus # type: ignore +from qiskit.transpiler import CouplingMap, PassManager # type: ignore +from qiskit.transpiler.passes import SabreLayout, SetLayout # type: ignore +from qiskit.transpiler.passmanager_config import PassManagerConfig # type: ignore +from qiskit.transpiler.preset_passmanagers import common # type: ignore -if TYPE_CHECKING: - from pytket.circuit import Circuit +from ..qiskit_convert import qiskit_to_tk, tk_to_qiskit _STATUS_MAP = { JobStatus.CANCELLED: StatusEnum.CANCELLED, @@ -70,3 +77,81 @@ def _batch_circuits( for n, indices in itertools.groupby(order, key=lambda i: n_shots[i]) ] return batches, batch_order + + +def _architecture_to_couplingmap(architecture: Architecture) -> CouplingMap: + """ + Converts a pytket Architecture object to a Qiskit CouplingMap object. + + :param architecture: Architecture to be converted + :return: A Qiskit CouplingMap object corresponding to the same connectivity + """ + # we can make some assumptions from how the Architecture object is + # originally constructed from the Qiskit CouplingMap: + # 1) All nodes are single indexed + # 2) All nodes are default register + # 3) Node with index "i" corresponds to integer "i" in the original coupling map + # We confirm assumption 1) and 2) while producing the coupling map + coupling_map: list[tuple[int, int]] = [] + for edge in architecture.coupling: + assert len(edge[0].index) == 1 + assert len(edge[1].index) == 1 + assert edge[0].reg_name == "node" + assert edge[1].reg_name == "node" + coupling_map.append((edge[0].index[0], edge[1].index[0])) + return CouplingMap(coupling_map) + + +def _gen_lightsabre_transformation( # type: ignore + architecture: Architecture, optimization_level: int = 2, seed=0, attempts=20 +) -> Callable[[Circuit], Circuit]: + """ + Generates a function that can be passed to CustomPass for running + LightSABRE routing. + + :param architecture: Architecture LightSABRE routes circuits to match + :param optimization_level: Corresponds to qiskit optmization levels + :param seed: LightSABRE routing is stochastic, with this parameter setting the seed + :param attempts: Number of generated random solutions to pick from. + :return: A function that accepts a pytket Circuit and returns a new Circuit that + has been routed to the architecture using LightSABRE + """ + config: PassManagerConfig = PassManagerConfig( + coupling_map=_architecture_to_couplingmap(architecture), + routing_method="sabre", + seed_transpiler=seed, + ) + sabre_pass: PassManager = PassManager( + [ + SetLayout(config.initial_layout), + ConditionalController( + [ + SabreLayout( + config.coupling_map, + max_iterations=2, + seed=config.seed_transpiler, + swap_trials=attempts, + layout_trials=attempts, + skip_routing=False, + ) + ], + condition=lambda property_set: not property_set["layout"], + ), + ConditionalController( + common.generate_embed_passmanager( + config.coupling_map + ).to_flow_controller(), + condition=lambda property_set: property_set["final_layout"] is None, + ), + ] + ) + + def lightsabre(circuit: Circuit) -> Circuit: + c: Circuit = qiskit_to_tk(sabre_pass.run(tk_to_qiskit(circuit))) + c.remove_blank_wires() + c.rename_units({q: Node(q.index[0]) for q in c.qubits}) + RebaseTket().apply(c) + Transform.DecomposeCXDirected(architecture).apply(c) + return c + + return lightsabre diff --git a/pytket/extensions/qiskit/backends/ibmq_emulator.py b/pytket/extensions/qiskit/backends/ibmq_emulator.py index ed33ab25..1d213be6 100644 --- a/pytket/extensions/qiskit/backends/ibmq_emulator.py +++ b/pytket/extensions/qiskit/backends/ibmq_emulator.py @@ -15,7 +15,6 @@ from collections.abc import Sequence from typing import ( TYPE_CHECKING, - Any, Optional, ) @@ -88,13 +87,12 @@ def required_predicates(self) -> list[Predicate]: def default_compilation_pass( self, optimisation_level: int = 2, - placement_options: Optional[dict[str, Any]] = None, ) -> BasePass: """ See documentation for :py:meth:`IBMQBackend.default_compilation_pass`. """ return self._ibmq.default_compilation_pass( - optimisation_level=optimisation_level, placement_options=placement_options + optimisation_level=optimisation_level ) @property diff --git a/pytket/extensions/qiskit/qiskit_convert.py b/pytket/extensions/qiskit/qiskit_convert.py index 51924e17..a214e381 100644 --- a/pytket/extensions/qiskit/qiskit_convert.py +++ b/pytket/extensions/qiskit/qiskit_convert.py @@ -756,7 +756,13 @@ def append_tk_command_to_qiskit( if a.reg_name != regname: raise NotImplementedError("Conditions can only use a single register") instruction = append_tk_command_to_qiskit( - op.op, args[width:], qcirc, qregmap, cregmap, symb_map, range_preds # type: ignore + op.op, # type: ignore + args[width:], + qcirc, + qregmap, + cregmap, + symb_map, + range_preds, ) if len(cregmap[regname]) == width: for i, a in enumerate(args[:width]): @@ -1051,7 +1057,7 @@ def return_value_if_found(iterator: Iterable["Nduv"], name: str) -> Optional[Any def get_avg_characterisation( - characterisation: dict[str, Any] + characterisation: dict[str, Any], ) -> dict[str, dict[Node, float]]: """ Convert gate-specific characterisation into readout, one- and two-qubit errors diff --git a/tests/backend_test.py b/tests/backend_test.py index 7c165329..8dc4104c 100644 --- a/tests/backend_test.py +++ b/tests/backend_test.py @@ -67,7 +67,11 @@ from pytket.mapping import LexiLabellingMethod, LexiRouteRoutingMethod, MappingManager from pytket.passes import CliffordSimp, FlattenRelabelRegistersPass from pytket.pauli import Pauli, QubitPauliString -from pytket.predicates import CompilationUnit, NoMidMeasurePredicate +from pytket.predicates import ( + CompilationUnit, + ConnectivityPredicate, + NoMidMeasurePredicate, +) from pytket.transform import Transform from pytket.utils.expectations import ( get_operator_expectation_value, @@ -990,6 +994,7 @@ def lift_perm(p: dict[int, int]) -> np.ndarray: @pytest.mark.skipif(skip_remote_tests, reason=REASON) def test_compilation_correctness(brisbane_backend: IBMQBackend) -> None: + # of routing c = Circuit(7) c.H(0).H(1).H(2) c.CX(0, 1).CX(1, 2) @@ -1002,30 +1007,16 @@ def test_compilation_correctness(brisbane_backend: IBMQBackend) -> None: c.Rz(0.125, 2).X(2).Rz(0.25, 2) c.SX(3).Rz(0.125, 3).SX(3) c.CX(0, 3).CX(0, 4) - u_backend = AerUnitaryBackend() c.remove_blank_wires() FlattenRelabelRegistersPass().apply(c) - u = u_backend.run_circuit(c).get_unitary() - ibm_backend = brisbane_backend + c_pred = ConnectivityPredicate( + cast(Architecture, brisbane_backend.backend_info.architecture) + ) for ol in range(3): - p = ibm_backend.default_compilation_pass(optimisation_level=ol) + p = brisbane_backend.default_compilation_pass(optimisation_level=ol) cu = CompilationUnit(c) p.apply(cu) - FlattenRelabelRegistersPass().apply(cu) - c1 = cu.circuit - compiled_u = u_backend.run_circuit(c1).get_unitary() - - # Adjust for placement - imap = cu.initial_map - fmap = cu.final_map - c_idx = {c.qubits[i]: i for i in range(5)} - c1_idx = {c1.qubits[i]: i for i in range(5)} - ini = {c_idx[qb]: c1_idx[node] for qb, node in imap.items()} # type: ignore - inv_fin = {c1_idx[node]: c_idx[qb] for qb, node in fmap.items()} # type: ignore - m_ini = lift_perm(ini) - m_inv_fin = lift_perm(inv_fin) - - assert compare_statevectors(u[:, 0], (m_inv_fin @ compiled_u @ m_ini)[:, 0]) + assert c_pred.verify(cu.circuit) # pytket-extensions issue #69 @@ -1469,7 +1460,6 @@ def test_noiseless_density_matrix_simulation() -> None: # https://github.com/CQCL/pytket-qiskit/issues/231 def test_noisy_density_matrix_simulation() -> None: - # Test that __init__ works with a very simple noise model noise_model = NoiseModel() noise_model.add_quantum_error(depolarizing_error(0.6, 2), ["cz"], [0, 1])