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/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/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index b5dfab96aa3..cfdfee22ab3 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/pennylane/devices/__init__.py b/pennylane/devices/__init__.py index ecbb9ef5fe7..f8297638854 100644 --- a/pennylane/devices/__init__.py +++ b/pennylane/devices/__init__.py @@ -158,4 +158,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..5c850329fe8 --- /dev/null +++ b/pennylane/devices/default_tensor.py @@ -0,0 +1,595 @@ +# 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 using ``quimb``. +""" +import copy +from dataclasses import replace +from numbers import Number +from typing import Callable, Optional, Sequence, Tuple, Union + +import numpy as np + +import pennylane as qml +from pennylane.devices import DefaultExecutionConfig, Device, ExecutionConfig +from pennylane.devices.modifiers import simulator_tracking, single_tape_support +from pennylane.devices.preprocess import ( + decompose, + validate_device_wires, + validate_measurements, + validate_observables, +) +from pennylane.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 + +Result_or_ResultBatch = Union[Result, ResultBatch] +QuantumTapeBatch = Sequence[QuantumTape] +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", + "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 ``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``.""" + return obs.name in _observables + + +@simulator_tracking +@single_tape_support +class DefaultTensor(Device): + """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 + contains unique labels for the wires as numbers (i.e., ``[-1, 0, 2]``) or strings + (``['aux_wire', 'q1', 'q2']``). + 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", + ) + + def __init__( + self, + wires, + *, + method="mps", + dtype=np.complex128, + **kwargs, + ) -> None: + + if wires is None: + raise TypeError("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. " + "It can be installed with:\n\npip install quimb" + ) # pragma: no cover + + if not accepted_methods(method): + raise ValueError( + f"Unsupported method: {method}. The only currently supported method is mps." + ) + + 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=None) + + 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" * (len(self._wires) if self._wires else 1), + "dtype": self._dtype.__name__, + "tags": [str(l) for l in self._wires.labels] if self._wires else None, + } + + 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: + # 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)) + + 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 + + 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/setup.py b/setup.py index 15863f6e839..892e962ec14 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 new file mode 100644 index 00000000000..4fdf300ee95 --- /dev/null +++ b/tests/devices/default_tensor/test_default_tensor.py @@ -0,0 +1,352 @@ +# 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 pytest +from scipy.sparse import csr_matrix + +import pennylane as qml + +quimb = pytest.importorskip("quimb") + +pytestmark = pytest.mark.external + +# gates for which device support is tested +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]), + "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 = operations_list.keys() + +# observables for which device support is tested +observables_list = { + "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 = observables_list.keys() + + +def test_name(): + """Test the name of DefaultTensor.""" + 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=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).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") + + 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.""" + 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"]) +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 = qml.device("default.tensor", wires=0, **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="Unexpected argument: fake_arg during initialization of the default.tensor device.", + ): + qml.device("default.tensor", wires=0, fake_arg=None) + + +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" + 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", 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", wires=0, dtype=float) + + +class TestSupportedGatesAndObservables: + """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 device can implement all its supported gates.""" + + dev = qml.device("default.tensor", wires=4, method="mps") + + tape = qml.tape.QuantumScript( + [operations_list[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 device can implement all its supported observables.""" + + dev = qml.device("default.tensor", wires=3, method="mps") + + if observable == "Projector": + for o in observables_list[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(observables_list[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.""" + + op = [qml.Identity(0)] + measurements = [qml.probs(qml.PauliZ(0))] + tape = qml.tape.QuantumScript(op, measurements) + + dev = qml.device("default.tensor", 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 = 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", wires=0) + 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 = 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.", + ): + dev.execute_and_compute_derivatives(circuits=None) + + def test_supports_vjp(self): + """Test that the device does not support VJP yet.""" + 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", wires=0) + 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 = 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.", + ): + 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_tensor_expval.py b/tests/devices/default_tensor/test_tensor_expval.py new file mode 100644 index 00000000000..663dba1ee64 --- /dev/null +++ b/tests/devices/default_tensor/test_tensor_expval.py @@ -0,0 +1,399 @@ +# 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 DefaultTensor device. +""" + +import numpy as np +import pytest + +import pennylane as qml +from pennylane.devices import DefaultQubit + +THETA = np.linspace(0.11, 1, 3) +PHI = np.linspace(0.32, 1, 3) +VARPHI = np.linspace(0.02, 1, 3) + +quimb = pytest.importorskip("quimb") + +pytestmark = pytest.mark.external + +# 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]) + results = dev.execute(tapes) + return transf_fn(results) + + +def execute(dev, tape): + """Execute the tape on the device and return the result.""" + 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.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.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.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 = qml.device("default.tensor", wires=wires, 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.dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + # pylint: disable=too-many-arguments + @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.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.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.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.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.dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + +@pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) +def test_multi_qubit_gates(theta, phi, dev): + """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 = 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_tensor_var.py b/tests/devices/default_tensor/test_tensor_var.py new file mode 100644 index 00000000000..a4aa35e40ed --- /dev/null +++ b/tests/devices/default_tensor/test_tensor_var.py @@ -0,0 +1,406 @@ +# 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 DefaultTensor device. +""" +import numpy as np +import pytest + +import pennylane as qml +from pennylane.tape import QuantumScript + +THETA = np.linspace(0.11, 1, 3) +PHI = np.linspace(0.32, 1, 3) +VARPHI = np.linspace(0.02, 1, 3) + +quimb = pytest.importorskip("quimb") + +pytestmark = pytest.mark.external + +# 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]) + results = dev.execute(tapes) + return transf_fn(results) + + +def execute(dev, tape): + """Execute the tape on the device and return the result.""" + 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.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.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.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 = qml.device("default.tensor", wires=wires, 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.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.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.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.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.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.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.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.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.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.dtype == np.complex64 else 1e-7 + + assert np.allclose(calculated_val, reference_val, tol) + + +@pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) +def test_multi_qubit_gates(theta, phi, dev): + """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 = qml.device("default.tensor", wires=tape.wires, dtype=np.complex128) + calculated_val = dev.execute(tape) + + assert np.allclose(calculated_val, reference_val)