Skip to content

Conversation

@matulni
Copy link
Contributor

@matulni matulni commented Nov 3, 2025

Summary

This is the first of a series of PRs refactoring the flow tools in the library.

In the current implementation, the transformation "Open graph" $\rightarrow$ "Flow" $\rightarrow$ "XZ-corrections" $\rightarrow$ "Pattern" occurs under the hood in the :func: OpenGraph.to_pattern() method. This refactor aims at exposing the intermediate objects to offer users more control on pattern extraction and ease the analysis of flow objects.

Related issues: #120, #306, #196, #276, #181

File structure

  • graphix.opengraph.py introduces the new OpenGraph objects. The associated file tests.test_opengraph.py contains the bulk of the unit tests verifying the correctness of the flow-finding algorithms (these tests have been refactor from tests.test_gflow.py and tests.test_find_pflow).

  • graphix.flow.core.py contains all the flow-related functionalities, except the flow-finding algorithms which are placed in graphix.flow._find_cflow.py and graphix.flow._find_gpflow.py

  • The existing files graphix.gflow.py, graphix.find_pflow.py will be eliminated once the refactor is completed. Their modification consists of just minor changes to comply with the new interface of OpenGraph. The module graphix.pattern still depends on the old flow implementation.

Refactor overview

Public API

flowchart LR
n0(**OpenGraph**)
subgraph s1[" "]
	n10(**PauliFlow**)
	n11(**GFlow**)
	n12(**CausalFlow**)
end
n2(**XZCorrections**)
n3(**Pattern**)

n0 --> s1
s1 .-> n0
n2 -- "+ total order" --> n3
n3 --> n2
n2 .-> n0
n3 .-> n0
s1 --> n2

n10 --> n11
n11 --> n12


linkStyle 7 stroke: #276cf5ff
linkStyle 8 stroke: #276cf5ff
linkStyle 2 stroke: #F54927
linkStyle 3 stroke: #F54927
linkStyle 5 stroke: #F54927
Loading
  • Blue arrows indicate inheritance (from parent to child).
  • Orange arrows indicate that the conversion contains information on the measurement angles.
  • Dashed arrows indicate "containment", e.g. XZCorrections contains an OpenGraph.

Examples

Define an open graph without measurement angles and extract a gflow

import networkx as nx
from graphix.fundamentals import Plane
from graphix.opengraph_ import OpenGraph

og = OpenGraph(
        graph=nx.Graph([(1, 3), (2, 4), (3, 4), (3, 5), (4, 6)]),
        input_nodes=[1, 2],
        output_nodes=[5, 6],
        measurements=dict.fromkeys(range(1, 5), Plane.XY),
    )
gflow = og.find_gflow()
print(gflow.correction_function) # {1: {3, 6}, 2: {4, 5}, 3: {5}, 4: {6}}
print(gflow.partial_order_layers) # [{5, 6}, {3, 4}, {1, 2}]

Define an open graph with measurement angles and extract flow, corrections and pattern

import networkx as nx
from graphix.measurements import Measurement
from graphix.fundamentals import Plane
from graphix.opengraph_ import OpenGraph

og = OpenGraph(
        graph=nx.Graph([(0, 1), (1, 2), (2, 3)]),
        input_nodes=[0],
        output_nodes=[3],
        measurements={
            0: Measurement(0, Plane.XY), # `Plane.XY` if causal or gflow, `Axis.X` if Pauli flow
            1: Measurement(0, Plane.XY), # `Plane.XY` if causal or gflow, `Axis.X` if Pauli flow
            2: Measurement(0.2, Plane.XY),
        },
    )

# Causal flow
cflow = og.find_causal_flow()

print(cflow.correction_function) # {2: {3}, 1: {2}, 0: {1}}
print(cflow.partial_order_layers) # [{3}, {2}, {1}, {0}]

corr = cflow.to_corrections() # {domain: nodes}
print(corr.x_corrections) # {2: {3}, 1: {2}, 0: {1}}
print(corr.z_corrections) # {1: {3}, 0: {2}}
print(corr.to_pattern().to_unicode()) # X₃² M₂(π/5) X₂¹ Z₃¹ M₁ X₁⁰ Z₂⁰ M₀ E₂₋₃ E₁₋₂ E₀₋₁ N₃ N₂ N₁


