Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
919d437
clean developement of improved qubit allocation drawing
albi3ro Apr 30, 2026
2c22dd3
this version working
albi3ro May 1, 2026
a1b3767
trying a different strategy
albi3ro May 4, 2026
ca4f905
drawable layers at least works now
albi3ro May 6, 2026
c9677be
i might just be making things worse
albi3ro May 6, 2026
83c643a
really convinced Im chasing my tail
albi3ro May 27, 2026
bda29fb
maybe finally working?
albi3ro May 27, 2026
65c264d
starting to clean it up and add docs
albi3ro May 28, 2026
d6af217
Merge branch 'main' into draw-allocation-v3
albi3ro May 28, 2026
3d075c6
mixing with mcms
albi3ro May 28, 2026
a86220d
main
albi3ro May 28, 2026
5d7196b
fix docstrings
albi3ro May 28, 2026
93b2f3b
more test fixes
albi3ro May 28, 2026
a18cbb4
inserting waiting ops differently
albi3ro May 28, 2026
bc714a7
example fix
albi3ro May 29, 2026
1853227
Update pennylane/transforms/decomp_inspector.py
albi3ro May 29, 2026
88590b9
adding tests
albi3ro May 29, 2026
2c4a84a
test fixes
albi3ro May 29, 2026
25f5ea5
Merge branch 'main' into draw-allocation-v3
albi3ro May 29, 2026
421e33e
minor fixes
albi3ro Jun 1, 2026
b63a28b
Apply suggestions from code review
albi3ro Jun 2, 2026
74b5c57
responding to feedback
albi3ro Jun 2, 2026
421d463
Merge branch 'main' into draw-allocation-v3
albi3ro Jun 2, 2026
56bba8e
so many combinations of features
albi3ro Jun 3, 2026
fb7ad54
Assisted by Cursor: add wire_map as argument to cwire_connections
albi3ro Jun 3, 2026
0450a27
TOO MANY COMBINATIONS OF THINGS
albi3ro Jun 3, 2026
ade978f
fixing up barriers
albi3ro Jun 10, 2026
ac92cb6
Merge branch 'main' into draw-allocation-v3
albi3ro Jun 10, 2026
0669771
test fix
albi3ro Jun 10, 2026
033918b
Merge branch 'main' into draw-allocation-v3
albi3ro Jun 10, 2026
99646a3
Test fix
albi3ro Jun 10, 2026
e8541f5
Merge branch 'draw-allocation-v3' of github.com:PennyLaneAI/pennylane…
albi3ro Jun 10, 2026
f5fbe2e
fix final failure hopefully
albi3ro Jun 11, 2026
8173c13
Merge branch 'main' into draw-allocation-v3
albi3ro Jun 15, 2026
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
34 changes: 17 additions & 17 deletions pennylane/allocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,10 @@ def circuit():
return qp.expval(qp.Z(0))

>>> print(qp.draw(circuit)())
0: ──H────────╭●────╭●──────────────────────┤ <Z>
<DynamicWire>: ──Allocate─╰X────╰X───────────Deallocate─┤
<DynamicWire>: ─╭Allocate─╭SWAP─╭Deallocate─────────────┤
<DynamicWire>: ─╰Allocate─╰SWAP─╰Deallocate─────────────┤
0: ──────────────H────╭●─╭●────┤ <Z>
|0>├─╭SWAP──┤ │ │
|0>├─╰SWAP──┤ │ │
|0>├─╰X─╰X──┤
Comment thread
astralcai marked this conversation as resolved.
Outdated

Here, three dynamic wires were allocated in the circuit originally. When PennyLane determines
which concrete values to use for dynamic wires to send to the device for execution, we can see
Expand Down Expand Up @@ -276,10 +276,10 @@ def circuit():
return qp.expval(qp.Z(0))

>>> print(qp.draw(circuit)())
0: ──H───────────────────────┤ <Z>
1: ──H───────────────────────┤
<DynamicWire>: ─╭Allocate──H─╭Deallocate─┤
<DynamicWire>: ─╰Allocate──H─╰Deallocate─┤
0: ───────────H─┤ <Z>
1: ───────────H─┤
|0>├──H──┤
|0>├──H──┤

Equivalenty, ``allocate`` can be used in-line along with :func:`~.deallocate` for manual
Comment thread
albi3ro marked this conversation as resolved.
Outdated
handling:
Expand Down Expand Up @@ -318,12 +318,12 @@ def circuit():
return qp.expval(qp.Z(0))

