Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
ea51ded
Add flow verification utils
matulni Nov 25, 2025
a7bfaf1
Up flow exceptions
matulni Nov 25, 2025
44c46f0
Refactor flow exceptions
matulni Nov 27, 2025
fff002d
Merge branch 'master' into rf_flow_iswellformed
matulni Nov 27, 2025
1a0746c
Add get measurement label method and check on planes in causal flow
matulni Nov 27, 2025
df39a01
Fix typing
matulni Nov 27, 2025
86148ab
wip
matulni Nov 28, 2025
75ec180
Add check partial order no duplicates
matulni Nov 28, 2025
f5368f1
Merge branch 'rf_flow_iswellformed' into rf_xz_iswellformed
matulni Nov 28, 2025
dea16c2
wip
matulni Nov 28, 2025
58af8bd
XZCorrections.check_well_formed passing tests
matulni Nov 28, 2025
3464b6e
XZCorrections.check_well_formed passing tests
matulni Nov 28, 2025
202b1f5
Merge branch 'master' into rf_flow_iswellformed
matulni Nov 28, 2025
86dfc91
Merge branch 'master' into rf_xz_iswellformed
matulni Nov 28, 2025
6faa3f0
XZCorrections.check_well_formed passing all tests
matulni Dec 1, 2025
488ff0d
Add partial_order module
matulni Dec 1, 2025
84a1837
Add partial order from pattern
matulni Dec 1, 2025
89189cd
wip
matulni Dec 2, 2025
d14175c
Merge branch 'master' into rf_flow_iswellformed
matulni Dec 2, 2025
8236dcb
Merge branch 'master' into rf_flow_from_p
matulni Dec 2, 2025
0bd03fb
Tests passing
matulni Dec 2, 2025
0dc5653
Merge branch 'master' into rf_flow_iswellformed
matulni Dec 2, 2025
57eee75
Merge branch 'master' into rf_xz_iswellformed
matulni Dec 2, 2025
ef33e5e
Merge branch 'master' into rf_flow_from_p
matulni Dec 2, 2025
966a272
Move flow extraction to standardized pattern
matulni Dec 3, 2025
6e53b7e
Adapt visualization to new API
matulni Dec 3, 2025
5165226
Remove dependence on graphix.gflow from graphix.pattern
matulni Dec 3, 2025
1cbdc95
Add feddback meeting
matulni Dec 4, 2025
95d2427
Add mods meeting
matulni Dec 4, 2025
3c821d9
Combine correction function errors and generic flow errors into a sin…
matulni Dec 4, 2025
50b901d
Merge branch 'rf_flow_iswellformed' into rf_xz_iswellformed
matulni Dec 4, 2025
c13057d
Remove MERGE_MSG
matulni Dec 8, 2025
095a2de
Add comment
matulni Dec 8, 2025
c46c944
Merge branch 'master' into rf_xz_iswellformed
matulni Dec 8, 2025
91860fa
Up CHANGELOG
matulni Dec 8, 2025
e5c004e
Merge branch 'rf_xz_iswellformed' into rf_flow_from_p
matulni Dec 9, 2025
f0c8a3c
wip
matulni Dec 9, 2025
9450293
Add FlowError exceptions in flow from pattern methods
matulni Dec 9, 2025
2a8e4d9
Remove dep on Pattern.get_layers
matulni Dec 9, 2025
d3eaee8
Add comments
matulni Dec 9, 2025
6c332e9
Merge branch 'master' into rf_flow_from_p
matulni Dec 9, 2025
4487c94
Add check on N commands when extracting open graph to form flow
matulni Dec 9, 2025
fcc279b
Merge branch 'master' into rf_flow_from_p
matulni Dec 9, 2025
4a3e79c
Fix pyright
matulni Dec 9, 2025
0b11c0c
Cast defaultdict into dict
matulni Dec 10, 2025
225e417
Up docs
matulni Dec 10, 2025
26a4cf9
Refactor flow extraction from standard pattern
matulni Dec 11, 2025
0ece2f2
Up docs
matulni Dec 11, 2025
a31fb70
Up docs
matulni Dec 11, 2025
b4df5ba
Merge branch 'master' into rf_flow_from_p
matulni Dec 12, 2025
af20a82
Merge branch 'master' into rf_flow_from_p
matulni Dec 15, 2025
ff8e6f0
Standardize pattern before extracting partial order layers
matulni Dec 15, 2025
596ec4b
Apply suggestions from Thierry's code review
matulni Jan 5, 2026
975f7aa
Make optimizations._update_corrections return None
matulni Jan 5, 2026
f5eca86
Merge branch 'master' into rf_flow_from_p
matulni Jan 5, 2026
f640645
Up CHANGELOG and fix ruff
matulni Jan 5, 2026
a9a9377
Update test visualization baseline
thierry-martinez Jan 6, 2026
f664c29
Merge branch 'master' into rf_flow_from_p
matulni Jan 6, 2026
b177d34
Shorten docstring
matulni Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
32 changes: 9 additions & 23 deletions graphix/flow/_find_gpflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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 = [
Expand Down
54 changes: 54 additions & 0 deletions graphix/flow/_partial_order.py
Original file line number Diff line number Diff line change
@@ -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)
206 changes: 205 additions & 1 deletion graphix/optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,8 +18,16 @@
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.flow.exceptions import (
FlowGenericError,
FlowGenericErrorReason,
)
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
from graphix.states import BasicStates

if TYPE_CHECKING:
from collections.abc import Iterable, Mapping
Expand Down Expand Up @@ -362,6 +371,180 @@ 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.

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:
- 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.
"""
oset = frozenset(self.output_nodes) # First layer by convention.
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).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] = {}

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]
): # Don't include multiple edges in the dag.
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)
if generations is None:
raise ValueError("Pattern domains form closed loops.")

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 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]
The causal flow associated with the current pattern.

Raises
------
FlowError
If the pattern:
- 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 or if the pattern corrections form closed loops.

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.
"""
correction_function: dict[int, set[int]] = defaultdict(set)
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}:
raise FlowGenericError(FlowGenericErrorReason.XYPlane)
_update_corrections(m.node, m.s_domain - pre_measured_nodes, correction_function)

for node, domain in self.x_dict.items():
_update_corrections(node, domain - pre_measured_nodes, 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]:
"""Extract the generalized flow (gflow) structure from the current measurement pattern.

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]
The gflow associated with the current pattern.

Raises
------
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 or if the pattern corrections form closed loops.

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

for m in self.m_list:
if m.plane in {Plane.XZ, Plane.YZ}:
correction_function.setdefault(m.node, set()).add(m.node)
_update_corrections(m.node, m.s_domain - pre_measured_nodes, correction_function)

for node, domain in self.x_dict.items():
_update_corrections(node, domain - pre_measured_nodes, 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, correction_function, partial_order_layers)
gf.check_well_formed()
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``.
Expand Down Expand Up @@ -418,6 +601,27 @@ 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]]) -> None:
"""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.

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)


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)
Expand Down
Loading