diff --git a/CHANGELOG.md b/CHANGELOG.md index cc07e2a80..c63573029 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 @@ -20,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #438: `ComplexUnit.try_from` now uses `cmath.isclose` for float comparison and has optional parameters `rel_tol` and `abs_tol`. +- #440, #441: `Statevec.nqubits` now returns the correct value. + ### Changed - #181, #423: Structural separation of Pauli measurements diff --git a/graphix/optimization.py b/graphix/optimization.py index ac2350d52..dca0392de 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,10 +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`).""" - if leave_nodes is None: - leave_nodes = set() + 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 : 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 + 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}. The constraint has no effect on these nodes.", + stacklevel=stacklevel + 1, + ) + shift_domains: dict[int, set[int]] = {} def expand_domain(domain: AbstractSet[int]) -> set[int]: @@ -268,7 +297,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 +621,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..939da4c5e 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: AbstractSet[Node] | None = None, + copy: bool = False, + standardize: bool = False, + *, + stacklevel: int = 1, + ) -> Pattern: + """Move Pauli measurements before the other measurements. + + Parameters + ---------- + 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 + 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