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).

@thierry-martinez
Copy link
Collaborator

LGTM! Thanks!

@matulni
Copy link
Contributor Author

matulni commented Nov 18, 2025

Hi @thierry-martinez, I've being thinking and I'm not fully convinced by 0c21256

By introducing the new functions find_gflow, find_pflow to generate flows without catching exceptions, we break a bit the object-oriented approach. The user may encounter this need too (e.g., generating random open graphs which may or may not have flow inside a loop), and recurring to find_gflow, find_pflow hidden in another module feels a bit hacky.

In addition, these new functions lead to a circular import structure (which, if not a red boundary on its own, can be symptomatic of bad design).

At the same time, as discussed above, having functions which return one type only is very handy for chaining method calls without getting static-typing errors.

Should we add two methods to OpenGraph (one with two return types and one that raises exceptions) ? I'm also not a big fan of this idea because it seems overly complex, but maybe it's justified ?

(Sorry to bring this up after your pass!)

@thierry-martinez
Copy link
Collaborator

I think that having both find_ and extract_ methods in OpenGraph is more symmetrical indeed.

Copy link
Contributor

@shinich1 shinich1 left a comment

Choose a reason for hiding this comment

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

Shouldn't we add/update examples? otherwise, LGTM.

@matulni
Copy link
Contributor Author

matulni commented Nov 20, 2025

Shouldn't we add/update examples? otherwise, LGTM.

Thanks for reviewing! Yes, we definitely should, but I propose that adding tutorials/documentation beyond the docstrings is done in separate PRs for conciseness.

@shinich1
Copy link
Contributor

Shouldn't we add/update examples? otherwise, LGTM.

Thanks for reviewing! Yes, we definitely should, but I propose that adding tutorials/documentation beyond the docstrings is done in separate PRs for conciseness.

understood!

@matulni matulni merged commit 941eeed into TeamGraphix:master Nov 24, 2025
24 checks passed
@emlynsg emlynsg removed their request for review November 24, 2025 16:03
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.

4 participants