diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 5d447801e9..8678453f99 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -41,6 +41,9 @@ ### Breaking changes +* Update `lightning.gpu` and `lightning.kokkos` to raise an error instead of falling back to `default.qubit`. + [(#689)](https://github.com/PennyLaneAI/pennylane-lightning/pull/689) + * Add `paths` directives to test workflows to avoid running tests that cannot be impacted by changes. [(#699)](https://github.com/PennyLaneAI/pennylane-lightning/pull/699) [(#695)](https://github.com/PennyLaneAI/pennylane-lightning/pull/695) diff --git a/.github/workflows/tests_linux_python.yml b/.github/workflows/tests_linux_python.yml index 5e6d9f0af9..17be3c3575 100644 --- a/.github/workflows/tests_linux_python.yml +++ b/.github/workflows/tests_linux_python.yml @@ -364,7 +364,7 @@ jobs: mv .coverage .coverage-${{ github.job }}-${{ matrix.pl_backend }} # TODO: Remove this if-cond with release v0.36.0 if [ -f tests/test_native_mcm.py ]; then - OMP_NUM_THREADS=1 PL_DEVICE=${DEVICENAME} python -m pytest -n auto tests/ -k "test_native_mcm" $COVERAGE_FLAGS --cov-append + OMP_NUM_THREADS=1 PL_DEVICE=${DEVICENAME} python -m pytest -n auto tests/test_native_mcm.py $COVERAGE_FLAGS --cov-append fi - name: Install all backend devices @@ -418,7 +418,7 @@ jobs: - name: Combine coverage files run: | python -m pip install coverage - python -m coverage combine .coverage-python* + python -m coverage combine .coverage* # Added cov xml -i to ignore "No source for code" random errors # https://stackoverflow.com/questions/2386975/no-source-for-code-message-in-coverage-py python -m coverage xml -i -o coverage-${{ github.job }}.xml diff --git a/.github/workflows/wheel_linux_aarch64.yml b/.github/workflows/wheel_linux_aarch64.yml index 6c670d6e59..498bdcce6a 100644 --- a/.github/workflows/wheel_linux_aarch64.yml +++ b/.github/workflows/wheel_linux_aarch64.yml @@ -141,7 +141,7 @@ jobs: CIBW_BEFORE_TEST: | python -m pip install -r requirements-tests.txt - if ${{ matrix.pl_backend == 'lightning_kokkos'}}; then SKIP_COMPILATION=True PL_BACKEND="lightning_qubit" pip install -e . -vv; fi + if ${{ matrix.pl_backend == 'lightning_kokkos'}}; then SKIP_COMPILATION=True PL_BACKEND="lightning_qubit" python -m pip install . -vv; fi CIBW_TEST_COMMAND: | DEVICENAME=`echo ${{ matrix.pl_backend }} | sed "s/_/./g"` diff --git a/.github/workflows/wheel_linux_x86_64.yml b/.github/workflows/wheel_linux_x86_64.yml index 0b42cdf085..d11649c4f0 100644 --- a/.github/workflows/wheel_linux_x86_64.yml +++ b/.github/workflows/wheel_linux_x86_64.yml @@ -156,7 +156,7 @@ jobs: CIBW_BEFORE_TEST: | python -m pip install -r requirements-tests.txt - if ${{ matrix.pl_backend == 'lightning_kokkos'}}; then SKIP_COMPILATION=True PL_BACKEND="lightning_qubit" pip install -e . -vv; fi + if ${{ matrix.pl_backend == 'lightning_kokkos'}}; then SKIP_COMPILATION=True PL_BACKEND="lightning_qubit" python -m pip install . -vv; fi CIBW_TEST_COMMAND: | DEVICENAME=`echo ${{ matrix.pl_backend }} | sed "s/_/./g"` diff --git a/.github/workflows/wheel_macos_arm64.yml b/.github/workflows/wheel_macos_arm64.yml index 57f8683cb3..d45e8df566 100644 --- a/.github/workflows/wheel_macos_arm64.yml +++ b/.github/workflows/wheel_macos_arm64.yml @@ -96,7 +96,7 @@ jobs: CIBW_BEFORE_TEST: | python -m pip install -r requirements-tests.txt - if ${{ matrix.pl_backend == 'lightning_kokkos'}}; then SKIP_COMPILATION=True PL_BACKEND="lightning_qubit" pip install -e . -vv; fi + if ${{ matrix.pl_backend == 'lightning_kokkos'}}; then SKIP_COMPILATION=True PL_BACKEND="lightning_qubit" python -m pip install . -vv; fi CIBW_TEST_COMMAND: | DEVICENAME=`echo ${{ matrix.pl_backend }} | sed "s/_/./g"` diff --git a/.github/workflows/wheel_macos_x86_64.yml b/.github/workflows/wheel_macos_x86_64.yml index ac8d493f55..534b8eb541 100644 --- a/.github/workflows/wheel_macos_x86_64.yml +++ b/.github/workflows/wheel_macos_x86_64.yml @@ -144,7 +144,7 @@ jobs: CIBW_BEFORE_TEST: | python -m pip install -r requirements-tests.txt - if ${{ matrix.pl_backend == 'lightning_kokkos'}}; then SKIP_COMPILATION=True PL_BACKEND="lightning_qubit" pip install -e . -vv; fi + if ${{ matrix.pl_backend == 'lightning_kokkos'}}; then SKIP_COMPILATION=True PL_BACKEND="lightning_qubit" python -m pip install . -vv; fi CIBW_TEST_COMMAND: | DEVICENAME=`echo ${{ matrix.pl_backend }} | sed "s/_/./g"` diff --git a/.github/workflows/wheel_win_x86_64.yml b/.github/workflows/wheel_win_x86_64.yml index 04861ae5fb..af4164b6d5 100644 --- a/.github/workflows/wheel_win_x86_64.yml +++ b/.github/workflows/wheel_win_x86_64.yml @@ -173,7 +173,7 @@ jobs: python -m pip install setuptools python -m pip install -r requirements-tests.txt if (${{ matrix.pl_backend == 'lightning_kokkos'}}) { - SKIP_COMPILATION=True PL_BACKEND="lightning_qubit" python -m pip install -e . -vv + SKIP_COMPILATION=True PL_BACKEND="lightning_qubit" python -m pip install . -vv } pushd wheelhouse $wheels = Get-ChildItem "./" -Filter *.whl diff --git a/.gitignore b/.gitignore index df49993fc2..ad4069550b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,37 @@ -venv/ -kokkos/ -prototypes/ -doc/_build/ -doc/code/api/ -PennyLane_Lightning.egg-info/ -PennyLane_Lightning_Kokkos.egg-info/ -build/ -build_lightning_*/ -Build/ -BuildCov/ -BuildGBench/ -BuildTests/ -BuildTidy/ -dist/ -tests/__pycache__/ -.idea -*.ipynp -*.ipynb_checkpoints __pycache__ -.pytest_cache/ -coverage_html_report/ +.cache/* .coverage -*.so -cpptests -*.o .DS_Store -.cache/* +.idea +.pytest_cache/ .vscode/ .ycm_extra_conf.py +*.ipynb_checkpoints +*.ipynp +*.o +*.so /.vs +/PennyLane_Lightning* /pennylane_lightning/.vs /pennylane_lightning/*.pyd -/pennylane_lightning/src/Kokkos/ /pennylane_lightning/src/GBenchmarks/ -/PennyLane_Lightning* \ No newline at end of file +/pennylane_lightning/src/Kokkos/ +build_lightning_*/ +build/ +Build/ +BuildCov/ +BuildGBench/ +BuildTests/ +BuildTidy/ +coverage_html_report/ +cpptests +dist/ +doc/_build/ +doc/code/api/ +kokkos/ +PennyLane_Lightning_Kokkos.egg-info/ +PennyLane_Lightning.egg-info/ +pennylane_lightning/core/src/utils/config.h +prototypes/ +tests/__pycache__/ +venv/ \ No newline at end of file diff --git a/mpitests/test_adjoint_jacobian.py b/mpitests/test_adjoint_jacobian.py index b5b6f56906..4b6eef51e3 100644 --- a/mpitests/test_adjoint_jacobian.py +++ b/mpitests/test_adjoint_jacobian.py @@ -28,6 +28,9 @@ from pennylane import qnode from scipy.stats import unitary_group +if not ld._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) + I, X, Y, Z = ( np.eye(2), qml.PauliX.compute_matrix(), @@ -109,12 +112,10 @@ def test_not_expval(self, dev): qml.RX(0.1, wires=0) qml.state() - if device_name == "lightning.gpu" and ld._CPP_BINARY_AVAILABLE: + if device_name == "lightning.gpu": message = "Adjoint differentiation does not support State measurements." - elif ld._CPP_BINARY_AVAILABLE: - message = "This method does not support statevector return type." else: - message = "Adjoint differentiation method does not support measurement StateMP" + message = "Adjoint differentiation method does not support measurement StateMP." with pytest.raises( qml.QuantumFunctionError, match=message, @@ -144,7 +145,6 @@ def test_empty_measurements(self, dev): jac = dev.adjoint_jacobian(tape) assert len(jac) == 0 - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_unsupported_op(self, dev): """Test if a QuantumFunctionError is raised for an unsupported operation, i.e., multi-parameter operations that are not qml.Rot""" @@ -159,7 +159,6 @@ def test_unsupported_op(self, dev): ): dev.adjoint_jacobian(tape) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_proj_unsupported(self, dev): """Test if a QuantumFunctionError is raised for a Projector observable""" with qml.tape.QuantumTape() as tape: @@ -332,7 +331,6 @@ def test_multiple_rx_gradient_expval_hermitian(self, tol, dev): qubit_ops = [getattr(qml, name) for name in qml.ops._qubit__ops__] # pylint: disable=no-member ops = {qml.RX, qml.RY, qml.RZ, qml.PhaseShift, qml.CRX, qml.CRY, qml.CRZ, qml.Rot} - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_multiple_rx_gradient_expval_hamiltonian(self, tol, dev): """Tests that the gradient of multiple RX gates in a circuit yields the correct result with Hermitian observable @@ -522,7 +520,6 @@ def test_gradient_gate_with_multiple_parameters_hermitian(self, dev): # the different methods agree assert np.allclose(grad_D, grad_F, atol=tol, rtol=0) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_gradient_gate_with_multiple_parameters_hamiltonian(self, dev): """Tests that gates with multiple free parameters yield correct gradients.""" x, y, z = [0.5, 0.3, -0.7] @@ -601,7 +598,6 @@ def test_provide_starting_state(self, tol, dev): dM2 = dev.adjoint_jacobian(tape, starting_state=state_vector) assert np.allclose(dM1, dM2, atol=tol, rtol=0) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_provide_wrong_starting_state(self, dev): """Tests raise an exception when provided starting state mismatches.""" x, y, z = [0.5, 0.3, -0.7] @@ -624,7 +620,6 @@ def test_provide_wrong_starting_state(self, dev): device_name == "lightning.gpu", reason="Adjoint differentiation does not support State measurements.", ) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_state_return_type(self, dev): """Tests raise an exception when the return type is State""" with qml.tape.QuantumTape() as tape: @@ -635,7 +630,7 @@ def test_state_return_type(self, dev): with pytest.raises( qml.QuantumFunctionError, - match="This method does not support statevector return type.", + match="Adjoint differentiation method does not support measurement StateMP.", ): dev.adjoint_jacobian(tape) @@ -675,7 +670,6 @@ def circ(x): ): qml.grad(circ)(0.1) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_qnode(self, mocker, dev): """Test that specifying diff_method allows the adjoint method to be selected""" args = np.array([0.54, 0.1, 0.5], requires_grad=True) @@ -766,7 +760,6 @@ def cost(p1, p2): assert np.allclose(grad_D[0], expected, atol=tol, rtol=0) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_gradient_repeated_gate_parameters(self, mocker, dev): """Tests that repeated use of a free parameter in a multi-parameter gate yields correct gradients.""" diff --git a/pennylane_lightning/core/lightning_base.py b/pennylane_lightning/core/lightning_base.py index 38183ea784..af45c04cc6 100644 --- a/pennylane_lightning/core/lightning_base.py +++ b/pennylane_lightning/core/lightning_base.py @@ -22,7 +22,6 @@ import numpy as np import pennylane as qml from pennylane import BasisState, QubitDevice, StatePrep -from pennylane.devices import DefaultQubitLegacy from pennylane.measurements import Expectation, MeasurementProcess, State from pennylane.operation import Operation, Tensor from pennylane.ops import Prod, Projector, SProd, Sum @@ -71,6 +70,12 @@ def __init__( shots=None, batch_obs=False, ): + if not self._CPP_BINARY_AVAILABLE: + raise ImportError( + f"Pre-compiled binaries for {self.short_name} are not available. " + "To manually compile from source, follow the instructions at " + "https://pennylane-lightning.readthedocs.io/en/latest/installation.html." + ) if c_dtype is np.complex64: r_dtype = np.float32 self.use_csingle = True @@ -446,31 +451,3 @@ def processing_fns(tapes): return vjps return processing_fns - - -class LightningBaseFallBack(DefaultQubitLegacy): # pragma: no cover - # pylint: disable=missing-class-docstring, too-few-public-methods - pennylane_requires = ">=0.34" - version = __version__ - author = "Xanadu Inc." - _CPP_BINARY_AVAILABLE = False - _new_API = False - - def __init__(self, wires, *, c_dtype=np.complex128, **kwargs): - if c_dtype is np.complex64: - r_dtype = np.float32 - elif c_dtype is np.complex128: - r_dtype = np.float64 - else: - raise TypeError(f"Unsupported complex type: {c_dtype}") - super().__init__(wires, r_dtype=r_dtype, c_dtype=c_dtype, **kwargs) - - @property - def state_vector(self): - """Returns a handle to the statevector.""" - return self._state - - @property - def dtype(self): - """State vector complex data type.""" - return self.C_DTYPE diff --git a/pennylane_lightning/lightning_gpu/lightning_gpu.py b/pennylane_lightning/lightning_gpu/lightning_gpu.py index 864ee6b507..34748f7627 100644 --- a/pennylane_lightning/lightning_gpu/lightning_gpu.py +++ b/pennylane_lightning/lightning_gpu/lightning_gpu.py @@ -17,14 +17,28 @@ interfaces with the NVIDIA cuQuantum cuStateVec simulator library for GPU-enabled calculations. """ +from ctypes.util import find_library +from importlib import util as imp_util +from itertools import product from pathlib import Path +from typing import List, Union from warnings import warn import numpy as np +import pennylane as qml +from pennylane import BasisState, DeviceError, QuantumFunctionError, Rot, StatePrep, math +from pennylane.measurements import Expectation, State +from pennylane.ops.op_math import Adjoint +from pennylane.wires import Wires -from pennylane_lightning.core.lightning_base import LightningBase, LightningBaseFallBack +from pennylane_lightning.core._serialize import QuantumScriptSerializer, global_phase_diagonal +from pennylane_lightning.core._version import __version__ + +# pylint: disable=import-error, no-name-in-module, ungrouped-imports +from pennylane_lightning.core.lightning_base import LightningBase try: + from pennylane_lightning.lightning_gpu_ops import ( DevPool, MeasurementsC64, @@ -35,6 +49,12 @@ get_gpu_arch, is_gpu_supported, ) + from pennylane_lightning.lightning_gpu_ops.algorithms import ( + AdjointJacobianC64, + AdjointJacobianC128, + create_ops_listC64, + create_ops_listC128, + ) try: # pylint: disable=no-name-in-module @@ -46,14 +66,17 @@ StateVectorMPIC64, StateVectorMPIC128, ) + from pennylane_lightning.lightning_gpu_ops.algorithmsMPI import ( + AdjointJacobianMPIC64, + AdjointJacobianMPIC128, + create_ops_listMPIC64, + create_ops_listMPIC128, + ) MPI_SUPPORT = True except ImportError: MPI_SUPPORT = False - from ctypes.util import find_library - from importlib import util as imp_util - if find_library("custatevec") is None and not imp_util.find_spec( "cuquantum" ): # pragma: no cover @@ -67,909 +90,861 @@ raise ValueError(f"CUDA device is an unsupported version: {get_gpu_arch()}") LGPU_CPP_BINARY_AVAILABLE = True -# except (ModuleNotFoundError, ImportError, ValueError) as e: except (ImportError, ValueError) as e: warn(str(e), UserWarning) + backend_info = None LGPU_CPP_BINARY_AVAILABLE = False -if LGPU_CPP_BINARY_AVAILABLE: - from itertools import product - from typing import List, Union - - import pennylane as qml - from pennylane import BasisState, DeviceError, QuantumFunctionError, Rot, StatePrep, math - from pennylane.measurements import Expectation, State - from pennylane.ops.op_math import Adjoint - from pennylane.wires import Wires - # pylint: disable=import-error, no-name-in-module, ungrouped-imports - from pennylane_lightning.core._serialize import QuantumScriptSerializer, global_phase_diagonal - from pennylane_lightning.core._version import __version__ - - # pylint: disable=no-name-in-module, ungrouped-imports - from pennylane_lightning.lightning_gpu_ops.algorithms import ( - AdjointJacobianC64, - AdjointJacobianC128, - create_ops_listC64, - create_ops_listC128, - ) - - if MPI_SUPPORT: - from pennylane_lightning.lightning_gpu_ops.algorithmsMPI import ( - AdjointJacobianMPIC64, - AdjointJacobianMPIC128, - create_ops_listMPIC64, - create_ops_listMPIC128, - ) - - def _gpu_dtype(dtype, mpi=False): - if dtype not in [np.complex128, np.complex64]: # pragma: no cover - raise ValueError(f"Data type is not supported for state-vector computation: {dtype}") - if mpi: - return StateVectorMPIC128 if dtype == np.complex128 else StateVectorMPIC64 - return StateVectorC128 if dtype == np.complex128 else StateVectorC64 - - def _adj_dtype(use_csingle, mpi=False): - if mpi: - return AdjointJacobianMPIC64 if use_csingle else AdjointJacobianMPIC128 - return AdjointJacobianC64 if use_csingle else AdjointJacobianC128 - - def _mebibytesToBytes(mebibytes): - return mebibytes * 1024 * 1024 - - allowed_operations = { - "Identity", - "BasisState", - "QubitStateVector", - "StatePrep", - "QubitUnitary", - "ControlledQubitUnitary", - "MultiControlledX", - "DiagonalQubitUnitary", - "PauliX", - "PauliY", - "PauliZ", - "MultiRZ", - "GlobalPhase", - "C(GlobalPhase)", - "Hadamard", - "S", - "Adjoint(S)", - "T", - "Adjoint(T)", - "SX", - "Adjoint(SX)", - "CNOT", - "SWAP", - "ISWAP", - "PSWAP", - "Adjoint(ISWAP)", - "SISWAP", - "Adjoint(SISWAP)", - "SQISW", - "CSWAP", - "Toffoli", - "CY", - "CZ", - "PhaseShift", - "ControlledPhaseShift", - "CPhase", - "RX", - "RY", - "RZ", - "Rot", - "CRX", - "CRY", - "CRZ", - "CRot", - "IsingXX", - "IsingYY", - "IsingZZ", - "IsingXY", - "SingleExcitation", - "SingleExcitationPlus", - "SingleExcitationMinus", - "DoubleExcitation", - "DoubleExcitationPlus", - "DoubleExcitationMinus", - "QubitCarry", - "QubitSum", - "OrbitalRotation", - "QFT", - "ECR", - "BlockEncode", - } - - allowed_observables = { - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", - "SparseHamiltonian", - "Hamiltonian", - "LinearCombination", - "Hermitian", - "Identity", - "Sum", - "Prod", - "SProd", - } - - gate_cache_needs_hash = ( - qml.BlockEncode, - qml.ControlledQubitUnitary, - qml.DiagonalQubitUnitary, - qml.MultiControlledX, - qml.OrbitalRotation, - qml.PSWAP, - qml.QubitUnitary, - ) - - class LightningGPU(LightningBase): # pylint: disable=too-many-instance-attributes - """PennyLane Lightning GPU device. - - A GPU-backed Lightning device using NVIDIA cuQuantum SDK. - - Use of this device requires pre-built binaries or compilation from source. Check out the - :doc:`/lightning_gpu/installation` guide for more details. +def _gpu_dtype(dtype, mpi=False): + if dtype not in [np.complex128, np.complex64]: # pragma: no cover + raise ValueError(f"Data type is not supported for state-vector computation: {dtype}") + if mpi: + return StateVectorMPIC128 if dtype == np.complex128 else StateVectorMPIC64 + return StateVectorC128 if dtype == np.complex128 else StateVectorC64 + + +def _adj_dtype(use_csingle, mpi=False): + if mpi: + return AdjointJacobianMPIC64 if use_csingle else AdjointJacobianMPIC128 + return AdjointJacobianC64 if use_csingle else AdjointJacobianC128 + + +def _mebibytesToBytes(mebibytes): + return mebibytes * 1024 * 1024 + + +allowed_operations = { + "Identity", + "BasisState", + "QubitStateVector", + "StatePrep", + "QubitUnitary", + "ControlledQubitUnitary", + "MultiControlledX", + "DiagonalQubitUnitary", + "PauliX", + "PauliY", + "PauliZ", + "MultiRZ", + "GlobalPhase", + "C(GlobalPhase)", + "Hadamard", + "S", + "Adjoint(S)", + "T", + "Adjoint(T)", + "SX", + "Adjoint(SX)", + "CNOT", + "SWAP", + "ISWAP", + "PSWAP", + "Adjoint(ISWAP)", + "SISWAP", + "Adjoint(SISWAP)", + "SQISW", + "CSWAP", + "Toffoli", + "CY", + "CZ", + "PhaseShift", + "ControlledPhaseShift", + "CPhase", + "RX", + "RY", + "RZ", + "Rot", + "CRX", + "CRY", + "CRZ", + "CRot", + "IsingXX", + "IsingYY", + "IsingZZ", + "IsingXY", + "SingleExcitation", + "SingleExcitationPlus", + "SingleExcitationMinus", + "DoubleExcitation", + "DoubleExcitationPlus", + "DoubleExcitationMinus", + "QubitCarry", + "QubitSum", + "OrbitalRotation", + "QFT", + "ECR", + "BlockEncode", +} + +allowed_observables = { + "PauliX", + "PauliY", + "PauliZ", + "Hadamard", + "SparseHamiltonian", + "Hamiltonian", + "LinearCombination", + "Hermitian", + "Identity", + "Sum", + "Prod", + "SProd", +} + +gate_cache_needs_hash = ( + qml.BlockEncode, + qml.ControlledQubitUnitary, + qml.DiagonalQubitUnitary, + qml.MultiControlledX, + qml.OrbitalRotation, + qml.PSWAP, + qml.QubitUnitary, +) + + +class LightningGPU(LightningBase): # pylint: disable=too-many-instance-attributes + """PennyLane Lightning GPU device. + + A GPU-backed Lightning device using NVIDIA cuQuantum SDK. + + Use of this device requires pre-built binaries or compilation from source. Check out the + :doc:`/lightning_gpu/installation` guide for more details. + + Args: + wires (int): the number of wires to initialize the device with + mpi (bool): enable MPI support. MPI support will be enabled if ``mpi`` is set as``True``. + mpi_buf_size (int): size of GPU memory (in MiB) set for MPI operation and its default value is 64 MiB. + sync (bool): immediately sync with host-sv after applying operations + c_dtype: Datatypes for statevector representation. Must be one of ``np.complex64`` or ``np.complex128``. + shots (int): How many times the circuit should be evaluated (or sampled) to estimate + the expectation values. Defaults to ``None`` if not specified. Setting + to ``None`` results in computing statistics like expectation values and + variances analytically. + batch_obs (Union[bool, int]): determine whether to use multiple GPUs within the same node or not + """ + + name = "Lightning GPU PennyLane plugin" + short_name = "lightning.gpu" + + operations = allowed_operations + observables = allowed_observables + _backend_info = backend_info + config = Path(__file__).parent / "lightning_gpu.toml" + _CPP_BINARY_AVAILABLE = LGPU_CPP_BINARY_AVAILABLE + + def __init__( + self, + wires, + *, + mpi: bool = False, + mpi_buf_size: int = 0, + sync=False, + c_dtype=np.complex128, + shots=None, + batch_obs: Union[bool, int] = False, + ): # pylint: disable=too-many-arguments + if c_dtype is np.complex64: + self.use_csingle = True + elif c_dtype is np.complex128: + self.use_csingle = False + else: + raise TypeError(f"Unsupported complex type: {c_dtype}") + + super().__init__(wires, shots=shots, c_dtype=c_dtype) + + self._dp = DevPool() + + if not mpi: + self._mpi = False + self._num_local_wires = self.num_wires + self._gpu_state = _gpu_dtype(c_dtype)(self._num_local_wires) + else: + self._mpi = True + self._mpi_init_helper(self.num_wires) + + if mpi_buf_size < 0: + raise TypeError(f"Unsupported mpi_buf_size value: {mpi_buf_size}") + + if mpi_buf_size: + if mpi_buf_size & (mpi_buf_size - 1): + raise TypeError( + f"Unsupported mpi_buf_size value: {mpi_buf_size}. mpi_buf_size should be power of 2." + ) + # Memory size in bytes + sv_memsize = np.dtype(c_dtype).itemsize * (1 << self._num_local_wires) + if _mebibytesToBytes(mpi_buf_size) > sv_memsize: + w_msg = "The MPI buffer size is larger than the local state vector size." + warn( + w_msg, + RuntimeWarning, + ) - Args: - wires (int): the number of wires to initialize the device with - mpi (bool): enable MPI support. MPI support will be enabled if ``mpi`` is set as``True``. - mpi_buf_size (int): size of GPU memory (in MiB) set for MPI operation and its default value is 64 MiB. - sync (bool): immediately sync with host-sv after applying operations - c_dtype: Datatypes for statevector representation. Must be one of ``np.complex64`` or ``np.complex128``. - shots (int): How many times the circuit should be evaluated (or sampled) to estimate - the expectation values. Defaults to ``None`` if not specified. Setting - to ``None`` results in computing statistics like expectation values and - variances analytically. - batch_obs (Union[bool, int]): determine whether to use multiple GPUs within the same node or not - """ + self._gpu_state = _gpu_dtype(c_dtype, mpi)( + self._mpi_manager, + self._devtag, + mpi_buf_size, + self._num_global_wires, + self._num_local_wires, + ) - name = "Lightning GPU PennyLane plugin" - short_name = "lightning.gpu" - - operations = allowed_operations - observables = allowed_observables - _backend_info = backend_info - config = Path(__file__).parent / "lightning_gpu.toml" - - def __init__( - self, - wires, - *, - mpi: bool = False, - mpi_buf_size: int = 0, - sync=False, - c_dtype=np.complex128, - shots=None, - batch_obs: Union[bool, int] = False, - ): # pylint: disable=too-many-arguments - if c_dtype is np.complex64: - self.use_csingle = True - elif c_dtype is np.complex128: - self.use_csingle = False - else: - raise TypeError(f"Unsupported complex type: {c_dtype}") + self._sync = sync + self._batch_obs = batch_obs + self._create_basis_state(0) + + def _mpi_init_helper(self, num_wires): + """Set up MPI checks.""" + if not MPI_SUPPORT: + raise ImportError("MPI related APIs are not found.") + # initialize MPIManager and config check in the MPIManager ctor + self._mpi_manager = MPIManager() + # check if number of GPUs per node is larger than + # number of processes per node + numDevices = self._dp.getTotalDevices() + numProcsNode = self._mpi_manager.getSizeNode() + if numDevices < numProcsNode: + raise ValueError( + "Number of devices should be larger than or equal to the number of processes on each node." + ) + # check if the process number is larger than number of statevector elements + if self._mpi_manager.getSize() > (1 << (num_wires - 1)): + raise ValueError( + "Number of processes should be smaller than the number of statevector elements." + ) + # set the number of global and local wires + commSize = self._mpi_manager.getSize() + self._num_global_wires = commSize.bit_length() - 1 + self._num_local_wires = num_wires - self._num_global_wires + # set GPU device + rank = self._mpi_manager.getRank() + deviceid = rank % numProcsNode + self._dp.setDeviceID(deviceid) + self._devtag = DevTag(deviceid) + + @staticmethod + def _asarray(arr, dtype=None): + arr = np.asarray(arr) # arr is not copied + + if arr.dtype.kind not in ["f", "c"]: + return arr - super().__init__(wires, shots=shots, c_dtype=c_dtype) + if not dtype: + dtype = arr.dtype - self._dp = DevPool() + return arr - if not mpi: - self._mpi = False - self._num_local_wires = self.num_wires - self._gpu_state = _gpu_dtype(c_dtype)(self._num_local_wires) - else: - self._mpi = True - self._mpi_init_helper(self.num_wires) - - if mpi_buf_size < 0: - raise TypeError(f"Unsupported mpi_buf_size value: {mpi_buf_size}") - - if mpi_buf_size: - if mpi_buf_size & (mpi_buf_size - 1): - raise TypeError( - f"Unsupported mpi_buf_size value: {mpi_buf_size}. mpi_buf_size should be power of 2." - ) - # Memory size in bytes - sv_memsize = np.dtype(c_dtype).itemsize * (1 << self._num_local_wires) - if _mebibytesToBytes(mpi_buf_size) > sv_memsize: - w_msg = "The MPI buffer size is larger than the local state vector size." - warn( - w_msg, - RuntimeWarning, - ) - - self._gpu_state = _gpu_dtype(c_dtype, mpi)( - self._mpi_manager, - self._devtag, - mpi_buf_size, - self._num_global_wires, - self._num_local_wires, - ) + # pylint disable=missing-function-docstring + def reset(self): + """Reset the device""" + super().reset() + # init the state vector to |00..0> + self._gpu_state.resetGPU(False) # Sync reset - self._sync = sync - self._batch_obs = batch_obs - self._create_basis_state(0) - - def _mpi_init_helper(self, num_wires): - """Set up MPI checks.""" - if not MPI_SUPPORT: - raise ImportError("MPI related APIs are not found.") - # initialize MPIManager and config check in the MPIManager ctor - self._mpi_manager = MPIManager() - # check if number of GPUs per node is larger than - # number of processes per node - numDevices = self._dp.getTotalDevices() - numProcsNode = self._mpi_manager.getSizeNode() - if numDevices < numProcsNode: - raise ValueError( - "Number of devices should be larger than or equal to the number of processes on each node." - ) - # check if the process number is larger than number of statevector elements - if self._mpi_manager.getSize() > (1 << (num_wires - 1)): - raise ValueError( - "Number of processes should be smaller than the number of statevector elements." - ) - # set the number of global and local wires - commSize = self._mpi_manager.getSize() - self._num_global_wires = commSize.bit_length() - 1 - self._num_local_wires = num_wires - self._num_global_wires - # set GPU device - rank = self._mpi_manager.getRank() - deviceid = rank % numProcsNode - self._dp.setDeviceID(deviceid) - self._devtag = DevTag(deviceid) - - @staticmethod - def _asarray(arr, dtype=None): - arr = np.asarray(arr) # arr is not copied - - if arr.dtype.kind not in ["f", "c"]: - return arr - - if not dtype: - dtype = arr.dtype + @property + def state(self): + # pylint disable=missing-function-docstring + """Copy the state vector data from the device to the host. - return arr + A state vector Numpy array is explicitly allocated on the host to store and return the data. - # pylint disable=missing-function-docstring - def reset(self): - """Reset the device""" - super().reset() - # init the state vector to |00..0> - self._gpu_state.resetGPU(False) # Sync reset - - @property - def state(self): - # pylint disable=missing-function-docstring - """Copy the state vector data from the device to the host. - - A state vector Numpy array is explicitly allocated on the host to store and return the data. - - **Example** - - >>> dev = qml.device('lightning.gpu', wires=1) - >>> dev.apply([qml.PauliX(wires=[0])]) - >>> print(dev.state) - [0.+0.j 1.+0.j] - """ - state = np.zeros(1 << self._num_local_wires, dtype=self.C_DTYPE) - state = self._asarray(state, dtype=self.C_DTYPE) - self.syncD2H(state) - return state - - @property - def create_ops_list(self): - """Returns create_ops_list function of the matching precision.""" - if self._mpi: - return create_ops_listMPIC64 if self.use_csingle else create_ops_listMPIC128 - return create_ops_listC64 if self.use_csingle else create_ops_listC128 + **Example** - @property - def measurements(self): - """Returns Measurements constructor of the matching precision.""" - if self._mpi: - return ( - MeasurementsMPIC64(self._gpu_state) - if self.use_csingle - else MeasurementsMPIC128(self._gpu_state) - ) + >>> dev = qml.device('lightning.gpu', wires=1) + >>> dev.apply([qml.PauliX(wires=[0])]) + >>> print(dev.state) + [0.+0.j 1.+0.j] + """ + state = np.zeros(1 << self._num_local_wires, dtype=self.C_DTYPE) + state = self._asarray(state, dtype=self.C_DTYPE) + self.syncD2H(state) + return state + + @property + def create_ops_list(self): + """Returns create_ops_list function of the matching precision.""" + if self._mpi: + return create_ops_listMPIC64 if self.use_csingle else create_ops_listMPIC128 + return create_ops_listC64 if self.use_csingle else create_ops_listC128 + + @property + def measurements(self): + """Returns Measurements constructor of the matching precision.""" + if self._mpi: return ( - MeasurementsC64(self._gpu_state) + MeasurementsMPIC64(self._gpu_state) if self.use_csingle - else MeasurementsC128(self._gpu_state) + else MeasurementsMPIC128(self._gpu_state) ) + return ( + MeasurementsC64(self._gpu_state) + if self.use_csingle + else MeasurementsC128(self._gpu_state) + ) - def syncD2H(self, state_vector, use_async=False): - """Copy the state vector data on device to a state vector on the host provided by the user - Args: - state_vector(array[complex]): the state vector array on host - use_async(bool): indicates whether to use asynchronous memory copy from host to device or not. - Note: This function only supports synchronized memory copy. - - **Example** - >>> dev = qml.device('lightning.gpu', wires=1) - >>> dev.apply([qml.PauliX(wires=[0])]) - >>> state_vector = np.zeros(2**dev.num_wires).astype(dev.C_DTYPE) - >>> dev.syncD2H(state_vector) - >>> print(state_vector) - [0.+0.j 1.+0.j] - """ - self._gpu_state.DeviceToHost(state_vector.ravel(order="C"), use_async) - - def syncH2D(self, state_vector, use_async=False): - """Copy the state vector data on host provided by the user to the state vector on the device - Args: - state_vector(array[complex]): the state vector array on host. - use_async(bool): indicates whether to use asynchronous memory copy from host to device or not. - Note: This function only supports synchronized memory copy. - - **Example** - >>> dev = qml.device('lightning.gpu', wires=3) - >>> obs = qml.Identity(0) @ qml.PauliX(1) @ qml.PauliY(2) - >>> obs1 = qml.Identity(1) - >>> H = qml.Hamiltonian([1.0, 1.0], [obs1, obs]) - >>> state_vector = np.array([0.0 + 0.0j, 0.0 + 0.1j, 0.1 + 0.1j, 0.1 + 0.2j, - 0.2 + 0.2j, 0.3 + 0.3j, 0.3 + 0.4j, 0.4 + 0.5j,], dtype=np.complex64,) - >>> dev.syncH2D(state_vector) - >>> res = dev.expval(H) - >>> print(res) - 1.0 - """ - self._gpu_state.HostToDevice(state_vector.ravel(order="C"), use_async) - - def _create_basis_state(self, index, use_async=False): - """Return a computational basis state over all wires. - Args: - index (int): integer representing the computational basis state. - use_async(bool): indicates whether to use asynchronous memory copy from host to device or not. - Note: This function only supports synchronized memory copy. - """ - self._gpu_state.setBasisState(index, use_async) - - def _apply_state_vector(self, state, device_wires, use_async=False): - """Initialize the state vector on GPU with a specified state on host. - Note that any use of this method will introduce host-overheads. - Args: - state (array[complex]): normalized input state (on host) of length ``2**len(wires)`` - or broadcasted state of shape ``(batch_size, 2**len(wires))`` - device_wires (Wires): wires that get initialized in the state + def syncD2H(self, state_vector, use_async=False): + """Copy the state vector data on device to a state vector on the host provided by the user + Args: + state_vector(array[complex]): the state vector array on host use_async(bool): indicates whether to use asynchronous memory copy from host to device or not. - Note: This function only supports synchronized memory copy from host to device. - """ - # translate to wire labels used by device - device_wires = self.map_wires(device_wires) - - state = self._asarray(state, dtype=self.C_DTYPE) # this operation on host - output_shape = [2] * self._num_local_wires - - if len(device_wires) == self.num_wires and Wires(sorted(device_wires)) == device_wires: - # Initialize the entire device state with the input state - if self.num_wires == self._num_local_wires: - self.syncH2D(self._reshape(state, output_shape)) - return - local_state = np.zeros(1 << self._num_local_wires, dtype=self.C_DTYPE) - self._mpi_manager.Scatter(state, local_state, 0) - # Initialize the entire device state with the input state - self.syncH2D(self._reshape(local_state, output_shape)) - return - - # generate basis states on subset of qubits via the cartesian product - basis_states = np.array(list(product([0, 1], repeat=len(device_wires)))) + Note: This function only supports synchronized memory copy. + + **Example** + >>> dev = qml.device('lightning.gpu', wires=1) + >>> dev.apply([qml.PauliX(wires=[0])]) + >>> state_vector = np.zeros(2**dev.num_wires).astype(dev.C_DTYPE) + >>> dev.syncD2H(state_vector) + >>> print(state_vector) + [0.+0.j 1.+0.j] + """ + self._gpu_state.DeviceToHost(state_vector.ravel(order="C"), use_async) - # get basis states to alter on full set of qubits - unravelled_indices = np.zeros((2 ** len(device_wires), self.num_wires), dtype=int) - unravelled_indices[:, device_wires] = basis_states + def syncH2D(self, state_vector, use_async=False): + """Copy the state vector data on host provided by the user to the state vector on the device + Args: + state_vector(array[complex]): the state vector array on host. + use_async(bool): indicates whether to use asynchronous memory copy from host to device or not. + Note: This function only supports synchronized memory copy. + + **Example** + >>> dev = qml.device('lightning.gpu', wires=3) + >>> obs = qml.Identity(0) @ qml.PauliX(1) @ qml.PauliY(2) + >>> obs1 = qml.Identity(1) + >>> H = qml.Hamiltonian([1.0, 1.0], [obs1, obs]) + >>> state_vector = np.array([0.0 + 0.0j, 0.0 + 0.1j, 0.1 + 0.1j, 0.1 + 0.2j, + 0.2 + 0.2j, 0.3 + 0.3j, 0.3 + 0.4j, 0.4 + 0.5j,], dtype=np.complex64,) + >>> dev.syncH2D(state_vector) + >>> res = dev.expval(H) + >>> print(res) + 1.0 + """ + self._gpu_state.HostToDevice(state_vector.ravel(order="C"), use_async) - # get indices for which the state is changed to input state vector elements - ravelled_indices = np.ravel_multi_index(unravelled_indices.T, [2] * self.num_wires) + def _create_basis_state(self, index, use_async=False): + """Return a computational basis state over all wires. + Args: + index (int): integer representing the computational basis state. + use_async(bool): indicates whether to use asynchronous memory copy from host to device or not. + Note: This function only supports synchronized memory copy. + """ + self._gpu_state.setBasisState(index, use_async) - # set the state vector on GPU with the unravelled_indices and their corresponding values - self._gpu_state.setStateVector( - ravelled_indices, state, use_async - ) # this operation on device + def _apply_state_vector(self, state, device_wires, use_async=False): + """Initialize the state vector on GPU with a specified state on host. + Note that any use of this method will introduce host-overheads. + Args: + state (array[complex]): normalized input state (on host) of length ``2**len(wires)`` + or broadcasted state of shape ``(batch_size, 2**len(wires))`` + device_wires (Wires): wires that get initialized in the state + use_async(bool): indicates whether to use asynchronous memory copy from host to device or not. + Note: This function only supports synchronized memory copy from host to device. + """ + # translate to wire labels used by device + device_wires = self.map_wires(device_wires) - def _apply_basis_state(self, state, wires): - """Initialize the state vector in a specified computational basis state on GPU directly. - Args: - state (array[int]): computational basis state (on host) of shape ``(wires,)`` - consisting of 0s and 1s. - wires (Wires): wires that the provided computational state should be initialized on - Note: This function does not support broadcasted inputs yet. - """ - # translate to wire labels used by device - device_wires = self.map_wires(wires) + state = self._asarray(state, dtype=self.C_DTYPE) # this operation on host + output_shape = [2] * self._num_local_wires - # length of basis state parameter - n_basis_state = len(state) - state = state.tolist() if hasattr(state, "tolist") else state - if not set(state).issubset({0, 1}): - raise ValueError("BasisState parameter must consist of 0 or 1 integers.") + if len(device_wires) == self.num_wires and Wires(sorted(device_wires)) == device_wires: + # Initialize the entire device state with the input state + if self.num_wires == self._num_local_wires: + self.syncH2D(self._reshape(state, output_shape)) + return + local_state = np.zeros(1 << self._num_local_wires, dtype=self.C_DTYPE) + self._mpi_manager.Scatter(state, local_state, 0) + # Initialize the entire device state with the input state + self.syncH2D(self._reshape(local_state, output_shape)) + return - if n_basis_state != len(device_wires): - raise ValueError("BasisState parameter and wires must be of equal length.") + # generate basis states on subset of qubits via the cartesian product + basis_states = np.array(list(product([0, 1], repeat=len(device_wires)))) - # get computational basis state number - basis_states = 2 ** (self.num_wires - 1 - np.array(device_wires)) - basis_states = qml.math.convert_like(basis_states, state) - num = int(qml.math.dot(state, basis_states)) + # get basis states to alter on full set of qubits + unravelled_indices = np.zeros((2 ** len(device_wires), self.num_wires), dtype=int) + unravelled_indices[:, device_wires] = basis_states - self._create_basis_state(num) + # get indices for which the state is changed to input state vector elements + ravelled_indices = np.ravel_multi_index(unravelled_indices.T, [2] * self.num_wires) - def apply_lightning(self, operations): - """Apply a list of operations to the state tensor. + # set the state vector on GPU with the unravelled_indices and their corresponding values + self._gpu_state.setStateVector( + ravelled_indices, state, use_async + ) # this operation on device + def _apply_basis_state(self, state, wires): + """Initialize the state vector in a specified computational basis state on GPU directly. Args: - operations (list[~pennylane.operation.Operation]): operations to apply - dtype (type): Type of numpy ``complex`` to be used. Can be important - to specify for large systems for memory allocation purposes. - - Returns: - array[complex]: the output state tensor - """ - # Skip over identity operations instead of performing - # matrix multiplication with the identity. - for ops in operations: - if isinstance(ops, qml.Identity): - continue - if isinstance(ops, Adjoint): - name = ops.base.name - invert_param = True - else: - name = ops.name - invert_param = False - method = getattr(self._gpu_state, name, None) - wires = self.wires.indices(ops.wires) - - if isinstance(ops, qml.ops.op_math.Controlled) and isinstance( - ops.base, qml.GlobalPhase - ): - controls = ops.control_wires - control_values = ops.control_values - param = ops.base.parameters[0] - matrix = global_phase_diagonal(param, self.wires, controls, control_values) - self._gpu_state.apply(name, wires, False, [], matrix) - elif method is None: - # Inverse can be set to False since qml.matrix(ops) is already in inverted form - try: - mat = qml.matrix(ops) - except AttributeError: # pragma: no cover - # To support older versions of PL - mat = ops.matrix - r_dtype = np.float32 if self.use_csingle else np.float64 - param = [[r_dtype(ops.hash)]] if isinstance(ops, gate_cache_needs_hash) else [] - if len(mat) == 0: - raise ValueError("Unsupported operation") - self._gpu_state.apply( - name, - wires, - False, - param, - mat.ravel(order="C"), # inv = False: Matrix already in correct form; - ) # Parameters can be ignored for explicit matrices; F-order for cuQuantum + state (array[int]): computational basis state (on host) of shape ``(wires,)`` + consisting of 0s and 1s. + wires (Wires): wires that the provided computational state should be initialized on + Note: This function does not support broadcasted inputs yet. + """ + # translate to wire labels used by device + device_wires = self.map_wires(wires) - else: - param = ops.parameters - method(wires, invert_param, param) - - # pylint: disable=unused-argument - def apply(self, operations, rotations=None, **kwargs): - """Applies a list of operations to the state tensor.""" - # State preparation is currently done in Python - if operations: # make sure operations[0] exists - if isinstance(operations[0], StatePrep): - self._apply_state_vector( - operations[0].parameters[0].copy(), operations[0].wires - ) - operations = operations[1:] - elif isinstance(operations[0], BasisState): - self._apply_basis_state(operations[0].parameters[0], operations[0].wires) - operations = operations[1:] - - for operation in operations: - if isinstance(operation, (StatePrep, BasisState)): - raise DeviceError( - f"Operation {operation.name} cannot be used after other " - + f"Operations have already been applied on a {self.short_name} device." - ) + # length of basis state parameter + n_basis_state = len(state) + state = state.tolist() if hasattr(state, "tolist") else state + if not set(state).issubset({0, 1}): + raise ValueError("BasisState parameter must consist of 0 or 1 integers.") - self.apply_lightning(operations) + if n_basis_state != len(device_wires): + raise ValueError("BasisState parameter and wires must be of equal length.") - @staticmethod - def _check_adjdiff_supported_operations(operations): - """Check Lightning adjoint differentiation method support for a tape. + # get computational basis state number + basis_states = 2 ** (self.num_wires - 1 - np.array(device_wires)) + basis_states = qml.math.convert_like(basis_states, state) + num = int(qml.math.dot(state, basis_states)) - Raise ``QuantumFunctionError`` if ``tape`` contains not supported measurements, - observables, or operations by the Lightning adjoint differentiation method. + self._create_basis_state(num) - Args: - tape (.QuantumTape): quantum tape to differentiate. - """ - for op in operations: - if op.num_params > 1 and not isinstance(op, Rot): - raise QuantumFunctionError( - f"The {op.name} operation is not supported using " - 'the "adjoint" differentiation method' - ) + def apply_lightning(self, operations): + """Apply a list of operations to the state tensor. - def _init_process_jacobian_tape(self, tape, starting_state, use_device_state): - """Generate an initial state vector for ``_process_jacobian_tape``.""" - if starting_state is not None: - if starting_state.size != 2 ** len(self.wires): - raise QuantumFunctionError( - "The number of qubits of starting_state must be the same as " - "that of the device." - ) - self._apply_state_vector(starting_state, self.wires) - elif not use_device_state: - self.reset() - self.apply(tape.operations) - return self._gpu_state - - # pylint: disable=too-many-branches - def adjoint_jacobian(self, tape, starting_state=None, use_device_state=False): - """Implements the adjoint method outlined in - `Jones and Gacon `__ to differentiate an input tape. - - After a forward pass, the circuit is reversed by iteratively applying adjoint - gates to scan backwards through the circuit. - """ - if self.shots is not None: - warn( - "Requested adjoint differentiation to be computed with finite shots." - " The derivative is always exact when using the adjoint differentiation method.", - UserWarning, + Args: + operations (list[~pennylane.operation.Operation]): operations to apply + dtype (type): Type of numpy ``complex`` to be used. Can be important + to specify for large systems for memory allocation purposes. + + Returns: + array[complex]: the output state tensor + """ + # Skip over identity operations instead of performing + # matrix multiplication with the identity. + for ops in operations: + if isinstance(ops, qml.Identity): + continue + if isinstance(ops, Adjoint): + name = ops.base.name + invert_param = True + else: + name = ops.name + invert_param = False + method = getattr(self._gpu_state, name, None) + wires = self.wires.indices(ops.wires) + + if isinstance(ops, qml.ops.op_math.Controlled) and isinstance( + ops.base, qml.GlobalPhase + ): + controls = ops.control_wires + control_values = ops.control_values + param = ops.base.parameters[0] + matrix = global_phase_diagonal(param, self.wires, controls, control_values) + self._gpu_state.apply(name, wires, False, [], matrix) + elif method is None: + # Inverse can be set to False since qml.matrix(ops) is already in inverted form + try: + mat = qml.matrix(ops) + except AttributeError: # pragma: no cover + # To support older versions of PL + mat = ops.matrix + r_dtype = np.float32 if self.use_csingle else np.float64 + param = [[r_dtype(ops.hash)]] if isinstance(ops, gate_cache_needs_hash) else [] + if len(mat) == 0: + raise ValueError("Unsupported operation") + self._gpu_state.apply( + name, + wires, + False, + param, + mat.ravel(order="C"), # inv = False: Matrix already in correct form; + ) # Parameters can be ignored for explicit matrices; F-order for cuQuantum + + else: + param = ops.parameters + method(wires, invert_param, param) + + # pylint: disable=unused-argument + def apply(self, operations, rotations=None, **kwargs): + """Applies a list of operations to the state tensor.""" + # State preparation is currently done in Python + if operations: # make sure operations[0] exists + if isinstance(operations[0], StatePrep): + self._apply_state_vector(operations[0].parameters[0].copy(), operations[0].wires) + operations = operations[1:] + elif isinstance(operations[0], BasisState): + self._apply_basis_state(operations[0].parameters[0], operations[0].wires) + operations = operations[1:] + + for operation in operations: + if isinstance(operation, (StatePrep, BasisState)): + raise DeviceError( + f"Operation {operation.name} cannot be used after other " + + f"Operations have already been applied on a {self.short_name} device." ) - tape_return_type = self._check_adjdiff_supported_measurements(tape.measurements) + self.apply_lightning(operations) - if not tape_return_type: # the tape does not have measurements - return np.array([], dtype=self.state.dtype) + @staticmethod + def _check_adjdiff_supported_operations(operations): + """Check Lightning adjoint differentiation method support for a tape. - if tape_return_type is State: # pragma: no cover + Raise ``QuantumFunctionError`` if ``tape`` contains not supported measurements, + observables, or operations by the Lightning adjoint differentiation method. + + Args: + tape (.QuantumTape): quantum tape to differentiate. + """ + for op in operations: + if op.num_params > 1 and not isinstance(op, Rot): + raise QuantumFunctionError( + f"The {op.name} operation is not supported using " + 'the "adjoint" differentiation method' + ) + + def _init_process_jacobian_tape(self, tape, starting_state, use_device_state): + """Generate an initial state vector for ``_process_jacobian_tape``.""" + if starting_state is not None: + if starting_state.size != 2 ** len(self.wires): raise QuantumFunctionError( - "This method does not support statevector return type. " - "Use vjp method instead for this purpose." + "The number of qubits of starting_state must be the same as " + "that of the device." ) + self._apply_state_vector(starting_state, self.wires) + elif not use_device_state: + self.reset() + self.apply(tape.operations) + return self._gpu_state + + # pylint: disable=too-many-branches + def adjoint_jacobian(self, tape, starting_state=None, use_device_state=False): + """Implements the adjoint method outlined in + `Jones and Gacon `__ to differentiate an input tape. + + After a forward pass, the circuit is reversed by iteratively applying adjoint + gates to scan backwards through the circuit. + """ + if self.shots is not None: + warn( + "Requested adjoint differentiation to be computed with finite shots." + " The derivative is always exact when using the adjoint differentiation method.", + UserWarning, + ) - # Check adjoint diff support - self._check_adjdiff_supported_operations(tape.operations) + tape_return_type = self._check_adjdiff_supported_measurements(tape.measurements) - processed_data = self._process_jacobian_tape( - tape, starting_state, use_device_state, self._mpi, self._batch_obs + if not tape_return_type: # the tape does not have measurements + return np.array([], dtype=self.state.dtype) + + if tape_return_type is State: # pragma: no cover + raise QuantumFunctionError( + "Adjoint differentiation method does not support measurement StateMP." + "Use vjp method instead for this purpose." ) - if not processed_data: # training_params is empty - return np.array([], dtype=self.state.dtype) - - trainable_params = processed_data["tp_shift"] - # pylint: disable=pointless-string-statement - """ - This path enables controlled batching over the requested observables, be they explicit, or part of a Hamiltonian. - The traditional path will assume there exists enough free memory to preallocate all arrays and run through each observable iteratively. - However, for larger system, this becomes impossible, and we hit memory issues very quickly. the batching support here enables several functionalities: - - Pre-allocate memory for all observables on the primary GPU (`batch_obs=False`, default behaviour): This is the simplest path, and works best for few observables, and moderate qubit sizes. All memory is preallocated for each observable, and run through iteratively on a single GPU. - - Evenly distribute the observables over all available GPUs (`batch_obs=True`): This will evenly split the data into ceil(num_obs/num_gpus) chunks, and allocate enough space on each GPU up-front before running through them concurrently. This relies on C++ threads to handle the orchestration. - - Allocate at most `n` observables per GPU (`batch_obs=n`): Providing an integer value restricts each available GPU to at most `n` copies of the statevector, and hence `n` given observables for a given batch. This will iterate over the data in chnuks of size `n*num_gpus`. - """ - adjoint_jacobian = _adj_dtype(self.use_csingle, self._mpi)() - - if self._batch_obs: # Batching of Measurements - if not self._mpi: # Single-node path, controlled batching over available GPUs - num_obs = len(processed_data["obs_serialized"]) - batch_size = ( - num_obs - if isinstance(self._batch_obs, bool) - else self._batch_obs * self._dp.getTotalDevices() - ) - jac = [] - for chunk in range(0, num_obs, batch_size): - obs_chunk = processed_data["obs_serialized"][chunk : chunk + batch_size] - jac_chunk = adjoint_jacobian.batched( - self._gpu_state, - obs_chunk, - processed_data["ops_serialized"], - trainable_params, - ) - jac.extend(jac_chunk) - else: # MPI path, restrict memory per known GPUs - jac = adjoint_jacobian.batched( + # Check adjoint diff support + self._check_adjdiff_supported_operations(tape.operations) + + processed_data = self._process_jacobian_tape( + tape, starting_state, use_device_state, self._mpi, self._batch_obs + ) + + if not processed_data: # training_params is empty + return np.array([], dtype=self.state.dtype) + + trainable_params = processed_data["tp_shift"] + # pylint: disable=pointless-string-statement + """ + This path enables controlled batching over the requested observables, be they explicit, or part of a Hamiltonian. + The traditional path will assume there exists enough free memory to preallocate all arrays and run through each observable iteratively. + However, for larger system, this becomes impossible, and we hit memory issues very quickly. the batching support here enables several functionalities: + - Pre-allocate memory for all observables on the primary GPU (`batch_obs=False`, default behaviour): This is the simplest path, and works best for few observables, and moderate qubit sizes. All memory is preallocated for each observable, and run through iteratively on a single GPU. + - Evenly distribute the observables over all available GPUs (`batch_obs=True`): This will evenly split the data into ceil(num_obs/num_gpus) chunks, and allocate enough space on each GPU up-front before running through them concurrently. This relies on C++ threads to handle the orchestration. + - Allocate at most `n` observables per GPU (`batch_obs=n`): Providing an integer value restricts each available GPU to at most `n` copies of the statevector, and hence `n` given observables for a given batch. This will iterate over the data in chnuks of size `n*num_gpus`. + """ + adjoint_jacobian = _adj_dtype(self.use_csingle, self._mpi)() + + if self._batch_obs: # Batching of Measurements + if not self._mpi: # Single-node path, controlled batching over available GPUs + num_obs = len(processed_data["obs_serialized"]) + batch_size = ( + num_obs + if isinstance(self._batch_obs, bool) + else self._batch_obs * self._dp.getTotalDevices() + ) + jac = [] + for chunk in range(0, num_obs, batch_size): + obs_chunk = processed_data["obs_serialized"][chunk : chunk + batch_size] + jac_chunk = adjoint_jacobian.batched( self._gpu_state, - processed_data["obs_serialized"], + obs_chunk, processed_data["ops_serialized"], trainable_params, ) - - else: - jac = adjoint_jacobian( + jac.extend(jac_chunk) + else: # MPI path, restrict memory per known GPUs + jac = adjoint_jacobian.batched( self._gpu_state, processed_data["obs_serialized"], processed_data["ops_serialized"], trainable_params, ) - jac = np.array(jac) # only for parameters differentiable with the adjoint method - jac = jac.reshape(-1, len(trainable_params)) - jac_r = np.zeros((len(tape.observables), processed_data["all_params"])) - if not self._batch_obs: - jac_r[:, processed_data["record_tp_rows"]] = jac - else: - # Reduce over decomposed expval(H), if required. - for idx in range(len(processed_data["obs_idx_offsets"][0:-1])): - if ( - processed_data["obs_idx_offsets"][idx + 1] - - processed_data["obs_idx_offsets"][idx] - ) > 1: - jac_r[idx, :] = np.sum( - jac[ - processed_data["obs_idx_offsets"][idx] : processed_data[ - "obs_idx_offsets" - ][idx + 1], - :, - ], - axis=0, - ) - else: - jac_r[idx, :] = jac[ + else: + jac = adjoint_jacobian( + self._gpu_state, + processed_data["obs_serialized"], + processed_data["ops_serialized"], + trainable_params, + ) + + jac = np.array(jac) # only for parameters differentiable with the adjoint method + jac = jac.reshape(-1, len(trainable_params)) + jac_r = np.zeros((len(tape.observables), processed_data["all_params"])) + if not self._batch_obs: + jac_r[:, processed_data["record_tp_rows"]] = jac + else: + # Reduce over decomposed expval(H), if required. + for idx in range(len(processed_data["obs_idx_offsets"][0:-1])): + if ( + processed_data["obs_idx_offsets"][idx + 1] + - processed_data["obs_idx_offsets"][idx] + ) > 1: + jac_r[idx, :] = np.sum( + jac[ processed_data["obs_idx_offsets"][idx] : processed_data[ "obs_idx_offsets" ][idx + 1], :, - ] + ], + axis=0, + ) + else: + jac_r[idx, :] = jac[ + processed_data["obs_idx_offsets"][idx] : processed_data["obs_idx_offsets"][ + idx + 1 + ], + :, + ] - return self._adjoint_jacobian_processing(jac_r) + return self._adjoint_jacobian_processing(jac_r) - # pylint: disable=inconsistent-return-statements, line-too-long, missing-function-docstring - def vjp(self, measurements, grad_vec, starting_state=None, use_device_state=False): - """Generate the processing function required to compute the vector-Jacobian products - of a tape. + # pylint: disable=inconsistent-return-statements, line-too-long, missing-function-docstring + def vjp(self, measurements, grad_vec, starting_state=None, use_device_state=False): + """Generate the processing function required to compute the vector-Jacobian products + of a tape. - This function can be used with multiple expectation values or a quantum state. - When a quantum state is given, + This function can be used with multiple expectation values or a quantum state. + When a quantum state is given, - .. code-block:: python + .. code-block:: python - vjp_f = dev.vjp([qml.state()], grad_vec) - vjp = vjp_f(tape) + vjp_f = dev.vjp([qml.state()], grad_vec) + vjp = vjp_f(tape) - computes :math:`w = (w_1,\\cdots,w_m)` where + computes :math:`w = (w_1,\\cdots,w_m)` where - .. math:: + .. math:: - w_k = \\langle v| \\frac{\\partial}{\\partial \\theta_k} | \\psi_{\\pmb{\\theta}} \\rangle. + w_k = \\langle v| \\frac{\\partial}{\\partial \\theta_k} | \\psi_{\\pmb{\\theta}} \\rangle. - Here, :math:`m` is the total number of trainable parameters, - :math:`\\pmb{\\theta}` is the vector of trainable parameters and - :math:`\\psi_{\\pmb{\\theta}}` is the output quantum state. + Here, :math:`m` is the total number of trainable parameters, + :math:`\\pmb{\\theta}` is the vector of trainable parameters and + :math:`\\psi_{\\pmb{\\theta}}` is the output quantum state. - Args: - measurements (list): List of measurement processes for vector-Jacobian product. - Now it must be expectation values or a quantum state. - grad_vec (tensor_like): Gradient-output vector. Must have shape matching the output - shape of the corresponding tape, i.e. number of measurements if the return - type is expectation or :math:`2^N` if the return type is statevector - starting_state (tensor_like): post-forward pass state to start execution with. - It should be complex-valued. Takes precedence over ``use_device_state``. - use_device_state (bool): use current device state to initialize. - A forward pass of the same circuit should be the last thing the device - has executed. If a ``starting_state`` is provided, that takes precedence. - - Returns: - The processing function required to compute the vector-Jacobian products of a tape. - """ - if self.shots is not None: - warn( - "Requested adjoint differentiation to be computed with finite shots." - " The derivative is always exact when using the adjoint differentiation method.", - UserWarning, - ) + Args: + measurements (list): List of measurement processes for vector-Jacobian product. + Now it must be expectation values or a quantum state. + grad_vec (tensor_like): Gradient-output vector. Must have shape matching the output + shape of the corresponding tape, i.e. number of measurements if the return + type is expectation or :math:`2^N` if the return type is statevector + starting_state (tensor_like): post-forward pass state to start execution with. + It should be complex-valued. Takes precedence over ``use_device_state``. + use_device_state (bool): use current device state to initialize. + A forward pass of the same circuit should be the last thing the device + has executed. If a ``starting_state`` is provided, that takes precedence. + + Returns: + The processing function required to compute the vector-Jacobian products of a tape. + """ + if self.shots is not None: + warn( + "Requested adjoint differentiation to be computed with finite shots." + " The derivative is always exact when using the adjoint differentiation method.", + UserWarning, + ) - tape_return_type = self._check_adjdiff_supported_measurements(measurements) + tape_return_type = self._check_adjdiff_supported_measurements(measurements) - if math.allclose(grad_vec, 0) or tape_return_type is None: - return lambda tape: math.convert_like( - np.zeros(len(tape.trainable_params)), grad_vec + if math.allclose(grad_vec, 0) or tape_return_type is None: + return lambda tape: math.convert_like(np.zeros(len(tape.trainable_params)), grad_vec) + + if tape_return_type is Expectation: + if len(grad_vec) != len(measurements): + raise ValueError( + "Number of observables in the tape must be the same as the length of grad_vec in the vjp method" ) - if tape_return_type is Expectation: - if len(grad_vec) != len(measurements): - raise ValueError( - "Number of observables in the tape must be the same as the length of grad_vec in the vjp method" - ) + if np.iscomplexobj(grad_vec): + raise ValueError( + "The vjp method only works with a real-valued grad_vec when the tape is returning an expectation value" + ) - if np.iscomplexobj(grad_vec): - raise ValueError( - "The vjp method only works with a real-valued grad_vec when the tape is returning an expectation value" - ) + ham = qml.Hamiltonian(grad_vec, [m.obs for m in measurements]) - ham = qml.Hamiltonian(grad_vec, [m.obs for m in measurements]) + # pylint: disable=protected-access + def processing_fn(tape): + nonlocal ham + num_params = len(tape.trainable_params) - # pylint: disable=protected-access - def processing_fn(tape): - nonlocal ham - num_params = len(tape.trainable_params) + if num_params == 0: + return np.array([], dtype=self.state.dtype) - if num_params == 0: - return np.array([], dtype=self.state.dtype) + new_tape = tape.copy() + new_tape._measurements = [qml.expval(ham)] - new_tape = tape.copy() - new_tape._measurements = [qml.expval(ham)] + return self.adjoint_jacobian(new_tape, starting_state, use_device_state) - return self.adjoint_jacobian(new_tape, starting_state, use_device_state) + return processing_fn - return processing_fn + # pylint: disable=attribute-defined-outside-init + def sample(self, observable, shot_range=None, bin_size=None, counts=False): + """Return samples of an observable.""" + diagonalizing_gates = observable.diagonalizing_gates() + if diagonalizing_gates: + self.apply(diagonalizing_gates) + if not isinstance(observable, qml.PauliZ): + self._samples = self.generate_samples() + results = super().sample( + observable, shot_range=shot_range, bin_size=bin_size, counts=counts + ) + if diagonalizing_gates: + self.apply([qml.adjoint(g, lazy=False) for g in reversed(diagonalizing_gates)]) + return results - # pylint: disable=attribute-defined-outside-init - def sample(self, observable, shot_range=None, bin_size=None, counts=False): - """Return samples of an observable.""" - diagonalizing_gates = observable.diagonalizing_gates() - if diagonalizing_gates: - self.apply(diagonalizing_gates) - if not isinstance(observable, qml.PauliZ): - self._samples = self.generate_samples() - results = super().sample( - observable, shot_range=shot_range, bin_size=bin_size, counts=counts - ) - if diagonalizing_gates: - self.apply([qml.adjoint(g, lazy=False) for g in reversed(diagonalizing_gates)]) - return results - - def generate_samples(self): - """Generate samples - - Returns: - array[int]: array of samples in binary representation with shape - ``(dev.shots, dev.num_wires)`` - """ - return self.measurements.generate_samples(len(self.wires), self.shots).astype( - int, copy=False - ) + def generate_samples(self): + """Generate samples - # pylint: disable=protected-access - def expval(self, observable, shot_range=None, bin_size=None): - """Expectation value of the supplied observable. + Returns: + array[int]: array of samples in binary representation with shape + ``(dev.shots, dev.num_wires)`` + """ + return self.measurements.generate_samples(len(self.wires), self.shots).astype( + int, copy=False + ) - Args: - observable: A PennyLane observable. - shot_range (tuple[int]): 2-tuple of integers specifying the range of samples - to use. If not specified, all samples are used. - bin_size (int): Divides the shot range into bins of size ``bin_size``, and - returns the measurement statistic separately over each bin. If not - provided, the entire shot range is treated as a single bin. - - Returns: - Expectation value of the observable - """ - if self.shots is not None: - # estimate the expectation value - samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) - return np.squeeze(np.mean(samples, axis=0)) - - if isinstance(observable, qml.SparseHamiltonian): - if self._mpi: - # Identity for CSR_SparseHamiltonian to pass to processes with rank != 0 to reduce - # host(cpu) memory requirements - obs = qml.Identity(0) - Hmat = qml.Hamiltonian([1.0], [obs]).sparse_matrix() - H_sparse = qml.SparseHamiltonian(Hmat, wires=range(1)) - CSR_SparseHamiltonian = H_sparse.sparse_matrix().tocsr() - # CSR_SparseHamiltonian for rank == 0 - if self._mpi_manager.getRank() == 0: - CSR_SparseHamiltonian = observable.sparse_matrix().tocsr() - else: - CSR_SparseHamiltonian = observable.sparse_matrix().tocsr() + # pylint: disable=protected-access + def expval(self, observable, shot_range=None, bin_size=None): + """Expectation value of the supplied observable. - return self.measurements.expval( - CSR_SparseHamiltonian.indptr, - CSR_SparseHamiltonian.indices, - CSR_SparseHamiltonian.data, - ) + Args: + observable: A PennyLane observable. + shot_range (tuple[int]): 2-tuple of integers specifying the range of samples + to use. If not specified, all samples are used. + bin_size (int): Divides the shot range into bins of size ``bin_size``, and + returns the measurement statistic separately over each bin. If not + provided, the entire shot range is treated as a single bin. + + Returns: + Expectation value of the observable + """ + if self.shots is not None: + # estimate the expectation value + samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) + return np.squeeze(np.mean(samples, axis=0)) - # use specialized functors to compute expval(Hermitian) - if isinstance(observable, qml.Hermitian): - observable_wires = self.map_wires(observable.wires) - if self._mpi and len(observable_wires) > self._num_local_wires: - raise RuntimeError( - "MPI backend does not support Hermitian with number of target wires larger than local wire number." - ) - matrix = observable.matrix() - return self.measurements.expval(matrix, observable_wires) + if isinstance(observable, qml.SparseHamiltonian): + if self._mpi: + # Identity for CSR_SparseHamiltonian to pass to processes with rank != 0 to reduce + # host(cpu) memory requirements + obs = qml.Identity(0) + Hmat = qml.Hamiltonian([1.0], [obs]).sparse_matrix() + H_sparse = qml.SparseHamiltonian(Hmat, wires=range(1)) + CSR_SparseHamiltonian = H_sparse.sparse_matrix().tocsr() + # CSR_SparseHamiltonian for rank == 0 + if self._mpi_manager.getRank() == 0: + CSR_SparseHamiltonian = observable.sparse_matrix().tocsr() + else: + CSR_SparseHamiltonian = observable.sparse_matrix().tocsr() - if ( - isinstance(observable, qml.ops.Hamiltonian) - or (observable.arithmetic_depth > 0) - or isinstance(observable.name, List) - ): - ob_serialized = QuantumScriptSerializer( - self.short_name, self.use_csingle, self._mpi - )._ob(observable, self.wire_map) - return self.measurements.expval(ob_serialized) + return self.measurements.expval( + CSR_SparseHamiltonian.indptr, + CSR_SparseHamiltonian.indices, + CSR_SparseHamiltonian.data, + ) - # translate to wire labels used by device + # use specialized functors to compute expval(Hermitian) + if isinstance(observable, qml.Hermitian): observable_wires = self.map_wires(observable.wires) + if self._mpi and len(observable_wires) > self._num_local_wires: + raise RuntimeError( + "MPI backend does not support Hermitian with number of target wires larger than local wire number." + ) + matrix = observable.matrix() + return self.measurements.expval(matrix, observable_wires) - return self.measurements.expval(observable.name, observable_wires) + if ( + isinstance(observable, qml.ops.Hamiltonian) + or (observable.arithmetic_depth > 0) + or isinstance(observable.name, List) + ): + ob_serialized = QuantumScriptSerializer( + self.short_name, self.use_csingle, self._mpi + )._ob(observable, self.wire_map) + return self.measurements.expval(ob_serialized) - def probability_lightning(self, wires=None): - """Return the probability of each computational basis state. + # translate to wire labels used by device + observable_wires = self.map_wires(observable.wires) - Args: - wires (Iterable[Number, str], Number, str, Wires): wires to return - marginal probabilities for. Wires not provided are traced out of the system. - - Returns: - array[float]: list of the probabilities - """ - # translate to wire labels used by device - observable_wires = self.map_wires(wires) - # Device returns as col-major orderings, so perform transpose on data for bit-index shuffle for now. - local_prob = self.measurements.probs(observable_wires) - if len(local_prob) > 0: - num_local_wires = len(local_prob).bit_length() - 1 if len(local_prob) > 0 else 0 - return local_prob.reshape([2] * num_local_wires).transpose().reshape(-1) - return local_prob - - def var(self, observable, shot_range=None, bin_size=None): - """Variance of the supplied observable. + return self.measurements.expval(observable.name, observable_wires) - Args: - observable: A PennyLane observable. - shot_range (tuple[int]): 2-tuple of integers specifying the range of samples - to use. If not specified, all samples are used. - bin_size (int): Divides the shot range into bins of size ``bin_size``, and - returns the measurement statistic separately over each bin. If not - provided, the entire shot range is treated as a single bin. - - Returns: - Variance of the observable - """ - if self.shots is not None: - # estimate the var - # Lightning doesn't support sampling yet - samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) - return np.squeeze(np.var(samples, axis=0)) - - if isinstance(observable, qml.SparseHamiltonian): - csr_hamiltonian = observable.sparse_matrix(wire_order=self.wires).tocsr(copy=False) - return self.measurements.var( - csr_hamiltonian.indptr, - csr_hamiltonian.indices, - csr_hamiltonian.data, - ) + def probability_lightning(self, wires=None): + """Return the probability of each computational basis state. - if ( - isinstance(observable, (qml.ops.Hamiltonian, qml.Hermitian)) - or (observable.arithmetic_depth > 0) - or isinstance(observable.name, List) - ): - ob_serialized = QuantumScriptSerializer( - self.short_name, self.use_csingle, self._mpi - )._ob(observable, self.wire_map) - return self.measurements.var(ob_serialized) + Args: + wires (Iterable[Number, str], Number, str, Wires): wires to return + marginal probabilities for. Wires not provided are traced out of the system. - # translate to wire labels used by device - observable_wires = self.map_wires(observable.wires) + Returns: + array[float]: list of the probabilities + """ + # translate to wire labels used by device + observable_wires = self.map_wires(wires) + # Device returns as col-major orderings, so perform transpose on data for bit-index shuffle for now. + local_prob = self.measurements.probs(observable_wires) + if len(local_prob) > 0: + num_local_wires = len(local_prob).bit_length() - 1 if len(local_prob) > 0 else 0 + return local_prob.reshape([2] * num_local_wires).transpose().reshape(-1) + return local_prob + + def var(self, observable, shot_range=None, bin_size=None): + """Variance of the supplied observable. - return self.measurements.var(observable.name, observable_wires) + Args: + observable: A PennyLane observable. + shot_range (tuple[int]): 2-tuple of integers specifying the range of samples + to use. If not specified, all samples are used. + bin_size (int): Divides the shot range into bins of size ``bin_size``, and + returns the measurement statistic separately over each bin. If not + provided, the entire shot range is treated as a single bin. + + Returns: + Variance of the observable + """ + if self.shots is not None: + # estimate the var + # Lightning doesn't support sampling yet + samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) + return np.squeeze(np.var(samples, axis=0)) + + if isinstance(observable, qml.SparseHamiltonian): + csr_hamiltonian = observable.sparse_matrix(wire_order=self.wires).tocsr(copy=False) + return self.measurements.var( + csr_hamiltonian.indptr, + csr_hamiltonian.indices, + csr_hamiltonian.data, + ) -else: # LGPU_CPP_BINARY_AVAILABLE: + if ( + isinstance(observable, (qml.Hermitian, qml.ops.Hamiltonian)) + or (observable.arithmetic_depth > 0) + or isinstance(observable.name, List) + ): + ob_serialized = QuantumScriptSerializer( + self.short_name, self.use_csingle, self._mpi + )._ob(observable, self.wire_map) + return self.measurements.var(ob_serialized) - class LightningGPU(LightningBaseFallBack): # pragma: no cover - # pylint: disable=missing-class-docstring, too-few-public-methods - name = "Lightning GPU PennyLane plugin: [No binaries found - Fallback: default.qubit]" - short_name = "lightning.gpu" + # translate to wire labels used by device + observable_wires = self.map_wires(observable.wires) - def __init__(self, wires, *, c_dtype=np.complex128, **kwargs): - w_msg = """ - "Pre-compiled binaries for lightning.gpu are not available. Falling back to " - "using the Python-based default.qubit implementation. To manually compile from " - "source, follow the instructions at " - "https://pennylane-lightning.readthedocs.io/en/latest/installation.html.", - """ - warn( - w_msg, - UserWarning, - ) - super().__init__(wires, c_dtype=c_dtype, **kwargs) + return self.measurements.var(observable.name, observable_wires) diff --git a/pennylane_lightning/lightning_kokkos/lightning_kokkos.py b/pennylane_lightning/lightning_kokkos/lightning_kokkos.py index 5aa01c1f44..4a9eede8ef 100644 --- a/pennylane_lightning/lightning_kokkos/lightning_kokkos.py +++ b/pennylane_lightning/lightning_kokkos/lightning_kokkos.py @@ -17,18 +17,22 @@ interfaces with C++ for fast linear algebra calculations. """ +from os import getenv from pathlib import Path +from typing import List from warnings import warn import numpy as np -from pennylane.measurements import MidMeasureMP +import pennylane as qml +from pennylane import BasisState, DeviceError, QuantumFunctionError, Rot, StatePrep, math +from pennylane.measurements import Expectation, MidMeasureMP, State from pennylane.ops import Conditional +from pennylane.ops.op_math import Adjoint +from pennylane.wires import Wires -from pennylane_lightning.core.lightning_base import ( - LightningBase, - LightningBaseFallBack, - _chunk_iterable, -) +from pennylane_lightning.core._serialize import QuantumScriptSerializer, global_phase_diagonal +from pennylane_lightning.core._version import __version__ +from pennylane_lightning.core.lightning_base import LightningBase, _chunk_iterable try: # pylint: disable=import-error, no-name-in-module @@ -43,24 +47,6 @@ print_configuration, ) - LK_CPP_BINARY_AVAILABLE = True -except ImportError: - LK_CPP_BINARY_AVAILABLE = False - -if LK_CPP_BINARY_AVAILABLE: - from os import getenv - from typing import List - - import pennylane as qml - from pennylane import BasisState, DeviceError, QuantumFunctionError, Rot, StatePrep, math - from pennylane.measurements import Expectation, State - from pennylane.ops.op_math import Adjoint - from pennylane.wires import Wires - - # pylint: disable=import-error, no-name-in-module, ungrouped-imports - from pennylane_lightning.core._serialize import QuantumScriptSerializer, global_phase_diagonal - from pennylane_lightning.core._version import __version__ - # pylint: disable=import-error, no-name-in-module, ungrouped-imports from pennylane_lightning.lightning_kokkos_ops.algorithms import ( AdjointJacobianC64, @@ -69,799 +55,778 @@ create_ops_listC128, ) - def _kokkos_dtype(dtype): - if dtype not in [np.complex128, np.complex64]: # pragma: no cover - raise ValueError(f"Data type is not supported for state-vector computation: {dtype}") - return StateVectorC128 if dtype == np.complex128 else StateVectorC64 - - def _kokkos_configuration(): - return print_configuration() - - allowed_operations = { - "Identity", - "BasisState", - "QubitStateVector", - "StatePrep", - "QubitUnitary", - "ControlledQubitUnitary", - "MultiControlledX", - "DiagonalQubitUnitary", - "PauliX", - "PauliY", - "PauliZ", - "MultiRZ", - "GlobalPhase", - "C(GlobalPhase)", - "Hadamard", - "S", - "Adjoint(S)", - "T", - "Adjoint(T)", - "SX", - "Adjoint(SX)", - "CNOT", - "SWAP", - "ISWAP", - "PSWAP", - "Adjoint(ISWAP)", - "SISWAP", - "Adjoint(SISWAP)", - "SQISW", - "CSWAP", - "Toffoli", - "CY", - "CZ", - "PhaseShift", - "ControlledPhaseShift", - "CPhase", - "RX", - "RY", - "RZ", - "Rot", - "CRX", - "CRY", - "CRZ", - "CRot", - "IsingXX", - "IsingYY", - "IsingZZ", - "IsingXY", - "SingleExcitation", - "SingleExcitationPlus", - "SingleExcitationMinus", - "DoubleExcitation", - "DoubleExcitationPlus", - "DoubleExcitationMinus", - "QubitCarry", - "QubitSum", - "OrbitalRotation", - "QFT", - "ECR", - "BlockEncode", - } - - allowed_observables = { - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", - "Hermitian", - "Identity", - "Projector", - "SparseHamiltonian", - "Hamiltonian", - "LinearCombination", - "Sum", - "SProd", - "Prod", - "Exp", - } - - class LightningKokkos(LightningBase): - """PennyLane Lightning Kokkos device. - - A device that interfaces with C++ to perform fast linear algebra calculations. - - Use of this device requires pre-built binaries or compilation from source. Check out the - :doc:`/lightning_kokkos/installation` guide for more details. + LK_CPP_BINARY_AVAILABLE = True +except ImportError: + LK_CPP_BINARY_AVAILABLE = False + backend_info = None + + +def _kokkos_dtype(dtype): + if dtype not in [np.complex128, np.complex64]: # pragma: no cover + raise ValueError(f"Data type is not supported for state-vector computation: {dtype}") + return StateVectorC128 if dtype == np.complex128 else StateVectorC64 + + +def _kokkos_configuration(): + return print_configuration() + + +allowed_operations = { + "Identity", + "BasisState", + "QubitStateVector", + "StatePrep", + "QubitUnitary", + "ControlledQubitUnitary", + "MultiControlledX", + "DiagonalQubitUnitary", + "PauliX", + "PauliY", + "PauliZ", + "MultiRZ", + "GlobalPhase", + "C(GlobalPhase)", + "Hadamard", + "S", + "Adjoint(S)", + "T", + "Adjoint(T)", + "SX", + "Adjoint(SX)", + "CNOT", + "SWAP", + "ISWAP", + "PSWAP", + "Adjoint(ISWAP)", + "SISWAP", + "Adjoint(SISWAP)", + "SQISW", + "CSWAP", + "Toffoli", + "CY", + "CZ", + "PhaseShift", + "ControlledPhaseShift", + "CPhase", + "RX", + "RY", + "RZ", + "Rot", + "CRX", + "CRY", + "CRZ", + "CRot", + "IsingXX", + "IsingYY", + "IsingZZ", + "IsingXY", + "SingleExcitation", + "SingleExcitationPlus", + "SingleExcitationMinus", + "DoubleExcitation", + "DoubleExcitationPlus", + "DoubleExcitationMinus", + "QubitCarry", + "QubitSum", + "OrbitalRotation", + "QFT", + "ECR", + "BlockEncode", +} + +allowed_observables = { + "PauliX", + "PauliY", + "PauliZ", + "Hadamard", + "Hermitian", + "Identity", + "Projector", + "SparseHamiltonian", + "Hamiltonian", + "LinearCombination", + "Sum", + "SProd", + "Prod", + "Exp", +} + + +class LightningKokkos(LightningBase): + """PennyLane Lightning Kokkos device. + + A device that interfaces with C++ to perform fast linear algebra calculations. + + Use of this device requires pre-built binaries or compilation from source. Check out the + :doc:`/lightning_kokkos/installation` guide for more details. + + Args: + wires (int): the number of wires to initialize the device with + sync (bool): immediately sync with host-sv after applying operations + c_dtype: Datatypes for statevector representation. Must be one of + ``np.complex64`` or ``np.complex128``. + kokkos_args (InitializationSettings): binding for Kokkos::InitializationSettings + (threading parameters). + shots (int): How many times the circuit should be evaluated (or sampled) to estimate + the expectation values. Defaults to ``None`` if not specified. Setting + to ``None`` results in computing statistics like expectation values and + variances analytically. + """ + + name = "Lightning Kokkos PennyLane plugin" + short_name = "lightning.kokkos" + kokkos_config = {} + operations = allowed_operations + observables = allowed_observables + _backend_info = backend_info + config = Path(__file__).parent / "lightning_kokkos.toml" + _CPP_BINARY_AVAILABLE = LK_CPP_BINARY_AVAILABLE + + def __init__( + self, + wires, + *, + sync=True, + c_dtype=np.complex128, + shots=None, + batch_obs=False, + kokkos_args=None, + ): # pylint: disable=unused-argument, too-many-arguments + super().__init__(wires, shots=shots, c_dtype=c_dtype) + + if kokkos_args is None: + self._kokkos_state = _kokkos_dtype(c_dtype)(self.num_wires) + elif isinstance(kokkos_args, InitializationSettings): + self._kokkos_state = _kokkos_dtype(c_dtype)(self.num_wires, kokkos_args) + else: + type0 = type(InitializationSettings()) + raise TypeError( + f"Argument kokkos_args must be of type {type0} but it is of {type(kokkos_args)}." + ) + self._sync = sync + + if not LightningKokkos.kokkos_config: + LightningKokkos.kokkos_config = _kokkos_configuration() + + @property + def stopping_condition(self): + """.BooleanFn: Returns the stopping condition for the device. The returned + function accepts a queueable object (including a PennyLane operation + and observable) and returns ``True`` if supported by the device.""" + fun = super().stopping_condition + + def accepts_obj(obj): + return fun(obj) or isinstance(obj, (qml.measurements.MidMeasureMP, qml.ops.Conditional)) + + return qml.BooleanFn(accepts_obj) + # pylint: disable=missing-function-docstring + @classmethod + def capabilities(cls): + capabilities = super().capabilities().copy() + capabilities.update( + supports_mid_measure=True, + ) + return capabilities + + @staticmethod + def _asarray(arr, dtype=None): + arr = np.asarray(arr) # arr is not copied + + if arr.dtype.kind not in ["f", "c"]: + return arr + + if not dtype: + dtype = arr.dtype + + # We allocate a new aligned memory and copy data to there if alignment + # or dtype mismatches + # Note that get_alignment does not necessarily return CPUMemoryModel(Unaligned) even for + # numpy allocated memory as the memory location happens to be aligned. + if arr.dtype != dtype: + new_arr = allocate_aligned_array(arr.size, np.dtype(dtype), False).reshape(arr.shape) + np.copyto(new_arr, arr) + arr = new_arr + return arr + + def _create_basis_state(self, index): + """Return a computational basis state over all wires. Args: - wires (int): the number of wires to initialize the device with - sync (bool): immediately sync with host-sv after applying operations - c_dtype: Datatypes for statevector representation. Must be one of - ``np.complex64`` or ``np.complex128``. - kokkos_args (InitializationSettings): binding for Kokkos::InitializationSettings - (threading parameters). - shots (int): How many times the circuit should be evaluated (or sampled) to estimate - the expectation values. Defaults to ``None`` if not specified. Setting - to ``None`` results in computing statistics like expectation values and - variances analytically. + index (int): integer representing the computational basis state + Returns: + array[complex]: complex array of shape ``[2]*self.num_wires`` + representing the statevector of the basis state + Note: This function does not support broadcasted inputs yet. """ + self._kokkos_state.setBasisState(index) - name = "Lightning Kokkos PennyLane plugin" - short_name = "lightning.kokkos" - kokkos_config = {} - operations = allowed_operations - observables = allowed_observables - _backend_info = backend_info - config = Path(__file__).parent / "lightning_kokkos.toml" - - def __init__( - self, - wires, - *, - sync=True, - c_dtype=np.complex128, - shots=None, - batch_obs=False, - kokkos_args=None, - ): # pylint: disable=unused-argument, too-many-arguments - super().__init__(wires, shots=shots, c_dtype=c_dtype) - - if kokkos_args is None: - self._kokkos_state = _kokkos_dtype(c_dtype)(self.num_wires) - elif isinstance(kokkos_args, InitializationSettings): - self._kokkos_state = _kokkos_dtype(c_dtype)(self.num_wires, kokkos_args) - else: - type0 = type(InitializationSettings()) - raise TypeError( - f"Argument kokkos_args must be of type {type0} but it is of {type(kokkos_args)}." - ) - self._sync = sync + def reset(self): + """Reset the device""" + super().reset() - if not LightningKokkos.kokkos_config: - LightningKokkos.kokkos_config = _kokkos_configuration() + # init the state vector to |00..0> + self._kokkos_state.resetStateVector() # Sync reset - @property - def stopping_condition(self): - """.BooleanFn: Returns the stopping condition for the device. The returned - function accepts a queueable object (including a PennyLane operation - and observable) and returns ``True`` if supported by the device.""" - fun = super().stopping_condition + def sync_h2d(self, state_vector): + """Copy the state vector data on host provided by the user to the state + vector on the device - def accepts_obj(obj): - return fun(obj) or isinstance( - obj, (qml.measurements.MidMeasureMP, qml.ops.Conditional) - ) + Args: + state_vector(array[complex]): the state vector array on host. - return qml.BooleanFn(accepts_obj) - # pylint: disable=missing-function-docstring - @classmethod - def capabilities(cls): - capabilities = super().capabilities().copy() - capabilities.update( - supports_mid_measure=True, - ) - return capabilities + **Example** - @staticmethod - def _asarray(arr, dtype=None): - arr = np.asarray(arr) # arr is not copied + >>> dev = qml.device('lightning.kokkos', wires=3) + >>> obs = qml.Identity(0) @ qml.PauliX(1) @ qml.PauliY(2) + >>> obs1 = qml.Identity(1) + >>> H = qml.Hamiltonian([1.0, 1.0], [obs1, obs]) + >>> state_vector = np.array([0.0 + 0.0j, 0.0 + 0.1j, 0.1 + 0.1j, 0.1 + 0.2j, 0.2 + 0.2j, 0.3 + 0.3j, 0.3 + 0.4j, 0.4 + 0.5j,], dtype=np.complex64) + >>> dev.sync_h2d(state_vector) + >>> res = dev.expval(H) + >>> print(res) + 1.0 + """ + self._kokkos_state.HostToDevice(state_vector.ravel(order="C")) - if arr.dtype.kind not in ["f", "c"]: - return arr + def sync_d2h(self, state_vector): + """Copy the state vector data on device to a state vector on the host provided + by the user - if not dtype: - dtype = arr.dtype + Args: + state_vector(array[complex]): the state vector array on host - # We allocate a new aligned memory and copy data to there if alignment - # or dtype mismatches - # Note that get_alignment does not necessarily return CPUMemoryModel(Unaligned) even for - # numpy allocated memory as the memory location happens to be aligned. - if arr.dtype != dtype: - new_arr = allocate_aligned_array(arr.size, np.dtype(dtype), False).reshape( - arr.shape - ) - np.copyto(new_arr, arr) - arr = new_arr - return arr - def _create_basis_state(self, index): - """Return a computational basis state over all wires. - Args: - index (int): integer representing the computational basis state - Returns: - array[complex]: complex array of shape ``[2]*self.num_wires`` - representing the statevector of the basis state - Note: This function does not support broadcasted inputs yet. - """ - self._kokkos_state.setBasisState(index) - - def reset(self): - """Reset the device""" - super().reset() - - # init the state vector to |00..0> - self._kokkos_state.resetStateVector() # Sync reset - - def sync_h2d(self, state_vector): - """Copy the state vector data on host provided by the user to the state - vector on the device - - Args: - state_vector(array[complex]): the state vector array on host. - - - **Example** - - >>> dev = qml.device('lightning.kokkos', wires=3) - >>> obs = qml.Identity(0) @ qml.PauliX(1) @ qml.PauliY(2) - >>> obs1 = qml.Identity(1) - >>> H = qml.Hamiltonian([1.0, 1.0], [obs1, obs]) - >>> state_vector = np.array([0.0 + 0.0j, 0.0 + 0.1j, 0.1 + 0.1j, 0.1 + 0.2j, 0.2 + 0.2j, 0.3 + 0.3j, 0.3 + 0.4j, 0.4 + 0.5j,], dtype=np.complex64) - >>> dev.sync_h2d(state_vector) - >>> res = dev.expval(H) - >>> print(res) - 1.0 - """ - self._kokkos_state.HostToDevice(state_vector.ravel(order="C")) - - def sync_d2h(self, state_vector): - """Copy the state vector data on device to a state vector on the host provided - by the user - - Args: - state_vector(array[complex]): the state vector array on host - - - **Example** - - >>> dev = qml.device('lightning.kokkos', wires=1) - >>> dev.apply([qml.PauliX(wires=[0])]) - >>> state_vector = np.zeros(2**dev.num_wires).astype(dev.C_DTYPE) - >>> dev.sync_d2h(state_vector) - >>> print(state_vector) - [0.+0.j 1.+0.j] - """ - self._kokkos_state.DeviceToHost(state_vector.ravel(order="C")) - - @property - def create_ops_list(self): - """Returns create_ops_list function of the matching precision.""" - return create_ops_listC64 if self.use_csingle else create_ops_listC128 - - @property - def measurements(self): - """Returns Measurements constructor of the matching precision.""" - state_vector = self.state_vector - return ( - MeasurementsC64(state_vector) - if self.use_csingle - else MeasurementsC128(state_vector) - ) + **Example** - @property - def state(self): - """Copy the state vector data from the device to the host. - - A state vector Numpy array is explicitly allocated on the host to store and return - the data. - - **Example** - - >>> dev = qml.device('lightning.kokkos', wires=1) - >>> dev.apply([qml.PauliX(wires=[0])]) - >>> print(dev.state) - [0.+0.j 1.+0.j] - """ - state = np.zeros(2**self.num_wires, dtype=self.C_DTYPE) - state = self._asarray(state, dtype=self.C_DTYPE) - self.sync_d2h(state) - return state - - @property - def state_vector(self): - """Returns a handle to the statevector.""" - return self._kokkos_state - - def _apply_state_vector(self, state, device_wires): - """Initialize the internal state vector in a specified state. - - Args: - state (array[complex]): normalized input state of length ``2**len(wires)`` - or broadcasted state of shape ``(batch_size, 2**len(wires))`` - device_wires (Wires): wires that get initialized in the state - """ - - if isinstance(state, self._kokkos_state.__class__): - state_data = allocate_aligned_array(state.size, np.dtype(self.C_DTYPE), True) - state.DeviceToHost(state_data) - state = state_data - - ravelled_indices, state = self._preprocess_state_vector(state, device_wires) - - # translate to wire labels used by device - device_wires = self.map_wires(device_wires) - output_shape = [2] * self.num_wires - - if len(device_wires) == self.num_wires and Wires(sorted(device_wires)) == device_wires: - # Initialize the entire device state with the input state - self.sync_h2d(self._reshape(state, output_shape)) - return - - self._kokkos_state.setStateVector(ravelled_indices, state) # this operation on device - - def _apply_basis_state(self, state, wires): - """Initialize the state vector in a specified computational basis state. - - Args: - state (array[int]): computational basis state of shape ``(wires,)`` - consisting of 0s and 1s. - wires (Wires): wires that the provided computational state should be initialized on - - Note: This function does not support broadcasted inputs yet. - """ - num = self._get_basis_state_index(state, wires) - self._create_basis_state(num) - - def _apply_lightning_midmeasure(self, operation: MidMeasureMP, mid_measurements: dict): - """Execute a MidMeasureMP operation and return the sample in mid_measurements. - Args: - operation (~pennylane.operation.Operation): mid-circuit measurement - Returns: - None - """ - wires = self.wires.indices(operation.wires) - wire = list(wires)[0] - sample = qml.math.reshape(self.generate_samples(shots=1), (-1,))[wire] - if operation.postselect is not None and sample != operation.postselect: - mid_measurements[operation] = -1 - return - mid_measurements[operation] = sample - getattr(self.state_vector, "collapse")(wire, bool(sample)) - if operation.reset and bool(sample): - self.apply([qml.PauliX(operation.wires)], mid_measurements=mid_measurements) - - def apply_lightning(self, operations, mid_measurements=None): - """Apply a list of operations to the state tensor. - - Args: - operations (list[~pennylane.operation.Operation]): operations to apply - dtype (type): Type of numpy ``complex`` to be used. Can be important - to specify for large systems for memory allocation purposes. - - Returns: - array[complex]: the output state tensor - """ - # Skip over identity operations instead of performing - # matrix multiplication with the identity. - state = self.state_vector - - for ops in operations: - if isinstance(ops, qml.Identity): - continue - if isinstance(ops, Adjoint): - name = ops.base.name - invert_param = True - else: - name = ops.name - invert_param = False - method = getattr(state, name, None) - wires = self.wires.indices(ops.wires) - - if isinstance(ops, Conditional): - if ops.meas_val.concretize(mid_measurements): - self.apply_lightning([ops.then_op]) - elif isinstance(ops, MidMeasureMP): - self._apply_lightning_midmeasure(ops, mid_measurements) - elif isinstance(ops, qml.ops.op_math.Controlled) and isinstance( - ops.base, qml.GlobalPhase - ): - controls = ops.control_wires - control_values = ops.control_values - param = ops.base.parameters[0] - matrix = global_phase_diagonal(param, self.wires, controls, control_values) - state.apply(name, wires, False, [[param]], matrix) - elif method is None: - # Inverse can be set to False since qml.matrix(ops) is already in inverted form - try: - mat = qml.matrix(ops) - except AttributeError: # pragma: no cover - # To support older versions of PL - mat = ops.matrix - - if len(mat) == 0: - raise ValueError("Unsupported operation") - state.apply( - name, - wires, - False, - [], - mat.ravel(order="C"), # inv = False: Matrix already in correct form; - ) # Parameters can be ignored for explicit matrices; F-order for cuQuantum - else: - param = ops.parameters - method(wires, invert_param, param) - - # pylint: disable=unused-argument - def apply(self, operations, rotations=None, mid_measurements=None, **kwargs): - """Applies a list of operations to the state tensor.""" - # State preparation is currently done in Python - if operations: # make sure operations[0] exists - if isinstance(operations[0], StatePrep): - self._apply_state_vector( - operations[0].parameters[0].copy(), operations[0].wires - ) - operations = operations[1:] - elif isinstance(operations[0], BasisState): - self._apply_basis_state(operations[0].parameters[0], operations[0].wires) - operations = operations[1:] - - for operation in operations: - if isinstance(operation, (StatePrep, BasisState)): - raise DeviceError( - f"Operation {operation.name} cannot be used after other " - + f"Operations have already been applied on a {self.short_name} device." - ) - - self.apply_lightning(operations, mid_measurements=mid_measurements) - if mid_measurements is not None and any(v == -1 for v in mid_measurements.values()): - self._apply_basis_state(np.zeros(self.num_wires), wires=self.wires) - - # pylint: disable=protected-access - def expval(self, observable, shot_range=None, bin_size=None): - """Expectation value of the supplied observable. - - Args: - observable: A PennyLane observable. - shot_range (tuple[int]): 2-tuple of integers specifying the range of samples - to use. If not specified, all samples are used. - bin_size (int): Divides the shot range into bins of size ``bin_size``, and - returns the measurement statistic separately over each bin. If not - provided, the entire shot range is treated as a single bin. - - Returns: - Expectation value of the observable - """ - if isinstance(observable, qml.Projector): - diagonalizing_gates = observable.diagonalizing_gates() - if self.shots is None and diagonalizing_gates: - self.apply(diagonalizing_gates) - results = super().expval(observable, shot_range=shot_range, bin_size=bin_size) - if self.shots is None and diagonalizing_gates: - self.apply([qml.adjoint(g, lazy=False) for g in reversed(diagonalizing_gates)]) - return results - - if self.shots is not None: - # estimate the expectation value - # LightningQubit doesn't support sampling yet - samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) - return np.squeeze(np.mean(samples, axis=0)) - - # Initialization of state - measure = ( - MeasurementsC64(self.state_vector) - if self.use_csingle - else MeasurementsC128(self.state_vector) - ) - if isinstance(observable, qml.SparseHamiltonian): - csr_hamiltonian = observable.sparse_matrix(wire_order=self.wires).tocsr(copy=False) - return measure.expval( - csr_hamiltonian.indptr, - csr_hamiltonian.indices, - csr_hamiltonian.data, - ) + >>> dev = qml.device('lightning.kokkos', wires=1) + >>> dev.apply([qml.PauliX(wires=[0])]) + >>> state_vector = np.zeros(2**dev.num_wires).astype(dev.C_DTYPE) + >>> dev.sync_d2h(state_vector) + >>> print(state_vector) + [0.+0.j 1.+0.j] + """ + self._kokkos_state.DeviceToHost(state_vector.ravel(order="C")) - # use specialized functors to compute expval(Hermitian) - if isinstance(observable, qml.Hermitian): - observable_wires = self.map_wires(observable.wires) - matrix = observable.matrix() - return measure.expval(matrix, observable_wires) + @property + def create_ops_list(self): + """Returns create_ops_list function of the matching precision.""" + return create_ops_listC64 if self.use_csingle else create_ops_listC128 - if ( - isinstance(observable, qml.ops.Hamiltonian) - or (observable.arithmetic_depth > 0) - or isinstance(observable.name, List) - ): - ob_serialized = QuantumScriptSerializer(self.short_name, self.use_csingle)._ob( - observable, self.wire_map - ) - return measure.expval(ob_serialized) + @property + def measurements(self): + """Returns Measurements constructor of the matching precision.""" + state_vector = self.state_vector + return MeasurementsC64(state_vector) if self.use_csingle else MeasurementsC128(state_vector) - # translate to wire labels used by device - observable_wires = self.map_wires(observable.wires) + @property + def state(self): + """Copy the state vector data from the device to the host. - return measure.expval(observable.name, observable_wires) - - def var(self, observable, shot_range=None, bin_size=None): - """Variance of the supplied observable. - - Args: - observable: A PennyLane observable. - shot_range (tuple[int]): 2-tuple of integers specifying the range of samples - to use. If not specified, all samples are used. - bin_size (int): Divides the shot range into bins of size ``bin_size``, and - returns the measurement statistic separately over each bin. If not - provided, the entire shot range is treated as a single bin. - - Returns: - Variance of the observable - """ - if isinstance(observable, qml.Projector): - diagonalizing_gates = observable.diagonalizing_gates() - if self.shots is None and diagonalizing_gates: - self.apply(diagonalizing_gates) - results = super().var(observable, shot_range=shot_range, bin_size=bin_size) - if self.shots is None and diagonalizing_gates: - self.apply([qml.adjoint(g, lazy=False) for g in reversed(diagonalizing_gates)]) - return results - - if self.shots is not None: - # estimate the var - # LightningKokkos doesn't support sampling yet - samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) - return np.squeeze(np.var(samples, axis=0)) - - # Initialization of state - measure = ( - MeasurementsC64(self.state_vector) - if self.use_csingle - else MeasurementsC128(self.state_vector) - ) + A state vector Numpy array is explicitly allocated on the host to store and return + the data. - if isinstance(observable, qml.SparseHamiltonian): - csr_hamiltonian = observable.sparse_matrix(wire_order=self.wires).tocsr(copy=False) - return measure.var( - csr_hamiltonian.indptr, - csr_hamiltonian.indices, - csr_hamiltonian.data, - ) + **Example** + + >>> dev = qml.device('lightning.kokkos', wires=1) + >>> dev.apply([qml.PauliX(wires=[0])]) + >>> print(dev.state) + [0.+0.j 1.+0.j] + """ + state = np.zeros(2**self.num_wires, dtype=self.C_DTYPE) + state = self._asarray(state, dtype=self.C_DTYPE) + self.sync_d2h(state) + return state + + @property + def state_vector(self): + """Returns a handle to the statevector.""" + return self._kokkos_state + + def _apply_state_vector(self, state, device_wires): + """Initialize the internal state vector in a specified state. + + Args: + state (array[complex]): normalized input state of length ``2**len(wires)`` + or broadcasted state of shape ``(batch_size, 2**len(wires))`` + device_wires (Wires): wires that get initialized in the state + """ + + if isinstance(state, self._kokkos_state.__class__): + state_data = allocate_aligned_array(state.size, np.dtype(self.C_DTYPE), True) + state.DeviceToHost(state_data) + state = state_data - if ( - isinstance(observable, (qml.ops.Hamiltonian, qml.Hermitian)) - or (observable.arithmetic_depth > 0) - or isinstance(observable.name, List) + ravelled_indices, state = self._preprocess_state_vector(state, device_wires) + + # translate to wire labels used by device + device_wires = self.map_wires(device_wires) + output_shape = [2] * self.num_wires + + if len(device_wires) == self.num_wires and Wires(sorted(device_wires)) == device_wires: + # Initialize the entire device state with the input state + self.sync_h2d(self._reshape(state, output_shape)) + return + + self._kokkos_state.setStateVector(ravelled_indices, state) # this operation on device + + def _apply_basis_state(self, state, wires): + """Initialize the state vector in a specified computational basis state. + + Args: + state (array[int]): computational basis state of shape ``(wires,)`` + consisting of 0s and 1s. + wires (Wires): wires that the provided computational state should be initialized on + + Note: This function does not support broadcasted inputs yet. + """ + num = self._get_basis_state_index(state, wires) + self._create_basis_state(num) + + def _apply_lightning_midmeasure(self, operation: MidMeasureMP, mid_measurements: dict): + """Execute a MidMeasureMP operation and return the sample in mid_measurements. + Args: + operation (~pennylane.operation.Operation): mid-circuit measurement + Returns: + None + """ + wires = self.wires.indices(operation.wires) + wire = list(wires)[0] + sample = qml.math.reshape(self.generate_samples(shots=1), (-1,))[wire] + if operation.postselect is not None and sample != operation.postselect: + mid_measurements[operation] = -1 + return + mid_measurements[operation] = sample + getattr(self.state_vector, "collapse")(wire, bool(sample)) + if operation.reset and bool(sample): + self.apply([qml.PauliX(operation.wires)], mid_measurements=mid_measurements) + + def apply_lightning(self, operations, mid_measurements=None): + """Apply a list of operations to the state tensor. + + Args: + operations (list[~pennylane.operation.Operation]): operations to apply + dtype (type): Type of numpy ``complex`` to be used. Can be important + to specify for large systems for memory allocation purposes. + + Returns: + array[complex]: the output state tensor + """ + # Skip over identity operations instead of performing + # matrix multiplication with the identity. + state = self.state_vector + + for ops in operations: + if isinstance(ops, Adjoint): + name = ops.base.name + invert_param = True + else: + name = ops.name + invert_param = False + if isinstance(ops, qml.Identity): + continue + method = getattr(state, name, None) + wires = self.wires.indices(ops.wires) + + if isinstance(ops, Conditional): + if ops.meas_val.concretize(mid_measurements): + self.apply_lightning([ops.then_op]) + elif isinstance(ops, MidMeasureMP): + self._apply_lightning_midmeasure(ops, mid_measurements) + elif isinstance(ops, qml.ops.op_math.Controlled) and isinstance( + ops.base, qml.GlobalPhase ): - ob_serialized = QuantumScriptSerializer(self.short_name, self.use_csingle)._ob( - observable, self.wire_map + controls = ops.control_wires + control_values = ops.control_values + param = ops.base.parameters[0] + matrix = global_phase_diagonal(param, self.wires, controls, control_values) + state.apply(name, wires, False, [[param]], matrix) + elif method is None: + # Inverse can be set to False since qml.matrix(ops) is already in inverted form + try: + mat = qml.matrix(ops) + except AttributeError: # pragma: no cover + # To support older versions of PL + mat = ops.matrix + + if len(mat) == 0: + raise ValueError("Unsupported operation") + state.apply( + name, + wires, + False, + [], + mat.ravel(order="C"), # inv = False: Matrix already in correct form; + ) # Parameters can be ignored for explicit matrices; F-order for cuQuantum + else: + param = ops.parameters + method(wires, invert_param, param) + + # pylint: disable=unused-argument + def apply(self, operations, rotations=None, mid_measurements=None, **kwargs): + """Applies a list of operations to the state tensor.""" + # State preparation is currently done in Python + if operations: # make sure operations[0] exists + if isinstance(operations[0], StatePrep): + self._apply_state_vector(operations[0].parameters[0].copy(), operations[0].wires) + operations = operations[1:] + elif isinstance(operations[0], BasisState): + self._apply_basis_state(operations[0].parameters[0], operations[0].wires) + operations = operations[1:] + + for operation in operations: + if isinstance(operation, (StatePrep, BasisState)): + raise DeviceError( + f"Operation {operation.name} cannot be used after other " + + f"Operations have already been applied on a {self.short_name} device." ) - return measure.var(ob_serialized) - # translate to wire labels used by device - observable_wires = self.map_wires(observable.wires) + self.apply_lightning(operations, mid_measurements=mid_measurements) + if mid_measurements is not None and any(v == -1 for v in mid_measurements.values()): + self._apply_basis_state(np.zeros(self.num_wires), wires=self.wires) + + # pylint: disable=protected-access + def expval(self, observable, shot_range=None, bin_size=None): + """Expectation value of the supplied observable. - return measure.var(observable.name, observable_wires) + Args: + observable: A PennyLane observable. + shot_range (tuple[int]): 2-tuple of integers specifying the range of samples + to use. If not specified, all samples are used. + bin_size (int): Divides the shot range into bins of size ``bin_size``, and + returns the measurement statistic separately over each bin. If not + provided, the entire shot range is treated as a single bin. + + Returns: + Expectation value of the observable + """ + if isinstance(observable, qml.Projector): + diagonalizing_gates = observable.diagonalizing_gates() + if self.shots is None and diagonalizing_gates: + self.apply(diagonalizing_gates) + results = super().expval(observable, shot_range=shot_range, bin_size=bin_size) + if self.shots is None and diagonalizing_gates: + self.apply([qml.adjoint(g, lazy=False) for g in reversed(diagonalizing_gates)]) + return results - def generate_samples(self, shots=None): - """Generate samples + if self.shots is not None: + # estimate the expectation value + # LightningQubit doesn't support sampling yet + samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) + return np.squeeze(np.mean(samples, axis=0)) + + # Initialization of state + measure = ( + MeasurementsC64(self.state_vector) + if self.use_csingle + else MeasurementsC128(self.state_vector) + ) + if isinstance(observable, qml.SparseHamiltonian): + csr_hamiltonian = observable.sparse_matrix(wire_order=self.wires).tocsr(copy=False) + return measure.expval( + csr_hamiltonian.indptr, + csr_hamiltonian.indices, + csr_hamiltonian.data, + ) - Returns: - array[int]: array of samples in binary representation with shape - ``(dev.shots, dev.num_wires)`` - """ - shots = self.shots if shots is None else shots - measure = ( - MeasurementsC64(self._kokkos_state) - if self.use_csingle - else MeasurementsC128(self._kokkos_state) + # use specialized functors to compute expval(Hermitian) + if isinstance(observable, qml.Hermitian): + observable_wires = self.map_wires(observable.wires) + matrix = observable.matrix() + return measure.expval(matrix, observable_wires) + + if ( + isinstance(observable, qml.ops.Hamiltonian) + or (observable.arithmetic_depth > 0) + or isinstance(observable.name, List) + ): + ob_serialized = QuantumScriptSerializer(self.short_name, self.use_csingle)._ob( + observable, self.wire_map ) - return measure.generate_samples(len(self.wires), shots).astype(int, copy=False) + return measure.expval(ob_serialized) - def probability_lightning(self, wires): - """Return the probability of each computational basis state. + # translate to wire labels used by device + observable_wires = self.map_wires(observable.wires) - Args: - wires (Iterable[Number, str], Number, str, Wires): wires to return - marginal probabilities for. Wires not provided are traced out of the system. + return measure.expval(observable.name, observable_wires) - Returns: - array[float]: list of the probabilities - """ - return self.measurements.probs(wires) + def var(self, observable, shot_range=None, bin_size=None): + """Variance of the supplied observable. - # pylint: disable=attribute-defined-outside-init - def sample(self, observable, shot_range=None, bin_size=None, counts=False): - """Return samples of an observable.""" + Args: + observable: A PennyLane observable. + shot_range (tuple[int]): 2-tuple of integers specifying the range of samples + to use. If not specified, all samples are used. + bin_size (int): Divides the shot range into bins of size ``bin_size``, and + returns the measurement statistic separately over each bin. If not + provided, the entire shot range is treated as a single bin. + + Returns: + Variance of the observable + """ + if isinstance(observable, qml.Projector): diagonalizing_gates = observable.diagonalizing_gates() - if diagonalizing_gates: + if self.shots is None and diagonalizing_gates: self.apply(diagonalizing_gates) - if not isinstance(observable, qml.PauliZ): - self._samples = self.generate_samples() - results = super().sample( - observable, shot_range=shot_range, bin_size=bin_size, counts=counts - ) - if diagonalizing_gates: + results = super().var(observable, shot_range=shot_range, bin_size=bin_size) + if self.shots is None and diagonalizing_gates: self.apply([qml.adjoint(g, lazy=False) for g in reversed(diagonalizing_gates)]) return results - @staticmethod - def _check_adjdiff_supported_operations(operations): - """Check Lightning adjoint differentiation method support for a tape. - - Raise ``QuantumFunctionError`` if ``tape`` contains not supported measurements, - observables, or operations by the Lightning adjoint differentiation method. - - Args: - tape (.QuantumTape): quantum tape to differentiate. - """ - for operation in operations: - if operation.num_params > 1 and not isinstance(operation, Rot): - raise QuantumFunctionError( - f"The {operation.name} operation is not supported using " - 'the "adjoint" differentiation method' - ) - - def _init_process_jacobian_tape(self, tape, starting_state, use_device_state): - """Generate an initial state vector for ``_process_jacobian_tape``.""" - if starting_state is not None: - if starting_state.size != 2 ** len(self.wires): - raise QuantumFunctionError( - "The number of qubits of starting_state must be the same as " - "that of the device." - ) - self._apply_state_vector(starting_state, self.wires) - elif not use_device_state: - self.reset() - self.apply(tape.operations) - return self.state_vector - - def adjoint_jacobian(self, tape, starting_state=None, use_device_state=False): - """Implements the adjoint method outlined in - `Jones and Gacon `__ to differentiate an input tape. - - After a forward pass, the circuit is reversed by iteratively applying adjoint - gates to scan backwards through the circuit. - """ - if self.shots is not None: - warn( - "Requested adjoint differentiation to be computed with finite shots." - " The derivative is always exact when using the adjoint " - "differentiation method.", - UserWarning, - ) + if self.shots is not None: + # estimate the var + # LightningKokkos doesn't support sampling yet + samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) + return np.squeeze(np.var(samples, axis=0)) + + # Initialization of state + measure = ( + MeasurementsC64(self.state_vector) + if self.use_csingle + else MeasurementsC128(self.state_vector) + ) + + if isinstance(observable, qml.SparseHamiltonian): + csr_hamiltonian = observable.sparse_matrix(wire_order=self.wires).tocsr(copy=False) + return measure.var( + csr_hamiltonian.indptr, + csr_hamiltonian.indices, + csr_hamiltonian.data, + ) + + if ( + isinstance(observable, (qml.Hamiltonian, qml.Hermitian)) + or (observable.arithmetic_depth > 0) + or isinstance(observable.name, List) + ): + ob_serialized = QuantumScriptSerializer(self.short_name, self.use_csingle)._ob( + observable, self.wire_map + ) + return measure.var(ob_serialized) + + # translate to wire labels used by device + observable_wires = self.map_wires(observable.wires) - tape_return_type = self._check_adjdiff_supported_measurements(tape.measurements) + return measure.var(observable.name, observable_wires) - if not tape_return_type: # the tape does not have measurements - return np.array([], dtype=self.state.dtype) + def generate_samples(self, shots=None): + """Generate samples - if tape_return_type is State: # pragma: no cover + Returns: + array[int]: array of samples in binary representation with shape + ``(dev.shots, dev.num_wires)`` + """ + shots = self.shots if shots is None else shots + measure = ( + MeasurementsC64(self._kokkos_state) + if self.use_csingle + else MeasurementsC128(self._kokkos_state) + ) + return measure.generate_samples(len(self.wires), shots).astype(int, copy=False) + + def probability_lightning(self, wires): + """Return the probability of each computational basis state. + + Args: + wires (Iterable[Number, str], Number, str, Wires): wires to return + marginal probabilities for. Wires not provided are traced out of the system. + + Returns: + array[float]: list of the probabilities + """ + return self.measurements.probs(wires) + + # pylint: disable=attribute-defined-outside-init + def sample(self, observable, shot_range=None, bin_size=None, counts=False): + """Return samples of an observable.""" + diagonalizing_gates = observable.diagonalizing_gates() + if diagonalizing_gates: + self.apply(diagonalizing_gates) + if not isinstance(observable, qml.PauliZ): + self._samples = self.generate_samples() + results = super().sample( + observable, shot_range=shot_range, bin_size=bin_size, counts=counts + ) + if diagonalizing_gates: + self.apply([qml.adjoint(g, lazy=False) for g in reversed(diagonalizing_gates)]) + return results + + @staticmethod + def _check_adjdiff_supported_operations(operations): + """Check Lightning adjoint differentiation method support for a tape. + + Raise ``QuantumFunctionError`` if ``tape`` contains not supported measurements, + observables, or operations by the Lightning adjoint differentiation method. + + Args: + tape (.QuantumTape): quantum tape to differentiate. + """ + for operation in operations: + if operation.num_params > 1 and not isinstance(operation, Rot): raise QuantumFunctionError( - "This method does not support statevector return type. " - "Use vjp method instead for this purpose." + f"The {operation.name} operation is not supported using " + 'the "adjoint" differentiation method' ) - self._check_adjdiff_supported_operations(tape.operations) + def _init_process_jacobian_tape(self, tape, starting_state, use_device_state): + """Generate an initial state vector for ``_process_jacobian_tape``.""" + if starting_state is not None: + if starting_state.size != 2 ** len(self.wires): + raise QuantumFunctionError( + "The number of qubits of starting_state must be the same as " + "that of the device." + ) + self._apply_state_vector(starting_state, self.wires) + elif not use_device_state: + self.reset() + self.apply(tape.operations) + return self.state_vector + + def adjoint_jacobian(self, tape, starting_state=None, use_device_state=False): + """Implements the adjoint method outlined in + `Jones and Gacon `__ to differentiate an input tape. + + After a forward pass, the circuit is reversed by iteratively applying adjoint + gates to scan backwards through the circuit. + """ + if self.shots is not None: + warn( + "Requested adjoint differentiation to be computed with finite shots." + " The derivative is always exact when using the adjoint " + "differentiation method.", + UserWarning, + ) - processed_data = self._process_jacobian_tape(tape, starting_state, use_device_state) + tape_return_type = self._check_adjdiff_supported_measurements(tape.measurements) - if not processed_data: # training_params is empty - return np.array([], dtype=self.state.dtype) + if not tape_return_type: # the tape does not have measurements + return np.array([], dtype=self.state.dtype) - trainable_params = processed_data["tp_shift"] + if tape_return_type is State: # pragma: no cover + raise QuantumFunctionError( + "Adjoint differentiation method does not support measurement StateMP." + "Use vjp method instead for this purpose." + ) - # If requested batching over observables, chunk into OMP_NUM_THREADS sized chunks. - # This will allow use of Lightning with adjoint for large-qubit numbers AND large - # numbers of observables, enabling choice between compute time and memory use. - requested_threads = int(getenv("OMP_NUM_THREADS", "1")) + self._check_adjdiff_supported_operations(tape.operations) - adjoint_jacobian = AdjointJacobianC64() if self.use_csingle else AdjointJacobianC128() + processed_data = self._process_jacobian_tape(tape, starting_state, use_device_state) - if self._batch_obs and requested_threads > 1: # pragma: no cover - obs_partitions = _chunk_iterable( - processed_data["obs_serialized"], requested_threads - ) - jac = [] - for obs_chunk in obs_partitions: - jac_local = adjoint_jacobian( - processed_data["state_vector"], - obs_chunk, - processed_data["ops_serialized"], - trainable_params, - ) - jac.extend(jac_local) - else: - jac = adjoint_jacobian( + if not processed_data: # training_params is empty + return np.array([], dtype=self.state.dtype) + + trainable_params = processed_data["tp_shift"] + + # If requested batching over observables, chunk into OMP_NUM_THREADS sized chunks. + # This will allow use of Lightning with adjoint for large-qubit numbers AND large + # numbers of observables, enabling choice between compute time and memory use. + requested_threads = int(getenv("OMP_NUM_THREADS", "1")) + + adjoint_jacobian = AdjointJacobianC64() if self.use_csingle else AdjointJacobianC128() + + if self._batch_obs and requested_threads > 1: # pragma: no cover + obs_partitions = _chunk_iterable(processed_data["obs_serialized"], requested_threads) + jac = [] + for obs_chunk in obs_partitions: + jac_local = adjoint_jacobian( processed_data["state_vector"], - processed_data["obs_serialized"], + obs_chunk, processed_data["ops_serialized"], trainable_params, ) - jac = np.array(jac) - jac = jac.reshape(-1, len(trainable_params)) - jac_r = np.zeros((jac.shape[0], processed_data["all_params"])) - jac_r[:, processed_data["record_tp_rows"]] = jac - if hasattr(qml, "active_return"): # pragma: no cover - return self._adjoint_jacobian_processing(jac_r) if qml.active_return() else jac_r - return self._adjoint_jacobian_processing(jac_r) - - # pylint: disable=inconsistent-return-statements, line-too-long - def vjp(self, measurements, grad_vec, starting_state=None, use_device_state=False): - """Generate the processing function required to compute the vector-Jacobian products - of a tape. - - This function can be used with multiple expectation values or a quantum state. - When a quantum state is given, - - .. code-block:: python - - vjp_f = dev.vjp([qml.state()], grad_vec) - vjp = vjp_f(tape) - - computes :math:`w = (w_1,\\cdots,w_m)` where - - .. math:: - - w_k = \\langle v| \\frac{\\partial}{\\partial \\theta_k} | \\psi_{\\pmb{\\theta}} \\rangle. - - Here, :math:`m` is the total number of trainable parameters, - :math:`\\pmb{\\theta}` is the vector of trainable parameters and - :math:`\\psi_{\\pmb{\\theta}}` is the output quantum state. - - Args: - measurements (list): List of measurement processes for vector-Jacobian product. - Now it must be expectation values or a quantum state. - grad_vec (tensor_like): Gradient-output vector. Must have shape matching the output - shape of the corresponding tape, i.e. number of measurements if the return - type is expectation or :math:`2^N` if the return type is statevector - starting_state (tensor_like): post-forward pass state to start execution with. - It should be complex-valued. Takes precedence over ``use_device_state``. - use_device_state (bool): use current device state to initialize. - A forward pass of the same circuit should be the last thing the device - has executed. If a ``starting_state`` is provided, that takes precedence. - - Returns: - The processing function required to compute the vector-Jacobian products of a tape. - """ - if self.shots is not None: - warn( - "Requested adjoint differentiation to be computed with finite shots." - " The derivative is always exact when using the adjoint " - "differentiation method.", - UserWarning, - ) + jac.extend(jac_local) + else: + jac = adjoint_jacobian( + processed_data["state_vector"], + processed_data["obs_serialized"], + processed_data["ops_serialized"], + trainable_params, + ) + jac = np.array(jac) + jac = jac.reshape(-1, len(trainable_params)) + jac_r = np.zeros((jac.shape[0], processed_data["all_params"])) + jac_r[:, processed_data["record_tp_rows"]] = jac + if hasattr(qml, "active_return"): # pragma: no cover + return self._adjoint_jacobian_processing(jac_r) if qml.active_return() else jac_r + return self._adjoint_jacobian_processing(jac_r) - tape_return_type = self._check_adjdiff_supported_measurements(measurements) + # pylint: disable=inconsistent-return-statements, line-too-long + def vjp(self, measurements, grad_vec, starting_state=None, use_device_state=False): + """Generate the processing function required to compute the vector-Jacobian products + of a tape. - if math.allclose(grad_vec, 0) or tape_return_type is None: - return lambda tape: math.convert_like( - np.zeros(len(tape.trainable_params)), grad_vec - ) + This function can be used with multiple expectation values or a quantum state. + When a quantum state is given, + + .. code-block:: python - if tape_return_type is Expectation: - if len(grad_vec) != len(measurements): - raise ValueError( - "Number of observables in the tape must be the same as the " - "length of grad_vec in the vjp method" - ) + vjp_f = dev.vjp([qml.state()], grad_vec) + vjp = vjp_f(tape) - if np.iscomplexobj(grad_vec): - raise ValueError( - "The vjp method only works with a real-valued grad_vec when " - "the tape is returning an expectation value" - ) + computes :math:`w = (w_1,\\cdots,w_m)` where - ham = qml.Hamiltonian(grad_vec, [m.obs for m in measurements]) + .. math:: - # pylint: disable=protected-access - def processing_fn(tape): - nonlocal ham - num_params = len(tape.trainable_params) + w_k = \\langle v| \\frac{\\partial}{\\partial \\theta_k} | \\psi_{\\pmb{\\theta}} \\rangle. - if num_params == 0: - return np.array([], dtype=self.state.dtype) + Here, :math:`m` is the total number of trainable parameters, + :math:`\\pmb{\\theta}` is the vector of trainable parameters and + :math:`\\psi_{\\pmb{\\theta}}` is the output quantum state. - new_tape = tape.copy() - new_tape._measurements = [qml.expval(ham)] + Args: + measurements (list): List of measurement processes for vector-Jacobian product. + Now it must be expectation values or a quantum state. + grad_vec (tensor_like): Gradient-output vector. Must have shape matching the output + shape of the corresponding tape, i.e. number of measurements if the return + type is expectation or :math:`2^N` if the return type is statevector + starting_state (tensor_like): post-forward pass state to start execution with. + It should be complex-valued. Takes precedence over ``use_device_state``. + use_device_state (bool): use current device state to initialize. + A forward pass of the same circuit should be the last thing the device + has executed. If a ``starting_state`` is provided, that takes precedence. + + Returns: + The processing function required to compute the vector-Jacobian products of a tape. + """ + if self.shots is not None: + warn( + "Requested adjoint differentiation to be computed with finite shots." + " The derivative is always exact when using the adjoint " + "differentiation method.", + UserWarning, + ) - return self.adjoint_jacobian(new_tape, starting_state, use_device_state) + tape_return_type = self._check_adjdiff_supported_measurements(measurements) - return processing_fn + if math.allclose(grad_vec, 0) or tape_return_type is None: + return lambda tape: math.convert_like(np.zeros(len(tape.trainable_params)), grad_vec) -else: + if tape_return_type is Expectation: + if len(grad_vec) != len(measurements): + raise ValueError( + "Number of observables in the tape must be the same as the " + "length of grad_vec in the vjp method" + ) - class LightningKokkos(LightningBaseFallBack): # pragma: no cover - # pylint: disable=missing-class-docstring, too-few-public-methods - name = "Lightning Kokkos PennyLane plugin [No binaries found - Fallback: default.qubit]" - short_name = "lightning.kokkos" + if np.iscomplexobj(grad_vec): + raise ValueError( + "The vjp method only works with a real-valued grad_vec when " + "the tape is returning an expectation value" + ) - def __init__(self, wires, *, c_dtype=np.complex128, **kwargs): - warn( - "Pre-compiled binaries for lightning.kokkos are not available. Falling back to " - "using the Python-based default.qubit implementation. To manually compile from " - "source, follow the instructions at " - "https://pennylane-lightning.readthedocs.io/en/latest/installation.html.", - UserWarning, - ) - super().__init__(wires, c_dtype=c_dtype, **kwargs) + ham = qml.Hamiltonian(grad_vec, [m.obs for m in measurements]) + + # pylint: disable=protected-access + def processing_fn(tape): + nonlocal ham + num_params = len(tape.trainable_params) + + if num_params == 0: + return np.array([], dtype=self.state.dtype) + + new_tape = tape.copy() + new_tape._measurements = [qml.expval(ham)] + + return self.adjoint_jacobian(new_tape, starting_state, use_device_state) + + return processing_fn diff --git a/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py b/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py index 76db319a54..5aecfcbb6c 100644 --- a/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py +++ b/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py @@ -204,7 +204,9 @@ def calculate_jacobian(self, tape: QuantumTape): return np.array([], dtype=self._dtype) if tape_return_type is State: - raise QuantumFunctionError("This method does not support statevector return type. ") + raise QuantumFunctionError( + "Adjoint differentiation method does not support measurement StateMP." + ) if any(m.return_type is not Expectation for m in tape.measurements): raise QuantumFunctionError( diff --git a/tests/conftest.py b/tests/conftest.py index 436dbe84ce..db058c5a07 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -123,21 +123,26 @@ def get_device(): # Device specification import pennylane_lightning.lightning_qubit as lightning_ops # Any definition of lightning_ops will do +LightningException = None + if device_name == "lightning.kokkos": from pennylane_lightning.lightning_kokkos import LightningKokkos as LightningDevice if hasattr(pennylane_lightning, "lightning_kokkos_ops"): import pennylane_lightning.lightning_kokkos_ops as lightning_ops + from pennylane_lightning.lightning_kokkos_ops import LightningException elif device_name == "lightning.gpu": from pennylane_lightning.lightning_gpu import LightningGPU as LightningDevice if hasattr(pennylane_lightning, "lightning_gpu_ops"): import pennylane_lightning.lightning_gpu_ops as lightning_ops + from pennylane_lightning.lightning_gpu_ops import LightningException else: from pennylane_lightning.lightning_qubit import LightningQubit as LightningDevice if hasattr(pennylane_lightning, "lightning_qubit_ops"): import pennylane_lightning.lightning_qubit_ops as lightning_ops + from pennylane_lightning.lightning_qubit_ops import LightningException # General qubit_device fixture, for any number of wires. diff --git a/tests/lightning_qubit/test_adjoint_jacobian_class.py b/tests/lightning_qubit/test_adjoint_jacobian_class.py index 7865a681af..4bce15422c 100644 --- a/tests/lightning_qubit/test_adjoint_jacobian_class.py +++ b/tests/lightning_qubit/test_adjoint_jacobian_class.py @@ -125,7 +125,7 @@ def test_not_supported_state(self, lightning_sv): with pytest.raises( qml.QuantumFunctionError, - match="This method does not support statevector return type", + match="Adjoint differentiation method does not support measurement StateMP.", ): self.calculate_jacobian(lightning_sv(num_wires=3), tape) diff --git a/tests/new_api/test_device.py b/tests/new_api/test_device.py index bf27cbcb47..d713745b68 100644 --- a/tests/new_api/test_device.py +++ b/tests/new_api/test_device.py @@ -714,12 +714,14 @@ def test_state_jacobian_not_supported(self, dev, batch_obs): config = ExecutionConfig(gradient_method="adjoint", device_options={"batch_obs": batch_obs}) with pytest.raises( - qml.QuantumFunctionError, match="This method does not support statevector return type" + qml.QuantumFunctionError, + match="Adjoint differentiation method does not support measurement StateMP.", ): _ = dev.compute_derivatives(qs, config) with pytest.raises( - qml.QuantumFunctionError, match="This method does not support statevector return type" + qml.QuantumFunctionError, + match="Adjoint differentiation method does not support measurement StateMP.", ): _ = dev.execute_and_compute_derivatives(qs, config) diff --git a/tests/new_api/test_no_binaries.py b/tests/new_api/test_no_binaries.py index fcf7a3a03b..5ecfb9b667 100644 --- a/tests/new_api/test_no_binaries.py +++ b/tests/new_api/test_no_binaries.py @@ -17,14 +17,10 @@ import pytest from conftest import LightningDevice -if not LightningDevice._new_API: - pytest.skip("Tests are for new API. Skipping", allow_module_level=True) - if LightningDevice._CPP_BINARY_AVAILABLE: pytest.skip("Binary module found. Skipping.", allow_module_level=True) -@pytest.mark.skipif(LightningDevice._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_no_binaries(): """Test no binaries were found for the device""" diff --git a/tests/test_adjoint_jacobian.py b/tests/test_adjoint_jacobian.py index 092752b780..27d5d2de6d 100644 --- a/tests/test_adjoint_jacobian.py +++ b/tests/test_adjoint_jacobian.py @@ -20,7 +20,7 @@ import pennylane as qml import pytest from conftest import LightningDevice as ld -from conftest import device_name +from conftest import LightningException, device_name from pennylane import QNode from pennylane import numpy as np from pennylane import qchem, qnode @@ -33,14 +33,11 @@ qml.Z.compute_matrix(), ) -if ld._new_API: - if not ld._CPP_BINARY_AVAILABLE: - pytest.skip("No binary module found. Skipping.", allow_module_level=True) - else: - from pennylane_lightning.lightning_qubit_ops import LightningException +if not ld._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) kokkos_args = [None] -if device_name == "lightning.kokkos" and ld._CPP_BINARY_AVAILABLE: +if device_name == "lightning.kokkos": from pennylane_lightning.lightning_kokkos_ops import InitializationSettings kokkos_args += [InitializationSettings().set_num_threads(2)] @@ -102,7 +99,7 @@ def get_derivatives_method(device): @pytest.fixture(params=fixture_params) def dev(self, request): params = request.param - if device_name == "lightning.kokkos" and ld._CPP_BINARY_AVAILABLE: + if device_name == "lightning.kokkos": return qml.device(device_name, wires=3, c_dtype=params[0], kokkos_args=params[1]) return qml.device(device_name, wires=3, c_dtype=params[0]) @@ -125,14 +122,12 @@ def test_not_expval(self, dev): qml.RX(0.1, wires=0) qml.state() - if device_name == "lightning.kokkos" and ld._CPP_BINARY_AVAILABLE: + if device_name == "lightning.kokkos": message = "Adjoint differentiation does not support State measurements." - elif device_name == "lightning.gpu" and ld._CPP_BINARY_AVAILABLE: + elif device_name == "lightning.gpu": message = "Adjoint differentiation does not support State measurements." - elif ld._CPP_BINARY_AVAILABLE: - message = "This method does not support statevector return type." else: - message = "Adjoint differentiation method does not support measurement StateMP" + message = "Adjoint differentiation method does not support measurement StateMP." with pytest.raises( qml.QuantumFunctionError, match=message, @@ -177,7 +172,6 @@ def test_empty_measurements(self, dev): jac = method(tape) assert len(jac) == 0 - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_unsupported_op(self, dev): """Test if a QuantumFunctionError is raised for an unsupported operation, i.e., multi-parameter operations that are not qml.Rot""" @@ -200,7 +194,6 @@ def test_unsupported_op(self, dev): @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.skipif(ld._new_API, reason="Old API required") - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_proj_unsupported(self, dev): """Test if a QuantumFunctionError is raised for a Projector observable""" with qml.tape.QuantumTape() as tape: @@ -273,7 +266,7 @@ def test_Rot_gradient(self, stateprep, theta, dev): assert np.allclose(calculated_val, numeric_val, atol=tol, rtol=0) @pytest.mark.skipif( - device_name != "lightning.qubit" or not ld._CPP_BINARY_AVAILABLE, + device_name != "lightning.qubit", reason="N-controlled operations only implemented in lightning.qubit.", ) @pytest.mark.parametrize("n_qubits", [1, 2, 3, 4]) @@ -403,7 +396,6 @@ def test_multiple_rx_gradient_expval_hermitian(self, tol, dev): ops = {qml.RX, qml.RY, qml.RZ, qml.PhaseShift, qml.CRX, qml.CRY, qml.CRZ, qml.Rot} @pytest.mark.usefixtures("use_legacy_and_new_opmath") - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_multiple_rx_gradient_expval_hamiltonian(self, tol, dev): """Tests that the gradient of multiple RX gates in a circuit yields the correct result with Hermitian observable @@ -582,7 +574,6 @@ def test_gradient_gate_with_multiple_parameters_hermitian(self, dev): assert np.allclose(grad_D, grad_F, atol=tol, rtol=0) @pytest.mark.usefixtures("use_legacy_and_new_opmath") - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_gradient_gate_with_multiple_parameters_hamiltonian(self, dev): """Tests that gates with multiple free parameters yield correct gradients.""" x, y, z = [0.5, 0.3, -0.7] @@ -660,7 +651,6 @@ def test_provide_starting_state(self, tol, dev): assert np.allclose(dM1, dM2, atol=tol, rtol=0) @pytest.mark.skipif(ld._new_API, reason="Old API required") - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_provide_wrong_starting_state(self, dev): """Tests raise an exception when provided starting state mismatches.""" x, y, z = [0.5, 0.3, -0.7] @@ -683,7 +673,6 @@ def test_provide_wrong_starting_state(self, dev): device_name == "lightning.kokkos" or device_name == "lightning.gpu", reason="Adjoint differentiation does not support State measurements.", ) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_state_return_type(self, dev): """Tests raise an exception when the return type is State""" with qml.tape.QuantumTape() as tape: @@ -694,7 +683,8 @@ def test_state_return_type(self, dev): method = self.get_derivatives_method(dev) with pytest.raises( - qml.QuantumFunctionError, match="This method does not support statevector return type." + qml.QuantumFunctionError, + match="Adjoint differentiation method does not support measurement StateMP.", ): method(tape) @@ -726,7 +716,6 @@ def circ(x): ): qml.grad(circ)(0.1) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_qnode(self, mocker, dev): """Test that specifying diff_method allows the adjoint method to be selected""" args = np.array([0.54, 0.1, 0.5], requires_grad=True) @@ -829,7 +818,7 @@ def circuit(p): assert np.allclose(jac_ad, jac_bp, atol=tol, rtol=0) @pytest.mark.skipif( - device_name != "lightning.qubit" or not ld._CPP_BINARY_AVAILABLE, + device_name != "lightning.qubit", reason="N-controlled operations only implemented in lightning.qubit.", ) @pytest.mark.parametrize( @@ -962,7 +951,6 @@ def cost(p1, p2): assert np.allclose(grad_D[0], expected, atol=tol, rtol=0) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_gradient_repeated_gate_parameters(self, mocker, dev): """Tests that repeated use of a free parameter in a multi-parameter gate yields correct gradients.""" @@ -1128,7 +1116,6 @@ def circuit_ansatz(params, wires): @pytest.mark.usefixtures("use_legacy_and_new_opmath") -@pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_tape_qchem(tol): """Tests the circuit Ansatz with a QChem Hamiltonian produces correct results""" @@ -1152,7 +1139,6 @@ def circuit(params): @pytest.mark.usefixtures("use_legacy_and_new_opmath") -@pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_tape_qchem_sparse(tol): """Tests the circuit Ansatz with a QChem Hamiltonian produces correct results""" @@ -1184,7 +1170,6 @@ def circuit(params): custom_wires = ["alice", 3.14, -1, 0] -@pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @pytest.mark.parametrize( "returns", [ @@ -1280,7 +1265,6 @@ def casted_to_array_lightning(params): assert np.allclose(j_def, j_lightning) -@pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_integration_chunk_observables(): """Integration tests that compare to default.qubit for a large circuit with multiple expectation values. Expvals are generated in parallelized chunks.""" dev_def = qml.device("default.qubit", wires=range(4)) @@ -1365,7 +1349,7 @@ def casted_to_array_lightning(params): @pytest.mark.skipif( - device_name != "lightning.gpu" or not ld._CPP_BINARY_AVAILABLE, + device_name != "lightning.gpu", reason="Tests only for lightning.gpu", ) @pytest.mark.parametrize( @@ -1418,7 +1402,7 @@ def convert_to_array_def(params): @pytest.mark.skipif( - device_name != "lightning.gpu" or not ld._CPP_BINARY_AVAILABLE, + device_name != "lightning.gpu", reason="Tests only for lightning.gpu", ) @pytest.mark.parametrize( @@ -1504,7 +1488,6 @@ def create_xyz_file(tmp_path_factory): yield file -@pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @pytest.mark.parametrize("batches", [False, True, 1, 2, 3, 4]) def test_integration_H2_Hamiltonian(create_xyz_file, batches): _ = pytest.importorskip("openfermionpyscf") diff --git a/tests/test_apply.py b/tests/test_apply.py index 39f9e886b5..de1f3114b7 100644 --- a/tests/test_apply.py +++ b/tests/test_apply.py @@ -26,7 +26,7 @@ from pennylane.operation import Operation from pennylane.wires import Wires -if ld._new_API and not ld._CPP_BINARY_AVAILABLE: +if not ld._CPP_BINARY_AVAILABLE: pytest.skip("No binary module found. Skipping.", allow_module_level=True) @@ -76,7 +76,6 @@ def test_apply_operation_single_wire_no_parameters( device_name == "lightning.kokkos" or device_name == "lightning.gpu", reason="Only meaningful for lightning_qubit", ) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @pytest.mark.parametrize("operation,input,expected_output", test_data_no_parameters) @pytest.mark.parametrize("C", [np.complex64, np.complex128]) def test_apply_operation_preserve_pointer_single_wire_no_parameters( @@ -128,7 +127,6 @@ def test_apply_operation_two_wires_no_parameters( assert np.allclose(dev.state, np.array(expected_output), atol=tol, rtol=0) assert dev.state.dtype == dev.C_DTYPE - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @pytest.mark.parametrize("operation,input,expected_output", test_data_two_wires_no_parameters) @pytest.mark.parametrize("C", [np.complex64, np.complex128]) def test_apply_operation_preserve_pointer_two_wires_no_parameters( @@ -167,7 +165,6 @@ def test_apply_operation_three_wires_no_parameters( assert np.allclose(dev.state, np.array(expected_output), atol=tol, rtol=0) assert dev.state.dtype == dev.C_DTYPE - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @pytest.mark.parametrize("operation,input,expected_output", test_data_three_wires_no_parameters) @pytest.mark.parametrize("C", [np.complex64, np.complex128]) def test_apply_operation_preserve_pointer_three_wires_no_parameters( @@ -312,7 +309,6 @@ def test_apply_operation_single_wire_with_parameters( assert np.allclose(dev.state, np.array(expected_output), atol=tol, rtol=0) assert dev.state.dtype == dev.C_DTYPE - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @pytest.mark.parametrize( "operation,input,expected_output,par", test_data_single_wire_with_parameters ) @@ -463,7 +459,6 @@ def test_apply_operation_two_wires_with_parameters( assert np.allclose(dev.state, np.array(expected_output), atol=tol, rtol=0) assert dev.state.dtype == dev.C_DTYPE - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @pytest.mark.parametrize( "operation,input,expected_output,par", test_data_two_wires_with_parameters ) @@ -512,10 +507,6 @@ def test_apply_errors_basis_state(self, qubit_device): dev.reset() dev.apply([qml.RZ(0.5, wires=[0]), qml.BasisState(np.array([1, 1]), wires=[0, 1])]) - @pytest.mark.skipif( - not ld._CPP_BINARY_AVAILABLE, - reason="Lightning binary required", - ) @pytest.mark.skipif( device_name != "lightning.qubit", reason="Only meaningful for LightningQubit.", @@ -744,7 +735,6 @@ def test_load_default_qubit_device(self): assert dev.short_name == device_name @pytest.mark.xfail(ld._new_API, reason="Old device API required.") - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_no_backprop(self): """Test that lightning device does not support the backprop differentiation method.""" @@ -759,7 +749,6 @@ def circuit(): qml.QNode(circuit, dev, diff_method="backprop") @pytest.mark.xfail(ld._new_API, reason="New device API currently has the wrong module path.") - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_best_gets_lightning(self): """Test that the best differentiation method returns lightning qubit.""" @@ -1290,10 +1279,7 @@ def test_apply_identity_skipped(self, mocker, tol): assert np.allclose(dev.state, starting_state, atol=tol, rtol=0) assert dev.state.dtype == dev.C_DTYPE - @pytest.mark.skipif( - not ld._CPP_BINARY_AVAILABLE or device_name != "lightning.gpu", - reason="Only meaningful when binary is available.", - ) + @pytest.mark.skipif(ld._new_API, reason="Old API required") def test_unsupported_operation(self, mocker, tol): """Test unsupported operations.""" @@ -1353,16 +1339,6 @@ def circuit(): assert np.allclose(results, expected) -@pytest.mark.skipif(ld._new_API, reason="Old API required.") -@pytest.mark.skipif( - ld._CPP_BINARY_AVAILABLE, reason="Test only applies when binaries are unavailable" -) -def test_warning(): - """Tests if a warning is raised when lightning device binaries are not available""" - with pytest.warns(UserWarning, match="Pre-compiled binaries for " + device_name): - qml.device(device_name, wires=1) - - @pytest.mark.parametrize( "op", [ diff --git a/tests/test_arrays.py b/tests/test_arrays.py index c90d78026e..1eef3f8364 100644 --- a/tests/test_arrays.py +++ b/tests/test_arrays.py @@ -17,23 +17,20 @@ import numpy as np import pytest from conftest import LightningDevice as ld +from conftest import device_name, lightning_ops -try: - from pennylane_lightning.lightning_qubit_ops import allocate_aligned_array -except (ImportError, ModuleNotFoundError): +if device_name == "lightning_gpu" or not ld._CPP_BINARY_AVAILABLE: pytest.skip("No binary module found. Skipping.", allow_module_level=True) -@pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @pytest.mark.parametrize("dt", [np.dtype(np.complex64), np.dtype(np.complex128)]) def test_allocate_aligned_array_unset(dt): - arr = allocate_aligned_array(1024, dt, False) + arr = lightning_ops.allocate_aligned_array(1024, dt, False) assert arr.dtype == dt -@pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @pytest.mark.parametrize("dt", [np.dtype(np.complex64), np.dtype(np.complex128)]) def test_allocate_aligned_array_set(dt): - arr = allocate_aligned_array(1024, dt, True) + arr = lightning_ops.allocate_aligned_array(1024, dt, True) assert arr.dtype == dt assert np.all(arr == 0) diff --git a/tests/test_comparison.py b/tests/test_comparison.py index e1f8112f87..631e5bfa1e 100644 --- a/tests/test_comparison.py +++ b/tests/test_comparison.py @@ -24,6 +24,9 @@ from conftest import LightningDevice as ld from conftest import device_name +if not ld._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) + def lightning_backend_dev(wires): """Loads the lightning backend""" @@ -65,7 +68,6 @@ class TestComparison: @pytest.mark.parametrize( "lightning_dev_version", [lightning_backend_dev, lightning_backend_batch_obs_dev] ) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @pytest.mark.parametrize("num_threads", [1, 2]) def test_one_qubit_circuit( self, monkeypatch, wires, lightning_dev_version, basis_state, num_threads @@ -102,7 +104,6 @@ def circuit(measurement): "lightning_dev_version", [lightning_backend_dev, lightning_backend_batch_obs_dev] ) @pytest.mark.parametrize("num_threads", [1, 2]) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_two_qubit_circuit( self, monkeypatch, wires, lightning_dev_version, basis_state, num_threads ): @@ -146,7 +147,6 @@ def circuit(measurement): "lightning_dev_version", [lightning_backend_dev, lightning_backend_batch_obs_dev] ) @pytest.mark.parametrize("num_threads", [1, 2]) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_three_qubit_circuit( self, monkeypatch, wires, lightning_dev_version, basis_state, num_threads ): @@ -198,7 +198,6 @@ def circuit(measurement): "lightning_dev_version", [lightning_backend_dev, lightning_backend_batch_obs_dev] ) @pytest.mark.parametrize("num_threads", [1, 2]) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_four_qubit_circuit( self, monkeypatch, wires, lightning_dev_version, basis_state, num_threads ): @@ -254,7 +253,6 @@ def circuit(measurement): ) @pytest.mark.parametrize("wires", range(1, 17)) @pytest.mark.parametrize("num_threads", [1, 2]) - @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @pytest.mark.parametrize("stateprep", [qml.QubitStateVector, qml.StatePrep]) def test_n_qubit_circuit( self, monkeypatch, stateprep, wires, lightning_dev_version, num_threads diff --git a/tests/test_device.py b/tests/test_device.py index 11945e89aa..72bd87de2a 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -42,7 +42,7 @@ def test_create_device_with_unsupported_dtype(): @pytest.mark.skipif( - device_name != "lightning.kokkos" or not ld._CPP_BINARY_AVAILABLE, + device_name != "lightning.kokkos", reason="Only lightning.kokkos has a kwarg kokkos_args.", ) def test_create_device_with_unsupported_kokkos_args(): @@ -51,7 +51,7 @@ def test_create_device_with_unsupported_kokkos_args(): @pytest.mark.skipif( - device_name != "lightning.gpu" or not ld._CPP_BINARY_AVAILABLE, + device_name != "lightning.gpu", reason="Only lightning.gpu has a kwarg mpi_buf_size.", ) def test_create_device_with_unsupported_mpi_buf_size(): diff --git a/tests/test_execute.py b/tests/test_execute.py index 49f3f21fd4..505c705543 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -21,7 +21,7 @@ from conftest import LightningDevice, device_name from pennylane import numpy as np -if LightningDevice._new_API and not LightningDevice._CPP_BINARY_AVAILABLE: +if not LightningDevice._CPP_BINARY_AVAILABLE: pytest.skip("No binary module found. Skipping.", allow_module_level=True) diff --git a/tests/test_expval.py b/tests/test_expval.py index 9725028014..3340ab78b4 100644 --- a/tests/test_expval.py +++ b/tests/test_expval.py @@ -22,7 +22,7 @@ from conftest import PHI, THETA, VARPHI from conftest import LightningDevice as ld -if ld._new_API and not ld._CPP_BINARY_AVAILABLE: +if not ld._CPP_BINARY_AVAILABLE: pytest.skip("No binary module found. Skipping.", allow_module_level=True) diff --git a/tests/test_gates.py b/tests/test_gates.py index 884ecc5bca..4958c7ccc1 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -24,7 +24,7 @@ from conftest import LightningDevice as ld from conftest import device_name -if ld._new_API and not ld._CPP_BINARY_AVAILABLE: +if not ld._CPP_BINARY_AVAILABLE: pytest.skip("No binary module found. Skipping.", allow_module_level=True) @@ -250,7 +250,6 @@ def output(input): @pytest.mark.skipif(ld._new_API, reason="Old API required") -@pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @pytest.mark.parametrize( "obs,has_rotation", [ diff --git a/tests/test_serialize_no_binaries.py b/tests/test_serialize_no_binaries.py index 28f72fcb14..fd548e3b1c 100644 --- a/tests/test_serialize_no_binaries.py +++ b/tests/test_serialize_no_binaries.py @@ -23,7 +23,6 @@ pytest.skip("Binary module found. Skipping.", allow_module_level=True) -@pytest.mark.skipif(LightningDevice._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_no_binaries(): """Test no binaries were found for the device""" diff --git a/tests/test_var.py b/tests/test_var.py index aaa86f1ce7..01500da73e 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -20,7 +20,7 @@ from conftest import PHI, THETA, VARPHI from conftest import LightningDevice as ld -if ld._new_API and not ld._CPP_BINARY_AVAILABLE: +if not ld._CPP_BINARY_AVAILABLE: pytest.skip("No binary module found. Skipping.", allow_module_level=True) np.random.seed(42) diff --git a/tests/test_vjp.py b/tests/test_vjp.py index 790c22e784..489134dc97 100644 --- a/tests/test_vjp.py +++ b/tests/test_vjp.py @@ -19,14 +19,10 @@ import pennylane as qml import pytest from conftest import LightningDevice as ld -from conftest import device_name +from conftest import LightningException, device_name from pennylane import numpy as np -if ld._CPP_BINARY_AVAILABLE: - if ld._new_API: - from pennylane_lightning.lightning_qubit_ops import LightningException - -else: +if not ld._CPP_BINARY_AVAILABLE: pytest.skip("No binary module found. Skipping.", allow_module_level=True)