diff --git a/CHANGELOG.md b/CHANGELOG.md index 06ed5e5a9..3a942ce86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added +- #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. + - Introduced new module `graphix.flow.exceptions` grouping flow exceptions. + - Introduced new methods `graphix.flow.core.PauliFlow.get_measurement_label` and `graphix.flow.core.GFlow.get_measurement_label` which return the measurement label of a given node following same criteria employed in the flow-finding algorithms. - #374: - Introduced new method `graphix.opengraph.OpenGraph.is_equal_structurally` which compares the underlying structure of two open graphs. diff --git a/graphix/flow/core.py b/graphix/flow/core.py index 94c88008c..4e698263a 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -9,12 +9,31 @@ 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.flow.exceptions import ( + FlowError, + FlowGenericError, + FlowGenericErrorReason, + FlowPropositionError, + FlowPropositionErrorReason, + FlowPropositionOrderError, + FlowPropositionOrderErrorReason, + PartialOrderError, + PartialOrderErrorReason, + PartialOrderLayerError, + PartialOrderLayerErrorReason, +) +from graphix.fundamentals import Axis, Plane if TYPE_CHECKING: from collections.abc import Mapping @@ -272,7 +291,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 +308,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 +367,161 @@ 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). + """ + _check_flow_general_properties(self) + + o_set = set(self.og.output_nodes) + 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 or layer & past_and_present_nodes: + 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 + ) + for node in layer: + correction_set = set(self.correction_function[node]) + + 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}: + 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 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 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 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 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 FlowPropositionError( + FlowPropositionErrorReason.P6, node=node, correction_set=correction_set + ) + elif meas == Axis.X: + if node not in odd_neighbors: + raise FlowPropositionError( + FlowPropositionErrorReason.P7, node=node, correction_set=correction_set + ) + elif meas == Axis.Z: + if node not in correction_set: + raise FlowPropositionError( + FlowPropositionErrorReason.P8, node=node, correction_set=correction_set + ) + elif meas == Axis.Y: + if node not in closed_odd_neighbors: + 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 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]): @@ -364,6 +538,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 +593,111 @@ 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 + """ + _check_flow_general_properties(self) + + o_set = set(self.og.output_nodes) + 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 or layer & past_and_present_nodes: + raise PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=layer_idx, layer=layer) + + 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 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 FlowPropositionOrderError( + FlowPropositionOrderErrorReason.G2, + node=node, + correction_set=correction_set, + past_and_present_nodes=past_and_present_nodes, + ) + + plane = self.get_measurement_label(node) + + if plane == Plane.XY: + if not (node not in correction_set and node in odd_neighbors): + 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 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 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 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]): @@ -442,6 +746,87 @@ 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 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. + + Specific properties of causal flows: + - 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), + 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 + ---------- + [1] Browne et al., 2007 New J. Phys. 9 250 (arXiv:quant-ph/0702212). + """ + _check_flow_general_properties(self) + + o_set = set(self.og.output_nodes) + 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 or layer & past_and_present_nodes: + raise PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, layer_index=layer_idx, layer=layer) + + past_and_present_nodes.update(layer) + + for node in layer: + correction_set = set(self.correction_function[node]) + + 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 FlowGenericError(FlowGenericErrorReason.XYPlane) + + neighbors = self.og.neighbors(correction_set) + + if node not in neighbors: + 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 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 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 PartialOrderError(PartialOrderErrorReason.IncorrectNodes) + def _corrections_to_dag( x_corrections: Mapping[int, AbstractSet[int]], z_corrections: Mapping[int, AbstractSet[int]] @@ -494,3 +879,53 @@ 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_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 FlowGenericError(FlowGenericErrorReason.IncorrectCorrectionFunctionDomain) + + if not _check_correction_function_image(flow.og, flow.correction_function): + raise FlowGenericError(FlowGenericErrorReason.IncorrectCorrectionFunctionImage) + + if len(flow.partial_order_layers) == 0: + 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 PartialOrderLayerError(PartialOrderLayerErrorReason.FirstLayer, layer_index=0, layer=first_layer) diff --git a/graphix/flow/exceptions.py b/graphix/flow/exceptions.py new file mode 100644 index 000000000..624964dda --- /dev/null +++ b/graphix/flow/exceptions.py @@ -0,0 +1,241 @@ +"""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 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`.""" + + 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." + + +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 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.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." + + 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 9b15a9a72..5fa370663 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 @@ -7,7 +8,25 @@ 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, + GFlow, + PauliFlow, + XZCorrections, +) +from graphix.flow.exceptions import ( + FlowError, + FlowGenericError, + FlowGenericErrorReason, + FlowPropositionError, + FlowPropositionErrorReason, + FlowPropositionOrderError, + FlowPropositionOrderErrorReason, + PartialOrderError, + PartialOrderErrorReason, + PartialOrderLayerError, + PartialOrderLayerErrorReason, +) from graphix.fundamentals import AbstractMeasurement, AbstractPlanarMeasurement, Axis, Plane from graphix.measurements import Measurement from graphix.opengraph import OpenGraph @@ -81,11 +100,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 +115,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 +171,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}], ) @@ -279,13 +298,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), @@ -369,6 +388,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 +398,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 +405,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: @@ -583,3 +599,324 @@ 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 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`.""" + + 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, 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", + [ + # 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}], + ), + FlowGenericError(FlowGenericErrorReason.XYPlane), + ), + # Incomplete correction function + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}}, + partial_order_layers=[{3}, {2}, {1}, {0}], + ), + FlowGenericError(FlowGenericErrorReason.IncorrectCorrectionFunctionDomain), + ), + # 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}], + ), + FlowGenericError(FlowGenericErrorReason.IncorrectCorrectionFunctionImage), + ), + # Empty partial order + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}, 2: {3}}, + partial_order_layers=[], + ), + PartialOrderError(PartialOrderErrorReason.Empty), + ), + # Incomplete partial order (first layer) + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}, 2: {3}}, + partial_order_layers=[{2}, {1}, {0}], + ), + PartialOrderLayerError(PartialOrderLayerErrorReason.FirstLayer, layer_index=0, 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}], + ), + 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( + og=og_c, + correction_function={0: {1}, 1: {2}, 2: {3}}, + partial_order_layers=[{3}, {2}, {3}, {1}, {0}], + ), + PartialOrderLayerError(PartialOrderLayerErrorReason.NthLayer, 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}], + ), + PartialOrderError(PartialOrderErrorReason.IncorrectNodes), + ), + # C0 + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2, 3}, 2: {3}}, + partial_order_layers=[{3}, {2}, {1}, {0}], + ), + FlowPropositionError(FlowPropositionErrorReason.C0, 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}], + ), + FlowPropositionError(FlowPropositionErrorReason.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}], + ), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.C2, node=0, correction_set={1}, past_and_present_nodes={0, 1} + ), + ), + # C3 + IncorrectFlowTestCase( + CausalFlow( + og=og_c, + correction_function={0: {1}, 1: {2}, 2: {3}}, + partial_order_layers=[{3}, {1}, {0, 2}], + ), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.C3, node=0, correction_set={1}, past_and_present_nodes={0, 2} + ), + ), + # 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}], + ), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.G1, node=1, correction_set={1, 2}, past_and_present_nodes={0, 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}], + ), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.G2, + node=2, + correction_set={2, 3, 4}, + past_and_present_nodes={0, 1, 2}, + ), + ), + # 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}], + ), + FlowPropositionError(FlowPropositionErrorReason.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}], + ), + FlowPropositionError(FlowPropositionErrorReason.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}], + ), + FlowPropositionError(FlowPropositionErrorReason.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}], + ), + FlowPropositionError(FlowPropositionErrorReason.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}], + ), + FlowPropositionError(FlowPropositionErrorReason.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}], + ), + FlowPropositionError(FlowPropositionErrorReason.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}], + ), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.P1, node=1, correction_set={2}, past_and_present_nodes={0, 1, 2} + ), + ), + # P2 + IncorrectFlowTestCase( + PauliFlow( + og=og_p, + correction_function={0: {1, 3}, 1: {3}, 2: {3}}, + partial_order_layers=[{3}, {2, 0, 1}], + ), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.P2, node=1, correction_set={3}, past_and_present_nodes={0, 1, 2} + ), + ), + # 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}], + ), + FlowPropositionOrderError( + FlowPropositionOrderErrorReason.P3, node=0, correction_set={1}, past_and_present_nodes={0, 1} + ), # Past and present nodes measured along Y. + ), + # P7 + IncorrectFlowTestCase( + PauliFlow( + og=og_p, + correction_function={0: {1, 3}, 1: {3}, 2: {3}}, + partial_order_layers=[{3}, {2}, {0, 1}], + ), + FlowPropositionError(FlowPropositionErrorReason.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}], + ), + FlowPropositionError(FlowPropositionErrorReason.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}], + ), + FlowPropositionError(FlowPropositionErrorReason.P9, node=1, correction_set={1, 2}), + ), + ], + ) + def test_check_flow_general_properties(self, test_case: IncorrectFlowTestCase) -> None: + with pytest.raises(FlowError) as exc_info: + test_case.flow.check_well_formed() + + for field in fields(exc_info.value): + attr = field.name + assert getattr(exc_info.value, attr) == getattr(test_case.exception, attr) diff --git a/tests/test_opengraph.py b/tests/test_opengraph.py index f86eced4f..8a91794b4 100644 --- a/tests/test_opengraph.py +++ b/tests/test_opengraph.py @@ -800,7 +800,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 @@ -831,7 +831,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."): @@ -842,7 +844,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."): @@ -853,7 +857,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."):