Source code for pennylane.transforms.resolve_dynamic_wires
# Copyright 2025 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This submodule contains a transform for resolving dynamic wires into real wires.
"""
from collections.abc import Hashable, Sequence
from pennylane.allocation import Allocate, Deallocate
from pennylane.measurements import measure
from pennylane.tape import QuantumScript, QuantumScriptBatch
from pennylane.typing import PostprocessingFn, Result, ResultBatch
from .core import transform
class _WireManager:
"""Handles converting dynamic wires into concrete values."""
def __init__(self, zeroed=(), any_state=(), min_int=None):
self._zeroed = list(zeroed)
self._any_state = list(any_state)
self._loaned = {} # wire to final register type
self.min_int = min_int
def get_wire(self, require_zeros, restored):
"""Retrieve a concrete wire label from available registers."""
if not self._zeroed and not self._any_state:
if self.min_int is None:
raise ValueError("no wires left to allocate.")
self._zeroed.append(self.min_int)
self.min_int += 1
if require_zeros:
if self._zeroed:
w = self._zeroed.pop()
self._loaned[w] = "zeroed" if restored else "any_state"
return w, []
w = self._any_state.pop()
self._loaned[w] = "zeroed" if restored else "any_state"
m = measure(w, reset=True)
return w, m.measurements
if self._any_state:
w = self._any_state.pop()
self._loaned[w] = "any_state"
return w, []
w = self._zeroed.pop()
self._loaned[w] = "zeroed" if restored else "any_state"
return w, []
def return_wire(self, wire):
"""Return a wire label back to be re-used."""
reg_type = self._loaned.pop(wire)
if reg_type == "zeroed":
self._zeroed.append(wire)
else:
self._any_state.append(wire)
def null_postprocessing(results: ResultBatch) -> Result:
"""An empty postprocessing function returned by resolve_dynamic_wires"""
return results[0]
[docs]
@transform
def resolve_dynamic_wires(
tape: QuantumScript,
zeroed: Sequence[Hashable] = (),
any_state: Sequence[Hashable] = (),
min_int: int | None = None,
) -> tuple[QuantumScriptBatch, PostprocessingFn]:
"""Map dynamic wires to concrete values determined by the provided ``zeroed`` and ``any_state`` registers.
Args:
tape (QuantumScript): A circuit that may contain dynamic wire allocations and deallocations
zeroed (Sequence[Hashable]): a register of wires known to be the zero state
any_state (Sequence[Hashable]): a register of wires with any state
min_int (Optional[int]): If not ``None``, new wire labels can be created starting at this
integer and incrementing whenever a new wire is needed.
Returns:
tuple[QuantumScript], Callable[[ResultBatch], Result]: A batch of tapes and a postprocessing function
.. note::
This transform currently uses a "Last In, First Out" (LIFO) stack based approach to distributing wires.
This minimizes the total number of wires used, at the cost of higher depth and more resets. Other
approaches could be taken as well, such as a "First In, First out" algorithm that minimizes depth.
This approach also means we pop wires from the *end* of the stack first.
For a dynamic wire requested to be in the zero state (``require_zeros=True``), we try three things before erroring:
#. If wires exist in the ``zeroed`` register, we take one from that register
#. If no ``zeroed`` wires exist, we pull one from ``any_state`` and apply a reset operation
#. If no wires exist in the ``zeroed`` or ``any_state`` registers and ``min_int`` is not ``None``,
we increment ``min_int`` and add a new wire.
For a dynamic wire with ``require_zeros=False``, we try:
#. If wires exist in the ``any_state``, we take one from that register
#. If no wires exist in ``any_state``, we pull one from ``zeroed``
#. If no wires exist in the ``zeroed`` or ``any_state`` registers and ``min_int`` is not ``None``,
we increment ``min_int`` and add a new wire
This transform uses a combination of two different modes: one with fixed registers specified by ``zeroed`` and
``any_state``, and one with a dynamically sized register characterized by the integer ``min_int``. We assume
that the upfront cost associated with using more wires has already been paid for anything in ``zeroed`` and
``any_state``. Whether or not we use them, they will still be there. In this case, using a fresh wire is cheaper
than reset. For the dynamically sized register, we assume that we have to pay an
additional cost each time we allocate a new wire. For the dynamically sized register, applying a reset
operation is therefor cheaper than allocating a new wire.
This approach minimizes the width of the circuit at the cost of more reset operations.
.. code-block:: python
def circuit(require_zeros=True):
with qml.allocation.allocate(1, require_zeros=require_zeros) as wires:
qml.X(wires)
with qml.allocation.allocate(1, require_zeros=require_zeros) as wires:
qml.Y(wires)
>>> print(qml.draw(circuit)())
<DynamicWire>: ──Allocate──X──Deallocate─┤
<DynamicWire>: ──Allocate──Y──Deallocate─┤
If we provide two zeroed qubits to the transform, we can see that the two operations have been
assigned to both wires known to be in the zero state.
>>> from pennylane.transforms import resolve_dynamic_wires
>>> assigned_two_zeroed = resolve_dynamic_wires(circuit, zeroed=("a", "b"))
>>> print(qml.draw(assigned_two_zeroed)())
a: ──Y─┤
b: ──X─┤
If we only provide one zeroed wire, we perform a reset on that wire before reusing for the ``Y`` operation.
>>> assigned_one_zeroed = resolve_dynamic_wires(circuit, zeroed=("a",))
>>> print(qml.draw(assigned_one_zeroed)())
a: ──X──┤↗│ │0⟩──Y─┤
If we only provide ``any_state`` qubits with unknown states, then they will be reset to zero before being used
in an operation that requires a zero state.
>>> assigned_any_state = resolve_dynamic_wires(circuit, any_state=("a", "b"))
>>> print(qml.draw(assigned_any_state)())
b: ──┤↗│ │0⟩──X──┤↗│ │0⟩──Y─|
Note that the last provided wire with label ``"b"`` is used first.
If the wire allocations had ``require_zeros=False``, no reset operations would occur:
>>> print(qml.draw(assigned_any_state)(require_zeros=False))
b: ──X──Y─┤
Instead of registers of available wires, a ``min_int`` can be specified instead. The ``min_int`` indicates
the first integer to start allocating wires to. Whenever we have no qubits available to allocate, we increment the integer
and add a new wire to the pool:
>>> circuit_integers = resolve_dynamic_wires(circuit, min_int=0)
>>> print(qml.draw(circuit_integers)())
0: ──X──┤↗│ │0⟩──Y─┤
Note that we still prefer using already created wires over creating new wires.
.. code-block:: python
def multiple_allocations():
with qml.allocation.allocate(1) as wires:
qml.X(wires)
with qml.allocation.allocate(3) as wires:
qml.Toffoli(wires)
>>> circuit_integers2 = resolve_dynamic_wires(multiple_allocations, min_int=0)
>>> print(qml.draw(circuit_integers2)())
0: ──X──┤↗│ │0⟩─╭●─┤
1: ──────────────├●─┤
2: ──────────────╰X─┤
If both an explicit register and ``min_int`` are specified, ``min_int`` will be used once all available
explicit wires are loaned out. Below, ``"a"`` is extracted and used first, but then wires
are extracted starting from ``0``.
>>> zeroed_and_min_int = resolve_dynamic_wires(multiple_allocations, zeroed=("a",), min_int=0)
>>> print(qml.draw(zeroed_and_min_int)())
a: ──X──┤↗│ │0⟩─╭●─┤
0: ──────────────├●─┤
1: ──────────────╰X─┤
"""
manager = _WireManager(zeroed=zeroed, any_state=any_state, min_int=min_int)
wire_map = {}
deallocated = set()
new_ops = []
for op in tape.operations:
if isinstance(op, Allocate):
for w in op.wires:
wire, ops = manager.get_wire(**op.hyperparameters)
new_ops += ops
wire_map[w] = wire
elif isinstance(op, Deallocate):
for w in op.wires:
deallocated.add(w)
manager.return_wire(wire_map.pop(w))
else:
op = op.map_wires(wire_map)
if intersection := deallocated.intersection(set(op.wires)):
raise ValueError(
f"Encountered deallocated wires {intersection} in {op}. Dynamic wires cannot be used after deallocation."
)
new_ops.append(op.map_wires(wire_map))
mps = [mp.map_wires(wire_map) for mp in tape.measurements]
for mp in mps:
if intersection := deallocated.intersection(set(mp.wires)):
raise ValueError(
f"Encountered deallocated wires {intersection} in {mp}. Dynamic wires cannot be used after deallocation."
)
return (tape.copy(ops=new_ops, measurements=mps),), null_postprocessing
_modules/pennylane/transforms/resolve_dynamic_wires
Download Python script
Download Notebook
View on GitHub