Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
501f9d1
Pretty-print circuits and patterns
thierry-martinez May 13, 2025
3e029d0
Fix use of `print_pattern` in examples
thierry-martinez May 13, 2025
d0a898b
Add missing import
thierry-martinez May 13, 2025
d940ff2
Update documentation
thierry-martinez May 14, 2025
5f24079
Update tutorial
thierry-martinez May 14, 2025
fecb67d
Restore deprecated print_pattern and add arguments to converters
thierry-martinez May 14, 2025
5b19f12
Add to CHANGELOG
thierry-martinez May 14, 2025
4fe575c
Use `Iterable` in `Circuit.__init__`
thierry-martinez May 14, 2025
f1bfa21
Remove useless list collection before calling `join`
thierry-martinez May 14, 2025
b8a00c0
Use `Iterable` in `Pattern.__init__` and `Pattern.extend`
thierry-martinez May 14, 2025
ff37ce7
Use `enum.auto` instead of strings
thierry-martinez May 14, 2025
a003b29
Use the simpler `to_ascii` method in examples
thierry-martinez May 14, 2025
c38e502
Mixin for pretty-printing dataclasses
thierry-martinez May 14, 2025
9616ddd
Mixin for pretty-printing Enum
thierry-martinez May 14, 2025
83c10e3
Check instance of Enum in EnumMixin
thierry-martinez May 19, 2025
5351cd5
Use `is not None` with `Iterable | None`
thierry-martinez May 19, 2025
4aceb72
Annotate `self` with `DataclassInstance`
thierry-martinez May 19, 2025
76fa4df
Fix ruff
thierry-martinez May 19, 2025
9ea27c5
Include `output_nodes` in `repr`
thierry-martinez May 19, 2025
38c7d16
ruff fix with new linters
thierry-martinez May 19, 2025
606bcb0
Fix swap and better code coverage
thierry-martinez May 19, 2025
c617563
Use raw f-string
thierry-martinez May 19, 2025
f572fad
Update tutorial with `output_nodes`
thierry-martinez May 19, 2025
256af8c
Use `is not None` for `target` in `pattern_to_str`
thierry-martinez May 19, 2025
9bdf94b
Add result type for `Circuit.__init__`
thierry-martinez May 19, 2025
d111615
Rename `DataclassPrettyPrintMixin` and `EnumPrettyPrintMixin`
thierry-martinez May 19, 2025
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Methods for pretty-printing `Pattern`: `to_ascii`, `to_unicode`,
`to_latex`.

### Fixed

- The result of `repr()` for `Pattern`, `Circuit`, `Command`,
`Instruction`, `Plane`, `Axis` and `Sign` is now a valid Python
expression and is more readable.

### Changed

- The method `Pattern.print_pattern` is now deprecated.

## [0.3.1] - 2025-04-21

### Added
Expand Down
6 changes: 5 additions & 1 deletion docs/source/modifier.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ Pattern Manipulation

.. automethod:: perform_pauli_measurements

.. automethod:: print_pattern
.. automethod:: to_ascii

.. automethod:: to_unicode

.. automethod:: to_latex

.. automethod:: standardize

