From a3071d35f21fbe9da74e15b893e4bf07e7809345 Mon Sep 17 00:00:00 2001 From: Christina Lee Date: Fri, 15 Mar 2024 17:05:41 -0400 Subject: [PATCH] Add `LightningQubit2` class (#607) * lightning interface for new device api * Auto update version * Merge Master * update version * update version * update lightning_qubit_2 * add LightningQubit2 to init * add LightningStateVector class * add LightningMeasurements class * add new QuantumScriptSerializer class * allow lightning.qubit2 to be tested within our suite * add tests and CI workflow for lightning_qubit_2 * update CI * update CI * add wire mapping, black * add tests for custom wires * add tests for custom wires * add review suggestions * format * remove python class to reverse order of PRs * update simulate to get a LightningStateVector * add reset state * update simulate * update docs * add Result import * Update pennylane_lightning/lightning_qubit/_measurements.py Co-authored-by: Christina Lee * Update pennylane_lightning/lightning_qubit/_measurements.py Co-authored-by: Christina Lee * fix reset state * Update pennylane_lightning/lightning_qubit/_state_vector.py Co-authored-by: Christina Lee * Update pennylane_lightning/lightning_qubit/_state_vector.py Co-authored-by: Christina Lee * Update pennylane_lightning/lightning_qubit/_state_vector.py Co-authored-by: Christina Lee * remove LightningQubit2 references * remove unnecessary modules * merging Serializer classes * update serialize tests * update measurements with new serialize class * remove outdated test * remove obsolete tests * remove unused dtype input from simulate * update measurements * update state_vector * update lightning_qubit2 * format * pylint * Update pennylane_lightning/lightning_qubit/_state_vector.py Co-authored-by: Christina Lee * remove old comment * some review suggestions * Auto update version * remove print * update state vector class * add state vector class tests * adding measurement tests * update state vector and tests * move and rename test files, and format * Auto update version * skip measurements class for other devices and in the absence of binaries * format * update measurements class * expand measurement class testing * garbage collection * typo * update coverage and StateVector class * expand measurements class coverage * Auto update version * add coverage for n-controlled operations * add map to standard wires to get_final_state for safety * update jax config import * Auto update version * trigger CI * update state vector class and tests for improved coverage * update measurement class tests * update dev version * remove device definition * update dev version * clean test_measurements_class.py * isort+black * review suggestion * fix docs * increase tolerance * Auto update version * isort * update dev version * add LightningAdjointJacobian class * add unit tests for the LightningAdjointJacobian class * format * add changelog for PR #613 * update changelog * update adjoint Jacobian * codefactor * fix processing_fn_expval * make a proper new_tape * trigger CI * Update .github/CHANGELOG.md Co-authored-by: Vincent Michaud-Rioux * Update pennylane_lightning/lightning_qubit/_adjoint_jacobian.py Co-authored-by: Vincent Michaud-Rioux * Update pennylane_lightning/lightning_qubit/_adjoint_jacobian.py Co-authored-by: Vincent Michaud-Rioux * Add probs support. * Add double-obs tests. * Add qml.var support. * Add probs support. * Add measurement tests with wires. * pytest.skip tests * Fix format * update * adding tests from add-simulate branch * merge conflicts * create state vector on initialization * remove import of modifier from lightning * Update pennylane_lightning/lightning_qubit/lightning_qubit2.py * minor test updates * register with setup.py, state vector fixes * add LightningQubit2 to init and format * add cpp binary available variable * reduce dependency on DefaultQubit for tests * update LightningQubit2 * Fixing rebase artifacts * remove adjoint diff support from supports derivatives * [skip ci] Added skeleton file for LQ2 unit tests * Lightning qubit2 upgrade api (#628) * update * adding tests from add-simulate branch * merge conflicts * create state vector on initialization * remove import of modifier from lightning * Update pennylane_lightning/lightning_qubit/lightning_qubit2.py * minor test updates * register with setup.py, state vector fixes * add LightningQubit2 to init and format * add cpp binary available variable * Auto update version * reduce dependency on DefaultQubit for tests * update LightningQubit2 * Introduce _new_API and fix/skip few tests. * Fix few more tests. * Skip shots, adjoint, vjp with new API. * Fix no-bin interface. * Remove duplicate class data. * Include LQ2 in linux ests. * --cov-append --------- Co-authored-by: albi3ro Co-authored-by: AmintorDusko Co-authored-by: Dev version update bot * Added init tests; Added skeleton tests for helpers * Resolving rebase artifacts * Added tests; integrated jacobian * Update pennylane_lightning/lightning_qubit/lightning_qubit2.py Co-authored-by: Amintor Dusko <87949283+AmintorDusko@users.noreply.github.com> * Auto update version * Small update to simulate_and_jacobian * Added execution tests; TODO: diff tests * Added derivative tests; TODO: tape batch tests * Finished adding LQ2 tests; TODO: Add var, probs tests * Minor test change * Added var tests; TODO: add probs tests * Auto update version * Added helpers to probs test file * Fixing tests * Fixed no trainable params test * Fixing test collection for new API * Updated apply tests, made LQ2 ops/obs attributes instead of properties * Fixed comparison,expval,gate tests * Added tests; Hamiltonian diff tests are failing * Updated tests for mode adjoint cases * Added more observables; stopped skipping adj-jac tests * Auto update version * Trigger CI * Fixed tests; added docstrings * Linting; formatting * Updated tests per code review * Linting * Fixed gate tests * Trigger CI * Trigger CI * Fixed adj-jac tests * Tidying up * Fixed tf/jax adj-jac tests * Apply suggestions from code review Co-authored-by: Amintor Dusko <87949283+AmintorDusko@users.noreply.github.com> * Adding coverage; addressing code review * Minor updates per code review * Auto update version * Skipping finite shots measurements tests * Update PL requirements; add LQ2 to test without binaries * Apply suggestions from code review Co-authored-by: Amintor Dusko <87949283+AmintorDusko@users.noreply.github.com> * Upadte unnecessarily skipped tests * Auto update version * [skip ci] Skip CI * Removing overlap test * Added dtype to old and new API; updating tests * Added Fallback device dtype property * Updated no binary tests * Auto update version * [skip ci] Skip CI * Trigger CI * Running isort * Trigger CI * Fixed LQ import in MP test; changed docstrings to inline comments * Update tests to use new API * Add changelog entry * Update changelog entry * Apply suggestions from code review Co-authored-by: Ali Asadi <10773383+maliasadi@users.noreply.github.com> * Added TODOs; Updated derivative tests to use batch_obs --------- Co-authored-by: Dev version update bot Co-authored-by: AmintorDusko Co-authored-by: Amintor Dusko <87949283+AmintorDusko@users.noreply.github.com> Co-authored-by: Vincent Michaud-Rioux Co-authored-by: Vincent Michaud-Rioux Co-authored-by: Mudit Pandey Co-authored-by: Ali Asadi <10773383+maliasadi@users.noreply.github.com> --- .github/CHANGELOG.md | 6 +- .github/workflows/tests_linux.yml | 1 + .github/workflows/tests_without_binary.yml | 1 + pennylane_lightning/core/_serialize.py | 2 +- pennylane_lightning/core/_version.py | 2 +- pennylane_lightning/core/lightning_base.py | 12 + .../lightning_qubit/__init__.py | 1 + .../lightning_qubit/_adjoint_jacobian.py | 19 +- .../lightning_qubit/_state_vector.py | 1 - .../lightning_qubit/lightning_qubit2.py | 516 ++++++++++++++ setup.py | 7 +- tests/conftest.py | 17 +- .../test_adjoint_jacobian_class.py | 33 +- .../test_measurements_class.py | 43 +- .../test_measurements_samples_MCMC.py | 6 +- .../test_state_vector_class.py | 9 +- tests/lightning_qubit2/test_expval_2.py | 338 ++++++++++ tests/lightning_qubit2/test_new_api_device.py | 636 ++++++++++++++++++ tests/lightning_qubit2/test_no_binaries.py | 32 + tests/lightning_qubit2/test_var_2.py | 351 ++++++++++ tests/test_adjoint_jacobian.py | 192 ++++-- tests/test_apply.py | 63 +- tests/test_comparison.py | 15 +- tests/test_decomposition.py | 1 + tests/test_execute.py | 5 +- tests/test_expval.py | 11 +- tests/test_gates.py | 12 +- tests/test_measurements.py | 17 +- tests/test_var.py | 7 +- tests/test_vjp.py | 3 + 30 files changed, 2216 insertions(+), 143 deletions(-) create mode 100644 pennylane_lightning/lightning_qubit/lightning_qubit2.py create mode 100644 tests/lightning_qubit2/test_expval_2.py create mode 100644 tests/lightning_qubit2/test_new_api_device.py create mode 100644 tests/lightning_qubit2/test_no_binaries.py create mode 100644 tests/lightning_qubit2/test_var_2.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index cb2ca1fd6f..91c865d1cc 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -11,6 +11,10 @@ * Add LightningAdjointJacobian to support `lightning.qubit2`. [(#631)](https://github.com/PennyLaneAI/pennylane-lightning/pull/631) +* Add `lightning.qubit2` device which uses the new device API. + [(#607)](https://github.com/PennyLaneAI/pennylane-lightning/pull/607) + [(#628)](https://github.com/PennyLaneAI/pennylane-lightning/pull/628) + ### Breaking changes ### Improvements @@ -32,7 +36,7 @@ This release contains contributions from (in alphabetical order): -Ali Asadi, Amintor Dusko, Vincent Michaud-Rioux +Ali Asadi, Amintor Dusko, Christina Lee, Vincent Michaud-Rioux, Mudit Pandey --- diff --git a/.github/workflows/tests_linux.yml b/.github/workflows/tests_linux.yml index f1641b27d2..4a0c85bd9e 100644 --- a/.github/workflows/tests_linux.yml +++ b/.github/workflows/tests_linux.yml @@ -185,6 +185,7 @@ jobs: cd main/ DEVICENAME=`echo ${{ matrix.pl_backend }} | sed "s/_/./g"` PL_DEVICE=${DEVICENAME} python -m pytest tests/ $COVERAGE_FLAGS + PL_DEVICE=lightning_qubit2 python -m pytest tests/ $COVERAGE_FLAGS --cov-append pl-device-test --device ${DEVICENAME} --skip-ops --shots=20000 $COVERAGE_FLAGS --cov-append pl-device-test --device ${DEVICENAME} --shots=None --skip-ops $COVERAGE_FLAGS --cov-append mv .coverage .coverage-${{ github.job }}-${{ matrix.pl_backend }} diff --git a/.github/workflows/tests_without_binary.yml b/.github/workflows/tests_without_binary.yml index 680b2b75c3..febea38be3 100644 --- a/.github/workflows/tests_without_binary.yml +++ b/.github/workflows/tests_without_binary.yml @@ -107,6 +107,7 @@ jobs: cd main/ DEVICENAME=`echo ${{ matrix.pl_backend }} | sed "s/_/./g"` PL_DEVICE=${DEVICENAME} python -m pytest tests/ $COVERAGE_FLAGS + if [ ${{ matrix.pl_backend }} == "lightning_qubit" ]; then PL_DEVICE=lightning_qubit2 python -m pytest tests/ $COVERAGE_FLAGS --cov-append; fi pl-device-test --device ${DEVICENAME} --skip-ops --shots=20000 $COVERAGE_FLAGS --cov-append pl-device-test --device ${DEVICENAME} --shots=None --skip-ops $COVERAGE_FLAGS --cov-append diff --git a/pennylane_lightning/core/_serialize.py b/pennylane_lightning/core/_serialize.py index fb9ff6dd82..502456533a 100644 --- a/pennylane_lightning/core/_serialize.py +++ b/pennylane_lightning/core/_serialize.py @@ -62,7 +62,7 @@ def __init__( self.use_csingle = use_csingle self.device_name = device_name self.split_obs = split_obs - if device_name == "lightning.qubit": + if device_name in ("lightning.qubit", "lightning.qubit2"): try: import pennylane_lightning.lightning_qubit_ops as lightning_ops except ImportError as exception: diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index ea4384e017..f95ce9d020 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.36.0-dev9" +__version__ = "0.36.0-dev10" diff --git a/pennylane_lightning/core/lightning_base.py b/pennylane_lightning/core/lightning_base.py index 42a0093f28..650fb0ed84 100644 --- a/pennylane_lightning/core/lightning_base.py +++ b/pennylane_lightning/core/lightning_base.py @@ -60,6 +60,7 @@ class LightningBase(QubitDevice): author = "Xanadu Inc." short_name = "lightning.base" _CPP_BINARY_AVAILABLE = True + _new_API = False def __init__( self, @@ -80,6 +81,11 @@ def __init__( super().__init__(wires, shots=shots, r_dtype=r_dtype, c_dtype=c_dtype) self._batch_obs = batch_obs + @property + def dtype(self): + """State vector complex data type.""" + return self.C_DTYPE + @property def stopping_condition(self): """.BooleanFn: Returns the stopping condition for the device. The returned @@ -396,6 +402,7 @@ class LightningBaseFallBack(DefaultQubitLegacy): # pragma: no cover version = __version__ author = "Xanadu Inc." _CPP_BINARY_AVAILABLE = False + _new_API = False def __init__(self, wires, *, c_dtype=np.complex128, **kwargs): if c_dtype is np.complex64: @@ -410,3 +417,8 @@ def __init__(self, wires, *, c_dtype=np.complex128, **kwargs): def state_vector(self): """Returns a handle to the statevector.""" return self._state + + @property + def dtype(self): + """State vector complex data type.""" + return self.C_DTYPE diff --git a/pennylane_lightning/lightning_qubit/__init__.py b/pennylane_lightning/lightning_qubit/__init__.py index a1b792afde..017f7fedec 100644 --- a/pennylane_lightning/lightning_qubit/__init__.py +++ b/pennylane_lightning/lightning_qubit/__init__.py @@ -16,3 +16,4 @@ from pennylane_lightning.core import __version__ from .lightning_qubit import LightningQubit +from .lightning_qubit2 import LightningQubit2 diff --git a/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py b/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py index 14695b1277..ae45a550e6 100644 --- a/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py +++ b/pennylane_lightning/lightning_qubit/_adjoint_jacobian.py @@ -16,7 +16,6 @@ """ from os import getenv from typing import List -from warnings import warn import numpy as np import pennylane as qml @@ -193,11 +192,10 @@ def calculate_jacobian(self, tape: QuantumTape): """ if tape.shots: - warn( + raise QuantumFunctionError( "Requested adjoint differentiation to be computed with finite shots. " "The derivative is always exact when using the adjoint " - "differentiation method.", - UserWarning, + "differentiation method." ) tape_return_type = self._get_return_type(tape.measurements) @@ -208,6 +206,12 @@ def calculate_jacobian(self, tape: QuantumTape): if tape_return_type is State: raise QuantumFunctionError("This method does not support statevector return type. ") + if any(m.return_type is not Expectation for m in tape.measurements): + raise QuantumFunctionError( + "Adjoint differentiation method does not support expectation return type " + "mixed with other return types" + ) + processed_data = self._process_jacobian_tape(tape) if not processed_data: # training_params is empty @@ -271,12 +275,11 @@ def calculate_vjp(self, tape: QuantumTape, grad_vec): Returns: The vector-Jacobian products of a tape. """ - if tape.shots is not None: - warn( + if tape.shots: + raise QuantumFunctionError( "Requested adjoint differentiation to be computed with finite shots. " "The derivative is always exact when using the adjoint differentiation " - "method.", - UserWarning, + "method." ) measurements = tape.measurements diff --git a/pennylane_lightning/lightning_qubit/_state_vector.py b/pennylane_lightning/lightning_qubit/_state_vector.py index c91f65f697..63cb791b8f 100644 --- a/pennylane_lightning/lightning_qubit/_state_vector.py +++ b/pennylane_lightning/lightning_qubit/_state_vector.py @@ -237,7 +237,6 @@ def _apply_lightning_controlled(self, operation): method(control_wires, control_values, target_wires, inv, param) else: # apply gate as an n-controlled matrix method = getattr(state, "applyControlledMatrix") - target_wires = self.wires.indices(operation.target_wires) method( qml.matrix(operation.base), control_wires, diff --git a/pennylane_lightning/lightning_qubit/lightning_qubit2.py b/pennylane_lightning/lightning_qubit/lightning_qubit2.py new file mode 100644 index 0000000000..7bd9e3fa02 --- /dev/null +++ b/pennylane_lightning/lightning_qubit/lightning_qubit2.py @@ -0,0 +1,516 @@ +# 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 LightningQubit2 class that inherits from the new device interface. +""" +from dataclasses import replace +from typing import Callable, Optional, Sequence, Union + +import numpy as np +import pennylane as qml +from pennylane.devices import DefaultExecutionConfig, Device, ExecutionConfig +from pennylane.devices.default_qubit import adjoint_ops +from pennylane.devices.modifiers import simulator_tracking, single_tape_support +from pennylane.devices.preprocess import ( + decompose, + no_sampling, + validate_adjoint_trainable_params, + validate_device_wires, + validate_measurements, + validate_observables, +) +from pennylane.tape import QuantumScript, QuantumTape +from pennylane.transforms.core import TransformProgram +from pennylane.typing import Result, ResultBatch + +from ._adjoint_jacobian import LightningAdjointJacobian +from ._measurements import LightningMeasurements +from ._state_vector import LightningStateVector + +try: + # pylint: disable=import-error, unused-import + import pennylane_lightning.lightning_qubit_ops + + LQ_CPP_BINARY_AVAILABLE = True +except ImportError: + LQ_CPP_BINARY_AVAILABLE = False + +Result_or_ResultBatch = Union[Result, ResultBatch] +QuantumTapeBatch = Sequence[QuantumTape] +QuantumTape_or_Batch = Union[QuantumTape, QuantumTapeBatch] +PostprocessingFn = Callable[[ResultBatch], Result_or_ResultBatch] + + +def simulate(circuit: QuantumScript, state: LightningStateVector) -> Result: + """Simulate a single quantum script. + + Args: + circuit (QuantumTape): The single circuit to simulate + state (LightningStateVector): handle to Lightning state vector + + Returns: + Tuple[TensorLike]: The results of the simulation + + Note that this function can return measurements for non-commuting observables simultaneously. + """ + circuit = circuit.map_to_standard_wires() + state.reset_state() + final_state = state.get_final_state(circuit) + return LightningMeasurements(final_state).measure_final_state(circuit) + + +def jacobian(circuit: QuantumTape, state: LightningStateVector, batch_obs=False): + """Compute the Jacobian for a single quantum script. + + Args: + circuit (QuantumTape): The single circuit to simulate + state (LightningStateVector): handle to Lightning state vector + batch_obs (bool): Determine whether we process observables in parallel when + computing the jacobian. This value is only relevant when the lightning + qubit is built with OpenMP. Default is False. + + Returns: + TensorLike: The Jacobian of the quantum script + """ + circuit = circuit.map_to_standard_wires() + state.reset_state() + final_state = state.get_final_state(circuit) + return LightningAdjointJacobian(final_state, batch_obs=batch_obs).calculate_jacobian(circuit) + + +def simulate_and_jacobian(circuit: QuantumTape, state: LightningStateVector, batch_obs=False): + """Simulate a single quantum script and compute its Jacobian. + + Args: + circuit (QuantumTape): The single circuit to simulate + state (LightningStateVector): handle to Lightning state vector + batch_obs (bool): Determine whether we process observables in parallel when + computing the jacobian. This value is only relevant when the lightning + qubit is built with OpenMP. Default is False. + + Returns: + Tuple[TensorLike]: The results of the simulation and the calculated Jacobian + + Note that this function can return measurements for non-commuting observables simultaneously. + """ + circuit = circuit.map_to_standard_wires() + res = simulate(circuit, state) + jac = LightningAdjointJacobian(state, batch_obs=batch_obs).calculate_jacobian(circuit) + return res, jac + + +_operations = frozenset( + { + "Identity", + "BasisState", + "QubitStateVector", + "StatePrep", + "QubitUnitary", + "ControlledQubitUnitary", + "MultiControlledX", + "DiagonalQubitUnitary", + "PauliX", + "PauliY", + "PauliZ", + "MultiRZ", + "GlobalPhase", + "Hadamard", + "S", + "Adjoint(S)", + "T", + "Adjoint(T)", + "SX", + "Adjoint(SX)", + "CNOT", + "SWAP", + "ISWAP", + "PSWAP", + "Adjoint(ISWAP)", + "SISWAP", + "Adjoint(SISWAP)", + "SQISW", + "CSWAP", + "Toffoli", + "CY", + "CZ", + "PhaseShift", + "ControlledPhaseShift", + "CPhase", + "RX", + "RY", + "RZ", + "Rot", + "CRX", + "CRY", + "CRZ", + "C(PauliX)", + "C(PauliY)", + "C(PauliZ)", + "C(Hadamard)", + "C(S)", + "C(T)", + "C(PhaseShift)", + "C(RX)", + "C(RY)", + "C(RZ)", + "C(Rot)", + "C(SWAP)", + "C(IsingXX)", + "C(IsingXY)", + "C(IsingYY)", + "C(IsingZZ)", + "C(SingleExcitation)", + "C(SingleExcitationMinus)", + "C(SingleExcitationPlus)", + "C(DoubleExcitation)", + "C(DoubleExcitationMinus)", + "C(DoubleExcitationPlus)", + "C(MultiRZ)", + "C(GlobalPhase)", + "CRot", + "IsingXX", + "IsingYY", + "IsingZZ", + "IsingXY", + "SingleExcitation", + "SingleExcitationPlus", + "SingleExcitationMinus", + "DoubleExcitation", + "DoubleExcitationPlus", + "DoubleExcitationMinus", + "QubitCarry", + "QubitSum", + "OrbitalRotation", + "QFT", + "ECR", + "BlockEncode", + } +) +# The set of supported operations. + + +_observables = frozenset( + { + "PauliX", + "PauliY", + "PauliZ", + "Hadamard", + "Hermitian", + "Identity", + "Projector", + "SparseHamiltonian", + "Hamiltonian", + "Sum", + "SProd", + "Prod", + "Exp", + } +) +# The set of supported observables. + + +def stopping_condition(op: qml.operation.Operator) -> bool: + """A function that determines whether or not an operation is supported by ``lightning.qubit``.""" + return op.name in _operations + + +def accepted_observables(obs: qml.operation.Operator) -> bool: + """A function that determines whether or not an observable is supported by ``lightning.qubit``.""" + return obs.name in _observables + + +def adjoint_measurements(mp: qml.measurements.MeasurementProcess) -> bool: + """Specifies whether or not an observable is compatible with adjoint differentiation on DefaultQubit.""" + return isinstance(mp, qml.measurements.ExpectationMP) + + +def _supports_adjoint(circuit): + if circuit is None: + return True + + prog = TransformProgram() + _add_adjoint_transforms(prog) + + try: + prog((circuit,)) + except (qml.operation.DecompositionUndefinedError, qml.DeviceError): + return False + return True + + +def _add_adjoint_transforms(program: TransformProgram) -> None: + """Private helper function for ``preprocess`` that adds the transforms specific + for adjoint differentiation. + + Args: + program (TransformProgram): where we will add the adjoint differentiation transforms + + Side Effects: + Adds transforms to the input program. + + """ + + name = "adjoint + lightning.qubit" + program.add_transform(no_sampling, name=name) + program.add_transform(decompose, stopping_condition=adjoint_ops, name=name) + program.add_transform(validate_observables, accepted_observables, name=name) + program.add_transform( + validate_measurements, analytic_measurements=adjoint_measurements, name=name + ) + program.add_transform(qml.transforms.broadcast_expand) + program.add_transform(validate_adjoint_trainable_params) + + +@simulator_tracking +@single_tape_support +class LightningQubit2(Device): + """PennyLane Lightning Qubit device. + + A device that interfaces with C++ to perform fast linear algebra calculations. + + Use of this device requires pre-built binaries or compilation from source. Check out the + :doc:`/lightning_qubit/installation` guide for more details. + + Args: + wires (int): the number of wires to initialize the device with + c_dtype: Datatypes for statevector representation. Must be one of + ``np.complex64`` or ``np.complex128``. + shots (int): How many times the circuit should be evaluated (or sampled) to estimate + the expectation values. Defaults to ``None`` if not specified. Setting + to ``None`` results in computing statistics like expectation values and + variances analytically. + seed (Union[str, None, int, array_like[int], SeedSequence, BitGenerator, Generator]): A + seed-like parameter matching that of ``seed`` for ``numpy.random.default_rng``, or + a request to seed from numpy's global random number generator. + The default, ``seed="global"`` pulls a seed from NumPy's global generator. ``seed=None`` + will pull a seed from the OS entropy. + mcmc (bool): Determine whether to use the approximate Markov Chain Monte Carlo + sampling method when generating samples. + kernel_name (str): name of transition kernel. The current version supports + two kernels: ``"Local"`` and ``"NonZeroRandom"``. + The local kernel conducts a bit-flip local transition between states. + The local kernel generates a random qubit site and then generates a random + number to determine the new bit at that qubit site. The ``"NonZeroRandom"`` kernel + randomly transits between states that have nonzero probability. + num_burnin (int): number of steps that will be dropped. Increasing this value will + result in a closer approximation but increased runtime. + batch_obs (bool): Determine whether we process observables in parallel when + computing the jacobian. This value is only relevant when the lightning + qubit is built with OpenMP. + """ + + _device_options = ("rng", "c_dtype", "batch_obs", "mcmc", "kernel_name", "num_burnin") + _CPP_BINARY_AVAILABLE = LQ_CPP_BINARY_AVAILABLE + _new_API = True + + # TODO: Move supported ops/obs to TOML file + operations = _operations + # The names of the supported operations. + + observables = _observables + # The names of the supported observables. + + def __init__( # pylint: disable=too-many-arguments + self, + wires, + *, + c_dtype=np.complex128, + shots=None, + seed="global", + mcmc=False, + kernel_name="Local", + num_burnin=100, + batch_obs=False, + ): + if not self._CPP_BINARY_AVAILABLE: + raise ImportError( + "Pre-compiled binaries for lightning.qubit are not available. " + "To manually compile from source, follow the instructions at " + "https://pennylane-lightning.readthedocs.io/en/latest/installation.html." + ) + + super().__init__(wires=wires, shots=shots) + + self._statevector = LightningStateVector(num_wires=len(self.wires), dtype=c_dtype) + + # TODO: Investigate usefulness of creating numpy random generator + seed = np.random.randint(0, high=10000000) if seed == "global" else seed + self._rng = np.random.default_rng(seed) + + self._c_dtype = c_dtype + self._batch_obs = batch_obs + self._mcmc = mcmc + if self._mcmc: + if kernel_name not in [ + "Local", + "NonZeroRandom", + ]: + raise NotImplementedError( + f"The {kernel_name} is not supported and currently " + "only 'Local' and 'NonZeroRandom' kernels are supported." + ) + if num_burnin >= shots: + raise ValueError("Shots should be greater than num_burnin.") + self._kernel_name = kernel_name + self._num_burnin = num_burnin + else: + self._kernel_name = None + self._num_burnin = None + + @property + def c_dtype(self): + """State vector complex data type.""" + return self._c_dtype + + dtype = c_dtype + + def _setup_execution_config(self, config): + """ + Update the execution config with choices for how the device should be used and the device options. + """ + updated_values = {} + if config.gradient_method == "best": + updated_values["gradient_method"] = "adjoint" + if config.use_device_gradient is None: + updated_values["use_device_gradient"] = config.gradient_method in ("best", "adjoint") + if config.grad_on_execution is None: + updated_values["grad_on_execution"] = True + + 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: + + * Supports any qubit operations that provide a matrix + * Currently does not support finite shots + * Currently does not intrinsically support parameter broadcasting + + """ + config = self._setup_execution_config(execution_config) + program = TransformProgram() + + program.add_transform(validate_measurements, name=self.name) + # TODO: Remove no_sampling from preprocess after shots support is added + program.add_transform(no_sampling) + program.add_transform(validate_observables, accepted_observables, name=self.name) + program.add_transform(validate_device_wires, self.wires, name=self.name) + program.add_transform(qml.defer_measurements, device=self) + program.add_transform(decompose, stopping_condition=stopping_condition, name=self.name) + program.add_transform(qml.transforms.broadcast_expand) + + if config.gradient_method == "adjoint": + _add_adjoint_transforms(program) + return program, config + + # pylint: disable=unused-argument + def execute( + self, + circuits: QuantumTape_or_Batch, + execution_config: ExecutionConfig = DefaultExecutionConfig, + ) -> Result_or_ResultBatch: + """Execute a circuit or a batch of circuits and turn it into results. + + Args: + circuits (Union[QuantumTape, Sequence[QuantumTape]]): the quantum circuits to be executed + execution_config (ExecutionConfig): a datastructure with additional information required for execution + + Returns: + TensorLike, tuple[TensorLike], tuple[tuple[TensorLike]]: A numeric result of the computation. + """ + results = [] + for circuit in circuits: + circuit = circuit.map_to_standard_wires() + results.append(simulate(circuit, self._statevector)) + + return tuple(results) + + 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. + + ``LightningQubit2`` supports adjoint differentiation with analytic results. + + 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 + + """ + if execution_config is None and circuit is None: + return True + if execution_config.gradient_method not in {"adjoint", "best"}: + return False + if circuit is None: + return True + return _supports_adjoint(circuit=circuit) + + 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 datastructure with all additional information required for execution + + Returns: + Tuple: The jacobian for each trainable parameter + """ + batch_obs = execution_config.device_options.get("batch_obs", self._batch_obs) + return tuple( + jacobian(circuit, self._statevector, batch_obs=batch_obs) for circuit in circuits + ) + + 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 datastructure with all additional information required for execution + + Returns: + tuple: A numeric result of the computation and the gradient. + """ + batch_obs = execution_config.device_options.get("batch_obs", self._batch_obs) + results = tuple( + simulate_and_jacobian(c, self._statevector, batch_obs=batch_obs) for c in circuits + ) + return tuple(zip(*results)) diff --git a/setup.py b/setup.py index 8c6fbfdf68..5e37d8e2b2 100644 --- a/setup.py +++ b/setup.py @@ -186,6 +186,7 @@ def build_extension(self, ext: CMakeExtension): suffix = suffix[0].upper() + suffix[1:] pennylane_plugins = [device_name + " = pennylane_lightning." + backend + ":Lightning" + suffix] +pennylane_plugins.append("lightning.qubit2 = pennylane_lightning.lightning_qubit.lightning_qubit2:LightningQubit2") pkg_suffix = "" if suffix == "Qubit" else "_" + suffix @@ -203,9 +204,9 @@ def build_extension(self, ext: CMakeExtension): "long_description": open("README.rst").read(), "long_description_content_type": "text/x-rst", "install_requires": requirements, - "ext_modules": [] - if os.environ.get("SKIP_COMPILATION", False) - else [CMakeExtension(f"{backend}_ops")], + "ext_modules": ( + [] if os.environ.get("SKIP_COMPILATION", False) else [CMakeExtension(f"{backend}_ops")] + ), "cmdclass": {"build_ext": CMakeBuild}, "ext_package": "pennylane_lightning", "extras_require": { diff --git a/tests/conftest.py b/tests/conftest.py index edd6dce5ba..e4a79e22ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -89,18 +89,18 @@ def n_subsystems(request): # Looking for the device for testing. default_device = "lightning.qubit" -supported_devices = {"lightning.kokkos", "lightning.qubit", "lightning.gpu"} +supported_devices = {"lightning.kokkos", "lightning.qubit", "lightning.qubit2", "lightning.gpu"} supported_devices.update({sb.replace(".", "_") for sb in supported_devices}) def get_device(): """Return the pennylane lightning device. - The device is ``lightning.qubit`` by default. - Allowed values are: "lightning.kokkos" and "lightning.qubit". - An underscore can also be used instead of a dot. - If the environment variable ``PL_DEVICE`` is defined, its value is used. - Underscores are replaced by dots upon exiting. + The device is ``lightning.qubit`` by default. Allowed values are: + "lightning.kokkos", "lightning.qubit2", and "lightning.qubit". An + underscore can also be used instead of a dot. If the environment + variable ``PL_DEVICE`` is defined, its value is used. Underscores + are replaced by dots upon exiting. """ device = None if "PL_DEVICE" in os.environ: @@ -133,6 +133,11 @@ def get_device(): if hasattr(pennylane_lightning, "lightning_gpu_ops"): import pennylane_lightning.lightning_gpu_ops as lightning_ops +elif device_name == "lightning.qubit2": + from pennylane_lightning.lightning_qubit import LightningQubit2 as LightningDevice + + if hasattr(pennylane_lightning, "lightning_qubit_ops"): + import pennylane_lightning.lightning_qubit_ops as lightning_ops else: from pennylane_lightning.lightning_qubit import LightningQubit as LightningDevice diff --git a/tests/lightning_qubit/test_adjoint_jacobian_class.py b/tests/lightning_qubit/test_adjoint_jacobian_class.py index 0cb1b37d9c..9b1dd435a8 100644 --- a/tests/lightning_qubit/test_adjoint_jacobian_class.py +++ b/tests/lightning_qubit/test_adjoint_jacobian_class.py @@ -23,17 +23,16 @@ from pennylane.tape import QuantumScript from scipy.stats import unitary_group -from pennylane_lightning.lightning_qubit import LightningQubit from pennylane_lightning.lightning_qubit._adjoint_jacobian import ( LightningAdjointJacobian, ) from pennylane_lightning.lightning_qubit._state_vector import LightningStateVector -if not LightningQubit._CPP_BINARY_AVAILABLE: - pytest.skip("No binary module found. Skipping.", allow_module_level=True) +if not LightningDevice._new_API: + pytest.skip("Exclusive tests for new API. Skipping.", allow_module_level=True) -if LightningDevice != LightningQubit: - pytest.skip("Exclusive tests for lightning.qubit. Skipping.", allow_module_level=True) +if not LightningDevice._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) I, X, Y, Z = ( @@ -102,12 +101,13 @@ def test_initialization(lightning_sv): class TestAdjointJacobian: """Tests for the adjoint Jacobian functionality""" - def test_finite_shots_warns(self, lightning_sv): - """Tests warning raised when finite shots specified""" + def test_finite_shots_error(self, lightning_sv): + """Tests error raised when finite shots specified""" tape = qml.tape.QuantumTape(measurements=[qml.expval(qml.PauliZ(0))], shots=1) - with pytest.warns( - UserWarning, match="Requested adjoint differentiation to be computed with finite shots." + with pytest.raises( + qml.QuantumFunctionError, + match="Requested adjoint differentiation to be computed with finite shots.", ): self.calculate_jacobian(lightning_sv(num_wires=1), tape) @@ -145,7 +145,7 @@ def test_empty_measurements(self, lightning_sv): def test_phaseshift_gradient(self, n_qubits, par, tol, lightning_sv): """Test that the gradient of the phaseshift gate matches the exact analytic formula.""" par = np.array(par) - # dev = qml.device("lightning.qubit", wires=n_qubits) + init_state = np.zeros(2**n_qubits) init_state[-2::] = np.array([1.0 / np.sqrt(2), 1.0 / np.sqrt(2)], requires_grad=False) @@ -423,18 +423,16 @@ def test_wrong_dy_expval(self, lightning_sv): ): self.calculate_vjp(statevector, tape1, dy2) - def test_finite_shots_warns(self, lightning_sv): - """Tests warning raised when finite shots specified""" + def test_finite_shots_error(self, lightning_sv): + """Tests error raised when finite shots specified""" statevector = lightning_sv(num_wires=2) - with qml.tape.QuantumTape() as tape: - qml.expval(qml.PauliZ(0)) - + tape = qml.tape.QuantumScript([], [qml.expval(qml.PauliZ(0))], shots=1) dy = np.array([1.0]) - with pytest.warns( - UserWarning, + with pytest.raises( + qml.QuantumFunctionError, match="Requested adjoint differentiation to be computed with finite shots.", ): self.calculate_vjp(statevector, tape, dy) @@ -507,7 +505,6 @@ def test_no_trainable_parameters(self, lightning_sv): tape.trainable_params = {} dy = np.array([1.0]) - vjp = self.calculate_vjp(statevector, tape, dy) assert len(vjp) == 0 diff --git a/tests/lightning_qubit/test_measurements_class.py b/tests/lightning_qubit/test_measurements_class.py index 7865359907..428825cced 100644 --- a/tests/lightning_qubit/test_measurements_class.py +++ b/tests/lightning_qubit/test_measurements_class.py @@ -19,21 +19,28 @@ import numpy as np import pennylane as qml import pytest -from conftest import LightningDevice # tested device +from conftest import LightningDevice, device_name # tested device from flaky import flaky from pennylane.devices import DefaultQubit from pennylane.measurements import VarianceMP from scipy.sparse import csr_matrix, random_array -from pennylane_lightning.lightning_qubit import LightningQubit +try: + from pennylane_lightning.lightning_qubit_ops import ( + MeasurementsC64, + MeasurementsC128, + ) +except ImportError: + pass + from pennylane_lightning.lightning_qubit._measurements import LightningMeasurements from pennylane_lightning.lightning_qubit._state_vector import LightningStateVector -if not LightningQubit._CPP_BINARY_AVAILABLE: - pytest.skip("No binary module found. Skipping.", allow_module_level=True) +if not LightningDevice._new_API: + pytest.skip("Exclusive tests for new API. Skipping.", allow_module_level=True) -if LightningDevice != LightningQubit: - pytest.skip("Exclusive tests for lightning.qubit. Skipping.", allow_module_level=True) +if not LightningDevice._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) THETA = np.linspace(0.11, 1, 3) PHI = np.linspace(0.32, 1, 3) @@ -421,6 +428,7 @@ def calculate_reference(tape, lightning_sv): ( [0], [1, 2], + [1, 0], qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2), @@ -547,6 +555,29 @@ def test_double_return_value(self, measurement, obs0_, obs1_, lightning_sv, tol) for r, e in zip(result, expected): assert np.allclose(r, e, max(tol, 1.0e-5)) + @pytest.mark.parametrize( + "cases", + [ + [[0, 1], [1, 0]], + [[1, 0], [0, 1]], + ], + ) + def test_probs_tape_unordered_wires(self, cases, tol): + """Test probs with a circuit on wires=[0] fails for out-of-order wires passed to probs.""" + + x, y, z = [0.5, 0.3, -0.7] + dev = qml.device(device_name, wires=cases[1]) + + def circuit(): + qml.RX(0.4, wires=[0]) + qml.Rot(x, y, z, wires=[0]) + qml.RY(-0.2, wires=[0]) + return qml.probs(wires=cases[0]) + + expected = qml.QNode(circuit, qml.device("default.qubit", wires=cases[1]))() + results = qml.QNode(circuit, dev)() + assert np.allclose(expected, results, tol) + class TestControlledOps: """Tests for controlled operations""" diff --git a/tests/lightning_qubit/test_measurements_samples_MCMC.py b/tests/lightning_qubit/test_measurements_samples_MCMC.py index e376294353..531c0e6164 100644 --- a/tests/lightning_qubit/test_measurements_samples_MCMC.py +++ b/tests/lightning_qubit/test_measurements_samples_MCMC.py @@ -21,12 +21,12 @@ from pennylane_lightning.lightning_qubit import LightningQubit -if not LightningQubit._CPP_BINARY_AVAILABLE: - pytest.skip("No binary module found. Skipping.", allow_module_level=True) - if LightningDevice != LightningQubit: pytest.skip("Exclusive tests for lightning.qubit. Skipping.", allow_module_level=True) +if not LightningDevice._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) + class TestMCMCSample: """Tests that samples are properly calculated.""" diff --git a/tests/lightning_qubit/test_state_vector_class.py b/tests/lightning_qubit/test_state_vector_class.py index 63e8cd2114..d7662862d1 100644 --- a/tests/lightning_qubit/test_state_vector_class.py +++ b/tests/lightning_qubit/test_state_vector_class.py @@ -24,14 +24,13 @@ from pennylane.tape import QuantumScript from pennylane.wires import Wires -from pennylane_lightning.lightning_qubit import LightningQubit from pennylane_lightning.lightning_qubit._state_vector import LightningStateVector -if not LightningQubit._CPP_BINARY_AVAILABLE: - pytest.skip("No binary module found. Skipping.", allow_module_level=True) +if not LightningDevice._new_API: + pytest.skip("Exclusive tests for new API. Skipping.", allow_module_level=True) -if LightningDevice != LightningQubit: - pytest.skip("Exclusive tests for lightning.qubit. Skipping.", allow_module_level=True) +if not LightningDevice._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) @pytest.mark.parametrize("num_wires", range(4)) diff --git a/tests/lightning_qubit2/test_expval_2.py b/tests/lightning_qubit2/test_expval_2.py new file mode 100644 index 0000000000..abfcc1fd33 --- /dev/null +++ b/tests/lightning_qubit2/test_expval_2.py @@ -0,0 +1,338 @@ +# 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 process and execute (expval calculation). +""" +# pylint: disable=too-many-arguments, redefined-outer-name +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 not LightningDevice._CPP_BINARY_AVAILABLE: # pylint: disable=protected-access + pytest.skip("No binary module found. Skipping.", allow_module_level=True) + + +@pytest.fixture(params=[np.complex64, np.complex128]) +def dev(request): + return LightningDevice(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 process_and_execute(dev, tape): + program, _ = dev.preprocess() + tapes, transf_fn = program([tape]) + results = dev.execute(tapes) + return transf_fn(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 = LightningDevice(wires=wires) + + 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 = process_and_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 = process_and_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 = process_and_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 = process_and_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 = process_and_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 = process_and_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 = process_and_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 = process_and_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 = process_and_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 = process_and_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) diff --git a/tests/lightning_qubit2/test_new_api_device.py b/tests/lightning_qubit2/test_new_api_device.py new file mode 100644 index 0000000000..9d9428cf6c --- /dev/null +++ b/tests/lightning_qubit2/test_new_api_device.py @@ -0,0 +1,636 @@ +# 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 unit tests for new device API Lightning classes. +""" +# pylint: disable=too-many-arguments + +import numpy as np +import pennylane as qml +import pytest +from conftest import PHI, THETA, VARPHI, LightningDevice +from pennylane.devices import DefaultExecutionConfig, DefaultQubit, ExecutionConfig +from pennylane.devices.default_qubit import adjoint_ops +from pennylane.tape import QuantumScript + +from pennylane_lightning.lightning_qubit.lightning_qubit2 import ( + _add_adjoint_transforms, + _supports_adjoint, + accepted_observables, + adjoint_measurements, + decompose, + no_sampling, + stopping_condition, + validate_adjoint_trainable_params, + validate_device_wires, + validate_measurements, + validate_observables, +) + +if not LightningDevice._new_API: + pytest.skip("Exclusive tests for new device API. Skipping.", allow_module_level=True) + +if not LightningDevice._CPP_BINARY_AVAILABLE: # pylint: disable=protected-access + pytest.skip("No binary module found. Skipping.", allow_module_level=True) + + +@pytest.fixture(params=[np.complex64, np.complex128]) +def dev(request): + return LightningDevice(wires=3, c_dtype=request.param) + + +class TestHelpers: + """Unit tests for helper functions""" + + class DummyOperator(qml.operation.Operation, qml.operation.Observable): + """Dummy operator""" + + num_wires = 1 + + def test_stopping_condition(self): + """Test that stopping_condition returns whether or not an operation + is supported by the device.""" + valid_op = qml.RX(1.23, 0) + invalid_op = self.DummyOperator(0) + + assert stopping_condition(valid_op) is True + assert stopping_condition(invalid_op) is False + + def test_accepted_observables(self): + """Test that accepted_observables returns whether or not an observable + is supported by the device.""" + valid_obs = qml.Projector([0], 0) + invalid_obs = self.DummyOperator(0) + + assert accepted_observables(valid_obs) is True + assert accepted_observables(invalid_obs) is False + + def test_add_adjoint_transforms(self): + """Test that the correct transforms are added to the program by _add_adjoint_transforms""" + expected_program = qml.transforms.core.TransformProgram() + + name = "adjoint + lightning.qubit" + expected_program.add_transform(no_sampling, name=name) + expected_program.add_transform( + decompose, + stopping_condition=adjoint_ops, + name=name, + ) + expected_program.add_transform(validate_observables, accepted_observables, name=name) + expected_program.add_transform( + validate_measurements, + analytic_measurements=adjoint_measurements, + name=name, + ) + expected_program.add_transform(qml.transforms.broadcast_expand) + expected_program.add_transform(validate_adjoint_trainable_params) + + actual_program = qml.transforms.core.TransformProgram() + _add_adjoint_transforms(actual_program) + assert actual_program == expected_program + + @pytest.mark.parametrize( + "circuit, expected", + [ + (None, True), + (QuantumScript([], [qml.state()]), False), + (QuantumScript([qml.RX(1.23, 0)], [qml.expval(qml.Z(0))]), True), + (QuantumScript([qml.CRot(1.23, 4.56, 7.89, [0, 1])], [qml.expval(qml.Z(0))]), True), + (QuantumScript([qml.Rot(1.23, 4.56, 7.89, 1)], [qml.var(qml.X(0))]), False), + ], + ) + def test_supports_adjoint(self, circuit, expected): + """Test that _supports_adjoint returns the correct boolean value.""" + assert _supports_adjoint(circuit) == expected + + +class TestInitialization: + """Unit tests for device initialization""" + + def test_invalid_num_burnin_error(self): + """Test that an error is raised when num_burnin is more than number of shots""" + n_shots = 10 + num_burnin = 11 + + with pytest.raises(ValueError, match="Shots should be greater than num_burnin."): + _ = LightningDevice(wires=2, shots=n_shots, mcmc=True, num_burnin=num_burnin) + + def test_invalid_kernel_name(self): + """Test that an error is raised when the kernel_name is not "Local" or "NonZeroRandom".""" + + _ = LightningDevice(wires=2, shots=1000, mcmc=True, kernel_name="Local") + _ = LightningDevice(wires=2, shots=1000, mcmc=True, kernel_name="NonZeroRandom") + + with pytest.raises( + NotImplementedError, match="only 'Local' and 'NonZeroRandom' kernels are supported" + ): + _ = LightningDevice(wires=2, shots=1000, mcmc=True, kernel_name="bleh") + + +class TestExecution: + """Unit tests for executing quantum tapes on a device""" + + @staticmethod + def calculate_reference(tape): + device = DefaultQubit(max_workers=1) + program, _ = device.preprocess() + tapes, transf_fn = program([tape]) + results = device.execute(tapes) + return transf_fn(results) + + @staticmethod + def process_and_execute(device, tape): + program, _ = device.preprocess() + tapes, transf_fn = program([tape]) + results = device.execute(tapes) + return transf_fn(results) + + _default_device_options = { + "c_dtype": np.complex128, + "batch_obs": False, + "mcmc": False, + "kernel_name": None, + "num_burnin": None, + } + + @pytest.mark.parametrize( + "config, expected_config", + [ + ( + DefaultExecutionConfig, + ExecutionConfig( + grad_on_execution=True, + use_device_gradient=False, + device_options=_default_device_options, + ), + ), + ( + ExecutionConfig(gradient_method="best"), + ExecutionConfig( + gradient_method="adjoint", + grad_on_execution=True, + use_device_gradient=True, + device_options=_default_device_options, + ), + ), + ( + ExecutionConfig( + device_options={ + "c_dtype": np.complex64, + "mcmc": True, + } + ), + ExecutionConfig( + grad_on_execution=True, + use_device_gradient=False, + device_options={ + "c_dtype": np.complex64, + "batch_obs": False, + "mcmc": True, + "kernel_name": None, + "num_burnin": None, + }, + ), + ), + ( + ExecutionConfig( + gradient_method="backprop", use_device_gradient=False, grad_on_execution=False + ), + ExecutionConfig( + gradient_method="backprop", + use_device_gradient=False, + grad_on_execution=False, + device_options=_default_device_options, + ), + ), + ], + ) + def test_preprocess_correct_config_setup(self, config, expected_config): + """Test that the execution config is set up correctly in preprocess""" + device = LightningDevice(wires=2) + _, new_config = device.preprocess(config) + del new_config.device_options["rng"] + + assert new_config == expected_config + + @pytest.mark.parametrize("adjoint", [True, False]) + def test_preprocess(self, adjoint): + """Test that the transform program returned by preprocess is correct""" + device = LightningDevice(wires=2) + + expected_program = qml.transforms.core.TransformProgram() + expected_program.add_transform(validate_measurements, name=device.name) + expected_program.add_transform(no_sampling) + expected_program.add_transform(validate_observables, accepted_observables, name=device.name) + expected_program.add_transform(validate_device_wires, device.wires, name=device.name) + expected_program.add_transform(qml.defer_measurements, device=device) + expected_program.add_transform( + decompose, stopping_condition=stopping_condition, name=device.name + ) + expected_program.add_transform(qml.transforms.broadcast_expand) + + if adjoint: + name = "adjoint + lightning.qubit" + expected_program.add_transform(no_sampling, name=name) + expected_program.add_transform( + decompose, + stopping_condition=adjoint_ops, + name=name, + ) + expected_program.add_transform(validate_observables, accepted_observables, name=name) + expected_program.add_transform( + validate_measurements, + analytic_measurements=adjoint_measurements, + name=name, + ) + expected_program.add_transform(qml.transforms.broadcast_expand) + expected_program.add_transform(validate_adjoint_trainable_params) + + gradient_method = "adjoint" if adjoint else None + config = ExecutionConfig(gradient_method=gradient_method) + actual_program, _ = device.preprocess(config) + assert actual_program == expected_program + + @pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) + @pytest.mark.parametrize( + "mp", + [ + qml.probs(wires=[1, 2]), + qml.probs(op=qml.Z(2)), + qml.expval(qml.Z(2)), + qml.var(qml.X(2)), + qml.expval(qml.sum(qml.X(0), qml.Z(0))), + qml.expval(qml.Hamiltonian([-0.5, 1.5], [qml.Y(1), qml.X(1)])), + qml.expval(qml.s_prod(2.5, qml.Z(0))), + qml.expval(qml.prod(qml.Z(0), qml.X(1))), + qml.expval(qml.sum(qml.Z(1), qml.X(1))), + qml.expval( + qml.SparseHamiltonian( + qml.Hamiltonian([-1.0, 1.5], [qml.Z(1), qml.X(1)]).sparse_matrix( + wire_order=[0, 1, 2] + ), + wires=[0, 1, 2], + ) + ), + qml.expval(qml.Projector([1], wires=2)), + ], + ) + def test_execute_single_measurement(self, theta, phi, mp, dev): + """Test that execute returns the correct results with a single measurement.""" + qs = QuantumScript( + [ + qml.RX(phi, 0), + qml.CNOT([0, 2]), + qml.RZ(theta, 1), + qml.CNOT([1, 2]), + ], + [mp], + ) + res = self.process_and_execute(dev, qs)[0] + expected = self.calculate_reference(qs)[0] + assert np.allclose(res, expected) + + @pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) + @pytest.mark.parametrize( + "mp1", + [ + qml.probs(wires=[1, 2]), + qml.expval(qml.Z(2)), + qml.var(qml.X(2)), + qml.var(qml.Hermitian(qml.Hadamard.compute_matrix(), 0)), + ], + ) + @pytest.mark.parametrize( + "mp2", + [ + qml.probs(op=qml.X(2)), + qml.expval(qml.Y(2)), + qml.var(qml.Y(2)), + qml.expval(qml.Hamiltonian([-0.5, 1.5, -1.1], [qml.Y(1), qml.X(1), qml.Z(0)])), + ], + ) + def test_execute_multi_measurement(self, theta, phi, dev, mp1, mp2): + """Test that execute returns the correct results with multiple measurements.""" + qs = QuantumScript( + [ + qml.RX(phi, 0), + qml.CNOT([0, 2]), + qml.RZ(theta, 1), + qml.CNOT([1, 2]), + ], + [mp1, mp2], + ) + res = self.process_and_execute(dev, qs)[0] + expected = self.calculate_reference(qs)[0] + assert len(res) == 2 + for r, e in zip(res, expected): + assert np.allclose(r, e) + + @pytest.mark.parametrize("phi, theta", list(zip(PHI, THETA))) + @pytest.mark.parametrize("wires", (["a", "b", -3], [0, "target", "other_target"])) + def test_custom_wires(self, phi, theta, wires): + """Test execution with custom wires""" + device = LightningDevice(wires=wires) + qs = QuantumScript( + [ + qml.RX(phi, wires[0]), + qml.RY(theta, wires[2]), + qml.CNOT([wires[0], wires[1]]), + qml.CNOT([wires[1], wires[2]]), + ], + [qml.expval(qml.Z(wires[0])), qml.expval(qml.Z(wires[2]))], + ) + + result = device.execute(qs) + + assert isinstance(result, tuple) + assert len(result) == 2 + + assert np.allclose(result[0], np.cos(phi)) + assert np.allclose(result[1], np.cos(phi) * np.cos(theta)) + + +@pytest.mark.parametrize("batch_obs", [True, False]) +class TestDerivatives: + """Unit tests for calculating derivatives with a device""" + + @staticmethod + def calculate_reference(tape, execute_and_derivatives=False): + device = DefaultQubit(max_workers=1) + program, config = device.preprocess(ExecutionConfig(gradient_method="adjoint")) + tapes, transf_fn = program([tape]) + + if execute_and_derivatives: + results, jac = device.execute_and_compute_derivatives(tapes, config) + else: + results = device.execute(tapes, config) + jac = device.compute_derivatives(tapes, config) + return transf_fn(results), jac + + @staticmethod + def process_and_execute(device, tape, execute_and_derivatives=False, obs_batch=False): + program, config = device.preprocess( + ExecutionConfig(gradient_method="adjoint", device_options={"batch_obs": obs_batch}) + ) + tapes, transf_fn = program([tape]) + + if execute_and_derivatives: + results, jac = device.execute_and_compute_derivatives(tapes, config) + else: + results = device.execute(tapes, config) + jac = device.compute_derivatives(tapes, config) + return transf_fn(results), jac + + # Test supports derivative + xfail tests + + @pytest.mark.parametrize( + "config, tape, expected", + [ + (None, None, True), + (DefaultExecutionConfig, None, False), + (ExecutionConfig(gradient_method="backprop"), None, False), + ( + ExecutionConfig(gradient_method="backprop"), + QuantumScript([qml.RX(0.123, 0)], [qml.expval(qml.Z(0))]), + False, + ), + (ExecutionConfig(gradient_method="best"), None, True), + (ExecutionConfig(gradient_method="adjoint"), None, True), + ( + ExecutionConfig(gradient_method="adjoint"), + QuantumScript([qml.RX(0.123, 0)], [qml.expval(qml.Z(0))]), + True, + ), + ( + ExecutionConfig(gradient_method="adjoint"), + QuantumScript([qml.RX(0.123, 0)], [qml.var(qml.Z(0))]), + False, + ), + ( + ExecutionConfig(gradient_method="adjoint"), + QuantumScript([qml.RX(0.123, 0)], [qml.state()]), + False, + ), + ( + ExecutionConfig(gradient_method="adjoint"), + QuantumScript([qml.RX(0.123, 0)], [qml.expval(qml.Z(0))], shots=10), + False, + ), + ], + ) + def test_supports_derivatives(self, dev, config, tape, expected, batch_obs): + """Test that supports_derivative returns the correct boolean value.""" + assert dev.supports_derivatives(config, tape) == expected + + @pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) + @pytest.mark.parametrize( + "obs", + [ + qml.Z(1), + qml.s_prod(2.5, qml.Z(0)), + qml.prod(qml.Z(0), qml.X(1)), + qml.sum(qml.Z(1), qml.X(1)), + qml.Hamiltonian([-1.0, 1.5], [qml.Z(1), qml.X(1)]), + qml.Hermitian(qml.Hadamard.compute_matrix(), 0), + qml.Projector([1], 1), + qml.operation.Tensor(qml.Z(0), qml.X(1)), + ], + ) + @pytest.mark.parametrize("execute_and_derivatives", [True, False]) + def test_derivatives_single_expval( + self, theta, phi, dev, obs, execute_and_derivatives, batch_obs + ): + """Test that the jacobian is correct when a tape has a single expectation value""" + qs = QuantumScript( + [qml.RX(theta, 0), qml.CNOT([0, 1]), qml.RY(phi, 1)], + [qml.expval(obs)], + trainable_params=[0, 1], + ) + + res, jac = self.process_and_execute( + dev, qs, execute_and_derivatives=execute_and_derivatives, obs_batch=batch_obs + ) + if isinstance(obs, qml.Hamiltonian): + qs = QuantumScript( + qs.operations, + [qml.expval(qml.Hermitian(qml.matrix(obs), wires=obs.wires))], + trainable_params=qs.trainable_params, + ) + expected, expected_jac = self.calculate_reference( + qs, execute_and_derivatives=execute_and_derivatives + ) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + assert len(res) == len(jac) == 1 + assert np.allclose(res, expected, atol=tol, rtol=0) + assert np.allclose(jac, expected_jac, atol=tol, rtol=0) + + @pytest.mark.parametrize("theta, phi, omega", list(zip(THETA, PHI, VARPHI))) + @pytest.mark.parametrize( + "obs1", + [ + qml.Z(1), + qml.s_prod(2.5, qml.Y(2)), + qml.Hamiltonian([-1.0, 1.5], [qml.Z(1), qml.X(1)]), + qml.operation.Tensor(qml.Z(0), qml.X(1)), + ], + ) + @pytest.mark.parametrize( + "obs2", + [ + qml.prod(qml.Y(0), qml.X(2)), + qml.sum(qml.Z(1), qml.X(1)), + qml.SparseHamiltonian( + qml.Hamiltonian([-1.0, 1.5], [qml.Z(1), qml.X(1)]).sparse_matrix( + wire_order=[0, 1, 2] + ), + wires=[0, 1, 2], + ), + qml.Projector([1], wires=2), + ], + ) + @pytest.mark.parametrize("execute_and_derivatives", [True, False]) + def test_derivatives_multi_expval( + self, theta, phi, omega, dev, obs1, obs2, execute_and_derivatives, batch_obs + ): + """Test that the jacobian is correct when a tape has multiple expectation values""" + qs = QuantumScript( + [ + qml.RX(theta, 0), + qml.CNOT([0, 1]), + qml.RY(phi, 1), + qml.CNOT([1, 2]), + qml.RZ(omega, 2), + ], + [qml.expval(obs1), qml.expval(obs2)], + trainable_params=[0, 1, 2], + ) + + res, jac = self.process_and_execute( + dev, qs, execute_and_derivatives=execute_and_derivatives, obs_batch=batch_obs + ) + if isinstance(obs1, qml.Hamiltonian): + qs = QuantumScript( + qs.operations, + [qml.expval(qml.Hermitian(qml.matrix(obs1), wires=obs1.wires)), qml.expval(obs2)], + trainable_params=qs.trainable_params, + ) + expected, expected_jac = self.calculate_reference( + qs, execute_and_derivatives=execute_and_derivatives + ) + res = res[0] + jac = jac[0] + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + assert len(res) == len(jac) == 2 + assert np.allclose(res, expected, atol=tol, rtol=0) + assert np.allclose(jac, expected_jac, atol=tol, rtol=0) + + @pytest.mark.parametrize("execute_and_derivatives", [True, False]) + def test_derivatives_no_trainable_params(self, dev, execute_and_derivatives, batch_obs): + """Test that the derivatives are empty with there are no trainable parameters.""" + qs = QuantumScript( + [qml.Hadamard(0), qml.CNOT([0, 1]), qml.S(1), qml.T(1)], [qml.expval(qml.Z(1))] + ) + res, jac = self.process_and_execute( + dev, qs, execute_and_derivatives=execute_and_derivatives, obs_batch=batch_obs + ) + expected, _ = self.calculate_reference(qs, execute_and_derivatives=execute_and_derivatives) + + tol = 1e-5 if dev.c_dtype == np.complex64 else 1e-7 + assert np.allclose(res, expected, atol=tol, rtol=0) + assert len(jac) == 1 + assert qml.math.shape(jac[0]) == (0,) + + def test_state_jacobian_not_supported(self, dev, batch_obs): + """Test that an error is raised if derivatives are requested for state measurement""" + qs = QuantumScript([qml.RX(1.23, 0)], [qml.state()], trainable_params=[0]) + config = ExecutionConfig(gradient_method="adjoint", device_options={"batch_obs": batch_obs}) + + with pytest.raises( + qml.QuantumFunctionError, match="This method does not support statevector return type" + ): + _ = dev.compute_derivatives(qs, config) + + with pytest.raises( + qml.QuantumFunctionError, match="This method does not support statevector return type" + ): + _ = dev.execute_and_compute_derivatives(qs, config) + + def test_shots_error_with_derivatives(self, dev, batch_obs): + """Test that an error is raised if the gradient method is adjoint when the tape has shots""" + qs = QuantumScript( + [qml.RX(1.23, 0)], [qml.expval(qml.Z(0))], shots=10, trainable_params=[0] + ) + config = ExecutionConfig(gradient_method="adjoint", device_options={"batch_obs": batch_obs}) + program, _ = dev.preprocess(config) + + with pytest.raises(qml.DeviceError, match="Finite shots are not supported"): + _, _ = program([qs]) + + @pytest.mark.parametrize("phi", PHI) + @pytest.mark.parametrize("execute_and_derivatives", [True, False]) + def test_derivatives_tape_batch(self, phi, execute_and_derivatives, batch_obs): + """Test that results are correct when we execute and compute derivatives for a batch of + tapes.""" + device = LightningDevice(wires=4, batch_obs=batch_obs) + + ops = [ + qml.X(0), + qml.X(1), + qml.ctrl(qml.RX(phi, 2), (0, 1, 3), control_values=[1, 1, 0]), + ] + + qs1 = QuantumScript( + ops, + [ + qml.expval(qml.sum(qml.Y(2), qml.Z(1))), + qml.expval(qml.s_prod(3, qml.Z(2))), + ], + trainable_params=[0], + ) + + ops = [qml.Hadamard(0), qml.IsingXX(phi, wires=(0, 1))] + qs2 = QuantumScript(ops, [qml.expval(qml.prod(qml.Z(0), qml.Z(1)))], trainable_params=[0]) + + if execute_and_derivatives: + results, jacs = device.execute_and_compute_derivatives((qs1, qs2)) + else: + results = device.execute((qs1, qs2)) + jacs = device.compute_derivatives((qs1, qs2)) + + # Assert results + expected1 = (-np.sin(phi) - 1, 3 * np.cos(phi)) + x1 = np.cos(phi / 2) ** 2 / 2 + x2 = np.sin(phi / 2) ** 2 / 2 + expected2 = sum([x1, -x2, -x1, x2]) # zero + expected = (expected1, expected2) + + assert len(results) == len(expected) + assert len(results[0]) == len(expected[0]) + assert np.allclose(results[0][0], expected[0][0]) + assert np.allclose(results[0][1], expected[0][1]) + assert np.allclose(results[1], expected[1]) + + # Assert derivatives + expected_jac1 = (-np.cos(phi), -3 * np.sin(phi)) + x1_jac = -np.cos(phi / 2) * np.sin(phi / 2) / 2 + x2_jac = np.sin(phi / 2) * np.cos(phi / 2) / 2 + expected_jac2 = sum([x1_jac, -x2_jac, -x1_jac, x2_jac]) # zero + expected_jac = (expected_jac1, expected_jac2) + + assert len(jacs) == len(expected_jac) + assert len(jacs[0]) == len(expected_jac[0]) + assert np.allclose(jacs[0], expected_jac[0]) + assert np.allclose(jacs[1], expected_jac[1]) diff --git a/tests/lightning_qubit2/test_no_binaries.py b/tests/lightning_qubit2/test_no_binaries.py new file mode 100644 index 0000000000..fcf7a3a03b --- /dev/null +++ b/tests/lightning_qubit2/test_no_binaries.py @@ -0,0 +1,32 @@ +# 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 checking binaries. +""" +import pytest +from conftest import LightningDevice + +if not LightningDevice._new_API: + pytest.skip("Tests are for new API. Skipping", allow_module_level=True) + +if LightningDevice._CPP_BINARY_AVAILABLE: + pytest.skip("Binary module found. Skipping.", allow_module_level=True) + + +@pytest.mark.skipif(LightningDevice._CPP_BINARY_AVAILABLE, reason="Lightning binary required") +def test_no_binaries(): + """Test no binaries were found for the device""" + + with pytest.raises(ImportError, match="Pre-compiled binaries for "): + LightningDevice(wires=2) diff --git a/tests/lightning_qubit2/test_var_2.py b/tests/lightning_qubit2/test_var_2.py new file mode 100644 index 0000000000..eb35d40e93 --- /dev/null +++ b/tests/lightning_qubit2/test_var_2.py @@ -0,0 +1,351 @@ +# 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 process and execute (variance calculation). +""" +import numpy as np +import pennylane as qml + +# pylint: disable=too-many-arguments, redefined-outer-name +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 not LightningDevice._CPP_BINARY_AVAILABLE: # pylint: disable=protected-access + pytest.skip("No binary module found. Skipping.", allow_module_level=True) + + +@pytest.fixture(params=[np.complex64, np.complex128]) +def dev(request): + return LightningDevice(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 process_and_execute(dev, tape): + program, _ = dev.preprocess() + tapes, transf_fn = program([tape]) + results = dev.execute(tapes) + return transf_fn(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 = LightningDevice(wires=wires) + + 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 = process_and_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 = process_and_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 = process_and_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 = process_and_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 = process_and_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 = process_and_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 = process_and_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 = process_and_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 = process_and_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 = process_and_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) diff --git a/tests/test_adjoint_jacobian.py b/tests/test_adjoint_jacobian.py index ad9db67239..4fc5058c4c 100644 --- a/tests/test_adjoint_jacobian.py +++ b/tests/test_adjoint_jacobian.py @@ -28,11 +28,17 @@ I, X, Y, Z = ( np.eye(2), - qml.PauliX.compute_matrix(), - qml.PauliY.compute_matrix(), - qml.PauliZ.compute_matrix(), + qml.X.compute_matrix(), + qml.Y.compute_matrix(), + qml.Z.compute_matrix(), ) +if ld._new_API: + if not ld._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) + else: + from pennylane_lightning.lightning_qubit_ops import LightningException + kokkos_args = [None] if device_name == "lightning.kokkos" and ld._CPP_BINARY_AVAILABLE: from pennylane_lightning.lightning_kokkos_ops import InitializationSettings @@ -78,9 +84,21 @@ def Rz(theta): return math.cos(theta / 2) * I + 1j * math.sin(-theta / 2) * Z +def get_tolerance_and_stepsize(device, step_size=False): + """Helper function to get tolerance and finite diff step size for + different device dtypes""" + tol = 1e-3 if device.dtype == np.complex64 else 1e-7 + h = tol if step_size else None + return tol, h + + class TestAdjointJacobian: """Tests for the adjoint_jacobian method""" + @staticmethod + def get_derivatives_method(device): + return device.compute_derivatives if device._new_API else device.adjoint_jacobian + @pytest.fixture(params=fixture_params) def dev(self, request): params = request.param @@ -96,10 +114,12 @@ def test_not_expval(self, dev): qml.RX(0.1, wires=0) qml.var(qml.PauliZ(0)) + method = self.get_derivatives_method(dev) + with pytest.raises( qml.QuantumFunctionError, match="Adjoint differentiation method does not" ): - dev.adjoint_jacobian(tape) + method(tape) with qml.tape.QuantumTape() as tape: qml.RX(0.1, wires=0) @@ -117,8 +137,9 @@ def test_not_expval(self, dev): qml.QuantumFunctionError, match=message, ): - dev.adjoint_jacobian(tape) + method(tape) + @pytest.mark.skipif(ld._new_API, reason="Requires old API") def test_finite_shots_warns(self): """Tests warning raised when finite shots specified""" @@ -132,13 +153,28 @@ def test_finite_shots_warns(self): ): dev.adjoint_jacobian(tape) + @pytest.mark.skipif(not ld._new_API, reason="Requires new API") + def test_finite_shots_error(self): + """Tests warning raised when finite shots specified""" + + dev = qml.device(device_name, wires=1) + + tape = qml.tape.QuantumScript([], [qml.expval(qml.PauliZ(0))], shots=1) + + with pytest.raises( + qml.QuantumFunctionError, + match="Requested adjoint differentiation to be computed with finite shots.", + ): + dev.compute_derivatives(tape) + def test_empty_measurements(self, dev): """Tests if an empty array is returned when the measurements of the tape is empty.""" with qml.tape.QuantumTape() as tape: qml.RX(0.4, wires=[0]) - jac = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + jac = method(tape) assert len(jac) == 0 @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @@ -150,11 +186,19 @@ def test_unsupported_op(self, dev): qml.CRot(0.1, 0.2, 0.3, wires=[0, 1]) qml.expval(qml.PauliZ(0)) - with pytest.raises( - qml.QuantumFunctionError, match="The CRot operation is not supported using the" - ): - dev.adjoint_jacobian(tape) + if dev._new_API: + with pytest.raises( + LightningException, + match="The operation is not supported using the adjoint differentiation method", + ): + dev.compute_derivatives(tape) + else: + with pytest.raises( + qml.QuantumFunctionError, match="The CRot operation is not supported using the" + ): + dev.adjoint_jacobian(tape) + @pytest.mark.skipif(ld._new_API, reason="Old API required") @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_proj_unsupported(self, dev): """Test if a QuantumFunctionError is raised for a Projector observable""" @@ -162,10 +206,12 @@ def test_proj_unsupported(self, dev): qml.CRX(0.1, wires=[0, 1]) qml.expval(qml.Projector([0, 1], wires=[0, 1])) + method = self.get_derivatives_method(dev) + with pytest.raises( qml.QuantumFunctionError, match="differentiation method does not support the Projector" ): - dev.adjoint_jacobian(tape) + method(tape) with qml.tape.QuantumTape() as tape: qml.CRX(0.1, wires=[0, 1]) @@ -174,7 +220,7 @@ def test_proj_unsupported(self, dev): with pytest.raises( qml.QuantumFunctionError, match="differentiation method does not support the Projector" ): - dev.adjoint_jacobian(tape) + method(tape) @pytest.mark.parametrize("theta", np.linspace(-2 * np.pi, 2 * np.pi, 7)) @pytest.mark.parametrize("G", [qml.RX, qml.RY, qml.RZ]) @@ -191,9 +237,10 @@ def test_pauli_rotation_gradient(self, stateprep, G, theta, dev): tape.trainable_params = {1} - calculated_val = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + calculated_val = method(tape) - tol = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 + tol, _ = get_tolerance_and_stepsize(dev) # compare to finite differences tapes, fn = qml.gradients.param_shift(tape) @@ -214,9 +261,10 @@ def test_Rot_gradient(self, stateprep, theta, dev): tape.trainable_params = {1, 2, 3} - calculated_val = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + calculated_val = method(tape) - tol = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 + tol, _ = get_tolerance_and_stepsize(dev) # compare to finite differences tapes, fn = qml.gradients.param_shift(tape) @@ -224,7 +272,7 @@ def test_Rot_gradient(self, stateprep, theta, dev): assert np.allclose(calculated_val, numeric_val, atol=tol, rtol=0) @pytest.mark.skipif( - device_name != "lightning.qubit" or not ld._CPP_BINARY_AVAILABLE, + device_name not in ("lightning.qubit", "lightning.qubit2") or not ld._CPP_BINARY_AVAILABLE, reason="N-controlled operations only implemented in lightning.qubit.", ) @pytest.mark.parametrize("n_qubits", [1, 2, 3, 4]) @@ -232,7 +280,7 @@ def test_Rot_gradient(self, stateprep, theta, dev): def test_phaseshift_gradient(self, n_qubits, par, tol): """Test that the gradient of the phaseshift gate matches the exact analytic formula.""" par = np.array(par) - dev = qml.device("lightning.qubit", wires=n_qubits) + dev = qml.device(device_name, wires=n_qubits) init_state = np.zeros(2**n_qubits) init_state[-2::] = np.array([1.0 / np.sqrt(2), 1.0 / np.sqrt(2)], requires_grad=False) @@ -244,7 +292,8 @@ def test_phaseshift_gradient(self, n_qubits, par, tol): tape.trainable_params = {1} exact = np.cos(par) - grad_A = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + grad_A = method(tape) # different methods must agree assert np.allclose(grad_A, exact, atol=tol, rtol=0) @@ -260,7 +309,8 @@ def test_ry_gradient(self, par, tol, dev): # gradients exact = np.cos(par) - grad_A = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + grad_A = method(tape) # different methods must agree assert np.allclose(grad_A, exact, atol=tol, rtol=0) @@ -274,7 +324,8 @@ def test_rx_gradient(self, tol, dev): qml.expval(qml.PauliZ(0)) # circuit jacobians - dev_jacobian = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + dev_jacobian = method(tape) expected_jacobian = -np.sin(a) assert np.allclose(dev_jacobian, expected_jacobian, atol=tol, rtol=0) @@ -291,7 +342,8 @@ def test_multiple_rx_gradient_pauliz(self, tol, dev): qml.expval(qml.PauliZ(idx)) # circuit jacobians - dev_jacobian = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + dev_jacobian = method(tape) expected_jacobian = -np.diag(np.sin(params)) assert np.allclose(dev_jacobian, expected_jacobian, atol=tol, rtol=0) @@ -311,7 +363,8 @@ def test_multiple_rx_gradient_hermitian(self, tol, dev): tape.trainable_params = {0, 1, 2} # circuit jacobians - dev_jacobian = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + dev_jacobian = method(tape) expected_jacobian = -np.diag(np.sin(params)) assert np.allclose(dev_jacobian, expected_jacobian, atol=tol, rtol=0) @@ -337,7 +390,8 @@ def test_multiple_rx_gradient_expval_hermitian(self, tol, dev): ) tape.trainable_params = {0, 1, 2} - dev_jacobian = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + dev_jacobian = method(tape) expected_jacobian = np.array( [-np.sin(params[0]) * np.cos(params[2]), 0, -np.cos(params[0]) * np.sin(params[2])] ) @@ -374,7 +428,8 @@ def test_multiple_rx_gradient_expval_hamiltonian(self, tol, dev): qml.expval(ham) tape.trainable_params = {0, 1, 2} - dev_jacobian = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + dev_jacobian = method(tape) expected_jacobian = ( 0.3 * np.array([-np.sin(params[0]), 0, 0]) + 0.3 * np.array([0, -np.sin(params[1]), 0]) @@ -422,10 +477,11 @@ def test_gradients_pauliz(self, op, obs, dev): tape.trainable_params = set(range(1, 1 + op.num_params)) - tol = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 + tol, _ = get_tolerance_and_stepsize(dev) grad_F = (lambda t, fn: fn(qml.execute(t, dev, None)))(*qml.gradients.param_shift(tape)) - grad_D = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + grad_D = method(tape) assert np.allclose(grad_D, grad_F, atol=tol, rtol=0) @@ -465,10 +521,11 @@ def test_gradients_hermitian(self, op, dev): tape.trainable_params = set(range(1, 1 + op.num_params)) - tol = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 + tol, _ = get_tolerance_and_stepsize(dev) grad_F = (lambda t, fn: fn(qml.execute(t, dev, None)))(*qml.gradients.param_shift(tape)) - grad_D = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + grad_D = method(tape) assert np.allclose(grad_D, grad_F, atol=tol, rtol=0) @@ -483,9 +540,10 @@ def test_gradient_gate_with_multiple_parameters_pauliz(self, dev): tape.trainable_params = {1, 2, 3} - tol = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 + tol, _ = get_tolerance_and_stepsize(dev) - grad_D = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + grad_D = method(tape) tapes, fn = qml.gradients.param_shift(tape) grad_F = fn(qml.execute(tapes, dev, None)) @@ -507,9 +565,10 @@ def test_gradient_gate_with_multiple_parameters_hermitian(self, dev): tape.trainable_params = {1, 2, 3} - tol = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 + tol, _ = get_tolerance_and_stepsize(dev) - grad_D = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + grad_D = method(tape) tapes, fn = qml.gradients.param_shift(tape) grad_F = fn(qml.execute(tapes, dev, None)) @@ -536,9 +595,10 @@ def test_gradient_gate_with_multiple_parameters_hamiltonian(self, dev): tape.trainable_params = {1, 2, 3} - tol = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 + tol, _ = get_tolerance_and_stepsize(dev) - grad_D = dev.adjoint_jacobian(tape) + method = self.get_derivatives_method(dev) + grad_D = method(tape) tapes, fn = qml.gradients.param_shift(tape) grad_F = fn(qml.execute(tapes, dev, None)) @@ -549,6 +609,7 @@ def test_gradient_gate_with_multiple_parameters_hamiltonian(self, dev): # the different methods agree assert np.allclose(grad_D, grad_F, atol=tol, rtol=0) + @pytest.mark.skipif(ld._new_API, reason="Old API required") def test_use_device_state(self, tol, dev): """Tests that when using the device state, the correct answer is still returned.""" @@ -569,6 +630,7 @@ def test_use_device_state(self, tol, dev): assert np.allclose(dM1, dM2, atol=tol, rtol=0) + @pytest.mark.skipif(ld._new_API, reason="Old API required") def test_provide_starting_state(self, tol, dev): """Tests provides correct answer when provided starting state.""" x, y, z = [0.5, 0.3, -0.7] @@ -594,6 +656,7 @@ def test_provide_starting_state(self, tol, dev): dM2 = dev.adjoint_jacobian(tape, starting_state=state_vector) assert np.allclose(dM1, dM2, atol=tol, rtol=0) + @pytest.mark.skipif(ld._new_API, reason="Old API required") @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_provide_wrong_starting_state(self, dev): """Tests raise an exception when provided starting state mismatches.""" @@ -625,11 +688,12 @@ def test_state_return_type(self, dev): qml.state() tape.trainable_params = {0} + method = self.get_derivatives_method(dev) with pytest.raises( qml.QuantumFunctionError, match="This method does not support statevector return type." ): - dev.adjoint_jacobian(tape) + method(tape) class TestAdjointJacobianQNode: @@ -639,6 +703,7 @@ class TestAdjointJacobianQNode: def dev(self, request): return qml.device(device_name, wires=2, c_dtype=request.param) + @pytest.mark.skipif(ld._new_API, reason="Old API required") def test_finite_shots_warning(self): """Tests that a warning is raised when computing the adjoint diff on a device with finite shots""" @@ -678,16 +743,18 @@ def circuit(x, y, z): return qml.expval(qml.PauliX(0) @ qml.PauliZ(1)) qnode1 = QNode(circuit, dev, diff_method="adjoint") - spy = mocker.spy(dev, "adjoint_jacobian") + spy = ( + mocker.spy(dev, "execute_and_compute_derivatives") + if ld._new_API + else mocker.spy(dev, "adjoint_jacobian") + ) + tol, h = get_tolerance_and_stepsize(dev, step_size=True) grad_fn = qml.grad(qnode1) grad_A = grad_fn(*args) spy.assert_called() - h = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 - tol = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 - qnode2 = QNode(circuit, dev, diff_method="finite-diff", h=h) grad_fn = qml.grad(qnode2) grad_F = grad_fn(*args) @@ -759,7 +826,7 @@ def circuit(p): assert np.allclose(jac_ad, jac_bp, atol=tol, rtol=0) @pytest.mark.skipif( - device_name != "lightning.qubit" or not ld._CPP_BINARY_AVAILABLE, + device_name not in ("lightning.qubit", "lightning.qubit2") or not ld._CPP_BINARY_AVAILABLE, reason="N-controlled operations only implemented in lightning.qubit.", ) @pytest.mark.parametrize( @@ -860,7 +927,10 @@ def cost(p1, p2): zero_state = np.array([1.0, 0.0]) cost(reused_p, other_p) - spy = mocker.spy(dev, "adjoint_jacobian") + if ld._new_API: + spy = mocker.spy(dev, "execute_and_compute_derivatives") + else: + spy = mocker.spy(dev, "adjoint_jacobian") # analytic gradient grad_fn = qml.grad(cost) @@ -900,10 +970,12 @@ def circuit(params): qml.Rot(params[1], params[0], 2 * params[0], wires=[0]) return qml.expval(qml.PauliX(0)) - spy_analytic = mocker.spy(dev, "adjoint_jacobian") - - h = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 - tol = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 + spy_analytic = ( + mocker.spy(dev, "execute_and_compute_derivatives") + if ld._new_API + else mocker.spy(dev, "adjoint_jacobian") + ) + tol, h = get_tolerance_and_stepsize(dev, step_size=True) cost = QNode(circuit, dev, diff_method="finite-diff", h=h) @@ -933,17 +1005,18 @@ def f(params1, params2): qml.RY(tf.cos(params2), wires=[0]) return qml.expval(qml.PauliZ(0)) - if dev.R_DTYPE == np.float32: + if dev._new_API: + # tf expects float32 with new device API tf_r_dtype = tf.float32 + h = 2e-3 + tol = 1e-3 else: - tf_r_dtype = tf.float64 + tf_r_dtype = tf.float32 if dev.dtype == np.complex64 else tf.float64 + tol, h = get_tolerance_and_stepsize(dev, step_size=True) params1 = tf.Variable(0.3, dtype=tf_r_dtype) params2 = tf.Variable(0.4, dtype=tf_r_dtype) - h = 2e-3 if dev.R_DTYPE == np.float32 else 1e-7 - tol = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 - qnode1 = QNode(f, dev, interface="tf", diff_method="adjoint") qnode2 = QNode(f, dev, interface="tf", diff_method="finite-diff", h=h) @@ -974,8 +1047,7 @@ def f(params1, params2): params1 = torch.tensor(0.3, requires_grad=True) params2 = torch.tensor(0.4, requires_grad=True) - h = 2e-3 if dev.R_DTYPE == np.float32 else 1e-7 - tol = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 + tol, h = get_tolerance_and_stepsize(dev, step_size=True) qnode1 = QNode(f, dev, interface="torch", diff_method="adjoint") qnode2 = QNode(f, dev, interface="torch", diff_method="finite-diff", h=h) @@ -997,7 +1069,9 @@ def test_interface_jax(self, dev): jax interface""" jax = pytest.importorskip("jax") - if dev.R_DTYPE == np.float64: + dtype = np.float32 if dev.dtype == np.complex64 else np.float64 + + if dtype == np.float64: from jax import config config.update("jax_enable_x64", True) @@ -1008,11 +1082,9 @@ def f(params1, params2): qml.RY(jax.numpy.cos(params2), wires=[0]) return qml.expval(qml.PauliZ(0)) - params1 = jax.numpy.array(0.3, dev.R_DTYPE) - params2 = jax.numpy.array(0.4, dev.R_DTYPE) - - h = 2e-3 if dev.R_DTYPE == np.float32 else 1e-7 - tol = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 + params1 = jax.numpy.array(0.3, dtype) + params2 = jax.numpy.array(0.4, dtype) + tol, h = get_tolerance_and_stepsize(dev, step_size=True) qnode_adjoint = QNode(f, dev, interface="jax", diff_method="adjoint") qnode_fd = QNode(f, dev, interface="jax", diff_method="finite-diff", h=h) @@ -1541,7 +1613,7 @@ def test_diff_qubit_unitary(n_targets): n_wires = 6 dev = qml.device(device_name, wires=n_wires) dev_def = qml.device("default.qubit", wires=n_wires) - h = 1e-3 if dev.R_DTYPE == np.float32 else 1e-7 + _, h = get_tolerance_and_stepsize(dev, step_size=True) np.random.seed(1337) init_state = np.random.rand(2**n_wires) + 1j * np.random.rand(2**n_wires) diff --git a/tests/test_apply.py b/tests/test_apply.py index f1be7fadfd..2fa37dab5a 100644 --- a/tests/test_apply.py +++ b/tests/test_apply.py @@ -26,7 +26,11 @@ from pennylane.operation import Operation from pennylane.wires import Wires +if ld._new_API and not ld._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) + +@pytest.mark.skipif(ld._new_API, reason="Old API required") class TestApply: """Tests that operations of certain operations are applied correctly or that the proper errors are raised. @@ -529,6 +533,7 @@ def test_apply_state_vector_lightning_handle(self, qubit_device, tol): class TestExpval: """Tests that expectation values are properly calculated or that the proper errors are raised.""" + @pytest.mark.skipif(ld._new_API, reason="Old API required") @pytest.mark.parametrize( "operation,input,expected_output", [ @@ -563,6 +568,7 @@ def test_expval_single_wire_no_parameters( assert np.isclose(res, expected_output, atol=tol, rtol=0) + @pytest.mark.xfail(ld._new_API, reason="Old API required") def test_expval_estimate(self): """Test that the expectation value is not analytically calculated""" dev = qml.device(device_name, wires=1, shots=3) @@ -581,6 +587,7 @@ def circuit(): class TestVar: """Tests that variances are properly calculated.""" + @pytest.mark.skipif(ld._new_API, reason="Old API required") @pytest.mark.parametrize( "operation,input,expected_output", [ @@ -615,6 +622,7 @@ def test_var_single_wire_no_parameters( assert np.isclose(res, expected_output, atol=tol, rtol=0) + @pytest.mark.xfail(ld._new_API, reason="Old API required") def test_var_estimate(self): """Test that the variance is not analytically calculated""" @@ -631,6 +639,7 @@ def circuit(): assert var != 1.0 +@pytest.mark.skipif(ld._new_API, reason="Old API required") class TestSample: """Tests that samples are properly calculated.""" @@ -698,9 +707,13 @@ def test_load_default_qubit_device(self): """Test that the default plugin loads correctly""" dev = qml.device(device_name, wires=2) - assert dev.num_wires == 2 - assert dev.shots is None - assert dev.short_name == device_name + if dev._new_API: + assert not dev.shots + assert len(dev.wires) == 2 + else: + assert dev.shots is None + assert dev.num_wires == 2 + assert dev.short_name == device_name @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_no_backprop(self): @@ -764,6 +777,7 @@ def circuit(x): assert np.isclose(circuit(p), 1, atol=tol, rtol=0) + @pytest.mark.xfail(ld._new_API, reason="Old API required") def test_nonzero_shots(self, tol_stochastic): """Test that the default qubit plugin provides correct result for high shot number""" @@ -801,7 +815,8 @@ def test_supported_gate_single_wire_no_parameters( dev = qubit_device(wires=1) op = getattr(qml.ops, name) - assert dev.supports_operation(name) + if hasattr(dev, "supports_operation"): + assert dev.supports_operation(name) @qml.qnode(dev) def circuit(): @@ -827,7 +842,8 @@ def test_supported_gate_two_wires_no_parameters( dev = qubit_device(wires=2) op = getattr(qml.ops, name) - assert dev.supports_operation(name) + if hasattr(dev, "supports_operation"): + assert dev.supports_operation(name) @qml.qnode(dev) def circuit(): @@ -850,7 +866,8 @@ def test_supported_gate_three_wires_no_parameters( dev = qubit_device(wires=3) op = getattr(qml.ops, name) - assert dev.supports_operation(name) + if hasattr(dev, "supports_operation"): + assert dev.supports_operation(name) @qml.qnode(dev) def circuit(): @@ -877,7 +894,8 @@ def test_supported_state_preparation(self, qubit_device, tol, name, par, expecte dev = qubit_device(wires=2) op = getattr(qml.ops, name) - assert dev.supports_operation(name) + if hasattr(dev, "supports_operation"): + assert dev.supports_operation(name) @qml.qnode(dev) def circuit(): @@ -995,7 +1013,8 @@ def test_supported_gate_single_wire_with_parameters( dev = qubit_device(wires=1) op = getattr(qml.ops, name) - assert dev.supports_operation(name) + if hasattr(dev, "supports_operation"): + assert dev.supports_operation(name) @qml.qnode(dev) def circuit(): @@ -1037,7 +1056,8 @@ def test_supported_gate_two_wires_with_parameters( dev = qubit_device(wires=2) op = getattr(qml.ops, name) - assert dev.supports_operation(name) + if hasattr(dev, "supports_operation"): + assert dev.supports_operation(name) @qml.qnode(dev) def circuit(): @@ -1072,7 +1092,8 @@ def test_supported_observable_single_wire_no_parameters( dev = qubit_device(wires=1) obs = getattr(qml.ops, name) - assert dev.supports_observable(name) + if hasattr(dev, "supports_observable"): + assert dev.supports_observable(name) @qml.qnode(dev) def circuit(): @@ -1097,7 +1118,8 @@ def test_supported_observable_single_wire_with_parameters( dev = qubit_device(wires=1) obs = getattr(qml.ops, name) - assert dev.supports_observable(name) + if hasattr(dev, "supports_observable"): + assert dev.supports_observable(name) @qml.qnode(dev) def circuit(): @@ -1106,6 +1128,7 @@ def circuit(): assert np.isclose(circuit(), expected_output, atol=tol, rtol=0) + @pytest.mark.xfail(ld._new_API, reason="Old API required") def test_multi_samples_return_correlated_results(self, qubit_device): """Tests if the samples returned by the sample function have the correct dimensions @@ -1123,6 +1146,7 @@ def circuit(): assert np.array_equal(outcomes[0], outcomes[1]) + @pytest.mark.xfail(ld._new_API, reason="Old API required") @pytest.mark.parametrize("num_wires", [3, 4, 5, 6, 7, 8]) def test_multi_samples_return_correlated_results_more_wires_than_size_of_observable( self, num_wires @@ -1143,6 +1167,7 @@ def circuit(): assert np.array_equal(outcomes[0], outcomes[1]) + @pytest.mark.xfail(ld._new_API, reason="Old API required") def test_snapshot_is_ignored_without_shot(self): """Tests if the Snapshot operator is ignored correctly""" dev = qml.device(device_name, wires=4) @@ -1159,6 +1184,7 @@ def circuit(): assert np.allclose(outcomes, [0.0]) + @pytest.mark.xfail(ld._new_API, reason="Old API required") def test_snapshot_is_ignored_with_shots(self): """Tests if the Snapshot operator is ignored correctly""" dev = qml.device(device_name, wires=4, shots=1000) @@ -1182,13 +1208,18 @@ def test_apply_qpe(self, qubit_device, tol): @qml.qnode(dev) def circuit(): qml.Hadamard(wires=0) - qml.QuantumPhaseEstimation(qml.matrix(qml.Hadamard)(wires=0), [0], [1]) + qml.QuantumPhaseEstimation(qml.matrix(qml.Hadamard, wire_order=[0])(wires=0), [0], [1]) return qml.probs(wires=[0, 1]) - circuit() + probs = circuit() - res_sv = dev.state - res_probs = dev.probability([0, 1]) + if ld._new_API: + # pylint: disable=protected-access + res_sv = dev._statevector.state + res_probs = probs + else: + res_sv = dev.state + res_probs = dev.probability([0, 1]) expected_sv = np.array( [ @@ -1207,6 +1238,7 @@ def circuit(): class TestApplyLightningMethod: """Unit tests for the apply_lightning method.""" + @pytest.mark.skipif(ld._new_API, reason="Old API required") def test_apply_identity_skipped(self, mocker, tol): """Test identity operation does not perform additional computations.""" dev = qml.device(device_name, wires=1) @@ -1282,6 +1314,7 @@ def circuit(): assert np.allclose(results, expected) +@pytest.mark.skipif(ld._new_API, reason="Old API required.") @pytest.mark.skipif( ld._CPP_BINARY_AVAILABLE, reason="Test only applies when binaries are unavailable" ) diff --git a/tests/test_comparison.py b/tests/test_comparison.py index 90bcc41128..e1f8112f87 100644 --- a/tests/test_comparison.py +++ b/tests/test_comparison.py @@ -88,7 +88,8 @@ def circuit(measurement): default = qml.QNode(circuit, dev_d) lightning(qml.expval(qml.PauliZ(0))) - lightning_state = dev_l.state + # pylint: disable=protected-access + lightning_state = dev_l._statevector.state if dev_l._new_API else dev_l.state default_state = default(qml.state) @@ -132,7 +133,8 @@ def circuit(measurement): default = qml.QNode(circuit, dev_d) lightning(qml.expval(qml.PauliZ(0))) - lightning_state = dev_l.state + # pylint: disable=protected-access + lightning_state = dev_l._statevector.state if dev_l._new_API else dev_l.state default_state = default(qml.state) @@ -183,7 +185,8 @@ def circuit(measurement): default = qml.QNode(circuit, dev_d) lightning(qml.expval(qml.PauliZ(0))) - lightning_state = dev_l.state + # pylint: disable=protected-access + lightning_state = dev_l._statevector.state if dev_l._new_API else dev_l.state default_state = default(qml.state) @@ -239,7 +242,8 @@ def circuit(measurement): default = qml.QNode(circuit, dev_d) lightning(qml.expval(qml.PauliZ(0))) - lightning_state = dev_l.state + # pylint: disable=protected-access + lightning_state = dev_l._statevector.state if dev_l._new_API else dev_l.state default_state = default(qml.state) @@ -276,7 +280,8 @@ def circuit(measurement): default = qml.QNode(circuit, dev_d) lightning(qml.expval(qml.PauliZ(0))) - lightning_state = dev_l.state + # pylint: disable=protected-access + lightning_state = dev_l._statevector.state if dev_l._new_API else dev_l.state default_state = default(qml.state) diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index 29a7874871..df9de740e2 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -23,6 +23,7 @@ pytest.skip("No binary module found. Skipping.", allow_module_level=True) +@pytest.mark.skipif(ld._new_API, reason="Old API required") class TestDenseMatrixDecompositionThreshold: """Tests, for QFT and Grover operators, the automatic transition from full matrix to decomposition on calculations.""" diff --git a/tests/test_execute.py b/tests/test_execute.py index 6e1db8ebb0..631ecc5b65 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -18,9 +18,12 @@ import pennylane as qml import pytest -from conftest import device_name +from conftest import LightningDevice, device_name from pennylane import numpy as np +if LightningDevice._new_API and not LightningDevice._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) + @pytest.mark.parametrize("diff_method", ("param_shift", "finite_diff")) class TestQChem: diff --git a/tests/test_expval.py b/tests/test_expval.py index 95a985c362..877dd10266 100644 --- a/tests/test_expval.py +++ b/tests/test_expval.py @@ -19,13 +19,17 @@ import numpy as np import pennylane as qml import pytest -from conftest import PHI, THETA, VARPHI, device_name +from conftest import PHI, THETA, VARPHI, LightningDevice, device_name + +if LightningDevice._new_API and not LightningDevice._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) @pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) class TestExpval: """Test expectation values""" + @pytest.mark.skipif(LightningDevice._new_API, reason="Old API required") def test_identity_expectation(self, theta, phi, qubit_device, tol): """Test that identity expectation value (i.e. the trace) is 1""" dev = qubit_device(wires=3) @@ -41,6 +45,7 @@ def test_identity_expectation(self, theta, phi, qubit_device, tol): res = np.array([dev.expval(O1), dev.expval(O2)]) assert np.allclose(res, np.array([1, 1]), tol) + @pytest.mark.skipif(LightningDevice._new_API, reason="Old API required") def test_pauliz_expectation(self, theta, phi, qubit_device, tol): """Test that PauliZ expectation value is correct""" dev = qubit_device(wires=3) @@ -56,6 +61,7 @@ def test_pauliz_expectation(self, theta, phi, qubit_device, tol): res = np.array([dev.expval(O1), dev.expval(O2)]) assert np.allclose(res, np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]), tol) + @pytest.mark.skipif(LightningDevice._new_API, reason="Old API required") def test_paulix_expectation(self, theta, phi, qubit_device, tol): """Test that PauliX expectation value is correct""" dev = qubit_device(wires=3) @@ -73,6 +79,7 @@ def test_paulix_expectation(self, theta, phi, qubit_device, tol): res, np.array([np.sin(theta) * np.sin(phi), np.sin(phi)], dtype=dev.C_DTYPE), tol * 10 ) + @pytest.mark.skipif(LightningDevice._new_API, reason="Old API required") def test_pauliy_expectation(self, theta, phi, qubit_device, tol): """Test that PauliY expectation value is correct""" dev = qubit_device(wires=3) @@ -88,6 +95,7 @@ def test_pauliy_expectation(self, theta, phi, qubit_device, tol): res = np.array([dev.expval(O1), dev.expval(O2)]) assert np.allclose(res, np.array([0, -np.cos(theta) * np.sin(phi)]), tol) + @pytest.mark.skipif(LightningDevice._new_API, reason="Old API required") def test_hadamard_expectation(self, theta, phi, qubit_device, tol): """Test that Hadamard expectation value is correct""" dev = qubit_device(wires=3) @@ -248,6 +256,7 @@ def circuit(x, y): assert qml.math.allclose(g, expected) +@pytest.mark.skipif(LightningDevice._new_API, reason="Old API required") @pytest.mark.parametrize("theta,phi,varphi", list(zip(THETA, PHI, VARPHI))) class TestTensorExpval: """Test tensor expectation values""" diff --git a/tests/test_gates.py b/tests/test_gates.py index 847c3a845a..18f925068e 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -22,6 +22,9 @@ import pytest from conftest import PHI, THETA, LightningDevice, device_name +if LightningDevice._new_API and not LightningDevice._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) + @pytest.fixture def op(op_name): @@ -244,6 +247,7 @@ def output(input): assert np.allclose(unitary, random_unitary_inv) +@pytest.mark.skipif(LightningDevice._new_API, reason="Old API required") @pytest.mark.skipif(not LightningDevice._CPP_BINARY_AVAILABLE, reason="Lightning binary required") @pytest.mark.parametrize( "obs,has_rotation", @@ -320,7 +324,7 @@ def circuit(): @pytest.mark.skipif( - device_name != "lightning.qubit", + device_name not in ("lightning.qubit", "lightning.qubit2"), reason="N-controlled operations only implemented in lightning.qubit.", ) @pytest.mark.parametrize("control_value", [False, True]) @@ -363,7 +367,7 @@ def circuit(): @pytest.mark.skipif( - device_name != "lightning.qubit", + device_name not in ("lightning.qubit", "lightning.qubit2"), reason="N-controlled operations only implemented in lightning.qubit.", ) @pytest.mark.parametrize( @@ -440,7 +444,7 @@ def circuit(): @pytest.mark.skipif( - device_name != "lightning.qubit", + device_name not in ("lightning.qubit", "lightning.qubit2"), reason="N-controlled operations only implemented in lightning.qubit.", ) def test_controlled_qubit_unitary_from_op(tol): @@ -461,7 +465,7 @@ def circuit(x): @pytest.mark.skipif( - device_name != "lightning.qubit", + device_name not in ("lightning.qubit", "lightning.qubit2"), reason="N-controlled operations only implemented in lightning.qubit.", ) @pytest.mark.parametrize("control_wires", range(4)) diff --git a/tests/test_measurements.py b/tests/test_measurements.py index a1aa1abd85..baca67189b 100644 --- a/tests/test_measurements.py +++ b/tests/test_measurements.py @@ -29,6 +29,7 @@ pytest.skip("No binary module found. Skipping.", allow_module_level=True) +@pytest.mark.skipif(ld._new_API, reason="Old API required") def test_measurements(): dev = qml.device(device_name, wires=2) m = dev.measurements @@ -56,6 +57,7 @@ class TestProbs: def dev(self, request): return qml.device(device_name, wires=2, c_dtype=request.param) + @pytest.mark.skipif(ld._new_API, reason="Old API required") def test_probs_dtype64(self, dev): """Test if probs changes the state dtype""" _state = dev._asarray( @@ -121,6 +123,7 @@ def circuit(): assert np.allclose(circuit(), cases[1], atol=tol, rtol=0) + @pytest.mark.skipif(ld._new_API, reason="Old API required") @pytest.mark.parametrize( "cases", [ @@ -200,6 +203,7 @@ def circuit(): assert np.allclose(circuit(), cases[1], atol=tol, rtol=0) + @pytest.mark.skipif(ld._new_API, reason="Old API required") @pytest.mark.parametrize( "cases", [ @@ -237,6 +241,7 @@ class TestExpval: def dev(self, request): return qml.device(device_name, wires=2, c_dtype=request.param) + @pytest.mark.skipif(ld._new_API, reason="Old API required") def test_expval_dtype64(self, dev): """Test if expval changes the state dtype""" _state = np.array([1, 0, 0, 0]).astype(dev.C_DTYPE) @@ -351,7 +356,7 @@ def circuit(): qml.RX(0.52, wires=0) return qml.expval(qml.RX(0.742, wires=[0])) - with pytest.raises(qml._device.DeviceError, match="Observable RX not supported"): + with pytest.raises(qml._device.DeviceError, match="Observable RX.*not supported"): circuit() def test_observable_return_type_is_expectation(self, dev): @@ -373,6 +378,7 @@ class TestVar: def dev(self, request): return qml.device(device_name, wires=2, c_dtype=request.param) + @pytest.mark.skipif(ld._new_API, reason="Old API required") def test_var_dtype64(self, dev): """Test if var changes the state dtype""" _state = np.array([1, 0, 0, 0]).astype(np.complex64) @@ -451,7 +457,7 @@ def circuit(): qml.RX(0.52, wires=0) return qml.var(qml.RX(0.742, wires=[0])) - with pytest.raises(qml._device.DeviceError, match="Observable RX not supported"): + with pytest.raises(qml._device.DeviceError, match="Observable RX.*not supported"): circuit() def test_observable_return_type_is_variance(self, dev): @@ -480,13 +486,14 @@ def circuit(): qml.RX(0.52, wires=0) return qml.var(qml.RX(0.742, wires=[0])) - with pytest.raises(qml._device.DeviceError, match="Observable RX not supported"): + with pytest.raises(qml._device.DeviceError, match="Observable RX.*not supported"): circuit() class TestWiresInExpval: """Test different Wires settings in Lightning's expval.""" + @pytest.mark.skipif(ld._new_API, reason="Old API required") @pytest.mark.parametrize( "wires1, wires2", [ @@ -531,6 +538,7 @@ def circuit2(): assert np.allclose(circuit1(), circuit2(), atol=tol) + @pytest.mark.skipif(ld._new_API, reason="Old API required") @pytest.mark.parametrize( "wires1, wires2", [ @@ -584,6 +592,7 @@ def circuit2(): assert np.allclose(circuit1(), circuit2(), atol=tol) +@pytest.mark.skipif(ld._new_API, reason="Old API required") class TestSample: """Tests that samples are properly calculated.""" @@ -630,6 +639,7 @@ def test_sample_values(self, qubit_device, tol): class TestWiresInVar: """Test different Wires settings in Lightning's var.""" + @pytest.mark.skipif(ld._new_API, reason="Old API required") @pytest.mark.parametrize( "wires1, wires2", [ @@ -673,6 +683,7 @@ def circuit2(): @flaky(max_runs=5) +@pytest.mark.skipif(ld._new_API, reason="Old API required") @pytest.mark.parametrize("shots", [10000, [10000, 11111]]) @pytest.mark.parametrize("measure_f", [qml.counts, qml.expval, qml.probs, qml.sample, qml.var]) @pytest.mark.parametrize( diff --git a/tests/test_var.py b/tests/test_var.py index bf0779da6f..8c3d9defe8 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -17,7 +17,10 @@ import numpy as np import pennylane as qml import pytest -from conftest import PHI, THETA, VARPHI +from conftest import PHI, THETA, VARPHI, LightningDevice + +if LightningDevice._new_API and not LightningDevice._CPP_BINARY_AVAILABLE: + pytest.skip("No binary module found. Skipping.", allow_module_level=True) np.random.seed(42) @@ -26,6 +29,7 @@ class TestVar: """Tests for the variance""" + @pytest.mark.skipif(LightningDevice._new_API, reason="Old API required") def test_var(self, theta, phi, qubit_device, tol): """Tests for variance calculation""" dev = qubit_device(wires=3) @@ -71,6 +75,7 @@ def circuit(): assert np.allclose(circ(), circ_def(), tol) +@pytest.mark.skipif(LightningDevice._new_API, reason="Old API required") @pytest.mark.parametrize("theta, phi, varphi", list(zip(THETA, PHI, VARPHI))) class TestTensorVar: """Tests for variance of tensor observables""" diff --git a/tests/test_vjp.py b/tests/test_vjp.py index d53b9b4135..70bd091bf9 100644 --- a/tests/test_vjp.py +++ b/tests/test_vjp.py @@ -25,6 +25,9 @@ if not ld._CPP_BINARY_AVAILABLE: pytest.skip("No binary module found. Skipping.", allow_module_level=True) +if ld._new_API: + pytest.skip("Old API required", allow_module_level=True) + class TestVectorJacobianProduct: """Tests for the `vjp` function"""