Skip to content
Open
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- #507: Introduced new methods `PauliString.__str__`, `PauliString.from_str`, `PauliString.to_tableau`, and `PauliString.from_tableau`.

- #510
- Added new attribute `OpenGraph.output_cliffords`
- Added `clifford` abstract method to `AbstractMeasurement`. Implemented it for `Plane` and `Axis`.

### Fixed

- #454, #481: Ensure `Pattern.minimize_space` only reduces max-space and does not increase it.

- #235, #489: Correct sign for `YZ` measurements in `from_pyzx_graph`. ZX diagrams are now correctly converted into open graphs, even if they are reduced.

- #510
- `Pattern.extract_opengraph` returns the same open graph before and after standardization.
- The round trip `Pattern` -> `OpenGraph` -> `Flow` -> `XZCorrections` -> `Pattern` is guaranteed for deterministic patterns in the LC fragment.

### Changed

- #452: Use `uv` for dependency management
Expand All @@ -51,6 +59,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- #507: Static method `PauliString.from_measured_node` is subsumed by the function `extraction_ps_from_corrected_node`.

- #510:
- `Pattern.extract_opengraph` standardizes the pattern first.
- `XZCorrections.to_pattern` applies Cliffords in `OpenGraph.output_cliffords` at the end of the pattern.
- `OpenGraph.isclose` and `OpenGraph.is_equal_structurally` check equality of `OpenGraph.output_cliffords`.
- `OpenGraph.compose` merges Clifford decorations with measurements or other Clifford decorations on outputs if required.
- `.draw` methods allow to show Clifford commands in the outputs.
- `PauliFlow.extract_circuit` raises `NotImplementedError` if the open graph has Clifford decorations.

- #512: Method `Circuit.simulate_statevector` accepts a `backend: DenseStateBackend[_DenseStateT] | Literal["statevector", "densitymatrix"]` parameter.

## [0.3.5] - 2026-03-26
Expand Down
2 changes: 1 addition & 1 deletion examples/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
# Instead of the measurement planes, we can show the local Clifford of the resource graph.
# see *clifford.py* for the details of the indices of each single-qubit Clifford operators.
# 6 is the Hadamard and 8 is the :math:`\sqrt{iY}` operator.
pattern.draw(flow_from_pattern=True, show_local_clifford=True)
pattern.draw(flow_from_pattern=True, local_clifford=True)

# %%
# Visualize based on the graph
Expand Down
19 changes: 12 additions & 7 deletions graphix/clifford.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,21 +132,26 @@ def __matmul__(self, other: Clifford) -> Clifford:
return Clifford(CLIFFORD_MUL[self.value][other.value])
return NotImplemented

def measure(self, pauli: Pauli) -> Pauli:
"""Compute C† P C."""
if pauli.symbol == I:
return copy.deepcopy(pauli)
def measure_axis(self, axis: Axis) -> Pauli:
"""Compute C† P C with P the Pauli +axis."""
table = CLIFFORD_MEASURE[self.value]
match pauli.symbol:
match axis:
case Axis.X:
symbol, sign = table.x
case Axis.Y:
symbol, sign = table.y
case Axis.Z:
symbol, sign = table.z
case _:
typing_extensions.assert_never(pauli.symbol)
return pauli.unit * Pauli(symbol, ComplexUnit.from_properties(sign=sign))
typing_extensions.assert_never(axis)
return Pauli(symbol, ComplexUnit.from_properties(sign=sign))

def measure(self, pauli: Pauli) -> Pauli:
"""Compute C† P C."""
if pauli.symbol == I:
return copy.deepcopy(pauli)
new_pauli = self.measure_axis(pauli.symbol)
return pauli.unit * new_pauli

def commute_domains(self, domains: Domains) -> Domains:
"""
Expand Down
25 changes: 8 additions & 17 deletions graphix/flow/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
PauliExponentialDAG,
extraction_ps_from_corrected_node,
)
from graphix.command import E, M, N, X, Z
from graphix.command import C, E, M, N, X, Z
from graphix.flow._find_gpflow import (
CorrectionMatrix,
compute_partial_order_layers,
Expand Down Expand Up @@ -200,6 +200,9 @@ def to_pattern(
for corrected_node in self.x_corrections.get(measured_node, []):
pattern.add(X(node=corrected_node, domain={measured_node}))

for output_node, clifford in self.og.output_cliffords.items():
pattern.add(C(node=output_node, clifford=clifford))

pattern.reorder_output_nodes(self.og.output_nodes)
return pattern

Expand Down Expand Up @@ -792,22 +795,8 @@ def draw(self, **options: Unpack[DrawKwargs]) -> None:

Parameters
----------
pauli_measurements : bool, default=True
If ``True``, Pauli-measured nodes are highlighted with distinct coloring.
measurement_labels : bool, default=False
If ``True``, measurement labels (planes and axis) are displayed in the visualization.
node_labels : bool | Mapping[int, str], default=True
If ``True``, display numeric node labels. If a mapping, use custom labels
for nodes specified in the mapping.
node_distance : tuple[float, float], default=(1, 1)
Scaling factors (x_scale, y_scale) applied to node positions.
legend : bool, default=True
If ``True``, legend is shown.
figsize : tuple[int, int] | None, default=None
Figure dimensions (width, height) in inches. If ``None``, dimensions are
determined automatically based on graph structure.
filename : Path | None, default=None
File path to save the visualization. If ``None``, figure is displayed but not saved.
options: Unpack[DrawKwargs]
Options controlling graph visualization. See :class:`VisualizationOptions`.
"""
from graphix.visualization import GraphVisualizer # noqa: PLC0415 Avoid circular imports

