From 044ffe8e06c87b57588aa72c4c7431464b25614c Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Wed, 27 Mar 2024 09:20:27 -0400 Subject: [PATCH] Add support for `LinearCombination` (#649) * 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 * add cpp binary available variable * remove device definition * update dev version * Auto update version * reduce dependency on DefaultQubit for tests * update LightningQubit2 * clean test_measurements_class.py * isort+black * review suggestion * fix docs * Add qml.var support. * Add probs support. * increase tolerance * Auto update version * isort * Add double-obs tests. * Pin pytest version (#624) * update dev version * update changelog * pin pytest version in requirement files * add a requirements file for tests against Pennylane master * update wheels' workflows * Version Bump (#626) * post release version bump * trigger CI --------- Co-authored-by: AmintorDusko Co-authored-by: AmintorDusko * increase tolerance * Introduce isort. (#623) * Introduce isort. * Auto update version * Update changelog * Auto update version * Update changelog. * trigger ci --------- Co-authored-by: Dev version update bot * Auto update version * isort * Add qml.var support. * Add probs support. * Add measurement tests with wires. * review suggestions * remove unused imports * Introduce _new_API and fix/skip few tests. * Fix few more tests. * Skip shots, adjoint, vjp with new API. * remove diagonalization gate application from state vector * pytest.skip tests * Auto update version * Fix format * Fix no-bin interface. * WIP * Initial shots support + fix test_measurement tests. * 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 * Add fewLQ2 tests. * remove adjoint diff support from supports derivatives * Remove print from test_apply * Add expval/var tests. * Remove duplicate class data. * Include LQ2 in linux ests. * Add _group_measurements support. * --cov-append * Add mcmc capability + tests. * Auto update version * update dev version * add LightningAdjointJacobian class * add unit tests for the LightningAdjointJacobian class * format * add changelog for PR #613 * [skip ci] Added skeleton file for LQ2 unit tests * update changelog * update adjoint Jacobian * Auto update version * codefactor * Add shots tests and fix bugs in LQ, LQ2. * 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 * fix processing_fn_expval * make a proper new_tape * Added init tests; Added skeleton tests for helpers * Fix more bug with shots. * trigger CI * Change pennylane branch for 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 * Refactor shots test. * 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 * Auto update version * Rerun isort. * Uncomment integration tests. * Reformat * Delete symlink * Fix pylint. * Run linux tests in parallel (when possible). * Run double obs tests with shots. * Revert linux tests * Fix bg in diag_gates. * Call isort/black with python -m * update dev version * Add docstrings, rm C_DTYPE. * Auto update version * comment isort check * trigger ci * Update tests/test_expval.py Co-authored-by: Amintor Dusko <87949283+AmintorDusko@users.noreply.github.com> * Init mcmc params to None in measurements. * Reformat with python3.11 * Reformat black * Auto update version * update QuantumScriptSerializer * remove LightningQubit2 from init * update setup.py * remove lightning.qubit2 from tests configuration * remove extra tests for lightning.qubit2 * migrate lightning.qubit2 to lightning.qubit on tests * make lightning.qubit2 the new lightning.qubit * add device name (necessary for pl-device-test) * Add _measure_hamiltonian_with_samples _measure_sum_with_samples * fix tests without binary * check for jac size before reshaping * remove obsolete tests * organize tests * fix test for Windows wheels * Adding LC to supported obs list for all devices * Updating serialization * Trying out test changes * Updated PL dependency * Running isort * Update CPP layer for observable accessible functions * Auto update version * Allow output from TP CPP layer * Fixed serialize tests * Added legacy fixture to failing tests * Updated requirements with debug branch * Trigger CI * Auto update version * Trigger CI * Trigger CI * Added recursive logic to find projectors * format * mark tests to be dual tested and some small fixes * example of test failing * remove print and move ham to tape scope * Fixed expval test; isort * Auto update version * isort * Updated device tests; changelog entry * Update mpitests/test_apply.py * Auto update version * Fix terms in ham serialization. * Use requirements-dev.txt to install black/isort. * Mark test_single_return_value flaky. Add comment on split_obs. * Apply suggestions from code review Co-authored-by: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Co-authored-by: Vincent Michaud-Rioux * Auto update version * Auto update version * Trigger CI * Fix format * Updated PL branch in reqs-dev * Update requirements * Added fix for prod with overlapping wires * Formatting * Point requirements to PL master * Pinning cmake * Pinned cmake in workflows * Pinned cmake in more workflows * Trigger CI * Apply suggestions from code review Co-authored-by: Ali Asadi <10773383+maliasadi@users.noreply.github.com> * [skip ci] Apply suggestions from code review Co-authored-by: Ali Asadi <10773383+maliasadi@users.noreply.github.com> * Trigger CI * Fix indent. * Fix workflow files indentation * Comment triggers. (#662) * Comment triggers. * pytest -x * trigger ci * Do not skip comp for LQ. * Revert triggers * Revert changes --------- Co-authored-by: AmintorDusko Co-authored-by: Amintor Dusko <87949283+AmintorDusko@users.noreply.github.com> Co-authored-by: Dev version update bot Co-authored-by: Vincent Michaud-Rioux Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: AmintorDusko Co-authored-by: Vincent Michaud-Rioux Co-authored-by: albi3ro Co-authored-by: Lee J. O'Riordan Co-authored-by: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Co-authored-by: Lee James O'Riordan --- .github/CHANGELOG.md | 3 + .github/workflows/tests_gpu_cuda.yml | 8 +- .github/workflows/tests_linux_x86_mpi_gpu.yml | 6 +- .github/workflows/wheel_macos_x86_64.yml | 4 +- .github/workflows/wheel_noarch.yml | 4 +- .github/workflows/wheel_win_x86_64.yml | 4 +- mpitests/test_apply.py | 4 +- pennylane_lightning/core/_serialize.py | 39 +++- pennylane_lightning/core/_version.py | 2 +- pennylane_lightning/core/lightning_base.py | 60 ++++- .../core/src/bindings/Bindings.hpp | 8 + .../lightning_gpu/lightning_gpu.py | 51 +--- .../lightning_kokkos/lightning_kokkos.py | 55 +---- .../lightning_qubit/lightning_qubit.py | 23 ++ requirements-dev.txt | 2 +- tests/conftest.py | 28 +++ .../test_adjoint_jacobian_class.py | 2 + .../test_measurements_class.py | 1 + tests/new_api/test_device.py | 55 ++++- tests/test_adjoint_jacobian.py | 22 +- tests/test_execute.py | 1 + tests/test_measurements.py | 6 + tests/test_serialize.py | 221 ++++++++++++++---- tests/test_vjp.py | 3 +- 24 files changed, 413 insertions(+), 199 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index bb55e38510..f2b3950bfb 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -24,6 +24,9 @@ [(#607)](https://github.com/PennyLaneAI/pennylane-lightning/pull/607) [(#628)](https://github.com/PennyLaneAI/pennylane-lightning/pull/628) +* Add support for using new operator arithmetic as the default. + [(#649)](https://github.com/PennyLaneAI/pennylane-lightning/pull/649) + ### Breaking changes * Migrate `lightning.qubit` to the new device API. diff --git a/.github/workflows/tests_gpu_cuda.yml b/.github/workflows/tests_gpu_cuda.yml index 06741a1152..d3abac6937 100644 --- a/.github/workflows/tests_gpu_cuda.yml +++ b/.github/workflows/tests_gpu_cuda.yml @@ -119,7 +119,9 @@ jobs: - name: Install required packages run: | - python -m pip install ninja cmake custatevec-cu${{ matrix.cuda_version }} + # Omitting the installation of cmake v3.29.0 due to + # https://github.com/scikit-build/cmake-python-distributions/pull/474 + python -m pip install ninja "cmake!=3.29.0" custatevec-cu${{ matrix.cuda_version }} sudo apt-get -y -q install liblapack-dev - name: Build and run unit tests @@ -241,7 +243,9 @@ jobs: run: | cd main python -m pip install -r requirements-dev.txt - python -m pip install cmake custatevec-cu${{ matrix.cuda_version }} openfermionpyscf + # Omitting the installation of cmake v3.29.0 due to + # https://github.com/scikit-build/cmake-python-distributions/pull/474 + python -m pip install "cmake!=3.29.0" custatevec-cu${{ matrix.cuda_version }} openfermionpyscf - name: Checkout PennyLane for release build if: inputs.pennylane-version == 'release' diff --git a/.github/workflows/tests_linux_x86_mpi_gpu.yml b/.github/workflows/tests_linux_x86_mpi_gpu.yml index 7af97b5046..b491363bfc 100644 --- a/.github/workflows/tests_linux_x86_mpi_gpu.yml +++ b/.github/workflows/tests_linux_x86_mpi_gpu.yml @@ -94,7 +94,9 @@ jobs: - name: Install required packages run: | python -m pip install -r requirements-dev.txt - python -m pip install cmake custatevec-cu12 + # Omitting the installation of cmake v3.29.0 due to + # https://github.com/scikit-build/cmake-python-distributions/pull/474 + python -m pip install "cmake!=3.29.0" custatevec-cu12 sudo apt-get -y -q install liblapack-dev - name: Validate GPU version and installed compiler and modules @@ -240,7 +242,7 @@ jobs: source /etc/profile.d/modules.sh && module use /opt/modules/ && module load ${{ matrix.mpilib }}/cuda-${{ matrix.cuda_version_maj }}.${{ matrix.cuda_version_min }} python -m pip install -r requirements-dev.txt python -m pip install custatevec-cu${{ matrix.cuda_version_maj }} mpi4py openfermionpyscf - SKIP_COMPILATION=True PL_BACKEND=lightning_qubit python -m pip install -e . -vv + PL_BACKEND=lightning_qubit python -m pip install -e . -vv - name: Checkout PennyLane for release build if: inputs.pennylane-version == 'release' diff --git a/.github/workflows/wheel_macos_x86_64.yml b/.github/workflows/wheel_macos_x86_64.yml index 7bcf05a793..e3931dbcff 100644 --- a/.github/workflows/wheel_macos_x86_64.yml +++ b/.github/workflows/wheel_macos_x86_64.yml @@ -68,7 +68,9 @@ jobs: run: | mkdir -p ${{ github.workspace}}/Kokkos_install/${{ matrix.exec_model }} cd kokkos - python -m pip install cmake ninja + # Omitting the installation of cmake v3.29.0 due to + # https://github.com/scikit-build/cmake-python-distributions/pull/474 + python -m pip install "cmake!=3.29.0" ninja cmake -BBuild . -DCMAKE_INSTALL_PREFIX=${{ github.workspace}}/Kokkos_install/${{ matrix.exec_model }} \ -DKokkos_ENABLE_COMPLEX_ALIGN=OFF \ diff --git a/.github/workflows/wheel_noarch.yml b/.github/workflows/wheel_noarch.yml index ed4bfbdff4..5de9cf6c5b 100644 --- a/.github/workflows/wheel_noarch.yml +++ b/.github/workflows/wheel_noarch.yml @@ -44,7 +44,9 @@ jobs: - name: Install CMake and ninja run: | - python -m pip install --upgrade cmake ninja + # Omitting the installation of cmake v3.29.0 due to + # https://github.com/scikit-build/cmake-python-distributions/pull/474 + python -m pip install --upgrade "cmake!=3.29.0" ninja - name: Build wheels if: ${{ matrix.pl_backend == 'lightning_qubit'}} diff --git a/.github/workflows/wheel_win_x86_64.yml b/.github/workflows/wheel_win_x86_64.yml index 88a9feba10..3d7e86f0e5 100644 --- a/.github/workflows/wheel_win_x86_64.yml +++ b/.github/workflows/wheel_win_x86_64.yml @@ -64,7 +64,9 @@ jobs: - name: Install dependencies if: steps.kokkos-cache.outputs.cache-hit != 'true' run: | - python -m pip install cmake build + # Omitting the installation of cmake v3.29.0 due to + # https://github.com/scikit-build/cmake-python-distributions/pull/474 + python -m pip install "cmake!=3.29.0" build - name: Build Kokkos core library if: steps.kokkos-cache.outputs.cache-hit != 'true' diff --git a/mpitests/test_apply.py b/mpitests/test_apply.py index 87152ee6fb..8bffcc6b29 100644 --- a/mpitests/test_apply.py +++ b/mpitests/test_apply.py @@ -450,7 +450,7 @@ def test_dev_reset(self, tol, dev_mpi): comm.Scatter(state_vector, local_state_vector, root=0) dev_cpu = qml.device("lightning.qubit", wires=num_wires, c_dtype=c_dtype) - dev_cpu.reset() + dev_cpu._statevector.reset_state() def circuit(): qml.PauliX(wires=[0]) @@ -556,7 +556,7 @@ def circuit(): cpu_qnode = qml.QNode(circuit, dev_cpu) expected_output_cpu = cpu_qnode() - comm.Bcast(expected_output_cpu, root=0) + comm.Bcast(np.array(expected_output_cpu), root=0) mpi_qnode = qml.QNode(circuit, dev_mpi) expected_output_mpi = mpi_qnode() diff --git a/pennylane_lightning/core/_serialize.py b/pennylane_lightning/core/_serialize.py index d4a88512e8..ae64574b61 100644 --- a/pennylane_lightning/core/_serialize.py +++ b/pennylane_lightning/core/_serialize.py @@ -14,7 +14,7 @@ r""" Helper functions for serializing quantum tapes. """ -from typing import List, Tuple +from typing import List, Sequence, Tuple import numpy as np from pennylane import ( @@ -34,6 +34,7 @@ ) from pennylane.math import unwrap from pennylane.operation import Tensor +from pennylane.ops import Prod, SProd, Sum from pennylane.tape import QuantumTape pauli_name_map = { @@ -183,19 +184,22 @@ def _named_obs(self, observable, wires_map: dict = None): def _hermitian_ob(self, observable, wires_map: dict = None): """Serializes a Hermitian observable""" - assert not isinstance(observable, Tensor) wires = [wires_map[w] for w in observable.wires] if wires_map else observable.wires.tolist() return self.hermitian_obs(matrix(observable).ravel().astype(self.ctype), wires) def _tensor_ob(self, observable, wires_map: dict = None): """Serialize a tensor observable""" - assert isinstance(observable, Tensor) - return self.tensor_obs([self._ob(obs, wires_map) for obs in observable.obs]) + obs = observable.obs if isinstance(observable, Tensor) else observable.operands + return self.tensor_obs([self._ob(o, wires_map) for o in obs]) def _hamiltonian(self, observable, wires_map: dict = None): - coeffs = np.array(unwrap(observable.coeffs)).astype(self.rtype) - terms = [self._ob(t, wires_map) for t in observable.ops] + coeffs, ops = observable.terms() + coeffs = np.array(unwrap(coeffs)).astype(self.rtype) + terms = [self._ob(t, wires_map) for t in ops] + # TODO: This is in case `_hamiltonian` is called recursively which would cause a list + # to be passed where `_ob` expects an observable. + terms = [t[0] if isinstance(t, Sequence) and len(t) == 1 else t for t in terms] if self.split_obs: return [self.hamiltonian_obs([c], [t]) for (c, t) in zip(coeffs, terms)] @@ -254,23 +258,33 @@ def _pauli_sentence(self, observable, wires_map: dict = None): terms = [self._pauli_word(pw, wires_map) for pw in pwords] coeffs = np.array(coeffs).astype(self.rtype) + # TODO: Add this + # if len(terms) == 1 and coeffs[0] == 1.0: + # return terms[0] + if self.split_obs: return [self.hamiltonian_obs([c], [t]) for (c, t) in zip(coeffs, terms)] return self.hamiltonian_obs(coeffs, terms) - # pylint: disable=protected-access + # pylint: disable=protected-access, too-many-return-statements def _ob(self, observable, wires_map: dict = None): """Serialize a :class:`pennylane.operation.Observable` into an Observable.""" - if isinstance(observable, Tensor): + if isinstance(observable, (Prod, Sum, SProd)) and observable.pauli_rep is not None: + return self._pauli_sentence(observable.pauli_rep, wires_map) + if isinstance(observable, Tensor) or ( + isinstance(observable, Prod) and not observable.has_overlapping_wires + ): return self._tensor_ob(observable, wires_map) - if observable.name == "Hamiltonian": + if observable.name in ("Hamiltonian", "LinearCombination"): return self._hamiltonian(observable, wires_map) if observable.name == "SparseHamiltonian": return self._sparse_hamiltonian(observable, wires_map) if isinstance(observable, (PauliX, PauliY, PauliZ, Identity, Hadamard)): return self._named_obs(observable, wires_map) - if observable._pauli_rep is not None: - return self._pauli_sentence(observable._pauli_rep, wires_map) + if observable.pauli_rep is not None: + return self._pauli_sentence(observable.pauli_rep, wires_map) + # if isinstance(observable, (Prod, Sum)): + # return self._hamiltonian(observable, wires_map) return self._hermitian_ob(observable, wires_map) def serialize_observables(self, tape: QuantumTape, wires_map: dict = None) -> List: @@ -282,7 +296,8 @@ def serialize_observables(self, tape: QuantumTape, wires_map: dict = None) -> Li Returns: list(ObsStructC128 or ObsStructC64): A list of observable objects compatible with - the C++ backend + the C++ backend. For unsupported observables, the observable matrix is used + to create a :class:`~pennylane.Hermitian` to be used for serialization. """ serialized_obs = [] diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index ec9ead0f50..0ae493ae06 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-dev18" +__version__ = "0.36.0-dev19" diff --git a/pennylane_lightning/core/lightning_base.py b/pennylane_lightning/core/lightning_base.py index 650fb0ed84..c4f72293a0 100644 --- a/pennylane_lightning/core/lightning_base.py +++ b/pennylane_lightning/core/lightning_base.py @@ -23,8 +23,9 @@ import pennylane as qml from pennylane import BasisState, QubitDevice, StatePrep from pennylane.devices import DefaultQubitLegacy -from pennylane.measurements import MeasurementProcess -from pennylane.operation import Operation +from pennylane.measurements import Expectation, MeasurementProcess, State +from pennylane.operation import Operation, Tensor +from pennylane.ops import Prod, Projector, SProd, Sum from pennylane.wires import Wires from ._serialize import QuantumScriptSerializer @@ -306,18 +307,69 @@ def _process_jacobian_tape( "obs_idx_offsets": obs_idx_offsets, } + @staticmethod + def _assert_adjdiff_no_projectors(observable): + """Helper function to validate that an observable is not or does not contain + Projectors + + Args: + observable (~pennylane.operation.Operator): Observable to check + + Raises: + ~pennylane.QuantumFunctionError: if a ``Projector`` is found. + """ + if isinstance(observable, Tensor): + if any(isinstance(o, Projector) for o in observable.non_identity_obs): + raise qml.QuantumFunctionError( + "Adjoint differentiation method does not support the Projector observable" + ) + + elif isinstance(observable, Projector): + raise qml.QuantumFunctionError( + "Adjoint differentiation method does not support the Projector observable" + ) + + elif isinstance(observable, SProd): + LightningBase._assert_adjdiff_no_projectors(observable.base) + + elif isinstance(observable, (Sum, Prod)): + for obs in observable: + LightningBase._assert_adjdiff_no_projectors(obs) + # pylint: disable=unnecessary-pass @staticmethod def _check_adjdiff_supported_measurements(measurements: List[MeasurementProcess]): - """Check whether given list of measurement is supported by adjoint_differentiation. + """Check whether given list of measurements is supported by adjoint_differentiation. Args: measurements (List[MeasurementProcess]): a list of measurement processes to check. Returns: Expectation or State: a common return type of measurements. + + Raises: + ~pennylane.QuantumFunctionError: if a measurement is unsupported with adjoint + differentiation. """ - pass + if not measurements: + return None + + if len(measurements) == 1 and measurements[0].return_type is State: + # return State + raise qml.QuantumFunctionError( + "Adjoint differentiation does not support State measurements." + ) + + # The return_type of measurement processes must be expectation + if any(m.return_type is not Expectation for m in measurements): + raise qml.QuantumFunctionError( + "Adjoint differentiation method does not support expectation return type " + "mixed with other return types" + ) + + for measurement in measurements: + LightningBase._assert_adjdiff_no_projectors(measurement.obs) + return Expectation @staticmethod def _adjoint_jacobian_processing(jac): diff --git a/pennylane_lightning/core/src/bindings/Bindings.hpp b/pennylane_lightning/core/src/bindings/Bindings.hpp index 9f06001d54..39170e4af2 100644 --- a/pennylane_lightning/core/src/bindings/Bindings.hpp +++ b/pennylane_lightning/core/src/bindings/Bindings.hpp @@ -350,6 +350,8 @@ void registerBackendAgnosticObservables(py::module_ &m) { .def("__repr__", &HermitianObs::getObsName) .def("get_wires", &HermitianObs::getWires, "Get wires of observables") + .def("get_matrix", &HermitianObs::getMatrix, + "Get matrix representation of Hermitian operator") .def( "__eq__", [](const HermitianObs &self, @@ -373,6 +375,8 @@ void registerBackendAgnosticObservables(py::module_ &m) { .def("__repr__", &TensorProdObs::getObsName) .def("get_wires", &TensorProdObs::getWires, "Get wires of observables") + .def("get_ops", &TensorProdObs::getObs, + "Get operations list") .def( "__eq__", [](const TensorProdObs &self, @@ -401,6 +405,10 @@ void registerBackendAgnosticObservables(py::module_ &m) { .def("__repr__", &Hamiltonian::getObsName) .def("get_wires", &Hamiltonian::getWires, "Get wires of observables") + .def("get_ops", &Hamiltonian::getObs, + "Get operations contained by Hamiltonian") + .def("get_coeffs", &Hamiltonian::getCoeffs, + "Get Hamiltonian coefficients") .def( "__eq__", [](const Hamiltonian &self, diff --git a/pennylane_lightning/lightning_gpu/lightning_gpu.py b/pennylane_lightning/lightning_gpu/lightning_gpu.py index 66047852a7..c1a0fd605e 100644 --- a/pennylane_lightning/lightning_gpu/lightning_gpu.py +++ b/pennylane_lightning/lightning_gpu/lightning_gpu.py @@ -77,17 +77,8 @@ from typing import List, Union import pennylane as qml - from pennylane import ( - BasisState, - DeviceError, - Projector, - QuantumFunctionError, - Rot, - StatePrep, - math, - ) - from pennylane.measurements import Expectation, MeasurementProcess, State - from pennylane.operation import Tensor + from pennylane import BasisState, DeviceError, QuantumFunctionError, Rot, StatePrep, math + from pennylane.measurements import Expectation, State from pennylane.ops.op_math import Adjoint from pennylane.wires import Wires @@ -196,6 +187,7 @@ def _mebibytesToBytes(mebibytes): "Hadamard", "SparseHamiltonian", "Hamiltonian", + "LinearCombination", "Hermitian", "Identity", "Sum", @@ -583,43 +575,6 @@ def apply(self, operations, rotations=None, **kwargs): self.apply_lightning(operations) - @staticmethod - def _check_adjdiff_supported_measurements(measurements: List[MeasurementProcess]): - """Check whether given list of measurement is supported by adjoint_diff. - Args: - measurements (List[MeasurementProcess]): a list of measurement processes to check. - Returns: - Expectation or State: a common return type of measurements. - """ - if not measurements: - return None - - if len(measurements) == 1 and measurements[0].return_type is State: - # return State - raise QuantumFunctionError( - "Adjoint differentiation does not support State measurements." - ) - - # The return_type of measurement processes must be expectation - if any(m.return_type is not Expectation for m in measurements): - raise QuantumFunctionError( - "Adjoint differentiation method does not support expectation return type " - "mixed with other return types" - ) - - for measurement in measurements: - if isinstance(measurement.obs, Tensor): - if any(isinstance(o, Projector) for o in measurement.obs.non_identity_obs): - raise QuantumFunctionError( - "Adjoint differentiation method does not support the " - "Projector observable" - ) - elif isinstance(measurement.obs, Projector): - raise QuantumFunctionError( - "Adjoint differentiation method does not support the Projector observable" - ) - return Expectation - @staticmethod def _check_adjdiff_supported_operations(operations): """Check Lightning adjoint differentiation method support for a tape. diff --git a/pennylane_lightning/lightning_kokkos/lightning_kokkos.py b/pennylane_lightning/lightning_kokkos/lightning_kokkos.py index c15c3583a9..f330ff69fa 100644 --- a/pennylane_lightning/lightning_kokkos/lightning_kokkos.py +++ b/pennylane_lightning/lightning_kokkos/lightning_kokkos.py @@ -50,17 +50,8 @@ from typing import List import pennylane as qml - from pennylane import ( - BasisState, - DeviceError, - Projector, - QuantumFunctionError, - Rot, - StatePrep, - math, - ) - from pennylane.measurements import Expectation, MeasurementProcess, State - from pennylane.operation import Tensor + from pennylane import BasisState, DeviceError, QuantumFunctionError, Rot, StatePrep, math + from pennylane.measurements import Expectation, State from pennylane.ops.op_math import Adjoint from pennylane.wires import Wires @@ -157,6 +148,7 @@ def _kokkos_configuration(): "Projector", "SparseHamiltonian", "Hamiltonian", + "LinearCombination", "Sum", "SProd", "Prod", @@ -624,47 +616,6 @@ def sample(self, observable, shot_range=None, bin_size=None, counts=False): self.apply([qml.adjoint(g, lazy=False) for g in reversed(diagonalizing_gates)]) return results - @staticmethod - def _check_adjdiff_supported_measurements( - measurements: List[MeasurementProcess], - ): - """Check whether given list of measurement is supported by adjoint_differentiation. - - Args: - measurements (List[MeasurementProcess]): a list of measurement processes to check. - - Returns: - Expectation or State: a common return type of measurements. - """ - if not measurements: - return None - - if len(measurements) == 1 and measurements[0].return_type is State: - # return State - raise QuantumFunctionError( - "Adjoint differentiation does not support State measurements." - ) - - # Now the return_type of measurement processes must be expectation - if any(m.return_type is not Expectation for m in measurements): - raise QuantumFunctionError( - "Adjoint differentiation method does not support expectation return type " - "mixed with other return types" - ) - - for measurement in measurements: - if isinstance(measurement.obs, Tensor): - if any(isinstance(o, Projector) for o in measurement.obs.non_identity_obs): - raise QuantumFunctionError( - "Adjoint differentiation method does not support the " - "Projector observable" - ) - elif isinstance(measurement.obs, Projector): - raise QuantumFunctionError( - "Adjoint differentiation method does not support the Projector observable" - ) - return Expectation - @staticmethod def _check_adjdiff_supported_operations(operations): """Check Lightning adjoint differentiation method support for a tape. diff --git a/pennylane_lightning/lightning_qubit/lightning_qubit.py b/pennylane_lightning/lightning_qubit/lightning_qubit.py index 53df30e45d..e507f37642 100644 --- a/pennylane_lightning/lightning_qubit/lightning_qubit.py +++ b/pennylane_lightning/lightning_qubit/lightning_qubit.py @@ -33,6 +33,8 @@ validate_observables, ) from pennylane.measurements import MidMeasureMP +from pennylane.operation import Tensor +from pennylane.ops import Prod, SProd, Sum from pennylane.tape import QuantumScript, QuantumTape from pennylane.transforms.core import TransformProgram from pennylane.typing import Result, ResultBatch @@ -230,6 +232,7 @@ def simulate_and_jacobian(circuit: QuantumTape, state: LightningStateVector, bat "Projector", "SparseHamiltonian", "Hamiltonian", + "LinearCombination", "Sum", "SProd", "Prod", @@ -249,6 +252,26 @@ def accepted_observables(obs: qml.operation.Operator) -> bool: return obs.name in _observables +def adjoint_observables(obs: qml.operation.Operator) -> bool: + """A function that determines whether or not an observable is supported by ``lightning.qubit`` + when using the adjoint differentiation method.""" + if isinstance(obs, qml.Projector): + return False + + if isinstance(obs, Tensor): + if any(isinstance(o, qml.Projector) for o in obs.non_identity_obs): + return False + return True + + if isinstance(obs, SProd): + return adjoint_observables(obs.base) + + if isinstance(obs, (Sum, Prod)): + return all(adjoint_observables(o) for o in obs) + + 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) diff --git a/requirements-dev.txt b/requirements-dev.txt index 02aa0c47f9..474ef50d1f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,6 +14,6 @@ clang-tidy~=16.0 clang-format~=16.0 isort==5.13.2 click==8.0.4 -cmake +cmake==3.28.4 custatevec-cu12 pylint diff --git a/tests/conftest.py b/tests/conftest.py index b0f202ac01..436dbe84ce 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -152,6 +152,34 @@ def _device(wires, shots=None): return _device +####################################################################### +# Fixtures for testing under new and old opmath + + +@pytest.fixture(scope="function") +def use_legacy_opmath(): + with qml.operation.disable_new_opmath_cm() as cm: + yield cm + + +@pytest.fixture(scope="function") +def use_new_opmath(): + with qml.operation.enable_new_opmath_cm() as cm: + yield cm + + +@pytest.fixture( + params=[qml.operation.disable_new_opmath_cm, qml.operation.enable_new_opmath_cm], + scope="function", +) +def use_legacy_and_new_opmath(request): + with request.param() as cm: + yield cm + + +####################################################################### + + def validate_counts(shots, results1, results2): """Compares two counts. diff --git a/tests/lightning_qubit/test_adjoint_jacobian_class.py b/tests/lightning_qubit/test_adjoint_jacobian_class.py index 5450364b32..7865a681af 100644 --- a/tests/lightning_qubit/test_adjoint_jacobian_class.py +++ b/tests/lightning_qubit/test_adjoint_jacobian_class.py @@ -250,6 +250,7 @@ def test_multiple_rx_gradient_expval_hermitian(self, tol, lightning_sv): assert np.allclose(expected, result, atol=tol, rtol=0) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_multiple_rx_gradient_expval_hamiltonian(self, tol, lightning_sv): """Tests that the gradient of multiple RX gates in a circuit yields the correct result with Hermitian observable @@ -360,6 +361,7 @@ def calculate_vjp(statevector, tape, vector): return LightningAdjointJacobian(statevector).calculate_vjp(tape, vector) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_multiple_measurements(self, tol, lightning_sv): """Tests provides correct answer when provided multiple measurements.""" x, y, z = [0.5, 0.3, -0.7] diff --git a/tests/lightning_qubit/test_measurements_class.py b/tests/lightning_qubit/test_measurements_class.py index 41e5be61e4..bd070629f8 100644 --- a/tests/lightning_qubit/test_measurements_class.py +++ b/tests/lightning_qubit/test_measurements_class.py @@ -418,6 +418,7 @@ def calculate_reference(tape, lightning_sv): m = LightningMeasurements(statevector) return m.measure_final_state(tape) + @flaky(max_runs=5) @pytest.mark.parametrize("measurement", [qml.expval, qml.probs, qml.var]) @pytest.mark.parametrize( "observable", diff --git a/tests/new_api/test_device.py b/tests/new_api/test_device.py index accb8d17c7..a7fe0ac7d3 100644 --- a/tests/new_api/test_device.py +++ b/tests/new_api/test_device.py @@ -29,6 +29,7 @@ _supports_adjoint, accepted_observables, adjoint_measurements, + adjoint_observables, decompose, no_sampling, stopping_condition, @@ -76,6 +77,27 @@ def test_accepted_observables(self): assert accepted_observables(valid_obs) is True assert accepted_observables(invalid_obs) is False + @pytest.mark.parametrize( + "obs, expected", + [ + (qml.operation.Tensor(qml.Projector([0], 0), qml.PauliZ(1)), False), + (qml.prod(qml.Projector([0], 0), qml.PauliZ(1)), False), + (qml.s_prod(1.5, qml.Projector([0], 0)), False), + (qml.sum(qml.Projector([0], 0), qml.Hadamard(1)), False), + (qml.sum(qml.prod(qml.Projector([0], 0), qml.Y(1)), qml.PauliX(1)), False), + (qml.operation.Tensor(qml.Y(0), qml.Z(1)), True), + (qml.prod(qml.Y(0), qml.PauliZ(1)), True), + (qml.s_prod(1.5, qml.Y(1)), True), + (qml.sum(qml.Y(1), qml.Hadamard(1)), True), + (qml.X(0), True), + (qml.Hermitian(np.eye(4), [0, 1]), True), + ], + ) + def test_adjoint_observables(self, obs, expected): + """Test that adjoint_observables returns the expected boolean result for + a given observable""" + assert adjoint_observables(obs) == expected + 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() @@ -265,6 +287,7 @@ def test_preprocess(self, adjoint): actual_program, _ = device.preprocess(config) assert actual_program == expected_program + @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) @pytest.mark.parametrize( "mp", @@ -273,11 +296,11 @@ def test_preprocess(self, adjoint): 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.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(2.5 * qml.Z(0)), + qml.expval(qml.Z(0) @ qml.X(1)), + qml.expval(qml.operation.Tensor(qml.Z(0), qml.X(1))), qml.expval( qml.SparseHamiltonian( qml.Hamiltonian([-1.0, 1.5], [qml.Z(1), qml.X(1)]).sparse_matrix( @@ -291,6 +314,9 @@ def test_preprocess(self, adjoint): ) def test_execute_single_measurement(self, theta, phi, mp, dev): """Test that execute returns the correct results with a single measurement.""" + if isinstance(mp.obs, qml.ops.LinearCombination) and not qml.operation.active_new_opmath(): + mp.obs = qml.operation.convert_to_legacy_H(mp.obs) + qs = QuantumScript( [ qml.RX(phi, 0), @@ -304,6 +330,7 @@ def test_execute_single_measurement(self, theta, phi, mp, dev): expected = self.calculate_reference(qs)[0] assert np.allclose(res, expected) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) @pytest.mark.parametrize( "mp1", @@ -325,6 +352,9 @@ def test_execute_single_measurement(self, theta, phi, mp, dev): ) def test_execute_multi_measurement(self, theta, phi, dev, mp1, mp2): """Test that execute returns the correct results with multiple measurements.""" + if isinstance(mp2.obs, qml.ops.LinearCombination) and not qml.operation.active_new_opmath(): + mp2.obs = qml.operation.convert_to_legacy_H(mp2.obs) + qs = QuantumScript( [ qml.RX(phi, 0), @@ -436,18 +466,19 @@ 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.usefixtures("use_legacy_and_new_opmath") @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)), + 2.5 * qml.Z(0), + qml.Z(0) @ qml.X(1), + qml.operation.Tensor(qml.Z(0), qml.X(1)), + 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]) @@ -455,6 +486,9 @@ 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""" + if isinstance(obs, qml.ops.LinearCombination) and not qml.operation.active_new_opmath(): + obs = qml.operation.convert_to_legacy_H(obs) + qs = QuantumScript( [qml.RX(theta, 0), qml.CNOT([0, 1]), qml.RY(phi, 1)], [qml.expval(obs)], @@ -508,6 +542,11 @@ 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""" + if isinstance(obs1, qml.ops.LinearCombination) and not qml.operation.active_new_opmath(): + obs1 = qml.operation.convert_to_legacy_H(obs1) + if isinstance(obs2, qml.ops.LinearCombination) and not qml.operation.active_new_opmath(): + obs2 = qml.operation.convert_to_legacy_H(obs2) + qs = QuantumScript( [ qml.RX(theta, 0), diff --git a/tests/test_adjoint_jacobian.py b/tests/test_adjoint_jacobian.py index 4efed12878..5e246b9f10 100644 --- a/tests/test_adjoint_jacobian.py +++ b/tests/test_adjoint_jacobian.py @@ -198,6 +198,7 @@ def test_unsupported_op(self, dev): ): dev.adjoint_jacobian(tape) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") @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): @@ -215,7 +216,7 @@ def test_proj_unsupported(self, dev): with qml.tape.QuantumTape() as tape: qml.CRX(0.1, wires=[0, 1]) - qml.expval(qml.Projector([0], wires=[0]) @ qml.PauliZ(0)) + qml.expval(qml.Projector([0], wires=[0]) @ qml.PauliZ(1)) with pytest.raises( qml.QuantumFunctionError, match="differentiation method does not support the Projector" @@ -401,6 +402,7 @@ def test_multiple_rx_gradient_expval_hermitian(self, tol, dev): qubit_ops = [getattr(qml, name) for name in qml.ops._qubit__ops__] ops = {qml.RX, qml.RY, qml.RZ, qml.PhaseShift, qml.CRX, qml.CRY, qml.CRZ, qml.Rot} + @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_multiple_rx_gradient_expval_hamiltonian(self, tol, dev): """Tests that the gradient of multiple RX gates in a circuit yields the correct result @@ -579,6 +581,7 @@ def test_gradient_gate_with_multiple_parameters_hermitian(self, dev): # the different methods agree assert np.allclose(grad_D, grad_F, atol=tol, rtol=0) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_gradient_gate_with_multiple_parameters_hamiltonian(self, dev): """Tests that gates with multiple free parameters yield correct gradients.""" @@ -1130,10 +1133,8 @@ def circuit_ansatz(params, wires): qml.RX(params[29], wires=wires[1]) -@pytest.mark.skipif( - device_name != "lightning.gpu" or not ld._CPP_BINARY_AVAILABLE, - reason="Lightning binary required", -) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") +@pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_tape_qchem(tol): """Tests the circuit Ansatz with a QChem Hamiltonian produces correct results""" @@ -1156,6 +1157,7 @@ def circuit(params): assert np.allclose(qml.grad(circuit_ld)(params), qml.grad(circuit_dq)(params), tol) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") def test_tape_qchem_sparse(tol): """Tests the circuit Ansatz with a QChem Hamiltonian produces correct results""" @@ -1501,14 +1503,8 @@ def create_xyz_file(tmp_path_factory): yield file -@pytest.mark.skipif( - not ld._CPP_BINARY_AVAILABLE, - reason="Tests only for lightning.gpu", -) -@pytest.mark.parametrize( - "batches", - [False, True, 1, 2, 3, 4], -) +@pytest.mark.skipif(not ld._CPP_BINARY_AVAILABLE, reason="Lightning binary required") +@pytest.mark.parametrize("batches", [False, True, 1, 2, 3, 4]) def test_integration_H2_Hamiltonian(create_xyz_file, batches): _ = pytest.importorskip("openfermionpyscf") n_electrons = 2 diff --git a/tests/test_execute.py b/tests/test_execute.py index 631ecc5b65..82f3c52a78 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -25,6 +25,7 @@ pytest.skip("No binary module found. Skipping.", allow_module_level=True) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("diff_method", ("param_shift", "finite_diff")) class TestQChem: """Test tapes returning the expectation values of a Hamiltonian, with a qchem workflow.""" diff --git a/tests/test_measurements.py b/tests/test_measurements.py index a1bfa173c4..181271f97d 100644 --- a/tests/test_measurements.py +++ b/tests/test_measurements.py @@ -298,6 +298,7 @@ def circuit(): assert np.allclose(circuit(), cases[1], atol=tol, rtol=0) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( "obs, coeffs, res", [ @@ -323,6 +324,11 @@ def circuit(): ) def test_expval_hamiltonian(self, obs, coeffs, res, tol, dev): """Test expval with Hamiltonian""" + if not qml.operation.active_new_opmath(): + obs = [ + qml.operation.convert_to_legacy_H(o).ops[0] if isinstance(o, qml.ops.Prod) else o + for o in obs + ] ham = qml.Hamiltonian(coeffs, obs) @qml.qnode(dev) diff --git a/tests/test_serialize.py b/tests/test_serialize.py index b56ac4ade7..efedf4969f 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -72,16 +72,20 @@ def test_wrong_device_name(): QuantumScriptSerializer("thunder.qubit") +@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( "obs,obs_type", [ (qml.PauliZ(0), NamedObsC128), - (qml.PauliZ(0) @ qml.PauliZ(1), TensorProdObsC128), + ( + qml.PauliZ(0) @ qml.PauliZ(1), + HamiltonianC128 if qml.operation.active_new_opmath() else TensorProdObsC128, + ), (qml.Hadamard(0), NamedObsC128), (qml.Hermitian(np.eye(2), wires=0), HermitianObsC128), ( qml.PauliZ(0) @ qml.Hadamard(1) @ (0.1 * (qml.PauliZ(2) + qml.PauliX(3))), - HamiltonianC128, + TensorProdObsC128 if qml.operation.active_new_opmath() else HamiltonianC128, ), ( ( @@ -112,6 +116,7 @@ def test_obs_returns_expected_type(obs, obs_type): assert isinstance(QuantumScriptSerializer(device_name)._ob(obs), obs_type) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") class TestSerializeObs: """Tests for the _observables function""" @@ -126,11 +131,17 @@ def test_tensor_non_tensor_return(self, use_csingle, wires_map): qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) qml.expval(qml.Hadamard(1)) - tensor_prod_obs = TensorProdObsC64 if use_csingle else TensorProdObsC128 named_obs = NamedObsC64 if use_csingle else NamedObsC128 + tensor_prod_obs = TensorProdObsC64 if use_csingle else TensorProdObsC128 + first_s = tensor_prod_obs([named_obs("PauliZ", [0]), named_obs("PauliX", [1])]) + + if qml.operation.active_new_opmath(): + ham_obs = HamiltonianC64 if use_csingle else HamiltonianC128 + tensor_obs = tensor_prod_obs([named_obs("PauliX", [1]), named_obs("PauliZ", [0])]) + first_s = ham_obs([1.0], [tensor_obs]) s_expected = [ - tensor_prod_obs([named_obs("PauliZ", [0]), named_obs("PauliX", [1])]), + first_s, named_obs("Hadamard", [1]), ] @@ -139,6 +150,26 @@ def test_tensor_non_tensor_return(self, use_csingle, wires_map): ) assert s == s_expected + @pytest.mark.parametrize("use_csingle", [True, False]) + @pytest.mark.parametrize("wires_map", [wires_dict, None]) + def test_prod_return_with_overlapping_wires(self, use_csingle, wires_map): + """Test the expected serialization for a Prod return with operands with overlapping wires.""" + obs = qml.prod( + qml.sum(qml.X(0), qml.s_prod(2, qml.Hadamard(0))), + qml.sum(qml.s_prod(3, qml.Z(1)), qml.Z(2), qml.Hermitian(np.eye(2), wires=0)), + ) + tape = qml.tape.QuantumScript([], [qml.expval(obs)]) + + hermitian_obs = HermitianObsC64 if use_csingle else HermitianObsC128 + c_dtype = np.complex64 if use_csingle else np.complex128 + mat = obs.matrix().ravel().astype(c_dtype) + + s, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( + tape, wires_map + ) + s_expected = hermitian_obs(mat, [0, 1, 2]) + assert s[0] == s_expected + @pytest.mark.parametrize("use_csingle", [True, False]) @pytest.mark.parametrize("wires_map", [wires_dict, None]) def test_hermitian_return(self, use_csingle, wires_map): @@ -243,7 +274,14 @@ def test_hamiltonian_return(self, use_csingle, wires_map): named_obs("PauliY", [2]), ] ), - tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]), + ( + hamiltonian_obs( + np.array([1], dtype=r_dtype), + [tensor_prod_obs([named_obs("PauliY", [2]), named_obs("PauliX", [0])])], + ) + if qml.operation.active_new_opmath() + else tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]) + ), hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), ], ) @@ -253,7 +291,7 @@ def test_hamiltonian_return(self, use_csingle, wires_map): @pytest.mark.parametrize("use_csingle", [True, False]) @pytest.mark.parametrize("wires_map", [wires_dict, None]) def test_hamiltonian_tensor_return(self, use_csingle, wires_map): - """Test expected serialization for a Hamiltonian return""" + """Test expected serialization for a tensor Hamiltonian return""" with qml.tape.QuantumTape() as tape: ham = qml.Hamiltonian( @@ -279,24 +317,67 @@ def test_hamiltonian_tensor_return(self, use_csingle, wires_map): # Expression (ham @ obs) is converted internally by Pennylane # where obs is appended to each term of the ham - s_expected = hamiltonian_obs( - np.array([0.3, 0.5, 0.4], dtype=r_dtype), - [ - tensor_prod_obs( - [ - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), - named_obs("PauliY", [2]), - named_obs("PauliZ", [3]), - ] - ), - tensor_prod_obs( - [named_obs("PauliX", [0]), named_obs("PauliY", [2]), named_obs("PauliZ", [3])] - ), - tensor_prod_obs( - [hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), named_obs("PauliZ", [3])] - ), - ], - ) + if qml.operation.active_new_opmath(): + s_expected = hamiltonian_obs( + np.array([0.3, 0.5, 0.4], dtype=r_dtype), + [ + tensor_prod_obs( + [ + tensor_prod_obs( + [ + hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), + named_obs("PauliY", [2]), + ] + ), + named_obs("PauliZ", [3]), + ] + ), + hamiltonian_obs( + np.array([1], dtype=r_dtype), + [ + tensor_prod_obs( + [ + named_obs("PauliY", [2]), + named_obs("PauliX", [0]), + named_obs("PauliZ", [3]), + ] + ) + ], + ), + tensor_prod_obs( + [ + hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), + named_obs("PauliZ", [3]), + ] + ), + ], + ) + else: + s_expected = hamiltonian_obs( + np.array([0.3, 0.5, 0.4], dtype=r_dtype), + [ + tensor_prod_obs( + [ + hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), + named_obs("PauliY", [2]), + named_obs("PauliZ", [3]), + ] + ), + tensor_prod_obs( + [ + named_obs("PauliX", [0]), + named_obs("PauliY", [2]), + named_obs("PauliZ", [3]), + ] + ), + tensor_prod_obs( + [ + hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), + named_obs("PauliZ", [3]), + ] + ), + ], + ) assert s[0] == s_expected @@ -332,32 +413,72 @@ def test_hamiltonian_mix_return(self, use_csingle, wires_map): s, _ = QuantumScriptSerializer(device_name, use_csingle).serialize_observables( tape, wires_map ) - - s_expected1 = hamiltonian_obs( - np.array([0.3, 0.5, 0.4], dtype=r_dtype), - [ - tensor_prod_obs( - [ - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), - named_obs("PauliY", [2]), - ] - ), - tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]), - hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), - ], - ) - s_expected2 = hamiltonian_obs( - np.array([0.7, 0.3], dtype=r_dtype), - [ - tensor_prod_obs( - [ - named_obs("PauliX", [0]), - hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [1, 2]), - ] - ), - tensor_prod_obs([named_obs("PauliY", [0]), named_obs("PauliX", [2])]), - ], - ) + if qml.operation.active_new_opmath(): + s_expected1 = hamiltonian_obs( + np.array([0.3, 0.5, 0.4], dtype=r_dtype), + [ + tensor_prod_obs( + [ + hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), + named_obs("PauliY", [2]), + ] + ), + ( + hamiltonian_obs( + np.array([1], dtype=r_dtype), + [tensor_prod_obs([named_obs("PauliY", [2]), named_obs("PauliX", [0])])], + ) + if qml.operation.active_new_opmath() + else tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]) + ), + hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), + ], + ) + s_expected2 = hamiltonian_obs( + np.array([0.7, 0.3], dtype=r_dtype), + [ + tensor_prod_obs( + [ + named_obs("PauliX", [0]), + hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [1, 2]), + ] + ), + ( + hamiltonian_obs( + np.array([1], dtype=r_dtype), + [tensor_prod_obs([named_obs("PauliX", [2]), named_obs("PauliY", [0])])], + ) + if qml.operation.active_new_opmath() + else tensor_prod_obs([named_obs("PauliY", [0]), named_obs("PauliX", [2])]) + ), + ], + ) + else: + s_expected1 = hamiltonian_obs( + np.array([0.3, 0.5, 0.4], dtype=r_dtype), + [ + tensor_prod_obs( + [ + hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [0, 1]), + named_obs("PauliY", [2]), + ] + ), + tensor_prod_obs([named_obs("PauliX", [0]), named_obs("PauliY", [2])]), + hermitian_obs(np.ones(64, dtype=c_dtype), [0, 1, 2]), + ], + ) + s_expected2 = hamiltonian_obs( + np.array([0.7, 0.3], dtype=r_dtype), + [ + tensor_prod_obs( + [ + named_obs("PauliX", [0]), + hermitian_obs(np.eye(4, dtype=c_dtype).ravel(), [1, 2]), + ] + ), + tensor_prod_obs([named_obs("PauliY", [0]), named_obs("PauliX", [2])]), + ], + ) assert s[0] == s_expected1 assert s[1] == s_expected2 diff --git a/tests/test_vjp.py b/tests/test_vjp.py index 70bd091bf9..8d2b3765cb 100644 --- a/tests/test_vjp.py +++ b/tests/test_vjp.py @@ -186,6 +186,7 @@ def test_unsupported_op(self, dev): ): dev.vjp(tape.measurements, dy)(tape) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_proj_unsupported(self, dev): """Test if a QuantumFunctionError is raised for a Projector observable""" @@ -203,7 +204,7 @@ def test_proj_unsupported(self, dev): with qml.tape.QuantumTape() as tape: qml.CRX(0.1, wires=[0, 1]) - qml.expval(qml.Projector([0], wires=[0]) @ qml.PauliZ(0)) + qml.expval(qml.Projector([0], wires=[0]) @ qml.PauliZ(1)) with pytest.raises( qml.QuantumFunctionError,