Skip to content

Commit ed7de34

Browse files
Pretty-print circuits and patterns (#277)
* Pretty-print circuits and patterns - Add dedicated pretty-printing functions for patterns. - Improve str() and repr() for circuits, patterns, and core data types (Sign, Axis, Plane, Command, Instruction). This is a focused extract from PR #245, removing any external dependencies: - Render patterns as ASCII, Unicode, or LaTeX snippets (PDF/image output removed). - Provide evaluable representations for patterns, circuits, and other core types. Per Shinichi’s suggestion (#245 (comment)), move most code from `pattern.py` into a new module `pretty_print.py`. Per EarlMilktea’s suggestion (#245 (comment)), add type checking to `pretty_print.py` and its tests (`test_pretty_print.py`). * Fix use of `print_pattern` in examples * Add missing import * Update documentation * Update tutorial * Restore deprecated print_pattern and add arguments to converters * Add to CHANGELOG * Use `Iterable` in `Circuit.__init__` Suggested by EarlMilktea: #277 (comment) * Remove useless list collection before calling `join` Suggested by EarlMilktea: #277 (comment) * Use `Iterable` in `Pattern.__init__` and `Pattern.extend` Suggested by EarlMilktea: #277 (comment) * Use `enum.auto` instead of strings Suggested by EarlMilktea: #277 (comment) * Use the simpler `to_ascii` method in examples * Mixin for pretty-printing dataclasses * Mixin for pretty-printing Enum * Check instance of Enum in EnumMixin Suggested by EarlMilktea: #277 (comment) * Use `is not None` with `Iterable | None` EarlMilktea reported that Iterable does not implement __bool__. #277 (comment) * Annotate `self` with `DataclassInstance` Suggested by EarlMilktea: #277 (comment) * Fix ruff * Include `output_nodes` in `repr` * ruff fix with new linters * Fix swap and better code coverage * Use raw f-string Suggested by EarlMilktea: #277 (comment) * Update tutorial with `output_nodes` * Use `is not None` for `target` in `pattern_to_str` Reported by EarlMilktea: #277 (comment) * Add result type for `Circuit.__init__` Suggested by EarlMilktea: #277 (comment) * Rename `DataclassPrettyPrintMixin` and `EnumPrettyPrintMixin` Suggested by EarlMilktea: #277 (comment)
1 parent f657712 commit ed7de34

File tree

15 files changed

+683
-256
lines changed

15 files changed

+683
-256
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Methods for pretty-printing `Pattern`: `to_ascii`, `to_unicode`,
13+
`to_latex`.
14+
1215
### Fixed
1316

17+
- The result of `repr()` for `Pattern`, `Circuit`, `Command`,
18+
`Instruction`, `Plane`, `Axis` and `Sign` is now a valid Python
19+
expression and is more readable.
20+
1421
### Changed
1522

23+
- The method `Pattern.print_pattern` is now deprecated.
24+
1625
## [0.3.1] - 2025-04-21
1726

1827
### Added

docs/source/modifier.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ Pattern Manipulation
3636

3737
.. automethod:: perform_pauli_measurements
3838

39-
.. automethod:: print_pattern
39+
.. automethod:: to_ascii
40+
41+
.. automethod:: to_unicode
42+
43+
.. automethod:: to_latex
4044

4145
.. automethod:: standardize
4246

docs/source/tutorial.rst

Lines changed: 20 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,14 @@ For any gate network, we can use the :class:`~graphix.transpiler.Circuit` class
3030
the :class:`~graphix.pattern.Pattern` object contains the sequence of commands according to the measurement calculus framework [#Danos2007]_.
3131
Let us print the pattern (command sequence) that we generated,
3232

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

3936
The command sequence represents the following sequence:
4037

41-
* starting with an input qubit :math:`|\psi_{in}\rangle_0`, we first prepare an ancilla qubit :math:`|+\rangle_1` with ['N', 1] command
42-
* We then apply CZ-gate by ['E', (0, 1)] command to create entanglement.
43-
* We measure the qubit 0 in Pauli X basis, by ['M'] command.
38+
* starting with an input qubit :math:`|\psi_{in}\rangle_0`, we first prepare an ancilla qubit :math:`|+\rangle_1` with N(1) command
39+
* We then apply CZ-gate by E((0, 1)) command to create entanglement.
40+
* We measure the qubit 0 in Pauli X basis, by M(0) command.
4441
* 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.
4542
* 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.
4643

@@ -86,19 +83,9 @@ As a more complex example than above, we show measurement patterns and graph sta
8683
| |
8784
| control: input=0, output=0; target: input=1, output=3 |
8885
+------------------------------------------------------------------------------+
89-
| >>> cnot_pattern.print_pattern() |
90-
| N, node = 0 |
91-
| N, node = 1 |
92-
| N, node = 2 |
93-
| N, node = 3 |
94-
| E, nodes = (1, 2) |
95-
| E, nodes = (0, 2) |
96-
| E, nodes = (2, 3) |
97-
| M, node = 1, plane = XY, angle(pi) = 0, s-domain = [], t_domain = [] |
98-
| M, node = 2, plane = XY, angle(pi) = 0, s-domain = [], t_domain = [] |
99-
| X byproduct, node = 3, domain = [2] |
100-
| Z byproduct, node = 3, domain = [1] |
101-
| Z byproduct, node = 0, domain = [1] |
86+
| >>> cnot_pattern |
87+
| Pattern(cmds=[N(0), N(1), N(2), N(3), E((1, 2)), E((0, 2)), E((2, 3)), M(1), |
88+
| M(2), X(3, {2}), Z(3, {1}), Z(0, {1})], output_nodes=[0, 3]) |
10289
+------------------------------------------------------------------------------+
10390
| **general rotation (an example with Euler angles 0.2pi, 0.15pi and 0.1 pi)** |
10491
+------------------------------------------------------------------------------+
@@ -108,18 +95,10 @@ As a more complex example than above, we show measurement patterns and graph sta
10895
| |
10996
| input = 0, output = 4 |
11097
+------------------------------------------------------------------------------+
111-
|>>> euler_rot_pattern.print_pattern() |
112-
| N, node = 0 |
113-
| N, node = 1 |
114-
| N, node = 2 |
115-
| N, node = 3 |
116-
| N, node = 4 |
117-
| M, node = 0, plane = XY, angle(pi) = -0.2, s-domain = [], t_domain = [] |
118-
| M, node = 1, plane = XY, angle(pi) = -0.15, s-domain = [0], t_domain = [] |
119-
| M, node = 2, plane = XY, angle(pi) = -0.1, s-domain = [1], t_domain = [] |
120-
| M, node = 3, plane = XY, angle(pi) = 0, s-domain = [], t_domain = [] |
121-
| Z byproduct, node = 4, domain = [0,2] |
122-
| X byproduct, node = 4, domain = [1,3] |
98+
|>>> euler_rot_pattern |
99+
| Pattern(cmds=[N(0), N(1), N(2), N(3), N(4), M(0, angle=-0.2), |
100+
| M(1, angle=-0.15, s_domain={0}), M(2, angle=-0.1, s_domain={1}), |
101+
| M(3), Z(4, domain={0, 2}), X(4, domain={1, 3})], output_nodes=[4]) |
123102
+------------------------------------------------------------------------------+
124103

125104

@@ -144,33 +123,8 @@ As an example, let us prepare a pattern to rotate two qubits in :math:`|+\rangle
144123
145124
This produces a rather long and complicated command sequence.
146125

147-
>>> pattern.print_pattern() # show the command sequence (pattern)
148-
N, node = 2
149-
N, node = 3
150-
E, nodes = (0, 2)
151-
E, nodes = (2, 3)
152-
M, node = 0, plane = XY, angle(pi) = -0.2975038024267561, s-domain = [], t_domain = []
153-
M, node = 2, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
154-
X byproduct, node = 3, domain = [2]
155-
Z byproduct, node = 3, domain = [0]
156-
N, node = 4
157-
N, node = 5
158-
E, nodes = (1, 4)
159-
E, nodes = (4, 5)
160-
M, node = 1, plane = XY, angle(pi) = -0.14788446865973076, s-domain = [], t_domain = []
161-
M, node = 4, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
162-
X byproduct, node = 5, domain = [4]
163-
Z byproduct, node = 5, domain = [1]
164-
N, node = 6
165-
N, node = 7
166-
E, nodes = (5, 6)
167-
E, nodes = (3, 6)
168-
E, nodes = (6, 7)
169-
M, node = 5, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
170-
M, node = 6, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
171-
X byproduct, node = 7, domain = [6]
172-
Z byproduct, node = 7, domain = [5]
173-
Z byproduct, node = 3, domain = [5]
126+
>>> pattern
127+
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])
174128

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

191145
>>> pattern.standardize()
192146
>>> pattern.shift_signals()
193-
>>> pattern.print_pattern()
194-
N, node = 2
195-
N, node = 3
196-
N, node = 4
197-
N, node = 5
198-
N, node = 6
199-
N, node = 7
200-
E, nodes = (0, 2)
201-
E, nodes = (2, 3)
202-
E, nodes = (1, 4)
203-
E, nodes = (4, 5)
204-
E, nodes = (5, 6)
205-
E, nodes = (6, 3)
206-
E, nodes = (6, 7)
207-
M, node = 0, plane = XY, angle(pi) = -0.2975038024267561, s-domain = [], t_domain = []
208-
M, node = 2, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
209-
M, node = 1, plane = XY, angle(pi) = -0.14788446865973076, s-domain = [], t_domain = []
210-
M, node = 4, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
211-
M, node = 5, plane = XY, angle(pi) = 0, s-domain = [4], t_domain = []
212-
M, node = 6, plane = XY, angle(pi) = 0, s-domain = [], t_domain = []
213-
X byproduct, node = 3, domain = [2]
214-
X byproduct, node = 7, domain = [2, 4, 6]
215-
Z byproduct, node = 3, domain = [0, 1, 5]
216-
Z byproduct, node = 7, domain = [1, 5]
147+
>>> pattern
148+
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])
217149

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

252184
>>> pattern.perform_pauli_measurements()
253-
>>> pattern.print_pattern()
254-
N, node = 3
255-
N, node = 7
256-
E, nodes = (0, 3)
257-
E, nodes = (1, 3)
258-
E, nodes = (1, 7)
259-
M, node = 0, plane = XY, angle(pi) = -0.2975038024267561, s-domain = [], t_domain = [], Clifford index = 6
260-
M, node = 1, plane = XY, angle(pi) = -0.14788446865973076, s-domain = [], t_domain = [], Clifford index = 6
261-
X byproduct, node = 3, domain = [2]
262-
X byproduct, node = 7, domain = [2, 4, 6]
263-
Z byproduct, node = 3, domain = [0, 1, 5]
264-
Z byproduct, node = 7, domain = [1, 5]
185+
>>> pattern
186+
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])
265187

266188

267189
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.
@@ -290,18 +212,8 @@ We exploit this fact to minimize the `space` of the pattern, which is crucial fo
290212
We can simply call :meth:`~graphix.pattern.Pattern.minimize_space()` to reduce the `space`:
291213

292214
>>> pattern.minimize_space()
293-
>>> pattern.print_pattern(lim=20)
294-
N, node = 3
295-
E, nodes = (0, 3)
296-
M, node = 0, plane = XY, angle(pi) = -0.2975038024267561, s-domain = [], t_domain = [], Clifford index = 6
297-
E, nodes = (1, 3)
298-
N, node = 7
299-
E, nodes = (1, 7)
300-
M, node = 1, plane = XY, angle(pi) = -0.14788446865973076, s-domain = [], t_domain = [], Clifford index = 6
301-
X byproduct, node = 3, domain = [2]
302-
X byproduct, node = 7, domain = [2, 4, 6]
303-
Z byproduct, node = 3, domain = [0, 1, 5]
304-
Z byproduct, node = 7, domain = [1, 5]
215+
>>> pattern
216+
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])
305217

306218

307219
With the original measurement pattern, the simulation should have proceeded as follows, with maximum of four qubits on the memory.

examples/deutsch_jozsa.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
# Now let us transpile into MBQC measurement pattern and inspect the pattern sequence and graph state
5959

6060
pattern = circuit.transpile().pattern
61-
pattern.print_pattern(lim=15)
61+
print(pattern.to_ascii(left_to_right=True, limit=15))
6262
pattern.draw_graph(flow_from_pattern=False)
6363

6464
# %%
@@ -68,13 +68,19 @@
6868

6969
pattern.standardize()
7070
pattern.shift_signals()
71-
pattern.print_pattern(lim=15)
71+
print(pattern.to_ascii(left_to_right=True, limit=15))
7272

7373
# %%
7474
# Now we preprocess all Pauli measurements
7575

7676
pattern.perform_pauli_measurements()
77-
pattern.print_pattern(lim=16, target=[CommandKind.N, CommandKind.M, CommandKind.C])
77+
print(
78+
pattern.to_ascii(
79+
left_to_right=True,
80+
limit=16,
81+
target=[CommandKind.N, CommandKind.M, CommandKind.C],
82+
)
83+
)
7884
pattern.draw_graph(flow_from_pattern=True)
7985

8086
# %%

examples/rotation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
# This returns :class:`~graphix.pattern.Pattern` object containing measurement pattern:
4747

4848
pattern = circuit.transpile().pattern
49-
pattern.print_pattern(lim=10)
49+
print(pattern.to_ascii(left_to_right=True, limit=10))
5050

5151
# %%
5252
# We can plot the graph state to run the above pattern.

graphix/command.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# Ruff suggests to move this import to a type-checking block, but dataclass requires it here
1919
from graphix.parameter import ExpressionOrFloat # noqa: TC001
2020
from graphix.pauli import Pauli
21+
from graphix.pretty_print import DataclassPrettyPrintMixin
2122
from graphix.states import BasicStates, State
2223

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

4647

47-
@dataclasses.dataclass
48-
class N(_KindChecker):
48+
@dataclasses.dataclass(repr=False)
49+
class N(_KindChecker, DataclassPrettyPrintMixin):
4950
"""Preparation command."""
5051

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

5556

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

6061
node: Node
@@ -80,51 +81,51 @@ def clifford(self, clifford_gate: Clifford) -> M:
8081
)
8182

8283

83-
@dataclasses.dataclass
84-
class E(_KindChecker):
84+
@dataclasses.dataclass(repr=False)
85+
class E(_KindChecker, DataclassPrettyPrintMixin):
8586
"""Entanglement command."""
8687

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

9091

91-
@dataclasses.dataclass
92-
class C(_KindChecker):
92+
@dataclasses.dataclass(repr=False)
93+
class C(_KindChecker, DataclassPrettyPrintMixin):
9394
"""Clifford command."""
9495

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

99100

100-
@dataclasses.dataclass
101-
class X(_KindChecker):
101+
@dataclasses.dataclass(repr=False)
102+
class X(_KindChecker, DataclassPrettyPrintMixin):
102103
"""X correction command."""
103104

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

108109

109-
@dataclasses.dataclass
110-
class Z(_KindChecker):
110+
@dataclasses.dataclass(repr=False)
111+
class Z(_KindChecker, DataclassPrettyPrintMixin):
111112
"""Z correction command."""
112113

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

117118

118-
@dataclasses.dataclass
119-
class S(_KindChecker):
119+
@dataclasses.dataclass(repr=False)
120+
class S(_KindChecker, DataclassPrettyPrintMixin):
120121
"""S command."""
121122

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

126127

127-
@dataclasses.dataclass
128+
@dataclasses.dataclass(repr=False)
128129
class T(_KindChecker):
129130
"""T command."""
130131

graphix/fundamentals.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from graphix.ops import Ops
1414
from graphix.parameter import cos_sin
15+
from graphix.pretty_print import EnumPrettyPrintMixin
1516

1617
if TYPE_CHECKING:
1718
import numpy as np
@@ -28,7 +29,7 @@
2829
SupportsComplexCtor = Union[SupportsComplex, SupportsFloat, SupportsIndex, complex]
2930

3031

31-
class Sign(Enum):
32+
class Sign(EnumPrettyPrintMixin, Enum):
3233
"""Sign, plus or minus."""
3334

3435
PLUS = 1
@@ -111,7 +112,7 @@ def __complex__(self) -> complex:
111112
return complex(self.value)
112113

113114

114-
class ComplexUnit(Enum):
115+
class ComplexUnit(EnumPrettyPrintMixin, Enum):
115116
"""
116117
Complex unit: 1, -1, j, -j.
117118
@@ -165,7 +166,7 @@ def __complex__(self) -> complex:
165166
return ret
166167

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

215216

216-
class Axis(Enum):
217+
class Axis(EnumPrettyPrintMixin, Enum):
217218
"""Axis: `X`, `Y` or `Z`."""
218219

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

234235

235-
class Plane(Enum):
236+
class Plane(EnumPrettyPrintMixin, Enum):
236237
# TODO: Refactor using match
237238
"""Plane: `XY`, `YZ` or `XZ`."""
238239

0 commit comments

Comments
 (0)