From abb9fa4fd5c08264cefdf0b945027ac3ece9acde Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 3 Feb 2025 20:28:34 +0900 Subject: [PATCH 01/23] :white_check_mark: Add tests for convert_to_phase_gadget --- tests/test_zxgraphstate.py | 107 +++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 4ec0caa88..db3e599f1 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -983,5 +983,112 @@ def test_random_graph(zx_graph: ZXGraphState) -> None: assert clifford_nodes == [] +@pytest.mark.parametrize( + ("measurements", "exp_measurements", "exp_edges"), + [ + # no pair of adjacent nodes with YZ measurements + # and no node with XZ measurement + ( + [ + (1, Plane.XY, 0.11 * np.pi), + (2, Plane.XY, 0.22 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + (4, Plane.XY, 0.44 * np.pi), + (5, Plane.XY, 0.55 * np.pi), + (6, Plane.XY, 0.66 * np.pi), + ], + [ + (1, Plane.XY, 0.11 * np.pi), + (2, Plane.XY, 0.22 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + (4, Plane.XY, 0.44 * np.pi), + (5, Plane.XY, 0.55 * np.pi), + (6, Plane.XY, 0.66 * np.pi), + ], + {(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)}, + ), + # a pair of adjacent nodes with YZ measurements + ( + [ + (1, Plane.XY, 0.11 * np.pi), + (2, Plane.YZ, 0.22 * np.pi), + (3, Plane.YZ, 0.33 * np.pi), + (4, Plane.XY, 0.44 * np.pi), + (5, Plane.XY, 0.55 * np.pi), + (6, Plane.XY, 0.66 * np.pi), + ], + [ + (1, Plane.XY, 0.11 * np.pi), + (2, Plane.XY, 0.22 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + (4, Plane.XY, 1.44 * np.pi), + (5, Plane.XY, 1.55 * np.pi), + (6, Plane.XY, 0.66 * np.pi), + ], + {(1, 3), (1, 4), (1, 5), (1, 6), (2, 3), (2, 4), (2, 5), (2, 6), (3, 4), (3, 5), (4, 6), (5, 6)}, + ), + # a node with XZ measurement + ( + [ + (1, Plane.XY, 0.11 * np.pi), + (2, Plane.XY, 0.22 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + (4, Plane.XZ, 0.44 * np.pi), + (5, Plane.XY, 0.55 * np.pi), + (6, Plane.XY, 0.66 * np.pi), + ], + [ + (1, Plane.XY, 0.11 * np.pi), + (2, Plane.XY, 1.72 * np.pi), + (3, Plane.XY, 1.83 * np.pi), + (4, Plane.XY, 1.94 * np.pi), + (5, Plane.XY, 0.55 * np.pi), + (6, Plane.XY, 0.66 * np.pi), + ], + {(1, 2), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)}, + ), + # a pair of adjacent nodes with YZ measurements and a node with XZ measurement + ( + [ + (1, Plane.XZ, 0.11 * np.pi), + (2, Plane.YZ, 0.22 * np.pi), + (3, Plane.YZ, 0.33 * np.pi), + (4, Plane.XZ, 0.44 * np.pi), + (5, Plane.XZ, 0.55 * np.pi), + (6, Plane.XZ, 0.66 * np.pi), + ], + [ + (1, Plane.XY, 0.61 * np.pi), + (2, Plane.XY, 1.22 * np.pi), + (3, Plane.XY, 1.83 * np.pi), + (4, Plane.XY, 1.56 * np.pi), + (5, Plane.XY, 1.45 * np.pi), + (6, Plane.YZ, 0.66 * np.pi), + ], + {(1, 3), (1, 4), (1, 5), (1, 6), (2, 3), (2, 4), (2, 5), (2, 6), (3, 6), (4, 5)}, + ), + ], +) +def test_convert_to_phase_gadget( + zx_graph: ZXGraphState, + measurements: list[tuple[int, Plane, float]], + exp_measurements: list[tuple[int, Plane, float]], + exp_edges: set[tuple[int, int]], +) -> None: + _initialize_graph( + zx_graph, + nodes=range(1, 7), + edges=[(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)], + ) + _apply_measurements(zx_graph, measurements) + zx_graph.convert_to_phase_gadget() + _test( + zx_graph, + exp_nodes={1, 2, 3, 4, 5, 6}, + exp_edges=exp_edges, + exp_measurements=exp_measurements, + ) + + if __name__ == "__main__": pytest.main() From 5ef6d904c31a26c6f60fc196115ea032f9e25e9d Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 3 Feb 2025 20:29:17 +0900 Subject: [PATCH 02/23] :sparkles: Add convert_to_phase_gadget to ZXGraphState --- graphix_zx/zxgraphstate.py | 47 +++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index e67724f19..8ea6b061d 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -30,7 +30,7 @@ class ZXGraphState(GraphState): set of output nodes physical_nodes : set[int] set of physical nodes - physical_edges : dict[int, set[int]] + physical_edges : set[tuple[int]] physical edges meas_bases : dict[int, MeasBasis] q_indices : dict[int, int] @@ -509,3 +509,48 @@ def remove_cliffords(self, atol: float = 1e-9) -> None: ] for action_func, check_func in steps: self._remove_cliffords(action_func, check_func, atol) + + def _extract_yz_adjacent_pair(self) -> tuple[int, int] | None: + """Call inside convert_to_phase_gadget. + + Find a pair of adjacent nodes that are both measured in the YZ-plane. + + Returns + ------- + tuple[int, int] | None + A pair of adjacent nodes that are both measured in the YZ-plane, or None if no such pair exists. + """ + yz_nodes = {node for node, basis in self.meas_bases.items() if basis.plane == Plane.YZ} + for u in yz_nodes: + for v in self.get_neighbors(u): + if v in yz_nodes: + return (min(u, v), max(u, v)) + return None + + def _extract_xz_node(self) -> int | None: + """Call inside convert_to_phase_gadget. + + Find a node that is measured in the XZ-plane. + + Returns + ------- + int | None + A node that is measured in the XZ-plane, or None if no such node exists. + """ + for node, basis in self.meas_bases.items(): + if basis.plane == Plane.XZ: + return node + return None + + def convert_to_phase_gadget(self) -> None: + """Convert a ZX-diagram with gflow in MBQC+LC form into its phase-gadget form while preserving gflow.""" + while True: + if pair := self._extract_yz_adjacent_pair(): + self.pivot(*pair) + del pair + continue + if u := self._extract_xz_node(): + self.local_complement(u) + del u + continue + break From ecd81a0c38f86f0c0f6f89c1317ec92d94dbdbae Mon Sep 17 00:00:00 2001 From: nabe98 Date: Wed, 5 Feb 2025 20:23:29 +0900 Subject: [PATCH 03/23] :art: Add diagramatic representation for each tests in convert_to_phase_gadget --- tests/test_zxgraphstate.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index db3e599f1..2e478d322 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -1008,6 +1008,11 @@ def test_random_graph(zx_graph: ZXGraphState) -> None: {(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)}, ), # a pair of adjacent nodes with YZ measurements + # 4(XY) 4(XY) 4 4 4 + # / \ | | | | + # 1(XY) - 2(YZ) - 3(YZ) - 6(XY) -> 1(XY) - 3(XY) - 2(XY) - 6(XY) - 1 + # \ / | | | | + # 5(XY) 5(XY) 5 5 5 ( [ (1, Plane.XY, 0.11 * np.pi), @@ -1027,7 +1032,13 @@ def test_random_graph(zx_graph: ZXGraphState) -> None: ], {(1, 3), (1, 4), (1, 5), (1, 6), (2, 3), (2, 4), (2, 5), (2, 6), (3, 4), (3, 5), (4, 6), (5, 6)}, ), - # a node with XZ measurement + # no pair of adjacent nodes with YZ measurements + # but a node with XZ measurement + # 4(XZ) 4(XY) + # / \ / \ + # 1(XY) - 2(XY) - 3(XY) - 6(XY) -> 1(XY) - 2(XY) 3(XY) - 6(XY) + # \ / \ / + # 5(XY) 5(XY) ( [ (1, Plane.XY, 0.11 * np.pi), @@ -1047,7 +1058,13 @@ def test_random_graph(zx_graph: ZXGraphState) -> None: ], {(1, 2), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)}, ), - # a pair of adjacent nodes with YZ measurements and a node with XZ measurement + # a pair of adjacent nodes with YZ measurements + # and a node with XZ measurement + # 4(XZ) 6(YZ) - 3(XY) + # / \ | x | + # 1(XZ) - 2(YZ) - 3(YZ) - 6(XZ) -> 1(XY) 2(XY) + # \ / | x | + # 5(XZ) 5(XY) - 4(XY) ( [ (1, Plane.XZ, 0.11 * np.pi), From d15f954a06f0394a170637630c75a99987c343a9 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Wed, 5 Feb 2025 21:33:44 +0900 Subject: [PATCH 04/23] :white_check_mark: Add tests for merge_yz_to_xy --- tests/test_zxgraphstate.py | 81 +++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 9 deletions(-) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 2e478d322..31494890e 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -25,7 +25,7 @@ def zx_graph() -> ZXGraphState: def _initialize_graph( zx_graph: ZXGraphState, nodes: range, - edges: list[tuple[int, int]], + edges: set[tuple[int, int]], inputs: tuple[int, ...] = (), outputs: tuple[int, ...] = (), ) -> None: @@ -125,7 +125,7 @@ def test_local_complement_with_no_edge(zx_graph: ZXGraphState) -> None: def test_local_complement_on_output_node(zx_graph: ZXGraphState) -> None: """Test local complement on an output node.""" - _initialize_graph(zx_graph, range(1, 4), [(1, 2), (2, 3)], outputs=(2,)) + _initialize_graph(zx_graph, range(1, 4), {(1, 2), (2, 3)}, outputs=(2,)) measurements = [ (1, Plane.XY, 1.1 * np.pi), (3, Plane.YZ, 1.3 * np.pi), @@ -470,13 +470,13 @@ def graph_1(zx_graph: ZXGraphState) -> None: # 4---1---2 4 2 # | -> # 3 3 - _initialize_graph(zx_graph, nodes=range(1, 5), edges=[(1, 2), (1, 3), (1, 4)]) + _initialize_graph(zx_graph, nodes=range(1, 5), edges={(1, 2), (1, 3), (1, 4)}) def graph_2(zx_graph: ZXGraphState) -> None: # _needs_lc # 1---2---3 -> 1---3 - _initialize_graph(zx_graph, nodes=range(1, 4), edges=[(1, 2), (2, 3)]) + _initialize_graph(zx_graph, nodes=range(1, 4), edges={(1, 2), (2, 3)}) def graph_3(zx_graph: ZXGraphState) -> None: @@ -487,7 +487,7 @@ def graph_3(zx_graph: ZXGraphState) -> None: # \ / \ | / # 5(I) 5(I) _initialize_graph( - zx_graph, nodes=range(1, 7), edges=[(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)], inputs=(1, 4, 5) + zx_graph, nodes=range(1, 7), edges={(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)}, inputs=(1, 4, 5) ) @@ -499,7 +499,7 @@ def graph_4(zx_graph: ZXGraphState) -> None: _initialize_graph( zx_graph, nodes=range(1, 6), - edges=[(1, 2), (2, 3), (2, 4), (3, 4), (4, 5)], + edges={(1, 2), (2, 3), (2, 4), (3, 4), (4, 5)}, inputs=(1,), outputs=(2, 3, 5), ) @@ -856,7 +856,7 @@ def test_remove_clifford_pivot2_with_xz_1p5_pi(zx_graph: ZXGraphState) -> None: def test_unremovable_clifford_vertex(zx_graph: ZXGraphState) -> None: - _initialize_graph(zx_graph, nodes=range(1, 4), edges=[(1, 2), (2, 3)], inputs=(1, 3)) + _initialize_graph(zx_graph, nodes=range(1, 4), edges={(1, 2), (2, 3)}, inputs=(1, 3)) measurements = [ (1, Plane.XY, 0.5 * np.pi), (2, Plane.XY, np.pi), @@ -869,7 +869,7 @@ def test_unremovable_clifford_vertex(zx_graph: ZXGraphState) -> None: def test_remove_cliffords(zx_graph: ZXGraphState) -> None: """Test removing multiple Clifford vertices.""" - _initialize_graph(zx_graph, nodes=range(1, 5), edges=[(1, 2), (1, 3), (1, 4)]) + _initialize_graph(zx_graph, nodes=range(1, 5), edges={(1, 2), (1, 3), (1, 4)}) measurements = [ (1, Plane.XY, 0.5 * np.pi), (2, Plane.XY, 0.5 * np.pi), @@ -1095,7 +1095,7 @@ def test_convert_to_phase_gadget( _initialize_graph( zx_graph, nodes=range(1, 7), - edges=[(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)], + edges={(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)}, ) _apply_measurements(zx_graph, measurements) zx_graph.convert_to_phase_gadget() @@ -1107,5 +1107,68 @@ def test_convert_to_phase_gadget( ) +@pytest.mark.parametrize( + ("initial_edges", "measurements", "exp_measurements", "exp_edges"), + [ + # 4(XY) 4(XY) + # | -> | + # 1(YZ) - 2(XY) - 3(XY) 2(XY) - 3(XY) + ( + {(1, 2), (2, 3), (2, 4)}, + [ + (1, Plane.YZ, 0.11 * np.pi), + (2, Plane.XY, 0.22 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + (4, Plane.XY, 0.44 * np.pi), + ], + [ + (2, Plane.XY, 0.33 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + (4, Plane.XY, 0.44 * np.pi), + ], + {(2, 3), (2, 4)}, + ), + # 4(YZ) 4(YZ) + # | \ -> | \ + # 1(YZ) - 2(XY) - 3(XY) 2(XY) - 3(XY) + ( + {(1, 2), (2, 3), (2, 4), (3, 4)}, + [ + (1, Plane.YZ, 0.11 * np.pi), + (2, Plane.XY, 0.22 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + (4, Plane.YZ, 0.44 * np.pi), + ], + [ + (2, Plane.XY, 0.33 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + (4, Plane.YZ, 0.44 * np.pi), + ], + {(2, 3), (2, 4), (3, 4)}, + ), + ], +) +def test_merge_yz_to_xy( + zx_graph: ZXGraphState, + initial_edges: list[tuple[int, Plane, float]], + measurements: list[tuple[int, Plane, float]], + exp_measurements: list[tuple[int, Plane, float]], + exp_edges: set[tuple[int, int]], +) -> None: + _initialize_graph( + zx_graph, + nodes=range(1, 5), + edges=initial_edges, + ) + _apply_measurements(zx_graph, measurements) + zx_graph.merge_yz_to_xy() + _test( + zx_graph, + exp_nodes={2, 3, 4}, + exp_edges=exp_edges, + exp_measurements=exp_measurements, + ) + + if __name__ == "__main__": pytest.main() From 912cc8a65fc975e4c360332d99176fe1ea802e09 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Wed, 5 Feb 2025 21:33:55 +0900 Subject: [PATCH 05/23] :art: Add merge_yz_to_xy --- graphix_zx/zxgraphstate.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index 8ea6b061d..48fb44354 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -554,3 +554,21 @@ def convert_to_phase_gadget(self) -> None: del u continue break + + def merge_yz_to_xy(self) -> None: + """Merge YZ-measured nodes that have only one neighbor with an XY-measured node. + + If a node u is measured in the YZ-plane and u has only one neighbor v with a XY-measurement, + then the node u can be merged into the node v. + """ + target_candidates = { + u for u, basis in self.meas_bases.items() if (basis.plane == Plane.YZ and len(self.get_neighbors(u)) == 1) + } + target_nodes = { + u for u in target_candidates if self.meas_bases[next(iter(self.get_neighbors(u)))].plane == Plane.XY + } + for u in target_nodes: + v = self.get_neighbors(u).pop() + new_angle = (self.meas_bases[u].angle + self.meas_bases[v].angle) % (2.0 * np.pi) + self.set_meas_basis(v, PlannerMeasBasis(Plane.XY, new_angle)) + self.remove_physical_node(u) From eb6f70e6e1876539f8f147f366accf0eb705cdab Mon Sep 17 00:00:00 2001 From: nabe98 Date: Thu, 6 Feb 2025 20:47:20 +0900 Subject: [PATCH 06/23] :art: Improve tests' readability --- tests/test_zxgraphstate.py | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 31494890e..aca91de07 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -1092,19 +1092,11 @@ def test_convert_to_phase_gadget( exp_measurements: list[tuple[int, Plane, float]], exp_edges: set[tuple[int, int]], ) -> None: - _initialize_graph( - zx_graph, - nodes=range(1, 7), - edges={(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)}, - ) + initial_edges = {(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)} + _initialize_graph(zx_graph, nodes=range(1, 7), edges=initial_edges) _apply_measurements(zx_graph, measurements) zx_graph.convert_to_phase_gadget() - _test( - zx_graph, - exp_nodes={1, 2, 3, 4, 5, 6}, - exp_edges=exp_edges, - exp_measurements=exp_measurements, - ) + _test(zx_graph, exp_nodes={1, 2, 3, 4, 5, 6}, exp_edges=exp_edges, exp_measurements=exp_measurements) @pytest.mark.parametrize( @@ -1150,24 +1142,15 @@ def test_convert_to_phase_gadget( ) def test_merge_yz_to_xy( zx_graph: ZXGraphState, - initial_edges: list[tuple[int, Plane, float]], + initial_edges: set[tuple[int, int]], measurements: list[tuple[int, Plane, float]], exp_measurements: list[tuple[int, Plane, float]], exp_edges: set[tuple[int, int]], ) -> None: - _initialize_graph( - zx_graph, - nodes=range(1, 5), - edges=initial_edges, - ) + _initialize_graph(zx_graph, nodes=range(1, 5), edges=initial_edges) _apply_measurements(zx_graph, measurements) zx_graph.merge_yz_to_xy() - _test( - zx_graph, - exp_nodes={2, 3, 4}, - exp_edges=exp_edges, - exp_measurements=exp_measurements, - ) + _test(zx_graph, exp_nodes={2, 3, 4}, exp_edges=exp_edges, exp_measurements=exp_measurements) if __name__ == "__main__": From 260577a12a536ccceffc2e7bc0052b795c101bb7 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Thu, 6 Feb 2025 20:57:17 +0900 Subject: [PATCH 07/23] :white_check_mark: Add tests for merge_yz_nodes --- tests/test_zxgraphstate.py | 91 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index aca91de07..a3cae6981 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -1153,5 +1153,96 @@ def test_merge_yz_to_xy( _test(zx_graph, exp_nodes={2, 3, 4}, exp_edges=exp_edges, exp_measurements=exp_measurements) +@pytest.mark.parametrize( + ("initial_edges", "measurements", "exp_zxgraph"), + [ + # 4(YZ) 4(YZ) + # / \ / \ + # 1(XY) - 2(XY) - 3(XY) -> 1(XY) - 2(XY) - 3(XY) + # \ / + # 5(YZ) + ( + {(1, 2), (1, 4), (1, 5), (2, 3), (3, 4), (3, 5)}, + [ + (1, Plane.XY, 0.11 * np.pi), + (2, Plane.XY, 0.22 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + (4, Plane.YZ, 0.44 * np.pi), + (5, Plane.YZ, 0.55 * np.pi), + ], + ( + [ + (1, Plane.XY, 0.11 * np.pi), + (2, Plane.XY, 0.22 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + (4, Plane.YZ, 0.99 * np.pi), + ], + {(1, 2), (1, 4), (2, 3), (3, 4)}, + {1, 2, 3, 4}, + ), + ), + # 4(YZ) + # / \ + # 1(XY) - 2(YZ) - 3(XY) -> 1(XY) - 2(YZ) - 3(XY) + # \ / + # 5(YZ) + ( + {(1, 2), (1, 4), (1, 5), (2, 3), (3, 4), (3, 5)}, + [ + (1, Plane.XY, 0.11 * np.pi), + (2, Plane.YZ, 0.22 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + (4, Plane.YZ, 0.44 * np.pi), + (5, Plane.YZ, 0.55 * np.pi), + ], + ( + [ + (1, Plane.XY, 0.11 * np.pi), + (2, Plane.YZ, 1.21 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + ], + {(1, 2), (2, 3)}, + {1, 2, 3}, + ), + ), + # 4(YZ) + # / \ + # 1(XY) - 2(YZ) - 3(XY) - 1(XY) -> 1(XY) - 2(YZ) - 3(XY) - 1(XY) + # \ / + # 5(YZ) + ( + {(1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (3, 4), (3, 5)}, + [ + (1, Plane.XY, 0.11 * np.pi), + (2, Plane.YZ, 0.22 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + (4, Plane.YZ, 0.44 * np.pi), + (5, Plane.YZ, 0.55 * np.pi), + ], + ( + [ + (1, Plane.XY, 0.11 * np.pi), + (2, Plane.YZ, 1.21 * np.pi), + (3, Plane.XY, 0.33 * np.pi), + ], + {(1, 2), (1, 3), (2, 3)}, + {1, 2, 3}, + ), + ), + ], +) +def test_merge_yz_nodes( + zx_graph: ZXGraphState, + initial_edges: set[tuple[int, int]], + measurements: list[tuple[int, Plane, float]], + exp_zxgraph: tuple[list[tuple[int, Plane, float]], set[tuple[int, int]], set[int]], +) -> None: + _initialize_graph(zx_graph, nodes=range(1, 6), edges=initial_edges) + _apply_measurements(zx_graph, measurements) + zx_graph.merge_yz_nodes() + exp_measurements, exp_edges, exp_nodes = exp_zxgraph + _test(zx_graph, exp_nodes, exp_edges, exp_measurements) + + if __name__ == "__main__": pytest.main() From 0777029415f9464b572f3fdb89898d69f340db3b Mon Sep 17 00:00:00 2001 From: nabe98 Date: Thu, 6 Feb 2025 20:57:36 +0900 Subject: [PATCH 08/23] :art: Add merge_yz_nodes --- graphix_zx/zxgraphstate.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index 48fb44354..8a14a5df0 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -572,3 +572,23 @@ def merge_yz_to_xy(self) -> None: new_angle = (self.meas_bases[u].angle + self.meas_bases[v].angle) % (2.0 * np.pi) self.set_meas_basis(v, PlannerMeasBasis(Plane.XY, new_angle)) self.remove_physical_node(u) + + def merge_yz_nodes(self) -> None: + """Merge isolated YZ-measured nodes into a single node. + + If u, v nodes are measured in the YZ-plane and u, v have the same neighbors, + then u, v can be merged into a single node. + """ + while True: + yz_nodes = {u for u, basis in self.meas_bases.items() if basis.plane == Plane.YZ} + least_nodes = 2 + if len(yz_nodes) < least_nodes: + break + u = yz_nodes.pop() + for v in yz_nodes: + if u > v or self.get_neighbors(u) != self.get_neighbors(v): + continue + + new_angle = (self.meas_bases[u].angle + self.meas_bases[v].angle) % (2.0 * np.pi) + self.set_meas_basis(u, PlannerMeasBasis(Plane.YZ, new_angle)) + self.remove_physical_node(v) From 735b32dc3f55752868f637d781e89d7bea1260ad Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 15 Feb 2025 19:26:03 +0900 Subject: [PATCH 09/23] :recycle: Refactor test codes --- tests/test_zxgraphstate.py | 561 +++++++++++++++++-------------------- 1 file changed, 257 insertions(+), 304 deletions(-) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index a3cae6981..5aebeb2d7 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -519,261 +519,226 @@ def _test_remove_clifford( _test(zx_graph, exp_nodes, exp_edges, exp_measurements) -def test_remove_clifford_removable_with_xz_0(zx_graph: ZXGraphState) -> None: - """Test removing a removable Clifford vertex with measurement plane XZ and angle 0.""" +@pytest.mark.parametrize( + ("measurements", "exp_measurements"), + [ + # XZ plane with angle 0 + ( + [ + (1, Plane.XZ, 0), + (2, Plane.XY, 0.1 * np.pi), + (3, Plane.XZ, 0.2 * np.pi), + (4, Plane.YZ, 0.3 * np.pi), + ], + [ + (2, Plane.XY, 0.1 * np.pi), + (3, Plane.XZ, 0.2 * np.pi), + (4, Plane.YZ, 0.3 * np.pi), + ], + ), + # XZ plane with angle pi + ( + [ + (1, Plane.XZ, np.pi), + (2, Plane.XY, 0.1 * np.pi), + (3, Plane.XZ, 0.2 * np.pi), + (4, Plane.YZ, 0.3 * np.pi), + ], + [ + (2, Plane.XY, 1.1 * np.pi), + (3, Plane.XZ, 1.8 * np.pi), + (4, Plane.YZ, 1.7 * np.pi), + ], + ), + # YZ plane with angle 0 + ( + [ + (1, Plane.YZ, 0), + (2, Plane.XY, 0.1 * np.pi), + (3, Plane.XZ, 0.2 * np.pi), + (4, Plane.YZ, 0.3 * np.pi), + ], + [ + (2, Plane.XY, 0.1 * np.pi), + (3, Plane.XZ, 0.2 * np.pi), + (4, Plane.YZ, 0.3 * np.pi), + ], + ), + # YZ plane with angle pi + ( + [ + (1, Plane.YZ, np.pi), + (2, Plane.XY, 0.1 * np.pi), + (3, Plane.XZ, 0.2 * np.pi), + (4, Plane.YZ, 0.3 * np.pi), + ], + [ + (2, Plane.XY, 1.1 * np.pi), + (3, Plane.XZ, 1.8 * np.pi), + (4, Plane.YZ, 1.7 * np.pi), + ], + ), + ], +) +def test_remove_clifford( + zx_graph: ZXGraphState, + measurements: list[tuple[int, Plane, float]], + exp_measurements: list[tuple[int, Plane, float]], +) -> None: + """Test removing a removable Clifford vertex.""" graph_1(zx_graph) - measurements = [ - (1, Plane.XZ, 0), - (2, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - ] - exp_measurements = [ - (2, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - ] _test_remove_clifford( zx_graph, node=1, measurements=measurements, exp_graph=({2, 3, 4}, set()), exp_measurements=exp_measurements ) -def test_remove_clifford_removable_with_xz_pi(zx_graph: ZXGraphState) -> None: - """Test removing a removable Clifford vertex with measurement plane XZ and angle pi.""" - graph_1(zx_graph) - measurements = [ - (1, Plane.XZ, np.pi), - (2, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - ] - exp_measurements = [ - (2, Plane.XY, 1.1 * np.pi), - (3, Plane.XZ, 1.8 * np.pi), - (4, Plane.YZ, 1.7 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=1, - measurements=measurements, - exp_graph=({2, 3, 4}, set()), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_removable_with_yz_0(zx_graph: ZXGraphState) -> None: - """Test removing a removable Clifford vertex with measurement plane YZ and angle 0.""" - graph_1(zx_graph) - measurements = [ - (1, Plane.YZ, 0), - (2, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - ] - exp_measurements = [ - (2, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=1, - measurements=measurements, - exp_graph=({2, 3, 4}, set()), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_removable_with_yz_pi(zx_graph: ZXGraphState) -> None: - """Test removing a removable Clifford vertex with measurement plane YZ and angle pi.""" - graph_1(zx_graph) - measurements = [ - (1, Plane.YZ, np.pi), - (2, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - ] - exp_measurements = [ - (2, Plane.XY, 1.1 * np.pi), - (3, Plane.XZ, 1.8 * np.pi), - (4, Plane.YZ, 1.7 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=1, - measurements=measurements, - exp_graph=({2, 3, 4}, set()), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_lc_with_xy_0p5_pi(zx_graph: ZXGraphState) -> None: - graph_2(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XY, 0.5 * np.pi), - (3, Plane.YZ, 0.2 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 1.6 * np.pi), - (3, Plane.XZ, 1.8 * np.pi), - ] - _test_remove_clifford( - zx_graph, node=2, measurements=measurements, exp_graph=({1, 3}, {(1, 3)}), exp_measurements=exp_measurements - ) - - -def test_remove_clifford_lc_with_xy_1p5_pi(zx_graph: ZXGraphState) -> None: +@pytest.mark.parametrize( + ("measurements", "exp_measurements"), + [ + # XY plane with angle 0.5 * pi + ( + [ + (1, Plane.XY, 0.1 * np.pi), + (2, Plane.XY, 0.5 * np.pi), + (3, Plane.YZ, 0.2 * np.pi), + ], + [ + (1, Plane.XY, 1.6 * np.pi), + (3, Plane.XZ, 1.8 * np.pi), + ], + ), + # XY plane with angle 1.5 * pi + ( + [ + (1, Plane.XY, 0.1 * np.pi), + (2, Plane.XY, 1.5 * np.pi), + (3, Plane.YZ, 0.2 * np.pi), + ], + [ + (1, Plane.XY, 0.6 * np.pi), + (3, Plane.XZ, 0.2 * np.pi), + ], + ), + # YZ plane with angle 0.5 * pi + ( + [ + (1, Plane.XY, 0.1 * np.pi), + (2, Plane.YZ, 0.5 * np.pi), + (3, Plane.XZ, 0.2 * np.pi), + ], + [ + (1, Plane.XY, 0.6 * np.pi), + (3, Plane.YZ, 1.8 * np.pi), + ], + ), + # YZ plane with angle 1.5 * pi + ( + [ + (1, Plane.XY, 0.1 * np.pi), + (2, Plane.YZ, 1.5 * np.pi), + (3, Plane.XZ, 0.2 * np.pi), + ], + [ + (1, Plane.XY, 1.6 * np.pi), + (3, Plane.YZ, 0.2 * np.pi), + ], + ), + ], +) +def test_remove_clifford_lc( + zx_graph: ZXGraphState, + measurements: list[tuple[int, Plane, float]], + exp_measurements: list[tuple[int, Plane, float]], +) -> None: graph_2(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XY, 1.5 * np.pi), - (3, Plane.YZ, 0.2 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 0.6 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - ] _test_remove_clifford( zx_graph, node=2, measurements=measurements, exp_graph=({1, 3}, {(1, 3)}), exp_measurements=exp_measurements ) -def test_remove_clifford_lc_with_yz_0p5_pi(zx_graph: ZXGraphState) -> None: - graph_2(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.YZ, 0.5 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 0.6 * np.pi), - (3, Plane.YZ, 1.8 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=2, - measurements=measurements, - exp_graph=({1, 3}, {(1, 3)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_lc_with_yz_1p5_pi(zx_graph: ZXGraphState) -> None: - graph_2(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.YZ, 1.5 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 1.6 * np.pi), - (3, Plane.YZ, 0.2 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=2, - measurements=measurements, - exp_graph=({1, 3}, {(1, 3)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot1_with_xy_0(zx_graph: ZXGraphState) -> None: - graph_3(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XY, 0), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 0.5 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.3 * np.pi), - (4, Plane.YZ, 1.7 * np.pi), - (5, Plane.XY, 1.4 * np.pi), - (6, Plane.XZ, 0.5 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=2, - measurements=measurements, - exp_graph=({1, 3, 4, 5, 6}, {(1, 3), (1, 4), (1, 5), (1, 6), (3, 4), (3, 5), (4, 6), (5, 6)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot1_with_xy_pi(zx_graph: ZXGraphState) -> None: - graph_3(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XY, np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 0.5 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 1.7 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 1.5 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=2, - measurements=measurements, - exp_graph=({1, 3, 4, 5, 6}, {(1, 3), (1, 4), (1, 5), (1, 6), (3, 4), (3, 5), (4, 6), (5, 6)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot1_with_xz_0p5_pi(zx_graph: ZXGraphState) -> None: - graph_3(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XZ, 0.5 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 0.5 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.3 * np.pi), - (4, Plane.YZ, 1.7 * np.pi), - (5, Plane.XY, 1.4 * np.pi), - (6, Plane.XZ, 0.5 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=2, - measurements=measurements, - exp_graph=({1, 3, 4, 5, 6}, {(1, 3), (1, 4), (1, 5), (1, 6), (3, 4), (3, 5), (4, 6), (5, 6)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot1_with_xz_1p5_pi(zx_graph: ZXGraphState) -> None: +@pytest.mark.parametrize( + ("measurements", "exp_measurements"), + [ + # XY plane with angle 0 + ( + [ + (1, Plane.XY, 0.1 * np.pi), + (2, Plane.XY, 0), + (3, Plane.XZ, 0.2 * np.pi), + (4, Plane.YZ, 0.3 * np.pi), + (5, Plane.XY, 0.4 * np.pi), + (6, Plane.XZ, 0.5 * np.pi), + ], + [ + (1, Plane.XY, 0.1 * np.pi), + (3, Plane.XZ, 0.3 * np.pi), + (4, Plane.YZ, 1.7 * np.pi), + (5, Plane.XY, 1.4 * np.pi), + (6, Plane.XZ, 0.5 * np.pi), + ], + ), + # XY plane with angle pi + ( + [ + (1, Plane.XY, 0.1 * np.pi), + (2, Plane.XY, np.pi), + (3, Plane.XZ, 0.2 * np.pi), + (4, Plane.YZ, 0.3 * np.pi), + (5, Plane.XY, 0.4 * np.pi), + (6, Plane.XZ, 0.5 * np.pi), + ], + [ + (1, Plane.XY, 0.1 * np.pi), + (3, Plane.XZ, 1.7 * np.pi), + (4, Plane.YZ, 0.3 * np.pi), + (5, Plane.XY, 0.4 * np.pi), + (6, Plane.XZ, 1.5 * np.pi), + ], + ), + # XZ plane with angle 0.5 * pi + ( + [ + (1, Plane.XY, 0.1 * np.pi), + (2, Plane.XZ, 0.5 * np.pi), + (3, Plane.XZ, 0.2 * np.pi), + (4, Plane.YZ, 0.3 * np.pi), + (5, Plane.XY, 0.4 * np.pi), + (6, Plane.XZ, 0.5 * np.pi), + ], + [ + (1, Plane.XY, 0.1 * np.pi), + (3, Plane.XZ, 0.3 * np.pi), + (4, Plane.YZ, 1.7 * np.pi), + (5, Plane.XY, 1.4 * np.pi), + (6, Plane.XZ, 0.5 * np.pi), + ], + ), + # XZ plane with angle 1.5 * pi + ( + [ + (1, Plane.XY, 0.1 * np.pi), + (2, Plane.XZ, 1.5 * np.pi), + (3, Plane.XZ, 0.2 * np.pi), + (4, Plane.YZ, 0.3 * np.pi), + (5, Plane.XY, 0.4 * np.pi), + (6, Plane.XZ, 0.5 * np.pi), + ], + [ + (1, Plane.XY, 0.1 * np.pi), + (3, Plane.XZ, 1.7 * np.pi), + (4, Plane.YZ, 0.3 * np.pi), + (5, Plane.XY, 0.4 * np.pi), + (6, Plane.XZ, 1.5 * np.pi), + ], + ), + ], +) +def test_remove_clifford_pivot1( + zx_graph: ZXGraphState, + measurements: list[tuple[int, Plane, float]], + exp_measurements: list[tuple[int, Plane, float]], +) -> None: graph_3(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XZ, 1.5 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 0.5 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 1.7 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 1.5 * np.pi), - ] _test_remove_clifford( zx_graph, node=2, @@ -783,69 +748,57 @@ def test_remove_clifford_pivot1_with_xz_1p5_pi(zx_graph: ZXGraphState) -> None: ) -def test_remove_clifford_pivot2_with_xy_0(zx_graph: ZXGraphState) -> None: - graph_4(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (4, Plane.XY, 0), - ] - exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=4, - measurements=measurements, - exp_graph=({1, 2, 3, 5}, {(1, 3), (1, 5), (2, 3), (2, 5), (3, 5)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot2_with_xy_pi(zx_graph: ZXGraphState) -> None: - graph_4(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (4, Plane.XY, np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 1.1 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=4, - measurements=measurements, - exp_graph=({1, 2, 3, 5}, {(1, 3), (1, 5), (2, 3), (2, 5), (3, 5)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot2_with_xz_0p5_pi(zx_graph: ZXGraphState) -> None: - graph_4(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (4, Plane.XZ, 0.5 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=4, - measurements=measurements, - exp_graph=({1, 2, 3, 5}, {(1, 3), (1, 5), (2, 3), (2, 5), (3, 5)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot2_with_xz_1p5_pi(zx_graph: ZXGraphState) -> None: +@pytest.mark.parametrize( + ("measurements", "exp_measurements"), + [ + # XY plane with angle 0 + ( + [ + (1, Plane.XY, 0.1 * np.pi), + (4, Plane.XY, 0), + ], + [ + (1, Plane.XY, 0.1 * np.pi), + ], + ), + # XY plane with angle pi + ( + [ + (1, Plane.XY, 0.1 * np.pi), + (4, Plane.XY, np.pi), + ], + [ + (1, Plane.XY, 1.1 * np.pi), + ], + ), + # XZ plane with angle 0.5 * pi + ( + [ + (1, Plane.XY, 0.1 * np.pi), + (4, Plane.XZ, 0.5 * np.pi), + ], + [ + (1, Plane.XY, 0.1 * np.pi), + ], + ), + # XZ plane with angle 1.5 * pi + ( + [ + (1, Plane.XY, 0.1 * np.pi), + (4, Plane.XZ, 1.5 * np.pi), + ], + [ + (1, Plane.XY, 1.1 * np.pi), + ], + ), + ], +) +def test_remove_clifford_pivot2( + zx_graph: ZXGraphState, + measurements: list[tuple[int, Plane, float]], + exp_measurements: list[tuple[int, Plane, float]], +) -> None: graph_4(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (4, Plane.XZ, 1.5 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 1.1 * np.pi), - ] _test_remove_clifford( zx_graph, node=4, From 85a79bb70b4fd9da23eba5d910d5523f0927c378 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 15 Feb 2025 20:54:50 +0900 Subject: [PATCH 10/23] :white_check_mark: Add tests for prune_non_clifford --- tests/test_zxgraphstate.py | 75 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 5aebeb2d7..4af493f0b 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -1197,5 +1197,80 @@ def test_merge_yz_nodes( _test(zx_graph, exp_nodes, exp_edges, exp_measurements) +@pytest.mark.parametrize( + ("initial_zxgraph", "measurements", "exp_zxgraph"), + [ + # test for a phase gadget: apply merge_yz_to_xy then remove_cliffords + ( + (range(1, 5), {(1, 2), (2, 3), (2, 4)}), + [ + (1, Plane.YZ, 0.1 * np.pi), + (2, Plane.XY, 0.4 * np.pi), + (3, Plane.XY, 0.3 * np.pi), + (4, Plane.XY, 0.4 * np.pi), + ], + ( + [ + (3, Plane.XY, 1.8 * np.pi), + (4, Plane.XY, 1.9 * np.pi), + ], + {(3, 4)}, + {3, 4}, + ), + ), + # apply convert_to_phase_gadget, merge_yz_to_xy, then remove_cliffords + ( + (range(1, 5), {(1, 2), (2, 3), (2, 4)}), + [ + (1, Plane.YZ, 0.1 * np.pi), + (2, Plane.XY, 0.9 * np.pi), + (3, Plane.XZ, 0.8 * np.pi), + (4, Plane.XY, 0.4 * np.pi), + ], + ( + [ + (3, Plane.XY, 1.8 * np.pi), + (4, Plane.XY, 1.9 * np.pi), + ], + {(3, 4)}, + {3, 4}, + ), + ), + # apply remove_cliffords, convert_to_phase_gadget, merge_yz_to_xy, then remove_cliffords + ( + (range(1, 7), {(1, 2), (2, 3), (2, 4), (3, 6), (4, 5)}), + [ + (1, Plane.YZ, 0.1 * np.pi), + (2, Plane.XY, 0.9 * np.pi), + (3, Plane.YZ, 1.2 * np.pi), + (4, Plane.XY, 1.4 * np.pi), + (5, Plane.YZ, 1.0 * np.pi), + (6, Plane.XY, 0.5 * np.pi), + ], + ( + [ + (3, Plane.XY, 1.8 * np.pi), + (4, Plane.XY, 1.9 * np.pi), + ], + {(3, 4)}, + {3, 4}, + ), + ), + ], +) +def test_prune_non_clifford( + zx_graph: ZXGraphState, + initial_zxgraph: tuple[range, set[tuple[int, int]]], + measurements: list[tuple[int, Plane, float]], + exp_zxgraph: tuple[list[tuple[int, Plane, float]], set[tuple[int, int]], set[int]], +) -> None: + nodes, edges = initial_zxgraph + _initialize_graph(zx_graph, nodes, edges) + exp_measurements, exp_edges, exp_nodes = exp_zxgraph + _apply_measurements(zx_graph, measurements) + zx_graph.prune_non_clifford() + _test(zx_graph, exp_nodes, exp_edges, exp_measurements) + + if __name__ == "__main__": pytest.main() From d8d07e60a30ab9055c7cf0c222c918120d4d1a39 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 15 Feb 2025 20:55:30 +0900 Subject: [PATCH 11/23] :sparkles: Add prune_non_clifford to ZXGraphState --- graphix_zx/zxgraphstate.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index 8a14a5df0..25e40909a 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -592,3 +592,29 @@ def merge_yz_nodes(self) -> None: new_angle = (self.meas_bases[u].angle + self.meas_bases[v].angle) % (2.0 * np.pi) self.set_meas_basis(u, PlannerMeasBasis(Plane.YZ, new_angle)) self.remove_physical_node(v) + + def prune_non_clifford(self, atol: float = 1e-9) -> None: + """Prune non-Clifford vertices from the graph state. + + Repeat the following steps until there are no non-Clifford vertices: + 1. remove_cliffords + 2. convert_to_phase_gadget + 3. merge_yz_to_xy + 4. merge_yz_nodes + 5. if there are some removable Clifford vertices, back to step 1. + + Parameters + ---------- + atol : float, optional + absolute tolerance, by default 1e-9 + """ + while True: + self.remove_cliffords(atol) + self.convert_to_phase_gadget() + self.merge_yz_to_xy() + self.merge_yz_nodes() + if not any( + is_clifford_angle(self.meas_bases[node].angle, atol) and self.is_removable_clifford(node, atol) + for node in self.physical_nodes - self.input_nodes - self.output_nodes + ): + break From 31fc41d193c83d10f996672beb77fbdba257d1bc Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 15 Feb 2025 20:56:05 +0900 Subject: [PATCH 12/23] :art: Apply ruff check --- graphix_zx/simulator.py | 4 ---- tests/test_graphstate.py | 4 ++-- tests/test_zxgraphstate.py | 14 +++++++------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/graphix_zx/simulator.py b/graphix_zx/simulator.py index f14b3cde3..6a96ca08b 100644 --- a/graphix_zx/simulator.py +++ b/graphix_zx/simulator.py @@ -346,8 +346,6 @@ def _apply_x(self, cmd: X) -> None: result ^= self.__results[node] if result: self.__state.evolve(np.asarray([[0, 1], [1, 0]]), [node_id]) - else: - pass def _apply_z(self, cmd: Z) -> None: node_id = self.__node_indices.index(cmd.node) @@ -357,8 +355,6 @@ def _apply_z(self, cmd: Z) -> None: result ^= self.__results[node] if result: self.__state.evolve(np.asarray([[1, 0], [0, -1]]), [node_id]) - else: - pass def _apply_c(self, cmd: C) -> None: clifford = C.local_clifford.get_matrix() diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index 32318cf1d..8c7abb876 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -173,14 +173,14 @@ def test_set_output_raises_1(graph: GraphState) -> None: graph.set_output(1) graph.add_physical_node(1) graph.set_meas_basis(1, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) - with pytest.raises(ValueError, match="Cannot set output node with measurement basis."): + with pytest.raises(ValueError, match=r"Cannot set output node with measurement basis."): graph.set_output(1) def test_set_output_raises_2(graph: GraphState) -> None: graph.add_physical_node(1) graph.set_meas_basis(1, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) - with pytest.raises(ValueError, match="Cannot set output node with measurement basis."): + with pytest.raises(ValueError, match=r"Cannot set output node with measurement basis."): graph.set_output(1) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 4af493f0b..a6f67a626 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -98,7 +98,7 @@ def test_local_complement_fails_with_input_node(zx_graph: ZXGraphState) -> None: """Test local complement fails with input node.""" zx_graph.add_physical_node(1) zx_graph.set_input(1) - with pytest.raises(ValueError, match="Cannot apply local complement to input node."): + with pytest.raises(ValueError, match=r"Cannot apply local complement to input node."): zx_graph.local_complement(1) @@ -816,7 +816,7 @@ def test_unremovable_clifford_vertex(zx_graph: ZXGraphState) -> None: (3, Plane.XY, 0.5 * np.pi), ] _apply_measurements(zx_graph, measurements) - with pytest.raises(ValueError, match="This Clifford vertex is unremovable."): + with pytest.raises(ValueError, match=r"This Clifford vertex is unremovable."): zx_graph.remove_clifford(2) @@ -961,11 +961,11 @@ def test_random_graph(zx_graph: ZXGraphState) -> None: {(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)}, ), # a pair of adjacent nodes with YZ measurements - # 4(XY) 4(XY) 4 4 4 - # / \ | | | | - # 1(XY) - 2(YZ) - 3(YZ) - 6(XY) -> 1(XY) - 3(XY) - 2(XY) - 6(XY) - 1 - # \ / | | | | - # 5(XY) 5(XY) 5 5 5 + # 4(XY) 4(XY) 4 4 4 + # / \ | | | | + # 1(XY) - 2(YZ) - 3(YZ) - 6(XY) -> 1(XY) - 3(XY) - 2(XY) - 6(XY) - 1(XY) + # \ / | | | | + # 5(XY) 5(XY) 5 5 5 ( [ (1, Plane.XY, 0.11 * np.pi), From 6d196b56fc0ffa1148e0680ebd5fa78a5e968b6b Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 15 Feb 2025 21:46:58 +0900 Subject: [PATCH 13/23] :pencil2: Fix typo --- graphix_zx/zxgraphstate.py | 2 +- tests/test_zxgraphstate.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index 25e40909a..b394d03fe 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -593,7 +593,7 @@ def merge_yz_nodes(self) -> None: self.set_meas_basis(u, PlannerMeasBasis(Plane.YZ, new_angle)) self.remove_physical_node(v) - def prune_non_clifford(self, atol: float = 1e-9) -> None: + def prune_non_cliffords(self, atol: float = 1e-9) -> None: """Prune non-Clifford vertices from the graph state. Repeat the following steps until there are no non-Clifford vertices: diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index a6f67a626..495321c54 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -1258,7 +1258,7 @@ def test_merge_yz_nodes( ), ], ) -def test_prune_non_clifford( +def test_prune_non_cliffords( zx_graph: ZXGraphState, initial_zxgraph: tuple[range, set[tuple[int, int]]], measurements: list[tuple[int, Plane, float]], @@ -1268,7 +1268,7 @@ def test_prune_non_clifford( _initialize_graph(zx_graph, nodes, edges) exp_measurements, exp_edges, exp_nodes = exp_zxgraph _apply_measurements(zx_graph, measurements) - zx_graph.prune_non_clifford() + zx_graph.prune_non_cliffords() _test(zx_graph, exp_nodes, exp_edges, exp_measurements) From 7e7a73dc13115952c5f40d407e4ae8bd97fac41e Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 15 Feb 2025 23:08:24 +0900 Subject: [PATCH 14/23] :bug: Fix merge_yz_to_xy & merge_yz_nodes --- graphix_zx/zxgraphstate.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index b394d03fe..0fda4cbfb 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -562,10 +562,14 @@ def merge_yz_to_xy(self) -> None: then the node u can be merged into the node v. """ target_candidates = { - u for u, basis in self.meas_bases.items() if (basis.plane == Plane.YZ and len(self.get_neighbors(u)) == 1) + u + for u, basis in self.meas_bases.items() + if (basis.plane == Plane.YZ and len(self.get_neighbors(u)) == 1 and (u not in self.output_nodes)) } target_nodes = { - u for u in target_candidates if self.meas_bases[next(iter(self.get_neighbors(u)))].plane == Plane.XY + u + for u in target_candidates + if any(self.meas_bases[v].plane == Plane.XY for v in self.get_neighbors(u) - self.output_nodes) } for u in target_nodes: v = self.get_neighbors(u).pop() @@ -579,19 +583,27 @@ def merge_yz_nodes(self) -> None: If u, v nodes are measured in the YZ-plane and u, v have the same neighbors, then u, v can be merged into a single node. """ + min_yz_nodes = 2 while True: yz_nodes = {u for u, basis in self.meas_bases.items() if basis.plane == Plane.YZ} - least_nodes = 2 - if len(yz_nodes) < least_nodes: + if len(yz_nodes) < min_yz_nodes: + break + merged = False + for u in sorted(yz_nodes): + for v in sorted(yz_nodes - {u}): + if self.get_neighbors(u) != self.get_neighbors(v): + continue + + new_angle = (self.meas_bases[u].angle + self.meas_bases[v].angle) % (2.0 * np.pi) + self.set_meas_basis(u, PlannerMeasBasis(Plane.YZ, new_angle)) + self.remove_physical_node(v) + + merged = True + break + if merged: + break + if not merged: break - u = yz_nodes.pop() - for v in yz_nodes: - if u > v or self.get_neighbors(u) != self.get_neighbors(v): - continue - - new_angle = (self.meas_bases[u].angle + self.meas_bases[v].angle) % (2.0 * np.pi) - self.set_meas_basis(u, PlannerMeasBasis(Plane.YZ, new_angle)) - self.remove_physical_node(v) def prune_non_cliffords(self, atol: float = 1e-9) -> None: """Prune non-Clifford vertices from the graph state. From fdf0aa4377fc29a5f16eac472dc2770271de352e Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 15 Feb 2025 23:08:54 +0900 Subject: [PATCH 15/23] :sparkles: Add get_random_gflow_circ --- graphix_zx/random_objects.py | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/graphix_zx/random_objects.py b/graphix_zx/random_objects.py index b5a368c50..7e9d44984 100644 --- a/graphix_zx/random_objects.py +++ b/graphix_zx/random_objects.py @@ -10,6 +10,7 @@ import numpy as np +from graphix_zx.circuit import MBQCCircuit from graphix_zx.common import default_meas_basis from graphix_zx.graphstate import GraphState @@ -82,3 +83,50 @@ def get_random_flow_graph( num_nodes += 1 return graph, flow + + +def get_random_gflow_circ( + width: int, + depth: int, + rng: np.random.Generator | None = None, + edge_p: float = 0.5, + angle_list: list | None = None, +) -> MBQCCircuit: + """Generate a random MBQC circuit which has gflow. + + Parameters + ---------- + width : int + circuit width + depth : int + circuit depth + rng : np.random.Generator, optional + random number generator, by default np.random.default_rng() + edge_p : float, optional + probability of adding CZ gate, by default 0.5 + angle_list : list, optional + list of angles, by default [0, np.pi / 3, 2 * np.pi / 3, np.pi] + + Returns + ------- + MBQCCircuit + generated MBQC circuit + """ + if rng is None: + rng = np.random.default_rng() + if angle_list is None: + angle_list = [0, np.pi / 3, 2 * np.pi / 3, np.pi] + circ = MBQCCircuit(width) + for d in range(depth): + for j in range(width): + circ.j(j, rng.choice(angle_list)) + if d < depth - 1: + for j in range(width): + if rng.random() < edge_p: + circ.cz(j, (j + 1) % width) + num = rng.integers(0, width) + if num > 0: + target = set(rng.choice(list(range(width)), num)) + circ.phase_gadget(target, rng.choice(angle_list)) + + return circ From 9a7c3a75c1c9048308d8d6fbffc7d189c63c6d4b Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 15 Feb 2025 23:09:32 +0900 Subject: [PATCH 16/23] :sparkles: Add an example for Clifford/non-Clifford removal --- .../measurement_pattern_simplification.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 examples/measurement_pattern_simplification.py diff --git a/examples/measurement_pattern_simplification.py b/examples/measurement_pattern_simplification.py new file mode 100644 index 000000000..5f5dc32b6 --- /dev/null +++ b/examples/measurement_pattern_simplification.py @@ -0,0 +1,42 @@ +"""Basic example of simplifying a measurement pattern via a ZX-diagram simplification. + +By using the `prune_non_cliffords` method, +we can remove certain Clifford nodes and non-Clifford nodes from the ZX-diagram, +which can simplify the resulting measurement pattern. +""" + +# %% +from copy import deepcopy + +import numpy as np + +from graphix_zx.circuit import circuit2graph +from graphix_zx.random_objects import get_random_gflow_circ +from graphix_zx.visualizer import visualize +from graphix_zx.zxgraphstate import ZXGraphState + +# %% +circ = get_random_gflow_circ(4, 4, angle_list=[0, np.pi / 3, 2 * np.pi / 3, np.pi]) +graph, flow = circuit2graph(circ) +zx_graph = ZXGraphState() +zx_graph.append(graph) + +visualize(zx_graph) +print("node | plane | angle (/pi)") +for node in zx_graph.input_nodes: + print(f"{node} (input)", zx_graph.meas_bases[node].plane, zx_graph.meas_bases[node].angle / np.pi) +for node in zx_graph.physical_nodes - zx_graph.input_nodes - zx_graph.output_nodes: + print(node, zx_graph.meas_bases[node].plane, zx_graph.meas_bases[node].angle / np.pi) + +# %% +zx_graph_smp = deepcopy(zx_graph) +zx_graph_smp.prune_non_cliffords() + +visualize(zx_graph_smp) +print("node | plane | angle (/pi)") +for node in zx_graph.input_nodes: + print(f"{node} (input)", zx_graph.meas_bases[node].plane, zx_graph.meas_bases[node].angle / np.pi) +for node in zx_graph_smp.physical_nodes - zx_graph.input_nodes - zx_graph_smp.output_nodes: + print(node, zx_graph_smp.meas_bases[node].plane, zx_graph_smp.meas_bases[node].angle / np.pi) + +# %% From d8520fe713a613dc7f029a2578b8850870a5f4a7 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 24 Feb 2025 21:12:53 +0900 Subject: [PATCH 17/23] :bug: Fix _angle_check --- graphix_zx/euler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix_zx/euler.py b/graphix_zx/euler.py index ecab91f7d..75309f30f 100644 --- a/graphix_zx/euler.py +++ b/graphix_zx/euler.py @@ -243,7 +243,7 @@ def _angle_check(cls, alpha: float, beta: float, gamma: float, atol: float = 1e- ValueError if any of the angles is not a Clifford angle """ - if not any(is_clifford_angle(angle, atol=atol) for angle in [alpha, beta, gamma]): + if not all(is_clifford_angle(angle, atol=atol) for angle in [alpha, beta, gamma]): msg = "The angles must be multiples of pi/2" raise ValueError(msg) From eab57879f4994551dd19dd856f8df5c187a70fd6 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 24 Feb 2025 21:15:31 +0900 Subject: [PATCH 18/23] :bug: Fix test_local_complement in test_euler.py --- tests/test_euler.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_euler.py b/tests/test_euler.py index 09e832f6e..6a7bf44f0 100644 --- a/tests/test_euler.py +++ b/tests/test_euler.py @@ -173,15 +173,15 @@ def test_lc_basis_update( def test_local_complement_target_update(plane: Plane, rng: np.random.Generator) -> None: lc = LocalClifford(0, np.pi / 2, 0) measurement_action: dict[Plane, tuple[Plane, Callable[[float], float]]] = { - Plane.XY: (Plane.XZ, lambda angle: angle + np.pi / 2), - Plane.XZ: (Plane.XY, lambda angle: np.pi / 2 - angle), - Plane.YZ: (Plane.YZ, lambda angle: angle + np.pi / 2), + Plane.XY: (Plane.XZ, lambda angle: -angle + np.pi / 2), + Plane.XZ: (Plane.XY, lambda angle: angle - np.pi / 2), + Plane.YZ: (Plane.YZ, lambda angle: angle - np.pi / 2), } angle = rng.random() * 2 * np.pi meas_basis = PlannerMeasBasis(plane, angle) - result_basis = update_lc_basis(lc.conjugate(), meas_basis) + result_basis = update_lc_basis(lc, meas_basis) ref_plane, ref_angle_func = measurement_action[plane] ref_angle = ref_angle_func(angle) @@ -193,15 +193,15 @@ def test_local_complement_target_update(plane: Plane, rng: np.random.Generator) def test_local_complement_neighbors(plane: Plane, rng: np.random.Generator) -> None: lc = LocalClifford(-np.pi / 2, 0, 0) measurement_action: dict[Plane, tuple[Plane, Callable[[float], float]]] = { - Plane.XY: (Plane.XY, lambda angle: angle + np.pi / 2), - Plane.XZ: (Plane.YZ, lambda angle: angle), - Plane.YZ: (Plane.XZ, lambda angle: -1 * angle), + Plane.XY: (Plane.XY, lambda angle: angle - np.pi / 2), + Plane.XZ: (Plane.YZ, lambda angle: -1 * angle), + Plane.YZ: (Plane.XZ, lambda angle: angle), } angle = rng.random() * 2 * np.pi meas_basis = PlannerMeasBasis(plane, angle) - result_basis = update_lc_basis(lc.conjugate(), meas_basis) + result_basis = update_lc_basis(lc, meas_basis) ref_plane, ref_angle_func = measurement_action[plane] ref_angle = ref_angle_func(angle) From 0c25e6093a49739d1332aebcd8a844e15edbef46 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 24 Feb 2025 21:30:20 +0900 Subject: [PATCH 19/23] :bug: Fix update rule --- tests/test_euler.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_euler.py b/tests/test_euler.py index 6a7bf44f0..1407ab946 100644 --- a/tests/test_euler.py +++ b/tests/test_euler.py @@ -173,15 +173,15 @@ def test_lc_basis_update( def test_local_complement_target_update(plane: Plane, rng: np.random.Generator) -> None: lc = LocalClifford(0, np.pi / 2, 0) measurement_action: dict[Plane, tuple[Plane, Callable[[float], float]]] = { - Plane.XY: (Plane.XZ, lambda angle: -angle + np.pi / 2), - Plane.XZ: (Plane.XY, lambda angle: angle - np.pi / 2), - Plane.YZ: (Plane.YZ, lambda angle: angle - np.pi / 2), + Plane.XY: (Plane.XZ, lambda angle: angle + np.pi / 2), + Plane.XZ: (Plane.XY, lambda angle: -angle + np.pi / 2), + Plane.YZ: (Plane.YZ, lambda angle: angle + np.pi / 2), } angle = rng.random() * 2 * np.pi meas_basis = PlannerMeasBasis(plane, angle) - result_basis = update_lc_basis(lc, meas_basis) + result_basis = update_lc_basis(lc.conjugate(), meas_basis) ref_plane, ref_angle_func = measurement_action[plane] ref_angle = ref_angle_func(angle) @@ -193,15 +193,15 @@ def test_local_complement_target_update(plane: Plane, rng: np.random.Generator) def test_local_complement_neighbors(plane: Plane, rng: np.random.Generator) -> None: lc = LocalClifford(-np.pi / 2, 0, 0) measurement_action: dict[Plane, tuple[Plane, Callable[[float], float]]] = { - Plane.XY: (Plane.XY, lambda angle: angle - np.pi / 2), - Plane.XZ: (Plane.YZ, lambda angle: -1 * angle), - Plane.YZ: (Plane.XZ, lambda angle: angle), + Plane.XY: (Plane.XY, lambda angle: angle + np.pi / 2), + Plane.XZ: (Plane.YZ, lambda angle: angle), + Plane.YZ: (Plane.XZ, lambda angle: -angle), } angle = rng.random() * 2 * np.pi meas_basis = PlannerMeasBasis(plane, angle) - result_basis = update_lc_basis(lc, meas_basis) + result_basis = update_lc_basis(lc.conjugate(), meas_basis) ref_plane, ref_angle_func = measurement_action[plane] ref_angle = ref_angle_func(angle) From 39cb744f20c8f90e2544cc4ec4292fd99319dfc9 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 28 Feb 2025 18:47:47 +0900 Subject: [PATCH 20/23] :bug: Add property method --- graphix_zx/graphstate.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index c48d54686..188cbc9ab 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -400,6 +400,28 @@ def local_cliffords(self) -> dict[int, LocalClifford]: """ return self.__local_cliffords + @property + def inner2nodes(self) -> dict[int, int]: + """Return inner index to node index mapping. + + Returns + ------- + dict[int, int] + inner index to node index mapping. + """ + return self.__inner2nodes + + @property + def nodes2inner(self) -> dict[int, int]: + """Return node index to inner index mapping. + + Returns + ------- + dict[int, int] + node index to inner index mapping. + """ + return self.__nodes2inner + def check_meas_basis(self) -> None: """Check if the measurement basis is set for all physical nodes except output nodes. From 6cb9263d7f1c6428cac03d0848604ef140bdabe0 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 28 Feb 2025 18:57:51 +0900 Subject: [PATCH 21/23] :art: Apply ruff --- tests/test_euler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_euler.py b/tests/test_euler.py index 1407ab946..5268e49b6 100644 --- a/tests/test_euler.py +++ b/tests/test_euler.py @@ -1,3 +1,4 @@ +import operator from typing import TYPE_CHECKING import numpy as np @@ -195,7 +196,7 @@ def test_local_complement_neighbors(plane: Plane, rng: np.random.Generator) -> N measurement_action: dict[Plane, tuple[Plane, Callable[[float], float]]] = { Plane.XY: (Plane.XY, lambda angle: angle + np.pi / 2), Plane.XZ: (Plane.YZ, lambda angle: angle), - Plane.YZ: (Plane.XZ, lambda angle: -angle), + Plane.YZ: (Plane.XZ, operator.neg), } angle = rng.random() * 2 * np.pi From 0e7357ac65953b1b4a8f40a10140bd46b4232fb5 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 28 Feb 2025 18:58:30 +0900 Subject: [PATCH 22/23] :white_check_mark: Add pivot tests for euler.py --- tests/test_euler.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/test_euler.py b/tests/test_euler.py index 5268e49b6..bd2eb740a 100644 --- a/tests/test_euler.py +++ b/tests/test_euler.py @@ -208,3 +208,43 @@ def test_local_complement_neighbors(plane: Plane, rng: np.random.Generator) -> N assert result_basis.plane == ref_plane assert _is_close_angle(result_basis.angle, ref_angle) + + +@pytest.mark.parametrize("plane", [Plane.XY, Plane.YZ, Plane.XZ]) +def test_pivot_target_update(plane: Plane, rng: np.random.Generator) -> None: + lc = LocalClifford(np.pi / 2, np.pi / 2, np.pi / 2) + measurement_action: dict[Plane, tuple[Plane, Callable[[float], float]]] = { + Plane.XY: (Plane.YZ, operator.neg), + Plane.XZ: (Plane.XZ, lambda angle: -angle + np.pi / 2), + Plane.YZ: (Plane.XY, operator.neg), + } + + angle = rng.random() * 2 * np.pi + + meas_basis = PlannerMeasBasis(plane, angle) + result_basis = update_lc_basis(lc.conjugate(), meas_basis) + ref_plane, ref_angle_func = measurement_action[plane] + ref_angle = ref_angle_func(angle) + + assert result_basis.plane == ref_plane + assert _is_close_angle(result_basis.angle, ref_angle) + + +@pytest.mark.parametrize("plane", [Plane.XY, Plane.YZ, Plane.XZ]) +def test_pivot_neighbors(plane: Plane, rng: np.random.Generator) -> None: + lc = LocalClifford(np.pi, 0, 0) + measurement_action: dict[Plane, tuple[Plane, Callable[[float], float]]] = { + Plane.XY: (Plane.XY, lambda angle: angle + np.pi), + Plane.XZ: (Plane.XZ, operator.neg), + Plane.YZ: (Plane.YZ, operator.neg), + } + + angle = rng.random() * 2 * np.pi + + meas_basis = PlannerMeasBasis(plane, angle) + result_basis = update_lc_basis(lc.conjugate(), meas_basis) + ref_plane, ref_angle_func = measurement_action[plane] + ref_angle = ref_angle_func(angle) + + assert result_basis.plane == ref_plane + assert _is_close_angle(result_basis.angle, ref_angle) From 1ec34c6891c090ac18c520233a7188c1fefb2628 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 12 May 2025 18:44:50 +0900 Subject: [PATCH 23/23] add circuit_extraction.py --- graphix_zx/circuit_extraction.py | 161 +++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 graphix_zx/circuit_extraction.py diff --git a/graphix_zx/circuit_extraction.py b/graphix_zx/circuit_extraction.py new file mode 100644 index 000000000..ee8c6a3e2 --- /dev/null +++ b/graphix_zx/circuit_extraction.py @@ -0,0 +1,161 @@ +from typing import Set, List, Tuple + +import numpy as np + +from graphix_zx.zxgraphstate import ZXGraphState +from graphix_zx.circuit import MBQCCircuit, circuit2graph +from graphix_zx.gates import CZ, CNOT, H, PhaseGadget + + +def extract_circuit_graph_state(d: ZXGraphState) -> ZXGraphState: + """ + Extract a circuit from a ZXGraphState in MBQC+LC form with gflow and return + the corresponding circuit-like graph state (as ZXGraphState). + + Implements Algorithm 2 (Circuit Extraction) from "There and back again" (Appendix D). + """ + # Step 0: Bring into phase-gadget form + d.convert_to_phase_gadget() + + # Initialize circuit and frontier + inputs = sorted(d.input_nodes) + n_qubits = len(inputs) + circ = MBQCCircuit(n_qubits) + frontier: Set[int] = set(d.output_nodes) + + # Process initial frontier + _process_frontier(d, frontier, circ) + + # Main extraction loop + while True: + # Remaining unextracted vertices + remaining = set(d.physical_nodes) - frontier + if not remaining: + break + _update_frontier(d, frontier, circ) + + # Final SWAP/H corrections if needed + _finalize_extraction(d, frontier, circ) + _revert_circuit(d, circ) + + # Convert MBQCCircuit back to ZXGraphState + # graph, _ = circuit2graph(circ) + # return graph + + return circ + + +def _process_frontier(d: ZXGraphState, frontier: Set[int], circ: MBQCCircuit) -> None: + """ + Process the frontier: extract local Cliffords and CZ between frontier vertices. + """ + lc = d.local_clifford + for v in sorted(frontier): + # Extract any local Clifford on v + if v in lc.keys(): + # to be implemented: add local Clifford gates + pass + # Extract any CZ edges between frontier vertices + for w in list(d.get_neighbors(v) & frontier): + circ.cz(v, w) + d.remove_physical_edge(v, w) + + +def _update_frontier(d: ZXGraphState, frontier: Set[int], circ: MBQCCircuit) -> None: + """ + Update the frontier by Gaussian elimination or pivots, then extract new frontier vertices. + """ + # Build bipartite adjacency: frontier vs neighbors + neigh = sorted(set().union(*(d.get_neighbors(v) for v in frontier))) + M = np.zeros((len(frontier), len(neigh)), dtype=int) + for i, v in enumerate(sorted(frontier)): + for j, u in enumerate(neigh): + if u in d.get_neighbors(v): + M[i, j] = 1 + # Gaussian eliminate over GF(2) + M_red, row_ops = _gauss_elim(M) + # Identify rows with single 1 + vs: List[int] = [] + for i, row in enumerate(M_red): + if row.sum() == 1: + col = int(np.nonzero(row)[0][0]) + vs.append(neigh[col]) + + if not vs: + # Step 4: pivot YZ vertices adjacent to frontier + # to be implemented + pass + # for u in list(d.physical_nodes - frontier): + # if d.meas_bases[u].plane.name == 'YZ' and d.get_neighbors(u) & frontier: + # w = next(iter(d.get_neighbors(u) & frontier)) + # d.pivot(u, w) + # _process_frontier(d, frontier, circ) + # return + + # Apply recorded CNOT row operations + for r1, r2 in row_ops: + circ.cnot(sorted(frontier)[r1], sorted(frontier)[r2]) # CNOT is not implemented in MBQCCircuit + # Update graph accordingly: add edge or local complement as needed + d.apply_cnot(sorted(frontier)[r1], sorted(frontier)[r2]) + + # Extract new frontier vertices + for v in vs: + # unique neighbor in frontier + w = next(iter(d.get_neighbors(v) & frontier)) + circ.add_gate(H(), [w]) + circ.add_gate(PhaseGadget(d.meas_bases[v].angle), [w]) + frontier.remove(w) + frontier.add(v) + _process_frontier(d, frontier, circ) + + +def _gauss_elim(M: np.ndarray) -> Tuple[np.ndarray, List[Tuple[int,int]]]: + """ + Perform Gaussian elimination over GF(2), returning reduced matrix and list of row ops. + """ + M = M.copy() % 2 + n, m = M.shape + row_ops: List[Tuple[int,int]] = [] + pivot_row = 0 + for col in range(m): + # find pivot + for r in range(pivot_row, n): + if M[r, col] == 1: + M[[pivot_row, r]] = M[[r, pivot_row]] + if r != pivot_row: + row_ops.append((pivot_row, r)) + break + else: + continue + # eliminate other rows + for r in range(n): + if r != pivot_row and M[r, col] == 1: + M[r] ^= M[pivot_row] + row_ops.append((r, pivot_row)) + pivot_row += 1 + if pivot_row == n: + break + return M, row_ops + + +def _finalize_extraction(d: ZXGraphState, frontier: Set[int], circ: MBQCCircuit) -> None: + """ + Extract final Hadamards or SWAPs to align frontier to inputs. + """ + # to be implemented + + # # Handle any remaining Hadamard on outputs + # for v in sorted(frontier): + # if d.has_hadamard_on_output(v): + # circ.add_gate(H(), [v]) + # # Permute frontier to match inputs via SWAPs + # perm = d.compute_permutation(list(frontier), list(d.input_nodes)) + # for (q1, q2) in perm: + # circ.add_gate(CNOT(), [q1, q2]) # SWAP as three CNOTs omitted for brevity + +def _revert_circuit(d: ZXGraphState, circ: MBQCCircuit) -> None: + """ + Revert the circuit. + """ + # to be implemented + pass \ No newline at end of file