diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 7be85112af2..74910e463b0 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -310,6 +310,9 @@ array([False, False])

Bug fixes 🐛

+* `qml.Projector` is pickle-able again. + [(#4452)](https://github.com/PennyLaneAI/pennylane/pull/4452) + * Allow sparse matrix calculation of `SProd`s containing a `Tensor`. When using `Tensor.sparse_matrix()`, it is recommended to use the `wire_order` keyword argument over `wires`. [(#4424)](https://github.com/PennyLaneAI/pennylane/pull/4424) diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py index 6a8369bca59..15ee883a947 100644 --- a/pennylane/_qubit_device.py +++ b/pennylane/_qubit_device.py @@ -63,6 +63,7 @@ VnEntropyMP, Shots, ) +from pennylane.ops.qubit.observables import BasisStateProjector from pennylane.resource import Resources from pennylane.operation import operation_derivative, Operation from pennylane.tape import QuantumScript, QuantumTape @@ -1674,9 +1675,7 @@ def marginal_prob(self, prob, wires=None): return self._reshape(prob, flat_shape) def expval(self, observable, shot_range=None, bin_size=None): - if observable.name == "Projector" and len(observable.parameters[0]) == len( - observable.wires - ): + if isinstance(observable, BasisStateProjector): # branch specifically to handle the basis state projector observable idx = int("".join(str(i) for i in observable.parameters[0]), 2) probs = self.probability( @@ -1706,9 +1705,7 @@ def expval(self, observable, shot_range=None, bin_size=None): return np.squeeze(np.mean(samples, axis=axis)) def var(self, observable, shot_range=None, bin_size=None): - if observable.name == "Projector" and len(observable.parameters[0]) == len( - observable.wires - ): + if isinstance(observable, BasisStateProjector): # branch specifically to handle the basis state projector observable idx = int("".join(str(i) for i in observable.parameters[0]), 2) probs = self.probability( diff --git a/pennylane/measurements/expval.py b/pennylane/measurements/expval.py index e696546ff56..883c6598103 100644 --- a/pennylane/measurements/expval.py +++ b/pennylane/measurements/expval.py @@ -19,7 +19,7 @@ import pennylane as qml from pennylane.operation import Operator -from pennylane.ops import Projector +from pennylane.ops.qubit.observables import BasisStateProjector from pennylane.wires import Wires from .measurements import Expectation, SampleMeasurement, StateMeasurement @@ -104,8 +104,8 @@ def process_samples( shot_range: Tuple[int] = None, bin_size: int = None, ): - if isinstance(self.obs, Projector): - # branch specifically to handle the projector observable + if isinstance(self.obs, BasisStateProjector): + # branch specifically to handle the basis state projector observable idx = int("".join(str(i) for i in self.obs.parameters[0]), 2) probs = qml.probs(wires=self.wires).process_samples( samples=samples, wire_order=wire_order, shot_range=shot_range, bin_size=bin_size @@ -122,8 +122,8 @@ def process_samples( return qml.math.squeeze(qml.math.mean(samples, axis=axis)) def process_state(self, state: Sequence[complex], wire_order: Wires): - if isinstance(self.obs, Projector): - # branch specifically to handle the projector observable + if isinstance(self.obs, BasisStateProjector): + # branch specifically to handle the basis state projector observable idx = int("".join(str(i) for i in self.obs.parameters[0]), 2) probs = qml.probs(wires=self.wires).process_state(state=state, wire_order=wire_order) return probs[idx] diff --git a/pennylane/measurements/var.py b/pennylane/measurements/var.py index 60fd35da323..568285b124e 100644 --- a/pennylane/measurements/var.py +++ b/pennylane/measurements/var.py @@ -20,7 +20,7 @@ import pennylane as qml from pennylane.operation import Operator -from pennylane.ops import Projector +from pennylane.ops.qubit.observables import BasisStateProjector from pennylane.wires import Wires from .measurements import SampleMeasurement, StateMeasurement, Variance @@ -104,8 +104,8 @@ def process_samples( shot_range: Tuple[int] = None, bin_size: int = None, ): - if isinstance(self.obs, Projector): - # branch specifically to handle the projector observable + if isinstance(self.obs, BasisStateProjector): + # branch specifically to handle the basis state projector observable idx = int("".join(str(i) for i in self.obs.parameters[0]), 2) # we use ``self.wires`` instead of ``self.obs`` because the observable was # already applied before the sampling @@ -125,8 +125,8 @@ def process_samples( return qml.math.squeeze(qml.math.var(samples, axis=axis)) def process_state(self, state: Sequence[complex], wire_order: Wires): - if isinstance(self.obs, Projector): - # branch specifically to handle the projector observable + if isinstance(self.obs, BasisStateProjector): + # branch specifically to handle the basis state projector observable idx = int("".join(str(i) for i in self.obs.parameters[0]), 2) # we use ``self.wires`` instead of ``self.obs`` because the observable was # already applied to the state diff --git a/pennylane/ops/qubit/observables.py b/pennylane/ops/qubit/observables.py index 9246de99dc7..823e2250755 100644 --- a/pennylane/ops/qubit/observables.py +++ b/pennylane/ops/qubit/observables.py @@ -365,11 +365,10 @@ class Projector(Observable): 0.25 """ + name = "Projector" num_wires = AnyWires num_params = 1 """int: Number of trainable parameters that the operator depends on.""" - _basis_state_type = None # type if Projector inherits from _BasisStateProjector - _state_vector_type = None # type if Projector inherits from _StateVectorProjector def __new__(cls, state, wires, **_): """Changes parents based on the state representation. @@ -396,16 +395,10 @@ def __new__(cls, state, wires, **_): raise ValueError(f"Input state must be one-dimensional; got shape {shape}.") if len(state) == len(wires): - if cls._basis_state_type is None: - base_cls = (_BasisStateProjector, Projector) - cls._basis_state_type = type("Projector", base_cls, dict(cls.__dict__)) - return object.__new__(cls._basis_state_type) + return object.__new__(BasisStateProjector) if len(state) == 2 ** len(wires): - if cls._state_vector_type is None: - base_cls = (_StateVectorProjector, Projector) - cls._state_vector_type = type("Projector", base_cls, dict(cls.__dict__)) - return object.__new__(cls._state_vector_type) + return object.__new__(StateVectorProjector) raise ValueError( "Input state should have the same length as the wires in the case " @@ -418,28 +411,25 @@ def pow(self, z): """Raise this projector to the power ``z``.""" return [copy(self)] if (isinstance(z, int) and z > 0) else super().pow(z) - def __copy__(self): - copied_op = self.__new__(Projector, self.data[0], self.wires) - copied_op.data = copy(self.data) - for attr, value in vars(self).items(): - if attr != "data": - setattr(copied_op, attr, value) - return copied_op +class BasisStateProjector(Projector): + r"""Observable corresponding to the state projector :math:`P=\ket{\phi}\bra{\phi}`, where + :math:`\phi` denotes a basis state.""" - -class _BasisStateProjector(Observable): # The call signature should be the same as Projector.__new__ for the positional # arguments, but with free key word arguments. def __init__(self, state, wires, id=None): wires = Wires(wires) - state = list(qml.math.toarray(state)) + state = list(qml.math.toarray(state).astype(int)) if not set(state).issubset({0, 1}): raise ValueError(f"Basis state must only consist of 0s and 1s; got {state}") super().__init__(state, wires=wires, id=id) + def __new__(cls): # pylint: disable=arguments-differ + return object.__new__(cls) + def label(self, decimals=None, base_label=None, cache=None): r"""A customizable string representation of the operator. @@ -455,7 +445,7 @@ def label(self, decimals=None, base_label=None, cache=None): **Example:** - >>> _BasisStateProjector([0, 1, 0], wires=(0, 1, 2)).label() + >>> BasisStateProjector([0, 1, 0], wires=(0, 1, 2)).label() '|010⟩⟨010|' """ @@ -472,7 +462,7 @@ def compute_matrix(basis_state): # pylint: disable=arguments-differ The canonical matrix is the textbook matrix representation that does not consider wires. Implicitly, this assumes that the wires of the operator correspond to the global wire order. - .. seealso:: :meth:`~._BasisStateProjector.matrix` + .. seealso:: :meth:`~.BasisStateProjector.matrix` Args: basis_state (Iterable): basis state to project on @@ -482,7 +472,7 @@ def compute_matrix(basis_state): # pylint: disable=arguments-differ **Example** - >>> _BasisStateProjector.compute_matrix([0, 1]) + >>> BasisStateProjector.compute_matrix([0, 1]) [[0. 0. 0. 0.] [0. 1. 0. 0.] [0. 0. 0. 0.] @@ -506,7 +496,7 @@ def compute_eigvals(basis_state): # pylint: disable=arguments-differ Otherwise, no particular order for the eigenvalues is guaranteed. - .. seealso:: :meth:`~._BasisStateProjector.eigvals` + .. seealso:: :meth:`~.BasisStateProjector.eigvals` Args: basis_state (Iterable): basis state to project on @@ -516,7 +506,7 @@ def compute_eigvals(basis_state): # pylint: disable=arguments-differ **Example** - >>> _BasisStateProjector.compute_eigvals([0, 1]) + >>> BasisStateProjector.compute_eigvals([0, 1]) [0. 1. 0. 0.] """ w = np.zeros(2 ** len(basis_state)) @@ -537,7 +527,7 @@ def compute_diagonalizing_gates( The diagonalizing gates rotate the state into the eigenbasis of the operator. - .. seealso:: :meth:`~._BasisStateProjector.diagonalizing_gates`. + .. seealso:: :meth:`~.BasisStateProjector.diagonalizing_gates`. Args: basis_state (Iterable): basis state that the operator projects on @@ -547,13 +537,16 @@ def compute_diagonalizing_gates( **Example** - >>> _BasisStateProjector.compute_diagonalizing_gates([0, 1, 0, 0], wires=[0, 1]) + >>> BasisStateProjector.compute_diagonalizing_gates([0, 1, 0, 0], wires=[0, 1]) [] """ return [] -class _StateVectorProjector(Observable): +class StateVectorProjector(Projector): + r"""Observable corresponding to the state projector :math:`P=\ket{\phi}\bra{\phi}`, where + :math:`\phi` denotes a state.""" + # The call signature should be the same as Projector.__new__ for the positional # arguments, but with free key word arguments. def __init__(self, state, wires, id=None): @@ -562,6 +555,9 @@ def __init__(self, state, wires, id=None): super().__init__(state, wires=wires, id=id) + def __new__(cls): # pylint: disable=arguments-differ + return object.__new__(cls) + def label(self, decimals=None, base_label=None, cache=None): r"""A customizable string representation of the operator. @@ -578,14 +574,14 @@ def label(self, decimals=None, base_label=None, cache=None): **Example:** >>> state_vector = np.array([0, 1, 1, 0])/np.sqrt(2) - >>> _StateVectorProjector(state_vector, wires=(0, 1)).label() + >>> StateVectorProjector(state_vector, wires=(0, 1)).label() 'P' - >>> _StateVectorProjector(state_vector, wires=(0, 1)).label(base_label="hi!") + >>> StateVectorProjector(state_vector, wires=(0, 1)).label(base_label="hi!") 'hi!' >>> dev = qml.device("default.qubit", wires=1) >>> @qml.qnode(dev) >>> def circuit(state): - ... return qml.expval(_StateVectorProjector(state, [0])) + ... return qml.expval(StateVectorProjector(state, [0])) >>> print(qml.draw(circuit)([1, 0])) 0: ───┤ <|0⟩⟨0|> >>> print(qml.draw(circuit)(np.array([1, 1]) / np.sqrt(2))) @@ -631,7 +627,7 @@ def compute_matrix(state_vector): # pylint: disable=arguments-differ,arguments- The projector of the state :math:`\frac{1}{\sqrt{2}}(\ket{01}+\ket{10})` - >>> _StateVectorProjector.compute_matrix([0, 1/np.sqrt(2), 1/np.sqrt(2), 0]) + >>> StateVectorProjector.compute_matrix([0, 1/np.sqrt(2), 1/np.sqrt(2), 0]) [[0. 0. 0. 0.] [0. 0.5 0.5 0.] [0. 0.5 0.5 0.] @@ -652,7 +648,7 @@ def compute_eigvals(state_vector): # pylint: disable=arguments-differ,arguments Otherwise, no particular order for the eigenvalues is guaranteed. - .. seealso:: :meth:`~._StateVectorProjector.eigvals` + .. seealso:: :meth:`~.StateVectorProjector.eigvals` Args: state_vector (Iterable): state vector to project on @@ -662,7 +658,7 @@ def compute_eigvals(state_vector): # pylint: disable=arguments-differ,arguments **Example** - >>> _StateVectorProjector.compute_eigvals([0, 0, 1, 0]) + >>> StateVectorProjector.compute_eigvals([0, 0, 1, 0]) array([1, 0, 0, 0]) """ w = qml.math.zeros_like(state_vector) @@ -682,7 +678,7 @@ def compute_diagonalizing_gates( The diagonalizing gates rotate the state into the eigenbasis of the operator. - .. seealso:: :meth:`~._StateVectorProjector.diagonalizing_gates`. + .. seealso:: :meth:`~.StateVectorProjector.diagonalizing_gates`. Args: state_vector (Iterable): state vector that the operator projects on. @@ -693,7 +689,7 @@ def compute_diagonalizing_gates( **Example** >>> state_vector = np.array([1., 1j])/np.sqrt(2) - >>> _StateVectorProjector.compute_diagonalizing_gates(state_vector, wires=[0]) + >>> StateVectorProjector.compute_diagonalizing_gates(state_vector, wires=[0]) [QubitUnitary(array([[ 0.70710678+0.j , 0. -0.70710678j], [ 0. +0.70710678j, -0.70710678+0.j ]]), wires=[0])] """ diff --git a/tests/devices/experimental/test_default_qubit_2.py b/tests/devices/experimental/test_default_qubit_2.py index e381fc63eb8..8beb9a45fff 100644 --- a/tests/devices/experimental/test_default_qubit_2.py +++ b/tests/devices/experimental/test_default_qubit_2.py @@ -1787,6 +1787,26 @@ def test_shot_vectors(self, max_workers, n_qubits, shots): assert np.all(np.logical_or(np.logical_or(r[1] == 0, r[1] == 1), r[1] == 2)) +class TestDynamicType: + """Tests the compatibility with dynamic type classes such as `qml.Projector`.""" + + @pytest.mark.parametrize("n_wires", [1, 2, 3]) + @pytest.mark.parametrize("max_workers", [None, 1, 2]) + def test_projector(self, max_workers, n_wires): + """Test that qml.Projector yields the expected results for both of its subclasses.""" + wires = list(range(n_wires)) + dev = DefaultQubit2(max_workers=max_workers) + ops = [qml.Hadamard(q) for q in wires] + basis_state = np.zeros((n_wires,)) + state_vector = np.zeros((2**n_wires,)) + state_vector[0] = 1 + + for state in [basis_state, state_vector]: + qs = qml.tape.QuantumScript(ops, [qml.expval(qml.Projector(state, wires))]) + res = dev.execute(qs) + assert np.isclose(res, 1 / 2**n_wires) + + @pytest.mark.parametrize("max_workers", [None, 1, 2]) def test_broadcasted_parameter(max_workers): """Test that DefaultQubit2 handles broadcasted parameters as expected.""" diff --git a/tests/gradients/parameter_shift/test_parameter_shift.py b/tests/gradients/parameter_shift/test_parameter_shift.py index 8aa6fe6cfa4..d2e9af06f72 100644 --- a/tests/gradients/parameter_shift/test_parameter_shift.py +++ b/tests/gradients/parameter_shift/test_parameter_shift.py @@ -2183,17 +2183,17 @@ def test_recycling_unshifted_tape_result(self): # + 2 operations x 2 shifted positions + 1 unshifted term <-- assert len(tapes) == (2 * 2 + 1) + (2 * 2 + 1) - def test_projector_variance(self, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector_variance(self, state, tol): """Test that the variance of a projector is correctly returned""" dev = qml.device("default.qubit", wires=2) - P = np.array([1]) x, y = 0.765, -0.654 with qml.queuing.AnnotatedQueue() as q: qml.RX(x, wires=0) qml.RY(y, wires=1) qml.CNOT(wires=[0, 1]) - qml.var(qml.Projector(P, wires=0) @ qml.PauliX(1)) + qml.var(qml.Projector(state, wires=0) @ qml.PauliX(1)) tape = qml.tape.QuantumScript.from_queue(q) tape.trainable_params = {0, 1} @@ -2983,17 +2983,17 @@ def test_expval_and_variance(self, tol): # assert gradA == pytest.approx(expected, abs=tol) # assert gradF == pytest.approx(expected, abs=tol) - def test_projector_variance(self, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector_variance(self, state, tol): """Test that the variance of a projector is correctly returned""" dev = qml.device("default.qubit", wires=2) - P = np.array([1]) x, y = 0.765, -0.654 with qml.queuing.AnnotatedQueue() as q: qml.RX(x, wires=0) qml.RY(y, wires=1) qml.CNOT(wires=[0, 1]) - qml.var(qml.Projector(P, wires=0) @ qml.PauliX(1)) + qml.var(qml.Projector(state, wires=0) @ qml.PauliX(1)) tape = qml.tape.QuantumScript.from_queue(q) tape.trainable_params = {0, 1} diff --git a/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py b/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py index f0e62b76e8f..c673d08cbb6 100644 --- a/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py +++ b/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py @@ -1835,18 +1835,18 @@ def test_expval_and_variance_multi_param(self): for gradF in all_gradF: assert gradF == pytest.approx(expected, abs=finite_diff_tol) - def test_projector_variance(self): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector_variance(self, state): """Test that the variance of a projector is correctly returned""" shot_vec = many_shots_shot_vector dev = qml.device("default.qubit", wires=2, shots=shot_vec) - P = np.array([1]) x, y = 0.765, -0.654 with qml.queuing.AnnotatedQueue() as q: qml.RX(x, wires=0) qml.RY(y, wires=1) qml.CNOT(wires=[0, 1]) - qml.var(qml.Projector(P, wires=0) @ qml.PauliX(1)) + qml.var(qml.Projector(state, wires=0) @ qml.PauliX(1)) tape = qml.tape.QuantumScript.from_queue(q, shots=shot_vec) tape.trainable_params = {0, 1} diff --git a/tests/interfaces/default_qubit_2_integration/test_autograd_qnode_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_autograd_qnode_default_qubit_2.py index 1cefb4e5793..5932332c68d 100644 --- a/tests/interfaces/default_qubit_2_integration/test_autograd_qnode_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_autograd_qnode_default_qubit_2.py @@ -1296,7 +1296,8 @@ def cost_fn(x, y): assert res[1].shape == () assert np.allclose(res, expected, atol=tol, rtol=0) - def test_projector(self, interface, dev, diff_method, grad_on_execution, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, interface, dev, diff_method, grad_on_execution, tol): """Test that the variance of a projector is correctly returned""" kwargs = dict( diff_method=diff_method, interface=interface, grad_on_execution=grad_on_execution @@ -1310,7 +1311,7 @@ def test_projector(self, interface, dev, diff_method, grad_on_execution, tol): elif diff_method == "hadamard": pytest.skip("Hadamard gradient does not support variances.") - P = np.array([1], requires_grad=False) + P = np.array(state, requires_grad=False) x, y = np.array([0.765, -0.654], requires_grad=True) @qnode(dev, **kwargs) diff --git a/tests/interfaces/default_qubit_2_integration/test_jax_jit_qnode_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_jax_jit_qnode_default_qubit_2.py index f0f9dc287b1..c5685328dbf 100644 --- a/tests/interfaces/default_qubit_2_integration/test_jax_jit_qnode_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_jax_jit_qnode_default_qubit_2.py @@ -1230,7 +1230,8 @@ def cost_fn(x, y): expected = np.array([-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2]) assert np.allclose(res, expected, atol=tol, rtol=0) - def test_projector(self, dev, diff_method, grad_on_execution, interface, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, dev, diff_method, grad_on_execution, interface, tol): """Test that the variance of a projector is correctly returned""" gradient_kwargs = {} if diff_method == "adjoint": @@ -1242,7 +1243,7 @@ def test_projector(self, dev, diff_method, grad_on_execution, interface, tol): gradient_kwargs = {"h": H_FOR_SPSA} tol = TOL_FOR_SPSA - P = jax.numpy.array([1]) + P = jax.numpy.array(state) x, y = 0.765, -0.654 @qnode( diff --git a/tests/interfaces/default_qubit_2_integration/test_jax_qnode_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_jax_qnode_default_qubit_2.py index 82193eb33a8..a102b851bae 100644 --- a/tests/interfaces/default_qubit_2_integration/test_jax_qnode_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_jax_qnode_default_qubit_2.py @@ -1151,7 +1151,8 @@ def cost_fn(x, y): expected = np.array([-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2]) assert np.allclose(res, expected, atol=tol, rtol=0) - def test_projector(self, dev, diff_method, grad_on_execution, interface, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, dev, diff_method, grad_on_execution, interface, tol): """Test that the variance of a projector is correctly returned""" gradient_kwargs = {} if diff_method == "adjoint": @@ -1163,7 +1164,7 @@ def test_projector(self, dev, diff_method, grad_on_execution, interface, tol): gradient_kwargs = {"h": H_FOR_SPSA} tol = TOL_FOR_SPSA - P = jax.numpy.array([1]) + P = jax.numpy.array(state) x, y = 0.765, -0.654 @qnode( diff --git a/tests/interfaces/default_qubit_2_integration/test_tensorflow_qnode_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_tensorflow_qnode_default_qubit_2.py index cf0b1fc2b2c..f7d722b3344 100644 --- a/tests/interfaces/default_qubit_2_integration/test_tensorflow_qnode_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_tensorflow_qnode_default_qubit_2.py @@ -908,7 +908,8 @@ def cost_fn(x, y): expected = [-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2] assert np.allclose(grad, expected, atol=tol, rtol=0) - def test_projector(self, dev, diff_method, grad_on_execution, tol, interface): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, dev, diff_method, grad_on_execution, tol, interface): """Test that the variance of a projector is correctly returned""" kwargs = dict( diff_method=diff_method, grad_on_execution=grad_on_execution, interface=interface @@ -922,7 +923,7 @@ def test_projector(self, dev, diff_method, grad_on_execution, tol, interface): kwargs["num_directions"] = 20 tol = TOL_FOR_SPSA - P = tf.constant([1]) + P = tf.constant(state) x, y = 0.765, -0.654 weights = tf.Variable([x, y], dtype=tf.float64) diff --git a/tests/interfaces/default_qubit_2_integration/test_torch_qnode_default_qubit_2.py b/tests/interfaces/default_qubit_2_integration/test_torch_qnode_default_qubit_2.py index 73752759be4..7e7c4fc1de5 100644 --- a/tests/interfaces/default_qubit_2_integration/test_torch_qnode_default_qubit_2.py +++ b/tests/interfaces/default_qubit_2_integration/test_torch_qnode_default_qubit_2.py @@ -1008,7 +1008,8 @@ def cost_fn(x, y): ) assert torch.allclose(res, expected, atol=tol, rtol=0) - def test_projector(self, interface, dev, diff_method, grad_on_execution, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, interface, dev, diff_method, grad_on_execution, tol): """Test that the variance of a projector is correctly returned""" kwargs = dict( diff_method=diff_method, interface=interface, grad_on_execution=grad_on_execution @@ -1022,7 +1023,7 @@ def test_projector(self, interface, dev, diff_method, grad_on_execution, tol): elif diff_method == "hadamard": pytest.skip("Hadamard does not support variances.") - P = torch.tensor([1], requires_grad=False) + P = torch.tensor(state, requires_grad=False) x, y = 0.765, -0.654 weights = torch.tensor([x, y], requires_grad=True, dtype=torch.float64) diff --git a/tests/interfaces/test_autograd_qnode.py b/tests/interfaces/test_autograd_qnode.py index 30d96a8e449..2c2c7f7a98c 100644 --- a/tests/interfaces/test_autograd_qnode.py +++ b/tests/interfaces/test_autograd_qnode.py @@ -1433,7 +1433,8 @@ def cost_fn(x, y): assert res[1].shape == () assert np.allclose(res, expected, atol=tol, rtol=0) - def test_projector(self, interface, dev_name, diff_method, grad_on_execution, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, interface, dev_name, diff_method, grad_on_execution, tol): """Test that the variance of a projector is correctly returned""" kwargs = dict( diff_method=diff_method, interface=interface, grad_on_execution=grad_on_execution @@ -1447,7 +1448,7 @@ def test_projector(self, interface, dev_name, diff_method, grad_on_execution, to pytest.skip("Hadamard gradient does not support variances.") dev = qml.device(dev_name, wires=2) - P = np.array([1], requires_grad=False) + P = np.array(state, requires_grad=False) x, y = np.array([0.765, -0.654], requires_grad=True) @qnode(dev, **kwargs) diff --git a/tests/interfaces/test_jax_jit_qnode.py b/tests/interfaces/test_jax_jit_qnode.py index 4209e9912d1..add55abf1ac 100644 --- a/tests/interfaces/test_jax_jit_qnode.py +++ b/tests/interfaces/test_jax_jit_qnode.py @@ -1368,7 +1368,8 @@ def cost_fn(x, y): expected = np.array([-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2]) assert np.allclose(res, expected, atol=tol, rtol=0) - def test_projector(self, dev_name, diff_method, grad_on_execution, interface, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, dev_name, diff_method, grad_on_execution, interface, tol): """Test that the variance of a projector is correctly returned""" gradient_kwargs = {} if diff_method == "adjoint": @@ -1381,7 +1382,7 @@ def test_projector(self, dev_name, diff_method, grad_on_execution, interface, to tol = TOL_FOR_SPSA dev = qml.device(dev_name, wires=2) - P = jax.numpy.array([1]) + P = jax.numpy.array(state) x, y = 0.765, -0.654 @qnode( diff --git a/tests/interfaces/test_jax_qnode.py b/tests/interfaces/test_jax_qnode.py index 6cb97570ec7..127d467b099 100644 --- a/tests/interfaces/test_jax_qnode.py +++ b/tests/interfaces/test_jax_qnode.py @@ -1315,7 +1315,8 @@ def cost_fn(x, y): expected = np.array([-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2]) assert np.allclose(res, expected, atol=tol, rtol=0) - def test_projector(self, dev_name, diff_method, grad_on_execution, interface, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, dev_name, diff_method, grad_on_execution, interface, tol): """Test that the variance of a projector is correctly returned""" gradient_kwargs = {} if diff_method == "adjoint": @@ -1328,7 +1329,7 @@ def test_projector(self, dev_name, diff_method, grad_on_execution, interface, to tol = TOL_FOR_SPSA dev = qml.device(dev_name, wires=2) - P = jax.numpy.array([1]) + P = jax.numpy.array(state) x, y = 0.765, -0.654 @qnode( diff --git a/tests/interfaces/test_tensorflow_qnode.py b/tests/interfaces/test_tensorflow_qnode.py index 5758dbafc16..e8bca0f901b 100644 --- a/tests/interfaces/test_tensorflow_qnode.py +++ b/tests/interfaces/test_tensorflow_qnode.py @@ -1147,7 +1147,8 @@ def cost_fn(x, y): expected = [-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2] assert np.allclose(grad, expected, atol=tol, rtol=0) - def test_projector(self, dev_name, diff_method, grad_on_execution, tol, interface): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, dev_name, diff_method, grad_on_execution, tol, interface): """Test that the variance of a projector is correctly returned""" kwargs = dict( diff_method=diff_method, grad_on_execution=grad_on_execution, interface=interface @@ -1161,7 +1162,7 @@ def test_projector(self, dev_name, diff_method, grad_on_execution, tol, interfac tol = TOL_FOR_SPSA dev = qml.device(dev_name, wires=2) - P = tf.constant([1]) + P = tf.constant(state) x, y = 0.765, -0.654 weights = tf.Variable([x, y], dtype=tf.float64) diff --git a/tests/interfaces/test_torch_qnode.py b/tests/interfaces/test_torch_qnode.py index c3f95740f36..28bae2e9f2e 100644 --- a/tests/interfaces/test_torch_qnode.py +++ b/tests/interfaces/test_torch_qnode.py @@ -1198,7 +1198,8 @@ def cost_fn(x, y): ) assert torch.allclose(res, expected, atol=tol, rtol=0) - def test_projector(self, interface, dev_name, diff_method, grad_on_execution, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, interface, dev_name, diff_method, grad_on_execution, tol): """Test that the variance of a projector is correctly returned""" kwargs = dict( diff_method=diff_method, grad_on_execution=grad_on_execution, interface=interface @@ -1212,7 +1213,7 @@ def test_projector(self, interface, dev_name, diff_method, grad_on_execution, to pytest.skip("Hadamard does not support variances.") dev = qml.device(dev_name, wires=2) - P = torch.tensor([1], requires_grad=False) + P = torch.tensor(state, requires_grad=False) x, y = 0.765, -0.654 weights = torch.tensor([x, y], requires_grad=True, dtype=torch.float64) diff --git a/tests/legacy/test_legacy_autograd_qnode_old.py b/tests/legacy/test_legacy_autograd_qnode_old.py index f9bdb596618..2800817e2ff 100644 --- a/tests/legacy/test_legacy_autograd_qnode_old.py +++ b/tests/legacy/test_legacy_autograd_qnode_old.py @@ -1206,7 +1206,8 @@ def cost_fn(x, y): expected = np.array([-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2]) assert np.allclose(res, expected, atol=tol, rtol=0) - def test_projector(self, interface, dev_name, diff_method, mode, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, interface, dev_name, diff_method, mode, tol): """Test that the variance of a projector is correctly returned""" kwargs = dict(diff_method=diff_method, interface=interface, mode=mode) if diff_method == "adjoint": @@ -1216,7 +1217,7 @@ def test_projector(self, interface, dev_name, diff_method, mode, tol): tol = TOL_FOR_SPSA dev = qml.device(dev_name, wires=2) - P = np.array([1], requires_grad=False) + P = np.array(state, requires_grad=False) x, y = np.array([0.765, -0.654], requires_grad=True) @qnode(dev, **kwargs) diff --git a/tests/legacy/test_legacy_jax_qnode_old.py b/tests/legacy/test_legacy_jax_qnode_old.py index 9384f55ff01..f44cf4a6db3 100644 --- a/tests/legacy/test_legacy_jax_qnode_old.py +++ b/tests/legacy/test_legacy_jax_qnode_old.py @@ -1041,7 +1041,8 @@ def cost_fn(x, y): expected = np.array([-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2]) assert np.allclose(res, expected, atol=tol, rtol=0) - def test_projector(self, dev_name, diff_method, mode, interface, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, dev_name, diff_method, mode, interface, tol): """Test that the variance of a projector is correctly returned""" kwargs = dict(diff_method=diff_method, interface=interface, mode=mode) if diff_method == "adjoint": @@ -1051,7 +1052,7 @@ def test_projector(self, dev_name, diff_method, mode, interface, tol): tol = TOL_FOR_SPSA dev = qml.device(dev_name, wires=2) - P = jax.numpy.array([1]) + P = jax.numpy.array(state) x, y = 0.765, -0.654 @qnode(dev, **kwargs) diff --git a/tests/legacy/test_legacy_parameter_shift_old.py b/tests/legacy/test_legacy_parameter_shift_old.py index ef54f4725a2..20ffd327452 100644 --- a/tests/legacy/test_legacy_parameter_shift_old.py +++ b/tests/legacy/test_legacy_parameter_shift_old.py @@ -1334,10 +1334,11 @@ def test_recycling_unshifted_tape_result(self): # + 2 operations x 2 shifted positions + 1 unshifted term <-- assert len(tapes) == (2 * 2 + 1) + (2 * 2 + 1) - def test_projector_variance(self, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector_variance(self, state, tol): """Test that the variance of a projector is correctly returned""" dev = qml.device("default.qubit", wires=2) - P = np.array([1]) + P = np.array(state) x, y = 0.765, -0.654 with qml.queuing.AnnotatedQueue() as q: @@ -2058,10 +2059,11 @@ def test_expval_and_variance(self, tol): assert gradF == pytest.approx(expected, abs=tol) """ - def test_projector_variance(self, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector_variance(self, state, tol): """Test that the variance of a projector is correctly returned""" dev = qml.device("default.qubit", wires=2) - P = np.array([1]) + P = np.array(state) x, y = 0.765, -0.654 with qml.queuing.AnnotatedQueue() as q: diff --git a/tests/legacy/test_legacy_tensorflow_qnode_old.py b/tests/legacy/test_legacy_tensorflow_qnode_old.py index d4e3b79f540..42e2a0fb7b9 100644 --- a/tests/legacy/test_legacy_tensorflow_qnode_old.py +++ b/tests/legacy/test_legacy_tensorflow_qnode_old.py @@ -965,7 +965,8 @@ def cost_fn(x, y): expected = [-np.sin(x) * np.cos(y) / 2, -np.cos(x) * np.sin(y) / 2] assert np.allclose(grad, expected, atol=tol, rtol=0) - def test_projector(self, interface, dev_name, diff_method, mode, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, interface, dev_name, diff_method, mode, tol): """Test that the variance of a projector is correctly returned""" kwargs = dict(diff_method=diff_method, interface=interface, mode=mode) if diff_method == "adjoint": @@ -975,7 +976,7 @@ def test_projector(self, interface, dev_name, diff_method, mode, tol): tol = TOL_FOR_SPSA dev = qml.device(dev_name, wires=2) - P = tf.constant([1]) + P = tf.constant(state) x, y = 0.765, -0.654 weights = tf.Variable([x, y], dtype=tf.float64) diff --git a/tests/legacy/test_legacy_torch_qnode_old.py b/tests/legacy/test_legacy_torch_qnode_old.py index eac213e1a3a..16ee1af124a 100644 --- a/tests/legacy/test_legacy_torch_qnode_old.py +++ b/tests/legacy/test_legacy_torch_qnode_old.py @@ -974,7 +974,8 @@ def cost_fn(x, y): ) assert torch.allclose(res, expected, atol=tol, rtol=0) - def test_projector(self, interface, dev_name, diff_method, mode, tol): + @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector + def test_projector(self, state, interface, dev_name, diff_method, mode, tol): """Test that the variance of a projector is correctly returned""" kwargs = dict(diff_method=diff_method, interface=interface, mode=mode) if diff_method == "adjoint": @@ -984,7 +985,7 @@ def test_projector(self, interface, dev_name, diff_method, mode, tol): tol = TOL_FOR_SPSA dev = qml.device(dev_name, wires=2) - P = torch.tensor([1], requires_grad=False) + P = torch.tensor(state, requires_grad=False) x, y = 0.765, -0.654 weights = torch.tensor([x, y], requires_grad=True, dtype=torch.float64) diff --git a/tests/measurements/test_expval.py b/tests/measurements/test_expval.py index efcdc082c26..3d0e5c009b2 100644 --- a/tests/measurements/test_expval.py +++ b/tests/measurements/test_expval.py @@ -149,25 +149,24 @@ def test_shape_shot_vector(self, obs): dev = qml.device("default.qubit", wires=3, shots=shot_vector) assert res.shape(dev, Shots(shot_vector)) == ((), (), ()) + @pytest.mark.parametrize("state", [np.array([0, 0, 0]), np.array([1, 0, 0, 0, 0, 0, 0, 0])]) @pytest.mark.parametrize("shots", [None, 1000, [1000, 10000]]) - def test_projector_expval(self, shots, mocker): - """Tests that the expectation of a ``Projector`` object is computed correctly.""" + def test_projector_expval(self, state, shots, mocker): + """Tests that the expectation of a ``Projector`` object is computed correctly for both of + its subclasses.""" dev = qml.device("default.qubit", wires=3, shots=shots) np.random.seed(42) - basis_state = np.array([0, 0, 0]) - @qml.qnode(dev) def circuit(): qml.Hadamard(0) - return qml.expval(qml.Projector(basis_state, wires=range(3))) + return qml.expval(qml.Projector(state, wires=range(3))) new_dev = circuit.device spy = mocker.spy(qml.QubitDevice, "expval") res = circuit() expected = [0.5, 0.5] if isinstance(shots, list) else 0.5 - assert np.allclose(res, expected, atol=0.02, rtol=0.02) custom_measurement_process(new_dev, spy) diff --git a/tests/measurements/test_var.py b/tests/measurements/test_var.py index e32a08dea86..191e6937cfa 100644 --- a/tests/measurements/test_var.py +++ b/tests/measurements/test_var.py @@ -139,18 +139,17 @@ def test_shape_shot_vector(self, obs): dev = qml.device("default.qubit", wires=3, shots=shot_vector) assert res.shape(dev, Shots(shot_vector)) == ((), (), ()) + @pytest.mark.parametrize("state", [np.array([0, 0, 0]), np.array([1, 0, 0, 0, 0, 0, 0, 0])]) @pytest.mark.parametrize("shots", [None, 1000, [1000, 10000]]) - def test_projector_var(self, shots, mocker): + def test_projector_var(self, state, shots, mocker): """Tests that the variance of a ``Projector`` object is computed correctly.""" dev = qml.device("default.qubit", wires=3, shots=shots) spy = mocker.spy(qml.QubitDevice, "var") - basis_state = np.array([0, 0, 0]) - @qml.qnode(dev) def circuit(): qml.Hadamard(0) - return qml.var(qml.Projector(basis_state, wires=range(3))) + return qml.var(qml.Projector(state, wires=range(3))) res = circuit() expected = [0.25, 0.25] if isinstance(shots, list) else 0.25 diff --git a/tests/ops/qubit/test_observables.py b/tests/ops/qubit/test_observables.py index e030e17014c..a52d6711e2c 100644 --- a/tests/ops/qubit/test_observables.py +++ b/tests/ops/qubit/test_observables.py @@ -14,12 +14,13 @@ """Unit tests for qubit observables.""" # pylint: disable=protected-access import functools +import pickle import pytest import numpy as np from gate_data import I, X, Y, Z, H import pennylane as qml -from pennylane.ops.qubit.observables import _BasisStateProjector, _StateVectorProjector +from pennylane.ops.qubit.observables import BasisStateProjector, StateVectorProjector @pytest.fixture(autouse=True) @@ -467,7 +468,7 @@ def test_basisstate_projector(self): basis_state = [0, 1, 1, 0] wires = range(len(basis_state)) basis_state_projector = qml.Projector(basis_state, wires) - assert isinstance(basis_state_projector, _BasisStateProjector) + assert isinstance(basis_state_projector, BasisStateProjector) second_projector = qml.Projector(basis_state, wires) assert qml.equal(second_projector, basis_state_projector) @@ -477,7 +478,7 @@ def test_statevector_projector(self): state_vector = np.array([1, 1, 1, 1]) / 2 wires = [0, 1] state_vector_projector = qml.Projector(state_vector, wires) - assert isinstance(state_vector_projector, _StateVectorProjector) + assert isinstance(state_vector_projector, StateVectorProjector) second_projector = qml.Projector(state_vector, wires) assert qml.equal(second_projector, state_vector_projector) @@ -518,6 +519,25 @@ def test_exception_bad_input(self): state = np.random.randint(2, size=(2, 4)) qml.Projector(state, range(4)) + def test_serialization(self): + """Tests that Projector is pickle-able.""" + # Basis state projector + proj = qml.Projector([1], wires=[0], id="Timmy") + serialization = pickle.dumps(proj) + new_proj = pickle.loads(serialization) + assert type(new_proj) is type(proj) + assert qml.equal(new_proj, proj) + assert new_proj.id == proj.id # Ensure they are identical + + # State vector projector + proj = qml.Projector([0, 1], wires=[0]) + serialization = pickle.dumps(proj) + new_proj = pickle.loads(serialization) + + assert type(new_proj) is type(proj) + assert qml.equal(new_proj, proj) + assert new_proj.id == proj.id # Ensure they are identical + class TestBasisStateProjector: """Tests for the basis state projector observable.""" @@ -551,7 +571,7 @@ def test_projector_diagonalization(self, basis_state): diag_gates = qml.Projector(basis_state, wires=range(num_wires)).diagonalizing_gates() assert diag_gates == [] - diag_gates_static = _BasisStateProjector.compute_diagonalizing_gates( + diag_gates_static = BasisStateProjector.compute_diagonalizing_gates( basis_state, wires=range(num_wires) ) assert diag_gates_static == [] @@ -612,7 +632,7 @@ def circuit(basis_state): def test_matrix_representation(self, basis_state, expected, n_wires, tol): """Test that the matrix representation is defined correctly""" res_dynamic = qml.Projector(basis_state, wires=range(n_wires)).matrix() - res_static = _BasisStateProjector.compute_matrix(basis_state) + res_static = BasisStateProjector.compute_matrix(basis_state) assert np.allclose(res_dynamic, expected, atol=tol) assert np.allclose(res_static, expected, atol=tol) @@ -637,7 +657,7 @@ def test_projector_diagonalization(self, state_vector, tol): num_wires = np.log2(len(state_vector)).astype(int) proj = qml.Projector(state_vector, wires=range(num_wires)) diag_gates = proj.diagonalizing_gates() - diag_gates_static = _StateVectorProjector.compute_diagonalizing_gates( + diag_gates_static = StateVectorProjector.compute_diagonalizing_gates( state_vector, wires=range(num_wires) ) @@ -654,7 +674,7 @@ def test_matrix_representation(self, state_vector, expected, tol): """Test that the matrix representation is defined correctly""" num_wires = np.log2(len(state_vector)).astype(int) res_dynamic = qml.Projector(state_vector, wires=range(num_wires)).matrix() - res_static = _StateVectorProjector.compute_matrix(state_vector) + res_static = StateVectorProjector.compute_matrix(state_vector) assert np.allclose(res_dynamic, expected, atol=tol) assert np.allclose(res_static, expected, atol=tol)