Expand Down
128 changes: 20 additions & 108 deletions docs/source/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,14 @@ For any gate network, we can use the :class:`~graphix.transpiler.Circuit` class
the :class:`~graphix.pattern.Pattern` object contains the sequence of commands according to the measurement calculus framework [#Danos2007]_.
Let us print the pattern (command sequence) that we generated,

>>> pattern.print_pattern() # show the command sequence (pattern)
N, node = 1
E, nodes = (0, 1)
M, node = 0, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
X byproduct, node = 1, domain = [0]
>>> pattern
Pattern(input_nodes=[0], cmds=[N(1), E((0, 1)), M(0), X(1, {0})], output_nodes=[1])

The command sequence represents the following sequence:

* starting with an input qubit :math:`|\psi_{in}\rangle_0`, we first prepare an ancilla qubit :math:`|+\rangle_1` with ['N', 1] command
* We then apply CZ-gate by ['E', (0, 1)] command to create entanglement.
* We measure the qubit 0 in Pauli X basis, by ['M'] command.
* starting with an input qubit :math:`|\psi_{in}\rangle_0`, we first prepare an ancilla qubit :math:`|+\rangle_1` with N(1) command
* We then apply CZ-gate by E((0, 1)) command to create entanglement.
* We measure the qubit 0 in Pauli X basis, by M(0) command.
* If the measurement outcome is :math:`s_0 = 1` (i.e. if the qubit is projected to :math:`|-\rangle`, the Pauli X eigenstate with eigenvalue of :math:`(-1)^{s_0} = -1`), the 'X' command is applied to qubit 1 to 'correct' the measurement byproduct (see :doc:`intro`) that ensure deterministic computation.
* Tracing out the qubit 0 (since the measurement is destructive), we have :math:`H|\psi_{in}\rangle_1` - the input qubit has teleported to qubit 1, while being transformed by Hadamard gate.

Expand Down Expand Up @@ -86,19 +83,9 @@ As a more complex example than above, we show measurement patterns and graph sta
| |
| control: input=0, output=0; target: input=1, output=3 |
+------------------------------------------------------------------------------+
| >>> cnot_pattern.print_pattern() |
| N, node = 0 |
| N, node = 1 |
| N, node = 2 |
| N, node = 3 |
| E, nodes = (1, 2) |
| E, nodes = (0, 2) |
| E, nodes = (2, 3) |
| M, node = 1, plane = XY, angle(pi) = 0, s-domain = [], t_domain = [] |
| M, node = 2, plane = XY, angle(pi) = 0, s-domain = [], t_domain = [] |
| X byproduct, node = 3, domain = [2] |
| Z byproduct, node = 3, domain = [1] |
| Z byproduct, node = 0, domain = [1] |
| >>> cnot_pattern |
| Pattern(cmds=[N(0), N(1), N(2), N(3), E((1, 2)), E((0, 2)), E((2, 3)), M(1), |
| M(2), X(3, {2}), Z(3, {1}), Z(0, {1})], output_nodes=[0, 3]) |
+------------------------------------------------------------------------------+
| **general rotation (an example with Euler angles 0.2pi, 0.15pi and 0.1 pi)** |
+------------------------------------------------------------------------------+
Expand All @@ -108,18 +95,10 @@ As a more complex example than above, we show measurement patterns and graph sta
| |
| input = 0, output = 4 |
+------------------------------------------------------------------------------+
|>>> euler_rot_pattern.print_pattern() |
| N, node = 0 |
| N, node = 1 |
| N, node = 2 |
| N, node = 3 |
| N, node = 4 |
| M, node = 0, plane = XY, angle(pi) = -0.2, s-domain = [], t_domain = [] |
| M, node = 1, plane = XY, angle(pi) = -0.15, s-domain = [0], t_domain = [] |
| M, node = 2, plane = XY, angle(pi) = -0.1, s-domain = [1], t_domain = [] |
| M, node = 3, plane = XY, angle(pi) = 0, s-domain = [], t_domain = [] |
| Z byproduct, node = 4, domain = [0,2] |
| X byproduct, node = 4, domain = [1,3] |
|>>> euler_rot_pattern |
| Pattern(cmds=[N(0), N(1), N(2), N(3), N(4), M(0, angle=-0.2), |
| M(1, angle=-0.15, s_domain={0}), M(2, angle=-0.1, s_domain={1}), |
| M(3), Z(4, domain={0, 2}), X(4, domain={1, 3})], output_nodes=[4]) |
+------------------------------------------------------------------------------+


Expand All @@ -144,33 +123,8 @@ As an example, let us prepare a pattern to rotate two qubits in :math:`|+\rangle

This produces a rather long and complicated command sequence.

>>> pattern.print_pattern() # show the command sequence (pattern)
N, node = 2
N, node = 3
E, nodes = (0, 2)
E, nodes = (2, 3)
M, node = 0, plane = XY, angle(pi) = -0.2975038024267561, s-domain = [], t_domain = []
M, node = 2, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
X byproduct, node = 3, domain = [2]
Z byproduct, node = 3, domain = [0]
N, node = 4
N, node = 5
E, nodes = (1, 4)
E, nodes = (4, 5)
M, node = 1, plane = XY, angle(pi) = -0.14788446865973076, s-domain = [], t_domain = []
M, node = 4, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
X byproduct, node = 5, domain = [4]
Z byproduct, node = 5, domain = [1]
N, node = 6
N, node = 7
E, nodes = (5, 6)
E, nodes = (3, 6)
E, nodes = (6, 7)
M, node = 5, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
M, node = 6, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
X byproduct, node = 7, domain = [6]
Z byproduct, node = 7, domain = [5]
Z byproduct, node = 3, domain = [5]
>>> pattern
Pattern(input_nodes=[0, 1], cmds=[N(2), N(3), E((0, 2)), E((2, 3)), M(0, angle=-0.08131311068764493), M(2), X(3, {2}), Z(3, {0}), N(4), N(5), E((1, 4)), E((4, 5)), M(1, angle=-0.2242107876075538), M(4), X(5, {4}), Z(5, {1}), N(6), N(7), E((5, 6)), E((3, 6)), E((6, 7)), M(5), M(6), X(7, {6}), Z(7, {5}), Z(3, {5})], output_nodes=[3, 7])

.. figure:: ./../imgs/pattern_visualization_2.png
:scale: 60 %
Expand All @@ -190,30 +144,8 @@ These can be called with :meth:`~graphix.pattern.Pattern.standardize` and :meth:

>>> pattern.standardize()
>>> pattern.shift_signals()
>>> pattern.print_pattern()
N, node = 2
N, node = 3
N, node = 4
N, node = 5
N, node = 6
N, node = 7
E, nodes = (0, 2)
E, nodes = (2, 3)
E, nodes = (1, 4)
E, nodes = (4, 5)
E, nodes = (5, 6)
E, nodes = (6, 3)
E, nodes = (6, 7)
M, node = 0, plane = XY, angle(pi) = -0.2975038024267561, s-domain = [], t_domain = []
M, node = 2, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
M, node = 1, plane = XY, angle(pi) = -0.14788446865973076, s-domain = [], t_domain = []
M, node = 4, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
M, node = 5, plane = XY, angle(pi) = 0, s-domain = [4], t_domain = []
M, node = 6, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
X byproduct, node = 3, domain = [2]
X byproduct, node = 7, domain = [2, 4, 6]
Z byproduct, node = 3, domain = [0, 1, 5]
Z byproduct, node = 7, domain = [1, 5]
>>> pattern
Pattern(input_nodes=[0, 1], cmds=[N(2), N(3), N(4), N(5), N(6), N(7), E((0, 2)), E((2, 3)), E((1, 4)), E((4, 5)), E((5, 6)), E((3, 6)), E((6, 7)), M(0, angle=-0.22152331776994327), M(2), M(1, angle=-0.18577010991028864), M(4), M(5, s_domain={4}), M(6), Z(3, {0, 1, 5}), Z(7, {1, 5}), X(3, {2}), X(7, {2, 4, 6})], output_nodes=[3, 7])

.. figure:: ./../imgs/pattern_visualization_3.png
:scale: 60 %
Expand Down Expand Up @@ -250,18 +182,8 @@ We can call this in a line by calling :meth:`~graphix.pattern.Pattern.perform_pa
We get an updated measurement pattern without Pauli measurements as follows:

>>> pattern.perform_pauli_measurements()
>>> pattern.print_pattern()
N, node = 3
N, node = 7
E, nodes = (0, 3)
E, nodes = (1, 3)
E, nodes = (1, 7)
M, node = 0, plane = XY, angle(pi) = -0.2975038024267561, s-domain = [], t_domain = [], Clifford index = 6
M, node = 1, plane = XY, angle(pi) = -0.14788446865973076, s-domain = [], t_domain = [], Clifford index = 6
X byproduct, node = 3, domain = [2]
X byproduct, node = 7, domain = [2, 4, 6]
Z byproduct, node = 3, domain = [0, 1, 5]
Z byproduct, node = 7, domain = [1, 5]
>>> pattern
Pattern(input_nodes=[0, 1], cmds=[N(3), N(7), E((0, 3)), E((1, 3)), E((1, 7)), M(0, Plane.YZ, 0.2907266109187514), M(1, Plane.YZ, 0.01258854060311348), C(3, Clifford.I), C(7, Clifford.I), Z(3, {0, 1, 5}), Z(7, {1, 5}), X(3, {2}), X(7, {2, 4, 6})], output_nodes=[3, 7])


Notice that all measurements with angle=0 (Pauli X measurements) disappeared - this means that a part of quantum computation was `classically` (and efficiently) preprocessed such that we only need much smaller quantum resource.
Expand Down Expand Up @@ -290,18 +212,8 @@ We exploit this fact to minimize the `space` of the pattern, which is crucial fo
We can simply call :meth:`~graphix.pattern.Pattern.minimize_space()` to reduce the `space`:

>>> pattern.minimize_space()
>>> pattern.print_pattern(lim=20)
N, node = 3
E, nodes = (0, 3)
M, node = 0, plane = XY, angle(pi) = -0.2975038024267561, s-domain = [], t_domain = [], Clifford index = 6
E, nodes = (1, 3)
N, node = 7
E, nodes = (1, 7)
M, node = 1, plane = XY, angle(pi) = -0.14788446865973076, s-domain = [], t_domain = [], Clifford index = 6
X byproduct, node = 3, domain = [2]
X byproduct, node = 7, domain = [2, 4, 6]
Z byproduct, node = 3, domain = [0, 1, 5]
Z byproduct, node = 7, domain = [1, 5]
>>> pattern
Pattern(input_nodes=[0, 1], cmds=[N(3), E((0, 3)), M(0, Plane.YZ, 0.11120090987081546), E((1, 3)), N(7), E((1, 7)), M(1, Plane.YZ, 0.230565199664617), C(3, Clifford.I), C(7, Clifford.I), Z(3, {0, 1, 5}), Z(7, {1, 5}), X(3, {2}), X(7, {2, 4, 6})], output_nodes=[3, 7])


With the original measurement pattern, the simulation should have proceeded as follows, with maximum of four qubits on the memory.
Expand Down
12 changes: 9 additions & 3 deletions examples/deutsch_jozsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
# Now let us transpile into MBQC measurement pattern and inspect the pattern sequence and graph state

pattern = circuit.transpile().pattern
pattern.print_pattern(lim=15)
print(pattern.to_ascii(left_to_right=True, limit=15))
pattern.draw_graph(flow_from_pattern=False)

# %%
Expand All @@ -68,13 +68,19 @@

pattern.standardize()
pattern.shift_signals()
pattern.print_pattern(lim=15)
print(pattern.to_ascii(left_to_right=True, limit=15))

# %%
# Now we preprocess all Pauli measurements

pattern.perform_pauli_measurements()
pattern.print_pattern(lim=16, target=[CommandKind.N, CommandKind.M, CommandKind.C])
print(
pattern.to_ascii(
left_to_right=True,
limit=16,
target=[CommandKind.N, CommandKind.M, CommandKind.C],
)
)
pattern.draw_graph(flow_from_pattern=True)

# %%
Expand Down
2 changes: 1 addition & 1 deletion examples/rotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
# This returns :class:`~graphix.pattern.Pattern` object containing measurement pattern:

pattern = circuit.transpile().pattern
pattern.print_pattern(lim=10)
print(pattern.to_ascii(left_to_right=True, limit=10))

# %%
# We can plot the graph state to run the above pattern.
Expand Down
31 changes: 16 additions & 15 deletions graphix/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# Ruff suggests to move this import to a type-checking block, but dataclass requires it here
from graphix.parameter import ExpressionOrFloat # noqa: TC001
from graphix.pauli import Pauli
from graphix.pretty_print import DataclassPrettyPrintMixin
from graphix.states import BasicStates, State

Node = int
Expand All @@ -44,17 +45,17 @@ def __init_subclass__(cls) -> None:
utils.check_kind(cls, {"CommandKind": CommandKind, "Clifford": Clifford})


@dataclasses.dataclass
class N(_KindChecker):
@dataclasses.dataclass(repr=False)
class N(_KindChecker, DataclassPrettyPrintMixin):
"""Preparation command."""

node: Node
state: State = dataclasses.field(default_factory=lambda: BasicStates.PLUS)
kind: ClassVar[Literal[CommandKind.N]] = dataclasses.field(default=CommandKind.N, init=False)


@dataclasses.dataclass
class M(_KindChecker):
@dataclasses.dataclass(repr=False)
class M(_KindChecker, DataclassPrettyPrintMixin):
"""Measurement command. By default the plane is set to 'XY', the angle to 0, empty domains and identity vop."""

node: Node
Expand All @@ -80,51 +81,51 @@ def clifford(self, clifford_gate: Clifford) -> M:
)


