From 415a49bda02b273fcf0dd7cf26c0720cb4f91e3a Mon Sep 17 00:00:00 2001 From: David Winderl Date: Sat, 21 Dec 2024 14:19:02 +0100 Subject: [PATCH 1/8] Implement and test basic clifford brute force algorithm Signed-off-by: David Winderl --- pauliopt/clifford/tableau.py | 24 ++- pauliopt/clifford/tableau_synthesis.py | 174 +++++++++++++++------- tests/clifford/test_clifford_synthesis.py | 31 +++- 3 files changed, 167 insertions(+), 62 deletions(-) diff --git a/pauliopt/clifford/tableau.py b/pauliopt/clifford/tableau.py index a3698eb9..a24bb618 100644 --- a/pauliopt/clifford/tableau.py +++ b/pauliopt/clifford/tableau.py @@ -132,8 +132,8 @@ def from_tableau(tableau, signs): """ n_qubits = tableau.shape[0] // 2 if not ( - tableau.shape == (2 * n_qubits, 2 * n_qubits) - and signs.shape == (2 * n_qubits,) + tableau.shape == (2 * n_qubits, 2 * n_qubits) + and signs.shape == (2 * n_qubits,) ): raise ValueError( "Tableau and signs must have shape " @@ -198,10 +198,24 @@ def z_out(self, row, col): col (int): Column index. """ return ( - self.tableau[row + self.n_qubits, col] - + 2 * self.tableau[row + self.n_qubits, col + self.n_qubits] + self.tableau[row + self.n_qubits, col] + + 2 * self.tableau[row + self.n_qubits, col + self.n_qubits] ) + @property + def x_matrix(self): + """ + Binary matrix representing the X-Basis of the clifford tableau. + :return: + """ + x_matrx = np.zeros((self.n_qubits, self.n_qubits), dtype=int) + for i in range(self.n_qubits): + for j in range(self.n_qubits): + x_matrx[i, j] = ( + self.tableau[i, j] + 2 * self.tableau[i, j + self.n_qubits] + ) + return x_matrx + def _xor_row(self, i, j): """ XOR the value of row j to row i and adjust the signs accordingly. @@ -363,7 +377,7 @@ def apply(self, other: "CliffordTableau"): for k in range(2 * self.n_qubits): row2 = other.tableau[k] x2 = other.tableau[k, : self.n_qubits] - z2 = other.tableau[k, self.n_qubits :] + z2 = other.tableau[k, self.n_qubits:] # Adding a factor of i for each Y in the image of an operator under the # first operation, since Y=iXZ diff --git a/pauliopt/clifford/tableau_synthesis.py b/pauliopt/clifford/tableau_synthesis.py index e336d7be..bc669130 100644 --- a/pauliopt/clifford/tableau_synthesis.py +++ b/pauliopt/clifford/tableau_synthesis.py @@ -1,4 +1,7 @@ +from typing import List, Tuple, Optional + import networkx as nx +import numpy as np from networkx.algorithms.approximation import steiner_tree from pauliopt.circuits import Circuit @@ -47,7 +50,7 @@ def pick_pivot(G, remaining: "CliffordTableau", possible_swaps, include_swaps): has_cutting_swappable = any([not is_cutting(i, G) for i in possible_swaps]) for col in G.nodes: if not is_cutting(col, G) or ( - include_swaps and has_cutting_swappable and col in possible_swaps + include_swaps and has_cutting_swappable and col in possible_swaps ): scores.append((col, col, heurisitc_fkt(col, G, remaining))) assert len(scores) > 0 @@ -91,14 +94,14 @@ def relabel_graph_inplace(G, parent, child): def compute_steiner_tree( - root: int, - nodes: [int], - sub_graph: nx.Graph, - include_swaps=False, - lookup=None, - swappable_nodes=None, - permutation=None, - n_qubits=None, + root: int, + nodes: [int], + sub_graph: nx.Graph, + include_swaps=False, + lookup=None, + swappable_nodes=None, + permutation=None, + n_qubits=None, ): """ Compute the steiner tree of the sub_graph with the given nodes. @@ -143,10 +146,10 @@ def compute_steiner_tree( # if the parent is zero and the child is one and both are swappable # then swap them if ( - lookup[parent] == 0 - and lookup[child] == 1 - and child in swappable_nodes - and parent in swappable_nodes + lookup[parent] == 0 + and lookup[child] == 1 + and child in swappable_nodes + and parent in swappable_nodes ): relabel_graph_inplace(steiner_stree, parent, child) relabel_graph_inplace(sub_graph, parent, child) @@ -165,7 +168,7 @@ def compute_steiner_tree( return list(reversed(list(traversal))) -def sanitize_z(row, row_z, remaining, apply): +def sanitize_z(pivot_row, pivot_column, row_z, remaining, apply): """ Sanitization process for the stabilizer part. @@ -174,21 +177,21 @@ def sanitize_z(row, row_z, remaining, apply): - If the z_out is X (=1), then apply H - :param row: The row of the clifford + :param pivot_row: The row of the clifford :param row_z: The row of the clifford for the stabilizer part :param remaining: The remaining clifford :param apply: The function to apply a gate """ for column in row_z: - if remaining.z_out(row, column) == 3: + if remaining.z_out(pivot_row, column) == 3: apply("S", (column,)) - if remaining.z_out(row, column) == 1: + if remaining.z_out(pivot_row, column) == 1: apply("H", (column,)) # caveat for the pivot - if remaining.x_out(row, row) == 3: - apply("S", (row,)) + if remaining.x_out(pivot_row, pivot_column) == 3: + apply("S", (pivot_column,)) def sanitize_field_x(row, row_x, remaining, apply): @@ -213,15 +216,16 @@ def sanitize_field_x(row, row_x, remaining, apply): def remove_interactions( - pivot, - row, - sub_graph, - remaining, - apply, - basis, - include_swaps=False, - swappable_nodes=None, - permutation=None, + pivot_col, + pivot_row, + row, + sub_graph, + remaining, + apply, + basis, + include_swaps=False, + swappable_nodes=None, + permutation=None, ): """ Remove the interactions of the destabilizer/stabilizer part. @@ -229,7 +233,7 @@ def remove_interactions( Include swaps requires swappable_nodes, permutation and include_swaps to be set. - :param pivot: The pivot of the clifford + :param pivot_row: The pivot of the clifford :param row: The specific row of the clifford :param sub_graph: The graph of the topology :param remaining: The remaining clifford @@ -240,10 +244,10 @@ def remove_interactions( :param include_swaps: Whether to include swaps in the steiner tree """ - row = list(set([pivot] + row)) - lookup = {node: int(remaining.x_out(pivot, node) != 0) for node in sub_graph.nodes} + row = list(set([pivot_col] + row)) + lookup = {node: int(remaining.x_out(pivot_row, node) != 0) for node in sub_graph.nodes} traversal = compute_steiner_tree( - pivot, + pivot_col, row, sub_graph, include_swaps=include_swaps, @@ -254,14 +258,14 @@ def remove_interactions( ) if basis == "x": for parent, child in traversal: - if remaining.x_out(pivot, parent) == 0: + if remaining.x_out(pivot_row, parent) == 0: apply("CNOT", (child, parent)) for parent, child in traversal: apply("CNOT", (parent, child)) elif basis == "z": for parent, child in traversal: - if remaining.z_out(pivot, parent) == 0: + if remaining.z_out(pivot_row, parent) == 0: apply("CNOT", (parent, child)) for parent, child in traversal: @@ -269,18 +273,19 @@ def remove_interactions( def steiner_reduce_column( - pivot, - sub_graph, - remaining, - apply, - swappable_nodes=None, - permutation=None, - include_swaps=False, + pivot_col, + pivot_row, + sub_graph, + remaining, + apply, + swappable_nodes=None, + permutation=None, + include_swaps=False, ): """ Steiner reduce a column of the clifford. - :param pivot: The pivot of the clifford + :param pivot_row: The pivot of the clifford :param sub_graph: The graph of the topology :param remaining: The remaining clifford :param apply: The function to apply a gate @@ -289,12 +294,12 @@ def steiner_reduce_column( :param include_swaps: Whether to include swaps in the steiner tree """ # 2. Sanitize the destabilizer row - row_x = [col for col in sub_graph.nodes if remaining.x_out(pivot, col) != 0] - sanitize_field_x(pivot, row_x, remaining, apply) - + row_x = [col for col in sub_graph.nodes if remaining.x_out(pivot_row, col) != 0] + sanitize_field_x(pivot_row, row_x, remaining, apply) # 3. Remove the interactions from the destabilizer row remove_interactions( - pivot, + pivot_col, + pivot_row, row_x, sub_graph, remaining, @@ -304,14 +309,14 @@ def steiner_reduce_column( swappable_nodes=swappable_nodes, permutation=permutation, ) - # 4. Sanitize the stabilizer row - row_z = [row for row in sub_graph.nodes if remaining.z_out(pivot, row) != 0] - sanitize_z(pivot, row_z, remaining, apply) + row_z = [row for row in sub_graph.nodes if remaining.z_out(pivot_row, row) != 0] + sanitize_z(pivot_row, pivot_col, row_z, remaining, apply) # 5. Remove the interactions from the stabilizer row remove_interactions( - pivot, + pivot_col, + pivot_row, row_z, sub_graph, remaining, @@ -324,8 +329,8 @@ def steiner_reduce_column( # ensure that the pivots are in ZX basis # (this is provided by the construction of a clifford) - assert remaining.x_out(pivot, pivot) == 1 - assert remaining.z_out(pivot, pivot) == 2 + assert remaining.x_out(pivot_row, pivot_col) == 1 + assert remaining.z_out(pivot_row, pivot_col) == 2 def get_non_cutting_vertex(G, pivot_col, swappable_nodes): @@ -340,6 +345,67 @@ def get_non_cutting_vertex(G, pivot_col, swappable_nodes): return non_cutting +def synthesize_tableau_permutation( + tableau: CliffordTableau, + topo: Topology, + pivot_permutation: List[Tuple[int, int]] +) -> Optional[Circuit]: + qc = Circuit(tableau.n_qubits) + + remaining = tableau.inverse() + swappable_nodes = list(range(tableau.n_qubits)) + + G = topo.to_nx + for e1, e2 in G.edges: + G[e1][e2]["weight"] = 0 + + def apply(gate_name: str, gate_data: tuple): + if gate_name == "CNOT": + remaining.append_cnot(gate_data[0], gate_data[1]) + qc.add_gate(CX(gate_data[0], gate_data[1])) + if gate_data[0] in swappable_nodes: + swappable_nodes.remove(gate_data[0]) + if gate_data[1] in swappable_nodes: + swappable_nodes.remove(gate_data[1]) + G[gate_data[0]][gate_data[1]]["weight"] = 2 + elif gate_name == "H": + remaining.append_h(gate_data[0]) + qc.add_gate(H(gate_data[0])) + elif gate_name == "S": + remaining.append_s(gate_data[0]) + qc.add_gate(S(gate_data[0])) + else: + raise Exception("Unknown Gate") + + for pivot_col, pivot_row in pivot_permutation: + if is_cutting(pivot_col, G): + return None + + steiner_reduce_column(pivot_col, pivot_row, G, remaining, apply) + + if pivot_col in swappable_nodes: + swappable_nodes.remove(pivot_col) + G.remove_node(pivot_col) + + final_permutation = np.argmax(remaining.x_matrix, axis=1) + qc.final_permutation = final_permutation + signs_copy_z = remaining.signs[remaining.n_qubits: 2 * remaining.n_qubits].copy() + + for col in range(remaining.n_qubits): + if signs_copy_z[col] != 0: + apply("H", (final_permutation[col],)) + apply("S", (final_permutation[col],)) + apply("S", (final_permutation[col],)) + apply("H", (final_permutation[col],)) + + for col in range(remaining.n_qubits): + if remaining.signs[col] != 0: + apply("S", (final_permutation[col],)) + apply("S", (final_permutation[col],)) + + return qc + + def synthesize_tableau(tableau: CliffordTableau, topo: Topology, include_swaps=True): """ Architecture aware synthesis of a Clifford tableau. @@ -400,14 +466,14 @@ def apply(gate_name: str, gate_data: tuple): ) steiner_reduce_column( - pivot_col, G, remaining, apply, swappable_nodes, permutation, include_swaps + pivot_col, pivot_col, G, remaining, apply, swappable_nodes, permutation, include_swaps ) if pivot_col in swappable_nodes: swappable_nodes.remove(pivot_col) G.remove_node(pivot_col) - signs_copy_z = remaining.signs[tableau.n_qubits : 2 * tableau.n_qubits].copy() + signs_copy_z = remaining.signs[tableau.n_qubits: 2 * tableau.n_qubits].copy() for col in range(tableau.n_qubits): if signs_copy_z[col] != 0: apply("H", (col,)) diff --git a/tests/clifford/test_clifford_synthesis.py b/tests/clifford/test_clifford_synthesis.py index 4f19d67a..44035653 100644 --- a/tests/clifford/test_clifford_synthesis.py +++ b/tests/clifford/test_clifford_synthesis.py @@ -1,13 +1,19 @@ +import itertools import unittest from parameterized import parameterized -from qiskit import QuantumCircuit +from qiskit.circuit.library import Permutation from pauliopt.clifford.tableau import CliffordTableau -from pauliopt.clifford.tableau_synthesis import synthesize_tableau +from pauliopt.clifford.tableau_synthesis import synthesize_tableau, synthesize_tableau_permutation +from pauliopt.topologies import Topology from tests.clifford.utils import tableau_from_circuit from tests.utils import verify_equality, random_hscx_circuit -from pauliopt.topologies import Topology + + +def enumerate_row_col_permutations(n): + for perm in itertools.permutations(range(n)): + yield list(zip(range(n), perm)) class TestTableauSynthesis(unittest.TestCase): @@ -37,3 +43,22 @@ def test_clifford_synthesis(self, _, n_qubits, n_gates, topo, include_swaps): verify_equality(circuit.to_qiskit(), qc), "The Synthesized circuit does not equal to original", ) + + @parameterized.expand( + [ + ("line_3", 3, 1000, Topology.line(3)), + ] + ) + def test_clifford_permutation_synthesis(self, _, n_qubits, n_gates, topo): + circuit = random_hscx_circuit(nr_qubits=n_qubits, nr_gates=n_gates) + for permutation in enumerate_row_col_permutations(n_qubits): + ct = CliffordTableau(n_qubits) + ct = tableau_from_circuit(ct, circuit.copy()) + qc = synthesize_tableau_permutation(ct, topo, permutation) + qc.final_permutation = [source for source, target in sorted(permutation, key=lambda x: x[1])] + qc = qc.to_qiskit() + + self.assertTrue( + verify_equality(circuit.to_qiskit(), qc), + "The Synthesized circuit does not equal to original", + ) From 6fb16ae30cbf09c19f72dc9e6c52a3d701a13809 Mon Sep 17 00:00:00 2001 From: David Winderl Date: Sat, 21 Dec 2024 22:30:40 +0100 Subject: [PATCH 2/8] add zmatrix property --- pauliopt/clifford/tableau.py | 16 +++++++++++++--- pauliopt/clifford/tableau_synthesis.py | 7 ------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pauliopt/clifford/tableau.py b/pauliopt/clifford/tableau.py index a24bb618..6cdc521b 100644 --- a/pauliopt/clifford/tableau.py +++ b/pauliopt/clifford/tableau.py @@ -211,9 +211,19 @@ def x_matrix(self): x_matrx = np.zeros((self.n_qubits, self.n_qubits), dtype=int) for i in range(self.n_qubits): for j in range(self.n_qubits): - x_matrx[i, j] = ( - self.tableau[i, j] + 2 * self.tableau[i, j + self.n_qubits] - ) + x_matrx[i, j] = self.x_out(i, j) + return x_matrx + + @property + def z_matrix(self): + """ + Binary matrix representing the X-Basis of the clifford tableau. + :return: + """ + x_matrx = np.zeros((self.n_qubits, self.n_qubits), dtype=int) + for i in range(self.n_qubits): + for j in range(self.n_qubits): + x_matrx[i, j] = self.z_out(i, j) return x_matrx def _xor_row(self, i, j): diff --git a/pauliopt/clifford/tableau_synthesis.py b/pauliopt/clifford/tableau_synthesis.py index bc669130..efe877fd 100644 --- a/pauliopt/clifford/tableau_synthesis.py +++ b/pauliopt/clifford/tableau_synthesis.py @@ -353,7 +353,6 @@ def synthesize_tableau_permutation( qc = Circuit(tableau.n_qubits) remaining = tableau.inverse() - swappable_nodes = list(range(tableau.n_qubits)) G = topo.to_nx for e1, e2 in G.edges: @@ -363,10 +362,6 @@ def apply(gate_name: str, gate_data: tuple): if gate_name == "CNOT": remaining.append_cnot(gate_data[0], gate_data[1]) qc.add_gate(CX(gate_data[0], gate_data[1])) - if gate_data[0] in swappable_nodes: - swappable_nodes.remove(gate_data[0]) - if gate_data[1] in swappable_nodes: - swappable_nodes.remove(gate_data[1]) G[gate_data[0]][gate_data[1]]["weight"] = 2 elif gate_name == "H": remaining.append_h(gate_data[0]) @@ -383,8 +378,6 @@ def apply(gate_name: str, gate_data: tuple): steiner_reduce_column(pivot_col, pivot_row, G, remaining, apply) - if pivot_col in swappable_nodes: - swappable_nodes.remove(pivot_col) G.remove_node(pivot_col) final_permutation = np.argmax(remaining.x_matrix, axis=1) From cbe16f5944c006d77a4e72cf733068aba94ef99e Mon Sep 17 00:00:00 2001 From: David Winderl Date: Sun, 22 Dec 2024 14:59:20 +0100 Subject: [PATCH 3/8] Adapt algorithm to allow callback for picking pivots Signed-off-by: David Winderl --- pauliopt/clifford/tableau_synthesis.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pauliopt/clifford/tableau_synthesis.py b/pauliopt/clifford/tableau_synthesis.py index efe877fd..145657cd 100644 --- a/pauliopt/clifford/tableau_synthesis.py +++ b/pauliopt/clifford/tableau_synthesis.py @@ -399,7 +399,7 @@ def apply(gate_name: str, gate_data: tuple): return qc -def synthesize_tableau(tableau: CliffordTableau, topo: Topology, include_swaps=True): +def synthesize_tableau(tableau: CliffordTableau, topo: Topology, include_swaps=True, pick_pivot_callback=None): """ Architecture aware synthesis of a Clifford tableau. This is the implementation of the algorithm described in Winderl et. al. [1] @@ -417,6 +417,8 @@ def synthesize_tableau(tableau: CliffordTableau, topo: Topology, include_swaps=T """ + if pick_pivot_callback is None: + pick_pivot_callback = pick_pivot qc = Circuit(tableau.n_qubits) remaining = tableau.inverse() @@ -447,7 +449,7 @@ def apply(gate_name: str, gate_data: tuple): while G.nodes: # 1. Pick a pivot - pivot_col, pivot_row = pick_pivot(G, remaining, swappable_nodes, include_swaps) + pivot_col, pivot_row = pick_pivot_callback(G, remaining, swappable_nodes, include_swaps) if is_cutting(pivot_col, G) and include_swaps: non_cutting = get_non_cutting_vertex(G, pivot_col, swappable_nodes) From f09bc66496945da1caf9bdfa7b1e43b5e0eff099 Mon Sep 17 00:00:00 2001 From: David Winderl Date: Sun, 22 Dec 2024 16:43:55 +0100 Subject: [PATCH 4/8] Add in perm row col implementation Signed-off-by: David Winderl --- pauliopt/clifford/tableau_synthesis.py | 99 +++++++++++++++++++++++ tests/clifford/test_clifford_synthesis.py | 30 ++++++- 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/pauliopt/clifford/tableau_synthesis.py b/pauliopt/clifford/tableau_synthesis.py index 145657cd..2eafa14b 100644 --- a/pauliopt/clifford/tableau_synthesis.py +++ b/pauliopt/clifford/tableau_synthesis.py @@ -34,6 +34,50 @@ def heurisitc_fkt(row, G, remaining: CliffordTableau): return dist_x + dist_z +def pick_row(G, remaining: "CliffordTableau", remaining_rows, choice_fn=min): + scores = [] + for row in remaining_rows: + row_x = [1 for col in G.nodes if remaining.x_out(row, col) != 0] + row_z = [1 for col in G.nodes if remaining.z_out(row, col) != 0] + dist_x = sum(row_x) + dist_z = sum(row_z) + scores.append((row, dist_x + dist_z)) + + return choice_fn(scores, key=lambda x: x[1])[0] + + +def pick_col( + G, + remaining: "CliffordTableau", + pivot_row, + choice_fn=min, +): + scores = [] + for col in G.nodes: + if not is_cutting(col, G): + row_x = [ + nx.shortest_path_length(G, source=col, target=other_col) + for other_col in G.nodes + if remaining.x_out(pivot_row, other_col) != 0 + ] + row_z = [ + nx.shortest_path_length(G, source=col, target=other_col) + for other_col in G.nodes + if remaining.z_out(pivot_row, other_col) != 0 + ] + dist_x = sum(row_x) + dist_z = sum(row_z) + scores.append((col, dist_x + dist_z)) + + return choice_fn(scores, key=lambda x: x[1])[0] + + +def pick_pivot_perm_row_col(G, remaining: "CliffordTableau", remaining_rows: List[int], choice_fn=min): + row = pick_row(G, remaining, remaining_rows, choice_fn) + col = pick_col(G, remaining, row, choice_fn) + return col, row + + def pick_pivot(G, remaining: "CliffordTableau", possible_swaps, include_swaps): """ Pick the pivot to eliminate the next column in the clifford synthesis algorithm. @@ -399,11 +443,66 @@ def apply(gate_name: str, gate_data: tuple): return qc +def synthesize_tableau_perm_row_col(tableau: CliffordTableau, topo: Topology, pick_pivot_callback=None): + if pick_pivot_callback is None: + pick_pivot_callback = pick_pivot_perm_row_col + qc = Circuit(tableau.n_qubits) + + remaining = tableau.inverse() + permutation = {v: v for v in range(tableau.n_qubits)} + remaining_rows = list(range(tableau.n_qubits)) + + G = topo.to_nx + for e1, e2 in G.edges: + G[e1][e2]["weight"] = 0 + + def apply(gate_name: str, gate_data: tuple): + if gate_name == "CNOT": + remaining.append_cnot(gate_data[0], gate_data[1]) + qc.add_gate(CX(gate_data[0], gate_data[1])) + G[gate_data[0]][gate_data[1]]["weight"] = 2 + elif gate_name == "H": + remaining.append_h(gate_data[0]) + qc.add_gate(H(gate_data[0])) + elif gate_name == "S": + remaining.append_s(gate_data[0]) + qc.add_gate(S(gate_data[0])) + else: + raise Exception("Unknown Gate") + + while G.nodes: + # 1. Pick a pivot + pivot_col, pivot_row = pick_pivot_callback(G, remaining, remaining_rows) + + steiner_reduce_column(pivot_col, pivot_col, G, remaining, apply) + remaining_rows.remove(pivot_row) + G.remove_node(pivot_col) + + final_permutation = np.argmax(remaining.x_matrix, axis=1) + qc.final_permutation = final_permutation + signs_copy_z = remaining.signs[remaining.n_qubits: 2 * remaining.n_qubits].copy() + + for col in range(remaining.n_qubits): + if signs_copy_z[col] != 0: + apply("H", (final_permutation[col],)) + apply("S", (final_permutation[col],)) + apply("S", (final_permutation[col],)) + apply("H", (final_permutation[col],)) + + for col in range(remaining.n_qubits): + if remaining.signs[col] != 0: + apply("S", (final_permutation[col],)) + apply("S", (final_permutation[col],)) + + return qc, permutation + + def synthesize_tableau(tableau: CliffordTableau, topo: Topology, include_swaps=True, pick_pivot_callback=None): """ Architecture aware synthesis of a Clifford tableau. This is the implementation of the algorithm described in Winderl et. al. [1] + :param pick_pivot_callback: :param tableau: The Clifford tableau :param topo: The topology :param include_swaps: Whether to allow initial and final measurement permutations diff --git a/tests/clifford/test_clifford_synthesis.py b/tests/clifford/test_clifford_synthesis.py index 44035653..486cc0dd 100644 --- a/tests/clifford/test_clifford_synthesis.py +++ b/tests/clifford/test_clifford_synthesis.py @@ -5,7 +5,8 @@ from qiskit.circuit.library import Permutation from pauliopt.clifford.tableau import CliffordTableau -from pauliopt.clifford.tableau_synthesis import synthesize_tableau, synthesize_tableau_permutation +from pauliopt.clifford.tableau_synthesis import synthesize_tableau, synthesize_tableau_permutation, \ + synthesize_tableau_perm_row_col from pauliopt.topologies import Topology from tests.clifford.utils import tableau_from_circuit from tests.utils import verify_equality, random_hscx_circuit @@ -44,6 +45,33 @@ def test_clifford_synthesis(self, _, n_qubits, n_gates, topo, include_swaps): "The Synthesized circuit does not equal to original", ) + @parameterized.expand( + [ + ("line_5", 5, 1000, Topology.line(5)), + ("line_6", 6, 1000, Topology.line(6)), + ("line_8", 8, 1000, Topology.line(8)), + ("grid_4", 4, 1000, Topology.grid(2, 2)), + ("grid_8", 8, 1000, Topology.grid(2, 4)), + ("line_5", 5, 1000, Topology.line(5)), + ("line_8", 8, 1000, Topology.line(8)), + ("grid_4", 4, 1000, Topology.grid(2, 2)), + ("grid_8", 8, 1000, Topology.grid(2, 4)), + ] + ) + def test_clifford_perm_row_col_synthesis(self, _, n_qubits, n_gates, topo): + circuit = random_hscx_circuit(nr_qubits=n_qubits, nr_gates=n_gates) + + ct = CliffordTableau(n_qubits) + ct = tableau_from_circuit(ct, circuit) + + qc, perm = synthesize_tableau_perm_row_col(ct, topo) + qc = qc.to_qiskit() + + self.assertTrue( + verify_equality(circuit.to_qiskit(), qc), + "The Synthesized circuit does not equal to original", + ) + @parameterized.expand( [ ("line_3", 3, 1000, Topology.line(3)), From 051a96597f5008da70fabd429542ce8d1166aeef Mon Sep 17 00:00:00 2001 From: David Winderl Date: Tue, 7 Jan 2025 07:33:33 +0100 Subject: [PATCH 5/8] fix small bug and add some documentation Signed-off-by: David Winderl --- pauliopt/clifford/tableau.py | 18 +++++------ pauliopt/clifford/tableau_synthesis.py | 41 +++++++++++++------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/pauliopt/clifford/tableau.py b/pauliopt/clifford/tableau.py index 6cdc521b..7785e2f8 100644 --- a/pauliopt/clifford/tableau.py +++ b/pauliopt/clifford/tableau.py @@ -173,13 +173,13 @@ def string_repr(self, sep=" ", sign_sep="| "): out = "" for i in range(self.n_qubits): for j in range(self.n_qubits): - x_str = ["I", "X", "Z", "Y"][int(self.x_out(i, j))] - z_str = ["I", "X", "Z", "Y"][int(self.z_out(i, j))] + x_str = ["I", "X", "Z", "Y"][int(self._x_out(i, j))] + z_str = ["I", "X", "Z", "Y"][int(self._z_out(i, j))] out += f"{x_str}/{z_str}" + sep out += sign_sep + f"{'+' if self.signs[i] == 0 else '-'} \n" return out - def x_out(self, row, col): + def _x_out(self, row, col): """ Get the X operator in row `row` and column `col`. @@ -189,7 +189,7 @@ def x_out(self, row, col): """ return self.tableau[row, col] + 2 * self.tableau[row, col + self.n_qubits] - def z_out(self, row, col): + def _z_out(self, row, col): """ Get the Z operator in row `row` and column `col`. @@ -211,20 +211,20 @@ def x_matrix(self): x_matrx = np.zeros((self.n_qubits, self.n_qubits), dtype=int) for i in range(self.n_qubits): for j in range(self.n_qubits): - x_matrx[i, j] = self.x_out(i, j) + x_matrx[i, j] = self._x_out(i, j) return x_matrx @property def z_matrix(self): """ - Binary matrix representing the X-Basis of the clifford tableau. + Binary matrix representing the Z-Basis of the clifford tableau. :return: """ - x_matrx = np.zeros((self.n_qubits, self.n_qubits), dtype=int) + z_matrx = np.zeros((self.n_qubits, self.n_qubits), dtype=int) for i in range(self.n_qubits): for j in range(self.n_qubits): - x_matrx[i, j] = self.z_out(i, j) - return x_matrx + z_matrx[i, j] = self._z_out(i, j) + return z_matrx def _xor_row(self, i, j): """ diff --git a/pauliopt/clifford/tableau_synthesis.py b/pauliopt/clifford/tableau_synthesis.py index 2eafa14b..911c7a67 100644 --- a/pauliopt/clifford/tableau_synthesis.py +++ b/pauliopt/clifford/tableau_synthesis.py @@ -22,12 +22,12 @@ def heurisitc_fkt(row, G, remaining: CliffordTableau): row_x = [ nx.shortest_path_length(G, source=row, target=col) for col in G.nodes - if remaining.x_out(row, col) != 0 + if remaining._x_out(row, col) != 0 ] row_z = [ nx.shortest_path_length(G, source=row, target=col) for col in G.nodes - if remaining.z_out(row, col) != 0 + if remaining._z_out(row, col) != 0 ] dist_x = sum(row_x) dist_z = sum(row_z) @@ -37,8 +37,8 @@ def heurisitc_fkt(row, G, remaining: CliffordTableau): def pick_row(G, remaining: "CliffordTableau", remaining_rows, choice_fn=min): scores = [] for row in remaining_rows: - row_x = [1 for col in G.nodes if remaining.x_out(row, col) != 0] - row_z = [1 for col in G.nodes if remaining.z_out(row, col) != 0] + row_x = [1 for col in G.nodes if remaining._x_out(row, col) != 0] + row_z = [1 for col in G.nodes if remaining._z_out(row, col) != 0] dist_x = sum(row_x) dist_z = sum(row_z) scores.append((row, dist_x + dist_z)) @@ -58,12 +58,12 @@ def pick_col( row_x = [ nx.shortest_path_length(G, source=col, target=other_col) for other_col in G.nodes - if remaining.x_out(pivot_row, other_col) != 0 + if remaining._x_out(pivot_row, other_col) != 0 ] row_z = [ nx.shortest_path_length(G, source=col, target=other_col) for other_col in G.nodes - if remaining.z_out(pivot_row, other_col) != 0 + if remaining._z_out(pivot_row, other_col) != 0 ] dist_x = sum(row_x) dist_z = sum(row_z) @@ -227,14 +227,14 @@ def sanitize_z(pivot_row, pivot_column, row_z, remaining, apply): :param apply: The function to apply a gate """ for column in row_z: - if remaining.z_out(pivot_row, column) == 3: + if remaining._z_out(pivot_row, column) == 3: apply("S", (column,)) - if remaining.z_out(pivot_row, column) == 1: + if remaining._z_out(pivot_row, column) == 1: apply("H", (column,)) # caveat for the pivot - if remaining.x_out(pivot_row, pivot_column) == 3: + if remaining._x_out(pivot_row, pivot_column) == 3: apply("S", (pivot_column,)) @@ -252,10 +252,10 @@ def sanitize_field_x(row, row_x, remaining, apply): :param apply: The function to apply a gate """ for column in row_x: - if remaining.x_out(row, column) == 3: + if remaining._x_out(row, column) == 3: apply("S", (column,)) - if remaining.x_out(row, column) == 2: + if remaining._x_out(row, column) == 2: apply("H", (column,)) @@ -289,7 +289,7 @@ def remove_interactions( """ row = list(set([pivot_col] + row)) - lookup = {node: int(remaining.x_out(pivot_row, node) != 0) for node in sub_graph.nodes} + lookup = {node: int(remaining._x_out(pivot_row, node) != 0) for node in sub_graph.nodes} traversal = compute_steiner_tree( pivot_col, row, @@ -302,14 +302,14 @@ def remove_interactions( ) if basis == "x": for parent, child in traversal: - if remaining.x_out(pivot_row, parent) == 0: + if remaining._x_out(pivot_row, parent) == 0: apply("CNOT", (child, parent)) for parent, child in traversal: apply("CNOT", (parent, child)) elif basis == "z": for parent, child in traversal: - if remaining.z_out(pivot_row, parent) == 0: + if remaining._z_out(pivot_row, parent) == 0: apply("CNOT", (parent, child)) for parent, child in traversal: @@ -338,7 +338,7 @@ def steiner_reduce_column( :param include_swaps: Whether to include swaps in the steiner tree """ # 2. Sanitize the destabilizer row - row_x = [col for col in sub_graph.nodes if remaining.x_out(pivot_row, col) != 0] + row_x = [col for col in sub_graph.nodes if remaining._x_out(pivot_row, col) != 0] sanitize_field_x(pivot_row, row_x, remaining, apply) # 3. Remove the interactions from the destabilizer row remove_interactions( @@ -354,7 +354,7 @@ def steiner_reduce_column( permutation=permutation, ) # 4. Sanitize the stabilizer row - row_z = [row for row in sub_graph.nodes if remaining.z_out(pivot_row, row) != 0] + row_z = [row for row in sub_graph.nodes if remaining._z_out(pivot_row, row) != 0] sanitize_z(pivot_row, pivot_col, row_z, remaining, apply) # 5. Remove the interactions from the stabilizer row @@ -373,8 +373,8 @@ def steiner_reduce_column( # ensure that the pivots are in ZX basis # (this is provided by the construction of a clifford) - assert remaining.x_out(pivot_row, pivot_col) == 1 - assert remaining.z_out(pivot_row, pivot_col) == 2 + assert remaining._x_out(pivot_row, pivot_col) == 1 + assert remaining._z_out(pivot_row, pivot_col) == 2 def get_non_cutting_vertex(G, pivot_col, swappable_nodes): @@ -471,11 +471,12 @@ def apply(gate_name: str, gate_data: tuple): raise Exception("Unknown Gate") while G.nodes: - # 1. Pick a pivot pivot_col, pivot_row = pick_pivot_callback(G, remaining, remaining_rows) - steiner_reduce_column(pivot_col, pivot_col, G, remaining, apply) + steiner_reduce_column(pivot_col, pivot_row, G, remaining, apply) remaining_rows.remove(pivot_row) + if not pivot_col in G.nodes: + raise Exception("Picked pivot column is not present in Graph. Please recheck your heuristic.") G.remove_node(pivot_col) final_permutation = np.argmax(remaining.x_matrix, axis=1) From 9967812f40c54e494f091d6bd3cbb3f64c229487 Mon Sep 17 00:00:00 2001 From: David Winderl Date: Tue, 7 Jan 2025 07:34:35 +0100 Subject: [PATCH 6/8] Format code correctly Signed-off-by: David Winderl --- pauliopt/clifford/tableau.py | 10 +- pauliopt/clifford/tableau_synthesis.py | 116 +++++++++++++--------- tests/clifford/test_clifford_synthesis.py | 11 +- 3 files changed, 81 insertions(+), 56 deletions(-) diff --git a/pauliopt/clifford/tableau.py b/pauliopt/clifford/tableau.py index 7785e2f8..8f842b65 100644 --- a/pauliopt/clifford/tableau.py +++ b/pauliopt/clifford/tableau.py @@ -132,8 +132,8 @@ def from_tableau(tableau, signs): """ n_qubits = tableau.shape[0] // 2 if not ( - tableau.shape == (2 * n_qubits, 2 * n_qubits) - and signs.shape == (2 * n_qubits,) + tableau.shape == (2 * n_qubits, 2 * n_qubits) + and signs.shape == (2 * n_qubits,) ): raise ValueError( "Tableau and signs must have shape " @@ -198,8 +198,8 @@ def _z_out(self, row, col): col (int): Column index. """ return ( - self.tableau[row + self.n_qubits, col] - + 2 * self.tableau[row + self.n_qubits, col + self.n_qubits] + self.tableau[row + self.n_qubits, col] + + 2 * self.tableau[row + self.n_qubits, col + self.n_qubits] ) @property @@ -387,7 +387,7 @@ def apply(self, other: "CliffordTableau"): for k in range(2 * self.n_qubits): row2 = other.tableau[k] x2 = other.tableau[k, : self.n_qubits] - z2 = other.tableau[k, self.n_qubits:] + z2 = other.tableau[k, self.n_qubits :] # Adding a factor of i for each Y in the image of an operator under the # first operation, since Y=iXZ diff --git a/pauliopt/clifford/tableau_synthesis.py b/pauliopt/clifford/tableau_synthesis.py index 911c7a67..bff781d0 100644 --- a/pauliopt/clifford/tableau_synthesis.py +++ b/pauliopt/clifford/tableau_synthesis.py @@ -47,10 +47,10 @@ def pick_row(G, remaining: "CliffordTableau", remaining_rows, choice_fn=min): def pick_col( - G, - remaining: "CliffordTableau", - pivot_row, - choice_fn=min, + G, + remaining: "CliffordTableau", + pivot_row, + choice_fn=min, ): scores = [] for col in G.nodes: @@ -72,7 +72,9 @@ def pick_col( return choice_fn(scores, key=lambda x: x[1])[0] -def pick_pivot_perm_row_col(G, remaining: "CliffordTableau", remaining_rows: List[int], choice_fn=min): +def pick_pivot_perm_row_col( + G, remaining: "CliffordTableau", remaining_rows: List[int], choice_fn=min +): row = pick_row(G, remaining, remaining_rows, choice_fn) col = pick_col(G, remaining, row, choice_fn) return col, row @@ -94,7 +96,7 @@ def pick_pivot(G, remaining: "CliffordTableau", possible_swaps, include_swaps): has_cutting_swappable = any([not is_cutting(i, G) for i in possible_swaps]) for col in G.nodes: if not is_cutting(col, G) or ( - include_swaps and has_cutting_swappable and col in possible_swaps + include_swaps and has_cutting_swappable and col in possible_swaps ): scores.append((col, col, heurisitc_fkt(col, G, remaining))) assert len(scores) > 0 @@ -138,14 +140,14 @@ def relabel_graph_inplace(G, parent, child): def compute_steiner_tree( - root: int, - nodes: [int], - sub_graph: nx.Graph, - include_swaps=False, - lookup=None, - swappable_nodes=None, - permutation=None, - n_qubits=None, + root: int, + nodes: [int], + sub_graph: nx.Graph, + include_swaps=False, + lookup=None, + swappable_nodes=None, + permutation=None, + n_qubits=None, ): """ Compute the steiner tree of the sub_graph with the given nodes. @@ -190,10 +192,10 @@ def compute_steiner_tree( # if the parent is zero and the child is one and both are swappable # then swap them if ( - lookup[parent] == 0 - and lookup[child] == 1 - and child in swappable_nodes - and parent in swappable_nodes + lookup[parent] == 0 + and lookup[child] == 1 + and child in swappable_nodes + and parent in swappable_nodes ): relabel_graph_inplace(steiner_stree, parent, child) relabel_graph_inplace(sub_graph, parent, child) @@ -260,16 +262,16 @@ def sanitize_field_x(row, row_x, remaining, apply): def remove_interactions( - pivot_col, - pivot_row, - row, - sub_graph, - remaining, - apply, - basis, - include_swaps=False, - swappable_nodes=None, - permutation=None, + pivot_col, + pivot_row, + row, + sub_graph, + remaining, + apply, + basis, + include_swaps=False, + swappable_nodes=None, + permutation=None, ): """ Remove the interactions of the destabilizer/stabilizer part. @@ -289,7 +291,9 @@ def remove_interactions( """ row = list(set([pivot_col] + row)) - lookup = {node: int(remaining._x_out(pivot_row, node) != 0) for node in sub_graph.nodes} + lookup = { + node: int(remaining._x_out(pivot_row, node) != 0) for node in sub_graph.nodes + } traversal = compute_steiner_tree( pivot_col, row, @@ -317,14 +321,14 @@ def remove_interactions( def steiner_reduce_column( - pivot_col, - pivot_row, - sub_graph, - remaining, - apply, - swappable_nodes=None, - permutation=None, - include_swaps=False, + pivot_col, + pivot_row, + sub_graph, + remaining, + apply, + swappable_nodes=None, + permutation=None, + include_swaps=False, ): """ Steiner reduce a column of the clifford. @@ -390,9 +394,7 @@ def get_non_cutting_vertex(G, pivot_col, swappable_nodes): def synthesize_tableau_permutation( - tableau: CliffordTableau, - topo: Topology, - pivot_permutation: List[Tuple[int, int]] + tableau: CliffordTableau, topo: Topology, pivot_permutation: List[Tuple[int, int]] ) -> Optional[Circuit]: qc = Circuit(tableau.n_qubits) @@ -426,7 +428,7 @@ def apply(gate_name: str, gate_data: tuple): final_permutation = np.argmax(remaining.x_matrix, axis=1) qc.final_permutation = final_permutation - signs_copy_z = remaining.signs[remaining.n_qubits: 2 * remaining.n_qubits].copy() + signs_copy_z = remaining.signs[remaining.n_qubits : 2 * remaining.n_qubits].copy() for col in range(remaining.n_qubits): if signs_copy_z[col] != 0: @@ -443,7 +445,9 @@ def apply(gate_name: str, gate_data: tuple): return qc -def synthesize_tableau_perm_row_col(tableau: CliffordTableau, topo: Topology, pick_pivot_callback=None): +def synthesize_tableau_perm_row_col( + tableau: CliffordTableau, topo: Topology, pick_pivot_callback=None +): if pick_pivot_callback is None: pick_pivot_callback = pick_pivot_perm_row_col qc = Circuit(tableau.n_qubits) @@ -476,12 +480,14 @@ def apply(gate_name: str, gate_data: tuple): steiner_reduce_column(pivot_col, pivot_row, G, remaining, apply) remaining_rows.remove(pivot_row) if not pivot_col in G.nodes: - raise Exception("Picked pivot column is not present in Graph. Please recheck your heuristic.") + raise Exception( + "Picked pivot column is not present in Graph. Please recheck your heuristic." + ) G.remove_node(pivot_col) final_permutation = np.argmax(remaining.x_matrix, axis=1) qc.final_permutation = final_permutation - signs_copy_z = remaining.signs[remaining.n_qubits: 2 * remaining.n_qubits].copy() + signs_copy_z = remaining.signs[remaining.n_qubits : 2 * remaining.n_qubits].copy() for col in range(remaining.n_qubits): if signs_copy_z[col] != 0: @@ -498,7 +504,12 @@ def apply(gate_name: str, gate_data: tuple): return qc, permutation -def synthesize_tableau(tableau: CliffordTableau, topo: Topology, include_swaps=True, pick_pivot_callback=None): +def synthesize_tableau( + tableau: CliffordTableau, + topo: Topology, + include_swaps=True, + pick_pivot_callback=None, +): """ Architecture aware synthesis of a Clifford tableau. This is the implementation of the algorithm described in Winderl et. al. [1] @@ -549,7 +560,9 @@ def apply(gate_name: str, gate_data: tuple): while G.nodes: # 1. Pick a pivot - pivot_col, pivot_row = pick_pivot_callback(G, remaining, swappable_nodes, include_swaps) + pivot_col, pivot_row = pick_pivot_callback( + G, remaining, swappable_nodes, include_swaps + ) if is_cutting(pivot_col, G) and include_swaps: non_cutting = get_non_cutting_vertex(G, pivot_col, swappable_nodes) @@ -561,14 +574,21 @@ def apply(gate_name: str, gate_data: tuple): ) steiner_reduce_column( - pivot_col, pivot_col, G, remaining, apply, swappable_nodes, permutation, include_swaps + pivot_col, + pivot_col, + G, + remaining, + apply, + swappable_nodes, + permutation, + include_swaps, ) if pivot_col in swappable_nodes: swappable_nodes.remove(pivot_col) G.remove_node(pivot_col) - signs_copy_z = remaining.signs[tableau.n_qubits: 2 * tableau.n_qubits].copy() + signs_copy_z = remaining.signs[tableau.n_qubits : 2 * tableau.n_qubits].copy() for col in range(tableau.n_qubits): if signs_copy_z[col] != 0: apply("H", (col,)) diff --git a/tests/clifford/test_clifford_synthesis.py b/tests/clifford/test_clifford_synthesis.py index 486cc0dd..a19a6b04 100644 --- a/tests/clifford/test_clifford_synthesis.py +++ b/tests/clifford/test_clifford_synthesis.py @@ -5,8 +5,11 @@ from qiskit.circuit.library import Permutation from pauliopt.clifford.tableau import CliffordTableau -from pauliopt.clifford.tableau_synthesis import synthesize_tableau, synthesize_tableau_permutation, \ - synthesize_tableau_perm_row_col +from pauliopt.clifford.tableau_synthesis import ( + synthesize_tableau, + synthesize_tableau_permutation, + synthesize_tableau_perm_row_col, +) from pauliopt.topologies import Topology from tests.clifford.utils import tableau_from_circuit from tests.utils import verify_equality, random_hscx_circuit @@ -83,7 +86,9 @@ def test_clifford_permutation_synthesis(self, _, n_qubits, n_gates, topo): ct = CliffordTableau(n_qubits) ct = tableau_from_circuit(ct, circuit.copy()) qc = synthesize_tableau_permutation(ct, topo, permutation) - qc.final_permutation = [source for source, target in sorted(permutation, key=lambda x: x[1])] + qc.final_permutation = [ + source for source, target in sorted(permutation, key=lambda x: x[1]) + ] qc = qc.to_qiskit() self.assertTrue( From 8156146eca9a4b7250d03cc2c4775191aacd3939 Mon Sep 17 00:00:00 2001 From: David Winderl Date: Tue, 7 Jan 2025 07:49:34 +0100 Subject: [PATCH 7/8] finalizes perm-row-col and add docs Signed-off-by: David Winderl --- pauliopt/clifford/tableau_synthesis.py | 103 +++++++++++-------------- 1 file changed, 43 insertions(+), 60 deletions(-) diff --git a/pauliopt/clifford/tableau_synthesis.py b/pauliopt/clifford/tableau_synthesis.py index bff781d0..eda4178e 100644 --- a/pauliopt/clifford/tableau_synthesis.py +++ b/pauliopt/clifford/tableau_synthesis.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Optional +from typing import List, Tuple, Optional, Callable import networkx as nx import numpy as np @@ -11,6 +11,11 @@ from pauliopt.topologies import Topology +class CliffordTableauSynthesisException(Exception): + + pass + + def heurisitc_fkt(row, G, remaining: CliffordTableau): """ The heuristic function for picking the pivot in the clifford synthesis algorithm. @@ -173,13 +178,21 @@ def compute_steiner_tree( return [] if include_swaps: if lookup is None: - raise Exception("Lookup table is required to include swaps") + raise CliffordTableauSynthesisException( + "Lookup table is required to include swaps" + ) if swappable_nodes is None: - raise Exception("Swappable nodes are required to include swaps") + raise CliffordTableauSynthesisException( + "Swappable nodes are required to include swaps" + ) if permutation is None: - raise Exception("Permutation is required to include swaps") + raise CliffordTableauSynthesisException( + "Permutation is required to include swaps" + ) if n_qubits is None: - raise Exception("Number of qubits is required to include swaps") + raise CliffordTableauSynthesisException( + "Number of qubits is required to include swaps" + ) for _ in range(n_qubits): dfs = list(reversed(list(nx.dfs_edges(steiner_stree, source=root)))) @@ -279,7 +292,8 @@ def remove_interactions( Include swaps requires swappable_nodes, permutation and include_swaps to be set. - :param pivot_row: The pivot of the clifford + :param pivot_col: The pivot column of the clifford tableau + :param pivot_row: The pivot row of the clifford tableau :param row: The specific row of the clifford :param sub_graph: The graph of the topology :param remaining: The remaining clifford @@ -333,7 +347,8 @@ def steiner_reduce_column( """ Steiner reduce a column of the clifford. - :param pivot_row: The pivot of the clifford + :param pivot_col: The pivot column of the clifford tableau + :param pivot_row: The pivot row of the clifford tableau :param sub_graph: The graph of the topology :param remaining: The remaining clifford :param apply: The function to apply a gate @@ -393,67 +408,35 @@ def get_non_cutting_vertex(G, pivot_col, swappable_nodes): return non_cutting -def synthesize_tableau_permutation( - tableau: CliffordTableau, topo: Topology, pivot_permutation: List[Tuple[int, int]] -) -> Optional[Circuit]: - qc = Circuit(tableau.n_qubits) - - remaining = tableau.inverse() - - G = topo.to_nx - for e1, e2 in G.edges: - G[e1][e2]["weight"] = 0 - - def apply(gate_name: str, gate_data: tuple): - if gate_name == "CNOT": - remaining.append_cnot(gate_data[0], gate_data[1]) - qc.add_gate(CX(gate_data[0], gate_data[1])) - G[gate_data[0]][gate_data[1]]["weight"] = 2 - elif gate_name == "H": - remaining.append_h(gate_data[0]) - qc.add_gate(H(gate_data[0])) - elif gate_name == "S": - remaining.append_s(gate_data[0]) - qc.add_gate(S(gate_data[0])) - else: - raise Exception("Unknown Gate") - - for pivot_col, pivot_row in pivot_permutation: - if is_cutting(pivot_col, G): - return None - - steiner_reduce_column(pivot_col, pivot_row, G, remaining, apply) +def synthesize_tableau_perm_row_col( + tableau: CliffordTableau, topo: Topology, pick_pivot_callback: Callable = None +) -> Circuit: + """ + Architecture-aware synthesis of a clifford tableau using the perm-row-col method. - G.remove_node(pivot_col) + The perm-row-col method itself is described in Meijer-van de Griend and Li [1]. The tableau reduction is adapted as in [2]. - final_permutation = np.argmax(remaining.x_matrix, axis=1) - qc.final_permutation = final_permutation - signs_copy_z = remaining.signs[remaining.n_qubits : 2 * remaining.n_qubits].copy() + We have further provided the option to pick a pivot by overwriting the `pick_pivot_callback` - for col in range(remaining.n_qubits): - if signs_copy_z[col] != 0: - apply("H", (final_permutation[col],)) - apply("S", (final_permutation[col],)) - apply("S", (final_permutation[col],)) - apply("H", (final_permutation[col],)) + *Note*: The permutation itself is stored as a final permutation object on the circuit and can be retrieved using + the `final_permutation` property. - for col in range(remaining.n_qubits): - if remaining.signs[col] != 0: - apply("S", (final_permutation[col],)) - apply("S", (final_permutation[col],)) + :param tableau: The clifford tableau to reduce + :param topo: The topology constraint + :param pick_pivot_callback: Heuristic way to choose the new row and column to reduce + :return: - return qc + References + [1] Meijer-van de Griend and Li "Dynamic Qubit Routing with CNOT Circuit Synthesis for Quantum Compilation," Electronic Proceedings in Theoretical Computer Science. -def synthesize_tableau_perm_row_col( - tableau: CliffordTableau, topo: Topology, pick_pivot_callback=None -): + [2] Winderl, Huang, et al. "Architecture-Aware Synthesis of Stabilizer Circuits from Clifford Tableaus." arXiv preprint arXiv:2309.08972 (2023). + """ if pick_pivot_callback is None: pick_pivot_callback = pick_pivot_perm_row_col qc = Circuit(tableau.n_qubits) remaining = tableau.inverse() - permutation = {v: v for v in range(tableau.n_qubits)} remaining_rows = list(range(tableau.n_qubits)) G = topo.to_nx @@ -472,7 +455,7 @@ def apply(gate_name: str, gate_data: tuple): remaining.append_s(gate_data[0]) qc.add_gate(S(gate_data[0])) else: - raise Exception("Unknown Gate") + raise CliffordTableauSynthesisException("Unknown Gate") while G.nodes: pivot_col, pivot_row = pick_pivot_callback(G, remaining, remaining_rows) @@ -480,7 +463,7 @@ def apply(gate_name: str, gate_data: tuple): steiner_reduce_column(pivot_col, pivot_row, G, remaining, apply) remaining_rows.remove(pivot_row) if not pivot_col in G.nodes: - raise Exception( + raise CliffordTableauSynthesisException( "Picked pivot column is not present in Graph. Please recheck your heuristic." ) G.remove_node(pivot_col) @@ -501,7 +484,7 @@ def apply(gate_name: str, gate_data: tuple): apply("S", (final_permutation[col],)) apply("S", (final_permutation[col],)) - return qc, permutation + return qc def synthesize_tableau( @@ -556,7 +539,7 @@ def apply(gate_name: str, gate_data: tuple): remaining.append_s(gate_data[0]) qc.add_gate(S(gate_data[0])) else: - raise Exception("Unknown Gate") + raise CliffordTableauSynthesisException("Unknown Gate") while G.nodes: # 1. Pick a pivot From 1f559332c039e8a1a9791eb16e6dcc2c7b7e409a Mon Sep 17 00:00:00 2001 From: David Winderl Date: Tue, 7 Jan 2025 07:51:00 +0100 Subject: [PATCH 8/8] fix unit tests Signed-off-by: David Winderl --- tests/clifford/test_clifford_synthesis.py | 25 +---------------------- 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/tests/clifford/test_clifford_synthesis.py b/tests/clifford/test_clifford_synthesis.py index a19a6b04..13bc5b38 100644 --- a/tests/clifford/test_clifford_synthesis.py +++ b/tests/clifford/test_clifford_synthesis.py @@ -2,12 +2,10 @@ import unittest from parameterized import parameterized -from qiskit.circuit.library import Permutation from pauliopt.clifford.tableau import CliffordTableau from pauliopt.clifford.tableau_synthesis import ( synthesize_tableau, - synthesize_tableau_permutation, synthesize_tableau_perm_row_col, ) from pauliopt.topologies import Topology @@ -67,31 +65,10 @@ def test_clifford_perm_row_col_synthesis(self, _, n_qubits, n_gates, topo): ct = CliffordTableau(n_qubits) ct = tableau_from_circuit(ct, circuit) - qc, perm = synthesize_tableau_perm_row_col(ct, topo) + qc = synthesize_tableau_perm_row_col(ct, topo) qc = qc.to_qiskit() self.assertTrue( verify_equality(circuit.to_qiskit(), qc), "The Synthesized circuit does not equal to original", ) - - @parameterized.expand( - [ - ("line_3", 3, 1000, Topology.line(3)), - ] - ) - def test_clifford_permutation_synthesis(self, _, n_qubits, n_gates, topo): - circuit = random_hscx_circuit(nr_qubits=n_qubits, nr_gates=n_gates) - for permutation in enumerate_row_col_permutations(n_qubits): - ct = CliffordTableau(n_qubits) - ct = tableau_from_circuit(ct, circuit.copy()) - qc = synthesize_tableau_permutation(ct, topo, permutation) - qc.final_permutation = [ - source for source, target in sorted(permutation, key=lambda x: x[1]) - ] - qc = qc.to_qiskit() - - self.assertTrue( - verify_equality(circuit.to_qiskit(), qc), - "The Synthesized circuit does not equal to original", - )