pennylane-lightning-tensor 0.42.0__cp311-cp311-manylinux_2_28_aarch64.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,615 @@
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
+ This module contains the LightningTensor class that inherits from the new device interface.
16
+ It is a device to perform tensor network simulations of quantum circuits using `cutensornet`.
17
+ """
18
+ from dataclasses import replace
19
+ from numbers import Number
20
+ from typing import Callable, Optional, Sequence, Tuple, Union
21
+ from warnings import warn
22
+
23
+ import numpy as np
24
+ import pennylane as qml
25
+ from pennylane.devices import DefaultExecutionConfig, Device, ExecutionConfig
26
+ from pennylane.devices.modifiers import simulator_tracking, single_tape_support
27
+ from pennylane.devices.preprocess import (
28
+ decompose,
29
+ validate_device_wires,
30
+ validate_measurements,
31
+ validate_observables,
32
+ )
33
+ from pennylane.operation import Operator
34
+ from pennylane.tape import QuantumScript, QuantumTape
35
+ from pennylane.transforms.core import TransformProgram
36
+ from pennylane.typing import Result, ResultBatch
37
+
38
+ from pennylane_lightning.core._version import __version__
39
+
40
+ from ._measurements import LightningTensorMeasurements
41
+ from ._tensornet import LightningTensorNet
42
+
43
+ try:
44
+ # pylint: disable=import-error, unused-import
45
+ from pennylane_lightning.lightning_tensor_ops import (
46
+ backend_info,
47
+ get_gpu_arch,
48
+ is_gpu_supported,
49
+ )
50
+
51
+ if not is_gpu_supported(): # pragma: no cover
52
+ raise ValueError(f"CUDA device is an unsupported version: {get_gpu_arch()}")
53
+
54
+ LT_CPP_BINARY_AVAILABLE = True
55
+
56
+ except ImportError as ex:
57
+ warn(str(ex), UserWarning)
58
+ LT_CPP_BINARY_AVAILABLE = False
59
+
60
+ Result_or_ResultBatch = Union[Result, ResultBatch]
61
+ QuantumTapeBatch = Sequence[QuantumTape]
62
+ QuantumTape_or_Batch = Union[QuantumTape, QuantumTapeBatch]
63
+ PostprocessingFn = Callable[[ResultBatch], Result_or_ResultBatch]
64
+
65
+
66
+ _backends = frozenset({"cutensornet"})
67
+ # The set of supported backends.
68
+
69
+ _methods = frozenset({"mps", "tn"})
70
+ # The set of supported methods.
71
+
72
+ _operations = frozenset(
73
+ {
74
+ "Identity",
75
+ "BasisState",
76
+ "MPSPrep",
77
+ "QubitUnitary",
78
+ "ControlledQubitUnitary",
79
+ "DiagonalQubitUnitary",
80
+ "PauliX",
81
+ "PauliY",
82
+ "PauliZ",
83
+ "Hadamard",
84
+ "GlobalPhase",
85
+ "S",
86
+ "Adjoint(S)",
87
+ "T",
88
+ "Adjoint(T)",
89
+ "SX",
90
+ "Adjoint(SX)",
91
+ "CNOT",
92
+ "SWAP",
93
+ "ISWAP",
94
+ "Adjoint(ISWAP)",
95
+ "PSWAP",
96
+ "Adjoint(SISWAP)",
97
+ "SISWAP",
98
+ "SQISW",
99
+ "CSWAP",
100
+ "Toffoli",
101
+ "CY",
102
+ "CZ",
103
+ "PhaseShift",
104
+ "ControlledPhaseShift",
105
+ "C(Hadamard)",
106
+ "C(S)",
107
+ "C(T)",
108
+ "C(PhaseShift)",
109
+ "C(RX)",
110
+ "C(RY)",
111
+ "C(RZ)",
112
+ "C(Rot)",
113
+ "C(IsingXX)",
114
+ "C(IsingYY)",
115
+ "C(IsingZZ)",
116
+ "C(IsingXY)",
117
+ "C(SingleExcitation)",
118
+ "C(SingleExcitationPlus)",
119
+ "C(SingleExcitationMinus)",
120
+ "C(DoubleExcitation)",
121
+ "C(DoubleExcitationMinus)",
122
+ "C(DoubleExcitationPlus)",
123
+ "C(GlobalPhase)",
124
+ "C(MultiRZ)",
125
+ "RX",
126
+ "RY",
127
+ "RZ",
128
+ "Rot",
129
+ "CRX",
130
+ "CRY",
131
+ "CRZ",
132
+ "CRot",
133
+ "IsingXX",
134
+ "IsingYY",
135
+ "IsingZZ",
136
+ "IsingXY",
137
+ "SingleExcitation",
138
+ "SingleExcitationPlus",
139
+ "SingleExcitationMinus",
140
+ "DoubleExcitation",
141
+ "DoubleExcitationPlus",
142
+ "DoubleExcitationMinus",
143
+ "QubitCarry",
144
+ "QubitSum",
145
+ "OrbitalRotation",
146
+ "ECR",
147
+ "BlockEncode",
148
+ "C(BlockEncode)",
149
+ }
150
+ )
151
+
152
+ _observables = frozenset(
153
+ {
154
+ "PauliX",
155
+ "PauliY",
156
+ "PauliZ",
157
+ "Hadamard",
158
+ "Hermitian",
159
+ "Identity",
160
+ "LinearCombination",
161
+ "Sum",
162
+ "SProd",
163
+ "Prod",
164
+ "Exp",
165
+ }
166
+ )
167
+ # The set of supported observables.
168
+
169
+
170
+ def stopping_condition(op: Operator) -> bool:
171
+ """A function that determines whether or not an operation is supported by ``lightning.tensor``."""
172
+ if isinstance(op, qml.ControlledQubitUnitary):
173
+ return True
174
+
175
+ if isinstance(op, qml.MPSPrep):
176
+ return True
177
+
178
+ if op.name in ("C(SProd)", "C(Exp)"):
179
+ return True
180
+
181
+ return op.has_matrix and op.name in _operations
182
+
183
+
184
+ def simulate(circuit: QuantumScript, tensornet: LightningTensorNet) -> Result:
185
+ """Simulate a single quantum script.
186
+
187
+ Args:
188
+ circuit (QuantumTape): The single circuit to simulate
189
+ tensornet (LightningTensorNet): handle to Lightning tensor network
190
+
191
+ Returns:
192
+ Tuple[TensorLike]: The results of the simulation
193
+
194
+ Note that this function can return measurements for non-commuting observables simultaneously.
195
+ """
196
+ tensornet.reset_state()
197
+ tensornet.set_tensor_network(circuit)
198
+ return LightningTensorMeasurements(tensornet).measure_tensor_network(circuit)
199
+
200
+
201
+ def accepted_observables(obs: Operator) -> bool:
202
+ """A function that determines whether or not an observable is supported by ``lightning.tensor``."""
203
+ return obs.name in _observables
204
+
205
+
206
+ def accepted_backends(backend: str) -> bool:
207
+ """A function that determines whether or not a backend is supported by ``lightning.tensor``."""
208
+ return backend in _backends
209
+
210
+
211
+ def accepted_methods(method: str) -> bool:
212
+ """A function that determines whether or not a method is supported by ``lightning.tensor``."""
213
+ return method in _methods
214
+
215
+
216
+ @simulator_tracking
217
+ @single_tape_support
218
+ class LightningTensor(Device):
219
+ """PennyLane Lightning Tensor device.
220
+
221
+ A device to perform tensor network operations on a quantum circuit.
222
+
223
+ This device is designed to simulate large-scale quantum circuits using tensor network methods. For
224
+ small circuits, other devices like ``lightning.qubit``, ``lightning.gpu`` or ``lightning.kokkos`` are
225
+ recommended.
226
+
227
+ Currently, the Matrix Product State (MPS) and the Exact Tensor Network methods are supported as implemented in the ``cutensornet`` backend.
228
+
229
+ Args:
230
+ wires (Optional[int, list]): The number of wires to initialize the device with. Defaults to ``None`` if not specified, and the device will allocate the number of wires depending on the circuit to execute.
231
+ Defaults to ``None`` if not specified.
232
+ shots (int): Measurements are performed drawing ``shots`` times from a discrete random variable distribution associated with a state vector and an observable. Defaults to ``None`` if not specified. Setting
233
+ to ``None`` results in computing statistics like expectation values and
234
+ variances analytically.
235
+ method (str): Supported method. The supported methods are ``"mps"`` (Matrix Product State) and ``"tn"`` (Exact Tensor Network). Default is ``"mps"``.
236
+ c_dtype: Datatypes for the tensor representation. Must be one of
237
+ ``numpy.complex64`` or ``numpy.complex128``. Default is ``numpy.complex128``.
238
+ Keyword Args:
239
+ max_bond_dim (int): (Only for ``method=mps``) The maximum bond dimension to be used in the MPS simulation. Default is 128.
240
+ The accuracy of the wavefunction representation comes with a memory tradeoff which can be
241
+ tuned with `max_bond_dim`. The larger the internal bond dimension, the more entanglement can
242
+ be described but the larger the memory requirements. Note that GPUs are ill-suited (i.e. less
243
+ competitive compared with CPUs) for simulating circuits with low bond dimensions and/or circuit
244
+ layers with a single or few gates because the arithmetic intensity is lower.
245
+ cutoff (float): (Only for ``method=mps``) The threshold used to truncate the singular values of the MPS tensors. The default is 0.
246
+ cutoff_mode (str): (Only for ``method=mps``) Singular value truncation mode for MPS tensors. The options are ``"rel"`` and ``"abs"``. Default is ``"abs"``.
247
+ backend (str): Supported backend. Currently, only ``cutensornet`` is supported. Default is ``cutensornet``.
248
+
249
+ **Example for the MPS method**
250
+
251
+ .. code-block:: python
252
+
253
+ import pennylane as qml
254
+
255
+ num_qubits = 100
256
+
257
+ dev = qml.device("lightning.tensor", wires=num_qubits, max_bond_dim=32)
258
+
259
+ @qml.qnode(dev)
260
+ def circuit(num_qubits):
261
+ for qubit in range(0, num_qubits - 1):
262
+ qml.CZ(wires=[qubit, qubit + 1])
263
+ qml.X(wires=[qubit])
264
+ qml.Z(wires=[qubit + 1])
265
+ return qml.expval(qml.Z(0))
266
+
267
+ >>> print(circuit(num_qubits))
268
+ -1.0
269
+
270
+ **Example for the Exact Tensor Network method**
271
+
272
+ .. code-block:: python
273
+
274
+ import pennylane as qml
275
+
276
+ num_qubits = 100
277
+
278
+ dev = qml.device("lightning.tensor", wires=num_qubits, method="tn")
279
+
280
+ @qml.qnode(dev)
281
+ def circuit(num_qubits):
282
+ for qubit in range(0, num_qubits - 1):
283
+ qml.CZ(wires=[qubit, qubit + 1])
284
+ qml.X(wires=[qubit])
285
+ qml.Z(wires=[qubit + 1])
286
+ return qml.expval(qml.Z(0))
287
+
288
+ >>> print(circuit(num_qubits))
289
+ -1.0
290
+ """
291
+
292
+ # pylint: disable=too-many-instance-attributes
293
+ pennylane_requires = ">=0.41"
294
+ version = __version__
295
+
296
+ _device_options = {
297
+ "mps": ("backend", "max_bond_dim", "cutoff", "cutoff_mode"),
298
+ "tn": ("backend"),
299
+ }
300
+
301
+ _CPP_BINARY_AVAILABLE = LT_CPP_BINARY_AVAILABLE
302
+
303
+ # TODO: Move supported ops/obs to TOML file
304
+ operations = _operations
305
+ # The names of the supported operations.
306
+
307
+ observables = _observables
308
+ # The names of the supported observables.
309
+
310
+ # pylint: disable=too-many-arguments,too-many-branches
311
+ def __init__(
312
+ self,
313
+ *,
314
+ wires=None,
315
+ shots=None,
316
+ method: str = "mps",
317
+ c_dtype=np.complex128,
318
+ **kwargs,
319
+ ):
320
+ if not self._CPP_BINARY_AVAILABLE:
321
+ raise ImportError("Pre-compiled binaries for lightning.tensor are not available. ")
322
+
323
+ if not accepted_methods(method):
324
+ raise ValueError(
325
+ f"Unsupported method: {method}. Supported methods are 'mps' (Matrix Product State) and 'tn' (Exact Tensor Network)."
326
+ )
327
+
328
+ if c_dtype not in [np.complex64, np.complex128]: # pragma: no cover
329
+ raise TypeError(f"Unsupported complex type: {c_dtype}")
330
+
331
+ super().__init__(wires=wires, shots=shots)
332
+
333
+ if isinstance(wires, int) or wires is None:
334
+ self._wire_map = None # should just use wires as is
335
+ else:
336
+ self._wire_map = {w: i for i, w in enumerate(self.wires)}
337
+
338
+ self._num_wires = len(self.wires) if self.wires else None
339
+ self._method = method
340
+ self._c_dtype = c_dtype
341
+
342
+ self._backend = kwargs.get("backend", "cutensornet")
343
+
344
+ for arg in kwargs:
345
+ if arg not in self._device_options[self._method]:
346
+ raise TypeError(
347
+ f"Unexpected argument: {arg} during initialization of the lightning.tensor device."
348
+ )
349
+
350
+ if not accepted_backends(self._backend):
351
+ raise ValueError(f"Unsupported backend: {self._backend}")
352
+ if self._method == "mps":
353
+ self._max_bond_dim = kwargs.get("max_bond_dim", 128)
354
+ self._cutoff = kwargs.get("cutoff", 0)
355
+ self._cutoff_mode = kwargs.get("cutoff_mode", "abs")
356
+
357
+ if not isinstance(self._max_bond_dim, int) or self._max_bond_dim < 1:
358
+ raise ValueError("The maximum bond dimension must be an integer greater than 0.")
359
+ if not isinstance(self._cutoff, (int, float)) or self._cutoff < 0:
360
+ raise ValueError("The cutoff must be a non-negative number.")
361
+ if self._cutoff_mode not in ["rel", "abs"]:
362
+ raise ValueError(f"Unsupported cutoff mode: {self._cutoff_mode}")
363
+
364
+ @property
365
+ def name(self):
366
+ """The name of the device."""
367
+ return "lightning.tensor"
368
+
369
+ @property
370
+ def num_wires(self):
371
+ """Number of wires addressed on this device."""
372
+ return self._num_wires
373
+
374
+ @property
375
+ def backend(self):
376
+ """Supported backend."""
377
+ return self._backend
378
+
379
+ @property
380
+ def method(self):
381
+ """Supported method."""
382
+ return self._method
383
+
384
+ @property
385
+ def c_dtype(self):
386
+ """Tensor complex data type."""
387
+ return self._c_dtype
388
+
389
+ def _tensornet(self, num_wires):
390
+ """Return the tensornet object."""
391
+ if self.method == "mps":
392
+ return LightningTensorNet(
393
+ num_wires,
394
+ self._method,
395
+ self._c_dtype,
396
+ device_name=self.name,
397
+ max_bond_dim=self._max_bond_dim,
398
+ cutoff=self._cutoff,
399
+ cutoff_mode=self._cutoff_mode,
400
+ )
401
+ return LightningTensorNet(num_wires, self._method, self._c_dtype, device_name=self.name)
402
+
403
+ dtype = c_dtype
404
+
405
+ def _setup_execution_config(
406
+ self, config: Optional[ExecutionConfig] = DefaultExecutionConfig
407
+ ) -> ExecutionConfig:
408
+ """
409
+ Update the execution config with choices for how the device should be used and the device options.
410
+ """
411
+ # TODO: add options for gradients next quarter
412
+
413
+ updated_values = {}
414
+
415
+ new_device_options = dict(config.device_options)
416
+ for option in self._device_options[self.method]:
417
+ if option not in new_device_options:
418
+ new_device_options[option] = getattr(self, f"_{option}", None)
419
+
420
+ return replace(config, **updated_values, device_options=new_device_options)
421
+
422
+ def dynamic_wires_from_circuit(self, circuit):
423
+ """Map circuit wires to Pennylane ``default.qubit`` standard wire order.
424
+
425
+ Args:
426
+ circuit (QuantumTape): The circuit to execute.
427
+
428
+ Returns:
429
+ QuantumTape: The updated circuit with the wires mapped to the standard wire order.
430
+ """
431
+
432
+ return circuit.map_to_standard_wires() if self.num_wires is None else circuit
433
+
434
+ def preprocess(
435
+ self,
436
+ execution_config: ExecutionConfig = DefaultExecutionConfig,
437
+ ):
438
+ """This function defines the device transform program to be applied and an updated device configuration.
439
+
440
+ Args:
441
+ execution_config (Union[ExecutionConfig, Sequence[ExecutionConfig]]): A data structure describing the
442
+ parameters needed to fully describe the execution.
443
+
444
+ Returns:
445
+ TransformProgram, ExecutionConfig: A transform program that when called returns :class:`~.QuantumTape`'s that the
446
+ device can natively execute as well as a postprocessing function to be called after execution, and a configuration
447
+ with unset specifications filled in.
448
+
449
+ This device currently:
450
+
451
+ * Does not support derivatives.
452
+ * Does not support vector-Jacobian products.
453
+ """
454
+
455
+ config = self._setup_execution_config(execution_config)
456
+
457
+ program = TransformProgram()
458
+
459
+ program.add_transform(validate_measurements, name=self.name)
460
+ program.add_transform(validate_observables, accepted_observables, name=self.name)
461
+ program.add_transform(validate_device_wires, self._wires, name=self.name)
462
+ program.add_transform(
463
+ decompose,
464
+ stopping_condition=stopping_condition,
465
+ stopping_condition_shots=stopping_condition,
466
+ skip_initial_state_prep=True,
467
+ name=self.name,
468
+ )
469
+ return program, config
470
+
471
+ # pylint: disable=unused-argument
472
+ def execute(
473
+ self,
474
+ circuits: QuantumTape_or_Batch,
475
+ execution_config: ExecutionConfig = DefaultExecutionConfig,
476
+ ) -> Result_or_ResultBatch:
477
+ """Execute a circuit or a batch of circuits and turn it into results.
478
+
479
+ Args:
480
+ circuits (Union[QuantumTape, Sequence[QuantumTape]]): the quantum circuits to be executed.
481
+ execution_config (ExecutionConfig): a data structure with additional information required for execution.
482
+
483
+ Returns:
484
+ TensorLike, tuple[TensorLike], tuple[tuple[TensorLike]]: A numeric result of the computation.
485
+ """
486
+
487
+ results = []
488
+
489
+ for circuit in circuits:
490
+ if self._wire_map is not None:
491
+ [circuit], _ = qml.map_wires(circuit, self._wire_map)
492
+ results.append(
493
+ simulate(
494
+ self.dynamic_wires_from_circuit(circuit),
495
+ self._tensornet(
496
+ self.num_wires if self.num_wires is not None else circuit.num_wires
497
+ ),
498
+ )
499
+ )
500
+
501
+ return tuple(results)
502
+
503
+ # pylint: disable=unused-argument
504
+ def supports_derivatives(
505
+ self,
506
+ execution_config: Optional[ExecutionConfig] = None,
507
+ circuit: Optional[qml.tape.QuantumTape] = None,
508
+ ) -> bool:
509
+ """Check whether or not derivatives are available for a given configuration and circuit.
510
+
511
+ Args:
512
+ execution_config (ExecutionConfig): The configuration of the desired derivative calculation.
513
+ circuit (QuantumTape): An optional circuit to check derivatives support for.
514
+
515
+ Returns:
516
+ Bool: Whether or not a derivative can be calculated provided the given information.
517
+
518
+ """
519
+ return False
520
+
521
+ def compute_derivatives(
522
+ self,
523
+ circuits: QuantumTape_or_Batch,
524
+ execution_config: ExecutionConfig = DefaultExecutionConfig,
525
+ ):
526
+ """Calculate the Jacobian of either a single or a batch of circuits on the device.
527
+
528
+ Args:
529
+ circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuits to calculate derivatives for.
530
+ execution_config (ExecutionConfig): a data structure with all additional information required for execution.
531
+
532
+ Returns:
533
+ Tuple: The Jacobian for each trainable parameter.
534
+ """
535
+ raise NotImplementedError(
536
+ "The computation of derivatives has yet to be implemented for the lightning.tensor device."
537
+ )
538
+
539
+ def execute_and_compute_derivatives(
540
+ self,
541
+ circuits: QuantumTape_or_Batch,
542
+ execution_config: ExecutionConfig = DefaultExecutionConfig,
543
+ ):
544
+ """Compute the results and Jacobians of circuits at the same time.
545
+
546
+ Args:
547
+ circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuits or batch of circuits.
548
+ execution_config (ExecutionConfig): a data structure with all additional information required for execution.
549
+
550
+ Returns:
551
+ tuple: A numeric result of the computation and the gradient.
552
+ """
553
+ raise NotImplementedError(
554
+ "The computation of derivatives has yet to be implemented for the lightning.tensor device."
555
+ )
556
+
557
+ # pylint: disable=unused-argument
558
+ def supports_vjp(
559
+ self,
560
+ execution_config: Optional[ExecutionConfig] = None,
561
+ circuit: Optional[QuantumTape] = None,
562
+ ) -> bool:
563
+ """Whether or not this device defines a custom vector-Jacobian product.
564
+
565
+ Args:
566
+ execution_config (ExecutionConfig): The configuration of the desired derivative calculation.
567
+ circuit (QuantumTape): An optional circuit to check derivatives support for.
568
+
569
+ Returns:
570
+ Bool: Whether or not a derivative can be calculated provided the given information.
571
+ """
572
+ return False
573
+
574
+ def compute_vjp(
575
+ self,
576
+ circuits: QuantumTape_or_Batch,
577
+ cotangents: Tuple[Number],
578
+ execution_config: ExecutionConfig = DefaultExecutionConfig,
579
+ ):
580
+ r"""The vector-Jacobian product used in reverse-mode differentiation.
581
+
582
+ Args:
583
+ circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuit or batch of circuits.
584
+ cotangents (Tuple[Number, Tuple[Number]]): Gradient-output vector. Must have shape matching the output shape of the
585
+ corresponding circuit. If the circuit has a single output, ``cotangents`` may be a single number, not an iterable
586
+ of numbers.
587
+ execution_config (ExecutionConfig): a data structure with all additional information required for execution.
588
+
589
+ Returns:
590
+ tensor-like: A numeric result of computing the vector-Jacobian product.
591
+ """
592
+ raise NotImplementedError(
593
+ "The computation of vector-Jacobian product has yet to be implemented for the lightning.tensor device."
594
+ )
595
+
596
+ def execute_and_compute_vjp(
597
+ self,
598
+ circuits: QuantumTape_or_Batch,
599
+ cotangents: Tuple[Number],
600
+ execution_config: ExecutionConfig = DefaultExecutionConfig,
601
+ ):
602
+ """Calculate both the results and the vector-Jacobian product used in reverse-mode differentiation.
603
+
604
+ Args:
605
+ circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuit or batch of circuits to be executed.
606
+ cotangents (Tuple[Number, Tuple[Number]]): Gradient-output vector. Must have shape matching the output shape of the
607
+ corresponding circuit.
608
+ execution_config (ExecutionConfig): a data structure with all additional information required for execution.
609
+
610
+ Returns:
611
+ Tuple, Tuple: the result of executing the scripts and the numeric result of computing the vector-Jacobian product
612
+ """
613
+ raise NotImplementedError(
614
+ "The computation of vector-Jacobian product has yet to be implemented for the lightning.tensor device."
615
+ )