# Pauli flow
pflow = og.find_pauli_flow()

print(pflow.correction_function) # {0: {1, 3}, 1: {2}, 2: {3}}
print(pflow.partial_order_layers) # [{3}, {2}, {0, 1}]

corr = pflow.to_corrections() # {domain: nodes}
print(corr.x_corrections) # {2: {3}, 0: {3}, 1: {2}}
print(corr.z_corrections) # {1: {3}}
print(corr.to_pattern().to_unicode()) # X₃² M₂(π/5) X₂¹ Z₃¹ M₁ X₃⁰ M₀ E₂₋₃ E₁₋₂ E₀₋₁ N₃ N₂ N₁

Attributes of the objects in the public API

  • OpenGraph (frozen dataclass)

    • graph: nx.Graph[int]
    • input_nodes: Sequence[int]
    • output_nodes: Sequence[int]
    • measurements: Mapping[int, _M_co]
  • PauliFlow (frozen dataclass)

    • og: OpenGraph[_M_co]
    • correction_function: Mapping[int, AbstractSet[int]]
    • partial_order_layers: Sequence[AbstractSet[int]]
  • GFlow, CausalFlow

    Same as PauliFlow but with og: OpenGraph[_PM_co]

  • XZCorrections (frozen dataclass)

    • og: OpenGraph[_M_co]
    • x_corrections: Mapping[int, AbstractSet[int]]
    • z_corrections: Mapping[int, AbstractSet[int]]
    • partial_order_layers: Sequence[AcstractSet[int]]

All the new classes inherit from Generic[_M_co]. We introduce the following parametric types:

  • _M_co = TypeVar("_M_co", bound=AbstractMeasurement, covariant=True)
  • _PM_co = TypeVar("_PM_co", bound=AbstractPlanarMeasurement, covariant=True)

Q&A attributes

Why do PauliFlow, its children and XZCorrections have a partial_order_layers attribute ?

In Ref. [1], it's noted that the correction function and the partial order of a flow are not independent, but rather, that the former determines the latter:

"In the problem of finding Pauli flow, instead of looking for correction function and partial order, it now suffices to look only for a (focused) extensive correction function. [...] Given a correction function $c$ we can define a minimal relation induced by the Pauli flow definition, and consider whether this relation extends to a partial order."

This observation suggests that flow objects should not have a "partial order" attribute, but instead, that it should be computed from the correction function when needed, e.g., when computing the $X,Z$-corrections for a Pauli flow. However, this operation is costly and, crucially, the partial order is a byproduct of the flow-finding algorithms. Therefore, it is reasonable to store it as an attribute at initialization time instead.

We store in the form of layers owing to the following reasons:

  • It is the most convenient form to compute the $X,Z$-corrections for a Pauli flow.
  • The recursive $O(N^2)$-algorithm (for causal flow) simultaneously computes the partial order in the form of layers and the correction function.
  • The algebraic $O(N^3)$-algorithm (for gflow and Pauli flow) computes the adjacency matrix of a directed graph. In order to verify that such directed graph is acyclical (and therefore, that it indeed encodes a partial order), we need to perform a topological sort, which amounts to computing the partial order layers.
  • The partial order of flows is often expressed in terms of an ordered sequence of sets in the literature.

Nevertheless, we endow the flow objects with class methods allowing to initialize them directly from a correction matrix or a correction function.

Similarly, the mappings XZCorrections.x_corrections and XZCorrections.z_corrections define a partial order which is necessary to extract a pattern (specifically, the input "total order" has to be compatible with the intrinsic partial order of the XZ-corrections). Computing this partial order is expensive (it amounts to extracting a DAG and doing a topological sort as well), therefore we also store it as an attribute. Further, when the XZCorrections instance is derived from a flow object, the flow's partial order is a valid partial order for the XZ-corrections, so it is useful to pass it as a parameter to the dataclass constructor.

We also introduce an static method allowing to initialize XZCorrections instances directly from the x_corrections and the z_corrections mappings.

