From af698bdf309263315a1dec5bd144cef40d35db94 Mon Sep 17 00:00:00 2001 From: SoshunNaito Date: Wed, 5 Jun 2024 03:44:45 +0900 Subject: [PATCH 1/6] add StaticPlacementPass --- bqskit/passes/mapping/placement/static.py | 82 +++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 bqskit/passes/mapping/placement/static.py diff --git a/bqskit/passes/mapping/placement/static.py b/bqskit/passes/mapping/placement/static.py new file mode 100644 index 000000000..48690327b --- /dev/null +++ b/bqskit/passes/mapping/placement/static.py @@ -0,0 +1,82 @@ +"""This module implements the StaticPlacementPass class.""" + +from __future__ import annotations + +import logging + +from timeout_decorator import timeout, TimeoutError +import networkx as nx + +from bqskit.compiler.basepass import BasePass +from bqskit.compiler.passdata import PassData +from bqskit.ir.circuit import Circuit +from bqskit.qis.graph import CouplingGraph + +_logger = logging.getLogger(__name__) + + +class StaticPlacementPass(BasePass): + """Find a subgraph monomorphic to the coupling graph so that no SWAPs are needed.""" + + def __init__(self, timeout_sec: float = 10) -> None: + self.timeout_sec = timeout_sec + + def find_monomorphic_subgraph( + self, + physical_graph: CouplingGraph, + logical_graph: CouplingGraph, + timeout_sec: float, + ) -> list[int]: + """Find an monomorphic subgraph.""" + + @timeout(timeout_sec) + def _find_monomorphic_subgraph( + physical_graph: CouplingGraph, logical_graph: CouplingGraph + ) -> list[int]: + + # Convert the coupling graph to a networkx graph + def coupling_graph_to_nx_graph(coupling_graph: CouplingGraph) -> nx.Graph: + nx_graph = nx.Graph() + nx_graph.add_nodes_from(range(coupling_graph.num_qudits)) + nx_graph.add_edges_from([e for e in coupling_graph]) + return nx_graph + + # Find an monomorphic subgraph + graph_matcher = nx.algorithms.isomorphism.GraphMatcher( + coupling_graph_to_nx_graph(physical_graph), + coupling_graph_to_nx_graph(logical_graph), + ) + if not graph_matcher.subgraph_is_monomorphic(): + return [] + + placement = list(range(logical_graph.num_qudits)) + for k, v in graph_matcher.mapping.items(): + placement[v] = k + return placement + + return _find_monomorphic_subgraph(physical_graph, logical_graph) + + async def run(self, circuit: Circuit, data: PassData) -> None: + """Perform the pass's operation, see :class:`BasePass` for more.""" + physical_graph = data.model.coupling_graph + logical_graph = circuit.coupling_graph + + # Find an monomorphic subgraph + try: + data.placement = self.find_monomorphic_subgraph( + physical_graph, logical_graph, self.timeout_sec + ) + if len(data.placement) == 0: + raise RuntimeError("No monomorphic subgraph found.") + except TimeoutError: + raise RuntimeError("Static placement search timed out.") + + _logger.info(f"Placed qudits on {data.placement}") + + # Raise an error if this is not a valid placement + if not all( + data.placement[e[1]] + in physical_graph.get_neighbors_of(data.placement[e[0]]) + for e in logical_graph + ): + raise RuntimeError("No valid placement found.") From ba4e2e4290ad0863b026f6fe94ed6e7c58263129 Mon Sep 17 00:00:00 2001 From: SoshunNaito Date: Sun, 9 Jun 2024 00:00:53 +0900 Subject: [PATCH 2/6] remove RuntimeErrors --- bqskit/passes/mapping/placement/static.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/bqskit/passes/mapping/placement/static.py b/bqskit/passes/mapping/placement/static.py index 48690327b..d3166fda0 100644 --- a/bqskit/passes/mapping/placement/static.py +++ b/bqskit/passes/mapping/placement/static.py @@ -62,21 +62,20 @@ async def run(self, circuit: Circuit, data: PassData) -> None: logical_graph = circuit.coupling_graph # Find an monomorphic subgraph + placement = [] try: - data.placement = self.find_monomorphic_subgraph( + placement = self.find_monomorphic_subgraph( physical_graph, logical_graph, self.timeout_sec ) - if len(data.placement) == 0: - raise RuntimeError("No monomorphic subgraph found.") + if len(placement) == 0: + return except TimeoutError: - raise RuntimeError("Static placement search timed out.") + return - _logger.info(f"Placed qudits on {data.placement}") - - # Raise an error if this is not a valid placement - if not all( - data.placement[e[1]] - in physical_graph.get_neighbors_of(data.placement[e[0]]) + # Set the placement if it is valid + if all( + placement[e[1]] in physical_graph.get_neighbors_of(placement[e[0]]) for e in logical_graph ): - raise RuntimeError("No valid placement found.") + data.placement = placement + _logger.info(f"Placed qudits on {data.placement}") From b6de3fc90c274123cd8c23619791a1c6002e0e0b Mon Sep 17 00:00:00 2001 From: SoshunNaito Date: Sun, 9 Jun 2024 02:02:48 +0900 Subject: [PATCH 3/6] remove package dependencies --- bqskit/passes/mapping/placement/static.py | 55 +++++++---------------- 1 file changed, 16 insertions(+), 39 deletions(-) diff --git a/bqskit/passes/mapping/placement/static.py b/bqskit/passes/mapping/placement/static.py index d3166fda0..1313d6539 100644 --- a/bqskit/passes/mapping/placement/static.py +++ b/bqskit/passes/mapping/placement/static.py @@ -3,9 +3,8 @@ from __future__ import annotations import logging - -from timeout_decorator import timeout, TimeoutError -import networkx as nx +import itertools +import time from bqskit.compiler.basepass import BasePass from bqskit.compiler.passdata import PassData @@ -25,36 +24,22 @@ def find_monomorphic_subgraph( self, physical_graph: CouplingGraph, logical_graph: CouplingGraph, - timeout_sec: float, ) -> list[int]: - """Find an monomorphic subgraph.""" - - @timeout(timeout_sec) - def _find_monomorphic_subgraph( - physical_graph: CouplingGraph, logical_graph: CouplingGraph - ) -> list[int]: - - # Convert the coupling graph to a networkx graph - def coupling_graph_to_nx_graph(coupling_graph: CouplingGraph) -> nx.Graph: - nx_graph = nx.Graph() - nx_graph.add_nodes_from(range(coupling_graph.num_qudits)) - nx_graph.add_edges_from([e for e in coupling_graph]) - return nx_graph + """Try all possible placements""" - # Find an monomorphic subgraph - graph_matcher = nx.algorithms.isomorphism.GraphMatcher( - coupling_graph_to_nx_graph(physical_graph), - coupling_graph_to_nx_graph(logical_graph), - ) - if not graph_matcher.subgraph_is_monomorphic(): + start_time = time.time() + for placement in itertools.permutations( + range(physical_graph.num_qudits), logical_graph.num_qudits + ): + if time.time() - start_time > self.timeout_sec: return [] - placement = list(range(logical_graph.num_qudits)) - for k, v in graph_matcher.mapping.items(): - placement[v] = k - return placement - - return _find_monomorphic_subgraph(physical_graph, logical_graph) + placement = list(placement) + if all( + placement[e[1]] in physical_graph.get_neighbors_of(placement[e[0]]) + for e in logical_graph + ): + return placement async def run(self, circuit: Circuit, data: PassData) -> None: """Perform the pass's operation, see :class:`BasePass` for more.""" @@ -62,18 +47,10 @@ async def run(self, circuit: Circuit, data: PassData) -> None: logical_graph = circuit.coupling_graph # Find an monomorphic subgraph - placement = [] - try: - placement = self.find_monomorphic_subgraph( - physical_graph, logical_graph, self.timeout_sec - ) - if len(placement) == 0: - return - except TimeoutError: - return + placement = self.find_monomorphic_subgraph(physical_graph, logical_graph) # Set the placement if it is valid - if all( + if len(placement) == logical_graph.num_qudits and all( placement[e[1]] in physical_graph.get_neighbors_of(placement[e[0]]) for e in logical_graph ): From 63e5880639a501593187cfe33307870b3c98f1aa Mon Sep 17 00:00:00 2001 From: SoshunNaito Date: Mon, 10 Jun 2024 12:33:38 +0900 Subject: [PATCH 4/6] reduced search space --- bqskit/passes/mapping/placement/static.py | 94 ++++++++++++++++++++--- 1 file changed, 82 insertions(+), 12 deletions(-) diff --git a/bqskit/passes/mapping/placement/static.py b/bqskit/passes/mapping/placement/static.py index 1313d6539..1f9af00ca 100644 --- a/bqskit/passes/mapping/placement/static.py +++ b/bqskit/passes/mapping/placement/static.py @@ -20,6 +20,59 @@ class StaticPlacementPass(BasePass): def __init__(self, timeout_sec: float = 10) -> None: self.timeout_sec = timeout_sec + def _find_monomorphic_subgraph( + self, + time_limit: float, + physical_graph: CouplingGraph, + num_logical_qudits: int, + minimal_degrees: list[int], + connected_indices: list[list[int]], + current_placement: list[int] = [], + current_index: int = 0, + ) -> list[int]: + """Recursively find a monomorphic subgraph""" + if current_index == num_logical_qudits: + return current_placement + + if time.time() > time_limit: + return [] + + # Find all possible placements for the current logical qudit + candidate_indices = set() + + # Filter out occupied qudits and qudits with insufficient degrees + physical_degrees = physical_graph.get_qudit_degrees() + for x in range(physical_graph.num_qudits): + if ( + physical_degrees[x] >= minimal_degrees[current_index] + and x not in current_placement + ): + candidate_indices.add(x) + + # Filter out qudits that are not connected to previous logical qudits + for i in connected_indices[current_index]: + candidate_indices &= set( + physical_graph.get_neighbors_of(current_placement[i]) + ) + + # Try all possible placements for the current logical qudit + for x in candidate_indices: + new_placement = current_placement + [x] + result = self._find_monomorphic_subgraph( + time_limit, + physical_graph, + num_logical_qudits, + minimal_degrees, + connected_indices, + new_placement, + current_index + 1, + ) + if len(result) == num_logical_qudits: + return result + + # If no valid placement is found, return an empty list + return [] + def find_monomorphic_subgraph( self, physical_graph: CouplingGraph, @@ -27,19 +80,36 @@ def find_monomorphic_subgraph( ) -> list[int]: """Try all possible placements""" - start_time = time.time() - for placement in itertools.permutations( - range(physical_graph.num_qudits), logical_graph.num_qudits - ): - if time.time() - start_time > self.timeout_sec: - return [] + # To be optimized later + logical_qubit_order = list(range(logical_graph.num_qudits)) - placement = list(placement) - if all( - placement[e[1]] in physical_graph.get_neighbors_of(placement[e[0]]) - for e in logical_graph - ): - return placement + minimum_degrees = [ + logical_graph.get_qudit_degrees()[i] for i in logical_qubit_order + ] + connected_indices = [[] for _ in range(logical_graph.num_qudits)] + for i in range(logical_graph.num_qudits): + for j in range(i): + if logical_qubit_order[j] in logical_graph.get_neighbors_of( + logical_qubit_order[i] + ): + connected_indices[i].append(j) + + # Find a monomorphic subgraph + index_to_physical = self._find_monomorphic_subgraph( + time.time() + self.timeout_sec, + physical_graph, + logical_graph.num_qudits, + minimum_degrees, + connected_indices, + ) + if len(index_to_physical) == 0: + return [] + + # Convert the result to a placement + placement = [-1] * logical_graph.num_qudits + for i, x in enumerate(logical_qubit_order): + placement[x] = index_to_physical[i] + return placement async def run(self, circuit: Circuit, data: PassData) -> None: """Perform the pass's operation, see :class:`BasePass` for more.""" From 613d5c7ab021e8ced5eec247ae69d9e47c02c47b Mon Sep 17 00:00:00 2001 From: SoshunNaito Date: Wed, 12 Jun 2024 03:47:25 +0900 Subject: [PATCH 5/6] added tests for StaticPlacementPass --- bqskit/passes/mapping/placement/static.py | 6 ++- tests/passes/mapping/test_static.py | 49 +++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/passes/mapping/test_static.py diff --git a/bqskit/passes/mapping/placement/static.py b/bqskit/passes/mapping/placement/static.py index 1f9af00ca..180f1942e 100644 --- a/bqskit/passes/mapping/placement/static.py +++ b/bqskit/passes/mapping/placement/static.py @@ -95,13 +95,15 @@ def find_monomorphic_subgraph( connected_indices[i].append(j) # Find a monomorphic subgraph + start_time = time.time() index_to_physical = self._find_monomorphic_subgraph( - time.time() + self.timeout_sec, + start_time + self.timeout_sec, physical_graph, logical_graph.num_qudits, minimum_degrees, connected_indices, ) + _logger.info(f"elapsed time: {time.time() - start_time}") if len(index_to_physical) == 0: return [] @@ -126,3 +128,5 @@ async def run(self, circuit: Circuit, data: PassData) -> None: ): data.placement = placement _logger.info(f"Placed qudits on {data.placement}") + else: + _logger.info(f"No valid placement found") diff --git a/tests/passes/mapping/test_static.py b/tests/passes/mapping/test_static.py new file mode 100644 index 000000000..203002df2 --- /dev/null +++ b/tests/passes/mapping/test_static.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import pytest + +from bqskit import Circuit +from bqskit import MachineModel +from bqskit.ir.gates import CNOTGate +from bqskit.qis import CouplingGraph + +from bqskit.compiler import Compiler +from bqskit.compiler import MachineModel +from bqskit.ir.circuit import Circuit +from bqskit.passes import IfThenElsePass, LogPass +from bqskit.passes import SetModelPass +from bqskit.passes import ApplyPlacement +from bqskit.passes import GreedyPlacementPass +from bqskit.passes import StaticPlacementPass +from bqskit.passes.control.predicates import PhysicalPredicate + + +def circular_circuit(n: int) -> Circuit: + circuit = Circuit(n) + for i in range(n): + circuit.append_gate(CNOTGate(), [i, (i + 1) % n]) + return circuit + + +@pytest.mark.parametrize( + ["grid_size", "logical_qudits"], + sum([[(n, i) for i in range(2, n**2, 2)] for n in range(2, 8)], []) + + sum([[(n, i) for i in range(3, n**2, 2)] for n in range(2, 6)], []), +) +def test_circular_to_grid( + grid_size: int, logical_qudits: int, compiler: Compiler +) -> None: + circuit = circular_circuit(logical_qudits) + cg = CouplingGraph.grid(grid_size, grid_size) + model = MachineModel(grid_size**2, cg) + workflow = [ + SetModelPass(model), + StaticPlacementPass(timeout_sec=1.0), + IfThenElsePass( + PhysicalPredicate(), + [LogPass("Static Placement Found")], + [LogPass("Greedy Placement Required"), GreedyPlacementPass()], + ), + ApplyPlacement(), + ] + out_circuit = compiler.compile(circuit, workflow) From f8584e0a1d3467114c000129247fa890dd6a8d4e Mon Sep 17 00:00:00 2001 From: SoshunNaito Date: Thu, 13 Jun 2024 13:22:48 +0900 Subject: [PATCH 6/6] fix format with pre-commit --- bqskit/passes/__init__.py | 2 ++ bqskit/passes/mapping/__init__.py | 2 ++ bqskit/passes/mapping/placement/__init__.py | 3 ++- bqskit/passes/mapping/placement/static.py | 27 ++++++++++++--------- tests/passes/mapping/test_static.py | 22 ++++++++--------- 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/bqskit/passes/__init__.py b/bqskit/passes/__init__.py index eb8e41607..8362c9161 100644 --- a/bqskit/passes/__init__.py +++ b/bqskit/passes/__init__.py @@ -234,6 +234,7 @@ from bqskit.passes.mapping.layout.pam import PAMLayoutPass from bqskit.passes.mapping.layout.sabre import GeneralizedSabreLayoutPass from bqskit.passes.mapping.placement.greedy import GreedyPlacementPass +from bqskit.passes.mapping.placement.static import StaticPlacementPass from bqskit.passes.mapping.placement.trivial import TrivialPlacementPass from bqskit.passes.mapping.routing.pam import PAMRoutingPass from bqskit.passes.mapping.routing.sabre import GeneralizedSabreRoutingPass @@ -364,6 +365,7 @@ 'GeneralizedSabreLayoutPass', 'GreedyPlacementPass', 'TrivialPlacementPass', + 'StaticPlacementPass', 'GeneralizedSabreRoutingPass', 'SetModelPass', 'U3Decomposition', diff --git a/bqskit/passes/mapping/__init__.py b/bqskit/passes/mapping/__init__.py index 5ffc9dd2f..33e17d223 100644 --- a/bqskit/passes/mapping/__init__.py +++ b/bqskit/passes/mapping/__init__.py @@ -6,6 +6,7 @@ from bqskit.passes.mapping.layout.pam import PAMLayoutPass from bqskit.passes.mapping.layout.sabre import GeneralizedSabreLayoutPass from bqskit.passes.mapping.placement.greedy import GreedyPlacementPass +from bqskit.passes.mapping.placement.static import StaticPlacementPass from bqskit.passes.mapping.placement.trivial import TrivialPlacementPass from bqskit.passes.mapping.routing.pam import PAMRoutingPass from bqskit.passes.mapping.routing.sabre import GeneralizedSabreRoutingPass @@ -22,6 +23,7 @@ 'GeneralizedSabreLayoutPass', 'GreedyPlacementPass', 'TrivialPlacementPass', + 'StaticPlacementPass', 'GeneralizedSabreRoutingPass', 'SetModelPass', 'ApplyPlacement', diff --git a/bqskit/passes/mapping/placement/__init__.py b/bqskit/passes/mapping/placement/__init__.py index bad954a86..8281c7c4d 100644 --- a/bqskit/passes/mapping/placement/__init__.py +++ b/bqskit/passes/mapping/placement/__init__.py @@ -7,6 +7,7 @@ from __future__ import annotations from bqskit.passes.mapping.placement.greedy import GreedyPlacementPass +from bqskit.passes.mapping.placement.static import StaticPlacementPass from bqskit.passes.mapping.placement.trivial import TrivialPlacementPass -__all__ = ['GreedyPlacementPass', 'TrivialPlacementPass'] +__all__ = ['GreedyPlacementPass', 'TrivialPlacementPass', 'StaticPlacementPass'] diff --git a/bqskit/passes/mapping/placement/static.py b/bqskit/passes/mapping/placement/static.py index 180f1942e..187eeb044 100644 --- a/bqskit/passes/mapping/placement/static.py +++ b/bqskit/passes/mapping/placement/static.py @@ -1,9 +1,7 @@ """This module implements the StaticPlacementPass class.""" - from __future__ import annotations import logging -import itertools import time from bqskit.compiler.basepass import BasePass @@ -15,7 +13,8 @@ class StaticPlacementPass(BasePass): - """Find a subgraph monomorphic to the coupling graph so that no SWAPs are needed.""" + """Find a subgraph monomorphic to the coupling graph so that no SWAPs are + needed.""" def __init__(self, timeout_sec: float = 10) -> None: self.timeout_sec = timeout_sec @@ -30,7 +29,7 @@ def _find_monomorphic_subgraph( current_placement: list[int] = [], current_index: int = 0, ) -> list[int]: - """Recursively find a monomorphic subgraph""" + """Recursively find a monomorphic subgraph.""" if current_index == num_logical_qudits: return current_placement @@ -52,7 +51,7 @@ def _find_monomorphic_subgraph( # Filter out qudits that are not connected to previous logical qudits for i in connected_indices[current_index]: candidate_indices &= set( - physical_graph.get_neighbors_of(current_placement[i]) + physical_graph.get_neighbors_of(current_placement[i]), ) # Try all possible placements for the current logical qudit @@ -78,7 +77,7 @@ def find_monomorphic_subgraph( physical_graph: CouplingGraph, logical_graph: CouplingGraph, ) -> list[int]: - """Try all possible placements""" + """Try all possible placements.""" # To be optimized later logical_qubit_order = list(range(logical_graph.num_qudits)) @@ -86,11 +85,13 @@ def find_monomorphic_subgraph( minimum_degrees = [ logical_graph.get_qudit_degrees()[i] for i in logical_qubit_order ] - connected_indices = [[] for _ in range(logical_graph.num_qudits)] + connected_indices: list[list[int]] = [ + [] for _ in range(logical_graph.num_qudits) + ] for i in range(logical_graph.num_qudits): for j in range(i): if logical_qubit_order[j] in logical_graph.get_neighbors_of( - logical_qubit_order[i] + logical_qubit_order[i], ): connected_indices[i].append(j) @@ -103,7 +104,7 @@ def find_monomorphic_subgraph( minimum_degrees, connected_indices, ) - _logger.info(f"elapsed time: {time.time() - start_time}") + _logger.info(f'elapsed time: {time.time() - start_time}') if len(index_to_physical) == 0: return [] @@ -119,7 +120,9 @@ async def run(self, circuit: Circuit, data: PassData) -> None: logical_graph = circuit.coupling_graph # Find an monomorphic subgraph - placement = self.find_monomorphic_subgraph(physical_graph, logical_graph) + placement = self.find_monomorphic_subgraph( + physical_graph, logical_graph, + ) # Set the placement if it is valid if len(placement) == logical_graph.num_qudits and all( @@ -127,6 +130,6 @@ async def run(self, circuit: Circuit, data: PassData) -> None: for e in logical_graph ): data.placement = placement - _logger.info(f"Placed qudits on {data.placement}") + _logger.info(f'Placed qudits on {data.placement}') else: - _logger.info(f"No valid placement found") + _logger.info('No valid placement found') diff --git a/tests/passes/mapping/test_static.py b/tests/passes/mapping/test_static.py index 203002df2..759d97935 100644 --- a/tests/passes/mapping/test_static.py +++ b/tests/passes/mapping/test_static.py @@ -2,20 +2,18 @@ import pytest -from bqskit import Circuit -from bqskit import MachineModel -from bqskit.ir.gates import CNOTGate -from bqskit.qis import CouplingGraph - from bqskit.compiler import Compiler from bqskit.compiler import MachineModel from bqskit.ir.circuit import Circuit -from bqskit.passes import IfThenElsePass, LogPass -from bqskit.passes import SetModelPass +from bqskit.ir.gates import CNOTGate from bqskit.passes import ApplyPlacement from bqskit.passes import GreedyPlacementPass +from bqskit.passes import IfThenElsePass +from bqskit.passes import LogPass +from bqskit.passes import SetModelPass from bqskit.passes import StaticPlacementPass from bqskit.passes.control.predicates import PhysicalPredicate +from bqskit.qis import CouplingGraph def circular_circuit(n: int) -> Circuit: @@ -26,12 +24,12 @@ def circular_circuit(n: int) -> Circuit: @pytest.mark.parametrize( - ["grid_size", "logical_qudits"], + ['grid_size', 'logical_qudits'], sum([[(n, i) for i in range(2, n**2, 2)] for n in range(2, 8)], []) + sum([[(n, i) for i in range(3, n**2, 2)] for n in range(2, 6)], []), ) def test_circular_to_grid( - grid_size: int, logical_qudits: int, compiler: Compiler + grid_size: int, logical_qudits: int, compiler: Compiler, ) -> None: circuit = circular_circuit(logical_qudits) cg = CouplingGraph.grid(grid_size, grid_size) @@ -41,9 +39,9 @@ def test_circular_to_grid( StaticPlacementPass(timeout_sec=1.0), IfThenElsePass( PhysicalPredicate(), - [LogPass("Static Placement Found")], - [LogPass("Greedy Placement Required"), GreedyPlacementPass()], + [LogPass('Static Placement Found')], + [LogPass('Greedy Placement Required'), GreedyPlacementPass()], ), ApplyPlacement(), ] - out_circuit = compiler.compile(circuit, workflow) + compiler.compile(circuit, workflow)