>>> print(qp.draw(circuit)())
0: ──H─────────────────────╭●───────────────────────╭●─────────────┤ <Z>
<DynamicWire>: ──Allocate──┤↗│ │0⟩──────────────Deallocate────│──────────────┤
<DynamicWire>: ──Allocate───║────────Z─╰X─────────Deallocate────│──────────────┤
<DynamicWire>: ─────────────║────────║──Allocate──┤↗│ │0⟩──────│───Deallocate─┤
<DynamicWire>: ─────────────║────────║──Allocate─────────────Z─╰X──Deallocate─┤
╚════════╝ ╚══════════╝
0: ──────────────────H─────╭●───────────╭●────┤ <Z>
|0>├──┤↗│ │0⟩────────│──────────┤ │
├─────║────────Z─────╰X─────────┤ │
║ ║|0>├──┤↗│ │0⟩────│───┤
║ ║├──────║────────Z─╰X──┤
Comment thread
astralcai marked this conversation as resolved.
Outdated
╚════════╝ ════════╝

The user-level circuit drawing shows four separate allocations and deallocations (two per
loop iteration). However, the circuit that the device receives gets automatically compiled
Expand Down Expand Up @@ -356,9 +356,9 @@ def circuit():
return qp.expval(qp.Z(0))

>>> print(qp.draw(circuit, level="user")())
<DynamicWire>: ──Allocate──H──Deallocate─┤
<DynamicWire>: ──Allocate──X──Deallocate─┤
0: ──────────────────────────┤ <Z>
0: ──────────────────────┤ <Z>
|0>├──X──┤
|0>├──H──┤
Comment thread
astralcai marked this conversation as resolved.
Outdated
>>> print(qp.draw(circuit, level="device")())
0: ─────────────────┤ <Z>
1: ──H──┤↗│ │0⟩──X─┤
Expand Down
10 changes: 5 additions & 5 deletions pennylane/decomposition/decomposition_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,11 +375,11 @@ def circuit():
return qp.probs(wires=[0, 1, 2, 3])

>>> print(qp.draw(circuit)())
<DynamicWire>: ──Allocate─╭X─╭●───────────────────╭X──Deallocate─┤
0: ───────────├●─────────────────────├●─────────────┤ ╭Probs
1: ───────────├●─────────────────────├●─────────────┤ ├Probs
2: ───────────╰●─│────────────────────╰●─────────────┤ ├Probs
3: ──────────────╰Rot(0.10,0.20,0.30)────────────────┤ ╰Probs
0: ───────╭●──────────────────────╭●────┤ ╭Probs
1: ───────├●─────────────────────├●────┤ ├Probs
2: ───────├●──────────────────────├●────┤ ├Probs
3: ───────│──╭Rot(0.10,0.20,0.30)─│─────┤ ╰Probs
|0>├─╰X─╰●───────────────────╰X──┤
Comment thread
albi3ro marked this conversation as resolved.

