Skip to content

Commit 23d998e

Browse files
dwierichsAntonNI8isaacdevlugt
authored
Add custom drawing for TemporaryAND elbows (#8017)
**Context:** We added elbow gates `qml.TemporaryAND`, which are drawn like any generic multi-qubit gate. **Description of the Change:** Add custom drawing. Also add the control values of `TemporaryAND` to its `repr`. Open questions: - [x] Should we remove zeroed wires more prominently (more complicated to implement)? i.e., do we prefer ```python 0: ─╭●──┤ <Z> 1: ─├●──┤ 2: ─╰───┤ a: ──●╮─┤ b: ──●┤─┤ c: ───╯─┤ ``` or ```python 0: ─╭●──┤ <Z> 1: ─├●──┤ 2: ╰───┤ a: ──●╮─┤ b: ──●┤─┤ c: ───╯ ┤ ``` ? **edit:** There seems to be agreement that not skipping wires is fine. - [x] Should the control nodes of left and right elbows align, at the expense of slightly larger drawing depth? I.e., do we prefer ```python 0: ─╭●──┤ <Z> 1: ─├●──┤ 2: ─╰───┤ a: ──●╮─┤ b: ──●┤─┤ c: ───╯─┤ ``` or ```python 0: ─╭●─┤ <Z> 1: ─├●─┤ 2: ─╰──┤ a: ─●╮─┤ b: ─●┤─┤ c: ──╯─┤ ``` ? There seems to be agreement that aligning the control nodes is nicer. **Benefits:** much nicer and easier to debug circuit diagrams. Example: ```python import pennylane as qml from pennylane.templates.subroutines.select import _select_decomp_partial_unary from functools import partial ops = [qml.X(0), qml.Y(1), qml.SWAP([0, 1]), qml.RX(0.7, 2), qml.IsingZZ(0.4, [0, 2]), qml. RX(0.7, 1)] control = ["c0", "c1", "c2"] work = ["aux0", "aux1"] @partial(qml.draw, wire_order=["c0", "c1", "aux0", "c2", "aux1"] + [0, 1, 2]) @qml.qnode(qml.device("default.qubit")) def node(): _select_decomp_partial_unary(ops, control, work) return qml.expval(qml.Z(0)) print(node()) ``` Before: ```python c0: ─╭TemporaryAND──X────────────────────────────────────╭●──X────────────────────────────── ··· c1: ─├TemporaryAND───────────────────────────────────────│────────────────────────────────── ··· aux0: ─╰TemporaryAND─╭TemporaryAND────╭●────╭TemporaryAND†─╰X─╭TemporaryAND───────╭●────────── ··· c2: ───────────────├TemporaryAND────│─────├TemporaryAND†────├TemporaryAND───────│─────────── ··· aux1: ───────────────╰TemporaryAND─╭●─╰X─╭●─╰TemporaryAND†────╰TemporaryAND─╭●────╰X─╭●─────── ··· 0: ─────────────────────────────╰X────│──────────────────────────────────├SWAP────│──────── ··· 1: ───────────────────────────────────╰Y─────────────────────────────────╰SWAP────│──────── ··· 2: ───────────────────────────────────────────────────────────────────────────────╰RX(0.70) ··· c0: ··· ────────────────╭TemporaryAND†─╭TemporaryAND────────────────╭●────────── ··· c1: ··· ────────────────├TemporaryAND†─│────────────────────────────│─────────── ··· aux0: ··· ─╭TemporaryAND†─╰TemporaryAND†─│────────────────────────────│─────────── ··· c2: ··· ─├TemporaryAND†────────────────├TemporaryAND────────────────│─────────── ··· aux1: ··· ─╰TemporaryAND†────────────────╰TemporaryAND─╭●─────────────╰X─╭●─────── ··· 0: ··· ─────────────────────────────────────────────├IsingZZ(0.40)────│──────── ··· 1: ··· ─────────────────────────────────────────────│─────────────────╰RX(0.70) ··· 2: ··· ─────────────────────────────────────────────╰IsingZZ(0.40)───────────── ··· c0: ··· ─╭TemporaryAND†─┤ c1: ··· ─│──────────────┤ aux0: ··· ─│──────────────┤ c2: ··· ─├TemporaryAND†─┤ aux1: ··· ─╰TemporaryAND†─┤ 0: ··· ────────────────┤ <Z> 1: ··· ────────────────┤ 2: ··· ────────────────┤ ``` After: ```python c0: ─╭○──X──────────────╭●──X─────────────────────────○╮─╭●────────────────╭●────────────●╮─┤ c1: ─├○─────────────────│─────────────────────────────●┤─│─────────────────│──────────────│─┤ aux0: ─╰──╭●────╭●─────●╮─╰X─╭●───────╭●────────────●╮───╯─│─────────────────│──────────────│─┤ c2: ────├○────│──────●┤────├○───────│─────────────●┤─────├○────────────────│─────────────●┤─┤ aux1: ────╰──╭●─╰X─╭●───╯────╰──╭●────╰X─╭●──────────╯─────╰──╭●─────────────╰X─╭●──────────╯─┤ 0: ───────╰X────│────────────├SWAP────│────────────────────├IsingZZ(0.40)────│─────────────┤ <Z> 1: ─────────────╰Y───────────╰SWAP────│────────────────────│─────────────────╰RX(0.70)─────┤ 2: ───────────────────────────────────╰RX(0.70)────────────╰IsingZZ(0.40)──────────────────┤ ``` **Possible Drawbacks:** N/A **Related GitHub Issues:** --------- Co-authored-by: ANT0N <[email protected]> Co-authored-by: Isaac De Vlugt <[email protected]>
1 parent aab8255 commit 23d998e

File tree

5 files changed

+133
-6
lines changed

5 files changed

+133
-6
lines changed

doc/releases/changelog-dev.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,30 @@
6464

6565
<h3>Improvements 🛠</h3>
6666

67+
* The printing and drawing of :class:`~.TemporaryAND`, also known as ``qml.Elbow``, and its adjoint
68+
have been improved to be more legible and consistent with how it's depicted in circuits in the literature.
69+
[(#8017)](https://github.com/PennyLaneAI/pennylane/pull/8017)
70+
71+
```python
72+
import pennylane as qml
73+
74+
@qml.draw
75+
@qml.qnode(qml.device("lightning.qubit", wires=4))
76+
def node():
77+
qml.TemporaryAND([0, 1, 2], control_values=[1, 0])
78+
qml.CNOT([2, 3])
79+
qml.adjoint(qml.TemporaryAND([0, 1, 2], control_values=[1, 0]))
80+
return qml.expval(qml.Z(3))
81+
```
82+
83+
```pycon
84+
print(node())
85+
0: ─╭●─────●╮─┤
86+
1: ─├○─────○┤─┤
87+
2: ─╰──╭●───╯─┤
88+
3: ────╰X─────┤ <Z>
89+
```
90+
6791
* Several templates now have decompositions that can be accessed within the graph-based
6892
decomposition system (:func:`~.decomposition.enable_graph`), allowing workflows
6993
that include these templates to be decomposed in a resource-efficient and performant

pennylane/drawer/_add_obj.py

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@
4242
VarianceMP,
4343
)
4444
from pennylane.operation import Operator
45-
from pennylane.ops import Conditional, Controlled, GlobalPhase, Identity
45+
from pennylane.ops import Adjoint, Conditional, Controlled, GlobalPhase, Identity
4646
from pennylane.tape import QuantumScript
47+
from pennylane.templates.subroutines import TemporaryAND
4748

4849

4950
def _add_cond_grouping_symbols(op, layer_str, config):
@@ -77,7 +78,7 @@ def _add_cond_grouping_symbols(op, layer_str, config):
7778
return layer_str
7879

7980

80-
def _add_grouping_symbols(op_wires, layer_str, config):
81+
def _add_grouping_symbols(op_wires, layer_str, config, closing=False):
8182
"""Adds symbols indicating the extent of a given sequence of wires.
8283
Does nothing if the sequence has length 0 or 1."""
8384

@@ -87,11 +88,18 @@ def _add_grouping_symbols(op_wires, layer_str, config):
8788
mapped_wires = [config.wire_map[w] for w in op_wires]
8889
min_w, max_w = min(mapped_wires), max(mapped_wires)
8990

90-
layer_str[min_w] = "╭"
91-
layer_str[max_w] = "╰"
91+
if closing:
92+
layer_str[min_w] += "╮"
93+
layer_str[max_w] += "╯"
9294

93-
for w in range(min_w + 1, max_w):
94-
layer_str[w] = "├" if w in mapped_wires else "│"
95+
for w in range(min_w + 1, max_w):
96+
layer_str[w] += "┤" if w in mapped_wires else "│"
97+
else:
98+
layer_str[min_w] = "╭"
99+
layer_str[max_w] = "╰"
100+
101+
for w in range(min_w + 1, max_w):
102+
layer_str[w] = "├" if w in mapped_wires else "│"
95103

96104
return layer_str
97105

@@ -159,6 +167,47 @@ def _add_controlled_global_op(obj, layer_str, config):
159167
return layer_str
160168

161169

170+
def _add_elbow_core(obj, layer_str, config):
171+
cvals = obj.hyperparameters["control_values"]
172+
mapped_wires = [config.wire_map[w] for w in obj.wires]
173+
layer_str[mapped_wires[0]] += "●" if cvals[0] else "○"
174+
layer_str[mapped_wires[1]] += "●" if cvals[1] else "○"
175+
layer_str[mapped_wires[2]] += "─"
176+
return layer_str, mapped_wires
177+
178+
179+
@_add_obj.register
180+
def _add_left_elbow(
181+
obj: TemporaryAND, layer_str, config, tape_cache=None, skip_grouping_symbols=False
182+
):
183+
"""Updates ``layer_str`` with ``op`` operation of type ``TemporaryAND``,
184+
also known as left elbow."""
185+
layer_str = _add_grouping_symbols(obj.wires, layer_str, config)
186+
layer_str, _ = _add_elbow_core(obj, layer_str, config)
187+
return layer_str
188+
189+
190+
def _add_right_elbow(obj: TemporaryAND, layer_str, config):
191+
"""Updates ``layer_str`` with ``op`` operation of type ``Adjoint(TemporaryAND)``,
192+
also known as right elbow."""
193+
layer_str, mapped_wires = _add_elbow_core(obj, layer_str, config)
194+
# Fill with "─" on intermediate wires the elbow does not act on, to shift "|" correctly
195+
for w in range(min(mapped_wires) + 1, max(mapped_wires)):
196+
if w not in mapped_wires:
197+
layer_str[w] += "─"
198+
return _add_grouping_symbols(obj.wires, layer_str, config, closing=True)
199+
200+
201+
@_add_obj.register
202+
def _add_adjoint(obj: Adjoint, layer_str, config, tape_cache=None, skip_grouping_symbols=False):
203+
"""Updates ``layer_str`` with ``op`` operation of type Adjoint. Currently
204+
only differs from ``_add_op`` if the base of the adjoint op is a ``TemporaryAND``,
205+
making the overall object a right elbow."""
206+
if isinstance(obj.base, TemporaryAND):
207+
return _add_right_elbow(obj.base, layer_str, config)
208+
return _add_op(obj, layer_str, config, tape_cache, skip_grouping_symbols)
209+
210+
162211
@_add_obj.register
163212
def _add_op(obj: Operator, layer_str, config, tape_cache=None, skip_grouping_symbols=False):
164213
"""Updates ``layer_str`` with ``op`` operation."""

pennylane/templates/subroutines/temporary_and.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ def circuit():
7474
7575
.. code-block:: pycon
7676
77+
>>> print(qml.draw(circuit)())
78+
0: ──X─╭●─────●╮─┤ ╭Sample
79+
1: ──X─├●─────●┤─┤ ├Sample
80+
2: ────╰──╭●───╯─┤ ├Sample
81+
3: ───────╰X─────┤ ╰Sample
7782
>>> print(circuit())
7883
[1 1 0 1]
7984
"""
@@ -89,6 +94,12 @@ def circuit():
8994

9095
resource_keys = set()
9196

97+
def __repr__(self):
98+
cvals = self.hyperparameters["control_values"]
99+
if all(cvals):
100+
return f"TemporaryAND(wires={self.wires})"
101+
return f"TemporaryAND(wires={self.wires}, control_values={cvals})"
102+
92103
def __init__(self, wires: WiresLike, control_values=(1, 1), id=None):
93104
wires = Wires(wires)
94105
self.hyperparameters["control_values"] = tuple(control_values)

tests/drawer/test_tape_text.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@ def test_add_measurements_cache(self):
294294
(qml.Snapshot(), ["─|Snap|", "─|Snap|", "─|Snap|", "─|Snap|"]),
295295
(qml.Barrier(), ["─||", "─||", "─||", "─||"]),
296296
(qml.S(0) @ qml.T(0), ["─S@T", "─", "─", "─"]),
297+
(qml.TemporaryAND([0, 1, 3]), ["╭●", "├●", "│", "╰─"]),
298+
(qml.TemporaryAND([1, 0, 3], control_values=(0, 1)), ["╭●", "├○", "│", "╰─"]),
297299
],
298300
)
299301
def test_add_obj(self, op, out):
@@ -675,6 +677,14 @@ def test_setting_max_length(self, ml):
675677
),
676678
"0: ─╭○─┤ \n1: ─├●─┤ \n2: ─├○─┤ \n3: ─├●─┤ \n4: ─╰Y─┤ ",
677679
),
680+
(
681+
qml.TemporaryAND([3, 0, 2], control_values=(1, 0)),
682+
"3: ─╭●─┤ \n0: ─├○─┤ \n2: ─╰──┤ ",
683+
),
684+
(
685+
qml.adjoint(qml.TemporaryAND([3, 0, 2], control_values=(0, 1))),
686+
"3: ──○╮─┤ \n0: ──●┤─┤ \n2: ───╯─┤ ",
687+
),
678688
]
679689

680690

@@ -727,6 +737,31 @@ def test_blocking_multiwire_gate(self):
727737

728738
assert tape_text(_tape, wire_order=[0, 1, 2]) == expected
729739

740+
def test_multiple_elbows(self):
741+
"""Test that multiple elbows are drawn correctly."""
742+
_tape = qml.tape.QuantumScript(
743+
[
744+
qml.TemporaryAND(["a", "b", "c"]),
745+
qml.adjoint(qml.TemporaryAND(["f", "d", "e"])),
746+
qml.adjoint(qml.TemporaryAND(["a", "d", "b"], control_values=(0, 0))),
747+
qml.TemporaryAND(["e", "h", "f"], control_values=(0, 1)),
748+
]
749+
)
750+
expected = (
751+
"a: ─╭●───○╮─┤ \n"
752+
"b: ─├●────┤─┤ \n"
753+
"c: ─╰─────│─┤ \n"
754+
"d: ──●╮──○╯─┤ \n"
755+
"e: ───┤─╭○──┤ \n"
756+
"f: ──●╯─├───┤ \n"
757+
"g: ─────│───┤ \n"
758+
"h: ─────╰●──┤ "
759+
)
760+
out = tape_text(
761+
_tape, wire_order=["a", "b", "c", "d", "e", "f", "g", "h"], show_all_wires=True
762+
)
763+
assert out == expected
764+
730765

731766
tape_matrices = qml.tape.QuantumScript(
732767
ops=[qml.StatePrep([1.0, 0.0, 0.0, 0.0], wires=(0, 1)), qml.QubitUnitary(np.eye(2), wires=0)],

tests/templates/test_subroutines/test_temporary_and.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@
2424
class TestTemporaryAND:
2525
"""Tests specific to the TemporaryAND operation"""
2626

27+
def test_repr(self):
28+
"""Test the repr of TemporaryAND."""
29+
assert repr(qml.TemporaryAND(wires=[0, "a", 2])) == "TemporaryAND(wires=Wires([0, 'a', 2]))"
30+
assert (
31+
repr(qml.TemporaryAND(wires=[0, "a", 2], control_values=(0, 1)))
32+
== "TemporaryAND(wires=Wires([0, 'a', 2]), control_values=(0, 1))"
33+
)
34+
2735
def test_alias(self):
2836
"""Test that Elbow is an alias of TemporaryAND"""
2937
op1 = qml.TemporaryAND(wires=[0, "a", 2], control_values=(0, 0))

0 commit comments

Comments
 (0)