From ea51deda08d0b9996f2ac8e13b8e6e505b9c15b7 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 25 Nov 2025 11:46:22 +0100 Subject: [PATCH 01/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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 225e4176fa7643c36f946e43768e882da6fa8335 Mon Sep 17 00:00:00 2001 From: matulni Date: Wed, 10 Dec 2025 17:00:53 +0100 Subject: [PATCH 31/40] 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 26a4cf9b08368bc5846abf703f467b42f0a5f0bb Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 11 Dec 2025 11:31:24 +0100 Subject: [PATCH 32/40] 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 0ece2f2ad3f831ad3c43c79fea52e5a4a302643c Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 11 Dec 2025 11:52:09 +0100 Subject: [PATCH 33/40] 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 34/40] 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 ff8e6f0d816b2f101c1473f31a0293525bec0856 Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 15 Dec 2025 11:35:30 +0100 Subject: [PATCH 35/40] 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 36/40] 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 37/40] 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 38/40] 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 a9a937764553e3a18315703be530557a6025611c Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Tue, 6 Jan 2026 08:10:26 +0100 Subject: [PATCH 39/40] 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:36:30 +0100 Subject: [PATCH 40/40] 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()