"""

Expand Down
24 changes: 20 additions & 4 deletions pennylane/drawer/_add_obj.py
Comment thread
albi3ro marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

from functools import singledispatch

from pennylane.allocation import Allocate, Deallocate
from pennylane.measurements import (
CountsMP,
DensityMatrixMP,
Expand Down Expand Up @@ -69,8 +70,8 @@ def _add_cond_grouping_symbols(op, layer_str, config):
ctrl_symbol = "╝"
layer_str[max_b + n_wires] = f"═{ctrl_symbol}"

for w in range(max_w + 1, max(config.wire_map.values()) + 1):
layer_str[w] = "─║"
for row in range(max_w + 1, config.n_wires):
layer_str[row] = config.wire_filler(row) + "║"

for b in range(max_b):
if b in mapped_bits:
Expand Down Expand Up @@ -121,8 +122,8 @@ def _add_mid_measure_grouping_symbols(op, layer_str, config):
bit = config.bit_map[op] + n_wires
layer_str[bit] += " ╚"

for w in range(mapped_wire + 1, n_wires):
layer_str[w] += "─║"
for row in range(mapped_wire + 1, config.n_wires):
layer_str[row] += config.wire_filler(row) + "║"

for b in range(n_wires, bit):
filler = " " if layer_str[b][-1] == " " else "═"
Expand Down Expand Up @@ -164,6 +165,21 @@ def _add_obj(
raise NotImplementedError(f"unable to draw object {obj}")


@_add_obj.register(Allocate)
def _(obj, layer_str, config, tape_cache=None, skip_grouping_symbols=False):
label = "|0>├" if obj.state.value == "zero" else "├"
Comment thread
astralcai marked this conversation as resolved.
for w in obj.wires:
layer_str[config.wire_map[w]] += label
return layer_str


@_add_obj.register(Deallocate)
def _(obj, layer_str, config, tape_cache=None, skip_grouping_symbols=False):
for w in obj.wires:
layer_str[config.wire_map[w]] += "┤"
return layer_str


@_add_obj.register
def _add_cond(obj: Conditional, layer_str, config, tape_cache=None, skip_grouping_symbols=False):
layer_str = _add_cond_grouping_symbols(obj, layer_str, config)
Expand Down
9 changes: 5 additions & 4 deletions pennylane/drawer/draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from typing import TYPE_CHECKING, Literal

from pennylane import math
from pennylane.allocation import DynamicWire
from pennylane.tape import make_qscript
from pennylane.workflow import construct_batch

Expand Down Expand Up @@ -317,9 +318,9 @@ def wrapper(*args, **kwargs):
_wire_order = wire_order
else:
try:
_wire_order = sorted(tape.wires)
_wire_order = sorted(w for w in tape.wires if not isinstance(w, DynamicWire))
except TypeError:
_wire_order = tape.wires
_wire_order = [w for w in tape.wires if not isinstance(w, DynamicWire)]

return tape_text(
tape,
Expand Down Expand Up @@ -356,9 +357,9 @@ def wrapper(*args, **kwargs):
_wire_order = qnode.device.wires
else:
try:
_wire_order = sorted(tapes[0].wires)
_wire_order = sorted(w for w in tapes[0].wires if not isinstance(w, DynamicWire))
except TypeError:
_wire_order = tapes[0].wires
_wire_order = [w for w in tapes[0].wires if not isinstance(w, DynamicWire)]

cache = {"tape_offset": 0, "matrices": []}
res = [
Expand Down
111 changes: 91 additions & 20 deletions pennylane/drawer/drawable_layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
This module contains a helper function to sort operations into layers.
"""

from dataclasses import dataclass, field
from functools import singledispatch

