diff --git a/doc/_static/draw_mpl/draw_mpl_examples.py b/doc/_static/draw_mpl/draw_mpl_examples.py index 820539ba63d..ef593fc4af0 100644 --- a/doc/_static/draw_mpl/draw_mpl_examples.py +++ b/doc/_static/draw_mpl/draw_mpl_examples.py @@ -102,7 +102,7 @@ def rcparams(circuit): def use_style(circuit): - fig, ax = qml.draw_mpl(circuit, style='sketch')(1.2345, 1.2345) + fig, ax = qml.draw_mpl(circuit, style="sketch")(1.2345, 1.2345) plt.savefig(folder / "sketch_style.png") plt.close() @@ -128,6 +128,26 @@ def circuit(): plt.close() +@qml.transforms.merge_rotations +@qml.transforms.cancel_inverses +@qml.qnode(qml.device("default.qubit"), diff_method="parameter-shift") +def _levels_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)) + + +def levels(): + for level in ("top", "user", None, slice(1, 2)): + draw_mpl(_levels_circ, level=level)() + plt.savefig(folder / f"level_{str(level).split('(')[0].lower()}.png") + plt.close + + if __name__ == "__main__": dev = qml.device("lightning.qubit", wires=(0, 1, 2, 3)) @@ -151,3 +171,4 @@ def circuit(x, z): rcparams(circuit) wires_labels(circuit) mid_measure() + levels() diff --git a/doc/_static/draw_mpl/level_none.png b/doc/_static/draw_mpl/level_none.png new file mode 100644 index 00000000000..8dbb6bbbd87 Binary files /dev/null and b/doc/_static/draw_mpl/level_none.png differ diff --git a/doc/_static/draw_mpl/level_slice.png b/doc/_static/draw_mpl/level_slice.png new file mode 100644 index 00000000000..33e11d0bb36 Binary files /dev/null and b/doc/_static/draw_mpl/level_slice.png differ diff --git a/doc/_static/draw_mpl/level_top.png b/doc/_static/draw_mpl/level_top.png new file mode 100644 index 00000000000..aaedfb9d098 Binary files /dev/null and b/doc/_static/draw_mpl/level_top.png differ diff --git a/doc/_static/draw_mpl/level_user.png b/doc/_static/draw_mpl/level_user.png new file mode 100644 index 00000000000..9499767d65c Binary files /dev/null and b/doc/_static/draw_mpl/level_user.png differ diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index cbd44014a80..c0ea2eb067b 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -307,6 +307,9 @@ * `qml.qchem.molecular_dipole` function is added for calculating the dipole operator using "dhf" and "openfermion" backends. [(#5764)](https://github.com/PennyLaneAI/pennylane/pull/5764) +* Circuits can now be plotted at any specific point of the transform program through the `level` keyword argument in `draw()` and `draw_mpl()`. + [(#5855)](https://github.com/PennyLaneAI/pennylane/pull/5855) + * Transforms applied to callables now use `functools.wraps` to preserve the docstring and call signature of the original function. [(#5857)](https://github.com/PennyLaneAI/pennylane/pull/5857) diff --git a/pennylane/drawer/draw.py b/pennylane/drawer/draw.py index 3655f9b5cc8..ff6ab2a149d 100644 --- a/pennylane/drawer/draw.py +++ b/pennylane/drawer/draw.py @@ -24,6 +24,24 @@ from .tape_mpl import tape_mpl from .tape_text import tape_text +_level_sentinel = object() + + +def _determine_draw_level(kwargs, qnode=None): + sentinel = _level_sentinel + + level = kwargs.get("level", sentinel) + expansion_strategy = kwargs.get("expansion_strategy", sentinel) + + if all(val != sentinel for val in (level, expansion_strategy)): + raise ValueError("Either 'level' or 'expansion_strategy' need to be set, but not both.") + + if level == sentinel: + if expansion_strategy == sentinel: + return qnode.expansion_strategy if qnode else sentinel + return expansion_strategy + return level + def catalyst_qjit(qnode): """A method checking whether a qnode is compiled by catalyst.qjit""" @@ -37,9 +55,9 @@ def draw( decimals=2, max_length=100, show_matrices=True, - expansion_strategy=None, + **kwargs, ): - """Create a function that draws the given qnode or quantum function. + 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. @@ -51,8 +69,13 @@ def draw( ``None`` will omit parameters from operation labels. max_length (int): Maximum string width (columns) when printing the circuit show_matrices=False (bool): show matrix valued parameters below all circuit diagrams + + Keyword Args: + 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. expansion_strategy (str): The strategy to use when circuit expansions or decompositions - are required. Note that this is ignored if the input is not a QNode. + are required. - ``gradient``: The QNode will attempt to decompose the internal circuit such that all circuit operations are supported by the gradient @@ -61,11 +84,18 @@ def draw( - ``device``: The QNode will attempt to decompose the internal circuit such that all circuit operations are natively supported by the device. - Returns: A function that has the same argument signature as ``qnode``. When called, the function will draw the QNode/qfunc. + .. note:: + + At most, one of ``level`` or ``expansion_strategy`` needs to be provided. If neither is provided, + ``qnode.expansion_strategy`` would be used instead. Users are encouraged to predominantly use ``level``, + as it allows for the same values as ``expansion_strategy``, and allows for more flexibility choosing + the wanted transforms/expansions. + + **Example** .. code-block:: python3 @@ -85,124 +115,175 @@ def circuit(a, w): .. 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─╭●─────────────────────────────────────────────────╭●───────────┤ ╭ + 1: ────╰RX(2.3000)──Rot(1.2000,3.2000,0.7000,"arbitrary")─╰RX(-2.3000)─┤ ╰ + + 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─╭●────────────────────╭●──┤ ╭ + 1: ────╰RX──Rot("arbitrary")─╰RX─┤ ╰ + + 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)─┤ + + 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: - 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─╭●─────────────────────────────────────────────────╭●───────────┤ ╭ - 1: ────╰RX(2.3000)──Rot(1.2000,3.2000,0.7000,"arbitrary")─╰RX(-2.3000)─┤ ╰ - - 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─╭●────────────────────╭●──┤ ╭ - 1: ────╰RX──Rot("arbitrary")─╰RX─┤ ╰ - - 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)─┤ - - 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 - .. code-block:: python + rng = np.random.default_rng(seed=42) + shape = qml.StronglyEntanglingLayers.shape(n_wires=3, n_layers=3) + params = rng.random(shape) - 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)] - @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)(params)) - print(qml.draw(longer_circuit, max_length=60)(params)) + .. code-block:: none - .. code-block:: none + 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─── - 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─┤ + ──╭X────────────────────Rot(0.35,0.97,0.89)─╰X─╭●─│──┤ + ──╰●────────────────────Rot(0.78,0.19,0.47)────╰X─╰●─┤ - ───Rot(0.83,0.63,0.76)──────────────────────╭●────╭X─┤ - ──╭X────────────────────Rot(0.35,0.97,0.89)─╰X─╭●─│──┤ - ──╰●────────────────────Rot(0.78,0.19,0.47)────╰X─╰●─┤ + The ``wire_order`` keyword specifies the order of the wires from + top to bottom: - 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)─╭RX(-2.30)─┤ ╭ + 0: ──H─╰●─────────────────────────────╰●─────────┤ ╰ - >>> 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)─╭RX(-2.30)─┤ ╭ - 0: ──H─╰●─────────────────────────────╰●─────────┤ ╰ + If the device or ``wire_order`` has wires not used by operations, those wires are omitted + unless requested with ``show_all_wires=True`` - 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: ───┤ + 1: ───┤ + 2: ───┤ - >>> 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: ───┤ - 1: ───┤ - 2: ───┤ + Drawing also works on batch transformed circuits: - Drawing also works on batch transformed circuits: + .. code-block:: python - .. code-block:: python + from functools import partial - from functools import partial + @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)) - @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))) - print(qml.draw(transformed_circuit)(np.array(1.0, requires_grad=True))) + .. code-block:: none - .. code-block:: none + 0: ──RX(1.10)─┤ - 0: ──RX(1.10)─┤ + 0: ──RX(0.90)─┤ - 0: ──RX(0.90)─┤ + 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. - 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─┤ - >>> 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)─┤ + 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─┤ + 1: ─╰RandomLayers(M0)─├Permute─┤ + 2: ───────────────────╰Permute─┤ + + To apply all of the transforms, including those carried out by the differentitation method and the device, use ``level=None``: + + >>> print(qml.draw(circ, level=None, show_matrices=False)(weights, order)) + 0: ──RY(1.00)──╭SWAP─┤ + 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─┤ + 1: ─╰RandomLayers(M0)─├Permute───────┤ + 2: ───────────────────╰Permute───────┤ """ if catalyst_qjit(qnode): @@ -216,12 +297,12 @@ def transformed_circuit(x): decimals=decimals, max_length=max_length, show_matrices=show_matrices, - expansion_strategy=expansion_strategy, + level=_determine_draw_level(kwargs, qnode), ) - if expansion_strategy is not None: + if _determine_draw_level(kwargs) != _level_sentinel: warnings.warn( - "When the input to qml.draw is not a QNode, the expansion_strategy argument is ignored.", + "When the input to qml.draw is not a QNode, the expansion_strategy and level arguments are ignored.", UserWarning, ) @@ -256,27 +337,11 @@ def _draw_qnode( decimals=2, max_length=100, show_matrices=True, - expansion_strategy=None, + level=None, ): @wraps(qnode) def wrapper(*args, **kwargs): - if isinstance(qnode.device, qml.devices.Device) and ( - expansion_strategy == "device" or getattr(qnode, "expansion_strategy", None) == "device" - ): - qnode.construct(args, kwargs) - tapes = qnode.transform_program([qnode.tape])[0] - program, _ = qnode.device.preprocess() - tapes = program(tapes)[0] - else: - original_expansion_strategy = getattr(qnode, "expansion_strategy", None) - try: - qnode.expansion_strategy = expansion_strategy or original_expansion_strategy - tapes = qnode.construct(args, kwargs) - program = qnode.transform_program - tapes = program([qnode.tape])[0] - - finally: - qnode.expansion_strategy = original_expansion_strategy + tapes, _ = qml.workflow.construct_batch(qnode, level=level)(*args, **kwargs) if wire_order: _wire_order = wire_order @@ -305,6 +370,8 @@ def wrapper(*args, **kwargs): 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 @@ -328,18 +395,15 @@ def draw_mpl( wire_order=None, show_all_wires=False, decimals=None, - expansion_strategy=None, style=None, *, fig=None, **kwargs, ): - """Draw a qnode with matplotlib + r"""Draw a qnode with matplotlib Args: qnode (.QNode or Callable): the input QNode/quantum function that is to be drawn. - - Keyword Args: 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. @@ -351,6 +415,9 @@ def draw_mpl( 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``. @@ -358,6 +425,9 @@ def draw_mpl( label_options (dict): matplotlib formatting options for 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. expansion_strategy (str): The strategy to use when circuit expansions or decompositions are required. @@ -367,13 +437,24 @@ def draw_mpl( - ``device``: The QNode will attempt to decompose the internal circuit such that all circuit operations are natively supported by the device. - fig (None or matplotlib.Figure): Matplotlib figure to plot onto. If None, then create a new figure 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``) + .. note:: + + At most, one of ``level`` or ``expansion_strategy`` needs to be provided. If neither is provided, + ``qnode.expansion_strategy`` would be used instead. Users are encouraged to predominantly use ``level``, + as it allows for the same values as ``expansion_strategy``, and allows for more flexibility choosing + the wanted transforms/expansions. + + .. 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 @@ -538,24 +619,98 @@ def circuit2(x, y): :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")() + fog.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 differentitation 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"): + resolved_level = _determine_draw_level(kwargs, qnode) + + kwargs.pop("expansion_strategy", None) + kwargs.pop("level", None) + return _draw_mpl_qnode( qnode, wire_order=wire_order, show_all_wires=show_all_wires, decimals=decimals, - expansion_strategy=expansion_strategy, + level=resolved_level, style=style, fig=fig, **kwargs, ) - if expansion_strategy is not None: + if _determine_draw_level(kwargs) != _level_sentinel: warnings.warn( - "When the input to qml.draw is not a QNode, the expansion_strategy argument is ignored.", + "When the input to qml.draw is not a QNode, the expansion_strategy and level arguments are ignored.", UserWarning, ) @@ -588,7 +743,7 @@ def _draw_mpl_qnode( wire_order=None, show_all_wires=False, decimals=None, - expansion_strategy=None, + level=None, style="black_white", *, fig=None, @@ -596,22 +751,14 @@ def _draw_mpl_qnode( ): @wraps(qnode) def wrapper(*args, **kwargs_qnode): - if expansion_strategy == "device" and isinstance(qnode.device, qml.devices.Device): - qnode.construct(args, kwargs) - tapes, _ = qnode.transform_program([qnode.tape]) - program, _ = qnode.device.preprocess() - tapes, _ = program(tapes) - tape = tapes[0] - else: - original_expansion_strategy = getattr(qnode, "expansion_strategy", None) + tapes, _ = qml.workflow.construct_batch(qnode, level=level)(*args, **kwargs_qnode) - try: - qnode.expansion_strategy = expansion_strategy or original_expansion_strategy - qnode.construct(args, kwargs_qnode) - program = qnode.transform_program - [tape], _ = program([qnode.tape]) - finally: - qnode.expansion_strategy = original_expansion_strategy + 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 diff --git a/pennylane/qnn/torch.py b/pennylane/qnn/torch.py index 1b37999f09d..9dff1a97fe8 100644 --- a/pennylane/qnn/torch.py +++ b/pennylane/qnn/torch.py @@ -458,7 +458,7 @@ def construct(self, args, kwargs): x = args[0] kwargs = { self.input_arg: x, - **{arg: weight.data.to(x) for arg, weight in self.qnode_weights.items()}, + **{arg: weight.to(x) for arg, weight in self.qnode_weights.items()}, } self.qnode.construct((), kwargs) diff --git a/pennylane/resource/specs.py b/pennylane/resource/specs.py index 3de1dd31540..bd6536feda7 100644 --- a/pennylane/resource/specs.py +++ b/pennylane/resource/specs.py @@ -121,71 +121,71 @@ def circuit(x, add_ry=True): 'gradient_fn': 'pennylane.gradients.parameter_shift.param_shift', 'num_gradient_executions': 2} - Here you can see how the number of gates and their types change as we apply different amounts of transforms - through the ``level`` argument: + .. details:: + :title: Usage Details - .. code-block:: python3 - - @qml.transforms.merge_rotations - @qml.transforms.undo_swaps - @qml.transforms.cancel_inverses - @qml.qnode(qml.device("default.qubit"), diff_method="parameter-shift", shifts=np.pi / 4) - def circuit(x): - qml.RandomLayers(qml.numpy.array([[1.0, 2.0]]), wires=(0, 1)) - qml.RX(x, wires=0) - qml.RX(-x, wires=0) - qml.SWAP((0, 1)) - qml.X(0) - qml.X(0) - return qml.expval(qml.X(0) + qml.Y(1)) + Here you can see how the number of gates and their types change as we apply different amounts of transforms + through the ``level`` argument: - return circuit + .. code-block:: python3 - First, we can check the resource information of the ``QNode`` without any modifications. Note that ``level=top`` would - return the same results: + @qml.transforms.merge_rotations + @qml.transforms.undo_swaps + @qml.transforms.cancel_inverses + @qml.qnode(qml.device("default.qubit"), diff_method="parameter-shift", shifts=np.pi / 4) + def circuit(x): + qml.RandomLayers(qml.numpy.array([[1.0, 2.0]]), wires=(0, 1)) + qml.RX(x, wires=0) + qml.RX(-x, wires=0) + qml.SWAP((0, 1)) + qml.X(0) + qml.X(0) + return qml.expval(qml.X(0) + qml.Y(1)) - >>> qml.specs(circuit, level=0)(0.1)["resources"] - Resources(num_wires=2, num_gates=6, gate_types=defaultdict(, {'RandomLayers': 1, 'RX': 2, 'SWAP': 1, 'PauliX': 2}), - gate_sizes=defaultdict(, {2: 2, 1: 4}), depth=6, shots=Shots(total_shots=None, shot_vector=())) + First, we can check the resource information of the ``QNode`` without any modifications. Note that ``level=top`` would + return the same results: - We then check the resources after applying all transforms: + >>> qml.specs(circuit, level=0)(0.1)["resources"] + Resources(num_wires=2, num_gates=6, gate_types=defaultdict(, {'RandomLayers': 1, 'RX': 2, 'SWAP': 1, 'PauliX': 2}), + gate_sizes=defaultdict(, {2: 2, 1: 4}), depth=6, shots=Shots(total_shots=None, shot_vector=())) - >>> qml.specs(circuit, level=None)(0.1)["resources"] - Resources(num_wires=2, num_gates=2, gate_types=defaultdict(, {'RY': 1, 'RX': 1}), - gate_sizes=defaultdict(, {1: 2}), depth=1, shots=Shots(total_shots=None, shot_vector=())) + We then check the resources after applying all transforms: - We can also notice that ``SWAP`` and ``PauliX`` are not present in the circuit if we set ``level=2``: + >>> qml.specs(circuit, level=None)(0.1)["resources"] + Resources(num_wires=2, num_gates=2, gate_types=defaultdict(, {'RY': 1, 'RX': 1}), + gate_sizes=defaultdict(, {1: 2}), depth=1, shots=Shots(total_shots=None, shot_vector=())) - >>> qml.specs(circuit, level=2)(0.1)["resources"] - Resources(num_wires=2, num_gates=3, gate_types=defaultdict(, {'RandomLayers': 1, 'RX': 2}), - gate_sizes=defaultdict(, {2: 1, 1: 2}), depth=3, shots=Shots(total_shots=None, shot_vector=())) + We can also notice that ``SWAP`` and ``PauliX`` are not present in the circuit if we set ``level=2``: - If we attempt to only apply the ``merge_rotations`` transform, we would end with only one trainable object, which is in ``RandomLayers``: + >>> qml.specs(circuit, level=2)(0.1)["resources"] + Resources(num_wires=2, num_gates=3, gate_types=defaultdict(, {'RandomLayers': 1, 'RX': 2}), + gate_sizes=defaultdict(, {2: 1, 1: 2}), depth=3, shots=Shots(total_shots=None, shot_vector=())) - >>> qml.specs(circuit, level=slice(2, 3))(0.1)["num_trainable_params"] - 1 + If we attempt to only apply the ``merge_rotations`` transform, we would end with only one trainable object, which is in ``RandomLayers``: - However, if we apply all transforms, ``RandomLayers`` would be decomposed to an ``RY`` and an ``RX``, giving us two trainable objects: + >>> qml.specs(circuit, level=slice(2, 3))(0.1)["num_trainable_params"] + 1 - >>> qml.specs(circuit, level=None)(0.1)["num_trainable_params"] - 2 + However, if we apply all transforms, ``RandomLayers`` would be decomposed to an ``RY`` and an ``RX``, giving us two trainable objects: - If a ``QNode`` with a tape-splitting transform is supplied to the function, with the transform included in the desired transforms, a dictionary - would be returned for each resulting tapes: + >>> qml.specs(circuit, level=None)(0.1)["num_trainable_params"] + 2 - .. code-block:: python3 + If a ``QNode`` with a tape-splitting transform is supplied to the function, with the transform included in the desired transforms, a dictionary + would be returned for each resulting tapes: - H = qml.Hamiltonian([0.2, -0.543], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Y(2)]) + .. code-block:: python3 + H = qml.Hamiltonian([0.2, -0.543], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Y(2)]) - @qml.transforms.hamiltonian_expand - @qml.qnode(qml.device("default.qubit"), diff_method="parameter-shift", shifts=np.pi / 4) - def circuit(): - qml.RandomLayers(qml.numpy.array([[1.0, 2.0]]), wires=(0, 1)) - return qml.expval(H) + @qml.transforms.hamiltonian_expand + @qml.qnode(qml.device("default.qubit"), diff_method="parameter-shift", shifts=np.pi / 4) + def circuit(): + qml.RandomLayers(qml.numpy.array([[1.0, 2.0]]), wires=(0, 1)) + return qml.expval(H) - >>> len(qml.specs(circuit, level="user")()) - 2 + >>> len(qml.specs(circuit, level="user")()) + 2 """ specs_level = _determine_spec_level(kwargs, qnode) diff --git a/pennylane/workflow/construct_batch.py b/pennylane/workflow/construct_batch.py index 4379ab7ff53..786cd21fdc1 100644 --- a/pennylane/workflow/construct_batch.py +++ b/pennylane/workflow/construct_batch.py @@ -15,6 +15,7 @@ """ import inspect +from contextlib import nullcontext from functools import wraps from typing import Callable, Literal, Optional, Tuple, Union @@ -54,16 +55,20 @@ def wrapped_expand_fn(tape, *args, **kwargs): def _get_full_transform_program(qnode: QNode) -> "qml.transforms.core.TransformProgram": program = qml.transforms.core.TransformProgram(qnode.transform_program) + if getattr(qnode.gradient_fn, "expand_transform", False): program.add_transform( qml.transform(qnode.gradient_fn.expand_transform), **qnode.gradient_kwargs, ) + if isinstance(qnode.device, qml.devices.Device): config = _make_execution_config(qnode, qnode.gradient_fn) return program + qnode.device.preprocess(config)[0] + program.add_transform(qml.transform(qnode.device.batch_transform)) program.add_transform(expand_fn_transform(qnode.device.expand_fn)) + return program @@ -316,28 +321,37 @@ def batch_constructor(*args, **kwargs) -> Tuple[Tuple["qml.tape.QuantumTape", Ca else: shots = kwargs.pop("shots", _get_device_shots(qnode.device)) + context_fn = nullcontext + if isinstance(qnode, qml.qnn.KerasLayer): # pylint: disable=import-outside-toplevel import tensorflow as tf - with tf.GradientTape() as tape: - tape.watch(list(qnode.qnode_weights.values())) - - kwargs = { - **{k: 1.0 * w for k, w in qnode.qnode_weights.items()}, - **kwargs, - } + context_fn = tf.GradientTape if isinstance(qnode, qml.qnn.TorchLayer): x = args[0] kwargs = { - **{arg: weight.data.to(x) for arg, weight in qnode.qnode_weights.items()}, + **{arg: weight.to(x) for arg, weight in qnode.qnode_weights.items()}, } - initial_tape = qml.tape.make_qscript(qnode.func, shots=shots)(*args, **kwargs) + with context_fn() as cntxt: + # If TF tape, use the watch function + if hasattr(cntxt, "watch"): + cntxt.watch(list(qnode.qnode_weights.values())) + + kwargs = { + **{k: 1.0 * w for k, w in qnode.qnode_weights.items()}, + **kwargs, + } + + initial_tape = qml.tape.make_qscript(qnode.func, shots=shots)(*args, **kwargs) + params = initial_tape.get_parameters(trainable_only=False) + initial_tape.trainable_params = qml.math.get_trainable_indices(params) qnode._update_gradient_fn(tape=initial_tape) program = get_transform_program(qnode, level=level) + return program((initial_tape,)) return batch_constructor diff --git a/tests/drawer/test_draw.py b/tests/drawer/test_draw.py index f0975725979..83e153b4401 100644 --- a/tests/drawer/test_draw.py +++ b/tests/drawer/test_draw.py @@ -20,7 +20,7 @@ import pytest import pennylane as qml -from pennylane import numpy as np +from pennylane import numpy as pnp from pennylane.drawer import draw @@ -128,7 +128,7 @@ def test_qml_numpy_parameters(self): """Test numpy parameters display as normal numbers.""" expected = " 0: ──RX(1.00)─┤ \n a: ──RY(2.00)─┤ \n1.234: ──RZ(3.00)─┤ " - assert draw(circuit)(np.array(1), np.array(2), np.array(3)) == expected + assert draw(circuit)(pnp.array(1), pnp.array(2), pnp.array(3)) == expected @pytest.mark.torch def test_torch_parameters(self): @@ -174,8 +174,8 @@ def test_matrix_parameters(self): @qml.qnode(qml.device("default.qubit", wires=2)) def matrices_circuit(): qml.StatePrep([1.0, 0.0, 0.0, 0.0], wires=(0, 1)) - qml.QubitUnitary(np.eye(2), wires=0) - return qml.expval(qml.Hermitian(np.eye(2), wires=0)) + qml.QubitUnitary(pnp.eye(2), wires=0) + return qml.expval(qml.Hermitian(pnp.eye(2), wires=0)) expected1 = "0: ─╭|Ψ⟩──U(M0)─┤ <𝓗(M0)>\n1: ─╰|Ψ⟩────────┤ " @@ -196,9 +196,9 @@ def test_matrix_parameters_batch_transform(self): @qml.qnode(qml.device("default.qubit", wires=2)) def matrices_circuit(x): qml.StatePrep([1.0, 0.0, 0.0, 0.0], wires=(0, 1)) - qml.QubitUnitary(np.eye(2, requires_grad=False), wires=0) + qml.QubitUnitary(pnp.eye(2, requires_grad=False), wires=0) qml.RX(x, wires=1) - return qml.expval(qml.Hermitian(np.eye(2, requires_grad=False), wires=1)) + return qml.expval(qml.Hermitian(pnp.eye(2, requires_grad=False), wires=1)) expected1 = ( "0: ─╭|Ψ⟩──U(M0)────┤ \n" @@ -207,7 +207,7 @@ def matrices_circuit(x): "1: ─╰|Ψ⟩──RX(0.80)─┤ <𝓗(M0)>\n\n" "M0 = \n[[1. 0.]\n [0. 1.]]" ) - output = draw(matrices_circuit)(np.array(1.0, requires_grad=True)) + output = draw(matrices_circuit, level="gradient")(pnp.array(1.0, requires_grad=True)) assert output == expected1 expected2 = ( @@ -216,7 +216,7 @@ def matrices_circuit(x): "0: ─╭|Ψ⟩──U(M0)────┤ \n" "1: ─╰|Ψ⟩──RX(0.80)─┤ <𝓗(M0)>" ) - output = draw(matrices_circuit, show_matrices=False)(np.array(1.0, requires_grad=True)) + output = draw(matrices_circuit, show_matrices=False)(pnp.array(1.0, requires_grad=True)) assert output == expected2 @@ -404,7 +404,7 @@ def circ(weights): qml.MultiRZ(0.5, [0, 2]) return qml.expval(qml.PauliZ(2)) - drawing = qml.draw(circ)(np.array([np.pi, 3.124, 0.456])) + drawing = qml.draw(circ)(pnp.array([pnp.pi, 3.124, 0.456])) expected_drawing = ( "0: ──RX(3.14)──┤↗│ │0⟩─╭●─────────────────────╭MultiRZ(0.50)─┤ \n" "1: ──RX(3.12)──┤↗├──────│─────────────╭●───────│──────────────┤ \n" @@ -423,7 +423,7 @@ def circ(phi): m0 = qml.measure(0) qml.cond(m0, qml.PauliX)(wires=1) - drawing = qml.draw(circ)(np.pi) + drawing = qml.draw(circ)(pnp.pi) expected_drawing = ( "0: ──RX(3.14)──┤↗├────┤ \n1: ─────────────║───X─┤ \n ╚═══╝ " ) @@ -440,7 +440,7 @@ def circ(phi, theta): qml.RY(theta, 2) qml.cond(m0, qml.CNOT)(wires=[1, 0]) - drawing = qml.draw(circ)(np.pi, np.pi / 2) + drawing = qml.draw(circ)(pnp.pi, pnp.pi / 2) expected_drawing = ( "0: ──RX(3.14)──┤↗├───────────╭X─┤ \n" "1: ─────────────║────────────╰●─┤ \n" @@ -877,6 +877,103 @@ def circ(): assert drawing == expected_drawing +class TestLevelExpansionStrategy: + @pytest.fixture( + params=[qml.device("default.qubit.legacy", wires=3), qml.devices.DefaultQubit()], + ) + def transforms_circuit(self, request): + @qml.transforms.merge_rotations + @qml.transforms.cancel_inverses + @qml.qnode(request.param, 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)) + + return circ + + @pytest.mark.parametrize( + "var1,var2,expected", + [ + ( + 0, + "top", + "0: ─╭RandomLayers(M0)─╭Permute──X──X──RX(0.10)──RX(-0.10)─┤ \n" + "1: ─╰RandomLayers(M0)─├Permute────────────────────────────┤ \n" + "2: ───────────────────╰Permute────────────────────────────┤ ", + ), + ( + 2, + "user", + "0: ─╭RandomLayers(M0)─╭Permute─┤ \n" + "1: ─╰RandomLayers(M0)─├Permute─┤ \n" + "2: ───────────────────╰Permute─┤ ", + ), + ( + 3, + "gradient", + "0: ──RY(1.00)──╭Permute─┤ \n" + "1: ──RX(20.00)─├Permute─┤ \n" + "2: ────────────╰Permute─┤ ", + ), + ( + 8, + "device", + "0: ──RY(1.00)──╭SWAP─┤ \n" + "1: ──RX(20.00)─│─────┤ \n" + "2: ────────────╰SWAP─┤ ", + ), + ], + ) + def test_equivalent_levels(self, transforms_circuit, var1, var2, expected): + order = [2, 1, 0] + weights = pnp.array([[1.0, 20]]) + + out1 = qml.draw(transforms_circuit, level=var1, show_matrices=False)(weights, order) + out2 = qml.draw(transforms_circuit, level=var2, show_matrices=False)(weights, order) + + assert out1 == out2 == expected + + def test_draw_at_level_1(self, transforms_circuit): + """Test that at level one the first transform has been applied, cancelling inverses.""" + + order = [2, 1, 0] + weights = pnp.array([[1.0, 20]]) + + out = qml.draw(transforms_circuit, level=1, show_matrices=False)(weights, order) + + expected = ( + "0: ─╭RandomLayers(M0)─╭Permute──RX(0.10)──RX(-0.10)─┤ \n" + "1: ─╰RandomLayers(M0)─├Permute──────────────────────┤ \n" + "2: ───────────────────╰Permute──────────────────────┤ " + ) + assert out == expected + + def test_draw_with_qfunc_warns_with_expansion_strategy(self): + """Test that draw warns the user about expansion_strategy being ignored.""" + + def qfunc(): + qml.PauliZ(0) + + with pytest.warns( + UserWarning, match="the expansion_strategy and level arguments are ignored" + ): + _ = qml.draw(qfunc, expansion_strategy="gradient") + + with pytest.warns( + UserWarning, match="the expansion_strategy and level arguments are ignored" + ): + _ = qml.draw(qfunc, level="gradient") + + def test_providing_both_level_and_expansion_raises_error(self, transforms_circuit): + with pytest.raises(ValueError, match="Either 'level' or 'expansion_strategy'"): + qml.draw(transforms_circuit, level=0, expansion_strategy="device") + + def test_draw_batch_transform(): """Test that drawing a batch transform works correctly.""" @@ -888,7 +985,7 @@ def circ(x): return qml.expval(qml.PauliZ(0)) expected = "0: ──H──RX(0.8)─┤ \n\n0: ──H──RX(0.4)─┤ " - assert draw(circ, decimals=1)(np.array(0.6, requires_grad=True)) == expected + assert draw(circ, decimals=1)(pnp.array(0.6, requires_grad=True)) == expected @pytest.mark.skip("Nested tapes are being deprecated") @@ -918,27 +1015,6 @@ def circ(): assert draw(circ)() == expected -@pytest.mark.parametrize( - "device", - [qml.device("default.qubit.legacy", wires=2), qml.devices.DefaultQubit(wires=2)], -) -def test_expansion_strategy(device): - """Test expansion strategy keyword modifies tape expansion.""" - - H = qml.PauliX(0) + qml.PauliZ(1) + 0.5 * qml.PauliX(0) @ qml.PauliX(1) - - @qml.qnode(device) - def circ(t): - qml.ApproxTimeEvolution(H, t, 2) - return qml.probs(wires=0) - - expected_gradient = "0: ─╭ApproxTimeEvolution─┤ Probs\n1: ─╰ApproxTimeEvolution─┤ " - assert draw(circ, expansion_strategy="gradient", decimals=None)(0.5) == expected_gradient - - expected_device = "0: ──RX─╭RXX──RX─╭RXX─┤ Probs\n1: ──RZ─╰RXX──RZ─╰RXX─┤ " - assert draw(circ, expansion_strategy="device", decimals=None)(0.5) == expected_device - - @pytest.mark.parametrize( "device", [qml.device("default.qubit.legacy", wires=2), qml.device("default.qubit", wires=2)], @@ -983,16 +1059,6 @@ def qfunc(x): assert qml.draw(qfunc)(1.1) == "0: ──RX(1.10)─╭●─┤ \n1: ───────────╰X─┤ " -def test_draw_with_qfunc_warns_with_expansion_strategy(): - """Test that draw warns the user about expansion_strategy being ignored.""" - - def qfunc(): - qml.PauliZ(0) - - with pytest.warns(UserWarning, match="the expansion_strategy argument is ignored"): - _ = qml.draw(qfunc, expansion_strategy="gradient") - - @pytest.mark.parametrize("use_qnode", [True, False]) def test_sort_wires(use_qnode): """Test that drawing a qnode with no wire order or device wires sorts the wires automatically.""" diff --git a/tests/drawer/test_draw_mpl.py b/tests/drawer/test_draw_mpl.py index b7a7e91a301..2a8d2c83330 100644 --- a/tests/drawer/test_draw_mpl.py +++ b/tests/drawer/test_draw_mpl.py @@ -21,6 +21,7 @@ import pytest import pennylane as qml +from pennylane import numpy as pnp mpl = pytest.importorskip("matplotlib") plt = pytest.importorskip("matplotlib.pyplot") @@ -79,26 +80,113 @@ def test_fig_argument(): assert output_fig == fig -@pytest.mark.parametrize( - "device", - [qml.device("default.qubit.legacy", wires=3), qml.devices.DefaultQubit(wires=3)], -) -@pytest.mark.parametrize( - "strategy, initial_strategy, n_lines", [("gradient", "device", 3), ("device", "gradient", 13)] -) -def test_expansion_strategy(device, strategy, initial_strategy, n_lines): - """Test that the expansion strategy keyword controls what operations are drawn.""" +class TestLevelExpansionStrategy: + @pytest.fixture( + params=[qml.device("default.qubit.legacy", wires=3), qml.devices.DefaultQubit()], + ) + def transforms_circuit(self, request): + @qml.transforms.merge_rotations + @qml.transforms.cancel_inverses + @qml.qnode(request.param, 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)) + + return circ - @qml.qnode(device, expansion_strategy=initial_strategy) - def circuit(): - qml.Permute([2, 0, 1], wires=(0, 1, 2)) - return qml.expval(qml.PauliZ(0)) + @pytest.mark.parametrize( + "levels,expected_metadata", + [ + ((0, "top"), (3, 9, 9)), + ((2, "user"), (3, 5, 5)), + ((3, "gradient"), (3, 6, 6)), + ((8, "device"), (8, 5, 5)), + ], + ) + def test_equivalent_levels(self, transforms_circuit, levels, expected_metadata): + """Test that the expansion strategy keyword controls what operations are drawn.""" + var1, var2 = levels + expected_lines, expected_patches, expected_texts = expected_metadata - _, ax = qml.draw_mpl(circuit, expansion_strategy=strategy)() + order = [2, 1, 0] + weights = pnp.array([[1.0, 20]]) - assert len(ax.lines) == n_lines - assert circuit.expansion_strategy == initial_strategy - plt.close() + _, ax1 = qml.draw_mpl(transforms_circuit, level=var1)(weights, order) + _, ax2 = qml.draw_mpl(transforms_circuit, level=var2)(weights, order) + + assert len(ax1.lines) == len(ax2.lines) == expected_lines + assert len(ax1.patches) == len(ax2.patches) == expected_patches + assert len(ax1.texts) == len(ax2.texts) == expected_texts + + plt.close("all") + + @pytest.mark.parametrize( + "device", + [qml.device("default.qubit.legacy", wires=3), qml.devices.DefaultQubit(wires=3)], + ) + @pytest.mark.parametrize( + "strategy, initial_strategy, n_lines", + [("gradient", "device", 3), ("device", "gradient", 13)], + ) + def test_expansion_strategy(self, device, strategy, initial_strategy, n_lines): + """Test that the expansion strategy keyword controls what operations are drawn.""" + + @qml.qnode(device, expansion_strategy=initial_strategy) + def circuit(): + qml.Permute([2, 0, 1], wires=(0, 1, 2)) + return qml.expval(qml.PauliZ(0)) + + _, ax = qml.draw_mpl(circuit, expansion_strategy=strategy)() + + assert len(ax.lines) == n_lines + assert circuit.expansion_strategy == initial_strategy + plt.close() + + def test_draw_at_level_1(self, transforms_circuit): + """Test that at level one the first transform has been applied, cancelling inverses.""" + + order = [2, 1, 0] + weights = pnp.array([[1.0, 20]]) + + _, ax = qml.draw_mpl(transforms_circuit, level=1)(weights, order) + + assert len(ax.lines) == 3 + assert len(ax.patches) == 7 + assert len(ax.texts) == 7 + + def test_providing_both_level_and_expansion_raises_error(self, transforms_circuit): + with pytest.raises(ValueError, match="Either 'level' or 'expansion_strategy'"): + qml.draw_mpl(transforms_circuit, level=0, expansion_strategy="device") + + def test_draw_with_qfunc_warns_with_expansion_strategy(self): + """Test that draw warns the user about expansion_strategy being ignored.""" + + def qfunc(): + qml.PauliZ(0) + + with pytest.warns( + UserWarning, match="the expansion_strategy and level arguments are ignored" + ): + qml.draw_mpl(qfunc, expansion_strategy="gradient") + + with pytest.warns( + UserWarning, match="the expansion_strategy and level arguments are ignored" + ): + qml.draw_mpl(qfunc, level="gradient") + + def test_split_tapes_raises_warning(self): + @qml.transforms.split_non_commuting + @qml.qnode(qml.device("default.qubit", wires=2)) + def circuit(): + return [qml.expval(qml.X(0)), qml.expval(qml.Z(0))] + + with pytest.warns(UserWarning, match="Multiple tapes constructed"): + qml.draw_mpl(circuit)() class TestKwargs: @@ -357,7 +445,7 @@ def test_draw_mpl_with_qfunc_warns_with_expansion_strategy(): def qfunc(): qml.PauliZ(0) - with pytest.warns(UserWarning, match="the expansion_strategy argument is ignored"): + with pytest.warns(UserWarning, match="the expansion_strategy and level arguments are ignored"): _ = qml.draw_mpl(qfunc, expansion_strategy="gradient") diff --git a/tests/interfaces/default_qubit_2_integration/test_tensorflow_qnode_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_tensorflow_qnode_default_qubit_2.py index 24aadead8ee..e3c4597ae06 100644 --- a/tests/interfaces/default_qubit_2_integration/test_tensorflow_qnode_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_tensorflow_qnode_default_qubit_2.py @@ -156,10 +156,10 @@ def circuit(p1, p2=y, **kwargs): qml.RY(p2[0] * p2[1], wires=1) qml.RX(kwargs["p3"], wires=0) qml.CNOT(wires=[0, 1]) - return qml.state() + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) result = qml.draw(circuit)(p1=x, p3=z) - expected = "0: ──RX(0.10)──RX(0.40)─╭●─┤ State\n1: ──RY(0.06)───────────╰X─┤ State" + expected = "0: ──RX(0.10)──RX(0.40)─╭●─┤ \n1: ──RY(0.06)───────────╰X─┤ " assert result == expected def test_jacobian(self, dev, diff_method, grad_on_execution, device_vjp, tol, interface): diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 68d3e5a09d0..af8d457cd9e 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -538,6 +538,22 @@ def test_compute_output_shape_2(self, get_circuit, output_dim): # pylint: disab output_shape = layer.compute_output_shape(inputs_shape) assert output_shape.as_list() == [None, 1] + @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(3)) + def test_construct(self, get_circuit, n_qubits, output_dim): + """Test that the construct method builds the correct tape with correct differentiability""" + c, w = get_circuit + layer = KerasLayer(c, w, output_dim) + + x = tf.ones((1, n_qubits)) + + layer.construct((x,), {}) + + assert layer.tape is not None + assert ( + len(layer.tape.get_parameters(trainable_only=False)) + == len(layer.tape.get_parameters(trainable_only=True)) + 1 + ) + @pytest.mark.all_interfaces @pytest.mark.parametrize("interface", ["autograd", "jax", "torch"]) @@ -957,7 +973,7 @@ def circuit(inputs, w1, w2): assert info["num_diagonalizing_gates"] == 0 assert info["num_device_wires"] == 3 assert info["num_tape_wires"] == 2 - assert info["num_trainable_params"] == 3 + assert info["num_trainable_params"] == 2 assert info["interface"] == "tf" assert info["device_name"] == "default.qubit" diff --git a/tests/qnn/test_qnn_torch.py b/tests/qnn/test_qnn_torch.py index aae611eab21..a25ee7d6949 100644 --- a/tests/qnn/test_qnn_torch.py +++ b/tests/qnn/test_qnn_torch.py @@ -564,6 +564,22 @@ def test_gradients(self, get_circuit, n_qubits): # pylint: disable=no-self-use assert torch.allclose(g1, g2) assert len(weights) == len(list(layer.parameters())) + @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(3)) + def test_construct(self, get_circuit, n_qubits): + """Test that the construct method builds the correct tape with correct differentiability""" + c, w = get_circuit + layer = TorchLayer(c, w) + + x = torch.ones(n_qubits) + + layer.construct((x,), {}) + + assert layer.tape is not None + assert ( + len(layer.tape.get_parameters(trainable_only=False)) + == len(layer.tape.get_parameters(trainable_only=True)) + 1 + ) + @pytest.mark.parametrize( "num_qubits, weight_shapes", @@ -945,6 +961,6 @@ def circuit(inputs, w1, w2): assert info["num_diagonalizing_gates"] == 0 assert info["num_device_wires"] == 3 assert info["num_tape_wires"] == 2 - assert info["num_trainable_params"] == 3 + assert info["num_trainable_params"] == 2 assert info["interface"] == "torch" assert info["device_name"] == "default.qubit" diff --git a/tests/resource/test_specs.py b/tests/resource/test_specs.py index b5ffc2f0d44..2dd69be8dd3 100644 --- a/tests/resource/test_specs.py +++ b/tests/resource/test_specs.py @@ -19,7 +19,7 @@ import pytest import pennylane as qml -from pennylane import numpy as np +from pennylane import numpy as pnp class TestSpecsTransform: @@ -29,7 +29,7 @@ def sample_circuit(self): @qml.transforms.merge_rotations @qml.transforms.undo_swaps @qml.transforms.cancel_inverses - @qml.qnode(qml.device("default.qubit"), diff_method="parameter-shift", shifts=np.pi / 4) + @qml.qnode(qml.device("default.qubit"), diff_method="parameter-shift", shifts=pnp.pi / 4) def circuit(x): qml.RandomLayers(qml.numpy.array([[1.0, 2.0]]), wires=(0, 1)) qml.RX(x, wires=0) @@ -68,7 +68,7 @@ def test_disallow_pos_args(self): @pytest.mark.parametrize( "level,expected_gates,exptected_train_params", - [(0, 6, 3), (1, 4, 3), (2, 3, 3), (3, 1, 1), (None, 2, 2)], + [(0, 6, 1), (1, 4, 3), (2, 3, 3), (3, 1, 1), (None, 2, 2)], ) def test_int_specs_level(self, level, expected_gates, exptected_train_params): circ = self.sample_circuit() @@ -152,8 +152,8 @@ def circuit(x, y, add_RY=True): qml.RY(x[4], wires=1) return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1)) - x = np.array([0.05, 0.1, 0.2, 0.3, 0.5], requires_grad=True) - y = np.array(0.1, requires_grad=False) + x = pnp.array([0.05, 0.1, 0.2, 0.3, 0.5], requires_grad=True) + y = pnp.array(0.1, requires_grad=False) info = qml.specs(circuit)(x, y, add_RY=False) @@ -170,7 +170,7 @@ def circuit(x, y, add_RY=True): assert info["num_diagonalizing_gates"] == 1 assert info["num_device_wires"] == 4 assert info["diff_method"] == diff_method - assert info["num_trainable_params"] == 5 + assert info["num_trainable_params"] == 4 assert info["device_name"] == dev.name assert info["level"] == "gradient" @@ -205,7 +205,7 @@ def test_splitting_transforms(self): @qml.transforms.hamiltonian_expand @qml.transforms.merge_rotations - @qml.qnode(qml.device("default.qubit"), diff_method="parameter-shift", shifts=np.pi / 4) + @qml.qnode(qml.device("default.qubit"), diff_method="parameter-shift", shifts=pnp.pi / 4) def circuit(x): qml.RandomLayers(qml.numpy.array([[1.0, 2.0]]), wires=(0, 1)) qml.RX(x, wires=0) @@ -215,11 +215,11 @@ def circuit(x): qml.X(0) return qml.expval(H) - specs_instance = qml.specs(circuit, level=1)(np.array([1.23, -1])) + specs_instance = qml.specs(circuit, level=1)(pnp.array([1.23, -1])) assert isinstance(specs_instance, dict) - specs_list = qml.specs(circuit, level=2)(np.array([1.23, -1])) + specs_list = qml.specs(circuit, level=2)(pnp.array([1.23, -1])) assert len(specs_list) == len(H) @@ -244,7 +244,7 @@ def circuit(params): return qml.expval(qml.PauliZ(0)) params_shape = qml.BasicEntanglerLayers.shape(n_layers=n_layers, n_wires=n_wires) - rng = np.random.default_rng(seed=10) + rng = pnp.random.default_rng(seed=10) params = rng.standard_normal(params_shape) # pylint:disable=no-member return circuit, params @@ -350,7 +350,7 @@ def circuit(): dev_specs = qml.specs(circuit, level="device")() assert "SpectralNormError" in top_specs["errors"] - assert np.allclose(top_specs["errors"]["SpectralNormError"].error, 13.824) + assert pnp.allclose(top_specs["errors"]["SpectralNormError"].error, 13.824) # At the device level, approximations don't exist anymore and therefore # we should expect an empty errors dictionary. diff --git a/tests/workflow/test_construct_batch.py b/tests/workflow/test_construct_batch.py index e40e5b50a77..3bf0a617ca1 100644 --- a/tests/workflow/test_construct_batch.py +++ b/tests/workflow/test_construct_batch.py @@ -243,9 +243,7 @@ def test_level_zero(self): ] expected = qml.tape.QuantumScript( - expected_ops, - [qml.expval(qml.PauliX(0))], - shots=10, + expected_ops, [qml.expval(qml.PauliX(0))], shots=10, trainable_params=[] ) qml.assert_equal(batch[0], expected) @@ -419,8 +417,9 @@ def circuit(x): assert len(batch) == 1 expected = qml.tape.QuantumScript( - [qml.RX(0.5, 0), qml.RX(0.5, 0)], [qml.expval(qml.PauliZ(0))] + [qml.RX(0.5, 0), qml.RX(0.5, 0)], [qml.expval(qml.PauliZ(0))], trainable_params=[] ) + qml.assert_equal(batch[0], expected) assert fn(("a",)) == ("a",)