From 59a4b49f07cb467a01a2ef79a0ec22ba5d4bdfe7 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 17:52:17 +0900 Subject: [PATCH 01/94] :sparkles: Add ZXGraphState --- graphix_zx/zxgraphstate.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 graphix_zx/zxgraphstate.py diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py new file mode 100644 index 000000000..3115978e8 --- /dev/null +++ b/graphix_zx/zxgraphstate.py @@ -0,0 +1,35 @@ +"""ZXGraph State classes for Measurement-based Quantum Computing. + +This module provides: +- ZXGraphState: Graph State for the ZX-calculus. +- bipartite_edges: Return a set of edges for the complete bipartite graph between two sets of nodes. +""" + +from __future__ import annotations + +from graphix_zx.graphstate import GraphState + + +class ZXGraphState(GraphState): + r"""Graph State for the ZX-calculus. + + Attributes + ---------- + input_nodes : `set`\[`int`\] + set of input nodes + output_nodes : `set`\[`int`\] + set of output nodes + physical_nodes : `set`\[`int`\] + set of physical nodes + physical_edges : `set`\[`tuple`\[`int`, `int`\]\] + physical edges + meas_bases : `dict`\[`int`, `MeasBasis`\] + measurement bases + q_indices : `dict`\[`int`, `int`\] + qubit indices + local_cliffords : `dict`\[`int`, `LocalClifford`\] + local clifford operators + """ + + def __init__(self) -> None: + super().__init__() From d2172a979ccdac57e68a5fce41b16ee14164cb07 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 17:53:34 +0900 Subject: [PATCH 02/94] :sparkles: Implement local_complement --- graphix_zx/zxgraphstate.py | 81 ++++++++- tests/test_zxgraphstate.py | 326 +++++++++++++++++++++++++++++++++++++ 2 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 tests/test_zxgraphstate.py diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index 3115978e8..c55b220d8 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -7,7 +7,19 @@ from __future__ import annotations -from graphix_zx.graphstate import GraphState +import operator +from collections import defaultdict +from itertools import combinations +from typing import TYPE_CHECKING + +import numpy as np + +from graphix_zx.common import Plane, PlannerMeasBasis, is_clifford_angle, is_close_angle +from graphix_zx.euler import LocalClifford +from graphix_zx.graphstate import BaseGraphState, GraphState, bipartite_edges + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable class ZXGraphState(GraphState): @@ -33,3 +45,70 @@ class ZXGraphState(GraphState): def __init__(self) -> None: super().__init__() + + def _update_connections(self, rmv_edges: Iterable[tuple[int, int]], new_edges: Iterable[tuple[int, int]]) -> None: + r"""Update the physical edges of the graph state. + + Parameters + ---------- + rmv_edges : `Iterable`\[`tuple`\[`int`, `int`\]\] + edges to remove + new_edges : `Iterable`\[`tuple`\[`int`, `int`\]\] + edges to add + """ + for edge in rmv_edges: + self.remove_physical_edge(edge[0], edge[1]) + for edge in new_edges: + self.add_physical_edge(edge[0], edge[1]) + + def local_complement(self, node: int) -> None: + r"""Local complement operation on the graph state: G*u. + + Non-input node u gets Rx(pi/2) and its neighbors get Rz(-pi/2). + The edges between the neighbors of u are complemented. + + Parameters + ---------- + node : `int` + node index. The node must not be an input node. + + Raises + ------ + ValueError + If the node does not exist, is an input node, or the graph is not a ZX-diagram. + """ + self._ensure_node_exists(node) + if node in self.input_node_indices: + msg = "Cannot apply local complement to input node." + raise ValueError(msg) + self._check_meas_basis() + + nbrs: set[int] = self.neighbors(node) + nbr_pairs = complete_graph_edges(nbrs) + new_edges = nbr_pairs - self.physical_edges + rmv_edges = self.physical_edges & nbr_pairs + + self._update_connections(rmv_edges, new_edges) + + # apply local clifford to node and its neighbors + lc = LocalClifford(0, np.pi / 2, 0) + self.apply_local_clifford(node, lc) + lc = LocalClifford(-np.pi / 2, 0, 0) + for v in nbrs: + self.apply_local_clifford(v, lc) + + +def complete_graph_edges(nodes: Iterable[int]) -> set[tuple[int, int]]: + r"""Return a set of edges for the complete graph on the given nodes. + + Parameters + ---------- + nodes : `Iterable`\[`int`\] + nodes + + Returns + ------- + `set`\[`tuple`\[`int`, `int`\]\] + edges of the complete graph + """ + return {tuple(sorted((u, v))) for u, v in combinations(nodes, 2)} diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py new file mode 100644 index 000000000..0f6a9526a --- /dev/null +++ b/tests/test_zxgraphstate.py @@ -0,0 +1,326 @@ +"""Tests for ZXGraphState + +Measurement actions for the followings are used: + - Local complement (LC): MEAS_ACTION_LC_* + - Pivot (PV): MEAS_ACTION_PV_* + - Remove Cliffords (RC): MEAS_ACTION_RC + +Reference: + M. Backens et al., Quantum 5, 421 (2021). + https://doi.org/10.22331/q-2021-03-25-421 +""" + +from __future__ import annotations + +import itertools +import operator +from copy import deepcopy +from typing import TYPE_CHECKING + +import numpy as np +import pytest + +from graphix_zx.common import Plane, PlannerMeasBasis, is_close_angle +from graphix_zx.random_objects import generate_random_flow_graph +from graphix_zx.zxgraphstate import ZXGraphState + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Callable + + Measurements = list[tuple[int, PlannerMeasBasis]] + +MEAS_ACTION_LC_TARGET: 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), +} +MEAS_ACTION_LC_NEIGHBORS: 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, operator.neg), +} +MEAS_ACTION_PV_TARGET: dict[Plane, tuple[Plane, Callable[[float], float]]] = { + Plane.XY: (Plane.YZ, operator.neg), + Plane.XZ: (Plane.XZ, lambda angle: (np.pi / 2 - angle)), + Plane.YZ: (Plane.XY, operator.neg), +} +MEAS_ACTION_PV_NEIGHBORS: dict[Plane, tuple[Plane, Callable[[float], float]]] = { + Plane.XY: (Plane.XY, lambda angle: (angle + np.pi) % (2.0 * np.pi)), + Plane.XZ: (Plane.XZ, lambda angle: -angle % (2.0 * np.pi)), + Plane.YZ: (Plane.YZ, lambda angle: -angle % (2.0 * np.pi)), +} +ATOL = 1e-9 +MEAS_ACTION_RC: dict[Plane, tuple[Plane, Callable[[float, float], float]]] = { + Plane.XY: ( + Plane.XY, + lambda a_pi, alpha: (alpha if is_close_angle(a_pi, 0, ATOL) else alpha + np.pi) % (2.0 * np.pi), + ), + Plane.XZ: ( + Plane.XZ, + lambda a_pi, alpha: (alpha if is_close_angle(a_pi, 0, ATOL) else -alpha) % (2.0 * np.pi), + ), + Plane.YZ: ( + Plane.YZ, + lambda a_pi, alpha: (alpha if is_close_angle(a_pi, 0, ATOL) else -alpha) % (2.0 * np.pi), + ), +} + + +def plane_combinations(n: int) -> list[tuple[Plane, ...]]: + """Generate all combinations of planes of length n. + + Parameters + ---------- + n : int + The length of the combinations. n > 1. + + Returns + ------- + list[tuple[Plane, ...]] + A list of tuples containing all combinations of planes of length n. + """ + return list(itertools.product(Plane, repeat=n)) + + +@pytest.fixture +def rng() -> np.random.Generator: + return np.random.default_rng() + + +@pytest.fixture +def zx_graph() -> ZXGraphState: + """Generate an empty ZXGraphState object. + + Returns + ------- + ZXGraphState: An empty ZXGraphState object. + """ + return ZXGraphState() + + +def _initialize_graph( + zx_graph: ZXGraphState, + nodes: range, + edges: set[tuple[int, int]], + inputs: Sequence[int] = (), + outputs: Sequence[int] = (), +) -> None: + """Initialize a ZXGraphState object with the given nodes and edges. + + Parameters + ---------- + zx_graph : ZXGraphState + The ZXGraphState object to initialize. + nodes : range + nodes to add to the graph. + edges : list[tuple[int, int]] + edges to add to the graph. + inputs : Sequence[int], optional + input nodes, by default (). + outputs : Sequence[int], optional + output nodes, by default (). + + Raises + ------ + ValueError + If the number of output nodes is greater than the number of input nodes. + """ + for _ in nodes: + zx_graph.add_physical_node() + for i, j in edges: + zx_graph.add_physical_edge(i, j) + + node2q_index: dict[int, int] = {} + q_indices: list[int] = [] + + for node in inputs: + q_index = zx_graph.register_input(node) + node2q_index[node] = q_index + q_indices.append(q_index) + + if len(outputs) > len(q_indices): + msg = "Cannot assign valid q_index to all output nodes." + raise ValueError(msg) + + for idx, node in enumerate(outputs): + q_index = node2q_index.get(node, idx) + zx_graph.register_output(node, q_index) + + +def _apply_measurements(zx_graph: ZXGraphState, measurements: Measurements) -> None: + for node_id, planner_meas_basis in measurements: + if node_id in zx_graph.output_node_indices: + continue + zx_graph.assign_meas_basis(node_id, planner_meas_basis) + + +def _test( + zx_graph: ZXGraphState, + exp_nodes: set[int], + exp_edges: set[tuple[int, int]], + exp_measurements: Measurements, +) -> None: + assert zx_graph.physical_nodes == exp_nodes + assert zx_graph.physical_edges == exp_edges + for node_id, planner_meas_basis in exp_measurements: + assert zx_graph.meas_bases[node_id].plane == planner_meas_basis.plane + assert is_close_angle(zx_graph.meas_bases[node_id].angle, planner_meas_basis.angle) + + +def test_local_complement_fails_if_nonexistent_node(zx_graph: ZXGraphState) -> None: + """Test local complement raises an error if the node does not exist.""" + with pytest.raises(ValueError, match="Node does not exist node=0"): + zx_graph.local_complement(0) + + +def test_local_complement_fails_if_not_zx_graph(zx_graph: ZXGraphState) -> None: + """Test local complement raises an error if the graph is not a ZX-diagram.""" + zx_graph.add_physical_node() + with pytest.raises(ValueError, match="Measurement basis not set for node 0"): + zx_graph.local_complement(0) + + +def test_local_complement_fails_with_input_node(zx_graph: ZXGraphState) -> None: + """Test local complement fails with input node.""" + zx_graph.add_physical_node() + zx_graph.register_input(0) + with pytest.raises(ValueError, match=r"Cannot apply local complement to input node."): + zx_graph.local_complement(0) + + +@pytest.mark.parametrize("plane", list(Plane)) +def test_local_complement_with_no_edge(zx_graph: ZXGraphState, plane: Plane, rng: np.random.Generator) -> None: + """Test local complement with a graph with a single node and no edges.""" + angle = rng.random() * 2 * np.pi + ref_plane, ref_angle_func = MEAS_ACTION_LC_TARGET[plane] + ref_angle = ref_angle_func(angle) + zx_graph.add_physical_node() + zx_graph.assign_meas_basis(0, PlannerMeasBasis(plane, angle)) + + zx_graph.local_complement(0) + assert zx_graph.physical_edges == set() + assert zx_graph.meas_bases[0].plane == ref_plane + assert is_close_angle(zx_graph.meas_bases[0].angle, ref_angle) + + +@pytest.mark.parametrize("planes", plane_combinations(3)) +def test_local_complement_with_minimal_graph( + zx_graph: ZXGraphState, planes: tuple[Plane, Plane, Plane], rng: np.random.Generator +) -> None: + """Test local complement with a minimal graph.""" + for _ in range(3): + zx_graph.add_physical_node() + for i, j in [(0, 1), (1, 2)]: + zx_graph.add_physical_edge(i, j) + angles = [rng.random() * 2 * np.pi for _ in range(3)] + for i in range(3): + zx_graph.assign_meas_basis(i, PlannerMeasBasis(planes[i], angles[i])) + zx_graph.local_complement(1) + ref_plane0, ref_angle_func0 = MEAS_ACTION_LC_NEIGHBORS[planes[0]] + ref_plane1, ref_angle_func1 = MEAS_ACTION_LC_TARGET[planes[1]] + ref_plane2, ref_angle_func2 = MEAS_ACTION_LC_NEIGHBORS[planes[2]] + ref_angle0 = ref_angle_func0(angles[0]) + ref_angle1 = ref_angle_func1(angles[1]) + ref_angle2 = ref_angle_func2(angles[2]) + exp_measurements = [ + (0, PlannerMeasBasis(ref_plane0, ref_angle0)), + (1, PlannerMeasBasis(ref_plane1, ref_angle1)), + (2, PlannerMeasBasis(ref_plane2, ref_angle2)), + ] + _test(zx_graph, exp_nodes={0, 1, 2}, exp_edges={(0, 1), (0, 2), (1, 2)}, exp_measurements=exp_measurements) + + zx_graph.local_complement(1) + ref_plane0, ref_angle_func0 = MEAS_ACTION_LC_NEIGHBORS[ref_plane0] + ref_plane1, ref_angle_func1 = MEAS_ACTION_LC_TARGET[ref_plane1] + ref_plane2, ref_angle_func2 = MEAS_ACTION_LC_NEIGHBORS[ref_plane2] + exp_measurements = [ + (0, PlannerMeasBasis(ref_plane0, ref_angle_func0(ref_angle0))), + (1, PlannerMeasBasis(ref_plane1, ref_angle_func1(ref_angle1))), + (2, PlannerMeasBasis(ref_plane2, ref_angle_func2(ref_angle2))), + ] + _test( + zx_graph, + exp_nodes={0, 1, 2}, + exp_edges={(0, 1), (1, 2)}, + exp_measurements=exp_measurements, + ) + + +@pytest.mark.parametrize(("plane1", "plane3"), plane_combinations(2)) +def test_local_complement_on_output_node( + zx_graph: ZXGraphState, plane1: Plane, plane3: Plane, rng: np.random.Generator +) -> None: + """Test local complement on an output node.""" + _initialize_graph(zx_graph, nodes=range(5), edges={(0, 1), (1, 2), (2, 3), (3, 4)}, inputs=(0, 4), outputs=(2,)) + angle1 = rng.random() * 2 * np.pi + angle3 = rng.random() * 2 * np.pi + measurements = [ + (0, PlannerMeasBasis(Plane.XY, 0.0)), + (1, PlannerMeasBasis(plane1, angle1)), + (3, PlannerMeasBasis(plane3, angle3)), + (4, PlannerMeasBasis(Plane.XY, 0.0)), + ] + _apply_measurements(zx_graph, measurements) + zx_graph.local_complement(2) + + ref_plane1, ref_angle_func1 = MEAS_ACTION_LC_NEIGHBORS[plane1] + ref_plane3, ref_angle_func3 = MEAS_ACTION_LC_NEIGHBORS[plane3] + exp_measurements = [ + (1, PlannerMeasBasis(ref_plane1, ref_angle_func1(measurements[1][1].angle))), + (3, PlannerMeasBasis(ref_plane3, ref_angle_func3(measurements[2][1].angle))), + ] + _test( + zx_graph, + exp_nodes={0, 1, 2, 3, 4}, + exp_edges={(0, 1), (1, 2), (1, 3), (2, 3), (3, 4)}, + exp_measurements=exp_measurements, + ) + assert zx_graph.meas_bases.get(2) is None + + +@pytest.mark.parametrize(("plane0", "plane1"), plane_combinations(2)) +def test_local_complement_with_two_nodes_graph( + zx_graph: ZXGraphState, plane0: Plane, plane1: Plane, rng: np.random.Generator +) -> None: + """Test local complement with a graph with two nodes.""" + zx_graph.add_physical_node() + zx_graph.add_physical_node() + zx_graph.add_physical_edge(0, 1) + angle0 = rng.random() * 2 * np.pi + angle1 = rng.random() * 2 * np.pi + zx_graph.assign_meas_basis(0, PlannerMeasBasis(plane0, angle0)) + zx_graph.assign_meas_basis(1, PlannerMeasBasis(plane1, angle1)) + zx_graph.local_complement(0) + + ref_plane0, ref_angle_func0 = MEAS_ACTION_LC_TARGET[plane0] + ref_plane1, ref_angle_func1 = MEAS_ACTION_LC_NEIGHBORS[plane1] + exp_measurements = [ + (0, PlannerMeasBasis(ref_plane0, ref_angle_func0(angle0))), + (1, PlannerMeasBasis(ref_plane1, ref_angle_func1(angle1))), + ] + _test(zx_graph, exp_nodes={0, 1}, exp_edges={(0, 1)}, exp_measurements=exp_measurements) + + +@pytest.mark.parametrize("planes", plane_combinations(3)) +def test_local_complement_4_times( + zx_graph: ZXGraphState, planes: tuple[Plane, Plane, Plane], rng: np.random.Generator +) -> None: + """Test 4 times local complement returns to the original graph.""" + for _ in range(3): + zx_graph.add_physical_node() + for i, j in [(0, 1), (1, 2)]: + zx_graph.add_physical_edge(i, j) + angles = [rng.random() * 2 * np.pi for _ in range(3)] + for i in range(3): + zx_graph.assign_meas_basis(i, PlannerMeasBasis(planes[i], angles[i])) + + for _ in range(4): + zx_graph.local_complement(1) + + exp_measurements = [(i, PlannerMeasBasis(planes[i], angles[i])) for i in range(3)] + _test(zx_graph, exp_nodes={0, 1, 2}, exp_edges={(0, 1), (1, 2)}, exp_measurements=exp_measurements) + + +if __name__ == "__main__": + pytest.main() From fab5a8a53f8b7eb62ca804d5028d75143113e6fb Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 18:01:44 +0900 Subject: [PATCH 03/94] :sparkles: Implement pivot --- graphix_zx/zxgraphstate.py | 78 ++++++++++++++++++++++++++++++++--- tests/test_zxgraphstate.py | 83 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 5 deletions(-) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index c55b220d8..32179bb81 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -7,19 +7,16 @@ from __future__ import annotations -import operator -from collections import defaultdict from itertools import combinations from typing import TYPE_CHECKING import numpy as np -from graphix_zx.common import Plane, PlannerMeasBasis, is_clifford_angle, is_close_angle from graphix_zx.euler import LocalClifford -from graphix_zx.graphstate import BaseGraphState, GraphState, bipartite_edges +from graphix_zx.graphstate import GraphState, bipartite_edges if TYPE_CHECKING: - from collections.abc import Callable, Iterable + from collections.abc import Iterable class ZXGraphState(GraphState): @@ -97,6 +94,77 @@ def local_complement(self, node: int) -> None: for v in nbrs: self.apply_local_clifford(v, lc) + def _swap(self, node1: int, node2: int) -> None: + """Swap nodes u and v in the graph state. + + Parameters + ---------- + node1 : `int` + node index + node2 : `int` + node index + """ + node1_nbrs = self.neighbors(node1) - {node2} + node2_nbrs = self.neighbors(node2) - {node1} + nbr_b = node1_nbrs - node2_nbrs + nbr_c = node2_nbrs - node1_nbrs + for b in nbr_b: + self.remove_physical_edge(node1, b) + self.add_physical_edge(node2, b) + for c in nbr_c: + self.remove_physical_edge(node2, c) + self.add_physical_edge(node1, c) + + def pivot(self, node1: int, node2: int) -> None: + """Pivot operation on the graph state: G∧(uv) (= G*u*v*u = G*v*u*v) for neighboring nodes u and v. + + In order to maintain the ZX-diagram simple, pi-spiders are shifted properly. + + Parameters + ---------- + node1 : `int` + node index. The node must not be an input node. + node2 : `int` + node index. The node must not be an input node. + + Raises + ------ + ValueError + If the nodes are input nodes, or the graph is not a ZX-diagram. + """ + self._ensure_node_exists(node1) + self._ensure_node_exists(node2) + if node1 in self.input_node_indices or node2 in self.input_node_indices: + msg = "Cannot apply pivot to input node" + raise ValueError(msg) + self._check_meas_basis() + + node1_nbrs = self.neighbors(node1) - {node2} + node2_nbrs = self.neighbors(node2) - {node1} + nbr_a = node1_nbrs & node2_nbrs + nbr_b = node1_nbrs - node2_nbrs + nbr_c = node2_nbrs - node1_nbrs + nbr_pairs = [ + bipartite_edges(nbr_a, nbr_b), + bipartite_edges(nbr_a, nbr_c), + bipartite_edges(nbr_b, nbr_c), + ] + rmv_edges = set().union(*(p & self.physical_edges for p in nbr_pairs)) + add_edges = set().union(*(p - self.physical_edges for p in nbr_pairs)) + + self._update_connections(rmv_edges, add_edges) + self._swap(node1, node2) + + # update node1 and node2 measurement + lc = LocalClifford(np.pi / 2, np.pi / 2, np.pi / 2) + for a in {node1, node2} - set(self.output_node_indices): + self.apply_local_clifford(a, lc) + + # update nodes measurement of nbr_a + lc = LocalClifford(np.pi, 0, 0) + for w in nbr_a - set(self.output_node_indices): + self.apply_local_clifford(w, lc) + def complete_graph_edges(nodes: Iterable[int]) -> set[tuple[int, int]]: r"""Return a set of edges for the complete graph on the given nodes. diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 0f6a9526a..da939340a 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -322,5 +322,88 @@ def test_local_complement_4_times( _test(zx_graph, exp_nodes={0, 1, 2}, exp_edges={(0, 1), (1, 2)}, exp_measurements=exp_measurements) +def test_pivot_fails_with_nonexistent_nodes(zx_graph: ZXGraphState) -> None: + """Test pivot fails with nonexistent nodes.""" + with pytest.raises(ValueError, match="Node does not exist node=0"): + zx_graph.pivot(0, 1) + zx_graph.add_physical_node() + with pytest.raises(ValueError, match="Node does not exist node=1"): + zx_graph.pivot(0, 1) + + +def test_pivot_fails_with_input_node(zx_graph: ZXGraphState) -> None: + """Test pivot fails with input node.""" + zx_graph.add_physical_node() + zx_graph.add_physical_node() + zx_graph.register_input(0) + with pytest.raises(ValueError, match="Cannot apply pivot to input node"): + zx_graph.pivot(0, 1) + + +def test_pivot_with_obvious_graph(zx_graph: ZXGraphState) -> None: + """Test pivot with an obvious graph.""" + # 0---1---2 + for _ in range(3): + zx_graph.add_physical_node() + + for i, j in [(0, 1), (1, 2)]: + zx_graph.add_physical_edge(i, j) + + measurements = [ + (0, PlannerMeasBasis(Plane.XY, 1.1 * np.pi)), + (1, PlannerMeasBasis(Plane.XZ, 1.2 * np.pi)), + (2, PlannerMeasBasis(Plane.YZ, 1.3 * np.pi)), + ] + _apply_measurements(zx_graph, measurements) + + original_zx_graph = deepcopy(zx_graph) + zx_graph.pivot(1, 2) + original_zx_graph.local_complement(1) + original_zx_graph.local_complement(2) + original_zx_graph.local_complement(1) + assert zx_graph.physical_edges == original_zx_graph.physical_edges + original_planes = [original_zx_graph.meas_bases[i].plane for i in range(3)] + planes = [zx_graph.meas_bases[i].plane for i in range(3)] + assert planes == original_planes + + +@pytest.mark.parametrize("planes", plane_combinations(5)) +def test_pivot_with_minimal_graph( + zx_graph: ZXGraphState, planes: tuple[Plane, Plane, Plane, Plane, Plane], rng: np.random.Generator +) -> None: + """Test pivot with a minimal graph.""" + # 0---1---2---4 + # \ / + # 3 + for _ in range(5): + zx_graph.add_physical_node() + + for i, j in [(0, 1), (1, 2), (1, 3), (2, 3), (2, 4)]: + zx_graph.add_physical_edge(i, j) + + angles = [rng.random() * 2 * np.pi for _ in range(5)] + measurements = [(i, PlannerMeasBasis(planes[i], angles[i])) for i in range(5)] + _apply_measurements(zx_graph, measurements) + zx_graph_cp = deepcopy(zx_graph) + + zx_graph.pivot(1, 2) + zx_graph_cp.local_complement(1) + zx_graph_cp.local_complement(2) + zx_graph_cp.local_complement(1) + assert zx_graph.physical_edges == zx_graph_cp.physical_edges + assert zx_graph.meas_bases[1].plane == zx_graph_cp.meas_bases[1].plane + assert zx_graph.meas_bases[2].plane == zx_graph_cp.meas_bases[2].plane + + _, ref_angle_func1 = MEAS_ACTION_PV_TARGET[planes[1]] + _, ref_angle_func2 = MEAS_ACTION_PV_TARGET[planes[2]] + _, ref_angle_func3 = MEAS_ACTION_PV_NEIGHBORS[planes[3]] + ref_angle1 = ref_angle_func1(angles[1]) + ref_angle2 = ref_angle_func2(angles[2]) + ref_angle3 = ref_angle_func3(angles[3]) + assert is_close_angle(zx_graph.meas_bases[1].angle, ref_angle1) + assert is_close_angle(zx_graph.meas_bases[2].angle, ref_angle2) + assert is_close_angle(zx_graph.meas_bases[3].angle, ref_angle3) + + if __name__ == "__main__": pytest.main() From 5e4737772d9b2a0e90f930b8aeeb22252f280369 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 18:19:50 +0900 Subject: [PATCH 04/94] :sparkles: Add remove_clifford --- graphix_zx/zxgraphstate.py | 180 ++++++++++++++++++++++++++++++++++++- tests/test_zxgraphstate.py | 110 +++++++++++++++++++++++ 2 files changed, 288 insertions(+), 2 deletions(-) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index 32179bb81..1b2b8e68a 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -7,16 +7,18 @@ from __future__ import annotations +import operator from itertools import combinations from typing import TYPE_CHECKING import numpy as np +from graphix_zx.common import Plane, is_clifford_angle, is_close_angle from graphix_zx.euler import LocalClifford -from graphix_zx.graphstate import GraphState, bipartite_edges +from graphix_zx.graphstate import BaseGraphState, GraphState, bipartite_edges if TYPE_CHECKING: - from collections.abc import Iterable + from collections.abc import Callable, Iterable class ZXGraphState(GraphState): @@ -43,6 +45,21 @@ class ZXGraphState(GraphState): def __init__(self) -> None: super().__init__() + @property + def _clifford_rules(self) -> tuple[tuple[Callable[[int, float], bool], Callable[[int], None]], ...]: + """List of rules (check_func, action_func) for removing local clifford nodes. + + The rules are applied in the order they are defined. + """ + return ( + (self._needs_lc, self.local_complement), + (self._needs_nop, lambda _: None), + ( + self._needs_pivot, + lambda node: self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))), + ), + ) + def _update_connections(self, rmv_edges: Iterable[tuple[int, int]], new_edges: Iterable[tuple[int, int]]) -> None: r"""Update the physical edges of the graph state. @@ -165,6 +182,165 @@ def pivot(self, node1: int, node2: int) -> None: for w in nbr_a - set(self.output_node_indices): self.apply_local_clifford(w, lc) + def _needs_nop(self, node: int, atol: float = 1e-9) -> bool: + """Check if the node does not need any operation in order to perform _remove_clifford. + + For this operation, the measurement measurement angle must be 0 or pi (mod 2pi) + and the measurement plane must be YZ or XZ. + + Parameters + ---------- + node : `int` + node index + atol : `float`, optional + absolute tolerance, by default 1e-9 + + Returns + ------- + `bool` + True if the node is a removable Clifford node. + """ + alpha = self.meas_bases[node].angle % (2.0 * np.pi) + return is_close_angle(2 * alpha, 0, atol) and (self.meas_bases[node].plane in {Plane.YZ, Plane.XZ}) + + def _needs_lc(self, node: int, atol: float = 1e-9) -> bool: + """Check if the node needs a local complementation in order to perform _remove_clifford. + + For this operation, the measurement angle must be 0.5 pi or 1.5 pi (mod 2pi) + and the measurement plane must be YZ or XY. + + Parameters + ---------- + node : `int` + node index + atol : `float`, optional + absolute tolerance, by default 1e-9 + + Returns + ------- + `bool` + True if the node needs a local complementation. + """ + alpha = self.meas_bases[node].angle % (2.0 * np.pi) + return is_close_angle(2 * (alpha - np.pi / 2), 0, atol) and ( + self.meas_bases[node].plane in {Plane.YZ, Plane.XY} + ) + + def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: + """Check if the nodes need a pivot operation in order to perform _remove_clifford. + + The pivot operation is performed on the non-input neighbor of the node. + For this operation, + (a) the measurement angle must be 0 or pi (mod 2pi) and the measurement plane must be XY, + or + (b) the measurement angle must be 0.5 pi or 1.5 pi (mod 2pi) and the measurement plane must be XZ. + + Parameters + ---------- + node : `int` + node index + atol : `float`, optional + absolute tolerance, by default 1e-9 + + Returns + ------- + `bool` + True if the nodes need a pivot operation. + """ + if not (self.neighbors(node) - set(self.input_node_indices)): + nbrs = self.neighbors(node) + if not (nbrs.issubset(set(self.output_node_indices)) and nbrs): + return False + + alpha = self.meas_bases[node].angle % (2.0 * np.pi) + # (a) the measurement angle is 0 or pi (mod 2pi) and the measurement plane is XY + case_a = is_close_angle(2 * alpha, 0, atol) and self.meas_bases[node].plane == Plane.XY + # (b) the measurement angle is 0.5 pi or 1.5 pi (mod 2pi) and the measurement plane is XZ + case_b = is_close_angle(2 * (alpha - np.pi / 2), 0, atol) and self.meas_bases[node].plane == Plane.XZ + return case_a or case_b + + def _remove_clifford(self, node: int, atol: float = 1e-9) -> None: + """Perform the Clifford node removal. + + Parameters + ---------- + node : `int` + node index + atol : `float`, optional + absolute tolerance, by default 1e-9 + """ + a_pi = self.meas_bases[node].angle % (2.0 * np.pi) + coeff = 0.0 if is_close_angle(a_pi, 0, atol) else 1.0 + lc = LocalClifford(coeff * np.pi, 0, 0) + for v in self.neighbors(node) - set(self.output_node_indices): + self.apply_local_clifford(v, lc) + + self.remove_physical_node(node) + + def remove_clifford(self, node: int, atol: float = 1e-9) -> None: + """Remove the local Clifford node. + + Parameters + ---------- + node : `int` + node index + atol : `float`, optional + absolute tolerance, by default 1e-9 + + Raises + ------ + ValueError + 1. If the node is an input node. + 2. If the node is not a Clifford node. + 3. If all neighbors are input nodes + in some special cases ((meas_plane, meas_angle) = (XY, a pi), (XZ, a pi/2) for a = 0, 1). + 4. If the node has no neighbors that are not connected only to output nodes. + """ + self._ensure_node_exists(node) + if node in self.input_node_indices or node in self.output_node_indices: + msg = "Clifford node removal not allowed for input node" + raise ValueError(msg) + + if not ( + is_clifford_angle(self.meas_bases[node].angle, atol) + and self.meas_bases[node].plane in {Plane.XY, Plane.XZ, Plane.YZ} + ): + msg = "This node is not a Clifford node." + raise ValueError(msg) + + for check, action in self._clifford_rules: + if not check(node, atol): + continue + action(node) + self._remove_clifford(node, atol) + return + + msg = "This Clifford node is unremovable." + raise ValueError(msg) + + def is_removable_clifford(self, node: int, atol: float = 1e-9) -> bool: + """Check if the node is a removable Clifford node. + + Parameters + ---------- + node : `int` + node index + atol : `float`, optional + absolute tolerance, by default 1e-9 + + Returns + ------- + `bool` + True if the node is a removable Clifford node. + """ + return any( + [ + self._needs_nop(node, atol), + self._needs_lc(node, atol), + self._needs_pivot(node, atol), + ] + ) + def complete_graph_edges(nodes: Iterable[int]) -> set[tuple[int, int]]: r"""Return a set of edges for the complete graph on the given nodes. diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index da939340a..f65570684 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -405,5 +405,115 @@ def test_pivot_with_minimal_graph( assert is_close_angle(zx_graph.meas_bases[3].angle, ref_angle3) +def test_remove_clifford_fails_if_nonexistent_node(zx_graph: ZXGraphState) -> None: + """Test remove_clifford raises an error if the node does not exist.""" + with pytest.raises(ValueError, match="Node does not exist node=0"): + zx_graph.remove_clifford(0) + + +def test_remove_clifford_fails_with_input_node(zx_graph: ZXGraphState) -> None: + zx_graph.add_physical_node() + zx_graph.register_input(0) + with pytest.raises(ValueError, match="Clifford node removal not allowed for input node"): + zx_graph.remove_clifford(0) + + +def test_remove_clifford_fails_with_invalid_plane(zx_graph: ZXGraphState) -> None: + """Test remove_clifford fails if the measurement plane is invalid.""" + zx_graph.add_physical_node() + zx_graph.assign_meas_basis( + 0, + PlannerMeasBasis("test_plane", 0.5 * np.pi), # type: ignore[reportArgumentType, arg-type, unused-ignore] + ) + with pytest.raises(ValueError, match="This node is not a Clifford node"): + zx_graph.remove_clifford(0) + + +def test_remove_clifford_fails_for_non_clifford_node(zx_graph: ZXGraphState) -> None: + zx_graph.add_physical_node() + zx_graph.assign_meas_basis(0, PlannerMeasBasis(Plane.XY, 0.1 * np.pi)) + with pytest.raises(ValueError, match="This node is not a Clifford node"): + zx_graph.remove_clifford(0) + + +def graph_1(zx_graph: ZXGraphState) -> None: + # _needs_nop + # 3---0---1 3 1 + # | -> + # 2 2 + _initialize_graph(zx_graph, nodes=range(4), edges={(0, 1), (0, 2), (0, 3)}) + + +def graph_2(zx_graph: ZXGraphState) -> None: + # _needs_lc + # 0---1---2 -> 0---2 + _initialize_graph(zx_graph, nodes=range(3), edges={(0, 1), (1, 2)}) + + +def graph_3(zx_graph: ZXGraphState) -> None: + # _needs_pivot_1 on (1, 2) + # 3(I) 3(I) + # / \ / | \ + # 0(I) - 1 - 2 - 5 -> 0(I) - 2 5 - 0(I) + # \ / \ | / + # 4(I) 4(I) + _initialize_graph( + zx_graph, nodes=range(6), edges={(0, 1), (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (2, 5)}, inputs=(0, 3, 4) + ) + + +def _test_remove_clifford( + zx_graph: ZXGraphState, + node: int, + measurements: Measurements, + exp_graph: tuple[set[int], set[tuple[int, int]]], + exp_measurements: Measurements, +) -> None: + _apply_measurements(zx_graph, measurements) + zx_graph.remove_clifford(node) + exp_nodes = exp_graph[0] + exp_edges = exp_graph[1] + _test(zx_graph, exp_nodes, exp_edges, exp_measurements) + + +@pytest.mark.parametrize( + "planes", + list(itertools.product(list(Plane), [Plane.XZ, Plane.YZ], list(Plane))), +) +def test_remove_clifford( + zx_graph: ZXGraphState, + planes: tuple[Plane, Plane, Plane], + rng: np.random.Generator, +) -> None: + graph_2(zx_graph) + angles = [rng.random() * 2 * np.pi for _ in range(3)] + epsilon = 1e-10 + angles[1] = rng.choice([0.0, np.pi, 2 * np.pi - epsilon]) + measurements = [(i, PlannerMeasBasis(planes[i], angles[i])) for i in range(3)] + ref_plane0, ref_angle_func0 = MEAS_ACTION_RC[planes[0]] + ref_plane2, ref_angle_func2 = MEAS_ACTION_RC[planes[2]] + ref_angle0 = ref_angle_func0(angles[1], angles[0]) + ref_angle2 = ref_angle_func2(angles[1], angles[2]) + exp_measurements = [ + (0, PlannerMeasBasis(ref_plane0, ref_angle0)), + (2, PlannerMeasBasis(ref_plane2, ref_angle2)), + ] + _test_remove_clifford( + zx_graph, node=1, measurements=measurements, exp_graph=({0, 2}, set()), exp_measurements=exp_measurements + ) + + +def test_unremovable_clifford_node(zx_graph: ZXGraphState) -> None: + _initialize_graph(zx_graph, nodes=range(3), edges={(0, 1), (1, 2)}, inputs=(0, 2)) + measurements = [ + (0, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)), + (1, PlannerMeasBasis(Plane.XY, np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)), + ] + _apply_measurements(zx_graph, measurements) + with pytest.raises(ValueError, match=r"This Clifford node is unremovable."): + zx_graph.remove_clifford(1) + + if __name__ == "__main__": pytest.main() From 6d02cdd367e0a133a85b4f69fb35541f15776ea7 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 18:39:04 +0900 Subject: [PATCH 05/94] :sparkles: Implement remove_cliffords --- graphix_zx/zxgraphstate.py | 77 +++++++++++++++++++++++++++- tests/test_zxgraphstate.py | 102 ++++++++++++++++++++++++++++++++++++- 2 files changed, 176 insertions(+), 3 deletions(-) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index 1b2b8e68a..e6a7b82ea 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -227,7 +227,7 @@ def _needs_lc(self, node: int, atol: float = 1e-9) -> bool: ) def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: - """Check if the nodes need a pivot operation in order to perform _remove_clifford. + """Check if the node needs a pivot operation in order to perform _remove_clifford. The pivot operation is performed on the non-input neighbor of the node. For this operation, @@ -245,7 +245,7 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: Returns ------- `bool` - True if the nodes need a pivot operation. + True if the node needs a pivot operation. """ if not (self.neighbors(node) - set(self.input_node_indices)): nbrs = self.neighbors(node) @@ -341,6 +341,79 @@ def is_removable_clifford(self, node: int, atol: float = 1e-9) -> bool: ] ) + def remove_cliffords(self, atol: float = 1e-9) -> None: + """Remove all local clifford nodes which are removable. + + Parameters + ---------- + atol : `float`, optional + absolute tolerance, by default 1e-9 + """ + self._check_meas_basis() + while any( + self.is_removable_clifford(n, atol) + for n in (self.physical_nodes - set(self.input_node_indices) - set(self.output_node_indices)) + ): + for check, action in self._clifford_rules: + while True: + candidates = self.physical_nodes - set(self.input_node_indices) - set(self.output_node_indices) + clifford_node = next((node for node in candidates if check(node, atol)), None) + if clifford_node is None: + break + action(clifford_node) + self._remove_clifford(clifford_node, atol) + + +def to_zx_graphstate(graph: BaseGraphState) -> ZXGraphState: + r"""Convert input graph to ZXGraphState. + + Parameters + ---------- + graph : `BaseGraphState` + The graph state to convert. + + Returns + ------- + `tuple`\[`ZXGraphState`, `dict`\[`int`, `int`\]\] + Converted ZXGraphState and node map for old node index to new node index. + + Raises + ------ + ValueError + If the input graph is not in canonical form. + TypeError + If the input graph is not an instance of GraphState. + """ + if not graph.is_canonical_form(): + msg = "The input graph must be in canonical form." + raise ValueError(msg) + if not isinstance(graph, GraphState): + msg = "The input graph must be an instance of GraphState." + raise TypeError(msg) + + node_map: dict[int, int] = {} + zx_graph = ZXGraphState() + + for node in graph.physical_nodes: + node_index = zx_graph.add_physical_node() + node_map[node] = node_index + meas_basis = graph.meas_bases.get(node, None) + if meas_basis is not None: + zx_graph.assign_meas_basis(node_index, meas_basis) + + q_index_map: dict[int, int] = {} + for input_node, old_q_index in sorted(graph.input_node_indices.items(), key=operator.itemgetter(1)): + new_q_index = zx_graph.register_input(node_map[input_node]) + q_index_map[old_q_index] = new_q_index + + for output_node, q_index in sorted(graph.output_node_indices.items()): + zx_graph.register_output(node_map[output_node], q_index_map[q_index]) + + for u, v in graph.physical_edges: + zx_graph.add_physical_edge(node_map[u], node_map[v]) + + return zx_graph, node_map + def complete_graph_edges(nodes: Iterable[int]) -> set[tuple[int, int]]: r"""Return a set of edges for the complete graph on the given nodes. diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index f65570684..9b09c35c7 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -22,7 +22,7 @@ from graphix_zx.common import Plane, PlannerMeasBasis, is_close_angle from graphix_zx.random_objects import generate_random_flow_graph -from graphix_zx.zxgraphstate import ZXGraphState +from graphix_zx.zxgraphstate import ZXGraphState, to_zx_graphstate if TYPE_CHECKING: from collections.abc import Sequence @@ -515,5 +515,105 @@ def test_unremovable_clifford_node(zx_graph: ZXGraphState) -> None: zx_graph.remove_clifford(1) +def test_remove_cliffords(zx_graph: ZXGraphState) -> None: + """Test removing multiple Clifford nodes.""" + _initialize_graph(zx_graph, nodes=range(4), edges={(0, 1), (0, 2), (0, 3)}) + measurements = [ + (0, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)), + (1, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)), + ] + _apply_measurements(zx_graph, measurements) + zx_graph.remove_cliffords() + _test(zx_graph, {2}, set(), []) + + +def test_remove_cliffords_graph1(zx_graph: ZXGraphState) -> None: + """Test removing multiple Clifford nodes.""" + graph_1(zx_graph) + measurements = [ + (0, PlannerMeasBasis(Plane.YZ, np.pi)), + (1, PlannerMeasBasis(Plane.XY, 0.1 * np.pi)), + (2, PlannerMeasBasis(Plane.XZ, 0.2 * np.pi)), + (3, PlannerMeasBasis(Plane.YZ, 0.3 * np.pi)), + ] + exp_measurements = [ + (1, PlannerMeasBasis(Plane.XY, 1.1 * np.pi)), + (2, PlannerMeasBasis(Plane.XZ, 1.8 * np.pi)), + (3, PlannerMeasBasis(Plane.YZ, 1.7 * np.pi)), + ] + _apply_measurements(zx_graph, measurements) + zx_graph.remove_cliffords() + _test(zx_graph, {1, 2, 3}, set(), exp_measurements=exp_measurements) + + +def test_remove_cliffords_graph2(zx_graph: ZXGraphState) -> None: + graph_2(zx_graph) + measurements = [ + (0, PlannerMeasBasis(Plane.XY, 0.1 * np.pi)), + (1, PlannerMeasBasis(Plane.YZ, 1.5 * np.pi)), + (2, PlannerMeasBasis(Plane.XZ, 0.2 * np.pi)), + ] + exp_measurements = [ + (0, PlannerMeasBasis(Plane.XY, 0.6 * np.pi)), + (2, PlannerMeasBasis(Plane.YZ, 0.2 * np.pi)), + ] + _apply_measurements(zx_graph, measurements) + zx_graph.remove_cliffords() + _test(zx_graph, {0, 2}, {(0, 2)}, exp_measurements=exp_measurements) + + +def test_remove_cliffords_graph3(zx_graph: ZXGraphState) -> None: + graph_3(zx_graph) + measurements = [ + (0, PlannerMeasBasis(Plane.XY, 0.1 * np.pi)), + (1, PlannerMeasBasis(Plane.XZ, 1.5 * np.pi)), + (2, PlannerMeasBasis(Plane.XZ, 0.2 * np.pi)), + (3, PlannerMeasBasis(Plane.YZ, 0.3 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 0.4 * np.pi)), + (5, PlannerMeasBasis(Plane.XZ, 0.5 * np.pi)), + ] + exp_measurements = [ + (0, PlannerMeasBasis(Plane.XY, 0.1 * np.pi)), + (2, PlannerMeasBasis(Plane.XZ, 1.7 * np.pi)), + (3, PlannerMeasBasis(Plane.YZ, 0.3 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 0.4 * np.pi)), + (5, PlannerMeasBasis(Plane.XZ, 1.5 * np.pi)), + ] + _apply_measurements(zx_graph, measurements) + zx_graph.remove_cliffords() + _test( + zx_graph, + {0, 2, 3, 4, 5}, + {(0, 2), (0, 3), (0, 4), (0, 5), (2, 3), (2, 4), (3, 5), (4, 5)}, + exp_measurements=exp_measurements, + ) + + +def test_random_graph(zx_graph: ZXGraphState) -> None: + """Test removing multiple Clifford nodes from a random graph.""" + random_graph, _ = generate_random_flow_graph(5, 5) + zx_graph, _ = to_zx_graphstate(random_graph) + + for i in zx_graph.physical_nodes - set(zx_graph.output_node_indices): + rng = np.random.default_rng(seed=0) + rnd = rng.random() + if 0 <= rnd < 0.33: + pass + elif 0.33 <= rnd < 0.66: + angle = zx_graph.meas_bases[i].angle + zx_graph.assign_meas_basis(i, PlannerMeasBasis(Plane.XZ, angle)) + else: + angle = zx_graph.meas_bases[i].angle + zx_graph.assign_meas_basis(i, PlannerMeasBasis(Plane.YZ, angle)) + + zx_graph.remove_cliffords() + atol = 1e-9 + nodes = zx_graph.physical_nodes - set(zx_graph.input_node_indices) - set(zx_graph.output_node_indices) + clifford_nodes = [node for node in nodes if zx_graph.is_removable_clifford(node, atol)] + assert clifford_nodes == [] + + if __name__ == "__main__": pytest.main() From 0ce2db8d6a155ffeda83c898a0d75816a3228add Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 18:41:10 +0900 Subject: [PATCH 06/94] :sparkles: Implement convert_to_phase_gadget --- graphix_zx/zxgraphstate.py | 42 ++++++++++++++++++++++++++++++++++++++ tests/test_zxgraphstate.py | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index e6a7b82ea..e3cad6a1d 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -363,6 +363,48 @@ def remove_cliffords(self, atol: float = 1e-9) -> None: action(clifford_node) self._remove_clifford(clifford_node, atol) + def _extract_yz_adjacent_pair(self) -> tuple[int, int] | None: + r"""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, v in self.physical_edges: + if u in yz_nodes and 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) + continue + if u := self._extract_xz_node(): + self.local_complement(u) + continue + break + def to_zx_graphstate(graph: BaseGraphState) -> ZXGraphState: r"""Convert input graph to ZXGraphState. diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 9b09c35c7..9763beb6d 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -615,5 +615,44 @@ 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 + ( + [ + (0, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (1, PlannerMeasBasis(Plane.XY, 0.22 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.44 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 0.55 * np.pi)), + (5, PlannerMeasBasis(Plane.XY, 0.66 * np.pi)), + ], + [ + (0, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (1, PlannerMeasBasis(Plane.XY, 0.22 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.44 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 0.55 * np.pi)), + (5, PlannerMeasBasis(Plane.XY, 0.66 * np.pi)), + ], + {(0, 1), (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (2, 5)}, + ), + ], +) +def test_convert_to_phase_gadget( + zx_graph: ZXGraphState, + measurements: Measurements, + exp_measurements: Measurements, + exp_edges: set[tuple[int, int]], +) -> None: + initial_edges = {(0, 1), (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (2, 5)} + _initialize_graph(zx_graph, nodes=range(6), edges=initial_edges) + _apply_measurements(zx_graph, measurements) + zx_graph.convert_to_phase_gadget() + _test(zx_graph, exp_nodes={0, 1, 2, 3, 4, 5}, exp_edges=exp_edges, exp_measurements=exp_measurements) + + if __name__ == "__main__": pytest.main() From b3a795ee0db07a366e071ad1b359b175beec4d63 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 18:45:47 +0900 Subject: [PATCH 07/94] :sparkles: Implement merge_yz_to_xy --- graphix_zx/zxgraphstate.py | 24 +++++++++++++++++ tests/test_zxgraphstate.py | 54 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index e3cad6a1d..c3d04158e 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -405,6 +405,30 @@ def convert_to_phase_gadget(self) -> None: 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.neighbors(u)) == 1) + } + target_nodes = { + u + for u in target_candidates + if ( + (v := next(iter(self.neighbors(u)))) + and (mb := self.meas_bases.get(v, None)) is not None + and mb.plane == Plane.XY + ) + } + for u in target_nodes: + (v,) = self.neighbors(u) + new_angle = (self.meas_bases[u].angle + self.meas_bases[v].angle) % (2.0 * np.pi) + self.assign_meas_basis(v, PlannerMeasBasis(Plane.XY, new_angle)) + self.remove_physical_node(u) + def to_zx_graphstate(graph: BaseGraphState) -> ZXGraphState: r"""Convert input graph to ZXGraphState. diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 9763beb6d..8bedc3b65 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -654,5 +654,59 @@ def test_convert_to_phase_gadget( _test(zx_graph, exp_nodes={0, 1, 2, 3, 4, 5}, exp_edges=exp_edges, exp_measurements=exp_measurements) +@pytest.mark.parametrize( + ("initial_edges", "measurements", "exp_measurements", "exp_edges"), + [ + # 3(XY) 3(XY) + # | -> | + # 0(YZ) - 1(XY) - 2(XY) 1(XY) - 2(XY) + ( + {(0, 1), (1, 2), (1, 3)}, + [ + (0, PlannerMeasBasis(Plane.YZ, 0.11 * np.pi)), + (1, PlannerMeasBasis(Plane.XY, 0.22 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.44 * np.pi)), + ], + [ + (1, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.44 * np.pi)), + ], + {(1, 2), (1, 3)}, + ), + # 3(YZ) 3(YZ) + # | \ -> | \ + # 0(YZ) - 1(XY) - 2(XY) 1(XY) - 2(XY) + ( + {(0, 1), (1, 2), (1, 3), (2, 3)}, + [ + (0, PlannerMeasBasis(Plane.YZ, 0.11 * np.pi)), + (1, PlannerMeasBasis(Plane.XY, 0.22 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (3, PlannerMeasBasis(Plane.YZ, 0.44 * np.pi)), + ], + [ + (1, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (3, PlannerMeasBasis(Plane.YZ, 0.44 * np.pi)), + ], + {(1, 2), (1, 3), (2, 3)}, + ), + ], +) +def test_merge_yz_to_xy( + zx_graph: ZXGraphState, + initial_edges: set[tuple[int, int]], + measurements: Measurements, + exp_measurements: Measurements, + exp_edges: set[tuple[int, int]], +) -> None: + _initialize_graph(zx_graph, nodes=range(4), edges=initial_edges) + _apply_measurements(zx_graph, measurements) + zx_graph.merge_yz_to_xy() + _test(zx_graph, exp_nodes={1, 2, 3}, exp_edges=exp_edges, exp_measurements=exp_measurements) + + if __name__ == "__main__": pytest.main() From c2ba8a550b8f831fdddfe5663787cc518bb7c032 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 18:51:05 +0900 Subject: [PATCH 08/94] :sparkles: Implement merge_yz_nodes --- graphix_zx/zxgraphstate.py | 26 ++++++++++- tests/test_zxgraphstate.py | 91 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index c3d04158e..c06d73fcc 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -8,12 +8,13 @@ from __future__ import annotations import operator +from collections import defaultdict from itertools import combinations from typing import TYPE_CHECKING import numpy as np -from graphix_zx.common import Plane, is_clifford_angle, is_close_angle +from graphix_zx.common import Plane, PlannerMeasBasis, is_clifford_angle, is_close_angle from graphix_zx.euler import LocalClifford from graphix_zx.graphstate import BaseGraphState, GraphState, bipartite_edges @@ -429,6 +430,29 @@ def merge_yz_to_xy(self) -> None: self.assign_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. + """ + min_nodes = 2 + yz_nodes = {u for u, basis in self.meas_bases.items() if basis.plane == Plane.YZ} + if len(yz_nodes) < min_nodes: + return + neighbor_groups: dict[frozenset[int], list[int]] = defaultdict(list) + for u in yz_nodes: + neighbors = frozenset(self.neighbors(u)) + neighbor_groups[neighbors].append(u) + + for neighbors, nodes in neighbor_groups.items(): + if len(nodes) < min_nodes or len(neighbors) < min_nodes: + continue + new_angle = sum(self.meas_bases[v].angle for v in nodes) % (2.0 * np.pi) + self.assign_meas_basis(nodes[0], PlannerMeasBasis(Plane.YZ, new_angle)) + for v in nodes[1:]: + self.remove_physical_node(v) + def to_zx_graphstate(graph: BaseGraphState) -> ZXGraphState: r"""Convert input graph to ZXGraphState. diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 8bedc3b65..70e78c916 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -708,5 +708,96 @@ def test_merge_yz_to_xy( _test(zx_graph, exp_nodes={1, 2, 3}, exp_edges=exp_edges, exp_measurements=exp_measurements) +@pytest.mark.parametrize( + ("initial_edges", "measurements", "exp_zxgraph"), + [ + # 3(YZ) 3(YZ) + # / \ / \ + # 0(XY) - 1(XY) - 2(XY) -> 0(XY) - 1(XY) - 2(XY) + # \ / + # 4(YZ) + ( + {(0, 1), (0, 3), (0, 4), (1, 2), (2, 3), (2, 4)}, + [ + (0, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (1, PlannerMeasBasis(Plane.XY, 0.22 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (3, PlannerMeasBasis(Plane.YZ, 0.44 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 0.55 * np.pi)), + ], + ( + [ + (0, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (1, PlannerMeasBasis(Plane.XY, 0.22 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (3, PlannerMeasBasis(Plane.YZ, 0.99 * np.pi)), + ], + {(0, 1), (0, 3), (1, 2), (2, 3)}, + {0, 1, 2, 3}, + ), + ), + # 3(YZ) + # / \ + # 0(XY) - 1(YZ) - 2(XY) -> 0(XY) - 1(YZ) - 2(XY) + # \ / + # 4(YZ) + ( + {(0, 1), (0, 3), (0, 4), (1, 2), (2, 3), (2, 4)}, + [ + (0, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (1, PlannerMeasBasis(Plane.YZ, 0.22 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (3, PlannerMeasBasis(Plane.YZ, 0.44 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 0.55 * np.pi)), + ], + ( + [ + (0, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (1, PlannerMeasBasis(Plane.YZ, 1.21 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + ], + {(0, 1), (1, 2)}, + {0, 1, 2}, + ), + ), + # 3(YZ) + # / \ + # 0(XY) - 1(YZ) - 2(XY) - 0(XY) -> 0(XY) - 1(YZ) - 2(XY) - 0(XY) + # \ / + # 4(YZ) + ( + {(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (2, 3), (2, 4)}, + [ + (0, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (1, PlannerMeasBasis(Plane.YZ, 0.22 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (3, PlannerMeasBasis(Plane.YZ, 0.44 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 0.55 * np.pi)), + ], + ( + [ + (0, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (1, PlannerMeasBasis(Plane.YZ, 1.21 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + ], + {(0, 1), (0, 2), (1, 2)}, + {0, 1, 2}, + ), + ), + ], +) +def test_merge_yz_nodes( + zx_graph: ZXGraphState, + initial_edges: set[tuple[int, int]], + measurements: Measurements, + exp_zxgraph: tuple[Measurements, set[tuple[int, int]], set[int]], +) -> None: + _initialize_graph(zx_graph, nodes=range(5), 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 f89521bdb19dd9b14c13d267480eaf0f9546b67d Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 18:54:01 +0900 Subject: [PATCH 09/94] :sparkles: Implement full_reduce --- graphix_zx/zxgraphstate.py | 26 +++++++++++++ tests/test_zxgraphstate.py | 75 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index c06d73fcc..83e2ba84c 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -453,6 +453,32 @@ def merge_yz_nodes(self) -> None: for v in nodes[1:]: self.remove_physical_node(v) + def full_reduce(self, atol: float = 1e-9) -> None: + """Reduce all Clifford nodes and some non-Clifford nodes. + + Repeat the following steps until there are no non-Clifford nodes: + 1. remove_cliffords + 2. convert_to_phase_gadget + 3. merge_yz_to_xy + 4. merge_yz_nodes + 5. if there are some removable Clifford nodes, 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( + self.is_removable_clifford(node, atol) + for node in self.physical_nodes - set(self.input_node_indices) - set(self.output_node_indices) + ): + break + def to_zx_graphstate(graph: BaseGraphState) -> ZXGraphState: r"""Convert input graph to ZXGraphState. diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 70e78c916..594cdd140 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -799,5 +799,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(4), {(0, 1), (1, 2), (1, 3)}), + [ + (0, PlannerMeasBasis(Plane.YZ, 0.1 * np.pi)), + (1, PlannerMeasBasis(Plane.XY, 0.4 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.3 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.4 * np.pi)), + ], + ( + [ + (2, PlannerMeasBasis(Plane.XY, 1.8 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 1.9 * np.pi)), + ], + {(2, 3)}, + {2, 3}, + ), + ), + # apply convert_to_phase_gadget, merge_yz_to_xy, then remove_cliffords + ( + (range(4), {(0, 1), (1, 2), (1, 3)}), + [ + (0, PlannerMeasBasis(Plane.YZ, 0.1 * np.pi)), + (1, PlannerMeasBasis(Plane.XY, 0.9 * np.pi)), + (2, PlannerMeasBasis(Plane.XZ, 0.8 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.4 * np.pi)), + ], + ( + [ + (2, PlannerMeasBasis(Plane.XY, 0.2 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.9 * np.pi)), + ], + {(2, 3)}, + {2, 3}, + ), + ), + # apply remove_cliffords, convert_to_phase_gadget, merge_yz_to_xy, then remove_cliffords + ( + (range(6), {(0, 1), (1, 2), (1, 3), (2, 5), (3, 4)}), + [ + (0, PlannerMeasBasis(Plane.YZ, 0.1 * np.pi)), + (1, PlannerMeasBasis(Plane.XY, 0.9 * np.pi)), + (2, PlannerMeasBasis(Plane.YZ, 1.2 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 1.4 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 1.0 * np.pi)), + (5, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)), + ], + ( + [ + (2, PlannerMeasBasis(Plane.XY, 1.8 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.9 * np.pi)), + ], + {(2, 3)}, + {2, 3}, + ), + ), + ], +) +def test_full_reduce( + zx_graph: ZXGraphState, + initial_zxgraph: tuple[range, set[tuple[int, int]]], + measurements: Measurements, + exp_zxgraph: tuple[Measurements, 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.full_reduce() + _test(zx_graph, exp_nodes, exp_edges, exp_measurements) + + if __name__ == "__main__": pytest.main() From 4525e5df94d76337b53ef46949432e258d95a852 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 19:44:42 +0900 Subject: [PATCH 10/94] :art: Fix docstring --- graphix_zx/zxgraphstate.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index 83e2ba84c..ce53f5f3b 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -20,6 +20,9 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterable + from typing import TypeAlias + + CliffordRule: TypeAlias = tuple[Callable[[int, float], bool], Callable[[int], None]] class ZXGraphState(GraphState): @@ -47,10 +50,17 @@ def __init__(self) -> None: super().__init__() @property - def _clifford_rules(self) -> tuple[tuple[Callable[[int, float], bool], Callable[[int], None]], ...]: - """List of rules (check_func, action_func) for removing local clifford nodes. + def _clifford_rules(self) -> tuple[CliffordRule, ...]: + r"""List of rules (check_func, action_func) for removing local clifford nodes. The rules are applied in the order they are defined. + + Returns + ------- + `tuple`\[`CliffordRule`, ...\] + Tuple of rules (check_func, action_func) before removing local clifford nodes. + If check_func(node) returns True, action_func(node) is executed. + Then, the removal of the local clifford node is performed if possible. """ return ( (self._needs_lc, self.local_complement), @@ -66,9 +76,9 @@ def _update_connections(self, rmv_edges: Iterable[tuple[int, int]], new_edges: I Parameters ---------- - rmv_edges : `Iterable`\[`tuple`\[`int`, `int`\]\] + rmv_edges : `collections.abc.Iterable`\[`tuple`\[`int`, `int`\]\] edges to remove - new_edges : `Iterable`\[`tuple`\[`int`, `int`\]\] + new_edges : `collections.abc.Iterable`\[`tuple`\[`int`, `int`\]\] edges to add """ for edge in rmv_edges: @@ -480,7 +490,7 @@ def full_reduce(self, atol: float = 1e-9) -> None: break -def to_zx_graphstate(graph: BaseGraphState) -> ZXGraphState: +def to_zx_graphstate(graph: BaseGraphState) -> tuple[ZXGraphState, dict[int, int]]: r"""Convert input graph to ZXGraphState. Parameters @@ -544,4 +554,4 @@ def complete_graph_edges(nodes: Iterable[int]) -> set[tuple[int, int]]: `set`\[`tuple`\[`int`, `int`\]\] edges of the complete graph """ - return {tuple(sorted((u, v))) for u, v in combinations(nodes, 2)} + return {(min(u, v), max(u, v)) for u, v in combinations(nodes, 2)} From aeb1bf8673bb25643bce25b5999396c71c02a49d Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 19:51:29 +0900 Subject: [PATCH 11/94] :art: Fix docstring --- graphix_zx/zxgraphstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index ce53f5f3b..0c0407390 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -51,7 +51,7 @@ def __init__(self) -> None: @property def _clifford_rules(self) -> tuple[CliffordRule, ...]: - r"""List of rules (check_func, action_func) for removing local clifford nodes. + r"""Tuple of rules (check_func, action_func) for removing local clifford nodes. The rules are applied in the order they are defined. From 0b29a92bed474f9dafe079243e0ec17ee15d5551 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 20:15:40 +0900 Subject: [PATCH 12/94] :art: Fix test --- tests/test_zxgraphstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 594cdd140..1e7184dfd 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -144,7 +144,7 @@ def _initialize_graph( raise ValueError(msg) for idx, node in enumerate(outputs): - q_index = node2q_index.get(node, idx) + q_index = node2q_index.get(node, q_indices[idx]) zx_graph.register_output(node, q_index) From 92843c0089bfa4abf8ba74acb682eef9f1a034a0 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 20:17:00 +0900 Subject: [PATCH 13/94] :sparkles: Add a function to generate random MBQC circuit --- 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 31b126731..37cfe3807 100644 --- a/graphix_zx/random_objects.py +++ b/graphix_zx/random_objects.py @@ -11,10 +11,13 @@ import numpy as np +from graphix_zx.circuit import MBQCCircuit from graphix_zx.common import default_meas_basis from graphix_zx.graphstate import GraphState if TYPE_CHECKING: + from collections.abc import Sequence + from numpy.random import Generator @@ -82,3 +85,48 @@ def generate_random_flow_graph( flow[node_index - width] = {node_index} return graph, flow + + +def random_circ( + width: int, + depth: int, + rng: np.random.Generator | None = None, + edge_p: float = 0.5, + angle_candidates: Sequence[float] = (0.0, np.pi / 3, 2 * np.pi / 3, np.pi), +) -> MBQCCircuit: + r"""Generate a random MBQC circuit. + + Parameters + ---------- + width : `int` + circuit width + depth : `int` + circuit depth + rng : `numpy.random.Generator`, optional + random number generator, by default numpy.random.default_rng() + edge_p : `float`, optional + probability of adding CZ gate, by default 0.5 + angle_candidates : `collections.abc.Sequence[float]`, optional + sequence 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() + circ = MBQCCircuit(width) + for d in range(depth): + for j in range(width): + circ.j(j, rng.choice(angle_candidates)) + 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 = sorted(rng.choice(range(width), num)) + circ.phase_gadget(target, rng.choice(angle_candidates)) + + return circ From 3674192af315c568e6cff35aaea330f4c8a78323 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 20:17:36 +0900 Subject: [PATCH 14/94] :sparkles: Add demonstration for zxgraph simplification --- examples/zxgraph_simplification.py | 62 ++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 examples/zxgraph_simplification.py diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py new file mode 100644 index 000000000..650a1ea16 --- /dev/null +++ b/examples/zxgraph_simplification.py @@ -0,0 +1,62 @@ +""" +Basic example of simplifying a ZX-diagram. +========================================== + +By using the `full_reduce` method, +we can remove all the internal Clifford nodes and some non-Clifford nodes from the graph state, +which generates a simpler ZX-diagram. +This example is a simple demonstration of the simplification process. + +Note that as a result of the simplification, local Clifford operations are applied to the input/output nodes. +""" + +# %% +from copy import deepcopy + +import numpy as np + +from graphix_zx.circuit import circuit2graph +from graphix_zx.random_objects import random_circ +from graphix_zx.visualizer import visualize +from graphix_zx.zxgraphstate import to_zx_graphstate + +# %% +# Create a random circuit and convert it to a ZXGraphState +circ = random_circ(4, 4) +graph, flow = circuit2graph(circ) +zx_graph, _ = to_zx_graphstate(graph) +visualize(zx_graph) + +# %% +# Initial graph state before simplification +print("node | plane | angle (/pi)") +for node in zx_graph.input_node_indices: + 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 - set(zx_graph.input_node_indices) - set(zx_graph.output_node_indices): + print(node, zx_graph.meas_bases[node].plane, zx_graph.meas_bases[node].angle / np.pi) +for node in zx_graph.output_node_indices: + print(f"{node} (output)", "-", "-") + +# %% +# Simplify the graph state by full_reduce method +zx_graph_smp = deepcopy(zx_graph) +zx_graph_smp.full_reduce() + +# %% +# Simplified graph state after full_reduce. +visualize(zx_graph_smp) +print("node | plane | angle (/pi)") +for node in zx_graph.input_node_indices: + 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 - set(zx_graph.input_node_indices) - set(zx_graph_smp.output_node_indices): + print(node, zx_graph_smp.meas_bases[node].plane, zx_graph_smp.meas_bases[node].angle / np.pi) +for node in zx_graph_smp.output_node_indices: + print(f"{node} (output)", "-", "-") + +# %% +# Supplementary Note: +# At first glance, the input/output nodes appear to remain unaffected. +# However, note that a local Clifford operation is actually applied as a result of the action of the full_reduce method. + +# If you visualize the circuit after executing the `expand_local_cliffords` method, +# you will see that the additional nodes are now visible on the input/output qubits. From d205a1d730041454e2d807c4d7263f88c353273c Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 21:24:50 +0900 Subject: [PATCH 15/94] :bug: Fix bug in generating circuit --- graphix_zx/random_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix_zx/random_objects.py b/graphix_zx/random_objects.py index 37cfe3807..727535032 100644 --- a/graphix_zx/random_objects.py +++ b/graphix_zx/random_objects.py @@ -126,7 +126,7 @@ def random_circ( circ.cz(j, (j + 1) % width) num = rng.integers(0, width) if num > 0: - target = sorted(rng.choice(range(width), num)) + target = sorted(set(rng.choice(range(width), num))) circ.phase_gadget(target, rng.choice(angle_candidates)) return circ From d62f07b94ebdec0fe4be724214474f137b5584ff Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 21:25:15 +0900 Subject: [PATCH 16/94] :art: Fix type hint --- graphix_zx/zxgraphstate.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index 0c0407390..10da03e84 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -20,6 +20,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterable + from collections.abc import Set as AbstractSet from typing import TypeAlias CliffordRule: TypeAlias = tuple[Callable[[int, float], bool], Callable[[int], None]] @@ -71,14 +72,16 @@ def _clifford_rules(self) -> tuple[CliffordRule, ...]: ), ) - def _update_connections(self, rmv_edges: Iterable[tuple[int, int]], new_edges: Iterable[tuple[int, int]]) -> None: + def _update_connections( + self, rmv_edges: AbstractSet[tuple[int, int]], new_edges: AbstractSet[tuple[int, int]] + ) -> None: r"""Update the physical edges of the graph state. Parameters ---------- - rmv_edges : `collections.abc.Iterable`\[`tuple`\[`int`, `int`\]\] + rmv_edges : `collections.abc.Set`\[`tuple`\[`int`, `int`\]\] edges to remove - new_edges : `collections.abc.Iterable`\[`tuple`\[`int`, `int`\]\] + new_edges : `collections.abc.Set`\[`tuple`\[`int`, `int`\]\] edges to add """ for edge in rmv_edges: @@ -109,7 +112,7 @@ def local_complement(self, node: int) -> None: self._check_meas_basis() nbrs: set[int] = self.neighbors(node) - nbr_pairs = complete_graph_edges(nbrs) + nbr_pairs: set[tuple[int, int]] = complete_graph_edges(nbrs) new_edges = nbr_pairs - self.physical_edges rmv_edges = self.physical_edges & nbr_pairs @@ -177,8 +180,10 @@ def pivot(self, node1: int, node2: int) -> None: bipartite_edges(nbr_a, nbr_c), bipartite_edges(nbr_b, nbr_c), ] - rmv_edges = set().union(*(p & self.physical_edges for p in nbr_pairs)) - add_edges = set().union(*(p - self.physical_edges for p in nbr_pairs)) + rmv_edges: set[tuple[int, int]] = set() + rmv_edges.update(*(p & self.physical_edges for p in nbr_pairs)) + add_edges: set[tuple[int, int]] = set() + add_edges.update(*(p - self.physical_edges for p in nbr_pairs)) self._update_connections(rmv_edges, add_edges) self._swap(node1, node2) From ccc3e91fe7ba66f18c86cdbc6fb144b0cdc7f3a9 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 22:18:27 +0900 Subject: [PATCH 17/94] :art: Fix docstrings --- examples/zxgraph_simplification.py | 2 +- graphix_zx/zxgraphstate.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py index 650a1ea16..5bebd25f2 100644 --- a/examples/zxgraph_simplification.py +++ b/examples/zxgraph_simplification.py @@ -2,7 +2,7 @@ Basic example of simplifying a ZX-diagram. ========================================== -By using the `full_reduce` method, +By using the full_reduce method, we can remove all the internal Clifford nodes and some non-Clifford nodes from the graph state, which generates a simpler ZX-diagram. This example is a simple demonstration of the simplification process. diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index 10da03e84..3887fcb1d 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -1,8 +1,10 @@ """ZXGraph State classes for Measurement-based Quantum Computing. This module provides: -- ZXGraphState: Graph State for the ZX-calculus. -- bipartite_edges: Return a set of edges for the complete bipartite graph between two sets of nodes. + +- `ZXGraphState`: Graph State for the ZX-calculus. +- `to_zx_graphstate`: Convert input GraphState to ZXGraphState. +- `complete_graph_edges`: Return a set of edges for the complete graph on the given nodes. """ from __future__ import annotations From c2a706b895fcaeaf12b8f97ebef51d131616f478 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 22:18:45 +0900 Subject: [PATCH 18/94] :art: Fix type hint --- graphix_zx/zxgraphstate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index 3887fcb1d..5be078c57 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -10,6 +10,7 @@ from __future__ import annotations import operator +import sys from collections import defaultdict from itertools import combinations from typing import TYPE_CHECKING @@ -20,10 +21,14 @@ from graphix_zx.euler import LocalClifford from graphix_zx.graphstate import BaseGraphState, GraphState, bipartite_edges +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + from typing_extensions import TypeAlias + if TYPE_CHECKING: from collections.abc import Callable, Iterable from collections.abc import Set as AbstractSet - from typing import TypeAlias CliffordRule: TypeAlias = tuple[Callable[[int, float], bool], Callable[[int], None]] From 0a8818bdffcfc60be0013cd134a6acce6b83dc7f Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 5 Oct 2025 22:19:16 +0900 Subject: [PATCH 19/94] :bulb: Add docs --- docs/source/references.rst | 1 + docs/source/zxgraphstate.rst | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 docs/source/zxgraphstate.rst diff --git a/docs/source/references.rst b/docs/source/references.rst index 41df0e5b1..8fd190aff 100644 --- a/docs/source/references.rst +++ b/docs/source/references.rst @@ -21,3 +21,4 @@ Module reference qompiler scheduler visualizer + zxgraphstate diff --git a/docs/source/zxgraphstate.rst b/docs/source/zxgraphstate.rst new file mode 100644 index 000000000..b9c1808ed --- /dev/null +++ b/docs/source/zxgraphstate.rst @@ -0,0 +1,20 @@ +ZXGraphState +============ + +:mod:`graphix_zx.zxgraphstate` module ++++++++++++++++++++++++++++++++++++++ + +.. automodule:: graphix_zx.zxgraphstate + +ZX Graph State +-------------- +.. autoclass:: graphix_zx.zxgraphstate.ZXGraphState + :members: + :show-inheritance: + :member-order: bysource + +Functions +--------- + +.. autofunction:: graphix_zx.zxgraphstate.to_zx_graphstate +.. autofunction:: graphix_zx.zxgraphstate.complete_graph_edges From 06f0490ab91bee8e430436fd718b401e16bd3769 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 17 Oct 2025 22:04:35 +0900 Subject: [PATCH 20/94] :bug: Fix bug after merge --- tests/test_zxgraphstate.py | 52 +++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 1e7184dfd..6c8e2b46e 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -20,9 +20,9 @@ import numpy as np import pytest -from graphix_zx.common import Plane, PlannerMeasBasis, is_close_angle -from graphix_zx.random_objects import generate_random_flow_graph -from graphix_zx.zxgraphstate import ZXGraphState, to_zx_graphstate +from graphqomb.common import Plane, PlannerMeasBasis, is_close_angle +from graphqomb.random_objects import generate_random_flow_graph +from graphqomb.zxgraphstate import ZXGraphState, to_zx_graphstate if TYPE_CHECKING: from collections.abc import Sequence @@ -134,17 +134,17 @@ def _initialize_graph( node2q_index: dict[int, int] = {} q_indices: list[int] = [] - for node in inputs: - q_index = zx_graph.register_input(node) - node2q_index[node] = q_index - q_indices.append(q_index) + for i, node in enumerate(inputs): + zx_graph.register_input(node, q_index=i) + node2q_index[node] = i + q_indices.append(i) if len(outputs) > len(q_indices): msg = "Cannot assign valid q_index to all output nodes." raise ValueError(msg) - for idx, node in enumerate(outputs): - q_index = node2q_index.get(node, q_indices[idx]) + for i, node in enumerate(outputs): + q_index = node2q_index.get(node, q_indices[i]) zx_graph.register_output(node, q_index) @@ -176,17 +176,17 @@ def test_local_complement_fails_if_nonexistent_node(zx_graph: ZXGraphState) -> N def test_local_complement_fails_if_not_zx_graph(zx_graph: ZXGraphState) -> None: """Test local complement raises an error if the graph is not a ZX-diagram.""" - zx_graph.add_physical_node() + node = zx_graph.add_physical_node() with pytest.raises(ValueError, match="Measurement basis not set for node 0"): - zx_graph.local_complement(0) + zx_graph.local_complement(node) def test_local_complement_fails_with_input_node(zx_graph: ZXGraphState) -> None: """Test local complement fails with input node.""" - zx_graph.add_physical_node() - zx_graph.register_input(0) + node = zx_graph.add_physical_node() + zx_graph.register_input(node, q_index=0) with pytest.raises(ValueError, match=r"Cannot apply local complement to input node."): - zx_graph.local_complement(0) + zx_graph.local_complement(node) @pytest.mark.parametrize("plane", list(Plane)) @@ -195,13 +195,13 @@ def test_local_complement_with_no_edge(zx_graph: ZXGraphState, plane: Plane, rng angle = rng.random() * 2 * np.pi ref_plane, ref_angle_func = MEAS_ACTION_LC_TARGET[plane] ref_angle = ref_angle_func(angle) - zx_graph.add_physical_node() - zx_graph.assign_meas_basis(0, PlannerMeasBasis(plane, angle)) + node = zx_graph.add_physical_node() + zx_graph.assign_meas_basis(node, PlannerMeasBasis(plane, angle)) - zx_graph.local_complement(0) + zx_graph.local_complement(node) assert zx_graph.physical_edges == set() - assert zx_graph.meas_bases[0].plane == ref_plane - assert is_close_angle(zx_graph.meas_bases[0].angle, ref_angle) + assert zx_graph.meas_bases[node].plane == ref_plane + assert is_close_angle(zx_graph.meas_bases[node].angle, ref_angle) @pytest.mark.parametrize("planes", plane_combinations(3)) @@ -333,11 +333,11 @@ def test_pivot_fails_with_nonexistent_nodes(zx_graph: ZXGraphState) -> None: def test_pivot_fails_with_input_node(zx_graph: ZXGraphState) -> None: """Test pivot fails with input node.""" - zx_graph.add_physical_node() - zx_graph.add_physical_node() - zx_graph.register_input(0) + node1 = zx_graph.add_physical_node() + node2 = zx_graph.add_physical_node() + zx_graph.register_input(node1, q_index=0) with pytest.raises(ValueError, match="Cannot apply pivot to input node"): - zx_graph.pivot(0, 1) + zx_graph.pivot(node1, node2) def test_pivot_with_obvious_graph(zx_graph: ZXGraphState) -> None: @@ -412,10 +412,10 @@ def test_remove_clifford_fails_if_nonexistent_node(zx_graph: ZXGraphState) -> No def test_remove_clifford_fails_with_input_node(zx_graph: ZXGraphState) -> None: - zx_graph.add_physical_node() - zx_graph.register_input(0) + node = zx_graph.add_physical_node() + zx_graph.register_input(node, q_index=0) with pytest.raises(ValueError, match="Clifford node removal not allowed for input node"): - zx_graph.remove_clifford(0) + zx_graph.remove_clifford(node) def test_remove_clifford_fails_with_invalid_plane(zx_graph: ZXGraphState) -> None: From 14f76c588a1146d68b32e031184145ef8bc9a109 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 17 Oct 2025 22:05:02 +0900 Subject: [PATCH 21/94] :recycle: Refactor to_zx_graphstate --- graphqomb/zxgraphstate.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 249589921..9a7ba1fd5 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -9,7 +9,6 @@ from __future__ import annotations -import operator import sys from collections import defaultdict from itertools import combinations @@ -517,14 +516,10 @@ def to_zx_graphstate(graph: BaseGraphState) -> tuple[ZXGraphState, dict[int, int Raises ------ - ValueError - If the input graph is not in canonical form. TypeError If the input graph is not an instance of GraphState. """ - if not graph.is_canonical_form(): - msg = "The input graph must be in canonical form." - raise ValueError(msg) + graph.check_canonical_form() if not isinstance(graph, GraphState): msg = "The input graph must be an instance of GraphState." raise TypeError(msg) @@ -532,6 +527,7 @@ def to_zx_graphstate(graph: BaseGraphState) -> tuple[ZXGraphState, dict[int, int node_map: dict[int, int] = {} zx_graph = ZXGraphState() + # Copy all physical nodes and measurement bases for node in graph.physical_nodes: node_index = zx_graph.add_physical_node() node_map[node] = node_index @@ -539,17 +535,22 @@ def to_zx_graphstate(graph: BaseGraphState) -> tuple[ZXGraphState, dict[int, int if meas_basis is not None: zx_graph.assign_meas_basis(node_index, meas_basis) - q_index_map: dict[int, int] = {} - for input_node, old_q_index in sorted(graph.input_node_indices.items(), key=operator.itemgetter(1)): - new_q_index = zx_graph.register_input(node_map[input_node]) - q_index_map[old_q_index] = new_q_index + # Register input nodes + for node, q_index in graph.input_node_indices.items(): + zx_graph.register_input(node_map[node], q_index) - for output_node, q_index in sorted(graph.output_node_indices.items()): - zx_graph.register_output(node_map[output_node], q_index_map[q_index]) + # Register output nodes + for node, q_index in graph.output_node_indices.items(): + zx_graph.register_output(node_map[node], q_index) + # Copy all physical edges for u, v in graph.physical_edges: zx_graph.add_physical_edge(node_map[u], node_map[v]) + # Copy local Clifford operators + for node, lc in graph.local_cliffords.items(): + zx_graph.apply_local_clifford(node_map[node], lc) + return zx_graph, node_map From 167ea75b9c4c4f2b8bd6f50087d183cdfcc9c61f Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 20 Oct 2025 21:11:33 +0900 Subject: [PATCH 22/94] :bug: Fix output node treatment --- graphqomb/zxgraphstate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 9a7ba1fd5..4f685d9ce 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -196,12 +196,12 @@ def pivot(self, node1: int, node2: int) -> None: # update node1 and node2 measurement lc = LocalClifford(np.pi / 2, np.pi / 2, np.pi / 2) - for a in {node1, node2} - set(self.output_node_indices): + for a in {node1, node2}: self.apply_local_clifford(a, lc) # update nodes measurement of nbr_a lc = LocalClifford(np.pi, 0, 0) - for w in nbr_a - set(self.output_node_indices): + for w in nbr_a: self.apply_local_clifford(w, lc) def _needs_nop(self, node: int, atol: float = 1e-9) -> bool: From e44defbbae70f9415141921bb4dfea7f64ea20b3 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 20 Oct 2025 22:46:05 +0900 Subject: [PATCH 23/94] :bug: Fix operation corresponding to lemma 4.7 --- graphqomb/zxgraphstate.py | 29 ++++++++++++++++++++++++++--- tests/test_zxgraphstate.py | 2 +- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 4f685d9ce..9c4900ee8 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -71,7 +71,7 @@ def _clifford_rules(self) -> tuple[CliffordRule, ...]: """ return ( (self._needs_lc, self.local_complement), - (self._needs_nop, lambda _: None), + (self._is_trivial_meas, self.absorb_trivial_meas), ( self._needs_pivot, lambda node: self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))), @@ -204,11 +204,14 @@ def pivot(self, node1: int, node2: int) -> None: for w in nbr_a: self.apply_local_clifford(w, lc) - def _needs_nop(self, node: int, atol: float = 1e-9) -> bool: + def _is_trivial_meas(self, node: int, atol: float = 1e-9) -> bool: """Check if the node does not need any operation in order to perform _remove_clifford. For this operation, the measurement measurement angle must be 0 or pi (mod 2pi) and the measurement plane must be YZ or XZ. + If True is returned and the node is output, all neighbors are registered as output nodes. + + ref: Quantum 5, 421 (2021). Lemma 4.7 Parameters ---------- @@ -221,6 +224,7 @@ def _needs_nop(self, node: int, atol: float = 1e-9) -> bool: ------- `bool` True if the node is a removable Clifford node. + If True and the node is output, all neighbors are registered as output nodes in absorb_trivial_meas method. """ alpha = self.meas_bases[node].angle % (2.0 * np.pi) return is_close_angle(2 * alpha, 0, atol) and (self.meas_bases[node].plane in {Plane.YZ, Plane.XZ}) @@ -281,6 +285,25 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: case_b = is_close_angle(2 * (alpha - np.pi / 2), 0, atol) and self.meas_bases[node].plane == Plane.XZ return case_a or case_b + def absorb_trivial_meas(self, node: int) -> None: + """Absorb the trivial measurement node. + + For this operation, the measurement measurement angle must be 0 or pi (mod 2pi) + and the measurement plane must be YZ or XZ. + If the node is output, all neighbors are registered as output nodes. + + Parameters + ---------- + node : `int` + node index + """ + nbrs = self.neighbors(node) + for v in nbrs: + # if the node is output, register neighbors as output nodes + if node in self.output_node_indices: + max_q_index = max(self.q_indices.values(), default=-1) + self.register_output(v, max_q_index + 1) + def _remove_clifford(self, node: int, atol: float = 1e-9) -> None: """Perform the Clifford node removal. @@ -357,7 +380,7 @@ def is_removable_clifford(self, node: int, atol: float = 1e-9) -> bool: """ return any( [ - self._needs_nop(node, atol), + self._is_trivial_meas(node, atol), self._needs_lc(node, atol), self._needs_pivot(node, atol), ] diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 6c8e2b46e..7fa94cb6b 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -437,7 +437,7 @@ def test_remove_clifford_fails_for_non_clifford_node(zx_graph: ZXGraphState) -> def graph_1(zx_graph: ZXGraphState) -> None: - # _needs_nop + # _is_trivial_meas # 3---0---1 3 1 # | -> # 2 2 From 6e97ac985beb7e57916e6c6dff40937be25b429c Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 20 Oct 2025 23:01:05 +0900 Subject: [PATCH 24/94] :bug: Fix handling of output node corresponding to lem. 4.7 --- graphqomb/zxgraphstate.py | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 9c4900ee8..9c577795b 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -71,7 +71,7 @@ def _clifford_rules(self) -> tuple[CliffordRule, ...]: """ return ( (self._needs_lc, self.local_complement), - (self._is_trivial_meas, self.absorb_trivial_meas), + (self._is_trivial_meas, lambda _: None), ( self._needs_pivot, lambda node: self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))), @@ -209,7 +209,7 @@ def _is_trivial_meas(self, node: int, atol: float = 1e-9) -> bool: For this operation, the measurement measurement angle must be 0 or pi (mod 2pi) and the measurement plane must be YZ or XZ. - If True is returned and the node is output, all neighbors are registered as output nodes. + If the node is output, False is returned. ref: Quantum 5, 421 (2021). Lemma 4.7 @@ -224,10 +224,14 @@ def _is_trivial_meas(self, node: int, atol: float = 1e-9) -> bool: ------- `bool` True if the node is a removable Clifford node. - If True and the node is output, all neighbors are registered as output nodes in absorb_trivial_meas method. + If the node is output, False is returned. """ alpha = self.meas_bases[node].angle % (2.0 * np.pi) - return is_close_angle(2 * alpha, 0, atol) and (self.meas_bases[node].plane in {Plane.YZ, Plane.XZ}) + return ( + is_close_angle(2 * alpha, 0, atol) + and (self.meas_bases[node].plane in {Plane.YZ, Plane.XZ}) + and (node not in self.output_node_indices) + ) def _needs_lc(self, node: int, atol: float = 1e-9) -> bool: """Check if the node needs a local complementation in order to perform _remove_clifford. @@ -285,25 +289,6 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: case_b = is_close_angle(2 * (alpha - np.pi / 2), 0, atol) and self.meas_bases[node].plane == Plane.XZ return case_a or case_b - def absorb_trivial_meas(self, node: int) -> None: - """Absorb the trivial measurement node. - - For this operation, the measurement measurement angle must be 0 or pi (mod 2pi) - and the measurement plane must be YZ or XZ. - If the node is output, all neighbors are registered as output nodes. - - Parameters - ---------- - node : `int` - node index - """ - nbrs = self.neighbors(node) - for v in nbrs: - # if the node is output, register neighbors as output nodes - if node in self.output_node_indices: - max_q_index = max(self.q_indices.values(), default=-1) - self.register_output(v, max_q_index + 1) - def _remove_clifford(self, node: int, atol: float = 1e-9) -> None: """Perform the Clifford node removal. From c0a0649995a48b4e499ffe256c468a5b808fbacb Mon Sep 17 00:00:00 2001 From: nabe98 Date: Wed, 22 Oct 2025 22:46:57 +0900 Subject: [PATCH 25/94] :art: Improve readability --- graphqomb/zxgraphstate.py | 54 ++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 9c577795b..ff4ea0f6a 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -207,11 +207,8 @@ def pivot(self, node1: int, node2: int) -> None: def _is_trivial_meas(self, node: int, atol: float = 1e-9) -> bool: """Check if the node does not need any operation in order to perform _remove_clifford. - For this operation, the measurement measurement angle must be 0 or pi (mod 2pi) - and the measurement plane must be YZ or XZ. - If the node is output, False is returned. - - ref: Quantum 5, 421 (2021). Lemma 4.7 + For this operation, the following must hold: + measurement plane = YZ or XZ and measurement angle = 0 or pi (mod 2pi) Parameters ---------- @@ -224,20 +221,19 @@ def _is_trivial_meas(self, node: int, atol: float = 1e-9) -> bool: ------- `bool` True if the node is a removable Clifford node. - If the node is output, False is returned. + + References + ---------- + [1] Backens et al., Quantum 5, 421 (2021). Lemma 4.7 """ alpha = self.meas_bases[node].angle % (2.0 * np.pi) - return ( - is_close_angle(2 * alpha, 0, atol) - and (self.meas_bases[node].plane in {Plane.YZ, Plane.XZ}) - and (node not in self.output_node_indices) - ) + return is_close_angle(2 * alpha, 0, atol) and (self.meas_bases[node].plane in {Plane.YZ, Plane.XZ}) def _needs_lc(self, node: int, atol: float = 1e-9) -> bool: """Check if the node needs a local complementation in order to perform _remove_clifford. - For this operation, the measurement angle must be 0.5 pi or 1.5 pi (mod 2pi) - and the measurement plane must be YZ or XY. + For this operation, the following must hold: + measurement plane = YZ or XY and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) Parameters ---------- @@ -250,6 +246,10 @@ def _needs_lc(self, node: int, atol: float = 1e-9) -> bool: ------- `bool` True if the node needs a local complementation. + + References + ---------- + [1] Backens et al., Quantum 5, 421 (2021). Lemma 4.8 """ alpha = self.meas_bases[node].angle % (2.0 * np.pi) return is_close_angle(2 * (alpha - np.pi / 2), 0, atol) and ( @@ -260,10 +260,9 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: """Check if the node needs a pivot operation in order to perform _remove_clifford. The pivot operation is performed on the non-input neighbor of the node. - For this operation, - (a) the measurement angle must be 0 or pi (mod 2pi) and the measurement plane must be XY, - or - (b) the measurement angle must be 0.5 pi or 1.5 pi (mod 2pi) and the measurement plane must be XZ. + For this operation, one of the following must hold: + (a) measurement plane = XY and measurement angle = 0 or pi (mod 2pi) + (b) measurement plane = XZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) Parameters ---------- @@ -276,17 +275,20 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: ------- `bool` True if the node needs a pivot operation. + + References + ---------- + [1] Backens et al., Quantum 5, 421 (2021), Lemma 4.9 """ - if not (self.neighbors(node) - set(self.input_node_indices)): - nbrs = self.neighbors(node) - if not (nbrs.issubset(set(self.output_node_indices)) and nbrs): - return False + non_input_nbrs = self.neighbors(node) - set(self.input_node_indices) + if not non_input_nbrs: + return False alpha = self.meas_bases[node].angle % (2.0 * np.pi) - # (a) the measurement angle is 0 or pi (mod 2pi) and the measurement plane is XY - case_a = is_close_angle(2 * alpha, 0, atol) and self.meas_bases[node].plane == Plane.XY - # (b) the measurement angle is 0.5 pi or 1.5 pi (mod 2pi) and the measurement plane is XZ - case_b = is_close_angle(2 * (alpha - np.pi / 2), 0, atol) and self.meas_bases[node].plane == Plane.XZ + # (a) measurement plane = XY and measurement angle = 0 or pi (mod 2pi) + case_a = self.meas_bases[node].plane == Plane.XY and is_close_angle(2 * alpha, 0, atol) + # (b) measurement plane = XZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) + case_b = self.meas_bases[node].plane == Plane.XZ and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) return case_a or case_b def _remove_clifford(self, node: int, atol: float = 1e-9) -> None: @@ -328,7 +330,7 @@ def remove_clifford(self, node: int, atol: float = 1e-9) -> None: """ self._ensure_node_exists(node) if node in self.input_node_indices or node in self.output_node_indices: - msg = "Clifford node removal not allowed for input node" + msg = "Clifford node removal not allowed for input or output nodes." raise ValueError(msg) if not ( From d882ed40b7d3e37b4ac763f2b8eb7274247b0352 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Wed, 22 Oct 2025 22:48:56 +0900 Subject: [PATCH 26/94] :recycle: Split case corresponds to lemma 4.11 --- graphqomb/zxgraphstate.py | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index ff4ea0f6a..a413888d6 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -76,6 +76,10 @@ def _clifford_rules(self) -> tuple[CliffordRule, ...]: self._needs_pivot, lambda node: self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))), ), + ( + self._is_noninput_with_io_nbrs, + lambda node: self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))), + ), ) def _update_connections( @@ -291,6 +295,48 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: case_b = self.meas_bases[node].plane == Plane.XZ and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) return case_a or case_b + def _is_noninput_with_io_nbrs(self, node: int, atol: float = 1e-9) -> bool: + """Check if the node is non-input and all neighbors are input or output nodes. + + If True, pivot operation is performed on the non-input neighbor and then the node will be removed. + + For this operation, one of the following must hold: + (a) measurement plane = XY and measurement angle = 0 or pi (mod 2pi) + (b) measurement plane = XZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) + + Parameters + ---------- + node : `int` + node index + atol : `float`, optional + absolute tolerance, by default 1e-9 + + Returns + ------- + `bool` + True if the node is non-input and all neighbors are input or output nodes. + + Notes + ----- + In order to follow the algorithm in Theorem 4.12 of Quantum 5, 421 (2021), + this function is not commonalized into _needs_pivot. + + References + ---------- + [1] Backens et al., Quantum 5, 421 (2021). Lemma 4.11 + """ + non_input_nbrs = self.neighbors(node) - set(self.input_node_indices) + # check non_input_nbrs is consisted of only output nodes and is not empty + if not (non_input_nbrs.issubset(set(self.output_node_indices)) and non_input_nbrs): + return False + + alpha = self.meas_bases[node].angle % (2.0 * np.pi) + # (a) measurement plane = XY and measurement angle = 0 or pi (mod 2pi) + case_a = self.meas_bases[node].plane == Plane.XY and is_close_angle(2 * alpha, 0, atol) + # (b) measurement plane = XZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) + case_b = self.meas_bases[node].plane == Plane.XZ and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) + return case_a or case_b + def _remove_clifford(self, node: int, atol: float = 1e-9) -> None: """Perform the Clifford node removal. From 26ba73e214eb4cdfa1196670e42061a82e25bc00 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Wed, 22 Oct 2025 22:49:26 +0900 Subject: [PATCH 27/94] :white_check_mark: Add tests for _is_noninput_with_io_nbrs --- tests/test_zxgraphstate.py | 50 +++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 7fa94cb6b..eef38c1c6 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -414,7 +414,7 @@ def test_remove_clifford_fails_if_nonexistent_node(zx_graph: ZXGraphState) -> No def test_remove_clifford_fails_with_input_node(zx_graph: ZXGraphState) -> None: node = zx_graph.add_physical_node() zx_graph.register_input(node, q_index=0) - with pytest.raises(ValueError, match="Clifford node removal not allowed for input node"): + with pytest.raises(ValueError, match="Clifford node removal not allowed for input or output nodes"): zx_graph.remove_clifford(node) @@ -564,6 +564,54 @@ def test_remove_cliffords_graph2(zx_graph: ZXGraphState) -> None: _test(zx_graph, {0, 2}, {(0, 2)}, exp_measurements=exp_measurements) +@pytest.mark.parametrize( + "planes", + list( + itertools.product( + list(Plane), + [Plane.XY], + ) + ), +) +def test_is_noninput_with_io_nbrs_xy( + zx_graph: ZXGraphState, + planes: tuple[Plane, Plane], + rng: np.random.Generator, +) -> None: + graph_2(zx_graph) + zx_graph.register_input(0, q_index=0) + zx_graph.register_output(2, q_index=0) + angles = [rng.random() * 2 * np.pi for _ in range(2)] + angles[1] = rng.choice([0.0, np.pi]) + measurements = [(i, PlannerMeasBasis(planes[i], angles[i])) for i in range(2)] + _apply_measurements(zx_graph, measurements) + assert zx_graph._is_noninput_with_io_nbrs(1, atol=1e-9) is True + + +@pytest.mark.parametrize( + "planes", + list( + itertools.product( + list(Plane), + [Plane.XZ], + ) + ), +) +def test_is_noninput_with_io_nbrs_xz( + zx_graph: ZXGraphState, + planes: tuple[Plane, Plane], + rng: np.random.Generator, +) -> None: + graph_2(zx_graph) + zx_graph.register_input(0, q_index=0) + zx_graph.register_output(2, q_index=0) + angles = [rng.random() * 2 * np.pi for _ in range(2)] + angles[1] = rng.choice([0.5 * np.pi, 1.5 * np.pi]) + measurements = [(i, PlannerMeasBasis(planes[i], angles[i])) for i in range(2)] + _apply_measurements(zx_graph, measurements) + assert zx_graph._is_noninput_with_io_nbrs(1, atol=1e-9) is True + + def test_remove_cliffords_graph3(zx_graph: ZXGraphState) -> None: graph_3(zx_graph) measurements = [ From 1fde3e921fe8d0e9491718a4fb68d1d8182676cb Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 28 Oct 2025 21:28:15 +0900 Subject: [PATCH 28/94] :bug: Correct pivot definition --- graphqomb/zxgraphstate.py | 111 +++++++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 38 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index a413888d6..f6cb0ebaa 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -156,6 +156,42 @@ def _swap(self, node1: int, node2: int) -> None: self.remove_physical_edge(node2, c) self.add_physical_edge(node1, c) + def _pivot(self, node1: int, node2: int) -> None: + """Pivot edges around nodes u and v in the graph state. + + Parameters + ---------- + node1 : `int` + node index + node2 : `int` + node index + """ + node1_nbrs = self.neighbors(node1) - {node2} + node2_nbrs = self.neighbors(node2) - {node1} + nbr_a = node1_nbrs & node2_nbrs + nbr_b = node1_nbrs - node2_nbrs + nbr_c = node2_nbrs - node1_nbrs + nbr_pairs = [ + bipartite_edges(nbr_a, nbr_b), + bipartite_edges(nbr_a, nbr_c), + bipartite_edges(nbr_b, nbr_c), + ] + + # complement edges between nbr_a, nbr_b, nbr_c + rmv_edges: set[tuple[int, int]] = set() + rmv_edges.update(*(p & self.physical_edges for p in nbr_pairs)) + add_edges: set[tuple[int, int]] = set() + add_edges.update(*(p - self.physical_edges for p in nbr_pairs)) + self._update_connections(rmv_edges, add_edges) + + # swap node u and node v + for b in nbr_b: + self.remove_physical_edge(node1, b) + self.add_physical_edge(node2, b) + for c in nbr_c: + self.remove_physical_edge(node2, c) + self.add_physical_edge(node1, c) + def pivot(self, node1: int, node2: int) -> None: """Pivot operation on the graph state: G∧(uv) (= G*u*v*u = G*v*u*v) for neighboring nodes u and v. @@ -172,6 +208,10 @@ def pivot(self, node1: int, node2: int) -> None: ------ ValueError If the nodes are input nodes, or the graph is not a ZX-diagram. + + References + ---------- + [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.5 with correction """ self._ensure_node_exists(node1) self._ensure_node_exists(node2) @@ -179,33 +219,16 @@ def pivot(self, node1: int, node2: int) -> None: msg = "Cannot apply pivot to input node" raise ValueError(msg) self._check_meas_basis() - - node1_nbrs = self.neighbors(node1) - {node2} - node2_nbrs = self.neighbors(node2) - {node1} - nbr_a = node1_nbrs & node2_nbrs - nbr_b = node1_nbrs - node2_nbrs - nbr_c = node2_nbrs - node1_nbrs - nbr_pairs = [ - bipartite_edges(nbr_a, nbr_b), - bipartite_edges(nbr_a, nbr_c), - bipartite_edges(nbr_b, nbr_c), - ] - rmv_edges: set[tuple[int, int]] = set() - rmv_edges.update(*(p & self.physical_edges for p in nbr_pairs)) - add_edges: set[tuple[int, int]] = set() - add_edges.update(*(p - self.physical_edges for p in nbr_pairs)) - - self._update_connections(rmv_edges, add_edges) - self._swap(node1, node2) + self._pivot(node1, node2) # update node1 and node2 measurement - lc = LocalClifford(np.pi / 2, np.pi / 2, np.pi / 2) + lc = LocalClifford(-np.pi / 2, np.pi / 2, -np.pi / 2) for a in {node1, node2}: self.apply_local_clifford(a, lc) - # update nodes measurement of nbr_a + # update nodes measurement of neighbors lc = LocalClifford(np.pi, 0, 0) - for w in nbr_a: + for w in self.neighbors(node1) | self.neighbors(node2) - {node1, node2}: self.apply_local_clifford(w, lc) def _is_trivial_meas(self, node: int, atol: float = 1e-9) -> bool: @@ -228,16 +251,16 @@ def _is_trivial_meas(self, node: int, atol: float = 1e-9) -> bool: References ---------- - [1] Backens et al., Quantum 5, 421 (2021). Lemma 4.7 + [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.7 """ alpha = self.meas_bases[node].angle % (2.0 * np.pi) - return is_close_angle(2 * alpha, 0, atol) and (self.meas_bases[node].plane in {Plane.YZ, Plane.XZ}) + return (self.meas_bases[node].plane in {Plane.YZ, Plane.XZ}) and is_close_angle(2 * alpha, 0, atol) def _needs_lc(self, node: int, atol: float = 1e-9) -> bool: """Check if the node needs a local complementation in order to perform _remove_clifford. For this operation, the following must hold: - measurement plane = YZ or XY and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) + measurement plane = YZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) Parameters ---------- @@ -253,20 +276,19 @@ def _needs_lc(self, node: int, atol: float = 1e-9) -> bool: References ---------- - [1] Backens et al., Quantum 5, 421 (2021). Lemma 4.8 + [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.8 with correction """ alpha = self.meas_bases[node].angle % (2.0 * np.pi) - return is_close_angle(2 * (alpha - np.pi / 2), 0, atol) and ( - self.meas_bases[node].plane in {Plane.YZ, Plane.XY} - ) + # measurement plane = YZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) + return self.meas_bases[node].plane == Plane.YZ and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: """Check if the node needs a pivot operation in order to perform _remove_clifford. - The pivot operation is performed on the non-input neighbor of the node. - For this operation, one of the following must hold: - (a) measurement plane = XY and measurement angle = 0 or pi (mod 2pi) - (b) measurement plane = XZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) + The pivot operation is performed on the node and its non-input neighbor. + For this operation, either of the following must hold: + (a) measurement plane = XY and measurement angle = 0 or pi (mod 2pi) + (b) measurement plane = XZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) Parameters ---------- @@ -282,7 +304,7 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: References ---------- - [1] Backens et al., Quantum 5, 421 (2021), Lemma 4.9 + [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.9 with correction """ non_input_nbrs = self.neighbors(node) - set(self.input_node_indices) if not non_input_nbrs: @@ -298,7 +320,7 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: def _is_noninput_with_io_nbrs(self, node: int, atol: float = 1e-9) -> bool: """Check if the node is non-input and all neighbors are input or output nodes. - If True, pivot operation is performed on the non-input neighbor and then the node will be removed. + If True, pivot operation is performed on the node and its non-input neighbor, and then the node will be removed. For this operation, one of the following must hold: (a) measurement plane = XY and measurement angle = 0 or pi (mod 2pi) @@ -323,7 +345,7 @@ def _is_noninput_with_io_nbrs(self, node: int, atol: float = 1e-9) -> bool: References ---------- - [1] Backens et al., Quantum 5, 421 (2021). Lemma 4.11 + [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.11 """ non_input_nbrs = self.neighbors(node) - set(self.input_node_indices) # check non_input_nbrs is consisted of only output nodes and is not empty @@ -346,11 +368,23 @@ def _remove_clifford(self, node: int, atol: float = 1e-9) -> None: node index atol : `float`, optional absolute tolerance, by default 1e-9 + + Raises + ------ + ValueError + If the node is not a Clifford node. + + References + ---------- + [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.7 """ a_pi = self.meas_bases[node].angle % (2.0 * np.pi) - coeff = 0.0 if is_close_angle(a_pi, 0, atol) else 1.0 - lc = LocalClifford(coeff * np.pi, 0, 0) - for v in self.neighbors(node) - set(self.output_node_indices): + if not is_close_angle(2 * a_pi, 0, atol): + msg = "This node cannot be removed by _remove_clifford." + raise ValueError(msg) + + lc = LocalClifford(a_pi, 0, 0) + for v in self.neighbors(node): self.apply_local_clifford(v, lc) self.remove_physical_node(node) @@ -416,6 +450,7 @@ def is_removable_clifford(self, node: int, atol: float = 1e-9) -> bool: self._is_trivial_meas(node, atol), self._needs_lc(node, atol), self._needs_pivot(node, atol), + self._is_noninput_with_io_nbrs(node, atol), ] ) From 68dbbce9285018ebd64777071ddfd428015ffeea Mon Sep 17 00:00:00 2001 From: nabe98 Date: Wed, 29 Oct 2025 20:08:14 +0900 Subject: [PATCH 29/94] :sparkles: Add new case for removing cliffords --- graphqomb/zxgraphstate.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index f6cb0ebaa..25ca1dfd3 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -76,6 +76,7 @@ def _clifford_rules(self) -> tuple[CliffordRule, ...]: self._needs_pivot, lambda node: self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))), ), + (self._needs_pivot_and_lc, self._apply_pivot_and_lc), ( self._is_noninput_with_io_nbrs, lambda node: self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))), @@ -317,6 +318,39 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: case_b = self.meas_bases[node].plane == Plane.XZ and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) return case_a or case_b + def _needs_pivot_and_lc(self, node: int, atol: float = 1e-9) -> bool: + """Check if the node needs a pivot and a local complementation in order to perform _remove_clifford. + + For this operation, the following must hold: + measurement plane = XZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) + + Parameters + ---------- + node : `int` + node index + atol : `float`, optional + absolute tolerance, by default 1e-9 + + Returns + ------- + `bool` + True if the node needs a pivot operation + followed by a local complementation. + """ + alpha = self.meas_bases[node].angle % (2.0 * np.pi) + return self.meas_bases[node].plane == Plane.XZ and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) + + def _apply_pivot_and_lc(self, node: int) -> None: + """Apply pivot and then local complement operations on the node. + + Parameters + ---------- + node : `int` + node index + """ + self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))) + self.local_complement(node) + def _is_noninput_with_io_nbrs(self, node: int, atol: float = 1e-9) -> bool: """Check if the node is non-input and all neighbors are input or output nodes. @@ -450,6 +484,7 @@ def is_removable_clifford(self, node: int, atol: float = 1e-9) -> bool: self._is_trivial_meas(node, atol), self._needs_lc(node, atol), self._needs_pivot(node, atol), + self._needs_pivot_and_lc(node, atol), self._is_noninput_with_io_nbrs(node, atol), ] ) From 8207c0d6fa338f819be2ae07a2ed984a4088978c Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 17 Nov 2025 20:22:11 +0900 Subject: [PATCH 30/94] :fire: Delete unnecessary process --- graphqomb/zxgraphstate.py | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 25ca1dfd3..f6cb0ebaa 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -76,7 +76,6 @@ def _clifford_rules(self) -> tuple[CliffordRule, ...]: self._needs_pivot, lambda node: self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))), ), - (self._needs_pivot_and_lc, self._apply_pivot_and_lc), ( self._is_noninput_with_io_nbrs, lambda node: self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))), @@ -318,39 +317,6 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: case_b = self.meas_bases[node].plane == Plane.XZ and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) return case_a or case_b - def _needs_pivot_and_lc(self, node: int, atol: float = 1e-9) -> bool: - """Check if the node needs a pivot and a local complementation in order to perform _remove_clifford. - - For this operation, the following must hold: - measurement plane = XZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) - - Parameters - ---------- - node : `int` - node index - atol : `float`, optional - absolute tolerance, by default 1e-9 - - Returns - ------- - `bool` - True if the node needs a pivot operation - followed by a local complementation. - """ - alpha = self.meas_bases[node].angle % (2.0 * np.pi) - return self.meas_bases[node].plane == Plane.XZ and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) - - def _apply_pivot_and_lc(self, node: int) -> None: - """Apply pivot and then local complement operations on the node. - - Parameters - ---------- - node : `int` - node index - """ - self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))) - self.local_complement(node) - def _is_noninput_with_io_nbrs(self, node: int, atol: float = 1e-9) -> bool: """Check if the node is non-input and all neighbors are input or output nodes. @@ -484,7 +450,6 @@ def is_removable_clifford(self, node: int, atol: float = 1e-9) -> bool: self._is_trivial_meas(node, atol), self._needs_lc(node, atol), self._needs_pivot(node, atol), - self._needs_pivot_and_lc(node, atol), self._is_noninput_with_io_nbrs(node, atol), ] ) From 8b1eb912797852709641ad9e0d55dfc679c3ba13 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 17 Nov 2025 20:23:23 +0900 Subject: [PATCH 31/94] :art: Fix docstring --- graphqomb/zxgraphstate.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index f6cb0ebaa..c54d18206 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -195,8 +195,6 @@ def _pivot(self, node1: int, node2: int) -> None: def pivot(self, node1: int, node2: int) -> None: """Pivot operation on the graph state: G∧(uv) (= G*u*v*u = G*v*u*v) for neighboring nodes u and v. - In order to maintain the ZX-diagram simple, pi-spiders are shifted properly. - Parameters ---------- node1 : `int` @@ -234,8 +232,9 @@ def pivot(self, node1: int, node2: int) -> None: def _is_trivial_meas(self, node: int, atol: float = 1e-9) -> bool: """Check if the node does not need any operation in order to perform _remove_clifford. - For this operation, the following must hold: - measurement plane = YZ or XZ and measurement angle = 0 or pi (mod 2pi) + For this operation, the followings must hold: + measurement plane = YZ or XZ + measurement angle = 0 or pi (mod 2pi) Parameters ---------- @@ -259,8 +258,9 @@ def _is_trivial_meas(self, node: int, atol: float = 1e-9) -> bool: def _needs_lc(self, node: int, atol: float = 1e-9) -> bool: """Check if the node needs a local complementation in order to perform _remove_clifford. - For this operation, the following must hold: - measurement plane = YZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) + For this operation, the followings must hold: + measurement plane = XY or YZ + measurement angle = 0.5 pi or 1.5 pi (mod 2pi) Parameters ---------- @@ -304,7 +304,7 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: References ---------- - [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.9 with correction + [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.9 """ non_input_nbrs = self.neighbors(node) - set(self.input_node_indices) if not non_input_nbrs: @@ -348,7 +348,7 @@ def _is_noninput_with_io_nbrs(self, node: int, atol: float = 1e-9) -> bool: [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.11 """ non_input_nbrs = self.neighbors(node) - set(self.input_node_indices) - # check non_input_nbrs is consisted of only output nodes and is not empty + # check non_input_nbrs is composed of only output nodes and is not empty if not (non_input_nbrs.issubset(set(self.output_node_indices)) and non_input_nbrs): return False @@ -407,6 +407,10 @@ def remove_clifford(self, node: int, atol: float = 1e-9) -> None: 3. If all neighbors are input nodes in some special cases ((meas_plane, meas_angle) = (XY, a pi), (XZ, a pi/2) for a = 0, 1). 4. If the node has no neighbors that are not connected only to output nodes. + + References + ---------- + [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Theorem 4.12 """ self._ensure_node_exists(node) if node in self.input_node_indices or node in self.output_node_indices: From 47eb80d1c0a4958f3827a8caa80ae3182ee83088 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 17 Nov 2025 20:24:14 +0900 Subject: [PATCH 32/94] :bug: Fix bug in preprocess --- graphqomb/zxgraphstate.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index c54d18206..048bdffb7 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -276,11 +276,10 @@ def _needs_lc(self, node: int, atol: float = 1e-9) -> bool: References ---------- - [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.8 with correction + [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.8 """ alpha = self.meas_bases[node].angle % (2.0 * np.pi) - # measurement plane = YZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) - return self.meas_bases[node].plane == Plane.YZ and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) + return self.meas_bases[node].plane in {Plane.XY, Plane.YZ} and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: """Check if the node needs a pivot operation in order to perform _remove_clifford. From 790aeb3ca3b6b1f61e01510842bfce498d8bf9dd Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 17 Nov 2025 20:58:42 +0900 Subject: [PATCH 33/94] :bug: Fix expand_local_cliffords --- graphqomb/graphstate.py | 90 +++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index 46638a256..13f3e5d16 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -21,16 +21,15 @@ from abc import ABC from typing import TYPE_CHECKING, NamedTuple +import numpy as np import typing_extensions from graphqomb.common import MeasBasis, Plane, PlannerMeasBasis -from graphqomb.euler import update_lc_basis, update_lc_lc +from graphqomb.euler import LocalClifford, is_close_angle, update_lc_basis, update_lc_lc if TYPE_CHECKING: from collections.abc import Set as AbstractSet - from graphqomb.euler import LocalClifford - class BaseGraphState(ABC): """Abstract base class for Graph State.""" @@ -540,15 +539,15 @@ def _pop_local_clifford(self, node: int) -> LocalClifford | None: """ return self.__local_cliffords.pop(node, None) - def _expand_input_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: + def _expand_input_local_cliffords(self) -> dict[int, InputLocalCliffordExpansion]: r"""Expand local Clifford operators applied on the input nodes. Returns ------- - `dict`\[`int`, `LocalCliffordExpansion`\] + `dict`\[`int`, `InputLocalCliffordExpansion`\] A dictionary mapping input node indices to the new node indices created. """ - node_index_addition_map: dict[int, LocalCliffordExpansion] = {} + node_index_addition_map: dict[int, InputLocalCliffordExpansion] = {} new_input_indices: dict[int, int] = {} for input_node, q_index in self.input_node_indices.items(): lc = self._pop_local_clifford(input_node) @@ -559,19 +558,39 @@ def _expand_input_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: new_node_index0 = self.add_physical_node() new_input_indices[new_node_index0] = q_index new_node_index1 = self.add_physical_node() - new_node_index2 = self.add_physical_node() self.add_physical_edge(new_node_index0, new_node_index1) - self.add_physical_edge(new_node_index1, new_node_index2) - self.add_physical_edge(new_node_index2, input_node) - - self.assign_meas_basis(new_node_index0, PlannerMeasBasis(Plane.XY, lc.alpha)) - self.assign_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, lc.beta)) - self.assign_meas_basis(new_node_index2, PlannerMeasBasis(Plane.XY, lc.gamma)) - - node_index_addition_map[input_node] = LocalCliffordExpansion( - new_node_index0, new_node_index1, new_node_index2 - ) + self.add_physical_edge(new_node_index1, input_node) + + self.assign_meas_basis(new_node_index0, PlannerMeasBasis(Plane.XY, -lc.alpha)) + self.assign_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, -lc.beta)) + + gamma = lc.gamma + delta = self.meas_bases[input_node].angle + meas_plane = self.meas_bases[input_node].plane + if meas_plane == Plane.XY: + meas_plane = Plane.XY + delta -= gamma + elif meas_plane == Plane.XZ and is_close_angle(2 * gamma, 0.0): + meas_plane = Plane.XZ + sign = 1 if is_close_angle(gamma, 0.0) else -1 + delta *= sign + elif meas_plane == Plane.XZ and is_close_angle(2 * (gamma - np.pi / 2), 0.0): + meas_plane = Plane.YZ + sign = 1 if is_close_angle(gamma + np.pi / 2, 0.0) else -1 + delta *= sign + elif meas_plane == Plane.YZ and is_close_angle(2 * gamma, 0.0): + meas_plane = Plane.YZ + sign = 1 if is_close_angle(gamma, 0.0) else -1 + delta *= sign + elif meas_plane == Plane.YZ and is_close_angle(2 * (gamma - np.pi / 2), 0.0): + meas_plane = Plane.XZ + sign = 1 if is_close_angle(gamma + np.pi / 2, 0.0) else -1 + delta *= sign + + self.assign_meas_basis(input_node, PlannerMeasBasis(meas_plane, delta)) + + node_index_addition_map[input_node] = InputLocalCliffordExpansion(new_node_index0, new_node_index1) self.__input_node_indices = {} for new_input_index, q_index in new_input_indices.items(): @@ -579,15 +598,15 @@ def _expand_input_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: return node_index_addition_map - def _expand_output_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: + def _expand_output_local_cliffords(self) -> dict[int, OutputLocalCliffordExpansion]: r"""Expand local Clifford operators applied on the output nodes. Returns ------- - `dict`\[`int`, `LocalCliffordExpansion`\] + `dict`\[`int`, `OutputLocalCliffordExpansion`\] A dictionary mapping output node indices to the new node indices created. """ - node_index_addition_map: dict[int, LocalCliffordExpansion] = {} + node_index_addition_map: dict[int, OutputLocalCliffordExpansion] = {} new_output_index_map: dict[int, int] = {} for output_node, q_index in self.output_node_indices.items(): lc = self._pop_local_clifford(output_node) @@ -598,18 +617,21 @@ def _expand_output_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: new_node_index0 = self.add_physical_node() new_node_index1 = self.add_physical_node() new_node_index2 = self.add_physical_node() - new_output_index_map[new_node_index2] = q_index + new_node_index3 = self.add_physical_node() + new_output_index_map[new_node_index3] = q_index self.add_physical_edge(output_node, new_node_index0) self.add_physical_edge(new_node_index0, new_node_index1) self.add_physical_edge(new_node_index1, new_node_index2) + self.add_physical_edge(new_node_index2, new_node_index3) - self.assign_meas_basis(output_node, PlannerMeasBasis(Plane.XY, lc.alpha)) - self.assign_meas_basis(new_node_index0, PlannerMeasBasis(Plane.XY, lc.beta)) - self.assign_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, lc.gamma)) + self.assign_meas_basis(output_node, PlannerMeasBasis(Plane.XY, -lc.alpha)) + self.assign_meas_basis(new_node_index0, PlannerMeasBasis(Plane.XY, -lc.beta)) + self.assign_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, -lc.gamma)) + self.assign_meas_basis(new_node_index2, PlannerMeasBasis(Plane.XY, 0.0)) - node_index_addition_map[output_node] = LocalCliffordExpansion( - new_node_index0, new_node_index1, new_node_index2 + node_index_addition_map[output_node] = OutputLocalCliffordExpansion( + new_node_index0, new_node_index1, new_node_index2, new_node_index3 ) self.__output_node_indices = {} @@ -619,19 +641,27 @@ def _expand_output_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: return node_index_addition_map -class LocalCliffordExpansion(NamedTuple): - """Local Clifford expansion map for each input/output node.""" +class InputLocalCliffordExpansion(NamedTuple): + """Local Clifford expansion map for input node.""" + + node1: int + node2: int + + +class OutputLocalCliffordExpansion(NamedTuple): + """Local Clifford expansion map for output node.""" node1: int node2: int node3: int + node4: int class ExpansionMaps(NamedTuple): """Expansion maps for inputs and outputs with Local Clifford.""" - input_node_map: dict[int, LocalCliffordExpansion] - output_node_map: dict[int, LocalCliffordExpansion] + input_node_map: dict[int, InputLocalCliffordExpansion] + output_node_map: dict[int, OutputLocalCliffordExpansion] def compose( # noqa: C901 From 92c962eb9003afe50513c14f2e1a172ff5c2846c Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 17 Nov 2025 21:01:32 +0900 Subject: [PATCH 34/94] :art: Add demo to assert zxgraph simplification validity --- examples/zxgraph_simplification.py | 175 ++++++++++++++++++++++++++--- 1 file changed, 162 insertions(+), 13 deletions(-) diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py index 5bebd25f2..41eee9d28 100644 --- a/examples/zxgraph_simplification.py +++ b/examples/zxgraph_simplification.py @@ -13,20 +13,46 @@ # %% from copy import deepcopy +import networkx as nx import numpy as np +import swiflow as sf +from swiflow import gflow -from graphix_zx.circuit import circuit2graph -from graphix_zx.random_objects import random_circ -from graphix_zx.visualizer import visualize -from graphix_zx.zxgraphstate import to_zx_graphstate +from graphqomb.common import Plane +from graphqomb.graphstate import BaseGraphState +from graphqomb.qompiler import qompile +from graphqomb.random_objects import generate_random_flow_graph +from graphqomb.simulator import PatternSimulator, SimulatorBackend +from graphqomb.visualizer import visualize +from graphqomb.zxgraphstate import ZXGraphState, to_zx_graphstate + +FlowLike = dict[int, set[int]] # %% -# Create a random circuit and convert it to a ZXGraphState -circ = random_circ(4, 4) -graph, flow = circuit2graph(circ) +# Create a random graph state with flow +graph, flow = generate_random_flow_graph(width=3, depth=4, edge_p=0.5) zx_graph, _ = to_zx_graphstate(graph) visualize(zx_graph) + +# %% +def print_boundary_lcs(zxgraph: ZXGraphState) -> None: + lc_map = zxgraph.local_cliffords + for node in zxgraph.input_node_indices | zxgraph.output_node_indices: + # check lc on input and output nodes + lc = lc_map.get(node, None) + if lc is not None: + if node in zxgraph.input_node_indices: + print(f"Input node {node} has local Clifford: alpha={lc.alpha}, beta={lc.beta}, gamma={lc.gamma}") + else: + print(f"Output node {node} has local Clifford: alpha={lc.alpha}, beta={lc.beta}, gamma={lc.gamma}") + else: + print(f"Node {node} has no local Clifford.") + + +# %% +print_boundary_lcs(zx_graph) + # %% # Initial graph state before simplification print("node | plane | angle (/pi)") @@ -37,26 +63,149 @@ for node in zx_graph.output_node_indices: print(f"{node} (output)", "-", "-") + # %% # Simplify the graph state by full_reduce method zx_graph_smp = deepcopy(zx_graph) -zx_graph_smp.full_reduce() +# zx_graph_smp.full_reduce() +zx_graph_smp.remove_cliffords() # %% # Simplified graph state after full_reduce. visualize(zx_graph_smp) print("node | plane | angle (/pi)") -for node in zx_graph.input_node_indices: - 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 - set(zx_graph.input_node_indices) - set(zx_graph_smp.output_node_indices): +for node in zx_graph_smp.input_node_indices: + print(f"{node} (input)", zx_graph_smp.meas_bases[node].plane, zx_graph_smp.meas_bases[node].angle / np.pi) +for node in zx_graph_smp.physical_nodes - set(zx_graph_smp.input_node_indices) - set(zx_graph_smp.output_node_indices): print(node, zx_graph_smp.meas_bases[node].plane, zx_graph_smp.meas_bases[node].angle / np.pi) for node in zx_graph_smp.output_node_indices: print(f"{node} (output)", "-", "-") +# %% +print(zx_graph_smp.input_node_indices, "\n", zx_graph_smp.output_node_indices, "\n", zx_graph_smp.physical_edges) +print_boundary_lcs(zx_graph_smp) + # %% # Supplementary Note: # At first glance, the input/output nodes appear to remain unaffected. # However, note that a local Clifford operation is actually applied as a result of the action of the full_reduce method. -# If you visualize the circuit after executing the `expand_local_cliffords` method, -# you will see that the additional nodes are now visible on the input/output qubits. +# If you visualize the graph state after executing the `expand_local_cliffords` method, +# you will see additional nodes connected to the former input/output nodes, +# indicating that local Clifford operations on the input/output nodes have been expanded into the graph. + + +# %% +# Let us compare the graph state before and after simplification. +# First, we simulate the original graph state and get the resulting statevector. +pattern = qompile(zx_graph, flow) +sim = PatternSimulator(pattern, backend=SimulatorBackend.StateVector) +sim.simulate() +statevec_original = sim.state + +# %% +# Next, we simulate the pattern obtained from the simplified graph state. +# Here is a wrapper function to find gflow using swiflow.gflow. + + +def gflow_wrapper(graphstate: BaseGraphState) -> FlowLike: + """Wrapper function for swiflow.gflow + + Parameters + ---------- + graphstate : `BaseGraphState` + graph state to find gflow + + Returns + ------- + `FlowLike` + gflow object + + Raises + ------ + ValueError + If no gflow is found + """ + graph = nx.Graph() + graph.add_nodes_from(graphstate.physical_nodes) + graph.add_edges_from(graphstate.physical_edges) + + bases = graphstate.meas_bases + planes = {node: bases[node].plane for node in bases} + swiflow_planes = {} + for node, plane in planes.items(): + if plane == Plane.XY: + swiflow_planes[node] = sf.common.Plane.XY + elif plane == Plane.YZ: + swiflow_planes[node] = sf.common.Plane.YZ + elif plane == Plane.XZ: + swiflow_planes[node] = sf.common.Plane.XZ + else: + msg = f"No match {plane}" + raise ValueError(msg) + + gflow_object = gflow.find( + graph, set(graphstate.input_node_indices), set(graphstate.output_node_indices), swiflow_planes + ) + print(gflow_object) + if gflow_object is None: + msg = "No flow found" + raise ValueError(msg) + + gflow_obj = gflow_object.f + + return {node: {child for child in children if child != node} for node, children in gflow_obj.items()} + + +# %% +# Note that we need to call the `expand_local_cliffords` method before generating the pattern to get the gflow. + +zx_graph_smp.expand_local_cliffords() +print("input_node_indices: ", set(zx_graph_smp.input_node_indices)) +print("output_node_indices: ", set(zx_graph_smp.output_node_indices)) +print("local_cliffords: ", zx_graph_smp.local_cliffords) +print("node | plane | angle (/pi)") +for node in zx_graph_smp.input_node_indices: + print(f"{node} (input)", zx_graph_smp.meas_bases[node].plane, zx_graph_smp.meas_bases[node].angle / np.pi) + +for node in zx_graph_smp.physical_nodes - set(zx_graph_smp.input_node_indices) - set(zx_graph_smp.output_node_indices): + print(node, zx_graph_smp.meas_bases[node].plane, zx_graph_smp.meas_bases[node].angle / np.pi) + +for node in zx_graph_smp.output_node_indices: + print(f"{node} (output)", "-", "-") +gflow_smp = gflow_wrapper(zx_graph_smp) +visualize(zx_graph_smp) +print_boundary_lcs(zx_graph_smp) + +# %% +# Now we can compile the simplified graph state into a measurement pattern and simulate it. +pattern_smp = qompile(zx_graph_smp, gflow_smp) +sim_smp = PatternSimulator(pattern_smp, backend=SimulatorBackend.StateVector) +sim_smp.simulate() +print(pattern_smp) + +# %% +statevec_smp = sim_smp.state +# %% +# normalization check +print("norm of original statevector:", np.linalg.norm(statevec_original.state())) +print("norm of simplified statevector:", np.linalg.norm(statevec_smp.state())) + +# %% +# Finally, we compare the expectation values of random observables before and after simplification. +rng = np.random.default_rng() +rand_mat = rng.random((2, 2)) + 1j * rng.random((2, 2)) +rand_mat += rand_mat.T.conj() +exp = statevec_original.expectation(rand_mat, [2]) +exp_cr = statevec_smp.expectation(rand_mat, [2]) +print("Expectation values for rand_mat\n===============================") +print("rand_mat: \n", rand_mat) + +print("Original: ", exp, "\nAfter simplification: ", exp_cr) +print("norm: ", np.linalg.norm(statevec_original.state()), np.linalg.norm(statevec_smp.state())) +print("data shape: ", statevec_original.state().shape, statevec_smp.state().shape) +psi_org = statevec_original.state() +psi_smp = statevec_smp.state() +print("inner product: ", np.sqrt(np.abs(np.vdot(psi_org, psi_smp)))) + +# %% From f6a55e011f606c66d9f5550aebc07fcc8517b0ca Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 17 Nov 2025 21:19:23 +0900 Subject: [PATCH 35/94] :art: Fix type hints --- examples/zxgraph_simplification.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py index 41eee9d28..14752deae 100644 --- a/examples/zxgraph_simplification.py +++ b/examples/zxgraph_simplification.py @@ -11,7 +11,11 @@ """ # %% + +from __future__ import annotations + from copy import deepcopy +from typing import TYPE_CHECKING, Any import networkx as nx import numpy as np @@ -19,13 +23,17 @@ from swiflow import gflow from graphqomb.common import Plane -from graphqomb.graphstate import BaseGraphState from graphqomb.qompiler import qompile from graphqomb.random_objects import generate_random_flow_graph from graphqomb.simulator import PatternSimulator, SimulatorBackend from graphqomb.visualizer import visualize from graphqomb.zxgraphstate import ZXGraphState, to_zx_graphstate +if TYPE_CHECKING: + from networkx import Graph as NxGraph + + from graphqomb.graphstate import BaseGraphState + FlowLike = dict[int, set[int]] # %% @@ -126,7 +134,7 @@ def gflow_wrapper(graphstate: BaseGraphState) -> FlowLike: ValueError If no gflow is found """ - graph = nx.Graph() + graph: NxGraph[Any] = nx.Graph() graph.add_nodes_from(graphstate.physical_nodes) graph.add_edges_from(graphstate.physical_edges) From 253d71f839ed151298eee334825ce15e9c0c958f Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 17 Nov 2025 21:31:53 +0900 Subject: [PATCH 36/94] :art: Fix import --- graphqomb/graphstate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index db51df46b..c0c0523e3 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -26,8 +26,8 @@ import numpy as np import typing_extensions -from graphqomb.common import MeasBasis, Plane, PlannerMeasBasis -from graphqomb.euler import LocalClifford, is_close_angle, update_lc_basis, update_lc_lc +from graphqomb.common import MeasBasis, Plane, PlannerMeasBasis, is_close_angle +from graphqomb.euler import LocalClifford, update_lc_basis, update_lc_lc if TYPE_CHECKING: from graphqomb.euler import LocalClifford From 525bd4250dff698ce395af45a0414228db2ad44d Mon Sep 17 00:00:00 2001 From: nabe98 Date: Mon, 17 Nov 2025 23:07:31 +0900 Subject: [PATCH 37/94] :bug: Fix bug in _expnad_input_local_cliffords --- graphqomb/graphstate.py | 2 +- tests/test_graphstate.py | 84 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index c0c0523e3..69be61e7f 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -589,7 +589,7 @@ def _expand_input_local_cliffords(self) -> dict[int, InputLocalCliffordExpansion delta *= sign elif meas_plane == Plane.YZ and is_close_angle(2 * (gamma - np.pi / 2), 0.0): meas_plane = Plane.XZ - sign = 1 if is_close_angle(gamma + np.pi / 2, 0.0) else -1 + sign = 1 if is_close_angle(gamma - np.pi / 2, 0.0) else -1 delta *= sign self.assign_meas_basis(input_node, PlannerMeasBasis(meas_plane, delta)) diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index e649ff916..9758bc351 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from graphqomb.common import Plane, PlannerMeasBasis +from graphqomb.common import Plane, PlannerMeasBasis, is_close_angle from graphqomb.euler import LocalClifford from graphqomb.graphstate import GraphState, bipartite_edges, odd_neighbors @@ -193,6 +193,88 @@ def test_assign_meas_basis(graph: GraphState) -> None: assert graph.meas_bases[node_index].angle == 0.5 * np.pi +@pytest.mark.parametrize( + "gamma", + [0, np.pi / 2, np.pi, 3 * np.pi / 2], +) +def test_expand_input_local_cliffords_xy_plane(graph: GraphState, gamma: float) -> None: + """Test expanding local Clifford operators on input nodes with XY measurement plane.""" + old_input_node = graph.add_physical_node() + output_node = graph.add_physical_node() + graph.add_physical_edge(old_input_node, output_node) + graph.register_input(old_input_node, 0) + graph.register_output(output_node, 0) + old_input_angle = np.pi / 3 + graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.XY, old_input_angle)) + + new_input_node = 2 + lc = LocalClifford(alpha=0.0, beta=0.0, gamma=gamma) + graph.apply_local_clifford(old_input_node, lc) + graph.expand_local_cliffords() + + assert graph.input_node_indices == {new_input_node: 0} + for node in graph.physical_nodes - set(graph.output_node_indices): + assert graph.meas_bases[node].plane == Plane.XY + assert is_close_angle(graph.meas_bases[new_input_node].angle, 0.0) + assert is_close_angle(graph.meas_bases[new_input_node + 1].angle, 0.0) + correction = gamma if is_close_angle(2 * gamma, 0) else -gamma + assert is_close_angle(graph.meas_bases[old_input_node].angle, old_input_angle + correction) + + +@pytest.mark.parametrize( + "gamma", + [0, np.pi, np.pi / 2, 3 * np.pi / 2], +) +def test_expand_input_local_cliffords_yz_plane(graph: GraphState, gamma: float) -> None: + """Test expanding local Clifford operators on input nodes with YZ measurement plane.""" + old_input_node = graph.add_physical_node() + output_node = graph.add_physical_node() + graph.add_physical_edge(old_input_node, output_node) + graph.register_input(old_input_node, 0) + graph.register_output(output_node, 0) + old_input_angle = np.pi / 3 + graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.YZ, old_input_angle)) + + lc = LocalClifford(alpha=0.0, beta=0.0, gamma=gamma) + graph.apply_local_clifford(old_input_node, lc) + graph.expand_local_cliffords() + + if is_close_angle(2 * gamma, 0): + assert graph.meas_bases[old_input_node].plane == Plane.YZ + exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle + elif is_close_angle(2 * (gamma - np.pi / 2), 0): + assert graph.meas_bases[old_input_node].plane == Plane.XZ + exp_angle = old_input_angle if is_close_angle(gamma - np.pi / 2, 0) else -old_input_angle + assert is_close_angle(graph.meas_bases[old_input_node].angle, exp_angle) + + +@pytest.mark.parametrize( + "gamma", + [0, np.pi], +) +def test_expand_input_local_cliffords_xz_plane(graph: GraphState, gamma: float) -> None: + """Test expanding local Clifford operators on input nodes with XZ measurement plane.""" + old_input_node = graph.add_physical_node() + output_node = graph.add_physical_node() + graph.add_physical_edge(old_input_node, output_node) + graph.register_input(old_input_node, 0) + graph.register_output(output_node, 0) + old_input_angle = np.pi / 3 + graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.XZ, old_input_angle)) + + lc = LocalClifford(alpha=0.0, beta=0.0, gamma=gamma) + graph.apply_local_clifford(old_input_node, lc) + graph.expand_local_cliffords() + + if is_close_angle(2 * gamma, 0): + assert graph.meas_bases[old_input_node].plane == Plane.XZ + exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle + elif is_close_angle(2 * (gamma + np.pi / 2), 0): + assert graph.meas_bases[old_input_node].plane == Plane.YZ + exp_angle = old_input_angle if is_close_angle(gamma + np.pi / 2, 0) else -old_input_angle + assert is_close_angle(graph.meas_bases[old_input_node].angle, exp_angle) + + def test_check_canonical_form_true(canonical_graph: GraphState) -> None: """Test if the graph is in canonical form.""" canonical_graph.check_canonical_form() From 41f5fe86aa7daf55eeb6fc3f1bedb98a06249297 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 23 Nov 2025 21:56:10 +0900 Subject: [PATCH 38/94] :white_check_mark: Update tests for expand_local_cliffords --- tests/test_graphstate.py | 66 ++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index 9758bc351..648c8ebdf 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -198,7 +198,7 @@ def test_assign_meas_basis(graph: GraphState) -> None: [0, np.pi / 2, np.pi, 3 * np.pi / 2], ) def test_expand_input_local_cliffords_xy_plane(graph: GraphState, gamma: float) -> None: - """Test expanding local Clifford operators on input nodes with XY measurement plane.""" + """Test expanding local Clifford operators on an input node with XY measurement plane.""" old_input_node = graph.add_physical_node() output_node = graph.add_physical_node() graph.add_physical_edge(old_input_node, output_node) @@ -207,18 +207,19 @@ def test_expand_input_local_cliffords_xy_plane(graph: GraphState, gamma: float) old_input_angle = np.pi / 3 graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.XY, old_input_angle)) - new_input_node = 2 - lc = LocalClifford(alpha=0.0, beta=0.0, gamma=gamma) + alpha = 0.0 + beta = 0.0 + lc = LocalClifford(alpha=alpha, beta=beta, gamma=gamma) graph.apply_local_clifford(old_input_node, lc) graph.expand_local_cliffords() + new_input_node = 2 assert graph.input_node_indices == {new_input_node: 0} for node in graph.physical_nodes - set(graph.output_node_indices): assert graph.meas_bases[node].plane == Plane.XY - assert is_close_angle(graph.meas_bases[new_input_node].angle, 0.0) - assert is_close_angle(graph.meas_bases[new_input_node + 1].angle, 0.0) - correction = gamma if is_close_angle(2 * gamma, 0) else -gamma - assert is_close_angle(graph.meas_bases[old_input_node].angle, old_input_angle + correction) + assert is_close_angle(graph.meas_bases[new_input_node].angle, old_input_angle - gamma) + assert is_close_angle(graph.meas_bases[new_input_node + 1].angle, -beta) + assert is_close_angle(graph.meas_bases[old_input_node].angle, -alpha) @pytest.mark.parametrize( @@ -226,7 +227,7 @@ def test_expand_input_local_cliffords_xy_plane(graph: GraphState, gamma: float) [0, np.pi, np.pi / 2, 3 * np.pi / 2], ) def test_expand_input_local_cliffords_yz_plane(graph: GraphState, gamma: float) -> None: - """Test expanding local Clifford operators on input nodes with YZ measurement plane.""" + """Test expanding local Clifford operators on an input node with YZ measurement plane.""" old_input_node = graph.add_physical_node() output_node = graph.add_physical_node() graph.add_physical_edge(old_input_node, output_node) @@ -235,25 +236,28 @@ def test_expand_input_local_cliffords_yz_plane(graph: GraphState, gamma: float) old_input_angle = np.pi / 3 graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.YZ, old_input_angle)) - lc = LocalClifford(alpha=0.0, beta=0.0, gamma=gamma) + alpha = 0.0 + beta = 0.0 + lc = LocalClifford(alpha=alpha, beta=beta, gamma=gamma) graph.apply_local_clifford(old_input_node, lc) graph.expand_local_cliffords() + new_input_node = 2 if is_close_angle(2 * gamma, 0): - assert graph.meas_bases[old_input_node].plane == Plane.YZ + assert graph.meas_bases[new_input_node].plane == Plane.YZ exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle elif is_close_angle(2 * (gamma - np.pi / 2), 0): - assert graph.meas_bases[old_input_node].plane == Plane.XZ + assert graph.meas_bases[new_input_node].plane == Plane.XZ exp_angle = old_input_angle if is_close_angle(gamma - np.pi / 2, 0) else -old_input_angle - assert is_close_angle(graph.meas_bases[old_input_node].angle, exp_angle) + assert is_close_angle(graph.meas_bases[new_input_node].angle, exp_angle) @pytest.mark.parametrize( "gamma", - [0, np.pi], + [0, np.pi / 2, np.pi, 3 * np.pi / 2], ) def test_expand_input_local_cliffords_xz_plane(graph: GraphState, gamma: float) -> None: - """Test expanding local Clifford operators on input nodes with XZ measurement plane.""" + """Test expanding local Clifford operators on an input node with XZ measurement plane.""" old_input_node = graph.add_physical_node() output_node = graph.add_physical_node() graph.add_physical_edge(old_input_node, output_node) @@ -262,17 +266,41 @@ def test_expand_input_local_cliffords_xz_plane(graph: GraphState, gamma: float) old_input_angle = np.pi / 3 graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.XZ, old_input_angle)) - lc = LocalClifford(alpha=0.0, beta=0.0, gamma=gamma) + alpha = 0.0 + beta = 0.0 + lc = LocalClifford(alpha=alpha, beta=beta, gamma=gamma) graph.apply_local_clifford(old_input_node, lc) graph.expand_local_cliffords() + new_input_node = 2 if is_close_angle(2 * gamma, 0): - assert graph.meas_bases[old_input_node].plane == Plane.XZ + assert graph.meas_bases[new_input_node].plane == Plane.XZ exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle - elif is_close_angle(2 * (gamma + np.pi / 2), 0): - assert graph.meas_bases[old_input_node].plane == Plane.YZ + elif is_close_angle(2 * (gamma - np.pi / 2), 0): + assert graph.meas_bases[new_input_node].plane == Plane.YZ exp_angle = old_input_angle if is_close_angle(gamma + np.pi / 2, 0) else -old_input_angle - assert is_close_angle(graph.meas_bases[old_input_node].angle, exp_angle) + assert is_close_angle(graph.meas_bases[new_input_node].angle, exp_angle) + + +def test_expand_output_local_cliffords(graph: GraphState) -> None: + """Test expanding local Clifford operators on an output node.""" + input_node = graph.add_physical_node() + old_output_node = graph.add_physical_node() + graph.add_physical_edge(input_node, old_output_node) + graph.register_input(input_node, 0) + graph.register_output(old_output_node, 0) + graph.assign_meas_basis(input_node, PlannerMeasBasis(Plane.XY, 0.0)) + + lc = LocalClifford(alpha=np.pi / 2, beta=np.pi, gamma=3 * np.pi / 2) + graph.apply_local_clifford(old_output_node, lc) + graph.expand_local_cliffords() + + new_output_node = 5 + assert graph.output_node_indices == {new_output_node: 0} + exp_results = [(1, np.pi / 2), (2, np.pi), (3, 3 * np.pi / 2), (4, 0.0)] + for node, angle in exp_results: + assert graph.meas_bases[node].plane == Plane.XY + assert is_close_angle(graph.meas_bases[node].angle, -angle) def test_check_canonical_form_true(canonical_graph: GraphState) -> None: From 023d2858612740f523e18586f50b432b17b4d4ae Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sun, 23 Nov 2025 21:58:20 +0900 Subject: [PATCH 39/94] :art: Update expand_local_cliffords --- graphqomb/graphstate.py | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index 69be61e7f..50ae229a4 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -566,33 +566,30 @@ def _expand_input_local_cliffords(self) -> dict[int, InputLocalCliffordExpansion self.add_physical_edge(new_node_index0, new_node_index1) self.add_physical_edge(new_node_index1, input_node) - self.assign_meas_basis(new_node_index0, PlannerMeasBasis(Plane.XY, -lc.alpha)) - self.assign_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, -lc.beta)) - gamma = lc.gamma - delta = self.meas_bases[input_node].angle - meas_plane = self.meas_bases[input_node].plane - if meas_plane == Plane.XY: - meas_plane = Plane.XY - delta -= gamma - elif meas_plane == Plane.XZ and is_close_angle(2 * gamma, 0.0): - meas_plane = Plane.XZ + i_angle = self.meas_bases[input_node].angle + i_meas_plane = self.meas_bases[input_node].plane + if i_meas_plane == Plane.XY: + i_meas_plane = Plane.XY + i_angle -= gamma + elif i_meas_plane == Plane.XZ and is_close_angle(2 * gamma, 0.0): sign = 1 if is_close_angle(gamma, 0.0) else -1 - delta *= sign - elif meas_plane == Plane.XZ and is_close_angle(2 * (gamma - np.pi / 2), 0.0): - meas_plane = Plane.YZ + i_angle *= sign + elif i_meas_plane == Plane.XZ and is_close_angle(2 * (gamma - np.pi / 2), 0.0): + i_meas_plane = Plane.YZ sign = 1 if is_close_angle(gamma + np.pi / 2, 0.0) else -1 - delta *= sign - elif meas_plane == Plane.YZ and is_close_angle(2 * gamma, 0.0): - meas_plane = Plane.YZ + i_angle *= sign + elif i_meas_plane == Plane.YZ and is_close_angle(2 * gamma, 0.0): sign = 1 if is_close_angle(gamma, 0.0) else -1 - delta *= sign - elif meas_plane == Plane.YZ and is_close_angle(2 * (gamma - np.pi / 2), 0.0): - meas_plane = Plane.XZ + i_angle *= sign + elif i_meas_plane == Plane.YZ and is_close_angle(2 * (gamma - np.pi / 2), 0.0): + i_meas_plane = Plane.XZ sign = 1 if is_close_angle(gamma - np.pi / 2, 0.0) else -1 - delta *= sign + i_angle *= sign - self.assign_meas_basis(input_node, PlannerMeasBasis(meas_plane, delta)) + self.assign_meas_basis(input_node, PlannerMeasBasis(Plane.XY, -lc.alpha)) + self.assign_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, -lc.beta)) + self.assign_meas_basis(new_node_index0, PlannerMeasBasis(i_meas_plane, i_angle)) node_index_addition_map[input_node] = InputLocalCliffordExpansion(new_node_index0, new_node_index1) From be9a8e4bc445f5b8dbbd0b2fedf5192b9effc6b3 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 03:14:49 +0900 Subject: [PATCH 40/94] :art: Improve readability --- graphqomb/zxgraphstate.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 048bdffb7..fcbe11db1 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -76,10 +76,7 @@ def _clifford_rules(self) -> tuple[CliffordRule, ...]: self._needs_pivot, lambda node: self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))), ), - ( - self._is_noninput_with_io_nbrs, - lambda node: self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))), - ), + (self._needs_pivot_on_boundary, self.pivot_on_boundary), ) def _update_connections( @@ -316,7 +313,7 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: case_b = self.meas_bases[node].plane == Plane.XZ and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) return case_a or case_b - def _is_noninput_with_io_nbrs(self, node: int, atol: float = 1e-9) -> bool: + def _needs_pivot_on_boundary(self, node: int, atol: float = 1e-9) -> bool: """Check if the node is non-input and all neighbors are input or output nodes. If True, pivot operation is performed on the node and its non-input neighbor, and then the node will be removed. @@ -358,6 +355,23 @@ def _is_noninput_with_io_nbrs(self, node: int, atol: float = 1e-9) -> bool: case_b = self.meas_bases[node].plane == Plane.XZ and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) return case_a or case_b + def pivot_on_boundary(self, node: int) -> None: + """Perform the Clifford node removal on a corner case. + + Parameters + ---------- + node : `int` + node index + atol : `float`, optional + absolute tolerance, by default 1e-9 + + References + ---------- + [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.11 + """ + output_nbr = next(self.neighbors(node) - set(self.input_node_indices)) + self.pivot(node, output_nbr) + def _remove_clifford(self, node: int, atol: float = 1e-9) -> None: """Perform the Clifford node removal. @@ -453,7 +467,7 @@ def is_removable_clifford(self, node: int, atol: float = 1e-9) -> bool: self._is_trivial_meas(node, atol), self._needs_lc(node, atol), self._needs_pivot(node, atol), - self._is_noninput_with_io_nbrs(node, atol), + self._needs_pivot_on_boundary(node, atol), ] ) From 6f378482a3af0025075aa0a9463d5cf6582ab335 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 03:15:29 +0900 Subject: [PATCH 41/94] :art: Update docs for lc and pivot --- graphqomb/zxgraphstate.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index fcbe11db1..f0790f937 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -111,6 +111,16 @@ def local_complement(self, node: int) -> None: ------ ValueError If the node does not exist, is an input node, or the graph is not a ZX-diagram. + + Notes + ----- + Here we adopt the definition (lemma) of local complementation from [1]. + In some literature, local complementation is defined with Rx(-pi/2) on the target node + and Rz(pi/2) on the neighbors, which is strictly equivalent. + + References + ---------- + [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 2.31 and lemma 4.3 """ self._ensure_node_exists(node) if node in self.input_node_indices: @@ -204,9 +214,17 @@ def pivot(self, node1: int, node2: int) -> None: ValueError If the nodes are input nodes, or the graph is not a ZX-diagram. + Notes + ----- + Here we adopt the definition (lemma) of pivot from [1]. + In some literature, pivot is defined as below: + Rz(pi/2) Rx(-pi/2) Rz(pi/2) on the target nodes, + Rz(pi) on all the neighbors of both target nodes (not including the target nodes). + This definition is strictly equivalent to the one adopted here. + References ---------- - [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.5 with correction + [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 2.32 and lemma 4.5 """ self._ensure_node_exists(node1) self._ensure_node_exists(node2) @@ -217,13 +235,13 @@ def pivot(self, node1: int, node2: int) -> None: self._pivot(node1, node2) # update node1 and node2 measurement - lc = LocalClifford(-np.pi / 2, np.pi / 2, -np.pi / 2) + lc = LocalClifford(np.pi / 2, np.pi / 2, np.pi / 2) for a in {node1, node2}: self.apply_local_clifford(a, lc) # update nodes measurement of neighbors lc = LocalClifford(np.pi, 0, 0) - for w in self.neighbors(node1) | self.neighbors(node2) - {node1, node2}: + for w in self.neighbors(node1) & self.neighbors(node2): self.apply_local_clifford(w, lc) def _is_trivial_meas(self, node: int, atol: float = 1e-9) -> bool: From 9619ba469f1d1d8499b3b125dafa1ee7554961ff Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 03:15:52 +0900 Subject: [PATCH 42/94] :fire: Remove _swap --- graphqomb/zxgraphstate.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index f0790f937..844c7d1ab 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -142,27 +142,6 @@ def local_complement(self, node: int) -> None: for v in nbrs: self.apply_local_clifford(v, lc) - def _swap(self, node1: int, node2: int) -> None: - """Swap nodes u and v in the graph state. - - Parameters - ---------- - node1 : `int` - node index - node2 : `int` - node index - """ - node1_nbrs = self.neighbors(node1) - {node2} - node2_nbrs = self.neighbors(node2) - {node1} - nbr_b = node1_nbrs - node2_nbrs - nbr_c = node2_nbrs - node1_nbrs - for b in nbr_b: - self.remove_physical_edge(node1, b) - self.add_physical_edge(node2, b) - for c in nbr_c: - self.remove_physical_edge(node2, c) - self.add_physical_edge(node1, c) - def _pivot(self, node1: int, node2: int) -> None: """Pivot edges around nodes u and v in the graph state. From 43bb48071d0abd053357c1b43d01e6f6b3a98bc8 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 03:18:23 +0900 Subject: [PATCH 43/94] :truck: Split gflow_wrapper --- examples/zxgraph_simplification.py | 63 +------------------------ graphqomb/gflow_utils.py | 74 ++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 62 deletions(-) create mode 100644 graphqomb/gflow_utils.py diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py index 14752deae..a678114ca 100644 --- a/examples/zxgraph_simplification.py +++ b/examples/zxgraph_simplification.py @@ -15,25 +15,17 @@ from __future__ import annotations from copy import deepcopy -from typing import TYPE_CHECKING, Any -import networkx as nx import numpy as np -import swiflow as sf -from swiflow import gflow from graphqomb.common import Plane +from graphqomb.gflow_utils import gflow_wrapper from graphqomb.qompiler import qompile from graphqomb.random_objects import generate_random_flow_graph from graphqomb.simulator import PatternSimulator, SimulatorBackend from graphqomb.visualizer import visualize from graphqomb.zxgraphstate import ZXGraphState, to_zx_graphstate -if TYPE_CHECKING: - from networkx import Graph as NxGraph - - from graphqomb.graphstate import BaseGraphState - FlowLike = dict[int, set[int]] # %% @@ -113,59 +105,6 @@ def print_boundary_lcs(zxgraph: ZXGraphState) -> None: # %% # Next, we simulate the pattern obtained from the simplified graph state. -# Here is a wrapper function to find gflow using swiflow.gflow. - - -def gflow_wrapper(graphstate: BaseGraphState) -> FlowLike: - """Wrapper function for swiflow.gflow - - Parameters - ---------- - graphstate : `BaseGraphState` - graph state to find gflow - - Returns - ------- - `FlowLike` - gflow object - - Raises - ------ - ValueError - If no gflow is found - """ - graph: NxGraph[Any] = nx.Graph() - graph.add_nodes_from(graphstate.physical_nodes) - graph.add_edges_from(graphstate.physical_edges) - - bases = graphstate.meas_bases - planes = {node: bases[node].plane for node in bases} - swiflow_planes = {} - for node, plane in planes.items(): - if plane == Plane.XY: - swiflow_planes[node] = sf.common.Plane.XY - elif plane == Plane.YZ: - swiflow_planes[node] = sf.common.Plane.YZ - elif plane == Plane.XZ: - swiflow_planes[node] = sf.common.Plane.XZ - else: - msg = f"No match {plane}" - raise ValueError(msg) - - gflow_object = gflow.find( - graph, set(graphstate.input_node_indices), set(graphstate.output_node_indices), swiflow_planes - ) - print(gflow_object) - if gflow_object is None: - msg = "No flow found" - raise ValueError(msg) - - gflow_obj = gflow_object.f - - return {node: {child for child in children if child != node} for node, children in gflow_obj.items()} - - -# %% # Note that we need to call the `expand_local_cliffords` method before generating the pattern to get the gflow. zx_graph_smp.expand_local_cliffords() diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py new file mode 100644 index 000000000..7a6b47052 --- /dev/null +++ b/graphqomb/gflow_utils.py @@ -0,0 +1,74 @@ +"""Utilities for generalized flow (gflow) computation. + +This module provides: + +- `gflow_wrapper`: Thin adapter around `swiflow.gflow` so that gflow can be computed directly +from a `BaseGraphState` instance. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import networkx as nx +import swiflow as sf +from swiflow import gflow + +from graphqomb.common import Plane + +if TYPE_CHECKING: + from typing import Any + + from networkx import Graph as NxGraph + + from graphqomb.graphstate import BaseGraphState + + FlowLike = dict[int, set[int]] + + +def gflow_wrapper(graphstate: BaseGraphState) -> FlowLike: + """Utilize `swiflow.gflow` to search gflow. + + Parameters + ---------- + graphstate : `BaseGraphState` + graph state to find gflow + + Returns + ------- + `FlowLike` + gflow object + + Raises + ------ + ValueError + If no gflow is found + """ + graph: NxGraph[Any] = nx.Graph() + graph.add_nodes_from(graphstate.physical_nodes) + graph.add_edges_from(graphstate.physical_edges) + + bases = graphstate.meas_bases + planes = {node: bases[node].plane for node in bases} + swiflow_planes = {} + for node, plane in planes.items(): + if plane == Plane.XY: + swiflow_planes[node] = sf.common.Plane.XY + elif plane == Plane.YZ: + swiflow_planes[node] = sf.common.Plane.YZ + elif plane == Plane.XZ: + swiflow_planes[node] = sf.common.Plane.XZ + else: + msg = f"No match {plane}" + raise ValueError(msg) + + gflow_object = gflow.find( + graph, set(graphstate.input_node_indices), set(graphstate.output_node_indices), swiflow_planes + ) + if gflow_object is None: + msg = "No flow found" + raise ValueError(msg) + + gflow_obj = gflow_object.f + + return {node: {child for child in children if child != node} for node, children in gflow_obj.items()} From 39af73689dc0a6157adb2bf95a56de236d69f614 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 03:22:56 +0900 Subject: [PATCH 44/94] :art: Fix method name --- tests/test_zxgraphstate.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index eef38c1c6..c53198e71 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -342,7 +342,7 @@ def test_pivot_fails_with_input_node(zx_graph: ZXGraphState) -> None: def test_pivot_with_obvious_graph(zx_graph: ZXGraphState) -> None: """Test pivot with an obvious graph.""" - # 0---1---2 + # 0---1---2 -> 0---2---1 for _ in range(3): zx_graph.add_physical_node() @@ -573,7 +573,7 @@ def test_remove_cliffords_graph2(zx_graph: ZXGraphState) -> None: ) ), ) -def test_is_noninput_with_io_nbrs_xy( +def test_needs_pivot_on_boundary_xy( zx_graph: ZXGraphState, planes: tuple[Plane, Plane], rng: np.random.Generator, @@ -585,7 +585,7 @@ def test_is_noninput_with_io_nbrs_xy( angles[1] = rng.choice([0.0, np.pi]) measurements = [(i, PlannerMeasBasis(planes[i], angles[i])) for i in range(2)] _apply_measurements(zx_graph, measurements) - assert zx_graph._is_noninput_with_io_nbrs(1, atol=1e-9) is True + assert zx_graph._needs_pivot_on_boundary(1, atol=1e-9) is True @pytest.mark.parametrize( @@ -597,7 +597,7 @@ def test_is_noninput_with_io_nbrs_xy( ) ), ) -def test_is_noninput_with_io_nbrs_xz( +def test_needs_pivot_on_boundary_xz( zx_graph: ZXGraphState, planes: tuple[Plane, Plane], rng: np.random.Generator, @@ -609,7 +609,7 @@ def test_is_noninput_with_io_nbrs_xz( angles[1] = rng.choice([0.5 * np.pi, 1.5 * np.pi]) measurements = [(i, PlannerMeasBasis(planes[i], angles[i])) for i in range(2)] _apply_measurements(zx_graph, measurements) - assert zx_graph._is_noninput_with_io_nbrs(1, atol=1e-9) is True + assert zx_graph._needs_pivot_on_boundary(1, atol=1e-9) is True def test_remove_cliffords_graph3(zx_graph: ZXGraphState) -> None: From adc92036570e321f029ca8c6e9f8b9e8a8db8ebe Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 03:24:19 +0900 Subject: [PATCH 45/94] :white_check_mark: Add inner product test --- tests/test_zxgraphstate.py | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index c53198e71..7c36ab674 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -21,7 +21,10 @@ import pytest from graphqomb.common import Plane, PlannerMeasBasis, is_close_angle +from graphqomb.gflow_utils import gflow_wrapper +from graphqomb.qompiler import qompile from graphqomb.random_objects import generate_random_flow_graph +from graphqomb.simulator import PatternSimulator, SimulatorBackend from graphqomb.zxgraphstate import ZXGraphState, to_zx_graphstate if TYPE_CHECKING: @@ -322,6 +325,47 @@ def test_local_complement_4_times( _test(zx_graph, exp_nodes={0, 1, 2}, exp_edges={(0, 1), (1, 2)}, exp_measurements=exp_measurements) +def test_remove_clifford_validity() -> None: + graph, flow = generate_random_flow_graph(width=1, depth=3, edge_p=0.5) + zx_graph, _ = to_zx_graphstate(graph) + + pattern = qompile(zx_graph, flow) + sim = PatternSimulator(pattern, backend=SimulatorBackend.StateVector) + sim.simulate() + psi_original = sim.state.state() + + zx_graph_cp = deepcopy(zx_graph) + zx_graph_cp.remove_clifford(1) + zx_graph_cp.expand_local_cliffords() + gflow_lc = gflow_wrapper(zx_graph_cp) + pattern_lc = qompile(zx_graph_cp, gflow_lc) + sim_lc = PatternSimulator(pattern_lc, backend=SimulatorBackend.StateVector) + sim_lc.simulate() + psi_lc = sim_lc.state.state() + assert np.isclose(np.abs(np.vdot(psi_original, psi_lc)), 1.0) + + +def test_pivot_validity() -> None: + graph, flow = generate_random_flow_graph(width=1, depth=4, edge_p=0.5) + zx_graph, _ = to_zx_graphstate(graph) + + pattern = qompile(zx_graph, flow) + sim = PatternSimulator(pattern, backend=SimulatorBackend.StateVector) + sim.simulate() + psi_original = sim.state.state() + + zx_graph.pivot(1, 2) + zx_graph.remove_clifford(1) + zx_graph.remove_clifford(2) + zx_graph.expand_local_cliffords() + gflow_lc = gflow_wrapper(zx_graph) + pattern_lc = qompile(zx_graph, gflow_lc) + sim_lc = PatternSimulator(pattern_lc, backend=SimulatorBackend.StateVector) + sim_lc.simulate() + psi_lc = sim_lc.state.state() + assert np.isclose(np.abs(np.vdot(psi_original, psi_lc)), 1.0) + + def test_pivot_fails_with_nonexistent_nodes(zx_graph: ZXGraphState) -> None: """Test pivot fails with nonexistent nodes.""" with pytest.raises(ValueError, match="Node does not exist node=0"): From 35de92e93fa7bd03bf497894004c852c5eb7d2f4 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 03:24:55 +0900 Subject: [PATCH 46/94] :art: Fix old test --- tests/test_zxgraphstate.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 7c36ab674..39defd5e7 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -394,21 +394,22 @@ def test_pivot_with_obvious_graph(zx_graph: ZXGraphState) -> None: zx_graph.add_physical_edge(i, j) measurements = [ - (0, PlannerMeasBasis(Plane.XY, 1.1 * np.pi)), - (1, PlannerMeasBasis(Plane.XZ, 1.2 * np.pi)), - (2, PlannerMeasBasis(Plane.YZ, 1.3 * np.pi)), + (0, PlannerMeasBasis(Plane.XY, np.pi)), + (1, PlannerMeasBasis(Plane.XZ, 1.4 * np.pi)), + (2, PlannerMeasBasis(Plane.YZ, 0.4 * np.pi)), ] _apply_measurements(zx_graph, measurements) - original_zx_graph = deepcopy(zx_graph) zx_graph.pivot(1, 2) - original_zx_graph.local_complement(1) - original_zx_graph.local_complement(2) - original_zx_graph.local_complement(1) - assert zx_graph.physical_edges == original_zx_graph.physical_edges - original_planes = [original_zx_graph.meas_bases[i].plane for i in range(3)] - planes = [zx_graph.meas_bases[i].plane for i in range(3)] - assert planes == original_planes + assert zx_graph.physical_edges == {(0, 2), (1, 2)} + ref_plane1, ref_angle_func1 = MEAS_ACTION_PV_TARGET[Plane.XZ] + ref_plane2, ref_angle_func2 = MEAS_ACTION_PV_TARGET[Plane.YZ] + assert zx_graph.meas_bases[0].plane == Plane.XY + assert zx_graph.meas_bases[1].plane == ref_plane1 + assert zx_graph.meas_bases[2].plane == ref_plane2 + assert is_close_angle(zx_graph.meas_bases[0].angle, np.pi) + assert is_close_angle(zx_graph.meas_bases[1].angle, ref_angle_func1(1.4 * np.pi)) + assert is_close_angle(zx_graph.meas_bases[2].angle, ref_angle_func2(0.4 * np.pi)) @pytest.mark.parametrize("planes", plane_combinations(5)) From c2f75b2e0706da85e92051d2dc05dd48f9c18669 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 23:20:34 +0900 Subject: [PATCH 47/94] :art: Change update_lc_basis logic --- graphqomb/euler.py | 2 +- tests/test_euler.py | 82 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/graphqomb/euler.py b/graphqomb/euler.py index 0b4e0bb92..a33ff7aae 100644 --- a/graphqomb/euler.py +++ b/graphqomb/euler.py @@ -290,7 +290,7 @@ def update_lc_basis(lc: LocalClifford, basis: MeasBasis) -> PlannerMeasBasis: `PlannerMeasBasis` updated `PlannerMeasBasis` """ - matrix = lc.matrix() + matrix = lc.matrix().conjugate().T vector = basis.vector() updated_vector = np.asarray(matrix @ vector, dtype=np.complex128) diff --git a/tests/test_euler.py b/tests/test_euler.py index fb3614998..71be3a17f 100644 --- a/tests/test_euler.py +++ b/tests/test_euler.py @@ -130,8 +130,8 @@ def test_lc_basis_update( angle = rng.uniform(0, 2 * np.pi) basis = PlannerMeasBasis(plane, angle) basis_updated = update_lc_basis(lc, basis) - ref_updated_basis = lc.matrix() @ basis.vector() - inner_product = abs(np.vdot(basis_updated.vector(), ref_updated_basis)) + ref_updated_vector = lc.conjugate().matrix() @ basis.vector() + inner_product = abs(np.vdot(basis_updated.vector(), ref_updated_vector)) assert np.isclose(inner_product, 1) @@ -140,17 +140,16 @@ def test_local_complement_target_update(plane: Plane, rng: np.random.Generator) 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.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) - assert result_basis.plane == ref_plane assert is_close_angle(result_basis.angle, ref_angle) @@ -167,9 +166,80 @@ def test_local_complement_neighbors(plane: Plane, rng: np.random.Generator) -> N 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) assert result_basis.plane == ref_plane assert is_close_angle(result_basis.angle, ref_angle) + + +@pytest.mark.parametrize("plane", list(Plane)) +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, lambda angle: -1 * angle), + Plane.XZ: (Plane.XZ, lambda angle: (np.pi / 2 - angle)), + Plane.YZ: (Plane.XY, lambda angle: -1 * angle), + } + + angle = rng.random() * 2 * np.pi + + meas_basis = PlannerMeasBasis(plane, angle) + result_basis = update_lc_basis(lc, 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", list(Plane)) +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) % (2.0 * np.pi)), + Plane.XZ: (Plane.XZ, lambda angle: -1 * angle), + Plane.YZ: (Plane.YZ, lambda angle: -1 * angle), + } + + angle = rng.random() * 2 * np.pi + + meas_basis = PlannerMeasBasis(plane, angle) + result_basis = update_lc_basis(lc, 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", list(Plane)) +def test_remove_clifford_update(plane: Plane, rng: np.random.Generator) -> None: + measurement_action: dict[Plane, tuple[Plane, Callable[[float, float], float]]] = { + Plane.XY: ( + Plane.XY, + lambda a_pi, alpha: (alpha if is_close_angle(a_pi, 0) else alpha + np.pi) % (2.0 * np.pi), + ), + Plane.XZ: ( + Plane.XZ, + lambda a_pi, alpha: (alpha if is_close_angle(a_pi, 0) else -alpha) % (2.0 * np.pi), + ), + Plane.YZ: ( + Plane.YZ, + lambda a_pi, alpha: (alpha if is_close_angle(a_pi, 0) else -alpha) % (2.0 * np.pi), + ), + } + + angle = rng.random() * 2 * np.pi + + a_pi = np.pi + for a_pi in (0.0, np.pi): + lc = LocalClifford(a_pi, 0, 0) + meas_basis = PlannerMeasBasis(plane, angle) + result_basis = update_lc_basis(lc, meas_basis) + ref_plane, ref_angle_func = measurement_action[plane] + ref_angle = ref_angle_func(a_pi, angle) + + assert result_basis.plane == ref_plane + assert is_close_angle(result_basis.angle, ref_angle) From 204a61a9515d7d4ff53112eb84008dbaec2b4e09 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 23:21:26 +0900 Subject: [PATCH 48/94] :bug: Fix bug --- graphqomb/graphstate.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index 50ae229a4..1e67c26c3 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -475,7 +475,7 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: self.__local_cliffords[node] = lc else: self._check_meas_basis() - new_meas_basis = update_lc_basis(lc.conjugate(), self.meas_bases[node]) + new_meas_basis = update_lc_basis(lc, self.meas_bases[node]) self.assign_meas_basis(node, new_meas_basis) @typing_extensions.override @@ -571,13 +571,13 @@ def _expand_input_local_cliffords(self) -> dict[int, InputLocalCliffordExpansion i_meas_plane = self.meas_bases[input_node].plane if i_meas_plane == Plane.XY: i_meas_plane = Plane.XY - i_angle -= gamma + i_angle += gamma elif i_meas_plane == Plane.XZ and is_close_angle(2 * gamma, 0.0): sign = 1 if is_close_angle(gamma, 0.0) else -1 i_angle *= sign elif i_meas_plane == Plane.XZ and is_close_angle(2 * (gamma - np.pi / 2), 0.0): i_meas_plane = Plane.YZ - sign = 1 if is_close_angle(gamma + np.pi / 2, 0.0) else -1 + sign = 1 if is_close_angle(gamma - np.pi / 2, 0.0) else -1 i_angle *= sign elif i_meas_plane == Plane.YZ and is_close_angle(2 * gamma, 0.0): sign = 1 if is_close_angle(gamma, 0.0) else -1 @@ -587,8 +587,8 @@ def _expand_input_local_cliffords(self) -> dict[int, InputLocalCliffordExpansion sign = 1 if is_close_angle(gamma - np.pi / 2, 0.0) else -1 i_angle *= sign - self.assign_meas_basis(input_node, PlannerMeasBasis(Plane.XY, -lc.alpha)) - self.assign_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, -lc.beta)) + self.assign_meas_basis(input_node, PlannerMeasBasis(Plane.XY, lc.alpha)) + self.assign_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, lc.beta)) self.assign_meas_basis(new_node_index0, PlannerMeasBasis(i_meas_plane, i_angle)) node_index_addition_map[input_node] = InputLocalCliffordExpansion(new_node_index0, new_node_index1) @@ -626,9 +626,9 @@ def _expand_output_local_cliffords(self) -> dict[int, OutputLocalCliffordExpansi self.add_physical_edge(new_node_index1, new_node_index2) self.add_physical_edge(new_node_index2, new_node_index3) - self.assign_meas_basis(output_node, PlannerMeasBasis(Plane.XY, -lc.alpha)) - self.assign_meas_basis(new_node_index0, PlannerMeasBasis(Plane.XY, -lc.beta)) - self.assign_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, -lc.gamma)) + self.assign_meas_basis(output_node, PlannerMeasBasis(Plane.XY, lc.alpha)) + self.assign_meas_basis(new_node_index0, PlannerMeasBasis(Plane.XY, lc.beta)) + self.assign_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, lc.gamma)) self.assign_meas_basis(new_node_index2, PlannerMeasBasis(Plane.XY, 0.0)) node_index_addition_map[output_node] = OutputLocalCliffordExpansion( From 9ee7ee284c580207f3bcd4c08aa4b6d57d45a12b Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 23:22:23 +0900 Subject: [PATCH 49/94] :art: Fix clifford expansion test --- tests/test_graphstate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index 648c8ebdf..5accfff48 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -217,7 +217,7 @@ def test_expand_input_local_cliffords_xy_plane(graph: GraphState, gamma: float) assert graph.input_node_indices == {new_input_node: 0} for node in graph.physical_nodes - set(graph.output_node_indices): assert graph.meas_bases[node].plane == Plane.XY - assert is_close_angle(graph.meas_bases[new_input_node].angle, old_input_angle - gamma) + assert is_close_angle(graph.meas_bases[new_input_node].angle, old_input_angle + gamma) assert is_close_angle(graph.meas_bases[new_input_node + 1].angle, -beta) assert is_close_angle(graph.meas_bases[old_input_node].angle, -alpha) @@ -278,7 +278,7 @@ def test_expand_input_local_cliffords_xz_plane(graph: GraphState, gamma: float) exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle elif is_close_angle(2 * (gamma - np.pi / 2), 0): assert graph.meas_bases[new_input_node].plane == Plane.YZ - exp_angle = old_input_angle if is_close_angle(gamma + np.pi / 2, 0) else -old_input_angle + exp_angle = old_input_angle if is_close_angle(gamma - np.pi / 2, 0) else -old_input_angle assert is_close_angle(graph.meas_bases[new_input_node].angle, exp_angle) @@ -300,7 +300,7 @@ def test_expand_output_local_cliffords(graph: GraphState) -> None: exp_results = [(1, np.pi / 2), (2, np.pi), (3, 3 * np.pi / 2), (4, 0.0)] for node, angle in exp_results: assert graph.meas_bases[node].plane == Plane.XY - assert is_close_angle(graph.meas_bases[node].angle, -angle) + assert is_close_angle(graph.meas_bases[node].angle, angle) def test_check_canonical_form_true(canonical_graph: GraphState) -> None: From 3a9b83a2a08da4be5ca870539ffb92534034583b Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 23:28:39 +0900 Subject: [PATCH 50/94] :art: Fix zxgraphstate test --- requirements.txt | 1 + tests/test_zxgraphstate.py | 44 +++++++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 59ee09d07..2316d62c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ numpy>=1.22,<3 matplotlib networkx ortools>=9,<10 +swiflow typing_extensions diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 39defd5e7..fee905393 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -21,6 +21,7 @@ import pytest from graphqomb.common import Plane, PlannerMeasBasis, is_close_angle +from graphqomb.euler import LocalClifford from graphqomb.gflow_utils import gflow_wrapper from graphqomb.qompiler import qompile from graphqomb.random_objects import generate_random_flow_graph @@ -50,8 +51,8 @@ } MEAS_ACTION_PV_NEIGHBORS: dict[Plane, tuple[Plane, Callable[[float], float]]] = { Plane.XY: (Plane.XY, lambda angle: (angle + np.pi) % (2.0 * np.pi)), - Plane.XZ: (Plane.XZ, lambda angle: -angle % (2.0 * np.pi)), - Plane.YZ: (Plane.YZ, lambda angle: -angle % (2.0 * np.pi)), + Plane.XZ: (Plane.XZ, operator.neg), + Plane.YZ: (Plane.YZ, operator.neg), } ATOL = 1e-9 MEAS_ACTION_RC: dict[Plane, tuple[Plane, Callable[[float, float], float]]] = { @@ -192,6 +193,20 @@ def test_local_complement_fails_with_input_node(zx_graph: ZXGraphState) -> None: zx_graph.local_complement(node) +def test_local_clifford_updates_xy_basis(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: + """Applying an LC should update stored XY angles using the public convention.""" + node = zx_graph.add_physical_node() + angle = rng.random() * 2 * np.pi + zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, angle)) + lc = LocalClifford(0.0, 0.0, -np.pi / 2) + + zx_graph.apply_local_clifford(node, lc) + meas_basis_vector = zx_graph.meas_bases[node].vector() + ref_vector = PlannerMeasBasis(Plane.XY, angle + np.pi / 2).vector() + inner_product = abs(np.vdot(meas_basis_vector, ref_vector)) + assert np.isclose(inner_product, 1) + + @pytest.mark.parametrize("plane", list(Plane)) def test_local_complement_with_no_edge(zx_graph: ZXGraphState, plane: Plane, rng: np.random.Generator) -> None: """Test local complement with a graph with a single node and no edges.""" @@ -325,6 +340,29 @@ def test_local_complement_4_times( _test(zx_graph, exp_nodes={0, 1, 2}, exp_edges={(0, 1), (1, 2)}, exp_measurements=exp_measurements) +def test_local_clifford_expansion() -> None: + graph, flow = generate_random_flow_graph(width=1, depth=3, edge_p=0.5) + zx_graph, _ = to_zx_graphstate(graph) + + pattern = qompile(zx_graph, flow) + sim = PatternSimulator(pattern, backend=SimulatorBackend.StateVector) + sim.simulate() + psi_original = sim.state.state() + + zx_graph_cp = deepcopy(zx_graph) + lc = LocalClifford(2 * np.pi, 2 * np.pi, 2 * np.pi) + zx_graph_cp.apply_local_clifford(0, lc) + lc = LocalClifford(2 * np.pi, 0.0, 0.0) + zx_graph_cp.apply_local_clifford(2, lc) + zx_graph_cp.expand_local_cliffords() + gflow_cp = gflow_wrapper(zx_graph_cp) + pattern_cp = qompile(zx_graph_cp, gflow_cp) + sim_cp = PatternSimulator(pattern_cp, backend=SimulatorBackend.StateVector) + sim_cp.simulate() + psi_cp = sim_cp.state.state() + assert np.isclose(np.abs(np.vdot(psi_original, psi_cp)), 1.0) + + def test_remove_clifford_validity() -> None: graph, flow = generate_random_flow_graph(width=1, depth=3, edge_p=0.5) zx_graph, _ = to_zx_graphstate(graph) @@ -335,7 +373,7 @@ def test_remove_clifford_validity() -> None: psi_original = sim.state.state() zx_graph_cp = deepcopy(zx_graph) - zx_graph_cp.remove_clifford(1) + zx_graph_cp.remove_cliffords() zx_graph_cp.expand_local_cliffords() gflow_lc = gflow_wrapper(zx_graph_cp) pattern_lc = qompile(zx_graph_cp, gflow_lc) From 3e12fe8980db3eea40741fbf0732336cf4904a55 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 23:30:50 +0900 Subject: [PATCH 51/94] :art: ruff --- graphqomb/zxgraphstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 844c7d1ab..cd4d46eea 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -366,7 +366,7 @@ def pivot_on_boundary(self, node: int) -> None: ---------- [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.11 """ - output_nbr = next(self.neighbors(node) - set(self.input_node_indices)) + output_nbr = min(self.neighbors(node) - set(self.input_node_indices)) self.pivot(node, output_nbr) def _remove_clifford(self, node: int, atol: float = 1e-9) -> None: From 73fb5848e4cf348578286f7907f6ed36627b1745 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 23:47:21 +0900 Subject: [PATCH 52/94] :art: pyright --- graphqomb/gflow_utils.py | 10 +++++----- tests/test_graphstate.py | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py index 7a6b47052..d5fb8e9f2 100644 --- a/graphqomb/gflow_utils.py +++ b/graphqomb/gflow_utils.py @@ -11,8 +11,8 @@ from typing import TYPE_CHECKING import networkx as nx -import swiflow as sf from swiflow import gflow +from swiflow.common import Plane as SfPlane from graphqomb.common import Plane @@ -50,14 +50,14 @@ def gflow_wrapper(graphstate: BaseGraphState) -> FlowLike: bases = graphstate.meas_bases planes = {node: bases[node].plane for node in bases} - swiflow_planes = {} + swiflow_planes: dict[int, SfPlane] = {} for node, plane in planes.items(): if plane == Plane.XY: - swiflow_planes[node] = sf.common.Plane.XY + swiflow_planes[node] = SfPlane.XY elif plane == Plane.YZ: - swiflow_planes[node] = sf.common.Plane.YZ + swiflow_planes[node] = SfPlane.YZ elif plane == Plane.XZ: - swiflow_planes[node] = sf.common.Plane.XZ + swiflow_planes[node] = SfPlane.XZ else: msg = f"No match {plane}" raise ValueError(msg) diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index 5accfff48..cc636e37f 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -243,6 +243,7 @@ def test_expand_input_local_cliffords_yz_plane(graph: GraphState, gamma: float) graph.expand_local_cliffords() new_input_node = 2 + exp_angle: float = old_input_angle if is_close_angle(2 * gamma, 0): assert graph.meas_bases[new_input_node].plane == Plane.YZ exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle @@ -273,6 +274,7 @@ def test_expand_input_local_cliffords_xz_plane(graph: GraphState, gamma: float) graph.expand_local_cliffords() new_input_node = 2 + exp_angle: float = old_input_angle if is_close_angle(2 * gamma, 0): assert graph.meas_bases[new_input_node].plane == Plane.XZ exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle From 779d9bdb7238698b03d3eaf954013e2da44fb5aa Mon Sep 17 00:00:00 2001 From: nabe98 Date: Tue, 25 Nov 2025 23:50:55 +0900 Subject: [PATCH 53/94] :art: ruff --- examples/zxgraph_simplification.py | 1 - graphqomb/zxgraphstate.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py index a678114ca..f0b16210f 100644 --- a/examples/zxgraph_simplification.py +++ b/examples/zxgraph_simplification.py @@ -18,7 +18,6 @@ import numpy as np -from graphqomb.common import Plane from graphqomb.gflow_utils import gflow_wrapper from graphqomb.qompiler import qompile from graphqomb.random_objects import generate_random_flow_graph diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index cd4d46eea..901ee4777 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -359,8 +359,6 @@ def pivot_on_boundary(self, node: int) -> None: ---------- node : `int` node index - atol : `float`, optional - absolute tolerance, by default 1e-9 References ---------- From 4bfa792fa4e6d257f8cbf3bea509afb3396bf198 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Wed, 26 Nov 2025 23:24:41 +0900 Subject: [PATCH 54/94] :bug: Fix bug in input/output expansion --- graphqomb/graphstate.py | 121 ++++++++++++++------------------------- tests/test_graphstate.py | 41 +++++++------ 2 files changed, 65 insertions(+), 97 deletions(-) diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index 1e67c26c3..23e2df738 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -26,7 +26,7 @@ import numpy as np import typing_extensions -from graphqomb.common import MeasBasis, Plane, PlannerMeasBasis, is_close_angle +from graphqomb.common import MeasBasis, Plane, PlannerMeasBasis from graphqomb.euler import LocalClifford, update_lc_basis, update_lc_lc if TYPE_CHECKING: @@ -543,55 +543,35 @@ def _pop_local_clifford(self, node: int) -> LocalClifford | None: """ return self.__local_cliffords.pop(node, None) - def _expand_input_local_cliffords(self) -> dict[int, InputLocalCliffordExpansion]: + def _expand_input_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: r"""Expand local Clifford operators applied on the input nodes. Returns ------- - `dict`\[`int`, `InputLocalCliffordExpansion`\] + `dict`\[`int`, `LocalCliffordExpansion`\] A dictionary mapping input node indices to the new node indices created. """ - node_index_addition_map: dict[int, InputLocalCliffordExpansion] = {} + node_index_addition_map: dict[int, LocalCliffordExpansion] = {} new_input_indices: dict[int, int] = {} - for input_node, q_index in self.input_node_indices.items(): - lc = self._pop_local_clifford(input_node) + for old_input_node, q_index in self.input_node_indices.items(): + lc = self._pop_local_clifford(old_input_node) if lc is None: - new_input_indices[input_node] = q_index + new_input_indices[old_input_node] = q_index continue - new_node_index0 = self.add_physical_node() - new_input_indices[new_node_index0] = q_index - new_node_index1 = self.add_physical_node() - - self.add_physical_edge(new_node_index0, new_node_index1) - self.add_physical_edge(new_node_index1, input_node) - - gamma = lc.gamma - i_angle = self.meas_bases[input_node].angle - i_meas_plane = self.meas_bases[input_node].plane - if i_meas_plane == Plane.XY: - i_meas_plane = Plane.XY - i_angle += gamma - elif i_meas_plane == Plane.XZ and is_close_angle(2 * gamma, 0.0): - sign = 1 if is_close_angle(gamma, 0.0) else -1 - i_angle *= sign - elif i_meas_plane == Plane.XZ and is_close_angle(2 * (gamma - np.pi / 2), 0.0): - i_meas_plane = Plane.YZ - sign = 1 if is_close_angle(gamma - np.pi / 2, 0.0) else -1 - i_angle *= sign - elif i_meas_plane == Plane.YZ and is_close_angle(2 * gamma, 0.0): - sign = 1 if is_close_angle(gamma, 0.0) else -1 - i_angle *= sign - elif i_meas_plane == Plane.YZ and is_close_angle(2 * (gamma - np.pi / 2), 0.0): - i_meas_plane = Plane.XZ - sign = 1 if is_close_angle(gamma - np.pi / 2, 0.0) else -1 - i_angle *= sign - - self.assign_meas_basis(input_node, PlannerMeasBasis(Plane.XY, lc.alpha)) - self.assign_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, lc.beta)) - self.assign_meas_basis(new_node_index0, PlannerMeasBasis(i_meas_plane, i_angle)) - - node_index_addition_map[input_node] = InputLocalCliffordExpansion(new_node_index0, new_node_index1) + new_input = self.add_physical_node() + new_node = self.add_physical_node() + new_input_indices[new_input] = q_index + + self.add_physical_edge(new_input, new_node) + self.add_physical_edge(new_node, old_input_node) + + self.assign_meas_basis(new_input, PlannerMeasBasis(Plane.XY, 0.0)) + self.assign_meas_basis(new_node, PlannerMeasBasis(Plane.XY, 0.0)) + meas_basis = self.meas_bases[old_input_node] + new_basis = update_lc_basis(lc, meas_basis) + self.assign_meas_basis(old_input_node, new_basis) + node_index_addition_map[old_input_node] = LocalCliffordExpansion(new_input, new_node) self.__input_node_indices = {} for new_input_index, q_index in new_input_indices.items(): @@ -599,45 +579,37 @@ def _expand_input_local_cliffords(self) -> dict[int, InputLocalCliffordExpansion return node_index_addition_map - def _expand_output_local_cliffords(self) -> dict[int, OutputLocalCliffordExpansion]: + def _expand_output_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: r"""Expand local Clifford operators applied on the output nodes. Returns ------- - `dict`\[`int`, `OutputLocalCliffordExpansion`\] + `dict`\[`int`, `LocalCliffordExpansion`\] A dictionary mapping output node indices to the new node indices created. """ - node_index_addition_map: dict[int, OutputLocalCliffordExpansion] = {} - new_output_index_map: dict[int, int] = {} - for output_node, q_index in self.output_node_indices.items(): - lc = self._pop_local_clifford(output_node) + node_index_addition_map: dict[int, LocalCliffordExpansion] = {} + new_output_node_index_map: dict[int, int] = {} + for old_output_node, q_index in self.output_node_indices.items(): + lc = self._pop_local_clifford(old_output_node) if lc is None: - new_output_index_map[output_node] = q_index + new_output_node_index_map[old_output_node] = q_index continue - new_node_index0 = self.add_physical_node() - new_node_index1 = self.add_physical_node() - new_node_index2 = self.add_physical_node() - new_node_index3 = self.add_physical_node() - new_output_index_map[new_node_index3] = q_index + new_node = self.add_physical_node() + new_output_node = self.add_physical_node() + new_output_node_index_map[new_output_node] = q_index - self.add_physical_edge(output_node, new_node_index0) - self.add_physical_edge(new_node_index0, new_node_index1) - self.add_physical_edge(new_node_index1, new_node_index2) - self.add_physical_edge(new_node_index2, new_node_index3) + self.__output_node_indices.pop(old_output_node) + self.register_output(new_output_node, q_index) - self.assign_meas_basis(output_node, PlannerMeasBasis(Plane.XY, lc.alpha)) - self.assign_meas_basis(new_node_index0, PlannerMeasBasis(Plane.XY, lc.beta)) - self.assign_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, lc.gamma)) - self.assign_meas_basis(new_node_index2, PlannerMeasBasis(Plane.XY, 0.0)) + self.add_physical_edge(old_output_node, new_node) + self.add_physical_edge(new_node, new_output_node) - node_index_addition_map[output_node] = OutputLocalCliffordExpansion( - new_node_index0, new_node_index1, new_node_index2, new_node_index3 - ) + self.assign_meas_basis(new_node, PlannerMeasBasis(Plane.XY, 0.0)) + meas_basis = update_lc_basis(lc, PlannerMeasBasis(Plane.XY, 0.0)) + self.assign_meas_basis(old_output_node, meas_basis) - self.__output_node_indices = {} - for new_output_index, q_index in new_output_index_map.items(): - self.register_output(new_output_index, q_index) + node_index_addition_map[old_output_node] = LocalCliffordExpansion(new_node, new_output_node) return node_index_addition_map @@ -814,27 +786,18 @@ def from_base_graph_state( return graph_state, node_map -class InputLocalCliffordExpansion(NamedTuple): - """Local Clifford expansion map for input node.""" - - node1: int - node2: int - - -class OutputLocalCliffordExpansion(NamedTuple): - """Local Clifford expansion map for output node.""" +class LocalCliffordExpansion(NamedTuple): + """Local Clifford expansion map.""" node1: int node2: int - node3: int - node4: int class ExpansionMaps(NamedTuple): """Expansion maps for inputs and outputs with Local Clifford.""" - input_node_map: dict[int, InputLocalCliffordExpansion] - output_node_map: dict[int, OutputLocalCliffordExpansion] + input_node_map: dict[int, LocalCliffordExpansion] + output_node_map: dict[int, LocalCliffordExpansion] def compose( # noqa: C901 diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index cc636e37f..a9c01fa5e 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -6,7 +6,7 @@ import pytest from graphqomb.common import Plane, PlannerMeasBasis, is_close_angle -from graphqomb.euler import LocalClifford +from graphqomb.euler import LocalClifford, update_lc_basis from graphqomb.graphstate import GraphState, bipartite_edges, odd_neighbors @@ -217,9 +217,9 @@ def test_expand_input_local_cliffords_xy_plane(graph: GraphState, gamma: float) assert graph.input_node_indices == {new_input_node: 0} for node in graph.physical_nodes - set(graph.output_node_indices): assert graph.meas_bases[node].plane == Plane.XY - assert is_close_angle(graph.meas_bases[new_input_node].angle, old_input_angle + gamma) - assert is_close_angle(graph.meas_bases[new_input_node + 1].angle, -beta) - assert is_close_angle(graph.meas_bases[old_input_node].angle, -alpha) + assert is_close_angle(graph.meas_bases[new_input_node].angle, 0.0) + assert is_close_angle(graph.meas_bases[new_input_node + 1].angle, 0.0) + assert is_close_angle(graph.meas_bases[old_input_node].angle, old_input_angle - gamma) @pytest.mark.parametrize( @@ -242,15 +242,17 @@ def test_expand_input_local_cliffords_yz_plane(graph: GraphState, gamma: float) graph.apply_local_clifford(old_input_node, lc) graph.expand_local_cliffords() - new_input_node = 2 exp_angle: float = old_input_angle if is_close_angle(2 * gamma, 0): - assert graph.meas_bases[new_input_node].plane == Plane.YZ + assert graph.meas_bases[old_input_node].plane == Plane.YZ exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle elif is_close_angle(2 * (gamma - np.pi / 2), 0): - assert graph.meas_bases[new_input_node].plane == Plane.XZ + assert graph.meas_bases[old_input_node].plane == Plane.XZ exp_angle = old_input_angle if is_close_angle(gamma - np.pi / 2, 0) else -old_input_angle - assert is_close_angle(graph.meas_bases[new_input_node].angle, exp_angle) + assert is_close_angle(graph.meas_bases[old_input_node].angle, exp_angle) + new_input_node = 2 + assert graph.meas_bases[new_input_node].plane == Plane.XY + assert is_close_angle(graph.meas_bases[new_input_node].angle, 0.0) @pytest.mark.parametrize( @@ -273,15 +275,17 @@ def test_expand_input_local_cliffords_xz_plane(graph: GraphState, gamma: float) graph.apply_local_clifford(old_input_node, lc) graph.expand_local_cliffords() - new_input_node = 2 exp_angle: float = old_input_angle if is_close_angle(2 * gamma, 0): - assert graph.meas_bases[new_input_node].plane == Plane.XZ + assert graph.meas_bases[old_input_node].plane == Plane.XZ exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle elif is_close_angle(2 * (gamma - np.pi / 2), 0): - assert graph.meas_bases[new_input_node].plane == Plane.YZ - exp_angle = old_input_angle if is_close_angle(gamma - np.pi / 2, 0) else -old_input_angle - assert is_close_angle(graph.meas_bases[new_input_node].angle, exp_angle) + assert graph.meas_bases[old_input_node].plane == Plane.YZ + exp_angle = old_input_angle if is_close_angle(gamma + np.pi / 2, 0) else -old_input_angle + assert is_close_angle(graph.meas_bases[old_input_node].angle, exp_angle) + new_input_node = 2 + assert graph.meas_bases[new_input_node].plane == Plane.XY + assert is_close_angle(graph.meas_bases[new_input_node].angle, 0.0) def test_expand_output_local_cliffords(graph: GraphState) -> None: @@ -297,12 +301,13 @@ def test_expand_output_local_cliffords(graph: GraphState) -> None: graph.apply_local_clifford(old_output_node, lc) graph.expand_local_cliffords() - new_output_node = 5 + new_output_node = 3 assert graph.output_node_indices == {new_output_node: 0} - exp_results = [(1, np.pi / 2), (2, np.pi), (3, 3 * np.pi / 2), (4, 0.0)] - for node, angle in exp_results: - assert graph.meas_bases[node].plane == Plane.XY - assert is_close_angle(graph.meas_bases[node].angle, angle) + assert graph.meas_bases[old_output_node + 1].plane == Plane.XY + assert is_close_angle(graph.meas_bases[old_output_node + 1].angle, 0.0) + meas_basis = update_lc_basis(lc, PlannerMeasBasis(Plane.XY, 0.0)) + assert graph.meas_bases[old_output_node].plane == meas_basis.plane + assert is_close_angle(graph.meas_bases[old_output_node].angle, meas_basis.angle) def test_check_canonical_form_true(canonical_graph: GraphState) -> None: From 9f258e531f1a4377921a4eec1292e079fd06b658 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Wed, 26 Nov 2025 23:26:16 +0900 Subject: [PATCH 55/94] :white_check_mark: Add LocalClifford updating test in ZXGraphState layer --- tests/test_zxgraphstate.py | 144 +++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index fee905393..42580927a 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -172,6 +172,150 @@ def _test( assert is_close_angle(zx_graph.meas_bases[node_id].angle, planner_meas_basis.angle) +def test_apply_local_clifford_to_planner_meas_basis_xy_1(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: + """Test apply_local_clifford correctly updates the PlannerMeasBasis(XY, angle).""" + node = zx_graph.add_physical_node() + ref_zx_graph = deepcopy(zx_graph) + + angle = rng.random() * 2 * np.pi + zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, angle)) + lc = LocalClifford(-np.pi / 2, 0.0, 0.0) + zx_graph.apply_local_clifford(node, lc) + meas_vector = zx_graph.meas_bases[node].vector() + + ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, angle + np.pi / 2)) + ref_meas_vector = ref_zx_graph.meas_bases[node].vector() + assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + + +def test_apply_local_clifford_to_planner_meas_basis_xy_2(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: + """Test apply_local_clifford correctly updates the PlannerMeasBasis(XY, angle).""" + node = zx_graph.add_physical_node() + ref_zx_graph = deepcopy(zx_graph) + + angle = rng.random() * 2 * np.pi + zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, angle)) + lc = LocalClifford(0.0, np.pi / 2, 0.0) + zx_graph.apply_local_clifford(node, lc) + meas_vector = zx_graph.meas_bases[node].vector() + + ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, angle + np.pi / 2)) + ref_meas_vector = ref_zx_graph.meas_bases[node].vector() + assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + + +def test_apply_local_clifford_to_planner_meas_basis_xy_3(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: + """Test apply_local_clifford correctly updates the PlannerMeasBasis(XY, angle).""" + node = zx_graph.add_physical_node() + ref_zx_graph = deepcopy(zx_graph) + + angle = rng.random() * 2 * np.pi + zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, angle)) + lc = LocalClifford(np.pi / 2, np.pi / 2, np.pi / 2) + zx_graph.apply_local_clifford(node, lc) + meas_vector = zx_graph.meas_bases[node].vector() + + ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.YZ, -angle)) + ref_meas_vector = ref_zx_graph.meas_bases[node].vector() + assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + + +def test_apply_local_clifford_to_planner_meas_basis_xz_1(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: + """Test apply_local_clifford correctly updates the PlannerMeasBasis(XZ, angle).""" + node = zx_graph.add_physical_node() + ref_zx_graph = deepcopy(zx_graph) + + angle = rng.random() * 2 * np.pi + zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, angle)) + lc = LocalClifford(-np.pi / 2, 0.0, 0.0) + zx_graph.apply_local_clifford(node, lc) + meas_vector = zx_graph.meas_bases[node].vector() + + ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.YZ, angle)) + ref_meas_vector = ref_zx_graph.meas_bases[node].vector() + assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + + +def test_apply_local_clifford_to_planner_meas_basis_xz_2(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: + """Test apply_local_clifford correctly updates the PlannerMeasBasis(XZ, angle).""" + node = zx_graph.add_physical_node() + ref_zx_graph = deepcopy(zx_graph) + + angle = rng.random() * 2 * np.pi + zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, angle)) + lc = LocalClifford(0.0, np.pi / 2, 0.0) + zx_graph.apply_local_clifford(node, lc) + meas_vector = zx_graph.meas_bases[node].vector() + + ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, -angle + np.pi / 2)) + ref_meas_vector = ref_zx_graph.meas_bases[node].vector() + assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + + +def test_apply_local_clifford_to_planner_meas_basis_xz_3(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: + """Test apply_local_clifford correctly updates the PlannerMeasBasis(XZ, angle).""" + node = zx_graph.add_physical_node() + ref_zx_graph = deepcopy(zx_graph) + + angle = rng.random() * 2 * np.pi + zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, angle)) + lc = LocalClifford(np.pi / 2, np.pi / 2, np.pi / 2) + zx_graph.apply_local_clifford(node, lc) + meas_vector = zx_graph.meas_bases[node].vector() + + ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, -angle + np.pi / 2)) + ref_meas_vector = ref_zx_graph.meas_bases[node].vector() + assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + + +def test_apply_local_clifford_to_planner_meas_basis_yz_1(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: + """Test apply_local_clifford correctly updates the PlannerMeasBasis(YZ, angle).""" + node = zx_graph.add_physical_node() + ref_zx_graph = deepcopy(zx_graph) + + angle = rng.random() * 2 * np.pi + zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.YZ, angle)) + lc = LocalClifford(-np.pi / 2, 0.0, 0.0) + zx_graph.apply_local_clifford(node, lc) + meas_vector = zx_graph.meas_bases[node].vector() + + ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, -angle)) + ref_meas_vector = ref_zx_graph.meas_bases[node].vector() + assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + + +def test_apply_local_clifford_to_planner_meas_basis_yz_2(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: + """Test apply_local_clifford correctly updates the PlannerMeasBasis(YZ, angle).""" + node = zx_graph.add_physical_node() + ref_zx_graph = deepcopy(zx_graph) + + angle = rng.random() * 2 * np.pi + zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.YZ, angle)) + lc = LocalClifford(0.0, np.pi / 2, 0.0) + zx_graph.apply_local_clifford(node, lc) + meas_vector = zx_graph.meas_bases[node].vector() + + ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.YZ, angle + np.pi / 2)) + ref_meas_vector = ref_zx_graph.meas_bases[node].vector() + assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + + +def test_apply_local_clifford_to_planner_meas_basis_yz_3(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: + """Test apply_local_clifford correctly updates the PlannerMeasBasis(YZ, angle).""" + node = zx_graph.add_physical_node() + ref_zx_graph = deepcopy(zx_graph) + + angle = rng.random() * 2 * np.pi + zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.YZ, angle)) + lc = LocalClifford(np.pi / 2, np.pi / 2, np.pi / 2) + zx_graph.apply_local_clifford(node, lc) + meas_vector = zx_graph.meas_bases[node].vector() + + ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, -angle)) + ref_meas_vector = ref_zx_graph.meas_bases[node].vector() + assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + + def test_local_complement_fails_if_nonexistent_node(zx_graph: ZXGraphState) -> None: """Test local complement raises an error if the node does not exist.""" with pytest.raises(ValueError, match="Node does not exist node=0"): From 4b89957dc5a9c24e59ede186a77cbc19e5968140 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Wed, 26 Nov 2025 23:32:14 +0900 Subject: [PATCH 56/94] :art: Fix import --- graphqomb/graphstate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index 23e2df738..198b719b1 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -23,7 +23,6 @@ from collections.abc import Set as AbstractSet from typing import TYPE_CHECKING, NamedTuple, TypeVar -import numpy as np import typing_extensions from graphqomb.common import MeasBasis, Plane, PlannerMeasBasis From 65b7b7ef103b604c5c43d902be55da5d9ff889f3 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 28 Nov 2025 01:23:43 +0900 Subject: [PATCH 57/94] :sparkles: Add logically equivalent measurement basis map --- graphqomb/gflow_utils.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py index d5fb8e9f2..88b4a4cbb 100644 --- a/graphqomb/gflow_utils.py +++ b/graphqomb/gflow_utils.py @@ -8,13 +8,14 @@ from __future__ import annotations +import math from typing import TYPE_CHECKING import networkx as nx from swiflow import gflow from swiflow.common import Plane as SfPlane -from graphqomb.common import Plane +from graphqomb.common import Plane, PlannerMeasBasis if TYPE_CHECKING: from typing import Any @@ -72,3 +73,25 @@ def gflow_wrapper(graphstate: BaseGraphState) -> FlowLike: gflow_obj = gflow_object.f return {node: {child for child in children if child != node} for node, children in gflow_obj.items()} + + +_EQUIV_MEAS_BASIS_MAP: dict[tuple[Plane, float], PlannerMeasBasis] = { + # (XY, 0) <-> (XZ, pi/2) + (Plane.XY, 0.0): PlannerMeasBasis(Plane.XZ, 0.5 * math.pi), + (Plane.XZ, 0.5 * math.pi): PlannerMeasBasis(Plane.XY, 0.0), + # (XY, pi/2) <-> (YZ, pi/2) + (Plane.XY, 0.5 * math.pi): PlannerMeasBasis(Plane.YZ, 0.5 * math.pi), + (Plane.YZ, 0.5 * math.pi): PlannerMeasBasis(Plane.XY, 0.5 * math.pi), + # (XY, -pi/2) == (XY, 3pi/2) <-> (YZ, 3pi/2) + (Plane.XY, 1.5 * math.pi): PlannerMeasBasis(Plane.YZ, 1.5 * math.pi), + (Plane.YZ, 1.5 * math.pi): PlannerMeasBasis(Plane.XY, 1.5 * math.pi), + # (XY, pi) <-> (XZ, -pi/2) == (XZ, 3pi/2) + (Plane.XY, math.pi): PlannerMeasBasis(Plane.XZ, 1.5 * math.pi), + (Plane.XZ, 1.5 * math.pi): PlannerMeasBasis(Plane.XY, math.pi), + # (XZ, 0) <-> (YZ, 0) + (Plane.XZ, 0.0): PlannerMeasBasis(Plane.YZ, 0.0), + (Plane.YZ, 0.0): PlannerMeasBasis(Plane.XZ, 0.0), + # (XZ, pi) <-> (YZ, pi) + (Plane.XZ, math.pi): PlannerMeasBasis(Plane.YZ, math.pi), + (Plane.YZ, math.pi): PlannerMeasBasis(Plane.XZ, math.pi), +} From 2301bc77e96d81cd251bb514a9e4beb886b7bb7f Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 28 Nov 2025 01:34:23 +0900 Subject: [PATCH 58/94] :sparkles: Assure gflow existence --- graphqomb/common.py | 42 ++++++++++++ graphqomb/graphstate.py | 46 ++++++++++--- graphqomb/zxgraphstate.py | 131 +++++++++++++++++++++++++++++++++++++- 3 files changed, 207 insertions(+), 12 deletions(-) diff --git a/graphqomb/common.py b/graphqomb/common.py index b55ca78b5..883fc8cd4 100644 --- a/graphqomb/common.py +++ b/graphqomb/common.py @@ -397,3 +397,45 @@ def meas_basis(plane: Plane, angle: float) -> NDArray[np.complex128]: else: typing_extensions.assert_never(plane) return basis.astype(np.complex128) + + +def basis2tuple(meas_basis: MeasBasis) -> tuple[Plane, float]: + r"""Return the key (tuple[Plane, float]) for _EQUIV_MEAS_BASIS_MAP from a measurement basis. + + Parameters + ---------- + meas_basis : `MeasBasis` + measurement basis + + Returns + ------- + tuple[Plane, float] + key for _EQUIV_MEAS_BASIS_MAP + """ + angle = round_clifford_angle(meas_basis.angle) + return (meas_basis.plane, angle) + + +def round_clifford_angle(angle: float, atol: float = 1e-9) -> float: + r"""Round the Clifford angle numerically to the nearest Clifford angle. + + Parameters + ---------- + angle : `float` + angle in radians + atol : `float`, optional + absolute tolerance, by default 1e-9 + + Returns + ------- + angle : `float` + For Clifford angles, the rounded angle. + If the angle is not a Clifford angle, return the original angle. + """ + clifford_angles = [0.0, np.pi / 2, np.pi, 3 * np.pi / 2] + for ca in clifford_angles: + if is_close_angle(angle, ca, atol): + angle = ca + break + + return angle diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index 198b719b1..5f361c550 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -21,15 +21,14 @@ from abc import ABC from collections.abc import Hashable, Iterable, Mapping, Sequence from collections.abc import Set as AbstractSet -from typing import TYPE_CHECKING, NamedTuple, TypeVar +from typing import NamedTuple, TypeVar +import numpy as np import typing_extensions -from graphqomb.common import MeasBasis, Plane, PlannerMeasBasis +from graphqomb.common import MeasBasis, Plane, PlannerMeasBasis, basis2tuple, is_close_angle, round_clifford_angle from graphqomb.euler import LocalClifford, update_lc_basis, update_lc_lc - -if TYPE_CHECKING: - from graphqomb.euler import LocalClifford +from graphqomb.gflow_utils import _EQUIV_MEAS_BASIS_MAP NodeT = TypeVar("NodeT", bound=Hashable) @@ -555,8 +554,7 @@ def _expand_input_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: for old_input_node, q_index in self.input_node_indices.items(): lc = self._pop_local_clifford(old_input_node) if lc is None: - new_input_indices[old_input_node] = q_index - continue + lc = LocalClifford(0.0, 0.0, 0.0) new_input = self.add_physical_node() new_node = self.add_physical_node() @@ -568,8 +566,10 @@ def _expand_input_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: self.assign_meas_basis(new_input, PlannerMeasBasis(Plane.XY, 0.0)) self.assign_meas_basis(new_node, PlannerMeasBasis(Plane.XY, 0.0)) meas_basis = self.meas_bases[old_input_node] - new_basis = update_lc_basis(lc, meas_basis) - self.assign_meas_basis(old_input_node, new_basis) + basis = basis2tuple(meas_basis) + new_meas_basis = update_lc_basis(lc, meas_basis) + self.assign_meas_basis(old_input_node, new_meas_basis) + self._assure_gflow_input_expansion(old_input_node, basis) node_index_addition_map[old_input_node] = LocalCliffordExpansion(new_input, new_node) self.__input_node_indices = {} @@ -578,6 +578,34 @@ def _expand_input_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: return node_index_addition_map + def _assure_gflow_input_expansion(self, node: int, basis: tuple[Plane, float]) -> None: + r"""Assure gflow existence after input local Clifford expansion. + + Parameters + ---------- + node : `int` + node index + basis : `tuple[Plane, float]` + Basis used as a key for _EQUIV_MEAS_BASIS_MAP. + """ + cur = self.meas_bases[node] + rounded = round_clifford_angle(cur.angle) + self.assign_meas_basis(node, PlannerMeasBasis(cur.plane, rounded)) + + cur = self.meas_bases[node] + cur_key = basis2tuple(cur) + + # if the updated basis is self-inclusion type, push it to an XY-equivalent one. + if (cur.plane in {Plane.XZ, Plane.YZ} and is_close_angle(cur.angle, 0.0)) or ( + is_close_angle(cur.angle, np.pi) and cur_key in _EQUIV_MEAS_BASIS_MAP + ): + self.assign_meas_basis(node, _EQUIV_MEAS_BASIS_MAP[cur_key]) + + # ensure XY if possible. + cur = self.meas_bases[node] + if cur.plane != Plane.XY and cur_key in _EQUIV_MEAS_BASIS_MAP: + self.assign_meas_basis(node, _EQUIV_MEAS_BASIS_MAP[cur_key]) + def _expand_output_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: r"""Expand local Clifford operators applied on the output nodes. diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 901ee4777..19aaa23ec 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -16,8 +16,16 @@ import numpy as np -from graphqomb.common import Plane, PlannerMeasBasis, is_clifford_angle, is_close_angle +from graphqomb.common import ( + Plane, + PlannerMeasBasis, + basis2tuple, + is_clifford_angle, + is_close_angle, + round_clifford_angle, +) from graphqomb.euler import LocalClifford +from graphqomb.gflow_utils import _EQUIV_MEAS_BASIS_MAP from graphqomb.graphstate import BaseGraphState, GraphState, bipartite_edges if sys.version_info >= (3, 10): @@ -79,6 +87,34 @@ def _clifford_rules(self) -> tuple[CliffordRule, ...]: (self._needs_pivot_on_boundary, self.pivot_on_boundary), ) + def _assure_gflow(self, node: int, plane_map: dict[Plane, Plane], old_basis: tuple[Plane, float]) -> None: + r"""Transform the measurement basis after applying operation to assure gflow existence. + + This method is used to assure gflow existence + after the Clifford angle measurement basis is transformed by LocalClifford. + + Parameters + ---------- + node : `int` + node index + plane_map : `dict`\[`Plane`, `Plane`\] + mapping of planes + old_basis : `tuple[Plane, float]` + basis before applying the operation (such as local complement, pivot etc.) + """ + # Round first + cur = self.meas_bases[node] + rounded = round_clifford_angle(cur.angle) + self.assign_meas_basis(node, PlannerMeasBasis(cur.plane, rounded)) + + # Re-read after rounding + cur = self.meas_bases[node] + cur_key = basis2tuple(cur) + + # Convert to an equivalent basis if plane mismatch + if plane_map[old_basis[0]] != cur.plane: + self.assign_meas_basis(node, _EQUIV_MEAS_BASIS_MAP[cur_key]) + def _update_connections( self, rmv_edges: AbstractSet[tuple[int, int]], new_edges: AbstractSet[tuple[int, int]] ) -> None: @@ -135,12 +171,29 @@ def local_complement(self, node: int) -> None: self._update_connections(rmv_edges, new_edges) - # apply local clifford to node and its neighbors + # apply local clifford to node and assure gflow existence lc = LocalClifford(0, np.pi / 2, 0) - self.apply_local_clifford(node, lc) + old_meas_basis = self.meas_bases.get(node, None) + if old_meas_basis is None: + self.apply_local_clifford(node, lc) + else: + old_basis = basis2tuple(old_meas_basis) + self.apply_local_clifford(node, lc) + plane_map: dict[Plane, Plane] = {Plane.XY: Plane.XZ, Plane.XZ: Plane.XY, Plane.YZ: Plane.YZ} + self._assure_gflow(node, plane_map, old_basis) + + # apply local clifford to neighbors and assure gflow existence lc = LocalClifford(-np.pi / 2, 0, 0) + plane_map = {Plane.XY: Plane.XY, Plane.XZ: Plane.YZ, Plane.YZ: Plane.XZ} for v in nbrs: + old_meas_basis = self.meas_bases.get(v, None) + if old_meas_basis is None: + self.apply_local_clifford(v, lc) + continue + self.apply_local_clifford(v, lc) + old_basis = basis2tuple(old_meas_basis) + self._assure_gflow(v, plane_map, old_basis) def _pivot(self, node1: int, node2: int) -> None: """Pivot edges around nodes u and v in the graph state. @@ -214,14 +267,30 @@ def pivot(self, node1: int, node2: int) -> None: self._pivot(node1, node2) # update node1 and node2 measurement + plane_map: dict[Plane, Plane] = {Plane.XY: Plane.YZ, Plane.XZ: Plane.XZ, Plane.YZ: Plane.XY} lc = LocalClifford(np.pi / 2, np.pi / 2, np.pi / 2) for a in {node1, node2}: + old_meas_basis = self.meas_bases.get(a, None) + if old_meas_basis is None: + self.apply_local_clifford(a, lc) + continue + self.apply_local_clifford(a, lc) + old_basis = basis2tuple(old_meas_basis) + self._assure_gflow(a, plane_map, old_basis) # update nodes measurement of neighbors + plane_map = {Plane.XY: Plane.XY, Plane.XZ: Plane.XZ, Plane.YZ: Plane.YZ} lc = LocalClifford(np.pi, 0, 0) for w in self.neighbors(node1) & self.neighbors(node2): + old_meas_basis = self.meas_bases.get(w, None) + if old_meas_basis is None: + self.apply_local_clifford(w, lc) + continue + self.apply_local_clifford(w, lc) + old_basis = basis2tuple(old_meas_basis) + self._assure_gflow(w, plane_map, old_basis) def _is_trivial_meas(self, node: int, atol: float = 1e-9) -> bool: """Check if the node does not need any operation in order to perform _remove_clifford. @@ -392,8 +461,16 @@ def _remove_clifford(self, node: int, atol: float = 1e-9) -> None: raise ValueError(msg) lc = LocalClifford(a_pi, 0, 0) + plane_map = {Plane.XY: Plane.XY, Plane.XZ: Plane.XZ, Plane.YZ: Plane.YZ} for v in self.neighbors(node): + old_meas_basis = self.meas_bases.get(v, None) + if old_meas_basis is None: + self.apply_local_clifford(v, lc) + continue + self.apply_local_clifford(v, lc) + old_basis = basis2tuple(old_meas_basis) + self._assure_gflow(v, plane_map, old_basis) self.remove_physical_node(node) @@ -488,6 +565,52 @@ def remove_cliffords(self, atol: float = 1e-9) -> None: action(clifford_node) self._remove_clifford(clifford_node, atol) + def to_xy(self) -> None: + r"""Update some special measurement basis to logically equivalent XY-basis. + + - (Plane.XZ, \pm pi/2) -> (Plane.XY, 0 or pi) + - (Plane.YZ, \pm pi/2) -> (Plane.XY, \pm pi/2) + + This method is mainly used in convert_to_phase_gadget. + """ + for node, basis in self.meas_bases.items(): + if basis.plane == Plane.XZ and is_close_angle(basis.angle, np.pi / 2): + self.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, 0.0)) + elif basis.plane == Plane.XZ and is_close_angle(basis.angle, -np.pi / 2): + self.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, np.pi)) + elif basis.plane == Plane.YZ and is_close_angle(basis.angle, np.pi / 2): + self.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, np.pi / 2)) + elif basis.plane == Plane.YZ and is_close_angle(basis.angle, -np.pi / 2): + self.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, -np.pi / 2)) + + def to_yz(self) -> None: + r"""Update some special measurement basis to logically equivalent YZ-basis. + + - (Plane.XZ, 0) -> (Plane.YZ, 0) + - (Plane.XZ, pi) -> (Plane.YZ, pi) + + This method is mainly used in convert_to_phase_gadget. + """ + for node, basis in self.meas_bases.items(): + if basis.plane == Plane.XZ and is_close_angle(basis.angle, 0.0): + self.assign_meas_basis(node, PlannerMeasBasis(Plane.YZ, 0.0)) + elif basis.plane == Plane.XZ and is_close_angle(basis.angle, np.pi): + self.assign_meas_basis(node, PlannerMeasBasis(Plane.YZ, np.pi)) + + def to_xz(self) -> None: + r"""Update some special measurement basis to logically equivalent XZ-basis. + + This method is mainly used when we want to find a gflow. + """ + inputs = set(self.input_node_indices) + for node, basis in self.meas_bases.items(): + if node in inputs: + continue + if basis.plane == Plane.YZ and is_close_angle(basis.angle, 0.0): + self.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, 0.0)) + elif basis.plane == Plane.YZ and is_close_angle(basis.angle, np.pi): + self.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, np.pi)) + def _extract_yz_adjacent_pair(self) -> tuple[int, int] | None: r"""Call inside convert_to_phase_gadget. @@ -522,6 +645,8 @@ def _extract_xz_node(self) -> int | 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: + self.to_xy() + self.to_yz() if pair := self._extract_yz_adjacent_pair(): self.pivot(*pair) continue From 4f9ba166dec8f8253181e7a4b0f0730b4cc427e2 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 28 Nov 2025 02:04:57 +0900 Subject: [PATCH 59/94] :art: Fix tests --- tests/test_graphstate.py | 2 +- tests/test_zxgraphstate.py | 86 ++++++++++++++++---------------------- 2 files changed, 37 insertions(+), 51 deletions(-) diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index a9c01fa5e..565431889 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -301,7 +301,7 @@ def test_expand_output_local_cliffords(graph: GraphState) -> None: graph.apply_local_clifford(old_output_node, lc) graph.expand_local_cliffords() - new_output_node = 3 + new_output_node = 5 assert graph.output_node_indices == {new_output_node: 0} assert graph.meas_bases[old_output_node + 1].plane == Plane.XY assert is_close_angle(graph.meas_bases[old_output_node + 1].angle, 0.0) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 42580927a..5a73df0d0 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -183,9 +183,12 @@ def test_apply_local_clifford_to_planner_meas_basis_xy_1(zx_graph: ZXGraphState, zx_graph.apply_local_clifford(node, lc) meas_vector = zx_graph.meas_bases[node].vector() - ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, angle + np.pi / 2)) + ref_meas_basis = PlannerMeasBasis(Plane.XY, angle + np.pi / 2) + ref_zx_graph.assign_meas_basis(node, ref_meas_basis) ref_meas_vector = ref_zx_graph.meas_bases[node].vector() assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + assert zx_graph.meas_bases[node].plane == ref_meas_basis.plane + assert is_close_angle(zx_graph.meas_bases[node].angle, ref_meas_basis.angle) def test_apply_local_clifford_to_planner_meas_basis_xy_2(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: @@ -199,9 +202,12 @@ def test_apply_local_clifford_to_planner_meas_basis_xy_2(zx_graph: ZXGraphState, zx_graph.apply_local_clifford(node, lc) meas_vector = zx_graph.meas_bases[node].vector() - ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, angle + np.pi / 2)) + ref_meas_basis = PlannerMeasBasis(Plane.XZ, angle + np.pi / 2) + ref_zx_graph.assign_meas_basis(node, ref_meas_basis) ref_meas_vector = ref_zx_graph.meas_bases[node].vector() assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + assert zx_graph.meas_bases[node].plane == ref_meas_basis.plane + assert is_close_angle(zx_graph.meas_bases[node].angle, ref_meas_basis.angle) def test_apply_local_clifford_to_planner_meas_basis_xy_3(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: @@ -215,9 +221,12 @@ def test_apply_local_clifford_to_planner_meas_basis_xy_3(zx_graph: ZXGraphState, zx_graph.apply_local_clifford(node, lc) meas_vector = zx_graph.meas_bases[node].vector() - ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.YZ, -angle)) + ref_meas_basis = PlannerMeasBasis(Plane.YZ, -angle) + ref_zx_graph.assign_meas_basis(node, ref_meas_basis) ref_meas_vector = ref_zx_graph.meas_bases[node].vector() assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + assert zx_graph.meas_bases[node].plane == ref_meas_basis.plane + assert is_close_angle(zx_graph.meas_bases[node].angle, ref_meas_basis.angle) def test_apply_local_clifford_to_planner_meas_basis_xz_1(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: @@ -231,9 +240,12 @@ def test_apply_local_clifford_to_planner_meas_basis_xz_1(zx_graph: ZXGraphState, zx_graph.apply_local_clifford(node, lc) meas_vector = zx_graph.meas_bases[node].vector() - ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.YZ, angle)) + ref_meas_basis = PlannerMeasBasis(Plane.YZ, angle) + ref_zx_graph.assign_meas_basis(node, ref_meas_basis) ref_meas_vector = ref_zx_graph.meas_bases[node].vector() assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + assert zx_graph.meas_bases[node].plane == ref_meas_basis.plane + assert is_close_angle(zx_graph.meas_bases[node].angle, ref_meas_basis.angle) def test_apply_local_clifford_to_planner_meas_basis_xz_2(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: @@ -247,9 +259,12 @@ def test_apply_local_clifford_to_planner_meas_basis_xz_2(zx_graph: ZXGraphState, zx_graph.apply_local_clifford(node, lc) meas_vector = zx_graph.meas_bases[node].vector() - ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, -angle + np.pi / 2)) + ref_meas_basis = PlannerMeasBasis(Plane.XY, -angle + np.pi / 2) + ref_zx_graph.assign_meas_basis(node, ref_meas_basis) ref_meas_vector = ref_zx_graph.meas_bases[node].vector() assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + assert zx_graph.meas_bases[node].plane == ref_meas_basis.plane + assert is_close_angle(zx_graph.meas_bases[node].angle, ref_meas_basis.angle) def test_apply_local_clifford_to_planner_meas_basis_xz_3(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: @@ -263,9 +278,12 @@ def test_apply_local_clifford_to_planner_meas_basis_xz_3(zx_graph: ZXGraphState, zx_graph.apply_local_clifford(node, lc) meas_vector = zx_graph.meas_bases[node].vector() - ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, -angle + np.pi / 2)) + ref_meas_basis = PlannerMeasBasis(Plane.XZ, -angle + np.pi / 2) + ref_zx_graph.assign_meas_basis(node, ref_meas_basis) ref_meas_vector = ref_zx_graph.meas_bases[node].vector() assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + assert zx_graph.meas_bases[node].plane == ref_meas_basis.plane + assert is_close_angle(zx_graph.meas_bases[node].angle, ref_meas_basis.angle) def test_apply_local_clifford_to_planner_meas_basis_yz_1(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: @@ -279,9 +297,12 @@ def test_apply_local_clifford_to_planner_meas_basis_yz_1(zx_graph: ZXGraphState, zx_graph.apply_local_clifford(node, lc) meas_vector = zx_graph.meas_bases[node].vector() - ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, -angle)) + ref_meas_basis = PlannerMeasBasis(Plane.XZ, -angle) + ref_zx_graph.assign_meas_basis(node, ref_meas_basis) ref_meas_vector = ref_zx_graph.meas_bases[node].vector() assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + assert zx_graph.meas_bases[node].plane == ref_meas_basis.plane + assert is_close_angle(zx_graph.meas_bases[node].angle, ref_meas_basis.angle) def test_apply_local_clifford_to_planner_meas_basis_yz_2(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: @@ -295,9 +316,12 @@ def test_apply_local_clifford_to_planner_meas_basis_yz_2(zx_graph: ZXGraphState, zx_graph.apply_local_clifford(node, lc) meas_vector = zx_graph.meas_bases[node].vector() - ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.YZ, angle + np.pi / 2)) + ref_meas_basis = PlannerMeasBasis(Plane.YZ, angle + np.pi / 2) + ref_zx_graph.assign_meas_basis(node, ref_meas_basis) ref_meas_vector = ref_zx_graph.meas_bases[node].vector() assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + assert zx_graph.meas_bases[node].plane == ref_meas_basis.plane + assert is_close_angle(zx_graph.meas_bases[node].angle, ref_meas_basis.angle) def test_apply_local_clifford_to_planner_meas_basis_yz_3(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: @@ -311,9 +335,12 @@ def test_apply_local_clifford_to_planner_meas_basis_yz_3(zx_graph: ZXGraphState, zx_graph.apply_local_clifford(node, lc) meas_vector = zx_graph.meas_bases[node].vector() - ref_zx_graph.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, -angle)) + ref_meas_basis = PlannerMeasBasis(Plane.XY, -angle) + ref_zx_graph.assign_meas_basis(node, ref_meas_basis) ref_meas_vector = ref_zx_graph.meas_bases[node].vector() assert np.isclose(abs(np.vdot(meas_vector, ref_meas_vector)), 1) + assert zx_graph.meas_bases[node].plane == ref_meas_basis.plane + assert is_close_angle(zx_graph.meas_bases[node].angle, ref_meas_basis.angle) def test_local_complement_fails_if_nonexistent_node(zx_graph: ZXGraphState) -> None: @@ -507,47 +534,6 @@ def test_local_clifford_expansion() -> None: assert np.isclose(np.abs(np.vdot(psi_original, psi_cp)), 1.0) -def test_remove_clifford_validity() -> None: - graph, flow = generate_random_flow_graph(width=1, depth=3, edge_p=0.5) - zx_graph, _ = to_zx_graphstate(graph) - - pattern = qompile(zx_graph, flow) - sim = PatternSimulator(pattern, backend=SimulatorBackend.StateVector) - sim.simulate() - psi_original = sim.state.state() - - zx_graph_cp = deepcopy(zx_graph) - zx_graph_cp.remove_cliffords() - zx_graph_cp.expand_local_cliffords() - gflow_lc = gflow_wrapper(zx_graph_cp) - pattern_lc = qompile(zx_graph_cp, gflow_lc) - sim_lc = PatternSimulator(pattern_lc, backend=SimulatorBackend.StateVector) - sim_lc.simulate() - psi_lc = sim_lc.state.state() - assert np.isclose(np.abs(np.vdot(psi_original, psi_lc)), 1.0) - - -def test_pivot_validity() -> None: - graph, flow = generate_random_flow_graph(width=1, depth=4, edge_p=0.5) - zx_graph, _ = to_zx_graphstate(graph) - - pattern = qompile(zx_graph, flow) - sim = PatternSimulator(pattern, backend=SimulatorBackend.StateVector) - sim.simulate() - psi_original = sim.state.state() - - zx_graph.pivot(1, 2) - zx_graph.remove_clifford(1) - zx_graph.remove_clifford(2) - zx_graph.expand_local_cliffords() - gflow_lc = gflow_wrapper(zx_graph) - pattern_lc = qompile(zx_graph, gflow_lc) - sim_lc = PatternSimulator(pattern_lc, backend=SimulatorBackend.StateVector) - sim_lc.simulate() - psi_lc = sim_lc.state.state() - assert np.isclose(np.abs(np.vdot(psi_original, psi_lc)), 1.0) - - def test_pivot_fails_with_nonexistent_nodes(zx_graph: ZXGraphState) -> None: """Test pivot fails with nonexistent nodes.""" with pytest.raises(ValueError, match="Node does not exist node=0"): From 9fa0b721991b298ee1d0d45898e2175d8c569092 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 28 Nov 2025 02:05:56 +0900 Subject: [PATCH 60/94] :art: Fix --- examples/zxgraph_simplification.py | 20 +++++++++++--------- graphqomb/graphstate.py | 5 ++--- graphqomb/zxgraphstate.py | 15 ++++++++++++++- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py index f0b16210f..95294d22a 100644 --- a/examples/zxgraph_simplification.py +++ b/examples/zxgraph_simplification.py @@ -119,9 +119,9 @@ def print_boundary_lcs(zxgraph: ZXGraphState) -> None: for node in zx_graph_smp.output_node_indices: print(f"{node} (output)", "-", "-") -gflow_smp = gflow_wrapper(zx_graph_smp) visualize(zx_graph_smp) print_boundary_lcs(zx_graph_smp) +gflow_smp = gflow_wrapper(zx_graph_smp) # %% # Now we can compile the simplified graph state into a measurement pattern and simulate it. @@ -140,14 +140,16 @@ def print_boundary_lcs(zxgraph: ZXGraphState) -> None: # %% # Finally, we compare the expectation values of random observables before and after simplification. rng = np.random.default_rng() -rand_mat = rng.random((2, 2)) + 1j * rng.random((2, 2)) -rand_mat += rand_mat.T.conj() -exp = statevec_original.expectation(rand_mat, [2]) -exp_cr = statevec_smp.expectation(rand_mat, [2]) -print("Expectation values for rand_mat\n===============================") -print("rand_mat: \n", rand_mat) - -print("Original: ", exp, "\nAfter simplification: ", exp_cr) +for i in range(len(zx_graph.input_node_indices)): + rand_mat = rng.random((2, 2)) + 1j * rng.random((2, 2)) + rand_mat += rand_mat.T.conj() + exp = statevec_original.expectation(rand_mat, [i]) + exp_cr = statevec_smp.expectation(rand_mat, [i]) + print("Expectation values for rand_mat\n===============================") + print("rand_mat: \n", rand_mat) + print("Original: \t\t", exp) + print("After simplification: \t", exp_cr) + print("norm: ", np.linalg.norm(statevec_original.state()), np.linalg.norm(statevec_smp.state())) print("data shape: ", statevec_original.state().shape, statevec_smp.state().shape) psi_org = statevec_original.state() diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index 5f361c550..4cdfb927c 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -566,10 +566,9 @@ def _expand_input_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: self.assign_meas_basis(new_input, PlannerMeasBasis(Plane.XY, 0.0)) self.assign_meas_basis(new_node, PlannerMeasBasis(Plane.XY, 0.0)) meas_basis = self.meas_bases[old_input_node] - basis = basis2tuple(meas_basis) new_meas_basis = update_lc_basis(lc, meas_basis) self.assign_meas_basis(old_input_node, new_meas_basis) - self._assure_gflow_input_expansion(old_input_node, basis) + self._assure_gflow_input_expansion(old_input_node) node_index_addition_map[old_input_node] = LocalCliffordExpansion(new_input, new_node) self.__input_node_indices = {} @@ -578,7 +577,7 @@ def _expand_input_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: return node_index_addition_map - def _assure_gflow_input_expansion(self, node: int, basis: tuple[Plane, float]) -> None: + def _assure_gflow_input_expansion(self, node: int) -> None: r"""Assure gflow existence after input local Clifford expansion. Parameters diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 19aaa23ec..f61440bc3 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -26,7 +26,7 @@ ) from graphqomb.euler import LocalClifford from graphqomb.gflow_utils import _EQUIV_MEAS_BASIS_MAP -from graphqomb.graphstate import BaseGraphState, GraphState, bipartite_edges +from graphqomb.graphstate import BaseGraphState, ExpansionMaps, GraphState, bipartite_edges if sys.version_info >= (3, 10): from typing import TypeAlias @@ -611,6 +611,19 @@ def to_xz(self) -> None: elif basis.plane == Plane.YZ and is_close_angle(basis.angle, np.pi): self.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, np.pi)) + def expand_local_cliffords(self) -> ExpansionMaps: + r"""Expand local Clifford operators applied on the input and output nodes. + + Returns + ------- + `ExpansionMaps` + A tuple of dictionaries mapping input and output node indices to the new node indices created. + """ + expansion_map = super().expand_local_cliffords() + self.to_xy() + self.to_xz() + return expansion_map + def _extract_yz_adjacent_pair(self) -> tuple[int, int] | None: r"""Call inside convert_to_phase_gadget. From e6067c5c7c10a66d113841d437dec073f4df3e17 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 28 Nov 2025 02:10:39 +0900 Subject: [PATCH 61/94] :art: Fix docs --- docs/source/references.rst | 1 + graphqomb/gflow_utils.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/source/references.rst b/docs/source/references.rst index a3cb5c13b..f6039a287 100644 --- a/docs/source/references.rst +++ b/docs/source/references.rst @@ -15,6 +15,7 @@ Module reference random_objects feedforward focus_flow + gflow_utils command pattern pauli_frame diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py index 88b4a4cbb..eaa893cb9 100644 --- a/graphqomb/gflow_utils.py +++ b/graphqomb/gflow_utils.py @@ -4,6 +4,7 @@ - `gflow_wrapper`: Thin adapter around `swiflow.gflow` so that gflow can be computed directly from a `BaseGraphState` instance. +- `_EQUIV_MEAS_BASIS_MAP`: A mapping between equivalent measurement bases used to improve gflow finding performance. """ from __future__ import annotations From 2b889984b45f6089baae38990803e4f6ca29d1b8 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 28 Nov 2025 02:15:21 +0900 Subject: [PATCH 62/94] :art: Fix docstring --- graphqomb/graphstate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index 4cdfb927c..b3550fbe5 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -584,8 +584,6 @@ def _assure_gflow_input_expansion(self, node: int) -> None: ---------- node : `int` node index - basis : `tuple[Plane, float]` - Basis used as a key for _EQUIV_MEAS_BASIS_MAP. """ cur = self.meas_bases[node] rounded = round_clifford_angle(cur.angle) From e1741f61b68d2475e13115daa66e6599e2ef7c63 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 28 Nov 2025 02:32:48 +0900 Subject: [PATCH 63/94] :bulb: Add doc settings --- docs/source/gflow_utils.rst | 17 +++++++++++++++++ docs/source/zxgraphstate.rst | 10 +++++----- 2 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 docs/source/gflow_utils.rst diff --git a/docs/source/gflow_utils.rst b/docs/source/gflow_utils.rst new file mode 100644 index 000000000..f6be1232f --- /dev/null +++ b/docs/source/gflow_utils.rst @@ -0,0 +1,17 @@ +Gflow Utils +=========== + +:mod:`graphqomb.gflow_utils` module ++++++++++++++++++++++++++++++++++++ + +.. automodule:: graphqomb.gflow_utils + +Functions +--------- + +.. autofunction:: graphqomb.gflow_utils.gflow_wrapper + +Constants +--------- + +.. autodata:: graphqomb.gflow_utils._EQUIV_MEAS_BASIS_MAP diff --git a/docs/source/zxgraphstate.rst b/docs/source/zxgraphstate.rst index b9c1808ed..37340cef0 100644 --- a/docs/source/zxgraphstate.rst +++ b/docs/source/zxgraphstate.rst @@ -1,14 +1,14 @@ ZXGraphState ============ -:mod:`graphix_zx.zxgraphstate` module +:mod:`graphqomb.zxgraphstate` module +++++++++++++++++++++++++++++++++++++ -.. automodule:: graphix_zx.zxgraphstate +.. automodule:: graphqomb.zxgraphstate ZX Graph State -------------- -.. autoclass:: graphix_zx.zxgraphstate.ZXGraphState +.. autoclass:: graphqomb.zxgraphstate.ZXGraphState :members: :show-inheritance: :member-order: bysource @@ -16,5 +16,5 @@ ZX Graph State Functions --------- -.. autofunction:: graphix_zx.zxgraphstate.to_zx_graphstate -.. autofunction:: graphix_zx.zxgraphstate.complete_graph_edges +.. autofunction:: graphqomb.zxgraphstate.to_zx_graphstate +.. autofunction:: graphqomb.zxgraphstate.complete_graph_edges From 5bc194d2e1cdc90378f3501be958afb752973939 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 28 Nov 2025 02:59:28 +0900 Subject: [PATCH 64/94] :art: Fix docs --- docs/source/zxgraphstate.rst | 1 + graphqomb/gflow_utils.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/source/zxgraphstate.rst b/docs/source/zxgraphstate.rst index 37340cef0..6961dc11a 100644 --- a/docs/source/zxgraphstate.rst +++ b/docs/source/zxgraphstate.rst @@ -8,6 +8,7 @@ ZXGraphState ZX Graph State -------------- + .. autoclass:: graphqomb.zxgraphstate.ZXGraphState :members: :show-inheritance: diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py index eaa893cb9..9a4b57945 100644 --- a/graphqomb/gflow_utils.py +++ b/graphqomb/gflow_utils.py @@ -2,7 +2,7 @@ This module provides: -- `gflow_wrapper`: Thin adapter around `swiflow.gflow` so that gflow can be computed directly +- `gflow_wrapper`: Thin adapter around ``swiflow.gflow`` so that gflow can be computed directly from a `BaseGraphState` instance. - `_EQUIV_MEAS_BASIS_MAP`: A mapping between equivalent measurement bases used to improve gflow finding performance. """ @@ -29,7 +29,7 @@ def gflow_wrapper(graphstate: BaseGraphState) -> FlowLike: - """Utilize `swiflow.gflow` to search gflow. + """Utilize ``swiflow.gflow`` to search gflow. Parameters ---------- @@ -38,7 +38,7 @@ def gflow_wrapper(graphstate: BaseGraphState) -> FlowLike: Returns ------- - `FlowLike` + ``FlowLike`` gflow object Raises @@ -76,6 +76,15 @@ def gflow_wrapper(graphstate: BaseGraphState) -> FlowLike: return {node: {child for child in children if child != node} for node, children in gflow_obj.items()} +#: Mapping between equivalent measurement bases. +#: +#: This map is used to replace a measurement basis by an equivalent one +#: to improve gflow search performance. +#: +#: Key: +#: ``(Plane, angle)`` where angle is in radians. +#: Value: +#: :class:`~graphqomb.common.PlannerMeasBasis`. _EQUIV_MEAS_BASIS_MAP: dict[tuple[Plane, float], PlannerMeasBasis] = { # (XY, 0) <-> (XZ, pi/2) (Plane.XY, 0.0): PlannerMeasBasis(Plane.XZ, 0.5 * math.pi), From 8a31d96fdf1c46d9828acc853d45225db92df878 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 28 Nov 2025 03:09:47 +0900 Subject: [PATCH 65/94] :bug: Fix docs --- examples/zxgraph_simplification.py | 2 ++ graphqomb/gflow_utils.py | 2 +- graphqomb/zxgraphstate.py | 17 +++-------------- tests/test_zxgraphstate.py | 2 ++ 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py index 95294d22a..37382b901 100644 --- a/examples/zxgraph_simplification.py +++ b/examples/zxgraph_simplification.py @@ -107,6 +107,8 @@ def print_boundary_lcs(zxgraph: ZXGraphState) -> None: # Note that we need to call the `expand_local_cliffords` method before generating the pattern to get the gflow. zx_graph_smp.expand_local_cliffords() +zx_graph_smp.to_xy() +zx_graph_smp.to_xz() print("input_node_indices: ", set(zx_graph_smp.input_node_indices)) print("output_node_indices: ", set(zx_graph_smp.output_node_indices)) print("local_cliffords: ", zx_graph_smp.local_cliffords) diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py index 9a4b57945..2dc82f256 100644 --- a/graphqomb/gflow_utils.py +++ b/graphqomb/gflow_utils.py @@ -3,7 +3,7 @@ This module provides: - `gflow_wrapper`: Thin adapter around ``swiflow.gflow`` so that gflow can be computed directly -from a `BaseGraphState` instance. + from a `BaseGraphState` instance. - `_EQUIV_MEAS_BASIS_MAP`: A mapping between equivalent measurement bases used to improve gflow finding performance. """ diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index f61440bc3..cc2267227 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -249,9 +249,11 @@ def pivot(self, node1: int, node2: int) -> None: Notes ----- Here we adopt the definition (lemma) of pivot from [1]. - In some literature, pivot is defined as below: + In some literature, pivot is defined as below:: + Rz(pi/2) Rx(-pi/2) Rz(pi/2) on the target nodes, Rz(pi) on all the neighbors of both target nodes (not including the target nodes). + This definition is strictly equivalent to the one adopted here. References @@ -611,19 +613,6 @@ def to_xz(self) -> None: elif basis.plane == Plane.YZ and is_close_angle(basis.angle, np.pi): self.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, np.pi)) - def expand_local_cliffords(self) -> ExpansionMaps: - r"""Expand local Clifford operators applied on the input and output nodes. - - Returns - ------- - `ExpansionMaps` - A tuple of dictionaries mapping input and output node indices to the new node indices created. - """ - expansion_map = super().expand_local_cliffords() - self.to_xy() - self.to_xz() - return expansion_map - def _extract_yz_adjacent_pair(self) -> tuple[int, int] | None: r"""Call inside convert_to_phase_gadget. diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 5a73df0d0..c6d6aa8d5 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -526,6 +526,8 @@ def test_local_clifford_expansion() -> None: lc = LocalClifford(2 * np.pi, 0.0, 0.0) zx_graph_cp.apply_local_clifford(2, lc) zx_graph_cp.expand_local_cliffords() + zx_graph_cp.to_xy() + zx_graph_cp.to_xz() gflow_cp = gflow_wrapper(zx_graph_cp) pattern_cp = qompile(zx_graph_cp, gflow_cp) sim_cp = PatternSimulator(pattern_cp, backend=SimulatorBackend.StateVector) From 69ffb37e890ab2eae8a493204aadecb61e2a562e Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 28 Nov 2025 03:31:54 +0900 Subject: [PATCH 66/94] :memo: Improve docs --- examples/zxgraph_simplification.py | 83 ++++++++++++++---------------- graphqomb/gflow_utils.py | 4 ++ graphqomb/zxgraphstate.py | 2 +- 3 files changed, 43 insertions(+), 46 deletions(-) diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py index 37382b901..464cb2e05 100644 --- a/examples/zxgraph_simplification.py +++ b/examples/zxgraph_simplification.py @@ -28,11 +28,18 @@ FlowLike = dict[int, set[int]] # %% -# Create a random graph state with flow +# Prepare an initial random graph state with flow graph, flow = generate_random_flow_graph(width=3, depth=4, edge_p=0.5) zx_graph, _ = to_zx_graphstate(graph) visualize(zx_graph) +# %% +# We can compile the graph state into a measurement pattern, simulate it, and get the resulting statevector. +pattern = qompile(zx_graph, flow) +sim = PatternSimulator(pattern, backend=SimulatorBackend.StateVector) +sim.simulate() +statevec_original = sim.state + # %% def print_boundary_lcs(zxgraph: ZXGraphState) -> None: @@ -49,88 +56,74 @@ def print_boundary_lcs(zxgraph: ZXGraphState) -> None: print(f"Node {node} has no local Clifford.") +def print_meas_bses(graph: ZXGraphState) -> None: + print("node | plane | angle (/pi)") + for node in graph.input_node_indices: + print(f"{node} (input)", graph.meas_bases[node].plane, graph.meas_bases[node].angle / np.pi) + for node in graph.physical_nodes - set(graph.input_node_indices) - set(graph.output_node_indices): + print(node, graph.meas_bases[node].plane, graph.meas_bases[node].angle / np.pi) + for node in graph.output_node_indices: + print(f"{node} (output)", "-", "-") + + # %% print_boundary_lcs(zx_graph) # %% # Initial graph state before simplification -print("node | plane | angle (/pi)") -for node in zx_graph.input_node_indices: - 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 - set(zx_graph.input_node_indices) - set(zx_graph.output_node_indices): - print(node, zx_graph.meas_bases[node].plane, zx_graph.meas_bases[node].angle / np.pi) -for node in zx_graph.output_node_indices: - print(f"{node} (output)", "-", "-") +print_meas_bses(zx_graph) # %% # Simplify the graph state by full_reduce method zx_graph_smp = deepcopy(zx_graph) -# zx_graph_smp.full_reduce() -zx_graph_smp.remove_cliffords() +zx_graph_smp.full_reduce() # %% # Simplified graph state after full_reduce. visualize(zx_graph_smp) -print("node | plane | angle (/pi)") -for node in zx_graph_smp.input_node_indices: - print(f"{node} (input)", zx_graph_smp.meas_bases[node].plane, zx_graph_smp.meas_bases[node].angle / np.pi) -for node in zx_graph_smp.physical_nodes - set(zx_graph_smp.input_node_indices) - set(zx_graph_smp.output_node_indices): - print(node, zx_graph_smp.meas_bases[node].plane, zx_graph_smp.meas_bases[node].angle / np.pi) -for node in zx_graph_smp.output_node_indices: - print(f"{node} (output)", "-", "-") - -# %% -print(zx_graph_smp.input_node_indices, "\n", zx_graph_smp.output_node_indices, "\n", zx_graph_smp.physical_edges) +print_meas_bses(zx_graph_smp) print_boundary_lcs(zx_graph_smp) # %% -# Supplementary Note: +# NOTE: # At first glance, the input/output nodes appear to remain unaffected. # However, note that a local Clifford operation is actually applied as a result of the action of the full_reduce method. # If you visualize the graph state after executing the `expand_local_cliffords` method, -# you will see additional nodes connected to the former input/output nodes, -# indicating that local Clifford operations on the input/output nodes have been expanded into the graph. +# you will see additional nodes connected to the former input/output nodes. # %% # Let us compare the graph state before and after simplification. -# First, we simulate the original graph state and get the resulting statevector. -pattern = qompile(zx_graph, flow) -sim = PatternSimulator(pattern, backend=SimulatorBackend.StateVector) -sim.simulate() -statevec_original = sim.state - -# %% -# Next, we simulate the pattern obtained from the simplified graph state. +# We simulate the pattern obtained from the simplified graph state. # Note that we need to call the `expand_local_cliffords` method before generating the pattern to get the gflow. zx_graph_smp.expand_local_cliffords() -zx_graph_smp.to_xy() -zx_graph_smp.to_xz() +zx_graph_smp.to_xy() # to improve gflow search performance +zx_graph_smp.to_xz() # to improve gflow search performance print("input_node_indices: ", set(zx_graph_smp.input_node_indices)) print("output_node_indices: ", set(zx_graph_smp.output_node_indices)) print("local_cliffords: ", zx_graph_smp.local_cliffords) -print("node | plane | angle (/pi)") -for node in zx_graph_smp.input_node_indices: - print(f"{node} (input)", zx_graph_smp.meas_bases[node].plane, zx_graph_smp.meas_bases[node].angle / np.pi) - -for node in zx_graph_smp.physical_nodes - set(zx_graph_smp.input_node_indices) - set(zx_graph_smp.output_node_indices): - print(node, zx_graph_smp.meas_bases[node].plane, zx_graph_smp.meas_bases[node].angle / np.pi) -for node in zx_graph_smp.output_node_indices: - print(f"{node} (output)", "-", "-") +print_meas_bses(zx_graph_smp) visualize(zx_graph_smp) print_boundary_lcs(zx_graph_smp) -gflow_smp = gflow_wrapper(zx_graph_smp) # %% -# Now we can compile the simplified graph state into a measurement pattern and simulate it. +# Now we can obtain the gflow for the simplified graph state. +# Then, we compile the simplified graph state into a measurement pattern, +# simulate it, and get the resulting statevector. + +# NOTE: +# gflow_wrapper does not support graph states with multiple subgraph structures in the gflow search wrapper below. +# Hence, in case you fail, ensure that the simplified graph state consists of a single connected component. +# To calculate the graph states with multiple subgraph structures, +# you need to calculate gflow for each connected component separately. +gflow_smp = gflow_wrapper(zx_graph_smp) pattern_smp = qompile(zx_graph_smp, gflow_smp) sim_smp = PatternSimulator(pattern_smp, backend=SimulatorBackend.StateVector) sim_smp.simulate() -print(pattern_smp) # %% statevec_smp = sim_smp.state @@ -156,6 +149,6 @@ def print_boundary_lcs(zxgraph: ZXGraphState) -> None: print("data shape: ", statevec_original.state().shape, statevec_smp.state().shape) psi_org = statevec_original.state() psi_smp = statevec_smp.state() -print("inner product: ", np.sqrt(np.abs(np.vdot(psi_org, psi_smp)))) +print("inner product: ", np.abs(np.vdot(psi_org, psi_smp))) # %% diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py index 2dc82f256..8c8c89a39 100644 --- a/graphqomb/gflow_utils.py +++ b/graphqomb/gflow_utils.py @@ -45,6 +45,10 @@ def gflow_wrapper(graphstate: BaseGraphState) -> FlowLike: ------ ValueError If no gflow is found + + Notes + ----- + This wrapper does not support graph states with multiple subgraph structures. """ graph: NxGraph[Any] = nx.Graph() graph.add_nodes_from(graphstate.physical_nodes) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index cc2267227..88f0878fe 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -26,7 +26,7 @@ ) from graphqomb.euler import LocalClifford from graphqomb.gflow_utils import _EQUIV_MEAS_BASIS_MAP -from graphqomb.graphstate import BaseGraphState, ExpansionMaps, GraphState, bipartite_edges +from graphqomb.graphstate import BaseGraphState, GraphState, bipartite_edges if sys.version_info >= (3, 10): from typing import TypeAlias From a745331f7496b2e262229a5aee5ba4783e857ed0 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 5 Dec 2025 17:04:34 +0900 Subject: [PATCH 67/94] :recycle: Move local_cliffords stuff into ZXGraphState --- graphqomb/graphstate.py | 227 +++---------------------------- graphqomb/zxgraphstate.py | 278 ++++++++++++++++++++++++++++++++------ 2 files changed, 252 insertions(+), 253 deletions(-) diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index b3550fbe5..8b667af8f 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -4,8 +4,6 @@ - `BaseGraphState`: Abstract base class for Graph State. - `GraphState`: Minimal implementation of Graph State. -- `LocalCliffordExpansion`: Local Clifford expansion. -- `ExpansionMaps`: Expansion maps for local clifford operators. - `compose`: Function to compose two graph states sequentially. - `bipartite_edges`: Function to create a complete bipartite graph between two sets of nodes. - `odd_neighbors`: Function to get odd neighbors of a node. @@ -21,14 +19,14 @@ from abc import ABC from collections.abc import Hashable, Iterable, Mapping, Sequence from collections.abc import Set as AbstractSet -from typing import NamedTuple, TypeVar +from typing import TYPE_CHECKING, TypeVar -import numpy as np import typing_extensions -from graphqomb.common import MeasBasis, Plane, PlannerMeasBasis, basis2tuple, is_close_angle, round_clifford_angle -from graphqomb.euler import LocalClifford, update_lc_basis, update_lc_lc -from graphqomb.gflow_utils import _EQUIV_MEAS_BASIS_MAP +if TYPE_CHECKING: + from typing_extensions import Self + + from graphqomb.common import MeasBasis NodeT = TypeVar("NodeT", bound=Hashable) @@ -172,22 +170,20 @@ def check_canonical_form(self) -> None: class GraphState(BaseGraphState): """Minimal implementation of GraphState.""" - __input_node_indices: dict[int, int] - __output_node_indices: dict[int, int] + _input_node_indices: dict[int, int] + _output_node_indices: dict[int, int] __physical_nodes: set[int] __physical_edges: dict[int, set[int]] __meas_bases: dict[int, MeasBasis] - __local_cliffords: dict[int, LocalClifford] __node_counter: int def __init__(self) -> None: - self.__input_node_indices = {} - self.__output_node_indices = {} + self._input_node_indices = {} + self._output_node_indices = {} self.__physical_nodes = set() self.__physical_edges = {} self.__meas_bases = {} - self.__local_cliffords = {} self.__node_counter = 0 @@ -201,7 +197,7 @@ def input_node_indices(self) -> dict[int, int]: `dict`\[`int`, `int`\] qubit indices map of input nodes. """ - return self.__input_node_indices.copy() + return self._input_node_indices.copy() @property @typing_extensions.override @@ -213,7 +209,7 @@ def output_node_indices(self) -> dict[int, int]: `dict`\[`int`, `int`\] qubit indices map of output nodes. """ - return self.__output_node_indices.copy() + return self._output_node_indices.copy() @property @typing_extensions.override @@ -256,17 +252,6 @@ def meas_bases(self) -> dict[int, MeasBasis]: """ return self.__meas_bases.copy() - @property - def local_cliffords(self) -> dict[int, LocalClifford]: - r"""Return local clifford nodes. - - Returns - ------- - `dict`\[`int`, `LocalClifford`\] - local clifford nodes. - """ - return self.__local_cliffords.copy() - def _check_meas_basis(self) -> None: """Check if the measurement basis is set for all physical nodes except output nodes. @@ -360,9 +345,8 @@ def remove_physical_node(self, node: int) -> None: del self.__physical_edges[node] if node in self.output_node_indices: - del self.__output_node_indices[node] + del self._output_node_indices[node] self.__meas_bases.pop(node, None) - self.__local_cliffords.pop(node, None) def remove_physical_edge(self, node1: int, node2: int) -> None: """Remove a physical edge from the graph state. @@ -404,13 +388,13 @@ def register_input(self, node: int, q_index: int) -> None: If the node is already registered as an input node. """ self._ensure_node_exists(node) - if node in self.__input_node_indices: + if node in self._input_node_indices: msg = "The node is already registered as an input node." raise ValueError(msg) if q_index in self.input_node_indices.values(): msg = "The q_index already exists in input qubit indices" raise ValueError(msg) - self.__input_node_indices[node] = q_index + self._input_node_indices[node] = q_index @typing_extensions.override def register_output(self, node: int, q_index: int) -> None: @@ -431,13 +415,13 @@ def register_output(self, node: int, q_index: int) -> None: 3. If the q_index already exists in output qubit indices. """ self._ensure_node_exists(node) - if node in self.__output_node_indices: + if node in self._output_node_indices: msg = "The node is already registered as an output node." raise ValueError(msg) if q_index in self.output_node_indices.values(): msg = "The q_index already exists in output qubit indices" raise ValueError(msg) - self.__output_node_indices[node] = q_index + self._output_node_indices[node] = q_index @typing_extensions.override def assign_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: @@ -453,29 +437,6 @@ def assign_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: self._ensure_node_exists(node) self.__meas_bases[node] = meas_basis - def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: - """Apply a local clifford to the node. - - Parameters - ---------- - node : `int` - node index - lc : `LocalClifford` - local clifford operator - """ - self._ensure_node_exists(node) - if node in self.input_node_indices or node in self.output_node_indices: - original_lc = self._pop_local_clifford(node) - if original_lc is not None: - new_lc = update_lc_lc(lc, original_lc) - self.__local_cliffords[node] = new_lc - else: - self.__local_cliffords[node] = lc - else: - self._check_meas_basis() - new_meas_basis = update_lc_basis(lc, self.meas_bases[node]) - self.assign_meas_basis(node, new_meas_basis) - @typing_extensions.override def neighbors(self, node: int) -> set[int]: r"""Return the neighbors of the node. @@ -498,7 +459,7 @@ def check_canonical_form(self) -> None: r"""Check if the graph state is in canonical form. The definition of canonical form is: - 1. No Clifford operators applied. + 1. No Clifford operators applied (here, always true) 2. All non-output nodes have measurement basis Raises @@ -506,137 +467,11 @@ def check_canonical_form(self) -> None: ValueError If the graph state is not in canonical form. """ - if self.__local_cliffords: - msg = "Clifford operators are applied." - raise ValueError(msg) for node in self.physical_nodes - set(self.output_node_indices): if self.meas_bases.get(node) is None: msg = "All non-output nodes must have measurement basis." raise ValueError(msg) - def expand_local_cliffords(self) -> ExpansionMaps: - r"""Expand local Clifford operators applied on the input and output nodes. - - Returns - ------- - `ExpansionMaps` - A tuple of dictionaries mapping input and output node indices to the new node indices created. - """ - input_node_map = self._expand_input_local_cliffords() - output_node_map = self._expand_output_local_cliffords() - return ExpansionMaps(input_node_map, output_node_map) - - def _pop_local_clifford(self, node: int) -> LocalClifford | None: - """Pop local clifford of the node. - - Parameters - ---------- - node : `int` - node index to remove local clifford. - - Returns - ------- - `LocalClifford` | `None` - removed local clifford - """ - return self.__local_cliffords.pop(node, None) - - def _expand_input_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: - r"""Expand local Clifford operators applied on the input nodes. - - Returns - ------- - `dict`\[`int`, `LocalCliffordExpansion`\] - A dictionary mapping input node indices to the new node indices created. - """ - node_index_addition_map: dict[int, LocalCliffordExpansion] = {} - new_input_indices: dict[int, int] = {} - for old_input_node, q_index in self.input_node_indices.items(): - lc = self._pop_local_clifford(old_input_node) - if lc is None: - lc = LocalClifford(0.0, 0.0, 0.0) - - new_input = self.add_physical_node() - new_node = self.add_physical_node() - new_input_indices[new_input] = q_index - - self.add_physical_edge(new_input, new_node) - self.add_physical_edge(new_node, old_input_node) - - self.assign_meas_basis(new_input, PlannerMeasBasis(Plane.XY, 0.0)) - self.assign_meas_basis(new_node, PlannerMeasBasis(Plane.XY, 0.0)) - meas_basis = self.meas_bases[old_input_node] - new_meas_basis = update_lc_basis(lc, meas_basis) - self.assign_meas_basis(old_input_node, new_meas_basis) - self._assure_gflow_input_expansion(old_input_node) - node_index_addition_map[old_input_node] = LocalCliffordExpansion(new_input, new_node) - - self.__input_node_indices = {} - for new_input_index, q_index in new_input_indices.items(): - self.register_input(new_input_index, q_index) - - return node_index_addition_map - - def _assure_gflow_input_expansion(self, node: int) -> None: - r"""Assure gflow existence after input local Clifford expansion. - - Parameters - ---------- - node : `int` - node index - """ - cur = self.meas_bases[node] - rounded = round_clifford_angle(cur.angle) - self.assign_meas_basis(node, PlannerMeasBasis(cur.plane, rounded)) - - cur = self.meas_bases[node] - cur_key = basis2tuple(cur) - - # if the updated basis is self-inclusion type, push it to an XY-equivalent one. - if (cur.plane in {Plane.XZ, Plane.YZ} and is_close_angle(cur.angle, 0.0)) or ( - is_close_angle(cur.angle, np.pi) and cur_key in _EQUIV_MEAS_BASIS_MAP - ): - self.assign_meas_basis(node, _EQUIV_MEAS_BASIS_MAP[cur_key]) - - # ensure XY if possible. - cur = self.meas_bases[node] - if cur.plane != Plane.XY and cur_key in _EQUIV_MEAS_BASIS_MAP: - self.assign_meas_basis(node, _EQUIV_MEAS_BASIS_MAP[cur_key]) - - def _expand_output_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: - r"""Expand local Clifford operators applied on the output nodes. - - Returns - ------- - `dict`\[`int`, `LocalCliffordExpansion`\] - A dictionary mapping output node indices to the new node indices created. - """ - node_index_addition_map: dict[int, LocalCliffordExpansion] = {} - new_output_node_index_map: dict[int, int] = {} - for old_output_node, q_index in self.output_node_indices.items(): - lc = self._pop_local_clifford(old_output_node) - if lc is None: - new_output_node_index_map[old_output_node] = q_index - continue - - new_node = self.add_physical_node() - new_output_node = self.add_physical_node() - new_output_node_index_map[new_output_node] = q_index - - self.__output_node_indices.pop(old_output_node) - self.register_output(new_output_node, q_index) - - self.add_physical_edge(old_output_node, new_node) - self.add_physical_edge(new_node, new_output_node) - - self.assign_meas_basis(new_node, PlannerMeasBasis(Plane.XY, 0.0)) - meas_basis = update_lc_basis(lc, PlannerMeasBasis(Plane.XY, 0.0)) - self.assign_meas_basis(old_output_node, meas_basis) - - node_index_addition_map[old_output_node] = LocalCliffordExpansion(new_node, new_output_node) - - return node_index_addition_map - @classmethod def from_graph( # noqa: C901, PLR0912 cls, @@ -752,8 +587,7 @@ def from_graph( # noqa: C901, PLR0912 def from_base_graph_state( cls, base: BaseGraphState, - copy_local_cliffords: bool = True, - ) -> tuple[GraphState, dict[int, int]]: + ) -> tuple[Self, dict[int, int]]: r"""Create a new GraphState from an existing BaseGraphState instance. This method creates a complete copy of the graph structure, including nodes, @@ -764,11 +598,6 @@ def from_base_graph_state( ---------- base : `BaseGraphState` The source graph state to copy from. - copy_local_cliffords : `bool`, optional - Whether to copy local Clifford operators if the source is a GraphState. - If True and the source has local Cliffords, they are copied. - If False, local Cliffords are not copied (canonical form only). - Default is True. Returns ------- @@ -801,29 +630,9 @@ def from_base_graph_state( for node, meas_basis in base.meas_bases.items(): graph_state.assign_meas_basis(node_map[node], meas_basis) - # Copy local Clifford operators if requested and source is GraphState - if copy_local_cliffords and isinstance(base, GraphState): - for node, lc in base.local_cliffords.items(): - # Access private attribute to copy local cliffords - graph_state.apply_local_clifford(node_map[node], lc) - return graph_state, node_map -class LocalCliffordExpansion(NamedTuple): - """Local Clifford expansion map.""" - - node1: int - node2: int - - -class ExpansionMaps(NamedTuple): - """Expansion maps for inputs and outputs with Local Clifford.""" - - input_node_map: dict[int, LocalCliffordExpansion] - output_node_map: dict[int, LocalCliffordExpansion] - - def compose( # noqa: C901 graph1: BaseGraphState, graph2: BaseGraphState ) -> tuple[GraphState, dict[int, int], dict[int, int]]: diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 88f0878fe..ef05b2fc3 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -3,8 +3,9 @@ This module provides: - `ZXGraphState`: Graph State for the ZX-calculus. -- `to_zx_graphstate`: Convert input GraphState to ZXGraphState. - `complete_graph_edges`: Return a set of edges for the complete graph on the given nodes. +- `LocalCliffordExpansion`: Local Clifford expansion. +- `ExpansionMaps`: Expansion maps for local clifford operators. """ from __future__ import annotations @@ -12,9 +13,10 @@ import sys from collections import defaultdict from itertools import combinations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, NamedTuple import numpy as np +import typing_extensions from graphqomb.common import ( Plane, @@ -24,7 +26,7 @@ is_close_angle, round_clifford_angle, ) -from graphqomb.euler import LocalClifford +from graphqomb.euler import LocalClifford, update_lc_basis, update_lc_lc from graphqomb.gflow_utils import _EQUIV_MEAS_BASIS_MAP from graphqomb.graphstate import BaseGraphState, GraphState, bipartite_edges @@ -61,8 +63,127 @@ class ZXGraphState(GraphState): local clifford operators """ + __local_cliffords: dict[int, LocalClifford] + def __init__(self) -> None: super().__init__() + self.__local_cliffords = {} + + @property + def local_cliffords(self) -> dict[int, LocalClifford]: + r"""Return local clifford nodes. + + Returns + ------- + `dict`\[`int`, `LocalClifford`\] + local clifford nodes. + """ + return self.__local_cliffords.copy() + + def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: + """Apply a local clifford to the node. + + Parameters + ---------- + node : `int` + node index + lc : `LocalClifford` + local clifford operator + """ + self._ensure_node_exists(node) + if node in self.input_node_indices or node in self.output_node_indices: + original_lc = self._pop_local_clifford(node) + if original_lc is not None: + new_lc = update_lc_lc(lc, original_lc) + self.__local_cliffords[node] = new_lc + else: + self.__local_cliffords[node] = lc + else: + self._check_meas_basis() + new_meas_basis = update_lc_basis(lc, self.meas_bases[node]) + self.assign_meas_basis(node, new_meas_basis) + + def remove_physical_node(self, node: int) -> None: + """Remove a physical node from the zxgraph state. + + Parameters + ---------- + node : `int` + node index + """ + super().remove_physical_node(node) + self.__local_cliffords.pop(node, None) + + @typing_extensions.override + def check_canonical_form(self) -> None: + r"""Check if the graph state is in canonical form. + + The definition of canonical form is: + 1. No Clifford operators applied. + 2. All non-output nodes have measurement basis + + Raises + ------ + ValueError + If the graph state is not in canonical form. + """ + if self.__local_cliffords: + msg = "Clifford operators are applied." + raise ValueError(msg) + super().check_canonical_form() + + def _pop_local_clifford(self, node: int) -> LocalClifford | None: + """Pop local clifford of the node. + + Parameters + ---------- + node : `int` + node index to remove local clifford. + + Returns + ------- + `LocalClifford` | `None` + removed local clifford + """ + return self.__local_cliffords.pop(node, None) + + @classmethod + def from_base_graph_state( + cls, + base: BaseGraphState, + copy_local_cliffords: bool = True, + ) -> tuple[ZXGraphState, dict[int, int]]: + r"""Create a new ZXGraphState from an existing BaseGraphState instance. + + This method creates a complete copy of the graph structure, including nodes, + edges, input/output registrations, and measurement bases. Useful for creating + mutable copies or converting between ZXGraphState implementations. + + Parameters + ---------- + base : `BaseGraphState` + The source graph state to copy from. + copy_local_cliffords : `bool`, optional + Whether to copy local Clifford operators if the source is a ZXGraphState. + If True and the source has local Cliffords, they are copied. + If False, local Cliffords are not copied (canonical form only). + Default is True. + + Returns + ------- + `tuple`\[`ZXGraphState`, `dict`\[`int`, `int`\]\] + - Created ZXGraphState instance + - Mapping from source node indices to new node indices + """ + zxgraph_state, node_map = super().from_base_graph_state(base) + + # Copy local Clifford operators if requested and source is ZXGraphState + if copy_local_cliffords and isinstance(base, ZXGraphState): + for node, lc in base.local_cliffords.items(): + # Access private attribute to copy local cliffords + zxgraph_state.apply_local_clifford(node_map[node], lc) + + return zxgraph_state, node_map @property def _clifford_rules(self) -> tuple[CliffordRule, ...]: @@ -730,58 +851,113 @@ def full_reduce(self, atol: float = 1e-9) -> None: ): break + def expand_local_cliffords(self) -> ExpansionMaps: + r"""Expand local Clifford operators applied on the input and output nodes. -def to_zx_graphstate(graph: BaseGraphState) -> tuple[ZXGraphState, dict[int, int]]: - r"""Convert input graph to ZXGraphState. + Returns + ------- + `ExpansionMaps` + A tuple of dictionaries mapping input and output node indices to the new node indices created. + """ + input_node_map = self._expand_input_local_cliffords() + output_node_map = self._expand_output_local_cliffords() + return ExpansionMaps(input_node_map, output_node_map) - Parameters - ---------- - graph : `BaseGraphState` - The graph state to convert. + def _expand_input_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: + r"""Expand local Clifford operators applied on the input nodes. - Returns - ------- - `tuple`\[`ZXGraphState`, `dict`\[`int`, `int`\]\] - Converted ZXGraphState and node map for old node index to new node index. + Returns + ------- + `dict`\[`int`, `LocalCliffordExpansion`\] + A dictionary mapping input node indices to the new node indices created. + """ + node_index_addition_map: dict[int, LocalCliffordExpansion] = {} + new_input_indices: dict[int, int] = {} + for old_input_node, q_index in self.input_node_indices.items(): + lc = self._pop_local_clifford(old_input_node) + if lc is None: + lc = LocalClifford(0.0, 0.0, 0.0) - Raises - ------ - TypeError - If the input graph is not an instance of GraphState. - """ - graph.check_canonical_form() - if not isinstance(graph, GraphState): - msg = "The input graph must be an instance of GraphState." - raise TypeError(msg) + new_input = self.add_physical_node() + new_node = self.add_physical_node() + new_input_indices[new_input] = q_index + + self.add_physical_edge(new_input, new_node) + self.add_physical_edge(new_node, old_input_node) + + self.assign_meas_basis(new_input, PlannerMeasBasis(Plane.XY, 0.0)) + self.assign_meas_basis(new_node, PlannerMeasBasis(Plane.XY, 0.0)) + meas_basis = self.meas_bases[old_input_node] + new_meas_basis = update_lc_basis(lc, meas_basis) + self.assign_meas_basis(old_input_node, new_meas_basis) + self._assure_gflow_input_expansion(old_input_node) + node_index_addition_map[old_input_node] = LocalCliffordExpansion(new_input, new_node) + + self._input_node_indices = {} + for new_input_index, q_index in new_input_indices.items(): + self.register_input(new_input_index, q_index) + + return node_index_addition_map + + def _assure_gflow_input_expansion(self, node: int) -> None: + r"""Assure gflow existence after input local Clifford expansion. + + Parameters + ---------- + node : `int` + node index + """ + cur = self.meas_bases[node] + rounded = round_clifford_angle(cur.angle) + self.assign_meas_basis(node, PlannerMeasBasis(cur.plane, rounded)) + + cur = self.meas_bases[node] + cur_key = basis2tuple(cur) + + # if the updated basis is self-inclusion type, push it to an XY-equivalent one. + if (cur.plane in {Plane.XZ, Plane.YZ} and is_close_angle(cur.angle, 0.0)) or ( + is_close_angle(cur.angle, np.pi) and cur_key in _EQUIV_MEAS_BASIS_MAP + ): + self.assign_meas_basis(node, _EQUIV_MEAS_BASIS_MAP[cur_key]) + + # ensure XY if possible. + cur = self.meas_bases[node] + if cur.plane != Plane.XY and cur_key in _EQUIV_MEAS_BASIS_MAP: + self.assign_meas_basis(node, _EQUIV_MEAS_BASIS_MAP[cur_key]) + + def _expand_output_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: + r"""Expand local Clifford operators applied on the output nodes. - node_map: dict[int, int] = {} - zx_graph = ZXGraphState() + Returns + ------- + `dict`\[`int`, `LocalCliffordExpansion`\] + A dictionary mapping output node indices to the new node indices created. + """ + node_index_addition_map: dict[int, LocalCliffordExpansion] = {} + new_output_node_index_map: dict[int, int] = {} + for old_output_node, q_index in self.output_node_indices.items(): + lc = self._pop_local_clifford(old_output_node) + if lc is None: + new_output_node_index_map[old_output_node] = q_index + continue - # Copy all physical nodes and measurement bases - for node in graph.physical_nodes: - node_index = zx_graph.add_physical_node() - node_map[node] = node_index - meas_basis = graph.meas_bases.get(node, None) - if meas_basis is not None: - zx_graph.assign_meas_basis(node_index, meas_basis) + new_node = self.add_physical_node() + new_output_node = self.add_physical_node() + new_output_node_index_map[new_output_node] = q_index - # Register input nodes - for node, q_index in graph.input_node_indices.items(): - zx_graph.register_input(node_map[node], q_index) + self._output_node_indices.pop(old_output_node) + self.register_output(new_output_node, q_index) - # Register output nodes - for node, q_index in graph.output_node_indices.items(): - zx_graph.register_output(node_map[node], q_index) + self.add_physical_edge(old_output_node, new_node) + self.add_physical_edge(new_node, new_output_node) - # Copy all physical edges - for u, v in graph.physical_edges: - zx_graph.add_physical_edge(node_map[u], node_map[v]) + self.assign_meas_basis(new_node, PlannerMeasBasis(Plane.XY, 0.0)) + meas_basis = update_lc_basis(lc, PlannerMeasBasis(Plane.XY, 0.0)) + self.assign_meas_basis(old_output_node, meas_basis) - # Copy local Clifford operators - for node, lc in graph.local_cliffords.items(): - zx_graph.apply_local_clifford(node_map[node], lc) + node_index_addition_map[old_output_node] = LocalCliffordExpansion(new_node, new_output_node) - return zx_graph, node_map + return node_index_addition_map def complete_graph_edges(nodes: Iterable[int]) -> set[tuple[int, int]]: @@ -798,3 +974,17 @@ def complete_graph_edges(nodes: Iterable[int]) -> set[tuple[int, int]]: edges of the complete graph """ return {(min(u, v), max(u, v)) for u, v in combinations(nodes, 2)} + + +class LocalCliffordExpansion(NamedTuple): + """Local Clifford expansion map.""" + + node1: int + node2: int + + +class ExpansionMaps(NamedTuple): + """Expansion maps for inputs and outputs with Local Clifford.""" + + input_node_map: dict[int, LocalCliffordExpansion] + output_node_map: dict[int, LocalCliffordExpansion] From c62441928cd97f33f393d33a9f1208671f811ad2 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 5 Dec 2025 17:05:41 +0900 Subject: [PATCH 68/94] :recycle: Move local_cliffords related tests into test_zxgraphstate --- tests/test_graphstate.py | 138 +-------------------------------- tests/test_zxgraphstate.py | 155 +++++++++++++++++++++++++++++++++++-- 2 files changed, 151 insertions(+), 142 deletions(-) diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index 565431889..a8430833a 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -5,8 +5,7 @@ import numpy as np import pytest -from graphqomb.common import Plane, PlannerMeasBasis, is_close_angle -from graphqomb.euler import LocalClifford, update_lc_basis +from graphqomb.common import Plane, PlannerMeasBasis from graphqomb.graphstate import GraphState, bipartite_edges, odd_neighbors @@ -193,123 +192,6 @@ def test_assign_meas_basis(graph: GraphState) -> None: assert graph.meas_bases[node_index].angle == 0.5 * np.pi -@pytest.mark.parametrize( - "gamma", - [0, np.pi / 2, np.pi, 3 * np.pi / 2], -) -def test_expand_input_local_cliffords_xy_plane(graph: GraphState, gamma: float) -> None: - """Test expanding local Clifford operators on an input node with XY measurement plane.""" - old_input_node = graph.add_physical_node() - output_node = graph.add_physical_node() - graph.add_physical_edge(old_input_node, output_node) - graph.register_input(old_input_node, 0) - graph.register_output(output_node, 0) - old_input_angle = np.pi / 3 - graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.XY, old_input_angle)) - - alpha = 0.0 - beta = 0.0 - lc = LocalClifford(alpha=alpha, beta=beta, gamma=gamma) - graph.apply_local_clifford(old_input_node, lc) - graph.expand_local_cliffords() - - new_input_node = 2 - assert graph.input_node_indices == {new_input_node: 0} - for node in graph.physical_nodes - set(graph.output_node_indices): - assert graph.meas_bases[node].plane == Plane.XY - assert is_close_angle(graph.meas_bases[new_input_node].angle, 0.0) - assert is_close_angle(graph.meas_bases[new_input_node + 1].angle, 0.0) - assert is_close_angle(graph.meas_bases[old_input_node].angle, old_input_angle - gamma) - - -@pytest.mark.parametrize( - "gamma", - [0, np.pi, np.pi / 2, 3 * np.pi / 2], -) -def test_expand_input_local_cliffords_yz_plane(graph: GraphState, gamma: float) -> None: - """Test expanding local Clifford operators on an input node with YZ measurement plane.""" - old_input_node = graph.add_physical_node() - output_node = graph.add_physical_node() - graph.add_physical_edge(old_input_node, output_node) - graph.register_input(old_input_node, 0) - graph.register_output(output_node, 0) - old_input_angle = np.pi / 3 - graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.YZ, old_input_angle)) - - alpha = 0.0 - beta = 0.0 - lc = LocalClifford(alpha=alpha, beta=beta, gamma=gamma) - graph.apply_local_clifford(old_input_node, lc) - graph.expand_local_cliffords() - - exp_angle: float = old_input_angle - if is_close_angle(2 * gamma, 0): - assert graph.meas_bases[old_input_node].plane == Plane.YZ - exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle - elif is_close_angle(2 * (gamma - np.pi / 2), 0): - assert graph.meas_bases[old_input_node].plane == Plane.XZ - exp_angle = old_input_angle if is_close_angle(gamma - np.pi / 2, 0) else -old_input_angle - assert is_close_angle(graph.meas_bases[old_input_node].angle, exp_angle) - new_input_node = 2 - assert graph.meas_bases[new_input_node].plane == Plane.XY - assert is_close_angle(graph.meas_bases[new_input_node].angle, 0.0) - - -@pytest.mark.parametrize( - "gamma", - [0, np.pi / 2, np.pi, 3 * np.pi / 2], -) -def test_expand_input_local_cliffords_xz_plane(graph: GraphState, gamma: float) -> None: - """Test expanding local Clifford operators on an input node with XZ measurement plane.""" - old_input_node = graph.add_physical_node() - output_node = graph.add_physical_node() - graph.add_physical_edge(old_input_node, output_node) - graph.register_input(old_input_node, 0) - graph.register_output(output_node, 0) - old_input_angle = np.pi / 3 - graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.XZ, old_input_angle)) - - alpha = 0.0 - beta = 0.0 - lc = LocalClifford(alpha=alpha, beta=beta, gamma=gamma) - graph.apply_local_clifford(old_input_node, lc) - graph.expand_local_cliffords() - - exp_angle: float = old_input_angle - if is_close_angle(2 * gamma, 0): - assert graph.meas_bases[old_input_node].plane == Plane.XZ - exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle - elif is_close_angle(2 * (gamma - np.pi / 2), 0): - assert graph.meas_bases[old_input_node].plane == Plane.YZ - exp_angle = old_input_angle if is_close_angle(gamma + np.pi / 2, 0) else -old_input_angle - assert is_close_angle(graph.meas_bases[old_input_node].angle, exp_angle) - new_input_node = 2 - assert graph.meas_bases[new_input_node].plane == Plane.XY - assert is_close_angle(graph.meas_bases[new_input_node].angle, 0.0) - - -def test_expand_output_local_cliffords(graph: GraphState) -> None: - """Test expanding local Clifford operators on an output node.""" - input_node = graph.add_physical_node() - old_output_node = graph.add_physical_node() - graph.add_physical_edge(input_node, old_output_node) - graph.register_input(input_node, 0) - graph.register_output(old_output_node, 0) - graph.assign_meas_basis(input_node, PlannerMeasBasis(Plane.XY, 0.0)) - - lc = LocalClifford(alpha=np.pi / 2, beta=np.pi, gamma=3 * np.pi / 2) - graph.apply_local_clifford(old_output_node, lc) - graph.expand_local_cliffords() - - new_output_node = 5 - assert graph.output_node_indices == {new_output_node: 0} - assert graph.meas_bases[old_output_node + 1].plane == Plane.XY - assert is_close_angle(graph.meas_bases[old_output_node + 1].angle, 0.0) - meas_basis = update_lc_basis(lc, PlannerMeasBasis(Plane.XY, 0.0)) - assert graph.meas_bases[old_output_node].plane == meas_basis.plane - assert is_close_angle(graph.meas_bases[old_output_node].angle, meas_basis.angle) - - def test_check_canonical_form_true(canonical_graph: GraphState) -> None: """Test if the graph is in canonical form.""" canonical_graph.check_canonical_form() @@ -324,24 +206,6 @@ def test_check_canonical_form_input_output_mismatch(canonical_graph: GraphState) canonical_graph.check_canonical_form() -def test_check_canonical_form_with_local_clifford_false(canonical_graph: GraphState) -> None: - """Test if the graph is in canonical form with local Clifford operator.""" - local_clifford = LocalClifford() - in_node = next(iter(canonical_graph.input_node_indices)) - canonical_graph.apply_local_clifford(in_node, local_clifford) - with pytest.raises(ValueError, match="Clifford operators are applied"): - canonical_graph.check_canonical_form() - - -def test_check_canonical_form_with_local_clifford_expansion_true(canonical_graph: GraphState) -> None: - """Test if the graph is in canonical form with local Clifford operator expansion.""" - local_clifford = LocalClifford() - in_node = next(iter(canonical_graph.input_node_indices)) - canonical_graph.apply_local_clifford(in_node, local_clifford) - canonical_graph.expand_local_cliffords() - canonical_graph.check_canonical_form() # Should not raise an exception - - def test_check_canonical_form_missing_meas_basis_false(canonical_graph: GraphState) -> None: """Test if the graph is in canonical form with missing measurement basis.""" _ = canonical_graph.add_physical_node() diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index c6d6aa8d5..e30199fb1 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -19,14 +19,13 @@ import numpy as np import pytest - from graphqomb.common import Plane, PlannerMeasBasis, is_close_angle -from graphqomb.euler import LocalClifford +from graphqomb.euler import LocalClifford, update_lc_basis from graphqomb.gflow_utils import gflow_wrapper from graphqomb.qompiler import qompile from graphqomb.random_objects import generate_random_flow_graph from graphqomb.simulator import PatternSimulator, SimulatorBackend -from graphqomb.zxgraphstate import ZXGraphState, to_zx_graphstate +from graphqomb.zxgraphstate import ZXGraphState if TYPE_CHECKING: from collections.abc import Sequence @@ -92,6 +91,19 @@ def rng() -> np.random.Generator: return np.random.default_rng() +@pytest.fixture +def canonical_zxgraph() -> ZXGraphState: + graph = ZXGraphState() + in_node = graph.add_physical_node() + out_node = graph.add_physical_node() + + q_idx = 0 + graph.register_input(in_node, q_idx) + graph.register_output(out_node, q_idx) + graph.assign_meas_basis(in_node, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) + return graph + + @pytest.fixture def zx_graph() -> ZXGraphState: """Generate an empty ZXGraphState object. @@ -172,6 +184,24 @@ def _test( assert is_close_angle(zx_graph.meas_bases[node_id].angle, planner_meas_basis.angle) +def test_check_canonical_form_with_local_clifford_false(canonical_zxgraph: ZXGraphState) -> None: + """Test if the graph is in canonical form with local Clifford operator.""" + local_clifford = LocalClifford() + in_node = next(iter(canonical_zxgraph.input_node_indices)) + canonical_zxgraph.apply_local_clifford(in_node, local_clifford) + with pytest.raises(ValueError, match="Clifford operators are applied"): + canonical_zxgraph.check_canonical_form() + + +def test_check_canonical_form_with_local_clifford_expansion_true(canonical_zxgraph: ZXGraphState) -> None: + """Test if the graph is in canonical form with local Clifford operator expansion.""" + local_clifford = LocalClifford() + in_node = next(iter(canonical_zxgraph.input_node_indices)) + canonical_zxgraph.apply_local_clifford(in_node, local_clifford) + canonical_zxgraph.expand_local_cliffords() + canonical_zxgraph.check_canonical_form() # Should not raise an exception + + def test_apply_local_clifford_to_planner_meas_basis_xy_1(zx_graph: ZXGraphState, rng: np.random.Generator) -> None: """Test apply_local_clifford correctly updates the PlannerMeasBasis(XY, angle).""" node = zx_graph.add_physical_node() @@ -511,9 +541,124 @@ def test_local_complement_4_times( _test(zx_graph, exp_nodes={0, 1, 2}, exp_edges={(0, 1), (1, 2)}, exp_measurements=exp_measurements) +@pytest.mark.parametrize( + "gamma", + [0, np.pi / 2, np.pi, 3 * np.pi / 2], +) +def test_expand_input_local_cliffords_xy_plane(zx_graph: ZXGraphState, gamma: float) -> None: + """Test expanding local Clifford operators on an input node with XY measurement plane.""" + old_input_node = zx_graph.add_physical_node() + output_node = zx_graph.add_physical_node() + zx_graph.add_physical_edge(old_input_node, output_node) + zx_graph.register_input(old_input_node, 0) + zx_graph.register_output(output_node, 0) + old_input_angle = np.pi / 3 + zx_graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.XY, old_input_angle)) + + alpha = 0.0 + beta = 0.0 + lc = LocalClifford(alpha=alpha, beta=beta, gamma=gamma) + zx_graph.apply_local_clifford(old_input_node, lc) + zx_graph.expand_local_cliffords() + + new_input_node = 2 + assert zx_graph.input_node_indices == {new_input_node: 0} + for node in zx_graph.physical_nodes - set(zx_graph.output_node_indices): + assert zx_graph.meas_bases[node].plane == Plane.XY + assert is_close_angle(zx_graph.meas_bases[new_input_node].angle, 0.0) + assert is_close_angle(zx_graph.meas_bases[new_input_node + 1].angle, 0.0) + assert is_close_angle(zx_graph.meas_bases[old_input_node].angle, old_input_angle - gamma) + + +@pytest.mark.parametrize( + "gamma", + [0, np.pi, np.pi / 2, 3 * np.pi / 2], +) +def test_expand_input_local_cliffords_yz_plane(zx_graph: ZXGraphState, gamma: float) -> None: + """Test expanding local Clifford operators on an input node with YZ measurement plane.""" + old_input_node = zx_graph.add_physical_node() + output_node = zx_graph.add_physical_node() + zx_graph.add_physical_edge(old_input_node, output_node) + zx_graph.register_input(old_input_node, 0) + zx_graph.register_output(output_node, 0) + old_input_angle = np.pi / 3 + zx_graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.YZ, old_input_angle)) + + alpha = 0.0 + beta = 0.0 + lc = LocalClifford(alpha=alpha, beta=beta, gamma=gamma) + zx_graph.apply_local_clifford(old_input_node, lc) + zx_graph.expand_local_cliffords() + + exp_angle: float = old_input_angle + if is_close_angle(2 * gamma, 0): + assert zx_graph.meas_bases[old_input_node].plane == Plane.YZ + exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle + elif is_close_angle(2 * (gamma - np.pi / 2), 0): + assert zx_graph.meas_bases[old_input_node].plane == Plane.XZ + exp_angle = old_input_angle if is_close_angle(gamma - np.pi / 2, 0) else -old_input_angle + assert is_close_angle(zx_graph.meas_bases[old_input_node].angle, exp_angle) + new_input_node = 2 + assert zx_graph.meas_bases[new_input_node].plane == Plane.XY + assert is_close_angle(zx_graph.meas_bases[new_input_node].angle, 0.0) + + +@pytest.mark.parametrize( + "gamma", + [0, np.pi / 2, np.pi, 3 * np.pi / 2], +) +def test_expand_input_local_cliffords_xz_plane(zx_graph: ZXGraphState, gamma: float) -> None: + """Test expanding local Clifford operators on an input node with XZ measurement plane.""" + old_input_node = zx_graph.add_physical_node() + output_node = zx_graph.add_physical_node() + zx_graph.add_physical_edge(old_input_node, output_node) + zx_graph.register_input(old_input_node, 0) + zx_graph.register_output(output_node, 0) + old_input_angle = np.pi / 3 + zx_graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.XZ, old_input_angle)) + + alpha = 0.0 + beta = 0.0 + lc = LocalClifford(alpha=alpha, beta=beta, gamma=gamma) + zx_graph.apply_local_clifford(old_input_node, lc) + zx_graph.expand_local_cliffords() + + exp_angle: float = old_input_angle + if is_close_angle(2 * gamma, 0): + assert zx_graph.meas_bases[old_input_node].plane == Plane.XZ + exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle + elif is_close_angle(2 * (gamma - np.pi / 2), 0): + assert zx_graph.meas_bases[old_input_node].plane == Plane.YZ + exp_angle = old_input_angle if is_close_angle(gamma + np.pi / 2, 0) else -old_input_angle + assert is_close_angle(zx_graph.meas_bases[old_input_node].angle, exp_angle) + new_input_node = 2 + assert zx_graph.meas_bases[new_input_node].plane == Plane.XY + assert is_close_angle(zx_graph.meas_bases[new_input_node].angle, 0.0) + + +def test_expand_output_local_cliffords(zx_graph: ZXGraphState) -> None: + """Test expanding local Clifford operators on an output node.""" + input_node = zx_graph.add_physical_node() + old_output_node = zx_graph.add_physical_node() + zx_graph.add_physical_edge(input_node, old_output_node) + zx_graph.register_input(input_node, 0) + zx_graph.register_output(old_output_node, 0) + zx_graph.assign_meas_basis(input_node, PlannerMeasBasis(Plane.XY, 0.0)) + lc = LocalClifford(alpha=np.pi / 2, beta=np.pi, gamma=3 * np.pi / 2) + zx_graph.apply_local_clifford(old_output_node, lc) + zx_graph.expand_local_cliffords() + new_output_node = 5 + assert zx_graph.output_node_indices == {new_output_node: 0} + assert zx_graph.meas_bases[old_output_node + 1].plane == Plane.XY + assert is_close_angle(zx_graph.meas_bases[old_output_node + 1].angle, 0.0) + meas_basis = update_lc_basis(lc, PlannerMeasBasis(Plane.XY, 0.0)) + assert zx_graph.meas_bases[old_output_node].plane == meas_basis.plane + assert is_close_angle(zx_graph.meas_bases[old_output_node].angle, meas_basis.angle) + + def test_local_clifford_expansion() -> None: graph, flow = generate_random_flow_graph(width=1, depth=3, edge_p=0.5) - zx_graph, _ = to_zx_graphstate(graph) + zx_graph, _ = ZXGraphState.from_base_graph_state(graph) pattern = qompile(zx_graph, flow) sim = PatternSimulator(pattern, backend=SimulatorBackend.StateVector) @@ -857,7 +1002,7 @@ def test_remove_cliffords_graph3(zx_graph: ZXGraphState) -> None: def test_random_graph(zx_graph: ZXGraphState) -> None: """Test removing multiple Clifford nodes from a random graph.""" random_graph, _ = generate_random_flow_graph(5, 5) - zx_graph, _ = to_zx_graphstate(random_graph) + zx_graph, _ = ZXGraphState.from_base_graph_state(random_graph) for i in zx_graph.physical_nodes - set(zx_graph.output_node_indices): rng = np.random.default_rng(seed=0) From 476fc1050060a8c3dc754d354af84ca4a6e90b42 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 5 Dec 2025 17:06:29 +0900 Subject: [PATCH 69/94] :art: Fix initialization --- examples/zxgraph_simplification.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py index 464cb2e05..e67410cd6 100644 --- a/examples/zxgraph_simplification.py +++ b/examples/zxgraph_simplification.py @@ -23,14 +23,14 @@ from graphqomb.random_objects import generate_random_flow_graph from graphqomb.simulator import PatternSimulator, SimulatorBackend from graphqomb.visualizer import visualize -from graphqomb.zxgraphstate import ZXGraphState, to_zx_graphstate +from graphqomb.zxgraphstate import ZXGraphState FlowLike = dict[int, set[int]] # %% # Prepare an initial random graph state with flow graph, flow = generate_random_flow_graph(width=3, depth=4, edge_p=0.5) -zx_graph, _ = to_zx_graphstate(graph) +zx_graph, _ = ZXGraphState.from_base_graph_state(graph) visualize(zx_graph) # %% From 50a68f9a704fa89f190de7a7d16f87bc6921ee9b Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 5 Dec 2025 17:08:01 +0900 Subject: [PATCH 70/94] :art: ruff --- tests/test_zxgraphstate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index e30199fb1..fc50ec9c1 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -19,6 +19,7 @@ import numpy as np import pytest + from graphqomb.common import Plane, PlannerMeasBasis, is_close_angle from graphqomb.euler import LocalClifford, update_lc_basis from graphqomb.gflow_utils import gflow_wrapper From 370d3bc8bfbe8b6a0422279dcd3f67c5e7f260db Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 5 Dec 2025 17:19:13 +0900 Subject: [PATCH 71/94] :memo: Fix docs --- docs/source/graphstate.rst | 8 -------- docs/source/zxgraphstate.rst | 10 +++++++++- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/source/graphstate.rst b/docs/source/graphstate.rst index 9e228eddb..392e90718 100644 --- a/docs/source/graphstate.rst +++ b/docs/source/graphstate.rst @@ -24,11 +24,3 @@ Functions .. autofunction:: graphqomb.graphstate.compose .. autofunction:: graphqomb.graphstate.bipartite_edges .. autofunction:: graphqomb.graphstate.odd_neighbors - -Auxiliary Classes ------------------- -.. autoclass:: graphqomb.graphstate.LocalCliffordExpansion - :members: - -.. autoclass:: graphqomb.graphstate.ExpansionMaps - :members: diff --git a/docs/source/zxgraphstate.rst b/docs/source/zxgraphstate.rst index 6961dc11a..97649be98 100644 --- a/docs/source/zxgraphstate.rst +++ b/docs/source/zxgraphstate.rst @@ -17,5 +17,13 @@ ZX Graph State Functions --------- -.. autofunction:: graphqomb.zxgraphstate.to_zx_graphstate .. autofunction:: graphqomb.zxgraphstate.complete_graph_edges + +Auxiliary Classes +------------------ + +.. autoclass:: graphqomb.zxgraphstate.LocalCliffordExpansion + :members: + +.. autoclass:: graphqomb.zxgraphstate.ExpansionMaps + :members: From 6c6cab14454dbbcc68367a8771ebe41eb8d53abb Mon Sep 17 00:00:00 2001 From: nabe98 Date: Fri, 5 Dec 2025 17:31:45 +0900 Subject: [PATCH 72/94] :art: Fix doc --- graphqomb/zxgraphstate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index ef05b2fc3..188713ddc 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -59,8 +59,6 @@ class ZXGraphState(GraphState): measurement bases q_indices : `dict`\[`int`, `int`\] qubit indices - local_cliffords : `dict`\[`int`, `LocalClifford`\] - local clifford operators """ __local_cliffords: dict[int, LocalClifford] From 59e41820e51ed0f0d97c8b609143a4e6751136d7 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Thu, 11 Dec 2025 17:27:53 +0900 Subject: [PATCH 73/94] :art: Replace FlowLike --- examples/zxgraph_simplification.py | 2 -- graphqomb/gflow_utils.py | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py index e67410cd6..85c310568 100644 --- a/examples/zxgraph_simplification.py +++ b/examples/zxgraph_simplification.py @@ -25,8 +25,6 @@ from graphqomb.visualizer import visualize from graphqomb.zxgraphstate import ZXGraphState -FlowLike = dict[int, set[int]] - # %% # Prepare an initial random graph state with flow graph, flow = generate_random_flow_graph(width=3, depth=4, edge_p=0.5) diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py index 8c8c89a39..3dbe76e40 100644 --- a/graphqomb/gflow_utils.py +++ b/graphqomb/gflow_utils.py @@ -25,10 +25,8 @@ from graphqomb.graphstate import BaseGraphState - FlowLike = dict[int, set[int]] - -def gflow_wrapper(graphstate: BaseGraphState) -> FlowLike: +def gflow_wrapper(graphstate: BaseGraphState) -> dict[int, set[int]]: """Utilize ``swiflow.gflow`` to search gflow. Parameters @@ -38,7 +36,7 @@ def gflow_wrapper(graphstate: BaseGraphState) -> FlowLike: Returns ------- - ``FlowLike`` + ``dict[int, set[int]]`` gflow object Raises From d73da3a0dd96e8a2be99c3c51176e815686a6170 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Thu, 11 Dec 2025 17:43:25 +0900 Subject: [PATCH 74/94] :fire: Remove Any --- graphqomb/gflow_utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py index 3dbe76e40..9a7a34013 100644 --- a/graphqomb/gflow_utils.py +++ b/graphqomb/gflow_utils.py @@ -19,8 +19,6 @@ from graphqomb.common import Plane, PlannerMeasBasis if TYPE_CHECKING: - from typing import Any - from networkx import Graph as NxGraph from graphqomb.graphstate import BaseGraphState @@ -48,7 +46,7 @@ def gflow_wrapper(graphstate: BaseGraphState) -> dict[int, set[int]]: ----- This wrapper does not support graph states with multiple subgraph structures. """ - graph: NxGraph[Any] = nx.Graph() + graph: NxGraph[int] = nx.Graph() graph.add_nodes_from(graphstate.physical_nodes) graph.add_edges_from(graphstate.physical_edges) From 53d01c14b28c020b96db732524e73e55d4d776d0 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Thu, 11 Dec 2025 17:47:02 +0900 Subject: [PATCH 75/94] :art: Replace by assert_never --- graphqomb/gflow_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py index 9a7a34013..29b4e2e46 100644 --- a/graphqomb/gflow_utils.py +++ b/graphqomb/gflow_utils.py @@ -15,6 +15,7 @@ import networkx as nx from swiflow import gflow from swiflow.common import Plane as SfPlane +from typing_extensions import assert_never from graphqomb.common import Plane, PlannerMeasBasis @@ -61,8 +62,7 @@ def gflow_wrapper(graphstate: BaseGraphState) -> dict[int, set[int]]: elif plane == Plane.XZ: swiflow_planes[node] = SfPlane.XZ else: - msg = f"No match {plane}" - raise ValueError(msg) + assert_never(plane) gflow_object = gflow.find( graph, set(graphstate.input_node_indices), set(graphstate.output_node_indices), swiflow_planes From 1acaa6ee81fbe1db47ea500ae72e737235022a41 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Thu, 11 Dec 2025 20:55:52 +0900 Subject: [PATCH 76/94] :art: Fix string --- graphqomb/graphstate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index 8b667af8f..c7f3e632b 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -459,8 +459,7 @@ def check_canonical_form(self) -> None: r"""Check if the graph state is in canonical form. The definition of canonical form is: - 1. No Clifford operators applied (here, always true) - 2. All non-output nodes have measurement basis + All non-output nodes have measurement basis Raises ------ From 24385f3468f85aa0959f5e44451c3cd6b2af889f Mon Sep 17 00:00:00 2001 From: nabe98 Date: Thu, 11 Dec 2025 20:58:11 +0900 Subject: [PATCH 77/94] :fire: Remove debug memo --- examples/zxgraph_simplification.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py index 85c310568..8e5e71a9f 100644 --- a/examples/zxgraph_simplification.py +++ b/examples/zxgraph_simplification.py @@ -83,14 +83,6 @@ def print_meas_bses(graph: ZXGraphState) -> None: print_meas_bses(zx_graph_smp) print_boundary_lcs(zx_graph_smp) -# %% -# NOTE: -# At first glance, the input/output nodes appear to remain unaffected. -# However, note that a local Clifford operation is actually applied as a result of the action of the full_reduce method. - -# If you visualize the graph state after executing the `expand_local_cliffords` method, -# you will see additional nodes connected to the former input/output nodes. - # %% # Let us compare the graph state before and after simplification. From 7959b82aa45ac6c99d6863df630669c0711b420c Mon Sep 17 00:00:00 2001 From: nabe98 Date: Thu, 11 Dec 2025 21:09:48 +0900 Subject: [PATCH 78/94] :art: Ensure base has local_clifford attr in from_base_graph_state --- graphqomb/zxgraphstate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 188713ddc..ffb711618 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -176,10 +176,10 @@ def from_base_graph_state( zxgraph_state, node_map = super().from_base_graph_state(base) # Copy local Clifford operators if requested and source is ZXGraphState - if copy_local_cliffords and isinstance(base, ZXGraphState): - for node, lc in base.local_cliffords.items(): - # Access private attribute to copy local cliffords - zxgraph_state.apply_local_clifford(node_map[node], lc) + if not copy_local_cliffords or not isinstance(base, ZXGraphState) or not hasattr(base, "local_cliffords"): + return zxgraph_state, node_map + for node, lc in base.local_cliffords.items(): + zxgraph_state.apply_local_clifford(node_map[node], lc) return zxgraph_state, node_map From e97d0601eb411cecda39201485c53ccd43ca9d27 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Thu, 11 Dec 2025 21:19:10 +0900 Subject: [PATCH 79/94] :art: Replace set creation --- graphqomb/gflow_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py index 29b4e2e46..38c073bfb 100644 --- a/graphqomb/gflow_utils.py +++ b/graphqomb/gflow_utils.py @@ -65,7 +65,7 @@ def gflow_wrapper(graphstate: BaseGraphState) -> dict[int, set[int]]: assert_never(plane) gflow_object = gflow.find( - graph, set(graphstate.input_node_indices), set(graphstate.output_node_indices), swiflow_planes + graph, graphstate.input_node_indices.keys(), graphstate.output_node_indices.keys(), swiflow_planes ) if gflow_object is None: msg = "No flow found" From 718e7ef9aa0283139e16455a2789f4ce042585ba Mon Sep 17 00:00:00 2001 From: nabe98 Date: Thu, 11 Dec 2025 21:34:04 +0900 Subject: [PATCH 80/94] :art: Fix gflow_wrapper return obj --- graphqomb/gflow_utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py index 38c073bfb..f540a2de4 100644 --- a/graphqomb/gflow_utils.py +++ b/graphqomb/gflow_utils.py @@ -71,9 +71,7 @@ def gflow_wrapper(graphstate: BaseGraphState) -> dict[int, set[int]]: msg = "No flow found" raise ValueError(msg) - gflow_obj = gflow_object.f - - return {node: {child for child in children if child != node} for node, children in gflow_obj.items()} + return gflow_object.f #: Mapping between equivalent measurement bases. From 9ee30dba77a115245fc95a1f677fc097a12983a9 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 01:49:43 +0900 Subject: [PATCH 81/94] :art: Add expected_plane parameter to preserve gflow existence --- graphqomb/euler.py | 73 ++++++++++++++++++++++++++++++++------------- tests/test_euler.py | 43 ++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 21 deletions(-) diff --git a/graphqomb/euler.py b/graphqomb/euler.py index a33ff7aae..6d073f9c0 100644 --- a/graphqomb/euler.py +++ b/graphqomb/euler.py @@ -213,17 +213,52 @@ def conjugate(self) -> LocalClifford: return LocalClifford(-self.gamma, -self.beta, -self.alpha) -def meas_basis_info(vector: NDArray[np.complex128]) -> tuple[Plane, float]: +def _meas_basis_candidates(vector: NDArray[np.complex128]) -> list[tuple[Plane, float]]: + r"""Return candidate measurement planes and angles corresponding to a vector. + + Parameters + ---------- + vector : `numpy.typing.NDArray`\[`numpy.complex128`\] + 1 qubit state vector + + Returns + ------- + `list`\[`tuple`\[`Plane`, `float`\]\] + candidate measurement planes and angles + """ + theta, phi = bloch_sphere_coordinates(vector) + candidates: list[tuple[Plane, float]] = [] + if is_clifford_angle(phi): + # YZ or XZ plane + if is_close_angle(2 * phi, 0): # 0 or pi + xz_theta = -theta if is_close_angle(phi, math.pi) else theta + candidates.append((Plane.XZ, xz_theta)) + if is_close_angle(phi, 0) and is_close_angle(2 * theta, 0): + candidates.append((Plane.YZ, theta)) + if is_close_angle(2 * (phi - math.pi / 2), 0): + yz_theta = -theta if is_close_angle(phi, 3 * math.pi / 2) else theta + candidates.append((Plane.YZ, yz_theta)) + if is_clifford_angle(theta) and not is_clifford_angle(theta / 2): + # XY plane + phi = phi + math.pi if is_close_angle(theta, 3 * math.pi / 2) else phi + candidates.append((Plane.XY, phi)) + + return candidates + + +def meas_basis_info(vector: NDArray[np.complex128], expected_plane: Plane | None = None) -> tuple[Plane, float]: r"""Return the measurement plane and angle corresponding to a vector. Parameters ---------- vector : `numpy.typing.NDArray`\[`numpy.complex128`\] 1 qubit state vector + expected_plane : `Plane` | `None`, optional + expected measurement plane to preserve gflow existence, by default None Returns ------- - `tuple`\[`Plane`, `float`] + `tuple`\[`Plane`, `float`\] measurement plane and angle Raises @@ -231,23 +266,17 @@ def meas_basis_info(vector: NDArray[np.complex128]) -> tuple[Plane, float]: ValueError if the vector does not lie on any of 3 planes """ - theta, phi = bloch_sphere_coordinates(vector) - if is_clifford_angle(phi): - # YZ or XZ plane - if is_clifford_angle(phi / 2): # 0 or pi - if is_close_angle(phi, math.pi): - theta = -theta - return Plane.XZ, theta - if is_close_angle(phi, 3 * math.pi / 2): - theta = -theta - return Plane.YZ, theta - if is_clifford_angle(theta) and not is_clifford_angle(theta / 2): - # XY plane - if is_close_angle(theta, 3 * math.pi / 2): - phi += math.pi - return Plane.XY, phi - msg = "The vector does not lie on any of 3 planes" - raise ValueError(msg) + candidates = _meas_basis_candidates(vector) + + if not candidates: + msg = "The vector does not lie on any of 3 planes" + raise ValueError(msg) + + if expected_plane is not None: + for plane, angle in candidates: + if plane == expected_plane: + return plane, angle + return candidates[0] # TODO(masa10-f): Algebraic backend for this computation(#023) @@ -275,7 +304,7 @@ def update_lc_lc(lc1: LocalClifford, lc2: LocalClifford) -> LocalClifford: # TODO(masa10-f): Algebraic backend for this computation(#023) -def update_lc_basis(lc: LocalClifford, basis: MeasBasis) -> PlannerMeasBasis: +def update_lc_basis(lc: LocalClifford, basis: MeasBasis, expected_plane: Plane | None = None) -> PlannerMeasBasis: """Update a `MeasBasis` object with an action of `LocalClifford` object. Parameters @@ -284,6 +313,8 @@ def update_lc_basis(lc: LocalClifford, basis: MeasBasis) -> PlannerMeasBasis: `LocalClifford` basis : `MeasBasis` `MeasBasis` + expected_plane : `Plane` | `None`, optional + expected measurement plane to preserve gflow existence, by default None Returns ------- @@ -294,7 +325,7 @@ def update_lc_basis(lc: LocalClifford, basis: MeasBasis) -> PlannerMeasBasis: vector = basis.vector() updated_vector = np.asarray(matrix @ vector, dtype=np.complex128) - plane, angle = meas_basis_info(updated_vector) + plane, angle = meas_basis_info(updated_vector, expected_plane=expected_plane) return PlannerMeasBasis(plane, angle) diff --git a/tests/test_euler.py b/tests/test_euler.py index 71be3a17f..fe99a4258 100644 --- a/tests/test_euler.py +++ b/tests/test_euler.py @@ -9,6 +9,7 @@ from graphqomb.euler import ( LocalClifford, LocalUnitary, + _meas_basis_candidates, bloch_sphere_coordinates, euler_decomposition, meas_basis_info, @@ -91,6 +92,37 @@ def test_bloch_sphere_coordinates_corner(plane: Plane, angle: float) -> None: assert np.isclose(inner_product, 1) +DEGENERATE_CASES: list[tuple[Plane, float, Plane, float]] = [ + (Plane.XZ, 0.0, Plane.YZ, 0.0), + (Plane.XZ, np.pi, Plane.YZ, np.pi), + (Plane.XZ, 0.5 * np.pi, Plane.XY, 0.0), + (Plane.XZ, 1.5 * np.pi, Plane.XY, np.pi), + (Plane.YZ, 0.5 * np.pi, Plane.XY, 0.5 * np.pi), + (Plane.YZ, 1.5 * np.pi, Plane.XY, 1.5 * np.pi), +] + + +def test_equivalence() -> None: + for plane1, angle1, plane2, angle2 in DEGENERATE_CASES: + basis1 = meas_basis(plane1, angle1) + basis2 = meas_basis(plane2, angle2) + inner_product = abs(np.vdot(basis1, basis2)) + assert np.isclose(inner_product, 1) + + +@pytest.mark.parametrize("case", DEGENERATE_CASES) +def test_meas_basis_candidates(case: tuple[Plane, float, Plane, float]) -> None: + plane1, angle1, plane2, angle2 = case + basis = meas_basis(plane1, angle1) + candidates = _meas_basis_candidates(basis) + expected_candidates = [(plane1, angle1), (plane2, angle2)] + assert len(candidates) == len(expected_candidates) + for expected in expected_candidates: + assert any( + candidate[0] == expected[0] and is_close_angle(candidate[1], expected[1]) for candidate in candidates + ) + + @pytest.mark.parametrize("plane", list(Plane)) def test_meas_basis_info(plane: Plane, rng: np.random.Generator) -> None: angle = rng.uniform(0, 2 * np.pi) @@ -100,6 +132,17 @@ def test_meas_basis_info(plane: Plane, rng: np.random.Generator) -> None: assert is_close_angle(angle, angle_get), f"Expected {angle}, got {angle_get}" +@pytest.mark.parametrize("case", DEGENERATE_CASES) +def test_meas_basis_info_degenerate(case: tuple[Plane, float, Plane, float]) -> None: + plane1, angle1, plane2, angle2 = case + basis = meas_basis(plane1, angle1) + + for expected_plane, expected_angle in ((plane1, angle1), (plane2, angle2)): + plane_get, angle_get = meas_basis_info(basis, expected_plane=expected_plane) + assert expected_plane == plane_get + assert is_close_angle(expected_angle, angle_get) + + def test_local_clifford(random_clifford_angles: tuple[float, float, float]) -> None: lc = LocalClifford(*random_clifford_angles) assert is_unitary(lc.matrix()) From 61a63f03c9e9d0b92f26826fc47b79b3d7e6f3e4 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 02:09:37 +0900 Subject: [PATCH 82/94] :bug: Fix bug in pivot --- graphqomb/zxgraphstate.py | 171 +++++++++++++------------------------- 1 file changed, 59 insertions(+), 112 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index ffb711618..2600c621d 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -21,13 +21,10 @@ from graphqomb.common import ( Plane, PlannerMeasBasis, - basis2tuple, is_clifford_angle, is_close_angle, - round_clifford_angle, ) from graphqomb.euler import LocalClifford, update_lc_basis, update_lc_lc -from graphqomb.gflow_utils import _EQUIV_MEAS_BASIS_MAP from graphqomb.graphstate import BaseGraphState, GraphState, bipartite_edges if sys.version_info >= (3, 10): @@ -78,7 +75,7 @@ def local_cliffords(self) -> dict[int, LocalClifford]: """ return self.__local_cliffords.copy() - def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: + def apply_local_clifford(self, node: int, lc: LocalClifford, expected_plane: Plane | None = None) -> None: """Apply a local clifford to the node. Parameters @@ -87,6 +84,8 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: node index lc : `LocalClifford` local clifford operator + expected_plane : `Plane` | `None`, optional + expected measurement plane to preserve gflow existence, by default None """ self._ensure_node_exists(node) if node in self.input_node_indices or node in self.output_node_indices: @@ -98,7 +97,7 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: self.__local_cliffords[node] = lc else: self._check_meas_basis() - new_meas_basis = update_lc_basis(lc, self.meas_bases[node]) + new_meas_basis = update_lc_basis(lc, self.meas_bases[node], expected_plane=expected_plane) self.assign_meas_basis(node, new_meas_basis) def remove_physical_node(self, node: int) -> None: @@ -201,39 +200,14 @@ def _clifford_rules(self) -> tuple[CliffordRule, ...]: (self._is_trivial_meas, lambda _: None), ( self._needs_pivot, - lambda node: self.pivot(node, min(self.neighbors(node) - set(self.input_node_indices))), + lambda node: self.pivot( + node, + min(self.neighbors(node) - set(self.input_node_indices) - set(self.output_node_indices)), + ), ), (self._needs_pivot_on_boundary, self.pivot_on_boundary), ) - def _assure_gflow(self, node: int, plane_map: dict[Plane, Plane], old_basis: tuple[Plane, float]) -> None: - r"""Transform the measurement basis after applying operation to assure gflow existence. - - This method is used to assure gflow existence - after the Clifford angle measurement basis is transformed by LocalClifford. - - Parameters - ---------- - node : `int` - node index - plane_map : `dict`\[`Plane`, `Plane`\] - mapping of planes - old_basis : `tuple[Plane, float]` - basis before applying the operation (such as local complement, pivot etc.) - """ - # Round first - cur = self.meas_bases[node] - rounded = round_clifford_angle(cur.angle) - self.assign_meas_basis(node, PlannerMeasBasis(cur.plane, rounded)) - - # Re-read after rounding - cur = self.meas_bases[node] - cur_key = basis2tuple(cur) - - # Convert to an equivalent basis if plane mismatch - if plane_map[old_basis[0]] != cur.plane: - self.assign_meas_basis(node, _EQUIV_MEAS_BASIS_MAP[cur_key]) - def _update_connections( self, rmv_edges: AbstractSet[tuple[int, int]], new_edges: AbstractSet[tuple[int, int]] ) -> None: @@ -290,29 +264,34 @@ def local_complement(self, node: int) -> None: self._update_connections(rmv_edges, new_edges) - # apply local clifford to node and assure gflow existence + # apply local clifford to node lc = LocalClifford(0, np.pi / 2, 0) old_meas_basis = self.meas_bases.get(node, None) if old_meas_basis is None: self.apply_local_clifford(node, lc) else: - old_basis = basis2tuple(old_meas_basis) - self.apply_local_clifford(node, lc) - plane_map: dict[Plane, Plane] = {Plane.XY: Plane.XZ, Plane.XZ: Plane.XY, Plane.YZ: Plane.YZ} - self._assure_gflow(node, plane_map, old_basis) - - # apply local clifford to neighbors and assure gflow existence + # assure gflow existence + plane_map: dict[Plane, Plane] = { + Plane.XY: Plane.XZ, + Plane.XZ: Plane.XY, + Plane.YZ: Plane.YZ, + } + self.apply_local_clifford(node, lc, expected_plane=plane_map[old_meas_basis.plane]) + + # apply local clifford to neighbors lc = LocalClifford(-np.pi / 2, 0, 0) - plane_map = {Plane.XY: Plane.XY, Plane.XZ: Plane.YZ, Plane.YZ: Plane.XZ} + plane_map = { + Plane.XY: Plane.XY, + Plane.XZ: Plane.YZ, + Plane.YZ: Plane.XZ, + } for v in nbrs: old_meas_basis = self.meas_bases.get(v, None) if old_meas_basis is None: self.apply_local_clifford(v, lc) continue - self.apply_local_clifford(v, lc) - old_basis = basis2tuple(old_meas_basis) - self._assure_gflow(v, plane_map, old_basis) + self.apply_local_clifford(v, lc, expected_plane=plane_map[old_meas_basis.plane]) def _pivot(self, node1: int, node2: int) -> None: """Pivot edges around nodes u and v in the graph state. @@ -367,13 +346,16 @@ def pivot(self, node1: int, node2: int) -> None: Notes ----- - Here we adopt the definition (lemma) of pivot from [1]. + Here we adopt the definition (lemma) of pivot from [1]: + Rz(pi/2) Rx(pi/2) Rz(pi/2) on the target nodes, + Rz(pi) on the common neighbors of both target nodes. + In some literature, pivot is defined as below:: Rz(pi/2) Rx(-pi/2) Rz(pi/2) on the target nodes, Rz(pi) on all the neighbors of both target nodes (not including the target nodes). - This definition is strictly equivalent to the one adopted here. + These definitions are equivalent. References ---------- @@ -385,10 +367,15 @@ def pivot(self, node1: int, node2: int) -> None: msg = "Cannot apply pivot to input node" raise ValueError(msg) self._check_meas_basis() + common_nbrs = self.neighbors(node1) & self.neighbors(node2) self._pivot(node1, node2) # update node1 and node2 measurement - plane_map: dict[Plane, Plane] = {Plane.XY: Plane.YZ, Plane.XZ: Plane.XZ, Plane.YZ: Plane.XY} + plane_map: dict[Plane, Plane] = { + Plane.XY: Plane.YZ, + Plane.XZ: Plane.XZ, + Plane.YZ: Plane.XY, + } lc = LocalClifford(np.pi / 2, np.pi / 2, np.pi / 2) for a in {node1, node2}: old_meas_basis = self.meas_bases.get(a, None) @@ -396,22 +383,22 @@ def pivot(self, node1: int, node2: int) -> None: self.apply_local_clifford(a, lc) continue - self.apply_local_clifford(a, lc) - old_basis = basis2tuple(old_meas_basis) - self._assure_gflow(a, plane_map, old_basis) + self.apply_local_clifford(a, lc, expected_plane=plane_map[old_meas_basis.plane]) # update nodes measurement of neighbors - plane_map = {Plane.XY: Plane.XY, Plane.XZ: Plane.XZ, Plane.YZ: Plane.YZ} + plane_map = { + Plane.XY: Plane.XY, + Plane.XZ: Plane.XZ, + Plane.YZ: Plane.YZ, + } lc = LocalClifford(np.pi, 0, 0) - for w in self.neighbors(node1) & self.neighbors(node2): + for w in common_nbrs: old_meas_basis = self.meas_bases.get(w, None) if old_meas_basis is None: self.apply_local_clifford(w, lc) continue - self.apply_local_clifford(w, lc) - old_basis = basis2tuple(old_meas_basis) - self._assure_gflow(w, plane_map, old_basis) + self.apply_local_clifford(w, lc, expected_plane=plane_map[old_meas_basis.plane]) def _is_trivial_meas(self, node: int, atol: float = 1e-9) -> bool: """Check if the node does not need any operation in order to perform _remove_clifford. @@ -580,20 +567,25 @@ def _remove_clifford(self, node: int, atol: float = 1e-9) -> None: if not is_close_angle(2 * a_pi, 0, atol): msg = "This node cannot be removed by _remove_clifford." raise ValueError(msg) + if self.meas_bases[node].plane not in {Plane.YZ, Plane.XZ}: + msg = "This node cannot be removed by _remove_clifford (plane must be YZ or XZ)." + raise ValueError(msg) + neighbors = self.neighbors(node) + self.remove_physical_node(node) lc = LocalClifford(a_pi, 0, 0) - plane_map = {Plane.XY: Plane.XY, Plane.XZ: Plane.XZ, Plane.YZ: Plane.YZ} - for v in self.neighbors(node): + plane_map = { + Plane.XY: Plane.XY, + Plane.XZ: Plane.XZ, + Plane.YZ: Plane.YZ, + } + for v in neighbors: old_meas_basis = self.meas_bases.get(v, None) if old_meas_basis is None: self.apply_local_clifford(v, lc) continue - self.apply_local_clifford(v, lc) - old_basis = basis2tuple(old_meas_basis) - self._assure_gflow(v, plane_map, old_basis) - - self.remove_physical_node(node) + self.apply_local_clifford(v, lc, expected_plane=plane_map[old_meas_basis.plane]) def remove_clifford(self, node: int, atol: float = 1e-9) -> None: """Remove the local Clifford node. @@ -634,6 +626,9 @@ def remove_clifford(self, node: int, atol: float = 1e-9) -> None: if not check(node, atol): continue action(node) + if node in self.input_node_indices or node in self.output_node_indices: + msg = "Clifford node removal not allowed for input or output nodes." + raise ValueError(msg) self._remove_clifford(node, atol) return @@ -686,52 +681,6 @@ def remove_cliffords(self, atol: float = 1e-9) -> None: action(clifford_node) self._remove_clifford(clifford_node, atol) - def to_xy(self) -> None: - r"""Update some special measurement basis to logically equivalent XY-basis. - - - (Plane.XZ, \pm pi/2) -> (Plane.XY, 0 or pi) - - (Plane.YZ, \pm pi/2) -> (Plane.XY, \pm pi/2) - - This method is mainly used in convert_to_phase_gadget. - """ - for node, basis in self.meas_bases.items(): - if basis.plane == Plane.XZ and is_close_angle(basis.angle, np.pi / 2): - self.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, 0.0)) - elif basis.plane == Plane.XZ and is_close_angle(basis.angle, -np.pi / 2): - self.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, np.pi)) - elif basis.plane == Plane.YZ and is_close_angle(basis.angle, np.pi / 2): - self.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, np.pi / 2)) - elif basis.plane == Plane.YZ and is_close_angle(basis.angle, -np.pi / 2): - self.assign_meas_basis(node, PlannerMeasBasis(Plane.XY, -np.pi / 2)) - - def to_yz(self) -> None: - r"""Update some special measurement basis to logically equivalent YZ-basis. - - - (Plane.XZ, 0) -> (Plane.YZ, 0) - - (Plane.XZ, pi) -> (Plane.YZ, pi) - - This method is mainly used in convert_to_phase_gadget. - """ - for node, basis in self.meas_bases.items(): - if basis.plane == Plane.XZ and is_close_angle(basis.angle, 0.0): - self.assign_meas_basis(node, PlannerMeasBasis(Plane.YZ, 0.0)) - elif basis.plane == Plane.XZ and is_close_angle(basis.angle, np.pi): - self.assign_meas_basis(node, PlannerMeasBasis(Plane.YZ, np.pi)) - - def to_xz(self) -> None: - r"""Update some special measurement basis to logically equivalent XZ-basis. - - This method is mainly used when we want to find a gflow. - """ - inputs = set(self.input_node_indices) - for node, basis in self.meas_bases.items(): - if node in inputs: - continue - if basis.plane == Plane.YZ and is_close_angle(basis.angle, 0.0): - self.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, 0.0)) - elif basis.plane == Plane.YZ and is_close_angle(basis.angle, np.pi): - self.assign_meas_basis(node, PlannerMeasBasis(Plane.XZ, np.pi)) - def _extract_yz_adjacent_pair(self) -> tuple[int, int] | None: r"""Call inside convert_to_phase_gadget. @@ -766,8 +715,6 @@ def _extract_xz_node(self) -> int | 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: - self.to_xy() - self.to_yz() if pair := self._extract_yz_adjacent_pair(): self.pivot(*pair) continue @@ -796,7 +743,7 @@ def merge_yz_to_xy(self) -> None: } for u in target_nodes: (v,) = self.neighbors(u) - new_angle = (self.meas_bases[u].angle + self.meas_bases[v].angle) % (2.0 * np.pi) + new_angle = (self.meas_bases[u].angle - self.meas_bases[v].angle) % (2.0 * np.pi) self.assign_meas_basis(v, PlannerMeasBasis(Plane.XY, new_angle)) self.remove_physical_node(u) From 9361a22eb9692d8fc57315f79337081920e842df Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 02:16:01 +0900 Subject: [PATCH 83/94] :bug: Fix bug in expand_local_cliffords --- graphqomb/zxgraphstate.py | 129 +++++++++++++++++++------------------- 1 file changed, 64 insertions(+), 65 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 2600c621d..7b9c424f5 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -4,7 +4,8 @@ - `ZXGraphState`: Graph State for the ZX-calculus. - `complete_graph_edges`: Return a set of edges for the complete graph on the given nodes. -- `LocalCliffordExpansion`: Local Clifford expansion. +- `InputLocalCliffordExpansion`: Local Clifford expansion for input nodes. +- `OutputLocalCliffordExpansion`: Local Clifford expansion for output nodes. - `ExpansionMaps`: Expansion maps for local clifford operators. """ @@ -808,99 +809,88 @@ def expand_local_cliffords(self) -> ExpansionMaps: output_node_map = self._expand_output_local_cliffords() return ExpansionMaps(input_node_map, output_node_map) - def _expand_input_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: + def _expand_input_local_cliffords(self) -> dict[int, InputLocalCliffordExpansion]: r"""Expand local Clifford operators applied on the input nodes. Returns ------- - `dict`\[`int`, `LocalCliffordExpansion`\] + `dict`\[`int`, `InputLocalCliffordExpansion`\] A dictionary mapping input node indices to the new node indices created. """ - node_index_addition_map: dict[int, LocalCliffordExpansion] = {} - new_input_indices: dict[int, int] = {} - for old_input_node, q_index in self.input_node_indices.items(): - lc = self._pop_local_clifford(old_input_node) + node_index_addition_map: dict[int, InputLocalCliffordExpansion] = {} + for old_input in self.input_node_indices: + lc = self._pop_local_clifford(old_input) if lc is None: - lc = LocalClifford(0.0, 0.0, 0.0) + continue new_input = self.add_physical_node() new_node = self.add_physical_node() - new_input_indices[new_input] = q_index self.add_physical_edge(new_input, new_node) - self.add_physical_edge(new_node, old_input_node) - - self.assign_meas_basis(new_input, PlannerMeasBasis(Plane.XY, 0.0)) - self.assign_meas_basis(new_node, PlannerMeasBasis(Plane.XY, 0.0)) - meas_basis = self.meas_bases[old_input_node] - new_meas_basis = update_lc_basis(lc, meas_basis) - self.assign_meas_basis(old_input_node, new_meas_basis) - self._assure_gflow_input_expansion(old_input_node) - node_index_addition_map[old_input_node] = LocalCliffordExpansion(new_input, new_node) - - self._input_node_indices = {} - for new_input_index, q_index in new_input_indices.items(): - self.register_input(new_input_index, q_index) - - return node_index_addition_map - - def _assure_gflow_input_expansion(self, node: int) -> None: - r"""Assure gflow existence after input local Clifford expansion. + self.add_physical_edge(new_node, old_input) - Parameters - ---------- - node : `int` - node index - """ - cur = self.meas_bases[node] - rounded = round_clifford_angle(cur.angle) - self.assign_meas_basis(node, PlannerMeasBasis(cur.plane, rounded)) + self.replace_input(old_input, new_input) - cur = self.meas_bases[node] - cur_key = basis2tuple(cur) + self.assign_meas_basis(new_input, PlannerMeasBasis(Plane.XY, -lc.gamma)) + self.assign_meas_basis(new_node, PlannerMeasBasis(Plane.XY, -lc.beta)) - # if the updated basis is self-inclusion type, push it to an XY-equivalent one. - if (cur.plane in {Plane.XZ, Plane.YZ} and is_close_angle(cur.angle, 0.0)) or ( - is_close_angle(cur.angle, np.pi) and cur_key in _EQUIV_MEAS_BASIS_MAP - ): - self.assign_meas_basis(node, _EQUIV_MEAS_BASIS_MAP[cur_key]) + if is_close_angle(2 * lc.alpha, 0.0): + plane_map = { + Plane.XY: Plane.XY, + Plane.XZ: Plane.XZ, + Plane.YZ: Plane.YZ, + } + else: + plane_map = { + Plane.XY: Plane.XY, + Plane.XZ: Plane.YZ, + Plane.YZ: Plane.XZ, + } + meas_basis = self.meas_bases[old_input] + meas_basis = update_lc_basis( + LocalClifford(lc.alpha, 0.0, 0.0), + meas_basis, + expected_plane=plane_map[meas_basis.plane], + ) + self.assign_meas_basis(old_input, meas_basis) + node_index_addition_map[old_input] = InputLocalCliffordExpansion(new_input, new_node) - # ensure XY if possible. - cur = self.meas_bases[node] - if cur.plane != Plane.XY and cur_key in _EQUIV_MEAS_BASIS_MAP: - self.assign_meas_basis(node, _EQUIV_MEAS_BASIS_MAP[cur_key]) + return node_index_addition_map - def _expand_output_local_cliffords(self) -> dict[int, LocalCliffordExpansion]: + def _expand_output_local_cliffords(self) -> dict[int, OutputLocalCliffordExpansion]: r"""Expand local Clifford operators applied on the output nodes. Returns ------- - `dict`\[`int`, `LocalCliffordExpansion`\] + `dict`\[`int`, `OutputLocalCliffordExpansion`\] A dictionary mapping output node indices to the new node indices created. """ - node_index_addition_map: dict[int, LocalCliffordExpansion] = {} - new_output_node_index_map: dict[int, int] = {} - for old_output_node, q_index in self.output_node_indices.items(): + node_index_addition_map: dict[int, OutputLocalCliffordExpansion] = {} + for old_output_node in self.output_node_indices: lc = self._pop_local_clifford(old_output_node) if lc is None: - new_output_node_index_map[old_output_node] = q_index continue - new_node = self.add_physical_node() + new_node1 = self.add_physical_node() + new_node2 = self.add_physical_node() + new_node3 = self.add_physical_node() new_output_node = self.add_physical_node() - new_output_node_index_map[new_output_node] = q_index - self._output_node_indices.pop(old_output_node) - self.register_output(new_output_node, q_index) + self.add_physical_edge(old_output_node, new_node1) + self.add_physical_edge(new_node1, new_node2) + self.add_physical_edge(new_node2, new_node3) + self.add_physical_edge(new_node3, new_output_node) - self.add_physical_edge(old_output_node, new_node) - self.add_physical_edge(new_node, new_output_node) + self.replace_output(old_output_node, new_output_node) - self.assign_meas_basis(new_node, PlannerMeasBasis(Plane.XY, 0.0)) - meas_basis = update_lc_basis(lc, PlannerMeasBasis(Plane.XY, 0.0)) - self.assign_meas_basis(old_output_node, meas_basis) + self.assign_meas_basis(old_output_node, PlannerMeasBasis(Plane.XY, -lc.alpha)) + self.assign_meas_basis(new_node1, PlannerMeasBasis(Plane.XY, -lc.beta)) + self.assign_meas_basis(new_node2, PlannerMeasBasis(Plane.XY, -lc.gamma)) + self.assign_meas_basis(new_node3, PlannerMeasBasis(Plane.XY, 0.0)) - node_index_addition_map[old_output_node] = LocalCliffordExpansion(new_node, new_output_node) + node_index_addition_map[old_output_node] = OutputLocalCliffordExpansion( + new_node1, new_node2, new_node3, new_output_node + ) return node_index_addition_map @@ -921,15 +911,24 @@ def complete_graph_edges(nodes: Iterable[int]) -> set[tuple[int, int]]: return {(min(u, v), max(u, v)) for u, v in combinations(nodes, 2)} -class LocalCliffordExpansion(NamedTuple): +class InputLocalCliffordExpansion(NamedTuple): """Local Clifford expansion map.""" node1: int node2: int +class OutputLocalCliffordExpansion(NamedTuple): + """Output local Clifford expansion map.""" + + node1: int + node2: int + node3: int + output_node: int + + class ExpansionMaps(NamedTuple): """Expansion maps for inputs and outputs with Local Clifford.""" - input_node_map: dict[int, LocalCliffordExpansion] - output_node_map: dict[int, LocalCliffordExpansion] + input_node_map: dict[int, InputLocalCliffordExpansion] + output_node_map: dict[int, OutputLocalCliffordExpansion] From 0cee9baf77af8e6d479370a44cdede11d2db85a1 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 02:17:11 +0900 Subject: [PATCH 84/94] :memo: Fix docs --- docs/source/gflow_utils.rst | 5 ----- docs/source/zxgraphstate.rst | 5 ++++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/source/gflow_utils.rst b/docs/source/gflow_utils.rst index f6be1232f..8c7e00376 100644 --- a/docs/source/gflow_utils.rst +++ b/docs/source/gflow_utils.rst @@ -10,8 +10,3 @@ Functions --------- .. autofunction:: graphqomb.gflow_utils.gflow_wrapper - -Constants ---------- - -.. autodata:: graphqomb.gflow_utils._EQUIV_MEAS_BASIS_MAP diff --git a/docs/source/zxgraphstate.rst b/docs/source/zxgraphstate.rst index 97649be98..c790cfd67 100644 --- a/docs/source/zxgraphstate.rst +++ b/docs/source/zxgraphstate.rst @@ -22,7 +22,10 @@ Functions Auxiliary Classes ------------------ -.. autoclass:: graphqomb.zxgraphstate.LocalCliffordExpansion +.. autoclass:: graphqomb.zxgraphstate.InputLocalCliffordExpansion + :members: + +.. autoclass:: graphqomb.zxgraphstate.OutputLocalCliffordExpansion :members: .. autoclass:: graphqomb.zxgraphstate.ExpansionMaps From 8059c8c3d182a8b41009f984f6006e2b6e60b769 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 02:37:20 +0900 Subject: [PATCH 85/94] :art: Fix tests --- tests/test_zxgraphstate.py | 67 +++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index fc50ec9c1..60e9bbb8e 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -21,7 +21,7 @@ import pytest from graphqomb.common import Plane, PlannerMeasBasis, is_close_angle -from graphqomb.euler import LocalClifford, update_lc_basis +from graphqomb.euler import LocalClifford from graphqomb.gflow_utils import gflow_wrapper from graphqomb.qompiler import qompile from graphqomb.random_objects import generate_random_flow_graph @@ -543,10 +543,10 @@ def test_local_complement_4_times( @pytest.mark.parametrize( - "gamma", + "alpha", [0, np.pi / 2, np.pi, 3 * np.pi / 2], ) -def test_expand_input_local_cliffords_xy_plane(zx_graph: ZXGraphState, gamma: float) -> None: +def test_expand_input_local_cliffords_xy_plane(zx_graph: ZXGraphState, alpha: float) -> None: """Test expanding local Clifford operators on an input node with XY measurement plane.""" old_input_node = zx_graph.add_physical_node() output_node = zx_graph.add_physical_node() @@ -556,26 +556,26 @@ def test_expand_input_local_cliffords_xy_plane(zx_graph: ZXGraphState, gamma: fl old_input_angle = np.pi / 3 zx_graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.XY, old_input_angle)) - alpha = 0.0 beta = 0.0 + gamma = 0.0 lc = LocalClifford(alpha=alpha, beta=beta, gamma=gamma) zx_graph.apply_local_clifford(old_input_node, lc) zx_graph.expand_local_cliffords() - new_input_node = 2 + new_input_node, new_node = (2, 3) assert zx_graph.input_node_indices == {new_input_node: 0} for node in zx_graph.physical_nodes - set(zx_graph.output_node_indices): assert zx_graph.meas_bases[node].plane == Plane.XY - assert is_close_angle(zx_graph.meas_bases[new_input_node].angle, 0.0) - assert is_close_angle(zx_graph.meas_bases[new_input_node + 1].angle, 0.0) - assert is_close_angle(zx_graph.meas_bases[old_input_node].angle, old_input_angle - gamma) + assert is_close_angle(zx_graph.meas_bases[new_node].angle, lc.beta) + assert is_close_angle(zx_graph.meas_bases[new_input_node].angle, lc.gamma) + assert is_close_angle(zx_graph.meas_bases[old_input_node].angle, old_input_angle - lc.alpha) @pytest.mark.parametrize( - "gamma", + "alpha", [0, np.pi, np.pi / 2, 3 * np.pi / 2], ) -def test_expand_input_local_cliffords_yz_plane(zx_graph: ZXGraphState, gamma: float) -> None: +def test_expand_input_local_cliffords_yz_plane(zx_graph: ZXGraphState, alpha: float) -> None: """Test expanding local Clifford operators on an input node with YZ measurement plane.""" old_input_node = zx_graph.add_physical_node() output_node = zx_graph.add_physical_node() @@ -585,19 +585,19 @@ def test_expand_input_local_cliffords_yz_plane(zx_graph: ZXGraphState, gamma: fl old_input_angle = np.pi / 3 zx_graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.YZ, old_input_angle)) - alpha = 0.0 beta = 0.0 + gamma = 0.0 lc = LocalClifford(alpha=alpha, beta=beta, gamma=gamma) zx_graph.apply_local_clifford(old_input_node, lc) zx_graph.expand_local_cliffords() exp_angle: float = old_input_angle - if is_close_angle(2 * gamma, 0): + if is_close_angle(2 * alpha, 0): assert zx_graph.meas_bases[old_input_node].plane == Plane.YZ - exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle - elif is_close_angle(2 * (gamma - np.pi / 2), 0): + exp_angle = old_input_angle if is_close_angle(alpha, 0) else -old_input_angle + elif is_close_angle(2 * (alpha - np.pi / 2), 0): assert zx_graph.meas_bases[old_input_node].plane == Plane.XZ - exp_angle = old_input_angle if is_close_angle(gamma - np.pi / 2, 0) else -old_input_angle + exp_angle = old_input_angle if is_close_angle(alpha - np.pi / 2, 0) else -old_input_angle assert is_close_angle(zx_graph.meas_bases[old_input_node].angle, exp_angle) new_input_node = 2 assert zx_graph.meas_bases[new_input_node].plane == Plane.XY @@ -605,10 +605,10 @@ def test_expand_input_local_cliffords_yz_plane(zx_graph: ZXGraphState, gamma: fl @pytest.mark.parametrize( - "gamma", + "alpha", [0, np.pi / 2, np.pi, 3 * np.pi / 2], ) -def test_expand_input_local_cliffords_xz_plane(zx_graph: ZXGraphState, gamma: float) -> None: +def test_expand_input_local_cliffords_xz_plane(zx_graph: ZXGraphState, alpha: float) -> None: """Test expanding local Clifford operators on an input node with XZ measurement plane.""" old_input_node = zx_graph.add_physical_node() output_node = zx_graph.add_physical_node() @@ -618,19 +618,19 @@ def test_expand_input_local_cliffords_xz_plane(zx_graph: ZXGraphState, gamma: fl old_input_angle = np.pi / 3 zx_graph.assign_meas_basis(old_input_node, PlannerMeasBasis(Plane.XZ, old_input_angle)) - alpha = 0.0 beta = 0.0 + gamma = 0.0 lc = LocalClifford(alpha=alpha, beta=beta, gamma=gamma) zx_graph.apply_local_clifford(old_input_node, lc) zx_graph.expand_local_cliffords() exp_angle: float = old_input_angle - if is_close_angle(2 * gamma, 0): + if is_close_angle(2 * alpha, 0): assert zx_graph.meas_bases[old_input_node].plane == Plane.XZ - exp_angle = old_input_angle if is_close_angle(gamma, 0) else -old_input_angle - elif is_close_angle(2 * (gamma - np.pi / 2), 0): + exp_angle = old_input_angle if is_close_angle(alpha, 0) else -old_input_angle + elif is_close_angle(2 * (alpha - np.pi / 2), 0): assert zx_graph.meas_bases[old_input_node].plane == Plane.YZ - exp_angle = old_input_angle if is_close_angle(gamma + np.pi / 2, 0) else -old_input_angle + exp_angle = old_input_angle if is_close_angle(alpha + np.pi / 2, 0) else -old_input_angle assert is_close_angle(zx_graph.meas_bases[old_input_node].angle, exp_angle) new_input_node = 2 assert zx_graph.meas_bases[new_input_node].plane == Plane.XY @@ -650,11 +650,12 @@ def test_expand_output_local_cliffords(zx_graph: ZXGraphState) -> None: zx_graph.expand_local_cliffords() new_output_node = 5 assert zx_graph.output_node_indices == {new_output_node: 0} - assert zx_graph.meas_bases[old_output_node + 1].plane == Plane.XY - assert is_close_angle(zx_graph.meas_bases[old_output_node + 1].angle, 0.0) - meas_basis = update_lc_basis(lc, PlannerMeasBasis(Plane.XY, 0.0)) - assert zx_graph.meas_bases[old_output_node].plane == meas_basis.plane - assert is_close_angle(zx_graph.meas_bases[old_output_node].angle, meas_basis.angle) + for node in zx_graph.physical_nodes - set(zx_graph.output_node_indices): + assert zx_graph.meas_bases[node].plane == Plane.XY + assert is_close_angle(zx_graph.meas_bases[old_output_node].angle, -lc.alpha) + assert is_close_angle(zx_graph.meas_bases[old_output_node + 1].angle, -lc.beta) + assert is_close_angle(zx_graph.meas_bases[old_output_node + 2].angle, -lc.gamma) + assert is_close_angle(zx_graph.meas_bases[old_output_node + 3].angle, 0.0) def test_local_clifford_expansion() -> None: @@ -672,8 +673,6 @@ def test_local_clifford_expansion() -> None: lc = LocalClifford(2 * np.pi, 0.0, 0.0) zx_graph_cp.apply_local_clifford(2, lc) zx_graph_cp.expand_local_cliffords() - zx_graph_cp.to_xy() - zx_graph_cp.to_xz() gflow_cp = gflow_wrapper(zx_graph_cp) pattern_cp = qompile(zx_graph_cp, gflow_cp) sim_cp = PatternSimulator(pattern_cp, backend=SimulatorBackend.StateVector) @@ -1078,7 +1077,7 @@ def test_convert_to_phase_gadget( (3, PlannerMeasBasis(Plane.XY, 0.44 * np.pi)), ], [ - (1, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (1, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), (3, PlannerMeasBasis(Plane.XY, 0.44 * np.pi)), ], @@ -1096,7 +1095,7 @@ def test_convert_to_phase_gadget( (3, PlannerMeasBasis(Plane.YZ, 0.44 * np.pi)), ], [ - (1, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (1, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), (3, PlannerMeasBasis(Plane.YZ, 0.44 * np.pi)), ], @@ -1215,7 +1214,7 @@ def test_merge_yz_nodes( ( (range(4), {(0, 1), (1, 2), (1, 3)}), [ - (0, PlannerMeasBasis(Plane.YZ, 0.1 * np.pi)), + (0, PlannerMeasBasis(Plane.YZ, -0.1 * np.pi)), (1, PlannerMeasBasis(Plane.XY, 0.4 * np.pi)), (2, PlannerMeasBasis(Plane.XY, 0.3 * np.pi)), (3, PlannerMeasBasis(Plane.XY, 0.4 * np.pi)), @@ -1233,7 +1232,7 @@ def test_merge_yz_nodes( ( (range(4), {(0, 1), (1, 2), (1, 3)}), [ - (0, PlannerMeasBasis(Plane.YZ, 0.1 * np.pi)), + (0, PlannerMeasBasis(Plane.YZ, -0.1 * np.pi)), (1, PlannerMeasBasis(Plane.XY, 0.9 * np.pi)), (2, PlannerMeasBasis(Plane.XZ, 0.8 * np.pi)), (3, PlannerMeasBasis(Plane.XY, 0.4 * np.pi)), @@ -1251,7 +1250,7 @@ def test_merge_yz_nodes( ( (range(6), {(0, 1), (1, 2), (1, 3), (2, 5), (3, 4)}), [ - (0, PlannerMeasBasis(Plane.YZ, 0.1 * np.pi)), + (0, PlannerMeasBasis(Plane.YZ, -0.1 * np.pi)), (1, PlannerMeasBasis(Plane.XY, 0.9 * np.pi)), (2, PlannerMeasBasis(Plane.YZ, 1.2 * np.pi)), (3, PlannerMeasBasis(Plane.XY, 1.4 * np.pi)), From 4998cf2c4c1010564d320c8f13428da554b17771 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 02:38:51 +0900 Subject: [PATCH 86/94] :art: Add unregister/replace method for inputs/outputs --- graphqomb/graphstate.py | 113 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 104 insertions(+), 9 deletions(-) diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index c7f3e632b..f5982fc53 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -179,8 +179,8 @@ class GraphState(BaseGraphState): __node_counter: int def __init__(self) -> None: - self._input_node_indices = {} - self._output_node_indices = {} + self.__input_node_indices = {} + self.__output_node_indices = {} self.__physical_nodes = set() self.__physical_edges = {} self.__meas_bases = {} @@ -197,7 +197,7 @@ def input_node_indices(self) -> dict[int, int]: `dict`\[`int`, `int`\] qubit indices map of input nodes. """ - return self._input_node_indices.copy() + return self.__input_node_indices.copy() @property @typing_extensions.override @@ -209,7 +209,7 @@ def output_node_indices(self) -> dict[int, int]: `dict`\[`int`, `int`\] qubit indices map of output nodes. """ - return self._output_node_indices.copy() + return self.__output_node_indices.copy() @property @typing_extensions.override @@ -345,7 +345,7 @@ def remove_physical_node(self, node: int) -> None: del self.__physical_edges[node] if node in self.output_node_indices: - del self._output_node_indices[node] + del self.__output_node_indices[node] self.__meas_bases.pop(node, None) def remove_physical_edge(self, node1: int, node2: int) -> None: @@ -388,13 +388,61 @@ def register_input(self, node: int, q_index: int) -> None: If the node is already registered as an input node. """ self._ensure_node_exists(node) - if node in self._input_node_indices: + if node in self.__input_node_indices: msg = "The node is already registered as an input node." raise ValueError(msg) if q_index in self.input_node_indices.values(): msg = "The q_index already exists in input qubit indices" raise ValueError(msg) - self._input_node_indices[node] = q_index + self.__input_node_indices[node] = q_index + + def unregister_input(self, node: int) -> int: + """Remove the input label from the node. + + Parameters + ---------- + node : `int` + node index + + Returns + ------- + `int` + logical qubit index of the unregistered input node + + Raises + ------ + ValueError + If the node is not registered as an input node. + """ + self._ensure_node_exists(node) + if node not in self.__input_node_indices: + msg = "The node is not registered as an input node." + raise ValueError(msg) + return self.__input_node_indices.pop(node) + + def replace_input(self, old_node: int, new_node: int) -> None: + """Replace the input node with a new node. + + Parameters + ---------- + old_node : `int` + node index whose input label to be replaced + new_node : `int` + node index to be set as the new input node + + Raises + ------ + ValueError + If the new_node is already registered as an input node. + """ + self._ensure_node_exists(new_node) + + q_index = self.unregister_input(old_node) + try: + self.register_input(new_node, q_index) + except ValueError: + self.register_input(old_node, q_index) # rollback + raise @typing_extensions.override def register_output(self, node: int, q_index: int) -> None: @@ -415,13 +463,60 @@ def register_output(self, node: int, q_index: int) -> None: 3. If the q_index already exists in output qubit indices. """ self._ensure_node_exists(node) - if node in self._output_node_indices: + if node in self.__output_node_indices: msg = "The node is already registered as an output node." raise ValueError(msg) if q_index in self.output_node_indices.values(): msg = "The q_index already exists in output qubit indices" raise ValueError(msg) - self._output_node_indices[node] = q_index + self.__output_node_indices[node] = q_index + + def unregister_output(self, node: int) -> int: + """Remove the output label from the node. + + Parameters + ---------- + node : `int` + node index + + Returns + ------- + `int` + logical qubit index of the unregistered output node + + Raises + ------ + ValueError + If the node is not registered as an output node. + """ + self._ensure_node_exists(node) + if node not in self.__output_node_indices: + msg = "The node is not registered as an output node." + raise ValueError(msg) + return self.__output_node_indices.pop(node) + + def replace_output(self, old_node: int, new_node: int) -> None: + """Replace the output node with a new node. + + Parameters + ---------- + old_node : `int` + node index whose output label to be replaced + new_node : `int` + node index to be set as the new output node + + Raises + ------ + ValueError + If the new_node is already registered as an output node. + """ + self._ensure_node_exists(new_node) + q_index = self.unregister_output(old_node) + try: + self.register_output(new_node, q_index) + except ValueError: + self.register_output(old_node, q_index) # rollback + raise @typing_extensions.override def assign_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: From 475f4e77407b831fbfc8d4c3b0f320daaab53eb6 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 02:42:11 +0900 Subject: [PATCH 87/94] :fire: Clean gflow_utils --- graphqomb/gflow_utils.py | 35 +---------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py index f540a2de4..f0b81ec1d 100644 --- a/graphqomb/gflow_utils.py +++ b/graphqomb/gflow_utils.py @@ -4,12 +4,10 @@ - `gflow_wrapper`: Thin adapter around ``swiflow.gflow`` so that gflow can be computed directly from a `BaseGraphState` instance. -- `_EQUIV_MEAS_BASIS_MAP`: A mapping between equivalent measurement bases used to improve gflow finding performance. """ from __future__ import annotations -import math from typing import TYPE_CHECKING import networkx as nx @@ -17,7 +15,7 @@ from swiflow.common import Plane as SfPlane from typing_extensions import assert_never -from graphqomb.common import Plane, PlannerMeasBasis +from graphqomb.common import Plane if TYPE_CHECKING: from networkx import Graph as NxGraph @@ -72,34 +70,3 @@ def gflow_wrapper(graphstate: BaseGraphState) -> dict[int, set[int]]: raise ValueError(msg) return gflow_object.f - - -#: Mapping between equivalent measurement bases. -#: -#: This map is used to replace a measurement basis by an equivalent one -#: to improve gflow search performance. -#: -#: Key: -#: ``(Plane, angle)`` where angle is in radians. -#: Value: -#: :class:`~graphqomb.common.PlannerMeasBasis`. -_EQUIV_MEAS_BASIS_MAP: dict[tuple[Plane, float], PlannerMeasBasis] = { - # (XY, 0) <-> (XZ, pi/2) - (Plane.XY, 0.0): PlannerMeasBasis(Plane.XZ, 0.5 * math.pi), - (Plane.XZ, 0.5 * math.pi): PlannerMeasBasis(Plane.XY, 0.0), - # (XY, pi/2) <-> (YZ, pi/2) - (Plane.XY, 0.5 * math.pi): PlannerMeasBasis(Plane.YZ, 0.5 * math.pi), - (Plane.YZ, 0.5 * math.pi): PlannerMeasBasis(Plane.XY, 0.5 * math.pi), - # (XY, -pi/2) == (XY, 3pi/2) <-> (YZ, 3pi/2) - (Plane.XY, 1.5 * math.pi): PlannerMeasBasis(Plane.YZ, 1.5 * math.pi), - (Plane.YZ, 1.5 * math.pi): PlannerMeasBasis(Plane.XY, 1.5 * math.pi), - # (XY, pi) <-> (XZ, -pi/2) == (XZ, 3pi/2) - (Plane.XY, math.pi): PlannerMeasBasis(Plane.XZ, 1.5 * math.pi), - (Plane.XZ, 1.5 * math.pi): PlannerMeasBasis(Plane.XY, math.pi), - # (XZ, 0) <-> (YZ, 0) - (Plane.XZ, 0.0): PlannerMeasBasis(Plane.YZ, 0.0), - (Plane.YZ, 0.0): PlannerMeasBasis(Plane.XZ, 0.0), - # (XZ, pi) <-> (YZ, pi) - (Plane.XZ, math.pi): PlannerMeasBasis(Plane.YZ, math.pi), - (Plane.YZ, math.pi): PlannerMeasBasis(Plane.XZ, math.pi), -} From 3de2d37733447d566f2ab33f451e5eb9db955085 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 02:47:31 +0900 Subject: [PATCH 88/94] :fire: Remove basis2tuple, round_clifford_angle --- examples/zxgraph_simplification.py | 2 -- graphqomb/common.py | 42 ------------------------------ 2 files changed, 44 deletions(-) diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py index 8e5e71a9f..0eb0a42c8 100644 --- a/examples/zxgraph_simplification.py +++ b/examples/zxgraph_simplification.py @@ -90,8 +90,6 @@ def print_meas_bses(graph: ZXGraphState) -> None: # Note that we need to call the `expand_local_cliffords` method before generating the pattern to get the gflow. zx_graph_smp.expand_local_cliffords() -zx_graph_smp.to_xy() # to improve gflow search performance -zx_graph_smp.to_xz() # to improve gflow search performance print("input_node_indices: ", set(zx_graph_smp.input_node_indices)) print("output_node_indices: ", set(zx_graph_smp.output_node_indices)) print("local_cliffords: ", zx_graph_smp.local_cliffords) diff --git a/graphqomb/common.py b/graphqomb/common.py index 883fc8cd4..b55ca78b5 100644 --- a/graphqomb/common.py +++ b/graphqomb/common.py @@ -397,45 +397,3 @@ def meas_basis(plane: Plane, angle: float) -> NDArray[np.complex128]: else: typing_extensions.assert_never(plane) return basis.astype(np.complex128) - - -def basis2tuple(meas_basis: MeasBasis) -> tuple[Plane, float]: - r"""Return the key (tuple[Plane, float]) for _EQUIV_MEAS_BASIS_MAP from a measurement basis. - - Parameters - ---------- - meas_basis : `MeasBasis` - measurement basis - - Returns - ------- - tuple[Plane, float] - key for _EQUIV_MEAS_BASIS_MAP - """ - angle = round_clifford_angle(meas_basis.angle) - return (meas_basis.plane, angle) - - -def round_clifford_angle(angle: float, atol: float = 1e-9) -> float: - r"""Round the Clifford angle numerically to the nearest Clifford angle. - - Parameters - ---------- - angle : `float` - angle in radians - atol : `float`, optional - absolute tolerance, by default 1e-9 - - Returns - ------- - angle : `float` - For Clifford angles, the rounded angle. - If the angle is not a Clifford angle, return the original angle. - """ - clifford_angles = [0.0, np.pi / 2, np.pi, 3 * np.pi / 2] - for ca in clifford_angles: - if is_close_angle(angle, ca, atol): - angle = ca - break - - return angle From b65529d453a1194dc54a139d3ad6d277657aaa01 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 02:54:49 +0900 Subject: [PATCH 89/94] :fire: Remove pivot_on_boundary --- graphqomb/zxgraphstate.py | 59 --------------------------------------- 1 file changed, 59 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index 7b9c424f5..ac547ab4f 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -206,7 +206,6 @@ def _clifford_rules(self) -> tuple[CliffordRule, ...]: min(self.neighbors(node) - set(self.input_node_indices) - set(self.output_node_indices)), ), ), - (self._needs_pivot_on_boundary, self.pivot_on_boundary), ) def _update_connections( @@ -488,63 +487,6 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: case_b = self.meas_bases[node].plane == Plane.XZ and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) return case_a or case_b - def _needs_pivot_on_boundary(self, node: int, atol: float = 1e-9) -> bool: - """Check if the node is non-input and all neighbors are input or output nodes. - - If True, pivot operation is performed on the node and its non-input neighbor, and then the node will be removed. - - For this operation, one of the following must hold: - (a) measurement plane = XY and measurement angle = 0 or pi (mod 2pi) - (b) measurement plane = XZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) - - Parameters - ---------- - node : `int` - node index - atol : `float`, optional - absolute tolerance, by default 1e-9 - - Returns - ------- - `bool` - True if the node is non-input and all neighbors are input or output nodes. - - Notes - ----- - In order to follow the algorithm in Theorem 4.12 of Quantum 5, 421 (2021), - this function is not commonalized into _needs_pivot. - - References - ---------- - [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.11 - """ - non_input_nbrs = self.neighbors(node) - set(self.input_node_indices) - # check non_input_nbrs is composed of only output nodes and is not empty - if not (non_input_nbrs.issubset(set(self.output_node_indices)) and non_input_nbrs): - return False - - alpha = self.meas_bases[node].angle % (2.0 * np.pi) - # (a) measurement plane = XY and measurement angle = 0 or pi (mod 2pi) - case_a = self.meas_bases[node].plane == Plane.XY and is_close_angle(2 * alpha, 0, atol) - # (b) measurement plane = XZ and measurement angle = 0.5 pi or 1.5 pi (mod 2pi) - case_b = self.meas_bases[node].plane == Plane.XZ and is_close_angle(2 * (alpha - np.pi / 2), 0, atol) - return case_a or case_b - - def pivot_on_boundary(self, node: int) -> None: - """Perform the Clifford node removal on a corner case. - - Parameters - ---------- - node : `int` - node index - - References - ---------- - [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.11 - """ - output_nbr = min(self.neighbors(node) - set(self.input_node_indices)) - self.pivot(node, output_nbr) - def _remove_clifford(self, node: int, atol: float = 1e-9) -> None: """Perform the Clifford node removal. @@ -656,7 +598,6 @@ def is_removable_clifford(self, node: int, atol: float = 1e-9) -> bool: self._is_trivial_meas(node, atol), self._needs_lc(node, atol), self._needs_pivot(node, atol), - self._needs_pivot_on_boundary(node, atol), ] ) From 1b6f15ffb421de72a2b2f019b77c407234dc5196 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 02:56:00 +0900 Subject: [PATCH 90/94] :bug: Fix bug in merge_yz_to_xy and pivot --- graphqomb/zxgraphstate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphqomb/zxgraphstate.py b/graphqomb/zxgraphstate.py index ac547ab4f..ae0b96aea 100644 --- a/graphqomb/zxgraphstate.py +++ b/graphqomb/zxgraphstate.py @@ -476,7 +476,7 @@ def _needs_pivot(self, node: int, atol: float = 1e-9) -> bool: ---------- [1] Backens et al., Quantum 5, 421 (2021); arXiv:2003.01664v3 [quant-ph]. Lemma 4.9 """ - non_input_nbrs = self.neighbors(node) - set(self.input_node_indices) + non_input_nbrs = self.neighbors(node) - set(self.input_node_indices) - set(self.output_node_indices) if not non_input_nbrs: return False @@ -686,7 +686,7 @@ def merge_yz_to_xy(self) -> None: for u in target_nodes: (v,) = self.neighbors(u) new_angle = (self.meas_bases[u].angle - self.meas_bases[v].angle) % (2.0 * np.pi) - self.assign_meas_basis(v, PlannerMeasBasis(Plane.XY, new_angle)) + self.assign_meas_basis(v, PlannerMeasBasis(Plane.XY, -new_angle)) self.remove_physical_node(u) def merge_yz_nodes(self) -> None: From b3a5bc371911e6b771df8d4b0fe6a179962f4b64 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 03:00:13 +0900 Subject: [PATCH 91/94] :art: Fix input/output node indices --- graphqomb/graphstate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphqomb/graphstate.py b/graphqomb/graphstate.py index f5982fc53..bc59ab9ac 100644 --- a/graphqomb/graphstate.py +++ b/graphqomb/graphstate.py @@ -170,8 +170,8 @@ def check_canonical_form(self) -> None: class GraphState(BaseGraphState): """Minimal implementation of GraphState.""" - _input_node_indices: dict[int, int] - _output_node_indices: dict[int, int] + __input_node_indices: dict[int, int] + __output_node_indices: dict[int, int] __physical_nodes: set[int] __physical_edges: dict[int, set[int]] __meas_bases: dict[int, MeasBasis] From 9f50f1d7f1fbde3df7e53786c0f76eb2de92c270 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 03:04:42 +0900 Subject: [PATCH 92/94] :fire: Remove old test --- tests/test_zxgraphstate.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 60e9bbb8e..02fd2bed8 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -924,30 +924,6 @@ def test_remove_cliffords_graph2(zx_graph: ZXGraphState) -> None: _test(zx_graph, {0, 2}, {(0, 2)}, exp_measurements=exp_measurements) -@pytest.mark.parametrize( - "planes", - list( - itertools.product( - list(Plane), - [Plane.XY], - ) - ), -) -def test_needs_pivot_on_boundary_xy( - zx_graph: ZXGraphState, - planes: tuple[Plane, Plane], - rng: np.random.Generator, -) -> None: - graph_2(zx_graph) - zx_graph.register_input(0, q_index=0) - zx_graph.register_output(2, q_index=0) - angles = [rng.random() * 2 * np.pi for _ in range(2)] - angles[1] = rng.choice([0.0, np.pi]) - measurements = [(i, PlannerMeasBasis(planes[i], angles[i])) for i in range(2)] - _apply_measurements(zx_graph, measurements) - assert zx_graph._needs_pivot_on_boundary(1, atol=1e-9) is True - - @pytest.mark.parametrize( "planes", list( From 3e43e5416e5f2f5db218359dd5b9674ee9fa9a65 Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 03:06:19 +0900 Subject: [PATCH 93/94] :fire: Remove old test --- tests/test_zxgraphstate.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 02fd2bed8..6351c8cda 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -924,30 +924,6 @@ def test_remove_cliffords_graph2(zx_graph: ZXGraphState) -> None: _test(zx_graph, {0, 2}, {(0, 2)}, exp_measurements=exp_measurements) -@pytest.mark.parametrize( - "planes", - list( - itertools.product( - list(Plane), - [Plane.XZ], - ) - ), -) -def test_needs_pivot_on_boundary_xz( - zx_graph: ZXGraphState, - planes: tuple[Plane, Plane], - rng: np.random.Generator, -) -> None: - graph_2(zx_graph) - zx_graph.register_input(0, q_index=0) - zx_graph.register_output(2, q_index=0) - angles = [rng.random() * 2 * np.pi for _ in range(2)] - angles[1] = rng.choice([0.5 * np.pi, 1.5 * np.pi]) - measurements = [(i, PlannerMeasBasis(planes[i], angles[i])) for i in range(2)] - _apply_measurements(zx_graph, measurements) - assert zx_graph._needs_pivot_on_boundary(1, atol=1e-9) is True - - def test_remove_cliffords_graph3(zx_graph: ZXGraphState) -> None: graph_3(zx_graph) measurements = [ From 5be19240441ddec8cf888f1939ed296f8b1ccfef Mon Sep 17 00:00:00 2001 From: nabe98 Date: Sat, 13 Dec 2025 03:12:10 +0900 Subject: [PATCH 94/94] :memo: Fixed docs for gflow_wrapper --- examples/zxgraph_simplification.py | 6 ------ graphqomb/gflow_utils.py | 4 ---- 2 files changed, 10 deletions(-) diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py index 0eb0a42c8..6123e7f9f 100644 --- a/examples/zxgraph_simplification.py +++ b/examples/zxgraph_simplification.py @@ -102,12 +102,6 @@ def print_meas_bses(graph: ZXGraphState) -> None: # Now we can obtain the gflow for the simplified graph state. # Then, we compile the simplified graph state into a measurement pattern, # simulate it, and get the resulting statevector. - -# NOTE: -# gflow_wrapper does not support graph states with multiple subgraph structures in the gflow search wrapper below. -# Hence, in case you fail, ensure that the simplified graph state consists of a single connected component. -# To calculate the graph states with multiple subgraph structures, -# you need to calculate gflow for each connected component separately. gflow_smp = gflow_wrapper(zx_graph_smp) pattern_smp = qompile(zx_graph_smp, gflow_smp) sim_smp = PatternSimulator(pattern_smp, backend=SimulatorBackend.StateVector) diff --git a/graphqomb/gflow_utils.py b/graphqomb/gflow_utils.py index f0b81ec1d..a107db716 100644 --- a/graphqomb/gflow_utils.py +++ b/graphqomb/gflow_utils.py @@ -40,10 +40,6 @@ def gflow_wrapper(graphstate: BaseGraphState) -> dict[int, set[int]]: ------ ValueError If no gflow is found - - Notes - ----- - This wrapper does not support graph states with multiple subgraph structures. """ graph: NxGraph[int] = nx.Graph() graph.add_nodes_from(graphstate.physical_nodes)