@dataclasses.dataclass
class E(_KindChecker):
@dataclasses.dataclass(repr=False)
class E(_KindChecker, DataclassPrettyPrintMixin):
"""Entanglement command."""

nodes: tuple[Node, Node]
kind: ClassVar[Literal[CommandKind.E]] = dataclasses.field(default=CommandKind.E, init=False)


@dataclasses.dataclass
class C(_KindChecker):
@dataclasses.dataclass(repr=False)
class C(_KindChecker, DataclassPrettyPrintMixin):
"""Clifford command."""

node: Node
clifford: Clifford
kind: ClassVar[Literal[CommandKind.C]] = dataclasses.field(default=CommandKind.C, init=False)


@dataclasses.dataclass
class X(_KindChecker):
@dataclasses.dataclass(repr=False)
class X(_KindChecker, DataclassPrettyPrintMixin):
"""X correction command."""

node: Node
domain: set[Node] = dataclasses.field(default_factory=set)
kind: ClassVar[Literal[CommandKind.X]] = dataclasses.field(default=CommandKind.X, init=False)


@dataclasses.dataclass
class Z(_KindChecker):
@dataclasses.dataclass(repr=False)
class Z(_KindChecker, DataclassPrettyPrintMixin):
"""Z correction command."""

node: Node
domain: set[Node] = dataclasses.field(default_factory=set)
kind: ClassVar[Literal[CommandKind.Z]] = dataclasses.field(default=CommandKind.Z, init=False)


