Source code for pennylane.drawer.draw

# pylint: disable=too-many-arguments

# Copyright 2018-2021 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.
"""
Contains the drawing function.
"""
import warnings
from functools import wraps
from typing import Literal, Union

import pennylane as qml

from .tape_mpl import tape_mpl
from .tape_text import tape_text


def catalyst_qjit(qnode):
    """A method checking whether a qnode is compiled by catalyst.qjit"""
    return qnode.__class__.__name__ == "QJIT" and hasattr(qnode, "user_function")


[docs]def draw( qnode, wire_order=None, show_all_wires=False, decimals=2, max_length=100, show_matrices=True, show_wire_labels=True, level: Union[None, Literal["top", "user", "device", "gradient"], int, slice] = "gradient", ): r"""Create a function that draws the given qnode or quantum function. Args: qnode (.QNode or Callable): the input QNode or quantum function that is to be drawn wire_order (Sequence[Any]): The order (from top to bottom) to print the wires of the circuit. Defaults to the device wires. If device wires are not available, the circuit wires are sorted if possible. show_all_wires (bool): If True, all wires, including empty wires, are printed. decimals (int): How many decimal points to include when formatting operation parameters. Defaults to ``2`` decimal points. ``None`` will omit parameters from operation labels. max_length (int): Maximum string width (columns) when printing the circuit. Defaults to ``100``. show_matrices (bool): Show matrix valued parameters below all circuit diagrams. Defaults to ``False``. show_wire_labels (bool): Whether or not to show the wire labels. Defaults to ``True``. level (None, str, int, slice): An indication of what transforms to apply before drawing. Defaults to ``"gradient"``. Check :func:`~.workflow.get_transform_program` for more information on the allowed values and usage details of this argument. Returns: A function that has the same argument signature as ``qnode``. When called, the function will draw the QNode/qfunc. **Example** .. code-block:: python3 @qml.qnode(qml.device('lightning.qubit', wires=2)) def circuit(a, w): qml.Hadamard(0) qml.CRX(a, wires=[0, 1]) qml.Rot(*w, wires=[1], id="arbitrary") qml.CRX(-a, wires=[0, 1]) return qml.expval(qml.Z(0) @ qml.Z(1)) >>> print(qml.draw(circuit)(a=2.3, w=[1.2, 3.2, 0.7])) 0: ──H─╭●─────────────────────────────────────────╭●─────────┤ ╭<Z@Z> 1: ────╰RX(2.30)──Rot(1.20,3.20,0.70,"arbitrary")─╰RX(-2.30)─┤ ╰<Z@Z> .. details:: :title: Usage Details By specifying the ``decimals`` keyword, parameters are displayed to the specified precision. >>> print(qml.draw(circuit, decimals=4)(a=2.3, w=[1.2, 3.2, 0.7])) 0: ──H─╭●─────────────────────────────────────────────────╭●───────────┤ ╭<Z@Z> 1: ────╰RX(2.3000)──Rot(1.2000,3.2000,0.7000,"arbitrary")─╰RX(-2.3000)─┤ ╰<Z@Z> Parameters can be omitted by requesting ``decimals=None``: >>> print(qml.draw(circuit, decimals=None)(a=2.3, w=[1.2, 3.2, 0.7])) 0: ──H─╭●────────────────────╭●──┤ ╭<Z@Z> 1: ────╰RX──Rot("arbitrary")─╰RX─┤ ╰<Z@Z> If the parameters are not acted upon by classical processing like ``-a``, then ``qml.draw`` can handle string-valued parameters as well: >>> @qml.qnode(qml.device('lightning.qubit', wires=1)) ... def circuit2(x): ... qml.RX(x, wires=0) ... return qml.expval(qml.Z(0)) >>> print(qml.draw(circuit2)("x")) 0: ──RX(x)─┤ <Z> When requested with ``show_matrices=True`` (the default), matrix valued parameters are printed below the circuit. For ``show_matrices=False``, they are not printed: >>> @qml.qnode(qml.device('default.qubit', wires=2)) ... def circuit3(): ... qml.QubitUnitary(np.eye(2), wires=0) ... qml.QubitUnitary(-np.eye(4), wires=(0,1)) ... return qml.expval(qml.Hermitian(np.eye(2), wires=1)) >>> print(qml.draw(circuit3)()) 0: ──U(M0)─╭U(M1)─┤ 1: ────────╰U(M1)─┤ <𝓗(M0)> M0 = [[1. 0.] [0. 1.]] M1 = [[-1. -0. -0. -0.] [-0. -1. -0. -0.] [-0. -0. -1. -0.] [-0. -0. -0. -1.]] >>> print(qml.draw(circuit3, show_matrices=False)()) 0: ──U(M0)─╭U(M1)─┤ 1: ────────╰U(M1)─┤ <𝓗(M0)> The ``max_length`` keyword warps long circuits: .. code-block:: python rng = np.random.default_rng(seed=42) shape = qml.StronglyEntanglingLayers.shape(n_wires=3, n_layers=3) params = rng.random(shape) @qml.qnode(qml.device('lightning.qubit', wires=3)) def longer_circuit(params): qml.StronglyEntanglingLayers(params, wires=range(3)) return [qml.expval(qml.Z(i)) for i in range(3)] >>> print(qml.draw(longer_circuit, max_length=60, level="device")(params)) 0: ──Rot(0.77,0.44,0.86)─╭●────╭X──Rot(0.45,0.37,0.93)─╭●─╭X 1: ──Rot(0.70,0.09,0.98)─╰X─╭●─│───Rot(0.64,0.82,0.44)─│──╰● 2: ──Rot(0.76,0.79,0.13)────╰X─╰●──Rot(0.23,0.55,0.06)─╰X─── ───Rot(0.83,0.63,0.76)──────────────────────╭●────╭X─┤ <Z> ──╭X────────────────────Rot(0.35,0.97,0.89)─╰X─╭●─│──┤ <Z> ──╰●────────────────────Rot(0.78,0.19,0.47)────╰X─╰●─┤ <Z> The ``wire_order`` keyword specifies the order of the wires from top to bottom: >>> print(qml.draw(circuit, wire_order=[1,0])(a=2.3, w=[1.2, 3.2, 0.7])) 1: ────╭RX(2.30)──Rot(1.20,3.20,0.70,"arbitrary")─╭RX(-2.30)─┤ ╭<Z@Z> 0: ──H─╰●─────────────────────────────────────────╰●─────────┤ ╰<Z@Z> If the device or ``wire_order`` has wires not used by operations, those wires are omitted unless requested with ``show_all_wires=True`` >>> empty_qfunc = lambda : qml.expval(qml.Z(0)) >>> empty_circuit = qml.QNode(empty_qfunc, qml.device('lightning.qubit', wires=3)) >>> print(qml.draw(empty_circuit, show_all_wires=True)()) 0: ───┤ <Z> 1: ───┤ 2: ───┤ Drawing also works on batch transformed circuits: .. code-block:: python from functools import partial from pennylane import numpy as np @partial(qml.gradients.param_shift, shifts=[(0.1,)]) @qml.qnode(qml.device('default.qubit', wires=1)) def transformed_circuit(x): qml.RX(x, wires=0) return qml.expval(qml.Z(0)) >>> print(qml.draw(transformed_circuit)(np.array(1.0, requires_grad=True))) 0: ──RX(1.10)─┤ <Z> 0: ──RX(0.90)─┤ <Z> The function also accepts quantum functions rather than QNodes. This can be especially helpful if you want to visualize only a part of a circuit that may not be convertible into a QNode, such as a sub-function that does not return any measurements. >>> def qfunc(x): ... qml.RX(x, wires=[0]) ... qml.CNOT(wires=[0, 1]) >>> print(qml.draw(qfunc)(1.1)) 0: ──RX(1.10)─╭●─┤ 1: ───────────╰X─┤ **Levels:** The ``level`` keyword argument allows one to select a subset of the transforms to apply on the ``QNode`` before carrying out any drawing. Take, for example, this circuit: .. code-block:: python @qml.transforms.merge_rotations @qml.transforms.cancel_inverses @qml.qnode(qml.device("default.qubit"), diff_method="parameter-shift") def circ(weights, order): qml.RandomLayers(weights, wires=(0, 1)) qml.Permute(order, wires=(0, 1, 2)) qml.PauliX(0) qml.PauliX(0) qml.RX(0.1, wires=0) qml.RX(-0.1, wires=0) return qml.expval(qml.PauliX(0)) order = [2, 1, 0] weights = qml.numpy.array([[1.0, 20]]) One can print the circuit without any transforms applied by passing ``level="top"`` or ``level=0``: >>> print(qml.draw(circ, level="top")(weights, order)) 0: ─╭RandomLayers(M0)─╭Permute──X──X──RX(0.10)──RX(-0.10)─┤ <X> 1: ─╰RandomLayers(M0)─├Permute────────────────────────────┤ 2: ───────────────────╰Permute────────────────────────────┤ M0 = [[ 1. 20.]] Or print the circuit after applying the transforms manually applied on the QNode (``merge_rotations`` and ``cancel_inverses``): >>> print(qml.draw(circ, level="user", show_matrices=False)(weights, order)) 0: ─╭RandomLayers(M0)─╭Permute─┤ <X> 1: ─╰RandomLayers(M0)─├Permute─┤ 2: ───────────────────╰Permute─┤ To apply all of the transforms, including those carried out by the differentiation method and the device, use ``level=None``: >>> print(qml.draw(circ, level=None, show_matrices=False)(weights, order)) 0: ──RY(1.00)──╭SWAP─┤ <X> 1: ──RX(20.00)─│─────┤ 2: ────────────╰SWAP─┤ Slices can also be passed to the ``level`` argument. So one can, for example, request that only the ``merge_rotations`` transform is applied: >>> print(qml.draw(circ, level=slice(1, 2), show_matrices=False)(weights, order)) 0: ─╭RandomLayers(M0)─╭Permute──X──X─┤ <X> 1: ─╰RandomLayers(M0)─├Permute───────┤ 2: ───────────────────╰Permute───────┤ """ if catalyst_qjit(qnode): qnode = qnode.user_function if hasattr(qnode, "construct"): return _draw_qnode( qnode, wire_order=wire_order, show_all_wires=show_all_wires, decimals=decimals, max_length=max_length, show_matrices=show_matrices, show_wire_labels=show_wire_labels, level=level, ) if level not in {"gradient", 0, "top"}: # default and no transform options warnings.warn( "When the input to qml.draw is not a QNode, the level argument is ignored.", UserWarning, ) @wraps(qnode) def wrapper(*args, **kwargs): tape = qml.tape.make_qscript(qnode)(*args, **kwargs) if wire_order: _wire_order = wire_order else: try: _wire_order = sorted(tape.wires) except TypeError: _wire_order = tape.wires return tape_text( tape, 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, ) return wrapper
def _draw_qnode( qnode, wire_order=None, show_all_wires=False, decimals=2, max_length=100, show_matrices=True, show_wire_labels=True, level: Union[None, Literal["top", "user", "device", "gradient"], int, slice] = "gradient", ): @wraps(qnode) def wrapper(*args, **kwargs): tapes, _ = qml.workflow.construct_batch(qnode, level=level)(*args, **kwargs) if wire_order: _wire_order = wire_order elif qnode.device.wires: _wire_order = qnode.device.wires else: try: _wire_order = sorted(tapes[0].wires) 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, ) return wrapper
[docs]def draw_mpl( qnode, wire_order=None, show_all_wires=False, decimals=None, style=None, *, fig=None, level: Union[None, Literal["top", "user", "device", "gradient"], int, slice] = "gradient", **kwargs, ): r"""Draw a qnode with matplotlib Args: qnode (.QNode or Callable): the input QNode/quantum function that is to be drawn. wire_order (Sequence[Any]): the order (from top to bottom) to print the wires of the circuit. If not provided, the wire order defaults to the device wires. If device wires are not available, the circuit wires are sorted if possible. show_all_wires (bool): If True, all wires, including empty wires, are printed. decimals (int): How many decimal points to include when formatting operation parameters. Default ``None`` will omit parameters from operation labels. style (str): visual style of plot. Valid strings are ``{'black_white', 'black_white_dark', 'sketch', 'pennylane', 'pennylane_sketch', 'sketch_dark', 'solarized_light', 'solarized_dark', 'default'}``. If no style is specified, the global style set with :func:`~.use_style` will be used, and the initial default is 'black_white'. If you would like to use your environment's current rcParams, set ``style`` to "rcParams". Setting style does not modify matplotlib global plotting settings. Keyword Args: fig (None or matplotlib.Figure): Matplotlib figure to plot onto. If None, then create a new figure fontsize (float or str): fontsize for text. Valid strings are ``{'xx-small', 'x-small', 'small', 'medium', large', 'x-large', 'xx-large'}``. Default is ``14``. wire_options (dict): matplotlib formatting options for the wire lines label_options (dict): matplotlib formatting options for the wire labels show_wire_labels (bool): Whether or not to show the wire labels. active_wire_notches (bool): whether or not to add notches indicating active wires. Defaults to ``True``. level (None, str, int, slice): An indication of what transforms to apply before drawing. Check :func:`~.workflow.get_transform_program` for more information on the allowed values and usage details of this argument. Returns: A function that has the same argument signature as ``qnode``. When called, the function will draw the QNode as a tuple of (``matplotlib.figure.Figure``, ``matplotlib.axes._axes.Axes``) .. warning:: Unlike :func:`~.draw`, this function can not draw the full result of a tape-splitting transform. In such cases, only the tape generated first will be plotted. **Example**: .. code-block:: python dev = qml.device('lightning.qubit', wires=(0,1,2,3)) @qml.qnode(dev) def circuit(x, z): qml.QFT(wires=(0,1,2,3)) qml.IsingXX(1.234, wires=(0,2)) qml.Toffoli(wires=(0,1,2)) mcm = qml.measure(1) mcm_out = qml.measure(2) qml.CSWAP(wires=(0,2,3)) qml.RX(x, wires=0) qml.cond(mcm, qml.RY)(np.pi / 4, wires=3) qml.CRZ(z, wires=(3,0)) return qml.expval(qml.Z(0)), qml.probs(op=mcm_out) fig, ax = qml.draw_mpl(circuit)(1.2345,1.2345) fig.show() .. figure:: ../../_static/draw_mpl/main_example.png :align: center :width: 60% :target: javascript:void(0); .. details:: :title: Usage Details **Decimals:** The keyword ``decimals`` controls how many decimal points to include when labelling the operations. The default value ``None`` omits parameters for brevity. .. code-block:: python @qml.qnode(dev) def circuit2(x, y): qml.RX(x, wires=0) qml.Rot(*y, wires=0) return qml.expval(qml.Z(0)) fig, ax = qml.draw_mpl(circuit2, decimals=2)(1.23456, [1.2345,2.3456,3.456]) fig.show() .. figure:: ../../_static/draw_mpl/decimals.png :align: center :width: 60% :target: javascript:void(0); **Wires:** The keywords ``wire_order`` and ``show_all_wires`` control the location of wires from top to bottom. .. code-block:: python fig, ax = qml.draw_mpl(circuit, wire_order=[3,2,1,0])(1.2345,1.2345) fig.show() .. figure:: ../../_static/draw_mpl/wire_order.png :align: center :width: 60% :target: javascript:void(0); If a wire is in ``wire_order``, but not in the ``tape``, it will be omitted by default. Only by selecting ``show_all_wires=True`` will empty wires be displayed. .. code-block:: python fig, ax = qml.draw_mpl(circuit, wire_order=["aux"], show_all_wires=True)(1.2345,1.2345) fig.show() .. figure:: ../../_static/draw_mpl/show_all_wires.png :align: center :width: 60% :target: javascript:void(0); **Integration with matplotlib:** This function returns matplotlib figure and axes objects. Using these objects, users can perform further customization of the graphic. .. code-block:: python fig, ax = qml.draw_mpl(circuit)(1.2345,1.2345) fig.suptitle("My Circuit", fontsize="xx-large") options = {'facecolor': "white", 'edgecolor': "#f57e7e", "linewidth": 6, "zorder": -1} box1 = plt.Rectangle((-0.5, -0.5), width=3.0, height=4.0, **options) ax.add_patch(box1) ax.annotate("CSWAP", xy=(5, 2.5), xycoords='data', xytext=(5.8,1.5), textcoords='data', arrowprops={'facecolor': 'black'}, fontsize=14) ax.annotate("classical control flow", xy=(3.5, 4.2), xycoords='data', xytext=(0.8,4.2), textcoords='data', arrowprops={'facecolor': 'blue'}, fontsize=14, va="center") fig.show() .. figure:: ../../_static/draw_mpl/postprocessing.png :align: center :width: 60% :target: javascript:void(0); **Formatting:** PennyLane has inbuilt styles for controlling the appearance of the circuit drawings. All available styles can be determined by evaluating ``qml.drawer.available_styles()``. Any available string can then be passed via the kwarg ``style`` to change the settings for that plot. This will not affect style settings for subsequent matplotlib plots. .. code-block:: python fig, ax = qml.draw_mpl(circuit, style='sketch')(1.2345,1.2345) fig.show() .. figure:: ../../_static/draw_mpl/sketch_style.png :align: center :width: 60% :target: javascript:void(0); You can also control the appearance with matplotlib's provided tools, see the `matplotlib docs <https://matplotlib.org/stable/tutorials/introductory/customizing.html>`_ . For example, we can customize ``plt.rcParams``. To use a customized appearance based on matplotlib's ``plt.rcParams``, ``qml.draw_mpl`` must be run with ``style="rcParams"``: .. code-block:: python plt.rcParams['patch.facecolor'] = 'mistyrose' plt.rcParams['patch.edgecolor'] = 'maroon' plt.rcParams['text.color'] = 'maroon' plt.rcParams['font.weight'] = 'bold' plt.rcParams['patch.linewidth'] = 4 plt.rcParams['patch.force_edgecolor'] = True plt.rcParams['lines.color'] = 'indigo' plt.rcParams['lines.linewidth'] = 2 plt.rcParams['figure.facecolor'] = 'ghostwhite' fig, ax = qml.draw_mpl(circuit, style="rcParams")(1.2345,1.2345) fig.show() .. figure:: ../../_static/draw_mpl/rcparams.png :align: center :width: 60% :target: javascript:void(0); The wires and wire labels can be manually formatted by passing in dictionaries of keyword-value pairs of matplotlib options. ``wire_options`` accepts options for lines, and ``label_options`` accepts text options. .. code-block:: python fig, ax = qml.draw_mpl(circuit, wire_options={'color':'teal', 'linewidth': 5}, label_options={'size': 20})(1.2345,1.2345) fig.show() .. figure:: ../../_static/draw_mpl/wires_labels.png :align: center :width: 60% :target: javascript:void(0); **Levels:** The ``level`` keyword argument allows one to select a subset of the transforms to apply on the ``QNode`` before carrying out any drawing. Take, for example, this circuit: .. code-block:: python @qml.transforms.merge_rotations @qml.transforms.cancel_inverses @qml.qnode(qml.device("default.qubit"), diff_method="parameter-shift") def circ(): qml.RandomLayers([[1.0, 20]], wires=(0, 1)) qml.Permute([2, 1, 0], wires=(0, 1, 2)) qml.PauliX(0) qml.PauliX(0) qml.RX(0.1, wires=0) qml.RX(-0.1, wires=0) return qml.expval(qml.PauliX(0)) One can plot the circuit without any transforms applied by passing ``level="top"`` or ``level=0``: .. code-block:: python fig, ax = qml.draw_mpl(circ, level="top")() fig.show() .. figure:: ../../_static/draw_mpl/level_top.png :align: center :width: 60% :target: javascript:void(0); Or plot the circuit after applying the transforms manually applied on the QNode (``merge_rotations`` and ``cancel_inverses``): .. code-block:: python fig, ax = qml.draw_mpl(circ, level="user")() fig.show() .. figure:: ../../_static/draw_mpl/level_user.png :align: center :width: 60% :target: javascript:void(0); To apply all of the transforms, including those carried out by the differentiation method and the device, use ``level=None``: .. code-block:: python fig, ax = qml.draw_mpl(circ, level=None)() fig.show() .. figure:: ../../_static/draw_mpl/level_none.png :align: center :width: 60% :target: javascript:void(0); Slices can also be passed to the ``level`` argument. So one can, for example, request that only the ``merge_rotations`` transform is applied: .. code-block:: python fig, ax = qml.draw_mpl(circ, level=slice(1, 2))() fig.show() .. figure:: ../../_static/draw_mpl/level_slice.png :align: center :width: 60% :target: javascript:void(0); """ if catalyst_qjit(qnode): qnode = qnode.user_function if hasattr(qnode, "construct"): return _draw_mpl_qnode( qnode, wire_order=wire_order, show_all_wires=show_all_wires, decimals=decimals, level=level, style=style, fig=fig, **kwargs, ) if level not in {"gradient", 0, "top"}: # default and no transform options warnings.warn( "When the input to qml.draw is not a QNode, the level argument is ignored.", UserWarning, ) @wraps(qnode) def wrapper(*args, **kwargs): tape = qml.tape.make_qscript(qnode)(*args, **kwargs) if wire_order: _wire_order = wire_order else: try: _wire_order = sorted(tape.wires) except TypeError: _wire_order = tape.wires return tape_mpl( tape, wire_order=_wire_order, show_all_wires=show_all_wires, decimals=decimals, style=style, fig=fig, level=level, **kwargs, ) return wrapper
def _draw_mpl_qnode( qnode, wire_order=None, show_all_wires=False, decimals=None, level="gradient", style="black_white", *, fig=None, **kwargs, ): @wraps(qnode) def wrapper(*args, **kwargs_qnode): tapes, _ = qml.workflow.construct_batch(qnode, level=level)(*args, **kwargs_qnode) if len(tapes) > 1: warnings.warn( "Multiple tapes constructed, but only displaying the first one.", UserWarning ) tape = tapes[0] if wire_order: _wire_order = wire_order elif qnode.device.wires: _wire_order = qnode.device.wires else: try: _wire_order = sorted(tape.wires) except TypeError: _wire_order = tape.wires return tape_mpl( tape, wire_order=_wire_order, show_all_wires=show_all_wires, decimals=decimals, style=style, fig=fig, **kwargs, ) return wrapper