From 10e1d3eb48096e18c24e6992dd6d30a1a8d3ecea Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Wed, 27 Sep 2023 11:09:53 +0400 Subject: [PATCH 01/13] idea of the algorithm and block class --- src/qibolab/transpilers/blocks.py | 48 +++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/qibolab/transpilers/blocks.py diff --git a/src/qibolab/transpilers/blocks.py b/src/qibolab/transpilers/blocks.py new file mode 100644 index 000000000..4cf339e9c --- /dev/null +++ b/src/qibolab/transpilers/blocks.py @@ -0,0 +1,48 @@ +from qibo import Circuit +from qibo.config import raise_error + + +class Block: + """A block contains a subset of gates acting on two qubits. + + Args: + qubits (tuple): qubits where the block is acting. + gates (list): list of gates that compose the block. + name (str): name of the block. + """ + + def __init__(self, qubits: tuple, gates: list, name: str = None): + self.qubits = qubits + self.gates = gates + self.name = name + + def rename(self, name): + self.name = name + + def add_gate(self, gate): + self.gates.append(gate) + + def get_qubits(self): + return self.qubits + + def get_gates(self): + return self.gates + + def get_name(self): + return self.name + + # TODO + def kak_decompose(self): + """Return KAK decomposition of the block""" + raise_error(NotImplementedError) + + +def block_decomposition(circuit: Circuit): + """Decompose a circuit into blocks of gates acting on two qubits. + + Args: + circuit (qibo.Circuit): circuit to be decomposed. + + Return: + blocks (list): list of blocks that act on two qubits. + """ From ecc477ba50bd5adcce7eb3b6a605b9b716734223 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Wed, 27 Sep 2023 13:01:30 +0400 Subject: [PATCH 02/13] dag circuit and initial blocking --- src/qibolab/transpilers/blocks.py | 94 +++++++++++++++++++++++++++++-- tests/test_transpilers_blocks.py | 26 +++++++++ 2 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 tests/test_transpilers_blocks.py diff --git a/src/qibolab/transpilers/blocks.py b/src/qibolab/transpilers/blocks.py index 4cf339e9c..3ebab0748 100644 --- a/src/qibolab/transpilers/blocks.py +++ b/src/qibolab/transpilers/blocks.py @@ -1,6 +1,11 @@ -from qibo import Circuit +from copy import copy + +import networkx as nx +from qibo import Circuit, gates from qibo.config import raise_error +from .abstract import find_gates_qubits_pairs + class Block: """A block contains a subset of gates acting on two qubits. @@ -9,18 +14,22 @@ class Block: qubits (tuple): qubits where the block is acting. gates (list): list of gates that compose the block. name (str): name of the block. + entangled (bool): true if there is at least a two qubit gate in the block. """ - def __init__(self, qubits: tuple, gates: list, name: str = None): + def __init__(self, qubits: tuple, gates: list, name: str = None, entangled: bool = True): self.qubits = qubits self.gates = gates self.name = name + self.entangled = entangled def rename(self, name): self.name = name - def add_gate(self, gate): + def add_gate(self, gate: gates.Gate): self.gates.append(gate) + if len(gate.qubits) == 2: + self.entangled = True def get_qubits(self): return self.qubits @@ -33,7 +42,9 @@ def get_name(self): # TODO def kak_decompose(self): - """Return KAK decomposition of the block""" + """Return KAK decomposition of the block. + This should be done only if the block is entangled. + """ raise_error(NotImplementedError) @@ -41,8 +52,81 @@ def block_decomposition(circuit: Circuit): """Decompose a circuit into blocks of gates acting on two qubits. Args: - circuit (qibo.Circuit): circuit to be decomposed. + circuit (qibo.models.Circuit): circuit to be decomposed. Return: blocks (list): list of blocks that act on two qubits. """ + dag = create_dag(circuit) + pairs = find_gates_qubits_pairs(circuit) + + +def initial_block_deomposition(circuit): + """Decompose a circuit into blocks of gates acting on two qubits. + This decomposition is not minimal. + + Args: + circuit (qibo.models.Circuit): circuit to be decomposed. + + Return: + blocks (list): list of blocks that act on two qubits. + """ + blocks = [] + all_gates = copy(circuit.queue) + # while len(all_gates)>3: + for idx, gate in enumerate(all_gates): + if len(gate.qubits) == 2: + qubits = gate.qubits + block_gates = find_previous_gates(all_gates[0:idx], qubits) + block_gates.append(gate) + print(block_gates) + block = Block(qubits=qubits, gates=block_gates) + for gate in block_gates: + all_gates.remove(gate) + blocks.append(block) + break + if len(gate.qubits) >= 3: + raise_error(ValueError, "Gates targeting more than 2 qubits are not supported") + print(all_gates) + return blocks + + +def find_previous_gates(gates: list, qubits: tuple): + """Return a list containing all gates acting on qubits.""" + previous_gates = [] + for gate in gates: + if gate.qubits[0] in qubits: + previous_gates.append(gate) + return previous_gates + + +def create_dag(circuit): + """Create direct acyclic graph (dag) of the circuit based on two qubit gates commutativity relations. + + Args: + circuit (qibo.models.Circuit): circuit to be transformed into dag. + + Returns: + cleaned_dag (nx.DiGraph): dag of the circuit. + """ + circuit = find_gates_qubits_pairs(circuit) + dag = nx.DiGraph() + dag.add_nodes_from(list(i for i in range(len(circuit)))) + # Find all successors + connectivity_list = [] + for idx, gate in enumerate(circuit): + for next_idx, next_gate in enumerate(circuit[idx + 1 :]): + for qubit in gate: + if qubit in next_gate: + connectivity_list.append((idx, next_idx + idx + 1)) + dag.add_edges_from(connectivity_list) + for layer, nodes in enumerate(nx.topological_generations(dag)): + for node in nodes: + dag.nodes[node]["layer"] = layer + # Remove redundant connections + cleaned_dag = dag.copy() + for node in dag.nodes: + for successor in dag.successors(node): + if not dag.nodes[successor]["layer"] == dag.nodes[node]["layer"] + 1: + cleaned_dag.remove_edge(node, successor) + return cleaned_dag diff --git a/tests/test_transpilers_blocks.py b/tests/test_transpilers_blocks.py new file mode 100644 index 000000000..e631ea4fe --- /dev/null +++ b/tests/test_transpilers_blocks.py @@ -0,0 +1,26 @@ +from qibo import Circuit, gates + +from qibolab.transpilers.blocks import initial_block_deomposition + +circ = Circuit(4) +circ.add(gates.H(0)) +circ.add(gates.H(1)) +circ.add(gates.CZ(0, 1)) +circ.add(gates.H(0)) +# circ.add(gates.H(1)) +# circ.add(gates.CZ(2,3)) +# circ.add(gates.H(2)) +# circ.add(gates.CZ(2,1)) +# circ.add(gates.H(1)) +# #circ.add(gates.CZ(2,1)) +# circ.add(gates.CZ(2,0)) + +# dag = create_dag(circ) +# pos = nx.multipartite_layout(dag, subset_key="layer") +# fig, ax = plt.subplots() +# nx.draw_networkx(dag, pos=pos, ax=ax) +# ax.set_title("DAG layout in topological order") +# fig.tight_layout() +# plt.show() + +initial_block_deomposition(circ) From d7bb3cf00505f57b5a884b0d632bae4d43b1e720 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Wed, 27 Sep 2023 16:58:25 +0400 Subject: [PATCH 03/13] initial blocking completed --- src/qibolab/transpilers/blocks.py | 142 +++++++++++++++++++++++++----- tests/test_transpilers_blocks.py | 33 ++++--- 2 files changed, 134 insertions(+), 41 deletions(-) diff --git a/src/qibolab/transpilers/blocks.py b/src/qibolab/transpilers/blocks.py index 3ebab0748..f1d92b018 100644 --- a/src/qibolab/transpilers/blocks.py +++ b/src/qibolab/transpilers/blocks.py @@ -6,6 +6,12 @@ from .abstract import find_gates_qubits_pairs +DEBUG = True + + +class BlockingError(Exception): + """Raise when an error occurs in the blocking procedure""" + class Block: """A block contains a subset of gates acting on two qubits. @@ -31,8 +37,11 @@ def add_gate(self, gate: gates.Gate): if len(gate.qubits) == 2: self.entangled = True + def count_2q_gates(self): + return count_2q_gates(gatelist=self.get_gates()) + def get_qubits(self): - return self.qubits + return tuple(sorted(self.qubits)) def get_gates(self): return self.gates @@ -40,6 +49,13 @@ def get_gates(self): def get_name(self): return self.name + def info(self): + print("Block Name: ", self.get_name()) + print("Qubits: ", self.get_qubits()) + print("Gates: ", self.get_gates()) + print("Number of two qubits gates: ", self.count_2q_gates()) + print("Entangled: ", self.entangled) + # TODO def kak_decompose(self): """Return KAK decomposition of the block. @@ -57,11 +73,14 @@ def block_decomposition(circuit: Circuit): Return: blocks (list): list of blocks that act on two qubits. """ + if circuit.nqubits < 2: + raise_error(BlockingError, "Only circuits with at least two qubits can be decomposed with this function.") dag = create_dag(circuit) - pairs = find_gates_qubits_pairs(circuit) + initial_blocks = initial_block_deomposition(circuit) + return initial_blocks -def initial_block_deomposition(circuit): +def initial_block_deomposition(circuit: Circuit): """Decompose a circuit into blocks of gates acting on two qubits. This decomposition is not minimal. @@ -73,24 +92,72 @@ def initial_block_deomposition(circuit): """ blocks = [] all_gates = copy(circuit.queue) - # while len(all_gates)>3: - for idx, gate in enumerate(all_gates): - if len(gate.qubits) == 2: - qubits = gate.qubits - block_gates = find_previous_gates(all_gates[0:idx], qubits) - block_gates.append(gate) - print(block_gates) - block = Block(qubits=qubits, gates=block_gates) - for gate in block_gates: - all_gates.remove(gate) - blocks.append(block) - break - if len(gate.qubits) >= 3: - raise_error(ValueError, "Gates targeting more than 2 qubits are not supported") - print(all_gates) + two_qubit_gates = count_2q_gates(all_gates) + while two_qubit_gates > 0: + for idx, gate in enumerate(all_gates): + if len(gate.qubits) == 2: + qubits = gate.qubits + block_gates = find_previous_gates(all_gates[0:idx], qubits) + block_gates.append(gate) + block_gates += find_successive_gates(all_gates[idx + 1 :], qubits) + block = Block(qubits=qubits, gates=block_gates) + remove_gates(all_gates, block_gates) + two_qubit_gates -= 1 + blocks.append(block) + break + if len(gate.qubits) >= 3: + raise_error(BlockingError, "Gates targeting more than 2 qubits are not supported.") + # Now we need to deal with the remaining spare single qubit gates + while len(all_gates) > 0: + first_qubit = all_gates[0].qubits[0] + block_gates = gates_on_qubit(gatelist=all_gates, qubit=first_qubit) + remove_gates(all_gates, block_gates) + # Add other single qubits if there are still single qubit gates + if len(all_gates) > 0: + second_qubit = all_gates[0].qubits[0] + second_qubit_block_gates = gates_on_qubit(gatelist=all_gates, qubit=second_qubit) + block_gates += second_qubit_block_gates + remove_gates(all_gates, second_qubit_block_gates) + block = Block(qubits=(first_qubit, second_qubit), gates=block_gates, entangled=False) + # In case there are no other spare single qubit gates create a block using a following qubit as placeholder + else: + block = Block(qubits=(first_qubit, (first_qubit + 1) % circuit.nqubits), gates=block_gates, entangled=False) + blocks.append(block) return blocks +def gates_on_qubit(gatelist, qubit): + """Return a list of all single qubit gates in gatelist acting on a specific qubit.""" + selected_gates = [] + for gate in gatelist: + if gate.qubits[0] == qubit: + selected_gates.append(gate) + return selected_gates + + +def remove_gates(gatelist, remove_list): + """Remove all gates present in remove_list from gatelist.""" + for gate in remove_list: + gatelist.remove(gate) + + +def count_2q_gates(gatelist: list): + """Return the number of two qubit gates in a list of gates.""" + return len([gate for gate in gatelist if len(gate.qubits) == 2]) + + +def find_successive_gates(gates: list, qubits: tuple): + """Return a list containing all gates acting on qubits until a new two qubit gate acting on qubits is found.""" + successive_gates = [] + for qubit in qubits: + for gate in gates: + if (len(gate.qubits) == 1) and (gate.qubits[0] == qubit): + successive_gates.append(gate) + if (len(gate.qubits) == 2) and (qubit in gate.qubits): + break + return successive_gates + + def find_previous_gates(gates: list, qubits: tuple): """Return a list containing all gates acting on qubits.""" previous_gates = [] @@ -123,10 +190,37 @@ def create_dag(circuit): for layer, nodes in enumerate(nx.topological_generations(dag)): for node in nodes: dag.nodes[node]["layer"] = layer - # Remove redundant connections - cleaned_dag = dag.copy() - for node in dag.nodes: - for successor in dag.successors(node): - if not dag.nodes[successor]["layer"] == dag.nodes[node]["layer"] + 1: - cleaned_dag.remove_edge(node, successor) + show_dag(dag) + cleaned_dag = remove_redundant_connections(dag) + for layer, nodes in enumerate(nx.topological_generations(dag)): + for node in nodes: + dag.nodes[node]["layer"] = layer + show_dag(cleaned_dag) return cleaned_dag + + +def remove_redundant_connections(G): + """Remove redundant connection from a DAG""" + # Create a copy of the input DAG + new_G = G.copy() + # Iterate through the nodes in topological order + for node in nx.topological_sort(G): + # Compute the set of nodes reachable from the current node + reachable_nodes = set(nx.descendants(new_G, node)) | {node} + # Remove edges that are redundant + for neighbor in list(new_G.neighbors(node)): + if neighbor in reachable_nodes: + new_G.remove_edge(node, neighbor) + return new_G + + +def show_dag(dag): + """Plot DAG""" + import matplotlib.pyplot as plt + + pos = nx.multipartite_layout(dag, subset_key="layer") + fig, ax = plt.subplots() + nx.draw_networkx(dag, pos=pos, ax=ax) + ax.set_title("DAG layout in topological order") + fig.tight_layout() + plt.show() diff --git a/tests/test_transpilers_blocks.py b/tests/test_transpilers_blocks.py index e631ea4fe..bcfabf0d5 100644 --- a/tests/test_transpilers_blocks.py +++ b/tests/test_transpilers_blocks.py @@ -1,26 +1,25 @@ from qibo import Circuit, gates -from qibolab.transpilers.blocks import initial_block_deomposition +from qibolab.transpilers.blocks import create_dag, initial_block_deomposition -circ = Circuit(4) +circ = Circuit(6) circ.add(gates.H(0)) circ.add(gates.H(1)) +circ.add(gates.H(2)) circ.add(gates.CZ(0, 1)) +circ.add(gates.CZ(0, 1)) +circ.add(gates.CZ(2, 3)) +circ.add(gates.CZ(1, 2)) +circ.add(gates.CZ(0, 1)) +circ.add(gates.CZ(2, 3)) circ.add(gates.H(0)) -# circ.add(gates.H(1)) -# circ.add(gates.CZ(2,3)) -# circ.add(gates.H(2)) -# circ.add(gates.CZ(2,1)) -# circ.add(gates.H(1)) -# #circ.add(gates.CZ(2,1)) -# circ.add(gates.CZ(2,0)) +circ.add(gates.H(3)) +circ.add(gates.H(4)) +circ.add(gates.H(5)) + -# dag = create_dag(circ) -# pos = nx.multipartite_layout(dag, subset_key="layer") -# fig, ax = plt.subplots() -# nx.draw_networkx(dag, pos=pos, ax=ax) -# ax.set_title("DAG layout in topological order") -# fig.tight_layout() -# plt.show() +dag = create_dag(circ) -initial_block_deomposition(circ) +blocks = initial_block_deomposition(circ) +for block in blocks: + block.info() From 9db4985fec51b0a0850caf7505111a195393b5da Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Thu, 28 Sep 2023 13:50:07 +0400 Subject: [PATCH 04/13] fixed code and first tests --- src/qibolab/transpilers/blocks.py | 167 ++++++++++++++---------------- tests/test_transpilers_blocks.py | 69 +++++++----- 2 files changed, 124 insertions(+), 112 deletions(-) diff --git a/src/qibolab/transpilers/blocks.py b/src/qibolab/transpilers/blocks.py index f1d92b018..a9911c842 100644 --- a/src/qibolab/transpilers/blocks.py +++ b/src/qibolab/transpilers/blocks.py @@ -1,12 +1,8 @@ -from copy import copy +from copy import copy, deepcopy -import networkx as nx -from qibo import Circuit, gates +from qibo import Circuit from qibo.config import raise_error - -from .abstract import find_gates_qubits_pairs - -DEBUG = True +from qibo.gates import Gate class BlockingError(Exception): @@ -24,7 +20,7 @@ class Block: """ def __init__(self, qubits: tuple, gates: list, name: str = None, entangled: bool = True): - self.qubits = qubits + self._qubits = qubits self.gates = gates self.name = name self.entangled = entangled @@ -32,55 +28,111 @@ def __init__(self, qubits: tuple, gates: list, name: str = None, entangled: bool def rename(self, name): self.name = name - def add_gate(self, gate: gates.Gate): + def add_gate(self, gate: Gate): + if not gate.qubits == self.qubits: + raise BlockingError( + "Gate acting on qubits {} can't be added to block acting on qubits {}.".format( + gate.qubits, self._qubits + ) + ) self.gates.append(gate) if len(gate.qubits) == 2: self.entangled = True def count_2q_gates(self): - return count_2q_gates(gatelist=self.get_gates()) + return count_2q_gates(self.gates) - def get_qubits(self): - return tuple(sorted(self.qubits)) + @property + def qubits(self): + return tuple(sorted(self._qubits)) - def get_gates(self): - return self.gates - - def get_name(self): - return self.name + @qubits.setter + def qubits(self, qubits): + self._qubits = qubits def info(self): - print("Block Name: ", self.get_name()) - print("Qubits: ", self.get_qubits()) - print("Gates: ", self.get_gates()) + print("Block Name: ", self.name) + print("Qubits: ", self.qubits) + print("Gates: ", self.gates) print("Number of two qubits gates: ", self.count_2q_gates()) print("Entangled: ", self.entangled) # TODO - def kak_decompose(self): + def kak_decompose(self): # pragma: no cover """Return KAK decomposition of the block. - This should be done only if the block is entangled. + This should be done only if the block is entangled and the number of + two qubit gates is higher than the number after the decomposition. """ raise_error(NotImplementedError) -def block_decomposition(circuit: Circuit): +def fuse_blocks(block_1: Block, block_2: Block, name=None): + """Fuse two gate blocks, the qubits they are acting on must coincide. + + Args: + block_1 (.transpilers.blocks.Block): first block. + block_2 (.transpilers.blocks.Block): second block. + name (str): name of the fused block. + + Return: + fused_block (.transpilers.blocks.Block): fusion of the two input blocks. + """ + if not block_1.qubits == block_2.qubits: + raise BlockingError("In order to fuse two blocks their qubits must coincide.") + entangled = block_1.entangled or block_2.entangled + return Block(qubits=block_1.qubits, gates=block_1.gates + block_2.gates, name=name, entangled=entangled) + + +def commute(block_1: Block, block_2: Block): + """Check if two blocks commute (share qubits). + + Args: + block_1 (.transpilers.blocks.Block): first block. + block_2 (.transpilers.blocks.Block): second block. + + Return: + True if the two blocks don't share any qubit. + False otherwise. + """ + for qubit in block_1.qubits: + if qubit in block_2.qubits: + return False + return True + + +def block_decomposition(circuit: Circuit, fuse: bool = True): """Decompose a circuit into blocks of gates acting on two qubits. Args: circuit (qibo.models.Circuit): circuit to be decomposed. + fuse (bool): fuse adjacent blocks acting on the same qubits. Return: blocks (list): list of blocks that act on two qubits. """ if circuit.nqubits < 2: - raise_error(BlockingError, "Only circuits with at least two qubits can be decomposed with this function.") - dag = create_dag(circuit) - initial_blocks = initial_block_deomposition(circuit) - return initial_blocks + raise BlockingError("Only circuits with at least two qubits can be decomposed with block_decomposition.") + initial_blocks = initial_block_decomposition(circuit) + if not fuse: + return initial_blocks + blocks = [] + while len(initial_blocks) > 0: + first_block = initial_blocks[0] + initial_blocks.remove(first_block) + if len(initial_blocks) > 0: + following_blocks = deepcopy(initial_blocks) + for idx, second_block in enumerate(following_blocks): + try: + first_block = fuse_blocks(first_block, second_block) + initial_blocks.remove(initial_blocks[idx]) + except BlockingError: + if not commute(first_block, second_block): + break + blocks.append(first_block) + return blocks -def initial_block_deomposition(circuit: Circuit): +def initial_block_decomposition(circuit: Circuit): """Decompose a circuit into blocks of gates acting on two qubits. This decomposition is not minimal. @@ -165,62 +217,3 @@ def find_previous_gates(gates: list, qubits: tuple): if gate.qubits[0] in qubits: previous_gates.append(gate) return previous_gates - - -def create_dag(circuit): - """Create direct acyclic graph (dag) of the circuit based on two qubit gates commutativity relations. - - Args: - circuit (qibo.models.Circuit): circuit to be transformed into dag. - - Returns: - cleaned_dag (nx.DiGraph): dag of the circuit. - """ - circuit = find_gates_qubits_pairs(circuit) - dag = nx.DiGraph() - dag.add_nodes_from(list(i for i in range(len(circuit)))) - # Find all successors - connectivity_list = [] - for idx, gate in enumerate(circuit): - for next_idx, next_gate in enumerate(circuit[idx + 1 :]): - for qubit in gate: - if qubit in next_gate: - connectivity_list.append((idx, next_idx + idx + 1)) - dag.add_edges_from(connectivity_list) - for layer, nodes in enumerate(nx.topological_generations(dag)): - for node in nodes: - dag.nodes[node]["layer"] = layer - show_dag(dag) - cleaned_dag = remove_redundant_connections(dag) - for layer, nodes in enumerate(nx.topological_generations(dag)): - for node in nodes: - dag.nodes[node]["layer"] = layer - show_dag(cleaned_dag) - return cleaned_dag - - -def remove_redundant_connections(G): - """Remove redundant connection from a DAG""" - # Create a copy of the input DAG - new_G = G.copy() - # Iterate through the nodes in topological order - for node in nx.topological_sort(G): - # Compute the set of nodes reachable from the current node - reachable_nodes = set(nx.descendants(new_G, node)) | {node} - # Remove edges that are redundant - for neighbor in list(new_G.neighbors(node)): - if neighbor in reachable_nodes: - new_G.remove_edge(node, neighbor) - return new_G - - -def show_dag(dag): - """Plot DAG""" - import matplotlib.pyplot as plt - - pos = nx.multipartite_layout(dag, subset_key="layer") - fig, ax = plt.subplots() - nx.draw_networkx(dag, pos=pos, ax=ax) - ax.set_title("DAG layout in topological order") - fig.tight_layout() - plt.show() diff --git a/tests/test_transpilers_blocks.py b/tests/test_transpilers_blocks.py index bcfabf0d5..2eb244302 100644 --- a/tests/test_transpilers_blocks.py +++ b/tests/test_transpilers_blocks.py @@ -1,25 +1,44 @@ -from qibo import Circuit, gates - -from qibolab.transpilers.blocks import create_dag, initial_block_deomposition - -circ = Circuit(6) -circ.add(gates.H(0)) -circ.add(gates.H(1)) -circ.add(gates.H(2)) -circ.add(gates.CZ(0, 1)) -circ.add(gates.CZ(0, 1)) -circ.add(gates.CZ(2, 3)) -circ.add(gates.CZ(1, 2)) -circ.add(gates.CZ(0, 1)) -circ.add(gates.CZ(2, 3)) -circ.add(gates.H(0)) -circ.add(gates.H(3)) -circ.add(gates.H(4)) -circ.add(gates.H(5)) - - -dag = create_dag(circ) - -blocks = initial_block_deomposition(circ) -for block in blocks: - block.info() +import pytest +from qibo import gates + +from qibolab.transpilers.blocks import Block, BlockingError + + +def test_count_2q_gates(): + block = Block(qubits=(0, 1), gates=[gates.CZ(0, 1), gates.CZ(0, 1), gates.H(0)]) + assert block.count_2q_gates() == 2 + + +def test_add_gate(): + block = Block(qubits=(0, 1), gates=[gates.H(0)], entangled=False) + block.add_gate(gates.CZ(0, 1)) + assert block.entangled == True + assert block.count_2q_gates() == 1 + + +def test_add_gate_error(): + block = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)]) + with pytest.raises(BlockingError): + block.add_gate(gates.CZ(0, 2)) + + +def test_fuse_blocks(): + block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)]) + block_2 = Block(qubits=(0, 1), gates=[gates.H(0)], entangled=False) + fused = fuse_blocks(block_1, block_2) + assert fused.qubits == (0, 1) + assert fused.entangled == True + assert fused.count_2q_gates() == 1 + + +def test_fuse_blocks_error(): + block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)]) + block_2 = Block(qubits=(1, 2), gates=[gates.CZ(1, 2)]) + with pytest.raises(BlockingError): + fused = fuse_blocks(block_1, block_2) + + +# circ = Circuit(3) +# circ.add(gates.CZ(0, 1)) +# circ.add(gates.CZ(0, 1)) +# circ.add(gates.CZ(1, 2)) From 0f3f7cb5c10e8cdb9252895af2b12e3737e11160 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Thu, 28 Sep 2023 14:24:07 +0400 Subject: [PATCH 05/13] new tests --- src/qibolab/transpilers/blocks.py | 28 +++++++------ tests/test_transpilers_blocks.py | 65 +++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 15 deletions(-) diff --git a/src/qibolab/transpilers/blocks.py b/src/qibolab/transpilers/blocks.py index a9911c842..d0173d01a 100644 --- a/src/qibolab/transpilers/blocks.py +++ b/src/qibolab/transpilers/blocks.py @@ -16,19 +16,24 @@ class Block: qubits (tuple): qubits where the block is acting. gates (list): list of gates that compose the block. name (str): name of the block. - entangled (bool): true if there is at least a two qubit gate in the block. + entangled (bool): True if the block entangles the qubits (there is at least one qubit gate). """ - def __init__(self, qubits: tuple, gates: list, name: str = None, entangled: bool = True): + def __init__(self, qubits: tuple, gates: list, name: str = None): self._qubits = qubits self.gates = gates self.name = name - self.entangled = entangled + if count_2q_gates(gates) > 0: + self.entangled = True + else: + self.entangled = False def rename(self, name): + """Rename block""" self.name = name def add_gate(self, gate: Gate): + """Add a new gate to the block.""" if not gate.qubits == self.qubits: raise BlockingError( "Gate acting on qubits {} can't be added to block acting on qubits {}.".format( @@ -40,10 +45,12 @@ def add_gate(self, gate: Gate): self.entangled = True def count_2q_gates(self): + """Return the number of two qubit gates in the block.""" return count_2q_gates(self.gates) @property def qubits(self): + """Return a sorted tuple with qubits of the block.""" return tuple(sorted(self._qubits)) @qubits.setter @@ -79,8 +86,7 @@ def fuse_blocks(block_1: Block, block_2: Block, name=None): """ if not block_1.qubits == block_2.qubits: raise BlockingError("In order to fuse two blocks their qubits must coincide.") - entangled = block_1.entangled or block_2.entangled - return Block(qubits=block_1.qubits, gates=block_1.gates + block_2.gates, name=name, entangled=entangled) + return Block(qubits=block_1.qubits, gates=block_1.gates + block_2.gates, name=name) def commute(block_1: Block, block_2: Block): @@ -149,9 +155,9 @@ def initial_block_decomposition(circuit: Circuit): for idx, gate in enumerate(all_gates): if len(gate.qubits) == 2: qubits = gate.qubits - block_gates = find_previous_gates(all_gates[0:idx], qubits) + block_gates = _find_previous_gates(all_gates[0:idx], qubits) block_gates.append(gate) - block_gates += find_successive_gates(all_gates[idx + 1 :], qubits) + block_gates += _find_successive_gates(all_gates[idx + 1 :], qubits) block = Block(qubits=qubits, gates=block_gates) remove_gates(all_gates, block_gates) two_qubit_gates -= 1 @@ -170,10 +176,10 @@ def initial_block_decomposition(circuit: Circuit): second_qubit_block_gates = gates_on_qubit(gatelist=all_gates, qubit=second_qubit) block_gates += second_qubit_block_gates remove_gates(all_gates, second_qubit_block_gates) - block = Block(qubits=(first_qubit, second_qubit), gates=block_gates, entangled=False) + block = Block(qubits=(first_qubit, second_qubit), gates=block_gates) # In case there are no other spare single qubit gates create a block using a following qubit as placeholder else: - block = Block(qubits=(first_qubit, (first_qubit + 1) % circuit.nqubits), gates=block_gates, entangled=False) + block = Block(qubits=(first_qubit, (first_qubit + 1) % circuit.nqubits), gates=block_gates) blocks.append(block) return blocks @@ -198,7 +204,7 @@ def count_2q_gates(gatelist: list): return len([gate for gate in gatelist if len(gate.qubits) == 2]) -def find_successive_gates(gates: list, qubits: tuple): +def _find_successive_gates(gates: list, qubits: tuple): """Return a list containing all gates acting on qubits until a new two qubit gate acting on qubits is found.""" successive_gates = [] for qubit in qubits: @@ -210,7 +216,7 @@ def find_successive_gates(gates: list, qubits: tuple): return successive_gates -def find_previous_gates(gates: list, qubits: tuple): +def _find_previous_gates(gates: list, qubits: tuple): """Return a list containing all gates acting on qubits.""" previous_gates = [] for gate in gates: diff --git a/tests/test_transpilers_blocks.py b/tests/test_transpilers_blocks.py index 2eb244302..ff7bdae4a 100644 --- a/tests/test_transpilers_blocks.py +++ b/tests/test_transpilers_blocks.py @@ -1,7 +1,17 @@ import pytest from qibo import gates -from qibolab.transpilers.blocks import Block, BlockingError +from qibolab.transpilers.blocks import ( + Block, + BlockingError, + _find_previous_gates, + _find_successive_gates, + commute, + count_2q_gates, + fuse_blocks, + gates_on_qubit, + remove_gates, +) def test_count_2q_gates(): @@ -9,8 +19,15 @@ def test_count_2q_gates(): assert block.count_2q_gates() == 2 -def test_add_gate(): - block = Block(qubits=(0, 1), gates=[gates.H(0)], entangled=False) +def test_rename(): + block = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)]) + block.rename("renamed_block") + assert block.name == "renamed_block" + + +def test_add_gate_and_entanglement(): + block = Block(qubits=(0, 1), gates=[gates.H(0)]) + assert block.entangled == False block.add_gate(gates.CZ(0, 1)) assert block.entangled == True assert block.count_2q_gates() == 1 @@ -24,7 +41,7 @@ def test_add_gate_error(): def test_fuse_blocks(): block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)]) - block_2 = Block(qubits=(0, 1), gates=[gates.H(0)], entangled=False) + block_2 = Block(qubits=(0, 1), gates=[gates.H(0)]) fused = fuse_blocks(block_1, block_2) assert fused.qubits == (0, 1) assert fused.entangled == True @@ -38,6 +55,46 @@ def test_fuse_blocks_error(): fused = fuse_blocks(block_1, block_2) +@pytest.mark.parametrize("qubits", [(0, 1), (2, 1)]) +def test_commute_false(qubits): + block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)]) + block_2 = Block(qubits=qubits, gates=[gates.CZ(*qubits)]) + assert commute(block_1, block_2) == False + + +def test_commute_true(): + block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)]) + block_2 = Block(qubits=(2, 3), gates=[gates.CZ(2, 3)]) + assert commute(block_1, block_2) == True + + +def test_gates_on_qubit(): + gatelist = [gates.H(0), gates.H(1), gates.H(2), gates.H(0)] + assert len(gates_on_qubit(gatelist, 0)) == 2 + assert len(gates_on_qubit(gatelist, 1)) == 1 + + +def test_remove_gates(): + gatelist = [gates.H(0), gates.CZ(0, 1), gates.H(2), gates.CZ(0, 2)] + delete_list = [gatelist[0], gatelist[3]] + remove_gates(gatelist, delete_list) + assert len(gatelist) == 2 + assert count_2q_gates(gatelist) == 1 + + +def test_find_previous_gates(): + gatelist = [gates.H(0), gates.H(1), gates.H(2)] + previous_gates = _find_previous_gates(gatelist, (0, 1)) + assert len(previous_gates) == 2 + + +def test_find_successive_gates(): + gatelist = [gates.H(0), gates.CZ(2, 3), gates.H(1), gates.H(2), gates.CZ(2, 1)] + previous_gates = _find_successive_gates(gatelist, (0, 1)) + assert len(previous_gates) == 2 + assert count_2q_gates(previous_gates) == 0 + + # circ = Circuit(3) # circ.add(gates.CZ(0, 1)) # circ.add(gates.CZ(0, 1)) From 5e925b5008e823c658e65590b83d3cf1aed985f1 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Thu, 28 Sep 2023 14:59:43 +0400 Subject: [PATCH 06/13] fixed and completed tests --- src/qibolab/transpilers/blocks.py | 19 +++----- tests/test_transpilers_blocks.py | 75 ++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 18 deletions(-) diff --git a/src/qibolab/transpilers/blocks.py b/src/qibolab/transpilers/blocks.py index d0173d01a..616c78476 100644 --- a/src/qibolab/transpilers/blocks.py +++ b/src/qibolab/transpilers/blocks.py @@ -1,4 +1,4 @@ -from copy import copy, deepcopy +from copy import copy from qibo import Circuit from qibo.config import raise_error @@ -57,13 +57,6 @@ def qubits(self): def qubits(self, qubits): self._qubits = qubits - def info(self): - print("Block Name: ", self.name) - print("Qubits: ", self.qubits) - print("Gates: ", self.gates) - print("Number of two qubits gates: ", self.count_2q_gates()) - print("Entangled: ", self.entangled) - # TODO def kak_decompose(self): # pragma: no cover """Return KAK decomposition of the block. @@ -124,17 +117,17 @@ def block_decomposition(circuit: Circuit, fuse: bool = True): blocks = [] while len(initial_blocks) > 0: first_block = initial_blocks[0] - initial_blocks.remove(first_block) - if len(initial_blocks) > 0: - following_blocks = deepcopy(initial_blocks) - for idx, second_block in enumerate(following_blocks): + remove_list = [first_block] + if len(initial_blocks[1:]) > 0: + for second_block in initial_blocks[1:]: try: first_block = fuse_blocks(first_block, second_block) - initial_blocks.remove(initial_blocks[idx]) + remove_list.append(second_block) except BlockingError: if not commute(first_block, second_block): break blocks.append(first_block) + remove_gates(initial_blocks, remove_list) return blocks diff --git a/tests/test_transpilers_blocks.py b/tests/test_transpilers_blocks.py index ff7bdae4a..929e266f8 100644 --- a/tests/test_transpilers_blocks.py +++ b/tests/test_transpilers_blocks.py @@ -1,15 +1,17 @@ import pytest -from qibo import gates +from qibo import Circuit, gates from qibolab.transpilers.blocks import ( Block, BlockingError, _find_previous_gates, _find_successive_gates, + block_decomposition, commute, count_2q_gates, fuse_blocks, gates_on_qubit, + initial_block_decomposition, remove_gates, ) @@ -95,7 +97,70 @@ def test_find_successive_gates(): assert count_2q_gates(previous_gates) == 0 -# circ = Circuit(3) -# circ.add(gates.CZ(0, 1)) -# circ.add(gates.CZ(0, 1)) -# circ.add(gates.CZ(1, 2)) +def test_initial_block_decomposition(): + circ = Circuit(4) + circ.add(gates.H(1)) + circ.add(gates.H(0)) + circ.add(gates.CZ(0, 1)) + circ.add(gates.CZ(0, 1)) + circ.add(gates.CZ(1, 2)) + circ.add(gates.H(3)) + blocks = initial_block_decomposition(circ) + assert len(blocks) == 4 + assert len(blocks[0].gates) == 3 + assert len(blocks[1].gates) == 1 + assert blocks[2].entangled == True + assert blocks[3].entangled == False + + +def test_block_decomposition_error(): + circ = Circuit(1) + with pytest.raises(BlockingError): + block_decomposition(circ) + + +def test_block_decomposition_no_fuse(): + circ = Circuit(4) + circ.add(gates.H(1)) + circ.add(gates.H(0)) + circ.add(gates.CZ(0, 1)) + circ.add(gates.H(0)) + circ.add(gates.CZ(0, 1)) + circ.add(gates.CZ(1, 2)) + circ.add(gates.H(1)) + circ.add(gates.H(3)) + blocks = block_decomposition(circ, fuse=False) + assert len(blocks) == 4 + assert len(blocks[0].gates) == 4 + assert len(blocks[1].gates) == 1 + assert blocks[2].entangled == True + assert blocks[3].entangled == False + + +def test_block_decomposition(): + circ = Circuit(4) + circ.add(gates.H(1)) # first block + circ.add(gates.H(0)) # first block + circ.add(gates.CZ(0, 1)) # first block + circ.add(gates.H(0)) # first block + circ.add(gates.CZ(0, 1)) # first block + circ.add(gates.CZ(1, 2)) # second block + circ.add(gates.CZ(1, 2)) # second block + circ.add(gates.H(1)) # second block + circ.add(gates.H(3)) # 4 block + circ.add(gates.CZ(0, 1)) # 3 block + circ.add(gates.CZ(0, 1)) # 3 block + circ.add(gates.CZ(2, 3)) # 4 block + circ.add(gates.CZ(0, 1)) # 3 block + blocks = block_decomposition(circ) + assert len(blocks) == 4 + assert blocks[0].count_2q_gates() == 2 + assert len(blocks[0].gates) == 5 + assert blocks[0].qubits == (0, 1) + assert blocks[1].count_2q_gates() == 2 + assert len(blocks[1].gates) == 3 + assert blocks[3].count_2q_gates() == 1 + assert len(blocks[3].gates) == 2 + assert blocks[3].qubits == (2, 3) + assert blocks[2].count_2q_gates() == 3 + assert len(blocks[2].gates) == 3 From dc8da3bec8d599bbf302ce6de80401a8b13aff53 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Thu, 28 Sep 2023 15:41:01 +0400 Subject: [PATCH 07/13] improve coverage --- src/qibolab/transpilers/blocks.py | 17 +++++++++++------ tests/test_transpilers_blocks.py | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/qibolab/transpilers/blocks.py b/src/qibolab/transpilers/blocks.py index 616c78476..0294f3a54 100644 --- a/src/qibolab/transpilers/blocks.py +++ b/src/qibolab/transpilers/blocks.py @@ -1,7 +1,6 @@ from copy import copy from qibo import Circuit -from qibo.config import raise_error from qibo.gates import Gate @@ -20,7 +19,7 @@ class Block: """ def __init__(self, qubits: tuple, gates: list, name: str = None): - self._qubits = qubits + self.qubits = qubits self.gates = gates self.name = name if count_2q_gates(gates) > 0: @@ -63,7 +62,7 @@ def kak_decompose(self): # pragma: no cover This should be done only if the block is entangled and the number of two qubit gates is higher than the number after the decomposition. """ - raise_error(NotImplementedError) + raise NotImplementedError def fuse_blocks(block_1: Block, block_2: Block, name=None): @@ -143,9 +142,10 @@ def initial_block_decomposition(circuit: Circuit): """ blocks = [] all_gates = copy(circuit.queue) - two_qubit_gates = count_2q_gates(all_gates) + two_qubit_gates = count_multi_qubit_gates(all_gates) while two_qubit_gates > 0: for idx, gate in enumerate(all_gates): + print(gate.qubits) if len(gate.qubits) == 2: qubits = gate.qubits block_gates = _find_previous_gates(all_gates[0:idx], qubits) @@ -156,8 +156,8 @@ def initial_block_decomposition(circuit: Circuit): two_qubit_gates -= 1 blocks.append(block) break - if len(gate.qubits) >= 3: - raise_error(BlockingError, "Gates targeting more than 2 qubits are not supported.") + elif len(gate.qubits) > 2: + raise BlockingError("Gates targeting more than 2 qubits are not supported.") # Now we need to deal with the remaining spare single qubit gates while len(all_gates) > 0: first_qubit = all_gates[0].qubits[0] @@ -197,6 +197,11 @@ def count_2q_gates(gatelist: list): return len([gate for gate in gatelist if len(gate.qubits) == 2]) +def count_multi_qubit_gates(gatelist: list): + """Return the number of multi qubit gates in a list of gates.""" + return len([gate for gate in gatelist if len(gate.qubits) >= 2]) + + def _find_successive_gates(gates: list, qubits: tuple): """Return a list containing all gates acting on qubits until a new two qubit gate acting on qubits is found.""" successive_gates = [] diff --git a/tests/test_transpilers_blocks.py b/tests/test_transpilers_blocks.py index 929e266f8..cfa0c0bd3 100644 --- a/tests/test_transpilers_blocks.py +++ b/tests/test_transpilers_blocks.py @@ -9,6 +9,7 @@ block_decomposition, commute, count_2q_gates, + count_multi_qubit_gates, fuse_blocks, gates_on_qubit, initial_block_decomposition, @@ -70,6 +71,11 @@ def test_commute_true(): assert commute(block_1, block_2) == True +def test_count_multi_qubit_gates(): + gatelist = [gates.CZ(0, 1), gates.H(0), gates.TOFFOLI(0, 1, 2)] + assert count_multi_qubit_gates(gatelist) == 2 + + def test_gates_on_qubit(): gatelist = [gates.H(0), gates.H(1), gates.H(2), gates.H(0)] assert len(gates_on_qubit(gatelist, 0)) == 2 @@ -98,19 +104,29 @@ def test_find_successive_gates(): def test_initial_block_decomposition(): - circ = Circuit(4) + circ = Circuit(5) circ.add(gates.H(1)) circ.add(gates.H(0)) circ.add(gates.CZ(0, 1)) circ.add(gates.CZ(0, 1)) circ.add(gates.CZ(1, 2)) circ.add(gates.H(3)) + circ.add(gates.H(4)) blocks = initial_block_decomposition(circ) assert len(blocks) == 4 assert len(blocks[0].gates) == 3 assert len(blocks[1].gates) == 1 assert blocks[2].entangled == True assert blocks[3].entangled == False + assert len(blocks[3].gates) == 2 + + +def test_initial_block_decomposition_error(): + circ = Circuit(3) + circ.add(gates.TOFFOLI(0, 1, 2)) + print(len(circ.queue[0].qubits)) + with pytest.raises(BlockingError): + blocks = initial_block_decomposition(circ) def test_block_decomposition_error(): From 396ed37f14da8f86671a88415b14aa0c3ec4b3c8 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Tue, 3 Oct 2023 15:11:35 +0400 Subject: [PATCH 08/13] suggested changes --- src/qibolab/transpilers/blocks.py | 87 ++++++++++++++----------------- src/qibolab/transpilers/router.py | 32 +++++++++++- tests/test_transpilers_blocks.py | 10 ++-- 3 files changed, 75 insertions(+), 54 deletions(-) diff --git a/src/qibolab/transpilers/blocks.py b/src/qibolab/transpilers/blocks.py index 0294f3a54..01bc71c88 100644 --- a/src/qibolab/transpilers/blocks.py +++ b/src/qibolab/transpilers/blocks.py @@ -1,5 +1,3 @@ -from copy import copy - from qibo import Circuit from qibo.gates import Gate @@ -15,17 +13,19 @@ class Block: qubits (tuple): qubits where the block is acting. gates (list): list of gates that compose the block. name (str): name of the block. - entangled (bool): True if the block entangles the qubits (there is at least one qubit gate). + + Properties: + entangled (bool): True if the block entangles the qubits (there is at least one two qubit gate). """ def __init__(self, qubits: tuple, gates: list, name: str = None): self.qubits = qubits self.gates = gates self.name = name - if count_2q_gates(gates) > 0: - self.entangled = True - else: - self.entangled = False + + @property + def entangled(self): + return self.count_2q_gates() > 0 def rename(self, name): """Rename block""" @@ -40,8 +40,6 @@ def add_gate(self, gate: Gate): ) ) self.gates.append(gate) - if len(gate.qubits) == 2: - self.entangled = True def count_2q_gates(self): """Return the number of two qubit gates in the block.""" @@ -56,6 +54,35 @@ def qubits(self): def qubits(self, qubits): self._qubits = qubits + def fuse(self, block, name: str = None): + """Fuse the current block with a new one, the qubits they are acting on must coincide. + + Args: + block (:class:`qibolab.transpilers.blocks.Block`): block to fuse. + name (str): name of the fused block. + + Return: + fused_block (:class:`qibolab.transpilers.blocks.Block`): fusion of the two input blocks. + """ + if not self.qubits == block.qubits: + raise BlockingError("In order to fuse two blocks their qubits must coincide.") + return Block(qubits=self.qubits, gates=self.gates + block.gates, name=name) + + def commute(self, block): + """Check if a block commutes with the current one. + + Args: + block (:class:`qibolab.transpilers.blocks.Block`): block to check commutation. + + Return: + True if the two blocks don't share any qubit. + False otherwise. + """ + for qubit in self.qubits: + if qubit in block.qubits: + return False + return True + # TODO def kak_decompose(self): # pragma: no cover """Return KAK decomposition of the block. @@ -65,39 +92,6 @@ def kak_decompose(self): # pragma: no cover raise NotImplementedError -def fuse_blocks(block_1: Block, block_2: Block, name=None): - """Fuse two gate blocks, the qubits they are acting on must coincide. - - Args: - block_1 (.transpilers.blocks.Block): first block. - block_2 (.transpilers.blocks.Block): second block. - name (str): name of the fused block. - - Return: - fused_block (.transpilers.blocks.Block): fusion of the two input blocks. - """ - if not block_1.qubits == block_2.qubits: - raise BlockingError("In order to fuse two blocks their qubits must coincide.") - return Block(qubits=block_1.qubits, gates=block_1.gates + block_2.gates, name=name) - - -def commute(block_1: Block, block_2: Block): - """Check if two blocks commute (share qubits). - - Args: - block_1 (.transpilers.blocks.Block): first block. - block_2 (.transpilers.blocks.Block): second block. - - Return: - True if the two blocks don't share any qubit. - False otherwise. - """ - for qubit in block_1.qubits: - if qubit in block_2.qubits: - return False - return True - - def block_decomposition(circuit: Circuit, fuse: bool = True): """Decompose a circuit into blocks of gates acting on two qubits. @@ -120,10 +114,10 @@ def block_decomposition(circuit: Circuit, fuse: bool = True): if len(initial_blocks[1:]) > 0: for second_block in initial_blocks[1:]: try: - first_block = fuse_blocks(first_block, second_block) + first_block = first_block.fuse(second_block) remove_list.append(second_block) except BlockingError: - if not commute(first_block, second_block): + if not first_block.commute(second_block): break blocks.append(first_block) remove_gates(initial_blocks, remove_list) @@ -141,16 +135,15 @@ def initial_block_decomposition(circuit: Circuit): blocks (list): list of blocks that act on two qubits. """ blocks = [] - all_gates = copy(circuit.queue) + all_gates = list(circuit.queue) two_qubit_gates = count_multi_qubit_gates(all_gates) while two_qubit_gates > 0: for idx, gate in enumerate(all_gates): - print(gate.qubits) if len(gate.qubits) == 2: qubits = gate.qubits block_gates = _find_previous_gates(all_gates[0:idx], qubits) block_gates.append(gate) - block_gates += _find_successive_gates(all_gates[idx + 1 :], qubits) + block_gates.extend(_find_successive_gates(all_gates[idx + 1 :], qubits)) block = Block(qubits=qubits, gates=block_gates) remove_gates(all_gates, block_gates) two_qubit_gates -= 1 diff --git a/src/qibolab/transpilers/router.py b/src/qibolab/transpilers/router.py index 2f3452100..7ba2b44bc 100644 --- a/src/qibolab/transpilers/router.py +++ b/src/qibolab/transpilers/router.py @@ -307,8 +307,38 @@ def update_qubit_map(self): self._qubit_map[value] = old_mapping[key] +def create_dag(circuit): + """Create direct acyclic graph (dag) of the circuit based on two qubit gates commutativity relations. + + Args: + circuit (qibo.models.Circuit): circuit to be transformed into dag. + + Returns: + cleaned_dag (nx.DiGraph): dag of the circuit. + """ + circuit = create_circuit_repr(circuit) + dag = nx.DiGraph() + dag.add_nodes_from(list(i for i in range(len(circuit)))) + # Find all successors + connectivity_list = [] + for idx, gate in enumerate(circuit): + for next_idx, next_gate in enumerate(circuit[idx + 1 :]): + for qubit in gate: + if qubit in next_gate: + connectivity_list.append((idx, next_idx + idx + 1)) + dag.add_edges_from(connectivity_list) + return remove_redundant_connections(dag) + + +def remove_redundant_connections(dag): + """Remove redundant connection from a DAG unsing transitive reduction.""" + new_dag = nx.DiGraph() + transitive_reduction = nx.transitive_reduction(dag) + new_dag.add_edges_from(transitive_reduction.edges) + return new_dag + + class Sabre(Router): - # TODO: requires block circuit """ Routing algorithm proposed in https://doi.org/10.48550/arXiv.1809.02573 diff --git a/tests/test_transpilers_blocks.py b/tests/test_transpilers_blocks.py index cfa0c0bd3..c5722b4cf 100644 --- a/tests/test_transpilers_blocks.py +++ b/tests/test_transpilers_blocks.py @@ -7,10 +7,8 @@ _find_previous_gates, _find_successive_gates, block_decomposition, - commute, count_2q_gates, count_multi_qubit_gates, - fuse_blocks, gates_on_qubit, initial_block_decomposition, remove_gates, @@ -45,7 +43,7 @@ def test_add_gate_error(): def test_fuse_blocks(): block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)]) block_2 = Block(qubits=(0, 1), gates=[gates.H(0)]) - fused = fuse_blocks(block_1, block_2) + fused = block_1.fuse(block_2) assert fused.qubits == (0, 1) assert fused.entangled == True assert fused.count_2q_gates() == 1 @@ -55,20 +53,20 @@ def test_fuse_blocks_error(): block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)]) block_2 = Block(qubits=(1, 2), gates=[gates.CZ(1, 2)]) with pytest.raises(BlockingError): - fused = fuse_blocks(block_1, block_2) + fused = block_1.fuse(block_2) @pytest.mark.parametrize("qubits", [(0, 1), (2, 1)]) def test_commute_false(qubits): block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)]) block_2 = Block(qubits=qubits, gates=[gates.CZ(*qubits)]) - assert commute(block_1, block_2) == False + assert block_1.commute(block_2) == False def test_commute_true(): block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)]) block_2 = Block(qubits=(2, 3), gates=[gates.CZ(2, 3)]) - assert commute(block_1, block_2) == True + assert block_1.commute(block_2) == True def test_count_multi_qubit_gates(): From 60bf501a6c2c2e0d20b0724c31d5e6d115df63b2 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Tue, 3 Oct 2023 15:34:25 +0400 Subject: [PATCH 09/13] corrected error with add gate --- src/qibolab/transpilers/blocks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qibolab/transpilers/blocks.py b/src/qibolab/transpilers/blocks.py index 01bc71c88..c1442be98 100644 --- a/src/qibolab/transpilers/blocks.py +++ b/src/qibolab/transpilers/blocks.py @@ -33,7 +33,7 @@ def rename(self, name): def add_gate(self, gate: Gate): """Add a new gate to the block.""" - if not gate.qubits == self.qubits: + if not set(gate.qubits).issubset(self.qubits): raise BlockingError( "Gate acting on qubits {} can't be added to block acting on qubits {}.".format( gate.qubits, self._qubits @@ -54,7 +54,7 @@ def qubits(self): def qubits(self, qubits): self._qubits = qubits - def fuse(self, block, name: str = None): + def fuse(self, block: "Block", name: str = None): """Fuse the current block with a new one, the qubits they are acting on must coincide. Args: @@ -68,7 +68,7 @@ def fuse(self, block, name: str = None): raise BlockingError("In order to fuse two blocks their qubits must coincide.") return Block(qubits=self.qubits, gates=self.gates + block.gates, name=name) - def commute(self, block): + def commute(self, block: "Block"): """Check if a block commutes with the current one. Args: From d0edfe71eac5d8e1df5d7c2314cc87363e95b2e4 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Tue, 3 Oct 2023 15:48:40 +0400 Subject: [PATCH 10/13] typo error --- src/qibolab/transpilers/router.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/src/qibolab/transpilers/router.py b/src/qibolab/transpilers/router.py index 7ba2b44bc..f51860f52 100644 --- a/src/qibolab/transpilers/router.py +++ b/src/qibolab/transpilers/router.py @@ -307,37 +307,6 @@ def update_qubit_map(self): self._qubit_map[value] = old_mapping[key] -def create_dag(circuit): - """Create direct acyclic graph (dag) of the circuit based on two qubit gates commutativity relations. - - Args: - circuit (qibo.models.Circuit): circuit to be transformed into dag. - - Returns: - cleaned_dag (nx.DiGraph): dag of the circuit. - """ - circuit = create_circuit_repr(circuit) - dag = nx.DiGraph() - dag.add_nodes_from(list(i for i in range(len(circuit)))) - # Find all successors - connectivity_list = [] - for idx, gate in enumerate(circuit): - for next_idx, next_gate in enumerate(circuit[idx + 1 :]): - for qubit in gate: - if qubit in next_gate: - connectivity_list.append((idx, next_idx + idx + 1)) - dag.add_edges_from(connectivity_list) - return remove_redundant_connections(dag) - - -def remove_redundant_connections(dag): - """Remove redundant connection from a DAG unsing transitive reduction.""" - new_dag = nx.DiGraph() - transitive_reduction = nx.transitive_reduction(dag) - new_dag.add_edges_from(transitive_reduction.edges) - return new_dag - - class Sabre(Router): """ Routing algorithm proposed in From ca53468da1afcf6eef4634c56012234ba9901ea1 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Wed, 4 Oct 2023 15:41:49 +0400 Subject: [PATCH 11/13] improve commute function and future enhancements proposal --- src/qibolab/transpilers/blocks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qibolab/transpilers/blocks.py b/src/qibolab/transpilers/blocks.py index c1442be98..bb97cb3b6 100644 --- a/src/qibolab/transpilers/blocks.py +++ b/src/qibolab/transpilers/blocks.py @@ -68,6 +68,7 @@ def fuse(self, block: "Block", name: str = None): raise BlockingError("In order to fuse two blocks their qubits must coincide.") return Block(qubits=self.qubits, gates=self.gates + block.gates, name=name) + # TODO: use real QM properties to check commutation def commute(self, block: "Block"): """Check if a block commutes with the current one. @@ -78,9 +79,8 @@ def commute(self, block: "Block"): True if the two blocks don't share any qubit. False otherwise. """ - for qubit in self.qubits: - if qubit in block.qubits: - return False + if len(set(self.qubits).intersection(block.qubits)) > 0: + return False return True # TODO From 84071fac05f1c5db0cacabd627d1fe03a6d2cb85 Mon Sep 17 00:00:00 2001 From: Simone-Bordoni <101353773+Simone-Bordoni@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:36:59 +0400 Subject: [PATCH 12/13] Update src/qibolab/transpilers/blocks.py Co-authored-by: BrunoLiegiBastonLiegi <45011234+BrunoLiegiBastonLiegi@users.noreply.github.com> --- src/qibolab/transpilers/blocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibolab/transpilers/blocks.py b/src/qibolab/transpilers/blocks.py index bb97cb3b6..40067748c 100644 --- a/src/qibolab/transpilers/blocks.py +++ b/src/qibolab/transpilers/blocks.py @@ -202,7 +202,7 @@ def _find_successive_gates(gates: list, qubits: tuple): for gate in gates: if (len(gate.qubits) == 1) and (gate.qubits[0] == qubit): successive_gates.append(gate) - if (len(gate.qubits) == 2) and (qubit in gate.qubits): + elif (len(gate.qubits) == 2) and (qubit in gate.qubits): break return successive_gates From d5abf08dd5b527b767d34082bac5847a66155264 Mon Sep 17 00:00:00 2001 From: Andrea Papaluca Date: Wed, 4 Oct 2023 17:37:38 +0400 Subject: [PATCH 13/13] improved tests --- tests/test_transpilers_blocks.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/tests/test_transpilers_blocks.py b/tests/test_transpilers_blocks.py index c5722b4cf..26929fe19 100644 --- a/tests/test_transpilers_blocks.py +++ b/tests/test_transpilers_blocks.py @@ -7,7 +7,6 @@ _find_previous_gates, _find_successive_gates, block_decomposition, - count_2q_gates, count_multi_qubit_gates, gates_on_qubit, initial_block_decomposition, @@ -15,6 +14,13 @@ ) +def assert_gates_equality(gates_1: list, gates_2: list): + """Check that the gates are the same.""" + for g_1, g_2 in zip(gates_1, gates_2): + assert g_1.qubits == g_2.qubits + assert g_1.__class__ == g_2.__class__ + + def test_count_2q_gates(): block = Block(qubits=(0, 1), gates=[gates.CZ(0, 1), gates.CZ(0, 1), gates.H(0)]) assert block.count_2q_gates() == 2 @@ -44,9 +50,7 @@ def test_fuse_blocks(): block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)]) block_2 = Block(qubits=(0, 1), gates=[gates.H(0)]) fused = block_1.fuse(block_2) - assert fused.qubits == (0, 1) - assert fused.entangled == True - assert fused.count_2q_gates() == 1 + assert_gates_equality(fused.gates, block_1.gates + block_2.gates) def test_fuse_blocks_error(): @@ -76,29 +80,29 @@ def test_count_multi_qubit_gates(): def test_gates_on_qubit(): gatelist = [gates.H(0), gates.H(1), gates.H(2), gates.H(0)] - assert len(gates_on_qubit(gatelist, 0)) == 2 - assert len(gates_on_qubit(gatelist, 1)) == 1 + assert_gates_equality(gates_on_qubit(gatelist, 0), [gatelist[0], gatelist[-1]]) + assert_gates_equality(gates_on_qubit(gatelist, 1), [gatelist[1]]) + assert_gates_equality(gates_on_qubit(gatelist, 2), [gatelist[2]]) def test_remove_gates(): gatelist = [gates.H(0), gates.CZ(0, 1), gates.H(2), gates.CZ(0, 2)] + remaining = [gates.CZ(0, 1), gates.H(2)] delete_list = [gatelist[0], gatelist[3]] remove_gates(gatelist, delete_list) - assert len(gatelist) == 2 - assert count_2q_gates(gatelist) == 1 + assert_gates_equality(gatelist, remaining) def test_find_previous_gates(): gatelist = [gates.H(0), gates.H(1), gates.H(2)] previous_gates = _find_previous_gates(gatelist, (0, 1)) - assert len(previous_gates) == 2 + assert_gates_equality(previous_gates, gatelist[:2]) def test_find_successive_gates(): gatelist = [gates.H(0), gates.CZ(2, 3), gates.H(1), gates.H(2), gates.CZ(2, 1)] - previous_gates = _find_successive_gates(gatelist, (0, 1)) - assert len(previous_gates) == 2 - assert count_2q_gates(previous_gates) == 0 + successive_gates = _find_successive_gates(gatelist, (0, 1)) + assert_gates_equality(successive_gates, [gatelist[0], gatelist[2]]) def test_initial_block_decomposition(): @@ -111,6 +115,7 @@ def test_initial_block_decomposition(): circ.add(gates.H(3)) circ.add(gates.H(4)) blocks = initial_block_decomposition(circ) + assert_gates_equality(blocks[0].gates, [gates.H(1), gates.H(0), gates.CZ(0, 1)]) assert len(blocks) == 4 assert len(blocks[0].gates) == 3 assert len(blocks[1].gates) == 1 @@ -144,6 +149,7 @@ def test_block_decomposition_no_fuse(): circ.add(gates.H(1)) circ.add(gates.H(3)) blocks = block_decomposition(circ, fuse=False) + assert_gates_equality(blocks[0].gates, [gates.H(1), gates.H(0), gates.CZ(0, 1), gates.H(0)]) assert len(blocks) == 4 assert len(blocks[0].gates) == 4 assert len(blocks[1].gates) == 1 @@ -167,6 +173,7 @@ def test_block_decomposition(): circ.add(gates.CZ(2, 3)) # 4 block circ.add(gates.CZ(0, 1)) # 3 block blocks = block_decomposition(circ) + assert_gates_equality(blocks[0].gates, [gates.H(1), gates.H(0), gates.CZ(0, 1), gates.H(0), gates.CZ(0, 1)]) assert len(blocks) == 4 assert blocks[0].count_2q_gates() == 2 assert len(blocks[0].gates) == 5