From db7a6a07d84e44ac8b0651c52baceac3abb0856a Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Fri, 20 Feb 2026 12:45:21 +0100 Subject: [PATCH 1/6] Add `perform_pauli_pushing` to `Pattern` This commit exposes the method `perform_pauli_pushing` in `Pattern`. The method performs the Pauli pushing in place, unless called with `copy=True`, in which case a new pattern is returned. By default, the method uses `to_space_optimal_pattern` to prepare the nodes on a need-by-need basis. If called with `standardize=True`, a standardized pattern is returned instead. Warnings are issued if Pauli nodes have not been inferred or if `leave_nodes` contains non-Pauli nodes. --- graphix/optimization.py | 43 +++++++++++++++++++++++++++++++--- graphix/pattern.py | 51 +++++++++++++++++++++++++++++++++++++++-- tests/test_pattern.py | 17 ++++++++++++++ 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index ac2350d52..d678738d7 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -8,6 +8,7 @@ from dataclasses import dataclass from types import MappingProxyType from typing import TYPE_CHECKING +from warnings import warn import networkx as nx @@ -244,8 +245,38 @@ def extract_graph(self) -> nx.Graph[int]: graph.add_edge(u, v) return graph - def perform_pauli_pushing(self, leave_nodes: set[Node] | None = None) -> Self: - """Move all Pauli measurements before the other measurements (except nodes in `leave_nodes`).""" + def perform_pauli_pushing(self, leave_nodes: set[Node] | None = None, *, stacklevel: int = 1) -> Self: + """Move Pauli measurements before the other measurements. + + Parameters + ---------- + leave_nodes : set[Node], optional + Nodes that should not be moved. This constraint only + applies to Pauli nodes and has no effect on non-Pauli nodes. + stacklevel : int, optional + Stack level to use for warnings. Defaults to 1, meaning that warnings + are reported at this function's call site. + + Returns + ------- + Pattern + The pattern in which Pauli measurements have been moved + before the other measurements. + """ + self._warn_non_inferred_pauli_measurements(stacklevel=stacklevel + 1) + + if leave_nodes: + leave_non_pauli_nodes = [ + cmd.node + for cmd in self.m_list + if not isinstance(cmd.measurement, PauliMeasurement) and cmd.node in leave_nodes + ] + if leave_non_pauli_nodes: + warn( + f"`leave_nodes` contains nodes that are not Pauli: {leave_non_pauli_nodes}.", + stacklevel=stacklevel + 1, + ) + if leave_nodes is None: leave_nodes = set() shift_domains: dict[int, set[int]] = {} @@ -268,7 +299,7 @@ def expand_domain(domain: AbstractSet[int]) -> set[int]: for cmd in self.m_list: s_domain = expand_domain(cmd.s_domain) t_domain = expand_domain(cmd.t_domain) - if not isinstance(cmd.measurement, PauliMeasurement) or cmd.node in leave_nodes: + if not isinstance(cmd.measurement, PauliMeasurement) or (leave_nodes and cmd.node in leave_nodes): non_pauli_list.append( command.M(node=cmd.node, measurement=cmd.measurement, s_domain=s_domain, t_domain=t_domain) ) @@ -592,6 +623,12 @@ def extract_xzcorrections(self) -> XZCorrections[Measurement]: og, x_corr, z_corr ) # Raises a `XZCorrectionsError` if the input dictionaries are not well formed. + def _warn_non_inferred_pauli_measurements(self, stacklevel: int) -> None: + for m in self.m_list: + if isinstance(m.measurement, BlochMeasurement) and m.measurement.try_to_pauli() is not None: + warn("Pattern with non-inferred Pauli measurements.", stacklevel=stacklevel + 1) + return + def _add_correction_domain(domain_dict: dict[Node, set[Node]], node: Node, domain: set[Node]) -> None: """Merge a correction domain into ``domain_dict`` for ``node``. diff --git a/graphix/pattern.py b/graphix/pattern.py index d77cf4aeb..4f8dbf240 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -21,7 +21,7 @@ from graphix import command, optimization from graphix.clifford import Clifford -from graphix.command import Command, CommandKind +from graphix.command import Command, CommandKind, Node from graphix.flow.exceptions import FlowError from graphix.fundamentals import Axis, Plane, Sign from graphix.graphsim import GraphState @@ -1690,6 +1690,53 @@ def to_bloch(self) -> Pattern: """ return self.map(lambda m: m.to_bloch()) + def perform_pauli_pushing( + self, + leave_nodes: set[Node] | None = None, + copy: bool = False, + standardize: bool = False, + *, + stacklevel: int = 1, + ) -> Pattern: + """Move Pauli measurements before the other measurements. + + Parameters + ---------- + leave_nodes : set[Node], optional + Nodes that should not be moved. This constraint only + applies to Pauli nodes and has no effect on non-Pauli nodes. + copy : bool, optional + If ``True``, the current pattern remains unchanged and a + new pattern is returned. The default is ``False``, meaning + that changes are performed in place. + standardize: bool, optional + If ``True``, the pattern is returned in standardized form. + The default is ``False``: the nodes are prepared on a + need-by-need basis, minimizing space usage. + stacklevel : int, optional + Stack level to use for warnings. Defaults to 1, meaning that warnings + are reported at this function's call site. + + Returns + ------- + Pattern + The pattern in which Pauli measurements have been moved + before the other measurements. If ``copy`` is ``False``, + the result is ``self``. + + Notes + ----- + This function relies on :func:`StandardizedPattern.perform_pauli_pushing`. + """ + standardized_pattern = optimization.StandardizedPattern.from_pattern(self).perform_pauli_pushing( + leave_nodes, stacklevel=stacklevel + 1 + ) + pattern = standardized_pattern.to_pattern() if standardize else standardized_pattern.to_space_optimal_pattern() + if copy: + return pattern + self.__seq = pattern.__seq + return self + class PatternError(Exception): """Exception subclass to handle pattern errors.""" @@ -1772,7 +1819,7 @@ def measure_pauli(pattern: Pattern, *, ignore_pauli_with_deps: bool = False, sta pat = Pattern() standardized_pattern = optimization.StandardizedPattern.from_pattern(pattern) if not ignore_pauli_with_deps: - standardized_pattern = standardized_pattern.perform_pauli_pushing() + standardized_pattern = standardized_pattern.perform_pauli_pushing(stacklevel=stacklevel + 1) output_nodes = set(pattern.output_nodes) graph = standardized_pattern.extract_graph() graph_state = GraphState(nodes=graph.nodes, edges=graph.edges, vops=standardized_pattern.c_dict) diff --git a/tests/test_pattern.py b/tests/test_pattern.py index dc9e9c070..44375afd0 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -1138,6 +1138,23 @@ def test_extract_xzc_easy_example(self) -> None: assert xzc.z_corrections == xzc_ref.z_corrections assert xzc.partial_order_layers == xzc_ref.partial_order_layers + def test_perform_pauli_pushing(self) -> None: + original_pattern = Pattern( + input_nodes=[0], cmds=[N(1), E((1, 0)), N(2), E((1, 2)), M(1, Measurement.XY(0.1)), M(0)] + ) + pattern = original_pattern.copy() + pauli_pushed_pattern = pattern.perform_pauli_pushing(copy=True) + assert pattern == original_pattern + assert list(pauli_pushed_pattern) == [N(1), E((0, 1)), M(0), N(2), E((1, 2)), M(1, Measurement.XY(0.1))] + pattern.perform_pauli_pushing() + assert pattern == pauli_pushed_pattern + assert original_pattern.perform_pauli_pushing(leave_nodes={0}, copy=True) == original_pattern + with pytest.warns(UserWarning, match="`leave_nodes` contains nodes that are not Pauli"): + original_pattern.perform_pauli_pushing(leave_nodes={1}, copy=True) + with pytest.warns(UserWarning, match="Pattern with non-inferred Pauli measurements."): + original_pattern.to_bloch().perform_pauli_pushing() + assert original_pattern.perform_pauli_pushing(copy=True, standardize=True).is_standard() + def cp(circuit: Circuit, theta: Angle, control: int, target: int) -> None: """Controlled rotation gate, decomposed.""" # noqa: D401 From 9e49fce6f6ad1ce6483239459b6aa41319173cf4 Mon Sep 17 00:00:00 2001 From: thierry-martinez Date: Fri, 20 Feb 2026 14:14:35 +0100 Subject: [PATCH 2/6] Update graphix/optimization.py Co-authored-by: matulni --- graphix/optimization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index d678738d7..b5bd7d1de 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -273,7 +273,7 @@ def perform_pauli_pushing(self, leave_nodes: set[Node] | None = None, *, stackle ] if leave_non_pauli_nodes: warn( - f"`leave_nodes` contains nodes that are not Pauli: {leave_non_pauli_nodes}.", + f"`leave_nodes` contains nodes that are not Pauli: {leave_non_pauli_nodes}. The constraint has no effect on these nodes.", stacklevel=stacklevel + 1, ) From 6be7458353b10ec4abca7f8faab2e7cdb0f4cfbe Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Fri, 20 Feb 2026 14:16:49 +0100 Subject: [PATCH 3/6] Use `AbstractSet` Suggested by Mateo. --- graphix/optimization.py | 4 ++-- graphix/pattern.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index b5bd7d1de..c948c4655 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -245,12 +245,12 @@ def extract_graph(self) -> nx.Graph[int]: graph.add_edge(u, v) return graph - def perform_pauli_pushing(self, leave_nodes: set[Node] | None = None, *, stacklevel: int = 1) -> Self: + def perform_pauli_pushing(self, leave_nodes: AbstractSet[Node] | None = None, *, stacklevel: int = 1) -> Self: """Move Pauli measurements before the other measurements. Parameters ---------- - leave_nodes : set[Node], optional + leave_nodes : AbstractSet[Node], optional Nodes that should not be moved. This constraint only applies to Pauli nodes and has no effect on non-Pauli nodes. stacklevel : int, optional diff --git a/graphix/pattern.py b/graphix/pattern.py index 4f8dbf240..939da4c5e 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -1692,7 +1692,7 @@ def to_bloch(self) -> Pattern: def perform_pauli_pushing( self, - leave_nodes: set[Node] | None = None, + leave_nodes: AbstractSet[Node] | None = None, copy: bool = False, standardize: bool = False, *, @@ -1702,7 +1702,7 @@ def perform_pauli_pushing( Parameters ---------- - leave_nodes : set[Node], optional + leave_nodes : AbstractSet[Node], optional Nodes that should not be moved. This constraint only applies to Pauli nodes and has no effect on non-Pauli nodes. copy : bool, optional From 358784979e7f2c4c079a02087374e1417f6ffdc2 Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Fri, 20 Feb 2026 17:51:11 +0100 Subject: [PATCH 4/6] Remove useless assignment --- graphix/optimization.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/graphix/optimization.py b/graphix/optimization.py index c948c4655..dca0392de 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -277,8 +277,6 @@ def perform_pauli_pushing(self, leave_nodes: AbstractSet[Node] | None = None, *, stacklevel=stacklevel + 1, ) - if leave_nodes is None: - leave_nodes = set() shift_domains: dict[int, set[int]] = {} def expand_domain(domain: AbstractSet[int]) -> set[int]: From e84062a854dddd2d273dae8f74cff3ea846a0d05 Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Fri, 20 Feb 2026 17:53:24 +0100 Subject: [PATCH 5/6] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21980c1c6..5851f530d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #386, #433: Added `Statevec.fidelity` and `Statevec.isclose` methods for pure-state fidelity computation and equality check up to global phase. +- #447: `Pattern.perform_pauli_pushing` which calls `StandardizedPattern.perform_pauli_pushing`. + ### Fixed - #429 From 088be2c201fd781ef7208fd6c6faf0db963075cb Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Fri, 20 Feb 2026 17:57:20 +0100 Subject: [PATCH 6/6] Update CHANGELOG.md for #440, #441 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5851f530d..0417df116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Moved the conditional logic to `graphix.simulator` to remove code duplication in the backends. - Solves [#428](https://github.com/TeamGraphix/graphix/issues/428). +- #440, #441: `Statevec.nqubits` now returns the correct value. + ### Changed - #181, #423: Structural separation of Pauli measurements