From a0871e54dac5dbb12ed734d81957c4c0080af080 Mon Sep 17 00:00:00 2001 From: Ahmed Darwish Date: Wed, 19 Jun 2024 16:44:09 -0400 Subject: [PATCH] Incorporate `level` keyword in `draw` and `draw_mpl` (#5855) **Context:** Currently, `draw()` and `draw_mpl()` can only be requested after applying the full transform program, with the exception of the stages provided through expansion_strategy. **Description of the Change:** Using the new `level` argument from construct_batch, the functions are adapted to make use of the argument as well, which allows for more flexible requests and ability to pinpoint where exactly in the transform program to draw a circuit. As done before, the new functionality works with transforms that split the tape (only in the case for `draw`. For `draw_mpl`, a warning is raised and only the first tape is plotted). **Benefits:** Better plotting UX. **Note for Reviewers:** Minor bugs in `construct_batch` have been discovered during work on this PR, and so expect minor fixes to tests relating to `specs`. [[sc-53735](https://app.shortcut.com/xanaduai/story/53735)] Supersedes #5139 --------- Co-authored-by: Mudit Pandey Co-authored-by: Thomas R. Bromley <49409390+trbromley@users.noreply.github.com> --- doc/_static/draw_mpl/draw_mpl_examples.py | 23 +- doc/_static/draw_mpl/level_none.png | Bin 0 -> 11144 bytes doc/_static/draw_mpl/level_slice.png | Bin 0 -> 15527 bytes doc/_static/draw_mpl/level_top.png | Bin 0 -> 17005 bytes doc/_static/draw_mpl/level_user.png | Bin 0 -> 14059 bytes doc/releases/changelog-dev.md | 3 + pennylane/drawer/draw.py | 437 ++++++++++++------ pennylane/qnn/torch.py | 2 +- pennylane/resource/specs.py | 94 ++-- pennylane/workflow/construct_batch.py | 32 +- tests/drawer/test_draw.py | 152 ++++-- tests/drawer/test_draw_mpl.py | 124 ++++- .../test_tensorflow_qnode_default_qubit_2.py | 4 +- tests/qnn/test_keras.py | 18 +- tests/qnn/test_qnn_torch.py | 18 +- tests/resource/test_specs.py | 22 +- tests/workflow/test_construct_batch.py | 7 +- 17 files changed, 653 insertions(+), 283 deletions(-) create mode 100644 doc/_static/draw_mpl/level_none.png create mode 100644 doc/_static/draw_mpl/level_slice.png create mode 100644 doc/_static/draw_mpl/level_top.png create mode 100644 doc/_static/draw_mpl/level_user.png 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 0000000000000000000000000000000000000000..8dbb6bbbd87e9d7a80720bf907dd70f8da39b306 GIT binary patch literal 11144 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 literal 0 HcmV?d00001 diff --git a/doc/_static/draw_mpl/level_top.png b/doc/_static/draw_mpl/level_top.png new file mode 100644 index 0000000000000000000000000000000000000000..aaedfb9d0980bfc9ed74e17aeaba4d95d8ec2b6f GIT binary patch literal 17005 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 F{{cSns)qmo literal 0 HcmV?d00001 diff --git a/doc/_static/draw_mpl/level_user.png b/doc/_static/draw_mpl/level_user.png new file mode 100644 index 0000000000000000000000000000000000000000..9499767d65c46f121ff802e6423fafeaa1e1d874 GIT binary patch literal 14059 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
s0 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 literal 0 HcmV?d00001 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",)