from pennylane.allocation import Allocate, Deallocate, DynamicWire
from pennylane.measurements import MeasurementProcess
from pennylane.ops import (
Conditional,
Expand Down Expand Up @@ -147,7 +149,71 @@ def _handle_cond(op: Conditional, wire_map, bit_map):
return set(range(min_wire, max_wire + 1))


def drawable_layers(operations, wire_map=None, bit_map=None):
@dataclass
class LayersData:
Comment thread
albi3ro marked this conversation as resolved.
Outdated
"""Data for putting operations into layers."""

ops_in_layer: list[list] = field(default_factory=lambda: [[]])
"""Lists of which operators should be placed in each layer."""

occupied_wires_per_layer: list[set[int]] = field(default_factory=lambda: [set()])
"""The mapped wires that will be occupied in the drawing at each layer."""

used_cwires_per_layer: list[set[int]] = field(default_factory=lambda: [set()])
"""The clasical wires that will be occupied in each layer."""
Comment thread
albi3ro marked this conversation as resolved.
Outdated

waiting_dynamic_wires: list[DynamicWire] = field(default_factory=list)
"""DynamicWires that are waiting for the first interaction between
the dynamic wires and an algorithmic wire.

This allows us to push the allocation and initial setup to the right and conserve
drawer space, keeping things together. See ``insert_waiting_ops``.
"""

waiting_dynamic_ops: list = field(default_factory=list)
"""The Allocate instructions and operators that are waiting on the first interaction between
the waiting_dynamic_wires and an algorithmic wire.
"""

@property
def max_layer(self) -> int:
"""The maximum number of layers present."""
return len(self.ops_in_layer) - 1

def add_to_layer(self, op, op_layer, occupied_wires, used_cwires):
"""Adds an operation to a layer."""
if op_layer > self.max_layer:
self.occupied_wires_per_layer.append(set())
self.ops_in_layer.append([])
self.used_cwires_per_layer.append(set())

self.ops_in_layer[op_layer].append(op)
self.occupied_wires_per_layer[op_layer].update(occupied_wires)
self.used_cwires_per_layer[op_layer].update(used_cwires)

def insert_waiting_ops(self, op_layer, wire_map, bit_map):
"""Insert ops in ``waiting_dynamic_ops`` before ``op_layer``."""
inner_layers = drawable_layers(
self.waiting_dynamic_ops,
wire_map=wire_map,
bit_map=bit_map,
_dynamic_wires=False,
)
for i, layer in enumerate(reversed(inner_layers)):
insert_layer = op_layer - i - 1
if insert_layer < 0:
self.ops_in_layer.insert(0, [])
op_layer += 1
insert_layer = op_layer - i - 1
self.occupied_wires_per_layer.insert(0, set())
self.used_cwires_per_layer.insert(0, set())
self.ops_in_layer[insert_layer].extend(layer)
self.waiting_dynamic_wires = []
self.waiting_dynamic_ops = []
return op_layer


def drawable_layers(operations, wire_map=None, bit_map=None, _dynamic_wires=True):
"""Determine non-overlapping yet dense placement of operations into layers for drawing.

Args:
Expand All @@ -157,6 +223,8 @@ def drawable_layers(operations, wire_map=None, bit_map=None):
wire_map (dict): A map from wire label to non-negative integers. Defaults to None.
bit_map (dict): A map containing mid-circuit measurements used for classical conditions
or collecting statistics as keys. Defaults to None.
_dynamic_wires (bool): **Internal**. Whether or not try pushing allocations and ops
Comment thread
albi3ro marked this conversation as resolved.
Outdated
on only dynamic wires to the right next to the first time the dynamic wires are used.

Returns:
(list[set[~.Operator]], list[set[~.MeasurementProcess]]) : Each index is a set of operations
Expand Down Expand Up @@ -188,14 +256,22 @@ def drawable_layers(operations, wire_map=None, bit_map=None):
bit_map = bit_map or {}

# initialize for operation layers
max_layer = 0
occupied_wires_per_layer = [set()]
ops_in_layer = [[]]
used_cwires_per_layer = [set()]
data = LayersData()
Comment thread
albi3ro marked this conversation as resolved.
Outdated

# loop over operations
for op in operations:
if isinstance(op, MeasurementProcess) and op.mv is not None:
if _dynamic_wires and isinstance(op, Allocate):
data.waiting_dynamic_ops.append(op)
data.waiting_dynamic_wires.extend(op.wires)

elif (
_dynamic_wires
and all(w in data.waiting_dynamic_wires for w in op.wires)
and op.wires # if no wires (GlobalPhase) then do not put into waiting_dynamic_ops
and not isinstance(op, Deallocate) # deallocate should force putting into circuit
):
data.waiting_dynamic_ops.append(op)
elif isinstance(op, MeasurementProcess) and op.mv is not None:
# Only terminal measurements that collect mid-circuit measurement statistics have
# op.mv != None.
# Get the occupied classical wires of the measurement process and find which layer
Expand All @@ -208,34 +284,29 @@ def drawable_layers(operations, wire_map=None, bit_map=None):
)
op_occupied_cwires = set(range(min(mapped_cwires), max(mapped_cwires) + 1))
op_layer = _recursive_find_mcm_stats_layer(
max_layer, op_occupied_cwires, used_cwires_per_layer
data.max_layer, op_occupied_cwires, data.used_cwires_per_layer
)

data.add_to_layer(op, op_layer, op_occupied_wires, op_occupied_cwires)

else:
# Find occupied wires of the operator/measurement process and find which layer to
# put it in.
op_occupied_wires = _get_op_occupied_wires(op, wire_map, bit_map)
try:
op_layer = _recursive_find_layer(
max_layer, op_occupied_wires, occupied_wires_per_layer
data.max_layer, op_occupied_wires, data.occupied_wires_per_layer
)
except RecursionError as e:
raise RecursionError(
f"Drawer is currently at depth {max_layer}, which is too deep to handle. "
f"Drawer is currently at depth {data.max_layer}, which is too deep to handle. "
"Try drawing a smaller subset of your circuit instead."
) from e
op_occupied_cwires = set()

# see if need to add new layer
if op_layer > max_layer:
max_layer += 1
occupied_wires_per_layer.append(set())
ops_in_layer.append([])
used_cwires_per_layer.append(set())
if _dynamic_wires and any(w in data.waiting_dynamic_wires for w in op.wires):
op_layer = data.insert_waiting_ops(op_layer, wire_map, bit_map)

# add to op_layer
ops_in_layer[op_layer].append(op)
occupied_wires_per_layer[op_layer].update(op_occupied_wires)
used_cwires_per_layer[op_layer].update(op_occupied_cwires)
data.add_to_layer(op, op_layer, op_occupied_wires, op_occupied_cwires)

return list(filter(None, ops_in_layer[:-1])) + ops_in_layer[-1:]
return list(filter(None, data.ops_in_layer[:-1])) + data.ops_in_layer[-1:]
Loading
Loading