From 75602094f0105b8fc721f3946c63efc21eed7c2d Mon Sep 17 00:00:00 2001 From: masa10-f Date: Mon, 7 Apr 2025 17:15:36 +0900 Subject: [PATCH 01/67] implement graphstate developed in mf branch --- graphix_zx/graphstate.py | 765 +++++++++++++++++++++++++++++++++++++++ tests/test_graphstate.py | 253 +++++++++++++ 2 files changed, 1018 insertions(+) create mode 100644 graphix_zx/graphstate.py create mode 100644 tests/test_graphstate.py diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py new file mode 100644 index 000000000..c48d54686 --- /dev/null +++ b/graphix_zx/graphstate.py @@ -0,0 +1,765 @@ +"""Graph State classes for Measurement-based Quantum Computing. + +This module provides: +- BaseGraphState: Abstract base class for Graph State. +- GraphState: Minimal implementation of Graph State. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from itertools import product +from typing import TYPE_CHECKING + +from graphix_zx.common import MeasBasis, Plane, PlannerMeasBasis, default_meas_basis +from graphix_zx.euler import update_lc_basis + +if TYPE_CHECKING: + from graphix_zx.euler import LocalClifford + + +class BaseGraphState(ABC): + """Abstract base class for Graph State.""" + + @abstractmethod + def __init__(self) -> None: + pass + + @property + @abstractmethod + def input_nodes(self) -> set[int]: + """Return set of input nodes. + + Returns + ------- + set[int] + set of input nodes. + """ + raise NotImplementedError + + @property + @abstractmethod + def output_nodes(self) -> set[int]: + """Return set of output nodes. + + Returns + ------- + set[int] + set of output nodes. + """ + raise NotImplementedError + + @property + @abstractmethod + def num_physical_nodes(self) -> int: + """Return the number of physical nodes. + + Returns + ------- + int + number of physical nodes. + """ + raise NotImplementedError + + @property + @abstractmethod + def num_physical_edges(self) -> int: + """Return the number of physical edges. + + Returns + ------- + int + number of physical edges. + """ + raise NotImplementedError + + @property + @abstractmethod + def physical_nodes(self) -> set[int]: + """Return set of physical nodes. + + Returns + ------- + set[int] + set of physical nodes. + """ + raise NotImplementedError + + @property + @abstractmethod + def physical_edges(self) -> set[tuple[int, int]]: + """Return set of physical edges. + + Returns + ------- + set[tuple[int, int]] + set of physical edges. + """ + raise NotImplementedError + + @property + @abstractmethod + # Generics? + def q_indices(self) -> dict[int, int]: + """Return local qubit indices. + + Returns + ------- + dict[int, int] + logical qubit indices of each physical node. + """ + raise NotImplementedError + + @property + @abstractmethod + def meas_bases(self) -> dict[int, MeasBasis]: + """Return measurement bases. + + Returns + ------- + dict[int, MeasBasis] + measurement bases of each physical node. + """ + raise NotImplementedError + + @property + @abstractmethod + def local_cliffords(self) -> dict[int, LocalClifford]: + """Return local clifford nodes. + + Returns + ------- + dict[int, LocalClifford] + local clifford nodes. + """ + raise NotImplementedError + + @abstractmethod + def add_physical_node( + self, + node: int, + q_index: int, + *, + is_input: bool = False, + is_output: bool = False, + ) -> None: + """Add a physical node to the graph state. + + Parameters + ---------- + node : int + node index + q_index : int + logical qubit index + is_input : bool + True if node is input node + is_output : bool + True if node is output node + """ + raise NotImplementedError + + @abstractmethod + def add_physical_edge(self, node1: int, node2: int) -> None: + """Add a physical edge to the graph state. + + Parameters + ---------- + node1 : int + node index + node2 : int + node index + """ + raise NotImplementedError + + @abstractmethod + def set_input(self, node: int) -> None: + """Set the node as an input node. + + Parameters + ---------- + node : int + node index + """ + raise NotImplementedError + + @abstractmethod + def set_output(self, node: int) -> None: + """Set the node as an output node. + + Parameters + ---------- + node : int + node index + """ + raise NotImplementedError + + @abstractmethod + def set_q_index(self, node: int, q_index: int) -> None: + """Set the qubit index of the node. + + Parameters + ---------- + node : int + node index + q_index: int + logical qubit index + """ + raise NotImplementedError + + @abstractmethod + def set_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: + """Set the measurement basis of the node. + + Parameters + ---------- + node : int + node index + meas_basis : MeasBasis + measurement basis + """ + raise NotImplementedError + + @abstractmethod + 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 + """ + raise NotImplementedError + + @abstractmethod + def get_neighbors(self, node: int) -> set[int]: + """Return the neighbors of the node. + + Parameters + ---------- + node : int + node index + + Returns + ------- + set[int] + set of neighboring nodes + """ + raise NotImplementedError + + +class GraphState(BaseGraphState): + """Minimal implementation of GraphState. + + 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 : dict[int, set[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 + """ + + __input_nodes: set[int] + __output_nodes: set[int] + __physical_nodes: set[int] + __physical_edges: dict[int, set[int]] + __meas_bases: dict[int, MeasBasis] + __q_indices: dict[int, int] + __local_cliffords: dict[int, LocalClifford] + + __inner_index: int + __inner2nodes: dict[int, int] + __nodes2inner: dict[int, int] + + def __init__(self) -> None: + self.__input_nodes = set() + self.__output_nodes = set() + self.__physical_nodes = set() + self.__physical_edges = {} + self.__meas_bases = {} + # NOTE: qubit index if allocated. -1 if not. used for simulation + self.__q_indices = {} + self.__local_cliffords = {} + + self.__inner_index = 0 + self.__inner2nodes = {} + self.__nodes2inner = {} + + @property + def input_nodes(self) -> set[int]: + """Return set of input nodes. + + Returns + ------- + set[int] + set of input nodes. + """ + return self.__input_nodes + + @property + def output_nodes(self) -> set[int]: + """Return set of output nodes. + + Returns + ------- + set[int] + set of output nodes. + """ + return self.__output_nodes + + @property + def num_physical_nodes(self) -> int: + """Return the number of physical nodes. + + Returns + ------- + int + number of physical nodes. + """ + return len(self.__physical_nodes) + + @property + def num_physical_edges(self) -> int: + """Return the number of physical edges. + + Returns + ------- + int + number of physical edges. + """ + return sum(len(edges) for edges in self.__physical_edges.values()) // 2 + + @property + def physical_nodes(self) -> set[int]: + """Return set of physical nodes. + + Returns + ------- + set[int] + set of physical nodes. + """ + return self.__physical_nodes + + @property + def physical_edges(self) -> set[tuple[int, int]]: + """Return set of physical edges. + + Returns + ------- + set[tuple[int, int]] + set of physical edges. + """ + edges = set() + for node1 in self.__physical_edges: + for node2 in self.__physical_edges[node1]: + if node1 < node2: + edges |= {(node1, node2)} + return edges + + @property + def q_indices(self) -> dict[int, int]: + """Return local qubit indices. + + Returns + ------- + dict[int, int] + logical qubit indices of each physical node. + """ + return self.__q_indices + + @property + def meas_bases(self) -> dict[int, MeasBasis]: + """Return measurement bases. + + Returns + ------- + dict[int, MeasBasis] + measurement bases of each physical node. + """ + return self.__meas_bases + + @property + def local_cliffords(self) -> dict[int, LocalClifford]: + """Return local clifford nodes. + + Returns + ------- + dict[int, LocalClifford] + local clifford nodes. + """ + return self.__local_cliffords + + def check_meas_basis(self) -> None: + """Check if the measurement basis is set for all physical nodes except output nodes. + + Raises + ------ + ValueError + If the measurement basis is not set for a node or the measurement plane is invalid. + """ + for v in self.physical_nodes - self.output_nodes: + if self.meas_bases.get(v) is None: + msg = f"Measurement basis not set for node {v}" + raise ValueError(msg) + + def add_physical_node( + self, + node: int, + q_index: int = -1, + *, + is_input: bool = False, + is_output: bool = False, + ) -> None: + """Add a physical node to the graph state. + + Parameters + ---------- + node : int + node index + q_index : int + logical qubit index + is_input : bool + True if node is input node + is_output : bool + True if node is output node + + Raises + ------ + ValueError + If the node already exists in the graph state. + """ + if node in self.__physical_nodes: + msg = f"Node already exists {node=}" + raise ValueError(msg) + self.__physical_nodes |= {node} + self.__physical_edges[node] = set() + self.set_q_index(node, q_index) + if is_input: + self.__input_nodes |= {node} + if is_output: + self.__output_nodes |= {node} + + self.__inner2nodes[self.__inner_index] = node + self.__nodes2inner[node] = self.__inner_index + self.__inner_index += 1 + + def ensure_node_exists(self, node: int) -> None: + """Ensure that the node exists in the graph state. + + Raises + ------ + ValueError + If the node does not exist in the graph state. + """ + if node not in self.__physical_nodes: + msg = f"Node does not exist {node=}" + raise ValueError(msg) + + def add_physical_edge(self, node1: int, node2: int) -> None: + """Add a physical edge to the graph state. + + Parameters + ---------- + node1 : int + node index + node2 : int + node index + + Raises + ------ + ValueError + If the edge already exists. + """ + self.ensure_node_exists(node1) + self.ensure_node_exists(node2) + if node1 in self.__physical_edges[node2] or node2 in self.__physical_edges[node1]: + msg = f"Edge already exists {node1=}, {node2=}" + raise ValueError(msg) + self.__physical_edges[node1] |= {node2} + self.__physical_edges[node2] |= {node1} + + def remove_physical_node(self, node: int) -> None: + """Remove a physical node from the graph state. + + Parameters + ---------- + node : int + + Raises + ------ + ValueError + If the node does not exist. + """ + if node not in self.__physical_nodes: + msg = f"Node does not exist {node=}" + raise ValueError(msg) + self.ensure_node_exists(node) + self.__physical_nodes -= {node} + del self.__physical_edges[node] + self.__input_nodes -= {node} + self.__output_nodes -= {node} + self.__meas_bases.pop(node, None) + self.__q_indices.pop(node, None) + self.__local_cliffords.pop(node, None) + for neighbor in self.__physical_edges: + self.__physical_edges[neighbor] -= {node} + + self.__inner2nodes.pop(self.__nodes2inner[node]) + self.__nodes2inner.pop(node) + + def remove_physical_edge(self, node1: int, node2: int) -> None: + """Remove a physical edge from the graph state. + + Parameters + ---------- + node1 : int + node index + node2 : int + node index + + Raises + ------ + ValueError + If the edge does not exist. + """ + self.ensure_node_exists(node1) + self.ensure_node_exists(node2) + if node1 not in self.__physical_edges[node2] or node2 not in self.__physical_edges[node1]: + msg = "Edge does not exist" + raise ValueError(msg) + self.__physical_edges[node1] -= {node2} + self.__physical_edges[node2] -= {node1} + + def set_input(self, node: int) -> None: + """Set the node as an input node. + + Parameters + ---------- + node : int + node index + """ + self.ensure_node_exists(node) + self.__input_nodes |= {node} + + def set_output(self, node: int) -> None: + """Set the node as an output node. + + Parameters + ---------- + node : int + node index + + Raises + ------ + ValueError + 1. If the node does not exist. + 2. If the node has a measurement basis. + """ + self.ensure_node_exists(node) + if self.meas_bases.get(node) is not None: + msg = "Cannot set output node with measurement basis." + raise ValueError(msg) + self.__output_nodes |= {node} + + def set_q_index(self, node: int, q_index: int = -1) -> None: + """Set the qubit index of the node. + + Parameters + ---------- + node : int + node index + q_index: int, optional + logical qubit index, by default -1 + + Raises + ------ + ValueError + If the qubit index is invalid. + """ + self.ensure_node_exists(node) + if q_index < -1: + msg = f"Invalid qubit index {q_index}. Must be -1 or greater" + raise ValueError(msg) + self.__q_indices[node] = q_index + + def set_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: + """Set the measurement basis of the node. + + Parameters + ---------- + node : int + node index + meas_basis : MeasBasis + measurement basis + """ + 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_nodes or node in self.output_nodes: + self.__local_cliffords[node] = lc + else: + new_meas_basis = update_lc_basis(lc.conjugate(), self.meas_bases[node]) + self.set_meas_basis(node, new_meas_basis) + + 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 parse_input_local_cliffords(self) -> None: + """Parse local Clifford operators applied on the input nodes.""" + for input_node in self.input_nodes: + lc = self.pop_local_clifford(input_node) + if lc is None: + continue + node_indices = (self.__inner_index, self.__inner_index + 1, self.__inner_index + 2) + + self.add_physical_node(node_indices[0], q_index=self.q_indices[input_node], is_input=True) + self.add_physical_node(node_indices[1], q_index=self.q_indices[input_node]) + self.add_physical_node(node_indices[2], q_index=self.q_indices[input_node]) + + self.add_physical_edge(node_indices[0], node_indices[1]) + self.add_physical_edge(node_indices[1], node_indices[2]) + self.add_physical_edge(node_indices[2], input_node) + + self.set_meas_basis(node_indices[0], PlannerMeasBasis(Plane.XY, lc.alpha)) + self.set_meas_basis(node_indices[1], PlannerMeasBasis(Plane.XY, lc.beta)) + self.set_meas_basis(node_indices[2], PlannerMeasBasis(Plane.XY, lc.gamma)) + + self._reset_input(input_node) + + def get_neighbors(self, node: int) -> set[int]: + """Return the neighbors of the node. + + Parameters + ---------- + node : int + node index + + Returns + ------- + set[int] + set of neighboring nodes + """ + self.ensure_node_exists(node) + return self.__physical_edges[node] + + def _reset_input(self, node: int) -> None: + """Reset the input status of the node. + + Parameters + ---------- + node : int + node index + """ + if node in self.__input_nodes: + self.__input_nodes.remove(node) + lc = self.pop_local_clifford(node) + if lc is not None: + self.apply_local_clifford(node, lc) + + def _reset_output(self, node: int) -> None: + """Reset the output status of the node. + + Parameters + ---------- + node : int + node index + """ + if node in self.__output_nodes: + self.__output_nodes.remove(node) + + def append(self, other: BaseGraphState) -> None: + """Append another graph state to the current graph state. + + Parameters + ---------- + other : BaseGraphState + another graph state to append + + Raises + ------ + ValueError + If the qubit indices do not match. + """ + common_nodes = self.physical_nodes & other.physical_nodes + border_nodes = self.output_nodes & other.input_nodes + + if common_nodes != border_nodes: + msg = "Qubit index mismatch" + raise ValueError(msg) + + for node in other.physical_nodes: + if node in border_nodes: + self._reset_input(node) + self._reset_output(node) + else: + self.add_physical_node(node) + if node in other.input_nodes - self.output_nodes: + self.set_input(node) + + if node in other.output_nodes: + self.set_output(node) + else: + meas_basis = other.meas_bases.get(node, default_meas_basis()) + self.set_meas_basis(node, meas_basis) + + for edge in other.physical_edges: + self.add_physical_edge(edge[0], edge[1]) + + # q_index update + for node, q_index in other.q_indices.items(): + if (node in common_nodes) and (self.q_indices[node] != q_index): + msg = "Qubit index mismatch." + raise ValueError(msg) + self.set_q_index(node, q_index) + + +def bipartite_edges(node_set1: set[int], node_set2: set[int]) -> set[tuple[int, int]]: + """Return a set of edges for the complete bipartite graph between two sets of nodes. + + Parameters + ---------- + node_set1 : set[int] + set of nodes + node_set2 : set[int] + set of nodes + + Returns + ------- + set[tuple[int, int]] + set of edges for the complete bipartite graph + """ + return {(min(a, b), max(a, b)) for a, b in product(node_set1, node_set2) if a != b} diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py new file mode 100644 index 000000000..8c7abb876 --- /dev/null +++ b/tests/test_graphstate.py @@ -0,0 +1,253 @@ +"""Tests for the GraphState class.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from graphix_zx.common import Plane, PlannerMeasBasis +from graphix_zx.graphstate import GraphState, bipartite_edges + + +@pytest.fixture +def graph() -> GraphState: + """Generate an empty GraphState object. + + Returns + ------- + GraphState: An empty GraphState object. + """ + return GraphState() + + +def test_add_physical_node(graph: GraphState) -> None: + """Test adding a physical node to the graph.""" + graph.add_physical_node(1) + assert 1 in graph.physical_nodes + assert graph.num_physical_nodes == 1 + + +def test_add_physical_node_input_output(graph: GraphState) -> None: + """Test adding a physical node as input and output.""" + graph.add_physical_node(1, is_input=True, is_output=True) + assert 1 in graph.input_nodes + assert 1 in graph.output_nodes + + +def test_add_duplicate_physical_node(graph: GraphState) -> None: + """Test adding a duplicate physical node to the graph.""" + graph.add_physical_node(1) + with pytest.raises(Exception, match="Node already exists"): + graph.add_physical_node(1) + + +def test_ensure_node_exists_raises(graph: GraphState) -> None: + """Test ensuring a node exists in the graph.""" + with pytest.raises(ValueError, match="Node does not exist node=1"): + graph.ensure_node_exists(1) + + +def test_ensure_node_exists(graph: GraphState) -> None: + """Test ensuring a node exists in the graph.""" + graph.add_physical_node(1) + graph.ensure_node_exists(1) + + +def test_get_neighbors(graph: GraphState) -> None: + """Test getting the neighbors of a node in the graph.""" + graph.add_physical_node(1) + graph.add_physical_node(2) + graph.add_physical_node(3) + graph.add_physical_edge(1, 2) + graph.add_physical_edge(2, 3) + assert graph.get_neighbors(1) == {2} + assert graph.get_neighbors(2) == {1, 3} + assert graph.get_neighbors(3) == {2} + + +def test_add_physical_edge(graph: GraphState) -> None: + """Test adding a physical edge to the graph.""" + graph.add_physical_node(1) + graph.add_physical_node(2) + graph.add_physical_edge(1, 2) + assert (1, 2) in graph.physical_edges or (2, 1) in graph.physical_edges + assert graph.num_physical_edges == 1 + + +def test_add_duplicate_physical_edge(graph: GraphState) -> None: + """Test adding a duplicate physical edge to the graph.""" + graph.add_physical_node(1) + graph.add_physical_node(2) + graph.add_physical_edge(1, 2) + with pytest.raises(ValueError, match="Edge already exists node1=1, node2=2"): + graph.add_physical_edge(1, 2) + + +def test_add_edge_with_nonexistent_node(graph: GraphState) -> None: + """Test adding an edge with a nonexistent node to the graph.""" + graph.add_physical_node(1) + with pytest.raises(ValueError, match="Node does not exist node=2"): + graph.add_physical_edge(1, 2) + + +def test_remove_physical_node_with_nonexistent_node(graph: GraphState) -> None: + """Test removing a nonexistent physical node from the graph.""" + with pytest.raises(ValueError, match="Node does not exist node=1"): + graph.remove_physical_node(1) + + +def test_remove_physical_node(graph: GraphState) -> None: + """Test removing a physical node from the graph.""" + graph.add_physical_node(1) + graph.remove_physical_node(1) + assert 1 not in graph.physical_nodes + assert graph.num_physical_nodes == 0 + + +def test_remove_physical_node_from_minimal_graph(graph: GraphState) -> None: + """Test removing a physical node from the graph with edges.""" + graph.add_physical_node(1) + graph.add_physical_node(2) + graph.add_physical_edge(1, 2) + graph.remove_physical_node(1) + assert 1 not in graph.physical_nodes + assert 2 in graph.physical_nodes + assert (1, 2) not in graph.physical_edges + assert (2, 1) not in graph.physical_edges + assert graph.num_physical_nodes == 1 + assert graph.num_physical_edges == 0 + + +def test_remove_physical_node_from_3_nodes_graph(graph: GraphState) -> None: + """Test removing a physical node from the graph with 3 nodes and edges.""" + graph.add_physical_node(1) + graph.add_physical_node(2) + graph.add_physical_node(3) + graph.add_physical_edge(1, 2) + graph.add_physical_edge(2, 3) + graph.set_input(2) + graph.set_output(2) + graph.remove_physical_node(2) + assert graph.physical_nodes == {1, 3} + assert graph.physical_edges == set() + assert graph.num_physical_nodes == 2 + assert graph.num_physical_edges == 0 + assert graph.input_nodes == set() + assert graph.output_nodes == set() + + +def test_remove_physical_edge_with_nonexistent_nodes(graph: GraphState) -> None: + """Test removing an edge with nonexistent nodes from the graph.""" + with pytest.raises(ValueError, match="Node does not exist node=1"): + graph.remove_physical_edge(1, 2) + + +def test_remove_physical_edge_with_nonexistent_edge(graph: GraphState) -> None: + """Test removing a nonexistent edge from the graph.""" + graph.add_physical_node(1) + graph.add_physical_node(2) + with pytest.raises(ValueError, match="Edge does not exist"): + graph.remove_physical_edge(1, 2) + + +def test_remove_physical_edge(graph: GraphState) -> None: + """Test removing a physical edge from the graph.""" + graph.add_physical_node(1) + graph.add_physical_node(2) + graph.add_physical_edge(1, 2) + graph.remove_physical_edge(1, 2) + assert (1, 2) not in graph.physical_edges + assert (2, 1) not in graph.physical_edges + assert graph.num_physical_edges == 0 + + +def test_set_input(graph: GraphState) -> None: + """Test setting a physical node as input.""" + graph.add_physical_node(1) + graph.set_input(1) + assert 1 in graph.input_nodes + + +def test_set_output_raises_1(graph: GraphState) -> None: + with pytest.raises(ValueError, match="Node does not exist node=1"): + graph.set_output(1) + graph.add_physical_node(1) + graph.set_meas_basis(1, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) + with pytest.raises(ValueError, match=r"Cannot set output node with measurement basis."): + graph.set_output(1) + + +def test_set_output_raises_2(graph: GraphState) -> None: + graph.add_physical_node(1) + graph.set_meas_basis(1, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) + with pytest.raises(ValueError, match=r"Cannot set output node with measurement basis."): + graph.set_output(1) + + +def test_set_output(graph: GraphState) -> None: + """Test setting a physical node as output.""" + graph.add_physical_node(1) + graph.set_output(1) + assert 1 in graph.output_nodes + + +def test_set_meas_basis(graph: GraphState) -> None: + """Test setting the measurement basis of a physical node.""" + graph.add_physical_node(1) + meas_basis = PlannerMeasBasis(Plane.XZ, 0.5 * np.pi) + graph.set_meas_basis(1, meas_basis) + assert graph.meas_bases[1].plane == Plane.XZ + assert graph.meas_bases[1].angle == 0.5 * np.pi + + +def test_append_graph() -> None: + """Test appending a graph to another graph.""" + graph1 = GraphState() + graph1.add_physical_node(1, is_input=True) + graph1.add_physical_node(2, is_output=True) + graph1.add_physical_edge(1, 2) + + graph2 = GraphState() + graph2.add_physical_node(2, is_input=True) + graph2.add_physical_node(3, is_output=True) + graph2.add_physical_edge(2, 3) + + graph1.append(graph2) + + assert graph1.num_physical_nodes == 3 + assert graph1.num_physical_edges == 2 + assert 1 in graph1.input_nodes + assert 3 in graph1.output_nodes + + +def test_check_meas_raises_value_error(graph: GraphState) -> None: + """Test if measurement planes and angles are set improperly.""" + graph.add_physical_node(1) + with pytest.raises(ValueError, match="Measurement basis not set for node 1"): + graph.check_meas_basis() + + +def test_check_meas_basis_success(graph: GraphState) -> None: + """Test if measurement planes and angles are set properly.""" + graph.check_meas_basis() + graph.add_physical_node(1) + meas_basis = PlannerMeasBasis(Plane.XY, 0.5 * np.pi) + graph.set_meas_basis(1, meas_basis) + graph.check_meas_basis() + + graph.add_physical_node(2) + graph.add_physical_edge(1, 2) + graph.set_output(2) + graph.check_meas_basis() + + +def test_bipartite_edges(graph: GraphState) -> None: + """Test the function that generate complete bipartite edges""" + assert bipartite_edges(set(), set()) == set() + assert bipartite_edges({1, 2, 3}, {1, 2, 3}) == {(1, 2), (1, 3), (2, 3)} + assert bipartite_edges({1, 2}, {3, 4}) == {(1, 3), (1, 4), (2, 3), (2, 4)} + graph.check_meas_basis() + + +if __name__ == "__main__": + pytest.main() From b7641a3cc723358234f2017852e1a2a5212a2a95 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Tue, 8 Apr 2025 20:07:38 +0900 Subject: [PATCH 02/67] minimize API in BaseGraphState --- graphix_zx/graphstate.py | 33 +++------------------------------ 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index c48d54686..2fe7dbb2a 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -1,8 +1,9 @@ """Graph State classes for Measurement-based Quantum Computing. This module provides: -- BaseGraphState: Abstract base class for Graph State. -- GraphState: Minimal implementation of Graph State. +- `BaseGraphState`: Abstract base class for Graph State. +- `GraphState`: Minimal implementation of Graph State. +- `bipartite_edges`: Function to create a complete bipartite graph between two sets of nodes. """ from __future__ import annotations @@ -21,10 +22,6 @@ class BaseGraphState(ABC): """Abstract base class for Graph State.""" - @abstractmethod - def __init__(self) -> None: - pass - @property @abstractmethod def input_nodes(self) -> set[int]: @@ -49,30 +46,6 @@ def output_nodes(self) -> set[int]: """ raise NotImplementedError - @property - @abstractmethod - def num_physical_nodes(self) -> int: - """Return the number of physical nodes. - - Returns - ------- - int - number of physical nodes. - """ - raise NotImplementedError - - @property - @abstractmethod - def num_physical_edges(self) -> int: - """Return the number of physical edges. - - Returns - ------- - int - number of physical edges. - """ - raise NotImplementedError - @property @abstractmethod def physical_nodes(self) -> set[int]: From 1581cf78cc8d1fc9bcda22b333e9933447028ce6 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Tue, 8 Apr 2025 20:19:45 +0900 Subject: [PATCH 03/67] add backticks for intersphinx --- graphix_zx/graphstate.py | 168 +++++++++++++++++++-------------------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 2fe7dbb2a..5b00b3c26 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -25,11 +25,11 @@ class BaseGraphState(ABC): @property @abstractmethod def input_nodes(self) -> set[int]: - """Return set of input nodes. + r"""Return set of input nodes. Returns ------- - set[int] + `set`\[`int`\] set of input nodes. """ raise NotImplementedError @@ -37,11 +37,11 @@ def input_nodes(self) -> set[int]: @property @abstractmethod def output_nodes(self) -> set[int]: - """Return set of output nodes. + r"""Return set of output nodes. Returns ------- - set[int] + `set`\[`int`\] set of output nodes. """ raise NotImplementedError @@ -49,11 +49,11 @@ def output_nodes(self) -> set[int]: @property @abstractmethod def physical_nodes(self) -> set[int]: - """Return set of physical nodes. + r"""Return set of physical nodes. Returns ------- - set[int] + `set`\[`int`\] set of physical nodes. """ raise NotImplementedError @@ -61,11 +61,11 @@ def physical_nodes(self) -> set[int]: @property @abstractmethod def physical_edges(self) -> set[tuple[int, int]]: - """Return set of physical edges. + r"""Return set of physical edges. Returns ------- - set[tuple[int, int]] + `set`\[`tuple`\[`int`, `int`\]` set of physical edges. """ raise NotImplementedError @@ -74,11 +74,11 @@ def physical_edges(self) -> set[tuple[int, int]]: @abstractmethod # Generics? def q_indices(self) -> dict[int, int]: - """Return local qubit indices. + r"""Return local qubit indices. Returns ------- - dict[int, int] + `dict`\[`int`, `int`\] logical qubit indices of each physical node. """ raise NotImplementedError @@ -86,11 +86,11 @@ def q_indices(self) -> dict[int, int]: @property @abstractmethod def meas_bases(self) -> dict[int, MeasBasis]: - """Return measurement bases. + r"""Return measurement bases. Returns ------- - dict[int, MeasBasis] + `dict`\[`int`, `MeasBasis`\] measurement bases of each physical node. """ raise NotImplementedError @@ -98,11 +98,11 @@ def meas_bases(self) -> dict[int, MeasBasis]: @property @abstractmethod def local_cliffords(self) -> dict[int, LocalClifford]: - """Return local clifford nodes. + r"""Return local clifford nodes. Returns ------- - dict[int, LocalClifford] + `dict`\[`int`, `LocalClifford`\] local clifford nodes. """ raise NotImplementedError @@ -120,13 +120,13 @@ def add_physical_node( Parameters ---------- - node : int + node : `int` node index - q_index : int + q_index : `int` logical qubit index - is_input : bool + is_input : `bool` True if node is input node - is_output : bool + is_output : `bool` True if node is output node """ raise NotImplementedError @@ -137,9 +137,9 @@ def add_physical_edge(self, node1: int, node2: int) -> None: Parameters ---------- - node1 : int + node1 : `int` node index - node2 : int + node2 : `int` node index """ raise NotImplementedError @@ -150,7 +150,7 @@ def set_input(self, node: int) -> None: Parameters ---------- - node : int + node : `int` node index """ raise NotImplementedError @@ -161,7 +161,7 @@ def set_output(self, node: int) -> None: Parameters ---------- - node : int + node : `int` node index """ raise NotImplementedError @@ -172,9 +172,9 @@ def set_q_index(self, node: int, q_index: int) -> None: Parameters ---------- - node : int + node : `int` node index - q_index: int + q_index: `int` logical qubit index """ raise NotImplementedError @@ -185,9 +185,9 @@ def set_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: Parameters ---------- - node : int + node : `int` node index - meas_basis : MeasBasis + meas_basis : `MeasBasis` measurement basis """ raise NotImplementedError @@ -198,48 +198,48 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: Parameters ---------- - node : int + node : `int` node index - lc : LocalClifford + lc : `LocalClifford` local clifford operator """ raise NotImplementedError @abstractmethod def get_neighbors(self, node: int) -> set[int]: - """Return the neighbors of the node. + r"""Return the neighbors of the node. Parameters ---------- - node : int + node : `int` node index Returns ------- - set[int] + `set`\[`int`\] set of neighboring nodes """ raise NotImplementedError class GraphState(BaseGraphState): - """Minimal implementation of GraphState. + r"""Minimal implementation of GraphState. Attributes ---------- - input_nodes : set[int] + input_nodes : `set`\[`int`\] set of input nodes - output_nodes : set[int] + output_nodes : `set`\[`int`\] set of output nodes - physical_nodes : set[int] + physical_nodes : `set`\[`int`\] set of physical nodes - physical_edges : dict[int, set[int]] + physical_edges : `dict`\[`int`, `set`\[`int`\] physical edges - meas_bases : dict[int, MeasBasis] + meas_bases : `dict`\[`int`, `MeasBasis`\] measurement bases - q_indices : dict[int, int] + q_indices : `dict`\[`int`, `int`\] qubit indices - local_cliffords : dict[int, LocalClifford] + local_cliffords : `dict`\[`int`, `LocalClifford`\] local clifford operators """ @@ -271,22 +271,22 @@ def __init__(self) -> None: @property def input_nodes(self) -> set[int]: - """Return set of input nodes. + r"""Return set of input nodes. Returns ------- - set[int] + `set`\[`int`\] set of input nodes. """ return self.__input_nodes @property def output_nodes(self) -> set[int]: - """Return set of output nodes. + r"""Return set of output nodes. Returns ------- - set[int] + `set`\[`int`\] set of output nodes. """ return self.__output_nodes @@ -297,7 +297,7 @@ def num_physical_nodes(self) -> int: Returns ------- - int + `int` number of physical nodes. """ return len(self.__physical_nodes) @@ -308,29 +308,29 @@ def num_physical_edges(self) -> int: Returns ------- - int + `int` number of physical edges. """ return sum(len(edges) for edges in self.__physical_edges.values()) // 2 @property def physical_nodes(self) -> set[int]: - """Return set of physical nodes. + r"""Return set of physical nodes. Returns ------- - set[int] + `set`\[`int`\] set of physical nodes. """ return self.__physical_nodes @property def physical_edges(self) -> set[tuple[int, int]]: - """Return set of physical edges. + r"""Return set of physical edges. Returns ------- - set[tuple[int, int]] + `set`\[`tuple`\[`int`, `int`\] set of physical edges. """ edges = set() @@ -342,33 +342,33 @@ def physical_edges(self) -> set[tuple[int, int]]: @property def q_indices(self) -> dict[int, int]: - """Return local qubit indices. + r"""Return local qubit indices. Returns ------- - dict[int, int] + `dict`\[`int`, `int`\] logical qubit indices of each physical node. """ return self.__q_indices @property def meas_bases(self) -> dict[int, MeasBasis]: - """Return measurement bases. + r"""Return measurement bases. Returns ------- - dict[int, MeasBasis] + `dict`\[`int`, `MeasBasis`\] measurement bases of each physical node. """ return self.__meas_bases @property def local_cliffords(self) -> dict[int, LocalClifford]: - """Return local clifford nodes. + r"""Return local clifford nodes. Returns ------- - dict[int, LocalClifford] + `dict`\[`int`, `LocalClifford`\] local clifford nodes. """ return self.__local_cliffords @@ -398,13 +398,13 @@ def add_physical_node( Parameters ---------- - node : int + node : `int` node index - q_index : int + q_index : `int` logical qubit index - is_input : bool + is_input : `bool` True if node is input node - is_output : bool + is_output : `bool` True if node is output node Raises @@ -444,9 +444,9 @@ def add_physical_edge(self, node1: int, node2: int) -> None: Parameters ---------- - node1 : int + node1 : `int` node index - node2 : int + node2 : `int` node index Raises @@ -467,7 +467,7 @@ def remove_physical_node(self, node: int) -> None: Parameters ---------- - node : int + node : `int` Raises ------ @@ -496,9 +496,9 @@ def remove_physical_edge(self, node1: int, node2: int) -> None: Parameters ---------- - node1 : int + node1 : `int` node index - node2 : int + node2 : `int` node index Raises @@ -519,7 +519,7 @@ def set_input(self, node: int) -> None: Parameters ---------- - node : int + node : `int` node index """ self.ensure_node_exists(node) @@ -530,7 +530,7 @@ def set_output(self, node: int) -> None: Parameters ---------- - node : int + node : `int` node index Raises @@ -550,9 +550,9 @@ def set_q_index(self, node: int, q_index: int = -1) -> None: Parameters ---------- - node : int + node : `int` node index - q_index: int, optional + q_index: `int`, optional logical qubit index, by default -1 Raises @@ -571,9 +571,9 @@ def set_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: Parameters ---------- - node : int + node : `int` node index - meas_basis : MeasBasis + meas_basis : `MeasBasis` measurement basis """ self.ensure_node_exists(node) @@ -584,9 +584,9 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: Parameters ---------- - node : int + node : `int` node index - lc : LocalClifford + lc : `LocalClifford` local clifford operator """ self.ensure_node_exists(node) @@ -601,12 +601,12 @@ def pop_local_clifford(self, node: int) -> LocalClifford | None: Parameters ---------- - node : int + node : `int` node index to remove local clifford. Returns ------- - LocalClifford | None + `LocalClifford` | `None` removed local clifford """ return self.__local_cliffords.pop(node, None) @@ -634,16 +634,16 @@ def parse_input_local_cliffords(self) -> None: self._reset_input(input_node) def get_neighbors(self, node: int) -> set[int]: - """Return the neighbors of the node. + r"""Return the neighbors of the node. Parameters ---------- - node : int + node : `int` node index Returns ------- - set[int] + `set`\[`int`\] set of neighboring nodes """ self.ensure_node_exists(node) @@ -654,7 +654,7 @@ def _reset_input(self, node: int) -> None: Parameters ---------- - node : int + node : `int` node index """ if node in self.__input_nodes: @@ -668,7 +668,7 @@ def _reset_output(self, node: int) -> None: Parameters ---------- - node : int + node : `int` node index """ if node in self.__output_nodes: @@ -679,7 +679,7 @@ def append(self, other: BaseGraphState) -> None: Parameters ---------- - other : BaseGraphState + other : `BaseGraphState` another graph state to append Raises @@ -721,18 +721,18 @@ def append(self, other: BaseGraphState) -> None: def bipartite_edges(node_set1: set[int], node_set2: set[int]) -> set[tuple[int, int]]: - """Return a set of edges for the complete bipartite graph between two sets of nodes. + r"""Return a set of edges for the complete bipartite graph between two sets of nodes. Parameters ---------- - node_set1 : set[int] + node_set1 : `set`\[`int`\] set of nodes - node_set2 : set[int] + node_set2 : `set`\[`int`\] set of nodes Returns ------- - set[tuple[int, int]] + `set`\[`tuple`\[`int`, `int`\] set of edges for the complete bipartite graph """ return {(min(a, b), max(a, b)) for a, b in product(node_set1, node_set2) if a != b} From d9efcd4a52bfc1ae0d3ae430957ab62b4dc9f137 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Tue, 8 Apr 2025 20:51:33 +0900 Subject: [PATCH 04/67] configure docs for graphstate --- docs/source/graphstate.rst | 24 ++++++++++++++++++++++++ docs/source/references.rst | 1 + graphix_zx/graphstate.py | 21 ++------------------- 3 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 docs/source/graphstate.rst diff --git a/docs/source/graphstate.rst b/docs/source/graphstate.rst new file mode 100644 index 000000000..c2e19bb4d --- /dev/null +++ b/docs/source/graphstate.rst @@ -0,0 +1,24 @@ +GraphState +========== + +:mod:`graphix_zx.graphstate` module ++++++++++++++++++++++++++++++++++++ + +.. automodule:: graphix_zx.graphstate + +Graph State Classes +------------------- + +.. autoclass:: graphix_zx.graphstate.BaseGraphState + :members: + :member-order: bysource + +.. autoclass:: graphix_zx.graphstate.GraphState + :members: + :show-inheritance: + :member-order: bysource + +Functions +--------- + +.. autofunction:: graphix_zx.graphstate.bipartite_edges diff --git a/docs/source/references.rst b/docs/source/references.rst index 9a6dcb462..22d9dbe22 100644 --- a/docs/source/references.rst +++ b/docs/source/references.rst @@ -7,3 +7,4 @@ Module reference common euler matrix + graphstate diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 5b00b3c26..c42f45e3f 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -1,6 +1,7 @@ """Graph State classes for Measurement-based Quantum Computing. This module provides: + - `BaseGraphState`: Abstract base class for Graph State. - `GraphState`: Minimal implementation of Graph State. - `bipartite_edges`: Function to create a complete bipartite graph between two sets of nodes. @@ -223,25 +224,7 @@ def get_neighbors(self, node: int) -> set[int]: class GraphState(BaseGraphState): - r"""Minimal implementation of GraphState. - - 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 : `dict`\[`int`, `set`\[`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 - """ + """Minimal implementation of GraphState.""" __input_nodes: set[int] __output_nodes: set[int] From 841dd35af7fa0f686bd2302fa3560f3ee4a4fe30 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Tue, 8 Apr 2025 21:04:51 +0900 Subject: [PATCH 05/67] refactor q_index attribute --- graphix_zx/graphstate.py | 189 +++++++++++---------------------------- 1 file changed, 53 insertions(+), 136 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index c42f45e3f..8eed134fb 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -25,25 +25,25 @@ class BaseGraphState(ABC): @property @abstractmethod - def input_nodes(self) -> set[int]: + def input_node_indices(self) -> dict[int, int]: r"""Return set of input nodes. Returns ------- - `set`\[`int`\] - set of input nodes. + `dict`\[`int`, `int`\] + qubit indices map of input nodes. """ raise NotImplementedError @property @abstractmethod - def output_nodes(self) -> set[int]: + def output_node_indices(self) -> dict[int, int]: r"""Return set of output nodes. Returns ------- - `set`\[`int`\] - set of output nodes. + `dict`\[`int`, `int`\] + qubit indices map of output nodes. """ raise NotImplementedError @@ -71,19 +71,6 @@ def physical_edges(self) -> set[tuple[int, int]]: """ raise NotImplementedError - @property - @abstractmethod - # Generics? - def q_indices(self) -> dict[int, int]: - r"""Return local qubit indices. - - Returns - ------- - `dict`\[`int`, `int`\] - logical qubit indices of each physical node. - """ - raise NotImplementedError - @property @abstractmethod def meas_bases(self) -> dict[int, MeasBasis]: @@ -112,10 +99,6 @@ def local_cliffords(self) -> dict[int, LocalClifford]: def add_physical_node( self, node: int, - q_index: int, - *, - is_input: bool = False, - is_output: bool = False, ) -> None: """Add a physical node to the graph state. @@ -123,12 +106,6 @@ def add_physical_node( ---------- node : `int` node index - q_index : `int` - logical qubit index - is_input : `bool` - True if node is input node - is_output : `bool` - True if node is output node """ raise NotImplementedError @@ -146,36 +123,27 @@ def add_physical_edge(self, node1: int, node2: int) -> None: raise NotImplementedError @abstractmethod - def set_input(self, node: int) -> None: + def set_input(self, node: int, q_index: int) -> None: """Set the node as an input node. Parameters ---------- node : `int` node index + q_index : `int` + logical qubit index """ raise NotImplementedError @abstractmethod - def set_output(self, node: int) -> None: + def set_output(self, node: int, q_index: int) -> None: """Set the node as an output node. Parameters ---------- node : `int` node index - """ - raise NotImplementedError - - @abstractmethod - def set_q_index(self, node: int, q_index: int) -> None: - """Set the qubit index of the node. - - Parameters - ---------- - node : `int` - node index - q_index: `int` + q_index : `int` logical qubit index """ raise NotImplementedError @@ -226,12 +194,11 @@ def get_neighbors(self, node: int) -> set[int]: class GraphState(BaseGraphState): """Minimal implementation of GraphState.""" - __input_nodes: set[int] - __output_nodes: set[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] - __q_indices: dict[int, int] __local_cliffords: dict[int, LocalClifford] __inner_index: int @@ -239,13 +206,11 @@ class GraphState(BaseGraphState): __nodes2inner: dict[int, int] def __init__(self) -> None: - self.__input_nodes = set() - self.__output_nodes = set() + self.__input_node_indices = {} + self.__output_node_indices = {} self.__physical_nodes = set() self.__physical_edges = {} self.__meas_bases = {} - # NOTE: qubit index if allocated. -1 if not. used for simulation - self.__q_indices = {} self.__local_cliffords = {} self.__inner_index = 0 @@ -253,26 +218,26 @@ def __init__(self) -> None: self.__nodes2inner = {} @property - def input_nodes(self) -> set[int]: - r"""Return set of input nodes. + def input_node_indices(self) -> dict[int, int]: + r"""Return map of input nodes. Returns ------- - `set`\[`int`\] - set of input nodes. + `dict`\[`int`, `int`\] + qubit indices map of input nodes. """ - return self.__input_nodes + return self.__input_node_indices @property - def output_nodes(self) -> set[int]: - r"""Return set of output nodes. + def output_node_indices(self) -> dict[int, int]: + r"""Return map of output nodes. Returns ------- - `set`\[`int`\] - set of output nodes. + `dict`\[`int`, `int`\] + qubit indices map of output nodes. """ - return self.__output_nodes + return self.__output_node_indices @property def num_physical_nodes(self) -> int: @@ -323,17 +288,6 @@ def physical_edges(self) -> set[tuple[int, int]]: edges |= {(node1, node2)} return edges - @property - def q_indices(self) -> dict[int, int]: - r"""Return local qubit indices. - - Returns - ------- - `dict`\[`int`, `int`\] - logical qubit indices of each physical node. - """ - return self.__q_indices - @property def meas_bases(self) -> dict[int, MeasBasis]: r"""Return measurement bases. @@ -364,7 +318,7 @@ def check_meas_basis(self) -> None: ValueError If the measurement basis is not set for a node or the measurement plane is invalid. """ - for v in self.physical_nodes - self.output_nodes: + for v in self.physical_nodes - set(self.output_node_indices.keys()): if self.meas_bases.get(v) is None: msg = f"Measurement basis not set for node {v}" raise ValueError(msg) @@ -372,10 +326,6 @@ def check_meas_basis(self) -> None: def add_physical_node( self, node: int, - q_index: int = -1, - *, - is_input: bool = False, - is_output: bool = False, ) -> None: """Add a physical node to the graph state. @@ -383,12 +333,6 @@ def add_physical_node( ---------- node : `int` node index - q_index : `int` - logical qubit index - is_input : `bool` - True if node is input node - is_output : `bool` - True if node is output node Raises ------ @@ -400,11 +344,6 @@ def add_physical_node( raise ValueError(msg) self.__physical_nodes |= {node} self.__physical_edges[node] = set() - self.set_q_index(node, q_index) - if is_input: - self.__input_nodes |= {node} - if is_output: - self.__output_nodes |= {node} self.__inner2nodes[self.__inner_index] = node self.__nodes2inner[node] = self.__inner_index @@ -463,10 +402,9 @@ def remove_physical_node(self, node: int) -> None: self.ensure_node_exists(node) self.__physical_nodes -= {node} del self.__physical_edges[node] - self.__input_nodes -= {node} - self.__output_nodes -= {node} + self.__input_node_indices.pop(node, None) + self.__output_node_indices.pop(node, None) self.__meas_bases.pop(node, None) - self.__q_indices.pop(node, None) self.__local_cliffords.pop(node, None) for neighbor in self.__physical_edges: self.__physical_edges[neighbor] -= {node} @@ -497,24 +435,28 @@ def remove_physical_edge(self, node1: int, node2: int) -> None: self.__physical_edges[node1] -= {node2} self.__physical_edges[node2] -= {node1} - def set_input(self, node: int) -> None: + def set_input(self, node: int, q_index: int) -> None: """Set the node as an input node. Parameters ---------- node : `int` node index + q_index : `int` + logical qubit index """ self.ensure_node_exists(node) - self.__input_nodes |= {node} + self.__input_node_indices[node] = q_index - def set_output(self, node: int) -> None: + def set_output(self, node: int, q_index: int) -> None: """Set the node as an output node. Parameters ---------- node : `int` node index + q_index : `int` + logical qubit index Raises ------ @@ -526,28 +468,7 @@ def set_output(self, node: int) -> None: if self.meas_bases.get(node) is not None: msg = "Cannot set output node with measurement basis." raise ValueError(msg) - self.__output_nodes |= {node} - - def set_q_index(self, node: int, q_index: int = -1) -> None: - """Set the qubit index of the node. - - Parameters - ---------- - node : `int` - node index - q_index: `int`, optional - logical qubit index, by default -1 - - Raises - ------ - ValueError - If the qubit index is invalid. - """ - self.ensure_node_exists(node) - if q_index < -1: - msg = f"Invalid qubit index {q_index}. Must be -1 or greater" - raise ValueError(msg) - self.__q_indices[node] = q_index + self.__output_node_indices[node] = q_index def set_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: """Set the measurement basis of the node. @@ -573,7 +494,7 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: local clifford operator """ self.ensure_node_exists(node) - if node in self.input_nodes or node in self.output_nodes: + if node in self.input_node_indices or node in self.output_node_indices: self.__local_cliffords[node] = lc else: new_meas_basis = update_lc_basis(lc.conjugate(), self.meas_bases[node]) @@ -596,15 +517,18 @@ def pop_local_clifford(self, node: int) -> LocalClifford | None: def parse_input_local_cliffords(self) -> None: """Parse local Clifford operators applied on the input nodes.""" - for input_node in self.input_nodes: + for input_node in self.input_node_indices: lc = self.pop_local_clifford(input_node) if lc is None: continue node_indices = (self.__inner_index, self.__inner_index + 1, self.__inner_index + 2) - self.add_physical_node(node_indices[0], q_index=self.q_indices[input_node], is_input=True) - self.add_physical_node(node_indices[1], q_index=self.q_indices[input_node]) - self.add_physical_node(node_indices[2], q_index=self.q_indices[input_node]) + self.add_physical_node(node_indices[0]) + self.set_input(node_indices[0], q_index=self.input_node_indices[input_node]) + self.add_physical_node(node_indices[1]) + self.set_input(node_indices[1], q_index=self.input_node_indices[input_node]) + self.add_physical_node(node_indices[2]) + self.set_input(node_indices[2], q_index=self.input_node_indices[input_node]) self.add_physical_edge(node_indices[0], node_indices[1]) self.add_physical_edge(node_indices[1], node_indices[2]) @@ -640,8 +564,8 @@ def _reset_input(self, node: int) -> None: node : `int` node index """ - if node in self.__input_nodes: - self.__input_nodes.remove(node) + if node in self.__input_node_indices: + self.__input_node_indices.pop(node) lc = self.pop_local_clifford(node) if lc is not None: self.apply_local_clifford(node, lc) @@ -654,8 +578,8 @@ def _reset_output(self, node: int) -> None: node : `int` node index """ - if node in self.__output_nodes: - self.__output_nodes.remove(node) + if node in self.__output_node_indices: + self.__output_node_indices.pop(node) def append(self, other: BaseGraphState) -> None: """Append another graph state to the current graph state. @@ -671,7 +595,7 @@ def append(self, other: BaseGraphState) -> None: If the qubit indices do not match. """ common_nodes = self.physical_nodes & other.physical_nodes - border_nodes = self.output_nodes & other.input_nodes + border_nodes = set(self.output_node_indices.keys()) & set(other.input_node_indices.keys()) if common_nodes != border_nodes: msg = "Qubit index mismatch" @@ -683,11 +607,11 @@ def append(self, other: BaseGraphState) -> None: self._reset_output(node) else: self.add_physical_node(node) - if node in other.input_nodes - self.output_nodes: - self.set_input(node) + if node in other.input_node_indices and node not in other.output_node_indices: + self.set_input(node, q_index=other.input_node_indices[node]) - if node in other.output_nodes: - self.set_output(node) + if node in other.output_node_indices: + self.set_output(node, q_index=other.output_node_indices[node]) else: meas_basis = other.meas_bases.get(node, default_meas_basis()) self.set_meas_basis(node, meas_basis) @@ -695,13 +619,6 @@ def append(self, other: BaseGraphState) -> None: for edge in other.physical_edges: self.add_physical_edge(edge[0], edge[1]) - # q_index update - for node, q_index in other.q_indices.items(): - if (node in common_nodes) and (self.q_indices[node] != q_index): - msg = "Qubit index mismatch." - raise ValueError(msg) - self.set_q_index(node, q_index) - def bipartite_edges(node_set1: set[int], node_set2: set[int]) -> set[tuple[int, int]]: r"""Return a set of edges for the complete bipartite graph between two sets of nodes. From f2a9a93fa64d5d499de263d0eab1a31189fd7a0a Mon Sep 17 00:00:00 2001 From: masa10-f Date: Tue, 8 Apr 2025 21:10:12 +0900 Subject: [PATCH 06/67] fix test of graphstate --- tests/test_graphstate.py | 60 +++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index 8c7abb876..0d2247f80 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -15,7 +15,7 @@ def graph() -> GraphState: Returns ------- - GraphState: An empty GraphState object. + GraphState: An empty GraphState object. """ return GraphState() @@ -29,9 +29,13 @@ def test_add_physical_node(graph: GraphState) -> None: def test_add_physical_node_input_output(graph: GraphState) -> None: """Test adding a physical node as input and output.""" - graph.add_physical_node(1, is_input=True, is_output=True) - assert 1 in graph.input_nodes - assert 1 in graph.output_nodes + graph.add_physical_node(1) + graph.set_input(1, 0) + graph.set_output(1, 0) + assert 1 in graph.input_node_indices + assert 1 in graph.output_node_indices + assert graph.input_node_indices[1] == 0 + assert graph.output_node_indices[1] == 0 def test_add_duplicate_physical_node(graph: GraphState) -> None: @@ -125,15 +129,15 @@ def test_remove_physical_node_from_3_nodes_graph(graph: GraphState) -> None: graph.add_physical_node(3) graph.add_physical_edge(1, 2) graph.add_physical_edge(2, 3) - graph.set_input(2) - graph.set_output(2) + graph.set_input(2, 0) + graph.set_output(2, 0) graph.remove_physical_node(2) assert graph.physical_nodes == {1, 3} assert graph.physical_edges == set() assert graph.num_physical_nodes == 2 assert graph.num_physical_edges == 0 - assert graph.input_nodes == set() - assert graph.output_nodes == set() + assert graph.input_node_indices == {} + assert graph.output_node_indices == {} def test_remove_physical_edge_with_nonexistent_nodes(graph: GraphState) -> None: @@ -161,34 +165,16 @@ def test_remove_physical_edge(graph: GraphState) -> None: assert graph.num_physical_edges == 0 -def test_set_input(graph: GraphState) -> None: - """Test setting a physical node as input.""" - graph.add_physical_node(1) - graph.set_input(1) - assert 1 in graph.input_nodes - - def test_set_output_raises_1(graph: GraphState) -> None: with pytest.raises(ValueError, match="Node does not exist node=1"): - graph.set_output(1) - graph.add_physical_node(1) - graph.set_meas_basis(1, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) - with pytest.raises(ValueError, match=r"Cannot set output node with measurement basis."): - graph.set_output(1) + graph.set_output(1, 0) def test_set_output_raises_2(graph: GraphState) -> None: graph.add_physical_node(1) graph.set_meas_basis(1, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) with pytest.raises(ValueError, match=r"Cannot set output node with measurement basis."): - graph.set_output(1) - - -def test_set_output(graph: GraphState) -> None: - """Test setting a physical node as output.""" - graph.add_physical_node(1) - graph.set_output(1) - assert 1 in graph.output_nodes + graph.set_output(1, 0) def test_set_meas_basis(graph: GraphState) -> None: @@ -203,21 +189,25 @@ def test_set_meas_basis(graph: GraphState) -> None: def test_append_graph() -> None: """Test appending a graph to another graph.""" graph1 = GraphState() - graph1.add_physical_node(1, is_input=True) - graph1.add_physical_node(2, is_output=True) + graph1.add_physical_node(1) + graph1.set_input(1, 0) + graph1.add_physical_node(2) + graph1.set_output(2, 0) graph1.add_physical_edge(1, 2) graph2 = GraphState() - graph2.add_physical_node(2, is_input=True) - graph2.add_physical_node(3, is_output=True) + graph2.add_physical_node(2) + graph2.set_input(2, 0) + graph2.add_physical_node(3) + graph2.set_output(3, 0) graph2.add_physical_edge(2, 3) graph1.append(graph2) assert graph1.num_physical_nodes == 3 assert graph1.num_physical_edges == 2 - assert 1 in graph1.input_nodes - assert 3 in graph1.output_nodes + assert 1 in graph1.input_node_indices + assert 3 in graph1.output_node_indices def test_check_meas_raises_value_error(graph: GraphState) -> None: @@ -237,7 +227,7 @@ def test_check_meas_basis_success(graph: GraphState) -> None: graph.add_physical_node(2) graph.add_physical_edge(1, 2) - graph.set_output(2) + graph.set_output(2, 0) graph.check_meas_basis() From 907ed1c0b7aa1263f272d3a0a48c8e1d653d7ee7 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Tue, 8 Apr 2025 21:12:44 +0900 Subject: [PATCH 07/67] specify error type --- tests/test_graphstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index 0d2247f80..747e627b2 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -41,7 +41,7 @@ def test_add_physical_node_input_output(graph: GraphState) -> None: def test_add_duplicate_physical_node(graph: GraphState) -> None: """Test adding a duplicate physical node to the graph.""" graph.add_physical_node(1) - with pytest.raises(Exception, match="Node already exists"): + with pytest.raises(ValueError, match="Node already exists"): graph.add_physical_node(1) From ad42976c99aa26a37ccdf6b3cc0ff5bb9ff2daca Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Sun, 13 Apr 2025 19:15:18 +0800 Subject: [PATCH 08/67] modify interface of add_physical_node --- graphix_zx/graphstate.py | 57 +++++++++++----------------------------- 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 8eed134fb..8d57e6cea 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -98,14 +98,13 @@ def local_cliffords(self) -> dict[int, LocalClifford]: @abstractmethod def add_physical_node( self, - node: int, - ) -> None: + ) -> int: """Add a physical node to the graph state. - Parameters - ---------- - node : `int` - node index + Returns + ------- + `int` + The node index intenally generated """ raise NotImplementedError @@ -202,8 +201,6 @@ class GraphState(BaseGraphState): __local_cliffords: dict[int, LocalClifford] __inner_index: int - __inner2nodes: dict[int, int] - __nodes2inner: dict[int, int] def __init__(self) -> None: self.__input_node_indices = {} @@ -214,8 +211,6 @@ def __init__(self) -> None: self.__local_cliffords = {} self.__inner_index = 0 - self.__inner2nodes = {} - self.__nodes2inner = {} @property def input_node_indices(self) -> dict[int, int]: @@ -325,30 +320,21 @@ def check_meas_basis(self) -> None: def add_physical_node( self, - node: int, - ) -> None: + ) -> int: """Add a physical node to the graph state. - Parameters - ---------- - node : `int` - node index - - Raises - ------ - ValueError - If the node already exists in the graph state. + Returns + ------- + `int` + The node index internally generated. """ - if node in self.__physical_nodes: - msg = f"Node already exists {node=}" - raise ValueError(msg) + node = self.__inner_index self.__physical_nodes |= {node} self.__physical_edges[node] = set() - - self.__inner2nodes[self.__inner_index] = node - self.__nodes2inner[node] = self.__inner_index self.__inner_index += 1 + return node + def ensure_node_exists(self, node: int) -> None: """Ensure that the node exists in the graph state. @@ -390,27 +376,14 @@ def remove_physical_node(self, node: int) -> None: Parameters ---------- node : `int` - - Raises - ------ - ValueError - If the node does not exist. """ - if node not in self.__physical_nodes: - msg = f"Node does not exist {node=}" - raise ValueError(msg) self.ensure_node_exists(node) self.__physical_nodes -= {node} + for neighbor in self.__physical_edges[node]: + self.__physical_edges[neighbor] -= {node} del self.__physical_edges[node] - self.__input_node_indices.pop(node, None) - self.__output_node_indices.pop(node, None) self.__meas_bases.pop(node, None) self.__local_cliffords.pop(node, None) - for neighbor in self.__physical_edges: - self.__physical_edges[neighbor] -= {node} - - self.__inner2nodes.pop(self.__nodes2inner[node]) - self.__nodes2inner.pop(node) def remove_physical_edge(self, node1: int, node2: int) -> None: """Remove a physical edge from the graph state. From 4ba895c22fe941c9bf577f62e1766e6cbcb2660f Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Sun, 13 Apr 2025 19:35:17 +0800 Subject: [PATCH 09/67] update parse_input_local_cliffords --- graphix_zx/graphstate.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 8d57e6cea..1bdc5bc50 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -488,30 +488,37 @@ def pop_local_clifford(self, node: int) -> LocalClifford | None: """ return self.__local_cliffords.pop(node, None) - def parse_input_local_cliffords(self) -> None: - """Parse local Clifford operators applied on the input nodes.""" + def parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: + r"""Parse local Clifford operators applied on the input nodes. + + Returns + ------- + `dict`\[`int`, `tuple`\[`int`, `int`, `int`\]\] + A dictionary mapping input node indices to the new node indices created. + """ + node_index_addition_map = {} for input_node in self.input_node_indices: lc = self.pop_local_clifford(input_node) if lc is None: continue - node_indices = (self.__inner_index, self.__inner_index + 1, self.__inner_index + 2) - self.add_physical_node(node_indices[0]) - self.set_input(node_indices[0], q_index=self.input_node_indices[input_node]) - self.add_physical_node(node_indices[1]) - self.set_input(node_indices[1], q_index=self.input_node_indices[input_node]) - self.add_physical_node(node_indices[2]) - self.set_input(node_indices[2], q_index=self.input_node_indices[input_node]) + new_node_index0 = self.add_physical_node() + self.set_input(new_node_index0, q_index=self.input_node_indices[input_node]) + new_node_index1 = self.add_physical_node() + new_node_index2 = self.add_physical_node() - self.add_physical_edge(node_indices[0], node_indices[1]) - self.add_physical_edge(node_indices[1], node_indices[2]) - self.add_physical_edge(node_indices[2], input_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.set_meas_basis(node_indices[0], PlannerMeasBasis(Plane.XY, lc.alpha)) - self.set_meas_basis(node_indices[1], PlannerMeasBasis(Plane.XY, lc.beta)) - self.set_meas_basis(node_indices[2], PlannerMeasBasis(Plane.XY, lc.gamma)) + self.set_meas_basis(new_node_index0, PlannerMeasBasis(Plane.XY, lc.alpha)) + self.set_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, lc.beta)) + self.set_meas_basis(new_node_index2, PlannerMeasBasis(Plane.XY, lc.gamma)) self._reset_input(input_node) + node_index_addition_map[input_node] = (new_node_index0, new_node_index1, new_node_index2) + + return node_index_addition_map def get_neighbors(self, node: int) -> set[int]: r"""Return the neighbors of the node. From 1f17cf5d5bc535a50a6db844e7cd6f9ed9088d1e Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Mon, 14 Apr 2025 00:20:20 +0800 Subject: [PATCH 10/67] remove append method --- graphix_zx/graphstate.py | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 1bdc5bc50..fec7938ac 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -561,44 +561,6 @@ def _reset_output(self, node: int) -> None: if node in self.__output_node_indices: self.__output_node_indices.pop(node) - def append(self, other: BaseGraphState) -> None: - """Append another graph state to the current graph state. - - Parameters - ---------- - other : `BaseGraphState` - another graph state to append - - Raises - ------ - ValueError - If the qubit indices do not match. - """ - common_nodes = self.physical_nodes & other.physical_nodes - border_nodes = set(self.output_node_indices.keys()) & set(other.input_node_indices.keys()) - - if common_nodes != border_nodes: - msg = "Qubit index mismatch" - raise ValueError(msg) - - for node in other.physical_nodes: - if node in border_nodes: - self._reset_input(node) - self._reset_output(node) - else: - self.add_physical_node(node) - if node in other.input_node_indices and node not in other.output_node_indices: - self.set_input(node, q_index=other.input_node_indices[node]) - - if node in other.output_node_indices: - self.set_output(node, q_index=other.output_node_indices[node]) - else: - meas_basis = other.meas_bases.get(node, default_meas_basis()) - self.set_meas_basis(node, meas_basis) - - for edge in other.physical_edges: - self.add_physical_edge(edge[0], edge[1]) - def bipartite_edges(node_set1: set[int], node_set2: set[int]) -> set[tuple[int, int]]: r"""Return a set of edges for the complete bipartite graph between two sets of nodes. From ad5bd4efb0ae229efb525ad78ce9cb8eff477e8a Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Mon, 14 Apr 2025 09:43:49 +0800 Subject: [PATCH 11/67] add random flow graph generator --- graphix_zx/random_objects.py | 82 ++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 graphix_zx/random_objects.py diff --git a/graphix_zx/random_objects.py b/graphix_zx/random_objects.py new file mode 100644 index 000000000..1f178591a --- /dev/null +++ b/graphix_zx/random_objects.py @@ -0,0 +1,82 @@ +"""Random object generator. + +This module provides: + +- `get_random_flow_graph`: Generate a random flow graph. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from graphix_zx.common import default_meas_basis +from graphix_zx.graphstate import GraphState + +if TYPE_CHECKING: + from numpy.random import Generator + + +def get_random_flow_graph( + width: int, + depth: int, + edge_p: float = 0.5, + rng: Generator | None = None, +) -> tuple[GraphState, dict[int, set[int]]]: + r"""Generate a random flow graph. + + Parameters + ---------- + width : `int` + The width of the graph. + depth : `int` + The depth of the graph. + edge_p : `float`, optional + The probability of adding an edge between two adjacent nodes. + Default is 0.5. + rng : `numpy.random.Generator`, optional + The random number generator. + Default is `None`. + + Returns + ------- + `GraphState` + The generated graph. + `dict`\[`int`, `set`\[`int`\]\] + The flow of the graph. + """ + graph = GraphState() + flow: dict[int, set[int]] = {} + + if rng is None: + rng = np.random.default_rng() + + # input nodes + for q_index in range(width): + node_index = graph.add_physical_node() + graph.set_input(node_index, q_index) + graph.set_meas_basis(node_index, default_meas_basis()) + + # internal nodes + for _ in range(depth - 2): + node_indices_layer = [] + for _ in range(width): + node_index = graph.add_physical_node() + graph.set_meas_basis(node_index, default_meas_basis()) + graph.add_physical_edge(node_index - width, node_index) + flow[node_index - width] = {node_index} + node_indices_layer.append(node_index) + + for w in range(width - 1): + if rng.random() < edge_p: + graph.add_physical_edge(node_indices_layer[w], node_indices_layer[w + 1]) + + # output nodes + for q_index in range(width): + node_index = graph.add_physical_node() + graph.set_output(node_index, q_index) + graph.add_physical_edge(node_index - width, node_index) + flow[node_index - width] = {node_index} + + return graph, flow From ebe0c1ef35181083eb6aafa3dd5b7a4f450fddf7 Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Mon, 14 Apr 2025 10:04:11 +0800 Subject: [PATCH 12/67] fix node removal --- graphix_zx/graphstate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index fec7938ac..fbb93a190 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -13,7 +13,7 @@ from itertools import product from typing import TYPE_CHECKING -from graphix_zx.common import MeasBasis, Plane, PlannerMeasBasis, default_meas_basis +from graphix_zx.common import MeasBasis, Plane, PlannerMeasBasis from graphix_zx.euler import update_lc_basis if TYPE_CHECKING: @@ -384,6 +384,8 @@ def remove_physical_node(self, node: int) -> None: del self.__physical_edges[node] self.__meas_bases.pop(node, None) self.__local_cliffords.pop(node, None) + self._reset_input(node) + self._reset_output(node) def remove_physical_edge(self, node1: int, node2: int) -> None: """Remove a physical edge from the graph state. From 4a653a3ae814d62fd04da0cde35531a977ec8e7c Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Mon, 14 Apr 2025 10:04:23 +0800 Subject: [PATCH 13/67] update test for graphstate --- tests/test_graphstate.py | 178 ++++++++++++++++----------------------- 1 file changed, 72 insertions(+), 106 deletions(-) diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index 747e627b2..7d1e4eeda 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -22,27 +22,20 @@ def graph() -> GraphState: def test_add_physical_node(graph: GraphState) -> None: """Test adding a physical node to the graph.""" - graph.add_physical_node(1) - assert 1 in graph.physical_nodes + node_index = graph.add_physical_node() + assert node_index in graph.physical_nodes assert graph.num_physical_nodes == 1 def test_add_physical_node_input_output(graph: GraphState) -> None: """Test adding a physical node as input and output.""" - graph.add_physical_node(1) - graph.set_input(1, 0) - graph.set_output(1, 0) - assert 1 in graph.input_node_indices - assert 1 in graph.output_node_indices - assert graph.input_node_indices[1] == 0 - assert graph.output_node_indices[1] == 0 - - -def test_add_duplicate_physical_node(graph: GraphState) -> None: - """Test adding a duplicate physical node to the graph.""" - graph.add_physical_node(1) - with pytest.raises(ValueError, match="Node already exists"): - graph.add_physical_node(1) + node_index = graph.add_physical_node() + graph.set_input(node_index, 0) + graph.set_output(node_index, 0) + assert node_index in graph.input_node_indices + assert node_index in graph.output_node_indices + assert graph.input_node_indices[node_index] == 0 + assert graph.output_node_indices[node_index] == 0 def test_ensure_node_exists_raises(graph: GraphState) -> None: @@ -53,45 +46,45 @@ def test_ensure_node_exists_raises(graph: GraphState) -> None: def test_ensure_node_exists(graph: GraphState) -> None: """Test ensuring a node exists in the graph.""" - graph.add_physical_node(1) - graph.ensure_node_exists(1) + node_index = graph.add_physical_node() + graph.ensure_node_exists(node_index) def test_get_neighbors(graph: GraphState) -> None: """Test getting the neighbors of a node in the graph.""" - graph.add_physical_node(1) - graph.add_physical_node(2) - graph.add_physical_node(3) - graph.add_physical_edge(1, 2) - graph.add_physical_edge(2, 3) - assert graph.get_neighbors(1) == {2} - assert graph.get_neighbors(2) == {1, 3} - assert graph.get_neighbors(3) == {2} + node_index1 = graph.add_physical_node() + node_index2 = graph.add_physical_node() + node_index3 = graph.add_physical_node() + graph.add_physical_edge(node_index1, node_index2) + graph.add_physical_edge(node_index2, node_index3) + assert graph.get_neighbors(node_index1) == {node_index2} + assert graph.get_neighbors(node_index2) == {node_index1, node_index3} + assert graph.get_neighbors(node_index3) == {node_index2} def test_add_physical_edge(graph: GraphState) -> None: """Test adding a physical edge to the graph.""" - graph.add_physical_node(1) - graph.add_physical_node(2) - graph.add_physical_edge(1, 2) - assert (1, 2) in graph.physical_edges or (2, 1) in graph.physical_edges + node_index1 = graph.add_physical_node() + node_index2 = graph.add_physical_node() + graph.add_physical_edge(node_index1, node_index2) + assert (node_index1, node_index2) in graph.physical_edges or (node_index2, node_index1) in graph.physical_edges assert graph.num_physical_edges == 1 def test_add_duplicate_physical_edge(graph: GraphState) -> None: """Test adding a duplicate physical edge to the graph.""" - graph.add_physical_node(1) - graph.add_physical_node(2) - graph.add_physical_edge(1, 2) - with pytest.raises(ValueError, match="Edge already exists node1=1, node2=2"): - graph.add_physical_edge(1, 2) + node_index1 = graph.add_physical_node() + node_index2 = graph.add_physical_node() + graph.add_physical_edge(node_index1, node_index2) + with pytest.raises(ValueError, match=f"Edge already exists node1={node_index1}, node2={node_index2}"): + graph.add_physical_edge(node_index1, node_index2) def test_add_edge_with_nonexistent_node(graph: GraphState) -> None: """Test adding an edge with a nonexistent node to the graph.""" - graph.add_physical_node(1) + node_index1 = graph.add_physical_node() with pytest.raises(ValueError, match="Node does not exist node=2"): - graph.add_physical_edge(1, 2) + graph.add_physical_edge(node_index1, 2) def test_remove_physical_node_with_nonexistent_node(graph: GraphState) -> None: @@ -102,38 +95,35 @@ def test_remove_physical_node_with_nonexistent_node(graph: GraphState) -> None: def test_remove_physical_node(graph: GraphState) -> None: """Test removing a physical node from the graph.""" - graph.add_physical_node(1) - graph.remove_physical_node(1) - assert 1 not in graph.physical_nodes + node_index = graph.add_physical_node() + graph.remove_physical_node(node_index) + assert node_index not in graph.physical_nodes assert graph.num_physical_nodes == 0 def test_remove_physical_node_from_minimal_graph(graph: GraphState) -> None: """Test removing a physical node from the graph with edges.""" - graph.add_physical_node(1) - graph.add_physical_node(2) - graph.add_physical_edge(1, 2) - graph.remove_physical_node(1) - assert 1 not in graph.physical_nodes - assert 2 in graph.physical_nodes - assert (1, 2) not in graph.physical_edges - assert (2, 1) not in graph.physical_edges + node_index1 = graph.add_physical_node() + node_index2 = graph.add_physical_node() + graph.add_physical_edge(node_index1, node_index2) + graph.remove_physical_node(node_index1) + assert node_index1 not in graph.physical_nodes + assert node_index2 in graph.physical_nodes assert graph.num_physical_nodes == 1 assert graph.num_physical_edges == 0 def test_remove_physical_node_from_3_nodes_graph(graph: GraphState) -> None: """Test removing a physical node from the graph with 3 nodes and edges.""" - graph.add_physical_node(1) - graph.add_physical_node(2) - graph.add_physical_node(3) - graph.add_physical_edge(1, 2) - graph.add_physical_edge(2, 3) - graph.set_input(2, 0) - graph.set_output(2, 0) - graph.remove_physical_node(2) - assert graph.physical_nodes == {1, 3} - assert graph.physical_edges == set() + node_index1 = graph.add_physical_node() + node_index2 = graph.add_physical_node() + node_index3 = graph.add_physical_node() + graph.add_physical_edge(node_index1, node_index2) + graph.add_physical_edge(node_index2, node_index3) + graph.set_input(node_index2, 0) + graph.set_output(node_index2, 0) + graph.remove_physical_node(node_index2) + assert graph.physical_nodes == {node_index1, node_index3} assert graph.num_physical_nodes == 2 assert graph.num_physical_edges == 0 assert graph.input_node_indices == {} @@ -142,26 +132,26 @@ def test_remove_physical_node_from_3_nodes_graph(graph: GraphState) -> None: def test_remove_physical_edge_with_nonexistent_nodes(graph: GraphState) -> None: """Test removing an edge with nonexistent nodes from the graph.""" - with pytest.raises(ValueError, match="Node does not exist node=1"): + with pytest.raises(ValueError, match="Node does not exist"): graph.remove_physical_edge(1, 2) def test_remove_physical_edge_with_nonexistent_edge(graph: GraphState) -> None: """Test removing a nonexistent edge from the graph.""" - graph.add_physical_node(1) - graph.add_physical_node(2) + node_index1 = graph.add_physical_node() + node_index2 = graph.add_physical_node() with pytest.raises(ValueError, match="Edge does not exist"): - graph.remove_physical_edge(1, 2) + graph.remove_physical_edge(node_index1, node_index2) def test_remove_physical_edge(graph: GraphState) -> None: """Test removing a physical edge from the graph.""" - graph.add_physical_node(1) - graph.add_physical_node(2) - graph.add_physical_edge(1, 2) - graph.remove_physical_edge(1, 2) - assert (1, 2) not in graph.physical_edges - assert (2, 1) not in graph.physical_edges + node_index1 = graph.add_physical_node() + node_index2 = graph.add_physical_node() + graph.add_physical_edge(node_index1, node_index2) + graph.remove_physical_edge(node_index1, node_index2) + assert (node_index1, node_index2) not in graph.physical_edges + assert (node_index2, node_index1) not in graph.physical_edges assert graph.num_physical_edges == 0 @@ -171,63 +161,39 @@ def test_set_output_raises_1(graph: GraphState) -> None: def test_set_output_raises_2(graph: GraphState) -> None: - graph.add_physical_node(1) - graph.set_meas_basis(1, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) + node_index = graph.add_physical_node() + graph.set_meas_basis(node_index, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) with pytest.raises(ValueError, match=r"Cannot set output node with measurement basis."): - graph.set_output(1, 0) + graph.set_output(node_index, 0) def test_set_meas_basis(graph: GraphState) -> None: """Test setting the measurement basis of a physical node.""" - graph.add_physical_node(1) + node_index = graph.add_physical_node() meas_basis = PlannerMeasBasis(Plane.XZ, 0.5 * np.pi) - graph.set_meas_basis(1, meas_basis) - assert graph.meas_bases[1].plane == Plane.XZ - assert graph.meas_bases[1].angle == 0.5 * np.pi - - -def test_append_graph() -> None: - """Test appending a graph to another graph.""" - graph1 = GraphState() - graph1.add_physical_node(1) - graph1.set_input(1, 0) - graph1.add_physical_node(2) - graph1.set_output(2, 0) - graph1.add_physical_edge(1, 2) - - graph2 = GraphState() - graph2.add_physical_node(2) - graph2.set_input(2, 0) - graph2.add_physical_node(3) - graph2.set_output(3, 0) - graph2.add_physical_edge(2, 3) - - graph1.append(graph2) - - assert graph1.num_physical_nodes == 3 - assert graph1.num_physical_edges == 2 - assert 1 in graph1.input_node_indices - assert 3 in graph1.output_node_indices + graph.set_meas_basis(node_index, meas_basis) + assert graph.meas_bases[node_index].plane == Plane.XZ + assert graph.meas_bases[node_index].angle == 0.5 * np.pi def test_check_meas_raises_value_error(graph: GraphState) -> None: """Test if measurement planes and angles are set improperly.""" - graph.add_physical_node(1) - with pytest.raises(ValueError, match="Measurement basis not set for node 1"): + node_index = graph.add_physical_node() + with pytest.raises(ValueError, match=f"Measurement basis not set for node {node_index}"): graph.check_meas_basis() def test_check_meas_basis_success(graph: GraphState) -> None: """Test if measurement planes and angles are set properly.""" graph.check_meas_basis() - graph.add_physical_node(1) + node_index1 = graph.add_physical_node() meas_basis = PlannerMeasBasis(Plane.XY, 0.5 * np.pi) - graph.set_meas_basis(1, meas_basis) + graph.set_meas_basis(node_index1, meas_basis) graph.check_meas_basis() - graph.add_physical_node(2) - graph.add_physical_edge(1, 2) - graph.set_output(2, 0) + node_index2 = graph.add_physical_node() + graph.add_physical_edge(node_index1, node_index2) + graph.set_output(node_index2, 0) graph.check_meas_basis() From 000180b94d1c7f6d95125fc3ff925fdd666eb944 Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Mon, 14 Apr 2025 10:11:38 +0800 Subject: [PATCH 14/67] fix mypy error in numpy.number --- graphix_zx/matrix.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/graphix_zx/matrix.py b/graphix_zx/matrix.py index 12812e3be..3fd460065 100644 --- a/graphix_zx/matrix.py +++ b/graphix_zx/matrix.py @@ -7,28 +7,22 @@ from __future__ import annotations -import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, TypeVar import numpy as np if TYPE_CHECKING: from numpy.typing import NDArray -if sys.version_info >= (3, 10): - Numeric = np.number -else: - from typing import Union +T = TypeVar("T", bound=np.number[Any]) - Numeric = Union[np.int64, np.float64, np.complex128] - -def is_unitary(mat: NDArray[Numeric]) -> bool: +def is_unitary(mat: NDArray[T]) -> bool: r"""Check if a matrix is unitary. Parameters ---------- - mat : `numpy.typing.NDArray`\[`numpy.number`\] + mat : `numpy.typing.NDArray`\[`T`] matrix to check Returns From ab97a763822bd24fdc8ec4a1de30ffe6bfd09a6c Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Mon, 14 Apr 2025 10:31:44 +0800 Subject: [PATCH 15/67] implement sequential composition --- graphix_zx/graphstate.py | 63 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index fbb93a190..c9d3b5b51 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -564,6 +564,69 @@ def _reset_output(self, node: int) -> None: self.__output_node_indices.pop(node) +def sequentialy_compose( # noqa: C901 + graph1: BaseGraphState, graph2: BaseGraphState +) -> tuple[BaseGraphState, dict[int, int], dict[int, int]]: + r"""Compose two graph states sequentially. + + Parameters + ---------- + graph1 : `BaseGraphState` + first graph state + graph2 : `BaseGraphState` + second graph state + + Returns + ------- + `tuple`\[`BaseGraphState`, `dict`\[`int`, `int`\], `dict`\[`int`, `int`\]\] + composed graph state, node map for graph1, node map for graph2 + + Raises + ------ + ValueError + If the logical qubit indices of output nodes in graph1 do not match input nodes in graph2. + """ + if set(graph1.output_node_indices.values()) != set(graph2.input_node_indices.values()): + msg = "Logical qubit indices of output nodes in graph1 must match input nodes in graph2." + raise ValueError(msg) + node_map1 = {} + node_map2 = {} + composed_graph = GraphState() + + for node in graph1.physical_nodes: + node_index = composed_graph.add_physical_node() + meas_basis = graph1.meas_bases.get(node, None) + if meas_basis is not None: + composed_graph.set_meas_basis(node_index, meas_basis) + lc = graph1.local_cliffords.get(node, None) + if lc is not None: + composed_graph.apply_local_clifford(node_index, lc) + node_map1[node] = node_index + + for node in graph2.physical_nodes: + node_index = composed_graph.add_physical_node() + meas_basis = graph2.meas_bases.get(node, None) + if meas_basis is not None: + composed_graph.set_meas_basis(node_index, meas_basis) + lc = graph2.local_cliffords.get(node, None) + if lc is not None: + composed_graph.apply_local_clifford(node_index, lc) + node_map2[node] = node_index + + for input_node, q_index in graph1.input_node_indices.items(): + composed_graph.set_input(node_map1[input_node], q_index) + + for output_node, q_index in graph2.output_node_indices.items(): + composed_graph.set_output(node_map2[output_node], q_index) + + for edge in graph1.physical_edges: + composed_graph.add_physical_edge(node_map1[edge[0]], node_map1[edge[1]]) + for edge in graph2.physical_edges: + composed_graph.add_physical_edge(node_map2[edge[0]], node_map2[edge[1]]) + + return composed_graph, node_map1, node_map2 + + def bipartite_edges(node_set1: set[int], node_set2: set[int]) -> set[tuple[int, int]]: r"""Return a set of edges for the complete bipartite graph between two sets of nodes. From b29682f2ddb813e6a4681b5c99fd466afa0551ff Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Mon, 14 Apr 2025 10:32:20 +0800 Subject: [PATCH 16/67] update docstring --- graphix_zx/graphstate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index c9d3b5b51..5626eaee7 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -4,6 +4,7 @@ - `BaseGraphState`: Abstract base class for Graph State. - `GraphState`: Minimal implementation of Graph State. +- `sequentialy_compose`: Function to compose two graph states sequentially. - `bipartite_edges`: Function to create a complete bipartite graph between two sets of nodes. """ From 9c0106de760f77df7f54aea7cb8ce19789083abb Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Mon, 14 Apr 2025 17:10:19 +0800 Subject: [PATCH 17/67] change the interface of q_index in input and output nodes --- graphix_zx/graphstate.py | 70 +++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 5626eaee7..7b41da15a 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -123,14 +123,17 @@ def add_physical_edge(self, node1: int, node2: int) -> None: raise NotImplementedError @abstractmethod - def set_input(self, node: int, q_index: int) -> None: + def set_input(self, node: int) -> int: """Set the node as an input node. Parameters ---------- node : `int` node index - q_index : `int` + + Returns + ------- + `int` logical qubit index """ raise NotImplementedError @@ -377,7 +380,16 @@ def remove_physical_node(self, node: int) -> None: Parameters ---------- node : `int` + node index to be removed + + Raises + ------ + ValueError + If the input node is specified """ + if node in self.input_node_indices: + msg = "The input node cannot be removed" + raise ValueError(msg) self.ensure_node_exists(node) self.__physical_nodes -= {node} for neighbor in self.__physical_edges[node]: @@ -385,8 +397,6 @@ def remove_physical_node(self, node: int) -> None: del self.__physical_edges[node] self.__meas_bases.pop(node, None) self.__local_cliffords.pop(node, None) - self._reset_input(node) - self._reset_output(node) def remove_physical_edge(self, node1: int, node2: int) -> None: """Remove a physical edge from the graph state. @@ -411,18 +421,23 @@ def remove_physical_edge(self, node1: int, node2: int) -> None: self.__physical_edges[node1] -= {node2} self.__physical_edges[node2] -= {node1} - def set_input(self, node: int, q_index: int) -> None: + def set_input(self, node: int) -> int: """Set the node as an input node. Parameters ---------- node : `int` node index - q_index : `int` + + Returns + ------- + `int` logical qubit index """ self.ensure_node_exists(node) + q_index = len(self.__input_node_indices) self.__input_node_indices[node] = q_index + return q_index def set_output(self, node: int, q_index: int) -> None: """Set the node as an output node. @@ -439,11 +454,15 @@ def set_output(self, node: int, q_index: int) -> None: ValueError 1. If the node does not exist. 2. If the node has a measurement basis. + 3. If the invalid q_index specified. """ self.ensure_node_exists(node) if self.meas_bases.get(node) is not None: msg = "Cannot set output node with measurement basis." raise ValueError(msg) + if q_index >= len(self.input_node_indices): + msg = "The q_index does not exist in input qubit indices" + raise ValueError(msg) self.__output_node_indices[node] = q_index def set_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: @@ -500,13 +519,14 @@ def parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: A dictionary mapping input node indices to the new node indices created. """ node_index_addition_map = {} - for input_node in self.input_node_indices: + new_input_indices = [] + for input_node in self.input_node_indices: # TODO: sort according to values lc = self.pop_local_clifford(input_node) if lc is None: continue new_node_index0 = self.add_physical_node() - self.set_input(new_node_index0, q_index=self.input_node_indices[input_node]) + new_input_indices.append(new_node_index0) new_node_index1 = self.add_physical_node() new_node_index2 = self.add_physical_node() @@ -518,9 +538,12 @@ def parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: self.set_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, lc.beta)) self.set_meas_basis(new_node_index2, PlannerMeasBasis(Plane.XY, lc.gamma)) - self._reset_input(input_node) node_index_addition_map[input_node] = (new_node_index0, new_node_index1, new_node_index2) + self.__input_node_indices = {} + for new_input_index in new_input_indices: + self.set_input(new_input_index) + return node_index_addition_map def get_neighbors(self, node: int) -> set[int]: @@ -539,31 +562,6 @@ def get_neighbors(self, node: int) -> set[int]: self.ensure_node_exists(node) return self.__physical_edges[node] - def _reset_input(self, node: int) -> None: - """Reset the input status of the node. - - Parameters - ---------- - node : `int` - node index - """ - if node in self.__input_node_indices: - self.__input_node_indices.pop(node) - lc = self.pop_local_clifford(node) - if lc is not None: - self.apply_local_clifford(node, lc) - - def _reset_output(self, node: int) -> None: - """Reset the output status of the node. - - Parameters - ---------- - node : `int` - node index - """ - if node in self.__output_node_indices: - self.__output_node_indices.pop(node) - def sequentialy_compose( # noqa: C901 graph1: BaseGraphState, graph2: BaseGraphState @@ -614,8 +612,8 @@ def sequentialy_compose( # noqa: C901 composed_graph.apply_local_clifford(node_index, lc) node_map2[node] = node_index - for input_node, q_index in graph1.input_node_indices.items(): - composed_graph.set_input(node_map1[input_node], q_index) + for input_node in graph1.input_node_indices: # TODO: sort according to q_index + composed_graph.set_input(node_map1[input_node]) for output_node, q_index in graph2.output_node_indices.items(): composed_graph.set_output(node_map2[output_node], q_index) From 46b87489f36c6b18073905622709aafa1c8fca32 Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Mon, 14 Apr 2025 17:10:40 +0800 Subject: [PATCH 18/67] update random object generator --- graphix_zx/random_objects.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/graphix_zx/random_objects.py b/graphix_zx/random_objects.py index 1f178591a..1e4436534 100644 --- a/graphix_zx/random_objects.py +++ b/graphix_zx/random_objects.py @@ -48,15 +48,17 @@ def get_random_flow_graph( """ graph = GraphState() flow: dict[int, set[int]] = {} + q_indices = [] if rng is None: rng = np.random.default_rng() # input nodes - for q_index in range(width): + for _ in range(width): node_index = graph.add_physical_node() - graph.set_input(node_index, q_index) + q_index = graph.set_input(node_index) graph.set_meas_basis(node_index, default_meas_basis()) + q_indices.append(q_index) # internal nodes for _ in range(depth - 2): @@ -73,9 +75,9 @@ def get_random_flow_graph( graph.add_physical_edge(node_indices_layer[w], node_indices_layer[w + 1]) # output nodes - for q_index in range(width): + for i in range(width): node_index = graph.add_physical_node() - graph.set_output(node_index, q_index) + graph.set_output(node_index, q_indices[i]) graph.add_physical_edge(node_index - width, node_index) flow[node_index - width] = {node_index} From 9216b15cf11e6adcba9cba2203e08d502c1fd4bf Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Mon, 14 Apr 2025 17:10:51 +0800 Subject: [PATCH 19/67] update graphstate test --- tests/test_graphstate.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index 7d1e4eeda..30574aeb6 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -30,12 +30,12 @@ def test_add_physical_node(graph: GraphState) -> None: def test_add_physical_node_input_output(graph: GraphState) -> None: """Test adding a physical node as input and output.""" node_index = graph.add_physical_node() - graph.set_input(node_index, 0) - graph.set_output(node_index, 0) + q_index = graph.set_input(node_index) + graph.set_output(node_index, q_index) assert node_index in graph.input_node_indices assert node_index in graph.output_node_indices - assert graph.input_node_indices[node_index] == 0 - assert graph.output_node_indices[node_index] == 0 + assert graph.input_node_indices[node_index] == q_index + assert graph.output_node_indices[node_index] == q_index def test_ensure_node_exists_raises(graph: GraphState) -> None: @@ -93,6 +93,14 @@ def test_remove_physical_node_with_nonexistent_node(graph: GraphState) -> None: graph.remove_physical_node(1) +def test_remove_physical_node_with_input_removal(graph: GraphState) -> None: + """Test removing an input node from the graph""" + node_index = graph.add_physical_node() + graph.set_input(node_index) + with pytest.raises(ValueError, match="The input node cannot be removed"): + graph.remove_physical_node(node_index) + + def test_remove_physical_node(graph: GraphState) -> None: """Test removing a physical node from the graph.""" node_index = graph.add_physical_node() @@ -120,14 +128,14 @@ def test_remove_physical_node_from_3_nodes_graph(graph: GraphState) -> None: node_index3 = graph.add_physical_node() graph.add_physical_edge(node_index1, node_index2) graph.add_physical_edge(node_index2, node_index3) - graph.set_input(node_index2, 0) - graph.set_output(node_index2, 0) + q_index = graph.set_input(node_index1) + graph.set_output(node_index3, q_index) graph.remove_physical_node(node_index2) assert graph.physical_nodes == {node_index1, node_index3} assert graph.num_physical_nodes == 2 assert graph.num_physical_edges == 0 - assert graph.input_node_indices == {} - assert graph.output_node_indices == {} + assert graph.input_node_indices == {node_index1: q_index} + assert graph.output_node_indices == {node_index3: q_index} def test_remove_physical_edge_with_nonexistent_nodes(graph: GraphState) -> None: @@ -187,13 +195,14 @@ def test_check_meas_basis_success(graph: GraphState) -> None: """Test if measurement planes and angles are set properly.""" graph.check_meas_basis() node_index1 = graph.add_physical_node() + q_index = graph.set_input(node_index1) meas_basis = PlannerMeasBasis(Plane.XY, 0.5 * np.pi) graph.set_meas_basis(node_index1, meas_basis) graph.check_meas_basis() node_index2 = graph.add_physical_node() graph.add_physical_edge(node_index1, node_index2) - graph.set_output(node_index2, 0) + graph.set_output(node_index2, q_index) graph.check_meas_basis() From af77484020cc033d91ee7fa2553040976b1fc207 Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Mon, 14 Apr 2025 19:10:45 +0800 Subject: [PATCH 20/67] resolve TODO with sorted iterator --- graphix_zx/graphstate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 7b41da15a..0e0bacbf0 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -10,6 +10,7 @@ from __future__ import annotations +import operator from abc import ABC, abstractmethod from itertools import product from typing import TYPE_CHECKING @@ -520,7 +521,7 @@ def parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: """ node_index_addition_map = {} new_input_indices = [] - for input_node in self.input_node_indices: # TODO: sort according to values + for input_node, _ in sorted(self.input_node_indices.items(), key=operator.itemgetter(1)): lc = self.pop_local_clifford(input_node) if lc is None: continue @@ -612,7 +613,7 @@ def sequentialy_compose( # noqa: C901 composed_graph.apply_local_clifford(node_index, lc) node_map2[node] = node_index - for input_node in graph1.input_node_indices: # TODO: sort according to q_index + for input_node, _ in sorted(graph1.input_node_indices.items(), key=operator.itemgetter(1)): composed_graph.set_input(node_map1[input_node]) for output_node, q_index in graph2.output_node_indices.items(): From 426bfa06ee35391438b086b731d4c053dddb7224 Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Mon, 14 Apr 2025 19:19:21 +0800 Subject: [PATCH 21/67] implement parallel composition --- graphix_zx/graphstate.py | 69 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 0e0bacbf0..2f66d47a8 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -4,7 +4,7 @@ - `BaseGraphState`: Abstract base class for Graph State. - `GraphState`: Minimal implementation of Graph State. -- `sequentialy_compose`: Function to compose two graph states sequentially. +- `sequential_compose`: Function to compose two graph states sequentially. - `bipartite_edges`: Function to create a complete bipartite graph between two sets of nodes. """ @@ -564,7 +564,7 @@ def get_neighbors(self, node: int) -> set[int]: return self.__physical_edges[node] -def sequentialy_compose( # noqa: C901 +def sequential_compose( # noqa: C901 graph1: BaseGraphState, graph2: BaseGraphState ) -> tuple[BaseGraphState, dict[int, int], dict[int, int]]: r"""Compose two graph states sequentially. @@ -627,6 +627,71 @@ def sequentialy_compose( # noqa: C901 return composed_graph, node_map1, node_map2 +def parallel_compose( # noqa: C901 + graph1: BaseGraphState, graph2: BaseGraphState +) -> tuple[BaseGraphState, dict[int, int], dict[int, int]]: + r"""Compose two graph states parallelly. + + Parameters + ---------- + graph1 : `BaseGraphState` + first graph state + graph2 : `BaseGraphState` + second graph state + + Returns + ------- + `tuple`\[`BaseGraphState`, `dict`\[`int`, `int`\], `dict`\[`int`, `int`\]\] + composed graph state, node map for graph1, node map for graph2 + """ + node_map1 = {} + node_map2 = {} + composed_graph = GraphState() + + for node in graph1.physical_nodes: + node_index = composed_graph.add_physical_node() + meas_basis = graph1.meas_bases.get(node, None) + if meas_basis is not None: + composed_graph.set_meas_basis(node_index, meas_basis) + lc = graph1.local_cliffords.get(node, None) + if lc is not None: + composed_graph.apply_local_clifford(node_index, lc) + node_map1[node] = node_index + + for node in graph2.physical_nodes: + node_index = composed_graph.add_physical_node() + meas_basis = graph2.meas_bases.get(node, None) + if meas_basis is not None: + composed_graph.set_meas_basis(node_index, meas_basis) + lc = graph2.local_cliffords.get(node, None) + if lc is not None: + composed_graph.apply_local_clifford(node_index, lc) + node_map2[node] = node_index + + q_index_map1 = {} + q_index_map2 = {} + for input_node, old_q_index in sorted(graph1.input_node_indices.items(), key=operator.itemgetter(1)): + new_q_index = composed_graph.set_input(node_map1[input_node]) + q_index_map1[old_q_index] = new_q_index + + for input_node, old_q_index in sorted(graph2.input_node_indices.items(), key=operator.itemgetter(1)): + new_q_index = composed_graph.set_input(node_map2[input_node]) + q_index_map2[old_q_index] = new_q_index + + for output_node, q_index in graph1.output_node_indices.items(): + composed_graph.set_output(node_map1[output_node], q_index_map1[q_index]) + + for output_node, q_index in graph2.output_node_indices.items(): + composed_graph.set_output(node_map2[output_node], q_index_map2[q_index]) + + for edge in graph1.physical_edges: + composed_graph.add_physical_edge(node_map1[edge[0]], node_map1[edge[1]]) + for edge in graph2.physical_edges: + composed_graph.add_physical_edge(node_map2[edge[0]], node_map2[edge[1]]) + + return composed_graph, node_map1, node_map2 + + def bipartite_edges(node_set1: set[int], node_set2: set[int]) -> set[tuple[int, int]]: r"""Return a set of edges for the complete bipartite graph between two sets of nodes. From cd2ec920959f6b5a56a00d7168875e61c285c70f Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Mon, 14 Apr 2025 19:20:06 +0800 Subject: [PATCH 22/67] update doc --- graphix_zx/graphstate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 2f66d47a8..df8e4bbe2 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -5,6 +5,7 @@ - `BaseGraphState`: Abstract base class for Graph State. - `GraphState`: Minimal implementation of Graph State. - `sequential_compose`: Function to compose two graph states sequentially. +- `parallel_compose`: Function to compose two graph states in parallel. - `bipartite_edges`: Function to create a complete bipartite graph between two sets of nodes. """ From d4dc700b5b09539bcbea93cb04febcd38dcd09fd Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Tue, 15 Apr 2025 00:25:09 +0800 Subject: [PATCH 23/67] fix docstring --- graphix_zx/matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix_zx/matrix.py b/graphix_zx/matrix.py index 3fd460065..1d5e870ac 100644 --- a/graphix_zx/matrix.py +++ b/graphix_zx/matrix.py @@ -22,7 +22,7 @@ def is_unitary(mat: NDArray[T]) -> bool: Parameters ---------- - mat : `numpy.typing.NDArray`\[`T`] + mat : `numpy.typing.NDArray`\[T] matrix to check Returns From 9f80ded8139f6298f0a6158e63aa22a9650afee4 Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Tue, 15 Apr 2025 00:25:27 +0800 Subject: [PATCH 24/67] update docs of graphstate --- docs/source/graphstate.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/graphstate.rst b/docs/source/graphstate.rst index c2e19bb4d..6352054e8 100644 --- a/docs/source/graphstate.rst +++ b/docs/source/graphstate.rst @@ -21,4 +21,6 @@ Graph State Classes Functions --------- +.. autofunction:: graphix_zx.graphstate.sequential_compose +.. autofunction:: graphix_zx.graphstate.parallel_compose .. autofunction:: graphix_zx.graphstate.bipartite_edges From c228ff93612a746d1fc424d917a390bcc1a6b1d9 Mon Sep 17 00:00:00 2001 From: Masato Fukushima Date: Tue, 15 Apr 2025 00:32:32 +0800 Subject: [PATCH 25/67] fix bipartite_edges --- graphix_zx/graphstate.py | 8 ++++++++ tests/test_graphstate.py | 4 +--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index df8e4bbe2..41abbe4a7 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -707,5 +707,13 @@ def bipartite_edges(node_set1: set[int], node_set2: set[int]) -> set[tuple[int, ------- `set`\[`tuple`\[`int`, `int`\] set of edges for the complete bipartite graph + + Raises + ------ + ValueError + If the two sets of nodes are not disjoint. """ + if node_set1 & node_set2: + msg = "The two sets of nodes must be disjoint." + raise ValueError(msg) return {(min(a, b), max(a, b)) for a, b in product(node_set1, node_set2) if a != b} diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index 30574aeb6..703ed785e 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -206,12 +206,10 @@ def test_check_meas_basis_success(graph: GraphState) -> None: graph.check_meas_basis() -def test_bipartite_edges(graph: GraphState) -> None: +def test_bipartite_edges() -> None: """Test the function that generate complete bipartite edges""" assert bipartite_edges(set(), set()) == set() - assert bipartite_edges({1, 2, 3}, {1, 2, 3}) == {(1, 2), (1, 3), (2, 3)} assert bipartite_edges({1, 2}, {3, 4}) == {(1, 3), (1, 4), (2, 3), (2, 4)} - graph.check_meas_basis() if __name__ == "__main__": From 6b3b7355bba05ba137cdd66977b9226a3f9f895e Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:51:55 +0900 Subject: [PATCH 26/67] :recycle: Iterate container directly --- graphix_zx/random_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphix_zx/random_objects.py b/graphix_zx/random_objects.py index 1e4436534..73a5f4d7f 100644 --- a/graphix_zx/random_objects.py +++ b/graphix_zx/random_objects.py @@ -75,9 +75,9 @@ def get_random_flow_graph( graph.add_physical_edge(node_indices_layer[w], node_indices_layer[w + 1]) # output nodes - for i in range(width): + for qi in q_indices: node_index = graph.add_physical_node() - graph.set_output(node_index, q_indices[i]) + graph.set_output(node_index, qi) graph.add_physical_edge(node_index - width, node_index) flow[node_index - width] = {node_index} From acc536eab1bd906e542f9bafa943f107adf53f7b Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:53:15 +0900 Subject: [PATCH 27/67] :recycle: Remove unnecessary TypeVar --- graphix_zx/matrix.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/graphix_zx/matrix.py b/graphix_zx/matrix.py index 1d5e870ac..91f0b36b4 100644 --- a/graphix_zx/matrix.py +++ b/graphix_zx/matrix.py @@ -7,22 +7,20 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING import numpy as np if TYPE_CHECKING: from numpy.typing import NDArray -T = TypeVar("T", bound=np.number[Any]) - -def is_unitary(mat: NDArray[T]) -> bool: +def is_unitary(mat: NDArray[np.number]) -> bool: r"""Check if a matrix is unitary. Parameters ---------- - mat : `numpy.typing.NDArray`\[T] + mat : `numpy.typing.NDArray`\[`numpy.number`\] matrix to check Returns From 2c34853b6dcd88b8e21af8d14ec206db6c1a7978 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:54:57 +0900 Subject: [PATCH 28/67] :recycle: Remove unnecessary raise Because abstractmethod cannot be called in the first place --- graphix_zx/graphstate.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 41abbe4a7..37a8e1ec5 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -36,7 +36,6 @@ def input_node_indices(self) -> dict[int, int]: `dict`\[`int`, `int`\] qubit indices map of input nodes. """ - raise NotImplementedError @property @abstractmethod @@ -48,7 +47,6 @@ def output_node_indices(self) -> dict[int, int]: `dict`\[`int`, `int`\] qubit indices map of output nodes. """ - raise NotImplementedError @property @abstractmethod @@ -60,7 +58,6 @@ def physical_nodes(self) -> set[int]: `set`\[`int`\] set of physical nodes. """ - raise NotImplementedError @property @abstractmethod @@ -72,7 +69,6 @@ def physical_edges(self) -> set[tuple[int, int]]: `set`\[`tuple`\[`int`, `int`\]` set of physical edges. """ - raise NotImplementedError @property @abstractmethod @@ -84,7 +80,6 @@ def meas_bases(self) -> dict[int, MeasBasis]: `dict`\[`int`, `MeasBasis`\] measurement bases of each physical node. """ - raise NotImplementedError @property @abstractmethod @@ -96,7 +91,6 @@ def local_cliffords(self) -> dict[int, LocalClifford]: `dict`\[`int`, `LocalClifford`\] local clifford nodes. """ - raise NotImplementedError @abstractmethod def add_physical_node( @@ -109,7 +103,6 @@ def add_physical_node( `int` The node index intenally generated """ - raise NotImplementedError @abstractmethod def add_physical_edge(self, node1: int, node2: int) -> None: @@ -122,7 +115,6 @@ def add_physical_edge(self, node1: int, node2: int) -> None: node2 : `int` node index """ - raise NotImplementedError @abstractmethod def set_input(self, node: int) -> int: @@ -138,7 +130,6 @@ def set_input(self, node: int) -> int: `int` logical qubit index """ - raise NotImplementedError @abstractmethod def set_output(self, node: int, q_index: int) -> None: @@ -151,7 +142,6 @@ def set_output(self, node: int, q_index: int) -> None: q_index : `int` logical qubit index """ - raise NotImplementedError @abstractmethod def set_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: @@ -164,7 +154,6 @@ def set_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: meas_basis : `MeasBasis` measurement basis """ - raise NotImplementedError @abstractmethod def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: @@ -177,7 +166,6 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: lc : `LocalClifford` local clifford operator """ - raise NotImplementedError @abstractmethod def get_neighbors(self, node: int) -> set[int]: @@ -193,7 +181,6 @@ def get_neighbors(self, node: int) -> set[int]: `set`\[`int`\] set of neighboring nodes """ - raise NotImplementedError class GraphState(BaseGraphState): From 044324b9f2c3ae5d4dd6cb2e2981d663ede08942 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:00:10 +0900 Subject: [PATCH 29/67] :recycle: Use unpack --- graphix_zx/graphstate.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 37a8e1ec5..6ed558a34 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -607,10 +607,10 @@ def sequential_compose( # noqa: C901 for output_node, q_index in graph2.output_node_indices.items(): composed_graph.set_output(node_map2[output_node], q_index) - for edge in graph1.physical_edges: - composed_graph.add_physical_edge(node_map1[edge[0]], node_map1[edge[1]]) - for edge in graph2.physical_edges: - composed_graph.add_physical_edge(node_map2[edge[0]], node_map2[edge[1]]) + for u, v in graph1.physical_edges: + composed_graph.add_physical_edge(node_map1[u], node_map1[v]) + for u, v in graph2.physical_edges: + composed_graph.add_physical_edge(node_map2[u], node_map2[v]) return composed_graph, node_map1, node_map2 @@ -672,10 +672,10 @@ def parallel_compose( # noqa: C901 for output_node, q_index in graph2.output_node_indices.items(): composed_graph.set_output(node_map2[output_node], q_index_map2[q_index]) - for edge in graph1.physical_edges: - composed_graph.add_physical_edge(node_map1[edge[0]], node_map1[edge[1]]) - for edge in graph2.physical_edges: - composed_graph.add_physical_edge(node_map2[edge[0]], node_map2[edge[1]]) + for u, v in graph1.physical_edges: + composed_graph.add_physical_edge(node_map1[u], node_map1[v]) + for u, v in graph2.physical_edges: + composed_graph.add_physical_edge(node_map2[u], node_map2[v]) return composed_graph, node_map1, node_map2 From 5c5e9ec43425c363dd278107f4b13077222ca0d3 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:03:38 +0900 Subject: [PATCH 30/67] :recycle: Use override deco --- graphix_zx/graphstate.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 6ed558a34..cdb62d294 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -16,6 +16,8 @@ from itertools import product from typing import TYPE_CHECKING +import typing_extensions + from graphix_zx.common import MeasBasis, Plane, PlannerMeasBasis from graphix_zx.euler import update_lc_basis @@ -206,6 +208,7 @@ def __init__(self) -> None: self.__inner_index = 0 @property + @typing_extensions.override def input_node_indices(self) -> dict[int, int]: r"""Return map of input nodes. @@ -217,6 +220,7 @@ def input_node_indices(self) -> dict[int, int]: return self.__input_node_indices @property + @typing_extensions.override def output_node_indices(self) -> dict[int, int]: r"""Return map of output nodes. @@ -250,6 +254,7 @@ def num_physical_edges(self) -> int: return sum(len(edges) for edges in self.__physical_edges.values()) // 2 @property + @typing_extensions.override def physical_nodes(self) -> set[int]: r"""Return set of physical nodes. @@ -261,6 +266,7 @@ def physical_nodes(self) -> set[int]: return self.__physical_nodes @property + @typing_extensions.override def physical_edges(self) -> set[tuple[int, int]]: r"""Return set of physical edges. @@ -277,6 +283,7 @@ def physical_edges(self) -> set[tuple[int, int]]: return edges @property + @typing_extensions.override def meas_bases(self) -> dict[int, MeasBasis]: r"""Return measurement bases. @@ -288,6 +295,7 @@ def meas_bases(self) -> dict[int, MeasBasis]: return self.__meas_bases @property + @typing_extensions.override def local_cliffords(self) -> dict[int, LocalClifford]: r"""Return local clifford nodes. @@ -311,6 +319,7 @@ def check_meas_basis(self) -> None: msg = f"Measurement basis not set for node {v}" raise ValueError(msg) + @typing_extensions.override def add_physical_node( self, ) -> int: @@ -340,6 +349,7 @@ def ensure_node_exists(self, node: int) -> None: msg = f"Node does not exist {node=}" raise ValueError(msg) + @typing_extensions.override def add_physical_edge(self, node1: int, node2: int) -> None: """Add a physical edge to the graph state. @@ -410,6 +420,7 @@ def remove_physical_edge(self, node1: int, node2: int) -> None: self.__physical_edges[node1] -= {node2} self.__physical_edges[node2] -= {node1} + @typing_extensions.override def set_input(self, node: int) -> int: """Set the node as an input node. @@ -428,6 +439,7 @@ def set_input(self, node: int) -> int: self.__input_node_indices[node] = q_index return q_index + @typing_extensions.override def set_output(self, node: int, q_index: int) -> None: """Set the node as an output node. @@ -454,6 +466,7 @@ def set_output(self, node: int, q_index: int) -> None: raise ValueError(msg) self.__output_node_indices[node] = q_index + @typing_extensions.override def set_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: """Set the measurement basis of the node. @@ -467,6 +480,7 @@ def set_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: self.ensure_node_exists(node) self.__meas_bases[node] = meas_basis + @typing_extensions.override def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: """Apply a local clifford to the node. @@ -535,6 +549,7 @@ def parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: return node_index_addition_map + @typing_extensions.override def get_neighbors(self, node: int) -> set[int]: r"""Return the neighbors of the node. From c104961178d17e2c300c7368a2756dc2bf87303d Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:04:24 +0900 Subject: [PATCH 31/67] :zap: Use isdisjoint method --- graphix_zx/graphstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index cdb62d294..5c4cffa20 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -715,7 +715,7 @@ def bipartite_edges(node_set1: set[int], node_set2: set[int]) -> set[tuple[int, ValueError If the two sets of nodes are not disjoint. """ - if node_set1 & node_set2: + if not node_set1.isdisjoint(node_set2): msg = "The two sets of nodes must be disjoint." raise ValueError(msg) return {(min(a, b), max(a, b)) for a, b in product(node_set1, node_set2) if a != b} From 44935be38223e5701ba0cb48b90ce1b82c88a487 Mon Sep 17 00:00:00 2001 From: SS <66886825+EarlMilktea@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:06:24 +0900 Subject: [PATCH 32/67] :recycle: Use ABC for input types --- graphix_zx/graphstate.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 5c4cffa20..3209b4651 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -22,6 +22,8 @@ from graphix_zx.euler import update_lc_basis if TYPE_CHECKING: + from collections.abc import Set as AbstractSet + from graphix_zx.euler import LocalClifford @@ -695,14 +697,14 @@ def parallel_compose( # noqa: C901 return composed_graph, node_map1, node_map2 -def bipartite_edges(node_set1: set[int], node_set2: set[int]) -> set[tuple[int, int]]: +def bipartite_edges(node_set1: AbstractSet[int], node_set2: AbstractSet[int]) -> set[tuple[int, int]]: r"""Return a set of edges for the complete bipartite graph between two sets of nodes. Parameters ---------- - node_set1 : `set`\[`int`\] + node_set1 : `collections.abc.Set`\[`int`\] set of nodes - node_set2 : `set`\[`int`\] + node_set2 : `collections.abc.Set`\[`int`\] set of nodes Returns From da952ae4e7bac5a61148f6f0688808b3c69dd0ad Mon Sep 17 00:00:00 2001 From: masa10-f Date: Fri, 18 Apr 2025 18:59:24 +0900 Subject: [PATCH 33/67] remove get_ --- graphix_zx/graphstate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 3209b4651..a7769a061 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -172,7 +172,7 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: """ @abstractmethod - def get_neighbors(self, node: int) -> set[int]: + def neighbors(self, node: int) -> set[int]: r"""Return the neighbors of the node. Parameters @@ -552,7 +552,7 @@ def parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: return node_index_addition_map @typing_extensions.override - def get_neighbors(self, node: int) -> set[int]: + def neighbors(self, node: int) -> set[int]: r"""Return the neighbors of the node. Parameters From 99484842c27e94f765fc160f0986066e223fff97 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 18:26:37 +0900 Subject: [PATCH 34/67] update test of graphstate --- tests/test_graphstate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index 703ed785e..26a0f675b 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -50,16 +50,16 @@ def test_ensure_node_exists(graph: GraphState) -> None: graph.ensure_node_exists(node_index) -def test_get_neighbors(graph: GraphState) -> None: +def test_neighbors(graph: GraphState) -> None: """Test getting the neighbors of a node in the graph.""" node_index1 = graph.add_physical_node() node_index2 = graph.add_physical_node() node_index3 = graph.add_physical_node() graph.add_physical_edge(node_index1, node_index2) graph.add_physical_edge(node_index2, node_index3) - assert graph.get_neighbors(node_index1) == {node_index2} - assert graph.get_neighbors(node_index2) == {node_index1, node_index3} - assert graph.get_neighbors(node_index3) == {node_index2} + assert graph.neighbors(node_index1) == {node_index2} + assert graph.neighbors(node_index2) == {node_index1, node_index3} + assert graph.neighbors(node_index3) == {node_index2} def test_add_physical_edge(graph: GraphState) -> None: From b20d291f3f042a2cd6d789613a15dda285e27c8f Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 18:34:58 +0900 Subject: [PATCH 35/67] use mark_in/output instead of set_ --- graphix_zx/graphstate.py | 30 +++++++++++++++--------------- tests/test_graphstate.py | 22 +++++++++++----------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index a7769a061..cc7dd0938 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -121,8 +121,8 @@ def add_physical_edge(self, node1: int, node2: int) -> None: """ @abstractmethod - def set_input(self, node: int) -> int: - """Set the node as an input node. + def mark_input(self, node: int) -> int: + """Mark the node as an input node. Parameters ---------- @@ -136,8 +136,8 @@ def set_input(self, node: int) -> int: """ @abstractmethod - def set_output(self, node: int, q_index: int) -> None: - """Set the node as an output node. + def mark_output(self, node: int, q_index: int) -> None: + """Mark the node as an output node. Parameters ---------- @@ -423,8 +423,8 @@ def remove_physical_edge(self, node1: int, node2: int) -> None: self.__physical_edges[node2] -= {node1} @typing_extensions.override - def set_input(self, node: int) -> int: - """Set the node as an input node. + def mark_input(self, node: int) -> int: + """Mark the node as an input node. Parameters ---------- @@ -442,8 +442,8 @@ def set_input(self, node: int) -> int: return q_index @typing_extensions.override - def set_output(self, node: int, q_index: int) -> None: - """Set the node as an output node. + def mark_output(self, node: int, q_index: int) -> None: + """Mark the node as an output node. Parameters ---------- @@ -547,7 +547,7 @@ def parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: self.__input_node_indices = {} for new_input_index in new_input_indices: - self.set_input(new_input_index) + self.mark_input(new_input_index) return node_index_addition_map @@ -619,10 +619,10 @@ def sequential_compose( # noqa: C901 node_map2[node] = node_index for input_node, _ in sorted(graph1.input_node_indices.items(), key=operator.itemgetter(1)): - composed_graph.set_input(node_map1[input_node]) + composed_graph.mark_input(node_map1[input_node]) for output_node, q_index in graph2.output_node_indices.items(): - composed_graph.set_output(node_map2[output_node], q_index) + composed_graph.mark_output(node_map2[output_node], q_index) for u, v in graph1.physical_edges: composed_graph.add_physical_edge(node_map1[u], node_map1[v]) @@ -676,18 +676,18 @@ def parallel_compose( # noqa: C901 q_index_map1 = {} q_index_map2 = {} for input_node, old_q_index in sorted(graph1.input_node_indices.items(), key=operator.itemgetter(1)): - new_q_index = composed_graph.set_input(node_map1[input_node]) + new_q_index = composed_graph.mark_input(node_map1[input_node]) q_index_map1[old_q_index] = new_q_index for input_node, old_q_index in sorted(graph2.input_node_indices.items(), key=operator.itemgetter(1)): - new_q_index = composed_graph.set_input(node_map2[input_node]) + new_q_index = composed_graph.mark_input(node_map2[input_node]) q_index_map2[old_q_index] = new_q_index for output_node, q_index in graph1.output_node_indices.items(): - composed_graph.set_output(node_map1[output_node], q_index_map1[q_index]) + composed_graph.mark_output(node_map1[output_node], q_index_map1[q_index]) for output_node, q_index in graph2.output_node_indices.items(): - composed_graph.set_output(node_map2[output_node], q_index_map2[q_index]) + composed_graph.mark_output(node_map2[output_node], q_index_map2[q_index]) for u, v in graph1.physical_edges: composed_graph.add_physical_edge(node_map1[u], node_map1[v]) diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index 26a0f675b..e21147837 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -30,8 +30,8 @@ def test_add_physical_node(graph: GraphState) -> None: def test_add_physical_node_input_output(graph: GraphState) -> None: """Test adding a physical node as input and output.""" node_index = graph.add_physical_node() - q_index = graph.set_input(node_index) - graph.set_output(node_index, q_index) + q_index = graph.mark_input(node_index) + graph.mark_output(node_index, q_index) assert node_index in graph.input_node_indices assert node_index in graph.output_node_indices assert graph.input_node_indices[node_index] == q_index @@ -96,7 +96,7 @@ def test_remove_physical_node_with_nonexistent_node(graph: GraphState) -> None: def test_remove_physical_node_with_input_removal(graph: GraphState) -> None: """Test removing an input node from the graph""" node_index = graph.add_physical_node() - graph.set_input(node_index) + graph.mark_input(node_index) with pytest.raises(ValueError, match="The input node cannot be removed"): graph.remove_physical_node(node_index) @@ -128,8 +128,8 @@ def test_remove_physical_node_from_3_nodes_graph(graph: GraphState) -> None: node_index3 = graph.add_physical_node() graph.add_physical_edge(node_index1, node_index2) graph.add_physical_edge(node_index2, node_index3) - q_index = graph.set_input(node_index1) - graph.set_output(node_index3, q_index) + q_index = graph.mark_input(node_index1) + graph.mark_output(node_index3, q_index) graph.remove_physical_node(node_index2) assert graph.physical_nodes == {node_index1, node_index3} assert graph.num_physical_nodes == 2 @@ -163,16 +163,16 @@ def test_remove_physical_edge(graph: GraphState) -> None: assert graph.num_physical_edges == 0 -def test_set_output_raises_1(graph: GraphState) -> None: +def test_mark_output_raises_1(graph: GraphState) -> None: with pytest.raises(ValueError, match="Node does not exist node=1"): - graph.set_output(1, 0) + graph.mark_output(1, 0) -def test_set_output_raises_2(graph: GraphState) -> None: +def test_mark_output_raises_2(graph: GraphState) -> None: node_index = graph.add_physical_node() graph.set_meas_basis(node_index, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) with pytest.raises(ValueError, match=r"Cannot set output node with measurement basis."): - graph.set_output(node_index, 0) + graph.mark_output(node_index, 0) def test_set_meas_basis(graph: GraphState) -> None: @@ -195,14 +195,14 @@ def test_check_meas_basis_success(graph: GraphState) -> None: """Test if measurement planes and angles are set properly.""" graph.check_meas_basis() node_index1 = graph.add_physical_node() - q_index = graph.set_input(node_index1) + q_index = graph.mark_input(node_index1) meas_basis = PlannerMeasBasis(Plane.XY, 0.5 * np.pi) graph.set_meas_basis(node_index1, meas_basis) graph.check_meas_basis() node_index2 = graph.add_physical_node() graph.add_physical_edge(node_index1, node_index2) - graph.set_output(node_index2, q_index) + graph.mark_output(node_index2, q_index) graph.check_meas_basis() From 5e53e5d23df995bef0518aded6c043b019bbb69a Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 18:40:01 +0900 Subject: [PATCH 36/67] ignore SLF001 in tests for verifing some private methods --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 86fa371b6..7d89a5f45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ docstring-code-format = true [tool.ruff.lint.per-file-ignores] "tests/*.py" = [ "S101", # `assert` detected + "SLF001", # private method "PLR2004", # magic value in test(should be removed) "D100", "D103", From d01c0620cee18c46fcaaa15a1326149a048d4586 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 18:40:56 +0900 Subject: [PATCH 37/67] make auxiliary methods into private --- graphix_zx/graphstate.py | 80 ++++++++++++++++++++-------------------- tests/test_graphstate.py | 12 +++--- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index cc7dd0938..25757c48c 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -308,7 +308,7 @@ def local_cliffords(self) -> dict[int, LocalClifford]: """ return self.__local_cliffords - def check_meas_basis(self) -> None: + def _check_meas_basis(self) -> None: """Check if the measurement basis is set for all physical nodes except output nodes. Raises @@ -321,6 +321,18 @@ def check_meas_basis(self) -> None: msg = f"Measurement basis not set for node {v}" raise ValueError(msg) + def _ensure_node_exists(self, node: int) -> None: + """Ensure that the node exists in the graph state. + + Raises + ------ + ValueError + If the node does not exist in the graph state. + """ + if node not in self.__physical_nodes: + msg = f"Node does not exist {node=}" + raise ValueError(msg) + @typing_extensions.override def add_physical_node( self, @@ -339,18 +351,6 @@ def add_physical_node( return node - def ensure_node_exists(self, node: int) -> None: - """Ensure that the node exists in the graph state. - - Raises - ------ - ValueError - If the node does not exist in the graph state. - """ - if node not in self.__physical_nodes: - msg = f"Node does not exist {node=}" - raise ValueError(msg) - @typing_extensions.override def add_physical_edge(self, node1: int, node2: int) -> None: """Add a physical edge to the graph state. @@ -367,8 +367,8 @@ def add_physical_edge(self, node1: int, node2: int) -> None: ValueError If the edge already exists. """ - self.ensure_node_exists(node1) - self.ensure_node_exists(node2) + self._ensure_node_exists(node1) + self._ensure_node_exists(node2) if node1 in self.__physical_edges[node2] or node2 in self.__physical_edges[node1]: msg = f"Edge already exists {node1=}, {node2=}" raise ValueError(msg) @@ -391,7 +391,7 @@ def remove_physical_node(self, node: int) -> None: if node in self.input_node_indices: msg = "The input node cannot be removed" raise ValueError(msg) - self.ensure_node_exists(node) + self._ensure_node_exists(node) self.__physical_nodes -= {node} for neighbor in self.__physical_edges[node]: self.__physical_edges[neighbor] -= {node} @@ -414,8 +414,8 @@ def remove_physical_edge(self, node1: int, node2: int) -> None: ValueError If the edge does not exist. """ - self.ensure_node_exists(node1) - self.ensure_node_exists(node2) + self._ensure_node_exists(node1) + self._ensure_node_exists(node2) if node1 not in self.__physical_edges[node2] or node2 not in self.__physical_edges[node1]: msg = "Edge does not exist" raise ValueError(msg) @@ -436,7 +436,7 @@ def mark_input(self, node: int) -> int: `int` logical qubit index """ - self.ensure_node_exists(node) + self._ensure_node_exists(node) q_index = len(self.__input_node_indices) self.__input_node_indices[node] = q_index return q_index @@ -459,7 +459,7 @@ def mark_output(self, node: int, q_index: int) -> None: 2. If the node has a measurement basis. 3. If the invalid q_index specified. """ - self.ensure_node_exists(node) + self._ensure_node_exists(node) if self.meas_bases.get(node) is not None: msg = "Cannot set output node with measurement basis." raise ValueError(msg) @@ -479,7 +479,7 @@ def set_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: meas_basis : `MeasBasis` measurement basis """ - self.ensure_node_exists(node) + self._ensure_node_exists(node) self.__meas_bases[node] = meas_basis @typing_extensions.override @@ -493,7 +493,7 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: lc : `LocalClifford` local clifford operator """ - self.ensure_node_exists(node) + self._ensure_node_exists(node) if node in self.input_node_indices or node in self.output_node_indices: self.__local_cliffords[node] = lc else: @@ -515,7 +515,24 @@ def pop_local_clifford(self, node: int) -> LocalClifford | None: """ return self.__local_cliffords.pop(node, None) - def parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: + @typing_extensions.override + def neighbors(self, node: int) -> set[int]: + r"""Return the neighbors of the node. + + Parameters + ---------- + node : `int` + node index + + Returns + ------- + `set`\[`int`\] + set of neighboring nodes + """ + self._ensure_node_exists(node) + return self.__physical_edges[node] + + def _parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: r"""Parse local Clifford operators applied on the input nodes. Returns @@ -551,23 +568,6 @@ def parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: return node_index_addition_map - @typing_extensions.override - def neighbors(self, node: int) -> set[int]: - r"""Return the neighbors of the node. - - Parameters - ---------- - node : `int` - node index - - Returns - ------- - `set`\[`int`\] - set of neighboring nodes - """ - self.ensure_node_exists(node) - return self.__physical_edges[node] - def sequential_compose( # noqa: C901 graph1: BaseGraphState, graph2: BaseGraphState diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index e21147837..fcdc233be 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -41,13 +41,13 @@ def test_add_physical_node_input_output(graph: GraphState) -> None: def test_ensure_node_exists_raises(graph: GraphState) -> None: """Test ensuring a node exists in the graph.""" with pytest.raises(ValueError, match="Node does not exist node=1"): - graph.ensure_node_exists(1) + graph._ensure_node_exists(1) def test_ensure_node_exists(graph: GraphState) -> None: """Test ensuring a node exists in the graph.""" node_index = graph.add_physical_node() - graph.ensure_node_exists(node_index) + graph._ensure_node_exists(node_index) def test_neighbors(graph: GraphState) -> None: @@ -188,22 +188,22 @@ def test_check_meas_raises_value_error(graph: GraphState) -> None: """Test if measurement planes and angles are set improperly.""" node_index = graph.add_physical_node() with pytest.raises(ValueError, match=f"Measurement basis not set for node {node_index}"): - graph.check_meas_basis() + graph._check_meas_basis() def test_check_meas_basis_success(graph: GraphState) -> None: """Test if measurement planes and angles are set properly.""" - graph.check_meas_basis() + graph._check_meas_basis() node_index1 = graph.add_physical_node() q_index = graph.mark_input(node_index1) meas_basis = PlannerMeasBasis(Plane.XY, 0.5 * np.pi) graph.set_meas_basis(node_index1, meas_basis) - graph.check_meas_basis() + graph._check_meas_basis() node_index2 = graph.add_physical_node() graph.add_physical_edge(node_index1, node_index2) graph.mark_output(node_index2, q_index) - graph.check_meas_basis() + graph._check_meas_basis() def test_bipartite_edges() -> None: From 30e3ff12f8241a84347bf0a1cb8992502404968d Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 18:43:42 +0900 Subject: [PATCH 38/67] add additional error case in mark_output --- graphix_zx/graphstate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 25757c48c..a24ffe492 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -458,6 +458,7 @@ def mark_output(self, node: int, q_index: int) -> None: 1. If the node does not exist. 2. If the node has a measurement basis. 3. If the invalid q_index specified. + 4. If the q_index already exists in output qubit indices. """ self._ensure_node_exists(node) if self.meas_bases.get(node) is not None: @@ -466,6 +467,9 @@ def mark_output(self, node: int, q_index: int) -> None: if q_index >= len(self.input_node_indices): msg = "The q_index does not exist in input qubit indices" 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 @typing_extensions.override From 79d3dfe0eb569653a0224a65c3866e768d2b1e19 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 18:44:56 +0900 Subject: [PATCH 39/67] update random graph generator --- graphix_zx/random_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphix_zx/random_objects.py b/graphix_zx/random_objects.py index 73a5f4d7f..10f6cc975 100644 --- a/graphix_zx/random_objects.py +++ b/graphix_zx/random_objects.py @@ -56,7 +56,7 @@ def get_random_flow_graph( # input nodes for _ in range(width): node_index = graph.add_physical_node() - q_index = graph.set_input(node_index) + q_index = graph.mark_input(node_index) graph.set_meas_basis(node_index, default_meas_basis()) q_indices.append(q_index) @@ -77,7 +77,7 @@ def get_random_flow_graph( # output nodes for qi in q_indices: node_index = graph.add_physical_node() - graph.set_output(node_index, qi) + graph.mark_output(node_index, qi) graph.add_physical_edge(node_index - width, node_index) flow[node_index - width] = {node_index} From 47973803f13f9ee976dd82552fdb03299b4fa97a Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:09:28 +0900 Subject: [PATCH 40/67] use MappingProxyType and frozenset --- graphix_zx/graphstate.py | 61 ++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index a24ffe492..283df1631 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -14,6 +14,7 @@ import operator from abc import ABC, abstractmethod from itertools import product +from types import MappingProxyType from typing import TYPE_CHECKING import typing_extensions @@ -32,67 +33,67 @@ class BaseGraphState(ABC): @property @abstractmethod - def input_node_indices(self) -> dict[int, int]: + def input_node_indices(self) -> MappingProxyType[int, int]: r"""Return set of input nodes. Returns ------- - `dict`\[`int`, `int`\] + `types.MappingProxyType`\[`int`, `int`\] qubit indices map of input nodes. """ @property @abstractmethod - def output_node_indices(self) -> dict[int, int]: + def output_node_indices(self) -> MappingProxyType[int, int]: r"""Return set of output nodes. Returns ------- - `dict`\[`int`, `int`\] + `types.MappingProxyType`\[`int`, `int`\] qubit indices map of output nodes. """ @property @abstractmethod - def physical_nodes(self) -> set[int]: + def physical_nodes(self) -> frozenset[int]: r"""Return set of physical nodes. Returns ------- - `set`\[`int`\] + `frozenset`\[`int`\] set of physical nodes. """ @property @abstractmethod - def physical_edges(self) -> set[tuple[int, int]]: + def physical_edges(self) -> frozenset[tuple[int, int]]: r"""Return set of physical edges. Returns ------- - `set`\[`tuple`\[`int`, `int`\]` + `frozenset`\[`tuple`\[`int`, `int`\]` set of physical edges. """ @property @abstractmethod - def meas_bases(self) -> dict[int, MeasBasis]: + def meas_bases(self) -> MappingProxyType[int, MeasBasis]: r"""Return measurement bases. Returns ------- - `dict`\[`int`, `MeasBasis`\] + `types.MappingProxyType`\[`int`, `MeasBasis`\] measurement bases of each physical node. """ @property @abstractmethod - def local_cliffords(self) -> dict[int, LocalClifford]: + def local_cliffords(self) -> MappingProxyType[int, LocalClifford]: r"""Return local clifford nodes. Returns ------- - `dict`\[`int`, `LocalClifford`\] + `types.MappingProxyType`\[`int`, `LocalClifford`\] local clifford nodes. """ @@ -172,7 +173,7 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: """ @abstractmethod - def neighbors(self, node: int) -> set[int]: + def neighbors(self, node: int) -> frozenset[int]: r"""Return the neighbors of the node. Parameters @@ -182,7 +183,7 @@ def neighbors(self, node: int) -> set[int]: Returns ------- - `set`\[`int`\] + `frozenset`\[`int`\] set of neighboring nodes """ @@ -211,7 +212,7 @@ def __init__(self) -> None: @property @typing_extensions.override - def input_node_indices(self) -> dict[int, int]: + def input_node_indices(self) -> MappingProxyType[int, int]: r"""Return map of input nodes. Returns @@ -219,11 +220,11 @@ def input_node_indices(self) -> dict[int, int]: `dict`\[`int`, `int`\] qubit indices map of input nodes. """ - return self.__input_node_indices + return MappingProxyType(self.__input_node_indices) @property @typing_extensions.override - def output_node_indices(self) -> dict[int, int]: + def output_node_indices(self) -> MappingProxyType[int, int]: r"""Return map of output nodes. Returns @@ -231,7 +232,7 @@ def output_node_indices(self) -> dict[int, int]: `dict`\[`int`, `int`\] qubit indices map of output nodes. """ - return self.__output_node_indices + return MappingProxyType(self.__output_node_indices) @property def num_physical_nodes(self) -> int: @@ -257,7 +258,7 @@ def num_physical_edges(self) -> int: @property @typing_extensions.override - def physical_nodes(self) -> set[int]: + def physical_nodes(self) -> frozenset[int]: r"""Return set of physical nodes. Returns @@ -265,16 +266,16 @@ def physical_nodes(self) -> set[int]: `set`\[`int`\] set of physical nodes. """ - return self.__physical_nodes + return frozenset(self.__physical_nodes) @property @typing_extensions.override - def physical_edges(self) -> set[tuple[int, int]]: + def physical_edges(self) -> frozenset[tuple[int, int]]: r"""Return set of physical edges. Returns ------- - `set`\[`tuple`\[`int`, `int`\] + `frozenset`\[`tuple`\[`int`, `int`\] set of physical edges. """ edges = set() @@ -282,11 +283,11 @@ def physical_edges(self) -> set[tuple[int, int]]: for node2 in self.__physical_edges[node1]: if node1 < node2: edges |= {(node1, node2)} - return edges + return frozenset(edges) @property @typing_extensions.override - def meas_bases(self) -> dict[int, MeasBasis]: + def meas_bases(self) -> MappingProxyType[int, MeasBasis]: r"""Return measurement bases. Returns @@ -294,11 +295,11 @@ def meas_bases(self) -> dict[int, MeasBasis]: `dict`\[`int`, `MeasBasis`\] measurement bases of each physical node. """ - return self.__meas_bases + return MappingProxyType(self.__meas_bases) @property @typing_extensions.override - def local_cliffords(self) -> dict[int, LocalClifford]: + def local_cliffords(self) -> MappingProxyType[int, LocalClifford]: r"""Return local clifford nodes. Returns @@ -306,7 +307,7 @@ def local_cliffords(self) -> dict[int, LocalClifford]: `dict`\[`int`, `LocalClifford`\] local clifford nodes. """ - return self.__local_cliffords + return MappingProxyType(self.__local_cliffords) def _check_meas_basis(self) -> None: """Check if the measurement basis is set for all physical nodes except output nodes. @@ -520,7 +521,7 @@ def pop_local_clifford(self, node: int) -> LocalClifford | None: return self.__local_cliffords.pop(node, None) @typing_extensions.override - def neighbors(self, node: int) -> set[int]: + def neighbors(self, node: int) -> frozenset[int]: r"""Return the neighbors of the node. Parameters @@ -530,11 +531,11 @@ def neighbors(self, node: int) -> set[int]: Returns ------- - `set`\[`int`\] + `frozenset`\[`int`\] set of neighboring nodes """ self._ensure_node_exists(node) - return self.__physical_edges[node] + return frozenset(self.__physical_edges[node]) def _parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: r"""Parse local Clifford operators applied on the input nodes. From 9f1e971fcbd5d6c815ddb64babd327afbd8207ab Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:10:35 +0900 Subject: [PATCH 41/67] rename set_meas_basis -> assign_meas_basis --- graphix_zx/graphstate.py | 22 +++++++++++----------- graphix_zx/random_objects.py | 4 ++-- tests/test_graphstate.py | 8 ++++---- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 283df1631..61b574f2c 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -149,8 +149,8 @@ def mark_output(self, node: int, q_index: int) -> None: """ @abstractmethod - def set_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: - """Set the measurement basis of the node. + def assign_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: + """Assign the measurement basis of the node. Parameters ---------- @@ -474,7 +474,7 @@ def mark_output(self, node: int, q_index: int) -> None: self.__output_node_indices[node] = q_index @typing_extensions.override - def set_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: + def assign_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: """Set the measurement basis of the node. Parameters @@ -503,7 +503,7 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: self.__local_cliffords[node] = lc else: new_meas_basis = update_lc_basis(lc.conjugate(), self.meas_bases[node]) - self.set_meas_basis(node, new_meas_basis) + self.assign_meas_basis(node, new_meas_basis) def pop_local_clifford(self, node: int) -> LocalClifford | None: """Pop local clifford of the node. @@ -561,9 +561,9 @@ def _parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: self.add_physical_edge(new_node_index1, new_node_index2) self.add_physical_edge(new_node_index2, input_node) - self.set_meas_basis(new_node_index0, PlannerMeasBasis(Plane.XY, lc.alpha)) - self.set_meas_basis(new_node_index1, PlannerMeasBasis(Plane.XY, lc.beta)) - self.set_meas_basis(new_node_index2, PlannerMeasBasis(Plane.XY, lc.gamma)) + 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] = (new_node_index0, new_node_index1, new_node_index2) @@ -607,7 +607,7 @@ def sequential_compose( # noqa: C901 node_index = composed_graph.add_physical_node() meas_basis = graph1.meas_bases.get(node, None) if meas_basis is not None: - composed_graph.set_meas_basis(node_index, meas_basis) + composed_graph.assign_meas_basis(node_index, meas_basis) lc = graph1.local_cliffords.get(node, None) if lc is not None: composed_graph.apply_local_clifford(node_index, lc) @@ -617,7 +617,7 @@ def sequential_compose( # noqa: C901 node_index = composed_graph.add_physical_node() meas_basis = graph2.meas_bases.get(node, None) if meas_basis is not None: - composed_graph.set_meas_basis(node_index, meas_basis) + composed_graph.assign_meas_basis(node_index, meas_basis) lc = graph2.local_cliffords.get(node, None) if lc is not None: composed_graph.apply_local_clifford(node_index, lc) @@ -662,7 +662,7 @@ def parallel_compose( # noqa: C901 node_index = composed_graph.add_physical_node() meas_basis = graph1.meas_bases.get(node, None) if meas_basis is not None: - composed_graph.set_meas_basis(node_index, meas_basis) + composed_graph.assign_meas_basis(node_index, meas_basis) lc = graph1.local_cliffords.get(node, None) if lc is not None: composed_graph.apply_local_clifford(node_index, lc) @@ -672,7 +672,7 @@ def parallel_compose( # noqa: C901 node_index = composed_graph.add_physical_node() meas_basis = graph2.meas_bases.get(node, None) if meas_basis is not None: - composed_graph.set_meas_basis(node_index, meas_basis) + composed_graph.assign_meas_basis(node_index, meas_basis) lc = graph2.local_cliffords.get(node, None) if lc is not None: composed_graph.apply_local_clifford(node_index, lc) diff --git a/graphix_zx/random_objects.py b/graphix_zx/random_objects.py index 10f6cc975..c046ee137 100644 --- a/graphix_zx/random_objects.py +++ b/graphix_zx/random_objects.py @@ -57,7 +57,7 @@ def get_random_flow_graph( for _ in range(width): node_index = graph.add_physical_node() q_index = graph.mark_input(node_index) - graph.set_meas_basis(node_index, default_meas_basis()) + graph.assign_meas_basis(node_index, default_meas_basis()) q_indices.append(q_index) # internal nodes @@ -65,7 +65,7 @@ def get_random_flow_graph( node_indices_layer = [] for _ in range(width): node_index = graph.add_physical_node() - graph.set_meas_basis(node_index, default_meas_basis()) + graph.assign_meas_basis(node_index, default_meas_basis()) graph.add_physical_edge(node_index - width, node_index) flow[node_index - width] = {node_index} node_indices_layer.append(node_index) diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index fcdc233be..892acd7c3 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -170,16 +170,16 @@ def test_mark_output_raises_1(graph: GraphState) -> None: def test_mark_output_raises_2(graph: GraphState) -> None: node_index = graph.add_physical_node() - graph.set_meas_basis(node_index, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) + graph.assign_meas_basis(node_index, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) with pytest.raises(ValueError, match=r"Cannot set output node with measurement basis."): graph.mark_output(node_index, 0) -def test_set_meas_basis(graph: GraphState) -> None: +def test_assign_meas_basis(graph: GraphState) -> None: """Test setting the measurement basis of a physical node.""" node_index = graph.add_physical_node() meas_basis = PlannerMeasBasis(Plane.XZ, 0.5 * np.pi) - graph.set_meas_basis(node_index, meas_basis) + graph.assign_meas_basis(node_index, meas_basis) assert graph.meas_bases[node_index].plane == Plane.XZ assert graph.meas_bases[node_index].angle == 0.5 * np.pi @@ -197,7 +197,7 @@ def test_check_meas_basis_success(graph: GraphState) -> None: node_index1 = graph.add_physical_node() q_index = graph.mark_input(node_index1) meas_basis = PlannerMeasBasis(Plane.XY, 0.5 * np.pi) - graph.set_meas_basis(node_index1, meas_basis) + graph.assign_meas_basis(node_index1, meas_basis) graph._check_meas_basis() node_index2 = graph.add_physical_node() From 34eded227c4d7221f99da5ae630eb9e520754b3a Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:19:07 +0900 Subject: [PATCH 42/67] rename funcs --- graphix_zx/graphstate.py | 44 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 61b574f2c..f5457a314 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -4,8 +4,8 @@ - `BaseGraphState`: Abstract base class for Graph State. - `GraphState`: Minimal implementation of Graph State. -- `sequential_compose`: Function to compose two graph states sequentially. -- `parallel_compose`: Function to compose two graph states in parallel. +- `compose_sequentially`: Function to compose two graph states sequentially. +- `compose_in_parallel`: Function to compose two graph states in parallel. - `bipartite_edges`: Function to create a complete bipartite graph between two sets of nodes. """ @@ -122,7 +122,7 @@ def add_physical_edge(self, node1: int, node2: int) -> None: """ @abstractmethod - def mark_input(self, node: int) -> int: + def register_input(self, node: int) -> int: """Mark the node as an input node. Parameters @@ -137,7 +137,7 @@ def mark_input(self, node: int) -> int: """ @abstractmethod - def mark_output(self, node: int, q_index: int) -> None: + def register_output(self, node: int, q_index: int) -> None: """Mark the node as an output node. Parameters @@ -198,7 +198,7 @@ class GraphState(BaseGraphState): __meas_bases: dict[int, MeasBasis] __local_cliffords: dict[int, LocalClifford] - __inner_index: int + __node_counter: int def __init__(self) -> None: self.__input_node_indices = {} @@ -208,7 +208,7 @@ def __init__(self) -> None: self.__meas_bases = {} self.__local_cliffords = {} - self.__inner_index = 0 + self.__node_counter = 0 @property @typing_extensions.override @@ -247,7 +247,7 @@ def num_physical_nodes(self) -> int: @property def num_physical_edges(self) -> int: - """Return the number of physical edges. + """Return the number of undirected physical edges. Returns ------- @@ -345,10 +345,10 @@ def add_physical_node( `int` The node index internally generated. """ - node = self.__inner_index + node = self.__node_counter self.__physical_nodes |= {node} self.__physical_edges[node] = set() - self.__inner_index += 1 + self.__node_counter += 1 return node @@ -424,7 +424,7 @@ def remove_physical_edge(self, node1: int, node2: int) -> None: self.__physical_edges[node2] -= {node1} @typing_extensions.override - def mark_input(self, node: int) -> int: + def register_input(self, node: int) -> int: """Mark the node as an input node. Parameters @@ -443,7 +443,7 @@ def mark_input(self, node: int) -> int: return q_index @typing_extensions.override - def mark_output(self, node: int, q_index: int) -> None: + def register_output(self, node: int, q_index: int) -> None: """Mark the node as an output node. Parameters @@ -537,7 +537,7 @@ def neighbors(self, node: int) -> frozenset[int]: self._ensure_node_exists(node) return frozenset(self.__physical_edges[node]) - def _parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: + def _expand_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: r"""Parse local Clifford operators applied on the input nodes. Returns @@ -569,12 +569,12 @@ def _parse_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: self.__input_node_indices = {} for new_input_index in new_input_indices: - self.mark_input(new_input_index) + self.register_input(new_input_index) return node_index_addition_map -def sequential_compose( # noqa: C901 +def compose_sequentially( # noqa: C901 graph1: BaseGraphState, graph2: BaseGraphState ) -> tuple[BaseGraphState, dict[int, int], dict[int, int]]: r"""Compose two graph states sequentially. @@ -624,10 +624,10 @@ def sequential_compose( # noqa: C901 node_map2[node] = node_index for input_node, _ in sorted(graph1.input_node_indices.items(), key=operator.itemgetter(1)): - composed_graph.mark_input(node_map1[input_node]) + composed_graph.register_input(node_map1[input_node]) for output_node, q_index in graph2.output_node_indices.items(): - composed_graph.mark_output(node_map2[output_node], q_index) + composed_graph.register_output(node_map2[output_node], q_index) for u, v in graph1.physical_edges: composed_graph.add_physical_edge(node_map1[u], node_map1[v]) @@ -637,10 +637,10 @@ def sequential_compose( # noqa: C901 return composed_graph, node_map1, node_map2 -def parallel_compose( # noqa: C901 +def compose_in_parallel( # noqa: C901 graph1: BaseGraphState, graph2: BaseGraphState ) -> tuple[BaseGraphState, dict[int, int], dict[int, int]]: - r"""Compose two graph states parallelly. + r"""Compose two graph states in parallel. Parameters ---------- @@ -681,18 +681,18 @@ def parallel_compose( # noqa: C901 q_index_map1 = {} q_index_map2 = {} for input_node, old_q_index in sorted(graph1.input_node_indices.items(), key=operator.itemgetter(1)): - new_q_index = composed_graph.mark_input(node_map1[input_node]) + new_q_index = composed_graph.register_input(node_map1[input_node]) q_index_map1[old_q_index] = new_q_index for input_node, old_q_index in sorted(graph2.input_node_indices.items(), key=operator.itemgetter(1)): - new_q_index = composed_graph.mark_input(node_map2[input_node]) + new_q_index = composed_graph.register_input(node_map2[input_node]) q_index_map2[old_q_index] = new_q_index for output_node, q_index in graph1.output_node_indices.items(): - composed_graph.mark_output(node_map1[output_node], q_index_map1[q_index]) + composed_graph.register_output(node_map1[output_node], q_index_map1[q_index]) for output_node, q_index in graph2.output_node_indices.items(): - composed_graph.mark_output(node_map2[output_node], q_index_map2[q_index]) + composed_graph.register_output(node_map2[output_node], q_index_map2[q_index]) for u, v in graph1.physical_edges: composed_graph.add_physical_edge(node_map1[u], node_map1[v]) From 475448b5081fd01a770ea7a8000219239694bdd4 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:19:21 +0900 Subject: [PATCH 43/67] update test and docs --- docs/source/graphstate.rst | 4 ++-- graphix_zx/random_objects.py | 4 ++-- tests/test_graphstate.py | 22 +++++++++++----------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/source/graphstate.rst b/docs/source/graphstate.rst index 6352054e8..24bf72e6c 100644 --- a/docs/source/graphstate.rst +++ b/docs/source/graphstate.rst @@ -21,6 +21,6 @@ Graph State Classes Functions --------- -.. autofunction:: graphix_zx.graphstate.sequential_compose -.. autofunction:: graphix_zx.graphstate.parallel_compose +.. autofunction:: graphix_zx.graphstate.compose_sequentially +.. autofunction:: graphix_zx.graphstate.compose_in_parallel .. autofunction:: graphix_zx.graphstate.bipartite_edges diff --git a/graphix_zx/random_objects.py b/graphix_zx/random_objects.py index c046ee137..f0f2b9e2d 100644 --- a/graphix_zx/random_objects.py +++ b/graphix_zx/random_objects.py @@ -56,7 +56,7 @@ def get_random_flow_graph( # input nodes for _ in range(width): node_index = graph.add_physical_node() - q_index = graph.mark_input(node_index) + q_index = graph.register_input(node_index) graph.assign_meas_basis(node_index, default_meas_basis()) q_indices.append(q_index) @@ -77,7 +77,7 @@ def get_random_flow_graph( # output nodes for qi in q_indices: node_index = graph.add_physical_node() - graph.mark_output(node_index, qi) + graph.register_output(node_index, qi) graph.add_physical_edge(node_index - width, node_index) flow[node_index - width] = {node_index} diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index 892acd7c3..ca510a4f8 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -30,8 +30,8 @@ def test_add_physical_node(graph: GraphState) -> None: def test_add_physical_node_input_output(graph: GraphState) -> None: """Test adding a physical node as input and output.""" node_index = graph.add_physical_node() - q_index = graph.mark_input(node_index) - graph.mark_output(node_index, q_index) + q_index = graph.register_input(node_index) + graph.register_output(node_index, q_index) assert node_index in graph.input_node_indices assert node_index in graph.output_node_indices assert graph.input_node_indices[node_index] == q_index @@ -96,7 +96,7 @@ def test_remove_physical_node_with_nonexistent_node(graph: GraphState) -> None: def test_remove_physical_node_with_input_removal(graph: GraphState) -> None: """Test removing an input node from the graph""" node_index = graph.add_physical_node() - graph.mark_input(node_index) + graph.register_input(node_index) with pytest.raises(ValueError, match="The input node cannot be removed"): graph.remove_physical_node(node_index) @@ -128,8 +128,8 @@ def test_remove_physical_node_from_3_nodes_graph(graph: GraphState) -> None: node_index3 = graph.add_physical_node() graph.add_physical_edge(node_index1, node_index2) graph.add_physical_edge(node_index2, node_index3) - q_index = graph.mark_input(node_index1) - graph.mark_output(node_index3, q_index) + q_index = graph.register_input(node_index1) + graph.register_output(node_index3, q_index) graph.remove_physical_node(node_index2) assert graph.physical_nodes == {node_index1, node_index3} assert graph.num_physical_nodes == 2 @@ -163,16 +163,16 @@ def test_remove_physical_edge(graph: GraphState) -> None: assert graph.num_physical_edges == 0 -def test_mark_output_raises_1(graph: GraphState) -> None: +def test_register_output_raises_1(graph: GraphState) -> None: with pytest.raises(ValueError, match="Node does not exist node=1"): - graph.mark_output(1, 0) + graph.register_output(1, 0) -def test_mark_output_raises_2(graph: GraphState) -> None: +def test_register_output_raises_2(graph: GraphState) -> None: node_index = graph.add_physical_node() graph.assign_meas_basis(node_index, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) with pytest.raises(ValueError, match=r"Cannot set output node with measurement basis."): - graph.mark_output(node_index, 0) + graph.register_output(node_index, 0) def test_assign_meas_basis(graph: GraphState) -> None: @@ -195,14 +195,14 @@ def test_check_meas_basis_success(graph: GraphState) -> None: """Test if measurement planes and angles are set properly.""" graph._check_meas_basis() node_index1 = graph.add_physical_node() - q_index = graph.mark_input(node_index1) + q_index = graph.register_input(node_index1) meas_basis = PlannerMeasBasis(Plane.XY, 0.5 * np.pi) graph.assign_meas_basis(node_index1, meas_basis) graph._check_meas_basis() node_index2 = graph.add_physical_node() graph.add_physical_edge(node_index1, node_index2) - graph.mark_output(node_index2, q_index) + graph.register_output(node_index2, q_index) graph._check_meas_basis() From b784929a70afeec9cf39c85e2d13f1e3f1e8805a Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:20:38 +0900 Subject: [PATCH 44/67] prohibit self-loop --- graphix_zx/graphstate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index f5457a314..5060b8310 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -366,13 +366,18 @@ def add_physical_edge(self, node1: int, node2: int) -> None: Raises ------ ValueError - If the edge already exists. + 1. If the node does not exist. + 2. If the edge already exists. + 3. If the edge is a self-loop. """ self._ensure_node_exists(node1) self._ensure_node_exists(node2) if node1 in self.__physical_edges[node2] or node2 in self.__physical_edges[node1]: msg = f"Edge already exists {node1=}, {node2=}" raise ValueError(msg) + if node1 == node2: + msg = "Self-loops are not allowed" + raise ValueError(msg) self.__physical_edges[node1] |= {node2} self.__physical_edges[node2] |= {node1} From 7089a02a40f9a14954690ee8d89ba73c7b65b4d0 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:21:41 +0900 Subject: [PATCH 45/67] check meas basis is properly set --- graphix_zx/graphstate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 5060b8310..222553ca2 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -507,6 +507,7 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: if node in self.input_node_indices or node in self.output_node_indices: self.__local_cliffords[node] = lc else: + self._check_meas_basis() new_meas_basis = update_lc_basis(lc.conjugate(), self.meas_bases[node]) self.assign_meas_basis(node, new_meas_basis) From 52af48c8d9869e7196d4fb7d756b3b527d7694c7 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:38:32 +0900 Subject: [PATCH 46/67] remove lc from composition --- graphix_zx/graphstate.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 222553ca2..cf82b32e0 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -609,14 +609,11 @@ def compose_sequentially( # noqa: C901 node_map2 = {} composed_graph = GraphState() - for node in graph1.physical_nodes: + for node in graph1.physical_nodes - graph1.output_node_indices.keys(): node_index = composed_graph.add_physical_node() meas_basis = graph1.meas_bases.get(node, None) if meas_basis is not None: composed_graph.assign_meas_basis(node_index, meas_basis) - lc = graph1.local_cliffords.get(node, None) - if lc is not None: - composed_graph.apply_local_clifford(node_index, lc) node_map1[node] = node_index for node in graph2.physical_nodes: @@ -624,9 +621,6 @@ def compose_sequentially( # noqa: C901 meas_basis = graph2.meas_bases.get(node, None) if meas_basis is not None: composed_graph.assign_meas_basis(node_index, meas_basis) - lc = graph2.local_cliffords.get(node, None) - if lc is not None: - composed_graph.apply_local_clifford(node_index, lc) node_map2[node] = node_index for input_node, _ in sorted(graph1.input_node_indices.items(), key=operator.itemgetter(1)): @@ -635,6 +629,13 @@ def compose_sequentially( # noqa: C901 for output_node, q_index in graph2.output_node_indices.items(): composed_graph.register_output(node_map2[output_node], q_index) + # overlapping node process + q_index2output_node_index1 = { + q_index: output_node_index1 for output_node_index1, q_index in graph1.output_node_indices.items() + } + for input_node_index2, q_index in graph2.input_node_indices.items(): + node_map1[q_index2output_node_index1[q_index]] = node_map2[input_node_index2] + for u, v in graph1.physical_edges: composed_graph.add_physical_edge(node_map1[u], node_map1[v]) for u, v in graph2.physical_edges: @@ -669,9 +670,6 @@ def compose_in_parallel( # noqa: C901 meas_basis = graph1.meas_bases.get(node, None) if meas_basis is not None: composed_graph.assign_meas_basis(node_index, meas_basis) - lc = graph1.local_cliffords.get(node, None) - if lc is not None: - composed_graph.apply_local_clifford(node_index, lc) node_map1[node] = node_index for node in graph2.physical_nodes: @@ -679,9 +677,6 @@ def compose_in_parallel( # noqa: C901 meas_basis = graph2.meas_bases.get(node, None) if meas_basis is not None: composed_graph.assign_meas_basis(node_index, meas_basis) - lc = graph2.local_cliffords.get(node, None) - if lc is not None: - composed_graph.apply_local_clifford(node_index, lc) node_map2[node] = node_index q_index_map1 = {} From 58338d217dfc06b3c3922f21fbe2a599c500db9b Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:40:34 +0900 Subject: [PATCH 47/67] revert to use TypeVar --- graphix_zx/matrix.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/graphix_zx/matrix.py b/graphix_zx/matrix.py index 91f0b36b4..d4b20b542 100644 --- a/graphix_zx/matrix.py +++ b/graphix_zx/matrix.py @@ -7,20 +7,22 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, TypeVar import numpy as np if TYPE_CHECKING: from numpy.typing import NDArray +T = TypeVar("T", bound=np.number[Any]) -def is_unitary(mat: NDArray[np.number]) -> bool: + +def is_unitary(mat: NDArray[T]) -> bool: r"""Check if a matrix is unitary. Parameters ---------- - mat : `numpy.typing.NDArray`\[`numpy.number`\] + mat : `numpy.typing.NDArray`\[T\] matrix to check Returns From 23cb79bfff95d6e4ff5b3330c82eb362e3cec302 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:41:06 +0900 Subject: [PATCH 48/67] add comment --- graphix_zx/matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix_zx/matrix.py b/graphix_zx/matrix.py index d4b20b542..8ff6f1416 100644 --- a/graphix_zx/matrix.py +++ b/graphix_zx/matrix.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: from numpy.typing import NDArray -T = TypeVar("T", bound=np.number[Any]) +T = TypeVar("T", bound=np.number[Any]) # can be removed >= 3.10 def is_unitary(mat: NDArray[T]) -> bool: From 55ae692cd127743d6f744c271d543fd2b947e20a Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:52:22 +0900 Subject: [PATCH 49/67] add canonical form checker --- graphix_zx/graphstate.py | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index cf82b32e0..4d8c92308 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -580,6 +580,47 @@ def _expand_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: return node_index_addition_map +def _check_canonical_form(graph: BaseGraphState) -> None: + """Check if the graph is in canonical form. + + Definition of canonical form: + 1. No local Cliffords + 2. Input and output must have single neighbor + 3. Internal nodes must not have more than 2 input/output neighbors + + Parameters + ---------- + graph : BaseGraphState + The graph state to check for canonical form. + + Raises + ------ + ValueError + If the graph state is not in canonical form due to local Cliffords. + ValueError + If any input/output node does not have a single neighbor. + ValueError + If any internal node has more than 2 input/output neighbors. + """ + # 1. no local Cliffords + if graph.local_cliffords: + msg = "Graph state is not in canonical form. Local Cliffords are not allowed." + raise ValueError(msg) + + node_connecting_input_or_output: set[int] = set() + for node in graph.input_node_indices | graph.output_node_indices: + neighbors = graph.neighbors(node) + # 2. input and output must have single neighbor + if len(neighbors) != 1: + msg = f"Input/Output node {node} must have a single neighbor." + raise ValueError(msg) + # 3. internal nodes must not have more than 2 input/output neighbors + if neighbors & node_connecting_input_or_output: + msg = f"Input/Output neighboring node {node} cannot connect to other input/output nodes." + raise ValueError(msg) + node_connecting_input_or_output |= neighbors + + def compose_sequentially( # noqa: C901 graph1: BaseGraphState, graph2: BaseGraphState ) -> tuple[BaseGraphState, dict[int, int], dict[int, int]]: From b91c023b7a3c5a31c6627aa7a8a41ce96d498e4d Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:53:47 +0900 Subject: [PATCH 50/67] canonical form check before composition --- graphix_zx/graphstate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 4d8c92308..65577563a 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -643,6 +643,8 @@ def compose_sequentially( # noqa: C901 ValueError If the logical qubit indices of output nodes in graph1 do not match input nodes in graph2. """ + _check_canonical_form(graph1) + _check_canonical_form(graph2) if set(graph1.output_node_indices.values()) != set(graph2.input_node_indices.values()): msg = "Logical qubit indices of output nodes in graph1 must match input nodes in graph2." raise ValueError(msg) @@ -702,6 +704,8 @@ def compose_in_parallel( # noqa: C901 `tuple`\[`BaseGraphState`, `dict`\[`int`, `int`\], `dict`\[`int`, `int`\]\] composed graph state, node map for graph1, node map for graph2 """ + _check_canonical_form(graph1) + _check_canonical_form(graph2) node_map1 = {} node_map2 = {} composed_graph = GraphState() From 703867fc7d4307f1a27656681023334e79c33da5 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:54:25 +0900 Subject: [PATCH 51/67] delete unnecessary branch --- graphix_zx/graphstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 65577563a..595420c94 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -771,4 +771,4 @@ def bipartite_edges(node_set1: AbstractSet[int], node_set2: AbstractSet[int]) -> if not node_set1.isdisjoint(node_set2): msg = "The two sets of nodes must be disjoint." raise ValueError(msg) - return {(min(a, b), max(a, b)) for a, b in product(node_set1, node_set2) if a != b} + return {(min(a, b), max(a, b)) for a, b in product(node_set1, node_set2)} From d387750b0fa10777dd06e65ce42275ba5a22fbe1 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:55:58 +0900 Subject: [PATCH 52/67] fix docstring --- graphix_zx/graphstate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 595420c94..a348760e1 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -34,7 +34,7 @@ class BaseGraphState(ABC): @property @abstractmethod def input_node_indices(self) -> MappingProxyType[int, int]: - r"""Return set of input nodes. + r"""Return map of input nodes to logical qubit indices. Returns ------- @@ -45,7 +45,7 @@ def input_node_indices(self) -> MappingProxyType[int, int]: @property @abstractmethod def output_node_indices(self) -> MappingProxyType[int, int]: - r"""Return set of output nodes. + r"""Return map of output nodes to logical qubit indices. Returns ------- @@ -213,7 +213,7 @@ def __init__(self) -> None: @property @typing_extensions.override def input_node_indices(self) -> MappingProxyType[int, int]: - r"""Return map of input nodes. + r"""Return map of input nodes to logical qubit indices. Returns ------- @@ -225,7 +225,7 @@ def input_node_indices(self) -> MappingProxyType[int, int]: @property @typing_extensions.override def output_node_indices(self) -> MappingProxyType[int, int]: - r"""Return map of output nodes. + r"""Return map of output nodes to logical qubit indices. Returns ------- From 41399afc30466e8da89f4c51097de7186fc50b8a Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 19:58:55 +0900 Subject: [PATCH 53/67] make pop_local_clifford private --- graphix_zx/graphstate.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index a348760e1..e0c3b7d3a 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -511,37 +511,37 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: new_meas_basis = update_lc_basis(lc.conjugate(), self.meas_bases[node]) self.assign_meas_basis(node, new_meas_basis) - def pop_local_clifford(self, node: int) -> LocalClifford | None: - """Pop local clifford of the node. + @typing_extensions.override + def neighbors(self, node: int) -> frozenset[int]: + r"""Return the neighbors of the node. Parameters ---------- node : `int` - node index to remove local clifford. + node index Returns ------- - `LocalClifford` | `None` - removed local clifford + `frozenset`\[`int`\] + set of neighboring nodes """ - return self.__local_cliffords.pop(node, None) + self._ensure_node_exists(node) + return frozenset(self.__physical_edges[node]) - @typing_extensions.override - def neighbors(self, node: int) -> frozenset[int]: - r"""Return the neighbors of the node. + def _pop_local_clifford(self, node: int) -> LocalClifford | None: + """Pop local clifford of the node. Parameters ---------- node : `int` - node index + node index to remove local clifford. Returns ------- - `frozenset`\[`int`\] - set of neighboring nodes + `LocalClifford` | `None` + removed local clifford """ - self._ensure_node_exists(node) - return frozenset(self.__physical_edges[node]) + return self.__local_cliffords.pop(node, None) def _expand_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: r"""Parse local Clifford operators applied on the input nodes. @@ -554,7 +554,7 @@ def _expand_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: node_index_addition_map = {} new_input_indices = [] for input_node, _ in sorted(self.input_node_indices.items(), key=operator.itemgetter(1)): - lc = self.pop_local_clifford(input_node) + lc = self._pop_local_clifford(input_node) if lc is None: continue From 31e383271bd6c8d82057ca67632d9308e5cf37e6 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 20:02:56 +0900 Subject: [PATCH 54/67] add output lc expansion method --- graphix_zx/graphstate.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index e0c3b7d3a..0ae835676 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -544,7 +544,7 @@ def _pop_local_clifford(self, node: int) -> LocalClifford | None: return self.__local_cliffords.pop(node, None) def _expand_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: - r"""Parse local Clifford operators applied on the input nodes. + r"""Expand local Clifford operators applied on the input nodes. Returns ------- @@ -579,6 +579,42 @@ def _expand_input_local_cliffords(self) -> dict[int, tuple[int, int, int]]: return node_index_addition_map + def _expand_output_local_cliffords(self) -> dict[int, tuple[int, int, int]]: + r"""Expand local Clifford operators applied on the output nodes. + + Returns + ------- + `dict`\[`int`, `tuple`\[`int`, `int`, `int`\]\] + A dictionary mapping output node indices to the new node indices created. + """ + node_index_addition_map = {} + new_output_indices = [] + for output_node, _ in sorted(self.output_node_indices.items(), key=operator.itemgetter(1)): + lc = self._pop_local_clifford(output_node) + if lc is None: + continue + + new_node_index0 = self.add_physical_node() + new_node_index1 = self.add_physical_node() + new_node_index2 = self.add_physical_node() + new_output_indices.append(new_node_index2) + + 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.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)) + + node_index_addition_map[output_node] = (new_node_index0, new_node_index1, new_node_index2) + + self.__output_node_indices = {} + for new_output_index in new_output_indices: + self.register_output(new_output_index, len(self.__output_node_indices)) + + return node_index_addition_map + def _check_canonical_form(graph: BaseGraphState) -> None: """Check if the graph is in canonical form. From c2e967e32702a60c2320818222bba5d9c9d99f7d Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 20:03:33 +0900 Subject: [PATCH 55/67] update docstring --- graphix_zx/graphstate.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 0ae835676..069316efd 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -632,11 +632,9 @@ def _check_canonical_form(graph: BaseGraphState) -> None: Raises ------ ValueError - If the graph state is not in canonical form due to local Cliffords. - ValueError - If any input/output node does not have a single neighbor. - ValueError - If any internal node has more than 2 input/output neighbors. + 1. If the graph state is not in canonical form due to local Cliffords. + 2. If any input/output node does not have a single neighbor. + 3. If any internal node has more than 2 input/output neighbors. """ # 1. no local Cliffords if graph.local_cliffords: From afad91f1c26ba3d1f89e9d0419b664fedf9f4a28 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Sun, 20 Apr 2025 20:11:15 +0900 Subject: [PATCH 56/67] add lc parser for composition --- graphix_zx/graphstate.py | 75 +++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 44 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 069316efd..48a4c7141 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -528,6 +528,18 @@ def neighbors(self, node: int) -> frozenset[int]: self._ensure_node_exists(node) return frozenset(self.__physical_edges[node]) + def expand_local_cliffords(self) -> tuple[dict[int, tuple[int, int, int]], dict[int, tuple[int, int, int]]]: + r"""Expand local Clifford operators applied on the input and output nodes. + + Returns + ------- + `tuple`\[`dict`\[`int`, `tuple`\[`int`, `int`, `int`\]\], `dict`\[`int`, `tuple`\[`int`, `int`, `int`\]\]\] + 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 input_node_map, output_node_map + def _pop_local_clifford(self, node: int) -> LocalClifford | None: """Pop local clifford of the node. @@ -616,45 +628,6 @@ def _expand_output_local_cliffords(self) -> dict[int, tuple[int, int, int]]: return node_index_addition_map -def _check_canonical_form(graph: BaseGraphState) -> None: - """Check if the graph is in canonical form. - - Definition of canonical form: - 1. No local Cliffords - 2. Input and output must have single neighbor - 3. Internal nodes must not have more than 2 input/output neighbors - - Parameters - ---------- - graph : BaseGraphState - The graph state to check for canonical form. - - Raises - ------ - ValueError - 1. If the graph state is not in canonical form due to local Cliffords. - 2. If any input/output node does not have a single neighbor. - 3. If any internal node has more than 2 input/output neighbors. - """ - # 1. no local Cliffords - if graph.local_cliffords: - msg = "Graph state is not in canonical form. Local Cliffords are not allowed." - raise ValueError(msg) - - node_connecting_input_or_output: set[int] = set() - for node in graph.input_node_indices | graph.output_node_indices: - neighbors = graph.neighbors(node) - # 2. input and output must have single neighbor - if len(neighbors) != 1: - msg = f"Input/Output node {node} must have a single neighbor." - raise ValueError(msg) - # 3. internal nodes must not have more than 2 input/output neighbors - if neighbors & node_connecting_input_or_output: - msg = f"Input/Output neighboring node {node} cannot connect to other input/output nodes." - raise ValueError(msg) - node_connecting_input_or_output |= neighbors - - def compose_sequentially( # noqa: C901 graph1: BaseGraphState, graph2: BaseGraphState ) -> tuple[BaseGraphState, dict[int, int], dict[int, int]]: @@ -675,10 +648,15 @@ def compose_sequentially( # noqa: C901 Raises ------ ValueError - If the logical qubit indices of output nodes in graph1 do not match input nodes in graph2. + 1. If graph1 or graph2 has local Cliffords. + 2. If the logical qubit indices of output nodes in graph1 do not match input nodes in graph2. """ - _check_canonical_form(graph1) - _check_canonical_form(graph2) + if graph1.local_cliffords: + msg = "Graph1 is not in canonical form. Local Cliffords are not allowed." + raise ValueError(msg) + if graph2.local_cliffords: + msg = "Graph2 is not in canonical form. Local Cliffords are not allowed." + raise ValueError(msg) if set(graph1.output_node_indices.values()) != set(graph2.input_node_indices.values()): msg = "Logical qubit indices of output nodes in graph1 must match input nodes in graph2." raise ValueError(msg) @@ -737,9 +715,18 @@ def compose_in_parallel( # noqa: C901 ------- `tuple`\[`BaseGraphState`, `dict`\[`int`, `int`\], `dict`\[`int`, `int`\]\] composed graph state, node map for graph1, node map for graph2 + + Raises + ------ + ValueError + 1. If graph1 or graph2 has local Cliffords. """ - _check_canonical_form(graph1) - _check_canonical_form(graph2) + if graph1.local_cliffords: + msg = "Graph1 is not in canonical form. Local Cliffords are not allowed." + raise ValueError(msg) + if graph2.local_cliffords: + msg = "Graph2 is not in canonical form. Local Cliffords are not allowed." + raise ValueError(msg) node_map1 = {} node_map2 = {} composed_graph = GraphState() From ec0bbffaa3c09e514e8d2dbc500ecc161fed4ee2 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Mon, 21 Apr 2025 21:18:54 +0900 Subject: [PATCH 57/67] remove local clifford api from base class --- graphix_zx/graphstate.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 48a4c7141..b255c7cee 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -86,17 +86,6 @@ def meas_bases(self) -> MappingProxyType[int, MeasBasis]: measurement bases of each physical node. """ - @property - @abstractmethod - def local_cliffords(self) -> MappingProxyType[int, LocalClifford]: - r"""Return local clifford nodes. - - Returns - ------- - `types.MappingProxyType`\[`int`, `LocalClifford`\] - local clifford nodes. - """ - @abstractmethod def add_physical_node( self, @@ -160,18 +149,6 @@ def assign_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: measurement basis """ - @abstractmethod - 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 - """ - @abstractmethod def neighbors(self, node: int) -> frozenset[int]: r"""Return the neighbors of the node. @@ -298,7 +275,6 @@ def meas_bases(self) -> MappingProxyType[int, MeasBasis]: return MappingProxyType(self.__meas_bases) @property - @typing_extensions.override def local_cliffords(self) -> MappingProxyType[int, LocalClifford]: r"""Return local clifford nodes. From 4406a71eead53e50fd0576be424358be72465931 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Mon, 21 Apr 2025 21:24:50 +0900 Subject: [PATCH 58/67] use update_lc_lc in apply_local_clifford --- graphix_zx/graphstate.py | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index b255c7cee..b2b49069f 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -20,7 +20,7 @@ import typing_extensions from graphix_zx.common import MeasBasis, Plane, PlannerMeasBasis -from graphix_zx.euler import update_lc_basis +from graphix_zx.euler import update_lc_basis, update_lc_lc if TYPE_CHECKING: from collections.abc import Set as AbstractSet @@ -468,7 +468,6 @@ def assign_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: self._ensure_node_exists(node) self.__meas_bases[node] = meas_basis - @typing_extensions.override def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: """Apply a local clifford to the node. @@ -481,7 +480,12 @@ def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: """ self._ensure_node_exists(node) if node in self.input_node_indices or node in self.output_node_indices: - self.__local_cliffords[node] = lc + 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.conjugate(), self.meas_bases[node]) @@ -624,15 +628,8 @@ def compose_sequentially( # noqa: C901 Raises ------ ValueError - 1. If graph1 or graph2 has local Cliffords. - 2. If the logical qubit indices of output nodes in graph1 do not match input nodes in graph2. + If the logical qubit indices of output nodes in graph1 do not match input nodes in graph2. """ - if graph1.local_cliffords: - msg = "Graph1 is not in canonical form. Local Cliffords are not allowed." - raise ValueError(msg) - if graph2.local_cliffords: - msg = "Graph2 is not in canonical form. Local Cliffords are not allowed." - raise ValueError(msg) if set(graph1.output_node_indices.values()) != set(graph2.input_node_indices.values()): msg = "Logical qubit indices of output nodes in graph1 must match input nodes in graph2." raise ValueError(msg) @@ -691,18 +688,7 @@ def compose_in_parallel( # noqa: C901 ------- `tuple`\[`BaseGraphState`, `dict`\[`int`, `int`\], `dict`\[`int`, `int`\]\] composed graph state, node map for graph1, node map for graph2 - - Raises - ------ - ValueError - 1. If graph1 or graph2 has local Cliffords. """ - if graph1.local_cliffords: - msg = "Graph1 is not in canonical form. Local Cliffords are not allowed." - raise ValueError(msg) - if graph2.local_cliffords: - msg = "Graph2 is not in canonical form. Local Cliffords are not allowed." - raise ValueError(msg) node_map1 = {} node_map2 = {} composed_graph = GraphState() From fc544f0874b17443b97aa5e0c6be875aa6ecf769 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Mon, 21 Apr 2025 21:33:10 +0900 Subject: [PATCH 59/67] implement canonical form checker --- graphix_zx/graphstate.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index b2b49069f..b23862a3d 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -293,7 +293,7 @@ def _check_meas_basis(self) -> None: ValueError If the measurement basis is not set for a node or the measurement plane is invalid. """ - for v in self.physical_nodes - set(self.output_node_indices.keys()): + for v in self.physical_nodes - set(self.output_node_indices): if self.meas_bases.get(v) is None: msg = f"Measurement basis not set for node {v}" raise ValueError(msg) @@ -508,6 +508,25 @@ def neighbors(self, node: int) -> frozenset[int]: self._ensure_node_exists(node) return frozenset(self.__physical_edges[node]) + def is_canonical_form(self) -> bool: + 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. + + Returns + ------- + `bool` + `True` if the graph state is in canonical form, `False` otherwise. + """ + if self.__local_cliffords: + return False + for node in self.physical_nodes - set(self.output_node_indices): + if self.meas_bases.get(node) is None: + return False + return True + def expand_local_cliffords(self) -> tuple[dict[int, tuple[int, int, int]], dict[int, tuple[int, int, int]]]: r"""Expand local Clifford operators applied on the input and output nodes. From 256b1dfbb48a91355c5f18c9e817b9d286b96a4d Mon Sep 17 00:00:00 2001 From: masa10-f Date: Mon, 21 Apr 2025 21:42:29 +0900 Subject: [PATCH 60/67] check canonical form before composition --- graphix_zx/graphstate.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index b23862a3d..2ae8dc55f 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -164,6 +164,16 @@ def neighbors(self, node: int) -> frozenset[int]: set of neighboring nodes """ + @abstractmethod + def is_canonical_form(self) -> bool: + r"""Check if the graph state is in canonical form. + + Returns + ------- + `bool` + `True` if the graph state is in canonical form, `False` otherwise. + """ + class GraphState(BaseGraphState): """Minimal implementation of GraphState.""" @@ -508,6 +518,7 @@ def neighbors(self, node: int) -> frozenset[int]: self._ensure_node_exists(node) return frozenset(self.__physical_edges[node]) + @typing_extensions.override def is_canonical_form(self) -> bool: r"""Check if the graph state is in canonical form. @@ -647,8 +658,15 @@ def compose_sequentially( # noqa: C901 Raises ------ ValueError - If the logical qubit indices of output nodes in graph1 do not match input nodes in graph2. + 1. If graph1 or graph2 is not in canonical form. + 2. If the logical qubit indices of output nodes in graph1 do not match input nodes in graph2. """ + if not graph1.is_canonical_form(): + msg = "graph1 must be in canonical form." + raise ValueError(msg) + if not graph2.is_canonical_form(): + msg = "graph2 must be in canonical form." + raise ValueError(msg) if set(graph1.output_node_indices.values()) != set(graph2.input_node_indices.values()): msg = "Logical qubit indices of output nodes in graph1 must match input nodes in graph2." raise ValueError(msg) @@ -707,7 +725,18 @@ def compose_in_parallel( # noqa: C901 ------- `tuple`\[`BaseGraphState`, `dict`\[`int`, `int`\], `dict`\[`int`, `int`\]\] composed graph state, node map for graph1, node map for graph2 + + Raises + ------ + ValueError + If graph1 or graph2 is not in canonical form. """ + if not graph1.is_canonical_form(): + msg = "graph1 must be in canonical form." + raise ValueError(msg) + if not graph2.is_canonical_form(): + msg = "graph2 must be in canonical form." + raise ValueError(msg) node_map1 = {} node_map2 = {} composed_graph = GraphState() From 8a982e1887482b658466d091f712b9ed8c47d3a8 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Mon, 21 Apr 2025 21:50:19 +0900 Subject: [PATCH 61/67] fix docstring --- graphix_zx/graphstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 2ae8dc55f..ba625dc00 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -279,7 +279,7 @@ def meas_bases(self) -> MappingProxyType[int, MeasBasis]: Returns ------- - `dict`\[`int`, `MeasBasis`\] + `types.MappingProxyType`\[`int`, `MeasBasis`\] measurement bases of each physical node. """ return MappingProxyType(self.__meas_bases) From ed24a667b3b6363c19ce187268d797b10f2e5523 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Mon, 21 Apr 2025 21:52:52 +0900 Subject: [PATCH 62/67] remove redundunt properties --- graphix_zx/graphstate.py | 22 ---------------------- tests/test_graphstate.py | 16 ++++++++-------- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index ba625dc00..a15421f13 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -221,28 +221,6 @@ def output_node_indices(self) -> MappingProxyType[int, int]: """ return MappingProxyType(self.__output_node_indices) - @property - def num_physical_nodes(self) -> int: - """Return the number of physical nodes. - - Returns - ------- - `int` - number of physical nodes. - """ - return len(self.__physical_nodes) - - @property - def num_physical_edges(self) -> int: - """Return the number of undirected physical edges. - - Returns - ------- - `int` - number of physical edges. - """ - return sum(len(edges) for edges in self.__physical_edges.values()) // 2 - @property @typing_extensions.override def physical_nodes(self) -> frozenset[int]: diff --git a/tests/test_graphstate.py b/tests/test_graphstate.py index ca510a4f8..576912f3f 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -24,7 +24,7 @@ def test_add_physical_node(graph: GraphState) -> None: """Test adding a physical node to the graph.""" node_index = graph.add_physical_node() assert node_index in graph.physical_nodes - assert graph.num_physical_nodes == 1 + assert len(graph.physical_nodes) == 1 def test_add_physical_node_input_output(graph: GraphState) -> None: @@ -68,7 +68,7 @@ def test_add_physical_edge(graph: GraphState) -> None: node_index2 = graph.add_physical_node() graph.add_physical_edge(node_index1, node_index2) assert (node_index1, node_index2) in graph.physical_edges or (node_index2, node_index1) in graph.physical_edges - assert graph.num_physical_edges == 1 + assert len(graph.physical_edges) == 1 def test_add_duplicate_physical_edge(graph: GraphState) -> None: @@ -106,7 +106,7 @@ def test_remove_physical_node(graph: GraphState) -> None: node_index = graph.add_physical_node() graph.remove_physical_node(node_index) assert node_index not in graph.physical_nodes - assert graph.num_physical_nodes == 0 + assert len(graph.physical_nodes) == 0 def test_remove_physical_node_from_minimal_graph(graph: GraphState) -> None: @@ -117,8 +117,8 @@ def test_remove_physical_node_from_minimal_graph(graph: GraphState) -> None: graph.remove_physical_node(node_index1) assert node_index1 not in graph.physical_nodes assert node_index2 in graph.physical_nodes - assert graph.num_physical_nodes == 1 - assert graph.num_physical_edges == 0 + assert len(graph.physical_nodes) == 1 + assert len(graph.physical_edges) == 0 def test_remove_physical_node_from_3_nodes_graph(graph: GraphState) -> None: @@ -132,8 +132,8 @@ def test_remove_physical_node_from_3_nodes_graph(graph: GraphState) -> None: graph.register_output(node_index3, q_index) graph.remove_physical_node(node_index2) assert graph.physical_nodes == {node_index1, node_index3} - assert graph.num_physical_nodes == 2 - assert graph.num_physical_edges == 0 + assert len(graph.physical_nodes) == 2 + assert len(graph.physical_edges) == 0 assert graph.input_node_indices == {node_index1: q_index} assert graph.output_node_indices == {node_index3: q_index} @@ -160,7 +160,7 @@ def test_remove_physical_edge(graph: GraphState) -> None: graph.remove_physical_edge(node_index1, node_index2) assert (node_index1, node_index2) not in graph.physical_edges assert (node_index2, node_index1) not in graph.physical_edges - assert graph.num_physical_edges == 0 + assert len(graph.physical_edges) == 0 def test_register_output_raises_1(graph: GraphState) -> None: From 8527b43c6a83a1a4f09e7a5b6f7d74a9eb507f5c Mon Sep 17 00:00:00 2001 From: masa10-f Date: Mon, 21 Apr 2025 21:56:04 +0900 Subject: [PATCH 63/67] fix docstring --- graphix_zx/graphstate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index a15421f13..f7757fb65 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -204,7 +204,7 @@ def input_node_indices(self) -> MappingProxyType[int, int]: Returns ------- - `dict`\[`int`, `int`\] + `types.MappingProxyType`\[`int`, `int`\] qubit indices map of input nodes. """ return MappingProxyType(self.__input_node_indices) @@ -216,7 +216,7 @@ def output_node_indices(self) -> MappingProxyType[int, int]: Returns ------- - `dict`\[`int`, `int`\] + `types.MappingProxyType`\[`int`, `int`\] qubit indices map of output nodes. """ return MappingProxyType(self.__output_node_indices) @@ -228,7 +228,7 @@ def physical_nodes(self) -> frozenset[int]: Returns ------- - `set`\[`int`\] + `frozenset`\[`int`\] set of physical nodes. """ return frozenset(self.__physical_nodes) @@ -268,7 +268,7 @@ def local_cliffords(self) -> MappingProxyType[int, LocalClifford]: Returns ------- - `dict`\[`int`, `LocalClifford`\] + `types.MappingProxyType`\[`int`, `LocalClifford`\] local clifford nodes. """ return MappingProxyType(self.__local_cliffords) From 7af18569df28b614e95fee112b176f8757ccf03d Mon Sep 17 00:00:00 2001 From: masa10-f Date: Mon, 21 Apr 2025 21:58:01 +0900 Subject: [PATCH 64/67] add error cases in register input/output --- graphix_zx/graphstate.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index f7757fb65..fe26e2d42 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -405,8 +405,16 @@ def register_input(self, node: int) -> int: ------- `int` logical qubit index + + Raises + ------ + ValueError + If the node is already registered as an input node. """ self._ensure_node_exists(node) + if node in self.__input_node_indices: + msg = "The node is already registered as an input node." + raise ValueError(msg) q_index = len(self.__input_node_indices) self.__input_node_indices[node] = q_index return q_index @@ -425,12 +433,15 @@ def register_output(self, node: int, q_index: int) -> None: Raises ------ ValueError - 1. If the node does not exist. + 1. If the node is already registered as an output node. 2. If the node has a measurement basis. 3. If the invalid q_index specified. 4. If the q_index already exists in output qubit indices. """ self._ensure_node_exists(node) + if node in self.__output_node_indices: + msg = "The node is already registered as an output node." + raise ValueError(msg) if self.meas_bases.get(node) is not None: msg = "Cannot set output node with measurement basis." raise ValueError(msg) From b14f70b04b5b81f251cb3bd7c862ee8ab03401d5 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Tue, 22 Apr 2025 21:14:13 +0900 Subject: [PATCH 65/67] update remove_output_node --- graphix_zx/graphstate.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index fe26e2d42..966370958 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -358,14 +358,17 @@ def remove_physical_node(self, node: int) -> None: ValueError If the input node is specified """ + self._ensure_node_exists(node) if node in self.input_node_indices: msg = "The input node cannot be removed" raise ValueError(msg) - self._ensure_node_exists(node) self.__physical_nodes -= {node} for neighbor in self.__physical_edges[node]: self.__physical_edges[neighbor] -= {node} del self.__physical_edges[node] + + if node in self.output_node_indices: + del self.__output_node_indices[node] self.__meas_bases.pop(node, None) self.__local_cliffords.pop(node, None) @@ -463,8 +466,16 @@ def assign_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: node index meas_basis : `MeasBasis` measurement basis + + Raises + ------ + ValueError + If the node is an output node. """ self._ensure_node_exists(node) + if node in self.output_node_indices: + msg = "Cannot set measurement basis for output node." + raise ValueError(msg) self.__meas_bases[node] = meas_basis def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: From 4e6d54dc469ef83b080e8d07600df5b760f22bdc Mon Sep 17 00:00:00 2001 From: masa10-f Date: Tue, 22 Apr 2025 22:14:01 +0900 Subject: [PATCH 66/67] WIP: refactor meas_bases --- graphix_zx/graphstate.py | 111 +++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 57 deletions(-) diff --git a/graphix_zx/graphstate.py b/graphix_zx/graphstate.py index 966370958..9024d0473 100644 --- a/graphix_zx/graphstate.py +++ b/graphix_zx/graphstate.py @@ -12,7 +12,9 @@ from __future__ import annotations import operator +import weakref from abc import ABC, abstractmethod +from collections.abc import Iterator, MutableMapping from itertools import product from types import MappingProxyType from typing import TYPE_CHECKING @@ -28,9 +30,61 @@ from graphix_zx.euler import LocalClifford +class _MeasBasesDict(MutableMapping): + _owner: BaseGraphState + _field_name: str + _store: dict[int, MeasBasis] + + def __init__(self, owner: BaseGraphState, field_name: str) -> None: + self._owner = weakref.proxy(owner) + self._field_name = field_name + self._store = {} + + def __getitem__(self, node: int) -> MeasBasis: + return self._store[node] + + def __iter__(self) -> Iterator[int]: + return iter(self._store) + + def __len__(self) -> int: + return len(self._store) + + def __delitem__(self, node: int) -> None: + del self._store[node] + + def __setitem__(self, node: int, meas_basis: MeasBasis) -> None: + if node not in self._owner.physical_nodes: + msg = f"Node {node} does not exist in the graph state." + raise ValueError(msg) + if node in self._owner.output_node_indices: + msg = "Cannot set measurement basis for output node." + raise ValueError(msg) + self._store[node] = meas_basis + + +class MeasBasesField: + def __set_name__(self, owner, name: str) -> None: + self._private_name = f"_{name}" + + def __get__(self, obj, owner) -> _MeasBasesDict | MeasBasesField: + if obj is None: + return self + mapping = getattr(obj, self._private_name, None) + if mapping is None: + mapping = _MeasBasesDict(obj, self._private_name) + setattr(obj, self._private_name, mapping) + return mapping + + def __set__(self, obj, value: _MeasBasesDict) -> None: + msg = f"Cannot set {self._private_name} directly." + raise AttributeError(msg) + + class BaseGraphState(ABC): """Abstract base class for Graph State.""" + meas_bases = MeasBasesField() + @property @abstractmethod def input_node_indices(self) -> MappingProxyType[int, int]: @@ -75,17 +129,6 @@ def physical_edges(self) -> frozenset[tuple[int, int]]: set of physical edges. """ - @property - @abstractmethod - def meas_bases(self) -> MappingProxyType[int, MeasBasis]: - r"""Return measurement bases. - - Returns - ------- - `types.MappingProxyType`\[`int`, `MeasBasis`\] - measurement bases of each physical node. - """ - @abstractmethod def add_physical_node( self, @@ -137,18 +180,6 @@ def register_output(self, node: int, q_index: int) -> None: logical qubit index """ - @abstractmethod - def assign_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: - """Assign the measurement basis of the node. - - Parameters - ---------- - node : `int` - node index - meas_basis : `MeasBasis` - measurement basis - """ - @abstractmethod def neighbors(self, node: int) -> frozenset[int]: r"""Return the neighbors of the node. @@ -250,18 +281,6 @@ def physical_edges(self) -> frozenset[tuple[int, int]]: edges |= {(node1, node2)} return frozenset(edges) - @property - @typing_extensions.override - def meas_bases(self) -> MappingProxyType[int, MeasBasis]: - r"""Return measurement bases. - - Returns - ------- - `types.MappingProxyType`\[`int`, `MeasBasis`\] - measurement bases of each physical node. - """ - return MappingProxyType(self.__meas_bases) - @property def local_cliffords(self) -> MappingProxyType[int, LocalClifford]: r"""Return local clifford nodes. @@ -456,28 +475,6 @@ def register_output(self, node: int, q_index: int) -> None: raise ValueError(msg) self.__output_node_indices[node] = q_index - @typing_extensions.override - def assign_meas_basis(self, node: int, meas_basis: MeasBasis) -> None: - """Set the measurement basis of the node. - - Parameters - ---------- - node : `int` - node index - meas_basis : `MeasBasis` - measurement basis - - Raises - ------ - ValueError - If the node is an output node. - """ - self._ensure_node_exists(node) - if node in self.output_node_indices: - msg = "Cannot set measurement basis for output node." - raise ValueError(msg) - self.__meas_bases[node] = meas_basis - def apply_local_clifford(self, node: int, lc: LocalClifford) -> None: """Apply a local clifford to the node. From 65662f5e64665a9441ccab0f0c0bbed1a5cc63c9 Mon Sep 17 00:00:00 2001 From: masa10-f Date: Tue, 22 Apr 2025 22:14:31 +0900 Subject: [PATCH 67/67] update 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 576912f3f..be82870b8 100644 --- a/tests/test_graphstate.py +++ b/tests/test_graphstate.py @@ -170,7 +170,7 @@ def test_register_output_raises_1(graph: GraphState) -> None: def test_register_output_raises_2(graph: GraphState) -> None: node_index = graph.add_physical_node() - graph.assign_meas_basis(node_index, PlannerMeasBasis(Plane.XY, 0.5 * np.pi)) + graph.meas_bases[node_index] = PlannerMeasBasis(Plane.XY, 0.5 * np.pi) with pytest.raises(ValueError, match=r"Cannot set output node with measurement basis."): graph.register_output(node_index, 0) @@ -179,7 +179,7 @@ def test_assign_meas_basis(graph: GraphState) -> None: """Test setting the measurement basis of a physical node.""" node_index = graph.add_physical_node() meas_basis = PlannerMeasBasis(Plane.XZ, 0.5 * np.pi) - graph.assign_meas_basis(node_index, meas_basis) + graph.meas_bases[node_index] = meas_basis assert graph.meas_bases[node_index].plane == Plane.XZ assert graph.meas_bases[node_index].angle == 0.5 * np.pi @@ -197,7 +197,7 @@ def test_check_meas_basis_success(graph: GraphState) -> None: node_index1 = graph.add_physical_node() q_index = graph.register_input(node_index1) meas_basis = PlannerMeasBasis(Plane.XY, 0.5 * np.pi) - graph.assign_meas_basis(node_index1, meas_basis) + graph.meas_bases[node_index1] = meas_basis graph._check_meas_basis() node_index2 = graph.add_physical_node()