From ffda6b156f62d92fb042ee23cd4356d19364b456 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Thu, 16 May 2024 15:12:30 -0400 Subject: [PATCH 01/29] WIP [ci skip] --- pennylane/devices/__init__.py | 2 + pennylane/devices/default_tensor.py | 584 ++++++++++++++++++ .../default_tensor/test_default_tensor.py | 121 ++++ tests/devices/default_tensor/test_expval.py | 398 ++++++++++++ .../devices/default_tensor/test_quimb_mps.py | 281 +++++++++ tests/devices/default_tensor/test_var.py | 407 ++++++++++++ 6 files changed, 1793 insertions(+) create mode 100644 pennylane/devices/default_tensor.py create mode 100644 tests/devices/default_tensor/test_default_tensor.py create mode 100644 tests/devices/default_tensor/test_expval.py create mode 100644 tests/devices/default_tensor/test_quimb_mps.py create mode 100644 tests/devices/default_tensor/test_var.py diff --git a/pennylane/devices/__init__.py b/pennylane/devices/__init__.py index ecbb9ef5fe7..d24133e0278 100644 --- a/pennylane/devices/__init__.py +++ b/pennylane/devices/__init__.py @@ -36,6 +36,7 @@ default_qutrit default_qutrit_mixed default_clifford + default_tensor null_qubit tests @@ -158,4 +159,5 @@ def execute(self, circuits, execution_config = qml.devices.DefaultExecutionConfi from .default_clifford import DefaultClifford from .null_qubit import NullQubit from .default_qutrit_mixed import DefaultQutritMixed +from .default_tensor import DefaultTensor from .._device import Device as LegacyDevice diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py new file mode 100644 index 00000000000..ea34ff12545 --- /dev/null +++ b/pennylane/devices/default_tensor.py @@ -0,0 +1,584 @@ +# 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. +""" +This module contains the default.tensor device to perform tensor network simulation of a quantum circuit. +""" +import copy +from dataclasses import replace +from numbers import Number +from typing import Callable, Optional, Sequence, Tuple, Union + +import numpy as np +import quimb.tensor as qtn + +import pennylane as qml +from pennylane.devices import DefaultExecutionConfig, Device, ExecutionConfig +from pennylane.devices.modifiers import simulator_tracking, single_tape_support +from pennylane.devices.preprocess import ( + decompose, + validate_device_wires, + validate_measurements, + validate_observables, +) +from pennylane.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 + +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( + { + "PauliX", + "PauliY", + "PauliZ", + "Hadamard", + "Hermitian", + "Identity", + "Projector", + "SparseHamiltonian", + "Hamiltonian", + "LinearCombination", + "Sum", + "SProd", + "Prod", + "Exp", + } +) +# The set of supported observables. + +_methods = frozenset({"mps"}) +# The set of supported methods. + + +def accepted_methods(method: str) -> bool: + """A function that determines whether or not a method is supported by ``default.tensor``.""" + return method in _methods + + +def stopping_condition(op: qml.operation.Operator) -> bool: + """A function that determines if an operation is supported by ``lightning.tensor`` for this interface.""" + return op.name in _operations # pragma: no cover + + +def accepted_observables(obs: qml.operation.Operator) -> bool: + """A function that determines if an observable is supported by ``lightning.tensor`` for this interface.""" + return obs.name in _observables # pragma: no cover + + +@simulator_tracking +@single_tape_support +class DefaultTensor(Device): + """A PennyLane device to perform tensor network operations on a quantum circuit. + + Args: + wires (int, Iterable[Number, str]): Number of wires present on the device, or iterable that + contains unique labels for the wires as numbers (i.e., ``[-1, 0, 2]``) or strings + (``['aux_wire', 'q1', 'q2']``). Default is ``None``. + shots (int, Sequence[int], Sequence[Union[int, Sequence[int]]]): The default number of shots + to use in executions involving this device. Currently, it can only be ``None``, so that computation of + statistics like expectation values and variances is performed analytically. + method (str): Supported method. Currently, only ``mps`` is supported. + dtype (type): Datatype for the tensor representation. Must be one of ``np.complex64`` or ``np.complex128``. + Default is ``np.complex128``. + **kwargs: keyword arguments. The following options are currently supported: + + ``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 ``np.finfo(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 + + # So far we just consider the options for MPS simulator + _device_options = ( + "contract", + "cutoff", + "dtype", + "method", + "max_bond_dim", + ) + + _new_API = True + + # pylint: disable=too-many-arguments + def __init__( + self, + *, + wires=None, + method="mps", + shots=None, + dtype=np.complex128, + **kwargs, + ): + + if not accepted_methods(method): + raise ValueError( + f"Unsupported method: {method}. The currently supported method is mps." + ) + + if shots is not None: + raise ValueError("default.tensor does not support finite shots.") + + if dtype not in [np.complex64, np.complex128]: # pragma: no cover + raise TypeError( + f"Unsupported type: {dtype}. Supported types are np.complex64 and np.complex128." + ) + + super().__init__(wires=wires, shots=shots) + + self._method = method + self._dtype = dtype + + # options for MPS + self._max_bond_dim = kwargs.get("max_bond_dim", None) + self._cutoff = kwargs.get("cutoff", np.finfo(self._dtype).eps) + self._contract = kwargs.get("contract", "auto-mps") + + device_options = self._setup_execution_config().device_options + + self._init_state_opts = { + "binary": "0" if self._wires is None else "0" * max(1, len(self._wires)), + "dtype": self._dtype.__name__, + "tags": None if self._wires is None else [str(l) for l in self._wires.labels], + } + + self._gate_opts = { + "parametrize": None, + "contract": device_options["contract"], + "cutoff": device_options["cutoff"], + "max_bond": device_options["max_bond_dim"], + } + + self._expval_opts = { + "dtype": self._dtype.__name__, + "simplify_sequence": "ADCRS", + "simplify_atol": 0.0, + } + + self._circuitMPS = qtn.CircuitMPS(psi0=self._initial_mps()) + + for arg in kwargs: + if arg not in self._device_options: + raise TypeError( + f"Unexpected argument: {arg} during initialization of the default.tensor device." + ) + + @property + def name(self): + """The name of the device.""" + return "default.tensor" + + @property + def method(self): + """Supported method.""" + return self._method + + @property + def dtype(self): + """Tensor complex data type.""" + return self._dtype + + def _reset_state(self) -> None: + """Reset the MPS.""" + self._circuitMPS = qtn.CircuitMPS(psi0=self._initial_mps()) + + def _initial_mps(self) -> qtn.MatrixProductState: + r""" + 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 _setup_execution_config( + self, config: Optional[ExecutionConfig] = DefaultExecutionConfig + ) -> ExecutionConfig: + """ + Update the execution config with choices for how the device should be used and the device options. + """ + # TODO: add options for gradients next quarter + + updated_values = {} + + new_device_options = dict(config.device_options) + for option in self._device_options: + if option not in new_device_options: + new_device_options[option] = getattr(self, f"_{option}", None) + + return replace(config, **updated_values, device_options=new_device_options) + + def preprocess( + self, + execution_config: ExecutionConfig = DefaultExecutionConfig, + ): + """This function defines the device transform program to be applied and an updated device configuration. + + Args: + execution_config (Union[ExecutionConfig, Sequence[ExecutionConfig]]): A data structure describing the + parameters needed to fully describe the execution. + + Returns: + TransformProgram, ExecutionConfig: A transform program that when called returns :class:`~.QuantumTape`'s that the + device can natively execute as well as a postprocessing function to be called after execution, and a configuration + with unset specifications filled in. + + This device currently: + + * Does not support finite shots. + * Does not support derivatives. + * Does not support vector-Jacobian products. + """ + + config = self._setup_execution_config(execution_config) + + program = TransformProgram() + + program.add_transform(validate_measurements, name=self.name) + program.add_transform(validate_observables, accepted_observables, name=self.name) + program.add_transform(validate_device_wires, self._wires, name=self.name) + program.add_transform( + decompose, + stopping_condition=stopping_condition, + skip_initial_state_prep=True, + name=self.name, + ) + program.add_transform(qml.transforms.broadcast_expand) + + return program, config + + def execute( + self, + circuits: QuantumTape_or_Batch, + execution_config: ExecutionConfig = DefaultExecutionConfig, + ) -> Result_or_ResultBatch: + """Execute a circuit or a batch of circuits and turn it into results. + + Args: + circuits (Union[QuantumTape, Sequence[QuantumTape]]): the quantum circuits to be executed. + execution_config (ExecutionConfig): a data structure with additional information required for execution. + + Returns: + TensorLike, tuple[TensorLike], tuple[tuple[TensorLike]]: A numeric result of the computation. + """ + + results = [] + for circuit in circuits: + 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 float(np.real(exp_val)) + + # pylint: disable=unused-argument + def supports_derivatives( + self, + execution_config: Optional[ExecutionConfig] = None, + circuit: Optional[qml.tape.QuantumTape] = None, + ) -> bool: + """Check whether or not derivatives are available for a given configuration and circuit. + + Args: + execution_config (ExecutionConfig): The configuration of the desired derivative calculation. + circuit (QuantumTape): An optional circuit to check derivatives support for. + + Returns: + Bool: Whether or not a derivative can be calculated provided the given information. + + """ + return False + + def compute_derivatives( + self, + circuits: QuantumTape_or_Batch, + execution_config: ExecutionConfig = DefaultExecutionConfig, + ): + """Calculate the Jacobian of either a single or a batch of circuits on the device. + + Args: + circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuits to calculate derivatives for. + execution_config (ExecutionConfig): a data structure with all additional information required for execution. + + Returns: + Tuple: The Jacobian for each trainable parameter. + """ + raise NotImplementedError( + "The computation of derivatives has yet to be implemented for the default.tensor device." + ) + + def execute_and_compute_derivatives( + self, + circuits: QuantumTape_or_Batch, + execution_config: ExecutionConfig = DefaultExecutionConfig, + ): + """Compute the results and Jacobians of circuits at the same time. + + Args: + circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuits or batch of circuits. + execution_config (ExecutionConfig): a data structure with all additional information required for execution. + + Returns: + tuple: A numeric result of the computation and the gradient. + """ + raise NotImplementedError( + "The computation of derivatives has yet to be implemented for the default.tensor device." + ) + + # pylint: disable=unused-argument + def supports_vjp( + self, + execution_config: Optional[ExecutionConfig] = None, + circuit: Optional[QuantumTape] = None, + ) -> bool: + """Whether or not this device defines a custom vector-Jacobian product. + + Args: + execution_config (ExecutionConfig): The configuration of the desired derivative calculation. + circuit (QuantumTape): An optional circuit to check derivatives support for. + + Returns: + Bool: Whether or not a derivative can be calculated provided the given information. + """ + return False + + def compute_vjp( + self, + circuits: QuantumTape_or_Batch, + cotangents: Tuple[Number], + execution_config: ExecutionConfig = DefaultExecutionConfig, + ): + r"""The vector-Jacobian product used in reverse-mode differentiation. + + Args: + circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuit or batch of circuits. + cotangents (Tuple[Number, Tuple[Number]]): Gradient-output vector. Must have shape matching the output shape of the + corresponding circuit. If the circuit has a single output, ``cotangents`` may be a single number, not an iterable + of numbers. + execution_config (ExecutionConfig): a data structure with all additional information required for execution. + + Returns: + tensor-like: A numeric result of computing the vector-Jacobian product. + """ + raise NotImplementedError( + "The computation of vector-Jacobian product has yet to be implemented for the default.tensor device." + ) + + def execute_and_compute_vjp( + self, + circuits: QuantumTape_or_Batch, + cotangents: Tuple[Number], + execution_config: ExecutionConfig = DefaultExecutionConfig, + ): + """Calculate both the results and the vector-Jacobian product used in reverse-mode differentiation. + + Args: + circuits (Union[QuantumTape, Sequence[QuantumTape]]): the circuit or batch of circuits to be executed. + cotangents (Tuple[Number, Tuple[Number]]): Gradient-output vector. Must have shape matching the output shape of the + corresponding circuit. + execution_config (ExecutionConfig): a data structure with all additional information required for execution. + + Returns: + Tuple, Tuple: the result of executing the scripts and the numeric result of computing the vector-Jacobian product + """ + raise NotImplementedError( + "The computation of vector-Jacobian product has yet to be implemented for the default.tensor device." + ) diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py new file mode 100644 index 00000000000..6ee483e7913 --- /dev/null +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -0,0 +1,121 @@ +# 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 DefaultTensor class. +""" + + +import numpy as np +import pennylane as qml +import pytest + +from pennylane.wires import Wires + +from pennylane_lightning.lightning_tensor import LightningTensor + + +@pytest.mark.parametrize("num_wires", [None, 4]) +@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, c_dtype=c_dtype) + assert dev.name == "lightning.tensor" + assert dev.c_dtype == c_dtype + assert dev.wires == wires + if num_wires is None: + assert dev.num_wires == 0 + else: + assert dev.num_wires == num_wires + + +@pytest.mark.parametrize("backend", ["fake_backend"]) +def test_invalid_backend(backend): + """Test an invalid backend.""" + with pytest.raises(ValueError, match=f"Unsupported backend: {backend}"): + LightningTensor(backend=backend) + + +@pytest.mark.parametrize("method", ["fake_method"]) +def test_invalid_method(method): + """Test an invalid method.""" + with pytest.raises(ValueError, match=f"Unsupported method: {method}"): + LightningTensor(method=method) + + +def test_invalid_keyword_arg(): + """Test an invalid keyword argument.""" + with pytest.raises( + TypeError, + match=f"Unexpected argument: fake_arg during initialization of the LightningTensor device.", + ): + LightningTensor(fake_arg=None) + + +def test_invalid_shots(): + """Test that an error is raised if finite number of shots are requestd.""" + with pytest.raises(ValueError, match="LightningTensor does not support finite shots."): + LightningTensor(shots=5) + + +def test_support_derivatives(): + """Test that the device does not support derivatives yet.""" + dev = LightningTensor() + assert not dev.supports_derivatives() + + +def test_compute_derivatives(): + """Test that an error is raised if the `compute_derivatives` method is called.""" + dev = LightningTensor() + 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, + 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() + assert not dev.supports_vjp() + + +def test_compute_vjp(): + """Test that an error is raised if `compute_vjp` method is called.""" + dev = LightningTensor() + 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, + 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/devices/default_tensor/test_expval.py b/tests/devices/default_tensor/test_expval.py new file mode 100644 index 00000000000..d32062bfad2 --- /dev/null +++ b/tests/devices/default_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/devices/default_tensor/test_quimb_mps.py b/tests/devices/default_tensor/test_quimb_mps.py new file mode 100644 index 00000000000..8f138fe1cd4 --- /dev/null +++ b/tests/devices/default_tensor/test_quimb_mps.py @@ -0,0 +1,281 @@ +# 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 ``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 import QNode +from pennylane.devices import DefaultQubit +from pennylane.wires import Wires +from scipy.sparse import csr_matrix + +from pennylane_lightning.lightning_tensor import LightningTensor + +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) + +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"]) +class TestQuimbMPS: + """Tests for the MPS method.""" + + @pytest.mark.parametrize("num_wires", [None, 4]) + @pytest.mark.parametrize("c_dtype", [np.complex64, np.complex128]) + @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, **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) + + def test_interface_jax(self, backend, method): + """Test the interface with JAX.""" + + jax = pytest.importorskip("jax") + dev = LightningTensor(wires=qml.wires.Wires(range(1)), backend=backend, method=method) + ref_dev = qml.device("default.qubit.jax", wires=1) + + def circuit(x): + qml.RX(x[1], wires=0) + qml.Rot(x[0], x[1], x[2], wires=0) + return qml.expval(qml.Z(0)) + + weights = jax.numpy.array([0.2, 0.5, 0.1]) + qnode = QNode(circuit, dev, interface="jax") + ref_qnode = QNode(circuit, ref_dev, interface="jax") + + assert np.allclose(qnode(weights), ref_qnode(weights)) + + def test_interface_jax_jit(self, backend, method): + """Test the interface with JAX's JIT compiler.""" + + jax = pytest.importorskip("jax") + dev = LightningTensor(wires=qml.wires.Wires(range(1)), backend=backend, method=method) + + @jax.jit + @qml.qnode(dev, interface="jax") + def circuit(): + qml.Hadamard(0) + return qml.expval(qml.Z(0)) + + assert np.allclose(circuit(), 0.0) + + def test_(self, backend, method): + """...""" + + # jax = pytest.importorskip("jax") + dev = LightningTensor(wires=qml.wires.Wires(range(1)), backend=backend, method=method) + + def circuit(): + qml.RX(0.0, wires=0) + + with pytest.raises(qml.QuantumFunctionError): + QNode(circuit, dev, interface="jax", diff_method="adjoint") diff --git a/tests/devices/default_tensor/test_var.py b/tests/devices/default_tensor/test_var.py new file mode 100644 index 00000000000..2cf3a50bdf1 --- /dev/null +++ b/tests/devices/default_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 f20aea1691d6aecb65a8674a127ff2fc8f4f8ff1 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Thu, 16 May 2024 16:53:52 -0400 Subject: [PATCH 02/29] WIP part 2 [ci skip] --- pennylane/devices/__init__.py | 4 +- pennylane/devices/default_tensor.py | 8 +- setup.py | 1 + .../default_tensor/test_default_tensor.py | 130 +++++++++++++----- tests/devices/default_tensor/test_expval.py | 3 +- .../devices/default_tensor/test_quimb_mps.py | 8 +- tests/devices/default_tensor/test_var.py | 3 +- 7 files changed, 109 insertions(+), 48 deletions(-) diff --git a/pennylane/devices/__init__.py b/pennylane/devices/__init__.py index d24133e0278..a59820cb12f 100644 --- a/pennylane/devices/__init__.py +++ b/pennylane/devices/__init__.py @@ -152,12 +152,12 @@ def execute(self, circuits, execution_config = qml.devices.DefaultExecutionConfi # DefaultQubitTF and DefaultQubitAutograd not imported here since this # would lead to an automatic import of tensorflow and autograd, which are -# not PennyLane core dependencies +# not PennyLane core dependencies. DefaultTensor is not imported here +# since it would lead to an automatic import of quimb. from .default_qubit_legacy import DefaultQubitLegacy from .default_gaussian import DefaultGaussian from .default_mixed import DefaultMixed from .default_clifford import DefaultClifford from .null_qubit import NullQubit from .default_qutrit_mixed import DefaultQutritMixed -from .default_tensor import DefaultTensor from .._device import Device as LegacyDevice diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index ea34ff12545..beaa4b7ebd0 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -20,7 +20,11 @@ from typing import Callable, Optional, Sequence, Tuple, Union import numpy as np -import quimb.tensor as qtn + +try: + import quimb.tensor as qtn +except ImportError as e: + raise ImportError("default.tensor device requires the quimb package") from e import pennylane as qml from pennylane.devices import DefaultExecutionConfig, Device, ExecutionConfig @@ -194,7 +198,7 @@ def __init__( if shots is not None: raise ValueError("default.tensor does not support finite shots.") - if dtype not in [np.complex64, np.complex128]: # pragma: no cover + if dtype not in [np.complex64, np.complex128]: raise TypeError( f"Unsupported type: {dtype}. Supported types are np.complex64 and np.complex128." ) diff --git a/setup.py b/setup.py index d1b84d2de59..2fedecf806f 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,7 @@ "default.qutrit = pennylane.devices.default_qutrit:DefaultQutrit", "default.clifford = pennylane.devices.default_clifford:DefaultClifford", "default.qutrit.mixed = pennylane.devices.default_qutrit_mixed:DefaultQutritMixed", + "default.tensor = pennylane.devices.default_tensor:DefaultTensor", ], "console_scripts": ["pl-device-test=pennylane.devices.tests:cli"], }, diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index 6ee483e7913..448731a18eb 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -17,105 +17,159 @@ import numpy as np -import pennylane as qml import pytest -from pennylane.wires import Wires +import pennylane as qml +from pennylane.devices.default_tensor import DefaultTensor +from pennylane.qnode import QNode + + +def test_name(): + """Test the name of DefaultTensor.""" + assert DefaultTensor().name == "default.tensor" + + +def test_wires(): + """Test that a device can be created with wires.""" + assert DefaultTensor().wires is None + assert DefaultTensor(wires=2).wires == qml.wires.Wires([0, 1]) + assert DefaultTensor(wires=[0, 2]).wires == qml.wires.Wires([0, 2]) -from pennylane_lightning.lightning_tensor import LightningTensor + with pytest.raises(AttributeError): + DefaultTensor().wires = [0, 1] -@pytest.mark.parametrize("num_wires", [None, 4]) -@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, c_dtype=c_dtype) - assert dev.name == "lightning.tensor" - assert dev.c_dtype == c_dtype - assert dev.wires == wires - if num_wires is None: - assert dev.num_wires == 0 - else: - assert dev.num_wires == num_wires +@pytest.mark.parametrize("dtype", [np.complex64, np.complex128]) +def test_data_type(dtype): + """Test the data type.""" + assert DefaultTensor(dtype=dtype).dtype == dtype -@pytest.mark.parametrize("backend", ["fake_backend"]) -def test_invalid_backend(backend): - """Test an invalid backend.""" - with pytest.raises(ValueError, match=f"Unsupported backend: {backend}"): - LightningTensor(backend=backend) +def test_ivalid_data_type(): + """Test that data type can only be np.complex64 or np.complex128.""" + with pytest.raises(TypeError): + DefaultTensor(dtype=float) -@pytest.mark.parametrize("method", ["fake_method"]) -def test_invalid_method(method): +def test_invalid_method(): """Test an invalid method.""" + method = "invalid_method" with pytest.raises(ValueError, match=f"Unsupported method: {method}"): - LightningTensor(method=method) + DefaultTensor(method=method) def test_invalid_keyword_arg(): """Test an invalid keyword argument.""" with pytest.raises( TypeError, - match=f"Unexpected argument: fake_arg during initialization of the LightningTensor device.", + match=f"Unexpected argument: fake_arg during initialization of the default.tensor device.", ): - LightningTensor(fake_arg=None) + DefaultTensor(fake_arg=None) def test_invalid_shots(): """Test that an error is raised if finite number of shots are requestd.""" - with pytest.raises(ValueError, match="LightningTensor does not support finite shots."): - LightningTensor(shots=5) + with pytest.raises(ValueError, match="default.tensor does not support finite shots."): + DefaultTensor(shots=5) + + with pytest.raises(AttributeError): + DefaultTensor().shots = 10 def test_support_derivatives(): """Test that the device does not support derivatives yet.""" - dev = LightningTensor() + dev = DefaultTensor() assert not dev.supports_derivatives() def test_compute_derivatives(): """Test that an error is raised if the `compute_derivatives` method is called.""" - dev = LightningTensor() + dev = DefaultTensor() with pytest.raises( NotImplementedError, - match="The computation of derivatives has yet to be implemented for the lightning.tensor device.", + match="The computation of derivatives has yet to be implemented for the default.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() + dev = DefaultTensor() with pytest.raises( NotImplementedError, - match="The computation of derivatives has yet to be implemented for the lightning.tensor device.", + match="The computation of derivatives has yet to be implemented for the default.tensor device.", ): dev.execute_and_compute_derivatives(circuits=None) def test_supports_vjp(): """Test that the device does not support VJP yet.""" - dev = LightningTensor() + dev = DefaultTensor() assert not dev.supports_vjp() def test_compute_vjp(): """Test that an error is raised if `compute_vjp` method is called.""" - dev = LightningTensor() + dev = DefaultTensor() with pytest.raises( NotImplementedError, - match="The computation of vector-Jacobian product has yet to be implemented for the lightning.tensor device.", + match="The computation of vector-Jacobian product has yet to be implemented for the default.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() + dev = DefaultTensor() with pytest.raises( NotImplementedError, - match="The computation of vector-Jacobian product has yet to be implemented for the lightning.tensor device.", + match="The computation of vector-Jacobian product has yet to be implemented for the default.tensor device.", ): dev.execute_and_compute_vjp(circuits=None, cotangents=None) + + +def test_interface_jax(self, backend, method): + """Test the interface with JAX.""" + + jax = pytest.importorskip("jax") + dev = DefaultTensor(wires=qml.wires.Wires(range(1)), backend=backend, method=method) + ref_dev = qml.device("default.qubit.jax", wires=1) + + def circuit(x): + qml.RX(x[1], wires=0) + qml.Rot(x[0], x[1], x[2], wires=0) + return qml.expval(qml.Z(0)) + + weights = jax.numpy.array([0.2, 0.5, 0.1]) + qnode = QNode(circuit, dev, interface="jax") + ref_qnode = QNode(circuit, ref_dev, interface="jax") + + assert np.allclose(qnode(weights), ref_qnode(weights)) + + +def test_interface_jax_jit(self, backend, method): + """Test the interface with JAX's JIT compiler.""" + + jax = pytest.importorskip("jax") + dev = DefaultTensor(wires=qml.wires.Wires(range(1)), backend=backend, method=method) + + @jax.jit + @qml.qnode(dev, interface="jax") + def circuit(): + qml.Hadamard(0) + return qml.expval(qml.Z(0)) + + assert np.allclose(circuit(), 0.0) + + +def test_(self, backend, method): + """...""" + + # jax = pytest.importorskip("jax") + dev = DefaultTensor(wires=qml.wires.Wires(range(1)), backend=backend, method=method) + + def circuit(): + qml.RX(0.0, wires=0) + + with pytest.raises(qml.QuantumFunctionError): + QNode(circuit, dev, interface="jax", diff_method="adjoint") diff --git a/tests/devices/default_tensor/test_expval.py b/tests/devices/default_tensor/test_expval.py index d32062bfad2..57d08c7c2b3 100644 --- a/tests/devices/default_tensor/test_expval.py +++ b/tests/devices/default_tensor/test_expval.py @@ -16,9 +16,10 @@ """ import numpy as np -import pennylane as qml import pytest from conftest import PHI, THETA, VARPHI, LightningDevice + +import pennylane as qml from pennylane.devices import DefaultQubit if not LightningDevice._new_API: diff --git a/tests/devices/default_tensor/test_quimb_mps.py b/tests/devices/default_tensor/test_quimb_mps.py index 8f138fe1cd4..bc4133e7efe 100644 --- a/tests/devices/default_tensor/test_quimb_mps.py +++ b/tests/devices/default_tensor/test_quimb_mps.py @@ -19,16 +19,16 @@ 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_lightning.lightning_tensor import LightningTensor +from scipy.sparse import csr_matrix + +import pennylane as qml from pennylane import QNode from pennylane.devices import DefaultQubit from pennylane.wires import Wires -from scipy.sparse import csr_matrix - -from pennylane_lightning.lightning_tensor import LightningTensor if not LightningDevice._new_API: pytest.skip("Exclusive tests for new API. Skipping.", allow_module_level=True) diff --git a/tests/devices/default_tensor/test_var.py b/tests/devices/default_tensor/test_var.py index 2cf3a50bdf1..94fbb43fa2a 100644 --- a/tests/devices/default_tensor/test_var.py +++ b/tests/devices/default_tensor/test_var.py @@ -15,9 +15,10 @@ 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 + +import pennylane as qml from pennylane.tape import QuantumScript if not LightningDevice._new_API: From cd07fd11f359fb84d5ae5c49c27f42b87919c6bb Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Fri, 17 May 2024 09:23:46 -0400 Subject: [PATCH 03/29] solving issue in codefactor [ci skip] --- pennylane/devices/default_tensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index beaa4b7ebd0..3889dcfa285 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -23,8 +23,8 @@ try: import quimb.tensor as qtn -except ImportError as e: - raise ImportError("default.tensor device requires the quimb package") from e +except ImportError as err: + raise ImportError("default.tensor device requires the quimb package") from err import pennylane as qml from pennylane.devices import DefaultExecutionConfig, Device, ExecutionConfig @@ -39,7 +39,6 @@ from pennylane.tape import QuantumScript, QuantumTape from pennylane.transforms.core import TransformProgram from pennylane.typing import Result, ResultBatch, TensorLike -from pennylane.wires import Wires Result_or_ResultBatch = Union[Result, ResultBatch] QuantumTapeBatch = Sequence[QuantumTape] From a4ee5b69e27e20501c80f58cf0f980fc3e5385e5 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Fri, 17 May 2024 10:54:46 -0400 Subject: [PATCH 04/29] Setting up base test for `default.tensor` [ci-skip] --- pennylane/devices/__init__.py | 1 - .../default_tensor/test_default_tensor.py | 366 +++++++++++++----- .../devices/default_tensor/test_quimb_mps.py | 281 -------------- 3 files changed, 259 insertions(+), 389 deletions(-) delete mode 100644 tests/devices/default_tensor/test_quimb_mps.py diff --git a/pennylane/devices/__init__.py b/pennylane/devices/__init__.py index a59820cb12f..3da86933daa 100644 --- a/pennylane/devices/__init__.py +++ b/pennylane/devices/__init__.py @@ -36,7 +36,6 @@ default_qutrit default_qutrit_mixed default_clifford - default_tensor null_qubit tests diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index 448731a18eb..707174b425f 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -18,10 +18,106 @@ import numpy as np import pytest +from scipy.sparse import csr_matrix import pennylane as qml from pennylane.devices.default_tensor import DefaultTensor -from pennylane.qnode import QNode + +# 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() def test_name(): @@ -39,6 +135,32 @@ def test_wires(): DefaultTensor().wires = [0, 1] +@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_kwargs(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} + + dev = DefaultTensor(**kwargs) + + _, config = dev.preprocess() + assert config.device_options["method"] == "mps" + assert config.device_options["max_bond_dim"] == max_bond_dim + assert config.device_options["cutoff"] == cutoff + assert config.device_options["contract"] == contract + + +def test_invalid_kwarg(): + """Test an invalid keyword argument.""" + with pytest.raises( + TypeError, + match=f"Unexpected argument: fake_arg during initialization of the default.tensor device.", + ): + DefaultTensor(fake_arg=None) + + @pytest.mark.parametrize("dtype", [np.complex64, np.complex128]) def test_data_type(dtype): """Test the data type.""" @@ -58,15 +180,6 @@ def test_invalid_method(): DefaultTensor(method=method) -def test_invalid_keyword_arg(): - """Test an invalid keyword argument.""" - with pytest.raises( - TypeError, - match=f"Unexpected argument: fake_arg during initialization of the default.tensor device.", - ): - DefaultTensor(fake_arg=None) - - def test_invalid_shots(): """Test that an error is raised if finite number of shots are requestd.""" with pytest.raises(ValueError, match="default.tensor does not support finite shots."): @@ -76,100 +189,139 @@ def test_invalid_shots(): DefaultTensor().shots = 10 -def test_support_derivatives(): - """Test that the device does not support derivatives yet.""" - dev = DefaultTensor() - assert not dev.supports_derivatives() - - -def test_compute_derivatives(): - """Test that an error is raised if the `compute_derivatives` method is called.""" - dev = DefaultTensor() - with pytest.raises( - NotImplementedError, - match="The computation of derivatives has yet to be implemented for the default.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 = DefaultTensor() - with pytest.raises( - NotImplementedError, - match="The computation of derivatives has yet to be implemented for the default.tensor device.", - ): - dev.execute_and_compute_derivatives(circuits=None) - - -def test_supports_vjp(): - """Test that the device does not support VJP yet.""" - dev = DefaultTensor() - assert not dev.supports_vjp() - - -def test_compute_vjp(): - """Test that an error is raised if `compute_vjp` method is called.""" - dev = DefaultTensor() - with pytest.raises( - NotImplementedError, - match="The computation of vector-Jacobian product has yet to be implemented for the default.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 = DefaultTensor() - with pytest.raises( - NotImplementedError, - match="The computation of vector-Jacobian product has yet to be implemented for the default.tensor device.", - ): - dev.execute_and_compute_vjp(circuits=None, cotangents=None) - - -def test_interface_jax(self, backend, method): - """Test the interface with JAX.""" - - jax = pytest.importorskip("jax") - dev = DefaultTensor(wires=qml.wires.Wires(range(1)), backend=backend, method=method) - ref_dev = qml.device("default.qubit.jax", wires=1) - - def circuit(x): - qml.RX(x[1], wires=0) - qml.Rot(x[0], x[1], x[2], wires=0) - return qml.expval(qml.Z(0)) - - weights = jax.numpy.array([0.2, 0.5, 0.1]) - qnode = QNode(circuit, dev, interface="jax") - ref_qnode = QNode(circuit, ref_dev, interface="jax") - - assert np.allclose(qnode(weights), ref_qnode(weights)) - - -def test_interface_jax_jit(self, backend, method): - """Test the interface with JAX's JIT compiler.""" - - jax = pytest.importorskip("jax") - dev = DefaultTensor(wires=qml.wires.Wires(range(1)), backend=backend, method=method) - - @jax.jit - @qml.qnode(dev, interface="jax") - def circuit(): - qml.Hadamard(0) - return qml.expval(qml.Z(0)) - - assert np.allclose(circuit(), 0.0) - - -def test_(self, backend, method): - """...""" - - # jax = pytest.importorskip("jax") - dev = DefaultTensor(wires=qml.wires.Wires(range(1)), backend=backend, method=method) - - def circuit(): - qml.RX(0.0, wires=0) - - with pytest.raises(qml.QuantumFunctionError): - QNode(circuit, dev, interface="jax", diff_method="adjoint") +class TestSupportedGatesAndObservables: + """Tests for the MPS method.""" + + @pytest.mark.parametrize("operation", all_ops) + def test_supported_gates_can_be_implemented(self, operation): + """Test that the interface can implement all its supported gates.""" + + dev = DefaultTensor(wires=qml.wires.Wires(range(4)), method="mps") + + 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): + """Test that the interface can implement all its supported observables.""" + + dev = DefaultTensor(wires=qml.wires.Wires(range(3)), method="mps") + + 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): + """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 = DefaultTensor(wires=tape.wires) + + with pytest.raises(NotImplementedError): + dev.execute(tape) + + +class TestSupportsDerivatives: + """Test that DefaultTensor states what kind of derivatives it supports.""" + + def test_support_derivatives(self): + """Test that the device does not support derivatives yet.""" + dev = DefaultTensor() + assert not dev.supports_derivatives() + + def test_compute_derivatives(self): + """Test that an error is raised if the `compute_derivatives` method is called.""" + dev = DefaultTensor() + with pytest.raises( + NotImplementedError, + match="The computation of derivatives has yet to be implemented for the default.tensor device.", + ): + dev.compute_derivatives(circuits=None) + + def test_execute_and_compute_derivatives(self): + """Test that an error is raised if `execute_and_compute_derivative` method is called.""" + dev = DefaultTensor() + with pytest.raises( + NotImplementedError, + match="The computation of derivatives has yet to be implemented for the default.tensor device.", + ): + dev.execute_and_compute_derivatives(circuits=None) + + def test_supports_vjp(self): + """Test that the device does not support VJP yet.""" + dev = DefaultTensor() + assert not dev.supports_vjp() + + def test_compute_vjp(self): + """Test that an error is raised if `compute_vjp` method is called.""" + dev = DefaultTensor() + with pytest.raises( + NotImplementedError, + match="The computation of vector-Jacobian product has yet to be implemented for the default.tensor device.", + ): + dev.compute_vjp(circuits=None, cotangents=None) + + def test_execute_and_compute_vjp(self): + """Test that an error is raised if `execute_and_compute_vjp` method is called.""" + dev = DefaultTensor() + with pytest.raises( + NotImplementedError, + match="The computation of vector-Jacobian product has yet to be implemented for the default.tensor device.", + ): + dev.execute_and_compute_vjp(circuits=None, cotangents=None) + + @pytest.mark.jax + def test_jax(self): + """Test the device with JAX.""" + + jax = pytest.importorskip("jax") + dev = qml.device("default.tensor", wires=1) + ref_dev = qml.device("default.qubit.jax", wires=1) + + def circuit(x): + qml.RX(x[1], wires=0) + qml.Rot(x[0], x[1], x[2], wires=0) + return qml.expval(qml.Z(0)) + + weights = jax.numpy.array([0.2, 0.5, 0.1]) + print(isinstance(dev, qml.Device)) + qnode = qml.QNode(circuit, dev, interface="jax") + ref_qnode = qml.QNode(circuit, ref_dev, interface="jax") + + assert np.allclose(qnode(weights), ref_qnode(weights)) + + @pytest.mark.jax + def test_jax_jit(self): + """Test the device with JAX's JIT compiler.""" + + jax = pytest.importorskip("jax") + dev = qml.device("default.tensor", wires=1) + + @jax.jit + @qml.qnode(dev, interface="jax") + def circuit(): + qml.Hadamard(0) + return qml.expval(qml.Z(0)) + + assert np.allclose(circuit(), 0.0) diff --git a/tests/devices/default_tensor/test_quimb_mps.py b/tests/devices/default_tensor/test_quimb_mps.py deleted file mode 100644 index bc4133e7efe..00000000000 --- a/tests/devices/default_tensor/test_quimb_mps.py +++ /dev/null @@ -1,281 +0,0 @@ -# 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 ``quimb`` interface. -""" - -import itertools -import math - -import numpy as np -import pytest -import quimb.tensor as qtn -from conftest import LightningDevice # tested device -from pennylane_lightning.lightning_tensor import LightningTensor -from scipy.sparse import csr_matrix - -import pennylane as qml -from pennylane import QNode -from pennylane.devices import DefaultQubit -from pennylane.wires import Wires - -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) - -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"]) -class TestQuimbMPS: - """Tests for the MPS method.""" - - @pytest.mark.parametrize("num_wires", [None, 4]) - @pytest.mark.parametrize("c_dtype", [np.complex64, np.complex128]) - @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, **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) - - def test_interface_jax(self, backend, method): - """Test the interface with JAX.""" - - jax = pytest.importorskip("jax") - dev = LightningTensor(wires=qml.wires.Wires(range(1)), backend=backend, method=method) - ref_dev = qml.device("default.qubit.jax", wires=1) - - def circuit(x): - qml.RX(x[1], wires=0) - qml.Rot(x[0], x[1], x[2], wires=0) - return qml.expval(qml.Z(0)) - - weights = jax.numpy.array([0.2, 0.5, 0.1]) - qnode = QNode(circuit, dev, interface="jax") - ref_qnode = QNode(circuit, ref_dev, interface="jax") - - assert np.allclose(qnode(weights), ref_qnode(weights)) - - def test_interface_jax_jit(self, backend, method): - """Test the interface with JAX's JIT compiler.""" - - jax = pytest.importorskip("jax") - dev = LightningTensor(wires=qml.wires.Wires(range(1)), backend=backend, method=method) - - @jax.jit - @qml.qnode(dev, interface="jax") - def circuit(): - qml.Hadamard(0) - return qml.expval(qml.Z(0)) - - assert np.allclose(circuit(), 0.0) - - def test_(self, backend, method): - """...""" - - # jax = pytest.importorskip("jax") - dev = LightningTensor(wires=qml.wires.Wires(range(1)), backend=backend, method=method) - - def circuit(): - qml.RX(0.0, wires=0) - - with pytest.raises(qml.QuantumFunctionError): - QNode(circuit, dev, interface="jax", diff_method="adjoint") From da382ce4779e4b5d3331810b466395e3c360afe3 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Fri, 17 May 2024 11:06:10 -0400 Subject: [PATCH 05/29] [ci skip] --- tests/devices/default_tensor/test_default_tensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index 707174b425f..7ad62bd3c37 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -23,7 +23,7 @@ import pennylane as qml from pennylane.devices.default_tensor import DefaultTensor -# gates for which interface support is tested +# gates for which device support is tested ops = { "Identity": qml.Identity(wires=[0]), "BlockEncode": qml.BlockEncode([[0.1, 0.2], [0.3, 0.4]], wires=[0, 1]), @@ -97,7 +97,7 @@ all_ops = ops.keys() -# observables for which interface support is tested +# observables for which device support is tested obs = { "Identity": qml.Identity(wires=[0]), "Hadamard": qml.Hadamard(wires=[0]), @@ -190,11 +190,11 @@ def test_invalid_shots(): class TestSupportedGatesAndObservables: - """Tests for the MPS method.""" + """Test that the DefaultTensor device supports all gates and observables that it claims to support.""" @pytest.mark.parametrize("operation", all_ops) def test_supported_gates_can_be_implemented(self, operation): - """Test that the interface can implement all its supported gates.""" + """Test that the device can implement all its supported gates.""" dev = DefaultTensor(wires=qml.wires.Wires(range(4)), method="mps") @@ -208,7 +208,7 @@ def test_supported_gates_can_be_implemented(self, operation): @pytest.mark.parametrize("observable", all_obs) def test_supported_observables_can_be_implemented(self, observable): - """Test that the interface can implement all its supported observables.""" + """Test that the device can implement all its supported observables.""" dev = DefaultTensor(wires=qml.wires.Wires(range(3)), method="mps") From b0926e46927cc5c1339681298ea526b9f07c826d Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Fri, 17 May 2024 11:18:58 -0400 Subject: [PATCH 06/29] tests for expval and var included [ci skip] --- tests/devices/default_tensor/test_expval.py | 42 +++++++---------- tests/devices/default_tensor/test_var.py | 52 +++++++++------------ 2 files changed, 38 insertions(+), 56 deletions(-) diff --git a/tests/devices/default_tensor/test_expval.py b/tests/devices/default_tensor/test_expval.py index 57d08c7c2b3..2777a74d30a 100644 --- a/tests/devices/default_tensor/test_expval.py +++ b/tests/devices/default_tensor/test_expval.py @@ -12,28 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Tests for the expectation value calculations on the LightningTensor device. +Tests for the expectation value calculations on the DefaultTensor device. """ import numpy as np import pytest -from conftest import PHI, THETA, VARPHI, LightningDevice import pennylane as qml 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) +THETA = np.linspace(0.11, 1, 3) +PHI = np.linspace(0.32, 1, 3) +VARPHI = np.linspace(0.02, 1, 3) -from pennylane_lightning.lightning_tensor import LightningTensor +from pennylane.devices.default_tensor import DefaultTensor @pytest.fixture(params=[np.complex64, np.complex128]) def dev(request): - return LightningTensor(wires=3, c_dtype=request.param) + return DefaultTensor(wires=3, dtype=request.param) def calculate_reference(tape): @@ -68,7 +65,7 @@ def test_Identity(self, theta, phi, dev, tol): result = dev.execute(tape) expected = np.cos(theta) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(result, expected, tol) @@ -81,7 +78,7 @@ def test_identity_expectation(self, theta, phi, dev, tol): ) result = dev.execute(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(1.0, result, tol) @@ -93,7 +90,7 @@ def test_multi_wire_identity_expectation(self, theta, phi, dev, tol): [qml.expval(qml.Identity(wires=[0, 1]))], ) result = dev.execute(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(1.0, result, tol) @@ -103,7 +100,7 @@ def test_multi_wire_identity_expectation(self, theta, phi, dev, tol): ) def test_custom_wires(self, theta, phi, tol, wires): """Tests custom wires.""" - dev = LightningTensor(wires=wires, c_dtype=np.complex128) + dev = DefaultTensor(wires=wires, dtype=np.complex128) tape = qml.tape.QuantumScript( [ @@ -120,7 +117,7 @@ def test_custom_wires(self, theta, phi, tol, wires): 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 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(calculated_val, reference_val, tol) @@ -264,7 +261,7 @@ def test_op_math(self, phi, dev, obs, tol): calculated_val = execute(dev, tape) reference_val = calculate_reference(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(calculated_val, reference_val, tol) @@ -284,7 +281,7 @@ def test_integration(self, phi, dev, tol): calculated_val = execute(dev, tape) reference_val = calculate_reference(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(calculated_val, reference_val, tol) @@ -307,7 +304,7 @@ def test_PauliX_PauliY(self, theta, phi, varphi, dev, tol): calculated_val = execute(dev, tape) reference_val = calculate_reference(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(calculated_val, reference_val, tol) @@ -326,7 +323,7 @@ def test_PauliZ_identity(self, theta, phi, varphi, dev, tol): calculated_val = execute(dev, tape) reference_val = calculate_reference(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(calculated_val, reference_val, tol) @@ -344,16 +341,11 @@ def test_PauliZ_hadamard_PauliY(self, theta, phi, varphi, dev, tol): calculated_val = execute(dev, tape) reference_val = calculate_reference(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.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.""" @@ -393,7 +385,7 @@ def test_multi_qubit_gates(theta, phi, dev, tol): tape = qml.tape.QuantumScript(ops=ops, measurements=meas) reference_val = calculate_reference(tape) - dev = LightningTensor(wires=tape.wires, c_dtype=np.complex128) + dev = DefaultTensor(wires=tape.wires, dtype=np.complex128) calculated_val = dev.execute(tape) assert np.allclose(calculated_val, reference_val) diff --git a/tests/devices/default_tensor/test_var.py b/tests/devices/default_tensor/test_var.py index 94fbb43fa2a..22d0762f260 100644 --- a/tests/devices/default_tensor/test_var.py +++ b/tests/devices/default_tensor/test_var.py @@ -12,28 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Tests for the variance calculation on the LightningTensor device. +Tests for the variance calculation on the DefaultTensor device. """ import numpy as np import pytest -from conftest import PHI, THETA, VARPHI, LightningDevice import pennylane as qml +from pennylane.devices.default_tensor import DefaultTensor 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 +THETA = np.linspace(0.11, 1, 3) +PHI = np.linspace(0.32, 1, 3) +VARPHI = np.linspace(0.02, 1, 3) @pytest.fixture(params=[np.complex64, np.complex128]) def dev(request): - return LightningTensor(wires=3, c_dtype=request.param) + return DefaultTensor(wires=3, dtype=request.param) def calculate_reference(tape): @@ -68,7 +63,7 @@ def test_Identity(self, theta, phi, dev): result = dev.execute(tape) expected = 1 - np.cos(theta) ** 2 - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(result, expected, atol=tol, rtol=0) @@ -81,7 +76,7 @@ def test_identity_variance(self, theta, phi, dev): ) result = dev.execute(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.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): @@ -92,7 +87,7 @@ def test_multi_wire_identity_variance(self, theta, phi, dev): [qml.var(qml.Identity(wires=[0, 1]))], ) result = dev.execute(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(0.0, result, atol=tol, rtol=0) @pytest.mark.parametrize( @@ -101,7 +96,7 @@ def test_multi_wire_identity_variance(self, theta, phi, dev): ) def test_custom_wires(self, theta, phi, wires): """Tests custom wires.""" - device = LightningTensor(wires=wires, c_dtype=np.complex128) + device = DefaultTensor(wires=wires, dtype=np.complex128) tape = qml.tape.QuantumScript( [ @@ -117,7 +112,7 @@ def test_custom_wires(self, theta, phi, wires): [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 + tol = 1e-5 if device.dtype == np.complex64 else 1e-7 assert np.allclose(calculated_val, reference_val, atol=tol, rtol=0) @pytest.mark.parametrize( @@ -164,7 +159,7 @@ def test_single_wire_observables_variance(self, Obs, Op, expected_fn, theta, phi result = execute(dev, tape) expected = expected_fn(theta, phi) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(result, expected, atol=tol, rtol=0) def test_hermitian_variance(self, theta, phi, dev): @@ -181,7 +176,7 @@ def test_hermitian_variance(self, theta, phi, dev): calculated_val = execute(dev, tape) reference_val = calculate_reference(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.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): @@ -209,7 +204,7 @@ def test_hamiltonian_variance(self, theta, phi, dev): calculated_val = execute(dev, tape1) reference_val = calculate_reference(tape2) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.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): @@ -241,7 +236,7 @@ def test_sparse_hamiltonian_variance(self, theta, phi, dev): calculated_val = execute(dev, tape1) reference_val = calculate_reference(tape2) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(calculated_val, reference_val, atol=tol, rtol=0) @@ -273,7 +268,7 @@ def test_op_math(self, phi, dev, obs, tol): calculated_val = execute(dev, tape) reference_val = calculate_reference(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(calculated_val, reference_val, tol) @@ -293,7 +288,7 @@ def test_integration(self, phi, dev, tol): calculated_val = execute(dev, tape) reference_val = calculate_reference(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(calculated_val, reference_val, tol) @@ -316,7 +311,7 @@ def test_PauliX_PauliY(self, theta, phi, varphi, dev, tol): calculated_val = execute(dev, tape) reference_val = calculate_reference(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(calculated_val, reference_val, tol) @@ -335,7 +330,7 @@ def test_PauliZ_identity(self, theta, phi, varphi, dev, tol): calculated_val = execute(dev, tape) reference_val = calculate_reference(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.dtype == np.complex64 else 1e-7 assert np.allclose(calculated_val, reference_val, tol) @@ -353,16 +348,11 @@ def test_PauliZ_hadamard_PauliY(self, theta, phi, varphi, dev, tol): calculated_val = execute(dev, tape) reference_val = calculate_reference(tape) - tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + tol = 1e-5 if dev.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.""" @@ -402,7 +392,7 @@ def test_multi_qubit_gates(theta, phi, dev, tol): tape = qml.tape.QuantumScript(ops=ops, measurements=meas) reference_val = calculate_reference(tape) - dev = LightningTensor(wires=tape.wires, c_dtype=np.complex128) + dev = DefaultTensor(wires=tape.wires, dtype=np.complex128) calculated_val = dev.execute(tape) assert np.allclose(calculated_val, reference_val) From 0c6b363970aad071edd6eef03f48687945a31ae2 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Fri, 17 May 2024 13:50:42 -0400 Subject: [PATCH 07/29] Push before discussion [ci skip] --- pennylane/devices/default_tensor.py | 16 +++++++--------- tests/devices/default_tensor/test_expval.py | 2 +- tests/devices/default_tensor/test_var.py | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index 3889dcfa285..204012bb22d 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -132,12 +132,12 @@ def accepted_methods(method: str) -> bool: def stopping_condition(op: qml.operation.Operator) -> bool: """A function that determines if an operation is supported by ``lightning.tensor`` for this interface.""" - return op.name in _operations # pragma: no cover + return op.name in _operations def accepted_observables(obs: qml.operation.Operator) -> bool: """A function that determines if an observable is supported by ``lightning.tensor`` for this interface.""" - return obs.name in _observables # pragma: no cover + return obs.name in _observables @simulator_tracking @@ -178,10 +178,8 @@ class DefaultTensor(Device): _new_API = True - # pylint: disable=too-many-arguments def __init__( self, - *, wires=None, method="mps", shots=None, @@ -191,7 +189,7 @@ def __init__( if not accepted_methods(method): raise ValueError( - f"Unsupported method: {method}. The currently supported method is mps." + f"Unsupported method: {method}. The only currently supported method is mps." ) if shots is not None: @@ -215,9 +213,9 @@ def __init__( device_options = self._setup_execution_config().device_options self._init_state_opts = { - "binary": "0" if self._wires is None else "0" * max(1, len(self._wires)), + "binary": "0" * (len(self._wires) if self._wires else 1), "dtype": self._dtype.__name__, - "tags": None if self._wires is None else [str(l) for l in self._wires.labels], + "tags": [str(l) for l in self._wires.labels] if self._wires else None, } self._gate_opts = { @@ -369,7 +367,7 @@ def simulate(self, circuit: QuantumScript) -> Result: return self.measurement(circuit.measurements[0]) return tuple(self.measurement(mp) for mp in circuit.measurements) - raise NotImplementedError # pragma: no cover + raise NotImplementedError def _apply_operation(self, op: qml.operation.Operator) -> None: """Apply a single operator to the circuit, keeping the state always in a MPS form. @@ -412,7 +410,7 @@ def _get_measurement_function( if isinstance(measurementprocess, VarianceMP): return self.var - raise NotImplementedError # pragma: no cover + raise NotImplementedError def expval(self, measurementprocess: MeasurementProcess) -> float: """Expectation value of the supplied observable contained in the MeasurementProcess. diff --git a/tests/devices/default_tensor/test_expval.py b/tests/devices/default_tensor/test_expval.py index 2777a74d30a..958922cfbb5 100644 --- a/tests/devices/default_tensor/test_expval.py +++ b/tests/devices/default_tensor/test_expval.py @@ -30,7 +30,7 @@ @pytest.fixture(params=[np.complex64, np.complex128]) def dev(request): - return DefaultTensor(wires=3, dtype=request.param) + return qml.device("default.tensor", wires=3, dtype=request.param) def calculate_reference(tape): diff --git a/tests/devices/default_tensor/test_var.py b/tests/devices/default_tensor/test_var.py index 22d0762f260..28f7c0fb6b0 100644 --- a/tests/devices/default_tensor/test_var.py +++ b/tests/devices/default_tensor/test_var.py @@ -28,7 +28,7 @@ @pytest.fixture(params=[np.complex64, np.complex128]) def dev(request): - return DefaultTensor(wires=3, dtype=request.param) + return qml.device("default.tensor", wires=3, dtype=request.param) def calculate_reference(tape): From 0a6e2e3c17b9165784228475bffd48591618a3a5 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Fri, 17 May 2024 14:16:40 -0400 Subject: [PATCH 08/29] trying to set up tests --- .github/workflows/interface-unit-tests.yml | 2 +- tests/devices/default_tensor/test_default_tensor.py | 4 ++++ tests/devices/default_tensor/test_expval.py | 3 +++ tests/devices/default_tensor/test_var.py | 4 ++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/interface-unit-tests.yml b/.github/workflows/interface-unit-tests.yml index 41f2f3434e3..ba2fc38154f 100644 --- a/.github/workflows/interface-unit-tests.yml +++ b/.github/workflows/interface-unit-tests.yml @@ -366,7 +366,7 @@ jobs: install_pennylane_lightning_master: false pytest_coverage_flags: ${{ inputs.pytest_coverage_flags }} pytest_markers: external - additional_pip_packages: pyzx pennylane-catalyst matplotlib stim + additional_pip_packages: pyzx pennylane-catalyst matplotlib stim quimb requirements_file: ${{ strategy.job-index == 0 && 'external.txt' || '' }} disable_new_opmath: ${{ inputs.disable_new_opmath }} diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index 7ad62bd3c37..303e63d5765 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -23,6 +23,10 @@ import pennylane as qml from pennylane.devices.default_tensor import DefaultTensor +stim = pytest.importorskip("quimb") + +pytestmark = pytest.mark.external + # gates for which device support is tested ops = { "Identity": qml.Identity(wires=[0]), diff --git a/tests/devices/default_tensor/test_expval.py b/tests/devices/default_tensor/test_expval.py index 958922cfbb5..ecb01f54251 100644 --- a/tests/devices/default_tensor/test_expval.py +++ b/tests/devices/default_tensor/test_expval.py @@ -27,6 +27,9 @@ from pennylane.devices.default_tensor import DefaultTensor +stim = pytest.importorskip("quimb") + +pytestmark = pytest.mark.external @pytest.fixture(params=[np.complex64, np.complex128]) def dev(request): diff --git a/tests/devices/default_tensor/test_var.py b/tests/devices/default_tensor/test_var.py index 28f7c0fb6b0..22b414a0b93 100644 --- a/tests/devices/default_tensor/test_var.py +++ b/tests/devices/default_tensor/test_var.py @@ -25,6 +25,10 @@ PHI = np.linspace(0.32, 1, 3) VARPHI = np.linspace(0.02, 1, 3) +stim = pytest.importorskip("quimb") + +pytestmark = pytest.mark.external + @pytest.fixture(params=[np.complex64, np.complex128]) def dev(request): From b434e8c167a932e771e5dd0027edabfabd178485 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Fri, 17 May 2024 15:22:50 -0400 Subject: [PATCH 09/29] Updating files with `default.tensor` --- doc/introduction/circuits.rst | 2 +- pennylane/__init__.py | 3 +++ pennylane/devices/__init__.py | 1 + pennylane/devices/default_tensor.py | 14 ++++++++++---- .../devices/default_tensor/test_default_tensor.py | 2 +- tests/devices/default_tensor/test_expval.py | 2 +- tests/devices/default_tensor/test_var.py | 2 +- 7 files changed, 18 insertions(+), 8 deletions(-) diff --git a/doc/introduction/circuits.rst b/doc/introduction/circuits.rst index bb637fc1f61..74f8ff8bf21 100644 --- a/doc/introduction/circuits.rst +++ b/doc/introduction/circuits.rst @@ -88,7 +88,7 @@ instantiated using the :func:`device ` loader. dev = qml.device('default.qubit', wires=2, shots=1000) PennyLane offers some basic devices such as the ``'default.qubit'``, ``'default.mixed'``, ``lightning.qubit``, -``'default.gaussian'``, and ``'default.clifford'`` simulators; additional devices can be installed as plugins +``'default.gaussian'``, ``'default.clifford'``, and ``'default.tensor'`` simulators; additional devices can be installed as plugins (see `available plugins `_ for more details). Note that the choice of a device significantly determines the speed of your computation, as well as the available options that can be passed to the device loader. diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 304ac4408f8..1619630ba71 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -223,6 +223,9 @@ def device(name, *args, **kwargs): * :mod:`'default.clifford' `: an efficient simulator of Clifford circuits. + * :mod:`'default.tensor' `: a simple + simulator of quantum circuits based on tensor networks. + Additional devices are supported through plugins — see the `available plugins `_ for more details. To list all currently installed devices, run diff --git a/pennylane/devices/__init__.py b/pennylane/devices/__init__.py index 3da86933daa..a59820cb12f 100644 --- a/pennylane/devices/__init__.py +++ b/pennylane/devices/__init__.py @@ -36,6 +36,7 @@ default_qutrit default_qutrit_mixed default_clifford + default_tensor null_qubit tests diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index 204012bb22d..e64f6e4191a 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This module contains the default.tensor device to perform tensor network simulation of a quantum circuit. +This module contains the default.tensor device to perform tensor network simulation of a quantum circuit using ``quimb``. """ import copy from dataclasses import replace @@ -21,10 +21,11 @@ import numpy as np +has_quimb = True try: import quimb.tensor as qtn -except ImportError as err: - raise ImportError("default.tensor device requires the quimb package") from err +except (ModuleNotFoundError, ImportError) as import_error: # pragma: no cover + has_quimb = False import pennylane as qml from pennylane.devices import DefaultExecutionConfig, Device, ExecutionConfig @@ -185,7 +186,12 @@ def __init__( shots=None, dtype=np.complex128, **kwargs, - ): + ) -> None: + if not has_quimb: + raise ImportError( + "This feature requires quimb, a library for tensor network manipulations. " + "It can be installed with:\n\npip install quimb" + ) if not accepted_methods(method): raise ValueError( diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index 303e63d5765..2199e9a09da 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -23,7 +23,7 @@ import pennylane as qml from pennylane.devices.default_tensor import DefaultTensor -stim = pytest.importorskip("quimb") +quimb = pytest.importorskip("quimb") pytestmark = pytest.mark.external diff --git a/tests/devices/default_tensor/test_expval.py b/tests/devices/default_tensor/test_expval.py index ecb01f54251..a6d804cb528 100644 --- a/tests/devices/default_tensor/test_expval.py +++ b/tests/devices/default_tensor/test_expval.py @@ -27,7 +27,7 @@ from pennylane.devices.default_tensor import DefaultTensor -stim = pytest.importorskip("quimb") +quimb = pytest.importorskip("quimb") pytestmark = pytest.mark.external diff --git a/tests/devices/default_tensor/test_var.py b/tests/devices/default_tensor/test_var.py index 22b414a0b93..608749a9c41 100644 --- a/tests/devices/default_tensor/test_var.py +++ b/tests/devices/default_tensor/test_var.py @@ -25,7 +25,7 @@ PHI = np.linspace(0.32, 1, 3) VARPHI = np.linspace(0.02, 1, 3) -stim = pytest.importorskip("quimb") +quimb = pytest.importorskip("quimb") pytestmark = pytest.mark.external From af51004945388b17a1ac871abe20b22c40e07e0b Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Fri, 17 May 2024 15:25:29 -0400 Subject: [PATCH 10/29] Trying formatting --- tests/devices/default_tensor/test_expval.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/devices/default_tensor/test_expval.py b/tests/devices/default_tensor/test_expval.py index a6d804cb528..be95fdc0fd1 100644 --- a/tests/devices/default_tensor/test_expval.py +++ b/tests/devices/default_tensor/test_expval.py @@ -31,6 +31,7 @@ pytestmark = pytest.mark.external + @pytest.fixture(params=[np.complex64, np.complex128]) def dev(request): return qml.device("default.tensor", wires=3, dtype=request.param) From 9be17b7c6ccf4c04f9eb4ddd536ac74df751687a Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Fri, 17 May 2024 15:37:20 -0400 Subject: [PATCH 11/29] Trying to fix some issues --- pennylane/__init__.py | 2 +- pennylane/devices/default_tensor.py | 12 ++++++------ tests/devices/default_tensor/test_default_tensor.py | 3 ++- tests/devices/default_tensor/test_expval.py | 4 ++-- tests/devices/default_tensor/test_var.py | 3 ++- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 1619630ba71..46817ced595 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -223,7 +223,7 @@ def device(name, *args, **kwargs): * :mod:`'default.clifford' `: an efficient simulator of Clifford circuits. - * :mod:`'default.tensor' `: a simple + * :mod:`'default.tensor' `: a simple simulator of quantum circuits based on tensor networks. Additional devices are supported through plugins — see diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index e64f6e4191a..eb9c1869a6f 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -21,12 +21,6 @@ import numpy as np -has_quimb = True -try: - import quimb.tensor as qtn -except (ModuleNotFoundError, ImportError) as import_error: # pragma: no cover - has_quimb = False - import pennylane as qml from pennylane.devices import DefaultExecutionConfig, Device, ExecutionConfig from pennylane.devices.modifiers import simulator_tracking, single_tape_support @@ -46,6 +40,12 @@ QuantumTape_or_Batch = Union[QuantumTape, QuantumTapeBatch] PostprocessingFn = Callable[[ResultBatch], Result_or_ResultBatch] +has_quimb = True +try: + import quimb.tensor as qtn +except (ModuleNotFoundError, ImportError) as import_error: # pragma: no cover + has_quimb = False + _operations = frozenset( { "Identity", diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index 2199e9a09da..ac7c7c26073 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -21,12 +21,13 @@ from scipy.sparse import csr_matrix import pennylane as qml -from pennylane.devices.default_tensor import DefaultTensor quimb = pytest.importorskip("quimb") pytestmark = pytest.mark.external +from pennylane.devices.default_tensor import DefaultTensor + # gates for which device support is tested ops = { "Identity": qml.Identity(wires=[0]), diff --git a/tests/devices/default_tensor/test_expval.py b/tests/devices/default_tensor/test_expval.py index be95fdc0fd1..84b7a3bfbe3 100644 --- a/tests/devices/default_tensor/test_expval.py +++ b/tests/devices/default_tensor/test_expval.py @@ -25,12 +25,12 @@ PHI = np.linspace(0.32, 1, 3) VARPHI = np.linspace(0.02, 1, 3) -from pennylane.devices.default_tensor import DefaultTensor - quimb = pytest.importorskip("quimb") pytestmark = pytest.mark.external +from pennylane.devices.default_tensor import DefaultTensor + @pytest.fixture(params=[np.complex64, np.complex128]) def dev(request): diff --git a/tests/devices/default_tensor/test_var.py b/tests/devices/default_tensor/test_var.py index 608749a9c41..8d60bdbe268 100644 --- a/tests/devices/default_tensor/test_var.py +++ b/tests/devices/default_tensor/test_var.py @@ -18,7 +18,6 @@ import pytest import pennylane as qml -from pennylane.devices.default_tensor import DefaultTensor from pennylane.tape import QuantumScript THETA = np.linspace(0.11, 1, 3) @@ -29,6 +28,8 @@ pytestmark = pytest.mark.external +from pennylane.devices.default_tensor import DefaultTensor + @pytest.fixture(params=[np.complex64, np.complex128]) def dev(request): From 4d8fb03c3fc4b076bc90719e2c95ba812076f747 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Fri, 17 May 2024 16:25:35 -0400 Subject: [PATCH 12/29] Trying to solve CI issues --- pennylane/__init__.py | 3 -- .../default_tensor/test_default_tensor.py | 50 +++++++++---------- .../{test_expval.py => test_tensor_expval.py} | 12 +++-- .../{test_var.py => test_tensor_var.py} | 11 ++-- 4 files changed, 39 insertions(+), 37 deletions(-) rename tests/devices/default_tensor/{test_expval.py => test_tensor_expval.py} (96%) rename tests/devices/default_tensor/{test_var.py => test_tensor_var.py} (96%) diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 46817ced595..304ac4408f8 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -223,9 +223,6 @@ def device(name, *args, **kwargs): * :mod:`'default.clifford' `: an efficient simulator of Clifford circuits. - * :mod:`'default.tensor' `: a simple - simulator of quantum circuits based on tensor networks. - Additional devices are supported through plugins — see the `available plugins `_ for more details. To list all currently installed devices, run diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index ac7c7c26073..aeaf5ff3a1f 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -26,8 +26,6 @@ pytestmark = pytest.mark.external -from pennylane.devices.default_tensor import DefaultTensor - # gates for which device support is tested ops = { "Identity": qml.Identity(wires=[0]), @@ -127,17 +125,17 @@ def test_name(): """Test the name of DefaultTensor.""" - assert DefaultTensor().name == "default.tensor" + assert qml.device("default.tensor").name == "default.tensor" def test_wires(): """Test that a device can be created with wires.""" - assert DefaultTensor().wires is None - assert DefaultTensor(wires=2).wires == qml.wires.Wires([0, 1]) - assert DefaultTensor(wires=[0, 2]).wires == qml.wires.Wires([0, 2]) + assert qml.device("default.tensor").wires is None + assert qml.device("default.tensor", wires=2).wires == qml.wires.Wires([0, 1]) + assert qml.device("default.tensor", wires=[0, 2]).wires == qml.wires.Wires([0, 2]) with pytest.raises(AttributeError): - DefaultTensor().wires = [0, 1] + qml.device("default.tensor").wires = [0, 1] @pytest.mark.parametrize("max_bond_dim", [None, 10]) @@ -148,7 +146,7 @@ def test_kwargs(max_bond_dim, cutoff, contract): kwargs = {"max_bond_dim": max_bond_dim, "cutoff": cutoff, "contract": contract} - dev = DefaultTensor(**kwargs) + dev = qml.device("default.tensor", **kwargs) _, config = dev.preprocess() assert config.device_options["method"] == "mps" @@ -161,37 +159,37 @@ def test_invalid_kwarg(): """Test an invalid keyword argument.""" with pytest.raises( TypeError, - match=f"Unexpected argument: fake_arg during initialization of the default.tensor device.", + match="Unexpected argument: fake_arg during initialization of the default.tensor device.", ): - DefaultTensor(fake_arg=None) + qml.device("default.tensor", fake_arg=None) @pytest.mark.parametrize("dtype", [np.complex64, np.complex128]) def test_data_type(dtype): """Test the data type.""" - assert DefaultTensor(dtype=dtype).dtype == dtype + assert qml.device("default.tensor", dtype=dtype).dtype == dtype def test_ivalid_data_type(): """Test that data type can only be np.complex64 or np.complex128.""" with pytest.raises(TypeError): - DefaultTensor(dtype=float) + qml.device("default.tensor", dtype=float) def test_invalid_method(): """Test an invalid method.""" method = "invalid_method" with pytest.raises(ValueError, match=f"Unsupported method: {method}"): - DefaultTensor(method=method) + qml.device("default.tensor", method=method) def test_invalid_shots(): """Test that an error is raised if finite number of shots are requestd.""" with pytest.raises(ValueError, match="default.tensor does not support finite shots."): - DefaultTensor(shots=5) + qml.device("default.tensor", shots=5) with pytest.raises(AttributeError): - DefaultTensor().shots = 10 + qml.device("default.tensor").shots = 10 class TestSupportedGatesAndObservables: @@ -201,7 +199,7 @@ class TestSupportedGatesAndObservables: def test_supported_gates_can_be_implemented(self, operation): """Test that the device can implement all its supported gates.""" - dev = DefaultTensor(wires=qml.wires.Wires(range(4)), method="mps") + dev = qml.device("default.tensor", wires=4, method="mps") tape = qml.tape.QuantumScript( [ops[operation]], @@ -215,7 +213,7 @@ def test_supported_gates_can_be_implemented(self, operation): def test_supported_observables_can_be_implemented(self, observable): """Test that the device can implement all its supported observables.""" - dev = DefaultTensor(wires=qml.wires.Wires(range(3)), method="mps") + dev = qml.device("default.tensor", wires=3, method="mps") if observable == "Projector": for o in obs[observable]: @@ -237,11 +235,11 @@ def test_supported_observables_can_be_implemented(self, observable): def test_not_implemented_meas(self): """Tests that support only exists for `qml.expval` and `qml.var` so far.""" - ops = [qml.Identity(0)] + op = [qml.Identity(0)] measurements = [qml.probs(qml.PauliZ(0))] - tape = qml.tape.QuantumScript(ops, measurements) + tape = qml.tape.QuantumScript(op, measurements) - dev = DefaultTensor(wires=tape.wires) + dev = qml.device("default.tensor", wires=tape.wires) with pytest.raises(NotImplementedError): dev.execute(tape) @@ -252,12 +250,12 @@ class TestSupportsDerivatives: def test_support_derivatives(self): """Test that the device does not support derivatives yet.""" - dev = DefaultTensor() + dev = qml.device("default.tensor") assert not dev.supports_derivatives() def test_compute_derivatives(self): """Test that an error is raised if the `compute_derivatives` method is called.""" - dev = DefaultTensor() + dev = qml.device("default.tensor") with pytest.raises( NotImplementedError, match="The computation of derivatives has yet to be implemented for the default.tensor device.", @@ -266,7 +264,7 @@ def test_compute_derivatives(self): def test_execute_and_compute_derivatives(self): """Test that an error is raised if `execute_and_compute_derivative` method is called.""" - dev = DefaultTensor() + dev = qml.device("default.tensor") with pytest.raises( NotImplementedError, match="The computation of derivatives has yet to be implemented for the default.tensor device.", @@ -275,12 +273,12 @@ def test_execute_and_compute_derivatives(self): def test_supports_vjp(self): """Test that the device does not support VJP yet.""" - dev = DefaultTensor() + dev = qml.device("default.tensor") assert not dev.supports_vjp() def test_compute_vjp(self): """Test that an error is raised if `compute_vjp` method is called.""" - dev = DefaultTensor() + dev = qml.device("default.tensor") with pytest.raises( NotImplementedError, match="The computation of vector-Jacobian product has yet to be implemented for the default.tensor device.", @@ -289,7 +287,7 @@ def test_compute_vjp(self): def test_execute_and_compute_vjp(self): """Test that an error is raised if `execute_and_compute_vjp` method is called.""" - dev = DefaultTensor() + dev = qml.device("default.tensor") with pytest.raises( NotImplementedError, match="The computation of vector-Jacobian product has yet to be implemented for the default.tensor device.", diff --git a/tests/devices/default_tensor/test_expval.py b/tests/devices/default_tensor/test_tensor_expval.py similarity index 96% rename from tests/devices/default_tensor/test_expval.py rename to tests/devices/default_tensor/test_tensor_expval.py index 84b7a3bfbe3..663dba1ee64 100644 --- a/tests/devices/default_tensor/test_expval.py +++ b/tests/devices/default_tensor/test_tensor_expval.py @@ -29,15 +29,17 @@ pytestmark = pytest.mark.external -from pennylane.devices.default_tensor import DefaultTensor +# pylint: disable=too-many-arguments, redefined-outer-name @pytest.fixture(params=[np.complex64, np.complex128]) def dev(request): + """Device fixture.""" return qml.device("default.tensor", wires=3, dtype=request.param) def calculate_reference(tape): + """Calculate the reference value of the tape using DefaultQubit.""" dev = DefaultQubit(max_workers=1) program, _ = dev.preprocess() tapes, transf_fn = program([tape]) @@ -46,6 +48,7 @@ def calculate_reference(tape): def execute(dev, tape): + """Execute the tape on the device and return the result.""" results = dev.execute(tape) return results @@ -104,7 +107,7 @@ def test_multi_wire_identity_expectation(self, theta, phi, dev, tol): ) def test_custom_wires(self, theta, phi, tol, wires): """Tests custom wires.""" - dev = DefaultTensor(wires=wires, dtype=np.complex128) + dev = qml.device("default.tensor", wires=wires, dtype=np.complex128) tape = qml.tape.QuantumScript( [ @@ -125,6 +128,7 @@ def test_custom_wires(self, theta, phi, tol, wires): assert np.allclose(calculated_val, reference_val, tol) + # pylint: disable=too-many-arguments @pytest.mark.parametrize( "Obs, Op, expected_fn", [ @@ -351,7 +355,7 @@ def test_PauliZ_hadamard_PauliY(self, theta, phi, varphi, dev, tol): @pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) -def test_multi_qubit_gates(theta, phi, dev, tol): +def test_multi_qubit_gates(theta, phi, dev): """Tests a simple circuit with multi-qubit gates.""" ops = [ @@ -389,7 +393,7 @@ def test_multi_qubit_gates(theta, phi, dev, tol): tape = qml.tape.QuantumScript(ops=ops, measurements=meas) reference_val = calculate_reference(tape) - dev = DefaultTensor(wires=tape.wires, dtype=np.complex128) + dev = qml.device("default.tensor", wires=tape.wires, dtype=np.complex128) calculated_val = dev.execute(tape) assert np.allclose(calculated_val, reference_val) diff --git a/tests/devices/default_tensor/test_var.py b/tests/devices/default_tensor/test_tensor_var.py similarity index 96% rename from tests/devices/default_tensor/test_var.py rename to tests/devices/default_tensor/test_tensor_var.py index 8d60bdbe268..a4aa35e40ed 100644 --- a/tests/devices/default_tensor/test_var.py +++ b/tests/devices/default_tensor/test_tensor_var.py @@ -28,15 +28,17 @@ pytestmark = pytest.mark.external -from pennylane.devices.default_tensor import DefaultTensor +# pylint: disable=too-many-arguments, redefined-outer-name @pytest.fixture(params=[np.complex64, np.complex128]) def dev(request): + """Device fixture.""" return qml.device("default.tensor", wires=3, dtype=request.param) def calculate_reference(tape): + """Calculate the reference value of the tape using DefaultQubit.""" dev = qml.device("default.qubit", max_workers=1) program, _ = dev.preprocess() tapes, transf_fn = program([tape]) @@ -45,6 +47,7 @@ def calculate_reference(tape): def execute(dev, tape): + """Execute the tape on the device and return the result.""" results = dev.execute(tape) return results @@ -101,7 +104,7 @@ def test_multi_wire_identity_variance(self, theta, phi, dev): ) def test_custom_wires(self, theta, phi, wires): """Tests custom wires.""" - device = DefaultTensor(wires=wires, dtype=np.complex128) + device = qml.device("default.tensor", wires=wires, dtype=np.complex128) tape = qml.tape.QuantumScript( [ @@ -359,7 +362,7 @@ def test_PauliZ_hadamard_PauliY(self, theta, phi, varphi, dev, tol): @pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) -def test_multi_qubit_gates(theta, phi, dev, tol): +def test_multi_qubit_gates(theta, phi, dev): """Tests a simple circuit with multi-qubit gates.""" ops = [ @@ -397,7 +400,7 @@ def test_multi_qubit_gates(theta, phi, dev, tol): tape = qml.tape.QuantumScript(ops=ops, measurements=meas) reference_val = calculate_reference(tape) - dev = DefaultTensor(wires=tape.wires, dtype=np.complex128) + dev = qml.device("default.tensor", wires=tape.wires, dtype=np.complex128) calculated_val = dev.execute(tape) assert np.allclose(calculated_val, reference_val) From c2b7fdb6e69887877a3e259a952a96ca1728814f Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Fri, 17 May 2024 17:28:30 -0400 Subject: [PATCH 13/29] Solving more issues and doe coverage --- pennylane/devices/default_tensor.py | 9 ++++++--- tests/devices/default_tensor/test_default_tensor.py | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index eb9c1869a6f..122a96c65d7 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -125,6 +125,8 @@ _methods = frozenset({"mps"}) # The set of supported methods. +# pylint: disable=trailing-whitespace + def accepted_methods(method: str) -> bool: """A function that determines whether or not a method is supported by ``default.tensor``.""" @@ -144,7 +146,8 @@ def accepted_observables(obs: qml.operation.Operator) -> bool: @simulator_tracking @single_tape_support class DefaultTensor(Device): - """A PennyLane device to perform tensor network operations on a quantum circuit. + """A PennyLane device to perform tensor network operations on a quantum circuit using + `quimb `_. Args: wires (int, Iterable[Number, str]): Number of wires present on the device, or iterable that @@ -191,7 +194,7 @@ def __init__( raise ImportError( "This feature requires quimb, a library for tensor network manipulations. " "It can be installed with:\n\npip install quimb" - ) + ) # pragma: no cover if not accepted_methods(method): raise ValueError( @@ -373,7 +376,7 @@ def simulate(self, circuit: QuantumScript) -> Result: return self.measurement(circuit.measurements[0]) return tuple(self.measurement(mp) for mp in circuit.measurements) - raise NotImplementedError + 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. diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index aeaf5ff3a1f..77998fac3c5 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -164,6 +164,11 @@ def test_invalid_kwarg(): qml.device("default.tensor", fake_arg=None) +def test_method(): + """Test the device method.""" + assert qml.device("default.tensor").method == "mps" + + @pytest.mark.parametrize("dtype", [np.complex64, np.complex128]) def test_data_type(dtype): """Test the data type.""" From 3976e15ed2ab5815ae61fd5449b19a62bfb8d04a Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Fri, 17 May 2024 17:29:01 -0400 Subject: [PATCH 14/29] forgot doc issue --- pennylane/devices/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pennylane/devices/__init__.py b/pennylane/devices/__init__.py index a59820cb12f..3da86933daa 100644 --- a/pennylane/devices/__init__.py +++ b/pennylane/devices/__init__.py @@ -36,7 +36,6 @@ default_qutrit default_qutrit_mixed default_clifford - default_tensor null_qubit tests From ca4f21cf908e5fbf4c1ad0952a8c4fe914f41149 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Tue, 21 May 2024 10:31:24 -0400 Subject: [PATCH 15/29] Added check on wires --- pennylane/devices/default_tensor.py | 11 ++++++++--- .../default_tensor/test_default_tensor.py | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index 122a96c65d7..23612745317 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This module contains the default.tensor device to perform tensor network simulation of a quantum circuit using ``quimb``. +This module contains the default.tensor device to perform tensor network simulation of a quantum circuit using ``quimb``. """ import copy from dataclasses import replace @@ -125,8 +125,6 @@ _methods = frozenset({"mps"}) # The set of supported methods. -# pylint: disable=trailing-whitespace - def accepted_methods(method: str) -> bool: """A function that determines whether or not a method is supported by ``default.tensor``.""" @@ -351,6 +349,13 @@ def execute( results = [] for circuit in circuits: + # we need to check if the wires of the circuit are compatible with the wires of the device + # since the initial tensor state is created with the wires of the device + if not self.wires.contains_wires(circuit.wires): + raise AttributeError( + f"Circuit has wires {circuit.wires.tolist()}. " + f"Tensor on device has wires: {self.wires.tolist()}" + ) circuit = circuit.map_to_standard_wires() results.append(self.simulate(circuit)) diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index 77998fac3c5..e49ed1b9fc8 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -138,6 +138,23 @@ def test_wires(): qml.device("default.tensor").wires = [0, 1] +def test_wires_execution_error(): + """Test that this device cannot execute a tape if its wires do not match the wires on the device.""" + dev = qml.device("default.tensor", wires=3) + ops = [ + qml.Identity(0), + qml.Identity((0, 1)), + qml.RX(2, 0), + qml.RY(1, 5), + qml.RX(2, 1), + ] + measurements = [qml.expval(qml.PauliZ(15))] + tape = qml.tape.QuantumScript(ops, measurements) + + with pytest.raises(AttributeError): + dev.execute(tape) + + @pytest.mark.parametrize("max_bond_dim", [None, 10]) @pytest.mark.parametrize("cutoff", [1e-16, 1e-12]) @pytest.mark.parametrize("contract", ["auto-mps", "nonlocal"]) From b2a8fe5ee973203160861d2e9c640826003c2b23 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Tue, 21 May 2024 11:33:59 -0400 Subject: [PATCH 16/29] Updating changelog --- doc/releases/changelog-dev.md | 3 +++ .../devices/default_tensor/test_default_tensor.py | 14 +++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 7409645b82e..d3630969422 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -4,6 +4,9 @@

New features since last release

+* The `default.tensor` device is introduced to perform tensor network simulation of a quantum circuit. + [(#5699)](https://github.com/PennyLaneAI/pennylane/pull/5699) +

Improvements 🛠

* The sorting order of parameter-shift terms is now guaranteed to resolve ties in the absolute value with the sign of the shifts. diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index e49ed1b9fc8..36c9951c882 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -27,7 +27,7 @@ pytestmark = pytest.mark.external # gates for which device support is tested -ops = { +operations_list = { "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]), @@ -98,10 +98,10 @@ "GlobalPhase": qml.GlobalPhase(0.123, wires=[0, 1]), } -all_ops = ops.keys() +all_ops = operations_list.keys() # observables for which device support is tested -obs = { +observables_list = { "Identity": qml.Identity(wires=[0]), "Hadamard": qml.Hadamard(wires=[0]), "Hermitian": qml.Hermitian(np.eye(2), wires=[0]), @@ -120,7 +120,7 @@ "LinearCombination": qml.ops.LinearCombination([1, 1], [qml.Z(0), qml.X(0)]), } -all_obs = obs.keys() +all_obs = observables_list.keys() def test_name(): @@ -224,7 +224,7 @@ def test_supported_gates_can_be_implemented(self, operation): dev = qml.device("default.tensor", wires=4, method="mps") tape = qml.tape.QuantumScript( - [ops[operation]], + [operations_list[operation]], [qml.expval(qml.Identity(wires=0))], ) @@ -238,7 +238,7 @@ def test_supported_observables_can_be_implemented(self, observable): dev = qml.device("default.tensor", wires=3, method="mps") if observable == "Projector": - for o in obs[observable]: + for o in observables_list[observable]: tape = qml.tape.QuantumScript( [qml.PauliX(0)], [qml.expval(o)], @@ -249,7 +249,7 @@ def test_supported_observables_can_be_implemented(self, observable): else: tape = qml.tape.QuantumScript( [qml.PauliX(0)], - [qml.expval(obs[observable])], + [qml.expval(observables_list[observable])], ) result = dev.execute(circuits=tape) assert isinstance(result, (float, np.ndarray)) From b0676276a6bddcaae887e69a7b1dbd1f25512fb3 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Tue, 21 May 2024 13:59:35 -0400 Subject: [PATCH 17/29] Typo in error message --- pennylane/devices/default_tensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index 23612745317..d3c25a93967 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -354,7 +354,7 @@ def execute( if not self.wires.contains_wires(circuit.wires): raise AttributeError( f"Circuit has wires {circuit.wires.tolist()}. " - f"Tensor on device has wires: {self.wires.tolist()}" + f"Tensor on device has wires {self.wires.tolist()}" ) circuit = circuit.map_to_standard_wires() results.append(self.simulate(circuit)) From 9184d6cc96d3165136d307713d03d45c989e335e Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Tue, 21 May 2024 16:24:15 -0400 Subject: [PATCH 18/29] Applying suggestions from code review and making `wires` argument mandatory --- pennylane/devices/default_tensor.py | 9 ++-- .../default_tensor/test_default_tensor.py | 51 ++++++++++--------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index d3c25a93967..f1e89bd968f 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -154,7 +154,7 @@ class DefaultTensor(Device): shots (int, Sequence[int], Sequence[Union[int, Sequence[int]]]): The default number of shots to use in executions involving this device. Currently, it can only be ``None``, so that computation of statistics like expectation values and variances is performed analytically. - method (str): Supported method. Currently, only ``mps`` is supported. + method (str): Supported method. Currently, only ``"mps"`` is supported. dtype (type): Datatype for the tensor representation. Must be one of ``np.complex64`` or ``np.complex128``. Default is ``np.complex128``. **kwargs: keyword arguments. The following options are currently supported: @@ -178,16 +178,15 @@ class DefaultTensor(Device): "max_bond_dim", ) - _new_API = True - def __init__( self, - wires=None, + wires, method="mps", shots=None, dtype=np.complex128, **kwargs, ) -> None: + print(has_quimb) if not has_quimb: raise ImportError( "This feature requires quimb, a library for tensor network manipulations. " @@ -265,7 +264,7 @@ def _reset_state(self) -> None: """Reset the MPS.""" self._circuitMPS = qtn.CircuitMPS(psi0=self._initial_mps()) - def _initial_mps(self) -> qtn.MatrixProductState: + def _initial_mps(self) -> "qtn.MatrixProductState": r""" Return an initial state to :math:`\ket{0}`. diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index 36c9951c882..2f984ebf79b 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -125,17 +125,23 @@ def test_name(): """Test the name of DefaultTensor.""" - assert qml.device("default.tensor").name == "default.tensor" + assert qml.device("default.tensor", wires=0).name == "default.tensor" def test_wires(): """Test that a device can be created with wires.""" - assert qml.device("default.tensor").wires is None + assert qml.device("default.tensor", wires=0).wires is not None assert qml.device("default.tensor", wires=2).wires == qml.wires.Wires([0, 1]) assert qml.device("default.tensor", wires=[0, 2]).wires == qml.wires.Wires([0, 2]) with pytest.raises(AttributeError): - qml.device("default.tensor").wires = [0, 1] + qml.device("default.tensor", wires=0).wires = [0, 1] + + +def test_wires_error(): + """Test that an error is raised if the wires are not provided.""" + with pytest.raises(TypeError): + qml.device("default.tensor") def test_wires_execution_error(): @@ -163,7 +169,7 @@ def test_kwargs(max_bond_dim, cutoff, contract): kwargs = {"max_bond_dim": max_bond_dim, "cutoff": cutoff, "contract": contract} - dev = qml.device("default.tensor", **kwargs) + dev = qml.device("default.tensor", wires=0, **kwargs) _, config = dev.preprocess() assert config.device_options["method"] == "mps" @@ -178,40 +184,39 @@ def test_invalid_kwarg(): TypeError, match="Unexpected argument: fake_arg during initialization of the default.tensor device.", ): - qml.device("default.tensor", fake_arg=None) + qml.device("default.tensor", wires=0, fake_arg=None) def test_method(): """Test the device method.""" - assert qml.device("default.tensor").method == "mps" + assert qml.device("default.tensor", wires=0).method == "mps" + +def test_invalid_method(): + """Test an invalid method.""" + method = "invalid_method" + with pytest.raises(ValueError, match=f"Unsupported method: {method}"): + qml.device("default.tensor", wires=0, method=method) @pytest.mark.parametrize("dtype", [np.complex64, np.complex128]) def test_data_type(dtype): """Test the data type.""" - assert qml.device("default.tensor", dtype=dtype).dtype == dtype + assert qml.device("default.tensor", wires=0, dtype=dtype).dtype == dtype def test_ivalid_data_type(): """Test that data type can only be np.complex64 or np.complex128.""" with pytest.raises(TypeError): - qml.device("default.tensor", dtype=float) - - -def test_invalid_method(): - """Test an invalid method.""" - method = "invalid_method" - with pytest.raises(ValueError, match=f"Unsupported method: {method}"): - qml.device("default.tensor", method=method) + qml.device("default.tensor", wires=0, dtype=float) def test_invalid_shots(): """Test that an error is raised if finite number of shots are requestd.""" with pytest.raises(ValueError, match="default.tensor does not support finite shots."): - qml.device("default.tensor", shots=5) + qml.device("default.tensor", wires=0, shots=5) with pytest.raises(AttributeError): - qml.device("default.tensor").shots = 10 + qml.device("default.tensor", wires=0).shots = 10 class TestSupportedGatesAndObservables: @@ -272,12 +277,12 @@ class TestSupportsDerivatives: def test_support_derivatives(self): """Test that the device does not support derivatives yet.""" - dev = qml.device("default.tensor") + dev = qml.device("default.tensor", wires=0) assert not dev.supports_derivatives() def test_compute_derivatives(self): """Test that an error is raised if the `compute_derivatives` method is called.""" - dev = qml.device("default.tensor") + dev = qml.device("default.tensor", wires=0) with pytest.raises( NotImplementedError, match="The computation of derivatives has yet to be implemented for the default.tensor device.", @@ -286,7 +291,7 @@ def test_compute_derivatives(self): def test_execute_and_compute_derivatives(self): """Test that an error is raised if `execute_and_compute_derivative` method is called.""" - dev = qml.device("default.tensor") + dev = qml.device("default.tensor", wires=0) with pytest.raises( NotImplementedError, match="The computation of derivatives has yet to be implemented for the default.tensor device.", @@ -295,12 +300,12 @@ def test_execute_and_compute_derivatives(self): def test_supports_vjp(self): """Test that the device does not support VJP yet.""" - dev = qml.device("default.tensor") + dev = qml.device("default.tensor", wires=0) assert not dev.supports_vjp() def test_compute_vjp(self): """Test that an error is raised if `compute_vjp` method is called.""" - dev = qml.device("default.tensor") + dev = qml.device("default.tensor", wires=0) with pytest.raises( NotImplementedError, match="The computation of vector-Jacobian product has yet to be implemented for the default.tensor device.", @@ -309,7 +314,7 @@ def test_compute_vjp(self): def test_execute_and_compute_vjp(self): """Test that an error is raised if `execute_and_compute_vjp` method is called.""" - dev = qml.device("default.tensor") + dev = qml.device("default.tensor", wires=0) with pytest.raises( NotImplementedError, match="The computation of vector-Jacobian product has yet to be implemented for the default.tensor device.", From 58163eaf9858dd37a3caf2ab7f9cf80ca3445afb Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Tue, 21 May 2024 16:25:25 -0400 Subject: [PATCH 19/29] Forgot removal --- pennylane/devices/default_tensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index f1e89bd968f..18d21fe8546 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -186,7 +186,7 @@ def __init__( dtype=np.complex128, **kwargs, ) -> None: - print(has_quimb) + if not has_quimb: raise ImportError( "This feature requires quimb, a library for tensor network manipulations. " From d7a151f11c1814292a2f8e7095bd68173432dc99 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Tue, 21 May 2024 17:26:13 -0400 Subject: [PATCH 20/29] Format check in test default tensor --- tests/devices/default_tensor/test_default_tensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index 2f984ebf79b..022a6868f3e 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -191,6 +191,7 @@ def test_method(): """Test the device method.""" assert qml.device("default.tensor", wires=0).method == "mps" + def test_invalid_method(): """Test an invalid method.""" method = "invalid_method" From 30b6b58f6a004151f0cd08e570345207096554e5 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Tue, 21 May 2024 18:26:07 -0400 Subject: [PATCH 21/29] importing `default.tensor` in `init` file --- pennylane/devices/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane/devices/__init__.py b/pennylane/devices/__init__.py index 3da86933daa..5c21620241e 100644 --- a/pennylane/devices/__init__.py +++ b/pennylane/devices/__init__.py @@ -151,12 +151,12 @@ def execute(self, circuits, execution_config = qml.devices.DefaultExecutionConfi # DefaultQubitTF and DefaultQubitAutograd not imported here since this # would lead to an automatic import of tensorflow and autograd, which are -# not PennyLane core dependencies. DefaultTensor is not imported here -# since it would lead to an automatic import of quimb. +# not PennyLane core dependencies. from .default_qubit_legacy import DefaultQubitLegacy from .default_gaussian import DefaultGaussian from .default_mixed import DefaultMixed from .default_clifford import DefaultClifford from .null_qubit import NullQubit from .default_qutrit_mixed import DefaultQutritMixed +from .default_tensor import DefaultTensor from .._device import Device as LegacyDevice From 1ee0da61ba66fd3269456472e3afdb783e62d815 Mon Sep 17 00:00:00 2001 From: albi3ro Date: Wed, 22 May 2024 09:00:50 -0400 Subject: [PATCH 22/29] stop testing a wire value of None --- tests/data/attributes/operator/test_operator.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/data/attributes/operator/test_operator.py b/tests/data/attributes/operator/test_operator.py index 0f7d9db085d..a528553a2d6 100644 --- a/tests/data/attributes/operator/test_operator.py +++ b/tests/data/attributes/operator/test_operator.py @@ -41,12 +41,10 @@ pauli_ops = [ op_cls(wires) - for op_cls, wires in itertools.product( - [qml.PauliX, qml.PauliY, qml.PauliZ], [0, 1, "q", None, [1]] - ) + for op_cls, wires in itertools.product([qml.PauliX, qml.PauliY, qml.PauliZ], [0, 1, "q", [1]]) ] -identity = [qml.Identity(wires) for wires in [0, 1, "q", None, [1, "a"]]] +identity = [qml.Identity(wires) for wires in [0, 1, "q", [1, "a"]]] hamiltonians = [ qml.Hamiltonian(*args) From 5050585d8dbaad76601c64ec51f148da26b96779 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Wed, 22 May 2024 09:07:29 -0400 Subject: [PATCH 23/29] Removed string in doc specifying default value of `wires` argument (which now is a positional argument) --- pennylane/devices/__init__.py | 2 +- pennylane/devices/default_tensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane/devices/__init__.py b/pennylane/devices/__init__.py index 5c21620241e..f8297638854 100644 --- a/pennylane/devices/__init__.py +++ b/pennylane/devices/__init__.py @@ -151,7 +151,7 @@ def execute(self, circuits, execution_config = qml.devices.DefaultExecutionConfi # DefaultQubitTF and DefaultQubitAutograd not imported here since this # would lead to an automatic import of tensorflow and autograd, which are -# not PennyLane core dependencies. +# not PennyLane core dependencies from .default_qubit_legacy import DefaultQubitLegacy from .default_gaussian import DefaultGaussian from .default_mixed import DefaultMixed diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index 18d21fe8546..a0b60f05faa 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -150,7 +150,7 @@ class DefaultTensor(Device): Args: wires (int, Iterable[Number, str]): Number of wires present on the device, or iterable that contains unique labels for the wires as numbers (i.e., ``[-1, 0, 2]``) or strings - (``['aux_wire', 'q1', 'q2']``). Default is ``None``. + (``['aux_wire', 'q1', 'q2']``). shots (int, Sequence[int], Sequence[Union[int, Sequence[int]]]): The default number of shots to use in executions involving this device. Currently, it can only be ``None``, so that computation of statistics like expectation values and variances is performed analytically. From dac89e6fe154bc959bb795de8444ea559edbdbaf Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Thu, 23 May 2024 09:34:30 -0400 Subject: [PATCH 24/29] Catching the case of `wires=None` in class instantion --- pennylane/devices/default_tensor.py | 4 ++++ tests/devices/default_tensor/test_default_tensor.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index a0b60f05faa..6b67a2be7fe 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -181,12 +181,16 @@ class DefaultTensor(Device): def __init__( self, wires, + *, method="mps", shots=None, dtype=np.complex128, **kwargs, ) -> None: + if wires is None: + raise TypeError(f"Wires must be provided for the default.tensor device.") + if not has_quimb: raise ImportError( "This feature requires quimb, a library for tensor network manipulations. " diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index 022a6868f3e..cb80dbbfcf6 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -143,6 +143,9 @@ def test_wires_error(): with pytest.raises(TypeError): qml.device("default.tensor") + with pytest.raises(TypeError): + qml.device("default.tensor", wires=None) + def test_wires_execution_error(): """Test that this device cannot execute a tape if its wires do not match the wires on the device.""" From 686b2b84d66dc9bfa191712cb6b150039ee5aee7 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Thu, 23 May 2024 09:37:38 -0400 Subject: [PATCH 25/29] Typo in formatting --- pennylane/devices/default_tensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index 6b67a2be7fe..d82bd0f8047 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -189,7 +189,7 @@ def __init__( ) -> None: if wires is None: - raise TypeError(f"Wires must be provided for the default.tensor device.") + raise TypeError("Wires must be provided for the default.tensor device.") if not has_quimb: raise ImportError( From f16d36459345baf8d753c2dbf16154672f2b68da Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Thu, 23 May 2024 09:46:42 -0400 Subject: [PATCH 26/29] Removing the `shot` argument --- pennylane/devices/default_tensor.py | 9 +-------- tests/devices/default_tensor/test_default_tensor.py | 9 --------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index d82bd0f8047..ce08769eba1 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -151,9 +151,6 @@ class DefaultTensor(Device): wires (int, Iterable[Number, str]): Number of wires present on the device, or iterable that contains unique labels for the wires as numbers (i.e., ``[-1, 0, 2]``) or strings (``['aux_wire', 'q1', 'q2']``). - shots (int, Sequence[int], Sequence[Union[int, Sequence[int]]]): The default number of shots - to use in executions involving this device. Currently, it can only be ``None``, so that computation of - statistics like expectation values and variances is performed analytically. method (str): Supported method. Currently, only ``"mps"`` is supported. dtype (type): Datatype for the tensor representation. Must be one of ``np.complex64`` or ``np.complex128``. Default is ``np.complex128``. @@ -183,7 +180,6 @@ def __init__( wires, *, method="mps", - shots=None, dtype=np.complex128, **kwargs, ) -> None: @@ -202,15 +198,12 @@ def __init__( f"Unsupported method: {method}. The only currently supported method is mps." ) - if shots is not None: - raise ValueError("default.tensor does not support finite shots.") - if dtype not in [np.complex64, np.complex128]: raise TypeError( f"Unsupported type: {dtype}. Supported types are np.complex64 and np.complex128." ) - super().__init__(wires=wires, shots=shots) + super().__init__(wires=wires, shots=None) self._method = method self._dtype = dtype diff --git a/tests/devices/default_tensor/test_default_tensor.py b/tests/devices/default_tensor/test_default_tensor.py index cb80dbbfcf6..4fdf300ee95 100644 --- a/tests/devices/default_tensor/test_default_tensor.py +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -214,15 +214,6 @@ def test_ivalid_data_type(): qml.device("default.tensor", wires=0, dtype=float) -def test_invalid_shots(): - """Test that an error is raised if finite number of shots are requestd.""" - with pytest.raises(ValueError, match="default.tensor does not support finite shots."): - qml.device("default.tensor", wires=0, shots=5) - - with pytest.raises(AttributeError): - qml.device("default.tensor", wires=0).shots = 10 - - class TestSupportedGatesAndObservables: """Test that the DefaultTensor device supports all gates and observables that it claims to support.""" From 37fe8dd687432de9b0f087114611f21130c94b78 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Thu, 23 May 2024 10:34:25 -0400 Subject: [PATCH 27/29] Typo in string [ci skip] --- pennylane/devices/default_tensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index ce08769eba1..8efef07d2b1 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -132,12 +132,12 @@ def accepted_methods(method: str) -> bool: def stopping_condition(op: qml.operation.Operator) -> bool: - """A function that determines if an operation is supported by ``lightning.tensor`` for this interface.""" + """A function that determines if an operation is supported by ``default.tensor`` for this interface.""" return op.name in _operations def accepted_observables(obs: qml.operation.Operator) -> bool: - """A function that determines if an observable is supported by ``lightning.tensor`` for this interface.""" + """A function that determines if an observable is supported by ``default.tensor`` for this interface.""" return obs.name in _observables From 73d596e91c49d388b5c120b61d257ce2bb658d02 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Thu, 23 May 2024 10:35:35 -0400 Subject: [PATCH 28/29] Typo in string [ci skip] --- pennylane/devices/default_tensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index 8efef07d2b1..5c850329fe8 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -132,12 +132,12 @@ def accepted_methods(method: str) -> bool: def stopping_condition(op: qml.operation.Operator) -> bool: - """A function that determines if an operation is supported by ``default.tensor`` for this interface.""" + """A function that determines if an operation is supported by ``default.tensor``.""" return op.name in _operations def accepted_observables(obs: qml.operation.Operator) -> bool: - """A function that determines if an observable is supported by ``default.tensor`` for this interface.""" + """A function that determines if an observable is supported by ``default.tensor``.""" return obs.name in _observables From a5937f806ca392632e6ef479523428e5f3155598 Mon Sep 17 00:00:00 2001 From: PietropaoloFrisoni Date: Thu, 23 May 2024 14:58:17 -0400 Subject: [PATCH 29/29] Triggering CI