Why do we treat open graphs as parametric objects ?
  • Trying to find a gflow (or a causal flow) only makes sense on an open graph whose measurement labels are planes (not Pauli axes). We want this check to be performed by the static type checker.
  • Extracting a pattern from an open graph (or an XZCorrections object) requires information on the measurement angles. Treating the angles list as an object independent of the open graph demands a rather cumbersome bookkeeping (e.g., angles must be defined w.r.t. measured nodes, for Pauli axes the notion of angle is replaced by a Sign, etc.). At the same time, certain operations on an open graph (namely, trying to find flows), do not need information on the angles. Therefore, it should be possible to have "plane/axis-open graphs" and "measurement-open graphs", respectively without and with information about the angles. The static type-checker should only allow "to_pattern" conversions on open graphs with angle information.

We propose to achieve this by implementing a "type superstructure" on top of the existing classes Axis, Plane and Measurement to avoid modifying the existing functionalities unrelated to flows. We detail this next.

Measurement types

flowchart TD
	n(**ABC**)

	n0("**AbstractMeasurement**")

	n1("**AbstractPlanarMeasurement**")

	n2("**Enum**")
	
	n3("**Plane**")
	n4("**Axis**")
	n5("**Measurement** (dataclass)")

n .-> n0
n0 .-> n1
n0 .-> n4
n1 .-> n5
n2 .-> n3
n2 .-> n4
n1 .-> n3
Loading

This type structure should allow the following:

  • On OpenGraph[Axis|Plane] we can look for Pauli flow only (enforced statically).
  • On OpenGraph[Plane] we can look for causal, g- and Pauli flow.
  • On OpenGraph[Measurement] we can look for causal, g- and Pauli flow.
    • If we look for causal or g-flow, measurements with a Pauli angle should be intepreted as planes.
    • If we look for a Pauli flow, measurements with a Pauli angle should be intepreted as axes.

To that purpose, we introduce the following abstract class methods:

  • :func: AbstractMeasurement.to_plane_or_axis
  • :func: AbstractPlanarMeasurement.to_plane

Further, a conversion to Pattern is only possible from OpenGraph[Measurement] and XZCorrections[Measurement] objects (which have information on the measurement angles).

Methods implemented in this PR

(See docstrings for further details)

OpenGraph

  • from_pattern (static method)
  • to_pattern
  • neighbors
  • odd_neighbors
  • find_causal_flow
  • find_gflow
  • find_pauli_flow

PauliFlow (and children)

  • from_correction_matrix (class method)

  • to_corrections

XZCorrections

  • from_measured_nodes_mapping (static method)

  • to_pattern

  • generate_total_measurement_order

  • extract_dag

  • is_compatible

Comments

  • The old function graphix.generator._pflow2pattern, graphix.generator._gflow2pattern and graphix.generator._flow2pattern are respectively implemented in PauliFlow/GFlow/CausalFlow.to_corrections and XZCorrections.to_pattern.

  • The method OpenGraph.to_pattern provides an immediate way to extract a deterministic pattern on a given resource open graph state in similar way to the old API: it attempts to compute a flow (first causal, then Pauli) and then uses the recipe in Ref. [3] to write the pattern.

    However, the new API also allows the following:

    • Extract a pattern from the open graphs's Pauli flow even if it also has a causal flow.
    • Extract a pattern from a custom flow.
    • Extract a pattern from a custom set of XZ-corrections (runnable, but not necessarily deterministic).
    • Construct a pattern from a set of XZ-corrections with a custom total order.

Finding flows of an open graph

We provide here additional details on the interface of the OpenGraph object with the flow-finding algorithms.

Causal flow

flowchart TD

	n0["OpenGraph.find_causal_flow()"]

	n1["find_cflow(self)"]

	n2("None")

	n3("CausalFlow")

n0 --> n1
n1 --> n2
n1 --> n3

style n2 stroke:#A6280A, stroke-width:4px
style n3 stroke:#0AA643, stroke-width:4px
Loading

The function graphix.flow._find_cflow.find_cflow implements the layer-by-layer algoritm with $O(N^2)$ complexity in Ref. [2]

Gflow and Pauli flow

