Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #360, #361: `StandardizedPattern.to_space_optimal_pattern` generates
a pattern that is space-optimal for a given measurement order.

- #193, #364: `Pattern.check_runnability` ensures that a pattern is runnable.

### Fixed

- #364: `Pattern.simulate_pattern`, `Pattern.shift_signals`,
`Pattern.standardize`, `Pattern.perform_pauli_measurements`,
`Pattern.minimize_space`, `Pattern.get_layers` check that the
pattern is runnable beforehand.

- #364: `Pattern.compute_max_degree` and `Pattern.draw_graph` no longer
fail on empty patterns.


### Changed

- #358: Refactor of flow tools - Part I
Expand All @@ -31,6 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- #361: `StandardizedPattern` is now an immutable dataclass. The class method `StandardizedPattern.from_pattern` instantiates a `StandardizedPattern` from `Pattern`.

- #364: `StandardizedPattern.perform_pauli_pushing` replaces `Pattern.move_pauli_measurements_to_the_front`.

- #371: Drop support for Python 3.9

## [0.3.3] - 2025-10-23
Expand Down
89 changes: 88 additions & 1 deletion graphix/optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@

import networkx as nx

# assert_never added in Python 3.11
from typing_extensions import assert_never

import graphix.pattern
from graphix import command
from graphix.clifford import Clifford
from graphix.command import CommandKind, Node
from graphix.measurements import Domains, Outcome
from graphix.fundamentals import Axis
from graphix.measurements import Domains, Outcome, PauliMeasurement

if TYPE_CHECKING:
from collections.abc import Iterable, Mapping
Expand Down Expand Up @@ -154,6 +158,13 @@ def from_pattern(cls, pattern: Pattern) -> Self:
z_dict: dict[Node, set[Node]] = {}
x_dict: dict[Node, set[Node]] = {}

# Standardization could turn non-runnable patterns into
# runnable ones, so we check runnability first to avoid hiding
# code-logic errors.
# For example, the non-runnable pattern E(0,1) M(0) N(1) N(0) would
# become M(0) E(0,1) N(1) N(0), which is runnable.
pattern.check_runnability()

for cmd in pattern:
if cmd.kind == CommandKind.N:
n_list.append(cmd)
Expand Down Expand Up @@ -221,6 +232,82 @@ 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()
shift_domains: dict[int, set[int]] = {}

def expand_domain(domain: AbstractSet[int]) -> set[int]:
"""Merge previously shifted domains into ``domain``.

Parameters
----------
domain : set[int]
Domain to update with any accumulated shift information.
"""
new_domain = set(domain)
for node in domain & shift_domains.keys():
new_domain ^= shift_domains[node]
return new_domain

pauli_list = []
non_pauli_list = []
for cmd in self.m_list:
s_domain = expand_domain(cmd.s_domain)
t_domain = expand_domain(cmd.t_domain)
pm = PauliMeasurement.try_from(
cmd.plane, cmd.angle
) # None returned if the measurement is not in Pauli basis
if pm is None or cmd.node in leave_nodes:
non_pauli_list.append(
command.M(node=cmd.node, angle=cmd.angle, plane=cmd.plane, s_domain=s_domain, t_domain=t_domain)
)
else:
if pm.axis == Axis.X:
# M^X X^s Z^t = M^{XY,0} X^s Z^t
# = M^{XY,(-1)^s·0+tπ}
# = S^t M^X
# M^{-X} X^s Z^t = M^{XY,π} X^s Z^t
# = M^{XY,(-1)^s·π+tπ}
# = S^t M^{-X}
shift_domains[cmd.node] = t_domain
elif pm.axis == Axis.Y:
# M^Y X^s Z^t = M^{XY,π/2} X^s Z^t
# = M^{XY,(-1)^s·π/2+tπ}
# = M^{XY,π/2+(s+t)π} (since -π/2 = π/2 - π ≡ π/2 + π (mod 2π))
# = S^{s+t} M^Y
# M^{-Y} X^s Z^t = M^{XY,-π/2} X^s Z^t
# = M^{XY,(-1)^s·(-π/2)+tπ}
# = M^{XY,-π/2+(s+t)π} (since π/2 = -π/2 + π)
# = S^{s+t} M^{-Y}
shift_domains[cmd.node] = s_domain ^ t_domain
elif pm.axis == Axis.Z:
# M^Z X^s Z^t = M^{XZ,0} X^s Z^t
# = M^{XZ,(-1)^t((-1)^s·0+sπ)}
# = M^{XZ,(-1)^t·sπ}
# = M^{XZ,sπ} (since (-1)^t·π ≡ π (mod 2π))
# = S^s M^Z
# M^{-Z} X^s Z^t = M^{XZ,π} X^s Z^t
# = M^{XZ,(-1)^t((-1)^s·π+sπ)}
# = M^{XZ,(s+1)π}
# = S^s M^{-Z}
shift_domains[cmd.node] = s_domain
else:
assert_never(pm.axis)
pauli_list.append(command.M(node=cmd.node, angle=cmd.angle, plane=cmd.plane))
return self.__class__(
self.input_nodes,
self.output_nodes,
self.results,
self.n_list,
self.e_set,
pauli_list + non_pauli_list,
self.c_dict,
{node: expand_domain(domain) for node, domain in self.z_dict.items()},
{node: expand_domain(domain) for node, domain in self.x_dict.items()},
)

def to_pattern(self) -> Pattern:
"""Return the standardized pattern."""
pattern = graphix.pattern.Pattern(input_nodes=self.input_nodes)
Expand Down
Loading