@dataclasses.dataclass
class S(_KindChecker):
@dataclasses.dataclass(repr=False)
class S(_KindChecker, DataclassPrettyPrintMixin):
"""S command."""

node: Node
domain: set[Node] = dataclasses.field(default_factory=set)
kind: ClassVar[Literal[CommandKind.S]] = dataclasses.field(default=CommandKind.S, init=False)


@dataclasses.dataclass
@dataclasses.dataclass(repr=False)
class T(_KindChecker):
"""T command."""

Expand Down
11 changes: 6 additions & 5 deletions graphix/fundamentals.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from graphix.ops import Ops
from graphix.parameter import cos_sin
from graphix.pretty_print import EnumPrettyPrintMixin

if TYPE_CHECKING:
import numpy as np
Expand All @@ -28,7 +29,7 @@
SupportsComplexCtor = Union[SupportsComplex, SupportsFloat, SupportsIndex, complex]


class Sign(Enum):
class Sign(EnumPrettyPrintMixin, Enum):
"""Sign, plus or minus."""

PLUS = 1
Expand Down Expand Up @@ -111,7 +112,7 @@ def __complex__(self) -> complex:
return complex(self.value)


class ComplexUnit(Enum):
class ComplexUnit(EnumPrettyPrintMixin, Enum):
"""
Complex unit: 1, -1, j, -j.

Expand Down Expand Up @@ -165,7 +166,7 @@ def __complex__(self) -> complex:
return ret

def __str__(self) -> str:
"""Return a string representation of the unit."""
"""Return a human-readable representation of the unit."""
result = "1j" if self.is_imag else "1"
if self.sign == Sign.MINUS:
result = "-" + result
Expand Down Expand Up @@ -213,7 +214,7 @@ def matrix(self) -> npt.NDArray[np.complex128]:
typing_extensions.assert_never(self)


class Axis(Enum):
class Axis(EnumPrettyPrintMixin, Enum):
"""Axis: `X`, `Y` or `Z`."""

X = enum.auto()
Expand All @@ -232,7 +233,7 @@ def matrix(self) -> npt.NDArray[np.complex128]:
typing_extensions.assert_never(self)


class Plane(Enum):
class Plane(EnumPrettyPrintMixin, Enum):
# TODO: Refactor using match
"""Plane: `XY`, `YZ` or `XZ`."""

Expand Down
Loading