pennylane-lightning-tensor 0.42.0__cp312-cp312-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.
@@ -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]]