From a0871e54dac5dbb12ed734d81957c4c0080af080 Mon Sep 17 00:00:00 2001 From: Ahmed Darwish Date: Wed, 19 Jun 2024 16:44:09 -0400 Subject: [PATCH 1/2] 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",) From 248a808a35419caa2b89e4379e2fb3edd11f549b Mon Sep 17 00:00:00 2001 From: Utkarsh Date: Wed, 19 Jun 2024 17:27:26 -0400 Subject: [PATCH 2/2] `add_noise` transform for adding noise models (#5718) **Context:** Adds a transform for adding `NoiseModels`. **Description of the Change:** Adds `add_noise.py` under `pennylane/transforms` that gives the said method for inserting operations according to a provided noise model. **Benefits:** We support noise models. **Possible Drawbacks:** None. **Related GitHub Issues:** [sc-64843] --------- Co-authored-by: Jay Soni --- doc/code/qml_noise.rst | 10 +- doc/releases/changelog-dev.md | 39 ++- pennylane/__init__.py | 1 + pennylane/noise/noise_model.py | 4 +- pennylane/transforms/__init__.py | 3 +- pennylane/transforms/add_noise.py | 232 +++++++++++++++ tests/transforms/test_add_noise.py | 451 +++++++++++++++++++++++++++++ 7 files changed, 727 insertions(+), 13 deletions(-) create mode 100644 pennylane/transforms/add_noise.py create mode 100644 tests/transforms/test_add_noise.py diff --git a/doc/code/qml_noise.rst b/doc/code/qml_noise.rst index 74254e1a265..29e73e17a63 100644 --- a/doc/code/qml_noise.rst +++ b/doc/code/qml_noise.rst @@ -4,6 +4,8 @@ qml.noise This module contains the functionality for building and manipulating insertion-based noise models, where noisy gates and channels are inserted based on the target operations. +.. _intro_noise_model: + Overview -------- @@ -22,7 +24,13 @@ noise-related metadata can also be supplied to construct a noise model using: Each conditional in the ``model_map`` evaluates the gate operations in the quantum circuit based on some condition of its attributes (e.g., type, parameters, wires, etc.) and use the corresponding callable to apply the noise operations, using the user-provided metadata (e.g., hardware topologies -or relaxation times), whenever the condition results true. +or relaxation times), whenever the condition results true. A noise model once built can be attached +to a circuit or device via the following transform: + +.. autosummary:: + :toctree: api + + ~add_noise .. _intro_boolean_fn: diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index c0ea2eb067b..e3641f9a557 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -47,20 +47,41 @@ * The `default.tensor` device is introduced to perform tensor network simulations of quantum circuits using the `mps` (Matrix Product State) method. [(#5699)](https://github.com/PennyLaneAI/pennylane/pull/5699) -* A new `qml.noise` module which contains utililty functions for building `NoiseModels`. +* A new `qml.noise` module which contains utility function for building `NoiseModels` + and an `add_noise` tranform for addding it to quantum circuits. [(#5674)](https://github.com/PennyLaneAI/pennylane/pull/5674) [(#5684)](https://github.com/PennyLaneAI/pennylane/pull/5684) + [(#5718)](https://github.com/PennyLaneAI/pennylane/pull/5718) - ```python - fcond = qml.noise.op_eq(qml.X) | qml.noise.op_eq(qml.Y) - noise = qml.noise.partial_wires(qml.AmplitudeDamping, 0.4) + ```pycon + >>> fcond1 = qml.noise.op_eq(qml.RX) & qml.noise.wires_in([0, 1]) + >>> noise1 = qml.noise.partial_wires(qml.PhaseDamping, 0.4) + >>> fcond2 = qml.noise.op_in([qml.RY, qml.RZ]) + >>> def noise2(op, **kwargs): + ... qml.ThermalRelaxationError(op.parameters[0] * 0.05, kwargs["t1"], 0.2, 0.6, op.wires) + >>> noise_model = qml.NoiseModel({fcond1: noise1, fcond2: noise2}, t1=2.0) + >>> noise_model + NoiseModel({ + OpEq(RX) & WiresIn([0, 1]) = PhaseDamping(gamma=0.4) + OpIn(['RY', 'RZ']) = noise2 + }, t1 = 2.0) ``` ```pycon - >>> qml.NoiseModel({fcond: noise}, t1=0.04) - NoiseModel({ - OpEq(PauliX) | OpEq(PauliY) = AmplitudeDamping(gamma=0.4) - }, t1 = 0.04) + >>> @partial(qml.transforms.add_noise, noise_model=noise_model) + ... @qml.qnode(dev) + ... def circuit(w, x, y, z): + ... qml.RX(w, wires=0) + ... qml.RY(x, wires=1) + ... qml.CNOT(wires=[0, 1]) + ... qml.RY(y, wires=0) + ... qml.RX(z, wires=1) + ... return qml.expval(qml.Z(0) @ qml.Z(1)) + >>> print(qml.draw(circuit)(0.9, 0.4, 0.5, 0.6)) + 0: ──RX(0.90)──PhaseDamping(0.40)──────────────────────────╭●──RY(0.50) + 1: ──RY(0.40)──ThermalRelaxationError(0.02,2.00,0.20,0.60)─╰X──RX(0.60) + ───ThermalRelaxationError(0.03,2.00,0.20,0.60)─┤ ╭ + ───PhaseDamping(0.40)──────────────────────────┤ ╰ ``` * The ``from_openfermion`` and ``to_openfermion`` functions are added to convert between @@ -72,8 +93,8 @@ of_op = openfermion.FermionOperator('0^ 2') pl_op = qml.from_openfermion(of_op) of_op_new = qml.to_openfermion(pl_op) - ``` + ```pycon >>> print(pl_op) a⁺(0) a(2) diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 61285764f26..998b355ce3a 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -105,6 +105,7 @@ pattern_matching, pattern_matching_optimization, clifford_t_decomposition, + add_noise, ) from pennylane.ops.functions import ( dot, diff --git a/pennylane/noise/noise_model.py b/pennylane/noise/noise_model.py index 12616c1d156..792f93e24ee 100644 --- a/pennylane/noise/noise_model.py +++ b/pennylane/noise/noise_model.py @@ -36,7 +36,7 @@ class NoiseModel: - The ``conditional`` should be either a function decorated with :class:`~.BooleanFn`, a callable object built via :ref:`constructor functions ` in - the ``noise`` module, or their bit-wise combination. + the ``qml.noise`` module, or their bit-wise combination. - The definition of ``noise_fn(op, **kwargs)`` should have the operations in same the order in which they are to be queued for an operation ``op``, whenever the corresponding ``conditional`` evaluates to ``True``. @@ -129,7 +129,7 @@ def check_model(model): for condition, noise in model.items(): if not isinstance(condition, qml.BooleanFn): raise ValueError( - f"{condition} must be a boolean conditional, i.e., an instance of" + f"{condition} must be a boolean conditional, i.e., an instance of " "BooleanFn or one of its subclasses." ) diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index f69d978040b..9781cd4b46d 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -107,6 +107,7 @@ ~batch_params ~batch_input ~transforms.insert + ~transforms.add_noise ~defer_measurements ~transforms.split_non_commuting ~transforms.broadcast_expand @@ -284,7 +285,7 @@ def circuit(x, y): from .batch_partial import batch_partial from .convert_to_numpy_parameters import convert_to_numpy_parameters from .compile import compile - +from .add_noise import add_noise from .decompositions import clifford_t_decomposition from .defer_measurements import defer_measurements diff --git a/pennylane/transforms/add_noise.py b/pennylane/transforms/add_noise.py new file mode 100644 index 00000000000..6b2caa3e86c --- /dev/null +++ b/pennylane/transforms/add_noise.py @@ -0,0 +1,232 @@ +# Copyright 2018-2024 Xanadu Quantum Technologies Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Transform for adding a noise model to a quantum circuit or device""" +from copy import copy +from functools import lru_cache + +import pennylane as qml +from pennylane.transforms.core import TransformContainer, transform + + +@transform +def add_noise(tape, noise_model, level=None): + """Insert operations according to a provided noise model. + + Circuits passed through this transform will be updated to apply the + insertion-based :class:`~.NoiseModel`, which contains a mapping + ``{BooleanFn: Callable}`` from conditions to the corresponding noise + gates. Each condition of the noise model will be evaluated on the + operations contained within the given circuit. For conditions that + evaluate to ``True``, the noisy gates contained within the ``Callable`` + will be inserted after the operation under consideration. + + Args: + tape (QNode or QuantumTape or Callable or pennylane.devices.Device): the input circuit or + device to be transformed + noise_model (~pennylane.NoiseModel): noise model according to which noise has to be inserted + level (None, str, int, slice): An indication of which stage in the transform program the + noise model should be applied to. Only relevant when transforming a ``QNode``. More details + on the following permissible values can be found in the :func:`~.workflow.get_transform_program` - + + * ``None``: expands the tape to have no ``Adjoint`` and ``Templates``. + * ``str``: acceptable keys are ``"top"``, ``"user"``, ``"device"``, and ``"gradient"`` + * ``int``: how many transforms to include, starting from the front of the program + * ``slice``: a slice to select out components of the transform program. + + Returns: + qnode (QNode) or quantum function (Callable) or tuple[List[.QuantumTape], function] or device (pennylane.devices.Device): + Transformed circuit as described in :func:`qml.transform `. + + Raises: + ValueError: argument ``noise_model`` is not a valid noise model. + + .. note:: + + For a given ``model_map`` within a ``NoiseModel``, if multiple conditionals in the ``model_map`` + evaluate to ``True`` for an operation, then the noise operations defined via their respective + noisy quantum functions will be added in the same order in which the conditionals appear in the + ``model_map``. + + **Example:** + + The following QNode can be transformed to add noise to the circuit: + + .. code-block:: python3 + + from functools import partial + + dev = qml.device("default.mixed", wires=2) + + fcond1 = qml.noise.op_eq(qml.RX) & qml.noise.wires_in([0, 1]) + noise1 = qml.noise.partial_wires(qml.PhaseDamping, 0.4) + + fcond2 = qml.noise.op_in([qml.RX, qml.RZ]) + def noise2(op, **kwargs): + qml.ThermalRelaxationError(op.parameters[0] * 0.5, kwargs["t1"], kwargs["t2"], 0.6, op.wires) + + noise_model = qml.NoiseModel({fcond1: noise1, fcond2: noise2}, t1=2.0, t2=0.2) + + @partial(qml.transforms.add_noise, noise_model=noise_model) + @qml.qnode(dev) + def circuit(w, x, y, z): + qml.RX(w, wires=0) + qml.RY(x, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(y, wires=0) + qml.RX(z, wires=1) + return qml.expval(qml.Z(0) @ qml.Z(1)) + + Executions of this circuit will differ from the noise-free value: + + >>> circuit(0.9, 0.4, 0.5, 0.6) + tensor(0.60722291, requires_grad=True) + >>> print(qml.draw(f)(0.9, 0.4, 0.5, 0.6)) + 0: ──RX(0.9)──PhaseDamping(0.4)───────────────────────╭●──RY(0.5)───ThermalRelaxationError(0.2,2.0,0.2,0.6)─┤ ╭ + 1: ──RY(0.4)──ThermalRelaxationError(0.2,2.0,0.2,0.6)─╰X──RX(0.6)───PhaseDamping(0.4)───────────────────────┤ ╰ + + .. details:: + :title: Tranform Levels + :href: add-noise-levels + + When transforming an already constructed ``QNode``, the ``add_noise`` transform will be + added at the end of the "user" transforms by default, i.e., after all the transforms + that have been manually applied to the QNode until that point. + + .. code-block:: python3 + + dev = qml.device("default.mixed", wires=2) + + @qml.metric_tensor + @qml.transforms.undo_swaps + @qml.transforms.merge_rotations + @qml.transforms.cancel_inverses + @qml.qnode(dev) + def circuit(w, x, y, z): + qml.RX(w, wires=0) + qml.RY(x, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(y, wires=0) + qml.RX(z, wires=1) + return qml.expval(qml.Z(0) @ qml.Z(1)) + + noisy_circuit = qml.transforms.add_noise(circuit, noise_model) + + >>> qml.workflow.get_transform_program(circuit) + TransformProgram(cancel_inverses, merge_rotations, undo_swaps, _expand_metric_tensor, batch_transform, expand_fn, metric_tensor) + >>> qml.workflow.get_transform_program(noisy_circuit) + TransformProgram(cancel_inverses, merge_rotations, undo_swaps, _expand_metric_tensor, add_noise, batch_transform, expand_fn, metric_tensor) + + However, one can request inserting it at any specific point of the transform program. Specifying the ``level`` keyword argument while + transforming a ``QNode``, will allow addition of the transform at the end of the transform program extracted at a designated level via + :func:`get_transform_program `. For example, one could specify ``None`` to add it at the end, + which will also ensure that the tape is expanded to have no ``Adjoint`` and ``Templates``: + + >>> qml.transforms.add_noise(circuit, noise_model, level=None).transform_program + TransformProgram(cancel_inverses, merge_rotations, undo_swaps, _expand_metric_tensor, batch_transform, expand_fn, add_noise, metric_tensor) + + Other, acceptable values for the level are ``"top"``, ``"user"``, ``"device"``, and ``"gradient"``. Among these, `"top"` will allow addition + to an empty transform program, `"user"` will allow addition at the end of user specified transforms, `"device"` will allow addition at the + end of device-specific transforms, and `"gradient"` will allow addition at the end of transform that expands trainable operations. For example: + + >>> qml.transforms.add_noise(circuit, noise_model, level="top").transform_program + TransformProgram(add_noise) + >>> qml.transforms.add_noise(circuit, noise_model, level="user").transform_program + TransformProgram(cancel_inverses, merge_rotations, undo_swaps, _expand_metric_tensor, add_noise, metric_tensor) + >>> qml.transforms.add_noise(circuit, noise_model, level="device").transform_program + TransformProgram(cancel_inverses, merge_rotations, undo_swaps, _expand_metric_tensor, batch_transform, expand_fn, add_noise, metric_tensor) + + Finally, more precise control over the insertion of the transform can be achieved by specifying + an integer or slice for indexing for extracting the transform program. For example, one can do: + + >>> qml.transforms.add_noise(circuit, noise_model, level=2).transform_program + TransformProgram(cancel_inverses, merge_rotations, add_noise) + >>> qml.transforms.add_noise(circuit, noise_model, level=slice(1,3)).transform_program + TransformProgram(merge_rotations, undo_swaps, add_noise) + + """ + if not hasattr(noise_model, "model_map") or not hasattr(noise_model, "metadata"): + raise ValueError( + f"Provided noise model object must define model_map and metatadata attributes, got {noise_model}." + ) + + if level is None or level == "user": # decompose templates and their adjoints + + def stop_at(obj): + if not isinstance(obj, qml.operation.Operator): + return True + if not obj.has_decomposition: + return True + return not (hasattr(qml.templates, obj.name) or isinstance(obj, qml.ops.Adjoint)) + + error_type = (qml.operation.DecompositionUndefinedError,) + decompose = qml.devices.preprocess.decompose + [tape], _ = decompose(tape, stopping_condition=stop_at, name="add_noise", error=error_type) + + conditions, noises = [], [] + metadata = noise_model.metadata + for condition, noise in noise_model.model_map.items(): + conditions.append(lru_cache(maxsize=512)(condition)) + noises.append(qml.tape.make_qscript(noise)) + + new_operations = [] + for operation in tape.operations: + curr_ops = [operation] + for condition, noise in zip(conditions, noises): + if condition(operation): + noise_ops = noise(operation, **metadata).operations + if operation in noise_ops and _check_queue_op(operation, noise, metadata): + ops_indx = noise_ops.index(operation) + curr_ops = noise_ops[:ops_indx] + curr_ops + noise_ops[ops_indx + 1 :] + else: + curr_ops.extend(noise_ops) + new_operations.extend(curr_ops) + + new_tape = type(tape)(new_operations, tape.measurements, shots=tape.shots) + post_processing_fn = qml.devices.preprocess.null_postprocessing + + return [new_tape], post_processing_fn + + +def _check_queue_op(operation, noise_func, metadata): + """Performs a secondary check for existence of an operation in the queue using a randomized ID""" + + test_id = "f49968bfc4W0H86df3A733bf6e92904d21a_!$-T-@!_c131S549b169b061I25b85398bfd8ec1S3c" + test_queue = noise_func( + qml.noise.partial_wires(operation, id=test_id)(operation.wires), **metadata + ).operations + + return any(test_id == getattr(o, "id", "") for o in test_queue) + + +# pylint:disable = protected-access +@add_noise.custom_qnode_transform +def custom_qnode_wrapper(self, qnode, targs, tkwargs): + """QNode execution wrapper for supporting ``add_noise`` with levels""" + cqnode = copy(qnode) + level = tkwargs.get("level", "user") + + transform_program = qml.workflow.get_transform_program(qnode, level=level) + + cqnode._transform_program = transform_program + cqnode.add_transform( + TransformContainer( + self._transform, + targs, + {**tkwargs}, + self._classical_cotransform, + self._is_informative, + self._final_transform, + ) + ) + + return cqnode diff --git a/tests/transforms/test_add_noise.py b/tests/transforms/test_add_noise.py new file mode 100644 index 00000000000..4a9fd4d5f7b --- /dev/null +++ b/tests/transforms/test_add_noise.py @@ -0,0 +1,451 @@ +# Copyright 2024 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Tests for the add_noise transform. +""" +from functools import partial + +import numpy as np +import pytest + +import pennylane as qml +from pennylane.measurements import Expectation +from pennylane.tape import QuantumScript +from pennylane.transforms.add_noise import add_noise + +# pylint:disable = no-member + + +class TestAddNoise: + """Tests for the add_noise transform using input tapes""" + + with qml.queuing.AnnotatedQueue() as q_tape: + qml.RX(0.9, wires=0) + qml.RY(0.4, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(0.5, wires=0) + qml.RX(0.6, wires=1) + qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + tape = QuantumScript.from_queue(q_tape) + + with qml.queuing.AnnotatedQueue() as q_tape_with_prep: + qml.StatePrep([1, 0], wires=0) + qml.RX(0.9, wires=0) + qml.RY(0.4, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(0.5, wires=0) + qml.RX(0.6, wires=1) + qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + tape_with_prep = QuantumScript.from_queue(q_tape_with_prep) + + # conditionals + c0 = qml.noise.op_eq(qml.RX) + c1 = qml.noise.op_in([qml.RY, qml.RZ]) + c2 = qml.noise.op_eq("StatePrep") + + # callables + @staticmethod + def n0(op, **kwargs): # pylint: disable=unused-argument + """Mapped callable for c0""" + qml.RZ(op.parameters[0] * 0.05, op.wires) + qml.apply(op) + qml.RZ(-op.parameters[0] * 0.05, op.wires) + + n1 = qml.noise.partial_wires(qml.AmplitudeDamping, 0.4) + + @staticmethod + def n2(op, **kwargs): + """Mapped callable for c2""" + qml.ThermalRelaxationError(0.4, kwargs["t1"], 0.2, 0.6, op.wires) + + noise_model = qml.NoiseModel({c0: n0.__func__, c1: n1}) + noise_model_with_prep = noise_model + qml.NoiseModel({c2: n2.__func__}, t1=0.4) + + def test_noise_model_error(self): + """Tests if a ValueError is raised when noise model is not given""" + with pytest.raises( + ValueError, + match="Provided noise model object must define model_map and metatadata attributes", + ): + add_noise(self.tape, {}) + + def test_noise_tape(self): + """Test if the expected tape is returned with the transform""" + [tape], _ = add_noise(self.tape, self.noise_model) + + with qml.queuing.AnnotatedQueue() as q_tape_exp: + qml.RZ(0.045, wires=0) + qml.RX(0.9, wires=0) + qml.RZ(-0.045, wires=0) + qml.RY(0.4, wires=1) + qml.AmplitudeDamping(0.4, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(0.5, wires=0) + qml.AmplitudeDamping(0.4, wires=0) + qml.RZ(0.03, wires=1) + qml.RX(0.6, wires=1) + qml.RZ(-0.03, wires=1) + qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + tape_exp = QuantumScript.from_queue(q_tape_exp) + + assert all(o1.name == o2.name for o1, o2 in zip(tape.operations, tape_exp.operations)) + assert all(o1.wires == o2.wires for o1, o2 in zip(tape.operations, tape_exp.operations)) + assert all( + np.allclose(o1.parameters, o2.parameters) + for o1, o2 in zip(tape.operations, tape_exp.operations) + ) + assert len(tape.measurements) == 1 + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) + assert tape.observables[0].wires.tolist() == [0, 1] + assert tape.measurements[0].return_type is Expectation + + def test_noise_tape_with_state_prep(self): + """Test if the expected tape is returned with the transform""" + [tape], _ = add_noise(self.tape_with_prep, self.noise_model_with_prep) + + with qml.queuing.AnnotatedQueue() as q_tape_exp: + qml.StatePrep([1, 0], wires=0) + qml.ThermalRelaxationError(0.4, 0.4, 0.2, 0.6, wires=0) + qml.RZ(0.045, wires=0) + qml.RX(0.9, wires=0) + qml.RZ(-0.045, wires=0) + qml.RY(0.4, wires=1) + qml.AmplitudeDamping(0.4, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(0.5, wires=0) + qml.AmplitudeDamping(0.4, wires=0) + qml.RZ(0.03, wires=1) + qml.RX(0.6, wires=1) + qml.RZ(-0.03, wires=1) + qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + tape_exp = QuantumScript.from_queue(q_tape_exp) + + assert all(o1.name == o2.name for o1, o2 in zip(tape.operations, tape_exp.operations)) + assert all(o1.wires == o2.wires for o1, o2 in zip(tape.operations, tape_exp.operations)) + assert all( + np.allclose(o1.parameters, o2.parameters) + for o1, o2 in zip(tape.operations, tape_exp.operations) + ) + assert len(tape.measurements) == 1 + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) + assert tape.observables[0].wires.tolist() == [0, 1] + assert tape.measurements[0].return_type is Expectation + + +class TestAddNoiseInterface: + """Tests for the add_noise transform using input qnode and devices""" + + def test_add_noise_qnode(self): + """Test that a QNode with add_noise decorator gives a different result.""" + dev = qml.device("default.mixed", wires=2) + + c, n = qml.noise.op_in([qml.RY, qml.RZ]), qml.noise.partial_wires(qml.AmplitudeDamping, 0.4) + + @partial(add_noise, noise_model=qml.NoiseModel({c: n})) + @qml.qnode(dev) + def f_noisy(w, x, y, z): + qml.RX(w, wires=0) + qml.RY(x, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(y, wires=0) + qml.RX(z, wires=1) + return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + + @qml.qnode(dev) + def f(w, x, y, z): + qml.RX(w, wires=0) + qml.RY(x, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(y, wires=0) + qml.RX(z, wires=1) + return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + + args = [0.1, 0.2, 0.3, 0.4] + + assert not np.isclose(f_noisy(*args), f(*args)) + + def test_add_noise_dev(self): + """Test if an device transformed by the add_noise transform does successfully add noise to + subsequent circuit executions""" + with qml.queuing.AnnotatedQueue() as q_in_tape: + qml.RX(0.9, wires=0) + qml.RY(0.4, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(0.5, wires=0) + qml.RX(0.6, wires=1) + qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + qml.expval(qml.PauliZ(0)) + + in_tape = QuantumScript.from_queue(q_in_tape) + dev = qml.device("default.qubit", wires=2) + program, _ = dev.preprocess() + res_without_noise = qml.execute( + [in_tape], dev, qml.gradients.param_shift, transform_program=program + ) + + c, n = qml.noise.op_in([qml.RX, qml.RY]), qml.noise.partial_wires(qml.PhaseShift, 0.4) + new_dev = add_noise(dev, noise_model=qml.NoiseModel({c: n})) + new_program, _ = new_dev.preprocess() + [tape], _ = new_program([in_tape]) + res_with_noise = qml.execute( + [in_tape], new_dev, qml.gradients, transform_program=new_program + ) + + with qml.queuing.AnnotatedQueue() as q_tape_exp: + qml.RX(0.9, wires=0) + qml.PhaseShift(0.4, wires=0) + qml.RY(0.4, wires=1) + qml.PhaseShift(0.4, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(0.5, wires=0) + qml.PhaseShift(0.4, wires=0) + qml.RX(0.6, wires=1) + qml.PhaseShift(0.4, wires=1) + qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + qml.expval(qml.PauliZ(0)) + + tape_exp = QuantumScript.from_queue(q_tape_exp) + assert all(o1.name == o2.name for o1, o2 in zip(tape.operations, tape_exp.operations)) + assert all(o1.wires == o2.wires for o1, o2 in zip(tape.operations, tape_exp.operations)) + assert all( + np.allclose(o1.parameters, o2.parameters) + for o1, o2 in zip(tape.operations, tape_exp.operations) + ) + assert len(tape.measurements) == 2 + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) + assert tape.observables[0].wires.tolist() == [0, 1] + assert tape.measurements[0].return_type is Expectation + assert tape.observables[1].name == "PauliZ" + assert tape.observables[1].wires.tolist() == [0] + assert tape.measurements[1].return_type is Expectation + + assert not np.allclose(res_without_noise, res_with_noise) + + def test_add_noise_old_dev(self, mocker): + """Test if a old device transformed by the add_noise function does successfully add noise to + subsequent circuit executions""" + with qml.queuing.AnnotatedQueue() as q_in_tape: + qml.RX(0.9, wires=0) + qml.RY(0.4, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(0.5, wires=0) + qml.RX(0.6, wires=1) + qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + qml.expval(qml.PauliZ(0)) + + in_tape = QuantumScript.from_queue(q_in_tape) + dev = qml.device("default.mixed", wires=2) + res_without_noise = qml.execute([in_tape], dev, qml.gradients.param_shift) + + c, n = qml.noise.op_in([qml.RX, qml.RY]), qml.noise.partial_wires(qml.PhaseDamping, 0.4) + new_dev = add_noise(dev, noise_model=qml.NoiseModel({c: n})) + spy = mocker.spy(new_dev, "default_expand_fn") + + res_with_noise = qml.execute([in_tape], new_dev, qml.gradients.param_shift) + tape = spy.call_args[0][0] + + with qml.queuing.AnnotatedQueue() as q_tape_exp: + qml.RX(0.9, wires=0) + qml.PhaseDamping(0.4, wires=0) + qml.RY(0.4, wires=1) + qml.PhaseDamping(0.4, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(0.5, wires=0) + qml.PhaseDamping(0.4, wires=0) + qml.RX(0.6, wires=1) + qml.PhaseDamping(0.4, wires=1) + qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + qml.expval(qml.PauliZ(0)) + tape_exp = QuantumScript.from_queue(q_tape_exp) + + assert all(o1.name == o2.name for o1, o2 in zip(tape.operations, tape_exp.operations)) + assert all(o1.wires == o2.wires for o1, o2 in zip(tape.operations, tape_exp.operations)) + assert all( + np.allclose(o1.parameters, o2.parameters) + for o1, o2 in zip(tape.operations, tape_exp.operations) + ) + assert len(tape.measurements) == 2 + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) + assert tape.observables[0].wires.tolist() == [0, 1] + assert tape.measurements[0].return_type is Expectation + assert tape.observables[1].name == "PauliZ" + assert tape.observables[1].wires.tolist() == [0] + assert tape.measurements[1].return_type is Expectation + + assert not np.allclose(res_without_noise, res_with_noise) + + def test_add_noise_template(self): + """Test that noisy ops are inserted correctly into a decomposed template""" + dev = qml.device("default.mixed", wires=2) + + c, n = qml.noise.op_in([qml.RX, qml.RY]), qml.noise.partial_wires(qml.PhaseDamping, 0.3) + + @partial(add_noise, noise_model=qml.NoiseModel({c: n})) + @qml.qnode(dev) + def f1(w1, w2): + qml.SimplifiedTwoDesign(w1, w2, wires=[0, 1]) + return qml.expval(qml.PauliZ(0)) + + @qml.qnode(dev) + def f2(w1, w2): + qml.RY(w1[0], wires=0) + qml.PhaseDamping(0.3, wires=0) + qml.RY(w1[1], wires=1) + qml.PhaseDamping(0.3, wires=1) + qml.CZ(wires=[0, 1]) + qml.RY(w2[0][0][0], wires=0) + qml.PhaseDamping(0.3, wires=0) + qml.RY(w2[0][0][1], wires=1) + qml.PhaseDamping(0.3, wires=1) + return qml.expval(qml.PauliZ(0)) + + w1 = np.random.random(2) + w2 = np.random.random((1, 1, 2)) + + assert np.allclose(f1(w1, w2), f2(w1, w2)) + + # pylint: disable=unused-argument + def test_add_noise_with_non_qwc_obs_and_mid_meas(self): + """Test that the add_noise transform catches and reports errors from the enclosed function.""" + + dev = qml.device("default.qubit", wires=3) + + fcond = qml.noise.wires_in([0, 1]) + + def noise(op, **kwargs): + qml.CNOT(wires=[1, 0]) + qml.CRX(kwargs["noise_param"], wires=[0, 1]) + + @qml.qnode(dev) + @partial(add_noise, noise_model=qml.NoiseModel({fcond: noise}, noise_param=0.3)) + def noisy_circuit(circuit_param): + qml.RY(circuit_param, wires=0) + qml.Hadamard(wires=0) + qml.T(wires=0) + m0 = qml.measure(0) + m1 = qml.measure(1) + qml.cond(~m0 & m1 == 0, qml.X)(wires=2) + return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0)) + + @qml.qnode(dev) + def explicit_circuit(circuit_param): + qml.RY(circuit_param, wires=0) + noise(op=None, noise_param=0.3) + qml.Hadamard(wires=0) + noise(op=None, noise_param=0.3) + qml.T(wires=0) + noise(op=None, noise_param=0.3) + m0 = qml.measure(0) + noise(op=None, noise_param=0.3) + m1 = qml.measure(1) + noise(op=None, noise_param=0.3) + qml.cond(~m0 & m1 == 0, qml.X)(wires=2) + return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0)) + + assert np.allclose(noisy_circuit(0.4), explicit_circuit(0.4)) + + +class TestAddNoiseLevels: + """Tests for custom insertion of add_noise transform at correct level.""" + + @pytest.mark.parametrize( + "level1, level2", + [ + ("top", 0), + (0, slice(0, 0)), + ("user", 4), + ("user", slice(0, 4)), + (None, slice(0, None)), + (-1, slice(0, -1)), + ("device", slice(0, None)), + ], + ) + def test_add_noise_level(self, level1, level2): + """Test that add_noise can be inserted to correct level in the TransformProgram""" + dev = qml.device("default.mixed", wires=2) + + @qml.metric_tensor + @qml.transforms.undo_swaps + @qml.transforms.merge_rotations + @qml.transforms.cancel_inverses + @qml.qnode(dev, diff_method="parameter-shift", shifts=np.pi / 4) + def f(w, x, y, z): + qml.RX(w, wires=0) + qml.RY(x, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(y, wires=0) + qml.RX(z, wires=1) + return qml.expval(qml.Z(0) @ qml.Z(1)) + + fcond = qml.noise.op_eq(qml.RX) + fcall = qml.noise.partial_wires(qml.PhaseDamping, 0.4) + noise_model = qml.NoiseModel({fcond: fcall}) + + noisy_qnode = add_noise(f, noise_model=noise_model, level=level1) + + transform_level1 = noisy_qnode.transform_program + transform_level2 = qml.workflow.get_transform_program(f, level=level2) + transform_level2.add_transform(add_noise, noise_model=noise_model, level=level1) + + assert len(transform_level1) == len(transform_level2) + bool(level1 == "user") + for t1, t2 in zip(transform_level1, transform_level2): + if t1.transform.__name__ == t2.transform.__name__ == "expand_fn": + continue + assert t1 == t2 + + def test_add_noise_level_with_final(self): + """Test that add_noise can be inserted in the TransformProgram with a final transform""" + dev = qml.device("default.mixed", wires=2) + + @qml.metric_tensor + @qml.transforms.undo_swaps + @qml.transforms.merge_rotations + @qml.transforms.cancel_inverses + @qml.qnode(dev, diff_method="parameter-shift", shifts=np.pi / 4) + def f(w, x, y, z): + qml.RX(w, wires=0) + qml.RY(x, wires=1) + qml.CNOT(wires=[0, 1]) + qml.RY(y, wires=0) + qml.RX(z, wires=1) + return qml.expval(qml.Z(0) @ qml.Z(1)) + + fcond = qml.noise.op_eq(qml.RX) + fcall = qml.noise.partial_wires(qml.PhaseDamping, 0.4) + noise_model = qml.NoiseModel({fcond: fcall}) + + noisy_qnode = add_noise(f, noise_model=noise_model) + + transform_level1 = qml.workflow.get_transform_program(f) + transform_level2 = qml.workflow.get_transform_program(noisy_qnode) + + assert len(transform_level1) == len(transform_level2) - 1 + assert transform_level2[4].transform == add_noise.transform + assert transform_level2[-1].transform == qml.metric_tensor.transform