Source code for pennylane.debugging.debugger
# Copyright 2018-2024 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 module contains functionality for the PennyLane Debugger (PLDB) to support
interactive debugging of quantum circuits.
"""
import copy
import pdb
import sys
from contextlib import contextmanager
import pennylane as qml
class PLDB(pdb.Pdb):
"""Custom debugging class integrated with Pdb.
This class is responsible for storing and updating a global device to be
used for executing quantum circuits while in debugging context. The core
debugger functionality is inherited from the native Python debugger (Pdb).
This class is not directly user-facing, but is interfaced with the
``qml.breakpoint()`` function and ``pldb_device_manager`` context manager.
The former is responsible for launching the debugger prompt and the latter
is responsible with extracting and storing the ``qnode.device``.
The device information is used for validation checks and to execute measurements.
"""
__active_dev = None
def __init__(self, *args, **kwargs):
"""Initialize the debugger, and set custom prompt string."""
super().__init__(*args, **kwargs)
self.prompt = "[pldb] "
@classmethod
def valid_context(cls):
"""Determine if the debugger is called in a valid context.
Raises:
RuntimeError: breakpoint is called outside of a qnode execution
TypeError: breakpoints not supported on this device
"""
if not qml.queuing.QueuingManager.recording() or not cls.has_active_dev():
raise RuntimeError("Can't call breakpoint outside of a qnode execution.")
if cls.get_active_device().name not in ("default.qubit", "lightning.qubit"):
raise TypeError("Breakpoints not supported on this device.")
@classmethod
def add_device(cls, dev):
"""Update the global active device.
Args:
dev (Union[Device, "qml.devices.Device"]): the active device
"""
cls.__active_dev = dev
@classmethod
def get_active_device(cls):
"""Return the active device.
Raises:
RuntimeError: No active device to get
Returns:
Union[Device, "qml.devices.Device"]: The active device
"""
if not cls.has_active_dev():
raise RuntimeError("No active device to get")
return cls.__active_dev
@classmethod
def has_active_dev(cls):
"""Determine if there is currently an active device.
Returns:
bool: True if there is an active device
"""
return bool(cls.__active_dev)
@classmethod
def reset_active_dev(cls):
"""Reset the global active device variable to None."""
cls.__active_dev = None
@classmethod
def _execute(cls, batch_tapes):
"""Execute the batch of tapes on the active device"""
dev = cls.get_active_device()
valid_batch = batch_tapes
if dev.wires:
valid_batch = qml.devices.preprocess.validate_device_wires(
batch_tapes, wires=dev.wires
)[0]
program, new_config = dev.preprocess()
new_batch, fn = program(valid_batch)
# TODO: remove [0] index once compatible with transforms
return fn(dev.execute(new_batch, new_config))[0]
@contextmanager
def pldb_device_manager(device):
"""Context manager to automatically set and reset active
device on the Pennylane Debugger (PLDB).
Args:
device (Union[Device, "qml.devices.Device"]): the active device instance
"""
try:
PLDB.add_device(device)
yield
finally:
PLDB.reset_active_dev()
[docs]def breakpoint():
"""A function which freezes execution and launches the PennyLane debugger (PLDB).
This function marks a location in a quantum circuit (QNode). When it is encountered during
execution of the quantum circuit, an interactive debugging prompt is launched to step
through the circuit execution. Since it is based on the `Python Debugger <https://docs.python.org/3/library/pdb.html>`_ (PDB), commands like
(:code:`list`, :code:`next`, :code:`continue`, :code:`quit`) can be used to navigate the code.
.. seealso:: :doc:`/code/qml_debugging`
**Example**
Consider the following python script containing the quantum circuit with breakpoints.
.. code-block:: python3
:linenos:
import pennylane as qml
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def circuit(x):
qml.breakpoint()
qml.RX(x, wires=0)
qml.Hadamard(wires=1)
qml.breakpoint()
qml.CNOT(wires=[0, 1])
return qml.expval(qml.Z(0))
circuit(1.23)
Running the above python script opens up the interactive :code:`[pldb]` prompt in the terminal.
The prompt specifies the path to the script along with the next line to be executed after the breakpoint.
.. code-block:: console
> /Users/your/path/to/script.py(9)circuit()
-> qml.RX(x, wires=0)
[pldb]
We can interact with the prompt using the commands: :code:`list` , :code:`next`,
:code:`continue`, and :code:`quit`. Additionally, we can also access any variables defined in the function.
.. code-block:: console
[pldb] x
1.23
The :code:`list` command will print a section of code around the breakpoint, highlighting the next line
to be executed.
.. code-block:: console
[pldb] list
5 @qml.qnode(dev)
6 def circuit(x):
7 qml.breakpoint()
8
9 -> qml.RX(x, wires=0)
10 qml.Hadamard(wires=1)
11
12 qml.breakpoint()
13
14 qml.CNOT(wires=[0, 1])
15 return qml.expval(qml.Z(0))
[pldb]
The :code:`next` command will execute the next line of code, and print the new line to be executed.
.. code-block:: console
[pldb] next
> /Users/your/path/to/script.py(10)circuit()
-> qml.Hadamard(wires=1)
[pldb]
The :code:`continue` command will resume code execution until another breakpoint is reached. It will
then print the new line to be executed. Finally, :code:`quit` will resume execution of the file and
terminate the debugging prompt.
.. code-block:: console
[pldb] continue
> /Users/your/path/to/script.py(14)circuit()
-> qml.CNOT(wires=[0, 1])
[pldb] quit
"""
PLDB.valid_context() # Ensure its being executed in a valid context
debugger = PLDB(skip=["pennylane.*"]) # skip internals when stepping through trace
debugger.set_trace(sys._getframe().f_back) # pylint: disable=protected-access
[docs]def debug_state():
"""Compute the quantum state at the current point in the quantum circuit.
Debugging measurements do not alter the state, it remains the same until the
next operation in the circuit.
Returns:
Array(complex): quantum state of the circuit
**Example**
While in a "debugging context", we can query the state as we would at the end of a circuit.
.. code-block:: python3
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def circuit(x):
qml.RX(x, wires=0)
qml.Hadamard(wires=1)
qml.breakpoint()
qml.CNOT(wires=[0, 1])
return qml.expval(qml.Z(0))
circuit(1.23)
Running the above python script opens up the interactive :code:`[pldb]` prompt in the terminal.
We can query the state:
.. code-block:: console
[pldb] longlist
4 @qml.qnode(dev)
5 def circuit(x):
6 qml.RX(x, wires=0)
7 qml.Hadamard(wires=1)
8
9 qml.breakpoint()
10
11 -> qml.CNOT(wires=[0, 1])
12 return qml.expval(qml.Z(0))
[pldb] qml.debug_state()
array([0.57754604+0.j , 0.57754604+0.j ,
0. -0.40797128j, 0. -0.40797128j])
"""
with qml.queuing.QueuingManager.stop_recording():
m = qml.state()
return _measure(m)
[docs]def debug_expval(op):
"""Compute the expectation value of an observable at the
current point in the quantum circuit.
Debugging measurements do not alter the state, it remains the same until the
next operation in the circuit.
Args:
op (Operator): the observable to compute the expectation value for.
Returns:
complex: expectation value of the operator
**Example**
While in a "debugging context", we can query the expectation value of an observable
as we would at the end of a circuit.
.. code-block:: python3
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def circuit(x):
qml.RX(x, wires=0)
qml.Hadamard(wires=1)
qml.breakpoint()
qml.CNOT(wires=[0, 1])
return qml.state()
circuit(1.23)
Running the above python script opens up the interactive :code:`[pldb]` prompt in the terminal.
We can query the expectation value:
.. code-block:: console
[pldb] longlist
4 @qml.qnode(dev)
5 def circuit(x):
6 qml.RX(x, wires=0)
7 qml.Hadamard(wires=1)
8
9 qml.breakpoint()
10
11 -> qml.CNOT(wires=[0, 1])
12 return qml.state()
[pldb] qml.debug_expval(qml.Z(0))
0.33423772712450256
"""
qml.queuing.QueuingManager.active_context().remove(op) # ensure we didn't accidentally queue op
with qml.queuing.QueuingManager.stop_recording():
m = qml.expval(op)
return _measure(m)
[docs]def debug_probs(wires=None, op=None):
"""Compute the probability distribution for the state at the current
point in the quantum circuit.
Debugging measurements do not alter the state, it remains the same until the
next operation in the circuit.
Args:
wires (Union[Iterable, int, str, list]): the wires the operation acts on
op (Union[Observable, MeasurementValue]): observable (with a ``diagonalizing_gates``
attribute) that rotates the computational basis, or a ``MeasurementValue``
corresponding to mid-circuit measurements.
Returns:
Array(float): the probability distribution of the bitstrings for the wires
**Example**
While in a "debugging context", we can query the probability distribution
as we would at the end of a circuit.
.. code-block:: python3
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def circuit(x):
qml.RX(x, wires=0)
qml.Hadamard(wires=1)
qml.breakpoint()
qml.CNOT(wires=[0, 1])
return qml.state()
circuit(1.23)
Running the above python script opens up the interactive :code:`[pldb]` prompt in the terminal.
We can query the probability distribution:
.. code-block:: console
[pldb] longlist
4 @qml.qnode(dev)
5 def circuit(x):
6 qml.RX(x, wires=0)
7 qml.Hadamard(wires=1)
8
9 qml.breakpoint()
10
11 -> qml.CNOT(wires=[0, 1])
12 return qml.state()
[pldb] qml.debug_probs()
array([0.33355943, 0.33355943, 0.16644057, 0.16644057])
"""
if op:
qml.queuing.QueuingManager.active_context().remove(
op
) # ensure we didn't accidentally queue op
with qml.queuing.QueuingManager.stop_recording():
m = qml.probs(wires, op)
return _measure(m)
def _measure(measurement):
"""Perform the measurement.
Args:
measurement (MeasurementProcess): the type of measurement to be performed
Returns:
tuple(complex): results from the measurement
"""
active_queue = qml.queuing.QueuingManager.active_context()
copied_queue = copy.deepcopy(active_queue)
copied_queue.append(measurement)
qtape = qml.tape.QuantumScript.from_queue(copied_queue)
return PLDB._execute((qtape,)) # pylint: disable=protected-access
[docs]def debug_tape():
"""Access the tape of the quantum circuit.
The tape can then be used to access all properties stored in :class:`~pennylane.tape.QuantumTape`.
This can be used to visualize the gates that have
been applied from the quantum circuit so far or otherwise process the operations.
Returns:
QuantumTape: the quantum tape representing the circuit
**Example**
While in a "debugging context", we can access the :code:`QuantumTape` representing the
operations we have applied so far:
.. code-block:: python3
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def circuit(x):
qml.RX(x, wires=0)
qml.Hadamard(wires=1)
qml.CNOT(wires=[0, 1])
qml.breakpoint()
return qml.expval(qml.Z(0))
circuit(1.23)
Running the above python script opens up the interactive :code:`[pldb]` prompt in the terminal.
We can access the tape and draw it as follows:
.. code-block:: console
[pldb] t = qml.debug_tape()
[pldb] print(t.draw())
0: ──RX─╭●─┤
1: ──H──╰X─┤
"""
active_queue = qml.queuing.QueuingManager.active_context()
return qml.tape.QuantumScript.from_queue(active_queue)
_modules/pennylane/debugging/debugger
Download Python script
Download Notebook
View on GitHub