Skip to content

Commit 461f73b

Browse files
Add shot expval in C++ layer (#556)
* init commit with PauliX,Y,Z, Hardmard support * add unit tests & sample * add PauliX, Y, Z & Hadamard support * move expval shots to base class * Auto update version * add MPI support * add samples_to_counts to measurement base class * tidy up tests * move tests to measurement base * move mpi tests to measurement base class * make format * add python layer * add mpi tests * remove else * add tensor product support * add more tests for tensor prod obs shots * make format * add Hamiltonian support * make format * quick fix * add hamiltonian tests * add more py tests for tensorprod * remove changes made in py layer * revert more changes in py layer * tidy up code * update measurement base and kokkos * make format * refactor and add _sample_state method * for loop sum to accumulate sum * refactor and add _preprocess_state() method * add more unit tests for LK backend * Auto update version * tidy up docstring * remove samples() and samples2counts method * fix typo * quick fix * update LQRaw object creation * fix for multiple backends * update format * format fix * format update * make format & trigger MPI CI * add changelog and codecov for expval(obs) method * add shot_range coverage * tidy up code * add more unit tests * fix typos * add MPI Hamiltonian tests for shots * add HamBase tests for applyInPlaceShots * update based on comments * add more unit tests * move tests for non-distributed backends to base * move all unit tests into the base class * move getTotalNumQubits to base and tidy up code * tidy up code * revert changes in Test_MeasurementLQ * fix typo
1 parent bf6e121 commit 461f73b

20 files changed

+1103
-16
lines changed

.github/CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
### New features since last release
44

5+
* Add shots support for expectation value calculation for given observables (`NamedObs`, `TensorProd` and `Hamiltonian`) based on Pauli words, `Identity` and `Hadamard` in the C++ layer by adding `measure_with_samples` to the measurement interface. All Lightning backends support this support feature.
6+
[(#556)](https://github.com/PennyLaneAI/pennylane-lightning/pull/556)
7+
58
* `qml.QubitUnitary` operators can be included in a circuit differentiated with the adjoint method. Lightning handles circuits with arbitrary non-differentiable `qml.QubitUnitary` operators. 1,2-qubit `qml.QubitUnitary` operators with differentiable parameters can be differentiated using decomposition.
69
[(#540)] (https://github.com/PennyLaneAI/pennylane-lightning/pull/540)
710

mpitests/test_expval.py

-13
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ class TestExpval:
3131
def test_identity_expectation(self, theta, phi, tol):
3232
"""Test that identity expectation value (i.e. the trace) is 1"""
3333
dev = qml.device(device_name, mpi=True, wires=3)
34-
if device_name == "lightning.gpu" and dev.R_DTYPE == np.float32:
35-
pytest.skip("Skipped FP32 tests for expval in lightning.gpu")
3634

3735
O1 = qml.Identity(wires=[0])
3836
O2 = qml.Identity(wires=[1])
@@ -49,9 +47,6 @@ def test_pauliz_expectation(self, theta, phi, tol):
4947
"""Test that PauliZ expectation value is correct"""
5048
dev = qml.device(device_name, mpi=True, wires=3)
5149

52-
if device_name == "lightning.gpu" and dev.R_DTYPE == np.float32:
53-
pytest.skip("Skipped FP32 tests for expval in lightning.gpu")
54-
5550
O1 = qml.PauliZ(wires=[0])
5651
O2 = qml.PauliZ(wires=[1])
5752

@@ -67,9 +62,6 @@ def test_paulix_expectation(self, theta, phi, tol):
6762
"""Test that PauliX expectation value is correct"""
6863
dev = qml.device(device_name, mpi=True, wires=3)
6964

70-
if device_name == "lightning.gpu" and dev.R_DTYPE == np.float32:
71-
pytest.skip("Skipped FP32 tests for expval in lightning.gpu")
72-
7365
O1 = qml.PauliX(wires=[0])
7466
O2 = qml.PauliX(wires=[1])
7567

@@ -89,9 +81,6 @@ def test_pauliy_expectation(self, theta, phi, tol):
8981
"""Test that PauliY expectation value is correct"""
9082
dev = qml.device(device_name, mpi=True, wires=3)
9183

92-
if device_name == "lightning.gpu" and dev.R_DTYPE == np.float32:
93-
pytest.skip("Skipped FP32 tests for expval in lightning.gpu")
94-
9584
O1 = qml.PauliY(wires=[0])
9685
O2 = qml.PauliY(wires=[1])
9786

@@ -130,8 +119,6 @@ def test_hermitian_expectation(self, n_wires, theta, phi, tol):
130119
n_qubits = 7
131120
dev_def = qml.device("default.qubit", wires=n_qubits)
132121
dev = qml.device(device_name, mpi=True, wires=n_qubits)
133-
if device_name == "lightning.gpu" and dev.R_DTYPE == np.float32:
134-
pytest.skip("Skipped FP32 tests for expval in lightning.gpu")
135122
comm = MPI.COMM_WORLD
136123

137124
m = 2**n_wires

pennylane_lightning/core/_version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
Version number (major.minor.patch[-label])
1717
"""
1818

19-
__version__ = "0.34.0-dev7"
19+
__version__ = "0.34.0-dev8"

pennylane_lightning/core/src/measurements/MeasurementsBase.hpp

+189-2
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
*/
1818
#pragma once
1919

20+
#include <string>
2021
#include <vector>
2122

2223
#include "Observables.hpp"
2324

25+
#include "CPUMemoryModel.hpp"
26+
2427
/// @cond DEV
2528
namespace {
2629
using namespace Pennylane::Observables;
@@ -111,6 +114,190 @@ template <class StateVectorT, class Derived> class MeasurementsBase {
111114
auto generate_samples(size_t num_samples) -> std::vector<size_t> {
112115
return static_cast<Derived *>(this)->generate_samples(num_samples);
113116
};
114-
};
115117

116-
} // namespace Pennylane::Measures
118+
/**
119+
* @brief Calculate the expectation value for a general Observable.
120+
*
121+
* @param obs Observable.
122+
* @param num_shots Number of shots used to generate samples
123+
* @param shot_range The range of samples to use. All samples are used
124+
* by default.
125+
*
126+
* @return Expectation value with respect to the given observable.
127+
*/
128+
auto expval(const Observable<StateVectorT> &obs, const size_t &num_shots,
129+
const std::vector<size_t> &shot_range = {}) -> PrecisionT {
130+
PrecisionT result{0.0};
131+
132+
if (obs.getObsName().find("SparseHamiltonian") != std::string::npos) {
133+
// SparseHamiltonian does not support samples in pennylane.
134+
PL_ABORT("For SparseHamiltonian Observables, expval calculation is "
135+
"not supported by shots");
136+
} else if (obs.getObsName().find("Hermitian") != std::string::npos) {
137+
// TODO support. This support requires an additional method to solve
138+
// eigenpair and unitary matrices, and the results of eigenpair and
139+
// unitary matrices data need to be added to the Hermitian class and
140+
// public methods are need to access eigen values. Note the
141+
// assumption that eigen values are -1 and 1 in the
142+
// `measurement_with_sample` method should be updated as well.
143+
PL_ABORT("For Hermitian Observables, expval calculation is not "
144+
"supported by shots");
145+
} else if (obs.getObsName().find("Hamiltonian") != std::string::npos) {
146+
auto coeffs = obs.getCoeffs();
147+
for (size_t obs_term_idx = 0; obs_term_idx < coeffs.size();
148+
obs_term_idx++) {
149+
auto obs_samples = measure_with_samples(
150+
obs, num_shots, shot_range, obs_term_idx);
151+
PrecisionT result_per_term = std::accumulate(
152+
obs_samples.begin(), obs_samples.end(), 0.0);
153+
154+
result +=
155+
coeffs[obs_term_idx] * result_per_term / obs_samples.size();
156+
}
157+
} else {
158+
auto obs_samples = measure_with_samples(obs, num_shots, shot_range);
159+
result =
160+
std::accumulate(obs_samples.begin(), obs_samples.end(), 0.0);
161+
result /= obs_samples.size();
162+
}
163+
return result;
164+
}
165+
166+
/**
167+
* @brief Calculate the expectation value for a general Observable.
168+
*
169+
* @param obs Observable.
170+
* @param num_shots Number of shots used to generate samples
171+
* @param shot_range The range of samples to use. All samples are used
172+
* by default.
173+
* @param term_idx Index of a Hamiltonian term
174+
*
175+
* @return Expectation value with respect to the given observable.
176+
*/
177+
auto measure_with_samples(const Observable<StateVectorT> &obs,
178+
const size_t &num_shots,
179+
const std::vector<size_t> &shot_range,
180+
size_t term_idx = 0) {
181+
const size_t num_qubits = _statevector.getTotalNumQubits();
182+
std::vector<size_t> obs_wires;
183+
std::vector<size_t> identity_wires;
184+
185+
auto sub_samples = _sample_state(obs, num_shots, shot_range, obs_wires,
186+
identity_wires, term_idx);
187+
188+
size_t num_samples = shot_range.empty() ? num_shots : shot_range.size();
189+
190+
std::vector<PrecisionT> obs_samples(num_samples, 0);
191+
192+
size_t num_identity_obs = identity_wires.size();
193+
if (!identity_wires.empty()) {
194+
size_t identity_obs_idx = 0;
195+
for (size_t i = 0; i < obs_wires.size(); i++) {
196+
if (identity_wires[identity_obs_idx] == obs_wires[i]) {
197+
std::swap(obs_wires[identity_obs_idx], obs_wires[i]);
198+
identity_obs_idx++;
199+
}
200+
}
201+
}
202+
203+
for (size_t i = 0; i < num_samples; i++) {
204+
std::vector<size_t> local_sample(obs_wires.size());
205+
size_t idx = 0;
206+
for (auto &obs_wire : obs_wires) {
207+
local_sample[idx] = sub_samples[i * num_qubits + obs_wire];
208+
idx++;
209+
}
210+
211+
if (num_identity_obs != obs_wires.size()) {
212+
// eigen values are `1` and `-1` for PauliX, PauliY, PauliZ,
213+
// Hadamard gates the eigen value for a eigen vector |00001> is
214+
// -1 since sum of the value at each bit position is odd
215+
size_t bitSum = static_cast<size_t>(
216+
std::accumulate(local_sample.begin() + num_identity_obs,
217+
local_sample.end(), 0));
218+
if ((bitSum & size_t{1}) == 1) {
219+
obs_samples[i] = -1;
220+
} else {
221+
obs_samples[i] = 1;
222+
}
223+
} else {
224+
// eigen value for Identity gate is `1`
225+
obs_samples[i] = 1;
226+
}
227+
}
228+
return obs_samples;
229+
}
230+
231+
private:
232+
/**
233+
* @brief Return preprocess state with a observable
234+
*
235+
* @param obs The observable to sample
236+
* @param obs_wires Observable wires.
237+
* @param identity_wires Wires of Identity gates
238+
* @param term_idx Index of a Hamiltonian term. For other observables, its
239+
* value is 0, which is set as default.
240+
*
241+
* @return a StateVectorT object
242+
*/
243+
auto _preprocess_state(const Observable<StateVectorT> &obs,
244+
std::vector<size_t> &obs_wires,
245+
std::vector<size_t> &identity_wires,
246+
const size_t &term_idx = 0) {
247+
if constexpr (std::is_same_v<
248+
typename StateVectorT::MemoryStorageT,
249+
Pennylane::Util::MemoryStorageLocation::External>) {
250+
StateVectorT sv(_statevector.getData(), _statevector.getLength());
251+
sv.updateData(_statevector.getData(), _statevector.getLength());
252+
obs.applyInPlaceShots(sv, identity_wires, obs_wires, term_idx);
253+
return sv;
254+
} else {
255+
StateVectorT sv(_statevector);
256+
obs.applyInPlaceShots(sv, identity_wires, obs_wires, term_idx);
257+
return sv;
258+
}
259+
}
260+
261+
/**
262+
* @brief Return samples of a observable
263+
*
264+
* @param obs The observable to sample
265+
* @param num_shots Number of shots used to generate samples
266+
* @param shot_range The range of samples to use. All samples are used by
267+
* default.
268+
* @param obs_wires Observable wires.
269+
* @param identity_wires Wires of Identity gates
270+
* @param term_idx Index of a Hamiltonian term. For other observables, its
271+
* value is 0, which is set as default.
272+
*
273+
* @return std::vector<size_t> samples in std::vector
274+
*/
275+
auto _sample_state(const Observable<StateVectorT> &obs,
276+
const size_t &num_shots,
277+
const std::vector<size_t> &shot_range,
278+
std::vector<size_t> &obs_wires,
279+
std::vector<size_t> &identity_wires,
280+
const size_t &term_idx = 0) {
281+
const size_t num_qubits = _statevector.getTotalNumQubits();
282+
auto sv = _preprocess_state(obs, obs_wires, identity_wires, term_idx);
283+
Derived measure(sv);
284+
auto samples = measure.generate_samples(num_shots);
285+
286+
if (!shot_range.empty()) {
287+
std::vector<size_t> sub_samples(shot_range.size() * num_qubits);
288+
// Get a slice of samples based on the shot_range vector
289+
size_t shot_idx = 0;
290+
for (const auto &i : shot_range) {
291+
for (size_t j = i * num_qubits; j < (i + 1) * num_qubits; j++) {
292+
// TODO some extra work to make it cache-friendly
293+
sub_samples[shot_idx * num_qubits + j - i * num_qubits] =
294+
samples[j];
295+
}
296+
shot_idx++;
297+
}
298+
return sub_samples;
299+
}
300+
return samples;
301+
}
302+
};
303+
} // namespace Pennylane::Measures

0 commit comments

Comments
 (0)