pennylane-lightning-tensor 0.42.0__cp313-cp313-manylinux_2_28_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pennylane_lightning/lightning_tensor/__init__.py +18 -0
- pennylane_lightning/lightning_tensor/_measurements.py +426 -0
- pennylane_lightning/lightning_tensor/_tensornet.py +787 -0
- pennylane_lightning/lightning_tensor/lightning_tensor.py +615 -0
- pennylane_lightning/lightning_tensor_ops.cpython-313-x86_64-linux-gnu.so +0 -0
- pennylane_lightning_tensor-0.42.0.dist-info/METADATA +220 -0
- pennylane_lightning_tensor-0.42.0.dist-info/RECORD +12 -0
- pennylane_lightning_tensor-0.42.0.dist-info/WHEEL +5 -0
- pennylane_lightning_tensor-0.42.0.dist-info/entry_points.txt +2 -0
- pennylane_lightning_tensor-0.42.0.dist-info/licenses/LICENSE +212 -0
- pennylane_lightning_tensor-0.42.0.dist-info/top_level.txt +2 -0
- pennylane_lightning_tensor.libs/libgomp-24e2ab19.so.1.0.0 +0 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
# Copyright 2018-2024 Xanadu Quantum Technologies Inc.
|
2
|
+
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
"""PennyLane lightning_tensor package."""
|
15
|
+
|
16
|
+
from pennylane_lightning.core import __version__
|
17
|
+
|
18
|
+
from .lightning_tensor import LightningTensor
|
@@ -0,0 +1,426 @@
|
|
1
|
+
# Copyright 2024 Xanadu Quantum Technologies Inc.
|
2
|
+
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
"""
|
15
|
+
Class implementation for tensornet measurements.
|
16
|
+
"""
|
17
|
+
|
18
|
+
# pylint: disable=import-error, no-name-in-module, ungrouped-imports
|
19
|
+
try:
|
20
|
+
from pennylane_lightning.lightning_tensor_ops import (
|
21
|
+
exactMeasurementsC64,
|
22
|
+
exactMeasurementsC128,
|
23
|
+
mpsMeasurementsC64,
|
24
|
+
mpsMeasurementsC128,
|
25
|
+
)
|
26
|
+
except ImportError:
|
27
|
+
pass
|
28
|
+
|
29
|
+
from functools import reduce
|
30
|
+
from typing import Callable, List, Union
|
31
|
+
|
32
|
+
import numpy as np
|
33
|
+
import pennylane as qml
|
34
|
+
from pennylane.devices.qubit.sampling import _group_measurements
|
35
|
+
from pennylane.measurements import (
|
36
|
+
ClassicalShadowMP,
|
37
|
+
CountsMP,
|
38
|
+
ExpectationMP,
|
39
|
+
MeasurementProcess,
|
40
|
+
ProbabilityMP,
|
41
|
+
SampleMeasurement,
|
42
|
+
ShadowExpvalMP,
|
43
|
+
Shots,
|
44
|
+
StateMeasurement,
|
45
|
+
VarianceMP,
|
46
|
+
)
|
47
|
+
from pennylane.operation import Operator
|
48
|
+
from pennylane.ops import SparseHamiltonian, Sum
|
49
|
+
from pennylane.tape import QuantumScript
|
50
|
+
from pennylane.typing import Result, TensorLike
|
51
|
+
from pennylane.wires import Wires
|
52
|
+
|
53
|
+
from pennylane_lightning.lightning_base._serialize import QuantumScriptSerializer
|
54
|
+
|
55
|
+
|
56
|
+
class LightningTensorMeasurements:
|
57
|
+
"""Lightning Tensor Measurements class
|
58
|
+
|
59
|
+
Measures the tensor network provided by the LightningTensorNet class.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
tensor_network(LightningTensorNet): Lightning tensornet class containing the tensor network to be measured.
|
63
|
+
"""
|
64
|
+
|
65
|
+
def __init__(
|
66
|
+
self,
|
67
|
+
tensor_network,
|
68
|
+
) -> None:
|
69
|
+
self._tensornet = tensor_network
|
70
|
+
self._dtype = tensor_network.dtype
|
71
|
+
self._method = tensor_network._method
|
72
|
+
self._measurement_lightning = self._measurement_dtype()(tensor_network.tensornet)
|
73
|
+
|
74
|
+
@property
|
75
|
+
def dtype(self):
|
76
|
+
"""Returns the simulation data type."""
|
77
|
+
return self._dtype
|
78
|
+
|
79
|
+
@staticmethod
|
80
|
+
def _observable_is_sparse(obs: Operator) -> bool:
|
81
|
+
"""States if the required observable is sparse.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
obs(Operator): PennyLane observable to check sparsity.
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
True if the measurement process only uses the sparse data representation.
|
88
|
+
"""
|
89
|
+
|
90
|
+
return isinstance(obs, qml.SparseHamiltonian)
|
91
|
+
|
92
|
+
def _measurement_dtype(self):
|
93
|
+
"""Binding to Lightning Measurements C++ class.
|
94
|
+
|
95
|
+
Returns: the Measurements class
|
96
|
+
"""
|
97
|
+
if self._method == "tn": # Using "tn" method
|
98
|
+
return exactMeasurementsC64 if self.dtype == np.complex64 else exactMeasurementsC128
|
99
|
+
# Using "mps" method
|
100
|
+
return mpsMeasurementsC64 if self.dtype == np.complex64 else mpsMeasurementsC128
|
101
|
+
|
102
|
+
def state_diagonalizing_gates(self, measurementprocess: StateMeasurement) -> TensorLike:
|
103
|
+
"""Apply a measurement to state when the measurement process has an observable with diagonalizing gates.
|
104
|
+
This method bypasses default.qubit's implementation of the measurement process.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
measurementprocess (StateMeasurement): measurement to apply to the state
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
TensorLike: the result of the measurement
|
111
|
+
"""
|
112
|
+
diagonalizing_gates = measurementprocess.diagonalizing_gates()
|
113
|
+
self._tensornet.apply_operations(diagonalizing_gates)
|
114
|
+
self._tensornet.appendFinalState()
|
115
|
+
state_array = self._tensornet.state
|
116
|
+
wires = Wires(range(self._tensornet.num_wires))
|
117
|
+
result = measurementprocess.process_state(state_array, wires)
|
118
|
+
self._tensornet.apply_operations([qml.adjoint(g) for g in reversed(diagonalizing_gates)])
|
119
|
+
self._tensornet.appendFinalState()
|
120
|
+
return result
|
121
|
+
|
122
|
+
# pylint: disable=protected-access
|
123
|
+
def expval(self, measurementprocess: MeasurementProcess):
|
124
|
+
"""Expectation value of the supplied observable contained in the MeasurementProcess.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
measurementprocess (StateMeasurement): measurement to apply to the tensor network
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
Expectation value of the observable
|
131
|
+
"""
|
132
|
+
if self._observable_is_sparse(measurementprocess.obs):
|
133
|
+
raise NotImplementedError("Sparse Observables are not supported.")
|
134
|
+
|
135
|
+
if isinstance(measurementprocess.obs, qml.Hermitian):
|
136
|
+
if len(measurementprocess.obs.wires) > 1:
|
137
|
+
raise ValueError("The number of Hermitian observables target wires should be 1.")
|
138
|
+
|
139
|
+
ob_serialized = QuantumScriptSerializer(
|
140
|
+
self._tensornet.device_name, self.dtype == np.complex64, tensor_backend=self._method
|
141
|
+
)._ob(measurementprocess.obs)
|
142
|
+
return self._measurement_lightning.expval(ob_serialized)
|
143
|
+
|
144
|
+
def probs(self, measurementprocess: MeasurementProcess):
|
145
|
+
"""Probabilities of the supplied observable or wires contained in the MeasurementProcess.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
measurementprocess (StateMeasurement): measurement to apply to the state
|
149
|
+
|
150
|
+
Returns:
|
151
|
+
Probabilities of the supplied observable or wires
|
152
|
+
"""
|
153
|
+
diagonalizing_gates = measurementprocess.diagonalizing_gates()
|
154
|
+
|
155
|
+
if diagonalizing_gates:
|
156
|
+
self._tensornet.apply_operations(diagonalizing_gates)
|
157
|
+
self._tensornet.appendFinalState()
|
158
|
+
|
159
|
+
measurewires = (
|
160
|
+
self._tensornet._wires
|
161
|
+
if measurementprocess.wires == Wires([])
|
162
|
+
else measurementprocess.wires.tolist()
|
163
|
+
)
|
164
|
+
|
165
|
+
results = self._measurement_lightning.probs(measurewires)
|
166
|
+
if diagonalizing_gates:
|
167
|
+
self._tensornet.apply_operations(
|
168
|
+
[qml.adjoint(g, lazy=False) for g in reversed(diagonalizing_gates)]
|
169
|
+
)
|
170
|
+
self._tensornet.appendFinalState()
|
171
|
+
return results
|
172
|
+
|
173
|
+
def var(self, measurementprocess: MeasurementProcess):
|
174
|
+
"""Variance of the supplied observable contained in the MeasurementProcess. Note that the variance is
|
175
|
+
calculated as <obs**2> - <obs>**2. The current implementation only supports single-wire observables.
|
176
|
+
Observables with more than 1 wire, projector and sparse-hamiltonian are not supported.
|
177
|
+
|
178
|
+
Args:
|
179
|
+
measurementprocess (StateMeasurement): measurement to apply to the state
|
180
|
+
|
181
|
+
Returns:
|
182
|
+
Variance of the observable
|
183
|
+
"""
|
184
|
+
if self._observable_is_sparse(measurementprocess.obs):
|
185
|
+
raise NotImplementedError("The var measurement does not support sparse observables.")
|
186
|
+
|
187
|
+
if isinstance(measurementprocess.obs, qml.Hermitian):
|
188
|
+
if len(measurementprocess.obs.wires) > 1:
|
189
|
+
raise ValueError("The number of Hermitian observables target wires should be 1.")
|
190
|
+
|
191
|
+
ob_serialized = QuantumScriptSerializer(
|
192
|
+
self._tensornet.device_name, self.dtype == np.complex64, tensor_backend=self._method
|
193
|
+
)._ob(measurementprocess.obs)
|
194
|
+
return self._measurement_lightning.var(ob_serialized)
|
195
|
+
|
196
|
+
def get_measurement_function(
|
197
|
+
self, measurementprocess: MeasurementProcess
|
198
|
+
) -> Callable[[MeasurementProcess, TensorLike], TensorLike]:
|
199
|
+
"""Get the appropriate method for performing a measurement.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
measurementprocess (MeasurementProcess): measurement process to apply to the graph
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
Callable: function that returns the measurement result
|
206
|
+
"""
|
207
|
+
if isinstance(measurementprocess, StateMeasurement):
|
208
|
+
if isinstance(measurementprocess, ExpectationMP):
|
209
|
+
if isinstance(measurementprocess.obs, qml.Identity):
|
210
|
+
return self.state_diagonalizing_gates
|
211
|
+
return self.expval
|
212
|
+
|
213
|
+
if isinstance(measurementprocess, VarianceMP):
|
214
|
+
if isinstance(measurementprocess.obs, qml.Identity):
|
215
|
+
return self.state_diagonalizing_gates
|
216
|
+
return self.var
|
217
|
+
|
218
|
+
if isinstance(measurementprocess, ProbabilityMP):
|
219
|
+
return self.probs
|
220
|
+
|
221
|
+
if measurementprocess.obs is None or measurementprocess.obs.has_diagonalizing_gates:
|
222
|
+
return self.state_diagonalizing_gates
|
223
|
+
|
224
|
+
raise NotImplementedError("Unsupported measurement type.")
|
225
|
+
|
226
|
+
def measurement(self, measurementprocess: MeasurementProcess) -> TensorLike:
|
227
|
+
"""Apply a measurement process to a tensor network.
|
228
|
+
|
229
|
+
Args:
|
230
|
+
measurementprocess (MeasurementProcess): measurement process to apply to the graph
|
231
|
+
|
232
|
+
Returns:
|
233
|
+
TensorLike: the result of the measurement
|
234
|
+
"""
|
235
|
+
return self.get_measurement_function(measurementprocess)(measurementprocess)
|
236
|
+
|
237
|
+
def measure_tensor_network(self, circuit: QuantumScript) -> Result:
|
238
|
+
"""
|
239
|
+
Perform the measurements required by the circuit on the provided tensor network.
|
240
|
+
|
241
|
+
This is an internal function that will be called by the successor to ``lightning.tensor``.
|
242
|
+
|
243
|
+
Args:
|
244
|
+
circuit (QuantumScript): The single circuit to simulate
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
Tuple[TensorLike]: The measurement results
|
248
|
+
"""
|
249
|
+
|
250
|
+
if circuit.shots:
|
251
|
+
# finite-shot case
|
252
|
+
results = self.measure_with_samples(
|
253
|
+
circuit.measurements,
|
254
|
+
shots=circuit.shots,
|
255
|
+
)
|
256
|
+
|
257
|
+
if len(circuit.measurements) == 1:
|
258
|
+
if circuit.shots.has_partitioned_shots:
|
259
|
+
return tuple(res[0] for res in results)
|
260
|
+
|
261
|
+
return results[0]
|
262
|
+
|
263
|
+
return results
|
264
|
+
# analytic case
|
265
|
+
if len(circuit.measurements) == 1:
|
266
|
+
return self.measurement(circuit.measurements[0])
|
267
|
+
|
268
|
+
return tuple(self.measurement(mp) for mp in circuit.measurements)
|
269
|
+
|
270
|
+
# pylint:disable = too-many-arguments
|
271
|
+
def measure_with_samples(
|
272
|
+
self,
|
273
|
+
measurements: List[Union[SampleMeasurement, ClassicalShadowMP, ShadowExpvalMP]],
|
274
|
+
shots: Shots,
|
275
|
+
) -> List[TensorLike]:
|
276
|
+
"""
|
277
|
+
Returns the samples of the measurement process performed on the given state.
|
278
|
+
This function assumes that the user-defined wire labels in the measurement process
|
279
|
+
have already been mapped to integer wires used in the device.
|
280
|
+
|
281
|
+
Args:
|
282
|
+
measurements (List[Union[SampleMeasurement, ClassicalShadowMP, ShadowExpvalMP]]):
|
283
|
+
The sample measurements to perform
|
284
|
+
shots (Shots): The number of samples to take
|
285
|
+
|
286
|
+
Returns:
|
287
|
+
List[TensorLike[Any]]: Sample measurement results
|
288
|
+
"""
|
289
|
+
mps = measurements
|
290
|
+
|
291
|
+
for measurement in mps:
|
292
|
+
if measurement.wires == Wires([]):
|
293
|
+
# This is required for the case where no wires is specific for the tensornet
|
294
|
+
# (i.e. dynamically determined from circuit), and no wires (and no observable)
|
295
|
+
# is provided for the measurement (e.g. qml.probs() or qml.counts() or
|
296
|
+
# qml.samples()). In the case where number of wires is provided for the statevector,
|
297
|
+
# the same operation is performed in validate_device_wires during preprocess.
|
298
|
+
|
299
|
+
# pylint:disable=protected-access
|
300
|
+
measurement._wires = Wires(range(self._tensornet.num_wires))
|
301
|
+
|
302
|
+
groups, indices = _group_measurements(mps)
|
303
|
+
|
304
|
+
all_res = []
|
305
|
+
for group in groups:
|
306
|
+
if isinstance(group[0], (ExpectationMP, VarianceMP)) and isinstance(
|
307
|
+
group[0].obs, SparseHamiltonian
|
308
|
+
):
|
309
|
+
raise TypeError(
|
310
|
+
"ExpectationMP/VarianceMP(SparseHamiltonian) cannot be computed with samples."
|
311
|
+
)
|
312
|
+
if isinstance(group[0], VarianceMP) and isinstance(group[0].obs, Sum):
|
313
|
+
raise TypeError("VarianceMP(Sum) cannot be computed with samples.")
|
314
|
+
if isinstance(group[0], (ClassicalShadowMP, ShadowExpvalMP)):
|
315
|
+
raise TypeError(
|
316
|
+
"ExpectationMP(ClassicalShadowMP, ShadowExpvalMP) cannot be computed with samples."
|
317
|
+
)
|
318
|
+
if isinstance(group[0], ExpectationMP) and isinstance(group[0].obs, Sum):
|
319
|
+
all_res.extend(self._measure_sum_with_samples(group, shots))
|
320
|
+
else:
|
321
|
+
all_res.extend(self._measure_with_samples_diagonalizing_gates(group, shots))
|
322
|
+
|
323
|
+
# reorder results
|
324
|
+
flat_indices = []
|
325
|
+
for row in indices:
|
326
|
+
flat_indices += row
|
327
|
+
sorted_res = tuple(
|
328
|
+
res for _, res in sorted(list(enumerate(all_res)), key=lambda r: flat_indices[r[0]])
|
329
|
+
)
|
330
|
+
|
331
|
+
# put the shot vector axis before the measurement axis
|
332
|
+
if shots.has_partitioned_shots:
|
333
|
+
sorted_res = tuple(zip(*sorted_res))
|
334
|
+
|
335
|
+
return sorted_res
|
336
|
+
|
337
|
+
def _apply_diagonalizing_gates(self, mps: List[SampleMeasurement], adjoint: bool = False):
|
338
|
+
if len(mps) == 1:
|
339
|
+
diagonalizing_gates = mps[0].diagonalizing_gates()
|
340
|
+
elif all(mp.obs for mp in mps):
|
341
|
+
diagonalizing_gates = qml.pauli.diagonalize_qwc_pauli_words([mp.obs for mp in mps])[0]
|
342
|
+
else:
|
343
|
+
diagonalizing_gates = []
|
344
|
+
|
345
|
+
if adjoint:
|
346
|
+
diagonalizing_gates = [
|
347
|
+
qml.adjoint(g, lazy=False) for g in reversed(diagonalizing_gates)
|
348
|
+
]
|
349
|
+
|
350
|
+
self._tensornet.apply_operations(diagonalizing_gates)
|
351
|
+
self._tensornet.appendFinalState()
|
352
|
+
|
353
|
+
def _measure_with_samples_diagonalizing_gates(
|
354
|
+
self,
|
355
|
+
mps: List[SampleMeasurement],
|
356
|
+
shots: Shots,
|
357
|
+
) -> TensorLike:
|
358
|
+
"""
|
359
|
+
Returns the samples of the measurement process performed on the given state,
|
360
|
+
by rotating the state into the measurement basis using the diagonalizing gates
|
361
|
+
given by the measurement process.
|
362
|
+
|
363
|
+
Args:
|
364
|
+
mps (~.measurements.SampleMeasurement): The sample measurements to perform
|
365
|
+
shots (~.measurements.Shots): The number of samples to take
|
366
|
+
|
367
|
+
Returns:
|
368
|
+
TensorLike[Any]: Sample measurement results
|
369
|
+
"""
|
370
|
+
# apply diagonalizing gates
|
371
|
+
self._apply_diagonalizing_gates(mps)
|
372
|
+
|
373
|
+
wires = reduce(sum, (mp.wires for mp in mps))
|
374
|
+
|
375
|
+
def _process_single_shot(samples):
|
376
|
+
processed = []
|
377
|
+
for mp in mps:
|
378
|
+
res = mp.process_samples(samples, wires)
|
379
|
+
if not isinstance(mp, CountsMP):
|
380
|
+
res = qml.math.squeeze(res)
|
381
|
+
|
382
|
+
processed.append(res)
|
383
|
+
|
384
|
+
return tuple(processed)
|
385
|
+
|
386
|
+
try:
|
387
|
+
samples = self._measurement_lightning.generate_samples(
|
388
|
+
list(wires), shots.total_shots
|
389
|
+
).astype(int, copy=False)
|
390
|
+
except ValueError as e:
|
391
|
+
if str(e) != "probabilities contain NaN":
|
392
|
+
raise e
|
393
|
+
samples = qml.math.full((shots.total_shots, len(wires)), 0)
|
394
|
+
|
395
|
+
self._apply_diagonalizing_gates(mps, adjoint=True)
|
396
|
+
|
397
|
+
# if there is a shot vector, use the shots.bins generator to
|
398
|
+
# split samples w.r.t. the shots
|
399
|
+
processed_samples = []
|
400
|
+
for lower, upper in shots.bins():
|
401
|
+
result = _process_single_shot(samples[..., lower:upper, :])
|
402
|
+
processed_samples.append(result)
|
403
|
+
|
404
|
+
return (
|
405
|
+
tuple(zip(*processed_samples)) if shots.has_partitioned_shots else processed_samples[0]
|
406
|
+
)
|
407
|
+
|
408
|
+
def _measure_sum_with_samples(
|
409
|
+
self,
|
410
|
+
mp: List[SampleMeasurement],
|
411
|
+
shots: Shots,
|
412
|
+
):
|
413
|
+
# the list contains only one element based on how we group measurements
|
414
|
+
mp = mp[0]
|
415
|
+
|
416
|
+
# if the measurement process involves a Sum, measure each
|
417
|
+
# of the terms separately and sum
|
418
|
+
def _sum_for_single_shot(s):
|
419
|
+
results = self.measure_with_samples(
|
420
|
+
[ExpectationMP(t) for t in mp.obs],
|
421
|
+
s,
|
422
|
+
)
|
423
|
+
return sum(results)
|
424
|
+
|
425
|
+
unsqueezed_results = tuple(_sum_for_single_shot(type(shots)(s)) for s in shots)
|
426
|
+
return [unsqueezed_results] if shots.has_partitioned_shots else [unsqueezed_results[0]]
|