From c9424971686c10f383f2b07b5af473308d4b242e Mon Sep 17 00:00:00 2001 From: AmintorDusko Date: Thu, 21 Mar 2024 10:53:14 -0400 Subject: [PATCH 01/46] port PR changes so far, after traumatic rebase --- .github/CHANGELOG.md | 3 + pennylane_lightning/core/_version.py | 2 +- .../lightning_qubit/_adjoint_jacobian.py | 6 + .../lightning_qubit/lightning_qubit.py | 126 +++ tests/test_vjp.py | 923 +++++++++--------- 5 files changed, 623 insertions(+), 437 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 076481918b..0c7d0f522a 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -21,6 +21,9 @@ [(#607)](https://github.com/PennyLaneAI/pennylane-lightning/pull/607) [(#628)](https://github.com/PennyLaneAI/pennylane-lightning/pull/628) +* Add Vector-Jacobian Product calculation support to `lightning.qubit`. + [(#644)](https://github.com/PennyLaneAI/pennylane-lightning/pull/644) + ### Breaking changes * Migrate `lightning.qubit` to the new device API. diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 070aab6b47..134b71b540 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.36.0-dev14" +__version__ = "0.36.0-dev15" diff --git a/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py b/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py index 3313fc9b62..183d043fcb 100644 --- a/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py +++ b/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py @@ -293,6 +293,12 @@ def calculate_vjp(self, tape: QuantumTape, grad_vec): "Adjoint differentiation does not support State measurements." ) + if any(m.return_type is not Expectation for m in tape.measurements): + raise QuantumFunctionError( + "Adjoint differentiation method does not support expectation return type " + "mixed with other return types" + ) + # Proceed, because tape_return_type is Expectation. if len(grad_vec) != len(measurements): raise ValueError( diff --git a/pennylane_lightning/lightning_qubit/lightning_qubit.py b/pennylane_lightning/lightning_qubit/lightning_qubit.py index 5f42947376..78207026cb 100644 --- a/pennylane_lightning/lightning_qubit/lightning_qubit.py +++ b/pennylane_lightning/lightning_qubit/lightning_qubit.py @@ -14,6 +14,8 @@ """ This module contains the LightningQubit class that inherits from the new device interface. """ +from numbers import Number +from typing import Optional, Union, Sequence, Callable, Tuple from dataclasses import replace from pathlib import Path from typing import Callable, Optional, Sequence, Union @@ -114,6 +116,54 @@ def simulate_and_jacobian(circuit: QuantumTape, state: LightningStateVector, bat jac = LightningAdjointJacobian(state, batch_obs=batch_obs).calculate_jacobian(circuit) return res, jac +def vjp( + circuit: QuantumTape, cotangents: Tuple[Number], state: LightningStateVector, batch_obs=False +): + """Compute the Vector-Jacobian Product (VJP) for a single quantum script. + Args: + circuit (QuantumTape): The single circuit to simulate + cotangents (Tuple[Number, Tuple[Number]]): Gradient-output vector. Must + have shape matching the output shape of the corresponding circuit. If + the circuit has a single output, ``cotangents`` may be a single number, + not an iterable of numbers. + state (LightningStateVector): handle to Lightning state vector + batch_obs (bool): Determine whether we process observables in parallel when + computing the VJP. This value is only relevant when the lightning + qubit is built with OpenMP. + Returns: + TensorLike: The VJP of the quantum script + """ + circuit = circuit.map_to_standard_wires() + state.reset_state() + final_state = state.get_final_state(circuit) + return LightningAdjointJacobian(final_state, batch_obs=batch_obs).calculate_vjp( + circuit, cotangents + ) + + +def simulate_and_vjp( + circuit: QuantumTape, cotangents: Tuple[Number], state: LightningStateVector, batch_obs=False +): + """Simulate a single quantum script and compute its Vector-Jacobian Product (VJP). + Args: + circuit (QuantumTape): The single circuit to simulate + cotangents (Tuple[Number, Tuple[Number]]): Gradient-output vector. Must + have shape matching the output shape of the corresponding circuit. If + the circuit has a single output, ``cotangents`` may be a single number, + not an iterable of numbers. + state (LightningStateVector): handle to Lightning state vector + batch_obs (bool): Determine whether we process observables in parallel when + computing the jacobian. This value is only relevant when the lightning + qubit is built with OpenMP. + Returns: + Tuple[TensorLike]: The results of the simulation and the calculated VJP + Note that this function can return measurements for non-commuting observables simultaneously. + """ + circuit = circuit.map_to_standard_wires() + res = simulate(circuit, state) + _vjp = LightningAdjointJacobian(state, batch_obs=batch_obs).calculate_vjp(circuit, cotangents) + return res, _vjp + _operations = frozenset( { @@ -530,3 +580,79 @@ def execute_and_compute_derivatives( simulate_and_jacobian(c, self._statevector, batch_obs=batch_obs) for c in circuits ) return tuple(zip(*results)) + + def supports_vjp( + self, + execution_config: Optional[ExecutionConfig] = None, + circuit: Optional[QuantumTape] = None, + ) -> bool: + """Whether or not this device defines a custom vector jacobian product. + ``LightningQubit`` supports adjoint differentiation with analytic results. + Args: + execution_config (ExecutionConfig): The configuration of the desired derivative calculation + circuit (QuantumTape): An optional circuit to check derivatives support for. + Returns: + Bool: Whether or not a derivative can be calculated provided the given information + """ + return self.supports_derivatives(execution_config, circuit) + + def compute_vjp( + self, + circuits: QuantumTape_or_Batch, + cotangents: Tuple[Number], + execution_config: ExecutionConfig = DefaultExecutionConfig, + ): + r"""The vector jacobian product used in reverse-mode differentiation. ``LightningQubit`` uses the + adjoint differentiation method to compute the VJP. + Args: + circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuit or batch of circuits + cotangents (Tuple[Number, Tuple[Number]]): Gradient-output vector. Must have shape matching the output shape of the + corresponding circuit. If the circuit has a single output, ``cotangents`` may be a single number, not an iterable + of numbers. + execution_config (ExecutionConfig): a datastructure with all additional information required for execution + Returns: + tensor-like: A numeric result of computing the vector jacobian product + **Definition of vjp:** + If we have a function with jacobian: + .. math:: + \vec{y} = f(\vec{x}) \qquad J_{i,j} = \frac{\partial y_i}{\partial x_j} + The vector jacobian product is the inner product of the derivatives of the output ``y`` with the + Jacobian matrix. The derivatives of the output vector are sometimes called the **cotangents**. + .. math:: + \text{d}x_i = \Sigma_{i} \text{d}y_i J_{i,j} + **Shape of cotangents:** + The value provided to ``cotangents`` should match the output of :meth:`~.execute`. For computing the full Jacobian, + the cotangents can be batched to vectorize the computation. In this case, the cotangents can have the following + shapes. ``batch_size`` below refers to the number of entries in the Jacobian: + * For a state measurement, the cotangents must have shape ``(batch_size, 2 ** n_wires)`` + * For ``n`` expectation values, the cotangents must have shape ``(n, batch_size)``. If ``n = 1``, + then the shape must be ``(batch_size,)``. + """ + batch_obs = execution_config.device_options.get("batch_obs", self._batch_obs) + return tuple( + vjp(circuit, cots, self._statevector, batch_obs=batch_obs) + for circuit, cots in zip(circuits, cotangents) + ) + + def execute_and_compute_vjp( + self, + circuits: QuantumTape_or_Batch, + cotangents: Tuple[Number], + execution_config: ExecutionConfig = DefaultExecutionConfig, + ): + """Calculate both the results and the vector jacobian product used in reverse-mode differentiation. + Args: + circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuit or batch of circuits to be executed + cotangents (Tuple[Number, Tuple[Number]]): Gradient-output vector. Must have shape matching the output shape of the + corresponding circuit. If the circuit has a single output, ``cotangents`` may be a single number, not an iterable + of numbers. + execution_config (ExecutionConfig): a datastructure with all additional information required for execution + Returns: + Tuple, Tuple: the result of executing the scripts and the numeric result of computing the vector jacobian product + """ + batch_obs = execution_config.device_options.get("batch_obs", self._batch_obs) + results = tuple( + simulate_and_vjp(circuit, cots, self._statevector, batch_obs=batch_obs) + for circuit, cots in zip(circuits, cotangents) + ) + return tuple(zip(*results)) \ No newline at end of file diff --git a/tests/test_vjp.py b/tests/test_vjp.py index 70bd091bf9..209cac3e28 100644 --- a/tests/test_vjp.py +++ b/tests/test_vjp.py @@ -22,11 +22,33 @@ from conftest import device_name from pennylane import numpy as np -if not ld._CPP_BINARY_AVAILABLE: +if ld._CPP_BINARY_AVAILABLE: + if device_name == "lightning.qubit2": + from pennylane_lightning.lightning_qubit_ops import LightningException + +else: pytest.skip("No binary module found. Skipping.", allow_module_level=True) -if ld._new_API: - pytest.skip("Old API required", allow_module_level=True) +# if ld._new_API: +# pytest.skip("Old API required", allow_module_level=True) + + +def get_vjp(device, tapes, dy): + """Helper to get VJP for a tape or batch of tapes""" + if device._new_API: + return device.compute_vjp(tapes, dy) + + if isinstance(tapes, qml.tape.QuantumScript): + return device.vjp(tapes.measurements, dy)(tapes) + + return device.batch_vjp(tapes, dy)(tapes) + + +def get_jacobian(device, tape): + """Helper to get Jacobian of a tape""" + if device._new_API: + return device.compute_derivatives(tape) + return device.adjoint_jacobian(tape) class TestVectorJacobianProduct: @@ -36,6 +58,7 @@ class TestVectorJacobianProduct: def dev(self, request): return qml.device(device_name, wires=2, c_dtype=request.param) + @pytest.mark.skipif(ld._new_API, reason="Old API required") def test_use_device_state(self, tol, dev): """Tests that when using the device state, the correct answer is still returned.""" x, y, z = [0.5, 0.3, -0.7] @@ -55,10 +78,11 @@ def test_use_device_state(self, tol, dev): qml.execute([tape], dev, None) fn2 = dev.vjp(tape.measurements, dy, use_device_state=True) - vjp2 = fn2(tape) + vjp2 = fn2(tape.measure) assert np.allclose(vjp1, vjp2, atol=tol, rtol=0) + @pytest.mark.skipif(ld._new_API, reason="Old API required") def test_provide_starting_state(self, tol, dev): """Tests provides correct answer when provided starting state.""" x, y, z = [0.5, 0.3, -0.7] @@ -107,10 +131,8 @@ def test_multiple_measurements(self, tol, dev): tape2.trainable_params = {1, 2, 3} - fn1 = dev.vjp(tape1.measurements, dy) - vjp1 = fn1(tape1) - - vjp2 = dev.adjoint_jacobian(tape2) + vjp1 = get_vjp(dev, tape1, dy) + vjp2 = get_jacobian(dev, tape2) assert np.allclose(vjp1, vjp2, atol=tol, rtol=0) @@ -127,18 +149,30 @@ def test_wrong_dy_expval(self, dev): qml.expval(qml.PauliZ(1)) dy1 = np.array([1.0, 2.0]) + dy2 = np.array([1.0 + 3.0j, 0.3 + 2.0j, 0.5 + 0.1j]) tape1.trainable_params = {1, 2, 3} - with pytest.raises( - ValueError, match="Number of observables in the tape must be the same as" - ): - dev.vjp(tape1.measurements, dy1) + if dev._new_API: + with pytest.raises( + ValueError, match="Number of observables in the tape must be the same as" + ): + dev.compute_vjp(tape1, dy1) - dy2 = np.array([1.0 + 3.0j, 0.3 + 2.0j, 0.5 + 0.1j]) - with pytest.raises( - ValueError, match="The vjp method only works with a real-valued grad_vec" - ): - dev.vjp(tape1.measurements, dy2) + with pytest.raises( + ValueError, match="The vjp method only works with a real-valued grad_vec" + ): + dev.compute_vjp(tape1, dy2) + + else: + with pytest.raises( + ValueError, match="Number of observables in the tape must be the same as" + ): + dev.vjp(tape1.measurements, dy1) + + with pytest.raises( + ValueError, match="The vjp method only works with a real-valued grad_vec" + ): + dev.vjp(tape1.measurements, dy2) def test_not_expval(self, dev): """Test if a QuantumFunctionError is raised for a tape with measurements that are not @@ -149,11 +183,19 @@ def test_not_expval(self, dev): dy = np.array([1.0]) - with pytest.raises( - qml.QuantumFunctionError, match="Adjoint differentiation method does not" - ): - dev.vjp(tape.measurements, dy)(tape) + if dev._new_API: + with pytest.raises( + qml.QuantumFunctionError, match="Adjoint differentiation method does not" + ): + dev.compute_vjp(tape, dy) + + else: + with pytest.raises( + qml.QuantumFunctionError, match="Adjoint differentiation method does not" + ): + dev.vjp(tape.measurements, dy)(tape) + @pytest.mark.skipif(ld._new_API, reason="Old API required") def test_finite_shots_warns(self): """Tests warning raised when finite shots specified""" @@ -170,6 +212,20 @@ def test_finite_shots_warns(self): ): dev.vjp(tape.measurements, dy)(tape) + @pytest.mark.skipif(not ld._new_API, reason="New API required") + def test_finite_shots_error(self): + """Test that an error is raised when finite shots specified""" + dev = qml.device(device_name, wires=1) + + tape = qml.tape.QuantumScript([], [qml.expval(qml.Z(0))], shots=1) + dy = np.array([1.0]) + + with pytest.raises( + qml.QuantumFunctionError, + match="Requested adjoint differentiation to be computed with finite shots.", + ): + dev.compute_vjp(tape, dy) + def test_unsupported_op(self, dev): """Test if a QuantumFunctionError is raised for an unsupported operation, i.e., multi-parameter operations that are not qml.Rot""" @@ -180,11 +236,19 @@ def test_unsupported_op(self, dev): dy = np.array([1.0]) - with pytest.raises( - qml.QuantumFunctionError, - match="The CRot operation is not supported using the", - ): - dev.vjp(tape.measurements, dy)(tape) + if dev._new_API: + with pytest.raises( + LightningException, + match="The operation is not supported using the", + ): + dev.compute_vjp(tape, dy) + + else: + with pytest.raises( + qml.QuantumFunctionError, + match="The CRot operation is not supported using the", + ): + dev.vjp(tape.measurements, dy)(tape) def test_proj_unsupported(self, dev): """Test if a QuantumFunctionError is raised for a Projector observable""" @@ -211,415 +275,402 @@ def test_proj_unsupported(self, dev): ): dev.vjp(tape.measurements, dy)(tape) - def test_hermitian_expectation(self, dev, tol): - obs = np.array([[1, 0], [0, -1]], dtype=dev.C_DTYPE, requires_grad=False) - dy = np.array([0.8]) - - fn = dev.vjp([qml.expval(qml.Hermitian(obs, wires=(0,)))], dy) - - for x in np.linspace(-2 * math.pi, 2 * math.pi, 7): - with qml.tape.QuantumTape() as tape: - qml.RY(x, wires=(0,)) - vjp = fn(tape) - assert np.allclose(vjp, -0.8 * np.sin(x), atol=tol) - - def test_hermitian_tensor_expectation(self, dev, tol): - obs = np.array([[1, 0], [0, -1]], dtype=dev.C_DTYPE, requires_grad=False) - dy = np.array([0.8]) - - fn = dev.vjp([qml.expval(qml.Hermitian(obs, wires=(0,)) @ qml.PauliZ(wires=1))], dy) - - for x in np.linspace(-2 * math.pi, 2 * math.pi, 7): - with qml.tape.QuantumTape() as tape: - qml.RY(x, wires=(0,)) - assert np.allclose(fn(tape), -0.8 * np.sin(x), atol=tol) - - @pytest.mark.skipif( - device_name == device_name, - reason="Adjoint differentiation does not support State measurements.", - ) - def test_statevector_ry(self, dev, tol): - dy = np.array( - [ - [1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - ] - ) - fn0 = dev.vjp([qml.state()], dy[0, :]) - fn1 = dev.vjp([qml.state()], dy[1, :]) - fn2 = dev.vjp([qml.state()], dy[2, :]) - fn3 = dev.vjp([qml.state()], dy[3, :]) - - for x in np.linspace(-2 * math.pi, 2 * math.pi, 7): - with qml.tape.QuantumTape() as tape: - qml.RY(x, wires=(0,)) - assert np.allclose(fn0(tape), -np.sin(x / 2) / 2, atol=tol) - assert np.allclose(fn1(tape), np.cos(x / 2) / 2, atol=tol) - assert np.allclose(fn2(tape), 0.0, atol=tol) - assert np.allclose(fn3(tape), 0.0, atol=tol) - - @pytest.mark.skipif( - device_name == device_name, - reason="Adjoint differentiation does not support State measurements.", - ) - def test_wrong_dy_statevector(self, dev): - """Tests raise an exception when dy is incorrect""" - x, y, z = [0.5, 0.3, -0.7] - - with qml.tape.QuantumTape() as tape: - qml.RX(0.4, wires=[0]) - qml.Rot(x, y, z, wires=[0]) - qml.RY(-0.2, wires=[0]) - qml.state() - - tape.trainable_params = {1, 2, 3} - - dy1 = np.ones(3, dtype=dev.C_DTYPE) - - with pytest.raises( - ValueError, - match="Size of the provided vector grad_vec must be the same as the size of", - ): - dev.vjp(tape.measurements, dy1) - - dy2 = np.ones(4, dtype=dev.R_DTYPE) - - with pytest.warns( - UserWarning, match="The vjp method only works with complex-valued grad_vec" - ): - dev.vjp(tape.measurements, dy2) - - @pytest.mark.skipif( - device_name == device_name, - reason="Adjoint differentiation does not support State measurements.", - ) - @pytest.mark.parametrize("stateprep", [qml.QubitStateVector, qml.StatePrep]) - def test_statevector_complex_circuit(self, stateprep, dev, tol): - dy = np.array( - [ - [1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - ] - ) - fn0 = dev.vjp([qml.state()], dy[0, :]) - fn1 = dev.vjp([qml.state()], dy[1, :]) - fn2 = dev.vjp([qml.state()], dy[2, :]) - fn3 = dev.vjp([qml.state()], dy[3, :]) - - params = [math.pi / 7, 6 * math.pi / 7] - - with qml.tape.QuantumTape() as tape: - stateprep(np.array([1.0] * 4) / 2, wires=range(2)) - qml.RY(params[0], wires=0) - qml.RZ(params[1], wires=1) - qml.CZ(wires=[0, 1]) - - tape.trainable_params = {2} # RZ - - psi_00_diff = ( - (math.cos(params[0] / 2) - math.sin(params[0] / 2)) - * (-math.sin(params[1] / 2) - 1j * math.cos(params[1] / 2)) - / 4 - ) - psi_01_diff = ( - (math.cos(params[0] / 2) + math.sin(params[0] / 2)) - * (-math.sin(params[1] / 2) - 1j * math.cos(params[1] / 2)) - / 4 - ) - psi_10_diff = ( - (math.cos(params[0] / 2) - math.sin(params[0] / 2)) - * (-math.sin(params[1] / 2) + 1j * math.cos(params[1] / 2)) - / 4 - ) - psi_11_diff = ( - -(math.cos(params[0] / 2) + math.sin(params[0] / 2)) - * (-math.sin(params[1] / 2) + 1j * math.cos(params[1] / 2)) - / 4 - ) - - assert np.allclose(fn0(tape), psi_00_diff, atol=tol) - assert np.allclose(fn1(tape), psi_01_diff, atol=tol) - assert np.allclose(fn2(tape), psi_10_diff, atol=tol) - assert np.allclose(fn3(tape), psi_11_diff, atol=tol) - - def test_no_trainable_parameters(self, dev): - """A tape with no trainable parameters will simply return None""" - x = 0.4 - - with qml.tape.QuantumTape() as tape: - qml.RX(x, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - tape.trainable_params = {} - dy = np.array([1.0]) - - fn = dev.vjp(tape.measurements, dy) - vjp = fn(tape) - - assert len(vjp) == 0 - - def test_no_trainable_parameters_NEW(self, dev): - """A tape with no trainable parameters will simply return None""" - _state = dev._asarray(dev.state) - dev._apply_state_vector(_state, dev.wires) - - x = 0.4 - - with qml.tape.QuantumTape() as tape: - qml.RX(x, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - tape.trainable_params = {} - dy = np.array([1.0]) - fn = dev.vjp(tape.measurements, dy) - vjp = fn(tape) - - assert len(vjp) == 0 - - def test_no_trainable_parameters(self, dev): - """A tape with no trainable parameters will simply return None""" - x = 0.4 - - with qml.tape.QuantumTape() as tape: - qml.RX(x, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - tape.trainable_params = {} - dy = np.array([1.0]) - - fn = dev.vjp(tape.measurements, dy) - vjp = fn(tape) - - assert len(vjp) == 0 - - def test_zero_dy(self, dev): - """A zero dy vector will return no tapes and a zero matrix""" - x = 0.4 - y = 0.6 - - with qml.tape.QuantumTape() as tape: - qml.RX(x, wires=0) - qml.RX(y, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - tape.trainable_params = {0, 1} - dy = np.array([0.0]) - - fn = dev.vjp(tape.measurements, dy) - vjp = fn(tape) - - assert np.all(vjp == np.zeros([len(tape.trainable_params)])) - - def test_single_expectation_value(self, tol, dev): - """Tests correct output shape and evaluation for a tape - with a single expval output""" - x = 0.543 - y = -0.654 - - with qml.tape.QuantumTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - - tape.trainable_params = {0, 1} - dy = np.array([1.0]) - - fn = dev.vjp(tape.measurements, dy) - vjp = fn(tape) - - expected = np.array([-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]) - assert np.allclose(vjp, expected, atol=tol, rtol=0) - - def test_multiple_expectation_values(self, tol, dev): - """Tests correct output shape and evaluation for a tape - with multiple expval outputs""" - x = 0.543 - y = -0.654 - - with qml.tape.QuantumTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.expval(qml.PauliX(1)) - - tape.trainable_params = {0, 1} - dy = np.array([1.0, 2.0]) - - fn = dev.vjp(tape.measurements, dy) - vjp = fn(tape) - - expected = np.array([-np.sin(x), 2 * np.cos(y)]) - assert np.allclose(vjp, expected, atol=tol, rtol=0) - - def test_prob_expectation_values(self, dev): - """Tests correct output shape and evaluation for a tape - with prob and expval outputs""" - _state = dev._asarray(dev.state) - dev._apply_state_vector(_state, dev.wires) - - x = 0.543 - y = -0.654 - - with qml.tape.QuantumTape() as tape: - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - qml.probs(wires=[0, 1]) - - tape.trainable_params = {0, 1} - dy = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - - with pytest.raises( - qml.QuantumFunctionError, - match="Adjoint differentiation method does not support", - ): - dev.vjp(tape.measurements, dy)(tape) - - -class TestBatchVectorJacobianProduct: - """Tests for the batch_vjp function""" - - @pytest.fixture(params=[np.complex64, np.complex128]) - def dev(self, request): - return qml.device(device_name, wires=2, c_dtype=request.param) - - def test_one_tape_no_trainable_parameters_1(self, dev): - """A tape with no trainable parameters will simply return None""" - with qml.tape.QuantumTape() as tape1: - qml.RX(0.4, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - with qml.tape.QuantumTape() as tape2: - qml.RX(0.4, wires=0) - qml.RX(0.6, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - tape1.trainable_params = {} - tape2.trainable_params = {0, 1} - - tapes = [tape1, tape2] - dys = [np.array([1.0]), np.array([1.0])] - - fn = dev.batch_vjp(tapes, dys) - vjps = fn(tapes) - - assert len(vjps[0]) == 0 - assert vjps[1] is not None - - def test_all_tapes_no_trainable_parameters_2(self, dev): - """If all tapes have no trainable parameters all outputs will be None""" - with qml.tape.QuantumTape() as tape1: - qml.RX(0.4, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - with qml.tape.QuantumTape() as tape2: - qml.RX(0.4, wires=0) - qml.RX(0.6, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - tape1.trainable_params = set() - tape2.trainable_params = set() - - tapes = [tape1, tape2] - dys = [np.array([1.0]), np.array([1.0])] - - fn = dev.batch_vjp(tapes, dys) - vjps = fn(tapes) - - assert len(vjps[0]) == 0 - assert len(vjps[1]) == 0 - - def test_zero_dy(self, dev): - """A zero dy vector will return no tapes and a zero matrix""" - with qml.tape.QuantumTape() as tape1: - qml.RX(0.4, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - with qml.tape.QuantumTape() as tape2: - qml.RX(0.4, wires=0) - qml.RX(0.6, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - tape1.trainable_params = {0} - tape2.trainable_params = {0, 1} - - tapes = [tape1, tape2] - dys = [np.array([0.0]), np.array([1.0])] - - fn = dev.batch_vjp(tapes, dys) - vjps = fn(tapes) - - assert np.allclose(vjps[0], 0) - - def test_reduction_append(self, dev): - """Test the 'append' reduction strategy""" - _state = dev._asarray(dev.state) - dev._apply_state_vector(_state, dev.wires) - - with qml.tape.QuantumTape() as tape1: - qml.RX(0.4, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - with qml.tape.QuantumTape() as tape2: - qml.RX(0.4, wires=0) - qml.RX(0.6, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - tape1.trainable_params = {0} - tape2.trainable_params = {0, 1} - - tapes = [tape1, tape2] - dys = [np.array([1.0]), np.array([1.0])] - - fn = dev.batch_vjp(tapes, dys, reduction="append") - vjps = fn(tapes) - - assert len(vjps) == 2 - assert len(vjps[1]) == 2 - assert isinstance(vjps[0], np.ndarray) - assert isinstance(vjps[1][0], np.ndarray) - assert isinstance(vjps[1][1], np.ndarray) - - @pytest.mark.parametrize("reduction_keyword", ("extend", list.extend)) - def test_reduction_extend(self, dev, reduction_keyword): - """Test the 'extend' reduction strategy""" - with qml.tape.QuantumTape() as tape1: - qml.RX(0.4, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - with qml.tape.QuantumTape() as tape2: - qml.RX(0.4, wires=0) - qml.RX(0.6, wires=0) - qml.CNOT(wires=[0, 1]) - qml.expval(qml.PauliZ(0)) - - tape1.trainable_params = {0} - tape2.trainable_params = {0, 1} - - tapes = [tape1, tape2] - dys = [np.array([1.0]), np.array([1.0])] - fn = dev.batch_vjp(tapes, dys, reduction=reduction_keyword) - vjps = fn(tapes) +# def test_hermitian_expectation(self, dev, tol): +# obs = np.array([[1, 0], [0, -1]], dtype=dev.C_DTYPE, requires_grad=False) +# dy = np.array([0.8]) + +# fn = dev.vjp([qml.expval(qml.Hermitian(obs, wires=(0,)))], dy) + +# for x in np.linspace(-2 * math.pi, 2 * math.pi, 7): +# with qml.tape.QuantumTape() as tape: +# qml.RY(x, wires=(0,)) +# vjp = fn(tape) +# assert np.allclose(vjp, -0.8 * np.sin(x), atol=tol) + +# def test_hermitian_tensor_expectation(self, dev, tol): +# obs = np.array([[1, 0], [0, -1]], dtype=dev.C_DTYPE, requires_grad=False) +# dy = np.array([0.8]) + +# fn = dev.vjp([qml.expval(qml.Hermitian(obs, wires=(0,)) @ qml.PauliZ(wires=1))], dy) + +# for x in np.linspace(-2 * math.pi, 2 * math.pi, 7): +# with qml.tape.QuantumTape() as tape: +# qml.RY(x, wires=(0,)) +# assert np.allclose(fn(tape), -0.8 * np.sin(x), atol=tol) + +# @pytest.mark.skipif( +# device_name == device_name, +# reason="Adjoint differentiation does not support State measurements.", +# ) +# def test_statevector_ry(self, dev, tol): +# dy = np.array( +# [ +# [1.0, 0.0, 0.0, 0.0], +# [0.0, 0.0, 1.0, 0.0], +# [0.0, 1.0, 0.0, 0.0], +# [0.0, 0.0, 0.0, 1.0], +# ] +# ) +# fn0 = dev.vjp([qml.state()], dy[0, :]) +# fn1 = dev.vjp([qml.state()], dy[1, :]) +# fn2 = dev.vjp([qml.state()], dy[2, :]) +# fn3 = dev.vjp([qml.state()], dy[3, :]) + +# for x in np.linspace(-2 * math.pi, 2 * math.pi, 7): +# with qml.tape.QuantumTape() as tape: +# qml.RY(x, wires=(0,)) +# assert np.allclose(fn0(tape), -np.sin(x / 2) / 2, atol=tol) +# assert np.allclose(fn1(tape), np.cos(x / 2) / 2, atol=tol) +# assert np.allclose(fn2(tape), 0.0, atol=tol) +# assert np.allclose(fn3(tape), 0.0, atol=tol) + +# @pytest.mark.skipif( +# device_name == device_name, +# reason="Adjoint differentiation does not support State measurements.", +# ) +# def test_wrong_dy_statevector(self, dev): +# """Tests raise an exception when dy is incorrect""" +# x, y, z = [0.5, 0.3, -0.7] + +# with qml.tape.QuantumTape() as tape: +# qml.RX(0.4, wires=[0]) +# qml.Rot(x, y, z, wires=[0]) +# qml.RY(-0.2, wires=[0]) +# qml.state() + +# tape.trainable_params = {1, 2, 3} + +# dy1 = np.ones(3, dtype=dev.C_DTYPE) + +# with pytest.raises( +# ValueError, +# match="Size of the provided vector grad_vec must be the same as the size of", +# ): +# dev.vjp(tape.measurements, dy1) + +# dy2 = np.ones(4, dtype=dev.R_DTYPE) + +# with pytest.warns( +# UserWarning, match="The vjp method only works with complex-valued grad_vec" +# ): +# dev.vjp(tape.measurements, dy2) + +# @pytest.mark.skipif( +# device_name == device_name, +# reason="Adjoint differentiation does not support State measurements.", +# ) +# @pytest.mark.parametrize("stateprep", [qml.QubitStateVector, qml.StatePrep]) +# def test_statevector_complex_circuit(self, stateprep, dev, tol): +# dy = np.array( +# [ +# [1.0, 0.0, 0.0, 0.0], +# [0.0, 0.0, 1.0, 0.0], +# [0.0, 1.0, 0.0, 0.0], +# [0.0, 0.0, 0.0, 1.0], +# ] +# ) +# fn0 = dev.vjp([qml.state()], dy[0, :]) +# fn1 = dev.vjp([qml.state()], dy[1, :]) +# fn2 = dev.vjp([qml.state()], dy[2, :]) +# fn3 = dev.vjp([qml.state()], dy[3, :]) + +# params = [math.pi / 7, 6 * math.pi / 7] + +# with qml.tape.QuantumTape() as tape: +# stateprep(np.array([1.0] * 4) / 2, wires=range(2)) +# qml.RY(params[0], wires=0) +# qml.RZ(params[1], wires=1) +# qml.CZ(wires=[0, 1]) + +# tape.trainable_params = {2} # RZ + +# psi_00_diff = ( +# (math.cos(params[0] / 2) - math.sin(params[0] / 2)) +# * (-math.sin(params[1] / 2) - 1j * math.cos(params[1] / 2)) +# / 4 +# ) +# psi_01_diff = ( +# (math.cos(params[0] / 2) + math.sin(params[0] / 2)) +# * (-math.sin(params[1] / 2) - 1j * math.cos(params[1] / 2)) +# / 4 +# ) +# psi_10_diff = ( +# (math.cos(params[0] / 2) - math.sin(params[0] / 2)) +# * (-math.sin(params[1] / 2) + 1j * math.cos(params[1] / 2)) +# / 4 +# ) +# psi_11_diff = ( +# -(math.cos(params[0] / 2) + math.sin(params[0] / 2)) +# * (-math.sin(params[1] / 2) + 1j * math.cos(params[1] / 2)) +# / 4 +# ) + +# assert np.allclose(fn0(tape), psi_00_diff, atol=tol) +# assert np.allclose(fn1(tape), psi_01_diff, atol=tol) +# assert np.allclose(fn2(tape), psi_10_diff, atol=tol) +# assert np.allclose(fn3(tape), psi_11_diff, atol=tol) + +# def test_no_trainable_parameters(self, dev): +# """A tape with no trainable parameters will simply return None""" +# x = 0.4 + +# with qml.tape.QuantumTape() as tape: +# qml.RX(x, wires=0) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) + +# tape.trainable_params = {} +# dy = np.array([1.0]) + +# fn = dev.vjp(tape.measurements, dy) +# vjp = fn(tape) + +# assert len(vjp) == 0 + +# @pytest.mark.skipif(ld._new_API, reason="Old API required") +# def test_no_trainable_parameters_NEW(self, dev): +# """A tape with no trainable parameters will simply return None""" +# _state = dev._asarray(dev.state) +# dev._apply_state_vector(_state, dev.wires) + +# x = 0.4 + +# with qml.tape.QuantumTape() as tape: +# qml.RX(x, wires=0) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) + +# tape.trainable_params = {} +# dy = np.array([1.0]) +# fn = dev.vjp(tape.measurements, dy) +# vjp = fn(tape) + +# assert len(vjp) == 0 + +# def test_zero_dy(self, dev): +# """A zero dy vector will return no tapes and a zero matrix""" +# x = 0.4 +# y = 0.6 + +# with qml.tape.QuantumTape() as tape: +# qml.RX(x, wires=0) +# qml.RX(y, wires=0) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) + +# tape.trainable_params = {0, 1} +# dy = np.array([0.0]) + +# fn = dev.vjp(tape.measurements, dy) +# vjp = fn(tape) + +# assert np.all(vjp == np.zeros([len(tape.trainable_params)])) + +# def test_single_expectation_value(self, tol, dev): +# """Tests correct output shape and evaluation for a tape +# with a single expval output""" +# x = 0.543 +# y = -0.654 + +# with qml.tape.QuantumTape() as tape: +# qml.RX(x, wires=[0]) +# qml.RY(y, wires=[1]) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) + +# tape.trainable_params = {0, 1} +# dy = np.array([1.0]) + +# fn = dev.vjp(tape.measurements, dy) +# vjp = fn(tape) + +# expected = np.array([-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]) +# assert np.allclose(vjp, expected, atol=tol, rtol=0) + +# def test_multiple_expectation_values(self, tol, dev): +# """Tests correct output shape and evaluation for a tape +# with multiple expval outputs""" +# x = 0.543 +# y = -0.654 + +# with qml.tape.QuantumTape() as tape: +# qml.RX(x, wires=[0]) +# qml.RY(y, wires=[1]) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) +# qml.expval(qml.PauliX(1)) + +# tape.trainable_params = {0, 1} +# dy = np.array([1.0, 2.0]) + +# fn = dev.vjp(tape.measurements, dy) +# vjp = fn(tape) + +# expected = np.array([-np.sin(x), 2 * np.cos(y)]) +# assert np.allclose(vjp, expected, atol=tol, rtol=0) + +# def test_prob_expectation_values(self, dev): +# """Tests correct output shape and evaluation for a tape +# with prob and expval outputs""" +# _state = dev._asarray(dev.state) +# dev._apply_state_vector(_state, dev.wires) + +# x = 0.543 +# y = -0.654 - assert len(vjps) == sum(len(t.trainable_params) for t in tapes) +# with qml.tape.QuantumTape() as tape: +# qml.RX(x, wires=[0]) +# qml.RY(y, wires=[1]) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) +# qml.probs(wires=[0, 1]) + +# tape.trainable_params = {0, 1} +# dy = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + +# with pytest.raises( +# qml.QuantumFunctionError, +# match="Adjoint differentiation method does not support", +# ): +# dev.vjp(tape.measurements, dy)(tape) + + +# @pytest.mark.skipif(ld._new_API, reason="Old API required") +# class TestBatchVectorJacobianProduct: +# """Tests for the batch_vjp function""" + +# @pytest.fixture(params=[np.complex64, np.complex128]) +# def dev(self, request): +# return qml.device(device_name, wires=2, c_dtype=request.param) + +# def test_one_tape_no_trainable_parameters_1(self, dev): +# """A tape with no trainable parameters will simply return None""" +# with qml.tape.QuantumTape() as tape1: +# qml.RX(0.4, wires=0) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) + +# with qml.tape.QuantumTape() as tape2: +# qml.RX(0.4, wires=0) +# qml.RX(0.6, wires=0) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) + +# tape1.trainable_params = {} +# tape2.trainable_params = {0, 1} + +# tapes = [tape1, tape2] +# dys = [np.array([1.0]), np.array([1.0])] + +# fn = dev.batch_vjp(tapes, dys) +# vjps = fn(tapes) + +# assert len(vjps[0]) == 0 +# assert vjps[1] is not None + +# def test_all_tapes_no_trainable_parameters_2(self, dev): +# """If all tapes have no trainable parameters all outputs will be None""" +# with qml.tape.QuantumTape() as tape1: +# qml.RX(0.4, wires=0) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) + +# with qml.tape.QuantumTape() as tape2: +# qml.RX(0.4, wires=0) +# qml.RX(0.6, wires=0) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) + +# tape1.trainable_params = set() +# tape2.trainable_params = set() + +# tapes = [tape1, tape2] +# dys = [np.array([1.0]), np.array([1.0])] + +# fn = dev.batch_vjp(tapes, dys) +# vjps = fn(tapes) + +# assert len(vjps[0]) == 0 +# assert len(vjps[1]) == 0 + +# def test_zero_dy(self, dev): +# """A zero dy vector will return no tapes and a zero matrix""" +# with qml.tape.QuantumTape() as tape1: +# qml.RX(0.4, wires=0) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) + +# with qml.tape.QuantumTape() as tape2: +# qml.RX(0.4, wires=0) +# qml.RX(0.6, wires=0) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) + +# tape1.trainable_params = {0} +# tape2.trainable_params = {0, 1} + +# tapes = [tape1, tape2] +# dys = [np.array([0.0]), np.array([1.0])] + +# fn = dev.batch_vjp(tapes, dys) +# vjps = fn(tapes) + +# assert np.allclose(vjps[0], 0) + +# @pytest.mark.skipif(ld._new_API, reason="Old API required") +# def test_reduction_append(self, dev): +# """Test the 'append' reduction strategy""" +# _state = dev._asarray(dev.state) +# dev._apply_state_vector(_state, dev.wires) + +# with qml.tape.QuantumTape() as tape1: +# qml.RX(0.4, wires=0) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) + +# with qml.tape.QuantumTape() as tape2: +# qml.RX(0.4, wires=0) +# qml.RX(0.6, wires=0) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) + +# tape1.trainable_params = {0} +# tape2.trainable_params = {0, 1} + +# tapes = [tape1, tape2] +# dys = [np.array([1.0]), np.array([1.0])] + +# fn = dev.batch_vjp(tapes, dys, reduction="append") +# vjps = fn(tapes) + +# assert len(vjps) == 2 +# assert len(vjps[1]) == 2 +# assert isinstance(vjps[0], np.ndarray) +# assert isinstance(vjps[1][0], np.ndarray) +# assert isinstance(vjps[1][1], np.ndarray) + +# @pytest.mark.parametrize("reduction_keyword", ("extend", list.extend)) +# def test_reduction_extend(self, dev, reduction_keyword): +# """Test the 'extend' reduction strategy""" +# with qml.tape.QuantumTape() as tape1: +# qml.RX(0.4, wires=0) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) + +# with qml.tape.QuantumTape() as tape2: +# qml.RX(0.4, wires=0) +# qml.RX(0.6, wires=0) +# qml.CNOT(wires=[0, 1]) +# qml.expval(qml.PauliZ(0)) + +# tape1.trainable_params = {0} +# tape2.trainable_params = {0, 1} + +# tapes = [tape1, tape2] +# dys = [np.array([1.0]), np.array([1.0])] + +# fn = dev.batch_vjp(tapes, dys, reduction=reduction_keyword) +# vjps = fn(tapes) + +# assert len(vjps) == sum(len(t.trainable_params) for t in tapes) \ No newline at end of file From e503442bcf88616ab8f296e627da5f0c063189dd Mon Sep 17 00:00:00 2001 From: AmintorDusko Date: Thu, 21 Mar 2024 10:55:48 -0400 Subject: [PATCH 02/46] update vjp tests --- tests/test_vjp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_vjp.py b/tests/test_vjp.py index 209cac3e28..8bc616934b 100644 --- a/tests/test_vjp.py +++ b/tests/test_vjp.py @@ -23,7 +23,7 @@ from pennylane import numpy as np if ld._CPP_BINARY_AVAILABLE: - if device_name == "lightning.qubit2": + if ld._new_API: from pennylane_lightning.lightning_qubit_ops import LightningException else: From 473c0300cd4cfd2860e187733bdba926854b9aba Mon Sep 17 00:00:00 2001 From: AmintorDusko Date: Thu, 21 Mar 2024 10:57:45 -0400 Subject: [PATCH 03/46] format --- pennylane_lightning/lightning_qubit/lightning_qubit.py | 8 ++++---- tests/test_vjp.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pennylane_lightning/lightning_qubit/lightning_qubit.py b/pennylane_lightning/lightning_qubit/lightning_qubit.py index 78207026cb..31a5a3e25b 100644 --- a/pennylane_lightning/lightning_qubit/lightning_qubit.py +++ b/pennylane_lightning/lightning_qubit/lightning_qubit.py @@ -14,11 +14,10 @@ """ This module contains the LightningQubit class that inherits from the new device interface. """ -from numbers import Number -from typing import Optional, Union, Sequence, Callable, Tuple from dataclasses import replace +from numbers import Number from pathlib import Path -from typing import Callable, Optional, Sequence, Union +from typing import Callable, Optional, Sequence, Tuple, Union import numpy as np import pennylane as qml @@ -116,6 +115,7 @@ def simulate_and_jacobian(circuit: QuantumTape, state: LightningStateVector, bat jac = LightningAdjointJacobian(state, batch_obs=batch_obs).calculate_jacobian(circuit) return res, jac + def vjp( circuit: QuantumTape, cotangents: Tuple[Number], state: LightningStateVector, batch_obs=False ): @@ -655,4 +655,4 @@ def execute_and_compute_vjp( simulate_and_vjp(circuit, cots, self._statevector, batch_obs=batch_obs) for circuit, cots in zip(circuits, cotangents) ) - return tuple(zip(*results)) \ No newline at end of file + return tuple(zip(*results)) diff --git a/tests/test_vjp.py b/tests/test_vjp.py index 8bc616934b..6c708a4e80 100644 --- a/tests/test_vjp.py +++ b/tests/test_vjp.py @@ -673,4 +673,4 @@ def test_proj_unsupported(self, dev): # fn = dev.batch_vjp(tapes, dys, reduction=reduction_keyword) # vjps = fn(tapes) -# assert len(vjps) == sum(len(t.trainable_params) for t in tapes) \ No newline at end of file +# assert len(vjps) == sum(len(t.trainable_params) for t in tapes) From e3ff0f73441083d35e91693656039b4a6d3ac40e Mon Sep 17 00:00:00 2001 From: AmintorDusko Date: Thu, 21 Mar 2024 12:09:29 -0400 Subject: [PATCH 04/46] update vjp tests --- tests/test_vjp.py | 74 +++++++++++++++++------------------------------ 1 file changed, 27 insertions(+), 47 deletions(-) diff --git a/tests/test_vjp.py b/tests/test_vjp.py index 6c708a4e80..3f6b68d9ae 100644 --- a/tests/test_vjp.py +++ b/tests/test_vjp.py @@ -29,10 +29,6 @@ else: pytest.skip("No binary module found. Skipping.", allow_module_level=True) -# if ld._new_API: -# pytest.skip("Old API required", allow_module_level=True) - - def get_vjp(device, tapes, dy): """Helper to get VJP for a tape or batch of tapes""" if device._new_API: @@ -78,7 +74,7 @@ def test_use_device_state(self, tol, dev): qml.execute([tape], dev, None) fn2 = dev.vjp(tape.measurements, dy, use_device_state=True) - vjp2 = fn2(tape.measure) + vjp2 = fn2(tape) assert np.allclose(vjp1, vjp2, atol=tol, rtol=0) @@ -131,10 +127,10 @@ def test_multiple_measurements(self, tol, dev): tape2.trainable_params = {1, 2, 3} - vjp1 = get_vjp(dev, tape1, dy) - vjp2 = get_jacobian(dev, tape2) + vjp = get_vjp(dev, tape1, dy) + jac = get_jacobian(dev, tape2) - assert np.allclose(vjp1, vjp2, atol=tol, rtol=0) + assert np.allclose(vjp, jac, atol=tol, rtol=0) def test_wrong_dy_expval(self, dev): """Tests raise an exception when dy is incorrect""" @@ -152,27 +148,16 @@ def test_wrong_dy_expval(self, dev): dy2 = np.array([1.0 + 3.0j, 0.3 + 2.0j, 0.5 + 0.1j]) tape1.trainable_params = {1, 2, 3} - if dev._new_API: - with pytest.raises( - ValueError, match="Number of observables in the tape must be the same as" - ): - dev.compute_vjp(tape1, dy1) - - with pytest.raises( - ValueError, match="The vjp method only works with a real-valued grad_vec" - ): - dev.compute_vjp(tape1, dy2) + with pytest.raises( + ValueError, match="Number of observables in the tape must be the same as" + ): + get_vjp(dev, tape1, dy1) - else: - with pytest.raises( - ValueError, match="Number of observables in the tape must be the same as" - ): - dev.vjp(tape1.measurements, dy1) + with pytest.raises( + ValueError, match="The vjp method only works with a real-valued grad_vec" + ): + get_vjp(dev, tape1, dy2) - with pytest.raises( - ValueError, match="The vjp method only works with a real-valued grad_vec" - ): - dev.vjp(tape1.measurements, dy2) def test_not_expval(self, dev): """Test if a QuantumFunctionError is raised for a tape with measurements that are not @@ -183,17 +168,10 @@ def test_not_expval(self, dev): dy = np.array([1.0]) - if dev._new_API: - with pytest.raises( - qml.QuantumFunctionError, match="Adjoint differentiation method does not" - ): - dev.compute_vjp(tape, dy) - - else: - with pytest.raises( - qml.QuantumFunctionError, match="Adjoint differentiation method does not" - ): - dev.vjp(tape.measurements, dy)(tape) + with pytest.raises( + qml.QuantumFunctionError, match="Adjoint differentiation method does not" + ): + get_vjp(dev, tape, dy) @pytest.mark.skipif(ld._new_API, reason="Old API required") def test_finite_shots_warns(self): @@ -250,6 +228,7 @@ def test_unsupported_op(self, dev): ): dev.vjp(tape.measurements, dy)(tape) + @pytest.mark.skipif(ld._new_API, reason="Old API required") def test_proj_unsupported(self, dev): """Test if a QuantumFunctionError is raised for a Projector observable""" @@ -276,17 +255,18 @@ def test_proj_unsupported(self, dev): dev.vjp(tape.measurements, dy)(tape) -# def test_hermitian_expectation(self, dev, tol): -# obs = np.array([[1, 0], [0, -1]], dtype=dev.C_DTYPE, requires_grad=False) -# dy = np.array([0.8]) + def test_hermitian_expectation(self, dev, tol): + obs = np.array([[1, 0], [0, -1]], dtype=dev.dtype, requires_grad=False) + dy = np.array([0.8]) -# fn = dev.vjp([qml.expval(qml.Hermitian(obs, wires=(0,)))], dy) + for x in np.linspace(-2 * math.pi, 2 * math.pi, 7): + with qml.tape.QuantumTape() as tape: + qml.RY(x, wires=(0,)) + qml.expval(qml.Hermitian(obs, wires=(0,))) -# for x in np.linspace(-2 * math.pi, 2 * math.pi, 7): -# with qml.tape.QuantumTape() as tape: -# qml.RY(x, wires=(0,)) -# vjp = fn(tape) -# assert np.allclose(vjp, -0.8 * np.sin(x), atol=tol) + tape.trainable_params = {0} + vjp = get_vjp(dev, tape, dy) + assert np.allclose(vjp, -0.8 * np.sin(x), atol=tol) # def test_hermitian_tensor_expectation(self, dev, tol): # obs = np.array([[1, 0], [0, -1]], dtype=dev.C_DTYPE, requires_grad=False) From 016388bef7c84f573aee56062f0fca26dfa48b44 Mon Sep 17 00:00:00 2001 From: AmintorDusko Date: Fri, 22 Mar 2024 13:26:41 -0400 Subject: [PATCH 05/46] update dev version --- pennylane_lightning/core/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 778353eb5b..4089152989 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.36.0-dev16" +__version__ = "0.36.0-dev17" From 3e8e2cd528310f9d3c149e44fcdf2d3a12e7f03f Mon Sep 17 00:00:00 2001 From: AmintorDusko Date: Fri, 22 Mar 2024 13:27:46 -0400 Subject: [PATCH 06/46] format --- tests/test_vjp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_vjp.py b/tests/test_vjp.py index 3f6b68d9ae..b3919253ac 100644 --- a/tests/test_vjp.py +++ b/tests/test_vjp.py @@ -29,6 +29,7 @@ else: pytest.skip("No binary module found. Skipping.", allow_module_level=True) + def get_vjp(device, tapes, dy): """Helper to get VJP for a tape or batch of tapes""" if device._new_API: @@ -158,7 +159,6 @@ def test_wrong_dy_expval(self, dev): ): get_vjp(dev, tape1, dy2) - def test_not_expval(self, dev): """Test if a QuantumFunctionError is raised for a tape with measurements that are not expectation values""" @@ -254,7 +254,6 @@ def test_proj_unsupported(self, dev): ): dev.vjp(tape.measurements, dy)(tape) - def test_hermitian_expectation(self, dev, tol): obs = np.array([[1, 0], [0, -1]], dtype=dev.dtype, requires_grad=False) dy = np.array([0.8]) @@ -268,6 +267,7 @@ def test_hermitian_expectation(self, dev, tol): vjp = get_vjp(dev, tape, dy) assert np.allclose(vjp, -0.8 * np.sin(x), atol=tol) + # def test_hermitian_tensor_expectation(self, dev, tol): # obs = np.array([[1, 0], [0, -1]], dtype=dev.C_DTYPE, requires_grad=False) # dy = np.array([0.8]) From 3032b65b80a13d897bb4538c0599794b36729d49 Mon Sep 17 00:00:00 2001 From: AmintorDusko Date: Fri, 22 Mar 2024 15:01:04 -0400 Subject: [PATCH 07/46] add tests for vjp --- tests/test_vjp.py | 652 +++++++++++++++++++--------------------------- 1 file changed, 265 insertions(+), 387 deletions(-) diff --git a/tests/test_vjp.py b/tests/test_vjp.py index b3919253ac..0b571fe4c9 100644 --- a/tests/test_vjp.py +++ b/tests/test_vjp.py @@ -1,4 +1,4 @@ -# Copyright 2018-2023 Xanadu Quantum Technologies Inc. +# Copyright 2018-2024 Xanadu Quantum Technologies Inc. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Tests for the ``vjp`` method of LightningKokkos. +Tests for the ``vjp`` method. """ import math @@ -267,390 +267,268 @@ def test_hermitian_expectation(self, dev, tol): vjp = get_vjp(dev, tape, dy) assert np.allclose(vjp, -0.8 * np.sin(x), atol=tol) + def test_hermitian_tensor_expectation(self, dev, tol): + obs = np.array([[1, 0], [0, -1]], dtype=dev.dtype, requires_grad=False) + dy = np.array([0.8]) + + for x in np.linspace(-2 * math.pi, 2 * math.pi, 7): + with qml.tape.QuantumTape() as tape: + qml.RY(x, wires=(0,)) + qml.expval(qml.Hermitian(obs, wires=(0,)) @ qml.PauliZ(wires=1)) + + tape.trainable_params = {0} + vjp = get_vjp(dev, tape, dy) + assert np.allclose(vjp, -0.8 * np.sin(x), atol=tol) + + def test_no_trainable_parameters(self, dev): + """A tape with no trainable parameters will simply return None""" + x = 0.4 + + with qml.tape.QuantumTape() as tape: + qml.RX(x, wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + + tape.trainable_params = {} + dy = np.array([1.0]) + + vjp = get_vjp(dev, tape, dy) + + assert len(vjp) == 0 + + @pytest.mark.skipif(ld._new_API, reason="Old API required") + def test_no_trainable_parameters_NEW(self, dev): + """A tape with no trainable parameters will simply return None""" + _state = dev._asarray(dev.state) + dev._apply_state_vector(_state, dev.wires) + + x = 0.4 + + with qml.tape.QuantumTape() as tape: + qml.RX(x, wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + + tape.trainable_params = {} + dy = np.array([1.0]) + vjp = get_vjp(dev, tape, dy) + + assert len(vjp) == 0 + + def test_zero_dy(self, dev): + """A zero dy vector will return no tapes and a zero matrix""" + x = 0.4 + y = 0.6 + + with qml.tape.QuantumTape() as tape: + qml.RX(x, wires=0) + qml.RX(y, wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + + tape.trainable_params = {0, 1} + dy = np.array([0.0]) + + vjp = get_vjp(dev, tape, dy) + + assert np.all(vjp == np.zeros([len(tape.trainable_params)])) + + def test_single_expectation_value(self, tol, dev): + """Tests correct output shape and evaluation for a tape + with a single expval output""" + x = 0.543 + y = -0.654 + + with qml.tape.QuantumTape() as tape: + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) + + tape.trainable_params = {0, 1} + dy = np.array([1.0]) + + vjp = get_vjp(dev, tape, dy) + + expected = np.array([-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]) + assert np.allclose(vjp, expected, atol=tol, rtol=0) + + def test_multiple_expectation_values(self, tol, dev): + """Tests correct output shape and evaluation for a tape + with multiple expval outputs""" + x = 0.543 + y = -0.654 + + with qml.tape.QuantumTape() as tape: + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + qml.expval(qml.PauliX(1)) + + tape.trainable_params = {0, 1} + dy = np.array([1.0, 2.0]) + + vjp = get_vjp(dev, tape, dy) + + expected = np.array([-np.sin(x), 2 * np.cos(y)]) + assert np.allclose(vjp, expected, atol=tol, rtol=0) + + def test_prob_expectation_values(self, dev): + """Tests correct output shape and evaluation for a tape + with prob and expval outputs""" + x = 0.543 + y = -0.654 + + with qml.tape.QuantumTape() as tape: + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + qml.probs(wires=[0, 1]) + + tape.trainable_params = {0, 1} + dy = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + + with pytest.raises( + qml.QuantumFunctionError, + match="Adjoint differentiation method does not support", + ): + get_vjp(dev, tape, dy) + + +class TestBatchVectorJacobianProduct: + """Tests for the batch_vjp function""" + + @pytest.fixture(params=[np.complex64, np.complex128]) + def dev(self, request): + return qml.device(device_name, wires=2, c_dtype=request.param) + + def test_one_tape_no_trainable_parameters_1(self, dev): + """A tape with no trainable parameters will simply return None""" + with qml.tape.QuantumTape() as tape1: + qml.RX(0.4, wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + + with qml.tape.QuantumTape() as tape2: + qml.RX(0.4, wires=0) + qml.RX(0.6, wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + + tape1.trainable_params = {} + tape2.trainable_params = {0, 1} + + tapes = [tape1, tape2] + dys = [np.array([1.0]), np.array([1.0])] + + vjps = get_vjp(dev, tapes, dys) + + assert len(vjps[0]) == 0 + assert vjps[1] is not None + + def test_all_tapes_no_trainable_parameters_2(self, dev): + """If all tapes have no trainable parameters all outputs will be None""" + with qml.tape.QuantumTape() as tape1: + qml.RX(0.4, wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + + with qml.tape.QuantumTape() as tape2: + qml.RX(0.4, wires=0) + qml.RX(0.6, wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + + tape1.trainable_params = set() + tape2.trainable_params = set() + + tapes = [tape1, tape2] + dys = [np.array([1.0]), np.array([1.0])] + + vjps = get_vjp(dev, tapes, dys) + + assert len(vjps[0]) == 0 + assert len(vjps[1]) == 0 + + def test_zero_dy(self, dev): + """A zero dy vector will return no tapes and a zero matrix""" + with qml.tape.QuantumTape() as tape1: + qml.RX(0.4, wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + + with qml.tape.QuantumTape() as tape2: + qml.RX(0.4, wires=0) + qml.RX(0.6, wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + + tape1.trainable_params = {0} + tape2.trainable_params = {0, 1} + + tapes = [tape1, tape2] + dys = [np.array([0.0]), np.array([1.0])] + + vjps = get_vjp(dev, tapes, dys) + + assert np.allclose(vjps[0], 0) + + @pytest.mark.skipif(ld._new_API, reason="Old API required") + def test_reduction_append(self, dev): + """Test the 'append' reduction strategy""" + _state = dev._asarray(dev.state) + dev._apply_state_vector(_state, dev.wires) + + with qml.tape.QuantumTape() as tape1: + qml.RX(0.4, wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + + with qml.tape.QuantumTape() as tape2: + qml.RX(0.4, wires=0) + qml.RX(0.6, wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + + tape1.trainable_params = {0} + tape2.trainable_params = {0, 1} + + tapes = [tape1, tape2] + dys = [np.array([1.0]), np.array([1.0])] + + fn = dev.batch_vjp(tapes, dys, reduction="append") + vjps = fn(tapes) + + assert len(vjps) == 2 + assert len(vjps[1]) == 2 + assert isinstance(vjps[0], np.ndarray) + assert isinstance(vjps[1][0], np.ndarray) + assert isinstance(vjps[1][1], np.ndarray) + + @pytest.mark.skipif(ld._new_API, reason="Old API required") + @pytest.mark.parametrize("reduction_keyword", ("extend", list.extend)) + def test_reduction_extend(self, dev, reduction_keyword): + """Test the 'extend' reduction strategy""" + with qml.tape.QuantumTape() as tape1: + qml.RX(0.4, wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + + with qml.tape.QuantumTape() as tape2: + qml.RX(0.4, wires=0) + qml.RX(0.6, wires=0) + qml.CNOT(wires=[0, 1]) + qml.expval(qml.PauliZ(0)) + + tape1.trainable_params = {0} + tape2.trainable_params = {0, 1} + + tapes = [tape1, tape2] + dys = [np.array([1.0]), np.array([1.0])] -# def test_hermitian_tensor_expectation(self, dev, tol): -# obs = np.array([[1, 0], [0, -1]], dtype=dev.C_DTYPE, requires_grad=False) -# dy = np.array([0.8]) - -# fn = dev.vjp([qml.expval(qml.Hermitian(obs, wires=(0,)) @ qml.PauliZ(wires=1))], dy) - -# for x in np.linspace(-2 * math.pi, 2 * math.pi, 7): -# with qml.tape.QuantumTape() as tape: -# qml.RY(x, wires=(0,)) -# assert np.allclose(fn(tape), -0.8 * np.sin(x), atol=tol) - -# @pytest.mark.skipif( -# device_name == device_name, -# reason="Adjoint differentiation does not support State measurements.", -# ) -# def test_statevector_ry(self, dev, tol): -# dy = np.array( -# [ -# [1.0, 0.0, 0.0, 0.0], -# [0.0, 0.0, 1.0, 0.0], -# [0.0, 1.0, 0.0, 0.0], -# [0.0, 0.0, 0.0, 1.0], -# ] -# ) -# fn0 = dev.vjp([qml.state()], dy[0, :]) -# fn1 = dev.vjp([qml.state()], dy[1, :]) -# fn2 = dev.vjp([qml.state()], dy[2, :]) -# fn3 = dev.vjp([qml.state()], dy[3, :]) - -# for x in np.linspace(-2 * math.pi, 2 * math.pi, 7): -# with qml.tape.QuantumTape() as tape: -# qml.RY(x, wires=(0,)) -# assert np.allclose(fn0(tape), -np.sin(x / 2) / 2, atol=tol) -# assert np.allclose(fn1(tape), np.cos(x / 2) / 2, atol=tol) -# assert np.allclose(fn2(tape), 0.0, atol=tol) -# assert np.allclose(fn3(tape), 0.0, atol=tol) - -# @pytest.mark.skipif( -# device_name == device_name, -# reason="Adjoint differentiation does not support State measurements.", -# ) -# def test_wrong_dy_statevector(self, dev): -# """Tests raise an exception when dy is incorrect""" -# x, y, z = [0.5, 0.3, -0.7] - -# with qml.tape.QuantumTape() as tape: -# qml.RX(0.4, wires=[0]) -# qml.Rot(x, y, z, wires=[0]) -# qml.RY(-0.2, wires=[0]) -# qml.state() - -# tape.trainable_params = {1, 2, 3} - -# dy1 = np.ones(3, dtype=dev.C_DTYPE) - -# with pytest.raises( -# ValueError, -# match="Size of the provided vector grad_vec must be the same as the size of", -# ): -# dev.vjp(tape.measurements, dy1) - -# dy2 = np.ones(4, dtype=dev.R_DTYPE) - -# with pytest.warns( -# UserWarning, match="The vjp method only works with complex-valued grad_vec" -# ): -# dev.vjp(tape.measurements, dy2) - -# @pytest.mark.skipif( -# device_name == device_name, -# reason="Adjoint differentiation does not support State measurements.", -# ) -# @pytest.mark.parametrize("stateprep", [qml.QubitStateVector, qml.StatePrep]) -# def test_statevector_complex_circuit(self, stateprep, dev, tol): -# dy = np.array( -# [ -# [1.0, 0.0, 0.0, 0.0], -# [0.0, 0.0, 1.0, 0.0], -# [0.0, 1.0, 0.0, 0.0], -# [0.0, 0.0, 0.0, 1.0], -# ] -# ) -# fn0 = dev.vjp([qml.state()], dy[0, :]) -# fn1 = dev.vjp([qml.state()], dy[1, :]) -# fn2 = dev.vjp([qml.state()], dy[2, :]) -# fn3 = dev.vjp([qml.state()], dy[3, :]) - -# params = [math.pi / 7, 6 * math.pi / 7] - -# with qml.tape.QuantumTape() as tape: -# stateprep(np.array([1.0] * 4) / 2, wires=range(2)) -# qml.RY(params[0], wires=0) -# qml.RZ(params[1], wires=1) -# qml.CZ(wires=[0, 1]) - -# tape.trainable_params = {2} # RZ - -# psi_00_diff = ( -# (math.cos(params[0] / 2) - math.sin(params[0] / 2)) -# * (-math.sin(params[1] / 2) - 1j * math.cos(params[1] / 2)) -# / 4 -# ) -# psi_01_diff = ( -# (math.cos(params[0] / 2) + math.sin(params[0] / 2)) -# * (-math.sin(params[1] / 2) - 1j * math.cos(params[1] / 2)) -# / 4 -# ) -# psi_10_diff = ( -# (math.cos(params[0] / 2) - math.sin(params[0] / 2)) -# * (-math.sin(params[1] / 2) + 1j * math.cos(params[1] / 2)) -# / 4 -# ) -# psi_11_diff = ( -# -(math.cos(params[0] / 2) + math.sin(params[0] / 2)) -# * (-math.sin(params[1] / 2) + 1j * math.cos(params[1] / 2)) -# / 4 -# ) - -# assert np.allclose(fn0(tape), psi_00_diff, atol=tol) -# assert np.allclose(fn1(tape), psi_01_diff, atol=tol) -# assert np.allclose(fn2(tape), psi_10_diff, atol=tol) -# assert np.allclose(fn3(tape), psi_11_diff, atol=tol) - -# def test_no_trainable_parameters(self, dev): -# """A tape with no trainable parameters will simply return None""" -# x = 0.4 - -# with qml.tape.QuantumTape() as tape: -# qml.RX(x, wires=0) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) - -# tape.trainable_params = {} -# dy = np.array([1.0]) - -# fn = dev.vjp(tape.measurements, dy) -# vjp = fn(tape) - -# assert len(vjp) == 0 - -# @pytest.mark.skipif(ld._new_API, reason="Old API required") -# def test_no_trainable_parameters_NEW(self, dev): -# """A tape with no trainable parameters will simply return None""" -# _state = dev._asarray(dev.state) -# dev._apply_state_vector(_state, dev.wires) - -# x = 0.4 - -# with qml.tape.QuantumTape() as tape: -# qml.RX(x, wires=0) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) - -# tape.trainable_params = {} -# dy = np.array([1.0]) -# fn = dev.vjp(tape.measurements, dy) -# vjp = fn(tape) - -# assert len(vjp) == 0 - -# def test_zero_dy(self, dev): -# """A zero dy vector will return no tapes and a zero matrix""" -# x = 0.4 -# y = 0.6 - -# with qml.tape.QuantumTape() as tape: -# qml.RX(x, wires=0) -# qml.RX(y, wires=0) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) - -# tape.trainable_params = {0, 1} -# dy = np.array([0.0]) - -# fn = dev.vjp(tape.measurements, dy) -# vjp = fn(tape) - -# assert np.all(vjp == np.zeros([len(tape.trainable_params)])) - -# def test_single_expectation_value(self, tol, dev): -# """Tests correct output shape and evaluation for a tape -# with a single expval output""" -# x = 0.543 -# y = -0.654 - -# with qml.tape.QuantumTape() as tape: -# qml.RX(x, wires=[0]) -# qml.RY(y, wires=[1]) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) - -# tape.trainable_params = {0, 1} -# dy = np.array([1.0]) - -# fn = dev.vjp(tape.measurements, dy) -# vjp = fn(tape) - -# expected = np.array([-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]) -# assert np.allclose(vjp, expected, atol=tol, rtol=0) - -# def test_multiple_expectation_values(self, tol, dev): -# """Tests correct output shape and evaluation for a tape -# with multiple expval outputs""" -# x = 0.543 -# y = -0.654 - -# with qml.tape.QuantumTape() as tape: -# qml.RX(x, wires=[0]) -# qml.RY(y, wires=[1]) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) -# qml.expval(qml.PauliX(1)) - -# tape.trainable_params = {0, 1} -# dy = np.array([1.0, 2.0]) - -# fn = dev.vjp(tape.measurements, dy) -# vjp = fn(tape) - -# expected = np.array([-np.sin(x), 2 * np.cos(y)]) -# assert np.allclose(vjp, expected, atol=tol, rtol=0) - -# def test_prob_expectation_values(self, dev): -# """Tests correct output shape and evaluation for a tape -# with prob and expval outputs""" -# _state = dev._asarray(dev.state) -# dev._apply_state_vector(_state, dev.wires) - -# x = 0.543 -# y = -0.654 + fn = dev.batch_vjp(tapes, dys, reduction=reduction_keyword) + vjps = fn(tapes) -# with qml.tape.QuantumTape() as tape: -# qml.RX(x, wires=[0]) -# qml.RY(y, wires=[1]) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) -# qml.probs(wires=[0, 1]) - -# tape.trainable_params = {0, 1} -# dy = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - -# with pytest.raises( -# qml.QuantumFunctionError, -# match="Adjoint differentiation method does not support", -# ): -# dev.vjp(tape.measurements, dy)(tape) - - -# @pytest.mark.skipif(ld._new_API, reason="Old API required") -# class TestBatchVectorJacobianProduct: -# """Tests for the batch_vjp function""" - -# @pytest.fixture(params=[np.complex64, np.complex128]) -# def dev(self, request): -# return qml.device(device_name, wires=2, c_dtype=request.param) - -# def test_one_tape_no_trainable_parameters_1(self, dev): -# """A tape with no trainable parameters will simply return None""" -# with qml.tape.QuantumTape() as tape1: -# qml.RX(0.4, wires=0) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) - -# with qml.tape.QuantumTape() as tape2: -# qml.RX(0.4, wires=0) -# qml.RX(0.6, wires=0) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) - -# tape1.trainable_params = {} -# tape2.trainable_params = {0, 1} - -# tapes = [tape1, tape2] -# dys = [np.array([1.0]), np.array([1.0])] - -# fn = dev.batch_vjp(tapes, dys) -# vjps = fn(tapes) - -# assert len(vjps[0]) == 0 -# assert vjps[1] is not None - -# def test_all_tapes_no_trainable_parameters_2(self, dev): -# """If all tapes have no trainable parameters all outputs will be None""" -# with qml.tape.QuantumTape() as tape1: -# qml.RX(0.4, wires=0) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) - -# with qml.tape.QuantumTape() as tape2: -# qml.RX(0.4, wires=0) -# qml.RX(0.6, wires=0) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) - -# tape1.trainable_params = set() -# tape2.trainable_params = set() - -# tapes = [tape1, tape2] -# dys = [np.array([1.0]), np.array([1.0])] - -# fn = dev.batch_vjp(tapes, dys) -# vjps = fn(tapes) - -# assert len(vjps[0]) == 0 -# assert len(vjps[1]) == 0 - -# def test_zero_dy(self, dev): -# """A zero dy vector will return no tapes and a zero matrix""" -# with qml.tape.QuantumTape() as tape1: -# qml.RX(0.4, wires=0) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) - -# with qml.tape.QuantumTape() as tape2: -# qml.RX(0.4, wires=0) -# qml.RX(0.6, wires=0) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) - -# tape1.trainable_params = {0} -# tape2.trainable_params = {0, 1} - -# tapes = [tape1, tape2] -# dys = [np.array([0.0]), np.array([1.0])] - -# fn = dev.batch_vjp(tapes, dys) -# vjps = fn(tapes) - -# assert np.allclose(vjps[0], 0) - -# @pytest.mark.skipif(ld._new_API, reason="Old API required") -# def test_reduction_append(self, dev): -# """Test the 'append' reduction strategy""" -# _state = dev._asarray(dev.state) -# dev._apply_state_vector(_state, dev.wires) - -# with qml.tape.QuantumTape() as tape1: -# qml.RX(0.4, wires=0) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) - -# with qml.tape.QuantumTape() as tape2: -# qml.RX(0.4, wires=0) -# qml.RX(0.6, wires=0) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) - -# tape1.trainable_params = {0} -# tape2.trainable_params = {0, 1} - -# tapes = [tape1, tape2] -# dys = [np.array([1.0]), np.array([1.0])] - -# fn = dev.batch_vjp(tapes, dys, reduction="append") -# vjps = fn(tapes) - -# assert len(vjps) == 2 -# assert len(vjps[1]) == 2 -# assert isinstance(vjps[0], np.ndarray) -# assert isinstance(vjps[1][0], np.ndarray) -# assert isinstance(vjps[1][1], np.ndarray) - -# @pytest.mark.parametrize("reduction_keyword", ("extend", list.extend)) -# def test_reduction_extend(self, dev, reduction_keyword): -# """Test the 'extend' reduction strategy""" -# with qml.tape.QuantumTape() as tape1: -# qml.RX(0.4, wires=0) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) - -# with qml.tape.QuantumTape() as tape2: -# qml.RX(0.4, wires=0) -# qml.RX(0.6, wires=0) -# qml.CNOT(wires=[0, 1]) -# qml.expval(qml.PauliZ(0)) - -# tape1.trainable_params = {0} -# tape2.trainable_params = {0, 1} - -# tapes = [tape1, tape2] -# dys = [np.array([1.0]), np.array([1.0])] - -# fn = dev.batch_vjp(tapes, dys, reduction=reduction_keyword) -# vjps = fn(tapes) - -# assert len(vjps) == sum(len(t.trainable_params) for t in tapes) + assert len(vjps) == sum(len(t.trainable_params) for t in tapes) From 9e7c6ed0b5fcc260c8890c6d100586752e37df77 Mon Sep 17 00:00:00 2001 From: Dev version update bot Date: Fri, 22 Mar 2024 19:01:28 +0000 Subject: [PATCH 08/46] Auto update version --- pennylane_lightning/core/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 4089152989..ec9ead0f50 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.36.0-dev17" +__version__ = "0.36.0-dev18" From e597adabd71cba46a99d8c52227cf308252db81e Mon Sep 17 00:00:00 2001 From: Dev version update bot Date: Tue, 2 Apr 2024 15:25:03 +0000 Subject: [PATCH 09/46] Auto update version --- pennylane_lightning/core/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 58da0aa862..19067082da 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.36.0-dev20" +__version__ = "0.36.0-dev21" From 27b00a6b94e462c9bd490a771ffafbed79de12b6 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Tue, 2 Apr 2024 17:05:35 -0400 Subject: [PATCH 10/46] Updated tests --- .../lightning_qubit/_adjoint_jacobian.py | 3 + tests/new_api/test_device.py | 347 +++++++++++++++++- 2 files changed, 342 insertions(+), 8 deletions(-) diff --git a/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py b/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py index 183d043fcb..76db319a54 100644 --- a/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py +++ b/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py @@ -300,6 +300,9 @@ def calculate_vjp(self, tape: QuantumTape, grad_vec): ) # Proceed, because tape_return_type is Expectation. + if qml.math.ndim(grad_vec) == 0: + grad_vec = (grad_vec,) + if len(grad_vec) != len(measurements): raise ValueError( "Number of observables in the tape must be the same as the " diff --git a/tests/new_api/test_device.py b/tests/new_api/test_device.py index a7fe0ac7d3..7f9e594326 100644 --- a/tests/new_api/test_device.py +++ b/tests/new_api/test_device.py @@ -14,7 +14,7 @@ """ This module contains unit tests for new device API Lightning classes. """ -# pylint: disable=too-many-arguments +# pylint: disable=too-many-arguments, unused-argument import numpy as np import pennylane as qml @@ -425,8 +425,6 @@ def process_and_execute(device, tape, execute_and_derivatives=False, obs_batch=F jac = device.compute_derivatives(tapes, config) return transf_fn(results), jac - # Test supports derivative + xfail tests - @pytest.mark.parametrize( "config, tape, expected", [ @@ -474,7 +472,6 @@ def test_supports_derivatives(self, dev, config, tape, expected, batch_obs): qml.Z(1), 2.5 * qml.Z(0), qml.Z(0) @ qml.X(1), - qml.operation.Tensor(qml.Z(0), qml.X(1)), qml.Z(1) + qml.X(1), qml.Hamiltonian([-1.0, 1.5], [qml.Z(1), qml.X(1)]), qml.Hermitian(qml.Hadamard.compute_matrix(), 0), @@ -518,16 +515,16 @@ def test_derivatives_single_expval( "obs1", [ qml.Z(1), - qml.s_prod(2.5, qml.Y(2)), + 2.5 * qml.Y(2), qml.Hamiltonian([-1.0, 1.5], [qml.Z(1), qml.X(1)]), - qml.operation.Tensor(qml.Z(0), qml.X(1)), + qml.Z(0) @ qml.X(1), ], ) @pytest.mark.parametrize( "obs2", [ - qml.prod(qml.Y(0), qml.X(2)), - qml.sum(qml.Z(1), qml.X(1)), + qml.Y(0) @ qml.X(2), + qml.Z(1) + qml.X(1), qml.SparseHamiltonian( qml.Hamiltonian([-1.0, 1.5], [qml.Z(1), qml.X(1)]).sparse_matrix( wire_order=[0, 1, 2] @@ -729,3 +726,337 @@ def test_derivatives_tape_batch(self, phi, execute_and_derivatives, batch_obs): assert len(jacs[0]) == len(expected_jac[0]) assert np.allclose(jacs[0], expected_jac[0]) assert np.allclose(jacs[1], expected_jac[1]) + + +@pytest.mark.parametrize("batch_obs", [True, False]) +class TestVJP: + """Unit tests for VJP computation with the new device API.""" + + @staticmethod + def calculate_reference(tape, dy, execute_and_derivatives=False): + device = DefaultQubit(max_workers=1) + program, config = device.preprocess(ExecutionConfig(gradient_method="adjoint")) + tapes, transf_fn = program([tape]) + dy = [dy] + + if execute_and_derivatives: + results, jac = device.execute_and_compute_vjp(tapes, dy, config) + else: + results = device.execute(tapes, config) + jac = device.compute_vjp(tapes, dy, config) + return transf_fn(results), jac + + @staticmethod + def process_and_execute(device, tape, dy, execute_and_derivatives=False, obs_batch=False): + program, config = device.preprocess( + ExecutionConfig(gradient_method="adjoint", device_options={"batch_obs": obs_batch}) + ) + tapes, transf_fn = program([tape]) + dy = [dy] + + if execute_and_derivatives: + results, jac = device.execute_and_compute_vjp(tapes, dy, config) + else: + results = device.execute(tapes, config) + jac = device.compute_vjp(tapes, dy, config) + return transf_fn(results), jac + + @pytest.mark.parametrize( + "config, tape, expected", + [ + (None, None, True), + (DefaultExecutionConfig, None, False), + (ExecutionConfig(gradient_method="backprop"), None, False), + ( + ExecutionConfig(gradient_method="backprop"), + QuantumScript([qml.RX(0.123, 0)], [qml.expval(qml.Z(0))]), + False, + ), + (ExecutionConfig(gradient_method="best"), None, True), + (ExecutionConfig(gradient_method="adjoint"), None, True), + ( + ExecutionConfig(gradient_method="adjoint"), + QuantumScript([qml.RX(0.123, 0)], [qml.expval(qml.Z(0))]), + True, + ), + ( + ExecutionConfig(gradient_method="adjoint"), + QuantumScript([qml.RX(0.123, 0)], [qml.var(qml.Z(0))]), + False, + ), + ( + ExecutionConfig(gradient_method="adjoint"), + QuantumScript([qml.RX(0.123, 0)], [qml.state()]), + False, + ), + ( + ExecutionConfig(gradient_method="adjoint"), + QuantumScript([qml.RX(0.123, 0)], [qml.expval(qml.Z(0))], shots=10), + False, + ), + ], + ) + def test_supports_vjp(self, dev, config, tape, expected, batch_obs): + """Test that supports_vjp returns the correct boolean value.""" + assert dev.supports_vjp(config, tape) == expected + + @pytest.mark.usefixtures("use_legacy_and_new_opmath") + @pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) + @pytest.mark.parametrize( + "obs", + [ + qml.Z(1), + 2.5 * qml.Z(0), + qml.Z(0) @ qml.X(1), + qml.Z(1) + qml.X(1), + qml.Hamiltonian([-1.0, 1.5], [qml.Z(1), qml.X(1)]), + qml.Hermitian(qml.Hadamard.compute_matrix(), 0), + qml.Projector([1], 1), + ], + ) + @pytest.mark.parametrize("execute_and_derivatives", [True, False]) + def test_vjp_single_expval(self, theta, phi, dev, obs, execute_and_derivatives, batch_obs): + """Test that the VJP is correct when a tape has a single expectation value""" + if isinstance(obs, qml.ops.LinearCombination) and not qml.operation.active_new_opmath(): + obs = qml.operation.convert_to_legacy_H(obs) + + qs = QuantumScript( + [qml.RX(theta, 0), qml.CNOT([0, 1]), qml.RY(phi, 1)], + [qml.expval(obs)], + trainable_params=[0, 1], + ) + + dy = 1.0 + res, jac = self.process_and_execute( + dev, qs, dy, execute_and_derivatives=execute_and_derivatives, obs_batch=batch_obs + ) + if isinstance(obs, qml.Hamiltonian): + qs = QuantumScript( + qs.operations, + [qml.expval(qml.Hermitian(qml.matrix(obs), wires=obs.wires))], + trainable_params=qs.trainable_params, + ) + expected, expected_jac = self.calculate_reference( + qs, dy, execute_and_derivatives=execute_and_derivatives + ) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + assert len(res) == len(jac) == 1 + assert np.allclose(res, expected, atol=tol, rtol=0) + assert np.allclose(jac, expected_jac, atol=tol, rtol=0) + + @pytest.mark.parametrize("theta, phi, omega", list(zip(THETA, PHI, VARPHI))) + @pytest.mark.parametrize( + "obs1", + [ + qml.Z(1), + 2.5 * qml.Y(2), + qml.Hamiltonian([-1.0, 1.5], [qml.Z(1), qml.X(1)]), + qml.Z(0) @ qml.X(1), + ], + ) + @pytest.mark.parametrize( + "obs2", + [ + qml.Y(0) @ qml.X(2), + qml.Z(1) + qml.X(1), + qml.SparseHamiltonian( + qml.Hamiltonian([-1.0, 1.5], [qml.Z(1), qml.X(1)]).sparse_matrix( + wire_order=[0, 1, 2] + ), + wires=[0, 1, 2], + ), + qml.Projector([1], wires=2), + ], + ) + @pytest.mark.parametrize("execute_and_derivatives", [True, False]) + def test_vjp_multi_expval( + self, theta, phi, omega, dev, obs1, obs2, execute_and_derivatives, batch_obs + ): + """Test that the VJP is correct when a tape has multiple expectation values""" + if isinstance(obs1, qml.ops.LinearCombination) and not qml.operation.active_new_opmath(): + obs1 = qml.operation.convert_to_legacy_H(obs1) + if isinstance(obs2, qml.ops.LinearCombination) and not qml.operation.active_new_opmath(): + obs2 = qml.operation.convert_to_legacy_H(obs2) + + qs = QuantumScript( + [ + qml.RX(theta, 0), + qml.CNOT([0, 1]), + qml.RY(phi, 1), + qml.CNOT([1, 2]), + qml.RZ(omega, 2), + ], + [qml.expval(obs1), qml.expval(obs2)], + trainable_params=[0, 1, 2], + ) + dy = (1.0, 2.0) + + res, jac = self.process_and_execute( + dev, qs, dy, execute_and_derivatives=execute_and_derivatives, obs_batch=batch_obs + ) + if isinstance(obs1, qml.Hamiltonian): + qs = QuantumScript( + qs.operations, + [qml.expval(qml.Hermitian(qml.matrix(obs1), wires=obs1.wires)), qml.expval(obs2)], + trainable_params=qs.trainable_params, + ) + expected, expected_jac = self.calculate_reference( + qs, dy, execute_and_derivatives=execute_and_derivatives + ) + res = res[0] + jac = jac[0] + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + assert len(res) == 2 + assert len(jac) == 3 + assert np.allclose(res, expected, atol=tol, rtol=0) + assert np.allclose(jac, expected_jac, atol=tol, rtol=0) + + @pytest.mark.parametrize("execute_and_derivatives", [True, False]) + def test_vjp_no_trainable_params(self, dev, execute_and_derivatives, batch_obs): + """Test that the VJP is empty with there are no trainable parameters.""" + qs = QuantumScript( + [qml.Hadamard(0), qml.CNOT([0, 1]), qml.S(1), qml.T(1)], [qml.expval(qml.Z(1))] + ) + dy = 1.0 + + res, jac = self.process_and_execute( + dev, qs, dy, execute_and_derivatives=execute_and_derivatives, obs_batch=batch_obs + ) + expected, _ = self.calculate_reference( + qs, dy, execute_and_derivatives=execute_and_derivatives + ) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + assert np.allclose(res, expected, atol=tol, rtol=0) + assert len(jac) == 1 + assert qml.math.shape(jac[0]) == (0,) + + @pytest.mark.parametrize("execute_and_derivatives", [True, False]) + @pytest.mark.parametrize( + "state_prep, params, wires", + [ + (qml.BasisState, [1, 1], [0, 1]), + (qml.StatePrep, [0.0, 0.0, 0.0, 1.0], [0, 1]), + (qml.StatePrep, qml.numpy.array([0.0, 1.0]), [1]), + ], + ) + @pytest.mark.parametrize( + "trainable_params", + [(0, 1, 2), (1, 2)], + ) + def test_state_prep_ops( + self, dev, state_prep, params, wires, execute_and_derivatives, batch_obs, trainable_params + ): + """Test that a circuit containing state prep operations is differentiated correctly.""" + qs = QuantumScript( + [state_prep(params, wires), qml.RX(1.23, 0), qml.CNOT([0, 1]), qml.RX(4.56, 1)], + [qml.expval(qml.PauliZ(1))], + ) + dy = [1.0] + + config = ExecutionConfig(gradient_method="adjoint", device_options={"batch_obs": batch_obs}) + program, new_config = dev.preprocess(config) + tapes, fn = program([qs]) + tapes[0].trainable_params = trainable_params + if execute_and_derivatives: + res, jac = dev.execute_and_compute_vjp(tapes, dy, new_config) + res = fn(res) + else: + res, jac = ( + fn(dev.execute(tapes, new_config)), + dev.compute_vjp(tapes, dy, new_config), + ) + + dev_ref = DefaultQubit(max_workers=1) + config = ExecutionConfig(gradient_method="adjoint") + program, new_config = dev_ref.preprocess(config) + tapes, fn = program([qs]) + tapes[0].trainable_params = trainable_params + if execute_and_derivatives: + expected, expected_jac = dev_ref.execute_and_compute_vjp(tapes, dy, new_config) + expected = fn(expected) + else: + expected, expected_jac = ( + fn(dev_ref.execute(tapes, new_config)), + dev_ref.compute_vjp(tapes, dy, new_config), + ) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + assert np.allclose(res, expected, atol=tol, rtol=0) + assert np.allclose(jac, expected_jac, atol=tol, rtol=0) + + def test_state_vjp_not_supported(self, dev, batch_obs): + """Test that an error is raised if VJP are requested for state measurement""" + qs = QuantumScript([qml.RX(1.23, 0)], [qml.state()], trainable_params=[0]) + config = ExecutionConfig(gradient_method="adjoint", device_options={"batch_obs": batch_obs}) + dy = 1.0 + + with pytest.raises( + qml.QuantumFunctionError, + match="Adjoint differentiation does not support State measurements", + ): + _ = dev.compute_vjp(qs, dy, config) + + with pytest.raises( + qml.QuantumFunctionError, + match="Adjoint differentiation does not support State measurements", + ): + _ = dev.execute_and_compute_vjp(qs, dy, config) + + @pytest.mark.parametrize("phi", PHI) + @pytest.mark.parametrize("execute_and_derivatives", [True, False]) + def test_vjp_tape_batch(self, phi, execute_and_derivatives, batch_obs): + """Test that results are correct when we execute and compute derivatives for a batch of + tapes.""" + device = LightningDevice(wires=4, batch_obs=batch_obs) + + ops = [ + qml.X(0), + qml.X(1), + qml.ctrl(qml.RX(phi, 2), (0, 1, 3), control_values=[1, 1, 0]), + ] + + qs1 = QuantumScript( + ops, + [ + qml.expval(qml.sum(qml.Y(2), qml.Z(1))), + qml.expval(qml.s_prod(3, qml.Z(2))), + ], + trainable_params=[0], + ) + + ops = [qml.Hadamard(0), qml.IsingXX(phi, wires=(0, 1))] + qs2 = QuantumScript(ops, [qml.expval(qml.prod(qml.Z(0), qml.Z(1)))], trainable_params=[0]) + dy = [(1.5, 2.5), 1.0] + + if execute_and_derivatives: + results, jacs = device.execute_and_compute_vjp((qs1, qs2), dy) + else: + results = device.execute((qs1, qs2)) + jacs = device.compute_vjp((qs1, qs2), dy) + + # Assert results + expected1 = (-np.sin(phi) - 1, 3 * np.cos(phi)) + x1 = np.cos(phi / 2) ** 2 / 2 + x2 = np.sin(phi / 2) ** 2 / 2 + expected2 = sum([x1, -x2, -x1, x2]) # zero + expected = (expected1, expected2) + + assert len(results) == len(expected) + assert len(results[0]) == len(expected[0]) + assert np.allclose(results[0][0], expected[0][0]) + assert np.allclose(results[0][1], expected[0][1]) + assert np.allclose(results[1], expected[1]) + + # Assert derivatives + expected_jac1 = -1.5 * np.cos(phi) - 2.5 * 3 * np.sin(phi) + x1_jac = -np.cos(phi / 2) * np.sin(phi / 2) / 2 + x2_jac = np.sin(phi / 2) * np.cos(phi / 2) / 2 + expected_jac2 = sum([x1_jac, -x2_jac, -x1_jac, x2_jac]) # zero + expected_jac = (expected_jac1, expected_jac2) + + assert len(jacs) == len(expected_jac) == 2 + assert np.allclose(jacs[0], expected_jac[0]) + assert np.allclose(jacs[1], expected_jac[1]) From 47fa871b955505ad6a57e280d0db27439e0ec515 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Tue, 2 Apr 2024 19:49:58 -0400 Subject: [PATCH 11/46] Updated serialization --- pennylane_lightning/core/_serialize.py | 28 +-- tests/test_serialize.py | 328 ++++++++++++------------- 2 files changed, 172 insertions(+), 184 deletions(-) diff --git a/pennylane_lightning/core/_serialize.py b/pennylane_lightning/core/_serialize.py index ae64574b61..29a28929c9 100644 --- a/pennylane_lightning/core/_serialize.py +++ b/pennylane_lightning/core/_serialize.py @@ -21,7 +21,6 @@ BasisState, DeviceError, Hadamard, - Hamiltonian, Identity, PauliX, PauliY, @@ -34,7 +33,7 @@ ) from pennylane.math import unwrap from pennylane.operation import Tensor -from pennylane.ops import Prod, SProd, Sum +from pennylane.ops import Prod, SProd, Sum, Hamiltonian, LinearCombination from pennylane.tape import QuantumTape pauli_name_map = { @@ -258,9 +257,8 @@ def _pauli_sentence(self, observable, wires_map: dict = None): terms = [self._pauli_word(pw, wires_map) for pw in pwords] coeffs = np.array(coeffs).astype(self.rtype) - # TODO: Add this - # if len(terms) == 1 and coeffs[0] == 1.0: - # return terms[0] + if len(terms) == 1 and coeffs[0] == 1.0: + return terms[0] if self.split_obs: return [self.hamiltonian_obs([c], [t]) for (c, t) in zip(coeffs, terms)] @@ -269,22 +267,22 @@ def _pauli_sentence(self, observable, wires_map: dict = None): # pylint: disable=protected-access, too-many-return-statements def _ob(self, observable, wires_map: dict = None): """Serialize a :class:`pennylane.operation.Observable` into an Observable.""" - if isinstance(observable, (Prod, Sum, SProd)) and observable.pauli_rep is not None: + if isinstance(observable, (PauliX, PauliY, PauliZ, Identity, Hadamard)): + return self._named_obs(observable, wires_map) + if isinstance(observable, Hamiltonian): + return self._hamiltonian(observable, wires_map) + if observable.pauli_rep is not None: return self._pauli_sentence(observable.pauli_rep, wires_map) if isinstance(observable, Tensor) or ( isinstance(observable, Prod) and not observable.has_overlapping_wires ): return self._tensor_ob(observable, wires_map) - if observable.name in ("Hamiltonian", "LinearCombination"): + if isinstance(observable, (LinearCombination, Prod, SProd, Sum)): + # if isinstance(observable, (Sum, Prod, SProd)): + # observable = observable.simplify() return self._hamiltonian(observable, wires_map) if observable.name == "SparseHamiltonian": return self._sparse_hamiltonian(observable, wires_map) - if isinstance(observable, (PauliX, PauliY, PauliZ, Identity, Hadamard)): - return self._named_obs(observable, wires_map) - if observable.pauli_rep is not None: - return self._pauli_sentence(observable.pauli_rep, wires_map) - # if isinstance(observable, (Prod, Sum)): - # return self._hamiltonian(observable, wires_map) return self._hermitian_ob(observable, wires_map) def serialize_observables(self, tape: QuantumTape, wires_map: dict = None) -> List: @@ -313,7 +311,9 @@ def serialize_observables(self, tape: QuantumTape, wires_map: dict = None) -> Li offset_indices.append(offset_indices[-1] + 1) return serialized_obs, offset_indices - def serialize_ops(self, tape: QuantumTape, wires_map: dict = None) -> Tuple[ + def serialize_ops( + self, tape: QuantumTape, wires_map: dict = None + ) -> Tuple[ List[List[str]], List[np.ndarray], List[List[int]], diff --git a/tests/test_serialize.py b/tests/test_serialize.py index efedf4969f..13e877b3f3 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -77,15 +77,20 @@ def test_wrong_device_name(): "obs,obs_type", [ (qml.PauliZ(0), NamedObsC128), - ( - qml.PauliZ(0) @ qml.PauliZ(1), - HamiltonianC128 if qml.operation.active_new_opmath() else TensorProdObsC128, - ), + (qml.PauliZ(0) @ qml.PauliZ(1), TensorProdObsC128), (qml.Hadamard(0), NamedObsC128), (qml.Hermitian(np.eye(2), wires=0), HermitianObsC128), ( qml.PauliZ(0) @ qml.Hadamard(1) @ (0.1 * (qml.PauliZ(2) + qml.PauliX(3))), - TensorProdObsC128 if qml.operation.active_new_opmath() else HamiltonianC128, + TensorProdObsC128, + ), + ( + qml.PauliZ(0) @ qml.PauliY(1) @ qml.PauliX(2), + TensorProdObsC128, + ), + ( + qml.PauliZ(0) @ qml.PauliY(1) @ (0.1 * (qml.PauliZ(2) + qml.PauliX(3))), + HamiltonianC128, ), ( ( @@ -95,25 +100,37 @@ def test_wrong_device_name(): ), TensorProdObsC128, ), + # The following case is disabled due to recursion errors. This needs to be investigated before the + # PR is merged. Simplifying this operator doesn't change anything. Maybe we raise an error in this + # case, or fall back to Hermitian, though I doubt the matrix will be hermitian. + pytest.param( + ( + qml.Hermitian(np.eye(2), wires=0) + @ qml.Hermitian(np.eye(2), wires=1) + @ qml.Projector([0], wires=1) + ), + HamiltonianC128, + marks=pytest.mark.xfail(reason="prods with overlapping wires unsupported"), + ), ( qml.PauliZ(0) @ qml.Hermitian(np.eye(2), wires=1) @ qml.Projector([0], wires=2), TensorProdObsC128, ), (qml.Projector([0], wires=0), HermitianObsC128), - (qml.Hamiltonian([1], [qml.PauliZ(0)]), HamiltonianC128), - (qml.sum(qml.Hadamard(0), qml.PauliX(1)), HermitianObsC128), + (qml.Hamiltonian([1], [qml.PauliZ(0)]), NamedObsC128), + (qml.sum(qml.Hadamard(0), qml.PauliX(1)), HamiltonianC128), ( qml.SparseHamiltonian(qml.Hamiltonian([1], [qml.PauliZ(0)]).sparse_matrix(), wires=[0]), SparseHamiltonianC128, ), + (2.5 * qml.PauliZ(0), HamiltonianC128), ], ) def test_obs_returns_expected_type(obs, obs_type): """Tests that observables get serialized to the expected type, with and without wires map""" - assert isinstance( - QuantumScriptSerializer(device_name)._ob(obs, dict(enumerate(obs.wires))), obs_type - ) - assert isinstance(QuantumScriptSerializer(device_name)._ob(obs), obs_type) + serializer = QuantumScriptSerializer(device_name) + assert isinstance(serializer._ob(obs, dict(enumerate(obs.wires))), obs_type) + assert isinstance(serializer._ob(obs), obs_type) @pytest.mark.usefixtures("use_legacy_and_new_opmath") @@ -133,12 +150,7 @@ def test_tensor_non_tensor_return(self, use_csingle, wires_map): named_obs = NamedObsC64 if use_csingle else NamedObsC128 tensor_prod_obs = TensorProdObsC64 if use_csingle else TensorProdObsC128 - first_s = tensor_prod_obs([named_obs("PauliZ", [0]), named_obs("PauliX", [1])]) - - if qml.operation.active_new_opmath(): - ham_obs = HamiltonianC64 if use_csingle else HamiltonianC128 - tensor_obs = tensor_prod_obs([named_obs("PauliX", [1]), named_obs("PauliZ", [0])]) - first_s = ham_obs([1.0], [tensor_obs]) + first_s = tensor_prod_obs([named_obs("PauliX", [1]), named_obs("PauliZ", [0])]) s_expected = [ first_s, @@ -150,6 +162,7 @@ def test_tensor_non_tensor_return(self, use_csingle, wires_map): ) assert s == s_expected + @pytest.mark.xfail(reason="Prod with overlapping wires not supported") @pytest.mark.parametrize("use_csingle", [True, False]) @pytest.mark.parametrize("wires_map", [wires_dict, None]) def test_prod_return_with_overlapping_wires(self, use_csingle, wires_map): @@ -274,14 +287,7 @@ def test_hamiltonian_return(self, use_csingle, wires_map): named_obs("PauliY", [2]), ] ), - ( - hamiltonian_obs( - np.array([1], dtype=r_dtype), - [tensor_prod_obs([named_obs("PauliY", [2]), named_obs("PauliX", [0])])], - ) - if qml.operation.active_new_opmath() - else tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]) - ), + tensor_prod_obs([named_obs("PauliY", [2]), named_obs("PauliX", [0])]), hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), ], ) @@ -318,67 +324,46 @@ def test_hamiltonian_tensor_return(self, use_csingle, wires_map): # Expression (ham @ obs) is converted internally by Pennylane # where obs is appended to each term of the ham if qml.operation.active_new_opmath(): - s_expected = hamiltonian_obs( - np.array([0.3, 0.5, 0.4], dtype=r_dtype), + first_term = tensor_prod_obs( [ tensor_prod_obs( [ - tensor_prod_obs( - [ - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), - named_obs("PauliY", [2]), - ] - ), - named_obs("PauliZ", [3]), - ] - ), - hamiltonian_obs( - np.array([1], dtype=r_dtype), - [ - tensor_prod_obs( - [ - named_obs("PauliY", [2]), - named_obs("PauliX", [0]), - named_obs("PauliZ", [3]), - ] - ) - ], - ), - tensor_prod_obs( - [ - hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), - named_obs("PauliZ", [3]), + hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), + named_obs("PauliY", [2]), ] ), - ], + named_obs("PauliZ", [3]), + ] ) else: - s_expected = hamiltonian_obs( - np.array([0.3, 0.5, 0.4], dtype=r_dtype), + first_term = tensor_prod_obs( [ - tensor_prod_obs( - [ - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), - named_obs("PauliY", [2]), - named_obs("PauliZ", [3]), - ] - ), - tensor_prod_obs( - [ - named_obs("PauliX", [0]), - named_obs("PauliY", [2]), - named_obs("PauliZ", [3]), - ] - ), - tensor_prod_obs( - [ - hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), - named_obs("PauliZ", [3]), - ] - ), - ], + hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), + named_obs("PauliY", [2]), + named_obs("PauliZ", [3]), + ] ) + s_expected = hamiltonian_obs( + np.array([0.3, 0.5, 0.4], dtype=r_dtype), + [ + first_term, + tensor_prod_obs( + [ + named_obs("PauliY", [2]), + named_obs("PauliX", [0]), + named_obs("PauliZ", [3]), + ] + ), + tensor_prod_obs( + [ + hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), + named_obs("PauliZ", [3]), + ] + ), + ], + ) + assert s[0] == s_expected @pytest.mark.parametrize("use_csingle", [True, False]) @@ -413,117 +398,120 @@ def test_hamiltonian_mix_return(self, use_csingle, wires_map): s, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( tape, wires_map ) - if qml.operation.active_new_opmath(): - s_expected1 = hamiltonian_obs( - np.array([0.3, 0.5, 0.4], dtype=r_dtype), - [ - tensor_prod_obs( - [ - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), - named_obs("PauliY", [2]), - ] - ), - ( - hamiltonian_obs( - np.array([1], dtype=r_dtype), - [tensor_prod_obs([named_obs("PauliY", [2]), named_obs("PauliX", [0])])], - ) - if qml.operation.active_new_opmath() - else tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]) - ), - hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), - ], - ) - s_expected2 = hamiltonian_obs( - np.array([0.7, 0.3], dtype=r_dtype), - [ - tensor_prod_obs( - [ - named_obs("PauliX", [0]), - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [1, 2]), - ] - ), - ( - hamiltonian_obs( - np.array([1], dtype=r_dtype), - [tensor_prod_obs([named_obs("PauliX", [2]), named_obs("PauliY", [0])])], - ) - if qml.operation.active_new_opmath() - else tensor_prod_obs([named_obs("PauliY", [0]), named_obs("PauliX", [2])]) - ), - ], - ) - else: - s_expected1 = hamiltonian_obs( - np.array([0.3, 0.5, 0.4], dtype=r_dtype), - [ - tensor_prod_obs( - [ - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), - named_obs("PauliY", [2]), - ] - ), - tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]), - hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), - ], - ) - s_expected2 = hamiltonian_obs( - np.array([0.7, 0.3], dtype=r_dtype), - [ - tensor_prod_obs( - [ - named_obs("PauliX", [0]), - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [1, 2]), - ] - ), - tensor_prod_obs([named_obs("PauliY", [0]), named_obs("PauliX", [2])]), - ], - ) + s_expected1 = hamiltonian_obs( + np.array([0.3, 0.5, 0.4], dtype=r_dtype), + [ + tensor_prod_obs( + [ + hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), + named_obs("PauliY", [2]), + ] + ), + tensor_prod_obs([named_obs("PauliY", [2]), named_obs("PauliX", [0])]), + hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), + ], + ) + s_expected2 = hamiltonian_obs( + np.array([0.7, 0.3], dtype=r_dtype), + [ + tensor_prod_obs( + [ + named_obs("PauliX", [0]), + hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [1, 2]), + ] + ), + tensor_prod_obs([named_obs("PauliX", [2]), named_obs("PauliY", [0])]), + ], + ) assert s[0] == s_expected1 assert s[1] == s_expected2 - @pytest.mark.parametrize( - "obs,coeffs,terms", - [ - (qml.prod(qml.PauliZ(0), qml.PauliX(1)), [1], [[("PauliX", 1), ("PauliZ", 0)]]), - (qml.s_prod(0.1, qml.PauliX(0)), [0.1], ("PauliX", 0)), - ( - qml.sum( - 0.5 * qml.prod(qml.PauliX(0), qml.PauliZ(1)), - 0.1 * qml.prod(qml.PauliZ(0), qml.PauliY(1)), - ), - [0.5, 0.1], - [[("PauliZ", 1), ("PauliX", 0)], [("PauliY", 1), ("PauliZ", 0)]], - ), - ], - ) @pytest.mark.parametrize("use_csingle", [True, False]) @pytest.mark.parametrize("wires_map", [wires_dict, None]) - def test_op_arithmetic_uses_hamiltonian(self, use_csingle, obs, coeffs, terms, wires_map): - """Tests that an arithmetic obs with a PauliRep serializes as a Hamiltonian.""" - tape = qml.tape.QuantumTape(measurements=[qml.expval(obs)]) + def test_sprod(self, use_csingle, wires_map): + """Test that SProds are serialized correctly""" + tape = qml.tape.QuantumScript([], [qml.expval(qml.s_prod(0.1, qml.PauliX(0)))]) + + hamiltonian_obs = HamiltonianC64 if use_csingle else HamiltonianC128 + named_obs = NamedObsC64 if use_csingle else NamedObsC128 + rtype = np.float32 if use_csingle else np.float64 + + res, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( + tape, wires_map + ) + assert len(res) == 1 + assert isinstance(res[0], hamiltonian_obs) + + coeffs = np.array([0.1]).astype(rtype) + s_expected = hamiltonian_obs(coeffs, [named_obs("PauliX", [0])]) + assert res[0] == s_expected + + @pytest.mark.parametrize("use_csingle", [True, False]) + @pytest.mark.parametrize("wires_map", [wires_dict, None]) + def test_prod(self, use_csingle, wires_map): + """Test that Prods are serialized correctly""" + tape = qml.tape.QuantumScript( + [], [qml.expval(qml.prod(qml.PauliZ(0), qml.PauliX(1)) @ qml.Hadamard(2))] + ) + + named_obs = NamedObsC64 if use_csingle else NamedObsC128 + tensor_obs = TensorProdObsC64 if use_csingle else TensorProdObsC128 + res, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( tape, wires_map ) assert len(res) == 1 - assert isinstance(res[0], HamiltonianC64 if use_csingle else HamiltonianC128) + assert isinstance(res[0], tensor_obs) + + s_expected = tensor_obs( + [ + tensor_obs([named_obs("PauliX", [1]), named_obs("PauliZ", [0])]), + named_obs("Hadamard", [2]), + ] + ) + assert res[0] == s_expected + + @pytest.mark.parametrize("use_csingle", [True, False]) + @pytest.mark.parametrize("wires_map", [wires_dict, None]) + def test_sum(self, use_csingle, wires_map): + """Test that Sums are serialized correctly""" + tape = qml.tape.QuantumScript( + [], + [ + qml.expval( + qml.sum( + 0.5 * qml.prod(qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)), + 0.1 * qml.prod(qml.PauliZ(0), qml.Hadamard(2), qml.PauliY(1)), + ) + ) + ], + ) hamiltonian_obs = HamiltonianC64 if use_csingle else HamiltonianC128 tensor_obs = TensorProdObsC64 if use_csingle else TensorProdObsC128 named_obs = NamedObsC64 if use_csingle else NamedObsC128 rtype = np.float32 if use_csingle else np.float64 - term_shape = np.array(terms).shape - if len(term_shape) == 1: # just a single pauli op - expected_terms = [named_obs(terms[0], [terms[1]])] - elif len(term_shape) == 3: # list of tensor products - expected_terms = [ - tensor_obs([named_obs(pauli, [wire]) for pauli, wire in term]) for term in terms - ] + res, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( + tape, wires_map + ) + assert len(res) == 1 + assert isinstance(res[0], hamiltonian_obs) - coeffs = np.array(coeffs).astype(rtype) - assert res[0] == hamiltonian_obs(coeffs, expected_terms) + coeffs = np.array([0.5, 0.1]).astype(rtype) + s_expected = hamiltonian_obs( + coeffs, + [ + tensor_obs( + [named_obs("PauliZ", [1]), named_obs("PauliX", [0]), named_obs("PauliX", [2])] + ), + tensor_obs( + [named_obs("PauliZ", [0]), named_obs("PauliY", [1]), named_obs("Hadamard", [2])] + ), + ], + ) + assert res[0] == s_expected @pytest.mark.parametrize("use_csingle", [True, False]) @pytest.mark.parametrize("wires_map", [wires_dict, None]) From 3fbeea11ff4dc46a8a05b13072b9d2a295d938ab Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Tue, 2 Apr 2024 19:54:38 -0400 Subject: [PATCH 12/46] Unpinned cmake --- .github/workflows/tests_gpu_cuda.yml | 8 ++------ .github/workflows/tests_linux_x86_mpi_gpu.yml | 4 +--- .github/workflows/wheel_macos_x86_64.yml | 4 +--- .github/workflows/wheel_noarch.yml | 4 +--- .github/workflows/wheel_win_x86_64.yml | 4 +--- requirements-dev.txt | 2 +- 6 files changed, 7 insertions(+), 19 deletions(-) diff --git a/.github/workflows/tests_gpu_cuda.yml b/.github/workflows/tests_gpu_cuda.yml index d3abac6937..06741a1152 100644 --- a/.github/workflows/tests_gpu_cuda.yml +++ b/.github/workflows/tests_gpu_cuda.yml @@ -119,9 +119,7 @@ jobs: - name: Install required packages run: | - # Omitting the installation of cmake v3.29.0 due to - # https://github.com/scikit-build/cmake-python-distributions/pull/474 - python -m pip install ninja "cmake!=3.29.0" custatevec-cu${{ matrix.cuda_version }} + python -m pip install ninja cmake custatevec-cu${{ matrix.cuda_version }} sudo apt-get -y -q install liblapack-dev - name: Build and run unit tests @@ -243,9 +241,7 @@ jobs: run: | cd main python -m pip install -r requirements-dev.txt - # Omitting the installation of cmake v3.29.0 due to - # https://github.com/scikit-build/cmake-python-distributions/pull/474 - python -m pip install "cmake!=3.29.0" custatevec-cu${{ matrix.cuda_version }} openfermionpyscf + python -m pip install cmake custatevec-cu${{ matrix.cuda_version }} openfermionpyscf - name: Checkout PennyLane for release build if: inputs.pennylane-version == 'release' diff --git a/.github/workflows/tests_linux_x86_mpi_gpu.yml b/.github/workflows/tests_linux_x86_mpi_gpu.yml index b491363bfc..da8745d132 100644 --- a/.github/workflows/tests_linux_x86_mpi_gpu.yml +++ b/.github/workflows/tests_linux_x86_mpi_gpu.yml @@ -94,9 +94,7 @@ jobs: - name: Install required packages run: | python -m pip install -r requirements-dev.txt - # Omitting the installation of cmake v3.29.0 due to - # https://github.com/scikit-build/cmake-python-distributions/pull/474 - python -m pip install "cmake!=3.29.0" custatevec-cu12 + python -m pip install cmake custatevec-cu12 sudo apt-get -y -q install liblapack-dev - name: Validate GPU version and installed compiler and modules diff --git a/.github/workflows/wheel_macos_x86_64.yml b/.github/workflows/wheel_macos_x86_64.yml index e3931dbcff..7bcf05a793 100644 --- a/.github/workflows/wheel_macos_x86_64.yml +++ b/.github/workflows/wheel_macos_x86_64.yml @@ -68,9 +68,7 @@ jobs: run: | mkdir -p ${{ github.workspace}}/Kokkos_install/${{ matrix.exec_model }} cd kokkos - # Omitting the installation of cmake v3.29.0 due to - # https://github.com/scikit-build/cmake-python-distributions/pull/474 - python -m pip install "cmake!=3.29.0" ninja + python -m pip install cmake ninja cmake -BBuild . -DCMAKE_INSTALL_PREFIX=${{ github.workspace}}/Kokkos_install/${{ matrix.exec_model }} \ -DKokkos_ENABLE_COMPLEX_ALIGN=OFF \ diff --git a/.github/workflows/wheel_noarch.yml b/.github/workflows/wheel_noarch.yml index 5de9cf6c5b..ed4bfbdff4 100644 --- a/.github/workflows/wheel_noarch.yml +++ b/.github/workflows/wheel_noarch.yml @@ -44,9 +44,7 @@ jobs: - name: Install CMake and ninja run: | - # Omitting the installation of cmake v3.29.0 due to - # https://github.com/scikit-build/cmake-python-distributions/pull/474 - python -m pip install --upgrade "cmake!=3.29.0" ninja + python -m pip install --upgrade cmake ninja - name: Build wheels if: ${{ matrix.pl_backend == 'lightning_qubit'}} diff --git a/.github/workflows/wheel_win_x86_64.yml b/.github/workflows/wheel_win_x86_64.yml index 3d7e86f0e5..88a9feba10 100644 --- a/.github/workflows/wheel_win_x86_64.yml +++ b/.github/workflows/wheel_win_x86_64.yml @@ -64,9 +64,7 @@ jobs: - name: Install dependencies if: steps.kokkos-cache.outputs.cache-hit != 'true' run: | - # Omitting the installation of cmake v3.29.0 due to - # https://github.com/scikit-build/cmake-python-distributions/pull/474 - python -m pip install "cmake!=3.29.0" build + python -m pip install cmake build - name: Build Kokkos core library if: steps.kokkos-cache.outputs.cache-hit != 'true' diff --git a/requirements-dev.txt b/requirements-dev.txt index 474ef50d1f..02aa0c47f9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,6 +14,6 @@ clang-tidy~=16.0 clang-format~=16.0 isort==5.13.2 click==8.0.4 -cmake==3.28.4 +cmake custatevec-cu12 pylint From 424fb83d4f82e276622d532a3f4f2aa832a51a85 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Tue, 2 Apr 2024 19:56:08 -0400 Subject: [PATCH 13/46] Update changelog --- .github/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index b65b64aa5d..72e735ae3b 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -46,6 +46,9 @@ * Add `isort` to `requirements-dev.txt` and run before `black` upon `make format` to sort Python imports. [(#623)](https://github.com/PennyLaneAI/pennylane-lightning/pull/623) +* Improve support for new operator arithmetic with `QuantumScriptSerializer.serialize_observables`. + [(#)]() + ### Documentation ### Bug fixes From 17c9b0f15a50c80c24b12e1ffa43618a9d34ea4d Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Tue, 2 Apr 2024 20:03:59 -0400 Subject: [PATCH 14/46] Update .github/CHANGELOG.md --- .github/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 72e735ae3b..840fcf99fa 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -47,7 +47,7 @@ [(#623)](https://github.com/PennyLaneAI/pennylane-lightning/pull/623) * Improve support for new operator arithmetic with `QuantumScriptSerializer.serialize_observables`. - [(#)]() + [(#670)](https://github.com/PennyLaneAI/pennylane-lightning/pull/670) ### Documentation From 2bd881f0eebbd68f28700ac55fdfa95ca96721af Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Wed, 3 Apr 2024 10:43:42 -0400 Subject: [PATCH 15/46] isort --- pennylane_lightning/core/_serialize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_lightning/core/_serialize.py b/pennylane_lightning/core/_serialize.py index 29a28929c9..1e071e8255 100644 --- a/pennylane_lightning/core/_serialize.py +++ b/pennylane_lightning/core/_serialize.py @@ -33,7 +33,7 @@ ) from pennylane.math import unwrap from pennylane.operation import Tensor -from pennylane.ops import Prod, SProd, Sum, Hamiltonian, LinearCombination +from pennylane.ops import Hamiltonian, LinearCombination, Prod, SProd, Sum from pennylane.tape import QuantumTape pauli_name_map = { From 08bea1cf83988b64991b8379338cc9df1c2fc550 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Wed, 3 Apr 2024 10:48:29 -0400 Subject: [PATCH 16/46] black --- pennylane_lightning/core/_serialize.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pennylane_lightning/core/_serialize.py b/pennylane_lightning/core/_serialize.py index 1e071e8255..c6ad4156c7 100644 --- a/pennylane_lightning/core/_serialize.py +++ b/pennylane_lightning/core/_serialize.py @@ -311,9 +311,7 @@ def serialize_observables(self, tape: QuantumTape, wires_map: dict = None) -> Li offset_indices.append(offset_indices[-1] + 1) return serialized_obs, offset_indices - def serialize_ops( - self, tape: QuantumTape, wires_map: dict = None - ) -> Tuple[ + def serialize_ops(self, tape: QuantumTape, wires_map: dict = None) -> Tuple[ List[List[str]], List[np.ndarray], List[List[int]], From bed54041077c87ce64f95b4ade97790ef985a527 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Wed, 3 Apr 2024 14:48:06 -0400 Subject: [PATCH 17/46] Run CI with updated PL branch --- requirements-dev.txt | 2 +- tests/test_adjoint_jacobian.py | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 02aa0c47f9..de03965307 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ pip~=22.0 -git+https://github.com/PennyLaneAI/pennylane.git@master +git+https://github.com/PennyLaneAI/pennylane.git@tf-device-float32 ninja flaky pybind11 diff --git a/tests/test_adjoint_jacobian.py b/tests/test_adjoint_jacobian.py index 5e246b9f10..1b9fdb2e67 100644 --- a/tests/test_adjoint_jacobian.py +++ b/tests/test_adjoint_jacobian.py @@ -1008,14 +1008,8 @@ def f(params1, params2): qml.RY(tf.cos(params2), wires=[0]) return qml.expval(qml.PauliZ(0)) - if dev._new_API: - # tf expects float32 with new device API - tf_r_dtype = tf.float32 - h = 2e-3 - tol = 1e-3 - else: - tf_r_dtype = tf.float32 if dev.dtype == np.complex64 else tf.float64 - tol, h = get_tolerance_and_stepsize(dev, step_size=True) + tf_r_dtype = tf.float32 if dev.dtype == np.complex64 else tf.float64 + tol, h = get_tolerance_and_stepsize(dev, step_size=True) params1 = tf.Variable(0.3, dtype=tf_r_dtype) params2 = tf.Variable(0.4, dtype=tf_r_dtype) From 858dc24a67d0c2515b5044b01f58eda765e37f4b Mon Sep 17 00:00:00 2001 From: Dev version update bot Date: Thu, 4 Apr 2024 15:09:53 +0000 Subject: [PATCH 18/46] Auto update version --- pennylane_lightning/core/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 19067082da..86e85a246a 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.36.0-dev21" +__version__ = "0.36.0-dev22" From 57807f31d94a128a9e5d618fd1e2d5094d7a6abe Mon Sep 17 00:00:00 2001 From: Vincent Michaud-Rioux Date: Fri, 5 Apr 2024 09:22:29 -0400 Subject: [PATCH 19/46] trigger ci From 7691b048e1067a9975b5cfa213130a51815ee975 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Mon, 8 Apr 2024 16:37:29 -0400 Subject: [PATCH 20/46] Added changes per review --- pennylane_lightning/core/_serialize.py | 22 +++++++------- tests/test_serialize.py | 40 ++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/pennylane_lightning/core/_serialize.py b/pennylane_lightning/core/_serialize.py index c6ad4156c7..fb6c31391e 100644 --- a/pennylane_lightning/core/_serialize.py +++ b/pennylane_lightning/core/_serialize.py @@ -36,7 +36,9 @@ from pennylane.ops import Hamiltonian, LinearCombination, Prod, SProd, Sum from pennylane.tape import QuantumTape -pauli_name_map = { +NAMED_OBS = (PauliX, PauliY, PauliZ, Identity, Hadamard) +OP_MATH_OBS = (LinearCombination, Prod, SProd, Sum) +PAULI_NAME_MAP = { "I": "Identity", "X": "PauliX", "Y": "PauliY", @@ -242,11 +244,11 @@ def map_wire(wire: int): if len(observable) == 1: wire, pauli = list(observable.items())[0] - return self.named_obs(pauli_name_map[pauli], [map_wire(wire)]) + return self.named_obs(PAULI_NAME_MAP[pauli], [map_wire(wire)]) return self.tensor_obs( [ - self.named_obs(pauli_name_map[pauli], [map_wire(wire)]) + self.named_obs(PAULI_NAME_MAP[pauli], [map_wire(wire)]) for wire, pauli in observable.items() ] ) @@ -267,21 +269,19 @@ def _pauli_sentence(self, observable, wires_map: dict = None): # pylint: disable=protected-access, too-many-return-statements def _ob(self, observable, wires_map: dict = None): """Serialize a :class:`pennylane.operation.Observable` into an Observable.""" - if isinstance(observable, (PauliX, PauliY, PauliZ, Identity, Hadamard)): + if isinstance(observable, NAMED_OBS): return self._named_obs(observable, wires_map) if isinstance(observable, Hamiltonian): return self._hamiltonian(observable, wires_map) if observable.pauli_rep is not None: return self._pauli_sentence(observable.pauli_rep, wires_map) - if isinstance(observable, Tensor) or ( - isinstance(observable, Prod) and not observable.has_overlapping_wires - ): + if isinstance(observable, (Tensor, Prod)): + if isinstance(observable, Prod) and observable.has_overlapping_wires: + return self._hermitian_ob(observable, wires_map) return self._tensor_ob(observable, wires_map) - if isinstance(observable, (LinearCombination, Prod, SProd, Sum)): - # if isinstance(observable, (Sum, Prod, SProd)): - # observable = observable.simplify() + if isinstance(observable, OP_MATH_OBS): return self._hamiltonian(observable, wires_map) - if observable.name == "SparseHamiltonian": + if isinstance(observable, SparseHamiltonian): return self._sparse_hamiltonian(observable, wires_map) return self._hermitian_ob(observable, wires_map) diff --git a/tests/test_serialize.py b/tests/test_serialize.py index 13e877b3f3..3cf2ff8709 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -427,11 +427,47 @@ def test_hamiltonian_mix_return(self, use_csingle, wires_map): assert s[0] == s_expected1 assert s[1] == s_expected2 + @pytest.mark.parametrize("use_csingle", [True, False]) + @pytest.mark.parametrize("wires_map", [wires_dict, None]) + def test_pauli_rep_return(self, use_csingle, wires_map): + """Test that an observable with a valid pauli rep is serialized correctly.""" + with qml.tape.QuantumTape() as tape: + qml.expval(qml.PauliX(0) + qml.PauliZ(0)) + + hamiltonian_obs = HamiltonianC64 if use_csingle else HamiltonianC128 + named_obs = NamedObsC64 if use_csingle else NamedObsC128 + r_dtype = np.float32 if use_csingle else np.float64 + + s, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( + tape, wires_map + ) + s_expected = hamiltonian_obs( + np.array([1, 1], dtype=r_dtype), [named_obs("PauliX", [0]), named_obs("PauliZ", [0])] + ) + assert s[0] == s_expected + + @pytest.mark.parametrize("use_csingle", [True, False]) + @pytest.mark.parametrize("wires_map", [wires_dict, None]) + def test_pauli_rep_single_term(self, use_csingle, wires_map): + """Test that an observable with a single term in the pauli rep is serialized correctly""" + with qml.tape.QuantumTape() as tape: + qml.expval(qml.PauliX(0) @ qml.PauliZ(1)) + + named_obs = NamedObsC64 if use_csingle else NamedObsC128 + tensor_prod_obs = TensorProdObsC64 if use_csingle else TensorProdObsC128 + r_dtype = np.float32 if use_csingle else np.float64 + + s, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( + tape, wires_map + ) + s_expected = tensor_prod_obs([named_obs("PauliZ", [1]), named_obs("PauliX", [0])]) + assert s[0] == s_expected + @pytest.mark.parametrize("use_csingle", [True, False]) @pytest.mark.parametrize("wires_map", [wires_dict, None]) def test_sprod(self, use_csingle, wires_map): """Test that SProds are serialized correctly""" - tape = qml.tape.QuantumScript([], [qml.expval(qml.s_prod(0.1, qml.PauliX(0)))]) + tape = qml.tape.QuantumScript([], [qml.expval(qml.s_prod(0.1, qml.Hadamard(0)))]) hamiltonian_obs = HamiltonianC64 if use_csingle else HamiltonianC128 named_obs = NamedObsC64 if use_csingle else NamedObsC128 @@ -444,7 +480,7 @@ def test_sprod(self, use_csingle, wires_map): assert isinstance(res[0], hamiltonian_obs) coeffs = np.array([0.1]).astype(rtype) - s_expected = hamiltonian_obs(coeffs, [named_obs("PauliX", [0])]) + s_expected = hamiltonian_obs(coeffs, [named_obs("Hadamard", [0])]) assert res[0] == s_expected @pytest.mark.parametrize("use_csingle", [True, False]) From 1ce158b905d87458f6ce07d5ea9c26320e642b34 Mon Sep 17 00:00:00 2001 From: Dev version update bot Date: Mon, 8 Apr 2024 20:37:47 +0000 Subject: [PATCH 21/46] Auto update version --- pennylane_lightning/core/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 86e85a246a..88f51b9d67 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.36.0-dev22" +__version__ = "0.36.0-dev23" From 3c198c3ea2db14554f1cc5e784d45bcf80024663 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Mon, 8 Apr 2024 16:38:38 -0400 Subject: [PATCH 22/46] Update requirements-dev.txt --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index de03965307..02aa0c47f9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ pip~=22.0 -git+https://github.com/PennyLaneAI/pennylane.git@tf-device-float32 +git+https://github.com/PennyLaneAI/pennylane.git@master ninja flaky pybind11 From e44e9dc1da13c516599846a1fc5825d83764b57a Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Tue, 9 Apr 2024 08:57:04 -0400 Subject: [PATCH 23/46] Update pennylane_lightning/core/_serialize.py Co-authored-by: Ali Asadi <10773383+maliasadi@users.noreply.github.com> --- pennylane_lightning/core/_serialize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane_lightning/core/_serialize.py b/pennylane_lightning/core/_serialize.py index fb6c31391e..efb5ce9a18 100644 --- a/pennylane_lightning/core/_serialize.py +++ b/pennylane_lightning/core/_serialize.py @@ -36,8 +36,8 @@ from pennylane.ops import Hamiltonian, LinearCombination, Prod, SProd, Sum from pennylane.tape import QuantumTape -NAMED_OBS = (PauliX, PauliY, PauliZ, Identity, Hadamard) -OP_MATH_OBS = (LinearCombination, Prod, SProd, Sum) +NAMED_OBS = (Identity, PauliX, PauliY, PauliZ, Hadamard) +OP_MATH_OBS = (Prod, SProd, Sum, LinearCombination) PAULI_NAME_MAP = { "I": "Identity", "X": "PauliX", From cdc2d9ca5bcda35a85069e247ba801beec29d346 Mon Sep 17 00:00:00 2001 From: Dev version update bot Date: Tue, 9 Apr 2024 12:57:18 +0000 Subject: [PATCH 24/46] Auto update version --- pennylane_lightning/core/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 88f51b9d67..b0da18e929 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.36.0-dev23" +__version__ = "0.36.0-dev24" From 98c77ee6d26863c394e353790e9614e7722a1948 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Tue, 9 Apr 2024 08:57:43 -0400 Subject: [PATCH 25/46] Trigger CI From d125a3ce6600ee5c26658dae11c09efe05fe4082 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Tue, 9 Apr 2024 15:49:20 -0400 Subject: [PATCH 26/46] Update overlapping wires prod serialization test --- tests/test_serialize.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_serialize.py b/tests/test_serialize.py index 3cf2ff8709..2598232b9b 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -100,17 +100,13 @@ def test_wrong_device_name(): ), TensorProdObsC128, ), - # The following case is disabled due to recursion errors. This needs to be investigated before the - # PR is merged. Simplifying this operator doesn't change anything. Maybe we raise an error in this - # case, or fall back to Hermitian, though I doubt the matrix will be hermitian. - pytest.param( + ( ( qml.Hermitian(np.eye(2), wires=0) @ qml.Hermitian(np.eye(2), wires=1) @ qml.Projector([0], wires=1) ), - HamiltonianC128, - marks=pytest.mark.xfail(reason="prods with overlapping wires unsupported"), + HermitianObsC128, ), ( qml.PauliZ(0) @ qml.Hermitian(np.eye(2), wires=1) @ qml.Projector([0], wires=2), From 0287abdeb618df98721f701aad4531c8e71997f6 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Wed, 10 Apr 2024 10:06:02 -0400 Subject: [PATCH 27/46] Updated tests to work with new pauli word order --- requirements-dev.txt | 2 +- tests/test_serialize.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 02aa0c47f9..c543ba4671 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ pip~=22.0 -git+https://github.com/PennyLaneAI/pennylane.git@master +git+https://github.com/PennyLaneAI/pennylane.git@pauli-swap-order ninja flaky pybind11 diff --git a/tests/test_serialize.py b/tests/test_serialize.py index 2598232b9b..cea30af11c 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -146,7 +146,7 @@ def test_tensor_non_tensor_return(self, use_csingle, wires_map): named_obs = NamedObsC64 if use_csingle else NamedObsC128 tensor_prod_obs = TensorProdObsC64 if use_csingle else TensorProdObsC128 - first_s = tensor_prod_obs([named_obs("PauliX", [1]), named_obs("PauliZ", [0])]) + first_s = tensor_prod_obs([named_obs("PauliZ", [0]), named_obs("PauliX", [1])]) s_expected = [ first_s, @@ -283,7 +283,7 @@ def test_hamiltonian_return(self, use_csingle, wires_map): named_obs("PauliY", [2]), ] ), - tensor_prod_obs([named_obs("PauliY", [2]), named_obs("PauliX", [0])]), + tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]), hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), ], ) @@ -346,8 +346,8 @@ def test_hamiltonian_tensor_return(self, use_csingle, wires_map): first_term, tensor_prod_obs( [ - named_obs("PauliY", [2]), named_obs("PauliX", [0]), + named_obs("PauliY", [2]), named_obs("PauliZ", [3]), ] ), @@ -403,7 +403,7 @@ def test_hamiltonian_mix_return(self, use_csingle, wires_map): named_obs("PauliY", [2]), ] ), - tensor_prod_obs([named_obs("PauliY", [2]), named_obs("PauliX", [0])]), + tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]), hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), ], ) @@ -416,7 +416,7 @@ def test_hamiltonian_mix_return(self, use_csingle, wires_map): hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [1, 2]), ] ), - tensor_prod_obs([named_obs("PauliX", [2]), named_obs("PauliY", [0])]), + tensor_prod_obs([named_obs("PauliY", [0]), named_obs("PauliX", [2])]), ], ) @@ -456,7 +456,7 @@ def test_pauli_rep_single_term(self, use_csingle, wires_map): s, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( tape, wires_map ) - s_expected = tensor_prod_obs([named_obs("PauliZ", [1]), named_obs("PauliX", [0])]) + s_expected = tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliZ", [1])]) assert s[0] == s_expected @pytest.mark.parametrize("use_csingle", [True, False]) @@ -498,7 +498,7 @@ def test_prod(self, use_csingle, wires_map): s_expected = tensor_obs( [ - tensor_obs([named_obs("PauliX", [1]), named_obs("PauliZ", [0])]), + tensor_obs([named_obs("PauliZ", [0]), named_obs("PauliX", [1])]), named_obs("Hadamard", [2]), ] ) @@ -536,7 +536,7 @@ def test_sum(self, use_csingle, wires_map): coeffs, [ tensor_obs( - [named_obs("PauliZ", [1]), named_obs("PauliX", [0]), named_obs("PauliX", [2])] + [named_obs("PauliX", [0]), named_obs("PauliZ", [1]), named_obs("PauliX", [2])] ), tensor_obs( [named_obs("PauliZ", [0]), named_obs("PauliY", [1]), named_obs("Hadamard", [2])] From 9dc4bb0b2f6ed325277457b32442e8bacc42aefa Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Wed, 10 Apr 2024 15:28:30 -0400 Subject: [PATCH 28/46] Added device_vjp tests --- tests/test_vjp.py | 114 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/tests/test_vjp.py b/tests/test_vjp.py index 790c22e784..4efb4d6c25 100644 --- a/tests/test_vjp.py +++ b/tests/test_vjp.py @@ -397,6 +397,120 @@ def test_prob_expectation_values(self, dev): ): get_vjp(dev, tape, dy) + def test_device_vjp_qnode_autograd(self, dev, tol): + """Test that requesting device_vjp=True with lightning device qnodes works as expected""" + + def circuit(x): + qml.RX(x[0], wires=[0]) + qml.RY(x[1], wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)) + + dev_ref = qml.device("default.qubit") + x = qml.numpy.array([0.543, -0.654], requires_grad=True) + + qnode = qml.QNode(circuit, dev, device_vjp=True) + qnode_ref = qml.QNode(circuit, dev_ref, device_vjp=True) + + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 + assert np.allclose(qnode(x), qnode_ref(x), atol=tol, rtol=0) + + grad = qml.grad(qnode)(x) + grad_ref = qml.grad(qnode_ref)(x) + assert np.allclose(grad, grad_ref, atol=tol, rtol=0) + + jac = qml.jacobian(qnode)(x) + jac_ref = qml.jacobian(qnode_ref)(x) + assert np.allclose(jac, jac_ref, atol=tol, rtol=0) + + def test_device_vjp_qnode_torch(self, dev, tol): + """Test that requesting device_vjp=True with lightning device qnodes works as expected""" + torch = pytest.importorskip("torch") + + def circuit(x): + qml.RX(x[0], wires=[0]) + qml.RY(x[1], wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)) + + dev_ref = qml.device("default.qubit") + x = torch.tensor([0.543, -0.654], requires_grad=True) + x_ref = torch.tensor([0.543, -0.654], requires_grad=True) + + qnode = qml.QNode(circuit, dev, device_vjp=True) + qnode_ref = qml.QNode(circuit, dev_ref, device_vjp=True) + + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 + res = qnode(x) + expected = qnode_ref(x_ref) + assert qml.math.allclose(res, expected, atol=tol, rtol=0) + + res.backward() + expected.backward() + assert qml.math.allclose(x.grad, x_ref.grad, atol=tol, rtol=0) + + def test_device_vjp_qnode_tf(self, dev, tol): + """Test that requesting device_vjp=True with lightning device qnodes works as expected""" + tf = pytest.importorskip("tensorflow") + dtype = tf.float32 if dev.dtype == np.complex64 else tf.float64 + + def circuit(x): + qml.RX(x[0], wires=[0]) + qml.RY(x[1], wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)) + + dev_ref = qml.device("default.qubit") + x = tf.Variable([0.543, -0.654], dtype=dtype) + + qnode = qml.QNode(circuit, dev, device_vjp=True) + qnode_ref = qml.QNode(circuit, dev_ref, device_vjp=True) + + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 + + with tf.GradientTape(persistent=True) as tape: + res = qnode(x) + with tf.GradientTape(persistent=True) as tape_ref: + expected = qnode_ref(x) + + assert np.allclose(res, expected, atol=tol, rtol=0) + + grad = tape.gradient(res, [x]) + grad_ref = tape_ref.gradient(expected, [x]) + assert np.allclose(grad, grad_ref, atol=tol, rtol=0) + + jac = tape.jacobian(res, [x], experimental_use_pfor=False) + jac_ref = tape_ref.jacobian(expected, [x], experimental_use_pfor=False) + assert np.allclose(jac, jac_ref, atol=tol, rtol=0) + + @pytest.mark.parametrize("use_jit", [True, False]) + def test_device_vjp_qnode_jax(self, use_jit, dev, tol): + """Test that requesting device_vjp=True with lightning device qnodes works as expected""" + jax = pytest.importorskip("jax") + + if dev.dtype == np.complex128: + jax.config.update("jax_enable_x64", True) + + def circuit(x): + qml.RX(x[0], wires=[0]) + qml.RY(x[1], wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)) + + dev_ref = qml.device("default.qubit") + x = jax.numpy.array([0.543, -0.654]) + + qnode = qml.QNode(circuit, dev, device_vjp=True) + qnode_ref = qml.QNode(circuit, dev_ref, device_vjp=True) + + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 + assert np.allclose(qnode(x), qnode_ref(x), atol=tol, rtol=0) + + # We don't test for jacobian with jax due to broadcasted dimensions to vjp dys + grad = jax.jit(jax.grad(qnode))(x) if use_jit else jax.grad(qnode)(x) + grad_ref = jax.jit(jax.grad(qnode_ref))(x) if use_jit else jax.grad(qnode_ref)(x) + assert np.allclose(grad, grad_ref, atol=tol, rtol=0) + class TestBatchVectorJacobianProduct: """Tests for the batch_vjp function""" From 6586d83a11a78a5c783ee599dda0f28df4930d2d Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Wed, 17 Apr 2024 16:15:18 -0400 Subject: [PATCH 29/46] Update serialize tests --- tests/test_serialize.py | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/tests/test_serialize.py b/tests/test_serialize.py index cea30af11c..01d094fa3a 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -319,31 +319,17 @@ def test_hamiltonian_tensor_return(self, use_csingle, wires_map): # Expression (ham @ obs) is converted internally by Pennylane # where obs is appended to each term of the ham - if qml.operation.active_new_opmath(): - first_term = tensor_prod_obs( - [ - tensor_prod_obs( - [ - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), - named_obs("PauliY", [2]), - ] - ), - named_obs("PauliZ", [3]), - ] - ) - else: - first_term = tensor_prod_obs( - [ - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), - named_obs("PauliY", [2]), - named_obs("PauliZ", [3]), - ] - ) s_expected = hamiltonian_obs( np.array([0.3, 0.5, 0.4], dtype=r_dtype), [ - first_term, + tensor_prod_obs( + [ + hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), + named_obs("PauliY", [2]), + named_obs("PauliZ", [3]), + ] + ), tensor_prod_obs( [ named_obs("PauliX", [0]), @@ -497,10 +483,7 @@ def test_prod(self, use_csingle, wires_map): assert isinstance(res[0], tensor_obs) s_expected = tensor_obs( - [ - tensor_obs([named_obs("PauliZ", [0]), named_obs("PauliX", [1])]), - named_obs("Hadamard", [2]), - ] + [named_obs("PauliZ", [0]), named_obs("PauliX", [1]), named_obs("Hadamard", [2])] ) assert res[0] == s_expected From e7fe445bb030ddd0c608dd0166ca80eeca593e27 Mon Sep 17 00:00:00 2001 From: Dev version update bot Date: Wed, 17 Apr 2024 20:23:52 +0000 Subject: [PATCH 30/46] Auto update version --- pennylane_lightning/core/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 7fbbc4aee8..22ae5e03f5 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.36.0-dev30" +__version__ = "0.36.0-dev31" From 12a681e8689af7fa235df4c669ef1b6a5f90e411 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Wed, 17 Apr 2024 17:00:59 -0400 Subject: [PATCH 31/46] Skipped tf test --- tests/test_vjp.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_vjp.py b/tests/test_vjp.py index 4efb4d6c25..b3b68bbd65 100644 --- a/tests/test_vjp.py +++ b/tests/test_vjp.py @@ -449,6 +449,9 @@ def circuit(x): expected.backward() assert qml.math.allclose(x.grad, x_ref.grad, atol=tol, rtol=0) + # TODO: Update the following test after TensorFlow dtype issues are resolved. + + @pytest.skipif(ld._new_API, reason="TensorFlow dtype issues with new API") def test_device_vjp_qnode_tf(self, dev, tol): """Test that requesting device_vjp=True with lightning device qnodes works as expected""" tf = pytest.importorskip("tensorflow") From 1716ff93b09ee855a9b1edfb72260cb1b11a8d80 Mon Sep 17 00:00:00 2001 From: Dev version update bot Date: Thu, 18 Apr 2024 14:31:35 +0000 Subject: [PATCH 32/46] Auto update version --- pennylane_lightning/core/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 22ae5e03f5..60d8f6e108 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.36.0-dev31" +__version__ = "0.36.0-dev32" From 7c40d6ad5b7243e7e9574670659095077987d87d Mon Sep 17 00:00:00 2001 From: Dev version update bot Date: Thu, 18 Apr 2024 17:55:55 +0000 Subject: [PATCH 33/46] Auto update version --- pennylane_lightning/core/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 60d8f6e108..9c6e90e856 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.36.0-dev32" +__version__ = "0.36.0-dev33" From 62a8b91d7d577e81baf1c66fc35c1a38b0d0a55e Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Thu, 18 Apr 2024 13:56:12 -0400 Subject: [PATCH 34/46] Trigger CI From b3f77168d9730706462c5c66c438422bf4a17d7a Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Thu, 18 Apr 2024 14:48:10 -0400 Subject: [PATCH 35/46] Updated torch test --- tests/test_vjp.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_vjp.py b/tests/test_vjp.py index b3b68bbd65..7bd210b2b8 100644 --- a/tests/test_vjp.py +++ b/tests/test_vjp.py @@ -449,9 +449,13 @@ def circuit(x): expected.backward() assert qml.math.allclose(x.grad, x_ref.grad, atol=tol, rtol=0) + jac = torch.autograd.functional.jacobian(qnode, (x,)) + jac_ref = torch.autograd.functional.jacobian(qnode_ref, (x_ref,)) + assert qml.math.allclose(jac, jac_ref, atol=tol, rtol=0) + # TODO: Update the following test after TensorFlow dtype issues are resolved. - @pytest.skipif(ld._new_API, reason="TensorFlow dtype issues with new API") + @pytest.mark.skipif(ld._new_API, reason="TensorFlow dtype issues with new API") def test_device_vjp_qnode_tf(self, dev, tol): """Test that requesting device_vjp=True with lightning device qnodes works as expected""" tf = pytest.importorskip("tensorflow") From 154e105759405cf51ee6f7e16033c53baf06b299 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Thu, 18 Apr 2024 16:03:53 -0400 Subject: [PATCH 36/46] Update serialization tests; removed xfail --- tests/test_serialize.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_serialize.py b/tests/test_serialize.py index d2cbe51835..d7ce9281d2 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -148,7 +148,7 @@ def test_tensor_non_tensor_return(self, use_csingle, wires_map): tensor_prod_obs = TensorProdObsC64 if use_csingle else TensorProdObsC128 s_expected = [ - tensor_prod_obs([named_obs("PauliX", [1]), named_obs("PauliZ", [0])]), + tensor_prod_obs([named_obs("PauliZ", [0]), named_obs("PauliX", [1])]), named_obs("Hadamard", [1]), ] @@ -157,7 +157,6 @@ def test_tensor_non_tensor_return(self, use_csingle, wires_map): ) assert s == s_expected - @pytest.mark.xfail(reason="Prod with overlapping wires not supported") @pytest.mark.parametrize("use_csingle", [True, False]) @pytest.mark.parametrize("wires_map", [wires_dict, None]) def test_prod_return_with_overlapping_wires(self, use_csingle, wires_map): From 65662c77ad5285955405b416f76596f150bceaf4 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Fri, 19 Apr 2024 11:32:43 -0400 Subject: [PATCH 37/46] Trigger CI From 2c34cfbd60285186d52bf07b77f6f2bd9e329658 Mon Sep 17 00:00:00 2001 From: Dev version update bot Date: Fri, 19 Apr 2024 15:33:01 +0000 Subject: [PATCH 38/46] Auto update version --- pennylane_lightning/core/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 9c6e90e856..01c5ebb276 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.36.0-dev33" +__version__ = "0.36.0-dev34" From 2158c6dc3f11805dfe820b5fbbd55f8dd10a7665 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Fri, 19 Apr 2024 12:01:04 -0400 Subject: [PATCH 39/46] Remove vjp tests --- tests/test_vjp.py | 121 ---------------------------------------------- 1 file changed, 121 deletions(-) diff --git a/tests/test_vjp.py b/tests/test_vjp.py index 7bd210b2b8..790c22e784 100644 --- a/tests/test_vjp.py +++ b/tests/test_vjp.py @@ -397,127 +397,6 @@ def test_prob_expectation_values(self, dev): ): get_vjp(dev, tape, dy) - def test_device_vjp_qnode_autograd(self, dev, tol): - """Test that requesting device_vjp=True with lightning device qnodes works as expected""" - - def circuit(x): - qml.RX(x[0], wires=[0]) - qml.RY(x[1], wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - dev_ref = qml.device("default.qubit") - x = qml.numpy.array([0.543, -0.654], requires_grad=True) - - qnode = qml.QNode(circuit, dev, device_vjp=True) - qnode_ref = qml.QNode(circuit, dev_ref, device_vjp=True) - - tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 - assert np.allclose(qnode(x), qnode_ref(x), atol=tol, rtol=0) - - grad = qml.grad(qnode)(x) - grad_ref = qml.grad(qnode_ref)(x) - assert np.allclose(grad, grad_ref, atol=tol, rtol=0) - - jac = qml.jacobian(qnode)(x) - jac_ref = qml.jacobian(qnode_ref)(x) - assert np.allclose(jac, jac_ref, atol=tol, rtol=0) - - def test_device_vjp_qnode_torch(self, dev, tol): - """Test that requesting device_vjp=True with lightning device qnodes works as expected""" - torch = pytest.importorskip("torch") - - def circuit(x): - qml.RX(x[0], wires=[0]) - qml.RY(x[1], wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - dev_ref = qml.device("default.qubit") - x = torch.tensor([0.543, -0.654], requires_grad=True) - x_ref = torch.tensor([0.543, -0.654], requires_grad=True) - - qnode = qml.QNode(circuit, dev, device_vjp=True) - qnode_ref = qml.QNode(circuit, dev_ref, device_vjp=True) - - tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 - res = qnode(x) - expected = qnode_ref(x_ref) - assert qml.math.allclose(res, expected, atol=tol, rtol=0) - - res.backward() - expected.backward() - assert qml.math.allclose(x.grad, x_ref.grad, atol=tol, rtol=0) - - jac = torch.autograd.functional.jacobian(qnode, (x,)) - jac_ref = torch.autograd.functional.jacobian(qnode_ref, (x_ref,)) - assert qml.math.allclose(jac, jac_ref, atol=tol, rtol=0) - - # TODO: Update the following test after TensorFlow dtype issues are resolved. - - @pytest.mark.skipif(ld._new_API, reason="TensorFlow dtype issues with new API") - def test_device_vjp_qnode_tf(self, dev, tol): - """Test that requesting device_vjp=True with lightning device qnodes works as expected""" - tf = pytest.importorskip("tensorflow") - dtype = tf.float32 if dev.dtype == np.complex64 else tf.float64 - - def circuit(x): - qml.RX(x[0], wires=[0]) - qml.RY(x[1], wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - dev_ref = qml.device("default.qubit") - x = tf.Variable([0.543, -0.654], dtype=dtype) - - qnode = qml.QNode(circuit, dev, device_vjp=True) - qnode_ref = qml.QNode(circuit, dev_ref, device_vjp=True) - - tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 - - with tf.GradientTape(persistent=True) as tape: - res = qnode(x) - with tf.GradientTape(persistent=True) as tape_ref: - expected = qnode_ref(x) - - assert np.allclose(res, expected, atol=tol, rtol=0) - - grad = tape.gradient(res, [x]) - grad_ref = tape_ref.gradient(expected, [x]) - assert np.allclose(grad, grad_ref, atol=tol, rtol=0) - - jac = tape.jacobian(res, [x], experimental_use_pfor=False) - jac_ref = tape_ref.jacobian(expected, [x], experimental_use_pfor=False) - assert np.allclose(jac, jac_ref, atol=tol, rtol=0) - - @pytest.mark.parametrize("use_jit", [True, False]) - def test_device_vjp_qnode_jax(self, use_jit, dev, tol): - """Test that requesting device_vjp=True with lightning device qnodes works as expected""" - jax = pytest.importorskip("jax") - - if dev.dtype == np.complex128: - jax.config.update("jax_enable_x64", True) - - def circuit(x): - qml.RX(x[0], wires=[0]) - qml.RY(x[1], wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) - - dev_ref = qml.device("default.qubit") - x = jax.numpy.array([0.543, -0.654]) - - qnode = qml.QNode(circuit, dev, device_vjp=True) - qnode_ref = qml.QNode(circuit, dev_ref, device_vjp=True) - - tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 - assert np.allclose(qnode(x), qnode_ref(x), atol=tol, rtol=0) - - # We don't test for jacobian with jax due to broadcasted dimensions to vjp dys - grad = jax.jit(jax.grad(qnode))(x) if use_jit else jax.grad(qnode)(x) - grad_ref = jax.jit(jax.grad(qnode_ref))(x) if use_jit else jax.grad(qnode_ref)(x) - assert np.allclose(grad, grad_ref, atol=tol, rtol=0) - class TestBatchVectorJacobianProduct: """Tests for the batch_vjp function""" From 899569c968e3dc24ad062d11e5e3d6acf4d18681 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Fri, 19 Apr 2024 15:19:43 -0400 Subject: [PATCH 40/46] Trigger CI From c7b57813b280c0e252280d31555f711c06ab397c Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Fri, 19 Apr 2024 16:33:31 -0400 Subject: [PATCH 41/46] Trigger CI From b05ddf969ddd0172593fd9470939b460cb7d59b5 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Mon, 22 Apr 2024 10:49:37 -0400 Subject: [PATCH 42/46] [skip ci] Revert requirements file --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 11db05d97f..c87c9154a4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ pip~=22.0 -git+https://github.com/PennyLaneAI/pennylane.git@pauli-swap-order +git+https://github.com/PennyLaneAI/pennylane.git@master ninja flaky pybind11 From d4f5c08d35ea67eeaed758f70adb3c3ee1ee6859 Mon Sep 17 00:00:00 2001 From: ringo-but-quantum Date: Tue, 23 Apr 2024 17:23:55 +0000 Subject: [PATCH 43/46] Auto update version from '0.36.0-dev35' to '0.36.0-dev36' --- pennylane_lightning/core/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 32534cfb8a..2cb26ae207 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.36.0-dev35" +__version__ = "0.36.0-dev36" From 80e7d5f0958d47c947d26827c2f436bcdf379772 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Tue, 23 Apr 2024 15:06:38 -0400 Subject: [PATCH 44/46] Trigger CI From 296ea6d7f00d334f33ddba7f8456dd599b31601e Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Tue, 23 Apr 2024 15:06:48 -0400 Subject: [PATCH 45/46] Trigger CI From 5bd3134a1db519365078a80d9fa6dd7982980a1b Mon Sep 17 00:00:00 2001 From: Vincent Michaud-Rioux Date: Tue, 23 Apr 2024 20:50:26 -0400 Subject: [PATCH 46/46] Fix native mcm workflow following dynamic_one_shot refactor. (#694) * Fix native mcm workflow following dynamic_one_shot refactor. * Auto update version from '0.36.0-dev35' to '0.36.0-dev36' * Update changelog. --------- Co-authored-by: ringo-but-quantum --- .github/CHANGELOG.md | 3 +++ .../lightning_qubit/_measurements.py | 22 ++++++++++++++++--- .../lightning_qubit/lightning_qubit.py | 7 ++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index eea553de19..2778ea71ba 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -86,6 +86,9 @@ ### Bug fixes +* `dynamic_one_shot` was refactored to use `SampleMP` measurements as a way to return the mid-circuit measurement samples. `LightningQubit`'s `simulate` is modified accordingly. + [(#694)](https://github.com/PennyLaneAI/pennylane/pull/694) + * `LightningQubit` correctly decomposes state prep operations when used in the middle of a circuit. [(#687)](https://github.com/PennyLaneAI/pennylane/pull/687) diff --git a/pennylane_lightning/lightning_qubit/_measurements.py b/pennylane_lightning/lightning_qubit/_measurements.py index 264a4b2bda..52d3c154b4 100644 --- a/pennylane_lightning/lightning_qubit/_measurements.py +++ b/pennylane_lightning/lightning_qubit/_measurements.py @@ -248,7 +248,7 @@ def measurement(self, measurementprocess: MeasurementProcess) -> TensorLike: """ return self.get_measurement_function(measurementprocess)(measurementprocess) - def measure_final_state(self, circuit: QuantumScript) -> Result: + def measure_final_state(self, circuit: QuantumScript, mid_measurements=None) -> Result: """ Perform the measurements required by the circuit on the provided state. @@ -256,6 +256,7 @@ def measure_final_state(self, circuit: QuantumScript) -> Result: Args: circuit (QuantumScript): The single circuit to simulate + mid_measurements (None, dict): Dictionary of mid-circuit measurements Returns: Tuple[TensorLike]: The measurement results @@ -272,6 +273,7 @@ def measure_final_state(self, circuit: QuantumScript) -> Result: results = self.measure_with_samples( circuit.measurements, shots=circuit.shots, + mid_measurements=mid_measurements, ) if len(circuit.measurements) == 1: @@ -285,8 +287,9 @@ def measure_final_state(self, circuit: QuantumScript) -> Result: # pylint:disable = too-many-arguments def measure_with_samples( self, - mps: List[Union[SampleMeasurement, ClassicalShadowMP, ShadowExpvalMP]], + measurements: List[Union[SampleMeasurement, ClassicalShadowMP, ShadowExpvalMP]], shots: Shots, + mid_measurements=None, ) -> List[TensorLike]: """ Returns the samples of the measurement process performed on the given state. @@ -294,18 +297,27 @@ def measure_with_samples( have already been mapped to integer wires used in the device. Args: - mps (List[Union[SampleMeasurement, ClassicalShadowMP, ShadowExpvalMP]]): + measurements (List[Union[SampleMeasurement, ClassicalShadowMP, ShadowExpvalMP]]): The sample measurements to perform shots (Shots): The number of samples to take + mid_measurements (None, dict): Dictionary of mid-circuit measurements Returns: List[TensorLike[Any]]: Sample measurement results """ + # last N measurements are sampling MCMs in ``dynamic_one_shot`` execution mode + mps = measurements[0 : -len(mid_measurements)] if mid_measurements else measurements + skip_measure = ( + any(v == -1 for v in mid_measurements.values()) if mid_measurements else False + ) groups, indices = _group_measurements(mps) all_res = [] for group in groups: + if skip_measure: + all_res.extend([None] * len(group)) + continue if isinstance(group[0], (ExpectationMP, VarianceMP)) and isinstance( group[0].obs, SparseHamiltonian ): @@ -333,6 +345,10 @@ def measure_with_samples( res for _, res in sorted(list(enumerate(all_res)), key=lambda r: flat_indices[r[0]]) ) + # append MCM samples + if mid_measurements: + sorted_res += tuple(mid_measurements.values()) + # put the shot vector axis before the measurement axis if shots.has_partitioned_shots: sorted_res = tuple(zip(*sorted_res)) diff --git a/pennylane_lightning/lightning_qubit/lightning_qubit.py b/pennylane_lightning/lightning_qubit/lightning_qubit.py index bfefacb4bd..8350804c18 100644 --- a/pennylane_lightning/lightning_qubit/lightning_qubit.py +++ b/pennylane_lightning/lightning_qubit/lightning_qubit.py @@ -80,11 +80,8 @@ def simulate(circuit: QuantumScript, state: LightningStateVector, mcmc: dict = N if circuit.shots and has_mcm: mid_measurements = {} final_state = state.get_final_state(circuit, mid_measurements=mid_measurements) - if any(v == -1 for v in mid_measurements.values()): - return None, mid_measurements - return ( - LightningMeasurements(final_state, **mcmc).measure_final_state(circuit), - mid_measurements, + return LightningMeasurements(final_state, **mcmc).measure_final_state( + circuit, mid_measurements=mid_measurements ) final_state = state.get_final_state(circuit) return LightningMeasurements(final_state, **mcmc).measure_final_state(circuit)