From d4e5bbc4d38d61e8c57cab2716c6349e733b35c2 Mon Sep 17 00:00:00 2001 From: "Daniel J. Egger" <38065505+eggerdj@users.noreply.github.com> Date: Wed, 11 May 2022 03:57:18 +0200 Subject: [PATCH] Follow-up on routing commuting 2q gates (#7979) * * Renamed directory. * * Typo. * * Commuting2qBlocks name * * logical -> virtual. * * from_line in the example. * * Reno name fix. * * Reno rewording. * * removed unnecessary line in example. * * Black. * * Started designing test on non-line graph. * * Added test on a T device. * * Renamed Communting2qBlocks to Commuting2qBlock * * Made test robust to arbitraryness of commuting gate order. * Added test. * * Added test and black. * * Fix missing gate test exception refactor. * * Bug fix with test. * * Removed SwapStrategy from the passes init. * * black --- qiskit/transpiler/passes/__init__.py | 2 - qiskit/transpiler/passes/routing/__init__.py | 4 +- .../__init__.py | 0 .../commuting_2q_block.py | 2 +- .../commuting_2q_gate_router.py | 39 ++--- .../pauli_2q_evolution_commutation.py | 11 +- .../swap_strategy.py | 0 .../swap-strategies-3ab013ca60f02b36.yaml | 15 +- test/python/circuit/test_gate_definitions.py | 2 +- test/python/transpiler/test_swap_strategy.py | 15 +- .../transpiler/test_swap_strategy_router.py | 159 +++++++++++++++++- 11 files changed, 210 insertions(+), 39 deletions(-) rename qiskit/transpiler/passes/routing/{swap_strategies => commuting_2q_gate_routing}/__init__.py (100%) rename qiskit/transpiler/passes/routing/{swap_strategies => commuting_2q_gate_routing}/commuting_2q_block.py (98%) rename qiskit/transpiler/passes/routing/{swap_strategies => commuting_2q_gate_routing}/commuting_2q_gate_router.py (91%) rename qiskit/transpiler/passes/routing/{swap_strategies => commuting_2q_gate_routing}/pauli_2q_evolution_commutation.py (92%) rename qiskit/transpiler/passes/routing/{swap_strategies => commuting_2q_gate_routing}/swap_strategy.py (100%) diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 6b3decfefe2d..172ad7d9d769 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -46,7 +46,6 @@ StochasticSwap SabreSwap BIPMapping - SwapStrategy Commuting2qGateRouter Basis Change @@ -190,7 +189,6 @@ from .routing import StochasticSwap from .routing import SabreSwap from .routing import BIPMapping -from .routing import SwapStrategy from .routing import Commuting2qGateRouter # basis change diff --git a/qiskit/transpiler/passes/routing/__init__.py b/qiskit/transpiler/passes/routing/__init__.py index 48fa3c7e3bef..618b2dc6a9f1 100644 --- a/qiskit/transpiler/passes/routing/__init__.py +++ b/qiskit/transpiler/passes/routing/__init__.py @@ -18,5 +18,5 @@ from .stochastic_swap import StochasticSwap from .sabre_swap import SabreSwap from .bip_mapping import BIPMapping -from .swap_strategies.commuting_2q_gate_router import Commuting2qGateRouter -from .swap_strategies.swap_strategy import SwapStrategy +from .commuting_2q_gate_routing.commuting_2q_gate_router import Commuting2qGateRouter +from .commuting_2q_gate_routing.swap_strategy import SwapStrategy diff --git a/qiskit/transpiler/passes/routing/swap_strategies/__init__.py b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/__init__.py similarity index 100% rename from qiskit/transpiler/passes/routing/swap_strategies/__init__.py rename to qiskit/transpiler/passes/routing/commuting_2q_gate_routing/__init__.py diff --git a/qiskit/transpiler/passes/routing/swap_strategies/commuting_2q_block.py b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_block.py similarity index 98% rename from qiskit/transpiler/passes/routing/swap_strategies/commuting_2q_block.py rename to qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_block.py index 7f9760abfd01..d121b70baa3f 100644 --- a/qiskit/transpiler/passes/routing/swap_strategies/commuting_2q_block.py +++ b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_block.py @@ -19,7 +19,7 @@ from qiskit.dagcircuit import DAGOpNode -class Commuting2qBlocks(Gate): +class Commuting2qBlock(Gate): """A gate made of commuting two-qubit gates. This gate is intended for use with commuting swap strategies to make it convenient diff --git a/qiskit/transpiler/passes/routing/swap_strategies/commuting_2q_gate_router.py b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py similarity index 91% rename from qiskit/transpiler/passes/routing/swap_strategies/commuting_2q_gate_router.py rename to qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py index a0c66671f5f7..eb1c3bbcfa98 100644 --- a/qiskit/transpiler/passes/routing/swap_strategies/commuting_2q_gate_router.py +++ b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py @@ -20,15 +20,17 @@ from qiskit.dagcircuit import DAGCircuit, DAGOpNode from qiskit.transpiler import TransformationPass, Layout, TranspilerError -from qiskit.transpiler.passes.routing.swap_strategies.swap_strategy import SwapStrategy -from qiskit.transpiler.passes.routing.swap_strategies.commuting_2q_block import Commuting2qBlocks +from qiskit.transpiler.passes.routing.commuting_2q_gate_routing.swap_strategy import SwapStrategy +from qiskit.transpiler.passes.routing.commuting_2q_gate_routing.commuting_2q_block import ( + Commuting2qBlock, +) class Commuting2qGateRouter(TransformationPass): """A class to swap route one or more commuting gates to the coupling map. - This pass routs blocks of commuting two-qubit gates encapsulated as - :class:`.Commuting2qBlocks` instructions. This pass will not apply to other instructions. + This pass routes blocks of commuting two-qubit gates encapsulated as + :class:`.Commuting2qBlock` instructions. This pass will not apply to other instructions. The mapping to the coupling map is done using swap strategies, see :class:`.SwapStrategy`. The swap strategy should suit the problem and the coupling map. This transpiler pass should ideally be executed before the quantum circuit is enlarged with any idle ancilla @@ -48,7 +50,7 @@ class Commuting2qGateRouter(TransformationPass): 4 To do this we use a line swap strategy for qubits 0, 1, 3, and 4 defined it in terms - of logical qubits 0, 1, 2, and 3. + of virtual qubits 0, 1, 2, and 3. .. code-block:: python @@ -61,20 +63,19 @@ class Commuting2qGateRouter(TransformationPass): from qiskit.transpiler.passes import ApplyLayout from qiskit.transpiler.passes import SetLayout - from qiskit.transpiler.passes.routing.swap_strategies import ( + from qiskit.transpiler.passes.routing.commuting_2q_gate_routing import ( SwapStrategy, FindCommutingPauliEvolutions, Commuting2qGateRouter, ) - # Define the circuit on logical qubits + # Define the circuit on virtual qubits op = PauliSumOp.from_list([("IZZI", 1), ("ZIIZ", 2), ("ZIZI", 3)]) circ = QuantumCircuit(4) circ.append(PauliEvolutionGate(op, 1), range(4)) # Define the swap strategy on qubits before the initial_layout is applied. - swap_cmap = CouplingMap(couplinglist=[(0, 1), (1, 2), (2, 3)]) - swap_strat = SwapStrategy(swap_cmap, swap_layers=[[(0, 1), (2, 3)], [(1, 2)]]) + swap_strat = SwapStrategy.from_line([0, 1, 2, 3]) # Chose qubits 0, 1, 3, and 4 from the backend coupling map shown above. backend_cmap = CouplingMap(couplinglist=[(0, 1), (1, 2), (1, 3), (3, 4)]) @@ -156,7 +157,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: self._bit_indices = {bit: index for index, bit in enumerate(dag.qubits)} for node in dag.topological_op_nodes(): - if isinstance(node.op, Commuting2qBlocks): + if isinstance(node.op, Commuting2qBlock): # Check that the swap strategy creates enough connectivity for the node. self._check_edges(node, swap_strategy) @@ -201,15 +202,15 @@ def _compose_non_swap_nodes( return new_dag.copy_empty_like() def _position_in_cmap(self, j: int, k: int, layout: Layout) -> Tuple[int, ...]: - """A helper function to track the movement of logical qubits through the swaps. + """A helper function to track the movement of virtual qubits through the swaps. Args: - j: The index of decision variable j (i.e. logical qubit). - k: The index of decision variable k (i.e. logical qubit). + j: The index of decision variable j (i.e. virtual qubit). + k: The index of decision variable k (i.e. virtual qubit). layout: The current layout that takes into account previous swap gates. Returns: - The position in the coupling map of the logical qubits j and k as a tuple. + The position in the coupling map of the virtual qubits j and k as a tuple. """ bit0 = self._bit_indices[layout.get_physical_bits()[j]] bit1 = self._bit_indices[layout.get_physical_bits()[k]] @@ -254,20 +255,20 @@ def _build_sub_layers(current_layer: Dict[tuple, Gate]) -> List[Dict[tuple, Gate def swap_decompose( self, dag: DAGCircuit, node: DAGOpNode, current_layout: Layout, swap_strategy: SwapStrategy ) -> DAGCircuit: - """Take an instance of :class:`.Commuting2qBlocks` and map it to the coupling map. + """Take an instance of :class:`.Commuting2qBlock` and map it to the coupling map. The mapping is done with the swap strategy. Args: - dag: The dag which contains the :class:`.Commuting2qBlocks` we route. - node: A node whose operation is a :class:`.Commuting2qBlocks`. + dag: The dag which contains the :class:`.Commuting2qBlock` we route. + node: A node whose operation is a :class:`.Commuting2qBlock`. current_layout: The layout before the swaps are applied. This function will modify the layout so that subsequent gates can be properly composed on the dag. swap_strategy: The swap strategy used to decompose the node. Returns: A dag that is compatible with the coupling map where swap gates have been added - to map the gates in the :class:`.Commuting2qBlocks` to the hardware. + to map the gates in the :class:`.Commuting2qBlock` to the hardware. """ trivial_layout = Layout.generate_trivial_layout(*dag.qregs.values()) gate_layers = self._make_op_layers(dag, node.op, current_layout, swap_strategy) @@ -305,7 +306,7 @@ def swap_decompose( return circuit_to_dag(circuit_with_swap) def _make_op_layers( - self, dag: DAGCircuit, op: Commuting2qBlocks, layout: Layout, swap_strategy: SwapStrategy + self, dag: DAGCircuit, op: Commuting2qBlock, layout: Layout, swap_strategy: SwapStrategy ) -> Dict[int, Dict[tuple, Gate]]: """Creates layers of two-qubit gates based on the distance in the swap strategy.""" diff --git a/qiskit/transpiler/passes/routing/swap_strategies/pauli_2q_evolution_commutation.py b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/pauli_2q_evolution_commutation.py similarity index 92% rename from qiskit/transpiler/passes/routing/swap_strategies/pauli_2q_evolution_commutation.py rename to qiskit/transpiler/passes/routing/commuting_2q_gate_routing/pauli_2q_evolution_commutation.py index 914085ef72b1..8b92c5cdaf53 100644 --- a/qiskit/transpiler/passes/routing/swap_strategies/pauli_2q_evolution_commutation.py +++ b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/pauli_2q_evolution_commutation.py @@ -21,7 +21,9 @@ from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler import TransformationPass from qiskit.quantum_info import SparsePauliOp, Pauli -from qiskit.transpiler.passes.routing.swap_strategies.commuting_2q_block import Commuting2qBlocks +from qiskit.transpiler.passes.routing.commuting_2q_gate_routing.commuting_2q_block import ( + Commuting2qBlock, +) class FindCommutingPauliEvolutions(TransformationPass): @@ -48,9 +50,8 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if self.summands_commute(node.op.operator): sub_dag = self._decompose_to_2q(dag, node.op) - block_op = Commuting2qBlocks(set(sub_dag.op_nodes())) - register = dag.qregs["q"] - wire_order = {qubit: register.index(qubit) for qubit in block_op.qubits} + block_op = Commuting2qBlock(set(sub_dag.op_nodes())) + wire_order = {wire: idx for idx, wire in enumerate(dag.qubits)} dag.replace_block_with_op([node], block_op, wire_order) return dag @@ -99,7 +100,7 @@ def _pauli_to_edge(pauli: Pauli) -> Tuple[int, ...]: Returns: A tuple representing where the Paulis are. For example, the Pauli "IZIZ" will - return (0, 2) since logical qubits 0 and 2 interact. + return (0, 2) since virtual qubits 0 and 2 interact. Raises: QiskitError: If the pauli does not exactly have two non-identity terms. diff --git a/qiskit/transpiler/passes/routing/swap_strategies/swap_strategy.py b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/swap_strategy.py similarity index 100% rename from qiskit/transpiler/passes/routing/swap_strategies/swap_strategy.py rename to qiskit/transpiler/passes/routing/commuting_2q_gate_routing/swap_strategy.py diff --git a/releasenotes/notes/swap-strategies-3ab013ca60f02b36.yaml b/releasenotes/notes/swap-strategies-3ab013ca60f02b36.yaml index 3d153e349d31..ed849df1ccc0 100644 --- a/releasenotes/notes/swap-strategies-3ab013ca60f02b36.yaml +++ b/releasenotes/notes/swap-strategies-3ab013ca60f02b36.yaml @@ -1,11 +1,12 @@ --- features: - | - New transpiler passes have been added. The transpiler pass :class:`.SwapStrategyRouter` + New transpiler passes have been added. The transpiler pass :class:`.Commuting2qGateRouter` uses swap strategies to route a block of commuting gates to the coupling map. Indeed, routing - is a hard problem but is significantly easier when the gates commute. This is often found in - variational algorithms such as QAOA. Such cases can be dealt with using swap strategies that - apply a predefined set of layers of SWAP gates. Furthermore, the new transpiler pass - :class:`.FindCommutingPauliEvolutions` identifies blocks of Pauli evolutions made of commuting - two-qubit terms. Here, a swap strategy is specified by the class :class:`.SwapStrategy`. Swap - strategies need to be tailored to the coupling map and, ideally, the circuit for the best results. + is a hard problem but is significantly easier when the gates commute as in CZ networks. + Blocks of commuting gates are also typically found in QAOA. Such cases can be dealt with + using swap strategies that apply a predefined set of layers of SWAP gates. Furthermore, the new + transpiler pass :class:`.FindCommutingPauliEvolutions` identifies blocks of Pauli evolutions + made of commuting two-qubit terms. Here, a swap strategy is specified by the class + :class:`.SwapStrategy`. Swap strategies need to be tailored to the coupling map and, ideally, + the circuit for the best results. diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 7e2ec650f068..bd1bad3658cf 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -281,7 +281,7 @@ def setUpClass(cls): "ClassicalElement", "StatePreparation", "LinearFunction", - "Commuting2qBlocks", + "Commuting2qBlock", } cls._gate_classes = [] for aclass in class_list: diff --git a/test/python/transpiler/test_swap_strategy.py b/test/python/transpiler/test_swap_strategy.py index 0149921e6563..1b32f11a661c 100644 --- a/test/python/transpiler/test_swap_strategy.py +++ b/test/python/transpiler/test_swap_strategy.py @@ -20,7 +20,7 @@ from qiskit.test import QiskitTestCase from qiskit.transpiler import CouplingMap -from qiskit.transpiler.passes.routing.swap_strategies import SwapStrategy +from qiskit.transpiler.passes.routing.commuting_2q_gate_routing import SwapStrategy @ddt @@ -152,6 +152,19 @@ def test_reaches_full_connectivity(self): strat = SwapStrategy(CouplingMap(ll27_map), tuple(swap_strat_ll)) self.assertEqual(len(strat.missing_couplings) == 0, result) + def test_new_connections(self): + """Test the new connections method.""" + new_cnx = self.line_strategy.new_connections(0) + expected = [{1, 0}, {2, 1}, {3, 2}, {4, 3}] + + self.assertListEqual(new_cnx, expected) + + # Test after first swap layer (0, 1) first + new_cnx = self.line_strategy.new_connections(1) + expected = [{3, 0}, {4, 2}] + + self.assertListEqual(new_cnx, expected) + def test_possible_edges(self): """Test that possible edges works as expected.""" coupling_map = CouplingMap(couplinglist=[(0, 1), (1, 2), (2, 3)]) diff --git a/test/python/transpiler/test_swap_strategy_router.py b/test/python/transpiler/test_swap_strategy_router.py index ad176a8fc964..357e5bff182c 100644 --- a/test/python/transpiler/test_swap_strategy_router.py +++ b/test/python/transpiler/test_swap_strategy_router.py @@ -18,6 +18,7 @@ from qiskit.circuit.library import PauliEvolutionGate from qiskit.circuit.library.n_local import QAOAAnsatz from qiskit.converters import circuit_to_dag +from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.quantum_info import Pauli from qiskit.transpiler.passes import FullAncillaAllocation @@ -27,7 +28,10 @@ from qiskit.test import QiskitTestCase -from qiskit.transpiler.passes.routing.swap_strategies import ( +from qiskit.transpiler.passes.routing.commuting_2q_gate_routing.commuting_2q_block import ( + Commuting2qBlock, +) +from qiskit.transpiler.passes.routing.commuting_2q_gate_routing import ( SwapStrategy, FindCommutingPauliEvolutions, Commuting2qGateRouter, @@ -122,6 +126,44 @@ def test_basic_xx(self): self.assertEqual(swapped, expected) + def test_idle_qubit(self): + """Test to route on an op that has an idle qubit. + + The op is :code:`[("IIXX", 1), ("IXIX", 2)]`. + + The expected circuit is: + + ..parsed-literal:: + + ┌─────────────────┐ + q_0: ┤0 ├─X──────────────────── + │ exp(-it XX)(3) │ │ ┌─────────────────┐ + q_1: ┤1 ├─X─┤0 ├ + └─────────────────┘ │ exp(-it XX)(6) │ + q_2: ──────────────────────┤1 ├ + └─────────────────┘ + q_3: ───────────────────────────────────────── + + """ + op = PauliSumOp.from_list([("IIXX", 1), ("IXIX", 2)]) + + cmap = CouplingMap(couplinglist=[(0, 1), (1, 2), (2, 3)]) + swap_strat = SwapStrategy(cmap, swap_layers=(((0, 1),),)) + + pm_ = PassManager([FindCommutingPauliEvolutions(), Commuting2qGateRouter(swap_strat)]) + + circ = QuantumCircuit(4) + circ.append(PauliEvolutionGate(op, 3), range(4)) + + swapped = pm_.run(circ) + + expected = QuantumCircuit(4) + expected.append(PauliEvolutionGate(Pauli("XX"), 3), (0, 1)) + expected.swap(0, 1) + expected.append(PauliEvolutionGate(Pauli("XX"), 6), (1, 2)) + + self.assertEqual(swapped, expected) + def test_basic_xx_with_measure(self): """Test to route an XX-based evolution op with measures. @@ -348,6 +390,113 @@ def test_ccx(self): self.assertEqual(swapped, expected) + def test_t_device(self): + """Test the swap strategy to route a complete problem on a T device. + + The coupling map in this test corresponds to + + .. parsed-literal:: + + 0 -- 1 -- 2 + | + 3 + | + 4 + + The problem being routed is a fully connect ZZ graph. It has 10 terms since there are + five qubits in the coupling map. This test checks that the circuit produced by the + commuting gate router has the instructions we expect. There are several circuits that are + valid since some of the Rzz gates commute. + """ + swaps = ( + ((1, 3),), + ((0, 1), (3, 4)), + ((1, 3),), + ) + + cmap = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) + cmap.make_symmetric() + + swap_strat = SwapStrategy(cmap, swaps) + + # A dense Pauli op. + op = PauliSumOp.from_list( + [ + ("IIIZZ", 1), + ("IIZIZ", 2), + ("IZIIZ", 3), + ("ZIIIZ", 4), + ("IIZZI", 5), + ("IZIZI", 6), + ("ZIIZI", 7), + ("IZZII", 8), + ("ZIZII", 9), + ("ZZIII", 10), + ] + ) + + circ = QuantumCircuit(5) + circ.append(PauliEvolutionGate(op, 1), range(5)) + + pm_ = PassManager( + [ + FindCommutingPauliEvolutions(), + Commuting2qGateRouter(swap_strat), + ] + ) + + swapped = circuit_to_dag(pm_.run(circ)) + + # The swapped circuit can take on several forms as some of the gates commute. + # We test that sets of gates are where we expected them in the circuit data + def inst_info(op, qargs, qreg): + """Get a tuple we can easily test.""" + param = None + if len(op.params) > 0: + param = op.params[0] + + return op.name, param, qreg.index(qargs[0]), qreg.index(qargs[1]) + + qreg = swapped.qregs["q"] + inst_list = list(inst_info(node.op, node.qargs, qreg) for node in swapped.op_nodes()) + + # First block has the Rzz gates ("IIIZZ", 1), ("IIZZI", 5), ("IZIZI", 6), ("ZZIII", 10) + expected = { + ("PauliEvolution", 1.0, 0, 1), + ("PauliEvolution", 5.0, 1, 2), + ("PauliEvolution", 6.0, 1, 3), + ("PauliEvolution", 10.0, 3, 4), + } + self.assertSetEqual(set(inst_list[0:4]), expected) + + # Block 2 is a swap + self.assertSetEqual({inst_list[4]}, {("swap", None, 1, 3)}) + + # Block 3 This combines a swap layer and two layers of Rzz gates. + expected = { + ("PauliEvolution", 8.0, 2, 1), + ("PauliEvolution", 7.0, 3, 4), + ("PauliEvolution", 3.0, 0, 1), + ("swap", None, 0, 1), + ("PauliEvolution", 2.0, 1, 2), + ("PauliEvolution", 4.0, 1, 3), + ("swap", None, 3, 4), + } + self.assertSetEqual(set(inst_list[5:12]), expected) + + # Test the remaining instructions. + self.assertSetEqual({inst_list[12]}, {("swap", None, 1, 3)}) + self.assertSetEqual({inst_list[13]}, {("PauliEvolution", 9.0, 2, 1)}) + + def test_single_qubit_circuit(self): + """Test that a circuit with only single qubit gates is left unchanged.""" + op = PauliSumOp.from_list([("IIIX", 1), ("IIXI", 2), ("IZII", 3), ("XIII", 4)]) + + circ = QuantumCircuit(4) + circ.append(PauliEvolutionGate(op, 1), range(4)) + + self.assertEqual(circ, self.pm_.run(circ)) + class TestSwapRouterExceptions(QiskitTestCase): """Test that exceptions are properly raises.""" @@ -402,3 +551,11 @@ def test_deficient_swap_strategy(self): with self.assertRaises(TranspilerError): pm_.run(self.circ) + + def test_commuting2qblocks_errors(self): + """Test the errors of the 2q commuting block.""" + circ = QuantumCircuit(3) + circ.ccx(0, 1, 2) + + with self.assertRaises(QiskitError): + Commuting2qBlock(circuit_to_dag(circ).op_nodes())