diff --git a/examples/zxgraph_simplification.py b/examples/zxgraph_simplification.py new file mode 100644 index 000000000..d3090b9fc --- /dev/null +++ b/examples/zxgraph_simplification.py @@ -0,0 +1,43 @@ +"""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. +""" + +# %% +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 ZXGraphState + +# %% +circ = random_circ(4, 4) +graph, flow = circuit2graph(circ) +zx_graph = ZXGraphState() +zx_graph.append(graph) + +visualize(zx_graph) +print("node | plane | angle (/pi)") +for node in zx_graph.input_nodes: + print(f"{node} (input)", zx_graph.meas_bases[node].plane, zx_graph.meas_bases[node].angle / np.pi) +for node in zx_graph.physical_nodes - zx_graph.input_nodes - zx_graph.output_nodes: + print(node, zx_graph.meas_bases[node].plane, zx_graph.meas_bases[node].angle / np.pi) + +# %% +zx_graph_smp = deepcopy(zx_graph) +zx_graph_smp.full_reduce() + +visualize(zx_graph_smp) +print("node | plane | angle (/pi)") +for node in zx_graph.input_nodes: + print(f"{node} (input)", zx_graph.meas_bases[node].plane, zx_graph.meas_bases[node].angle / np.pi) +for node in zx_graph_smp.physical_nodes - zx_graph.input_nodes - zx_graph_smp.output_nodes: + print(node, zx_graph_smp.meas_bases[node].plane, zx_graph_smp.meas_bases[node].angle / np.pi) + +# %% diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index c48d54686..188cbc9ab 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -400,6 +400,28 @@ def local_cliffords(self) -> dict[int, LocalClifford]: """ return self.__local_cliffords + @property + def inner2nodes(self) -> dict[int, int]: + """Return inner index to node index mapping. + + Returns + ------- + dict[int, int] + inner index to node index mapping. + """ + return self.__inner2nodes + + @property + def nodes2inner(self) -> dict[int, int]: + """Return node index to inner index mapping. + + Returns + ------- + dict[int, int] + node index to inner index mapping. + """ + return self.__nodes2inner + def check_meas_basis(self) -> None: """Check if the measurement basis is set for all physical nodes except output nodes. diff --git a/graphix_zx/random_objects.py b/graphix_zx/random_objects.py index b5a368c50..3550ac555 100644 --- a/graphix_zx/random_objects.py +++ b/graphix_zx/random_objects.py @@ -2,6 +2,7 @@ This module provides: - get_random_flow_graph: Generate a random flow graph. +- random_circ: Generate a random MBQC circuit. """ from __future__ import annotations @@ -10,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 +86,48 @@ def get_random_flow_graph( num_nodes += 1 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: + """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 + list of angles, by default (0, np.pi / 3, 2 * np.pi / 3, np.pi) + + Returns + ------- + MBQCCircuit + generated MBQC circuit + """ + if rng is None: + rng = np.random.default_rng() + 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 = set(rng.choice(range(width), num)) + circ.phase_gadget(target, rng.choice(angle_candidates)) + + return circ diff --git a/graphix_zx/zxgraphstate.py b/graphix_zx/zxgraphstate.py index e67724f19..a65bb4fb4 100644 --- a/graphix_zx/zxgraphstate.py +++ b/graphix_zx/zxgraphstate.py @@ -7,16 +7,18 @@ from __future__ import annotations +from collections import defaultdict +from functools import cached_property from typing import TYPE_CHECKING import numpy as np from graphix_zx.common import Plane, PlannerMeasBasis -from graphix_zx.euler import is_clifford_angle +from graphix_zx.euler import LocalClifford, _is_close_angle, is_clifford_angle from graphix_zx.graphstate import GraphState, bipartite_edges if TYPE_CHECKING: - from collections.abc import Callable, Iterable, Mapping + from collections.abc import Callable, Iterable class ZXGraphState(GraphState): @@ -30,18 +32,35 @@ class ZXGraphState(GraphState): set of output nodes physical_nodes : set[int] set of physical nodes - physical_edges : dict[int, set[int]] + physical_edges : set[tuple[int]] physical edges meas_bases : dict[int, MeasBasis] q_indices : dict[int, int] qubit indices local_cliffords : dict[int, LocalClifford] local clifford operators + _clifford_rules : tuple[tuple[Callable[[int, float], bool], Callable[[int], None]], ...] + tuple of rules (check_func, action_func) for removing local clifford nodes """ def __init__(self) -> None: super().__init__() + @cached_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.get_neighbors(node) - self.input_nodes)), + ), + ) + def _update_connections(self, rmv_edges: Iterable[tuple[int, int]], new_edges: Iterable[tuple[int, int]]) -> None: """Update the physical edges of the graph state. @@ -57,23 +76,6 @@ def _update_connections(self, rmv_edges: Iterable[tuple[int, int]], new_edges: I for edge in new_edges: self.add_physical_edge(edge[0], edge[1]) - def _update_node_measurement( - self, measurement_action: Mapping[Plane, tuple[Plane, Callable[[float], float]]], v: int - ) -> None: - """Update the measurement action of the node. - - Parameters - ---------- - measurement_action : Mapping[Plane, tuple[Plane, Callable[[float], float]]] - mapping of the measurement plane to the new measurement plane and function to update the angle - v : int - node index - """ - new_plane, new_angle_func = measurement_action[self.meas_bases[v].plane] - if new_plane: - new_angle = new_angle_func(v) % (2.0 * np.pi) - self.set_meas_basis(v, PlannerMeasBasis(new_plane, new_angle)) - def local_complement(self, node: int) -> None: """Local complement operation on the graph state: G*u. @@ -101,23 +103,14 @@ def local_complement(self, node: int) -> None: self._update_connections(rmv_edges, new_edges) # update node measurement if not output node - measurement_action = { - Plane.XY: (Plane.XZ, lambda v: (0.5 * np.pi - self.meas_bases[v].angle) % (2.0 * np.pi)), - Plane.XZ: (Plane.XY, lambda v: (self.meas_bases[v].angle - 0.5 * np.pi) % (2.0 * np.pi)), - Plane.YZ: (Plane.YZ, lambda v: (self.meas_bases[v].angle + 0.5 * np.pi) % (2.0 * np.pi)), - } + lc = LocalClifford(0, np.pi / 2, 0) if node not in self.output_nodes: - self._update_node_measurement(measurement_action, node) + self.apply_local_clifford(node, lc) # update neighbors measurement if not output node - measurement_action = { - Plane.XY: (Plane.XY, lambda v: (self.meas_bases[v].angle - 0.5 * np.pi) % (2.0 * np.pi)), - Plane.XZ: (Plane.YZ, lambda v: self.meas_bases[v].angle), - Plane.YZ: (Plane.XZ, lambda v: -self.meas_bases[v].angle % (2.0 * np.pi)), - } - + lc = LocalClifford(-np.pi / 2, 0, 0) for v in nbrs - self.output_nodes: - self._update_node_measurement(measurement_action, v) + self.apply_local_clifford(v, lc) def _swap(self, node1: int, node2: int) -> None: """Swap nodes u and v in the graph state. @@ -181,24 +174,14 @@ def pivot(self, node1: int, node2: int) -> None: self._swap(node1, node2) # update node1 and node2 measurement - measurement_action = { - Plane.XY: (Plane.YZ, lambda v: self.meas_bases[v].angle), - Plane.XZ: (Plane.XZ, lambda v: (0.5 * np.pi - self.meas_bases[v].angle)), - Plane.YZ: (Plane.XY, lambda v: self.meas_bases[v].angle), - } - + lc = LocalClifford(np.pi / 2, np.pi / 2, np.pi / 2) for a in {node1, node2} - self.output_nodes: - self._update_node_measurement(measurement_action, a) + self.apply_local_clifford(a, lc) # update nodes measurement of nbr_a - measurement_action = { - Plane.XY: (Plane.XY, lambda v: (self.meas_bases[v].angle + np.pi) % (2.0 * np.pi)), - Plane.XZ: (Plane.XZ, lambda v: -self.meas_bases[v].angle % (2.0 * np.pi)), - Plane.YZ: (Plane.YZ, lambda v: -self.meas_bases[v].angle % (2.0 * np.pi)), - } - + lc = LocalClifford(np.pi, 0, 0) for w in nbr_a - self.output_nodes: - self._update_node_measurement(measurement_action, w) + 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. @@ -216,10 +199,10 @@ def _needs_nop(self, node: int, atol: float = 1e-9) -> bool: Returns ------- bool - True if the node is a removable Clifford vertex. + True if the node is a removable Clifford node. """ alpha = self.meas_bases[node].angle % (2.0 * np.pi) - return abs(alpha % np.pi) < 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}) 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. @@ -240,16 +223,18 @@ def _needs_lc(self, node: int, atol: float = 1e-9) -> bool: True if the node needs a local complementation. """ alpha = self.meas_bases[node].angle % (2.0 * np.pi) - return abs((alpha + 0.5 * np.pi) % np.pi) < atol and self.meas_bases[node].plane in {Plane.YZ, Plane.XY} + 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_1(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. The pivot operation is performed on the non-input neighbor of the node. For this operation, - (i) the measurement angle must be 0 or pi (mod 2pi) and the measurement plane must be XY, + (a) the measurement angle must be 0 or pi (mod 2pi) and the measurement plane must be XY, or - (ii) the measurement angle must be 0.5 pi or 1.5 pi (mod 2pi) and the measurement plane must be XZ. + (b) the measurement angle must be 0.5 pi or 1.5 pi (mod 2pi) and the measurement plane must be XZ. Parameters ---------- @@ -263,46 +248,20 @@ def _needs_pivot_1(self, node: int, atol: float = 1e-9) -> bool: bool True if the nodes need a pivot operation. """ - if not self.get_neighbors(node) - self.input_nodes: - return False - - alpha = self.meas_bases[node].angle % (2.0 * np.pi) - case_a = abs(alpha % np.pi) < atol and self.meas_bases[node].plane == Plane.XY - case_b = abs((alpha + 0.5 * np.pi) % np.pi) < atol and self.meas_bases[node].plane == Plane.XZ - return case_a or case_b - - def _needs_pivot_2(self, node: int, atol: float = 1e-9) -> bool: - """Check if the node needs a pivot operation on output nodes in order to perform _remove_clifford. - - The pivot operation is performed on the non-input but output neighbor of the node. - For this operation, - (i) the measurement angle must be 0 or pi (mod 2pi) and the measurement plane must be XY, - or - (ii) 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 node needs a pivot operation on output nodes. - """ - nbrs = self.get_neighbors(node) - if not (nbrs.issubset(self.output_nodes) and nbrs): - return False + if not (self.get_neighbors(node) - self.input_nodes): + nbrs = self.get_neighbors(node) + if not (nbrs.issubset(self.output_nodes) and nbrs): + return False alpha = self.meas_bases[node].angle % (2.0 * np.pi) - case_a = abs(alpha % np.pi) < atol and self.meas_bases[node].plane == Plane.XY - case_b = abs((alpha + 0.5 * np.pi) % np.pi) < atol and self.meas_bases[node].plane == Plane.XZ + # (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 vertex removal. + """Perform the Clifford node removal. Parameters ---------- @@ -311,34 +270,16 @@ def _remove_clifford(self, node: int, atol: float = 1e-9) -> None: atol : float, optional absolute tolerance, by default 1e-9 """ - alpha = self.meas_bases[node].angle % (2.0 * np.pi) - measurement_action = { - Plane.XY: ( - Plane.XY, - lambda v: self.meas_bases[v].angle - if abs(alpha % (2.0 * np.pi)) < atol - else (self.meas_bases[v].angle + np.pi) % (2.0 * np.pi), - ), - Plane.XZ: ( - Plane.XZ, - lambda v: self.meas_bases[v].angle - if abs(alpha % (2.0 * np.pi)) < atol - else -self.meas_bases[v].angle % (2.0 * np.pi), - ), - Plane.YZ: ( - Plane.YZ, - lambda v: self.meas_bases[v].angle - if abs(alpha % (2.0 * np.pi)) < atol - else -self.meas_bases[v].angle % (2.0 * np.pi), - ), - } + 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.get_neighbors(node) - self.output_nodes: - self._update_node_measurement(measurement_action, v) + 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. + """Remove the local Clifford node. Parameters ---------- @@ -351,40 +292,35 @@ def remove_clifford(self, node: int, atol: float = 1e-9) -> None: ------ ValueError 1. If the node is an input node. - 2. If the node is not a Clifford vertex. + 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_nodes or node in self.output_nodes: - msg = "Clifford vertex removal not allowed for input node" + 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 vertex." + msg = "This node is not a Clifford node." raise ValueError(msg) - if self._needs_nop(node, atol): - pass - elif self._needs_lc(node, atol): - self.local_complement(node) - elif self._needs_pivot_1(node, atol) or self._needs_pivot_2(node, atol): - nbrs = self.get_neighbors(node) - self.input_nodes - v = min(nbrs) - nbrs.remove(v) - self.pivot(node, v) - else: - msg = "This Clifford vertex is unremovable." - raise ValueError(msg) + for check, action in self._clifford_rules: + if not check(node, atol): + continue + action(node) + self._remove_clifford(node, atol) + return - self._remove_clifford(node, atol) + 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 vertex. + """Check if the node is a removable Clifford node. Parameters ---------- @@ -396,116 +332,148 @@ def is_removable_clifford(self, node: int, atol: float = 1e-9) -> bool: Returns ------- bool - True if the node is a removable Clifford vertex. + True if the node is a removable Clifford node. """ return any( [ self._needs_nop(node, atol), self._needs_lc(node, atol), - self._needs_pivot_1(node, atol), - self._needs_pivot_2(node, atol), + self._needs_pivot(node, atol), ] ) - def _remove_cliffords( - self, action_func: Callable[[int, float], None], check_func: Callable[[int, float], bool], atol: float = 1e-9 - ) -> None: - """Remove all local clifford nodes which are specified by the check_func and action_func. + def remove_cliffords(self, atol: float = 1e-9) -> None: + """Remove all local clifford nodes which are removable. Parameters ---------- - action_func : Callable[[int, float], None] - action to perform on the node - check_func : Callable[[int, float], bool] - check if the node is a removable Clifford vertex + atol : float, optional + absolute tolerance, by default 1e-9 """ self.check_meas_basis() - while True: - nodes = self.physical_nodes - self.input_nodes - self.output_nodes - clifford_nodes = [node for node in nodes if check_func(node, atol)] - clifford_node = min(clifford_nodes, default=None) - if clifford_node is None: - break - action_func(clifford_node, atol) + while any( + self.is_removable_clifford(n, atol) for n in (self.physical_nodes - self.input_nodes - self.output_nodes) + ): + for check, action in self._clifford_rules: + while True: + candidates = self.physical_nodes - self.input_nodes - self.output_nodes + 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 _step1_action(self, node: int, atol: float = 1e-9) -> None: - """If _needs_lc is True, apply local complement to the node, and remove it. + def _extract_yz_adjacent_pair(self) -> tuple[int, int] | None: + """Call inside convert_to_phase_gadget. - Parameters - ---------- - node : int - node index - atol : float, optional - absolute tolerance, by default 1e-9 + 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. """ - self.local_complement(node) - self._remove_clifford(node, atol) + 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 _step2_action(self, node: int, atol: float = 1e-9) -> None: - """If _needs_nop is True, remove the node. + def _extract_xz_node(self) -> int | None: + """Call inside convert_to_phase_gadget. - Parameters - ---------- - node : int - node index - atol : float, optional - absolute tolerance, by default 1e-9 - """ - self._remove_clifford(node, atol) + Find a node that is measured in the XZ-plane. - def _step3_action(self, node: int, atol: float = 1e-9) -> None: - """If _needs_pivot_1 is True, apply pivot operation to the node, and remove it. + 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 - Parameters - ---------- - node : int - node index - atol : float, optional - absolute tolerance, by default 1e-9 + 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 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. """ - nbrs = self.get_neighbors(node) - self.input_nodes - nbr = min(nbrs) - self.pivot(node, nbr) - self._remove_clifford(node, atol) + target_candidates = { + u for u, basis in self.meas_bases.items() if (basis.plane == Plane.YZ and len(self.get_neighbors(u)) == 1) + } + target_nodes = { + u + for u in target_candidates + if ( + (v := next(iter(self.get_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.get_neighbors(u) + new_angle = (self.meas_bases[u].angle + self.meas_bases[v].angle) % (2.0 * np.pi) + self.set_meas_basis(v, PlannerMeasBasis(Plane.XY, new_angle)) + self.remove_physical_node(u) - def _step4_action(self, node: int, atol: float = 1e-9) -> None: - """If _needs_pivot_2 is True, apply pivot operation to the node, and remove it. + def merge_yz_nodes(self) -> None: + """Merge isolated YZ-measured nodes into a single node. - Parameters - ---------- - node : int - node index - atol : float, optional - absolute tolerance, by default 1e-9 + 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. """ - nbrs = self.get_neighbors(node) - self.input_nodes - nbr = min(nbrs) - self.pivot(node, nbr) - self._remove_clifford(node, atol) - - def remove_cliffords(self, atol: float = 1e-9) -> None: - """Remove all local clifford nodes which are removable. + 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.get_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.set_meas_basis(nodes[0], PlannerMeasBasis(Plane.YZ, new_angle)) + 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 """ - self.check_meas_basis() while True: - nodes = self.physical_nodes - self.input_nodes - self.output_nodes - clifford_nodes = [ - node - for node in nodes - if is_clifford_angle(self.meas_bases[node].angle, atol) and self.is_removable_clifford(node, atol) - ] - if clifford_nodes == []: + 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 - self.input_nodes - self.output_nodes + ): break - steps = [ - (self._step1_action, self._needs_lc), - (self._step2_action, self._needs_nop), - (self._step3_action, self._needs_pivot_1), - (self._step4_action, self._needs_pivot_2), - ] - for action_func, check_func in steps: - self._remove_cliffords(action_func, check_func, atol) diff --git a/tests/test_euler.py b/tests/test_euler.py index 09e832f6e..bd2eb740a 100644 --- a/tests/test_euler.py +++ b/tests/test_euler.py @@ -1,3 +1,4 @@ +import operator from typing import TYPE_CHECKING import numpy as np @@ -174,7 +175,7 @@ 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), } @@ -195,7 +196,47 @@ def test_local_complement_neighbors(plane: Plane, rng: np.random.Generator) -> N measurement_action: dict[Plane, tuple[Plane, Callable[[float], float]]] = { Plane.XY: (Plane.XY, lambda angle: angle + np.pi / 2), Plane.XZ: (Plane.YZ, lambda angle: angle), - Plane.YZ: (Plane.XZ, lambda angle: -1 * angle), + Plane.YZ: (Plane.XZ, operator.neg), + } + + angle = rng.random() * 2 * np.pi + + meas_basis = PlannerMeasBasis(plane, angle) + result_basis = update_lc_basis(lc.conjugate(), meas_basis) + ref_plane, ref_angle_func = measurement_action[plane] + ref_angle = ref_angle_func(angle) + + assert result_basis.plane == ref_plane + assert _is_close_angle(result_basis.angle, ref_angle) + + +@pytest.mark.parametrize("plane", [Plane.XY, Plane.YZ, Plane.XZ]) +def test_pivot_target_update(plane: Plane, rng: np.random.Generator) -> None: + lc = LocalClifford(np.pi / 2, np.pi / 2, np.pi / 2) + measurement_action: dict[Plane, tuple[Plane, Callable[[float], float]]] = { + Plane.XY: (Plane.YZ, operator.neg), + Plane.XZ: (Plane.XZ, lambda angle: -angle + np.pi / 2), + Plane.YZ: (Plane.XY, operator.neg), + } + + angle = rng.random() * 2 * np.pi + + meas_basis = PlannerMeasBasis(plane, angle) + result_basis = update_lc_basis(lc.conjugate(), meas_basis) + ref_plane, ref_angle_func = measurement_action[plane] + ref_angle = ref_angle_func(angle) + + assert result_basis.plane == ref_plane + assert _is_close_angle(result_basis.angle, ref_angle) + + +@pytest.mark.parametrize("plane", [Plane.XY, Plane.YZ, Plane.XZ]) +def test_pivot_neighbors(plane: Plane, rng: np.random.Generator) -> None: + lc = LocalClifford(np.pi, 0, 0) + measurement_action: dict[Plane, tuple[Plane, Callable[[float], float]]] = { + Plane.XY: (Plane.XY, lambda angle: angle + np.pi), + Plane.XZ: (Plane.XZ, operator.neg), + Plane.YZ: (Plane.YZ, operator.neg), } angle = rng.random() * 2 * np.pi diff --git a/tests/test_zxgraphstate.py b/tests/test_zxgraphstate.py index 0ad3d4882..7f947eb24 100644 --- a/tests/test_zxgraphstate.py +++ b/tests/test_zxgraphstate.py @@ -1,15 +1,92 @@ +"""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 -from graphix_zx.euler import is_clifford_angle +from graphix_zx.euler import _is_close_angle from graphix_zx.random_objects import get_random_flow_graph from graphix_zx.zxgraphstate import ZXGraphState +if TYPE_CHECKING: + 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: @@ -25,7 +102,7 @@ def zx_graph() -> ZXGraphState: def _initialize_graph( zx_graph: ZXGraphState, nodes: range, - edges: list[tuple[int, int]], + edges: set[tuple[int, int]], inputs: tuple[int, ...] = (), outputs: tuple[int, ...] = (), ) -> None: @@ -55,24 +132,24 @@ def _initialize_graph( zx_graph.set_output(i) -def _apply_measurements(zx_graph: ZXGraphState, measurements: list[tuple[int, Plane, float]]) -> None: - for node_id, plane, angle in measurements: +def _apply_measurements(zx_graph: ZXGraphState, measurements: Measurements) -> None: + for node_id, planner_meas_basis in measurements: if node_id in zx_graph.output_nodes: continue - zx_graph.set_meas_basis(node_id, PlannerMeasBasis(plane, angle)) + zx_graph.set_meas_basis(node_id, planner_meas_basis) def _test( zx_graph: ZXGraphState, exp_nodes: set[int], exp_edges: set[tuple[int, int]], - exp_measurements: list[tuple[int, Plane, float]], + exp_measurements: Measurements, ) -> None: assert zx_graph.physical_nodes == exp_nodes assert zx_graph.physical_edges == exp_edges - for node_id, plane, angle in exp_measurements: - assert zx_graph.meas_bases[node_id].plane == plane - assert np.isclose(zx_graph.meas_bases[node_id].angle, angle) + 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: @@ -102,166 +179,121 @@ def test_local_complement_fails_with_input_node(zx_graph: ZXGraphState) -> None: zx_graph.local_complement(1) -def test_local_complement_with_no_edge(zx_graph: ZXGraphState) -> None: - """Test local complement with a graph with no edge.""" +@pytest.mark.parametrize("plane", list(Plane)) +def test_local_complement_with_no_edge(zx_graph: ZXGraphState, plane: Plane, rng: np.random.Generator) -> None: + 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(1) - zx_graph.set_meas_basis(1, PlannerMeasBasis(Plane.XY, 1.1 * np.pi)) - zx_graph.local_complement(1) - assert zx_graph.physical_edges == set() - assert zx_graph.meas_bases[1].plane == Plane.XZ - assert np.isclose(zx_graph.meas_bases[1].angle, 1.4 * np.pi) - - zx_graph.set_meas_basis(1, PlannerMeasBasis(Plane.XZ, 1.1 * np.pi)) - zx_graph.local_complement(1) - # this might be a bug in mypy, as it's useful comparison - assert zx_graph.meas_bases[1].plane == Plane.XY # type: ignore[comparison-overlap] - assert np.isclose(zx_graph.meas_bases[1].angle, 0.6 * np.pi) + zx_graph.set_meas_basis(1, PlannerMeasBasis(plane, angle)) - zx_graph.set_meas_basis(1, PlannerMeasBasis(Plane.YZ, 1.1 * np.pi)) zx_graph.local_complement(1) - assert zx_graph.meas_bases[1].plane == Plane.YZ - assert np.isclose(zx_graph.meas_bases[1].angle, 1.6 * np.pi) + assert zx_graph.physical_edges == set() + assert zx_graph.meas_bases[1].plane == ref_plane + assert _is_close_angle(zx_graph.meas_bases[1].angle, ref_angle) -def test_local_complement_on_output_node(zx_graph: ZXGraphState) -> None: +@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, range(1, 4), [(1, 2), (2, 3)], outputs=(2,)) - measurements = [ - (1, Plane.XY, 1.1 * np.pi), - (3, Plane.YZ, 1.3 * np.pi), - ] + _initialize_graph(zx_graph, range(1, 4), {(1, 2), (2, 3)}, outputs=(2,)) + angle1 = rng.random() * 2 * np.pi + angle3 = rng.random() * 2 * np.pi + measurements = [(1, PlannerMeasBasis(plane1, angle1)), (3, PlannerMeasBasis(plane3, angle3))] _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, Plane.XY, 0.6 * np.pi), - (3, Plane.XZ, 0.7 * np.pi), + (1, PlannerMeasBasis(ref_plane1, ref_angle_func1(measurements[0][1].angle))), + (3, PlannerMeasBasis(ref_plane3, ref_angle_func3(measurements[1][1].angle))), ] _test(zx_graph, exp_nodes={1, 2, 3}, exp_edges={(1, 2), (1, 3), (2, 3)}, exp_measurements=exp_measurements) + assert zx_graph.meas_bases.get(2) is None -def test_local_complement_with_two_nodes_graph(zx_graph: ZXGraphState) -> None: +@pytest.mark.parametrize(("plane1", "plane2"), plane_combinations(2)) +def test_local_complement_with_two_nodes_graph( + zx_graph: ZXGraphState, plane1: Plane, plane2: Plane, rng: np.random.Generator +) -> None: """Test local complement with a graph with two nodes.""" zx_graph.add_physical_node(1) zx_graph.add_physical_node(2) zx_graph.add_physical_edge(1, 2) - zx_graph.set_meas_basis(1, PlannerMeasBasis(Plane.XZ, 1.1 * np.pi)) - zx_graph.set_meas_basis(2, PlannerMeasBasis(Plane.XZ, 1.2 * np.pi)) - original_edges = zx_graph.physical_edges.copy() + angle1 = rng.random() * 2 * np.pi + angle2 = rng.random() * 2 * np.pi + zx_graph.set_meas_basis(1, PlannerMeasBasis(plane1, angle1)) + zx_graph.set_meas_basis(2, PlannerMeasBasis(plane2, angle2)) zx_graph.local_complement(1) - assert zx_graph.physical_edges == original_edges - for node_id, plane, angle in [(1, Plane.XY, 0.6 * np.pi), (2, Plane.YZ, 1.2 * np.pi)]: - assert zx_graph.meas_bases[node_id].plane == plane - assert is_clifford_angle(zx_graph.meas_bases[node_id].angle, angle) + + ref_plane1, ref_angle_func1 = MEAS_ACTION_LC_TARGET[plane1] + ref_plane2, ref_angle_func2 = MEAS_ACTION_LC_NEIGHBORS[plane2] + exp_measurements = [ + (1, PlannerMeasBasis(ref_plane1, ref_angle_func1(angle1))), + (2, PlannerMeasBasis(ref_plane2, ref_angle_func2(angle2))), + ] + _test(zx_graph, exp_nodes={1, 2}, exp_edges={(1, 2)}, exp_measurements=exp_measurements) -def test_local_complement_with_minimal_graph(zx_graph: ZXGraphState) -> None: +@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.""" - zx_graph.add_physical_node(1) - zx_graph.add_physical_node(2) - zx_graph.add_physical_node(3) - zx_graph.add_physical_edge(1, 2) - zx_graph.add_physical_edge(2, 3) - zx_graph.set_meas_basis(1, PlannerMeasBasis(Plane.XY, 1.1 * np.pi)) - zx_graph.set_meas_basis(2, PlannerMeasBasis(Plane.XZ, 1.2 * np.pi)) - zx_graph.set_meas_basis(3, PlannerMeasBasis(Plane.YZ, 1.3 * np.pi)) - original_edges = zx_graph.physical_edges.copy() + for i in range(1, 4): + zx_graph.add_physical_node(i) + for i, j in [(1, 2), (2, 3)]: + zx_graph.add_physical_edge(i, j) + angles = [rng.random() * 2 * np.pi for _ in range(3)] + for i in range(1, 4): + zx_graph.set_meas_basis(i, PlannerMeasBasis(planes[i - 1], angles[i - 1])) zx_graph.local_complement(2) - assert zx_graph.physical_edges == {(1, 2), (2, 3), (1, 3)} + ref_plane1, ref_angle_func1 = MEAS_ACTION_LC_NEIGHBORS[planes[0]] + ref_plane2, ref_angle_func2 = MEAS_ACTION_LC_TARGET[planes[1]] + ref_plane3, ref_angle_func3 = MEAS_ACTION_LC_NEIGHBORS[planes[2]] + ref_angle1 = ref_angle_func1(angles[0]) + ref_angle2 = ref_angle_func2(angles[1]) + ref_angle3 = ref_angle_func3(angles[2]) exp_measurements = [ - (1, Plane.XY, 0.6 * np.pi), - (2, Plane.XY, 0.7 * np.pi), - (3, Plane.XZ, 0.7 * np.pi), + (1, PlannerMeasBasis(ref_plane1, ref_angle1)), + (2, PlannerMeasBasis(ref_plane2, ref_angle2)), + (3, PlannerMeasBasis(ref_plane3, ref_angle3)), ] - for node_id, plane, angle in exp_measurements: - assert zx_graph.meas_bases[node_id].plane == plane - assert is_clifford_angle(zx_graph.meas_bases[node_id].angle, angle) + _test(zx_graph, exp_nodes={1, 2, 3}, exp_edges={(1, 2), (2, 3), (1, 3)}, exp_measurements=exp_measurements) zx_graph.local_complement(2) - assert zx_graph.physical_edges == original_edges + ref_plane1, ref_angle_func1 = MEAS_ACTION_LC_NEIGHBORS[ref_plane1] + ref_plane2, ref_angle_func2 = MEAS_ACTION_LC_TARGET[ref_plane2] + ref_plane3, ref_angle_func3 = MEAS_ACTION_LC_NEIGHBORS[ref_plane3] exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XZ, 1.8 * np.pi), - (3, Plane.YZ, 0.7 * np.pi), + (1, PlannerMeasBasis(ref_plane1, ref_angle_func1(ref_angle1))), + (2, PlannerMeasBasis(ref_plane2, ref_angle_func2(ref_angle2))), + (3, PlannerMeasBasis(ref_plane3, ref_angle_func3(ref_angle3))), ] - for node_id, plane, angle in exp_measurements: - assert zx_graph.meas_bases[node_id].plane == plane - assert np.isclose(zx_graph.meas_bases[node_id].angle, angle) + _test(zx_graph, exp_nodes={1, 2, 3}, exp_edges={(1, 2), (2, 3)}, exp_measurements=exp_measurements) -def test_local_complement_4_times(zx_graph: ZXGraphState) -> None: +@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 local complement is applied 4 times and the graph goes back to the original shape.""" - zx_graph.add_physical_node(1) - zx_graph.add_physical_node(2) - zx_graph.add_physical_node(3) - zx_graph.add_physical_edge(1, 2) - zx_graph.add_physical_edge(2, 3) - zx_graph.set_meas_basis(1, PlannerMeasBasis(Plane.XY, 1.1 * np.pi)) - zx_graph.set_meas_basis(2, PlannerMeasBasis(Plane.XZ, 1.2 * np.pi)) - zx_graph.set_meas_basis(3, PlannerMeasBasis(Plane.YZ, 1.3 * np.pi)) - original_edges = zx_graph.physical_edges.copy() - for _ in range(4): - zx_graph.local_complement(2) - assert zx_graph.physical_edges == original_edges - exp_measurements = [ - (1, Plane.XY, 1.1 * np.pi), - (2, Plane.XZ, 1.2 * np.pi), - (3, Plane.YZ, 1.3 * np.pi), - ] - for node_id, plane, angle in exp_measurements: - assert zx_graph.meas_bases[node_id].plane == plane - assert np.isclose(zx_graph.meas_bases[node_id].angle, angle) - - -def test_local_complement_with_h_shaped_graph(zx_graph: ZXGraphState) -> None: - """Test local complement with an H-shaped graph.""" - for i in range(1, 7): + for i in range(1, 4): zx_graph.add_physical_node(i) - - zx_graph.set_input(1) - zx_graph.set_input(4) - - for i, j in [(1, 2), (2, 3), (2, 5), (4, 5), (5, 6)]: + for i, j in [(1, 2), (2, 3)]: zx_graph.add_physical_edge(i, j) + angles = [rng.random() * 2 * np.pi for _ in range(3)] + for i in range(1, 4): + zx_graph.set_meas_basis(i, PlannerMeasBasis(planes[i - 1], angles[i - 1])) - measurements = [ - (1, Plane.XY, 1.1 * np.pi), - (2, Plane.XZ, 1.2 * np.pi), - (3, Plane.YZ, 1.3 * np.pi), - (4, Plane.XY, 1.4 * np.pi), - (5, Plane.XZ, 1.5 * np.pi), - (6, Plane.YZ, 1.6 * np.pi), - ] - _apply_measurements(zx_graph, measurements) - - original_edges = zx_graph.physical_edges.copy() - zx_graph.local_complement(2) - assert zx_graph.physical_edges == {(1, 2), (1, 3), (1, 5), (2, 3), (2, 5), (3, 5), (4, 5), (5, 6)} - exp_measurements = [ - (1, Plane.XY, 0.6 * np.pi), - (2, Plane.XY, 0.7 * np.pi), - (3, Plane.XZ, 0.7 * np.pi), - (4, Plane.XY, 1.4 * np.pi), - (5, Plane.YZ, 1.5 * np.pi), - (6, Plane.YZ, 1.6 * np.pi), - ] - for node_id, plane, angle in exp_measurements: - assert zx_graph.meas_bases[node_id].plane == plane - assert np.isclose(zx_graph.meas_bases[node_id].angle, angle) + for _ in range(4): + zx_graph.local_complement(2) - zx_graph.local_complement(2) - assert zx_graph.physical_edges == original_edges - exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XZ, 1.8 * np.pi), - (3, Plane.YZ, 0.7 * np.pi), - (4, Plane.XY, 1.4 * np.pi), - (5, Plane.XZ, 0.5 * np.pi), - (6, Plane.YZ, 1.6 * np.pi), - ] - for node_id, plane, angle in exp_measurements: - assert zx_graph.meas_bases[node_id].plane == plane - assert np.isclose(zx_graph.meas_bases[node_id].angle, angle) + exp_measurements = [(i, PlannerMeasBasis(planes[i - 1], angles[i - 1])) for i in range(1, 4)] + _test(zx_graph, exp_nodes={1, 2, 3}, exp_edges={(1, 2), (2, 3)}, exp_measurements=exp_measurements) def test_pivot_fails_with_nonexistent_nodes(zx_graph: ZXGraphState) -> None: @@ -292,9 +324,9 @@ def test_pivot_with_obvious_graph(zx_graph: ZXGraphState) -> None: zx_graph.add_physical_edge(i, j) measurements = [ - (1, Plane.XY, 1.1 * np.pi), - (2, Plane.XZ, 1.2 * np.pi), - (3, Plane.YZ, 1.3 * np.pi), + (1, PlannerMeasBasis(Plane.XY, 1.1 * np.pi)), + (2, PlannerMeasBasis(Plane.XZ, 1.2 * np.pi)), + (3, PlannerMeasBasis(Plane.YZ, 1.3 * np.pi)), ] _apply_measurements(zx_graph, measurements) @@ -309,7 +341,10 @@ def test_pivot_with_obvious_graph(zx_graph: ZXGraphState) -> None: assert planes == original_planes -def test_pivot_with_minimal_graph(zx_graph: ZXGraphState) -> None: +@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.""" # 1---2---3---5 # \ / @@ -320,118 +355,28 @@ def test_pivot_with_minimal_graph(zx_graph: ZXGraphState) -> None: for i, j in [(1, 2), (2, 3), (2, 4), (3, 4), (3, 5)]: zx_graph.add_physical_edge(i, j) - measurements = [ - (1, Plane.XY, 1.1 * np.pi), - (2, Plane.XZ, 1.2 * np.pi), - (3, Plane.YZ, 1.3 * np.pi), - (4, Plane.XY, 1.4 * np.pi), - (5, Plane.XZ, 1.5 * np.pi), - ] + angles = [rng.random() * 2 * np.pi for _ in range(5)] + measurements = [(i, PlannerMeasBasis(planes[i - 1], angles[i - 1])) for i in range(1, 6)] _apply_measurements(zx_graph, measurements) + zx_graph_cp = deepcopy(zx_graph) - original_edges = zx_graph.physical_edges.copy() - expected_edges = {(1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (4, 5)} - zx_graph.pivot(2, 3) - assert zx_graph.physical_edges == expected_edges zx_graph.pivot(2, 3) - assert zx_graph.physical_edges == original_edges - zx_graph.pivot(3, 2) - assert zx_graph.physical_edges == expected_edges - zx_graph.pivot(3, 2) - assert zx_graph.physical_edges == original_edges - - -def test_pivot_with_h_shaped_graph(zx_graph: ZXGraphState) -> None: - """Test pivot with an H-shaped graph.""" - # 3 6 - # | | - # 2---5 - # | | - # 1 4 - for i in range(1, 7): - zx_graph.add_physical_node(i) - for i, j in [(1, 2), (2, 3), (2, 5), (4, 5), (5, 6)]: - zx_graph.add_physical_edge(i, j) - - measurements = [ - (1, Plane.XY, 1.1 * np.pi), - (2, Plane.XZ, 1.2 * np.pi), - (3, Plane.YZ, 1.3 * np.pi), - (4, Plane.XY, 1.4 * np.pi), - (5, Plane.XZ, 1.5 * np.pi), - (6, Plane.YZ, 1.6 * np.pi), - ] - _apply_measurements(zx_graph, measurements) - - original_edges = zx_graph.physical_edges.copy() - expected_edges = {(1, 4), (1, 5), (1, 6), (2, 4), (2, 5), (2, 6), (3, 4), (3, 5), (3, 6)} - zx_graph.pivot(2, 5) - assert zx_graph.physical_edges == expected_edges - zx_graph.pivot(2, 5) - assert zx_graph.physical_edges == original_edges - zx_graph.pivot(5, 2) - assert zx_graph.physical_edges == expected_edges - zx_graph.pivot(5, 2) - assert zx_graph.physical_edges == original_edges - - -def test_pivot_with_8_nodes_graph(zx_graph: ZXGraphState) -> None: - """Test pivot with a graph with 8 nodes.""" - # 1 4 7 - # \ / \ / - # 3 - 6 - # / \ / \ - # 2 5 8 - for i in range(1, 9): - zx_graph.add_physical_node(i) - - for i, j in [(1, 3), (2, 3), (3, 4), (3, 5), (3, 6), (4, 6), (5, 6), (6, 7), (6, 8)]: - zx_graph.add_physical_edge(i, j) - - measurements = [ - (1, Plane.XY, 1.1), - (2, Plane.XZ, 1.2), - (3, Plane.YZ, 1.3), - (4, Plane.XY, 1.4), - (5, Plane.XZ, 1.5), - (6, Plane.YZ, 1.6), - (7, Plane.XY, 1.7), - (8, Plane.XZ, 1.8), - ] - _apply_measurements(zx_graph, measurements) - - original_edges = zx_graph.physical_edges.copy() - expected_edges = { - (1, 4), - (1, 5), - (1, 6), - (1, 7), - (1, 8), - (2, 4), - (2, 5), - (2, 6), - (2, 7), - (2, 8), - (3, 4), - (3, 5), - (3, 6), - (3, 7), - (3, 8), - (4, 6), - (4, 7), - (4, 8), - (5, 6), - (5, 7), - (5, 8), - } - zx_graph.pivot(3, 6) - assert zx_graph.physical_edges == expected_edges - zx_graph.pivot(3, 6) - assert zx_graph.physical_edges == original_edges - zx_graph.pivot(6, 3) - assert zx_graph.physical_edges == expected_edges - zx_graph.pivot(6, 3) - assert zx_graph.physical_edges == original_edges + zx_graph_cp.local_complement(2) + zx_graph_cp.local_complement(3) + zx_graph_cp.local_complement(2) + assert zx_graph.physical_edges == zx_graph_cp.physical_edges + assert zx_graph.meas_bases[2].plane == zx_graph_cp.meas_bases[2].plane + assert zx_graph.meas_bases[3].plane == zx_graph_cp.meas_bases[3].plane + + _, ref_angle_func2 = MEAS_ACTION_PV_TARGET[planes[1]] + _, ref_angle_func3 = MEAS_ACTION_PV_TARGET[planes[2]] + _, ref_angle_func4 = MEAS_ACTION_PV_NEIGHBORS[planes[3]] + ref_angle2 = ref_angle_func2(angles[1]) + ref_angle3 = ref_angle_func3(angles[2]) + ref_angle4 = ref_angle_func4(angles[3]) + assert _is_close_angle(zx_graph.meas_bases[2].angle, ref_angle2) + assert _is_close_angle(zx_graph.meas_bases[3].angle, ref_angle3) + assert _is_close_angle(zx_graph.meas_bases[4].angle, ref_angle4) def test_remove_clifford_fails_if_nonexistent_node(zx_graph: ZXGraphState) -> None: @@ -443,7 +388,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: zx_graph.add_physical_node(1) zx_graph.set_input(1) - with pytest.raises(ValueError, match="Clifford vertex removal not allowed for input node"): + with pytest.raises(ValueError, match="Clifford node removal not allowed for input node"): zx_graph.remove_clifford(1) @@ -454,14 +399,14 @@ def test_remove_clifford_fails_with_invalid_plane(zx_graph: ZXGraphState) -> Non 1, 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 vertex"): + with pytest.raises(ValueError, match="This node is not a Clifford node"): zx_graph.remove_clifford(1) -def test_remove_clifford_fails_for_non_clifford_vertex(zx_graph: ZXGraphState) -> None: +def test_remove_clifford_fails_for_non_clifford_node(zx_graph: ZXGraphState) -> None: zx_graph.add_physical_node(1) zx_graph.set_meas_basis(1, PlannerMeasBasis(Plane.XY, 0.1 * np.pi)) - with pytest.raises(ValueError, match="This node is not a Clifford vertex"): + with pytest.raises(ValueError, match="This node is not a Clifford node"): zx_graph.remove_clifford(1) @@ -470,13 +415,13 @@ def graph_1(zx_graph: ZXGraphState) -> None: # 4---1---2 4 2 # | -> # 3 3 - _initialize_graph(zx_graph, nodes=range(1, 5), edges=[(1, 2), (1, 3), (1, 4)]) + _initialize_graph(zx_graph, nodes=range(1, 5), edges={(1, 2), (1, 3), (1, 4)}) def graph_2(zx_graph: ZXGraphState) -> None: # _needs_lc # 1---2---3 -> 1---3 - _initialize_graph(zx_graph, nodes=range(1, 4), edges=[(1, 2), (2, 3)]) + _initialize_graph(zx_graph, nodes=range(1, 4), edges={(1, 2), (2, 3)}) def graph_3(zx_graph: ZXGraphState) -> None: @@ -487,7 +432,7 @@ def graph_3(zx_graph: ZXGraphState) -> None: # \ / \ | / # 5(I) 5(I) _initialize_graph( - zx_graph, nodes=range(1, 7), edges=[(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)], inputs=(1, 4, 5) + zx_graph, nodes=range(1, 7), edges={(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)}, inputs=(1, 4, 5) ) @@ -499,7 +444,7 @@ def graph_4(zx_graph: ZXGraphState) -> None: _initialize_graph( zx_graph, nodes=range(1, 6), - edges=[(1, 2), (2, 3), (2, 4), (3, 4), (4, 5)], + edges={(1, 2), (2, 3), (2, 4), (3, 4), (4, 5)}, inputs=(1,), outputs=(2, 3, 5), ) @@ -508,9 +453,9 @@ def graph_4(zx_graph: ZXGraphState) -> None: def _test_remove_clifford( zx_graph: ZXGraphState, node: int, - measurements: list[tuple[int, Plane, float]], + measurements: Measurements, exp_graph: tuple[set[int], set[tuple[int, int]]], - exp_measurements: list[tuple[int, Plane, float]], + exp_measurements: Measurements, ) -> None: _apply_measurements(zx_graph, measurements) zx_graph.remove_clifford(node) @@ -519,362 +464,53 @@ def _test_remove_clifford( _test(zx_graph, exp_nodes, exp_edges, exp_measurements) -def test_remove_clifford_removable_with_xz_0(zx_graph: ZXGraphState) -> None: - """Test removing a removable Clifford vertex with measurement plane XZ and angle 0.""" - graph_1(zx_graph) - measurements = [ - (1, Plane.XZ, 0), - (2, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - ] - exp_measurements = [ - (2, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - ] - _test_remove_clifford( - zx_graph, node=1, measurements=measurements, exp_graph=({2, 3, 4}, set()), exp_measurements=exp_measurements - ) - - -def test_remove_clifford_removable_with_xz_pi(zx_graph: ZXGraphState) -> None: - """Test removing a removable Clifford vertex with measurement plane XZ and angle pi.""" - graph_1(zx_graph) - measurements = [ - (1, Plane.XZ, np.pi), - (2, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - ] - exp_measurements = [ - (2, Plane.XY, 1.1 * np.pi), - (3, Plane.XZ, 1.8 * np.pi), - (4, Plane.YZ, 1.7 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=1, - measurements=measurements, - exp_graph=({2, 3, 4}, set()), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_removable_with_yz_0(zx_graph: ZXGraphState) -> None: - """Test removing a removable Clifford vertex with measurement plane YZ and angle 0.""" - graph_1(zx_graph) - measurements = [ - (1, Plane.YZ, 0), - (2, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - ] - exp_measurements = [ - (2, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=1, - measurements=measurements, - exp_graph=({2, 3, 4}, set()), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_removable_with_yz_pi(zx_graph: ZXGraphState) -> None: - """Test removing a removable Clifford vertex with measurement plane YZ and angle pi.""" - graph_1(zx_graph) - measurements = [ - (1, Plane.YZ, np.pi), - (2, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - ] - exp_measurements = [ - (2, Plane.XY, 1.1 * np.pi), - (3, Plane.XZ, 1.8 * np.pi), - (4, Plane.YZ, 1.7 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=1, - measurements=measurements, - exp_graph=({2, 3, 4}, set()), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_lc_with_xy_0p5_pi(zx_graph: ZXGraphState) -> None: - graph_2(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XY, 0.5 * np.pi), - (3, Plane.YZ, 0.2 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 1.6 * np.pi), - (3, Plane.XZ, 1.8 * np.pi), - ] - _test_remove_clifford( - zx_graph, node=2, measurements=measurements, exp_graph=({1, 3}, {(1, 3)}), exp_measurements=exp_measurements - ) - - -def test_remove_clifford_lc_with_xy_1p5_pi(zx_graph: ZXGraphState) -> None: - graph_2(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XY, 1.5 * np.pi), - (3, Plane.YZ, 0.2 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 0.6 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - ] - _test_remove_clifford( - zx_graph, node=2, measurements=measurements, exp_graph=({1, 3}, {(1, 3)}), exp_measurements=exp_measurements - ) - - -def test_remove_clifford_lc_with_yz_0p5_pi(zx_graph: ZXGraphState) -> None: - graph_2(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.YZ, 0.5 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 0.6 * np.pi), - (3, Plane.YZ, 1.8 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=2, - measurements=measurements, - exp_graph=({1, 3}, {(1, 3)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_lc_with_yz_1p5_pi(zx_graph: ZXGraphState) -> None: +@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) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.YZ, 1.5 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 1.6 * np.pi), - (3, Plane.YZ, 0.2 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=2, - measurements=measurements, - exp_graph=({1, 3}, {(1, 3)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot1_with_xy_0(zx_graph: ZXGraphState) -> None: - graph_3(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XY, 0), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 0.5 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.3 * np.pi), - (4, Plane.YZ, 1.7 * np.pi), - (5, Plane.XY, 1.4 * np.pi), - (6, Plane.XZ, 0.5 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=2, - measurements=measurements, - exp_graph=({1, 3, 4, 5, 6}, {(1, 3), (1, 4), (1, 5), (1, 6), (3, 4), (3, 5), (4, 6), (5, 6)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot1_with_xy_pi(zx_graph: ZXGraphState) -> None: - graph_3(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XY, np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 0.5 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 1.7 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 1.5 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=2, - measurements=measurements, - exp_graph=({1, 3, 4, 5, 6}, {(1, 3), (1, 4), (1, 5), (1, 6), (3, 4), (3, 5), (4, 6), (5, 6)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot1_with_xz_0p5_pi(zx_graph: ZXGraphState) -> None: - graph_3(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XZ, 0.5 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 0.5 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.3 * np.pi), - (4, Plane.YZ, 1.7 * np.pi), - (5, Plane.XY, 1.4 * np.pi), - (6, Plane.XZ, 0.5 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=2, - measurements=measurements, - exp_graph=({1, 3, 4, 5, 6}, {(1, 3), (1, 4), (1, 5), (1, 6), (3, 4), (3, 5), (4, 6), (5, 6)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot1_with_xz_1p5_pi(zx_graph: ZXGraphState) -> None: - graph_3(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XZ, 1.5 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 0.5 * np.pi), - ] + 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 - 1], angles[i - 1])) for i in range(1, 4)] + ref_plane1, ref_angle_func1 = MEAS_ACTION_RC[planes[0]] + ref_plane3, ref_angle_func3 = MEAS_ACTION_RC[planes[2]] + ref_angle1 = ref_angle_func1(angles[1], angles[0]) + ref_angle3 = ref_angle_func3(angles[1], angles[2]) exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 1.7 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 1.5 * np.pi), + (1, PlannerMeasBasis(ref_plane1, ref_angle1)), + (3, PlannerMeasBasis(ref_plane3, ref_angle3)), ] _test_remove_clifford( - zx_graph, - node=2, - measurements=measurements, - exp_graph=({1, 3, 4, 5, 6}, {(1, 3), (1, 4), (1, 5), (1, 6), (3, 4), (3, 5), (4, 6), (5, 6)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot2_with_xy_0(zx_graph: ZXGraphState) -> None: - graph_4(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (4, Plane.XY, 0), - ] - exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=4, - measurements=measurements, - exp_graph=({1, 2, 3, 5}, {(1, 3), (1, 5), (2, 3), (2, 5), (3, 5)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot2_with_xy_pi(zx_graph: ZXGraphState) -> None: - graph_4(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (4, Plane.XY, np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 1.1 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=4, - measurements=measurements, - exp_graph=({1, 2, 3, 5}, {(1, 3), (1, 5), (2, 3), (2, 5), (3, 5)}), - exp_measurements=exp_measurements, + zx_graph, node=2, measurements=measurements, exp_graph=({1, 3}, set()), exp_measurements=exp_measurements ) -def test_remove_clifford_pivot2_with_xz_0p5_pi(zx_graph: ZXGraphState) -> None: - graph_4(zx_graph) +def test_unremovable_clifford_node(zx_graph: ZXGraphState) -> None: + _initialize_graph(zx_graph, nodes=range(1, 4), edges={(1, 2), (2, 3)}, inputs=(1, 3)) measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (4, Plane.XZ, 0.5 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=4, - measurements=measurements, - exp_graph=({1, 2, 3, 5}, {(1, 3), (1, 5), (2, 3), (2, 5), (3, 5)}), - exp_measurements=exp_measurements, - ) - - -def test_remove_clifford_pivot2_with_xz_1p5_pi(zx_graph: ZXGraphState) -> None: - graph_4(zx_graph) - measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (4, Plane.XZ, 1.5 * np.pi), - ] - exp_measurements = [ - (1, Plane.XY, 1.1 * np.pi), - ] - _test_remove_clifford( - zx_graph, - node=4, - measurements=measurements, - exp_graph=({1, 2, 3, 5}, {(1, 3), (1, 5), (2, 3), (2, 5), (3, 5)}), - exp_measurements=exp_measurements, - ) - - -def test_unremovable_clifford_vertex(zx_graph: ZXGraphState) -> None: - _initialize_graph(zx_graph, nodes=range(1, 4), edges=[(1, 2), (2, 3)], inputs=(1, 3)) - measurements = [ - (1, Plane.XY, 0.5 * np.pi), - (2, Plane.XY, np.pi), - (3, Plane.XY, 0.5 * np.pi), + (1, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)), ] _apply_measurements(zx_graph, measurements) - with pytest.raises(ValueError, match=r"This Clifford vertex is unremovable."): + with pytest.raises(ValueError, match=r"This Clifford node is unremovable."): zx_graph.remove_clifford(2) def test_remove_cliffords(zx_graph: ZXGraphState) -> None: - """Test removing multiple Clifford vertices.""" - _initialize_graph(zx_graph, nodes=range(1, 5), edges=[(1, 2), (1, 3), (1, 4)]) + """Test removing multiple Clifford nodes.""" + _initialize_graph(zx_graph, nodes=range(1, 5), edges={(1, 2), (1, 3), (1, 4)}) measurements = [ - (1, Plane.XY, 0.5 * np.pi), - (2, Plane.XY, 0.5 * np.pi), - (3, Plane.XY, 0.5 * np.pi), - (4, 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)), + (4, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)), ] _apply_measurements(zx_graph, measurements) zx_graph.remove_cliffords() @@ -882,18 +518,18 @@ def test_remove_cliffords(zx_graph: ZXGraphState) -> None: def test_remove_cliffords_graph1(zx_graph: ZXGraphState) -> None: - """Test removing multiple Clifford vertices.""" + """Test removing multiple Clifford nodes.""" graph_1(zx_graph) measurements = [ - (1, Plane.YZ, np.pi), - (2, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), + (1, PlannerMeasBasis(Plane.YZ, np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.1 * np.pi)), + (3, PlannerMeasBasis(Plane.XZ, 0.2 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 0.3 * np.pi)), ] exp_measurements = [ - (2, Plane.XY, 1.1 * np.pi), - (3, Plane.XZ, 1.8 * np.pi), - (4, Plane.YZ, 1.7 * np.pi), + (2, PlannerMeasBasis(Plane.XY, 1.1 * np.pi)), + (3, PlannerMeasBasis(Plane.XZ, 1.8 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 1.7 * np.pi)), ] _apply_measurements(zx_graph, measurements) zx_graph.remove_cliffords() @@ -903,13 +539,13 @@ def test_remove_cliffords_graph1(zx_graph: ZXGraphState) -> None: def test_remove_cliffords_graph2(zx_graph: ZXGraphState) -> None: graph_2(zx_graph) measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.YZ, 1.5 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), + (1, PlannerMeasBasis(Plane.XY, 0.1 * np.pi)), + (2, PlannerMeasBasis(Plane.YZ, 1.5 * np.pi)), + (3, PlannerMeasBasis(Plane.XZ, 0.2 * np.pi)), ] exp_measurements = [ - (1, Plane.XY, 1.6 * np.pi), - (3, Plane.YZ, 0.2 * np.pi), + (1, PlannerMeasBasis(Plane.XY, 0.6 * np.pi)), + (3, PlannerMeasBasis(Plane.YZ, 0.2 * np.pi)), ] _apply_measurements(zx_graph, measurements) zx_graph.remove_cliffords() @@ -919,19 +555,19 @@ def test_remove_cliffords_graph2(zx_graph: ZXGraphState) -> None: def test_remove_cliffords_graph3(zx_graph: ZXGraphState) -> None: graph_3(zx_graph) measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (2, Plane.XZ, 1.5 * np.pi), - (3, Plane.XZ, 0.2 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 0.5 * np.pi), + (1, PlannerMeasBasis(Plane.XY, 0.1 * np.pi)), + (2, PlannerMeasBasis(Plane.XZ, 1.5 * np.pi)), + (3, PlannerMeasBasis(Plane.XZ, 0.2 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 0.3 * np.pi)), + (5, PlannerMeasBasis(Plane.XY, 0.4 * np.pi)), + (6, PlannerMeasBasis(Plane.XZ, 0.5 * np.pi)), ] exp_measurements = [ - (1, Plane.XY, 0.1 * np.pi), - (3, Plane.XZ, 1.7 * np.pi), - (4, Plane.YZ, 0.3 * np.pi), - (5, Plane.XY, 0.4 * np.pi), - (6, Plane.XZ, 1.5 * np.pi), + (1, PlannerMeasBasis(Plane.XY, 0.1 * np.pi)), + (3, PlannerMeasBasis(Plane.XZ, 1.7 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 0.3 * np.pi)), + (5, PlannerMeasBasis(Plane.XY, 0.4 * np.pi)), + (6, PlannerMeasBasis(Plane.XZ, 1.5 * np.pi)), ] _apply_measurements(zx_graph, measurements) zx_graph.remove_cliffords() @@ -944,19 +580,19 @@ def test_remove_cliffords_graph3(zx_graph: ZXGraphState) -> None: def test_remove_cliffords_graph4(zx_graph: ZXGraphState) -> None: - """Test removing multiple Clifford vertices.""" + """Test removing multiple Clifford nodes.""" graph_4(zx_graph) measurements = [ - (1, Plane.XY, np.pi), - (4, Plane.XZ, 0.5 * np.pi), + (1, PlannerMeasBasis(Plane.XY, np.pi)), + (4, PlannerMeasBasis(Plane.XZ, 0.5 * np.pi)), ] _apply_measurements(zx_graph, measurements) zx_graph.remove_cliffords() - _test(zx_graph, {1, 2, 3, 5}, {(1, 3), (1, 5), (2, 3), (2, 5), (3, 5)}, [(1, Plane.XY, np.pi)]) + _test(zx_graph, {1, 2, 3, 5}, {(1, 3), (1, 5), (2, 3), (2, 5), (3, 5)}, [(1, PlannerMeasBasis(Plane.XY, np.pi))]) def test_random_graph(zx_graph: ZXGraphState) -> None: - """Test removing multiple Clifford vertices from a random graph.""" + """Test removing multiple Clifford nodes from a random graph.""" random_graph, _ = get_random_flow_graph(5, 5) zx_graph.append(random_graph) @@ -975,13 +611,268 @@ def test_random_graph(zx_graph: ZXGraphState) -> None: zx_graph.remove_cliffords() atol = 1e-9 nodes = zx_graph.physical_nodes - zx_graph.input_nodes - zx_graph.output_nodes - clifford_nodes = [ - node - for node in nodes - if is_clifford_angle(zx_graph.meas_bases[node].angle, atol) and zx_graph.is_removable_clifford(node, atol) - ] + clifford_nodes = [node for node in nodes if zx_graph.is_removable_clifford(node, atol)] assert clifford_nodes == [] +@pytest.mark.parametrize( + ("measurements", "exp_measurements", "exp_edges"), + [ + # no pair of adjacent nodes with YZ measurements + # and no node with XZ measurement + ( + [ + (1, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.22 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 0.44 * np.pi)), + (5, PlannerMeasBasis(Plane.XY, 0.55 * np.pi)), + (6, PlannerMeasBasis(Plane.XY, 0.66 * np.pi)), + ], + [ + (1, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.22 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 0.44 * np.pi)), + (5, PlannerMeasBasis(Plane.XY, 0.55 * np.pi)), + (6, PlannerMeasBasis(Plane.XY, 0.66 * np.pi)), + ], + {(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)}, + ), + ], +) +def test_convert_to_phase_gadget( + zx_graph: ZXGraphState, + measurements: Measurements, + exp_measurements: Measurements, + exp_edges: set[tuple[int, int]], +) -> None: + initial_edges = {(1, 2), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)} + _initialize_graph(zx_graph, nodes=range(1, 7), edges=initial_edges) + _apply_measurements(zx_graph, measurements) + zx_graph.convert_to_phase_gadget() + _test(zx_graph, exp_nodes={1, 2, 3, 4, 5, 6}, exp_edges=exp_edges, exp_measurements=exp_measurements) + + +@pytest.mark.parametrize( + ("initial_edges", "measurements", "exp_measurements", "exp_edges"), + [ + # 4(XY) 4(XY) + # | -> | + # 1(YZ) - 2(XY) - 3(XY) 2(XY) - 3(XY) + ( + {(1, 2), (2, 3), (2, 4)}, + [ + (1, PlannerMeasBasis(Plane.YZ, 0.11 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.22 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 0.44 * np.pi)), + ], + [ + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 0.44 * np.pi)), + ], + {(2, 3), (2, 4)}, + ), + # 4(YZ) 4(YZ) + # | \ -> | \ + # 1(YZ) - 2(XY) - 3(XY) 2(XY) - 3(XY) + ( + {(1, 2), (2, 3), (2, 4), (3, 4)}, + [ + (1, PlannerMeasBasis(Plane.YZ, 0.11 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.22 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 0.44 * np.pi)), + ], + [ + (2, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 0.44 * np.pi)), + ], + {(2, 3), (2, 4), (3, 4)}, + ), + ], +) +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(1, 5), edges=initial_edges) + _apply_measurements(zx_graph, measurements) + zx_graph.merge_yz_to_xy() + _test(zx_graph, exp_nodes={2, 3, 4}, exp_edges=exp_edges, exp_measurements=exp_measurements) + + +@pytest.mark.parametrize( + ("initial_edges", "measurements", "exp_zxgraph"), + [ + # 4(YZ) 4(YZ) + # / \ / \ + # 1(XY) - 2(XY) - 3(XY) -> 1(XY) - 2(XY) - 3(XY) + # \ / + # 5(YZ) + ( + {(1, 2), (1, 4), (1, 5), (2, 3), (3, 4), (3, 5)}, + [ + (1, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.22 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 0.44 * np.pi)), + (5, PlannerMeasBasis(Plane.YZ, 0.55 * np.pi)), + ], + ( + [ + (1, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.22 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 0.99 * np.pi)), + ], + {(1, 2), (1, 4), (2, 3), (3, 4)}, + {1, 2, 3, 4}, + ), + ), + # 4(YZ) + # / \ + # 1(XY) - 2(YZ) - 3(XY) -> 1(XY) - 2(YZ) - 3(XY) + # \ / + # 5(YZ) + ( + {(1, 2), (1, 4), (1, 5), (2, 3), (3, 4), (3, 5)}, + [ + (1, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (2, PlannerMeasBasis(Plane.YZ, 0.22 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 0.44 * np.pi)), + (5, PlannerMeasBasis(Plane.YZ, 0.55 * np.pi)), + ], + ( + [ + (1, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (2, PlannerMeasBasis(Plane.YZ, 1.21 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + ], + {(1, 2), (2, 3)}, + {1, 2, 3}, + ), + ), + # 4(YZ) + # / \ + # 1(XY) - 2(YZ) - 3(XY) - 1(XY) -> 1(XY) - 2(YZ) - 3(XY) - 1(XY) + # \ / + # 5(YZ) + ( + {(1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (3, 4), (3, 5)}, + [ + (1, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (2, PlannerMeasBasis(Plane.YZ, 0.22 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + (4, PlannerMeasBasis(Plane.YZ, 0.44 * np.pi)), + (5, PlannerMeasBasis(Plane.YZ, 0.55 * np.pi)), + ], + ( + [ + (1, PlannerMeasBasis(Plane.XY, 0.11 * np.pi)), + (2, PlannerMeasBasis(Plane.YZ, 1.21 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.33 * np.pi)), + ], + {(1, 2), (1, 3), (2, 3)}, + {1, 2, 3}, + ), + ), + ], +) +def test_merge_yz_nodes( + zx_graph: ZXGraphState, + initial_edges: set[tuple[int, int]], + measurements: Measurements, + exp_zxgraph: tuple[Measurements, set[tuple[int, int]], set[int]], +) -> None: + _initialize_graph(zx_graph, nodes=range(1, 6), edges=initial_edges) + _apply_measurements(zx_graph, measurements) + zx_graph.merge_yz_nodes() + exp_measurements, exp_edges, exp_nodes = exp_zxgraph + _test(zx_graph, exp_nodes, exp_edges, exp_measurements) + + +@pytest.mark.parametrize( + ("initial_zxgraph", "measurements", "exp_zxgraph"), + [ + # test for a phase gadget: apply merge_yz_to_xy then remove_cliffords + ( + (range(1, 5), {(1, 2), (2, 3), (2, 4)}), + [ + (1, PlannerMeasBasis(Plane.YZ, 0.1 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.4 * np.pi)), + (3, PlannerMeasBasis(Plane.XY, 0.3 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 0.4 * np.pi)), + ], + ( + [ + (3, PlannerMeasBasis(Plane.XY, 1.8 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 1.9 * np.pi)), + ], + {(3, 4)}, + {3, 4}, + ), + ), + # apply convert_to_phase_gadget, merge_yz_to_xy, then remove_cliffords + ( + (range(1, 5), {(1, 2), (2, 3), (2, 4)}), + [ + (1, PlannerMeasBasis(Plane.YZ, 0.1 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.9 * np.pi)), + (3, PlannerMeasBasis(Plane.XZ, 0.8 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 0.4 * np.pi)), + ], + ( + [ + (3, PlannerMeasBasis(Plane.XY, 0.2 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 0.9 * np.pi)), + ], + {(3, 4)}, + {3, 4}, + ), + ), + # apply remove_cliffords, convert_to_phase_gadget, merge_yz_to_xy, then remove_cliffords + ( + (range(1, 7), {(1, 2), (2, 3), (2, 4), (3, 6), (4, 5)}), + [ + (1, PlannerMeasBasis(Plane.YZ, 0.1 * np.pi)), + (2, PlannerMeasBasis(Plane.XY, 0.9 * np.pi)), + (3, PlannerMeasBasis(Plane.YZ, 1.2 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 1.4 * np.pi)), + (5, PlannerMeasBasis(Plane.YZ, 1.0 * np.pi)), + (6, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)), + ], + ( + [ + (3, PlannerMeasBasis(Plane.XY, 1.8 * np.pi)), + (4, PlannerMeasBasis(Plane.XY, 0.9 * np.pi)), + ], + {(3, 4)}, + {3, 4}, + ), + ), + ], +) +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()