From aebe7fcc0e7a6a3d423faf03b19829bc81109d6a Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 17 Sep 2024 14:38:09 -0400 Subject: [PATCH 01/14] Add logic to verify that csc_dot_product is usable --- pennylane/devices/qubit/measure.py | 22 ++++++++++++++++++++-- pennylane/operation.py | 12 ++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/pennylane/devices/qubit/measure.py b/pennylane/devices/qubit/measure.py index ae2ddb07c02..9547945d36a 100644 --- a/pennylane/devices/qubit/measure.py +++ b/pennylane/devices/qubit/measure.py @@ -18,6 +18,7 @@ from scipy.sparse import csr_matrix +import pennylane as qml from pennylane import math from pennylane.measurements import ( ExpectationMP, @@ -195,13 +196,30 @@ def get_measurement_function( backprop_mode = math.get_interface(state, *measurementprocess.obs.data) != "numpy" if isinstance(measurementprocess.obs, (Hamiltonian, LinearCombination)): - # need to work out thresholds for when its faster to use "backprop mode" measurements - return sum_of_terms_method if backprop_mode else csr_dot_products + + # need to work out thresholds for when it's faster to use "backprop mode" + if backprop_mode: + return sum_of_terms_method + + if not all(obs.has_sparse_matrix for obs in measurementprocess.obs.terms()[1]): + return sum_of_terms_method + + if isinstance(measurementprocess.obs, Hamiltonian) and any( + any(len(o.wires) > 1 for o in qml.operation.Tensor(op).obs) + for op in measurementprocess.obs.ops + ): + return sum_of_terms_method + + return csr_dot_products if isinstance(measurementprocess.obs, Sum): if backprop_mode: # always use sum_of_terms_method for Sum observables in backprop mode return sum_of_terms_method + + if not all(obs.has_sparse_matrix() for _, obs in measurementprocess.obs.terms()): + return sum_of_terms_method + if ( measurementprocess.obs.has_overlapping_wires and len(measurementprocess.obs.wires) > 7 diff --git a/pennylane/operation.py b/pennylane/operation.py index 77f989e6383..46a8d4e5bb7 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -870,6 +870,18 @@ def compute_sparse_matrix( """ raise SparseMatrixUndefinedError + # pylint: disable=no-self-argument, comparison-with-callable + @classproperty + def has_sparse_matrix(cls) -> bool: + r"""Bool: Whether the Operator returns a defined sparse matrix. + + Note: Child classes may have this as an instance property instead of as a class property. + """ + return ( + cls.compute_sparse_matrix != Operator.compute_sparse_matrix + or cls.sparse_matrix != Operator.sparse_matrix + ) + def sparse_matrix(self, wire_order: Optional[WiresLike] = None) -> csr_matrix: r"""Representation of the operator as a sparse matrix in the computational basis. From 9789d8b9da41a794ed10c502cc2a49279bfdc4ca Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 17 Sep 2024 15:25:58 -0400 Subject: [PATCH 02/14] make pylint happy --- pennylane/devices/qubit/measure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/devices/qubit/measure.py b/pennylane/devices/qubit/measure.py index 9547945d36a..072797c5a9a 100644 --- a/pennylane/devices/qubit/measure.py +++ b/pennylane/devices/qubit/measure.py @@ -169,7 +169,7 @@ def sum_of_terms_method( ) -# pylint: disable=too-many-return-statements +# pylint: disable=too-many-return-statements,too-many-branches def get_measurement_function( measurementprocess: MeasurementProcess, state: TensorLike ) -> Callable[[MeasurementProcess, TensorLike], TensorLike]: From ec212afd22126f30c920b34738740f66754ccb3e Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 17 Sep 2024 15:38:50 -0400 Subject: [PATCH 03/14] fix bug --- pennylane/devices/qubit/measure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/devices/qubit/measure.py b/pennylane/devices/qubit/measure.py index 072797c5a9a..cf26eab6a32 100644 --- a/pennylane/devices/qubit/measure.py +++ b/pennylane/devices/qubit/measure.py @@ -217,7 +217,7 @@ def get_measurement_function( # always use sum_of_terms_method for Sum observables in backprop mode return sum_of_terms_method - if not all(obs.has_sparse_matrix() for _, obs in measurementprocess.obs.terms()): + if not all(obs.has_sparse_matrix for obs in measurementprocess.obs): return sum_of_terms_method if ( From 167baae126497f45cdd5d2e0ff438255130e8514 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 18 Sep 2024 09:50:35 -0400 Subject: [PATCH 04/14] add coverage --- pennylane/ops/op_math/prod.py | 4 +++ pennylane/ops/op_math/sprod.py | 4 +++ tests/devices/qubit/test_measure.py | 44 +++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/pennylane/ops/op_math/prod.py b/pennylane/ops/op_math/prod.py index 1bbe30e29f2..284e193d9e2 100644 --- a/pennylane/ops/op_math/prod.py +++ b/pennylane/ops/op_math/prod.py @@ -324,6 +324,10 @@ def sparse_matrix(self, wire_order=None): full_mat = reduce(sparse_kron, mats) return math.expand_matrix(full_mat, self.wires, wire_order=wire_order) + @property + def has_sparse_matrix(self): + return self.pauli_rep is not None or all(op.has_sparse_matrix for op in self) + # pylint: disable=protected-access @property def _queue_category(self): diff --git a/pennylane/ops/op_math/sprod.py b/pennylane/ops/op_math/sprod.py index b3b2a9cc96f..3ab57061c19 100644 --- a/pennylane/ops/op_math/sprod.py +++ b/pennylane/ops/op_math/sprod.py @@ -263,6 +263,10 @@ def sparse_matrix(self, wire_order=None): mat.eliminate_zeros() return mat + @property + def has_sparse_matrix(self): + return self.pauli_rep is not None or self.base.has_sparse_matrix + @property def has_matrix(self): """Bool: Whether or not the Operator returns a defined matrix.""" diff --git a/tests/devices/qubit/test_measure.py b/tests/devices/qubit/test_measure.py index 47e4d8c2a31..cc5b5f44e46 100644 --- a/tests/devices/qubit/test_measure.py +++ b/tests/devices/qubit/test_measure.py @@ -39,6 +39,7 @@ def test_sample_based_observable(self): _ = measure(qml.sample(wires=0), state) +@pytest.mark.unit class TestMeasurementDispatch: """Test that get_measurement_function dispatchs to the correct place.""" @@ -96,6 +97,49 @@ def test_sum_sum_of_terms_when_backprop(self): state = qml.numpy.zeros(2) assert get_measurement_function(qml.expval(S), state) is sum_of_terms_method + @pytest.mark.usefixtures("use_legacy_opmath") + def test_hamiltonian_with_multi_wire_obs(self): + """Check that a Hamiltonian with a multi-wire observable uses the sum of terms method.""" + + S = qml.Hamiltonian( + [0.5, 0.5], + [ + qml.X(0), + qml.Hermitian( + np.array( + [ + [0.5, 1.0j, 0.0, -3j], + [-1.0j, -1.1, 0.0, -0.1], + [0.0, 0.0, -0.9, 12.0], + [3j, -0.1, 12.0, 0.0], + ] + ), + wires=[0, 1], + ), + ], + ) + state = np.zeros(2) + assert get_measurement_function(qml.expval(S), state) is sum_of_terms_method + + def test_no_sparse_matrix(self): + """Tests that Hamiltonians/Sums containing observables that does not have sparse matrix.""" + + class DummyOp(qml.operation.Operator): # pylint: disable=too-few-public-methods + num_wires = 1 + + S1 = qml.Hamiltonian([0.5, 0.5], [qml.X(0), DummyOp(wires=1)]) + state = np.zeros(2) + assert get_measurement_function(qml.expval(S1), state) is sum_of_terms_method + + S2 = qml.X(0) + DummyOp(wires=1) + assert get_measurement_function(qml.expval(S2), state) is sum_of_terms_method + + S3 = 0.5 * qml.X(0) + 0.5 * DummyOp(wires=1) + assert get_measurement_function(qml.expval(S3), state) is sum_of_terms_method + + S4 = qml.Y(0) + qml.X(0) @ DummyOp(wires=1) + assert get_measurement_function(qml.expval(S4), state) is sum_of_terms_method + class TestMeasurements: @pytest.mark.parametrize( From 213e6b93c491749fae12d897f334ff846545d6ec Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 19 Sep 2024 10:26:44 -0400 Subject: [PATCH 05/14] add tests and has_sparse_matrix to Sum --- pennylane/ops/functions/assert_valid.py | 19 +++++++++++++++++++ pennylane/ops/op_math/sum.py | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index e3795ff6608..4309148e76f 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -21,6 +21,7 @@ from string import ascii_lowercase import numpy as np +import scipy.sparse import pennylane as qml from pennylane.operation import EigvalsUndefinedError @@ -111,6 +112,23 @@ def _check_matrix(op): op.matrix, qml.operation.MatrixUndefinedError, failure_comment=failure_comment )() +def _check_sparse_matrix(op): + """Check that if the operation says it has a sparse matrix, it does. Otherwise a ``SparseMatrixUndefinedError`` should be raised.""" + if op.has_sparse_matrix: + mat = op.sparse_matrix() + assert isinstance(mat, scipy.sparse.csc_matrix), "matrix must be a TensorLike" + l = 2 ** len(op.wires) + failure_comment = f"matrix must be two dimensional with shape ({l}, {l})" + assert qml.math.shape(mat) == (l, l), failure_comment + else: + failure_comment = ( + "If has_sparse_matrix is False, the matrix method must raise a ``SparseMatrixUndefinedError``." + ) + _assert_error_raised( + op.matrix, qml.operation.SparseMatrixUndefinedError, failure_comment=failure_comment + )() + + def _check_matrix_matches_decomp(op): """Check that if both the matrix and decomposition are defined, they match.""" @@ -331,6 +349,7 @@ def __init__(self, wires): _check_decomposition(op, skip_wire_mapping) _check_matrix(op) + _check_sparse_matrix(op) _check_matrix_matches_decomp(op) _check_eigendecomposition(op) _check_capture(op) diff --git a/pennylane/ops/op_math/sum.py b/pennylane/ops/op_math/sum.py index f585607da20..d3e7abe81df 100644 --- a/pennylane/ops/op_math/sum.py +++ b/pennylane/ops/op_math/sum.py @@ -342,6 +342,10 @@ def matrix(self, wire_order=None): return math.expand_matrix(reduced_mat, sum_wires, wire_order=wire_order) + @property + def has_sparse_matrix(self) -> bool: + return self.pauli_rep is not None or all(op.has_sparse_matrix for op in self) + def sparse_matrix(self, wire_order=None): if self.pauli_rep: # Get the sparse matrix from the PauliSentence representation return self.pauli_rep.to_mat(wire_order=wire_order or self.wires, format="csr") From fe0048f85668108b7b1f553b7cdddfe488c59cae Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 19 Sep 2024 10:28:07 -0400 Subject: [PATCH 06/14] make pylint happy --- pennylane/ops/op_math/sum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/ops/op_math/sum.py b/pennylane/ops/op_math/sum.py index d3e7abe81df..ca4444d76fe 100644 --- a/pennylane/ops/op_math/sum.py +++ b/pennylane/ops/op_math/sum.py @@ -343,7 +343,7 @@ def matrix(self, wire_order=None): return math.expand_matrix(reduced_mat, sum_wires, wire_order=wire_order) @property - def has_sparse_matrix(self) -> bool: + def has_sparse_matrix(self) -> bool: # pylint: disable=arguments-renamed return self.pauli_rep is not None or all(op.has_sparse_matrix for op in self) def sparse_matrix(self, wire_order=None): From fe5e84b9512687e27252b2c42c3f15ec14085f5c Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 19 Sep 2024 10:35:13 -0400 Subject: [PATCH 07/14] fix typo --- pennylane/ops/functions/assert_valid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index 4309148e76f..dce5b179366 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -116,7 +116,7 @@ def _check_sparse_matrix(op): """Check that if the operation says it has a sparse matrix, it does. Otherwise a ``SparseMatrixUndefinedError`` should be raised.""" if op.has_sparse_matrix: mat = op.sparse_matrix() - assert isinstance(mat, scipy.sparse.csc_matrix), "matrix must be a TensorLike" + assert isinstance(mat, scipy.sparse.csr_matrix), "matrix must be a TensorLike" l = 2 ** len(op.wires) failure_comment = f"matrix must be two dimensional with shape ({l}, {l})" assert qml.math.shape(mat) == (l, l), failure_comment From fb5ede560b81a518d9aa14e587ce41b1c525ac64 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 19 Sep 2024 10:50:25 -0400 Subject: [PATCH 08/14] make pylint happy --- pennylane/ops/op_math/sum.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pennylane/ops/op_math/sum.py b/pennylane/ops/op_math/sum.py index ca4444d76fe..c70adadfbcb 100644 --- a/pennylane/ops/op_math/sum.py +++ b/pennylane/ops/op_math/sum.py @@ -342,8 +342,9 @@ def matrix(self, wire_order=None): return math.expand_matrix(reduced_mat, sum_wires, wire_order=wire_order) + # pylint: disable=arguments-renamed, invalid-overridden-method @property - def has_sparse_matrix(self) -> bool: # pylint: disable=arguments-renamed + def has_sparse_matrix(self) -> bool: return self.pauli_rep is not None or all(op.has_sparse_matrix for op in self) def sparse_matrix(self, wire_order=None): From 33b716f153a1e0385a9d9ac07df7a4a9faca679e Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 19 Sep 2024 10:51:15 -0400 Subject: [PATCH 09/14] make black happy --- pennylane/ops/functions/assert_valid.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index dce5b179366..26936f018d1 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -112,6 +112,7 @@ def _check_matrix(op): op.matrix, qml.operation.MatrixUndefinedError, failure_comment=failure_comment )() + def _check_sparse_matrix(op): """Check that if the operation says it has a sparse matrix, it does. Otherwise a ``SparseMatrixUndefinedError`` should be raised.""" if op.has_sparse_matrix: @@ -121,15 +122,12 @@ def _check_sparse_matrix(op): failure_comment = f"matrix must be two dimensional with shape ({l}, {l})" assert qml.math.shape(mat) == (l, l), failure_comment else: - failure_comment = ( - "If has_sparse_matrix is False, the matrix method must raise a ``SparseMatrixUndefinedError``." - ) + failure_comment = "If has_sparse_matrix is False, the matrix method must raise a ``SparseMatrixUndefinedError``." _assert_error_raised( op.matrix, qml.operation.SparseMatrixUndefinedError, failure_comment=failure_comment )() - def _check_matrix_matches_decomp(op): """Check that if both the matrix and decomposition are defined, they match.""" if op.has_matrix and op.has_decomposition: From 796fb7a47c28100717cb369474c8a48b723ed25c Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 19 Sep 2024 10:58:36 -0400 Subject: [PATCH 10/14] oooops --- pennylane/ops/functions/assert_valid.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index 26936f018d1..1d5dd3e036b 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -124,7 +124,9 @@ def _check_sparse_matrix(op): else: failure_comment = "If has_sparse_matrix is False, the matrix method must raise a ``SparseMatrixUndefinedError``." _assert_error_raised( - op.matrix, qml.operation.SparseMatrixUndefinedError, failure_comment=failure_comment + op.sparse_matrix, + qml.operation.SparseMatrixUndefinedError, + failure_comment=failure_comment, )() From 8decdbba25f4a0b6fd0d2184cd16784cffbb5cbe Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 19 Sep 2024 11:10:46 -0400 Subject: [PATCH 11/14] oooops --- pennylane/ops/op_math/adjoint.py | 5 +++++ pennylane/ops/op_math/pow.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/pennylane/ops/op_math/adjoint.py b/pennylane/ops/op_math/adjoint.py index 325b80c1b7e..e09ea4b007d 100644 --- a/pennylane/ops/op_math/adjoint.py +++ b/pennylane/ops/op_math/adjoint.py @@ -367,6 +367,11 @@ def matrix(self, wire_order=None): return moveaxis(conj(base_matrix), -2, -1) + # pylint: disable=arguments-renamed, invalid-overridden-method + @property + def has_sparse_matrix(self) -> bool: + return self.base.has_sparse_matrix + # pylint: disable=arguments-differ def sparse_matrix(self, wire_order=None, format="csr"): base_matrix = self.base.sparse_matrix(wire_order=wire_order) diff --git a/pennylane/ops/op_math/pow.py b/pennylane/ops/op_math/pow.py index 6251fb6fdbb..f8ad370d81f 100644 --- a/pennylane/ops/op_math/pow.py +++ b/pennylane/ops/op_math/pow.py @@ -246,6 +246,11 @@ def _matrix(scalar, mat): return fractional_matrix_power(mat, scalar) + # pylint: disable=arguments-renamed, invalid-overridden-method + @property + def has_sparse_matrix(self) -> bool: + return self.base.has_sparse_matrix and isinstance(self.z, int) + # pylint: disable=arguments-differ @staticmethod def compute_sparse_matrix(*params, base=None, z=0): From 3954d738a7ac69b3ba26f2e44dbd3cec6d9e426b Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 19 Sep 2024 11:23:57 -0400 Subject: [PATCH 12/14] change orders --- pennylane/ops/functions/assert_valid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index 1d5dd3e036b..09a0821cb35 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -349,7 +349,7 @@ def __init__(self, wires): _check_decomposition(op, skip_wire_mapping) _check_matrix(op) - _check_sparse_matrix(op) _check_matrix_matches_decomp(op) + _check_sparse_matrix(op) _check_eigendecomposition(op) _check_capture(op) From 1f9832f31a779e202b67876367be9a2a4f7d3630 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 19 Sep 2024 15:27:31 -0400 Subject: [PATCH 13/14] Update tests/devices/qubit/test_measure.py Co-authored-by: Pietropaolo Frisoni --- tests/devices/qubit/test_measure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/devices/qubit/test_measure.py b/tests/devices/qubit/test_measure.py index cc5b5f44e46..e764ede4616 100644 --- a/tests/devices/qubit/test_measure.py +++ b/tests/devices/qubit/test_measure.py @@ -122,7 +122,7 @@ def test_hamiltonian_with_multi_wire_obs(self): assert get_measurement_function(qml.expval(S), state) is sum_of_terms_method def test_no_sparse_matrix(self): - """Tests that Hamiltonians/Sums containing observables that does not have sparse matrix.""" + """Tests Hamiltonians/Sums containing observables that do not have a sparse matrix.""" class DummyOp(qml.operation.Operator): # pylint: disable=too-few-public-methods num_wires = 1 From f8a971f759744a748e0af6642c7df9a64abcaab2 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Thu, 19 Sep 2024 15:28:47 -0400 Subject: [PATCH 14/14] Update pennylane/devices/qubit/measure.py --- pennylane/devices/qubit/measure.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pennylane/devices/qubit/measure.py b/pennylane/devices/qubit/measure.py index cf26eab6a32..da4d51b7aec 100644 --- a/pennylane/devices/qubit/measure.py +++ b/pennylane/devices/qubit/measure.py @@ -204,6 +204,7 @@ def get_measurement_function( if not all(obs.has_sparse_matrix for obs in measurementprocess.obs.terms()[1]): return sum_of_terms_method + # Hamiltonian.sparse_matrix raises a ValueError for this scenario. if isinstance(measurementprocess.obs, Hamiltonian) and any( any(len(o.wires) > 1 for o in qml.operation.Tensor(op).obs) for op in measurementprocess.obs.ops