From 182b9cd73b9e1a30ee889c4881b04664ed880b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Alfredo=20Nu=C3=B1ez=20Meneses?= Date: Wed, 11 Dec 2024 12:04:32 -0500 Subject: [PATCH] Add exactn cpp binding (#1014) **Context:** Adding the backend **Exact Tensor Network** from `lightning.tensor` to the Python layer **Description of the Change:** * Add pybind layer for the `ExaTNCuda` class * Update the python layer unit tests. * Python layer refactoring to allow runtime selection of MPS and ExaTN * Python layer unit tests update (gates, analytical measurement) **Benefits:** 1. Refactor MPSTNCuda class to TNCuda class * Both MPS and Exact TensorNetwork backends will be handled by the TNCuda class * User can select either MPS or Exact TensorNetwork at runtime by passing str (`mps` or `exatn`) to the constructor of theTNCuda class. 2. Measurement class * `expval()` support can be get without changing current code base for the MPS backend. **Possible Drawbacks:** * `qml.StatePrep()` won't be supported for 'exatn' **Related GitHub Issues:** [sc-77837][sc-77840] --------- Co-authored-by: Shuli Shu <08cnbj@gmail.com> Co-authored-by: ringo-but-quantum Co-authored-by: Shuli Shu <31480676+multiphaseCFD@users.noreply.github.com> Co-authored-by: Ali Asadi <10773383+maliasadi@users.noreply.github.com> --- .github/CHANGELOG.md | 3 + .github/workflows/tests_gpu_python.yml | 2 +- pennylane_lightning/core/_serialize.py | 120 ++- pennylane_lightning/core/_version.py | 2 +- .../core/src/bindings/Bindings.cpp | 2 +- .../core/src/bindings/Bindings.hpp | 141 ++- .../lightning_tensor/tncuda/ExactTNCuda.hpp | 2 +- .../tncuda/bindings/LTensorTNCudaBindings.hpp | 97 +- .../measurements/MeasurementsTNCuda.hpp | 1 - .../lightning_tensor/_measurements.py | 17 +- .../lightning_tensor/_tensornet.py | 75 +- .../lightning_tensor/lightning_tensor.py | 24 +- .../lightning_tensor/test_gates_and_expval.py | 151 ++-- .../lightning_tensor/test_lightning_tensor.py | 188 ++-- .../test_measurements_class.py | 76 +- .../test_serialize_chunk_obs_tensor.py | 56 ++ .../lightning_tensor/test_serialize_tensor.py | 843 ++++++++++++++++++ .../lightning_tensor/test_tensornet_class.py | 39 +- tests/test_serialize.py | 299 ++----- tests/test_serialize_chunk_obs.py | 6 + 20 files changed, 1635 insertions(+), 509 deletions(-) create mode 100644 tests/lightning_tensor/test_serialize_chunk_obs_tensor.py create mode 100644 tests/lightning_tensor/test_serialize_tensor.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index aacd7e9269..2f90fb0b5e 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -29,6 +29,9 @@ ### Improvements +* Add Exact Tensor Network cpp binding. +[(#1014)](https://github.com/PennyLaneAI/pennylane-lightning/pull/1014/) + * Catalyst device interfaces support dynamic shots, and no longer parses the device init op's attribute dictionary for a static shots literal. [(#1017)](https://github.com/PennyLaneAI/pennylane-lightning/pull/1017) diff --git a/.github/workflows/tests_gpu_python.yml b/.github/workflows/tests_gpu_python.yml index d12fbe72d0..44198bf717 100644 --- a/.github/workflows/tests_gpu_python.yml +++ b/.github/workflows/tests_gpu_python.yml @@ -183,7 +183,7 @@ jobs: run: | rm -rf build PL_BACKEND=lightning_qubit python scripts/configure_pyproject_toml.py || true - PL_BACKEND=lightning_qubit SKIP_COMPILATION=True python -m pip install . -vv + PL_BACKEND=lightning_qubit python -m pip install . -vv rm -rf build PL_BACKEND=${{ matrix.pl_backend }} python scripts/configure_pyproject_toml.py || true diff --git a/pennylane_lightning/core/_serialize.py b/pennylane_lightning/core/_serialize.py index 1266665782..40338b991f 100644 --- a/pennylane_lightning/core/_serialize.py +++ b/pennylane_lightning/core/_serialize.py @@ -54,12 +54,18 @@ class QuantumScriptSerializer: use_csingle (bool): whether to use np.complex64 instead of np.complex128 use_mpi (bool, optional): If using MPI to accelerate calculation. Defaults to False. split_obs (Union[bool, int], optional): If splitting the observables in a list. Defaults to False. + tensor_backend (str): If using `lightning.tensor` and select the TensorNetwork backend, mps or exact. Default to '' """ - # pylint: disable=import-outside-toplevel, too-many-instance-attributes, c-extension-no-member, too-many-branches, too-many-statements + # pylint: disable=import-outside-toplevel, too-many-instance-attributes, c-extension-no-member, too-many-branches, too-many-statements too-many-positional-arguments too-many-arguments def __init__( - self, device_name, use_csingle: bool = False, use_mpi: bool = False, split_obs: bool = False + self, + device_name, + use_csingle: bool = False, + use_mpi: bool = False, + split_obs: bool = False, + tensor_backend: str = str(), ): self.use_csingle = use_csingle self.device_name = device_name @@ -95,43 +101,14 @@ def __init__( else: raise DeviceError(f'The device name "{device_name}" is not a valid option.') - if device_name == "lightning.tensor": - self.tensornetwork_c64 = lightning_ops.TensorNetC64 - self.tensornetwork_c128 = lightning_ops.TensorNetC128 - else: - self.statevector_c64 = lightning_ops.StateVectorC64 - self.statevector_c128 = lightning_ops.StateVectorC128 - - self.named_obs_c64 = lightning_ops.observables.NamedObsC64 - self.named_obs_c128 = lightning_ops.observables.NamedObsC128 - self.hermitian_obs_c64 = lightning_ops.observables.HermitianObsC64 - self.hermitian_obs_c128 = lightning_ops.observables.HermitianObsC128 - self.tensor_prod_obs_c64 = lightning_ops.observables.TensorProdObsC64 - self.tensor_prod_obs_c128 = lightning_ops.observables.TensorProdObsC128 - self.hamiltonian_c64 = lightning_ops.observables.HamiltonianC64 - self.hamiltonian_c128 = lightning_ops.observables.HamiltonianC128 - - if device_name != "lightning.tensor": - self.sparse_hamiltonian_c64 = lightning_ops.observables.SparseHamiltonianC64 - self.sparse_hamiltonian_c128 = lightning_ops.observables.SparseHamiltonianC128 - self._use_mpi = use_mpi - if self._use_mpi: - self.statevector_mpi_c64 = lightning_ops.StateVectorMPIC64 - self.statevector_mpi_c128 = lightning_ops.StateVectorMPIC128 - self.named_obs_mpi_c64 = lightning_ops.observablesMPI.NamedObsMPIC64 - self.named_obs_mpi_c128 = lightning_ops.observablesMPI.NamedObsMPIC128 - self.hermitian_obs_mpi_c64 = lightning_ops.observablesMPI.HermitianObsMPIC64 - self.hermitian_obs_mpi_c128 = lightning_ops.observablesMPI.HermitianObsMPIC128 - self.tensor_prod_obs_mpi_c64 = lightning_ops.observablesMPI.TensorProdObsMPIC64 - self.tensor_prod_obs_mpi_c128 = lightning_ops.observablesMPI.TensorProdObsMPIC128 - self.hamiltonian_mpi_c64 = lightning_ops.observablesMPI.HamiltonianMPIC64 - self.hamiltonian_mpi_c128 = lightning_ops.observablesMPI.HamiltonianMPIC128 - self.sparse_hamiltonian_mpi_c64 = lightning_ops.observablesMPI.SparseHamiltonianMPIC64 - self.sparse_hamiltonian_mpi_c128 = lightning_ops.observablesMPI.SparseHamiltonianMPIC128 - - self._mpi_manager = lightning_ops.MPIManager + if device_name in ["lightning.qubit", "lightning.kokkos", "lightning.gpu"]: + assert tensor_backend == str() + self._set_lightning_state_bindings(lightning_ops) + else: + self._tensor_backend = tensor_backend + self._set_lightning_tensor_bindings(tensor_backend, lightning_ops) @property def ctype(self): @@ -193,6 +170,75 @@ def sparse_hamiltonian_obs(self): ) return self.sparse_hamiltonian_c64 if self.use_csingle else self.sparse_hamiltonian_c128 + def _set_lightning_state_bindings(self, lightning_ops): + """Define the variables needed to access the modules from the C++ bindings for state vector.""" + + self.statevector_c64 = lightning_ops.StateVectorC64 + self.statevector_c128 = lightning_ops.StateVectorC128 + + self.named_obs_c64 = lightning_ops.observables.NamedObsC64 + self.named_obs_c128 = lightning_ops.observables.NamedObsC128 + self.hermitian_obs_c64 = lightning_ops.observables.HermitianObsC64 + self.hermitian_obs_c128 = lightning_ops.observables.HermitianObsC128 + self.tensor_prod_obs_c64 = lightning_ops.observables.TensorProdObsC64 + self.tensor_prod_obs_c128 = lightning_ops.observables.TensorProdObsC128 + self.hamiltonian_c64 = lightning_ops.observables.HamiltonianC64 + self.hamiltonian_c128 = lightning_ops.observables.HamiltonianC128 + + self.sparse_hamiltonian_c64 = lightning_ops.observables.SparseHamiltonianC64 + self.sparse_hamiltonian_c128 = lightning_ops.observables.SparseHamiltonianC128 + + if self._use_mpi: + self.statevector_mpi_c64 = lightning_ops.StateVectorMPIC64 + self.statevector_mpi_c128 = lightning_ops.StateVectorMPIC128 + + self.named_obs_mpi_c64 = lightning_ops.observablesMPI.NamedObsMPIC64 + self.named_obs_mpi_c128 = lightning_ops.observablesMPI.NamedObsMPIC128 + self.hermitian_obs_mpi_c64 = lightning_ops.observablesMPI.HermitianObsMPIC64 + self.hermitian_obs_mpi_c128 = lightning_ops.observablesMPI.HermitianObsMPIC128 + self.tensor_prod_obs_mpi_c64 = lightning_ops.observablesMPI.TensorProdObsMPIC64 + self.tensor_prod_obs_mpi_c128 = lightning_ops.observablesMPI.TensorProdObsMPIC128 + self.hamiltonian_mpi_c64 = lightning_ops.observablesMPI.HamiltonianMPIC64 + self.hamiltonian_mpi_c128 = lightning_ops.observablesMPI.HamiltonianMPIC128 + + self.sparse_hamiltonian_mpi_c64 = lightning_ops.observablesMPI.SparseHamiltonianMPIC64 + self.sparse_hamiltonian_mpi_c128 = lightning_ops.observablesMPI.SparseHamiltonianMPIC128 + + self._mpi_manager = lightning_ops.MPIManager + + def _set_lightning_tensor_bindings(self, tensor_backend, lightning_ops): + """Define the variables needed to access the modules from the C++ bindings for tensor network.""" + if tensor_backend == "mps": + self.tensornetwork_c64 = lightning_ops.mpsTensorNetC64 + self.tensornetwork_c128 = lightning_ops.mpsTensorNetC128 + + self.named_obs_c64 = lightning_ops.observables.mpsNamedObsC64 + self.named_obs_c128 = lightning_ops.observables.mpsNamedObsC128 + self.hermitian_obs_c64 = lightning_ops.observables.mpsHermitianObsC64 + self.hermitian_obs_c128 = lightning_ops.observables.mpsHermitianObsC128 + self.tensor_prod_obs_c64 = lightning_ops.observables.mpsTensorProdObsC64 + self.tensor_prod_obs_c128 = lightning_ops.observables.mpsTensorProdObsC128 + self.hamiltonian_c64 = lightning_ops.observables.mpsHamiltonianC64 + self.hamiltonian_c128 = lightning_ops.observables.mpsHamiltonianC128 + + elif tensor_backend == "tn": + self.tensornetwork_c64 = lightning_ops.exactTensorNetC64 + self.tensornetwork_c128 = lightning_ops.exactTensorNetC128 + + self.named_obs_c64 = lightning_ops.observables.exactNamedObsC64 + self.named_obs_c128 = lightning_ops.observables.exactNamedObsC128 + self.hermitian_obs_c64 = lightning_ops.observables.exactHermitianObsC64 + self.hermitian_obs_c128 = lightning_ops.observables.exactHermitianObsC128 + self.tensor_prod_obs_c64 = lightning_ops.observables.exactTensorProdObsC64 + self.tensor_prod_obs_c128 = lightning_ops.observables.exactTensorProdObsC128 + self.hamiltonian_c64 = lightning_ops.observables.exactHamiltonianC64 + self.hamiltonian_c128 = lightning_ops.observables.exactHamiltonianC128 + + else: + raise ValueError( + f"Unsupported method: {tensor_backend}. Supported methods are 'mps' (Matrix Product State) and 'tn' (Exact Tensor Network)." + ) + def _named_obs(self, observable, wires_map: dict = None): """Serializes a Named observable""" wires = [wires_map[w] for w in observable.wires] if wires_map else observable.wires.tolist() diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 1706e33407..5b8fe400f6 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.40.0-dev33" +__version__ = "0.40.0-dev34" diff --git a/pennylane_lightning/core/src/bindings/Bindings.cpp b/pennylane_lightning/core/src/bindings/Bindings.cpp index 2733e6f5e0..596eef765e 100644 --- a/pennylane_lightning/core/src/bindings/Bindings.cpp +++ b/pennylane_lightning/core/src/bindings/Bindings.cpp @@ -85,6 +85,6 @@ PYBIND11_MODULE( // Register bindings for backend-specific info: registerBackendSpecificInfo(m); - registerLightningTensorClassBindings(m); + registerLightningTensorClassBindings(m); } #endif diff --git a/pennylane_lightning/core/src/bindings/Bindings.hpp b/pennylane_lightning/core/src/bindings/Bindings.hpp index fc6e3e60a9..e3b7811116 100644 --- a/pennylane_lightning/core/src/bindings/Bindings.hpp +++ b/pennylane_lightning/core/src/bindings/Bindings.hpp @@ -749,7 +749,7 @@ void registerLightningTensorBackendAgnosticMeasurements(PyClass &pyclass) { "Variance of an observable object.") .def("generate_samples", [](MeasurementsT &M, const std::vector &wires, - const std::size_t num_shots) { + std::size_t num_shots) { constexpr auto sz = sizeof(std::size_t); const std::size_t num_wires = wires.size(); const std::size_t ndim = 2; @@ -769,23 +769,154 @@ void registerLightningTensorBackendAgnosticMeasurements(PyClass &pyclass) { }); } +/** + * @brief Register observable classes for TensorNetwork. + * + * @tparam LightningBackendT + * @param m Pybind module + * @param name backend name of TN (mps, tn) + */ +template +void registerBackendAgnosticObservablesTensor(py::module_ &m, + const std::string &name) { + using PrecisionT = + typename LightningBackendT::PrecisionT; // LightningBackendT's's + // precision. + using ComplexT = + typename LightningBackendT::ComplexT; // LightningBackendT's + // complex type. + using ParamT = PrecisionT; // Parameter's data precision + + const std::string bitsize = + std::to_string(sizeof(std::complex) * 8); + + using np_arr_c = py::array_t, py::array::c_style>; + using np_arr_r = py::array_t; + + using ObservableT = ObservableTNCuda; + using NamedObsT = NamedObsTNCuda; + using HermitianObsT = HermitianObsTNCuda; + using TensorProdObsT = TensorProdObsTNCuda; + using HamiltonianT = HamiltonianTNCuda; + + std::string class_name; + + class_name = std::string(name) + "ObservableC" + bitsize; + py::class_>(m, class_name.c_str(), + py::module_local()); + + class_name = std::string(name) + "NamedObsC" + bitsize; + py::class_, ObservableT>( + m, class_name.c_str(), py::module_local()) + .def(py::init( + [](const std::string &name, const std::vector &wires) { + return NamedObsT(name, wires); + })) + .def("__repr__", &NamedObsT::getObsName) + .def("get_wires", &NamedObsT::getWires, "Get wires of observables") + .def( + "__eq__", + [](const NamedObsT &self, py::handle other) -> bool { + if (!py::isinstance(other)) { + return false; + } + auto &&other_cast = other.cast(); + return self == other_cast; + }, + "Compare two observables"); + + class_name = std::string(name) + "HermitianObsC" + bitsize; + py::class_, ObservableT>( + m, class_name.c_str(), py::module_local()) + .def(py::init([](const np_arr_c &matrix, + const std::vector &wires) { + auto const &buffer = matrix.request(); + const auto ptr = static_cast(buffer.ptr); + return HermitianObsT(std::vector(ptr, ptr + buffer.size), + wires); + })) + .def("__repr__", &HermitianObsT::getObsName) + .def("get_wires", &HermitianObsT::getWires, "Get wires of observables") + .def("get_matrix", &HermitianObsT::getMatrix, + "Get matrix representation of Hermitian operator") + .def( + "__eq__", + [](const HermitianObsT &self, py::handle other) -> bool { + if (!py::isinstance(other)) { + return false; + } + auto &&other_cast = other.cast(); + return self == other_cast; + }, + "Compare two observables"); + + class_name = std::string(name) + "TensorProdObsC" + bitsize; + py::class_, ObservableT>( + m, class_name.c_str(), py::module_local()) + .def(py::init([](const std::vector> &obs) { + return TensorProdObsT(obs); + })) + .def("__repr__", &TensorProdObsT::getObsName) + .def("get_wires", &TensorProdObsT::getWires, "Get wires of observables") + .def("get_ops", &TensorProdObsT::getObs, "Get operations list") + .def( + "__eq__", + [](const TensorProdObsT &self, py::handle other) -> bool { + if (!py::isinstance(other)) { + return false; + } + auto &&other_cast = other.cast(); + return self == other_cast; + }, + "Compare two observables"); + + class_name = std::string(name) + "HamiltonianC" + bitsize; + using ObsPtr = std::shared_ptr; + py::class_, ObservableT>( + m, class_name.c_str(), py::module_local()) + .def(py::init( + [](const np_arr_r &coeffs, const std::vector &obs) { + auto const &buffer = coeffs.request(); + const auto ptr = static_cast(buffer.ptr); + return HamiltonianT{std::vector(ptr, ptr + buffer.size), + obs}; + })) + .def("__repr__", &HamiltonianT::getObsName) + .def("get_wires", &HamiltonianT::getWires, "Get wires of observables") + .def("get_ops", &HamiltonianT::getObs, + "Get operations contained by Hamiltonian") + .def("get_coeffs", &HamiltonianT::getCoeffs, + "Get Hamiltonian coefficients") + .def( + "__eq__", + [](const HamiltonianT &self, py::handle other) -> bool { + if (!py::isinstance(other)) { + return false; + } + auto &&other_cast = other.cast(); + return self == other_cast; + }, + "Compare two observables"); +} + /** * @brief Templated class to build lightning.tensor class bindings. * - * @tparam TensorNetT Tensor network type + * @tparam TensorNetT Tensor network type. * @param m Pybind11 module. */ template void lightningTensorClassBindings(py::module_ &m) { using PrecisionT = typename TensorNetT::PrecisionT; // TensorNet's precision. // Enable module name to be based on size of complex datatype + auto name = TensorNetT::method; // TensorNet's backend name [mps, exact]. const std::string bitsize = std::to_string(sizeof(std::complex) * 8); //***********************************************************************// // TensorNet //***********************************************************************// - std::string class_name = "TensorNetC" + bitsize; + std::string class_name = std::string(name) + "TensorNetC" + bitsize; auto pyclass = py::class_(m, class_name.c_str(), py::module_local()); @@ -797,12 +928,12 @@ template void lightningTensorClassBindings(py::module_ &m) { /* Observables submodule */ py::module_ obs_submodule = m.def_submodule("observables", "Submodule for observables classes."); - registerBackendAgnosticObservables(obs_submodule); + registerBackendAgnosticObservablesTensor(obs_submodule, name); //***********************************************************************// // Measurements //***********************************************************************// - class_name = "MeasurementsC" + bitsize; + class_name = std::string(name) + "MeasurementsC" + bitsize; auto pyclass_measurements = py::class_>( m, class_name.c_str(), py::module_local()); diff --git a/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/ExactTNCuda.hpp b/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/ExactTNCuda.hpp index 0afc1f23a4..4dfb67f78c 100644 --- a/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/ExactTNCuda.hpp +++ b/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/ExactTNCuda.hpp @@ -51,7 +51,7 @@ class ExactTNCuda final : public TNCuda> { using BaseType = TNCuda; public: - constexpr static auto method = "exacttn"; + constexpr static auto method = "exact"; using CFP_t = decltype(cuUtil::getCudaType(Precision{})); using ComplexT = std::complex; diff --git a/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/bindings/LTensorTNCudaBindings.hpp b/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/bindings/LTensorTNCudaBindings.hpp index d80c254f0e..1522305a82 100644 --- a/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/bindings/LTensorTNCudaBindings.hpp +++ b/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/bindings/LTensorTNCudaBindings.hpp @@ -28,6 +28,7 @@ #include "DevTag.hpp" #include "DevicePool.hpp" #include "Error.hpp" +#include "ExactTNCuda.cpp" #include "MPSTNCuda.hpp" #include "TypeList.hpp" #include "Util.hpp" @@ -45,8 +46,9 @@ using Pennylane::LightningTensor::TNCuda::MPSTNCuda; namespace py = pybind11; namespace Pennylane::LightningTensor::TNCuda { -using TensorNetBackends = - Pennylane::Util::TypeList, MPSTNCuda, void>; +using TensorNetworkBackends = + Pennylane::Util::TypeList, MPSTNCuda, + ExactTNCuda, ExactTNCuda, void>; /** * @brief Register controlled matrix kernel. @@ -102,12 +104,14 @@ void registerControlledGate(PyClass &pyclass) { } /** - * @brief Get a gate kernel map for a tensor network. + * @brief Get a gate kernel map for a tensor network using MPS. + * + * @tparam TensorNetT + * @tparam PyClass + * @param pyclass Pybind11's measurements class to bind methods. */ template -void registerBackendClassSpecificBindings(PyClass &pyclass) { - registerGatesForTensorNet(pyclass); - registerControlledGate(pyclass); +void registerBackendClassSpecificBindingsMPS(PyClass &pyclass) { using PrecisionT = typename TensorNet::PrecisionT; // TensorNet's precision using ParamT = PrecisionT; // Parameter's data precision @@ -115,9 +119,8 @@ void registerBackendClassSpecificBindings(PyClass &pyclass) { py::array::c_style | py::array::forcecast>; pyclass - .def(py::init()) // num_qubits, max_bond_dim - .def(py::init()) // num_qubits, max_bond_dim + .def(py::init>()) // num_qubits, max_bond_dim, dev-tag .def( "getState", @@ -153,7 +156,7 @@ void registerBackendClassSpecificBindings(PyClass &pyclass) { .def( "applyMPOOperation", [](TensorNet &tensor_network, std::vector &tensors, - std::vector &wires, const std::size_t MPOBondDims) { + const std::vector &wires, std::size_t MPOBondDims) { using ComplexT = typename TensorNet::ComplexT; std::vector> conv_tensors; for (const auto &tensor : tensors) { @@ -169,13 +172,85 @@ void registerBackendClassSpecificBindings(PyClass &pyclass) { .def( "appendMPSFinalState", [](TensorNet &tensor_network, double cutoff, - std::string cutoff_mode) { + const std::string &cutoff_mode) { tensor_network.append_mps_final_state(cutoff, cutoff_mode); }, "Get the final state.") .def("reset", &TensorNet::reset, "Reset the statevector."); } +/** + * @brief Get a gate kernel map for a tensor network using ExactTN. + * + * @tparam TensorNetT + * @tparam PyClass + * @param pyclass Pybind11's measurements class to bind methods. + */ +template +void registerBackendClassSpecificBindingsExactTNCuda(PyClass &pyclass) { + using PrecisionT = typename TensorNet::PrecisionT; // TensorNet's precision + using ParamT = PrecisionT; // Parameter's data precision + using np_arr_c = py::array_t, + py::array::c_style | py::array::forcecast>; + + pyclass + .def(py::init()) // num_qubits + .def(py::init>()) // num_qubits, dev-tag + .def( + "getState", + [](TensorNet &tensor_network, np_arr_c &state) { + py::buffer_info numpyArrayInfo = state.request(); + auto *data_ptr = + static_cast *>(numpyArrayInfo.ptr); + + tensor_network.getData(data_ptr, state.size()); + }, + "Copy StateVector data into a Numpy array.") + .def("applyControlledMatrix", &applyControlledMatrix, + "Apply controlled operation") + .def( + "setBasisState", + [](TensorNet &tensor_network, + std::vector &basisState) { + tensor_network.setBasisState(basisState); + }, + "Create Basis State on GPU.") + .def( + "updateMPSSitesData", + [](TensorNet &tensor_network, std::vector &tensors) { + for (std::size_t idx = 0; idx < tensors.size(); idx++) { + py::buffer_info numpyArrayInfo = tensors[idx].request(); + auto *data_ptr = static_cast *>( + numpyArrayInfo.ptr); + tensor_network.updateSiteData(idx, data_ptr, + tensors[idx].size()); + } + }, + "Pass MPS site data to the C++ backend.") + .def("reset", &TensorNet::reset, "Reset the statevector."); +} + +/** + * @brief Get a gate kernel map for a tensor network. + * + * @tparam TensorNetT + * @tparam PyClass + * @param pyclass Pybind11's measurements class to bind methods. + */ +template +void registerBackendClassSpecificBindings(PyClass &pyclass) { + registerGatesForTensorNet(pyclass); + registerControlledGate(pyclass); + + if constexpr (std::is_same_v> || + std::is_same_v>) { + registerBackendClassSpecificBindingsMPS(pyclass); + } + if constexpr (std::is_same_v> || + std::is_same_v>) { + registerBackendClassSpecificBindingsExactTNCuda(pyclass); + } +} /** * @brief Provide backend information. */ diff --git a/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/measurements/MeasurementsTNCuda.hpp b/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/measurements/MeasurementsTNCuda.hpp index 146f29f1b9..7a56bde1cd 100644 --- a/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/measurements/MeasurementsTNCuda.hpp +++ b/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/measurements/MeasurementsTNCuda.hpp @@ -27,7 +27,6 @@ #include #include "LinearAlg.hpp" -#include "MPSTNCuda.hpp" #include "ObservablesTNCuda.hpp" #include "ObservablesTNCudaOperator.hpp" diff --git a/pennylane_lightning/lightning_tensor/_measurements.py b/pennylane_lightning/lightning_tensor/_measurements.py index 4389a385fb..6f63a18e43 100644 --- a/pennylane_lightning/lightning_tensor/_measurements.py +++ b/pennylane_lightning/lightning_tensor/_measurements.py @@ -17,7 +17,12 @@ # pylint: disable=import-error, no-name-in-module, ungrouped-imports try: - from pennylane_lightning.lightning_tensor_ops import MeasurementsC64, MeasurementsC128 + from pennylane_lightning.lightning_tensor_ops import ( + exactMeasurementsC64, + exactMeasurementsC128, + mpsMeasurementsC64, + mpsMeasurementsC128, + ) except ImportError: pass @@ -62,6 +67,7 @@ def __init__( ) -> None: self._tensornet = tensor_network self._dtype = tensor_network.dtype + self._method = tensor_network._method self._measurement_lightning = self._measurement_dtype()(tensor_network.tensornet) @property @@ -74,7 +80,10 @@ def _measurement_dtype(self): Returns: the Measurements class """ - return MeasurementsC64 if self.dtype == np.complex64 else MeasurementsC128 + if self._method == "tn": # Using "tn" method + return exactMeasurementsC64 if self.dtype == np.complex64 else exactMeasurementsC128 + # Using "mps" method + return mpsMeasurementsC64 if self.dtype == np.complex64 else mpsMeasurementsC128 def state_diagonalizing_gates(self, measurementprocess: StateMeasurement) -> TensorLike: """Apply a measurement to state when the measurement process has an observable with diagonalizing gates. @@ -114,7 +123,7 @@ def expval(self, measurementprocess: MeasurementProcess): raise ValueError("The number of Hermitian observables target wires should be 1.") ob_serialized = QuantumScriptSerializer( - self._tensornet.device_name, self.dtype == np.complex64 + self._tensornet.device_name, self.dtype == np.complex64, tensor_backend=self._method )._ob(measurementprocess.obs) return self._measurement_lightning.expval(ob_serialized) @@ -160,7 +169,7 @@ def var(self, measurementprocess: MeasurementProcess): raise ValueError("The number of Hermitian observables target wires should be 1.") ob_serialized = QuantumScriptSerializer( - self._tensornet.device_name, self.dtype == np.complex64 + self._tensornet.device_name, self.dtype == np.complex64, tensor_backend=self._method )._ob(measurementprocess.obs) return self._measurement_lightning.var(ob_serialized) diff --git a/pennylane_lightning/lightning_tensor/_tensornet.py b/pennylane_lightning/lightning_tensor/_tensornet.py index d1964cd28b..d0f61206b2 100644 --- a/pennylane_lightning/lightning_tensor/_tensornet.py +++ b/pennylane_lightning/lightning_tensor/_tensornet.py @@ -17,7 +17,12 @@ # pylint: disable=import-error, no-name-in-module, ungrouped-imports try: - from pennylane_lightning.lightning_tensor_ops import TensorNetC64, TensorNetC128 + from pennylane_lightning.lightning_tensor_ops import ( + exactTensorNetC64, + exactTensorNetC128, + mpsTensorNetC64, + mpsTensorNetC128, + ) except ImportError: pass @@ -128,7 +133,8 @@ class LightningTensorNet: num_wires(int): the number of wires to initialize the device with c_dtype: Datatypes for tensor network representation. Must be one of ``np.complex64`` or ``np.complex128``. Default is ``np.complex128`` - method(string): tensor network method. Options: ["mps"]. Default is "mps". + method(string): tensor network method. Supported methods are "mps" (Matrix Product State) and + "tn" (Exact Tensor Network). Options: ["mps", "tn"]. max_bond_dim(int): maximum bond dimension for the tensor network cutoff(float): threshold for singular value truncation. Default is 0. cutoff_mode(string): singular value truncation mode. Options: ["rel", "abs"]. @@ -146,23 +152,29 @@ def __init__( cutoff_mode: str = "abs", device_name="lightning.tensor", ): - self._num_wires = num_wires - self._max_bond_dim = max_bond_dim - self._method = method - self._cutoff = cutoff - self._cutoff_mode = cutoff_mode - self._c_dtype = c_dtype - if device_name != "lightning.tensor": raise DeviceError(f'The device name "{device_name}" is not a valid option.') if num_wires < 2: raise ValueError("Number of wires must be greater than 1.") + self._num_wires = num_wires + self._method = method + self._c_dtype = c_dtype + self._max_bond_dim = max_bond_dim + self._cutoff = cutoff + self._cutoff_mode = cutoff_mode + self._device_name = device_name + self._wires = Wires(range(num_wires)) - self._device_name = device_name - self._tensornet = self._tensornet_dtype()(self._num_wires, self._max_bond_dim) + self._tensornet = None + if self._method == "mps": + self._tensornet = self._tensornet_dtype()(self._num_wires, self._max_bond_dim) + elif self._method == "tn": + self._tensornet = self._tensornet_dtype()(self._num_wires) + else: + raise DeviceError(f"The method {self._method} is not supported.") @property def dtype(self): @@ -179,6 +191,11 @@ def num_wires(self): """Number of wires addressed on this device""" return self._num_wires + @property + def method(self): + """Returns the method (mps or tn) for evaluating the tensor network.""" + return self._method + @property def tensornet(self): """Returns a handle to the tensor network.""" @@ -196,7 +213,10 @@ def _tensornet_dtype(self): Returns: the tensor network class """ - return TensorNetC128 if self.dtype == np.complex128 else TensorNetC64 + if self.method == "tn": # Using "tn" method + return exactTensorNetC128 if self.dtype == np.complex128 else exactTensorNetC64 + # Using "mps" method + return mpsTensorNetC128 if self.dtype == np.complex128 else mpsTensorNetC64 def reset_state(self): """Reset the device's initial quantum state""" @@ -270,12 +290,14 @@ def _apply_state_vector(self, state, device_wires: Wires): or broadcasted state of shape ``(batch_size, 2**len(device_wires))`` device_wires (Wires): wires that get initialized in the state """ + if self.method == "tn": + raise DeviceError("Exact Tensor Network does not support StatePrep") - state = self._preprocess_state_vector(state, device_wires) - mps_site_shape = [2] - M = decompose_dense(state, self._num_wires, mps_site_shape, self._max_bond_dim) - - self._tensornet.updateMPSSitesData(M) + if self.method == "mps": + state = self._preprocess_state_vector(state, device_wires) + mps_site_shape = [2] + M = decompose_dense(state, self._num_wires, mps_site_shape, self._max_bond_dim) + self._tensornet.updateMPSSitesData(M) def _apply_basis_state(self, state, wires): """Initialize the quantum state in a specified computational basis state. @@ -397,15 +419,25 @@ def _apply_lightning(self, operations): # To support older versions of PL gate_ops_matrix = operation.matrix() - self._apply_MPO(gate_ops_matrix, wires) + if self.method == "mps": + self._apply_MPO(gate_ops_matrix, wires) + if self.method == "tn": + method = getattr(tensornet, "applyMatrix") + method(gate_ops_matrix, wires, False) def apply_operations(self, operations): """Append operations to the tensor network graph.""" # 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:] + if self.method == "tn": + raise DeviceError("Exact Tensor Network does not support StatePrep") + + if self.method == "mps": + 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:] @@ -423,6 +455,7 @@ def set_tensor_network(self, circuit: QuantumScript): """ self.apply_operations(circuit.operations) self.appendMPSFinalState() + return self def appendMPSFinalState(self): @@ -430,5 +463,5 @@ def appendMPSFinalState(self): Append the final state to the tensor network for the MPS backend. This is an function to be called by once apply_operations is called. """ - if self._method == "mps": + if self.method == "mps": self._tensornet.appendMPSFinalState(self._cutoff, self._cutoff_mode) diff --git a/pennylane_lightning/lightning_tensor/lightning_tensor.py b/pennylane_lightning/lightning_tensor/lightning_tensor.py index 8bba55a135..5490c86439 100644 --- a/pennylane_lightning/lightning_tensor/lightning_tensor.py +++ b/pennylane_lightning/lightning_tensor/lightning_tensor.py @@ -64,7 +64,7 @@ _backends = frozenset({"cutensornet"}) # The set of supported backends. -_methods = frozenset({"mps"}) +_methods = frozenset({"mps", "tn"}) # The set of supported methods. _operations = frozenset( @@ -165,7 +165,7 @@ def stopping_condition(op: Operator) -> bool: - """A function that determines whether or not an operation is supported by the ``mps`` method of ``lightning.tensor``.""" + """A function that determines whether or not an operation is supported by ``lightning.tensor``.""" if isinstance(op, qml.ControlledQubitUnitary): return True @@ -223,7 +223,7 @@ class LightningTensor(Device): shots (int): Measurements are performed drawing ``shots`` times from a discrete random variable distribution associated with a state vector and an observable. Defaults to ``None`` if not specified. Setting to ``None`` results in computing statistics like expectation values and variances analytically. - method (str): Supported method. Currently, only ``mps`` is supported. + method (str): Supported method. The supported methods are ``"mps"`` (Matrix Product State) and ``"tn"`` (Tensor Network). c_dtype: Datatypes for the tensor representation. Must be one of ``numpy.complex64`` or ``numpy.complex128``. Default is ``numpy.complex128``. Keyword Args: @@ -262,7 +262,11 @@ def circuit(num_qubits): # pylint: disable=too-many-instance-attributes # So far we just consider the options for MPS simulator - _device_options = ("backend", "max_bond_dim", "cutoff", "cutoff_mode") + _device_options = { + "mps": ("backend", "max_bond_dim", "cutoff", "cutoff_mode"), + "tn": ("backend", "cutoff", "cutoff_mode"), + } + _CPP_BINARY_AVAILABLE = LT_CPP_BINARY_AVAILABLE _new_API = True @@ -287,7 +291,9 @@ def __init__( raise ImportError("Pre-compiled binaries for lightning.tensor are not available. ") if not accepted_methods(method): - raise ValueError(f"Unsupported method: {method}") + raise ValueError( + f"Unsupported method: {method}. Supported methods are 'mps' (Matrix Product State) and 'tn' (Exact Tensor Network)." + ) if c_dtype not in [np.complex64, np.complex128]: # pragma: no cover raise TypeError(f"Unsupported complex type: {c_dtype}") @@ -312,7 +318,11 @@ def __init__( self._backend = kwargs.get("backend", "cutensornet") for arg in kwargs: - if arg not in self._device_options: + if self._method == "mps" and arg not in self._device_options["mps"]: + raise TypeError( + f"Unexpected argument: {arg} during initialization of the lightning.tensor device." + ) + if self._method == "tn" and arg not in self._device_options["tn"]: raise TypeError( f"Unexpected argument: {arg} during initialization of the lightning.tensor device." ) @@ -375,7 +385,7 @@ def _setup_execution_config( updated_values = {} new_device_options = dict(config.device_options) - for option in self._device_options: + for option in self._device_options[self.method]: if option not in new_device_options: new_device_options[option] = getattr(self, f"_{option}", None) diff --git a/tests/lightning_tensor/test_gates_and_expval.py b/tests/lightning_tensor/test_gates_and_expval.py index 66604c2b05..297a60b793 100644 --- a/tests/lightning_tensor/test_gates_and_expval.py +++ b/tests/lightning_tensor/test_gates_and_expval.py @@ -138,49 +138,60 @@ def circuit_ansatz(params, wires): qml.ECR(wires=[wires[1], wires[3]]) +# The expected values were generated using default.qubit +@pytest.mark.parametrize("method", [{"method": "mps", "max_bond_dim": 128}, {"method": "tn"}]) @pytest.mark.parametrize( - "returns", + "returns,expected_value", [ - (qml.PauliX(0),), - (qml.PauliY(0),), - (qml.PauliZ(0),), - (qml.PauliX(1),), - (qml.PauliY(1),), - (qml.PauliZ(1),), - (qml.PauliX(2),), - (qml.PauliY(2),), - (qml.PauliZ(2),), - (qml.PauliX(3),), - (qml.PauliY(3),), - (qml.PauliZ(3),), - (qml.PauliX(0), qml.PauliY(1)), + ((qml.PauliX(0),), -0.094606003), + ((qml.PauliY(0),), -0.138130983), + ((qml.PauliZ(0),), 0.052683073), + ((qml.PauliX(1),), -0.027114956), + ((qml.PauliY(1),), 0.035227835), + ((qml.PauliZ(1),), 0.130383680), + ((qml.PauliX(2),), -0.112239026), + ((qml.PauliY(2),), -0.043408985), + ((qml.PauliZ(2),), -0.186733557), + ((qml.PauliX(3),), 0.081030290), + ((qml.PauliY(3),), 0.136389367), + ((qml.PauliZ(3),), -0.024382650), + ((qml.PauliX(0), qml.PauliY(1)), [-0.094606, 0.03522784]), ( - qml.PauliZ(0), - qml.PauliX(1), - qml.PauliY(2), + ( + qml.PauliZ(0), + qml.PauliX(1), + qml.PauliY(2), + ), + [0.05268307, -0.02711496, -0.04340899], ), ( - qml.PauliY(0), - qml.PauliZ(1), - qml.PauliY(3), + ( + qml.PauliY(0), + qml.PauliZ(1), + qml.PauliY(3), + ), + [-0.13813098, 0.13038368, 0.13638937], ), - (qml.PauliZ(0) @ qml.PauliY(3),), - (qml.Hadamard(2),), - (qml.Hadamard(3) @ qml.PauliZ(2),), - (qml.PauliX(0) @ qml.PauliY(3),), - (qml.PauliY(0) @ qml.PauliY(2) @ qml.PauliY(3),), - (qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliZ(2),), - (0.5 * qml.PauliZ(0) @ qml.PauliZ(2),), - (qml.ops.LinearCombination([1.0, 2.0], [qml.X(0) @ qml.Z(1), qml.Y(3) @ qml.Z(2)])), - (qml.ops.prod(qml.X(0), qml.Y(1))), + ((qml.PauliZ(0) @ qml.PauliY(3),), 0.174335019), + ((qml.Hadamard(2),), -0.211405541), + ((qml.Hadamard(3) @ qml.PauliZ(2),), -0.024206963), + ((qml.PauliX(0) @ qml.PauliY(3),), 0.088232689), + ((qml.PauliY(0) @ qml.PauliY(2) @ qml.PauliY(3),), 0.193644667), + ((qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliZ(2),), -0.034583947), + ((0.5 * qml.PauliZ(0) @ qml.PauliZ(2),), 0.002016079), + ( + (qml.ops.LinearCombination([1.0, 2.0], [qml.X(0) @ qml.Z(1), qml.Y(3) @ qml.Z(2)])), + [0.08618213, 0.09506244], + ), + ((qml.ops.prod(qml.X(0), qml.Y(1))), [-0.094606, 0.03522784]), ], ) -def test_integration_for_all_supported_gates(returns): +def test_integration_for_all_supported_gates(returns, expected_value, method): """Integration tests that compare to default.qubit for a large circuit containing parametrized operations""" + num_wires = 8 - dev_default = qml.device("default.qubit", wires=range(num_wires)) - dev_ltensor = LightningTensor(wires=range(num_wires), max_bond_dim=128, c_dtype=np.complex128) + dev_ltensor = LightningTensor(wires=range(num_wires), c_dtype=np.complex128, **method) def circuit(params): qml.BasisState(np.array([1, 0, 1, 0, 1, 0, 1, 0]), wires=range(num_wires)) @@ -192,33 +203,30 @@ def circuit(params): params_init = np.random.rand(n_params) params = np.array(params_init, requires_grad=True) - qnode_ltensor = qml.QNode(circuit, dev_ltensor) - qnode_default = qml.QNode(circuit, dev_default) - j_ltensor = qnode_ltensor(params) - j_default = qnode_default(params) - assert np.allclose(j_ltensor, j_default, rtol=1e-6) + assert np.allclose(j_ltensor, expected_value, rtol=1e-6) +@pytest.mark.parametrize("method", [{"method": "mps", "max_bond_dim": 128}, {"method": "tn"}]) class TestSparseHExpval: """Test sparseH expectation values""" @pytest.mark.parametrize( "cases", [ - [qml.PauliX(0) @ qml.Identity(1), 0.00000000000000000, 1.000000000000000000], - [qml.Identity(0) @ qml.PauliX(1), -0.19866933079506122, 0.960530638694763184], - [qml.PauliY(0) @ qml.Identity(1), -0.38941834230865050, 0.848353326320648193], - [qml.Identity(0) @ qml.PauliY(1), 0.00000000000000000, 1.000000119209289551], - [qml.PauliZ(0) @ qml.Identity(1), 0.92106099400288520, 0.151646673679351807], - [qml.Identity(0) @ qml.PauliZ(1), 0.98006657784124170, 0.039469480514526367], + [qml.PauliX(0) @ qml.Identity(1), 0.000000000, 1.000000000], + [qml.Identity(0) @ qml.PauliX(1), -0.198669330, 0.960530638], + [qml.PauliY(0) @ qml.Identity(1), -0.389418342, 0.848353326], + [qml.Identity(0) @ qml.PauliY(1), 0.000000000, 1.000000119], + [qml.PauliZ(0) @ qml.Identity(1), 0.921060994, 0.151646673], + [qml.Identity(0) @ qml.PauliZ(1), 0.980066577, 0.039469480], ], ) - def test_sparse_Pauli_words(self, cases, qubit_device): + def test_sparse_Pauli_words(self, cases, qubit_device, method): """Test expval of some simple sparse Hamiltonian""" - dev = qubit_device(wires=4) + dev = qml.device(device_name, wires=4, **method) @qml.qnode(dev, diff_method="parameter-shift") def circuit_expval(): @@ -233,23 +241,25 @@ def circuit_expval(): with pytest.raises(DeviceError): circuit_expval() - def test_expval_sparseH_not_supported(self): + def test_expval_sparseH_not_supported(self, method): """Test that expval of SparseH is not supported.""" with qml.queuing.AnnotatedQueue() as q: qml.expval(qml.SparseHamiltonian(qml.PauliX.compute_sparse_matrix(), wires=0)) - tensornet = LightningTensorNet(4, 10) + tensornet = LightningTensorNet(4, **method) + m = LightningTensorMeasurements(tensornet) with pytest.raises(NotImplementedError, match="Sparse Hamiltonians are not supported."): m.expval(q.queue[0]) - def test_var_sparseH_not_supported(self): + def test_var_sparseH_not_supported(self, method): """Test that var of SparseH is not supported.""" with qml.queuing.AnnotatedQueue() as q: qml.var(qml.SparseHamiltonian(qml.PauliX.compute_sparse_matrix(), wires=0)) - tensornet = LightningTensorNet(4, 10) + tensornet = LightningTensorNet(4, **method) + m = LightningTensorMeasurements(tensornet) with pytest.raises( @@ -258,12 +268,13 @@ def test_var_sparseH_not_supported(self): ): m.var(q.queue[0]) - def test_expval_hermitian_not_supported(self): + def test_expval_hermitian_not_supported(self, method): """Test that expval of Hermitian with 1+ wires is not supported.""" with qml.queuing.AnnotatedQueue() as q: qml.expval(qml.Hermitian(np.eye(4), wires=[0, 1])) - tensornet = LightningTensorNet(4, 10) + tensornet = LightningTensorNet(4, **method) + m = LightningTensorMeasurements(tensornet) with pytest.raises( @@ -271,12 +282,13 @@ def test_expval_hermitian_not_supported(self): ): m.expval(q.queue[0]) - def test_var_hermitian_not_supported(self): + def test_var_hermitian_not_supported(self, method): """Test that var of Hermitian with 1+ wires is not supported.""" with qml.queuing.AnnotatedQueue() as q: qml.var(qml.Hermitian(np.eye(4), wires=[0, 1])) - tensornet = LightningTensorNet(4, 10) + tensornet = LightningTensorNet(4, **method) + m = LightningTensorMeasurements(tensornet) with pytest.raises( @@ -285,11 +297,19 @@ def test_var_hermitian_not_supported(self): m.var(q.queue[0]) -class QChem: +@pytest.mark.parametrize("method", [{"method": "mps", "max_bond_dim": 128}, {"method": "tn"}]) +class TestQChem: """Integration tests for qchem module by parameter-shift and finite-diff differentiation methods.""" - @pytest.mark.parametrize("diff_approach", ["parameter-shift", "finite-diff"]) - def test_integration_H2_Hamiltonian(self, diff_approach): + # The expected values were generated using default.qubit + @pytest.mark.parametrize( + "diff_approach, expected_value", + [ + ("parameter-shift", -0.17987143), + ("finite-diff", -0.17987139), + ], + ) + def test_integration_H2_Hamiltonian(self, diff_approach, expected_value, method): symbols = ["H", "H"] geometry = np.array( @@ -307,15 +327,13 @@ def test_integration_H2_Hamiltonian(self, diff_approach): singles, doubles = qml.qchem.excitations(mol.n_electrons, len(H.wires)) - excitations = singles + doubles num_params = len(singles + doubles) params = np.zeros(num_params, requires_grad=True) hf_state = qml.qchem.hf_state(mol.n_electrons, qubits) # Choose different batching supports here - dev = qml.device(device_name, wires=qubits) - dev_comp = qml.device("default.qubit", wires=qubits) + dev = qml.device(device_name, wires=qubits, **method) @qml.qnode(dev, diff_method=diff_approach) def circuit(params, excitations): @@ -327,22 +345,9 @@ def circuit(params, excitations): qml.SingleExcitation(params[i], wires=excitation) return qml.expval(H) - @qml.qnode(dev_comp, diff_method=diff_approach) - def circuit_compare(params, excitations): - qml.BasisState(hf_state, wires=range(qubits)) - - for i, excitation in enumerate(excitations): - if len(excitation) == 4: - qml.DoubleExcitation(params[i], wires=excitation) - else: - qml.SingleExcitation(params[i], wires=excitation) - return qml.expval(H) - jac_func = qml.jacobian(circuit) - jac_func_comp = qml.jacobian(circuit_compare) params = qml.numpy.array([0.0] * len(doubles), requires_grad=True) jacs = jac_func(params, excitations=doubles) - jacs_comp = jac_func_comp(params, excitations=doubles) - assert np.allclose(jacs, jacs_comp) + assert np.allclose(jacs, expected_value) diff --git a/tests/lightning_tensor/test_lightning_tensor.py b/tests/lightning_tensor/test_lightning_tensor.py index 44b1ee9229..ff6b6aedba 100644 --- a/tests/lightning_tensor/test_lightning_tensor.py +++ b/tests/lightning_tensor/test_lightning_tensor.py @@ -30,26 +30,6 @@ pytest.skip("Device doesn't have C++ support yet.", allow_module_level=True) -@pytest.mark.parametrize("num_wires", [3, 4, 5]) -@pytest.mark.parametrize("c_dtype", [np.complex64, np.complex128]) -def test_device_name_and_init(num_wires, c_dtype): - """Test the class initialization and returned properties.""" - wires = Wires(range(num_wires)) if num_wires else None - dev = LightningTensor(wires=wires, max_bond_dim=10, c_dtype=c_dtype) - assert dev.name == "lightning.tensor" - assert dev.c_dtype == c_dtype - assert dev.wires == wires - assert dev.num_wires == num_wires - - -def test_device_available_as_plugin(): - """Test that the device can be instantiated using ``qml.device``.""" - dev = qml.device("lightning.tensor", wires=2) - assert isinstance(dev, LightningTensor) - assert dev.backend == "cutensornet" - assert dev.method in ["mps"] - - @pytest.mark.parametrize("backend", ["fake_backend"]) def test_invalid_backend(backend): """Test an invalid backend.""" @@ -57,12 +37,6 @@ def test_invalid_backend(backend): LightningTensor(wires=1, backend=backend) -def test_invalid_arg(): - """Test that an error is raised if an invalid argument is provided.""" - with pytest.raises(TypeError): - LightningTensor(wires=2, kwargs="invalid_arg") - - @pytest.mark.parametrize("method", ["fake_method"]) def test_invalid_method(method): """Test an invalid method.""" @@ -70,71 +44,97 @@ def test_invalid_method(method): LightningTensor(method=method) -def test_invalid_bonddims(): - """Test that an error is raised if bond dimensions are less than 1.""" - with pytest.raises(ValueError): - LightningTensor(wires=5, max_bond_dim=0) - - -def test_invalid_wires_none(): - """Test that an error is raised if wires are none.""" - with pytest.raises(ValueError): - LightningTensor(wires=None) - - -def test_invalid_cutoff_mode(): - """Test that an error is raised if an invalid cutoff mode is provided.""" - with pytest.raises(ValueError): - LightningTensor(wires=2, cutoff_mode="invalid_mode") - - -def test_support_derivatives(): - """Test that the device does not support derivatives yet.""" - dev = LightningTensor(wires=2) - assert not dev.supports_derivatives() - - -def test_compute_derivatives(): - """Test that an error is raised if the `compute_derivatives` method is called.""" - dev = LightningTensor(wires=2) - with pytest.raises( - NotImplementedError, - match="The computation of derivatives has yet to be implemented for the lightning.tensor device.", - ): - dev.compute_derivatives(circuits=None) - - -def test_execute_and_compute_derivatives(): - """Test that an error is raised if `execute_and_compute_derivative` method is called.""" - dev = LightningTensor(wires=2) - with pytest.raises( - NotImplementedError, - match="The computation of derivatives has yet to be implemented for the lightning.tensor device.", - ): - dev.execute_and_compute_derivatives(circuits=None) - - -def test_supports_vjp(): - """Test that the device does not support VJP yet.""" - dev = LightningTensor(wires=2) - assert not dev.supports_vjp() - - -def test_compute_vjp(): - """Test that an error is raised if `compute_vjp` method is called.""" - dev = LightningTensor(wires=2) - with pytest.raises( - NotImplementedError, - match="The computation of vector-Jacobian product has yet to be implemented for the lightning.tensor device.", - ): - dev.compute_vjp(circuits=None, cotangents=None) - - -def test_execute_and_compute_vjp(): - """Test that an error is raised if `execute_and_compute_vjp` method is called.""" - dev = LightningTensor(wires=2) - with pytest.raises( - NotImplementedError, - match="The computation of vector-Jacobian product has yet to be implemented for the lightning.tensor device.", - ): - dev.execute_and_compute_vjp(circuits=None, cotangents=None) +@pytest.mark.parametrize("method", [{"method": "mps", "max_bond_dim": 128}, {"method": "tn"}]) +class TestTensorNet: + + @pytest.mark.parametrize("num_wires", [3, 4, 5]) + @pytest.mark.parametrize("c_dtype", [np.complex64, np.complex128]) + def test_device_name_and_init(self, num_wires, c_dtype, method): + """Test the class initialization and returned properties.""" + wires = Wires(range(num_wires)) if num_wires else None + dev = LightningTensor(wires=wires, c_dtype=c_dtype, **method) + + assert dev.name == "lightning.tensor" + assert dev.c_dtype == c_dtype + assert dev.wires == wires + assert dev.num_wires == num_wires + + def test_device_available_as_plugin(self, method): + """Test that the device can be instantiated using ``qml.device``.""" + dev = qml.device("lightning.tensor", wires=2, **method) + assert isinstance(dev, LightningTensor) + assert dev.backend == "cutensornet" + assert dev.method in ["mps", "tn"] + + def test_invalid_arg(self, method): + """Test that an error is raised if an invalid argument is provided.""" + with pytest.raises(TypeError): + LightningTensor(wires=2, kwargs="invalid_arg", **method) + + def test_invalid_bonddims_mps(self, method): + """Test that an error is raised if bond dimensions are less than 1 in mps method.""" + if method["method"] == "mps": + with pytest.raises(ValueError): + LightningTensor(wires=5, max_bond_dim=0, method="mps") + + def test_invalid_bonddims_tn(self, method): + """Test that an error is raised if bond dimensions are passing as arg in tn method.""" + if method["method"] == "tn": + with pytest.raises(TypeError): + LightningTensor(wires=5, max_bond_dim=10, method="tn") + + def test_invalid_wires_none(self, method): + """Test that an error is raised if wires are none.""" + with pytest.raises(ValueError): + LightningTensor(wires=None, **method) + + def test_invalid_cutoff_mode(self, method): + """Test that an error is raised if an invalid cutoff mode is provided.""" + with pytest.raises(ValueError): + LightningTensor(wires=2, cutoff_mode="invalid_mode", **method) + + def test_support_derivatives(self, method): + """Test that the device does not support derivatives yet.""" + dev = LightningTensor(wires=2, **method) + assert not dev.supports_derivatives() + + def test_compute_derivatives(self, method): + """Test that an error is raised if the `compute_derivatives` method is called.""" + dev = LightningTensor(wires=2, **method) + with pytest.raises( + NotImplementedError, + match="The computation of derivatives has yet to be implemented for the lightning.tensor device.", + ): + dev.compute_derivatives(circuits=None) + + def test_execute_and_compute_derivatives(self, method): + """Test that an error is raised if `execute_and_compute_derivative` method is called.""" + dev = LightningTensor(wires=2, **method) + with pytest.raises( + NotImplementedError, + match="The computation of derivatives has yet to be implemented for the lightning.tensor device.", + ): + dev.execute_and_compute_derivatives(circuits=None) + + def test_supports_vjp(self, method): + """Test that the device does not support VJP yet.""" + dev = LightningTensor(wires=2, **method) + assert not dev.supports_vjp() + + def test_compute_vjp(self, method): + """Test that an error is raised if `compute_vjp` method is called.""" + dev = LightningTensor(wires=2, **method) + with pytest.raises( + NotImplementedError, + match="The computation of vector-Jacobian product has yet to be implemented for the lightning.tensor device.", + ): + dev.compute_vjp(circuits=None, cotangents=None) + + def test_execute_and_compute_vjp(self, method): + """Test that an error is raised if `execute_and_compute_vjp` method is called.""" + dev = LightningTensor(wires=2, **method) + with pytest.raises( + NotImplementedError, + match="The computation of vector-Jacobian product has yet to be implemented for the lightning.tensor device.", + ): + dev.execute_and_compute_vjp(circuits=None, cotangents=None) diff --git a/tests/lightning_tensor/test_measurements_class.py b/tests/lightning_tensor/test_measurements_class.py index 1b0e9736d8..8cd48f23b3 100644 --- a/tests/lightning_tensor/test_measurements_class.py +++ b/tests/lightning_tensor/test_measurements_class.py @@ -14,16 +14,10 @@ """ Unit tests for measurements class. """ -from typing import Sequence - import numpy as np import pennylane as qml import pytest from conftest import LightningDevice, device_name # tested device -from flaky import flaky -from pennylane.devices import DefaultQubit -from pennylane.measurements import VarianceMP -from scipy.sparse import csr_matrix, random_array if device_name != "lightning.tensor": pytest.skip( @@ -39,16 +33,26 @@ THETA = np.linspace(0.11, 1, 3) PHI = np.linspace(0.32, 1, 3) +device_args = [] +for method in ["mps", "tn"]: + for c_dtype in [np.complex64, np.complex128]: + device_arg = {} + device_arg["method"] = method + device_arg["c_dtype"] = c_dtype + if method == "mps": + device_arg["max_bond_dim"] = 128 + device_args.append(device_arg) + # General LightningTensorNet fixture, for any number of wires. @pytest.fixture( - params=[np.complex64, np.complex128], + params=device_args, ) def lightning_tn(request): """Fixture for creating a LightningTensorNet object.""" def _lightning_tn(n_wires): - return LightningTensorNet(num_wires=n_wires, max_bond_dim=128, c_dtype=request.param) + return LightningTensorNet(num_wires=n_wires, **request.param) return _lightning_tn @@ -73,10 +77,10 @@ def test_not_implemented_state_measurements(self, lightning_tn): with pytest.raises(NotImplementedError): m.get_measurement_function(mp) - def test_not_supported_sparseH_shot_measurements(self): + def test_not_supported_sparseH_shot_measurements(self, lightning_tn): """Test than a TypeError is raised if the measurement is not supported.""" - tensornetwork = LightningTensorNet(num_wires=3, max_bond_dim=128) + tensornetwork = lightning_tn(3) m = LightningTensorMeasurements(tensornetwork) @@ -93,10 +97,10 @@ def test_not_supported_sparseH_shot_measurements(self): with pytest.raises(TypeError): m.measure_tensor_network(tape) - def test_not_supported_ham_sum_shot_measurements(self): - """Test than a TypeError is raised if the measurement is not supported.""" + def test_not_supported_ham_sum_shot_measurements(self, lightning_tn): + """Test to see if an exception is raised when the measurement is not supported.""" - tensornetwork = LightningTensorNet(num_wires=3, max_bond_dim=128) + tensornetwork = lightning_tn(3) m = LightningTensorMeasurements(tensornetwork) @@ -112,10 +116,10 @@ def test_not_supported_ham_sum_shot_measurements(self): with pytest.raises(TypeError): m.measure_tensor_network(tape) - def test_not_supported_shadowmp_shot_measurements(self): + def test_not_supported_shadowmp_shot_measurements(self, lightning_tn): """Test than a TypeError is raised if the measurement is not supported.""" - tensornetwork = LightningTensorNet(num_wires=3, max_bond_dim=128) + tensornetwork = lightning_tn(3) m = LightningTensorMeasurements(tensornetwork) @@ -127,15 +131,16 @@ def test_not_supported_shadowmp_shot_measurements(self): with pytest.raises(TypeError): m.measure_tensor_network(tape) + @pytest.mark.parametrize("method", [{"method": "mps", "max_bond_dim": 128}, {"method": "tn"}]) @pytest.mark.parametrize("n_qubits", range(4, 14, 2)) @pytest.mark.parametrize("n_targets", list(range(1, 4)) + list(range(4, 14, 2))) - def test_probs_many_wires(self, n_qubits, n_targets, tol): + def test_probs_many_wires(self, method, n_qubits, n_targets, tol): """Test probs measuring many wires of a random quantum state.""" if n_targets >= n_qubits: pytest.skip("Number of targets cannot exceed the number of wires.") - dev = qml.device(device_name, wires=n_qubits) - dq = qml.device("default.qubit", wires=n_qubits) + dev = qml.device(device_name, wires=n_qubits, **method) + dq = qml.device("lightning.qubit", wires=n_qubits) init_state = np.random.rand(2**n_qubits) + 1.0j * np.random.rand(2**n_qubits) init_state /= np.linalg.norm(init_state) @@ -145,7 +150,38 @@ def test_probs_many_wires(self, n_qubits, n_targets, tol): mp = qml.probs(wires=range(n_targets)) tape = qml.tape.QuantumScript(ops, [mp]) - res = dev.execute(tape) ref = dq.execute(tape) - assert np.allclose(res, ref, atol=tol, rtol=0) + if method["method"] == "tn": + with pytest.raises(qml.DeviceError): + res = dev.execute(tape) + else: + res = dev.execute(tape) + assert np.allclose(res, ref, atol=tol, rtol=0) + + @pytest.mark.parametrize("method", [{"method": "mps", "max_bond_dim": 128}, {"method": "tn"}]) + @pytest.mark.parametrize("n_qubits", range(4, 14, 2)) + @pytest.mark.parametrize("n_targets", list(range(1, 4)) + list(range(4, 14, 2))) + def test_state_many_wires(self, method, n_qubits, n_targets, tol): + """Test probs measuring many wires of a random quantum state.""" + if n_targets >= n_qubits: + pytest.skip("Number of targets cannot exceed the number of wires.") + + dev = qml.device(device_name, wires=n_qubits, **method) + dq = qml.device("lightning.qubit", wires=n_qubits) + + init_state = np.random.rand(2**n_qubits) + 1.0j * np.random.rand(2**n_qubits) + init_state /= np.linalg.norm(init_state) + + ops = [qml.StatePrep(init_state, wires=range(n_qubits))] + + mp = qml.state() + + tape = qml.tape.QuantumScript(ops, [mp]) + ref = dq.execute(tape) + if method["method"] == "tn": + with pytest.raises(qml.DeviceError): + res = dev.execute(tape) + else: + res = dev.execute(tape) + assert np.allclose(res, ref, atol=tol, rtol=0) diff --git a/tests/lightning_tensor/test_serialize_chunk_obs_tensor.py b/tests/lightning_tensor/test_serialize_chunk_obs_tensor.py new file mode 100644 index 0000000000..82adf545fd --- /dev/null +++ b/tests/lightning_tensor/test_serialize_chunk_obs_tensor.py @@ -0,0 +1,56 @@ +# Copyright 2018-2023 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Unit tests for the serialization helper functions. +""" +import pennylane as qml +import pytest +from conftest import LightningDevice as ld +from conftest import device_name + +from pennylane_lightning.core._serialize import QuantumScriptSerializer + +if device_name != "lightning.tensor": + pytest.skip("Skipping tests for the LightningTensor class.", allow_module_level=True) + + +if not ld._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) + + +@pytest.mark.parametrize("method", ["mps", "tn"]) +class TestSerializeObs: + """Tests for the _serialize_observables function""" + + wires_dict = {i: i for i in range(10)} + + @pytest.mark.parametrize("use_csingle", [True, False]) + @pytest.mark.parametrize("obs_chunk, expected", [(1, 5), (2, 6), (3, 7), (7, 7)]) + def test_chunk_obs(self, method, use_csingle, obs_chunk, expected): + """Test chunking of observable array""" + with qml.tape.QuantumTape() as tape: + qml.expval( + 0.5 * qml.PauliX(0) @ qml.PauliZ(1) + + 0.7 * qml.PauliZ(0) @ qml.PauliX(1) + + 1.2 * qml.PauliY(0) @ qml.PauliY(1) + ) + qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) + qml.expval(qml.PauliY(wires=1)) + qml.expval(qml.PauliX(0) @ qml.Hermitian([[0, 1], [1, 0]], wires=3) @ qml.Hadamard(2)) + qml.expval(qml.Hermitian(qml.PauliZ.compute_matrix(), wires=0) @ qml.Identity(1)) + s, obs_idx = QuantumScriptSerializer( + device_name, use_csingle, split_obs=obs_chunk, tensor_backend=method + ).serialize_observables(tape, self.wires_dict) + assert expected == len(s) + assert [0] * (expected - 4) + [1, 2, 3, 4] == obs_idx diff --git a/tests/lightning_tensor/test_serialize_tensor.py b/tests/lightning_tensor/test_serialize_tensor.py new file mode 100644 index 0000000000..aab257e38d --- /dev/null +++ b/tests/lightning_tensor/test_serialize_tensor.py @@ -0,0 +1,843 @@ +# Copyright 2018-2024 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Unit tests for the serialization helper functions. +""" +import numpy as np +import pennylane as qml +import pytest +from conftest import LightningDevice, device_name + +from pennylane_lightning.core._serialize import QuantumScriptSerializer, global_phase_diagonal + +if not LightningDevice._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) + +if device_name != "lightning.tensor": + pytest.skip("Skipping tests for the LightningTensor class.", allow_module_level=True) + +if device_name == "lightning.tensor": + from pennylane_lightning.lightning_tensor_ops import ( + exactTensorNetC64, + exactTensorNetC128, + mpsTensorNetC64, + mpsTensorNetC128, + ) + from pennylane_lightning.lightning_tensor_ops.observables import ( + exactHamiltonianC64, + exactHamiltonianC128, + exactHermitianObsC64, + exactHermitianObsC128, + exactNamedObsC64, + exactNamedObsC128, + exactTensorProdObsC64, + exactTensorProdObsC128, + mpsHamiltonianC64, + mpsHamiltonianC128, + mpsHermitianObsC64, + mpsHermitianObsC128, + mpsNamedObsC64, + mpsNamedObsC128, + mpsTensorProdObsC64, + mpsTensorProdObsC128, + ) + + +@pytest.fixture(params=[["mps", "mps"], ["exact", "tn"]]) +def tn_backend_names(request): + return request.param + + +def get_module_name(tn_back, name, dtype): + mod_name = tn_back + name + mod_name = mod_name + "64" if dtype else mod_name + "128" + return globals().get(mod_name) + + +def test_wrong_device_name(): + """Test the device name is not a valid option""" + + with pytest.raises(qml.DeviceError, match="The device name"): + QuantumScriptSerializer("thunder.qubit") + + +@pytest.mark.parametrize("dtype", ["64", "128"]) +@pytest.mark.parametrize( + "obs,obs_type", + [ + (qml.PauliZ(0), "NamedObsC"), + (qml.PauliZ(0) @ qml.PauliZ(1), "TensorProdObsC"), + (qml.Hadamard(0), "NamedObsC"), + (qml.Hermitian(np.eye(2), wires=0), "HermitianObsC"), + ((qml.PauliZ(0) @ qml.Hadamard(1) @ qml.PauliZ(2) @ qml.PauliX(3)), "TensorProdObsC"), + ( + qml.PauliZ(0) @ qml.PauliY(1) @ qml.PauliX(2), + "TensorProdObsC", + ), + ( + qml.PauliZ(0) @ qml.PauliY(1) @ (0.1 * (qml.PauliZ(2) + qml.PauliX(3))), + "HamiltonianC", + ), + ( + ( + qml.Hermitian(np.eye(2), wires=0) + @ qml.Hermitian(np.eye(2), wires=1) + @ qml.Projector([0], wires=2) + ), + "TensorProdObsC", + ), + ( + (qml.Hermitian(np.eye(2), wires=0)), + "HermitianObsC", + ), + ( + qml.PauliZ(0) @ qml.Hermitian(np.eye(2), wires=1) @ qml.Projector([0], wires=2), + "TensorProdObsC", + ), + (qml.Projector([0], wires=0), "HermitianObsC"), + (qml.Hamiltonian([1], [qml.PauliZ(0)]), "NamedObsC"), + (qml.sum(qml.Hadamard(0), qml.PauliX(1)), "HamiltonianC"), + ( + (0.5 * qml.PauliX(0)), + "HamiltonianC", + ), + (2.5 * qml.PauliZ(0), "HamiltonianC"), + ], +) +def test_obs_returns_expected_type(tn_backend_names, dtype, obs, obs_type): + """Tests that observables get serialized to the expected type, with and without wires map""" + + mod_name = tn_backend_names[0] + obs_type + dtype + obs_type_mod = globals().get(mod_name) + + serializer = QuantumScriptSerializer( + device_name, True if dtype == "64" else False, tensor_backend=tn_backend_names[1] + ) + assert isinstance(serializer._ob(obs, dict(enumerate(obs.wires))), obs_type_mod) + assert isinstance(serializer._ob(obs), obs_type_mod) + + +wires_dict = {i: i for i in range(10)} + + +@pytest.mark.parametrize("use_csingle", [True, False]) +@pytest.mark.parametrize("wires_map", [wires_dict, None]) +class TestSerializeObs: + """Tests for the _observables function""" + + @pytest.fixture(autouse=True) + def set_tn_backend(self, tn_backend_names): + self.tn_backend = tn_backend_names[0] + self.tn_method = tn_backend_names[1] + + def test_tensor_non_tensor_return(self, use_csingle, wires_map): + """Test expected serialization for a mixture of tensor product and non-tensor product + return""" + with qml.tape.QuantumTape() as tape: + qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) + qml.expval(qml.Hadamard(1)) + + named_obs = get_module_name(self.tn_backend, "NamedObsC", use_csingle) + tensor_prod_obs = get_module_name(self.tn_backend, "TensorProdObsC", use_csingle) + + s_expected = [ + tensor_prod_obs([named_obs("PauliZ", [0]), named_obs("PauliX", [1])]), + named_obs("Hadamard", [1]), + ] + + s, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + assert s == s_expected + + def test_prod_return_with_overlapping_wires(self, use_csingle, wires_map): + """Test the expected serialization for a Prod return with operands with overlapping wires.""" + obs = qml.prod( + qml.sum(qml.X(0), qml.s_prod(2, qml.Hadamard(0))), + qml.sum(qml.s_prod(3, qml.Z(1)), qml.Z(2), qml.Hermitian(np.eye(2), wires=0)), + ) + tape = qml.tape.QuantumScript([], [qml.expval(obs)]) + + hermitian_obs = get_module_name(self.tn_backend, "HermitianObsC", use_csingle) + + c_dtype = np.complex64 if use_csingle else np.complex128 + mat = obs.matrix().ravel().astype(c_dtype) + with pytest.raises( + ValueError, match="The number of Hermitian observables target wires should be 1." + ): + + s, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + + def test_hermitian_return(self, use_csingle, wires_map): + """Test expected serialization for a Hermitian return""" + with qml.tape.QuantumTape() as tape: + qml.expval(qml.Hermitian(np.eye(4), wires=[0, 1])) + + hermitian_obs = get_module_name(self.tn_backend, "HermitianObsC", use_csingle) + + with pytest.raises( + ValueError, match="The number of Hermitian observables target wires should be 1." + ): + + s, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + + def test_hermitian_tensor_return(self, use_csingle, wires_map): + """Test expected serialization for a Hermitian return""" + with qml.tape.QuantumTape() as tape: + qml.expval( + qml.Hermitian( + np.eye(2), + wires=[1], + ) + @ qml.Hermitian(np.eye(2), wires=[2]) + ) + + c_dtype = np.complex64 if use_csingle else np.complex128 + tensor_prod_obs = get_module_name(self.tn_backend, "TensorProdObsC", use_csingle) + hermitian_obs = get_module_name(self.tn_backend, "HermitianObsC", use_csingle) + + s, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + + s_expected = tensor_prod_obs( + [ + hermitian_obs(np.eye(2, dtype=c_dtype).ravel(), [1]), + hermitian_obs(np.eye(2, dtype=c_dtype).ravel(), [2]), + ] + ) + + assert s[0] == s_expected + + def test_mixed_tensor_return(self, use_csingle, wires_map): + """Test expected serialization for a mixture of Hermitian and Pauli return""" + with qml.tape.QuantumTape() as tape: + qml.expval( + qml.Hermitian( + np.eye(2 if device_name == "lightning.tensor" else 4), + wires=[0] if device_name == "lightning.tensor" else [0, 1], + ) + @ qml.PauliY(2) + ) + + c_dtype = np.complex64 if use_csingle else np.complex128 + tensor_prod_obs = get_module_name(self.tn_backend, "TensorProdObsC", use_csingle) + hermitian_obs = get_module_name(self.tn_backend, "HermitianObsC", use_csingle) + named_obs = get_module_name(self.tn_backend, "NamedObsC", use_csingle) + + s, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + + s_expected = tensor_prod_obs( + [ + hermitian_obs( + np.eye(2, dtype=c_dtype).ravel(), + [0], + ), + named_obs("PauliY", [2]), + ] + ) + + assert s[0] == s_expected + + @pytest.mark.parametrize( + "test_hermobs0", + [(qml.Hermitian(np.eye(2), wires=[0]))], + ) + @pytest.mark.parametrize( + "test_hermobs1", + [(qml.Hermitian(np.ones((2, 2)), wires=[0]))], + ) + def test_hamiltonian_return(self, test_hermobs0, test_hermobs1, use_csingle, wires_map): + """Test expected serialization for a Hamiltonian return""" + + ham = qml.Hamiltonian( + [0.3, 0.5, 0.4], + [ + (test_hermobs0 @ qml.PauliY(2)), + qml.PauliX(0) @ qml.PauliY(2), + (test_hermobs1), + ], + ) + + with qml.tape.QuantumTape() as tape: + qml.expval(ham) + + hamiltonian_obs = get_module_name(self.tn_backend, "HamiltonianC", use_csingle) + tensor_prod_obs = get_module_name(self.tn_backend, "TensorProdObsC", use_csingle) + hermitian_obs = get_module_name(self.tn_backend, "HermitianObsC", use_csingle) + named_obs = get_module_name(self.tn_backend, "NamedObsC", use_csingle) + + r_dtype = np.float32 if use_csingle else np.float64 + c_dtype = np.complex64 if use_csingle else np.complex128 + + s, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + + s_expected = hamiltonian_obs( + np.array([0.3, 0.5, 0.4], dtype=r_dtype), + [ + tensor_prod_obs( + [ + (hermitian_obs(np.eye(2, dtype=c_dtype).ravel(), [0])), + named_obs("PauliY", [2]), + ] + ), + tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]), + (hermitian_obs(np.ones(4, dtype=c_dtype), [0])), + ], + ) + + assert s[0] == s_expected + + def test_hamiltonian_tensor_return(self, use_csingle, wires_map): + """Test expected serialization for a tensor Hamiltonian return""" + + with qml.tape.QuantumTape() as tape: + ham = qml.Hamiltonian( + [0.3, 0.5, 0.4], + [ + (qml.Hermitian(np.eye(2), wires=[0]) @ qml.PauliY(2)), + qml.PauliX(0) @ qml.PauliY(2), + (qml.Hermitian(np.ones((2, 2)), wires=[0])), + ], + ) + qml.expval(ham @ qml.PauliZ(3)) + + hamiltonian_obs = get_module_name(self.tn_backend, "HamiltonianC", use_csingle) + tensor_prod_obs = get_module_name(self.tn_backend, "TensorProdObsC", use_csingle) + hermitian_obs = get_module_name(self.tn_backend, "HermitianObsC", use_csingle) + named_obs = get_module_name(self.tn_backend, "NamedObsC", use_csingle) + + r_dtype = np.float32 if use_csingle else np.float64 + c_dtype = np.complex64 if use_csingle else np.complex128 + + s, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + + # Expression (ham @ obs) is converted internally by Pennylane + # where obs is appended to each term of the ham + + s_expected = hamiltonian_obs( + np.array([0.3, 0.5, 0.4], dtype=r_dtype), + [ + tensor_prod_obs( + [ + (hermitian_obs(np.eye(2, dtype=c_dtype).ravel(), [0])), + named_obs("PauliY", [2]), + named_obs("PauliZ", [3]), + ] + ), + tensor_prod_obs( + [ + named_obs("PauliX", [0]), + named_obs("PauliY", [2]), + named_obs("PauliZ", [3]), + ] + ), + tensor_prod_obs( + [ + (hermitian_obs(np.ones(4, dtype=c_dtype), [0])), + named_obs("PauliZ", [3]), + ] + ), + ], + ) + + assert s[0] == s_expected + + def test_hamiltonian_mix_return(self, use_csingle, wires_map): + """Test expected serialization for a Hamiltonian return""" + + ham1 = qml.Hamiltonian( + [0.3, 0.5, 0.4], + [ + ((qml.Hermitian(np.eye(2), wires=[0])) @ qml.PauliY(2)), + qml.PauliX(0) @ qml.PauliY(2), + ((qml.Hermitian(np.ones((2, 2)), wires=[0]))), + ], + ) + ham2 = qml.Hamiltonian( + [0.7, 0.3], + [ + (qml.PauliX(0) @ qml.Hermitian(np.eye(2), wires=[1])), + qml.PauliY(0) @ qml.PauliX(2), + ], + ) + + with qml.tape.QuantumTape() as tape: + qml.expval(ham1) + qml.expval(ham2) + + hamiltonian_obs = get_module_name(self.tn_backend, "HamiltonianC", use_csingle) + tensor_prod_obs = get_module_name(self.tn_backend, "TensorProdObsC", use_csingle) + hermitian_obs = get_module_name(self.tn_backend, "HermitianObsC", use_csingle) + named_obs = get_module_name(self.tn_backend, "NamedObsC", use_csingle) + + r_dtype = np.float32 if use_csingle else np.float64 + c_dtype = np.complex64 if use_csingle else np.complex128 + + s, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + s_expected1 = hamiltonian_obs( + np.array([0.3, 0.5, 0.4], dtype=r_dtype), + [ + tensor_prod_obs( + [ + (hermitian_obs(np.eye(2, dtype=c_dtype).ravel(), [0])), + named_obs("PauliY", [2]), + ] + ), + tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]), + (hermitian_obs(np.ones(4, dtype=c_dtype), [0])), + ], + ) + s_expected2 = hamiltonian_obs( + np.array([0.7, 0.3], dtype=r_dtype), + [ + tensor_prod_obs( + [ + named_obs("PauliX", [0]), + (hermitian_obs(np.eye(2, dtype=c_dtype).ravel(), [1])), + ] + ), + tensor_prod_obs([named_obs("PauliY", [0]), named_obs("PauliX", [2])]), + ], + ) + + assert s[0] == s_expected1 + assert s[1] == s_expected2 + + def test_pauli_rep_return(self, use_csingle, wires_map): + """Test that an observable with a valid pauli rep is serialized correctly.""" + with qml.tape.QuantumTape() as tape: + qml.expval(qml.PauliX(0) + qml.PauliZ(0)) + + hamiltonian_obs = get_module_name(self.tn_backend, "HamiltonianC", use_csingle) + named_obs = get_module_name(self.tn_backend, "NamedObsC", use_csingle) + + r_dtype = np.float32 if use_csingle else np.float64 + + s, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + s_expected = hamiltonian_obs( + np.array([1, 1], dtype=r_dtype), [named_obs("PauliX", [0]), named_obs("PauliZ", [0])] + ) + assert s[0] == s_expected + + def test_pauli_rep_single_term(self, use_csingle, wires_map): + """Test that an observable with a single term in the pauli rep is serialized correctly""" + with qml.tape.QuantumTape() as tape: + qml.expval(qml.PauliX(0) @ qml.PauliZ(1)) + + tensor_prod_obs = get_module_name(self.tn_backend, "TensorProdObsC", use_csingle) + named_obs = get_module_name(self.tn_backend, "NamedObsC", use_csingle) + + s, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + s_expected = tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliZ", [1])]) + assert s[0] == s_expected + + def test_sprod(self, use_csingle, wires_map): + """Test that SProds are serialized correctly""" + tape = qml.tape.QuantumScript([], [qml.expval(qml.s_prod(0.1, qml.Hadamard(0)))]) + + hamiltonian_obs = get_module_name(self.tn_backend, "HamiltonianC", use_csingle) + named_obs = get_module_name(self.tn_backend, "NamedObsC", use_csingle) + rtype = np.float32 if use_csingle else np.float64 + + res, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + assert len(res) == 1 + assert isinstance(res[0], hamiltonian_obs) + + coeffs = np.array([0.1]).astype(rtype) + s_expected = hamiltonian_obs(coeffs, [named_obs("Hadamard", [0])]) + assert res[0] == s_expected + + def test_prod(self, use_csingle, wires_map): + """Test that Prods are serialized correctly""" + tape = qml.tape.QuantumScript( + [], [qml.expval(qml.prod(qml.PauliZ(0), qml.PauliX(1)) @ qml.Hadamard(2))] + ) + + tensor_prod_obs = get_module_name(self.tn_backend, "TensorProdObsC", use_csingle) + named_obs = get_module_name(self.tn_backend, "NamedObsC", use_csingle) + + res, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + assert len(res) == 1 + assert isinstance(res[0], tensor_prod_obs) + + s_expected = tensor_prod_obs( + [named_obs("PauliZ", [0]), named_obs("PauliX", [1]), named_obs("Hadamard", [2])] + ) + assert res[0] == s_expected + + def test_sum(self, use_csingle, wires_map): + """Test that Sums are serialized correctly""" + tape = qml.tape.QuantumScript( + [], + [ + qml.expval( + qml.sum( + 0.5 * qml.prod(qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)), + 0.1 * qml.prod(qml.PauliZ(0), qml.Hadamard(2), qml.PauliY(1)), + ) + ) + ], + ) + + hamiltonian_obs = get_module_name(self.tn_backend, "HamiltonianC", use_csingle) + tensor_prod_obs = get_module_name(self.tn_backend, "TensorProdObsC", use_csingle) + named_obs = get_module_name(self.tn_backend, "NamedObsC", use_csingle) + + rtype = np.float32 if use_csingle else np.float64 + + res, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + assert len(res) == 1 + assert isinstance(res[0], hamiltonian_obs) + + coeffs = np.array([0.5, 0.1]).astype(rtype) + s_expected = hamiltonian_obs( + coeffs, + [ + tensor_prod_obs( + [named_obs("PauliX", [0]), named_obs("PauliZ", [1]), named_obs("PauliX", [2])] + ), + tensor_prod_obs( + [named_obs("PauliZ", [0]), named_obs("PauliY", [1]), named_obs("Hadamard", [2])] + ), + ], + ) + assert res[0] == s_expected + + def test_multi_wire_identity(self, use_csingle, wires_map): + """Tests that multi-wire Identity does not fail serialization.""" + tape = qml.tape.QuantumTape(measurements=[qml.expval(qml.Identity(wires=[1, 2]))]) + + res, _ = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_observables(tape, wires_map) + assert len(res) == 1 + + named_obs = get_module_name(self.tn_backend, "NamedObsC", use_csingle) + assert res[0] == named_obs("Identity", [1]) + + +@pytest.mark.parametrize("wires_map", [wires_dict, None]) +@pytest.mark.parametrize("use_csingle", [True, False]) +class TestSerializeOps: + """Tests for the _ops function""" + + wires_dict = {i: i for i in range(10)} + + @pytest.fixture(autouse=True) + def set_tn_backend(self, tn_backend_names): + self.tn_backend = tn_backend_names[0] + self.tn_method = tn_backend_names[1] + + def test_basic_circuit(self, use_csingle, wires_map): + """Test expected serialization for a simple circuit""" + with qml.tape.QuantumTape() as tape: + qml.RX(0.4, wires=0) + qml.RY(0.6, wires=1) + qml.CNOT(wires=[0, 1]) + + s = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_ops(tape, wires_map) + s_expected = ( + ( + ["RX", "RY", "CNOT"], + [np.array([0.4]), np.array([0.6]), []], + [[0], [1], [0, 1]], + [False, False, False], + [[], [], []], + [[], [], []], + [[], [], []], + ), + False, + ) + assert s == s_expected + + def test_Rot_in_circuit(self, use_csingle, wires_map): + """Test expected serialization for a circuit with Rot which should be decomposed""" + + with qml.queuing.AnnotatedQueue() as q: + qml.Rot(0.1, 0.2, 0.3, wires=0) + + tape = qml.tape.QuantumScript.from_queue(q) + + s = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_ops(tape, wires_map) + s_expected = ( + ( + ["RZ", "RY", "RZ"], + [np.array([0.1]), np.array([0.2]), np.array([0.3])], + [[0], [0], [0]], + [False, False, False], + [[], [], []], + [[], [], []], + [[], [], []], + ), + False, + ) + assert s == s_expected + + def test_basic_circuit_not_implemented_ctrl_ops(self, use_csingle, wires_map): + """Test expected serialization for a simple circuit""" + ops = qml.OrbitalRotation(0.1234, wires=range(4)) + with qml.tape.QuantumTape() as tape: + qml.RX(0.4, wires=0) + qml.RY(0.6, wires=1) + qml.ctrl(ops, [4, 5]) + + s = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_ops(tape, wires_map) + s_expected = ( + ( + ["RX", "RY", "QubitUnitary"], + [np.array([0.4]), np.array([0.6]), [0.0]], + [[0], [1], list(ops.wires)], + [False, False, False], + [[], [], [qml.matrix(ops)]], + [[], [], [4, 5]], + ), + False, + ) + assert s[0][0] == s_expected[0][0] + assert s[0][1] == s_expected[0][1] + assert s[0][2] == s_expected[0][2] + assert s[0][3] == s_expected[0][3] + assert all(np.allclose(s0, s1) for s0, s1 in zip(s[0][4], s_expected[0][4])) + assert s[0][5] == s_expected[0][5] + assert s[1] == s_expected[1] + + def test_multicontrolledx(self, use_csingle, wires_map): + """Test expected serialization for a simple circuit""" + with qml.tape.QuantumTape() as tape: + qml.RX(0.4, wires=0) + qml.RY(0.6, wires=1) + qml.ctrl(qml.PauliX(wires=0), [1, 2, 3], control_values=[True, False, False]) + + s = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_ops(tape, wires_map) + s_expected = ( + ( + ["RX", "RY", "PauliX"], + [np.array([0.4]), np.array([0.6]), []], + [[0], [1], [0]], + [False, False, False], + [[], [], []], + [[], [], [1, 2, 3]], + [[], [], [True, False, False]], + ), + False, + ) + assert s == s_expected + + def test_skips_prep_circuit(self, use_csingle, wires_map): + """Test expected serialization for a simple circuit with state preparation, such that + the state preparation is skipped""" + with qml.tape.QuantumTape() as tape: + qml.StatePrep([1, 0], wires=0) + qml.BasisState([1], wires=1) + qml.RX(0.4, wires=0) + qml.RY(0.6, wires=1) + qml.CNOT(wires=[0, 1]) + + s = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_ops(tape, wires_map) + s_expected = ( + ( + ["RX", "RY", "CNOT"], + [[0.4], [0.6], []], + [[0], [1], [0, 1]], + [False, False, False], + [[], [], []], + [[], [], []], + [[], [], []], + ), + True, + ) + assert s == s_expected + + def test_unsupported_kernel_circuit(self, use_csingle, wires_map): + """Test expected serialization for a circuit including gates that do not have a dedicated + kernel""" + with qml.tape.QuantumTape() as tape: + qml.CNOT(wires=[0, 1]) + qml.RZ(0.2, wires=2) + + s = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_ops(tape, wires_map) + s_expected = ( + ( + ["CNOT", "RZ"], + [[], [0.2]], + [[0, 1], [2]], + [False, False], + ), + False, + ) + assert s[0][0] == s_expected[0][0] + assert s[0][1] == s_expected[0][1] + + def test_custom_wires_circuit(self, use_csingle, wires_map): + """Test expected serialization for a simple circuit with custom wire labels""" + wires_dict = {"a": 0, 3.2: 1} + with qml.tape.QuantumTape() as tape: + qml.RX(0.4, wires="a") + qml.RY(0.6, wires=3.2) + qml.CNOT(wires=["a", 3.2]) + qml.SingleExcitation(0.5, wires=["a", 3.2]) + qml.SingleExcitationPlus(0.4, wires=["a", 3.2]) + qml.adjoint(qml.SingleExcitationMinus(0.5, wires=["a", 3.2]), lazy=False) + + s = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_ops(tape, wires_dict) + s_expected = ( + ( + [ + "RX", + "RY", + "CNOT", + "SingleExcitation", + "SingleExcitationPlus", + "SingleExcitationMinus", + ], + [[0.4], [0.6], [], [0.5], [0.4], [-0.5]], + [[0], [1], [0, 1], [0, 1], [0, 1], [0, 1]], + [False, False, False, False, False, False], + [[], [], [], [], [], []], + [[], [], [], [], [], []], + [[], [], [], [], [], []], + ), + False, + ) + assert s == s_expected + + def test_integration(self, use_csingle, wires_map): + """Test expected serialization for a random circuit""" + with qml.tape.QuantumTape() as tape: + qml.RX(0.4, wires=0) + qml.RY(0.6, wires=1) + qml.CNOT(wires=[0, 1]) + qml.QubitUnitary(np.eye(4), wires=[0, 1]) + qml.templates.QFT(wires=[0, 1, 2]) + qml.DoubleExcitation(0.555, wires=[3, 2, 1, 0]) + qml.DoubleExcitationMinus(0.555, wires=[0, 1, 2, 3]) + qml.DoubleExcitationPlus(0.555, wires=[0, 1, 2, 3]) + + s = QuantumScriptSerializer( + device_name, use_csingle, tensor_backend=self.tn_method + ).serialize_ops(tape, wires_map) + + dtype = np.complex64 if use_csingle else np.complex128 + s_expected = ( + ( + [ + "RX", + "RY", + "CNOT", + "QubitUnitary", + "QFT", + "DoubleExcitation", + "DoubleExcitationMinus", + "DoubleExcitationPlus", + ], + [[0.4], [0.6], [], [0.0], [], [0.555], [0.555], [0.555]], + [[0], [1], [0, 1], [0, 1], [0, 1, 2], [3, 2, 1, 0], [0, 1, 2, 3], [0, 1, 2, 3]], + [False, False, False, False, False, False, False, False], + [ + [], + [], + [], + qml.matrix(qml.QubitUnitary(np.eye(4, dtype=dtype), wires=[0, 1])), + qml.matrix(qml.templates.QFT(wires=[0, 1, 2])), + [], + [], + [], + ], + ), + False, + ) + assert s[0][0] == s_expected[0][0] + assert s[0][1] == s_expected[0][1] + assert s[0][2] == s_expected[0][2] + assert s[0][3] == s_expected[0][3] + assert s[1] == s_expected[1] + + assert all(np.allclose(s1, s2) for s1, s2 in zip(s[0][4], s_expected[0][4])) + + +def check_global_phase_diagonal(par, wires, targets, controls, control_values): + op = qml.ctrl(qml.GlobalPhase(par, wires=targets), controls, control_values=control_values) + return np.diag(op.matrix(wires)) + + +def test_global_phase(): + """Validate global_phase_diagonal with various combinations of num_qubits, targets and controls.""" + import itertools + + nmax = 7 + par = 0.1 + for nq in range(2, nmax): + wires = range(nq) + for nw in range(nq, nmax): + wire_lists = list(itertools.permutations(wires, nw)) + for wire_list in wire_lists: + for i in range(len(wire_list) - 1): + targets = wire_list[0:i] + controls = wire_list[i:] + control_values = [i % 2 == 0 for i in controls] + D0 = check_global_phase_diagonal(par, wires, targets, controls, control_values) + D1 = global_phase_diagonal(par, wires, controls, control_values) + assert np.allclose(D0, D1) + + +@pytest.mark.parametrize("use_csingle", [True, False]) +@pytest.mark.parametrize("backend,init", [("mps", (3, 3)), ("exact", (3,))]) +def test_tensornet_dtype(use_csingle, backend, init): + """Tests that the correct TensorNet type is used for the device""" + + tn_method = backend + tn_method = "tn" if tn_method == "exact" else tn_method + + serializer_c = QuantumScriptSerializer(device_name, use_csingle, tensor_backend=tn_method) + + tensor_net = get_module_name(backend, "TensorNetC", use_csingle) + + assert isinstance(serializer_c.sv_type(*init), tensor_net) == True diff --git a/tests/lightning_tensor/test_tensornet_class.py b/tests/lightning_tensor/test_tensornet_class.py index 638af52e2c..62ef974749 100644 --- a/tests/lightning_tensor/test_tensornet_class.py +++ b/tests/lightning_tensor/test_tensornet_class.py @@ -14,15 +14,11 @@ """ Unit tests for the tensornet functions. """ - -import math - import numpy as np import pennylane as qml import pytest import scipy from conftest import LightningDevice, device_name # tested device -from pennylane.wires import Wires if device_name != "lightning.tensor": pytest.skip("Skipping tests for the tensornet class.", allow_module_level=True) @@ -37,36 +33,57 @@ pytest.skip("No binary module found. Skipping.", allow_module_level=True) +@pytest.mark.parametrize("tn_backend", ["mps", "tn"]) @pytest.mark.parametrize("num_wires", range(1, 4)) @pytest.mark.parametrize("bondDims", [1, 2, 3, 4]) @pytest.mark.parametrize("dtype", [np.complex64, np.complex128]) @pytest.mark.parametrize("device_name", ["lightning.tensor"]) -def test_device_name_and_init(num_wires, bondDims, dtype, device_name): +def test_device_name_and_init(num_wires, bondDims, dtype, device_name, tn_backend): """Test the class initialization and returned properties.""" if num_wires < 2: with pytest.raises(ValueError, match="Number of wires must be greater than 1."): - LightningTensorNet(num_wires, bondDims, c_dtype=dtype, device_name=device_name) + LightningTensorNet( + num_wires, + max_bond_dim=bondDims, + c_dtype=dtype, + device_name=device_name, + method=tn_backend, + ) return else: - tensornet = LightningTensorNet(num_wires, bondDims, c_dtype=dtype, device_name=device_name) + tensornet = LightningTensorNet( + num_wires, + max_bond_dim=bondDims, + c_dtype=dtype, + device_name=device_name, + method=tn_backend, + ) assert tensornet.dtype == dtype assert tensornet.device_name == device_name assert tensornet.num_wires == num_wires + assert tensornet._method == tn_backend def test_wrong_device_name(): """Test an invalid device name""" with pytest.raises(qml.DeviceError, match="The device name"): - LightningTensorNet(3, 5, device_name="thunder.tensor") + LightningTensorNet(3, max_bond_dim=5, device_name="thunder.tensor") + + +def test_wrong_method_name(): + """Test an invalid device name""" + with pytest.raises(qml.DeviceError, match="The method "): + LightningTensorNet(3, max_bond_dim=5, device_name="lightning.tensor", method="spider_web") -def test_errors_basis_state(): +@pytest.mark.parametrize("tn_backend", ["mps", "tn"]) +def test_errors_basis_state(tn_backend): """Test that errors are raised when applying a BasisState operation.""" with pytest.raises(ValueError, match="Basis state must only consist of 0s and 1s;"): - tensornet = LightningTensorNet(3, 5) + tensornet = LightningTensorNet(3, max_bond_dim=5, method=tn_backend) tensornet.apply_operations([qml.BasisState(np.array([-0.2, 4.2]), wires=[0, 1])]) with pytest.raises(ValueError, match="State must be of length 1;"): - tensornet = LightningTensorNet(3, 5) + tensornet = LightningTensorNet(3, max_bond_dim=5, method=tn_backend) tensornet.apply_operations([qml.BasisState(np.array([0, 1]), wires=[0])]) diff --git a/tests/test_serialize.py b/tests/test_serialize.py index b6d1929514..183a69647e 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -51,16 +51,9 @@ TensorProdObsC128, ) elif device_name == "lightning.tensor": - from pennylane_lightning.lightning_tensor_ops import TensorNetC64, TensorNetC128 - from pennylane_lightning.lightning_tensor_ops.observables import ( - HamiltonianC64, - HamiltonianC128, - HermitianObsC64, - HermitianObsC128, - NamedObsC64, - NamedObsC128, - TensorProdObsC64, - TensorProdObsC128, + pytest.skip( + "Lightning Tensor serialization is tested separately in tests/lightning_tensor/test_serialize_tensor.py", + allow_module_level=True, ) else: from pennylane_lightning.lightning_qubit_ops.observables import ( @@ -84,28 +77,25 @@ def test_wrong_device_name(): QuantumScriptSerializer("thunder.qubit") +@pytest.mark.parametrize("dtype", ["64", "128"]) @pytest.mark.parametrize( "obs,obs_type", [ - (qml.PauliZ(0), NamedObsC128), - (qml.PauliZ(0) @ qml.PauliZ(1), TensorProdObsC128), - (qml.Hadamard(0), NamedObsC128), - (qml.Hermitian(np.eye(2), wires=0), HermitianObsC128), + (qml.PauliZ(0), "NamedObsC"), + (qml.PauliZ(0) @ qml.PauliZ(1), "TensorProdObsC"), + (qml.Hadamard(0), "NamedObsC"), + (qml.Hermitian(np.eye(2), wires=0), "HermitianObsC"), ( - ( - qml.PauliZ(0) @ qml.Hadamard(1) @ (0.1 * (qml.PauliZ(2) + qml.PauliX(3))) - if device_name != "lightning.tensor" - else qml.PauliZ(0) @ qml.Hadamard(1) @ qml.PauliZ(2) @ qml.PauliX(3) - ), - TensorProdObsC128, + (qml.PauliZ(0) @ qml.Hadamard(1) @ (0.1 * (qml.PauliZ(2) + qml.PauliX(3)))), + "TensorProdObsC", ), ( qml.PauliZ(0) @ qml.PauliY(1) @ qml.PauliX(2), - TensorProdObsC128, + "TensorProdObsC", ), ( qml.PauliZ(0) @ qml.PauliY(1) @ (0.1 * (qml.PauliZ(2) + qml.PauliX(3))), - HamiltonianC128, + "HamiltonianC", ), ( ( @@ -113,43 +103,41 @@ def test_wrong_device_name(): @ qml.Hermitian(np.eye(2), wires=1) @ qml.Projector([0], wires=2) ), - TensorProdObsC128, + "TensorProdObsC", ), ( ( qml.Hermitian(np.eye(2), wires=0) @ qml.Hermitian(np.eye(2), wires=1) @ qml.Projector([0], wires=1) - if device_name != "lightning.tensor" - else qml.Hermitian(np.eye(2), wires=0) ), - HermitianObsC128, + "HermitianObsC", ), ( qml.PauliZ(0) @ qml.Hermitian(np.eye(2), wires=1) @ qml.Projector([0], wires=2), - TensorProdObsC128, + "TensorProdObsC", ), - (qml.Projector([0], wires=0), HermitianObsC128), - (qml.Hamiltonian([1], [qml.PauliZ(0)]), NamedObsC128), - (qml.sum(qml.Hadamard(0), qml.PauliX(1)), HamiltonianC128), + (qml.Projector([0], wires=0), "HermitianObsC"), + (qml.Hamiltonian([1], [qml.PauliZ(0)]), "NamedObsC"), + (qml.sum(qml.Hadamard(0), qml.PauliX(1)), "HamiltonianC"), ( ( qml.SparseHamiltonian( qml.Hamiltonian([1], [qml.PauliZ(0)]).sparse_matrix(), wires=[0] ) - if device_name != "lightning.tensor" - else 0.5 * qml.PauliX(0) ), - SparseHamiltonianC128 if device_name != "lightning.tensor" else HamiltonianC128, + "SparseHamiltonianC", ), - (2.5 * qml.PauliZ(0), HamiltonianC128), + (2.5 * qml.PauliZ(0), "HamiltonianC"), ], ) -def test_obs_returns_expected_type(obs, obs_type): +def test_obs_returns_expected_type(dtype, obs, obs_type): """Tests that observables get serialized to the expected type, with and without wires map""" - serializer = QuantumScriptSerializer(device_name) - assert isinstance(serializer._ob(obs, dict(enumerate(obs.wires))), obs_type) - assert isinstance(serializer._ob(obs), obs_type) + obs_type_mod = globals().get(obs_type + dtype) + + serializer = QuantumScriptSerializer(device_name, True if dtype == "64" else False) + assert isinstance(serializer._ob(obs, dict(enumerate(obs.wires))), obs_type_mod) + assert isinstance(serializer._ob(obs), obs_type_mod) class TestSerializeObs: @@ -192,19 +180,12 @@ def test_prod_return_with_overlapping_wires(self, use_csingle, wires_map): hermitian_obs = HermitianObsC64 if use_csingle else HermitianObsC128 c_dtype = np.complex64 if use_csingle else np.complex128 mat = obs.matrix().ravel().astype(c_dtype) - if device_name == "lightning.tensor": - with pytest.raises( - ValueError, match="The number of Hermitian observables target wires should be 1." - ): - s, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( - tape, wires_map - ) - else: - s, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( - tape, wires_map - ) - s_expected = hermitian_obs(mat, [0, 1, 2]) - assert s[0] == s_expected + + s, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( + tape, wires_map + ) + s_expected = hermitian_obs(mat, [0, 1, 2]) + assert s[0] == s_expected @pytest.mark.parametrize("use_csingle", [True, False]) @pytest.mark.parametrize("wires_map", [wires_dict, None]) @@ -216,55 +197,41 @@ def test_hermitian_return(self, use_csingle, wires_map): hermitian_obs = HermitianObsC64 if use_csingle else HermitianObsC128 c_dtype = np.complex64 if use_csingle else np.complex128 - if device_name == "lightning.tensor": - with pytest.raises( - ValueError, match="The number of Hermitian observables target wires should be 1." - ): - s, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( - tape, wires_map - ) - else: - s, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( - tape, wires_map - ) - s_expected = hermitian_obs( - np.array( - [ - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0, - ], - dtype=c_dtype, - ), - [0, 1], - ) - assert s[0] == s_expected + s, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( + tape, wires_map + ) + s_expected = hermitian_obs( + np.array( + [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + ], + dtype=c_dtype, + ), + [0, 1], + ) + assert s[0] == s_expected @pytest.mark.parametrize("use_csingle", [True, False]) @pytest.mark.parametrize("wires_map", [wires_dict, None]) def test_hermitian_tensor_return(self, use_csingle, wires_map): """Test expected serialization for a Hermitian return""" with qml.tape.QuantumTape() as tape: - qml.expval( - qml.Hermitian( - np.eye(2 if device_name == "lightning.tensor" else 4), - wires=[1] if device_name == "lightning.tensor" else [0, 1], - ) - @ qml.Hermitian(np.eye(2), wires=[2]) - ) + qml.expval(qml.Hermitian(np.eye(4), wires=[0, 1]) @ qml.Hermitian(np.eye(2), wires=[2])) c_dtype = np.complex64 if use_csingle else np.complex128 tensor_prod_obs = TensorProdObsC64 if use_csingle else TensorProdObsC128 @@ -275,10 +242,7 @@ def test_hermitian_tensor_return(self, use_csingle, wires_map): s_expected = tensor_prod_obs( [ - hermitian_obs( - np.eye(2 if device_name == "lightning.tensor" else 4, dtype=c_dtype).ravel(), - [1] if device_name == "lightning.tensor" else [0, 1], - ), + hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), hermitian_obs(np.eye(2, dtype=c_dtype).ravel(), [2]), ] ) @@ -290,13 +254,7 @@ def test_hermitian_tensor_return(self, use_csingle, wires_map): def test_mixed_tensor_return(self, use_csingle, wires_map): """Test expected serialization for a mixture of Hermitian and Pauli return""" with qml.tape.QuantumTape() as tape: - qml.expval( - qml.Hermitian( - np.eye(2 if device_name == "lightning.tensor" else 4), - wires=[0] if device_name == "lightning.tensor" else [0, 1], - ) - @ qml.PauliY(2) - ) + qml.expval(qml.Hermitian(np.eye(4), wires=[0, 1]) @ qml.PauliY(2)) c_dtype = np.complex64 if use_csingle else np.complex128 tensor_prod_obs = TensorProdObsC64 if use_csingle else TensorProdObsC128 @@ -309,10 +267,7 @@ def test_mixed_tensor_return(self, use_csingle, wires_map): s_expected = tensor_prod_obs( [ - hermitian_obs( - np.eye(2 if device_name == "lightning.tensor" else 4, dtype=c_dtype).ravel(), - [0] if device_name == "lightning.tensor" else [0, 1], - ), + hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), named_obs("PauliY", [2]), ] ) @@ -321,23 +276,11 @@ def test_mixed_tensor_return(self, use_csingle, wires_map): @pytest.mark.parametrize( "test_hermobs0", - [ - ( - qml.Hermitian(np.eye(4), wires=[0, 1]) - if device_name != "lightning.tensor" - else qml.Hermitian(np.eye(2), wires=[0]) - ) - ], + [(qml.Hermitian(np.eye(4), wires=[0, 1]))], ) @pytest.mark.parametrize( "test_hermobs1", - [ - ( - qml.Hermitian(np.ones((8, 8)), wires=range(3)) - if device_name != "lightning.tensor" - else qml.Hermitian(np.ones((2, 2)), wires=[0]) - ) - ], + [(qml.Hermitian(np.ones((8, 8)), wires=range(3)))], ) @pytest.mark.parametrize("use_csingle", [True, False]) @pytest.mark.parametrize("wires_map", [wires_dict, None]) @@ -372,20 +315,12 @@ def test_hamiltonian_return(self, test_hermobs0, test_hermobs1, use_csingle, wir [ tensor_prod_obs( [ - ( - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]) - if device_name != "lightning.tensor" - else hermitian_obs(np.eye(2, dtype=c_dtype).ravel(), [0]) - ), + (hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1])), named_obs("PauliY", [2]), ] ), tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]), - ( - hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]) - if device_name != "lightning.tensor" - else hermitian_obs(np.ones(4, dtype=c_dtype), [0]) - ), + (hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2])), ], ) @@ -393,23 +328,11 @@ def test_hamiltonian_return(self, test_hermobs0, test_hermobs1, use_csingle, wir @pytest.mark.parametrize( "test_hermobs0", - [ - ( - qml.Hermitian(np.eye(4), wires=[0, 1]) - if device_name != "lightning.tensor" - else qml.Hermitian(np.eye(2), wires=[0]) - ) - ], + [(qml.Hermitian(np.eye(4), wires=[0, 1]))], ) @pytest.mark.parametrize( "test_hermobs1", - [ - ( - qml.Hermitian(np.ones((8, 8)), wires=range(3)) - if device_name != "lightning.tensor" - else qml.Hermitian(np.ones((2, 2)), wires=[0]) - ) - ], + [(qml.Hermitian(np.ones((8, 8)), wires=range(3)))], ) @pytest.mark.parametrize("use_csingle", [True, False]) @pytest.mark.parametrize("wires_map", [wires_dict, None]) @@ -446,11 +369,7 @@ def test_hamiltonian_tensor_return(self, test_hermobs0, test_hermobs1, use_csing [ tensor_prod_obs( [ - ( - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]) - if device_name != "lightning.tensor" - else hermitian_obs(np.eye(2, dtype=c_dtype).ravel(), [0]) - ), + (hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1])), named_obs("PauliY", [2]), named_obs("PauliZ", [3]), ] @@ -464,11 +383,7 @@ def test_hamiltonian_tensor_return(self, test_hermobs0, test_hermobs1, use_csing ), tensor_prod_obs( [ - ( - hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]) - if device_name != "lightning.tensor" - else hermitian_obs(np.ones(4, dtype=c_dtype), [0]) - ), + (hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2])), named_obs("PauliZ", [3]), ] ), @@ -479,23 +394,11 @@ def test_hamiltonian_tensor_return(self, test_hermobs0, test_hermobs1, use_csing @pytest.mark.parametrize( "test_hermobs0", - [ - ( - qml.Hermitian(np.eye(4), wires=[0, 1]) - if device_name != "lightning.tensor" - else qml.Hermitian(np.eye(2), wires=[0]) - ) - ], + [(qml.Hermitian(np.eye(4), wires=[0, 1]))], ) @pytest.mark.parametrize( "test_hermobs1", - [ - ( - qml.Hermitian(np.ones((8, 8)), wires=range(3)) - if device_name != "lightning.tensor" - else qml.Hermitian(np.ones((2, 2)), wires=[0]) - ) - ], + [(qml.Hermitian(np.ones((8, 8)), wires=range(3)))], ) @pytest.mark.parametrize("use_csingle", [True, False]) @pytest.mark.parametrize("wires_map", [wires_dict, None]) @@ -513,11 +416,7 @@ def test_hamiltonian_mix_return(self, test_hermobs0, test_hermobs1, use_csingle, ham2 = qml.Hamiltonian( [0.7, 0.3], [ - ( - qml.PauliX(0) @ qml.Hermitian(np.eye(4), wires=[1, 2]) - if device_name != "lightning.tensor" - else qml.PauliX(0) @ qml.Hermitian(np.eye(2), wires=[1]) - ), + (qml.PauliX(0) @ qml.Hermitian(np.eye(4), wires=[1, 2])), qml.PauliY(0) @ qml.PauliX(2), ], ) @@ -541,20 +440,12 @@ def test_hamiltonian_mix_return(self, test_hermobs0, test_hermobs1, use_csingle, [ tensor_prod_obs( [ - ( - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]) - if device_name != "lightning.tensor" - else hermitian_obs(np.eye(2, dtype=c_dtype).ravel(), [0]) - ), + (hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1])), named_obs("PauliY", [2]), ] ), tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]), - ( - hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]) - if device_name != "lightning.tensor" - else hermitian_obs(np.ones(4, dtype=c_dtype), [0]) - ), + (hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2])), ], ) s_expected2 = hamiltonian_obs( @@ -563,11 +454,7 @@ def test_hamiltonian_mix_return(self, test_hermobs0, test_hermobs1, use_csingle, tensor_prod_obs( [ named_obs("PauliX", [0]), - ( - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [1, 2]) - if device_name != "lightning.tensor" - else hermitian_obs(np.eye(2, dtype=c_dtype).ravel(), [1]) - ), + (hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [1, 2])), ] ), tensor_prod_obs([named_obs("PauliY", [0]), named_obs("PauliX", [2])]), @@ -970,33 +857,3 @@ def test_global_phase(): D0 = check_global_phase_diagonal(par, wires, targets, controls, control_values) D1 = global_phase_diagonal(par, wires, controls, control_values) assert np.allclose(D0, D1) - - -@pytest.mark.skipif( - device_name != "lightning.tensor", reason="lightning.tensor does not support Sparse Hamiltonian" -) -@pytest.mark.parametrize( - "obs", - [qml.SparseHamiltonian(qml.Hamiltonian([1], [qml.PauliZ(0)]).sparse_matrix(), wires=[0])], -) -def test_unsupported_obs_returns_expected_type(obs): - """Tests that observables get serialized to the expected type, with and without wires map""" - serializer = QuantumScriptSerializer(device_name) - with pytest.raises( - NotImplementedError, - match="SparseHamiltonian is not supported on the lightning.tensor device.", - ): - serializer._ob(obs, dict(enumerate(obs.wires))) - - -@pytest.mark.skipif( - device_name != "lightning.tensor", reason="Only lightning.tensor requires the dtype check" -) -def test_tensornet_dtype(): - """Tests that the correct TensorNet type is used for the device""" - - serializer_c64 = QuantumScriptSerializer(device_name, use_csingle=True) - serializer_c128 = QuantumScriptSerializer(device_name, use_csingle=False) - - assert isinstance(serializer_c64.sv_type(3, 3), TensorNetC64) == True - assert isinstance(serializer_c128.sv_type(3, 3), TensorNetC128) == True diff --git a/tests/test_serialize_chunk_obs.py b/tests/test_serialize_chunk_obs.py index 760633551f..0c3dffbdf4 100644 --- a/tests/test_serialize_chunk_obs.py +++ b/tests/test_serialize_chunk_obs.py @@ -24,6 +24,12 @@ if not ld._CPP_BINARY_AVAILABLE: pytest.skip("No binary module found. Skipping.", allow_module_level=True) +if device_name == "lightning.tensor": + pytest.skip( + "Lightning Tensor serialization is tested separately in tests/lightning_tensor/test_serialize_chunk_obs_tensor.py", + allow_module_level=True, + ) + class TestSerializeObs: """Tests for the _serialize_observables function"""