# 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 supportinteractive debugging of quantum circuits."""importcopyimportpdbimportsysfromcontextlibimportcontextmanagerimportpennylaneasqmlclassPLDB(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=Nonedef__init__(self,*args,**kwargs):"""Initialize the debugger, and set custom prompt string."""super().__init__(*args,**kwargs)self.prompt="[pldb] "@classmethoddefvalid_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 """ifnotqml.queuing.QueuingManager.recording()ornotcls.has_active_dev():raiseRuntimeError("Can't call breakpoint outside of a qnode execution.")ifcls.get_active_device().namenotin("default.qubit","lightning.qubit"):raiseTypeError("Breakpoints not supported on this device.")@classmethoddefadd_device(cls,dev):"""Update the global active device. Args: dev (Union[Device, "qml.devices.Device"]): the active device """cls.__active_dev=dev@classmethoddefget_active_device(cls):"""Return the active device. Raises: RuntimeError: No active device to get Returns: Union[Device, "qml.devices.Device"]: The active device """ifnotcls.has_active_dev():raiseRuntimeError("No active device to get")returncls.__active_dev@classmethoddefhas_active_dev(cls):"""Determine if there is currently an active device. Returns: bool: True if there is an active device """returnbool(cls.__active_dev)@classmethoddefreset_active_dev(cls):"""Reset the global active device variable to None."""cls.__active_dev=None@classmethoddef_execute(cls,batch_tapes):"""Execute the batch of tapes on the active device"""dev=cls.get_active_device()valid_batch=batch_tapesifdev.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 transformsreturnfn(dev.execute(new_batch,new_config))[0]@contextmanagerdefpldb_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)yieldfinally:PLDB.reset_active_dev()
[docs]defbreakpoint():"""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 contextdebugger=PLDB(skip=["pennylane.*"])# skip internals when stepping through tracedebugger.set_trace(sys._getframe().f_back)# pylint: disable=protected-access
[docs]defdebug_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]) """withqml.queuing.QueuingManager.stop_recording():m=qml.state()return_measure(m)
[docs]defdebug_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 opwithqml.queuing.QueuingManager.stop_recording():m=qml.expval(op)return_measure(m)
[docs]defdebug_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]) """ifop:qml.queuing.QueuingManager.active_context().remove(op)# ensure we didn't accidentally queue opwithqml.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)returnPLDB._execute((qtape,))# pylint: disable=protected-access
[docs]defdebug_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()returnqml.tape.QuantumScript.from_queue(active_queue)