From 5a5f11229c996bddeda8969d265f6cff7f437f95 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Tue, 5 Dec 2023 10:25:59 +0400 Subject: [PATCH 01/19] restrict qubits function --- src/qibo/transpiler/router.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 8bea586444..191087ffaf 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -34,6 +34,29 @@ def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): ) +def restrict_connectivity_qubits(connectivity: nx.Graph, qubits: list): + """Restrict the connectivity to selected qubits. + + Args: + connectivity (:class:`networkx.Graph`): chip connectivity. + qubits (list): list of physical qubits to be used. + """ + if set(qubits).issubset(connectivity.nodes): + raise_error( + ConnectivityError, "Some qubits are not in the original connectivity." + ) + new_connectivity = nx.Graph() + new_connectivity.add_nodes_from(qubits) + new_edges = [] + for edge in connectivity.edges: + if edge[0] in qubits and edge[1] in qubits: + new_edges.append(edge) + new_connectivity.add_edges_from(new_edges) + if not nx.is_connected(new_connectivity): + raise_error(ConnectivityError, "The new connectivity graph is not connected.") + return new_connectivity + + # TODO: make this class work with CircuitMap class ShortestPaths(Router): """A class to perform initial qubit mapping and connectivity matching. @@ -716,8 +739,16 @@ def _create_dag(gates_qubits_pairs): return _remove_redundant_connections(dag) -def _remove_redundant_connections(dag: nx.Graph): - """Remove redundant connection from a DAG unsing transitive reduction.""" +def _remove_redundant_connections(dag: nx.DiGraph): + """Helper method for :func:`_create_dag`. + Remove redundant connection from a DAG using transitive reduction. + + Args: + dag (:class:`networkx.DiGraph`): dag to be reduced. + + Returns: + (:class:`networkx.DiGraph`): reduced dag. + """ new_dag = nx.DiGraph() new_dag.add_nodes_from(range(dag.number_of_nodes())) transitive_reduction = nx.transitive_reduction(dag) From 9899a22f0f5d986f1ce456148444b16c0df9a7db Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Tue, 5 Dec 2023 11:28:00 +0400 Subject: [PATCH 02/19] added tests --- src/qibo/transpiler/router.py | 5 ++++- tests/test_transpiler_router.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 191087ffaf..f9ff7dc28a 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -40,8 +40,11 @@ def restrict_connectivity_qubits(connectivity: nx.Graph, qubits: list): Args: connectivity (:class:`networkx.Graph`): chip connectivity. qubits (list): list of physical qubits to be used. + + Returns: + (:class:`networkx.Graph`): restricted connectivity. """ - if set(qubits).issubset(connectivity.nodes): + if not set(qubits).issubset(set(connectivity.nodes)): raise_error( ConnectivityError, "Some qubits are not in the original connectivity." ) diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index 621f7f69e3..ea4ca73275 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -14,6 +14,7 @@ ShortestPaths, _find_gates_qubits_pairs, assert_connectivity, + restrict_connectivity_qubits, ) @@ -79,6 +80,24 @@ def matched_circuit(): return circuit +def test_restrict_qubits_error_no_subset(): + with pytest.raises(ConnectivityError) as excinfo: + restrict_connectivity_qubits(star_connectivity(), [1, 2, 6]) + assert "Some qubits are not in the original connectivity." in str(excinfo.value) + + +def test_restrict_qubits_error_not_connected(): + with pytest.raises(ConnectivityError) as excinfo: + restrict_connectivity_qubits(star_connectivity(), [1, 3]) + assert "The new connectivity graph is not connected." in str(excinfo.value) + + +def test_restrict_qubits(): + new_connectivity = restrict_connectivity_qubits(star_connectivity(), [1, 2, 3]) + assert list(new_connectivity.nodes) == [1, 2, 3] + assert list(new_connectivity.edges) == [(1, 2), (2, 3)] + + def test_assert_connectivity(): assert_connectivity(star_connectivity(), matched_circuit()) From 6ada47cfc3e56042eaff1990133a62e4e7343e41 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Fri, 8 Dec 2023 16:03:55 +0400 Subject: [PATCH 03/19] restricted topology for placer --- src/qibo/transpiler/pipeline.py | 51 ++++++++++++--- src/qibo/transpiler/placer.py | 74 ++++++++++++++------- src/qibo/transpiler/router.py | 29 +-------- tests/test_transpiler_pipeline.py | 32 +++++++-- tests/test_transpiler_placer.py | 105 +++++++++++++++++++++++++++++- tests/test_transpiler_router.py | 19 ------ 6 files changed, 222 insertions(+), 88 deletions(-) diff --git a/src/qibo/transpiler/pipeline.py b/src/qibo/transpiler/pipeline.py index b88da8da8f..96e4be76d6 100644 --- a/src/qibo/transpiler/pipeline.py +++ b/src/qibo/transpiler/pipeline.py @@ -131,40 +131,71 @@ def assert_transpiling( ) +def restrict_connectivity_qubits(connectivity: nx.Graph, qubits: list): + """Restrict the connectivity to selected qubits. + + Args: + connectivity (:class:`networkx.Graph`): chip connectivity. + qubits (list): list of physical qubits to be used. + + Returns: + (:class:`networkx.Graph`): restricted connectivity. + """ + if not set(qubits).issubset(set(connectivity.nodes)): + raise_error( + ConnectivityError, "Some qubits are not in the original connectivity." + ) + new_connectivity = nx.Graph() + new_connectivity.add_nodes_from(qubits) + new_edges = [] + for edge in connectivity.edges: + if edge[0] in qubits and edge[1] in qubits: + new_edges.append(edge) + new_connectivity.add_edges_from(new_edges) + if not nx.is_connected(new_connectivity): + raise_error(ConnectivityError, "The new connectivity graph is not connected.") + return new_connectivity + + class Passes: """Define a transpiler pipeline consisting of smaller transpiler steps that are applied sequentially: Args: passes (list): list of passes to be applied sequentially, - if None default transpiler will be used, it requires hardware connectivity. - connectivity (nx.Graph): hardware qubit connectivity. + if None default transpiler will be used. + connectivity (nx.Graph): physical qubits connectivity. + native_gates (NativeGates): native gates. + on_qubits (list): list of physical qubits to be used. If "None" all qubits are used. """ def __init__( self, - passes: list = None, - connectivity: nx.Graph = None, + passes: list, + connectivity: nx.Graph, native_gates: NativeGates = NativeGates.default(), + on_qubits: list = None, ): + if on_qubits is not None: + connectivity = restrict_connectivity_qubits(connectivity, on_qubits) + self.connectivity = connectivity self.native_gates = native_gates if passes is None: - self.passes = self.default(connectivity) + self.passes = self.default() else: self.passes = passes - self.connectivity = connectivity - def default(self, connectivity: nx.Graph): + def default(self): """Return the default transpiler pipeline for the required hardware connectivity.""" - if not isinstance(connectivity, nx.Graph): + if not isinstance(self.connectivity, nx.Graph): raise_error( TranspilerPipelineError, "Define the hardware chip connectivity to use default transpiler", ) default_passes = [] # preprocessing - default_passes.append(Preprocessing(connectivity=connectivity)) + default_passes.append(Preprocessing(connectivity=self.connectivity)) # default placer pass - default_passes.append(Trivial(connectivity=connectivity)) + default_passes.append(Trivial(connectivity=self.connectivity)) # default router pass default_passes.append(StarConnectivity()) # default unroller pass diff --git a/src/qibo/transpiler/placer.py b/src/qibo/transpiler/placer.py index aa33536445..f74a1bbf76 100644 --- a/src/qibo/transpiler/placer.py +++ b/src/qibo/transpiler/placer.py @@ -11,14 +11,18 @@ from qibo.transpiler.router import _find_gates_qubits_pairs -def assert_placement(circuit: Circuit, layout: dict) -> bool: +def assert_placement( + circuit: Circuit, layout: dict, connectivity: nx.graph = None +) -> bool: """Check if layout is in the correct form and matches the number of qubits of the circuit. Args: circuit (:class:`qibo.models.circuit.Circuit`): Circuit model to check. layout (dict): physical to logical qubit mapping. + connectivity (:class:`networkx.Graph`, optional): chip connectivity. This argument is necessary if the + layout applied to a subset of qubits in the original connectivity graph. """ - assert_mapping_consistency(layout) + assert_mapping_consistency(layout=layout, connectivity=connectivity) if circuit.nqubits > len(layout): raise_error( PlacementError, @@ -31,16 +35,20 @@ def assert_placement(circuit: Circuit, layout: dict) -> bool: ) -def assert_mapping_consistency(layout: dict): +def assert_mapping_consistency(layout: dict, connectivity: nx.Graph = None): """Check if layout is in the correct form. Args: layout (dict): physical to logical qubit mapping. + connectivity (:class:`networkx.Graph`, optional): chip connectivity. This argument is necessary if the + layout applied to a subset of qubits in the original connectivity graph. """ values = sorted(layout.values()) - keys = list(layout) - ref_keys = ["q" + str(i) for i in range(len(keys))] - if keys != ref_keys: + if connectivity is not None: + ref_keys = ["q" + str(i) for i in list(connectivity.nodes)] + else: + ref_keys = ["q" + str(i) for i in range(len(values))] + if list(layout.keys()) != ref_keys: raise_error( PlacementError, "Some physical qubits in the layout may be missing or duplicated.", @@ -81,12 +89,20 @@ def __call__(self, circuit: Circuit): "The number of nodes of the connectivity graph must match " + "the number of qubits in the circuit", ) - return dict( - zip( - list("q" + str(i) for i in range(circuit.nqubits)), - range(circuit.nqubits), + trivial_layout = dict( + zip( + ["q" + str(i) for i in list(self.connectivity.nodes())], + range(circuit.nqubits), + ) ) - ) + else: + trivial_layout = dict( + zip( + ["q" + str(i) for i in range(circuit.nqubits)], + range(circuit.nqubits), + ) + ) + return trivial_layout class Custom(Placer): @@ -99,6 +115,8 @@ class Custom(Placer): to assign the physical qubits :math:`\\{0, 1, 2\\}` to the logical qubits :math:`[1, 2, 0]`. connectivity (networkx.Graph, optional): chip connectivity. + This argument is necessary if the layout applied to a subset of + qubits of the original connectivity graph. """ def __init__(self, map: Union[list, dict], connectivity: nx.Graph = None): @@ -117,15 +135,20 @@ def __call__(self, circuit=None): if isinstance(self.map, dict): pass elif isinstance(self.map, list): - self.map = dict( - zip(list("q" + str(i) for i in range(len(self.map))), self.map) - ) + if self.connectivity is not None: + self.map = dict( + zip(["q" + str(i) for i in self.connectivity.nodes()], self.map) + ) + else: + self.map = dict( + zip(["q" + str(i) for i in range(len(self.map))], self.map) + ) else: raise_error(TypeError, "Use dict or list to define mapping.") if circuit is not None: - assert_placement(circuit, self.map) + assert_placement(circuit, self.map, connectivity=self.connectivity) else: - assert_mapping_consistency(self.map) + assert_mapping_consistency(self.map, connectivity=self.connectivity) return self.map @@ -145,6 +168,7 @@ def __init__(self, connectivity: nx.Graph): def __call__(self, circuit: Circuit): """Find the initial layout of the given circuit using subgraph isomorphism. + Circuit must contain at least two two-qubit gates to implement subgraph placement. Args: circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. @@ -159,7 +183,7 @@ def __call__(self, circuit: Circuit): "Circuit must contain at least two two-qubit gates to implement subgraph placement.", ) circuit_subgraph = nx.Graph() - circuit_subgraph.add_nodes_from(range(self.connectivity.number_of_nodes())) + circuit_subgraph.add_nodes_from(list(range(circuit.nqubits))) matcher = nx.algorithms.isomorphism.GraphMatcher( self.connectivity, circuit_subgraph ) @@ -180,7 +204,11 @@ def __call__(self, circuit: Circuit): or i == len(gates_qubits_pairs) - 1 ): break - return {"q" + str(i): result.mapping[i] for i in range(len(result.mapping))} + + sorted_result = dict(sorted(result.mapping.items())) + return dict( + zip(["q" + str(i) for i in sorted_result.keys()], sorted_result.values()) + ) class Random(Placer): @@ -210,23 +238,21 @@ def __call__(self, circuit): gates_qubits_pairs = _find_gates_qubits_pairs(circuit) nodes = self.connectivity.number_of_nodes() keys = list(self.connectivity.nodes()) + dict_keys = ["q" + str(i) for i in keys] final_mapping = dict(zip(keys, range(nodes))) final_graph = nx.relabel_nodes(self.connectivity, final_mapping) - final_mapping = { - "q" + str(i): final_mapping[i] for i in range(len(final_mapping)) - } final_cost = self._cost(final_graph, gates_qubits_pairs) for _ in range(self.samples): mapping = dict(zip(keys, random.sample(range(nodes), nodes))) graph = nx.relabel_nodes(self.connectivity, mapping) cost = self._cost(graph, gates_qubits_pairs) if cost == 0: - return {"q" + str(i): mapping[i] for i in range(len(mapping))} + return dict(zip(dict_keys, list(mapping.values()))) if cost < final_cost: final_graph = graph - final_mapping = {"q" + str(i): mapping[i] for i in range(len(mapping))} + final_mapping = {dict_keys[i]: mapping[i] for i in range(len(mapping))} final_cost = cost - return final_mapping + return dict(zip(dict_keys, list(final_mapping.values()))) def _cost(self, graph: nx.Graph, gates_qubits_pairs: list): """ diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index f9ff7dc28a..e3180d84bf 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -34,32 +34,6 @@ def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): ) -def restrict_connectivity_qubits(connectivity: nx.Graph, qubits: list): - """Restrict the connectivity to selected qubits. - - Args: - connectivity (:class:`networkx.Graph`): chip connectivity. - qubits (list): list of physical qubits to be used. - - Returns: - (:class:`networkx.Graph`): restricted connectivity. - """ - if not set(qubits).issubset(set(connectivity.nodes)): - raise_error( - ConnectivityError, "Some qubits are not in the original connectivity." - ) - new_connectivity = nx.Graph() - new_connectivity.add_nodes_from(qubits) - new_edges = [] - for edge in connectivity.edges: - if edge[0] in qubits and edge[1] in qubits: - new_edges.append(edge) - new_connectivity.add_edges_from(new_edges) - if not nx.is_connected(new_connectivity): - raise_error(ConnectivityError, "The new connectivity graph is not connected.") - return new_connectivity - - # TODO: make this class work with CircuitMap class ShortestPaths(Router): """A class to perform initial qubit mapping and connectivity matching. @@ -101,6 +75,7 @@ def __call__(self, circuit: Circuit, initial_layout: dict): (:class:`qibo.models.circuit.Circuit`, dict): circut mapped to hardware topology, and final qubit mapping. """ self._mapping = initial_layout + dict_keys = list(initial_layout.keys()) init_qubit_map = np.asarray(list(initial_layout.values())) self._initial_checks(circuit.nqubits) self._gates_qubits_pairs = _find_gates_qubits_pairs(circuit) @@ -114,7 +89,7 @@ def __call__(self, circuit: Circuit, initial_layout: dict): self._transpiler_step(circuit) hardware_mapped_circuit = self._remap_circuit(np.argsort(init_qubit_map)) final_mapping = { - "q" + str(j): self._swap_map[j] + dict_keys[j]: self._swap_map[j] for j in range(self._graph.number_of_nodes()) } diff --git a/tests/test_transpiler_pipeline.py b/tests/test_transpiler_pipeline.py index 1c72b3eeaa..bc82956485 100644 --- a/tests/test_transpiler_pipeline.py +++ b/tests/test_transpiler_pipeline.py @@ -6,10 +6,12 @@ from qibo.models import Circuit from qibo.transpiler.optimizer import Preprocessing from qibo.transpiler.pipeline import ( + ConnectivityError, Passes, TranspilerPipelineError, assert_circuit_equivalence, assert_transpiling, + restrict_connectivity_qubits, ) from qibo.transpiler.placer import Random, ReverseTraversal, Trivial from qibo.transpiler.router import ShortestPaths @@ -68,10 +70,28 @@ def star_connectivity(): return chip +def test_restrict_qubits_error_no_subset(): + with pytest.raises(ConnectivityError) as excinfo: + restrict_connectivity_qubits(star_connectivity(), [1, 2, 6]) + assert "Some qubits are not in the original connectivity." in str(excinfo.value) + + +def test_restrict_qubits_error_not_connected(): + with pytest.raises(ConnectivityError) as excinfo: + restrict_connectivity_qubits(star_connectivity(), [1, 3]) + assert "The new connectivity graph is not connected." in str(excinfo.value) + + +def test_restrict_qubits(): + new_connectivity = restrict_connectivity_qubits(star_connectivity(), [1, 2, 3]) + assert list(new_connectivity.nodes) == [1, 2, 3] + assert list(new_connectivity.edges) == [(1, 2), (2, 3)] + + @pytest.mark.parametrize("ngates", [5, 10, 50]) def test_pipeline_default(ngates): circ = generate_random_circuit(nqubits=5, ngates=ngates) - default_transpiler = Passes(connectivity=star_connectivity()) + default_transpiler = Passes(passes=None, connectivity=star_connectivity()) transpiled_circ, final_layout = default_transpiler(circ) initial_layout = default_transpiler.get_initial_layout() assert_transpiling( @@ -127,11 +147,11 @@ def test_assert_circuit_equivalence_wrong_nqubits(): def test_error_connectivity(): with pytest.raises(TranspilerPipelineError): - default_transpiler = Passes() + default_transpiler = Passes(passes=None, connectivity=None) def test_is_satisfied(): - default_transpiler = Passes(connectivity=star_connectivity()) + default_transpiler = Passes(passes=None, connectivity=star_connectivity()) circuit = Circuit(5) circuit.add(gates.CZ(0, 2)) circuit.add(gates.Z(0)) @@ -139,7 +159,7 @@ def test_is_satisfied(): def test_is_satisfied_false_decomposition(): - default_transpiler = Passes(connectivity=star_connectivity()) + default_transpiler = Passes(passes=None, connectivity=star_connectivity()) circuit = Circuit(5) circuit.add(gates.CZ(0, 2)) circuit.add(gates.X(0)) @@ -147,7 +167,7 @@ def test_is_satisfied_false_decomposition(): def test_is_satisfied_false_connectivity(): - default_transpiler = Passes(connectivity=star_connectivity()) + default_transpiler = Passes(passes=None, connectivity=star_connectivity()) circuit = Circuit(5) circuit.add(gates.CZ(0, 1)) circuit.add(gates.Z(0)) @@ -241,7 +261,7 @@ def test_custom_passes_no_placer(): def test_custom_passes_wrong_pass(): custom_passes = [0] - custom_pipeline = Passes(passes=custom_passes) + custom_pipeline = Passes(passes=custom_passes, connectivity=None) circ = generate_random_circuit(nqubits=5, ngates=5) with pytest.raises(TranspilerPipelineError): transpiled_circ, final_layout = custom_pipeline(circ) diff --git a/tests/test_transpiler_placer.py b/tests/test_transpiler_placer.py index 748d469f0b..ae34bcec1e 100644 --- a/tests/test_transpiler_placer.py +++ b/tests/test_transpiler_placer.py @@ -3,6 +3,7 @@ from qibo import gates from qibo.models import Circuit +from qibo.transpiler.pipeline import restrict_connectivity_qubits from qibo.transpiler.placer import ( Custom, PlacementError, @@ -53,7 +54,7 @@ def test_assert_placement_false(qubits, layout): assert_placement(circuit, layout) -def test_mapping_consistency_true(): +def test_mapping_consistency(): layout = {"q0": 0, "q1": 2, "q2": 1, "q3": 4, "q4": 3} assert_mapping_consistency(layout) @@ -65,11 +66,32 @@ def test_mapping_consistency_true(): {"q0": 0, "q1": 2, "q0": 1, "q3": 4, "q4": 3}, ], ) -def test_mapping_consistency_false(layout): +def test_mapping_consistency_error(layout): with pytest.raises(PlacementError): assert_mapping_consistency(layout) +def test_mapping_consistency_restricted(): + layout = {"q0": 0, "q2": 1} + connectivity = star_connectivity() + restricted_connecyivity = restrict_connectivity_qubits(connectivity, [0, 2]) + assert_mapping_consistency(layout, restricted_connecyivity) + + +@pytest.mark.parametrize( + "layout", + [ + {"q0": 0, "q2": 2}, + {"q0": 0, "q1": 1}, + ], +) +def test_mapping_consistency_restricted_error(layout): + connectivity = star_connectivity() + restricted_connecyivity = restrict_connectivity_qubits(connectivity, [0, 2]) + with pytest.raises(PlacementError): + assert_mapping_consistency(layout, restricted_connecyivity) + + def test_trivial(): circuit = Circuit(5) connectivity = star_connectivity() @@ -79,6 +101,18 @@ def test_trivial(): assert_placement(circuit, layout) +def test_trivial_restricted(): + circuit = Circuit(2) + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2]) + placer = Trivial(connectivity=restricted_connectivity) + layout = placer(circuit) + assert layout == {"q0": 0, "q2": 1} + assert_placement( + circuit=circuit, layout=layout, connectivity=restricted_connectivity + ) + + def test_trivial_error(): circuit = Circuit(4) connectivity = star_connectivity() @@ -102,6 +136,19 @@ def test_custom(custom_layout, give_circuit): assert layout == {"q0": 4, "q1": 3, "q2": 2, "q3": 1, "q4": 0} +@pytest.mark.parametrize("custom_layout", [[1, 0], {"q0": 1, "q2": 0}]) +def test_custom_restricted(custom_layout): + circuit = Circuit(2) + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2]) + placer = Custom(connectivity=restricted_connectivity, map=custom_layout) + layout = placer(circuit) + assert layout == {"q0": 1, "q2": 0} + assert_placement( + circuit=circuit, layout=layout, connectivity=restricted_connectivity + ) + + def test_custom_error_circuit(): circuit = Circuit(3) custom_layout = [4, 3, 2, 1, 0] @@ -165,6 +212,23 @@ def test_subgraph_error(): layout = placer(circuit) +def test_subgraph_restricted(): + circuit = Circuit(4) + circuit.add(gates.CNOT(0, 3)) + circuit.add(gates.CNOT(0, 1)) + circuit.add(gates.CNOT(3, 2)) + circuit.add(gates.CNOT(2, 1)) + circuit.add(gates.CNOT(1, 2)) + circuit.add(gates.CNOT(3, 1)) + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3, 4]) + placer = Subgraph(connectivity=restricted_connectivity) + layout = placer(circuit) + assert_placement( + circuit=circuit, layout=layout, connectivity=restricted_connectivity + ) + + @pytest.mark.parametrize("reps", [1, 10, 100]) def test_random(reps): connectivity = star_connectivity() @@ -182,6 +246,23 @@ def test_random_perfect(): assert_placement(star_circuit(), layout) +def test_random_restricted(): + circuit = Circuit(4) + circuit.add(gates.CNOT(1, 3)) + circuit.add(gates.CNOT(2, 1)) + circuit.add(gates.CNOT(3, 2)) + circuit.add(gates.CNOT(2, 1)) + circuit.add(gates.CNOT(1, 2)) + circuit.add(gates.CNOT(3, 1)) + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3, 4]) + placer = Random(connectivity=restricted_connectivity, samples=100) + layout = placer(circuit) + assert_placement( + circuit=circuit, layout=layout, connectivity=restricted_connectivity + ) + + @pytest.mark.parametrize("gates", [None, 5, 13]) def test_reverse_traversal(gates): circuit = star_circuit() @@ -199,3 +280,23 @@ def test_reverse_traversal_no_gates(): circuit = Circuit(5) with pytest.raises(ValueError): layout = placer(circuit) + + +def test_reverse_traversal_restricted(): + circuit = Circuit(4) + circuit.add(gates.CNOT(1, 3)) + circuit.add(gates.CNOT(2, 1)) + circuit.add(gates.CNOT(3, 2)) + circuit.add(gates.CNOT(2, 1)) + circuit.add(gates.CNOT(1, 2)) + circuit.add(gates.CNOT(3, 1)) + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3, 4]) + routing = ShortestPaths(connectivity=restricted_connectivity) + placer = ReverseTraversal( + connectivity=restricted_connectivity, routing_algorithm=routing, depth=5 + ) + layout = placer(circuit) + assert_placement( + circuit=circuit, layout=layout, connectivity=restricted_connectivity + ) diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index ea4ca73275..621f7f69e3 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -14,7 +14,6 @@ ShortestPaths, _find_gates_qubits_pairs, assert_connectivity, - restrict_connectivity_qubits, ) @@ -80,24 +79,6 @@ def matched_circuit(): return circuit -def test_restrict_qubits_error_no_subset(): - with pytest.raises(ConnectivityError) as excinfo: - restrict_connectivity_qubits(star_connectivity(), [1, 2, 6]) - assert "Some qubits are not in the original connectivity." in str(excinfo.value) - - -def test_restrict_qubits_error_not_connected(): - with pytest.raises(ConnectivityError) as excinfo: - restrict_connectivity_qubits(star_connectivity(), [1, 3]) - assert "The new connectivity graph is not connected." in str(excinfo.value) - - -def test_restrict_qubits(): - new_connectivity = restrict_connectivity_qubits(star_connectivity(), [1, 2, 3]) - assert list(new_connectivity.nodes) == [1, 2, 3] - assert list(new_connectivity.edges) == [(1, 2), (2, 3)] - - def test_assert_connectivity(): assert_connectivity(star_connectivity(), matched_circuit()) From d58778685bd4b4579b920f36aee135b793f1b70f Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Mon, 11 Dec 2023 18:46:20 +0400 Subject: [PATCH 04/19] refactor shortest paths --- src/qibo/transpiler/placer.py | 24 +- src/qibo/transpiler/router.py | 653 +++++++++++++---------------- src/qibo/transpiler/test_new_ss.py | 24 ++ tests/test_transpiler_router.py | 68 ++- 4 files changed, 388 insertions(+), 381 deletions(-) create mode 100644 src/qibo/transpiler/test_new_ss.py diff --git a/src/qibo/transpiler/placer.py b/src/qibo/transpiler/placer.py index f74a1bbf76..fb687a38b2 100644 --- a/src/qibo/transpiler/placer.py +++ b/src/qibo/transpiler/placer.py @@ -8,7 +8,6 @@ from qibo.models import Circuit from qibo.transpiler.abstract import Placer, Router from qibo.transpiler.exceptions import PlacementError -from qibo.transpiler.router import _find_gates_qubits_pairs def assert_placement( @@ -60,6 +59,29 @@ def assert_mapping_consistency(layout: dict, connectivity: nx.Graph = None): ) +def _find_gates_qubits_pairs(circuit: Circuit): + """Helper method for :meth:`qibo.transpiler.placer`. + Translate qibo circuit into a list of pairs of qubits to be used by the router and placer. + + Args: + circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. + + Returns: + gates_qubits_pairs (list): list containing pairs of qubits targeted by two qubits gates. + """ + gates_qubits_pairs = [] + for gate in circuit.queue: + if isinstance(gate, gates.M): + pass + elif len(gate.qubits) == 2: + gates_qubits_pairs.append(sorted(gate.qubits)) + elif len(gate.qubits) >= 3: + raise_error( + ValueError, "Gates targeting more than 2 qubits are not supported" + ) + return gates_qubits_pairs + + class Trivial(Placer): """Place qubits according to the following notation: diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index e3180d84bf..42f4905bdd 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -22,7 +22,15 @@ def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): circuit (:class:`qibo.models.circuit.Circuit`): circuit model to check. connectivity (:class:`networkx.Graph`): chip connectivity. """ - + if list(connectivity.nodes) != list(range(connectivity.number_of_nodes())): + node_mapping = {node: i for i, node in enumerate(connectivity.nodes)} + new_connectivity = nx.Graph() + for new_name in node_mapping.values(): + new_connectivity.add_node(new_name) + new_connectivity.add_edges_from( + [(node_mapping[u], node_mapping[v]) for u, v in connectivity.edges] + ) + connectivity = new_connectivity for gate in circuit.queue: if len(gate.qubits) > 2 and not isinstance(gate, gates.M): raise_error(ConnectivityError, f"{gate.name} acts on more than two qubits.") @@ -34,339 +42,6 @@ def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): ) -# TODO: make this class work with CircuitMap -class ShortestPaths(Router): - """A class to perform initial qubit mapping and connectivity matching. - - Args: - connectivity (:class:`networkx.Graph`): chip connectivity. - sampling_split (float, optional): fraction of paths tested - (between :math:`0` and :math:`1`). Defaults to :math:`1.0`. - verbose (bool, optional): If ``True``, print info messages. Defaults to ``False``. - """ - - def __init__( - self, connectivity: nx.Graph, sampling_split: float = 1.0, verbose: bool = False - ): - self.connectivity = connectivity - self.sampling_split = sampling_split - self.verbose = verbose - - self.initial_layout = None - self._added_swaps = 0 - self.final_map = None - self._gates_qubits_pairs = None - self._mapping = None - self._swap_map = None - self._added_swaps_list = [] - self._graph = None - self._qubit_map = None - self._transpiled_circuit = None - self._circuit_position = 0 - - def __call__(self, circuit: Circuit, initial_layout: dict): - """Circuit connectivity matching. - - Args: - circuit (:class:`qibo.models.circuit.Circuit`): circuit to be matched to hardware connectivity. - initial_layout (dict): initial physical-to-logical qubit mapping - - Returns: - (:class:`qibo.models.circuit.Circuit`, dict): circut mapped to hardware topology, and final qubit mapping. - """ - self._mapping = initial_layout - dict_keys = list(initial_layout.keys()) - init_qubit_map = np.asarray(list(initial_layout.values())) - self._initial_checks(circuit.nqubits) - self._gates_qubits_pairs = _find_gates_qubits_pairs(circuit) - self._mapping = dict(zip(range(len(initial_layout)), initial_layout.values())) - self._graph = nx.relabel_nodes(self.connectivity, self._mapping) - self._qubit_map = np.sort(init_qubit_map) - self._swap_map = deepcopy(init_qubit_map) - self._first_transpiler_step(circuit) - - while len(self._gates_qubits_pairs) != 0: - self._transpiler_step(circuit) - hardware_mapped_circuit = self._remap_circuit(np.argsort(init_qubit_map)) - final_mapping = { - dict_keys[j]: self._swap_map[j] - for j in range(self._graph.number_of_nodes()) - } - - return hardware_mapped_circuit, final_mapping - - @property - def added_swaps(self): - """Number of added swaps during transpiling.""" - return self._added_swaps - - @property - def sampling_split(self): - """Fraction of possible shortest paths to be analyzed.""" - return self._sampling_split - - @sampling_split.setter - def sampling_split(self, sampling_split: float): - """Set the sampling split, the fraction of possible shortest paths to be analyzed. - - Args: - sampling_split (float): define fraction of shortest path tested. - """ - - if 0.0 < sampling_split <= 1.0: - self._sampling_split = sampling_split - else: - raise_error(ValueError, "Sampling_split must be in (0:1].") - - def _transpiler_step(self, circuit: Circuit): - """Transpilation step. Find new mapping, add swap gates and apply gates that can be run with this configuration. - - Args: - circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. - """ - len_before_step = len(self._gates_qubits_pairs) - path, meeting_point = self._relocate() - self._add_swaps(path, meeting_point) - self._update_qubit_map() - self._add_gates(circuit, len_before_step - len(self._gates_qubits_pairs)) - - def _first_transpiler_step(self, circuit: Circuit): - """First transpilation step. Apply gates that can be run with the initial qubit mapping. - - Args: - circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. - """ - self._circuit_position = 0 - self._added_swaps = 0 - self._added_swaps_list = [] - len_2q_circuit = len(self._gates_qubits_pairs) - self._gates_qubits_pairs = self._reduce(self._graph) - self._add_gates(circuit, len_2q_circuit - len(self._gates_qubits_pairs)) - - def _reduce(self, graph: nx.Graph): - """Reduces the circuit by deleting two-qubit gates if it can be applied on the current configuration. - - Args: - graph (:class:`networkx.Graph`): current hardware qubit mapping. - - Returns: - (list): reduced circuit. - """ - new_circuit = self._gates_qubits_pairs.copy() - while ( - new_circuit != [] - and (new_circuit[0][0], new_circuit[0][1]) in graph.edges() - ): - del new_circuit[0] - return new_circuit - - def _map_list(self, path: list): - """Return all possible walks of qubits, or a fraction, for a given path. - - Args: - path (list): path to move qubits. - - Returns: - (list, list): all possible walks of qubits, or a fraction of them based on self.sampling_split, for a given path, and qubit meeting point for each path. - """ - path_ends = [path[0], path[-1]] - path_middle = path[1:-1] - mapping_list = [] - meeting_point_list = [] - test_paths = range(len(path) - 1) - if self.sampling_split != 1.0: - test_paths = np.random.choice( - test_paths, - size=int(np.ceil(len(test_paths) * self.sampling_split)), - replace=False, - ) - for i in test_paths: - values = path_middle[:i] + path_ends + path_middle[i:] - mapping = dict(zip(path, values)) - mapping_list.append(mapping) - meeting_point_list.append(i) - - return mapping_list, meeting_point_list - - def _relocate(self): - """Greedy algorithm to decide which path to take, and how qubits should walk. - - Returns: - (list, int): best path to move qubits and qubit meeting point in the path. - """ - nodes = self._graph.number_of_nodes() - circuit = self._reduce(self._graph) - final_circuit = circuit - keys = list(range(nodes)) - final_graph = self._graph - final_mapping = dict(zip(keys, keys)) - # Consider all shortest paths - path_list = [ - p - for p in nx.all_shortest_paths( - self._graph, source=circuit[0][0], target=circuit[0][1] - ) - ] - self._added_swaps += len(path_list[0]) - 2 - # Here test all paths - for path in path_list: - # map_list uses self.sampling_split - list_, meeting_point_list = self._map_list(path) - for j, mapping in enumerate(list_): - new_graph = nx.relabel_nodes(self._graph, mapping) - new_circuit = self._reduce(new_graph) - # Greedy looking for the optimal path and the optimal walk on this path - if len(new_circuit) < len(final_circuit): - final_graph = new_graph - final_circuit = new_circuit - final_mapping = mapping - final_path = path - meeting_point = meeting_point_list[j] - self._graph = final_graph - self._mapping = final_mapping - self._gates_qubits_pairs = final_circuit - - return final_path, meeting_point - - def _initial_checks(self, qubits: int): - """Initializes the transpiled circuit and check if it can be mapped to the defined connectivity. - - Args: - qubits (int): number of qubits in the circuit to be transpiled. - """ - nodes = self.connectivity.number_of_nodes() - if qubits > nodes: - raise_error( - ValueError, - "There are not enough physical qubits in the hardware to map the circuit.", - ) - if qubits == nodes: - new_circuit = Circuit(nodes) - else: - if self.verbose: - log.info( - "You are using more physical qubits than required by the circuit, some ancillary qubits will be added to the circuit." - ) - new_circuit = Circuit(nodes) - self._transpiled_circuit = new_circuit - - def _add_gates(self, circuit: Circuit, matched_gates: int): - """Adds one and two qubit gates to transpiled circuit until connectivity is matched. - - Args: - circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. - matched_gates (int): number of two-qubit gates that - can be applied with the current qubit mapping. - """ - index = 0 - while self._circuit_position < len(circuit.queue): - gate = circuit.queue[self._circuit_position] - if isinstance(gate, gates.M): - measured_qubits = gate.qubits - self._transpiled_circuit.add( - gate.on_qubits( - { - measured_qubits[i]: self._qubit_map[measured_qubits[i]] - for i in range(len(measured_qubits)) - } - ) - ) - self._circuit_position += 1 - elif len(gate.qubits) == 1: - self._transpiled_circuit.add( - gate.on_qubits({gate.qubits[0]: self._qubit_map[gate.qubits[0]]}) - ) - self._circuit_position += 1 - else: - index += 1 - if index == matched_gates + 1: - break - self._transpiled_circuit.add( - gate.on_qubits( - { - gate.qubits[0]: self._qubit_map[gate.qubits[0]], - gate.qubits[1]: self._qubit_map[gate.qubits[1]], - } - ) - ) - self._circuit_position += 1 - - def _add_swaps(self, path: list, meeting_point: int): - """Adds swaps to the transpiled circuit to move qubits. - - Args: - path (list): path to move qubits. - meeting_point (int): qubit meeting point in the path. - """ - forward = path[0 : meeting_point + 1] - backward = list(reversed(path[meeting_point + 1 :])) - if len(forward) > 1: - for f1, f2 in zip(forward[:-1], forward[1:]): - gate = gates.SWAP(self._qubit_map[f1], self._qubit_map[f2]) - self._transpiled_circuit.add(gate) - self._added_swaps_list.append(gate) - - if len(backward) > 1: - for b1, b2 in zip(backward[:-1], backward[1:]): - gate = gates.SWAP(self._qubit_map[b1], self._qubit_map[b2]) - self._transpiled_circuit.add(gate) - self._added_swaps_list.append(gate) - - def _update_swap_map(self, swap: tuple): - """Updates the qubit swap map.""" - temp = self._swap_map[swap[0]] - self._swap_map[swap[0]] = self._swap_map[swap[1]] - self._swap_map[swap[1]] = temp - - def _update_qubit_map(self): - """Update the qubit mapping after adding swaps.""" - old_mapping = self._qubit_map.copy() - for key, value in self._mapping.items(): - self._qubit_map[value] = old_mapping[key] - - def _remap_circuit(self, qubit_map): - """Map logical to physical qubits in a circuit. - - Args: - qubit_map (ndarray): new qubit mapping. - - Returns: - (:class:`qibo.models.circuit.Circuit`): transpiled circuit mapped with initial qubit mapping. - """ - new_circuit = Circuit(self._transpiled_circuit.nqubits) - for gate in self._transpiled_circuit.queue: - new_circuit.add(gate.on_qubits({q: qubit_map[q] for q in gate.qubits})) - if gate in self._added_swaps_list: - self._update_swap_map( - tuple(qubit_map[gate.qubits[i]] for i in range(2)) - ) - return new_circuit - - -def _find_gates_qubits_pairs(circuit: Circuit): - """Helper method for :meth:`qibo.transpiler.router.ShortestPaths`. - Translate qibo circuit into a list of pairs of qubits to be used by the router and placer. - - Args: - circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. - - Returns: - (list): list containing qubits targeted by two qubit gates. - """ - translated_circuit = [] - for gate in circuit.queue: - if isinstance(gate, gates.M): - pass - elif len(gate.qubits) == 2: - translated_circuit.append(sorted(gate.qubits)) - elif len(gate.qubits) >= 3: - raise_error( - ValueError, "Gates targeting more than 2 qubits are not supported" - ) - - return translated_circuit - - class CircuitMap: """Class to keep track of the circuit and physical-logical mapping during routing, this class also implements the initial two qubit blocks decomposition. @@ -383,6 +58,7 @@ def __init__(self, initial_layout: dict, circuit: Circuit, blocks=None): else: self.circuit_blocks = CircuitBlocks(circuit, index_names=True) self.initial_layout = initial_layout + self._graph_qubits_names = [int(key[1:]) for key in initial_layout.keys()] self._circuit_logical = list(range(len(initial_layout))) self._physical_logical = list(initial_layout.values()) self._routed_blocks = CircuitBlocks(Circuit(circuit.nqubits)) @@ -400,7 +76,9 @@ def execute_block(self, block: Block): """Executes a block by removing it from the circuit representation and adding it to the routed circuit. """ - self._routed_blocks.add_block(block.on_qubits(self.get_physical_qubits(block))) + self._routed_blocks.add_block( + block.on_qubits(self.get_physical_qubits(block, index=True)) + ) self.circuit_blocks.remove_block(block) def routed_circuit(self, circuit_kwargs=None): @@ -424,7 +102,7 @@ def update(self, swap: tuple): and add the SWAP gate to the routed blocks, the swap is represented by a tuple containing the logical qubits to be swapped. """ - physical_swap = self.logical_to_physical(swap) + physical_swap = self.logical_to_physical(swap, index=True) self._routed_blocks.add_block( Block(qubits=physical_swap, gates=[gates.SWAP(*physical_swap)]) ) @@ -438,14 +116,25 @@ def get_logical_qubits(self, block: Block): """Returns the current logical qubits where a block is acting""" return self.circuit_to_logical(block.qubits) - def get_physical_qubits(self, block: Block or int): - """Returns the physical qubits where a block is acting.""" + def get_physical_qubits(self, block: Block or int, index=False): + """Returns the physical qubits where a block is acting. + If index is True the qubits are returned as indices of the connectivity nodes. + """ if isinstance(block, int): block = self.circuit_blocks.search_by_index(block) - return self.logical_to_physical(self.get_logical_qubits(block)) + return self.logical_to_physical(self.get_logical_qubits(block), index=index) - def logical_to_physical(self, logical_qubits: tuple): - """Returns the physical qubits associated to the logical qubits.""" + def logical_to_physical(self, logical_qubits: tuple, index=False): + """Returns the physical qubits associated to the logical qubits. + If index is True the qubits are returned as indices of the connectivity nodes. + """ + if not index: + return tuple( + self._graph_qubits_names[ + self._physical_logical.index(logical_qubits[i]) + ] + for i in range(2) + ) return tuple(self._physical_logical.index(logical_qubits[i]) for i in range(2)) def circuit_to_logical(self, circuit_qubits: tuple): @@ -454,7 +143,250 @@ def circuit_to_logical(self, circuit_qubits: tuple): def circuit_to_physical(self, circuit_qubit: int): """Returns the current physical qubit associated to an initial circuit qubit.""" - return self._physical_logical.index(self._circuit_logical[circuit_qubit]) + return self._graph_qubits_names[ + self._physical_logical.index(self._circuit_logical[circuit_qubit]) + ] + + def physical_to_logical(self, physical_qubit: int): + """Returns the current logical qubit associated to a physical qubit (connectivity graph node).""" + physical_qubit_index = self._graph_qubits_names.index(physical_qubit) + return self._physical_logical[physical_qubit_index] + + +class ShortestPaths(Router): + """A class to perform initial qubit mapping and connectivity matching. + + Args: + connectivity (:class:`networkx.Graph`): chip connectivity. + sampling_split (float, optional): fraction of paths tested + (between :math:`0` and :math:`1`). Defaults to :math:`1.0`. + seed (int): seed for the random number generator. + """ + + def __init__(self, connectivity: nx.Graph, seed=42): + self.connectivity = connectivity + self._front_layer = None + self.circuit = None + self._dag = None + self._final_measurements = None + random.seed(seed) + + @property + def added_swaps(self): + """Number of SWAP gates added to the circuit during routing.""" + return self.circuit._swaps + + def __call__(self, circuit: Circuit, initial_layout: dict): + """Circuit connectivity matching. + + Args: + circuit (:class:`qibo.models.circuit.Circuit`): circuit to be matched to hardware connectivity. + initial_layout (dict): initial physical-to-logical qubit mapping + + Returns: + (:class:`qibo.models.circuit.Circuit`, dict): circut mapped to hardware topology, and final physical-to-logical qubit mapping. + """ + self._preprocessing(circuit=circuit, initial_layout=initial_layout) + while self._dag.number_of_nodes() != 0: + print("new step, front layer:", self._front_layer) + execute_block_list = self._check_execution() + print("executable gates:", execute_block_list) + if execute_block_list is not None: + self._execute_blocks(execute_block_list) + else: + self._find_new_mapping() + + routed_circuit = self.circuit.routed_circuit(circuit_kwargs=circuit.init_kwargs) + if self._final_measurements is not None: + routed_circuit = self._append_final_measurements( + routed_circuit=routed_circuit + ) + + return routed_circuit, self.circuit.final_layout() + + def _find_new_mapping(self): + """Find new qubit mapping. The mapping is found by looking for the shortest path.""" + candidates_evaluation = [] + for candidate in self._candidates(): + cost = self._compute_cost(candidate) + candidates_evaluation.append((candidate, cost)) + print("candidate evaluation:", (candidate, cost)) + best_cost = min(candidate[1] for candidate in candidates_evaluation) + best_candidates = [ + candidate[0] + for candidate in candidates_evaluation + if candidate[1] == best_cost + ] + print("best cost:", best_cost) + print("best candidates:", best_candidates) + best_candidate = random.choice(best_candidates) + print("best candidate:", best_candidate) + self._add_swaps(best_candidate, self.circuit) + + def _candidates(self): + """Return all possible shortest paths, + a list contains the new mapping and a second list contains the path meeting point. + """ + target_qubits = self.circuit.get_physical_qubits(self._front_layer[0]) + path_list = list( + nx.all_shortest_paths( + self.connectivity, source=target_qubits[0], target=target_qubits[1] + ) + ) + all_candidates = [] + for path in path_list: + for meeting_point in range(len(path) - 1): + all_candidates.append((path, meeting_point)) + return all_candidates + + @staticmethod + def _add_swaps(candidate: tuple, circuitmap: CircuitMap): + """Adds swaps to the circuit to move qubits. + + Args: + candidate (tuple): contains path to move qubits and qubit meeting point in the path. + circuitmap (CircuitMap): representation of the circuit. + """ + path = candidate[0] + meeting_point = candidate[1] + forward = path[0 : meeting_point + 1] + backward = list(reversed(path[meeting_point + 1 :])) + if len(forward) > 1: + for f1, f2 in zip(forward[:-1], forward[1:]): + circuitmap.update( + ( + circuitmap.physical_to_logical(f1), + circuitmap.physical_to_logical(f2), + ) + ) + if len(backward) > 1: + for b1, b2 in zip(backward[:-1], backward[1:]): + circuitmap.update( + ( + circuitmap.physical_to_logical(b1), + circuitmap.physical_to_logical(b2), + ) + ) + + def _compute_cost(self, candidate): + """Greedy algorithm to decide which path to take, and how qubits should walk. + + Returns: + (list, int): best path to move qubits and qubit meeting point in the path. + """ + temporary_circuit = CircuitMap( + initial_layout=self.circuit.initial_layout, + circuit=Circuit(len(self.circuit.initial_layout)), + blocks=self.circuit.circuit_blocks, + ) + temporary_circuit.set_circuit_logical(deepcopy(self.circuit._circuit_logical)) + self._add_swaps(candidate, temporary_circuit) + temporary_dag = deepcopy(self._dag) + successive_executed_gates = 0 + while temporary_dag.number_of_nodes() != 0: + for layer, nodes in enumerate(nx.topological_generations(temporary_dag)): + for node in nodes: + temporary_dag.nodes[node]["layer"] = layer + temporary_front_layer = [ + node[0] for node in temporary_dag.nodes(data="layer") if node[1] == 0 + ] + all_executed = True + for block in temporary_front_layer: + print("check temp block:", block) + if ( + temporary_circuit.get_physical_qubits(block) + in self.connectivity.edges + or not temporary_circuit.circuit_blocks.search_by_index( + block + ).entangled + ): + successive_executed_gates += 1 + temporary_circuit.execute_block( + temporary_circuit.circuit_blocks.search_by_index(block) + ) + temporary_dag.remove_node(block) + print("executed") + else: + all_executed = False + if not all_executed: + break + print("cost:", successive_executed_gates) + return -successive_executed_gates + + def _check_execution(self): + """Check if some blocks in the front layer can be executed in the current configuration. + + Returns: + (list): executable blocks if there are, ``None`` otherwise. + """ + executable_blocks = [] + for block in self._front_layer: + if ( + self.circuit.get_physical_qubits(block) in self.connectivity.edges + or not self.circuit.circuit_blocks.search_by_index(block).entangled + ): + executable_blocks.append(block) + if len(executable_blocks) == 0: + return None + return executable_blocks + + def _execute_blocks(self, blocklist: list): + """Execute a list of blocks: + -Remove the correspondent nodes from the dag and circuit representation. + -Add the executed blocks to the routed circuit. + -Update the dag layers and front layer. + """ + for block_id in blocklist: + block = self.circuit.circuit_blocks.search_by_index(block_id) + self.circuit.execute_block(block) + self._dag.remove_node(block_id) + self._update_front_layer() + + def _update_front_layer(self): + """Update the front layer of the dag.""" + for layer, nodes in enumerate(nx.topological_generations(self._dag)): + for node in nodes: + self._dag.nodes[node]["layer"] = layer + self._front_layer = [ + node[0] for node in self._dag.nodes(data="layer") if node[1] == 0 + ] + + def _preprocessing(self, circuit: Circuit, initial_layout: dict): + """The following objects will be initialised: + - circuit: class to represent circuit and to perform logical-physical qubit mapping. + - _final_measurements: measurement gates at the end of the circuit. + - _front_layer: list containing the blocks to be executed. + """ + copied_circuit = _copy_circuit(circuit) + self._final_measurements = self._detach_final_measurements(copied_circuit) + self.circuit = CircuitMap(initial_layout, copied_circuit) + self._dag = _create_dag(self.circuit.blocks_qubits_pairs()) + self._update_front_layer() + + def _detach_final_measurements(self, circuit: Circuit): + """Detach measurement gates at the end of the circuit for separate handling.""" + final_measurements = [] + for gate in circuit.queue[::-1]: + if isinstance(gate, gates.M): + final_measurements.append(gate) + circuit.queue.remove(gate) + else: + break + if not final_measurements: + return None + return final_measurements[::-1] + + def _append_final_measurements(self, routed_circuit: Circuit): + """Append the final measurment gates on the correct qubits conserving the measurement register.""" + for measurement in self._final_measurements: + original_qubits = measurement.qubits + routed_qubits = ( + self.circuit.circuit_to_physical(qubit) for qubit in original_qubits + ) + routed_circuit.add( + measurement.on_qubits(dict(zip(original_qubits, routed_qubits))) + ) + return routed_circuit class Sabre(Router): @@ -536,9 +468,9 @@ def _preprocessing(self, circuit: Circuit, initial_layout: dict): - _delta_register: list containing the special weigh added to qubits to prevent overlapping swaps. """ - copy_circuit = self._copy_circuit(circuit) - self._final_measurements = self._detach_final_measurements(copy_circuit) - self.circuit = CircuitMap(initial_layout, copy_circuit) + copied_circuit = _copy_circuit(circuit) + self._final_measurements = self._detach_final_measurements(copied_circuit) + self.circuit = CircuitMap(initial_layout, copied_circuit) self._dist_matrix = nx.floyd_warshall_numpy(self.connectivity) self._dag = _create_dag(self.circuit.blocks_qubits_pairs()) self._memory_map = [] @@ -546,15 +478,6 @@ def _preprocessing(self, circuit: Circuit, initial_layout: dict): self._update_front_layer() self._delta_register = [1.0 for _ in range(circuit.nqubits)] - @staticmethod - def _copy_circuit(circuit: Circuit): - """Return a copy of the circuit to avoid altering the original circuit. - This copy conserves the registers of the measurement gates.""" - new_circuit = Circuit(circuit.nqubits) - for gate in circuit.queue: - new_circuit.add(gate) - return new_circuit - def _detach_final_measurements(self, circuit: Circuit): """Detach measurement gates at the end of the circuit for separate handling.""" final_measurements = [] @@ -605,7 +528,7 @@ def _find_new_mapping(self): key for key, value in candidates_evaluation.items() if value == best_cost ] best_candidate = random.choice(best_candidates) - for qubit in self.circuit.logical_to_physical(best_candidate): + for qubit in self.circuit.logical_to_physical(best_candidate, index=True): self._delta_register[qubit] += self.delta self.circuit.update(best_candidate) @@ -626,7 +549,7 @@ def _compute_cost(self, candidate): layer_gates = self._get_dag_layer(layer) avg_layer_distance = 0.0 for gate in layer_gates: - qubits = temporary_circuit.get_physical_qubits(gate) + qubits = temporary_circuit.get_physical_qubits(gate, index=True) avg_layer_distance += ( max(self._delta_register[i] for i in qubits) * (self._dist_matrix[qubits[0], qubits[1]] - 1.0) @@ -647,8 +570,8 @@ def _swap_candidates(self): candidate = tuple( sorted( ( - self.circuit._physical_logical[qubit], - self.circuit._physical_logical[connected], + self.circuit.physical_to_logical(qubit), + self.circuit.physical_to_logical(connected), ) ) ) @@ -657,7 +580,7 @@ def _swap_candidates(self): return candidates def _check_execution(self): - """Check if some gatesblocks in the front layer can be executed in the current configuration. + """Check if some blocks in the front layer can be executed in the current configuration. Returns: (list): executable blocks if there are, ``None`` otherwise. @@ -690,6 +613,18 @@ def _execute_blocks(self, blocklist: list): self._delta_register = [1.0 for _ in self._delta_register] +def _copy_circuit(circuit: Circuit): + """Helper method for :meth:`qibo.transpiler.router`. + + Return a copy of the circuit to avoid altering the original circuit. + This copy conserves the registers of the measurement gates. + """ + new_circuit = Circuit(circuit.nqubits) + for gate in circuit.queue: + new_circuit.add(gate) + return new_circuit + + def _create_dag(gates_qubits_pairs): """Helper method for :meth:`qibo.transpiler.router.Sabre`. Create direct acyclic graph (dag) of the circuit based on two qubit gates commutativity relations. diff --git a/src/qibo/transpiler/test_new_ss.py b/src/qibo/transpiler/test_new_ss.py new file mode 100644 index 0000000000..67b1c7ca0f --- /dev/null +++ b/src/qibo/transpiler/test_new_ss.py @@ -0,0 +1,24 @@ +import networkx as nx + +from qibo import Circuit, gates +from qibo.transpiler.router import ShortestPaths + + +def star_connectivity(): + Q = [i for i in range(5)] + chip = nx.Graph() + chip.add_nodes_from(Q) + graph_list = [(Q[i], Q[2]) for i in range(5) if i != 2] + chip.add_edges_from(graph_list) + return chip + + +circuit = Circuit(5) +circuit.add(gates.CNOT(1, 3)) +circuit.add(gates.CNOT(2, 1)) +circuit.add(gates.CNOT(4, 1)) +initial_layout = {"q0": 0, "q1": 1, "q2": 2, "q3": 3, "q4": 4} +transpiler = ShortestPaths(connectivity=star_connectivity()) +routed_circ, final_layout = transpiler(circuit, initial_layout) + +print(routed_circ.draw()) diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index 621f7f69e3..1c5dbdbc57 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -5,14 +5,16 @@ from qibo import gates from qibo.models import Circuit from qibo.transpiler.optimizer import Preprocessing -from qibo.transpiler.pipeline import assert_circuit_equivalence +from qibo.transpiler.pipeline import ( + assert_circuit_equivalence, + restrict_connectivity_qubits, +) from qibo.transpiler.placer import Custom, Random, Subgraph, Trivial, assert_placement from qibo.transpiler.router import ( CircuitMap, ConnectivityError, Sabre, ShortestPaths, - _find_gates_qubits_pairs, assert_connectivity, ) @@ -105,22 +107,6 @@ def test_split_setter(split): ) -def test_insufficient_qubits(): - circuit = generate_random_circuit(10, 20) - placer = Trivial() - initial_layout = placer(circuit) - transpiler = ShortestPaths(connectivity=star_connectivity()) - with pytest.raises(ValueError): - transpiler(circuit, initial_layout) - - -def test_find_pairs_error(): - circuit = Circuit(3) - circuit.add(gates.TOFFOLI(0, 1, 2)) - with pytest.raises(ValueError): - _find_gates_qubits_pairs(circuit) - - @pytest.mark.parametrize("gates", [5, 25]) @pytest.mark.parametrize("qubits", [3, 5]) @pytest.mark.parametrize("placer", [Trivial, Random]) @@ -130,9 +116,7 @@ def test_random_circuits_5q(gates, qubits, placer, connectivity, split): placer = placer(connectivity=connectivity) layout_circ = Circuit(5) initial_layout = placer(layout_circ) - transpiler = ShortestPaths( - connectivity=connectivity, verbose=True, sampling_split=split - ) + transpiler = ShortestPaths(connectivity=connectivity, sampling_split=split) circuit = generate_random_circuit(nqubits=qubits, ngates=gates) transpiled_circuit, final_qubit_map = transpiler(circuit, initial_layout) assert transpiler.added_swaps >= 0 @@ -341,3 +325,45 @@ def test_sabre_intermediate_measurements(): routed_circ, final_layout = router(circuit=circ, initial_layout=initial_layout) circuit_result = routed_circ.execute(nshots=100) assert routed_circ.queue[3].result is measurement.result + + +def test_sabre_restrict_qubits(): + circ = Circuit(3) + circ.add(gates.CZ(0, 1)) + circ.add(gates.CZ(0, 2)) + circ.add(gates.CZ(2, 1)) + initial_layout = {"q0": 0, "q2": 2, "q3": 1} + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3]) + router = Sabre(connectivity=restricted_connectivity) + routed_circ, final_layout = router(circuit=circ, initial_layout=initial_layout) + assert_circuit_equivalence( + original_circuit=circ, + transpiled_circuit=routed_circ, + final_map=final_layout, + initial_map=initial_layout, + ) + assert_connectivity(restricted_connectivity, routed_circ) + print(final_layout) + print(routed_circ.draw()) + assert_placement(routed_circ, final_layout, connectivity=restricted_connectivity) + + +def test_shortest_paths_restrict_qubits(): + circ = Circuit(3) + circ.add(gates.CZ(0, 1)) + circ.add(gates.CZ(0, 2)) + circ.add(gates.CZ(2, 1)) + initial_layout = {"q0": 0, "q2": 2, "q3": 1} + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3]) + router = ShortestPaths(connectivity=restricted_connectivity) + routed_circ, final_layout = router(circuit=circ, initial_layout=initial_layout) + assert_circuit_equivalence( + original_circuit=circ, + transpiled_circuit=routed_circ, + final_map=final_layout, + initial_map=initial_layout, + ) + assert_connectivity(restricted_connectivity, routed_circ) + assert_placement(routed_circ, final_layout, connectivity=restricted_connectivity) From 1e87f5aa77c9e064f46b4559facf103306f15b96 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Tue, 12 Dec 2023 14:45:49 +0400 Subject: [PATCH 05/19] completed new shortest paths --- src/qibo/transpiler/router.py | 2 +- src/qibo/transpiler/test_new_ss.py | 2 ++ tests/test_transpiler_router.py | 16 +++------------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 42f4905bdd..03462b53a4 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -277,7 +277,7 @@ def _compute_cost(self, candidate): temporary_circuit = CircuitMap( initial_layout=self.circuit.initial_layout, circuit=Circuit(len(self.circuit.initial_layout)), - blocks=self.circuit.circuit_blocks, + blocks=deepcopy(self.circuit.circuit_blocks), ) temporary_circuit.set_circuit_logical(deepcopy(self.circuit._circuit_logical)) self._add_swaps(candidate, temporary_circuit) diff --git a/src/qibo/transpiler/test_new_ss.py b/src/qibo/transpiler/test_new_ss.py index 67b1c7ca0f..e878c3f214 100644 --- a/src/qibo/transpiler/test_new_ss.py +++ b/src/qibo/transpiler/test_new_ss.py @@ -17,6 +17,8 @@ def star_connectivity(): circuit.add(gates.CNOT(1, 3)) circuit.add(gates.CNOT(2, 1)) circuit.add(gates.CNOT(4, 1)) +circuit.add(gates.CNOT(4, 2)) +circuit.add(gates.CNOT(4, 3)) initial_layout = {"q0": 0, "q1": 1, "q2": 2, "q3": 3, "q4": 4} transpiler = ShortestPaths(connectivity=star_connectivity()) routed_circ, final_layout = transpiler(circuit, initial_layout) diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index 1c5dbdbc57..f20d970608 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -99,25 +99,15 @@ def test_assert_connectivity_3q(): assert_connectivity(star_connectivity(), circuit) -@pytest.mark.parametrize("split", [2.0, -1.0]) -def test_split_setter(split): - with pytest.raises(ValueError): - transpiler = ShortestPaths( - connectivity=star_connectivity(), sampling_split=split - ) - - @pytest.mark.parametrize("gates", [5, 25]) -@pytest.mark.parametrize("qubits", [3, 5]) @pytest.mark.parametrize("placer", [Trivial, Random]) @pytest.mark.parametrize("connectivity", [star_connectivity(), grid_connectivity()]) -@pytest.mark.parametrize("split", [1.0, 0.5]) -def test_random_circuits_5q(gates, qubits, placer, connectivity, split): +def test_random_circuits_5q(gates, placer, connectivity): placer = placer(connectivity=connectivity) layout_circ = Circuit(5) initial_layout = placer(layout_circ) - transpiler = ShortestPaths(connectivity=connectivity, sampling_split=split) - circuit = generate_random_circuit(nqubits=qubits, ngates=gates) + transpiler = ShortestPaths(connectivity=connectivity) + circuit = generate_random_circuit(nqubits=5, ngates=gates) transpiled_circuit, final_qubit_map = transpiler(circuit, initial_layout) assert transpiler.added_swaps >= 0 assert_connectivity(connectivity, transpiled_circuit) From d2d6abdd1733b217c275f52f5abb355cc8a4935c Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Tue, 12 Dec 2023 14:54:36 +0400 Subject: [PATCH 06/19] remove prints --- src/qibo/transpiler/placer.py | 2 +- src/qibo/transpiler/router.py | 10 +--------- src/qibo/transpiler/test_new_ss.py | 26 -------------------------- tests/test_transpiler_router.py | 2 -- 4 files changed, 2 insertions(+), 38 deletions(-) delete mode 100644 src/qibo/transpiler/test_new_ss.py diff --git a/src/qibo/transpiler/placer.py b/src/qibo/transpiler/placer.py index fb687a38b2..97bfc09a56 100644 --- a/src/qibo/transpiler/placer.py +++ b/src/qibo/transpiler/placer.py @@ -11,7 +11,7 @@ def assert_placement( - circuit: Circuit, layout: dict, connectivity: nx.graph = None + circuit: Circuit, layout: dict, connectivity: nx.Graph = None ) -> bool: """Check if layout is in the correct form and matches the number of qubits of the circuit. diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 03462b53a4..dd9f583130 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -188,9 +188,7 @@ def __call__(self, circuit: Circuit, initial_layout: dict): """ self._preprocessing(circuit=circuit, initial_layout=initial_layout) while self._dag.number_of_nodes() != 0: - print("new step, front layer:", self._front_layer) execute_block_list = self._check_execution() - print("executable gates:", execute_block_list) if execute_block_list is not None: self._execute_blocks(execute_block_list) else: @@ -210,17 +208,13 @@ def _find_new_mapping(self): for candidate in self._candidates(): cost = self._compute_cost(candidate) candidates_evaluation.append((candidate, cost)) - print("candidate evaluation:", (candidate, cost)) best_cost = min(candidate[1] for candidate in candidates_evaluation) best_candidates = [ candidate[0] for candidate in candidates_evaluation if candidate[1] == best_cost ] - print("best cost:", best_cost) - print("best candidates:", best_candidates) best_candidate = random.choice(best_candidates) - print("best candidate:", best_candidate) self._add_swaps(best_candidate, self.circuit) def _candidates(self): @@ -270,6 +264,7 @@ def _add_swaps(candidate: tuple, circuitmap: CircuitMap): def _compute_cost(self, candidate): """Greedy algorithm to decide which path to take, and how qubits should walk. + The cost is computed as minus the number of successive gates that can be executed. Returns: (list, int): best path to move qubits and qubit meeting point in the path. @@ -292,7 +287,6 @@ def _compute_cost(self, candidate): ] all_executed = True for block in temporary_front_layer: - print("check temp block:", block) if ( temporary_circuit.get_physical_qubits(block) in self.connectivity.edges @@ -305,12 +299,10 @@ def _compute_cost(self, candidate): temporary_circuit.circuit_blocks.search_by_index(block) ) temporary_dag.remove_node(block) - print("executed") else: all_executed = False if not all_executed: break - print("cost:", successive_executed_gates) return -successive_executed_gates def _check_execution(self): diff --git a/src/qibo/transpiler/test_new_ss.py b/src/qibo/transpiler/test_new_ss.py deleted file mode 100644 index e878c3f214..0000000000 --- a/src/qibo/transpiler/test_new_ss.py +++ /dev/null @@ -1,26 +0,0 @@ -import networkx as nx - -from qibo import Circuit, gates -from qibo.transpiler.router import ShortestPaths - - -def star_connectivity(): - Q = [i for i in range(5)] - chip = nx.Graph() - chip.add_nodes_from(Q) - graph_list = [(Q[i], Q[2]) for i in range(5) if i != 2] - chip.add_edges_from(graph_list) - return chip - - -circuit = Circuit(5) -circuit.add(gates.CNOT(1, 3)) -circuit.add(gates.CNOT(2, 1)) -circuit.add(gates.CNOT(4, 1)) -circuit.add(gates.CNOT(4, 2)) -circuit.add(gates.CNOT(4, 3)) -initial_layout = {"q0": 0, "q1": 1, "q2": 2, "q3": 3, "q4": 4} -transpiler = ShortestPaths(connectivity=star_connectivity()) -routed_circ, final_layout = transpiler(circuit, initial_layout) - -print(routed_circ.draw()) diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index f20d970608..bee84e47b6 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -334,8 +334,6 @@ def test_sabre_restrict_qubits(): initial_map=initial_layout, ) assert_connectivity(restricted_connectivity, routed_circ) - print(final_layout) - print(routed_circ.draw()) assert_placement(routed_circ, final_layout, connectivity=restricted_connectivity) From 9b31bfa047910dfc878e8ebd0cd664210d228506 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Wed, 13 Dec 2023 14:39:38 +0400 Subject: [PATCH 07/19] fix coverage and tests --- src/qibo/transpiler/pipeline.py | 11 ++++- src/qibo/transpiler/placer.py | 2 +- src/qibo/transpiler/star_connectivity.py | 2 + tests/test_transpiler_pipeline.py | 55 ++++++++++++++++++++++-- tests/test_transpiler_placer.py | 25 ++++++++++- 5 files changed, 87 insertions(+), 8 deletions(-) diff --git a/src/qibo/transpiler/pipeline.py b/src/qibo/transpiler/pipeline.py index 96e4be76d6..289429e40f 100644 --- a/src/qibo/transpiler/pipeline.py +++ b/src/qibo/transpiler/pipeline.py @@ -120,8 +120,12 @@ def assert_transpiling( if original_circuit.nqubits != transpiled_circuit.nqubits: qubit_matcher = Preprocessing(connectivity=connectivity) original_circuit = qubit_matcher(circuit=original_circuit) - assert_placement(circuit=original_circuit, layout=initial_layout) - assert_placement(circuit=transpiled_circuit, layout=final_layout) + assert_placement( + circuit=original_circuit, layout=initial_layout, connectivity=connectivity + ) + assert_placement( + circuit=transpiled_circuit, layout=final_layout, connectivity=connectivity + ) if check_circuit_equivalence: assert_circuit_equivalence( original_circuit=original_circuit, @@ -207,8 +211,10 @@ def __call__(self, circuit): final_layout = None for transpiler_pass in self.passes: if isinstance(transpiler_pass, Optimizer): + transpiler_pass.connectivity = self.connectivity circuit = transpiler_pass(circuit) elif isinstance(transpiler_pass, Placer): + transpiler_pass.connectivity = self.connectivity if self.initial_layout == None: self.initial_layout = transpiler_pass(circuit) else: @@ -217,6 +223,7 @@ def __call__(self, circuit): "You are defining more than one placer pass.", ) elif isinstance(transpiler_pass, Router): + transpiler_pass.connectivity = self.connectivity if self.initial_layout is not None: circuit, final_layout = transpiler_pass( circuit, self.initial_layout diff --git a/src/qibo/transpiler/placer.py b/src/qibo/transpiler/placer.py index 97bfc09a56..d47bc30da0 100644 --- a/src/qibo/transpiler/placer.py +++ b/src/qibo/transpiler/placer.py @@ -272,7 +272,7 @@ def __call__(self, circuit): return dict(zip(dict_keys, list(mapping.values()))) if cost < final_cost: final_graph = graph - final_mapping = {dict_keys[i]: mapping[i] for i in range(len(mapping))} + final_mapping = mapping final_cost = cost return dict(zip(dict_keys, list(final_mapping.values()))) diff --git a/src/qibo/transpiler/star_connectivity.py b/src/qibo/transpiler/star_connectivity.py index 3439211be9..693df030c1 100644 --- a/src/qibo/transpiler/star_connectivity.py +++ b/src/qibo/transpiler/star_connectivity.py @@ -3,6 +3,7 @@ from qibo.transpiler.router import ConnectivityError +# TODO: split into routing plus placer steps class StarConnectivity(Router): """Transforms an arbitrary circuit to one that can be executed on hardware. @@ -23,6 +24,7 @@ class StarConnectivity(Router): def __init__(self, connectivity=None, middle_qubit: int = 2): self.middle_qubit = middle_qubit + self.connectivity = connectivity def __call__(self, circuit: Circuit, initial_layout=None): """Apply the transpiler transformation on a given circuit. diff --git a/tests/test_transpiler_pipeline.py b/tests/test_transpiler_pipeline.py index bc82956485..feeaba4e84 100644 --- a/tests/test_transpiler_pipeline.py +++ b/tests/test_transpiler_pipeline.py @@ -14,7 +14,7 @@ restrict_connectivity_qubits, ) from qibo.transpiler.placer import Random, ReverseTraversal, Trivial -from qibo.transpiler.router import ShortestPaths +from qibo.transpiler.router import Sabre, ShortestPaths from qibo.transpiler.unroller import NativeGates, Unroller @@ -174,13 +174,24 @@ def test_is_satisfied_false_connectivity(): assert not default_transpiler.is_satisfied(circuit) +@pytest.mark.parametrize("reps", [range(3)]) +@pytest.mark.parametrize("placer", [Random, Trivial, ReverseTraversal]) +@pytest.mark.parametrize("routing", [ShortestPaths, Sabre]) @pytest.mark.parametrize( "circ", [generate_random_circuit(nqubits=5, ngates=20), small_circuit()] ) -def test_custom_passes(circ): +def test_custom_passes(circ, placer, routing, reps): custom_passes = [] custom_passes.append(Preprocessing(connectivity=star_connectivity())) - custom_passes.append(Random(connectivity=star_connectivity())) + if placer == ReverseTraversal: + custom_passes.append( + placer( + connectivity=star_connectivity(), + routing_algorithm=routing(connectivity=star_connectivity()), + ) + ) + else: + custom_passes.append(placer(connectivity=star_connectivity())) custom_passes.append(ShortestPaths(connectivity=star_connectivity())) custom_passes.append(Unroller(native_gates=NativeGates.default())) custom_pipeline = Passes( @@ -200,6 +211,44 @@ def test_custom_passes(circ): ) +@pytest.mark.parametrize("reps", [range(3)]) +@pytest.mark.parametrize("placer", [Random, Trivial, ReverseTraversal]) +@pytest.mark.parametrize("routing", [ShortestPaths, Sabre]) +def test_custom_passes_restict(reps, placer, routing): + circ = generate_random_circuit(nqubits=3, ngates=20) + custom_passes = [] + custom_passes.append(Preprocessing(connectivity=star_connectivity())) + if placer == ReverseTraversal: + custom_passes.append( + placer( + connectivity=star_connectivity(), + routing_algorithm=routing(connectivity=star_connectivity()), + ) + ) + else: + custom_passes.append(placer(connectivity=star_connectivity())) + custom_passes.append(routing(connectivity=star_connectivity())) + custom_passes.append(Unroller(native_gates=NativeGates.default())) + custom_pipeline = Passes( + custom_passes, + connectivity=star_connectivity(), + native_gates=NativeGates.default(), + on_qubits=[1, 2, 3], + ) + transpiled_circ, final_layout = custom_pipeline(circ) + initial_layout = custom_pipeline.get_initial_layout() + print(initial_layout) + print(final_layout) + assert_transpiling( + original_circuit=circ, + transpiled_circuit=transpiled_circ, + connectivity=restrict_connectivity_qubits(star_connectivity(), [1, 2, 3]), + initial_layout=initial_layout, + final_layout=final_layout, + native_gates=NativeGates.default(), + ) + + @pytest.mark.parametrize( "circ", [generate_random_circuit(nqubits=5, ngates=20), small_circuit()] ) diff --git a/tests/test_transpiler_placer.py b/tests/test_transpiler_placer.py index ae34bcec1e..c76e341981 100644 --- a/tests/test_transpiler_placer.py +++ b/tests/test_transpiler_placer.py @@ -11,6 +11,7 @@ ReverseTraversal, Subgraph, Trivial, + _find_gates_qubits_pairs, assert_mapping_consistency, assert_placement, ) @@ -92,6 +93,22 @@ def test_mapping_consistency_restricted_error(layout): assert_mapping_consistency(layout, restricted_connecyivity) +def test_gates_qubits_pairs(): + circuit = Circuit(5) + circuit.add(gates.CNOT(0, 1)) + circuit.add(gates.CNOT(1, 2)) + circuit.add(gates.M(1, 2)) + gates_qubits_pairs = _find_gates_qubits_pairs(circuit) + assert gates_qubits_pairs == [[0, 1], [1, 2]] + + +def test_gates_qubits_pairs_error(): + circuit = Circuit(5) + circuit.add(gates.TOFFOLI(0, 1, 2)) + with pytest.raises(ValueError): + gates_qubits_pairs = _find_gates_qubits_pairs(circuit) + + def test_trivial(): circuit = Circuit(5) connectivity = star_connectivity() @@ -125,12 +142,16 @@ def test_trivial_error(): "custom_layout", [[4, 3, 2, 1, 0], {"q0": 4, "q1": 3, "q2": 2, "q3": 1, "q4": 0}] ) @pytest.mark.parametrize("give_circuit", [True, False]) -def test_custom(custom_layout, give_circuit): +@pytest.mark.parametrize("give_connectivity", [True, False]) +def test_custom(custom_layout, give_circuit, give_connectivity): if give_circuit: circuit = Circuit(5) else: circuit = None - connectivity = star_connectivity() + if give_connectivity: + connectivity = star_connectivity() + else: + connectivity = None placer = Custom(connectivity=connectivity, map=custom_layout) layout = placer(circuit) assert layout == {"q0": 4, "q1": 3, "q2": 2, "q3": 1, "q4": 0} From 44e2560631f4e90ebf068d9da3e9aa7dd05aa28b Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Thu, 14 Dec 2023 12:34:56 +0400 Subject: [PATCH 08/19] small correction in unroller --- src/qibo/transpiler/unroller.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/qibo/transpiler/unroller.py b/src/qibo/transpiler/unroller.py index ef5f0c8d30..1358056600 100644 --- a/src/qibo/transpiler/unroller.py +++ b/src/qibo/transpiler/unroller.py @@ -11,9 +11,8 @@ class NativeGates(Flag): """Define native gates supported by the unroller. A native gate set should contain at least one two-qubit gate (CZ or iSWAP) and at least one single qubit gate (GPI2 or U3). - Gates I, Z, RZ and M are always included in the single qubit native gates set. - - Should have the same names with qibo gates. + Possible values are: + I, Z, RZ, M, GPI2, U3, CZ, iSWAP. """ I = auto() @@ -56,8 +55,6 @@ class Unroller: """Translates a circuit to native gates. Args: - circuit (:class:`qibo.models.circuit.Circuit`): circuit model to translate - into native gates. native_gates (:class:`qibo.transpiler.unroller.NativeGates`): native gates to use in the transpiled circuit. Returns: @@ -71,6 +68,14 @@ def __init__( self.native_gates = native_gates def __call__(self, circuit: Circuit): + """Decomposes a circuit into native gates. + + Args: + circuit (:class:`qibo.models.circuit.Circuit`): circuit model to decompose. + + Returns: + (:class:`qibo.models.circuit.Circuit`): equivalent circuit with native gates. + """ translated_circuit = circuit.__class__(circuit.nqubits) for gate in circuit.queue: translated_circuit.add( From 150e29a842fe98a658f2aebfde334eee55d4f925 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 09:50:21 +0400 Subject: [PATCH 09/19] improve docstrings `pipeline` --- src/qibo/transpiler/pipeline.py | 48 +++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/qibo/transpiler/pipeline.py b/src/qibo/transpiler/pipeline.py index 289429e40f..a88c90de10 100644 --- a/src/qibo/transpiler/pipeline.py +++ b/src/qibo/transpiler/pipeline.py @@ -149,15 +149,17 @@ def restrict_connectivity_qubits(connectivity: nx.Graph, qubits: list): raise_error( ConnectivityError, "Some qubits are not in the original connectivity." ) + new_connectivity = nx.Graph() new_connectivity.add_nodes_from(qubits) - new_edges = [] - for edge in connectivity.edges: - if edge[0] in qubits and edge[1] in qubits: - new_edges.append(edge) + new_edges = [ + edge for edge in connectivity.edges if edge[0] in qubits and edge[1] in qubits + ] new_connectivity.add_edges_from(new_edges) + if not nx.is_connected(new_connectivity): - raise_error(ConnectivityError, "The new connectivity graph is not connected.") + raise_error(ConnectivityError, "New connectivity graph is not connected.") + return new_connectivity @@ -165,17 +167,22 @@ class Passes: """Define a transpiler pipeline consisting of smaller transpiler steps that are applied sequentially: Args: - passes (list): list of passes to be applied sequentially, - if None default transpiler will be used. - connectivity (nx.Graph): physical qubits connectivity. - native_gates (NativeGates): native gates. - on_qubits (list): list of physical qubits to be used. If "None" all qubits are used. + passes (list, optional): list of passes to be applied sequentially. + If ``None``, default transpiler will be used. + Defaults to ``None``. + connectivity (:class:`networkx.Graph`, optional): physical qubits connectivity. + If ``None``, :class:`` is used. + Defaults to ``None``. + native_gates (:class:`qibo.transpiler.unroller.NativeGates`, optional): native gates. + Defaults to :math:`qibo.transpiler.unroller.NativeGates.default`. + on_qubits (list, optional): list of physical qubits to be used. + If "None" all qubits are used. Defaults to ``None``. """ def __init__( self, - passes: list, - connectivity: nx.Graph, + passes: list = None, + connectivity: nx.Graph = None, native_gates: NativeGates = NativeGates.default(), on_qubits: list = None, ): @@ -183,10 +190,7 @@ def __init__( connectivity = restrict_connectivity_qubits(connectivity, on_qubits) self.connectivity = connectivity self.native_gates = native_gates - if passes is None: - self.passes = self.default() - else: - self.passes = passes + self.passes = self.default() if passes is None else passes def default(self): """Return the default transpiler pipeline for the required hardware connectivity.""" @@ -204,6 +208,7 @@ def default(self): default_passes.append(StarConnectivity()) # default unroller pass default_passes.append(Unroller(native_gates=self.native_gates)) + return default_passes def __call__(self, circuit): @@ -239,14 +244,17 @@ def __call__(self, circuit): TranspilerPipelineError, f"Unrecognised transpiler pass: {transpiler_pass}", ) + return circuit, final_layout - def is_satisfied(self, circuit): - """Return True if the circuit respects the hardware connectivity and native gates, False otherwise. + def is_satisfied(self, circuit: Circuit): + """Returns ``True`` if the circuit respects the hardware connectivity and native gates, ``False`` otherwise. Args: - circuit (qibo.models.Circuit): circuit to be checked. - native_gates (NativeGates): two qubit native gates. + circuit (:class:`qibo.models.circuit.Circuit`): circuit to be checked. + + Returns: + (bool): satisfiability condition. """ try: assert_connectivity(circuit=circuit, connectivity=self.connectivity) From 438f95de87950f92cae25cbfcfcfe3d3ac2ece2c Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 09:58:18 +0400 Subject: [PATCH 10/19] improved docstring `unroller` --- src/qibo/transpiler/unroller.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/qibo/transpiler/unroller.py b/src/qibo/transpiler/unroller.py index 1358056600..076f358b5a 100644 --- a/src/qibo/transpiler/unroller.py +++ b/src/qibo/transpiler/unroller.py @@ -8,11 +8,20 @@ class NativeGates(Flag): - """Define native gates supported by the unroller. - A native gate set should contain at least one two-qubit gate (CZ or iSWAP) - and at least one single qubit gate (GPI2 or U3). - Possible values are: - I, Z, RZ, M, GPI2, U3, CZ, iSWAP. + """Define native gates supported by the unroller. A native gate set should contain at least + one two-qubit gate (:class:`qibo.gates.gates.CZ` or :class:`qibo.gates.gates.iSWAP`), + and at least one single-qubit gate + (:class:`qibo.gates.gates.GPI2` or :class:`qibo.gates.gates.U3`). + + Possible gates are: + - :class:`qibo.gates.gates.I` + - :class:`qibo.gates.gates.Z` + - :class:`qibo.gates.gates.RZ` + - :class:`qibo.gates.gates.M` + - :class:`qibo.gates.gates.GPI2` + - :class:`qibo.gates.gates.U3` + - :class:`qibo.gates.gates.CZ` + - :class:`qibo.gates.gates.iSWAP` """ I = auto() @@ -31,7 +40,7 @@ def default(cls): @classmethod def from_gatelist(cls, gatelist: list): - """Create a NativeGates object containing all gates from a gatelist.""" + """Create a NativeGates object containing all gates from a ``gatelist``.""" natives = cls(0) for gate in gatelist: natives |= cls.from_gate(gate) @@ -39,9 +48,8 @@ def from_gatelist(cls, gatelist: list): @classmethod def from_gate(cls, gate: gates.Gate): - """Create a NativeGates object from a gate. - The gate can be either a class:`qibo.gates.Gate` or an instance of this class. - """ + """Create a :class:`qibo.transpiler.unroller.NativeGates` + object from a :class:`qibo.gates.gates.Gate`.""" if isinstance(gate, gates.Gate): return cls.from_gate(gate.__class__) try: @@ -55,7 +63,8 @@ class Unroller: """Translates a circuit to native gates. Args: - native_gates (:class:`qibo.transpiler.unroller.NativeGates`): native gates to use in the transpiled circuit. + native_gates (:class:`qibo.transpiler.unroller.NativeGates`): native gates to use + in the transpiled circuit. Returns: (:class:`qibo.models.circuit.Circuit`): equivalent circuit with native gates. From 1e5ae8a96de4cab8ae180171b7100562dc1e5fa3 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 10:00:36 +0400 Subject: [PATCH 11/19] hiding `exceptions` submodule --- src/qibo/transpiler/{exceptions.py => _exceptions.py} | 0 src/qibo/transpiler/blocks.py | 2 +- src/qibo/transpiler/pipeline.py | 2 +- src/qibo/transpiler/placer.py | 2 +- src/qibo/transpiler/router.py | 5 ++--- src/qibo/transpiler/unroller.py | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) rename src/qibo/transpiler/{exceptions.py => _exceptions.py} (100%) diff --git a/src/qibo/transpiler/exceptions.py b/src/qibo/transpiler/_exceptions.py similarity index 100% rename from src/qibo/transpiler/exceptions.py rename to src/qibo/transpiler/_exceptions.py diff --git a/src/qibo/transpiler/blocks.py b/src/qibo/transpiler/blocks.py index 764213e62f..199979767e 100644 --- a/src/qibo/transpiler/blocks.py +++ b/src/qibo/transpiler/blocks.py @@ -3,7 +3,7 @@ from qibo import Circuit, gates from qibo.config import raise_error from qibo.gates import Gate -from qibo.transpiler.exceptions import BlockingError +from qibo.transpiler._exceptions import BlockingError class Block: diff --git a/src/qibo/transpiler/pipeline.py b/src/qibo/transpiler/pipeline.py index a88c90de10..d1bd6fdc6c 100644 --- a/src/qibo/transpiler/pipeline.py +++ b/src/qibo/transpiler/pipeline.py @@ -7,8 +7,8 @@ from qibo.config import raise_error from qibo.models import Circuit from qibo.quantum_info.random_ensembles import random_statevector +from qibo.transpiler._exceptions import TranspilerPipelineError from qibo.transpiler.abstract import Optimizer, Placer, Router -from qibo.transpiler.exceptions import TranspilerPipelineError from qibo.transpiler.optimizer import Preprocessing from qibo.transpiler.placer import Trivial, assert_placement from qibo.transpiler.router import ConnectivityError, assert_connectivity diff --git a/src/qibo/transpiler/placer.py b/src/qibo/transpiler/placer.py index d47bc30da0..8cbd09117d 100644 --- a/src/qibo/transpiler/placer.py +++ b/src/qibo/transpiler/placer.py @@ -6,8 +6,8 @@ from qibo import gates from qibo.config import raise_error from qibo.models import Circuit +from qibo.transpiler._exceptions import PlacementError from qibo.transpiler.abstract import Placer, Router -from qibo.transpiler.exceptions import PlacementError def assert_placement( diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index dd9f583130..3befa6edbe 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -2,14 +2,13 @@ from copy import deepcopy import networkx as nx -import numpy as np from qibo import gates -from qibo.config import log, raise_error +from qibo.config import raise_error from qibo.models import Circuit +from qibo.transpiler._exceptions import ConnectivityError from qibo.transpiler.abstract import Router from qibo.transpiler.blocks import Block, CircuitBlocks -from qibo.transpiler.exceptions import ConnectivityError def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): diff --git a/src/qibo/transpiler/unroller.py b/src/qibo/transpiler/unroller.py index 076f358b5a..c62ae4e273 100644 --- a/src/qibo/transpiler/unroller.py +++ b/src/qibo/transpiler/unroller.py @@ -3,8 +3,8 @@ from qibo import gates from qibo.config import raise_error from qibo.models import Circuit +from qibo.transpiler._exceptions import DecompositionError from qibo.transpiler.decompositions import cz_dec, gpi2_dec, iswap_dec, opt_dec, u3_dec -from qibo.transpiler.exceptions import DecompositionError class NativeGates(Flag): From dc9feeecde5b01d8848db3c94a2267c464be29cb Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 10:12:38 +0400 Subject: [PATCH 12/19] rewrite imports for exceptions in tests --- tests/test_transpiler_blocks.py | 2 +- tests/test_transpiler_pipeline.py | 3 +-- tests/test_transpiler_placer.py | 2 +- tests/test_transpiler_router.py | 9 ++------- tests/test_transpiler_star_connectivity.py | 2 +- tests/test_transpiler_unroller.py | 2 +- 6 files changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/test_transpiler_blocks.py b/tests/test_transpiler_blocks.py index 48207890bd..af652f72f5 100644 --- a/tests/test_transpiler_blocks.py +++ b/tests/test_transpiler_blocks.py @@ -1,9 +1,9 @@ import pytest from qibo import Circuit, gates +from qibo.transpiler._exceptions import BlockingError from qibo.transpiler.blocks import ( Block, - BlockingError, CircuitBlocks, _check_multi_qubit_measurements, _count_multi_qubit_gates, diff --git a/tests/test_transpiler_pipeline.py b/tests/test_transpiler_pipeline.py index feeaba4e84..86e84b3b01 100644 --- a/tests/test_transpiler_pipeline.py +++ b/tests/test_transpiler_pipeline.py @@ -4,11 +4,10 @@ from qibo import gates from qibo.models import Circuit +from qibo.transpiler._exceptions import ConnectivityError, TranspilerPipelineError from qibo.transpiler.optimizer import Preprocessing from qibo.transpiler.pipeline import ( - ConnectivityError, Passes, - TranspilerPipelineError, assert_circuit_equivalence, assert_transpiling, restrict_connectivity_qubits, diff --git a/tests/test_transpiler_placer.py b/tests/test_transpiler_placer.py index c76e341981..09d4a80d2a 100644 --- a/tests/test_transpiler_placer.py +++ b/tests/test_transpiler_placer.py @@ -3,10 +3,10 @@ from qibo import gates from qibo.models import Circuit +from qibo.transpiler._exceptions import PlacementError from qibo.transpiler.pipeline import restrict_connectivity_qubits from qibo.transpiler.placer import ( Custom, - PlacementError, Random, ReverseTraversal, Subgraph, diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index bee84e47b6..0a2d8422f3 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -4,19 +4,14 @@ from qibo import gates from qibo.models import Circuit +from qibo.transpiler._exceptions import ConnectivityError from qibo.transpiler.optimizer import Preprocessing from qibo.transpiler.pipeline import ( assert_circuit_equivalence, restrict_connectivity_qubits, ) from qibo.transpiler.placer import Custom, Random, Subgraph, Trivial, assert_placement -from qibo.transpiler.router import ( - CircuitMap, - ConnectivityError, - Sabre, - ShortestPaths, - assert_connectivity, -) +from qibo.transpiler.router import CircuitMap, Sabre, ShortestPaths, assert_connectivity def star_connectivity(): diff --git a/tests/test_transpiler_star_connectivity.py b/tests/test_transpiler_star_connectivity.py index 5a8e9a4b57..2330afd76c 100644 --- a/tests/test_transpiler_star_connectivity.py +++ b/tests/test_transpiler_star_connectivity.py @@ -7,8 +7,8 @@ from qibo.backends import NumpyBackend from qibo.models import Circuit from qibo.quantum_info.random_ensembles import random_unitary +from qibo.transpiler._exceptions import ConnectivityError from qibo.transpiler.pipeline import _transpose_qubits -from qibo.transpiler.router import ConnectivityError from qibo.transpiler.star_connectivity import StarConnectivity diff --git a/tests/test_transpiler_unroller.py b/tests/test_transpiler_unroller.py index 26529c8f2c..89c44ca9c7 100644 --- a/tests/test_transpiler_unroller.py +++ b/tests/test_transpiler_unroller.py @@ -2,8 +2,8 @@ from qibo import gates from qibo.models import Circuit +from qibo.transpiler._exceptions import DecompositionError from qibo.transpiler.unroller import ( - DecompositionError, NativeGates, Unroller, assert_decomposition, From 7316cb76424fe0e724716cd6bd10bc6d10a21fc3 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 10:28:20 +0400 Subject: [PATCH 13/19] improved docstring `placer` --- src/qibo/transpiler/placer.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/qibo/transpiler/placer.py b/src/qibo/transpiler/placer.py index 8cbd09117d..2dd2ee19f4 100644 --- a/src/qibo/transpiler/placer.py +++ b/src/qibo/transpiler/placer.py @@ -18,8 +18,9 @@ def assert_placement( Args: circuit (:class:`qibo.models.circuit.Circuit`): Circuit model to check. layout (dict): physical to logical qubit mapping. - connectivity (:class:`networkx.Graph`, optional): chip connectivity. This argument is necessary if the - layout applied to a subset of qubits in the original connectivity graph. + connectivity (:class:`networkx.Graph`, optional): chip connectivity. + This argument is necessary if the layout applied to a subset of + qubits in the original connectivity graph. Defaults to ``None``. """ assert_mapping_consistency(layout=layout, connectivity=connectivity) if circuit.nqubits > len(layout): @@ -39,8 +40,9 @@ def assert_mapping_consistency(layout: dict, connectivity: nx.Graph = None): Args: layout (dict): physical to logical qubit mapping. - connectivity (:class:`networkx.Graph`, optional): chip connectivity. This argument is necessary if the - layout applied to a subset of qubits in the original connectivity graph. + connectivity (:class:`networkx.Graph`, optional): chip connectivity. + This argument is necessary if the layout applied to a subset of + qubits in the original connectivity graph. Defaults to ``None``. """ values = sorted(layout.values()) if connectivity is not None: @@ -61,13 +63,13 @@ def assert_mapping_consistency(layout: dict, connectivity: nx.Graph = None): def _find_gates_qubits_pairs(circuit: Circuit): """Helper method for :meth:`qibo.transpiler.placer`. - Translate qibo circuit into a list of pairs of qubits to be used by the router and placer. + Translate circuit into a list of pairs of qubits to be used by the router and placer. Args: circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. Returns: - gates_qubits_pairs (list): list containing pairs of qubits targeted by two qubits gates. + (list): Pairs of qubits targeted by two qubits gates. """ gates_qubits_pairs = [] for gate in circuit.queue: @@ -136,9 +138,9 @@ class Custom(Placer): :math:`{\\textup{"q0"}: 1, \\textup{"q1"}: 2, \\textup{"q2"}:0}` to assign the physical qubits :math:`\\{0, 1, 2\\}` to the logical qubits :math:`[1, 2, 0]`. - connectivity (networkx.Graph, optional): chip connectivity. + connectivity (:class:`networkx.Graph`, optional): chip connectivity. This argument is necessary if the layout applied to a subset of - qubits of the original connectivity graph. + qubits of the original connectivity graph. Defaults to ``None``. """ def __init__(self, map: Union[list, dict], connectivity: nx.Graph = None): @@ -228,6 +230,7 @@ def __call__(self, circuit: Circuit): break sorted_result = dict(sorted(result.mapping.items())) + return dict( zip(["q" + str(i) for i in sorted_result.keys()], sorted_result.values()) ) @@ -261,6 +264,7 @@ def __call__(self, circuit): nodes = self.connectivity.number_of_nodes() keys = list(self.connectivity.nodes()) dict_keys = ["q" + str(i) for i in keys] + final_mapping = dict(zip(keys, range(nodes))) final_graph = nx.relabel_nodes(self.connectivity, final_mapping) final_cost = self._cost(final_graph, gates_qubits_pairs) @@ -274,6 +278,7 @@ def __call__(self, circuit): final_graph = graph final_mapping = mapping final_cost = cost + return dict(zip(dict_keys, list(final_mapping.values()))) def _cost(self, graph: nx.Graph, gates_qubits_pairs: list): @@ -290,6 +295,7 @@ def _cost(self, graph: nx.Graph, gates_qubits_pairs: list): for allowed, gate in enumerate(gates_qubits_pairs): if gate not in graph.edges(): return len(gates_qubits_pairs) - allowed - 1 + return 0 @@ -334,11 +340,11 @@ def __call__(self, circuit: Circuit): Returns: (dict): physical to logical qubit mapping. """ - initial_placer = Trivial(self.connectivity) initial_placement = initial_placer(circuit=circuit) new_circuit = self._assemble_circuit(circuit) final_placement = self._routing_step(initial_placement, new_circuit) + return final_placement def _assemble_circuit(self, circuit: Circuit): @@ -385,6 +391,6 @@ def _routing_step(self, layout: dict, circuit: Circuit): layout (dict): intial qubit layout. circuit (:class:`qibo.models.circuit.Circuit`): circuit to be routed. """ - _, final_mapping = self.routing_algorithm(circuit, layout) + return final_mapping From 209aac6b8ffdc2af7e2815a1ca2e5c5a3caafb63 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 10:58:33 +0400 Subject: [PATCH 14/19] improved docstrings `router` --- src/qibo/transpiler/router.py | 135 ++++++++++++++++++++++++++-------- 1 file changed, 105 insertions(+), 30 deletions(-) diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 3befa6edbe..54ff6f3899 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -1,5 +1,6 @@ import random from copy import deepcopy +from typing import Optional, Union import networkx as nx @@ -42,16 +43,24 @@ def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): class CircuitMap: - """Class to keep track of the circuit and physical-logical mapping during routing, - this class also implements the initial two qubit blocks decomposition. + """Class that stores the circuit and physical-logical mapping during routing. + + Also implements the initial two-qubit block decompositions. Args: initial_layout (dict): initial logical-to-physical qubit mapping. - circuit (Circuit): circuit to be routed. - blocks (CircuitBlocks): circuit blocks representation, if None the blocks will be computed from the circuit. + circuit (:class:`qibo.models.circuit.Circuit`): circuit to be routed. + blocks (:class:`qibo.transpiler.blocks.CircuitBlocks`, optional): circuit block representation. + If ``None``, the blocks will be computed from the circuit. + Defaults to ``None``. """ - def __init__(self, initial_layout: dict, circuit: Circuit, blocks=None): + def __init__( + self, + initial_layout: dict, + circuit: Circuit, + blocks: Optional[CircuitBlocks] = None, + ): if blocks is not None: self.circuit_blocks = blocks else: @@ -64,7 +73,13 @@ def __init__(self, initial_layout: dict, circuit: Circuit, blocks=None): self._swaps = 0 def set_circuit_logical(self, circuit_logical_map: list): - """Set the current circuit to logical qubit mapping.""" + """Sets the current circuit to logical qubit mapping. + + Method works in-place. + + Args: + circuit_logical_map (list): logical mapping. + """ self._circuit_logical = circuit_logical_map def blocks_qubits_pairs(self): @@ -74,17 +89,25 @@ def blocks_qubits_pairs(self): def execute_block(self, block: Block): """Executes a block by removing it from the circuit representation and adding it to the routed circuit. + + Method works in-place. + + Args: + block (:class:`qibo.transpiler.blocks.Block`): block to be removed. """ self._routed_blocks.add_block( block.on_qubits(self.get_physical_qubits(block, index=True)) ) self.circuit_blocks.remove_block(block) - def routed_circuit(self, circuit_kwargs=None): - """Return the routed circuit. + def routed_circuit(self, circuit_kwargs: Optional[dict] = None): + """Returns the routed circuit. Args: - circuit_kwargs (dict): original circuit init_kwargs. + circuit_kwargs (dict): original circuit ``init_kwargs``. + + Returns: + :class:`qibo.models.circuit.Circuit`: Routed circuit. """ return self._routed_blocks.circuit(circuit_kwargs=circuit_kwargs) @@ -94,12 +117,17 @@ def final_layout(self): "q" + str(self.circuit_to_physical(i)): i for i in range(len(self._circuit_logical)) } + return dict(sorted(unsorted_dict.items())) def update(self, swap: tuple): - """Updates the logical-physical qubit mapping after applying a SWAP - and add the SWAP gate to the routed blocks, the swap is represented by a tuple containing - the logical qubits to be swapped. + """Updates the logical-physical qubit mapping after applying a ``SWAP`` + + Adds the :class:`qibo.gates.gates.SWAP` gate to the routed blocks. + Method works in-place. + + Args: + swap (tuple): tuple containing the logical qubits to be swapped. """ physical_swap = self.logical_to_physical(swap, index=True) self._routed_blocks.add_block( @@ -112,20 +140,43 @@ def update(self, swap: tuple): self._circuit_logical[idx_0], self._circuit_logical[idx_1] = swap[1], swap[0] def get_logical_qubits(self, block: Block): - """Returns the current logical qubits where a block is acting""" + """Returns the current logical qubits where a block is acting on. + + Args: + block (:class:`qibo.transpiler.blocks.Block`): block to be analysed. + + Returns: + (tuple): logical qubits where a block is acting on. + """ return self.circuit_to_logical(block.qubits) - def get_physical_qubits(self, block: Block or int, index=False): - """Returns the physical qubits where a block is acting. - If index is True the qubits are returned as indices of the connectivity nodes. + def get_physical_qubits(self, block: Union[int, Block], index: bool = False): + """Returns the physical qubits where a block is acting on. + + Args: + block (int or :class:`qibo.transpiler.blocks.Block`): block to be analysed. + index (bool, optional): If ``True``, qubits are returned as indices of + the connectivity nodes. Defaults to ``False``. + + Returns: + (tuple): physical qubits where a block is acting on. + """ if isinstance(block, int): block = self.circuit_blocks.search_by_index(block) + return self.logical_to_physical(self.get_logical_qubits(block), index=index) - def logical_to_physical(self, logical_qubits: tuple, index=False): + def logical_to_physical(self, logical_qubits: tuple, index: bool = False): """Returns the physical qubits associated to the logical qubits. - If index is True the qubits are returned as indices of the connectivity nodes. + + Args: + logical_qubits (tuple): physical qubits. + index (bool, optional): If ``True``, qubits are returned as indices of + `the connectivity nodes. Defaults to ``False``. + + Returns: + (tuple): physical qubits associated to the logical qubits. """ if not index: return tuple( @@ -134,21 +185,44 @@ def logical_to_physical(self, logical_qubits: tuple, index=False): ] for i in range(2) ) + return tuple(self._physical_logical.index(logical_qubits[i]) for i in range(2)) def circuit_to_logical(self, circuit_qubits: tuple): - """Returns the current logical qubits associated to the initial circuit qubits.""" + """Returns the current logical qubits associated to the initial circuit qubits. + + Args: + circuit_qubits (tuple): circuit qubits. + + Returns: + (tuple): logical qubits. + """ return tuple(self._circuit_logical[circuit_qubits[i]] for i in range(2)) def circuit_to_physical(self, circuit_qubit: int): - """Returns the current physical qubit associated to an initial circuit qubit.""" + """Returns the current physical qubit associated to an initial circuit qubit. + + Args: + circuit_qubit (int): circuit qubit. + + Returns: + (int): physical qubit. + """ return self._graph_qubits_names[ self._physical_logical.index(self._circuit_logical[circuit_qubit]) ] def physical_to_logical(self, physical_qubit: int): - """Returns the current logical qubit associated to a physical qubit (connectivity graph node).""" + """Returns the current logical qubit associated to a physical qubit (connectivity graph node). + + Args: + physical_qubit (int): physical qubit. + + Returns: + (int): logical qubit. + """ physical_qubit_index = self._graph_qubits_names.index(physical_qubit) + return self._physical_logical[physical_qubit_index] @@ -157,17 +231,18 @@ class ShortestPaths(Router): Args: connectivity (:class:`networkx.Graph`): chip connectivity. - sampling_split (float, optional): fraction of paths tested - (between :math:`0` and :math:`1`). Defaults to :math:`1.0`. - seed (int): seed for the random number generator. + seed (int, optional): seed for the random number generator. + If ``None``, defaults to :math:`42`. Defaults to ``None``. """ - def __init__(self, connectivity: nx.Graph, seed=42): + def __init__(self, connectivity: nx.Graph, seed: Optional[int] = None): self.connectivity = connectivity self._front_layer = None self.circuit = None self._dag = None self._final_measurements = None + if seed is None: + seed = 42 random.seed(seed) @property @@ -387,18 +462,18 @@ def __init__( lookahead: int = 2, decay_lookahead: float = 0.6, delta: float = 0.001, - seed=None, + seed: Optional[int] = None, ): """Routing algorithm proposed in Ref [1]. Args: connectivity (dict): hardware chip connectivity. - lookahead (int): lookahead factor, how many dag layers will be considered in computing the cost. - decay_lookahead (float): value in interval [0,1]. + lookahead (int, optional): lookahead factor, how many dag layers will be considered in computing the cost. + decay_lookahead (float, optional): value in interval [0,1]. How the weight of the distance in the dag layers decays in computing the cost. - delta (float): this parameter defines the number of swaps vs depth trade-off by deciding + delta (float, optional): this parameter defines the number of swaps vs depth trade-off by deciding how the algorithm tends to select non-overlapping SWAPs. - seed (int): seed for the candidate random choice as tiebraker. + seed (int, optional): seed for the candidate random choice as tiebraker. References: 1. G. Li, Y. Ding, and Y. Xie, *Tackling the Qubit Mapping Problem for NISQ-Era Quantum Devices*. From 8d2dd1faee9dcb7cd5d02b2c5cf34027ca18aaa5 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 11:01:07 +0400 Subject: [PATCH 15/19] fix test --- tests/test_transpiler_pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_transpiler_pipeline.py b/tests/test_transpiler_pipeline.py index 86e84b3b01..cc01a09b61 100644 --- a/tests/test_transpiler_pipeline.py +++ b/tests/test_transpiler_pipeline.py @@ -78,7 +78,7 @@ def test_restrict_qubits_error_no_subset(): def test_restrict_qubits_error_not_connected(): with pytest.raises(ConnectivityError) as excinfo: restrict_connectivity_qubits(star_connectivity(), [1, 3]) - assert "The new connectivity graph is not connected." in str(excinfo.value) + assert "New connectivity graph is not connected." in str(excinfo.value) def test_restrict_qubits(): From 38aae682d7330e7262fb15b98aa9721f25d086c2 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 11:33:23 +0400 Subject: [PATCH 16/19] improved docstrings `router` --- src/qibo/transpiler/router.py | 174 +++++++++++++++++++++++----------- 1 file changed, 120 insertions(+), 54 deletions(-) diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 54ff6f3899..222c4b7198 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -247,7 +247,7 @@ def __init__(self, connectivity: nx.Graph, seed: Optional[int] = None): @property def added_swaps(self): - """Number of SWAP gates added to the circuit during routing.""" + """Returns the number of SWAP gates added to the circuit during routing.""" return self.circuit._swaps def __call__(self, circuit: Circuit, initial_layout: dict): @@ -277,7 +277,10 @@ def __call__(self, circuit: Circuit, initial_layout: dict): return routed_circuit, self.circuit.final_layout() def _find_new_mapping(self): - """Find new qubit mapping. The mapping is found by looking for the shortest path.""" + """Find new qubit mapping. Mapping is found by looking for the shortest path. + + Method works in-place. + """ candidates_evaluation = [] for candidate in self._candidates(): cost = self._compute_cost(candidate) @@ -292,8 +295,8 @@ def _find_new_mapping(self): self._add_swaps(best_candidate, self.circuit) def _candidates(self): - """Return all possible shortest paths, - a list contains the new mapping and a second list contains the path meeting point. + """Returns all possible shortest paths in a ``list`` that contains + the new mapping and a second ``list`` containing the path meeting point. """ target_qubits = self.circuit.get_physical_qubits(self._front_layer[0]) path_list = list( @@ -305,12 +308,15 @@ def _candidates(self): for path in path_list: for meeting_point in range(len(path) - 1): all_candidates.append((path, meeting_point)) + return all_candidates @staticmethod def _add_swaps(candidate: tuple, circuitmap: CircuitMap): """Adds swaps to the circuit to move qubits. + Method works in-place. + Args: candidate (tuple): contains path to move qubits and qubit meeting point in the path. circuitmap (CircuitMap): representation of the circuit. @@ -336,10 +342,14 @@ def _add_swaps(candidate: tuple, circuitmap: CircuitMap): ) ) - def _compute_cost(self, candidate): - """Greedy algorithm to decide which path to take, and how qubits should walk. + def _compute_cost(self, candidate: tuple): + """Greedy algorithm that decides which path to take and how qubits should be walked. + The cost is computed as minus the number of successive gates that can be executed. + Args: + candidate (tuple): contains path to move qubits and qubit meeting point in the path. + Returns: (list, int): best path to move qubits and qubit meeting point in the path. """ @@ -377,10 +387,11 @@ def _compute_cost(self, candidate): all_executed = False if not all_executed: break + return -successive_executed_gates def _check_execution(self): - """Check if some blocks in the front layer can be executed in the current configuration. + """Checks if some blocks in the front layer can be executed in the current configuration. Returns: (list): executable blocks if there are, ``None`` otherwise. @@ -394,13 +405,19 @@ def _check_execution(self): executable_blocks.append(block) if len(executable_blocks) == 0: return None + return executable_blocks def _execute_blocks(self, blocklist: list): - """Execute a list of blocks: - -Remove the correspondent nodes from the dag and circuit representation. - -Add the executed blocks to the routed circuit. - -Update the dag layers and front layer. + """Executes a list of blocks: + -Remove the correspondent nodes from the dag and circuit representation. + -Add the executed blocks to the routed circuit. + -Update the dag layers and front layer. + + Method works in-place. + + Args: + blocklist (list): list of blocks. """ for block_id in blocklist: block = self.circuit.circuit_blocks.search_by_index(block_id) @@ -409,7 +426,10 @@ def _execute_blocks(self, blocklist: list): self._update_front_layer() def _update_front_layer(self): - """Update the front layer of the dag.""" + """Updates the front layer of the dag. + + Method works in-place. + """ for layer, nodes in enumerate(nx.topological_generations(self._dag)): for node in nodes: self._dag.nodes[node]["layer"] = layer @@ -419,18 +439,29 @@ def _update_front_layer(self): def _preprocessing(self, circuit: Circuit, initial_layout: dict): """The following objects will be initialised: - - circuit: class to represent circuit and to perform logical-physical qubit mapping. - - _final_measurements: measurement gates at the end of the circuit. - - _front_layer: list containing the blocks to be executed. + - circuit: class to represent circuit and to perform logical-physical qubit mapping. + - _final_measurements: measurement gates at the end of the circuit. + - _front_layer: list containing the blocks to be executed. + + Args: + circuit (:class:`qibo.models.circuit.Circuit`): circuit to be preprocessed. + initial_layout (dict): initial physical-to-logical qubit mapping. """ - copied_circuit = _copy_circuit(circuit) + copied_circuit = circuit.copy(deep=True) self._final_measurements = self._detach_final_measurements(copied_circuit) self.circuit = CircuitMap(initial_layout, copied_circuit) self._dag = _create_dag(self.circuit.blocks_qubits_pairs()) self._update_front_layer() def _detach_final_measurements(self, circuit: Circuit): - """Detach measurement gates at the end of the circuit for separate handling.""" + """Detaches measurement gates at the end of the circuit for separate handling. + + Args: + circuit (:class:`qibo.models.circuit.Circuit`): circuits to be processed. + + Returns: + (NoneType or list): list of measurements. If no measurements, returns ``None``. + """ final_measurements = [] for gate in circuit.queue[::-1]: if isinstance(gate, gates.M): @@ -440,10 +471,11 @@ def _detach_final_measurements(self, circuit: Circuit): break if not final_measurements: return None + return final_measurements[::-1] def _append_final_measurements(self, routed_circuit: Circuit): - """Append the final measurment gates on the correct qubits conserving the measurement register.""" + """Appends the final measurment gates on the correct qubits conserving the measurement register.""" for measurement in self._final_measurements: original_qubits = measurement.qubits routed_qubits = ( @@ -452,6 +484,7 @@ def _append_final_measurements(self, routed_circuit: Circuit): routed_circuit.add( measurement.on_qubits(dict(zip(original_qubits, routed_qubits))) ) + return routed_circuit @@ -467,13 +500,17 @@ def __init__( """Routing algorithm proposed in Ref [1]. Args: - connectivity (dict): hardware chip connectivity. - lookahead (int, optional): lookahead factor, how many dag layers will be considered in computing the cost. - decay_lookahead (float, optional): value in interval [0,1]. + connectivity (:class:`networkx.Graph`): hardware chip connectivity. + lookahead (int, optional): lookahead factor, how many dag layers will be considered + in computing the cost. Defaults to :math:`2`. + decay_lookahead (float, optional): value in interval :math:`[0, 1]`. How the weight of the distance in the dag layers decays in computing the cost. - delta (float, optional): this parameter defines the number of swaps vs depth trade-off by deciding + Defaults to :math:`0.6`. + delta (float, optional): defines the number of SWAPs vs depth trade-off by deciding how the algorithm tends to select non-overlapping SWAPs. + Defaults to math:`10^{-3}`. seed (int, optional): seed for the candidate random choice as tiebraker. + Defaults to ``None``. References: 1. G. Li, Y. Ding, and Y. Xie, *Tackling the Qubit Mapping Problem for NISQ-Era Quantum Devices*. @@ -520,21 +557,25 @@ def __call__(self, circuit: Circuit, initial_layout: dict): @property def added_swaps(self): - """Number of SWAP gates added to the circuit during routing.""" + """Returns the number of SWAP gates added to the circuit during routing.""" return self.circuit._swaps def _preprocessing(self, circuit: Circuit, initial_layout: dict): """The following objects will be initialised: - - circuit: class to represent circuit and to perform logical-physical qubit mapping. - - _final_measurements: measurement gates at the end of the circuit. - - _dist_matrix: matrix reporting the shortest path lengh between all node pairs. - - _dag: direct acyclic graph of the circuit based on commutativity. - - _memory_map: list to remember previous SWAP moves. - - _front_layer: list containing the blocks to be executed. - - _delta_register: list containing the special weigh added to qubits - to prevent overlapping swaps. + - circuit: class to represent circuit and to perform logical-physical qubit mapping. + - _final_measurements: measurement gates at the end of the circuit. + - _dist_matrix: matrix reporting the shortest path lengh between all node pairs. + - _dag: direct acyclic graph of the circuit based on commutativity. + - _memory_map: list to remember previous SWAP moves. + - _front_layer: list containing the blocks to be executed. + - _delta_register: list containing the special weigh added to qubits + to prevent overlapping swaps. + + Args: + circuit (:class:`qibo.models.circuit.Circuit`): circuit to be preprocessed. + initial_layout (dict): initial physical-to-logical qubit mapping. """ - copied_circuit = _copy_circuit(circuit) + copied_circuit = circuit.copy(deep=True) self._final_measurements = self._detach_final_measurements(copied_circuit) self.circuit = CircuitMap(initial_layout, copied_circuit) self._dist_matrix = nx.floyd_warshall_numpy(self.connectivity) @@ -558,7 +599,14 @@ def _detach_final_measurements(self, circuit: Circuit): return final_measurements[::-1] def _append_final_measurements(self, routed_circuit: Circuit): - """Append the final measurment gates on the correct qubits conserving the measurement register.""" + """Appends final measurment gates on the correct qubits conserving the measurement register. + + Args: + routed_circuit (:class:`qibo.models.circuit.Circuit`): original circuit. + + Returns: + (:class:`qibo.models.circuit.Circuit`) routed circuit. + """ for measurement in self._final_measurements: original_qubits = measurement.qubits routed_qubits = ( @@ -567,16 +615,23 @@ def _append_final_measurements(self, routed_circuit: Circuit): routed_circuit.add( measurement.on_qubits(dict(zip(original_qubits, routed_qubits))) ) + return routed_circuit def _update_dag_layers(self): - """Update dag layers and put them in topological order.""" + """Update dag layers and put them in topological order. + + Method works in-place. + """ for layer, nodes in enumerate(nx.topological_generations(self._dag)): for node in nodes: self._dag.nodes[node]["layer"] = layer def _update_front_layer(self): - """Update the front layer of the dag.""" + """Update the front layer of the dag. + + Method works in-place. + """ self._front_layer = self._get_dag_layer(0) def _get_dag_layer(self, n_layer): @@ -589,16 +644,18 @@ def _find_new_mapping(self): self._memory_map.append(deepcopy(self.circuit._circuit_logical)) for candidate in self._swap_candidates(): candidates_evaluation[candidate] = self._compute_cost(candidate) + best_cost = min(candidates_evaluation.values()) best_candidates = [ key for key, value in candidates_evaluation.items() if value == best_cost ] best_candidate = random.choice(best_candidates) + for qubit in self.circuit.logical_to_physical(best_candidate, index=True): self._delta_register[qubit] += self.delta self.circuit.update(best_candidate) - def _compute_cost(self, candidate): + def _compute_cost(self, candidate: int): """Compute the cost associated to a possible SWAP candidate.""" temporary_circuit = CircuitMap( initial_layout=self.circuit.initial_layout, @@ -607,8 +664,10 @@ def _compute_cost(self, candidate): ) temporary_circuit.set_circuit_logical(deepcopy(self.circuit._circuit_logical)) temporary_circuit.update(candidate) + if temporary_circuit._circuit_logical in self._memory_map: return float("inf") + tot_distance = 0.0 weight = 1.0 for layer in range(self.lookahead + 1): @@ -623,11 +682,17 @@ def _compute_cost(self, candidate): ) tot_distance += weight * avg_layer_distance weight *= self.decay + return tot_distance def _swap_candidates(self): - """Return a list of possible candidate SWAPs (to be applied on logical qubits directly). - The possible candidates are the ones sharing at least one qubit with a block in the front layer. + """Returns a list of possible candidate SWAPs to be applied on logical qubits directly. + + The possible candidates are the ones sharing at least one qubit + with a block in the front layer. + + Returns: + (list): list of candidates. """ candidates = [] for block in self._front_layer: @@ -643,6 +708,7 @@ def _swap_candidates(self): ) if candidate not in candidates: candidates.append(candidate) + return candidates def _check_execution(self): @@ -658,16 +724,23 @@ def _check_execution(self): or not self.circuit.circuit_blocks.search_by_index(block).entangled ): executable_blocks.append(block) + if len(executable_blocks) == 0: return None + return executable_blocks def _execute_blocks(self, blocklist: list): - """Execute a list of blocks: + """Executes a list of blocks: -Remove the correspondent nodes from the dag and circuit representation. -Add the executed blocks to the routed circuit. -Update the dag layers and front layer. -Reset the mapping memory. + + Method works in-place. + + Args: + blocklist (list): list of blocks. """ for block_id in blocklist: block = self.circuit.circuit_blocks.search_by_index(block_id) @@ -679,21 +752,11 @@ def _execute_blocks(self, blocklist: list): self._delta_register = [1.0 for _ in self._delta_register] -def _copy_circuit(circuit: Circuit): - """Helper method for :meth:`qibo.transpiler.router`. - - Return a copy of the circuit to avoid altering the original circuit. - This copy conserves the registers of the measurement gates. - """ - new_circuit = Circuit(circuit.nqubits) - for gate in circuit.queue: - new_circuit.add(gate) - return new_circuit - - -def _create_dag(gates_qubits_pairs): +def _create_dag(gates_qubits_pairs: list): """Helper method for :meth:`qibo.transpiler.router.Sabre`. - Create direct acyclic graph (dag) of the circuit based on two qubit gates commutativity relations. + + Create direct acyclic graph (dag) of the circuit based on two qubit gates + commutativity relations. Args: gates_qubits_pairs (list): list of qubits tuples where gates/blocks acts. @@ -715,11 +778,13 @@ def _create_dag(gates_qubits_pairs): if len(saturated_qubits) >= 2: break dag.add_edges_from(connectivity_list) + return _remove_redundant_connections(dag) def _remove_redundant_connections(dag: nx.DiGraph): - """Helper method for :func:`_create_dag`. + """Helper method for :func:`qibo.transpiler.router._create_dag`. + Remove redundant connection from a DAG using transitive reduction. Args: @@ -732,4 +797,5 @@ def _remove_redundant_connections(dag: nx.DiGraph): new_dag.add_nodes_from(range(dag.number_of_nodes())) transitive_reduction = nx.transitive_reduction(dag) new_dag.add_edges_from(transitive_reduction.edges) + return new_dag From a7aa8aa206d5fad7d77723f5bf67180a78b53e2e Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Fri, 22 Dec 2023 12:53:32 +0400 Subject: [PATCH 17/19] corrections --- src/qibo/transpiler/pipeline.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/qibo/transpiler/pipeline.py b/src/qibo/transpiler/pipeline.py index 289429e40f..dcebab620c 100644 --- a/src/qibo/transpiler/pipeline.py +++ b/src/qibo/transpiler/pipeline.py @@ -151,10 +151,9 @@ def restrict_connectivity_qubits(connectivity: nx.Graph, qubits: list): ) new_connectivity = nx.Graph() new_connectivity.add_nodes_from(qubits) - new_edges = [] - for edge in connectivity.edges: - if edge[0] in qubits and edge[1] in qubits: - new_edges.append(edge) + new_edges = [ + edge for edge in connectivity.edges if (edge[0] in qubits and edge[1] in qubits) + ] new_connectivity.add_edges_from(new_edges) if not nx.is_connected(new_connectivity): raise_error(ConnectivityError, "The new connectivity graph is not connected.") @@ -165,8 +164,7 @@ class Passes: """Define a transpiler pipeline consisting of smaller transpiler steps that are applied sequentially: Args: - passes (list): list of passes to be applied sequentially, - if None default transpiler will be used. + passes (list): list of passes to be applied sequentially. connectivity (nx.Graph): physical qubits connectivity. native_gates (NativeGates): native gates. on_qubits (list): list of physical qubits to be used. If "None" all qubits are used. @@ -183,10 +181,7 @@ def __init__( connectivity = restrict_connectivity_qubits(connectivity, on_qubits) self.connectivity = connectivity self.native_gates = native_gates - if passes is None: - self.passes = self.default() - else: - self.passes = passes + self.passes = passes def default(self): """Return the default transpiler pipeline for the required hardware connectivity.""" From b564fc4408533253bbce99d3e82a3c7feb4dc4e2 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Fri, 22 Dec 2023 13:29:35 +0400 Subject: [PATCH 18/19] corrections --- src/qibo/transpiler/router.py | 2 -- tests/test_transpiler_pipeline.py | 57 ++++--------------------------- tests/test_transpiler_placer.py | 8 ++--- tests/test_transpiler_router.py | 25 ++------------ 4 files changed, 14 insertions(+), 78 deletions(-) diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 1d19cdaa7e..00e6a86da5 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -25,8 +25,6 @@ def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): if list(connectivity.nodes) != list(range(connectivity.number_of_nodes())): node_mapping = {node: i for i, node in enumerate(connectivity.nodes)} new_connectivity = nx.Graph() - # for new_name in node_mapping.values(): - # new_connectivity.add_node(new_name) new_connectivity.add_edges_from( [(node_mapping[u], node_mapping[v]) for u, v in connectivity.edges] ) diff --git a/tests/test_transpiler_pipeline.py b/tests/test_transpiler_pipeline.py index cc01a09b61..65e6cf2534 100644 --- a/tests/test_transpiler_pipeline.py +++ b/tests/test_transpiler_pipeline.py @@ -53,13 +53,6 @@ def generate_random_circuit(nqubits, ngates, seed=None): return circuit -def small_circuit(): - circuit = Circuit(2) - circuit.add(gates.H(0)) - circuit.add(gates.CZ(0, 1)) - return circuit - - def star_connectivity(): Q = [i for i in range(5)] chip = nx.Graph() @@ -173,13 +166,11 @@ def test_is_satisfied_false_connectivity(): assert not default_transpiler.is_satisfied(circuit) -@pytest.mark.parametrize("reps", [range(3)]) +@pytest.mark.parametrize("gates", [5, 20]) @pytest.mark.parametrize("placer", [Random, Trivial, ReverseTraversal]) @pytest.mark.parametrize("routing", [ShortestPaths, Sabre]) -@pytest.mark.parametrize( - "circ", [generate_random_circuit(nqubits=5, ngates=20), small_circuit()] -) -def test_custom_passes(circ, placer, routing, reps): +def test_custom_passes(placer, routing, gates): + circ = generate_random_circuit(nqubits=5, ngates=gates) custom_passes = [] custom_passes.append(Preprocessing(connectivity=star_connectivity())) if placer == ReverseTraversal: @@ -191,7 +182,7 @@ def test_custom_passes(circ, placer, routing, reps): ) else: custom_passes.append(placer(connectivity=star_connectivity())) - custom_passes.append(ShortestPaths(connectivity=star_connectivity())) + custom_passes.append(routing(connectivity=star_connectivity())) custom_passes.append(Unroller(native_gates=NativeGates.default())) custom_pipeline = Passes( custom_passes, @@ -210,11 +201,11 @@ def test_custom_passes(circ, placer, routing, reps): ) -@pytest.mark.parametrize("reps", [range(3)]) +@pytest.mark.parametrize("gates", [5, 20]) @pytest.mark.parametrize("placer", [Random, Trivial, ReverseTraversal]) @pytest.mark.parametrize("routing", [ShortestPaths, Sabre]) -def test_custom_passes_restict(reps, placer, routing): - circ = generate_random_circuit(nqubits=3, ngates=20) +def test_custom_passes_restict(gates, placer, routing): + circ = generate_random_circuit(nqubits=3, ngates=gates) custom_passes = [] custom_passes.append(Preprocessing(connectivity=star_connectivity())) if placer == ReverseTraversal: @@ -236,8 +227,6 @@ def test_custom_passes_restict(reps, placer, routing): ) transpiled_circ, final_layout = custom_pipeline(circ) initial_layout = custom_pipeline.get_initial_layout() - print(initial_layout) - print(final_layout) assert_transpiling( original_circuit=circ, transpiled_circuit=transpiled_circ, @@ -248,38 +237,6 @@ def test_custom_passes_restict(reps, placer, routing): ) -@pytest.mark.parametrize( - "circ", [generate_random_circuit(nqubits=5, ngates=20), small_circuit()] -) -def test_custom_passes_reverse(circ): - custom_passes = [] - custom_passes.append(Preprocessing(connectivity=star_connectivity())) - custom_passes.append( - ReverseTraversal( - connectivity=star_connectivity(), - routing_algorithm=ShortestPaths(connectivity=star_connectivity()), - depth=20, - ) - ) - custom_passes.append(ShortestPaths(connectivity=star_connectivity())) - custom_passes.append(Unroller(native_gates=NativeGates.default())) - custom_pipeline = Passes( - custom_passes, - connectivity=star_connectivity(), - native_gates=NativeGates.default(), - ) - transpiled_circ, final_layout = custom_pipeline(circ) - initial_layout = custom_pipeline.get_initial_layout() - assert_transpiling( - original_circuit=circ, - transpiled_circuit=transpiled_circ, - connectivity=star_connectivity(), - initial_layout=initial_layout, - final_layout=final_layout, - native_gates=NativeGates.default(), - ) - - def test_custom_passes_multiple_placer(): custom_passes = [] custom_passes.append(Random(connectivity=star_connectivity())) diff --git a/tests/test_transpiler_placer.py b/tests/test_transpiler_placer.py index 09d4a80d2a..434037e7a4 100644 --- a/tests/test_transpiler_placer.py +++ b/tests/test_transpiler_placer.py @@ -75,8 +75,8 @@ def test_mapping_consistency_error(layout): def test_mapping_consistency_restricted(): layout = {"q0": 0, "q2": 1} connectivity = star_connectivity() - restricted_connecyivity = restrict_connectivity_qubits(connectivity, [0, 2]) - assert_mapping_consistency(layout, restricted_connecyivity) + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2]) + assert_mapping_consistency(layout, restricted_connectivity) @pytest.mark.parametrize( @@ -88,9 +88,9 @@ def test_mapping_consistency_restricted(): ) def test_mapping_consistency_restricted_error(layout): connectivity = star_connectivity() - restricted_connecyivity = restrict_connectivity_qubits(connectivity, [0, 2]) + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2]) with pytest.raises(PlacementError): - assert_mapping_consistency(layout, restricted_connecyivity) + assert_mapping_consistency(layout, restricted_connectivity) def test_gates_qubits_pairs(): diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index 0a2d8422f3..716f0f02bd 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -312,7 +312,8 @@ def test_sabre_intermediate_measurements(): assert routed_circ.queue[3].result is measurement.result -def test_sabre_restrict_qubits(): +@pytest.mark.parametrize("router_algorithm", [Sabre, ShortestPaths]) +def test_restrict_qubits(router_algorithm): circ = Circuit(3) circ.add(gates.CZ(0, 1)) circ.add(gates.CZ(0, 2)) @@ -320,27 +321,7 @@ def test_sabre_restrict_qubits(): initial_layout = {"q0": 0, "q2": 2, "q3": 1} connectivity = star_connectivity() restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3]) - router = Sabre(connectivity=restricted_connectivity) - routed_circ, final_layout = router(circuit=circ, initial_layout=initial_layout) - assert_circuit_equivalence( - original_circuit=circ, - transpiled_circuit=routed_circ, - final_map=final_layout, - initial_map=initial_layout, - ) - assert_connectivity(restricted_connectivity, routed_circ) - assert_placement(routed_circ, final_layout, connectivity=restricted_connectivity) - - -def test_shortest_paths_restrict_qubits(): - circ = Circuit(3) - circ.add(gates.CZ(0, 1)) - circ.add(gates.CZ(0, 2)) - circ.add(gates.CZ(2, 1)) - initial_layout = {"q0": 0, "q2": 2, "q3": 1} - connectivity = star_connectivity() - restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3]) - router = ShortestPaths(connectivity=restricted_connectivity) + router = router_algorithm(connectivity=restricted_connectivity) routed_circ, final_layout = router(circuit=circ, initial_layout=initial_layout) assert_circuit_equivalence( original_circuit=circ, From 015a60c8c82b9fcf77259ceaff356b4e07e383d1 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Fri, 22 Dec 2023 14:35:19 +0400 Subject: [PATCH 19/19] 100 coverage --- tests/test_transpiler_pipeline.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_transpiler_pipeline.py b/tests/test_transpiler_pipeline.py index 65e6cf2534..ce8afee66d 100644 --- a/tests/test_transpiler_pipeline.py +++ b/tests/test_transpiler_pipeline.py @@ -142,9 +142,10 @@ def test_error_connectivity(): default_transpiler = Passes(passes=None, connectivity=None) -def test_is_satisfied(): +@pytest.mark.parametrize("qubits", [3, 5]) +def test_is_satisfied(qubits): default_transpiler = Passes(passes=None, connectivity=star_connectivity()) - circuit = Circuit(5) + circuit = Circuit(qubits) circuit.add(gates.CZ(0, 2)) circuit.add(gates.Z(0)) assert default_transpiler.is_satisfied(circuit) @@ -166,11 +167,12 @@ def test_is_satisfied_false_connectivity(): assert not default_transpiler.is_satisfied(circuit) +@pytest.mark.parametrize("qubits", [2, 5]) @pytest.mark.parametrize("gates", [5, 20]) @pytest.mark.parametrize("placer", [Random, Trivial, ReverseTraversal]) @pytest.mark.parametrize("routing", [ShortestPaths, Sabre]) -def test_custom_passes(placer, routing, gates): - circ = generate_random_circuit(nqubits=5, ngates=gates) +def test_custom_passes(placer, routing, gates, qubits): + circ = generate_random_circuit(nqubits=qubits, ngates=gates) custom_passes = [] custom_passes.append(Preprocessing(connectivity=star_connectivity())) if placer == ReverseTraversal: