Source code for pennylane_lightning.lightning_tensor.lightning_tensor
# Copyright 2024 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This module contains the LightningTensor class that inherits from the new device interface.
It is a device to perform tensor network simulations of quantum circuits using `cutensornet`.
"""
from dataclasses import replace
from numbers import Number
from typing import Callable, Optional, Sequence, Tuple, Union
from warnings import warn
import numpy as np
import pennylane as qml
from pennylane.devices import DefaultExecutionConfig, Device, ExecutionConfig
from pennylane.devices.modifiers import simulator_tracking, single_tape_support
from pennylane.devices.preprocess import (
decompose,
validate_device_wires,
validate_measurements,
validate_observables,
)
from pennylane.operation import Operator
from pennylane.tape import QuantumScript, QuantumTape
from pennylane.transforms.core import TransformProgram
from pennylane.typing import Result, ResultBatch
from ._measurements import LightningTensorMeasurements
from ._tensornet import LightningTensorNet
try:
# pylint: disable=import-error, unused-import
from pennylane_lightning.lightning_tensor_ops import (
backend_info,
get_gpu_arch,
is_gpu_supported,
)
if not is_gpu_supported(): # pragma: no cover
raise ValueError(f"CUDA device is an unsupported version: {get_gpu_arch()}")
LT_CPP_BINARY_AVAILABLE = True
except ImportError as ex:
warn(str(ex), UserWarning)
LT_CPP_BINARY_AVAILABLE = False
Result_or_ResultBatch = Union[Result, ResultBatch]
QuantumTapeBatch = Sequence[QuantumTape]
QuantumTape_or_Batch = Union[QuantumTape, QuantumTapeBatch]
PostprocessingFn = Callable[[ResultBatch], Result_or_ResultBatch]
_backends = frozenset({"cutensornet"})
# The set of supported backends.
_methods = frozenset({"mps"})
# The set of supported methods.
_operations = frozenset(
{
"Identity",
"BasisState",
"QubitUnitary",
"ControlledQubitUnitary",
"DiagonalQubitUnitary",
"PauliX",
"PauliY",
"PauliZ",
"Hadamard",
"GlobalPhase",
"S",
"Adjoint(S)",
"T",
"Adjoint(T)",
"SX",
"Adjoint(SX)",
"CNOT",
"SWAP",
"ISWAP",
"Adjoint(ISWAP)",
"PSWAP",
"Adjoint(SISWAP)",
"SISWAP",
"SQISW",
"CSWAP",
"Toffoli",
"CY",
"CZ",
"PhaseShift",
"ControlledPhaseShift",
"C(Hadamard)",
"C(S)",
"C(T)",
"C(PhaseShift)",
"C(RX)",
"C(RY)",
"C(RZ)",
"C(Rot)",
"C(IsingXX)",
"C(IsingYY)",
"C(IsingZZ)",
"C(IsingXY)",
"C(SingleExcitation)",
"C(SingleExcitationPlus)",
"C(SingleExcitationMinus)",
"C(DoubleExcitation)",
"C(DoubleExcitationMinus)",
"C(DoubleExcitationPlus)",
"C(GlobalPhase)",
"C(MultiRZ)",
"RX",
"RY",
"RZ",
"Rot",
"CRX",
"CRY",
"CRZ",
"CRot",
"IsingXX",
"IsingYY",
"IsingZZ",
"IsingXY",
"SingleExcitation",
"SingleExcitationPlus",
"SingleExcitationMinus",
"DoubleExcitation",
"DoubleExcitationPlus",
"DoubleExcitationMinus",
"QubitCarry",
"QubitSum",
"OrbitalRotation",
"ECR",
"BlockEncode",
"C(BlockEncode)",
}
)
_observables = frozenset(
{
"PauliX",
"PauliY",
"PauliZ",
"Hadamard",
"Hermitian",
"Identity",
"LinearCombination",
"Sum",
"SProd",
"Prod",
"Exp",
}
)
# The set of supported observables.
def stopping_condition(op: Operator) -> bool:
"""A function that determines whether or not an operation is supported by the ``mps`` method of ``lightning.tensor``."""
if isinstance(op, qml.ControlledQubitUnitary):
return True
return op.has_matrix and op.name in _operations
def simulate(circuit: QuantumScript, tensornet: LightningTensorNet) -> Result:
"""Simulate a single quantum script.
Args:
circuit (QuantumTape): The single circuit to simulate
tensornet (LightningTensorNet): handle to Lightning tensor network
Returns:
Tuple[TensorLike]: The results of the simulation
Note that this function can return measurements for non-commuting observables simultaneously.
"""
tensornet.reset_state()
tensornet.set_tensor_network(circuit)
return LightningTensorMeasurements(tensornet).measure_tensor_network(circuit)
def accepted_observables(obs: Operator) -> bool:
"""A function that determines whether or not an observable is supported by ``lightning.tensor``."""
return obs.name in _observables
def accepted_backends(backend: str) -> bool:
"""A function that determines whether or not a backend is supported by ``lightning.tensor``."""
return backend in _backends
def accepted_methods(method: str) -> bool:
"""A function that determines whether or not a method is supported by ``lightning.tensor``."""
return method in _methods
[docs]@simulator_tracking
@single_tape_support
class LightningTensor(Device):
"""PennyLane Lightning Tensor device.
A device to perform tensor network operations on a quantum circuit.
This device is designed to simulate large-scale quantum circuits using tensor network methods. For
small circuits, other devices like ``lightning.qubit``, ``lightning.gpu``or ``lightning.kokkos`` are
recommended.
Currently, only the Matrix Product State (MPS) method as implemented in the ``cutensornet`` backend is supported.
Args:
wires (int): The number of wires to initialize the device with.
Defaults to ``None`` if not specified.
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
to ``None`` results in computing statistics like expectation values and
variances analytically.
method (str): Supported method. Currently, only ``mps`` is supported.
c_dtype: Datatypes for the tensor representation. Must be one of
``numpy.complex64`` or ``numpy.complex128``. Default is ``numpy.complex128``.
Keyword Args:
max_bond_dim (int): The maximum bond dimension to be used in the MPS simulation. Default is 128.
The accuracy of the wavefunction representation comes with a memory tradeoff which can be
tuned with `max_bond_dim`. The larger the internal bond dimension, the more entanglement can
be described but the larger the memory requirements. Note that GPUs are ill-suited (i.e. less
competitive compared with CPUs) for simulating circuits with low bond dimensions and/or circuit
layers with a single or few gates because the arithmetic intensity is lower.
cutoff (float): The threshold used to truncate the singular values of the MPS tensors. The default is 0.
cutoff_mode (str): Singular value truncation mode. The options are ``"rel"`` and ``"abs"``. The default is ``"abs"``.
backend (str): Supported backend. Currently, only ``cutensornet`` is supported.
**Example**
.. code-block:: python
import pennylane as qml
num_qubits = 100
dev = qml.device("lightning.tensor", wires=num_qubits)
@qml.qnode(dev)
def circuit(num_qubits):
for qubit in range(0, num_qubits - 1):
qml.CZ(wires=[qubit, qubit + 1])
qml.X(wires=[qubit])
qml.Z(wires=[qubit + 1])
return qml.expval(qml.Z(0))
>>> print(circuit(num_qubits))
-1.0
"""
# pylint: disable=too-many-instance-attributes
# So far we just consider the options for MPS simulator
_device_options = ("backend", "max_bond_dim", "cutoff", "cutoff_mode")
_CPP_BINARY_AVAILABLE = LT_CPP_BINARY_AVAILABLE
_new_API = True
# TODO: Move supported ops/obs to TOML file
operations = _operations
# The names of the supported operations.
observables = _observables
# The names of the supported observables.
# pylint: disable=too-many-arguments
def __init__(
self,
*,
wires=None,
shots=None,
method: str = "mps",
c_dtype=np.complex128,
**kwargs,
):
if not self._CPP_BINARY_AVAILABLE:
raise ImportError("Pre-compiled binaries for lightning.tensor are not available. ")
if not accepted_methods(method):
raise ValueError(f"Unsupported method: {method}")
if c_dtype not in [np.complex64, np.complex128]: # pragma: no cover
raise TypeError(f"Unsupported complex type: {c_dtype}")
if wires is None:
raise ValueError("The number of wires must be specified.")
super().__init__(wires=wires, shots=shots)
if isinstance(wires, int):
self._wire_map = None # should just use wires as is
else:
self._wire_map = {w: i for i, w in enumerate(self.wires)}
self._num_wires = len(self.wires) if self.wires else 0
self._method = method
self._c_dtype = c_dtype
self._max_bond_dim = kwargs.get("max_bond_dim", 128)
self._cutoff = kwargs.get("cutoff", 0)
self._cutoff_mode = kwargs.get("cutoff_mode", "abs")
self._backend = kwargs.get("backend", "cutensornet")
for arg in kwargs:
if arg not in self._device_options:
raise TypeError(
f"Unexpected argument: {arg} during initialization of the lightning.tensor device."
)
if not accepted_backends(self._backend):
raise ValueError(f"Unsupported backend: {self._backend}")
if self._cutoff_mode not in ["rel", "abs"]:
raise ValueError(f"Unsupported cutoff mode: {self._cutoff_mode}")
if not isinstance(self._max_bond_dim, int) or self._max_bond_dim < 1:
raise ValueError("The maximum bond dimension must be an integer greater than 0.")
@property
def name(self):
"""The name of the device."""
return "lightning.tensor"
@property
def num_wires(self):
"""Number of wires addressed on this device."""
return self._num_wires
@property
def backend(self):
"""Supported backend."""
return self._backend
@property
def method(self):
"""Supported method."""
return self._method
@property
def c_dtype(self):
"""Tensor complex data type."""
return self._c_dtype
def _tensornet(self):
"""Return the tensornet object."""
return LightningTensorNet(
self._num_wires,
self._method,
self._c_dtype,
self._max_bond_dim,
self._cutoff,
self._cutoff_mode,
)
dtype = c_dtype
def _setup_execution_config(
self, config: Optional[ExecutionConfig] = DefaultExecutionConfig
) -> ExecutionConfig:
"""
Update the execution config with choices for how the device should be used and the device options.
"""
# TODO: add options for gradients next quarter
updated_values = {}
new_device_options = dict(config.device_options)
for option in self._device_options:
if option not in new_device_options:
new_device_options[option] = getattr(self, f"_{option}", None)
return replace(config, **updated_values, device_options=new_device_options)
[docs] def preprocess(
self,
execution_config: ExecutionConfig = DefaultExecutionConfig,
):
"""This function defines the device transform program to be applied and an updated device configuration.
Args:
execution_config (Union[ExecutionConfig, Sequence[ExecutionConfig]]): A data structure describing the
parameters needed to fully describe the execution.
Returns:
TransformProgram, ExecutionConfig: A transform program that when called returns :class:`~.QuantumTape`'s that the
device can natively execute as well as a postprocessing function to be called after execution, and a configuration
with unset specifications filled in.
This device currently:
* Does not support derivatives.
* Does not support vector-Jacobian products.
"""
config = self._setup_execution_config(execution_config)
program = TransformProgram()
program.add_transform(validate_measurements, name=self.name)
program.add_transform(validate_observables, accepted_observables, name=self.name)
program.add_transform(validate_device_wires, self._wires, name=self.name)
program.add_transform(
decompose,
stopping_condition=stopping_condition,
stopping_condition_shots=stopping_condition,
skip_initial_state_prep=True,
name=self.name,
)
return program, config
# pylint: disable=unused-argument
[docs] def execute(
self,
circuits: QuantumTape_or_Batch,
execution_config: ExecutionConfig = DefaultExecutionConfig,
) -> Result_or_ResultBatch:
"""Execute a circuit or a batch of circuits and turn it into results.
Args:
circuits (Union[QuantumTape, Sequence[QuantumTape]]): the quantum circuits to be executed.
execution_config (ExecutionConfig): a data structure with additional information required for execution.
Returns:
TensorLike, tuple[TensorLike], tuple[tuple[TensorLike]]: A numeric result of the computation.
"""
results = []
for circuit in circuits:
if self._wire_map is not None:
[circuit], _ = qml.map_wires(circuit, self._wire_map)
results.append(simulate(circuit, self._tensornet()))
return tuple(results)
# pylint: disable=unused-argument
[docs] def supports_derivatives(
self,
execution_config: Optional[ExecutionConfig] = None,
circuit: Optional[qml.tape.QuantumTape] = None,
) -> bool:
"""Check whether or not derivatives are available for a given configuration and circuit.
Args:
execution_config (ExecutionConfig): The configuration of the desired derivative calculation.
circuit (QuantumTape): An optional circuit to check derivatives support for.
Returns:
Bool: Whether or not a derivative can be calculated provided the given information.
"""
return False
[docs] def compute_derivatives(
self,
circuits: QuantumTape_or_Batch,
execution_config: ExecutionConfig = DefaultExecutionConfig,
):
"""Calculate the Jacobian of either a single or a batch of circuits on the device.
Args:
circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuits to calculate derivatives for.
execution_config (ExecutionConfig): a data structure with all additional information required for execution.
Returns:
Tuple: The Jacobian for each trainable parameter.
"""
raise NotImplementedError(
"The computation of derivatives has yet to be implemented for the lightning.tensor device."
)
[docs] def execute_and_compute_derivatives(
self,
circuits: QuantumTape_or_Batch,
execution_config: ExecutionConfig = DefaultExecutionConfig,
):
"""Compute the results and Jacobians of circuits at the same time.
Args:
circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuits or batch of circuits.
execution_config (ExecutionConfig): a data structure with all additional information required for execution.
Returns:
tuple: A numeric result of the computation and the gradient.
"""
raise NotImplementedError(
"The computation of derivatives has yet to be implemented for the lightning.tensor device."
)
# pylint: disable=unused-argument
[docs] def supports_vjp(
self,
execution_config: Optional[ExecutionConfig] = None,
circuit: Optional[QuantumTape] = None,
) -> bool:
"""Whether or not this device defines a custom vector-Jacobian product.
Args:
execution_config (ExecutionConfig): The configuration of the desired derivative calculation.
circuit (QuantumTape): An optional circuit to check derivatives support for.
Returns:
Bool: Whether or not a derivative can be calculated provided the given information.
"""
return False
[docs] def compute_vjp(
self,
circuits: QuantumTape_or_Batch,
cotangents: Tuple[Number],
execution_config: ExecutionConfig = DefaultExecutionConfig,
):
r"""The vector-Jacobian product used in reverse-mode differentiation.
Args:
circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuit or batch of circuits.
cotangents (Tuple[Number, Tuple[Number]]): Gradient-output vector. Must have shape matching the output shape of the
corresponding circuit. If the circuit has a single output, ``cotangents`` may be a single number, not an iterable
of numbers.
execution_config (ExecutionConfig): a data structure with all additional information required for execution.
Returns:
tensor-like: A numeric result of computing the vector-Jacobian product.
"""
raise NotImplementedError(
"The computation of vector-Jacobian product has yet to be implemented for the lightning.tensor device."
)
[docs] def execute_and_compute_vjp(
self,
circuits: QuantumTape_or_Batch,
cotangents: Tuple[Number],
execution_config: ExecutionConfig = DefaultExecutionConfig,
):
"""Calculate both the results and the vector-Jacobian product used in reverse-mode differentiation.
Args:
circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuit or batch of circuits to be executed.
cotangents (Tuple[Number, Tuple[Number]]): Gradient-output vector. Must have shape matching the output shape of the
corresponding circuit.
execution_config (ExecutionConfig): a data structure with all additional information required for execution.
Returns:
Tuple, Tuple: the result of executing the scripts and the numeric result of computing the vector-Jacobian product
"""
raise NotImplementedError(
"The computation of vector-Jacobian product has yet to be implemented for the lightning.tensor device."
)
_modules/pennylane_lightning/lightning_tensor/lightning_tensor
Download Python script
Download Notebook
View on GitHub