From ea51deda08d0b9996f2ac8e13b8e6e505b9c15b7 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 25 Nov 2025 11:46:22 +0100 Subject: [PATCH 01/58] Add flow verification utils --- graphix/flow/core.py | 479 +++++++++++++++++++++++++++++++++++++++- tests/test_flow_core.py | 132 ++++++++++- 2 files changed, 603 insertions(+), 8 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index 94c88008c..45512511e 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -2,19 +2,27 @@ from __future__ import annotations +import enum from collections.abc import Sequence from copy import copy from dataclasses import dataclass +from enum import Enum from typing import TYPE_CHECKING, Generic import networkx as nx -# override introduced in Python 3.12 -from typing_extensions import override +# `override` introduced in Python 3.12, `assert_never` introduced in Python 3.11 +from typing_extensions import assert_never, override import graphix.pattern from graphix.command import E, M, N, X, Z -from graphix.flow._find_gpflow import CorrectionMatrix, _M_co, _PM_co, compute_partial_order_layers +from graphix.flow._find_gpflow import ( + CorrectionMatrix, + _M_co, + _PM_co, + compute_partial_order_layers, +) +from graphix.fundamentals import Axis, Plane if TYPE_CHECKING: from collections.abc import Mapping @@ -272,7 +280,7 @@ class PauliFlow(Generic[_M_co]): ----- - See Definition 5 in Ref. [1] for a definition of Pauli flow. - - The flow's correction function defines a partial order (see Def. 2.8 and 2.9, Lemma 2.11 and Theorem 2.12 in Ref. [2]), therefore, only `og` and `correction_function` are necessary to initialize an `PauliFlow` instance (see :func:`PauliFlow.from_correction_matrix`). However, flow-finding algorithms generate a partial order in a layer form, which is necessary to extract the flow's XZ-corrections, so it is stored as an attribute. + - The flow's correction function defines a partial order (see Def. 2.8 and 2.9, Lemma 2.11 and Theorem 2.12 in Ref. [2]), therefore, only `og` and `correction_function` are necessary to initialize an `PauliFlow` instance (see :func:`PauliFlow.try_from_correction_matrix`). However, flow-finding algorithms generate a partial order in a layer form, which is necessary to extract the flow's XZ-corrections, so it is stored as an attribute. - A correct flow can only exist on an open graph with output nodes, so `layers[0]` always contains a finite set of nodes. @@ -289,9 +297,9 @@ class PauliFlow(Generic[_M_co]): @classmethod def try_from_correction_matrix(cls, correction_matrix: CorrectionMatrix[_M_co]) -> Self | None: - """Initialize a Pauli flow object from a matrix encoding a correction function. + """Initialize a `PauliFlow` object from a matrix encoding a correction function. - Attributes + Parameters ---------- correction_matrix : CorrectionMatrix[_M_co] Algebraic representation of the correction function. @@ -348,6 +356,123 @@ def to_corrections(self) -> XZCorrections[_M_co]: return XZCorrections(self.og, x_corrections, z_corrections, self.partial_order_layers) + def is_well_formed(self) -> bool: + """Verify if flow is well formed. + + This method is a wrapper over :func:`self.check_well_formed` catching the `FlowError` exceptions. + + Returns + ------- + ``True`` if ``self`` is a well-formed flow, ``False`` otherwise. + """ + try: + self.check_well_formed() + except FlowError: + return False + return True + + def check_well_formed(self) -> None: + r"""Verify if the Pauli flow is well formed. + + Raises + ------ + FlowError + if the Pauli flow is not well formed. + + Notes + ----- + General properties of flows: + - The domain of the correction function is :math:`O^c`, the non-output nodes of the open graph. + - The image of the correction function is a subset of :math:`I^c`, the non-input nodes of the open graph. + - The nodes in the partial order are the nodes in the open graph. + - The first layer of the partial order layers is :math:`O`, the output nodes of the open graph. This is guaranteed because open graphs without outputs do not have flow. + + Specific properties of Pauli flows: + - If :math:`j \in p(i), i \neq j, \lambda(j) \notin \{X, Y\}`, then :math:`i \prec j` (P1). + - If :math:`j \in Odd(p(i)), i \neq j, \lambda(j) \notin \{Y, Z\}`, then :math:`i \prec j` (P2). + - If :math:`neg i \prec j, i \neq j, \lambda(j) = Y`, then either :math:`j \notin p(i)` and :math:`j \in Odd((p(i)))` or :math:`j \in p(i)` and :math:`j \notin Odd((p(i)))` (P3). + - If :math:`\lambda(i) = XY`, then :math:`i \notin p(i)` and :math:`i \in Odd((p(i)))` (P4). + - If :math:`\lambda(i) = XZ`, then :math:`i \in p(i)` and :math:`i \in Odd((p(i)))` (P5). + - If :math:`\lambda(i) = YZ`, then :math:`i \in p(i)` and :math:`i \notin Odd((p(i)))` (P6). + - If :math:`\lambda(i) = X`, then :math:`i \in Odd((p(i)))` (P7). + - If :math:`\lambda(i) = Z`, then :math:`i \in p(i)` (P8). + - If :math:`\lambda(i) = Y`, then either :math:`i \notin p(i)` and :math:`i \in Odd((p(i)))` or :math:`i \in p(i)` and :math:`i \notin Odd((p(i)))` (P9), + where :math:`i \in O^c`, :math:`c` is the correction function, :math:`prec` denotes the partial order, :math:`\lambda(i)` is the measurement plane or axis of node :math:`i`, and :math:`Odd(s)` is the odd neighbourhood of the set :math:`s` in the open graph. + + See Definition 5 in Ref. [1] or Definition 2.4 in Ref. [2]. + + References + ---------- + [1] Browne et al., 2007 New J. Phys. 9 250 (arXiv:quant-ph/0702212). + [2] Mitosek and Backens, 2024 (arXiv:2410.23439). + """ + if not _check_correction_function_domain(self.og, self.correction_function): + raise FlowError(FlowErrorReason.CorrectionFunctionDomain) + + if not _check_correction_function_image(self.og, self.correction_function): + raise FlowError(FlowErrorReason.CorrectionFunctionImage) + + o_set = set(self.og.output_nodes) + oc_set = set(self.og.graph.nodes - o_set) + + first_layer = self.partial_order_layers[0] + if first_layer != o_set or not first_layer: + raise FlowError(FlowErrorReason.PartialOrderFirstLayer, layer=first_layer) + + past_and_present_nodes: set[int] = set() + past_and_present_nodes_y_meas: set[int] = set() + + for layer in reversed(self.partial_order_layers[1:]): + if not oc_set.issuperset(layer) or not layer: + raise FlowError(FlowErrorReason.PartialOrderNthLayer) + + past_and_present_nodes.update(layer) + for node in layer: + correction_set = set(self.correction_function[node]) + + meas = self.og.measurements[node].to_plane_or_axis() + + for i in (correction_set - {node}) & past_and_present_nodes: + if self.og.measurements[i].to_plane_or_axis() not in {Axis.X, Axis.Y}: + raise FlowError(FlowErrorReason.P1, node=node, correction_set=correction_set) + + odd_neighbors = self.og.odd_neighbors(correction_set) + + for i in (odd_neighbors - {node}) & past_and_present_nodes: + if self.og.measurements[i].to_plane_or_axis() not in {Axis.Y, Axis.Z}: + raise FlowError(FlowErrorReason.P2, node=node, correction_set=correction_set) + + closed_odd_neighbors = (odd_neighbors | correction_set) - (odd_neighbors & correction_set) + + # This check must be done before adding the node to `past_and_present_nodes_y_meas` + if past_and_present_nodes_y_meas & closed_odd_neighbors: + raise FlowError(FlowErrorReason.P3, node=node, correction_set=correction_set) + + if meas == Plane.XY: + if not (node not in correction_set and node in odd_neighbors): + raise FlowError(FlowErrorReason.P4, node=node, correction_set=correction_set) + elif meas == Plane.XZ: + if not (node in correction_set and node in odd_neighbors): + raise FlowError(FlowErrorReason.P5, node=node, correction_set=correction_set) + elif meas == Plane.YZ: + if not (node in correction_set and node not in odd_neighbors): + raise FlowError(FlowErrorReason.P6, node=node, correction_set=correction_set) + elif meas == Axis.X: + if node not in odd_neighbors: + raise FlowError(FlowErrorReason.P7, node=node, correction_set=correction_set) + elif meas == Axis.Z: + if node not in correction_set: + raise FlowError(FlowErrorReason.P8, node=node, correction_set=correction_set) + elif meas == Axis.Y: + past_and_present_nodes_y_meas.add(node) + if node not in closed_odd_neighbors: + raise FlowError(FlowErrorReason.P9, node=node, correction_set=correction_set) + else: + assert_never(meas) + + if {*o_set, *past_and_present_nodes} != set(self.og.graph.nodes): + raise FlowError(FlowErrorReason.PartialOrderNodes) + @dataclass(frozen=True) class GFlow(PauliFlow[_PM_co], Generic[_PM_co]): @@ -364,6 +489,31 @@ class GFlow(PauliFlow[_PM_co], Generic[_PM_co]): """ + @override + @classmethod + def try_from_correction_matrix(cls, correction_matrix: CorrectionMatrix[_PM_co]) -> Self | None: + """Initialize a `GFlow` object from a matrix encoding a correction function. + + Parameters + ---------- + correction_matrix : CorrectionMatrix[_PM_co] + Algebraic representation of the correction function. + + Returns + ------- + Self | None + A gflow if it exists, ``None`` otherwise. + + Notes + ----- + This method verifies if there exists a partial measurement order on the input open graph compatible with the input correction matrix. See Lemma 3.12, and Theorem 3.1 in Ref. [1]. Failure to find a partial order implies the non-existence of a generalised flow if the correction matrix was calculated by means of Algorithms 2 and 3 in [1]. + + References + ---------- + [1] Mitosek and Backens, 2024 (arXiv:2410.23439). + """ + return super().try_from_correction_matrix(correction_matrix) + @override def to_corrections(self) -> XZCorrections[_PM_co]: r"""Compute the XZ-corrections induced by the generalised flow encoded in `self`. @@ -394,6 +544,84 @@ def to_corrections(self) -> XZCorrections[_PM_co]: return XZCorrections(self.og, x_corrections, z_corrections, self.partial_order_layers) + def check_well_formed(self) -> None: + r"""Verify if the generalised flow is well formed. + + Raises + ------ + FlowError + if the gflow is not well formed. + + Notes + ----- + General properties of flows: + - The domain of the correction function is :math:`O^c`, the non-output nodes of the open graph. + - The image of the correction function is a subset of :math:`I^c`, the non-input nodes of the open graph. + - The nodes in the partial order are the nodes in the open graph. + - The first layer of the partial order layers is :math:`O`, the output nodes of the open graph. This is guaranteed because open graphs without outputs do not have flow. + + Specific properties of gflows: + - If :math:`j \in g(i), i \neq j`, then :math:`i \prec j` (G1). + - If :math:`j \in Odd(g(i)), i \neq j`, then :math:`i \prec j` (G2). + - If :math:`\lambda(i) = XY`, then :math:`i \notin g(i)` and :math:`i \in Odd((g(i)))` (G3). + - If :math:`\lambda(i) = XZ`, then :math:`i \in g(i)` and :math:`i \in Odd((g(i)))` (G4). + - If :math:`\lambda(i) = YZ`, then :math:`i \in g(i)` and :math:`i \notin Odd((g(i)))` (G5), + where :math:`i \in O^c`, :math:`g` is the correction function, :math:`prec` denotes the partial order, :math:`\lambda(i)` is the measurement plane of node :math:`i`, and :math:`Odd(s)` is the odd neighbourhood of the set :math:`s` in the open graph. + + See Definition 2.36 in Ref. [1]. + + References + ---------- + [1] Backens et al., Quantum 5, 421 (2021), doi.org/10.22331/q-2021-03-25-421 + """ + if not _check_correction_function_domain(self.og, self.correction_function): + raise FlowError(FlowErrorReason.CorrectionFunctionDomain) + + if not _check_correction_function_image(self.og, self.correction_function): + raise FlowError(FlowErrorReason.CorrectionFunctionImage) + + o_set = set(self.og.output_nodes) + oc_set = set(self.og.graph.nodes - o_set) + + first_layer = self.partial_order_layers[0] + if first_layer != o_set or not first_layer: + raise FlowError(FlowErrorReason.PartialOrderFirstLayer, layer=first_layer) + + past_and_present_nodes: set[int] = set() + for layer in reversed(self.partial_order_layers[1:]): + if not oc_set.issuperset(layer) or not layer: + raise FlowError(FlowErrorReason.PartialOrderNthLayer) + + past_and_present_nodes.update(layer) + + for node in layer: + correction_set = set(self.correction_function[node]) + + if (correction_set - {node}) & past_and_present_nodes: + raise FlowError(FlowErrorReason.G1, node=node, correction_set=correction_set) + + odd_neighbors = self.og.odd_neighbors(correction_set) + + if (odd_neighbors - {node}) & past_and_present_nodes: + raise FlowError(FlowErrorReason.G2, node=node, correction_set=correction_set) + + plane = self.og.measurements[node].to_plane() + + if plane == Plane.XY: + if not (node not in correction_set and node in odd_neighbors): + raise FlowError(FlowErrorReason.G3, node=node, correction_set=correction_set) + elif plane == Plane.XZ: + if not (node in correction_set and node in odd_neighbors): + raise FlowError(FlowErrorReason.G4, node=node, correction_set=correction_set) + elif plane == Plane.YZ: + if not (node in correction_set and node not in odd_neighbors): + raise FlowError(FlowErrorReason.G5, node=node, correction_set=correction_set) + else: + assert_never(plane) + + if {*o_set, *past_and_present_nodes} != set(self.og.graph.nodes): + raise FlowError(FlowErrorReason.PartialOrderNodes) + @dataclass(frozen=True) class CausalFlow(GFlow[_PM_co], Generic[_PM_co]): @@ -442,6 +670,75 @@ def to_corrections(self) -> XZCorrections[_PM_co]: return XZCorrections(self.og, x_corrections, z_corrections, self.partial_order_layers) + def check_well_formed(self) -> None: + r"""Verify if the causal flow is well formed. + + Raises + ------ + FlowError + if the Pauli flow is not well formed. + + Notes + ----- + General properties of flows: + - The domain of the correction function is :math:`O^c`, the non-output nodes of the open graph. + - The image of the correction function is a subset of :math:`I^c`, the non-input nodes of the open graph. + - The nodes in the partial order are the nodes in the open graph. + - The first layer of the partial order layers is :math:`O`, the output nodes of the open graph. This is guaranteed because open graphs without outputs do not have flow. + + Specific properties of causal flows: + - Correction sets have one element only, + - :math:`i \sim c(i)` (C1), + - :math:`i \prec c(i)` (C2), + - :math:`\forall k \in N_G(c(i)) \setminus \{i\}, i \prec k` (C3), + where :math:`i \in O^c`, :math:`c` is the correction function and :math:`prec` denotes the partial order. + + See Definition 2 in Ref. [1]. + + References + ---------- + [1] Browne et al., 2007 New J. Phys. 9 250 (arXiv:quant-ph/0702212). + """ + if not _check_correction_function_domain(self.og, self.correction_function): + raise FlowError(FlowErrorReason.CorrectionFunctionDomain) + + if not _check_correction_function_image(self.og, self.correction_function): + raise FlowError(FlowErrorReason.CorrectionFunctionImage) + + o_set = set(self.og.output_nodes) + oc_set = set(self.og.graph.nodes - o_set) + + first_layer = self.partial_order_layers[0] + if first_layer != o_set or not first_layer: + raise FlowError(FlowErrorReason.PartialOrderFirstLayer, layer=first_layer) + + past_and_present_nodes: set[int] = set() + for layer in reversed(self.partial_order_layers[1:]): + if not oc_set.issuperset(layer) or not layer: + raise FlowError(FlowErrorReason.PartialOrderNthLayer) + + past_and_present_nodes.update(layer) + + for node in layer: + correction_set = set(self.correction_function[node]) + + if len(correction_set) != 1: + raise FlowError(FlowErrorReason.CorrectionSetCausalFlow, node=node, correction_set=correction_set) + + neighbors = self.og.neighbors(correction_set) + + if node not in neighbors: + raise FlowError(FlowErrorReason.C1, node=node, correction_set=correction_set) + + if correction_set & past_and_present_nodes: + raise FlowError(FlowErrorReason.C2, node=node, correction_set=correction_set) + + if (neighbors - {node}) & past_and_present_nodes: + raise FlowError(FlowErrorReason.C3, node=node, correction_set=correction_set) + + if {*o_set, *past_and_present_nodes} != set(self.og.graph.nodes): + raise FlowError(FlowErrorReason.PartialOrderNodes) + def _corrections_to_dag( x_corrections: Mapping[int, AbstractSet[int]], z_corrections: Mapping[int, AbstractSet[int]] @@ -494,3 +791,173 @@ def _dag_to_partial_order_layers(dag: nx.DiGraph[int]) -> list[frozenset[int]] | return None return [frozenset(layer) for layer in topo_gen] + + +def _check_correction_function_domain( + og: OpenGraph[_M_co], correction_function: Mapping[int, AbstractSet[int]] +) -> bool: + """Verify that the domain of the correction function is the set of non-output nodes of the open graph.""" + oc_set = og.graph.nodes - set(og.output_nodes) + return correction_function.keys() == oc_set + + +def _check_correction_function_image(og: OpenGraph[_M_co], correction_function: Mapping[int, AbstractSet[int]]) -> bool: + """Verify that the image of the correction function is a subset of non-input nodes of the open graph.""" + ic_set = og.graph.nodes - set(og.input_nodes) + image = set().union(*correction_function.values()) + return image.issubset(ic_set) + + +# def _check_partial_order_layers(og: OpenGraph[_M_co], partial_order_layers: Sequence[AbstractSet[int]]) -> bool: +# """Verify that the partial order contains all the nodes of the open graph and that there are not empty layers.""" +# nodes: set[int] = set() +# for layer in partial_order_layers: +# if not layer: +# return False +# nodes.update(layer) +# return nodes == set(og.graph.nodes) + + +class FlowErrorReason(Enum): + """Describe the reason of a `FlowError`.""" + + CorrectionFunctionDomain = enum.auto() + """The domain of the correction function is not the set of non-output nodes (measured qubits) of the open graph.""" + + CorrectionFunctionImage = enum.auto() + """The image of the correction function is not a subset of non-input nodes (prepared qubits) of the open graph.""" + + PartialOrderFirstLayer = enum.auto() + """The first layer of the partial order is not the set of output nodes (non-measured qubits) of the open graph or is be empty.""" # A well-defined flow cannot exist on an open graph without outputs. + + PartialOrderNthLayer = enum.auto() + """Nodes in the partial order beyond the first layer are not non-output nodes (measured qubits) of the open graph or layer is empty.""" + + PartialOrderNodes = enum.auto() + """The partial order does not contain all the nodes of the open graph or contains nodes that are not in the open graph.""" + + CorrectionSetCausalFlow = enum.auto() + """A correction set in a causal flow has more than one element.""" + + C1 = enum.auto() + """Causal flow (C1). A node and its corrector must be neighbors.""" + + C2 = enum.auto() + """Causal flow (C2). Nodes must be in the past of their correction set.""" + + C3 = enum.auto() + """Causal flow (C3). Neighbors of the correcting nodes (except the corrected node) must be in the future of the corrected node.""" + + G1 = enum.auto() + """Gflow (G1). Equivalent to (C1) but for gflows.""" + + G2 = enum.auto() + """Gflow (G2). The odd neighbourhood (except the corrected node) of the correcting nodes must be in the future of the corrected node.""" + + G3 = enum.auto() + """Gflow (G3). Nodes measured on plane XY cannot be in their own correcting set and must belong to the odd neighbourhood of their own correcting set.""" + + G4 = enum.auto() + """Gflow (G4). Nodes measured on plane XZ must belong to their own correcting set and its odd neighbourhood.""" + + G5 = enum.auto() + """Gflow (G5). Nodes measured on plane YZ must belong to their own correcting set and cannot be in the odd neighbourhood of their own correcting set.""" + + P1 = enum.auto() + """Pauli flow (P1). Nodes must be in the past of their correcting nodes that are not measured along the X or the Y axes.""" + + P2 = enum.auto() + """Pauli flow (P2). The odd neighbourhood (except the corrected node and nodes measured along axes Y or Z) of the correcting nodes must be in the future of the corrected node.""" + + P3 = enum.auto() + """Pauli flow (P3). Nodes that are measured along axis Y and that are not in the future of the corrected node (except the corrected node itself) cannot be in the closed odd neighbourhood of the correcting set.""" + + P4 = enum.auto() + """Pauli flow (P4). Equivalent to (G3) but for Pauli flows.""" + + P5 = enum.auto() + """Pauli flow (P5). Equivalent to (G4) but for Pauli flows.""" + + P6 = enum.auto() + """Pauli flow (P6). Equivalent to (G5) but for Pauli flows.""" + + P7 = enum.auto() + """Pauli flow (P7). Nodes measured along axis X must belong to the odd neighbourhood of their own correcting set.""" + + P8 = enum.auto() + """Pauli flow (P8). Nodes measured along axis Z must belong to their own correcting set.""" + + P9 = enum.auto() + """Pauli flow (P9). Nodes measured along axis Y must belong to the closed odd neighbourhood of their own correcting set.""" + + +@dataclass +class FlowError(Exception): + """Exception subclass to handle flow errors.""" + + reason: FlowErrorReason + node: int | None = None + correction_set: AbstractSet[int] | None = None + layer_index: int | None = None + layer: AbstractSet[int] | None = None + + def __str__(self) -> str: + """Explain the error.""" + if self.reason == FlowErrorReason.CorrectionFunctionDomain: + return "The domain of the correction function must be the set of non-output nodes (measured qubits) of the open graph." + + if self.reason == FlowErrorReason.CorrectionFunctionImage: + return "The image of the correction function must be a subset of non-input nodes (prepared qubits) of the open graph." + + if self.reason == FlowErrorReason.PartialOrderFirstLayer: + return f"The first layer of the partial order must contain all the output nodes of the open graph and cannot be empty. First layer: {self.layer}" + + if self.reason == FlowErrorReason.PartialOrderNthLayer: + return f"Partial order layer {self.layer_index} = {self.layer} contains non-measured nodes of the open graph or is empty." + + if self.reason == FlowErrorReason.PartialOrderNodes: + return "The partial order does not contain all the nodes of the open graph or contains nodes that are not in the open graph." + + if self.reason == FlowErrorReason.CorrectionSetCausalFlow: + return f"Correction set c({self.node}) = {self.correction_set} has more than one element." + + if self.reason == FlowErrorReason.C1: + return f"{self.reason.name}: a node and its corrector must be neighbors. Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowErrorReason.C2 or self.reason == FlowErrorReason.G1: # noqa: PLR1714 + return f"{self.reason.name}: nodes must be in the past of their correction set. Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowErrorReason.C3: + return f"{self.reason.name}: neighbors of the correcting nodes (except the corrected node) must be in the future of the corrected node. Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowErrorReason.G2: + return f"{self.reason.name}: the odd neighbourhood (except the corrected node) of the correcting nodes must be in the future of the corrected node. Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowErrorReason.G3 or self.reason == FlowErrorReason.P4: # noqa: PLR1714 + return f"{self.reason.name}: nodes measured on plane XY cannot be in their own correcting set and must belong to the odd neighbourhood of their own correcting set. Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowErrorReason.G4 or self.reason == FlowErrorReason.P5: # noqa: PLR1714 + return f"{self.reason.name}: nodes measured on plane XZ must belong to their own correcting set and its odd neighbourhood. Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowErrorReason.G5 or self.reason == FlowErrorReason.P6: # noqa: PLR1714 + return f"{self.reason.name}: nodes measured on plane YZ must belong to their own correcting set and cannot be in the odd neighbourhood of their own correcting set. Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowErrorReason.P1: + return f"{self.reason.name}: nodes must be in the past of their correcting nodes that are not measured along the X or the Y axes. Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowErrorReason.P2: + return f"{self.reason.name}: the odd neighbourhood (except the corrected node and nodes measured along axes Y or Z) of the correcting nodes must be in the future of the corrected node. Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowErrorReason.P3: + return f"{self.reason.name}: nodes that are measured along axis Y and that are not in the future of the corrected node (except the corrected node itself) cannot be in the closed odd neighbourhood of the correcting set. Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowErrorReason.P7: + return f"{self.reason.name}: nodes measured along axis X must belong to the odd neighbourhood of their own correcting set. Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowErrorReason.P8: + return f"{self.reason.name}: nodes measured along axis Z must belong to their own correcting set. Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowErrorReason.P9: + return f"{self.reason.name}: nodes measured along axis Y must belong to the closed odd neighbourhood of their own correcting set. Error found at c({self.node}) = {self.correction_set}." + + assert_never(self.reason) diff --git a/tests/test_flow_core.py b/tests/test_flow_core.py index 9b15a9a72..5237e0723 100644 --- a/tests/test_flow_core.py +++ b/tests/test_flow_core.py @@ -279,13 +279,13 @@ def prepare_test_xzcorrections() -> list[XZCorrectionsTestCase]: E((3, 1)), E((3, 5)), M(0), - Z(3, {0}), Z(4, {0}), X(2, {0}), + X(5, {0}), M(1), - Z(2, {1}), Z(5, {1}), X(3, {1}), + X(4, {1}), M(2), X(4, {2}), M(3), @@ -583,3 +583,131 @@ def test_from_measured_nodes_mapping_exceptions(self) -> None: ValueError, match=r"Values of input mapping contain labels which are not nodes of the input open graph." ): XZCorrections.from_measured_nodes_mapping(og=og, x_corrections={0: {4}}) + + +class TestIncorrectFlows: + """Bundle for unit tests of :func:`PauliFlow.is_well_formed` (and children) on incorrect flows. Correct flows are extensively tested in `tests.test_opengraph.py`.""" + + og_c = OpenGraph( + graph=nx.Graph([(0, 1), (1, 2), (2, 3)]), + input_nodes=[0], + output_nodes=[3], + measurements=dict.fromkeys(range(3), Plane.XY), + ) + og_g = OpenGraph( + graph=nx.Graph([(0, 3), (0, 4), (1, 4), (2, 4)]), + input_nodes=[0], + output_nodes=[3, 4], + measurements={0: Plane.XY, 1: Plane.YZ, 2: Plane.XZ}, + ) + og_p = OpenGraph( + graph=nx.Graph([(0, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5), (4, 6), (5, 7)]), + input_nodes=[0, 1], + output_nodes=[6, 7], + measurements={ + 0: Measurement(0.1, Plane.XY), # XY + 1: Measurement(0.1, Plane.XY), # XY + 2: Measurement(0.0, Plane.XY), # X + 3: Measurement(0.1, Plane.XY), # XY + 4: Measurement(0.0, Plane.XY), # X + 5: Measurement(0.5, Plane.XY), # Y + }, + ) + + @pytest.mark.parametrize( + "test_case", + [ + # Incomplete correction function + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}}, + partial_order_layers=[{3}, {2}, {1}, {0}], + ), + # Incomplete partial order + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}}, + partial_order_layers=[{2}, {1}, {0}], + ), + # Wrong correction function + CausalFlow( + og=og_c, + correction_function={0: {0}, 1: {2}}, + partial_order_layers=[{3}, {2}, {1}, {0}], + ), + # Wrong correction function + CausalFlow( + og=og_c, + correction_function={0: {0, 1}, 1: {2}}, + partial_order_layers=[{3}, {2}, {1}, {0}], + ), + # Wrong correction function + CausalFlow( + og=og_c, + correction_function={0: {0}, 1: set()}, + partial_order_layers=[{3}, {2}, {1}, {0}], + ), + # Wrong partial order + CausalFlow( + og=og_c, + correction_function={0: {0}, 1: {2}}, + partial_order_layers=[{3}, {1, 2}, {0}], + ), + # Wrong correction function + GFlow( + og=og_g, + correction_function={0: {0, 3}, 1: {1}, 2: {2, 3, 4}}, + partial_order_layers=[{3, 4}, {1}, {0, 2}], + ), + # Wrong correction function + GFlow( + og=og_g, + correction_function={0: {3}, 1: {1}, 2: {3, 4}}, + partial_order_layers=[{3, 4}, {1}, {0, 2}], + ), + # Wrong correction function + GFlow( + og=og_g, + correction_function={0: {3}, 1: {1, 4}, 2: {2, 3, 4}}, + partial_order_layers=[{3, 4}, {1}, {0, 2}], + ), + # Partial order with duplicates + GFlow( + og=og_g, + correction_function={0: {3}, 1: {1}, 2: {3, 4}}, + partial_order_layers=[{3, 4}, {1, 0}, {0, 2}], + ), + # Wrong partial order duplicates + GFlow( + og=og_g, + correction_function={0: {3}, 1: {1}, 2: {3, 4}}, + partial_order_layers=[{3, 4}, {1, 0, 2}], + ), + # Nodes in partial order not in open graph + PauliFlow( + og=og_p, + correction_function={0: {2, 5, 7}, 1: {3, 4}, 2: {4, 7}, 3: {5, 6, 7}, 4: {6}, 5: {7}}, + partial_order_layers=[{6, 7}, {3, 100}, {0, 1, 2, 4, 5}], + ), + # Inputs in co-domain of correction function + PauliFlow( + og=og_p, + correction_function={0: {0, 2, 5, 7}, 1: {3, 4}, 2: {4, 7}, 3: {5, 6, 7}, 4: {6}, 5: {7}}, + partial_order_layers=[{6, 7}, {3}, {0, 1, 2, 4, 5}], + ), + # Wrong correction function + PauliFlow( + og=og_p, + correction_function={0: {2, 5, 7}, 1: {3, 4}, 2: {3, 4, 7}, 3: {5, 6, 7}, 4: {6}, 5: {7}}, + partial_order_layers=[{6, 7}, {3}, {0, 1, 2, 4, 5}], + ), + # Wrong partial order + PauliFlow( + og=og_p, + correction_function={0: {2, 5, 7}, 1: {3, 4}, 2: {4, 7}, 3: {5, 6, 7}, 4: {6}, 5: {7}}, + partial_order_layers=[{6, 7}, {3, 0, 1, 2, 4, 5}], + ), + ], + ) + def test_flow_is_well_formed(self, test_case: PauliFlow[Plane | Axis]) -> None: + assert not test_case.is_well_formed() From a7bfaf186255f02d7aab30c03e01bb7c143a97b5 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 25 Nov 2025 17:27:29 +0100 Subject: [PATCH 02/58] Up flow exceptions --- graphix/flow/core.py | 104 ++++++----- tests/test_flow_core.py | 382 ++++++++++++++++++++++++++++------------ tests/test_opengraph.py | 14 +- 3 files changed, 340 insertions(+), 160 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index 45512511e..081e64c0b 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -406,27 +406,22 @@ def check_well_formed(self) -> None: [1] Browne et al., 2007 New J. Phys. 9 250 (arXiv:quant-ph/0702212). [2] Mitosek and Backens, 2024 (arXiv:2410.23439). """ - if not _check_correction_function_domain(self.og, self.correction_function): - raise FlowError(FlowErrorReason.CorrectionFunctionDomain) - - if not _check_correction_function_image(self.og, self.correction_function): - raise FlowError(FlowErrorReason.CorrectionFunctionImage) + _check_flow_general_properties(self) o_set = set(self.og.output_nodes) oc_set = set(self.og.graph.nodes - o_set) - first_layer = self.partial_order_layers[0] - if first_layer != o_set or not first_layer: - raise FlowError(FlowErrorReason.PartialOrderFirstLayer, layer=first_layer) - past_and_present_nodes: set[int] = set() past_and_present_nodes_y_meas: set[int] = set() + layer_idx = len(self.partial_order_layers) - 1 + past_and_present_nodes: set[int] = set() for layer in reversed(self.partial_order_layers[1:]): if not oc_set.issuperset(layer) or not layer: - raise FlowError(FlowErrorReason.PartialOrderNthLayer) + raise FlowError(FlowErrorReason.PartialOrderNthLayer, layer_index=layer_idx, layer=layer) past_and_present_nodes.update(layer) + past_and_present_nodes_y_meas.update(node for node in layer if self.og.measurements[node].to_plane_or_axis() == Axis.Y) for node in layer: correction_set = set(self.correction_function[node]) @@ -444,8 +439,7 @@ def check_well_formed(self) -> None: closed_odd_neighbors = (odd_neighbors | correction_set) - (odd_neighbors & correction_set) - # This check must be done before adding the node to `past_and_present_nodes_y_meas` - if past_and_present_nodes_y_meas & closed_odd_neighbors: + if (past_and_present_nodes_y_meas - {node}) & closed_odd_neighbors: raise FlowError(FlowErrorReason.P3, node=node, correction_set=correction_set) if meas == Plane.XY: @@ -464,12 +458,13 @@ def check_well_formed(self) -> None: if node not in correction_set: raise FlowError(FlowErrorReason.P8, node=node, correction_set=correction_set) elif meas == Axis.Y: - past_and_present_nodes_y_meas.add(node) if node not in closed_odd_neighbors: raise FlowError(FlowErrorReason.P9, node=node, correction_set=correction_set) else: assert_never(meas) + layer_idx -= 1 + if {*o_set, *past_and_present_nodes} != set(self.og.graph.nodes): raise FlowError(FlowErrorReason.PartialOrderNodes) @@ -574,23 +569,16 @@ def check_well_formed(self) -> None: ---------- [1] Backens et al., Quantum 5, 421 (2021), doi.org/10.22331/q-2021-03-25-421 """ - if not _check_correction_function_domain(self.og, self.correction_function): - raise FlowError(FlowErrorReason.CorrectionFunctionDomain) - - if not _check_correction_function_image(self.og, self.correction_function): - raise FlowError(FlowErrorReason.CorrectionFunctionImage) + _check_flow_general_properties(self) o_set = set(self.og.output_nodes) oc_set = set(self.og.graph.nodes - o_set) - first_layer = self.partial_order_layers[0] - if first_layer != o_set or not first_layer: - raise FlowError(FlowErrorReason.PartialOrderFirstLayer, layer=first_layer) - + layer_idx = len(self.partial_order_layers) - 1 past_and_present_nodes: set[int] = set() for layer in reversed(self.partial_order_layers[1:]): if not oc_set.issuperset(layer) or not layer: - raise FlowError(FlowErrorReason.PartialOrderNthLayer) + raise FlowError(FlowErrorReason.PartialOrderNthLayer, layer_index=layer_idx, layer=layer) past_and_present_nodes.update(layer) @@ -619,6 +607,8 @@ def check_well_formed(self) -> None: else: assert_never(plane) + layer_idx -= 1 + if {*o_set, *past_and_present_nodes} != set(self.og.graph.nodes): raise FlowError(FlowErrorReason.PartialOrderNodes) @@ -676,7 +666,7 @@ def check_well_formed(self) -> None: Raises ------ FlowError - if the Pauli flow is not well formed. + if the causal flow is not well formed. Notes ----- @@ -699,23 +689,16 @@ def check_well_formed(self) -> None: ---------- [1] Browne et al., 2007 New J. Phys. 9 250 (arXiv:quant-ph/0702212). """ - if not _check_correction_function_domain(self.og, self.correction_function): - raise FlowError(FlowErrorReason.CorrectionFunctionDomain) - - if not _check_correction_function_image(self.og, self.correction_function): - raise FlowError(FlowErrorReason.CorrectionFunctionImage) + _check_flow_general_properties(self) o_set = set(self.og.output_nodes) oc_set = set(self.og.graph.nodes - o_set) - first_layer = self.partial_order_layers[0] - if first_layer != o_set or not first_layer: - raise FlowError(FlowErrorReason.PartialOrderFirstLayer, layer=first_layer) - + layer_idx = len(self.partial_order_layers) - 1 past_and_present_nodes: set[int] = set() for layer in reversed(self.partial_order_layers[1:]): if not oc_set.issuperset(layer) or not layer: - raise FlowError(FlowErrorReason.PartialOrderNthLayer) + raise FlowError(FlowErrorReason.PartialOrderNthLayer, layer_index=layer_idx, layer=layer) past_and_present_nodes.update(layer) @@ -736,6 +719,8 @@ def check_well_formed(self) -> None: if (neighbors - {node}) & past_and_present_nodes: raise FlowError(FlowErrorReason.C3, node=node, correction_set=correction_set) + layer_idx -= 1 + if {*o_set, *past_and_present_nodes} != set(self.og.graph.nodes): raise FlowError(FlowErrorReason.PartialOrderNodes) @@ -808,14 +793,39 @@ def _check_correction_function_image(og: OpenGraph[_M_co], correction_function: return image.issubset(ic_set) -# def _check_partial_order_layers(og: OpenGraph[_M_co], partial_order_layers: Sequence[AbstractSet[int]]) -> bool: -# """Verify that the partial order contains all the nodes of the open graph and that there are not empty layers.""" -# nodes: set[int] = set() -# for layer in partial_order_layers: -# if not layer: -# return False -# nodes.update(layer) -# return nodes == set(og.graph.nodes) +def _check_flow_general_properties(flow: PauliFlow[_M_co]) -> None: + """Verify the general properties of a flow. + + Parameters + ---------- + flow : PauliFlow[_M_co] + + Raises + ------ + FlowError + If the causal flow is not well formed. + + Notes + ----- + General properties of flows: + - The domain of the correction function is :math:`O^c`, the non-output nodes of the open graph. + - The image of the correction function is a subset of :math:`I^c`, the non-input nodes of the open graph. + - The nodes in the partial order are the nodes in the open graph. + - The first layer of the partial order layers is :math:`O`, the output nodes of the open graph. This is guaranteed because open graphs without outputs do not have flow. + """ + if not _check_correction_function_domain(flow.og, flow.correction_function): + raise FlowError(FlowErrorReason.CorrectionFunctionDomain) + + if not _check_correction_function_image(flow.og, flow.correction_function): + raise FlowError(FlowErrorReason.CorrectionFunctionImage) + + if len(flow.partial_order_layers) == 0: + raise FlowError(FlowErrorReason.PartialOrderEmpty) + + first_layer = flow.partial_order_layers[0] + o_set = set(flow.og.output_nodes) + if first_layer != o_set or not first_layer: + raise FlowError(FlowErrorReason.PartialOrderFirstLayer, layer=first_layer) class FlowErrorReason(Enum): @@ -827,8 +837,11 @@ class FlowErrorReason(Enum): CorrectionFunctionImage = enum.auto() """The image of the correction function is not a subset of non-input nodes (prepared qubits) of the open graph.""" + PartialOrderEmpty = enum.auto() + """The partial order is empty.""" + PartialOrderFirstLayer = enum.auto() - """The first layer of the partial order is not the set of output nodes (non-measured qubits) of the open graph or is be empty.""" # A well-defined flow cannot exist on an open graph without outputs. + """The first layer of the partial order is not the set of output nodes (non-measured qubits) of the open graph or is empty.""" # A well-defined flow cannot exist on an open graph without outputs. PartialOrderNthLayer = enum.auto() """Nodes in the partial order beyond the first layer are not non-output nodes (measured qubits) of the open graph or layer is empty.""" @@ -909,6 +922,9 @@ def __str__(self) -> str: if self.reason == FlowErrorReason.CorrectionFunctionImage: return "The image of the correction function must be a subset of non-input nodes (prepared qubits) of the open graph." + if self.reason == FlowErrorReason.PartialOrderEmpty: + return "The partial order cannot be empty." + if self.reason == FlowErrorReason.PartialOrderFirstLayer: return f"The first layer of the partial order must contain all the output nodes of the open graph and cannot be empty. First layer: {self.layer}" @@ -943,7 +959,7 @@ def __str__(self) -> str: return f"{self.reason.name}: nodes measured on plane YZ must belong to their own correcting set and cannot be in the odd neighbourhood of their own correcting set. Error found at c({self.node}) = {self.correction_set}." if self.reason == FlowErrorReason.P1: - return f"{self.reason.name}: nodes must be in the past of their correcting nodes that are not measured along the X or the Y axes. Error found at c({self.node}) = {self.correction_set}." + return f"{self.reason.name}: nodes must be in the past of their correcting nodes unless these are measured along the X or the Y axes. Error found at c({self.node}) = {self.correction_set}." if self.reason == FlowErrorReason.P2: return f"{self.reason.name}: the odd neighbourhood (except the corrected node and nodes measured along axes Y or Z) of the correcting nodes must be in the future of the corrected node. Error found at c({self.node}) = {self.correction_set}." diff --git a/tests/test_flow_core.py b/tests/test_flow_core.py index 5237e0723..a1b4befd4 100644 --- a/tests/test_flow_core.py +++ b/tests/test_flow_core.py @@ -7,7 +7,14 @@ import pytest from graphix.command import E, M, N, X, Z -from graphix.flow.core import CausalFlow, GFlow, PauliFlow, XZCorrections +from graphix.flow.core import ( + CausalFlow, + FlowError, + FlowErrorReason, + GFlow, + PauliFlow, + XZCorrections, +) from graphix.fundamentals import AbstractMeasurement, AbstractPlanarMeasurement, Axis, Plane from graphix.measurements import Measurement from graphix.opengraph import OpenGraph @@ -81,11 +88,11 @@ def generate_gflow_0() -> GFlow[Measurement]: GFlow: g(0) = {2, 5}, g(1) = {3, 4}, g(2) = {4}, g(3) = {5} - {4, 5} > {0, 1, 2, 3} + {4, 5} > {2, 3} > {0, 1} Notes ----- - This is the same open graph as in `:func: generate_causal_flow_1` but now we consider a gflow which has lower depth than the causal flow. + This is the same open graph as in `:func: generate_causal_flow_1` but now we consider a gflow. """ og = OpenGraph( graph=nx.Graph([(0, 2), (2, 3), (1, 3), (2, 4), (3, 5)]), @@ -96,7 +103,7 @@ def generate_gflow_0() -> GFlow[Measurement]: return GFlow( og=og, correction_function={0: {2, 5}, 1: {3, 4}, 2: {4}, 3: {5}}, - partial_order_layers=[{4, 5}, {0, 1, 2, 3}], + partial_order_layers=[{4, 5}, {2, 3}, {0, 1}], ) @@ -152,7 +159,7 @@ def generate_gflow_2() -> GFlow[Plane]: return GFlow( og=og, correction_function={0: {4, 5}, 1: {3, 4, 5}, 2: {3, 4}}, - partial_order_layers=[{3, 4}, {1}, {0, 2}], + partial_order_layers=[{3, 4, 5}, {1}, {0, 2}], ) @@ -369,6 +376,7 @@ class TestFlowPatternConversion: @pytest.mark.parametrize("test_case", prepare_test_xzcorrections()) def test_flow_to_corrections(self, test_case: XZCorrectionsTestCase) -> None: flow = test_case.flow + flow.check_well_formed() corrections = flow.to_corrections() assert corrections.z_corrections == test_case.z_corr assert corrections.x_corrections == test_case.x_corr @@ -378,7 +386,6 @@ def test_corrections_to_pattern(self, test_case: XZCorrectionsTestCase, fx_rng: if test_case.pattern is not None: pattern = test_case.flow.to_corrections().to_pattern() # type: ignore[misc] n_shots = 2 - results = [] for plane in {Plane.XY, Plane.XZ, Plane.YZ}: alpha = 2 * np.pi * fx_rng.random() @@ -386,11 +393,8 @@ def test_corrections_to_pattern(self, test_case: XZCorrectionsTestCase, fx_rng: for _ in range(n_shots): state = pattern.simulate_pattern(input_state=PlanarState(plane, alpha)) - results.append(np.abs(np.dot(state.flatten().conjugate(), state_ref.flatten()))) - - avg = sum(results) / (n_shots * 3) - - assert avg == pytest.approx(1) + result = np.abs(np.dot(state.flatten().conjugate(), state_ref.flatten())) + assert result == pytest.approx(1) class TestXZCorrections: @@ -585,6 +589,11 @@ def test_from_measured_nodes_mapping_exceptions(self) -> None: XZCorrections.from_measured_nodes_mapping(og=og, x_corrections={0: {4}}) +class IncorrectFlowTestCase(NamedTuple): + flow: PauliFlow[AbstractMeasurement] + exception: FlowError + + class TestIncorrectFlows: """Bundle for unit tests of :func:`PauliFlow.is_well_formed` (and children) on incorrect flows. Correct flows are extensively tested in `tests.test_opengraph.py`.""" @@ -601,113 +610,262 @@ class TestIncorrectFlows: measurements={0: Plane.XY, 1: Plane.YZ, 2: Plane.XZ}, ) og_p = OpenGraph( - graph=nx.Graph([(0, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5), (4, 6), (5, 7)]), - input_nodes=[0, 1], - output_nodes=[6, 7], - measurements={ - 0: Measurement(0.1, Plane.XY), # XY - 1: Measurement(0.1, Plane.XY), # XY - 2: Measurement(0.0, Plane.XY), # X - 3: Measurement(0.1, Plane.XY), # XY - 4: Measurement(0.0, Plane.XY), # X - 5: Measurement(0.5, Plane.XY), # Y - }, + graph=nx.Graph([(0, 1), (1, 2), (2, 3)]), + input_nodes=[0], + output_nodes=[3], + measurements={0: Plane.XY, 1: Axis.X, 2: Plane.XY}, ) @pytest.mark.parametrize( "test_case", [ # Incomplete correction function - CausalFlow( - og=og_c, - correction_function={0: {1}, 1: {2}}, - partial_order_layers=[{3}, {2}, {1}, {0}], - ), - # Incomplete partial order - CausalFlow( - og=og_c, - correction_function={0: {1}, 1: {2}}, - partial_order_layers=[{2}, {1}, {0}], - ), - # Wrong correction function - CausalFlow( - og=og_c, - correction_function={0: {0}, 1: {2}}, - partial_order_layers=[{3}, {2}, {1}, {0}], - ), - # Wrong correction function - CausalFlow( - og=og_c, - correction_function={0: {0, 1}, 1: {2}}, - partial_order_layers=[{3}, {2}, {1}, {0}], - ), - # Wrong correction function - CausalFlow( - og=og_c, - correction_function={0: {0}, 1: set()}, - partial_order_layers=[{3}, {2}, {1}, {0}], - ), - # Wrong partial order - CausalFlow( - og=og_c, - correction_function={0: {0}, 1: {2}}, - partial_order_layers=[{3}, {1, 2}, {0}], - ), - # Wrong correction function - GFlow( - og=og_g, - correction_function={0: {0, 3}, 1: {1}, 2: {2, 3, 4}}, - partial_order_layers=[{3, 4}, {1}, {0, 2}], - ), - # Wrong correction function - GFlow( - og=og_g, - correction_function={0: {3}, 1: {1}, 2: {3, 4}}, - partial_order_layers=[{3, 4}, {1}, {0, 2}], - ), - # Wrong correction function - GFlow( - og=og_g, - correction_function={0: {3}, 1: {1, 4}, 2: {2, 3, 4}}, - partial_order_layers=[{3, 4}, {1}, {0, 2}], - ), - # Partial order with duplicates - GFlow( - og=og_g, - correction_function={0: {3}, 1: {1}, 2: {3, 4}}, - partial_order_layers=[{3, 4}, {1, 0}, {0, 2}], - ), - # Wrong partial order duplicates - GFlow( - og=og_g, - correction_function={0: {3}, 1: {1}, 2: {3, 4}}, - partial_order_layers=[{3, 4}, {1, 0, 2}], - ), - # Nodes in partial order not in open graph - PauliFlow( - og=og_p, - correction_function={0: {2, 5, 7}, 1: {3, 4}, 2: {4, 7}, 3: {5, 6, 7}, 4: {6}, 5: {7}}, - partial_order_layers=[{6, 7}, {3, 100}, {0, 1, 2, 4, 5}], - ), - # Inputs in co-domain of correction function - PauliFlow( - og=og_p, - correction_function={0: {0, 2, 5, 7}, 1: {3, 4}, 2: {4, 7}, 3: {5, 6, 7}, 4: {6}, 5: {7}}, - partial_order_layers=[{6, 7}, {3}, {0, 1, 2, 4, 5}], - ), - # Wrong correction function - PauliFlow( - og=og_p, - correction_function={0: {2, 5, 7}, 1: {3, 4}, 2: {3, 4, 7}, 3: {5, 6, 7}, 4: {6}, 5: {7}}, - partial_order_layers=[{6, 7}, {3}, {0, 1, 2, 4, 5}], - ), - # Wrong partial order - PauliFlow( - og=og_p, - correction_function={0: {2, 5, 7}, 1: {3, 4}, 2: {4, 7}, 3: {5, 6, 7}, 4: {6}, 5: {7}}, - partial_order_layers=[{6, 7}, {3, 0, 1, 2, 4, 5}], + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}}, + partial_order_layers=[{3}, {2}, {1}, {0}], + ), + FlowError(FlowErrorReason.CorrectionFunctionDomain), + ), + # Extra node in correction function image + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}, 2: {4}}, + partial_order_layers=[{3}, {2}, {1}, {0}], + ), + FlowError(FlowErrorReason.CorrectionFunctionImage), + ), + # Empty partial order + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}, 2: {3}}, + partial_order_layers=[], + ), + FlowError(FlowErrorReason.PartialOrderEmpty), + ), + # Incomplete partial order (first layer) + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}, 2: {3}}, + partial_order_layers=[{2}, {1}, {0}], + ), + FlowError(FlowErrorReason.PartialOrderFirstLayer, layer={2}), + ), + # Empty layer + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}, 2: {3}}, + partial_order_layers=[{3}, {2}, {1}, set(), {0}], + ), + FlowError(FlowErrorReason.PartialOrderNthLayer, layer_index=3, layer=set()), + ), + # Output node in nth layer + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}, 2: {3}}, + partial_order_layers=[{3}, {2}, {3}, {1}, {0}], + ), + FlowError(FlowErrorReason.PartialOrderNthLayer, layer_index=2, layer={3}), + ), + # Incomplete partial order (nth layer) + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}, 2: {3}}, + partial_order_layers=[{3}, {2}, {1}], + ), + FlowError(FlowErrorReason.PartialOrderNodes), + ), + # Correction function with more than one node in domain + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2, 3}, 2: {3}}, + partial_order_layers=[{3}, {2}, {1}, {0}], + ), + FlowError(FlowErrorReason.CorrectionSetCausalFlow, node=1, correction_set={2, 3}), + ), + # C1 + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {2}, 2: {1}, 1: {3}}, + partial_order_layers=[{3}, {1}, {2}, {0}], + ), + FlowError(FlowErrorReason.C1, node=0, correction_set={2}), + ), + # C2 + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}, 2: {3}}, + partial_order_layers=[{3}, {2}, {0, 1}], + ), + FlowError(FlowErrorReason.C2, node=0, correction_set={1}), + ), + # C3 + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}, 2: {3}}, + partial_order_layers=[{3}, {1}, {0, 2}], + ), + FlowError(FlowErrorReason.C3, node=0, correction_set={1}), + ), + # G1 + IncorrectFlowTestCase( + GFlow( + og=og_g, + correction_function={0: {3}, 1: {1, 2}, 2: {2, 3, 4}}, + partial_order_layers=[{3, 4}, {1}, {0, 2}], + ), + FlowError(FlowErrorReason.G1, node=1, correction_set={1, 2}), + ), + # G2 + IncorrectFlowTestCase( + GFlow( + og=og_g, + correction_function={0: {3}, 1: {1}, 2: {2, 3, 4}}, + partial_order_layers=[{3, 4}, {1, 0, 2}], + ), + FlowError(FlowErrorReason.G2, node=2, correction_set={2, 3, 4}), + ), + # G3 + IncorrectFlowTestCase( + GFlow( + og=og_g, + correction_function={0: {3, 4}, 1: {1}, 2: {2, 3, 4}}, + partial_order_layers=[{3, 4}, {1}, {2}, {0}], + ), + FlowError(FlowErrorReason.G3, node=0, correction_set={3, 4}), + ), + # P4 (same as G3 but for Pauli flow) + IncorrectFlowTestCase( + PauliFlow( + og=og_g, + correction_function={0: {3, 4}, 1: {1}, 2: {2, 3, 4}}, + partial_order_layers=[{3, 4}, {1}, {2}, {0}], + ), + FlowError(FlowErrorReason.P4, node=0, correction_set={3, 4}), + ), + # G4 + IncorrectFlowTestCase( + GFlow( + og=og_g, + correction_function={0: {3}, 1: {1}, 2: {3, 4}}, + partial_order_layers=[{3, 4}, {1}, {2}, {0}], + ), + FlowError(FlowErrorReason.G4, node=2, correction_set={3, 4}), + ), + # P5 (same as G4 but for Pauli flow) + IncorrectFlowTestCase( + PauliFlow( + og=og_g, + correction_function={0: {3}, 1: {1}, 2: {3, 4}}, + partial_order_layers=[{3, 4}, {1}, {2}, {0}], + ), + FlowError(FlowErrorReason.P5, node=2, correction_set={3, 4}), + ), + # G5 + IncorrectFlowTestCase( + GFlow( + og=og_g, + correction_function={0: {3}, 1: set(), 2: {2, 3, 4}}, + partial_order_layers=[{3, 4}, {1}, {2}, {0}], + ), + FlowError(FlowErrorReason.G5, node=1, correction_set=set()), + ), + # P6 (same as G5 but for Pauli flow) + IncorrectFlowTestCase( + PauliFlow( + og=og_g, + correction_function={0: {3}, 1: set(), 2: {2, 3, 4}}, + partial_order_layers=[{3, 4}, {1}, {2}, {0}], + ), + FlowError(FlowErrorReason.P6, node=1, correction_set=set()), + ), + # P1 + IncorrectFlowTestCase( + PauliFlow( + og=og_p, + correction_function={0: {1, 3}, 1: {2}, 2: {3}}, + partial_order_layers=[{3}, {2, 0, 1}], + ), + FlowError(FlowErrorReason.P1, node=1, correction_set={2}), + ), + # P2 + IncorrectFlowTestCase( + PauliFlow( + og=og_p, + correction_function={0: {1, 3}, 1: {3}, 2: {3}}, + partial_order_layers=[{3}, {2, 0, 1}], + ), + FlowError(FlowErrorReason.P2, node=1, correction_set={3}), + ), + # P3 + IncorrectFlowTestCase( + PauliFlow( + og=OpenGraph( + graph=nx.Graph([(0, 1), (1, 2)]), + input_nodes=[0], + output_nodes=[2], + measurements=dict.fromkeys(range(2), Measurement(0.5, Plane.XY)), + ), + correction_function={0: {1}, 1: {2}}, + partial_order_layers=[{2}, {0, 1}], + ), + FlowError(FlowErrorReason.P3, node=0, correction_set={1}), + ), + # P7 + IncorrectFlowTestCase( + PauliFlow( + og=og_p, + correction_function={0: {1, 3}, 1: {3}, 2: {3}}, + partial_order_layers=[{3}, {2}, {0, 1}], + ), + FlowError(FlowErrorReason.P7, node=1, correction_set={3}), + ), + # P8 + IncorrectFlowTestCase( + PauliFlow( + og=OpenGraph( + graph=nx.Graph([(0, 1)]), + input_nodes=[0], + output_nodes=[1], + measurements={0: Measurement(0, Plane.XZ)}, + ), + correction_function={0: {1}}, + partial_order_layers=[{1}, {0}], + ), + FlowError(FlowErrorReason.P8, node=0, correction_set={1}), + ), + # P9 + IncorrectFlowTestCase( + PauliFlow( + og=OpenGraph( + graph=nx.Graph([(0, 1), (1, 2), (2, 3)]), + input_nodes=[0], + output_nodes=[3], + measurements={0: Plane.XY, 1: Axis.Y, 2: Plane.XY}, + ), + correction_function={0: {2, 3}, 1: {1, 2}, 2: {3}}, + partial_order_layers=[{3}, {2}, {0}, {1}], + ), + FlowError(FlowErrorReason.P9, node=1, correction_set={1, 2}), ), ], ) - def test_flow_is_well_formed(self, test_case: PauliFlow[Plane | Axis]) -> None: - assert not test_case.is_well_formed() + def test_check_flow_general_properties(self, test_case: IncorrectFlowTestCase) -> None: + with pytest.raises(FlowError) as exc_info: + test_case.flow.check_well_formed() + assert exc_info.value.reason == test_case.exception.reason + assert exc_info.value.node == test_case.exception.node + assert exc_info.value.correction_set == test_case.exception.correction_set + assert exc_info.value.layer_index == test_case.exception.layer_index + assert exc_info.value.layer == test_case.exception.layer diff --git a/tests/test_opengraph.py b/tests/test_opengraph.py index bb0955a52..32aba1b22 100644 --- a/tests/test_opengraph.py +++ b/tests/test_opengraph.py @@ -554,7 +554,7 @@ def check_determinism(pattern: Pattern, fx_rng: Generator, n_shots: int = 3) -> state = pattern.simulate_pattern(input_state=PlanarState(plane, alpha)) result = np.abs(np.dot(state.flatten().conjugate(), state_ref.flatten())) - if result: + if result == pytest.approx(1): continue return False @@ -585,7 +585,9 @@ def test_cflow(self, test_case: OpenGraphFlowTestCase, fx_rng: Generator) -> Non og = test_case.og if test_case.has_cflow: - pattern = og.extract_causal_flow().to_corrections().to_pattern() + cf = og.extract_causal_flow() + cf.check_well_formed() + pattern = cf.to_corrections().to_pattern() assert check_determinism(pattern, fx_rng) else: with pytest.raises(OpenGraphError, match=r"The open graph does not have a causal flow."): @@ -596,7 +598,9 @@ def test_gflow(self, test_case: OpenGraphFlowTestCase, fx_rng: Generator) -> Non og = test_case.og if test_case.has_gflow: - pattern = og.extract_gflow().to_corrections().to_pattern() + gf = og.extract_gflow() + gf.check_well_formed() + pattern = gf.to_corrections().to_pattern() assert check_determinism(pattern, fx_rng) else: with pytest.raises(OpenGraphError, match=r"The open graph does not have a gflow."): @@ -607,7 +611,9 @@ def test_pflow(self, test_case: OpenGraphFlowTestCase, fx_rng: Generator) -> Non og = test_case.og if test_case.has_pflow: - pattern = og.extract_pauli_flow().to_corrections().to_pattern() + pf = og.extract_pauli_flow() + pf.check_well_formed() + pattern = pf.to_corrections().to_pattern() assert check_determinism(pattern, fx_rng) else: with pytest.raises(OpenGraphError, match=r"The open graph does not have a Pauli flow."): From 44c46f0cc512a416f587754ea9dc44a11c551cac Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 27 Nov 2025 16:03:12 +0100 Subject: [PATCH 03/58] Refactor flow exceptions --- graphix/flow/core.py | 348 +++++++++++++++++++++++++++------------- tests/test_flow_core.py | 109 +++++++++---- 2 files changed, 312 insertions(+), 145 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index 081e64c0b..ccb08e009 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -7,7 +7,7 @@ from copy import copy from dataclasses import dataclass from enum import Enum -from typing import TYPE_CHECKING, Generic +from typing import TYPE_CHECKING, Generic, TypeVar import networkx as nx @@ -415,13 +415,14 @@ def check_well_formed(self) -> None: past_and_present_nodes_y_meas: set[int] = set() layer_idx = len(self.partial_order_layers) - 1 - past_and_present_nodes: set[int] = set() for layer in reversed(self.partial_order_layers[1:]): if not oc_set.issuperset(layer) or not layer: - raise FlowError(FlowErrorReason.PartialOrderNthLayer, layer_index=layer_idx, layer=layer) + raise PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=layer_idx, layer=layer) past_and_present_nodes.update(layer) - past_and_present_nodes_y_meas.update(node for node in layer if self.og.measurements[node].to_plane_or_axis() == Axis.Y) + past_and_present_nodes_y_meas.update( + node for node in layer if self.og.measurements[node].to_plane_or_axis() == Axis.Y + ) for node in layer: correction_set = set(self.correction_function[node]) @@ -429,44 +430,71 @@ def check_well_formed(self) -> None: for i in (correction_set - {node}) & past_and_present_nodes: if self.og.measurements[i].to_plane_or_axis() not in {Axis.X, Axis.Y}: - raise FlowError(FlowErrorReason.P1, node=node, correction_set=correction_set) + raise FlowPropositionOrderError( + FlowPropositionOrderErrorReason.P1, + node=node, + correction_set=correction_set, + past_and_present_nodes=past_and_present_nodes, + ) odd_neighbors = self.og.odd_neighbors(correction_set) for i in (odd_neighbors - {node}) & past_and_present_nodes: if self.og.measurements[i].to_plane_or_axis() not in {Axis.Y, Axis.Z}: - raise FlowError(FlowErrorReason.P2, node=node, correction_set=correction_set) + raise FlowPropositionOrderError( + FlowPropositionOrderErrorReason.P2, + node=node, + correction_set=correction_set, + past_and_present_nodes=past_and_present_nodes, + ) closed_odd_neighbors = (odd_neighbors | correction_set) - (odd_neighbors & correction_set) if (past_and_present_nodes_y_meas - {node}) & closed_odd_neighbors: - raise FlowError(FlowErrorReason.P3, node=node, correction_set=correction_set) + raise FlowPropositionOrderError( + FlowPropositionOrderErrorReason.P3, + node=node, + correction_set=correction_set, + past_and_present_nodes=past_and_present_nodes_y_meas, + ) if meas == Plane.XY: if not (node not in correction_set and node in odd_neighbors): - raise FlowError(FlowErrorReason.P4, node=node, correction_set=correction_set) + raise FlowPropositionError( + FlowPropositionErrorReason.P4, node=node, correction_set=correction_set + ) elif meas == Plane.XZ: if not (node in correction_set and node in odd_neighbors): - raise FlowError(FlowErrorReason.P5, node=node, correction_set=correction_set) + raise FlowPropositionError( + FlowPropositionErrorReason.P5, node=node, correction_set=correction_set + ) elif meas == Plane.YZ: if not (node in correction_set and node not in odd_neighbors): - raise FlowError(FlowErrorReason.P6, node=node, correction_set=correction_set) + raise FlowPropositionError( + FlowPropositionErrorReason.P6, node=node, correction_set=correction_set + ) elif meas == Axis.X: if node not in odd_neighbors: - raise FlowError(FlowErrorReason.P7, node=node, correction_set=correction_set) + raise FlowPropositionError( + FlowPropositionErrorReason.P7, node=node, correction_set=correction_set + ) elif meas == Axis.Z: if node not in correction_set: - raise FlowError(FlowErrorReason.P8, node=node, correction_set=correction_set) + raise FlowPropositionError( + FlowPropositionErrorReason.P8, node=node, correction_set=correction_set + ) elif meas == Axis.Y: if node not in closed_odd_neighbors: - raise FlowError(FlowErrorReason.P9, node=node, correction_set=correction_set) + raise FlowPropositionError( + FlowPropositionErrorReason.P9, node=node, correction_set=correction_set + ) else: assert_never(meas) layer_idx -= 1 if {*o_set, *past_and_present_nodes} != set(self.og.graph.nodes): - raise FlowError(FlowErrorReason.PartialOrderNodes) + raise PartialOrderError(PartialOrderErrorReason.IncorrectNodes) @dataclass(frozen=True) @@ -578,7 +606,7 @@ def check_well_formed(self) -> None: past_and_present_nodes: set[int] = set() for layer in reversed(self.partial_order_layers[1:]): if not oc_set.issuperset(layer) or not layer: - raise FlowError(FlowErrorReason.PartialOrderNthLayer, layer_index=layer_idx, layer=layer) + raise PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=layer_idx, layer=layer) past_and_present_nodes.update(layer) @@ -586,31 +614,47 @@ def check_well_formed(self) -> None: correction_set = set(self.correction_function[node]) if (correction_set - {node}) & past_and_present_nodes: - raise FlowError(FlowErrorReason.G1, node=node, correction_set=correction_set) + raise FlowPropositionOrderError( + FlowPropositionOrderErrorReason.G1, + node=node, + correction_set=correction_set, + past_and_present_nodes=past_and_present_nodes, + ) odd_neighbors = self.og.odd_neighbors(correction_set) if (odd_neighbors - {node}) & past_and_present_nodes: - raise FlowError(FlowErrorReason.G2, node=node, correction_set=correction_set) + raise FlowPropositionOrderError( + FlowPropositionOrderErrorReason.G2, + node=node, + correction_set=correction_set, + past_and_present_nodes=past_and_present_nodes, + ) plane = self.og.measurements[node].to_plane() if plane == Plane.XY: if not (node not in correction_set and node in odd_neighbors): - raise FlowError(FlowErrorReason.G3, node=node, correction_set=correction_set) + raise FlowPropositionError( + FlowPropositionErrorReason.G3, node=node, correction_set=correction_set + ) elif plane == Plane.XZ: if not (node in correction_set and node in odd_neighbors): - raise FlowError(FlowErrorReason.G4, node=node, correction_set=correction_set) + raise FlowPropositionError( + FlowPropositionErrorReason.G4, node=node, correction_set=correction_set + ) elif plane == Plane.YZ: if not (node in correction_set and node not in odd_neighbors): - raise FlowError(FlowErrorReason.G5, node=node, correction_set=correction_set) + raise FlowPropositionError( + FlowPropositionErrorReason.G5, node=node, correction_set=correction_set + ) else: assert_never(plane) layer_idx -= 1 if {*o_set, *past_and_present_nodes} != set(self.og.graph.nodes): - raise FlowError(FlowErrorReason.PartialOrderNodes) + raise PartialOrderError(PartialOrderErrorReason.IncorrectNodes) @dataclass(frozen=True) @@ -677,7 +721,7 @@ def check_well_formed(self) -> None: - The first layer of the partial order layers is :math:`O`, the output nodes of the open graph. This is guaranteed because open graphs without outputs do not have flow. Specific properties of causal flows: - - Correction sets have one element only, + - Correction sets have one element only (C0), - :math:`i \sim c(i)` (C1), - :math:`i \prec c(i)` (C2), - :math:`\forall k \in N_G(c(i)) \setminus \{i\}, i \prec k` (C3), @@ -698,7 +742,7 @@ def check_well_formed(self) -> None: past_and_present_nodes: set[int] = set() for layer in reversed(self.partial_order_layers[1:]): if not oc_set.issuperset(layer) or not layer: - raise FlowError(FlowErrorReason.PartialOrderNthLayer, layer_index=layer_idx, layer=layer) + raise PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=layer_idx, layer=layer) past_and_present_nodes.update(layer) @@ -706,23 +750,34 @@ def check_well_formed(self) -> None: correction_set = set(self.correction_function[node]) if len(correction_set) != 1: - raise FlowError(FlowErrorReason.CorrectionSetCausalFlow, node=node, correction_set=correction_set) + raise FlowPropositionError(FlowPropositionErrorReason.C0, node=node, correction_set=correction_set) neighbors = self.og.neighbors(correction_set) if node not in neighbors: - raise FlowError(FlowErrorReason.C1, node=node, correction_set=correction_set) + raise FlowPropositionError(FlowPropositionErrorReason.C1, node=node, correction_set=correction_set) + # If some nodes of the correction set are in the past or in the present of the current node, they cannot be in its future, so the flow is incorrrect. if correction_set & past_and_present_nodes: - raise FlowError(FlowErrorReason.C2, node=node, correction_set=correction_set) + raise FlowPropositionOrderError( + FlowPropositionOrderErrorReason.C2, + node=node, + correction_set=correction_set, + past_and_present_nodes=past_and_present_nodes, + ) if (neighbors - {node}) & past_and_present_nodes: - raise FlowError(FlowErrorReason.C3, node=node, correction_set=correction_set) + raise FlowPropositionOrderError( + FlowPropositionOrderErrorReason.C3, + node=node, + correction_set=correction_set, + past_and_present_nodes=past_and_present_nodes, + ) layer_idx -= 1 if {*o_set, *past_and_present_nodes} != set(self.og.graph.nodes): - raise FlowError(FlowErrorReason.PartialOrderNodes) + raise PartialOrderError(PartialOrderErrorReason.IncorrectNodes) def _corrections_to_dag( @@ -814,59 +869,43 @@ def _check_flow_general_properties(flow: PauliFlow[_M_co]) -> None: - The first layer of the partial order layers is :math:`O`, the output nodes of the open graph. This is guaranteed because open graphs without outputs do not have flow. """ if not _check_correction_function_domain(flow.og, flow.correction_function): - raise FlowError(FlowErrorReason.CorrectionFunctionDomain) + raise CorrectionFunctionError(CorrectionFunctionErrorReason.IncorrectDomain) if not _check_correction_function_image(flow.og, flow.correction_function): - raise FlowError(FlowErrorReason.CorrectionFunctionImage) + raise CorrectionFunctionError(CorrectionFunctionErrorReason.IncorrectImage) if len(flow.partial_order_layers) == 0: - raise FlowError(FlowErrorReason.PartialOrderEmpty) + raise PartialOrderError(PartialOrderErrorReason.Empty) first_layer = flow.partial_order_layers[0] o_set = set(flow.og.output_nodes) if first_layer != o_set or not first_layer: - raise FlowError(FlowErrorReason.PartialOrderFirstLayer, layer=first_layer) + raise PartialOrderLayerError(PartialOrderLayerErrorReason.FirstLayer, layer_index=0, layer=first_layer) -class FlowErrorReason(Enum): +class FlowErrorReason: """Describe the reason of a `FlowError`.""" - CorrectionFunctionDomain = enum.auto() - """The domain of the correction function is not the set of non-output nodes (measured qubits) of the open graph.""" - CorrectionFunctionImage = enum.auto() - """The image of the correction function is not a subset of non-input nodes (prepared qubits) of the open graph.""" +class CorrectionFunctionErrorReason(FlowErrorReason, Enum): + """Describe the reason of a `CorrectionFunctionError` exception.""" - PartialOrderEmpty = enum.auto() - """The partial order is empty.""" + IncorrectDomain = enum.auto() + """The domain of the correction function is not the set of non-output nodes (measured qubits) of the open graph.""" - PartialOrderFirstLayer = enum.auto() - """The first layer of the partial order is not the set of output nodes (non-measured qubits) of the open graph or is empty.""" # A well-defined flow cannot exist on an open graph without outputs. + IncorrectImage = enum.auto() + """The image of the correction function is not a subset of non-input nodes (prepared qubits) of the open graph.""" - PartialOrderNthLayer = enum.auto() - """Nodes in the partial order beyond the first layer are not non-output nodes (measured qubits) of the open graph or layer is empty.""" - PartialOrderNodes = enum.auto() - """The partial order does not contain all the nodes of the open graph or contains nodes that are not in the open graph.""" +class FlowPropositionErrorReason(FlowErrorReason, Enum): + """Describe the reason of a `FlowPropositionError` exception.""" - CorrectionSetCausalFlow = enum.auto() + C0 = enum.auto() """A correction set in a causal flow has more than one element.""" C1 = enum.auto() """Causal flow (C1). A node and its corrector must be neighbors.""" - C2 = enum.auto() - """Causal flow (C2). Nodes must be in the past of their correction set.""" - - C3 = enum.auto() - """Causal flow (C3). Neighbors of the correcting nodes (except the corrected node) must be in the future of the corrected node.""" - - G1 = enum.auto() - """Gflow (G1). Equivalent to (C1) but for gflows.""" - - G2 = enum.auto() - """Gflow (G2). The odd neighbourhood (except the corrected node) of the correcting nodes must be in the future of the corrected node.""" - G3 = enum.auto() """Gflow (G3). Nodes measured on plane XY cannot be in their own correcting set and must belong to the odd neighbourhood of their own correcting set.""" @@ -876,15 +915,6 @@ class FlowErrorReason(Enum): G5 = enum.auto() """Gflow (G5). Nodes measured on plane YZ must belong to their own correcting set and cannot be in the odd neighbourhood of their own correcting set.""" - P1 = enum.auto() - """Pauli flow (P1). Nodes must be in the past of their correcting nodes that are not measured along the X or the Y axes.""" - - P2 = enum.auto() - """Pauli flow (P2). The odd neighbourhood (except the corrected node and nodes measured along axes Y or Z) of the correcting nodes must be in the future of the corrected node.""" - - P3 = enum.auto() - """Pauli flow (P3). Nodes that are measured along axis Y and that are not in the future of the corrected node (except the corrected node itself) cannot be in the closed odd neighbourhood of the correcting set.""" - P4 = enum.auto() """Pauli flow (P4). Equivalent to (G3) but for Pauli flows.""" @@ -904,76 +934,174 @@ class FlowErrorReason(Enum): """Pauli flow (P9). Nodes measured along axis Y must belong to the closed odd neighbourhood of their own correcting set.""" +class FlowPropositionOrderErrorReason(FlowErrorReason, Enum): + """Describe the reason of a `FlowPropositionOrderError` exception.""" + + C2 = enum.auto() + """Causal flow (C2). Nodes must be in the past of their correction set.""" + + C3 = enum.auto() + """Causal flow (C3). Neighbors of the correcting nodes (except the corrected node) must be in the future of the corrected node.""" + + G1 = enum.auto() + """Gflow (G1). Equivalent to (C2) but for gflows.""" + + G2 = enum.auto() + """Gflow (G2). The odd neighbourhood (except the corrected node) of the correcting nodes must be in the future of the corrected node.""" + + P1 = enum.auto() + """Pauli flow (P1). Nodes must be in the past of their correcting nodes that are not measured along the X or the Y axes.""" + + P2 = enum.auto() + """Pauli flow (P2). The odd neighbourhood (except the corrected node and nodes measured along axes Y or Z) of the correcting nodes must be in the future of the corrected node.""" + + P3 = enum.auto() + """Pauli flow (P3). Nodes that are measured along axis Y and that are not in the future of the corrected node (except the corrected node itself) cannot be in the closed odd neighbourhood of the correcting set.""" + + +# NOTE: In the near future, this class may inherit from `XZCorrectionsErrorReason` too. +class PartialOrderErrorReason(FlowErrorReason, Enum): + """Describe the reason of a `PartialOrderError` exception.""" + + Empty = enum.auto() + """The partial order is empty.""" + + IncorrectNodes = enum.auto() + """The partial order does not contain all the nodes of the open graph or contains nodes that are not in the open graph.""" + + +class PartialOrderLayerErrorReason(FlowErrorReason, Enum): + """Describe the reason of a `PartialOrderLayerError` exception.""" + + FirstLayer = enum.auto() + """The first layer of the partial order is not the set of output nodes (non-measured qubits) of the open graph or is empty.""" # A well-defined flow cannot exist on an open graph without outputs. + + NthLayer = enum.auto() + """Nodes in the partial order beyond the first layer are not non-output nodes (measured qubits) of the open graph or layer is empty.""" + + +_Reason = TypeVar("_Reason", bound=FlowErrorReason) + + @dataclass -class FlowError(Exception): +class FlowError(Exception, Generic[_Reason]): """Exception subclass to handle flow errors.""" - reason: FlowErrorReason - node: int | None = None - correction_set: AbstractSet[int] | None = None - layer_index: int | None = None - layer: AbstractSet[int] | None = None + reason: _Reason + + +@dataclass +class CorrectionFunctionError(FlowError[CorrectionFunctionErrorReason]): + """Exception subclass to handle general flow errors in the correction function.""" def __str__(self) -> str: """Explain the error.""" - if self.reason == FlowErrorReason.CorrectionFunctionDomain: + if self.reason == CorrectionFunctionErrorReason.IncorrectDomain: return "The domain of the correction function must be the set of non-output nodes (measured qubits) of the open graph." - if self.reason == FlowErrorReason.CorrectionFunctionImage: + if self.reason == CorrectionFunctionErrorReason.IncorrectImage: return "The image of the correction function must be a subset of non-input nodes (prepared qubits) of the open graph." - if self.reason == FlowErrorReason.PartialOrderEmpty: - return "The partial order cannot be empty." + assert_never(self.reason) - if self.reason == FlowErrorReason.PartialOrderFirstLayer: - return f"The first layer of the partial order must contain all the output nodes of the open graph and cannot be empty. First layer: {self.layer}" - if self.reason == FlowErrorReason.PartialOrderNthLayer: - return f"Partial order layer {self.layer_index} = {self.layer} contains non-measured nodes of the open graph or is empty." +@dataclass +class FlowPropositionError(FlowError[FlowPropositionErrorReason]): + """Exception subclass to handle violations of the flow-definition propositions which concern the correction function only (C0, C1, G1, G3, G4, G5, P4, P5, P6, P7, P8, P9).""" - if self.reason == FlowErrorReason.PartialOrderNodes: - return "The partial order does not contain all the nodes of the open graph or contains nodes that are not in the open graph." + node: int + correction_set: AbstractSet[int] - if self.reason == FlowErrorReason.CorrectionSetCausalFlow: + def __str__(self) -> str: + """Explain the error.""" + error_help = f"Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowPropositionErrorReason.C0: return f"Correction set c({self.node}) = {self.correction_set} has more than one element." - if self.reason == FlowErrorReason.C1: - return f"{self.reason.name}: a node and its corrector must be neighbors. Error found at c({self.node}) = {self.correction_set}." + if self.reason == FlowPropositionErrorReason.C1: + return f"{self.reason.name}: a node and its corrector must be neighbors. {error_help}" + + if self.reason == FlowPropositionErrorReason.G3 or self.reason == FlowPropositionErrorReason.P4: # noqa: PLR1714 + return f"{self.reason.name}: nodes measured on plane XY cannot be in their own correcting set and must belong to the odd neighbourhood of their own correcting set.\n{error_help}" + + if self.reason == FlowPropositionErrorReason.G4 or self.reason == FlowPropositionErrorReason.P5: # noqa: PLR1714 + return f"{self.reason.name}: nodes measured on plane XZ must belong to their own correcting set and its odd neighbourhood.\n{error_help}" + + if self.reason == FlowPropositionErrorReason.G5 or self.reason == FlowPropositionErrorReason.P6: # noqa: PLR1714 + return f"{self.reason.name}: nodes measured on plane YZ must belong to their own correcting set and cannot be in the odd neighbourhood of their own correcting set.\n{error_help}" - if self.reason == FlowErrorReason.C2 or self.reason == FlowErrorReason.G1: # noqa: PLR1714 - return f"{self.reason.name}: nodes must be in the past of their correction set. Error found at c({self.node}) = {self.correction_set}." + if self.reason == FlowPropositionErrorReason.P7: + return f"{self.reason.name}: nodes measured along axis X must belong to the odd neighbourhood of their own correcting set.\n{error_help}" - if self.reason == FlowErrorReason.C3: - return f"{self.reason.name}: neighbors of the correcting nodes (except the corrected node) must be in the future of the corrected node. Error found at c({self.node}) = {self.correction_set}." + if self.reason == FlowPropositionErrorReason.P8: + return f"{self.reason.name}: nodes measured along axis Z must belong to their own correcting set.\n{error_help}" - if self.reason == FlowErrorReason.G2: - return f"{self.reason.name}: the odd neighbourhood (except the corrected node) of the correcting nodes must be in the future of the corrected node. Error found at c({self.node}) = {self.correction_set}." + if self.reason == FlowPropositionErrorReason.P9: + return f"{self.reason.name}: nodes measured along axis Y must belong to the closed odd neighbourhood of their own correcting set.\n{error_help}" - if self.reason == FlowErrorReason.G3 or self.reason == FlowErrorReason.P4: # noqa: PLR1714 - return f"{self.reason.name}: nodes measured on plane XY cannot be in their own correcting set and must belong to the odd neighbourhood of their own correcting set. Error found at c({self.node}) = {self.correction_set}." + assert_never(self.reason) + + +@dataclass +class FlowPropositionOrderError(FlowError[FlowPropositionOrderErrorReason]): + """Exception subclass to handle violations of the flow-definition propositions which concern the correction function and the partial order (C2, C3, G1, G2, P1, P2, P3).""" + + node: int + correction_set: AbstractSet[int] + past_and_present_nodes: AbstractSet[int] + + def __str__(self) -> str: + """Explain the error.""" + error_help = f"Error found at c({self.node}) = {self.correction_set}. Partial order: {self.past_and_present_nodes} ≼ {self.node}." + + if self.reason == FlowPropositionOrderErrorReason.C2 or self.reason == FlowPropositionOrderErrorReason.G1: # noqa: PLR1714 + return f"{self.reason.name}: nodes must be in the past of their correction set.\n{error_help}" - if self.reason == FlowErrorReason.G4 or self.reason == FlowErrorReason.P5: # noqa: PLR1714 - return f"{self.reason.name}: nodes measured on plane XZ must belong to their own correcting set and its odd neighbourhood. Error found at c({self.node}) = {self.correction_set}." + if self.reason == FlowPropositionOrderErrorReason.C3: + return f"{self.reason.name}: neighbors of the correcting nodes (except the corrected node) must be in the future of the corrected node.\n{error_help}" - if self.reason == FlowErrorReason.G5 or self.reason == FlowErrorReason.P6: # noqa: PLR1714 - return f"{self.reason.name}: nodes measured on plane YZ must belong to their own correcting set and cannot be in the odd neighbourhood of their own correcting set. Error found at c({self.node}) = {self.correction_set}." + if self.reason == FlowPropositionOrderErrorReason.G2: + return f"{self.reason.name}: the odd neighbourhood (except the corrected node) of the correcting nodes must be in the future of the corrected node.\n{error_help}" - if self.reason == FlowErrorReason.P1: - return f"{self.reason.name}: nodes must be in the past of their correcting nodes unless these are measured along the X or the Y axes. Error found at c({self.node}) = {self.correction_set}." + if self.reason == FlowPropositionOrderErrorReason.P1: + return f"{self.reason.name}: nodes must be in the past of their correcting nodes unless these are measured along the X or the Y axes.\n{error_help}" - if self.reason == FlowErrorReason.P2: - return f"{self.reason.name}: the odd neighbourhood (except the corrected node and nodes measured along axes Y or Z) of the correcting nodes must be in the future of the corrected node. Error found at c({self.node}) = {self.correction_set}." + if self.reason == FlowPropositionOrderErrorReason.P2: + return f"{self.reason.name}: the odd neighbourhood (except the corrected node and nodes measured along axes Y or Z) of the correcting nodes must be in the future of the corrected node.\n{error_help}" - if self.reason == FlowErrorReason.P3: - return f"{self.reason.name}: nodes that are measured along axis Y and that are not in the future of the corrected node (except the corrected node itself) cannot be in the closed odd neighbourhood of the correcting set. Error found at c({self.node}) = {self.correction_set}." + if self.reason == FlowPropositionOrderErrorReason.P3: + return f"{self.reason.name}: nodes that are measured along axis Y and that are not in the future of the corrected node (except the corrected node itself) cannot be in the closed odd neighbourhood of the correcting set.\nError found at c({self.node}) = {self.correction_set}. Partial order for Y-measured nodes: {self.past_and_present_nodes} ≼ {self.node}." - if self.reason == FlowErrorReason.P7: - return f"{self.reason.name}: nodes measured along axis X must belong to the odd neighbourhood of their own correcting set. Error found at c({self.node}) = {self.correction_set}." + assert_never(self.reason) - if self.reason == FlowErrorReason.P8: - return f"{self.reason.name}: nodes measured along axis Z must belong to their own correcting set. Error found at c({self.node}) = {self.correction_set}." - if self.reason == FlowErrorReason.P9: - return f"{self.reason.name}: nodes measured along axis Y must belong to the closed odd neighbourhood of their own correcting set. Error found at c({self.node}) = {self.correction_set}." +@dataclass +class PartialOrderError(FlowError[PartialOrderErrorReason]): + """Exception subclass to handle general flow errors in the partial order.""" + def __str__(self) -> str: + """Explain the error.""" + if self.reason == PartialOrderErrorReason.Empty: + return "The partial order cannot be empty." + + if self.reason == PartialOrderErrorReason.IncorrectNodes: + return "The partial order does not contain all the nodes of the open graph or contains nodes that are not in the open graph." + assert_never(self.reason) + + +@dataclass +class PartialOrderLayerError(FlowError[PartialOrderLayerErrorReason]): + """Exception subclass to handle flow errors concerning a specific layer of the partial order.""" + + layer_index: int + layer: AbstractSet[int] + + def __str__(self) -> str: + """Explain the error.""" + if self.reason == PartialOrderLayerErrorReason.FirstLayer: + return f"The first layer of the partial order must contain all the output nodes of the open graph and cannot be empty. First layer: {self.layer}" + + if self.reason == PartialOrderLayerErrorReason.NthLayer: + return f"Partial order layer {self.layer_index} = {self.layer} contains non-measured nodes of the open graph or is empty." assert_never(self.reason) diff --git a/tests/test_flow_core.py b/tests/test_flow_core.py index a1b4befd4..68f3e9a79 100644 --- a/tests/test_flow_core.py +++ b/tests/test_flow_core.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Generic, NamedTuple import networkx as nx import numpy as np @@ -9,11 +9,21 @@ from graphix.command import E, M, N, X, Z from graphix.flow.core import ( CausalFlow, + CorrectionFunctionError, + CorrectionFunctionErrorReason, FlowError, - FlowErrorReason, + FlowPropositionError, + FlowPropositionErrorReason, + FlowPropositionOrderError, + FlowPropositionOrderErrorReason, GFlow, + PartialOrderError, + PartialOrderErrorReason, + PartialOrderLayerError, + PartialOrderLayerErrorReason, PauliFlow, XZCorrections, + _Reason, ) from graphix.fundamentals import AbstractMeasurement, AbstractPlanarMeasurement, Axis, Plane from graphix.measurements import Measurement @@ -589,9 +599,9 @@ def test_from_measured_nodes_mapping_exceptions(self) -> None: XZCorrections.from_measured_nodes_mapping(og=og, x_corrections={0: {4}}) -class IncorrectFlowTestCase(NamedTuple): +class IncorrectFlowTestCase(NamedTuple, Generic[_Reason]): flow: PauliFlow[AbstractMeasurement] - exception: FlowError + exception: FlowError[_Reason] class TestIncorrectFlows: @@ -626,7 +636,7 @@ class TestIncorrectFlows: correction_function={0: {1}, 1: {2}}, partial_order_layers=[{3}, {2}, {1}, {0}], ), - FlowError(FlowErrorReason.CorrectionFunctionDomain), + CorrectionFunctionError(CorrectionFunctionErrorReason.IncorrectDomain), ), # Extra node in correction function image IncorrectFlowTestCase( @@ -635,7 +645,7 @@ class TestIncorrectFlows: correction_function={0: {1}, 1: {2}, 2: {4}}, partial_order_layers=[{3}, {2}, {1}, {0}], ), - FlowError(FlowErrorReason.CorrectionFunctionImage), + CorrectionFunctionError(CorrectionFunctionErrorReason.IncorrectImage), ), # Empty partial order IncorrectFlowTestCase( @@ -644,7 +654,7 @@ class TestIncorrectFlows: correction_function={0: {1}, 1: {2}, 2: {3}}, partial_order_layers=[], ), - FlowError(FlowErrorReason.PartialOrderEmpty), + PartialOrderError(PartialOrderErrorReason.Empty), ), # Incomplete partial order (first layer) IncorrectFlowTestCase( @@ -653,7 +663,7 @@ class TestIncorrectFlows: correction_function={0: {1}, 1: {2}, 2: {3}}, partial_order_layers=[{2}, {1}, {0}], ), - FlowError(FlowErrorReason.PartialOrderFirstLayer, layer={2}), + PartialOrderLayerError(PartialOrderLayerErrorReason.FirstLayer, layer_index=0, layer={2}), ), # Empty layer IncorrectFlowTestCase( @@ -662,7 +672,7 @@ class TestIncorrectFlows: correction_function={0: {1}, 1: {2}, 2: {3}}, partial_order_layers=[{3}, {2}, {1}, set(), {0}], ), - FlowError(FlowErrorReason.PartialOrderNthLayer, layer_index=3, layer=set()), + PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=3, layer=set()), ), # Output node in nth layer IncorrectFlowTestCase( @@ -671,7 +681,7 @@ class TestIncorrectFlows: correction_function={0: {1}, 1: {2}, 2: {3}}, partial_order_layers=[{3}, {2}, {3}, {1}, {0}], ), - FlowError(FlowErrorReason.PartialOrderNthLayer, layer_index=2, layer={3}), + PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=2, layer={3}), ), # Incomplete partial order (nth layer) IncorrectFlowTestCase( @@ -680,16 +690,16 @@ class TestIncorrectFlows: correction_function={0: {1}, 1: {2}, 2: {3}}, partial_order_layers=[{3}, {2}, {1}], ), - FlowError(FlowErrorReason.PartialOrderNodes), + PartialOrderError(PartialOrderErrorReason.IncorrectNodes), ), - # Correction function with more than one node in domain + # C0 IncorrectFlowTestCase( CausalFlow( og=og_c, correction_function={0: {1}, 1: {2, 3}, 2: {3}}, partial_order_layers=[{3}, {2}, {1}, {0}], ), - FlowError(FlowErrorReason.CorrectionSetCausalFlow, node=1, correction_set={2, 3}), + FlowPropositionError(FlowPropositionErrorReason.C0, node=1, correction_set={2, 3}), ), # C1 IncorrectFlowTestCase( @@ -698,7 +708,7 @@ class TestIncorrectFlows: correction_function={0: {2}, 2: {1}, 1: {3}}, partial_order_layers=[{3}, {1}, {2}, {0}], ), - FlowError(FlowErrorReason.C1, node=0, correction_set={2}), + FlowPropositionError(FlowPropositionErrorReason.C1, node=0, correction_set={2}), ), # C2 IncorrectFlowTestCase( @@ -707,7 +717,9 @@ class TestIncorrectFlows: correction_function={0: {1}, 1: {2}, 2: {3}}, partial_order_layers=[{3}, {2}, {0, 1}], ), - FlowError(FlowErrorReason.C2, node=0, correction_set={1}), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.C2, node=0, correction_set={1}, past_and_present_nodes={0, 1} + ), ), # C3 IncorrectFlowTestCase( @@ -716,7 +728,9 @@ class TestIncorrectFlows: correction_function={0: {1}, 1: {2}, 2: {3}}, partial_order_layers=[{3}, {1}, {0, 2}], ), - FlowError(FlowErrorReason.C3, node=0, correction_set={1}), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.C3, node=0, correction_set={1}, past_and_present_nodes={0, 2} + ), ), # G1 IncorrectFlowTestCase( @@ -725,7 +739,9 @@ class TestIncorrectFlows: correction_function={0: {3}, 1: {1, 2}, 2: {2, 3, 4}}, partial_order_layers=[{3, 4}, {1}, {0, 2}], ), - FlowError(FlowErrorReason.G1, node=1, correction_set={1, 2}), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.G1, node=1, correction_set={1, 2}, past_and_present_nodes={0, 1, 2} + ), ), # G2 IncorrectFlowTestCase( @@ -734,7 +750,12 @@ class TestIncorrectFlows: correction_function={0: {3}, 1: {1}, 2: {2, 3, 4}}, partial_order_layers=[{3, 4}, {1, 0, 2}], ), - FlowError(FlowErrorReason.G2, node=2, correction_set={2, 3, 4}), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.G2, + node=2, + correction_set={2, 3, 4}, + past_and_present_nodes={0, 1, 2}, + ), ), # G3 IncorrectFlowTestCase( @@ -743,7 +764,7 @@ class TestIncorrectFlows: correction_function={0: {3, 4}, 1: {1}, 2: {2, 3, 4}}, partial_order_layers=[{3, 4}, {1}, {2}, {0}], ), - FlowError(FlowErrorReason.G3, node=0, correction_set={3, 4}), + FlowPropositionError(FlowPropositionErrorReason.G3, node=0, correction_set={3, 4}), ), # P4 (same as G3 but for Pauli flow) IncorrectFlowTestCase( @@ -752,7 +773,7 @@ class TestIncorrectFlows: correction_function={0: {3, 4}, 1: {1}, 2: {2, 3, 4}}, partial_order_layers=[{3, 4}, {1}, {2}, {0}], ), - FlowError(FlowErrorReason.P4, node=0, correction_set={3, 4}), + FlowPropositionError(FlowPropositionErrorReason.P4, node=0, correction_set={3, 4}), ), # G4 IncorrectFlowTestCase( @@ -761,7 +782,7 @@ class TestIncorrectFlows: correction_function={0: {3}, 1: {1}, 2: {3, 4}}, partial_order_layers=[{3, 4}, {1}, {2}, {0}], ), - FlowError(FlowErrorReason.G4, node=2, correction_set={3, 4}), + FlowPropositionError(FlowPropositionErrorReason.G4, node=2, correction_set={3, 4}), ), # P5 (same as G4 but for Pauli flow) IncorrectFlowTestCase( @@ -770,7 +791,7 @@ class TestIncorrectFlows: correction_function={0: {3}, 1: {1}, 2: {3, 4}}, partial_order_layers=[{3, 4}, {1}, {2}, {0}], ), - FlowError(FlowErrorReason.P5, node=2, correction_set={3, 4}), + FlowPropositionError(FlowPropositionErrorReason.P5, node=2, correction_set={3, 4}), ), # G5 IncorrectFlowTestCase( @@ -779,7 +800,7 @@ class TestIncorrectFlows: correction_function={0: {3}, 1: set(), 2: {2, 3, 4}}, partial_order_layers=[{3, 4}, {1}, {2}, {0}], ), - FlowError(FlowErrorReason.G5, node=1, correction_set=set()), + FlowPropositionError(FlowPropositionErrorReason.G5, node=1, correction_set=set()), ), # P6 (same as G5 but for Pauli flow) IncorrectFlowTestCase( @@ -788,7 +809,7 @@ class TestIncorrectFlows: correction_function={0: {3}, 1: set(), 2: {2, 3, 4}}, partial_order_layers=[{3, 4}, {1}, {2}, {0}], ), - FlowError(FlowErrorReason.P6, node=1, correction_set=set()), + FlowPropositionError(FlowPropositionErrorReason.P6, node=1, correction_set=set()), ), # P1 IncorrectFlowTestCase( @@ -797,7 +818,9 @@ class TestIncorrectFlows: correction_function={0: {1, 3}, 1: {2}, 2: {3}}, partial_order_layers=[{3}, {2, 0, 1}], ), - FlowError(FlowErrorReason.P1, node=1, correction_set={2}), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.P1, node=1, correction_set={2}, past_and_present_nodes={0, 1, 2} + ), ), # P2 IncorrectFlowTestCase( @@ -806,7 +829,9 @@ class TestIncorrectFlows: correction_function={0: {1, 3}, 1: {3}, 2: {3}}, partial_order_layers=[{3}, {2, 0, 1}], ), - FlowError(FlowErrorReason.P2, node=1, correction_set={3}), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.P2, node=1, correction_set={3}, past_and_present_nodes={0, 1, 2} + ), ), # P3 IncorrectFlowTestCase( @@ -820,7 +845,9 @@ class TestIncorrectFlows: correction_function={0: {1}, 1: {2}}, partial_order_layers=[{2}, {0, 1}], ), - FlowError(FlowErrorReason.P3, node=0, correction_set={1}), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.P3, node=0, correction_set={1}, past_and_present_nodes={0, 1} + ), # Past and present nodes measured along Y. ), # P7 IncorrectFlowTestCase( @@ -829,7 +856,7 @@ class TestIncorrectFlows: correction_function={0: {1, 3}, 1: {3}, 2: {3}}, partial_order_layers=[{3}, {2}, {0, 1}], ), - FlowError(FlowErrorReason.P7, node=1, correction_set={3}), + FlowPropositionError(FlowPropositionErrorReason.P7, node=1, correction_set={3}), ), # P8 IncorrectFlowTestCase( @@ -843,7 +870,7 @@ class TestIncorrectFlows: correction_function={0: {1}}, partial_order_layers=[{1}, {0}], ), - FlowError(FlowErrorReason.P8, node=0, correction_set={1}), + FlowPropositionError(FlowPropositionErrorReason.P8, node=0, correction_set={1}), ), # P9 IncorrectFlowTestCase( @@ -857,15 +884,27 @@ class TestIncorrectFlows: correction_function={0: {2, 3}, 1: {1, 2}, 2: {3}}, partial_order_layers=[{3}, {2}, {0}, {1}], ), - FlowError(FlowErrorReason.P9, node=1, correction_set={1, 2}), + FlowPropositionError(FlowPropositionErrorReason.P9, node=1, correction_set={1, 2}), ), ], ) - def test_check_flow_general_properties(self, test_case: IncorrectFlowTestCase) -> None: + def test_check_flow_general_properties(self, test_case: IncorrectFlowTestCase[_Reason]) -> None: with pytest.raises(FlowError) as exc_info: test_case.flow.check_well_formed() assert exc_info.value.reason == test_case.exception.reason - assert exc_info.value.node == test_case.exception.node - assert exc_info.value.correction_set == test_case.exception.correction_set - assert exc_info.value.layer_index == test_case.exception.layer_index - assert exc_info.value.layer == test_case.exception.layer + + if isinstance(test_case.exception, FlowPropositionError): + assert isinstance(exc_info.value, FlowPropositionError) + assert exc_info.value.node == test_case.exception.node + assert exc_info.value.correction_set == test_case.exception.correction_set + + if isinstance(test_case.exception, FlowPropositionOrderError): + assert isinstance(exc_info.value, FlowPropositionOrderError) + assert exc_info.value.node == test_case.exception.node + assert exc_info.value.correction_set == test_case.exception.correction_set + assert exc_info.value.past_and_present_nodes == test_case.exception.past_and_present_nodes + + if isinstance(test_case.exception, PartialOrderLayerError): + assert isinstance(exc_info.value, PartialOrderLayerError) + assert exc_info.value.layer_index == test_case.exception.layer_index + assert exc_info.value.layer == test_case.exception.layer From 1a0746c08e375522ad9c80af338b8f892599ea10 Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 27 Nov 2025 16:43:46 +0100 Subject: [PATCH 04/58] Add get measurement label method and check on planes in causal flow --- graphix/flow/core.py | 42 ++++++++++++++++++++++++++++++++++++++--- tests/test_flow_core.py | 14 ++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index ccb08e009..6a42faea8 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -426,7 +426,7 @@ def check_well_formed(self) -> None: for node in layer: correction_set = set(self.correction_function[node]) - meas = self.og.measurements[node].to_plane_or_axis() + meas = self.get_measurement_label(node) for i in (correction_set - {node}) & past_and_present_nodes: if self.og.measurements[i].to_plane_or_axis() not in {Axis.X, Axis.Y}: @@ -496,6 +496,21 @@ def check_well_formed(self) -> None: if {*o_set, *past_and_present_nodes} != set(self.og.graph.nodes): raise PartialOrderError(PartialOrderErrorReason.IncorrectNodes) + def get_measurement_label(self, node: int) -> Plane | Axis: + """Get the measurement label of a given node in the open graph. + + This method interprets measurements with a Pauli angle as `Axis` instances, in consistence with the Pauli flow extraction routine. + + Parameters + ---------- + node : int + + Returns + ------- + Plane | Axis + """ + return self.og.measurements[node].to_plane_or_axis() + @dataclass(frozen=True) class GFlow(PauliFlow[_PM_co], Generic[_PM_co]): @@ -631,7 +646,7 @@ def check_well_formed(self) -> None: past_and_present_nodes=past_and_present_nodes, ) - plane = self.og.measurements[node].to_plane() + plane = self.get_measurement_label(node) if plane == Plane.XY: if not (node not in correction_set and node in odd_neighbors): @@ -656,6 +671,22 @@ def check_well_formed(self) -> None: if {*o_set, *past_and_present_nodes} != set(self.og.graph.nodes): raise PartialOrderError(PartialOrderErrorReason.IncorrectNodes) + @override + def get_measurement_label(self, node: int) -> Plane: + """Get the measurement label of a given node in the open graph. + + This method interprets measurements with a Pauli angle as `Plane` instances, in consistence with the gflow extraction routine. + + Parameters + ---------- + node : int + + Returns + ------- + Plane + """ + return self.og.measurements[node].to_plane() + @dataclass(frozen=True) class CausalFlow(GFlow[_PM_co], Generic[_PM_co]): @@ -752,6 +783,10 @@ def check_well_formed(self) -> None: if len(correction_set) != 1: raise FlowPropositionError(FlowPropositionErrorReason.C0, node=node, correction_set=correction_set) + meas = self.get_measurement_label(node) + if meas != Plane.XY: + raise FlowError("Causal flow is only defined on open graphs with XY measurements.") + neighbors = self.og.neighbors(correction_set) if node not in neighbors: @@ -980,7 +1015,8 @@ class PartialOrderLayerErrorReason(FlowErrorReason, Enum): """Nodes in the partial order beyond the first layer are not non-output nodes (measured qubits) of the open graph or layer is empty.""" -_Reason = TypeVar("_Reason", bound=FlowErrorReason) +# We bind `_Reason` to `str` to allow passing generic strings to a `FlowError` exception. +_Reason = TypeVar("_Reason", bound=FlowErrorReason | str) @dataclass diff --git a/tests/test_flow_core.py b/tests/test_flow_core.py index 68f3e9a79..9a3da78a1 100644 --- a/tests/test_flow_core.py +++ b/tests/test_flow_core.py @@ -629,6 +629,20 @@ class TestIncorrectFlows: @pytest.mark.parametrize( "test_case", [ + # Correct flow on an open graph with XZ measurements. + IncorrectFlowTestCase( + CausalFlow( + og=OpenGraph( + graph=nx.Graph([(0, 1)]), + input_nodes=[0], + output_nodes=[1], + measurements={0: Plane.XZ}, + ), + correction_function={0: {1}}, + partial_order_layers=[{1}, {0}], + ), + FlowError("Causal flow is only defined on open graphs with XY measurements."), + ), # Incomplete correction function IncorrectFlowTestCase( CausalFlow( From df39a019e36ed0dbd739a2ce105a4149a58cd79a Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 27 Nov 2025 17:03:26 +0100 Subject: [PATCH 05/58] Fix typing --- graphix/flow/core.py | 2 ++ tests/test_flow_core.py | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index 6a42faea8..1804f2315 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -758,6 +758,8 @@ def check_well_formed(self) -> None: - :math:`\forall k \in N_G(c(i)) \setminus \{i\}, i \prec k` (C3), where :math:`i \in O^c`, :math:`c` is the correction function and :math:`prec` denotes the partial order. + Causal flows are defined on open graphs with XY measurements only. + See Definition 2 in Ref. [1]. References diff --git a/tests/test_flow_core.py b/tests/test_flow_core.py index 9a3da78a1..ce9bc8f9d 100644 --- a/tests/test_flow_core.py +++ b/tests/test_flow_core.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Generic, NamedTuple +from typing import TYPE_CHECKING, NamedTuple import networkx as nx import numpy as np @@ -23,7 +23,6 @@ PartialOrderLayerErrorReason, PauliFlow, XZCorrections, - _Reason, ) from graphix.fundamentals import AbstractMeasurement, AbstractPlanarMeasurement, Axis, Plane from graphix.measurements import Measurement @@ -599,9 +598,19 @@ def test_from_measured_nodes_mapping_exceptions(self) -> None: XZCorrections.from_measured_nodes_mapping(og=og, x_corrections={0: {4}}) -class IncorrectFlowTestCase(NamedTuple, Generic[_Reason]): +ErrorT = ( + FlowError[str] + | CorrectionFunctionError + | FlowPropositionError + | FlowPropositionOrderError + | PartialOrderError + | PartialOrderLayerError +) + + +class IncorrectFlowTestCase(NamedTuple): flow: PauliFlow[AbstractMeasurement] - exception: FlowError[_Reason] + exception: ErrorT class TestIncorrectFlows: @@ -902,7 +911,7 @@ class TestIncorrectFlows: ), ], ) - def test_check_flow_general_properties(self, test_case: IncorrectFlowTestCase[_Reason]) -> None: + def test_check_flow_general_properties(self, test_case: IncorrectFlowTestCase) -> None: with pytest.raises(FlowError) as exc_info: test_case.flow.check_well_formed() assert exc_info.value.reason == test_case.exception.reason From 86148abff65c04a43f5b294f9d0235a78792e317 Mon Sep 17 00:00:00 2001 From: matulni Date: Fri, 28 Nov 2025 10:41:47 +0100 Subject: [PATCH 06/58] wip --- graphix/flow/core.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index 1804f2315..41e204d67 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -263,6 +263,38 @@ def is_compatible(self, total_measurement_order: TotalOrder) -> bool: return True + def check_well_formed(self) -> None: + + if len(self.partial_order_layers) == 0: + if len(self.og.graph) == 0: + return + raise PartialOrderError(PartialOrderErrorReason.Empty) + + o_set = set(self.og.output_nodes) + oc_set = set(self.og.measurements) + + first_layer = self.partial_order_layers[0] + + # Unlike for flows, XZCorrections can be well defined on open graphs without outputs + if o_set: + if first_layer != o_set: + raise PartialOrderLayerError(PartialOrderLayerErrorReason.FirstLayer, layer_index=0, layer=first_layer) + shift = 1 + else: + shift = 0 + + measured_layers = reversed(self.partial_order_layers[shift:]) + layer_idx = len(self.partial_order_layers) - 1 + past_and_present_nodes: set[int] = set() + for layer in measured_layers: + if not oc_set.issuperset(layer) or not layer: + raise PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=layer_idx, layer=layer) + if + + past_and_present_nodes.update(layer) + + + @dataclass(frozen=True) class PauliFlow(Generic[_M_co]): """An unmutable dataclass providing a representation of a Pauli flow. From 75ec180c8e848822d76e7e8625e8074a58bda662 Mon Sep 17 00:00:00 2001 From: matulni Date: Fri, 28 Nov 2025 10:47:42 +0100 Subject: [PATCH 07/58] Add check partial order no duplicates --- graphix/flow/core.py | 16 ++++++++-------- tests/test_flow_core.py | 9 +++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index 1804f2315..b3385c032 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -409,14 +409,14 @@ def check_well_formed(self) -> None: _check_flow_general_properties(self) o_set = set(self.og.output_nodes) - oc_set = set(self.og.graph.nodes - o_set) + oc_set = set(self.og.measurements) past_and_present_nodes: set[int] = set() past_and_present_nodes_y_meas: set[int] = set() layer_idx = len(self.partial_order_layers) - 1 for layer in reversed(self.partial_order_layers[1:]): - if not oc_set.issuperset(layer) or not layer: + if not oc_set.issuperset(layer) or not layer or layer & past_and_present_nodes: raise PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=layer_idx, layer=layer) past_and_present_nodes.update(layer) @@ -615,12 +615,12 @@ def check_well_formed(self) -> None: _check_flow_general_properties(self) o_set = set(self.og.output_nodes) - oc_set = set(self.og.graph.nodes - o_set) + oc_set = set(self.og.measurements) layer_idx = len(self.partial_order_layers) - 1 past_and_present_nodes: set[int] = set() for layer in reversed(self.partial_order_layers[1:]): - if not oc_set.issuperset(layer) or not layer: + if not oc_set.issuperset(layer) or not layer or layer & past_and_present_nodes: raise PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=layer_idx, layer=layer) past_and_present_nodes.update(layer) @@ -769,12 +769,12 @@ def check_well_formed(self) -> None: _check_flow_general_properties(self) o_set = set(self.og.output_nodes) - oc_set = set(self.og.graph.nodes - o_set) + oc_set = set(self.og.measurements) layer_idx = len(self.partial_order_layers) - 1 past_and_present_nodes: set[int] = set() for layer in reversed(self.partial_order_layers[1:]): - if not oc_set.issuperset(layer) or not layer: + if not oc_set.issuperset(layer) or not layer or layer & past_and_present_nodes: raise PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=layer_idx, layer=layer) past_and_present_nodes.update(layer) @@ -1014,7 +1014,7 @@ class PartialOrderLayerErrorReason(FlowErrorReason, Enum): """The first layer of the partial order is not the set of output nodes (non-measured qubits) of the open graph or is empty.""" # A well-defined flow cannot exist on an open graph without outputs. NthLayer = enum.auto() - """Nodes in the partial order beyond the first layer are not non-output nodes (measured qubits) of the open graph or layer is empty.""" + """Nodes in the partial order beyond the first layer are not non-output nodes (measured qubits) of the open graph, layer is empty or contains duplicates.""" # We bind `_Reason` to `str` to allow passing generic strings to a `FlowError` exception. @@ -1141,5 +1141,5 @@ def __str__(self) -> str: return f"The first layer of the partial order must contain all the output nodes of the open graph and cannot be empty. First layer: {self.layer}" if self.reason == PartialOrderLayerErrorReason.NthLayer: - return f"Partial order layer {self.layer_index} = {self.layer} contains non-measured nodes of the open graph or is empty." + return f"Partial order layer {self.layer_index} = {self.layer} contains non-measured nodes of the open graph, is empty or contains nodes in previous layers." assert_never(self.reason) diff --git a/tests/test_flow_core.py b/tests/test_flow_core.py index ce9bc8f9d..333538617 100644 --- a/tests/test_flow_core.py +++ b/tests/test_flow_core.py @@ -697,6 +697,15 @@ class TestIncorrectFlows: ), PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=3, layer=set()), ), + # Duplicate layer + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}, 2: {3}}, + partial_order_layers=[{3}, {2}, {1}, {1}, {0}], + ), + PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=2, layer={1}), + ), # Output node in nth layer IncorrectFlowTestCase( CausalFlow( From dea16c254ba12e424e2a61af4d46959a5530bae7 Mon Sep 17 00:00:00 2001 From: matulni Date: Fri, 28 Nov 2025 12:01:03 +0100 Subject: [PATCH 08/58] wip --- graphix/flow/core.py | 89 ++++++++++++++++++++++++++++++++++------- tests/test_flow_core.py | 14 +++++++ 2 files changed, 89 insertions(+), 14 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index eb3f9fc48..5103c38a3 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -262,9 +262,7 @@ def is_compatible(self, total_measurement_order: TotalOrder) -> bool: return True - def check_well_formed(self) -> None: - if len(self.partial_order_layers) == 0: if len(self.og.graph) == 0: return @@ -273,6 +271,9 @@ def check_well_formed(self) -> None: o_set = set(self.og.output_nodes) oc_set = set(self.og.measurements) + if not oc_set.issuperset(self.x_corrections.keys() | self.z_corrections.keys()): + raise XZCorrectionsError("Keys of corrections dictionaries are not a subset of the measured nodes.") + first_layer = self.partial_order_layers[0] # Unlike for flows, XZCorrections can be well defined on open graphs without outputs @@ -286,13 +287,30 @@ def check_well_formed(self) -> None: measured_layers = reversed(self.partial_order_layers[shift:]) layer_idx = len(self.partial_order_layers) - 1 past_and_present_nodes: set[int] = set() + for layer in measured_layers: - if not oc_set.issuperset(layer) or not layer: + if not oc_set.issuperset(layer) or not layer or layer & past_and_present_nodes: raise PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=layer_idx, layer=layer) - if past_and_present_nodes.update(layer) + for node in layer: + for corrections, reason in zip( + [self.x_corrections, self.z_corrections], XZCorrectionsOrderErrorReason, strict=True + ): + correction_set = corrections.get(node, set()) + if correction_set & past_and_present_nodes: + raise XZCorrectionsOrderError( + reason, + node=node, + correction_set=correction_set, + past_and_present_nodes=past_and_present_nodes, + ) + + layer_idx -= 1 + + if {*o_set, *past_and_present_nodes} != set(self.og.graph.nodes): + raise PartialOrderError(PartialOrderErrorReason.IncorrectNodes) @dataclass(frozen=True) @@ -956,6 +974,10 @@ class FlowErrorReason: """Describe the reason of a `FlowError`.""" +class XZCorrectionsErrorReason: + """Describe the reason of a `XZCorrectionsError`.""" + + class CorrectionFunctionErrorReason(FlowErrorReason, Enum): """Describe the reason of a `CorrectionFunctionError` exception.""" @@ -1029,7 +1051,7 @@ class FlowPropositionOrderErrorReason(FlowErrorReason, Enum): # NOTE: In the near future, this class may inherit from `XZCorrectionsErrorReason` too. -class PartialOrderErrorReason(FlowErrorReason, Enum): +class PartialOrderErrorReason(FlowErrorReason, XZCorrectionsErrorReason, Enum): """Describe the reason of a `PartialOrderError` exception.""" Empty = enum.auto() @@ -1039,18 +1061,31 @@ class PartialOrderErrorReason(FlowErrorReason, Enum): """The partial order does not contain all the nodes of the open graph or contains nodes that are not in the open graph.""" -class PartialOrderLayerErrorReason(FlowErrorReason, Enum): +class PartialOrderLayerErrorReason(FlowErrorReason, XZCorrectionsErrorReason, Enum): """Describe the reason of a `PartialOrderLayerError` exception.""" FirstLayer = enum.auto() - """The first layer of the partial order is not the set of output nodes (non-measured qubits) of the open graph or is empty.""" # A well-defined flow cannot exist on an open graph without outputs. + """The first layer of the partial order is not the set of output nodes (non-measured qubits) of the open graph or is empty. + + XZ-corrections can be defined on open graphs without outputs. That is not the case for correct flows. + """ NthLayer = enum.auto() """Nodes in the partial order beyond the first layer are not non-output nodes (measured qubits) of the open graph, layer is empty or contains duplicates.""" -# We bind `_Reason` to `str` to allow passing generic strings to a `FlowError` exception. -_Reason = TypeVar("_Reason", bound=FlowErrorReason | str) +class XZCorrectionsOrderErrorReason(XZCorrectionsErrorReason, Enum): + """Describe the reason of a `XZCorrectionsOrderError` exception.""" + + X = enum.auto() + """An X-correction set contains nodes in the present or the past of the corrected node.""" + + Z = enum.auto() + """An X-correction set contains nodes in the present or the past of the corrected node.""" + + +# We bind `_Reason` to `str` to allow passing generic strings to `FlowError` and `XZCorrectionsError` exceptions. +_Reason = TypeVar("_Reason", bound=FlowErrorReason | XZCorrectionsErrorReason | str) @dataclass @@ -1060,6 +1095,13 @@ class FlowError(Exception, Generic[_Reason]): reason: _Reason +@dataclass +class XZCorrectionsError(Exception, Generic[_Reason]): + """Exception subclass to handle XZCorrections errors.""" + + reason: _Reason + + @dataclass class CorrectionFunctionError(FlowError[CorrectionFunctionErrorReason]): """Exception subclass to handle general flow errors in the correction function.""" @@ -1147,8 +1189,8 @@ def __str__(self) -> str: @dataclass -class PartialOrderError(FlowError[PartialOrderErrorReason]): - """Exception subclass to handle general flow errors in the partial order.""" +class PartialOrderError(FlowError[PartialOrderErrorReason], XZCorrectionsError[PartialOrderErrorReason]): + """Exception subclass to handle general flow and XZ-corrections errors in the partial order.""" def __str__(self) -> str: """Explain the error.""" @@ -1161,8 +1203,8 @@ def __str__(self) -> str: @dataclass -class PartialOrderLayerError(FlowError[PartialOrderLayerErrorReason]): - """Exception subclass to handle flow errors concerning a specific layer of the partial order.""" +class PartialOrderLayerError(FlowError[PartialOrderLayerErrorReason], XZCorrectionsError[PartialOrderLayerErrorReason]): + """Exception subclass to handle flow and XZ-corrections errors concerning a specific layer of the partial order.""" layer_index: int layer: AbstractSet[int] @@ -1170,8 +1212,27 @@ class PartialOrderLayerError(FlowError[PartialOrderLayerErrorReason]): def __str__(self) -> str: """Explain the error.""" if self.reason == PartialOrderLayerErrorReason.FirstLayer: - return f"The first layer of the partial order must contain all the output nodes of the open graph and cannot be empty. First layer: {self.layer}" + return f"The first layer of the partial order must contain all the output nodes of the open graph and cannot be empty. First layer: {self.layer}." + + # Note: A flow defined on an open graph without outputs will trigger this error. This is not the case for an XZ-corrections object. if self.reason == PartialOrderLayerErrorReason.NthLayer: return f"Partial order layer {self.layer_index} = {self.layer} contains non-measured nodes of the open graph, is empty or contains nodes in previous layers." assert_never(self.reason) + + +@dataclass +class XZCorrectionsOrderError(XZCorrectionsError[XZCorrectionsOrderErrorReason]): + node: int + correction_set: AbstractSet[int] + past_and_present_nodes: AbstractSet[int] + + def __str__(self) -> str: + """Explain the error.""" + if self.reason == XZCorrectionsOrderErrorReason.X: + return "The X-correction set {self.node} -> {self.correction_set} contains nodes in the present or the past of the corrected node. Partial order: {self.past_and_present_nodes} ≼ {self.node}." + + if self.reason == XZCorrectionsOrderErrorReason.Z: + return "The Z-correction set {self.node} -> {self.correction_set} contains nodes in the present or the past of the corrected node. Partial order: {self.past_and_present_nodes} ≼ {self.node}." + + assert_never(self.reason) diff --git a/tests/test_flow_core.py b/tests/test_flow_core.py index 333538617..93a537a5f 100644 --- a/tests/test_flow_core.py +++ b/tests/test_flow_core.py @@ -387,6 +387,7 @@ def test_flow_to_corrections(self, test_case: XZCorrectionsTestCase) -> None: flow = test_case.flow flow.check_well_formed() corrections = flow.to_corrections() + corrections.check_well_formed() assert corrections.z_corrections == test_case.z_corr assert corrections.x_corrections == test_case.x_corr @@ -434,6 +435,8 @@ def test_order_1(self) -> None: og=og, x_corrections={0: {2}, 1: {3}, 2: {4}, 3: {5}}, z_corrections={0: {3, 4}, 1: {2, 5}} ) + corrections.check_well_formed() + assert corrections.is_compatible([0, 1, 2, 3]) assert corrections.is_compatible([1, 0, 2, 3]) assert corrections.is_compatible([1, 0, 3, 2]) @@ -457,6 +460,8 @@ def test_order_2(self) -> None: corrections = XZCorrections.from_measured_nodes_mapping(og=og, x_corrections={1: {0}}) + corrections.check_well_formed() + assert corrections.partial_order_layers == (frozenset({2, 3}), frozenset({0}), frozenset({1})) assert corrections.is_compatible([1, 0]) assert not corrections.is_compatible([0, 1]) # Wrong order @@ -479,6 +484,7 @@ def test_order_3(self) -> None: og=og, x_corrections={0: {1, 2}}, z_corrections={0: {1}} ) + corrections.check_well_formed() assert corrections.partial_order_layers == (frozenset({1, 2}), frozenset({0})) assert corrections.is_compatible([0, 1, 2]) assert not corrections.is_compatible([2, 0, 1]) # Wrong order @@ -496,6 +502,8 @@ def test_from_measured_nodes_mapping_0(self) -> None: ) corrections = XZCorrections.from_measured_nodes_mapping(og=og) + + corrections.check_well_formed() assert corrections.x_corrections == {} assert corrections.z_corrections == {} assert corrections.partial_order_layers == (frozenset({0, 1}),) @@ -510,6 +518,8 @@ def test_from_measured_nodes_mapping_1(self) -> None: ) corrections = XZCorrections.from_measured_nodes_mapping(og=og) + + corrections.check_well_formed() assert corrections.x_corrections == {} assert corrections.z_corrections == {} assert corrections.partial_order_layers == (frozenset({1}), frozenset({0})) @@ -525,6 +535,7 @@ def test_from_measured_nodes_mapping_2(self) -> None: corrections = XZCorrections.from_measured_nodes_mapping(og=og, x_corrections=x_corrections) + corrections.check_well_formed() assert all(corrections.partial_order_layers) # No empty sets assert all( sum(1 for layer in corrections.partial_order_layers if node in layer) == 1 for node in og.graph.nodes @@ -542,6 +553,7 @@ def test_from_measured_nodes_mapping_3(self) -> None: corrections = XZCorrections.from_measured_nodes_mapping(og=og, x_corrections=x_corrections) + corrections.check_well_formed() assert corrections.partial_order_layers == (frozenset({1, 3}), frozenset({0, 2})) # Some output nodes in corrections @@ -556,6 +568,7 @@ def test_from_measured_nodes_mapping_4(self) -> None: corrections = XZCorrections.from_measured_nodes_mapping(og=og, x_corrections=x_corrections) + corrections.check_well_formed() assert corrections.partial_order_layers == (frozenset({1, 3}), frozenset({0, 2})) # No output nodes in corrections @@ -570,6 +583,7 @@ def test_from_measured_nodes_mapping_5(self) -> None: corrections = XZCorrections.from_measured_nodes_mapping(og=og, x_corrections=x_corrections) + corrections.check_well_formed() assert corrections.partial_order_layers == (frozenset({1, 3}), frozenset({2}), frozenset({0})) # Test exceptions From 58af8bd880a7c18e86c5d8784da1a898d8fadfbe Mon Sep 17 00:00:00 2001 From: matulni Date: Fri, 28 Nov 2025 13:42:53 +0100 Subject: [PATCH 09/58] XZCorrections.check_well_formed passing tests --- graphix/flow/core.py | 21 +++++- tests/test_flow_core.py | 139 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 155 insertions(+), 5 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index 5103c38a3..ab8b51079 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -263,8 +263,25 @@ def is_compatible(self, total_measurement_order: TotalOrder) -> bool: return True def check_well_formed(self) -> None: + r"""Verify if the the XZ-corrections are well formed. + + Raises + ------ + XZCorrectionsError + if the XZ-corrections are not well formed. + + Notes + ----- + A correct `XZCorrections` instance verifies the following properties: + - Keys of the correction dictionaries are measured nodes, i.e., a subset of :math:`O^c`. + - Corrections respect the partial order. + - The first layer of the partial order contains all the output nodes if there are any. + - The partial order contains all the nodes (without duplicates) and it does not have empty layers. + + This method assumes that the open graph is well formed. + """ if len(self.partial_order_layers) == 0: - if len(self.og.graph) == 0: + if not (self.og.graph or self.x_corrections or self.z_corrections): return raise PartialOrderError(PartialOrderErrorReason.Empty) @@ -1223,6 +1240,8 @@ def __str__(self) -> str: @dataclass class XZCorrectionsOrderError(XZCorrectionsError[XZCorrectionsOrderErrorReason]): + """Exception subclass to handle incorrect XZ-corrections objects which concern the correction dictionaries and the partial order.""" + node: int correction_set: AbstractSet[int] past_and_present_nodes: AbstractSet[int] diff --git a/tests/test_flow_core.py b/tests/test_flow_core.py index 93a537a5f..ed996cf1c 100644 --- a/tests/test_flow_core.py +++ b/tests/test_flow_core.py @@ -23,6 +23,9 @@ PartialOrderLayerErrorReason, PauliFlow, XZCorrections, + XZCorrectionsError, + XZCorrectionsOrderError, + XZCorrectionsOrderErrorReason, ) from graphix.fundamentals import AbstractMeasurement, AbstractPlanarMeasurement, Axis, Plane from graphix.measurements import Measurement @@ -612,7 +615,7 @@ def test_from_measured_nodes_mapping_exceptions(self) -> None: XZCorrections.from_measured_nodes_mapping(og=og, x_corrections={0: {4}}) -ErrorT = ( +FlowErrorT = ( FlowError[str] | CorrectionFunctionError | FlowPropositionError @@ -624,11 +627,11 @@ def test_from_measured_nodes_mapping_exceptions(self) -> None: class IncorrectFlowTestCase(NamedTuple): flow: PauliFlow[AbstractMeasurement] - exception: ErrorT + exception: FlowErrorT class TestIncorrectFlows: - """Bundle for unit tests of :func:`PauliFlow.is_well_formed` (and children) on incorrect flows. Correct flows are extensively tested in `tests.test_opengraph.py`.""" + """Bundle for unit tests of :func:`PauliFlow.check_well_formed` (and children) on incorrect flows. Correct flows are extensively tested in `tests.test_opengraph.py`.""" og_c = OpenGraph( graph=nx.Graph([(0, 1), (1, 2), (2, 3)]), @@ -934,7 +937,7 @@ class TestIncorrectFlows: ), ], ) - def test_check_flow_general_properties(self, test_case: IncorrectFlowTestCase) -> None: + def test_flow_well_formed(self, test_case: IncorrectFlowTestCase) -> None: with pytest.raises(FlowError) as exc_info: test_case.flow.check_well_formed() assert exc_info.value.reason == test_case.exception.reason @@ -954,3 +957,131 @@ def test_check_flow_general_properties(self, test_case: IncorrectFlowTestCase) - assert isinstance(exc_info.value, PartialOrderLayerError) assert exc_info.value.layer_index == test_case.exception.layer_index assert exc_info.value.layer == test_case.exception.layer + + +XZErrorT = XZCorrectionsError[str] | XZCorrectionsOrderError | PartialOrderError | PartialOrderLayerError + + +class IncorrectXZCTestCase(NamedTuple): + xzcorr: XZCorrections[AbstractMeasurement] + exception: XZErrorT + + +class TestIncorrectXZC: + """Bundle for unit tests of :func:`XZCorrections.check_well_formed` on incorrect instances. Correct instances are extensively tested in class:`TestXZCorrections`.""" + + og = OpenGraph( + graph=nx.Graph([(0, 1), (1, 2), (2, 3)]), + input_nodes=[0], + output_nodes=[3], + measurements=dict.fromkeys(range(3), Plane.XY), + ) + + @pytest.mark.parametrize( + "test_case", + [ + # Empty partial order + IncorrectXZCTestCase( + XZCorrections( + og=og, + x_corrections={}, + z_corrections={}, + partial_order_layers=[], + ), + PartialOrderError(PartialOrderErrorReason.Empty), + ), + # Non-measured node in corrections dictionary keys + IncorrectXZCTestCase( + XZCorrections( + og=og, + x_corrections={}, + z_corrections={3: {1, 2}}, + partial_order_layers=[{3}, {2}, {1}, {0}], + ), + XZCorrectionsError("Keys of corrections dictionaries are not a subset of the measured nodes."), + ), + # First layer contains non-output nodes + IncorrectXZCTestCase( + XZCorrections( + og=og, + x_corrections={}, + z_corrections={}, + partial_order_layers=[{3, 2}, {1}, {0}], + ), + PartialOrderLayerError(PartialOrderLayerErrorReason.FirstLayer, layer_index=0, layer={2, 3}), + ), + # Duplicate nodes in partial order + IncorrectXZCTestCase( + XZCorrections( + og=og, + x_corrections={}, + z_corrections={}, + partial_order_layers=[{3}, {2, 1}, {0, 1}], + ), + PartialOrderLayerError( + PartialOrderLayerErrorReason.NthLayer, + layer_index=1, + layer={1, 2}, + ), + ), + # Output nodes in nth layer + IncorrectXZCTestCase( + XZCorrections( + og=og, + x_corrections={}, + z_corrections={}, + partial_order_layers=[{3}, {2}, {0, 1, 3}], + ), + PartialOrderLayerError( + PartialOrderLayerErrorReason.NthLayer, + layer_index=2, + layer={0, 1, 3}, + ), + ), + # Closed loop + IncorrectXZCTestCase( + XZCorrections( + og=og, + x_corrections={0: {1}, 1: {2}}, + z_corrections={2: {0}}, + partial_order_layers=[{3}, {2}, {1}, {0}], + ), + XZCorrectionsOrderError( + XZCorrectionsOrderErrorReason.Z, + node=2, + correction_set={0}, + past_and_present_nodes={0, 1, 2}, + ), + ), + # Self-correcting node + IncorrectXZCTestCase( + XZCorrections( + og=og, + x_corrections={0: {1}, 1: {1}}, + z_corrections={0: {1}}, + partial_order_layers=[{3}, {2}, {1}, {0}], + ), + XZCorrectionsOrderError( + XZCorrectionsOrderErrorReason.X, + node=1, + correction_set={1}, + past_and_present_nodes={0, 1}, + ), + ), + ], + ) + def test_xzc_well_formed(self, test_case: IncorrectXZCTestCase) -> None: + with pytest.raises(XZCorrectionsError) as exc_info: + test_case.xzcorr.check_well_formed() + assert exc_info.value.reason == test_case.exception.reason + + if isinstance(test_case.exception, XZCorrectionsOrderError): + assert isinstance(exc_info.value, XZCorrectionsOrderError) + assert exc_info.value.node == test_case.exception.node + assert exc_info.value.correction_set == test_case.exception.correction_set + assert exc_info.value.past_and_present_nodes == test_case.exception.past_and_present_nodes + + if isinstance(test_case.exception, PartialOrderLayerError): + assert isinstance(exc_info.value, PartialOrderLayerError) + assert exc_info.value.layer_index == test_case.exception.layer_index + assert exc_info.value.layer == test_case.exception.layer From 3464b6e48d61abb1a616b26272f4bf7dab84df9a Mon Sep 17 00:00:00 2001 From: matulni Date: Fri, 28 Nov 2025 13:42:53 +0100 Subject: [PATCH 10/58] XZCorrections.check_well_formed passing tests --- graphix/flow/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index ab8b51079..b705ff452 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -1067,7 +1067,6 @@ class FlowPropositionOrderErrorReason(FlowErrorReason, Enum): """Pauli flow (P3). Nodes that are measured along axis Y and that are not in the future of the corrected node (except the corrected node itself) cannot be in the closed odd neighbourhood of the correcting set.""" -# NOTE: In the near future, this class may inherit from `XZCorrectionsErrorReason` too. class PartialOrderErrorReason(FlowErrorReason, XZCorrectionsErrorReason, Enum): """Describe the reason of a `PartialOrderError` exception.""" From 6faa3f09fc2b1dfff5434ee399e63e3e8ff573b9 Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 1 Dec 2025 10:21:58 +0100 Subject: [PATCH 11/58] XZCorrections.check_well_formed passing all tests --- MERGE_MSG | 6 ++++++ graphix/flow/core.py | 35 ++++++++++++++++++++++++----------- tests/test_flow_core.py | 25 +++++++++++++++---------- 3 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 MERGE_MSG diff --git a/MERGE_MSG b/MERGE_MSG new file mode 100644 index 000000000..8374bc949 --- /dev/null +++ b/MERGE_MSG @@ -0,0 +1,6 @@ +Merge branch 'master' into rf_flow_iswellformed +# Please enter a commit message to explain why this merge is necessary, +# especially if it merges an updated upstream into a topic branch. +# +# Lines starting with '#' will be ignored, and an empty message aborts +# the commit. diff --git a/graphix/flow/core.py b/graphix/flow/core.py index b705ff452..71ab8c159 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -83,6 +83,14 @@ def from_measured_nodes_mapping( ------- XZCorrections[_M_co] + Raises + ------ + XZCorrectionsError + If the input dictionaries are not well formed. In well-formed correction dictionaries: + - Keys are a subset of the measured nodes. + - Values correspond to nodes of the open graph. + - Corrections do not form closed loops. + Notes ----- This method computes the partial order induced by the XZ-corrections. @@ -92,18 +100,16 @@ def from_measured_nodes_mapping( nodes_set = set(og.graph.nodes) outputs_set = frozenset(og.output_nodes) - non_outputs_set = nodes_set - outputs_set + non_outputs_set = set(og.measurements) - if not non_outputs_set.issuperset(x_corrections): - raise ValueError("Keys of input X-corrections contain non-measured nodes.") - if not set(z_corrections).issubset(non_outputs_set): - raise ValueError("Keys of input Z-corrections contain non-measured nodes.") + if not non_outputs_set.issuperset(x_corrections.keys() | z_corrections.keys()): + raise XZCorrectionsError("Keys of correction dictionaries are not a subset of the measured nodes.") dag = _corrections_to_dag(x_corrections, z_corrections) partial_order_layers = _dag_to_partial_order_layers(dag) if partial_order_layers is None: - raise ValueError( + raise XZCorrectionsError( "Input XZ-corrections are not runnable since the induced directed graph contains closed loops." ) @@ -133,7 +139,9 @@ def from_measured_nodes_mapping( ordered_nodes = frozenset.union(*partial_order_layers) if not ordered_nodes.issubset(nodes_set): - raise ValueError("Values of input mapping contain labels which are not nodes of the input open graph.") + raise XZCorrectionsError( + "Values of input mapping contain labels which are not nodes of the input open graph." + ) # We include all the non-output nodes not involved in the corrections in the last layer (first measured nodes). if unordered_nodes := frozenset(nodes_set - ordered_nodes): @@ -157,6 +165,11 @@ def to_pattern( ------- Pattern + Raises + ------ + XZCorrectionsError + If the input total measurement order is not compatible with the partial order induced by the XZ-corrections. + Notes ----- - The `XZCorrections` instance must be of parametric type `Measurement` to allow for a pattern extraction, otherwise the underlying open graph does not contain information about the measurement angles. @@ -170,7 +183,7 @@ def to_pattern( if total_measurement_order is None: total_measurement_order = self.generate_total_measurement_order() elif not self.is_compatible(total_measurement_order): - raise ValueError( + raise XZCorrectionsError( "The input total measurement order is not compatible with the partial order induced by the XZ-corrections." ) @@ -263,7 +276,7 @@ def is_compatible(self, total_measurement_order: TotalOrder) -> bool: return True def check_well_formed(self) -> None: - r"""Verify if the the XZ-corrections are well formed. + r"""Verify if the XZ-corrections are well formed. Raises ------ @@ -289,7 +302,7 @@ def check_well_formed(self) -> None: oc_set = set(self.og.measurements) if not oc_set.issuperset(self.x_corrections.keys() | self.z_corrections.keys()): - raise XZCorrectionsError("Keys of corrections dictionaries are not a subset of the measured nodes.") + raise XZCorrectionsError("Keys of correction dictionaries are not a subset of the measured nodes.") first_layer = self.partial_order_layers[0] @@ -1239,7 +1252,7 @@ def __str__(self) -> str: @dataclass class XZCorrectionsOrderError(XZCorrectionsError[XZCorrectionsOrderErrorReason]): - """Exception subclass to handle incorrect XZ-corrections objects which concern the correction dictionaries and the partial order.""" + """Exception subclass to handle incorrect XZ-corrections objects where the error concerns the correction dictionaries and the partial order.""" node: int correction_set: AbstractSet[int] diff --git a/tests/test_flow_core.py b/tests/test_flow_core.py index ed996cf1c..efc5a63bd 100644 --- a/tests/test_flow_core.py +++ b/tests/test_flow_core.py @@ -597,22 +597,27 @@ def test_from_measured_nodes_mapping_exceptions(self) -> None: output_nodes=[3], measurements=dict.fromkeys(range(3), Measurement(angle=0, plane=Plane.XY)), ) - with pytest.raises(ValueError, match=r"Keys of input X-corrections contain non-measured nodes."): + with pytest.raises(XZCorrectionsError) as exc_info: XZCorrections.from_measured_nodes_mapping(og=og, x_corrections={3: {1, 2}}) + assert exc_info.value.reason == "Keys of correction dictionaries are not a subset of the measured nodes." - with pytest.raises(ValueError, match=r"Keys of input Z-corrections contain non-measured nodes."): + with pytest.raises(XZCorrectionsError) as exc_info: XZCorrections.from_measured_nodes_mapping(og=og, z_corrections={3: {1, 2}}) + assert exc_info.value.reason == "Keys of correction dictionaries are not a subset of the measured nodes." - with pytest.raises( - ValueError, - match=r"Input XZ-corrections are not runnable since the induced directed graph contains closed loops.", - ): + with pytest.raises(XZCorrectionsError) as exc_info: XZCorrections.from_measured_nodes_mapping(og=og, x_corrections={0: {1}, 1: {2}}, z_corrections={2: {0}}) + assert ( + exc_info.value.reason + == "Input XZ-corrections are not runnable since the induced directed graph contains closed loops." + ) - with pytest.raises( - ValueError, match=r"Values of input mapping contain labels which are not nodes of the input open graph." - ): + with pytest.raises(XZCorrectionsError) as exc_info: XZCorrections.from_measured_nodes_mapping(og=og, x_corrections={0: {4}}) + assert ( + exc_info.value.reason + == "Values of input mapping contain labels which are not nodes of the input open graph." + ) FlowErrorT = ( @@ -998,7 +1003,7 @@ class TestIncorrectXZC: z_corrections={3: {1, 2}}, partial_order_layers=[{3}, {2}, {1}, {0}], ), - XZCorrectionsError("Keys of corrections dictionaries are not a subset of the measured nodes."), + XZCorrectionsError("Keys of correction dictionaries are not a subset of the measured nodes."), ), # First layer contains non-output nodes IncorrectXZCTestCase( From 488ff0dd2baa2b482926dd1af012a42edefefe89 Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 1 Dec 2025 11:46:36 +0100 Subject: [PATCH 12/58] Add partial_order module --- graphix/flow/_find_gpflow.py | 32 ++++++-------------- graphix/flow/_partial_order.py | 54 ++++++++++++++++++++++++++++++++++ tests/test_flow_find_gpflow.py | 13 ++++---- 3 files changed, 70 insertions(+), 29 deletions(-) create mode 100644 graphix/flow/_partial_order.py diff --git a/graphix/flow/_find_gpflow.py b/graphix/flow/_find_gpflow.py index 65e3ac917..ea903a1cd 100644 --- a/graphix/flow/_find_gpflow.py +++ b/graphix/flow/_find_gpflow.py @@ -20,6 +20,7 @@ from typing_extensions import override from graphix._linalg import MatGF2, solve_f2_linear_system +from graphix.flow._partial_order import compute_topological_generations from graphix.fundamentals import AbstractMeasurement, AbstractPlanarMeasurement, Axis, Plane from graphix.sim.base_backend import NodeIndex @@ -533,7 +534,7 @@ def _compute_correction_matrix_general_case( return c_prime_matrix.mat_mul(cb_matrix) -def _compute_topological_generations(ordering_matrix: MatGF2) -> list[list[int]] | None: +def _try_ordering_matrix_to_topological_generations(ordering_matrix: MatGF2) -> tuple[frozenset[int], ...] | None: """Stratify the directed acyclic graph (DAG) represented by the ordering matrix into generations. Parameters @@ -543,8 +544,8 @@ def _compute_topological_generations(ordering_matrix: MatGF2) -> list[list[int]] Returns ------- - list[list[int]] - topological generations. Integers represent the indices of the matrix `ordering_matrix`, not the labelling of the nodes. + tuple[frozenset[int], ...] + Topological generations. Integers represent the indices of the matrix `ordering_matrix`, not the labelling of the nodes. or `None` if `ordering_matrix` is not a DAG. @@ -558,31 +559,16 @@ def _compute_topological_generations(ordering_matrix: MatGF2) -> list[list[int]] adj_mat = ordering_matrix indegree_map: dict[int, int] = {} - zero_indegree: list[int] = [] + zero_indegree: set[int] = set() neighbors = {node: set(np.flatnonzero(row).astype(int)) for node, row in enumerate(adj_mat.T)} for node, col in enumerate(adj_mat): parents = np.flatnonzero(col) if parents.size: indegree_map[node] = parents.size else: - zero_indegree.append(node) - - generations: list[list[int]] = [] - - while zero_indegree: - this_generation = zero_indegree - zero_indegree = [] - for node in this_generation: - for child in neighbors[node]: - indegree_map[child] -= 1 - if indegree_map[child] == 0: - zero_indegree.append(child) - del indegree_map[child] - generations.append(this_generation) - - if indegree_map: - return None - return generations + zero_indegree.add(node) + + return compute_topological_generations(neighbors, indegree_map, zero_indegree) def compute_partial_order_layers(correction_matrix: CorrectionMatrix[_M_co]) -> tuple[frozenset[int], ...] | None: @@ -612,7 +598,7 @@ def compute_partial_order_layers(correction_matrix: CorrectionMatrix[_M_co]) -> aog, c_matrix = correction_matrix.aog, correction_matrix.c_matrix ordering_matrix = aog.order_demand_matrix.mat_mul(c_matrix) - if (topo_gen := _compute_topological_generations(ordering_matrix)) is None: + if (topo_gen := _try_ordering_matrix_to_topological_generations(ordering_matrix)) is None: return None # The NC matrix is not a DAG, therefore there's no flow. layers = [ diff --git a/graphix/flow/_partial_order.py b/graphix/flow/_partial_order.py new file mode 100644 index 000000000..d642c3cf6 --- /dev/null +++ b/graphix/flow/_partial_order.py @@ -0,0 +1,54 @@ +"""Tools for computing the partial orders.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Mapping + from collections.abc import Set as AbstractSet + + +def compute_topological_generations( + dag: Mapping[int, AbstractSet[int]], indegree_map: Mapping[int, int], zero_indegree: AbstractSet[int] +) -> tuple[frozenset[int], ...] | None: + """Stratify the directed acyclic graph (DAG) into generations. + + Parameters + ---------- + dag : Mapping[int, AbstractSet[int]] + Mapping encoding the directed acyclic graph. + + indegree_map : Mapping[int, int] + Indegree of the input DAG. A pair (``key``, ``value``) represents a node in the DAG and the number of incoming edges incident on it. It is assumed that indegree values are larger than 0. + + zero_indegree : AbstractSet[int] + Nodes in the DAG without any incoming edges. + + Returns + ------- + tuple[frozenset[int], ...] | None + Topological generations. `None` if the input DAG contains closed loops. + + Notes + ----- + This function is adapted from `:func: networkx.algorithms.dag.topological_generations` so that it works directly on dictionaries instead of a `:class: nx.DiGraph` object. + """ + generations: list[frozenset[int]] = [] + indegree_map = dict(indegree_map) + + while zero_indegree: + this_generation = zero_indegree + zero_indegree = set() + for node in this_generation: + for child in dag[node]: + indegree_map[child] -= 1 + if indegree_map[child] == 0: + zero_indegree.add(child) + del indegree_map[child] + generations.append(frozenset(this_generation)) + + if indegree_map: + return None + + return tuple(generations) diff --git a/tests/test_flow_find_gpflow.py b/tests/test_flow_find_gpflow.py index 704008db2..c0dfeda25 100644 --- a/tests/test_flow_find_gpflow.py +++ b/tests/test_flow_find_gpflow.py @@ -21,7 +21,7 @@ from graphix.flow._find_gpflow import ( AlgebraicOpenGraph, PlanarAlgebraicOpenGraph, - _compute_topological_generations, + _try_ordering_matrix_to_topological_generations, compute_correction_matrix, ) from graphix.fundamentals import Axis, Plane @@ -42,7 +42,7 @@ class AlgebraicOpenGraphTestCase(NamedTuple): class DAGTestCase(NamedTuple): adj_mat: MatGF2 - generations: list[list[int]] | None + generations: tuple[frozenset[int], ...] | None def prepare_test_og() -> list[AlgebraicOpenGraphTestCase]: @@ -230,14 +230,15 @@ def prepare_test_dag() -> list[DAGTestCase]: test_cases.extend( ( # Simple DAG DAGTestCase( - adj_mat=MatGF2([[0, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0], [0, 1, 1, 0]]), generations=[[0], [1, 2], [3]] + adj_mat=MatGF2([[0, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0], [0, 1, 1, 0]]), + generations=(frozenset({0}), frozenset({1, 2}), frozenset({3})), ), # Graph with loop DAGTestCase(adj_mat=MatGF2([[0, 0, 0, 0], [1, 0, 0, 1], [1, 0, 0, 0], [0, 1, 1, 0]]), generations=None), # Disconnected graph DAGTestCase( adj_mat=MatGF2([[0, 0, 0, 0, 0], [1, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0]]), - generations=[[0, 2], [1, 3, 4]], + generations=(frozenset({0, 2}), frozenset({1, 3, 4})), ), ) ) @@ -273,8 +274,8 @@ def test_correction_matrix(self, test_case: AlgebraicOpenGraphTestCase) -> None: assert corr_matrix is None @pytest.mark.parametrize("test_case", prepare_test_dag()) - def test_compute_topological_generations(self, test_case: DAGTestCase) -> None: + def test_try_ordering_matrix_to_topological_generations(self, test_case: DAGTestCase) -> None: adj_mat = test_case.adj_mat generations_ref = test_case.generations - assert generations_ref == _compute_topological_generations(adj_mat) + assert generations_ref == _try_ordering_matrix_to_topological_generations(adj_mat) From 84a1837dcfd522fe9b203fa89d5ad99ea1c21f6a Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 1 Dec 2025 17:48:30 +0100 Subject: [PATCH 13/58] Add partial order from pattern --- graphix/flow/_partial_order.py | 2 +- graphix/pattern.py | 59 ++++++++++++++++++++++++++++++++++ tests/test_pattern.py | 33 ++++++++++++++++++- 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/graphix/flow/_partial_order.py b/graphix/flow/_partial_order.py index d642c3cf6..569419c29 100644 --- a/graphix/flow/_partial_order.py +++ b/graphix/flow/_partial_order.py @@ -28,7 +28,7 @@ def compute_topological_generations( Returns ------- tuple[frozenset[int], ...] | None - Topological generations. `None` if the input DAG contains closed loops. + Topological generations. ``None`` if the input DAG contains closed loops. Notes ----- diff --git a/graphix/pattern.py b/graphix/pattern.py index 9820e8e1a..2df079bac 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -21,6 +21,7 @@ from graphix import command, optimization, parameter from graphix.clifford import Clifford from graphix.command import Command, CommandKind +from graphix.flow._partial_order import compute_topological_generations from graphix.fundamentals import Axis, Plane, Sign from graphix.gflow import find_flow, find_gflow, get_layers from graphix.graphsim import GraphState @@ -885,6 +886,64 @@ def update_dependency(measured: AbstractSet[int], dependency: dict[int, set[int] for i in dependency: dependency[i] -= measured + def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: + """Extract the measurement order of the pattern in the form of layers. + + This method builds a directed acyclical diagram (DAG) from the pattern and then performs a topological sort. + + Returns + ------- + tuple[frozenset[int], ...] + Measurement partial order between the pattern's nodes in a layer form. + + Raises + ------ + RunnabilityError + If the pattern is not runnable. + + Notes + ----- + The returned object follows the same conventions as the ``partial_order_layers`` attribute of :class:`PauliFlow` and :class:`XZCorrections` objects: + - Nodes in the same layer can be measured simultaneously. + - Nodes in layer ``i`` must be measured before nodes in layer ``i + 1``. + - All output nodes (if any) are in the first layer. + - There cannot be any empty layers. + """ + self.check_runnability() + + oset = frozenset(self.output_nodes) # First layer by convention + pre_measured_nodes = set(self.results.keys()) # Not included in the partial order layers + excluded_nodes = oset | pre_measured_nodes + + zero_indegree = set(self.input_nodes) - excluded_nodes + dag: dict[int, set[int]] = {node: set() for node in zero_indegree} + indegree_map: dict[int, int] = {} + + for cmd in self: + domain = set() + if cmd.kind == CommandKind.N: + if cmd.node not in oset: # pre-measured nodes only appear in domains. + dag[cmd.node] = set() + zero_indegree.add(cmd.node) + elif cmd.kind == CommandKind.M: + node, domain = cmd.node, cmd.s_domain | cmd.t_domain + elif cmd.kind == CommandKind.X or cmd.kind == CommandKind.Z: # noqa: PLR1714 + node, domain = cmd.node, cmd.domain + + for dep_node in domain: + if not {node, dep_node} & excluded_nodes: + dag[dep_node].add(node) + indegree_map[node] = indegree_map.get(node, 0) + 1 + if domain: + zero_indegree.discard(node) + + generations = compute_topological_generations(dag, indegree_map, zero_indegree) + assert generations is not None # DAG can't contain loops because pattern is runnable. + + if oset: + return oset, *generations[::-1] + return generations[::-1] + def get_layers(self) -> tuple[int, dict[int, set[int]]]: """Construct layers(l_k) from dependency information. diff --git a/tests/test_pattern.py b/tests/test_pattern.py index fc4a61b5a..fe89dcfd3 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -751,7 +751,7 @@ def test_check_runnability_failures(self) -> None: pattern = Pattern(cmds=[N(0), M(0, s_domain={0})]) with pytest.raises(RunnabilityError) as exc_info: - pattern.get_layers() + pattern.extract_partial_order_layers() assert exc_info.value.node == 0 assert exc_info.value.reason == RunnabilityErrorReason.DomainSelfLoop @@ -770,6 +770,37 @@ def test_check_runnability_failures(self) -> None: def test_compute_max_degree_empty_pattern(self) -> None: assert Pattern().compute_max_degree() == 0 + @pytest.mark.parametrize( + "test_case", + [ + ( + Pattern(input_nodes=[0], cmds=[N(1), E((0, 1)), M(0), M(1)]), + (frozenset({0, 1}),), + ), + ( + Pattern(input_nodes=[0], cmds=[N(1), N(2), E((0, 1)), E((1, 2)), M(0), M(1), X(2, {1}), Z(2, {0})]), + (frozenset({2}), frozenset({0, 1})), + ), + ( + Pattern(input_nodes=[0, 1], cmds=[M(1), M(0, s_domain={1}), N(2)]), + (frozenset({2}), frozenset({0}), frozenset({1})), + ), + ], + ) + def test_extract_partial_order_layers(self, test_case: tuple[Pattern, tuple[frozenset[int], ...]]) -> None: + assert test_case[0].extract_partial_order_layers() == test_case[1] + + def test_extract_partial_order_layers_results(self) -> None: + c = Circuit(1) + c.rz(0, 0.2) + p = c.transpile().pattern + p.perform_pauli_measurements() + assert p.extract_partial_order_layers() == (frozenset({2}), frozenset({0})) + + p = Pattern(cmds=[N(0), N(1), N(2), M(0), E((1, 2)), X(1, {0}), M(2, angle=0.3)]) + p.perform_pauli_measurements() + assert p.extract_partial_order_layers() == (frozenset({1}), frozenset({2})) + def cp(circuit: Circuit, theta: float, control: int, target: int) -> None: """Controlled rotation gate, decomposed.""" # noqa: D401 From 89189cd3447a63ca12f2ce80b3e312cf4151e25a Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 2 Dec 2025 15:47:10 +0100 Subject: [PATCH 14/58] wip --- graphix/pattern.py | 71 ++++++++++++++++++++++++++++++++++++++-- graphix/visualization.py | 6 ++-- tests/test_pattern.py | 4 +++ 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/graphix/pattern.py b/graphix/pattern.py index 2df079bac..ed8e0989b 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -18,6 +18,7 @@ import networkx as nx from typing_extensions import assert_never +import graphix.flow.core as flow from graphix import command, optimization, parameter from graphix.clifford import Clifford from graphix.command import Command, CommandKind @@ -931,11 +932,11 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: node, domain = cmd.node, cmd.domain for dep_node in domain: - if not {node, dep_node} & excluded_nodes: + if not {node, dep_node} & excluded_nodes and node not in dag[dep_node]: dag[dep_node].add(node) indegree_map[node] = indegree_map.get(node, 0) + 1 - if domain: - zero_indegree.discard(node) + + zero_indegree -= indegree_map.keys() generations = compute_topological_generations(dag, indegree_map, zero_indegree) assert generations is not None # DAG can't contain loops because pattern is runnable. @@ -944,6 +945,70 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: return oset, *generations[::-1] return generations[::-1] + def extract_causal_flow(self) -> flow.CausalFlow[Measurement]: + """Extract the causal flow structure from the current measurement pattern. + + This method reconstructs the underlying open graph, validates measurement constraints, builds correction dependencies, and verifies that the resulting :class:`flow.CausalFlow` satisfies all well-formedness conditions. + + Returns + ------- + flow.CausalFlow[Measurement] + The causal flow associated with the current pattern. + + Raises + ------ + ValueError + If the pattern: + - contains measurements in forbidden planes (XZ or YZ), + - assigns more than one correcting node to the same measured node, + - is empty, or + - fails the well-formedness checks for a valid causal flow. + + Notes + ----- + A causal flow is a structural property of MBQC patterns ensuring that corrections can be assigned deterministically with *single-element* correcting sets and without requiring measurements in the XZ or YZ planes. + """ + nodes = set(self.input_nodes) + edges: set[tuple[int, int]] = set() + measurements: dict[int, Measurement] = {} + correction_function: dict[int, set[int]] = {} + + for cmd in self.__seq: + if cmd.kind == CommandKind.N: + nodes.add(cmd.node) + elif cmd.kind == CommandKind.E: + u, v = cmd.nodes + if u > v: + u, v = v, u + edges.symmetric_difference_update({(u, v)}) + elif cmd.kind == CommandKind.M: + node = cmd.node + measurements[node] = Measurement(cmd.angle, cmd.plane) + if cmd.plane in {Plane.XZ, Plane.YZ}: + raise ValueError(f"Pattern does not have causal flow. Node {node} is measured in {cmd.plane}.") + elif cmd.kind == CommandKind.X: + corrected_node = cmd.node + for measured_node in cmd.domain: + if measured_node in correction_function: + raise ValueError( + f"Pattern does not have causal flow. Node {measured_node} is corrected by nodes {correction_function[measured_node].pop()} and {corrected_node} but correcting sets in causal flows can have one element only." + ) + correction_function[measured_node] = {corrected_node} + + graph = nx.Graph(edges) + graph.add_nodes_from(nodes) + og = opengraph.OpenGraph(graph, self.input_nodes, self.output_nodes, measurements) + + partial_order_layers = self.extract_partial_order_layers() + if len(partial_order_layers) == 0: + raise ValueError("Pattern is empty.") + + cf = flow.CausalFlow(og, correction_function, partial_order_layers) + + if not cf.is_well_formed(): + raise ValueError("Pattern does not have causal flow.") + return cf + def get_layers(self) -> tuple[int, dict[int, set[int]]]: """Construct layers(l_k) from dependency information. diff --git a/graphix/visualization.py b/graphix/visualization.py index 9c68bce11..250e21943 100644 --- a/graphix/visualization.py +++ b/graphix/visualization.py @@ -232,10 +232,8 @@ def get_paths( corrections = None else: print("The pattern is not consistent with flow or gflow structure.") - depth, layers = pattern.get_layers() - unfolded_layers = {element: key for key, value_set in layers.items() for element in value_set} - for output in pattern.output_nodes: - unfolded_layers[output] = depth + 1 + po_layers = pattern.extract_partial_order_layers() + unfolded_layers = {node: layer_idx for layer_idx, layer in enumerate(po_layers[::-1]) for node in layer} xflow, zflow = gflow.get_corrections_from_pattern(pattern) xzflow: dict[int, set[int]] = deepcopy(xflow) for key, value in zflow.items(): diff --git a/tests/test_pattern.py b/tests/test_pattern.py index fe89dcfd3..cda0484d0 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -785,6 +785,10 @@ def test_compute_max_degree_empty_pattern(self) -> None: Pattern(input_nodes=[0, 1], cmds=[M(1), M(0, s_domain={1}), N(2)]), (frozenset({2}), frozenset({0}), frozenset({1})), ), + ( + Pattern(input_nodes=[0], cmds=[N(1), N(2), E((0, 1)), E((1, 2)), M(0), M(1), X(2, {1}), Z(2, {1}), M(2)]), + (frozenset({2}), frozenset({0, 1})), + ), # double edge in DAG ], ) def test_extract_partial_order_layers(self, test_case: tuple[Pattern, tuple[frozenset[int], ...]]) -> None: From 0bd03fb8d5350da06f6a35dff01e82e197617815 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 2 Dec 2025 17:34:19 +0100 Subject: [PATCH 15/58] Tests passing --- graphix/pattern.py | 67 ++++++++++++- tests/test_pattern.py | 216 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 279 insertions(+), 4 deletions(-) diff --git a/graphix/pattern.py b/graphix/pattern.py index ed8e0989b..b4c97d00b 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -9,6 +9,7 @@ import dataclasses import enum import warnings +from collections import defaultdict from collections.abc import Iterable, Iterator from dataclasses import dataclass from enum import Enum @@ -963,6 +964,8 @@ def extract_causal_flow(self) -> flow.CausalFlow[Measurement]: - assigns more than one correcting node to the same measured node, - is empty, or - fails the well-formedness checks for a valid causal flow. + RunnabilityError + If the pattern is not runnable. Notes ----- @@ -997,8 +1000,9 @@ def extract_causal_flow(self) -> flow.CausalFlow[Measurement]: graph = nx.Graph(edges) graph.add_nodes_from(nodes) - og = opengraph.OpenGraph(graph, self.input_nodes, self.output_nodes, measurements) + og = OpenGraph(graph, self.input_nodes, self.output_nodes, measurements) + # Calling `self.extract_partial_order_layers` iterates over the pattern two more times. We sacrifice efficiency to avoid excessive code repetition. partial_order_layers = self.extract_partial_order_layers() if len(partial_order_layers) == 0: raise ValueError("Pattern is empty.") @@ -1009,6 +1013,67 @@ def extract_causal_flow(self) -> flow.CausalFlow[Measurement]: raise ValueError("Pattern does not have causal flow.") return cf + def extract_gflow(self) -> flow.GFlow[Measurement]: + """Extract the generalized flow (gflow) structure from the current measurement pattern. + + The method reconstructs the underlying open graph, and determines the correction dependencies and the partial order required for a valid gflow. It then constructs and validates a :class:`flow.GFlow` object. + + Returns + ------- + flow.GFlow[Measurement] + The gflow associated with the current pattern. + + Raises + ------ + ValueError + If the pattern is empty or if the extracted structure does not satisfy + the well-formedness conditions required for a valid gflow. + RunnabilityError + If the pattern is not runnable. + + Notes + ----- + A gflow is a structural property of measurement-based quantum computation + (MBQC) patterns that ensures determinism and proper correction propagation. + """ + nodes = set(self.input_nodes) + edges: set[tuple[int, int]] = set() + measurements: dict[int, Measurement] = {} + correction_function: dict[int, set[int]] = defaultdict(set) + + for cmd in self.__seq: + if cmd.kind == CommandKind.N: + nodes.add(cmd.node) + elif cmd.kind == CommandKind.E: + u, v = cmd.nodes + if u > v: + u, v = v, u + edges.symmetric_difference_update({(u, v)}) + elif cmd.kind == CommandKind.M: + node = cmd.node + measurements[node] = Measurement(cmd.angle, cmd.plane) + if cmd.plane in {Plane.XZ, Plane.YZ}: + correction_function[node].add(node) + elif cmd.kind == CommandKind.X: + corrected_node = cmd.node + for measured_node in cmd.domain: + correction_function[measured_node].add(corrected_node) + + graph = nx.Graph(edges) + graph.add_nodes_from(nodes) + og = OpenGraph(graph, self.input_nodes, self.output_nodes, measurements) + + # Calling `self.extract_partial_order_layers` iterates over the pattern two more times. We sacrifice efficiency to avoid excessive code repetition. + partial_order_layers = self.extract_partial_order_layers() + if len(partial_order_layers) == 0: + raise ValueError("Pattern is empty.") + + gf = flow.GFlow(og, correction_function, partial_order_layers) + + if not gf.is_well_formed(): + raise ValueError("Pattern does not have gflow.") + return gf + def get_layers(self) -> tuple[int, dict[int, set[int]]]: """Construct layers(l_k) from dependency information. diff --git a/tests/test_pattern.py b/tests/test_pattern.py index cda0484d0..6e1d71a80 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -3,7 +3,7 @@ import copy import itertools import typing -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, NamedTuple import networkx as nx import numpy as np @@ -14,7 +14,8 @@ from graphix.clifford import Clifford from graphix.command import C, Command, CommandKind, E, M, N, X, Z from graphix.fundamentals import Plane -from graphix.measurements import Outcome, PauliMeasurement +from graphix.measurements import Measurement, Outcome, PauliMeasurement +from graphix.opengraph import OpenGraph from graphix.pattern import Pattern, RunnabilityError, RunnabilityErrorReason, shift_outcomes from graphix.random_objects import rand_circuit, rand_gate from graphix.sim.density_matrix import DensityMatrix @@ -786,7 +787,9 @@ def test_compute_max_degree_empty_pattern(self) -> None: (frozenset({2}), frozenset({0}), frozenset({1})), ), ( - Pattern(input_nodes=[0], cmds=[N(1), N(2), E((0, 1)), E((1, 2)), M(0), M(1), X(2, {1}), Z(2, {1}), M(2)]), + Pattern( + input_nodes=[0], cmds=[N(1), N(2), E((0, 1)), E((1, 2)), M(0), M(1), X(2, {1}), Z(2, {1}), M(2)] + ), (frozenset({2}), frozenset({0, 1})), ), # double edge in DAG ], @@ -805,6 +808,213 @@ def test_extract_partial_order_layers_results(self) -> None: p.perform_pauli_measurements() assert p.extract_partial_order_layers() == (frozenset({1}), frozenset({2})) + class PatternFlowTestCase(NamedTuple): + pattern: Pattern + has_cflow: bool + has_gflow: bool + error_cflow: str | None = None + error_gflow: str | None = None + + PATTERN_FLOW_TEST_CASES: list[PatternFlowTestCase] = [ # noqa: RUF012 + PatternFlowTestCase( + # General example + Pattern( + input_nodes=[0, 1], + cmds=[ + N(2), + N(3), + N(4), + N(5), + N(6), + N(7), + E((0, 2)), + E((2, 3)), + E((2, 4)), + E((1, 3)), + E((3, 5)), + E((4, 5)), + E((4, 6)), + E((5, 7)), + M(0, angle=0.1), + Z(3, {0}), + Z(4, {0}), + X(2, {0}), + M(1, angle=0.1), + Z(2, {1}), + Z(5, {1}), + X(3, {1}), + M(2, angle=0.1), + Z(5, {2}), + Z(6, {2}), + X(4, {2}), + M(3, angle=0.1), + Z(4, {3}), + Z(7, {3}), + X(5, {3}), + M(4, angle=0.1), + X(6, {4}), + M(5, angle=0.4), + X(7, {5}), + ], + output_nodes=[6, 7], + ), + has_cflow=True, + has_gflow=True, + ), + PatternFlowTestCase( + # No measurements or corrections + Pattern(input_nodes=[0, 1], cmds=[E((0, 1))]), + has_cflow=True, + has_gflow=True, + ), + PatternFlowTestCase( + # Disconnected nodes and unordered outputs + Pattern(input_nodes=[2], cmds=[N(0), N(1), E((0, 1)), M(0), X(1, {0})], output_nodes=[2, 1]), + has_cflow=True, + has_gflow=True, + ), + PatternFlowTestCase( + # Pattern with XZ measurements. + Pattern(cmds=[N(0), N(1), E((0, 1)), M(0, Plane.XZ, 0.3), Z(1, {0}), X(1, {0})], output_nodes=[1]), + has_cflow=False, + has_gflow=True, + error_cflow="Pattern does not have causal flow. Node 0 is measured in Plane.XZ.", + ), + PatternFlowTestCase( + # Pattern with gflow but without causal flow and XY measurements. + Pattern( + input_nodes=[1, 2, 3], + cmds=[ + N(4), + N(5), + N(6), + E((1, 4)), + E((1, 6)), + E((4, 2)), + E((6, 2)), + E((6, 3)), + E((2, 5)), + E((5, 3)), + M(1, angle=0.1), + X(5, {1}), + X(6, {1}), + M(2, angle=0.2), + X(4, {2}), + X(5, {2}), + X(6, {2}), + M(3, angle=0.3), + X(4, {3}), + X(6, {3}), + ], + output_nodes=[4, 5, 6], + ), + has_cflow=False, + has_gflow=True, + error_cflow="Pattern does not have causal flow. Node 1 is corrected by nodes 5 and 6 but correcting sets in causal flows can have one element only.", + ), + PatternFlowTestCase( + # Non-deterministic pattern + Pattern(input_nodes=[0], cmds=[N(1), E((0, 1)), M(0, Plane.XY, 0.3)]), + has_cflow=False, + has_gflow=False, + error_cflow="Pattern does not have causal flow.", + error_gflow="Pattern does not have gflow.", + ), + ] + + # Extract causal flow from random circuits + @pytest.mark.skip(reason="Fix transpilation #349") + @pytest.mark.parametrize("jumps", range(1, 11)) + def test_extract_causal_flow_0(self, fx_bg: PCG64, jumps: int) -> None: + rng = Generator(fx_bg.jumped(jumps)) + nqubits = 4 + depth = 2 + circuit_1 = rand_circuit(nqubits, depth, rng, use_ccx=True) + p_ref = circuit_1.transpile().pattern + cf = p_ref.extract_gflow() + p_test = cf.to_corrections().to_pattern() + + s_ref = p_ref.simulate_pattern(rng=rng) + s_test = p_test.simulate_pattern(rng=rng) + assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + + @pytest.mark.parametrize("test_case", PATTERN_FLOW_TEST_CASES) + def test_extract_causal_flow(self, fx_rng: Generator, test_case: PatternFlowTestCase) -> None: + if test_case.has_cflow: + alpha = 2 * np.pi * fx_rng.random() + s_ref = test_case.pattern.simulate_pattern(input_state=PlanarState(Plane.XZ, alpha)) + + p_test = test_case.pattern.extract_causal_flow().to_corrections().to_pattern() + s_test = p_test.simulate_pattern(input_state=PlanarState(Plane.XZ, alpha), rng=fx_rng) + + assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + else: + with pytest.raises(ValueError) as err: + test_case.pattern.extract_causal_flow() + assert str(err.value) == test_case.error_cflow + + @pytest.mark.parametrize("test_case", PATTERN_FLOW_TEST_CASES) + def test_extract_gflow(self, fx_rng: Generator, test_case: PatternFlowTestCase) -> None: + if test_case.has_gflow: + alpha = 2 * np.pi * fx_rng.random() + s_ref = test_case.pattern.simulate_pattern(input_state=PlanarState(Plane.XZ, alpha)) + + p_test = test_case.pattern.extract_gflow().to_corrections().to_pattern() + s_test = p_test.simulate_pattern(input_state=PlanarState(Plane.XZ, alpha), rng=fx_rng) + + assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + else: + with pytest.raises(ValueError) as err: + test_case.pattern.extract_gflow() + assert str(err.value) == test_case.error_gflow + + # From open graph + def test_extract_cflow_og(self, fx_rng: Generator) -> None: + alpha = 2 * np.pi * fx_rng.random() + + og = OpenGraph( + graph=nx.Graph([(1, 3), (2, 4), (3, 4), (3, 5), (4, 6)]), + input_nodes=[1, 2], + output_nodes=[6, 5], + measurements={ + 1: Measurement(0.1, Plane.XY), + 2: Measurement(0.2, Plane.XY), + 3: Measurement(0.3, Plane.XY), + 4: Measurement(0.4, Plane.XY), + }, + ) + p_ref = og.extract_causal_flow().to_corrections().to_pattern() + s_ref = p_ref.simulate_pattern(input_state=PlanarState(Plane.XZ, alpha)) + + p_test = p_ref.extract_causal_flow().to_corrections().to_pattern() + s_test = p_test.simulate_pattern(input_state=PlanarState(Plane.XZ, alpha)) + + assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + + # From open graph + def test_extract_gflow_og(self, fx_rng: Generator) -> None: + alpha = 2 * np.pi * fx_rng.random() + + og = OpenGraph( + graph=nx.Graph([(1, 3), (2, 4), (3, 4), (3, 5), (4, 6)]), + input_nodes=[1, 2], + output_nodes=[6, 5], + measurements={ + 1: Measurement(0.1, Plane.XY), + 2: Measurement(0.2, Plane.XY), + 3: Measurement(0.3, Plane.XY), + 4: Measurement(0.4, Plane.XY), + }, + ) + + p_ref = og.extract_gflow().to_corrections().to_pattern() + s_ref = p_ref.simulate_pattern(input_state=PlanarState(Plane.XZ, alpha)) + + p_test = p_ref.extract_gflow().to_corrections().to_pattern() + s_test = p_test.simulate_pattern(input_state=PlanarState(Plane.XZ, alpha)) + + assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + def cp(circuit: Circuit, theta: float, control: int, target: int) -> None: """Controlled rotation gate, decomposed.""" # noqa: D401 From 966a272987d13898daf7ffb53a39e52791d17890 Mon Sep 17 00:00:00 2001 From: matulni Date: Wed, 3 Dec 2025 15:51:05 +0100 Subject: [PATCH 16/58] Move flow extraction to standardized pattern --- graphix/optimization.py | 168 +++++++++++++++++++++++++++++++++++++++- graphix/pattern.py | 81 +------------------ tests/test_pattern.py | 30 +++++-- 3 files changed, 193 insertions(+), 86 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index cc67f2833..05e2e77b0 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -3,6 +3,7 @@ from __future__ import annotations import dataclasses +from collections import defaultdict from copy import copy from dataclasses import dataclass from types import MappingProxyType @@ -17,8 +18,11 @@ from graphix import command from graphix.clifford import Clifford from graphix.command import CommandKind, Node +from graphix.flow._partial_order import compute_topological_generations +from graphix.flow.core import CausalFlow, GFlow from graphix.fundamentals import Axis, Plane -from graphix.measurements import Domains, Outcome, PauliMeasurement +from graphix.measurements import Domains, Measurement, Outcome, PauliMeasurement +from graphix.opengraph import OpenGraph if TYPE_CHECKING: from collections.abc import Iterable, Mapping @@ -362,6 +366,168 @@ def ensure_neighborhood(node: Node) -> None: done.add(node) return pattern + def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: + """Extract the measurement order of the pattern in the form of layers. + + This method builds a directed acyclical diagram (DAG) from the pattern and then performs a topological sort. + + Returns + ------- + tuple[frozenset[int], ...] + Measurement partial order between the pattern's nodes in a layer form. + + Notes + ----- + The returned object follows the same conventions as the ``partial_order_layers`` attribute of :class:`PauliFlow` and :class:`XZCorrections` objects: + - Nodes in the same layer can be measured simultaneously. + - Nodes in layer ``i`` must be measured before nodes in layer ``i + 1``. + - All output nodes (if any) are in the first layer. + - There cannot be any empty layers. + """ + oset = frozenset(self.output_nodes) # First layer by convention + pre_measured_nodes = set(self.results.keys()) # Not included in the partial order layers + excluded_nodes = oset | pre_measured_nodes + + zero_indegree = set(self.input_nodes) - excluded_nodes + dag: dict[int, set[int]] = {node: set() for node in zero_indegree} + indegree_map: dict[int, int] = {} + + for n in self.n_list: + if n.node not in oset: # pre-measured nodes only appear in domains. + dag[n.node] = set() + zero_indegree.add(n.node) + + def process_domain(node: Node, domain: AbstractSet[Node]) -> None: + for dep_node in domain: + if not {node, dep_node} & excluded_nodes and node not in dag[dep_node]: + dag[dep_node].add(node) + indegree_map[node] = indegree_map.get(node, 0) + 1 + + domain: AbstractSet[Node] + + for m in self.m_list: + node, domain = m.node, m.s_domain | m.t_domain + process_domain(node, domain) + + for corrections in [self.z_dict, self.x_dict]: + for node, domain in corrections.items(): + process_domain(node, domain) + + zero_indegree -= indegree_map.keys() + + generations = compute_topological_generations(dag, indegree_map, zero_indegree) + assert generations is not None # DAG can't contain loops because pattern is runnable. + + if oset: + return oset, *generations[::-1] + return generations[::-1] + + def extract_causal_flow(self) -> CausalFlow[Measurement]: + """Extract the causal flow structure from the current measurement pattern. + + This method reconstructs the underlying open graph, validates measurement constraints, builds correction dependencies, and verifies that the resulting :class:`flow.CausalFlow` satisfies all well-formedness conditions. + + Returns + ------- + flow.CausalFlow[Measurement] + The causal flow associated with the current pattern. + + Raises + ------ + ValueError + If the pattern: + - contains measurements in forbidden planes (XZ or YZ), + - assigns more than one correcting node to the same measured node, + - is empty, or + - fails the well-formedness checks for a valid causal flow. + + Notes + ----- + A causal flow is a structural property of MBQC patterns ensuring that corrections can be assigned deterministically with *single-element* correcting sets and without requiring measurements in the XZ or YZ planes. + """ + measurements: dict[int, Measurement] = {} + correction_function: dict[int, set[int]] = {} + + def process_domain(node: Node, domain: AbstractSet[Node]) -> None: + for measured_node in domain: + if measured_node in correction_function: + raise ValueError( + f"Pattern does not have causal flow. Node {measured_node} is corrected by nodes {correction_function[measured_node].pop()} and {node} but correcting sets in causal flows can have one element only." + ) + correction_function[measured_node] = {node} + + for m in self.m_list: + if m.plane in {Plane.XZ, Plane.YZ}: + raise ValueError(f"Pattern does not have causal flow. Node {m.node} is measured in {m.plane}.") + measurements[m.node] = Measurement(m.angle, m.plane) + process_domain(m.node, m.s_domain) + + for node, domain in self.x_dict.items(): + process_domain(node, domain) + + partial_order_layers = self.extract_partial_order_layers() + if len(partial_order_layers) == 0: + raise ValueError("Pattern is empty.") + + og = OpenGraph(self.extract_graph(), self.input_nodes, self.output_nodes, measurements) + + cf = CausalFlow(og, correction_function, partial_order_layers) + + if not cf.is_well_formed(): + raise ValueError("Pattern does not have causal flow.") + return cf + + def extract_gflow(self) -> GFlow[Measurement]: + """Extract the generalized flow (gflow) structure from the current measurement pattern. + + The method reconstructs the underlying open graph, and determines the correction dependencies and the partial order required for a valid gflow. It then constructs and validates a :class:`flow.GFlow` object. + + Returns + ------- + flow.GFlow[Measurement] + The gflow associated with the current pattern. + + Raises + ------ + ValueError + If the pattern is empty or if the extracted structure does not satisfy + the well-formedness conditions required for a valid gflow. + RunnabilityError + If the pattern is not runnable. + + Notes + ----- + A gflow is a structural property of measurement-based quantum computation + (MBQC) patterns that ensures determinism and proper correction propagation. + """ + measurements: dict[int, Measurement] = {} + correction_function: dict[int, set[int]] = defaultdict(set) + + def process_domain(node: Node, domain: AbstractSet[Node]) -> None: + for measured_node in domain: + correction_function[measured_node].add(node) + + for m in self.m_list: + measurements[m.node] = Measurement(m.angle, m.plane) + if m.plane in {Plane.XZ, Plane.YZ}: + correction_function[m.node].add(m.node) + process_domain(m.node, m.s_domain) + + for node, domain in self.x_dict.items(): + process_domain(node, domain) + + partial_order_layers = self.extract_partial_order_layers() + if len(partial_order_layers) == 0: + raise ValueError("Pattern is empty.") + + og = OpenGraph(self.extract_graph(), self.input_nodes, self.output_nodes, measurements) + + gf = GFlow(og, correction_function, partial_order_layers) + + if not gf.is_well_formed(): + raise ValueError("Pattern does not have gflow.") + return gf + def _add_correction_domain(domain_dict: dict[Node, set[Node]], node: Node, domain: set[Node]) -> None: """Merge a correction domain into ``domain_dict`` for ``node``. diff --git a/graphix/pattern.py b/graphix/pattern.py index b4c97d00b..4ba21a55d 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -9,7 +9,6 @@ import dataclasses import enum import warnings -from collections import defaultdict from collections.abc import Iterable, Iterator from dataclasses import dataclass from enum import Enum @@ -971,47 +970,7 @@ def extract_causal_flow(self) -> flow.CausalFlow[Measurement]: ----- A causal flow is a structural property of MBQC patterns ensuring that corrections can be assigned deterministically with *single-element* correcting sets and without requiring measurements in the XZ or YZ planes. """ - nodes = set(self.input_nodes) - edges: set[tuple[int, int]] = set() - measurements: dict[int, Measurement] = {} - correction_function: dict[int, set[int]] = {} - - for cmd in self.__seq: - if cmd.kind == CommandKind.N: - nodes.add(cmd.node) - elif cmd.kind == CommandKind.E: - u, v = cmd.nodes - if u > v: - u, v = v, u - edges.symmetric_difference_update({(u, v)}) - elif cmd.kind == CommandKind.M: - node = cmd.node - measurements[node] = Measurement(cmd.angle, cmd.plane) - if cmd.plane in {Plane.XZ, Plane.YZ}: - raise ValueError(f"Pattern does not have causal flow. Node {node} is measured in {cmd.plane}.") - elif cmd.kind == CommandKind.X: - corrected_node = cmd.node - for measured_node in cmd.domain: - if measured_node in correction_function: - raise ValueError( - f"Pattern does not have causal flow. Node {measured_node} is corrected by nodes {correction_function[measured_node].pop()} and {corrected_node} but correcting sets in causal flows can have one element only." - ) - correction_function[measured_node] = {corrected_node} - - graph = nx.Graph(edges) - graph.add_nodes_from(nodes) - og = OpenGraph(graph, self.input_nodes, self.output_nodes, measurements) - - # Calling `self.extract_partial_order_layers` iterates over the pattern two more times. We sacrifice efficiency to avoid excessive code repetition. - partial_order_layers = self.extract_partial_order_layers() - if len(partial_order_layers) == 0: - raise ValueError("Pattern is empty.") - - cf = flow.CausalFlow(og, correction_function, partial_order_layers) - - if not cf.is_well_formed(): - raise ValueError("Pattern does not have causal flow.") - return cf + return optimization.StandardizedPattern.from_pattern(self).extract_causal_flow() def extract_gflow(self) -> flow.GFlow[Measurement]: """Extract the generalized flow (gflow) structure from the current measurement pattern. @@ -1036,43 +995,7 @@ def extract_gflow(self) -> flow.GFlow[Measurement]: A gflow is a structural property of measurement-based quantum computation (MBQC) patterns that ensures determinism and proper correction propagation. """ - nodes = set(self.input_nodes) - edges: set[tuple[int, int]] = set() - measurements: dict[int, Measurement] = {} - correction_function: dict[int, set[int]] = defaultdict(set) - - for cmd in self.__seq: - if cmd.kind == CommandKind.N: - nodes.add(cmd.node) - elif cmd.kind == CommandKind.E: - u, v = cmd.nodes - if u > v: - u, v = v, u - edges.symmetric_difference_update({(u, v)}) - elif cmd.kind == CommandKind.M: - node = cmd.node - measurements[node] = Measurement(cmd.angle, cmd.plane) - if cmd.plane in {Plane.XZ, Plane.YZ}: - correction_function[node].add(node) - elif cmd.kind == CommandKind.X: - corrected_node = cmd.node - for measured_node in cmd.domain: - correction_function[measured_node].add(corrected_node) - - graph = nx.Graph(edges) - graph.add_nodes_from(nodes) - og = OpenGraph(graph, self.input_nodes, self.output_nodes, measurements) - - # Calling `self.extract_partial_order_layers` iterates over the pattern two more times. We sacrifice efficiency to avoid excessive code repetition. - partial_order_layers = self.extract_partial_order_layers() - if len(partial_order_layers) == 0: - raise ValueError("Pattern is empty.") - - gf = flow.GFlow(og, correction_function, partial_order_layers) - - if not gf.is_well_formed(): - raise ValueError("Pattern does not have gflow.") - return gf + return optimization.StandardizedPattern.from_pattern(self).extract_gflow() def get_layers(self) -> tuple[int, dict[int, set[int]]]: """Construct layers(l_k) from dependency information. diff --git a/tests/test_pattern.py b/tests/test_pattern.py index 6e1d71a80..ec03996de 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -923,16 +923,34 @@ class PatternFlowTestCase(NamedTuple): ] # Extract causal flow from random circuits - @pytest.mark.skip(reason="Fix transpilation #349") @pytest.mark.parametrize("jumps", range(1, 11)) - def test_extract_causal_flow_0(self, fx_bg: PCG64, jumps: int) -> None: + def test_extract_causal_flow_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None: rng = Generator(fx_bg.jumped(jumps)) - nqubits = 4 + nqubits = 2 depth = 2 - circuit_1 = rand_circuit(nqubits, depth, rng, use_ccx=True) + circuit_1 = rand_circuit(nqubits, depth, rng, use_ccx=False) p_ref = circuit_1.transpile().pattern - cf = p_ref.extract_gflow() - p_test = cf.to_corrections().to_pattern() + p_test = p_ref.extract_causal_flow().to_corrections().to_pattern() + + p_ref.perform_pauli_measurements() + p_test.perform_pauli_measurements() + + s_ref = p_ref.simulate_pattern(rng=rng) + s_test = p_test.simulate_pattern(rng=rng) + assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + + # Extract gflow from random circuits + @pytest.mark.parametrize("jumps", range(1, 11)) + def test_extract_gflow_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None: + rng = Generator(fx_bg.jumped(jumps)) + nqubits = 2 + depth = 2 + circuit_1 = rand_circuit(nqubits, depth, rng, use_ccx=False) + p_ref = circuit_1.transpile().pattern + p_test = p_ref.extract_gflow().to_corrections().to_pattern() + + p_ref.perform_pauli_measurements() + p_test.perform_pauli_measurements() s_ref = p_ref.simulate_pattern(rng=rng) s_test = p_test.simulate_pattern(rng=rng) From 6e53b7eb9177348d9f409db311e7f6c1666febb3 Mon Sep 17 00:00:00 2001 From: matulni Date: Wed, 3 Dec 2025 16:50:00 +0100 Subject: [PATCH 17/58] Adapt visualization to new API --- graphix/optimization.py | 2 - graphix/visualization.py | 111 ++++++++++++++++++++---------------- tests/test_optimization.py | 6 +- tests/test_transpiler.py | 6 +- tests/test_visualization.py | 9 ++- 5 files changed, 71 insertions(+), 63 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index 05e2e77b0..ae13d79e8 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -492,8 +492,6 @@ def extract_gflow(self) -> GFlow[Measurement]: ValueError If the pattern is empty or if the extracted structure does not satisfy the well-formedness conditions required for a valid gflow. - RunnabilityError - If the pattern is not runnable. Notes ----- diff --git a/graphix/visualization.py b/graphix/visualization.py index 250e21943..8499064dc 100644 --- a/graphix/visualization.py +++ b/graphix/visualization.py @@ -13,6 +13,8 @@ from graphix import gflow from graphix.fundamentals import Plane from graphix.measurements import PauliMeasurement +from graphix.opengraph import OpenGraph +from graphix.optimization import StandardizedPattern if TYPE_CHECKING: from collections.abc import Callable, Collection, Hashable, Iterable, Mapping, Sequence @@ -24,6 +26,8 @@ # MEMO: Potential circular import from graphix.clifford import Clifford + from graphix.flow.core import CausalFlow, GFlow + from graphix.fundamentals import AbstractPlanarMeasurement from graphix.parameter import ExpressionOrFloat from graphix.pattern import Pattern @@ -127,28 +131,38 @@ def visualize( If not None, filename of the png file to save the plot. If None, the plot is not saved. Default in None. """ - f, l_k = gflow.find_flow(self.graph, set(self.v_in), set(self.v_out), meas_planes=self.meas_planes) # try flow - if f is not None and l_k is not None: + og = OpenGraph(self.graph, list(self.v_in), list(self.v_out), self.meas_planes) + causal_flow = og.find_causal_flow() + if causal_flow is not None: print("Flow detected in the graph.") - pos = self.get_pos_from_flow(f, l_k) + pos = self.get_pos_from_flow(causal_flow) + cf = causal_flow.correction_function + l_k = { + node: layer_idx for layer_idx, layer in enumerate(causal_flow.partial_order_layers) for node in layer + } def get_paths( pos: Mapping[int, _Point], ) -> tuple[Mapping[_Edge, Sequence[_Point]], Mapping[_Edge, Sequence[_Point]] | None]: - return self.get_edge_path(f, pos) + return self.get_edge_path(cf, pos) + else: - g, l_k = gflow.find_gflow(self.graph, set(self.v_in), set(self.v_out), self.meas_planes) # try gflow - if g is not None and l_k is not None: + g_flow = og.find_gflow() + if g_flow is not None: print("Gflow detected in the graph. (flow not detected)") - pos = self.get_pos_from_gflow(g, l_k) + pos = self.get_pos_from_gflow(g_flow) + cf = g_flow.correction_function + l_k = {node: layer_idx for layer_idx, layer in enumerate(g_flow.partial_order_layers) for node in layer} def get_paths( pos: Mapping[int, _Point], ) -> tuple[Mapping[_Edge, Sequence[_Point]], Mapping[_Edge, Sequence[_Point]] | None]: - return self.get_edge_path(g, pos) + return self.get_edge_path(cf, pos) + else: print("No flow or gflow detected in the graph.") pos = self.get_pos_wo_structure() + l_k = None def get_paths( pos: Mapping[int, _Point], @@ -207,30 +221,26 @@ def visualize_from_pattern( If not None, filename of the png file to save the plot. If None, the plot is not saved. Default in None. """ - f, l_k = gflow.flow_from_pattern(pattern) # try flow - if f is not None and l_k is not None: - print("The pattern is consistent with flow structure.") - pos = self.get_pos_from_flow(f, l_k) - - def get_paths( - pos: Mapping[int, _Point], - ) -> tuple[Mapping[_Edge, Sequence[_Point]], Mapping[_Edge, Sequence[_Point]] | None]: - return self.get_edge_path(f, pos) + pattern_std = StandardizedPattern.from_pattern(pattern) + try: + causal_flow = pattern_std.extract_causal_flow() + print("The pattern is consistent with flow structure.") + pos = self.get_pos_from_flow(causal_flow) + cf = causal_flow.correction_function + l_k = { + node: layer_idx for layer_idx, layer in enumerate(causal_flow.partial_order_layers) for node in layer + } corrections: tuple[Mapping[int, AbstractSet[int]], Mapping[int, AbstractSet[int]]] | None = None - else: - g, l_k = gflow.gflow_from_pattern(pattern) # try gflow - if g is not None and l_k is not None: + except ValueError: + try: + g_flow = pattern_std.extract_gflow() print("The pattern is consistent with gflow structure. (not with flow)") - pos = self.get_pos_from_gflow(g, l_k) - - def get_paths( - pos: Mapping[int, _Point], - ) -> tuple[Mapping[_Edge, Sequence[_Point]], Mapping[_Edge, Sequence[_Point]] | None]: - return self.get_edge_path(g, pos) - + pos = self.get_pos_from_gflow(g_flow) + cf = g_flow.correction_function + l_k = {node: layer_idx for layer_idx, layer in enumerate(g_flow.partial_order_layers) for node in layer} corrections = None - else: + except ValueError: print("The pattern is not consistent with flow or gflow structure.") po_layers = pattern.extract_partial_order_layers() unfolded_layers = {node: layer_idx for layer_idx, layer in enumerate(po_layers[::-1]) for node in layer} @@ -242,13 +252,15 @@ def get_paths( else: xzflow[key] = set(value) # copy pos = self.get_pos_all_correction(unfolded_layers) + cf = xzflow + l_k = None + corrections = xflow, zflow - def get_paths( - pos: Mapping[int, _Point], - ) -> tuple[Mapping[_Edge, Sequence[_Point]], Mapping[_Edge, Sequence[_Point]] | None]: - return self.get_edge_path(xzflow, pos) + def get_paths( + pos: Mapping[int, _Point], + ) -> tuple[Mapping[_Edge, Sequence[_Point]], Mapping[_Edge, Sequence[_Point]] | None]: + return self.get_edge_path(cf, pos) - corrections = xflow, zflow self.visualize_graph( pos, get_paths, @@ -509,7 +521,7 @@ def get_figsize( return (width * node_distance[0], height * node_distance[1]) def get_edge_path( - self, flow: Mapping[int, int | set[int]], pos: Mapping[int, _Point] + self, flow: Mapping[int, AbstractSet[int]], pos: Mapping[int, _Point] ) -> tuple[dict[_Edge, list[_Point]], dict[_Edge, list[_Point]]]: """ Return the path of edges and gflow arrows. @@ -531,7 +543,7 @@ def get_edge_path( edge_path = self.get_edge_path_wo_structure(pos) edge_set = set(self.graph.edges()) arrow_path: dict[_Edge, list[_Point]] = {} - flow_arrows = {(k, v) for k, values in flow.items() for v in ((values,) if isinstance(values, int) else values)} + flow_arrows = {(k, v) for k, values in flow.items() for v in values} for arrow in flow_arrows: if arrow[0] == arrow[1]: # Self loop @@ -630,7 +642,7 @@ def get_edge_path_wo_structure(self, pos: Mapping[int, _Point]) -> dict[_Edge, l """ return {edge: self._find_bezier_path(edge, [pos[edge[0]], pos[edge[1]]], pos) for edge in self.graph.edges()} - def get_pos_from_flow(self, f: Mapping[int, set[int]], l_k: Mapping[int, int]) -> dict[int, _Point]: + def get_pos_from_flow(self, flow: CausalFlow[AbstractPlanarMeasurement]) -> dict[int, _Point]: """ Return the position of nodes based on the flow. @@ -646,6 +658,7 @@ def get_pos_from_flow(self, f: Mapping[int, set[int]], l_k: Mapping[int, int]) - pos : dict dictionary of node positions. """ + f = flow.correction_function values_union = set().union(*f.values()) start_nodes = set(self.graph.nodes()) - values_union pos = {node: [0, 0] for node in self.graph.nodes()} @@ -656,16 +669,15 @@ def get_pos_from_flow(self, f: Mapping[int, set[int]], l_k: Mapping[int, int]) - node = next(iter(f[node])) pos[node][1] = i - if not l_k: - return {} - - lmax = max(l_k.values()) + layers = flow.partial_order_layers + lmax = len(layers) - 1 # Change the x coordinates of the nodes based on their layer, sort in descending order - for node, layer in l_k.items(): - pos[node][0] = lmax - layer + for layer_idx, layer in enumerate(layers): + for node in layer: + pos[node][0] = lmax - layer_idx return {k: (x, y) for k, (x, y) in pos.items()} - def get_pos_from_gflow(self, g: Mapping[int, set[int]], l_k: Mapping[int, int]) -> dict[int, _Point]: + def get_pos_from_gflow(self, flow: GFlow[AbstractPlanarMeasurement]) -> dict[int, _Point]: """ Return the position of nodes based on the gflow. @@ -683,6 +695,8 @@ def get_pos_from_gflow(self, g: Mapping[int, set[int]], l_k: Mapping[int, int]) """ g_edges: list[_Edge] = [] + g = flow.correction_function + for node, node_list in g.items(): g_edges.extend((node, n) for n in node_list) @@ -690,15 +704,16 @@ def get_pos_from_gflow(self, g: Mapping[int, set[int]], l_k: Mapping[int, int]) g_prime.add_nodes_from(self.graph.nodes()) g_prime.add_edges_from(g_edges) - l_max = max(l_k.values()) - l_reverse = {v: l_max - l for v, l in l_k.items()} + layers = flow.partial_order_layers + l_max = len(layers) - 1 + l_reverse = {node: l_max - layer_idx for layer_idx, layer in enumerate(layers) for node in layer} _set_node_attributes(g_prime, l_reverse, "subset") - pos = nx.multipartite_layout(g_prime) - for node, layer in l_k.items(): - pos[node][0] = l_max - layer + for layer_idx, layer in enumerate(layers): + for node in layer: + pos[node][0] = l_max - layer_idx vert = list({pos[node][1] for node in self.graph.nodes()}) vert.sort() diff --git a/tests/test_optimization.py b/tests/test_optimization.py index 7693845df..1e2fdf18b 100644 --- a/tests/test_optimization.py +++ b/tests/test_optimization.py @@ -7,7 +7,6 @@ from graphix.clifford import Clifford from graphix.command import C, Command, CommandKind, E, M, N, X, Z from graphix.fundamentals import Plane -from graphix.gflow import gflow_from_pattern from graphix.optimization import StandardizedPattern, incorporate_pauli_results, remove_useless_domains from graphix.pattern import Pattern from graphix.random_objects import rand_circuit @@ -81,9 +80,8 @@ def test_flow_after_pauli_preprocessing(fx_bg: PCG64, jumps: int) -> None: # pattern.move_pauli_measurements_to_the_front() pattern.perform_pauli_measurements() pattern2 = incorporate_pauli_results(pattern) - pattern2.standardize() - f, _l = gflow_from_pattern(pattern2) - assert f is not None + gflow = pattern2.extract_gflow() + gflow.check_well_formed() @pytest.mark.parametrize("jumps", range(1, 11)) diff --git a/tests/test_transpiler.py b/tests/test_transpiler.py index d12d0a21e..3cbddc6fd 100644 --- a/tests/test_transpiler.py +++ b/tests/test_transpiler.py @@ -8,7 +8,6 @@ from graphix import instruction from graphix.fundamentals import Plane -from graphix.gflow import flow_from_pattern from graphix.random_objects import rand_circuit, rand_gate, rand_state_vector from graphix.transpiler import Circuit @@ -182,9 +181,8 @@ def test_add_extend(self) -> None: def test_instruction_flow(self, fx_rng: Generator, instruction: InstructionTestCase) -> None: circuit = Circuit(3, instr=[instruction(fx_rng)]) pattern = circuit.transpile().pattern - pattern.standardize() - f, _l = flow_from_pattern(pattern) - assert f is not None + flow = pattern.extract_causal_flow() + flow.check_well_formed() @pytest.mark.parametrize("jumps", range(1, 11)) @pytest.mark.parametrize("instruction", INSTRUCTION_TEST_CASES) diff --git a/tests/test_visualization.py b/tests/test_visualization.py index e0a9e2120..bc41e2633 100644 --- a/tests/test_visualization.py +++ b/tests/test_visualization.py @@ -9,7 +9,7 @@ import networkx as nx import pytest -from graphix import Circuit, Pattern, command, gflow, visualization +from graphix import Circuit, Pattern, command, visualization from graphix.fundamentals import Plane from graphix.measurements import Measurement from graphix.opengraph import OpenGraph, OpenGraphError @@ -102,10 +102,9 @@ def test_get_pos_from_flow() -> None: meas_angles = pattern.get_angles() local_clifford = pattern.get_vops() vis = visualization.GraphVisualizer(graph, vin, vout, meas_planes, meas_angles, local_clifford) - f, l_k = gflow.find_flow(graph, set(vin), set(vout), meas_planes) - assert f is not None - assert l_k is not None - pos = vis.get_pos_from_flow(f, l_k) + og = OpenGraph(graph, vin, vout, meas_planes) + causal_flow = og.extract_causal_flow() + pos = vis.get_pos_from_flow(causal_flow) assert pos is not None From 5165226fa5ac7e7809a13ef78846a04ef92b4e9e Mon Sep 17 00:00:00 2001 From: matulni Date: Wed, 3 Dec 2025 17:23:57 +0100 Subject: [PATCH 18/58] Remove dependence on graphix.gflow from graphix.pattern --- graphix/pattern.py | 53 +++------------------------------------------- 1 file changed, 3 insertions(+), 50 deletions(-) diff --git a/graphix/pattern.py b/graphix/pattern.py index 4ba21a55d..1ac4e3da1 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -8,6 +8,7 @@ import copy import dataclasses import enum +import itertools import warnings from collections.abc import Iterable, Iterator from dataclasses import dataclass @@ -24,7 +25,6 @@ from graphix.command import Command, CommandKind from graphix.flow._partial_order import compute_topological_generations from graphix.fundamentals import Axis, Plane, Sign -from graphix.gflow import find_flow, find_gflow, get_layers from graphix.graphsim import GraphState from graphix.measurements import Measurement, Outcome, PauliMeasurement, toggle_outcome from graphix.opengraph import OpenGraph @@ -1096,54 +1096,6 @@ def _measurement_order_space(self) -> list[int]: edges -= removable_edges return meas_order - def get_measurement_order_from_flow(self) -> list[int] | None: - """Return a measurement order generated from flow. If a graph has flow, the minimum 'max_space' of a pattern is guaranteed to width+1. - - Returns - ------- - meas_order: list of int - measurement order - """ - graph = self.extract_graph() - vin = set(self.input_nodes) - vout = set(self.output_nodes) - meas_planes = self.get_meas_plane() - f, l_k = find_flow(graph, vin, vout, meas_planes=meas_planes) - if f is None or l_k is None: - return None - depth, layer = get_layers(l_k) - meas_order: list[int] = [] - for i in range(depth): - k = depth - i - nodes = layer[k] - meas_order += nodes # NOTE this is list concatenation - return meas_order - - def get_measurement_order_from_gflow(self) -> list[int]: - """Return a list containing the node indices, in the order of measurements which can be performed with minimum depth. - - Returns - ------- - meas_order : list of int - measurement order - """ - graph = self.extract_graph() - isolated = list(nx.isolates(graph)) - if isolated: - raise ValueError("The input graph must be connected") - vin = set(self.input_nodes) - vout = set(self.output_nodes) - meas_planes = self.get_meas_plane() - flow, l_k = find_gflow(graph, vin, vout, meas_planes=meas_planes) - if flow is None or l_k is None: # We check both to avoid typing issues with `get_layers`. - raise ValueError("No gflow found") - k, layers = get_layers(l_k) - meas_order: list[int] = [] - while k > 0: - meas_order.extend(layers[k]) - k -= 1 - return meas_order - def sort_measurement_commands(self, meas_order: list[int]) -> list[command.M]: """Convert measurement order to sequence of measurement commands. @@ -1390,7 +1342,8 @@ def minimize_space(self) -> None: self.standardize() meas_order = None if not self._pauli_preprocessed: - meas_order = self.get_measurement_order_from_flow() + cf = self.extract_opengraph().find_causal_flow() + meas_order = list(itertools.chain(*reversed(cf.partial_order_layers[1:]))) if cf is not None else None if meas_order is None: meas_order = self._measurement_order_space() self._reorder_pattern(self.sort_measurement_commands(meas_order)) From 1cbdc95d02cc96ee0dad45318c9eb89bfc5ed602 Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 4 Dec 2025 16:22:05 +0100 Subject: [PATCH 19/58] Add feddback meeting --- graphix/flow/core.py | 246 +++-------------------------------- graphix/flow/exceptions.py | 256 +++++++++++++++++++++++++++++++++++++ tests/test_flow_core.py | 46 ++----- 3 files changed, 287 insertions(+), 261 deletions(-) create mode 100644 graphix/flow/exceptions.py diff --git a/graphix/flow/core.py b/graphix/flow/core.py index b3385c032..2a83fefa9 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -2,12 +2,10 @@ from __future__ import annotations -import enum from collections.abc import Sequence from copy import copy from dataclasses import dataclass -from enum import Enum -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import TYPE_CHECKING, Generic import networkx as nx @@ -22,6 +20,21 @@ _PM_co, compute_partial_order_layers, ) +from graphix.flow.exceptions import ( + CorrectionFunctionError, + CorrectionFunctionErrorReason, + FlowError, + FlowGenericError, + FlowGenericErrorReason, + FlowPropositionError, + FlowPropositionErrorReason, + FlowPropositionOrderError, + FlowPropositionOrderErrorReason, + PartialOrderError, + PartialOrderErrorReason, + PartialOrderLayerError, + PartialOrderLayerErrorReason, +) from graphix.fundamentals import Axis, Plane if TYPE_CHECKING: @@ -787,7 +800,7 @@ def check_well_formed(self) -> None: meas = self.get_measurement_label(node) if meas != Plane.XY: - raise FlowError("Causal flow is only defined on open graphs with XY measurements.") + raise FlowGenericError(FlowGenericErrorReason.XYPlane) neighbors = self.og.neighbors(correction_set) @@ -918,228 +931,3 @@ def _check_flow_general_properties(flow: PauliFlow[_M_co]) -> None: o_set = set(flow.og.output_nodes) if first_layer != o_set or not first_layer: raise PartialOrderLayerError(PartialOrderLayerErrorReason.FirstLayer, layer_index=0, layer=first_layer) - - -class FlowErrorReason: - """Describe the reason of a `FlowError`.""" - - -class CorrectionFunctionErrorReason(FlowErrorReason, Enum): - """Describe the reason of a `CorrectionFunctionError` exception.""" - - IncorrectDomain = enum.auto() - """The domain of the correction function is not the set of non-output nodes (measured qubits) of the open graph.""" - - IncorrectImage = enum.auto() - """The image of the correction function is not a subset of non-input nodes (prepared qubits) of the open graph.""" - - -class FlowPropositionErrorReason(FlowErrorReason, Enum): - """Describe the reason of a `FlowPropositionError` exception.""" - - C0 = enum.auto() - """A correction set in a causal flow has more than one element.""" - - C1 = enum.auto() - """Causal flow (C1). A node and its corrector must be neighbors.""" - - G3 = enum.auto() - """Gflow (G3). Nodes measured on plane XY cannot be in their own correcting set and must belong to the odd neighbourhood of their own correcting set.""" - - G4 = enum.auto() - """Gflow (G4). Nodes measured on plane XZ must belong to their own correcting set and its odd neighbourhood.""" - - G5 = enum.auto() - """Gflow (G5). Nodes measured on plane YZ must belong to their own correcting set and cannot be in the odd neighbourhood of their own correcting set.""" - - P4 = enum.auto() - """Pauli flow (P4). Equivalent to (G3) but for Pauli flows.""" - - P5 = enum.auto() - """Pauli flow (P5). Equivalent to (G4) but for Pauli flows.""" - - P6 = enum.auto() - """Pauli flow (P6). Equivalent to (G5) but for Pauli flows.""" - - P7 = enum.auto() - """Pauli flow (P7). Nodes measured along axis X must belong to the odd neighbourhood of their own correcting set.""" - - P8 = enum.auto() - """Pauli flow (P8). Nodes measured along axis Z must belong to their own correcting set.""" - - P9 = enum.auto() - """Pauli flow (P9). Nodes measured along axis Y must belong to the closed odd neighbourhood of their own correcting set.""" - - -class FlowPropositionOrderErrorReason(FlowErrorReason, Enum): - """Describe the reason of a `FlowPropositionOrderError` exception.""" - - C2 = enum.auto() - """Causal flow (C2). Nodes must be in the past of their correction set.""" - - C3 = enum.auto() - """Causal flow (C3). Neighbors of the correcting nodes (except the corrected node) must be in the future of the corrected node.""" - - G1 = enum.auto() - """Gflow (G1). Equivalent to (C2) but for gflows.""" - - G2 = enum.auto() - """Gflow (G2). The odd neighbourhood (except the corrected node) of the correcting nodes must be in the future of the corrected node.""" - - P1 = enum.auto() - """Pauli flow (P1). Nodes must be in the past of their correcting nodes that are not measured along the X or the Y axes.""" - - P2 = enum.auto() - """Pauli flow (P2). The odd neighbourhood (except the corrected node and nodes measured along axes Y or Z) of the correcting nodes must be in the future of the corrected node.""" - - P3 = enum.auto() - """Pauli flow (P3). Nodes that are measured along axis Y and that are not in the future of the corrected node (except the corrected node itself) cannot be in the closed odd neighbourhood of the correcting set.""" - - -# NOTE: In the near future, this class may inherit from `XZCorrectionsErrorReason` too. -class PartialOrderErrorReason(FlowErrorReason, Enum): - """Describe the reason of a `PartialOrderError` exception.""" - - Empty = enum.auto() - """The partial order is empty.""" - - IncorrectNodes = enum.auto() - """The partial order does not contain all the nodes of the open graph or contains nodes that are not in the open graph.""" - - -class PartialOrderLayerErrorReason(FlowErrorReason, Enum): - """Describe the reason of a `PartialOrderLayerError` exception.""" - - FirstLayer = enum.auto() - """The first layer of the partial order is not the set of output nodes (non-measured qubits) of the open graph or is empty.""" # A well-defined flow cannot exist on an open graph without outputs. - - NthLayer = enum.auto() - """Nodes in the partial order beyond the first layer are not non-output nodes (measured qubits) of the open graph, layer is empty or contains duplicates.""" - - -# We bind `_Reason` to `str` to allow passing generic strings to a `FlowError` exception. -_Reason = TypeVar("_Reason", bound=FlowErrorReason | str) - - -@dataclass -class FlowError(Exception, Generic[_Reason]): - """Exception subclass to handle flow errors.""" - - reason: _Reason - - -@dataclass -class CorrectionFunctionError(FlowError[CorrectionFunctionErrorReason]): - """Exception subclass to handle general flow errors in the correction function.""" - - def __str__(self) -> str: - """Explain the error.""" - if self.reason == CorrectionFunctionErrorReason.IncorrectDomain: - return "The domain of the correction function must be the set of non-output nodes (measured qubits) of the open graph." - - if self.reason == CorrectionFunctionErrorReason.IncorrectImage: - return "The image of the correction function must be a subset of non-input nodes (prepared qubits) of the open graph." - - assert_never(self.reason) - - -@dataclass -class FlowPropositionError(FlowError[FlowPropositionErrorReason]): - """Exception subclass to handle violations of the flow-definition propositions which concern the correction function only (C0, C1, G1, G3, G4, G5, P4, P5, P6, P7, P8, P9).""" - - node: int - correction_set: AbstractSet[int] - - def __str__(self) -> str: - """Explain the error.""" - error_help = f"Error found at c({self.node}) = {self.correction_set}." - - if self.reason == FlowPropositionErrorReason.C0: - return f"Correction set c({self.node}) = {self.correction_set} has more than one element." - - if self.reason == FlowPropositionErrorReason.C1: - return f"{self.reason.name}: a node and its corrector must be neighbors. {error_help}" - - if self.reason == FlowPropositionErrorReason.G3 or self.reason == FlowPropositionErrorReason.P4: # noqa: PLR1714 - return f"{self.reason.name}: nodes measured on plane XY cannot be in their own correcting set and must belong to the odd neighbourhood of their own correcting set.\n{error_help}" - - if self.reason == FlowPropositionErrorReason.G4 or self.reason == FlowPropositionErrorReason.P5: # noqa: PLR1714 - return f"{self.reason.name}: nodes measured on plane XZ must belong to their own correcting set and its odd neighbourhood.\n{error_help}" - - if self.reason == FlowPropositionErrorReason.G5 or self.reason == FlowPropositionErrorReason.P6: # noqa: PLR1714 - return f"{self.reason.name}: nodes measured on plane YZ must belong to their own correcting set and cannot be in the odd neighbourhood of their own correcting set.\n{error_help}" - - if self.reason == FlowPropositionErrorReason.P7: - return f"{self.reason.name}: nodes measured along axis X must belong to the odd neighbourhood of their own correcting set.\n{error_help}" - - if self.reason == FlowPropositionErrorReason.P8: - return f"{self.reason.name}: nodes measured along axis Z must belong to their own correcting set.\n{error_help}" - - if self.reason == FlowPropositionErrorReason.P9: - return f"{self.reason.name}: nodes measured along axis Y must belong to the closed odd neighbourhood of their own correcting set.\n{error_help}" - - assert_never(self.reason) - - -@dataclass -class FlowPropositionOrderError(FlowError[FlowPropositionOrderErrorReason]): - """Exception subclass to handle violations of the flow-definition propositions which concern the correction function and the partial order (C2, C3, G1, G2, P1, P2, P3).""" - - node: int - correction_set: AbstractSet[int] - past_and_present_nodes: AbstractSet[int] - - def __str__(self) -> str: - """Explain the error.""" - error_help = f"Error found at c({self.node}) = {self.correction_set}. Partial order: {self.past_and_present_nodes} ≼ {self.node}." - - if self.reason == FlowPropositionOrderErrorReason.C2 or self.reason == FlowPropositionOrderErrorReason.G1: # noqa: PLR1714 - return f"{self.reason.name}: nodes must be in the past of their correction set.\n{error_help}" - - if self.reason == FlowPropositionOrderErrorReason.C3: - return f"{self.reason.name}: neighbors of the correcting nodes (except the corrected node) must be in the future of the corrected node.\n{error_help}" - - if self.reason == FlowPropositionOrderErrorReason.G2: - return f"{self.reason.name}: the odd neighbourhood (except the corrected node) of the correcting nodes must be in the future of the corrected node.\n{error_help}" - - if self.reason == FlowPropositionOrderErrorReason.P1: - return f"{self.reason.name}: nodes must be in the past of their correcting nodes unless these are measured along the X or the Y axes.\n{error_help}" - - if self.reason == FlowPropositionOrderErrorReason.P2: - return f"{self.reason.name}: the odd neighbourhood (except the corrected node and nodes measured along axes Y or Z) of the correcting nodes must be in the future of the corrected node.\n{error_help}" - - if self.reason == FlowPropositionOrderErrorReason.P3: - return f"{self.reason.name}: nodes that are measured along axis Y and that are not in the future of the corrected node (except the corrected node itself) cannot be in the closed odd neighbourhood of the correcting set.\nError found at c({self.node}) = {self.correction_set}. Partial order for Y-measured nodes: {self.past_and_present_nodes} ≼ {self.node}." - - assert_never(self.reason) - - -@dataclass -class PartialOrderError(FlowError[PartialOrderErrorReason]): - """Exception subclass to handle general flow errors in the partial order.""" - - def __str__(self) -> str: - """Explain the error.""" - if self.reason == PartialOrderErrorReason.Empty: - return "The partial order cannot be empty." - - if self.reason == PartialOrderErrorReason.IncorrectNodes: - return "The partial order does not contain all the nodes of the open graph or contains nodes that are not in the open graph." - assert_never(self.reason) - - -@dataclass -class PartialOrderLayerError(FlowError[PartialOrderLayerErrorReason]): - """Exception subclass to handle flow errors concerning a specific layer of the partial order.""" - - layer_index: int - layer: AbstractSet[int] - - def __str__(self) -> str: - """Explain the error.""" - if self.reason == PartialOrderLayerErrorReason.FirstLayer: - return f"The first layer of the partial order must contain all the output nodes of the open graph and cannot be empty. First layer: {self.layer}" - - if self.reason == PartialOrderLayerErrorReason.NthLayer: - return f"Partial order layer {self.layer_index} = {self.layer} contains non-measured nodes of the open graph, is empty or contains nodes in previous layers." - assert_never(self.reason) diff --git a/graphix/flow/exceptions.py b/graphix/flow/exceptions.py new file mode 100644 index 000000000..931fd64e7 --- /dev/null +++ b/graphix/flow/exceptions.py @@ -0,0 +1,256 @@ +"""Module for flows and XZ-corrections exceptions.""" + +from __future__ import annotations + +import enum +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING + +# `override` introduced in Python 3.12, `assert_never` introduced in Python 3.11 +from typing_extensions import assert_never + +if TYPE_CHECKING: + from collections.abc import Set as AbstractSet + + +class CorrectionFunctionErrorReason(Enum): + """Describe the reason of a `CorrectionFunctionError` exception.""" + + IncorrectDomain = enum.auto() + """The domain of the correction function is not the set of non-output nodes (measured qubits) of the open graph.""" + + IncorrectImage = enum.auto() + """The image of the correction function is not a subset of non-input nodes (prepared qubits) of the open graph.""" + + +class FlowPropositionErrorReason(Enum): + """Describe the reason of a `FlowPropositionError` exception.""" + + C0 = enum.auto() + """A correction set in a causal flow has more than one element.""" + + C1 = enum.auto() + """Causal flow (C1). A node and its corrector must be neighbors.""" + + G3 = enum.auto() + """Gflow (G3). Nodes measured on plane XY cannot be in their own correcting set and must belong to the odd neighbourhood of their own correcting set.""" + + G4 = enum.auto() + """Gflow (G4). Nodes measured on plane XZ must belong to their own correcting set and its odd neighbourhood.""" + + G5 = enum.auto() + """Gflow (G5). Nodes measured on plane YZ must belong to their own correcting set and cannot be in the odd neighbourhood of their own correcting set.""" + + P4 = enum.auto() + """Pauli flow (P4). Equivalent to (G3) but for Pauli flows.""" + + P5 = enum.auto() + """Pauli flow (P5). Equivalent to (G4) but for Pauli flows.""" + + P6 = enum.auto() + """Pauli flow (P6). Equivalent to (G5) but for Pauli flows.""" + + P7 = enum.auto() + """Pauli flow (P7). Nodes measured along axis X must belong to the odd neighbourhood of their own correcting set.""" + + P8 = enum.auto() + """Pauli flow (P8). Nodes measured along axis Z must belong to their own correcting set.""" + + P9 = enum.auto() + """Pauli flow (P9). Nodes measured along axis Y must belong to the closed odd neighbourhood of their own correcting set.""" + + +class FlowPropositionOrderErrorReason(Enum): + """Describe the reason of a `FlowPropositionOrderError` exception.""" + + C2 = enum.auto() + """Causal flow (C2). Nodes must be in the past of their correction set.""" + + C3 = enum.auto() + """Causal flow (C3). Neighbors of the correcting nodes (except the corrected node) must be in the future of the corrected node.""" + + G1 = enum.auto() + """Gflow (G1). Equivalent to (C2) but for gflows.""" + + G2 = enum.auto() + """Gflow (G2). The odd neighbourhood (except the corrected node) of the correcting nodes must be in the future of the corrected node.""" + + P1 = enum.auto() + """Pauli flow (P1). Nodes must be in the past of their correcting nodes that are not measured along the X or the Y axes.""" + + P2 = enum.auto() + """Pauli flow (P2). The odd neighbourhood (except the corrected node and nodes measured along axes Y or Z) of the correcting nodes must be in the future of the corrected node.""" + + P3 = enum.auto() + """Pauli flow (P3). Nodes that are measured along axis Y and that are not in the future of the corrected node (except the corrected node itself) cannot be in the closed odd neighbourhood of the correcting set.""" + + +class FlowGenericErrorReason(Enum): + """Describe the reason of a `FlowGenericError`.""" + + XYPlane = enum.auto() + "A causal flow is defined on an open graphs with non-XY measurements." + + +class PartialOrderErrorReason(Enum): + """Describe the reason of a `PartialOrderError` exception.""" + + Empty = enum.auto() + """The partial order is empty.""" + + IncorrectNodes = enum.auto() + """The partial order does not contain all the nodes of the open graph or contains nodes that are not in the open graph.""" + + +class PartialOrderLayerErrorReason(Enum): + """Describe the reason of a `PartialOrderLayerError` exception.""" + + FirstLayer = enum.auto() + """The first layer of the partial order is not the set of output nodes (non-measured qubits) of the open graph or is empty.""" # A well-defined flow cannot exist on an open graph without outputs. + + NthLayer = enum.auto() + """Nodes in the partial order beyond the first layer are not non-output nodes (measured qubits) of the open graph, layer is empty or contains duplicates.""" + + +@dataclass +class FlowError(Exception): + """Exception subclass to handle flow errors.""" + + +@dataclass +class CorrectionFunctionError(FlowError): + """Exception subclass to handle general flow errors in the correction function.""" + + reason: CorrectionFunctionErrorReason + + def __str__(self) -> str: + """Explain the error.""" + if self.reason == CorrectionFunctionErrorReason.IncorrectDomain: + return "The domain of the correction function must be the set of non-output nodes (measured qubits) of the open graph." + + if self.reason == CorrectionFunctionErrorReason.IncorrectImage: + return "The image of the correction function must be a subset of non-input nodes (prepared qubits) of the open graph." + + assert_never(self.reason) + + +@dataclass +class FlowPropositionError(FlowError): + """Exception subclass to handle violations of the flow-definition propositions which concern the correction function only (C0, C1, G1, G3, G4, G5, P4, P5, P6, P7, P8, P9).""" + + reason: FlowPropositionErrorReason + node: int + correction_set: AbstractSet[int] + + def __str__(self) -> str: + """Explain the error.""" + error_help = f"Error found at c({self.node}) = {self.correction_set}." + + if self.reason == FlowPropositionErrorReason.C0: + return f"Correction set c({self.node}) = {self.correction_set} has more than one element." + + if self.reason == FlowPropositionErrorReason.C1: + return f"{self.reason.name}: a node and its corrector must be neighbors. {error_help}" + + if self.reason == FlowPropositionErrorReason.G3 or self.reason == FlowPropositionErrorReason.P4: # noqa: PLR1714 + return f"{self.reason.name}: nodes measured on plane XY cannot be in their own correcting set and must belong to the odd neighbourhood of their own correcting set.\n{error_help}" + + if self.reason == FlowPropositionErrorReason.G4 or self.reason == FlowPropositionErrorReason.P5: # noqa: PLR1714 + return f"{self.reason.name}: nodes measured on plane XZ must belong to their own correcting set and its odd neighbourhood.\n{error_help}" + + if self.reason == FlowPropositionErrorReason.G5 or self.reason == FlowPropositionErrorReason.P6: # noqa: PLR1714 + return f"{self.reason.name}: nodes measured on plane YZ must belong to their own correcting set and cannot be in the odd neighbourhood of their own correcting set.\n{error_help}" + + if self.reason == FlowPropositionErrorReason.P7: + return f"{self.reason.name}: nodes measured along axis X must belong to the odd neighbourhood of their own correcting set.\n{error_help}" + + if self.reason == FlowPropositionErrorReason.P8: + return f"{self.reason.name}: nodes measured along axis Z must belong to their own correcting set.\n{error_help}" + + if self.reason == FlowPropositionErrorReason.P9: + return f"{self.reason.name}: nodes measured along axis Y must belong to the closed odd neighbourhood of their own correcting set.\n{error_help}" + + assert_never(self.reason) + + +@dataclass +class FlowPropositionOrderError(FlowError): + """Exception subclass to handle violations of the flow-definition propositions which concern the correction function and the partial order (C2, C3, G1, G2, P1, P2, P3).""" + + reason: FlowPropositionOrderErrorReason + node: int + correction_set: AbstractSet[int] + past_and_present_nodes: AbstractSet[int] + + def __str__(self) -> str: + """Explain the error.""" + error_help = f"The flow's partial order implies that {self.past_and_present_nodes - {self.node}} ≼ {self.node}. This is incompatible with the correction set c({self.node}) = {self.correction_set}." + + if self.reason == FlowPropositionOrderErrorReason.C2 or self.reason == FlowPropositionOrderErrorReason.G1: # noqa: PLR1714 + return f"{self.reason.name}: nodes must be in the past of their correction set.\n{error_help}" + + if self.reason == FlowPropositionOrderErrorReason.C3: + return f"{self.reason.name}: neighbors of the correcting nodes (except the corrected node) must be in the future of the corrected node.\n{error_help}" + + if self.reason == FlowPropositionOrderErrorReason.G2: + return f"{self.reason.name}: the odd neighbourhood (except the corrected node) of the correcting nodes must be in the future of the corrected node.\n{error_help}" + + if self.reason == FlowPropositionOrderErrorReason.P1: + return f"{self.reason.name}: nodes must be in the past of their correcting nodes unless these are measured along the X or the Y axes.\n{error_help}" + + if self.reason == FlowPropositionOrderErrorReason.P2: + return f"{self.reason.name}: the odd neighbourhood (except the corrected node and nodes measured along axes Y or Z) of the correcting nodes must be in the future of the corrected node.\n{error_help}" + + if self.reason == FlowPropositionOrderErrorReason.P3: + return f"{self.reason.name}: nodes that are measured along axis Y and that are not in the future of the corrected node (except the corrected node itself) cannot be in the closed odd neighbourhood of the correcting set.\n{error_help}" + + assert_never(self.reason) + + +@dataclass +class FlowGenericError(FlowError): + """Exception subclass to handle generic flow errors.""" + + reason: FlowGenericErrorReason + + def __str__(self) -> str: + """Explain the error.""" + if self.reason == FlowGenericErrorReason.XYPlane: + return "Causal flow is only defined on open graphs with XY measurements." + + assert_never(self.reason) + + +@dataclass +class PartialOrderError(FlowError): + """Exception subclass to handle general flow errors in the partial order.""" + + reason: PartialOrderErrorReason + + def __str__(self) -> str: + """Explain the error.""" + if self.reason == PartialOrderErrorReason.Empty: + return "The partial order cannot be empty." + + if self.reason == PartialOrderErrorReason.IncorrectNodes: + return "The partial order does not contain all the nodes of the open graph or contains nodes that are not in the open graph." + assert_never(self.reason) + + +@dataclass +class PartialOrderLayerError(FlowError): + """Exception subclass to handle flow errors concerning a specific layer of the partial order.""" + + reason: PartialOrderLayerErrorReason + layer_index: int + layer: AbstractSet[int] + + def __str__(self) -> str: + """Explain the error.""" + if self.reason == PartialOrderLayerErrorReason.FirstLayer: + return f"The first layer of the partial order must contain all the output nodes of the open graph and cannot be empty. First layer: {self.layer}" + + if self.reason == PartialOrderLayerErrorReason.NthLayer: + return f"Partial order layer {self.layer_index} = {self.layer} contains non-measured nodes of the open graph, is empty or contains nodes in previous layers." + assert_never(self.reason) diff --git a/tests/test_flow_core.py b/tests/test_flow_core.py index 333538617..203d13dea 100644 --- a/tests/test_flow_core.py +++ b/tests/test_flow_core.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import fields from typing import TYPE_CHECKING, NamedTuple import networkx as nx @@ -9,20 +10,24 @@ from graphix.command import E, M, N, X, Z from graphix.flow.core import ( CausalFlow, + GFlow, + PauliFlow, + XZCorrections, +) +from graphix.flow.exceptions import ( CorrectionFunctionError, CorrectionFunctionErrorReason, FlowError, + FlowGenericError, + FlowGenericErrorReason, FlowPropositionError, FlowPropositionErrorReason, FlowPropositionOrderError, FlowPropositionOrderErrorReason, - GFlow, PartialOrderError, PartialOrderErrorReason, PartialOrderLayerError, PartialOrderLayerErrorReason, - PauliFlow, - XZCorrections, ) from graphix.fundamentals import AbstractMeasurement, AbstractPlanarMeasurement, Axis, Plane from graphix.measurements import Measurement @@ -598,19 +603,9 @@ def test_from_measured_nodes_mapping_exceptions(self) -> None: XZCorrections.from_measured_nodes_mapping(og=og, x_corrections={0: {4}}) -ErrorT = ( - FlowError[str] - | CorrectionFunctionError - | FlowPropositionError - | FlowPropositionOrderError - | PartialOrderError - | PartialOrderLayerError -) - - class IncorrectFlowTestCase(NamedTuple): flow: PauliFlow[AbstractMeasurement] - exception: ErrorT + exception: FlowError class TestIncorrectFlows: @@ -650,7 +645,7 @@ class TestIncorrectFlows: correction_function={0: {1}}, partial_order_layers=[{1}, {0}], ), - FlowError("Causal flow is only defined on open graphs with XY measurements."), + FlowGenericError(FlowGenericErrorReason.XYPlane), ), # Incomplete correction function IncorrectFlowTestCase( @@ -923,20 +918,7 @@ class TestIncorrectFlows: def test_check_flow_general_properties(self, test_case: IncorrectFlowTestCase) -> None: with pytest.raises(FlowError) as exc_info: test_case.flow.check_well_formed() - assert exc_info.value.reason == test_case.exception.reason - - if isinstance(test_case.exception, FlowPropositionError): - assert isinstance(exc_info.value, FlowPropositionError) - assert exc_info.value.node == test_case.exception.node - assert exc_info.value.correction_set == test_case.exception.correction_set - - if isinstance(test_case.exception, FlowPropositionOrderError): - assert isinstance(exc_info.value, FlowPropositionOrderError) - assert exc_info.value.node == test_case.exception.node - assert exc_info.value.correction_set == test_case.exception.correction_set - assert exc_info.value.past_and_present_nodes == test_case.exception.past_and_present_nodes - - if isinstance(test_case.exception, PartialOrderLayerError): - assert isinstance(exc_info.value, PartialOrderLayerError) - assert exc_info.value.layer_index == test_case.exception.layer_index - assert exc_info.value.layer == test_case.exception.layer + + for field in fields(exc_info.value): + attr = field.name + assert getattr(exc_info.value, attr) == getattr(test_case.exception, attr) From 3c821d915b7f9f89c98e06408cb1852e511a1132 Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 4 Dec 2025 17:13:35 +0100 Subject: [PATCH 20/58] Combine correction function errors and generic flow errors into a single class --- graphix/flow/core.py | 6 ++---- graphix/flow/exceptions.py | 39 ++++++++++++-------------------------- tests/test_flow_core.py | 6 ++---- 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index 2a83fefa9..4e698263a 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -21,8 +21,6 @@ compute_partial_order_layers, ) from graphix.flow.exceptions import ( - CorrectionFunctionError, - CorrectionFunctionErrorReason, FlowError, FlowGenericError, FlowGenericErrorReason, @@ -919,10 +917,10 @@ def _check_flow_general_properties(flow: PauliFlow[_M_co]) -> None: - The first layer of the partial order layers is :math:`O`, the output nodes of the open graph. This is guaranteed because open graphs without outputs do not have flow. """ if not _check_correction_function_domain(flow.og, flow.correction_function): - raise CorrectionFunctionError(CorrectionFunctionErrorReason.IncorrectDomain) + raise FlowGenericError(FlowGenericErrorReason.IncorrectCorrectionFunctionDomain) if not _check_correction_function_image(flow.og, flow.correction_function): - raise CorrectionFunctionError(CorrectionFunctionErrorReason.IncorrectImage) + raise FlowGenericError(FlowGenericErrorReason.IncorrectCorrectionFunctionImage) if len(flow.partial_order_layers) == 0: raise PartialOrderError(PartialOrderErrorReason.Empty) diff --git a/graphix/flow/exceptions.py b/graphix/flow/exceptions.py index 931fd64e7..624964dda 100644 --- a/graphix/flow/exceptions.py +++ b/graphix/flow/exceptions.py @@ -14,16 +14,6 @@ from collections.abc import Set as AbstractSet -class CorrectionFunctionErrorReason(Enum): - """Describe the reason of a `CorrectionFunctionError` exception.""" - - IncorrectDomain = enum.auto() - """The domain of the correction function is not the set of non-output nodes (measured qubits) of the open graph.""" - - IncorrectImage = enum.auto() - """The image of the correction function is not a subset of non-input nodes (prepared qubits) of the open graph.""" - - class FlowPropositionErrorReason(Enum): """Describe the reason of a `FlowPropositionError` exception.""" @@ -89,6 +79,12 @@ class FlowPropositionOrderErrorReason(Enum): class FlowGenericErrorReason(Enum): """Describe the reason of a `FlowGenericError`.""" + IncorrectCorrectionFunctionDomain = enum.auto() + """The domain of the correction function is not the set of non-output nodes (measured qubits) of the open graph.""" + + IncorrectCorrectionFunctionImage = enum.auto() + """The image of the correction function is not a subset of non-input nodes (prepared qubits) of the open graph.""" + XYPlane = enum.auto() "A causal flow is defined on an open graphs with non-XY measurements." @@ -118,23 +114,6 @@ class FlowError(Exception): """Exception subclass to handle flow errors.""" -@dataclass -class CorrectionFunctionError(FlowError): - """Exception subclass to handle general flow errors in the correction function.""" - - reason: CorrectionFunctionErrorReason - - def __str__(self) -> str: - """Explain the error.""" - if self.reason == CorrectionFunctionErrorReason.IncorrectDomain: - return "The domain of the correction function must be the set of non-output nodes (measured qubits) of the open graph." - - if self.reason == CorrectionFunctionErrorReason.IncorrectImage: - return "The image of the correction function must be a subset of non-input nodes (prepared qubits) of the open graph." - - assert_never(self.reason) - - @dataclass class FlowPropositionError(FlowError): """Exception subclass to handle violations of the flow-definition propositions which concern the correction function only (C0, C1, G1, G3, G4, G5, P4, P5, P6, P7, P8, P9).""" @@ -216,6 +195,12 @@ class FlowGenericError(FlowError): def __str__(self) -> str: """Explain the error.""" + if self.reason == FlowGenericErrorReason.IncorrectCorrectionFunctionDomain: + return "The domain of the correction function must be the set of non-output nodes (measured qubits) of the open graph." + + if self.reason == FlowGenericErrorReason.IncorrectCorrectionFunctionImage: + return "The image of the correction function must be a subset of non-input nodes (prepared qubits) of the open graph." + if self.reason == FlowGenericErrorReason.XYPlane: return "Causal flow is only defined on open graphs with XY measurements." diff --git a/tests/test_flow_core.py b/tests/test_flow_core.py index 203d13dea..5fa370663 100644 --- a/tests/test_flow_core.py +++ b/tests/test_flow_core.py @@ -15,8 +15,6 @@ XZCorrections, ) from graphix.flow.exceptions import ( - CorrectionFunctionError, - CorrectionFunctionErrorReason, FlowError, FlowGenericError, FlowGenericErrorReason, @@ -654,7 +652,7 @@ class TestIncorrectFlows: correction_function={0: {1}, 1: {2}}, partial_order_layers=[{3}, {2}, {1}, {0}], ), - CorrectionFunctionError(CorrectionFunctionErrorReason.IncorrectDomain), + FlowGenericError(FlowGenericErrorReason.IncorrectCorrectionFunctionDomain), ), # Extra node in correction function image IncorrectFlowTestCase( @@ -663,7 +661,7 @@ class TestIncorrectFlows: correction_function={0: {1}, 1: {2}, 2: {4}}, partial_order_layers=[{3}, {2}, {1}, {0}], ), - CorrectionFunctionError(CorrectionFunctionErrorReason.IncorrectImage), + FlowGenericError(FlowGenericErrorReason.IncorrectCorrectionFunctionImage), ), # Empty partial order IncorrectFlowTestCase( From c13057d5d57ed34298e8f7c08327aeba6b763f55 Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 8 Dec 2025 15:13:19 +0100 Subject: [PATCH 21/58] Remove MERGE_MSG --- MERGE_MSG | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 MERGE_MSG diff --git a/MERGE_MSG b/MERGE_MSG deleted file mode 100644 index 8374bc949..000000000 --- a/MERGE_MSG +++ /dev/null @@ -1,6 +0,0 @@ -Merge branch 'master' into rf_flow_iswellformed -# Please enter a commit message to explain why this merge is necessary, -# especially if it merges an updated upstream into a topic branch. -# -# Lines starting with '#' will be ignored, and an empty message aborts -# the commit. From 095a2ded6aeffec623eb085f71a1eef097d2666c Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 8 Dec 2025 15:20:08 +0100 Subject: [PATCH 22/58] Add comment --- graphix/flow/core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index e5e666de3..bb331404e 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -324,7 +324,9 @@ def check_well_formed(self) -> None: shift = 0 measured_layers = reversed(self.partial_order_layers[shift:]) - layer_idx = len(self.partial_order_layers) - 1 + layer_idx = ( + len(self.partial_order_layers) - 1 + ) # To keep track of the layer index when iterating `self.partial_order_layers` in reverse order. past_and_present_nodes: set[int] = set() for layer in measured_layers: From 91860fa80d2cd6013a4e61d4535fd5381644991c Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 8 Dec 2025 17:59:33 +0100 Subject: [PATCH 23/58] Up CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed6423ac8..8ad1bf569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added +- #385 + - Introduced `graphix.flow.core.XZCorrections.check_well_formed` which verifies the correctness of an XZ-corrections instance and raises an exception if incorrect. + - Added XZ-correction exceptions to module `graphix.flow.core.exceptions`. + - #378: - Introduced new method `graphix.flow.core.PauliFlow.check_well_formed`, `graphix.flow.core.GFlow.check_well_formed` and `graphix.flow.core.CausalFlow.check_well_formed` which verify the correctness of flow objects and raise exceptions when the flow is incorrect. - Introduced new method `graphix.flow.core.PauliFlow.is_well_formed` which verify the correctness of flow objects and returns a boolean when the flow is incorrect. From f0c8a3cddd11655988094afedc01425df7185804 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 9 Dec 2025 11:25:48 +0100 Subject: [PATCH 24/58] wip --- graphix/optimization.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index ae13d79e8..df4e87b6e 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -466,15 +466,9 @@ def process_domain(node: Node, domain: AbstractSet[Node]) -> None: process_domain(node, domain) partial_order_layers = self.extract_partial_order_layers() - if len(partial_order_layers) == 0: - raise ValueError("Pattern is empty.") - og = OpenGraph(self.extract_graph(), self.input_nodes, self.output_nodes, measurements) - cf = CausalFlow(og, correction_function, partial_order_layers) - - if not cf.is_well_formed(): - raise ValueError("Pattern does not have causal flow.") + cf.check_well_formed() return cf def extract_gflow(self) -> GFlow[Measurement]: @@ -515,15 +509,9 @@ def process_domain(node: Node, domain: AbstractSet[Node]) -> None: process_domain(node, domain) partial_order_layers = self.extract_partial_order_layers() - if len(partial_order_layers) == 0: - raise ValueError("Pattern is empty.") - og = OpenGraph(self.extract_graph(), self.input_nodes, self.output_nodes, measurements) - gf = GFlow(og, correction_function, partial_order_layers) - - if not gf.is_well_formed(): - raise ValueError("Pattern does not have gflow.") + gf.check_well_formed() return gf From 945029367c8609f9e30f1e291c23d9e4057f6a15 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 9 Dec 2025 14:47:36 +0100 Subject: [PATCH 25/58] Add FlowError exceptions in flow from pattern methods --- graphix/optimization.py | 16 +++++++++++----- graphix/visualization.py | 33 +++++++++++++++++++-------------- tests/test_pattern.py | 15 +++++---------- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index df4e87b6e..62a06aa62 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -20,6 +20,12 @@ from graphix.command import CommandKind, Node from graphix.flow._partial_order import compute_topological_generations from graphix.flow.core import CausalFlow, GFlow +from graphix.flow.exceptions import ( + FlowGenericError, + FlowGenericErrorReason, + FlowPropositionError, + FlowPropositionErrorReason, +) from graphix.fundamentals import Axis, Plane from graphix.measurements import Domains, Measurement, Outcome, PauliMeasurement from graphix.opengraph import OpenGraph @@ -434,7 +440,7 @@ def extract_causal_flow(self) -> CausalFlow[Measurement]: Raises ------ - ValueError + FlowError If the pattern: - contains measurements in forbidden planes (XZ or YZ), - assigns more than one correcting node to the same measured node, @@ -451,14 +457,14 @@ def extract_causal_flow(self) -> CausalFlow[Measurement]: def process_domain(node: Node, domain: AbstractSet[Node]) -> None: for measured_node in domain: if measured_node in correction_function: - raise ValueError( - f"Pattern does not have causal flow. Node {measured_node} is corrected by nodes {correction_function[measured_node].pop()} and {node} but correcting sets in causal flows can have one element only." + raise FlowPropositionError( + FlowPropositionErrorReason.C0, measured_node, {node, *correction_function[measured_node]} ) correction_function[measured_node] = {node} for m in self.m_list: if m.plane in {Plane.XZ, Plane.YZ}: - raise ValueError(f"Pattern does not have causal flow. Node {m.node} is measured in {m.plane}.") + raise FlowGenericError(FlowGenericErrorReason.XYPlane) measurements[m.node] = Measurement(m.angle, m.plane) process_domain(m.node, m.s_domain) @@ -483,7 +489,7 @@ def extract_gflow(self) -> GFlow[Measurement]: Raises ------ - ValueError + FlowError If the pattern is empty or if the extracted structure does not satisfy the well-formedness conditions required for a valid gflow. diff --git a/graphix/visualization.py b/graphix/visualization.py index 8499064dc..d8d7669e4 100644 --- a/graphix/visualization.py +++ b/graphix/visualization.py @@ -11,6 +11,7 @@ from matplotlib import pyplot as plt from graphix import gflow +from graphix.flow.exceptions import FlowError from graphix.fundamentals import Plane from graphix.measurements import PauliMeasurement from graphix.opengraph import OpenGraph @@ -222,25 +223,15 @@ def visualize_from_pattern( Default in None. """ pattern_std = StandardizedPattern.from_pattern(pattern) + cf: Mapping[int, AbstractSet[int]] + corrections: tuple[Mapping[int, AbstractSet[int]], Mapping[int, AbstractSet[int]]] | None try: causal_flow = pattern_std.extract_causal_flow() - print("The pattern is consistent with flow structure.") - pos = self.get_pos_from_flow(causal_flow) - cf = causal_flow.correction_function - l_k = { - node: layer_idx for layer_idx, layer in enumerate(causal_flow.partial_order_layers) for node in layer - } - corrections: tuple[Mapping[int, AbstractSet[int]], Mapping[int, AbstractSet[int]]] | None = None - except ValueError: + except FlowError: try: g_flow = pattern_std.extract_gflow() - print("The pattern is consistent with gflow structure. (not with flow)") - pos = self.get_pos_from_gflow(g_flow) - cf = g_flow.correction_function - l_k = {node: layer_idx for layer_idx, layer in enumerate(g_flow.partial_order_layers) for node in layer} - corrections = None - except ValueError: + except FlowError: print("The pattern is not consistent with flow or gflow structure.") po_layers = pattern.extract_partial_order_layers() unfolded_layers = {node: layer_idx for layer_idx, layer in enumerate(po_layers[::-1]) for node in layer} @@ -255,6 +246,20 @@ def visualize_from_pattern( cf = xzflow l_k = None corrections = xflow, zflow + else: + print("The pattern is consistent with gflow structure. (not with flow)") + pos = self.get_pos_from_gflow(g_flow) + cf = g_flow.correction_function + l_k = {node: layer_idx for layer_idx, layer in enumerate(g_flow.partial_order_layers) for node in layer} + corrections = None + else: + print("The pattern is consistent with flow structure.") + pos = self.get_pos_from_flow(causal_flow) + cf = causal_flow.correction_function + l_k = { + node: layer_idx for layer_idx, layer in enumerate(causal_flow.partial_order_layers) for node in layer + } + corrections = None def get_paths( pos: Mapping[int, _Point], diff --git a/tests/test_pattern.py b/tests/test_pattern.py index ec03996de..eadbcb6e6 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -13,6 +13,9 @@ from graphix.branch_selector import ConstBranchSelector, FixedBranchSelector from graphix.clifford import Clifford from graphix.command import C, Command, CommandKind, E, M, N, X, Z +from graphix.flow.exceptions import ( + FlowError, +) from graphix.fundamentals import Plane from graphix.measurements import Measurement, Outcome, PauliMeasurement from graphix.opengraph import OpenGraph @@ -812,8 +815,6 @@ class PatternFlowTestCase(NamedTuple): pattern: Pattern has_cflow: bool has_gflow: bool - error_cflow: str | None = None - error_gflow: str | None = None PATTERN_FLOW_TEST_CASES: list[PatternFlowTestCase] = [ # noqa: RUF012 PatternFlowTestCase( @@ -878,7 +879,6 @@ class PatternFlowTestCase(NamedTuple): Pattern(cmds=[N(0), N(1), E((0, 1)), M(0, Plane.XZ, 0.3), Z(1, {0}), X(1, {0})], output_nodes=[1]), has_cflow=False, has_gflow=True, - error_cflow="Pattern does not have causal flow. Node 0 is measured in Plane.XZ.", ), PatternFlowTestCase( # Pattern with gflow but without causal flow and XY measurements. @@ -910,15 +910,12 @@ class PatternFlowTestCase(NamedTuple): ), has_cflow=False, has_gflow=True, - error_cflow="Pattern does not have causal flow. Node 1 is corrected by nodes 5 and 6 but correcting sets in causal flows can have one element only.", ), PatternFlowTestCase( # Non-deterministic pattern Pattern(input_nodes=[0], cmds=[N(1), E((0, 1)), M(0, Plane.XY, 0.3)]), has_cflow=False, has_gflow=False, - error_cflow="Pattern does not have causal flow.", - error_gflow="Pattern does not have gflow.", ), ] @@ -967,9 +964,8 @@ def test_extract_causal_flow(self, fx_rng: Generator, test_case: PatternFlowTest assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) else: - with pytest.raises(ValueError) as err: + with pytest.raises(FlowError): test_case.pattern.extract_causal_flow() - assert str(err.value) == test_case.error_cflow @pytest.mark.parametrize("test_case", PATTERN_FLOW_TEST_CASES) def test_extract_gflow(self, fx_rng: Generator, test_case: PatternFlowTestCase) -> None: @@ -982,9 +978,8 @@ def test_extract_gflow(self, fx_rng: Generator, test_case: PatternFlowTestCase) assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) else: - with pytest.raises(ValueError) as err: + with pytest.raises(FlowError): test_case.pattern.extract_gflow() - assert str(err.value) == test_case.error_gflow # From open graph def test_extract_cflow_og(self, fx_rng: Generator) -> None: From 2a8e4d9769ab43b15fdf9f974f2d8290c8621ca3 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 9 Dec 2025 15:36:42 +0100 Subject: [PATCH 26/58] Remove dep on Pattern.get_layers --- graphix/optimization.py | 2 +- graphix/pattern.py | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index 62a06aa62..4426f8165 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -386,7 +386,7 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: ----- The returned object follows the same conventions as the ``partial_order_layers`` attribute of :class:`PauliFlow` and :class:`XZCorrections` objects: - Nodes in the same layer can be measured simultaneously. - - Nodes in layer ``i`` must be measured before nodes in layer ``i + 1``. + - Nodes in layer ``i`` must be measured after nodes in layer ``i + 1``. - All output nodes (if any) are in the first layer. - There cannot be any empty layers. """ diff --git a/graphix/pattern.py b/graphix/pattern.py index 871af8214..a0673695e 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -906,7 +906,7 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: ----- The returned object follows the same conventions as the ``partial_order_layers`` attribute of :class:`PauliFlow` and :class:`XZCorrections` objects: - Nodes in the same layer can be measured simultaneously. - - Nodes in layer ``i`` must be measured before nodes in layer ``i + 1``. + - Nodes in layer ``i`` must be measured after nodes in layer ``i + 1``. - All output nodes (if any) are in the first layer. - There cannot be any empty layers. """ @@ -1037,14 +1037,11 @@ def _measurement_order_depth(self) -> list[int]: Returns ------- - meas_order: list of int + list[int] optimal measurement order for parallel computing """ - d, l_k = self.get_layers() - meas_order: list[int] = [] - for i in range(d): - meas_order.extend(l_k[i]) - return meas_order + partial_order_layers = self.extract_partial_order_layers() + return list(itertools.chain(*reversed(partial_order_layers[1:]))) @staticmethod def connected_edges(node: int, edges: set[tuple[int, int]]) -> set[tuple[int, int]]: From d3eaee8d52c7b27484ad3e96e58e979c15df84b1 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 9 Dec 2025 16:29:17 +0100 Subject: [PATCH 27/58] Add comments --- graphix/optimization.py | 16 ++++++++++------ graphix/pattern.py | 12 ++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index 4426f8165..250403eb5 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -390,22 +390,26 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: - All output nodes (if any) are in the first layer. - There cannot be any empty layers. """ - oset = frozenset(self.output_nodes) # First layer by convention - pre_measured_nodes = set(self.results.keys()) # Not included in the partial order layers + oset = frozenset(self.output_nodes) # First layer by convention. + pre_measured_nodes = set(self.results.keys()) # Not included in the partial order layers. excluded_nodes = oset | pre_measured_nodes zero_indegree = set(self.input_nodes) - excluded_nodes - dag: dict[int, set[int]] = {node: set() for node in zero_indegree} + dag: dict[int, set[int]] = { + node: set() for node in zero_indegree + } # `i: {j}` represents `i -> j` which means that node `i` must be measured before node `j`. indegree_map: dict[int, int] = {} for n in self.n_list: - if n.node not in oset: # pre-measured nodes only appear in domains. + if n.node not in oset: # Pre-measured nodes only appear in domains. dag[n.node] = set() zero_indegree.add(n.node) def process_domain(node: Node, domain: AbstractSet[Node]) -> None: for dep_node in domain: - if not {node, dep_node} & excluded_nodes and node not in dag[dep_node]: + if ( + not {node, dep_node} & excluded_nodes and node not in dag[dep_node] + ): # Don't include multiple edges in the dag. dag[dep_node].add(node) indegree_map[node] = indegree_map.get(node, 0) + 1 @@ -422,7 +426,7 @@ def process_domain(node: Node, domain: AbstractSet[Node]) -> None: zero_indegree -= indegree_map.keys() generations = compute_topological_generations(dag, indegree_map, zero_indegree) - assert generations is not None # DAG can't contain loops because pattern is runnable. + assert generations is not None # DAG can't contain loops because `self` is a runnable pattern. if oset: return oset, *generations[::-1] diff --git a/graphix/pattern.py b/graphix/pattern.py index a0673695e..9c2918561 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -917,7 +917,9 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: excluded_nodes = oset | pre_measured_nodes zero_indegree = set(self.input_nodes) - excluded_nodes - dag: dict[int, set[int]] = {node: set() for node in zero_indegree} + dag: dict[int, set[int]] = { + node: set() for node in zero_indegree + } # `i: {j}` represents `i -> j` which means that node `i` must be measured before node `j`. indegree_map: dict[int, int] = {} for cmd in self: @@ -932,7 +934,9 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: node, domain = cmd.node, cmd.domain for dep_node in domain: - if not {node, dep_node} & excluded_nodes and node not in dag[dep_node]: + if ( + not {node, dep_node} & excluded_nodes and node not in dag[dep_node] + ): # Don't include multiple edges in the dag. dag[dep_node].add(node) indegree_map[node] = indegree_map.get(node, 0) + 1 @@ -957,7 +961,7 @@ def extract_causal_flow(self) -> flow.CausalFlow[Measurement]: Raises ------ - ValueError + FlowError If the pattern: - contains measurements in forbidden planes (XZ or YZ), - assigns more than one correcting node to the same measured node, @@ -984,7 +988,7 @@ def extract_gflow(self) -> flow.GFlow[Measurement]: Raises ------ - ValueError + FlowError If the pattern is empty or if the extracted structure does not satisfy the well-formedness conditions required for a valid gflow. RunnabilityError From 4487c94a17420472a128bf1c5c7887a1f9431bbd Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 9 Dec 2025 17:01:33 +0100 Subject: [PATCH 28/58] Add check on N commands when extracting open graph to form flow --- graphix/optimization.py | 17 +++++++++++++++++ graphix/pattern.py | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/graphix/optimization.py b/graphix/optimization.py index 250403eb5..fc07abede 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -29,6 +29,7 @@ from graphix.fundamentals import Axis, Plane from graphix.measurements import Domains, Measurement, Outcome, PauliMeasurement from graphix.opengraph import OpenGraph +from graphix.states import BasicStates if TYPE_CHECKING: from collections.abc import Iterable, Mapping @@ -450,6 +451,8 @@ def extract_causal_flow(self) -> CausalFlow[Measurement]: - assigns more than one correcting node to the same measured node, - is empty, or - fails the well-formedness checks for a valid causal flow. + ValueError + If `N` commands in the pattern do not represent a |+⟩ state. Notes ----- @@ -458,6 +461,12 @@ def extract_causal_flow(self) -> CausalFlow[Measurement]: measurements: dict[int, Measurement] = {} correction_function: dict[int, set[int]] = {} + for n in self.n_list: + if n.state != BasicStates.PLUS: + raise ValueError( + f"Open graph construction in flow extraction requires N commands to represent a |+⟩ state. Error found in {n}." + ) + def process_domain(node: Node, domain: AbstractSet[Node]) -> None: for measured_node in domain: if measured_node in correction_function: @@ -496,6 +505,8 @@ def extract_gflow(self) -> GFlow[Measurement]: FlowError If the pattern is empty or if the extracted structure does not satisfy the well-formedness conditions required for a valid gflow. + ValueError + If `N` commands in the pattern do not represent a |+⟩ state. Notes ----- @@ -505,6 +516,12 @@ def extract_gflow(self) -> GFlow[Measurement]: measurements: dict[int, Measurement] = {} correction_function: dict[int, set[int]] = defaultdict(set) + for n in self.n_list: + if n.state != BasicStates.PLUS: + raise ValueError( + f"Open graph construction in flow extraction requires N commands to represent a |+⟩ state. Error found in {n}." + ) + def process_domain(node: Node, domain: AbstractSet[Node]) -> None: for measured_node in domain: correction_function[measured_node].add(node) diff --git a/graphix/pattern.py b/graphix/pattern.py index 9c2918561..d7caf4b23 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -967,6 +967,8 @@ def extract_causal_flow(self) -> flow.CausalFlow[Measurement]: - assigns more than one correcting node to the same measured node, - is empty, or - fails the well-formedness checks for a valid causal flow. + ValueError + If `N` commands in the pattern do not represent a |+⟩ state. RunnabilityError If the pattern is not runnable. @@ -991,6 +993,8 @@ def extract_gflow(self) -> flow.GFlow[Measurement]: FlowError If the pattern is empty or if the extracted structure does not satisfy the well-formedness conditions required for a valid gflow. + ValueError + If `N` commands in the pattern do not represent a |+⟩ state. RunnabilityError If the pattern is not runnable. From 4a3e79c78bb4de62e25070b8e5dc047888d63dcc Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 9 Dec 2025 17:42:09 +0100 Subject: [PATCH 29/58] Fix pyright --- graphix/pattern.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/graphix/pattern.py b/graphix/pattern.py index d7caf4b23..438bf47bb 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -921,6 +921,7 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: node: set() for node in zero_indegree } # `i: {j}` represents `i -> j` which means that node `i` must be measured before node `j`. indegree_map: dict[int, int] = {} + node: int | None = None # To avoid Pyright's "PossiblyUnboundVariable" error for cmd in self: domain = set() @@ -934,6 +935,7 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: node, domain = cmd.node, cmd.domain for dep_node in domain: + assert node is not None if ( not {node, dep_node} & excluded_nodes and node not in dag[dep_node] ): # Don't include multiple edges in the dag. From 0b11c0cad9b5bc77947df7115c20bc6298c30ad4 Mon Sep 17 00:00:00 2001 From: matulni Date: Wed, 10 Dec 2025 09:50:38 +0100 Subject: [PATCH 30/58] Cast defaultdict into dict --- graphix/optimization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index fc07abede..9b3ab85fd 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -537,7 +537,7 @@ def process_domain(node: Node, domain: AbstractSet[Node]) -> None: partial_order_layers = self.extract_partial_order_layers() og = OpenGraph(self.extract_graph(), self.input_nodes, self.output_nodes, measurements) - gf = GFlow(og, correction_function, partial_order_layers) + gf = GFlow(og, dict(correction_function), partial_order_layers) gf.check_well_formed() return gf From 3b2bc9b0f699ea318d157f2a40b67bdb9c314c8e Mon Sep 17 00:00:00 2001 From: matulni Date: Wed, 10 Dec 2025 11:57:43 +0100 Subject: [PATCH 31/58] wip --- graphix/pattern.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/graphix/pattern.py b/graphix/pattern.py index d7caf4b23..fc221bb83 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -10,6 +10,7 @@ import enum import itertools import warnings +from collections import defaultdict from collections.abc import Iterable, Iterator from dataclasses import dataclass from enum import Enum @@ -1005,6 +1006,50 @@ def extract_gflow(self) -> flow.GFlow[Measurement]: """ return optimization.StandardizedPattern.from_pattern(self).extract_gflow() + def extract_xzcorrections(self) -> flow.XZCorrections[Measurement]: + nodes = set(self.input_nodes) + edges: set[tuple[int, int]] = set() + measurements: dict[int, Measurement] = {} + x_corr: dict[int, set[int]] = defaultdict(set) + z_corr: dict[int, set[int]] = defaultdict(set) + + pre_measured_nodes = set(self.results.keys()) # Not included in the xz-corrections. + + def update_corrections( + node: int, domain: AbstractSet[int], corrections: dict[int, set[int]] + ) -> dict[int, set[int]]: + for measured_node in domain: + corrections[measured_node].symmetric_difference_update({node}) + return corrections + + for cmd in self.__seq: + if cmd.kind == CommandKind.N: + if cmd.state != BasicStates.PLUS: + raise ValueError( + f"Open graph construction in XZ-corrections extraction requires N commands to represent a |+⟩ state. Error found in {cmd}." + ) + nodes.add(cmd.node) + elif cmd.kind == CommandKind.E: + u, v = cmd.nodes + if u > v: + u, v = v, u + edges.symmetric_difference_update({(u, v)}) + elif cmd.kind == CommandKind.M: + node = cmd.node + measurements[node] = Measurement(cmd.angle, cmd.plane) + x_corr = update_corrections(node, cmd.s_domain - pre_measured_nodes, x_corr) + z_corr = update_corrections(node, cmd.t_domain - pre_measured_nodes, z_corr) + elif cmd.kind == CommandKind.X: + x_corr = update_corrections(cmd.node, cmd.domain - pre_measured_nodes, x_corr) + elif cmd.kind == CommandKind.Z: + z_corr = update_corrections(cmd.node, cmd.domain - pre_measured_nodes, z_corr) + + graph = nx.Graph(edges) + graph.add_nodes_from(nodes) + og = OpenGraph(graph, self.input_nodes, self.output_nodes, measurements) + + return flow.XZCorrections.from_measured_nodes_mapping(og, dict(x_corr), dict(z_corr)) + def get_layers(self) -> tuple[int, dict[int, set[int]]]: """Construct layers(l_k) from dependency information. From d840db7725fc48e523ee108ab7387d5719a5f7d7 Mon Sep 17 00:00:00 2001 From: matulni Date: Wed, 10 Dec 2025 13:12:28 +0100 Subject: [PATCH 32/58] wip --- tests/test_pattern.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_pattern.py b/tests/test_pattern.py index eadbcb6e6..0d0aa4178 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -1028,6 +1028,23 @@ def test_extract_gflow_og(self, fx_rng: Generator) -> None: assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + # Extract xz-corrections from random circuits + @pytest.mark.parametrize("jumps", range(1, 11)) + def test_extract_xzc_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None: + rng = Generator(fx_bg.jumped(jumps)) + nqubits = 2 + depth = 2 + circuit_1 = rand_circuit(nqubits, depth, rng, use_ccx=False) + p_ref = circuit_1.transpile().pattern + p_test = p_ref.extract_xzcorrections().to_pattern() + + p_ref.perform_pauli_measurements() + p_test.perform_pauli_measurements() + + s_ref = p_ref.simulate_pattern(rng=rng) + s_test = p_test.simulate_pattern(rng=rng) + assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + def cp(circuit: Circuit, theta: float, control: int, target: int) -> None: """Controlled rotation gate, decomposed.""" # noqa: D401 From af617b6ee7f95bb1185f22b1d7dc667c605894ec Mon Sep 17 00:00:00 2001 From: matulni Date: Wed, 10 Dec 2025 14:43:03 +0100 Subject: [PATCH 33/58] Add new function to compute partial order layers from corrections --- graphix/flow/core.py | 119 +++++++++++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 37 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index 943594c21..f4f916da8 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections import defaultdict from collections.abc import Sequence from copy import copy from dataclasses import dataclass @@ -21,6 +22,7 @@ _PM_co, compute_partial_order_layers, ) +from graphix.flow._partial_order import compute_topological_generations from graphix.flow.exceptions import ( FlowError, FlowGenericError, @@ -121,43 +123,45 @@ def from_measured_nodes_mapping( if not non_outputs_set.issuperset(x_corrections.keys() | z_corrections.keys()): raise XZCorrectionsGenericError(XZCorrectionsGenericErrorReason.IncorrectKeys) - dag = _corrections_to_dag(x_corrections, z_corrections) - partial_order_layers = _dag_to_partial_order_layers(dag) - - if partial_order_layers is None: - raise XZCorrectionsGenericError(XZCorrectionsGenericErrorReason.ClosedLoop) - - # If there're no corrections, the partial order has 2 layers only: outputs and measured nodes. - if len(partial_order_layers) == 0: - partial_order_layers = [outputs_set] if outputs_set else [] - if non_outputs_set: - partial_order_layers.append(frozenset(non_outputs_set)) - return XZCorrections(og, x_corrections, z_corrections, tuple(partial_order_layers)) - - # If the open graph has outputs, the first element in the output of `_dag_to_partial_order_layers(dag)` may or may not contain all or some output nodes. - if outputs_set: - if measured_layer_0 := partial_order_layers[0] - outputs_set: - # `partial_order_layers[0]` contains (some or all) outputs and measured nodes - partial_order_layers = [ - outputs_set, - frozenset(measured_layer_0), - *partial_order_layers[1:], - ] - else: - # `partial_order_layers[0]` contains only (some or all) outputs - partial_order_layers = [ - outputs_set, - *partial_order_layers[1:], - ] - - ordered_nodes = frozenset.union(*partial_order_layers) - - if not ordered_nodes.issubset(nodes_set): - raise XZCorrectionsGenericError(XZCorrectionsGenericErrorReason.IncorrectValues) - - # We include all the non-output nodes not involved in the corrections in the last layer (first measured nodes). - if unordered_nodes := frozenset(nodes_set - ordered_nodes): - partial_order_layers[-1] |= unordered_nodes + partial_order_layers = _corrections_to_partial_order_layers(og, x_corrections, z_corrections) + + # dag = _corrections_to_dag(x_corrections, z_corrections) + # partial_order_layers = _dag_to_partial_order_layers(dag) + + # if partial_order_layers is None: + # raise XZCorrectionsGenericError(XZCorrectionsGenericErrorReason.ClosedLoop) + + # # If there're no corrections, the partial order has 2 layers only: outputs and measured nodes. + # if len(partial_order_layers) == 0: + # partial_order_layers = [outputs_set] if outputs_set else [] + # if non_outputs_set: + # partial_order_layers.append(frozenset(non_outputs_set)) + # return XZCorrections(og, x_corrections, z_corrections, tuple(partial_order_layers)) + + # # If the open graph has outputs, the first element in the output of `_dag_to_partial_order_layers(dag)` may or may not contain all or some output nodes. + # if outputs_set: + # if measured_layer_0 := partial_order_layers[0] - outputs_set: + # # `partial_order_layers[0]` contains (some or all) outputs and measured nodes + # partial_order_layers = [ + # outputs_set, + # frozenset(measured_layer_0), + # *partial_order_layers[1:], + # ] + # else: + # # `partial_order_layers[0]` contains only (some or all) outputs + # partial_order_layers = [ + # outputs_set, + # *partial_order_layers[1:], + # ] + + # ordered_nodes = frozenset.union(*partial_order_layers) + + # if not ordered_nodes.issubset(nodes_set): + # raise XZCorrectionsGenericError(XZCorrectionsGenericErrorReason.IncorrectValues) + + # # We include all the non-output nodes not involved in the corrections in the last layer (first measured nodes). + # if unordered_nodes := frozenset(nodes_set - ordered_nodes): + # partial_order_layers[-1] |= unordered_nodes return XZCorrections(og, x_corrections, z_corrections, tuple(partial_order_layers)) @@ -940,6 +944,47 @@ def _corrections_to_dag( return nx.DiGraph(relations) +def _corrections_to_partial_order_layers(og: OpenGraph[_M_co], + x_corrections: Mapping[int, AbstractSet[int]], z_corrections: Mapping[int, AbstractSet[int]] +) -> tuple[frozenset[int], ...]: + oset = frozenset(og.output_nodes) # First layer by convention if not empty + dag: dict[int, set[int]] = defaultdict(set) # `i: {j}` represents `i -> j`, i.e., a correction applied to qubit `j`, conditioned on the measurement outcome of qubit `i`. + indegree_map: dict[int, int] = {} + + for corrections in [x_corrections, z_corrections]: + for measured_node, corrected_nodes in corrections.items(): + if measured_node not in oset: + for corrected_node in corrected_nodes - oset: + if corrected_node not in dag[measured_node]: # Don't include multiple edges in the dag. + dag[measured_node].add(corrected_node) + indegree_map[corrected_node] = indegree_map.get(corrected_node, 0) + 1 + + zero_indegree = og.graph.nodes - oset - indegree_map.keys() + generations = compute_topological_generations(dag, indegree_map, zero_indegree) + if generations is None: + raise XZCorrectionsGenericError(XZCorrectionsGenericErrorReason.ClosedLoop) + + # If there're no corrections, the partial order has 2 layers only: outputs and measured nodes. + if len(generations) == 0: + if oset: + return (oset, ) + return () + + ordered_nodes = frozenset.union(*generations) + + if not ordered_nodes.issubset(og.graph.nodes): + raise XZCorrectionsGenericError(XZCorrectionsGenericErrorReason.IncorrectValues) + + # We include all the non-output nodes not involved in the corrections in the last layer (first measured nodes). + if unordered_nodes := frozenset(og.graph.nodes - ordered_nodes - oset): + if oset: + return oset, *generations[::-1], unordered_nodes + return *generations[::-1], unordered_nodes + if oset: + return oset, *generations[::-1] + return generations[::-1] + + def _dag_to_partial_order_layers(dag: nx.DiGraph[int]) -> list[frozenset[int]] | None: """Return the partial order encoded in a directed graph in a layer form if it exists. From e5256bfbe0553bcd9db2767fa3cc1ec22613b981 Mon Sep 17 00:00:00 2001 From: matulni Date: Wed, 10 Dec 2025 14:56:25 +0100 Subject: [PATCH 34/58] Wip --- graphix/flow/core.py | 91 ++++++++++++++------------------------------ 1 file changed, 28 insertions(+), 63 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index f4f916da8..48f98a1aa 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -116,52 +116,12 @@ def from_measured_nodes_mapping( x_corrections = x_corrections or {} z_corrections = z_corrections or {} - nodes_set = set(og.graph.nodes) - outputs_set = frozenset(og.output_nodes) non_outputs_set = set(og.measurements) if not non_outputs_set.issuperset(x_corrections.keys() | z_corrections.keys()): raise XZCorrectionsGenericError(XZCorrectionsGenericErrorReason.IncorrectKeys) - partial_order_layers = _corrections_to_partial_order_layers(og, x_corrections, z_corrections) - - # dag = _corrections_to_dag(x_corrections, z_corrections) - # partial_order_layers = _dag_to_partial_order_layers(dag) - - # if partial_order_layers is None: - # raise XZCorrectionsGenericError(XZCorrectionsGenericErrorReason.ClosedLoop) - - # # If there're no corrections, the partial order has 2 layers only: outputs and measured nodes. - # if len(partial_order_layers) == 0: - # partial_order_layers = [outputs_set] if outputs_set else [] - # if non_outputs_set: - # partial_order_layers.append(frozenset(non_outputs_set)) - # return XZCorrections(og, x_corrections, z_corrections, tuple(partial_order_layers)) - - # # If the open graph has outputs, the first element in the output of `_dag_to_partial_order_layers(dag)` may or may not contain all or some output nodes. - # if outputs_set: - # if measured_layer_0 := partial_order_layers[0] - outputs_set: - # # `partial_order_layers[0]` contains (some or all) outputs and measured nodes - # partial_order_layers = [ - # outputs_set, - # frozenset(measured_layer_0), - # *partial_order_layers[1:], - # ] - # else: - # # `partial_order_layers[0]` contains only (some or all) outputs - # partial_order_layers = [ - # outputs_set, - # *partial_order_layers[1:], - # ] - - # ordered_nodes = frozenset.union(*partial_order_layers) - - # if not ordered_nodes.issubset(nodes_set): - # raise XZCorrectionsGenericError(XZCorrectionsGenericErrorReason.IncorrectValues) - - # # We include all the non-output nodes not involved in the corrections in the last layer (first measured nodes). - # if unordered_nodes := frozenset(nodes_set - ordered_nodes): - # partial_order_layers[-1] |= unordered_nodes + partial_order_layers = _corrections_to_partial_order_layers(og, x_corrections, z_corrections) # Raises an `XZCorrectionsError` if mappings are not well formed. return XZCorrections(og, x_corrections, z_corrections, tuple(partial_order_layers)) @@ -943,10 +903,37 @@ def _corrections_to_dag( return nx.DiGraph(relations) +# TODO: UP docstring + def _corrections_to_partial_order_layers(og: OpenGraph[_M_co], x_corrections: Mapping[int, AbstractSet[int]], z_corrections: Mapping[int, AbstractSet[int]] ) -> tuple[frozenset[int], ...]: + """Return the partial order encoded in a directed graph in a layer form if it exists. + + Parameters + ---------- + og : OpenGraph[_M_co] + The open graph with respect to which the XZ-corrections are defined. + x_corrections : Mapping[int, AbstractSet[int]] + Mapping of X-corrections: in each (`key`, `value`) pair, `key` is a measured node, and `value` is the set of nodes on which an X-correction must be applied depending on the measurement result of `key`. + z_corrections : Mapping[int, AbstractSet[int]] + Mapping of Z-corrections: in each (`key`, `value`) pair, `key` is a measured node, and `value` is the set of nodes on which an Z-correction must be applied depending on the measurement result of `key`. + + Returns + ------- + tuple[frozenset[int], ...] + Partial order between the open graph's in a layer form. + The set `layers[i]` comprises the nodes in layer `i`. Nodes in layer `i` are "larger" in the partial order than nodes in layer `i+1`. + + Raises + ------ + XZCorrectionsError + If the input dictionaries are not well formed. In well-formed correction dictionaries: + - Keys are a subset of the measured nodes. + - Values correspond to nodes of the open graph. + - Corrections do not form closed loops. + """ oset = frozenset(og.output_nodes) # First layer by convention if not empty dag: dict[int, set[int]] = defaultdict(set) # `i: {j}` represents `i -> j`, i.e., a correction applied to qubit `j`, conditioned on the measurement outcome of qubit `i`. indegree_map: dict[int, int] = {} @@ -985,28 +972,6 @@ def _corrections_to_partial_order_layers(og: OpenGraph[_M_co], return generations[::-1] -def _dag_to_partial_order_layers(dag: nx.DiGraph[int]) -> list[frozenset[int]] | None: - """Return the partial order encoded in a directed graph in a layer form if it exists. - - Parameters - ---------- - dag : nx.DiGraph[int] - A directed graph. - - Returns - ------- - list[set[int]] | None - Partial order between corrected qubits in a layer form or `None` if the input directed graph is not acyclical. - The set `layers[i]` comprises the nodes in layer `i`. Nodes in layer `i` are "larger" in the partial order than nodes in layer `i+1`. - """ - try: - topo_gen = reversed(list(nx.topological_generations(dag))) - except nx.NetworkXUnfeasible: - return None - - return [frozenset(layer) for layer in topo_gen] - - def _check_correction_function_domain( og: OpenGraph[_M_co], correction_function: Mapping[int, AbstractSet[int]] ) -> bool: From 225e4176fa7643c36f946e43768e882da6fa8335 Mon Sep 17 00:00:00 2001 From: matulni Date: Wed, 10 Dec 2025 17:00:53 +0100 Subject: [PATCH 35/58] Up docs --- graphix/optimization.py | 33 ++++++++++++++++++++------------- graphix/pattern.py | 4 +++- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index 9b3ab85fd..b02b33d70 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -376,13 +376,18 @@ def ensure_neighborhood(node: Node) -> None: def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: """Extract the measurement order of the pattern in the form of layers. - This method builds a directed acyclical diagram (DAG) from the pattern and then performs a topological sort. + This method builds a directed acyclical graph (DAG) from the pattern and then performs a topological sort. Returns ------- tuple[frozenset[int], ...] Measurement partial order between the pattern's nodes in a layer form. + Raises + ------ + ValueError + If the correction domains in the pattern form closed loops. This implies that the pattern is not runnable. + Notes ----- The returned object follows the same conventions as the ``partial_order_layers`` attribute of :class:`PauliFlow` and :class:`XZCorrections` objects: @@ -427,7 +432,8 @@ def process_domain(node: Node, domain: AbstractSet[Node]) -> None: zero_indegree -= indegree_map.keys() generations = compute_topological_generations(dag, indegree_map, zero_indegree) - assert generations is not None # DAG can't contain loops because `self` is a runnable pattern. + if generations is None: + raise ValueError("Pattern domains form closed loops.") if oset: return oset, *generations[::-1] @@ -436,7 +442,7 @@ def process_domain(node: Node, domain: AbstractSet[Node]) -> None: def extract_causal_flow(self) -> CausalFlow[Measurement]: """Extract the causal flow structure from the current measurement pattern. - This method reconstructs the underlying open graph, validates measurement constraints, builds correction dependencies, and verifies that the resulting :class:`flow.CausalFlow` satisfies all well-formedness conditions. + This method does not call the flow-extraction routine on the underlying open graph, but constructs the flow from the pattern corrections instead. Returns ------- @@ -447,16 +453,17 @@ def extract_causal_flow(self) -> CausalFlow[Measurement]: ------ FlowError If the pattern: - - contains measurements in forbidden planes (XZ or YZ), - - assigns more than one correcting node to the same measured node, - - is empty, or - - fails the well-formedness checks for a valid causal flow. + - Contains measurements in forbidden planes (XZ or YZ), + - Assigns more than one correcting node to the same measured node, + - Is empty, or + - Induces a correction function and a partial order which fail the well-formedness checks for a valid causal flow. ValueError - If `N` commands in the pattern do not represent a |+⟩ state. + If `N` commands in the pattern do not represent a |+⟩ state or if the pattern corrections form closed loops. Notes ----- - A causal flow is a structural property of MBQC patterns ensuring that corrections can be assigned deterministically with *single-element* correcting sets and without requiring measurements in the XZ or YZ planes. + This method makes use of :func:`StandardizedPattern.extract_partial_order_layers` which computes the pattern's direct acyclical graph (DAG) induced by the corrections and returns a particular layer stratification (obtained by doing a topological sort on the DAG). Further, it constructs the pattern's induced correction function from :math:`M` and :math:`X` commands. + In general, there may exist various layerings which represent the corrections of the pattern. To ensure that a given layering is compatible with the pattern's induced correction function, the partial order must be extracted from a standardized pattern. Commutation of entanglement commands with X and Z corrections in the standardization procedure may generate new corrections, which guarantees that all the topological information of the underlying graph is encoded in the extracted partial order. """ measurements: dict[int, Measurement] = {} correction_function: dict[int, set[int]] = {} @@ -493,7 +500,7 @@ def process_domain(node: Node, domain: AbstractSet[Node]) -> None: def extract_gflow(self) -> GFlow[Measurement]: """Extract the generalized flow (gflow) structure from the current measurement pattern. - The method reconstructs the underlying open graph, and determines the correction dependencies and the partial order required for a valid gflow. It then constructs and validates a :class:`flow.GFlow` object. + This method does not call the flow-extraction routine on the underlying open graph, but constructs the gflow from the pattern corrections instead. Returns ------- @@ -506,12 +513,12 @@ def extract_gflow(self) -> GFlow[Measurement]: If the pattern is empty or if the extracted structure does not satisfy the well-formedness conditions required for a valid gflow. ValueError - If `N` commands in the pattern do not represent a |+⟩ state. + If `N` commands in the pattern do not represent a |+⟩ state or if the pattern corrections form closed loops. Notes ----- - A gflow is a structural property of measurement-based quantum computation - (MBQC) patterns that ensures determinism and proper correction propagation. + This method makes use of :func:`StandardizedPattern.extract_partial_order_layers` which computes the pattern's direct acyclical graph (DAG) induced by the corrections and returns a particular layer stratification (obtained by doing a topological sort on the DAG). Further, it constructs the pattern's induced correction function from :math:`M` and :math:`X` commands. + In general, there may exist various layerings which represent the corrections of the pattern. To ensure that a given layering is compatible with the pattern's induced correction function, the partial order must be extracted from a standardized pattern. Commutation of entanglement commands with X and Z corrections in the standardization procedure may generate new corrections, which guarantees that all the topological information of the underlying graph is encoded in the extracted partial order. """ measurements: dict[int, Measurement] = {} correction_function: dict[int, set[int]] = defaultdict(set) diff --git a/graphix/pattern.py b/graphix/pattern.py index 438bf47bb..34b24b14c 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -890,7 +890,7 @@ def update_dependency(measured: AbstractSet[int], dependency: dict[int, set[int] def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: """Extract the measurement order of the pattern in the form of layers. - This method builds a directed acyclical diagram (DAG) from the pattern and then performs a topological sort. + This method builds a directed acyclical graph (DAG) from the pattern and then performs a topological sort. Returns ------- @@ -909,6 +909,8 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: - Nodes in layer ``i`` must be measured after nodes in layer ``i + 1``. - All output nodes (if any) are in the first layer. - There cannot be any empty layers. + + The partial order layerings obtained with this method before and after standardizing a pattern may differ, but they will always be compatible. This occurs because the commutation of entanglement commands with X and Z corrections in the standardization procedure may generate new corrections which add additional constrains to pattern's induced DAG. """ self.check_runnability() From 2f44562add0105ffd401023b929df6fe070cb5dc Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 11 Dec 2025 10:53:26 +0100 Subject: [PATCH 36/58] wip --- graphix/flow/core.py | 20 ++++++++++++-------- graphix/optimization.py | 41 ++++++++++++++++++++++++++++++++++++++++- tests/test_pattern.py | 6 +++++- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index 48f98a1aa..d342e2e7c 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -121,7 +121,9 @@ def from_measured_nodes_mapping( if not non_outputs_set.issuperset(x_corrections.keys() | z_corrections.keys()): raise XZCorrectionsGenericError(XZCorrectionsGenericErrorReason.IncorrectKeys) - partial_order_layers = _corrections_to_partial_order_layers(og, x_corrections, z_corrections) # Raises an `XZCorrectionsError` if mappings are not well formed. + partial_order_layers = _corrections_to_partial_order_layers( + og, x_corrections, z_corrections + ) # Raises an `XZCorrectionsError` if mappings are not well formed. return XZCorrections(og, x_corrections, z_corrections, tuple(partial_order_layers)) @@ -903,11 +905,12 @@ def _corrections_to_dag( return nx.DiGraph(relations) + # TODO: UP docstring -def _corrections_to_partial_order_layers(og: OpenGraph[_M_co], - x_corrections: Mapping[int, AbstractSet[int]], z_corrections: Mapping[int, AbstractSet[int]] +def _corrections_to_partial_order_layers( + og: OpenGraph[_M_co], x_corrections: Mapping[int, AbstractSet[int]], z_corrections: Mapping[int, AbstractSet[int]] ) -> tuple[frozenset[int], ...]: """Return the partial order encoded in a directed graph in a layer form if it exists. @@ -935,7 +938,9 @@ def _corrections_to_partial_order_layers(og: OpenGraph[_M_co], - Corrections do not form closed loops. """ oset = frozenset(og.output_nodes) # First layer by convention if not empty - dag: dict[int, set[int]] = defaultdict(set) # `i: {j}` represents `i -> j`, i.e., a correction applied to qubit `j`, conditioned on the measurement outcome of qubit `i`. + dag: dict[int, set[int]] = defaultdict( + set + ) # `i: {j}` represents `i -> j`, i.e., a correction applied to qubit `j`, conditioned on the measurement outcome of qubit `i`. indegree_map: dict[int, int] = {} for corrections in [x_corrections, z_corrections]: @@ -954,7 +959,7 @@ def _corrections_to_partial_order_layers(og: OpenGraph[_M_co], # If there're no corrections, the partial order has 2 layers only: outputs and measured nodes. if len(generations) == 0: if oset: - return (oset, ) + return (oset,) return () ordered_nodes = frozenset.union(*generations) @@ -964,9 +969,8 @@ def _corrections_to_partial_order_layers(og: OpenGraph[_M_co], # We include all the non-output nodes not involved in the corrections in the last layer (first measured nodes). if unordered_nodes := frozenset(og.graph.nodes - ordered_nodes - oset): - if oset: - return oset, *generations[::-1], unordered_nodes - return *generations[::-1], unordered_nodes + generations = *generations[:-1], frozenset(generations[-1] | unordered_nodes) + if oset: return oset, *generations[::-1] return generations[::-1] diff --git a/graphix/optimization.py b/graphix/optimization.py index b02b33d70..8c611ed67 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -19,7 +19,7 @@ from graphix.clifford import Clifford from graphix.command import CommandKind, Node from graphix.flow._partial_order import compute_topological_generations -from graphix.flow.core import CausalFlow, GFlow +from graphix.flow.core import CausalFlow, GFlow, XZCorrections from graphix.flow.exceptions import ( FlowGenericError, FlowGenericErrorReason, @@ -548,6 +548,45 @@ def process_domain(node: Node, domain: AbstractSet[Node]) -> None: gf.check_well_formed() return gf + def extract_xzcorrections(self) -> XZCorrections[Measurement]: + nodes = set(self.input_nodes) + edges: set[tuple[int, int]] = set() + measurements: dict[int, Measurement] = {} + x_corr: dict[int, set[int]] = defaultdict(set) + z_corr: dict[int, set[int]] = defaultdict(set) + + pre_measured_nodes = set(self.results.keys()) # Not included in the xz-corrections. + + for n in self.n_list: + if n.state != BasicStates.PLUS: + raise ValueError( + f"Open graph construction in flow extraction requires N commands to represent a |+⟩ state. Error found in {n}." + ) + + def update_corrections( + node: int, domain: AbstractSet[int], corrections: dict[int, set[int]] + ) -> dict[int, set[int]]: + for measured_node in domain: + corrections[measured_node].add(node) + return corrections + + for m in self.m_list: + measurements[m.node] = Measurement(m.angle, m.plane) + x_corr = update_corrections(m.node, m.s_domain - pre_measured_nodes, x_corr) + z_corr = update_corrections(m.node, m.t_domain - pre_measured_nodes, z_corr) + + for node, domain in self.x_dict.items(): + x_corr = update_corrections(node, domain - pre_measured_nodes, x_corr) + + for node, domain in self.z_dict.items(): + z_corr = update_corrections(node, domain - pre_measured_nodes, z_corr) + + graph = nx.Graph(edges) + graph.add_nodes_from(nodes) + og = OpenGraph(graph, self.input_nodes, self.output_nodes, measurements) + + return XZCorrections.from_measured_nodes_mapping(og, dict(x_corr), dict(z_corr)) + def _add_correction_domain(domain_dict: dict[Node, set[Node]], node: Node, domain: set[Node]) -> None: """Merge a correction domain into ``domain_dict`` for ``node``. diff --git a/tests/test_pattern.py b/tests/test_pattern.py index 0d0aa4178..7531fbda7 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -1036,7 +1036,11 @@ def test_extract_xzc_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None: depth = 2 circuit_1 = rand_circuit(nqubits, depth, rng, use_ccx=False) p_ref = circuit_1.transpile().pattern - p_test = p_ref.extract_xzcorrections().to_pattern() + pc = p_ref.copy() + pc.standardize() + xzc = pc.extract_xzcorrections() + xzc.check_well_formed() + p_test = xzc.to_pattern() p_ref.perform_pauli_measurements() p_test.perform_pauli_measurements() From 26a4cf9b08368bc5846abf703f467b42f0a5f0bb Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 11 Dec 2025 11:31:24 +0100 Subject: [PATCH 37/58] Refactor flow extraction from standard pattern --- graphix/optimization.py | 123 +++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 46 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index b02b33d70..88896057c 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -23,8 +23,6 @@ from graphix.flow.exceptions import ( FlowGenericError, FlowGenericErrorReason, - FlowPropositionError, - FlowPropositionErrorReason, ) from graphix.fundamentals import Axis, Plane from graphix.measurements import Domains, Measurement, Outcome, PauliMeasurement @@ -373,6 +371,30 @@ def ensure_neighborhood(node: Node) -> None: done.add(node) return pattern + def extract_opengraph(self) -> OpenGraph[Measurement]: + """Extract the underlying resource-state open graph from the pattern. + + Returns + ------- + OpenGraph[Measurement] + + Raises + ------ + ValueError + If `N` commands in the pattern do not represent a |+⟩ state. + + Notes + ----- + This operation loses all the information on the Clifford commands. + """ + for n in self.n_list: + if n.state != BasicStates.PLUS: + raise ValueError( + f"Open graph construction in flow extraction requires N commands to represent a |+⟩ state. Error found in {n}." + ) + measurements = {m.node: Measurement(m.angle, m.plane) for m in self.m_list} + return OpenGraph(self.extract_graph(), self.input_nodes, self.output_nodes, measurements) + def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: """Extract the measurement order of the pattern in the form of layers. @@ -454,7 +476,6 @@ def extract_causal_flow(self) -> CausalFlow[Measurement]: FlowError If the pattern: - Contains measurements in forbidden planes (XZ or YZ), - - Assigns more than one correcting node to the same measured node, - Is empty, or - Induces a correction function and a partial order which fail the well-formedness checks for a valid causal flow. ValueError @@ -463,38 +484,26 @@ def extract_causal_flow(self) -> CausalFlow[Measurement]: Notes ----- This method makes use of :func:`StandardizedPattern.extract_partial_order_layers` which computes the pattern's direct acyclical graph (DAG) induced by the corrections and returns a particular layer stratification (obtained by doing a topological sort on the DAG). Further, it constructs the pattern's induced correction function from :math:`M` and :math:`X` commands. - In general, there may exist various layerings which represent the corrections of the pattern. To ensure that a given layering is compatible with the pattern's induced correction function, the partial order must be extracted from a standardized pattern. Commutation of entanglement commands with X and Z corrections in the standardization procedure may generate new corrections, which guarantees that all the topological information of the underlying graph is encoded in the extracted partial order. + In general, there may exist various layerings which represent the corrections of the pattern. To ensure that a given layering is compatible with the pattern's induced correction function, the partial order must be extracted from a standardized pattern. Commutation of entanglement commands with X and Z corrections in the standardization procedure may generate new corrections, which guarantees that all the topological information of the underlying graph is encoded in the extracted partial order. """ - measurements: dict[int, Measurement] = {} - correction_function: dict[int, set[int]] = {} - - for n in self.n_list: - if n.state != BasicStates.PLUS: - raise ValueError( - f"Open graph construction in flow extraction requires N commands to represent a |+⟩ state. Error found in {n}." - ) - - def process_domain(node: Node, domain: AbstractSet[Node]) -> None: - for measured_node in domain: - if measured_node in correction_function: - raise FlowPropositionError( - FlowPropositionErrorReason.C0, measured_node, {node, *correction_function[measured_node]} - ) - correction_function[measured_node] = {node} + correction_function: dict[int, set[int]] = defaultdict(set) for m in self.m_list: if m.plane in {Plane.XZ, Plane.YZ}: raise FlowGenericError(FlowGenericErrorReason.XYPlane) - measurements[m.node] = Measurement(m.angle, m.plane) - process_domain(m.node, m.s_domain) + correction_function = _update_corrections(m.node, m.s_domain, correction_function) for node, domain in self.x_dict.items(): - process_domain(node, domain) - - partial_order_layers = self.extract_partial_order_layers() - og = OpenGraph(self.extract_graph(), self.input_nodes, self.output_nodes, measurements) - cf = CausalFlow(og, correction_function, partial_order_layers) - cf.check_well_formed() + correction_function = _update_corrections(node, domain, correction_function) + + og = ( + self.extract_opengraph() + ) # Raises a `ValueError` if `N` commands in the pattern do not represent a |+⟩ state. + partial_order_layers = ( + self.extract_partial_order_layers() + ) # Raises a `ValueError` if the pattern corrections form closed loops. + cf = CausalFlow(og, dict(correction_function), partial_order_layers) + cf.check_well_formed() # Raises a `FlowError` if the partial order and the correction function are not compatible, or if a measured node is corrected by more than one node. return cf def extract_gflow(self) -> GFlow[Measurement]: @@ -518,32 +527,24 @@ def extract_gflow(self) -> GFlow[Measurement]: Notes ----- This method makes use of :func:`StandardizedPattern.extract_partial_order_layers` which computes the pattern's direct acyclical graph (DAG) induced by the corrections and returns a particular layer stratification (obtained by doing a topological sort on the DAG). Further, it constructs the pattern's induced correction function from :math:`M` and :math:`X` commands. - In general, there may exist various layerings which represent the corrections of the pattern. To ensure that a given layering is compatible with the pattern's induced correction function, the partial order must be extracted from a standardized pattern. Commutation of entanglement commands with X and Z corrections in the standardization procedure may generate new corrections, which guarantees that all the topological information of the underlying graph is encoded in the extracted partial order. + In general, there may exist various layerings which represent the corrections of the pattern. To ensure that a given layering is compatible with the pattern's induced correction function, the partial order must be extracted from a standardized pattern. Commutation of entanglement commands with X and Z corrections in the standardization procedure may generate new corrections, which guarantees that all the topological information of the underlying graph is encoded in the extracted partial order. """ - measurements: dict[int, Measurement] = {} correction_function: dict[int, set[int]] = defaultdict(set) - for n in self.n_list: - if n.state != BasicStates.PLUS: - raise ValueError( - f"Open graph construction in flow extraction requires N commands to represent a |+⟩ state. Error found in {n}." - ) - - def process_domain(node: Node, domain: AbstractSet[Node]) -> None: - for measured_node in domain: - correction_function[measured_node].add(node) - for m in self.m_list: - measurements[m.node] = Measurement(m.angle, m.plane) if m.plane in {Plane.XZ, Plane.YZ}: correction_function[m.node].add(m.node) - process_domain(m.node, m.s_domain) + correction_function = _update_corrections(m.node, m.s_domain, correction_function) for node, domain in self.x_dict.items(): - process_domain(node, domain) - - partial_order_layers = self.extract_partial_order_layers() - og = OpenGraph(self.extract_graph(), self.input_nodes, self.output_nodes, measurements) + correction_function = _update_corrections(node, domain, correction_function) + + og = ( + self.extract_opengraph() + ) # Raises a `ValueError` if `N` commands in the pattern do not represent a |+⟩ state. + partial_order_layers = ( + self.extract_partial_order_layers() + ) # Raises a `ValueError` if the pattern corrections form closed loops. gf = GFlow(og, dict(correction_function), partial_order_layers) gf.check_well_formed() return gf @@ -604,6 +605,36 @@ def _incorporate_pauli_results_in_domain( return odd_outcome == 1, new_domain +def _update_corrections( + node: Node, domain: AbstractSet[Node], correction: dict[Node, set[Node]] +) -> dict[Node, set[Node]]: + """Update the correction mapping by adding a node to all entries in a domain. + + Parameters + ---------- + node : Node + The node to add as a correction. + domain : AbstractSet[Node] + A set of measured nodes whose corresponding correction sets should be updated. + correction : dict[Node, set[Node]] + A mapping from measured nodes to sets of nodes on which corrections are applied. This + dictionary is modified in place. + + Returns + ------- + dict[Node, set[Node]] + The updated correction dictionary with `node` added to the correction + sets of all nodes in `domain`. + + Notes + ----- + This function is used to extract the correction function from :math:`X`, :math:`Z` and :math:`M` commands when constructing a flow. + """ + for measured_node in domain: + correction[measured_node].add(node) + return correction + + def incorporate_pauli_results(pattern: Pattern) -> Pattern: """Return an equivalent pattern where results from Pauli presimulation are integrated in corrections.""" result = graphix.pattern.Pattern(input_nodes=pattern.input_nodes) From 7f3038068b38686e8321eddec8ae8a77b550ecf2 Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 11 Dec 2025 11:47:26 +0100 Subject: [PATCH 38/58] wip --- graphix/optimization.py | 49 ++++++++++++++++++++--------------------- graphix/pattern.py | 47 +++------------------------------------ 2 files changed, 27 insertions(+), 69 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index 4a454ff88..ba8ca2f9d 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -550,43 +550,42 @@ def extract_gflow(self) -> GFlow[Measurement]: return gf def extract_xzcorrections(self) -> XZCorrections[Measurement]: - nodes = set(self.input_nodes) - edges: set[tuple[int, int]] = set() - measurements: dict[int, Measurement] = {} + """Extract the XZ-corrections from the current measurement pattern. + + Returns + ------- + XZCorrections[Measurement] + The XZ-corrections associated with the current pattern. + + Raises + ------ + XZCorrectionsError + If the extracted correction dictionaries are not well formed. + ValueError + If `N` commands in the pattern do not represent a |+⟩ state or if the pattern corrections form closed loops. + """ x_corr: dict[int, set[int]] = defaultdict(set) z_corr: dict[int, set[int]] = defaultdict(set) pre_measured_nodes = set(self.results.keys()) # Not included in the xz-corrections. - for n in self.n_list: - if n.state != BasicStates.PLUS: - raise ValueError( - f"Open graph construction in flow extraction requires N commands to represent a |+⟩ state. Error found in {n}." - ) - - def update_corrections( - node: int, domain: AbstractSet[int], corrections: dict[int, set[int]] - ) -> dict[int, set[int]]: - for measured_node in domain: - corrections[measured_node].add(node) - return corrections - for m in self.m_list: - measurements[m.node] = Measurement(m.angle, m.plane) - x_corr = update_corrections(m.node, m.s_domain - pre_measured_nodes, x_corr) - z_corr = update_corrections(m.node, m.t_domain - pre_measured_nodes, z_corr) + x_corr = _update_corrections(m.node, m.s_domain - pre_measured_nodes, x_corr) + z_corr = _update_corrections(m.node, m.t_domain - pre_measured_nodes, z_corr) for node, domain in self.x_dict.items(): - x_corr = update_corrections(node, domain - pre_measured_nodes, x_corr) + x_corr = _update_corrections(node, domain - pre_measured_nodes, x_corr) for node, domain in self.z_dict.items(): - z_corr = update_corrections(node, domain - pre_measured_nodes, z_corr) + z_corr = _update_corrections(node, domain - pre_measured_nodes, z_corr) - graph = nx.Graph(edges) - graph.add_nodes_from(nodes) - og = OpenGraph(graph, self.input_nodes, self.output_nodes, measurements) + og = ( + self.extract_opengraph() + ) # Raises a `ValueError` if `N` commands in the pattern do not represent a |+⟩ state. - return XZCorrections.from_measured_nodes_mapping(og, dict(x_corr), dict(z_corr)) + return XZCorrections.from_measured_nodes_mapping( + og, dict(x_corr), dict(z_corr) + ) # Raises a `XZCorrectionsError` if the input dictionaries are not well formed. def _add_correction_domain(domain_dict: dict[Node, set[Node]], node: Node, domain: set[Node]) -> None: diff --git a/graphix/pattern.py b/graphix/pattern.py index 6c627ee16..0bc968bf1 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -10,7 +10,6 @@ import enum import itertools import warnings -from collections import defaultdict from collections.abc import Iterable, Iterator from dataclasses import dataclass from enum import Enum @@ -41,6 +40,7 @@ from numpy.random import Generator + from graphix.flow.core import XZCorrections from graphix.parameter import ExpressionOrFloat, ExpressionOrSupportsFloat, Parameter from graphix.sim import Backend, BackendState, Data @@ -1010,49 +1010,8 @@ def extract_gflow(self) -> flow.GFlow[Measurement]: """ return optimization.StandardizedPattern.from_pattern(self).extract_gflow() - def extract_xzcorrections(self) -> flow.XZCorrections[Measurement]: - nodes = set(self.input_nodes) - edges: set[tuple[int, int]] = set() - measurements: dict[int, Measurement] = {} - x_corr: dict[int, set[int]] = defaultdict(set) - z_corr: dict[int, set[int]] = defaultdict(set) - - pre_measured_nodes = set(self.results.keys()) # Not included in the xz-corrections. - - def update_corrections( - node: int, domain: AbstractSet[int], corrections: dict[int, set[int]] - ) -> dict[int, set[int]]: - for measured_node in domain: - corrections[measured_node].symmetric_difference_update({node}) - return corrections - - for cmd in self.__seq: - if cmd.kind == CommandKind.N: - if cmd.state != BasicStates.PLUS: - raise ValueError( - f"Open graph construction in XZ-corrections extraction requires N commands to represent a |+⟩ state. Error found in {cmd}." - ) - nodes.add(cmd.node) - elif cmd.kind == CommandKind.E: - u, v = cmd.nodes - if u > v: - u, v = v, u - edges.symmetric_difference_update({(u, v)}) - elif cmd.kind == CommandKind.M: - node = cmd.node - measurements[node] = Measurement(cmd.angle, cmd.plane) - x_corr = update_corrections(node, cmd.s_domain - pre_measured_nodes, x_corr) - z_corr = update_corrections(node, cmd.t_domain - pre_measured_nodes, z_corr) - elif cmd.kind == CommandKind.X: - x_corr = update_corrections(cmd.node, cmd.domain - pre_measured_nodes, x_corr) - elif cmd.kind == CommandKind.Z: - z_corr = update_corrections(cmd.node, cmd.domain - pre_measured_nodes, z_corr) - - graph = nx.Graph(edges) - graph.add_nodes_from(nodes) - og = OpenGraph(graph, self.input_nodes, self.output_nodes, measurements) - - return flow.XZCorrections.from_measured_nodes_mapping(og, dict(x_corr), dict(z_corr)) + def extract_xzcorrections(self) -> XZCorrections[Measurement]: + return optimization.StandardizedPattern.from_pattern(self).extract_xzcorrections() def get_layers(self) -> tuple[int, dict[int, set[int]]]: """Construct layers(l_k) from dependency information. From 0ece2f2ad3f831ad3c43c79fea52e5a4a302643c Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 11 Dec 2025 11:52:09 +0100 Subject: [PATCH 39/58] Up docs --- graphix/pattern.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/graphix/pattern.py b/graphix/pattern.py index 34b24b14c..b5b2c46f1 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -19,7 +19,6 @@ import networkx as nx from typing_extensions import assert_never -import graphix.flow.core as flow from graphix import command, optimization, parameter from graphix.clifford import Clifford from graphix.command import Command, CommandKind @@ -40,6 +39,7 @@ from numpy.random import Generator + from graphix.flow.core import CausalFlow, GFlow from graphix.parameter import ExpressionOrFloat, ExpressionOrSupportsFloat, Parameter from graphix.sim import Backend, BackendState, Data @@ -953,43 +953,40 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: return oset, *generations[::-1] return generations[::-1] - def extract_causal_flow(self) -> flow.CausalFlow[Measurement]: + def extract_causal_flow(self) -> CausalFlow[Measurement]: """Extract the causal flow structure from the current measurement pattern. - This method reconstructs the underlying open graph, validates measurement constraints, builds correction dependencies, and verifies that the resulting :class:`flow.CausalFlow` satisfies all well-formedness conditions. + This method does not call the flow-extraction routine on the underlying open graph, but constructs the flow from the pattern corrections instead. Returns ------- - flow.CausalFlow[Measurement] + CausalFlow[Measurement] The causal flow associated with the current pattern. Raises ------ FlowError If the pattern: - - contains measurements in forbidden planes (XZ or YZ), - - assigns more than one correcting node to the same measured node, - - is empty, or - - fails the well-formedness checks for a valid causal flow. + - Contains measurements in forbidden planes (XZ or YZ), + - Is empty, or + - Induces a correction function and a partial order which fail the well-formedness checks for a valid causal flow. ValueError - If `N` commands in the pattern do not represent a |+⟩ state. - RunnabilityError - If the pattern is not runnable. + If `N` commands in the pattern do not represent a |+⟩ state or if the pattern corrections form closed loops. Notes ----- - A causal flow is a structural property of MBQC patterns ensuring that corrections can be assigned deterministically with *single-element* correcting sets and without requiring measurements in the XZ or YZ planes. + See :func:`optimization.StandardizedPattern.extract_causal_flow` for additional information on why it is required to standardized the pattern to extract a causal flow. """ return optimization.StandardizedPattern.from_pattern(self).extract_causal_flow() - def extract_gflow(self) -> flow.GFlow[Measurement]: + def extract_gflow(self) -> GFlow[Measurement]: """Extract the generalized flow (gflow) structure from the current measurement pattern. - The method reconstructs the underlying open graph, and determines the correction dependencies and the partial order required for a valid gflow. It then constructs and validates a :class:`flow.GFlow` object. + This method does not call the flow-extraction routine on the underlying open graph, but constructs the gflow from the pattern corrections instead. Returns ------- - flow.GFlow[Measurement] + GFlow[Measurement] The gflow associated with the current pattern. Raises @@ -998,14 +995,11 @@ def extract_gflow(self) -> flow.GFlow[Measurement]: If the pattern is empty or if the extracted structure does not satisfy the well-formedness conditions required for a valid gflow. ValueError - If `N` commands in the pattern do not represent a |+⟩ state. - RunnabilityError - If the pattern is not runnable. + If `N` commands in the pattern do not represent a |+⟩ state or if the pattern corrections form closed loops. Notes ----- - A gflow is a structural property of measurement-based quantum computation - (MBQC) patterns that ensures determinism and proper correction propagation. + See :func:`optimization.StandardizedPattern.extract_gflow` for additional information on why it is required to standardized the pattern to extract a gflow. """ return optimization.StandardizedPattern.from_pattern(self).extract_gflow() From a31fb7013ebf1440a8009eba87ce4e33a2a39ede Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 11 Dec 2025 16:12:07 +0100 Subject: [PATCH 40/58] Up docs --- graphix/optimization.py | 10 ++++++---- graphix/pattern.py | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index 88896057c..55e5652de 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -487,14 +487,15 @@ def extract_causal_flow(self) -> CausalFlow[Measurement]: In general, there may exist various layerings which represent the corrections of the pattern. To ensure that a given layering is compatible with the pattern's induced correction function, the partial order must be extracted from a standardized pattern. Commutation of entanglement commands with X and Z corrections in the standardization procedure may generate new corrections, which guarantees that all the topological information of the underlying graph is encoded in the extracted partial order. """ correction_function: dict[int, set[int]] = defaultdict(set) + pre_measured_nodes = set(self.results.keys()) # Not included in the flow. for m in self.m_list: if m.plane in {Plane.XZ, Plane.YZ}: raise FlowGenericError(FlowGenericErrorReason.XYPlane) - correction_function = _update_corrections(m.node, m.s_domain, correction_function) + correction_function = _update_corrections(m.node, m.s_domain - pre_measured_nodes, correction_function) for node, domain in self.x_dict.items(): - correction_function = _update_corrections(node, domain, correction_function) + correction_function = _update_corrections(node, domain - pre_measured_nodes, correction_function) og = ( self.extract_opengraph() @@ -530,14 +531,15 @@ def extract_gflow(self) -> GFlow[Measurement]: In general, there may exist various layerings which represent the corrections of the pattern. To ensure that a given layering is compatible with the pattern's induced correction function, the partial order must be extracted from a standardized pattern. Commutation of entanglement commands with X and Z corrections in the standardization procedure may generate new corrections, which guarantees that all the topological information of the underlying graph is encoded in the extracted partial order. """ correction_function: dict[int, set[int]] = defaultdict(set) + pre_measured_nodes = set(self.results.keys()) # Not included in the flow. for m in self.m_list: if m.plane in {Plane.XZ, Plane.YZ}: correction_function[m.node].add(m.node) - correction_function = _update_corrections(m.node, m.s_domain, correction_function) + correction_function = _update_corrections(m.node, m.s_domain - pre_measured_nodes, correction_function) for node, domain in self.x_dict.items(): - correction_function = _update_corrections(node, domain, correction_function) + correction_function = _update_corrections(node, domain - pre_measured_nodes, correction_function) og = ( self.extract_opengraph() diff --git a/graphix/pattern.py b/graphix/pattern.py index b5b2c46f1..62e83751c 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -975,7 +975,8 @@ def extract_causal_flow(self) -> CausalFlow[Measurement]: Notes ----- - See :func:`optimization.StandardizedPattern.extract_causal_flow` for additional information on why it is required to standardized the pattern to extract a causal flow. + - See :func:`optimization.StandardizedPattern.extract_causal_flow` for additional information on why it is required to standardized the pattern to extract a causal flow. + - Applying the chain ``Pattern.extract_causal_flow().to_corrections().to_pattern()`` to a strongly deterministic pattern returns a new pattern implementing the same unitary transformation. This equivalence holds as long as the original pattern contains no Clifford commands, since those are discarded during open-graph extraction. """ return optimization.StandardizedPattern.from_pattern(self).extract_causal_flow() @@ -999,7 +1000,8 @@ def extract_gflow(self) -> GFlow[Measurement]: Notes ----- - See :func:`optimization.StandardizedPattern.extract_gflow` for additional information on why it is required to standardized the pattern to extract a gflow. + - See :func:`optimization.StandardizedPattern.extract_gflow` for additional information on why it is required to standardized the pattern to extract a gflow. + - Applying the chain ``Pattern.extract_gflow().to_corrections().to_pattern()`` to a strongly deterministic pattern returns a new pattern implementing the same unitary transformation. This equivalence holds as long as the original pattern contains no Clifford commands, since those are discarded during open-graph extraction. """ return optimization.StandardizedPattern.from_pattern(self).extract_gflow() From 77e0719ee98ea9f898a43f74df88452b85212f8a Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 11 Dec 2025 16:28:10 +0100 Subject: [PATCH 41/58] Add docs and tests --- graphix/pattern.py | 20 ++++++++++++++++++++ tests/test_pattern.py | 11 ++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/graphix/pattern.py b/graphix/pattern.py index b06357560..1701b5e4b 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -1006,6 +1006,26 @@ def extract_gflow(self) -> GFlow[Measurement]: return optimization.StandardizedPattern.from_pattern(self).extract_gflow() def extract_xzcorrections(self) -> XZCorrections[Measurement]: + """Extract the XZ-corrections from the current measurement pattern. + + Returns + ------- + XZCorrections[Measurement] + The XZ-corrections associated with the current pattern. + + Raises + ------ + XZCorrectionsError + If the extracted correction dictionaries are not well formed. + ValueError + If `N` commands in the pattern do not represent a |+⟩ state or if the pattern corrections form closed loops. + + Notes + ----- + To ensure that applying the chain ``Pattern.extract_xzcorrections().to_pattern()`` to a strongly deterministic pattern returns a new pattern implementing the same unitary transformation, XZ-corrections must be extracted from a standardized pattern. This requirement arises for the same reason that flow extraction also operates correctly on standardized patterns only. + This equivalence holds as long as the original pattern contains no Clifford commands, since those are discarded during open-graph extraction. + See docstring in :func:`optimization.StandardizedPattern.extract_gflow` for additional information. + """ return optimization.StandardizedPattern.from_pattern(self).extract_xzcorrections() def get_layers(self) -> tuple[int, dict[int, set[int]]]: diff --git a/tests/test_pattern.py b/tests/test_pattern.py index 7531fbda7..c9101a8ea 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -1036,9 +1036,7 @@ def test_extract_xzc_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None: depth = 2 circuit_1 = rand_circuit(nqubits, depth, rng, use_ccx=False) p_ref = circuit_1.transpile().pattern - pc = p_ref.copy() - pc.standardize() - xzc = pc.extract_xzcorrections() + xzc = p_ref.extract_xzcorrections() xzc.check_well_formed() p_test = xzc.to_pattern() @@ -1049,6 +1047,13 @@ def test_extract_xzc_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None: s_test = p_test.simulate_pattern(rng=rng) assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + def test_extract_xzc_empty_domains(self) -> None: + p = Pattern(input_nodes=[0], cmds=[N(1), E((0, 1))]) + xzc = p.extract_xzcorrections() + assert dict(xzc.x_corrections) == {} + assert dict(xzc.z_corrections) == {} + assert xzc.partial_order_layers == (frozenset({0, 1}),) + def cp(circuit: Circuit, theta: float, control: int, target: int) -> None: """Controlled rotation gate, decomposed.""" # noqa: D401 From 8ebde0dd6265410191cf08952f4e00dddfd6e600 Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 11 Dec 2025 16:36:09 +0100 Subject: [PATCH 42/58] Remove gflow file --- graphix/find_pflow.py | 613 ------------------------- graphix/gflow.py | 950 --------------------------------------- graphix/visualization.py | 7 +- tests/test_gflow.py | 596 ------------------------ 4 files changed, 3 insertions(+), 2163 deletions(-) delete mode 100644 graphix/find_pflow.py delete mode 100644 graphix/gflow.py delete mode 100644 tests/test_gflow.py diff --git a/graphix/find_pflow.py b/graphix/find_pflow.py deleted file mode 100644 index 609c00cd8..000000000 --- a/graphix/find_pflow.py +++ /dev/null @@ -1,613 +0,0 @@ -"""Pauli flow finding algorithm. - -This module implements the algorithm presented in [1]. For a given labelled open graph (G, I, O, meas_plane), this algorithm finds a maximally delayed Pauli flow [2] in polynomial time with the number of nodes, :math:`O(N^3)`. -If the input graph does not have Pauli measurements, the algorithm returns a general flow (gflow) if it exists by definition. - -References ----------- -[1] Mitosek and Backens, 2024 (arXiv:2410.23439). -[2] Browne et al., 2007 New J. Phys. 9 250 (arXiv:quant-ph/0702212) -""" - -from __future__ import annotations - -from copy import deepcopy -from typing import TYPE_CHECKING - -import numpy as np - -from graphix._linalg import MatGF2, solve_f2_linear_system -from graphix.fundamentals import Axis, Plane -from graphix.measurements import PauliMeasurement -from graphix.sim.base_backend import NodeIndex - -if TYPE_CHECKING: - from collections.abc import Set as AbstractSet - - from graphix.measurements import Measurement - from graphix.opengraph import OpenGraph - - -class OpenGraphIndex: - """A class for managing the mapping between node numbers of a given open graph and matrix indices in the Pauli flow finding algorithm. - - It reuses the class `:class: graphix.sim.base_backend.NodeIndex` introduced for managing the mapping between node numbers and qubit indices in the internal state of the backend. - - Attributes - ---------- - og (OpenGraph) - non_inputs (NodeIndex) : Mapping between matrix indices and non-input nodes (labelled with integers). - non_outputs (NodeIndex) : Mapping between matrix indices and non-output nodes (labelled with integers). - non_outputs_optim (NodeIndex) : Mapping between matrix indices and a subset of non-output nodes (labelled with integers). - - Notes - ----- - At initialization, `non_outputs_optim` is a copy of `non_outputs`. The nodes corresponding to zero-rows of the order-demand matrix are removed for calculating the P matrix more efficiently in the `:func: _find_pflow_general` routine. - """ - - def __init__(self, og: OpenGraph[Measurement]) -> None: - self.og = og - nodes = set(og.graph.nodes) - - # Nodes don't need to be sorted. We do it for debugging purposes, so we can check the matrices in intermediate steps of the algorithm. - - nodes_non_input = sorted(nodes - set(og.input_nodes)) - nodes_non_output = sorted(nodes - set(og.output_nodes)) - - self.non_inputs = NodeIndex() - self.non_inputs.extend(nodes_non_input) - - self.non_outputs = NodeIndex() - self.non_outputs.extend(nodes_non_output) - - # Needs to be a deep copy because it may be modified during runtime. - self.non_outputs_optim = deepcopy(self.non_outputs) - - -def _compute_reduced_adj(ogi: OpenGraphIndex) -> MatGF2: - r"""Return the reduced adjacency matrix (RAdj) of the input open graph. - - Parameters - ---------- - ogi : OpenGraphIndex - Open graph whose RAdj is computed. - - Returns - ------- - adj_red : MatGF2 - Reduced adjacency matrix. - - Notes - ----- - The adjacency matrix of a graph :math:`Adj_G` is an :math:`n \times n` matrix. - - The RAdj matrix of an open graph OG is an :math:`(n - n_O) \times (n - n_I)` submatrix of :math:`Adj_G` constructed by removing the output rows and input columns of :math:`Adj_G`. - - See Definition 3.3 in Mitosek and Backens, 2024 (arXiv:2410.23439). - """ - graph = ogi.og.graph - row_tags = ogi.non_outputs - col_tags = ogi.non_inputs - - adj_red = np.zeros((len(row_tags), len(col_tags)), dtype=np.uint8).view(MatGF2) - - for n1, n2 in graph.edges: - for u, v in ((n1, n2), (n2, n1)): - if u in row_tags and v in col_tags: - i, j = row_tags.index(u), col_tags.index(v) - adj_red[i, j] = 1 - - return adj_red - - -def _compute_pflow_matrices(ogi: OpenGraphIndex) -> tuple[MatGF2, MatGF2]: - r"""Construct flow-demand and order-demand matrices. - - Parameters - ---------- - ogi : OpenGraphIndex - Open graph whose flow-demand and order-demand matrices are computed. - - Returns - ------- - flow_demand_matrix : MatGF2 - order_demand_matrix : MatGF2 - - Notes - ----- - See Definitions 3.4 and 3.5, and Algorithm 1 in Mitosek and Backens, 2024 (arXiv:2410.23439). - """ - flow_demand_matrix = _compute_reduced_adj(ogi) - order_demand_matrix = flow_demand_matrix.copy() - - inputs_set = set(ogi.og.input_nodes) - meas = ogi.og.measurements - - row_tags = ogi.non_outputs - col_tags = ogi.non_inputs - - # TODO: integrate pauli measurements in open graphs - meas_planes = {i: m.plane for i, m in meas.items()} - meas_angles = {i: m.angle for i, m in meas.items()} - meas_plane_axis = { - node: pm.axis if (pm := PauliMeasurement.try_from(plane, meas_angles[node])) else plane - for node, plane in meas_planes.items() - } - - for v in row_tags: # v is a node tag - i = row_tags.index(v) - plane_axis_v = meas_plane_axis[v] - - if plane_axis_v in {Plane.YZ, Plane.XZ, Axis.Z}: - flow_demand_matrix[i, :] = 0 # Set row corresponding to node v to 0 - if plane_axis_v in {Plane.YZ, Plane.XZ, Axis.Y, Axis.Z} and v not in inputs_set: - j = col_tags.index(v) - flow_demand_matrix[i, j] = 1 # Set element (v, v) = 0 - if plane_axis_v in {Plane.XY, Axis.X, Axis.Y, Axis.Z}: - order_demand_matrix[i, :] = 0 # Set row corresponding to node v to 0 - if plane_axis_v in {Plane.XY, Plane.XZ} and v not in inputs_set: - j = col_tags.index(v) - order_demand_matrix[i, j] = 1 # Set element (v, v) = 1 - - return flow_demand_matrix, order_demand_matrix - - -def _find_pflow_simple(ogi: OpenGraphIndex) -> tuple[MatGF2, MatGF2] | None: - r"""Construct the correction matrix :math:`C` and the ordering matrix, :math:`NC` for an open graph with equal number of inputs and outputs. - - Parameters - ---------- - ogi : OpenGraphIndex - Open graph for which :math:`C` and :math:`NC` are computed. - - Returns - ------- - correction_matrix : MatGF2 - Matrix encoding the correction function. - ordering_matrix : MatGF2 - Matrix encoding the partial ordering between nodes. - - or `None` - if the input open graph does not have Pauli flow. - - Notes - ----- - - The ordering matrix is defined as the product of the order-demand matrix :math:`N` and the correction matrix. - - - The function only returns `None` when the flow-demand matrix is not invertible (meaning that `ogi` does not have Pauli flow). The condition that the ordering matrix :math:`NC` must encode a directed acyclic graph (DAG) is verified in a subsequent step by `:func: _compute_topological_generations`. - - See Definitions 3.4, 3.5 and 3.6, Theorems 3.1 and 4.1, and Algorithm 2 in Mitosek and Backens, 2024 (arXiv:2410.23439). - """ - flow_demand_matrix, order_demand_matrix = _compute_pflow_matrices(ogi) - - correction_matrix = flow_demand_matrix.right_inverse() # C matrix - - if correction_matrix is None: - return None # The flow-demand matrix is not invertible, therefore there's no flow. - - ordering_matrix = order_demand_matrix.mat_mul(correction_matrix) # NC matrix - - return correction_matrix, ordering_matrix - - -def _compute_p_matrix(ogi: OpenGraphIndex, nb_matrix: MatGF2) -> MatGF2 | None: - r"""Perform the steps 8 - 12 of the general case (larger number of outputs than inputs) algorithm. - - Parameters - ---------- - ogi : OpenGraphIndex - Open graph for which the matrix :math:`P` is computed. - nb_matrix : MatGF2 - Matrix :math:`N_B` - - Returns - ------- - p_matrix : MatGF2 - Matrix encoding the correction function. - - or `None` - if the input open graph does not have Pauli flow. - - Notes - ----- - See Theorem 4.4, steps 8 - 12 in Mitosek and Backens, 2024 (arXiv:2410.23439). - """ - n_no = len(ogi.non_outputs) # number of columns of P matrix. - n_oi_diff = len(ogi.og.output_nodes) - len(ogi.og.input_nodes) # number of rows of P matrix. - n_no_optim = len(ogi.non_outputs_optim) # number of rows and columns of the third block of the K_{LS} matrix. - - # Steps 8, 9 and 10 - kils_matrix = np.concatenate( - (nb_matrix[:, n_no:], nb_matrix[:, :n_no], np.eye(n_no_optim, dtype=np.uint8)), axis=1 - ).view(MatGF2) # N_R | N_L | 1 matrix. - kls_matrix = kils_matrix.gauss_elimination(ncols=n_oi_diff, copy=True) # RREF form is not needed, only REF. - - # Step 11 - p_matrix = np.zeros((n_oi_diff, n_no), dtype=np.uint8).view(MatGF2) - solved_nodes: set[int] = set() - non_outputs_set = set(ogi.non_outputs) - - # Step 12 - while solved_nodes != non_outputs_set: - solvable_nodes = _find_solvable_nodes(ogi, kls_matrix, non_outputs_set, solved_nodes, n_oi_diff) # Step 12.a - if not solvable_nodes: - return None - - _update_p_matrix(ogi, kls_matrix, p_matrix, solvable_nodes, n_oi_diff) # Steps 12.b, 12.c - _update_kls_matrix(ogi, kls_matrix, kils_matrix, solvable_nodes, n_oi_diff, n_no, n_no_optim) # Step 12.d - solved_nodes.update(solvable_nodes) - - return p_matrix - - -def _find_solvable_nodes( - ogi: OpenGraphIndex, - kls_matrix: MatGF2, - non_outputs_set: AbstractSet[int], - solved_nodes: AbstractSet[int], - n_oi_diff: int, -) -> set[int]: - """Return the set nodes whose associated linear system is solvable. - - A node is solvable if: - - It has not been solved yet. - - Its column in the second block of :math:`K_{LS}` (which determines the constants in each equation) has only zeros where it intersects rows for which all the coefficients in the first block are 0s. - - See Theorem 4.4, step 12.a in Mitosek and Backens, 2024 (arXiv:2410.23439). - """ - solvable_nodes: set[int] = set() - - row_idxs = np.flatnonzero( - ~kls_matrix[:, :n_oi_diff].any(axis=1) - ) # Row indices of the 0-rows in the first block of K_{LS}. - if row_idxs.size: - for v in non_outputs_set - solved_nodes: - j = n_oi_diff + ogi.non_outputs.index(v) # `n_oi_diff` is the column offset from the first block of K_{LS}. - if not kls_matrix[row_idxs, j].any(): - solvable_nodes.add(v) - else: - # If the first block of K_{LS} does not have 0-rows, all non-solved nodes are solvable. - solvable_nodes = set(non_outputs_set - solved_nodes) - - return solvable_nodes - - -def _update_p_matrix( - ogi: OpenGraphIndex, kls_matrix: MatGF2, p_matrix: MatGF2, solvable_nodes: AbstractSet[int], n_oi_diff: int -) -> None: - """Update `p_matrix`. - - The solution of the linear system associated with node :math:`v` in `solvable_nodes` corresponds to the column of `p_matrix` associated with node :math:`v`. - - See Theorem 4.4, steps 12.b and 12.c in Mitosek and Backens, 2024 (arXiv:2410.23439). - """ - for v in solvable_nodes: - j = ogi.non_outputs.index(v) - j_shift = n_oi_diff + j # `n_oi_diff` is the column offset from the first block of K_{LS}. - mat = MatGF2(kls_matrix[:, :n_oi_diff]) # First block of K_{LS}, in row echelon form. - b = MatGF2(kls_matrix[:, j_shift]) - x = solve_f2_linear_system(mat, b) - p_matrix[:, j] = x - - -def _update_kls_matrix( - ogi: OpenGraphIndex, - kls_matrix: MatGF2, - kils_matrix: MatGF2, - solvable_nodes: AbstractSet[int], - n_oi_diff: int, - n_no: int, - n_no_optim: int, -) -> None: - """Update `kls_matrix`. - - Bring the linear system encoded in :math:`K_{LS}` to the row-echelon form (REF) that would be achieved by Gaussian elimination if the row and column vectors corresponding to vertices in `solvable_nodes` where not included in the starting matrix. - - See Theorem 4.4, step 12.d in Mitosek and Backens, 2024 (arXiv:2410.23439). - """ - shift = n_oi_diff + n_no # `n_oi_diff` + `n_no` is the column offset from the first two blocks of K_{LS}. - row_permutation: list[int] - - def reorder(old_pos: int, new_pos: int) -> None: # Used in step 12.d.vi - """Reorder the elements of `row_permutation`. - - The element at `old_pos` is placed on the right of the element at `new_pos`. - Example: - ``` - row_permutation = [0, 1, 2, 3, 4] - reorder(1, 3) -> [0, 2, 3, 1, 4] - reorder(2, -1) -> [2, 0, 1, 3, 4] - ``` - """ - val = row_permutation.pop(old_pos) - row_permutation.insert(new_pos + (new_pos < old_pos), val) - - for v in solvable_nodes: - if ( - v in ogi.non_outputs_optim - ): # if `v` corresponded to a zero row in N_B, it was not present in `kls_matrix` because we removed it in the optimization process, so there's no need to do Gaussian elimination for that vertex. - # Step 12.d.ii - j = ogi.non_outputs_optim.index(v) - j_shift = shift + j - row_idxs = np.flatnonzero( - kls_matrix[:, j_shift] - ).tolist() # Row indices with 1s in column of node `v` in third block. - - # `row_idxs` can't be empty: - # The third block of K_{LS} is initially the identity matrix, so all columns have initially a 1. Row permutations and row additions in the Gaussian elimination routine can't remove all 1s from a given column. - k = row_idxs.pop() - - # Step 12.d.iii - kls_matrix[row_idxs] ^= kls_matrix[k] # Adding a row to previous rows preserves REF. - - # Step 12.d.iv - kls_matrix[k] ^= kils_matrix[j] # Row `k` may now break REF. - - # Step 12.d.v - pivots: list[np.int_] = [] # Store pivots for next step. - for i, row in enumerate(kls_matrix): - if i != k: - col_idxs = np.flatnonzero(row[:n_oi_diff]) # Column indices with 1s in first block. - if col_idxs.size == 0: - # Row `i` has all zeros in the first block. Only row `k` can break REF, so rows below have all zeros in the first block too. - break - pivots.append(p := col_idxs[0]) - if kls_matrix[k, p]: # Row `k` has a 1 in the column corresponding to the leading 1 of row `i`. - kls_matrix[k] ^= row - - row_permutation = list(range(n_no_optim)) # Row indices of `kls_matrix`. - n_pivots = len(pivots) - - col_idxs = np.flatnonzero(kls_matrix[k, :n_oi_diff]) - pk = col_idxs[0] if col_idxs.size else None # Pivot of row `k`. - - if pk and k >= n_pivots: # Row `k` is non-zero in the FB (first block) and it's among zero rows. - # Find row `new_pos` s.t. `pivots[new_pos] <= pk < pivots[new_pos+1]`. - new_pos = ( - int(np.argmax(np.array(pivots) > pk) - 1) if pivots else -1 - ) # `pivots` can be empty. If so, we bring row `k` to the top since it's non-zero. - elif pk: # Row `k` is non-zero in the FB and it's among non-zero rows. - # Find row `new_pos` s.t. `pivots[new_pos] <= pk < pivots[new_pos+1]` - new_pos = int(np.argmax(np.array(pivots) > pk) - 1) - # We skipped row `k` in loop of step 12.d.v, so `pivots[j]` can be the pivot of row `j` or `j+1`. - if new_pos >= k: - new_pos += 1 - elif k < n_pivots: # Row `k` is zero in the first block and it's among non-zero rows. - new_pos = ( - n_pivots # Move row `k` to the top of the zeros block (i.e., below the row of the last pivot). - ) - else: # Row `k` is zero in the first block and it's among zero rows. - new_pos = k # Do nothing. - - if new_pos != k: - reorder(k, new_pos) # Modify `row_permutation` in-place. - kls_matrix[:] = kls_matrix[ - row_permutation - ] # `[:]` is crucial to modify the data pointed by `kls_matrix`. - - -def _find_pflow_general(ogi: OpenGraphIndex) -> tuple[MatGF2, MatGF2] | None: - r"""Construct the generalized correction matrix :math:`C'C^B` and the generalized ordering matrix, :math:`NC'C^B` for an open graph with larger number of outputs than inputs. - - Parameters - ---------- - ogi : OpenGraphIndex - Open graph for which :math:`C'C^B` and :math:`NC'C^B` are computed. - - Returns - ------- - correction_matrix : MatGF2 - Matrix encoding the correction function. - ordering_matrix : MatGF2 - Matrix encoding the partial ordering between nodes. - - or `None` - if the input open graph does not have Pauli flow. - - Notes - ----- - - The function returns `None` if - a) The flow-demand matrix is not invertible, or - b) Not all linear systems of equations associated to the non-output nodes are solvable, - meaning that `ogi` does not have Pauli flow. - Condition (b) is satisfied when the flow-demand matrix :math:`M` does not have a right inverse :math:`C` such that :math:`NC` represents a directed acyclical graph (DAG). - - See Theorem 4.4 and Algorithm 3 in Mitosek and Backens, 2024 (arXiv:2410.23439). - """ - n_no = len(ogi.non_outputs) - n_oi_diff = len(ogi.og.output_nodes) - len(ogi.og.input_nodes) - - # Steps 1 and 2 - flow_demand_matrix, order_demand_matrix = _compute_pflow_matrices(ogi) - - # Steps 3 and 4 - correction_matrix_0 = flow_demand_matrix.right_inverse() # C0 matrix. - if correction_matrix_0 is None: - return None # The flow-demand matrix is not invertible, therefore there's no flow. - - # Steps 5, 6 and 7 - ker_flow_demand_matrix = flow_demand_matrix.null_space().transpose() # F matrix. - c_prime_matrix = np.concatenate((correction_matrix_0, ker_flow_demand_matrix), axis=1).view(MatGF2) - - row_idxs = np.flatnonzero(order_demand_matrix.any(axis=1)) # Row indices of the non-zero rows. - - if row_idxs.size: - # The p-matrix finding algorithm runs on the `order_demand_matrix` without the zero rows. - # This optimization is allowed because: - # - The zero rows remain zero after the change of basis (multiplication by `c_prime_matrix`). - # - The zero rows remain zero after gaussian elimination. - # - Removing the zero rows does not change the solvability condition of the open graph nodes. - nb_matrix_optim = ( - order_demand_matrix[row_idxs].view(MatGF2).mat_mul(c_prime_matrix) - ) # `view` is used to keep mypy happy without copying data. - for i in set(range(order_demand_matrix.shape[0])).difference(row_idxs): - ogi.non_outputs_optim.remove(ogi.non_outputs[i]) # Update the node-index mapping. - - # Steps 8 - 12 - if (p_matrix := _compute_p_matrix(ogi, nb_matrix_optim)) is None: - return None - else: - # If all rows of `order_demand_matrix` are zero, any matrix will solve the associated linear system of equations. - p_matrix = np.zeros((n_oi_diff, n_no), dtype=np.uint8).view(MatGF2) - - # Step 13 - cb_matrix = np.concatenate((np.eye(n_no, dtype=np.uint8), p_matrix), axis=0).view(MatGF2) - - correction_matrix = c_prime_matrix.mat_mul(cb_matrix) - ordering_matrix = order_demand_matrix.mat_mul(correction_matrix) - - return correction_matrix, ordering_matrix - - -def _compute_topological_generations(ordering_matrix: MatGF2) -> list[list[int]] | None: - """Stratify the directed acyclic graph (DAG) represented by the ordering matrix into generations. - - Parameters - ---------- - ordering_matrix : MatGF2 - Matrix encoding the partial ordering between nodes interpreted as the adjacency matrix of a directed graph. - - Returns - ------- - list[list[int]] - Topological generations. Integers represent the indices of the matrix `ordering_matrix`, not the labelling of the nodes. - - or `None` - if `ordering_matrix` is not a DAG. - - Notes - ----- - This function is adapted from `:func: networkx.algorithms.dag.topological_generations` so that it works directly on the adjacency matrix (which is the output of the Pauli-flow finding algorithm) instead of a `:class: nx.DiGraph` object. This avoids calling the function `nx.from_numpy_array` which can be expensive for certain graph instances. - - Here we use the convention that the element `ordering_matrix[i,j]` represents a link `j -> i`. NetworkX uses the opposite convention. - """ - adj_mat = ordering_matrix - - indegree_map: dict[int, int] = {} - zero_indegree: list[int] = [] - neighbors = {node: set(np.flatnonzero(row).astype(int)) for node, row in enumerate(adj_mat.T)} - for node, col in enumerate(adj_mat): - parents = np.flatnonzero(col) - if parents.size: - indegree_map[node] = parents.size - else: - zero_indegree.append(node) - - generations: list[list[int]] = [] - - while zero_indegree: - this_generation = zero_indegree - zero_indegree = [] - for node in this_generation: - for child in neighbors[node]: - indegree_map[child] -= 1 - if indegree_map[child] == 0: - zero_indegree.append(child) - del indegree_map[child] - generations.append(this_generation) - - if indegree_map: - return None - return generations - - -def _cnc_matrices2pflow( - ogi: OpenGraphIndex, - correction_matrix: MatGF2, - ordering_matrix: MatGF2, -) -> tuple[dict[int, set[int]], dict[int, int]] | None: - r"""Transform the correction and ordering matrices into a Pauli flow in its standard form (correction function and partial order). - - Parameters - ---------- - ogi : OpenGraphIndex - Open graph whose Pauli flow is calculated. - correction_matrix : MatGF2 - Matrix encoding the correction function. - ordering_matrix : MatGF2 - Matrix encoding the partial ordering between nodes (DAG). - - Returns - ------- - pf : dict[int, set[int]] - Pauli flow correction function. pf[i] is the set of qubits to be corrected for the measurement of qubit i. - l_k : dict[int, int] - Partial order between corrected qubits, such that the pair (`key`, `value`) corresponds to (node, depth). - - or `None` - if the ordering matrix is not a DAG, in which case the input open graph does not have Pauli flow. - - Notes - ----- - - The correction matrix :math:`C` is an :math:`(n - n_I) \times (n - n_O)` matrix related to the correction function :math:`c(v) = \{u \in I^c|C_{u,v} = 1\}`, where :math:`I^c` are the non-input nodes of `ogi`. In other words, the column :math:`v` of :math:`C` encodes the correction set of :math:`v`, :math:`c(v)`. - - - The Pauli flow's ordering :math:`<_c` is the transitive closure of :math:`\lhd_c`, where the latter is related to the ordering matrix :math:`NC` as :math:`v \lhd_c w \Leftrightarrow (NC)_{w,v} = 1`, for :math:`v, w, \in O^c` two non-output nodes of `ogi`. - - See Definition 3.6, Lemma 3.12, and Theorem 3.1 in Mitosek and Backens, 2024 (arXiv:2410.23439). - """ - row_tags = ogi.non_inputs - col_tags = ogi.non_outputs - - # Calculation of the partial ordering - - if (topo_gen := _compute_topological_generations(ordering_matrix)) is None: - return None # The NC matrix is not a DAG, therefore there's no flow. - - l_k = dict.fromkeys(ogi.og.output_nodes, 0) # Output nodes are always in layer 0. - - # If m >_c n, with >_c the flow order for two nodes m, n, then layer(n) > layer(m). - # Therefore, we iterate the topological sort of the graph in _reverse_ order to obtain the order of measurements. - for layer, idx in enumerate(reversed(topo_gen), start=1): - l_k.update({col_tags[i]: layer for i in idx}) - - # Calculation of the correction function - - pf: dict[int, set[int]] = {} - for node in col_tags: - i = col_tags.index(node) - correction_set = {row_tags[j] for j in np.flatnonzero(correction_matrix[:, i])} - pf[node] = correction_set - - return pf, l_k - - -def find_pflow(og: OpenGraph[Measurement]) -> tuple[dict[int, set[int]], dict[int, int]] | None: - """Return a Pauli flow of the input open graph if it exists. - - Parameters - ---------- - og : OpenGraph - Open graph whose Pauli flow is calculated. - - Returns - ------- - pf : dict[int, set[int]] - Pauli flow correction function. `pf[i]` is the set of qubits to be corrected for the measurement of qubit `i`. - l_k : dict[int, int] - Partial order between corrected qubits, such that the pair (`key`, `value`) corresponds to (node, depth). - - or `None` - if the input open graph does not have Pauli flow. - - Notes - ----- - See Theorems 3.1, 4.2 and 4.4, and Algorithms 2 and 3 in Mitosek and Backens, 2024 (arXiv:2410.23439). - """ - ni = len(og.input_nodes) - no = len(og.output_nodes) - - if ni > no: - return None - - ogi = OpenGraphIndex(og) - - cnc_matrices = _find_pflow_simple(ogi) if ni == no else _find_pflow_general(ogi) - if cnc_matrices is None: - return None - pflow = _cnc_matrices2pflow(ogi, *cnc_matrices) - if pflow is None: - return None - - pf, l_k = pflow - - return pf, l_k diff --git a/graphix/gflow.py b/graphix/gflow.py deleted file mode 100644 index 62a7c8499..000000000 --- a/graphix/gflow.py +++ /dev/null @@ -1,950 +0,0 @@ -"""Flow finding algorithm. - -For a given underlying graph (G, I, O, meas_plane), this method finds a (generalized) flow [NJP 9, 250 (2007)] in polynomial time. -In particular, this outputs gflow with minimum depth, maximally delayed gflow. - -Ref: Mhalla and Perdrix, International Colloquium on Automata, -Languages, and Programming (Springer, 2008), pp. 857-868. -Ref: Backens et al., Quantum 5, 421 (2021). - -""" - -from __future__ import annotations - -from copy import deepcopy -from typing import TYPE_CHECKING - -from typing_extensions import assert_never - -import graphix.find_pflow -import graphix.opengraph -from graphix.command import CommandKind -from graphix.fundamentals import Axis, Plane -from graphix.measurements import Measurement, PauliMeasurement -from graphix.parameter import Placeholder - -if TYPE_CHECKING: - from collections.abc import Mapping - from collections.abc import Set as AbstractSet - - import networkx as nx - - from graphix.parameter import ExpressionOrFloat - from graphix.pattern import Pattern - - -# TODO: This should be ensured by type-checking. -def check_meas_planes(meas_planes: dict[int, Plane]) -> None: - """Check that all planes are valid planes.""" - for node, plane in meas_planes.items(): - if not isinstance(plane, Plane): - raise TypeError(f"Measure plane for {node} is `{plane}`, which is not an instance of `Plane`") - - -# NOTE: In a future version this function will take an `OpenGraph` object as input. -def find_gflow( - graph: nx.Graph[int], - iset: AbstractSet[int], - oset: AbstractSet[int], - meas_planes: Mapping[int, Plane], - mode: str = "single", # noqa: ARG001 Compatibility with old API -) -> tuple[dict[int, set[int]], dict[int, int]] | tuple[None, None]: - r"""Return a maximally delayed general flow (gflow) of the input open graph if it exists. - - Parameters - ---------- - graph: :class:`networkx.Graph` - Graph (including input and output). - iset: AbstractSet[int] - Set of input nodes. - oset: AbstractSet[int] - Set of output nodes. - meas_planes: Mapping[int, Plane] - Measurement planes for each qubit. meas_planes[i] is the measurement plane for qubit i. - mode: str - Deprecated. Reminiscent of old API, it will be removed in future versions. - - Returns - ------- - dict[int, set[int]] - Gflow correction function. In a given pair (key, value), value is the set of qubits to be corrected for the measurement of qubit key. - dict[int, int] - Partial order between corrected qubits, such that the pair (key, value) corresponds to (node, depth). - - or None, None - if the input open graph does not have gflow. - - Notes - ----- - This function implements the algorithm in [1], see module graphix.find_pflow. - See [1] or [2] for a definition of gflow. - - References - ---------- - [1] Mitosek and Backens, 2024 (arXiv:2410.23439). - [2] Backens et al., Quantum 5, 421 (2021). - """ - meas = {node: Measurement(Placeholder("Angle"), plane) for node, plane in meas_planes.items()} - og = graphix.opengraph.OpenGraph( - graph=graph, - input_nodes=list(iset), - output_nodes=list(oset), - measurements=meas, - ) - gf = graphix.find_pflow.find_pflow(og) - if gf is None: - return None, None # This is to comply with old API. It will be change in the future to `None`` - return gf[0], gf[1] - - -def find_flow( - graph: nx.Graph[int], - iset: set[int], - oset: set[int], - meas_planes: dict[int, Plane] | None = None, -) -> tuple[dict[int, set[int]], dict[int, int]] | tuple[None, None]: - """Causal flow finding algorithm. - - For open graph g with input, output, and measurement planes, this returns causal flow. - For more detail of causal flow, see Danos and Kashefi, PRA 74, 052310 (2006). - - Original algorithm by Mhalla and Perdrix, - International Colloquium on Automata, Languages, and Programming (2008), - pp. 857-868. - - Parameters - ---------- - graph: :class:`networkx.Graph` - Graph (incl. input and output) - iset: set - set of node labels for input - oset: set - set of node labels for output - meas_planes: dict(int, Plane) - measurement planes for each qubits. meas_planes[i] is the measurement plane for qubit i. - Note that an underlying graph has a causal flow only if all measurement planes are Plane.XY. - If not specified, all measurement planes are interpreted as Plane.XY. - - Returns - ------- - f: list of nodes - causal flow function. f[i] is the qubit to be measured after qubit i. - l_k: dict - layers obtained by gflow algorithm. l_k[d] is a node set of depth d. - """ - check_meas_planes(meas_planes) - nodes = set(graph.nodes) - edges = set(graph.edges) - - if meas_planes is None: - meas_planes = dict.fromkeys(nodes - oset, Plane.XY) - - for plane in meas_planes.values(): - if plane != Plane.XY: - return None, None - - l_k = dict.fromkeys(nodes, 0) - f = {} - k = 1 - v_c = oset - iset - return flowaux(nodes, edges, iset, oset, v_c, f, l_k, k) - - -def flowaux( - nodes: set[int], - edges: set[tuple[int, int]], - iset: set[int], - oset: set[int], - v_c: set[int], - f: dict[int, set[int]], - l_k: dict[int, int], - k: int, -): - """Find one layer of the flow. - - Ref: Mhalla and Perdrix, International Colloquium on Automata, - Languages, and Programming (Springer, 2008), pp. 857-868. - - Parameters - ---------- - nodes: set - labels of all qubits (nodes) - edges: set - edges - iset: set - set of node labels for input - oset: set - set of node labels for output - v_c: set - correction candidate qubits - f: dict - flow function. f[i] is the qubit to be measured after qubit i. - l_k: dict - layers obtained by flow algorithm. l_k[d] is a node set of depth d. - k: int - current layer number. - meas_planes: dict - measurement planes for each qubits. meas_planes[i] is the measurement plane for qubit i. - - Outputs - ------- - f: list of nodes - causal flow function. f[i] is the qubit to be measured after qubit i. - l_k: dict - layers obtained by gflow algorithm. l_k[d] is a node set of depth d. - """ - v_out_prime = set() - c_prime = set() - - for q in v_c: - nb = search_neighbor(q, edges) - p_set = nb & (nodes - oset) - if len(p_set) == 1: - # Iterate over p_set assuming there is only one element p - (p,) = p_set - f[p] = {q} - l_k[p] = k - v_out_prime |= {p} - c_prime |= {q} - # determine whether there exists flow - if not v_out_prime: - if oset == nodes: - return f, l_k - return None, None - return flowaux( - nodes, - edges, - iset, - oset | v_out_prime, - (v_c - c_prime) | (v_out_prime & (nodes - iset)), - f, - l_k, - k + 1, - ) - - -# NOTE: In a future version this function will take an `OpenGraph` object as input. -def find_pauliflow( - graph: nx.Graph[int], - iset: AbstractSet[int], - oset: AbstractSet[int], - meas_planes: Mapping[int, Plane], - meas_angles: Mapping[int, ExpressionOrFloat], - mode: str = "single", # noqa: ARG001 Compatibility with old API -) -> tuple[dict[int, set[int]], dict[int, int]] | tuple[None, None]: - r"""Return a maximally delayed Pauli flow of the input open graph if it exists. - - Parameters - ---------- - graph: :class:`networkx.Graph` - Graph (including input and output). - iset: AbstractSet[int] - Set of input nodes. - oset: AbstractSet[int] - Set of output nodes. - meas_planes: Mapping[int, Plane] - Measurement planes for each qubit. meas_planes[i] is the measurement plane for qubit i. - meas_angles: Mapping[int, ExpressionOrFloat] - Measurement angles for each qubit. meas_angles[i] is the measurement angle for qubit i. - mode: str - Deprecated. Reminiscent of old API, it will be removed in future versions. - - Returns - ------- - dict[int, set[int]] - Pauli flow correction function. In a given pair (key, value), value is the set of qubits to be corrected for the measurement of qubit key. - dict[int, int] - Partial order between corrected qubits, such that the pair (key, value) corresponds to (node, depth). - - or None, None - if the input open graph does not have gflow. - - Notes - ----- - This function implements the algorithm in [1], see module graphix.find_pflow. - See [1] or [2] for a definition of Pauli flow. - - References - ---------- - [1] Mitosek and Backens, 2024 (arXiv:2410.23439). - [2] Browne et al., 2007 New J. Phys. 9 250 (arXiv:quant-ph/0702212) - """ - meas = {node: Measurement(angle, meas_planes[node]) for node, angle in meas_angles.items()} - og = graphix.opengraph.OpenGraph( - graph=graph, - input_nodes=list(iset), - output_nodes=list(oset), - measurements=meas, - ) - pf = graphix.find_pflow.find_pflow(og) - if pf is None: - return None, None # This is to comply with old API. It will be change in the future to `None`` - return pf[0], pf[1] - - -def flow_from_pattern(pattern: Pattern) -> tuple[dict[int, set[int]], dict[int, int]] | tuple[None, None]: - """Check if the pattern has a valid flow. If so, return the flow and layers. - - Parameters - ---------- - pattern: Pattern - pattern to be based on - - Returns - ------- - None, None: - The tuple ``(None, None)`` is returned if the pattern does not have a valid causal flow. - f: dict[int, set[int]] - flow function. g[i] is the set of qubits to be corrected for the measurement of qubit i. - l_k: dict[int, int] - layers obtained by flow algorithm. l_k[d] is a node set of depth d. - """ - if not pattern.is_standard(strict=True): - raise ValueError("The pattern should be standardized first.") - meas_planes = pattern.get_meas_plane() - for plane in meas_planes.values(): - if plane != Plane.XY: - return None, None - graph = pattern.extract_graph() - input_nodes = pattern.input_nodes if not pattern.input_nodes else set() - output_nodes = set(pattern.output_nodes) - - layers = pattern.get_layers() - l_k = {} - for l in layers[1]: - for n in layers[1][l]: - l_k[n] = l - lmax = max(l_k.values()) if l_k else 0 - for node, val in l_k.items(): - l_k[node] = lmax - val + 1 - for output_node in pattern.output_nodes: - l_k[output_node] = 0 - - xflow, zflow = get_corrections_from_pattern(pattern) - - if verify_flow(graph, input_nodes, output_nodes, xflow): # if xflow is valid - zflow_from_xflow = {} - for node, corrections in deepcopy(xflow).items(): - cand = find_odd_neighbor(graph, corrections) - {node} - if cand: - zflow_from_xflow[node] = cand - if zflow_from_xflow != zflow: # if zflow is consistent with xflow - return None, None - return xflow, l_k - return None, None - - -def gflow_from_pattern(pattern: Pattern) -> tuple[dict[int, set[int]], dict[int, int]] | tuple[None, None]: - """Check if the pattern has a valid gflow. If so, return the gflow and layers. - - Parameters - ---------- - pattern: Pattern - pattern to be based on - - Returns - ------- - None, None: - The tuple ``(None, None)`` is returned if the pattern does not have a valid gflow. - g: dict[int, set[int]] - gflow function. g[i] is the set of qubits to be corrected for the measurement of qubit i. - l_k: dict[int, int] - layers obtained by gflow algorithm. l_k[d] is a node set of depth d. - """ - if not pattern.is_standard(strict=True): - raise ValueError("The pattern should be standardized first.") - graph = pattern.extract_graph() - input_nodes = set(pattern.input_nodes) if pattern.input_nodes else set() - output_nodes = set(pattern.output_nodes) - meas_planes = pattern.get_meas_plane() - - layers = pattern.get_layers() - l_k = {} - for l in layers[1]: - for n in layers[1][l]: - l_k[n] = l - lmax = max(l_k.values()) if l_k else 0 - for node, val in l_k.items(): - l_k[node] = lmax - val + 1 - for output_node in pattern.output_nodes: - l_k[output_node] = 0 - - xflow, zflow = get_corrections_from_pattern(pattern) - for node, plane in meas_planes.items(): - if plane in {Plane.XZ, Plane.YZ}: - if node not in xflow: - xflow[node] = {node} - xflow[node] |= {node} - - if verify_gflow(graph, input_nodes, output_nodes, xflow, meas_planes): # if xflow is valid - zflow_from_xflow = {} - for node, corrections in deepcopy(xflow).items(): - cand = find_odd_neighbor(graph, corrections) - {node} - if cand: - zflow_from_xflow[node] = cand - if zflow_from_xflow != zflow: # if zflow is consistent with xflow - return None, None - return xflow, l_k - return None, None - - -# TODO: Shouldn't call `find_pauliflow` -def pauliflow_from_pattern( - pattern: Pattern, - mode="single", # noqa: ARG001 Compatibility with old API -) -> tuple[dict[int, set[int]], dict[int, int]] | tuple[None, None]: - """Check if the pattern has a valid Pauliflow. If so, return the Pauliflow and layers. - - Parameters - ---------- - pattern: Pattern - pattern to be based on - mode: str - The Pauliflow finding algorithm can yield multiple equivalent solutions. So there are two options - - "single": Returns a single solution - - "all": Returns all possible solutions - - Optional. Default is "single". - - Returns - ------- - None, None: - The tuple ``(None, None)`` is returned if the pattern does not have a valid Pauli flow. - p: dict[int, set[int]] - Pauli flow function. p[i] is the set of qubits to be corrected for the measurement of qubit i. - l_k: dict[int, int] - layers obtained by Pauli flow algorithm. l_k[d] is a node set of depth d. - """ - if not pattern.is_standard(strict=True): - raise ValueError("The pattern should be standardized first.") - graph = pattern.extract_graph() - input_nodes = set(pattern.input_nodes) if pattern.input_nodes else set() - output_nodes = set(pattern.output_nodes) if pattern.output_nodes else set() - meas_planes = pattern.get_meas_plane() - meas_angles = pattern.get_angles() - - return find_pauliflow(graph, input_nodes, output_nodes, meas_planes, meas_angles) - - -def get_corrections_from_pattern(pattern: Pattern) -> tuple[dict[int, set[int]], dict[int, set[int]]]: - """Get x and z corrections from pattern. - - Parameters - ---------- - pattern: graphix.Pattern object - pattern to be based on - - Returns - ------- - xflow: dict - xflow function. xflow[i] is the set of qubits to be corrected in the X basis for the measurement of qubit i. - zflow: dict - zflow function. zflow[i] is the set of qubits to be corrected in the Z basis for the measurement of qubit i. - """ - nodes = pattern.extract_nodes() - xflow = {} - zflow = {} - for cmd in pattern: - if cmd.kind == CommandKind.M: - target = cmd.node - xflow_source = cmd.s_domain & nodes - zflow_source = cmd.t_domain & nodes - for node in xflow_source: - if node not in xflow: - xflow[node] = set() - xflow[node] |= {target} - for node in zflow_source: - if node not in zflow: - zflow[node] = set() - zflow[node] |= {target} - if cmd.kind == CommandKind.X: - target = cmd.node - xflow_source = cmd.domain & nodes - for node in xflow_source: - if node not in xflow: - xflow[node] = set() - xflow[node] |= {target} - if cmd.kind == CommandKind.Z: - target = cmd.node - zflow_source = cmd.domain & nodes - for node in zflow_source: - if node not in zflow: - zflow[node] = set() - zflow[node] |= {target} - return xflow, zflow - - -def search_neighbor(node: int, edges: set[tuple[int, int]]) -> set[int]: - """Find neighborhood of node in edges. This is an ancillary method for `flowaux()`. - - Parameter - ------- - node: int - target node number whose neighboring nodes will be collected - edges: set of taples - set of edges in the graph - - Outputs - ------ - N: list of ints - neighboring nodes - """ - nb = set() - for edge in edges: - if node == edge[0]: - nb |= {edge[1]} - elif node == edge[1]: - nb |= {edge[0]} - return nb - - -def get_min_depth(l_k: Mapping[int, int]) -> int: - """Get minimum depth of graph. - - Parameters - ---------- - l_k: dict - layers obtained by flow or gflow - - Returns - ------- - d: int - minimum depth of graph - """ - return max(l_k.values()) - - -def find_odd_neighbor(graph: nx.Graph[int], vertices: AbstractSet[int]) -> set[int]: - """Return the set containing the odd neighbor of a set of vertices. - - Parameters - ---------- - graph : :class:`networkx.Graph` - Underlying graph - vertices : set - set of nodes indices to find odd neighbors - - Returns - ------- - odd_neighbors : set - set of indices for odd neighbor of set `vertices`. - """ - odd_neighbors = set() - for vertex in vertices: - neighbors = set(graph.neighbors(vertex)) - odd_neighbors ^= neighbors - return odd_neighbors - - -def get_layers(l_k: Mapping[int, int]) -> tuple[int, dict[int, set[int]]]: - """Get components of each layer. - - Parameters - ---------- - l_k: dict - layers obtained by flow or gflow algorithms - - Returns - ------- - d: int - minimum depth of graph - layers: dict of set - components of each layer - """ - d = get_min_depth(l_k) - layers: dict[int, set[int]] = {k: set() for k in range(d + 1)} - for i, val in l_k.items(): - layers[val] |= {i} - return d, layers - - -def get_dependence_flow( - inputs: set[int], - flow: dict[int, set[int]], - odd_flow: dict[int, set[int]], -) -> dict[int, set[int]]: - """Get dependence flow from flow. - - Parameters - ---------- - inputs: set[int] - set of input nodes - flow: dict[int, set] - flow function. flow[i] is the set of qubits to be corrected for the measurement of qubit i. - odd_flow: dict[int, set] - odd neighbors of flow or gflow. - odd_flow[i] is the set of odd neighbors of f(i), Odd(f(i)). - - Returns - ------- - dependence_flow: dict[int, set] - dependence flow function. dependence_flow[i] is the set of qubits to be corrected for the measurement of qubit i. - """ - dependence_flow = {u: set() for u in inputs} - # concatenate flow and odd_flow - combined_flow = {} - for node, corrections in flow.items(): - combined_flow[node] = corrections | odd_flow[node] - for node, corrections in combined_flow.items(): - for correction in corrections: - if correction not in dependence_flow: - dependence_flow[correction] = set() - dependence_flow[correction] |= {node} - return dependence_flow - - -def get_dependence_pauliflow( - inputs: set[int], - flow: dict[int, set[int]], - odd_flow: dict[int, set[int]], - ls: tuple[set[int], set[int], set[int]], -): - """Get dependence flow from Pauli flow. - - Parameters - ---------- - inputs: set[int] - set of input nodes - flow: dict[int, set[int]] - Pauli flow function. p[i] is the set of qubits to be corrected for the measurement of qubit i. - odd_flow: dict[int, set[int]] - odd neighbors of Pauli flow or gflow. Odd(p(i)) - ls: tuple - ls = (l_x, l_y, l_z) where l_x, l_y, l_z are sets of qubits whose measurement operators are X, Y, Z, respectively. - - Returns - ------- - dependence_pauliflow: dict[int, set[int]] - dependence flow function. dependence_pauliflow[i] is the set of qubits to be corrected for the measurement of qubit i. - """ - l_x, l_y, l_z = ls - dependence_pauliflow = {u: set() for u in inputs} - # concatenate p and odd_p - combined_flow = {} - for node, corrections in flow.items(): - combined_flow[node] = (corrections - (l_x | l_y)) | (odd_flow[node] - (l_y | l_z)) - for ynode in l_y: - if ynode in corrections.symmetric_difference(odd_flow[node]): - combined_flow[node] |= {ynode} - for node, corrections in combined_flow.items(): - for correction in corrections: - if correction not in dependence_pauliflow: - dependence_pauliflow[correction] = set() - dependence_pauliflow[correction] |= {node} - return dependence_pauliflow - - -def get_layers_from_flow( - flow: dict[int, set], - odd_flow: dict[int, set], - inputs: set[int], - outputs: set[int], - ls: tuple[set[int], set[int], set[int]] | None = None, -) -> tuple[dict[int, set], int]: - """Get layers from flow (incl. gflow, Pauli flow). - - Parameters - ---------- - flow: dict[int, set] - flow function. flow[i] is the set of qubits to be corrected for the measurement of qubit i. - odd_flow: dict[int, set] - odd neighbors of flow or gflow. Odd(f(node)) - inputs: set - set of input nodes - outputs: set - set of output nodes - ls: tuple - ls = (l_x, l_y, l_z) where l_x, l_y, l_z are sets of qubits whose measurement operators are X, Y, Z, respectively. - If not None, the layers are obtained based on Pauli flow. - - Returns - ------- - layers: dict[int, set] - layers obtained from flow - depth: int - depth of the layers - - Raises - ------ - ValueError - If the flow is not valid(e.g. there is no partial order). - """ - layers = {} - depth = 0 - if ls is None: - dependence_flow = get_dependence_flow(inputs, odd_flow, flow) - else: - dependence_flow = get_dependence_pauliflow(inputs, flow, odd_flow, ls) - left_nodes = set(flow.keys()) - for output in outputs: - if output in left_nodes: - raise ValueError("Invalid flow") - while True: - layers[depth] = set() - for node in left_nodes: - if node not in dependence_flow or len(dependence_flow[node]) == 0 or dependence_flow[node] == {node}: - layers[depth] |= {node} - left_nodes -= layers[depth] - for node in left_nodes: - dependence_flow[node] -= layers[depth] - if len(layers[depth]) == 0: - if len(left_nodes) == 0: - layers[depth] = outputs - depth += 1 - break - raise ValueError("Invalid flow") - depth += 1 - return layers, depth - - -def verify_flow( - graph: nx.Graph, - iset: set[int], - oset: set[int], - flow: dict[int, set], - meas_planes: dict[int, Plane] | None = None, -) -> bool: - """Check whether the flow is valid. - - Parameters - ---------- - graph: :class:`networkx.Graph` - Graph (incl. input and output) - flow: dict[int, set] - flow function. flow[i] is the set of qubits to be corrected for the measurement of qubit i. - meas_planes: dict[int, str] - optional: measurement planes for each qubits. meas_planes[i] is the measurement plane for qubit i. - - - Returns - ------- - valid_flow: bool - True if the flow is valid. False otherwise. - """ - if meas_planes is None: - meas_planes = {} - check_meas_planes(meas_planes) - valid_flow = True - non_outputs = set(graph.nodes) - oset - # if meas_planes is given, check whether all measurement planes are "XY" - for node, plane in meas_planes.items(): - if plane != Plane.XY or node not in non_outputs: - return False - - odd_flow = {node: find_odd_neighbor(graph, corrections) for node, corrections in flow.items()} - - try: - _, _ = get_layers_from_flow(flow, odd_flow, iset, oset) - except ValueError: - return False - # check if v ~ f(v) for each node - edges = set(graph.edges) - for node, corrections in flow.items(): - if len(corrections) > 1: - return False - correction = next(iter(corrections)) - if (node, correction) not in edges and (correction, node) not in edges: - return False - return valid_flow - - -def verify_gflow( - graph: nx.Graph, - iset: set[int], - oset: set[int], - gflow: dict[int, set], - meas_planes: dict[int, Plane], -) -> bool: - """Check whether the gflow is valid. - - Parameters - ---------- - graph: :class:`networkx.Graph` - Graph (incl. input and output) - iset: set - set of node labels for input - oset: set - set of node labels for output - gflow: dict[int, set] - gflow function. gflow[i] is the set of qubits to be corrected for the measurement of qubit i. - .. seealso:: :func:`find_gflow` - meas_planes: dict[int, str] - measurement planes for each qubits. meas_planes[i] is the measurement plane for qubit i. - - Returns - ------- - valid_gflow: bool - True if the gflow is valid. False otherwise. - """ - check_meas_planes(meas_planes) - valid_gflow = True - non_outputs = set(graph.nodes) - oset - odd_flow = {} - for non_output in non_outputs: - if non_output not in gflow: - gflow[non_output] = set() - odd_flow[non_output] = set() - else: - odd_flow[non_output] = find_odd_neighbor(graph, gflow[non_output]) - - try: - _, _ = get_layers_from_flow(gflow, odd_flow, iset, oset) - except ValueError: - return False - - # check for each measurement plane - for node, plane in meas_planes.items(): - # index = node_order.index(node) - if plane == Plane.XY: - valid_gflow &= (node not in gflow[node]) and (node in odd_flow[node]) - elif plane == Plane.XZ: - valid_gflow &= (node in gflow[node]) and (node in odd_flow[node]) - elif plane == Plane.YZ: - valid_gflow &= (node in gflow[node]) and (node not in odd_flow[node]) - - return valid_gflow - - -def verify_pauliflow( - graph: nx.Graph, - iset: set[int], - oset: set[int], - pauliflow: dict[int, set[int]], - meas_planes: dict[int, Plane], - meas_angles: dict[int, float], -) -> bool: - """Check whether the Pauliflow is valid. - - Parameters - ---------- - graph: :class:`networkx.Graph` - Graph (incl. input and output) - iset: set - set of node labels for input - oset: set - set of node labels for output - pauliflow: dict[int, set] - Pauli flow function. pauliflow[i] is the set of qubits to be corrected for the measurement of qubit i. - meas_planes: dict[int, Plane] - measurement planes for each qubits. meas_planes[i] is the measurement plane for qubit i. - meas_angles: dict[int, float] - measurement angles for each qubits. meas_angles[i] is the measurement angle for qubit i. - - Returns - ------- - valid_pauliflow: bool - True if the Pauliflow is valid. False otherwise. - """ - check_meas_planes(meas_planes) - l_x, l_y, l_z = get_pauli_nodes(meas_planes, meas_angles) - - valid_pauliflow = True - non_outputs = set(graph.nodes) - oset - odd_flow = {} - for non_output in non_outputs: - if non_output not in pauliflow: - pauliflow[non_output] = set() - odd_flow[non_output] = set() - else: - odd_flow[non_output] = find_odd_neighbor(graph, pauliflow[non_output]) - - try: - layers, depth = get_layers_from_flow(pauliflow, odd_flow, iset, oset, (l_x, l_y, l_z)) - except ValueError: - return False - node_order = [] - for d in range(depth): - node_order.extend(list(layers[d])) - - for node, plane in meas_planes.items(): - if node in l_x: - valid_pauliflow &= node in odd_flow[node] - elif node in l_z: - valid_pauliflow &= node in pauliflow[node] - elif node in l_y: - valid_pauliflow &= node in pauliflow[node].symmetric_difference(odd_flow[node]) - elif plane == Plane.XY: - valid_pauliflow &= (node not in pauliflow[node]) and (node in odd_flow[node]) - elif plane == Plane.XZ: - valid_pauliflow &= (node in pauliflow[node]) and (node in odd_flow[node]) - elif plane == Plane.YZ: - valid_pauliflow &= (node in pauliflow[node]) and (node not in odd_flow[node]) - - return valid_pauliflow - - -def get_input_from_flow(flow: dict[int, set]) -> set: - """Get input nodes from flow. - - Parameters - ---------- - flow: dict[int, set] - flow function. flow[i] is the set of qubits to be corrected for the measurement of qubit i. - - Returns - ------- - inputs: set - set of input nodes - """ - non_output = set(flow.keys()) - for correction in flow.values(): - non_output -= correction - return non_output - - -def get_output_from_flow(flow: dict[int, set]) -> set: - """Get output nodes from flow. - - Parameters - ---------- - flow: dict[int, set] - flow function. flow[i] is the set of qubits to be corrected for the measurement of qubit i. - - Returns - ------- - outputs: set - set of output nodes - """ - non_outputs = set(flow.keys()) - non_inputs = set() - for correction in flow.values(): - non_inputs |= correction - return non_inputs - non_outputs - - -def get_pauli_nodes( - meas_planes: dict[int, Plane], meas_angles: Mapping[int, ExpressionOrFloat] -) -> tuple[set[int], set[int], set[int]]: - """Get sets of nodes measured in X, Y, Z basis. - - Parameters - ---------- - meas_planes: dict[int, Plane] - measurement planes for each node. - meas_angles: dict[int, float] - measurement angles for each node. - - Returns - ------- - l_x: set - set of nodes measured in X basis. - l_y: set - set of nodes measured in Y basis. - l_z: set - set of nodes measured in Z basis. - """ - check_meas_planes(meas_planes) - l_x, l_y, l_z = set(), set(), set() - for node, plane in meas_planes.items(): - pm = PauliMeasurement.try_from(plane, meas_angles[node]) - if pm is None: - continue - if pm.axis == Axis.X: - l_x |= {node} - elif pm.axis == Axis.Y: - l_y |= {node} - elif pm.axis == Axis.Z: - l_z |= {node} - else: - assert_never(pm.axis) - return l_x, l_y, l_z diff --git a/graphix/visualization.py b/graphix/visualization.py index ffcd295b6..81f61d193 100644 --- a/graphix/visualization.py +++ b/graphix/visualization.py @@ -3,14 +3,12 @@ from __future__ import annotations import math -from copy import deepcopy from typing import TYPE_CHECKING import networkx as nx import numpy as np from matplotlib import pyplot as plt -from graphix import gflow from graphix.flow.exceptions import FlowError from graphix.fundamentals import Plane from graphix.measurements import PauliMeasurement @@ -235,8 +233,9 @@ def visualize_from_pattern( print("The pattern is not consistent with flow or gflow structure.") po_layers = pattern.extract_partial_order_layers() unfolded_layers = {node: layer_idx for layer_idx, layer in enumerate(po_layers[::-1]) for node in layer} - xflow, zflow = gflow.get_corrections_from_pattern(pattern) - xzflow: dict[int, set[int]] = deepcopy(xflow) + xzc = pattern.extract_xzcorrections() + xflow, zflow = xzc.x_corrections, xzc.z_corrections + xzflow = dict(xflow) for key, value in zflow.items(): if key in xzflow: xzflow[key] |= value diff --git a/tests/test_gflow.py b/tests/test_gflow.py deleted file mode 100644 index 87fbae81a..000000000 --- a/tests/test_gflow.py +++ /dev/null @@ -1,596 +0,0 @@ -from __future__ import annotations - -import itertools -from typing import TYPE_CHECKING, NamedTuple - -import networkx as nx -import pytest -from numpy.random import PCG64, Generator - -from graphix import command -from graphix.fundamentals import Plane -from graphix.gflow import ( - find_flow, - find_gflow, - find_pauliflow, - get_corrections_from_pattern, - verify_flow, - verify_gflow, - verify_pauliflow, -) -from graphix.pattern import Pattern -from graphix.random_objects import rand_circuit - -if TYPE_CHECKING: - from collections.abc import Iterable, Iterator - -seed = 30 - - -class GraphForTest(NamedTuple): - graph: nx.Graph - inputs: set[int] - outputs: set[int] - meas_planes: dict[int, str] - meas_angles: dict[int, float] | None - label: str - flow_exist: bool - gflow_exist: bool - pauliflow_exist: bool | None - - -def _graph1() -> GraphForTest: - # no measurement - # 1 - # | - # 2 - nodes = [1, 2] - edges = [(1, 2)] - graph = nx.Graph() - graph.add_nodes_from(nodes) - graph.add_edges_from(edges) - inputs = {1, 2} - outputs = {1, 2} - meas_planes = {} - return GraphForTest( - graph, - inputs, - outputs, - meas_planes, - None, - "no measurement", - flow_exist=True, - gflow_exist=True, - pauliflow_exist=None, - ) - - -def _graph2() -> GraphForTest: - # line graph with flow and gflow - # 1 - 2 - 3 - 4 - 5 - nodes = [1, 2, 3, 4, 5] - edges = [(1, 2), (2, 3), (3, 4), (4, 5)] - graph = nx.Graph() - graph.add_nodes_from(nodes) - graph.add_edges_from(edges) - inputs = {1} - outputs = {5} - meas_planes = { - 1: Plane.XY, - 2: Plane.XY, - 3: Plane.XY, - 4: Plane.XY, - } - return GraphForTest( - graph, - inputs, - outputs, - meas_planes, - None, - "line graph with flow and gflow", - flow_exist=True, - gflow_exist=True, - pauliflow_exist=None, - ) - - -def _graph3() -> GraphForTest: - # graph with flow and gflow - # 1 - 3 - 5 - # | - # 2 - 4 - 6 - nodes = [1, 2, 3, 4, 5, 6] - edges = [(1, 3), (2, 4), (3, 4), (3, 5), (4, 6)] - graph = nx.Graph() - graph.add_nodes_from(nodes) - graph.add_edges_from(edges) - inputs = {1, 2} - outputs = {5, 6} - meas_planes = { - 1: Plane.XY, - 2: Plane.XY, - 3: Plane.XY, - 4: Plane.XY, - } - return GraphForTest( - graph, - inputs, - outputs, - meas_planes, - None, - "graph with flow and gflow", - flow_exist=True, - gflow_exist=True, - pauliflow_exist=None, - ) - - -def _graph4() -> GraphForTest: - # graph with gflow but flow - # ______ - # / | - # 1 - 4 | - # / | - # / | - # / | - # 2 - 5 | - # \ / | - # X / - # / \ / - # 3 - 6 - nodes = [1, 2, 3, 4, 5, 6] - edges = [(1, 4), (1, 6), (2, 4), (2, 5), (2, 6), (3, 5), (3, 6)] - inputs = {1, 2, 3} - outputs = {4, 5, 6} - graph = nx.Graph() - graph.add_nodes_from(nodes) - graph.add_edges_from(edges) - meas_planes = {1: Plane.XY, 2: Plane.XY, 3: Plane.XY} - return GraphForTest( - graph, - inputs, - outputs, - meas_planes, - None, - "graph with gflow but no flow", - flow_exist=False, - gflow_exist=True, - pauliflow_exist=None, - ) - - -def _graph5() -> GraphForTest: - # graph with extended gflow but flow - # 0 - 1 - # /| | - # 4 | | - # \| | - # 2 - 5 - 3 - nodes = [0, 1, 2, 3, 4, 5] - edges = [(0, 1), (0, 2), (0, 4), (1, 5), (2, 4), (2, 5), (3, 5)] - inputs = {0, 1} - outputs = {4, 5} - graph = nx.Graph() - graph.add_nodes_from(nodes) - graph.add_edges_from(edges) - meas_planes = { - 0: Plane.XY, - 1: Plane.XY, - 2: Plane.XZ, - 3: Plane.YZ, - } - return GraphForTest( - graph, - inputs, - outputs, - meas_planes, - None, - "graph with extended gflow but no flow", - flow_exist=False, - gflow_exist=True, - pauliflow_exist=None, - ) - - -def _graph6() -> GraphForTest: - # graph with no flow and no gflow - # 1 - 3 - # \ / - # X - # / \ - # 2 - 4 - nodes = [1, 2, 3, 4] - edges = [(1, 3), (1, 4), (2, 3), (2, 4)] - inputs = {1, 2} - outputs = {3, 4} - graph = nx.Graph() - graph.add_nodes_from(nodes) - graph.add_edges_from(edges) - meas_planes = {1: Plane.XY, 2: Plane.XY} - return GraphForTest( - graph, - inputs, - outputs, - meas_planes, - None, - "graph with no flow and no gflow", - flow_exist=False, - gflow_exist=False, - pauliflow_exist=None, - ) - - -def _graph7() -> GraphForTest: - # graph with no flow or gflow but pauliflow, No.1 - # 3 - # | - # 2 - # | - # 0 - 1 - 4 - nodes = [0, 1, 2, 3, 4] - edges = [(0, 1), (1, 2), (1, 4), (2, 3)] - inputs = {0} - outputs = {4} - graph = nx.Graph() - graph.add_nodes_from(nodes) - graph.add_edges_from(edges) - meas_planes = { - 0: Plane.XY, - 1: Plane.XY, - 2: Plane.XY, - 3: Plane.XY, - } - meas_angles = {0: 0.1, 1: 0, 2: 0.1, 3: 0} - return GraphForTest( - graph, - inputs, - outputs, - meas_planes, - meas_angles, - "graph with no flow and no gflow but pauliflow, No.1", - flow_exist=False, - gflow_exist=False, - pauliflow_exist=True, - ) - - -def _graph8() -> GraphForTest: - # graph with no flow or gflow but pauliflow, No.2 - # 1 2 3 - # | / | - # 0 - - - 4 - nodes = [0, 1, 2, 3, 4] - edges = [(0, 1), (0, 2), (0, 4), (3, 4)] - inputs = {0} - outputs = {4} - graph = nx.Graph() - graph.add_nodes_from(nodes) - graph.add_edges_from(edges) - meas_planes = { - 0: Plane.YZ, - 1: Plane.XZ, - 2: Plane.XY, - 3: Plane.YZ, - } - meas_angles = {0: 0, 1: 0, 2: 0.5, 3: 0.5} - return GraphForTest( - graph, - inputs, - outputs, - meas_planes, - meas_angles, - "graph with no flow and no gflow but pauliflow, No.2", - flow_exist=False, - gflow_exist=False, - pauliflow_exist=False, - ) - - -def _graph9() -> GraphForTest: - # graph with no flow or gflow but pauliflow, No.3 - # 0 - 1 -- 3 - # \| /| - # |\ / | - # | /\ | - # 2 -- 4 - nodes = [0, 1, 2, 3, 4] - edges = [(0, 1), (0, 4), (1, 2), (1, 3), (2, 3), (2, 4), (3, 4)] - inputs = {0} - outputs = {3, 4} - graph = nx.Graph() - graph.add_nodes_from(nodes) - graph.add_edges_from(edges) - meas_planes = {0: Plane.YZ, 1: Plane.XZ, 2: Plane.XY} - meas_angles = {0: 0, 1: 0.1, 2: 0.5} - return GraphForTest( - graph, - inputs, - outputs, - meas_planes, - meas_angles, - "graph with no flow and no gflow but pauliflow, No.3", - flow_exist=False, - gflow_exist=False, - pauliflow_exist=False, - ) - - -def generate_test_graphs() -> list[GraphForTest]: - return [ - _graph1(), - _graph2(), - _graph3(), - _graph4(), - _graph5(), - _graph6(), - _graph7(), - _graph8(), - _graph9(), - ] - - -FlowTestCaseType = dict[str, dict[str, tuple[bool, dict[int, set[int]]]]] -FlowTestDataType = tuple[GraphForTest, tuple[bool, dict[int, set[int]]]] - -FLOW_TEST_CASES: FlowTestCaseType = { - "no measurement": { - "empty flow": (True, {}), - "measure output": (False, {1: {2}}), - }, - "line graph with flow and gflow": { - "correct flow": (True, {1: {2}, 2: {3}, 3: {4}, 4: {5}}), - "acausal flow": (False, {1: {3}, 3: {2, 4}, 2: {1}, 4: {5}}), - "gflow": (False, {1: {2, 5}, 2: {3, 5}, 3: {4, 5}, 4: {5}}), - }, - "graph with flow and gflow": { - "correct flow": (True, {1: {3}, 2: {4}, 3: {5}, 4: {6}}), - "acausal flow": (False, {1: {4}, 2: {3}, 3: {4}, 4: {1}}), - "gflow": (False, {1: {3, 5}, 2: {4, 5}, 3: {5, 6}, 4: {6}}), - }, -} - - -GFLOW_TEST_CASES: FlowTestCaseType = { - "no measurement": { - "empty flow": (True, {}), - "measure output": (False, {1: {2}}), - }, - "line graph with flow and gflow": { - "correct flow": (True, {1: {2}, 2: {3}, 3: {4}, 4: {5}}), - "acausal flow": (False, {1: {3}, 3: {2, 4}, 2: {1}, 4: {5}}), - "gflow": (True, {1: {2, 5}, 2: {3, 5}, 3: {4, 5}, 4: {5}}), - }, - "graph with flow and gflow": { - "correct flow": (True, {1: {3}, 2: {4}, 3: {5}, 4: {6}}), - "acausal flow": (False, {1: {4}, 2: {3}, 3: {4}, 4: {1}}), - "gflow": (True, {1: {3, 5}, 2: {4, 5}, 3: {5, 6}, 4: {6}}), - }, - "graph with extended gflow but no flow": { - "correct gflow": ( - True, - {0: {1, 2, 3, 4}, 1: {2, 3, 4, 5}, 2: {2, 4}, 3: {3}}, - ), - "correct gflow 2": (True, {0: {1, 2, 4}, 1: {3, 5}, 2: {2, 4}, 3: {3}}), - "incorrect gflow": ( - False, - {0: {1, 2, 3, 4}, 1: {2, 3, 4, 5}, 2: {2, 4}, 3: {3, 4}}, - ), - "incorrect gflow 2": ( - False, - {0: {1, 3, 4}, 1: {2, 3, 4, 5}, 2: {2, 4}, 3: {3}}, - ), - }, -} - -PAULIFLOW_TEST_CASES: FlowTestCaseType = { - "graph with no flow and no gflow but pauliflow, No.1": { - "correct pauliflow": (True, {0: {1}, 1: {4}, 2: {3}, 3: {2, 4}}), - "correct pauliflow 2": (True, {0: {1, 3}, 1: {3, 4}, 2: {3}, 3: {2, 3, 4}}), - "incorrect pauliflow": (False, {0: {1}, 1: {2}, 2: {3}, 3: {4}}), - "incorrect pauliflow 2": (False, {0: {1, 3}, 1: {3, 4}, 2: {3, 4}, 3: {2, 3, 4}}), - }, - "graph with no flow and no gflow but pauliflow, No.2": { - "correct pauliflow": (True, {0: {0, 1}, 1: {1}, 2: {2}, 3: {4}}), - "correct pauliflow 2": (True, {0: {0, 1, 2}, 1: {1}, 2: {2}, 3: {1, 2, 4}}), - "incorrect pauliflow": (False, {0: {1}, 1: {1, 2}, 2: {2, 3}, 3: {4}}), - "incorrect pauliflow 2": (False, {0: {0}, 1: {1}, 2: {3}, 3: {3}}), - }, - "graph with no flow and no gflow but pauliflow, No.3": { - "correct pauliflow": (True, {0: {0, 3, 4}, 1: {1, 2}, 2: {4}}), - "correct pauliflow 2": (True, {0: {0, 2, 4}, 1: {1, 3}, 2: {2, 3, 4}}), - "incorrect pauliflow": (False, {0: {0, 3, 4}, 1: {1}, 2: {3, 4}}), - "incorrect pauliflow 2": (False, {0: {0, 3}, 1: {1, 2, 3}, 2: {2, 3, 4}}), - }, -} - - -def iterate_compatible( - graphs: Iterable[GraphForTest], - cases: FlowTestCaseType, -) -> Iterator[FlowTestDataType]: - for g in graphs: - for k, v in cases.items(): - if g.label != k: - continue - for vv in v.values(): - yield (g, vv) - - -class RandomMeasGraph(NamedTuple): - graph: nx.Graph - vin: set[int] - vout: set[int] - meas_planes: dict[int, str] - meas_angles: dict[int, float] - - -def get_rand_graph(rng: Generator, n_nodes: int, edge_prob: float = 0.3) -> RandomMeasGraph: - graph = nx.Graph() - nodes = range(n_nodes) - graph.add_nodes_from(nodes) - edge_candidates = set(itertools.product(nodes, nodes)) - {(i, i) for i in nodes} - for edge in edge_candidates: - if rng.uniform() < edge_prob: - graph.add_edge(*edge) - - input_nodes_number = rng.integers(1, len(nodes) - 1) - vin = set(rng.choice(nodes, input_nodes_number, replace=False)) - output_nodes_number = rng.integers(1, len(nodes) - input_nodes_number) - vout = set(rng.choice(list(set(nodes) - vin), output_nodes_number, replace=False)) - - meas_planes = {} - meas_plane_candidates = [Plane.XY, Plane.XZ, Plane.YZ] - meas_angles = {} - meas_angle_candidates = [0, 0.25, 0.5, 0.75] - - for node in set(graph.nodes()) - vout: - meas_planes[node] = rng.choice(meas_plane_candidates) - meas_angles[node] = rng.choice(meas_angle_candidates) - - return RandomMeasGraph(graph, vin, vout, meas_planes, meas_angles) - - -class TestGflow: - @pytest.mark.parametrize("test_graph", generate_test_graphs()) - def test_flow(self, test_graph: GraphForTest) -> None: - f, _ = find_flow( - test_graph.graph, - test_graph.inputs, - test_graph.outputs, - test_graph.meas_planes, - ) - assert test_graph.flow_exist == (f is not None) - - @pytest.mark.parametrize("test_graph", generate_test_graphs()) - def test_gflow(self, test_graph: GraphForTest) -> None: - g, _ = find_gflow( - test_graph.graph, - test_graph.inputs, - test_graph.outputs, - test_graph.meas_planes, - ) - assert test_graph.gflow_exist == (g is not None) - - @pytest.mark.parametrize("data", iterate_compatible(generate_test_graphs(), FLOW_TEST_CASES)) - def test_verify_flow(self, data: FlowTestDataType) -> None: - test_graph, test_case = data - expected, flow = test_case - valid = verify_flow( - test_graph.graph, - test_graph.inputs, - test_graph.outputs, - flow, - test_graph.meas_planes, - ) - assert expected == valid - - @pytest.mark.parametrize("data", iterate_compatible(generate_test_graphs(), GFLOW_TEST_CASES)) - def test_verify_gflow(self, data: FlowTestDataType) -> None: - test_graph, test_case = data - expected, gflow = test_case - - valid = verify_gflow( - test_graph.graph, - test_graph.inputs, - test_graph.outputs, - gflow, - test_graph.meas_planes, - ) - assert expected == valid - - @pytest.mark.parametrize("data", iterate_compatible(generate_test_graphs(), PAULIFLOW_TEST_CASES)) - def test_verify_pauliflow(self, data: FlowTestDataType) -> None: - test_graph, test_case = data - expected, pauliflow = test_case - angles = test_graph.meas_angles - assert angles is not None - - valid = verify_pauliflow( - test_graph.graph, - test_graph.inputs, - test_graph.outputs, - pauliflow, - test_graph.meas_planes, - angles, - ) - assert expected == valid - - def test_with_rand_circ(self, fx_rng: Generator) -> None: - # test for large graph - # graph transpiled from circuit always has a flow - circ = rand_circuit(10, 10, fx_rng) - pattern = circ.transpile().pattern - graph = pattern.extract_graph() - input_ = set(pattern.input_nodes) - output = set(pattern.output_nodes) - meas_planes = pattern.get_meas_plane() - f, _ = find_flow(graph, input_, output, meas_planes) - valid = verify_flow(graph, input_, output, f, meas_planes) - - assert valid - - # TODO: Remove after fixed - @pytest.mark.skip - def test_rand_circ_gflow(self, fx_rng: Generator) -> None: - # test for large graph - # pauli-node measured graph always has gflow - circ = rand_circuit(5, 5, fx_rng) - pattern = circ.transpile().pattern - pattern.standardize() - pattern.shift_signals() - pattern.perform_pauli_measurements() - graph = pattern.extract_graph() - input_ = set() - output = set(pattern.output_nodes) - meas_planes = pattern.get_meas_plane() - g, _ = find_gflow(graph, input_, output, meas_planes) - - valid = verify_gflow(graph, input_, output, g, meas_planes) - - assert valid - - @pytest.mark.parametrize("jumps", range(1, 51)) - def test_rand_graph_flow(self, fx_bg: PCG64, jumps: int) -> None: - # test finding algorithm and verification for random graphs - rng = Generator(fx_bg.jumped(jumps)) - n_nodes = 5 - graph, vin, vout, meas_planes, _ = get_rand_graph(rng, n_nodes) - f, _ = find_flow(graph, vin, vout, meas_planes) - if f: - valid = verify_flow(graph, vin, vout, f, meas_planes) - assert valid - - @pytest.mark.parametrize("jumps", range(1, 51)) - def test_rand_graph_gflow(self, fx_bg: PCG64, jumps: int) -> None: - # test finding algorithm and verification for random graphs - rng = Generator(fx_bg.jumped(jumps)) - n_nodes = 5 - graph, vin, vout, meas_planes, _ = get_rand_graph(rng, n_nodes) - - g, _ = find_gflow(graph, vin, vout, meas_planes) - if g: - valid = verify_gflow(graph, vin, vout, g, meas_planes) - assert valid - - @pytest.mark.parametrize("jumps", range(1, 51)) - def test_rand_graph_pauliflow(self, fx_bg: PCG64, jumps: int) -> None: - # test finding algorithm and verification for random graphs - rng = Generator(fx_bg.jumped(jumps)) - n_nodes = 5 - graph, vin, vout, meas_planes, meas_angles = get_rand_graph(rng, n_nodes) - - p, _ = find_pauliflow(graph, vin, vout, meas_planes, meas_angles) - if p: - valid = verify_pauliflow(graph, vin, vout, p, meas_planes, meas_angles) - assert valid - - def test_corrections_from_pattern(self) -> None: - pattern = Pattern(input_nodes=list(range(5))) - pattern.add(command.M(node=0)) - pattern.add(command.M(node=1)) - pattern.add(command.M(node=2, s_domain={0}, t_domain={1})) - pattern.add(command.X(node=3, domain={2})) - pattern.add(command.Z(node=4, domain={3})) - xflow, zflow = get_corrections_from_pattern(pattern) - assert xflow == {0: {2}, 2: {3}} - assert zflow == {1: {2}, 3: {4}} From bef15076991601dbbe4fecfdd329dfd7577d3722 Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 11 Dec 2025 16:39:53 +0100 Subject: [PATCH 43/58] Remove get_layers from pattern.py --- graphix/pattern.py | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/graphix/pattern.py b/graphix/pattern.py index 1701b5e4b..a35e36f4c 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -1028,41 +1028,6 @@ def extract_xzcorrections(self) -> XZCorrections[Measurement]: """ return optimization.StandardizedPattern.from_pattern(self).extract_xzcorrections() - def get_layers(self) -> tuple[int, dict[int, set[int]]]: - """Construct layers(l_k) from dependency information. - - kth layer must be measured before measuring k+1th layer - and nodes in the same layer can be measured simultaneously. - - Returns - ------- - depth : int - depth of graph - layers : dict of set - nodes grouped by layer index(k) - """ - self.check_runnability() # prevent infinite loop: e.g., [N(0), M(0, s_domain={0})] - dependency = self._get_dependency() - measured = self.results.keys() - self.update_dependency(measured, dependency) - not_measured = set(self.__input_nodes) - for cmd in self.__seq: - if cmd.kind == CommandKind.N and cmd.node not in self.output_nodes: - not_measured |= {cmd.node} - depth = 0 - l_k: dict[int, set[int]] = {} - k = 0 - while not_measured: - l_k[k] = set() - for i in not_measured: - if not dependency[i]: - l_k[k] |= {i} - self.update_dependency(l_k[k], dependency) - not_measured -= l_k[k] - k += 1 - depth = k - return depth, l_k - def _measurement_order_depth(self) -> list[int]: """Obtain a measurement order which reduces the depth of a pattern. From 5ccff13fd95b7c17f8b30c51f18eb82c029d336a Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 11 Dec 2025 16:59:04 +0100 Subject: [PATCH 44/58] Add test and fix bug in OpenGraph.isclose --- graphix/opengraph.py | 4 ++-- tests/test_pattern.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/graphix/opengraph.py b/graphix/opengraph.py index 93d235ecb..379ac807a 100644 --- a/graphix/opengraph.py +++ b/graphix/opengraph.py @@ -167,8 +167,8 @@ def is_equal_structurally(self, other: OpenGraph[AbstractMeasurement]) -> bool: """ return ( nx.utils.graphs_equal(self.graph, other.graph) - and self.input_nodes == other.input_nodes - and other.output_nodes == other.output_nodes + and tuple(self.input_nodes) == tuple(other.input_nodes) + and tuple(self.output_nodes) == tuple(other.output_nodes) ) def neighbors(self, nodes: Collection[int]) -> set[int]: diff --git a/tests/test_pattern.py b/tests/test_pattern.py index c9101a8ea..e962ace11 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -13,6 +13,7 @@ from graphix.branch_selector import ConstBranchSelector, FixedBranchSelector from graphix.clifford import Clifford from graphix.command import C, Command, CommandKind, E, M, N, X, Z +from graphix.flow.core import XZCorrections from graphix.flow.exceptions import ( FlowError, ) @@ -1054,6 +1055,21 @@ def test_extract_xzc_empty_domains(self) -> None: assert dict(xzc.z_corrections) == {} assert xzc.partial_order_layers == (frozenset({0, 1}),) + def test_extract_xzc_easy_example(self) -> None: + pattern = Pattern( + input_nodes=list(range(5)), + cmds=[M(0), M(1), M(2, s_domain={0}, t_domain={1}), X(3, domain={2}), M(3), Z(4, domain={3})], + ) + + xzc = pattern.extract_xzcorrections() + xzc_ref = XZCorrections.from_measured_nodes_mapping( + pattern.extract_opengraph(), x_corrections={0: {2}, 2: {3}}, z_corrections={1: {2}, 3: {4}} + ) + assert xzc.og.isclose(xzc_ref.og) + assert xzc.x_corrections == xzc_ref.x_corrections + assert xzc.z_corrections == xzc_ref.z_corrections + assert xzc.partial_order_layers == xzc_ref.partial_order_layers + def cp(circuit: Circuit, theta: float, control: int, target: int) -> None: """Controlled rotation gate, decomposed.""" # noqa: D401 From 5a2048d2c1ecb2dba3299b00d655fdfd651622b3 Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 11 Dec 2025 17:09:02 +0100 Subject: [PATCH 45/58] Up docs and examples --- docs/source/flow.rst | 30 ------------------------------ examples/rotation.py | 2 +- pyproject.toml | 4 ---- 3 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 docs/source/flow.rst diff --git a/docs/source/flow.rst b/docs/source/flow.rst deleted file mode 100644 index c556400ff..000000000 --- a/docs/source/flow.rst +++ /dev/null @@ -1,30 +0,0 @@ -flow and gflow -============== - -:mod:`graphix.gflow` module -+++++++++++++++++++++++++++++++++++ - -This provides functions to find flow structures (causal flow, gflow, Pauli flow) in a graph, -to verify if a given flow structure is valid, and to extract flow structures from a given pattern. - -.. automodule:: graphix.gflow - -.. currentmodule:: graphix.gflow - -.. autofunction:: find_flow - -.. autofunction:: find_gflow - -.. autofunction:: find_pauliflow - -.. autofunction:: verify_flow - -.. autofunction:: verify_gflow - -.. autofunction:: verify_pauliflow - -.. autofunction:: flow_from_pattern - -.. autofunction:: gflow_from_pattern - -.. autofunction:: pauliflow_from_pattern diff --git a/examples/rotation.py b/examples/rotation.py index 07b127f1f..e72ebc12f 100644 --- a/examples/rotation.py +++ b/examples/rotation.py @@ -53,7 +53,7 @@ # Since there's no two-qubit gates applied to the two qubits in the original gate sequence, # we see decoupled 1D graphs representing the evolution of single qubits. # The arrows are the ``information flow `` -# of the MBQC pattern, obtained using the flow-finding algorithm implemented in :class:`graphix.gflow.flow`. +# of the MBQC pattern, obtained using the flow-finding algorithm implemented in :class:`graphix.flow`. # Below we list the meaning of the node boundary and face colors. # # - Nodes with red boundaries are the *input nodes* where the computation starts. diff --git a/pyproject.toml b/pyproject.toml index c3063748e..5c82103e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -142,10 +142,8 @@ exclude = [ '^examples/qnn\.py$', '^examples/rotation\.py$', '^examples/tn_simulation\.py$', - '^graphix/gflow\.py$', '^graphix/linalg\.py$', '^tests/test_density_matrix\.py$', - '^tests/test_gflow\.py$', '^tests/test_linalg\.py$', '^tests/test_noisy_density_matrix\.py$', '^tests/test_statevec\.py$', @@ -169,10 +167,8 @@ exclude = [ "examples/qnn.py", "examples/rotation.py", "examples/tn_simulation.py", - "graphix/gflow.py", "graphix/linalg.py", "tests/test_density_matrix.py", - "tests/test_gflow.py", "tests/test_linalg.py", "tests/test_noisy_density_matrix.py", "tests/test_statevec.py", From ff8e6f0d816b2f101c1473f31a0293525bec0856 Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 15 Dec 2025 11:35:30 +0100 Subject: [PATCH 46/58] Standardize pattern before extracting partial order layers --- graphix/pattern.py | 46 +++------------------------------------------- 1 file changed, 3 insertions(+), 43 deletions(-) diff --git a/graphix/pattern.py b/graphix/pattern.py index c8a934118..add35da58 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -22,7 +22,6 @@ from graphix import command, optimization, parameter from graphix.clifford import Clifford from graphix.command import Command, CommandKind -from graphix.flow._partial_order import compute_topological_generations from graphix.fundamentals import Axis, Plane, Sign from graphix.graphsim import GraphState from graphix.measurements import Measurement, Outcome, PauliMeasurement, toggle_outcome @@ -890,7 +889,7 @@ def update_dependency(measured: AbstractSet[int], dependency: dict[int, set[int] def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: """Extract the measurement order of the pattern in the form of layers. - This method builds a directed acyclical graph (DAG) from the pattern and then performs a topological sort. + This method standardizes the pattern, builds a directed acyclical graph (DAG) from measurement and correction domains, and then performs a topological sort. Returns ------- @@ -910,48 +909,9 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: - All output nodes (if any) are in the first layer. - There cannot be any empty layers. - The partial order layerings obtained with this method before and after standardizing a pattern may differ, but they will always be compatible. This occurs because the commutation of entanglement commands with X and Z corrections in the standardization procedure may generate new corrections which add additional constrains to pattern's induced DAG. + - See :func:`optimization.StandardizedPattern.extract_causal_flow` for additional information on why it is required to standardized the pattern to extract the partial order layering. """ - self.check_runnability() - - oset = frozenset(self.output_nodes) # First layer by convention - pre_measured_nodes = set(self.results.keys()) # Not included in the partial order layers - excluded_nodes = oset | pre_measured_nodes - - zero_indegree = set(self.input_nodes) - excluded_nodes - dag: dict[int, set[int]] = { - node: set() for node in zero_indegree - } # `i: {j}` represents `i -> j` which means that node `i` must be measured before node `j`. - indegree_map: dict[int, int] = {} - node: int | None = None # To avoid Pyright's "PossiblyUnboundVariable" error - - for cmd in self: - domain = set() - if cmd.kind == CommandKind.N: - if cmd.node not in oset: # pre-measured nodes only appear in domains. - dag[cmd.node] = set() - zero_indegree.add(cmd.node) - elif cmd.kind == CommandKind.M: - node, domain = cmd.node, cmd.s_domain | cmd.t_domain - elif cmd.kind == CommandKind.X or cmd.kind == CommandKind.Z: # noqa: PLR1714 - node, domain = cmd.node, cmd.domain - - for dep_node in domain: - assert node is not None - if ( - not {node, dep_node} & excluded_nodes and node not in dag[dep_node] - ): # Don't include multiple edges in the dag. - dag[dep_node].add(node) - indegree_map[node] = indegree_map.get(node, 0) + 1 - - zero_indegree -= indegree_map.keys() - - generations = compute_topological_generations(dag, indegree_map, zero_indegree) - assert generations is not None # DAG can't contain loops because pattern is runnable. - - if oset: - return oset, *generations[::-1] - return generations[::-1] + return optimization.StandardizedPattern.from_pattern(self).extract_partial_order_layers() def extract_causal_flow(self) -> CausalFlow[Measurement]: """Extract the causal flow structure from the current measurement pattern. From 596ec4b3454de4dfadb69f59e97af5a13cd04ba8 Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 5 Jan 2026 10:02:23 +0100 Subject: [PATCH 47/58] Apply suggestions from Thierry's code review Co-authored-by: thierry-martinez --- graphix/optimization.py | 24 +++++++++--------------- graphix/pattern.py | 6 +----- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index 55e5652de..28a899bb7 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -419,20 +419,15 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: - There cannot be any empty layers. """ oset = frozenset(self.output_nodes) # First layer by convention. - pre_measured_nodes = set(self.results.keys()) # Not included in the partial order layers. + pre_measured_nodes = self.results.keys() # Not included in the partial order layers. excluded_nodes = oset | pre_measured_nodes - zero_indegree = set(self.input_nodes) - excluded_nodes + zero_indegree = set(self.input_nodes).union(n.node for n in self.n_list) - excluded_nodes dag: dict[int, set[int]] = { node: set() for node in zero_indegree } # `i: {j}` represents `i -> j` which means that node `i` must be measured before node `j`. indegree_map: dict[int, int] = {} - for n in self.n_list: - if n.node not in oset: # Pre-measured nodes only appear in domains. - dag[n.node] = set() - zero_indegree.add(n.node) - def process_domain(node: Node, domain: AbstractSet[Node]) -> None: for dep_node in domain: if ( @@ -487,7 +482,7 @@ def extract_causal_flow(self) -> CausalFlow[Measurement]: In general, there may exist various layerings which represent the corrections of the pattern. To ensure that a given layering is compatible with the pattern's induced correction function, the partial order must be extracted from a standardized pattern. Commutation of entanglement commands with X and Z corrections in the standardization procedure may generate new corrections, which guarantees that all the topological information of the underlying graph is encoded in the extracted partial order. """ correction_function: dict[int, set[int]] = defaultdict(set) - pre_measured_nodes = set(self.results.keys()) # Not included in the flow. + pre_measured_nodes = self.results.keys() # Not included in the flow. for m in self.m_list: if m.plane in {Plane.XZ, Plane.YZ}: @@ -527,15 +522,14 @@ def extract_gflow(self) -> GFlow[Measurement]: Notes ----- - This method makes use of :func:`StandardizedPattern.extract_partial_order_layers` which computes the pattern's direct acyclical graph (DAG) induced by the corrections and returns a particular layer stratification (obtained by doing a topological sort on the DAG). Further, it constructs the pattern's induced correction function from :math:`M` and :math:`X` commands. - In general, there may exist various layerings which represent the corrections of the pattern. To ensure that a given layering is compatible with the pattern's induced correction function, the partial order must be extracted from a standardized pattern. Commutation of entanglement commands with X and Z corrections in the standardization procedure may generate new corrections, which guarantees that all the topological information of the underlying graph is encoded in the extracted partial order. + The notes provided in :meth:`extract_causal_flow` apply here as well. """ - correction_function: dict[int, set[int]] = defaultdict(set) - pre_measured_nodes = set(self.results.keys()) # Not included in the flow. + correction_function: dict[int, set[int]] = {} + pre_measured_nodes = self.results.keys() # Not included in the flow. for m in self.m_list: if m.plane in {Plane.XZ, Plane.YZ}: - correction_function[m.node].add(m.node) + correction_function.setdefault(m.node, set()).add(m.node) correction_function = _update_corrections(m.node, m.s_domain - pre_measured_nodes, correction_function) for node, domain in self.x_dict.items(): @@ -547,7 +541,7 @@ def extract_gflow(self) -> GFlow[Measurement]: partial_order_layers = ( self.extract_partial_order_layers() ) # Raises a `ValueError` if the pattern corrections form closed loops. - gf = GFlow(og, dict(correction_function), partial_order_layers) + gf = GFlow(og, correction_function, partial_order_layers) gf.check_well_formed() return gf @@ -633,7 +627,7 @@ def _update_corrections( This function is used to extract the correction function from :math:`X`, :math:`Z` and :math:`M` commands when constructing a flow. """ for measured_node in domain: - correction[measured_node].add(node) + correction.setdefault(measured_node, set()).add(node) return correction diff --git a/graphix/pattern.py b/graphix/pattern.py index add35da58..936842bac 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -903,11 +903,7 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]: Notes ----- - The returned object follows the same conventions as the ``partial_order_layers`` attribute of :class:`PauliFlow` and :class:`XZCorrections` objects: - - Nodes in the same layer can be measured simultaneously. - - Nodes in layer ``i`` must be measured after nodes in layer ``i + 1``. - - All output nodes (if any) are in the first layer. - - There cannot be any empty layers. + - This function wraps :func:`optimization.StandardizedPattern.extract_partial_order_layers`, and the returned object is described in the notes of this method. - See :func:`optimization.StandardizedPattern.extract_causal_flow` for additional information on why it is required to standardized the pattern to extract the partial order layering. """ From 975f7aa3429340fa0ac28264aaa837de02867eea Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 5 Jan 2026 10:07:19 +0100 Subject: [PATCH 48/58] Make optimizations._update_corrections return None --- graphix/optimization.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index 28a899bb7..a299bb529 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -487,10 +487,10 @@ def extract_causal_flow(self) -> CausalFlow[Measurement]: for m in self.m_list: if m.plane in {Plane.XZ, Plane.YZ}: raise FlowGenericError(FlowGenericErrorReason.XYPlane) - correction_function = _update_corrections(m.node, m.s_domain - pre_measured_nodes, correction_function) + _update_corrections(m.node, m.s_domain - pre_measured_nodes, correction_function) for node, domain in self.x_dict.items(): - correction_function = _update_corrections(node, domain - pre_measured_nodes, correction_function) + _update_corrections(node, domain - pre_measured_nodes, correction_function) og = ( self.extract_opengraph() @@ -530,10 +530,10 @@ def extract_gflow(self) -> GFlow[Measurement]: for m in self.m_list: if m.plane in {Plane.XZ, Plane.YZ}: correction_function.setdefault(m.node, set()).add(m.node) - correction_function = _update_corrections(m.node, m.s_domain - pre_measured_nodes, correction_function) + _update_corrections(m.node, m.s_domain - pre_measured_nodes, correction_function) for node, domain in self.x_dict.items(): - correction_function = _update_corrections(node, domain - pre_measured_nodes, correction_function) + _update_corrections(node, domain - pre_measured_nodes, correction_function) og = ( self.extract_opengraph() @@ -601,9 +601,7 @@ def _incorporate_pauli_results_in_domain( return odd_outcome == 1, new_domain -def _update_corrections( - node: Node, domain: AbstractSet[Node], correction: dict[Node, set[Node]] -) -> dict[Node, set[Node]]: +def _update_corrections(node: Node, domain: AbstractSet[Node], correction: dict[Node, set[Node]]) -> None: """Update the correction mapping by adding a node to all entries in a domain. Parameters @@ -616,19 +614,12 @@ def _update_corrections( A mapping from measured nodes to sets of nodes on which corrections are applied. This dictionary is modified in place. - Returns - ------- - dict[Node, set[Node]] - The updated correction dictionary with `node` added to the correction - sets of all nodes in `domain`. - Notes ----- This function is used to extract the correction function from :math:`X`, :math:`Z` and :math:`M` commands when constructing a flow. """ for measured_node in domain: correction.setdefault(measured_node, set()).add(node) - return correction def incorporate_pauli_results(pattern: Pattern) -> Pattern: From f640645ac3dfadaec4b68d3a872bd8224481aeb2 Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 5 Jan 2026 10:21:31 +0100 Subject: [PATCH 49/58] Up CHANGELOG and fix ruff --- CHANGELOG.md | 6 ++++++ tests/test_transpiler.py | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2df08aadb..763cc30b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- #393 + - Introduced new method `graphix.optimization.StandardizedPattern.extract_partial_order_layer` which constructs a partial order layering from the dependency domains of M, X and Z commands. + - Introduced new methods `graphix.optimization.StandardizedPattern.extract_causal_flow`, `graphix.optimization.StandardizedPattern.extract_gflow` which respectively attempt to extract a causal flow and a gflow from a standardized pattern. + - Introduced new wrapper methods in the `Pattern` class: `graphix.pattern.Pattern.extract_partial_order_layers`, `graphix.pattern.Pattern.extract_causal_flow` and `graphix.pattern.Pattern.extract_gflow`. + - Introduced new module `graphix.flow._partial_order` with the function :func:`compute_topological_generations`. + - #385 - Introduced `graphix.flow.core.XZCorrections.check_well_formed` which verifies the correctness of an XZ-corrections instance and raises an exception if incorrect. - Added XZ-correction exceptions to module `graphix.flow.core.exceptions`. diff --git a/tests/test_transpiler.py b/tests/test_transpiler.py index 2a37bb6b0..447ed49e9 100644 --- a/tests/test_transpiler.py +++ b/tests/test_transpiler.py @@ -9,7 +9,6 @@ from graphix import instruction from graphix.branch_selector import ConstBranchSelector from graphix.fundamentals import Axis, Sign -from graphix.gflow import flow_from_pattern from graphix.instruction import InstructionKind from graphix.random_objects import rand_circuit, rand_gate, rand_state_vector from graphix.states import BasicStates From 136721ff1a2065f00a75fada90395bf43d17fcd0 Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 5 Jan 2026 13:06:57 +0100 Subject: [PATCH 50/58] Up docstrings --- graphix/flow/core.py | 12 ++++-------- graphix/optimization.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index d342e2e7c..74b866bca 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -906,13 +906,10 @@ def _corrections_to_dag( return nx.DiGraph(relations) -# TODO: UP docstring - - def _corrections_to_partial_order_layers( og: OpenGraph[_M_co], x_corrections: Mapping[int, AbstractSet[int]], z_corrections: Mapping[int, AbstractSet[int]] ) -> tuple[frozenset[int], ...]: - """Return the partial order encoded in a directed graph in a layer form if it exists. + """Return the partial order encoded in the correction mappings in a layer form if it exists. Parameters ---------- @@ -934,11 +931,11 @@ def _corrections_to_partial_order_layers( XZCorrectionsError If the input dictionaries are not well formed. In well-formed correction dictionaries: - Keys are a subset of the measured nodes. - - Values correspond to nodes of the open graph. - - Corrections do not form closed loops. + - Values correspond to nodes of the open graph. + - Corrections do not form closed loops. """ oset = frozenset(og.output_nodes) # First layer by convention if not empty - dag: dict[int, set[int]] = defaultdict( + dag: defaultdict[int, set[int]] = defaultdict( set ) # `i: {j}` represents `i -> j`, i.e., a correction applied to qubit `j`, conditioned on the measurement outcome of qubit `i`. indegree_map: dict[int, int] = {} @@ -956,7 +953,6 @@ def _corrections_to_partial_order_layers( if generations is None: raise XZCorrectionsGenericError(XZCorrectionsGenericErrorReason.ClosedLoop) - # If there're no corrections, the partial order has 2 layers only: outputs and measured nodes. if len(generations) == 0: if oset: return (oset,) diff --git a/graphix/optimization.py b/graphix/optimization.py index fde7b354e..b1600d8b9 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -560,27 +560,27 @@ def extract_xzcorrections(self) -> XZCorrections[Measurement]: ValueError If `N` commands in the pattern do not represent a |+⟩ state or if the pattern corrections form closed loops. """ - x_corr: dict[int, set[int]] = defaultdict(set) - z_corr: dict[int, set[int]] = defaultdict(set) + x_corr: dict[int, set[int]] = {} + z_corr: dict[int, set[int]] = {} - pre_measured_nodes = set(self.results.keys()) # Not included in the xz-corrections. + pre_measured_nodes = self.results.keys() # Not included in the xz-corrections. for m in self.m_list: - x_corr = _update_corrections(m.node, m.s_domain - pre_measured_nodes, x_corr) - z_corr = _update_corrections(m.node, m.t_domain - pre_measured_nodes, z_corr) + _update_corrections(m.node, m.s_domain - pre_measured_nodes, x_corr) + _update_corrections(m.node, m.t_domain - pre_measured_nodes, z_corr) for node, domain in self.x_dict.items(): - x_corr = _update_corrections(node, domain - pre_measured_nodes, x_corr) + _update_corrections(node, domain - pre_measured_nodes, x_corr) for node, domain in self.z_dict.items(): - z_corr = _update_corrections(node, domain - pre_measured_nodes, z_corr) + _update_corrections(node, domain - pre_measured_nodes, z_corr) og = ( self.extract_opengraph() ) # Raises a `ValueError` if `N` commands in the pattern do not represent a |+⟩ state. return XZCorrections.from_measured_nodes_mapping( - og, dict(x_corr), dict(z_corr) + og, x_corr, z_corr ) # Raises a `XZCorrectionsError` if the input dictionaries are not well formed. From a9a937764553e3a18315703be530557a6025611c Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Tue, 6 Jan 2026 08:10:26 +0100 Subject: [PATCH 51/58] Update test visualization baseline This commit updates the baseline while #392 is not merged. --- .../test_draw_graph_reference_True.png | Bin 9990 -> 14963 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/baseline/test_draw_graph_reference_True.png b/tests/baseline/test_draw_graph_reference_True.png index 27f65ee5a5a4e899cc40ca81152cc4fe8faacceb..2f79065a648bd48d4e3e922f3d8f9ced6380c2b9 100644 GIT binary patch literal 14963 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 IiW&y~U%v}h6951J literal 9990 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 Date: Tue, 6 Jan 2026 11:33:29 +0100 Subject: [PATCH 52/58] Undo tuple-casting in opengraph comparison --- graphix/opengraph.py | 6 +++--- graphix/pattern.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/graphix/opengraph.py b/graphix/opengraph.py index 379ac807a..51f890901 100644 --- a/graphix/opengraph.py +++ b/graphix/opengraph.py @@ -160,15 +160,15 @@ def is_equal_structurally(self, other: OpenGraph[AbstractMeasurement]) -> bool: ----- This method verifies the open graphs have: - Truly equal underlying graphs (not up to an isomorphism). - - Equal input and output nodes. + - Equal input and output nodes. This assumes equal types as well, i.e., if ``self.input_nodes`` is a ``list`` and ``other.input_nodes`` is a ``tuple``, this method will return ``False``. It assumes the open graphs are well formed. The static typer allows comparing the structure of two open graphs with different parametric type. """ return ( nx.utils.graphs_equal(self.graph, other.graph) - and tuple(self.input_nodes) == tuple(other.input_nodes) - and tuple(self.output_nodes) == tuple(other.output_nodes) + and self.input_nodes == other.input_nodes + and self.output_nodes == other.output_nodes ) def neighbors(self, nodes: Collection[int]) -> set[int]: diff --git a/graphix/pattern.py b/graphix/pattern.py index 5ac16f61b..43a0a31cb 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -1200,7 +1200,8 @@ def extract_opengraph(self) -> OpenGraph[Measurement]: graph = nx.Graph(edges) graph.add_nodes_from(nodes) - return OpenGraph(graph, self.input_nodes, self.output_nodes, measurements) + # Inputs and outputs are casted to `tuple` to replicate the behavior of `:func: graphix.opitmization.StandardizedPattern.extract_opengraph`. + return OpenGraph(graph, tuple(self.__input_nodes), tuple(self.__output_nodes), measurements) def get_vops(self, conj: bool = False, include_identity: bool = False) -> dict[int, Clifford]: """Get local-Clifford decorations from measurement or Clifford commands. From b177d347ba1b2a7608f4c30641995aaeed98e997 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 6 Jan 2026 11:36:30 +0100 Subject: [PATCH 53/58] Shorten docstring --- graphix/optimization.py | 2 +- graphix/pattern.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index a299bb529..893debe30 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -522,7 +522,7 @@ def extract_gflow(self) -> GFlow[Measurement]: Notes ----- - The notes provided in :meth:`extract_causal_flow` apply here as well. + The notes provided in :func:`self.extract_causal_flow` apply here as well. """ correction_function: dict[int, set[int]] = {} pre_measured_nodes = self.results.keys() # Not included in the flow. diff --git a/graphix/pattern.py b/graphix/pattern.py index 936842bac..4ff84c602 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -956,8 +956,7 @@ def extract_gflow(self) -> GFlow[Measurement]: Notes ----- - - See :func:`optimization.StandardizedPattern.extract_gflow` for additional information on why it is required to standardized the pattern to extract a gflow. - - Applying the chain ``Pattern.extract_gflow().to_corrections().to_pattern()`` to a strongly deterministic pattern returns a new pattern implementing the same unitary transformation. This equivalence holds as long as the original pattern contains no Clifford commands, since those are discarded during open-graph extraction. + The notes provided in :func:`self.extract_causal_flow` apply here as well. """ return optimization.StandardizedPattern.from_pattern(self).extract_gflow() From 85b9877e16790035c6d0164a8518a99cb0751af0 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 6 Jan 2026 16:18:09 +0100 Subject: [PATCH 54/58] Fix errors in docs --- docs/source/modifier.rst | 2 -- docs/source/references.rst | 1 - 2 files changed, 3 deletions(-) diff --git a/docs/source/modifier.rst b/docs/source/modifier.rst index ff82ba2d1..1c74ee3a4 100644 --- a/docs/source/modifier.rst +++ b/docs/source/modifier.rst @@ -58,8 +58,6 @@ Pattern Manipulation .. automethod:: max_space - .. automethod:: get_layers - .. automethod:: to_qasm3 diff --git a/docs/source/references.rst b/docs/source/references.rst index 8ebd74370..be757a2bb 100644 --- a/docs/source/references.rst +++ b/docs/source/references.rst @@ -10,7 +10,6 @@ Module reference simulator graphsim extraction - flow clifford visualization channels From 570aa7accebd0db0c9906166f2bd3c0d75f7dcdd Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 6 Jan 2026 16:35:57 +0100 Subject: [PATCH 55/58] Up baseline images --- .../test_draw_graph_reference_False.png | Bin 10964 -> 10955 bytes .../test_draw_graph_reference_True.png | Bin 14963 -> 15022 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/baseline/test_draw_graph_reference_False.png b/tests/baseline/test_draw_graph_reference_False.png index 22999333f6e5eb31d91183309bd867ba8b2b1088..1d0e72a16f192d2508096616f228d71a1a5ff231 100644 GIT binary patch delta 9212 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 N`I$PrOx_ate*m6XH*Np` delta 9214 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 IiW&y~U%v}h6951J From 86d0cf97565704c8cc8fbd684225b068eadd3fd8 Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 8 Jan 2026 11:27:23 +0100 Subject: [PATCH 56/58] Remove inputs before Pauli presim in tests --- .../test_draw_graph_reference_False.png | Bin 10955 -> 2897 bytes .../test_draw_graph_reference_True.png | Bin 15022 -> 17048 bytes tests/test_pattern.py | 5 +++-- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/baseline/test_draw_graph_reference_False.png b/tests/baseline/test_draw_graph_reference_False.png index 1d0e72a16f192d2508096616f228d71a1a5ff231..bb7b73fc414265b4dc8393cca2a4848f2abd0c84 100644 GIT binary patch literal 2897 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 T;OCyCab!AEV+$C;&@Jh|*Trx| literal 10955 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 TT*_NOor<8MpaH3pvxNOG0$V`Y diff --git a/tests/baseline/test_draw_graph_reference_True.png b/tests/baseline/test_draw_graph_reference_True.png index 5a003d91747380e21657d1c71511ebaba2b03c41..7252879c3aece0b7648799e769b2c75e85e97ec3 100644 GIT binary patch literal 17048 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 y;Osw*ki^@G*8($2eE# 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 None: xzc.check_well_formed() p_test = xzc.to_pattern() - p_ref.perform_pauli_measurements() - p_test.perform_pauli_measurements() + for p in [p_ref, p_test]: + p.remove_input_nodes() + p.perform_pauli_measurements() s_ref = p_ref.simulate_pattern(rng=rng) s_test = p_test.simulate_pattern(rng=rng) From 06dad956c5f24e8ac300533c12569b1b17f88451 Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 8 Jan 2026 13:19:06 +0100 Subject: [PATCH 57/58] Add thierry's suggestions --- graphix/flow/core.py | 2 +- tests/test_pattern.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/graphix/flow/core.py b/graphix/flow/core.py index 74b866bca..3079e392d 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -125,7 +125,7 @@ def from_measured_nodes_mapping( og, x_corrections, z_corrections ) # Raises an `XZCorrectionsError` if mappings are not well formed. - return XZCorrections(og, x_corrections, z_corrections, tuple(partial_order_layers)) + return XZCorrections(og, x_corrections, z_corrections, partial_order_layers) def to_pattern( self: XZCorrections[Measurement], diff --git a/tests/test_pattern.py b/tests/test_pattern.py index 671791388..163bfac3c 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -1065,8 +1065,8 @@ def test_extract_xzc_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None: def test_extract_xzc_empty_domains(self) -> None: p = Pattern(input_nodes=[0], cmds=[N(1), E((0, 1))]) xzc = p.extract_xzcorrections() - assert dict(xzc.x_corrections) == {} - assert dict(xzc.z_corrections) == {} + assert xzc.x_corrections == {} + assert xzc.z_corrections == {} assert xzc.partial_order_layers == (frozenset({0, 1}),) def test_extract_xzc_easy_example(self) -> None: From c21618265f4fe1fdba72eb7b5756b2592968fde4 Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 8 Jan 2026 13:25:18 +0100 Subject: [PATCH 58/58] Up CHANGELOG --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad4d1276e..d12871c60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #394: The method `Circuit.transpile_measurements_to_z_axis` returns an equivalent circuit where all measurements are on the Z axis. This can be used to prepare a circuit for export to OpenQASM with `circuit_to_qasm3`. +- #407: Introduced new method `graphix.optimization.StandardizedPattern.extract_xzcorrections` and its wrapper `graphix.pattern.Pattern.extract_xzcorrections` which extract an `XZCorrections` instance from a pattern. + + ### Fixed - #392: `Pattern.remove_input_nodes` is required before the `Pattern.perform_pauli_measurements` method to ensure input nodes are removed and fixed in the |+> state. @@ -51,6 +54,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #409: Axis labels are shown when visualizing a pattern. Legend is placed outside the plot so that the graph remains visible. +- #407: Fixed an unreported bug in `OpenGraph.is_equal_structurally` which failed to compare open graphs differing on the output nodes only. + ### Changed - #374: Adapted existing method `graphix.opengraph.OpenGraph.isclose` to the new API introduced in #358. @@ -64,6 +69,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 π. In particular, angles that appear in parameters of circuit instructions are now expressed in units of π. +- #407: + - Modified the constructor `XZCorrections.from_measured_nodes_mapping` so that it doesn't need to create an `nx.DiGraph` instance. This fixes an unreported bug in the method. + - Removed modules `graphix.gflow` and `graphix.find_pflow`. + ## [0.3.3] - 2025-10-23 ### Added