diff --git a/.github/workflows/interface-unit-tests.yml b/.github/workflows/interface-unit-tests.yml index 17088962858..41f2f3434e3 100644 --- a/.github/workflows/interface-unit-tests.yml +++ b/.github/workflows/interface-unit-tests.yml @@ -527,6 +527,7 @@ jobs: # shots: None - device: default.qubit.autograd shots: None + skip_interface: jax,tf,torch - device: default.mixed shots: None python-version: >- @@ -542,9 +543,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: true 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 b117f4b3f82..b4456e59708 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -209,6 +209,13 @@ [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) + * `qml.transforms.hamiltonian_expand` can now handle multi-term observables with a constant offset. [(#5414)](https://github.com/PennyLaneAI/pennylane/pull/5414) @@ -241,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) @@ -322,6 +332,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) @@ -339,6 +352,12 @@

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) + * 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/_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/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/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/devices/tests/conftest.py b/pennylane/devices/tests/conftest.py index 779088c79db..0a4be0264c5 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)) 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/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/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/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/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 diff --git a/tests/devices/default_qubit/test_default_qubit_tracking.py b/tests/devices/default_qubit/test_default_qubit_tracking.py index 5390ee04fde..f12af5eab8d 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/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.""" 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 diff --git a/tests/resource/test_error/test_error.py b/tests/resource/test_error/test_error.py index 8e07e0c1c03..4b690e49a18 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,112 @@ 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 + + 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 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})