---
config:
layout: elk
---
flowchart TD

	n0["OpenGraph.find_gflow()"]

	n00["PlanarAlgebraicOpenGraph(self)"]

	n1["compute_correction_matrix(aog)"]

	n3["GFlow.from_correction_matrix(correction_matrix)"]

	n9("GFlow")
	
	n2("None")

	nn0["OpenGraph.find_pauli_flow()"]

	nn00["AlgebraicOpenGraph(self)"]

	nn1["compute_correction_matrix(aog)"]

	nn3["PauliFlow.from_correction_matrix(correction_matrix)"]

	nn9("PauliFlow")
	
	nn2("None")

n0 --> n00
n00 --"aog"--> n1
n1 --> n2
n1 -- "correction_matrix"--> n3
n3 --> n9
n3 --> n2

nn0 --> nn00
nn00 --"aog"--> nn1
nn1 --> nn2
nn1 -- "correction_matrix"--> nn3
nn3 --> nn9
nn3 --> nn2

style n2 stroke:#A6280A, stroke-width:4px
style n9 stroke:#0AA643, stroke-width:4px

style nn2 stroke:#A6280A, stroke-width:4px
style nn9 stroke:#0AA643, stroke-width:4px
Loading

The function graphix.flow._find_gpflow.compute_correction_matrix performs the first part of the algebraic flow-finding algorithm in Ref. [1]. The second part (i.e., verifying that a partial order compatible with the correction matrix exists) is done by the class method .from_correction_matrix:

---
config:
layout: elk
---
flowchart TD

	n3[".from_correction_matrix(correction_matrix)"]

	n7["compute_partial_order_layers(correction_matrix)"]

	n5["correction_matrix.to_correction_function()"]
	n2("None")

	n9("cls(
	aog.og, correction_function, partial_order_layers)")
	


n3 --> n5
n3 --> n7
n7 -- "partial_order_layers" --> n9
n5 -- "correction_function" --> n9
n7 --> n2


style n2 stroke:#A6280A, stroke-width:4px
style n9 stroke:#0AA643, stroke-width:4px
Loading
Details on the interface with the algebraic flow-finding algorithm

We introduce two parametric dataclasses:

flowchart TD
	n0("**AlgebraicOpenGraph[_M_co]**")
	n1("**PlanarAlgebraicOpenGraph[_PM_co]**")
n0 --> n1

linkStyle 0 stroke: #276cf5ff
Loading

where the parametric type variable _M_co and _PM_co where defined above.

AlgebraicOpenGraph is a dataclass to manage the mapping between the open graph nodes and the row and column indices of the matrices involved in the algebraic flow-finding algorithm. This class replaces the class OpenGraphIndex introduced in PR 337.

The class PlanarAlgebraicOpenGraph only rewrites the method _compute_og_matrices which calculates the flow-demand $M$ and the order-demand $N$ matrices by reading out the measurement planes (or axes) of the open graph. In particular,

  • AlgebraicOpenGraph._compute_og_matrices calls self.og.measurements[node].to_plane_or_axis which will intepret measurements with a Pauli angle as axes.

    This is the adequate behavior when we seek to compute a Pauli flow.

  • PlanarAlgebraicOpenGraph._compute_og_matrices calls self.og.measurements[node].to_plane which will intepret measurements with a Pauli angle as planes.

    This is the adequate behavior when we seek to compute a gflow or a causal flow.

