From 0eaa4530487c1a4e3eea1cad2e3be4017aca2a87 Mon Sep 17 00:00:00 2001 From: BorjaRequena Date: Tue, 8 Aug 2023 17:45:08 -0400 Subject: [PATCH 01/13] compatibility with DefaultQubit2 --- pennylane/measurements/expval.py | 4 ++-- pennylane/measurements/var.py | 4 ++-- pennylane/ops/qubit/observables.py | 14 ++++++++++++- .../experimental/test_default_qubit_2.py | 20 +++++++++++++++++++ tests/ops/qubit/test_observables.py | 18 +++++++++++++++++ 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/pennylane/measurements/expval.py b/pennylane/measurements/expval.py index e696546ff56..033f811545b 100644 --- a/pennylane/measurements/expval.py +++ b/pennylane/measurements/expval.py @@ -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, Projector) and len(self.obs.parameters[0]) == len(self.obs.wires): + # 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..126fb95344b 100644 --- a/pennylane/measurements/var.py +++ b/pennylane/measurements/var.py @@ -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, Projector) and len(self.obs.parameters[0]) == len(self.obs.wires): + # 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..6945c328a91 100644 --- a/pennylane/ops/qubit/observables.py +++ b/pennylane/ops/qubit/observables.py @@ -427,13 +427,25 @@ def __copy__(self): return copied_op + def __reduce__(self): + """Defines how to pickle and unpickle a :class:`~.Projector`. Circumbents the pickling + issues caused by its dynamic type: + >>> qml.Projector([1], wires=[0]) is qml.Projector + False + >>> isinstance(qml.Projector([1], wires=[0]), qml.Projector) + True + For more information, see: https://docs.python.org/3/library/pickle.html#object.__reduce__ + """ + state = self.data[0] + return (Projector, (state, self.wires), {"_id": self.id}) + 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}") diff --git a/tests/devices/experimental/test_default_qubit_2.py b/tests/devices/experimental/test_default_qubit_2.py index 8980e15aadd..f586efbb2df 100644 --- a/tests/devices/experimental/test_default_qubit_2.py +++ b/tests/devices/experimental/test_default_qubit_2.py @@ -1760,6 +1760,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` or `qml.Pow`.""" + + @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((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/ops/qubit/test_observables.py b/tests/ops/qubit/test_observables.py index e030e17014c..07e64e37058 100644 --- a/tests/ops/qubit/test_observables.py +++ b/tests/ops/qubit/test_observables.py @@ -14,6 +14,7 @@ """Unit tests for qubit observables.""" # pylint: disable=protected-access import functools +import pickle import pytest import numpy as np @@ -518,6 +519,23 @@ 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="Andy") + serialization = pickle.dumps(proj) + new_proj = pickle.loads(serialization) + 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 qml.equal(new_proj, proj) + assert new_proj.id == proj.id # Ensure they are identical + class TestBasisStateProjector: """Tests for the basis state projector observable.""" From 86860fd510addfc85bd47ecae21274b614d000d5 Mon Sep 17 00:00:00 2001 From: BorjaRequena Date: Tue, 8 Aug 2023 19:28:24 -0400 Subject: [PATCH 02/13] update changelog --- doc/releases/changelog-dev.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index ce09034bb36..01ece9bafd4 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -221,6 +221,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) From 34abc153a777589c3e6b4928ab88fd04810710cb Mon Sep 17 00:00:00 2001 From: BorjaRequena Date: Fri, 11 Aug 2023 09:41:38 -0400 Subject: [PATCH 03/13] testing tests --- pennylane/measurements/expval.py | 4 ++-- pennylane/measurements/var.py | 4 ++-- tests/devices/experimental/test_default_qubit_2.py | 2 +- tests/measurements/test_expval.py | 12 ++++++------ 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pennylane/measurements/expval.py b/pennylane/measurements/expval.py index 033f811545b..e6163b2e8b2 100644 --- a/pennylane/measurements/expval.py +++ b/pennylane/measurements/expval.py @@ -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, Projector) and len(self.obs.parameters[0]) == len(self.obs.wires): + # 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 diff --git a/pennylane/measurements/var.py b/pennylane/measurements/var.py index 126fb95344b..8d9be1c5a4a 100644 --- a/pennylane/measurements/var.py +++ b/pennylane/measurements/var.py @@ -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, Projector) and len(self.obs.parameters[0]) == len(self.obs.wires): + # 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 diff --git a/tests/devices/experimental/test_default_qubit_2.py b/tests/devices/experimental/test_default_qubit_2.py index 6f7f20eb277..7ba24b1fe39 100644 --- a/tests/devices/experimental/test_default_qubit_2.py +++ b/tests/devices/experimental/test_default_qubit_2.py @@ -1788,7 +1788,7 @@ def test_shot_vectors(self, max_workers, n_qubits, shots): class TestDynamicType: - """Tests the compatibility with dynamic type classes such as `qml.Projector` or `qml.Pow`.""" + """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]) diff --git a/tests/measurements/test_expval.py b/tests/measurements/test_expval.py index efcdc082c26..e2ff9ff9d5f 100644 --- a/tests/measurements/test_expval.py +++ b/tests/measurements/test_expval.py @@ -149,25 +149,25 @@ 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))) + res = circuit() 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) From 3cce6630e219154a6f7838e8059fba84815869d1 Mon Sep 17 00:00:00 2001 From: BorjaRequena Date: Fri, 11 Aug 2023 14:26:26 -0400 Subject: [PATCH 04/13] extensive testing --- .../test_autograd_qnode_default_qubit_2.py | 5 +++-- .../test_jax_jit_qnode_default_qubit_2.py | 5 +++-- .../test_jax_qnode_default_qubit_2.py | 5 +++-- .../test_tensorflow_qnode_default_qubit_2.py | 5 +++-- .../test_torch_qnode_default_qubit_2.py | 5 +++-- tests/measurements/test_expval.py | 1 - tests/measurements/test_var.py | 7 +++---- 7 files changed, 18 insertions(+), 15 deletions(-) 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 309c7a94a7a..f14f71f4952 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 @@ -1284,7 +1284,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""" if diff_method == "adjoint": pytest.skip("Adjoint does not support projectors") @@ -1294,7 +1295,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( 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 d01a2154001..4388058935d 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 @@ -1221,7 +1221,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": @@ -1233,7 +1234,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 91db91066c8..7367d276d59 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 @@ -1139,7 +1139,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": @@ -1151,7 +1152,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 5a4c73e1b43..b9892a9257f 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 @@ -902,7 +902,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""" if diff_method == "adjoint": pytest.skip("Adjoint does not support projectors") @@ -912,7 +913,7 @@ def test_projector(self, dev, diff_method, grad_on_execution, tol, interface): np.random.seed(SEED_FOR_SPSA) 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 0c32c72a44b..13790a1269f 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 @@ -1002,7 +1002,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""" if diff_method == "adjoint": pytest.skip("Adjoint does not support projectors") @@ -1012,7 +1013,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/measurements/test_expval.py b/tests/measurements/test_expval.py index e2ff9ff9d5f..3d0e5c009b2 100644 --- a/tests/measurements/test_expval.py +++ b/tests/measurements/test_expval.py @@ -162,7 +162,6 @@ def circuit(): qml.Hadamard(0) return qml.expval(qml.Projector(state, wires=range(3))) - res = circuit() new_dev = circuit.device spy = mocker.spy(qml.QubitDevice, "expval") 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 From 657815d2acb0aa2deca8f484e499d50fa40cf683 Mon Sep 17 00:00:00 2001 From: BorjaRequena Date: Fri, 11 Aug 2023 14:57:20 -0400 Subject: [PATCH 05/13] more tests --- .../parameter_shift/test_parameter_shift.py | 12 ++++++------ .../parameter_shift/test_parameter_shift_shot_vec.py | 6 +++--- tests/interfaces/test_autograd_qnode.py | 5 +++-- tests/interfaces/test_jax_jit_qnode.py | 5 +++-- tests/interfaces/test_jax_qnode.py | 5 +++-- tests/interfaces/test_tensorflow_qnode.py | 5 +++-- tests/interfaces/test_torch_qnode.py | 5 +++-- tests/legacy/test_legacy_autograd_qnode_old.py | 5 +++-- tests/legacy/test_legacy_jax_qnode_old.py | 5 +++-- tests/legacy/test_legacy_parameter_shift_old.py | 10 ++++++---- tests/legacy/test_legacy_tensorflow_qnode_old.py | 5 +++-- tests/legacy/test_legacy_torch_qnode_old.py | 5 +++-- 12 files changed, 42 insertions(+), 31 deletions(-) diff --git a/tests/gradients/parameter_shift/test_parameter_shift.py b/tests/gradients/parameter_shift/test_parameter_shift.py index 86bc90f9216..6e98aec03da 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/test_autograd_qnode.py b/tests/interfaces/test_autograd_qnode.py index a2d3be61d14..c42447fb0e6 100644 --- a/tests/interfaces/test_autograd_qnode.py +++ b/tests/interfaces/test_autograd_qnode.py @@ -1427,7 +1427,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""" if diff_method == "adjoint": pytest.skip("Adjoint does not support projectors") @@ -1438,7 +1439,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( diff --git a/tests/interfaces/test_jax_jit_qnode.py b/tests/interfaces/test_jax_jit_qnode.py index 6e600a63aa9..b49d4e65d65 100644 --- a/tests/interfaces/test_jax_jit_qnode.py +++ b/tests/interfaces/test_jax_jit_qnode.py @@ -1365,7 +1365,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": @@ -1378,7 +1379,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 b52deaac9ec..cc017a5f198 100644 --- a/tests/interfaces/test_jax_qnode.py +++ b/tests/interfaces/test_jax_qnode.py @@ -1304,7 +1304,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": @@ -1317,7 +1318,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 8e7bc6ea451..8ff3eb9bc05 100644 --- a/tests/interfaces/test_tensorflow_qnode.py +++ b/tests/interfaces/test_tensorflow_qnode.py @@ -1144,7 +1144,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""" if diff_method == "adjoint": pytest.skip("Adjoint does not support projectors") @@ -1155,7 +1156,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 12c113b094d..c8543b67710 100644 --- a/tests/interfaces/test_torch_qnode.py +++ b/tests/interfaces/test_torch_qnode.py @@ -1194,7 +1194,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""" if diff_method == "adjoint": pytest.skip("Adjoint does not support projectors") @@ -1205,7 +1206,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 d3f2152f752..1d275ad5275 100644 --- a/tests/legacy/test_legacy_autograd_qnode_old.py +++ b/tests/legacy/test_legacy_autograd_qnode_old.py @@ -1200,7 +1200,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""" if diff_method == "adjoint": pytest.skip("Adjoint does not support projectors") @@ -1209,7 +1210,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, diff_method=diff_method, interface=interface, mode=mode) diff --git a/tests/legacy/test_legacy_jax_qnode_old.py b/tests/legacy/test_legacy_jax_qnode_old.py index b616cafb479..c2453b2309e 100644 --- a/tests/legacy/test_legacy_jax_qnode_old.py +++ b/tests/legacy/test_legacy_jax_qnode_old.py @@ -1033,7 +1033,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""" if diff_method == "adjoint": pytest.skip("Adjoint does not support projectors") @@ -1042,7 +1043,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, diff_method=diff_method, interface=interface, mode=mode) 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 2e0c46fa296..16a7fce43dd 100644 --- a/tests/legacy/test_legacy_tensorflow_qnode_old.py +++ b/tests/legacy/test_legacy_tensorflow_qnode_old.py @@ -962,7 +962,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""" if diff_method == "adjoint": pytest.skip("Adjoint does not support projectors") @@ -971,7 +972,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 1376aadc4e7..6ade3fc4415 100644 --- a/tests/legacy/test_legacy_torch_qnode_old.py +++ b/tests/legacy/test_legacy_torch_qnode_old.py @@ -971,7 +971,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""" if diff_method == "adjoint": pytest.skip("Adjoint does not support projectors") @@ -980,7 +981,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) From 7c066dd1e1e934ddf8390bd227fc03797fab6d06 Mon Sep 17 00:00:00 2001 From: BorjaRequena <59647767+BorjaRequena@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:53:39 -0400 Subject: [PATCH 06/13] Fix `__reduce__` docstring Co-authored-by: Matthew Silverman --- pennylane/ops/qubit/observables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/ops/qubit/observables.py b/pennylane/ops/qubit/observables.py index 6945c328a91..b7fb720e1f4 100644 --- a/pennylane/ops/qubit/observables.py +++ b/pennylane/ops/qubit/observables.py @@ -430,7 +430,7 @@ def __copy__(self): def __reduce__(self): """Defines how to pickle and unpickle a :class:`~.Projector`. Circumbents the pickling issues caused by its dynamic type: - >>> qml.Projector([1], wires=[0]) is qml.Projector + >>> type(qml.Projector([1], wires=[0])) is qml.Projector False >>> isinstance(qml.Projector([1], wires=[0]), qml.Projector) True From 8d966b65515bfca74b33262291c74a0f79b7aa49 Mon Sep 17 00:00:00 2001 From: BorjaRequena <59647767+BorjaRequena@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:53:56 -0400 Subject: [PATCH 07/13] Fix test Co-authored-by: Matthew Silverman --- tests/devices/experimental/test_default_qubit_2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/devices/experimental/test_default_qubit_2.py b/tests/devices/experimental/test_default_qubit_2.py index 7ba24b1fe39..8beb9a45fff 100644 --- a/tests/devices/experimental/test_default_qubit_2.py +++ b/tests/devices/experimental/test_default_qubit_2.py @@ -1798,7 +1798,7 @@ def test_projector(self, max_workers, 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((n_wires,)) + state_vector = np.zeros((2**n_wires,)) state_vector[0] = 1 for state in [basis_state, state_vector]: From 15da63353bdd3d5e0f8268766248bb4b028b746c Mon Sep 17 00:00:00 2001 From: BorjaRequena Date: Mon, 14 Aug 2023 15:59:07 -0400 Subject: [PATCH 08/13] simplify typing --- pennylane/ops/qubit/observables.py | 51 ++++++++++++++--------------- tests/ops/qubit/test_observables.py | 14 ++++---- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/pennylane/ops/qubit/observables.py b/pennylane/ops/qubit/observables.py index b7fb720e1f4..3b35ca757bd 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 " @@ -440,7 +433,9 @@ def __reduce__(self): return (Projector, (state, self.wires), {"_id": self.id}) -class _BasisStateProjector(Observable): +class BasisStateProjector(Projector, Observable): + r"""Observable corresponding to the state projector :math:`P=\ket{\phi}\bra{\phi}`, where + :math:`\phi` denotes a basis 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): @@ -467,7 +462,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|' """ @@ -484,7 +479,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 @@ -494,7 +489,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.] @@ -518,7 +513,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 @@ -528,7 +523,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)) @@ -549,7 +544,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 @@ -559,13 +554,15 @@ 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, Observable): + 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): @@ -590,14 +587,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))) @@ -643,7 +640,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.] @@ -664,7 +661,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 @@ -674,7 +671,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) @@ -694,7 +691,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. @@ -705,7 +702,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/ops/qubit/test_observables.py b/tests/ops/qubit/test_observables.py index 07e64e37058..316927f6c70 100644 --- a/tests/ops/qubit/test_observables.py +++ b/tests/ops/qubit/test_observables.py @@ -20,7 +20,7 @@ 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) @@ -468,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) @@ -478,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) @@ -569,7 +569,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 == [] @@ -630,7 +630,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) @@ -655,7 +655,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) ) @@ -672,7 +672,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) From 888053875326f7891a0d02faa37fa4f751dcb954 Mon Sep 17 00:00:00 2001 From: BorjaRequena Date: Mon, 14 Aug 2023 16:05:55 -0400 Subject: [PATCH 09/13] formatting --- pennylane/ops/qubit/observables.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pennylane/ops/qubit/observables.py b/pennylane/ops/qubit/observables.py index 3b35ca757bd..dc6561708d3 100644 --- a/pennylane/ops/qubit/observables.py +++ b/pennylane/ops/qubit/observables.py @@ -436,6 +436,7 @@ def __reduce__(self): class BasisStateProjector(Projector, Observable): r"""Observable corresponding to the state projector :math:`P=\ket{\phi}\bra{\phi}`, where :math:`\phi` denotes a basis 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): @@ -563,6 +564,7 @@ def compute_diagonalizing_gates( class StateVectorProjector(Projector, Observable): 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): From 7b0711817ac4b3f1fca9e062669e3ec10922970a Mon Sep 17 00:00:00 2001 From: BorjaRequena Date: Mon, 14 Aug 2023 16:12:46 -0400 Subject: [PATCH 10/13] simplify measurement logic --- pennylane/_qubit_device.py | 9 +++------ pennylane/measurements/expval.py | 6 +++--- pennylane/measurements/var.py | 6 +++--- 3 files changed, 9 insertions(+), 12 deletions(-) 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 e6163b2e8b2..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,7 +104,7 @@ def process_samples( shot_range: Tuple[int] = None, bin_size: int = None, ): - if isinstance(self.obs, Projector) and len(self.obs.parameters[0]) == len(self.obs.wires): + 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( @@ -122,7 +122,7 @@ 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) and len(self.obs.parameters[0]) == len(self.obs.wires): + 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) diff --git a/pennylane/measurements/var.py b/pennylane/measurements/var.py index 8d9be1c5a4a..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,7 +104,7 @@ def process_samples( shot_range: Tuple[int] = None, bin_size: int = None, ): - if isinstance(self.obs, Projector) and len(self.obs.parameters[0]) == len(self.obs.wires): + 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 @@ -125,7 +125,7 @@ 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) and len(self.obs.parameters[0]) == len(self.obs.wires): + 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 From 724437ffb27928175de9e3e61e00a57a046affd8 Mon Sep 17 00:00:00 2001 From: BorjaRequena Date: Tue, 15 Aug 2023 11:49:51 -0400 Subject: [PATCH 11/13] serialize without --- pennylane/ops/qubit/observables.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/pennylane/ops/qubit/observables.py b/pennylane/ops/qubit/observables.py index dc6561708d3..7f3185c3d89 100644 --- a/pennylane/ops/qubit/observables.py +++ b/pennylane/ops/qubit/observables.py @@ -420,18 +420,6 @@ def __copy__(self): return copied_op - def __reduce__(self): - """Defines how to pickle and unpickle a :class:`~.Projector`. Circumbents the pickling - issues caused by its dynamic type: - >>> type(qml.Projector([1], wires=[0])) is qml.Projector - False - >>> isinstance(qml.Projector([1], wires=[0]), qml.Projector) - True - For more information, see: https://docs.python.org/3/library/pickle.html#object.__reduce__ - """ - state = self.data[0] - return (Projector, (state, self.wires), {"_id": self.id}) - class BasisStateProjector(Projector, Observable): r"""Observable corresponding to the state projector :math:`P=\ket{\phi}\bra{\phi}`, where @@ -448,6 +436,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. @@ -573,6 +564,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. From 92e2b3204ce01ef3ba9f071ef7566599182c3491 Mon Sep 17 00:00:00 2001 From: BorjaRequena Date: Tue, 15 Aug 2023 13:53:48 -0400 Subject: [PATCH 12/13] remove copy and extend serialization tests --- pennylane/ops/qubit/observables.py | 9 --------- tests/ops/qubit/test_observables.py | 4 +++- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/pennylane/ops/qubit/observables.py b/pennylane/ops/qubit/observables.py index 7f3185c3d89..2a4e066101f 100644 --- a/pennylane/ops/qubit/observables.py +++ b/pennylane/ops/qubit/observables.py @@ -411,15 +411,6 @@ 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, Observable): r"""Observable corresponding to the state projector :math:`P=\ket{\phi}\bra{\phi}`, where diff --git a/tests/ops/qubit/test_observables.py b/tests/ops/qubit/test_observables.py index 316927f6c70..a52d6711e2c 100644 --- a/tests/ops/qubit/test_observables.py +++ b/tests/ops/qubit/test_observables.py @@ -522,9 +522,10 @@ def test_exception_bad_input(self): def test_serialization(self): """Tests that Projector is pickle-able.""" # Basis state projector - proj = qml.Projector([1], wires=[0], id="Andy") + 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 @@ -533,6 +534,7 @@ def test_serialization(self): 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 From e624c3292ab8e9a08b970b751696e2962b8a5459 Mon Sep 17 00:00:00 2001 From: BorjaRequena Date: Tue, 15 Aug 2023 16:48:54 -0400 Subject: [PATCH 13/13] remove redundant inheritance --- pennylane/ops/qubit/observables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane/ops/qubit/observables.py b/pennylane/ops/qubit/observables.py index 2a4e066101f..823e2250755 100644 --- a/pennylane/ops/qubit/observables.py +++ b/pennylane/ops/qubit/observables.py @@ -412,7 +412,7 @@ def pow(self, z): return [copy(self)] if (isinstance(z, int) and z > 0) else super().pow(z) -class BasisStateProjector(Projector, Observable): +class BasisStateProjector(Projector): r"""Observable corresponding to the state projector :math:`P=\ket{\phi}\bra{\phi}`, where :math:`\phi` denotes a basis state.""" @@ -543,7 +543,7 @@ def compute_diagonalizing_gates( return [] -class StateVectorProjector(Projector, Observable): +class StateVectorProjector(Projector): r"""Observable corresponding to the state projector :math:`P=\ket{\phi}\bra{\phi}`, where :math:`\phi` denotes a state."""