Expand Down Expand Up @@ -937,6 +926,8 @@ def extract_circuit(self: PauliFlow[Measurement]) -> ExtractionResult:
[1] Simmons, 2021 (arXiv:2109.05654).
[2] Mitosek and Backens, 2024 (arXiv:2410.23439).
"""
if self.og.output_cliffords:
raise NotImplementedError("Circuit extraction is not supported for open graphs with Clifford decorations.")
pexp_dag = PauliExponentialDAG.from_focused_flow(self)
clifford_map = CliffordMap.from_focused_flow(self)

Expand Down
53 changes: 52 additions & 1 deletion graphix/fundamentals.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
from graphix.repr_mixins import EnumReprMixin

if TYPE_CHECKING:
from typing import TypeAlias
from typing import Self, TypeAlias

from graphix.clifford import Clifford
from graphix.parameter import ExpressionOrFloat

Angle: TypeAlias = float
Expand Down Expand Up @@ -293,6 +294,48 @@ def isclose(self, other: AbstractMeasurement, rel_tol: float = 1e-09, abs_tol: f
"""
return self == other

@abstractmethod
def clifford(self, clifford_gate: Clifford) -> Self:
r"""Return a new measurement command with a :class:`Clifford` applied.

Parameters
----------
clifford_gate : Clifford
Clifford gate to apply before the measurement.

Returns
-------
Self
Equivalent measurement representing the pattern ``MC``.

Notes
-----
- The return type is ``Self``, meaning that a Clifford applied
to a Bloch measurement returns a Bloch measurement, and a
Clifford applied to a Pauli measurement returns a Pauli
measurement.
- The method :func:`Measurement.clifford` does not always
commute with the method :func:`Measurement.to_bloch`: the
underlying Pauli measurement will be the same but the Bloch
representation can be on different planes.

Examples
--------
>>> from graphix.clifford import Clifford
>>> from graphix.measurements import Measurement, PauliMeasurement
>>> Measurement.XY(0.25).clifford(Clifford.H)
Measurement.YZ(1.75)
>>> Measurement.X.clifford(Clifford.S)
-Measurement.Y
>>> for pauli in PauliMeasurement:
... for clifford in Clifford:
... assert pauli.to_bloch().clifford(clifford).try_to_pauli() == pauli.clifford(clifford)
>>> Measurement.Y.clifford(Clifford.H).to_bloch()
Measurement.XY(1.5)
>>> Measurement.Y.to_bloch().clifford(Clifford.H)
Measurement.YZ(1.5)
"""


class AbstractPlanarMeasurement(AbstractMeasurement):
"""Abstract base class for planar measurement objects.
Expand Down Expand Up @@ -328,6 +371,10 @@ class Axis(AbstractMeasurement, EnumReprMixin, Enum, metaclass=CustomMeta):
def to_plane_or_axis(self) -> Axis:
return self

@override
def clifford(self, clifford_gate: Clifford) -> Axis:
return clifford_gate.measure_axis(self).axis


class SingletonI(Enum):
"""Singleton I."""
Expand Down Expand Up @@ -457,3 +504,7 @@ def to_plane(self) -> Plane:
Plane
"""
return self

@override
def clifford(self, clifford_gate: Clifford) -> Plane:
return Plane.from_axes(*(axis.clifford(clifford_gate) for axis in self.axes))
45 changes: 1 addition & 44 deletions graphix/measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,49 +88,6 @@ def XZ(angle: ParameterizedAngle) -> BlochMeasurement: # noqa: N802
"""Return a Bloch measurement on the XZ plane."""
return BlochMeasurement(angle, Plane.XZ)

@abstractmethod
def clifford(self, clifford_gate: Clifford) -> Self:
r"""Return a new measurement command with a :class:`Clifford` applied.

Parameters
----------
clifford_gate : Clifford
Clifford gate to apply before the measurement.

Returns
-------
Self
Equivalent measurement representing the pattern ``MC``.

Notes
-----
- The return type is ``Self``, meaning that a Clifford applied
to a Bloch measurement returns a Bloch measurement, and a
Clifford applied to a Pauli measurement returns a Pauli
measurement.
- The method :func:`Measurement.clifford` does not always
commute with the method :func:`Measurement.to_bloch`: the
underlying Pauli measurement will be the same but the Bloch
representation can be on different planes.

Examples
--------
>>> from graphix.clifford import Clifford
>>> from graphix.measurements import Measurement, PauliMeasurement
>>> Measurement.XY(0.25).clifford(Clifford.H)
Measurement.YZ(1.75)
>>> Measurement.X.clifford(Clifford.S)
-Measurement.Y
>>> for pauli in PauliMeasurement:
... for clifford in Clifford:
... assert pauli.to_bloch().clifford(clifford).try_to_pauli() == pauli.clifford(clifford)
>>> Measurement.Y.clifford(Clifford.H).to_bloch()
Measurement.XY(1.5)
>>> Measurement.Y.to_bloch().clifford(Clifford.H)
Measurement.YZ(1.5)

"""

@abstractmethod
def to_bloch(self) -> BlochMeasurement:
"""Return the measurement description as an angle and a plane on the Bloch sphere.
Expand Down Expand Up @@ -362,7 +319,7 @@ def to_plane(self) -> Plane:

@override
def clifford(self, clifford_gate: Clifford) -> BlochMeasurement:
new_plane = Plane.from_axes(*(PauliMeasurement(axis).clifford(clifford_gate).axis for axis in self.plane.axes))
new_plane = self.plane.clifford(clifford_gate)
cos_pauli = PauliMeasurement(self.plane.cos).clifford(clifford_gate)
sin_pauli = PauliMeasurement(self.plane.sin).clifford(clifford_gate)
exchange = cos_pauli.axis != new_plane.cos
Expand Down
Loading
Loading