Skip to content

Commit

Permalink
Deprecate tape and qtape properties on the QNode (#6583)
Browse files Browse the repository at this point in the history
**Context:**

The QNode currently has `QNode.tape` and `QNode.qtape` properties, which
track the last tape generated during a construct call. Users can check
these to verify what was executed most recently.

Now, we’ve added `construct_tape(qnode)(*args, **kwargs)` in the
workflow module as a preferred method for constructing tapes. This
function creates a tape without modifying the QNode, ensuring it doesn’t
interfere with other parts of the codebase.

**Description of the change**

- _Hopefully_ removed all calls to `QNode.tape` and `QNode.qtape` in the
PennyLane source code.
- Since many tests rely on these properties to access tape information,
I’ve temporarily suppressed the warnings. This needs to be revisited in
the backlog and gradually updated to use the preferred code [sc-78317].

Found references in,
- QML: PennyLaneAI/qml#1266
- Qiskit:  PennyLaneAI/pennylane-qiskit#602

No references in,
- Lightning
- Catalyst
- AQT
- IONQ
- Qulacs
- Cirq

**Benefits:** Removes problems and confusion with having a mutable tape
property.

**Possible Drawbacks:** None

[sc-76836]

---------

Co-authored-by: Christina Lee <[email protected]>
Co-authored-by: Mudit Pandey <[email protected]>
  • Loading branch information
3 people authored Nov 18, 2024
1 parent 8df1025 commit 9a3dbdd
Show file tree
Hide file tree
Showing 68 changed files with 640 additions and 155 deletions.
8 changes: 7 additions & 1 deletion doc/development/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ deprecations are listed below.
Pending deprecations
--------------------

* The ``tape`` and ``qtape`` properties of ``QNode`` have been deprecated.
Instead, use the ``qml.workflow.construct_tape`` function.

- Deprecated in v0.40
- Will be removed in v0.41

* The ``max_expansion`` argument in :func:`~pennylane.devices.preprocess.decompose` is deprecated.

- Deprecated in v0.40
Expand All @@ -25,7 +31,7 @@ Pending deprecations
- Will be removed in v0.41

* The ``QNode.get_best_method`` and ``QNode.best_method_str`` methods have been deprecated.
Instead, use the ``qml.workflow.get_best_diff_method``.
Instead, use the ``qml.workflow.get_best_diff_method`` function.

- Deprecated in v0.40
- Will be removed in v0.41
Expand Down
3 changes: 2 additions & 1 deletion doc/introduction/inspecting_circuits.rst
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ or to check whether two gates causally influence each other.
import pennylane as qml
from pennylane import CircuitGraph
from pennylane.workflow import construct_tape
dev = qml.device('lightning.qubit', wires=(0,1,2,3))
Expand All @@ -292,7 +293,7 @@ or to check whether two gates causally influence each other.
circuit()
tape = circuit.qtape
tape = construct_tape(circuit)()
ops = tape.operations
obs = tape.observables
g = CircuitGraph(ops, obs, tape.wires)
Expand Down
4 changes: 4 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ following 4 sets of functions have been either moved or removed[(#6588)](https:/

<h3>Deprecations 👋</h3>

* The `tape` and `qtape` properties of `QNode` have been deprecated.
Instead, use the `qml.workflow.construct_tape` function.
[(#6583)](https://github.com/PennyLaneAI/pennylane/pull/6583)

* The `max_expansion` argument in `qml.devices.preprocess.decompose` is deprecated and will be removed in v0.41.
[(#6400)](https://github.com/PennyLaneAI/pennylane/pull/6400)

Expand Down
8 changes: 4 additions & 4 deletions pennylane/circuit_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,14 +516,14 @@ def max_simultaneous_measurements(self):
>>> def circuit_measure_max_once():
... return qml.expval(qml.X(0))
>>> qnode = qml.QNode(circuit_measure_max_once, dev)
>>> qnode()
>>> qnode.qtape.graph.max_simultaneous_measurements
>>> tape = qml.workflow.construct_tape(qnode)()
>>> tape.graph.max_simultaneous_measurements
1
>>> def circuit_measure_max_twice():
... return qml.expval(qml.X(0)), qml.probs(wires=0)
>>> qnode = qml.QNode(circuit_measure_max_twice, dev)
>>> qnode()
>>> qnode.qtape.graph.max_simultaneous_measurements
>>> tape = qml.workflow.construct_tape(qnode)()
>>> tape.graph.max_simultaneous_measurements
2
Returns:
Expand Down
4 changes: 2 additions & 2 deletions pennylane/debugging/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ def snapshots_qnode(self, qnode, targs, tkwargs):

def get_snapshots(*args, **kwargs):
# Need to construct to generate the tape and be able to validate
qnode.construct(args, kwargs)
qml.devices.preprocess.validate_measurements(qnode.tape)
tape = qml.workflow.construct_tape(qnode)(*args, **kwargs)
qml.devices.preprocess.validate_measurements(tape)

old_interface = qnode.interface
if old_interface == "auto":
Expand Down
59 changes: 24 additions & 35 deletions pennylane/drawer/draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,41 +314,30 @@ def wrapper(*args, **kwargs):
except TypeError:
_wire_order = tapes[0].wires

if tapes is not None:
cache = {"tape_offset": 0, "matrices": []}
res = [
tape_text(
t,
wire_order=_wire_order,
show_all_wires=show_all_wires,
decimals=decimals,
show_matrices=False,
show_wire_labels=show_wire_labels,
max_length=max_length,
cache=cache,
)
for t in tapes
]
if show_matrices and cache["matrices"]:
mat_str = ""
for i, mat in enumerate(cache["matrices"]):
if qml.math.requires_grad(mat) and hasattr(mat, "detach"):
mat = mat.detach()
mat_str += f"\nM{i} = \n{mat}"
if mat_str:
mat_str = "\n" + mat_str
return "\n\n".join(res) + mat_str
return "\n\n".join(res)

return tape_text(
qnode.qtape,
wire_order=_wire_order,
show_all_wires=show_all_wires,
decimals=decimals,
show_matrices=show_matrices,
show_wire_labels=show_wire_labels,
max_length=max_length,
)
cache = {"tape_offset": 0, "matrices": []}
res = [
tape_text(
t,
wire_order=_wire_order,
show_all_wires=show_all_wires,
decimals=decimals,
show_matrices=False,
show_wire_labels=show_wire_labels,
max_length=max_length,
cache=cache,
)
for t in tapes
]
if show_matrices and cache["matrices"]:
mat_str = ""
for i, mat in enumerate(cache["matrices"]):
if qml.math.requires_grad(mat) and hasattr(mat, "detach"):
mat = mat.detach()
mat_str += f"\nM{i} = \n{mat}"
if mat_str:
mat_str = "\n" + mat_str
return "\n\n".join(res) + mat_str
return "\n\n".join(res)

return wrapper

Expand Down
5 changes: 3 additions & 2 deletions pennylane/fourier/qnode_spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,15 +400,16 @@ def wrapper(*args, **kwargs):
)
# After construction, check whether invalid operations (for a spectrum)
# are present in the QNode
for m in qnode.qtape.measurements:
tape = qml.workflow.construct_tape(qnode)(*args, **kwargs)
for m in tape.measurements:
if not isinstance(m, (qml.measurements.ExpectationMP, qml.measurements.ProbabilityMP)):
raise ValueError(
f"The measurement {m.__class__.__name__} is not supported as it likely does "
"not admit a Fourier spectrum."
)
cjacs = jac_fn(*args, **kwargs)
spectra = {}
tape = qml.transforms.expand_multipar(qnode.qtape)
tape = qml.transforms.expand_multipar(tape)
par_info = tape.par_info

# Iterate over jacobians per argument
Expand Down
3 changes: 1 addition & 2 deletions pennylane/gradients/classical_jacobian.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,7 @@ def classical_preprocessing(*args, **kwargs):
"""Returns the trainable gate parameters for a given QNode input."""
kwargs.pop("shots", None)
kwargs.pop("argnums", None)
qnode.construct(args, kwargs)
tape = qnode.qtape
tape = qml.workflow.construct_tape(qnode)(*args, **kwargs)

if expand_fn is not None:
tape = expand_fn(tape)
Expand Down
3 changes: 1 addition & 2 deletions pennylane/gradients/parameter_shift_hessian.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,8 +521,7 @@ def param_shift_hessian(
the parameter-shifted tapes and a post-processing function to combine the execution
results of these tapes into the Hessian:
>>> circuit(x) # generate the QuantumTape inside the QNode
>>> tape = circuit.qtape
>>> tape = qml.workflow.construct_tape(circuit)(x)
>>> hessian_tapes, postproc_fn = qml.gradients.param_shift_hessian(tape)
>>> len(hessian_tapes)
13
Expand Down
6 changes: 3 additions & 3 deletions pennylane/gradients/pulse_gradient_odegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,9 +504,9 @@ def circuit(params):
Alternatively, we may apply the transform to the tape of the pulse program, obtaining
the tapes with inserted ``PauliRot`` gates together with the post-processing function:
>>> circuit.construct((params,), {}) # Build the tape of the circuit.
>>> circuit.tape.trainable_params = [0, 1, 2]
>>> tapes, fun = qml.gradients.pulse_odegen(circuit.tape, argnum=[0, 1, 2])
>>> tape = qml.workflow.construct_tape(circuit)(params) # Build the tape of the circuit.
>>> tape.trainable_params = [0, 1, 2]
>>> tapes, fun = qml.gradients.pulse_odegen(tape, argnum=[0, 1, 2])
>>> len(tapes)
12
Expand Down
5 changes: 3 additions & 2 deletions pennylane/math/multi_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from autoray import numpy as np
from numpy import ndarray

import pennylane as qml

from . import single_dispatch # pylint:disable=unused-import
from .utils import cast, cast_like, get_interface, requires_grad

Expand Down Expand Up @@ -1032,8 +1034,7 @@ def jax_argnums_to_tape_trainable(qnode, argnums, program, args, kwargs):
for i, arg in enumerate(args)
]

qnode.construct(args_jvp, kwargs)
tape = qnode.qtape
tape = qml.workflow.construct_tape(qnode, level=0)(*args_jvp, **kwargs)
tapes, _ = program((tape,))
del trace
return tuple(tape.get_parameters(trainable_only=False) for tape in tapes)
Expand Down
3 changes: 2 additions & 1 deletion pennylane/ops/functions/map_wires.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ def map_wires(
>>> mapped_circuit = qml.map_wires(circuit, wire_map)
>>> mapped_circuit()
tensor([0.92885434, 0.07114566], requires_grad=True)
>>> list(mapped_circuit.tape)
>>> tape = qml.workflow.construct_tape(mapped_circuit)()
>>> list(tape)
[((RX(0.54, wires=[3]) @ X(2)) @ Z(1)) @ RY(1.23, wires=[0]), probs(wires=[3])]
"""
if isinstance(input, (Operator, MeasurementProcess)):
Expand Down
3 changes: 2 additions & 1 deletion pennylane/ops/functions/simplify.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ def simplify(input: Union[Operator, MeasurementProcess, QuantumScript, QNode, Ca
... return qml.probs(wires=0)
>>> circuit()
tensor([0.64596329, 0.35403671], requires_grad=True)
>>> list(circuit.tape)
>>> tape = qml.workflow.construct_tape(circuit)()
>>> list(tape)
[RZ(11.566370614359172, wires=[0]) @ RY(11.566370614359172, wires=[0]) @ RX(11.566370614359172, wires=[0]),
probs(wires=[0])]
"""
Expand Down
3 changes: 2 additions & 1 deletion pennylane/optimize/adaptive.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,14 +199,15 @@ def step_and_cost(self, circuit, operator_pool, drain_pool=False, params_zero=Tr
"""
cost = circuit()
qnode = copy.copy(circuit)
tape = qml.workflow.construct_tape(qnode)()

if drain_pool:
operator_pool = [
gate
for gate in operator_pool
if all(
gate.name != operation.name or gate.wires != operation.wires
for operation in circuit.tape.operations
for operation in tape.operations
)
]

Expand Down
23 changes: 12 additions & 11 deletions pennylane/optimize/qnspsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,10 @@ def _get_spsa_grad_tapes(self, cost, args, kwargs):
args_plus[index] = arg + self.finite_diff_step * direction
args_minus[index] = arg - self.finite_diff_step * direction

cost.construct(args_plus, kwargs)
tape_plus = cost.tape.copy(copy_operations=True)
cost.construct(args_minus, kwargs)
tape_minus = cost.tape.copy(copy_operations=True)
tape = qml.workflow.construct_tape(cost)(*args_plus, **kwargs)
tape_plus = tape.copy(copy_operations=True)
tape = qml.workflow.construct_tape(cost)(*args_minus, **kwargs)
tape_minus = tape.copy(copy_operations=True)
return [tape_plus, tape_minus], dirs

def _update_tensor(self, tensor_raw):
Expand Down Expand Up @@ -425,22 +425,23 @@ def _get_overlap_tape(self, cost, args1, args2, kwargs):
op_inv = self._get_operations(cost, args2, kwargs)

new_ops = op_forward + [qml.adjoint(op) for op in reversed(op_inv)]
return qml.tape.QuantumScript(new_ops, [qml.probs(wires=cost.tape.wires.labels)])
tape = qml.workflow.construct_tape(cost)(*args1, **kwargs)
return qml.tape.QuantumScript(new_ops, [qml.probs(wires=tape.wires.labels)])

@staticmethod
def _get_operations(cost, args, kwargs):
cost.construct(args, kwargs)
return cost.tape.operations
tape = qml.workflow.construct_tape(cost)(*args, **kwargs)
return tape.operations

def _apply_blocking(self, cost, args, kwargs, params_next):
cost.construct(args, kwargs)
tape_loss_curr = cost.tape.copy(copy_operations=True)
tape = qml.workflow.construct_tape(cost)(*args, **kwargs)
tape_loss_curr = tape.copy(copy_operations=True)

if not isinstance(params_next, list):
params_next = [params_next]

cost.construct(params_next, kwargs)
tape_loss_next = cost.tape.copy(copy_operations=True)
tape = qml.workflow.construct_tape(cost)(*params_next, **kwargs)
tape_loss_next = tape.copy(copy_operations=True)

program, _ = cost.device.preprocess()

Expand Down
3 changes: 2 additions & 1 deletion pennylane/optimize/riemannian_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,9 @@ def get_omegas(self):

obs_groupings, _ = qml.pauli.group_observables(self.observables, self.coeffs)
# get all circuits we need to calculate the coefficients
tape = qml.workflow.construct_tape(self.circuit)()
circuits = algebra_commutator(
self.circuit.qtape,
tape,
obs_groupings,
self.lie_algebra_basis_names,
self.nqubits,
Expand Down
3 changes: 1 addition & 2 deletions pennylane/optimize/shot_adaptive.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,7 @@ def _single_shot_qnode_gradients(self, qnode, args, kwargs):
"""Compute the single shot gradients of a QNode."""
self.check_device(qnode.device)

qnode.construct(args, kwargs)
tape = qnode.tape
tape = qml.workflow.construct_tape(qnode)(*args, **kwargs)
[expval] = tape.measurements
coeffs, observables = (
expval.obs.terms()
Expand Down
7 changes: 3 additions & 4 deletions pennylane/transforms/core/transform_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,7 @@ def classical_preprocessing(program, *args, **kwargs):
"""Returns the trainable gate parameters for a given QNode input."""
kwargs.pop("shots", None)
kwargs.pop("argnums", None)
qnode.construct(args, kwargs)
tape = qnode.qtape
tape = qml.workflow.construct_tape(qnode, level=0)(*args, **kwargs)
tapes, _ = program((tape,))
res = tuple(qml.math.stack(tape.get_parameters(trainable_only=True)) for tape in tapes)
if len(tapes) == 1:
Expand Down Expand Up @@ -456,8 +455,8 @@ def _jacobian(*args, **kwargs):
classical_jacobian = jacobian(
classical_preprocessing, sub_program, argnums, *args, **kwargs
)
qnode.construct(args, kwargs)
tapes, _ = sub_program((qnode.tape,))
tape = qml.workflow.construct_tape(qnode, level=0)(*args, **kwargs)
tapes, _ = sub_program((tape,))
multi_tapes = len(tapes) > 1
if not multi_tapes:
classical_jacobian = [classical_jacobian]
Expand Down
24 changes: 18 additions & 6 deletions pennylane/workflow/qnode.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,19 @@ def best_method_str(device: SupportedDeviceAPIs, interface: SupportedInterfaceUs

@property
def tape(self) -> QuantumTape:
"""The quantum tape"""
"""The quantum tape
.. warning::
This property is deprecated in v0.40 and will be removed in v0.41.
Instead, use the :func:`qml.workflow.construct_tape <.workflow.construct_tape>` function.
"""

warnings.warn(
"The tape/qtape property is deprecated and will be removed in v0.41. "
"Instead, use the qml.workflow.get_best_diff_method function.",
qml.PennyLaneDeprecationWarning,
)
return self._tape

qtape = tape # for backwards compatibility
Expand All @@ -859,10 +871,10 @@ def construct(self, args, kwargs): # pylint: disable=too-many-branches

self._tape = QuantumScript.from_queue(q, shots)

params = self.tape.get_parameters(trainable_only=False)
self.tape.trainable_params = qml.math.get_trainable_indices(params)
params = self._tape.get_parameters(trainable_only=False)
self._tape.trainable_params = qml.math.get_trainable_indices(params)

_validate_qfunc_output(self._qfunc_output, self.tape.measurements)
_validate_qfunc_output(self._qfunc_output, self._tape.measurements)

def _execution_component(self, args: tuple, kwargs: dict) -> qml.typing.Result:
"""Construct the transform program and execute the tapes. Helper function for ``__call__``
Expand All @@ -883,7 +895,7 @@ def _execution_component(self, args: tuple, kwargs: dict) -> qml.typing.Result:
gradient_fn = qml.gradients.param_shift
else:
gradient_fn = QNode.get_gradient_fn(
self.device, self.interface, self.diff_method, tape=self.tape
self.device, self.interface, self.diff_method, tape=self._tape
)[0]
execute_kwargs = copy.copy(self.execute_kwargs)

Expand Down Expand Up @@ -949,7 +961,7 @@ def _execution_component(self, args: tuple, kwargs: dict) -> qml.typing.Result:
# convert result to the interface in case the qfunc has no parameters

if (
len(self.tape.get_parameters(trainable_only=False)) == 0
len(self._tape.get_parameters(trainable_only=False)) == 0
and not self.transform_program.is_informative
):
res = _convert_to_interface(res, self.interface)
Expand Down
Loading

0 comments on commit 9a3dbdd

Please sign in to comment.