diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 2c7e8d5883..ab0157ce39 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -28,6 +28,9 @@ ### Bug fixes +* Ensure error raised when asking for out of order marginal probabilities. Prevents the return of incorrect results. +[(#416)](https://github.com/PennyLaneAI/pennylane-lightning/pull/416) + * Fix Github shields in README. [(#402)](https://github.com/PennyLaneAI/pennylane-lightning/pull/402) diff --git a/pennylane_lightning/_serialize.py b/pennylane_lightning/_serialize.py index 0338b7c240..a506b47114 100644 --- a/pennylane_lightning/_serialize.py +++ b/pennylane_lightning/_serialize.py @@ -21,6 +21,7 @@ BasisState, Hadamard, Projector, + Hamiltonian, QubitStateVector, Rot, ) @@ -65,10 +66,13 @@ def _obs_has_kernel(ob: Observable) -> bool: """ if is_pauli_word(ob): return True - if isinstance(ob, (Hadamard, Projector)): + if isinstance(ob, (Hadamard)): return True + if isinstance(ob, Hamiltonian): + return all(_obs_has_kernel(o) for o in ob.ops) if isinstance(ob, Tensor): return all(_obs_has_kernel(o) for o in ob.obs) + return False diff --git a/pennylane_lightning/_version.py b/pennylane_lightning/_version.py index e0b04f640b..e7b9c36e20 100644 --- a/pennylane_lightning/_version.py +++ b/pennylane_lightning/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.29.0-dev9" +__version__ = "0.29.0-dev10" diff --git a/pennylane_lightning/lightning_qubit.py b/pennylane_lightning/lightning_qubit.py index 73c4c8e518..4f72bd77f4 100644 --- a/pennylane_lightning/lightning_qubit.py +++ b/pennylane_lightning/lightning_qubit.py @@ -753,9 +753,19 @@ def probability(self, wires=None, shot_range=None, bin_size=None): # translate to wire labels used by device device_wires = self.map_wires(wires) + if ( + device_wires + and len(device_wires) > 1 + and (not np.all(np.array(device_wires)[:-1] <= np.array(device_wires)[1:])) + ): + raise RuntimeError( + "Lightning does not currently support out-of-order indices for probabilities" + ) + # To support np.complex64 based on the type of self._state dtype = self._state.dtype ket = np.ravel(self._state) + state_vector = StateVectorC64(ket) if self.use_csingle else StateVectorC128(ket) M = MeasuresC64(state_vector) if self.use_csingle else MeasuresC128(state_vector) return M.probs(device_wires) @@ -789,15 +799,10 @@ def expval(self, observable, shot_range=None, bin_size=None): Returns: Expectation value of the observable """ - if ( - (observable.arithmetic_depth > 0) - or isinstance(observable.name, List) - or observable.name - in [ - "Identity", - "Projector", - ] - ): + if observable.name in [ + "Identity", + "Projector", + ]: return super().expval(observable, shot_range=shot_range, bin_size=bin_size) if self.shots is not None: @@ -813,8 +818,11 @@ def expval(self, observable, shot_range=None, bin_size=None): M = MeasuresC64(state_vector) if self.use_csingle else MeasuresC128(state_vector) if observable.name == "SparseHamiltonian": if Kokkos_info()["USE_KOKKOS"] == True: - # converting COO to CSR sparse representation. - CSR_SparseHamiltonian = observable.data[0].tocsr(copy=False) + # ensuring CSR sparse representation. + + CSR_SparseHamiltonian = observable.sparse_matrix(wire_order=self.wires).tocsr( + copy=False + ) return M.expval( CSR_SparseHamiltonian.indptr, CSR_SparseHamiltonian.indices, @@ -824,7 +832,11 @@ def expval(self, observable, shot_range=None, bin_size=None): "The expval of a SparseHamiltonian requires Kokkos and Kokkos Kernels." ) - if observable.name in ["Hamiltonian", "Hermitian"]: + if ( + observable.name in ["Hamiltonian", "Hermitian"] + or (observable.arithmetic_depth > 0) + or isinstance(observable.name, List) + ): ob_serialized = _serialize_ob(observable, self.wire_map, use_csingle=self.use_csingle) return M.expval(ob_serialized) @@ -847,10 +859,9 @@ def var(self, observable, shot_range=None, bin_size=None): Returns: Variance of the observable """ - if isinstance(observable.name, List) or observable.name in [ + if observable.name in [ "Identity", "Projector", - "Hermitian", ]: return super().var(observable, shot_range=shot_range, bin_size=bin_size) @@ -866,6 +877,30 @@ def var(self, observable, shot_range=None, bin_size=None): state_vector = StateVectorC64(ket) if self.use_csingle else StateVectorC128(ket) M = MeasuresC64(state_vector) if self.use_csingle else MeasuresC128(state_vector) + if observable.name == "SparseHamiltonian": + if Kokkos_info()["USE_KOKKOS"] == True: + # ensuring CSR sparse representation. + + CSR_SparseHamiltonian = observable.sparse_matrix(wire_order=self.wires).tocsr( + copy=False + ) + return M.var( + CSR_SparseHamiltonian.indptr, + CSR_SparseHamiltonian.indices, + CSR_SparseHamiltonian.data, + ) + raise NotImplementedError( + "The expval of a SparseHamiltonian requires Kokkos and Kokkos Kernels." + ) + + if ( + observable.name in ["Hamiltonian", "Hermitian"] + or (observable.arithmetic_depth > 0) + or isinstance(observable.name, List) + ): + ob_serialized = _serialize_ob(observable, self.wire_map, use_csingle=self.use_csingle) + return M.var(ob_serialized) + # translate to wire labels used by device observable_wires = self.map_wires(observable.wires) diff --git a/pennylane_lightning/src/bindings/Bindings.cpp b/pennylane_lightning/src/bindings/Bindings.cpp index 92caecefe9..a664ed0bf6 100644 --- a/pennylane_lightning/src/bindings/Bindings.cpp +++ b/pennylane_lightning/src/bindings/Bindings.cpp @@ -87,11 +87,12 @@ void lightning_class_bindings(py::module_ &m) { .def(py::init &>()) .def("probs", [](Measures &M, const std::vector &wires) { - if (wires.empty()) { - return py::array_t(py::cast(M.probs())); - } return py::array_t(py::cast(M.probs(wires))); }) + .def("probs", + [](Measures &M) { + return py::array_t(py::cast(M.probs())); + }) .def("expval", static_cast::*)( const std::string &, const std::vector &)>( @@ -134,10 +135,36 @@ void lightning_class_bindings(py::module_ &m) { strides /* strides for each axis */ )); }) - .def("var", [](Measures &M, const std::string &operation, - const std::vector &wires) { - return M.var(operation, wires); - }); + .def("var", + [](Measures &M, const std::string &operation, + const std::vector &wires) { + return M.var(operation, wires); + }) + .def("var", + static_cast::*)( + const std::string &, const std::vector &)>( + &Measures::var), + "Variance of an operation by name.") + .def( + "var", + [](Measures &M, + const std::shared_ptr> &ob) { + return M.var(*ob); + }, + "Variance of an operation object.") + .def( + "var", + [](Measures &M, const np_arr_sparse_ind row_map, + const np_arr_sparse_ind entries, const np_arr_c values) { + return M.var( + static_cast(row_map.request().ptr), + static_cast(row_map.request().size), + static_cast(entries.request().ptr), + static_cast *>( + values.request().ptr), + static_cast(values.request().size)); + }, + "Expected value of a sparse Hamiltonian."); } template diff --git a/pennylane_lightning/src/simulator/Measures.hpp b/pennylane_lightning/src/simulator/Measures.hpp index be7f86fe36..c0bf75fad5 100644 --- a/pennylane_lightning/src/simulator/Measures.hpp +++ b/pennylane_lightning/src/simulator/Measures.hpp @@ -80,7 +80,9 @@ class Measures { * @return Floating point std::vector with probabilities. * The basis columns are rearranged according to wires. */ - std::vector probs(const std::vector &wires) { + std::vector + probs(const std::vector &wires, + [[maybe_unused]] const std::vector &device_wires = {}) { // Determining index that would sort the vector. // This information is needed later. const auto sorted_ind_wires = Util::sorting_indices(wires); @@ -232,6 +234,26 @@ class Measures { return std::real(inner_prod); } + /** + * @brief Variance value for a general Observable + * + * @param ob Observable + */ + auto var(const Observable &ob) -> fp_t { + // Copying the original state vector, for the application of the + // observable operator. + StateVectorManagedCPU op_sv(original_statevector); + ob.applyInPlace(op_sv); + + const fp_t mean_square = std::real(Util::innerProdC( + op_sv.getData(), op_sv.getData(), op_sv.getLength())); + const fp_t squared_mean = static_cast(std::pow( + std::real(Util::innerProdC(original_statevector.getData(), + op_sv.getData(), op_sv.getLength())), + 2)); + return (mean_square - squared_mean); + } + /** * @brief Variance of an observable. * @@ -309,6 +331,43 @@ class Measures { return expected_value_list; }; + /** + * @brief Variance of a Sparse Hamiltonian. + * + * @tparam index_type integer type used as indices of the sparse matrix. + * @param row_map_ptr row_map array pointer. + * The j element encodes the number of non-zeros above + * row j. + * @param row_map_size row_map array size. + * @param entries_ptr pointer to an array with column indices of the + * non-zero elements. + * @param values_ptr pointer to an array with the non-zero elements. + * @param numNNZ number of non-zero elements. + * @return fp_t Variance value. + */ + template + fp_t var(const index_type *row_map_ptr, const index_type row_map_size, + const index_type *entries_ptr, const CFP_t *values_ptr, + const index_type numNNZ) { + PL_ABORT_IF( + (original_statevector.getLength() != (size_t(row_map_size) - 1)), + "Statevector and Hamiltonian have incompatible sizes."); + auto operator_vector = Util::apply_Sparse_Matrix( + original_statevector.getData(), + static_cast(original_statevector.getLength()), + row_map_ptr, row_map_size, entries_ptr, values_ptr, numNNZ); + + const fp_t mean_square = std::real( + Util::innerProdC(operator_vector.data(), operator_vector.data(), + operator_vector.size())); + const fp_t squared_mean = static_cast( + std::pow(std::real(Util::innerProdC(operator_vector.data(), + original_statevector.getData(), + operator_vector.size())), + 2)); + return (mean_square - squared_mean); + }; + /** * @brief Generate samples using the alias method. * Reference: https://en.wikipedia.org/wiki/Alias_method diff --git a/tests/test_measures.py b/tests/test_measures.py index afc2f1bc2e..16108d6d06 100644 --- a/tests/test_measures.py +++ b/tests/test_measures.py @@ -97,7 +97,6 @@ def circuit(): "cases", [ [[0, 1], [0.9165164490394898, 0.0, 0.08348355096051052, 0.0]], - [[1, 0], [0.9165164490394898, 0.08348355096051052, 0.0, 0.0]], [0, [0.9165164490394898, 0.08348355096051052]], [[0], [0.9165164490394898, 0.08348355096051052]], ], @@ -116,6 +115,54 @@ def circuit(): assert np.allclose(circuit(), cases[1], atol=tol, rtol=0) + @pytest.mark.parametrize( + "cases", + [ + [[0, 1], [1, 0]], + [[1, 0], [0, 1]], + ], + ) + def test_fail_probs_tape_unordered_wires(self, cases, tol): + """Test probs with a circuit on wires=[0] fails for out-of-order wires passed to probs.""" + + x, y, z = [0.5, 0.3, -0.7] + dev = qml.device("lightning.qubit", wires=cases[1]) + + @qml.qnode(dev) + def circuit(): + qml.RX(0.4, wires=[0]) + qml.Rot(x, y, z, wires=[0]) + qml.RY(-0.2, wires=[0]) + return qml.probs(wires=cases[0]) + + with pytest.raises( + RuntimeError, + match="Lightning does not currently support out-of-order indices for probabilities", + ): + _ = circuit() + + @pytest.mark.parametrize( + "cases", + [ + [[1, 0], [1, 0], [0.9165164490394898, 0.08348355096051052, 0.0, 0.0]], + [[2, 0], [2, 0, 1], [0.9165164490394898, 0.08348355096051052, 0.0, 0.0]], + ], + ) + def test_probs_matching_device_wire_order(self, cases, tol): + """Test probs with a circuit on wires=[0] passes if wires are sorted wrt device wires.""" + + x, y, z = [0.5, 0.3, -0.7] + dev = qml.device("lightning.qubit", wires=cases[1]) + + @qml.qnode(dev) + def circuit(): + qml.RX(0.4, wires=[0]) + qml.Rot(x, y, z, wires=[0]) + qml.RY(-0.2, wires=[0]) + return qml.probs(wires=cases[0]) + + assert np.allclose(circuit(), cases[2], atol=tol, rtol=0) + @pytest.mark.parametrize( "cases", [ @@ -128,6 +175,24 @@ def circuit(): 0.0013668981445561978, ], ], + [0, [0.938791280945186, 0.061208719054813635]], + [[0], [0.938791280945186, 0.061208719054813635]], + ], + ) + def test_probs_tape_wire01(self, cases, tol, dev): + """Test probs with a circuit on wires=[0,1]""" + + @qml.qnode(dev) + def circuit(): + qml.RX(0.5, wires=[0]) + qml.RY(0.3, wires=[1]) + return qml.probs(wires=cases[0]) + + assert np.allclose(circuit(), cases[1], atol=tol, rtol=0) + + @pytest.mark.parametrize( + "cases", + [ [ [1, 0], [ @@ -137,11 +202,9 @@ def circuit(): 0.0013668981445561978, ], ], - [0, [0.938791280945186, 0.061208719054813635]], - [[0], [0.938791280945186, 0.061208719054813635]], ], ) - def test_probs_tape_wire01(self, cases, tol, dev): + def test_fail_probs_tape_wire01(self, cases, tol, dev): """Test probs with a circuit on wires=[0,1]""" @qml.qnode(dev) @@ -150,7 +213,11 @@ def circuit(): qml.RY(0.3, wires=[1]) return qml.probs(wires=cases[0]) - assert np.allclose(circuit(), cases[1], atol=tol, rtol=0) + with pytest.raises( + RuntimeError, + match="Lightning does not currently support out-of-order indices for probabilities", + ): + assert np.allclose(circuit(), cases[1], atol=tol, rtol=0) class TestExpval: diff --git a/tests/test_measures_sparse.py b/tests/test_measures_sparse.py index 30226f86f9..27c65ac14c 100644 --- a/tests/test_measures_sparse.py +++ b/tests/test_measures_sparse.py @@ -45,9 +45,7 @@ def circuit(): qml.RY(-0.2, wires=[1]) return qml.expval( qml.SparseHamiltonian( - qml.utils.sparse_hamiltonian( - qml.Hamiltonian([1], [qml.PauliX(0) @ qml.Identity(1)]) - ), + qml.Hamiltonian([1], [qml.PauliX(0) @ qml.Identity(1)]).sparse_matrix(), wires=[0, 1], ) ) @@ -98,7 +96,7 @@ class TestSparseExpvalQChem: symbols, geometry, ) - H_sparse = qml.utils.sparse_hamiltonian(H) + H_sparse = H.sparse_matrix() active_electrons = 1 @@ -114,7 +112,7 @@ def dev(self, request): @pytest.mark.parametrize( "qubits, wires, H_sparse, hf_state, excitations", [ - [qubits, np.arange(qubits), H_sparse, hf_state, excitations], + [qubits, range(qubits), H_sparse, hf_state, excitations], [ qubits, np.random.permutation(np.arange(qubits)), diff --git a/tests/test_serialize.py b/tests/test_serialize.py index 7ec05ea5e7..c8aa9428c4 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -61,29 +61,33 @@ def test_hadamard(self): o = qml.Hadamard(0) assert _obs_has_kernel(o) - def test_projector(self): - """Tests if return is true for a Projector observable""" - o = qml.Projector([0], wires=0) - assert _obs_has_kernel(o) - def test_hermitian(self): """Tests if return is false for a Hermitian observable""" o = qml.Hermitian(np.eye(2), wires=0) assert not _obs_has_kernel(o) def test_tensor_product_of_valid_terms(self): - """Tests if return is true for a tensor product of Pauli, Hadamard, and Projector terms""" - o = qml.PauliZ(0) @ qml.Hadamard(1) @ qml.Projector([0], wires=2) + """Tests if return is true for a tensor product of Pauli, Hadamard, and Hamiltonian terms""" + o = qml.PauliZ(0) @ qml.Hadamard(1) @ (0.1 * (qml.PauliZ(2) + qml.PauliX(3))) assert _obs_has_kernel(o) def test_tensor_product_of_invalid_terms(self): """Tests if return is false for a tensor product of Hermitian terms""" - o = qml.Hermitian(np.eye(2), wires=0) @ qml.Hermitian(np.eye(2), wires=1) + o = ( + qml.Hermitian(np.eye(2), wires=0) + @ qml.Hermitian(np.eye(2), wires=1) + @ qml.Projector([0], wires=2) + ) assert not _obs_has_kernel(o) def test_tensor_product_of_mixed_terms(self): """Tests if return is false for a tensor product of valid and invalid terms""" - o = qml.PauliZ(0) @ qml.Hermitian(np.eye(2), wires=1) + o = qml.PauliZ(0) @ qml.Hermitian(np.eye(2), wires=1) @ qml.Projector([0], wires=2) + assert not _obs_has_kernel(o) + + def test_projector(self): + """Tests if return is false for a Projector observable""" + o = qml.Projector([0], wires=0) assert not _obs_has_kernel(o)