From 3f52824f65caea27f1b7377bc7d5996c0f6063f6 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Fri, 12 Apr 2024 11:13:06 -0400 Subject: [PATCH 1/6] Operations/measurements on all wires are drawn correctly with mid-circuit measurements (#5501) **Context:** Bug fix for #5499 **Description of the Change:** Adding labels to layer strings only extends to wires, not classical bits. **Benefits:** Drawer works correctly with mid-circuit measurements **Possible Drawbacks:** **Related GitHub Issues:** --- doc/releases/changelog-dev.md | 3 +++ pennylane/drawer/tape_text.py | 6 +++-- tests/drawer/test_draw.py | 47 +++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index b117f4b3f82..d0edbac6bfd 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -339,6 +339,9 @@

Bug fixes 🐛

+* Operators applied to all wires are now drawn correctly in a circuit with mid-circuit measurements. + [(#5501)](https://github.com/PennyLaneAI/pennylane/pull/5501) + * Fix a bug where certain unary mid-circuit measurement expressions would raise an uncaught error. [(#5480)](https://github.com/PennyLaneAI/pennylane/pull/5480) diff --git a/pennylane/drawer/tape_text.py b/pennylane/drawer/tape_text.py index 8e635ef50ea..7069e4749fe 100644 --- a/pennylane/drawer/tape_text.py +++ b/pennylane/drawer/tape_text.py @@ -142,7 +142,8 @@ def _add_op(op, layer_str, config): label = op.label(decimals=config.decimals, cache=config.cache).replace("\n", "") if len(op.wires) == 0: # operation (e.g. barrier, snapshot) across all wires - for i, s in enumerate(layer_str): + n_wires = len(config.wire_map) + for i, s in enumerate(layer_str[:n_wires]): layer_str[i] = s + label else: for w in op.wires: @@ -225,7 +226,8 @@ def _add_measurement(m, layer_str, config): meas_label = m.return_type.value if len(m.wires) == 0: # state or probability across all wires - for i, s in enumerate(layer_str): + n_wires = len(config.wire_map) + for i, s in enumerate(layer_str[:n_wires]): layer_str[i] = s + meas_label for w in m.wires: diff --git a/tests/drawer/test_draw.py b/tests/drawer/test_draw.py index 1bd7790f523..b571ed4fd0d 100644 --- a/tests/drawer/test_draw.py +++ b/tests/drawer/test_draw.py @@ -284,6 +284,8 @@ def circ(): class TestMidCircuitMeasurements: """Tests for drawing mid-circuit measurements and classical conditions.""" + # pylint: disable=too-many-public-methods + @pytest.mark.parametrize("device_name", ["default.qubit"]) def test_qnode_mid_circuit_measurement_not_deferred(self, device_name, mocker): """Test that a circuit containing mid-circuit measurements is transformed by the drawer @@ -329,6 +331,51 @@ def func(): assert drawing == expected_drawing + @pytest.mark.parametrize( + "op", [qml.GlobalPhase(0.1), qml.Identity(), qml.Snapshot(), qml.Barrier()] + ) + @pytest.mark.parametrize("decimals", [None, 2]) + def test_draw_all_wire_ops(self, op, decimals): + """Test that operators acting on all wires are drawn correctly""" + + def func(): + qml.X(0) + qml.X(1) + m = qml.measure(0) + qml.cond(m, qml.X)(0) + qml.apply(op) + return qml.expval(qml.Z(0)) + + # Stripping to remove trailing white-space because length of white-space at the + # end of the drawing depends on the length of each individual line + drawing = qml.draw(func, decimals=decimals)().strip() + label = op.label(decimals=decimals).replace("\n", "") + expected_drawing = ( + f"0: ──X──┤↗├──X──{label}─┤ \n1: ──X───║───║──{label}─┤ \n ╚═══╝" + ) + + assert drawing == expected_drawing + + @pytest.mark.parametrize( + "mp, label", [(qml.sample(), "Sample"), (qml.probs(), "Probs"), (qml.counts(), "Counts")] + ) + def test_draw_all_wire_measurements(self, mp, label): + """Test that operators acting on all wires are drawn correctly""" + + def func(): + qml.X(0) + qml.X(1) + m = qml.measure(0) + qml.cond(m, qml.X)(0) + return qml.apply(mp) + + # Stripping to remove trailing white-space because length of white-space at the + # end of the drawing depends on the length of each individual line + drawing = qml.draw(func)().strip() + expected_drawing = f"0: ──X──┤↗├──X─┤ {label}\n1: ──X───║───║─┤ {label}\n ╚═══╝" + + assert drawing == expected_drawing + def test_draw_mid_circuit_measurement_multiple_wires(self): """Test that mid-circuit measurements are correctly drawn in circuits with multiple wires.""" From b2b0c36608cce4772cda7200d776d67eac9dfff6 Mon Sep 17 00:00:00 2001 From: Pietropaolo Frisoni Date: Fri, 12 Apr 2024 12:36:48 -0400 Subject: [PATCH 2/6] [bugfix] QNSPSAOptimizer improperly handles optimization on lightning.qubit (#5497) **Context:** The `QNSPSAOptimizer` improperly handles optimization on devices that don't follow the new API design. **Description of the Change:** In the workflow of the `step_and_cost` method of `QNSPSAOptimizer`, the code now checks whether the device follows the new API guidelines before calling the `preprocess` function (which is not present in the old device interface). If this is not the case, the code simply calls the `qml.execute` function. **Benefits:** Now, `qml.QNSPSAOptimizer` can handle legacy devices that do not follow the new API design, such as `default.qubit.legacy`. Therefore, the following code now works: ``` from pennylane import numpy as np import pennylane as qml dev = qml.device("default.qubit.legacy", wires=2) @qml.qnode(dev) def cost(params): qml.RX(params[0], wires=0) qml.CRY(params[1], wires=[0, 1]) return qml.expval(qml.Z(0) @ qml.Z(1)) opt = qml.QNSPSAOptimizer(stepsize=5e-2) params = np.random.rand(2) for _ in range(10): params, loss = opt.step_and_cost(cost, params) ``` **Possible Drawbacks:** None that I can think of. **Related GitHub Issues:** #5460 [sc-60727], [sc-60328] --- doc/releases/changelog-dev.md | 3 +++ pennylane/optimize/qnspsa.py | 14 ++++++++++---- tests/optimize/test_qnspsa.py | 10 +++++++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index d0edbac6bfd..71880d7607a 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -339,6 +339,9 @@

Bug fixes 🐛

+* The `qml.QNSPSAOptimizer` now correctly handles optimization for legacy devices that do not follow the new API design. + [(#5497)](https://github.com/PennyLaneAI/pennylane/pull/5497) + * Operators applied to all wires are now drawn correctly in a circuit with mid-circuit measurements. [(#5501)](https://github.com/PennyLaneAI/pennylane/pull/5501) diff --git a/pennylane/optimize/qnspsa.py b/pennylane/optimize/qnspsa.py index be183d285cd..da1df886e86 100644 --- a/pennylane/optimize/qnspsa.py +++ b/pennylane/optimize/qnspsa.py @@ -439,10 +439,16 @@ def _apply_blocking(self, cost, args, kwargs, params_next): cost.construct(params_next, kwargs) tape_loss_next = cost.tape.copy(copy_operations=True) - program, _ = cost.device.preprocess() - loss_curr, loss_next = qml.execute( - [tape_loss_curr, tape_loss_next], cost.device, None, transform_program=program - ) + + if isinstance(cost.device, qml.devices.Device): + program, _ = cost.device.preprocess() + + loss_curr, loss_next = qml.execute( + [tape_loss_curr, tape_loss_next], cost.device, None, transform_program=program + ) + + else: + loss_curr, loss_next = qml.execute([tape_loss_curr, tape_loss_next], cost.device, None) # self.k has been updated earlier ind = (self.k - 2) % self.last_n_steps.size diff --git a/tests/optimize/test_qnspsa.py b/tests/optimize/test_qnspsa.py index 40c8bb37aee..0ffd652c64f 100644 --- a/tests/optimize/test_qnspsa.py +++ b/tests/optimize/test_qnspsa.py @@ -347,8 +347,12 @@ def test_step_and_cost_from_multi_input(self, finite_diff_step, seed): new_params_tensor_expected, ) - def test_step_and_cost_with_non_trainable_input(self, finite_diff_step, seed): - """Test step_and_cost() function with the qnode with non-trainable input.""" + @pytest.mark.parametrize("device_name", ["default.qubit", "default.qubit.legacy"]) + def test_step_and_cost_with_non_trainable_input(self, device_name, finite_diff_step, seed): + """ + Test step_and_cost() function with the qnode with non-trainable input, + both using the `default.qubit` and `default.qubit.legacy` device. + """ regularization = 1e-3 stepsize = 1e-2 opt = qml.QNSPSAOptimizer( @@ -362,7 +366,7 @@ def test_step_and_cost_with_non_trainable_input(self, finite_diff_step, seed): ) # a deep copy of the same opt, to be applied to qnode_reduced target_opt = deepcopy(opt) - dev = qml.device("default.qubit", wires=2) + dev = qml.device(device_name, wires=2) non_trainable_param = np.random.rand(1) non_trainable_param.requires_grad = False From d5e3b609b8bddf7de1f3a62f09809b7ee55b6106 Mon Sep 17 00:00:00 2001 From: Utkarsh Date: Fri, 12 Apr 2024 15:07:38 -0400 Subject: [PATCH 3/6] Add algorithmic errors tracking to `qml.specs` (#5464) **Context:** Update `qml.specs` to track and combine error **Description of the Change:** Adds a `_compute_algo_error` method that combines the individual errors of similar types. Further updates `specs` method of `QuantumScript` to use the prior method for the required computation in a similar fashion to `resources`. **Benefits:** `specs` will track algorithmic errors. **Possible Drawbacks:** N/A **Related GitHub Issues:** N/A --------- Co-authored-by: Jay Soni --- doc/releases/changelog-dev.md | 3 + pennylane/resource/error/__init__.py | 2 +- pennylane/resource/error/error.py | 24 ++++++ pennylane/resource/specs.py | 11 ++- pennylane/tape/qscript.py | 7 +- tests/resource/test_error/test_error.py | 103 +++++++++++++++++++++++- tests/resource/test_specs.py | 8 +- tests/tape/test_qscript.py | 4 +- tests/tape/test_tape.py | 8 +- 9 files changed, 152 insertions(+), 18 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 71880d7607a..ecda32c06ea 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -209,6 +209,9 @@ [stim](https://github.com/quantumlib/Stim) `v1.13.0`. [(#5409)](https://github.com/PennyLaneAI/pennylane/pull/5409) +* `qml.specs` now returns information regarding algorithmic errors for the qnode as well. + [(#5464)](https://github.com/PennyLaneAI/pennylane/pull/5464) + * `qml.transforms.hamiltonian_expand` can now handle multi-term observables with a constant offset. [(#5414)](https://github.com/PennyLaneAI/pennylane/pull/5414) diff --git a/pennylane/resource/error/__init__.py b/pennylane/resource/error/__init__.py index fd758061035..8abe9f434bc 100644 --- a/pennylane/resource/error/__init__.py +++ b/pennylane/resource/error/__init__.py @@ -17,4 +17,4 @@ """ from .trotter_error import _one_norm_error, _commutator_error -from .error import AlgorithmicError, ErrorOperation, SpectralNormError +from .error import AlgorithmicError, ErrorOperation, SpectralNormError, _compute_algo_error diff --git a/pennylane/resource/error/error.py b/pennylane/resource/error/error.py index acb7a5bf424..53a5ca39c9d 100644 --- a/pennylane/resource/error/error.py +++ b/pennylane/resource/error/error.py @@ -15,6 +15,7 @@ Stores classes and logic to define and track algorithmic error in a quantum workflow. """ from abc import ABC, abstractmethod +from typing import Dict import pennylane as qml from pennylane.operation import Operation, Operator @@ -142,3 +143,26 @@ def get_error(approximate_op: Operator, exact_op: Operator): m1 = qml.matrix(exact_op, wire_order=wire_order) m2 = qml.matrix(approximate_op, wire_order=wire_order) return qml.math.max(qml.math.svd(m1 - m2, compute_uv=False)) + + +def _compute_algo_error(tape) -> Dict[str, AlgorithmicError]: + """Given a quantum circuit (tape), this function computes the algorithmic error + generated by standard PennyLane operations. + + Args: + tape (.QuantumTape): The quantum circuit for which we compute errors + + Returns: + dict[str->.AlgorithmicError]: dict with error name and combined error as key-value pair + """ + + algo_errors = {} + for op in tape.operations: + if isinstance(op, ErrorOperation): + op_error = op.error() + error_name = op_error.__class__.__name__ + algo_error = algo_errors.get(error_name, None) + error_value = op_error if algo_error is None else algo_error.combine(op_error) + algo_errors[error_name] = error_value + + return algo_errors diff --git a/pennylane/resource/specs.py b/pennylane/resource/specs.py index 1112e1b9ac1..d515ef84d6c 100644 --- a/pennylane/resource/specs.py +++ b/pennylane/resource/specs.py @@ -52,19 +52,23 @@ def specs(qnode, max_expansion=None, expansion_strategy=None): .. code-block:: python3 x = np.array([0.1, 0.2]) + hamiltonian = qml.dot([1.0, 0.5], [qml.X(0), qml.Y(0)]) dev = qml.device('default.qubit', wires=2) @qml.qnode(dev, diff_method="parameter-shift", shifts=np.pi / 4) def circuit(x, add_ry=True): qml.RX(x[0], wires=0) qml.CNOT(wires=(0,1)) + qml.TrotterProduct(hamiltonian, time=1.0, n=4, order=2) if add_ry: qml.RY(x[1], wires=1) + qml.TrotterProduct(hamiltonian, time=1.0, n=4, order=4) return qml.probs(wires=(0,1)) >>> qml.specs(circuit)(x, add_ry=False) - {'resources': Resources(num_wires=2, num_gates=2, gate_types=defaultdict(, {'RX': 1, 'CNOT': 1}), - gate_sizes=defaultdict(, {1: 1, 2: 1}), depth=2, shots=Shots(total_shots=None, shot_vector=())), + {'resources': Resources(num_wires=2, num_gates=4, gate_types=defaultdict(, {'RX': 1, 'CNOT': 1, 'TrotterPro + duct': 2}}), gate_sizes=defaultdict(, {1: 3, 2: 1}), depth=4, shots=Shots(total_shots=None, shot_vector=())), + 'errors': {'SpectralNormError': SpectralNormError(0.42998560822421455)}, 'num_observables': 1, 'num_diagonalizing_gates': 0, 'num_trainable_params': 1, @@ -74,7 +78,7 @@ def circuit(x, add_ry=True): 'gradient_options': {'shifts': 0.7853981633974483}, 'interface': 'auto', 'diff_method': 'parameter-shift', - 'gradient_fn': 'pennylane.transforms.core.transform_dispatcher.param_shift', + 'gradient_fn': 'pennylane.gradients.parameter_shift.param_shift', 'num_gradient_executions': 2} """ @@ -87,6 +91,7 @@ def specs_qnode(*args, **kwargs): * ``"num_observables"`` number of observables in the qnode * ``"num_diagonalizing_gates"`` number of diagonalizing gates required for execution of the qnode * ``"resources"``: a :class:`~.resource.Resources` object containing resource quantities used by the qnode + * ``"errors"``: combined algorithmic errors from the quantum operations executed by the qnode * ``"num_used_wires"``: number of wires used by the circuit * ``"num_device_wires"``: number of wires in device * ``"depth"``: longest path in directed acyclic graph representation diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index f450bf0161f..3a681d2f61f 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -979,13 +979,14 @@ def specs(self): gate_sizes: {1: 4, 2: 2} """ + # pylint: disable=protected-access if self._specs is None: - resources = qml.resource.resource._count_resources( - self - ) # pylint: disable=protected-access + resources = qml.resource.resource._count_resources(self) + algo_errors = qml.resource.error._compute_algo_error(self) self._specs = { "resources": resources, + "errors": algo_errors, "num_observables": len(self.observables), "num_diagonalizing_gates": len(self.diagonalizing_gates), "num_trainable_params": self.num_params, diff --git a/tests/resource/test_error/test_error.py b/tests/resource/test_error/test_error.py index 8e07e0c1c03..d1b171bab44 100644 --- a/tests/resource/test_error/test_error.py +++ b/tests/resource/test_error/test_error.py @@ -20,7 +20,12 @@ import numpy as np import pennylane as qml -from pennylane.resource.error import AlgorithmicError, SpectralNormError, ErrorOperation +from pennylane.resource.error import ( + AlgorithmicError, + SpectralNormError, + ErrorOperation, + _compute_algo_error, +) from pennylane.operation import Operation @@ -181,3 +186,99 @@ class NoErrorOp(ErrorOperation): num_wires = 3 _ = NoErrorOp(wires=[1, 2, 3]) + + +class MultiplicativeError(AlgorithmicError): + """Multiplicative error object""" + + def combine(self, other): + return self.__class__(self.error * other.error) + + def __repr__(self): + """Return formal string representation.""" + return f"MultiplicativeError({self.error})" + + +class AdditiveError(AlgorithmicError): + """Additive error object""" + + def combine(self, other): + return self.__class__(self.error + other.error) + + def __repr__(self): + """Return formal string representation.""" + return f"AdditiveError({self.error})" + + +class CustomErrorOp1(ErrorOperation): + """Custome error operation with multiplicative error""" + + def __init__(self, phase, wires): + self.phase = phase + super().__init__(phase, wires=wires) + + def error(self, *args, **kwargs): + return MultiplicativeError(self.phase) + + +class CustomErrorOp2(ErrorOperation): + """Custome error with additive error""" + + def __init__(self, flips, wires): + self.flips = flips + super().__init__(flips, wires=wires) + + def error(self, *args, **kwargs): + return AdditiveError(self.flips) + + +_HAMILTONIAN = qml.dot([1.0, -0.5], [qml.X(0) @ qml.Y(1), qml.Y(0) @ qml.Y(1)]) + + +class TestSpecAndTracker: + """Test capture of ErrorOperation in specs and tracker.""" + + # TODO: remove this when support for below is present + # little hack for stopping device-level decomposition for custom ops + @staticmethod + def preprocess(execution_config=qml.devices.DefaultExecutionConfig): + """A vanilla preprocesser""" + return qml.transforms.core.TransformProgram(), execution_config + + dev = qml.device("null.qubit", wires=2) + dev.preprocess = preprocess.__func__ + + @staticmethod + @qml.qnode(dev) + def circuit(): + """circuit with custom ops""" + qml.TrotterProduct(_HAMILTONIAN, time=1.0, n=4, order=2) + CustomErrorOp1(0.31, [0]) + CustomErrorOp2(0.12, [1]) + qml.TrotterProduct(_HAMILTONIAN, time=1.0, n=4, order=4) + CustomErrorOp1(0.24, [1]) + CustomErrorOp2(0.73, [0]) + return qml.state() + + errors_types = ["MultiplicativeError", "AdditiveError", "SpectralNormError"] + + def test_computation(self): + """Test that _compute_algo_error are adding up errors as expected.""" + + _ = self.circuit() + algo_errors = _compute_algo_error(self.circuit.qtape) + assert len(algo_errors) == 3 + assert all(error in algo_errors for error in self.errors_types) + assert algo_errors["MultiplicativeError"].error == 0.31 * 0.24 + assert algo_errors["AdditiveError"].error == 0.73 + 0.12 + assert algo_errors["SpectralNormError"].error == 0.25 + 0.17998560822421455 + + def test_specs(self): + """Test that specs are tracking errors as expected.""" + + algo_errors = qml.specs(self.circuit)()["errors"] + assert len(algo_errors) == 3 + assert all(error in algo_errors for error in self.errors_types) + assert algo_errors["MultiplicativeError"].error == 0.31 * 0.24 + assert algo_errors["AdditiveError"].error == 0.73 + 0.12 + assert algo_errors["SpectralNormError"].error == 0.25 + 0.17998560822421455 diff --git a/tests/resource/test_specs.py b/tests/resource/test_specs.py index be31aaf1365..788348d2496 100644 --- a/tests/resource/test_specs.py +++ b/tests/resource/test_specs.py @@ -25,7 +25,7 @@ class TestSpecsTransform: """Tests for the transform specs using the QNode""" @pytest.mark.parametrize( - "diff_method, len_info", [("backprop", 11), ("parameter-shift", 12), ("adjoint", 11)] + "diff_method, len_info", [("backprop", 12), ("parameter-shift", 13), ("adjoint", 12)] ) def test_empty(self, diff_method, len_info): dev = qml.device("default.qubit", wires=1) @@ -57,7 +57,7 @@ def circ(): assert info["gradient_fn"] == "pennylane.gradients.parameter_shift.param_shift" @pytest.mark.parametrize( - "diff_method, len_info", [("backprop", 11), ("parameter-shift", 12), ("adjoint", 11)] + "diff_method, len_info", [("backprop", 12), ("parameter-shift", 13), ("adjoint", 12)] ) def test_specs(self, diff_method, len_info): """Test the specs transforms works in standard situations""" @@ -102,7 +102,7 @@ def circuit(x, y, add_RY=True): assert info["num_gradient_executions"] == 6 @pytest.mark.parametrize( - "diff_method, len_info", [("backprop", 11), ("parameter-shift", 12), ("adjoint", 11)] + "diff_method, len_info", [("backprop", 12), ("parameter-shift", 13), ("adjoint", 12)] ) def test_specs_state(self, diff_method, len_info): """Test specs works when state returned""" @@ -173,7 +173,7 @@ def test_expansion_strategy(self): info = qml.specs(circuit, expansion_strategy="device")(params) assert circuit.expansion_strategy == "gradient" - assert len(info) == 11 + assert len(info) == 12 def test_gradient_transform(self): """Test that a gradient transform is properly labelled""" diff --git a/tests/tape/test_qscript.py b/tests/tape/test_qscript.py index 9c002c4baf4..8493a8a80b3 100644 --- a/tests/tape/test_qscript.py +++ b/tests/tape/test_qscript.py @@ -448,7 +448,7 @@ def test_empty_qs_specs(self): assert qs.specs["num_diagonalizing_gates"] == 0 assert qs.specs["num_trainable_params"] == 0 - assert len(qs.specs) == 4 + assert len(qs.specs) == 5 assert qs._specs is qs.specs @@ -460,7 +460,7 @@ def test_specs_tape(self, make_script): specs = qs.specs assert qs._specs is specs - assert len(specs) == 4 + assert len(specs) == 5 gate_types = defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1}) gate_sizes = defaultdict(int, {1: 3, 2: 1}) diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index 5a1e42d72f7..3f8e8138035 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -551,7 +551,7 @@ def test_specs_empty_tape(self, make_empty_tape): assert tape.specs["num_diagonalizing_gates"] == 0 assert tape.specs["num_trainable_params"] == 0 - assert len(tape.specs) == 4 + assert len(tape.specs) == 5 def test_specs_tape(self, make_tape): """Tests that regular tapes return correct specifications""" @@ -559,7 +559,7 @@ def test_specs_tape(self, make_tape): specs = tape.specs - assert len(specs) == 4 + assert len(specs) == 5 gate_sizes = defaultdict(int, {1: 3, 2: 1}) gate_types = defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1}) @@ -577,7 +577,7 @@ def test_specs_add_to_tape(self, make_extendible_tape): tape = make_extendible_tape specs1 = tape.specs - assert len(specs1) == 4 + assert len(specs1) == 5 gate_sizes = defaultdict(int, {1: 3, 2: 1}) gate_types = defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 1}) @@ -599,7 +599,7 @@ def test_specs_add_to_tape(self, make_extendible_tape): specs2 = tape.specs - assert len(specs2) == 4 + assert len(specs2) == 5 gate_sizes = defaultdict(int, {1: 4, 2: 2}) gate_types = defaultdict(int, {"RX": 2, "Rot": 1, "CNOT": 2, "RZ": 1}) From 9b609981a9617b77850622941db010e38e8b9038 Mon Sep 17 00:00:00 2001 From: Utkarsh Date: Fri, 12 Apr 2024 16:00:29 -0400 Subject: [PATCH 4/6] Add algorithmic errors tracking to `qml.Tracker` (#5465) **Context:** Update `qml.Tracker` to track and combine error **Description of the Change:** Add algorithmic error tracking in the `simulator_tracking` device modifier. **Benefits:** `tracker` will track algorithmic errors. **Possible Drawbacks:** N/A **Related GitHub Issues:** N/A --------- Co-authored-by: Jay Soni --- doc/releases/changelog-dev.md | 4 ++++ pennylane/devices/modifiers/simulator_tracking.py | 12 ++++++++---- pennylane/tracker.py | 4 +++- .../default_qubit/test_default_qubit_tracking.py | 3 +++ tests/devices/modifiers/test_all_modifiers.py | 2 +- tests/devices/modifiers/test_simulator_tracking.py | 2 +- tests/devices/test_default_clifford.py | 1 + tests/devices/test_null_qubit.py | 1 + tests/resource/test_error/test_error.py | 13 +++++++++++++ 9 files changed, 35 insertions(+), 7 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index ecda32c06ea..6cba45117fa 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -209,6 +209,10 @@ [stim](https://github.com/quantumlib/Stim) `v1.13.0`. [(#5409)](https://github.com/PennyLaneAI/pennylane/pull/5409) +* `qml.specs` and `qml.Tracker` now return information about algorithmic errors for the qnode as well. + [(#5464)](https://github.com/PennyLaneAI/pennylane/pull/5464) + [(#5465)](https://github.com/PennyLaneAI/pennylane/pull/5465) + * `qml.specs` now returns information regarding algorithmic errors for the qnode as well. [(#5464)](https://github.com/PennyLaneAI/pennylane/pull/5464) diff --git a/pennylane/devices/modifiers/simulator_tracking.py b/pennylane/devices/modifiers/simulator_tracking.py index 72f86d93547..0ebde382ead 100644 --- a/pennylane/devices/modifiers/simulator_tracking.py +++ b/pennylane/devices/modifiers/simulator_tracking.py @@ -46,6 +46,7 @@ def execute(self, circuits, execution_config=DefaultExecutionConfig): results=r, shots=shots, resources=c.specs["resources"], + errors=c.specs["errors"], ) else: self.tracker.update( @@ -53,6 +54,7 @@ def execute(self, circuits, execution_config=DefaultExecutionConfig): executions=qpu_executions, results=r, resources=c.specs["resources"], + errors=c.specs["errors"], ) self.tracker.record() return results @@ -85,7 +87,7 @@ def execute_and_compute_derivatives(self, circuits, execution_config=DefaultExec if self.tracker.active: batch = (circuits,) if isinstance(circuits, QuantumScript) else circuits for c in batch: - self.tracker.update(resources=c.specs["resources"]) + self.tracker.update(resources=c.specs["resources"], errors=c.specs["errors"]) self.tracker.update( execute_and_derivative_batches=1, executions=len(batch), @@ -119,7 +121,7 @@ def execute_and_compute_jvp(self, circuits, tangents, execution_config=DefaultEx if self.tracker.active: batch = (circuits,) if isinstance(circuits, QuantumScript) else circuits for c in batch: - self.tracker.update(resources=c.specs["resources"]) + self.tracker.update(resources=c.specs["resources"], errors=c.specs["errors"]) self.tracker.update(execute_and_jvp_batches=1, executions=len(batch), jvps=len(batch)) self.tracker.record() @@ -153,7 +155,7 @@ def execute_and_compute_vjp( if self.tracker.active: batch = (circuits,) if isinstance(circuits, QuantumScript) else circuits for c in batch: - self.tracker.update(resources=c.specs["resources"]) + self.tracker.update(resources=c.specs["resources"], errors=c.specs["errors"]) self.tracker.update(execute_and_vjp_batches=1, executions=len(batch), vjps=len(batch)) self.tracker.record() return untracked_execute_and_compute_vjp(self, circuits, cotangents, execution_config) @@ -176,6 +178,7 @@ def simulator_tracking(cls: type) -> type: * ``executions``: the number of unique circuits that would be required on quantum hardware * ``shots``: the number of shots * ``resources``: the :class:`~.resource.Resources` for the executed circuit. + * ``"errors"``: combined algorithmic errors from the quantum operations executed by the qnode. * ``simulations``: the number of simulations performed. One simulation can cover multiple QPU executions, such as for non-commuting measurements and batched parameters. * ``batches``: The number of times :meth:`~pennylane.devices.Device.execute` is called. @@ -218,7 +221,8 @@ def execute(self, circuits, execution_config = qml.devices.DefaultExecutionConfi 'shots': [100], 'resources': [Resources(num_wires=1, num_gates=1, gate_types=defaultdict(, {'S': 1}), gate_sizes=defaultdict(, {1: 1}), depth=1, shots=Shots(total_shots=50, - shot_vector=(ShotCopies(50 shots x 1),)))]} + shot_vector=(ShotCopies(50 shots x 1),)))], + 'errors': {}} """ if not issubclass(cls, Device): diff --git a/pennylane/tracker.py b/pennylane/tracker.py index 5b07867fe2b..24aeda55df1 100644 --- a/pennylane/tracker.py +++ b/pennylane/tracker.py @@ -82,7 +82,9 @@ def circuit(x): gate_types=defaultdict(, {'RX': 1}), gate_sizes=defaultdict(, {1: 1}), depth=1, - shots=Shots(total_shots=100, shot_vector=(ShotCopies(100 shots x 1),)))} + shots=Shots(total_shots=100, shot_vector=(ShotCopies(100 shots x 1),))), + 'errors': {} + } >>> tracker.history.keys() dict_keys(['batches', 'simulations', 'executions', 'results', 'shots', 'resources']) >>> tracker.history['results'] diff --git a/tests/devices/default_qubit/test_default_qubit_tracking.py b/tests/devices/default_qubit/test_default_qubit_tracking.py index 3975a42145c..ff52c6e7239 100644 --- a/tests/devices/default_qubit/test_default_qubit_tracking.py +++ b/tests/devices/default_qubit/test_default_qubit_tracking.py @@ -59,6 +59,7 @@ def test_tracking_batch(self): "resources": [Resources(num_wires=1), Resources(num_wires=1), Resources(num_wires=1)], "derivative_batches": [1], "derivatives": [1], + "errors": [{}, {}, {}], } assert tracker.totals == { "batches": 2, @@ -73,6 +74,7 @@ def test_tracking_batch(self): "simulations": 1, "results": 1, "resources": Resources(num_wires=1), + "errors": {}, } def test_tracking_execute_and_derivatives(self): @@ -103,6 +105,7 @@ def test_tracking_execute_and_derivatives(self): "vjp_batches": [1], "execute_and_vjp_batches": [1], "resources": [Resources(num_wires=1)] * 12, + "errors": [{}] * 12, } def test_tracking_resources(self): diff --git a/tests/devices/modifiers/test_all_modifiers.py b/tests/devices/modifiers/test_all_modifiers.py index 69a40f3cf0b..fa315dbbc81 100644 --- a/tests/devices/modifiers/test_all_modifiers.py +++ b/tests/devices/modifiers/test_all_modifiers.py @@ -44,7 +44,7 @@ def execute(self, circuits, execution_config=qml.devices.DefaultExecutionConfig) # result unwrapped assert out == 0.0 - assert len(dev.tracker.history) == 6 + assert len(dev.tracker.history) == 7 assert dev.tracker.history["batches"] == [1] assert dev.tracker.history["simulations"] == [1] assert dev.tracker.history["executions"] == [1] diff --git a/tests/devices/modifiers/test_simulator_tracking.py b/tests/devices/modifiers/test_simulator_tracking.py index 2d889c2d46b..88109eed8f6 100644 --- a/tests/devices/modifiers/test_simulator_tracking.py +++ b/tests/devices/modifiers/test_simulator_tracking.py @@ -44,7 +44,7 @@ def execute(self, circuits, execution_config=qml.devices.DefaultExecutionConfig) out = dev.execute((tape1, tape2)) assert out == ((0.0, 0.0), 0.0) - assert len(dev.tracker.history) == 6 + assert len(dev.tracker.history) == 7 assert dev.tracker.history["batches"] == [1] assert dev.tracker.history["simulations"] == [1, 1] assert dev.tracker.history["executions"] == [2, 2] diff --git a/tests/devices/test_default_clifford.py b/tests/devices/test_default_clifford.py index 9ceb1fdb492..1ff88630ed7 100644 --- a/tests/devices/test_default_clifford.py +++ b/tests/devices/test_default_clifford.py @@ -484,6 +484,7 @@ def test_tracker(): "batches": [1, 1], "simulations": [1, 1], "executions": [1, 1], + "errors": [{}, {}], } diff --git a/tests/devices/test_null_qubit.py b/tests/devices/test_null_qubit.py index 775ae7adde9..507fc9f2ea4 100644 --- a/tests/devices/test_null_qubit.py +++ b/tests/devices/test_null_qubit.py @@ -106,6 +106,7 @@ def test_tracking(): ) ] * 13, + "errors": [{}] * 13, } diff --git a/tests/resource/test_error/test_error.py b/tests/resource/test_error/test_error.py index d1b171bab44..4b690e49a18 100644 --- a/tests/resource/test_error/test_error.py +++ b/tests/resource/test_error/test_error.py @@ -282,3 +282,16 @@ def test_specs(self): assert algo_errors["MultiplicativeError"].error == 0.31 * 0.24 assert algo_errors["AdditiveError"].error == 0.73 + 0.12 assert algo_errors["SpectralNormError"].error == 0.25 + 0.17998560822421455 + + def test_tracker(self): + """Test that tracker are tracking errors as expected.""" + + with qml.Tracker(self.dev) as tracker: + self.circuit() + + algo_errors = tracker.latest["errors"] + assert len(algo_errors) == 3 + assert all(error in algo_errors for error in self.errors_types) + assert algo_errors["MultiplicativeError"].error == 0.31 * 0.24 + assert algo_errors["AdditiveError"].error == 0.73 + 0.12 + assert algo_errors["SpectralNormError"].error == 0.25 + 0.17998560822421455 From 3beb4be97435bdedf59c47edb1cc0ca35198e276 Mon Sep 17 00:00:00 2001 From: Christina Lee Date: Fri, 12 Apr 2024 16:43:43 -0400 Subject: [PATCH 5/6] Add document explaining return types specification (#5418) **Context:** The return type specification can often be a source of confusion, and is currently only documented in a partial form in `pennyane.devices.Device.execute`. **Description of the Change:** Adds `workflow/return_types_spec.rst` explaining and providing examples for the return types specification. **Benefits:** Eliminates possible sources of confusion. **Possible Drawbacks:** Needs to be maintained as pennylane evolves. **Related GitHub Issues:** [sc-48846] --------- Co-authored-by: Josh Izaac Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Thomas R. Bromley <49409390+trbromley@users.noreply.github.com> --- doc/releases/changelog-dev.md | 3 + pennylane/devices/device_api.py | 2 + pennylane/workflow/__init__.py | 2 + pennylane/workflow/return_types_spec.rst | 179 +++++++++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 pennylane/workflow/return_types_spec.rst diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 6cba45117fa..944aa066cad 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -329,6 +329,9 @@

Documentation 📝

+* Adds a page explaining the shapes and nesting of result objects. + [(#5418)](https://github.com/PennyLaneAI/pennylane/pull/5418) + * Removed some redundant documentation for the `evolve` function. [(#5347)](https://github.com/PennyLaneAI/pennylane/pull/5347) diff --git a/pennylane/devices/device_api.py b/pennylane/devices/device_api.py index 8c5639002ac..2f6abd19f1f 100644 --- a/pennylane/devices/device_api.py +++ b/pennylane/devices/device_api.py @@ -324,6 +324,8 @@ def execute( .. details:: :title: Return Shape + See :ref:`Return Type Specification ` for more detailed information. + The result for each :class:`~.QuantumTape` must match the shape specified by :class:`~.QuantumTape.shape`. The level of priority for dimensions from outer dimension to inner dimension is: diff --git a/pennylane/workflow/__init__.py b/pennylane/workflow/__init__.py index e8c2d739785..5103c404a3f 100644 --- a/pennylane/workflow/__init__.py +++ b/pennylane/workflow/__init__.py @@ -52,6 +52,8 @@ ~workflow.jacobian_products.DeviceJacobianProducts ~workflow.jacobian_products.LightningVJPs +.. include:: ../../pennylane/workflow/return_types_spec.rst + """ from .set_shots import set_shots from .execution import execute, SUPPORTED_INTERFACES, INTERFACE_MAP diff --git a/pennylane/workflow/return_types_spec.rst b/pennylane/workflow/return_types_spec.rst new file mode 100644 index 00000000000..57ff7c6e196 --- /dev/null +++ b/pennylane/workflow/return_types_spec.rst @@ -0,0 +1,179 @@ + +.. _ReturnTypeSpec: + +Return Type Specification +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This section describes the shape and type of the numerical output from executing a quantum circuit +in PennyLane. + +The specification applies for the entire workflow, from the device instance all the +way up to the ``QNode``. The result object corresponding to a given circuit +will match whether the circuit is being passed to a device, processed +by a transform, having it's derivative bound to an ML interface, or returned from a ``QNode``. + +While this section says ``tuple`` and includes examples using ``tuple`` throughout this document, the +return type specification allows ``tuple`` and ``list`` to be used interchangably. +When examining and postprocessing +results, you should always allow for a ``list`` to be substituted for a ``tuple``. Given their +improved performance and protection against unintended side-effects, ``tuple``'s are recommended +over ``list`` where feasible. + +The nesting for dimensions from outer dimension to inner dimension is: + +1. Quantum Tape in batch. This dimension will always exist for a batch of tapes. +2. Shot choice in a shot vector. This dimension will not exist of a shot vector is not present. +3. Measurement in the quantum tape. This dimension will not exist if the quantum tape only has one measurement. +4. Parameter broadcasting. Does not exist if no parameter broadcasting. Adds to array shape instead of adding tuple nesting. +5. Fundamental measurement shape. + +Individual measurements +----------------------- + +Each individual measurement corresponds to its own type of result. This result can be +a Tensor-like (Python number, numpy array, ML array), but may also be any other type of object. +For example, :class:`~.CountsMP` corresponds to a dictionary. We can also imagine a scenario where +a measurement corresponds to some other type of custom data structure. + +>>> def example_value(m): +... tape = qml.tape.QuantumScript((), (m,), shots=50) +... return qml.device('default.qubit').execute(tape) +>>> example_value(qml.probs(wires=0)) +array([1., 0.]) +>>> example_value(qml.expval(qml.Z(0))) +1.0 +>>> example_value(qml.counts(wires=0)) +{'0': 50} +>>> example_value(qml.sample(wires=0)) +array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0]) + + +Empty Wires +^^^^^^^^^^^ + +Some measurments allow broadcasting over all available wires, like ``qml.probs()``, ``qml.sample()``, +or ``qml.state()``. In such a case, the measurement process instance should have empty wires. +The shape of the result object may be dictated either by the device or the other operations present in the circuit. + +>>> qml.probs().wires + +>>> tape = qml.tape.QuantumScript([qml.S(0)], (qml.probs(),)) +>>> qml.device('default.qubit').execute(tape) +array([1., 0.]) +>>> qml.device('lightning.qubit', wires=(0,1,2)).execute(tape) +array([1., 0., 0., 0., 0., 0., 0., 0.]) + +Broadcasting +^^^^^^^^^^^^ + +Parameter broadcasting adds a leading dimension to the numeric array itself. + +If the corresponding tape has a ``batch_size`` and the result object is numeric, then the numeric object should +gain a leading dimension. + +>>> op = qml.RX((0, np.pi/4, np.pi/2), wires=0)) +>>> tape = qml.tape.QuantumScript((op,), [qml.probs(wires=0)]) +>>> result = qml.device('default.qubit').execute(tape) +>>> result +array([[1. , 0. ], + [0.85355339, 0.14644661], + [0.5 , 0.5 ]]) +>>> result.shape +(3, 2) +>>> tape = qml.tape.QuantumScript((op,), [qml.expval(qml.Z(0))]) +>>> result = qml.device('default.qubit').execute(tape) +>>> result +array([1.00000000e+00, 7.07106781e-01, 2.22044605e-16]) +>>> result.shape +(3,) + +Non-tensorlike arrays may handle broadcasting in different ways. The ``'default.qubit'`` output +for :class:`~.CountsMP` is a list of dictionaries, but when used in conjunction with +:func:`~.transforms.broadcast_expand`, the result object becomes a ``numpy.ndarray`` of dtype ``object``. + +>>> tape = qml.tape.QuantumScript((op,), (qml.counts(),), shots=50) +>>> result = qml.device('default.qubit').execute(tape) +>>> result +[{'0': 50}, {'0': 46, '1': 4}, {'0': 32, '1': 18}] +>>> batch, fn = qml.transforms.broadcast_expand(tape) +>>> fn(qml.device('default.qubit').execute(batch)) +array([{'0': 50}, {'0': 39, '1': 11}, {'0': 28, '1': 22}], dtype=object) + + +Single Tape +----------- + +If the tape has a single measurement, then the result corresponding to that tape simply obeys the specification +above. Otherwise, the result for a single tape is a ``tuple`` where each entry corresponds to each +of the corresponding measurements. In the below example, the first entry corresponds to the first +measurement process ``qml.expval(qml.Z(0))``, the second entry corresponds to the second measurement process +``qml.probs(wires=0)``, and the third result corresponds to the third measurement process ``qml.state()``. + +>>> tape = qml.tape.QuantumScript((), (qml.expval(qml.Z(0)), qml.probs(wires=0), qml.state())) +>>> qml.device('default.qubit').execute(tape) +(1.0, array([1., 0.]), array([1.+0.j, 0.+0.j])) + +Shot vectors +^^^^^^^^^^^^ + +When a shot vector is present ``shots.has_partitioned_shot``, the measurement instead becomes a +tuple where each entry corresponds to a different shot value. + +>>> measurements = (qml.expval(qml.Z(0)), qml.probs(wires=0)) +>>> tape = qml.tape.QuantumScript((), measurements, shots=(50,50,50)) +>>> result = qml.device('default.qubit').execute(tape) +>>> result +((1.0, array([1., 0.])), (1.0, array([1., 0.])), (1.0, array([1., 0.]))) +>>> result[0] +(1.0, array([1., 0.])) +>>> tape = qml.tape.QuantumScript((), [qml.counts(wires=0)], shots=(1, 10, 100)) +>>> qml.device('default.qubit').execute(tape) +({'0': 1}, {'0': 10}, {'0': 100}) + +Let's look at an example with all forms of nesting. Here, we have a tape with a batch size of ``3``, three +diferent measurements with different fundamental shapes, and a shot vector with three different values. + +>>> op = qml.RX((1.2, 2.3, 3.4), 0) +>>> ms = (qml.expval(qml.Z(0)), qml.probs(wires=0), qml.counts()) +>>> tape = qml.tape.QuantumScript((op,), ms, shots=(1, 100, 1000)) +>>> result = qml.device('default.qubit').execute(tape) +>>> result +((array([ 1., -1., -1.]), +array([[1., 0.], + [0., 1.], + [0., 1.]]), +[{'0': 1}, {'1': 1}, {'1': 1}]), +(array([ 0.3 , -0.66, -0.98]), +array([[0.61, 0.39], + [0.13, 0.87], + [0.03, 0.97]]), +[{'0': 61, '1': 39}, {'0': 13, '1': 87}, {'0': 3, '1': 97}]), +(array([ 0.364, -0.648, -0.962]), +array([[0.669, 0.331], + [0.165, 0.835], + [0.012, 0.988]]), +[{'0': 669, '1': 331}, {'0': 165, '1': 835}, {'0': 12, '1': 988}])) + + +>>> result[0][0] # first shot value, first measurement +array([ 1., -1., -1.]) +>>> result[0][0][0] # first shot value, first measurement, and parameter of 1.2 +1.0 +>>> result[1][2] # second shot value, third measurement, all three parameter values +[{'0': 74, '1': 26}, {'0': 23, '1': 77}, {'1': 100}] + + +Batches +------- + +A batch is a tuple or list of multiple tapes. In this case, the result should always be a tuple +where each entry corresponds to the result for the corresponding tape. + +>>> tape1 = qml.tape.QuantumScript([qml.X(0)], [qml.state()]) +>>> tape2 = qml.tape.QuantumScript([qml.Hadamard(0)], [qml.counts()], shots=100) +>>> tape3 = qml.tape.QuantumScript([], [qml.expval(qml.Z(0)), qml.expval(qml.X(0))]) +>>> batch = (tape1, tape2, tape3) +>>> qml.device('default.qubit').execute(batch) +(array([0.+0.j, 1.+0.j]), {'0': 50, '1': 50}, (1.0, 0.0)) \ No newline at end of file From f7834de2bbb0f785bf79162d09142fa36476f567 Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Fri, 12 Apr 2024 17:49:35 -0400 Subject: [PATCH 6/6] Extend the device test suite: gradient, op-arithmetic and template tests (#5273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I ran the plugin test suite against this branch so I could see which tests fail right away on which devices (done, see [here](https://github.com/PennyLaneAI/plugin-test-matrix/actions?query=workflow:*-timmy-latest), braket and quantuminspire are failing for their own reasons), and skip them for those devices. Everything is now passing as expected. **Context:** The device test suite doesn't cover a lot of PennyLane's features, so I'm adding more coverage. **Description of the Change:** - modified CI to install interfaces if needed (basically always except for `default.qubit.autograd`) - updated `test_measurements.py` to test some basic arithmetic ops by comparing the result to `default.qubit` - Added 5 basic differentiation tests for each interface: `test_basic_grad`, `test_backprop_state`, `test_parameter_shift`, `test_probs`, `test_multi_meas` `test_hessian`. They do what they sound like they do 😄 - Added a test for every single template in PennyLane 🐳 I just grabbed the example from each template's docstring, got the result with default.qubit, and I'm asserting that every other device also gets that result. If the docstring didn't have one (or it just returned 1 when there are better examples), I tried to get one from the tests for that template. I'd also try to swap `qml.state()` with `qml.probs()` whenever possible so the test would work with finite-shot devices. **Benefits:** More confidence in our plugin devices! **Possible Drawbacks:** - Slower device tests - New failures to manage? - Some tests needed explicit skips from certain devices. I opened a story to track those explicit skips being done in the device test suite upgrade epic [sc-57488] --------- Co-authored-by: Christina Lee Co-authored-by: Mudit Pandey --- .github/workflows/interface-unit-tests.yml | 7 +- doc/releases/changelog-dev.md | 3 + pennylane/_qubit_device.py | 2 +- pennylane/devices/tests/conftest.py | 16 +- .../devices/tests/test_gradients_autograd.py | 201 ++++ pennylane/devices/tests/test_gradients_jax.py | 216 ++++ pennylane/devices/tests/test_gradients_tf.py | 226 ++++ .../devices/tests/test_gradients_torch.py | 218 ++++ pennylane/devices/tests/test_measurements.py | 31 +- pennylane/devices/tests/test_templates.py | 1006 +++++++++++++++++ 10 files changed, 1919 insertions(+), 7 deletions(-) create mode 100644 pennylane/devices/tests/test_gradients_autograd.py create mode 100644 pennylane/devices/tests/test_gradients_jax.py create mode 100644 pennylane/devices/tests/test_gradients_tf.py create mode 100644 pennylane/devices/tests/test_gradients_torch.py create mode 100644 pennylane/devices/tests/test_templates.py diff --git a/.github/workflows/interface-unit-tests.yml b/.github/workflows/interface-unit-tests.yml index fca423794b1..a5247123171 100644 --- a/.github/workflows/interface-unit-tests.yml +++ b/.github/workflows/interface-unit-tests.yml @@ -510,6 +510,7 @@ jobs: # shots: None - device: default.qubit.autograd shots: None + skip_interface: jax,tf,torch - device: default.mixed shots: None python-version: >- @@ -525,9 +526,9 @@ jobs: coverage_artifact_name: devices-coverage-${{ matrix.config.device }}-${{ matrix.config.shots }} python_version: ${{ matrix.python-version }} pipeline_mode: ${{ inputs.pipeline_mode }} - install_jax: ${{ contains(matrix.config.device, 'jax') }} - install_tensorflow: ${{ contains(matrix.config.device, 'tf') }} - install_pytorch: ${{ contains(matrix.config.device, 'torch') }} + install_jax: ${{ !contains(matrix.config.skip_interface, 'jax') }} + install_tensorflow: ${{ !contains(matrix.config.skip_interface, 'tf') }} + install_pytorch: ${{ !contains(matrix.config.skip_interface, 'torch') }} install_pennylane_lightning_master: false pytest_test_directory: pennylane/devices/tests pytest_coverage_flags: ${{ inputs.pytest_coverage_flags }} diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 944aa066cad..b4456e59708 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -248,6 +248,9 @@ [(#5256)](https://github.com/PennyLaneAI/pennylane/pull/5256) [(#5395)](https://github.com/PennyLaneAI/pennylane/pull/5395) +* Extend the device test suite to cover gradient methods, templates and arithmetic observables. + [(#5273)](https://github.com/PennyLaneAI/pennylane/pull/5273) + * Add type hints for unimplemented methods of the abstract class `Operator`. [(#5490)](https://github.com/PennyLaneAI/pennylane/pull/5490) diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py index e52a3a66704..c02e7443765 100644 --- a/pennylane/_qubit_device.py +++ b/pennylane/_qubit_device.py @@ -152,7 +152,7 @@ def _const_mul(constant, array): "Identity", "Projector", "Sum", - "Sprod", + "SProd", "Prod", } diff --git a/pennylane/devices/tests/conftest.py b/pennylane/devices/tests/conftest.py index ce4d823fe3d..62d6592bc0a 100755 --- a/pennylane/devices/tests/conftest.py +++ b/pennylane/devices/tests/conftest.py @@ -85,8 +85,20 @@ def _skip_if(dev, capabilities): return _skip_if -@pytest.fixture(scope="function") -def device(device_kwargs): +@pytest.fixture +def validate_diff_method(device, diff_method, device_kwargs): + """Skip tests if a device does not support a diff_method""" + if diff_method == "backprop" and device_kwargs.get("shots") is not None: + pytest.skip(reason="test should only be run in analytic mode") + dev = device(1) + if isinstance(dev, qml.Device): + passthru_devices = dev.capabilities().get("passthru_devices") + if diff_method == "backprop" and passthru_devices is None: + pytest.skip(reason="device does not support backprop") + + +@pytest.fixture(scope="function", name="device") +def fixture_device(device_kwargs): """Fixture to create a device.""" # internally used by pytest diff --git a/pennylane/devices/tests/test_gradients_autograd.py b/pennylane/devices/tests/test_gradients_autograd.py new file mode 100644 index 00000000000..2b0414ee184 --- /dev/null +++ b/pennylane/devices/tests/test_gradients_autograd.py @@ -0,0 +1,201 @@ +# 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 trainable circuits using the Autograd interface.""" +# pylint:disable=no-self-use +import pytest + +import numpy as np + +import pennylane as qml +from pennylane import numpy as pnp + + +@pytest.mark.usefixtures("validate_diff_method") +@pytest.mark.parametrize("diff_method", ["backprop", "parameter-shift", "hadamard"]) +class TestGradients: + """Test various gradient computations.""" + + def test_basic_grad(self, diff_method, device, tol): + """Test a basic function with one RX and one expectation.""" + wires = 2 if diff_method == "hadamard" else 1 + dev = device(wires=wires) + tol = tol(dev.shots) + if diff_method == "hadamard": + tol += 0.01 + + @qml.qnode(dev, diff_method=diff_method) + def circuit(x): + qml.RX(x, 0) + return qml.expval(qml.Z(0)) + + x = pnp.array(0.5) + res = qml.grad(circuit)(x) + assert np.isclose(res, -pnp.sin(x), atol=tol, rtol=0) + + def test_backprop_state(self, diff_method, device, tol): + """Test the trainability of parameters in a circuit returning the state.""" + if diff_method != "backprop": + pytest.skip(reason="test only works with backprop") + dev = device(2) + if dev.shots: + pytest.skip("test uses backprop, must be in analytic mode") + if "mixed" in dev.name: + pytest.skip("mixed-state simulator will wrongly use grad on non-scalar results") + tol = tol(dev.shots) + + x = pnp.array(0.543) + y = pnp.array(-0.654) + + @qml.qnode(dev, diff_method=diff_method, grad_on_execution=True) + def circuit(x, y): + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.state() + + def cost_fn(x, y): + res = circuit(x, y) + probs = pnp.abs(res) ** 2 + return probs[0] + probs[2] + + res = qml.grad(cost_fn)(x, y) + expected = np.array([-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2]) + assert np.allclose(res, expected, atol=tol, rtol=0) + + y = pnp.array(-0.654, requires_grad=False) + res = qml.grad(cost_fn)(x, y) + assert np.allclose(res, expected[0], atol=tol, rtol=0) + + def test_parameter_shift(self, diff_method, device, tol): + """Test a multi-parameter circuit with parameter-shift.""" + if diff_method != "parameter-shift": + pytest.skip(reason="test only works with parameter-shift") + + a = pnp.array(0.1) + b = pnp.array(0.2) + + dev = device(2) + tol = tol(dev.shots) + + @qml.qnode(dev, diff_method="parameter-shift", grad_on_execution=False) + def circuit(a, b): + qml.RY(a, wires=0) + qml.RX(b, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.Hamiltonian([1, 1], [qml.Z(0), qml.Y(1)])) + + res = qml.grad(circuit)(a, b) + expected = [-np.sin(a) + np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)] + assert np.allclose(res, expected, atol=tol, rtol=0) + + # make the second QNode argument a constant + b = pnp.array(0.2, requires_grad=False) + res = qml.grad(circuit)(a, b) + assert np.allclose(res, expected[0], atol=tol, rtol=0) + + def test_probs(self, diff_method, device, tol): + """Test differentiation of a circuit returning probs().""" + wires = 3 if diff_method == "hadamard" else 2 + dev = device(wires=wires) + tol = tol(dev.shots) + x = pnp.array(0.543) + y = pnp.array(-0.654) + + @qml.qnode(dev, diff_method=diff_method) + def circuit(x, y): + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.probs(wires=[1]) + + res = qml.jacobian(circuit)(x, y) + + expected = np.array( + [ + [-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2], + [np.cos(y) * np.sin(x) / 2, np.cos(x) * np.sin(y) / 2], + ] + ) + + assert isinstance(res, tuple) + assert len(res) == 2 + + assert isinstance(res[0], pnp.ndarray) + assert res[0].shape == (2,) + + assert isinstance(res[1], pnp.ndarray) + assert res[1].shape == (2,) + + if diff_method == "hadamard" and "raket" in dev.name: + pytest.xfail(reason="braket gets wrong results for hadamard here") + assert np.allclose(res[0], expected.T[0], atol=tol, rtol=0) + assert np.allclose(res[1], expected.T[1], atol=tol, rtol=0) + + def test_multi_meas(self, diff_method, device, tol): + """Test differentiation of a circuit with both scalar and array-like returns.""" + wires = 3 if diff_method == "hadamard" else 2 + dev = device(wires=wires) + tol = tol(dev.shots) + x = pnp.array(0.543) + y = pnp.array(-0.654, requires_grad=False) + + @qml.qnode(dev, diff_method=diff_method) + def circuit(x, y): + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.Z(0)), qml.probs(wires=[1]) + + def cost_fn(x, y): + return pnp.hstack(circuit(x, y)) + + jac = qml.jacobian(cost_fn)(x, y) + + expected = [-np.sin(x), -np.sin(x) * np.cos(y) / 2, np.cos(y) * np.sin(x) / 2] + assert isinstance(jac, pnp.ndarray) + assert np.allclose(jac, expected, atol=tol, rtol=0) + + def test_hessian(self, diff_method, device, tol): + """Test hessian computation.""" + wires = 3 if diff_method == "hadamard" else 1 + dev = device(wires=wires) + tol = tol(dev.shots) + + @qml.qnode(dev, diff_method=diff_method, max_diff=2) + def circuit(x): + qml.RY(x[0], wires=0) + qml.RX(x[1], wires=0) + return qml.expval(qml.Z(0)) + + x = pnp.array([1.0, 2.0]) + res = circuit(x) + + a, b = x + + expected_res = np.cos(a) * np.cos(b) + assert np.allclose(res, expected_res, atol=tol, rtol=0) + + grad_fn = qml.grad(circuit) + g = grad_fn(x) + + expected_g = [-np.sin(a) * np.cos(b), -np.cos(a) * np.sin(b)] + assert np.allclose(g, expected_g, atol=tol, rtol=0) + + hess = qml.jacobian(grad_fn)(x) + + expected_hess = [ + [-np.cos(a) * np.cos(b), np.sin(a) * np.sin(b)], + [np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)], + ] + assert np.allclose(hess, expected_hess, atol=tol, rtol=0) diff --git a/pennylane/devices/tests/test_gradients_jax.py b/pennylane/devices/tests/test_gradients_jax.py new file mode 100644 index 00000000000..8e9b40c254c --- /dev/null +++ b/pennylane/devices/tests/test_gradients_jax.py @@ -0,0 +1,216 @@ +# 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 trainable circuits using the JAX interface.""" +# pylint:disable=no-self-use +import pytest + +import numpy as np + +import pennylane as qml + +jax = pytest.importorskip("jax") +jnp = pytest.importorskip("jax.numpy") + + +@pytest.mark.usefixtures("validate_diff_method") +@pytest.mark.parametrize("diff_method", ["backprop", "parameter-shift", "hadamard"]) +class TestGradients: + """Test various gradient computations.""" + + def test_basic_grad(self, diff_method, device, tol): + """Test a basic function with one RX and one expectation.""" + wires = 2 if diff_method == "hadamard" else 1 + dev = device(wires=wires) + tol = tol(dev.shots) + if diff_method == "hadamard": + tol += 0.01 + + @qml.qnode(dev, diff_method=diff_method) + def circuit(x): + qml.RX(x, 0) + return qml.expval(qml.Z(0)) + + x = jnp.array(0.5) + res = jax.grad(circuit)(x) + assert np.isclose(res, -jnp.sin(x), atol=tol, rtol=0) + + def test_backprop_state(self, diff_method, device, tol): + """Test the trainability of parameters in a circuit returning the state.""" + if diff_method != "backprop": + pytest.skip(reason="test only works with backprop") + dev = device(2) + if dev.shots: + pytest.skip("test uses backprop, must be in analytic mode") + if "mixed" in dev.name: + pytest.skip("mixed-state simulator will wrongly use grad on non-scalar results") + tol = tol(dev.shots) + + x = jnp.array(0.543) + y = jnp.array(-0.654) + + @qml.qnode(dev, diff_method="backprop", grad_on_execution=True) + def circuit(x, y): + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.state() + + def cost_fn(x, y): + res = circuit(x, y) + probs = jnp.abs(res) ** 2 + return probs[0] + probs[2] + + res = jax.grad(cost_fn, argnums=[0, 1])(x, y) + expected = np.array([-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2]) + assert np.allclose(res, expected, atol=tol, rtol=0) + + res = jax.grad(cost_fn, argnums=[0])(x, y) + assert np.allclose(res, expected[0], atol=tol, rtol=0) + + def test_parameter_shift(self, diff_method, device, tol): + """Test a multi-parameter circuit with parameter-shift.""" + if diff_method != "parameter-shift": + pytest.skip(reason="test only works with parameter-shift") + + a = jnp.array(0.1) + b = jnp.array(0.2) + + dev = device(2) + tol = tol(dev.shots) + + @qml.qnode(dev, diff_method="parameter-shift", grad_on_execution=False) + def circuit(a, b): + qml.RY(a, wires=0) + qml.RX(b, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.Hamiltonian([1, 1], [qml.Z(0), qml.Y(1)])) + + res = jax.grad(circuit, argnums=[0, 1])(a, b) + expected = [-np.sin(a) + np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)] + assert np.allclose(res, expected, atol=tol, rtol=0) + + # make the second QNode argument a constant + res = jax.grad(circuit, argnums=[0])(a, b) + assert np.allclose(res, expected[0], atol=tol, rtol=0) + + def test_probs(self, diff_method, device, tol): + """Test differentiation of a circuit returning probs().""" + wires = 3 if diff_method == "hadamard" else 2 + dev = device(wires=wires) + tol = tol(dev.shots) + x = jnp.array(0.543) + y = jnp.array(-0.654) + + @qml.qnode(dev, diff_method=diff_method) + def circuit(x, y): + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.probs(wires=[1]) + + res = jax.jacobian(circuit, argnums=[0, 1])(x, y) + + expected = np.array( + [ + [-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2], + [np.cos(y) * np.sin(x) / 2, np.cos(x) * np.sin(y) / 2], + ] + ) + + assert isinstance(res, tuple) + assert len(res) == 2 + + assert isinstance(res[0], jnp.ndarray) + assert res[0].shape == (2,) + + assert isinstance(res[1], jnp.ndarray) + assert res[1].shape == (2,) + + if diff_method == "hadamard" and "raket" in dev.name: + pytest.xfail(reason="braket gets wrong results for hadamard here") + assert np.allclose(res[0], expected.T[0], atol=tol, rtol=0) + assert np.allclose(res[1], expected.T[1], atol=tol, rtol=0) + + def test_multi_meas(self, diff_method, device, tol): + """Test differentiation of a circuit with both scalar and array-like returns.""" + wires = 3 if diff_method == "hadamard" else 2 + dev = device(wires=wires) + tol = tol(dev.shots) + x = jnp.array(0.543) + y = jnp.array(-0.654) + + @qml.qnode(dev, diff_method=diff_method) + def circuit(x, y): + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.Z(0)), qml.probs(wires=[1]) + + jac = jax.jacobian(circuit, argnums=[0])(x, y) + + expected = [ + [-np.sin(x), 0], + [ + [-np.sin(x) * np.cos(y) / 2, np.cos(y) * np.sin(x) / 2], + [-np.cos(x) * np.sin(y) / 2, np.cos(x) * np.sin(y) / 2], + ], + ] + assert isinstance(jac, tuple) + assert len(jac) == 2 + + assert isinstance(jac[0], tuple) + assert len(jac[0]) == 1 + assert isinstance(jac[0][0], jnp.ndarray) + assert jac[0][0].shape == () + assert np.allclose(jac[0][0], expected[0][0], atol=tol, rtol=0) + + assert isinstance(jac[1], tuple) + assert len(jac[1]) == 1 + assert isinstance(jac[1][0], jnp.ndarray) + assert jac[1][0].shape == (2,) + assert np.allclose(jac[1][0], expected[1][0], atol=tol, rtol=0) + + def test_hessian(self, diff_method, device, tol): + """Test hessian computation.""" + wires = 3 if diff_method == "hadamard" else 1 + dev = device(wires=wires) + tol = tol(dev.shots) + + @qml.qnode(dev, diff_method=diff_method, max_diff=2) + def circuit(x): + qml.RY(x[0], wires=0) + qml.RX(x[1], wires=0) + return qml.expval(qml.Z(0)) + + x = jnp.array([1.0, 2.0]) + res = circuit(x) + + a, b = x + + expected_res = np.cos(a) * np.cos(b) + assert np.allclose(res, expected_res, atol=tol, rtol=0) + + grad_fn = jax.grad(circuit) + g = grad_fn(x) + + expected_g = [-np.sin(a) * np.cos(b), -np.cos(a) * np.sin(b)] + assert np.allclose(g, expected_g, atol=tol, rtol=0) + + hess = jax.jacobian(grad_fn)(x) + + expected_hess = [ + [-np.cos(a) * np.cos(b), np.sin(a) * np.sin(b)], + [np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)], + ] + assert np.allclose(hess, expected_hess, atol=tol, rtol=0) diff --git a/pennylane/devices/tests/test_gradients_tf.py b/pennylane/devices/tests/test_gradients_tf.py new file mode 100644 index 00000000000..2621060795e --- /dev/null +++ b/pennylane/devices/tests/test_gradients_tf.py @@ -0,0 +1,226 @@ +# 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 trainable circuits using the TensorFlow interface.""" +# pylint:disable=no-self-use +import pytest + +import numpy as np + +import pennylane as qml + +tf = pytest.importorskip("tensorflow") + + +@pytest.mark.usefixtures("validate_diff_method") +@pytest.mark.parametrize("diff_method", ["backprop", "parameter-shift", "hadamard"]) +class TestGradients: + """Test various gradient computations.""" + + @pytest.fixture(autouse=True) + def skip_if_braket(self, device): + """Skip braket tests with tensorflow.""" + dev = device(1) + if "raket" in dev.name: + pytest.skip(reason="braket cannot convert TF variables to literal") + + def test_basic_grad(self, diff_method, device, tol): + """Test a basic function with one RX and one expectation.""" + wires = 2 if diff_method == "hadamard" else 1 + dev = device(wires=wires) + tol = tol(dev.shots) + if diff_method == "hadamard": + tol += 0.01 + + @qml.qnode(dev, diff_method=diff_method) + def circuit(x): + qml.RX(x, 0) + return qml.expval(qml.Z(0)) + + x = tf.Variable(0.5) + with tf.GradientTape() as tape: + res = circuit(x) + grad = tape.gradient(res, x) + assert np.isclose(grad, -np.sin(x), atol=tol, rtol=0) + + def test_backprop_state(self, diff_method, device, tol): + """Test the trainability of parameters in a circuit returning the state.""" + if diff_method != "backprop": + pytest.skip(reason="test only works with backprop") + dev = device(2) + if dev.shots: + pytest.skip("test uses backprop, must be in analytic mode") + if "mixed" in dev.name: + pytest.skip("mixed-state simulator will wrongly use grad on non-scalar results") + tol = tol(dev.shots) + + x = tf.Variable(0.543) + y = tf.Variable(-0.654) + + @qml.qnode(dev, diff_method="backprop", grad_on_execution=True) + def circuit(x, y): + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.state() + + def cost_fn(x, y): + res = circuit(x, y) + probs = tf.abs(res) ** 2 + return probs[0] + probs[2] + + with tf.GradientTape() as tape: + res = cost_fn(x, y) + grad = tape.gradient(res, [x, y]) + expected = np.array([-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2]) + assert np.allclose(grad, expected, atol=tol, rtol=0) + + def test_parameter_shift(self, diff_method, device, tol): + """Test a multi-parameter circuit with parameter-shift.""" + if diff_method != "parameter-shift": + pytest.skip(reason="test only works with parameter-shift") + + a = tf.Variable(0.1) + b = tf.Variable(0.2) + + dev = device(2) + tol = tol(dev.shots) + + @qml.qnode(dev, diff_method="parameter-shift", grad_on_execution=False) + def circuit(a, b): + qml.RY(a, wires=0) + qml.RX(b, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.Hamiltonian([1, 1], [qml.Z(0), qml.Y(1)])) + + with tf.GradientTape() as tape: + res = circuit(a, b) + grad = tape.gradient(res, [a, b]) + expected = [-np.sin(a) + np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)] + assert np.allclose(grad, expected, atol=tol, rtol=0) + + def test_probs(self, diff_method, device, tol): + """Test differentiation of a circuit returning probs().""" + wires = 3 if diff_method == "hadamard" else 2 + dev = device(wires=wires) + tol = tol(dev.shots) + x = tf.Variable(0.543) + y = tf.Variable(-0.654) + + @qml.qnode(dev, diff_method=diff_method) + def circuit(x, y): + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.probs(wires=[1]) + + with tf.GradientTape() as tape: + res0 = circuit(x, y) + res = tape.jacobian(res0, [x, y]) + + expected = np.array( + [ + [-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2], + [np.cos(y) * np.sin(x) / 2, np.cos(x) * np.sin(y) / 2], + ] + ) + + assert isinstance(res, list) + assert len(res) == 2 + + assert isinstance(res[0], tf.Tensor) + assert res[0].shape == (2,) + + assert isinstance(res[1], tf.Tensor) + assert res[1].shape == (2,) + + assert np.allclose(res[0], expected.T[0], atol=tol, rtol=0) + assert np.allclose(res[1], expected.T[1], atol=tol, rtol=0) + + def test_multi_meas(self, diff_method, device, tol): + """Test differentiation of a circuit with both scalar and array-like returns.""" + wires = 3 if diff_method == "hadamard" else 2 + dev = device(wires=wires) + tol = tol(dev.shots) + x = tf.Variable(0.543) + y = tf.Variable(-0.654) + + @qml.qnode(dev, diff_method=diff_method) + def circuit(x, y): + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.Z(0)), qml.probs(wires=[1]) + + with tf.GradientTape() as tape: + res = circuit(x, y) + res = tf.experimental.numpy.hstack(res) + + jac = tape.jacobian(res, [x, y]) + + expected = np.array( + [ + [-np.sin(x), -np.sin(x) * np.cos(y) / 2, np.cos(y) * np.sin(x) / 2], + [0.0, -np.cos(x) * np.sin(y) / 2, np.cos(x) * np.sin(y) / 2], + ] + ) + assert isinstance(jac, list) + assert len(jac) == 2 + assert all(isinstance(j, tf.Tensor) and j.shape == (3,) for j in jac) + assert np.allclose(jac, expected, atol=tol, rtol=0) + + # assert isinstance(jac[0], tuple) + # assert len(jac[0]) == 1 + # assert isinstance(jac[0][0], tf.Tensor) + # assert jac[0][0].shape == () + # assert np.allclose(jac[0][0], expected[0][0], atol=tol, rtol=0) + + # assert isinstance(jac[1], tuple) + # assert len(jac[1]) == 1 + # assert isinstance(jac[1][0], tf.Tensor) + # assert jac[1][0].shape == (2,) + # assert np.allclose(jac[1][0], expected[1][0], atol=tol, rtol=0) + + def test_hessian(self, diff_method, device, tol): + """Test hessian computation.""" + wires = 3 if diff_method == "hadamard" else 1 + dev = device(wires=wires) + tol = tol(dev.shots) + + @qml.qnode(dev, diff_method=diff_method, max_diff=2) + def circuit(x): + qml.RY(x[0], wires=0) + qml.RX(x[1], wires=0) + return qml.expval(qml.Z(0)) + + x = tf.Variable([1.0, 2.0], dtype=tf.float64) + + with tf.GradientTape() as tape1: + with tf.GradientTape() as tape2: + res = circuit(x) + g = tape2.gradient(res, x) + + hess = tape1.jacobian(g, x) + a, b = x + + expected_res = np.cos(a) * np.cos(b) + assert np.allclose(res, expected_res, atol=tol, rtol=0) + + expected_g = [-np.sin(a) * np.cos(b), -np.cos(a) * np.sin(b)] + assert np.allclose(g, expected_g, atol=tol, rtol=0) + + expected_hess = [ + [-np.cos(a) * np.cos(b), np.sin(a) * np.sin(b)], + [np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)], + ] + assert np.allclose(hess, expected_hess, atol=tol, rtol=0) diff --git a/pennylane/devices/tests/test_gradients_torch.py b/pennylane/devices/tests/test_gradients_torch.py new file mode 100644 index 00000000000..f4644535cb8 --- /dev/null +++ b/pennylane/devices/tests/test_gradients_torch.py @@ -0,0 +1,218 @@ +# 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 trainable circuits using the Torch interface.""" +# pylint:disable=no-self-use,no-member +import pytest + +import numpy as np + +import pennylane as qml + +torch = pytest.importorskip("torch") + + +@pytest.mark.usefixtures("validate_diff_method") +@pytest.mark.parametrize("diff_method", ["backprop", "parameter-shift", "hadamard"]) +class TestGradients: + """Test various gradient computations.""" + + def test_basic_grad(self, diff_method, device, tol): + """Test a basic function with one RX and one expectation.""" + wires = 2 if diff_method == "hadamard" else 1 + dev = device(wires=wires) + tol = tol(dev.shots) + if diff_method == "hadamard": + tol += 0.01 + + @qml.qnode(dev, diff_method=diff_method) + def circuit(x): + qml.RX(x, 0) + return qml.expval(qml.Z(0)) + + x = torch.tensor(0.5, requires_grad=True) + res = circuit(x) + res.backward() + assert np.isclose(x.grad, -np.sin(x.detach()), atol=tol, rtol=0) + + def test_backprop_state(self, diff_method, device, tol): + """Test the trainability of parameters in a circuit returning the state.""" + if diff_method != "backprop": + pytest.skip(reason="test only works with backprop") + dev = device(2) + if dev.shots: + pytest.skip("test uses backprop, must be in analytic mode") + if "mixed" in dev.name: + pytest.skip("mixed-state simulator will wrongly use grad on non-scalar results") + tol = tol(dev.shots) + + x = torch.tensor(0.543, requires_grad=True) + y = torch.tensor(-0.654, requires_grad=True) + + @qml.qnode(dev, diff_method="backprop", grad_on_execution=True) + def circuit(x, y): + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.state() + + def cost_fn(x, y): + res = circuit(x, y) + probs = torch.abs(res) ** 2 + return probs[0] + probs[2] + + res = cost_fn(x, y) + res.backward() + grad = [x.grad, y.grad] + x, y = x.detach(), y.detach() + expected = np.array([-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2]) + assert np.allclose(grad, expected, atol=tol, rtol=0) + + def test_parameter_shift(self, diff_method, device, tol): + """Test a multi-parameter circuit with parameter-shift.""" + if diff_method != "parameter-shift": + pytest.skip(reason="test only works with parameter-shift") + + a = torch.tensor(0.1, requires_grad=True) + b = torch.tensor(0.2, requires_grad=True) + + dev = device(2) + tol = tol(dev.shots) + + @qml.qnode(dev, diff_method="parameter-shift", grad_on_execution=False) + def circuit(a, b): + qml.RY(a, wires=0) + qml.RX(b, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.Hamiltonian([1, 1], [qml.Z(0), qml.Y(1)])) + + res = circuit(a, b) + res.backward() + grad = [a.grad, b.grad] + a, b = a.detach(), b.detach() + expected = [-np.sin(a) + np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)] + assert np.allclose(grad, expected, atol=tol, rtol=0) + + def test_probs(self, diff_method, device, tol): + """Test differentiation of a circuit returning probs().""" + wires = 3 if diff_method == "hadamard" else 2 + dev = device(wires=wires) + tol = tol(dev.shots) + x = torch.tensor(0.543, requires_grad=True) + y = torch.tensor(-0.654, requires_grad=True) + + @qml.qnode(dev, diff_method=diff_method) + def circuit(x, y): + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.probs(wires=[1]) + + res = torch.autograd.functional.jacobian(circuit, (x, y)) + + x, y = x.detach(), y.detach() + expected = np.array( + [ + [-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2], + [np.cos(y) * np.sin(x) / 2, np.cos(x) * np.sin(y) / 2], + ] + ) + + assert isinstance(res, tuple) + assert len(res) == 2 + + assert isinstance(res[0], torch.Tensor) + assert res[0].shape == (2,) + + assert isinstance(res[1], torch.Tensor) + assert res[1].shape == (2,) + + if diff_method == "hadamard" and "raket" in dev.name: + pytest.xfail(reason="braket gets wrong results for hadamard here") + assert np.allclose(res[0], expected.T[0], atol=tol, rtol=0) + assert np.allclose(res[1], expected.T[1], atol=tol, rtol=0) + + def test_multi_meas(self, diff_method, device, tol): + """Test differentiation of a circuit with both scalar and array-like returns.""" + wires = 3 if diff_method == "hadamard" else 2 + dev = device(wires=wires) + tol = tol(dev.shots) + x = torch.tensor(0.543, requires_grad=True) + y = torch.tensor(-0.654, requires_grad=True) + + @qml.qnode(dev, diff_method=diff_method) + def circuit(x, y): + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.Z(0)), qml.probs(wires=[1]) + + jac = torch.autograd.functional.jacobian(circuit, (x, y)) + + x, y = x.detach(), y.detach() + expected = [ + [-np.sin(x), 0], + [ + [-np.sin(x) * np.cos(y) / 2, np.cos(y) * np.sin(x) / 2], + [-np.cos(x) * np.sin(y) / 2, np.cos(x) * np.sin(y) / 2], + ], + ] + assert isinstance(jac, tuple) + assert len(jac) == 2 + + assert isinstance(jac[0], tuple) + assert len(jac[0]) == 2 + assert all(isinstance(j, torch.Tensor) and j.shape == () for j in jac[0]) + assert np.allclose(jac[0], expected[0], atol=tol, rtol=0) + + assert isinstance(jac[1], tuple) + assert len(jac[1]) == 2 + assert all(isinstance(j, torch.Tensor) and j.shape == (2,) for j in jac[1]) + assert np.allclose(jac[1], expected[1], atol=tol, rtol=0) + + def test_hessian(self, diff_method, device, tol): + """Test hessian computation.""" + wires = 3 if diff_method == "hadamard" else 1 + dev = device(wires=wires) + tol = tol(dev.shots) + + @qml.qnode(dev, diff_method=diff_method, max_diff=2) + def circuit(x): + qml.RY(x[0], wires=0) + qml.RX(x[1], wires=0) + return qml.expval(qml.Z(0)) + + x = torch.tensor([1.0, 2.0], requires_grad=True) + res = circuit(x) + + res.backward() + g = x.grad + + hess = torch.autograd.functional.hessian(circuit, x) + a, b = x.detach().numpy() + + assert isinstance(hess, torch.Tensor) + assert tuple(hess.shape) == (2, 2) + + expected_res = np.cos(a) * np.cos(b) + assert np.allclose(res.detach(), expected_res, atol=tol, rtol=0) + + expected_g = [-np.sin(a) * np.cos(b), -np.cos(a) * np.sin(b)] + assert np.allclose(g.detach(), expected_g, atol=tol, rtol=0) + + expected_hess = [ + [-np.cos(a) * np.cos(b), np.sin(a) * np.sin(b)], + [np.sin(a) * np.sin(b), -np.cos(a) * np.cos(b)], + ] + + assert np.allclose(hess.detach(), expected_hess, atol=tol, rtol=0) diff --git a/pennylane/devices/tests/test_measurements.py b/pennylane/devices/tests/test_measurements.py index d0362451a50..393d498c5ea 100644 --- a/pennylane/devices/tests/test_measurements.py +++ b/pennylane/devices/tests/test_measurements.py @@ -51,13 +51,18 @@ ], "SparseHamiltonian": qml.SparseHamiltonian(csr_matrix(np.eye(8)), wires=[0, 1, 2]), "Hamiltonian": qml.Hamiltonian([1, 1], [qml.Z(0), qml.X(0)]), + "Prod": qml.prod(qml.X(0), qml.Z(1)), + "SProd": qml.s_prod(0.1, qml.Z(0)), + "Sum": qml.sum(qml.s_prod(0.1, qml.Z(0)), qml.prod(qml.X(0), qml.Z(1))), "LinearCombination": qml.ops.LinearCombination([1, 1], [qml.Z(0), qml.X(0)]), } all_obs = obs.keys() # All qubit observables should be available to test in the device test suite -all_available_obs = qml.ops._qubit__obs__.copy() # pylint: disable=protected-access +all_available_obs = qml.ops._qubit__obs__.copy().union( # pylint: disable=protected-access + {"Prod", "SProd", "Sum"} +) # Note that the identity is not technically a qubit observable all_available_obs |= {"Identity"} @@ -408,6 +413,30 @@ def circuit(): assert np.allclose(res, expected, atol=tol(dev.shots)) + @pytest.mark.parametrize( + "o", + [ + qml.prod(qml.X(0), qml.Z(1)), + qml.s_prod(0.1, qml.Z(0)), + qml.sum(qml.s_prod(0.1, qml.Z(0)), qml.prod(qml.X(0), qml.Z(1))), + ], + ) + def test_op_arithmetic_matches_default_qubit(self, o, device, tol): + """Test that devices (which support the observable) match default.qubit results.""" + dev = device(2) + if isinstance(dev, qml.Device) and o.name not in dev.observables: + pytest.skip(f"Skipped because device does not support the {o.name} observable.") + + def circuit(): + qml.Hadamard(0) + qml.CNOT([0, 1]) + return qml.expval(o) + + res_dq = qml.QNode(circuit, qml.device("default.qubit"))() + res = qml.QNode(circuit, dev)() + assert res.shape == () + assert np.isclose(res, res_dq, atol=tol(dev.shots)) + @flaky(max_runs=10) class TestTensorExpval: diff --git a/pennylane/devices/tests/test_templates.py b/pennylane/devices/tests/test_templates.py new file mode 100644 index 00000000000..434e6210a8a --- /dev/null +++ b/pennylane/devices/tests/test_templates.py @@ -0,0 +1,1006 @@ +# 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 that various templates work correctly on a device.""" +# pylint: disable=no-self-use + +# Can generate a list of all templates using the following code: +# +# from inspect import getmembers, isclass +# all_templates = [i for (_, i) in getmembers(qml.templates) if isclass(i) and issubclass(i, qml.operation.Operator)] + +from functools import partial +import pytest +import numpy as np +from scipy.stats import norm + +import pennylane as qml +from pennylane import math + + +pytestmark = pytest.mark.skip_unsupported + + +def check_op_supported(op, dev): + """Skip test if device does not support an operation. Works with both device APIs""" + if isinstance(dev, qml.Device): + if op.name not in dev.operations: + pytest.skip("operation not supported.") + else: + prog, _ = dev.preprocess() + tape = qml.tape.QuantumScript([op]) + try: + prog((tape,)) + except qml.DeviceError: + pytest.skip("operation not supported on the device") + + +class TestTemplates: # pylint:disable=too-many-public-methods + """Test various templates.""" + + def test_AQFT(self, device, tol): + """Test the AQFT template.""" + wires = 3 + dev = device(wires=wires) + + @qml.qnode(dev) + def circuit_aqft(): + qml.X(0) + qml.Hadamard(1) + qml.AQFT(order=1, wires=range(wires)) + return qml.probs() + + expected = [0.25, 0.125, 0.0, 0.125, 0.25, 0.125, 0.0, 0.125] + assert np.allclose(circuit_aqft(), expected, atol=tol(dev.shots)) + + def test_AllSinglesDoubles(self, device, tol): + """Test the AllSinglesDoubles template.""" + qubits = 4 + dev = device(qubits) + + electrons = 2 + + # Define the HF state + hf_state = qml.qchem.hf_state(electrons, qubits) + + # Generate all single and double excitations + singles, doubles = qml.qchem.excitations(electrons, qubits) + + wires = range(qubits) + + @qml.qnode(dev) + def circuit(weights, hf_state, singles, doubles): + qml.AllSinglesDoubles(weights, wires, hf_state, singles, doubles) + return qml.expval(qml.Z(0)) + + # Evaluate the QNode for a given set of parameters + params = np.array([0.12, 1.23, 2.34]) + res = circuit(params, hf_state, singles=singles, doubles=doubles) + assert np.isclose(res, 0.6905612772956113, atol=tol(dev.shots)) + + def test_AmplitudeEmbedding(self, device, tol): + """Test the AmplitudeEmbedding template.""" + dev = device(2) + + @qml.qnode(dev) + def circuit(f): + qml.AmplitudeEmbedding(features=f, wires=range(2)) + return qml.probs() + + res = circuit([1 / 2] * 4) + expected = [1 / 4] * 4 + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_AngleEmbedding(self, device, tol): + """Test the AngleEmbedding template.""" + n_wires = 3 + dev = device(n_wires) + + @qml.qnode(dev) + def circuit(feature_vector): + qml.AngleEmbedding(features=feature_vector, wires=range(n_wires), rotation="X") + qml.Hadamard(0) + return qml.probs(wires=range(3)) + + x = [np.pi / 2] * 3 + res = circuit(x) + expected = [0.125] * 8 + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_ApproxTimeEvolution(self, device, tol): + """Test the ApproxTimeEvolution template.""" + n_wires = 2 + dev = device(n_wires) + wires = range(n_wires) + + coeffs = [1, 1] + obs = [qml.X(0), qml.X(1)] + hamiltonian = qml.Hamiltonian(coeffs, obs) + + @qml.qnode(dev) + def circuit(time): + qml.ApproxTimeEvolution(hamiltonian, time, 1) + return [qml.expval(qml.Z(i)) for i in wires] + + res = circuit(1) + expected = [-0.41614684, -0.41614684] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_ArbitraryStatePreparation(self, device, tol): + """Test the ArbitraryStatePreparation template.""" + dev = device(2) + + @qml.qnode(dev) + def circuit(weights): + qml.ArbitraryStatePreparation(weights, wires=[0, 1]) + return qml.probs() + + weights = np.arange(1, 7) / 10 + res = circuit(weights) + expected = [0.784760658335564, 0.0693785880617069, 0.00158392607496555, 0.1442768275277600] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_ArbitraryUnitary(self, device, tol): + """Test the ArbitraryUnitary template.""" + dev = device(1) + + @qml.qnode(dev) + def circuit(weights): + qml.ArbitraryUnitary(weights, wires=[0]) + return qml.probs() + + weights = np.arange(3) + res = circuit(weights) + expected = [0.77015115293406, 0.22984884706593] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_BasicEntanglerLayers(self, device, tol): + """Test the BasicEntanglerLayers template.""" + n_wires = 3 + dev = device(n_wires) + + @qml.qnode(dev) + def circuit(weights): + qml.BasicEntanglerLayers(weights=weights, wires=range(n_wires)) + return [qml.expval(qml.Z(i)) for i in range(n_wires)] + + params = [[np.pi, np.pi, np.pi]] + res = circuit(params) + expected = [1.0, 1.0, -1.0] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_BasisEmbedding(self, device, tol): + """Test the BasisEmbedding template.""" + dev = device(3) + + @qml.qnode(dev) + def circuit(basis): + qml.BasisEmbedding(basis, wires=range(3)) + return qml.probs() + + basis = (1, 0, 1) + res = circuit(basis) + + basis_idx = np.dot(basis, 2 ** np.arange(3)) + expected = np.zeros(8) + expected[basis_idx] = 1.0 + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_BasisRotation(self, device, tol): + """Test the BasisRotation template.""" + dev = device(2) + if dev.shots or "mixed" in dev.name or "Mixed" in dev.name: + pytest.skip("test only works with analytic-mode pure statevector simulators") + + unitary_matrix = np.array( + [ + [-0.77228482 + 0.0j, -0.02959195 + 0.63458685j], + [0.63527644 + 0.0j, -0.03597397 + 0.77144651j], + ] + ) + eigen_values = np.array([-1.45183325, 3.47550075]) + exp_state = np.array([0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.43754907 - 0.89919453j]) + + @qml.qnode(dev) + def circuit(): + qml.PauliX(0) + qml.PauliX(1) + qml.adjoint(qml.BasisRotation(wires=[0, 1], unitary_matrix=unitary_matrix)) + for idx, eigenval in enumerate(eigen_values): + qml.RZ(-eigenval, wires=[idx]) + qml.BasisRotation(wires=[0, 1], unitary_matrix=unitary_matrix) + return qml.state() + + assert np.allclose( + [math.fidelity_statevector(circuit(), exp_state)], [1.0], atol=tol(dev.shots) + ) + + def test_BasisStatePreparation(self, device, tol): + """Test the BasisStatePreparation template.""" + dev = device(4) + + @qml.qnode(dev) + def circuit(basis_state): + qml.BasisStatePreparation(basis_state, wires=range(4)) + return [qml.expval(qml.Z(i)) for i in range(4)] + + basis_state = [0, 1, 1, 0] + res = circuit(basis_state) + expected = [1.0, -1.0, -1.0, 1.0] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + @pytest.mark.xfail(reason="most devices do not support CV") + def test_CVNeuralNetLayers(self, device): + """Test the CVNeuralNetLayers template.""" + dev = device(2) + + @qml.qnode(dev) + def circuit(weights): + qml.CVNeuralNetLayers(*weights, wires=[0, 1]) + return qml.expval(qml.QuadX(0)) + + shapes = qml.CVNeuralNetLayers.shape(n_layers=2, n_wires=2) + weights = [np.random.random(shape) for shape in shapes] + + circuit(weights) + + def test_CommutingEvolution(self, device, tol): + """Test the CommutingEvolution template.""" + n_wires = 2 + dev = device(n_wires) + coeffs = [1, -1] + obs = [qml.X(0) @ qml.Y(1), qml.Y(0) @ qml.X(1)] + hamiltonian = qml.Hamiltonian(coeffs, obs) + frequencies = (2, 4) + + @qml.qnode(dev) + def circuit(time): + qml.X(0) + qml.CommutingEvolution(hamiltonian, time, frequencies) + return qml.expval(qml.Z(0)) + + res = circuit(1) + expected = 0.6536436208636115 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_ControlledSequence(self, device, tol): + """Test the ControlledSequence template.""" + dev = device(4) + + @qml.qnode(dev) + def circuit(): + for i in range(3): + qml.Hadamard(wires=i) + qml.ControlledSequence(qml.RX(0.25, wires=3), control=[0, 1, 2]) + qml.adjoint(qml.QFT)(wires=range(3)) + return qml.probs(wires=range(3)) + + res = circuit() + expected = [ + 0.92059345, + 0.02637178, + 0.00729619, + 0.00423258, + 0.00360545, + 0.00423258, + 0.00729619, + 0.02637178, + ] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_CosineWindow(self, device, tol): + """Test the CosineWindow template.""" + dev = device(2) + + @qml.qnode(dev) + def circuit(): + qml.CosineWindow(wires=range(2)) + return qml.probs() + + res = circuit() + expected = [0.0, 0.25, 0.5, 0.25] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + @pytest.mark.xfail(reason="most devices do not support CV") + def test_DisplacementEmbedding(self, device, tol): + """Test the DisplacementEmbedding template.""" + dev = device(3) + + @qml.qnode(dev) + def circuit(feature_vector): + qml.DisplacementEmbedding(features=feature_vector, wires=range(3)) + qml.QuadraticPhase(0.1, wires=1) + return qml.expval(qml.NumberOperator(wires=1)) + + X = [1, 2, 3] + + res = circuit(X) + expected = 4.1215690638748494 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_FermionicDoubleExcitation(self, device, tol): + """Test the FermionicDoubleExcitation template.""" + dev = device(5) + if getattr(dev, "short_name", None) == "cirq.mixedsimulator" and dev.shots: + pytest.xfail(reason="device is generating negative probabilities") + + @qml.qnode(dev) + def circuit(weight, wires1=None, wires2=None): + qml.FermionicDoubleExcitation(weight, wires1=wires1, wires2=wires2) + return qml.expval(qml.Z(0)) + + res = circuit(1.34817, wires1=[0, 1], wires2=[2, 3, 4]) + expected = 1.0 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_FermionicSingleExcitation(self, device, tol): + """Test the FermionicSingleExcitation template.""" + dev = device(3) + if getattr(dev, "short_name", None) == "cirq.mixedsimulator" and dev.shots: + pytest.xfail(reason="device is generating negative probabilities") + + @qml.qnode(dev) + def circuit(weight, wires=None): + qml.FermionicSingleExcitation(weight, wires=wires) + return qml.expval(qml.Z(0)) + + res = circuit(0.56, wires=[0, 1, 2]) + expected = 1.0 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_FlipSign(self, device, tol): + """Test the FlipSign template.""" + dev = device(2) + if dev.shots: + pytest.skip("test only works with analytic-mode simulations") + basis_state = [1, 0] + + @qml.qnode(dev) + def circuit(): + for wire in list(range(2)): + qml.Hadamard(wires=wire) + qml.FlipSign(basis_state, wires=list(range(2))) + return qml.state() + + res = circuit() + expected = [0.5, 0.5, -0.5, 0.5] + if "mixed" in dev.name or "Mixed" in dev.name: + expected = math.dm_from_state_vector(expected) + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_GroverOperator(self, device, tol): + """Test the GroverOperator template.""" + n_wires = 3 + dev = device(n_wires) + wires = list(range(n_wires)) + + def oracle(): + qml.Hadamard(wires[-1]) + qml.Toffoli(wires=wires) + qml.Hadamard(wires[-1]) + + @qml.qnode(dev) + def circuit(num_iterations=1): + for wire in wires: + qml.Hadamard(wire) + + for _ in range(num_iterations): + oracle() + qml.GroverOperator(wires=wires) + return qml.probs(wires) + + res = circuit(num_iterations=2) + expected = [ + 0.0078125, + 0.0078125, + 0.0078125, + 0.0078125, + 0.0078125, + 0.0078125, + 0.0078125, + 0.9453125, + ] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_HilbertSchmidt(self, device, tol): + """Test the HilbertSchmidt template.""" + dev = device(2) + u_tape = qml.tape.QuantumScript([qml.Hadamard(0)]) + + def v_function(params): + qml.RZ(params[0], wires=1) + + @qml.qnode(dev) + def hilbert_test(v_params, v_function, v_wires, u_tape): + qml.HilbertSchmidt(v_params, v_function=v_function, v_wires=v_wires, u_tape=u_tape) + return qml.probs(u_tape.wires + v_wires) + + def cost_hst(parameters, v_function, v_wires, u_tape): + # pylint:disable=unsubscriptable-object + return ( + 1 + - hilbert_test( + v_params=parameters, v_function=v_function, v_wires=v_wires, u_tape=u_tape + )[0] + ) + + res = cost_hst([0], v_function=v_function, v_wires=[1], u_tape=u_tape) + expected = 1.0 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_IQPEmbedding(self, device, tol): + """Test the IQPEmbedding template.""" + dev = device(3) + + @qml.qnode(dev) + def circuit(features): + qml.IQPEmbedding(features, wires=range(3), n_repeats=4) + return [qml.expval(qml.Z(w)) for w in range(3)] + + res = circuit([1.0, 2.0, 3.0]) + expected = [0.40712208, 0.32709118, 0.89125407] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + @pytest.mark.xfail(reason="most devices do not support CV") + def test_Interferometer(self, device): + """Test the Interferometer template.""" + dev = device(4) + + @qml.qnode(dev) + def circuit(params): + qml.Interferometer(*params, wires=range(4)) + return qml.expval(qml.Identity(0)) + + shapes = [[6], [6], [4]] + params = [] + for shape in shapes: + params.append(np.random.random(shape)) + + _ = circuit(params) + + def test_LocalHilbertSchmidt(self, device, tol): + """Test the LocalHilbertSchmidt template.""" + dev = device(4) + u_tape = qml.tape.QuantumScript([qml.CZ(wires=(0, 1))]) + + def v_function(params): + qml.RZ(params[0], wires=2) + qml.RZ(params[1], wires=3) + qml.CNOT(wires=[2, 3]) + qml.RZ(params[2], wires=3) + qml.CNOT(wires=[2, 3]) + + @qml.qnode(dev) + def local_hilbert_test(v_params, v_function, v_wires, u_tape): + qml.LocalHilbertSchmidt(v_params, v_function=v_function, v_wires=v_wires, u_tape=u_tape) + return qml.probs(u_tape.wires + v_wires) + + def cost_lhst(parameters, v_function, v_wires, u_tape): + # pylint:disable=unsubscriptable-object + return ( + 1 + - local_hilbert_test( + v_params=parameters, v_function=v_function, v_wires=v_wires, u_tape=u_tape + )[0] + ) + + res = cost_lhst( + [3 * np.pi / 2, 3 * np.pi / 2, np.pi / 2], + v_function=v_function, + v_wires=[2, 3], + u_tape=u_tape, + ) + expected = 0.5 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_MERA(self, device, tol): + """Test the MERA template.""" + + def block(weights, wires): + qml.CNOT(wires=[wires[0], wires[1]]) + qml.RY(weights[0], wires=wires[0]) + qml.RY(weights[1], wires=wires[1]) + + n_wires = 4 + n_block_wires = 2 + n_params_block = 2 + n_blocks = qml.MERA.get_n_blocks(range(n_wires), n_block_wires) + template_weights = [[0.1, -0.3]] * n_blocks + dev = device(n_wires) + + @qml.qnode(dev) + def circuit(template_weights): + qml.MERA(range(n_wires), n_block_wires, block, n_params_block, template_weights) + return qml.expval(qml.Z(1)) + + res = circuit(template_weights) + expected = 0.799260896638786 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_MPS(self, device, tol): + """Test the MPS template.""" + + def block(weights, wires): + qml.CNOT(wires=[wires[0], wires[1]]) + qml.RY(weights[0], wires=wires[0]) + qml.RY(weights[1], wires=wires[1]) + + n_wires = 4 + n_block_wires = 2 + n_params_block = 2 + n_blocks = qml.MPS.get_n_blocks(range(n_wires), n_block_wires) + template_weights = [[0.1, -0.3]] * n_blocks + dev = device(n_wires) + + @qml.qnode(dev) + def circuit(template_weights): + qml.MPS(range(n_wires), n_block_wires, block, n_params_block, template_weights) + return qml.expval(qml.Z(n_wires - 1)) + + res = circuit(template_weights) + expected = 0.8719048589118708 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_MottonenStatePreparation_probs(self, device, tol): + """Test the MottonenStatePreparation template (up to a phase).""" + dev = device(3) + + @qml.qnode(dev) + def circuit(state): + qml.MottonenStatePreparation(state_vector=state, wires=range(3)) + return qml.probs() + + state = np.array([1, 2j, 3, 4j, 5, 6j, 7, 8j]) + state = state / np.linalg.norm(state) + res = circuit(state) + expected = np.abs(state**2) + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_MottonenStatePreparation_state(self, device, tol): + """Test the MottonenStatePreparation template on analytic-mode devices.""" + dev = device(3) + if dev.shots: + pytest.skip("test only works with analytic-mode simulations") + + @qml.qnode(dev) + def circuit(state): + qml.MottonenStatePreparation(state_vector=state, wires=range(3)) + return qml.state() + + state = np.array([1, 2j, 3, 4j, 5, 6j, 7, 8j]) + state = state / np.linalg.norm(state) + res = circuit(state) + expected = state + if "mixed" in dev.name or "Mixed" in dev.name: + expected = math.dm_from_state_vector(expected) + if np.allclose(res, expected, atol=tol(dev.shots)): + # GlobalPhase supported + return + # GlobalPhase not supported + global_phase = qml.math.sum(-1 * qml.math.angle(expected) / len(expected)) + global_phase = np.exp(-1j * global_phase) + assert np.allclose(expected / res, global_phase) + + def test_Permute(self, device, tol): + """Test the Permute template.""" + dev = device(2) + + @qml.qnode(dev) + def circuit(): + qml.StatePrep([1 / np.sqrt(2), 0.4, 0.5, 0.3], wires=[0, 1]) + qml.Permute([1, 0], [0, 1]) + return qml.probs() + + res = circuit() + expected = [0.5, 0.25, 0.16, 0.09] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_QAOAEmbedding(self, device, tol): + """Test the QAOAEmbedding template.""" + dev = device(2) + + @qml.qnode(dev) + def circuit(weights, f=None): + qml.QAOAEmbedding(features=f, weights=weights, wires=range(2)) + return qml.expval(qml.Z(0)) + + features = [1.0, 2.0] + layer1 = [0.1, -0.3, 1.5] + layer2 = [3.1, 0.2, -2.8] + weights = [layer1, layer2] + + res = circuit(weights, f=features) + expected = 0.49628561029741747 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_QDrift(self, device, tol): + """Test the QDrift template.""" + dev = device(2) + coeffs = [0.25, 0.75] + ops = [qml.X(0), qml.Z(0)] + H = qml.dot(coeffs, ops) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(0) + qml.QDrift(H, time=1.2, n=10, seed=10) + return qml.probs() + + res = circuit() + expected = [0.65379493, 0.0, 0.34620507, 0.0] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_QFT(self, device, tol): + """Test the QFT template.""" + wires = 3 + dev = device(wires) + + @qml.qnode(dev) + def circuit_qft(state): + qml.StatePrep(state, wires=range(wires)) + qml.QFT(wires=range(wires)) + return qml.probs() + + res = circuit_qft([0.8, 0.6] + [0.0] * 6) + expected = [0.245, 0.20985281, 0.125, 0.04014719, 0.005, 0.04014719, 0.125, 0.20985281] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_QSVT(self, device, tol): + """Test the QSVT template.""" + dev = device(2) + A = np.array([[0.1]]) + block_encode = qml.BlockEncode(A, wires=[0, 1]) + check_op_supported(block_encode, dev) + shifts = [qml.PCPhase(i + 0.1, dim=1, wires=[0, 1]) for i in range(3)] + + @qml.qnode(dev) + def circuit(): + qml.QSVT(block_encode, shifts) + return qml.expval(qml.Z(1)) + + res = circuit() + expected = 0.9370953557566887 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_QuantumMonteCarlo(self, device, tol): + """Test the QuantumMonteCarlo template.""" + m = 2 + M = 2**m + + xmax = np.pi # bound to region [-pi, pi] + xs = np.linspace(-xmax, xmax, M) + + probs = np.array([norm().pdf(x) for x in xs]) + probs /= np.sum(probs) + + def func(i): + return np.sin(xs[i]) ** 2 + + n = 3 + N = 2**n + + target_wires = range(m + 1) + estimation_wires = range(m + 1, n + m + 1) + dev = device(wires=n + m + 1) + check_op_supported(qml.ControlledQubitUnitary(np.eye(2), [1], [0]), dev) + + @qml.qnode(dev) + def circuit(): + qml.QuantumMonteCarlo( + probs, + func, + target_wires=target_wires, + estimation_wires=estimation_wires, + ) + return qml.probs(estimation_wires) + + # pylint:disable=unsubscriptable-object + phase_estimated = np.argmax(circuit()[: int(N / 2)]) / N + res = (1 - np.cos(np.pi * phase_estimated)) / 2 + expected = 0.3086582838174551 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_QuantumPhaseEstimation(self, device, tol): + """Test the QuantumPhaseEstimation template.""" + unitary = qml.RX(np.pi / 2, wires=[0]) @ qml.CNOT(wires=[0, 1]) + eigenvector = np.array([-1 / 2, -1 / 2, 1 / 2, 1 / 2]) + + n_estimation_wires = 3 + estimation_wires = range(2, n_estimation_wires + 2) + target_wires = [0, 1] + + dev = device(wires=n_estimation_wires + 2) + + @qml.qnode(dev) + def circuit(): + qml.StatePrep(eigenvector, wires=target_wires) + qml.QuantumPhaseEstimation( + unitary, + estimation_wires=estimation_wires, + ) + return qml.probs(estimation_wires) + + res = np.argmax(circuit()) / 2**n_estimation_wires + expected = 0.125 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_QutritBasisStatePreparation(self, device, tol): + """Test the QutritBasisStatePreparation template.""" + dev = device(4) + if "qutrit" not in dev.name: + pytest.skip("QutritBasisState template only works on qutrit devices") + + @qml.qnode(dev) + def circuit(basis_state, obs): + qml.QutritBasisStatePreparation(basis_state, wires=range(4)) + return [qml.expval(qml.THermitian(obs, wires=i)) for i in range(4)] + + basis_state = [0, 1, 1, 0] + obs = np.array([[1, 1, 0], [1, -1, 0], [0, 0, np.sqrt(2)]]) / np.sqrt(2) + + res = circuit(basis_state, obs) + expected = np.array([1, -1, -1, 1]) / np.sqrt(2) + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_RandomLayers(self, device, tol): + """Test the RandomLayers template.""" + dev = device(2) + weights = np.array([[0.1, -2.1, 1.4]]) + + @qml.qnode(dev) + def circuit(weights): + qml.RandomLayers(weights=weights, wires=range(2), seed=42) + return qml.expval(qml.Z(0)) + + res = circuit(weights) + expected = 0.9950041652780259 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_Select(self, device, tol): + """Test the Select template.""" + dev = device(4) + check_op_supported(qml.MultiControlledX(wires=[0, 1, 2]), dev) + + ops = [qml.X(2), qml.X(3), qml.Y(2), qml.SWAP([2, 3])] + + @qml.qnode(dev) + def circuit(): + qml.Select(ops, control=[0, 1]) + return qml.probs() + + res = circuit() + expected = np.zeros(16) + expected[2] = 1.0 + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_SimplifiedTwoDesign(self, device, tol): + """Test the SimplifiedTwoDesign template.""" + n_wires = 3 + dev = device(n_wires) + + @qml.qnode(dev) + def circuit(init_weights, weights): + qml.SimplifiedTwoDesign( + initial_layer_weights=init_weights, weights=weights, wires=range(n_wires) + ) + return [qml.expval(qml.Z(i)) for i in range(n_wires)] + + init_weights = [np.pi, np.pi, np.pi] + weights_layer1 = [[0.0, np.pi], [0.0, np.pi]] + weights_layer2 = [[np.pi, 0.0], [np.pi, 0.0]] + weights = [weights_layer1, weights_layer2] + + res = circuit(init_weights, weights) + expected = [1.0, -1.0, 1.0] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + @pytest.mark.xfail(reason="most devices do not support CV") + def test_SqueezingEmbedding(self, device, tol): + """Test the SqueezingEmbedding template.""" + dev = device(2) + + @qml.qnode(dev) + def circuit(feature_vector): + qml.SqueezingEmbedding(features=feature_vector, wires=range(3)) + qml.QuadraticPhase(0.1, wires=1) + return qml.expval(qml.NumberOperator(wires=1)) + + X = [1, 2, 3] + + res = circuit(X) + expected = 13.018280763205285 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_StronglyEntanglingLayers(self, device, tol): + """Test the StronglyEntanglingLayers template.""" + dev = device(4) + + @qml.qnode(dev) + def circuit(parameters): + qml.StronglyEntanglingLayers(weights=parameters, wires=range(4)) + return qml.expval(qml.Z(0)) + + shape = qml.StronglyEntanglingLayers.shape(n_layers=2, n_wires=4) + params = np.arange(1, np.prod(shape) + 1).reshape(shape) / 10 + res = circuit(params) + expected = -0.07273693957824906 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_TTN(self, device, tol): + """Test the TTN template.""" + + def block(weights, wires): + qml.CNOT(wires=[wires[0], wires[1]]) + qml.RY(weights[0], wires=wires[0]) + qml.RY(weights[1], wires=wires[1]) + + n_wires = 4 + n_block_wires = 2 + n_params_block = 2 + n_blocks = qml.TTN.get_n_blocks(range(n_wires), n_block_wires) + template_weights = [[0.1, -0.3]] * n_blocks + dev = device(n_wires) + + @qml.qnode(dev) + def circuit(template_weights): + qml.TTN(range(n_wires), n_block_wires, block, n_params_block, template_weights) + return qml.expval(qml.Z(n_wires - 1)) + + res = circuit(template_weights) + expected = 0.7845726663667097 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_TrotterProduct(self, device, tol): + """Test the TrotterProduct template.""" + dev = device(2) + coeffs = [0.25, 0.75] + ops = [qml.X(0), qml.Z(0)] + H = qml.dot(coeffs, ops) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(0) + qml.TrotterProduct(H, time=2.4, order=2) + return qml.probs() + + res = circuit() + expected = [0.37506708, 0.0, 0.62493292, 0.0] + assert np.allclose(res, expected, atol=tol(dev.shots)) + + def test_TwoLocalSwapNetwork(self, device, tol): + """Test the TwoLocalSwapNetwork template.""" + dev = device(3) + + def acquaintances(index, wires, param=None): # pylint:disable=unused-argument + return qml.CNOT(index) + + @qml.qnode(dev) + def circuit(state): + qml.StatePrep(state, range(3)) + qml.TwoLocalSwapNetwork(dev.wires, acquaintances, fermionic=True, shift=False) + return qml.probs() + + state = np.arange(8, dtype=float) + state /= np.linalg.norm(state) + probs = state**2 + res = circuit(state) + order = np.argsort(np.argsort(res)) + tol = tol(dev.shots) + assert all(np.isclose(val, probs[i], atol=tol) for i, val in zip(order, res)) + + +class TestMoleculeTemplates: + """Test templates using the H2 molecule.""" + + @pytest.fixture(scope="class") + def h2(self): + """Return attributes needed for H2.""" + symbols, coordinates = (["H", "H"], np.array([0.0, 0.0, -0.66140414, 0.0, 0.0, 0.66140414])) + h, qubits = qml.qchem.molecular_hamiltonian(symbols, coordinates) + electrons = 2 + ref_state = qml.qchem.hf_state(electrons, qubits) + return qubits, ref_state, h + + def test_GateFabric(self, device, tol, h2): + """Test the GateFabric template.""" + qubits, ref_state, H = h2 + dev = device(qubits) + if getattr(dev, "short_name", None) == "cirq.mixedsimulator" and dev.shots: + pytest.xfail(reason="device is generating negative probabilities") + + @qml.qnode(dev) + def circuit(weights): + qml.GateFabric(weights, wires=[0, 1, 2, 3], init_state=ref_state, include_pi=True) + return qml.expval(H) + + layers = 2 + shape = qml.GateFabric.shape(n_layers=layers, n_wires=qubits) + weights = np.array([0.1, 0.2, 0.3, 0.4]).reshape(shape) + res = circuit(weights) + expected = -0.9453094224618628 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_ParticleConservingU1(self, device, tol, h2): + """Test the ParticleConservingU1 template.""" + qubits, ref_state, h = h2 + dev = device(qubits) + ansatz = partial(qml.ParticleConservingU1, init_state=ref_state, wires=dev.wires) + + @qml.qnode(dev) + def circuit(params): + ansatz(params) + return qml.expval(h) + + layers = 2 + shape = qml.ParticleConservingU1.shape(layers, qubits) + params = np.arange(1, 13).reshape(shape) / 10 + res = circuit(params) + expected = -0.5669084184194393 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_ParticleConservingU2(self, device, tol, h2): + """Test the ParticleConservingU2 template.""" + qubits, ref_state, h = h2 + dev = device(qubits) + ansatz = partial(qml.ParticleConservingU2, init_state=ref_state, wires=dev.wires) + + @qml.qnode(dev) + def circuit(params): + ansatz(params) + return qml.expval(h) + + layers = 1 + shape = qml.ParticleConservingU2.shape(layers, qubits) + params = np.arange(1, 8).reshape(shape) / 10 + res = circuit(params) + expected = -0.8521967086461301 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_UCCSD(self, device, tol, h2): + """Test the UCCSD template.""" + qubits, hf_state, H = h2 + electrons = 2 + singles, doubles = qml.qchem.excitations(electrons, qubits) + s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles) + dev = device(qubits) + + @qml.qnode(dev) + def circuit(params, wires, s_wires, d_wires, hf_state): + qml.UCCSD(params, wires, s_wires, d_wires, hf_state) + return qml.expval(H) + + params = np.arange(len(singles) + len(doubles)) / 4 + res = circuit( + params, wires=range(qubits), s_wires=s_wires, d_wires=d_wires, hf_state=hf_state + ) + expected = -1.0864433121798176 + assert np.isclose(res, expected, atol=tol(dev.shots)) + + def test_kUpCCGSD(self, device, tol, h2): + """Test the kUpCCGSD template.""" + qubits, ref_state, H = h2 + dev = device(qubits) + if getattr(dev, "short_name", None) == "cirq.mixedsimulator" and dev.shots: + pytest.xfail(reason="device is generating negative probabilities") + + @qml.qnode(dev) + def circuit(weights): + qml.kUpCCGSD(weights, wires=[0, 1, 2, 3], k=1, delta_sz=0, init_state=ref_state) + return qml.expval(H) + + layers = 1 + shape = qml.kUpCCGSD.shape(k=layers, n_wires=qubits, delta_sz=0) + weights = np.arange(np.prod(shape)).reshape(shape) / 10 + res = circuit(weights) + expected = -1.072648130451027 + assert np.isclose(res, expected, atol=tol(dev.shots))