From a2af361f1f03c6759ccdda73f6c56999baeaa74d Mon Sep 17 00:00:00 2001 From: Pietropaolo Frisoni Date: Tue, 7 May 2024 16:52:59 -0400 Subject: [PATCH 1/4] Computing `expval` and `var` with `LightningTensor` using `quimb` MPS (#686) * empty commit (triggering CI) * Auto update version * Definition of the two front-end classes * adding the `lightning_tensor` string to the supported backends in `setup.py` * adding `__init__.py` file to directly import the `lightning_tensor` device class * re-naming file * Auto update version * Creating the first prototype of initial MPS state tensor using `quimb` * providing the `backend`, `method` parameters and making `wires` optional * Changing names and structure * Auto update version * adding method required by the new device API design * Auto update version * using the `kwargs` parameter in `LightningTensor` and `CircuitMPS` in `quimb` * taking some further inputs from the new device API * Perhaps decided the overall structure of `LIghtningTensor` * Auto update version * adding docs to methods * temporary changes so that `pylint` does not complain at this stage * running `isort` * re-running formatter after `isort` * re-running formatter after `isort` * Applying suggested formatting change from CI * adding tmp unit tests * Adding `quimb` in `requirements.txt` * runing `isort` on mps test * removing `quimb` from requirement and deleting unit tests for `lightning.tensor` * Auto update version * re-inserting unit tests with an additional `yml` file * running isort on quimb test * changing name of yml file * preventing error in import * updating yml file * inserting `quimb` package in requirements-dev * strange error with `quimb` * strange error with `quimb` * specifying scipy version * removing installation of scipy from yml file * removing the new `yml` file * testing if tests are tested * Covering all lines in tests * forgot final line for formatter * Python formatter on CI complaints * covering missing lines * formatter on CI complaints * Trying not to skip test if Cpp is enabled * skipping tests if Cpp is enabled * removing the only line not covered by tests so far * Auto update version * Applying suggestions from code review and making the `state` attribute private (new API design) * Python formatter * removing params from `QuimbMPS` * Auto update version * removing `**kwargs` from `QuimbMPS` * removing unnecessary param at this stage * covering test line * formatter... * removing param description * Making `pylint` happy * forgot new arg in test * Updating base class and `preprocess` function * empty commit * Core structure (TODO: add tests) * Running `isort` * Updating `LightningTensor` class with new names from more advanced PR * Adding unit tests for the `expval` calculation * Auto update version * Auto update version * Triggering CI * Adding Hamiltonian expval (`pylint` keeps complaining about my formatter) * Auto update version * Trying to remove pin from `quimb` in `requirements.dev` * Auto update version * Auto update version * Removing infos on derivatives and using config options to pass parameters to interface * Usual `pylint` failures * Trying to solve formatting errors * Style and format update * typo in docstring * Sunday update: improved docstrings and structure * Support for expval and var plus some tests * Removing comments from lines in tests * Removing method that was supposed to be in next PR * removing old TODO comment * Adding API skip lines in test * removing old TODO comment * Forgot function from forked repo * Removing lines in tests * Removing changes from the `setup.py` file * restoring previous format to `setup.py` * Auto update version from '0.36.0-dev34' to '0.36.0-dev41' * Auto update version from '0.36.0-dev40' to '0.36.0-dev41' * Removing kwargs as suggested from code review * Addressing comments from CR * Skipping tests if CPP binary is available * -am "[ci skip]" * -am "[ci skip]" * -am "[ci skip]" * -am "[ci skip]" * Removing the `draw` function * Added changelog * Added statement in docstring * Renaming description in files and removing comments * Typo in docstring * Merging from master (now that the first PR has been merged) * Auto update version from '0.36.0-dev45' to '0.36.0-dev46' * Suggestions from CodeFactor (slipped during merging from master) * Suggestions from code review * Removing `Arg` from the `preprocess` function in the interface * applying suggestion by calling `expval` in `var` * Modified changelog and removed variations from supported operations * Switching to `quimb 1.8.1`, adding `contract` option to kwargs, removing recursive decomposition for 3+ qubit gates * removing `recursive decomposition` from the source dode * Suggestions from CR * Trying to make `Codecov` happy by inserting # pragma: no cover (although the lines are tested) * Auto update version from '0.36.0-dev46' to '0.36.0-dev47' * Usual formatting check that I forgot * "[skip ci]" * Auto update version from '0.36.0-dev47' to '0.36.0-dev48' * Trying to make Codecov happy with # pragma: no cover, even though lines are tested indeed! * Removing # pragma - no cover since Codecov is not happy anyway * Auto update version from '0.37.0-dev2' to '0.37.0-dev3' * Updating changelog --------- Co-authored-by: Dev version update bot Co-authored-by: ringo-but-quantum --- .github/CHANGELOG.md | 7 +- pennylane_lightning/core/_version.py | 2 +- .../lightning_tensor/backends/quimb/_mps.py | 289 ++++++++++++- .../lightning_tensor/lightning_tensor.py | 56 +-- requirements-dev.txt | 2 +- tests/lightning_tensor/test_expval.py | 398 +++++++++++++++++ .../lightning_tensor/test_lightning_tensor.py | 22 +- tests/lightning_tensor/test_quimb_mps.py | 191 +++++++- tests/lightning_tensor/test_var.py | 407 ++++++++++++++++++ 9 files changed, 1325 insertions(+), 49 deletions(-) create mode 100644 tests/lightning_tensor/test_expval.py create mode 100644 tests/lightning_tensor/test_var.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 034c8adb1b..a55ef60238 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -12,6 +12,9 @@ * Update Linux wheels to use manylinux_2_28 images. [(#667)](https://github.com/PennyLaneAI/pennylane-lightning/pull/667) +* Add support for `qml.expval` and `qml.var` in the `lightning.tensor` device for the `quimb` interface and the MPS method. + [(#686)](https://github.com/PennyLaneAI/pennylane-lightning/pull/686) + ### Documentation ### Bug fixes @@ -20,7 +23,7 @@ This release contains contributions from (in alphabetical order): -Amintor Dusko, Vincent Michaud-Rioux +Amintor Dusko, Pietropaolo Frisoni, Vincent Michaud-Rioux --- @@ -182,7 +185,7 @@ Amintor Dusko, Vincent Michaud-Rioux This release contains contributions from (in alphabetical order): -Ali Asadi, Amintor Dusko, Thomas Germain, Christina Lee, Erick Ochoa Lopez, Vincent Michaud-Rioux, Rashid N H M, Lee James O'Riordan, Mudit Pandey, Shuli Shu +Ali Asadi, Amintor Dusko, Pietropaolo Frisoni, Thomas Germain, Christina Lee, Erick Ochoa Lopez, Vincent Michaud-Rioux, Rashid N H M, Lee James O'Riordan, Mudit Pandey, Shuli Shu --- diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 35a436a592..82031f4caa 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.37.0-dev2" +__version__ = "0.37.0-dev3" diff --git a/pennylane_lightning/lightning_tensor/backends/quimb/_mps.py b/pennylane_lightning/lightning_tensor/backends/quimb/_mps.py index c5f53b15ec..ecc9885078 100644 --- a/pennylane_lightning/lightning_tensor/backends/quimb/_mps.py +++ b/pennylane_lightning/lightning_tensor/backends/quimb/_mps.py @@ -14,16 +14,104 @@ """ Class implementation for the Quimb MPS interface for simulating quantum circuits while keeping the state always in MPS form. """ +import copy +from typing import Callable, Sequence, Union -import numpy as np import pennylane as qml import quimb.tensor as qtn +from pennylane import numpy as np +from pennylane.devices import DefaultExecutionConfig, ExecutionConfig +from pennylane.devices.preprocess import ( + decompose, + validate_device_wires, + validate_measurements, + validate_observables, +) +from pennylane.measurements import ExpectationMP, MeasurementProcess, StateMeasurement, VarianceMP +from pennylane.tape import QuantumScript, QuantumTape +from pennylane.transforms.core import TransformProgram +from pennylane.typing import Result, ResultBatch, TensorLike from pennylane.wires import Wires -_operations = frozenset({}) # pragma: no cover +Result_or_ResultBatch = Union[Result, ResultBatch] +QuantumTapeBatch = Sequence[QuantumTape] +QuantumTape_or_Batch = Union[QuantumTape, QuantumTapeBatch] +PostprocessingFn = Callable[[ResultBatch], Result_or_ResultBatch] + +_operations = frozenset( + { + "Identity", + "QubitUnitary", + "ControlledQubitUnitary", + "MultiControlledX", + "DiagonalQubitUnitary", + "PauliX", + "PauliY", + "PauliZ", + "MultiRZ", + "GlobalPhase", + "Hadamard", + "S", + "T", + "SX", + "CNOT", + "SWAP", + "ISWAP", + "PSWAP", + "SISWAP", + "SQISW", + "CSWAP", + "Toffoli", + "CY", + "CZ", + "PhaseShift", + "ControlledPhaseShift", + "CPhase", + "RX", + "RY", + "RZ", + "Rot", + "CRX", + "CRY", + "CRZ", + "CRot", + "IsingXX", + "IsingYY", + "IsingZZ", + "IsingXY", + "SingleExcitation", + "SingleExcitationPlus", + "SingleExcitationMinus", + "DoubleExcitation", + "QubitCarry", + "QubitSum", + "OrbitalRotation", + "QFT", + "ECR", + "BlockEncode", + } +) # The set of supported operations. -_observables = frozenset({}) # pragma: no cover + +_observables = frozenset( + { + "PauliX", + "PauliY", + "PauliZ", + "Hadamard", + "Hermitian", + "Identity", + "Projector", + "SparseHamiltonian", + "Hamiltonian", + "LinearCombination", + "Sum", + "SProd", + "Prod", + "Exp", + } +) # The set of supported observables. @@ -57,15 +145,15 @@ def __init__(self, num_wires, interf_opts, dtype=np.complex128): self._wires = Wires(range(num_wires)) self._dtype = dtype - self._init_state_ops = { + self._init_state_opts = { "binary": "0" * max(1, len(self._wires)), "dtype": self._dtype.__name__, "tags": [str(l) for l in self._wires.labels], } self._gate_opts = { - "contract": "swap+split", "parametrize": None, + "contract": interf_opts["contract"], "cutoff": interf_opts["cutoff"], "max_bond": interf_opts["max_bond_dim"], } @@ -79,22 +167,205 @@ def __init__(self, num_wires, interf_opts, dtype=np.complex128): self._circuitMPS = qtn.CircuitMPS(psi0=self._initial_mps()) @property - def state(self): - """Current MPS handled by the interface.""" + def interface_name(self) -> str: + """The name of this interface.""" + return "QuimbMPS interface" + + @property + def state(self) -> qtn.MatrixProductState: + """Return the current MPS handled by the interface.""" return self._circuitMPS.psi def state_to_array(self) -> np.ndarray: """Contract the MPS into a dense array.""" return self._circuitMPS.to_dense() + def _reset_state(self) -> None: + """Reset the MPS.""" + self._circuitMPS = qtn.CircuitMPS(psi0=self._initial_mps()) + def _initial_mps(self) -> qtn.MatrixProductState: r""" - Returns an initial state to :math:`\ket{0}`. + Return an initial state to :math:`\ket{0}`. Internally, it uses `quimb`'s `MPS_computational_state` method. Returns: MatrixProductState: The initial MPS of a circuit. """ + return qtn.MPS_computational_state(**self._init_state_opts) + + def preprocess(self) -> TransformProgram: + """This function defines the device transform program to be applied for this interface. + + Returns: + TransformProgram: A transform program that when called returns :class:`~.QuantumTape`'s that the + device can natively execute as well as a postprocessing function to be called after execution. + + This interface: + + * Supports any one or two-qubit operations that provide a matrix. + * Supports any three or four-qubit operations that provide a decomposition method. + * Currently does not support finite shots. + """ + + program = TransformProgram() + + program.add_transform(validate_measurements, name=self.interface_name) + program.add_transform(validate_observables, accepted_observables, name=self.interface_name) + program.add_transform(validate_device_wires, self._wires, name=self.interface_name) + program.add_transform( + decompose, + stopping_condition=stopping_condition, + skip_initial_state_prep=True, + name=self.interface_name, + ) + program.add_transform(qml.transforms.broadcast_expand) + + return program + + # pylint: disable=unused-argument + def execute( + self, + circuits: QuantumTape_or_Batch, + execution_config: ExecutionConfig = DefaultExecutionConfig, + ) -> Result_or_ResultBatch: + """Execute a circuit or a batch of circuits and turn it into results. + + Args: + circuits (Union[QuantumTape, Sequence[QuantumTape]]): the quantum circuits to be executed + execution_config (ExecutionConfig): a datastructure with additional information required for execution + + Returns: + TensorLike, tuple[TensorLike], tuple[tuple[TensorLike]]: A numeric result of the computation. + """ + + results = [] + for circuit in circuits: + circuit = circuit.map_to_standard_wires() + results.append(self.simulate(circuit)) + + return tuple(results) + + def simulate(self, circuit: QuantumScript) -> Result: + """Simulate a single quantum script. This function assumes that all operations provide matrices. + + Args: + circuit (QuantumScript): The single circuit to simulate. + + Returns: + Tuple[TensorLike]: The results of the simulation. + """ + + self._reset_state() + + for op in circuit.operations: + self._apply_operation(op) + + if not circuit.shots: + if len(circuit.measurements) == 1: + return self.measurement(circuit.measurements[0]) + return tuple(self.measurement(mp) for mp in circuit.measurements) + + raise NotImplementedError # pragma: no cover + + def _apply_operation(self, op: qml.operation.Operator) -> None: + """Apply a single operator to the circuit, keeping the state always in a MPS form. + + Internally it uses `quimb`'s `apply_gate` method. + + Args: + op (Operator): The operation to apply. + """ + + self._circuitMPS.apply_gate(op.matrix().astype(self._dtype), *op.wires, **self._gate_opts) + + def measurement(self, measurementprocess: MeasurementProcess) -> TensorLike: + """Measure the measurement required by the circuit over the MPS. + + Args: + measurementprocess (MeasurementProcess): measurement to apply to the state. + + Returns: + TensorLike: the result of the measurement. + """ + + return self._get_measurement_function(measurementprocess)(measurementprocess) + + def _get_measurement_function( + self, measurementprocess: MeasurementProcess + ) -> Callable[[MeasurementProcess, TensorLike], TensorLike]: + """Get the appropriate method for performing a measurement. + + Args: + measurementprocess (MeasurementProcess): measurement process to apply to the state + + Returns: + Callable: function that returns the measurement result + """ + if isinstance(measurementprocess, StateMeasurement): + if isinstance(measurementprocess, ExpectationMP): + return self.expval + + if isinstance(measurementprocess, VarianceMP): + return self.var + + raise NotImplementedError # pragma: no cover + + def expval(self, measurementprocess: MeasurementProcess) -> float: + """Expectation value of the supplied observable contained in the MeasurementProcess. + + Args: + measurementprocess (StateMeasurement): measurement to apply to the MPS. + + Returns: + Expectation value of the observable. + """ + + obs = measurementprocess.obs + + result = self._local_expectation(obs.matrix(), tuple(obs.wires)) + + return result + + def var(self, measurementprocess: MeasurementProcess) -> float: + """Variance of the supplied observable contained in the MeasurementProcess. + + Args: + measurementprocess (StateMeasurement): measurement to apply to the MPS. + + Returns: + Variance of the observable. + """ + + obs = measurementprocess.obs + + obs_mat = obs.matrix() + expect_op = self.expval(measurementprocess) + expect_squar_op = self._local_expectation(obs_mat @ obs_mat.conj().T, tuple(obs.wires)) + + return expect_squar_op - np.square(expect_op) + + def _local_expectation(self, matrix, wires) -> float: + """Compute the local expectation value of a matrix on the MPS. + + Internally, it uses `quimb`'s `local_expectation` method. + + Args: + matrix (array): the matrix to compute the expectation value of. + wires (tuple[int]): the wires the matrix acts on. + + Returns: + Local expectation value of the matrix on the MPS. + """ + + # We need to copy the MPS to avoid modifying the original state + qc = copy.deepcopy(self._circuitMPS) + + exp_val = qc.local_expectation( + matrix, + wires, + **self._expval_opts, + ) - return qtn.MPS_computational_state(**self._init_state_ops) + return float(np.real(exp_val)) diff --git a/pennylane_lightning/lightning_tensor/lightning_tensor.py b/pennylane_lightning/lightning_tensor/lightning_tensor.py index fc8974cda2..fa54cff0bc 100644 --- a/pennylane_lightning/lightning_tensor/lightning_tensor.py +++ b/pennylane_lightning/lightning_tensor/lightning_tensor.py @@ -24,7 +24,6 @@ from pennylane.devices import DefaultExecutionConfig, Device, ExecutionConfig from pennylane.devices.modifiers import simulator_tracking, single_tape_support from pennylane.tape import QuantumTape -from pennylane.transforms.core import TransformProgram from pennylane.typing import Result, ResultBatch from .backends.quimb._mps import QuimbMPS @@ -73,7 +72,10 @@ class LightningTensor(Device): ``max_bond_dim`` (int): Maximum bond dimension for the MPS simulator. It corresponds to the number of Schmidt coefficients retained at the end of the SVD algorithm when applying gates. Default is ``None``. - ``cutoff`` (float): Truncation threshold for the Schmidt coefficients in a MPS simulator. Default is ``1e-16``. + ``cutoff`` (float): Truncation threshold for the Schmidt coefficients in a MPS simulator. Default is ``np.finfo(c_dtype).eps``. + ``contract`` (str): The contraction method for applying gates. It can be either ``auto-mps`` or ``nonlocal``. + ``nonlocal`` turns each gate into a MPO and applies it directly to the MPS, while ``auto-mps`` swaps nonlocal qubits in 2-qubit gates to be next + to each other before applying the gate, then swaps them back. Default is ``auto-mps``. """ # pylint: disable=too-many-instance-attributes @@ -82,6 +84,7 @@ class LightningTensor(Device): _device_options = ( "backend", "c_dtype", + "contract", "cutoff", "method", "max_bond_dim", @@ -120,6 +123,7 @@ def __init__( # options for MPS self._max_bond_dim = kwargs.get("max_bond_dim", None) self._cutoff = kwargs.get("cutoff", np.finfo(self._c_dtype).eps) + self._contract = kwargs.get("contract", "auto-mps") self._interface = None interface_opts = self._setup_execution_config().device_options @@ -201,18 +205,15 @@ def preprocess( device can natively execute as well as a postprocessing function to be called after execution, and a configuration with unset specifications filled in. - This device: + This device currently: - * Supports any qubit operations that provide a matrix. - * Currently does not support finite shots. + * Does not support finite shots. + * Does not support derivatives. + * Does not support vector-Jacobian products. """ config = self._setup_execution_config(execution_config) - - program = TransformProgram() - - # more in the next PR - + program = self._interface.preprocess() return program, config def execute( @@ -224,13 +225,13 @@ def execute( Args: circuits (Union[QuantumTape, Sequence[QuantumTape]]): the quantum circuits to be executed. - execution_config (ExecutionConfig): a datastructure with additional information required for execution. + execution_config (ExecutionConfig): a data structure with additional information required for execution. Returns: TensorLike, tuple[TensorLike], tuple[tuple[TensorLike]]: A numeric result of the computation. """ - # comment is removed in the next PR - # return self._interface.execute(circuits, execution_config) + + return self._interface.execute(circuits, execution_config) # pylint: disable=unused-argument def supports_derivatives( @@ -255,14 +256,14 @@ def compute_derivatives( circuits: QuantumTape_or_Batch, execution_config: ExecutionConfig = DefaultExecutionConfig, ): - """Calculate the jacobian of either a single or a batch of circuits on the device. + """Calculate the Jacobian of either a single or a batch of circuits on the device. Args: circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuits to calculate derivatives for. - execution_config (ExecutionConfig): a datastructure with all additional information required for execution. + execution_config (ExecutionConfig): a data structure with all additional information required for execution. Returns: - Tuple: The jacobian for each trainable parameter. + Tuple: The Jacobian for each trainable parameter. """ raise NotImplementedError( "The computation of derivatives has yet to be implemented for the lightning.tensor device." @@ -273,11 +274,11 @@ def execute_and_compute_derivatives( circuits: QuantumTape_or_Batch, execution_config: ExecutionConfig = DefaultExecutionConfig, ): - """Compute the results and jacobians of circuits at the same time. + """Compute the results and Jacobians of circuits at the same time. Args: circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuits or batch of circuits. - execution_config (ExecutionConfig): a datastructure with all additional information required for execution. + execution_config (ExecutionConfig): a data structure with all additional information required for execution. Returns: tuple: A numeric result of the computation and the gradient. @@ -292,7 +293,7 @@ def supports_vjp( execution_config: Optional[ExecutionConfig] = None, circuit: Optional[QuantumTape] = None, ) -> bool: - """Whether or not this device defines a custom vector jacobian product. + """Whether or not this device defines a custom vector-Jacobian product. Args: execution_config (ExecutionConfig): The configuration of the desired derivative calculation. @@ -301,7 +302,6 @@ def supports_vjp( Returns: Bool: Whether or not a derivative can be calculated provided the given information. """ - # TODO: implement during next quarter return False def compute_vjp( @@ -310,20 +310,20 @@ def compute_vjp( cotangents: Tuple[Number], execution_config: ExecutionConfig = DefaultExecutionConfig, ): - r"""The vector jacobian product used in reverse-mode differentiation. + r"""The vector-Jacobian product used in reverse-mode differentiation. Args: circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuit or batch of circuits. cotangents (Tuple[Number, Tuple[Number]]): Gradient-output vector. Must have shape matching the output shape of the corresponding circuit. If the circuit has a single output, ``cotangents`` may be a single number, not an iterable of numbers. - execution_config (ExecutionConfig): a datastructure with all additional information required for execution. + execution_config (ExecutionConfig): a data structure with all additional information required for execution. Returns: - tensor-like: A numeric result of computing the vector jacobian product. + tensor-like: A numeric result of computing the vector-Jacobian product. """ raise NotImplementedError( - "The computation of vector jacobian product has yet to be implemented for the lightning.tensor device." + "The computation of vector-Jacobian product has yet to be implemented for the lightning.tensor device." ) def execute_and_compute_vjp( @@ -332,17 +332,17 @@ def execute_and_compute_vjp( cotangents: Tuple[Number], execution_config: ExecutionConfig = DefaultExecutionConfig, ): - """Calculate both the results and the vector jacobian product used in reverse-mode differentiation. + """Calculate both the results and the vector-Jacobian product used in reverse-mode differentiation. Args: circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuit or batch of circuits to be executed. cotangents (Tuple[Number, Tuple[Number]]): Gradient-output vector. Must have shape matching the output shape of the corresponding circuit. - execution_config (ExecutionConfig): a datastructure with all additional information required for execution. + execution_config (ExecutionConfig): a data structure with all additional information required for execution. Returns: - Tuple, Tuple: the result of executing the scripts and the numeric result of computing the vector jacobian product + Tuple, Tuple: the result of executing the scripts and the numeric result of computing the vector-Jacobian product """ raise NotImplementedError( - "The computation of vector jacobian product has yet to be implemented for the lightning.tensor device." + "The computation of vector-Jacobian product has yet to be implemented for the lightning.tensor device." ) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1da7141aef..3ed0f57c0f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,4 +19,4 @@ cmake custatevec-cu12 pylint scipy~=1.12.0 -quimb +git+https://github.com/jcmgray/quimb.git diff --git a/tests/lightning_tensor/test_expval.py b/tests/lightning_tensor/test_expval.py new file mode 100644 index 0000000000..d32062bfad --- /dev/null +++ b/tests/lightning_tensor/test_expval.py @@ -0,0 +1,398 @@ +# 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. +""" +Tests for the expectation value calculations on the LightningTensor device. +""" + +import numpy as np +import pennylane as qml +import pytest +from conftest import PHI, THETA, VARPHI, LightningDevice +from pennylane.devices import DefaultQubit + +if not LightningDevice._new_API: + pytest.skip("Exclusive tests for new API. Skipping.", allow_module_level=True) + +if LightningDevice._CPP_BINARY_AVAILABLE: + pytest.skip("Device doesn't have C++ support yet.", allow_module_level=True) + +from pennylane_lightning.lightning_tensor import LightningTensor + + +@pytest.fixture(params=[np.complex64, np.complex128]) +def dev(request): + return LightningTensor(wires=3, c_dtype=request.param) + + +def calculate_reference(tape): + dev = DefaultQubit(max_workers=1) + program, _ = dev.preprocess() + tapes, transf_fn = program([tape]) + results = dev.execute(tapes) + return transf_fn(results) + + +def execute(dev, tape): + results = dev.execute(tape) + return results + + +@pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) +class TestExpval: + """Test expectation value calculations""" + + def test_Identity(self, theta, phi, dev, tol): + """Tests applying identities.""" + + ops = [ + qml.Identity(0), + qml.Identity((0, 1)), + qml.RX(theta, 0), + qml.Identity((1, 2)), + qml.RX(phi, 1), + ] + measurements = [qml.expval(qml.PauliZ(0))] + tape = qml.tape.QuantumScript(ops, measurements) + + result = dev.execute(tape) + expected = np.cos(theta) + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(result, expected, tol) + + def test_identity_expectation(self, theta, phi, dev, tol): + """Tests identity expectations.""" + + tape = qml.tape.QuantumScript( + [qml.RX(theta, wires=[0]), qml.RX(phi, wires=[1]), qml.CNOT(wires=[0, 1])], + [qml.expval(qml.Identity(wires=[0])), qml.expval(qml.Identity(wires=[1]))], + ) + result = dev.execute(tape) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(1.0, result, tol) + + def test_multi_wire_identity_expectation(self, theta, phi, dev, tol): + """Tests multi-wire identity.""" + + tape = qml.tape.QuantumScript( + [qml.RX(theta, wires=[0]), qml.RX(phi, wires=[1]), qml.CNOT(wires=[0, 1])], + [qml.expval(qml.Identity(wires=[0, 1]))], + ) + result = dev.execute(tape) + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(1.0, result, tol) + + @pytest.mark.parametrize( + "wires", + [([0, 1]), (["a", 1]), (["b", "a"]), ([-1, 2.5])], + ) + def test_custom_wires(self, theta, phi, tol, wires): + """Tests custom wires.""" + dev = LightningTensor(wires=wires, c_dtype=np.complex128) + + tape = qml.tape.QuantumScript( + [ + qml.RX(theta, wires=wires[0]), + qml.RX(phi, wires=wires[1]), + qml.CNOT(wires=wires), + ], + [ + qml.expval(qml.PauliZ(wires=wires[0])), + qml.expval(qml.PauliZ(wires=wires[1])), + ], + ) + + calculated_val = execute(dev, tape) + reference_val = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + @pytest.mark.parametrize( + "Obs, Op, expected_fn", + [ + ( + [qml.PauliX(wires=[0]), qml.PauliX(wires=[1])], + qml.RY, + lambda theta, phi: np.array([np.sin(theta) * np.sin(phi), np.sin(phi)]), + ), + ( + [qml.PauliY(wires=[0]), qml.PauliY(wires=[1])], + qml.RX, + lambda theta, phi: np.array([0, -np.cos(theta) * np.sin(phi)]), + ), + ( + [qml.PauliZ(wires=[0]), qml.PauliZ(wires=[1])], + qml.RX, + lambda theta, phi: np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]), + ), + ( + [qml.Hadamard(wires=[0]), qml.Hadamard(wires=[1])], + qml.RY, + lambda theta, phi: np.array( + [ + np.sin(theta) * np.sin(phi) + np.cos(theta), + np.cos(theta) * np.cos(phi) + np.sin(phi), + ] + ) + / np.sqrt(2), + ), + ], + ) + def test_single_wire_observables_expectation(self, Obs, Op, expected_fn, theta, phi, tol, dev): + """Test that expectation values for single wire observables are correct""" + + tape = qml.tape.QuantumScript( + [Op(theta, wires=[0]), Op(phi, wires=[1]), qml.CNOT(wires=[0, 1])], + [qml.expval(Obs[0]), qml.expval(Obs[1])], + ) + result = execute(dev, tape) + expected = expected_fn(theta, phi) + + assert np.allclose(result, expected, tol) + + def test_hermitian_expectation(self, theta, phi, tol, dev): + """Tests an Hermitian operator.""" + + with qml.tape.QuantumTape() as tape: + qml.RX(theta, wires=0) + qml.RX(phi, wires=1) + qml.RX(theta + phi, wires=2) + + for idx in range(3): + qml.expval(qml.Hermitian([[1, 0], [0, -1]], wires=[idx])) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + assert np.allclose(calculated_val, reference_val, tol) + + def test_hamiltonian_expectation(self, theta, phi, tol, dev): + """Tests a Hamiltonian.""" + + ham = qml.Hamiltonian( + [1.0, 0.3, 0.3, 0.4], + [ + qml.PauliX(0) @ qml.PauliX(1), + qml.PauliZ(0), + qml.PauliZ(1), + qml.PauliX(0) @ qml.PauliY(1), + ], + ) + + with qml.tape.QuantumTape() as tape: + qml.RX(theta, wires=0) + qml.RX(phi, wires=1) + qml.RX(theta + phi, wires=2) + + qml.expval(ham) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + assert np.allclose(calculated_val, reference_val, tol) + + def test_sparse_hamiltonian_expectation(self, theta, phi, tol, dev): + """Tests a Hamiltonian.""" + + ham = qml.SparseHamiltonian( + qml.Hamiltonian( + [1.0, 0.3, 0.3, 0.4], + [ + qml.PauliX(0) @ qml.PauliX(1), + qml.PauliZ(0), + qml.PauliZ(1), + qml.PauliX(0) @ qml.PauliY(1), + ], + ).sparse_matrix(), + wires=[0, 1], + ) + + with qml.tape.QuantumTape() as tape: + qml.RX(theta, wires=0) + qml.RX(phi, wires=1) + + qml.expval(ham) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + assert np.allclose(calculated_val, reference_val, tol) + + +@pytest.mark.parametrize("phi", PHI) +class TestOperatorArithmetic: + """Test integration with SProd, Prod, and Sum.""" + + @pytest.mark.parametrize( + "obs", + [ + qml.s_prod(0.5, qml.PauliZ(0)), + qml.prod(qml.PauliZ(0), qml.PauliX(1)), + qml.sum(qml.PauliZ(0), qml.PauliX(1)), + ], + ) + def test_op_math(self, phi, dev, obs, tol): + """Tests the `SProd`, `Prod`, and `Sum` classes.""" + + tape = qml.tape.QuantumScript( + [ + qml.RX(phi, wires=[0]), + qml.Hadamard(wires=[1]), + qml.PauliZ(wires=[1]), + qml.RX(-1.1 * phi, wires=[1]), + ], + [qml.expval(obs)], + ) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + def test_integration(self, phi, dev, tol): + """Test a Combination of `Sum`, `SProd`, and `Prod`.""" + + obs = qml.sum( + qml.s_prod(2.3, qml.PauliZ(0)), + -0.5 * qml.prod(qml.PauliY(0), qml.PauliZ(1)), + ) + + tape = qml.tape.QuantumScript( + [qml.RX(phi, wires=[0]), qml.RX(-1.1 * phi, wires=[0])], + [qml.expval(obs)], + ) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + +@pytest.mark.parametrize("theta, phi, varphi", list(zip(THETA, PHI, VARPHI))) +class TestTensorExpval: + """Test tensor expectation values""" + + def test_PauliX_PauliY(self, theta, phi, varphi, dev, tol): + """Tests a tensor product involving PauliX and PauliY.""" + + with qml.tape.QuantumTape() as tape: + qml.RX(theta, wires=[0]) + qml.RX(phi, wires=[1]) + qml.RX(varphi, wires=[2]) + qml.CNOT(wires=[0, 1]) + qml.CNOT(wires=[1, 2]) + qml.expval(qml.PauliX(0) @ qml.PauliY(2)) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + def test_PauliZ_identity(self, theta, phi, varphi, dev, tol): + """Tests a tensor product involving PauliZ and Identity.""" + + with qml.tape.QuantumTape() as tape: + qml.Identity(wires=[0]) + qml.RX(theta, wires=[0]) + qml.RX(phi, wires=[1]) + qml.RX(varphi, wires=[2]) + qml.CNOT(wires=[0, 1]) + qml.CNOT(wires=[1, 2]) + qml.expval(qml.PauliZ(0) @ qml.Identity(1) @ qml.PauliZ(2)) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + def test_PauliZ_hadamard_PauliY(self, theta, phi, varphi, dev, tol): + """Tests a tensor product involving PauliY, PauliZ and Hadamard.""" + + with qml.tape.QuantumTape() as tape: + qml.RX(theta, wires=[0]) + qml.RX(phi, wires=[1]) + qml.RX(varphi, wires=[2]) + qml.CNOT(wires=[0, 1]) + qml.CNOT(wires=[1, 2]) + qml.expval(qml.PauliZ(0) @ qml.Hadamard(1) @ qml.PauliY(2)) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + +# Define the parameter values +THETA = np.linspace(0.11, 1, 3) +PHI = np.linspace(0.32, 1, 3) + + +@pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) +def test_multi_qubit_gates(theta, phi, dev, tol): + """Tests a simple circuit with multi-qubit gates.""" + + ops = [ + qml.PauliX(wires=[0]), + qml.RX(theta, wires=[0]), + qml.CSWAP(wires=[7, 0, 5]), + qml.RX(phi, wires=[1]), + qml.CNOT(wires=[3, 4]), + qml.DoubleExcitation(phi, wires=[1, 2, 3, 4]), + qml.CZ(wires=[4, 5]), + qml.Hadamard(wires=[4]), + qml.CCZ(wires=[0, 1, 2]), + qml.CSWAP(wires=[2, 3, 4]), + qml.QFT(wires=[0, 1, 2]), + qml.CNOT(wires=[2, 4]), + qml.Toffoli(wires=[0, 1, 2]), + qml.DoubleExcitation(phi, wires=[0, 1, 3, 4]), + ] + + meas = [ + qml.expval(qml.PauliY(wires=[2])), + qml.expval(qml.Hamiltonian([1, 5, 6], [qml.Z(6), qml.X(0), qml.Hadamard(4)])), + qml.expval( + qml.Hamiltonian( + [4, 5, 7], + [ + qml.Z(6) @ qml.Y(4), + qml.X(7), + qml.Hadamard(4), + ], + ) + ), + ] + + tape = qml.tape.QuantumScript(ops=ops, measurements=meas) + + reference_val = calculate_reference(tape) + dev = LightningTensor(wires=tape.wires, c_dtype=np.complex128) + calculated_val = dev.execute(tape) + + assert np.allclose(calculated_val, reference_val) diff --git a/tests/lightning_tensor/test_lightning_tensor.py b/tests/lightning_tensor/test_lightning_tensor.py index 3d7d1033b8..e835a10657 100644 --- a/tests/lightning_tensor/test_lightning_tensor.py +++ b/tests/lightning_tensor/test_lightning_tensor.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Unit tests for the generic lightning tensor class. +Unit tests for the LightningTensor class. """ @@ -84,14 +84,20 @@ def test_support_derivatives(): def test_compute_derivatives(): """Test that an error is raised if the `compute_derivatives` method is called.""" dev = LightningTensor() - with pytest.raises(NotImplementedError): + 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() - with pytest.raises(NotImplementedError): + 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) @@ -104,12 +110,18 @@ def test_supports_vjp(): def test_compute_vjp(): """Test that an error is raised if `compute_vjp` method is called.""" dev = LightningTensor() - with pytest.raises(NotImplementedError): + 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() - with pytest.raises(NotImplementedError): + 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_quimb_mps.py b/tests/lightning_tensor/test_quimb_mps.py index 8f3e376e56..5583a7e7db 100644 --- a/tests/lightning_tensor/test_quimb_mps.py +++ b/tests/lightning_tensor/test_quimb_mps.py @@ -15,12 +15,17 @@ Unit tests for the ``quimb`` interface. """ +import itertools +import math import numpy as np +import pennylane as qml import pytest import quimb.tensor as qtn from conftest import LightningDevice # tested device +from pennylane.devices import DefaultQubit from pennylane.wires import Wires +from scipy.sparse import csr_matrix from pennylane_lightning.lightning_tensor import LightningTensor @@ -30,6 +35,105 @@ if LightningDevice._CPP_BINARY_AVAILABLE: pytest.skip("Device doesn't have C++ support yet.", allow_module_level=True) +THETA = np.linspace(0.11, 1, 3) +PHI = np.linspace(0.32, 1, 3) + +# gates for which interface support is tested +ops = { + "Identity": qml.Identity(wires=[0]), + "BlockEncode": qml.BlockEncode([[0.1, 0.2], [0.3, 0.4]], wires=[0, 1]), + "CNOT": qml.CNOT(wires=[0, 1]), + "CRX": qml.CRX(0, wires=[0, 1]), + "CRY": qml.CRY(0, wires=[0, 1]), + "CRZ": qml.CRZ(0, wires=[0, 1]), + "CRot": qml.CRot(0, 0, 0, wires=[0, 1]), + "CSWAP": qml.CSWAP(wires=[0, 1, 2]), + "CZ": qml.CZ(wires=[0, 1]), + "CCZ": qml.CCZ(wires=[0, 1, 2]), + "CY": qml.CY(wires=[0, 1]), + "CH": qml.CH(wires=[0, 1]), + "DiagonalQubitUnitary": qml.DiagonalQubitUnitary(np.array([1, 1]), wires=[0]), + "Hadamard": qml.Hadamard(wires=[0]), + "MultiRZ": qml.MultiRZ(0, wires=[0]), + "PauliX": qml.X(0), + "PauliY": qml.Y(0), + "PauliZ": qml.Z(0), + "X": qml.X([0]), + "Y": qml.Y([0]), + "Z": qml.Z([0]), + "PhaseShift": qml.PhaseShift(0, wires=[0]), + "PCPhase": qml.PCPhase(0, 1, wires=[0, 1]), + "ControlledPhaseShift": qml.ControlledPhaseShift(0, wires=[0, 1]), + "CPhaseShift00": qml.CPhaseShift00(0, wires=[0, 1]), + "CPhaseShift01": qml.CPhaseShift01(0, wires=[0, 1]), + "CPhaseShift10": qml.CPhaseShift10(0, wires=[0, 1]), + "QubitUnitary": qml.QubitUnitary(np.eye(2), wires=[0]), + "SpecialUnitary": qml.SpecialUnitary(np.array([0.2, -0.1, 2.3]), wires=1), + "ControlledQubitUnitary": qml.ControlledQubitUnitary(np.eye(2), control_wires=[1], wires=[0]), + "MultiControlledX": qml.MultiControlledX(wires=[1, 2, 0]), + "IntegerComparator": qml.IntegerComparator(1, geq=True, wires=[0, 1, 2]), + "RX": qml.RX(0, wires=[0]), + "RY": qml.RY(0, wires=[0]), + "RZ": qml.RZ(0, wires=[0]), + "Rot": qml.Rot(0, 0, 0, wires=[0]), + "S": qml.S(wires=[0]), + "Adjoint(S)": qml.adjoint(qml.S(wires=[0])), + "SWAP": qml.SWAP(wires=[0, 1]), + "ISWAP": qml.ISWAP(wires=[0, 1]), + "PSWAP": qml.PSWAP(0, wires=[0, 1]), + "ECR": qml.ECR(wires=[0, 1]), + "Adjoint(ISWAP)": qml.adjoint(qml.ISWAP(wires=[0, 1])), + "T": qml.T(wires=[0]), + "Adjoint(T)": qml.adjoint(qml.T(wires=[0])), + "SX": qml.SX(wires=[0]), + "Adjoint(SX)": qml.adjoint(qml.SX(wires=[0])), + "Toffoli": qml.Toffoli(wires=[0, 1, 2]), + "QFT": qml.templates.QFT(wires=[0, 1, 2]), + "IsingXX": qml.IsingXX(0, wires=[0, 1]), + "IsingYY": qml.IsingYY(0, wires=[0, 1]), + "IsingZZ": qml.IsingZZ(0, wires=[0, 1]), + "IsingXY": qml.IsingXY(0, wires=[0, 1]), + "SingleExcitation": qml.SingleExcitation(0, wires=[0, 1]), + "SingleExcitationPlus": qml.SingleExcitationPlus(0, wires=[0, 1]), + "SingleExcitationMinus": qml.SingleExcitationMinus(0, wires=[0, 1]), + "DoubleExcitation": qml.DoubleExcitation(0, wires=[0, 1, 2, 3]), + "QubitCarry": qml.QubitCarry(wires=[0, 1, 2, 3]), + "QubitSum": qml.QubitSum(wires=[0, 1, 2]), + "PauliRot": qml.PauliRot(0, "XXYY", wires=[0, 1, 2, 3]), + "U1": qml.U1(0, wires=0), + "U2": qml.U2(0, 0, wires=0), + "U3": qml.U3(0, 0, 0, wires=0), + "SISWAP": qml.SISWAP(wires=[0, 1]), + "Adjoint(SISWAP)": qml.adjoint(qml.SISWAP(wires=[0, 1])), + "OrbitalRotation": qml.OrbitalRotation(0, wires=[0, 1, 2, 3]), + "FermionicSWAP": qml.FermionicSWAP(0, wires=[0, 1]), + "GlobalPhase": qml.GlobalPhase(0.123, wires=[0, 1]), +} + +all_ops = ops.keys() + +# observables for which interface support is tested +obs = { + "Identity": qml.Identity(wires=[0]), + "Hadamard": qml.Hadamard(wires=[0]), + "Hermitian": qml.Hermitian(np.eye(2), wires=[0]), + "PauliX": qml.PauliX(0), + "PauliY": qml.PauliY(0), + "PauliZ": qml.PauliZ(0), + "X": qml.X(0), + "Y": qml.Y(0), + "Z": qml.Z(0), + "Projector": [ + qml.Projector(np.array([1]), wires=[0]), + qml.Projector(np.array([0, 1]), wires=[0]), + ], + "SparseHamiltonian": qml.SparseHamiltonian(csr_matrix(np.eye(8)), wires=[0, 1, 2]), + "Hamiltonian": qml.Hamiltonian([1, 1], [qml.Z(0), qml.X(0)]), + "LinearCombination": qml.ops.LinearCombination([1, 1], [qml.Z(0), qml.X(0)]), +} + +all_obs = obs.keys() + @pytest.mark.parametrize("backend", ["quimb"]) @pytest.mark.parametrize("method", ["mps"]) @@ -38,14 +142,95 @@ class TestQuimbMPS: @pytest.mark.parametrize("num_wires", [None, 4]) @pytest.mark.parametrize("c_dtype", [np.complex64, np.complex128]) - def test_device_init(self, num_wires, c_dtype, backend, method): - """Test the class initialization and returned properties.""" + @pytest.mark.parametrize("max_bond_dim", [None, 10]) + @pytest.mark.parametrize("cutoff", [1e-16, 1e-12]) + @pytest.mark.parametrize("contract", ["auto-mps", "nonlocal"]) + def test_device_init(self, num_wires, c_dtype, backend, method, max_bond_dim, cutoff, contract): + """Test the class initialization with different arguments and returned properties.""" + + kwargs = {"max_bond_dim": max_bond_dim, "cutoff": cutoff, "contract": contract} wires = Wires(range(num_wires)) if num_wires else None - dev = LightningTensor(wires=wires, backend=backend, method=method, c_dtype=c_dtype) + dev = LightningTensor( + wires=wires, backend=backend, method=method, c_dtype=c_dtype, **kwargs + ) assert isinstance(dev._interface.state, qtn.MatrixProductState) assert isinstance(dev._interface.state_to_array(), np.ndarray) _, config = dev.preprocess() + assert config.device_options["c_dtype"] == c_dtype assert config.device_options["backend"] == backend assert config.device_options["method"] == method + assert config.device_options["max_bond_dim"] == max_bond_dim + assert config.device_options["cutoff"] == cutoff + assert config.device_options["contract"] == contract + + @pytest.mark.parametrize("operation", all_ops) + def test_supported_gates_can_be_implemented(self, operation, backend, method): + """Test that the interface can implement all its supported gates.""" + + dev = LightningTensor( + wires=Wires(range(4)), backend=backend, method=method, c_dtype=np.complex64 + ) + + tape = qml.tape.QuantumScript( + [ops[operation]], + [qml.expval(qml.Identity(wires=0))], + ) + + result = dev.execute(circuits=tape) + assert np.allclose(result, 1.0) + + @pytest.mark.parametrize("observable", all_obs) + def test_supported_observables_can_be_implemented(self, observable, backend, method): + """Test that the interface can implement all its supported observables.""" + dev = LightningTensor( + wires=Wires(range(3)), backend=backend, method=method, c_dtype=np.complex64 + ) + + if observable == "Projector": + for o in obs[observable]: + tape = qml.tape.QuantumScript( + [qml.PauliX(0)], + [qml.expval(o)], + ) + result = dev.execute(circuits=tape) + assert isinstance(result, (float, np.ndarray)) + + else: + + tape = qml.tape.QuantumScript( + [qml.PauliX(0)], + [qml.expval(obs[observable])], + ) + result = dev.execute(circuits=tape) + assert isinstance(result, (float, np.ndarray)) + + def test_not_implemented_meas(self, backend, method): + """Tests that support only exists for `qml.expval` and `qml.var` so far.""" + + ops = [qml.Identity(0)] + measurements = [qml.probs(qml.PauliZ(0))] + tape = qml.tape.QuantumScript(ops, measurements) + + dev = LightningTensor( + wires=tape.wires, backend=backend, method=method, c_dtype=np.complex64 + ) + + with pytest.raises(NotImplementedError): + dev.execute(tape) + + def test_not_implemented_shots(self, backend, method): + """Tests that this interface does not support measurements with finite shots.""" + + ops = [qml.Identity(0)] + measurements = [qml.expval(qml.PauliZ(0))] + tape = qml.tape.QuantumScript(ops, measurements) + tape._shots = 5 + + dev = LightningTensor( + wires=tape.wires, backend=backend, method=method, c_dtype=np.complex64 + ) + + with pytest.raises(NotImplementedError): + dev.execute(tape) diff --git a/tests/lightning_tensor/test_var.py b/tests/lightning_tensor/test_var.py new file mode 100644 index 0000000000..2cf3a50bdf --- /dev/null +++ b/tests/lightning_tensor/test_var.py @@ -0,0 +1,407 @@ +# 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. +""" +Tests for the variance calculation on the LightningTensor device. +""" +import numpy as np +import pennylane as qml +import pytest +from conftest import PHI, THETA, VARPHI, LightningDevice +from pennylane.tape import QuantumScript + +if not LightningDevice._new_API: + pytest.skip("Exclusive tests for new API. Skipping.", allow_module_level=True) + +if LightningDevice._CPP_BINARY_AVAILABLE: + pytest.skip("Device doesn't have C++ support yet.", allow_module_level=True) + + +from pennylane_lightning.lightning_tensor import LightningTensor + + +@pytest.fixture(params=[np.complex64, np.complex128]) +def dev(request): + return LightningTensor(wires=3, c_dtype=request.param) + + +def calculate_reference(tape): + dev = qml.device("default.qubit", max_workers=1) + program, _ = dev.preprocess() + tapes, transf_fn = program([tape]) + results = dev.execute(tapes) + return transf_fn(results) + + +def execute(dev, tape): + results = dev.execute(tape) + return results + + +@pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) +class TestVar: + """Tests for the variance""" + + def test_Identity(self, theta, phi, dev): + """Tests applying identities.""" + + ops = [ + qml.Identity(0), + qml.Identity((0, 1)), + qml.RX(theta, 0), + qml.Identity((1, 2)), + qml.RX(phi, 1), + ] + measurements = [qml.var(qml.PauliZ(0))] + tape = qml.tape.QuantumScript(ops, measurements) + + result = dev.execute(tape) + expected = 1 - np.cos(theta) ** 2 + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(result, expected, atol=tol, rtol=0) + + def test_identity_variance(self, theta, phi, dev): + """Tests identity variances.""" + + tape = qml.tape.QuantumScript( + [qml.RX(theta, wires=[0]), qml.RX(phi, wires=[1]), qml.CNOT(wires=[0, 1])], + [qml.var(qml.Identity(wires=[0])), qml.var(qml.Identity(wires=[1]))], + ) + result = dev.execute(tape) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + assert np.allclose(0.0, result, atol=tol, rtol=0) + + def test_multi_wire_identity_variance(self, theta, phi, dev): + """Tests multi-wire identity.""" + + tape = qml.tape.QuantumScript( + [qml.RX(theta, wires=[0]), qml.RX(phi, wires=[1]), qml.CNOT(wires=[0, 1])], + [qml.var(qml.Identity(wires=[0, 1]))], + ) + result = dev.execute(tape) + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + assert np.allclose(0.0, result, atol=tol, rtol=0) + + @pytest.mark.parametrize( + "wires", + [([0, 1]), (["a", 1]), (["b", "a"]), ([-1, 2.5])], + ) + def test_custom_wires(self, theta, phi, wires): + """Tests custom wires.""" + device = LightningTensor(wires=wires, c_dtype=np.complex128) + + tape = qml.tape.QuantumScript( + [ + qml.RX(theta, wires=wires[0]), + qml.RX(phi, wires=wires[1]), + qml.CNOT(wires=wires), + ], + [qml.var(qml.PauliZ(wires=wires[0])), qml.var(qml.PauliZ(wires=wires[1]))], + ) + + calculated_val = execute(device, tape) + reference_val = np.array( + [1 - np.cos(theta) ** 2, 1 - np.cos(theta) ** 2 * np.cos(phi) ** 2] + ) + + tol = 1e-5 if device.c_dtype == np.complex64 else 1e-7 + assert np.allclose(calculated_val, reference_val, atol=tol, rtol=0) + + @pytest.mark.parametrize( + "Obs, Op, expected_fn", + [ + ( + [qml.PauliX(wires=[0]), qml.PauliX(wires=[1])], + qml.RY, + lambda theta, phi: np.array( + [1 - np.sin(theta) ** 2 * np.sin(phi) ** 2, 1 - np.sin(phi) ** 2] + ), + ), + ( + [qml.PauliY(wires=[0]), qml.PauliY(wires=[1])], + qml.RX, + lambda theta, phi: np.array([1, 1 - np.cos(theta) ** 2 * np.sin(phi) ** 2]), + ), + ( + [qml.PauliZ(wires=[0]), qml.PauliZ(wires=[1])], + qml.RX, + lambda theta, phi: np.array( + [1 - np.cos(theta) ** 2, 1 - np.cos(theta) ** 2 * np.cos(phi) ** 2] + ), + ), + ( + [qml.Hadamard(wires=[0]), qml.Hadamard(wires=[1])], + qml.RY, + lambda theta, phi: np.array( + [ + 1 - (np.sin(theta) * np.sin(phi) + np.cos(theta)) ** 2 / 2, + 1 - (np.cos(theta) * np.cos(phi) + np.sin(phi)) ** 2 / 2, + ] + ), + ), + ], + ) + def test_single_wire_observables_variance(self, Obs, Op, expected_fn, theta, phi, dev): + """Test that variance values for single wire observables are correct""" + + tape = qml.tape.QuantumScript( + [Op(theta, wires=[0]), Op(phi, wires=[1]), qml.CNOT(wires=[0, 1])], + [qml.var(Obs[0]), qml.var(Obs[1])], + ) + result = execute(dev, tape) + expected = expected_fn(theta, phi) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + assert np.allclose(result, expected, atol=tol, rtol=0) + + def test_hermitian_variance(self, theta, phi, dev): + """Tests an Hermitian operator.""" + + with qml.tape.QuantumTape() as tape: + qml.RX(theta, wires=0) + qml.RX(phi, wires=1) + qml.RX(theta + phi, wires=2) + + for idx in range(3): + qml.var(qml.Hermitian([[1, 0], [0, -1]], wires=[idx])) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + assert np.allclose(calculated_val, reference_val, atol=tol, rtol=0) + + def test_hamiltonian_variance(self, theta, phi, dev): + """Tests a Hamiltonian.""" + + ham = qml.Hamiltonian( + [1.0, 0.3, 0.3, 0.4], + [ + qml.PauliX(0) @ qml.PauliX(1), + qml.PauliZ(0), + qml.PauliZ(1), + qml.PauliX(0) @ qml.PauliY(1), + ], + ) + + with qml.tape.QuantumTape() as tape1: + qml.RX(theta, wires=0) + qml.RX(phi, wires=1) + qml.RX(theta + phi, wires=2) + + qml.var(ham) + + tape2 = QuantumScript(tape1.operations, [qml.var(qml.dot(ham.coeffs, ham.ops))]) + + calculated_val = execute(dev, tape1) + reference_val = calculate_reference(tape2) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + assert np.allclose(calculated_val, reference_val, atol=tol, rtol=0) + + def test_sparse_hamiltonian_variance(self, theta, phi, dev): + """Tests a Hamiltonian.""" + + ham = qml.SparseHamiltonian( + qml.Hamiltonian( + [1.0, 0.3, 0.3, 0.4], + [ + qml.PauliX(0) @ qml.PauliX(1), + qml.PauliZ(0), + qml.PauliZ(1), + qml.PauliX(0) @ qml.PauliY(1), + ], + ).sparse_matrix(), + wires=[0, 1], + ) + + with qml.tape.QuantumTape() as tape1: + qml.RX(theta, wires=0) + qml.RX(phi, wires=1) + + qml.var(ham) + + tape2 = QuantumScript( + tape1.operations, [qml.var(qml.Hermitian(ham.matrix(), wires=[0, 1]))] + ) + + calculated_val = execute(dev, tape1) + reference_val = calculate_reference(tape2) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + assert np.allclose(calculated_val, reference_val, atol=tol, rtol=0) + + +@pytest.mark.parametrize("phi", PHI) +class TestOperatorArithmetic: + """Test integration with SProd, Prod, and Sum.""" + + @pytest.mark.parametrize( + "obs", + [ + qml.s_prod(0.5, qml.PauliZ(0)), + qml.prod(qml.PauliZ(0), qml.PauliX(1)), + qml.sum(qml.PauliZ(0), qml.PauliX(1)), + ], + ) + def test_op_math(self, phi, dev, obs, tol): + """Tests the `SProd`, `Prod`, and `Sum` classes.""" + + tape = qml.tape.QuantumScript( + [ + qml.RX(phi, wires=[0]), + qml.Hadamard(wires=[1]), + qml.PauliZ(wires=[1]), + qml.RX(-1.1 * phi, wires=[1]), + ], + [qml.var(obs)], + ) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + def test_integration(self, phi, dev, tol): + """Test a Combination of `Sum`, `SProd`, and `Prod`.""" + + obs = qml.sum( + qml.s_prod(2.3, qml.PauliZ(0)), + -0.5 * qml.prod(qml.PauliY(0), qml.PauliZ(1)), + ) + + tape = qml.tape.QuantumScript( + [qml.RX(phi, wires=[0]), qml.RX(-1.1 * phi, wires=[0])], + [qml.var(obs)], + ) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + +@pytest.mark.parametrize("theta, phi, varphi", list(zip(THETA, PHI, VARPHI))) +class TestTensorVar: + """Test tensor variances""" + + def test_PauliX_PauliY(self, theta, phi, varphi, dev, tol): + """Tests a tensor product involving PauliX and PauliY.""" + + with qml.tape.QuantumTape() as tape: + qml.RX(theta, wires=[0]) + qml.RX(phi, wires=[1]) + qml.RX(varphi, wires=[2]) + qml.CNOT(wires=[0, 1]) + qml.CNOT(wires=[1, 2]) + qml.var(qml.PauliX(0) @ qml.PauliY(2)) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + def test_PauliZ_identity(self, theta, phi, varphi, dev, tol): + """Tests a tensor product involving PauliZ and Identity.""" + + with qml.tape.QuantumTape() as tape: + qml.Identity(wires=[0]) + qml.RX(theta, wires=[0]) + qml.RX(phi, wires=[1]) + qml.RX(varphi, wires=[2]) + qml.CNOT(wires=[0, 1]) + qml.CNOT(wires=[1, 2]) + qml.var(qml.PauliZ(0) @ qml.Identity(1) @ qml.PauliZ(2)) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + def test_PauliZ_hadamard_PauliY(self, theta, phi, varphi, dev, tol): + """Tests a tensor product involving PauliY, PauliZ and Hadamard.""" + + with qml.tape.QuantumTape() as tape: + qml.RX(theta, wires=[0]) + qml.RX(phi, wires=[1]) + qml.RX(varphi, wires=[2]) + qml.CNOT(wires=[0, 1]) + qml.CNOT(wires=[1, 2]) + qml.var(qml.PauliZ(0) @ qml.Hadamard(1) @ qml.PauliY(2)) + + calculated_val = execute(dev, tape) + reference_val = calculate_reference(tape) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + +# Define the parameter values +THETA = np.linspace(0.11, 1, 3) +PHI = np.linspace(0.32, 1, 3) + + +@pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) +def test_multi_qubit_gates(theta, phi, dev, tol): + """Tests a simple circuit with multi-qubit gates.""" + + ops = [ + qml.PauliX(wires=[0]), + qml.RX(theta, wires=[0]), + qml.CSWAP(wires=[7, 0, 5]), + qml.RX(phi, wires=[1]), + qml.CNOT(wires=[3, 4]), + qml.DoubleExcitation(phi, wires=[1, 2, 3, 4]), + qml.CZ(wires=[4, 5]), + qml.Hadamard(wires=[4]), + qml.CCZ(wires=[0, 1, 2]), + qml.CSWAP(wires=[2, 3, 4]), + qml.QFT(wires=[0, 1, 2]), + qml.CNOT(wires=[2, 4]), + qml.Toffoli(wires=[0, 1, 2]), + qml.DoubleExcitation(phi, wires=[0, 1, 3, 4]), + ] + + meas = [ + qml.var(qml.PauliY(wires=[2])), + qml.var(qml.Hamiltonian([1, 5, 6], [qml.Z(6), qml.X(0), qml.Hadamard(4)])), + qml.var( + qml.Hamiltonian( + [4, 5, 7], + [ + qml.Z(6) @ qml.Y(4), + qml.X(0), + qml.Hadamard(7), + ], + ) + ), + ] + + tape = qml.tape.QuantumScript(ops=ops, measurements=meas) + + reference_val = calculate_reference(tape) + dev = LightningTensor(wires=tape.wires, c_dtype=np.complex128) + calculated_val = dev.execute(tape) + + assert np.allclose(calculated_val, reference_val) From 5c4606469d19e97475008c6ce7bafd167b1e3576 Mon Sep 17 00:00:00 2001 From: Shuli Shu <08cnbj@gmail.com> Date: Wed, 1 May 2024 14:59:28 -0400 Subject: [PATCH 2/4] add changelog --- .github/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index a55ef60238..9d4816c9d0 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -31,6 +31,9 @@ Amintor Dusko, Pietropaolo Frisoni, Vincent Michaud-Rioux ### New features since last release +* Add `cutensornet` backed `MPS` C++ layer to `lightning.tensor`. + [(#704)](https://github.com/PennyLaneAI/pennylane-lightning/pull/704) + * Add Python class for the `lightning.tensor` device which uses the new device API and the interface for `quimb` based on the MPS method. [(#671)](https://github.com/PennyLaneAI/pennylane-lightning/pull/671) From 0c06808d917db70471eb625afdf59f590fcb6957 Mon Sep 17 00:00:00 2001 From: ringo-but-quantum Date: Tue, 7 May 2024 22:54:50 +0000 Subject: [PATCH 3/4] Auto update version from '0.37.0-dev3' to '0.37.0-dev4' --- pennylane_lightning/core/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 82031f4caa..b46423dcc8 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.37.0-dev3" +__version__ = "0.37.0-dev4" From 03b04349e7f1503aecbae6a5f96dc36781a76323 Mon Sep 17 00:00:00 2001 From: Shuli Shu <08cnbj@gmail.com> Date: Thu, 9 May 2024 01:50:32 +0000 Subject: [PATCH 4/4] fix multiple reset()/setbasisstate() & resolve comments --- .github/workflows/tests_lmps_tncuda_cpp.yml | 15 ++++--- .../lightning_tensor/tensor/CMakeLists.txt | 2 +- .../tensor/base/TensorBase.hpp | 17 ++++---- .../tensor/tncuda/TensorCuda.hpp | 7 ++-- .../tensor/tncuda/tests/Tests_TensorCuda.cpp | 2 +- .../lightning_tensor/tncuda/MPSTNCuda.hpp | 42 +++++-------------- .../lightning_tensor/tncuda/TNCudaBase.hpp | 8 ++-- .../tncuda/tests/Tests_MPSTNCuda.cpp | 25 ++++++++++- .../utils/tncuda_utils/tncuda_helpers.hpp | 2 - 9 files changed, 61 insertions(+), 59 deletions(-) diff --git a/.github/workflows/tests_lmps_tncuda_cpp.yml b/.github/workflows/tests_lmps_tncuda_cpp.yml index 29c07d8c3e..d65f43f3f3 100644 --- a/.github/workflows/tests_lmps_tncuda_cpp.yml +++ b/.github/workflows/tests_lmps_tncuda_cpp.yml @@ -70,7 +70,7 @@ jobs: pl_tensor_backend: ["cutensornet"] cuda_version: ["12"] - name: C++ tests (Lightning-Tensor-MPS-TNCuda) + name: C++ Tests (${{ matrix.pl_backend }}, method-${{ matrix.pl_tensor_method }}, backend-${{ matrix.pl_tensor_backend }}, cuda-${{ matrix.cuda_version }}) runs-on: - ${{ matrix.os }} - self-hosted @@ -95,7 +95,9 @@ jobs: name: Install Python with: python-version: '3.9' - + + # Since the self-hosted runner can be re-used. It is best to set up all package + # installations in a virtual environment that gets cleaned at the end of each workflow run - name: Setup Python virtual environment id: setup_venv env: @@ -103,13 +105,17 @@ jobs: run: | # Clear any pre-existing venvs rm -rf venv_* + # Create new venv for this workflow_run python --version python -m venv ${{ env.VENV_NAME }} + # Add the venv to PATH for subsequent steps echo ${{ env.VENV_NAME }}/bin >> $GITHUB_PATH + # Adding venv name as an output for subsequent steps to reference if needed echo "venv_name=${{ env.VENV_NAME }}" >> $GITHUB_OUTPUT + - name: Display Python-Path id: python_path run: | @@ -185,8 +191,5 @@ jobs: if: always() run: | rm -rf ${{ steps.setup_venv.outputs.venv_name }} - rm -rf * - rm -rf .git - rm -rf .gitignore - rm -rf .github + rm -rf * .git .gitignore .github pip cache purge diff --git a/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/CMakeLists.txt b/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/CMakeLists.txt index 0686a86ab9..59ed74f34e 100644 --- a/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/CMakeLists.txt +++ b/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/CMakeLists.txt @@ -17,4 +17,4 @@ endif() target_include_directories(${PL_BACKEND}_tensor INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/base) target_link_libraries(${PL_BACKEND}_tensor INTERFACE tensorBase lightning_utils lightning_compile_options lightning_external_libs) -set_property(TARGET ${PL_BACKEND}_tensor PROPERTY POSITION_INDEPENDENT_CODE ON) \ No newline at end of file +set_property(TARGET ${PL_BACKEND}_tensor PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/base/TensorBase.hpp b/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/base/TensorBase.hpp index d737c39d1a..88d9cd153d 100644 --- a/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/base/TensorBase.hpp +++ b/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/base/TensorBase.hpp @@ -33,21 +33,20 @@ namespace Pennylane::LightningTensor { */ template class TensorBase { private: - std::size_t rank_; // A rank N tensor has N modes - std::size_t length_; // Number of elements - std::vector modes_; // modes for contraction identify - std::vector extents_; // Number of elements in each mode + const std::size_t rank_; // A rank N tensor has N modes + std::size_t length_; // Number of elements + const std::vector modes_; // modes for contraction identify + const std::vector extents_; // Number of elements in each mode public: - TensorBase(std::size_t rank, const std::vector &modes, - const std::vector &extents) + explicit TensorBase(std::size_t rank, const std::vector &modes, + const std::vector &extents) : rank_(rank), modes_(modes), extents_(extents) { PL_ABORT_IF_NOT(rank_ == extents_.size(), "Please check if rank or extents are set correctly."); length_ = 1; - for (auto extent : extents) { - length_ *= extent; - } + length_ = std::accumulate(extents.begin(), extents.end(), + std::size_t{1}, std::multiplies<>()); } ~TensorBase() {} diff --git a/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/tncuda/TensorCuda.hpp b/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/tncuda/TensorCuda.hpp index c1258d83d8..8cae9e8501 100644 --- a/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/tncuda/TensorCuda.hpp +++ b/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/tncuda/TensorCuda.hpp @@ -46,9 +46,10 @@ class TensorCuda final : public TensorBase> { using BaseType = TensorBase; using CFP_t = decltype(cuUtil::getCudaType(PrecisionT{})); - TensorCuda(const std::size_t rank, const std::vector &modes, - const std::vector &extents, - const DevTag &dev_tag, bool device_alloc = true) + explicit TensorCuda(const std::size_t rank, + const std::vector &modes, + const std::vector &extents, + const DevTag &dev_tag, bool device_alloc = true) : TensorBase>(rank, modes, extents), data_buffer_{std::make_shared>( BaseType::getLength(), dev_tag, device_alloc)} {} diff --git a/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/tncuda/tests/Tests_TensorCuda.cpp b/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/tncuda/tests/Tests_TensorCuda.cpp index b34055ef93..c2713bf725 100644 --- a/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/tncuda/tests/Tests_TensorCuda.cpp +++ b/pennylane_lightning/core/src/simulators/lightning_tensor/tensor/tncuda/tests/Tests_TensorCuda.cpp @@ -67,4 +67,4 @@ TEMPLATE_TEST_CASE("TensorCuda::baseMethods", "[TensorCuda]", float, double) { SECTION("getExtents()") { CHECK(tensor.getExtents() == extents); } SECTION("getLength()") { CHECK(tensor.getLength() == length); } -} \ No newline at end of file +} diff --git a/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/MPSTNCuda.hpp b/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/MPSTNCuda.hpp index d3f22e6fc1..8d705ac5d4 100644 --- a/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/MPSTNCuda.hpp +++ b/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/MPSTNCuda.hpp @@ -61,7 +61,6 @@ class MPSTNCuda final : public TNCudaBase> { using BaseType = TNCudaBase; MPSStatus MPSInitialized_ = MPSStatus::MPSInitNotSet; - MPSStatus MPSFinalized_ = MPSStatus::MPSFinalizedNotSet; const std::size_t maxBondDim_; @@ -161,11 +160,6 @@ class MPSTNCuda final : public TNCudaBase> { "Please ensure all elements of a basis state should be " "either 0 or 1."); - PL_ABORT_IF(MPSInitialized_ == MPSStatus::MPSInitSet, - "setBasisState() can be called only once."); - - MPSInitialized_ = MPSStatus::MPSInitSet; - CFP_t value_cu = Pennylane::LightningGPU::Util::complexToCu({1.0, 0.0}); @@ -186,8 +180,10 @@ class MPSTNCuda final : public TNCudaBase> { &value_cu, sizeof(CFP_t), cudaMemcpyHostToDevice)); } - updateQuantumStateMPS_(getSitesExtentsPtr().data(), - getTensorsDataPtr().data()); + if (MPSInitialized_ == MPSStatus::MPSInitNotSet) { + MPSInitialized_ = MPSStatus::MPSInitSet; + updateQuantumStateMPS_(); + } }; /** @@ -201,13 +197,6 @@ class MPSTNCuda final : public TNCudaBase> { * quantum state on host */ auto getDataVector() -> std::vector { - PL_ABORT_IF(MPSFinalized_ == MPSStatus::MPSFinalizedSet, - "getDataVector() method to return the full state " - "vector can't be called " - "after cutensornetStateFinalizeMPS is called"); - - MPSFinalized_ = MPSStatus::MPSFinalizedSet; - // 1D representation std::vector output_modes(std::size_t{1}, std::size_t{1}); std::vector output_extent( @@ -216,17 +205,10 @@ class MPSTNCuda final : public TNCudaBase> { output_extent, BaseType::getDevTag()); - std::vector output_tensorPtr( - std::size_t{1}, - static_cast(output_tensor.getDataBuffer().getData())); - - std::vector output_extentsPtr; - std::vector extent_int64( - std::size_t{1}, - static_cast(std::size_t{1} << BaseType::getNumQubits())); - output_extentsPtr.emplace_back(extent_int64.data()); + void *output_tensorPtr[] = { + static_cast(output_tensor.getDataBuffer().getData())}; - this->computeState(output_extentsPtr, output_tensorPtr); + this->computeState(output_tensorPtr); std::vector results(output_extent.front()); output_tensor.CopyGpuDataToHost(results.data(), results.size()); @@ -328,19 +310,17 @@ class MPSTNCuda final : public TNCudaBase> { * @brief Update quantumState (cutensornetState_t) with data provided by a * user * - * @param extentsIn Extents of each sites - * @param tensorsIn Pointer to tensors provided by a user */ - void updateQuantumStateMPS_(const int64_t *const *extentsIn, - uint64_t **tensorsIn) { + void updateQuantumStateMPS_() { PL_CUTENSORNET_IS_SUCCESS(cutensornetStateInitializeMPS( /*const cutensornetHandle_t */ BaseType::getTNCudaHandle(), /*cutensornetState_t*/ BaseType::getQuantumState(), /*cutensornetBoundaryCondition_t */ CUTENSORNET_BOUNDARY_CONDITION_OPEN, - /*const int64_t *const* */ extentsIn, + /*const int64_t *const* */ getSitesExtentsPtr().data(), /*const int64_t *const* */ nullptr, - /*void ** */ reinterpret_cast(tensorsIn))); + /*void ** */ + reinterpret_cast(getTensorsDataPtr().data()))); } }; } // namespace Pennylane::LightningTensor::TNCuda diff --git a/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/TNCudaBase.hpp b/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/TNCudaBase.hpp index 6628d94d13..eafcc4b08f 100644 --- a/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/TNCudaBase.hpp +++ b/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/TNCudaBase.hpp @@ -187,11 +187,9 @@ class TNCudaBase : public TensornetBase { /** * @brief Save quantumState information to data provided by a user * - * @param extentsPtr Extents of each sites * @param tensorPtr Pointer to tensors provided by a user */ - void computeState(std::vector &extentsPtr, - std::vector &tensorPtr) { + void computeState(void **tensorPtr) { cutensornetWorkspaceDescriptor_t workDesc; PL_CUTENSORNET_IS_SUCCESS( cutensornetCreateWorkspaceDescriptor(getTNCudaHandle(), &workDesc)); @@ -223,9 +221,9 @@ class TNCudaBase : public TensornetBase { /* const cutensornetHandle_t */ getTNCudaHandle(), /* cutensornetState_t */ getQuantumState(), /* cutensornetWorkspaceDescriptor_t */ workDesc, - /* int64_t * */ extentsPtr.data(), + /* int64_t * */ nullptr, /* int64_t *stridesOut */ nullptr, - /* void * */ tensorPtr.data(), + /* void * */ tensorPtr, /* cudaStream_t */ getDevTag().getStreamID())); PL_CUTENSORNET_IS_SUCCESS( diff --git a/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/tests/Tests_MPSTNCuda.cpp b/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/tests/Tests_MPSTNCuda.cpp index e338d36435..557ad1e0ea 100644 --- a/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/tests/Tests_MPSTNCuda.cpp +++ b/pennylane_lightning/core/src/simulators/lightning_tensor/tncuda/tests/Tests_MPSTNCuda.cpp @@ -135,10 +135,33 @@ TEMPLATE_TEST_CASE("MPSTNCuda::SetBasisStates() & reset()", "[MPSTNCuda]", CHECK(expected_state == Pennylane::Util::approx(mps_state.getDataVector())); } + + SECTION("Test different bondDim and different basisstate & reset()") { + std::size_t bondDim = GENERATE(2, 3, 4, 5); + std::size_t stateIdx = GENERATE(0, 1, 2, 3, 4, 5, 6, 7); + std::size_t num_qubits = 3; + std::size_t maxBondDim = bondDim; + + MPSTNCuda mps_state{num_qubits, maxBondDim}; + + mps_state.setBasisState(basisStates[stateIdx]); + + mps_state.reset(); + + std::vector> expected_state( + size_t{1} << num_qubits, std::complex({0.0, 0.0})); + + std::size_t index = 0; + + expected_state[index] = {1.0, 0.0}; + + CHECK(expected_state == + Pennylane::Util::approx(mps_state.getDataVector())); + } } TEMPLATE_TEST_CASE("MPSTNCuda::getDataVector()", "[MPSTNCuda]", float, double) { - std::size_t num_qubits = 3; + std::size_t num_qubits = 10; std::size_t maxBondDim = 2; DevTag dev_tag{0, 0}; diff --git a/pennylane_lightning/core/src/simulators/lightning_tensor/utils/tncuda_utils/tncuda_helpers.hpp b/pennylane_lightning/core/src/simulators/lightning_tensor/utils/tncuda_utils/tncuda_helpers.hpp index abf98e41f2..0c635d1817 100644 --- a/pennylane_lightning/core/src/simulators/lightning_tensor/utils/tncuda_utils/tncuda_helpers.hpp +++ b/pennylane_lightning/core/src/simulators/lightning_tensor/utils/tncuda_utils/tncuda_helpers.hpp @@ -29,8 +29,6 @@ enum class MPSStatus : uint32_t { BEGIN = 0, MPSInitNotSet = 0, MPSInitSet, - MPSFinalizedNotSet, - MPSFinalizedSet, END };