Feautures to be included in the next PR of this series

  • Methods to verify the correctness of flow and XZ-corrections objects:

    • PauliFlow.is_well_formed
    • GFlow.is_well_formed
    • CausalFlow.is_well_formed
    • XZCorrections.is_well_formed
  • Adapt exisiting methods in the class OpenGraph:

    • Adapt OpenGraph.compose
    • Adapt OpenGraph.is_close
  • Methods to replace parametric angles with float values in PauliFlow, its children and XZCorrections.

  • Method to extract XZCorrections from a Pattern instance.

  • Class method .from_correction_function for PauliFlow and its children.

  • Method to a extract a GFlow from a Pattern instance. This requires analysis of the existing algorithms in the codebase.

  • Re-introduce benchmark tests on flow algorithms (formerly in tests.test_find_pflow.py.

  • Visualization features (optional)

    • Implement pretty print methods for flow and XZ-corrections objects.
    • Introduce a tool to visualize flow objects (similar to the existing tool for pattern visualization).

References

[1] Mitosek and Backens, 2024 (arXiv:2410.23439).

[2] Mhalla and Perdrix, (2008), Finding Optimal Flows Efficiently, doi.org/10.1007/978-3-540-70575-8_70

[3] Browne et al., 2007 New J. Phys. 9 250 (arXiv:quant-ph/0702212).

@codecov
Copy link

codecov bot commented Nov 3, 2025

Codecov Report

❌ Patch coverage is 95.46279% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.40%. Comparing base (35f4e85) to head (264a0af).

Files with missing lines Patch % Lines
graphix/pyzx.py 0.00% 10 Missing ⚠️
graphix/opengraph.py 91.89% 6 Missing ⚠️
graphix/flow/_find_gpflow.py 97.83% 5 Missing ⚠️
graphix/flow/core.py 98.05% 3 Missing ⚠️
graphix/fundamentals.py 95.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #358      +/-   ##
==========================================
+ Coverage   79.68%   80.40%   +0.71%     
==========================================
  Files          42       44       +2     
  Lines        5854     6251     +397     
==========================================
+ Hits         4665     5026     +361     
- Misses       1189     1225      +36     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@emlynsg emlynsg self-assigned this Nov 5, 2025
@emlynsg emlynsg self-requested a review November 5, 2025 09:03
@emlynsg emlynsg removed their assignment Nov 5, 2025

from graphix.pattern import Pattern

# TODO: Maybe move these definitions to graphix.fundamentals and graphix.measurements ? Now they are redefined in graphix.flow._find_gpflow, not very elegant.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree these definitions would make more sense in those files rather than in opengraph.py

Copy link

@emlynsg emlynsg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reviewed the PR.
Two points to make before approving:

  • Some of the renamed attributes (e.g. in OpenGraph) will break existing external code referencing them (e.g. inside to obtain the nx graph), but I don't think it's a big problem.
  • It would be good for my understanding to discuss the differences between how we have written the cflow algorithm and what is in Mhalla & Perdrix (2008), as we seem to have made some subtle changes.
    I'm not too concerned given the tests, so if someone else is able to verify the flow parts of the PR, I am happy to approve it.

@matulni
Copy link
Contributor Author

matulni commented Nov 5, 2025

I noticed that PauliFlow.to_corrections() was inadvertently mutating the first element of the partial_order_attributes. If fixed this in commit 264a0af by making an appropriate copy, and further replaced set by frozenset in the correction function and partial order attributes of flow objects to emphasise that they should not be changed after instantiation.

@matulni
Copy link
Contributor Author

matulni commented Nov 5, 2025

I have reviewed the PR. Two points to make before approving:

  • Some of the renamed attributes (e.g. in OpenGraph) will break existing external code referencing them (e.g. inside to obtain the nx graph), but I don't think it's a big problem.
  • It would be good for my understanding to discuss the differences between how we have written the cflow algorithm and what is in Mhalla & Perdrix (2008), as we seem to have made some subtle changes.
    I'm not too concerned given the tests, so if someone else is able to verify the flow parts of the PR, I am happy to approve it.

Thanks for the review @emlynsg.

About the renaming I think that we don't guarantee backwards compatibility, but I'm open to discuss it. The rationale is as follows:

  • I find OpenGraph.graph more intuitive than OpenGraph.inside.
  • I think it's more user-friendly to call the input and output nodes in OpenGraph and Pattern the same way, hence the change OpenGraph.inputs $\rightarrow$ OpenGraph.input_nodes, etc.
  • In the literature we often define open graphs as the tuple $(G, I, O, \lambda)$, so that's why I changed the order of the attributes (which defines the order of the input parameters of the dataclass constructor).

About the causal flow algorithm: indeed the implementation seems to be more succinct than the algorithm in the original paper, but this was the case before the refactor. I think I convinced myself that they are equivalent, we can of course discuss it in person.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants