From 93e9667c263059bd8235d60de88913ffa893cd9d Mon Sep 17 00:00:00 2001 From: Andrija Paurevic <46359773+andrijapau@users.noreply.github.com> Date: Fri, 15 Nov 2024 16:06:33 -0500 Subject: [PATCH 01/35] Fix coverage due to removal of `max_expansion` in `clifford_t_decomposition.py` (#6571) **Context:** #6531 was accidentally merged into master with a net negative hit to the project code coverage. This PR is designed to reconcile that impact and clean-up the coverage. **Description of the Change:** The following logic branches were removed because they are not possible to reach given the deprecation/removal of `expand_depth` in `compile.py`. For the sake of record keeping, comments by me are attached to each logic branch indicating why it was removed. **Benefits:** Restore original project code coverage. **Possible Drawbacks:** None that I know of. [sc-77499] --- doc/releases/changelog-dev.md | 1 + .../decompositions/clifford_t_transform.py | 43 ++------------ tests/transforms/test_cliffordt_transform.py | 59 +++++++++++++++---- 3 files changed, 54 insertions(+), 49 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index faa9cf7e27d..164775e1d26 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -97,6 +97,7 @@ * The `max_expansion` argument for `qml.transforms.clifford_t_decomposition` has been removed. [(#6531)](https://github.com/PennyLaneAI/pennylane/pull/6531) + [(#6571)](https://github.com/PennyLaneAI/pennylane/pull/6571) * The `expand_depth` argument for `qml.compile` has been removed. [(#6531)](https://github.com/PennyLaneAI/pennylane/pull/6531) diff --git a/pennylane/transforms/decompositions/clifford_t_transform.py b/pennylane/transforms/decompositions/clifford_t_transform.py index 06e656a0af5..85e4946e6e9 100644 --- a/pennylane/transforms/decompositions/clifford_t_transform.py +++ b/pennylane/transforms/decompositions/clifford_t_transform.py @@ -371,7 +371,7 @@ def circuit(x, y): with QueuingManager.stop_recording(): # Build the basis set and the pipeline for initial compilation pass - basis_set = [op.__name__ for op in _PARAMETER_GATES + _CLIFFORD_T_GATES] + basis_set = [op.__name__ for op in _PARAMETER_GATES + _CLIFFORD_T_GATES + _SKIP_OP_TYPES] pipelines = [remove_barrier, commute_controlled, cancel_inverses, merge_rotations] # Compile the tape according to depth provided by the user and expand it @@ -411,44 +411,11 @@ def circuit(x, y): d_ops = _two_qubit_decompose(op) decomp_ops.extend(d_ops) - # For special multi-qubit gates and ones constructed from matrix + # If we don't know how to decompose the operation else: - try: - # Attempt decomposing the operation - md_ops = op.decomposition() - idx = 0 # might not be fast but at least is not recursive - while idx < len(md_ops): - md_op = md_ops[idx] - if md_op.name not in basis_set or not check_clifford_t(md_op): - # For the gates acting on one qubit - if len(md_op.wires) == 1: - if md_op.name in basis_set: # For known recipe - d_ops = _rot_decompose(md_op) - else: # Resort to decomposing manually - d_ops, g_op = _one_qubit_decompose(md_op) - gphase_ops.append(g_op) - - # For the gates acting on two qubits - elif len(md_op.wires) == 2: - # Resort to decomposing manually - d_ops = _two_qubit_decompose(md_op) - - # Final resort (should not enter in an ideal situation) - else: - d_ops = md_op.decomposition() - - # Expand the list and iterate over - del md_ops[idx] - md_ops[idx:idx] = d_ops - idx += 1 - - decomp_ops.extend(md_ops) - - # If we don't know how to decompose the operation - except Exception as exc: - raise ValueError( - f"Cannot unroll {op} into the Clifford+T basis as no rule exists for its decomposition" - ) from exc + raise ValueError( + f"Cannot unroll {op} into the Clifford+T basis as no rule exists for its decomposition" + ) # Merge RZ rotations together merged_ops, number_ops = _merge_param_gates(decomp_ops, merge_ops=["RZ"]) diff --git a/tests/transforms/test_cliffordt_transform.py b/tests/transforms/test_cliffordt_transform.py index db3dc9cbca1..1eb6119f4b2 100644 --- a/tests/transforms/test_cliffordt_transform.py +++ b/tests/transforms/test_cliffordt_transform.py @@ -37,6 +37,24 @@ PI = math.pi +# pylint: disable=too-few-public-methods +class CustomOneQubitOperation(qml.operation.Operation): + num_wires = 1 + + @staticmethod + def compute_matrix(): + return qml.math.conj(qml.math.transpose(qml.S.compute_matrix())) + + +# pylint: disable=too-few-public-methods +class CustomTwoQubitOperation(qml.operation.Operation): + num_wires = 2 + + @staticmethod + def compute_matrix(): + return qml.math.conj(qml.math.transpose(qml.CNOT.compute_matrix())) + + def circuit_1(): """Circuit 1 with quantum chemistry gates""" qml.RZ(1.0, wires=[0]) @@ -81,13 +99,6 @@ def circuit_5(): return qml.expval(qml.PauliZ(0)) -def circuit_6(): - """Circuit 6 with skippable operations""" - qml.RZ(1.0, wires=[0]) - qml.Barrier(wires=0) - return qml.expval(qml.PauliZ(0)) - - class TestCliffordCompile: """Unit tests for clifford compilation function.""" @@ -111,7 +122,7 @@ def test_clifford_checker(self, op, res): @pytest.mark.parametrize( "circuit", - [circuit_1, circuit_2, circuit_3, circuit_4, circuit_5, circuit_6], + [circuit_1, circuit_2, circuit_3, circuit_4, circuit_5], ) def test_decomposition(self, circuit): """Test decomposition for the Clifford transform.""" @@ -181,9 +192,7 @@ def test_total_error(self, epsilon, circuit): @pytest.mark.parametrize( "op", - [ - qml.RY(qml.numpy.pi / 4, wires=0), - ], + [CustomOneQubitOperation(wires=0)], ) def test_zxz_rotation_decomposition(self, op): """Test single-qubit gates are decomposed correctly using ZXZ rotations""" @@ -209,6 +218,34 @@ def circuit(): ) qml.math.isclose(res1, tape_fn([res2]), atol=1e-2) + @pytest.mark.parametrize( + "op", + [CustomTwoQubitOperation(wires=[0, 1])], + ) + def test_su4_rotation_decomposition(self, op): + """Test two-qubit gates are decomposed correctly using SU(4) rotations""" + + def circuit(): + qml.apply(op) + return qml.probs(wires=0) + + old_tape = qml.tape.make_qscript(circuit)() + + [new_tape], tape_fn = clifford_t_decomposition(old_tape) + + assert all( + isinstance(op, _CLIFFORD_PHASE_GATES) + or isinstance(getattr(op, "base", None), _CLIFFORD_PHASE_GATES) + for op in new_tape.operations + ) + + dev = qml.device("default.qubit") + transform_program, _ = dev.preprocess() + res1, res2 = qml.execute( + [old_tape, new_tape], device=dev, transform_program=transform_program + ) + qml.math.isclose(res1, tape_fn([res2]), atol=1e-2) + @pytest.mark.parametrize( "op", [qml.RX(1.0, wires="a"), qml.U3(1, 2, 3, wires=[1]), qml.PhaseShift(1.0, wires=[2])] ) From 080e5eddf4846c65241b61036e3684346c4edb9f Mon Sep 17 00:00:00 2001 From: "Yushao Chen (Jerry)" Date: Fri, 15 Nov 2024 16:49:43 -0500 Subject: [PATCH 02/35] Move contents of `pennylane.utils` to appropriate modules (#6588) **Context:** The legacy remains `qml.utils` module has been hanging around for long. Now, finally we are to delete them and move the remaining folks to wherever they're supposed to be. Specifically, the following 4 sets of functions have been either moved or removed: * `qml.utils._flatten`, `qml.utils.unflatten` has been moved and renamed to `qml.pytrees.flatten_np` and `qml.pytrees.unflatten_np` respectively. * `qml.utils._inv_dict` and `qml._get_default_args` have been removed. * `qml.utils.pauli_eigs` has been moved to `qml.pauli.utils`. * `qml.utils.expand_vector` has been moved to `qml.math.expand_vector`. **Description of the Change:** **Benefits:** Less redundancy **Possible Drawbacks:** Rare chance that some downstreaming repo might be using these funcationalities: - [x] lightning - [x] catalyst - [x] qml - [x] plugins We will come back to check them one by one to make sure nothing is to break **Related GitHub Issues:** **Related Shortcut Stories:** [sc-76906] --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> --- doc/code/qml_utils.rst | 14 -- doc/index.rst | 1 - doc/releases/changelog-dev.md | 11 + pennylane/math/__init__.py | 3 +- pennylane/math/matrix_manipulation.py | 54 ++++ pennylane/operation.py | 5 +- pennylane/ops/op_math/composite.py | 4 +- pennylane/ops/op_math/linear_combination.py | 4 +- pennylane/ops/qubit/non_parametric_ops.py | 9 +- .../ops/qubit/parametric_ops_multi_qubit.py | 5 +- pennylane/optimize/momentum_qng.py | 7 +- pennylane/optimize/qng.py | 85 ++++++- pennylane/optimize/rotoselect.py | 9 +- pennylane/pauli/__init__.py | 1 + pennylane/pauli/utils.py | 18 ++ pennylane/pytrees/__init__.py | 9 +- .../transforms/sign_expand/sign_expand.py | 1 - pennylane/utils.py | 208 --------------- tests/math/test_matrix_manipulation.py | 66 ++++- tests/ops/qubit/test_parametric_ops.py | 2 +- tests/optimize/test_qng.py | 64 +++++ tests/optimize/test_rotosolve.py | 32 +-- tests/pauli/test_pauli_utils.py | 44 +++- tests/test_utils.py | 237 ------------------ 24 files changed, 385 insertions(+), 508 deletions(-) delete mode 100644 doc/code/qml_utils.rst delete mode 100644 pennylane/utils.py delete mode 100644 tests/test_utils.py diff --git a/doc/code/qml_utils.rst b/doc/code/qml_utils.rst deleted file mode 100644 index 5faeb61440d..00000000000 --- a/doc/code/qml_utils.rst +++ /dev/null @@ -1,14 +0,0 @@ -qml.utils -========= - -.. currentmodule:: pennylane.utils - -.. warning:: - - Unless you are a PennyLane or plugin developer, you likely do not need - to use these utility functions. - -.. automodapi:: pennylane.utils - :no-heading: - :include-all-objects: - :skip: Iterable diff --git a/doc/index.rst b/doc/index.rst index 8bde3d6470f..4398685fdf5 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -230,6 +230,5 @@ PennyLane is **free** and **open source**, released under the Apache License, Ve code/qml_operation code/qml_queuing code/qml_tape - code/qml_utils code/qml_wires code/qml_workflow diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 164775e1d26..d7c504dfed4 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -66,6 +66,17 @@

Breaking changes 💔

+* The developer-facing `qml.utils` module has been removed. Specifically, the +following 4 sets of functions have been either moved or removed[(#6588)](https://github.com/PennyLaneAI/pennylane/pull/6588): + + * `qml.utils._flatten`, `qml.utils.unflatten` has been moved and renamed to `qml.optimize.qng._flatten_np` and `qml.optimize.qng._unflatten_np` respectively. + + * `qml.utils._inv_dict` and `qml._get_default_args` have been removed. + + * `qml.utils.pauli_eigs` has been moved to `qml.pauli.utils`. + + * `qml.utils.expand_vector` has been moved to `qml.math.expand_vector`. + * The `qml.qinfo` module has been removed. Please see the respective functions in the `qml.math` and `qml.measurements` modules instead. [(#6584)](https://github.com/PennyLaneAI/pennylane/pull/6584) diff --git a/pennylane/math/__init__.py b/pennylane/math/__init__.py index 731979daac9..afcf55316f1 100644 --- a/pennylane/math/__init__.py +++ b/pennylane/math/__init__.py @@ -34,7 +34,7 @@ import autoray as ar from .is_independent import is_independent -from .matrix_manipulation import expand_matrix, reduce_matrices, get_batch_size +from .matrix_manipulation import expand_matrix, expand_vector, reduce_matrices, get_batch_size from .multi_dispatch import ( add, array, @@ -152,6 +152,7 @@ def __getattr__(name): "dot", "einsum", "expand_matrix", + "expand_vector", "expectation_value", "eye", "fidelity", diff --git a/pennylane/math/matrix_manipulation.py b/pennylane/math/matrix_manipulation.py index 98bea694eae..0e4149c21f0 100644 --- a/pennylane/math/matrix_manipulation.py +++ b/pennylane/math/matrix_manipulation.py @@ -14,6 +14,7 @@ """This module contains methods to expand the matrix representation of an operator to a higher hilbert space with re-ordered wires.""" import itertools +import numbers from collections.abc import Callable, Generator, Iterable from functools import reduce @@ -348,3 +349,56 @@ def get_batch_size(tensor, expected_shape, expected_size): raise err return None + + +def expand_vector(vector, original_wires, expanded_wires): + r"""Expand a vector to more wires. + + Args: + vector (array): :math:`2^n` vector where n = len(original_wires). + original_wires (Sequence[int]): original wires of vector + expanded_wires (Union[Sequence[int], int]): expanded wires of vector, can be shuffled + If a single int m is given, corresponds to list(range(m)) + + Returns: + array: :math:`2^m` vector where m = len(expanded_wires). + """ + if len(original_wires) == 0: + val = qml.math.squeeze(vector) + return val * qml.math.ones(2 ** len(expanded_wires)) + if isinstance(expanded_wires, numbers.Integral): + expanded_wires = list(range(expanded_wires)) + + N = len(original_wires) + M = len(expanded_wires) + D = M - N + + len_vector = qml.math.shape(vector)[0] + qudit_order = int(2 ** (np.log2(len_vector) / N)) + + if not set(expanded_wires).issuperset(original_wires): + raise ValueError("Invalid target subsystems provided in 'original_wires' argument.") + + if qml.math.shape(vector) != (qudit_order**N,): + raise ValueError(f"Vector parameter must be of length {qudit_order}**len(original_wires)") + + dims = [qudit_order] * N + tensor = qml.math.reshape(vector, dims) + + if D > 0: + extra_dims = [qudit_order] * D + ones = qml.math.ones(qudit_order**D).reshape(extra_dims) + expanded_tensor = qml.math.tensordot(tensor, ones, axes=0) + else: + expanded_tensor = tensor + + wire_indices = [expanded_wires.index(wire) for wire in original_wires] + wire_indices = np.array(wire_indices) + + # Order tensor factors according to wires + original_indices = np.array(range(N)) + expanded_tensor = qml.math.moveaxis( + expanded_tensor, tuple(original_indices), tuple(wire_indices) + ) + + return qml.math.reshape(expanded_tensor, (qudit_order**M,)) diff --git a/pennylane/operation.py b/pennylane/operation.py index f405ec259b8..1cc41052f6f 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -264,7 +264,6 @@ from pennylane.wires import Wires, WiresLike from .pytrees import register_pytree -from .utils import pauli_eigs # ============================================================================= # Errors @@ -2346,7 +2345,7 @@ def eigvals(self): standard_observables = {"PauliX", "PauliY", "PauliZ", "Hadamard"} # observable should be Z^{\otimes n} - self._eigvals_cache = pauli_eigs(len(self.wires)) + self._eigvals_cache = qml.pauli.pauli_eigs(len(self.wires)) # check if there are any non-standard observables (such as Identity) if set(self.name) - standard_observables: @@ -2357,7 +2356,7 @@ def eigvals(self): if k: # Subgroup g contains only standard observables. self._eigvals_cache = qml.math.kron( - self._eigvals_cache, pauli_eigs(len(list(g))) + self._eigvals_cache, qml.pauli.pauli_eigs(len(list(g))) ) else: # Subgroup g contains only non-standard observables. diff --git a/pennylane/ops/op_math/composite.py b/pennylane/ops/op_math/composite.py index 51dd94fb605..132fa3cd516 100644 --- a/pennylane/ops/op_math/composite.py +++ b/pennylane/ops/op_math/composite.py @@ -193,12 +193,12 @@ def eigvals(self): for ops in self.overlapping_ops: if len(ops) == 1: eigvals.append( - qml.utils.expand_vector(ops[0].eigvals(), list(ops[0].wires), list(self.wires)) + math.expand_vector(ops[0].eigvals(), list(ops[0].wires), list(self.wires)) ) else: tmp_composite = self.__class__(*ops) eigvals.append( - qml.utils.expand_vector( + math.expand_vector( tmp_composite.eigendecomposition["eigval"], list(tmp_composite.wires), list(self.wires), diff --git a/pennylane/ops/op_math/linear_combination.py b/pennylane/ops/op_math/linear_combination.py index bbada7385f7..348a95cfab0 100644 --- a/pennylane/ops/op_math/linear_combination.py +++ b/pennylane/ops/op_math/linear_combination.py @@ -471,12 +471,12 @@ def eigvals(self): for ops in self.overlapping_ops: if len(ops) == 1: eigvals.append( - qml.utils.expand_vector(ops[0].eigvals(), list(ops[0].wires), list(self.wires)) + qml.math.expand_vector(ops[0].eigvals(), list(ops[0].wires), list(self.wires)) ) else: tmp_composite = Sum(*ops) # only change compared to CompositeOp.eigvals() eigvals.append( - qml.utils.expand_vector( + qml.math.expand_vector( tmp_composite.eigendecomposition["eigval"], list(tmp_composite.wires), list(self.wires), diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index 8e12f6d333c..1855e0217a2 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -27,7 +27,6 @@ import pennylane as qml from pennylane.operation import Observable, Operation from pennylane.typing import TensorLike -from pennylane.utils import pauli_eigs from pennylane.wires import Wires, WiresLike INV_SQRT2 = 1 / qml.math.sqrt(2) @@ -126,7 +125,7 @@ def compute_eigvals() -> np.ndarray: # pylint: disable=arguments-differ >>> print(qml.Hadamard.compute_eigvals()) [ 1 -1] """ - return pauli_eigs(1) + return qml.pauli.pauli_eigs(1) @staticmethod def compute_diagonalizing_gates(wires: WiresLike) -> list[qml.operation.Operator]: @@ -315,7 +314,7 @@ def compute_eigvals() -> np.ndarray: # pylint: disable=arguments-differ >>> print(qml.X.compute_eigvals()) [ 1 -1] """ - return pauli_eigs(1) + return qml.pauli.pauli_eigs(1) @staticmethod def compute_diagonalizing_gates(wires: WiresLike) -> list[qml.operation.Operator]: @@ -506,7 +505,7 @@ def compute_eigvals() -> np.ndarray: # pylint: disable=arguments-differ >>> print(qml.Y.compute_eigvals()) [ 1 -1] """ - return pauli_eigs(1) + return qml.pauli.pauli_eigs(1) @staticmethod def compute_diagonalizing_gates(wires: WiresLike) -> list[qml.operation.Operator]: @@ -695,7 +694,7 @@ def compute_eigvals() -> np.ndarray: # pylint: disable=arguments-differ >>> print(qml.Z.compute_eigvals()) [ 1 -1] """ - return pauli_eigs(1) + return qml.pauli.pauli_eigs(1) @staticmethod def compute_diagonalizing_gates( # pylint: disable=unused-argument diff --git a/pennylane/ops/qubit/parametric_ops_multi_qubit.py b/pennylane/ops/qubit/parametric_ops_multi_qubit.py index 39278435ad1..1064315965c 100644 --- a/pennylane/ops/qubit/parametric_ops_multi_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_multi_qubit.py @@ -27,7 +27,6 @@ from pennylane.math import expand_matrix from pennylane.operation import AnyWires, FlatPytree, Operation from pennylane.typing import TensorLike -from pennylane.utils import pauli_eigs from pennylane.wires import Wires, WiresLike from .non_parametric_ops import Hadamard, PauliX, PauliY, PauliZ @@ -105,7 +104,7 @@ def compute_matrix( [0.0000+0.0000j, 0.0000+0.0000j, 0.9988+0.0500j, 0.0000+0.0000j], [0.0000+0.0000j, 0.0000+0.0000j, 0.0000+0.0000j, 0.9988-0.0500j]]) """ - eigs = qml.math.convert_like(pauli_eigs(num_wires), theta) + eigs = qml.math.convert_like(qml.pauli.pauli_eigs(num_wires), theta) if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) @@ -153,7 +152,7 @@ def compute_eigvals( tensor([0.9689-0.2474j, 0.9689+0.2474j, 0.9689+0.2474j, 0.9689-0.2474j, 0.9689+0.2474j, 0.9689-0.2474j, 0.9689-0.2474j, 0.9689+0.2474j]) """ - eigs = qml.math.convert_like(pauli_eigs(num_wires), theta) + eigs = qml.math.convert_like(qml.pauli.pauli_eigs(num_wires), theta) if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) diff --git a/pennylane/optimize/momentum_qng.py b/pennylane/optimize/momentum_qng.py index 611c204b2bd..376b18d60c1 100644 --- a/pennylane/optimize/momentum_qng.py +++ b/pennylane/optimize/momentum_qng.py @@ -16,9 +16,8 @@ # pylint: disable=too-many-branches # pylint: disable=too-many-arguments from pennylane import numpy as pnp -from pennylane.utils import _flatten, unflatten -from .qng import QNGOptimizer +from .qng import QNGOptimizer, _flatten_np, _unflatten_np class MomentumQNGOptimizer(QNGOptimizer): @@ -131,12 +130,12 @@ def apply_grad(self, grad, args): for index, arg in enumerate(args): if getattr(arg, "requires_grad", False): - grad_flat = pnp.array(list(_flatten(grad[trained_index]))) + grad_flat = pnp.array(list(_flatten_np(grad[trained_index]))) # self.metric_tensor has already been reshaped to 2D, matching flat gradient. qng_update = pnp.linalg.pinv(metric_tensor[trained_index]) @ grad_flat self.accumulation[trained_index] *= self.momentum - self.accumulation[trained_index] += self.stepsize * unflatten( + self.accumulation[trained_index] += self.stepsize * _unflatten_np( qng_update, grad[trained_index] ) args_new[index] = arg - self.accumulation[trained_index] diff --git a/pennylane/optimize/qng.py b/pennylane/optimize/qng.py index a5c31e7218f..c55a5cb06bd 100644 --- a/pennylane/optimize/qng.py +++ b/pennylane/optimize/qng.py @@ -12,12 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. """Quantum natural gradient optimizer""" +import numbers +from collections.abc import Iterable + import pennylane as qml # pylint: disable=too-many-branches # pylint: disable=too-many-arguments from pennylane import numpy as pnp -from pennylane.utils import _flatten, unflatten from .gradient_descent import GradientDescentOptimizer @@ -277,11 +279,88 @@ def apply_grad(self, grad, args): trained_index = 0 for index, arg in enumerate(args): if getattr(arg, "requires_grad", False): - grad_flat = pnp.array(list(_flatten(grad[trained_index]))) + grad_flat = pnp.array(list(_flatten_np(grad[trained_index]))) # self.metric_tensor has already been reshaped to 2D, matching flat gradient. update = pnp.linalg.pinv(mt[trained_index]) @ grad_flat - args_new[index] = arg - self.stepsize * unflatten(update, grad[trained_index]) + args_new[index] = arg - self.stepsize * _unflatten_np(update, grad[trained_index]) trained_index += 1 return tuple(args_new) + + +def _flatten_np(x): + """Iterate recursively through an arbitrarily nested structure in depth-first order. + + See also :func:`_unflatten`. + + Args: + x (array, Iterable, Any): each element of an array or an Iterable may itself be any of these types + + Yields: + Any: elements of x in depth-first order + """ + if isinstance(x, pnp.ndarray): + yield from _flatten_np( + x.flat + ) # should we allow object arrays? or just "yield from x.flat"? + elif isinstance(x, qml.wires.Wires): + # Reursive calls to flatten `Wires` will cause infinite recursion (`Wires` atoms are `Wires`). + # Since Wires are always flat, just yield. + yield from x + elif isinstance(x, Iterable) and not isinstance(x, (str, bytes)): + for item in x: + yield from _flatten_np(item) + else: + yield x + + +def _unflatten_np_dispatch(flat, model): + """Restores an arbitrary nested structure to a flattened iterable. + + See also :func:`_flatten`. + + Args: + flat (array): 1D array of items + model (array, Iterable, Number): model nested structure + + Raises: + TypeError: if ``model`` contains an object of unsupported type + + Returns: + Union[array, list, Any], array: first elements of flat arranged into the nested + structure of model, unused elements of flat + """ + if isinstance(model, (numbers.Number, str)): + return flat[0], flat[1:] + + if isinstance(model, pnp.ndarray): + idx = model.size + res = pnp.array(flat)[:idx].reshape(model.shape) + return res, flat[idx:] + + if isinstance(model, Iterable): + res = [] + for x in model: + val, flat = _unflatten_np_dispatch(flat, x) + res.append(val) + return res, flat + + raise TypeError(f"Unsupported type in the model: {type(model)}") + + +def _unflatten_np(flat, model): + """Wrapper for :func:`_unflatten`. + + Args: + flat (array): 1D array of items + model (array, Iterable, Number): model nested structure + + Raises: + ValueError: if ``flat`` has more elements than ``model`` + """ + # pylint:disable=len-as-condition + res, tail = _unflatten_np_dispatch(pnp.asarray(flat), model) + if len(tail) != 0: + raise ValueError("Flattened iterable has more elements than the model.") + return res diff --git a/pennylane/optimize/rotoselect.py b/pennylane/optimize/rotoselect.py index 1d1669648a9..c61b27ba628 100644 --- a/pennylane/optimize/rotoselect.py +++ b/pennylane/optimize/rotoselect.py @@ -16,7 +16,8 @@ import numpy as np import pennylane as qml -from pennylane.utils import _flatten, unflatten + +from .qng import _flatten_np, _unflatten_np class RotoselectOptimizer: @@ -132,11 +133,11 @@ def step(self, objective_fn, x, generators, **kwargs): Returns: array: The new variable values :math:`x^{(t+1)}` as well as the new generators. """ - x_flat = np.fromiter(_flatten(x), dtype=float) + x_flat = np.fromiter(_flatten_np(x), dtype=float) # wrap the objective function so that it accepts the flattened parameter array # pylint:disable=unnecessary-lambda-assignment objective_fn_flat = lambda x_flat, gen: objective_fn( - unflatten(x_flat, x), generators=gen, **kwargs + _unflatten_np(x_flat, x), generators=gen, **kwargs ) try: @@ -151,7 +152,7 @@ def step(self, objective_fn, x, generators, **kwargs): objective_fn_flat, x_flat, generators, d ) - return unflatten(x_flat, x), generators + return _unflatten_np(x_flat, x), generators def _find_optimal_generators(self, objective_fn, x, generators, d): r"""Optimizer for the generators. diff --git a/pennylane/pauli/__init__.py b/pennylane/pauli/__init__.py index ab90d27df81..394be72cb77 100644 --- a/pennylane/pauli/__init__.py +++ b/pennylane/pauli/__init__.py @@ -34,6 +34,7 @@ diagonalize_qwc_pauli_words, diagonalize_qwc_groupings, simplify, + pauli_eigs, ) from .pauli_interface import pauli_word_prefactor diff --git a/pennylane/pauli/utils.py b/pennylane/pauli/utils.py index 7365e3131e1..8f0ec975ec0 100644 --- a/pennylane/pauli/utils.py +++ b/pennylane/pauli/utils.py @@ -1335,3 +1335,21 @@ def _binary_matrix_from_pws(terms, num_qubits, wire_map=None): binary_matrix[idx][wire_map[wire]] = 1 return binary_matrix + + +@lru_cache() +def pauli_eigs(n): + r"""Eigenvalues for :math:`A^{\otimes n}`, where :math:`A` is + Pauli operator, or shares its eigenvalues. + + As an example if n==2, then the eigenvalues of a tensor product consisting + of two matrices sharing the eigenvalues with Pauli matrices is returned. + + Args: + n (int): the number of qubits the matrix acts on + Returns: + list: the eigenvalues of the specified observable + """ + if n == 1: + return np.array([1.0, -1.0]) + return np.concatenate([pauli_eigs(n - 1), -pauli_eigs(n - 1)]) diff --git a/pennylane/pytrees/__init__.py b/pennylane/pytrees/__init__.py index 6180de919f5..420bd25503e 100644 --- a/pennylane/pytrees/__init__.py +++ b/pennylane/pytrees/__init__.py @@ -15,7 +15,14 @@ An internal module for working with pytrees. """ -from .pytrees import PyTreeStructure, flatten, is_pytree, leaf, register_pytree, unflatten +from .pytrees import ( + PyTreeStructure, + flatten, + is_pytree, + leaf, + register_pytree, + unflatten, +) __all__ = [ "PyTreeStructure", diff --git a/pennylane/transforms/sign_expand/sign_expand.py b/pennylane/transforms/sign_expand/sign_expand.py index 928079f29c5..aaad22cb63e 100644 --- a/pennylane/transforms/sign_expand/sign_expand.py +++ b/pennylane/transforms/sign_expand/sign_expand.py @@ -310,7 +310,6 @@ def circuit(): hamiltonian = tape.measurements[0].obs wires = hamiltonian.wires - # TODO qml.utils.sparse_hamiltonian at the moment does not allow autograd to push gradients through if ( not isinstance(hamiltonian, (qml.ops.Hamiltonian, qml.ops.LinearCombination)) or len(tape.measurements) > 1 diff --git a/pennylane/utils.py b/pennylane/utils.py deleted file mode 100644 index e9eab2c3171..00000000000 --- a/pennylane/utils.py +++ /dev/null @@ -1,208 +0,0 @@ -# Copyright 2018-2021 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 utilities and auxiliary functions which are shared -across the PennyLane submodules. -""" -import functools -import inspect -import numbers - -# pylint: disable=protected-access,too-many-branches -from collections.abc import Iterable - -import numpy as np - -import pennylane as qml - - -def _flatten(x): - """Iterate recursively through an arbitrarily nested structure in depth-first order. - - See also :func:`_unflatten`. - - Args: - x (array, Iterable, Any): each element of an array or an Iterable may itself be any of these types - - Yields: - Any: elements of x in depth-first order - """ - if isinstance(x, np.ndarray): - yield from _flatten(x.flat) # should we allow object arrays? or just "yield from x.flat"? - elif isinstance(x, qml.wires.Wires): - # Reursive calls to flatten `Wires` will cause infinite recursion (`Wires` atoms are `Wires`). - # Since Wires are always flat, just yield. - yield from x - elif isinstance(x, Iterable) and not isinstance(x, (str, bytes)): - for item in x: - yield from _flatten(item) - else: - yield x - - -def _unflatten(flat, model): - """Restores an arbitrary nested structure to a flattened iterable. - - See also :func:`_flatten`. - - Args: - flat (array): 1D array of items - model (array, Iterable, Number): model nested structure - - Raises: - TypeError: if ``model`` contains an object of unsupported type - - Returns: - Union[array, list, Any], array: first elements of flat arranged into the nested - structure of model, unused elements of flat - """ - if isinstance(model, (numbers.Number, str)): - return flat[0], flat[1:] - - if isinstance(model, np.ndarray): - idx = model.size - res = np.array(flat)[:idx].reshape(model.shape) - return res, flat[idx:] - - if isinstance(model, Iterable): - res = [] - for x in model: - val, flat = _unflatten(flat, x) - res.append(val) - return res, flat - - raise TypeError(f"Unsupported type in the model: {type(model)}") - - -def unflatten(flat, model): - """Wrapper for :func:`_unflatten`. - - Args: - flat (array): 1D array of items - model (array, Iterable, Number): model nested structure - - Raises: - ValueError: if ``flat`` has more elements than ``model`` - """ - # pylint:disable=len-as-condition - res, tail = _unflatten(np.asarray(flat), model) - if len(tail) != 0: - raise ValueError("Flattened iterable has more elements than the model.") - return res - - -def _inv_dict(d): - """Reverse a dictionary mapping. - - Returns multimap where the keys are the former values, - and values are sets of the former keys. - - Args: - d (dict[a->b]): mapping to reverse - - Returns: - dict[b->set[a]]: reversed mapping - """ - ret = {} - for k, v in d.items(): - ret.setdefault(v, set()).add(k) - return ret - - -def _get_default_args(func): - """Get the default arguments of a function. - - Args: - func (callable): a function - - Returns: - dict[str, tuple]: mapping from argument name to (positional idx, default value) - """ - signature = inspect.signature(func) - return { - k: (idx, v.default) - for idx, (k, v) in enumerate(signature.parameters.items()) - if v.default is not inspect.Parameter.empty - } - - -@functools.lru_cache() -def pauli_eigs(n): - r"""Eigenvalues for :math:`A^{\otimes n}`, where :math:`A` is - Pauli operator, or shares its eigenvalues. - - As an example if n==2, then the eigenvalues of a tensor product consisting - of two matrices sharing the eigenvalues with Pauli matrices is returned. - - Args: - n (int): the number of qubits the matrix acts on - Returns: - list: the eigenvalues of the specified observable - """ - if n == 1: - return np.array([1.0, -1.0]) - return np.concatenate([pauli_eigs(n - 1), -pauli_eigs(n - 1)]) - - -def expand_vector(vector, original_wires, expanded_wires): - r"""Expand a vector to more wires. - - Args: - vector (array): :math:`2^n` vector where n = len(original_wires). - original_wires (Sequence[int]): original wires of vector - expanded_wires (Union[Sequence[int], int]): expanded wires of vector, can be shuffled - If a single int m is given, corresponds to list(range(m)) - - Returns: - array: :math:`2^m` vector where m = len(expanded_wires). - """ - if len(original_wires) == 0: - val = qml.math.squeeze(vector) - return val * qml.math.ones(2 ** len(expanded_wires)) - if isinstance(expanded_wires, numbers.Integral): - expanded_wires = list(range(expanded_wires)) - - N = len(original_wires) - M = len(expanded_wires) - D = M - N - - len_vector = qml.math.shape(vector)[0] - qudit_order = int(2 ** (np.log2(len_vector) / N)) - - if not set(expanded_wires).issuperset(original_wires): - raise ValueError("Invalid target subsystems provided in 'original_wires' argument.") - - if qml.math.shape(vector) != (qudit_order**N,): - raise ValueError(f"Vector parameter must be of length {qudit_order}**len(original_wires)") - - dims = [qudit_order] * N - tensor = qml.math.reshape(vector, dims) - - if D > 0: - extra_dims = [qudit_order] * D - ones = qml.math.ones(qudit_order**D).reshape(extra_dims) - expanded_tensor = qml.math.tensordot(tensor, ones, axes=0) - else: - expanded_tensor = tensor - - wire_indices = [expanded_wires.index(wire) for wire in original_wires] - wire_indices = np.array(wire_indices) - - # Order tensor factors according to wires - original_indices = np.array(range(N)) - expanded_tensor = qml.math.moveaxis( - expanded_tensor, tuple(original_indices), tuple(wire_indices) - ) - - return qml.math.reshape(expanded_tensor, (qudit_order**M,)) diff --git a/tests/math/test_matrix_manipulation.py b/tests/math/test_matrix_manipulation.py index d6b4029a0ca..8ea97f2ee18 100644 --- a/tests/math/test_matrix_manipulation.py +++ b/tests/math/test_matrix_manipulation.py @@ -22,7 +22,7 @@ import pennylane as qml from pennylane import numpy as pnp -from pennylane.math import expand_matrix +from pennylane.math import expand_matrix, expand_vector # Define a list of dtypes to test dtypes = ["complex64", "complex128"] @@ -1004,3 +1004,67 @@ def test_partial_trace_single_matrix(self, ml_framework, c_dtype): expected = qml.math.asarray(np.array([[1, 0], [0, 0]], dtype=c_dtype), like=ml_framework) assert qml.math.allclose(result, expected) + + +class TestExpandVector: + """Tests vector expansion to more wires""" + + VECTOR1 = np.array([1, -1]) + ONES = np.array([1, 1]) + + @pytest.mark.parametrize( + "original_wires,expanded_wires,expected", + [ + ([0], 3, np.kron(np.kron(VECTOR1, ONES), ONES)), + ([1], 3, np.kron(np.kron(ONES, VECTOR1), ONES)), + ([2], 3, np.kron(np.kron(ONES, ONES), VECTOR1)), + ([0], [0, 4, 7], np.kron(np.kron(VECTOR1, ONES), ONES)), + ([4], [0, 4, 7], np.kron(np.kron(ONES, VECTOR1), ONES)), + ([7], [0, 4, 7], np.kron(np.kron(ONES, ONES), VECTOR1)), + ([0], [0, 4, 7], np.kron(np.kron(VECTOR1, ONES), ONES)), + ([4], [4, 0, 7], np.kron(np.kron(VECTOR1, ONES), ONES)), + ([7], [7, 4, 0], np.kron(np.kron(VECTOR1, ONES), ONES)), + ], + ) + def test_expand_vector_single_wire(self, original_wires, expanded_wires, expected, tol): + """Test that expand_vector works with a single-wire vector.""" + + res = expand_vector(TestExpandVector.VECTOR1, original_wires, expanded_wires) + + assert np.allclose(res, expected, atol=tol, rtol=0) + + VECTOR2 = np.array([1, 2, 3, 4]) + ONES = np.array([1, 1]) + + @pytest.mark.parametrize( + "original_wires,expanded_wires,expected", + [ + ([0, 1], 3, np.kron(VECTOR2, ONES)), + ([1, 2], 3, np.kron(ONES, VECTOR2)), + ([0, 2], 3, np.array([1, 2, 1, 2, 3, 4, 3, 4])), + ([0, 5], [0, 5, 9], np.kron(VECTOR2, ONES)), + ([5, 9], [0, 5, 9], np.kron(ONES, VECTOR2)), + ([0, 9], [0, 5, 9], np.array([1, 2, 1, 2, 3, 4, 3, 4])), + ([9, 0], [0, 5, 9], np.array([1, 3, 1, 3, 2, 4, 2, 4])), + ([0, 1], [0, 1], VECTOR2), + ], + ) + def test_expand_vector_two_wires(self, original_wires, expanded_wires, expected, tol): + """Test that expand_vector works with a single-wire vector.""" + + res = expand_vector(TestExpandVector.VECTOR2, original_wires, expanded_wires) + + assert np.allclose(res, expected, atol=tol, rtol=0) + + def test_expand_vector_invalid_wires(self): + """Test exception raised if unphysical subsystems provided.""" + with pytest.raises( + ValueError, + match="Invalid target subsystems provided in 'original_wires' argument", + ): + expand_vector(TestExpandVector.VECTOR2, [-1, 5], 4) + + def test_expand_vector_invalid_vector(self): + """Test exception raised if incorrect sized vector provided.""" + with pytest.raises(ValueError, match="Vector parameter must be of length"): + expand_vector(TestExpandVector.VECTOR1, [0, 1], 4) diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py index f9fe0e6d9ab..66a44b98e9b 100644 --- a/tests/ops/qubit/test_parametric_ops.py +++ b/tests/ops/qubit/test_parametric_ops.py @@ -3260,7 +3260,7 @@ def test_multirz_generator(self, qubits, mocker): qml.assert_equal(gen, qml.Hamiltonian([-0.5], [expected_gen])) - spy = mocker.spy(qml.utils, "pauli_eigs") + spy = mocker.spy(qml.pauli.utils, "pauli_eigs") op.generator() spy.assert_not_called() diff --git a/tests/optimize/test_qng.py b/tests/optimize/test_qng.py index 3347fdca290..a382a73aef3 100644 --- a/tests/optimize/test_qng.py +++ b/tests/optimize/test_qng.py @@ -18,6 +18,7 @@ import pennylane as qml from pennylane import numpy as np +from pennylane.optimize.qng import _flatten_np, _unflatten_np class TestBasics: @@ -394,3 +395,66 @@ def gradient(params): assert np.allclose(circuit(x, y), qml.eigvals(H).min(), atol=tol, rtol=0) if qml.operation.active_new_opmath(): assert len(recwarn) == 0 + + +flat_dummy_array = np.linspace(-1, 1, 64) +test_shapes = [ + (64,), + (64, 1), + (32, 2), + (16, 4), + (8, 8), + (16, 2, 2), + (8, 2, 2, 2), + (4, 2, 2, 2, 2), + (2, 2, 2, 2, 2, 2), +] + + +class TestFlatten: + """Tests the flatten and unflatten functions""" + + @pytest.mark.parametrize("shape", test_shapes) + def test_flatten(self, shape): + """Tests that _flatten successfully flattens multidimensional arrays.""" + + reshaped = np.reshape(flat_dummy_array, shape) + flattened = np.array(list(_flatten_np(reshaped))) + + assert flattened.shape == flat_dummy_array.shape + assert np.array_equal(flattened, flat_dummy_array) + + @pytest.mark.parametrize("shape", test_shapes) + def test_unflatten(self, shape): + """Tests that _unflatten successfully unflattens multidimensional arrays.""" + + reshaped = np.reshape(flat_dummy_array, shape) + unflattened = np.array(list(_unflatten_np(flat_dummy_array, reshaped))) + + assert unflattened.shape == reshaped.shape + assert np.array_equal(unflattened, reshaped) + + def test_unflatten_error_unsupported_model(self): + """Tests that unflatten raises an error if the given model is not supported""" + + with pytest.raises(TypeError, match="Unsupported type in the model"): + model = lambda x: x # not a valid model for unflatten + _unflatten_np(flat_dummy_array, model) + + def test_unflatten_error_too_many_elements(self): + """Tests that unflatten raises an error if the given iterable has + more elements than the model""" + + reshaped = np.reshape(flat_dummy_array, (16, 2, 2)) + + with pytest.raises(ValueError, match="Flattened iterable has more elements than the model"): + _unflatten_np(np.concatenate([flat_dummy_array, flat_dummy_array]), reshaped) + + def test_flatten_wires(self): + """Tests flattening a Wires object.""" + wires = qml.wires.Wires([3, 4]) + wires_int = [3, 4] + + wires = _flatten_np(wires) + for i, wire in enumerate(wires): + assert wires_int[i] == wire diff --git a/tests/optimize/test_rotosolve.py b/tests/optimize/test_rotosolve.py index 2db9075e5bc..e000ea5f03c 100644 --- a/tests/optimize/test_rotosolve.py +++ b/tests/optimize/test_rotosolve.py @@ -23,7 +23,7 @@ import pennylane as qml from pennylane import numpy as np from pennylane.optimize import RotosolveOptimizer -from pennylane.utils import _flatten, unflatten +from pennylane.optimize.qng import _flatten_np, _unflatten_np def expand_num_freq(num_freq, param): @@ -47,11 +47,11 @@ def expand_num_freq(num_freq, param): def successive_params(par1, par2): """Return a list of parameter configurations, successively walking from par1 to par2 coordinate-wise.""" - par1_flat = np.fromiter(_flatten(par1), dtype=float) - par2_flat = np.fromiter(_flatten(par2), dtype=float) + par1_flat = np.fromiter(_flatten_np(par1), dtype=float) + par2_flat = np.fromiter(_flatten_np(par2), dtype=float) walking_param = [] for i in range(len(par1_flat) + 1): - walking_param.append(unflatten(np.append(par2_flat[:i], par1_flat[i:]), par1)) + walking_param.append(_unflatten_np(np.append(par2_flat[:i], par1_flat[i:]), par1)) return walking_param @@ -253,8 +253,8 @@ def test_single_step_convergence( assert len(x_min) == len(new_param_step) assert np.allclose( - np.fromiter(_flatten(x_min), dtype=float), - np.fromiter(_flatten(new_param_step), dtype=float), + np.fromiter(_flatten_np(x_min), dtype=float), + np.fromiter(_flatten_np(new_param_step), dtype=float), atol=1e-5, ) @@ -271,8 +271,8 @@ def test_single_step_convergence( assert len(x_min) == len(new_param_step_and_cost) assert np.allclose( - np.fromiter(_flatten(new_param_step_and_cost), dtype=float), - np.fromiter(_flatten(new_param_step), dtype=float), + np.fromiter(_flatten_np(new_param_step_and_cost), dtype=float), + np.fromiter(_flatten_np(new_param_step), dtype=float), atol=1e-5, ) assert np.isclose(old_cost, fun(*param)) @@ -331,8 +331,8 @@ def test_multiple_steps(fun, x_min, param, num_freq): assert (np.isscalar(x_min) and np.isscalar(param)) or len(x_min) == len(param) assert np.allclose( - np.fromiter(_flatten(x_min), dtype=float), - np.fromiter(_flatten(param), dtype=float), + np.fromiter(_flatten_np(x_min), dtype=float), + np.fromiter(_flatten_np(param), dtype=float), atol=1e-5, ) @@ -390,8 +390,8 @@ def test_single_step(self, fun, x_min, param, num_freq): assert len(x_min) == len(new_param_step) assert np.allclose( - np.fromiter(_flatten(x_min), dtype=float), - np.fromiter(_flatten(new_param_step), dtype=float), + np.fromiter(_flatten_np(x_min), dtype=float), + np.fromiter(_flatten_np(new_param_step), dtype=float), atol=1e-5, ) @@ -407,8 +407,8 @@ def test_single_step(self, fun, x_min, param, num_freq): assert len(x_min) == len(new_param_step_and_cost) assert np.allclose( - np.fromiter(_flatten(new_param_step_and_cost), dtype=float), - np.fromiter(_flatten(new_param_step), dtype=float), + np.fromiter(_flatten_np(new_param_step_and_cost), dtype=float), + np.fromiter(_flatten_np(new_param_step), dtype=float), atol=1e-5, ) assert np.isclose(old_cost, fun(*param)) @@ -517,8 +517,8 @@ def test_single_step( new_param_step_and_cost = (new_param_step_and_cost,) assert np.allclose( - np.fromiter(_flatten(new_param_step_and_cost), dtype=float), - np.fromiter(_flatten(new_param_step), dtype=float), + np.fromiter(_flatten_np(new_param_step_and_cost), dtype=float), + np.fromiter(_flatten_np(new_param_step), dtype=float), ) assert np.isclose(qnode(*param), old_cost) diff --git a/tests/pauli/test_pauli_utils.py b/tests/pauli/test_pauli_utils.py index cfba6dc1195..a8c7e3a060a 100644 --- a/tests/pauli/test_pauli_utils.py +++ b/tests/pauli/test_pauli_utils.py @@ -14,9 +14,11 @@ """ Unit tests for the :mod:`pauli` utility functions in ``pauli/utils.py``. """ +# pylint: disable=too-few-public-methods,too-many-public-methods +import functools +import itertools import warnings -# pylint: disable=too-few-public-methods,too-many-public-methods import numpy as np import pytest @@ -45,6 +47,7 @@ is_qwc, observables_to_binary_matrix, partition_pauli_group, + pauli_eigs, pauli_group, pauli_to_binary, pauli_word_to_matrix, @@ -1135,3 +1138,42 @@ def test_binary_matrix_from_pws(self, terms, num_qubits, result): pws_lst = [list(qml.pauli.pauli_sentence(t))[0] for t in terms] binary_matrix = qml.pauli.utils._binary_matrix_from_pws(pws_lst, num_qubits) assert (binary_matrix == result).all() + + +class TestPauliEigs: + """Tests for the auxiliary function to return the eigenvalues for Paulis""" + + paulix = np.array([[0, 1], [1, 0]]) + pauliy = np.array([[0, -1j], [1j, 0]]) + pauliz = np.array([[1, 0], [0, -1]]) + hadamard = 1 / np.sqrt(2) * np.array([[1, 1], [1, -1]]) + + standard_observables = [paulix, pauliy, pauliz, hadamard] + + matrix_pairs = [ + np.kron(x, y) + for x, y in list(itertools.product(standard_observables, standard_observables)) + ] + + def test_correct_eigenvalues_paulis(self): + """Test the paulieigs function for one qubit""" + assert np.array_equal(pauli_eigs(1), np.diag(self.pauliz)) + + def test_correct_eigenvalues_pauli_kronecker_products_two_qubits(self): + """Test the paulieigs function for two qubits""" + assert np.array_equal(pauli_eigs(2), np.diag(np.kron(self.pauliz, self.pauliz))) + + def test_correct_eigenvalues_pauli_kronecker_products_three_qubits(self): + """Test the paulieigs function for three qubits""" + assert np.array_equal( + pauli_eigs(3), + np.diag(np.kron(self.pauliz, np.kron(self.pauliz, self.pauliz))), + ) + + @pytest.mark.parametrize("depth", list(range(1, 6))) + def test_cache_usage(self, depth): + """Test that the right number of cachings have been executed after clearing the cache""" + pauli_eigs.cache_clear() + pauli_eigs(depth) + # pylint: disable=protected-access + assert functools._CacheInfo(depth - 1, depth, 128, depth) == pauli_eigs.cache_info() diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 47dac02148f..00000000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,237 +0,0 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Unit tests for the :mod:`pennylane.utils` module. -""" -# pylint: disable=no-self-use,too-many-arguments,protected-access -import functools -import itertools - -import numpy as np -import pytest - -import pennylane as qml -import pennylane.utils as pu - -flat_dummy_array = np.linspace(-1, 1, 64) -test_shapes = [ - (64,), - (64, 1), - (32, 2), - (16, 4), - (8, 8), - (16, 2, 2), - (8, 2, 2, 2), - (4, 2, 2, 2, 2), - (2, 2, 2, 2, 2, 2), -] - - -class TestFlatten: - """Tests the flatten and unflatten functions""" - - @pytest.mark.parametrize("shape", test_shapes) - def test_flatten(self, shape): - """Tests that _flatten successfully flattens multidimensional arrays.""" - - reshaped = np.reshape(flat_dummy_array, shape) - flattened = np.array(list(pu._flatten(reshaped))) - - assert flattened.shape == flat_dummy_array.shape - assert np.array_equal(flattened, flat_dummy_array) - - @pytest.mark.parametrize("shape", test_shapes) - def test_unflatten(self, shape): - """Tests that _unflatten successfully unflattens multidimensional arrays.""" - - reshaped = np.reshape(flat_dummy_array, shape) - unflattened = np.array(list(pu.unflatten(flat_dummy_array, reshaped))) - - assert unflattened.shape == reshaped.shape - assert np.array_equal(unflattened, reshaped) - - def test_unflatten_error_unsupported_model(self): - """Tests that unflatten raises an error if the given model is not supported""" - - with pytest.raises(TypeError, match="Unsupported type in the model"): - model = lambda x: x # not a valid model for unflatten - pu.unflatten(flat_dummy_array, model) - - def test_unflatten_error_too_many_elements(self): - """Tests that unflatten raises an error if the given iterable has - more elements than the model""" - - reshaped = np.reshape(flat_dummy_array, (16, 2, 2)) - - with pytest.raises(ValueError, match="Flattened iterable has more elements than the model"): - pu.unflatten(np.concatenate([flat_dummy_array, flat_dummy_array]), reshaped) - - def test_flatten_wires(self): - """Tests flattening a Wires object.""" - wires = qml.wires.Wires([3, 4]) - wires_int = [3, 4] - - wires = qml.utils._flatten(wires) - for i, wire in enumerate(wires): - assert wires_int[i] == wire - - -class TestPauliEigs: - """Tests for the auxiliary function to return the eigenvalues for Paulis""" - - paulix = np.array([[0, 1], [1, 0]]) - pauliy = np.array([[0, -1j], [1j, 0]]) - pauliz = np.array([[1, 0], [0, -1]]) - hadamard = 1 / np.sqrt(2) * np.array([[1, 1], [1, -1]]) - - standard_observables = [paulix, pauliy, pauliz, hadamard] - - matrix_pairs = [ - np.kron(x, y) - for x, y in list(itertools.product(standard_observables, standard_observables)) - ] - - def test_correct_eigenvalues_paulis(self): - """Test the paulieigs function for one qubit""" - assert np.array_equal(pu.pauli_eigs(1), np.diag(self.pauliz)) - - def test_correct_eigenvalues_pauli_kronecker_products_two_qubits(self): - """Test the paulieigs function for two qubits""" - assert np.array_equal(pu.pauli_eigs(2), np.diag(np.kron(self.pauliz, self.pauliz))) - - def test_correct_eigenvalues_pauli_kronecker_products_three_qubits(self): - """Test the paulieigs function for three qubits""" - assert np.array_equal( - pu.pauli_eigs(3), - np.diag(np.kron(self.pauliz, np.kron(self.pauliz, self.pauliz))), - ) - - @pytest.mark.parametrize("depth", list(range(1, 6))) - def test_cache_usage(self, depth): - """Test that the right number of cachings have been executed after clearing the cache""" - pu.pauli_eigs.cache_clear() - pu.pauli_eigs(depth) - assert functools._CacheInfo(depth - 1, depth, 128, depth) == pu.pauli_eigs.cache_info() - - -class TestArgumentHelpers: - """Tests for auxiliary functions to help with parsing - Python function arguments""" - - def test_no_default_args(self): - """Test that empty dict is returned if function has - no default arguments""" - - def dummy_func(a, b): # pylint: disable=unused-argument - pass - - res = pu._get_default_args(dummy_func) - assert not res - - def test_get_default_args(self): - """Test that default arguments are correctly extracted""" - - def dummy_func( - a, b, c=8, d=[0, 0.65], e=np.array([4]), f=None - ): # pylint: disable=unused-argument,dangerous-default-value - pass - - res = pu._get_default_args(dummy_func) - expected = { - "c": (2, 8), - "d": (3, [0, 0.65]), - "e": (4, np.array([4])), - "f": (5, None), - } - - assert res == expected - - def test_inv_dict(self): - """Test _inv_dict correctly inverts a dictionary""" - test_data = {"c": 8, "d": (0, 0.65), "e": "hi", "f": None, "g": 8} - res = pu._inv_dict(test_data) - expected = {8: {"g", "c"}, (0, 0.65): {"d"}, "hi": {"e"}, None: {"f"}} - - assert res == expected - - def test_inv_dict_unhashable_key(self): - """Test _inv_dict raises an exception if a dictionary value is unhashable""" - test_data = {"c": 8, "d": [0, 0.65], "e": "hi", "f": None, "g": 8} - - with pytest.raises(TypeError, match="unhashable type"): - pu._inv_dict(test_data) - - -class TestExpandVector: - """Tests vector expansion to more wires""" - - VECTOR1 = np.array([1, -1]) - ONES = np.array([1, 1]) - - @pytest.mark.parametrize( - "original_wires,expanded_wires,expected", - [ - ([0], 3, np.kron(np.kron(VECTOR1, ONES), ONES)), - ([1], 3, np.kron(np.kron(ONES, VECTOR1), ONES)), - ([2], 3, np.kron(np.kron(ONES, ONES), VECTOR1)), - ([0], [0, 4, 7], np.kron(np.kron(VECTOR1, ONES), ONES)), - ([4], [0, 4, 7], np.kron(np.kron(ONES, VECTOR1), ONES)), - ([7], [0, 4, 7], np.kron(np.kron(ONES, ONES), VECTOR1)), - ([0], [0, 4, 7], np.kron(np.kron(VECTOR1, ONES), ONES)), - ([4], [4, 0, 7], np.kron(np.kron(VECTOR1, ONES), ONES)), - ([7], [7, 4, 0], np.kron(np.kron(VECTOR1, ONES), ONES)), - ], - ) - def test_expand_vector_single_wire(self, original_wires, expanded_wires, expected, tol): - """Test that expand_vector works with a single-wire vector.""" - - res = pu.expand_vector(TestExpandVector.VECTOR1, original_wires, expanded_wires) - - assert np.allclose(res, expected, atol=tol, rtol=0) - - VECTOR2 = np.array([1, 2, 3, 4]) - ONES = np.array([1, 1]) - - @pytest.mark.parametrize( - "original_wires,expanded_wires,expected", - [ - ([0, 1], 3, np.kron(VECTOR2, ONES)), - ([1, 2], 3, np.kron(ONES, VECTOR2)), - ([0, 2], 3, np.array([1, 2, 1, 2, 3, 4, 3, 4])), - ([0, 5], [0, 5, 9], np.kron(VECTOR2, ONES)), - ([5, 9], [0, 5, 9], np.kron(ONES, VECTOR2)), - ([0, 9], [0, 5, 9], np.array([1, 2, 1, 2, 3, 4, 3, 4])), - ([9, 0], [0, 5, 9], np.array([1, 3, 1, 3, 2, 4, 2, 4])), - ([0, 1], [0, 1], VECTOR2), - ], - ) - def test_expand_vector_two_wires(self, original_wires, expanded_wires, expected, tol): - """Test that expand_vector works with a single-wire vector.""" - - res = pu.expand_vector(TestExpandVector.VECTOR2, original_wires, expanded_wires) - - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_expand_vector_invalid_wires(self): - """Test exception raised if unphysical subsystems provided.""" - with pytest.raises( - ValueError, - match="Invalid target subsystems provided in 'original_wires' argument", - ): - pu.expand_vector(TestExpandVector.VECTOR2, [-1, 5], 4) - - def test_expand_vector_invalid_vector(self): - """Test exception raised if incorrect sized vector provided.""" - with pytest.raises(ValueError, match="Vector parameter must be of length"): - pu.expand_vector(TestExpandVector.VECTOR1, [0, 1], 4) From 97c852b2d71e8790cfd9355427bedb3d7577863c Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Sun, 17 Nov 2024 17:38:55 -0500 Subject: [PATCH 03/35] Remove old opmath (#6548) **Context:** It's time to fully remove the legacy operator arithmetic (`Tensor` and `qml.ops.Hamiltonian`, and all of the structure surrounding them). **Description of the Change:** - [x] remove `qml.ops.Hamiltonian` - [x] remove `Tensor` - [x] remove `disable_` and `enable_new_opmath` - [x] remove `operation.convert_to_legacy_H` - [x] remove `qml.pauli.simplify` - [x] remove `PauliWord.hamiltonian` and `PauliSentence.hamiltonian`, and any other functionality in the pauli module that supported legacy opmath - [x] remove any if/else handling in place to deal with legacy opmath through the code base - [x] remove `legacy_opmath_only`, `new_opmath_only` and `use_legacy_and_new_opmath` test fixtures, and all irrelevant tests - [x] remove the warning suppressions added in [PR6287](https://github.com/PennyLaneAI/pennylane/pull/6287) - [x] remove the legacy test fixture (--disable-opmath) [sc-77523] --------- Co-authored-by: Christina Lee Co-authored-by: Astral Cai --- .github/workflows/interface-unit-tests.yml | 29 +- .github/workflows/legacy_op_math.yml | 16 - doc/conf.py | 2 +- doc/development/deprecations.rst | 70 +- doc/releases/changelog-dev.md | 6 + pennylane/__init__.py | 8 +- .../data/attributes/operator/operator.py | 18 +- pennylane/devices/_legacy_device.py | 45 +- pennylane/devices/default_clifford.py | 15 +- pennylane/devices/default_qubit.py | 12 - pennylane/devices/default_qutrit_mixed.py | 4 +- pennylane/devices/default_tensor.py | 9 +- pennylane/devices/preprocess.py | 13 +- pennylane/devices/qubit/measure.py | 28 +- pennylane/devices/qubit/sampling.py | 8 +- pennylane/devices/qutrit_mixed/measure.py | 26 +- pennylane/devices/qutrit_mixed/sampling.py | 13 +- pennylane/devices/tests/conftest.py | 18 - pennylane/devices/tests/test_measurements.py | 7 +- pennylane/gradients/hadamard_gradient.py | 7 +- pennylane/gradients/parameter_shift.py | 20 +- pennylane/measurements/probs.py | 2 +- pennylane/operation.py | 941 +------ .../ops/functions/bind_new_parameters.py | 22 +- pennylane/ops/functions/dot.py | 5 +- pennylane/ops/functions/eigvals.py | 10 - pennylane/ops/functions/equal.py | 80 +- pennylane/ops/functions/generator.py | 42 +- pennylane/ops/functions/is_commuting.py | 6 +- pennylane/ops/functions/matrix.py | 8 - pennylane/ops/op_math/adjoint.py | 6 +- pennylane/ops/op_math/composite.py | 2 +- pennylane/ops/op_math/exp.py | 17 +- pennylane/ops/op_math/linear_combination.py | 181 +- pennylane/ops/op_math/prod.py | 11 +- pennylane/ops/op_math/sprod.py | 5 +- pennylane/ops/op_math/sum.py | 8 +- pennylane/ops/op_math/symbolicop.py | 7 +- pennylane/ops/qubit/__init__.py | 2 - pennylane/ops/qubit/attributes.py | 8 +- pennylane/ops/qubit/hamiltonian.py | 916 ------- pennylane/optimize/riemannian_gradient.py | 6 +- pennylane/optimize/shot_adaptive.py | 2 +- pennylane/pauli/__init__.py | 1 - pennylane/pauli/conversion.py | 46 +- pennylane/pauli/grouping/group_observables.py | 27 +- pennylane/pauli/pauli_arithmetic.py | 55 +- pennylane/pauli/pauli_interface.py | 25 +- pennylane/pauli/utils.py | 176 +- pennylane/pulse/hardware_hamiltonian.py | 4 +- pennylane/pulse/parametrized_evolution.py | 2 +- pennylane/pulse/parametrized_hamiltonian.py | 4 +- pennylane/qaoa/layers.py | 5 +- pennylane/qaoa/mixers.py | 6 +- pennylane/qchem/convert.py | 42 +- pennylane/qchem/hamiltonian.py | 46 +- pennylane/qchem/observable_hf.py | 38 +- pennylane/qchem/tapering.py | 34 +- pennylane/qcut/cutcircuit.py | 4 +- pennylane/qcut/tapes.py | 7 +- pennylane/shadows/classical_shadow.py | 12 - pennylane/tape/tape.py | 5 +- pennylane/templates/subroutines/qdrift.py | 10 +- pennylane/templates/subroutines/trotter.py | 2 +- .../transforms/diagonalize_measurements.py | 51 - .../transforms/sign_expand/sign_expand.py | 2 +- pennylane/transforms/split_non_commuting.py | 20 +- pennylane/transforms/transpile.py | 10 +- tests/capture/test_operators.py | 2 - tests/capture/test_templates.py | 8 - .../circuit_graph/test_circuit_graph_hash.py | 7 +- tests/conftest.py | 56 - .../data/attributes/operator/test_operator.py | 57 +- tests/default_qubit_legacy.py | 12 +- .../default_qubit/test_default_qubit.py | 20 +- .../test_default_qubit_native_mcm.py | 6 - .../test_default_qubit_preprocessing.py | 42 - .../default_tensor/test_scalability.py | 12 - .../devices/default_tensor/test_tensor_var.py | 3 - tests/devices/qubit/test_measure.py | 29 +- tests/devices/qubit/test_sampling.py | 2 - tests/devices/qubit/test_simulate.py | 10 - .../qutrit_mixed/test_qutrit_mixed_measure.py | 1 - .../test_qutrit_mixed_preprocessing.py | 1 - .../test_qutrit_mixed_sampling.py | 10 +- .../test_qutrit_mixed_tracking.py | 1 - tests/devices/test_default_clifford.py | 1 - tests/devices/test_default_qutrit_mixed.py | 10 - tests/devices/test_legacy_device.py | 46 - tests/devices/test_null_qubit.py | 17 +- tests/devices/test_preprocess.py | 8 - tests/fermi/test_bravyi_kitaev.py | 436 ---- tests/fermi/test_fermi_mapping.py | 362 --- tests/fermi/test_parity_mapping.py | 446 ---- tests/gradients/core/test_adjoint_diff.py | 19 - tests/gradients/core/test_metric_tensor.py | 5 +- tests/gradients/core/test_pulse_gradient.py | 28 - .../parameter_shift/test_parameter_shift.py | 3 - .../test_parameter_shift_shot_vec.py | 3 +- tests/interfaces/test_autograd.py | 15 +- tests/interfaces/test_jax.py | 15 +- tests/interfaces/test_tensorflow.py | 13 +- tests/interfaces/test_torch.py | 15 +- tests/measurements/test_classical_shadow.py | 6 +- tests/ops/functions/conftest.py | 15 +- tests/ops/functions/test_assert_valid.py | 6 +- .../ops/functions/test_bind_new_parameters.py | 67 - tests/ops/functions/test_commutator.py | 27 +- tests/ops/functions/test_dot.py | 15 +- tests/ops/functions/test_eigvals.py | 18 - tests/ops/functions/test_equal.py | 236 +- tests/ops/functions/test_generator.py | 23 +- tests/ops/functions/test_is_commuting.py | 9 - tests/ops/op_math/test_adjoint.py | 48 +- tests/ops/op_math/test_composite.py | 25 +- tests/ops/op_math/test_evolution.py | 9 - tests/ops/op_math/test_exp.py | 26 - tests/ops/op_math/test_linear_combination.py | 45 - tests/ops/op_math/test_pow_op.py | 51 - tests/ops/op_math/test_sum.py | 3 - tests/ops/qubit/test_attributes.py | 4 - tests/ops/qubit/test_hamiltonian.py | 2195 ----------------- tests/ops/qubit/test_parametric_ops.py | 17 +- tests/ops/qubit/test_qchem_ops.py | 3 +- tests/ops/qutrit/test_qutrit_observables.py | 13 - tests/optimize/test_momentum_qng.py | 5 +- tests/optimize/test_qng.py | 4 +- .../grouping/test_pauli_group_observables.py | 30 - tests/pauli/test_conversion.py | 181 +- tests/pauli/test_pauli_arithmetic.py | 136 - tests/pauli/test_pauli_interface.py | 12 +- tests/pauli/test_pauli_utils.py | 135 +- tests/pulse/test_rydberg.py | 1 - tests/pytrees/test_pytrees.py | 1 - .../openfermion_pyscf_tests/test_convert.py | 27 +- .../test_convert_openfermion.py | 4 +- .../openfermion_pyscf_tests/test_dipole_of.py | 11 +- .../test_molecular_dipole.py | 14 +- .../test_molecular_hamiltonian.py | 47 +- tests/qchem/test_dipole.py | 5 +- tests/qchem/test_factorization.py | 2 - tests/qchem/test_hamiltonians.py | 13 +- tests/qchem/test_observable_hf.py | 7 +- tests/qchem/test_particle_number.py | 4 +- tests/qchem/test_spin.py | 9 +- tests/qchem/test_structure.py | 6 +- tests/qchem/test_tapering.py | 10 - tests/resource/test_specs.py | 4 +- tests/shadow/test_shadow_class.py | 17 +- tests/spin/test_spin_hamiltonian.py | 1 - tests/tape/test_tape.py | 17 - .../test_subroutines/test_qubitization.py | 34 - tests/test_operation.py | 1170 +-------- tests/test_qaoa.py | 129 +- tests/test_qnode_legacy.py | 1 - tests/test_queuing.py | 37 - tests/test_vqe.py | 75 - tests/transforms/test_add_noise.py | 21 +- .../test_convert_to_numpy_parameters.py | 6 +- tests/transforms/test_defer_measurements.py | 2 +- .../test_diagonalize_measurements.py | 73 - tests/transforms/test_insert_ops.py | 66 +- tests/transforms/test_qcut.py | 11 +- tests/transforms/test_split_non_commuting.py | 125 +- .../transforms/test_split_to_single_terms.py | 50 +- tests/transforms/test_transpile.py | 25 +- 166 files changed, 736 insertions(+), 9669 deletions(-) delete mode 100644 .github/workflows/legacy_op_math.yml delete mode 100644 pennylane/ops/qubit/hamiltonian.py delete mode 100644 tests/ops/qubit/test_hamiltonian.py diff --git a/.github/workflows/interface-unit-tests.yml b/.github/workflows/interface-unit-tests.yml index 814772a5d6b..ad7356b0a07 100644 --- a/.github/workflows/interface-unit-tests.yml +++ b/.github/workflows/interface-unit-tests.yml @@ -54,11 +54,6 @@ on: required: false type: string default: '' - disable_new_opmath: - description: Whether to disable the new op_math or not when running the tests - required: false - type: string - default: "False" additional_python_packages: description: Additional Python packages to install separated by a space required: false @@ -246,7 +241,7 @@ jobs: ${{ needs.default-dependency-versions.outputs.pytorch-version }} ${{ inputs.additional_python_packages }} additional_pip_packages_post: ${{ needs.default-dependency-versions.outputs.pennylane-lightning-latest }} - pytest_additional_args: --disable-opmath=${{ inputs.disable_new_opmath }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} + pytest_additional_args: -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} pytest_coverage_flags: ${{ inputs.pytest_coverage_flags }} pytest_markers: torch and not qcut and not finite-diff and not param-shift requirements_file: ${{ github.event_name == 'schedule' && strategy.job-index == 0 && 'torch.txt' || '' }} @@ -282,7 +277,7 @@ jobs: additional_pip_packages: ${{ inputs.additional_python_packages }} additional_pip_packages_post: ${{ needs.default-dependency-versions.outputs.pennylane-lightning-latest }} pytest_coverage_flags: ${{ inputs.pytest_coverage_flags }} - pytest_additional_args: --disable-opmath=${{ inputs.disable_new_opmath }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} + pytest_additional_args: -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} pytest_markers: autograd and not qcut and not finite-diff and not param-shift @@ -320,7 +315,7 @@ jobs: additional_pip_packages_post: ${{ needs.default-dependency-versions.outputs.pennylane-lightning-latest }} pytest_coverage_flags: ${{ inputs.pytest_coverage_flags }} pytest_markers: tf and not qcut and not finite-diff and not param-shift - pytest_additional_args: --splits 3 --group ${{ matrix.group }} --disable-opmath=${{ inputs.disable_new_opmath }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} + pytest_additional_args: --splits 3 --group ${{ matrix.group }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} pytest_durations_file_path: '.github/durations/tf_tests_durations.json' requirements_file: ${{ github.event_name == 'schedule' && strategy.job-index == 0 && 'tf.txt' || '' }} @@ -358,7 +353,7 @@ jobs: additional_pip_packages_post: ${{ needs.default-dependency-versions.outputs.pennylane-lightning-latest }} pytest_coverage_flags: ${{ inputs.pytest_coverage_flags }} pytest_markers: jax and not qcut and not finite-diff and not param-shift - pytest_additional_args: --dist=loadscope --splits 4 --group ${{ matrix.group }} --disable-opmath=${{ inputs.disable_new_opmath }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} + pytest_additional_args: --dist=loadscope --splits 4 --group ${{ matrix.group }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} pytest_durations_file_path: '.github/durations/jax_tests_durations.json' requirements_file: ${{ github.event_name == 'schedule' && strategy.job-index == 0 && 'jax.txt' || '' }} @@ -395,7 +390,7 @@ jobs: additional_pip_packages_post: ${{ needs.default-dependency-versions.outputs.pennylane-lightning-latest }} pytest_coverage_flags: ${{ inputs.pytest_coverage_flags }} pytest_markers: core and not qcut and not finite-diff and not param-shift - pytest_additional_args: --splits 6 --group ${{ matrix.group }} --disable-opmath=${{ inputs.disable_new_opmath }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} + pytest_additional_args: --splits 6 --group ${{ matrix.group }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} pytest_durations_file_path: '.github/durations/core_tests_durations.json' requirements_file: ${{ github.event_name == 'schedule' && strategy.job-index == 0 && 'core.txt' || '' }} @@ -435,7 +430,7 @@ jobs: additional_pip_packages_post: ${{ needs.default-dependency-versions.outputs.pennylane-lightning-latest }} pytest_coverage_flags: ${{ inputs.pytest_coverage_flags }} pytest_markers: all_interfaces - pytest_additional_args: --disable-opmath=${{ inputs.disable_new_opmath }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} + pytest_additional_args: -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} requirements_file: ${{ github.event_name == 'schedule' && strategy.job-index == 0 && 'all_interfaces.txt' || '' }} @@ -468,7 +463,7 @@ jobs: python_version: ${{ matrix.python-version }} pytest_coverage_flags: ${{ inputs.pytest_coverage_flags }} pytest_markers: external - pytest_additional_args: --disable-opmath=${{ inputs.disable_new_opmath }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} + pytest_additional_args: -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} additional_pip_packages: | pyzx matplotlib stim quimb mitiq ply git+https://github.com/PennyLaneAI/pennylane-qiskit.git@master @@ -515,7 +510,7 @@ jobs: python_version: ${{ matrix.python-version }} pytest_coverage_flags: ${{ inputs.pytest_coverage_flags }} pytest_markers: qcut - pytest_additional_args: --disable-opmath=${{ inputs.disable_new_opmath }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} + pytest_additional_args: -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} additional_pip_packages: | kahypar==1.1.7 opt_einsum @@ -557,7 +552,7 @@ jobs: additional_pip_packages_post: ${{ needs.default-dependency-versions.outputs.pennylane-lightning-latest }} pytest_coverage_flags: ${{ inputs.pytest_coverage_flags }} pytest_markers: qchem - pytest_additional_args: --disable-opmath=${{ inputs.disable_new_opmath }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} + pytest_additional_args: -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} additional_pip_packages: | openfermionpyscf basis-set-exchange ${{ inputs.additional_python_packages }} @@ -592,7 +587,7 @@ jobs: branch: ${{ inputs.branch }} coverage_artifact_name: gradients-${{ matrix.config.suite }}-coverage python_version: ${{ matrix.python-version }} - pytest_additional_args: --disable-opmath=${{ inputs.disable_new_opmath }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} + pytest_additional_args: -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} additional_pip_packages: | ${{ needs.default-dependency-versions.outputs.jax-version }} ${{ needs.default-dependency-versions.outputs.tensorflow-version }} @@ -632,7 +627,7 @@ jobs: python_version: ${{ matrix.python-version }} additional_pip_packages_post: ${{ needs.default-dependency-versions.outputs.pennylane-lightning-latest }} pytest_coverage_flags: ${{ inputs.pytest_coverage_flags }} - pytest_additional_args: --disable-opmath=${{ inputs.disable_new_opmath }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} + pytest_additional_args: -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} pytest_markers: data additional_pip_packages: | h5py @@ -681,7 +676,7 @@ jobs: additional_pip_packages_post: ${{ needs.default-dependency-versions.outputs.pennylane-lightning-latest }} pytest_test_directory: pennylane/devices/tests pytest_coverage_flags: ${{ inputs.pytest_coverage_flags }} - pytest_additional_args: --device=${{ matrix.config.device }} --shots=${{ matrix.config.shots }} --disable-opmath=${{ inputs.disable_new_opmath }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} + pytest_additional_args: --device=${{ matrix.config.device }} --shots=${{ matrix.config.shots }} -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} upload-to-codecov: diff --git a/.github/workflows/legacy_op_math.yml b/.github/workflows/legacy_op_math.yml deleted file mode 100644 index b78fe7684d6..00000000000 --- a/.github/workflows/legacy_op_math.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Legacy opmath tests - -on: - schedule: - - cron: "0 2 * * *" - workflow_dispatch: - -jobs: - tests: - uses: ./.github/workflows/interface-unit-tests.yml - secrets: - codecov_token: ${{ secrets.CODECOV_TOKEN }} - with: - branch: 'master' - run_lightened_ci: false - disable_new_opmath: "True" diff --git a/doc/conf.py b/doc/conf.py index 99063fdf06d..193c7b91270 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -113,8 +113,8 @@ # built documents. import pennylane +pennylane.Hamiltonian = pennylane.ops.op_math.linear_combination.Hamiltonian -pennylane.Hamiltonian = pennylane.ops.Hamiltonian # The full version, including alpha/beta/rc tags. release = pennylane.__version__ diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst index 34b053834d0..9fbcb25cb34 100644 --- a/doc/development/deprecations.rst +++ b/doc/development/deprecations.rst @@ -44,58 +44,47 @@ Pending deprecations - Deprecated in v0.39 - Will be removed in v0.40 -New operator arithmetic deprecations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In PennyLane v0.39, the legacy operator arithmetic system has been deprecated. Check out the :ref:`Updated operators ` page -for details on how to port your legacy code to the new system. The old system is still accessible via :func:`~.disable_new_opmath`, though -it is not recommended, as the old system is deprecated and will be removed in the v0.40 release. The following functionality will explicitly -raise a deprecation warning when used: - -* In PennyLane v0.39, legacy operator arithmetic has been deprecated. This includes :func:`~pennylane.operation.enable_new_opmath`, - :func:`~pennylane.operation.disable_new_opmath`, :class:`~pennylane.ops.Hamiltonian`, and :class:`~pennylane.operation.Tensor`. Note - that when new operator arithmetic is enabled, ``qml.Hamiltonian`` will continue to dispatch to :class:`~pennylane.ops.LinearCombination`; - this behaviour is not deprecated. - - - Deprecated in v0.39 - - Will be removed in v0.40 - -* :meth:`~pennylane.pauli.PauliSentence.hamiltonian` and :meth:`~pennylane.pauli.PauliWord.hamiltonian` are deprecated. Instead, please use - :meth:`~pennylane.pauli.PauliSentence.operation` and :meth:`~pennylane.pauli.PauliWord.operation` respectively. - - - Deprecated in v0.39 - - Will be removed in v0.40 - -* :func:`pennylane.pauli.simplify` is deprecated. Instead, please use :func:`pennylane.simplify` or :meth:`~pennylane.operation.Operator.simplify`. - - - Deprecated in v0.39 - - Will be removed in v0.40 - -* ``op.ops`` and ``op.coeffs`` will be deprecated in the future. Use +* ``op.ops`` and ``op.coeffs`` for ``Sum`` and ``Prod`` will be removed in the future. Use :meth:`~.Operator.terms` instead. - - Added and deprecated for ``Sum`` and ``Prod`` instances in v0.35 + - deprecated in v0.35 * Accessing terms of a tensor product (e.g., ``op = X(0) @ X(1)``) via ``op.obs`` is deprecated with new operator arithmetic. A user should use :class:`op.operands <~.CompositeOp>` instead. - Deprecated in v0.36 - -Other deprecations -~~~~~~~~~~~~~~~~~~ - -* PennyLane Lightning and Catalyst will no longer support ``manylinux2014`` (GLIBC 2.17) compatibile Linux operating systems, and will be migrated to ``manylinux_2_28`` (GLIBC 2.28). See `pypa/manylinux `_ for additional details. - - - Last supported version of ``manylinux2014`` with v0.36 - - Fully migrated to ``manylinux_2_28`` with v0.37 - * ``MultiControlledX`` is the only controlled operation that still supports specifying control values with a bit string. In the future, it will no longer accepts strings as control values. - Deprecated in v0.36 - Will be removed in v0.37 +Completed removal of legacy operator arithmetic +----------------------------------------------- + +In PennyLane v0.40, the legacy operator arithmetic system has been removed, and is fully replaced by the new +operator arithmetic functionality that was introduced in v0.36. Check out the :ref:`Updated operators ` page +for details on how to port your legacy code to the new system. The following functionality has been removed: + +* In PennyLane v0.40, legacy operator arithmetic has been removed. This includes :func:`~pennylane.operation.enable_new_opmath`, + :func:`~pennylane.operation.disable_new_opmath`, :class:`~pennylane.ops.Hamiltonian`, and :class:`~pennylane.operation.Tensor`. Note + that ``qml.Hamiltonian`` will continue to dispatch to :class:`~pennylane.ops.LinearCombination`. + + - Deprecated in v0.39 + - Removed in v0.40 + +* :meth:`~pennylane.pauli.PauliSentence.hamiltonian` and :meth:`~pennylane.pauli.PauliWord.hamiltonian` has been removed. Instead, please use + :meth:`~pennylane.pauli.PauliSentence.operation` and :meth:`~pennylane.pauli.PauliWord.operation` respectively. + + - Deprecated in v0.39 + - Removed in v0.40 + +* :func:`pennylane.pauli.simplify` has been removed. Instead, please use :func:`pennylane.simplify` or :meth:`~pennylane.operation.Operator.simplify`. + + - Deprecated in v0.39 + - Removed in v0.40 + Completed deprecation cycles ---------------------------- @@ -164,6 +153,11 @@ Completed deprecation cycles - Deprecated in v0.39 - Removed in v0.40 +* PennyLane Lightning and Catalyst will no longer support ``manylinux2014`` (GLIBC 2.17) compatibile Linux operating systems, and will be migrated to ``manylinux_2_28`` (GLIBC 2.28). See `pypa/manylinux `_ for additional details. + + - Last supported version of ``manylinux2014`` with v0.36 + - Fully migrated to ``manylinux_2_28`` with v0.37 + * The ``simplify`` argument in ``qml.Hamiltonian`` and ``qml.ops.LinearCombination`` has been removed. Instead, ``qml.simplify()`` can be called on the constructed operator. diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index d7c504dfed4..3f1332c3261 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -66,6 +66,12 @@

Breaking changes 💔

+* Legacy operator arithmetic has been removed. This includes `qml.ops.Hamiltonian`, `qml.operation.Tensor`, + `qml.operation.enable_new_opmath`, `qml.operation.disable_new_opmath`, and `qml.operation.convert_to_legacy_H`. + Note that `qml.Hamiltonian` will continue to dispatch to `qml.ops.LinearCombination`. For more information, + check out the [updated operator troubleshooting page](https://docs.pennylane.ai/en/stable/news/new_opmath.html). + [(#6548)](https://github.com/PennyLaneAI/pennylane/pull/6548) + * The developer-facing `qml.utils` module has been removed. Specifically, the following 4 sets of functions have been either moved or removed[(#6588)](https://github.com/PennyLaneAI/pennylane/pull/6588): diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 1e71e9d1691..334b860e492 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -74,6 +74,7 @@ ) from pennylane.ops import * from pennylane.ops import adjoint, ctrl, cond, exp, sum, pow, prod, s_prod +from pennylane.ops import LinearCombination as Hamiltonian from pennylane.templates import layer from pennylane.templates.embeddings import * from pennylane.templates.layers import * @@ -165,14 +166,7 @@ class PennyLaneDeprecationWarning(UserWarning): """Warning raised when a PennyLane feature is being deprecated.""" -del globals()["Hamiltonian"] - - def __getattr__(name): - if name == "Hamiltonian": - if pennylane.operation.active_new_opmath(): - return pennylane.ops.LinearCombination - return pennylane.ops.Hamiltonian if name == "plugin_devices": return pennylane.devices.device_constructor.plugin_devices diff --git a/pennylane/data/attributes/operator/operator.py b/pennylane/data/attributes/operator/operator.py index 1a048c0c280..c9685640df7 100644 --- a/pennylane/data/attributes/operator/operator.py +++ b/pennylane/data/attributes/operator/operator.py @@ -24,7 +24,7 @@ import pennylane as qml from pennylane.data.base.attribute import DatasetAttribute from pennylane.data.base.hdf5 import HDF5Group, h5py -from pennylane.operation import Operator, Tensor +from pennylane.operation import Operator from ._wires import wires_to_json @@ -44,8 +44,6 @@ class DatasetOperator(Generic[Op], DatasetAttribute[HDF5Group, Op, Op]): arguments. - Hyperparameters are not used or are automatically derived by ``__init__()``. - Almost all operators meet these conditions. This type also supports serializing the - ``Hamiltonian`` and ``Tensor`` operators. """ type_id = "operator" @@ -56,13 +54,9 @@ def supported_ops(cls) -> frozenset[Type[Operator]]: """Set of supported operators.""" return frozenset( ( - # pennylane/operation/Tensor - Tensor, # pennylane/ops/qubit/arithmetic_qml.py qml.QubitCarry, qml.QubitSum, - # pennylane/ops/qubit/hamiltonian.py - qml.ops.Hamiltonian, # pennylane/ops/op_math/linear_combination.py qml.ops.LinearCombination, # pennylane/ops/op_math - prod.py, s_prod.py, sum.py @@ -219,10 +213,7 @@ def _ops_to_hdf5( f"Serialization of operator type '{type(op).__name__}' is not supported." ) - if isinstance(op, Tensor): - self._ops_to_hdf5(bind, op_key, op.obs) - op_wire_labels.append("null") - elif isinstance(op, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): + if isinstance(op, qml.ops.LinearCombination): coeffs, ops = op.terms() ham_grp = self._ops_to_hdf5(bind, op_key, ops) ham_grp["hamiltonian_coeffs"] = coeffs @@ -260,10 +251,7 @@ def _hdf5_to_ops(self, bind: HDF5Group) -> list[Operator]: op_key = f"op_{i}" op_cls = self._supported_ops_dict()[op_class_name] - if op_cls is Tensor: - prod_op = qml.ops.Prod if qml.operation.active_new_opmath() else Tensor - ops.append(prod_op(*self._hdf5_to_ops(bind[op_key]))) - elif op_cls in (qml.ops.Hamiltonian, qml.ops.LinearCombination): + if op_cls is qml.ops.LinearCombination: ops.append( qml.Hamiltonian( coeffs=list(bind[op_key]["hamiltonian_coeffs"]), diff --git a/pennylane/devices/_legacy_device.py b/pennylane/devices/_legacy_device.py index dd1b97dd4a5..93342f9d2e2 100644 --- a/pennylane/devices/_legacy_device.py +++ b/pennylane/devices/_legacy_device.py @@ -35,8 +35,8 @@ State, Variance, ) -from pennylane.operation import Observable, Operation, Operator, StatePrepBase, Tensor -from pennylane.ops import Hamiltonian, LinearCombination, Prod, SProd, Sum +from pennylane.operation import Observable, Operation, Operator, StatePrepBase +from pennylane.ops import LinearCombination, Prod, SProd, Sum from pennylane.queuing import QueuingManager from pennylane.tape import QuantumScript, expand_tape_state_prep from pennylane.wires import WireError, Wires @@ -465,22 +465,18 @@ def execute(self, queue, observables, parameters=None, **kwargs): for mp in observables: obs = mp.obs if isinstance(mp, MeasurementProcess) and mp.obs is not None else mp - if isinstance(obs, Tensor): - wires = [ob.wires for ob in obs.obs] - else: - wires = obs.wires if mp.return_type is Expectation: - results.append(self.expval(obs.name, wires, obs.parameters)) + results.append(self.expval(obs.name, obs.wires, obs.parameters)) elif mp.return_type is Variance: - results.append(self.var(obs.name, wires, obs.parameters)) + results.append(self.var(obs.name, obs.wires, obs.parameters)) elif mp.return_type is Sample: - results.append(np.array(self.sample(obs.name, wires, obs.parameters))) + results.append(np.array(self.sample(obs.name, obs.wires, obs.parameters))) elif mp.return_type is Probability: - results.append(list(self.probability(wires=wires).values())) + results.append(list(self.probability(wires=obs.wires).values())) elif mp.return_type is State: raise qml.QuantumFunctionError("Returning the state is not supported") @@ -680,7 +676,7 @@ def default_expand_fn(self, circuit, max_expansion=10): ) obs_on_same_wire = len(circuit.obs_sharing_wires) > 0 or comp_basis_sampled_multi_measure obs_on_same_wire &= not any( - isinstance(o, (Hamiltonian, LinearCombination)) for o in circuit.obs_sharing_wires + isinstance(o, LinearCombination) for o in circuit.obs_sharing_wires ) ops_not_supported = not all(self.stopping_condition(op) for op in circuit.operations) @@ -753,11 +749,11 @@ def null_postprocess(results): is_analytic_or_shadow = not finite_shots or has_shadow all_obs_usable = self._all_multi_term_obs_supported(circuit) exists_multi_term_obs = any( - isinstance(m.obs, (Hamiltonian, Sum, Prod, SProd)) for m in circuit.measurements + isinstance(m.obs, (Sum, Prod, SProd)) for m in circuit.measurements ) has_overlapping_wires = len(circuit.obs_sharing_wires) > 0 single_hamiltonian = len(circuit.measurements) == 1 and isinstance( - circuit.measurements[0].obs, (Hamiltonian, Sum) + circuit.measurements[0].obs, Sum ) single_hamiltonian_with_grouping_known = ( single_hamiltonian and circuit.measurements[0].obs.grouping_indices is not None @@ -779,10 +775,10 @@ def null_postprocess(results): elif single_hamiltonian_with_grouping_known: # Use qwc grouping if the circuit contains a single measurement of a - # Hamiltonian/Sum with grouping indices already calculated. + # Sum with grouping indices already calculated. circuits, processing_fn = qml.transforms.split_non_commuting(circuit, "qwc") - elif any(isinstance(m.obs, (Hamiltonian, LinearCombination)) for m in circuit.measurements): + elif any(isinstance(m.obs, LinearCombination) for m in circuit.measurements): # Otherwise, use wire-based grouping if the circuit contains a Hamiltonian # that is potentially very large. @@ -821,7 +817,6 @@ def _all_multi_term_obs_supported(self, circuit): return False if mp.obs.name in ( - "Hamiltonian", "Sum", "Prod", "SProd", @@ -1006,23 +1001,7 @@ def check_validity(self, queue, observables): if o is None: continue - if isinstance(o, Tensor): - # TODO: update when all capabilities keys changed to "supports_tensor_observables" - supports_tensor = self.capabilities().get( - "supports_tensor_observables", False - ) or self.capabilities().get("tensor_observables", False) - if not supports_tensor: - raise qml.DeviceError( - f"Tensor observables not supported on device {self.short_name}" - ) - - for i in o.obs: - if not self.supports_observable(i.name): - raise qml.DeviceError( - f"Observable {i.name} not supported on device {self.short_name}" - ) - - elif isinstance(o, qml.ops.Prod): + if isinstance(o, qml.ops.Prod): supports_prod = self.supports_observable(o.name) if not supports_prod: diff --git a/pennylane/devices/default_clifford.py b/pennylane/devices/default_clifford.py index 5d6ecdbd69a..a3fb9d941a2 100644 --- a/pennylane/devices/default_clifford.py +++ b/pennylane/devices/default_clifford.py @@ -73,7 +73,6 @@ "Hermitian", "Identity", "Projector", - "Hamiltonian", "LinearCombination", "Sum", "SProd", @@ -147,10 +146,9 @@ def _pl_op_to_stim(op): return stim_op, " ".join(stim_tg) -def _pl_obs_to_linear_comb(meas_op): +def _pl_obs_to_linear_comb(meas_obs): """Convert a PennyLane observable to a linear combination of Pauli strings""" - meas_obs = qml.operation.convert_to_opmath(meas_op) meas_rep = meas_obs.pauli_rep # Use manual decomposition for enabling Hermitian and partial Projector support @@ -160,11 +158,11 @@ def _pl_obs_to_linear_comb(meas_op): # A Pauli decomposition for the observable must exist if meas_rep is None: raise NotImplementedError( - f"default.clifford doesn't support expectation value calculation with {type(meas_op).__name__} at the moment." + f"default.clifford doesn't support expectation value calculation with {type(meas_obs).__name__} at the moment." ) coeffs, paulis = np.array(list(meas_rep.values())), [] - meas_op_wires = list(meas_op.wires) + meas_op_wires = list(meas_obs.wires) for pw in meas_rep: p_wire, p_word = pw.keys(), pw.values() if not p_word: @@ -792,8 +790,7 @@ def _measure_expectation(self, meas, tableau_simulator, **kwargs): def _measure_variance(self, meas, tableau_simulator, **_): """Measure the variance with respect to the state of simulator device.""" - meas_obs = qml.operation.convert_to_opmath(meas.obs) - meas_obs1 = meas_obs.simplify() + meas_obs1 = meas.obs.simplify() meas_obs2 = (meas_obs1**2).simplify() # use the naive formula for variance, i.e., Var(Q) = ⟨𝑄^2⟩−⟨𝑄⟩^2 @@ -1029,9 +1026,7 @@ def _sample_expectation(self, meas, stim_circuit, shots, seed): def _sample_variance(self, meas, stim_circuit, shots, seed): """Measure the variance with respect to samples from simulator device.""" # Get the observable for the expectation value measurement - meas_op = meas.obs - meas_obs = qml.operation.convert_to_opmath(meas_op) - meas_obs1 = meas_obs.simplify() + meas_obs1 = meas.obs.simplify() meas_obs2 = (meas_obs1**2).simplify() # use the naive formula for variance, i.e., Var(Q) = ⟨𝑄^2⟩−⟨𝑄⟩^2 diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py index 13788e1e4c9..4d2f4d5a76e 100644 --- a/pennylane/devices/default_qubit.py +++ b/pennylane/devices/default_qubit.py @@ -91,12 +91,6 @@ def observable_accepts_sampling(obs: qml.operation.Operator) -> bool: if isinstance(obs, qml.ops.SymbolicOp): return observable_accepts_sampling(obs.base) - if isinstance(obs, qml.ops.Hamiltonian): - return all(observable_accepts_sampling(o) for o in obs.ops) - - if isinstance(obs, qml.operation.Tensor): - return all(observable_accepts_sampling(o) for o in obs.obs) - return obs.has_diagonalizing_gates @@ -109,12 +103,6 @@ def observable_accepts_analytic(obs: qml.operation.Operator, is_expval=False) -> if isinstance(obs, qml.ops.SymbolicOp): return observable_accepts_analytic(obs.base, is_expval) - if isinstance(obs, qml.ops.Hamiltonian): - return all(observable_accepts_analytic(o, is_expval) for o in obs.ops) - - if isinstance(obs, qml.operation.Tensor): - return all(observable_accepts_analytic(o, is_expval) for o in obs.obs) - if is_expval and isinstance(obs, (qml.ops.SparseHamiltonian, qml.ops.Hermitian)): return True diff --git a/pennylane/devices/default_qutrit_mixed.py b/pennylane/devices/default_qutrit_mixed.py index 01f341b8cc1..fbea3d0edee 100644 --- a/pennylane/devices/default_qutrit_mixed.py +++ b/pennylane/devices/default_qutrit_mixed.py @@ -55,11 +55,9 @@ def observable_stopping_condition(obs: qml.operation.Operator) -> bool: """Specifies whether an observable is accepted by DefaultQutritMixed.""" - if isinstance(obs, qml.operation.Tensor): - return all(observable_stopping_condition(observable) for observable in obs.obs) if obs.name in {"Prod", "Sum"}: return all(observable_stopping_condition(observable) for observable in obs.operands) - if obs.name in {"LinearCombination", "Hamiltonian"}: + if obs.name == "LinearCombination": return all(observable_stopping_condition(observable) for observable in obs.terms()[1]) if obs.name == "SProd": return observable_stopping_condition(obs.base) diff --git a/pennylane/devices/default_tensor.py b/pennylane/devices/default_tensor.py index 64046df1c2c..e6f349c2243 100644 --- a/pennylane/devices/default_tensor.py +++ b/pennylane/devices/default_tensor.py @@ -41,7 +41,7 @@ StateMP, VarianceMP, ) -from pennylane.operation import Observable, Operation, Tensor +from pennylane.operation import Observable, Operation from pennylane.ops import LinearCombination, Prod, SProd, Sum from pennylane.tape import QuantumScript, QuantumScriptOrBatch from pennylane.templates.subroutines.trotter import _recursive_expression @@ -125,7 +125,6 @@ "Identity", "Projector", "SparseHamiltonian", - "Hamiltonian", "LinearCombination", "Sum", "SProd", @@ -1028,12 +1027,6 @@ def expval_core(obs: Observable, device) -> float: return device._local_expectation(qml.matrix(obs), tuple(obs.wires)) -@expval_core.register -def expval_core_tensor(obs: Tensor, device) -> float: - """Computes the expval of a Tensor.""" - return expval_core(Prod(*obs._args), device) - - @expval_core.register def expval_core_prod(obs: Prod, device) -> float: """Computes the expval of a Prod.""" diff --git a/pennylane/devices/preprocess.py b/pennylane/devices/preprocess.py index beec009529f..0d75b88284f 100644 --- a/pennylane/devices/preprocess.py +++ b/pennylane/devices/preprocess.py @@ -26,7 +26,7 @@ import pennylane as qml from pennylane import Snapshot, transform from pennylane.measurements import SampleMeasurement, StateMeasurement -from pennylane.operation import StatePrepBase, Tensor +from pennylane.operation import StatePrepBase from pennylane.tape import QuantumScript, QuantumScriptBatch from pennylane.typing import PostprocessingFn from pennylane.wires import WireError @@ -458,17 +458,10 @@ def validate_observables( >>> validate_observables(tape, accepted_observable) qml.DeviceError: Observable Z(0) + Y(0) not supported on device - Note that if the observable is a :class:`~.Tensor`, the validation is run on each object in the - ``Tensor`` instead. - """ for m in tape.measurements: - if m.obs is not None: - if isinstance(m.obs, Tensor): - if any(not stopping_condition(o) for o in m.obs.obs): - raise qml.DeviceError(f"Observable {repr(m.obs)} not supported on {name}") - elif not stopping_condition(m.obs): - raise qml.DeviceError(f"Observable {repr(m.obs)} not supported on {name}") + if m.obs is not None and not stopping_condition(m.obs): + raise qml.DeviceError(f"Observable {repr(m.obs)} not supported on {name}") return (tape,), null_postprocessing diff --git a/pennylane/devices/qubit/measure.py b/pennylane/devices/qubit/measure.py index da4d51b7aec..2e4745af378 100644 --- a/pennylane/devices/qubit/measure.py +++ b/pennylane/devices/qubit/measure.py @@ -18,7 +18,6 @@ from scipy.sparse import csr_matrix -import pennylane as qml from pennylane import math from pennylane.measurements import ( ExpectationMP, @@ -26,7 +25,7 @@ MeasurementValue, StateMeasurement, ) -from pennylane.ops import Hamiltonian, LinearCombination, Sum +from pennylane.ops import LinearCombination, Sum from pennylane.pauli.conversion import is_pauli_sentence, pauli_sentence from pennylane.typing import TensorLike from pennylane.wires import Wires @@ -144,7 +143,7 @@ def full_dot_products( def sum_of_terms_method( measurementprocess: ExpectationMP, state: TensorLike, is_state_batched: bool = False ) -> TensorLike: - """Measure the expecation value of the state when the measured observable is a ``Hamiltonian`` or ``Sum`` + """Measure the expectation value of the state when the measured observable is a ``Hamiltonian`` or ``Sum`` and it must be backpropagation compatible. Args: @@ -155,17 +154,11 @@ def sum_of_terms_method( Returns: TensorLike: the result of the measurement """ - if isinstance(measurementprocess.obs, Sum): - # Recursively call measure on each term, so that the best measurement method can - # be used for each term - return sum( - measure(ExpectationMP(term), state, is_state_batched=is_state_batched) - for term in measurementprocess.obs - ) - # else hamiltonian + # Recursively call measure on each term, so that the best measurement method can + # be used for each term return sum( - c * measure(ExpectationMP(t), state, is_state_batched=is_state_batched) - for c, t in zip(*measurementprocess.obs.terms()) + measure(ExpectationMP(term), state, is_state_batched=is_state_batched) + for term in measurementprocess.obs ) @@ -195,7 +188,7 @@ def get_measurement_function( return full_dot_products backprop_mode = math.get_interface(state, *measurementprocess.obs.data) != "numpy" - if isinstance(measurementprocess.obs, (Hamiltonian, LinearCombination)): + if isinstance(measurementprocess.obs, LinearCombination): # need to work out thresholds for when it's faster to use "backprop mode" if backprop_mode: @@ -204,13 +197,6 @@ def get_measurement_function( if not all(obs.has_sparse_matrix for obs in measurementprocess.obs.terms()[1]): return sum_of_terms_method - # Hamiltonian.sparse_matrix raises a ValueError for this scenario. - if isinstance(measurementprocess.obs, Hamiltonian) and any( - any(len(o.wires) > 1 for o in qml.operation.Tensor(op).obs) - for op in measurementprocess.obs.ops - ): - return sum_of_terms_method - return csr_dot_products if isinstance(measurementprocess.obs, Sum): diff --git a/pennylane/devices/qubit/sampling.py b/pennylane/devices/qubit/sampling.py index 85c89de3701..fbdba2dae2f 100644 --- a/pennylane/devices/qubit/sampling.py +++ b/pennylane/devices/qubit/sampling.py @@ -25,7 +25,7 @@ ShadowExpvalMP, Shots, ) -from pennylane.ops import Hamiltonian, LinearCombination, Prod, SProd, Sum +from pennylane.ops import LinearCombination, Prod, SProd, Sum from pennylane.typing import TensorLike from .apply_operation import apply_operation @@ -158,7 +158,7 @@ def get_num_shots_and_executions(tape: qml.tape.QuantumScript) -> tuple[int, int num_shots = 0 for group in groups: if isinstance(group[0], ExpectationMP) and isinstance( - group[0].obs, (qml.ops.Hamiltonian, qml.ops.LinearCombination) + group[0].obs, qml.ops.LinearCombination ): H_executions = _get_num_executions_for_expval_H(group[0].obs) num_executions += H_executions @@ -238,9 +238,7 @@ def measure_with_samples( groups, indices = _group_measurements(mps) all_res = [] for group in groups: - if isinstance(group[0], ExpectationMP) and isinstance( - group[0].obs, (Hamiltonian, LinearCombination) - ): + if isinstance(group[0], ExpectationMP) and isinstance(group[0].obs, LinearCombination): measure_fn = _measure_hamiltonian_with_samples elif isinstance(group[0], ExpectationMP) and isinstance(group[0].obs, Sum): measure_fn = _measure_sum_with_samples diff --git a/pennylane/devices/qutrit_mixed/measure.py b/pennylane/devices/qutrit_mixed/measure.py index 3489cb3ee35..1feefa0ec3d 100644 --- a/pennylane/devices/qutrit_mixed/measure.py +++ b/pennylane/devices/qutrit_mixed/measure.py @@ -27,7 +27,7 @@ StateMP, VarianceMP, ) -from pennylane.ops import Hamiltonian, Sum +from pennylane.ops import Sum from pennylane.typing import TensorLike from pennylane.wires import Wires @@ -221,28 +221,16 @@ def calculate_expval_sum_of_terms( Returns: TensorLike: the expectation value of the sum of Hamiltonian observable wrt the state. """ - if isinstance(measurementprocess.obs, Sum): - # Recursively call measure on each term, so that the best measurement method can - # be used for each term - return sum( - measure( - ExpectationMP(term), - state, - is_state_batched=is_state_batched, - readout_errors=readout_errors, - ) - for term in measurementprocess.obs - ) - # else hamiltonian + # Recursively call measure on each term, so that the best measurement method can + # be used for each term return sum( - c - * measure( - ExpectationMP(t), + measure( + ExpectationMP(term), state, is_state_batched=is_state_batched, readout_errors=readout_errors, ) - for c, t in zip(*measurementprocess.obs.terms()) + for term in measurementprocess.obs ) @@ -262,7 +250,7 @@ def get_measurement_function( """ if isinstance(measurementprocess, StateMeasurement): if isinstance(measurementprocess, ExpectationMP): - if isinstance(measurementprocess.obs, (Hamiltonian, Sum)): + if isinstance(measurementprocess.obs, Sum): return calculate_expval_sum_of_terms if measurementprocess.obs.has_matrix: return calculate_expval diff --git a/pennylane/devices/qutrit_mixed/sampling.py b/pennylane/devices/qutrit_mixed/sampling.py index 0e5e661f085..f7ad19c97d1 100644 --- a/pennylane/devices/qutrit_mixed/sampling.py +++ b/pennylane/devices/qutrit_mixed/sampling.py @@ -194,14 +194,11 @@ def _measure_sum_with_samples( prng_key=None, readout_errors: list[Callable] = None, ): - """Compute expectation values of Sum or Hamiltonian Observables""" - # mp.obs returns is the list of observables for Sum, - # mp.obs.terms()[1] returns is the list of observables for Hamiltonian - obs_terms = mp.obs if isinstance(mp.obs, Sum) else mp.obs.terms()[1] + """Compute expectation values of Sum Observables""" def _sum_for_single_shot(s): results = [] - for term in obs_terms: + for term in mp.obs: results.append( measure_with_samples( ExpectationMP(term), @@ -214,10 +211,6 @@ def _sum_for_single_shot(s): ) ) - if isinstance(mp.obs, qml.ops.Hamiltonian): - # If Hamiltonian apply coefficients - return sum((c * res for c, res in zip(mp.obs.terms()[0], results))) - return sum(results) if shots.has_partitioned_shots: @@ -462,7 +455,7 @@ def measure_with_samples( TensorLike[Any]: Sample measurement results """ - if isinstance(mp, ExpectationMP) and isinstance(mp.obs, (qml.ops.Hamiltonian, Sum)): + if isinstance(mp, ExpectationMP) and isinstance(mp.obs, Sum): measure_fn = _measure_sum_with_samples else: # measure with the usual method (rotate into the measurement basis) diff --git a/pennylane/devices/tests/conftest.py b/pennylane/devices/tests/conftest.py index bcd48a3f164..217aca0938e 100755 --- a/pennylane/devices/tests/conftest.py +++ b/pennylane/devices/tests/conftest.py @@ -14,7 +14,6 @@ """Contains shared fixtures for the device tests.""" import argparse import os -from warnings import warn import numpy as np import pytest @@ -219,23 +218,6 @@ def pytest_addoption(parser): ) -# pylint: disable=eval-used -@pytest.fixture(scope="session", autouse=True) -def disable_opmath_if_requested(request): - """Check the value of the --disable-opmath option and turn off - if True before running the tests""" - disable_opmath = request.config.getoption("--disable-opmath") - # value from yaml file is a string, convert to boolean - if eval(disable_opmath): - warn( - "Disabling the new Operator arithmetic system for legacy support. " - "If you need help troubleshooting your code, please visit " - "https://docs.pennylane.ai/en/stable/news/new_opmath.html", - UserWarning, - ) - qml.operation.disable_new_opmath(warn=False) - - def pytest_generate_tests(metafunc): """Set up device_kwargs fixture from command line options. diff --git a/pennylane/devices/tests/test_measurements.py b/pennylane/devices/tests/test_measurements.py index 372ec5c8084..ef0bfdb7f94 100644 --- a/pennylane/devices/tests/test_measurements.py +++ b/pennylane/devices/tests/test_measurements.py @@ -51,7 +51,6 @@ qml.Projector(np.array([0, 1]), wires=[0]), ], "SparseHamiltonian": qml.SparseHamiltonian(csr_matrix(np.eye(8)), wires=[0, 1, 2]), - "Hamiltonian": qml.Hamiltonian([1, 1], [qml.Z(0), qml.X(0)]), "Prod": qml.prod(qml.X(0), qml.Z(1)), "SProd": qml.s_prod(0.1, qml.Z(0)), "Sum": qml.sum(qml.s_prod(0.1, qml.Z(0)), qml.prod(qml.X(0), qml.Z(1))), @@ -160,9 +159,7 @@ def circuit(): class TestHamiltonianSupport: """Separate test to ensure that the device can differentiate Hamiltonian observables.""" - @pytest.mark.parametrize("ham_constructor", [qml.ops.Hamiltonian, qml.ops.LinearCombination]) - @pytest.mark.filterwarnings("ignore::pennylane.PennyLaneDeprecationWarning") - def test_hamiltonian_diff(self, ham_constructor, device_kwargs, tol): + def test_hamiltonian_diff(self, device_kwargs, tol): """Tests a simple VQE gradient using parameter-shift rules.""" device_kwargs["wires"] = 1 @@ -175,7 +172,7 @@ def circuit(coeffs, param): qml.RX(param, wires=0) qml.RY(param, wires=0) return qml.expval( - ham_constructor( + qml.Hamiltonian( coeffs, [qml.X(0), qml.Z(0)], ) diff --git a/pennylane/gradients/hadamard_gradient.py b/pennylane/gradients/hadamard_gradient.py index beb9faf828f..5335bd0a060 100644 --- a/pennylane/gradients/hadamard_gradient.py +++ b/pennylane/gradients/hadamard_gradient.py @@ -371,17 +371,14 @@ def _expval_hadamard_grad(tape, argnum, aux_wire): measurements = [] # Add the Y measurement on the aux qubit for m in tape.measurements: - if isinstance(m.obs, qml.operation.Tensor): - obs_new = m.obs.obs.copy() - elif m.obs: + if m.obs: obs_new = [m.obs] else: m_wires = m.wires if len(m.wires) > 0 else tape.wires obs_new = [qml.Z(i) for i in m_wires] obs_new.append(qml.Y(aux_wire)) - obs_type = qml.prod if qml.operation.active_new_opmath() else qml.operation.Tensor - obs_new = obs_type(*obs_new) + obs_new = qml.prod(*obs_new) if isinstance(m, qml.measurements.ExpectationMP): measurements.append(qml.expval(op=obs_new)) diff --git a/pennylane/gradients/parameter_shift.py b/pennylane/gradients/parameter_shift.py index 63fe21f9eb8..5c25237db58 100644 --- a/pennylane/gradients/parameter_shift.py +++ b/pennylane/gradients/parameter_shift.py @@ -62,15 +62,6 @@ def _square_observable(obs): """Returns the square of an observable.""" - if isinstance(obs, qml.operation.Tensor): - # Observable is a tensor, we must consider its - # component observables independently. Note that - # we assume all component observables are on distinct wires. - components_squared = [ - NONINVOLUTORY_OBS[o.name](o) for o in obs.obs if o.name in NONINVOLUTORY_OBS - ] - return qml.operation.Tensor(*components_squared) - if isinstance(obs, qml.ops.Prod): components_squared = [ NONINVOLUTORY_OBS[o.name](o) for o in obs if o.name in NONINVOLUTORY_OBS @@ -380,7 +371,7 @@ def expval_param_shift( op, op_idx, _ = tape.get_operation(idx) - if op.name in ["Hamiltonian", "LinearCombination"]: + if op.name == "LinearCombination": # operation is a Hamiltonian if tape[op_idx].return_type is not qml.measurements.Expectation: raise ValueError( @@ -634,12 +625,7 @@ def _get_non_involuntory_indices(tape, var_indices): for i in var_indices: obs = tape.measurements[i].obs - if isinstance(obs, qml.operation.Tensor): - # Observable is a tensor product, we must investigate all constituent observables. - if any(o.name in NONINVOLUTORY_OBS for o in tape.measurements[i].obs.obs): - non_involutory_indices.append(i) - - elif isinstance(tape.measurements[i].obs, qml.ops.Prod): + if isinstance(tape.measurements[i].obs, qml.ops.Prod): if any(o.name in NONINVOLUTORY_OBS for o in tape.measurements[i].obs): non_involutory_indices.append(i) @@ -695,7 +681,7 @@ def var_param_shift(tape, argnum, shifts=None, gradient_recipes=None, f0=None, b for i in var_indices: obs = new_measurements[i].obs new_measurements[i] = qml.expval(op=obs) - if obs.name in ["Hamiltonian", "LinearCombination", "Sum"]: + if obs.name in ["LinearCombination", "Sum"]: first_obs_idx = len(tape.operations) for t_idx in reversed(range(len(tape.trainable_params))): op, op_idx, _ = tape.get_operation(t_idx) diff --git a/pennylane/measurements/probs.py b/pennylane/measurements/probs.py index 64e9e86bffb..77c9f61e4df 100644 --- a/pennylane/measurements/probs.py +++ b/pennylane/measurements/probs.py @@ -116,7 +116,7 @@ def circuit(): return ProbabilityMP(obs=op) - if isinstance(op, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): + if isinstance(op, qml.ops.LinearCombination): raise qml.QuantumFunctionError("Hamiltonians are not supported for rotating probabilities.") if op is not None and not qml.math.is_abstract(op) and not op.has_diagonalizing_gates: diff --git a/pennylane/operation.py b/pennylane/operation.py index 1cc41052f6f..10452589156 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -120,12 +120,11 @@ ~CVObservable ~CVOperation ~Channel - ~Tensor ~StatePrepBase .. currentmodule:: pennylane.operation -.. inheritance-diagram:: Operator Operation Observable Channel CV CVObservable CVOperation Tensor StatePrepBase +.. inheritance-diagram:: Operator Operation Observable Channel CV CVObservable CVOperation StatePrepBase :parts: 1 Errors @@ -173,27 +172,6 @@ ~is_trainable ~not_tape -Enabling New Arithmetic Operators -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -PennyLane is in the process of replacing :class:`~pennylane.Hamiltonian` and :class:`~.Tensor` -with newer, more general arithmetic operators. These consist of :class:`~pennylane.ops.op_math.Prod`, -:class:`~pennylane.ops.op_math.Sum` and :class:`~pennylane.ops.op_math.SProd`. By default, using dunder -methods (eg. ``+``, ``-``, ``@``, ``*``) to combine operators with scalars or other operators will -create the aforementioned newer operators. To toggle the dunders to return the older arithmetic operators, -the ``operation`` module provides the following helper functions: - -.. currentmodule:: pennylane.operation - -.. autosummary:: - :toctree: api - - ~enable_new_opmath - ~disable_new_opmath - ~active_new_opmath - ~convert_to_opmath - ~convert_to_legacy_H - Other ~~~~~ @@ -244,17 +222,13 @@ # pylint:disable=access-member-before-definition,global-statement import abc import copy -import functools -import itertools import warnings from collections.abc import Hashable, Iterable -from contextlib import contextmanager from enum import IntEnum from typing import Any, Callable, Literal, Optional, Union import numpy as np -from numpy.linalg import multi_dot -from scipy.sparse import coo_matrix, csr_matrix, eye, kron +from scipy.sparse import csr_matrix import pennylane as qml from pennylane.capture import ABCCaptureMeta, create_operator_primitive @@ -270,7 +244,6 @@ # ============================================================================= SUPPORTED_INTERFACES = {"numpy", "scipy", "autograd", "torch", "tensorflow", "jax"} -__use_new_opmath = True _UNSET_BATCH_SIZE = -1 # indicates that the (lazy) batch size has not yet been accessed/computed @@ -1154,7 +1127,6 @@ def __init__( ( qml.Barrier, qml.Snapshot, - qml.ops.Hamiltonian, qml.ops.LinearCombination, qml.GlobalPhase, qml.Identity, @@ -1956,8 +1928,9 @@ def _queue_category(self) -> Literal["_ops", "_measurements", None]: * `"_measurements"` * None - Non-pauli observables, like Tensor, Hermitian, and Hamiltonian, should not be processed into any queue. - The Pauli observables double as Operations, and should therefore be processed into `_ops` if unowned. + Non-pauli observables like Hermitian should not be processed into any queue. + The Pauli observables double as Operations, and should therefore be processed + into `_ops` if unowned. """ return "_ops" if isinstance(self, Operation) else None @@ -1966,50 +1939,13 @@ def is_hermitian(self) -> bool: """All observables must be hermitian""" return True - def __matmul__(self, other: Operator) -> Operator: - if active_new_opmath(): - return super().__matmul__(other=other) - - if isinstance(other, (Tensor, qml.ops.Hamiltonian, qml.ops.LinearCombination)): - return other.__rmatmul__(self) - - if isinstance(other, Observable): - return Tensor(self, other) - - return super().__matmul__(other=other) - - def _obs_data(self) -> set[tuple[str, Wires, tuple[int, ...]]]: - r"""Extracts the data from a Observable or Tensor and serializes it in an order-independent fashion. - - This allows for comparison between observables that are equivalent, but are expressed - in different orders. For example, `qml.X(0) @ qml.Z(1)` and - `qml.Z(1) @ qml.X(0)` are equivalent observables with different orderings. - - **Example** - - >>> tensor = qml.X(0) @ qml.Z(1) - >>> print(tensor._obs_data()) - {("PauliZ", Wires([1]), ()), ("PauliX", Wires([0]), ())} - """ - obs = Tensor(self).non_identity_obs - tensor = set() - - for ob in obs: - parameters = tuple(param.tobytes() for param in ob.parameters) - if isinstance(ob, qml.GellMann): - parameters += (ob.hyperparameters["index"],) - tensor.add((ob.name, ob.wires, parameters)) - - return tensor - def compare( self, - other: Union["Tensor", "Observable", "qml.ops.Hamiltonian", "qml.ops.LinearCombination"], + other: Union["Observable", "qml.ops.LinearCombination"], ) -> bool: - r"""Compares with another :class:`~.Hamiltonian`, :class:`~Tensor`, or :class:`~Observable`, - to determine if they are equivalent. + r"""Compares with another :class:`~Observable`, to determine if they are equivalent. - Observables/Hamiltonians are equivalent if they represent the same operator + Observables are equivalent if they represent the same operator (their matrix representations are equal), and they are defined on the same wires. .. Warning:: @@ -2017,8 +1953,8 @@ def compare( The compare method does **not** check if the matrix representation of a :class:`~.Hermitian` observable is equal to an equivalent observable expressed in terms of Pauli matrices. - To do so would require the matrix form of Hamiltonians and Tensors - be calculated, which would drastically increase runtime. + To do so would require the matrix form to be calculated, which would + drastically increase runtime. Returns: (bool): True if equivalent. @@ -2034,595 +1970,7 @@ def compare( >>> ob1.compare(ob2) False """ - if isinstance(other, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): - return other.compare(self) - if isinstance(other, (Tensor, Observable)): - return other._obs_data() == self._obs_data() - - raise ValueError( - "Can only compare an Observable/Tensor, and a Hamiltonian/Observable/Tensor." - ) - - def __add__(self, other: Operator) -> Operator: - r"""The addition operation between Observables/Tensors/qml.Hamiltonian objects.""" - if active_new_opmath(): - return super().__add__(other=other) - - if isinstance(other, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): - return other + self - if isinstance(other, (Observable, Tensor)): - return qml.simplify(qml.Hamiltonian([1, 1], [self, other])) - - return super().__add__(other=other) - - __radd__ = __add__ - - def __mul__(self, a): - r"""The scalar multiplication operation between a scalar and an Observable/Tensor.""" - if active_new_opmath(): - return super().__mul__(other=a) - - if isinstance(a, (int, float)): - return qml.simplify(qml.Hamiltonian([a], [self])) - - return super().__mul__(other=a) - - __rmul__ = __mul__ - - def __sub__(self, other: Operator) -> Operator: - r"""The subtraction operation between Observables/Tensors/qml.Hamiltonian objects.""" - if active_new_opmath(): - return super().__sub__(other=other) - - if isinstance(other, (Observable, Tensor, qml.ops.Hamiltonian, qml.ops.LinearCombination)): - return self + (-1 * other) - - return super().__sub__(other=other) - - -class Tensor(Observable): - """Container class representing tensor products of observables. - - To create a tensor, simply initiate it like so: - - >>> T = Tensor(qml.X(0), qml.Hermitian(A, [1, 2])) - - You can also create a tensor from other Tensors: - - >>> T = Tensor(T, qml.Z(4)) - - The ``@`` symbol can be used as a tensor product operation: - - >>> T = qml.X(0) @ qml.Hadamard(2) - - .. note: - - This class is marked for deletion or overhaul. - """ - - # pylint: disable=abstract-method - tensor = True - has_matrix = True - - def _flatten(self) -> FlatPytree: - return tuple(self.obs), tuple() - - @classmethod - def _unflatten(cls, data, _): - return cls(*data) - - @classmethod - def _primitive_bind_call(cls, *args, **kwargs): - return cls._primitive.bind(*args) - - def __init__(self, *args): # pylint: disable=super-init-not-called - self._eigvals_cache = None - self.obs: list[Observable] = [] - self._args = args - self._batch_size = None - self._pauli_rep = None - self.queue(init=True) - - warnings.warn( - "qml.operation.Tensor uses the old approach to operator arithmetic, which will become " - "unavailable in version 0.40 of PennyLane. If you are experiencing issues, visit " - "https://docs.pennylane.ai/en/stable/news/new_opmath.html or contact the PennyLane " - "team on the discussion forum: https://discuss.pennylane.ai/.", - qml.PennyLaneDeprecationWarning, - ) - - wires = [op.wires for op in self.obs] - if len(wires) != len(set(wires)): - warnings.warn( - "Tensor object acts on overlapping wires; in some PennyLane functions this will " - "lead to undefined behaviour", - UserWarning, - ) - - # Queue before updating pauli_rep because self.queue updates self.obs - if all(prs := [o.pauli_rep for o in self.obs]): - self._pauli_rep = functools.reduce(lambda a, b: a @ b, prs) - else: - self._pauli_rep = None - - def label( - self, - decimals: Optional[int] = None, - base_label: Optional[str] = None, - cache: Optional[dict] = None, - ) -> str: - r"""How the operator is represented in diagrams and drawings. - - Args: - decimals=None (Int): If ``None``, no parameters are included. Else, - how to round the parameters. - base_label=None (Iterable[str]): overwrite the non-parameter component of the label. - Must be same length as ``obs`` attribute. - cache=None (dict): dictionary that carries information between label calls - in the same drawing - - Returns: - str: label to use in drawings - - >>> T = qml.X(0) @ qml.Hadamard(2) - >>> T.label() - 'X@H' - >>> T.label(base_label=["X0", "H2"]) - 'X0@H2' - - """ - if base_label is not None: - if len(base_label) != len(self.obs): - raise ValueError( - "Tensor label requires ``base_label`` keyword to be same length " - "as tensor components." - ) - return "@".join( - ob.label(decimals=decimals, base_label=lbl) for ob, lbl in zip(self.obs, base_label) - ) - - return "@".join(ob.label(decimals=decimals) for ob in self.obs) - - def queue(self, context=QueuingManager, init=False): # pylint: disable=arguments-differ - constituents = self._args if init else self.obs - for o in constituents: - if init: - if isinstance(o, Tensor): - self.obs.extend(o.obs) - elif isinstance(o, Observable): - self.obs.append(o) - else: - raise ValueError("Can only perform tensor products between observables.") - - context.remove(o) - - context.append(self) - return self - - def __copy__(self): - cls = self.__class__ - copied_op = cls.__new__(cls) # pylint: disable=no-value-for-parameter - copied_op.obs = self.obs.copy() - copied_op._eigvals_cache = self._eigvals_cache - copied_op._batch_size = self._batch_size - copied_op._pauli_rep = self._pauli_rep - return copied_op - - def __repr__(self) -> str: - """Constructor-call-like representation.""" - return " @ ".join([repr(o) for o in self.obs]) - - @property - def name(self) -> list[str]: - """All constituent observable names making up the tensor product. - - Returns: - list[str]: list containing all observable names - """ - return [o.name for o in self.obs] - - @property - def num_wires(self) -> int: - """Number of wires the tensor product acts on. - - Returns: - int: number of wires - """ - return len(self.wires) - - @property - def wires(self) -> Wires: - """All wires in the system the tensor product acts on. - - Returns: - Wires: wires addressed by the observables in the tensor product - """ - return Wires.all_wires([o.wires for o in self.obs]) - - @property - def data(self): - """Raw parameters of all constituent observables in the tensor product. - - Returns: - tuple[Any]: flattened list containing all dependent parameters - """ - return tuple(d for op in self.obs for d in op.data) - - @data.setter - def data(self, new_data): - """Setter used to set the parameters of all constituent observables in the tensor product. - - The ``new_data`` argument should contain a list of elements, where each element corresponds - to a list containing the parameters of each observable (in order). If an observable doesn't - have any parameter, an empty list must be used. - - **Example:** - - >>> op = qml.X(0) @ qml.Hermitian(np.eye(2), wires=1) - >>> op.data - [array([[1., 0.], - [0., 1.]])] - >>> op.data = [[], [np.eye(2) * 5]] - >>> op.data - [array([[5., 0.], - [0., 5.]])] - """ - if isinstance(new_data, tuple): - start = 0 - for op in self.obs: - op.data = new_data[start : start + len(op.data)] - start += len(op.data) - else: - for new_entry, op in zip(new_data, self.obs): - op.data = tuple(new_entry) - - @property - def num_params(self) -> int: - """Raw parameters of all constituent observables in the tensor product. - - Returns: - list[Any]: flattened list containing all dependent parameters - """ - return len(self.data) - - @property - def parameters(self): - """Evaluated parameter values of all constituent observables in the tensor product. - - Returns: - list[list[Any]]: nested list containing the parameters per observable - in the tensor product - """ - return [o.parameters for o in self.obs] - - @property - def non_identity_obs(self): - """Returns the non-identity observables contained in the tensor product. - - Returns: - list[:class:`~.Observable`]: list containing the non-identity observables - in the tensor product - """ - return [obs for obs in self.obs if not isinstance(obs, qml.Identity)] - - @property - def arithmetic_depth(self) -> int: - return 1 + max(o.arithmetic_depth for o in self.obs) - - def __matmul__(self, other: Operator) -> Operator: - if isinstance(other, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): - return other.__rmatmul__(self) - - if isinstance(other, Observable): - return Tensor(self, other) - - if isinstance(other, Operator): - return qml.prod(*self.obs, other) - - return NotImplemented - - def __rmatmul__(self, other): - if isinstance(other, Observable): - return Tensor(other, self) - - return NotImplemented - - __imatmul__ = __matmul__ - - def eigvals(self): - """Return the eigenvalues of the specified tensor product observable. - - This method uses pre-stored eigenvalues for standard observables where - possible. - - Returns: - array[float]: array containing the eigenvalues of the tensor product - observable - """ - if self._eigvals_cache is not None: - return self._eigvals_cache - - standard_observables = {"PauliX", "PauliY", "PauliZ", "Hadamard"} - - # observable should be Z^{\otimes n} - self._eigvals_cache = qml.pauli.pauli_eigs(len(self.wires)) - - # check if there are any non-standard observables (such as Identity) - if set(self.name) - standard_observables: - # Tensor product of observables contains a mixture - # of standard and non-standard observables - self._eigvals_cache = np.array([1]) - for k, g in itertools.groupby(self.obs, lambda x: x.name in standard_observables): - if k: - # Subgroup g contains only standard observables. - self._eigvals_cache = qml.math.kron( - self._eigvals_cache, qml.pauli.pauli_eigs(len(list(g))) - ) - else: - # Subgroup g contains only non-standard observables. - for ns_ob in g: - # loop through all non-standard observables - self._eigvals_cache = qml.math.kron(self._eigvals_cache, ns_ob.eigvals()) - - return self._eigvals_cache - - # pylint: disable=arguments-renamed, invalid-overridden-method - @property - def has_diagonalizing_gates(self): - r"""Bool: Whether or not the Tensor returns defined diagonalizing gates.""" - return all(o.has_diagonalizing_gates for o in self.obs) - - def diagonalizing_gates(self): - """Return the gate set that diagonalizes a circuit according to the - specified tensor observable. - - This method uses pre-stored eigenvalues for standard observables where - possible and stores the corresponding eigenvectors from the eigendecomposition. - - Returns: - list: list containing the gates diagonalizing the tensor observable - """ - diag_gates = [] - for o in self.obs: - diag_gates.extend(o.diagonalizing_gates()) - - return diag_gates - - def matrix(self, wire_order=None): - r"""Matrix representation of the Tensor operator - in the computational basis. - - .. note:: - - The wire_order argument is added for compatibility, but currently not implemented. - The Tensor class is planned to be removed soon. - - Args: - wire_order (Iterable): global wire order, must contain all wire labels in the operator's wires - - Returns: - array: matrix representation - - **Example** - - >>> O = qml.Z(0) @ qml.Z(2) - >>> O.matrix() - array([[ 1, 0, 0, 0], - [ 0, -1, 0, 0], - [ 0, 0, -1, 0], - [ 0, 0, 0, 1]]) - - To get the full :math:`2^3\times 2^3` Hermitian matrix - acting on the 3-qubit system, the identity on wire 1 - must be explicitly included: - - >>> O = qml.Z(0) @ qml.Identity(1) @ qml.Z(2) - >>> O.matrix() - array([[ 1., 0., 0., 0., 0., 0., 0., 0.], - [ 0., -1., 0., -0., 0., -0., 0., -0.], - [ 0., 0., 1., 0., 0., 0., 0., 0.], - [ 0., -0., 0., -1., 0., -0., 0., -0.], - [ 0., 0., 0., 0., -1., -0., -0., -0.], - [ 0., -0., 0., -0., -0., 1., -0., 0.], - [ 0., 0., 0., 0., -0., -0., -1., -0.], - [ 0., -0., 0., -0., -0., 0., -0., 1.]]) - """ - - if wire_order is not None: - raise NotImplementedError("The wire_order argument is currently not implemented.") - - # Check for partially (but not fully) overlapping wires in the observables - partial_overlap = self.check_wires_partial_overlap() - - # group the observables based on what wires they act on - U_list = [] - for _, g in itertools.groupby(self.obs, lambda x: x.wires.labels): - # extract the matrices of each diagonalizing gate - mats = [i.matrix() for i in g] - - if len(mats) > 1: - # multiply all unitaries together before appending - mats = [multi_dot(mats)] - - # append diagonalizing unitary for specific wire to U_list - U_list.append(mats[0]) - - mat_size = np.prod([qml.math.shape(mat)[0] for mat in U_list]) - wire_size = 2 ** len(self.wires) - if mat_size != wire_size: - if partial_overlap: - warnings.warn( - "The matrix for Tensors of Tensors/Observables with partially " - "overlapping wires might yield unexpected results. In particular " - "the matrix size might be larger than intended." - ) - else: - warnings.warn( - f"The size of the returned matrix ({mat_size}) will not be compatible " - f"with the subspace of the wires of the Tensor ({wire_size}). " - "This likely is due to wires being used in multiple tensor product " - "factors of the Tensor." - ) - - # Return the Hermitian matrix representing the observable - # over the defined wires. - return functools.reduce(qml.math.kron, U_list) - - def check_wires_partial_overlap(self): - r"""Tests whether any two observables in the Tensor have partially - overlapping wires and raise a warning if they do. - - .. note:: - - Fully overlapping wires, i.e., observables with - same (sets of) wires are not reported, as the ``matrix`` method is - well-defined and implemented for this scenario. - """ - for o1, o2 in itertools.combinations(self.obs, r=2): - shared = qml.wires.Wires.shared_wires([o1.wires, o2.wires]) - if shared and (shared != o1.wires or shared != o2.wires): - return 1 - return 0 - - @property - def has_sparse_matrix(self): - return all(op.has_matrix for op in self.obs) - - def sparse_matrix( - self, wire_order=None, wires=None, format="csr" - ): # pylint:disable=arguments-renamed, arguments-differ - r"""Computes, by default, a `scipy.sparse.csr_matrix` representation of this Tensor. - - This is useful for larger qubit numbers, where the dense matrix becomes very large, while - consisting mostly of zero entries. - - Args: - wire_order (Iterable): Wire labels that indicate the order of wires according to which the matrix - is constructed. If not provided, ``self.wires`` is used. - wires (Iterable): Same as ``wire_order`` to ensure compatibility with all the classes. Must only - provide one: either ``wire_order`` or ``wires``. - format: the output format for the sparse representation. All scipy sparse formats are accepted. - - Raises: - ValueError: if both ``wire_order`` and ``wires`` are provided at the same time. - - Returns: - :class:`scipy.sparse._csr.csr_matrix`: sparse matrix representation - - **Example** - - Consider the following tensor: - - >>> t = qml.X(0) @ qml.Z(1) - - Without passing wires, the sparse representation is given by: - - >>> print(t.sparse_matrix()) - (0, 2) 1 - (1, 3) -1 - (2, 0) 1 - (3, 1) -1 - - If we define a custom wire ordering, the matrix representation changes - accordingly: - - >>> print(t.sparse_matrix(wire_order=[1, 0])) - (0, 1) 1 - (1, 0) 1 - (2, 3) -1 - (3, 2) -1 - - We can also enforce implicit identities by passing wire labels that - are not present in the constituent operations: - - >>> res = t.sparse_matrix(wire_order=[0, 1, 2]) - >>> print(res.shape) - (8, 8) - """ - if wires is not None and wire_order is not None: - raise ValueError( - "Wire order has been specified twice. Provide only one of either " - "``wire_order`` or ``wires``, but not both." - ) - - wires = wires or wire_order - wires = self.wires if wires is None else Wires(wires) - list_of_sparse_ops = [eye(2, format="coo")] * len(wires) - - for o in self.obs: - if len(o.wires) > 1: - # todo: deal with multi-qubit operations that do not act on consecutive qubits - raise ValueError( - f"Can only compute sparse representation for tensors whose operations " - f"act on consecutive wires; got {o}." - ) - # store the single-qubit ops according to the order of their wires - idx = wires.index(o.wires) - list_of_sparse_ops[idx] = coo_matrix(o.matrix()) - - return functools.reduce(lambda i, j: kron(i, j, format=format), list_of_sparse_ops) - - def prune(self): - """Returns a pruned tensor product of observables by removing :class:`~.Identity` instances from - the observables building up the :class:`~.Tensor`. - - If the tensor product only contains one observable, then this observable instance is - returned. - - Note that, as a result, this method can return observables that are not a :class:`~.Tensor` - instance. - - **Example:** - - Pruning that returns a :class:`~.Tensor`: - - >>> O = qml.Z(0) @ qml.Identity(1) @ qml.Z(2) - >>> O.prune() - >> [(o.name, o.wires) for o in O.prune().obs] - [('PauliZ', [0]), ('PauliZ', [2])] - - Pruning that returns a single observable: - - >>> O = qml.Z(0) @ qml.Identity(1) - >>> O_pruned = O.prune() - >>> (O_pruned.name, O_pruned.wires) - ('PauliZ', [0]) - - Returns: - ~.Observable: the pruned tensor product of observables - """ - if qml.QueuingManager.recording(): - qml.QueuingManager.remove(self) - - if len(self.non_identity_obs) == 0: - # Return a single Identity as the tensor only contains Identities - return qml.Identity(self.wires[0]) if self.wires else qml.Identity() - return ( - self.non_identity_obs[0] - if len(self.non_identity_obs) == 1 - else Tensor(*self.non_identity_obs) - ) - - def map_wires(self, wire_map: dict): - """Returns a copy of the current tensor with its wires changed according to the given - wire map. - - Args: - wire_map (dict): dictionary containing the old wires as keys and the new wires as values - - Returns: - .Tensor: new tensor - """ - cls = self.__class__ - new_op = cls.__new__(cls) # pylint: disable=no-value-for-parameter - new_op.obs = [obs.map_wires(wire_map) for obs in self.obs] - new_op._eigvals_cache = self._eigvals_cache - new_op._batch_size = self._batch_size - new_op._pauli_rep = ( - self._pauli_rep.map_wires(wire_map) if self.pauli_rep is not None else None - ) - return new_op + return qml.equal(self, other) # ============================================================================= @@ -3040,272 +2388,7 @@ def gen_is_multi_term_hamiltonian(obj): except (AttributeError, OperatorPropertyUndefined, GeneratorUndefinedError): return False - return isinstance(o, (qml.ops.Hamiltonian, qml.ops.LinearCombination)) and len(o.coeffs) > 1 - - -def enable_new_opmath(warn=True): - """ - Change dunder methods to return arithmetic operators instead of Hamiltonians and Tensors - - .. warning:: - - Using legacy operator arithmetic is deprecated, and will be removed in PennyLane v0.40. - For further details, see :doc:`Updated Operators `. - - Args: - warn (bool): Whether or not to emit a warning for re-enabling new opmath. Default is ``True``. - - **Example** - - >>> qml.operation.active_new_opmath() - False - >>> type(qml.X(0) @ qml.Z(1)) - - >>> qml.operation.enable_new_opmath() - >>> type(qml.X(0) @ qml.Z(1)) - - """ - if warn: - warnings.warn( - "Toggling the new approach to operator arithmetic is deprecated. From version 0.40 of " - "PennyLane, only the new approach to operator arithmetic will be available. If you are " - "experiencing issues, visit https://docs.pennylane.ai/en/stable/news/new_opmath.html " - "or contact the PennyLane team on the discussion forum: https://discuss.pennylane.ai/.", - qml.PennyLaneDeprecationWarning, - ) - global __use_new_opmath - __use_new_opmath = True - - -def disable_new_opmath(warn=True): - """ - Change dunder methods to return Hamiltonians and Tensors instead of arithmetic operators - - .. warning:: - - Using legacy operator arithmetic is deprecated, and will be removed in PennyLane v0.40. - For further details, see :doc:`Updated Operators `. - - Args: - warn (bool): Whether or not to emit a warning for disabling new opmath. Default is ``True``. - - **Example** - - >>> qml.operation.active_new_opmath() - True - >>> type(qml.X(0) @ qml.Z(1)) - - >>> qml.operation.disable_new_opmath() - >>> type(qml.X(0) @ qml.Z(1)) - - """ - if warn: - warnings.warn( - "Disabling the new approach to operator arithmetic is deprecated. From version 0.40 of " - "PennyLane, only the new approach to operator arithmetic will be available. If you are " - "experiencing issues, visit https://docs.pennylane.ai/en/stable/news/new_opmath.html " - "or contact the PennyLane team on the discussion forum: https://discuss.pennylane.ai/.", - qml.PennyLaneDeprecationWarning, - ) - global __use_new_opmath - __use_new_opmath = False - - -def active_new_opmath(): - """ - Function that checks if the new arithmetic operator dunders are active - - .. warning:: - - Using legacy operator arithmetic is deprecated, and will be removed in PennyLane v0.40. - For further details, see :doc:`Updated Operators `. - - Returns: - bool: Returns ``True`` if the new arithmetic operator dunders are active - - **Example** - - >>> qml.operation.active_new_opmath() - False - >>> qml.operation.enable_new_opmath() - >>> qml.operation.active_new_opmath() - True - """ - return __use_new_opmath - - -def convert_to_opmath(op): - """ - Converts :class:`~pennylane.Hamiltonian` and :class:`.Tensor` instances - into arithmetic operators. Objects of any other type are returned directly. - - Arithmetic operators include :class:`~pennylane.ops.op_math.Prod`, - :class:`~pennylane.ops.op_math.Sum` and :class:`~pennylane.ops.op_math.SProd`. - - Args: - op (Operator): The operator instance to convert - - Returns: - Operator: An operator using the new arithmetic operations, if relevant - """ - if isinstance(op, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): - if qml.QueuingManager.recording(): - qml.QueuingManager.remove(op) - c, ops = op.terms() - ops = tuple(convert_to_opmath(o) for o in ops) - return qml.dot(c, ops) - if isinstance(op, Tensor): - if qml.QueuingManager.recording(): - qml.QueuingManager.remove(op) - return qml.prod(*op.obs) - return op - - -@contextmanager -def disable_new_opmath_cm(warn=True): - r"""Allows to use the old operator arithmetic within a - temporary context using the `with` statement.""" - if warn: - warnings.warn( - "Disabling the new approach to operator arithmetic is deprecated. From version 0.40 of " - "PennyLane, only the new approach to operator arithmetic will be available. If you are " - "experiencing issues, visit https://docs.pennylane.ai/en/stable/news/new_opmath.html " - "or contact the PennyLane team on the discussion forum: https://discuss.pennylane.ai/.", - qml.PennyLaneDeprecationWarning, - ) - - was_active = qml.operation.active_new_opmath() - try: - if was_active: - disable_new_opmath(warn=False) # Only warn once - yield - except Exception as e: - raise e - finally: - if was_active: - enable_new_opmath(warn=False) # Only warn once - else: - disable_new_opmath(warn=False) # Only warn once - - -@contextmanager -def enable_new_opmath_cm(warn=True): - r"""Allows to use the new operator arithmetic within a - temporary context using the `with` statement.""" - if warn: - warnings.warn( - "Toggling the new approach to operator arithmetic is deprecated. From version 0.40 of " - "PennyLane, only the new approach to operator arithmetic will be available. If you are " - "experiencing issues, visit https://docs.pennylane.ai/en/stable/news/new_opmath.html " - "or contact the PennyLane team on the discussion forum: https://discuss.pennylane.ai/.", - qml.PennyLaneDeprecationWarning, - ) - - was_active = qml.operation.active_new_opmath() - if not was_active: - enable_new_opmath(warn=False) # Only warn once - yield - if was_active: - enable_new_opmath(warn=False) # Only warn once - else: - disable_new_opmath(warn=False) # Only warn once - - -# pylint: disable=too-many-branches -def convert_to_H(op): - """ - Converts arithmetic operators into a :class:`~pennylane.ops.Hamiltonian` or - :class:`~pennylane.ops.LinearCombination` instance, depending on whether - new_opmath is enabled. Objects of any other type are returned directly. - - Arithmetic operators include :class:`~pennylane.ops.op_math.Prod`, - :class:`~pennylane.ops.op_math.Sum` and :class:`~pennylane.ops.op_math.SProd`. - - Args: - op (Operator): The operator instance to convert. - - Returns: - Operator: The operator as a :class:`~pennylane.ops.LinearCombination` instance - if `active_new_opmath()`, otherwise a :class:`~pennylane.ops.Hamiltonian` - """ - if not isinstance(op, (qml.ops.op_math.Prod, qml.ops.op_math.SProd, qml.ops.op_math.Sum)): - return op - - coeffs = [] - ops = [] - - op = qml.simplify(op) - product = qml.ops.op_math.Prod if active_new_opmath() else Tensor - - if isinstance(op, Observable): - coeffs.append(1.0) - ops.append(op) - - elif isinstance(op, qml.ops.SProd): - coeffs.append(op.scalar) - if isinstance(op.base, Observable): - ops.append(op.base) - elif isinstance(op.base, qml.ops.op_math.Prod): - ops.append(product(*op.base)) - else: - raise ValueError("The base of scalar product must be an observable or a product.") - - elif isinstance(op, qml.ops.Prod): - coeffs.append(1.0) - ops.append(product(*op)) - - elif isinstance(op, qml.ops.Sum): - for factor in op: - if isinstance(factor, (qml.ops.SProd)): - coeffs.append(factor.scalar) - if isinstance(factor.base, Observable): - ops.append(factor.base) - elif isinstance(factor.base, qml.ops.op_math.Prod): - ops.append(product(*factor.base)) - else: - raise ValueError( - "The base of scalar product must be an observable or a product." - ) - elif isinstance(factor, (qml.ops.Prod)): - coeffs.append(1.0) - ops.append(product(*factor)) - elif isinstance(factor, Observable): - coeffs.append(1.0) - ops.append(factor) - else: - raise ValueError( - "Could not convert to Hamiltonian. Some or all observables are not valid." - ) - - else: - raise ValueError("Could not convert to Hamiltonian. Some or all observables are not valid.") - - return qml.Hamiltonian(coeffs, ops) - - -def convert_to_legacy_H(op): - """ - Converts arithmetic operators into a legacy :class:`~pennylane.Hamiltonian` instance. - Objects of any other type are returned directly. - - Arithmetic operators include :class:`~pennylane.ops.op_math.Prod`, - :class:`~pennylane.ops.op_math.Sum` and :class:`~pennylane.ops.op_math.SProd`. - - .. warning:: - - Using legacy operator arithmetic is deprecated, and will be removed in PennyLane v0.40. - For further details, see :doc:`Updated Operators `. - - Args: - op (Operator): The operator instance to convert. - - Returns: - Operator: The operator as a :class:`~pennylane.Hamiltonian` instance - """ - with disable_new_opmath_cm(warn=False): - # Suppress warning because constructing Hamiltonian will raise a warning anyway - res = convert_to_H(op) - return res + return isinstance(o, qml.ops.LinearCombination) and len(o.coeffs) > 1 def __getattr__(name): diff --git a/pennylane/ops/functions/bind_new_parameters.py b/pennylane/ops/functions/bind_new_parameters.py index af29d1576af..f6f57ff5952 100644 --- a/pennylane/ops/functions/bind_new_parameters.py +++ b/pennylane/ops/functions/bind_new_parameters.py @@ -22,7 +22,7 @@ from typing import Union import pennylane as qml -from pennylane.operation import Operator, Tensor +from pennylane.operation import Operator from pennylane.typing import TensorLike from ..identity import Identity @@ -233,26 +233,6 @@ def bind_new_parameters_pow(op: Pow, params: Sequence[TensorLike]): return Pow(bind_new_parameters(op.base, params), op.scalar) -@bind_new_parameters.register -def bind_new_parameters_hamiltonian(op: qml.ops.Hamiltonian, params: Sequence[TensorLike]): - new_H = qml.ops.Hamiltonian(params, op.ops) - if op.grouping_indices is not None: - new_H.grouping_indices = op.grouping_indices - return new_H - - -@bind_new_parameters.register -def bind_new_parameters_tensor(op: Tensor, params: Sequence[TensorLike]): - new_obs = [] - - for obs in op.obs: - sub_params = params[: obs.num_params] - params = params[obs.num_params :] - new_obs.append(bind_new_parameters(obs, sub_params)) - - return Tensor(*new_obs) - - @bind_new_parameters.register def bind_new_parameters_conditional(op: qml.ops.Conditional, params: Sequence[TensorLike]): then_op = bind_new_parameters(op.base, params) diff --git a/pennylane/ops/functions/dot.py b/pennylane/ops/functions/dot.py index dd5b85cb547..d54adf5d335 100644 --- a/pennylane/ops/functions/dot.py +++ b/pennylane/ops/functions/dot.py @@ -21,7 +21,7 @@ from typing import Union import pennylane as qml -from pennylane.operation import Operator, convert_to_opmath +from pennylane.operation import Operator from pennylane.pauli import PauliSentence, PauliWord from pennylane.pulse import ParametrizedHamiltonian @@ -165,9 +165,6 @@ def dot( # Convert possible PauliWord and PauliSentence instances to operation ops = [op.operation() if isinstance(op, (PauliWord, PauliSentence)) else op for op in ops] - # When casting a Hamiltonian to a Sum, we also cast its inner Tensors to Prods - ops = (convert_to_opmath(op) for op in ops) - operands = [op if coeff == 1 else qml.s_prod(coeff, op) for coeff, op in zip(coeffs, ops)] return ( operands[0] diff --git a/pennylane/ops/functions/eigvals.py b/pennylane/ops/functions/eigvals.py index 423ea7e51f7..5f71d62dc72 100644 --- a/pennylane/ops/functions/eigvals.py +++ b/pennylane/ops/functions/eigvals.py @@ -115,16 +115,6 @@ def circuit(theta): raise TransformError("Input is not an Operator, tape, QNode, or quantum function") return _eigvals_tranform(op, k=k, which=which) - if isinstance(op, qml.ops.Hamiltonian): - - warnings.warn( - "For Hamiltonians, the eigenvalues will be computed numerically. " - "This may be computationally intensive for a large number of wires. " - "Consider using a sparse representation of the Hamiltonian with qml.SparseHamiltonian.", - UserWarning, - ) - return qml.math.linalg.eigvalsh(qml.matrix(op)) - if isinstance(op, qml.SparseHamiltonian): sparse_matrix = op.sparse_matrix() if k < sparse_matrix.shape[0] - 1: diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py index 93598fe52cb..546b5aae0eb 100644 --- a/pennylane/ops/functions/equal.py +++ b/pennylane/ops/functions/equal.py @@ -20,25 +20,14 @@ from typing import Union import pennylane as qml -from pennylane import Hermitian from pennylane.measurements import MeasurementProcess from pennylane.measurements.classical_shadow import ShadowExpvalMP from pennylane.measurements.counts import CountsMP from pennylane.measurements.mid_measure import MeasurementValue, MidMeasureMP from pennylane.measurements.mutual_info import MutualInfoMP from pennylane.measurements.vn_entropy import VnEntropyMP -from pennylane.operation import Observable, Operator, Tensor -from pennylane.ops import ( - Adjoint, - CompositeOp, - Conditional, - Controlled, - Exp, - Hamiltonian, - LinearCombination, - Pow, - SProd, -) +from pennylane.operation import Observable, Operator +from pennylane.ops import Adjoint, CompositeOp, Conditional, Controlled, Exp, Pow, SProd from pennylane.pulse.parametrized_evolution import ParametrizedEvolution from pennylane.tape import QuantumScript from pennylane.templates.subroutines import ControlledSequence, PrepSelPrep @@ -61,17 +50,16 @@ def equal( .. Warning:: The ``qml.equal`` function is based on a comparison of the types and attributes of the - measurements or operators, not their mathematical representations. While mathematical - comparisons between some classes, such as ``Tensor`` and ``Hamiltonian``, are supported, - mathematically equivalent operators defined via different classes may return False when - compared via ``qml.equal``. To be more thorough would require the matrix forms to be - calculated, which may drastically increase runtime. + measurements or operators, not their mathematical representations. Mathematically equivalent + operators defined via different classes may return False when compared via ``qml.equal``. + To be more thorough would require the matrix forms to be calculated, which may drastically + increase runtime. .. Warning:: - The interfaces and trainability of data within some observables including ``Tensor``, - ``Hamiltonian``, ``Prod``, ``Sum`` are sometimes ignored, regardless of what the user - specifies for ``check_interface`` and ``check_trainability``. + The interfaces and trainability of data within some observables including ``Prod`` and + ``Sum`` are sometimes ignored, regardless of what the user specifies for ``check_interface`` + and ``check_trainability``. Args: op1 (.Operator or .MeasurementProcess or .QuantumTape): First object to compare @@ -93,15 +81,15 @@ def equal( >>> qml.equal(op1, op1), qml.equal(op1, op2) (True, False) - >>> T1 = qml.X(0) @ qml.Y(1) - >>> T2 = qml.Y(1) @ qml.X(0) - >>> T3 = qml.X(1) @ qml.Y(0) - >>> qml.equal(T1, T2), qml.equal(T1, T3) + >>> prod1 = qml.X(0) @ qml.Y(1) + >>> prod2 = qml.Y(1) @ qml.X(0) + >>> prod3 = qml.X(1) @ qml.Y(0) + >>> qml.equal(prod1, prod2), qml.equal(prod1, prod3) (True, False) - >>> T = qml.X(0) @ qml.Y(1) - >>> H = qml.Hamiltonian([1], [qml.X(0) @ qml.Y(1)]) - >>> qml.equal(T, H) + >>> prod = qml.X(0) @ qml.Y(1) + >>> ham = qml.Hamiltonian([1], [qml.X(0) @ qml.Y(1)]) + >>> qml.equal(prod, ham) True >>> H1 = qml.Hamiltonian([0.5, 0.5], [qml.Z(0) @ qml.Y(1), qml.Y(1) @ qml.Z(0) @ qml.Identity("a")]) @@ -152,9 +140,6 @@ def equal( """ - if isinstance(op2, (Hamiltonian, Tensor)): - op1, op2 = op2, op1 - dispatch_result = _equal( op1, op2, @@ -577,39 +562,6 @@ def _equal_sprod(op1: SProd, op2: SProd, **kwargs): return True -@_equal_dispatch.register -# pylint: disable=unused-argument -def _equal_tensor(op1: Tensor, op2: Observable, **kwargs): - """Determine whether a Tensor object is equal to a Hamiltonian/Tensor""" - - if not isinstance(op2, Observable): - return f"{op2} is not of type Observable" - - if isinstance(op2, (Hamiltonian, LinearCombination, Hermitian)): - return ( - op2.compare(op1) or f"'{op1}' and '{op2}' are not the same for an unspecified reason." - ) - - if isinstance(op2, Tensor): - return ( - op1._obs_data() == op2._obs_data() # pylint: disable=protected-access - or f"{op1} and {op2} have different _obs_data outputs" - ) - - return f"{op1} is of type {type(op1)} and {op2} is of type {type(op2)}" - - -@_equal_dispatch.register -# pylint: disable=unused-argument -def _equal_hamiltonian(op1: Hamiltonian, op2: Observable, **kwargs): - """Determine whether a Hamiltonian object is equal to a Hamiltonian/Tensor objects""" - - if not isinstance(op2, Observable): - return f"{op2} is not of type Observable" - - return op1.compare(op2) or f"'{op1}' and '{op2}' are not the same for an unspecified reason" - - @_equal_dispatch.register def _equal_parametrized_evolution(op1: ParametrizedEvolution, op2: ParametrizedEvolution, **kwargs): # check times match diff --git a/pennylane/ops/functions/generator.py b/pennylane/ops/functions/generator.py index a004c757f63..d9115808bc4 100644 --- a/pennylane/ops/functions/generator.py +++ b/pennylane/ops/functions/generator.py @@ -21,34 +21,30 @@ import numpy as np import pennylane as qml -from pennylane.operation import convert_to_H -from pennylane.ops import Hamiltonian, LinearCombination, Prod, SProd, Sum +from pennylane.ops import LinearCombination, Prod, SProd, Sum # pylint: disable=too-many-branches def _generator_hamiltonian(gen, op): - """Return the generator as type :class:`~.Hamiltonian`.""" - wires = op.wires + """Return the generator as type :class:`~ops.LinearCombination`.""" - if isinstance(gen, (Hamiltonian, LinearCombination)): - H = gen + if isinstance(gen, LinearCombination): + return gen - elif isinstance(gen, (qml.Hermitian, qml.SparseHamiltonian)): + if isinstance(gen, (qml.Hermitian, qml.SparseHamiltonian)): if isinstance(gen, qml.Hermitian): mat = gen.parameters[0] elif isinstance(gen, qml.SparseHamiltonian): mat = gen.parameters[0].toarray() - H = qml.pauli_decompose(mat, wire_order=wires, hide_identity=True) + return qml.pauli_decompose(mat, wire_order=op.wires, hide_identity=True) - elif isinstance(gen, qml.operation.Observable): - H = qml.Hamiltonian([1.0], [gen]) + if isinstance(gen, (SProd, Prod, Sum)): + coeffs, ops = gen.terms() + return qml.Hamiltonian(coeffs, ops) - elif isinstance(gen, (SProd, Prod, Sum)): - H = convert_to_H(gen) - - return H + return qml.Hamiltonian([1.0], [gen]) # pylint: disable=no-member @@ -64,12 +60,15 @@ def _generator_prefactor(gen): prefactor = 1.0 - if isinstance(gen, Prod): - gen = qml.simplify(gen) + gen = qml.simplify(gen) if isinstance(gen, Prod) else gen - if isinstance(gen, (Hamiltonian, LinearCombination)): + if isinstance(gen, LinearCombination): gen = qml.dot(gen.coeffs, gen.ops) # convert to Sum + if isinstance(gen, Prod): + coeffs, ops = gen.terms() + return ops[0], coeffs[0] + if isinstance(gen, Sum): ops = [o.base if isinstance(o, SProd) else o for o in gen] coeffs = [o.scalar if isinstance(o, SProd) else 1 for o in gen] @@ -134,8 +133,7 @@ def generator(op: qml.operation.Operator, format="prefactor"): * ``"observable"``: Return the generator as a single observable as directly defined by ``op``. Returned generators may be any type of observable, including - :class:`~.Hermitian`, :class:`~.Tensor`, - :class:`~.SparseHamiltonian`, or :class:`~.Hamiltonian`. + :class:`~.Hermitian`, :class:`~.SparseHamiltonian`, or :class:`~.Hamiltonian`. * ``"hamiltonian"``: Similar to ``"observable"``, however the returned observable will always be converted into :class:`~.Hamiltonian` regardless of how ``op`` @@ -169,12 +167,8 @@ def generator(op: qml.operation.Operator, format="prefactor"): >>> op = qml.RX(0.2, wires=0) >>> qml.generator(op, format="prefactor") # output will always be (obs, prefactor) (X(0), -0.5) - >>> qml.generator(op, format="hamiltonian") # output will always be a Hamiltonian/LinearCombination + >>> qml.generator(op, format="hamiltonian") # output will be a LinearCombination -0.5 * X(0) - >>> with qml.operation.disable_new_opmath_cm(): - ... gen = qml.generator(op, format="hamiltonian")) # legacy Hamiltonian class - ... print(gen, type(gen)) - (-0.5) [X0] >>> qml.generator(qml.PhaseShift(0.1, wires=0), format="observable") # ouput will be a simplified obs where possible Projector([1], wires=[0]) >>> qml.generator(op, format="arithmetic") # output is an instance of `SProd` diff --git a/pennylane/ops/functions/is_commuting.py b/pennylane/ops/functions/is_commuting.py index c835236de7d..4f01ccf72aa 100644 --- a/pennylane/ops/functions/is_commuting.py +++ b/pennylane/ops/functions/is_commuting.py @@ -152,16 +152,16 @@ def commutes_inner(op_name1, op_name2): def _check_opmath_operations(operation1, operation2): - """Check that `Tensor`, `SProd`, `Prod`, and `Sum` instances only contain Pauli words.""" + """Check that `SProd`, `Prod`, and `Sum` instances only contain Pauli words.""" for op in [operation1, operation2]: if op.pauli_rep is not None: continue - if isinstance(op, (qml.operation.Tensor, SProd, Prod, Sum)): + if isinstance(op, (SProd, Prod, Sum)): raise qml.QuantumFunctionError( - f"Operation {op} currently not supported. Tensor, Prod, Sprod, and Sum instances must have a valid Pauli representation." + f"Operation {op} currently not supported. Prod, Sprod, and Sum instances must have a valid Pauli representation." ) diff --git a/pennylane/ops/functions/matrix.py b/pennylane/ops/functions/matrix.py index 1fad95575d1..c2b5e259f1a 100644 --- a/pennylane/ops/functions/matrix.py +++ b/pennylane/ops/functions/matrix.py @@ -219,14 +219,6 @@ def circuit(): raise TransformError("Input is not an Operator, tape, QNode, or quantum function") return _matrix_transform(op, wire_order=wire_order) - - if isinstance(op, qml.operation.Tensor) and wire_order is not None: - op = 1.0 * op # convert to a Hamiltonian - - if isinstance(op, qml.ops.Hamiltonian): - - return op.sparse_matrix(wire_order=wire_order).toarray() - try: return op.matrix(wire_order=wire_order) except: # pylint: disable=bare-except diff --git a/pennylane/ops/op_math/adjoint.py b/pennylane/ops/op_math/adjoint.py index fd2d6e12dd5..92e9fd071d5 100644 --- a/pennylane/ops/op_math/adjoint.py +++ b/pennylane/ops/op_math/adjoint.py @@ -364,11 +364,7 @@ def label(self, decimals=None, base_label=None, cache=None): ) def matrix(self, wire_order=None): - if isinstance(self.base, qml.ops.Hamiltonian): - base_matrix = qml.matrix(self.base, wire_order=wire_order) - else: - base_matrix = self.base.matrix(wire_order=wire_order) - + base_matrix = self.base.matrix(wire_order=wire_order) return moveaxis(conj(base_matrix), -2, -1) # pylint: disable=arguments-renamed, invalid-overridden-method diff --git a/pennylane/ops/op_math/composite.py b/pennylane/ops/op_math/composite.py index 132fa3cd516..19fee35a5b7 100644 --- a/pennylane/ops/op_math/composite.py +++ b/pennylane/ops/op_math/composite.py @@ -177,7 +177,7 @@ def is_hermitian(self): @property @handle_recursion_error def has_matrix(self): - return all(op.has_matrix or isinstance(op, qml.ops.Hamiltonian) for op in self) + return all(op.has_matrix for op in self) @handle_recursion_error def eigvals(self): diff --git a/pennylane/ops/op_math/exp.py b/pennylane/ops/op_math/exp.py index 106f387b1e2..52ac51c1700 100644 --- a/pennylane/ops/op_math/exp.py +++ b/pennylane/ops/op_math/exp.py @@ -29,11 +29,9 @@ Operation, Operator, OperatorPropertyUndefined, - Tensor, ) from pennylane.wires import Wires -from ..qubit.hamiltonian import Hamiltonian from .linear_combination import LinearCombination from .sprod import SProd from .sum import Sum @@ -215,15 +213,13 @@ def _queue_category(self): @property def has_decomposition(self): # TODO: Support nested sums in method - if isinstance(self.base, Tensor) and len(self.base.wires) != len(self.base.obs): - return False base = self.base coeff = self.coeff if isinstance(base, SProd): coeff *= base.scalar base = base.base is_pauli_rot = qml.pauli.is_pauli_word(self.base) and math.real(self.coeff) == 0 - is_hamiltonian = isinstance(base, (Hamiltonian, LinearCombination)) + is_hamiltonian = isinstance(base, LinearCombination) is_sum_of_pauli_words = isinstance(base, Sum) and all( qml.pauli.is_pauli_word(o) for o in base ) @@ -260,18 +256,9 @@ def _recursive_decomposition(self, base: Operator, coeff: complex): Returns: List[Operator]: decomposition """ - if isinstance(base, Tensor) and len(base.wires) != len(base.obs): - raise DecompositionUndefinedError( - "Unable to determine if the exponential has a decomposition " - "when the base operator is a Tensor object with overlapping wires. " - f"Received base {base}." - ) - # Change base to `Sum`/`Prod` - if isinstance(base, (Hamiltonian, LinearCombination)): + if isinstance(base, LinearCombination): base = qml.dot(base.coeffs, base.ops) - elif isinstance(base, Tensor): - base = qml.prod(*base.obs) if isinstance(base, SProd): return self._recursive_decomposition(base.base, base.scalar * coeff) diff --git a/pennylane/ops/op_math/linear_combination.py b/pennylane/ops/op_math/linear_combination.py index 348a95cfab0..9e897aa0739 100644 --- a/pennylane/ops/op_math/linear_combination.py +++ b/pennylane/ops/op_math/linear_combination.py @@ -18,12 +18,11 @@ import numbers # pylint: disable=too-many-arguments, protected-access, too-many-instance-attributes -import warnings from copy import copy from typing import Union import pennylane as qml -from pennylane.operation import Observable, Operator, Tensor, convert_to_opmath +from pennylane.operation import Observable, Operator from .sum import Sum @@ -137,7 +136,7 @@ def __init__( self._coeffs = coeffs - self._ops = [convert_to_opmath(op) for op in observables] + self._ops = list(observables) self._hyperparameters = {"ops": self._ops} @@ -345,21 +344,6 @@ def compare(self, other): pr2.simplify() return pr1 == pr2 - if isinstance(other, (qml.ops.Hamiltonian, Tensor)): - warnings.warn( - f"Attempting to compare a legacy operator class instance {other} of type {type(other)} with {self} of type {type(self)}." - f"You are likely disabling/enabling new opmath in the same script or explicitly create legacy operator classes Tensor and ops.Hamiltonian." - f"Please visit https://docs.pennylane.ai/en/stable/news/new_opmath.html for more information and help troubleshooting.", - UserWarning, - ) - op1 = self.simplify() - op2 = other.simplify() - - op2 = qml.operation.convert_to_opmath(op2) - op2 = qml.ops.LinearCombination(*op2.terms()) - - return qml.equal(op1, op2) - op1 = self.simplify() op2 = other.simplify() return qml.equal(op1, op2) @@ -410,7 +394,7 @@ def __add__(self, H: Union[numbers.Number, Operator]) -> Operator: if isinstance(H, numbers.Number) and H == 0: return self - if isinstance(H, (LinearCombination, qml.ops.Hamiltonian)): + if isinstance(H, LinearCombination): coeffs = qml.math.concatenate([self_coeffs, H.coeffs], axis=0) ops.extend(H.ops) if (pr1 := self.pauli_rep) is not None and (pr2 := H.pauli_rep) is not None: @@ -443,8 +427,8 @@ def __mul__(self, a: Union[int, float, complex]) -> "LinearCombination": __rmul__ = __mul__ def __sub__(self, H: Observable) -> Observable: - r"""The subtraction operation between a LinearCombination and a LinearCombination/Tensor/Observable.""" - if isinstance(H, (LinearCombination, qml.ops.Hamiltonian, Tensor, Observable)): + r"""The subtraction operation between a LinearCombination and a LinearCombination/Observable.""" + if isinstance(H, (LinearCombination, Observable)): return self + qml.s_prod(-1.0, H, lazy=False) return NotImplemented @@ -542,3 +526,158 @@ def _(*args, n_obs, **kwargs): coeffs = args[:n_obs] observables = args[n_obs:] return type.__call__(LinearCombination, coeffs, observables, **kwargs) + + +# this just exists for the docs build for now, since we're waiting until the next PR to fix the docs +# pylint: disable=too-few-public-methods +class Hamiltonian: + r"""Returns an operator representing a Hamiltonian. + + The Hamiltonian is represented as a linear combination of other operators, e.g., + :math:`\sum_{k=0}^{N-1} c_k O_k`, where the :math:`c_k` are trainable parameters. + + .. note:: + + ``qml.Hamiltonian`` dispatches to :class:`~pennylane.ops.op_math.LinearCombination`. + + Args: + coeffs (tensor_like): coefficients of the Hamiltonian expression + observables (Iterable[Observable]): observables in the Hamiltonian expression, of same length as coeffs + grouping_type (str): If not None, compute and store information on how to group commuting + observables upon initialization. This information may be accessed when QNodes containing this + Hamiltonian are executed on devices. The string refers to the type of binary relation between Pauli words. + Can be ``'qwc'`` (qubit-wise commuting), ``'commuting'``, or ``'anticommuting'``. + method (str): The graph colouring heuristic to use in solving minimum clique cover for grouping, which + can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest First). Ignored if ``grouping_type=None``. + id (str): name to be assigned to this Hamiltonian instance + + **Example:** + + ``qml.Hamiltonian`` takes in a list of coefficients and a list of operators. + + >>> coeffs = [0.2, -0.543] + >>> obs = [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Hadamard(2)] + >>> H = qml.Hamiltonian(coeffs, obs) + >>> print(H) + 0.2 * (X(0) @ Z(1)) + -0.543 * (Z(0) @ Hadamard(wires=[2])) + + The coefficients can be a trainable tensor, for example: + + >>> coeffs = tf.Variable([0.2, -0.543], dtype=tf.double) + >>> obs = [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Hadamard(2)] + >>> H = qml.Hamiltonian(coeffs, obs) + >>> print(H) + 0.2 * (X(0) @ Z(1)) + -0.543 * (Z(0) @ Hadamard(wires=[2])) + + A ``qml.Hamiltonian`` stores information on which commuting observables should be measured + together in a circuit: + + >>> obs = [qml.X(0), qml.X(1), qml.Z(0)] + >>> coeffs = np.array([1., 2., 3.]) + >>> H = qml.Hamiltonian(coeffs, obs, grouping_type='qwc') + >>> H.grouping_indices + ((0, 1), (2,)) + + This attribute can be used to compute groups of coefficients and observables: + + >>> grouped_coeffs = [coeffs[list(indices)] for indices in H.grouping_indices] + >>> grouped_obs = [[H.ops[i] for i in indices] for indices in H.grouping_indices] + >>> grouped_coeffs + [array([1., 2.]), array([3.])] + >>> grouped_obs + [[X(0), X(1)], [Z(0)]] + + Devices that evaluate a ``qml.Hamiltonian`` expectation by splitting it into its local + observables can use this information to reduce the number of circuits evaluated. + + Note that one can compute the ``grouping_indices`` for an already initialized ``qml.Hamiltonian`` + by using the :func:`compute_grouping ` method. + + .. details:: + :title: Old Hamiltonian behaviour + + The following code examples show the behaviour of ``qml.Hamiltonian`` using old operator + arithmetic. See :doc:`Updated Operators ` for more details. The old + behaviour can be reactivated by calling the deprecated + + >>> qml.operation.disable_new_opmath() + + Alternatively, ``qml.ops.Hamiltonian`` provides a permanent access point for Hamiltonian + behaviour before ``v0.36``. + + >>> coeffs = [0.2, -0.543] + >>> obs = [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Hadamard(2)] + >>> H = qml.Hamiltonian(coeffs, obs) + >>> print(H) + (-0.543) [Z0 H2] + + (0.2) [X0 Z1] + + The coefficients can be a trainable tensor, for example: + + >>> coeffs = tf.Variable([0.2, -0.543], dtype=tf.double) + >>> obs = [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Hadamard(2)] + >>> H = qml.Hamiltonian(coeffs, obs) + >>> print(H) + (-0.543) [Z0 H2] + + (0.2) [X0 Z1] + + The user can also provide custom observables: + + >>> obs_matrix = np.array([[0.5, 1.0j, 0.0, -3j], + [-1.0j, -1.1, 0.0, -0.1], + [0.0, 0.0, -0.9, 12.0], + [3j, -0.1, 12.0, 0.0]]) + >>> obs = qml.Hermitian(obs_matrix, wires=[0, 1]) + >>> H = qml.Hamiltonian((0.8, ), (obs, )) + >>> print(H) + (0.8) [Hermitian0,1] + + Alternatively, the :func:`~.molecular_hamiltonian` function from the + :doc:`/introduction/chemistry` module can be used to generate a molecular + Hamiltonian. + + In many cases, Hamiltonians can be constructed using Pythonic arithmetic operations. + For example: + + >>> qml.Hamiltonian([1.], [qml.X(0)]) + 2 * qml.Z(0) @ qml.Z(1) + + is equivalent to the following Hamiltonian: + + >>> qml.Hamiltonian([1, 2], [qml.X(0), qml.Z(0) @ qml.Z(1)]) + + While scalar multiplication requires native python floats or integer types, + addition, subtraction, and tensor multiplication of Hamiltonians with Hamiltonians or + other observables is possible with tensor-valued coefficients, i.e., + + >>> H1 = qml.Hamiltonian(torch.tensor([1.]), [qml.X(0)]) + >>> H2 = qml.Hamiltonian(torch.tensor([2., 3.]), [qml.Y(0), qml.X(1)]) + >>> obs3 = [qml.X(0), qml.Y(0), qml.X(1)] + >>> H3 = qml.Hamiltonian(torch.tensor([1., 2., 3.]), obs3) + >>> H3.compare(H1 + H2) + True + + A Hamiltonian can store information on which commuting observables should be measured together in + a circuit: + + >>> obs = [qml.X(0), qml.X(1), qml.Z(0)] + >>> coeffs = np.array([1., 2., 3.]) + >>> H = qml.Hamiltonian(coeffs, obs, grouping_type='qwc') + >>> H.grouping_indices + [[0, 1], [2]] + + This attribute can be used to compute groups of coefficients and observables: + + >>> grouped_coeffs = [coeffs[indices] for indices in H.grouping_indices] + >>> grouped_obs = [[H.ops[i] for i in indices] for indices in H.grouping_indices] + >>> grouped_coeffs + [tensor([1., 2.], requires_grad=True), tensor([3.], requires_grad=True)] + >>> grouped_obs + [[qml.X(0), qml.X(1)], [qml.Z(0)]] + + Devices that evaluate a Hamiltonian expectation by splitting it into its local observables can + use this information to reduce the number of circuits evaluated. + + Note that one can compute the ``grouping_indices`` for an already initialized Hamiltonian by + using the :func:`compute_grouping ` method. + + """ diff --git a/pennylane/ops/op_math/prod.py b/pennylane/ops/op_math/prod.py index 7190142f2d3..97da3e157ae 100644 --- a/pennylane/ops/op_math/prod.py +++ b/pennylane/ops/op_math/prod.py @@ -26,7 +26,7 @@ import pennylane as qml from pennylane import math -from pennylane.operation import Operator, convert_to_opmath +from pennylane.operation import Operator from pennylane.ops.op_math.pow import Pow from pennylane.ops.op_math.sprod import SProd from pennylane.ops.op_math.sum import Sum @@ -98,7 +98,6 @@ def prod(*ops, id=None, lazy=True): >>> prod_op CNOT(wires=[0, 1]) @ RX(1.1, wires=[0]) """ - ops = tuple(convert_to_opmath(op) for op in ops) if len(ops) == 1: if isinstance(ops[0], qml.operation.Operator): return ops[0] @@ -282,13 +281,7 @@ def matrix(self, wire_order=None): mats: list[TensorLike] = [] batched: list[bool] = [] # batched[i] tells if mats[i] is batched or not for ops in self.overlapping_ops: - gen = ( - ( - (qml.matrix(op) if isinstance(op, qml.ops.Hamiltonian) else op.matrix()), - op.wires, - ) - for op in ops - ) + gen = ((op.matrix(), op.wires) for op in ops) reduced_mat, _ = math.reduce_matrices(gen, reduce_func=math.matmul) diff --git a/pennylane/ops/op_math/sprod.py b/pennylane/ops/op_math/sprod.py index d73b29fce54..bd8e6a3a3e5 100644 --- a/pennylane/ops/op_math/sprod.py +++ b/pennylane/ops/op_math/sprod.py @@ -20,7 +20,7 @@ import pennylane as qml import pennylane.math as qnp -from pennylane.operation import Operator, TermsUndefinedError, convert_to_opmath +from pennylane.operation import Operator, TermsUndefinedError from pennylane.ops.op_math.pow import Pow from pennylane.ops.op_math.sum import Sum from pennylane.queuing import QueuingManager @@ -73,7 +73,6 @@ def s_prod(scalar, operator, lazy=True, id=None): array([[ 0., 2.], [ 2., 0.]]) """ - operator = convert_to_opmath(operator) if lazy or not isinstance(operator, SProd): return SProd(scalar, operator, id=id) @@ -282,7 +281,7 @@ def has_sparse_matrix(self): @handle_recursion_error def has_matrix(self): """Bool: Whether or not the Operator returns a defined matrix.""" - return isinstance(self.base, qml.ops.Hamiltonian) or self.base.has_matrix + return self.base.has_matrix @staticmethod @handle_recursion_error diff --git a/pennylane/ops/op_math/sum.py b/pennylane/ops/op_math/sum.py index 58590480f40..5401b230ac3 100644 --- a/pennylane/ops/op_math/sum.py +++ b/pennylane/ops/op_math/sum.py @@ -25,7 +25,7 @@ import pennylane as qml from pennylane import math -from pennylane.operation import Operator, convert_to_opmath +from pennylane.operation import Operator from pennylane.queuing import QueuingManager from .composite import CompositeOp, handle_recursion_error @@ -103,7 +103,6 @@ def sum(*summands, grouping_type=None, method="rlf", id=None, lazy=True): ``method`` can be ``"rlf"`` or ``"lf"``. To see more details about how these affect grouping, see :ref:`Pauli Graph Colouring` and :func:`~pennylane.pauli.group_observables`. """ - summands = tuple(convert_to_opmath(op) for op in summands) if lazy: return Sum(*summands, grouping_type=grouping_type, method=method, id=id) @@ -337,10 +336,7 @@ def matrix(self, wire_order=None): """ if self.pauli_rep: return self.pauli_rep.to_mat(wire_order=wire_order or self.wires) - gen = ( - (qml.matrix(op) if isinstance(op, qml.ops.Hamiltonian) else op.matrix(), op.wires) - for op in self - ) + gen = ((op.matrix(), op.wires) for op in self) reduced_mat, sum_wires = math.reduce_matrices(gen, reduce_func=math.add) diff --git a/pennylane/ops/op_math/symbolicop.py b/pennylane/ops/op_math/symbolicop.py index 304684d06e5..5d8ea2a0bfa 100644 --- a/pennylane/ops/op_math/symbolicop.py +++ b/pennylane/ops/op_math/symbolicop.py @@ -210,7 +210,7 @@ def data(self, new_data): @property @handle_recursion_error def has_matrix(self): - return self.base.has_matrix or isinstance(self.base, qml.ops.Hamiltonian) + return self.base.has_matrix @property @handle_recursion_error @@ -259,10 +259,7 @@ def matrix(self, wire_order=None): tensor_like: matrix representation """ # compute base matrix - if isinstance(self.base, qml.ops.Hamiltonian): - base_matrix = qml.matrix(self.base) - else: - base_matrix = self.base.matrix() + base_matrix = self.base.matrix() scalar_interface = qml.math.get_interface(self.scalar) scalar = self.scalar diff --git a/pennylane/ops/qubit/__init__.py b/pennylane/ops/qubit/__init__.py index 9c0172c7384..2c2b932ad0f 100644 --- a/pennylane/ops/qubit/__init__.py +++ b/pennylane/ops/qubit/__init__.py @@ -32,7 +32,6 @@ from ..identity import GlobalPhase, Identity from ..meta import Barrier, Snapshot, WireCut from .arithmetic_ops import * -from .hamiltonian import Hamiltonian from .matrix_ops import * from .non_parametric_ops import * from .observables import * @@ -116,7 +115,6 @@ "Hermitian", "Projector", "SparseHamiltonian", - "Hamiltonian", } diff --git a/pennylane/ops/qubit/attributes.py b/pennylane/ops/qubit/attributes.py index 401c81b155e..934afdd8d09 100644 --- a/pennylane/ops/qubit/attributes.py +++ b/pennylane/ops/qubit/attributes.py @@ -17,7 +17,7 @@ """ from inspect import isclass -from pennylane.operation import Operator, Tensor +from pennylane.operation import Operator class Attribute(set): @@ -74,12 +74,6 @@ def __contains__(self, obj): if isinstance(obj, str): return super().__contains__(obj) - # Hotfix: return False for all tensors. - # Can be removed or updated when tensor class is - # improved. - if isinstance(obj, Tensor): - return False - if isinstance(obj, Operator): return super().__contains__(obj.name) diff --git a/pennylane/ops/qubit/hamiltonian.py b/pennylane/ops/qubit/hamiltonian.py deleted file mode 100644 index 9c37be86146..00000000000 --- a/pennylane/ops/qubit/hamiltonian.py +++ /dev/null @@ -1,916 +0,0 @@ -# Copyright 2018-2021 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 submodule contains the discrete-variable quantum operations that perform -arithmetic operations on their input states. -""" -import functools - -# pylint: disable=too-many-arguments,too-many-instance-attributes -import itertools -import numbers -from collections.abc import Iterable -from copy import copy -from typing import Hashable, Literal, Optional, Union -from warnings import warn - -import numpy as np -import scipy - -import pennylane as qml -from pennylane.operation import FlatPytree, Observable, Tensor -from pennylane.typing import TensorLike -from pennylane.wires import Wires, WiresLike - -OBS_MAP = {"PauliX": "X", "PauliY": "Y", "PauliZ": "Z", "Hadamard": "H", "Identity": "I"} - - -def _compute_grouping_indices( - observables: list[Observable], - grouping_type: Literal["qwc", "commuting", "anticommuting"] = "qwc", - method: Literal["lf", "rlf"] = "lf", -): - # todo: directly compute the - # indices, instead of extracting groups of observables first - observable_groups = qml.pauli.group_observables( - observables, coefficients=None, grouping_type=grouping_type, method=method - ) - - observables = copy(observables) - - indices = [] - available_indices = list(range(len(observables))) - for partition in observable_groups: # pylint:disable=too-many-nested-blocks - indices_this_group = [] - for pauli_word in partition: - # find index of this pauli word in remaining original observables, - for ind, observable in enumerate(observables): - if qml.pauli.are_identical_pauli_words(pauli_word, observable): - indices_this_group.append(available_indices[ind]) - # delete this observable and its index, so it cannot be found again - observables.pop(ind) - available_indices.pop(ind) - break - indices.append(tuple(indices_this_group)) - - return tuple(indices) - - -class Hamiltonian(Observable): - r"""Operator representing a Hamiltonian. - - The Hamiltonian is represented as a linear combination of other operators, e.g., - :math:`\sum_{k=0}^{N-1} c_k O_k`, where the :math:`c_k` are trainable parameters. - - .. warning:: - - As of ``v0.39``, ``qml.ops.Hamiltonian`` is deprecated. When using the new operator arithmetic, - ``qml.Hamiltonian`` will dispatch to :class:`~pennylane.ops.op_math.LinearCombination`. See - :doc:`Updated Operators ` for more details. - - Args: - coeffs (tensor_like): coefficients of the Hamiltonian expression - observables (Iterable[Observable]): observables in the Hamiltonian expression, of same length as coeffs - grouping_type (str): If not None, compute and store information on how to group commuting - observables upon initialization. This information may be accessed when QNodes containing this - Hamiltonian are executed on devices. The string refers to the type of binary relation between Pauli words. - Can be ``'qwc'`` (qubit-wise commuting), ``'commuting'``, or ``'anticommuting'``. - method (str): The graph colouring heuristic to use in solving minimum clique cover for grouping, which - can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest First). Ignored if ``grouping_type=None``. - id (str): name to be assigned to this Hamiltonian instance - - **Example:** - - .. note:: - As of ``v0.36``, ``qml.Hamiltonian`` dispatches to :class:`~.pennylane.ops.op_math.LinearCombination` - by default, so the following examples assume this behaviour. - - ``qml.Hamiltonian`` takes in a list of coefficients and a list of operators. - - >>> coeffs = [0.2, -0.543] - >>> obs = [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Hadamard(2)] - >>> H = qml.Hamiltonian(coeffs, obs) - >>> print(H) - 0.2 * (X(0) @ Z(1)) + -0.543 * (Z(0) @ H(2)) - - The coefficients can be a trainable tensor, for example: - - >>> coeffs = tf.Variable([0.2, -0.543], dtype=tf.double) - >>> obs = [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Hadamard(2)] - >>> H = qml.Hamiltonian(coeffs, obs) - >>> print(H) - 0.2 * (X(0) @ Z(1)) + -0.543 * (Z(0) @ H(2)) - - A ``qml.Hamiltonian`` stores information on which commuting observables should be measured - together in a circuit: - - >>> obs = [qml.X(0), qml.X(1), qml.Z(0)] - >>> coeffs = np.array([1., 2., 3.]) - >>> H = qml.Hamiltonian(coeffs, obs, grouping_type='qwc') - >>> H.grouping_indices - ((0, 1), (2,)) - - This attribute can be used to compute groups of coefficients and observables: - - >>> grouped_coeffs = [coeffs[list(indices)] for indices in H.grouping_indices] - >>> grouped_obs = [[H.ops[i] for i in indices] for indices in H.grouping_indices] - >>> grouped_coeffs - [array([1., 2.]), array([3.])] - >>> grouped_obs - [[X(0), X(1)], [Z(0)]] - - Devices that evaluate a ``qml.Hamiltonian`` expectation by splitting it into its local - observables can use this information to reduce the number of circuits evaluated. - - Note that one can compute the ``grouping_indices`` for an already initialized ``qml.Hamiltonian`` - by using the :func:`compute_grouping ` method. - - .. details:: - :title: Old Hamiltonian behaviour - - The following code examples show the behaviour of ``qml.Hamiltonian`` using old operator - arithmetic. See :doc:`Updated Operators ` for more details. The old - behaviour can be reactivated by calling the deprecated - - >>> qml.operation.disable_new_opmath() - - Alternatively, ``qml.ops.Hamiltonian`` provides a permanent access point for Hamiltonian - behaviour before ``v0.36``. - - >>> coeffs = [0.2, -0.543] - >>> obs = [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Hadamard(2)] - >>> H = qml.Hamiltonian(coeffs, obs) - >>> print(H) - (-0.543) [Z0 H2] - + (0.2) [X0 Z1] - - The coefficients can be a trainable tensor, for example: - - >>> coeffs = tf.Variable([0.2, -0.543], dtype=tf.double) - >>> obs = [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Hadamard(2)] - >>> H = qml.Hamiltonian(coeffs, obs) - >>> print(H) - (-0.543) [Z0 H2] - + (0.2) [X0 Z1] - - The user can also provide custom observables: - - >>> obs_matrix = np.array([[0.5, 1.0j, 0.0, -3j], - [-1.0j, -1.1, 0.0, -0.1], - [0.0, 0.0, -0.9, 12.0], - [3j, -0.1, 12.0, 0.0]]) - >>> obs = qml.Hermitian(obs_matrix, wires=[0, 1]) - >>> H = qml.Hamiltonian((0.8, ), (obs, )) - >>> print(H) - (0.8) [Hermitian0,1] - - Alternatively, the :func:`~.molecular_hamiltonian` function from the - :doc:`/introduction/chemistry` module can be used to generate a molecular - Hamiltonian. - - In many cases, Hamiltonians can be constructed using Pythonic arithmetic operations. - For example: - - >>> qml.Hamiltonian([1.], [qml.X(0)]) + 2 * qml.Z(0) @ qml.Z(1) - - is equivalent to the following Hamiltonian: - - >>> qml.Hamiltonian([1, 2], [qml.X(0), qml.Z(0) @ qml.Z(1)]) - - While scalar multiplication requires native python floats or integer types, - addition, subtraction, and tensor multiplication of Hamiltonians with Hamiltonians or - other observables is possible with tensor-valued coefficients, i.e., - - >>> H1 = qml.Hamiltonian(torch.tensor([1.]), [qml.X(0)]) - >>> H2 = qml.Hamiltonian(torch.tensor([2., 3.]), [qml.Y(0), qml.X(1)]) - >>> obs3 = [qml.X(0), qml.Y(0), qml.X(1)] - >>> H3 = qml.Hamiltonian(torch.tensor([1., 2., 3.]), obs3) - >>> H3.compare(H1 + H2) - True - - A Hamiltonian can store information on which commuting observables should be measured together in - a circuit: - - >>> obs = [qml.X(0), qml.X(1), qml.Z(0)] - >>> coeffs = np.array([1., 2., 3.]) - >>> H = qml.Hamiltonian(coeffs, obs, grouping_type='qwc') - >>> H.grouping_indices - [[0, 1], [2]] - - This attribute can be used to compute groups of coefficients and observables: - - >>> grouped_coeffs = [coeffs[indices] for indices in H.grouping_indices] - >>> grouped_obs = [[H.ops[i] for i in indices] for indices in H.grouping_indices] - >>> grouped_coeffs - [tensor([1., 2.], requires_grad=True), tensor([3.], requires_grad=True)] - >>> grouped_obs - [[qml.X(0), qml.X(1)], [qml.Z(0)]] - - Devices that evaluate a Hamiltonian expectation by splitting it into its local observables can - use this information to reduce the number of circuits evaluated. - - Note that one can compute the ``grouping_indices`` for an already initialized Hamiltonian by - using the :func:`compute_grouping ` method. - - """ - - num_wires = qml.operation.AnyWires - grad_method = "A" # supports analytic gradients - batch_size = None - ndim_params = None # could be (0,) * len(coeffs), but it is not needed. Define at class-level - - def _flatten(self) -> FlatPytree: - # note that we are unable to restore grouping type or method without creating new properties - return (self.data, self._ops), (self.grouping_indices,) - - @classmethod - def _unflatten( - cls, data: tuple[tuple[float, ...], list[Observable]], metadata: tuple[list[list[int]]] - ): - return cls(data[0], data[1], _grouping_indices=metadata[0]) - - # pylint: disable=arguments-differ - @classmethod - def _primitive_bind_call(cls, coeffs, observables, **kwargs): - return cls._primitive.bind(*coeffs, *observables, **kwargs, n_obs=len(observables)) - - def __init__( - self, - coeffs: TensorLike, - observables: Iterable[Observable], - grouping_type: Literal[None, "qwc", "commuting", "anticommuting"] = None, - _grouping_indices: Optional[list[list[int]]] = None, - method: Literal["lf", "rlf"] = "rlf", - id: str = None, - ): - warn( - "qml.ops.Hamiltonian uses the old approach to operator arithmetic, which will become " - "unavailable in version 0.40 of PennyLane. If you are experiencing issues, visit " - "https://docs.pennylane.ai/en/stable/news/new_opmath.html or contact the PennyLane " - "team on the discussion forum: https://discuss.pennylane.ai/.", - qml.PennyLaneDeprecationWarning, - ) - - if qml.math.shape(coeffs)[0] != len(observables): - raise ValueError( - "Could not create valid Hamiltonian; " - "number of coefficients and operators does not match." - ) - - for obs in observables: - if not isinstance(obs, Observable): - raise ValueError( - "Could not create circuits. Some or all observables are not valid." - ) - - self._coeffs = coeffs - self._ops = list(observables) - - # TODO: avoid having multiple ways to store ops and coeffs, - # ideally only use parameters for coeffs, and hyperparameters for ops - self._hyperparameters = {"ops": self._ops} - - self._wires = qml.wires.Wires.all_wires([op.wires for op in self.ops], sort=True) - - # attribute to store indices used to form groups of - # commuting observables, since recomputation is costly - self._grouping_indices = _grouping_indices - - if grouping_type is not None: - with qml.QueuingManager.stop_recording(): - self._grouping_indices = _compute_grouping_indices( - self.ops, grouping_type=grouping_type, method=method - ) - - coeffs_flat = [self._coeffs[i] for i in range(qml.math.shape(self._coeffs)[0])] - - # create the operator using each coefficient as a separate parameter; - # this causes H.data to be a list of tensor scalars, - # while H.coeffs is the original tensor - - super().__init__(*coeffs_flat, wires=self._wires, id=id) - self._pauli_rep = "unset" - - def __len__(self) -> int: - """The number of terms in the Hamiltonian.""" - return len(self.ops) - - @property - def pauli_rep(self) -> Optional["qml.pauli.PauliSentence"]: - if self._pauli_rep != "unset": - return self._pauli_rep - - if any(op.pauli_rep is None for op in self.ops): - self._pauli_rep = None - return self._pauli_rep - - ps = qml.pauli.PauliSentence() - for coeff, term in zip(*self.terms()): - ps += term.pauli_rep * coeff - - self._pauli_rep = ps - return self._pauli_rep - - def _check_batching(self): - """Override for Hamiltonian, batching is not yet supported.""" - - def label( - self, - decimals: Optional[int] = None, - base_label: Optional[str] = None, - cache: Optional[dict] = None, - ): - decimals = None if (len(self.parameters) > 3) else decimals - return super().label(decimals=decimals, base_label=base_label or "𝓗", cache=cache) - - @property - def coeffs(self) -> TensorLike: - """Return the coefficients defining the Hamiltonian. - - Returns: - Sequence[float]): coefficients in the Hamiltonian expression - """ - return self._coeffs - - @property - def ops(self) -> list[Observable]: - """Return the operators defining the Hamiltonian. - - Returns: - list[Observable]): observables in the Hamiltonian expression - """ - return self._ops - - def terms(self) -> tuple[list[TensorLike], list[Observable]]: - r"""Representation of the operator as a linear combination of other operators. - - .. math:: O = \sum_i c_i O_i - - .. seealso:: :meth:`~.Hamiltonian.terms` - - Returns: - tuple[Iterable[tensor_like or float], list[.Operator]]: coefficients and operations - - **Example** - >>> coeffs = [1., 2.] - >>> ops = [qml.X(0), qml.Z(0)] - >>> H = qml.Hamiltonian(coeffs, ops) - - >>> H.terms() - [1., 2.], [qml.X(0), qml.Z(0)] - - The coefficients are differentiable and can be stored as tensors: - >>> import tensorflow as tf - >>> H = qml.Hamiltonian([tf.Variable(1.), tf.Variable(2.)], [qml.X(0), qml.Z(0)]) - >>> t = H.terms() - - >>> t[0] - [, ] - """ - return self.parameters, self.ops - - @property - def wires(self) -> Wires: - r"""The sorted union of wires from all operators. - - Returns: - (Wires): Combined wires present in all terms, sorted. - """ - return self._wires - - @property - def name(self) -> str: - return "Hamiltonian" - - @property - def grouping_indices(self) -> Optional[list[list[int]]]: - """Return the grouping indices attribute. - - Returns: - list[list[int]]: indices needed to form groups of commuting observables - """ - return self._grouping_indices - - @grouping_indices.setter - def grouping_indices(self, value: Iterable[Iterable[int]]): - """Set the grouping indices, if known without explicit computation, or if - computation was done externally. The groups are not verified. - - **Example** - - Examples of valid groupings for the Hamiltonian - - >>> H = qml.Hamiltonian([qml.X('a'), qml.X('b'), qml.Y('b')]) - - are - - >>> H.grouping_indices = [[0, 1], [2]] - - or - - >>> H.grouping_indices = [[0, 2], [1]] - - since both ``qml.X('a'), qml.X('b')`` and ``qml.X('a'), qml.Y('b')`` commute. - - - Args: - value (list[list[int]]): List of lists of indexes of the observables in ``self.ops``. Each sublist - represents a group of commuting observables. - """ - - if ( - not isinstance(value, Iterable) - or any(not isinstance(sublist, Iterable) for sublist in value) - or any(i not in range(len(self.ops)) for i in [i for sl in value for i in sl]) - ): - raise ValueError( - f"The grouped index value needs to be a tuple of tuples of integers between 0 and the " - f"number of observables in the Hamiltonian; got {value}" - ) - # make sure all tuples so can be hashable - self._grouping_indices = tuple(tuple(sublist) for sublist in value) - - def compute_grouping( - self, - grouping_type: Literal["qwc", "commuting", "anticommuting"] = "qwc", - method: Literal["lf", "rlf"] = "lf", - ): - """ - Compute groups of indices corresponding to commuting observables of this - Hamiltonian, and store it in the ``grouping_indices`` attribute. - - Args: - grouping_type (str): The type of binary relation between Pauli words used to compute the grouping. - Can be ``'qwc'``, ``'commuting'``, or ``'anticommuting'``. - method (str): The graph colouring heuristic to use in solving minimum clique cover for grouping, which - can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest First). - """ - - with qml.QueuingManager.stop_recording(): - self._grouping_indices = _compute_grouping_indices( - self.ops, grouping_type=grouping_type, method=method - ) - - def sparse_matrix(self, wire_order: Optional[WiresLike] = None) -> scipy.sparse.csr_matrix: - r"""Computes the sparse matrix representation of a Hamiltonian in the computational basis. - - Args: - wire_order (Iterable): global wire order, must contain all wire labels from the operator's wires. - If not provided, the default order of the wires (self.wires) of the Hamiltonian is used. - - Returns: - csr_matrix: a sparse matrix in scipy Compressed Sparse Row (CSR) format with dimension - :math:`(2^n, 2^n)`, where :math:`n` is the number of wires - - **Example:** - - >>> coeffs = [1, -0.45] - >>> obs = [qml.Z(0) @ qml.Z(1), qml.Y(0) @ qml.Z(1)] - >>> H = qml.Hamiltonian(coeffs, obs) - >>> H_sparse = H.sparse_matrix() - >>> H_sparse - <4x4 sparse matrix of type '' - with 8 stored elements in Compressed Sparse Row format> - - The resulting sparse matrix can be either used directly or transformed into a numpy array: - - >>> H_sparse.toarray() - array([[ 1.+0.j , 0.+0.j , 0.+0.45j, 0.+0.j ], - [ 0.+0.j , -1.+0.j , 0.+0.j , 0.-0.45j], - [ 0.-0.45j, 0.+0.j , -1.+0.j , 0.+0.j ], - [ 0.+0.j , 0.+0.45j, 0.+0.j , 1.+0.j ]]) - """ - if wire_order is None: - wires = self.wires - else: - wires = wire_order - n = len(wires) - matrix = scipy.sparse.csr_matrix((2**n, 2**n), dtype="complex128") - - coeffs = qml.math.toarray(self.data) - - temp_mats = [] - for coeff, op in zip(coeffs, self.ops): - obs = [] - for o in qml.operation.Tensor(op).obs: - if len(o.wires) > 1: - # todo: deal with operations created from multi-qubit operations such as Hermitian - raise ValueError( - f"Can only sparsify Hamiltonians whose constituent observables consist of " - f"(tensor products of) single-qubit operators; got {op}." - ) - obs.append(o.matrix()) - - # Array to store the single-wire observables which will be Kronecker producted together - mat = [] - # i_count tracks the number of consecutive single-wire identity matrices encountered - # in order to avoid unnecessary Kronecker products, since I_n x I_m = I_{n+m} - i_count = 0 - for wire_lab in wires: - if wire_lab in op.wires: - if i_count > 0: - mat.append(scipy.sparse.eye(2**i_count, format="coo")) - i_count = 0 - idx = op.wires.index(wire_lab) - # obs is an array storing the single-wire observables which - # make up the full Hamiltonian term - sp_obs = scipy.sparse.coo_matrix(obs[idx]) - mat.append(sp_obs) - else: - i_count += 1 - - if i_count > 0: - mat.append(scipy.sparse.eye(2**i_count, format="coo")) - - red_mat = ( - functools.reduce(lambda i, j: scipy.sparse.kron(i, j, format="coo"), mat) * coeff - ) - - temp_mats.append(red_mat.tocsr()) - # Value of 100 arrived at empirically to balance time savings vs memory use. At this point - # the `temp_mats` are summed into the final result and the temporary storage array is - # cleared. - if (len(temp_mats) % 100) == 0: - matrix += sum(temp_mats) - temp_mats = [] - - matrix += sum(temp_mats) - return matrix - - def simplify(self) -> "Hamiltonian": - r"""Simplifies the Hamiltonian by combining like-terms. - - **Example** - - >>> ops = [qml.Y(2), qml.X(0) @ qml.Identity(1), qml.X(0)] - >>> H = qml.Hamiltonian([1, 1, -2], ops) - >>> H.simplify() - >>> print(H) - (-1) [X0] - + (1) [Y2] - - .. warning:: - - Calling this method will reset ``grouping_indices`` to None, since - the observables it refers to are updated. - """ - - # Todo: make simplify return a new operation, so - # it does not mutate this one - - new_coeffs = [] - new_ops = [] - - for i in range(len(self.ops)): # pylint: disable=consider-using-enumerate - op = self.ops[i] - c = self.coeffs[i] - op = op if isinstance(op, Tensor) else Tensor(op) - - ind = next((j for j, o in enumerate(new_ops) if op.compare(o)), None) - if ind is not None: - new_coeffs[ind] += c - if np.isclose(qml.math.toarray(new_coeffs[ind]), np.array(0.0)): - del new_coeffs[ind] - del new_ops[ind] - else: - new_ops.append(op.prune()) - new_coeffs.append(c) - - # hotfix: We `self.data`, since `self.parameters` returns a copy of the data and is now returned in - # self.terms(). To be improved soon. - self.data = tuple(new_coeffs) - # hotfix: We overwrite the hyperparameter entry, which is now returned in self.terms(). - # To be improved soon. - self.hyperparameters["ops"] = new_ops - - self._coeffs = qml.math.stack(new_coeffs) if new_coeffs else [] - self._ops = new_ops - self._wires = qml.wires.Wires.all_wires([op.wires for op in self.ops], sort=True) - # reset grouping, since the indices refer to the old observables and coefficients - self._grouping_indices = None - return self - - def __str__(self) -> str: - def wires_print(ob: Observable): - """Function that formats the wires.""" - return ",".join(map(str, ob.wires.tolist())) - - list_of_coeffs = self.data # list of scalar tensors - paired_coeff_obs = list(zip(list_of_coeffs, self.ops)) - paired_coeff_obs.sort(key=lambda pair: (len(pair[1].wires), qml.math.real(pair[0]))) - - terms_ls = [] - - for coeff, obs in paired_coeff_obs: - if isinstance(obs, Tensor): - obs_strs = [f"{OBS_MAP.get(ob.name, ob.name)}{wires_print(ob)}" for ob in obs.obs] - ob_str = " ".join(obs_strs) - elif isinstance(obs, Observable): - ob_str = f"{OBS_MAP.get(obs.name, obs.name)}{wires_print(obs)}" - - term_str = f"({coeff}) [{ob_str}]" - - terms_ls.append(term_str) - - return " " + "\n+ ".join(terms_ls) - - def __repr__(self) -> str: - # Constructor-call-like representation - return f"" - - def _ipython_display_(self): # pragma: no-cover - """Displays __str__ in ipython instead of __repr__ - See https://ipython.readthedocs.io/en/stable/config/integrating.html - """ - if len(self.ops) < 15: - print(str(self)) - else: # pragma: no-cover - print(repr(self)) - - def _obs_data(self) -> set[tuple[TensorLike, frozenset[tuple[str, Wires, list[str]]]]]: - r"""Extracts the data from a Hamiltonian and serializes it in an order-independent fashion. - - This allows for comparison between Hamiltonians that are equivalent, but are defined with terms and tensors - expressed in different orders. For example, `qml.X(0) @ qml.Z(1)` and - `qml.Z(1) @ qml.X(0)` are equivalent observables with different orderings. - - .. Note:: - - In order to store the data from each term of the Hamiltonian in an order-independent serialization, - we make use of sets. Note that all data contained within each term must be immutable, hence the use of - strings and frozensets. - - **Example** - - >>> H = qml.Hamiltonian([1, 1], [qml.X(0) @ qml.X(1), qml.Z(0)]) - >>> print(H._obs_data()) - {(1, frozenset({('PauliX', Wires([1]), ()), ('PauliX', Wires([0]), ())})), - (1, frozenset({('PauliZ', Wires([0]), ())}))} - """ - data = set() - - coeffs_arr = qml.math.toarray(self.coeffs) - for co, op in zip(coeffs_arr, self.ops): - obs = op.non_identity_obs if isinstance(op, Tensor) else [op] - tensor = [] - for ob in obs: - parameters = tuple( - str(param) for param in ob.parameters - ) # Converts params into immutable type - if isinstance(ob, qml.GellMann): - parameters += (ob.hyperparameters["index"],) - tensor.append((ob.name, ob.wires, parameters)) - data.add((co, frozenset(tensor))) - - return data - - def compare(self, other: Observable) -> bool: - r"""Determines whether the operator is equivalent to another. - - Currently only supported for :class:`~Hamiltonian`, :class:`~.Observable`, or :class:`~.Tensor`. - Hamiltonians/observables are equivalent if they represent the same operator - (their matrix representations are equal), and they are defined on the same wires. - - .. Warning:: - - The compare method does **not** check if the matrix representation - of a :class:`~.Hermitian` observable is equal to an equivalent - observable expressed in terms of Pauli matrices, or as a - linear combination of Hermitians. - To do so would require the matrix form of Hamiltonians and Tensors - be calculated, which would drastically increase runtime. - - Returns: - (bool): True if equivalent. - - **Examples** - - >>> H = qml.Hamiltonian( - ... [0.5, 0.5], - ... [qml.Z(0) @ qml.Y(1), qml.Y(1) @ qml.Z(0) @ qml.Identity("a")] - ... ) - >>> obs = qml.Z(0) @ qml.Y(1) - >>> print(H.compare(obs)) - True - - >>> H1 = qml.Hamiltonian([1, 1], [qml.X(0), qml.Z(1)]) - >>> H2 = qml.Hamiltonian([1, 1], [qml.Z(0), qml.X(1)]) - >>> H1.compare(H2) - False - - >>> ob1 = qml.Hamiltonian([1], [qml.X(0)]) - >>> ob2 = qml.Hermitian(np.array([[0, 1], [1, 0]]), 0) - >>> ob1.compare(ob2) - False - """ - - if isinstance(other, qml.operation.Operator): - if (pr1 := self.pauli_rep) is not None and (pr2 := other.pauli_rep) is not None: - pr1.simplify() - pr2.simplify() - return pr1 == pr2 - - if isinstance(other, Hamiltonian): - self.simplify() - other.simplify() - return self._obs_data() == other._obs_data() # pylint: disable=protected-access - - if isinstance(other, (Tensor, Observable)): - self.simplify() - return self._obs_data() == { - (1, frozenset(other._obs_data())) # pylint: disable=protected-access - } - - raise ValueError("Can only compare a Hamiltonian, and a Hamiltonian/Observable/Tensor.") - - def __matmul__(self, H: Observable) -> Observable: - r"""The tensor product operation between a Hamiltonian and a Hamiltonian/Tensor/Observable.""" - coeffs1 = copy(self.coeffs) - ops1 = self.ops.copy() - - qml.QueuingManager.remove(H) - qml.QueuingManager.remove(self) - - if isinstance(H, Hamiltonian): - shared_wires = Wires.shared_wires([self.wires, H.wires]) - if len(shared_wires) > 0: - raise ValueError( - "Hamiltonians can only be multiplied together if they act on " - "different sets of wires" - ) - - coeffs2 = H.coeffs - ops2 = H.ops - - coeffs = qml.math.kron(coeffs1, coeffs2) - ops_list = itertools.product(ops1, ops2) - terms = [qml.operation.Tensor(t[0], t[1]) for t in ops_list] - return qml.simplify(Hamiltonian(coeffs, terms)) - - if isinstance(H, (Tensor, Observable)): - terms = [op @ copy(H) for op in ops1] - - return qml.simplify(Hamiltonian(coeffs1, terms)) - - return NotImplemented - - def __rmatmul__(self, H: Observable): - r"""The tensor product operation (from the right) between a Hamiltonian and - a Hamiltonian/Tensor/Observable (ie. Hamiltonian.__rmul__(H) = H @ Hamiltonian). - """ - if isinstance(H, Hamiltonian): # can't be accessed by '@' - return H.__matmul__(self) - - coeffs1 = copy(self.coeffs) - ops1 = self.ops.copy() - - if isinstance(H, (Tensor, Observable)): - qml.QueuingManager.remove(H) - qml.QueuingManager.remove(self) - terms = [copy(H) @ op for op in ops1] - return qml.simplify(Hamiltonian(coeffs1, terms)) - - return NotImplemented - - def __add__(self, H: Observable) -> Observable: - r"""The addition operation between a Hamiltonian and a Hamiltonian/Tensor/Observable.""" - ops = self.ops.copy() - self_coeffs = copy(self.coeffs) - - if isinstance(H, numbers.Number) and H == 0: - return self - - if isinstance(H, Hamiltonian): - qml.QueuingManager.remove(H) - qml.QueuingManager.remove(self) - coeffs = qml.math.concatenate([self_coeffs, copy(H.coeffs)], axis=0) - ops.extend(H.ops.copy()) - return qml.simplify(Hamiltonian(coeffs, ops)) - - if isinstance(H, (Tensor, Observable)): - qml.QueuingManager.remove(H) - qml.QueuingManager.remove(self) - coeffs = qml.math.concatenate( - [self_coeffs, qml.math.cast_like([1.0], self_coeffs)], axis=0 - ) - ops.append(H) - return qml.simplify(Hamiltonian(coeffs, ops)) - - return NotImplemented - - __radd__ = __add__ - - def __mul__(self, a: Union[int, float]): - r"""The scalar multiplication operation between a scalar and a Hamiltonian.""" - if isinstance(a, (int, float)): - self_coeffs = copy(self.coeffs) - coeffs = qml.math.multiply(a, self_coeffs) - return Hamiltonian(coeffs, self.ops.copy()) - - return NotImplemented - - __rmul__ = __mul__ - - def __sub__(self, H: Observable) -> Observable: - r"""The subtraction operation between a Hamiltonian and a Hamiltonian/Tensor/Observable.""" - if isinstance(H, (Hamiltonian, Tensor, Observable)): - return self + (-1 * H) - - return NotImplemented - - def __iadd__(self, H: Union[Observable, numbers.Number]): - r"""The inplace addition operation between a Hamiltonian and a Hamiltonian/Tensor/Observable.""" - if isinstance(H, numbers.Number) and H == 0: - return self - - if isinstance(H, Hamiltonian): - self._coeffs = qml.math.concatenate([self._coeffs, H.coeffs], axis=0) - self._ops.extend(H.ops.copy()) - self.simplify() - return self - - if isinstance(H, (Tensor, Observable)): - self._coeffs = qml.math.concatenate( - [self._coeffs, qml.math.cast_like([1.0], self._coeffs)], axis=0 - ) - self._ops.append(H) - self.simplify() - return self - - return NotImplemented - - def __imul__(self, a: Union[int, float]): - r"""The inplace scalar multiplication operation between a scalar and a Hamiltonian.""" - if isinstance(a, (int, float)): - self._coeffs = qml.math.multiply(a, self._coeffs) - if self.pauli_rep is not None: - self._pauli_rep = qml.math.multiply(a, self._pauli_rep) - return self - - return NotImplemented - - def __isub__(self, H: Observable): - r"""The inplace subtraction operation between a Hamiltonian and a Hamiltonian/Tensor/Observable.""" - if isinstance(H, (Hamiltonian, Tensor, Observable)): - self.__iadd__(H.__mul__(-1)) - return self - - return NotImplemented - - def queue( - self, context: Union[qml.QueuingManager, qml.queuing.AnnotatedQueue] = qml.QueuingManager - ): - """Queues a qml.Hamiltonian instance""" - for o in self.ops: - context.remove(o) - context.append(self) - return self - - def map_wires(self, wire_map: dict[Hashable, Hashable]): - """Returns a copy of the current hamiltonian with its wires changed according to the given - wire map. - - Args: - wire_map (dict): dictionary containing the old wires as keys and the new wires as values - - Returns: - .Hamiltonian: new hamiltonian - """ - cls = self.__class__ - new_op = cls.__new__(cls) - new_op.data = copy(self.data) - new_op._wires = Wires( # pylint: disable=protected-access - [wire_map.get(wire, wire) for wire in self.wires] - ) - new_op._ops = [ # pylint: disable=protected-access - op.map_wires(wire_map) for op in self.ops - ] - for attr, value in vars(self).items(): - if attr not in {"data", "_wires", "_ops"}: - setattr(new_op, attr, value) - new_op.hyperparameters["ops"] = new_op._ops # pylint: disable=protected-access - new_op._pauli_rep = "unset" # pylint: disable=protected-access - return new_op - - -# The primitive will be None if jax is not installed in the environment -# If defined, we need to update the implementation to repack the coefficients and observables -# See capture module for more information -if Hamiltonian._primitive is not None: # pylint: disable=protected-access - - @Hamiltonian._primitive.def_impl # pylint: disable=protected-access - def _(*args, n_obs, **kwargs): - coeffs = args[:n_obs] - observables = args[n_obs:] - return type.__call__(Hamiltonian, coeffs, observables, **kwargs) diff --git a/pennylane/optimize/riemannian_gradient.py b/pennylane/optimize/riemannian_gradient.py index 730b45f1852..f60c55678e8 100644 --- a/pennylane/optimize/riemannian_gradient.py +++ b/pennylane/optimize/riemannian_gradient.py @@ -267,7 +267,7 @@ def __init__(self, circuit, stepsize=0.01, restriction=None, exact=False, trotte self.circuit = circuit self.circuit.construct([], {}) self.hamiltonian = circuit.func().obs - if not isinstance(self.hamiltonian, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): + if not isinstance(self.hamiltonian, qml.ops.LinearCombination): raise TypeError( f"circuit must return the expectation value of a Hamiltonian," f"received {type(circuit.func().obs)}" @@ -280,9 +280,7 @@ def __init__(self, circuit, stepsize=0.01, restriction=None, exact=False, trotte f"optimizing a {self.nqubits} qubit circuit may be slow.", UserWarning, ) - if restriction is not None and not isinstance( - restriction, (qml.ops.Hamiltonian, qml.ops.LinearCombination) - ): + if restriction is not None and not isinstance(restriction, qml.ops.LinearCombination): raise TypeError(f"restriction must be a Hamiltonian, received {type(restriction)}") ( self.lie_algebra_basis_ops, diff --git a/pennylane/optimize/shot_adaptive.py b/pennylane/optimize/shot_adaptive.py index 3dbe5d48358..6fc69b01063 100644 --- a/pennylane/optimize/shot_adaptive.py +++ b/pennylane/optimize/shot_adaptive.py @@ -313,7 +313,7 @@ def _single_shot_qnode_gradients(self, qnode, args, kwargs): [expval] = tape.measurements coeffs, observables = ( expval.obs.terms() - if isinstance(expval.obs, (qml.ops.LinearCombination, qml.ops.Hamiltonian)) + if isinstance(expval.obs, qml.ops.LinearCombination) else ([1.0], [expval.obs]) ) diff --git a/pennylane/pauli/__init__.py b/pennylane/pauli/__init__.py index 394be72cb77..1f7d141d5c4 100644 --- a/pennylane/pauli/__init__.py +++ b/pennylane/pauli/__init__.py @@ -33,7 +33,6 @@ diagonalize_pauli_word, diagonalize_qwc_pauli_words, diagonalize_qwc_groupings, - simplify, pauli_eigs, ) diff --git a/pennylane/pauli/conversion.py b/pennylane/pauli/conversion.py index 9e1ddbd38be..859d6ac2670 100644 --- a/pennylane/pauli/conversion.py +++ b/pennylane/pauli/conversion.py @@ -21,18 +21,7 @@ import pennylane as qml from pennylane.math.utils import is_abstract -from pennylane.operation import Tensor -from pennylane.ops import ( - Hamiltonian, - Identity, - LinearCombination, - PauliX, - PauliY, - PauliZ, - Prod, - SProd, - Sum, -) +from pennylane.ops import Identity, LinearCombination, PauliX, PauliY, PauliZ, Prod, SProd, Sum from pennylane.ops.qubit.matrix_ops import _walsh_hadamard_transform from .pauli_arithmetic import I, PauliSentence, PauliWord, X, Y, Z, op_map @@ -230,7 +219,7 @@ def _generalized_pauli_decompose( def pauli_decompose( H, hide_identity=False, wire_order=None, pauli=False, check_hermitian=True -) -> Union[Hamiltonian, PauliSentence]: +) -> Union[LinearCombination, PauliSentence]: r"""Decomposes a Hermitian matrix into a linear combination of Pauli operators. Args: @@ -401,15 +390,6 @@ def _(op: Identity): # pylint:disable=unused-argument return PauliSentence({PauliWord({}): 1.0}) -@_pauli_sentence.register -def _(op: Tensor): - if not is_pauli_word(op): - raise ValueError(f"Op must be a linear combination of Pauli operators only, got: {op}") - - factors = (_pauli_sentence(factor) for factor in op.obs) - return reduce(lambda a, b: a @ b, factors) - - @_pauli_sentence.register def _(op: Prod): factors = (_pauli_sentence(factor) for factor in op) @@ -424,28 +404,6 @@ def _(op: SProd): return ps -@_pauli_sentence.register(qml.ops.Hamiltonian) -def _(op: qml.ops.Hamiltonian): - if not all(is_pauli_word(o) for o in op.ops): - raise ValueError(f"Op must be a linear combination of Pauli operators only, got: {op}") - - def term_2_pauli_word(term): - if isinstance(term, Tensor): - pw = {obs.wires[0]: obs.name[-1] for obs in term.non_identity_obs} - elif isinstance(term, Identity): - pw = {} - else: - pw = dict([(term.wires[0], term.name[-1])]) - return PauliWord(pw) - - ps = PauliSentence() - for coeff, term in zip(*op.terms()): - sub_ps = PauliSentence({term_2_pauli_word(term): coeff}) - ps += sub_ps - - return ps - - @_pauli_sentence.register(LinearCombination) def _(op: LinearCombination): if not all(is_pauli_word(o) for o in op.ops): diff --git a/pennylane/pauli/grouping/group_observables.py b/pennylane/pauli/grouping/group_observables.py index 948d35e50f7..45e1124d066 100644 --- a/pennylane/pauli/grouping/group_observables.py +++ b/pennylane/pauli/grouping/group_observables.py @@ -25,7 +25,6 @@ import rustworkx as rx import pennylane as qml -from pennylane.ops import Prod, SProd from pennylane.pauli.utils import ( are_identical_pauli_words, binary_to_pauli, @@ -477,8 +476,8 @@ def group_observables( graph using graph-colouring heuristic algorithms. Args: - observables (list[Observable]): a list of Pauli word ``Observable`` instances (Pauli - operation instances and :class:`~.Tensor` instances thereof) + observables (list[Operator]): a list of Pauli word ``Observable`` instances (Pauli + operation instances and tensor products thereof) coefficients (TensorLike): A tensor or list of coefficients. If not specified, output ``partitioned_coeffs`` is not returned. grouping_type (str): The type of binary relation between Pauli words. @@ -537,18 +536,7 @@ def group_observables( wires_obs, grouping_type=grouping_type, graph_colourer=method ) - # Handles legacy op_math - temp_opmath = not qml.operation.active_new_opmath() and any( - isinstance(o, (Prod, SProd)) for o in observables - ) - if temp_opmath: - qml.operation.enable_new_opmath(warn=False) - - try: - partitioned_paulis = pauli_groupper.partition_observables() - finally: - if temp_opmath: - qml.operation.disable_new_opmath(warn=False) + partitioned_paulis = pauli_groupper.partition_observables() # Add observables without wires back to the first partition partitioned_paulis[0].extend(no_wires_obs) @@ -582,15 +570,6 @@ def _partition_coeffs(partitioned_paulis, observables, coefficients): for pauli_word in partition: # find index of this pauli word in remaining original observables, for ind, observable in enumerate(observables): - if isinstance(observable, qml.ops.Hamiltonian): - # are_identical_pauli_words cannot handle Hamiltonian - coeffs, ops = observable.terms() - # Assuming the Hamiltonian has only one term - observable = qml.s_prod(coeffs[0], ops[0]) - if isinstance(pauli_word, qml.ops.Hamiltonian): - # Need to add this case because rx methods do not change type of observables. - coeffs, ops = pauli_word.terms() - pauli_word = qml.s_prod(coeffs[0], ops[0]) if are_identical_pauli_words(pauli_word, observable): indices.append(coeff_indices[ind]) observables.pop(ind) diff --git a/pennylane/pauli/pauli_arithmetic.py b/pennylane/pauli/pauli_arithmetic.py index 9822fd6cadf..b453582397e 100644 --- a/pennylane/pauli/pauli_arithmetic.py +++ b/pennylane/pauli/pauli_arithmetic.py @@ -15,14 +15,12 @@ # pylint:disable=protected-access from copy import copy from functools import lru_cache, reduce -from warnings import warn import numpy as np from scipy import sparse import pennylane as qml from pennylane import math -from pennylane.operation import Tensor from pennylane.ops import Identity, PauliX, PauliY, PauliZ, Prod, SProd, Sum from pennylane.typing import TensorLike from pennylane.wires import Wires @@ -506,7 +504,7 @@ def _get_csr_indices(self, wire_order): current_size *= 2 return indices - def operation(self, wire_order=None, get_as_tensor=False): + def operation(self, wire_order=None): """Returns a native PennyLane :class:`~pennylane.operation.Operation` representing the PauliWord.""" if len(self) == 0: return Identity(wires=wire_order) @@ -517,33 +515,9 @@ def operation(self, wire_order=None, get_as_tensor=False): else: factors = [_make_operation(op, wire) for wire, op in self.items()] - if get_as_tensor: - return factors[0] if len(factors) == 1 else Tensor(*factors) pauli_rep = PauliSentence({self: 1}) return factors[0] if len(factors) == 1 else Prod(*factors, _pauli_rep=pauli_rep) - def hamiltonian(self, wire_order=None): - """Return :class:`~pennylane.Hamiltonian` representing the PauliWord. - - .. warning:: - - :meth:`~pennylane.pauli.PauliWord.hamiltonian` is deprecated. Instead, please use - :meth:`~pennylane.pauli.PauliWord.operation` - - """ - warn( - "PauliWord.hamiltonian() is deprecated. Please use PauliWord.operation() instead.", - qml.PennyLaneDeprecationWarning, - ) - - if len(self) == 0: - if wire_order in (None, [], Wires([])): - raise ValueError("Can't get the Hamiltonian for an empty PauliWord.") - return qml.Hamiltonian([1], [Identity(wires=wire_order)]) - - obs = [_make_operation(op, wire) for wire, op in self.items()] - return qml.Hamiltonian([1], [obs[0] if len(obs) == 1 else Tensor(*obs)]) - def map_wires(self, wire_map: dict) -> "PauliWord": """Return a new PauliWord with the wires mapped.""" return self.__class__({wire_map.get(w, w): op for w, op in self.items()}) @@ -1038,33 +1012,6 @@ def operation(self, wire_order=None): summands.append(pw_op if coeff == 1 else SProd(coeff, pw_op, _pauli_rep=rep)) return summands[0] if len(summands) == 1 else Sum(*summands, _pauli_rep=self) - def hamiltonian(self, wire_order=None): - """Returns a native PennyLane :class:`~pennylane.Hamiltonian` representing the PauliSentence. - - .. warning:: - - :meth:`~pennylane.pauli.PauliSentence.hamiltonian` is deprecated. Instead, please use - :meth:`~pennylane.pauli.PauliSentence.operation` - - """ - warn( - "PauliSentence.hamiltonian() is deprecated. Please use PauliSentence.operation() instead.", - qml.PennyLaneDeprecationWarning, - ) - - if len(self) == 0: - if wire_order in (None, [], Wires([])): - raise ValueError("Can't get the Hamiltonian for an empty PauliSentence.") - return qml.Hamiltonian([], []) - - wire_order = wire_order or self.wires - wire_order = list(wire_order) - - return qml.Hamiltonian( - list(self.values()), - [pw.operation(wire_order=wire_order, get_as_tensor=True) for pw in self], - ) - def simplify(self, tol=1e-8): """Remove any PauliWords in the PauliSentence with coefficients less than the threshold tolerance.""" items = list(self.items()) diff --git a/pennylane/pauli/pauli_interface.py b/pennylane/pauli/pauli_interface.py index 064ebd20170..9d252ab6e60 100644 --- a/pennylane/pauli/pauli_interface.py +++ b/pennylane/pauli/pauli_interface.py @@ -17,19 +17,8 @@ from functools import singledispatch from typing import Union -from pennylane.operation import Tensor -from pennylane.ops import ( - Hamiltonian, - Identity, - LinearCombination, - PauliX, - PauliY, - PauliZ, - Prod, - SProd, -) - -from .conversion import pauli_sentence +from pennylane.ops import Identity, LinearCombination, PauliX, PauliY, PauliZ, Prod, SProd + from .utils import is_pauli_word @@ -74,16 +63,8 @@ def _pw_prefactor_pauli( return 1 -@_pauli_word_prefactor.register -def _pw_prefactor_tensor(observable: Tensor): - if is_pauli_word(observable): - return list(pauli_sentence(observable).values())[0] # only one term, - raise ValueError(f"Expected a valid Pauli word, got {observable}") - - -@_pauli_word_prefactor.register(Hamiltonian) @_pauli_word_prefactor.register(LinearCombination) -def _pw_prefactor_ham(observable: Union[Hamiltonian, LinearCombination]): +def _pw_prefactor_ham(observable: LinearCombination): if is_pauli_word(observable): return observable.coeffs[0] raise ValueError(f"Expected a valid Pauli word, got {observable}") diff --git a/pennylane/pauli/utils.py b/pennylane/pauli/utils.py index 8f0ec975ec0..42ea2600af1 100644 --- a/pennylane/pauli/utils.py +++ b/pennylane/pauli/utils.py @@ -23,22 +23,11 @@ from functools import lru_cache, singledispatch from itertools import product from typing import Union -from warnings import warn import numpy as np import pennylane as qml -from pennylane.operation import Tensor -from pennylane.ops import ( - Hamiltonian, - Identity, - LinearCombination, - PauliX, - PauliY, - PauliZ, - Prod, - SProd, -) +from pennylane.ops import Identity, PauliX, PauliY, PauliZ, Prod, SProd, Sum from pennylane.wires import Wires # To make this quicker later on @@ -68,13 +57,11 @@ def is_pauli_word(observable): * A single pauli operator (see :class:`~.PauliX` for an example). - * A :class:`.Tensor` instance containing Pauli operators. - * A :class:`.Prod` instance containing Pauli operators. * A :class:`.SProd` instance containing a valid Pauli word. - * A :class:`.Hamiltonian` instance with only one term. + * A :class:`.Sum` instance with only one term. .. Warning:: @@ -123,16 +110,10 @@ def _is_pw_pauli( return True -@_is_pauli_word.register -def _is_pw_tensor(observable: Tensor): - pauli_word_names = ["Identity", "PauliX", "PauliY", "PauliZ"] - return set(observable.name).issubset(pauli_word_names) - - -@_is_pauli_word.register(Hamiltonian) -@_is_pauli_word.register(LinearCombination) -def _is_pw_ham(observable: Union[Hamiltonian, LinearCombination]): - return False if len(observable.ops) != 1 else is_pauli_word(observable.ops[0]) +@_is_pauli_word.register(Sum) +def _is_pw_ham(observable: Sum): + ops = observable.terms()[1] + return False if len(ops) != 1 else is_pauli_word(ops[0]) @_is_pauli_word.register @@ -149,20 +130,19 @@ def are_identical_pauli_words(pauli_1, pauli_2): # pylint: disable=isinstance-second-argument-not-valid-type """Performs a check if two Pauli words have the same ``wires`` and ``name`` attributes. - This is a convenience function that checks if two given :class:`~.Tensor` or :class:`~.Prod` + This is a convenience function that checks if two given :class:`~.Prod` instances specify the same Pauli word. Args: - pauli_1 (Union[Identity, PauliX, PauliY, PauliZ, Tensor, Prod, SProd]): the first Pauli word - pauli_2 (Union[Identity, PauliX, PauliY, PauliZ, Tensor, Prod, SProd]): the second Pauli word + pauli_1 (Union[Identity, PauliX, PauliY, PauliZ, Prod, SProd]): the first Pauli word + pauli_2 (Union[Identity, PauliX, PauliY, PauliZ, Prod, SProd]): the second Pauli word Returns: bool: whether ``pauli_1`` and ``pauli_2`` have the same wires and name attributes Raises: TypeError: if ``pauli_1`` or ``pauli_2`` are not :class:`~.Identity`, :class:`~.PauliX`, - :class:`~.PauliY`, :class:`~.PauliZ`, :class:`~.Tensor`, :class:`~.SProd`, or - :class:`~.Prod` instances + :class:`~.PauliY`, :class:`~.PauliZ`, :class:`~.SProd`, or :class:`~.Prod` instances **Example** @@ -173,8 +153,6 @@ def are_identical_pauli_words(pauli_1, pauli_2): >>> are_identical_pauli_words(qml.Z(0) @ qml.Z(1), qml.Z(0) @ qml.X(3)) False """ - if pauli_1.name == "Hamiltonian" or pauli_2.name == "Hamiltonian": - return False if not (is_pauli_word(pauli_1) and is_pauli_word(pauli_2)): raise TypeError(f"Expected Pauli word observables, instead got {pauli_1} and {pauli_2}.") @@ -192,7 +170,7 @@ def pauli_to_binary(pauli_word, n_qubits=None, wire_map=None, check_is_pauli_wor PauliX placements while the last half specify PauliZ placements. Args: - pauli_word (Union[Identity, PauliX, PauliY, PauliZ, Tensor, Prod, SProd]): the Pauli word to be + pauli_word (Union[Identity, PauliX, PauliY, PauliZ, Prod, SProd]): the Pauli word to be converted to binary vector representation n_qubits (int): number of qubits to specify dimension of binary vector representation wire_map (dict): dictionary containing all wire labels used in the Pauli word as keys, and @@ -308,11 +286,9 @@ def binary_to_pauli(binary_vector, wire_map=None): # pylint: disable=too-many-b unique integer labels as their values Returns: - Union[Tensor, Prod]: The Pauli word corresponding to the input binary vector. + Union[Prod]: The Pauli word corresponding to the input binary vector. Note that if a zero vector is input, then the resulting Pauli word will be - an :class:`~.Identity` instance. If new operator arithmetic is enabled via - :func:`~.pennylane.operation.enable_new_opmath`, a :class:`~.Prod` will be - returned, else a :class:`~.Tensor` will be returned. + an :class:`~.Identity` instance. Raises: TypeError: if length of binary vector is not even, or if vector does not have strictly @@ -324,13 +300,13 @@ def binary_to_pauli(binary_vector, wire_map=None): # pylint: disable=too-many-b components, i.e., the ``i`` and ``N+i`` components specify the Pauli operation on wire ``i``, >>> binary_to_pauli([0,1,1,0,1,0]) - Tensor(Y(1), X(2)) + Y(1) @ X(2) An arbitrary labelling can be assigned by using ``wire_map``: >>> wire_map = {'a': 0, 'b': 1, 'c': 2} >>> binary_to_pauli([0,1,1,0,1,0], wire_map=wire_map) - Tensor(Y('b'), X('c')) + Y('b') @ X('c') Note that the values of ``wire_map``, if specified, must be ``0,1,..., N``, where ``N`` is the dimension of the vector divided by two, i.e., @@ -393,13 +369,11 @@ def pauli_word_to_string(pauli_word, wire_map=None): * A single pauli operator (see :class:`~.PauliX` for an example). - * A :class:`.Tensor` instance containing Pauli operators. - * A :class:`.Prod` instance containing Pauli operators. * A :class:`.SProd` instance containing a Pauli operator. - * A :class:`.Hamiltonian` instance with only one term. + * A :class:`.Sum` instance with only one term. Given a Pauli in observable form, convert it into string of characters from ``['I', 'X', 'Y', 'Z']``. This representation is required for @@ -421,8 +395,8 @@ def pauli_word_to_string(pauli_word, wire_map=None): 'X' Args: - pauli_word (Observable): an observable, either a :class:`~.Tensor` instance or - single-qubit observable representing a Pauli group element. + pauli_word (Union[Observable, Prod, SProd, Sum]): an observable, either a single-qubit observable + representing a Pauli group element, or a tensor product of single-qubit observables. wire_map (dict[Union[str, int], int]): dictionary containing all wire labels used in the Pauli word as keys, and unique integer labels as their values @@ -443,9 +417,6 @@ def pauli_word_to_string(pauli_word, wire_map=None): if not is_pauli_word(pauli_word): raise TypeError(f"Expected Pauli word observables, instead got {pauli_word}") - if isinstance(pauli_word, qml.ops.Hamiltonian): - # hamiltonian contains only one term - return _pauli_word_to_string_legacy(pauli_word, wire_map) pr = next(iter(pauli_word.pauli_rep.keys())) @@ -464,36 +435,6 @@ def pauli_word_to_string(pauli_word, wire_map=None): return "".join(pauli_string) -def _pauli_word_to_string_legacy(pauli_word, wire_map): - """Turn a legacy Hamiltonian operator to strings""" - # TODO: Give Hamiltonian a pauli rep to make this branch obsolete - pauli_word = pauli_word.ops[0] - - # If there is no wire map, we must infer from the structure of Paulis - if wire_map is None: - wire_map = {pauli_word.wires.labels[i]: i for i in range(len(pauli_word.wires))} - - character_map = {"Identity": "I", "PauliX": "X", "PauliY": "Y", "PauliZ": "Z"} - - n_qubits = len(wire_map) - - # Set default value of all characters to identity - pauli_string = ["I"] * n_qubits - - # Special case is when there is a single Pauli term - if not isinstance(pauli_word.name, list): - if pauli_word.name != "Identity": - wire_idx = wire_map[pauli_word.wires[0]] - pauli_string[wire_idx] = character_map[pauli_word.name] - return "".join(pauli_string) - - for name, wire_label in zip(pauli_word.name, pauli_word.wires): - wire_idx = wire_map[wire_label] - pauli_string[wire_idx] = character_map[name] - - return "".join(pauli_string) - - def string_to_pauli_word(pauli_string, wire_map=None): """Convert a string in terms of ``'I'``, ``'X'``, ``'Y'``, and ``'Z'`` into a Pauli word for the given wire map. @@ -561,14 +502,24 @@ def string_to_pauli_word(pauli_string, wire_map=None): def pauli_word_to_matrix(pauli_word, wire_map=None): """Convert a Pauli word from a tensor to its matrix representation. + A Pauli word can be either: + + * A single pauli operator (see :class:`~.PauliX` for an example). + + * A :class:`.Prod` instance containing Pauli operators. + + * A :class:`.SProd` instance containing a Pauli operator. + + * A :class:`.Sum` instance with only one term. + The matrix representation of a Pauli word has dimension :math:`2^n \\times 2^n`, where :math:`n` is the number of qubits provided in ``wire_map``. For wires that the Pauli word does not act on, identities must be inserted into the tensor product at the correct positions. Args: - pauli_word (Observable): an observable, either a :class:`~.Tensor`, :class:`~.Prod` or - single-qubit observable representing a Pauli group element. + pauli_word (Union[Observable, Prod, SProd, Sum]): an observable, either a single-qubit observable + representing a Pauli group element, or a tensor product of single-qubit observables. wire_map (dict[Union[str, int], int]): dictionary containing all wire labels used in the Pauli word as keys, and unique integer labels as their values @@ -741,7 +692,7 @@ def observables_to_binary_matrix(observables, n_qubits=None, wire_map=None): being acted on non-trivially by the Pauli words in observables. Args: - observables (list[Union[Identity, PauliX, PauliY, PauliZ, Tensor, Prod, SProd]]): the list + observables (list[Union[Identity, PauliX, PauliY, PauliZ, Prod, SProd]]): the list of Pauli words n_qubits (int): number of qubits to specify dimension of binary vector representation wire_map (dict): dictionary containing all wire labels used in the Pauli words as keys, and @@ -1070,7 +1021,7 @@ def diagonalize_pauli_word(pauli_word): Raises: TypeError: if the input is not a Pauli word, i.e., a Pauli operator, - :class:`~.Identity`, or :class:`~.Tensor` instances thereof + :class:`~.Identity`, or tensor products thereof **Example** @@ -1088,9 +1039,6 @@ def diagonalize_pauli_word(pauli_word): if not components: return qml.Identity(wires=pauli_word.wires) - if isinstance(pauli_word, Tensor): - return components[0] if len(components) == 1 else Tensor(*components) - prod = qml.prod(*components) coeff = pauli_word.pauli_rep[pw] return prod if qml.math.allclose(coeff, 1) else coeff * prod @@ -1133,7 +1081,7 @@ def diagonalize_qwc_pauli_words( new_ops = [] for term in qwc_grouping: pauli_rep = term.pauli_rep - if pauli_rep is None or len(pauli_rep) > 1 or term.name == "Hamiltonian": + if pauli_rep is None or len(pauli_rep) > 1: raise ValueError("This function only supports pauli words.") pw = next(iter(pauli_rep)) for wire, pauli_type in pw.items(): @@ -1205,64 +1153,6 @@ def diagonalize_qwc_groupings(qwc_groupings): return post_rotations, diag_groupings -# from observable_hf.py ------------------------- -def simplify(h, cutoff=1.0e-12): - r"""Add together identical terms in the Hamiltonian. - - The Hamiltonian terms with identical Pauli words are added together and eliminated if the - overall coefficient is smaller than a cutoff value. - - .. warning:: - - :func:`~pennylane.pauli.simplify` is deprecated. Instead, please use :func:`pennylane.simplify` - or :meth:`~pennylane.operation.Operator.simplify`. - - Args: - h (Hamiltonian): PennyLane Hamiltonian - cutoff (float): cutoff value for discarding the negligible terms - - Returns: - Hamiltonian: Simplified PennyLane Hamiltonian - - **Example** - - >>> c = np.array([0.5, 0.5]) - >>> h = qml.Hamiltonian(c, [qml.X(0) @ qml.Y(1), qml.X(0) @ qml.Y(1)]) - >>> print(simplify(h)) - (1.0) [X0 Y1] - """ - warn( - "qml.pauli.simplify() has been deprecated. Instead, please use " - "qml.simplify(op) or op.simplify().", - qml.PennyLaneDeprecationWarning, - ) - wiremap = dict(zip(h.wires, range(len(h.wires) + 1))) - - c, o = [], [] - for i, op in enumerate(h.ops): - op = qml.operation.Tensor(op).prune() - op = qml.pauli.pauli_word_to_string(op, wire_map=wiremap) - if op not in o: - c.append(h.coeffs[i]) - o.append(op) - else: - c[o.index(op)] += h.coeffs[i] - - coeffs, ops = [], [] - c = qml.math.convert_like(c, c[0]) - nonzero_ind = qml.math.argwhere(abs(c) > cutoff).flatten() - for i in nonzero_ind: - coeffs.append(c[i]) - ops.append(qml.pauli.string_to_pauli_word(o[i], wire_map=wiremap)) - - try: - coeffs = qml.math.stack(coeffs) - except ValueError: - pass - - return qml.Hamiltonian(qml.math.array(coeffs), ops) - - pauli_mult_dict = { "XX": "I", "YY": "I", diff --git a/pennylane/pulse/hardware_hamiltonian.py b/pennylane/pulse/hardware_hamiltonian.py index 8139d884a8b..154e8da54d6 100644 --- a/pennylane/pulse/hardware_hamiltonian.py +++ b/pennylane/pulse/hardware_hamiltonian.py @@ -355,9 +355,7 @@ def __add__(self, other): # pylint: disable=too-many-return-statements settings = self.settings pulses = self.pulses - if isinstance( - other, (qml.ops.Hamiltonian, qml.ops.LinearCombination, ParametrizedHamiltonian) - ): + if isinstance(other, (qml.ops.LinearCombination, ParametrizedHamiltonian)): new_coeffs = coeffs + list(other.coeffs.copy()) new_ops = ops + other.ops.copy() return HardwareHamiltonian( diff --git a/pennylane/pulse/parametrized_evolution.py b/pennylane/pulse/parametrized_evolution.py index fc335b65a80..240280c4304 100644 --- a/pennylane/pulse/parametrized_evolution.py +++ b/pennylane/pulse/parametrized_evolution.py @@ -381,7 +381,7 @@ def __init__( id=None, **odeint_kwargs, ): - if not all(op.has_matrix or isinstance(op, qml.ops.Hamiltonian) for op in H.ops): + if not all(op.has_matrix for op in H.ops): raise ValueError( "All operators inside the parametrized hamiltonian must have a matrix defined." ) diff --git a/pennylane/pulse/parametrized_hamiltonian.py b/pennylane/pulse/parametrized_hamiltonian.py index 88e31b567ad..a53bc9748a0 100644 --- a/pennylane/pulse/parametrized_hamiltonian.py +++ b/pennylane/pulse/parametrized_hamiltonian.py @@ -342,7 +342,7 @@ def __add__(self, H): ops = self.ops.copy() coeffs = self.coeffs.copy() - if isinstance(H, (qml.ops.Hamiltonian, qml.ops.LinearCombination, ParametrizedHamiltonian)): + if isinstance(H, (qml.ops.LinearCombination, ParametrizedHamiltonian)): # if Hamiltonian, coeffs array must be converted to list new_coeffs = coeffs + list(H.coeffs.copy()) new_ops = ops + H.ops.copy() @@ -367,7 +367,7 @@ def __radd__(self, H): ops = self.ops.copy() coeffs = self.coeffs.copy() - if isinstance(H, (qml.ops.Hamiltonian, qml.ops.LinearCombination, ParametrizedHamiltonian)): + if isinstance(H, (qml.ops.LinearCombination, ParametrizedHamiltonian)): # if Hamiltonian, coeffs array must be converted to list new_coeffs = list(H.coeffs.copy()) + coeffs new_ops = H.ops.copy() + ops diff --git a/pennylane/qaoa/layers.py b/pennylane/qaoa/layers.py index dde6a39a998..3df3b4b43ab 100644 --- a/pennylane/qaoa/layers.py +++ b/pennylane/qaoa/layers.py @@ -15,7 +15,6 @@ Methods that define cost and mixer layers for use in QAOA workflows. """ import pennylane as qml -from pennylane.operation import Tensor def _diagonal_terms(hamiltonian): @@ -30,9 +29,7 @@ def _diagonal_terms(hamiltonian): """ for op in hamiltonian.terms()[1]: - if isinstance(op, Tensor): - obs = op.obs - elif isinstance(op, qml.ops.Prod): + if isinstance(op, qml.ops.Prod): obs = op.operands else: obs = [op] diff --git a/pennylane/qaoa/mixers.py b/pennylane/qaoa/mixers.py index 76dffccaf29..fbe8d5f7484 100644 --- a/pennylane/qaoa/mixers.py +++ b/pennylane/qaoa/mixers.py @@ -231,11 +231,7 @@ def bit_flip_mixer(graph: Union[nx.Graph, rx.PyGraph], b: int): ] n_coeffs = [[1, sign] for _ in neighbours] - final_terms = ( - [qml.prod(*list(m)).simplify() for m in itertools.product(*n_terms)] - if qml.operation.active_new_opmath() - else [qml.operation.Tensor(*list(m)).prune() for m in itertools.product(*n_terms)] - ) + final_terms = [qml.prod(*list(m)).simplify() for m in itertools.product(*n_terms)] final_coeffs = [ (0.5**degree) * functools.reduce(lambda x, y: x * y, list(m), 1) diff --git a/pennylane/qchem/convert.py b/pennylane/qchem/convert.py index dbba83fe2be..9273f151caf 100644 --- a/pennylane/qchem/convert.py +++ b/pennylane/qchem/convert.py @@ -21,8 +21,6 @@ # pylint: disable= import-outside-toplevel,no-member,too-many-function-args import pennylane as qml -from pennylane.operation import Tensor, active_new_opmath -from pennylane.pauli import pauli_sentence from pennylane.wires import Wires @@ -146,9 +144,6 @@ def _openfermion_to_pennylane(qubit_operator, wires=None, tol=1.0e-16): 0.2 [Y0 Z2] >>> _openfermion_to_pennylane(q_op, wires=['w0','w1','w2','extra_wire']) (tensor([0.1, 0.2], requires_grad=False), [X('w0'), Y('w0') @ Z('w2')]) - - If the new op-math is active, the list of operators will be cast as :class:`~.Prod` instances instead of - :class:`~.Tensor` instances when appropriate. """ n_wires = ( 1 + max(max(i for i, _ in t) if t else 1 for t in qubit_operator.terms) @@ -165,10 +160,7 @@ def _openfermion_to_pennylane(qubit_operator, wires=None, tol=1.0e-16): def _get_op(term, wires): """A function to compute the PL operator associated with the term string.""" if len(term) > 1: - if active_new_opmath(): - return qml.prod(*[xyz2pauli[op[1]](wires=wires[op[0]]) for op in term]) - - return Tensor(*[xyz2pauli[op[1]](wires=wires[op[0]]) for op in term]) + return qml.prod(*[xyz2pauli[op[1]](wires=wires[op[0]]) for op in term]) if len(term) == 1: return xyz2pauli[term[0][1]](wires=wires[term[0][0]]) @@ -228,8 +220,8 @@ def _pennylane_to_openfermion(coeffs, ops, wires=None, tol=1.0e-16): >>> coeffs = np.array([0.1, 0.2, 0.3, 0.4]) >>> ops = [ - ... qml.operation.Tensor(qml.X('w0')), - ... qml.operation.Tensor(qml.Y('w0'), qml.Z('w2')), + ... qml.prod(qml.X('w0')), + ... qml.prod(qml.Y('w0'), qml.Z('w2')), ... qml.sum(qml.Z('w0'), qml.s_prod(-0.5, qml.X('w0'))), ... qml.prod(qml.X('w0'), qml.Z('w1')), ... ] @@ -264,16 +256,8 @@ def _pennylane_to_openfermion(coeffs, ops, wires=None, tol=1.0e-16): q_op = openfermion.QubitOperator() for coeff, op in zip(coeffs, ops): - if isinstance(op, Tensor): - try: - ps = pauli_sentence(op) - except ValueError as e: - raise ValueError( - f"Expected a Pennylane operator with a valid Pauli word representation, " - f"but got {op}." - ) from e - - elif (ps := op.pauli_rep) is None: + + if (ps := op.pauli_rep) is None: raise ValueError( f"Expected a Pennylane operator with a valid Pauli word representation, but got {op}." ) @@ -342,20 +326,9 @@ def import_operator(qubit_observable, format="openfermion", wires=None, tol=1e01 **Example** - >>> assert qml.operation.active_new_opmath() == True >>> h_pl = import_operator(h_of, format='openfermion') >>> print(h_pl) (-0.0548 * X(0 @ X(1) @ Y(2) @ Y(3))) + (0.14297 * Z(0 @ Z(1))) - - If the new op-math is deactivated, a :class:`~Hamiltonian` is returned instead. - - >>> assert qml.operation.active_new_opmath() == False - >>> from openfermion import QubitOperator - >>> h_of = QubitOperator('X0 X1 Y2 Y3', -0.0548) + QubitOperator('Z0 Z1', 0.14297) - >>> h_pl = import_operator(h_of, format='openfermion') - >>> print(h_pl) - (0.14297) [Z0 Z1] - + (-0.0548) [X0 X1 Y2 Y3] """ if format not in ["openfermion"]: raise TypeError(f"Converter does not exist for {format} format.") @@ -369,10 +342,7 @@ def import_operator(qubit_observable, format="openfermion", wires=None, tol=1e01 f" {list(coeffs[np.iscomplex(coeffs)])}" ) - if active_new_opmath(): - return qml.dot(*_openfermion_to_pennylane(qubit_observable, wires=wires)) - - return qml.Hamiltonian(*_openfermion_to_pennylane(qubit_observable, wires=wires)) + return qml.dot(*_openfermion_to_pennylane(qubit_observable, wires=wires)) def _excitations(electrons, orbitals): diff --git a/pennylane/qchem/hamiltonian.py b/pennylane/qchem/hamiltonian.py index 68cd5fd286d..b39eb24d8be 100644 --- a/pennylane/qchem/hamiltonian.py +++ b/pennylane/qchem/hamiltonian.py @@ -19,8 +19,6 @@ # pylint: disable= too-many-branches, too-many-arguments, too-many-locals, too-many-nested-blocks # pylint: disable=consider-using-generator, protected-access import pennylane as qml -from pennylane.operation import active_new_opmath -from pennylane.pauli.utils import simplify from .basis_data import atomic_numbers from .hartree_fock import nuclear_energy, scf @@ -301,7 +299,7 @@ def molecular_hamiltonian(*args, **kwargs): Returns: - tuple[pennylane.Hamiltonian, int]: the fermionic-to-qubit transformed Hamiltonian + tuple[pennylane.Operator, int]: the fermionic-to-qubit transformed Hamiltonian and the number of qubits .. note:: @@ -556,19 +554,11 @@ def _molecular_hamiltonian( else qml.qchem.diff_hamiltonian(mol, core=core, active=active, mapping=mapping)() ) - if active_new_opmath(): - h_as_ps = qml.pauli.pauli_sentence(h) - coeffs = qml.numpy.real(list(h_as_ps.values()), requires_grad=requires_grad) + h_as_ps = qml.pauli.pauli_sentence(h) + coeffs = qml.numpy.real(list(h_as_ps.values()), requires_grad=requires_grad) - h_as_ps = qml.pauli.PauliSentence(dict(zip(h_as_ps.keys(), coeffs))) - h = ( - qml.s_prod(0, qml.Identity(h.wires[0])) - if len(h_as_ps) == 0 - else h_as_ps.operation() - ) - else: - coeffs = qml.numpy.real(h.coeffs, requires_grad=requires_grad) - h = qml.Hamiltonian(coeffs, h.ops) + h_as_ps = qml.pauli.PauliSentence(dict(zip(h_as_ps.keys(), coeffs))) + h = qml.s_prod(0, qml.Identity(h.wires[0])) if len(h_as_ps) == 0 else h_as_ps.operation() if wires: h = qml.map_wires(h, wires_map) @@ -583,24 +573,14 @@ def _molecular_hamiltonian( mapping = mapping.strip().lower() qubits = len(hf.wires) - if active_new_opmath(): - if mapping == "jordan_wigner": - h_pl = qml.jordan_wigner(hf, wire_map=wires_map, tol=1.0e-10) - elif mapping == "parity": - h_pl = qml.parity_transform(hf, qubits, wire_map=wires_map, tol=1.0e-10) - elif mapping == "bravyi_kitaev": - h_pl = qml.bravyi_kitaev(hf, qubits, wire_map=wires_map, tol=1.0e-10) - - h_pl.simplify() - else: - if mapping == "jordan_wigner": - h_pl = qml.jordan_wigner(hf, ps=True, wire_map=wires_map, tol=1.0e-10) - elif mapping == "parity": - h_pl = qml.parity_transform(hf, qubits, ps=True, wire_map=wires_map, tol=1.0e-10) - elif mapping == "bravyi_kitaev": - h_pl = qml.bravyi_kitaev(hf, qubits, ps=True, wire_map=wires_map, tol=1.0e-10) - - h_pl = simplify(h_pl.hamiltonian()) + if mapping == "jordan_wigner": + h_pl = qml.jordan_wigner(hf, wire_map=wires_map, tol=1.0e-10) + elif mapping == "parity": + h_pl = qml.parity_transform(hf, qubits, wire_map=wires_map, tol=1.0e-10) + elif mapping == "bravyi_kitaev": + h_pl = qml.bravyi_kitaev(hf, qubits, wire_map=wires_map, tol=1.0e-10) + + h_pl = h_pl.simplify() return h_pl, len(h_pl.wires) diff --git a/pennylane/qchem/observable_hf.py b/pennylane/qchem/observable_hf.py index 276bfac9104..4ef1fe1f9a3 100644 --- a/pennylane/qchem/observable_hf.py +++ b/pennylane/qchem/observable_hf.py @@ -19,9 +19,7 @@ import pennylane as qml from pennylane.fermi import FermiSentence, FermiWord -from pennylane.operation import active_new_opmath from pennylane.pauli import PauliSentence -from pennylane.pauli.utils import simplify def fermionic_observable(constant, one=None, two=None, cutoff=1.0e-12): @@ -114,25 +112,6 @@ def qubit_observable(o_ferm, cutoff=1.0e-12, mapping="jordan_wigner"): >>> s = qml.fermi.FermiSentence({w1 : 1.2, w2: 3.1}) >>> print(qubit_observable(s)) -0.775j * (Y(0) @ X(1)) + 0.775 * (Y(0) @ Y(1)) + 0.775 * (X(0) @ X(1)) + 0.775j * (X(0) @ Y(1)) - - If the new op-math is deactivated, a legacy :class:`~pennylane.ops.Hamiltonian` instance is returned. - - >>> qml.operation.disable_new_opmath() - UserWarning: Disabling the new Operator arithmetic system for legacy support. - If you need help troubleshooting your code, please visit - https://docs.pennylane.ai/en/stable/news/new_opmath.html - >>> w1 = qml.fermi.FermiWord({(0, 0) : '+', (1, 1) : '-'}) - >>> w2 = qml.fermi.FermiWord({(0, 1) : '+', (1, 2) : '-'}) - >>> s = qml.fermi.FermiSentence({w1 : 1.2, w2: 3.1}) - >>> print(qubit_observable(s)) - (-0.3j) [Y0 X1] - + (0.3j) [X0 Y1] - + (-0.775j) [Y1 X2] - + (0.775j) [X1 Y2] - + ((0.3+0j)) [Y0 Y1] - + ((0.3+0j)) [X0 X1] - + ((0.775+0j)) [Y1 Y2] - + ((0.775+0j)) [X1 X2] """ if mapping == "jordan_wigner": h = qml.jordan_wigner(o_ferm, ps=True, tol=cutoff) @@ -150,19 +129,6 @@ def qubit_observable(o_ferm, cutoff=1.0e-12, mapping="jordan_wigner"): h.simplify(tol=cutoff) - if active_new_opmath(): - if not h.wires: - return h.operation(wire_order=[0]) - return h.operation() - if not h.wires: - h = h.hamiltonian(wire_order=[0]) - return qml.Hamiltonian( - h.coeffs, [qml.Identity(0) if o.name == "Identity" else o for o in h.ops] - ) - - h = h.hamiltonian() - - return simplify( - qml.Hamiltonian(h.coeffs, [qml.Identity(0) if o.name == "Identity" else o for o in h.ops]) - ) + return h.operation(wire_order=[0]) + return h.operation() diff --git a/pennylane/qchem/tapering.py b/pennylane/qchem/tapering.py index 4c7c9b8fd7c..3029de2b555 100644 --- a/pennylane/qchem/tapering.py +++ b/pennylane/qchem/tapering.py @@ -23,8 +23,7 @@ import scipy import pennylane as qml -from pennylane.operation import active_new_opmath, convert_to_opmath -from pennylane.pauli import PauliSentence, PauliWord, pauli_sentence, simplify +from pennylane.pauli import PauliSentence, PauliWord, pauli_sentence from pennylane.pauli.utils import _binary_matrix_from_pws from pennylane.wires import Wires @@ -170,7 +169,7 @@ def symmetry_generators(h): tau[idx] = pauli_map[f"{x}{z}"] ham = qml.pauli.PauliSentence({qml.pauli.PauliWord(tau): 1.0}) - ham = ham.operation(h.wires) if active_new_opmath() else ham.hamiltonian(h.wires) + ham = ham.operation(h.wires) generators.append(ham) return generators @@ -256,7 +255,7 @@ def clifford(generators, paulixops): u = functools.reduce(lambda p, q: p @ q, cliff) - return u.operation() if active_new_opmath() else u.hamiltonian() + return u.operation() def _split_pauli_sentence(pl_sentence, max_size=15000): @@ -320,9 +319,7 @@ def _taper_pauli_sentence(ps_h, generators, paulixops, paulix_sector): c = qml.math.stack(qml.math.multiply(val * complex(1.0), list(ts_ps.values()))) - tapered_ham = ( - qml.simplify(qml.dot(c, o)) if active_new_opmath() else simplify(qml.Hamiltonian(c, o)) - ) + tapered_ham = qml.simplify(qml.dot(c, o)) # If simplified Hamiltonian is missing wires, then add wires manually for consistency if set(wires_tap) != tapered_ham.wires.toset(): identity_op = functools.reduce( @@ -333,12 +330,8 @@ def _taper_pauli_sentence(ps_h, generators, paulixops, paulix_sector): ], ) - if active_new_opmath(): - return tapered_ham + (0.0 * identity_op) + return tapered_ham + (0.0 * identity_op) - tapered_ham = qml.Hamiltonian( - np.array([*tapered_ham.coeffs, 0.0]), [*tapered_ham.ops, identity_op] - ) return tapered_ham @@ -584,21 +577,16 @@ def _build_generator(operation, wire_order, op_gen=None): op_gen.pop(PauliWord({}), 0.0) else: # Single-parameter gates try: - # TODO: simplify when qml.generator has a proper support for "arithmetic". - op_gen = ( - operation.generator() - if active_new_opmath() - else qml.generator(operation, "arithmetic") - ).pauli_rep + op_gen = operation.generator().pauli_rep except (ValueError, qml.operation.GeneratorUndefinedError) as exc: raise NotImplementedError( f"Generator for {operation} is not implemented, please provide it with 'op_gen' args." ) from exc else: # check that user-provided generator is correct - if not isinstance( - op_gen, (qml.ops.Hamiltonian, qml.ops.LinearCombination, PauliSentence) - ) and not isinstance(getattr(op_gen, "pauli_rep", None), PauliSentence): + if not isinstance(op_gen, (qml.ops.LinearCombination, PauliSentence)) and not isinstance( + getattr(op_gen, "pauli_rep", None), PauliSentence + ): raise ValueError( f"Generator for the operation needs to be a valid operator, but got {type(op_gen)}." ) @@ -618,7 +606,7 @@ def _build_generator(operation, wire_order, op_gen=None): raise ValueError( f"Given op_gen: {op_gen} doesn't seem to be the correct generator for the {operation}." ) - op_gen = convert_to_opmath(op_gen).pauli_rep + op_gen = op_gen.pauli_rep return op_gen @@ -771,7 +759,7 @@ def _is_commuting(ps1, ps2): # Obtain the tapered generator for the operation with qml.QueuingManager.stop_recording(): # Get pauli rep for symmetery generators - ps_gen = list(map(lambda x: convert_to_opmath(x).pauli_rep, generators)) + ps_gen = list(map(lambda x: x.pauli_rep, generators)) gen_tapered = PauliSentence({}) if all(_is_commuting(sym, op_gen) for sym in ps_gen) and not qml.math.allclose( diff --git a/pennylane/qcut/cutcircuit.py b/pennylane/qcut/cutcircuit.py index 90f585e4bdb..2cf3d34ba1f 100644 --- a/pennylane/qcut/cutcircuit.py +++ b/pennylane/qcut/cutcircuit.py @@ -52,9 +52,7 @@ def processing_fn(res): # Expand the tapes for handling Hamiltonian with two or more terms tape_meas_ops = tape.measurements - if tape_meas_ops and isinstance( - tape_meas_ops[0].obs, (qml.ops.Hamiltonian, qml.ops.LinearCombination) - ): + if tape_meas_ops and isinstance(tape_meas_ops[0].obs, qml.ops.Sum): if len(tape_meas_ops) > 1: raise NotImplementedError( "Hamiltonian expansion is supported only with a single Hamiltonian" diff --git a/pennylane/qcut/tapes.py b/pennylane/qcut/tapes.py index 1bea5fd5aa6..bf856c70b45 100644 --- a/pennylane/qcut/tapes.py +++ b/pennylane/qcut/tapes.py @@ -25,7 +25,7 @@ import pennylane as qml from pennylane import expval from pennylane.measurements import ExpectationMP, MeasurementProcess, SampleMP -from pennylane.operation import Operator, Tensor +from pennylane.operation import Operator from pennylane.ops.meta import WireCut from pennylane.pauli import string_to_pauli_word from pennylane.queuing import WrappedObj @@ -81,7 +81,7 @@ def tape_to_graph(tape: QuantumScript) -> MultiDiGraph: order += 1 # pylint: disable=undefined-loop-variable for m in tape.measurements: obs = getattr(m, "obs", None) - if obs is not None and isinstance(obs, (Tensor, qml.ops.Prod)): + if obs is not None and isinstance(obs, qml.ops.Prod): if isinstance(m, SampleMP): raise ValueError( "Sampling from tensor products of observables " @@ -204,8 +204,7 @@ def graph_to_tape(graph: MultiDiGraph) -> QuantumScript: if measurement_type is ExpectationMP: if len(observables) > 1: - prod_type = qml.prod if qml.operation.active_new_opmath() else Tensor - measurements_from_graph.append(qml.expval(prod_type(*observables))) + measurements_from_graph.append(qml.expval(qml.prod(*observables))) else: measurements_from_graph.append(qml.expval(obs)) diff --git a/pennylane/shadows/classical_shadow.py b/pennylane/shadows/classical_shadow.py index c3556841b1e..4daee81f6a7 100644 --- a/pennylane/shadows/classical_shadow.py +++ b/pennylane/shadows/classical_shadow.py @@ -266,18 +266,6 @@ def pauli_list_to_word(obs): word = pauli_list_to_word([observable]) return [(1, word)] - if isinstance(observable, qml.operation.Tensor): - word = pauli_list_to_word(observable.obs) - return [(1, word)] - - if isinstance(observable, qml.ops.Hamiltonian): - coeffs_and_words = [] - for coeff, op in zip(observable.data, observable.ops): - coeffs_and_words.extend( - [(coeff * c, w) for c, w in self._convert_to_pauli_words(op)] - ) - return coeffs_and_words - # Support for all operators with a valid pauli_rep if (pr := observable.pauli_rep) is not None: return self._convert_to_pauli_words_with_pauli_rep(pr, num_wires) diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py index 3c3e21caabb..6dff9d64479 100644 --- a/pennylane/tape/tape.py +++ b/pennylane/tape/tape.py @@ -78,9 +78,10 @@ def _validate_computational_basis_sampling(tape): with ( QueuingManager.stop_recording() ): # stop recording operations - the constructed operator is just aux - prod_op = qml.ops.Prod if qml.operation.active_new_opmath() else qml.operation.Tensor pauliz_for_cb_obs = ( - qml.Z(all_wires) if len(all_wires) == 1 else prod_op(*[qml.Z(w) for w in all_wires]) + qml.Z(all_wires) + if len(all_wires) == 1 + else qml.ops.Prod(*[qml.Z(w) for w in all_wires]) ) for obs in non_comp_basis_sampling_obs: diff --git a/pennylane/templates/subroutines/qdrift.py b/pennylane/templates/subroutines/qdrift.py index a65152586e0..cf5132c2440 100644 --- a/pennylane/templates/subroutines/qdrift.py +++ b/pennylane/templates/subroutines/qdrift.py @@ -17,12 +17,12 @@ import pennylane as qml from pennylane.math import requires_grad, unwrap from pennylane.operation import Operation -from pennylane.ops import Hamiltonian, LinearCombination, Sum +from pennylane.ops import LinearCombination, Sum from pennylane.wires import Wires def _check_hamiltonian_type(hamiltonian): - if not isinstance(hamiltonian, (Hamiltonian, LinearCombination, Sum)): + if not isinstance(hamiltonian, Sum): raise TypeError( f"The given operator must be a PennyLane ~.Hamiltonian or ~.Sum, got {hamiltonian}" ) @@ -30,9 +30,9 @@ def _check_hamiltonian_type(hamiltonian): def _extract_hamiltonian_coeffs_and_ops(hamiltonian): """Extract the coefficients and operators from a Hamiltonian that is - a ``Hamiltonian``, a ``LinearCombination`` or a ``Sum``.""" + a ``LinearCombination`` or a ``Sum``.""" # Note that potentially_trainable_coeffs does *not* contain all coeffs - if isinstance(hamiltonian, (Hamiltonian, LinearCombination)): + if isinstance(hamiltonian, LinearCombination): coeffs, ops = hamiltonian.terms() elif isinstance(hamiltonian, Sum): @@ -279,7 +279,7 @@ def error(hamiltonian, time, n=1): terms to be added to the product. For more details see `Phys. Rev. Lett. 123, 070503 (2019) `_. Args: - hamiltonian (Union[.Hamiltonian, .Sum]): The Hamiltonian written as a sum of operations + hamiltonian (Sum): The Hamiltonian written as a sum of operations time (float): The time of evolution, namely the parameter :math:`t` in :math:`e^{-iHt}` n (int): An integer representing the number of exponentiated terms. default is 1 diff --git a/pennylane/templates/subroutines/trotter.py b/pennylane/templates/subroutines/trotter.py index 87dfac458d6..2429d43c8bd 100644 --- a/pennylane/templates/subroutines/trotter.py +++ b/pennylane/templates/subroutines/trotter.py @@ -201,7 +201,7 @@ def __init__( # pylint: disable=too-many-arguments f"The order of a TrotterProduct must be 1 or a positive even integer, got {order}." ) - if isinstance(hamiltonian, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): + if isinstance(hamiltonian, qml.ops.LinearCombination): coeffs, ops = hamiltonian.terms() if len(coeffs) < 2: raise ValueError( diff --git a/pennylane/transforms/diagonalize_measurements.py b/pennylane/transforms/diagonalize_measurements.py index c7b8ec4b666..ba424efb4da 100644 --- a/pennylane/transforms/diagonalize_measurements.py +++ b/pennylane/transforms/diagonalize_measurements.py @@ -17,7 +17,6 @@ from functools import singledispatch import pennylane as qml -from pennylane.operation import Tensor from pennylane.ops import CompositeOp, LinearCombination, SymbolicOp from pennylane.pauli import diagonalize_qwc_pauli_words from pennylane.tape.tape import ( @@ -294,28 +293,6 @@ def _change_symbolic_op(observable: SymbolicOp): return diagonalizing_gates, new_observable -@_change_obs_to_Z.register -def _change_tensor(observable: Tensor): - diagonalizing_gates, new_obs = diagonalize_qwc_pauli_words( - observable.obs, - ) - - new_observable = Tensor(*new_obs) - - return diagonalizing_gates, new_observable - - -@_change_obs_to_Z.register -def _change_hamiltonian(observable: qml.ops.Hamiltonian): - diagonalizing_gates, new_ops = diagonalize_qwc_pauli_words( - observable.ops, - ) - - new_observable = qml.ops.Hamiltonian(observable.coeffs, new_ops) - - return diagonalizing_gates, new_observable - - @_change_obs_to_Z.register def _change_linear_combination(observable: LinearCombination): coeffs, obs = observable.terms() @@ -471,34 +448,6 @@ def _diagonalize_symbolic_op( return diagonalizing_gates, new_observable, _visited_obs -@_diagonalize_compound_observable.register -def _diagonalize_tensor( - observable: Tensor, _visited_obs, supported_base_obs=_default_supported_obs -): - diagonalizing_gates, new_obs, _visited_obs = _get_obs_and_gates( - observable.obs, _visited_obs, supported_base_obs - ) - - new_observable = Tensor(*new_obs) - - return diagonalizing_gates, new_observable, _visited_obs - - -@_diagonalize_compound_observable.register -def _diagonalize_hamiltonian( - observable: qml.ops.Hamiltonian, - _visited_obs, - supported_base_obs=_default_supported_obs, -): - diagonalizing_gates, new_ops, _visited_obs = _get_obs_and_gates( - observable.ops, _visited_obs, supported_base_obs - ) - - new_observable = qml.ops.Hamiltonian(observable.coeffs, new_ops) - - return diagonalizing_gates, new_observable, _visited_obs - - @_diagonalize_compound_observable.register def _diagonalize_linear_combination( observable: LinearCombination, _visited_obs, supported_base_obs=_default_supported_obs diff --git a/pennylane/transforms/sign_expand/sign_expand.py b/pennylane/transforms/sign_expand/sign_expand.py index aaad22cb63e..fe5e3868a4f 100644 --- a/pennylane/transforms/sign_expand/sign_expand.py +++ b/pennylane/transforms/sign_expand/sign_expand.py @@ -311,7 +311,7 @@ def circuit(): wires = hamiltonian.wires if ( - not isinstance(hamiltonian, (qml.ops.Hamiltonian, qml.ops.LinearCombination)) + not isinstance(hamiltonian, qml.ops.LinearCombination) or len(tape.measurements) > 1 or tape.measurements[0].return_type not in [qml.measurements.Expectation, qml.measurements.Variance] diff --git a/pennylane/transforms/split_non_commuting.py b/pennylane/transforms/split_non_commuting.py index 3f2775bba18..c940e40cc9d 100644 --- a/pennylane/transforms/split_non_commuting.py +++ b/pennylane/transforms/split_non_commuting.py @@ -23,7 +23,7 @@ import pennylane as qml from pennylane.measurements import ExpectationMP, MeasurementProcess, Shots, StateMP -from pennylane.ops import Hamiltonian, LinearCombination, Prod, SProd, Sum +from pennylane.ops import LinearCombination, Prod, SProd, Sum from pennylane.tape import QuantumScript, QuantumScriptBatch from pennylane.transforms import transform from pennylane.typing import PostprocessingFn, Result, ResultBatch, TensorLike, Union @@ -259,12 +259,12 @@ def circuit(x): if len(tape.measurements) == 0: return [tape], null_postprocessing - # Special case for a single measurement of a Sum or Hamiltonian, in which case + # Special case for a single measurement of a Sum, in which case # the grouping information can be computed and cached in the observable. if ( len(tape.measurements) == 1 and isinstance(tape.measurements[0], ExpectationMP) - and isinstance(tape.measurements[0].obs, (Hamiltonian, Sum)) + and isinstance(tape.measurements[0].obs, Sum) and ( ( grouping_strategy in ("default", "qwc") @@ -292,7 +292,7 @@ def circuit(x): grouping_strategy == "wires" or grouping_strategy == "default" and any( - isinstance(m, ExpectationMP) and isinstance(m.obs, (LinearCombination, Hamiltonian)) + isinstance(m, ExpectationMP) and isinstance(m.obs, LinearCombination) for m in tape.measurements ) or any( @@ -306,7 +306,7 @@ def circuit(x): # compute, but inefficient quantum-wise. If this transform is to be added to a device's # `preprocess`, it will be performed for every circuit execution, which can get very # expensive if there is a large number of observables. The reasoning here is, large - # Hamiltonians typically come in the form of a `LinearCombination` or `Hamiltonian`, so + # Hamiltonians typically come in the form of a `LinearCombination`, so # if we see one of those, use wires grouping to be safe. Otherwise, use qwc grouping. return _split_using_wires_grouping(tape, single_term_obs_mps, offsets) @@ -314,7 +314,7 @@ def circuit(x): def _split_ham_with_grouping(tape: qml.tape.QuantumScript): - """Splits a tape measuring a single Hamiltonian or Sum and group commuting observables.""" + """Splits a tape measuring a single Sum and group commuting observables.""" obs = tape.measurements[0].obs if obs.grouping_indices is None: @@ -322,7 +322,7 @@ def _split_ham_with_grouping(tape: qml.tape.QuantumScript): coeffs, obs_list = obs.terms() - # The constant offset of the Hamiltonian, typically arising from Identity terms. + # The constant offset of the Sum, typically arising from Identity terms. offset = 0 # A dictionary for measurements of each unique single-term observable, mapped to the @@ -348,7 +348,7 @@ def _split_ham_with_grouping(tape: qml.tape.QuantumScript): else: new_mp = qml.expval(obs_list[obs_idx]) if new_mp in single_term_obs_mps: - # If the Hamiltonian contains duplicate observables, it can be reused, + # If the Sum contains duplicate observables, it can be reused, # and the coefficients for each duplicate should be combined. single_term_obs_mps[new_mp] = ( single_term_obs_mps[new_mp][0], @@ -542,7 +542,7 @@ def _split_all_multi_term_obs_mps(tape: qml.tape.QuantumScript): for mp_idx, mp in enumerate(tape.measurements): obs = mp.obs offset = 0 - if isinstance(mp, ExpectationMP) and isinstance(obs, (Hamiltonian, Sum, Prod, SProd)): + if isinstance(mp, ExpectationMP) and isinstance(obs, (Sum, Prod, SProd)): # Break the observable into terms, and construct an ExpectationMP with each term. for c, o in zip(*obs.terms()): # If the observable is an identity, track it with a constant offset @@ -561,7 +561,7 @@ def _split_all_multi_term_obs_mps(tape: qml.tape.QuantumScript): else: if isinstance(obs, SProd): obs = obs.simplify() - if isinstance(obs, (Hamiltonian, Sum)): + if isinstance(obs, Sum): raise RuntimeError( f"Cannot split up terms in sums for MeasurementProcess {type(mp)}" ) diff --git a/pennylane/transforms/transpile.py b/pennylane/transforms/transpile.py index 4a50a283793..edc50779004 100644 --- a/pennylane/transforms/transpile.py +++ b/pennylane/transforms/transpile.py @@ -7,8 +7,7 @@ import networkx as nx import pennylane as qml -from pennylane.operation import Tensor -from pennylane.ops import Hamiltonian, LinearCombination +from pennylane.ops import LinearCombination from pennylane.ops import __all__ as all_ops from pennylane.ops.qubit import SWAP from pennylane.queuing import QueuingManager @@ -143,12 +142,9 @@ def circuit(): f"Not all wires present in coupling map! wires: {wires}, coupling map: {coupling_graph.nodes}" ) - if any( - isinstance(m.obs, (Hamiltonian, LinearCombination, Tensor, qml.ops.Prod)) - for m in tape.measurements - ): + if any(isinstance(m.obs, (LinearCombination, qml.ops.Prod)) for m in tape.measurements): raise NotImplementedError( - "Measuring expectation values of tensor products, Prods, or Hamiltonians is not yet supported" + "Measuring expectation values of tensor products or Hamiltonians is not yet supported" ) if any(len(op.wires) > 2 for op in tape.operations): diff --git a/tests/capture/test_operators.py b/tests/capture/test_operators.py index ca2f98adab4..530c43a289f 100644 --- a/tests/capture/test_operators.py +++ b/tests/capture/test_operators.py @@ -53,7 +53,6 @@ def test_abstract_operator(): # arithmetic dunders integration tested -@pytest.mark.usefixtures("new_opmath_only") def test_operators_constructed_when_plxpr_enabled(): """Test that normal operators can still be constructed when plxpr is enabled.""" @@ -422,7 +421,6 @@ def qfunc(): assert isinstance(eqn.outvars[0].aval, AbstractOperator) - @pytest.mark.usefixtures("new_opmath_only") def test_mul(self): """Test that the scalar multiplication dunder works.""" diff --git a/tests/capture/test_templates.py b/tests/capture/test_templates.py index 3990b2821e4..784a6f248b4 100644 --- a/tests/capture/test_templates.py +++ b/tests/capture/test_templates.py @@ -623,7 +623,6 @@ def qfunc(probs): assert len(q) == 1 assert q.queue[0] == qml.QuantumMonteCarlo(probs, **kwargs) - @pytest.mark.usefixtures("new_opmath_only") def test_qubitization(self): """Test the primitive bind call of Qubitization.""" @@ -654,7 +653,6 @@ def qfunc(): assert len(q) == 1 qml.assert_equal(q.queue[0], qml.Qubitization(**kwargs)) - @pytest.mark.usefixtures("new_opmath_only") def test_qrom(self): """Test the primitive bind call of QROM.""" @@ -689,7 +687,6 @@ def qfunc(): assert len(q) == 1 qml.assert_equal(q.queue[0], qml.QROM(**kwargs)) - @pytest.mark.usefixtures("new_opmath_only") def test_phase_adder(self): """Test the primitive bind call of PhaseAdder.""" @@ -724,7 +721,6 @@ def qfunc(): assert len(q) == 1 qml.assert_equal(q.queue[0], qml.PhaseAdder(**kwargs)) - @pytest.mark.usefixtures("new_opmath_only") def test_adder(self): """Test the primitive bind call of Adder.""" @@ -759,7 +755,6 @@ def qfunc(): assert len(q) == 1 qml.assert_equal(q.queue[0], qml.Adder(**kwargs)) - @pytest.mark.usefixtures("new_opmath_only") def test_multiplier(self): """Test the primitive bind call of Multiplier.""" @@ -794,7 +789,6 @@ def qfunc(): assert len(q) == 1 qml.assert_equal(q.queue[0], qml.Multiplier(**kwargs)) - @pytest.mark.usefixtures("new_opmath_only") def test_out_multiplier(self): """Test the primitive bind call of OutMultiplier.""" @@ -830,7 +824,6 @@ def qfunc(): assert len(q) == 1 qml.assert_equal(q.queue[0], qml.OutMultiplier(**kwargs)) - @pytest.mark.usefixtures("new_opmath_only") def test_out_adder(self): """Test the primitive bind call of OutAdder.""" @@ -866,7 +859,6 @@ def qfunc(): assert len(q) == 1 qml.assert_equal(q.queue[0], qml.OutAdder(**kwargs)) - @pytest.mark.usefixtures("new_opmath_only") def test_mod_exp(self): """Test the primitive bind call of ModExp.""" diff --git a/tests/circuit_graph/test_circuit_graph_hash.py b/tests/circuit_graph/test_circuit_graph_hash.py index 6fb966c25ad..9622b81af97 100644 --- a/tests/circuit_graph/test_circuit_graph_hash.py +++ b/tests/circuit_graph/test_circuit_graph_hash.py @@ -19,7 +19,6 @@ import pennylane as qml from pennylane.circuit_graph import CircuitGraph -from pennylane.operation import Tensor from pennylane.wires import Wires @@ -53,7 +52,7 @@ def test_serialize_numeric_arguments(self, queue, observable_queue, expected_str observable1 = qml.PauliZ(wires=[0]) observable2 = qml.Hermitian(np.array([[1, 0], [0, -1]]), wires=[0]) - observable3 = Tensor(qml.PauliZ(0), qml.PauliZ(1)) + observable3 = qml.prod(qml.PauliZ(0), qml.PauliZ(1)) numeric_observable_queue = [ (returntype1, observable1, "|||ObservableReturnTypes.Expectation!PauliZ[0]"), @@ -65,7 +64,7 @@ def test_serialize_numeric_arguments(self, queue, observable_queue, expected_str ( returntype1, observable3, - "|||ObservableReturnTypes.Expectation!['PauliZ', 'PauliZ'][0, 1]", + "|||ObservableReturnTypes.Expectation!Prod[0, 1]", ), (returntype2, observable1, "|||ObservableReturnTypes.Variance!PauliZ[0]"), ( @@ -76,7 +75,7 @@ def test_serialize_numeric_arguments(self, queue, observable_queue, expected_str ( returntype2, observable3, - "|||ObservableReturnTypes.Variance!['PauliZ', 'PauliZ'][0, 1]", + "|||ObservableReturnTypes.Variance!Prod[0, 1]", ), ] diff --git a/tests/conftest.py b/tests/conftest.py index 82e1f843169..a01254df37b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,14 +18,12 @@ import os import pathlib import sys -from warnings import filterwarnings, warn import numpy as np import pytest import pennylane as qml from pennylane.devices import DefaultGaussian -from pennylane.operation import disable_new_opmath_cm, enable_new_opmath_cm sys.path.append(os.path.join(os.path.dirname(__file__), "helpers")) @@ -127,60 +125,6 @@ def tear_down_thermitian(): qml.THermitian._eigs = {} -####################################################################### -# Fixtures for testing under new and old opmath - - -def pytest_addoption(parser): - parser.addoption( - "--disable-opmath", action="store", default="False", help="Whether to disable new_opmath" - ) - - -# pylint: disable=eval-used -@pytest.fixture(scope="session", autouse=True) -def disable_opmath_if_requested(request): - disable_opmath = request.config.getoption("--disable-opmath") - # value from yaml file is a string, convert to boolean - if eval(disable_opmath): - warn( - "Disabling the new Operator arithmetic system for legacy support. " - "If you need help troubleshooting your code, please visit " - "https://docs.pennylane.ai/en/stable/news/new_opmath.html", - UserWarning, - ) - qml.operation.disable_new_opmath(warn=False) - - # Suppressing warnings so that Hamiltonians and Tensors constructed outside tests - # don't raise deprecation warnings - filterwarnings("ignore", "qml.ops.Hamiltonian", qml.PennyLaneDeprecationWarning) - filterwarnings("ignore", "qml.operation.Tensor", qml.PennyLaneDeprecationWarning) - filterwarnings("ignore", "qml.pauli.simplify", qml.PennyLaneDeprecationWarning) - filterwarnings("ignore", "PauliSentence.hamiltonian", qml.PennyLaneDeprecationWarning) - filterwarnings("ignore", "PauliWord.hamiltonian", qml.PennyLaneDeprecationWarning) - - -@pytest.fixture(params=[disable_new_opmath_cm, enable_new_opmath_cm], scope="function") -def use_legacy_and_new_opmath(request): - with request.param(warn=False) as cm: - yield cm - - -@pytest.fixture -def new_opmath_only(): - if not qml.operation.active_new_opmath(): - pytest.skip("This feature only works with new opmath enabled") - - -@pytest.fixture -def legacy_opmath_only(): - if qml.operation.active_new_opmath(): - pytest.skip("This test exclusively tests legacy opmath") - - -####################################################################### - - @pytest.fixture(autouse=True) def restore_global_seed(): original_state = np.random.get_state() diff --git a/tests/data/attributes/operator/test_operator.py b/tests/data/attributes/operator/test_operator.py index a489134916d..29b28363dc5 100644 --- a/tests/data/attributes/operator/test_operator.py +++ b/tests/data/attributes/operator/test_operator.py @@ -24,7 +24,7 @@ import pennylane as qml from pennylane.data.attributes import DatasetOperator, DatasetPyTree from pennylane.data.base.typing_util import get_type_str -from pennylane.operation import Operator, Tensor +from pennylane.operation import Operator pytestmark = pytest.mark.data @@ -71,21 +71,18 @@ ] ] -tensors = [Tensor(qml.PauliX(1), qml.PauliY(2))] +tensors = [qml.prod(qml.PauliX(1), qml.PauliY(2))] -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("attribute_cls", [DatasetOperator, DatasetPyTree]) @pytest.mark.parametrize("obs_in", [*hermitian_ops, *pauli_ops, *identity, *hamiltonians, *tensors]) class TestDatasetOperatorObservable: - """Tests serializing Observable operators using the ``compare()`` method.""" + """Tests serializing Observable operators using the ``qml.equal`` function.""" - def test_value_init(self, attribute_cls, obs_in, recwarn): + def test_value_init(self, attribute_cls, obs_in): """Test that a DatasetOperator can be value-initialized from an observable, and that the deserialized operator is equivalent.""" - if not qml.operation.active_new_opmath() and isinstance(obs_in, qml.ops.LinearCombination): - obs_in = qml.operation.convert_to_legacy_H(obs_in) dset_op = attribute_cls(obs_in) @@ -93,26 +90,11 @@ def test_value_init(self, attribute_cls, obs_in, recwarn): assert dset_op.info["py_type"] == get_type_str(type(obs_in)) obs_out = dset_op.get_value() - if ( - qml.operation.active_new_opmath() - and isinstance(obs_in, Tensor) - and attribute_cls is DatasetOperator - ): - assert isinstance(obs_out, qml.ops.Prod) - for o1, o2 in zip(obs_in.obs, obs_out.operands): - qml.assert_equal(o1, o2) - - # No Tensor deprecation warnings are raised - assert len(recwarn) == 0 - else: - qml.assert_equal(obs_out, obs_in) - assert obs_in.compare(obs_out) - - def test_bind_init(self, attribute_cls, obs_in, recwarn): + qml.assert_equal(obs_out, obs_in) + + def test_bind_init(self, attribute_cls, obs_in): """Test that DatasetOperator can be initialized from a HDF5 group that contains an operator attribute.""" - if not qml.operation.active_new_opmath() and isinstance(obs_in, qml.ops.LinearCombination): - obs_in = qml.operation.convert_to_legacy_H(obs_in) bind = attribute_cls(obs_in).bind @@ -122,20 +104,7 @@ def test_bind_init(self, attribute_cls, obs_in, recwarn): assert dset_op.info["py_type"] == get_type_str(type(obs_in)) obs_out = dset_op.get_value() - if ( - qml.operation.active_new_opmath() - and isinstance(obs_in, Tensor) - and attribute_cls is DatasetOperator - ): - assert isinstance(obs_out, qml.ops.Prod) - for o1, o2 in zip(obs_in.obs, obs_out.operands): - qml.assert_equal(o1, o2) - - # No Tensor deprecation warnings are raised - assert len(recwarn) == 0 - else: - qml.assert_equal(obs_out, obs_in) - assert obs_in.compare(obs_out) + qml.assert_equal(obs_out, obs_in) @pytest.mark.parametrize("attribute_cls", [DatasetOperator, DatasetPyTree]) @@ -156,8 +125,6 @@ def test_value_init(self, attribute_cls, obs_in): """Test that a DatasetOperator can be value-initialized from an observable, and that the deserialized operator is equivalent.""" - if not qml.operation.active_new_opmath() and isinstance(obs_in, qml.ops.LinearCombination): - obs_in = qml.operation.convert_to_legacy_H(obs_in) dset_op = attribute_cls(obs_in) @@ -170,8 +137,6 @@ def test_value_init(self, attribute_cls, obs_in): def test_bind_init(self, attribute_cls, obs_in): """Test that DatasetOperator can be initialized from a HDF5 group that contains an operator attribute.""" - if not qml.operation.active_new_opmath() and isinstance(obs_in, qml.ops.LinearCombination): - obs_in = qml.operation.convert_to_legacy_H(obs_in) bind = attribute_cls(obs_in).bind @@ -200,9 +165,6 @@ def test_value_init(self, attribute_cls, op_in): from an operator, and that the deserialized operator is equivalent.""" - if not qml.operation.active_new_opmath() and isinstance(op_in, qml.ops.LinearCombination): - op_in = qml.operation.convert_to_legacy_H(op_in) - dset_op = attribute_cls(op_in) assert dset_op.info["type_id"] == attribute_cls.type_id @@ -227,9 +189,6 @@ def test_bind_init(self, attribute_cls, op_in): from an operator, and that the deserialized operator is equivalent.""" - if not qml.operation.active_new_opmath() and isinstance(op_in, qml.ops.LinearCombination): - op_in = qml.operation.convert_to_legacy_H(op_in) - bind = attribute_cls(op_in).bind dset_op = attribute_cls(bind=bind) diff --git a/tests/default_qubit_legacy.py b/tests/default_qubit_legacy.py index 183f56c3df5..d6485a02f5b 100644 --- a/tests/default_qubit_legacy.py +++ b/tests/default_qubit_legacy.py @@ -23,7 +23,7 @@ from string import ascii_letters as ABC import numpy as np -from scipy.sparse import csr_matrix +from scipy.sparse import coo_matrix, csr_matrix import pennylane as qml from pennylane import BasisState, Snapshot, StatePrep @@ -603,7 +603,7 @@ def expval(self, observable, shot_range=None, bin_size=None): # intercept other Hamiltonians # TODO: Ideally, this logic should not live in the Device, but be moved # to a component that can be re-used by devices as needed. - if observable.name not in ("Hamiltonian", "SparseHamiltonian", "LinearCombination"): + if observable.name not in ("SparseHamiltonian", "LinearCombination"): return super().expval(observable, shot_range=shot_range, bin_size=bin_size) assert self.shots is None, f"{observable.name} must be used with shots=None" @@ -612,7 +612,7 @@ def expval(self, observable, shot_range=None, bin_size=None): backprop_mode = ( not isinstance(self.state, np.ndarray) or any(not isinstance(d, (float, np.ndarray)) for d in observable.data) - ) and observable.name in ["Hamiltonian", "LinearCombination"] + ) and observable.name == "LinearCombination" if backprop_mode: # TODO[dwierichs]: This branch is not adapted to broadcasting yet @@ -635,7 +635,8 @@ def expval(self, observable, shot_range=None, bin_size=None): # that the user provided. for op, coeff in zip(observable.ops, observable.data): # extract a scipy.sparse.coo_matrix representation of this Pauli word - coo = qml.operation.Tensor(op).sparse_matrix(wire_order=self.wires, format="coo") + sparse_mat = qml.prod(op).sparse_matrix(wire_order=self.wires) + coo = coo_matrix(sparse_mat) Hmat = qml.math.cast(qml.math.convert_like(coo.data, self.state), self.C_DTYPE) product = ( @@ -1087,7 +1088,6 @@ def _get_diagonalizing_gates(self, circuit: qml.tape.QuantumScript) -> list[Oper meas_filtered = [ m for m in circuit.measurements - if m.obs is None - or not isinstance(m.obs, (qml.ops.Hamiltonian, qml.ops.LinearCombination)) + if m.obs is None or not isinstance(m.obs, qml.ops.LinearCombination) ] return super()._get_diagonalizing_gates(qml.tape.QuantumScript(measurements=meas_filtered)) diff --git a/tests/devices/default_qubit/test_default_qubit.py b/tests/devices/default_qubit/test_default_qubit.py index f41bbdafe12..2f9a53eca92 100644 --- a/tests/devices/default_qubit/test_default_qubit.py +++ b/tests/devices/default_qubit/test_default_qubit.py @@ -821,9 +821,7 @@ def f(dev, scale, n_wires=10, offset=0.1, style="sum"): t1 = 2.5 * qml.prod(*(qml.PauliZ(i) for i in range(n_wires))) t2 = 6.2 * qml.prod(*(qml.PauliY(i) for i in range(n_wires))) H = t1 + t2 - if style == "hamiltonian": - H = H.pauli_rep.hamiltonian() - elif style == "hermitian": + if style == "hermitian": H = qml.Hermitian(H.matrix(), wires=H.wires) qs = qml.tape.QuantumScript(ops, [qml.expval(H)]) return dev.execute(qs) @@ -836,12 +834,9 @@ def f_hashable(scale, n_wires=10, offset=0.1, style="sum"): t1 = 2.5 * qml.prod(*(qml.PauliZ(i) for i in range(n_wires))) t2 = 6.2 * qml.prod(*(qml.PauliY(i) for i in range(n_wires))) H = t1 + t2 - if style == "hamiltonian": - H = H.pauli_rep.hamiltonian() - elif style == "hermitian": + if style == "hermitian": H = qml.Hermitian(H.matrix(), wires=H.wires) qs = qml.tape.QuantumScript(ops, [qml.expval(H)]) - qs = qml.tape.QuantumScript(ops, [qml.expval(H)]) return DefaultQubit().execute(qs) @staticmethod @@ -853,7 +848,7 @@ def expected(scale, n_wires=10, offset=0.1, like="numpy"): return 2.5 * qml.math.prod(cosines) + 6.2 * qml.math.prod(sines) @pytest.mark.autograd - @pytest.mark.parametrize("style", ("sum", "hamiltonian", "hermitian")) + @pytest.mark.parametrize("style", ("sum", "hermitian")) def test_autograd_backprop(self, style): """Test that backpropagation derivatives work in autograd with hamiltonians and large sums.""" dev = DefaultQubit() @@ -868,7 +863,7 @@ def test_autograd_backprop(self, style): @pytest.mark.jax @pytest.mark.parametrize("use_jit", (True, False)) - @pytest.mark.parametrize("style", ("sum", "hamiltonian", "hermitian")) + @pytest.mark.parametrize("style", ("sum", "hermitian")) def test_jax_backprop(self, style, use_jit): """Test that backpropagation derivatives work with jax with hamiltonians and large sums.""" import jax @@ -885,7 +880,7 @@ def test_jax_backprop(self, style, use_jit): assert qml.math.allclose(g, expected_g) @pytest.mark.torch - @pytest.mark.parametrize("style", ("sum", "hamiltonian", "hermitian")) + @pytest.mark.parametrize("style", ("sum", "hermitian")) def test_torch_backprop(self, style): """Test that backpropagation derivatives work with torch with hamiltonians and large sums.""" import torch @@ -903,7 +898,7 @@ def test_torch_backprop(self, style): assert qml.math.allclose(x.grad, x2.grad) @pytest.mark.tf - @pytest.mark.parametrize("style", ("sum", "hamiltonian", "hermitian")) + @pytest.mark.parametrize("style", ("sum", "hermitian")) def test_tf_backprop(self, style): """Test that backpropagation derivatives work with tensorflow with hamiltonians and large sums.""" import tensorflow as tf @@ -2162,9 +2157,6 @@ def test_differentiate_jitted_qnode(self, measurement_func): """Test that a jitted qnode can be correctly differentiated""" import jax - if measurement_func is qml.var and not qml.operation.active_new_opmath(): - pytest.skip(reason="Variance for this test circuit not supported with legacy opmath") - dev = DefaultQubit() def qfunc(x, y): diff --git a/tests/devices/default_qubit/test_default_qubit_native_mcm.py b/tests/devices/default_qubit/test_default_qubit_native_mcm.py index eed50f29e0f..ae764a0b025 100644 --- a/tests/devices/default_qubit/test_default_qubit_native_mcm.py +++ b/tests/devices/default_qubit/test_default_qubit_native_mcm.py @@ -136,12 +136,6 @@ def test_multiple_measurements_and_reset(mcm_method, shots, params, postselect, and a conditional gate. Multiple measurements of the mid-circuit measurement value are performed. This function also tests `reset` parametrizing over the parameter.""" - if mcm_method == "tree-traversal" and not qml.operation.active_new_opmath(): - pytest.xfail( - "The tree-traversal method does not work with legacy opmath with " - "`qml.var` of pauli observables in the circuit." - ) - if mcm_method == "one-shot" and shots is None: pytest.skip("`mcm_method='one-shot'` is incompatible with analytic mode (`shots=None`)") diff --git a/tests/devices/default_qubit/test_default_qubit_preprocessing.py b/tests/devices/default_qubit/test_default_qubit_preprocessing.py index 11a4bf001c2..fec4593bfee 100644 --- a/tests/devices/default_qubit/test_default_qubit_preprocessing.py +++ b/tests/devices/default_qubit/test_default_qubit_preprocessing.py @@ -575,32 +575,6 @@ def test_preprocess_check_validity_fail(self): with pytest.raises(qml.DeviceError, match="Operator NoMatNoDecompOp"): program(tapes) - @pytest.mark.usefixtures("legacy_opmath_only") - @pytest.mark.parametrize( - "ops, measurement, message", - [ - ( - [qml.RX(0.1, wires=0)], - [qml.probs(op=qml.PauliX(0))], - "adjoint diff supports either all expectation values or", - ), - ( - [qml.RX(0.1, wires=0)], - [qml.expval(qml.ops.Hamiltonian([1], [qml.PauliZ(0)]))], - "not supported on adjoint", - ), - ], - ) - @pytest.mark.filterwarnings("ignore:Differentiating with respect to") - def test_preprocess_invalid_tape_adjoint_legacy_opmath(self, ops, measurement, message): - """Test that preprocessing fails if adjoint differentiation is requested and an - invalid tape is used""" - qs = qml.tape.QuantumScript(ops, measurement) - execution_config = qml.devices.ExecutionConfig(gradient_method="adjoint") - program, _ = qml.device("default.qubit").preprocess(execution_config) - with pytest.raises(qml.DeviceError, match=message): - program([qs]) - @pytest.mark.parametrize( "ops, measurement, message", [ @@ -876,22 +850,6 @@ def test_u3_non_trainable_params(self): assert len(res.operations) == 5 assert res.trainable_params == [0, 1, 2, 3, 4] - @pytest.mark.usefixtures( - "legacy_opmath_only" - ) # this is only an issue for legacy Hamiltonian that does not define a matrix method - def test_unsupported_obs_legacy_opmath(self): - """Test that the correct error is raised if a Hamiltonian measurement is differentiated""" - obs = qml.Hamiltonian([2, 0.5], [qml.PauliZ(0), qml.PauliY(1)]) - qs = qml.tape.QuantumScript([qml.RX(0.5, wires=1)], [qml.expval(obs)]) - qs.trainable_params = {0} - - program = qml.device("default.qubit").preprocess( - ExecutionConfig(gradient_method="adjoint") - )[0] - - with pytest.raises(qml.DeviceError, match=r"Observable "): - program((qs,)) - def test_trainable_hermitian_warns(self): """Test attempting to compute the gradient of a tape that obtains the expectation value of a Hermitian operator emits a warning if the diff --git a/tests/devices/default_tensor/test_scalability.py b/tests/devices/default_tensor/test_scalability.py index af6ec5b6fc4..cb330fc5122 100644 --- a/tests/devices/default_tensor/test_scalability.py +++ b/tests/devices/default_tensor/test_scalability.py @@ -113,18 +113,6 @@ def circuit(): _ = qml.QNode(circuit, dev)() - def test_tensor(self, method): - """Test that the device can compute the expval of a multi-qubit Tensor.""" - - wires = 30 - dev = qml.device("default.tensor", wires=wires, method=method) - - def circuit(): - return qml.expval(qml.operation.Tensor(*(qml.PauliY(i) for i in range(wires)))) - - _ = qml.QNode(circuit, dev)() - - @pytest.mark.usefixtures("new_opmath_only") def test_hamiltonian(self, method): """Test that the device can compute the expval of a multi-qubit Hamiltonian.""" diff --git a/tests/devices/default_tensor/test_tensor_var.py b/tests/devices/default_tensor/test_tensor_var.py index 6c29b3bda26..794798db8a5 100644 --- a/tests/devices/default_tensor/test_tensor_var.py +++ b/tests/devices/default_tensor/test_tensor_var.py @@ -367,9 +367,6 @@ def test_PauliZ_hadamard_PauliY(self, theta, phi, varphi, dev): assert np.allclose(calculated_val, reference_val, tol) -# This test is only for the new opmath since there is an error -# in the tape computation with `default.qubit`, that we use as reference. -@pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("theta, phi", list(zip(THETA, PHI))) @pytest.mark.parametrize("method", ["mps", "tn"]) def test_multi_qubit_gates(theta, phi, method): diff --git a/tests/devices/qubit/test_measure.py b/tests/devices/qubit/test_measure.py index 8c6b7959463..8ddc076d028 100644 --- a/tests/devices/qubit/test_measure.py +++ b/tests/devices/qubit/test_measure.py @@ -97,30 +97,6 @@ def test_sum_sum_of_terms_when_backprop(self): state = qml.numpy.zeros(2) assert get_measurement_function(qml.expval(S), state) is sum_of_terms_method - @pytest.mark.usefixtures("legacy_opmath_only") - def test_hamiltonian_with_multi_wire_obs(self): - """Check that a Hamiltonian with a multi-wire observable uses the sum of terms method.""" - - S = qml.Hamiltonian( - [0.5, 0.5], - [ - qml.X(0), - qml.Hermitian( - np.array( - [ - [0.5, 1.0j, 0.0, -3j], - [-1.0j, -1.1, 0.0, -0.1], - [0.0, 0.0, -0.9, 12.0], - [3j, -0.1, 12.0, 0.0], - ] - ), - wires=[0, 1], - ), - ], - ) - state = np.zeros(2) - assert get_measurement_function(qml.expval(S), state) is sum_of_terms_method - def test_no_sparse_matrix(self): """Tests Hamiltonians/Sums containing observables that do not have a sparse matrix.""" @@ -230,7 +206,6 @@ def qnode(t1, t2): t1, t2 = 0.5, 1.0 assert qml.math.allclose(qnode(t1, t2), jax.jit(qnode)(t1, t2)) - @pytest.mark.usefixtures("new_opmath_only") def test_measure_identity_no_wires(self): """Test that measure can handle the expectation value of identity on no wires.""" @@ -441,8 +416,8 @@ def f(scale, coeffs, n_wires=10, offset=0.1, convert_to_hamiltonian=False): H = qml.Hamiltonian( coeffs, [ - qml.operation.Tensor(*(qml.PauliZ(i) for i in range(n_wires))), - qml.operation.Tensor(*(qml.PauliY(i) for i in range(n_wires))), + qml.prod(*(qml.PauliZ(i) for i in range(n_wires))), + qml.prod(*(qml.PauliY(i) for i in range(n_wires))), ], ) else: diff --git a/tests/devices/qubit/test_sampling.py b/tests/devices/qubit/test_sampling.py index 1675508b98b..c2c140e4941 100644 --- a/tests/devices/qubit/test_sampling.py +++ b/tests/devices/qubit/test_sampling.py @@ -517,7 +517,6 @@ def test_identity_on_no_wires(self): [result] = measure_with_samples([mp], state, shots=qml.measurements.Shots(1)) assert qml.math.allclose(result, 1.0) - @pytest.mark.usefixtures("new_opmath_only") def test_identity_on_no_wires_with_other_observables(self): """Test that measuring an identity on no wires can be used in conjunction with other measurements.""" @@ -1150,7 +1149,6 @@ class TestHamiltonianSamples: """Test that the measure_with_samples function works as expected for Hamiltonian and Sum observables""" - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_hamiltonian_expval(self, seed): """Test that sampling works well for Hamiltonian observables""" x, y = np.array(0.67), np.array(0.95) diff --git a/tests/devices/qubit/test_simulate.py b/tests/devices/qubit/test_simulate.py index c926a4d655c..f399201b2bb 100644 --- a/tests/devices/qubit/test_simulate.py +++ b/tests/devices/qubit/test_simulate.py @@ -1306,16 +1306,6 @@ def test_simple_dynamic_circuit(self, shots, measure_f, postselect, reset, meas_ The above combinations should work for finite shots, shot vectors and post-selecting of either the 0 or 1 branch. """ - if ( - isinstance(meas_obj, (qml.X, qml.Z, qml.Y)) - and measure_f in (qml.var,) - and not qml.operation.active_new_opmath() - ): - pytest.xfail( - "The tree-traversal method does not work with legacy opmath with " - "`qml.var` of pauli observables in the circuit." - ) - if measure_f in (qml.expval, qml.var) and ( isinstance(meas_obj, list) or meas_obj == "mcm_list" ): diff --git a/tests/devices/qutrit_mixed/test_qutrit_mixed_measure.py b/tests/devices/qutrit_mixed/test_qutrit_mixed_measure.py index 56d61fa339b..8898b00e129 100644 --- a/tests/devices/qutrit_mixed/test_qutrit_mixed_measure.py +++ b/tests/devices/qutrit_mixed/test_qutrit_mixed_measure.py @@ -427,7 +427,6 @@ def test_variance_measurement(self, observable, ml_framework, two_qutrit_batched assert np.allclose(res, expected) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") class TestSumOfTermsDifferentiability: x = 0.52 diff --git a/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py b/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py index a49265ce082..f72f433ff51 100644 --- a/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py +++ b/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py @@ -144,7 +144,6 @@ def test_accepted_operator(self, op, expected): (qml.QutritDepolarizingChannel(0.4, 0), False), (qml.GellMann(0, 1), True), (qml.Snapshot(), False), - (qml.operation.Tensor(qml.GellMann(0, 1), qml.GellMann(3, 3)), True), (qml.ops.op_math.SProd(1.2, qml.GellMann(0, 1)), True), (qml.sum(qml.ops.op_math.SProd(1.2, qml.GellMann(0, 1)), qml.GellMann(1, 3)), True), (qml.ops.op_math.Prod(qml.GellMann(0, 1), qml.GellMann(3, 3)), True), diff --git a/tests/devices/qutrit_mixed/test_qutrit_mixed_sampling.py b/tests/devices/qutrit_mixed/test_qutrit_mixed_sampling.py index 176d9c92b31..618fa08da97 100644 --- a/tests/devices/qutrit_mixed/test_qutrit_mixed_sampling.py +++ b/tests/devices/qutrit_mixed/test_qutrit_mixed_sampling.py @@ -373,7 +373,7 @@ def test_sample_observables(self): qml.sample(qml.GellMann(0, 1) @ qml.GellMann(1, 1)), state, shots=shots ) assert results_gel_1s.shape == (shots.total_shots,) - assert results_gel_1s.dtype == np.float64 if qml.operation.active_new_opmath() else np.int64 + assert results_gel_1s.dtype == np.float64 assert sorted(np.unique(results_gel_1s)) == [-1, 0, 1] @flaky @@ -654,13 +654,9 @@ class TestHamiltonianSamples: """Test that the measure_with_samples function works as expected for Hamiltonian and Sum observables""" - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_hamiltonian_expval(self, obs, seed): """Test that sampling works well for Hamiltonian and Sum observables""" - if not qml.operation.active_new_opmath(): - obs = qml.operation.convert_to_legacy_H(obs) - shots = qml.measurements.Shots(10000) x, y = np.array(0.67), np.array(0.95) ops = [qml.TRY(x, wires=0), qml.TRZ(y, wires=0)] @@ -674,13 +670,9 @@ def test_hamiltonian_expval(self, obs, seed): assert isinstance(res, np.float64) assert np.allclose(res, expected, atol=APPROX_ATOL) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_hamiltonian_expval_shot_vector(self, obs, seed): """Test that sampling works well for Hamiltonian and Sum observables with a shot vector""" - if not qml.operation.active_new_opmath(): - obs = qml.operation.convert_to_legacy_H(obs) - shots = qml.measurements.Shots((10000, 100000)) x, y = np.array(0.67), np.array(0.95) ops = [qml.TRY(x, wires=0), qml.TRZ(y, wires=0)] diff --git a/tests/devices/qutrit_mixed/test_qutrit_mixed_tracking.py b/tests/devices/qutrit_mixed/test_qutrit_mixed_tracking.py index 41a324aab7b..73f801253a7 100644 --- a/tests/devices/qutrit_mixed/test_qutrit_mixed_tracking.py +++ b/tests/devices/qutrit_mixed/test_qutrit_mixed_tracking.py @@ -201,7 +201,6 @@ def test_single_expval(self, mps, expected_exec, expected_shots): assert dev.tracker.totals["simulations"] == 1 assert dev.tracker.totals["shots"] == 3 * expected_shots - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_multiple_expval_with_prods(self): """ Test tracker tracks default qutrit mixed execute number of shots for new and old opmath tensors. diff --git a/tests/devices/test_default_clifford.py b/tests/devices/test_default_clifford.py index ba871dc961b..9f3b55933af 100644 --- a/tests/devices/test_default_clifford.py +++ b/tests/devices/test_default_clifford.py @@ -256,7 +256,6 @@ def circuit_fn(): assert qml.math.shape(samples[2]) == (shots,) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("tableau", [True, False]) @pytest.mark.parametrize("shots", [None, 50000]) @pytest.mark.parametrize( diff --git a/tests/devices/test_default_qutrit_mixed.py b/tests/devices/test_default_qutrit_mixed.py index 8c9c635509a..e7a84344862 100644 --- a/tests/devices/test_default_qutrit_mixed.py +++ b/tests/devices/test_default_qutrit_mixed.py @@ -749,7 +749,6 @@ def test_tf(self): assert qml.math.allclose(jacobian_1, jacobian_3) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") class TestSumOfTermsDifferentiability: """Tests Hamiltonian and sum expvals are still differentiable. This is a copy of the tests in `test_qutrit_mixed_measure.py`, but using the device instead. @@ -1142,7 +1141,6 @@ def test_different_executions_same_prng_key(self): qml.s_prod(0.8, qml.GellMann(0, 3)) + qml.s_prod(0.5, qml.GellMann(0, 1)), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") class TestHamiltonianSamples: """Test that the measure_with_samples function works as expected for Hamiltonian and Sum observables. @@ -1151,8 +1149,6 @@ class TestHamiltonianSamples: def test_hamiltonian_expval(self, obs, seed): """Tests that sampling works well for Hamiltonian and Sum observables.""" - if not qml.operation.active_new_opmath(): - obs = qml.operation.convert_to_legacy_H(obs) x, y = np.array(0.67), np.array(0.95) ops = [qml.TRY(x, wires=0), qml.TRZ(y, wires=0)] @@ -1167,9 +1163,6 @@ def test_hamiltonian_expval(self, obs, seed): def test_hamiltonian_expval_shot_vector(self, obs, seed): """Test that sampling works well for Hamiltonian and Sum observables with a shot vector.""" - if not qml.operation.active_new_opmath(): - obs = qml.operation.convert_to_legacy_H(obs) - shots = qml.measurements.Shots((10000, 100000)) x, y = np.array(0.67), np.array(0.95) ops = [qml.TRY(x, wires=0), qml.TRZ(y, wires=0)] @@ -1245,9 +1238,6 @@ def test_differentiate_jitted_qnode(self, measurement_func): """Test that a jitted qnode can be correctly differentiated""" import jax - if measurement_func is qml.var and not qml.operation.active_new_opmath(): - pytest.skip(reason="Variance for this test circuit not supported with legacy opmath") - dev = qml.device("default.qutrit.mixed") def qfunc(x, y): diff --git a/tests/devices/test_legacy_device.py b/tests/devices/test_legacy_device.py index dacf7b3b1d6..671425692d6 100644 --- a/tests/devices/test_legacy_device.py +++ b/tests/devices/test_legacy_device.py @@ -314,7 +314,6 @@ def test_check_validity_on_valid_queue(self, mock_device_supporting_paulis): # Raises an error if queue or observables are invalid dev.check_validity(queue, observables) - @pytest.mark.usefixtures("new_opmath_only") def test_check_validity_containing_prod(self, mock_device_supporting_prod): """Tests that the function Device.check_validity works with Prod""" @@ -332,7 +331,6 @@ def test_check_validity_containing_prod(self, mock_device_supporting_prod): dev.check_validity(queue, observables) - @pytest.mark.usefixtures("new_opmath_only") def test_prod_containing_unsupported_nested_observables(self, mock_device_supporting_prod): """Tests that the observables nested within Prod are checked for validity""" @@ -350,24 +348,6 @@ def test_prod_containing_unsupported_nested_observables(self, mock_device_suppor with pytest.raises(qml.DeviceError, match="Observable PauliY not supported"): dev.check_validity(queue, unsupported_nested_observables) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_check_validity_on_tensor_support_legacy_opmath(self, mock_device_supporting_paulis): - """Tests the function Device.check_validity with tensor support capability""" - dev = mock_device_supporting_paulis() - - queue = [ - qml.PauliX(wires=0), - qml.PauliY(wires=1), - qml.PauliZ(wires=2), - ] - - observables = [qml.expval(qml.PauliZ(0) @ qml.PauliX(1))] - - # mock device does not support Tensor product - with pytest.raises(qml.DeviceError, match="Tensor observables not supported"): - dev.check_validity(queue, observables) - - @pytest.mark.usefixtures("new_opmath_only") def test_check_validity_on_prod_support(self, mock_device_supporting_paulis): """Tests the function Device.check_validity with prod support capability""" dev = mock_device_supporting_paulis() @@ -384,32 +364,6 @@ def test_check_validity_on_prod_support(self, mock_device_supporting_paulis): with pytest.raises(qml.DeviceError, match="Observable Prod not supported"): dev.check_validity(queue, observables) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_check_validity_on_invalid_observable_with_tensor_support(self, monkeypatch): - """Tests the function Device.check_validity with tensor support capability - but with an invalid observable""" - queue = [ - qml.PauliX(wires=0), - qml.PauliY(wires=1), - qml.PauliZ(wires=2), - ] - - observables = [qml.expval(qml.PauliZ(0) @ qml.Hadamard(1))] - - D = Device - with monkeypatch.context() as m: - m.setattr(D, "__abstractmethods__", frozenset()) - m.setattr(D, "operations", ["PauliX", "PauliY", "PauliZ"]) - m.setattr(D, "observables", ["PauliX", "PauliY", "PauliZ"]) - m.setattr(D, "capabilities", lambda self: {"supports_tensor_observables": True}) - m.setattr(D, "short_name", "Dummy") - - dev = D() - - # mock device supports Tensor products but not hadamard - with pytest.raises(qml.DeviceError, match="Observable Hadamard not supported"): - dev.check_validity(queue, observables) - def test_check_validity_on_invalid_queue(self, mock_device_supporting_paulis): """Tests the function Device.check_validity with invalid queue and valid observables""" dev = mock_device_supporting_paulis() diff --git a/tests/devices/test_null_qubit.py b/tests/devices/test_null_qubit.py index d0565b1b9e6..928a4c7ea90 100644 --- a/tests/devices/test_null_qubit.py +++ b/tests/devices/test_null_qubit.py @@ -658,9 +658,7 @@ def f(dev, scale, n_wires=10, offset=0.1, style="sum"): t1 = 2.5 * qml.prod(*(qml.PauliZ(i) for i in range(n_wires))) t2 = 6.2 * qml.prod(*(qml.PauliY(i) for i in range(n_wires))) H = t1 + t2 - if style == "hamiltonian": - H = H.pauli_rep.hamiltonian() - elif style == "hermitian": + if style == "hermitian": H = qml.Hermitian(H.matrix(), wires=H.wires) qs = qml.tape.QuantumScript(ops, [qml.expval(H)]) config = ExecutionConfig(interface=qml.math.get_interface(scale)) @@ -674,12 +672,9 @@ def f_hashable(scale, n_wires=10, offset=0.1, style="sum"): t1 = 2.5 * qml.prod(*(qml.PauliZ(i) for i in range(n_wires))) t2 = 6.2 * qml.prod(*(qml.PauliY(i) for i in range(n_wires))) H = t1 + t2 - if style == "hamiltonian": - H = H.pauli_rep.hamiltonian() - elif style == "hermitian": + if style == "hermitian": H = qml.Hermitian(H.matrix(), wires=H.wires) qs = qml.tape.QuantumScript(ops, [qml.expval(H)]) - qs = qml.tape.QuantumScript(ops, [qml.expval(H)]) return NullQubit().execute(qs) @staticmethod @@ -691,7 +686,7 @@ def expected(scale, n_wires=10, offset=0.1, like="numpy"): return 2.5 * qml.math.prod(cosines) + 6.2 * qml.math.prod(sines) @pytest.mark.autograd - @pytest.mark.parametrize("style", ("sum", "hamiltonian", "hermitian")) + @pytest.mark.parametrize("style", ("sum", "hermitian")) def test_autograd_backprop(self, style): """Test that backpropagation derivatives work in autograd with hamiltonians and large sums.""" dev = NullQubit() @@ -701,7 +696,7 @@ def test_autograd_backprop(self, style): @pytest.mark.jax @pytest.mark.parametrize("use_jit", (True, False)) - @pytest.mark.parametrize("style", ("sum", "hamiltonian", "hermitian")) + @pytest.mark.parametrize("style", ("sum", "hermitian")) def test_jax_backprop(self, style, use_jit): """Test that backpropagation derivatives work with jax with hamiltonians and large sums.""" import jax @@ -714,7 +709,7 @@ def test_jax_backprop(self, style, use_jit): @pytest.mark.xfail(reason="torch backprop does not work") @pytest.mark.torch - @pytest.mark.parametrize("style", ("sum", "hamiltonian", "hermitian")) + @pytest.mark.parametrize("style", ("sum", "hermitian")) def test_torch_backprop(self, style): """Test that backpropagation derivatives work with torch with hamiltonians and large sums.""" import torch @@ -730,7 +725,7 @@ def test_torch_backprop(self, style): @pytest.mark.xfail(reason="tf can't track derivatives") @pytest.mark.tf - @pytest.mark.parametrize("style", ("sum", "hamiltonian", "hermitian")) + @pytest.mark.parametrize("style", ("sum", "hermitian")) def test_tf_backprop(self, style): """Test that backpropagation derivatives work with tensorflow with hamiltonians and large sums.""" import tensorflow as tf diff --git a/tests/devices/test_preprocess.py b/tests/devices/test_preprocess.py index 9a6c4a2e472..861f1ff078d 100644 --- a/tests/devices/test_preprocess.py +++ b/tests/devices/test_preprocess.py @@ -306,14 +306,6 @@ def test_invalid_tensor_observable(self): with pytest.raises(qml.DeviceError, match="not supported on device"): validate_observables(tape, lambda obj: obj.name == "PauliX") - @pytest.mark.usefixtures("legacy_opmath_only") # only required for legacy observables - def test_valid_tensor_observable_legacy_opmath(self): - """Test that a valid tensor ovservable passes without error.""" - tape = QuantumScript([], [qml.expval(qml.PauliZ(0) @ qml.PauliY(1))]) - assert ( - validate_observables(tape, lambda obs: obs.name in {"PauliZ", "PauliY"})[0][0] is tape - ) - class TestValidateMeasurements: """Tests for the validate measurements transform.""" diff --git a/tests/fermi/test_bravyi_kitaev.py b/tests/fermi/test_bravyi_kitaev.py index 07a1fea5187..a78e0cb3735 100644 --- a/tests/fermi/test_bravyi_kitaev.py +++ b/tests/fermi/test_bravyi_kitaev.py @@ -427,412 +427,7 @@ def test_error_is_raised_for_dimension_mismatch(): ), ] -with qml.operation.disable_new_opmath_cm(warn=False): - FERMI_WORDS_AND_OPS_LEGACY = [ - ( - FermiWord({(0, 0): "+"}), - 1, - # trivial case of a creation operator with one qubit, 0^ -> (X_0 - iY_0) / 2 : Same as Jordan-Wigner - ([0.5, -0.5j], [qml.PauliX(0), qml.PauliY(0)]), - ), - ( - FermiWord({(0, 0): "-"}), - 1, - # trivial case of an annihilation operator with one qubit , 0 -> (X_0 + iY_0) / 2 : Same as Jordan-Wigner - ([(0.5 + 0j), (0.0 + 0.5j)], [qml.PauliX(0), qml.PauliY(0)]), - ), - ( - FermiWord({(0, 0): "+"}), - 2, - # trivial case of a creation operator with two qubits, 0^ -> (X_0 @ X_1 - iY_0 @ X_1) / 2 - ([0.5, -0.5j], [qml.PauliX(0) @ qml.PauliX(1), qml.PauliY(0) @ qml.PauliX(1)]), - ), - ( - FermiWord({(0, 0): "-"}), - 2, - # trivial case of an annihilation operator with two qubits , 0 -> (X_0 @ X_1 + iY_0 @ X_1) / 2 - ( - [(0.5 + 0j), (0.0 + 0.5j)], - [qml.PauliX(0) @ qml.PauliX(1), qml.PauliY(0) @ qml.PauliX(1)], - ), - ), - ( - FermiWord({(0, 0): "+", (1, 0): "-"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('0^ 0'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output: (0.5+0j) [] + (-0.5+0j) [Z0] - ([(0.5 + 0j), (-0.5 + 0j)], [qml.Identity(0), qml.PauliZ(0)]), - ), - ( - FermiWord({(0, 0): "-", (1, 0): "+"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('0 0^'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output: (0.5+0j) [] + (0.5+0j) [Z0] - ([(0.5 + 0j), (0.5 + 0j)], [qml.Identity(0), qml.PauliZ(0)]), - ), - ( - FermiWord({(0, 0): "-", (1, 1): "+"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('0 1^'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output: - # (-0.25+0j) [X0] + - # 0.25 [X0 Z1] + - # (-0-0.25j) [Y0] + - # 0.25j [Y0 Z1] - ( - [(-0.25 + 0j), 0.25, (-0 - 0.25j), (0.25j)], - [ - qml.PauliX(0), - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliY(0), - qml.PauliY(0) @ qml.PauliZ(1), - ], - ), - ), - ( - FermiWord({(0, 3): "+", (1, 0): "-"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('3^ 0'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output - # (-0.25+0j) [X0 X1 Z3] + - # 0.25j [X0 Y1 Z2] + - # -0.25j [Y0 X1 Z3] + - # (-0.25+0j) [Y0 Y1 Z2] - ( - [(-0.25 + 0j), 0.25j, (-0 - 0.25j), -0.25], - [ - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliZ(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliZ(2), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliZ(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliZ(2), - ], - ), - ), - ( - FermiWord({(0, 5): "+", (1, 5): "-", (2, 5): "+", (3, 5): "-"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('5^ 5 5^ 5'), parity_code(n_qubits)) with 6 qubits - ( - [(0.5 + 0j), (-0.5 + 0j)], - [qml.Identity(0), qml.PauliZ(4) @ qml.PauliZ(5)], - ), - ), - ( - FermiWord({(0, 3): "+", (1, 3): "-", (2, 3): "+", (3, 1): "-"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('3^ 3 3^ 1'), parity_code(n_qubits)) with 6 qubits - # (-0.25+0j) [Z0 X1 Z3] + - # 0.25j [Z0 Y1 Z2] + - # (0.25+0j) [X1 Z2] + - # -0.25j [Y1 Z3] - ( - [-0.25, 0.25j, -0.25j, 0.25], - [ - qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliZ(3), - qml.PauliZ(0) @ qml.PauliY(1) @ qml.PauliZ(2), - qml.PauliY(1) @ qml.PauliZ(3), - qml.PauliX(1) @ qml.PauliZ(2), - ], - ), - ), - ( - FermiWord({(0, 1): "+", (1, 0): "-", (2, 1): "+", (3, 1): "-"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('1^ 0 1^ 1'), parity_code(n_qubits)) with 6 qubits - ([0], [qml.Identity(0)]), - ), - ] - - FERMI_OPS_COMPLEX_LEGACY = [ - ( - FermiWord({(0, 2): "-", (1, 0): "+", (2, 3): "+"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('2 0^ 3^'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output - # (0.125+0j) [X0 X1 X2 X3] + - # 0.125j [X0 X1 Y2 X3] + - # (0.125+0j) [X0 Y1 X2 Y3] + - # 0.125j [X0 Y1 Y2 Y3] + - # -0.125j [Y0 X1 X2 X3] + - # (0.125+0j) [Y0 X1 Y2 X3] + - # -0.125j [Y0 Y1 X2 Y3] + - # (0.125+0j) [Y0 Y1 Y2 Y3] - ( - [0.125, 0.125j, 0.125 + 0j, 0.125j, -0.125j, 0.125 + 0j, -0.125j, 0.125], - [ - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliY(3), - ], - ), - ), - ( - FermiWord({(0, 0): "-", (1, 3): "+"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('0 3^'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output - # (0.25+0j) [X0 X1 Z3] + - # -0.25j [X0 Y1 Z2] + - # 0.25j [Y0 X1 Z3] + - # (0.25+0j) [Y0 Y1 Z2] - ( - [0.25, -0.25j, 0.25j, 0.25], - [ - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliZ(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliZ(2), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliZ(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliZ(2), - ], - ), - ), - ( - FermiWord({(0, 3): "+", (1, 1): "+", (2, 3): "-", (3, 1): "-"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('3^ 1^ 3 1'), parity_code(n_qubits)) with 6 qubits - # -0.25 [] + - # 0.25 [Z0 Z1] + - # -0.25 [Z0 Z2 Z3] + - # 0.25 [Z1 Z2 Z3] - # reformatted the original openfermion output - ( - [(-0.25 + 0j), (0.25 + 0j), (-0.25 + 0j), (0.25 + 0j)], - [ - qml.Identity(0), - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliZ(0) @ qml.PauliZ(2) @ qml.PauliZ(3), - qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliZ(3), - ], - ), - ), - ( - FermiWord({(0, 1): "+", (1, 4): "-", (2, 3): "-", (3, 4): "+"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('1^ 4 3 4^'), parity_code(n_qubits)) with 6 qubits - # reformatted the original openfermion output - # (0.125+0j) [Z0 X1 Z3] + - # (0.125+0j) [Z0 X1 Z3 Z4] + - # 0.125j [Z0 Y1 Z2] + - # 0.125j [Z0 Y1 Z2 Z4] + - # (-0.125+0j) [X1 Z2] + - # (-0.125+0j) [X1 Z2 Z4] + - # -0.125j [Y1 Z3] + - # -0.125j [Y1 Z3 Z4] - ( - [0.125, 0.125, 0.125j, 0.125j, -0.125, -0.125, -0.125j, -0.125j], - [ - qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliZ(3), - qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliZ(3) @ qml.PauliZ(4), - qml.PauliZ(0) @ qml.PauliY(1) @ qml.PauliZ(2), - qml.PauliZ(0) @ qml.PauliY(1) @ qml.PauliZ(2) @ qml.PauliZ(4), - qml.PauliX(1) @ qml.PauliZ(2), - qml.PauliX(1) @ qml.PauliZ(2) @ qml.PauliZ(4), - qml.PauliY(1) @ qml.PauliZ(3), - qml.PauliY(1) @ qml.PauliZ(3) @ qml.PauliZ(4), - ], - ), - ), - ( - FermiWord({(0, 3): "+", (1, 1): "-", (2, 3): "+", (3, 1): "-"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('3^ 1 3^ 1'), parity_code(n_qubits)) with 6 qubits - ([0], [qml.Identity(3)]), - ), - ( - FermiWord({(0, 1): "+", (1, 0): "+", (2, 4): "-", (3, 5): "-"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('1^ 0^ 4 5'), parity_code(n_qubits)) with 6 qubits - # (0.0625+0j) [X0 Z1 X4] + - # (0.0625+0j) [X0 Z1 X4 Z5] + - # 0.0625j [X0 Z1 Y4] + - # 0.0625j [X0 Z1 Y4 Z5] + - # (0.0625+0j) [X0 X4] + - # (0.0625+0j) [X0 X4 Z5] + - # 0.0625j [X0 Y4] + - # 0.0625j [X0 Y4 Z5] + - # -0.0625j [Y0 Z1 X4] + - # -0.0625j [Y0 Z1 X4 Z5] + - # (0.0625+0j) [Y0 Z1 Y4] + - # (0.0625+0j) [Y0 Z1 Y4 Z5] + - # -0.0625j [Y0 X4] + - # -0.0625j [Y0 X4 Z5] + - # (0.0625+0j) [Y0 Y4] + - # (0.0625+0j) [Y0 Y4 Z5] - ( - [ - 0.0625, - 0.0625, - 0.0625j, - 0.0625j, - 0.0625, - 0.0625, - 0.0625j, - 0.0625j, - -0.0625j, - -0.0625j, - 0.0625, - 0.0625, - -0.0625j, - -0.0625j, - 0.0625, - 0.0625, - ], - [ - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliX(4), - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliY(4), - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliX(0) @ qml.PauliX(4), - qml.PauliX(0) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliX(0) @ qml.PauliY(4), - qml.PauliX(0) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliX(4), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliY(4), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliY(0) @ qml.PauliX(4), - qml.PauliY(0) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliY(0) @ qml.PauliY(4), - qml.PauliY(0) @ qml.PauliY(4) @ qml.PauliZ(5), - ], - ), - ), - ( - FermiWord({(0, 1): "-", (1, 0): "+"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('1 0^'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output: - # -0.25 [X0] + - # 0.25 [X0 Z1] + - # 0.25j [Y0] + - # (-0-0.25j) [Y0 Z1] - ( - [(-0.25 + 0j), 0.25, 0.25j, (-0 - 0.25j)], - [ - qml.PauliX(0), - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliY(0), - qml.PauliY(0) @ qml.PauliZ(1), - ], - ), - ), - ( - FermiWord( - {(0, 1): "+", (1, 1): "-", (2, 3): "+", (3, 4): "-", (4, 2): "+", (5, 5): "-"} - ), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('1^ 1 3^ 4 2^ 5'), parity_code(n_qubits)) with 6 qubits - # (0.03125+0j) [Z0 Z1 X2 X4] + - # (0.03125+0j) [Z0 Z1 X2 X4 Z5] + - # 0.03125j [Z0 Z1 X2 Y4] + - # 0.03125j [Z0 Z1 X2 Y4 Z5] + - # -0.03125j [Z0 Z1 Y2 X4] + - # -0.03125j [Z0 Z1 Y2 X4 Z5] + - # (0.03125+0j) [Z0 Z1 Y2 Y4] + - # (0.03125+0j) [Z0 Z1 Y2 Y4 Z5] + - # (0.03125+0j) [Z0 X2 Z3 X4] + - # (0.03125+0j) [Z0 X2 Z3 X4 Z5] + - # 0.03125j [Z0 X2 Z3 Y4] + - # 0.03125j [Z0 X2 Z3 Y4 Z5] + - # -0.03125j [Z0 Y2 Z3 X4] + - # -0.03125j [Z0 Y2 Z3 X4 Z5] + - # (0.03125+0j) [Z0 Y2 Z3 Y4] + - # (0.03125+0j) [Z0 Y2 Z3 Y4 Z5] + - # (-0.03125+0j) [Z1 X2 Z3 X4] + - # (-0.03125+0j) [Z1 X2 Z3 X4 Z5] + - # -0.03125j [Z1 X2 Z3 Y4] + - # -0.03125j [Z1 X2 Z3 Y4 Z5] + - # 0.03125j [Z1 Y2 Z3 X4] + - # 0.03125j [Z1 Y2 Z3 X4 Z5] + - # (-0.03125+0j) [Z1 Y2 Z3 Y4] + - # (-0.03125+0j) [Z1 Y2 Z3 Y4 Z5] + - # (-0.03125+0j) [X2 X4] + - # (-0.03125+0j) [X2 X4 Z5] + - # -0.03125j [X2 Y4] + - # -0.03125j [X2 Y4 Z5] + - # 0.03125j [Y2 X4] + - # 0.03125j [Y2 X4 Z5] + - # (-0.03125+0j) [Y2 Y4] + - # (-0.03125+0j) [Y2 Y4 Z5] - ( - [ - 0.03125, - 0.03125, - 0.03125j, - 0.03125j, - -0.03125j, - -0.03125j, - 0.03125, - 0.03125, - 0.03125, - 0.03125, - 0.03125j, - 0.03125j, - -0.03125j, - -0.03125j, - 0.03125, - 0.03125, - -0.03125, - -0.03125, - -0.03125j, - -0.03125j, - 0.03125j, - 0.03125j, - -0.03125, - -0.03125, - -0.03125, - -0.03125, - -0.03125j, - -0.03125j, - 0.03125j, - 0.03125j, - -0.03125, - -0.03125, - ], - [ - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliX(4), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliY(4), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliX(4), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliY(4), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliX(4), - qml.PauliZ(0) @ qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliY(4), - qml.PauliZ(0) @ qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliX(4), - qml.PauliZ(0) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliY(4), - qml.PauliZ(0) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliX(4), - qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliY(4), - qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliX(4), - qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliY(4), - qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliX(2) @ qml.PauliX(4), - qml.PauliX(2) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliX(2) @ qml.PauliY(4), - qml.PauliX(2) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliY(2) @ qml.PauliX(4), - qml.PauliY(2) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliY(2) @ qml.PauliY(4), - qml.PauliY(2) @ qml.PauliY(4) @ qml.PauliZ(5), - ], - ), - ), - ] - -@pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("fermionic_op, n_qubits, result", FERMI_WORDS_AND_OPS + FERMI_OPS_COMPLEX) def test_bravyi_kitaev_fermi_word_ps(fermionic_op, n_qubits, result): """Test that the parity_transform function returns the correct qubit operator.""" @@ -847,24 +442,6 @@ def test_bravyi_kitaev_fermi_word_ps(fermionic_op, n_qubits, result): assert qubit_op == expected_op -@pytest.mark.usefixtures("legacy_opmath_only") -@pytest.mark.parametrize( - "fermionic_op, n_qubits, result", FERMI_WORDS_AND_OPS_LEGACY + FERMI_OPS_COMPLEX_LEGACY -) -def test_bravyi_kitaev_fermi_word_ps_legacy(fermionic_op, n_qubits, result): - """Test that the parity_transform function returns the correct qubit operator.""" - # convert FermiWord to PauliSentence and simplify - qubit_op = bravyi_kitaev(fermionic_op, n_qubits, ps=True) - qubit_op.simplify() - - # get expected op as PauliSentence and simplify - expected_op = pauli_sentence(qml.Hamiltonian(result[0], result[1])) - expected_op.simplify() - - assert qubit_op == expected_op - - -@pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("fermionic_op, n_qubits, result", FERMI_WORDS_AND_OPS) def test_bravyi_kitaev_fermi_word_operation(fermionic_op, n_qubits, result): wires = fermionic_op.wires or [0] @@ -877,19 +454,6 @@ def test_bravyi_kitaev_fermi_word_operation(fermionic_op, n_qubits, result): qml.assert_equal(qubit_op.simplify(), expected_op.simplify()) -@pytest.mark.usefixtures("legacy_opmath_only") -@pytest.mark.parametrize("fermionic_op, n_qubits, result", FERMI_WORDS_AND_OPS_LEGACY) -def test_bravyi_kitaev_fermi_word_operation_legacy(fermionic_op, n_qubits, result): - wires = fermionic_op.wires or [0] - - qubit_op = bravyi_kitaev(fermionic_op, n_qubits) - - expected_op = pauli_sentence(qml.Hamiltonian(result[0], result[1])) - expected_op = expected_op.operation(wires) - - qml.assert_equal(qubit_op.simplify(), expected_op.simplify()) - - def test_bravyi_kitaev_for_identity(): """Test that the bravyi_kitaev function returns the correct qubit operator for Identity.""" qml.assert_equal(bravyi_kitaev(FermiWord({}), 2), qml.Identity(0)) diff --git a/tests/fermi/test_fermi_mapping.py b/tests/fermi/test_fermi_mapping.py index 3853ce26a99..492d7195157 100644 --- a/tests/fermi/test_fermi_mapping.py +++ b/tests/fermi/test_fermi_mapping.py @@ -349,336 +349,6 @@ ] -with qml.operation.disable_new_opmath_cm(warn=False): - FERMI_WORDS_AND_OPS_LEGACY = [ - ( - FermiWord({(0, 0): "+"}), - # trivial case of a creation operator, 0^ -> (X_0 - iY_0) / 2 - ([0.5, -0.5j], [qml.PauliX(0), qml.PauliY(0)]), - ), - ( - FermiWord({(0, 0): "-"}), - # trivial case of an annihilation operator, 0 -> (X_0 + iY_0) / 2 - ([(0.5 + 0j), (0.0 + 0.5j)], [qml.PauliX(0), qml.PauliY(0)]), - ), - ( - FermiWord({(0, 0): "+", (1, 0): "-"}), - # obtained with openfermion using: jordan_wigner(FermionOperator('0^ 0', 1)) - # reformatted the original openfermion output: (0.5+0j) [] + (-0.5+0j) [Z0] - ([(0.5 + 0j), (-0.5 + 0j)], [qml.Identity(0), qml.PauliZ(0)]), - ), - ( - FermiWord({(0, 0): "-", (1, 0): "+"}), - # obtained with openfermion using: jordan_wigner(FermionOperator('0 0^')) - # reformatted the original openfermion output: (0.5+0j) [] + (0.5+0j) [Z0] - ([(0.5 + 0j), (0.5 + 0j)], [qml.Identity(0), qml.PauliZ(0)]), - ), - ( - FermiWord({(0, 0): "-", (1, 1): "+"}), - # obtained with openfermion using: jordan_wigner(FermionOperator('0 1^')) - # reformatted the original openfermion output: - # (-0.25+0j) [X0 X1] + - # 0.25j [X0 Y1] + - # -0.25j [Y0 X1] + - # (-0.25+0j) [Y0 Y1] - ( - [(-0.25 + 0j), 0.25j, -0.25j, (-0.25 + 0j)], - [ - qml.PauliX(0) @ qml.PauliX(1), - qml.PauliX(0) @ qml.PauliY(1), - qml.PauliY(0) @ qml.PauliX(1), - qml.PauliY(0) @ qml.PauliY(1), - ], - ), - ), - ( - FermiWord({(0, 1): "-", (1, 0): "+"}), - # obtained with openfermion using: jordan_wigner(FermionOperator('1 0^')) - # reformatted the original openfermion output: - # (-0.25+0j) [X0 X1] + - # -0.25j [X0 Y1] + - # 0.25j [Y0 X1] + - # (-0.25+0j) [Y0 Y1] - ( - [(-0.25 + 0j), -0.25j, 0.25j, (-0.25 + 0j)], - [ - qml.PauliX(0) @ qml.PauliX(1), - qml.PauliX(0) @ qml.PauliY(1), - qml.PauliY(0) @ qml.PauliX(1), - qml.PauliY(0) @ qml.PauliY(1), - ], - ), - ), - ( - FermiWord({(0, 3): "+", (1, 0): "-"}), - # obtained with openfermion using: jordan_wigner(FermionOperator('3^ 0', 1)) - # reformatted the original openfermion output - ( - [(0.25 + 0j), -0.25j, 0.25j, (0.25 + 0j)], - [ - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliY(3), - ], - ), - ), - ( - FermiWord({(0, 0): "-", (1, 3): "+"}), - # obtained with openfermion using: jordan_wigner(FermionOperator('0 3^')) - # reformatted the original openfermion output - ( - [(-0.25 + 0j), 0.25j, -0.25j, (-0.25 + 0j)], - [ - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliY(3), - ], - ), - ), - ( - FermiWord({(0, 3): "-", (1, 0): "+"}), - # obtained with openfermion using: jordan_wigner(FermionOperator('3 0^')) - # reformatted the original openfermion output - ( - [(-0.25 + 0j), -0.25j, 0.25j, (-0.25 + 0j)], - [ - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliY(3), - ], - ), - ), - ( - FermiWord({(0, 1): "+", (1, 4): "-"}), - # obtained with openfermion using: jordan_wigner(FermionOperator('1^ 4', 1)) - # reformatted the original openfermion output - ( - [(0.25 + 0j), 0.25j, -0.25j, (0.25 + 0j)], - [ - qml.PauliX(1) @ qml.PauliZ(2) @ qml.PauliZ(3) @ qml.PauliX(4), - qml.PauliX(1) @ qml.PauliZ(2) @ qml.PauliZ(3) @ qml.PauliY(4), - qml.PauliY(1) @ qml.PauliZ(2) @ qml.PauliZ(3) @ qml.PauliX(4), - qml.PauliY(1) @ qml.PauliZ(2) @ qml.PauliZ(3) @ qml.PauliY(4), - ], - ), - ), - ( - FermiWord({(0, 1): "+", (1, 1): "+", (2, 1): "-", (3, 1): "-"}), # [1, 1, 1, 1], - # obtained with openfermion using: jordan_wigner(FermionOperator('1^ 1^ 1 1', 1)) - ([0], [qml.Identity(1)]), - ), - ( - FermiWord({(0, 3): "+", (1, 1): "+", (2, 3): "-", (3, 1): "-"}), # [3, 1, 3, 1], - # obtained with openfermion using: jordan_wigner(FermionOperator('3^ 1^ 3 1', 1)) - # reformatted the original openfermion output - ( - [(-0.25 + 0j), (0.25 + 0j), (-0.25 + 0j), (0.25 + 0j)], - [qml.Identity(0), qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(3), qml.PauliZ(3)], - ), - ), - ( - FermiWord({(0, 3): "+", (1, 1): "-", (2, 3): "+", (3, 1): "-"}), # [3, 1, 3, 1], - # obtained with openfermion using: jordan_wigner(FermionOperator('3^ 1 3^ 1', 1)) - ([0], [qml.Identity(1)]), - ), - ( - FermiWord({(0, 1): "+", (1, 0): "-", (2, 1): "+", (3, 1): "-"}), # [1, 0, 1, 1], - # obtained with openfermion using: jordan_wigner(FermionOperator('1^ 0 1^ 1', 1)) - ([0], [qml.Identity(0)]), - ), - ( - FermiWord({(0, 1): "+", (1, 1): "-", (2, 0): "+", (3, 0): "-"}), # [1, 1, 0, 0], - # obtained with openfermion using: jordan_wigner(FermionOperator('1^ 1 0^ 0', 1)) - ( - [(0.25 + 0j), (-0.25 + 0j), (0.25 + 0j), (-0.25 + 0j)], - [qml.Identity(0), qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1)], - ), - ), - ( - FermiWord({(0, 5): "+", (1, 5): "-", (2, 5): "+", (3, 5): "-"}), # [5, 5, 5, 5], - # obtained with openfermion using: jordan_wigner(FermionOperator('5^ 5 5^ 5', 1)) - ( - [(0.5 + 0j), (-0.5 + 0j)], - [qml.Identity(0), qml.PauliZ(5)], - ), - ), - ( - FermiWord({(0, 3): "+", (1, 3): "-", (2, 3): "+", (3, 1): "-"}), - # obtained with openfermion using: jordan_wigner(FermionOperator('3^ 3 3^ 1', 1)) - ( - [(0.25 + 0j), (-0.25j), (0.25j), (0.25 + 0j)], - [ - qml.PauliX(1) @ qml.PauliZ(2) @ qml.PauliX(3), - qml.PauliX(1) @ qml.PauliZ(2) @ qml.PauliY(3), - qml.PauliY(1) @ qml.PauliZ(2) @ qml.PauliX(3), - qml.PauliY(1) @ qml.PauliZ(2) @ qml.PauliY(3), - ], - ), - ), - ] - - # can't be tested with conversion to operators yet, because the resulting operators - # are too complicated for qml.equal to successfully compare - FERMI_WORDS_AND_OPS_EXTENDED_LEGACY = [ - ( - FermiWord({(0, 3): "+", (1, 0): "-", (2, 2): "+", (3, 1): "-"}), - # obtained with openfermion using: jordan_wigner(FermionOperator('3^ 0 2^ 1', 1)) - ( - [ - (-0.0625 + 0j), - 0.0625j, - 0.0625j, - (0.0625 + 0j), - -0.0625j, - (-0.0625 + 0j), - (-0.0625 + 0j), - 0.0625j, - -0.0625j, - (-0.0625 + 0j), - (-0.0625 + 0j), - 0.0625j, - (0.0625 + 0j), - -0.0625j, - -0.0625j, - (-0.0625 + 0j), - ], - [ - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliY(3), - ], - ), - ), - ( - FermiWord({(0, 3): "-", (1, 0): "+", (2, 2): "-", (3, 1): "+"}), - # obtained with openfermion using: jordan_wigner(FermionOperator('3 0^ 2 1^')) - ( - [ - (-0.0625 + 0j), - -0.0625j, - -0.0625j, - (0.0625 + 0j), - 0.0625j, - (-0.0625 + 0j), - (-0.0625 + 0j), - -0.0625j, - 0.0625j, - (-0.0625 + 0j), - (-0.0625 + 0j), - -0.0625j, - (0.0625 + 0j), - 0.0625j, - 0.0625j, - (-0.0625 + 0j), - ], - [ - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliY(3), - ], - ), - ), - ( - FermiWord({(0, 3): "-", (1, 0): "+", (2, 2): "-", (3, 1): "+"}), - # obtained with openfermion using: jordan_wigner(FermionOperator('3 0^ 2 1^')) - ( - [ - (-0.0625 + 0j), - -0.0625j, - -0.0625j, - (0.0625 + 0j), - 0.0625j, - (-0.0625 + 0j), - (-0.0625 + 0j), - -0.0625j, - 0.0625j, - (-0.0625 + 0j), - (-0.0625 + 0j), - -0.0625j, - (0.0625 + 0j), - 0.0625j, - 0.0625j, - (-0.0625 + 0j), - ], - [ - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliY(3), - ], - ), - ), - ( - FermiWord({(0, 0): "-", (1, 0): "+", (2, 2): "+", (3, 1): "-"}), - # obtained with openfermion using: jordan_wigner(FermionOperator('0 0^ 2^ 1')) - ( - [ - (0.125 + 0j), - -0.125j, - 0.125j, - (0.125 + 0j), - (0.125 + 0j), - -0.125j, - 0.125j, - (0.125 + 0j), - ], - [ - qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliX(2), - qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliY(2), - qml.PauliZ(0) @ qml.PauliY(1) @ qml.PauliX(2), - qml.PauliZ(0) @ qml.PauliY(1) @ qml.PauliY(2), - qml.PauliX(1) @ qml.PauliX(2), - qml.PauliX(1) @ qml.PauliY(2), - qml.PauliY(1) @ qml.PauliX(2), - qml.PauliY(1) @ qml.PauliY(2), - ], - ), - ), - ] - - -@pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("fermionic_op, result", FERMI_WORDS_AND_OPS + FERMI_WORDS_AND_OPS_EXTENDED) def test_jordan_wigner_fermi_word_ps(fermionic_op, result): """Test that the jordan_wigner function returns the correct qubit operator.""" @@ -693,24 +363,6 @@ def test_jordan_wigner_fermi_word_ps(fermionic_op, result): assert qubit_op == expected_op -@pytest.mark.usefixtures("legacy_opmath_only") -@pytest.mark.parametrize( - "fermionic_op, result", FERMI_WORDS_AND_OPS_LEGACY + FERMI_WORDS_AND_OPS_EXTENDED_LEGACY -) -def test_jordan_wigner_fermi_word_ps_legacy(fermionic_op, result): - """Test that the jordan_wigner function returns the correct qubit operator.""" - # convert FermiWord to PauliSentence and simplify - qubit_op = jordan_wigner(fermionic_op, ps=True) - qubit_op.simplify() - - # get expected op as PauliSentence and simplify - expected_op = pauli_sentence(qml.Hamiltonian(result[0], result[1])) - expected_op.simplify() - - assert qubit_op == expected_op - - -@pytest.mark.usefixtures("new_opmath_only") # TODO: if qml.equal is extended to compare layers of nested ops, also test with FERMI_WORDS_AND_OPS_EXTENDED @pytest.mark.parametrize("fermionic_op, result", FERMI_WORDS_AND_OPS) def test_jordan_wigner_fermi_word_operation(fermionic_op, result): @@ -724,20 +376,6 @@ def test_jordan_wigner_fermi_word_operation(fermionic_op, result): qml.assert_equal(qubit_op.simplify(), expected_op.simplify()) -@pytest.mark.usefixtures("legacy_opmath_only") -# TODO: if qml.equal is extended to compare layers of nested ops, also test with FERMI_WORDS_AND_OPS_EXTENDED -@pytest.mark.parametrize("fermionic_op, result", FERMI_WORDS_AND_OPS_LEGACY) -def test_jordan_wigner_fermi_word_operation_legacy(fermionic_op, result): - wires = fermionic_op.wires or [0] - - qubit_op = jordan_wigner(fermionic_op) - - expected_op = pauli_sentence(qml.Hamiltonian(result[0], result[1])) - expected_op = expected_op.operation(wires) - - qml.assert_equal(qubit_op.simplify(), expected_op.simplify()) - - def test_jordan_wigner_for_identity(): """Test that the jordan_wigner function returns the correct qubit operator for Identity.""" qml.assert_equal(jordan_wigner(FermiWord({})), qml.Identity(0)) diff --git a/tests/fermi/test_parity_mapping.py b/tests/fermi/test_parity_mapping.py index 537be708afe..87ac986bcb7 100644 --- a/tests/fermi/test_parity_mapping.py +++ b/tests/fermi/test_parity_mapping.py @@ -437,422 +437,7 @@ def test_error_is_raised_for_dimension_mismatch(): ), ] -with qml.operation.disable_new_opmath_cm(warn=False): - FERMI_WORDS_AND_OPS_LEGACY = [ - ( - FermiWord({(0, 0): "+"}), - 1, - # trivial case of a creation operator with one qubit, 0^ -> (X_0 - iY_0) / 2 : Same as Jordan-Wigner - ([0.5, -0.5j], [qml.PauliX(0), qml.PauliY(0)]), - ), - ( - FermiWord({(0, 0): "-"}), - 1, - # trivial case of an annihilation operator with one qubit , 0 -> (X_0 + iY_0) / 2 : Same as Jordan-Wigner - ([(0.5 + 0j), (0.0 + 0.5j)], [qml.PauliX(0), qml.PauliY(0)]), - ), - ( - FermiWord({(0, 0): "+"}), - 2, - # trivial case of a creation operator with two qubits, 0^ -> (X_0 @ X_1 - iY_0 @ X_1) / 2 - ([0.5, -0.5j], [qml.PauliX(0) @ qml.PauliX(1), qml.PauliY(0) @ qml.PauliX(1)]), - ), - ( - FermiWord({(0, 0): "-"}), - 2, - # trivial case of an annihilation operator with two qubits , 0 -> (X_0 @ X_1 + iY_0 @ X_1) / 2 - ( - [(0.5 + 0j), (0.0 + 0.5j)], - [qml.PauliX(0) @ qml.PauliX(1), qml.PauliY(0) @ qml.PauliX(1)], - ), - ), - ( - FermiWord({(0, 0): "+", (1, 0): "-"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('0^ 0'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output: (0.5+0j) [] + (-0.5+0j) [Z0] - ([(0.5 + 0j), (-0.5 + 0j)], [qml.Identity(0), qml.PauliZ(0)]), - ), - ( - FermiWord({(0, 0): "-", (1, 0): "+"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('0 0^'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output: (0.5+0j) [] + (0.5+0j) [Z0] - ([(0.5 + 0j), (0.5 + 0j)], [qml.Identity(0), qml.PauliZ(0)]), - ), - ( - FermiWord({(0, 0): "-", (1, 1): "+"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('0 1^'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output: - # (-0.25+0j) [X0] + - # 0.25 [X0 Z1] + - # (-0-0.25j) [Y0] + - # 0.25j [Y0 Z1] - ( - [(-0.25 + 0j), 0.25, (-0 - 0.25j), (0.25j)], - [ - qml.PauliX(0), - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliY(0), - qml.PauliY(0) @ qml.PauliZ(1), - ], - ), - ), - ( - FermiWord({(0, 3): "+", (1, 0): "-"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('3^ 0'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output - # -0.25 [X0 X1 X2 Z3] + - # 0.25j [X0 X1 Y2] + - # (-0-0.25j) [Y0 X1 X2 Z3] + - # -0.25 [Y0 X1 Y2] - ( - [(-0.25 + 0j), 0.25j, (-0 - 0.25j), -0.25], - [ - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliZ(3), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliY(2), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliZ(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliY(2), - ], - ), - ), - ( - FermiWord({(0, 5): "+", (1, 5): "-", (2, 5): "+", (3, 5): "-"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('5^ 5 5^ 5'), parity_code(n_qubits)) with 6 qubits - ( - [(0.5 + 0j), (-0.5 + 0j)], - [qml.Identity(0), qml.PauliZ(4) @ qml.PauliZ(5)], - ), - ), - ( - FermiWord({(0, 3): "+", (1, 3): "-", (2, 3): "+", (3, 1): "-"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('3^ 3 3^ 1'), parity_code(n_qubits)) with 6 qubits - # -0.25 [Z0 X1 X2 Z3] + - # 0.25j [Z0 X1 Y2] + - # (-0-0.25j) [Y1 X2 Z3] + - # -0.25 [Y1 Y2] - ( - [-0.25, 0.25j, -0.25j, -0.25], - [ - qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliZ(3), - qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliY(2), - qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliZ(3), - qml.PauliY(1) @ qml.PauliY(2), - ], - ), - ), - ( - FermiWord({(0, 1): "+", (1, 0): "-", (2, 1): "+", (3, 1): "-"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('1^ 0 1^ 1'), parity_code(n_qubits)) with 6 qubits - ([0], [qml.Identity(0)]), - ), - ] - - FERMI_OPS_COMPLEX_LEGACY = [ - ( - FermiWord({(0, 2): "-", (1, 0): "+", (2, 3): "+"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('2 0^ 3^'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output - # (-0-0.125j) [X0 X1 Z2 Y3] + - # 0.125 [X0 X1 X3] + - # 0.125j [X0 Y1 Z2 X3] + - # 0.125 [X0 Y1 Y3] + - # -0.125 [Y0 X1 Z2 Y3] + - # (-0-0.125j) [Y0 X1 X3] + - # 0.125 [Y0 Y1 Z2 X3] + - # (-0-0.125j) [Y0 Y1 Y3] - ( - [(-0 - 0.125j), 0.125, 0.125j, 0.125, -0.125, -0.125j, 0.125, -0.125j], - [ - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliZ(2) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliZ(2) @ qml.PauliX(3), - qml.PauliX(0) @ qml.PauliY(1) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliZ(2) @ qml.PauliY(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliZ(2) @ qml.PauliX(3), - qml.PauliY(0) @ qml.PauliY(1) @ qml.PauliY(3), - ], - ), - ), - ( - FermiWord({(0, 0): "-", (1, 3): "+"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('0 3^'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output - # 0.25 [X0 X1 X2 Z3] + - # (-0-0.25j) [X0 X1 Y2] + - # 0.25j [Y0 X1 X2 Z3] + - # 0.25 [Y0 X1 Y2] - ( - [0.25, -0.25j, 0.25j, 0.25], - [ - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliZ(3), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliY(2), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliZ(3), - qml.PauliY(0) @ qml.PauliX(1) @ qml.PauliY(2), - ], - ), - ), - ( - FermiWord({(0, 3): "+", (1, 1): "+", (2, 3): "-", (3, 1): "-"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('3^ 1^ 3 1'), parity_code(n_qubits)) with 6 qubits - # -0.25 [] + - # 0.25 [Z0 Z1] + - # -0.25 [Z0 Z1 Z2 Z3] + - # 0.25 [Z2 Z3] - # reformatted the original openfermion output - ( - [(-0.25 + 0j), (0.25 + 0j), (-0.25 + 0j), (0.25 + 0j)], - [ - qml.Identity(0), - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliZ(3), - qml.PauliZ(2) @ qml.PauliZ(3), - ], - ), - ), - ( - FermiWord({(0, 1): "+", (1, 4): "-", (2, 3): "-", (3, 4): "+"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('1^ 4 3 4^'), parity_code(n_qubits)) with 6 qubits - # reformatted the original openfermion output - # 0.125 [Z0 X1 X2 Z3] + - # 0.125 [Z0 X1 X2 Z4] + - # 0.125j [Z0 X1 Y2] + - # 0.125j [Z0 X1 Y2 Z3 Z4] + - # (-0-0.125j) [Y1 X2 Z3] + - # (-0-0.125j) [Y1 X2 Z4] + - # 0.125 [Y1 Y2] + - # 0.125 [Y1 Y2 Z3 Z4] - ( - [0.125, 0.125, 0.125j, 0.125j, -0.125j, -0.125j, 0.125, 0.125], - [ - qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliZ(3), - qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliX(2) @ qml.PauliZ(4), - qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliY(2), - qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliZ(4), - qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliZ(3), - qml.PauliY(1) @ qml.PauliX(2) @ qml.PauliZ(4), - qml.PauliY(1) @ qml.PauliY(2), - qml.PauliY(1) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliZ(4), - ], - ), - ), - ( - FermiWord({(0, 3): "+", (1, 1): "-", (2, 3): "+", (3, 1): "-"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('3^ 1 3^ 1'), parity_code(n_qubits)) with 6 qubits - ([0], [qml.Identity(3)]), - ), - ( - FermiWord({(0, 1): "+", (1, 0): "+", (2, 4): "-", (3, 5): "-"}), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('1^ 0^ 4 5^'), parity_code(n_qubits)) with 6 qubits - # 0.0625 [X0 Z1 Z3 X4 Z5] + - # 0.0625j [X0 Z1 Z3 Y4] + - # 0.0625 [X0 Z1 X4] + - # 0.0625j [X0 Z1 Y4 Z5] + - # 0.0625 [X0 Z3 X4 Z5] + - # 0.0625j [X0 Z3 Y4] + - # 0.0625 [X0 X4] + - # 0.0625j [X0 Y4 Z5] + - # (-0-0.0625j) [Y0 Z1 Z3 X4 Z5] + - # 0.0625 [Y0 Z1 Z3 Y4] + - # (-0-0.0625j) [Y0 Z1 X4] + - # 0.0625 [Y0 Z1 Y4 Z5] + - # (-0-0.0625j) [Y0 Z3 X4 Z5] + - # 0.0625 [Y0 Z3 Y4] + - # (-0-0.0625j) [Y0 X4] + - # 0.0625 [Y0 Y4 Z5]) - ( - [ - 0.0625, - 0.0625j, - 0.0625, - 0.0625j, - 0.0625, - 0.0625j, - 0.0625, - 0.0625j, - -0.0625j, - 0.0625, - -0.0625j, - 0.0625, - -0.0625j, - 0.0625, - -0.0625j, - 0.0625, - ], - [ - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(3) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(3) @ qml.PauliY(4), - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliX(4), - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliX(0) @ qml.PauliZ(3) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliX(0) @ qml.PauliZ(3) @ qml.PauliY(4), - qml.PauliX(0) @ qml.PauliX(4), - qml.PauliX(0) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliZ(3) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliZ(3) @ qml.PauliY(4), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliX(4), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliY(0) @ qml.PauliZ(3) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliY(0) @ qml.PauliZ(3) @ qml.PauliY(4), - qml.PauliY(0) @ qml.PauliX(4), - qml.PauliY(0) @ qml.PauliY(4) @ qml.PauliZ(5), - ], - ), - ), - ( - FermiWord({(0, 1): "-", (1, 0): "+"}), - 4, - # obtained with openfermion using: binary_code_transform(FermionOperator('1 0^'), parity_code(n_qubits)) for n_qubits = 4 - # reformatted the original openfermion output: - # -0.25 [X0] + - # 0.25 [X0 Z1] + - # 0.25j [Y0] + - # (-0-0.25j) [Y0 Z1] - ( - [(-0.25 + 0j), 0.25, 0.25j, (-0 - 0.25j)], - [ - qml.PauliX(0), - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliY(0), - qml.PauliY(0) @ qml.PauliZ(1), - ], - ), - ), - ( - FermiWord( - {(0, 1): "+", (1, 1): "-", (2, 3): "+", (3, 4): "-", (4, 2): "+", (5, 5): "-"} - ), - 6, - # obtained with openfermion using: binary_code_transform(FermionOperator('1^ 1 3^ 4 2^ 5'), parity_code(n_qubits)) with 6 qubits - # 0.03125 [Z0 Z1 X2 Z3 X4 Z5] + - # 0.03125j [Z0 Z1 X2 Z3 Y4] + - # 0.03125 [Z0 Z1 X2 X4] + - # 0.03125j [Z0 Z1 X2 Y4 Z5] + - # (-0-0.03125j) [Z0 Z1 Y2 Z3 X4] + - # 0.03125 [Z0 Z1 Y2 Z3 Y4 Z5] + - # (-0-0.03125j) [Z0 Z1 Y2 X4 Z5] + - # 0.03125 [Z0 Z1 Y2 Y4] + - # 0.03125 [Z0 X2 Z3 X4] + - # 0.03125j [Z0 X2 Z3 Y4 Z5] + - # 0.03125 [Z0 X2 X4 Z5] + - # 0.03125j [Z0 X2 Y4] + - # (-0-0.03125j) [Z0 Y2 Z3 X4 Z5] + - # 0.03125 [Z0 Y2 Z3 Y4] + - # (-0-0.03125j) [Z0 Y2 X4] + - # 0.03125 [Z0 Y2 Y4 Z5] + - # -0.03125 [Z1 X2 Z3 X4] + - # (-0-0.03125j) [Z1 X2 Z3 Y4 Z5] + - # -0.03125 [Z1 X2 X4 Z5] + - # (-0-0.03125j) [Z1 X2 Y4] + - # 0.03125j [Z1 Y2 Z3 X4 Z5] + - # -0.03125 [Z1 Y2 Z3 Y4] + - # 0.03125j [Z1 Y2 X4] + - # -0.03125 [Z1 Y2 Y4 Z5] + - # -0.03125 [X2 Z3 X4 Z5] + - # (-0-0.03125j) [X2 Z3 Y4] + - # -0.03125 [X2 X4] + - # (-0-0.03125j) [X2 Y4 Z5] + - # 0.03125j [Y2 Z3 X4] + - # -0.03125 [Y2 Z3 Y4 Z5] + - # 0.03125j [Y2 X4 Z5] + - # -0.03125 [Y2 Y4] - ( - [ - 0.03125, - 0.03125j, - 0.03125, - 0.03125j, - -0.03125j, - 0.03125, - -0.03125j, - 0.03125, - 0.03125, - 0.03125j, - 0.03125, - 0.03125j, - -0.03125j, - 0.03125, - -0.03125j, - 0.03125, - -0.03125, - -0.03125j, - -0.03125, - -0.03125j, - 0.03125j, - -0.03125, - 0.03125j, - -0.03125, - -0.03125, - -0.03125j, - -0.03125, - -0.03125j, - 0.03125j, - -0.03125, - 0.03125j, - -0.03125, - ], - [ - qml.PauliZ(0) - @ qml.PauliZ(1) - @ qml.PauliX(2) - @ qml.PauliZ(3) - @ qml.PauliX(4) - @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliY(4), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliX(4), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliX(4), - qml.PauliZ(0) - @ qml.PauliZ(1) - @ qml.PauliY(2) - @ qml.PauliZ(3) - @ qml.PauliY(4) - @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliY(4), - qml.PauliZ(0) @ qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliX(4), - qml.PauliZ(0) @ qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliX(2) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliX(2) @ qml.PauliY(4), - qml.PauliZ(0) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliZ(0) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliY(4), - qml.PauliZ(0) @ qml.PauliY(2) @ qml.PauliX(4), - qml.PauliZ(0) @ qml.PauliY(2) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliX(4), - qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliZ(1) @ qml.PauliX(2) @ qml.PauliY(4), - qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliY(4), - qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliX(4), - qml.PauliZ(1) @ qml.PauliY(2) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliX(2) @ qml.PauliZ(3) @ qml.PauliY(4), - qml.PauliX(2) @ qml.PauliX(4), - qml.PauliX(2) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliX(4), - qml.PauliY(2) @ qml.PauliZ(3) @ qml.PauliY(4) @ qml.PauliZ(5), - qml.PauliY(2) @ qml.PauliX(4) @ qml.PauliZ(5), - qml.PauliY(2) @ qml.PauliY(4), - ], - ), - ), - ] - -@pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("fermionic_op, n_qubits, result", FERMI_WORDS_AND_OPS + FERMI_OPS_COMPLEX) def test_parity_transform_fermi_word_ps(fermionic_op, n_qubits, result): """Test that the parity_transform function returns the correct qubit operator.""" @@ -867,24 +452,6 @@ def test_parity_transform_fermi_word_ps(fermionic_op, n_qubits, result): assert qubit_op == expected_op -@pytest.mark.usefixtures("legacy_opmath_only") -@pytest.mark.parametrize( - "fermionic_op, n_qubits, result", FERMI_WORDS_AND_OPS_LEGACY + FERMI_OPS_COMPLEX_LEGACY -) -def test_parity_transform_fermi_word_ps_legacy(fermionic_op, n_qubits, result): - """Test that the parity_transform function returns the correct qubit operator.""" - # convert FermiWord to PauliSentence and simplify - qubit_op = parity_transform(fermionic_op, n_qubits, ps=True) - qubit_op.simplify() - - # get expected op as PauliSentence and simplify - expected_op = pauli_sentence(qml.Hamiltonian(result[0], result[1])) - expected_op.simplify() - - assert qubit_op == expected_op - - -@pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("fermionic_op, n_qubits, result", FERMI_WORDS_AND_OPS) def test_parity_transform_fermi_word_operation(fermionic_op, n_qubits, result): wires = fermionic_op.wires or [0] @@ -897,19 +464,6 @@ def test_parity_transform_fermi_word_operation(fermionic_op, n_qubits, result): qml.assert_equal(qubit_op.simplify(), expected_op.simplify()) -@pytest.mark.usefixtures("legacy_opmath_only") -@pytest.mark.parametrize("fermionic_op, n_qubits, result", FERMI_WORDS_AND_OPS_LEGACY) -def test_parity_transform_fermi_word_operation_legacy(fermionic_op, n_qubits, result): - wires = fermionic_op.wires or [0] - - qubit_op = parity_transform(fermionic_op, n_qubits) - - expected_op = pauli_sentence(qml.Hamiltonian(result[0], result[1])) - expected_op = expected_op.operation(wires) - - qml.assert_equal(qubit_op.simplify(), expected_op.simplify()) - - def test_parity_transform_for_identity(): """Test that the parity_transform function returns the correct qubit operator for Identity.""" qml.assert_equal(parity_transform(FermiWord({}), 2), qml.Identity(0)) diff --git a/tests/gradients/core/test_adjoint_diff.py b/tests/gradients/core/test_adjoint_diff.py index ec907a5827f..bf7aa2e606b 100644 --- a/tests/gradients/core/test_adjoint_diff.py +++ b/tests/gradients/core/test_adjoint_diff.py @@ -55,25 +55,6 @@ def test_finite_shots_warns(self): ): dev.adjoint_jacobian(tape) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_hamiltonian_error_legacy_opmath(self, dev): - """Test that error is raised for qml.Hamiltonian""" - - with qml.queuing.AnnotatedQueue() as q: - qml.expval( - qml.Hamiltonian( - [np.array(-0.05), np.array(0.17)], - [qml.PauliX(0), qml.PauliZ(0)], - ) - ) - - tape = qml.tape.QuantumScript.from_queue(q) - with pytest.raises( - qml.QuantumFunctionError, - match="Adjoint differentiation method does not support Hamiltonian observables", - ): - dev.adjoint_jacobian(tape) - def test_linear_combination_adjoint_warning(self, dev): """Test that error is raised for qml.Hamiltonian""" diff --git a/tests/gradients/core/test_metric_tensor.py b/tests/gradients/core/test_metric_tensor.py index 361a9ec156a..387a7f15d63 100644 --- a/tests/gradients/core/test_metric_tensor.py +++ b/tests/gradients/core/test_metric_tensor.py @@ -1506,7 +1506,7 @@ def circuit1(x, z): qml.metric_tensor(circuit1, approx=None, allow_nonunitary=allow_nonunitary)(x, z) -def test_no_error_missing_aux_wire_not_used(recwarn): +def test_no_error_missing_aux_wire_not_used(): """Tests that a no error is raised if the requested (or default, if not given) auxiliary wire for the Hadamard test is missing but it is not used, either because ``approx`` is used or because there only is a diagonal contribution.""" @@ -1541,9 +1541,6 @@ def circuit_multi_block(x, z): qml.metric_tensor(circuit_multi_block, approx="block-diag")(x, z) qml.metric_tensor(circuit_multi_block, approx="block-diag", aux_wire="aux_wire")(x, z) - if qml.operation.active_new_opmath(): - assert len(recwarn) == 0 - def test_raises_circuit_that_uses_missing_wire(): """Test that an error in the original circuit is reraised properly and not caught. This avoids diff --git a/tests/gradients/core/test_pulse_gradient.py b/tests/gradients/core/test_pulse_gradient.py index 164c047c776..f8f09bf0a45 100644 --- a/tests/gradients/core/test_pulse_gradient.py +++ b/tests/gradients/core/test_pulse_gradient.py @@ -16,7 +16,6 @@ """ import copy -import warnings import numpy as np import pytest @@ -182,33 +181,6 @@ def test_with_general_ob(self, ham, params, time, ob, seed): # Check that the inserted exponential is correct qml.assert_equal(qml.exp(qml.dot([-1j * exp_shift], [ob])), _ops[1]) - @pytest.mark.usefixtures("legacy_opmath_only") # this is only an issue with legacy Hamiltonian - def test_warnings_legacy_opmath(self): - """Test that a warning is raised for computing eigenvalues of a Hamiltonian - for more than four wires but not for fewer wires.""" - import jax - - jax.config.update("jax_enable_x64", True) - ham = qml.pulse.constant * qml.PauliY(0) - op = qml.evolve(ham)([0.3], 2.0) - ob = qml.Hamiltonian( - [0.4, 0.2], [qml.operation.Tensor(*[qml.PauliY(i) for i in range(5)]), qml.PauliX(0)] - ) - with pytest.warns(UserWarning, match="the eigenvalues will be computed numerically"): - _split_evol_ops(op, ob, tau=0.4) - - ob = qml.Hamiltonian( - [0.4, 0.2], [qml.operation.Tensor(*[qml.PauliY(i) for i in range(4)]), qml.PauliX(0)] - ) - with warnings.catch_warnings(): - warnings.filterwarnings("error") - warnings.filterwarnings( - "ignore", - "qml.operation.Tensor uses the old approach", - qml.PennyLaneDeprecationWarning, - ) - _split_evol_ops(op, ob, tau=0.4) - @pytest.mark.jax class TestSplitEvolTapes: diff --git a/tests/gradients/parameter_shift/test_parameter_shift.py b/tests/gradients/parameter_shift/test_parameter_shift.py index 57ac1741bfa..3e58192b660 100644 --- a/tests/gradients/parameter_shift/test_parameter_shift.py +++ b/tests/gradients/parameter_shift/test_parameter_shift.py @@ -1514,7 +1514,6 @@ def test_gradients_agree_finite_differences(self, tol): assert np.allclose(grad_A, grad_F1, atol=tol, rtol=0) assert np.allclose(grad_A, grad_F2, atol=tol, rtol=0) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_variance_gradients_agree_finite_differences(self, tol): """Tests that the variance parameter-shift rule agrees with the first and second order finite differences""" @@ -2418,7 +2417,6 @@ def test_recycling_unshifted_tape_result(self): # + 2 operations x 2 shifted positions + 1 unshifted term <-- assert len(tapes) == (2 * 2 + 1) + (2 * 2 + 1) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("state", [[1], [0, 1]]) # Basis state and state vector def test_projector_variance(self, state, tol): """Test that the variance of a projector is correctly returned""" @@ -3321,7 +3319,6 @@ def cost_fn(x): assert np.allclose(res, expected, atol=tol, rtol=0) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("broadcast", [True, False]) class TestHamiltonianExpvalGradients: """Test that tapes ending with expval(H) can be diff --git a/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py b/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py index b1f6e86b9b7..8dc982f6b39 100644 --- a/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py +++ b/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py @@ -2007,7 +2007,8 @@ def test_multi_measure_no_warning(self, broadcast): # TODO: allow broadcast=True -@pytest.mark.usefixtures("use_legacy_and_new_opmath") + + @pytest.mark.parametrize("broadcast", [False]) class TestHamiltonianExpvalGradients: """Test that tapes ending with expval(H) can be diff --git a/tests/interfaces/test_autograd.py b/tests/interfaces/test_autograd.py index 0e0bb8f3f09..56dad40a4c7 100644 --- a/tests/interfaces/test_autograd.py +++ b/tests/interfaces/test_autograd.py @@ -770,7 +770,6 @@ def cost_fn(x): @pytest.mark.parametrize("execute_kwargs, shots, device_name", test_matrix) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") class TestHamiltonianWorkflows: """Test that tapes ending with expectations of Hamiltonians provide correct results and gradients""" @@ -784,13 +783,11 @@ def cost_fn(self, execute_kwargs, shots, device_name, seed): def _cost_fn(weights, coeffs1, coeffs2): obs1 = [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1), qml.PauliY(0)] H1 = qml.Hamiltonian(coeffs1, obs1) - if qml.operation.active_new_opmath(): - H1 = qml.pauli.pauli_sentence(H1).operation() + H1 = qml.pauli.pauli_sentence(H1).operation() obs2 = [qml.PauliZ(0)] H2 = qml.Hamiltonian(coeffs2, obs2) - if qml.operation.active_new_opmath(): - H2 = qml.pauli.pauli_sentence(H2).operation() + H2 = qml.pauli.pauli_sentence(H2).operation() with qml.queuing.AnnotatedQueue() as q: qml.RX(weights[0], wires=0) @@ -832,12 +829,9 @@ def cost_fn_jacobian(weights, coeffs1, coeffs2): ] ) - def test_multiple_hamiltonians_not_trainable(self, execute_kwargs, cost_fn, shots): + def test_multiple_hamiltonians_not_trainable(self, cost_fn, shots): """Test hamiltonian with no trainable parameters.""" - if execute_kwargs["diff_method"] == "adjoint" and not qml.operation.active_new_opmath(): - pytest.skip("adjoint differentiation does not support hamiltonians.") - coeffs1 = pnp.array([0.1, 0.2, 0.3], requires_grad=False) coeffs2 = pnp.array([0.7], requires_grad=False) weights = pnp.array([0.4, 0.5], requires_grad=True) @@ -858,12 +852,11 @@ def test_multiple_hamiltonians_not_trainable(self, execute_kwargs, cost_fn, shot else: assert np.allclose(res, expected, atol=atol_for_shots(shots), rtol=0) + @pytest.mark.xfail(reason="parameter shift derivatives do not yet support sums.") def test_multiple_hamiltonians_trainable(self, execute_kwargs, cost_fn, shots): """Test hamiltonian with trainable parameters.""" if execute_kwargs["diff_method"] == "adjoint": pytest.skip("trainable hamiltonians not supported with adjoint") - if qml.operation.active_new_opmath(): - pytest.skip("parameter shift derivatives do not yet support sums.") coeffs1 = pnp.array([0.1, 0.2, 0.3], requires_grad=True) coeffs2 = pnp.array([0.7], requires_grad=True) diff --git a/tests/interfaces/test_jax.py b/tests/interfaces/test_jax.py index 12a354eccb3..8222804bcfd 100644 --- a/tests/interfaces/test_jax.py +++ b/tests/interfaces/test_jax.py @@ -730,7 +730,6 @@ def cost_fn(x): @pytest.mark.parametrize("execute_kwargs, shots, device_name", test_matrix) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") class TestHamiltonianWorkflows: """Test that tapes ending with expectations of Hamiltonians provide correct results and gradients""" @@ -744,13 +743,11 @@ def cost_fn(self, execute_kwargs, shots, device_name, seed): def _cost_fn(weights, coeffs1, coeffs2): obs1 = [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1), qml.PauliY(0)] H1 = qml.Hamiltonian(coeffs1, obs1) - if qml.operation.active_new_opmath(): - H1 = qml.pauli.pauli_sentence(H1).operation() + H1 = qml.pauli.pauli_sentence(H1).operation() obs2 = [qml.PauliZ(0)] H2 = qml.Hamiltonian(coeffs2, obs2) - if qml.operation.active_new_opmath(): - H2 = qml.pauli.pauli_sentence(H2).operation() + H2 = qml.pauli.pauli_sentence(H2).operation() with qml.queuing.AnnotatedQueue() as q: qml.RX(weights[0], wires=0) @@ -795,12 +792,9 @@ def cost_fn_jacobian(weights, coeffs1, coeffs2): ] ) - def test_multiple_hamiltonians_not_trainable(self, execute_kwargs, cost_fn, shots): + def test_multiple_hamiltonians_not_trainable(self, cost_fn, shots): """Test hamiltonian with no trainable parameters.""" - if execute_kwargs["diff_method"] == "adjoint" and not qml.operation.active_new_opmath(): - pytest.skip("adjoint differentiation does not suppport hamiltonians.") - coeffs1 = jnp.array([0.1, 0.2, 0.3]) coeffs2 = jnp.array([0.7]) weights = jnp.array([0.4, 0.5]) @@ -821,12 +815,11 @@ def test_multiple_hamiltonians_not_trainable(self, execute_kwargs, cost_fn, shot else: assert np.allclose(res, expected, atol=atol_for_shots(shots), rtol=0) + @pytest.mark.xfail(reason="parameter shift derivatives do not yet support sums.") def test_multiple_hamiltonians_trainable(self, execute_kwargs, cost_fn, shots): """Test hamiltonian with trainable parameters.""" if execute_kwargs["diff_method"] == "adjoint": pytest.skip("trainable hamiltonians not supported with adjoint") - if qml.operation.active_new_opmath(): - pytest.skip("parameter shift derivatives do not yet support sums.") coeffs1 = jnp.array([0.1, 0.2, 0.3]) coeffs2 = jnp.array([0.7]) diff --git a/tests/interfaces/test_tensorflow.py b/tests/interfaces/test_tensorflow.py index edd43c75c3c..c13268dc6e8 100644 --- a/tests/interfaces/test_tensorflow.py +++ b/tests/interfaces/test_tensorflow.py @@ -729,7 +729,6 @@ def cost_fn(x): @pytest.mark.parametrize("execute_kwargs, shots, device_name", test_matrix) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") class TestHamiltonianWorkflows: """Test that tapes ending with expectations of Hamiltonians provide correct results and gradients""" @@ -743,13 +742,11 @@ def cost_fn(self, execute_kwargs, shots, device_name, seed): def _cost_fn(weights, coeffs1, coeffs2): obs1 = [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1), qml.PauliY(0)] H1 = qml.Hamiltonian(coeffs1, obs1) - if qml.operation.active_new_opmath(): - H1 = qml.pauli.pauli_sentence(H1).operation() + H1 = qml.pauli.pauli_sentence(H1).operation() obs2 = [qml.PauliZ(0)] H2 = qml.Hamiltonian(coeffs2, obs2) - if qml.operation.active_new_opmath(): - H2 = qml.pauli.pauli_sentence(H2).operation() + H2 = qml.pauli.pauli_sentence(H2).operation() with qml.queuing.AnnotatedQueue() as q: qml.RX(weights[0], wires=0) @@ -794,9 +791,6 @@ def cost_fn_jacobian(weights, coeffs1, coeffs2): def test_multiple_hamiltonians_not_trainable(self, execute_kwargs, cost_fn, shots): """Test hamiltonian with no trainable parameters.""" - if execute_kwargs["diff_method"] == "adjoint" and not qml.operation.active_new_opmath(): - pytest.skip("adjoint differentiation does not suppport hamiltonians.") - device_vjp = execute_kwargs.get("device_vjp", False) coeffs1 = tf.constant([0.1, 0.2, 0.3], dtype=tf.float64) @@ -813,12 +807,11 @@ def test_multiple_hamiltonians_not_trainable(self, execute_kwargs, cost_fn, shot expected = self.cost_fn_jacobian(weights, coeffs1, coeffs2)[:, :2] assert np.allclose(jac, expected, atol=atol_for_shots(shots), rtol=0) + @pytest.mark.xfail(reason="parameter shift derivatives do not yet support sums.") def test_multiple_hamiltonians_trainable(self, cost_fn, execute_kwargs, shots): """Test hamiltonian with trainable parameters.""" if execute_kwargs["diff_method"] == "adjoint": pytest.skip("trainable hamiltonians not supported with adjoint") - if qml.operation.active_new_opmath(): - pytest.skip("parameter shift derivatives do not yet support sums.") coeffs1 = tf.Variable([0.1, 0.2, 0.3], dtype=tf.float64) coeffs2 = tf.Variable([0.7], dtype=tf.float64) diff --git a/tests/interfaces/test_torch.py b/tests/interfaces/test_torch.py index 82814821222..a926a9ddcb6 100644 --- a/tests/interfaces/test_torch.py +++ b/tests/interfaces/test_torch.py @@ -758,7 +758,6 @@ def cost_fn(x): @pytest.mark.parametrize("execute_kwargs, shots, device_name", test_matrix) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") class TestHamiltonianWorkflows: """Test that tapes ending with expectations of Hamiltonians provide correct results and gradients""" @@ -771,13 +770,11 @@ def cost_fn(self, execute_kwargs, shots, device_name, seed): def _cost_fn(weights, coeffs1, coeffs2): obs1 = [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1), qml.PauliY(0)] H1 = qml.Hamiltonian(coeffs1, obs1) - if qml.operation.active_new_opmath(): - H1 = qml.pauli.pauli_sentence(H1).operation() + H1 = qml.pauli.pauli_sentence(H1).operation() obs2 = [qml.PauliZ(0)] H2 = qml.Hamiltonian(coeffs2, obs2) - if qml.operation.active_new_opmath(): - H2 = qml.pauli.pauli_sentence(H2).operation() + H2 = qml.pauli.pauli_sentence(H2).operation() with qml.queuing.AnnotatedQueue() as q: qml.RX(weights[0], wires=0) @@ -827,12 +824,9 @@ def cost_fn_jacobian(weights, coeffs1, coeffs2): ] ) - def test_multiple_hamiltonians_not_trainable(self, execute_kwargs, cost_fn, shots): + def test_multiple_hamiltonians_not_trainable(self, cost_fn, shots): """Test hamiltonian with no trainable parameters.""" - if execute_kwargs["diff_method"] == "adjoint" and not qml.operation.active_new_opmath(): - pytest.skip("adjoint differentiation does not suppport hamiltonians.") - coeffs1 = torch.tensor([0.1, 0.2, 0.3], requires_grad=False) coeffs2 = torch.tensor([0.7], requires_grad=False) weights = torch.tensor([0.4, 0.5], requires_grad=True) @@ -853,12 +847,11 @@ def test_multiple_hamiltonians_not_trainable(self, execute_kwargs, cost_fn, shot else: assert torch.allclose(res, expected, atol=atol_for_shots(shots), rtol=0) + @pytest.mark.xfail(reason="parameter shift derivatives do not yet support sums.") def test_multiple_hamiltonians_trainable(self, execute_kwargs, cost_fn, shots): """Test hamiltonian with trainable parameters.""" if execute_kwargs["diff_method"] == "adjoint": pytest.skip("trainable hamiltonians not supported with adjoint") - if qml.operation.active_new_opmath(): - pytest.skip("parameter shift derivatives do not yet support sums.") coeffs1 = torch.tensor([0.1, 0.2, 0.3], requires_grad=True) coeffs2 = torch.tensor([0.7], requires_grad=True) diff --git a/tests/measurements/test_classical_shadow.py b/tests/measurements/test_classical_shadow.py index 7d019093628..5290224c490 100644 --- a/tests/measurements/test_classical_shadow.py +++ b/tests/measurements/test_classical_shadow.py @@ -604,11 +604,7 @@ def test_non_pauli_error(self): """Test that an error is raised when a non-Pauli observable is passed""" circuit = hadamard_circuit(3) - legacy_msg = "Observable must be a linear combination of Pauli observables" - new_opmath_msg = "Observable must have a valid pauli representation." - msg = new_opmath_msg if qml.operation.active_new_opmath() else legacy_msg - - with pytest.raises(ValueError, match=msg): + with pytest.raises(ValueError, match="Observable must have a valid pauli representation."): circuit(qml.Hadamard(0) @ qml.Hadamard(2)) diff --git a/tests/ops/functions/conftest.py b/tests/ops/functions/conftest.py index 6f174cafb51..fd1a56566e1 100644 --- a/tests/ops/functions/conftest.py +++ b/tests/ops/functions/conftest.py @@ -16,14 +16,13 @@ Generates parametrizations of operators to test in test_assert_valid.py. """ -import warnings from inspect import getmembers, isclass import numpy as np import pytest import pennylane as qml -from pennylane.operation import Channel, Observable, Operation, Operator, Tensor +from pennylane.operation import Channel, Observable, Operation, Operator from pennylane.ops.op_math.adjoint import Adjoint, AdjointObs, AdjointOperation, AdjointOpObs from pennylane.ops.op_math.pow import PowObs, PowOperation, PowOpObs @@ -52,7 +51,6 @@ (qml.BlockEncode([[0.1, 0.2], [0.3, 0.4]], wires=[0, 1]), {"skip_differentiation": True}), (qml.adjoint(qml.PauliX(0)), {}), (qml.adjoint(qml.RX(1.1, 0)), {}), - (Tensor(qml.PauliX(0), qml.PauliX(1)), {}), (qml.ops.LinearCombination([1.1, 2.2], [qml.PauliX(0), qml.PauliZ(0)]), {}), (qml.s_prod(1.1, qml.RX(1.1, 0)), {}), (qml.prod(qml.PauliX(0), qml.PauliY(1), qml.PauliZ(0)), {}), @@ -69,17 +67,6 @@ ] """Valid operator instances that could not be auto-generated.""" -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", "qml.ops.Hamiltonian uses", qml.PennyLaneDeprecationWarning) - _INSTANCES_TO_TEST.append( - ( - qml.operation.convert_to_legacy_H( - qml.Hamiltonian([1.1, 2.2], [qml.PauliX(0), qml.PauliZ(0)]) - ), - {}, - ) - ) - _INSTANCES_TO_FAIL = [ ( diff --git a/tests/ops/functions/test_assert_valid.py b/tests/ops/functions/test_assert_valid.py index 70cea786063..d67c71d5511 100644 --- a/tests/ops/functions/test_assert_valid.py +++ b/tests/ops/functions/test_assert_valid.py @@ -402,11 +402,7 @@ def test_generated_list_of_ops(class_to_validate, str_wires): def test_explicit_list_of_ops(valid_instance_and_kwargs): """Test the validity of operators that could not be auto-generated.""" valid_instance, kwargs = valid_instance_and_kwargs - if valid_instance.name == "Hamiltonian": - with qml.operation.disable_new_opmath_cm(warn=False): - assert_valid(valid_instance, **kwargs) - else: - assert_valid(valid_instance, **kwargs) + assert_valid(valid_instance, **kwargs) @pytest.mark.jax diff --git a/tests/ops/functions/test_bind_new_parameters.py b/tests/ops/functions/test_bind_new_parameters.py index 94872282052..00dc631b64a 100644 --- a/tests/ops/functions/test_bind_new_parameters.py +++ b/tests/ops/functions/test_bind_new_parameters.py @@ -14,14 +14,12 @@ """ This module contains unit tests for ``qml.bind_parameters``. """ -import warnings import numpy as np import pytest from gate_data import GELL_MANN, I, X, Y, Z import pennylane as qml -from pennylane.operation import Tensor from pennylane.ops.functions import bind_new_parameters @@ -185,70 +183,6 @@ def test_controlled_sequence(): qml.assert_equal(new_op.base, qml.RX(0.5, wires=3)) -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", "qml.ops.Hamiltonian uses", qml.PennyLaneDeprecationWarning) - warnings.filterwarnings("ignore", "qml.operation.Tensor uses", qml.PennyLaneDeprecationWarning) - - TEST_BIND_LEGACY_HAMILTONIAN = [ - ( - qml.ops.Hamiltonian( - [1.1, 2.1, 3.1], - [Tensor(qml.PauliZ(0), qml.PauliX(1)), qml.Hadamard(1), qml.PauliY(0)], - ), - [1.2, 2.2, 3.2], - qml.ops.Hamiltonian( - [1.2, 2.2, 3.2], - [Tensor(qml.PauliZ(0), qml.PauliX(1)), qml.Hadamard(1), qml.PauliY(0)], - ), - ), - ( - qml.ops.Hamiltonian([1.6, -1], [qml.Hermitian(X, wires=1), qml.PauliX(1)]), - [-1, 1.6], - qml.ops.Hamiltonian([-1, 1.6], [qml.Hermitian(X, wires=1), qml.PauliX(1)]), - ), - ] - - TEST_BIND_LEGACY_TENSOR = [ - ( - Tensor(qml.Hermitian(Y, wires=0), qml.PauliZ(1)), - [X], - Tensor(qml.Hermitian(X, wires=0), qml.PauliZ(1)), - ), - ( - Tensor(qml.Hermitian(qml.math.kron(X, Z), wires=[0, 1]), qml.Hermitian(I, wires=2)), - [qml.math.kron(I, I), Z], - Tensor(qml.Hermitian(qml.math.kron(I, I), wires=[0, 1]), qml.Hermitian(Z, wires=2)), - ), - (Tensor(qml.PauliZ(0), qml.PauliX(1)), [], Tensor(qml.PauliZ(0), qml.PauliX(1))), - ] - - -@pytest.mark.parametrize( - "H, new_coeffs, expected_H", - TEST_BIND_LEGACY_HAMILTONIAN, -) -def test_hamiltonian_legacy_opmath(H, new_coeffs, expected_H): - """Test that `bind_new_parameters` with `Hamiltonian` returns a new - operator with the new parameters without mutating the original - operator.""" - new_H = bind_new_parameters(H, new_coeffs) - - qml.assert_equal(new_H, expected_H) - assert new_H is not H - - -@pytest.mark.parametrize("op, new_params, expected_op", TEST_BIND_LEGACY_TENSOR) -def test_tensor(op, new_params, expected_op): - """Test that `bind_new_parameters` with `Tensor` returns a new - operator with the new parameters without mutating the original - operator.""" - new_op = bind_new_parameters(op, new_params) - - qml.assert_equal(new_op, expected_op) - assert new_op is not op - assert all(n_obs is not obs for n_obs, obs in zip(new_op.obs, op.obs)) - - TEST_BIND_LINEARCOMBINATION = [ ( # LinearCombination with only data being the coeffs qml.ops.LinearCombination( @@ -340,7 +274,6 @@ def test_linear_combination(H, new_coeffs, expected_H): assert new_H is not H -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_hamiltonian_grouping_indices(): """Test that bind_new_parameters with a Hamiltonian preserves the grouping indices.""" H = qml.Hamiltonian([1.0, 2.0], [qml.PauliX(0), qml.PauliX(1)]) diff --git a/tests/ops/functions/test_commutator.py b/tests/ops/functions/test_commutator.py index 98585e501c2..2ca0285c3aa 100644 --- a/tests/ops/functions/test_commutator.py +++ b/tests/ops/functions/test_commutator.py @@ -19,7 +19,7 @@ import pennylane as qml from pennylane.operation import Operator -from pennylane.ops import SProd, Sum +from pennylane.ops import SProd from pennylane.pauli import PauliSentence, PauliWord X, Y, Z, Id = qml.PauliX, qml.PauliY, qml.PauliZ, qml.Identity @@ -46,31 +46,6 @@ def _id(p): return p -class TestLegacySupport: - """Test support for legacy operator classes like Tensor and Hamiltonian""" - - def test_Hamiltonian_single(self): - """Test that Hamiltonians get transformed to new operator classes and return the correct result""" - H1 = qml.Hamiltonian([1.0], [qml.PauliX(0)]) - H2 = qml.Hamiltonian([1.0], [qml.PauliY(0)]) - res = qml.commutator(H1, H2) - true_res = qml.s_prod(2j, qml.PauliZ(0)) - assert isinstance(res, SProd) - assert true_res == res - - def test_Hamiltonian_sum(self): - """Test that Hamiltonians with Tensors and sums get transformed to new operator classes and return the correct result""" - H1 = qml.Hamiltonian([1.0], [qml.PauliX(0) @ qml.PauliX(1)]) - H2 = qml.Hamiltonian([1.0], [qml.PauliY(0) + qml.PauliY(1)]) - true_res = qml.sum( - qml.s_prod(2j, qml.PauliZ(0) @ qml.PauliX(1)), - qml.s_prod(2j, qml.PauliX(0) @ qml.PauliZ(1)), - ) - res = qml.commutator(H1, H2).simplify() - assert isinstance(res, Sum) - qml.assert_equal(true_res, res) - - def test_alias(): """Test that the alias qml.comm() works as expected""" res1 = qml.comm(X0, Y0) diff --git a/tests/ops/functions/test_dot.py b/tests/ops/functions/test_dot.py index 26ed43d9071..48216986355 100644 --- a/tests/ops/functions/test_dot.py +++ b/tests/ops/functions/test_dot.py @@ -307,9 +307,10 @@ def test_dot_returns_pauli_sentence(self): def test_coeffs_and_ops(self): """Test that the coefficients and operators of the returned PauliSentence are correct.""" ps = qml.dot(coeffs0, ops0, pauli=True) - h = ps.hamiltonian() - assert qml.math.allequal(h.coeffs, coeffs0) - for _op1, _op2 in zip(h.ops, ops0): + h = ps.operation() + hcoeffs, hops = h.terms() + assert qml.math.allequal(hcoeffs, coeffs0) + for _op1, _op2 in zip(hops, ops0): qml.assert_equal(_op1, _op2) def test_dot_simplifies_linear_combination(self): @@ -318,15 +319,15 @@ def test_dot_simplifies_linear_combination(self): coeffs=[0.12, 1.2, 12], ops=[qml.PauliX(0), qml.PauliX(0), qml.PauliX(0)], pauli=True ) assert len(ps) == 1 - h = ps.hamiltonian() - assert len(h.ops) == 1 - qml.assert_equal(h.ops[0], qml.PauliX(0)) + h = ps.operation() + assert isinstance(h, qml.ops.SProd) + qml.assert_equal(h.base, qml.PauliX(0)) def test_dot_returns_hamiltonian_simplified(self): """Test that hamiltonian computed from the PauliSentence created by the dot function is equal to the simplified hamiltonian.""" ps = qml.dot(coeffs0, ops0, pauli=True) - h_ps = ps.hamiltonian() + h_ps = ps.operation() h = qml.Hamiltonian(coeffs0, ops0) h.simplify() qml.assert_equal(h_ps, h) diff --git a/tests/ops/functions/test_eigvals.py b/tests/ops/functions/test_eigvals.py index 569e9cc1969..edb9e774b40 100644 --- a/tests/ops/functions/test_eigvals.py +++ b/tests/ops/functions/test_eigvals.py @@ -118,13 +118,6 @@ def test_ctrl(self): expected = np.linalg.eigvals(qml.matrix(qml.CNOT(wires=[0, 1]))) assert np.allclose(np.sort(res), np.sort(expected)) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_tensor_product_legacy_opmath(self): - """Test a tensor product""" - res = qml.eigvals(qml.PauliX(0) @ qml.Identity(1) @ qml.PauliZ(1)) - expected = reduce(np.kron, [[1, -1], [1, 1], [1, -1]]) - assert np.allclose(res, expected) - def test_tensor_product(self): """Test a tensor product""" res = qml.eigvals(qml.prod(qml.PauliX(0), qml.Identity(1), qml.PauliZ(1), lazy=False)) @@ -140,17 +133,6 @@ def test_hamiltonian(self): expected = np.linalg.eigvalsh(reduce(np.kron, [Z, Y]) - 0.5 * reduce(np.kron, [I, X])) assert np.allclose(res, expected) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_hamiltonian_legacy_opmath(self): - """Test that the matrix of a Hamiltonian is correctly returned""" - ham = qml.PauliZ(0) @ qml.PauliY(1) - 0.5 * qml.PauliX(1) - - with pytest.warns(UserWarning, match="the eigenvalues will be computed numerically"): - res = qml.eigvals(ham) - - expected = np.linalg.eigvalsh(reduce(np.kron, [Z, Y]) - 0.5 * reduce(np.kron, [I, X])) - assert np.allclose(res, expected) - @pytest.mark.xfail( reason="This test will fail because Hamiltonians are not queued to tapes yet!" ) diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py index 6ed3a953b7e..7151b357050 100644 --- a/tests/ops/functions/test_equal.py +++ b/tests/ops/functions/test_equal.py @@ -236,23 +236,6 @@ qml.Hamiltonian([1, 1], [qml.PauliX("b"), qml.PauliZ("a")]), False, ), - (qml.Hamiltonian([1], [qml.PauliZ(0) @ qml.PauliX(1)]), qml.PauliZ(0) @ qml.PauliX(1), True), - (qml.Hamiltonian([1], [qml.PauliZ(0)]), qml.PauliZ(0), True), - ( - qml.Hamiltonian( - [1, 1, 1], - [ - qml.Hermitian(np.array([[1, 0], [0, -1]]), "b") @ qml.Identity(7), - qml.PauliZ(3), - qml.Identity(1.2), - ], - ), - qml.Hamiltonian( - [1, 1, 1], - [qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), qml.PauliZ(3), qml.Identity(1.2)], - ), - True, - ), ( qml.Hamiltonian([1, 1], [qml.PauliZ(3) @ qml.Identity(1.2), qml.PauliZ(3)]), qml.Hamiltonian([2], [qml.PauliZ(3)]), @@ -260,53 +243,6 @@ ), ] -equal_tensors = [ - (qml.PauliX(0) @ qml.PauliY(1), qml.PauliY(1) @ qml.PauliX(0), True), - (qml.PauliX(0) @ qml.Identity(1) @ qml.PauliZ(2), qml.PauliX(0) @ qml.PauliZ(2), True), - (qml.PauliX(0) @ qml.Identity(2) @ qml.PauliZ(1), qml.PauliX(0) @ qml.PauliZ(2), False), - (qml.PauliX(0) @ qml.PauliZ(1), qml.PauliX(0) @ qml.PauliZ(2), False), - (qml.PauliX("a") @ qml.PauliZ("b"), qml.PauliX("a") @ qml.PauliZ("b"), True), - (qml.PauliX("a") @ qml.PauliZ("b"), qml.PauliX("c") @ qml.PauliZ("d"), False), - (qml.PauliX("a") @ qml.PauliZ("b"), qml.PauliX("b") @ qml.PauliZ("a"), False), - (qml.PauliX(1.1) @ qml.PauliZ(1.2), qml.PauliX(1.1) @ qml.PauliZ(1.2), True), - (qml.PauliX(1.1) @ qml.PauliZ(1.2), qml.PauliX(1.2) @ qml.PauliZ(0.9), False), -] - -equal_hamiltonians_and_tensors = [ - (qml.Hamiltonian([1], [qml.PauliX(0) @ qml.PauliY(1)]), qml.PauliY(1) @ qml.PauliX(0), True), - ( - qml.Hamiltonian( - [0.5, 0.5], - [qml.PauliZ(0) @ qml.PauliY(1), qml.PauliY(1) @ qml.PauliZ(0) @ qml.Identity("a")], - ), - qml.PauliZ(0) @ qml.PauliY(1), - True, - ), - (qml.Hamiltonian([1], [qml.PauliX(0) @ qml.PauliY(1)]), qml.PauliX(0) @ qml.PauliY(1), True), - (qml.Hamiltonian([2], [qml.PauliX(0) @ qml.PauliY(1)]), qml.PauliX(0) @ qml.PauliY(1), False), - (qml.Hamiltonian([1], [qml.PauliX(0) @ qml.PauliY(1)]), qml.PauliX(4) @ qml.PauliY(1), False), - ( - qml.Hamiltonian([1], [qml.PauliX("a") @ qml.PauliZ("b")]), - qml.PauliX("a") @ qml.PauliZ("b"), - True, - ), - ( - qml.Hamiltonian([1], [qml.PauliX("a") @ qml.PauliZ("b")]), - qml.PauliX("b") @ qml.PauliZ("a"), - False, - ), - ( - qml.Hamiltonian([1], [qml.PauliX(1.2) @ qml.PauliZ(0.2)]), - qml.PauliX(1.2) @ qml.PauliZ(0.2), - True, - ), - ( - qml.Hamiltonian([1], [qml.PauliX(1.2) @ qml.PauliZ(0.2)]), - qml.PauliX(1.3) @ qml.PauliZ(2), - False, - ), -] - equal_pauli_operators = [ (qml.PauliX(0), qml.PauliX(0), True), (qml.PauliY("a"), qml.PauliY("a"), True), @@ -318,10 +254,6 @@ (qml.PauliY("a"), qml.PauliX("a"), False), (qml.PauliZ(0.3), qml.PauliY(0.3), False), (qml.PauliZ(0), qml.RX(1.23, 0), False), - (qml.Hamiltonian([1], [qml.PauliX("a")]), qml.PauliX("a"), True), - (qml.Hamiltonian([1], [qml.PauliX("a")]), qml.PauliX("b"), False), - (qml.Hamiltonian([1], [qml.PauliX(1.2)]), qml.PauliX(1.2), True), - (qml.Hamiltonian([1], [qml.PauliX(1.2)]), qml.PauliX(1.3), False), ] @@ -354,6 +286,29 @@ def _(op1: RandomType, op2, **_): class TestEqual: + + def test_identity_equal(self): + """Test that comparing two Identities always returns True regardless of wires""" + I1 = qml.Identity() + I2 = qml.Identity(wires=[-1]) + I3 = qml.Identity(wires=[0, 1, 2, 3]) + I4 = qml.Identity(wires=["a", "b"]) + + assert qml.equal(I1, I2) + assert qml.equal(I1, I3) + assert qml.equal(I1, I4) + assert qml.equal(I2, I3) + assert qml.equal(I2, I4) + assert qml.equal(I3, I4) + + @pytest.mark.parametrize(("op1", "op2", "res"), equal_pauli_operators) + def test_pauli_operator_equals(self, op1, op2, res): + """Tests that equality can be checked between PauliX/Y/Z operators, and between Pauli operators + and Hamiltonians""" + + assert qml.equal(op1, op2) == qml.equal(op2, op1) + assert qml.equal(op1, op2) == res + @pytest.mark.parametrize("ops", PARAMETRIZED_OPERATIONS_COMBINATIONS) def test_equal_simple_diff_op(self, ops): """Test different operators return False""" @@ -1464,113 +1419,11 @@ def test_shadow_expval_list_versus_operator(self): assert not qml.equal(m1, m2) -@pytest.mark.usefixtures("legacy_opmath_only") # TODO update qml.equal with new opmath -class TestObservablesComparisons: - """Tests comparisons between Hamiltonians, Tensors and PauliX/Y/Z operators""" +def test_unsupported_object_type_not_implemented(): + dev = qml.device("default.qubit", wires=1) - def test_identity_equal(self): - """Test that comparing two Identities always returns True regardless of wires""" - I1 = qml.Identity() - I2 = qml.Identity(wires=[-1]) - I3 = qml.Identity(wires=[0, 1, 2, 3]) - I4 = qml.Identity(wires=["a", "b"]) - - assert qml.equal(I1, I2) - assert qml.equal(I1, I3) - assert qml.equal(I1, I4) - assert qml.equal(I2, I3) - assert qml.equal(I2, I4) - assert qml.equal(I3, I4) - - @pytest.mark.parametrize(("H1", "H2", "res"), equal_hamiltonians) - def test_hamiltonian_equal(self, H1, H2, res): - """Tests that equality can be checked between Hamiltonians""" - if not qml.operation.active_new_opmath(): - H1 = qml.operation.convert_to_legacy_H(H1) - H2 = qml.operation.convert_to_legacy_H(H2) - - assert qml.equal(H1, H2) == qml.equal(H2, H1) - assert qml.equal(H1, H2) == res - if not res: - error_message_pattern = re.compile(r"'([^']+)' and '([^']+)' are not the same") - with pytest.raises(AssertionError, match=error_message_pattern): - assert_equal(H1, H2) - - @pytest.mark.parametrize(("T1", "T2", "res"), equal_tensors) - def test_tensors_equal(self, T1, T2, res): - """Tests that equality can be checked between Tensors""" - assert qml.equal(T1, T2) == qml.equal(T2, T1) - assert qml.equal(T1, T2) == res - - def test_tensors_not_equal(self): - """Tensors are not equal because of different observable data""" - op1 = qml.operation.Tensor(qml.X(0), qml.Y(1)) - op2 = qml.operation.Tensor(qml.Y(0), qml.X(1)) - with pytest.raises(AssertionError, match="have different _obs_data outputs"): - assert_equal(op1, op2) - - @pytest.mark.parametrize(("H", "T", "res"), equal_hamiltonians_and_tensors) - def test_hamiltonians_and_tensors_equal(self, H, T, res): - """Tests that equality can be checked between a Hamiltonian and a Tensor""" - if not qml.operation.active_new_opmath(): - H = qml.operation.convert_to_legacy_H(H) - T = qml.operation.Tensor(*T.operands) - - assert qml.equal(H, T) == qml.equal(T, H) - assert qml.equal(H, T) == res - - @pytest.mark.parametrize(("op1", "op2", "res"), equal_pauli_operators) - def test_pauli_operator_equals(self, op1, op2, res): - """Tests that equality can be checked between PauliX/Y/Z operators, and between Pauli operators and Hamiltonians""" - if not qml.operation.active_new_opmath(): - op1 = qml.operation.convert_to_legacy_H(op1) - op2 = qml.operation.convert_to_legacy_H(op2) - - assert qml.equal(op1, op2) == qml.equal(op2, op1) - assert qml.equal(op1, op2) == res - - def test_hamiltonian_and_operation_not_equal(self): - """Tests that comparing a Hamiltonian with an Operator that is not an Observable returns False""" - op1 = qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliY(0)]) - op2 = qml.RX(1.2, 0) - assert qml.equal(op1, op2) is False - assert qml.equal(op2, op1) is False - with pytest.raises(AssertionError, match="is not of type Observable"): - assert_equal(op1, op2) - - def test_tensor_and_operation_not_equal(self): - """Tests that comparing a Tensor with an Operator that is not an Observable returns False""" - op1 = qml.PauliX(0) @ qml.PauliY(1) - op2 = qml.RX(1.2, 0) - assert qml.equal(op1, op2) is False - assert qml.equal(op2, op1) is False - with pytest.raises(AssertionError, match="is not of type Observable"): - assert_equal(op1, op2) - - def test_tensor_and_observable_not_equal(self): - """Tests that comparing a Tensor with an Observable that is not a Tensor returns False""" - op1 = qml.PauliX(0) @ qml.PauliY(1) - op2 = qml.Z(0) - assert qml.equal(op1, op2) is False - assert qml.equal(op2, op1) is False - with pytest.raises(AssertionError, match="is of type "): - assert_equal(op1, op2) - - def test_tensor_and_unsupported_observable_returns_false(self): - """Tests that trying to compare a Tensor to something other than another Tensor or a Hamiltonian returns False""" - op1 = qml.PauliX(0) @ qml.PauliY(1) - op2 = qml.Hermitian([[0, 1], [1, 0]], 0) - - assert not qml.equal(op1, op2) - error_message_pattern = re.compile(r"'([^']+)' and '([^']+)' are not the same") - with pytest.raises(AssertionError, match=error_message_pattern): - assert_equal(op1, op2) - - def test_unsupported_object_type_not_implemented(self): - dev = qml.device("default.qubit", wires=1) - - with pytest.raises(NotImplementedError, match="Comparison of"): - qml.equal(dev, dev) + with pytest.raises(NotImplementedError, match="Comparison of"): + qml.equal(dev, dev) class TestSymbolicOpComparison: @@ -2141,7 +1994,6 @@ def test_s_prod_base_op_comparison_with_trainability(self): assert not qml.equal(op1, op2, check_interface=False, check_trainability=True) -@pytest.mark.usefixtures("new_opmath_only") class TestProdComparisons: """Tests comparisons between Prod operators""" @@ -2189,6 +2041,25 @@ class TestProdComparisons: ), ] + @pytest.mark.parametrize( + ("T1", "T2", "res"), + [ + (qml.PauliX(0) @ qml.PauliY(1), qml.PauliY(1) @ qml.PauliX(0), True), + (qml.PauliX(0) @ qml.Identity(1) @ qml.PauliZ(2), qml.PauliX(0) @ qml.PauliZ(2), True), + (qml.PauliX(0) @ qml.Identity(2) @ qml.PauliZ(1), qml.PauliX(0) @ qml.PauliZ(2), False), + (qml.PauliX(0) @ qml.PauliZ(1), qml.PauliX(0) @ qml.PauliZ(2), False), + (qml.PauliX("a") @ qml.PauliZ("b"), qml.PauliX("a") @ qml.PauliZ("b"), True), + (qml.PauliX("a") @ qml.PauliZ("b"), qml.PauliX("c") @ qml.PauliZ("d"), False), + (qml.PauliX("a") @ qml.PauliZ("b"), qml.PauliX("b") @ qml.PauliZ("a"), False), + (qml.PauliX(1.1) @ qml.PauliZ(1.2), qml.PauliX(1.1) @ qml.PauliZ(1.2), True), + (qml.PauliX(1.1) @ qml.PauliZ(1.2), qml.PauliX(1.2) @ qml.PauliZ(0.9), False), + ], + ) + def test_prods_equal(self, T1, T2, res): + """Tests that equality can be checked between Prods""" + assert qml.equal(T1, T2) == qml.equal(T2, T1) + assert qml.equal(T1, T2) == res + def test_non_commuting_order_swap_not_equal(self): """Test that changing the order of non-commuting operators is not equal""" op1 = qml.prod(qml.PauliX(0), qml.PauliY(0)) @@ -2247,7 +2118,6 @@ def test_prod_global_phase(self): assert qml.equal(p1, p2) -@pytest.mark.usefixtures("new_opmath_only") class TestSumComparisons: """Tests comparisons between Sum operators""" @@ -2386,6 +2256,20 @@ def test_sum_global_phase(self): op2 = qml.sum(qml.GlobalPhase(np.pi), qml.X(0)) assert qml.equal(op1, op2) + @pytest.mark.parametrize(("H1", "H2", "res"), equal_hamiltonians) + def test_hamiltonian_equal(self, H1, H2, res): + """Tests that equality can be checked between LinearCombinations""" + + assert qml.equal(H1, H2) == qml.equal(H2, H1) + assert qml.equal(H1, H2) == res + if not res: + if len(H1) != len(H2): + error_message = "op1 and op2 have different number of operands" + else: + error_message = re.compile(r"op1 and op2 have different operands") + with pytest.raises(AssertionError, match=error_message): + assert_equal(H1, H2) + def f1(p, t): return np.polyval(p, t) diff --git a/tests/ops/functions/test_generator.py b/tests/ops/functions/test_generator.py index aa91323fc3e..9c997204632 100644 --- a/tests/ops/functions/test_generator.py +++ b/tests/ops/functions/test_generator.py @@ -341,28 +341,12 @@ class TestObservableReturn: """Tests for format="observable". This format preserves the initial generator encoded in the operator.""" - @pytest.mark.usefixtures("legacy_opmath_only") - def test_observable_legacy_opmath(self): - """Test a generator that returns a single observable is correct with opmath disabled""" - gen = qml.generator(ObservableOp, format="observable")(0.5, wires=0) - assert gen.name == "Hamiltonian" - assert gen.compare(ObservableOp(0.5, wires=0).generator()) - - @pytest.mark.usefixtures("new_opmath_only") def test_observable(self): """Test a generator that returns a single observable is correct""" gen = qml.generator(ObservableOp, format="observable")(0.5, wires=0) assert gen.name == "SProd" qml.assert_equal(gen, ObservableOp(0.5, wires=0).generator()) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_tensor_observable_legacy_opmath(self): - """Test a generator that returns a tensor observable is correct with opmath disabled""" - gen = qml.generator(TensorOp, format="observable")(0.5, wires=[0, 1]) - assert gen.name == "Hamiltonian" - assert gen.compare(TensorOp(0.5, wires=[0, 1]).generator()) - - @pytest.mark.usefixtures("new_opmath_only") def test_tensor_observable(self): """Test a generator that returns a tensor observable is correct""" gen = qml.generator(TensorOp, format="observable")(0.5, wires=[0, 1]) @@ -390,11 +374,9 @@ def test_sparse_hamiltonian(self): assert np.all(gen.parameters[0].toarray() == SparseOp.H.toarray()) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") class TestHamiltonianReturn: """Tests for format="hamiltonian". This format always returns the generator - as a Hamiltonian (either a qml.ops.Hamiltonian or a qml.ops.LinearCombination - depending on whether new_opmath is enabled.)""" + as a qml.ops.LinearCombination.""" def test_observable_no_coeff(self): """Test a generator that returns an observable with no coefficient is correct""" @@ -408,7 +390,8 @@ def test_observable(self): assert isinstance(gen, qml.Hamiltonian) assert gen.compare(ObservableOp(0.5, wires=0).generator()) - @pytest.mark.usefixtures("legacy_opmath_only") + # removed fixture to only run with legacy opmath - not clear why it was added, + # but we'll see once things are tidied up enough for tests to run def test_tensor_observable(self): """Test a generator that returns a tensor observable is correct""" gen = qml.generator(TensorOp, format="hamiltonian")(0.5, wires=[0, 1]) diff --git a/tests/ops/functions/test_is_commuting.py b/tests/ops/functions/test_is_commuting.py index 7e1ecaef810..a1171f295e6 100644 --- a/tests/ops/functions/test_is_commuting.py +++ b/tests/ops/functions/test_is_commuting.py @@ -791,11 +791,6 @@ def test_barrier_u3_identity(self, wires, res): qml.prod(qml.PauliX("a"), qml.PauliZ("c"), qml.PauliY("d")), False, ), - ( - qml.operation.Tensor(qml.PauliX("a"), qml.PauliY("b"), qml.PauliZ("d")), - qml.operation.Tensor(qml.PauliX("a"), qml.PauliZ("c"), qml.PauliY("d")), - False, - ), ( qml.sum(qml.PauliZ("a"), qml.PauliY("b"), qml.PauliZ("d")), qml.sum(qml.PauliX("a"), qml.PauliZ("c"), qml.PauliY("d")), @@ -825,10 +820,6 @@ def test_pauli_words(self, pauli_word_1, pauli_word_2, commute_status): qml.prod(qml.PauliX(0), qml.Hadamard(1), qml.Identity(2)), qml.sum(qml.PauliX(0), qml.PauliY(2)), ), - ( - qml.sum(qml.PauliX(0), qml.PauliY(2)), - qml.operation.Tensor(qml.PauliX(0), qml.Hadamard(1), qml.Identity(2)), - ), (qml.PauliX(2), qml.sum(qml.Hadamard(1), qml.prod(qml.PauliX(1), qml.Identity(2)))), (qml.prod(qml.PauliX(1), qml.PauliY(2)), qml.s_prod(0.5, qml.Hadamard(1))), ], diff --git a/tests/ops/op_math/test_adjoint.py b/tests/ops/op_math/test_adjoint.py index 6fba91f1537..ac7b508d2c5 100644 --- a/tests/ops/op_math/test_adjoint.py +++ b/tests/ops/op_math/test_adjoint.py @@ -76,7 +76,6 @@ class CustomOp(qml.operation.Operation): assert "grad_recipe" in dir(op) assert "control_wires" in dir(op) - @pytest.mark.usefixtures("legacy_opmath_only") def test_observable(self): """Test that when the base is an Observable, Adjoint will also inherit from Observable.""" @@ -96,8 +95,7 @@ class CustomObs(qml.operation.Observable): # Check some basic observable functionality assert ob.compare(ob) - with pytest.warns(UserWarning, match="Tensor object acts on overlapping"): - assert isinstance(1.0 * ob @ ob, qml.Hamiltonian) + assert isinstance(1.0 * ob @ ob, qml.ops.Prod) # check the dir assert "grad_recipe" not in dir(ob) @@ -107,7 +105,7 @@ class CustomObs(qml.operation.Observable): ( PlainOperator(1.2, wires=0), qml.RX(1.2, wires=0), - qml.operation.Tensor(qml.PauliX(0), qml.PauliX(1)), + qml.Hermitian([[1, 0], [0, 1]], wires=0), qml.PauliX(0), ), ) @@ -180,24 +178,6 @@ def test_template_base(self, seed): assert op.wires == qml.wires.Wires((0, 1)) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_hamiltonian_base(self): - """Test adjoint initialization for a hamiltonian.""" - with pytest.warns(UserWarning, match="Tensor object acts on overlapping"): - base = 2.0 * qml.PauliX(0) @ qml.PauliY(0) + qml.PauliZ("b") - - op = Adjoint(base) - - assert op.base is base - assert op.hyperparameters["base"] is base - assert op.name == "Adjoint(Hamiltonian)" - - assert op.num_params == 2 - assert qml.math.allclose(op.parameters, [2.0, 1.0]) - assert qml.math.allclose(op.data, [2.0, 1.0]) - - assert op.wires == qml.wires.Wires([0, "b"]) - class TestProperties: """Test Adjoint properties.""" @@ -320,12 +300,6 @@ def test_queue_category(self): op = Adjoint(qml.PauliX(0)) assert op._queue_category == "_ops" # pylint: disable=protected-access - @pytest.mark.usefixtures("legacy_opmath_only") - def test_queue_category_None(self): - """Test that the queue category `None` for some observables carries over.""" - op = Adjoint(qml.PauliX(0) @ qml.PauliY(1)) - assert op._queue_category is None # pylint: disable=protected-access - @pytest.mark.parametrize("value", (True, False)) def test_is_hermitian(self, value): """Test `is_hermitian` property mirrors that of the base.""" @@ -492,7 +466,6 @@ def test_has_generator_false(self): assert op.has_generator is False - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_generator(self): """Assert that the generator of an Adjoint is -1.0 times the base generator.""" base = qml.RX(1.23, wires=0) @@ -653,7 +626,6 @@ def test_no_matrix_defined(self, seed): with pytest.raises(qml.operation.MatrixUndefinedError): Adjoint(base).matrix() - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_adj_hamiltonian(self): """Test that a we can take the adjoint of a hamiltonian.""" U = qml.Hamiltonian([1.0], [qml.PauliX(wires=0) @ qml.PauliZ(wires=1)]) @@ -868,22 +840,6 @@ def test_single_op_defined_outside_queue_eager(self): assert len(q) == 1 assert q.queue[0] is out - @pytest.mark.usefixtures("legacy_opmath_only") - def test_single_observable(self): - """Test passing a single preconstructed observable in a queuing context.""" - - with qml.queuing.AnnotatedQueue() as q: - base = qml.PauliX(0) @ qml.PauliY(1) - out = adjoint(base) - - assert len(q) == 1 - assert q.queue[0] is out - assert out.base is base - assert isinstance(out, Adjoint) - - qs = qml.tape.QuantumScript.from_queue(q) - assert len(qs) == 0 - def test_correct_queued_operators(self): """Test that args and kwargs do not add operators to the queue.""" diff --git a/tests/ops/op_math/test_composite.py b/tests/ops/op_math/test_composite.py index d6cfdc3f07a..b0f52a84526 100644 --- a/tests/ops/op_math/test_composite.py +++ b/tests/ops/op_math/test_composite.py @@ -16,7 +16,7 @@ """ import inspect -# pylint:disable=protected-access +# pylint:disable=protected-access, use-implicit-booleaness-not-comparison from copy import copy import numpy as np @@ -24,7 +24,7 @@ import pennylane as qml from pennylane.operation import DecompositionUndefinedError -from pennylane.ops.op_math import CompositeOp, Prod, SProd, Sum +from pennylane.ops.op_math import CompositeOp from pennylane.wires import Wires ops = ( @@ -213,27 +213,6 @@ def test_build_pauli_rep(self): op = ValidOp(*self.simple_operands) assert op._build_pauli_rep() == qml.pauli.PauliSentence({}) - def test_tensor_and_hamiltonian_converted(self): - """Test that Tensor and Hamiltonian instances get converted to Prod and Sum.""" - operands = [ - qml.Hamiltonian( - [1.1, 2.2], [qml.PauliZ(0), qml.operation.Tensor(qml.PauliX(0), qml.PauliZ(1))] - ), - qml.prod(qml.PauliX(0), qml.PauliZ(1)), - qml.operation.Tensor(qml.PauliX(2), qml.PauliZ(3)), - ] - op = qml.sum(*operands) - assert isinstance(op[0], Sum) - assert isinstance(op[0][1], SProd) - assert isinstance(op[0][1].base, Prod) - assert op[1] is operands[1] - assert isinstance(op[2], Prod) - assert op.operands == ( - qml.dot([1.1, 2.2], [qml.PauliZ(0), qml.prod(qml.PauliX(0), qml.PauliZ(1))]), - operands[1], - qml.prod(qml.PauliX(2), qml.PauliZ(3)), - ) - @pytest.mark.parametrize("math_op", [qml.prod, qml.sum]) def test_no_recursion_error_raised(math_op): diff --git a/tests/ops/op_math/test_evolution.py b/tests/ops/op_math/test_evolution.py index 1c3c8b1e0ad..2838a9f5f69 100644 --- a/tests/ops/op_math/test_evolution.py +++ b/tests/ops/op_math/test_evolution.py @@ -71,15 +71,6 @@ def test_generator(self): U = Evolution(qml.PauliX(0), 3) assert U.generator() == -1 * U.base - @pytest.mark.usefixtures("legacy_opmath_only") - def test_num_params_for_parametric_base_legacy_opmath(self): - base_op = 0.5 * qml.PauliY(0) + qml.PauliZ(0) @ qml.PauliX(1) - op = Evolution(base_op, 1.23) - - assert base_op.num_params == 2 - assert op.num_params == 1 - - @pytest.mark.usefixtures("new_opmath_only") def test_num_params_for_parametric_base(self): base_op = 0.5 * qml.PauliY(0) + qml.PauliZ(0) @ qml.PauliX(1) op = Evolution(base_op, 1.23) diff --git a/tests/ops/op_math/test_exp.py b/tests/ops/op_math/test_exp.py index 76da143d621..e73ec05d703 100644 --- a/tests/ops/op_math/test_exp.py +++ b/tests/ops/op_math/test_exp.py @@ -432,16 +432,6 @@ def test_non_pauli_word_base_no_decomposition(self): ): op.decomposition() - @pytest.mark.usefixtures("legacy_opmath_only") - def test_nontensor_tensor_no_decomposition(self): - """Checks that accessing the decomposition throws an error if the base is a Tensor - object that is not a mathematical tensor""" - base_op = qml.PauliX(0) @ qml.PauliZ(0) - op = Exp(base_op, 1j) - assert not op.has_decomposition - with pytest.raises(DecompositionUndefinedError): - _ = op.decomposition() - @pytest.mark.parametrize( "base, base_string", ( @@ -458,22 +448,6 @@ def test_decomposition_into_pauli_rot(self, base, base_string): pr = op.decomposition()[0] qml.assert_equal(pr, qml.PauliRot(3.21, base_string, base.wires)) - @pytest.mark.parametrize( - "base, base_string", - ( - (qml.operation.Tensor(qml.PauliZ(0), qml.PauliY(1)), "ZY"), - (qml.operation.Tensor(qml.PauliY(0), qml.Identity(1), qml.PauliZ(2)), "YIZ"), - ), - ) - def test_decomposition_tensor_into_pauli_rot(self, base, base_string): - """Check that Exp decomposes into PauliRot if base is a pauli word with more than one term.""" - theta = 3.21 - op = Exp(base, -0.5j * theta) - - assert op.has_decomposition - pr = op.decomposition()[0] - qml.assert_equal(pr, qml.PauliRot(3.21, base_string, base.wires)) - @pytest.mark.parametrize("op_name", all_qubit_operators) @pytest.mark.parametrize("str_wires", (True, False)) def test_generator_decomposition(self, op_name, str_wires): diff --git a/tests/ops/op_math/test_linear_combination.py b/tests/ops/op_math/test_linear_combination.py index b75b7d01fd9..728ec636966 100644 --- a/tests/ops/op_math/test_linear_combination.py +++ b/tests/ops/op_math/test_linear_combination.py @@ -25,54 +25,10 @@ import pennylane as qml from pennylane import X, Y, Z from pennylane import numpy as pnp -from pennylane.operation import enable_new_opmath_cm from pennylane.ops import LinearCombination from pennylane.pauli import PauliSentence, PauliWord from pennylane.wires import Wires - -@pytest.mark.usefixtures("legacy_opmath_only") -def test_switching(): - """Test that switching to new from old opmath changes the dispatch of qml.Hamiltonian""" - Ham = qml.Hamiltonian([1.0, 2.0, 3.0], [X(0), X(0) @ X(1), X(2)]) - assert isinstance(Ham, qml.Hamiltonian) - assert not isinstance(Ham, qml.ops.LinearCombination) - - with enable_new_opmath_cm(warn=False): - LC = qml.Hamiltonian([1.0, 2.0, 3.0], [X(0), X(0) @ X(1), X(2)]) - assert isinstance(LC, qml.Hamiltonian) - assert isinstance(LC, qml.ops.LinearCombination) - - -def test_isinstance_Hamiltonian(): - """Test that Hamiltonian and LinearCombination can be used interchangeably when new opmath is disabled or enabled""" - H = qml.Hamiltonian([1.0, 2.0, 3.0], [X(0), X(0) @ X(1), X(2)]) - assert isinstance(H, qml.Hamiltonian) - - -def test_mixed_legacy_warning_Hamiltonian_legacy(): - """Test that mixing legacy ops and LinearCombination.compare raises a warning in legacy opmath""" - - op1 = qml.ops.LinearCombination([0.5, 0.5], [X(0) @ X(1), qml.Hadamard(0)]) - op2 = qml.ops.Hamiltonian([0.5, 0.5], [qml.operation.Tensor(X(0), X(1)), qml.Hadamard(0)]) - - with pytest.warns(UserWarning, match="Attempting to compare a legacy operator class instance"): - res = op1.compare(op2) - - assert res - - -def test_mixed_legacy_warning_Tensor(): - """Test that mixing legacy ops and LinearCombination.compare raises a warning""" - op1 = qml.ops.LinearCombination([1.0], [X(0) @ qml.Hadamard(1)]) - op2 = qml.operation.Tensor(X(0), qml.Hadamard(1)) - - with pytest.warns(UserWarning, match="Attempting to compare a legacy operator class instance"): - res = op1.compare(op2) - - assert res - - # Make test data in different interfaces, if installed COEFFS_PARAM_INTERFACE = [ ([-0.05, 0.17], 1.7, "autograd"), @@ -555,7 +511,6 @@ def circuit2(param): dev = qml.device("default.qubit", wires=2) -@pytest.mark.usefixtures("new_opmath_only") class TestLinearCombination: """Test the LinearCombination class""" diff --git a/tests/ops/op_math/test_pow_op.py b/tests/ops/op_math/test_pow_op.py index 22b0ab4d83a..e667f77e465 100644 --- a/tests/ops/op_math/test_pow_op.py +++ b/tests/ops/op_math/test_pow_op.py @@ -164,30 +164,6 @@ class CustomObs(qml.operation.Observable): # check the dir assert "grad_recipe" not in dir(ob) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_observable_legacy_opmath(self, power_method): - """Test that when the base is an Observable, Pow will also inherit from Observable.""" - - class CustomObs(qml.operation.Observable): - num_wires = 1 - num_params = 0 - - base = CustomObs(wires=0) - ob: Pow = power_method(base=base, z=-1.2) - - assert isinstance(ob, Pow) - assert isinstance(ob, qml.operation.Operator) - assert not isinstance(ob, qml.operation.Operation) - assert isinstance(ob, qml.operation.Observable) - assert not isinstance(ob, PowOperation) - - # Check some basic observable functionality - assert ob.compare(ob) - assert isinstance(1.0 * ob @ ob, qml.Hamiltonian) - - # check the dir - assert "grad_recipe" not in dir(ob) - @pytest.mark.parametrize("power_method", [Pow, pow_using_dunder_method, qml.pow]) class TestInitialization: @@ -258,26 +234,6 @@ def test_template_base(self, power_method, seed): assert op.wires == qml.wires.Wires((0, 1)) assert op.num_wires == 2 - @pytest.mark.usefixtures("legacy_opmath_only") - def test_hamiltonian_base(self, power_method): - """Test pow initialization for a hamiltonian.""" - base = qml.Hamiltonian([2.0, 1.0], [qml.PauliX(0) @ qml.PauliY(0), qml.PauliZ("b")]) - - op: Pow = power_method(base=base, z=3.4) - - assert op.base is base - assert op.z == 3.4 - assert op.hyperparameters["base"] is base - assert op.hyperparameters["z"] == 3.4 - assert op.name == "Hamiltonian**3.4" - - assert op.num_params == 2 - assert qml.math.allclose(op.parameters, [2.0, 1.0]) - assert qml.math.allclose(op.data, [2.0, 1.0]) - - assert op.wires == qml.wires.Wires([0, "b"]) - assert op.num_wires == 2 - # pylint: disable=too-many-public-methods @pytest.mark.parametrize("power_method", [Pow, pow_using_dunder_method, qml.pow]) @@ -423,12 +379,6 @@ def test_queue_category(self, power_method): op: Pow = power_method(base=qml.PauliX(0), z=3.5) assert op._queue_category == "_ops" # pylint: disable=protected-access - @pytest.mark.usefixtures("legacy_opmath_only") - def test_queue_category_None(self, power_method): - """Test that the queue category `None` for some observables carries over.""" - op: Pow = power_method(base=qml.PauliX(0) @ qml.PauliY(1), z=-1.1) - assert op._queue_category is None # pylint: disable=protected-access - def test_batching_properties(self, power_method): """Test the batching properties and methods.""" @@ -876,7 +826,6 @@ def test_matrix_wire_order(self): assert qml.math.allclose(op_mat, compare_mat) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_pow_hamiltonian(self): """Test that a hamiltonian object can be exponentiated.""" U = qml.Hamiltonian([1.0], [qml.PauliX(wires=0)]) diff --git a/tests/ops/op_math/test_sum.py b/tests/ops/op_math/test_sum.py index d6bc1668f9f..a9d1233ce64 100644 --- a/tests/ops/op_math/test_sum.py +++ b/tests/ops/op_math/test_sum.py @@ -344,7 +344,6 @@ def test_repr(self, op, repr_true): # qml.sum(*[0.5 * X(i) for i in range(10)]) # multiline output needs fixing of https://github.com/PennyLaneAI/pennylane/issues/5162 before working ) - @pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("op", SUM_REPR_EVAL) def test_eval_sum(self, op): """Test that string representations of Sum can be evaluated and yield the same operator""" @@ -681,7 +680,6 @@ def test_queue_category(self, ops_lst, sum_method): sum_op = sum_method(*ops_lst) assert sum_op._queue_category is None # pylint: disable=protected-access - @pytest.mark.usefixtures("new_opmath_only") def test_eigvals_Identity_no_wires(self): """Test that eigenvalues can be computed for a sum containing identity with no wires.""" @@ -710,7 +708,6 @@ def test_eigendecomposition(self): assert np.allclose(eig_vals, true_eigvals) assert np.allclose(eig_vecs, true_eigvecs) - @pytest.mark.usefixtures("new_opmath_only") def test_eigendecomposition_repeat_operations(self): """Test that the eigendecomposition works with a repeated operation.""" op1 = qml.X(0) + qml.X(0) + qml.X(0) diff --git a/tests/ops/qubit/test_attributes.py b/tests/ops/qubit/test_attributes.py index 9808c07d71d..a9bf5d3d4e2 100644 --- a/tests/ops/qubit/test_attributes.py +++ b/tests/ops/qubit/test_attributes.py @@ -83,10 +83,6 @@ def test_inclusion_after_addition(self): assert "RY" in new_attribute assert len(new_attribute) == 8 - def test_tensor_check(self): - """Test that we can ask if a tensor is in the attribute.""" - assert qml.operation.Tensor(qml.PauliX(wires=0), qml.PauliZ(wires=1)) not in new_attribute - single_scalar_single_wire_ops = [ "RX", diff --git a/tests/ops/qubit/test_hamiltonian.py b/tests/ops/qubit/test_hamiltonian.py deleted file mode 100644 index 8b27b2cb7b4..00000000000 --- a/tests/ops/qubit/test_hamiltonian.py +++ /dev/null @@ -1,2195 +0,0 @@ -# Copyright 2018-2021 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Tests for the Hamiltonian class. -""" -import warnings - -# pylint: disable=too-many-public-methods, superfluous-parens, unnecessary-dunder-call -from collections.abc import Iterable -from unittest.mock import patch - -import numpy as np -import pytest -import scipy - -import pennylane as qml -from pennylane import numpy as pnp -from pennylane.wires import Wires - -pytestmark = pytest.mark.filterwarnings( - "ignore:qml.ops.Hamiltonian uses:pennylane.PennyLaneDeprecationWarning" -) - - -# Make test data in different interfaces, if installed -COEFFS_PARAM_INTERFACE = [ - ([-0.05, 0.17], 1.7, "autograd"), - (np.array([-0.05, 0.17]), np.array(1.7), "autograd"), - (pnp.array([-0.05, 0.17], requires_grad=True), pnp.array(1.7, requires_grad=True), "autograd"), -] - -try: - import jax - from jax import numpy as jnp - - COEFFS_PARAM_INTERFACE.append((jnp.array([-0.05, 0.17]), jnp.array(1.7), "jax")) -except ImportError: - pass - -try: - import tensorflow as tf - - COEFFS_PARAM_INTERFACE.append( - (tf.Variable([-0.05, 0.17], dtype=tf.double), tf.Variable(1.7, dtype=tf.double), "tf") - ) -except ImportError: - pass - -try: - import torch - - COEFFS_PARAM_INTERFACE.append((torch.tensor([-0.05, 0.17]), torch.tensor(1.7), "torch")) -except ImportError: - pass - -H_ONE_QUBIT = np.array([[1.0, 0.5j], [-0.5j, 2.5]]) - -H_TWO_QUBITS = np.array( - [[0.5, 1.0j, 0.0, -3j], [-1.0j, -1.1, 0.0, -0.1], [0.0, 0.0, -0.9, 12.0], [3j, -0.1, 12.0, 0.0]] -) - -COEFFS = [(0.5, 1.2, -0.7), (2.2, -0.2, 0.0), (0.33,)] - -with qml.operation.disable_new_opmath_cm(warn=False): - - OBSERVABLES = [ - (qml.PauliZ(0), qml.PauliY(0), qml.PauliZ(1)), - (qml.PauliX(0) @ qml.PauliZ(1), qml.PauliY(0) @ qml.PauliZ(1), qml.PauliZ(1)), - (qml.Hermitian(H_TWO_QUBITS, [0, 1]),), - ] - - valid_hamiltonians = [ - ((1.0,), (qml.Hermitian(H_TWO_QUBITS, [0, 1]),)), - ((-0.8,), (qml.PauliZ(0),)), - ((0.6,), (qml.PauliX(0) @ qml.PauliX(1),)), - ((0.5, -1.6), (qml.PauliX(0), qml.PauliY(1))), - ((0.5, -1.6), (qml.PauliX(1), qml.PauliY(1))), - ((0.5, -1.6), (qml.PauliX("a"), qml.PauliY("b"))), - ((1.1, -0.4, 0.333), (qml.PauliX(0), qml.Hermitian(H_ONE_QUBIT, 2), qml.PauliZ(2))), - ((-0.4, 0.15), (qml.Hermitian(H_TWO_QUBITS, [0, 2]), qml.PauliZ(1))), - ([1.5, 2.0], [qml.PauliZ(0), qml.PauliY(2)]), - (np.array([-0.1, 0.5]), [qml.Hermitian(H_TWO_QUBITS, [0, 1]), qml.PauliY(0)]), - ((0.5, 1.2), (qml.PauliX(0), qml.PauliX(0) @ qml.PauliX(1))), - ((0.5 + 1.2j, 1.2 + 0.5j), (qml.PauliX(0), qml.PauliY(1))), - ((0.7 + 0j, 0 + 1.3j), (qml.PauliX(0), qml.PauliY(1))), - ] - - valid_hamiltonians_str = [ - " (1.0) [Hermitian0,1]", - " (-0.8) [Z0]", - " (0.6) [X0 X1]", - " (-1.6) [Y1]\n+ (0.5) [X0]", - " (-1.6) [Y1]\n+ (0.5) [X1]", - " (-1.6) [Yb]\n+ (0.5) [Xa]", - " (-0.4) [Hermitian2]\n+ (0.333) [Z2]\n+ (1.1) [X0]", - " (0.15) [Z1]\n+ (-0.4) [Hermitian0,2]", - " (1.5) [Z0]\n+ (2.0) [Y2]", - " (0.5) [Y0]\n+ (-0.1) [Hermitian0,1]", - " (0.5) [X0]\n+ (1.2) [X0 X1]", - " ((0.5+1.2j)) [X0]\n+ ((1.2+0.5j)) [Y1]", - " (1.3j) [Y1]\n+ ((0.7+0j)) [X0]", - ] - - valid_hamiltonians_repr = [ - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - ] - - invalid_hamiltonians = [ - ((), (qml.PauliZ(0),)), - ((), (qml.PauliZ(0), qml.PauliY(1))), - ((3.5,), ()), - ((1.2, -0.4), ()), - ((0.5, 1.2), (qml.PauliZ(0),)), - ((1.0,), (qml.PauliZ(0), qml.PauliY(0))), - ] - - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", "qml.ops.Hamiltonian uses", qml.PennyLaneDeprecationWarning - ) - - simplify_hamiltonians = [ - ( - qml.Hamiltonian( - [1, 1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliX(0), qml.PauliX(1)] - ), - qml.Hamiltonian([2, 1], [qml.PauliX(0), qml.PauliX(1)]), - ), - ( - qml.Hamiltonian( - [-1, 1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliX(0), qml.PauliX(1)] - ), - qml.Hamiltonian([1], [qml.PauliX(1)]), - ), - ( - qml.Hamiltonian( - [1, 0.5], - [ - qml.PauliX(0) @ qml.PauliY(1), - qml.PauliY(1) @ qml.Identity(2) @ qml.PauliX(0), - ], - ), - qml.Hamiltonian([1.5], [qml.PauliX(0) @ qml.PauliY(1)]), - ), - ( - qml.Hamiltonian( - [1, 1, 0.5], - [ - qml.Hermitian(np.array([[1, 0], [0, -1]]), "a"), - qml.PauliX("b") @ qml.PauliY(1.3), - qml.PauliY(1.3) @ qml.Identity(-0.9) @ qml.PauliX("b"), - ], - ), - qml.Hamiltonian( - [1, 1.5], - [ - qml.Hermitian(np.array([[1, 0], [0, -1]]), "a"), - qml.PauliX("b") @ qml.PauliY(1.3), - ], - ), - ), - # Simplifies to zero Hamiltonian - ( - qml.Hamiltonian( - [1, -0.5, -0.5], [qml.PauliX(0) @ qml.Identity(1), qml.PauliX(0), qml.PauliX(0)] - ), - qml.Hamiltonian([], []), - ), - ( - qml.Hamiltonian( - [1, -1], - [ - qml.PauliX(4) @ qml.Identity(0) @ qml.PauliX(1), - qml.PauliX(4) @ qml.PauliX(1), - ], - ), - qml.Hamiltonian([], []), - ), - ( - qml.Hamiltonian([0], [qml.Identity(0)]), - qml.Hamiltonian([0], [qml.Identity(0)]), - ), - ] - - equal_hamiltonians = [ - ( - qml.Hamiltonian([1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliZ(0)]), - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(0)]), - True, - ), - ( - qml.Hamiltonian( - [1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliY(2) @ qml.PauliZ(0)] - ), - qml.Hamiltonian( - [1, 1], [qml.PauliX(0), qml.PauliZ(0) @ qml.PauliY(2) @ qml.Identity(1)] - ), - True, - ), - ( - qml.Hamiltonian( - [1, 1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliZ(0), qml.Identity(1)] - ), - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(0)]), - False, - ), - ( - qml.Hamiltonian([1], [qml.PauliZ(0) @ qml.PauliX(1)]), - qml.PauliZ(0) @ qml.PauliX(1), - True, - ), - (qml.Hamiltonian([1], [qml.PauliZ(0)]), qml.PauliZ(0), True), - ( - qml.Hamiltonian( - [1, 1, 1], - [ - qml.Hermitian(np.array([[1, 0], [0, -1]]), "b") @ qml.Identity(7), - qml.PauliZ(3), - qml.Identity(1.2), - ], - ), - qml.Hamiltonian( - [1, 1, 1], - [ - qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), - qml.PauliZ(3), - qml.Identity(1.2), - ], - ), - True, - ), - ( - qml.Hamiltonian([1, 1], [qml.PauliZ(3) @ qml.Identity(1.2), qml.PauliZ(3)]), - qml.Hamiltonian([2], [qml.PauliZ(3)]), - True, - ), - ] - - add_hamiltonians = [ - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([0.5, 0.3, 1], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)]), - qml.Hamiltonian( - [1.5, 1.2, 1.1, 0.3], - [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)], - ), - ), - ( - qml.Hamiltonian( - [1.3, 0.2, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] - ), - qml.Hamiltonian( - [0.5, 0.3, 1.6], [qml.PauliX(0), qml.PauliX(1) @ qml.PauliX(0), qml.PauliX(2)] - ), - qml.Hamiltonian( - [1.6, 0.2, 2.3, 0.5], - [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2), qml.PauliX(0)], - ), - ), - ( - qml.Hamiltonian( - [1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] - ), - qml.Hamiltonian( - [0.5, 0.5], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] - ), - qml.Hamiltonian( - [1.5, 1.5], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] - ), - ), - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.PauliX(0) @ qml.Identity(1), - qml.Hamiltonian([2, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - ), - ( - qml.Hamiltonian( - [1.3, 0.2, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] - ), - qml.Hadamard(1), - qml.Hamiltonian( - [1.3, 1.2, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] - ), - ), - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX("b"), qml.PauliZ(3.1), qml.PauliX(1.6)]), - qml.PauliX("b") @ qml.Identity(5), - qml.Hamiltonian([2, 1.2, 0.1], [qml.PauliX("b"), qml.PauliZ(3.1), qml.PauliX(1.6)]), - ), - # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists - ( - qml.Hamiltonian((1, 1.2, 0.1), (qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2))), - qml.Hamiltonian( - np.array([0.5, 0.3, 1]), np.array([qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)]) - ), - qml.Hamiltonian( - (1.5, 1.2, 1.1, 0.3), - np.array([qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)]), - ), - ), - # Case where the 1st hamiltonian doesn't contain all wires - ( - qml.Hamiltonian([1.23, -3.45], [qml.PauliX(0), qml.PauliY(1)]), - qml.Hamiltonian([6.78], [qml.PauliZ(2)]), - qml.Hamiltonian([1.23, -3.45, 6.78], [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]), - ), - ] - - add_zero_hamiltonians = [ - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), - qml.Hamiltonian( - [1.5, 1.2, 1.1, 0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)] - ), - ] - - iadd_zero_hamiltonians = [ - # identical hamiltonians - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - ), - ( - qml.Hamiltonian( - [1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] - ), - qml.Hamiltonian( - [1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] - ), - ), - ( - qml.Hamiltonian( - [1.5, 1.2, 1.1, 0.3], - [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)], - ), - qml.Hamiltonian( - [1.5, 1.2, 1.1, 0.3], - [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)], - ), - ), - ] - - sub_hamiltonians = [ - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([0.5, 0.3, 1.6], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)]), - qml.Hamiltonian( - [0.5, 1.2, -1.5, -0.3], - [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)], - ), - ), - ( - qml.Hamiltonian( - [1.3, 0.2, 1], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] - ), - qml.Hamiltonian( - [0.5, 0.3, 1], [qml.PauliX(0), qml.PauliX(1) @ qml.PauliX(0), qml.PauliX(2)] - ), - qml.Hamiltonian( - [1, 0.2, -0.5], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(0)] - ), - ), - ( - qml.Hamiltonian( - [1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] - ), - qml.Hamiltonian( - [0.5, 0.5], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] - ), - qml.Hamiltonian( - [0.5, 0.5], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] - ), - ), - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.PauliX(0) @ qml.Identity(1), - qml.Hamiltonian([1.2, 0.1], [qml.PauliZ(1), qml.PauliX(2)]), - ), - ( - qml.Hamiltonian( - [1.3, 0.2, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] - ), - qml.Hadamard(1), - qml.Hamiltonian( - [1.3, -0.8, 0.7], - [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)], - ), - ), - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX("b"), qml.PauliZ(3.1), qml.PauliX(1.6)]), - qml.PauliX("b") @ qml.Identity(1), - qml.Hamiltonian([1.2, 0.1], [qml.PauliZ(3.1), qml.PauliX(1.6)]), - ), - # The result is the zero Hamiltonian - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([], []), - ), - ( - qml.Hamiltonian([1, 2], [qml.PauliX(4), qml.PauliZ(2)]), - qml.Hamiltonian([1, 2], [qml.PauliX(4), qml.PauliZ(2)]), - qml.Hamiltonian([], []), - ), - # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists - ( - qml.Hamiltonian((1, 1.2, 0.1), (qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2))), - qml.Hamiltonian( - np.array([0.5, 0.3, 1.6]), - np.array([qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)]), - ), - qml.Hamiltonian( - (0.5, 1.2, -1.5, -0.3), - np.array([qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)]), - ), - ), - # Case where the 1st hamiltonian doesn't contain all wires - ( - qml.Hamiltonian([1.23, -3.45], [qml.PauliX(0), qml.PauliY(1)]), - qml.Hamiltonian([6.78], [qml.PauliZ(2)]), - qml.Hamiltonian( - [1.23, -3.45, -6.78], [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)] - ), - ), - ] - - mul_hamiltonians = [ - ( - 0.5, - qml.Hamiltonian( - [1, 2], [qml.PauliX(0), qml.PauliZ(1)] - ), # Case where the types of the coefficient and the scalar differ - qml.Hamiltonian([0.5, 1.0], [qml.PauliX(0), qml.PauliZ(1)]), - ), - ( - 3, - qml.Hamiltonian([1.5, 0.5], [qml.PauliX(0), qml.PauliZ(1)]), - qml.Hamiltonian([4.5, 1.5], [qml.PauliX(0), qml.PauliZ(1)]), - ), - ( - -1.3, - qml.Hamiltonian([1, -0.3], [qml.PauliX(0), qml.PauliZ(1) @ qml.PauliZ(2)]), - qml.Hamiltonian([-1.3, 0.39], [qml.PauliX(0), qml.PauliZ(1) @ qml.PauliZ(2)]), - ), - ( - -1.3, - qml.Hamiltonian( - [1, -0.3], - [ - qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), - qml.PauliZ(23) @ qml.PauliZ(0), - ], - ), - qml.Hamiltonian( - [-1.3, 0.39], - [ - qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), - qml.PauliZ(23) @ qml.PauliZ(0), - ], - ), - ), - # The result is the zero Hamiltonian - ( - 0, - qml.Hamiltonian([1], [qml.PauliX(0)]), - qml.Hamiltonian([0], [qml.PauliX(0)]), - ), - ( - 0, - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([0, 0, 0], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - ), - # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists - ( - 3, - qml.Hamiltonian((1.5, 0.5), (qml.PauliX(0), qml.PauliZ(1))), - qml.Hamiltonian(np.array([4.5, 1.5]), np.array([qml.PauliX(0), qml.PauliZ(1)])), - ), - ] - - matmul_hamiltonians = [ - ( - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]), - qml.Hamiltonian([0.5, 0.5], [qml.PauliZ(2), qml.PauliZ(3)]), - qml.Hamiltonian( - [0.5, 0.5, 0.5, 0.5], - [ - qml.PauliX(0) @ qml.PauliZ(2), - qml.PauliX(0) @ qml.PauliZ(3), - qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliZ(1) @ qml.PauliZ(3), - ], - ), - ), - ( - qml.Hamiltonian([0.5, 0.25], [qml.PauliX(0) @ qml.PauliX(1), qml.PauliZ(0)]), - qml.Hamiltonian([1, 1], [qml.PauliX(3) @ qml.PauliZ(2), qml.PauliZ(2)]), - qml.Hamiltonian( - [0.5, 0.5, 0.25, 0.25], - [ - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(3) @ qml.PauliZ(2), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliZ(2), - qml.PauliZ(0) @ qml.PauliX(3) @ qml.PauliZ(2), - qml.PauliZ(0) @ qml.PauliZ(2), - ], - ), - ), - ( - qml.Hamiltonian( - [1, 1], [qml.PauliX("b"), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] - ), - qml.Hamiltonian([2, 2], [qml.PauliZ(1.2), qml.PauliY("c")]), - qml.Hamiltonian( - [2, 2, 2, 2], - [ - qml.PauliX("b") @ qml.PauliZ(1.2), - qml.PauliX("b") @ qml.PauliY("c"), - qml.Hermitian(np.array([[1, 0], [0, -1]]), 0) @ qml.PauliZ(1.2), - qml.Hermitian(np.array([[1, 0], [0, -1]]), 0) @ qml.PauliY("c"), - ], - ), - ), - ( - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]), - qml.PauliX(2), - qml.Hamiltonian( - [1, 1], [qml.PauliX(0) @ qml.PauliX(2), qml.PauliZ(1) @ qml.PauliX(2)] - ), - ), - # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists - ( - qml.Hamiltonian((1, 1), (qml.PauliX(0), qml.PauliZ(1))), - qml.Hamiltonian(np.array([0.5, 0.5]), np.array([qml.PauliZ(2), qml.PauliZ(3)])), - qml.Hamiltonian( - (0.5, 0.5, 0.5, 0.5), - np.array( - [ - qml.PauliX(0) @ qml.PauliZ(2), - qml.PauliX(0) @ qml.PauliZ(3), - qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliZ(1) @ qml.PauliZ(3), - ] - ), - ), - ), - ] - - rmatmul_hamiltonians = [ - ( - qml.Hamiltonian([0.5, 0.5], [qml.PauliZ(2), qml.PauliZ(3)]), - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]), - qml.Hamiltonian( - [0.5, 0.5, 0.5, 0.5], - [ - qml.PauliX(0) @ qml.PauliZ(2), - qml.PauliX(0) @ qml.PauliZ(3), - qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliZ(1) @ qml.PauliZ(3), - ], - ), - ), - ( - qml.Hamiltonian([1, 1], [qml.PauliX(3) @ qml.PauliZ(2), qml.PauliZ(2)]), - qml.Hamiltonian([0.5, 0.25], [qml.PauliX(0) @ qml.PauliX(1), qml.PauliZ(0)]), - qml.Hamiltonian( - [0.5, 0.5, 0.25, 0.25], - [ - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(3) @ qml.PauliZ(2), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliZ(2), - qml.PauliZ(0) @ qml.PauliX(3) @ qml.PauliZ(2), - qml.PauliZ(0) @ qml.PauliZ(2), - ], - ), - ), - ( - qml.Hamiltonian([2, 2], [qml.PauliZ(1.2), qml.PauliY("c")]), - qml.Hamiltonian( - [1, 1], [qml.PauliX("b"), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] - ), - qml.Hamiltonian( - [2, 2, 2, 2], - [ - qml.PauliX("b") @ qml.PauliZ(1.2), - qml.PauliX("b") @ qml.PauliY("c"), - qml.Hermitian(np.array([[1, 0], [0, -1]]), 0) @ qml.PauliZ(1.2), - qml.Hermitian(np.array([[1, 0], [0, -1]]), 0) @ qml.PauliY("c"), - ], - ), - ), - ( - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]), - qml.PauliX(2), - qml.Hamiltonian( - [1, 1], [qml.PauliX(2) @ qml.PauliX(0), qml.PauliX(2) @ qml.PauliZ(1)] - ), - ), - # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists - ( - qml.Hamiltonian(np.array([0.5, 0.5]), np.array([qml.PauliZ(2), qml.PauliZ(3)])), - qml.Hamiltonian((1, 1), (qml.PauliX(0), qml.PauliZ(1))), - qml.Hamiltonian( - (0.5, 0.5, 0.5, 0.5), - np.array( - [ - qml.PauliX(0) @ qml.PauliZ(2), - qml.PauliX(0) @ qml.PauliZ(3), - qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliZ(1) @ qml.PauliZ(3), - ] - ), - ), - ), - ] - - big_hamiltonian_coeffs = np.array( - [ - -0.04207898, - 0.17771287, - 0.17771287, - -0.24274281, - -0.24274281, - 0.17059738, - 0.04475014, - -0.04475014, - -0.04475014, - 0.04475014, - 0.12293305, - 0.16768319, - 0.16768319, - 0.12293305, - 0.17627641, - ] - ) - - big_hamiltonian_ops = [ - qml.Identity(wires=[0]), - qml.PauliZ(wires=[0]), - qml.PauliZ(wires=[1]), - qml.PauliZ(wires=[2]), - qml.PauliZ(wires=[3]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[1]), - qml.PauliY(wires=[0]) - @ qml.PauliX(wires=[1]) - @ qml.PauliX(wires=[2]) - @ qml.PauliY(wires=[3]), - qml.PauliY(wires=[0]) - @ qml.PauliY(wires=[1]) - @ qml.PauliX(wires=[2]) - @ qml.PauliX(wires=[3]), - qml.PauliX(wires=[0]) - @ qml.PauliX(wires=[1]) - @ qml.PauliY(wires=[2]) - @ qml.PauliY(wires=[3]), - qml.PauliX(wires=[0]) - @ qml.PauliY(wires=[1]) - @ qml.PauliY(wires=[2]) - @ qml.PauliX(wires=[3]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[2]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[3]), - qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[2]), - qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[3]), - qml.PauliZ(wires=[2]) @ qml.PauliZ(wires=[3]), - ] - - big_hamiltonian = qml.Hamiltonian(big_hamiltonian_coeffs, big_hamiltonian_ops) - - big_hamiltonian_grad = ( - np.array( - [ - [ - [6.52084595e-18, -2.11464420e-02, -1.16576858e-02], - [-8.22589330e-18, -5.20597922e-02, -1.85365365e-02], - [-2.73850768e-17, 1.14202988e-01, -5.45041403e-03], - [-1.27514307e-17, -1.10465531e-01, 5.19489457e-02], - ], - [ - [-2.45428288e-02, 8.38921555e-02, -2.00641818e-17], - [-2.21085973e-02, 7.39332741e-04, -1.25580654e-17], - [9.62058625e-03, -1.51398765e-01, 2.02129847e-03], - [1.10020832e-03, -3.49066271e-01, 2.13669117e-03], - ], - ] - ), - ) - - -def circuit1(param): - """First Pauli subcircuit""" - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval(qml.PauliX(0)) - - -def circuit2(param): - """Second Pauli subcircuit""" - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval(qml.PauliZ(0)) - - -dev = qml.device("default.qubit", wires=2) - - -@pytest.mark.usefixtures("use_legacy_and_new_opmath") -def test_matmul_queuing(): - """Test that the other and self are removed during Hamiltonian.__matmul__ .""" - - with qml.queuing.AnnotatedQueue() as q: - H = 0.5 * qml.X(0) @ qml.Y(1) - - assert len(q) == 1 - assert q.queue[0] is H - - -@pytest.mark.usefixtures("use_legacy_and_new_opmath") -def test_deprecation(): - """Test that a warning is raised if attempting to create a legacy Hamiltonian.""" - with pytest.warns( - qml.PennyLaneDeprecationWarning, - match="qml.ops.Hamiltonian uses the old approach", - ): - _ = qml.ops.Hamiltonian([1.0], [qml.X(0)]) - - -@pytest.mark.usefixtures("legacy_opmath_only") -class TestHamiltonian: - """Test the Hamiltonian class""" - - @pytest.mark.parametrize("coeffs, ops", valid_hamiltonians) - def test_hamiltonian_valid_init(self, coeffs, ops): - """Tests that the Hamiltonian object is created with - the correct attributes""" - - H = qml.Hamiltonian(coeffs, ops) - assert np.allclose(H.terms()[0], coeffs) - assert H.terms()[1] == list(ops) - - @pytest.mark.parametrize("coeffs, ops", invalid_hamiltonians) - def test_hamiltonian_invalid_init_exception(self, coeffs, ops): - """Tests that an exception is raised when giving an invalid - combination of coefficients and ops""" - with pytest.raises(ValueError, match="number of coefficients and operators does not match"): - qml.Hamiltonian(coeffs, ops) - - @pytest.mark.parametrize( - "obs", [[qml.PauliX(0), qml.CNOT(wires=[0, 1])], [qml.PauliZ, qml.PauliZ(0)]] - ) - def test_hamiltonian_invalid_observables(self, obs): - """Tests that an exception is raised when - a complex Hamiltonian is given""" - coeffs = [0.1, 0.2] - - with pytest.raises(ValueError, match="observables are not valid"): - qml.Hamiltonian(coeffs, obs) - - # pylint: disable=protected-access - @pytest.mark.parametrize("coeffs, ops", valid_hamiltonians) - @pytest.mark.parametrize("grouping_type", (None, "qwc")) - def test_flatten_unflatten(self, coeffs, ops, grouping_type): - """Test the flatten and unflatten methods for hamiltonians""" - assert not qml.operation.active_new_opmath() - if any(not qml.pauli.is_pauli_word(t) for t in ops) and grouping_type: - pytest.skip("grouping type must be none if a term is not a pauli word.") - - H = qml.Hamiltonian(coeffs, ops, grouping_type=grouping_type) - data, metadata = H._flatten() - assert metadata[0] == H.grouping_indices - assert hash(metadata) - assert len(data) == 2 - assert data[0] is H.data - assert data[1] is H._ops - - new_H = qml.Hamiltonian._unflatten(*H._flatten()) - qml.assert_equal(H, new_H) - assert new_H.grouping_indices == H.grouping_indices - - @pytest.mark.parametrize("coeffs, ops", valid_hamiltonians) - def test_hamiltonian_wires(self, coeffs, ops): - """Tests that the Hamiltonian object has correct wires.""" - H = qml.Hamiltonian(coeffs, ops) - assert set(H.wires) == set(w for op in H.ops for w in op.wires) - - def test_label(self): - """Tests the label method of Hamiltonian when <=3 coefficients.""" - H = qml.Hamiltonian((-0.8,), (qml.PauliZ(0),)) - assert H.label() == "𝓗" - assert H.label(decimals=2) == "𝓗\n(-0.80)" - - def test_label_many_coefficients(self): - """Tests the label method of Hamiltonian when >3 coefficients.""" - H = ( - 0.1 * qml.PauliX(0) - + 0.1 * qml.PauliY(1) - + 0.3 * qml.PauliZ(0) @ qml.PauliX(1) - + 0.4 * qml.PauliX(3) - ) - assert H.label() == "𝓗" - assert H.label(decimals=2) == "𝓗" - - @pytest.mark.parametrize("terms, string", zip(valid_hamiltonians, valid_hamiltonians_str)) - def test_hamiltonian_str(self, terms, string): - """Tests that the __str__ function for printing is correct""" - H = qml.Hamiltonian(*terms) - assert H.__str__() == string - - @patch("builtins.print") - def test_small_hamiltonian_ipython_display(self, mock_print): - """Test that the ipython_dipslay method prints __str__.""" - # pylint: disable=protected-access - H = 1.0 * qml.PauliX(0) - H._ipython_display_() - mock_print.assert_called_with(str(H)) - - @patch("builtins.print") - def test_big_hamiltonian_ipython_display(self, mock_print): - """Test that the ipython_display method prints __repr__ when H has more than 15 terms.""" - # pylint: disable=protected-access - H = qml.Hamiltonian([1] * 16, [qml.PauliX(i) for i in range(16)]) - H._ipython_display_() - mock_print.assert_called_with(repr(H)) - - @pytest.mark.parametrize("terms, string", zip(valid_hamiltonians, valid_hamiltonians_repr)) - def test_hamiltonian_repr(self, terms, string): - """Tests that the __repr__ function for printing is correct""" - H = qml.Hamiltonian(*terms) - assert H.__repr__() == string - - def test_hamiltonian_name(self): - """Tests the name property of the Hamiltonian class""" - H = qml.Hamiltonian([], []) - assert H.name == "Hamiltonian" - - @pytest.mark.parametrize(("old_H", "new_H"), simplify_hamiltonians) - def test_simplify(self, old_H, new_H): - """Tests the simplify method""" - old_H.simplify() - assert old_H.compare(new_H) - - def test_simplify_while_queueing(self): - """Tests that simplifying a Hamiltonian in a tape context - queues the simplified Hamiltonian.""" - - with qml.queuing.AnnotatedQueue() as q: - a = qml.PauliX(wires=0) - b = qml.PauliY(wires=1) - c = qml.Identity(wires=2) - d = b @ c - H = qml.Hamiltonian([1.0, 2.0], [a, d]) - H.simplify() - - # check that H is simplified - assert H.ops == [a, b] - # check that the simplified Hamiltonian is in the queue - assert q.get_info(H) is not None - - def test_data(self): - """Tests the obs_data method""" - # pylint: disable=protected-access - - H = qml.Hamiltonian( - [1, 1, 0.5], - [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1), qml.PauliX(2) @ qml.Identity(1)], - ) - data = H._obs_data() - - assert data == { - (1, frozenset([("PauliZ", qml.wires.Wires(0), ())])), - ( - 1, - frozenset([("PauliZ", qml.wires.Wires(0), ()), ("PauliX", qml.wires.Wires(1), ())]), - ), - (0.5, frozenset([("PauliX", qml.wires.Wires(2), ())])), - } - - def test_data_gell_mann(self): - """Tests that the obs_data method for Hamiltonians with qml.GellMann - observables includes the Gell-Mann index.""" - H = qml.Hamiltonian( - [1, -1, 0.5], - [ - qml.GellMann(wires=0, index=3), - qml.GellMann(wires=0, index=3) @ qml.GellMann(wires=1, index=1), - qml.GellMann(wires=2, index=2), - ], - ) - data = H._obs_data() - - assert data == { - (1, frozenset([("GellMann", qml.wires.Wires(0), (3,))])), - ( - -1, - frozenset( - [("GellMann", qml.wires.Wires(0), (3,)), ("GellMann", qml.wires.Wires(1), (1,))] - ), - ), - (0.5, frozenset([("GellMann", qml.wires.Wires(2), (2,))])), - } - - def test_compare_gell_mann(self): - """Tests that the compare method returns the correct result for Hamiltonians - with qml.GellMann present.""" - H1 = qml.Hamiltonian([1], [qml.GellMann(wires=2, index=2)]) - H2 = qml.Hamiltonian([1], [qml.GellMann(wires=2, index=1) @ qml.GellMann(wires=1, index=2)]) - H3 = qml.Hamiltonian([1], [qml.GellMann(wires=2, index=1)]) - H4 = qml.Hamiltonian([1], [qml.GellMann(wires=2, index=1) @ qml.GellMann(wires=1, index=3)]) - - assert H1.compare(qml.GellMann(wires=2, index=2)) is True - assert H1.compare(qml.GellMann(wires=2, index=1)) is False - assert H1.compare(H3) is False - assert H2.compare(qml.GellMann(wires=2, index=1) @ qml.GellMann(wires=1, index=2)) is True - assert H2.compare(qml.GellMann(wires=2, index=2) @ qml.GellMann(wires=1, index=2)) is False - assert H2.compare(H4) is False - - def test_hamiltonian_equal_error(self): - """Tests that the correct error is raised when compare() is called on invalid type""" - - H = qml.Hamiltonian([1], [qml.PauliZ(0)]) - with pytest.raises( - ValueError, - match=r"Can only compare a Hamiltonian, and a Hamiltonian/Observable/Tensor.", - ): - H.compare([[1, 0], [0, -1]]) - - @pytest.mark.parametrize(("H1", "H2", "res"), equal_hamiltonians) - def test_hamiltonian_equal(self, H1, H2, res): - """Tests that equality can be checked between Hamiltonians""" - assert H1.compare(H2) == res - - @pytest.mark.parametrize(("H1", "H2", "H"), add_hamiltonians) - def test_hamiltonian_add(self, H1, H2, H): - """Tests that Hamiltonians are added correctly""" - assert H.compare(H1 + H2) - - @pytest.mark.parametrize("H", add_zero_hamiltonians) - def test_hamiltonian_add_zero(self, H): - """Tests that Hamiltonians can be added to zero""" - assert H.compare(H + 0) - assert H.compare(0 + H) - assert H.compare(H + 0.0) - assert H.compare(0.0 + H) - assert H.compare(H + 0e1) - assert H.compare(0e1 + H) - - @pytest.mark.parametrize(("coeff", "H", "res"), mul_hamiltonians) - def test_hamiltonian_mul(self, coeff, H, res): - """Tests that scalars and Hamiltonians are multiplied correctly""" - assert res.compare(coeff * H) - assert res.compare(H * coeff) - - def test_hamiltonian_mul_coeff_cast(self): - """Test that the coefficients are correct when the type of the existing - and the new coefficients differ.""" - h = 0.5 * (qml.PauliX(0) @ qml.PauliX(0) + qml.PauliY(0) @ qml.PauliY(1)) - assert np.all(h.coeffs == np.array([0.5, 0.5])) - - @pytest.mark.parametrize(("H1", "H2", "H"), sub_hamiltonians) - def test_hamiltonian_sub(self, H1, H2, H): - """Tests that Hamiltonians are subtracted correctly""" - assert H.compare(H1 - H2) - - def test_hamiltonian_tensor_matmul(self): - """Tests that a hamiltonian can be multiplied by a tensor.""" - H = qml.PauliX(0) + qml.PauliY(0) - t = qml.PauliZ(1) @ qml.PauliZ(2) - out = H @ t - - expected = qml.Hamiltonian( - [1, 1], - [ - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliY(0) @ qml.PauliZ(1) @ qml.PauliZ(2), - ], - ) - qml.assert_equal(out, expected) - - @pytest.mark.parametrize(("H1", "H2", "H"), matmul_hamiltonians) - def test_hamiltonian_matmul(self, H1, H2, H): - """Tests that Hamiltonians are tensored correctly""" - assert H.compare(H1 @ H2) - - @pytest.mark.parametrize(("H1", "H2", "H"), rmatmul_hamiltonians) - def test_hamiltonian_rmatmul(self, H1, H2, H): - """Tests that Hamiltonians are tensored correctly when using __rmatmul__""" - assert H.compare(H1.__rmatmul__(H2)) - - def test_hamiltonian_same_wires(self): - """Test if a ValueError is raised when multiplication between Hamiltonians acting on the - same wires is attempted""" - h1 = qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(1)]) - - with pytest.raises(ValueError, match="Hamiltonians can only be multiplied together if"): - _ = h1 @ h1 - - @pytest.mark.parametrize(("H1", "H2", "H"), add_hamiltonians) - def test_hamiltonian_iadd(self, H1, H2, H): - """Tests that Hamiltonians are added inline correctly""" - H1 += H2 - assert H.compare(H1) - assert H.wires == H1.wires - - @pytest.mark.parametrize(("H1", "H2"), iadd_zero_hamiltonians) - def test_hamiltonian_iadd_zero(self, H1, H2): - """Tests in-place addition between Hamiltonians and zero""" - H1 += 0 - assert H1.compare(H2) - H1 += 0.0 - assert H1.compare(H2) - H1 += 0e1 - assert H1.compare(H2) - - @pytest.mark.parametrize(("coeff", "H", "res"), mul_hamiltonians) - def test_hamiltonian_imul(self, coeff, H, res): - """Tests that scalars and Hamiltonians are multiplied inline correctly""" - H *= coeff - assert res.compare(H) - - @pytest.mark.parametrize(("H1", "H2", "H"), sub_hamiltonians) - def test_hamiltonian_isub(self, H1, H2, H): - """Tests that Hamiltonians are subtracted inline correctly""" - H1 -= H2 - assert H.compare(H1) - assert H.wires == H1.wires - - def test_arithmetic_errors(self): - """Tests that the arithmetic operations thrown the correct errors""" - H = qml.Hamiltonian([1], [qml.PauliZ(0)]) - A = [[1, 0], [0, -1]] - with pytest.raises(TypeError, match="unsupported operand type"): - _ = H @ A - with pytest.raises(TypeError, match="unsupported operand type"): - _ = A @ H - with pytest.raises(TypeError, match="unsupported operand type"): - _ = H + A - with pytest.raises(TypeError, match="can't multiply sequence by non-int"): - _ = H * A - with pytest.raises(TypeError, match="unsupported operand type"): - _ = H - A - with pytest.raises(TypeError, match="unsupported operand type"): - H += A - with pytest.raises(TypeError, match="unsupported operand type"): - H *= A - with pytest.raises(TypeError, match="unsupported operand type"): - H -= A - - def test_hamiltonian_queue_outside(self): - """Tests that Hamiltonian are queued correctly when components are defined outside the recording context.""" - - H = qml.PauliX(1) + 3 * qml.PauliZ(0) @ qml.PauliZ(2) + qml.PauliZ(1) - - with qml.queuing.AnnotatedQueue() as q: - qml.Hadamard(wires=1) - qml.PauliX(wires=0) - qml.expval(H) - - assert len(q.queue) == 3 - assert isinstance(q.queue[0], qml.Hadamard) - assert isinstance(q.queue[1], qml.PauliX) - assert isinstance(q.queue[2], qml.measurements.MeasurementProcess) - assert H.compare(q.queue[2].obs) - - def test_hamiltonian_queue_inside(self): - """Tests that Hamiltonian are queued correctly when components are instantiated inside the recording context.""" - - with qml.queuing.AnnotatedQueue() as q: - m = qml.expval( - qml.Hamiltonian( - [1, 3, 1], [qml.PauliX(1), qml.PauliZ(0) @ qml.PauliZ(2), qml.PauliZ(1)] - ) - ) - - assert len(q.queue) == 1 - assert q.queue[0] is m - - def test_terms(self): - """Tests that the terms representation is returned correctly.""" - coeffs = pnp.array([1.0, 2.0], requires_grad=True) - ops = [qml.PauliX(0), qml.PauliZ(1)] - h = qml.Hamiltonian(coeffs, ops) - c, o = h.terms() - assert isinstance(c, Iterable) - assert isinstance(o, list) - assert all(isinstance(item, np.ndarray) for item in c) - assert all(item.requires_grad for item in c) - assert all(isinstance(item, qml.operation.Operator) for item in o) - - def test_hamiltonian_no_empty_wire_list_error(self): - """Test that empty Hamiltonian does not raise an empty wire error.""" - hamiltonian = qml.Hamiltonian([], []) - assert isinstance(hamiltonian, qml.Hamiltonian) - - def test_map_wires(self): - """Test the map_wires method.""" - coeffs = pnp.array([1.0, 2.0, -3.0], requires_grad=True) - ops = [qml.PauliX(0), qml.PauliZ(1), qml.PauliY(2)] - h = qml.Hamiltonian(coeffs, ops) - wire_map = {0: 10, 1: 11, 2: 12} - mapped_h = h.map_wires(wire_map=wire_map) - final_obs = [qml.PauliX(10), qml.PauliZ(11), qml.PauliY(12)] - assert h is not mapped_h - assert h.wires == Wires([0, 1, 2]) - assert mapped_h.wires == Wires([10, 11, 12]) - for obs1, obs2 in zip(mapped_h.ops, final_obs): - qml.assert_equal(obs1, obs2) - for coeff1, coeff2 in zip(mapped_h.coeffs, h.coeffs): - assert coeff1 == coeff2 - - def test_hermitian_tensor_prod(self): - """Test that the tensor product of a Hamiltonian with Hermitian observable works.""" - tensor = qml.PauliX(0) @ qml.PauliX(1) - herm = qml.Hermitian([[1, 0], [0, 1]], wires=4) - - ham = qml.Hamiltonian([1.0, 1.0], [tensor, qml.PauliX(2)]) @ qml.Hamiltonian([1.0], [herm]) - assert isinstance(ham, qml.Hamiltonian) - - def test_hamiltonian_pauli_rep(self): - """Test that the pauli rep is set for a hamiltonian that is a linear combination of paulis.""" - h = qml.Hamiltonian([1.0, 2.0], [qml.X(0) @ qml.Y(1), qml.Z(0) @ qml.Z(2)]) - - pw1 = qml.pauli.PauliWord({0: "X", 1: "Y"}) - pw2 = qml.pauli.PauliWord({0: "Z", 2: "Z"}) - ps = 1.0 * pw1 + 2.0 * pw2 - assert h.pauli_rep == ps - - def test_hamiltonian_no_pauli_rep(self): - """Test that the pauli_rep for a hamiltonian is None if it is not a linear combination of paulis.""" - h = qml.Hamiltonian([1.0, 2.0], [qml.X(0), qml.Hermitian(np.eye(2), 2)]) - assert h.pauli_rep is None - - -@pytest.mark.usefixtures("legacy_opmath_only") -class TestHamiltonianCoefficients: - """Test the creation of a Hamiltonian""" - - @pytest.mark.parametrize("coeffs", [el[0] for el in COEFFS_PARAM_INTERFACE]) - def test_creation_different_coeff_types(self, coeffs): - """Check that Hamiltonian's coefficients and data attributes are set correctly.""" - H = qml.Hamiltonian(coeffs, [qml.PauliX(0), qml.PauliZ(0)]) - assert np.allclose(coeffs, H.coeffs) - assert np.allclose([coeffs[i] for i in range(qml.math.shape(coeffs)[0])], H.data) - - @pytest.mark.parametrize("coeffs", [el[0] for el in COEFFS_PARAM_INTERFACE]) - def test_simplify(self, coeffs): - """Test that simplify works with different coefficient types.""" - H1 = qml.Hamiltonian(coeffs, [qml.PauliX(0), qml.PauliZ(1)]) - H2 = qml.Hamiltonian(coeffs, [qml.PauliX(0), qml.Identity(0) @ qml.PauliZ(1)]) - H2.simplify() - assert H1.compare(H2) - assert H1.data == H2.data - - -@pytest.mark.tf -@pytest.mark.usefixtures("legacy_opmath_only") -class TestHamiltonianArithmeticTF: - """Tests creation of Hamiltonians using arithmetic - operations with TensorFlow tensor coefficients.""" - - def test_hamiltonian_equal(self): - """Tests equality""" - coeffs = tf.Variable([0.5, -1.6]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = tf.Variable([-1.6, 0.5]) - obs2 = [qml.PauliY(1), qml.PauliX(0)] - H2 = qml.Hamiltonian(coeffs2, obs2) - - assert H1.compare(H2) - - def test_hamiltonian_add(self): - """Tests that Hamiltonians are added correctly""" - coeffs = tf.Variable([0.5, -1.6]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = tf.Variable([0.5, -0.4]) - H2 = qml.Hamiltonian(coeffs2, obs) - - coeffs_expected = tf.Variable([1.0, -2.0]) - H = qml.Hamiltonian(coeffs_expected, obs) - - assert H.compare(H1 + H2) - - H1 += H2 - assert H.compare(H1) - - def test_hamiltonian_sub(self): - """Tests that Hamiltonians are subtracted correctly""" - coeffs = tf.Variable([1.0, -2.0]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = tf.Variable([0.5, -0.4]) - H2 = qml.Hamiltonian(coeffs2, obs) - - coeffs_expected = tf.Variable([0.5, -1.6]) - H = qml.Hamiltonian(coeffs_expected, obs) - - assert H.compare(H1 - H2) - - H1 -= H2 - assert H.compare(H1) - - def test_hamiltonian_matmul(self): - """Tests that Hamiltonians are tensored correctly""" - coeffs = tf.Variable([1.0, 2.0]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = tf.Variable([-1.0, -2.0]) - obs2 = [qml.PauliX(2), qml.PauliY(3)] - H2 = qml.Hamiltonian(coeffs2, obs2) - - coeffs_expected = tf.Variable([-4.0, -2.0, -2.0, -1.0]) - obs_expected = [ - qml.PauliY(1) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliY(3), - qml.PauliX(2) @ qml.PauliY(1), - qml.PauliX(0) @ qml.PauliX(2), - ] - H = qml.Hamiltonian(coeffs_expected, obs_expected) - - assert H.compare(H1 @ H2) - - -@pytest.mark.usefixtures("legacy_opmath_only") -class TestHamiltonianArithmeticTorch: - """Tests creation of Hamiltonians using arithmetic - operations with torch tensor coefficients.""" - - @pytest.mark.torch - def test_hamiltonian_equal(self): - """Tests equality""" - coeffs = torch.tensor([0.5, -1.6]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = torch.tensor([-1.6, 0.5]) - obs2 = [qml.PauliY(1), qml.PauliX(0)] - H2 = qml.Hamiltonian(coeffs2, obs2) - - assert H1.compare(H2) - - @pytest.mark.torch - def test_hamiltonian_add(self): - """Tests that Hamiltonians are added correctly""" - coeffs = torch.tensor([0.5, -1.6]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = torch.tensor([0.5, -0.4]) - H2 = qml.Hamiltonian(coeffs2, obs) - - coeffs_expected = torch.tensor([1.0, -2.0]) - H = qml.Hamiltonian(coeffs_expected, obs) - - assert H.compare(H1 + H2) - - H1 += H2 - assert H.compare(H1) - - @pytest.mark.torch - def test_hamiltonian_sub(self): - """Tests that Hamiltonians are subtracted correctly""" - coeffs = torch.tensor([1.0, -2.0]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = torch.tensor([0.5, -0.4]) - H2 = qml.Hamiltonian(coeffs2, obs) - - coeffs_expected = torch.tensor([0.5, -1.6]) - H = qml.Hamiltonian(coeffs_expected, obs) - - assert H.compare(H1 - H2) - - H1 -= H2 - assert H.compare(H1) - - @pytest.mark.torch - def test_hamiltonian_matmul(self): - """Tests that Hamiltonians are tensored correctly""" - coeffs = torch.tensor([1.0, 2.0]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = torch.tensor([-1.0, -2.0]) - obs2 = [qml.PauliX(2), qml.PauliY(3)] - H2 = qml.Hamiltonian(coeffs2, obs2) - - coeffs_expected = torch.tensor([-4.0, -2.0, -2.0, -1.0]) - obs_expected = [ - qml.PauliY(1) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliY(3), - qml.PauliX(2) @ qml.PauliY(1), - qml.PauliX(0) @ qml.PauliX(2), - ] - H = qml.Hamiltonian(coeffs_expected, obs_expected) - - assert H.compare(H1 @ H2) - - -@pytest.mark.usefixtures("legacy_opmath_only") -class TestHamiltonianArithmeticAutograd: - """Tests creation of Hamiltonians using arithmetic - operations with autograd tensor coefficients.""" - - @pytest.mark.autograd - def test_hamiltonian_equal(self): - """Tests equality""" - coeffs = pnp.array([0.5, -1.6]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = pnp.array([-1.6, 0.5]) - obs2 = [qml.PauliY(1), qml.PauliX(0)] - H2 = qml.Hamiltonian(coeffs2, obs2) - - assert H1.compare(H2) - - @pytest.mark.autograd - def test_hamiltonian_add(self): - """Tests that Hamiltonians are added correctly""" - coeffs = pnp.array([0.5, -1.6]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = pnp.array([0.5, -0.4]) - H2 = qml.Hamiltonian(coeffs2, obs) - - coeffs_expected = pnp.array([1.0, -2.0]) - H = qml.Hamiltonian(coeffs_expected, obs) - - assert H.compare(H1 + H2) - - H1 += H2 - assert H.compare(H1) - - @pytest.mark.autograd - def test_hamiltonian_sub(self): - """Tests that Hamiltonians are subtracted correctly""" - coeffs = pnp.array([1.0, -2.0]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = pnp.array([0.5, -0.4]) - H2 = qml.Hamiltonian(coeffs2, obs) - - coeffs_expected = pnp.array([0.5, -1.6]) - H = qml.Hamiltonian(coeffs_expected, obs) - - assert H.compare(H1 - H2) - - H1 -= H2 - assert H.compare(H1) - - @pytest.mark.autograd - def test_hamiltonian_matmul(self): - """Tests that Hamiltonians are tensored correctly""" - coeffs = pnp.array([1.0, 2.0]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = pnp.array([-1.0, -2.0]) - obs2 = [qml.PauliX(2), qml.PauliY(3)] - H2 = qml.Hamiltonian(coeffs2, obs2) - - coeffs_expected = pnp.array([-4.0, -2.0, -2.0, -1.0]) - obs_expected = [ - qml.PauliY(1) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliY(3), - qml.PauliX(2) @ qml.PauliY(1), - qml.PauliX(0) @ qml.PauliX(2), - ] - H = qml.Hamiltonian(coeffs_expected, obs_expected) - - assert H.compare(H1 @ H2) - - -with qml.operation.disable_new_opmath_cm(warn=False): - TEST_SPARSE_MATRIX = [ - ( - [1, -0.45], - [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliY(0) @ qml.PauliZ(1)], - None, - np.array( - [ - [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.45j, 0.0 + 0.0j], - [0.0 + 0.0j, -1.0 + 0.0j, 0.0 + 0.0j, 0.0 - 0.45j], - [0.0 - 0.45j, 0.0 + 0.0j, -1.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.45j, 0.0 + 0.0j, 1.0 + 0.0j], - ] - ), - ), - ( - [0.1], - [qml.PauliZ("b") @ qml.PauliX("a")], - ["a", "c", "b"], - np.array( - [ - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.1 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - -0.1 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.1 + 0.0j, - 0.0 + 0.0j, - ], - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - -0.1 + 0.0j, - ], - [ - 0.1 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - [ - 0.0 + 0.0j, - -0.1 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.1 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - -0.1 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - ] - ), - ), - ( - [0.21, -0.78, 0.52], - [ - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliY(0) @ qml.PauliZ(1), - ], - None, - np.array( - [ - [0.21 + 0.0j, 0.0 + 0.0j, -0.78 - 0.52j, 0.0 + 0.0j], - [0.0 + 0.0j, -0.21 + 0.0j, 0.0 + 0.0j, 0.78 + 0.52j], - [-0.78 + 0.52j, 0.0 + 0.0j, -0.21 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.78 - 0.52j, 0.0 + 0.0j, 0.21 + 0.0j], - ] - ), - ), - ] - - -@pytest.mark.usefixtures("legacy_opmath_only") -class TestHamiltonianSparseMatrix: - """Tests for sparse matrix representation.""" - - @pytest.mark.parametrize(["coeffs", "obs", "wires", "ref_matrix"], TEST_SPARSE_MATRIX) - def test_sparse_matrix(self, coeffs, obs, wires, ref_matrix): - """Tests that sparse_hamiltonian returns a correct sparse matrix""" - H = qml.Hamiltonian(coeffs, obs) - - sparse_matrix = H.sparse_matrix(wire_order=wires) - - assert np.allclose(sparse_matrix.toarray(), ref_matrix) - - def test_sparse_format(self): - """Tests that sparse_hamiltonian returns a scipy.sparse.csr_matrix object""" - - coeffs = [-0.25, 0.75] - obs = [ - qml.PauliX(wires=[0]) @ qml.PauliZ(wires=[1]), - qml.PauliY(wires=[0]) @ qml.PauliZ(wires=[1]), - ] - H = qml.Hamiltonian(coeffs, obs) - - sparse_matrix = H.sparse_matrix() - - assert isinstance(sparse_matrix, scipy.sparse.csr_matrix) - - def test_observable_error(self): - """Tests that an error is thrown if the observables are themselves constructed from multi-qubit - operations.""" - with pytest.raises(ValueError, match="Can only sparsify Hamiltonians"): - H = qml.Hamiltonian( - [0.1], [qml.PauliZ("c") @ qml.Hermitian(np.eye(4), wires=["a", "b"])] - ) - H.sparse_matrix(wire_order=["a", "c", "b"]) - - -@pytest.mark.jax -@pytest.mark.usefixtures("legacy_opmath_only") -class TestHamiltonianArithmeticJax: - """Tests creation of Hamiltonians using arithmetic - operations with jax tensor coefficients.""" - - def test_hamiltonian_equal(self): - """Tests equality""" - coeffs = jnp.array([0.5, -1.6]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = jnp.array([-1.6, 0.5]) - obs2 = [qml.PauliY(1), qml.PauliX(0)] - H2 = qml.Hamiltonian(coeffs2, obs2) - - assert H1.compare(H2) - - def test_hamiltonian_add(self): - """Tests that Hamiltonians are added correctly""" - coeffs = jnp.array([0.5, -1.6]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = jnp.array([0.5, -0.4]) - H2 = qml.Hamiltonian(coeffs2, obs) - - coeffs_expected = jnp.array([1.0, -2.0]) - H = qml.Hamiltonian(coeffs_expected, obs) - - assert H.compare(H1 + H2) - - H1 += H2 - assert H.compare(H1) - - def test_hamiltonian_sub(self): - """Tests that Hamiltonians are subtracted correctly""" - - coeffs = jnp.array([1.0, -2.0]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = jnp.array([0.5, -0.4]) - H2 = qml.Hamiltonian(coeffs2, obs) - - coeffs_expected = jnp.array([0.5, -1.6]) - H = qml.Hamiltonian(coeffs_expected, obs) - - assert H.compare(H1 - H2) - - H1 -= H2 - assert H.compare(H1) - - def test_hamiltonian_matmul(self): - """Tests that Hamiltonians are tensored correctly""" - coeffs = jnp.array([1.0, 2.0]) - obs = [qml.PauliX(0), qml.PauliY(1)] - H1 = qml.Hamiltonian(coeffs, obs) - - coeffs2 = jnp.array([-1.0, -2.0]) - obs2 = [qml.PauliX(2), qml.PauliY(3)] - H2 = qml.Hamiltonian(coeffs2, obs2) - - coeffs_expected = jnp.array([-4.0, -2.0, -2.0, -1.0]) - obs_expected = [ - qml.PauliY(1) @ qml.PauliY(3), - qml.PauliX(0) @ qml.PauliY(3), - qml.PauliX(2) @ qml.PauliY(1), - qml.PauliX(0) @ qml.PauliX(2), - ] - H = qml.Hamiltonian(coeffs_expected, obs_expected) - - assert H.compare(H1 @ H2) - - -@pytest.mark.usefixtures("legacy_opmath_only") -class TestGrouping: - """Tests for the grouping functionality""" - - def test_indentities_preserved(self): - """Tests that the grouping indices do not drop identity terms when the wire order is nonstandard.""" - - obs = [qml.PauliZ(1), qml.PauliZ(0), qml.Identity(0)] - - H = qml.Hamiltonian([1.0, 1.0, 1.0], obs, grouping_type="qwc") - assert H.grouping_indices == ((0, 1, 2),) - - def test_grouping_is_correct_kwarg(self): - """Basic test checking that grouping with a kwarg works as expected""" - a = qml.PauliX(0) - b = qml.PauliX(1) - c = qml.PauliZ(0) - obs = [a, b, c] - coeffs = [1.0, 2.0, 3.0] - - H = qml.Hamiltonian(coeffs, obs, grouping_type="qwc") - assert H.grouping_indices == ((0, 1), (2,)) - - def test_grouping_is_correct_compute_grouping(self): - """Basic test checking that grouping with compute_grouping works as expected""" - a = qml.PauliX(0) - b = qml.PauliX(1) - c = qml.PauliZ(0) - obs = [a, b, c] - coeffs = [1.0, 2.0, 3.0] - - H = qml.Hamiltonian(coeffs, obs, grouping_type="qwc") - H.compute_grouping() - assert H.grouping_indices == ((0, 1), (2,)) - - def test_set_grouping(self): - """Test that we can set grouping indices.""" - H = qml.Hamiltonian([1.0, 2.0, 3.0], [qml.PauliX(0), qml.PauliX(1), qml.PauliZ(0)]) - H.grouping_indices = [[0, 1], [2]] - - assert H.grouping_indices == ((0, 1), (2,)) - - def test_set_grouping_error(self): - """Test that grouping indices are validated.""" - H = qml.Hamiltonian([1.0, 2.0, 3.0], [qml.PauliX(0), qml.PauliX(1), qml.PauliZ(0)]) - - with pytest.raises(ValueError, match="The grouped index value"): - H.grouping_indices = [[3, 1], [2]] - - with pytest.raises(ValueError, match="The grouped index value"): - H.grouping_indices = "a" - - def test_grouping_for_non_groupable_hamiltonians(self): - """Test that grouping is computed correctly, even if no observables commute""" - a = qml.PauliX(0) - b = qml.PauliY(0) - c = qml.PauliZ(0) - obs = [a, b, c] - coeffs = [1.0, 2.0, 3.0] - - H = qml.Hamiltonian(coeffs, obs, grouping_type="qwc") - assert H.grouping_indices == ((0,), (1,), (2,)) - - def test_grouping_is_reset_when_simplifying(self): - """Tests that calling simplify() resets the grouping""" - obs = [qml.PauliX(0), qml.PauliX(1), qml.PauliZ(0)] - coeffs = [1.0, 2.0, 3.0] - - H = qml.Hamiltonian(coeffs, obs, grouping_type="qwc") - assert H.grouping_indices is not None - - H.simplify() - assert H.grouping_indices is None - - def test_grouping_does_not_alter_queue(self): - """Tests that grouping is invisible to the queue.""" - a = qml.PauliX(0) - b = qml.PauliX(1) - c = qml.PauliZ(0) - obs = [a, b, c] - coeffs = [1.0, 2.0, 3.0] - - with qml.queuing.AnnotatedQueue() as q: - H = qml.Hamiltonian(coeffs, obs, grouping_type="qwc") - - assert q.queue == [H] - - def test_grouping_method_can_be_set(self): - r"""Tests that the grouping method can be controlled by kwargs. - This is done by changing from default to 'lf' and checking the result.""" - # Create a graph with unique solution so that test does not depend on solver/implementation - a = qml.PauliX(0) - b = qml.PauliX(0) - c = qml.PauliZ(0) - obs = [a, b, c] - coeffs = [1.0, 2.0, 3.0] - - # compute grouping during construction - H2 = qml.Hamiltonian(coeffs, obs, grouping_type="qwc", method="lf") - assert set(H2.grouping_indices) == set(((0, 1), (2,))) - - # compute grouping separately - H3 = qml.Hamiltonian(coeffs, obs, grouping_type=None) - H3.compute_grouping(method="lf") - assert set(H3.grouping_indices) == set(((0, 1), (2,))) - - -@pytest.mark.usefixtures("legacy_opmath_only") -class TestHamiltonianEvaluation: - """Test the usage of a Hamiltonian as an observable""" - - @pytest.mark.parametrize("coeffs, param, interface", COEFFS_PARAM_INTERFACE) - def test_vqe_forward_different_coeff_types(self, coeffs, param, interface): - """Check that manually splitting a Hamiltonian expectation has the same - result as passing the Hamiltonian as an observable""" - device = qml.device("default.qubit", wires=2) - H = qml.Hamiltonian(coeffs, [qml.PauliX(0), qml.PauliZ(0)]) - - @qml.qnode(device, interface=interface) - def circuit(): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval(H) - - @qml.qnode(device, interface=interface) - def node1(): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval(qml.PauliX(0)) - - @qml.qnode(device, interface=interface) - def node2(): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval(qml.PauliZ(0)) - - res = circuit() - res_expected = coeffs[0] * node1() + coeffs[1] * node2() - assert np.isclose(res, res_expected) - - def test_simplify_reduces_tape_parameters(self): - """Test that simplifying a Hamiltonian reduces the number of parameters on a tape""" - device = qml.device("default.qubit", wires=2) - - @qml.qnode(device) - def circuit(): - qml.RY(0.1, wires=0) - return qml.expval( - qml.simplify(qml.Hamiltonian([1.0, 2.0], [qml.PauliX(1), qml.PauliX(1)])) - ) - - circuit() - pars = circuit.qtape.get_parameters(trainable_only=False) - # simplify worked and added 1. and 2. - assert pars == [0.1, 3.0] - - -@pytest.mark.usefixtures("legacy_opmath_only") -class TestHamiltonianDifferentiation: - """Test that the Hamiltonian coefficients are differentiable""" - - @pytest.mark.parametrize("simplify", [True, False]) - @pytest.mark.parametrize("group", [None, "qwc"]) - def test_trainable_coeffs_paramshift(self, simplify, group): - """Test the parameter-shift method by comparing the differentiation of linearly combined subcircuits - with the differentiation of a Hamiltonian expectation""" - coeffs = pnp.array([-0.05, 0.17], requires_grad=True) - param = pnp.array(1.7, requires_grad=True) - - # differentiating a circuit with measurement expval(H) - @qml.qnode(dev, diff_method="parameter-shift") - def circuit(coeffs, param): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval( - qml.simplify(qml.Hamiltonian(coeffs, [qml.X(0), qml.Z(0)], grouping_type=group)) - if simplify - else qml.Hamiltonian(coeffs, [qml.X(0), qml.Z(0)], grouping_type=group) - ) - - grad_fn = qml.grad(circuit) - grad = grad_fn(coeffs, param) - - # differentiating a cost that combines circuits with - # measurements expval(Pauli) - half1 = qml.QNode(circuit1, dev, diff_method="parameter-shift") - half2 = qml.QNode(circuit2, dev, diff_method="parameter-shift") - - def combine(coeffs, param): - return coeffs[0] * half1(param) + coeffs[1] * half2(param) - - grad_fn_expected = qml.grad(combine) - grad_expected = grad_fn_expected(coeffs, param) - - assert np.allclose(grad[0], grad_expected[0]) - assert np.allclose(grad[1], grad_expected[1]) - - def test_nontrainable_coeffs_paramshift(self): - """Test the parameter-shift method if the coefficients are explicitly set non-trainable - by not passing them to the qnode.""" - coeffs = np.array([-0.05, 0.17]) - param = pnp.array(1.7, requires_grad=True) - - # differentiating a circuit with measurement expval(H) - @qml.qnode(dev, diff_method="parameter-shift") - def circuit(param): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval( - qml.Hamiltonian( - coeffs, - [qml.PauliX(0), qml.PauliZ(0)], - ) - ) - - grad_fn = qml.grad(circuit) - grad = grad_fn(param) - - # differentiating a cost that combines circuits with - # measurements expval(Pauli) - half1 = qml.QNode(circuit1, dev, diff_method="parameter-shift") - half2 = qml.QNode(circuit2, dev, diff_method="parameter-shift") - - def combine(param): - return coeffs[0] * half1(param) + coeffs[1] * half2(param) - - grad_fn_expected = qml.grad(combine) - grad_expected = grad_fn_expected(param) - - assert np.allclose(grad, grad_expected) - - @pytest.mark.autograd - @pytest.mark.parametrize("simplify", [True, False]) - @pytest.mark.parametrize("group", [None, "qwc"]) - def test_trainable_coeffs_autograd(self, simplify, group): - """Test the autograd interface by comparing the differentiation of linearly combined subcircuits - with the differentiation of a Hamiltonian expectation""" - coeffs = pnp.array([-0.05, 0.17], requires_grad=True) - param = pnp.array(1.7, requires_grad=True) - - # differentiating a circuit with measurement expval(H) - @qml.qnode(dev, interface="autograd") - def circuit(coeffs, param): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval( - qml.simplify(qml.Hamiltonian(coeffs, [qml.X(0), qml.Z(0)], grouping_type=group)) - if simplify - else qml.ops.Hamiltonian(coeffs, [qml.X(0), qml.Z(0)], grouping_type=group) - ) - - grad_fn = qml.grad(circuit) - grad = grad_fn(coeffs, param) - - # differentiating a cost that combines circuits with - # measurements expval(Pauli) - half1 = qml.QNode(circuit1, dev, interface="autograd") - half2 = qml.QNode(circuit2, dev, interface="autograd") - - def combine(coeffs, param): - return coeffs[0] * half1(param) + coeffs[1] * half2(param) - - grad_fn_expected = qml.grad(combine) - grad_expected = grad_fn_expected(coeffs, param) - - assert np.allclose(grad[0], grad_expected[0]) - assert np.allclose(grad[1], grad_expected[1]) - - @pytest.mark.autograd - def test_nontrainable_coeffs_autograd(self): - """Test the autograd interface if the coefficients are explicitly set non-trainable""" - coeffs = pnp.array([-0.05, 0.17], requires_grad=False) - param = pnp.array(1.7, requires_grad=True) - - # differentiating a circuit with measurement expval(H) - @qml.qnode(dev, interface="autograd") - def circuit(coeffs, param): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval(qml.Hamiltonian(coeffs, [qml.PauliX(0), qml.PauliZ(0)])) - - grad_fn = qml.grad(circuit) - grad = grad_fn(coeffs, param) - - # differentiating a cost that combines circuits with - # measurements expval(Pauli) - half1 = qml.QNode(circuit1, dev, interface="autograd") - half2 = qml.QNode(circuit2, dev, interface="autograd") - - def combine(coeffs, param): - return coeffs[0] * half1(param) + coeffs[1] * half2(param) - - grad_fn_expected = qml.grad(combine) - grad_expected = grad_fn_expected(coeffs, param) - - assert np.allclose(grad, grad_expected) - - @pytest.mark.jax - @pytest.mark.parametrize("simplify", [True, False]) - @pytest.mark.parametrize("group", [None, "qwc"]) - def test_trainable_coeffs_jax(self, simplify, group): - """Test the jax interface by comparing the differentiation of linearly - combined subcircuits with the differentiation of a Hamiltonian expectation""" - - coeffs = jnp.array([-0.05, 0.17]) - param = jnp.array(1.7) - - # differentiating a circuit with measurement expval(H) - @qml.qnode(dev, interface="jax", diff_method="backprop") - def circuit(coeffs, param): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval( - qml.simplify(qml.Hamiltonian(coeffs, [qml.X(0), qml.Z(0)], grouping_type=group)) - if simplify - else qml.ops.Hamiltonian(coeffs, [qml.X(0), qml.Z(0)], grouping_type=group) - ) - - grad_fn = jax.grad(circuit) - grad = grad_fn(coeffs, param) - - # differentiating a cost that combines circuits with - # measurements expval(Pauli) - half1 = qml.QNode(circuit1, dev, interface="jax", diff_method="backprop") - half2 = qml.QNode(circuit2, dev, interface="jax", diff_method="backprop") - - def combine(coeffs, param): - return coeffs[0] * half1(param) + coeffs[1] * half2(param) - - grad_fn_expected = jax.grad(combine) - grad_expected = grad_fn_expected(coeffs, param) - - assert np.allclose(grad[0], grad_expected[0]) - assert np.allclose(grad[1], grad_expected[1]) - - @pytest.mark.jax - def test_nontrainable_coeffs_jax(self): - """Test the jax interface if the coefficients are explicitly set non-trainable""" - coeffs = np.array([-0.05, 0.17]) - param = jnp.array(1.7) - - # differentiating a circuit with measurement expval(H) - @qml.qnode(dev, interface="jax", diff_method="backprop") - def circuit(coeffs, param): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval(qml.Hamiltonian(coeffs, [qml.PauliX(0), qml.PauliZ(0)])) - - grad_fn = jax.grad(circuit, argnums=(1)) - grad = grad_fn(coeffs, param) - - # differentiating a cost that combines circuits with - # measurements expval(Pauli) - half1 = qml.QNode(circuit1, dev, interface="jax", diff_method="backprop") - half2 = qml.QNode(circuit2, dev, interface="jax", diff_method="backprop") - - def combine(coeffs, param): - return coeffs[0] * half1(param) + coeffs[1] * half2(param) - - grad_fn_expected = jax.grad(combine, argnums=(1)) - grad_expected = grad_fn_expected(coeffs, param) - - assert np.allclose(grad, grad_expected) - - @pytest.mark.torch - @pytest.mark.parametrize("simplify", [True, False]) - @pytest.mark.parametrize("group", [None, "qwc"]) - def test_trainable_coeffs_torch(self, simplify, group): - """Test the torch interface by comparing the differentiation of linearly combined subcircuits - with the differentiation of a Hamiltonian expectation""" - coeffs = torch.tensor([-0.05, 0.17], requires_grad=True) - param = torch.tensor(1.7, requires_grad=True) - - # differentiating a circuit with measurement expval(H) - @qml.qnode(dev, interface="torch", diff_method="backprop") - def circuit(coeffs, param): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval( - qml.simplify(qml.Hamiltonian(coeffs, [qml.X(0), qml.Z(0)], grouping_type=group)) - if simplify - else qml.ops.Hamiltonian(coeffs, [qml.X(0), qml.Z(0)], grouping_type=group) - ) - - res = circuit(coeffs, param) - res.backward() # pylint:disable=no-member - grad = (coeffs.grad, param.grad) - - # differentiating a cost that combines circuits with - # measurements expval(Pauli) - - # we need to create new tensors here - coeffs2 = torch.tensor([-0.05, 0.17], requires_grad=True) - param2 = torch.tensor(1.7, requires_grad=True) - - half1 = qml.QNode(circuit1, dev, interface="torch", diff_method="backprop") - half2 = qml.QNode(circuit2, dev, interface="torch", diff_method="backprop") - - def combine(coeffs, param): - return coeffs[0] * half1(param) + coeffs[1] * half2(param) - - res_expected = combine(coeffs2, param2) - res_expected.backward() - grad_expected = (coeffs2.grad, param2.grad) - - assert np.allclose(grad[0], grad_expected[0]) - assert np.allclose(grad[1], grad_expected[1]) - - @pytest.mark.torch - def test_nontrainable_coeffs_torch(self): - """Test the torch interface if the coefficients are explicitly set non-trainable""" - coeffs = torch.tensor([-0.05, 0.17], requires_grad=False) - param = torch.tensor(1.7, requires_grad=True) - - # differentiating a circuit with measurement expval(H) - @qml.qnode(dev, interface="torch", diff_method="backprop") - def circuit(coeffs, param): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval( - qml.Hamiltonian( - coeffs, - [qml.PauliX(0), qml.PauliZ(0)], - ) - ) - - res = circuit(coeffs, param) - res.backward() # pylint:disable=no-member - - # differentiating a cost that combines circuits with - # measurements expval(Pauli) - - # we need to create new tensors here - coeffs2 = torch.tensor([-0.05, 0.17], requires_grad=False) - param2 = torch.tensor(1.7, requires_grad=True) - - half1 = qml.QNode(circuit1, dev, interface="torch", diff_method="backprop") - half2 = qml.QNode(circuit2, dev, interface="torch", diff_method="backprop") - - def combine(coeffs, param): - return coeffs[0] * half1(param) + coeffs[1] * half2(param) - - res_expected = combine(coeffs2, param2) - res_expected.backward() - - assert coeffs.grad is None - assert np.allclose(param.grad, param2.grad) - - @pytest.mark.tf - @pytest.mark.parametrize("simplify", [True, False]) - @pytest.mark.parametrize("group", [None, "qwc"]) - def test_trainable_coeffs_tf(self, simplify, group): - """Test the tf interface by comparing the differentiation of linearly combined subcircuits - with the differentiation of a Hamiltonian expectation""" - coeffs = tf.Variable([-0.05, 0.17], dtype=tf.double) - param = tf.Variable(1.7, dtype=tf.double) - - # differentiating a circuit with measurement expval(H) - @qml.qnode(dev, interface="tf", diff_method="backprop") - def circuit(coeffs, param): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval( - qml.simplify(qml.Hamiltonian(coeffs, [qml.X(0), qml.Z(0)], grouping_type=group)) - if simplify - else qml.ops.Hamiltonian(coeffs, [qml.X(0), qml.Z(0)], grouping_type=group) - ) - - with tf.GradientTape() as tape: - res = circuit(coeffs, param) - - grad = tape.gradient(res, [coeffs, param]) - - # differentiating a cost that combines circuits with - # measurements expval(Pauli) - - # we need to create new tensors here - coeffs2 = tf.Variable([-0.05, 0.17], dtype=tf.double) - param2 = tf.Variable(1.7, dtype=tf.double) - half1 = qml.QNode(circuit1, dev, interface="tf", diff_method="backprop") - half2 = qml.QNode(circuit2, dev, interface="tf", diff_method="backprop") - - def combine(coeffs, param): - return coeffs[0] * half1(param) + coeffs[1] * half2(param) - - with tf.GradientTape() as tape2: - res_expected = combine(coeffs2, param2) - grad_expected = tape2.gradient(res_expected, [coeffs2, param2]) - - assert np.allclose(grad[0], grad_expected[0]) - assert np.allclose(grad[1], grad_expected[1]) - - @pytest.mark.tf - def test_nontrainable_coeffs_tf(self): - """Test the tf interface if the coefficients are explicitly set non-trainable""" - - coeffs = tf.constant([-0.05, 0.17], dtype=tf.double) - param = tf.Variable(1.7, dtype=tf.double) - - # differentiating a circuit with measurement expval(H) - @qml.qnode(dev, interface="tf", diff_method="backprop") - def circuit(coeffs, param): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval( - qml.Hamiltonian( - coeffs, - [qml.PauliX(0), qml.PauliZ(0)], - ) - ) - - with tf.GradientTape() as tape: - res = circuit(coeffs, param) - grad = tape.gradient(res, [coeffs, param]) - - # differentiating a cost that combines circuits with - # measurements expval(Pauli) - - # we need to create new tensors here - coeffs2 = tf.constant([-0.05, 0.17], dtype=tf.double) - param2 = tf.Variable(1.7, dtype=tf.double) - half1 = qml.QNode(circuit1, dev, interface="tf", diff_method="backprop") - half2 = qml.QNode(circuit2, dev, interface="tf", diff_method="backprop") - - def combine(coeffs, param): - return coeffs[0] * half1(param) + coeffs[1] * half2(param) - - with tf.GradientTape() as tape2: - res_expected = combine(coeffs2, param2) - grad_expected = tape2.gradient(res_expected, [coeffs2, param2]) - - assert grad[0] is None - assert np.allclose(grad[1], grad_expected[1]) - - def test_not_supported_by_adjoint_differentiation(self): - """Test that error is raised when attempting the adjoint differentiation method.""" - device = qml.device("default.qubit", wires=2) - - coeffs = pnp.array([-0.05, 0.17], requires_grad=True) - param = pnp.array(1.7, requires_grad=True) - - @qml.qnode(device, diff_method="adjoint") - def circuit(coeffs, param): - qml.RX(param, wires=0) - qml.RY(param, wires=0) - return qml.expval( - qml.Hamiltonian( - coeffs, - [qml.PauliX(0), qml.PauliZ(0)], - ) - ) - - grad_fn = qml.grad(circuit) - with pytest.raises( - qml.QuantumFunctionError, - match="does not support adjoint with requested circuit.", - ): - grad_fn(coeffs, param) diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py index 66a44b98e9b..f5c29ea4d32 100644 --- a/tests/ops/qubit/test_parametric_ops.py +++ b/tests/ops/qubit/test_parametric_ops.py @@ -231,7 +231,7 @@ def test_pcphase_raises_error(self): class TestParameterFrequencies: - @pytest.mark.usefixtures("use_legacy_and_new_opmath") + @pytest.mark.parametrize("op", PARAMETRIZED_OPERATIONS) def test_parameter_frequencies_match_generator(self, op, tol): if not qml.operation.has_gen(op): @@ -3011,7 +3011,6 @@ def test_init_incorrect_pauli_word_length_error(self, pauli_word, wires): ("IIIXYZ"), ], ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_multirz_generator(self, pauli_word): """Test that the generator of the MultiRZ gate is correct.""" op = qml.PauliRot(0.3, pauli_word, wires=range(len(pauli_word))) @@ -3054,19 +3053,6 @@ def test_pauli_rot_identity_torch(self, torch_device, theta): exp = torch.tensor(np.diag([val, val]), device=torch_device) assert qml.math.allclose(mat, exp) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_pauli_rot_generator_legacy_opmath(self): - """Test that the generator of the PauliRot operation - is correctly returned.""" - op = qml.PauliRot(0.65, "ZY", wires=["a", 7]) - gen, coeff = qml.generator(op) - expected = qml.PauliZ("a") @ qml.PauliY(7) - - assert coeff == -0.5 - assert gen.operands[0].name == expected.obs[0].name - assert gen.operands[1].wires == expected.obs[1].wires - - @pytest.mark.usefixtures("new_opmath_only") def test_pauli_rot_generator(self): """Test that the generator of the PauliRot operation is correctly returned.""" @@ -3246,7 +3232,6 @@ def decomp_circuit(theta): assert np.allclose(qml.jacobian(circuit)(angle), qml.jacobian(decomp_circuit)(angle)) @pytest.mark.parametrize("qubits", range(3, 6)) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_multirz_generator(self, qubits, mocker): """Test that the generator of the MultiRZ gate is correct.""" op = qml.MultiRZ(0.3, wires=range(qubits)) diff --git a/tests/ops/qubit/test_qchem_ops.py b/tests/ops/qubit/test_qchem_ops.py index f652da4c5a0..8b11969ffda 100644 --- a/tests/ops/qubit/test_qchem_ops.py +++ b/tests/ops/qubit/test_qchem_ops.py @@ -46,7 +46,7 @@ class TestParameterFrequencies: - @pytest.mark.usefixtures("use_legacy_and_new_opmath") + @pytest.mark.parametrize("op", PARAMETRIZED_QCHEM_OPERATIONS) def test_parameter_frequencies_match_generator(self, op, tol): if not qml.operation.has_gen(op): @@ -1234,7 +1234,6 @@ def test_label_method(op, label1, label2, label3): assert op.label(decimals=0) == label3 -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("op", PARAMETRIZED_QCHEM_OPERATIONS) def test_generators(op): """Check that the type of the generator returned by the qchem ops is diff --git a/tests/ops/qutrit/test_qutrit_observables.py b/tests/ops/qutrit/test_qutrit_observables.py index be2ac895869..5ab0111b385 100644 --- a/tests/ops/qutrit/test_qutrit_observables.py +++ b/tests/ops/qutrit/test_qutrit_observables.py @@ -372,19 +372,6 @@ def test_matrix(self, index, mat, eigs, tol): assert np.allclose(res_static, mat) assert np.allclose(res_dynamic, mat) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_obs_data(self): - """Test that the _obs_data() method of qml.GellMann returns the correct - observable data.""" - ob1 = qml.GellMann(wires=0, index=2) - ob2 = qml.GellMann(wires=0, index=2) @ qml.GellMann(wires=1, index=1) - - assert ob1._obs_data() == {("GellMann", qml.wires.Wires(0), (2,))} - assert ob2._obs_data() == { - ("GellMann", qml.wires.Wires(0), (2,)), - ("GellMann", qml.wires.Wires(1), (1,)), - } - @pytest.mark.parametrize("index, mat, eigs", GM_OBSERVABLES) def test_eigvals(self, index, mat, eigs, tol): """Test that the Gell-Mann eigenvalues are correct""" diff --git a/tests/optimize/test_momentum_qng.py b/tests/optimize/test_momentum_qng.py index e3153cadf3a..42e7ddd6fad 100644 --- a/tests/optimize/test_momentum_qng.py +++ b/tests/optimize/test_momentum_qng.py @@ -81,7 +81,6 @@ def circuit(params): var -= accum assert np.allclose([var1, var2], var) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_step_and_cost_autograd_with_gen_hamiltonian(self): """Test that the correct cost and step is returned after 8 optimization steps via the step_and_cost method for the MomentumQNG optimizer when the generator @@ -304,7 +303,7 @@ def gradient(params): # check final cost assert np.allclose(circuit(theta), -1, atol=1e-4) - def test_single_qubit_vqe_using_expval_h_multiple_input_params(self, tol, recwarn): + def test_single_qubit_vqe_using_expval_h_multiple_input_params(self, tol): """Test single-qubit VQE by returning qml.expval(H) in the QNode and check for the correct MomentumQNG value every step, the correct parameter updates, and correct cost after a few steps""" @@ -356,5 +355,3 @@ def gradient(params): # check final cost assert np.allclose(circuit(x, y), qml.eigvals(H).min(), atol=tol, rtol=0) - if qml.operation.active_new_opmath(): - assert len(recwarn) == 0 diff --git a/tests/optimize/test_qng.py b/tests/optimize/test_qng.py index a382a73aef3..21bf3d8ab0d 100644 --- a/tests/optimize/test_qng.py +++ b/tests/optimize/test_qng.py @@ -162,7 +162,6 @@ def circuit(params): assert np.allclose(step1, expected_step) assert np.allclose(step2, expected_step) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_step_and_cost_autograd_with_gen_hamiltonian(self): """Test that the correct cost and step is returned via the step_and_cost method for the QNG optimizer when the generator @@ -393,8 +392,7 @@ def gradient(params): # check final cost assert np.allclose(circuit(x, y), qml.eigvals(H).min(), atol=tol, rtol=0) - if qml.operation.active_new_opmath(): - assert len(recwarn) == 0 + assert len(recwarn) == 0 flat_dummy_array = np.linspace(-1, 1, 64) diff --git a/tests/pauli/grouping/test_pauli_group_observables.py b/tests/pauli/grouping/test_pauli_group_observables.py index c36dd7e94ff..a2256b8a0e0 100644 --- a/tests/pauli/grouping/test_pauli_group_observables.py +++ b/tests/pauli/grouping/test_pauli_group_observables.py @@ -22,7 +22,6 @@ import pennylane as qml from pennylane import Identity, PauliX, PauliY, PauliZ from pennylane import numpy as pnp -from pennylane.operation import Tensor from pennylane.pauli import are_identical_pauli_words, are_pauli_words_qwc from pennylane.pauli.grouping.group_observables import ( PauliGroupingStrategy, @@ -459,34 +458,6 @@ def test_return_list_coefficients(self): _, grouped_coeffs = group_observables(obs, coeffs) assert isinstance(grouped_coeffs[0], list) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_return_new_opmath_legacy_opmath(self): - """Test that using new opmath causes grouped observables to have Prods instead of - Tensors""" - old_observables = [ - Tensor(PauliX(0), PauliZ(1)), - Tensor(PauliY(2), PauliZ(1)), - Tensor(PauliZ(1), PauliZ(2)), - ] - - old_groups = group_observables(old_observables) - - assert all(isinstance(o, Tensor) for g in old_groups for o in g) - - @pytest.mark.usefixtures("legacy_opmath_only") - def test_return_deactive_opmath_prod(self): - """Test that using new opmath causes grouped observables to have Prods instead of - Tensors""" - observables = [ - qml.prod(PauliX(0), PauliZ(1)), - qml.prod(PauliY(2), PauliZ(1)), - qml.prod(PauliZ(1), PauliZ(2)), - ] - - old_groups = group_observables(observables) - - assert all(isinstance(o, qml.ops.Prod) for g in old_groups for o in g) - def test_observables_on_no_wires(self): """Test that observables on no wires are stuck in the first group.""" @@ -511,7 +482,6 @@ def test_no_observables_with_wires(self): assert groups == [observables] assert coeffs == [[1, 2]] - @pytest.mark.usefixtures("new_opmath_only") def test_observables_on_no_wires_coeffs(self): """Test that observables on no wires are stuck in the first group and coefficients are tracked when provided.""" diff --git a/tests/pauli/test_conversion.py b/tests/pauli/test_conversion.py index 5e8267104b8..30ec54390b4 100644 --- a/tests/pauli/test_conversion.py +++ b/tests/pauli/test_conversion.py @@ -18,7 +18,6 @@ import pytest import pennylane as qml -from pennylane.operation import Tensor from pennylane.ops import Identity, PauliX, PauliY, PauliZ from pennylane.pauli import PauliSentence, PauliWord, pauli_sentence from pennylane.pauli.conversion import _generalized_pauli_decompose @@ -45,51 +44,49 @@ test_diff_matrix1 = [[[-2, -2 + 1j]], [[-2, -2 + 1j], [-1, -1j]]] test_diff_matrix2 = [[[-2, -2 + 1j], [-2 - 1j, 0]], [[2.5, -0.5], [-0.5, 2.5]]] -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", "qml.ops.Hamiltonian uses", qml.PennyLaneDeprecationWarning) - hamiltonian_ps = ( - ( - qml.ops.Hamiltonian([], []), - PauliSentence(), - ), - ( - qml.ops.Hamiltonian([2], [qml.PauliZ(wires=0)]), - PauliSentence({PauliWord({0: "Z"}): 2}), +hamiltonian_ps = ( + ( + qml.Hamiltonian([], []), + PauliSentence(), + ), + ( + qml.Hamiltonian([2], [qml.PauliZ(wires=0)]), + PauliSentence({PauliWord({0: "Z"}): 2}), + ), + ( + qml.Hamiltonian([2], [qml.PauliZ(wires=0)]), + PauliSentence({PauliWord({0: "Z"}): 2}), + ), + ( + qml.Hamiltonian( + [2, -0.5], + [qml.PauliZ(wires=0), qml.prod(qml.X(wires=0), qml.Z(wires=1))], ), - ( - qml.Hamiltonian([2], [qml.PauliZ(wires=0)]), - PauliSentence({PauliWord({0: "Z"}): 2}), + PauliSentence( + { + PauliWord({0: "Z"}): 2, + PauliWord({0: "X", 1: "Z"}): -0.5, + } ), - ( - qml.ops.Hamiltonian( - [2, -0.5], - [qml.PauliZ(wires=0), qml.operation.Tensor(qml.X(wires=0), qml.Z(wires=1))], - ), - PauliSentence( - { - PauliWord({0: "Z"}): 2, - PauliWord({0: "X", 1: "Z"}): -0.5, - } - ), + ), + ( + qml.Hamiltonian( + [2, -0.5, 3.14], + [ + qml.PauliZ(wires=0), + qml.prod(qml.X(wires=0), qml.Z(wires="a")), + qml.Identity(wires="b"), + ], ), - ( - qml.ops.Hamiltonian( - [2, -0.5, 3.14], - [ - qml.PauliZ(wires=0), - qml.operation.Tensor(qml.X(wires=0), qml.Z(wires="a")), - qml.Identity(wires="b"), - ], - ), - PauliSentence( - { - PauliWord({0: "Z"}): 2, - PauliWord({0: "X", "a": "Z"}): -0.5, - PauliWord({}): 3.14, - } - ), + PauliSentence( + { + PauliWord({0: "Z"}): 2, + PauliWord({0: "X", "a": "Z"}): -0.5, + PauliWord({}): 3.14, + } ), - ) + ), +) class TestDecomposition: @@ -116,33 +113,22 @@ def test_hide_identity_true(self): when hide_identity=True""" H = np.array(np.diag([0, 0, 0, 1])) _, obs_list = qml.pauli_decompose(H, hide_identity=True).terms() - tensors = filter(lambda obs: isinstance(obs, Tensor), obs_list) + tensors = filter(lambda obs: isinstance(obs, qml.ops.Prod), obs_list) for tensor in tensors: - all_identities = all(isinstance(o, Identity) for o in tensor.obs) - no_identities = not any(isinstance(o, Identity) for o in tensor.obs) + all_identities = all(isinstance(o, Identity) for o in tensor.operands) + no_identities = not any(isinstance(o, Identity) for o in tensor.operands) assert all_identities or no_identities def test_hide_identity_true_all_identities(self): """Tests that the all identity operator remains even with hide_identity = True.""" H = np.eye(4) _, obs_list = qml.pauli_decompose(H, hide_identity=True).terms() - tensors = filter(lambda obs: isinstance(obs, Tensor), obs_list) + tensors = filter(lambda obs: isinstance(obs, qml.ops.Prod), obs_list) for tensor in tensors: - assert all(isinstance(o, Identity) for o in tensor.obs) + assert all(isinstance(o, Identity) for o in tensor.operands) - @pytest.mark.usefixtures("legacy_opmath_only") - @pytest.mark.parametrize("hide_identity", [True, False]) - @pytest.mark.parametrize("hamiltonian", test_hamiltonians) - def test_observable_types_legacy_opmath(self, hamiltonian, hide_identity): - """Tests that the Hamiltonian decomposes into a linear combination of Pauli words.""" - allowed_obs = (Tensor, Identity, PauliX, PauliY, PauliZ) - - _, decomposed_obs = qml.pauli_decompose(hamiltonian, hide_identity).terms() - assert all((isinstance(o, allowed_obs) for o in decomposed_obs)) - - @pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("hide_identity", [True, False]) @pytest.mark.parametrize("hamiltonian", test_hamiltonians) def test_observable_types(self, hamiltonian, hide_identity): @@ -159,8 +145,8 @@ def test_result_length(self, hamiltonian): _, decomposed_obs = qml.pauli_decompose(hamiltonian).terms() n = int(np.log2(len(hamiltonian))) - tensors = filter(lambda obs: isinstance(obs, Tensor), decomposed_obs) - assert all(len(tensor.obs) == n for tensor in tensors) + tensors = filter(lambda obs: isinstance(obs, qml.ops.Prod), decomposed_obs) + assert all(len(tensor.operands) == n for tensor in tensors) # pylint: disable = consider-using-generator @pytest.mark.parametrize("hamiltonian", test_hamiltonians) @@ -259,36 +245,22 @@ def test_hide_identity_true(self): when hide_identity=True""" H = np.array(np.diag([0, 0, 0, 1])) _, obs_list = qml.pauli_decompose(H, hide_identity=True, check_hermitian=False).terms() - tensors = filter(lambda obs: isinstance(obs, Tensor), obs_list) + tensors = filter(lambda obs: isinstance(obs, qml.ops.Prod), obs_list) for tensor in tensors: - all_identities = all(isinstance(o, Identity) for o in tensor.obs) - no_identities = not any(isinstance(o, Identity) for o in tensor.obs) + all_identities = all(isinstance(o, Identity) for o in tensor.operands) + no_identities = not any(isinstance(o, Identity) for o in tensor.operands) assert all_identities or no_identities def test_hide_identity_true_all_identities(self): """Tests that the all identity operator remains even with hide_identity = True.""" H = np.eye(4) _, obs_list = qml.pauli_decompose(H, hide_identity=True, check_hermitian=False).terms() - tensors = filter(lambda obs: isinstance(obs, Tensor), obs_list) + tensors = filter(lambda obs: isinstance(obs, qml.ops.Prod), obs_list) for tensor in tensors: - assert all(isinstance(o, Identity) for o in tensor.obs) + assert all(isinstance(o, Identity) for o in tensor.operands) - @pytest.mark.usefixtures("legacy_opmath_only") - @pytest.mark.parametrize("hide_identity", [True, False]) - @pytest.mark.parametrize("hamiltonian", test_hamiltonians) - def test_observable_types_legacy_opmath(self, hamiltonian, hide_identity): - """Tests that the Hamiltonian decomposes into a linear combination of tensors, - the identity matrix, and Pauli matrices.""" - allowed_obs = (Tensor, Identity, PauliX, PauliY, PauliZ) - - _, decomposed_obs = qml.pauli_decompose( - hamiltonian, hide_identity, check_hermitian=False - ).terms() - assert all((isinstance(o, allowed_obs) for o in decomposed_obs)) - - @pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("hide_identity", [True, False]) @pytest.mark.parametrize("hamiltonian", test_hamiltonians) def test_observable_types(self, hamiltonian, hide_identity): @@ -308,8 +280,8 @@ def test_result_length(self, hamiltonian): _, decomposed_obs = qml.pauli_decompose(hamiltonian, check_hermitian=False).terms() n = int(np.log2(len(hamiltonian))) - tensors = filter(lambda obs: isinstance(obs, Tensor), decomposed_obs) - assert all(len(tensor.obs) == n for tensor in tensors) + tensors = filter(lambda obs: isinstance(obs, qml.ops.Prod), decomposed_obs) + assert all(len(tensor.operands) == n for tensor in tensors) # pylint: disable = consider-using-generator @pytest.mark.parametrize("hamiltonian", test_hamiltonians) @@ -339,36 +311,7 @@ def test_to_paulisentence(self, hamiltonian): assert np.allclose(hamiltonian, ps.to_mat(range(num_qubits))) # pylint: disable = consider-using-generator - @pytest.mark.usefixtures("legacy_opmath_only") - @pytest.mark.parametrize("hide_identity", [True, False]) - @pytest.mark.parametrize("matrix", test_general_matrix) - def test_observable_types_general_legacy_opmath(self, matrix, hide_identity): - """Tests that the matrix decomposes into a linear combination of tensors, - the identity matrix, and Pauli matrices.""" - shape = matrix.shape - num_qubits = int(np.ceil(np.log2(max(shape)))) - allowed_obs = (Tensor, Identity, PauliX, PauliY, PauliZ) - - decomposed_coeff, decomposed_obs = qml.pauli_decompose( - matrix, hide_identity, check_hermitian=False - ).terms() - - assert all((isinstance(o, allowed_obs) for o in decomposed_obs)) - linear_comb = sum( - [ - decomposed_coeff[i] * qml.matrix(o, wire_order=range(num_qubits)) - for i, o in enumerate(decomposed_obs) - ] - ) - assert np.allclose(matrix, linear_comb[: shape[0], : shape[1]]) - - if not hide_identity: - tensors = filter(lambda obs: isinstance(obs, Tensor), decomposed_obs) - assert all(len(tensor.obs) == num_qubits for tensor in tensors) - - # pylint: disable = consider-using-generator - @pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("hide_identity", [True, False]) @pytest.mark.parametrize("matrix", test_general_matrix) def test_observable_types_general(self, matrix, hide_identity): @@ -515,7 +458,6 @@ class TestPauliSentence: (qml.Identity(wires=0), PauliSentence({PauliWord({}): 1})), ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("op, ps", pauli_op_ps) def test_pauli_ops(self, op, ps): """Test that PL Pauli ops are properly cast to a PauliSentence.""" @@ -537,13 +479,11 @@ def test_pauli_ops(self, op, ps): (qml.PauliX(wires=0) @ qml.PauliY(wires=0), PauliSentence({PauliWord({0: "Z"}): 1j})), ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("op, ps", tensor_ps) def test_tensor(self, op, ps): """Test that Tensors of Pauli ops are properly cast to a PauliSentence.""" assert pauli_sentence(op) == ps - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_tensor_raises_error(self): """Test that Tensors of non-Pauli ops raise error when cast to a PauliSentence.""" h_mat = np.array([[1, 1], [1, -1]]) @@ -553,15 +493,9 @@ def test_tensor_raises_error(self): with pytest.raises(ValueError, match="Op must be a linear combination of"): pauli_sentence(op) - @pytest.mark.filterwarnings( - "ignore:qml.ops.Hamiltonian uses:pennylane.PennyLaneDeprecationWarning" - ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("op, ps", hamiltonian_ps) def test_hamiltonian(self, op, ps): """Test that a Hamiltonian is properly cast to a PauliSentence.""" - if qml.operation.active_new_opmath(): - op = qml.operation.convert_to_legacy_H(op) assert pauli_sentence(op) == ps operator_ps = ( @@ -585,14 +519,6 @@ def test_hamiltonian(self, op, ps): } ), ), - ( - qml.operation.Tensor(qml.PauliX(wires=0), qml.PauliZ(wires=1)), - PauliSentence( - { - PauliWord({0: "X", 1: "Z"}): 1, - } - ), - ), ( qml.sum( qml.s_prod(2, qml.PauliZ(wires=0)), @@ -627,7 +553,6 @@ def test_operator_private_ps(self, op, ps): qml.Hadamard(wires=0), qml.Hamiltonian([1, 2], [qml.Projector([0], wires=0), qml.PauliZ(wires=1)]), qml.RX(1.23, wires="a") + qml.PauliZ(wires=0), - qml.ops.Hamiltonian([1, 2], [qml.Projector([0], wires=0), qml.Z(1)]), ) @pytest.mark.parametrize("op", error_ps) diff --git a/tests/pauli/test_pauli_arithmetic.py b/tests/pauli/test_pauli_arithmetic.py index 6aee89c5dc9..0a26db4b9aa 100644 --- a/tests/pauli/test_pauli_arithmetic.py +++ b/tests/pauli/test_pauli_arithmetic.py @@ -22,7 +22,6 @@ import pennylane as qml from pennylane import numpy as np -from pennylane.operation import Tensor from pennylane.pauli.pauli_arithmetic import I, PauliSentence, PauliWord, X, Y, Z matI = np.eye(2) @@ -403,11 +402,6 @@ def test_operation(self, pw, op): assert pw_op.name == op.name assert pw_op.wires == op.wires - if isinstance(op, qml.ops.Prod): # pylint: disable=no-member - pw_tensor_op = pw.operation(get_as_tensor=True) - expected_tensor_op = qml.operation.Tensor(*op.operands) - qml.assert_equal(pw_tensor_op, expected_tensor_op) - def test_operation_empty(self): """Test that an empty PauliWord with wire_order returns Identity.""" op = PauliWord({}).operation(wire_order=[0, 1]) @@ -420,63 +414,6 @@ def test_operation_empty_nowires(self): res = pw4.operation() assert res == qml.Identity() - tup_pw_hamiltonian = ( - (PauliWord({0: X}), qml.Hamiltonian([1], [qml.PauliX(wires=0)])), - ( - pw1, - qml.Hamiltonian([1], [qml.operation.Tensor(qml.PauliX(wires=1), qml.PauliY(wires=2))]), - ), - ( - pw2, - qml.Hamiltonian( - [1], - [ - qml.operation.Tensor( - qml.PauliX(wires="a"), qml.PauliX(wires="b"), qml.PauliZ(wires="c") - ) - ], - ), - ), - ( - pw3, - qml.Hamiltonian( - [1], - [ - qml.operation.Tensor( - qml.PauliZ(wires=0), qml.PauliZ(wires="b"), qml.PauliZ(wires="c") - ) - ], - ), - ), - ) - - @pytest.mark.usefixtures("legacy_opmath_only") - @pytest.mark.parametrize("pw, h", tup_pw_hamiltonian) - def test_hamiltonian(self, pw, h): - """Test that a PauliWord can be cast to a Hamiltonian.""" - pw_h = pw.hamiltonian() - h = qml.operation.convert_to_legacy_H(h) - assert pw_h.compare(h) - - @pytest.mark.usefixtures("legacy_opmath_only") - def test_hamiltonian_empty(self): - """Test that an empty PauliWord with wire_order returns Identity Hamiltonian.""" - op = PauliWord({}).hamiltonian(wire_order=[0, 1]) - id = qml.Hamiltonian([1], [qml.Identity(wires=[0, 1])]) - assert op.compare(id) - - @pytest.mark.usefixtures("legacy_opmath_only") - def test_hamiltonian_empty_error(self): - """Test that a ValueError is raised if an empty PauliWord is - cast to a Hamiltonian.""" - with pytest.raises(ValueError, match="Can't get the Hamiltonian for an empty PauliWord."): - pw4.hamiltonian() - - def test_hamiltonian_deprecation(self): - """Test that the correct deprecation warning is raised when calling hamiltonian()""" - with pytest.warns(qml.PennyLaneDeprecationWarning, match="PauliWord.hamiltonian"): - _ = pw1.hamiltonian() - def test_pickling(self): """Check that pauliwords can be pickled and unpickled.""" pw = PauliWord({2: "X", 3: "Y", 4: "Z"}) @@ -1008,79 +945,6 @@ def test_operation_wire_order(self): qml.assert_equal(op, id) - tup_ps_hamiltonian = ( - (PauliSentence({PauliWord({0: X}): 1}), qml.Hamiltonian([1], [qml.PauliX(wires=0)])), - ( - ps1_hamiltonian, - qml.Hamiltonian( - [1.23, 4.0, -0.5], - [ - Tensor(qml.PauliX(wires=1), qml.PauliY(wires=2)), - Tensor(qml.PauliX(wires="a"), qml.PauliX(wires="b"), qml.PauliZ(wires="c")), - Tensor(qml.PauliZ(wires=0), qml.PauliZ(wires="b"), qml.PauliZ(wires="c")), - ], - ), - ), - ( - ps2_hamiltonian, - qml.Hamiltonian( - [-1.23, -4.0, 0.5], - [ - Tensor(qml.PauliX(wires=1), qml.PauliY(wires=2)), - Tensor(qml.PauliX(wires="a"), qml.PauliX(wires="b"), qml.PauliZ(wires="c")), - Tensor(qml.PauliZ(wires=0), qml.PauliZ(wires="b"), qml.PauliZ(wires="c")), - ], - ), - ), - ( - ps3, - qml.Hamiltonian( - [-0.5, 1.0], - [ - Tensor(qml.PauliZ(wires=0), qml.PauliZ(wires="b"), qml.PauliZ(wires="c")), - qml.Identity(wires=[0, "b", "c"]), - ], - ), - ), - ) - - @pytest.mark.usefixtures("legacy_opmath_only") - @pytest.mark.parametrize("ps, h", tup_ps_hamiltonian) - def test_hamiltonian(self, ps, h): - """Test that a PauliSentence can be cast to a Hamiltonian.""" - ps_h = ps.hamiltonian() - h = qml.operation.convert_to_legacy_H(h) - assert ps_h.compare(h) - - @pytest.mark.usefixtures("legacy_opmath_only") - def test_hamiltonian_empty(self): - """Test that an empty PauliSentence with wire_order returns Identity.""" - op = ps5.hamiltonian(wire_order=[0, 1]) - id = qml.Hamiltonian([], []) - assert op.compare(id) - - @pytest.mark.usefixtures("legacy_opmath_only") - def test_hamiltonian_empty_error(self): - """Test that a ValueError is raised if an empty PauliSentence is - cast to a Hamiltonian.""" - with pytest.raises( - ValueError, match="Can't get the Hamiltonian for an empty PauliSentence." - ): - ps5.hamiltonian() - - @pytest.mark.usefixtures("legacy_opmath_only") - def test_hamiltonian_wire_order(self): - """Test that the wire_order parameter is used when the pauli representation is empty""" - op = ps5.hamiltonian(wire_order=["a", "b"]) - id = qml.Hamiltonian([], []) - - qml.assert_equal(op, id) - - def test_hamiltonian_deprecation(self): - """Test that the correct deprecation warning is raised when calling hamiltonian()""" - with pytest.warns(qml.PennyLaneDeprecationWarning, match="PauliSentence.hamiltonian"): - _ = ps1.hamiltonian() - def test_pickling(self): """Check that paulisentences can be pickled and unpickled.""" word1 = PauliWord({2: "X", 3: "Y", 4: "Z"}) diff --git a/tests/pauli/test_pauli_interface.py b/tests/pauli/test_pauli_interface.py index 7bb31caa86c..42ffa93dde5 100644 --- a/tests/pauli/test_pauli_interface.py +++ b/tests/pauli/test_pauli_interface.py @@ -14,7 +14,6 @@ """ Unit tests for the :mod:`pauli` interface functions in ``pauli/pauli_interface.py``. """ -import numpy as np import pytest import pennylane as qml @@ -27,10 +26,9 @@ (qml.Identity(0), 1), (qml.PauliX(0) @ qml.PauliY(1), 1), (qml.PauliX(0) @ qml.PauliY(0), 1j), - (qml.operation.Tensor(qml.X(0), qml.Y(1)), 1), - (qml.operation.Tensor(qml.X(0), qml.Y(0)), 1j), (qml.Hamiltonian([-1.23], [qml.PauliZ(0)]), -1.23), (qml.prod(qml.PauliX(0), qml.PauliY(1)), 1), + (qml.prod(qml.X(0), qml.Y(0)), 1j), (qml.s_prod(1.23, qml.s_prod(-1j, qml.PauliZ(0))), -1.23j), ) @@ -41,13 +39,6 @@ def test_pauli_word_prefactor(op, true_prefactor): assert pauli_word_prefactor(op) == true_prefactor -def test_pauli_word_prefactor_tensor_error(): - """Test that an error is raised is the tensor is not a pauli sentence.""" - op = qml.operation.Tensor(qml.Hermitian(np.eye(2), wires=0), qml.Hadamard(wires=1)) - with pytest.raises(ValueError, match="Expected a valid Pauli word"): - pauli_word_prefactor(op) - - ops = ( qml.Hadamard(0), qml.Hadamard(0) @ qml.PauliZ(1), @@ -59,7 +50,6 @@ def test_pauli_word_prefactor_tensor_error(): ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("op", ops) def test_pauli_word_prefactor_raises_error(op): """Test that an error is raised when the operator provided is not a valid PauliWord.""" diff --git a/tests/pauli/test_pauli_utils.py b/tests/pauli/test_pauli_utils.py index a8c7e3a060a..d493ab1c736 100644 --- a/tests/pauli/test_pauli_utils.py +++ b/tests/pauli/test_pauli_utils.py @@ -17,7 +17,6 @@ # pylint: disable=too-few-public-methods,too-many-public-methods import functools import itertools -import warnings import numpy as np import pytest @@ -28,7 +27,6 @@ RY, U3, Hadamard, - Hamiltonian, Hermitian, Identity, PauliX, @@ -36,7 +34,6 @@ PauliZ, is_commuting, ) -from pennylane.operation import Tensor from pennylane.pauli import ( are_identical_pauli_words, are_pauli_words_qwc, @@ -54,7 +51,6 @@ pauli_word_to_string, qwc_complement_adj_matrix, qwc_rotation, - simplify, string_to_pauli_word, ) @@ -229,7 +225,8 @@ def test_observables_to_binary_matrix_n_qubits_arg(self): ValueError, observables_to_binary_matrix, observables, n_qubits_invalid ) - @pytest.mark.usefixtures("legacy_opmath_only") + # removed a fixture to only use legacy_opmath because its not clear why it there + # we'll see what happens when we are ready to run the tests def test_is_qwc(self): """Determining if two Pauli words are qubit-wise commuting.""" @@ -320,11 +317,11 @@ def test_is_qwc_not_binary_vectors(self): (PauliZ(1) @ PauliX(2) @ PauliZ(4), True), (PauliX(1) @ Hadamard(4), False), (Hadamard(0), False), - (Hamiltonian([], []), False), - (Hamiltonian([0.5], [PauliZ(1) @ PauliX(2)]), True), - (Hamiltonian([0.5], [PauliZ(1) @ PauliX(1)]), True), - (Hamiltonian([1.0], [Hadamard(0)]), False), - (Hamiltonian([1.0, 0.5], [PauliX(0), PauliZ(1) @ PauliX(2)]), False), + (qml.Hamiltonian([], []), False), + (qml.Hamiltonian([0.5], [PauliZ(1) @ PauliX(2)]), True), + (qml.Hamiltonian([0.5], [PauliZ(1) @ PauliX(1)]), True), + (qml.Hamiltonian([1.0], [Hadamard(0)]), False), + (qml.Hamiltonian([1.0, 0.5], [PauliX(0), PauliZ(1) @ PauliX(2)]), False), (qml.prod(qml.PauliX(0), qml.PauliY(0)), True), (qml.prod(qml.PauliX(0), qml.PauliY(1)), True), (qml.prod(qml.PauliX(0), qml.Hadamard(1)), False), @@ -352,7 +349,7 @@ class DummyOp(qml.operation.Operator): def test_are_identical_pauli_words(self): """Tests for determining if two Pauli words have the same ``wires`` and ``name`` attributes.""" - pauli_word_1 = Tensor(PauliX(0)) + pauli_word_1 = qml.ops.Prod(PauliX(0)) pauli_word_2 = PauliX(0) assert are_identical_pauli_words(pauli_word_1, pauli_word_2) @@ -360,7 +357,7 @@ def test_are_identical_pauli_words(self): pauli_word_1 = PauliX(0) @ PauliY(1) pauli_word_2 = PauliY(1) @ PauliX(0) - pauli_word_3 = Tensor(PauliX(0), PauliY(1)) + pauli_word_3 = qml.ops.Prod(PauliX(0), PauliY(1)) pauli_word_4 = PauliX(1) @ PauliZ(2) pauli_word_5 = qml.s_prod(1.5, qml.PauliX(0)) pauli_word_6 = qml.sum(qml.s_prod(0.5, qml.PauliX(0)), qml.s_prod(1.0, qml.PauliX(0))) @@ -375,14 +372,6 @@ def test_are_identical_pauli_words(self): assert not are_identical_pauli_words(pauli_word_7, pauli_word_4) assert not are_identical_pauli_words(pauli_word_6, pauli_word_4) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_are_identical_pauli_words_hamiltonian_unsupported(self): - """Test that using Hamiltonians that are valid Pauli words with are_identical_pauli_words - always returns False""" - pauli_word_1 = qml.Hamiltonian([1.0], [qml.PauliX(0)]) - pauli_word_2 = qml.PauliX(0) - assert not are_identical_pauli_words(pauli_word_1, pauli_word_2) - def test_identities_always_pauli_words(self): """Tests that identity terms are always identical.""" assert are_identical_pauli_words(qml.Identity(0), qml.Identity("a")) @@ -445,42 +434,18 @@ def test_qwc_complement_adj_matrix_exception(self): PAULI_WORD_STRINGS = _make_pauli_word_strings() - @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("pauli_word,wire_map,expected_string", PAULI_WORD_STRINGS) def test_pauli_word_to_string(self, pauli_word, wire_map, expected_string): """Test that Pauli words are correctly converted into strings.""" obtained_string = pauli_word_to_string(pauli_word, wire_map) assert obtained_string == expected_string - def test_pauli_word_to_string_tensor(self): - """Test pauli_word_to_string with tensor instances.""" - op = qml.operation.Tensor(qml.X(0), qml.Y(1)) - assert pauli_word_to_string(op) == "XY" - - op = qml.operation.Tensor(qml.Z(0), qml.Y(1), qml.X(2)) - assert pauli_word_to_string(op) == "ZYX" - - with qml.operation.disable_new_opmath_cm(warn=False): - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", "qml.ops.Hamiltonian uses", qml.PennyLaneDeprecationWarning - ) - PAULI_WORD_STRINGS_LEGACY = _make_pauli_word_strings() - - @pytest.mark.usefixtures("legacy_opmath_only") - @pytest.mark.parametrize("pauli_word,wire_map,expected_string", PAULI_WORD_STRINGS_LEGACY) - def test_pauli_word_to_string_legacy_opmath(self, pauli_word, wire_map, expected_string): - """Test that Pauli words are correctly converted into strings.""" - obtained_string = pauli_word_to_string(pauli_word, wire_map) - assert obtained_string == expected_string - @pytest.mark.parametrize("non_pauli_word", non_pauli_words) def test_pauli_word_to_string_invalid_input(self, non_pauli_word): """Ensure invalid inputs are handled properly when converting Pauli words to strings.""" with pytest.raises(TypeError): pauli_word_to_string(non_pauli_word) - @pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize( "pauli_string,wire_map,expected_pauli", [ @@ -735,7 +700,7 @@ def test_pauli_mult_using_prod(self, pauli_word_1, pauli_word_2, expected_produc obtained_product = qml.prod(pauli_word_1, pauli_word_2).simplify() if isinstance(obtained_product, qml.ops.SProd): # don't care about phase here obtained_product = obtained_product.base - assert obtained_product == qml.operation.convert_to_opmath(expected_product) + assert obtained_product == expected_product @pytest.mark.parametrize( "pauli_word_1,pauli_word_2,expected_phase", @@ -958,16 +923,10 @@ def test_diagonalize_pauli_word_catch_non_pauli_word(self, non_pauli_word): ), ] - @pytest.mark.parametrize("convert_to_opmath", (True, False)) @pytest.mark.parametrize("qwc_grouping,qwc_sol_tuple", qwc_diagonalization_io) - def test_diagonalize_qwc_pauli_words(self, qwc_grouping, qwc_sol_tuple, convert_to_opmath): + def test_diagonalize_qwc_pauli_words(self, qwc_grouping, qwc_sol_tuple): """Tests for validating diagonalize_qwc_pauli_words solutions.""" - if convert_to_opmath: - qwc_grouping = [qml.operation.convert_to_opmath(o) for o in qwc_grouping] - diag_terms = [qml.operation.convert_to_opmath(o) for o in qwc_sol_tuple[1]] - qwc_sol_tuple = (qwc_sol_tuple[0], diag_terms) - qwc_rot, diag_qwc_grouping = diagonalize_qwc_pauli_words(qwc_grouping) qwc_rot_sol, diag_qwc_grouping_sol = qwc_sol_tuple @@ -993,77 +952,9 @@ def test_diagonalize_qwc_pauli_words_catch_when_not_qwc(self, not_qwc_grouping): assert pytest.raises(ValueError, diagonalize_qwc_pauli_words, not_qwc_grouping) - @pytest.mark.usefixtures( - "legacy_opmath_only" - ) # Handling a LinearCombination is not a problem under new opmath anymore - def test_diagonalize_qwc_pauli_words_catch_invalid_type(self): - """Test for ValueError raise when diagonalize_qwc_pauli_words is given a list - containing invalid operator types.""" - invalid_ops = [qml.PauliX(0), qml.Hamiltonian([1.0], [qml.PauliZ(1)])] - - with pytest.raises(ValueError, match="This function only supports pauli words."): - _ = diagonalize_qwc_pauli_words(invalid_ops) - - -class TestObservableHF: - - with qml.operation.disable_new_opmath_cm(warn=False): - HAMILTONIAN_SIMPLIFY = [ - ( - qml.Hamiltonian( - np.array([0.5, 0.5]), - [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliY(1)], - ), - qml.Hamiltonian(np.array([1.0]), [qml.PauliX(0) @ qml.PauliY(1)]), - ), - ( - qml.Hamiltonian( - np.array([0.5, -0.5]), - [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliY(1)], - ), - qml.Hamiltonian([], []), - ), - ( - qml.Hamiltonian( - np.array([0.0, -0.5]), - [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliZ(1)], - ), - qml.Hamiltonian(np.array([-0.5]), [qml.PauliX(0) @ qml.PauliZ(1)]), - ), - ( - qml.Hamiltonian( - np.array([0.25, 0.25, 0.25, -0.25]), - [ - qml.PauliX(0) @ qml.PauliY(1), - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliX(0) @ qml.PauliY(1), - qml.PauliX(0) @ qml.PauliY(1), - ], - ), - qml.Hamiltonian( - np.array([0.25, 0.25]), - [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliZ(1)], - ), - ), - ] - - @pytest.mark.usefixtures("legacy_opmath_only") - @pytest.mark.parametrize(("hamiltonian", "result"), HAMILTONIAN_SIMPLIFY) - def test_simplify(self, hamiltonian, result): - r"""Test that simplify returns the correct hamiltonian.""" - h = simplify(hamiltonian) - assert h.compare(result) - - def test_simplify_deprecation(self): - """Test that a deprecation warning is raised when using simplify""" - with pytest.warns(qml.PennyLaneDeprecationWarning, match="qml.ops.Hamiltonian"): - h = qml.ops.Hamiltonian([1.5, 2.5], [qml.X(0), qml.Z(0)]) - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="qml.pauli.simplify"): - _ = simplify(h) - -@pytest.mark.usefixtures("legacy_opmath_only") +# removed a fixture to only use legacy_opmath because its not clear why it there +# we'll see what happens when we are ready to run the tests class TestTapering: terms_bin_mat_data = [ diff --git a/tests/pulse/test_rydberg.py b/tests/pulse/test_rydberg.py index 35ad7bb164e..dc17dbe43a3 100644 --- a/tests/pulse/test_rydberg.py +++ b/tests/pulse/test_rydberg.py @@ -205,7 +205,6 @@ def f(p, t): assert len(Hd.ops) == 1 qml.assert_equal(Hd.ops[0], ops_expected[0]) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_no_detuning(self): """Test that when detuning not specified, the drive term is correctly defined.""" diff --git a/tests/pytrees/test_pytrees.py b/tests/pytrees/test_pytrees.py index 518938c8a5c..d6f5e946203 100644 --- a/tests/pytrees/test_pytrees.py +++ b/tests/pytrees/test_pytrees.py @@ -105,7 +105,6 @@ def test_dict(): assert new_x == {"a": 5, "b": {"c": 6, "d": 7}} -@pytest.mark.usefixtures("new_opmath_only") def test_nested_pl_object(): """Test that we can flatten and unflatten nested pennylane object.""" diff --git a/tests/qchem/openfermion_pyscf_tests/test_convert.py b/tests/qchem/openfermion_pyscf_tests/test_convert.py index 99f143c9720..c5cc661c881 100644 --- a/tests/qchem/openfermion_pyscf_tests/test_convert.py +++ b/tests/qchem/openfermion_pyscf_tests/test_convert.py @@ -24,14 +24,12 @@ import pennylane as qml from pennylane import numpy as np from pennylane import qchem -from pennylane.operation import active_new_opmath openfermion = pytest.importorskip("openfermion") openfermionpyscf = pytest.importorskip("openfermionpyscf") pyscf = pytest.importorskip("pyscf") pauli_ops_and_prod = (qml.PauliX, qml.PauliY, qml.PauliZ, qml.Identity, qml.ops.Prod) -pauli_ops_and_tensor = (qml.PauliX, qml.PauliY, qml.PauliZ, qml.Identity, qml.operation.Tensor) @pytest.fixture( @@ -394,7 +392,6 @@ def test_observable_conversion(_, terms_ref, custom_wires, monkeypatch): ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("pl_op, of_op, wire_order", ops_wires) def test_operation_conversion(pl_op, of_op, wire_order): """Assert the conversion between pennylane and openfermion operators""" @@ -404,10 +401,7 @@ def test_operation_conversion(pl_op, of_op, wire_order): converted_of_op = qml.qchem.convert._openfermion_to_pennylane(of_op) _, converted_of_op_terms = converted_of_op - assert all( - isinstance(term, pauli_ops_and_prod if active_new_opmath() else pauli_ops_and_tensor) - for term in converted_of_op_terms - ) + assert all(isinstance(term, pauli_ops_and_prod) for term in converted_of_op_terms) assert np.allclose( qml.matrix(qml.dot(*pl_op), wire_order=wire_order), @@ -433,7 +427,7 @@ def test_convert_format_not_supported(terms_ref, lib_name, monkeypatch): invalid_ops = ( - qml.operation.Tensor(qml.PauliZ(0), qml.QuadOperator(0.1, wires=1)), + qml.prod(qml.PauliZ(0), qml.QuadOperator(0.1, wires=1)), qml.prod(qml.PauliX(0), qml.Hadamard(1)), qml.sum(qml.PauliZ(0), qml.Hadamard(1)), ) @@ -447,7 +441,7 @@ def test_not_xyz_pennylane_to_openfermion(op): qml.qchem.convert._pennylane_to_openfermion( np.array([0.1 + 0.0j, 0.0]), [ - qml.operation.Tensor(qml.PauliX(0)), + qml.prod(qml.PauliX(0)), op, ], ) @@ -462,8 +456,8 @@ def test_wires_not_covered_pennylane_to_openfermion(): qml.qchem.convert._pennylane_to_openfermion( np.array([0.1, 0.2]), [ - qml.operation.Tensor(qml.PauliX(wires=["w0"])), - qml.operation.Tensor(qml.PauliY(wires=["w0"]), qml.PauliZ(wires=["w2"])), + qml.prod(qml.PauliX(wires=["w0"])), + qml.prod(qml.PauliY(wires=["w0"]), qml.PauliZ(wires=["w2"])), ], wires=qml.wires.Wires(["w0", "w1"]), ) @@ -529,14 +523,13 @@ def test_types_consistency(): ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("of_op, pl_h, pl_op, wires", of_pl_ops) def test_import_operator(of_op, pl_h, pl_op, wires): """Test the import_operator function correctly imports an OpenFermion operator into a PL one.""" of_h = qml.qchem.convert.import_operator(of_op, "openfermion", wires=wires) assert qml.pauli.pauli_sentence(pl_h) == qml.pauli.pauli_sentence(of_h) - assert isinstance(of_h, type(pl_op) if active_new_opmath() else qml.Hamiltonian) + assert isinstance(of_h, type(pl_op)) if isinstance(of_h, qml.ops.Sum): assert all( @@ -614,8 +607,8 @@ def test_pennylane_to_openfermion_no_decomp(): """Test the _pennylane_to_openfermion function with custom wires.""" coeffs = np.array([0.1, 0.2]) ops = [ - qml.operation.Tensor(qml.PauliX(wires=["w0"])), - qml.operation.Tensor(qml.PauliY(wires=["w0"]), qml.PauliZ(wires=["w2"])), + qml.prod(qml.PauliX(wires=["w0"])), + qml.prod(qml.PauliY(wires=["w0"]), qml.PauliZ(wires=["w2"])), ] op_str = str( qml.qchem.convert._pennylane_to_openfermion( @@ -736,8 +729,8 @@ def test_fail_import_openfermion(monkeypatch): qml.qchem.convert._pennylane_to_openfermion( np.array([0.1 + 0.0j, 0.0]), [ - qml.operation.Tensor(qml.PauliX(0)), - qml.operation.Tensor(qml.PauliZ(0), qml.QuadOperator(0.1, wires=1)), + qml.prod(qml.PauliX(0)), + qml.prod(qml.PauliZ(0), qml.QuadOperator(0.1, wires=1)), ], ) diff --git a/tests/qchem/openfermion_pyscf_tests/test_convert_openfermion.py b/tests/qchem/openfermion_pyscf_tests/test_convert_openfermion.py index 2938ca07a29..cb2cc19020f 100644 --- a/tests/qchem/openfermion_pyscf_tests/test_convert_openfermion.py +++ b/tests/qchem/openfermion_pyscf_tests/test_convert_openfermion.py @@ -310,7 +310,7 @@ def test_mapping_wires(self, pl_op, of_op, wires): assert q_op == of_op INVALID_OPS = ( - qml.operation.Tensor(qml.PauliZ(0), qml.QuadOperator(0.1, wires=1)), + qml.prod(qml.PauliZ(0), qml.QuadOperator(0.1, wires=1)), qml.prod(qml.PauliX(0), qml.Hadamard(1)), qml.sum(qml.PauliZ(0), qml.Hadamard(1)), ) @@ -321,7 +321,7 @@ def test_not_xyz(self, op): _match = "Expected a Pennylane operator with a valid Pauli word representation," pl_op = qml.ops.LinearCombination( - np.array([0.1 + 0.0j, 0.0]), [qml.operation.Tensor(qml.PauliX(0)), op] + np.array([0.1 + 0.0j, 0.0]), [qml.prod(qml.PauliX(0)), op] ) with pytest.raises(ValueError, match=_match): qml.to_openfermion(qml.to_openfermion(pl_op)) diff --git a/tests/qchem/openfermion_pyscf_tests/test_dipole_of.py b/tests/qchem/openfermion_pyscf_tests/test_dipole_of.py index 739965c5813..710a757923c 100644 --- a/tests/qchem/openfermion_pyscf_tests/test_dipole_of.py +++ b/tests/qchem/openfermion_pyscf_tests/test_dipole_of.py @@ -207,7 +207,7 @@ (h2o, x_h2o, 0, range(4), [4, 5], "bravyi_kitaev", coeffs_h2o, ops_h2o), ], ) -@pytest.mark.usefixtures("skip_if_no_openfermion_support", "use_legacy_and_new_opmath") +@pytest.mark.usefixtures("skip_if_no_openfermion_support") def test_dipole_obs(symbols, coords, charge, core, active, mapping, coeffs, ops, tol, tmpdir): r"""Tests the correctness of the dipole observable computed by the ``dipole`` function.""" @@ -230,15 +230,6 @@ def test_dipole_obs(symbols, coords, charge, core, active, mapping, coeffs, ops, assert np.allclose(calc_coeffs, exp_coeffs, **tol) r_ops = ops[i] - if not qml.operation.active_new_opmath(): - r_ops = [ - ( - qml.operation.Tensor(*obs.simplify()) - if isinstance(obs.simplify(), (qml.ops.op_math.Prod)) - else obs.simplify() - ) - for obs in ops[i] - ] assert all(isinstance(o1, o2.__class__) for o1, o2 in zip(d_ops, r_ops)) for o1, o2 in zip(d_ops, r_ops): diff --git a/tests/qchem/openfermion_pyscf_tests/test_molecular_dipole.py b/tests/qchem/openfermion_pyscf_tests/test_molecular_dipole.py index 3102d07a579..3c4202bd6ac 100644 --- a/tests/qchem/openfermion_pyscf_tests/test_molecular_dipole.py +++ b/tests/qchem/openfermion_pyscf_tests/test_molecular_dipole.py @@ -227,7 +227,6 @@ (h2o, x_h2o, 0, 2, 2, "bravyi_kitaev", coeffs_h2o, ops_h2o), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_openfermion_molecular_dipole( symbols, geometry, charge, active_el, active_orb, mapping, coeffs, ops, tol, tmpdir ): @@ -252,15 +251,6 @@ def test_openfermion_molecular_dipole( assert np.allclose(calc_coeffs, exp_coeffs, **tol) r_ops = ops[i] - if not qml.operation.active_new_opmath(): - r_ops = [ - ( - qml.operation.Tensor(*obs.simplify()) - if isinstance(obs.simplify(), (qml.ops.op_math.Prod)) - else obs.simplify() - ) - for obs in ops[i] - ] assert all(isinstance(o1, o2.__class__) for o1, o2 in zip(d_ops, r_ops)) for o1, o2 in zip(d_ops, r_ops): @@ -284,7 +274,6 @@ def test_openfermion_molecular_dipole( (h2o, x_h2o, 0, 2, 2, "bravyi_kitaev", eig_h2o), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_differentiable_molecular_dipole( symbols, geometry, charge, active_el, active_orb, mapping, eig_ref, tmpdir ): @@ -309,7 +298,6 @@ def test_differentiable_molecular_dipole( assert np.allclose(np.sort(eig), np.sort(eig_ref[idx])) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( ("wiremap"), [ @@ -379,7 +367,7 @@ def test_molecular_dipole_error(): ), ], ) -@pytest.mark.usefixtures("skip_if_no_openfermion_support", "use_legacy_and_new_opmath") +@pytest.mark.usefixtures("skip_if_no_openfermion_support") def test_real_dipole(method, args, tmpdir): r"""Test that the generated operator has real coefficients.""" diff --git a/tests/qchem/openfermion_pyscf_tests/test_molecular_hamiltonian.py b/tests/qchem/openfermion_pyscf_tests/test_molecular_hamiltonian.py index 9285facb58b..6985cda41ac 100644 --- a/tests/qchem/openfermion_pyscf_tests/test_molecular_hamiltonian.py +++ b/tests/qchem/openfermion_pyscf_tests/test_molecular_hamiltonian.py @@ -21,7 +21,6 @@ from pennylane import I, X, Y, Z from pennylane import numpy as np from pennylane import qchem -from pennylane.operation import active_new_opmath test_symbols = ["C", "C", "N", "H", "H", "H", "H", "H"] test_coordinates = np.array( @@ -70,7 +69,7 @@ (2, 1, "pyscf", 2, 2, "BRAVYI_kitaev"), ], ) -@pytest.mark.usefixtures("skip_if_no_openfermion_support", "use_legacy_and_new_opmath") +@pytest.mark.usefixtures("skip_if_no_openfermion_support") def test_building_hamiltonian( charge, mult, @@ -98,10 +97,7 @@ def test_building_hamiltonian( built_hamiltonian, qubits = qchem.molecular_hamiltonian(*args, **kwargs) - if active_new_opmath(): - assert not isinstance(built_hamiltonian, qml.Hamiltonian) - else: - assert isinstance(built_hamiltonian, qml.Hamiltonian) + assert isinstance(built_hamiltonian, qml.ops.Sum) assert qubits == 2 * nact_orbs @@ -121,7 +117,7 @@ def test_building_hamiltonian( (2, 1, "pyscf", 2, 2, "BRAVYI_kitaev"), ], ) -@pytest.mark.usefixtures("skip_if_no_openfermion_support", "use_legacy_and_new_opmath") +@pytest.mark.usefixtures("skip_if_no_openfermion_support") def test_building_hamiltonian_molecule_class( charge, mult, @@ -147,10 +143,7 @@ def test_building_hamiltonian_molecule_class( built_hamiltonian, qubits = qchem.molecular_hamiltonian(args, **kwargs) - if active_new_opmath(): - assert not isinstance(built_hamiltonian, qml.Hamiltonian) - else: - assert isinstance(built_hamiltonian, qml.Hamiltonian) + assert isinstance(built_hamiltonian, qml.ops.Sum) assert qubits == 2 * nact_orbs @@ -348,7 +341,6 @@ def test_building_hamiltonian_molecule_class( ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_differentiable_hamiltonian(symbols, geometry, mapping, h_ref_data): r"""Test that molecular_hamiltonian returns the correct Hamiltonian with the differentiable backend.""" @@ -362,10 +354,7 @@ def test_differentiable_hamiltonian(symbols, geometry, mapping, h_ref_data): geometry.requires_grad = False h_noargs = qchem.molecular_hamiltonian(symbols, geometry, method="dhf", mapping=mapping)[0] - ops = [ - qml.operation.Tensor(*op) if isinstance(op, qml.ops.Prod) else op - for op in map(qml.simplify, h_ref_data[1]) - ] + ops = list(map(qml.simplify, h_ref_data[1])) h_ref = qml.Hamiltonian(h_ref_data[0], ops) h_ref_coeffs, h_ref_ops = h_ref.terms() @@ -580,7 +569,6 @@ def test_differentiable_hamiltonian(symbols, geometry, mapping, h_ref_data): ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_differentiable_hamiltonian_molecule_class(symbols, geometry, mapping, h_ref_data): r"""Test that molecular_hamiltonian generated using the molecule class returns the correct Hamiltonian with the differentiable backend.""" @@ -594,10 +582,7 @@ def test_differentiable_hamiltonian_molecule_class(symbols, geometry, mapping, h molecule = qchem.Molecule(symbols, geometry) h_noargs = qchem.molecular_hamiltonian(molecule, method="dhf", mapping=mapping)[0] - ops = [ - qml.operation.Tensor(*op) if isinstance(op, qml.ops.Prod) else op - for op in map(qml.simplify, h_ref_data[1]) - ] + ops = list(map(qml.simplify, h_ref_data[1])) h_ref = qml.Hamiltonian(h_ref_data[0], ops) h_ref_coeffs, h_ref_ops = h_ref.terms() @@ -618,7 +603,6 @@ def test_differentiable_hamiltonian_molecule_class(symbols, geometry, mapping, h ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( ("wiremap"), [ @@ -670,7 +654,6 @@ def test_custom_wiremap_hamiltonian_pyscf_molecule_class(wiremap, tmpdir): assert set(hamiltonian.wires) == set(wiremap) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( ("wiremap", "args"), [ @@ -711,7 +694,6 @@ def test_custom_wiremap_hamiltonian_dhf(wiremap, args, tmpdir): assert wiremap_calc == wiremap_dict -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( ("wiremap", "args"), [ @@ -853,7 +835,7 @@ def test_diff_hamiltonian_error_molecule_class(): ), ], ) -@pytest.mark.usefixtures("skip_if_no_openfermion_support", "use_legacy_and_new_opmath") +@pytest.mark.usefixtures("skip_if_no_openfermion_support") def test_real_hamiltonian(method, args, tmpdir): r"""Test that the generated Hamiltonian has real coefficients.""" @@ -888,7 +870,7 @@ def test_real_hamiltonian(method, args, tmpdir): ), ], ) -@pytest.mark.usefixtures("skip_if_no_openfermion_support", "use_legacy_and_new_opmath") +@pytest.mark.usefixtures("skip_if_no_openfermion_support") def test_real_hamiltonian_molecule_class(method, args, tmpdir): r"""Test that the generated Hamiltonian has real coefficients.""" @@ -940,7 +922,7 @@ def test_pyscf_integrals(symbols, geometry, core_ref, one_ref, two_ref): assert np.allclose(two, two_ref) -@pytest.mark.usefixtures("skip_if_no_openfermion_support", "use_legacy_and_new_opmath") +@pytest.mark.usefixtures("skip_if_no_openfermion_support") def test_molecule_as_kwargs(tmpdir): r"""Test that molecular_hamiltonian function works with molecule as keyword argument @@ -958,10 +940,7 @@ def test_molecule_as_kwargs(tmpdir): outpath=tmpdir.strpath, ) - if active_new_opmath(): - assert not isinstance(built_hamiltonian, qml.Hamiltonian) - else: - assert isinstance(built_hamiltonian, qml.Hamiltonian) + assert isinstance(built_hamiltonian, qml.ops.Sum) assert qubits == 4 @@ -1233,7 +1212,6 @@ def test_error_raised_for_missing_molecule_information(): ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_mapped_hamiltonian_pyscf_openfermion( symbols, geometry, charge, mapping, h_ref_data, tmpdir ): @@ -1247,10 +1225,7 @@ def test_mapped_hamiltonian_pyscf_openfermion( molecule, method=method, mapping=mapping, outpath=tmpdir.strpath )[0] - ops = [ - qml.operation.Tensor(*op) if isinstance(op, qml.ops.Prod) else op - for op in map(qml.simplify, h_ref_data[1]) - ] + ops = list(map(qml.simplify, h_ref_data[1])) h_ref = qml.Hamiltonian(h_ref_data[0], ops) h_ref_coeffs, h_ref_ops = h_ref.terms() diff --git a/tests/qchem/test_dipole.py b/tests/qchem/test_dipole.py index 5856ad5087a..c49967039d6 100644 --- a/tests/qchem/test_dipole.py +++ b/tests/qchem/test_dipole.py @@ -22,7 +22,6 @@ from pennylane import numpy as np from pennylane import qchem from pennylane.fermi import from_string -from pennylane.operation import Tensor @pytest.mark.parametrize( @@ -183,13 +182,12 @@ def test_fermionic_dipole(symbols, geometry, core, charge, active, f_ref): ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_dipole_moment(symbols, geometry, core, charge, active, coeffs, ops): r"""Test that dipole_moment returns the correct result.""" mol = qchem.Molecule(symbols, geometry, charge=charge) args = [p for p in [geometry] if p.requires_grad] d = qchem.dipole_moment(mol, core=core, active=active, cutoff=1.0e-8)(*args)[0] - dops = [Tensor(*op) if isinstance(op, qml.ops.Prod) else op for op in map(qml.simplify, ops)] + dops = list(map(qml.simplify, ops)) d_ref = qml.Hamiltonian(coeffs, dops) d_coeff, d_ops = d.terms() @@ -216,7 +214,6 @@ def test_dipole_moment(symbols, geometry, core, charge, active, coeffs, ops): ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_dipole_moment_631g_basis(symbols, geometry, core, active): r"""Test that the dipole moment is constructed properly with basis sets having different numbers of primitive Gaussian functions.""" diff --git a/tests/qchem/test_factorization.py b/tests/qchem/test_factorization.py index bea18d3c09a..b1261faff65 100644 --- a/tests/qchem/test_factorization.py +++ b/tests/qchem/test_factorization.py @@ -316,7 +316,6 @@ def test_empty_error(two_tensor): ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_basis_rotation_output( one_matrix, two_tensor, tol_factor, coeffs_ref, ops_ref, eigvecs_ref ): @@ -364,7 +363,6 @@ def test_basis_rotation_output( ) ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_basis_rotation_utransform(core, one_electron, two_electron): r"""Test that basis_rotation function returns the correct transformation matrices. This test constructs the matrix representation of a factorized Hamiltonian and then applies the diff --git a/tests/qchem/test_hamiltonians.py b/tests/qchem/test_hamiltonians.py index 26c640b3f8e..09b8dde8325 100644 --- a/tests/qchem/test_hamiltonians.py +++ b/tests/qchem/test_hamiltonians.py @@ -22,7 +22,6 @@ from pennylane import numpy as np from pennylane import qchem from pennylane.fermi import from_string -from pennylane.operation import active_new_opmath @pytest.mark.parametrize( @@ -224,7 +223,6 @@ def test_fermionic_hamiltonian(symbols, geometry, alpha, h_ref): ) ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_diff_hamiltonian(symbols, geometry, h_ref_data): r"""Test that diff_hamiltonian returns the correct Hamiltonian.""" @@ -232,10 +230,7 @@ def test_diff_hamiltonian(symbols, geometry, h_ref_data): args = [] h = qchem.diff_hamiltonian(mol)(*args) - ops = [ - qml.operation.Tensor(*op) if isinstance(op, qml.ops.Prod) else op - for op in map(qml.simplify, h_ref_data[1]) - ] + ops = list(map(qml.simplify, h_ref_data[1])) h_ref = qml.Hamiltonian(h_ref_data[0], ops) assert np.allclose(np.sort(h.terms()[0]), np.sort(h_ref.terms()[0])) @@ -243,7 +238,7 @@ def test_diff_hamiltonian(symbols, geometry, h_ref_data): qml.Hamiltonian(np.ones(len(h_ref.terms()[0])), h_ref.terms()[1]) ) - assert isinstance(h, qml.ops.Sum if active_new_opmath() else qml.Hamiltonian) + assert isinstance(h, qml.ops.Sum) wire_order = h_ref.wires assert np.allclose( @@ -252,7 +247,6 @@ def test_diff_hamiltonian(symbols, geometry, h_ref_data): ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_diff_hamiltonian_active_space(): r"""Test that diff_hamiltonian works when an active space is defined.""" @@ -264,7 +258,7 @@ def test_diff_hamiltonian_active_space(): h = qchem.diff_hamiltonian(mol, core=[0], active=[1, 2])(*args) - assert isinstance(h, qml.ops.Sum if active_new_opmath() else qml.Hamiltonian) + assert isinstance(h, qml.ops.Sum) @pytest.mark.parametrize( @@ -345,7 +339,6 @@ def circuit(*args): assert np.allclose(grad_qml[0][0], grad_finitediff) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") class TestJax: @pytest.mark.jax def test_gradient_expvalH(self): diff --git a/tests/qchem/test_observable_hf.py b/tests/qchem/test_observable_hf.py index f6cbd120ec9..f10dd9c9b31 100644 --- a/tests/qchem/test_observable_hf.py +++ b/tests/qchem/test_observable_hf.py @@ -166,14 +166,10 @@ def test_fermionic_observable(core_constant, integral_one, integral_two, f_ref): (1.23 * from_string(""), [[1.23], [qml.Identity(0)]]), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_qubit_observable(f_observable, q_observable): r"""Test that qubit_observable returns the correct operator.""" h_as_op = qchem.qubit_observable(f_observable) - ops = [ - qml.operation.Tensor(*op) if isinstance(op, qml.ops.Prod) else op - for op in map(qml.simplify, q_observable[1]) - ] + ops = list(map(qml.simplify, q_observable[1])) h_ref = qml.Hamiltonian(q_observable[0], ops) assert h_ref.compare(h_as_op) @@ -195,7 +191,6 @@ def test_qubit_observable(f_observable, q_observable): ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_qubit_observable_cutoff(f_observable, cut_off): """Test that qubit_observable returns the correct operator when a cutoff is provided.""" h_ref, h_ref_op = (qml.Hamiltonian([], []), qml.s_prod(0, qml.Identity(0))) diff --git a/tests/qchem/test_particle_number.py b/tests/qchem/test_particle_number.py index 2c244f78882..bd1987ab205 100644 --- a/tests/qchem/test_particle_number.py +++ b/tests/qchem/test_particle_number.py @@ -20,10 +20,8 @@ from pennylane import Identity, PauliZ from pennylane import numpy as np from pennylane import qchem -from pennylane.operation import active_new_opmath -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( ("orbitals", "coeffs_ref", "ops_ref"), [ @@ -60,7 +58,7 @@ def test_particle_number(orbitals, coeffs_ref, ops_ref): n = qchem.particle_number(orbitals) n_ref = qml.Hamiltonian(coeffs_ref, ops_ref) assert n_ref.compare(n) - assert isinstance(n, qml.ops.Sum if active_new_opmath() else qml.Hamiltonian) + assert isinstance(n, qml.ops.Sum) wire_order = n_ref.wires assert np.allclose( diff --git a/tests/qchem/test_spin.py b/tests/qchem/test_spin.py index dbe7fb39d50..294254566d4 100644 --- a/tests/qchem/test_spin.py +++ b/tests/qchem/test_spin.py @@ -20,7 +20,6 @@ from pennylane import Identity, PauliX, PauliY, PauliZ from pennylane import numpy as np from pennylane import qchem, simplify -from pennylane.operation import Tensor, active_new_opmath @pytest.mark.parametrize( @@ -116,7 +115,6 @@ def test_spin2_matrix_elements(n_spin_orbs, matrix_ref): assert np.allclose(s2_me_result, matrix_ref) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( ("electrons", "orbitals", "coeffs_ref", "ops_ref"), [ @@ -175,10 +173,10 @@ def test_spin2(electrons, orbitals, coeffs_ref, ops_ref): built by the function `'spin2'`. """ s2 = qchem.spin.spin2(electrons, orbitals) - sops = [Tensor(*op) if isinstance(op, qml.ops.Prod) else op for op in map(simplify, ops_ref)] + sops = list(map(simplify, ops_ref)) s2_ref = qml.Hamiltonian(coeffs_ref, sops) assert s2_ref.compare(s2) - assert isinstance(s2, qml.ops.Sum if active_new_opmath() else qml.Hamiltonian) + assert isinstance(s2, qml.ops.Sum) wire_order = s2_ref.wires assert np.allclose( @@ -204,7 +202,6 @@ def test_exception_spin2(electrons, orbitals, msg_match): qchem.spin.spin2(electrons, orbitals) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( ("orbitals", "coeffs_ref", "ops_ref"), [ @@ -234,7 +231,7 @@ def test_spinz(orbitals, coeffs_ref, ops_ref): sz = qchem.spin.spinz(orbitals) sz_ref = qml.Hamiltonian(coeffs_ref, ops_ref) assert sz_ref.compare(sz) - assert isinstance(sz, qml.ops.Sum if active_new_opmath() else qml.Hamiltonian) + assert isinstance(sz, qml.ops.Sum) wire_order = sz_ref.wires assert np.allclose( diff --git a/tests/qchem/test_structure.py b/tests/qchem/test_structure.py index 6fda2ba8a21..82f01c45204 100644 --- a/tests/qchem/test_structure.py +++ b/tests/qchem/test_structure.py @@ -354,9 +354,9 @@ def test_hf_state_basis(electrons, symbols, geometry, charge): state_parity = qchem.hf_state(electrons, qubits, basis="parity") state_bk = qchem.hf_state(electrons, qubits, basis="bravyi_kitaev") - h_occ = qml.jordan_wigner(h_ferm, ps=True, tol=1e-16).hamiltonian() - h_parity = qml.parity_transform(h_ferm, qubits, ps=True, tol=1e-16).hamiltonian() - h_bk = qml.bravyi_kitaev(h_ferm, qubits, ps=True, tol=1e-16).hamiltonian() + h_occ = qml.jordan_wigner(h_ferm, ps=True, tol=1e-16).operation() + h_parity = qml.parity_transform(h_ferm, qubits, ps=True, tol=1e-16).operation() + h_bk = qml.bravyi_kitaev(h_ferm, qubits, ps=True, tol=1e-16).operation() dev = qml.device("default.qubit", wires=qubits) diff --git a/tests/qchem/test_tapering.py b/tests/qchem/test_tapering.py index 08644749ba2..05388b27060 100644 --- a/tests/qchem/test_tapering.py +++ b/tests/qchem/test_tapering.py @@ -196,7 +196,6 @@ def test_kernel(binary_matrix, result): ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_generate_paulis(generators, num_qubits, result): r"""Test that generate_paulis returns the correct result.""" pauli_ops = qml.paulix_ops(generators, num_qubits) @@ -225,7 +224,6 @@ def test_generate_paulis(generators, num_qubits, result): ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_symmetry_generators(symbols, geometry, res_generators): r"""Test that symmetry_generators returns the correct result.""" @@ -264,7 +262,6 @@ def test_symmetry_generators(symbols, geometry, res_generators): ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_clifford(generator, paulixops, result): r"""Test that clifford returns the correct operator.""" u = clifford(generator, paulixops) @@ -296,7 +293,6 @@ def test_clifford(generator, paulixops, result): ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_transform_hamiltonian(symbols, geometry, generator, paulixops, paulix_sector, ham_ref): r"""Test that transform_hamiltonian returns the correct hamiltonian.""" mol = qml.qchem.Molecule(symbols, geometry) @@ -353,7 +349,6 @@ def test_transform_hamiltonian(symbols, geometry, generator, paulixops, paulix_s ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_optimal_sector(symbols, geometry, charge, generators, num_electrons, result): r"""Test that find_optimal_sector returns the correct result.""" mol = qml.qchem.Molecule(symbols, geometry, charge) @@ -459,7 +454,6 @@ def test_exceptions_optimal_sector(symbols, geometry, generators, num_electrons, ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_transform_hf(generators, paulixops, paulix_sector, num_electrons, num_wires, result): r"""Test that transform_hf returns the correct result.""" @@ -507,7 +501,6 @@ def test_transform_hf(generators, paulixops, paulix_sector, num_electrons, num_w ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_taper_obs(symbols, geometry, charge): r"""Test that the expectation values of tapered observables with respect to the tapered Hartree-Fock state (:math:`\langle HF|obs|HF \rangle`) are consistent.""" @@ -603,7 +596,6 @@ def test_taper_obs(symbols, geometry, charge): ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_taper_excitations( symbols, geometry, charge, generators, paulixops, paulix_sector, num_commuting ): @@ -738,7 +730,6 @@ def test_inconsistent_taper_ops(operation, op_gen, message_match): ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_consistent_taper_ops(operation, op_gen): r"""Test that operations are tapered consistently when their generators are provided manually and when they are constructed internally""" @@ -897,7 +888,6 @@ def test_taper_callable_ops(operation, op_wires, op_gen): ), ], ) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_taper_matrix_ops(operation, op_wires, op_gen): """Test that taper_operation can be used with gate operation built using matrices""" diff --git a/tests/resource/test_specs.py b/tests/resource/test_specs.py index aebe0af0d19..a02b35ef97b 100644 --- a/tests/resource/test_specs.py +++ b/tests/resource/test_specs.py @@ -209,11 +209,11 @@ def circ(): specs = qml.specs(circ)() assert specs["resources"].num_gates == 1 - assert specs["num_diagonalizing_gates"] == (1 if qml.operation.active_new_opmath() else 0) + assert specs["num_diagonalizing_gates"] == 1 specs = qml.specs(circ, level="device")() assert specs["resources"].num_gates == 3 - assert specs["num_diagonalizing_gates"] == (3 if qml.operation.active_new_opmath() else 0) + assert specs["num_diagonalizing_gates"] == 3 def test_splitting_transforms(self): coeffs = [0.2, -0.543, 0.1] diff --git a/tests/shadow/test_shadow_class.py b/tests/shadow/test_shadow_class.py index e9fcb8ef90a..e6bae91ecb6 100644 --- a/tests/shadow/test_shadow_class.py +++ b/tests/shadow/test_shadow_class.py @@ -76,7 +76,6 @@ class TestIntegrationShadows: """Integration tests for classical shadows class""" @pytest.mark.parametrize("shadow", shadows) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_pauli_string_expval(self, shadow): """Testing the output of expectation values match those of exact evaluation""" @@ -100,11 +99,8 @@ def test_pauli_string_expval(self, shadow): @pytest.mark.parametrize("H", Hs) @pytest.mark.parametrize("shadow", shadows) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_expval_input_types(self, shadow, H): """Test ClassicalShadow.expval can handle different inputs""" - if not qml.operation.active_new_opmath(): - H = qml.operation.convert_to_legacy_H(H) assert qml.math.allclose(shadow.expval(H, k=2), 1.0, atol=1e-1) def test_reconstruct_bell_state(self): @@ -349,12 +345,7 @@ def test_non_pauli_error(self): H = qml.Hadamard(0) @ qml.Hadamard(2) - msg = ( - "Observable must have a valid pauli representation" - if qml.operation.active_new_opmath() - else "Observable must be a linear combination of Pauli observables" - ) - with pytest.raises(ValueError, match=msg): + with pytest.raises(ValueError, match="Observable must have a valid pauli representation"): shadow.expval(H, k=10) def test_non_pauli_error_no_pauli_rep(self): @@ -365,11 +356,7 @@ def test_non_pauli_error_no_pauli_rep(self): H = qml.Hadamard(0) @ qml.Hadamard(2) - legacy_msg = "Observable must be a linear combination of Pauli observables" - new_opmath_msg = "Observable must have a valid pauli representation." - msg = new_opmath_msg if qml.operation.active_new_opmath() else legacy_msg - - with pytest.raises(ValueError, match=msg): + with pytest.raises(ValueError, match="Observable must have a valid pauli representation."): shadow.expval(H, k=10) diff --git a/tests/spin/test_spin_hamiltonian.py b/tests/spin/test_spin_hamiltonian.py index 6caccafba21..fe113fa7d2b 100644 --- a/tests/spin/test_spin_hamiltonian.py +++ b/tests/spin/test_spin_hamiltonian.py @@ -33,7 +33,6 @@ ) # pylint: disable=too-many-arguments -pytestmark = pytest.mark.usefixtures("new_opmath_only") def test_coupling_error(): diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index 72033dda15c..fe9e29f46a4 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -143,23 +143,6 @@ def test_tensor_observables_rmatmul(self): assert tape.measurements[0].return_type is qml.measurements.Expectation assert tape.measurements[0].obs is t_obs2 - @pytest.mark.usefixtures("legacy_opmath_only") - def test_tensor_observables_tensor_init(self): - """Test that tensor observables are correctly processed from the annotated - queue. Here, we test multiple tensor observables constructed via explicit - Tensor creation.""" - - with QuantumTape() as tape: - op_ = qml.RX(1.0, wires=0) - t_obs1 = qml.PauliZ(1) @ qml.PauliX(0) - t_obs2 = qml.operation.Tensor(t_obs1, qml.Hadamard(2)) - qml.expval(t_obs2) - - assert tape.operations == [op_] - assert tape.observables == [t_obs2] - assert tape.measurements[0].return_type is qml.measurements.Expectation - assert tape.measurements[0].obs is t_obs2 - def test_tensor_observables_tensor_matmul(self): """Test that tensor observables are correctly processed from the annotated queue". Here, wetest multiple tensor observables constructed via matmul diff --git a/tests/templates/test_subroutines/test_qubitization.py b/tests/templates/test_subroutines/test_qubitization.py index 7b8e439d77c..d9cd30e4fdf 100644 --- a/tests/templates/test_subroutines/test_qubitization.py +++ b/tests/templates/test_subroutines/test_qubitization.py @@ -79,19 +79,6 @@ def test_standard_validity(lcu, control, skip_diff): qml.ops.functions.assert_valid(op, skip_differentiation=skip_diff) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") -def test_legacy_new_opmath(): - coeffs, ops = [0.1, -0.3, -0.3], [qml.X(0), qml.Z(1), qml.Y(0) @ qml.Z(2)] - - H1 = qml.dot(coeffs, ops) - matrix_H1 = qml.matrix(qml.Qubitization(H1, control=[3, 4]), wire_order=[3, 4, 0, 1, 2]) - - H2 = qml.Hamiltonian(coeffs, ops) - matrix_H2 = qml.matrix(qml.Qubitization(H2, control=[3, 4]), wire_order=[3, 4, 0, 1, 2]) - - assert np.allclose(matrix_H1, matrix_H2) - - @pytest.mark.parametrize( "hamiltonian, expected_decomposition", ( @@ -248,27 +235,6 @@ def test_qnode_tf(self, shots, seed): assert qml.math.shape(jac) == (4,) assert qml.math.allclose(res, self.exp_grad, atol=0.001) - @pytest.mark.xfail(reason="see https://github.com/PennyLaneAI/pennylane/issues/5507") - @pytest.mark.usefixtures("use_legacy_and_new_opmath") - def test_legacy_new_opmath_diff(self): - coeffs, ops = np.array([0.1, -0.3, -0.3]), [qml.X(0), qml.Z(1), qml.Y(0) @ qml.Z(2)] - - dev = qml.device("default.qubit") - - @qml.qnode(dev) - def circuit_dot(coeffs): - H = qml.dot(coeffs, ops) - qml.Qubitization(H, control=[3, 4]) - return qml.expval(qml.PauliZ(0)) - - @qml.qnode(dev) - def circuit_Hamiltonian(coeffs): - H = qml.Hamiltonian(coeffs, ops) - qml.Qubitization(H, control=[3, 4]) - return qml.expval(qml.PauliZ(0)) - - assert np.allclose(qml.grad(circuit_dot)(coeffs), qml.grad(circuit_Hamiltonian)(coeffs)) - def test_copy(): """Test that a Qubitization operator can be copied.""" diff --git a/tests/test_operation.py b/tests/test_operation.py index f1c9c6bd5d4..e22a2fda953 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -15,14 +15,10 @@ Unit tests for :mod:`pennylane.operation`. """ import copy -import itertools -import warnings -from functools import reduce import numpy as np import pytest from gate_data import CNOT, I, Toffoli, X -from numpy.linalg import multi_dot import pennylane as qml from pennylane import numpy as pnp @@ -31,11 +27,9 @@ Operation, Operator, StatePrepBase, - Tensor, - convert_to_legacy_H, operation_derivative, ) -from pennylane.ops import Prod, SProd, Sum, cv +from pennylane.ops import Prod, SProd, Sum from pennylane.wires import Wires # pylint: disable=no-self-use, no-member, protected-access, redefined-outer-name, too-few-public-methods @@ -930,25 +924,6 @@ class DummyOp(qml.operation.Operation): assert op.is_hermitian is False -@pytest.mark.usefixtures("use_legacy_and_new_opmath") -class TestObservableTensorLegacySupport: - """Test legacy support of observables with new opmath types""" - - def test_prod_matmul_with_new_opmath(self): - """Test matmul of an Observable with a new opmath instance""" - res = qml.Hadamard(0) @ qml.s_prod(0.5, qml.PauliX(0)) - assert isinstance(res, qml.ops.Prod) - - def test_Observable_sub_with_new_opmath(self): - """Test sub of an Observable with a new opmath instance""" - res = qml.Hadamard(0) - qml.s_prod(0.5, qml.PauliX(0)) - assert isinstance(res, qml.ops.Sum) - - def test_Tensor_arithmetic_depth(self): - op = qml.operation.Tensor(qml.Hadamard(0), qml.Hadamard(1), qml.Hadamard(2)) - assert op.arithmetic_depth == 1 - - class TestObservableConstruction: """Test custom observables construction.""" @@ -1335,936 +1310,6 @@ def test_label_for_operations_with_id(self): assert '"test_with_id"' not in op.label(decimals=2) -# This test is outside the TestTensor class because that class only runs when legacy op math -# is enabled. We want this test to run in normal CI as well. -def test_tensor_deprecation(): - """Test that a deprecation warning is raised when initializing a Tensor""" - ops = [qml.Z(0), qml.Z(1)] - with pytest.warns(qml.PennyLaneDeprecationWarning, match="qml.operation.Tensor"): - _ = Tensor(*ops) - - -@pytest.mark.usefixtures("legacy_opmath_only") -class TestTensor: - """Unit tests for the Tensor class""" - - def test_construct(self): - """Test construction of a tensor product""" - X = qml.PauliX(0) - Y = qml.PauliY(2) - T = Tensor(X, Y) - assert T.obs == [X, Y] - - T = Tensor(T, Y) - assert T.obs == [X, Y, Y] - - with pytest.raises( - ValueError, match="Can only perform tensor products between observables" - ): - Tensor(T, qml.CNOT(wires=[0, 1])) - - def test_flatten_unflatten(self): - """Test flattening and unflattening for tensors.""" - op1 = qml.PauliX(0) - op2 = qml.Hermitian(np.eye(2), wires=1) - t = Tensor(op1, op2) - - data, metadata = t._flatten() - qml.assert_equal(data[0], op1) - qml.assert_equal(data[1], op2) - assert not metadata - assert hash(metadata) - - new_op = Tensor._unflatten(*t._flatten()) - qml.assert_equal(t, new_op) - - def test_warning_for_overlapping_wires(self): - """Test that creating a Tensor with overlapping wires raises a warning""" - X = qml.PauliX(0) - Y = qml.PauliY(0) - op = qml.PauliX(0) @ qml.PauliY(1) - - with pytest.warns(UserWarning, match="Tensor object acts on overlapping wires"): - Tensor(X, Y) - - with pytest.warns(UserWarning, match="Tensor object acts on overlapping wires"): - _ = op @ qml.PauliZ(1) - - def test_queuing_defined_outside(self): - """Test the queuing of a Tensor object.""" - - op1 = qml.PauliX(0) - op2 = qml.PauliY(1) - T = Tensor(op1, op2) - - with qml.queuing.AnnotatedQueue() as q: - T.queue() - - assert len(q.queue) == 1 - assert q.queue[0] is T - - def test_queuing(self): - """Test the queuing of a Tensor object.""" - - with qml.queuing.AnnotatedQueue() as q: - op1 = qml.PauliX(0) - op2 = qml.PauliY(1) - T = Tensor(op1, op2) - - assert len(q) == 1 - assert q.queue[0] is T - - def test_queuing_observable_matmul(self): - """Test queuing when tensor constructed with matmul.""" - - with qml.queuing.AnnotatedQueue() as q: - op1 = qml.PauliX(0) - op2 = qml.PauliY(1) - t = op1 @ op2 - - assert len(q) == 1 - assert q.queue[0] is t - - def test_queuing_tensor_matmul(self): - """Tests the tensor-specific matmul method updates queuing metadata.""" - - with qml.queuing.AnnotatedQueue() as q: - op1 = qml.PauliX(0) - op2 = qml.PauliY(1) - t = Tensor(op1, op2) - - op3 = qml.PauliZ(2) - t2 = t @ op3 - - assert len(q) == 1 - assert q.queue[0] is t2 - - def test_queuing_tensor_matmul_components_outside(self): - """Tests the tensor-specific matmul method when components are defined outside the - queuing context.""" - - op1 = qml.PauliX(0) - op2 = qml.PauliY(1) - t1 = Tensor(op1, op2) - - with qml.queuing.AnnotatedQueue() as q: - op3 = qml.PauliZ(2) - t2 = t1 @ op3 - - assert len(q) == 1 - assert q.queue[0] is t2 - - def test_queuing_tensor_rmatmul(self): - """Tests tensor-specific rmatmul updates queuing metatadata.""" - - with qml.queuing.AnnotatedQueue() as q: - op1 = qml.PauliX(0) - op2 = qml.PauliY(1) - - t1 = op1 @ op2 - - op3 = qml.PauliZ(3) - - t2 = op3 @ t1 - - assert len(q.queue) == 1 - assert q.queue[0] is t2 - - def test_name(self): - """Test that the names of the observables are - returned as expected""" - X = qml.PauliX(0) - Y = qml.PauliY(2) - t = Tensor(X, Y) - assert t.name == [X.name, Y.name] - - def test_batch_size(self): - """Test that the batch_size attribute of the Tensor is initialized as None.""" - X = qml.PauliX(0) - Y = qml.PauliY(2) - t = Tensor(X, Y) - assert t.batch_size is None - - def test_pauli_rep(self): - """Test that the _pauli_rep attribute of the Tensor is initialized correctly.""" - # Pauli rep not None for Pauli observables - X = qml.PauliX(0) - Y = qml.PauliY(2) - t = Tensor(X, Y) - assert t.pauli_rep == qml.pauli.PauliSentence({qml.pauli.PauliWord({0: "X", 2: "Y"}): 1.0}) - - # Puli rep None if observables not valid Pauli observables - H = qml.Hadamard(1) - t = Tensor(X, H) - assert t.pauli_rep is None - - def test_has_matrix(self): - """Test that the Tensor class has a ``has_matrix`` static attribute set to True.""" - assert Tensor.has_matrix is True - - def test_num_wires(self): - """Test that the correct number of wires is returned""" - p = np.eye(4) - X = qml.PauliX(0) - Y = qml.Hermitian(p, wires=[1, 2]) - t = Tensor(X, Y) - assert t.num_wires == 3 - - def test_wires(self): - """Test that the correct nested list of wires is returned""" - p = np.eye(4) - X = qml.PauliX(0) - Y = qml.Hermitian(p, wires=[1, 2]) - t = Tensor(X, Y) - assert t.wires == Wires([0, 1, 2]) - - def test_params(self): - """Test that the correct flattened list of parameters is returned""" - p = np.eye(4) - X = qml.PauliX(0) - Y = qml.Hermitian(p, wires=[1, 2]) - t = Tensor(X, Y) - assert t.data == (p,) - - def test_data_setter_list(self): - """Test the data setter with a list""" - p = np.eye(4) - X = qml.PauliX(0) - Y = qml.Hermitian(p, wires=[1, 2]) - t = Tensor(X, Y) - assert t.data == (p,) - new_data = np.eye(4) * 6 - t.data = [(), (new_data,)] - assert qml.math.allequal(t.data, (new_data,)) - - def test_data_setter_tuple(self): - """Test the data setter with a tuple""" - p = np.eye(4) - X = qml.PauliX(0) - Y = qml.Hermitian(p, wires=[1, 2]) - t = Tensor(X, Y) - assert t.data == (p,) - new_data = np.eye(4) * 6 - t.data = (new_data,) - assert qml.math.allequal(t.data, (new_data,)) - - def test_num_params(self): - """Test that the correct number of parameters is returned""" - p = np.eye(4) - X = qml.PauliX(0) - Y = qml.Hermitian(p, wires=[1, 2]) - Z = qml.Hermitian(p, wires=[3, 4]) - t = Tensor(X, Y, Z) - assert t.num_params == 2 - - def test_parameters(self): - """Test that the correct nested list of parameters is returned""" - p = np.eye(4) - X = qml.PauliX(0) - Y = qml.Hermitian(p, wires=[1, 2]) - t = Tensor(X, Y) - assert t.parameters == [[], [p]] - - def test_label(self): - """Test that Tensors are labelled as expected""" - - x = qml.PauliX(0) - y = qml.PauliZ(2) - T = Tensor(x, y) - - assert T.label() == "X@Z" - assert T.label(decimals=2) == "X@Z" - assert T.label(base_label=["X0", "Z2"]) == "X0@Z2" - - with pytest.raises(ValueError, match=r"Tensor label requires"): - T.label(base_label="nope") - - def test_multiply_obs(self): - """Test that multiplying two observables - produces a tensor""" - X = qml.PauliX(0) - Y = qml.Hadamard(2) - t = X @ Y - assert isinstance(t, Tensor) - assert t.obs == [X, Y] - - def test_multiply_obs_tensor(self): - """Test that multiplying an observable by a tensor - produces a tensor""" - X = qml.PauliX(0) - Y = qml.Hadamard(2) - Z = qml.PauliZ(1) - - t = X @ Y - t = Z @ t - - assert isinstance(t, Tensor) - assert t.obs == [Z, X, Y] - - def test_multiply_tensor_obs(self): - """Test that multiplying a tensor by an observable - produces a tensor""" - X = qml.PauliX(0) - Y = qml.Hadamard(2) - Z = qml.PauliZ(1) - - t = X @ Y - t = t @ Z - - assert isinstance(t, Tensor) - assert t.obs == [X, Y, Z] - - def test_multiply_tensor_tensor(self): - """Test that multiplying a tensor by a tensor - produces a tensor""" - X = qml.PauliX(0) - Y = qml.PauliY(2) - Z = qml.PauliZ(1) - H = qml.Hadamard(3) - - t1 = X @ Y - t2 = Z @ H - t = t2 @ t1 - - assert isinstance(t, Tensor) - assert t.obs == [Z, H, X, Y] - - def test_multiply_tensor_hamiltonian(self): - """Test that a tensor can be multiplied by a hamiltonian.""" - H = qml.PauliX(0) + qml.PauliY(0) - t = qml.PauliZ(1) @ qml.PauliZ(2) - out = t @ H - - expected = qml.Hamiltonian( - [1, 1], - [ - qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliX(0), - qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliY(0), - ], - ) - qml.assert_equal(out, expected) - - def test_multiply_tensor_in_place(self): - """Test that multiplying a tensor in-place - produces a tensor""" - X = qml.PauliX(0) - Y = qml.PauliY(2) - Z = qml.PauliZ(1) - H = qml.Hadamard(3) - - t = X - t @= Y - t @= Z @ H - - assert isinstance(t, Tensor) - assert t.obs == [X, Y, Z, H] - - def test_operation_multiply_invalid(self): - """Test that an exception is raised if an observable - is matrix-multiplied by a scalar""" - X = qml.PauliX(0) - Z = qml.PauliZ(1) - - with pytest.raises(TypeError, match="unsupported operand type"): - T = X @ Z - _ = 4 @ T - - def test_tensor_matmul_op_is_prod(self): - """Test that Tensor @ non-observable returns a Prod.""" - tensor = qml.PauliX(0) @ qml.PauliY(1) - assert isinstance(tensor, Tensor) - prod = tensor @ qml.S(0) - assert isinstance(prod, qml.ops.Prod) - assert prod.operands == (qml.PauliX(0), qml.PauliY(1), qml.S(0)) - - def test_eigvals(self): - """Test that the correct eigenvalues are returned for the Tensor""" - X = qml.PauliX(0) - Y = qml.PauliY(2) - t = Tensor(X, Y) - assert np.array_equal(t.eigvals(), np.kron([1, -1], [1, -1])) - - # test that the eigvals are now cached and not recalculated - assert np.array_equal(t._eigvals_cache, t.eigvals()) - - @pytest.mark.usefixtures("tear_down_hermitian") - def test_eigvals_hermitian(self, tol): - """Test that the correct eigenvalues are returned for the Tensor containing an Hermitian observable""" - X = qml.PauliX(0) - hamiltonian = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) - Herm = qml.Hermitian(hamiltonian, wires=[1, 2]) - t = Tensor(X, Herm) - d = np.kron(np.array([1.0, -1.0]), np.array([-1.0, 1.0, 1.0, 1.0])) - t = t.eigvals() - assert np.allclose(t, d, atol=tol, rtol=0) - - def test_eigvals_identity(self, tol): - """Test that the correct eigenvalues are returned for the Tensor containing an Identity""" - X = qml.PauliX(0) - Iden = qml.Identity(1) - t = Tensor(X, Iden) - d = np.kron(np.array([1.0, -1.0]), np.array([1.0, 1.0])) - t = t.eigvals() - assert np.allclose(t, d, atol=tol, rtol=0) - - def test_eigvals_identity_and_hermitian(self, tol): - """Test that the correct eigenvalues are returned for the Tensor containing - multiple types of observables""" - H = np.diag([1, 2, 3, 4]) - O = qml.PauliX(0) @ qml.Identity(2) @ qml.Hermitian(H, wires=[4, 5]) - res = O.eigvals() - expected = np.kron(np.array([1.0, -1.0]), np.kron(np.array([1.0, 1.0]), np.arange(1, 5))) - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_diagonalizing_gates(self, tol): - """Test that the correct diagonalizing gate set is returned for a Tensor of observables""" - H = np.diag([1, 2, 3, 4]) - O = qml.PauliX(0) @ qml.Identity(2) @ qml.PauliY(1) @ qml.Hermitian(H, [5, 6]) - - res = O.diagonalizing_gates() - - # diagonalize the PauliX on wire 0 (H.X.H = Z) - assert isinstance(res[0], qml.Hadamard) - assert res[0].wires == Wires([0]) - - # diagonalize the PauliY on wire 1 (U.Y.U^\dagger = Z - # where U = HSZ). - assert isinstance(res[1], qml.PauliZ) - assert res[1].wires == Wires([1]) - assert isinstance(res[2], qml.S) - assert res[2].wires == Wires([1]) - assert isinstance(res[3], qml.Hadamard) - assert res[3].wires == Wires([1]) - - # diagonalize the Hermitian observable on wires 5, 6 - assert isinstance(res[4], qml.QubitUnitary) - assert res[4].wires == Wires([5, 6]) - - O = O @ qml.Hadamard(4) - res = O.diagonalizing_gates() - - # diagonalize the Hadamard observable on wire 4 - # (RY(-pi/4).H.RY(pi/4) = Z) - assert isinstance(res[-1], qml.RY) - assert res[-1].wires == Wires([4]) - assert np.allclose(res[-1].parameters, -np.pi / 4, atol=tol, rtol=0) - - def test_diagonalizing_gates_numerically_diagonalizes(self, tol): - """Test that the diagonalizing gate set numerically - diagonalizes the tensor observable""" - - # create a tensor observable acting on consecutive wires - H = np.diag([1, 2, 3, 4]) - O = qml.PauliX(0) @ qml.PauliY(1) @ qml.Hermitian(H, [2, 3]) - - O_mat = O.matrix() - diag_gates = O.diagonalizing_gates() - - # group the diagonalizing gates based on what wires they act on - U_list = [] - for _, g in itertools.groupby(diag_gates, lambda x: x.wires.tolist()): - # extract the matrices of each diagonalizing gate - mats = [i.matrix() for i in g] - - # Need to revert the order in which the matrices are applied such that they adhere to the order - # of matrix multiplication - # E.g. for PauliY: [PauliZ(wires=self.wires), S(wires=self.wires), Hadamard(wires=self.wires)] - # becomes Hadamard @ S @ PauliZ, where @ stands for matrix multiplication - mats = mats[::-1] - - if len(mats) > 1: - # multiply all unitaries together before appending - mats = [multi_dot(mats)] - - # append diagonalizing unitary for specific wire to U_list - U_list.append(mats[0]) - - # since the test is assuming consecutive wires for each observable - # in the tensor product, it is sufficient to Kronecker product - # the entire list. - U = reduce(np.kron, U_list) - - res = U @ O_mat @ U.conj().T - expected = np.diag(O.eigvals()) - - # once diagonalized by U, the result should be a diagonal - # matrix of the eigenvalues. - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_tensor_matrix(self, tol): - """Test that the tensor product matrix method returns - the correct result""" - H = np.diag([1, 2, 3, 4]) - O = qml.PauliX(0) @ qml.PauliY(1) @ qml.Hermitian(H, [2, 3]) - - res = O.matrix() - expected = reduce(np.kron, [qml.PauliX.compute_matrix(), qml.PauliY.compute_matrix(), H]) - - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_matrix_wire_order_not_implemented(self): - """Test that an exception is raised if a wire_order is passed to the matrix method""" - O = qml.PauliX(0) @ qml.PauliY(1) - with pytest.raises(NotImplementedError, match="wire_order"): - O.matrix(wire_order=[1, 0]) - - def test_tensor_matrix_partial_wires_overlap_warning(self): - """Tests that a warning is raised if the wires the factors in - the tensor product act on have partial overlaps.""" - H = np.diag([1, 2, 3, 4]) - O1 = qml.PauliX(0) @ qml.Hermitian(H, [0, 1]) - O2 = qml.Hermitian(H, [0, 1]) @ qml.PauliY(1) - - for O in (O1, O2): - with pytest.warns(UserWarning, match="partially overlapping"): - O.matrix() - - def test_tensor_matrix_too_large_warning(self): - """Tests that a warning is raised if wires occur in multiple of the - factors in the tensor product, leading to a wrongly-sized matrix.""" - with pytest.warns(UserWarning, match="acts on overlapping wires"): - O = qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(0) - with pytest.warns(UserWarning, match="The size of the returned matrix"): - O.matrix() - - @pytest.mark.parametrize("classes", [(qml.PauliX, qml.PauliX), (qml.PauliZ, qml.PauliX)]) - def test_multiplication_matrix(self, tol, classes): - """If using the ``@`` operator on two observables acting on the - same wire, the tensor class should treat this as matrix multiplication.""" - c1, c2 = classes - with pytest.warns(UserWarning, match="acts on overlapping wires"): - O = c1(0) @ c2(0) - - res = O.matrix() - expected = c1.compute_matrix() @ c2.compute_matrix() - - assert np.allclose(res, expected, atol=tol, rtol=0) - - herm_matrix = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) - - with qml.operation.disable_new_opmath_cm(warn=False): - tensor_obs = [ - (qml.PauliZ(0) @ qml.Identity(1) @ qml.PauliZ(2), [qml.PauliZ(0), qml.PauliZ(2)]), - ( - qml.Identity(0) - @ qml.PauliX(1) - @ qml.Identity(2) - @ qml.PauliZ(3) - @ qml.PauliZ(4) - @ qml.Identity(5), - [qml.PauliX(1), qml.PauliZ(3), qml.PauliZ(4)], - ), - # List containing single observable is returned - (qml.PauliZ(0) @ qml.Identity(1), [qml.PauliZ(0)]), - (qml.Identity(0) @ qml.PauliX(1) @ qml.Identity(2), [qml.PauliX(1)]), - (qml.Identity(0) @ qml.Identity(1), [qml.Identity(0)]), - ( - qml.Identity(0) @ qml.Identity(1) @ qml.Hermitian(herm_matrix, wires=[2, 3]), - [qml.Hermitian(herm_matrix, wires=[2, 3])], - ), - ] - - @pytest.mark.parametrize("tensor_observable, expected", tensor_obs) - def test_non_identity_obs(self, tensor_observable, expected): - """Tests that the non_identity_obs property returns a list that contains no Identity instances.""" - - O = tensor_observable - for idx, obs in enumerate(O.non_identity_obs): - assert isinstance(obs, type(expected[idx])) - assert obs.wires == expected[idx].wires - - with qml.operation.disable_new_opmath_cm(warn=False): - tensor_obs_pruning = [ - (qml.PauliZ(0) @ qml.Identity(1) @ qml.PauliZ(2), qml.PauliZ(0) @ qml.PauliZ(2)), - ( - qml.Identity(0) - @ qml.PauliX(1) - @ qml.Identity(2) - @ qml.PauliZ(3) - @ qml.PauliZ(4) - @ qml.Identity(5), - qml.PauliX(1) @ qml.PauliZ(3) @ qml.PauliZ(4), - ), - # Single observable is returned - (qml.PauliZ(0) @ qml.Identity(1), qml.PauliZ(0)), - (qml.Identity(0) @ qml.PauliX(1) @ qml.Identity(2), qml.PauliX(1)), - (qml.Identity(0) @ qml.Identity(1), qml.Identity(0)), - (qml.Identity(0) @ qml.Identity(1), qml.Identity(0)), - ( - qml.Identity(0) @ qml.Identity(1) @ qml.Hermitian(herm_matrix, wires=[2, 3]), - qml.Hermitian(herm_matrix, wires=[2, 3]), - ), - ] - - @pytest.mark.parametrize("tensor_observable, expected", tensor_obs_pruning) - def test_prune(self, tensor_observable, expected): - """Tests that the prune method returns the expected Tensor or single non-Tensor Observable.""" - O = tensor_observable - - O_pruned = O.prune() - assert isinstance(O_pruned, type(expected)) - assert O_pruned.wires == expected.wires - - def test_prune_while_queuing_return_tensor(self): - """Tests that pruning a tensor to a tensor in a tape context registers - the pruned tensor as owned by the measurement, - and turns the original tensor into an orphan without an owner.""" - - with qml.queuing.AnnotatedQueue() as q: - # we assign operations to variables here so we can compare them below - a = qml.PauliX(wires=0) - b = qml.PauliY(wires=1) - c = qml.Identity(wires=2) - T = qml.operation.Tensor(a, b, c) - T_pruned = T.prune() - m = qml.expval(T_pruned) - - assert len(q.queue) == 1 - assert q.queue[0] is m - - def test_prune_while_queueing_return_obs(self): - """Tests that pruning a tensor to an observable in a tape context registers - the pruned observable as owned by the measurement, - and turns the original tensor into an orphan without an owner.""" - - with qml.queuing.AnnotatedQueue() as q: - a = qml.PauliX(wires=0) - c = qml.Identity(wires=2) - T = qml.operation.Tensor(a, c) - T_pruned = T.prune() - m = qml.expval(T_pruned) - - assert len(q.queue) == 1 - assert q.queue[0] is m - - def test_sparse_matrix_no_wires(self): - """Tests that the correct sparse matrix representation is used.""" - - t = qml.PauliX(0) @ qml.PauliZ(1) - s = t.sparse_matrix() - - assert np.allclose(s.data, [1, -1, 1, -1]) - assert np.allclose(s.indices, [2, 3, 0, 1]) - assert np.allclose(s.indptr, [0, 1, 2, 3, 4]) - - def test_sparse_matrix_swapped_wires(self): - """Tests that the correct sparse matrix representation is used - when the custom wires swap the order.""" - - t = qml.PauliX(0) @ qml.PauliZ(1) - data = [1, 1, -1, -1] - indices = [1, 0, 3, 2] - indptr = [0, 1, 2, 3, 4] - - s = t.sparse_matrix(wires=[1, 0]) - - assert np.allclose(s.data, data) - assert np.allclose(s.indices, indices) - assert np.allclose(s.indptr, indptr) - - s = t.sparse_matrix(wire_order=[1, 0]) - - assert np.allclose(s.data, data) - assert np.allclose(s.indices, indices) - assert np.allclose(s.indptr, indptr) - - def test_sparse_matrix_extra_wire(self): - """Tests that the correct sparse matrix representation is used - when the custom wires add an extra wire with an implied identity operation.""" - - t = qml.PauliX(0) @ qml.PauliZ(1) - data = [1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0] - indices = [4, 5, 6, 7, 0, 1, 2, 3] - indptr = [0, 1, 2, 3, 4, 5, 6, 7, 8] - - s = t.sparse_matrix(wires=[0, 1, 2]) - - assert s.shape == (8, 8) - assert np.allclose(s.data, data) - assert np.allclose(s.indices, indices) - assert np.allclose(s.indptr, indptr) - - s = t.sparse_matrix(wire_order=[0, 1, 2]) - - assert s.shape == (8, 8) - assert np.allclose(s.data, data) - assert np.allclose(s.indices, indices) - assert np.allclose(s.indptr, indptr) - - def test_sparse_matrix_errors(self): - """Tests that errors are raised when the sparse matrix is computed for a tensor - whose constituent operations are not all single-qubit gates, and when both ``wires`` - and ``wire_order`` at specified at once.""" - - t = qml.PauliX(0) @ qml.Hermitian(np.eye(4), wires=[1, 2]) - with pytest.raises(ValueError, match="Can only compute"): - t.sparse_matrix() - - t = qml.PauliX(0) @ qml.PauliZ(1) - with pytest.raises(ValueError, match="Wire order has been specified twice"): - t.sparse_matrix(wires=[0, 1], wire_order=[0, 1]) - - def test_copy(self): - """Test copying of a Tensor.""" - tensor = Tensor(qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)) - c = copy.copy(tensor) - assert c is not tensor - assert c.wires == Wires([0, 1, 2]) - assert c.batch_size == tensor.batch_size == None - for obs1, obs2 in zip(c.obs, tensor.obs): - qml.assert_equal(obs1, obs2) - - def test_map_wires(self): - """Test the map_wires method.""" - tensor = Tensor(qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)) - wire_map = {0: 10, 1: 11, 2: 12} - mapped_tensor = tensor.map_wires(wire_map=wire_map) - final_obs = [qml.PauliX(10), qml.PauliY(11), qml.PauliZ(12)] - assert tensor is not mapped_tensor - assert tensor.wires == Wires([0, 1, 2]) - assert mapped_tensor.wires == Wires([10, 11, 12]) - assert mapped_tensor.batch_size == tensor.batch_size - for obs1, obs2 in zip(mapped_tensor.obs, final_obs): - qml.assert_equal(obs1, obs2) - assert mapped_tensor.pauli_rep == Tensor(*final_obs).pauli_rep - - def test_map_wires_no_pauli_rep(self): - """Test that map_wires sets the pauli rep correctly if the original - Tensor did not have a pauli rep.""" - tensor = Tensor(qml.PauliX(0), qml.Hadamard(1)) - wire_map = {0: 10, 1: 11} - expected_tensor = Tensor(qml.PauliX(10), qml.Hadamard(11)) - - mapped_tensor = tensor.map_wires(wire_map=wire_map) - assert tensor is not mapped_tensor - assert mapped_tensor == expected_tensor - assert mapped_tensor.pauli_rep is None - - def test_matmul_not_implemented(self): - """Test that matrix multiplication raises TypeError if unsupported - object is used.""" - - op = Tensor(qml.PauliX(0), qml.PauliZ(1)) - - with pytest.raises(TypeError, match="unsupported operand type"): - _ = op @ 1.0 - - @pytest.mark.jax - def test_matrix_jax_projector(self): - """Test that matrix can be computed with a jax projector.""" - - import jax - - def f(state): - op = qml.Projector(state, wires=0) - return qml.operation.Tensor(op, qml.Z(1)).matrix() - - res = jax.jit(f)(jax.numpy.array([0, 1])) - expected = np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]]) - assert qml.math.allclose(res, expected) - - -with qml.operation.disable_new_opmath_cm(warn=False): - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", "qml.ops.Hamiltonian uses", qml.PennyLaneDeprecationWarning - ) - warnings.filterwarnings( - "ignore", "qml.operation.Tensor uses", qml.PennyLaneDeprecationWarning - ) - - equal_obs = [ - (qml.PauliZ(0), qml.PauliZ(0), True), - (qml.PauliZ(0) @ qml.PauliX(1), qml.PauliZ(0) @ qml.PauliX(1) @ qml.Identity(2), True), - (qml.PauliZ("b"), qml.PauliZ("b") @ qml.Identity(1.3), True), - (qml.PauliZ(0) @ qml.Identity(1), qml.PauliZ(0), True), - (qml.PauliZ(0), qml.PauliZ(1) @ qml.Identity(0), False), - ( - qml.Hermitian(np.array([[0, 1], [1, 0]]), 0), - qml.Identity(1) @ qml.Hermitian(np.array([[0, 1], [1, 0]]), 0), - True, - ), - (qml.PauliZ("a") @ qml.PauliX(1), qml.PauliX(1) @ qml.PauliZ("a"), True), - (qml.PauliZ("a"), qml.Hamiltonian([1], [qml.PauliZ("a")]), True), - ] - - add_obs = [ - (qml.PauliZ(0) @ qml.Identity(1), qml.PauliZ(0), qml.Hamiltonian([2], [qml.PauliZ(0)])), - ( - qml.PauliZ(0), - qml.PauliZ(0) @ qml.PauliX(1), - qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1)]), - ), - ( - qml.PauliZ("b") @ qml.Identity(1), - qml.Hamiltonian([3], [qml.PauliZ("b")]), - qml.Hamiltonian([4], [qml.PauliZ("b")]), - ), - ( - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliZ(1) @ qml.Identity(2) @ qml.PauliX(0), - qml.Hamiltonian([2], [qml.PauliX(0) @ qml.PauliZ(1)]), - ), - ( - qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2), - qml.Hamiltonian([3], [qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2)]), - qml.Hamiltonian([4], [qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2)]), - ), - ] - - add_zero_obs = [ - qml.PauliX(0), - qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2), - qml.PauliX(0) @ qml.Hadamard(2), - # qml.Projector(np.array([1, 1]), wires=[0, 1]), - # qml.SparseHamiltonian(csr_matrix(np.array([[1, 0], [-1.5, 0]])), 1), - # CVObservables - qml.Identity(1), - cv.NumberOperator(wires=[1]), - cv.TensorN(wires=[1]), - cv.QuadX(wires=[1]), - cv.QuadP(wires=[1]), - # cv.QuadOperator(1.234, wires=0), - # cv.FockStateProjector([1,2,3], wires=[0, 1, 2]), - cv.PolyXP(np.array([1.0, 2.0, 3.0]), wires=[0]), - ] - - mul_obs = [ - (qml.PauliZ(0), 3, qml.Hamiltonian([3], [qml.PauliZ(0)])), - (qml.PauliZ(0) @ qml.Identity(1), 3, qml.Hamiltonian([3], [qml.PauliZ(0)])), - ( - qml.PauliZ(0) @ qml.PauliX(1), - 4.5, - qml.Hamiltonian([4.5], [qml.PauliZ(0) @ qml.PauliX(1)]), - ), - ( - qml.Hermitian(np.array([[1, 0], [0, -1]]), "c"), - 3, - qml.Hamiltonian([3], [qml.Hermitian(np.array([[1, 0], [0, -1]]), "c")]), - ), - ] - - matmul_obs = [ - (qml.PauliX(0), qml.PauliZ(1), Tensor(qml.PauliX(0), qml.PauliZ(1))), # obs @ obs - ( - qml.PauliX(0), - qml.PauliZ(1) @ qml.PauliY(2), - Tensor(qml.PauliX(0), qml.PauliZ(1), qml.PauliY(2)), - ), # obs @ tensor - ( - qml.PauliX(0), - qml.Hamiltonian([1.0], [qml.PauliY(1)]), - qml.Hamiltonian([1.0], [qml.PauliX(0) @ qml.PauliY(1)]), - ), # obs @ hamiltonian - ] - - sub_obs = [ - (qml.PauliZ(0) @ qml.Identity(1), qml.PauliZ(0), qml.Hamiltonian([], [])), - ( - qml.PauliZ(0), - qml.PauliZ(0) @ qml.PauliX(1), - qml.Hamiltonian([1, -1], [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1)]), - ), - ( - qml.PauliZ(0) @ qml.Identity(1), - qml.Hamiltonian([3], [qml.PauliZ(0)]), - qml.Hamiltonian([-2], [qml.PauliZ(0)]), - ), - ( - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliZ(3) @ qml.Identity(2) @ qml.PauliX(0), - qml.Hamiltonian( - [1, -1], [qml.PauliX(0) @ qml.PauliZ(1), qml.PauliZ(3) @ qml.PauliX(0)] - ), - ), - ( - qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2), - qml.Hamiltonian([3], [qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2)]), - qml.Hamiltonian([-2], [qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2)]), - ), - ] - - -@pytest.mark.usefixtures("legacy_opmath_only") -class TestTensorObservableOperations: - """Tests arithmetic operations between observables/tensors""" - - def test_data(self): - """Tests the data() method for Tensors and Observables""" - - obs = qml.PauliZ(0) - data = obs._obs_data() - - assert data == {("PauliZ", Wires(0), ())} - - obs = qml.PauliZ(0) @ qml.PauliX(1) - data = obs._obs_data() - - assert data == {("PauliZ", Wires(0), ()), ("PauliX", Wires(1), ())} - - obs = qml.Hermitian(np.array([[1, 0], [0, -1]]), 0) - data = obs._obs_data() - - assert data == { - ( - "Hermitian", - Wires(0), - ( - b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff", - ), - ) - } - - def test_equality_error(self): - """Tests that the correct error is raised when compare() is called on invalid type""" - - obs = qml.PauliZ(0) - tensor = qml.PauliZ(0) @ qml.PauliX(1) - A = [[1, 0], [0, -1]] - with pytest.raises( - ValueError, - match=r"Can only compare an Observable/Tensor, and a Hamiltonian/Observable/Tensor.", - ): - obs.compare(A) - tensor.compare(A) - - @pytest.mark.parametrize(("obs1", "obs2", "res"), equal_obs) - def test_equality(self, obs1, obs2, res): - """Tests the compare() method for Tensors and Observables""" - assert obs1.compare(obs2) == res - - @pytest.mark.parametrize(("obs1", "obs2", "obs"), add_obs) - def test_addition(self, obs1, obs2, obs): - """Tests addition between Tensors and Observables""" - assert obs.compare(obs1 + obs2) - - @pytest.mark.parametrize("obs", add_zero_obs) - def test_add_zero(self, obs): - """Tests adding Tensors and Observables to zero""" - assert obs.compare(obs + 0) - assert obs.compare(0 + obs) - assert obs.compare(obs + 0.0) - assert obs.compare(0.0 + obs) - assert obs.compare(obs + 0e1) - assert obs.compare(0e1 + obs) - - @pytest.mark.parametrize(("coeff", "obs", "res_obs"), mul_obs) - def test_scalar_multiplication(self, coeff, obs, res_obs): - """Tests scalar multiplication of Tensors and Observables""" - assert res_obs.compare(coeff * obs) - assert res_obs.compare(obs * coeff) - - @pytest.mark.parametrize(("obs1", "obs2", "obs"), sub_obs) - def test_subtraction(self, obs1, obs2, obs): - """Tests subtraction between Tensors and Observables""" - assert obs.compare(obs1 - obs2) - - @pytest.mark.parametrize(("obs1", "obs2", "res"), matmul_obs) - def test_tensor_product(self, obs1, obs2, res): - """Tests the tensor product between Observables""" - assert res.compare(obs1 @ obs2) - - # Dummy class inheriting from Operator class MyOp(Operator): num_wires = 1 @@ -2642,7 +1687,6 @@ def test_composed(self): ] -@pytest.mark.usefixtures("new_opmath_only") class TestNewOpMath: """Tests dunder operations with new operator arithmetic enabled.""" @@ -2767,7 +1811,6 @@ def test_mul_does_auto_simplify(self): class TestHamiltonianLinearCombinationAlias: """Unit tests for using qml.Hamiltonian as an alias for LinearCombination""" - @pytest.mark.usefixtures("new_opmath_only") def test_hamiltonian_linear_combination_alias_enabled(self): """Test that qml.Hamiltonian is an alias for LinearCombination with new operator arithmetic enabled""" @@ -2775,21 +1818,6 @@ def test_hamiltonian_linear_combination_alias_enabled(self): assert isinstance(op, qml.ops.LinearCombination) assert isinstance(op, qml.Hamiltonian) - assert not isinstance(op, qml.ops.Hamiltonian) - assert not isinstance(op, qml.ops.qubit.Hamiltonian) - assert not isinstance(op, qml.ops.qubit.hamiltonian.Hamiltonian) - - @pytest.mark.usefixtures("legacy_opmath_only") - def test_hamiltonian_linear_combination_alias_disabled(self): - """Test that qml.Hamiltonian is not an alias for LinearCombination with new operator - arithmetic disabled""" - op = qml.Hamiltonian([1.0], [qml.X(0)]) - - assert not isinstance(op, qml.ops.LinearCombination) - assert isinstance(op, qml.Hamiltonian) - assert isinstance(op, qml.ops.Hamiltonian) - assert isinstance(op, qml.ops.qubit.Hamiltonian) - assert isinstance(op, qml.ops.qubit.hamiltonian.Hamiltonian) @pytest.mark.parametrize( @@ -2828,46 +1856,6 @@ def test_symmetric_matrix_early_return(op, mocker): assert np.allclose(actual, manually_expanded) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") -def test_op_arithmetic_toggle(): - """Tests toggling op arithmetic on and off""" - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="Toggling the new approach"): - with qml.operation.enable_new_opmath_cm(): - assert qml.operation.active_new_opmath() - assert isinstance(qml.PauliX(0) @ qml.PauliZ(1), Prod) - - with pytest.warns(qml.PennyLaneDeprecationWarning, match="Disabling the new approach"): - with qml.operation.disable_new_opmath_cm(): - assert not qml.operation.active_new_opmath() - assert isinstance(qml.PauliX(0) @ qml.PauliZ(1), Tensor) - - -@pytest.mark.usefixtures("new_opmath_only") -def test_op_arithmetic_default(): - """Test that new op math is enabled by default""" - assert qml.operation.active_new_opmath() - - -@pytest.mark.usefixtures("new_opmath_only") -def test_disable_enable_new_opmath(): - """Test that disabling and re-enabling new opmath works and raises the correct warning""" - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="Disabling the new approach to operator arithmetic" - ): - qml.operation.disable_new_opmath() - - assert not qml.operation.active_new_opmath() - - with pytest.warns( - qml.PennyLaneDeprecationWarning, - match="Toggling the new approach to operator arithmetic", - ): - qml.operation.enable_new_opmath() - - assert qml.operation.active_new_opmath() - - def test_docstring_example_of_operator_class(tol): """Tests an example of how to create an operator which is used in the Operator class docstring, as well as in the 'adding_operators' @@ -2941,137 +1929,6 @@ class CustomOperator(qml.operation.Operator): qml.assert_equal(new_op, CustomOperator(2.3, wires=0)) -@pytest.mark.usefixtures("new_opmath_only") -def test_use_new_opmath_fixture(): - """Test that the fixture for using new opmath in a context works as expected""" - assert qml.operation.active_new_opmath() - - -@pytest.mark.usefixtures("legacy_opmath_only") -def test_legacy_opmath_only_fixture(): - """Test that the fixture for using new opmath in a context works as expected""" - assert not qml.operation.active_new_opmath() - - -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", "qml.operation.Tensor uses", qml.PennyLaneDeprecationWarning) - - CONVERT_HAMILTONIAN = [ - ( - [1.5, 0.5, 1, 1], - [ - qml.Identity(1), - Tensor(qml.Z(1), qml.Z(2)), - Tensor(qml.X(1), qml.Y(2)), - qml.Hadamard(1), - ], - ), - ([0.5], [qml.X(1)]), - ([1], [Tensor(qml.X(0), qml.Y(1))]), - ( - [-0.5, 0.4, -0.3, 0.2], - [ - qml.Identity(0, 1), - Tensor(qml.X(1), qml.Y(2)), - qml.Identity(1), - Tensor(qml.Z(1), qml.Z(2)), - ], - ), - ( - [0.0625, 0.0625, -0.0625, 0.0625, -0.0625, 0.0625, -0.0625, -0.0625], - [ - Tensor(qml.Hadamard(0), qml.X(1), qml.X(2), qml.Y(3)), - Tensor(qml.X(0), qml.X(1), qml.Y(2), qml.X(3)), - Tensor(qml.X(0), qml.Y(1), qml.X(2), qml.X(3)), - Tensor(qml.X(0), qml.Y(1), qml.Y(2), qml.Y(3)), - Tensor(qml.Y(0), qml.X(1), qml.X(2), qml.X(3)), - Tensor(qml.Y(0), qml.X(1), qml.Hadamard(2), qml.Y(3)), - Tensor(qml.Y(0), qml.Y(1), qml.X(2), qml.Y(3)), - Tensor(qml.Y(0), qml.Y(1), qml.Y(2), qml.Hadamard(3)), - ], - ), - ] - - -@pytest.mark.usefixtures("new_opmath_only") -@pytest.mark.parametrize("coeffs, obs", CONVERT_HAMILTONIAN) -def test_convert_to_hamiltonian(coeffs, obs): - """Test that arithmetic operators can be converted to Hamiltonian instances""" - - opmath_instance = qml.dot(coeffs, obs) - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="qml.ops.Hamiltonian uses the old approach" - ): - converted_opmath = convert_to_legacy_H(opmath_instance) - hamiltonian_instance = qml.ops.Hamiltonian(coeffs, obs) - - assert isinstance(converted_opmath, qml.ops.Hamiltonian) - qml.assert_equal(converted_opmath, hamiltonian_instance) - - -@pytest.mark.usefixtures("legacy_opmath_only") -@pytest.mark.parametrize( - "coeffs, obs", [([1], [qml.Hadamard(1)]), ([0.5, 0.5], [qml.Identity(1), qml.Identity(1)])] -) -def test_convert_to_hamiltonian_trivial(coeffs, obs): - """Test that non-arithmetic operator after simplification is returned as an Observable""" - - opmath_instance = qml.dot(coeffs, obs) - converted_opmath = convert_to_legacy_H(opmath_instance) - assert isinstance(converted_opmath, qml.operation.Observable) - - -@pytest.mark.parametrize( - "coeffs, obs", - [ - ([2], [qml.T(1)]), - ([0.5, 2], [qml.T(0), qml.Identity(1)]), - ([1, 2], [qml.T(0), qml.Identity(1)]), - ([0.5, 0.5], [qml.T(0), qml.T(0)]), - ], -) -def test_convert_to_hamiltonian_error(coeffs, obs): - """Test that arithmetic operator raise an error if there is a non-Observable""" - - with pytest.raises(ValueError): - convert_to_legacy_H(qml.dot(coeffs, obs)) - - -@pytest.mark.usefixtures("new_opmath_only") -@pytest.mark.filterwarnings("ignore::pennylane.PennyLaneDeprecationWarning") -def test_convert_to_H(): - operator = ( - 2 * qml.X(0) - + 3 * qml.X(0) - + qml.Y(1) @ qml.Z(2) @ (2 * qml.X(3)) - + 2 * (qml.Hadamard(3) + 3 * qml.Z(2)) - ) - with qml.operation.disable_new_opmath_cm(warn=False): - legacy_H = qml.operation.convert_to_H(operator) - linear_combination = qml.operation.convert_to_H(operator) - - assert isinstance(legacy_H, qml.ops.Hamiltonian) - assert isinstance(linear_combination, qml.ops.LinearCombination) - - # coeffs match - legacy_coeffs, legacy_ops = legacy_H.terms() - coeffs, ops = linear_combination.terms() - assert np.all(legacy_coeffs == coeffs) - - # legacy version has Tensors and not Prods, new version opposite - assert Tensor in [type(o) for o in legacy_ops] - assert Tensor not in [type(o) for o in ops] - assert qml.ops.op_math.Prod not in [type(o) for o in legacy_ops] - assert qml.ops.op_math.Prod in [type(o) for o in ops] - - # ops match - for legacy_op, op in zip(legacy_ops, ops): - assert np.all(legacy_op.matrix() == op.matrix()) - - # the converted op is the same as the original op - qml.assert_equal(operator.simplify(), linear_combination.simplify()) - - # pylint: disable=unused-import,no-name-in-module def test_get_attr(): """Test that importing attributes of operation work as expected""" @@ -3092,28 +1949,3 @@ def test_get_attr(): assert ( StatePrep is qml.operation.StatePrepBase ) # StatePrep imported from operation.py is an alias for StatePrepBase - - -@pytest.mark.parametrize( - "make_op", - [ - lambda: qml.Hamiltonian([1, 2], [qml.PauliX(0), qml.PauliY(1)]), - lambda: 1.2 * qml.PauliX(0), - ], -) -@pytest.mark.usefixtures("use_legacy_and_new_opmath") -def test_convert_to_opmath_queueing(make_op): - """Tests that converting to opmath dequeues the original operation""" - - with qml.queuing.AnnotatedQueue() as q: - if not qml.operation.active_new_opmath(): - with pytest.warns( - qml.PennyLaneDeprecationWarning, match="qml.ops.Hamiltonian uses the old approach" - ): - original_op = make_op() - else: - original_op = make_op() - new_op = qml.operation.convert_to_opmath(original_op) - - assert len(q.queue) == 1 - assert q.queue[0] is new_op diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py index da8d948e4e0..326b46297a3 100644 --- a/tests/test_qaoa.py +++ b/tests/test_qaoa.py @@ -105,7 +105,7 @@ def matrix(hamiltonian: qml.Hamiltonian, n_wires: int) -> csc_matrix: op_matrix = op.sparse_matrix(wire_order=list(range(n_wires))) else: op_wires = np.array(op.wires.tolist()) - op_list = op.non_identity_obs if isinstance(op, qml.operation.Tensor) else [op] + op_list = [op] op_matrices = [] for wire in range(n_wires): @@ -325,7 +325,6 @@ def make_bit_flip_mixer_test_cases(): class TestMixerHamiltonians: """Tests that the mixer Hamiltonians are being generated correctly""" - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_x_mixer_output(self): """Tests that the output of the Pauli-X mixer is correct""" @@ -337,7 +336,6 @@ def test_x_mixer_output(self): ) assert mixer_hamiltonian.compare(expected_hamiltonian) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_x_mixer_grouping(self): """Tests that the grouping information is set and correct""" @@ -360,13 +358,9 @@ def test_xy_mixer_type_error(self): ): qaoa.xy_mixer(graph) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize(("graph", "target_hamiltonian"), make_xy_mixer_test_cases()) def test_xy_mixer_output(self, graph, target_hamiltonian): """Tests that the output of the XY mixer is correct""" - - if not qml.operation.active_new_opmath(): - target_hamiltonian = qml.operation.convert_to_legacy_H(target_hamiltonian) hamiltonian = qaoa.xy_mixer(graph) assert hamiltonian.compare(target_hamiltonian) @@ -387,12 +381,8 @@ def test_bit_flip_mixer_errors(self): ("graph", "n", "target_hamiltonian"), make_bit_flip_mixer_test_cases(), ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): """Tests that the output of the bit-flip mixer is correct""" - - if not qml.operation.active_new_opmath(): - target_hamiltonian = qml.operation.convert_to_legacy_H(target_hamiltonian) hamiltonian = qaoa.bit_flip_mixer(graph, n) assert hamiltonian.compare(target_hamiltonian) @@ -928,7 +918,6 @@ def test_bit_driver_error(self): with pytest.raises(ValueError, match=r"'b' must be either 0 or 1"): qaoa.bit_driver(range(3), 2) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_bit_driver_output(self): """Tests that the bit driver Hamiltonian has the correct output""" @@ -953,13 +942,9 @@ def test_edge_driver_errors(self): with pytest.raises(ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph"): qaoa.edge_driver([(0, 1), (1, 2)], ["00", "11"]) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize(("graph", "reward", "hamiltonian"), make_edge_driver_cost_test_cases()) def test_edge_driver_output(self, graph, reward, hamiltonian): """Tests that the edge driver Hamiltonian throws the correct errors""" - - if not qml.operation.active_new_opmath(): - hamiltonian = qml.operation.convert_to_legacy_H(hamiltonian) H = qaoa.edge_driver(graph, reward) assert hamiltonian.compare(H) @@ -988,18 +973,12 @@ def test_cost_graph_error(self): @pytest.mark.parametrize( ("graph", "cost_hamiltonian", "mixer_hamiltonian"), make_max_cut_test_cases() ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_maxcut_output(self, graph, cost_hamiltonian, mixer_hamiltonian): """Tests that the output of the MaxCut method is correct""" - - if not qml.operation.active_new_opmath(): - cost_hamiltonian = qml.operation.convert_to_legacy_H(cost_hamiltonian) - mixer_hamiltonian = qml.operation.convert_to_legacy_H(mixer_hamiltonian) cost_h, mixer_h = qaoa.maxcut(graph) assert cost_h.compare(cost_hamiltonian) assert mixer_h.compare(mixer_hamiltonian) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_maxcut_grouping(self): """Tests that the grouping information is set and correct""" @@ -1017,18 +996,12 @@ def test_maxcut_grouping(self): ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian"), make_max_independent_test_cases(), ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_mis_output(self, graph, constrained, cost_hamiltonian, mixer_hamiltonian): """Tests that the output of the Max Indepenent Set method is correct""" - - if not qml.operation.active_new_opmath(): - cost_hamiltonian = qml.operation.convert_to_legacy_H(cost_hamiltonian) - mixer_hamiltonian = qml.operation.convert_to_legacy_H(mixer_hamiltonian) cost_h, mixer_h = qaoa.max_independent_set(graph, constrained=constrained) assert cost_h.compare(cost_hamiltonian) assert mixer_h.compare(mixer_hamiltonian) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_mis_grouping(self): """Tests that the grouping information is set and correct""" @@ -1046,18 +1019,12 @@ def test_mis_grouping(self): ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian"), make_min_vertex_cover_test_cases(), ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_mvc_output(self, graph, constrained, cost_hamiltonian, mixer_hamiltonian): """Tests that the output of the Min Vertex Cover method is correct""" - - if not qml.operation.active_new_opmath(): - cost_hamiltonian = qml.operation.convert_to_legacy_H(cost_hamiltonian) - mixer_hamiltonian = qml.operation.convert_to_legacy_H(mixer_hamiltonian) cost_h, mixer_h = qaoa.min_vertex_cover(graph, constrained=constrained) assert cost_h.compare(cost_hamiltonian) assert mixer_h.compare(mixer_hamiltonian) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_mvc_grouping(self): """Tests that the grouping information is set and correct""" @@ -1075,18 +1042,12 @@ def test_mvc_grouping(self): ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian"), make_max_clique_test_cases(), ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_max_clique_output(self, graph, constrained, cost_hamiltonian, mixer_hamiltonian): """Tests that the output of the Maximum Clique method is correct""" - - if not qml.operation.active_new_opmath(): - cost_hamiltonian = qml.operation.convert_to_legacy_H(cost_hamiltonian) - mixer_hamiltonian = qml.operation.convert_to_legacy_H(mixer_hamiltonian) cost_h, mixer_h = qaoa.max_clique(graph, constrained=constrained) assert cost_h.compare(cost_hamiltonian) assert mixer_h.compare(mixer_hamiltonian) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_max_clique_grouping(self): """Tests that the grouping information is set and correct""" @@ -1105,21 +1066,15 @@ def test_max_clique_grouping(self): ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian", "mapping"), make_max_weighted_cycle_test_cases(), ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_max_weight_cycle_output( self, graph, constrained, cost_hamiltonian, mixer_hamiltonian, mapping ): """Tests that the output of the maximum weighted cycle method is correct""" - - if not qml.operation.active_new_opmath(): - cost_hamiltonian = qml.operation.convert_to_legacy_H(cost_hamiltonian) - mixer_hamiltonian = qml.operation.convert_to_legacy_H(mixer_hamiltonian) cost_h, mixer_h, m = qaoa.max_weight_cycle(graph, constrained=constrained) assert cost_h.compare(cost_hamiltonian) assert mixer_h.compare(mixer_hamiltonian) assert mapping == m - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_max_weight_cycle_grouping(self): """Tests that the grouping information is set and correct""" @@ -1139,7 +1094,7 @@ class TestUtils: """Tests that the utility functions are working properly""" # pylint: disable=protected-access - @pytest.mark.usefixtures("legacy_opmath_only") + # removed a fixture to only use legacy opmath here for now, because I'm not sure why its relevant @pytest.mark.parametrize( ("hamiltonian", "value"), ( @@ -1269,7 +1224,6 @@ def test_cost_layer_output(self, cost, gates): class TestIntegration: """Test integration of the QAOA module with PennyLane""" - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_module_example(self, tol): """Test the example in the QAOA module docstring""" @@ -1306,7 +1260,6 @@ def cost_function(params): assert np.allclose(res, expected, atol=tol, rtol=0) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_module_example_rx(self, tol): """Test the example in the QAOA module docstring""" @@ -1424,7 +1377,6 @@ def test_wires_to_edges_rx(self): "g", [nx.complete_graph(4).to_directed(), rx.generators.directed_mesh_graph(4, [0, 1, 2, 3])], ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_partial_cycle_mixer_complete(self, g): """Test if the _partial_cycle_mixer function returns the expected Hamiltonian for a fixed example""" @@ -1486,7 +1438,6 @@ def test_partial_cycle_mixer_error(self, g): "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])], ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_cycle_mixer(self, g): """Test if the cycle_mixer Hamiltonian maps valid cycles to valid cycles""" @@ -1561,7 +1512,6 @@ def test_cycle_mixer_error(self, g): cycle_mixer(g) @pytest.mark.parametrize("g", [nx.lollipop_graph(3, 1), lollipop_graph_rx(3, 1)]) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_matrix(self, g): """Test that the matrix function works as expected on a fixed example""" h = qml.qaoa.bit_flip_mixer(g, 0) @@ -1590,7 +1540,6 @@ def test_matrix(self, g): assert np.allclose(mat.toarray(), mat_expected) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_matrix_rx(self): """Test that the matrix function works as expected on a fixed example""" g = rx.generators.star_graph(4, [0, 1, 2, 3]) @@ -1671,7 +1620,6 @@ def test_wires_to_edges_directed(self, g): @pytest.mark.parametrize( "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_loss_hamiltonian_complete(self, g): """Test if the loss_hamiltonian function returns the expected result on a manually-calculated example of a 3-node complete digraph""" @@ -1710,7 +1658,6 @@ def test_loss_hamiltonian_error(self): @pytest.mark.parametrize( "g", [nx.lollipop_graph(4, 1).to_directed(), lollipop_graph_rx(4, 1, to_directed=True)] ) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_loss_hamiltonian_incomplete(self, g): """Test if the loss_hamiltonian function returns the expected result on a manually-calculated example of a 4-node incomplete digraph""" @@ -1805,7 +1752,6 @@ def test_missing_edge_weight_data_without_weights(self): with pytest.raises(TypeError, match="does not contain weight data"): loss_hamiltonian(g) - @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_square_hamiltonian_terms(self): """Test if the _square_hamiltonian_terms function returns the expected result on a fixed example""" @@ -1906,31 +1852,6 @@ def test_inner_net_flow_constraint_hamiltonian(self, g): for op, expected_op in zip(non_zero_ops, expected_ops): assert op.pauli_rep == expected_op.pauli_rep - @pytest.mark.parametrize( - "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] - ) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_inner_net_flow_constraint_hamiltonian_legacy_opmath(self, g): - """Test if the _inner_net_flow_constraint_hamiltonian function returns the expected result on a manually-calculated - example of a 3-node complete digraph relative to the 0 node""" - h = _inner_net_flow_constraint_hamiltonian(g, 0) - - expected_ops = [ - qml.Identity(0), - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliZ(0) @ qml.PauliZ(2), - qml.PauliZ(0) @ qml.PauliZ(4), - qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliZ(1) @ qml.PauliZ(4), - qml.PauliZ(2) @ qml.PauliZ(4), - ] - expected_coeffs = [4, 2, -2, -2, -2, -2, 2] - - assert np.allclose(expected_coeffs, h.coeffs) - for i, expected_op in enumerate(expected_ops): - assert str(h.ops[i]) == str(expected_op) - assert all(op.wires == exp.wires for op, exp in zip(h.ops, expected_ops)) - @pytest.mark.parametrize("g", [nx.complete_graph(3), rx.generators.mesh_graph(3, [0, 1, 2])]) def test_inner_net_flow_constraint_hamiltonian_error(self, g): """Test if the _inner_net_flow_constraint_hamiltonian function returns raises ValueError""" @@ -1940,7 +1861,6 @@ def test_inner_net_flow_constraint_hamiltonian_error(self, g): @pytest.mark.parametrize( "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] ) - @pytest.mark.usefixtures("new_opmath_only") def test_inner_out_flow_constraint_hamiltonian_non_complete(self, g): """Test if the _inner_out_flow_constraint_hamiltonian function returns the expected result on a manually-calculated example of a 3-node complete digraph relative to the 0 node, with @@ -1956,25 +1876,6 @@ def test_inner_out_flow_constraint_hamiltonian_non_complete(self, g): for op, expected_op in zip(ops, expected_ops): assert op.pauli_rep == expected_op.pauli_rep - @pytest.mark.parametrize( - "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] - ) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_inner_out_flow_constraint_hamiltonian_non_complete_legacy_opmath(self, g): - """Test if the _inner_out_flow_constraint_hamiltonian function returns the expected result - on a manually-calculated example of a 3-node complete digraph relative to the 0 node, with - the (0, 1) edge removed""" - g.remove_edge(0, 1) - h = _inner_out_flow_constraint_hamiltonian(g, 0) - - expected_ops = [qml.PauliZ(wires=[0])] - expected_coeffs = [0] - - assert np.allclose(expected_coeffs, h.coeffs) - for i, expected_op in enumerate(expected_ops): - assert str(h.ops[i]) == str(expected_op) - assert all(op.wires == exp.wires for op, exp in zip(h.ops, expected_ops)) - @pytest.mark.parametrize( "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] ) @@ -1999,32 +1900,6 @@ def test_inner_net_flow_constraint_hamiltonian_non_complete(self, g): for op, expected_op in zip(ops, expected_ops): assert op.pauli_rep == expected_op.pauli_rep - @pytest.mark.parametrize( - "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] - ) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_inner_net_flow_constraint_hamiltonian_non_complete_legacy_opmath(self, g): - """Test if the _inner_net_flow_constraint_hamiltonian function returns the expected result on a manually-calculated - example of a 3-node complete digraph relative to the 0 node, with the (1, 0) edge removed""" - g.remove_edge(1, 0) - h = _inner_net_flow_constraint_hamiltonian(g, 0) - - expected_ops = [ - qml.Identity(0), - qml.PauliZ(0), - qml.PauliZ(1), - qml.PauliZ(3), - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliZ(0) @ qml.PauliZ(3), - qml.PauliZ(1) @ qml.PauliZ(3), - ] - expected_coeffs = [4, -2, -2, 2, 2, -2, -2] - - assert np.allclose(expected_coeffs, h.coeffs) - for i, expected_op in enumerate(expected_ops): - assert str(h.ops[i]) == str(expected_op) - assert all(op.wires == exp.wires for op, exp in zip(h.ops, expected_ops)) - def test_out_flow_constraint_raises(self): """Test the out-flow constraint function may raise an error.""" diff --git a/tests/test_qnode_legacy.py b/tests/test_qnode_legacy.py index f620f51b9e6..4c953cbb810 100644 --- a/tests/test_qnode_legacy.py +++ b/tests/test_qnode_legacy.py @@ -1595,7 +1595,6 @@ def circuit(): assert len(tapes) == 2 - @pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("grouping", [True, False]) def test_multiple_hamiltonian_expansion_finite_shots(self, grouping): """Test that multiple Hamiltonians works correctly (sum_expand should be used)""" diff --git a/tests/test_queuing.py b/tests/test_queuing.py index 111c5846df7..f9a953e174d 100644 --- a/tests/test_queuing.py +++ b/tests/test_queuing.py @@ -197,31 +197,6 @@ def test_append_qubit_observables(self): ] assert q.queue == ops - @pytest.mark.usefixtures("legacy_opmath_only") - def test_append_tensor_ops(self): - """Test that ops which are used as inputs to `Tensor` - are successfully added to the queue, as well as the `Tensor` object.""" - - with AnnotatedQueue() as q: - A = qml.PauliZ(0) - B = qml.PauliY(1) - tensor_op = qml.operation.Tensor(A, B) - assert q.queue == [tensor_op] - assert tensor_op.obs == [A, B] - - @pytest.mark.usefixtures("legacy_opmath_only") - def test_append_tensor_ops_overloaded(self): - """Test that Tensor ops created using `@` - are successfully added to the queue, as well as the `Tensor` object.""" - - with AnnotatedQueue() as q: - A = qml.PauliZ(0) - B = qml.PauliY(1) - tensor_op = A @ B - assert q.queue == [tensor_op] - assert tensor_op.obs == [A, B] - - @pytest.mark.usefixtures("new_opmath_only") def test_append_prod_ops_overloaded(self): """Test that Prod ops created using `@` are successfully added to the queue, as well as the `Prod` object.""" @@ -293,16 +268,6 @@ def test_update_info_not_in_queue(self): q.update_info(B, inv=True) assert len(q.queue) == 1 - def test_append_annotating_object(self): - """Test appending an object that writes annotations when queuing itself""" - - with AnnotatedQueue() as q: - A = qml.PauliZ(0) - B = qml.PauliY(1) - tensor_op = qml.operation.Tensor(A, B) - - assert q.queue == [tensor_op] - def test_parallel_queues_are_isolated(self): """Tests that parallel queues do not queue each other's constituents.""" q1 = AnnotatedQueue() @@ -325,8 +290,6 @@ def queue_pauli(arg): test_observables = [ qml.PauliZ(0) @ qml.PauliZ(1), - qml.operation.Tensor(qml.PauliZ(0), qml.PauliX(1)), - qml.operation.Tensor(qml.PauliZ(0), qml.PauliX(1)) @ qml.Hadamard(2), qml.Hamiltonian( [0.1, 0.2, 0.3], [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliY(1), qml.Identity(2)] ), diff --git a/tests/test_vqe.py b/tests/test_vqe.py index 60b16bd2fc1..8ef5b27497b 100644 --- a/tests/test_vqe.py +++ b/tests/test_vqe.py @@ -229,22 +229,6 @@ def amp_embed_and_strong_ent_layer(params, wires=None): add_queue = zip(QUEUE_HAMILTONIANS_1, QUEUE_HAMILTONIANS_2, QUEUES) -##################################################### -# Helper functions - - -def _convert_obs_to_legacy_opmath(obs): - """Convert single-term observables to legacy opmath""" - - if isinstance(obs, qml.ops.Prod): - return qml.operation.Tensor(*obs.operands) - - if isinstance(obs, (list, tuple)): - return [_convert_obs_to_legacy_opmath(o) for o in obs] - - return obs - - ##################################################### # Tests @@ -256,8 +240,6 @@ class TestVQE: @pytest.mark.parametrize("coeffs, observables", list(zip(COEFFS, OBSERVABLES))) def test_cost_evaluate(self, params, ansatz, coeffs, observables): """Tests that the cost function evaluates properly""" - if not qml.operation.active_new_opmath(): - observables = _convert_obs_to_legacy_opmath(observables) hamiltonian = qml.Hamiltonian(coeffs, observables) dev = qml.device("default.qubit", wires=3) expval = generate_cost_fn(ansatz, hamiltonian, dev) @@ -269,8 +251,6 @@ def test_cost_evaluate(self, params, ansatz, coeffs, observables): ) def test_cost_expvals(self, coeffs, observables, expected): """Tests that the cost function returns correct expectation values""" - if not qml.operation.active_new_opmath() and (not coeffs or all(c == 0 for c in coeffs)): - pytest.skip("Legacy opmath does not support zero Hamiltonians") dev = qml.device("default.qubit", wires=2) hamiltonian = qml.Hamiltonian(coeffs, observables) cost = generate_cost_fn(lambda params, **kwargs: None, hamiltonian, dev) @@ -732,9 +712,6 @@ class TestNewVQE: def test_circuits_evaluate(self, ansatz, observables, params, tol): """Tests simple VQE evaluations.""" - if not qml.operation.active_new_opmath(): - observables = _convert_obs_to_legacy_opmath(observables) - coeffs = [1.0] * len(observables) dev = qml.device("default.qubit", wires=3) H = qml.Hamiltonian(coeffs, observables) @@ -883,40 +860,6 @@ def circuit1(): assert res[0] == circuit1() assert res[1] == circuit1() - # the LinearCombination implementation does have diagonalizing gates, - # but legacy Hamiltonian does not and fails - @pytest.mark.usefixtures("legacy_opmath_only") - def test_error_var_measurement(self): - """Tests that error is thrown if var(H) is measured.""" - observables = [qml.PauliZ(0), qml.PauliY(0), qml.PauliZ(1)] - coeffs = [1.0] * len(observables) - dev = qml.device("default.qubit", wires=2) - H = qml.Hamiltonian(coeffs, observables) - - @qml.qnode(dev) - def circuit(): - return qml.var(H) - - with pytest.raises(NotImplementedError): - circuit() - - # the LinearCombination implementation does have diagonalizing gates, - # but legacy Hamiltonian does not and fails - @pytest.mark.usefixtures("legacy_opmath_only") - def test_error_sample_measurement(self): - """Tests that error is thrown if sample(H) is measured.""" - observables = [qml.PauliZ(0), qml.PauliY(0), qml.PauliZ(1)] - coeffs = [1.0] * len(observables) - dev = qml.device("default.qubit", wires=2, shots=10) - H = qml.Hamiltonian(coeffs, observables) - - @qml.qnode(dev) - def circuit(): - return qml.sample(H) - - with pytest.raises(qml.operation.DiagGatesUndefinedError): - circuit() - @pytest.mark.autograd @pytest.mark.parametrize("diff_method", ["parameter-shift", "best"]) def test_grad_autograd(self, diff_method, tol): @@ -1015,7 +958,6 @@ def circuit(w): @pytest.mark.xfail( reason="diagonalizing gates defined but not used, should not be included in specs" ) - @pytest.mark.usefixtures("new_opmath_only") def test_specs(self): """Test that the specs of a VQE circuit can be computed""" dev = qml.device("default.qubit", wires=2) @@ -1036,23 +978,6 @@ def circuit(): # to be revisited in [sc-59117] assert res["num_diagonalizing_gates"] == 0 - @pytest.mark.usefixtures("legacy_opmath_only") - def test_specs_legacy_opmath(self): - """Test that the specs of a VQE circuit can be computed""" - dev = qml.device("default.qubit", wires=2) - H = qml.Hamiltonian([0.1, 0.2], [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1)]) - - @qml.qnode(dev) - def circuit(): - qml.Hadamard(wires=0) - qml.CNOT(wires=[0, 1]) - return qml.expval(H) - - res = qml.specs(circuit)() - - assert res["num_observables"] == 1 - assert res["num_diagonalizing_gates"] == 0 - class TestInterfaces: """Tests for VQE with interfaces.""" diff --git a/tests/transforms/test_add_noise.py b/tests/transforms/test_add_noise.py index abd8d5f665c..082f8ce6b1b 100644 --- a/tests/transforms/test_add_noise.py +++ b/tests/transforms/test_add_noise.py @@ -106,11 +106,8 @@ def test_noise_tape(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" + assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -142,11 +139,8 @@ def test_noise_tape_with_state_prep(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" + assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -245,11 +239,8 @@ def test_add_noise_dev(self, dev_name): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 2 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" + assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation assert tape.observables[1].name == "PauliZ" diff --git a/tests/transforms/test_convert_to_numpy_parameters.py b/tests/transforms/test_convert_to_numpy_parameters.py index d09f80c7eee..693f6b08b82 100644 --- a/tests/transforms/test_convert_to_numpy_parameters.py +++ b/tests/transforms/test_convert_to_numpy_parameters.py @@ -113,10 +113,10 @@ def test_unwraps_arithmetic_op_measurement(): @pytest.mark.autograd -def test_unwraps_tensor_observables(): - """Test that the measurement helper function can set data on a tensor observable.""" +def test_unwraps_prod_observables(): + """Test that the measurement helper function can set data on a prod observable.""" mat = qml.numpy.eye(2) - obs = qml.operation.Tensor(qml.PauliZ(0), qml.Hermitian(mat, 1)) + obs = qml.prod(qml.PauliZ(0), qml.Hermitian(mat, 1)) m = qml.expval(obs) unwrapped_m = _convert_measurement_to_numpy_data(m) diff --git a/tests/transforms/test_defer_measurements.py b/tests/transforms/test_defer_measurements.py index 91d01a46415..aee71070ead 100644 --- a/tests/transforms/test_defer_measurements.py +++ b/tests/transforms/test_defer_measurements.py @@ -630,7 +630,7 @@ def test_measure_with_tensor_obs(self, mid_measure_wire, tp_wires): with qml.queuing.AnnotatedQueue() as q: qml.measure(mid_measure_wire) - qml.expval(qml.operation.Tensor(*[qml.PauliZ(w) for w in tp_wires])) + qml.expval(qml.prod(*[qml.PauliZ(w) for w in tp_wires])) tape = qml.tape.QuantumScript.from_queue(q) tape, _ = qml.defer_measurements(tape) diff --git a/tests/transforms/test_diagonalize_measurements.py b/tests/transforms/test_diagonalize_measurements.py index 70122f5bb69..7cf9efc4349 100644 --- a/tests/transforms/test_diagonalize_measurements.py +++ b/tests/transforms/test_diagonalize_measurements.py @@ -97,11 +97,6 @@ def test_visited_obs_arg(self, obs, apply_gates): "compound_obs, expected_res, base_obs", [ (X(0) @ Y(2), Z(0) @ Z(2), [X(0), Y(2)]), # prod - ( - qml.operation.Tensor(X(0), Y(2)), - qml.operation.Tensor(Z(0), Z(2)), - [X(0), Y(2)], - ), # tensor (X(1) + Y(2), Z(1) + Z(2), [X(1), Y(2)]), # sum (2 * X(1), 2 * Z(1), [X(1)]), # sprod ( @@ -132,35 +127,10 @@ def test_compound_observables(self, compound_obs, expected_res, base_obs): assert visited_obs == (set(base_obs), {o.wires[0] for o in base_obs}) assert diagonalizing_gates == list(expected_diag_gates) - def test_legacy_hamiltonian(self): - """Test that _diagonalize_observable works on legacy Hamiltonians observables""" - - if qml.operation.active_new_opmath(): - with pytest.warns(): - compound_obs = qml.ops.Hamiltonian([2, 3], [Y(0), X(1)]) - expected_res = qml.ops.Hamiltonian([2, 3], [Z(0), Z(1)]) - diagonalizing_gates, new_obs, visited_obs = _diagonalize_observable(compound_obs) - else: - compound_obs = qml.ops.Hamiltonian([2, 3], [Y(0), X(1)]) - expected_res = qml.ops.Hamiltonian([2, 3], [Z(0), Z(1)]) - diagonalizing_gates, new_obs, visited_obs = _diagonalize_observable(compound_obs) - - base_obs = [Y(0), X(1)] - expected_diag_gates = np.concatenate([o.diagonalizing_gates() for o in base_obs]) - - assert new_obs == expected_res - assert visited_obs == (set(base_obs), {o.wires[0] for o in base_obs}) - assert diagonalizing_gates == list(expected_diag_gates) - @pytest.mark.parametrize( "compound_obs, expected_res, base_obs", [ (X(0) @ Y(2), X(0) @ Z(2), [X(0), Y(2)]), # prod - ( - qml.operation.Tensor(X(0), Y(2)), - qml.operation.Tensor(X(0), Z(2)), - [X(0), Y(2)], - ), # tensor (X(1) + Y(2), X(1) + Z(2), [X(1), Y(2)]), # sum (2 * X(1), 2 * X(1), [X(1)]), # sprod ( @@ -327,7 +297,6 @@ def test_diagonalize_all_measurements(self, to_eigvals): assert fn == null_postprocessing - @pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize( "obs, expected_obs, diag_gates", [ @@ -370,47 +339,6 @@ def test_diagonalize_all_measurements_composite_obs( assert fn == null_postprocessing - @pytest.mark.usefixtures("legacy_opmath_only") - def test_diagonalize_all_measurements_hamiltonian(self): - """Test that the diagonalize_measurements transform diagonalizes a Hamiltonian with a pauli_rep - when diagonalizing all measurements""" - obs = qml.ops.Hamiltonian([1, 2], [X(1), Y(2)]) - expected_obs = qml.ops.Hamiltonian([1, 2], [Z(1), Z(2)]) - - assert obs.pauli_rep is not None - - measurements = [qml.expval(obs)] - - tape = QuantumScript([], measurements=measurements) - tapes, fn = diagonalize_measurements(tape) - new_tape = tapes[0] - - assert new_tape.measurements == [qml.expval(expected_obs)] - assert new_tape.operations == diagonalize_qwc_pauli_words(obs.ops)[0] - - assert fn == null_postprocessing - - @pytest.mark.usefixtures("legacy_opmath_only") - def test_diagonalize_all_measurements_tensor(self): - """Test that the diagonalize_measurements transform diagonalizes a Tensor with a pauli_rep - when diagonalizing all measurements""" - - obs = qml.operation.Tensor(X(1), Y(2)) - expected_obs = qml.operation.Tensor(Z(1), Z(2)) - - assert obs.pauli_rep is not None - - measurements = [qml.expval(obs)] - - tape = QuantumScript([], measurements=measurements) - tapes, fn = diagonalize_measurements(tape) - new_tape = tapes[0] - - assert new_tape.measurements == [qml.expval(expected_obs)] - assert new_tape.operations == diagonalize_qwc_pauli_words(obs.obs)[0] - - assert fn == null_postprocessing - def test_diagonalize_subset_of_measurements(self): """Test that the diagonalize_measurements transform diagonalizes the measurements on the tape when diagonalizing a subset of the measurements""" @@ -621,7 +549,6 @@ def test_bad_to_eigvals_input_raises_error(self, supported_base_obs): QuantumScript([]), supported_base_obs=supported_base_obs, to_eigvals=True ) - @pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("to_eigvals", [True, False]) @pytest.mark.parametrize("supported_base_obs", ([qml.Z], [qml.Z, qml.X], [qml.Z, qml.X, qml.Y])) @pytest.mark.parametrize("shots", [None, 2000, (4000, 5000, 6000)]) diff --git a/tests/transforms/test_insert_ops.py b/tests/transforms/test_insert_ops.py index 9a21d7268ec..bf03d9b7c5c 100644 --- a/tests/transforms/test_insert_ops.py +++ b/tests/transforms/test_insert_ops.py @@ -92,11 +92,7 @@ def test_start(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -127,11 +123,7 @@ def test_all(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -161,11 +153,7 @@ def test_before(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -204,11 +192,7 @@ def test_operation_as_position(self, op): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -237,11 +221,7 @@ def test_operation_list_as_position(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -268,11 +248,7 @@ def test_end(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -301,11 +277,7 @@ def test_start_with_state_prep(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -338,11 +310,7 @@ def test_all_with_state_prep(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -373,11 +341,7 @@ def test_end_with_state_prep(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -412,11 +376,7 @@ def op(x, y, wires): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -500,11 +460,7 @@ def test_insert_dev(dev_name): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 2 - assert ( - tape.observables[0].name == "Prod" - if qml.operation.active_new_opmath() - else ["PauliZ", "PauliZ"] - ) + assert tape.observables[0].name == "Prod" assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation assert tape.observables[1].name == "PauliZ" diff --git a/tests/transforms/test_qcut.py b/tests/transforms/test_qcut.py index 82d2fefb4cd..1574f9aa45c 100644 --- a/tests/transforms/test_qcut.py +++ b/tests/transforms/test_qcut.py @@ -1585,15 +1585,8 @@ def test_single_measurement(self): assert obs[0].wires.tolist() == [1, 0, 2] assert obs[1].wires.tolist() == [1, 0] - if qml.operation.active_new_opmath(): - - assert [get_name(o) for o in obs[0].terms()[1]] == ["Prod"] - assert [get_name(o) for o in obs[1].terms()[1]] == ["Prod"] - - else: - - assert [get_name(o) for o in obs[0].obs] == ["PauliZ", "PauliX", "PauliZ"] - assert [get_name(o) for o in obs[1].obs] == ["PauliZ", "PauliX"] + assert [get_name(o) for o in obs[0].terms()[1]] == ["Prod"] + assert [get_name(o) for o in obs[1].terms()[1]] == ["Prod"] class TestExpandFragmentTapes: diff --git a/tests/transforms/test_split_non_commuting.py b/tests/transforms/test_split_non_commuting.py index 37931f70ea3..7dd60342224 100644 --- a/tests/transforms/test_split_non_commuting.py +++ b/tests/transforms/test_split_non_commuting.py @@ -70,18 +70,6 @@ ] -def _convert_obs_to_legacy_opmath(obs): - """Convert single-term observables to legacy opmath""" - - if isinstance(obs, qml.ops.Prod): - return qml.operation.Tensor(*obs.operands) - - if isinstance(obs, list): - return [_convert_obs_to_legacy_opmath(o) for o in obs] - - return obs - - def complex_no_grouping_processing_fn(results): """The expected processing function without grouping of complex_obs_list""" @@ -172,8 +160,6 @@ def test_number_of_tapes_single_hamiltonian(self, grouping_strategy, n_tapes, ma """Tests that the correct number of tapes is returned for a single Hamiltonian""" obs_list = single_term_obs_list - if not qml.operation.active_new_opmath(): - obs_list = _convert_obs_to_legacy_opmath(obs_list) obs_list = obs_list + [qml.Y(0), qml.X(0) @ qml.Y(1)] # add duplicate terms coeffs = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7] @@ -224,8 +210,6 @@ def test_existing_grouping_used_for_single_hamiltonian(self, grouping_strategy, what is requested through the ``grouping_strategy`` argument.""" obs_list = single_term_obs_list - if not qml.operation.active_new_opmath(): - obs_list = _convert_obs_to_legacy_opmath(obs_list) H = make_H(obs_list) H.compute_grouping() @@ -354,19 +338,10 @@ def test_grouping_strategies(self): assert qml.math.allclose(fn([0.1, 0.2, 0.3, 0.4, 0.5]), [0.01, 0.04, 0.09, 0.16, 0.25]) tapes, fn = split_non_commuting(tape, grouping_strategy="default") - # When new opmath is disabled, c * o gives Hamiltonians, which leads to wires grouping - if qml.operation.active_new_opmath(): - for actual_tape, expected_tape in zip(tapes, expected_tapes_qwc_grouping): - qml.assert_equal(actual_tape, expected_tape) - assert qml.math.allclose( - fn([[0.1, 0.2], [0.3, 0.4, 0.5]]), [0.01, 0.06, 0.12, 0.08, 0.25] - ) - else: - for actual_tape, expected_tape in zip(tapes, expected_tapes_wires_grouping): - qml.assert_equal(actual_tape, expected_tape) - assert qml.math.allclose( - fn([[0.1, 0.2], 0.3, 0.4, 0.5]), [0.01, 0.06, 0.06, 0.16, 0.25] - ) + + for actual_tape, expected_tape in zip(tapes, expected_tapes_qwc_grouping): + qml.assert_equal(actual_tape, expected_tape) + assert qml.math.allclose(fn([[0.1, 0.2], [0.3, 0.4, 0.5]]), [0.01, 0.06, 0.12, 0.08, 0.25]) tapes, fn = split_non_commuting(tape, grouping_strategy="qwc") for actual_tape, expected_tape in zip(tapes, expected_tapes_qwc_grouping): @@ -388,12 +363,9 @@ def test_grouping_strategies(self): def test_grouping_strategies_single_hamiltonian(self, make_H): """Tests that a single Hamiltonian or Sum is split correctly""" - coeffs, obs_list = [0.1, 0.2, 0.3, 0.4, 0.5], single_term_obs_list - qwc_groups = single_term_qwc_groups - - if not qml.operation.active_new_opmath(): - obs_list = _convert_obs_to_legacy_opmath(obs_list) - qwc_groups = _convert_obs_to_legacy_opmath(single_term_qwc_groups) + coeffs = [0.1, 0.2, 0.3, 0.4, 0.5] + obs_list = single_term_obs_list + H = make_H(coeffs, obs_list) # Tests that constant offset is handled expected_tapes_no_grouping = [ qml.tape.QuantumScript([], [qml.expval(o)], shots=100) for o in obs_list @@ -401,32 +373,24 @@ def test_grouping_strategies_single_hamiltonian(self, make_H): expected_tapes_qwc_grouping = [ qml.tape.QuantumScript([], [qml.expval(o) for o in group], shots=100) - for group in qwc_groups + for group in single_term_qwc_groups ] - if qml.operation.active_new_opmath(): - coeffs, obs_list = coeffs + [0.6], obs_list + [qml.I()] - + coeffs, obs_list = coeffs + [0.6], obs_list + [qml.I()] H = make_H(coeffs, obs_list) # Tests that constant offset is handled - if not qml.operation.active_new_opmath() and isinstance(H, qml.ops.Sum): - pytest.skip("Sum is not part of legacy opmath") - tape = qml.tape.QuantumScript([], [qml.expval(H)], shots=100) tapes, fn = split_non_commuting(tape, grouping_strategy=None) for actual_tape, expected_tape in zip(tapes, expected_tapes_no_grouping): qml.assert_equal(actual_tape, expected_tape) - expected = 0.55 if not qml.operation.active_new_opmath() else 1.15 - assert qml.math.allclose(fn([0.1, 0.2, 0.3, 0.4, 0.5]), expected) + assert qml.math.allclose(fn([0.1, 0.2, 0.3, 0.4, 0.5]), 1.15) tapes, fn = split_non_commuting(tape, grouping_strategy="default") for actual_tape, expected_tape in zip(tapes, expected_tapes_qwc_grouping): qml.assert_equal(actual_tape, expected_tape) - expected = 0.52 if not qml.operation.active_new_opmath() else 1.12 - assert qml.math.allclose(fn([[0.1, 0.2], [0.3, 0.4, 0.5]]), expected) + assert qml.math.allclose(fn([[0.1, 0.2], [0.3, 0.4, 0.5]]), 1.12) - @pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize( "H", [ @@ -446,20 +410,6 @@ def test_single_hamiltonian_non_pauli_words(self, H): for actual_tape, expected_tape in zip(tapes, expected_tapes): qml.assert_equal(actual_tape, expected_tape) - @pytest.mark.usefixtures("legacy_opmath_only") - def test_single_hamiltonian_non_pauli_words_legacy(self): - """Tests that a single Hamiltonian with non-pauli words is split correctly""" - - H = qml.Hamiltonian([1, 2, 3], [qml.X(0), qml.Hadamard(1) @ qml.Z(0), qml.Y(1)]) - tape = qml.tape.QuantumScript([], [qml.expval(H)], shots=100) - tapes, _ = split_non_commuting(tape) - expected_tapes = [ - qml.tape.QuantumScript([], [qml.expval(qml.X(0)), qml.expval(qml.Y(1))], shots=100), - qml.tape.QuantumScript([], [qml.expval(qml.Hadamard(1) @ qml.Z(0))], shots=100), - ] - for actual_tape, expected_tape in zip(tapes, expected_tapes): - qml.assert_equal(actual_tape, expected_tape) - @pytest.mark.parametrize( "grouping_strategy, expected_tapes, processing_fn, mock_results", [ @@ -498,8 +448,6 @@ def test_grouping_strategies_complex( """Tests that the tape is split correctly when containing more complex observables""" obs_list = complex_obs_list - if not qml.operation.active_new_opmath(): - obs_list = obs_list[:-1] # exclude the identity term measurements = [qml.expval(o) for o in obs_list] tape = qml.tape.QuantumScript([], measurements, shots=100) @@ -509,8 +457,6 @@ def test_grouping_strategies_complex( qml.assert_equal(actual_tape, expected_tape) expected = processing_fn(mock_results) - if not qml.operation.active_new_opmath(): - expected = expected[:-1] # exclude the identity term assert qml.math.allclose(fn(mock_results), expected) @@ -561,10 +507,6 @@ def test_tape_with_non_pauli_obs(self, non_pauli_obs): obs_list = single_term_obs_list + non_pauli_obs - if not qml.operation.active_new_opmath(): - non_pauli_obs = _convert_obs_to_legacy_opmath(non_pauli_obs) - obs_list = _convert_obs_to_legacy_opmath(obs_list) - measurements = [ qml.expval(c * o) for c, o in zip([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7], obs_list) ] @@ -665,12 +607,8 @@ def test_single_expval(self, grouping_strategy, shots, params, expected_results) coeffs, obs = [0.1, 0.2, 0.3, 0.4, 0.5], single_term_obs_list - if not qml.operation.active_new_opmath(): - obs = _convert_obs_to_legacy_opmath(obs) - - if qml.operation.active_new_opmath(): - # test constant offset with new opmath - coeffs, obs = coeffs + [0.6], obs + [qml.I()] + # test constant offset + coeffs, obs = coeffs + [0.6], obs + [qml.I()] dev = qml.device("default.qubit", wires=2, shots=shots) @@ -685,9 +623,8 @@ def circuit(angles): circuit = split_non_commuting(circuit, grouping_strategy=grouping_strategy) res = circuit(params) - if qml.operation.active_new_opmath(): - identity_results = [1] if len(np.shape(params)) == 1 else [[1, 1]] - expected_results = expected_results + identity_results + identity_results = [1] if len(np.shape(params)) == 1 else [[1, 1]] + expected_results = expected_results + identity_results expected = np.dot(coeffs, expected_results) @@ -746,8 +683,6 @@ def test_multiple_expval(self, grouping_strategy, shots, params, expected_result dev = qml.device("default.qubit", wires=2, shots=shots, seed=seed) obs_list = complex_obs_list - if not qml.operation.active_new_opmath(): - obs_list = obs_list[:-1] # exclude the identity term @qml.qnode(dev) def circuit(angles): @@ -760,9 +695,6 @@ def circuit(angles): circuit = split_non_commuting(circuit, grouping_strategy=grouping_strategy) res = circuit(params) - if not qml.operation.active_new_opmath(): - expected_results = expected_results[:-1] # exclude the identity term - if isinstance(shots, list): assert qml.math.shape(res) == (3, *np.shape(expected_results)) for i in range(3): @@ -820,8 +752,6 @@ def test_mixed_measurement_types( dev = qml.device("default.qubit", wires=2, shots=shots, seed=seed) obs_list = complex_obs_list - if not qml.operation.active_new_opmath(): - obs_list = obs_list[:-1] # exclude the identity term @qml.qnode(dev) def circuit(angles): @@ -840,9 +770,6 @@ def circuit(angles): circuit = split_non_commuting(circuit, grouping_strategy=grouping_strategy) res = circuit(params) - if not qml.operation.active_new_opmath(): - expected_results = expected_results[:-1] # exclude the identity term - if isinstance(shots, list): assert len(res) == 3 for i in range(3): @@ -900,7 +827,6 @@ def circuit(): assert dev.tracker.totals == {} assert qml.math.allclose(res, 4.0) - @pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) def test_no_obs_tape(self, grouping_strategy): """Tests tapes with only constant offsets (only measurements on Identity)""" @@ -919,7 +845,6 @@ def circuit(): assert _dev.tracker.totals == {} assert qml.math.allclose(res, 1.5) - @pytest.mark.usefixtures("new_opmath_only") @pytest.mark.parametrize("grouping_strategy", [None, "default", "qwc", "wires"]) def test_no_obs_tape_multi_measurement(self, grouping_strategy): """Tests tapes with only constant offsets (only measurements on Identity)""" @@ -996,8 +921,6 @@ def test_autograd(self, grouping_strategy): dev = qml.device("default.qubit", wires=2) obs_list = complex_obs_list - if not qml.operation.active_new_opmath(): - obs_list = obs_list[:-1] # exclude the identity term @partial(split_non_commuting, grouping_strategy=grouping_strategy) @qml.qnode(dev) @@ -1017,9 +940,6 @@ def cost(theta, phi): expected_grad_1 = expected_grad_param_0 expected_grad_2 = expected_grad_param_1 - if not qml.operation.active_new_opmath(): - expected_grad_1 = expected_grad_param_0[:-1] - expected_grad_2 = expected_grad_param_1[:-1] assert qml.math.allclose(grad1, expected_grad_1) assert qml.math.allclose(grad2, expected_grad_2) @@ -1078,8 +998,6 @@ def test_jax(self, grouping_strategy, use_jit): dev = qml.device("default.qubit", wires=2) obs_list = complex_obs_list - if not qml.operation.active_new_opmath(): - obs_list = obs_list[:-1] # exclude the identity term @partial(split_non_commuting, grouping_strategy=grouping_strategy) @qml.qnode(dev) @@ -1102,9 +1020,6 @@ def cost(theta, phi): expected_grad_1 = expected_grad_param_0 expected_grad_2 = expected_grad_param_1 - if not qml.operation.active_new_opmath(): - expected_grad_1 = expected_grad_param_0[:-1] - expected_grad_2 = expected_grad_param_1[:-1] assert qml.math.allclose(grad1, expected_grad_1) assert qml.math.allclose(grad2, expected_grad_2) @@ -1146,8 +1061,6 @@ def test_torch(self, grouping_strategy): dev = qml.device("default.qubit", wires=2) obs_list = complex_obs_list - if not qml.operation.active_new_opmath(): - obs_list = obs_list[:-1] # exclude the identity term @partial(split_non_commuting, grouping_strategy=grouping_strategy) @qml.qnode(dev) @@ -1167,9 +1080,6 @@ def cost(theta, phi): expected_grad_1 = expected_grad_param_0 expected_grad_2 = expected_grad_param_1 - if not qml.operation.active_new_opmath(): - expected_grad_1 = expected_grad_param_0[:-1] - expected_grad_2 = expected_grad_param_1[:-1] assert qml.math.allclose(grad1, expected_grad_1, atol=1e-5) assert qml.math.allclose(grad2, expected_grad_2, atol=1e-5) @@ -1206,8 +1116,6 @@ def test_tensorflow(self, grouping_strategy): dev = qml.device("default.qubit", wires=2) obs_list = complex_obs_list - if not qml.operation.active_new_opmath(): - obs_list = obs_list[:-1] # exclude the identity term @qml.qnode(dev) def circuit(theta, phi): @@ -1227,9 +1135,6 @@ def circuit(theta, phi): expected_grad_1 = expected_grad_param_0 expected_grad_2 = expected_grad_param_1 - if not qml.operation.active_new_opmath(): - expected_grad_1 = expected_grad_param_0[:-1] - expected_grad_2 = expected_grad_param_1[:-1] assert qml.math.allclose(grad1, expected_grad_1, atol=1e-5) assert qml.math.allclose(grad2, expected_grad_2, atol=1e-5) diff --git a/tests/transforms/test_split_to_single_terms.py b/tests/transforms/test_split_to_single_terms.py index 9293b69167c..e30e64985cb 100644 --- a/tests/transforms/test_split_to_single_terms.py +++ b/tests/transforms/test_split_to_single_terms.py @@ -46,26 +46,14 @@ ] -def _convert_obs_to_legacy_opmath(obs): - """Convert single-term observables to legacy opmath""" - - if isinstance(obs, qml.ops.Prod): - return qml.operation.Tensor(*obs.operands) - - if isinstance(obs, list): - return [_convert_obs_to_legacy_opmath(o) for o in obs] - - return obs - - # pylint: disable=too-few-public-methods class NoTermsDevice(qml.devices.DefaultQubit): - """A device that builds on default.qubit, but won't accept Hamiltonian, LinearCombination and Sum""" + """A device that builds on default.qubit, but won't accept LinearCombination or Sum""" def execute(self, circuits, execution_config=qml.devices.DefaultExecutionConfig): for t in circuits: for mp in t.measurements: - if mp.obs and isinstance(mp.obs, (qml.ops.Hamiltonian, qml.ops.Sum)): + if mp.obs and isinstance(mp.obs, qml.ops.Sum): raise ValueError( "no terms device does not accept observables with multiple terms" ) @@ -248,18 +236,15 @@ def test_splitting_sums_in_unsupported_mps_raises_error(self, observable): class TestIntegration: """Tests the ``split_to_single_terms`` transform performed on a QNode. In these tests, - the supported observables of ``default_qubit`` are mocked to make the device reject Sum, - Hamiltonian and LinearCombination, to ensure the transform works as intended.""" + the supported observables of ``default_qubit`` are mocked to make the device reject Sum + and LinearCombination, to ensure the transform works as intended.""" def test_splitting_sums(self): """Test that the transform takes a tape that is not executable on a device that - doesn't support Sum/Hamiltonian, and turns it into one that is""" + doesn't support Sum, and turns it into one that is""" coeffs, obs = [0.1, 0.2, 0.3, 0.4, 0.5], single_term_obs_list - if not qml.operation.active_new_opmath(): - obs = _convert_obs_to_legacy_opmath(obs) - dev = NoTermsDevice(wires=2) @qml.qnode(dev) @@ -309,14 +294,8 @@ def circuit_split(): def test_single_expval(self, shots, params, expected_results): """Tests that a QNode with a single expval measurement is executed correctly""" - coeffs, obs = [0.1, 0.2, 0.3, 0.4, 0.5], single_term_obs_list - - if not qml.operation.active_new_opmath(): - obs = _convert_obs_to_legacy_opmath(obs) - - if qml.operation.active_new_opmath(): - # test constant offset with new opmath - coeffs, obs = coeffs + [0.6], obs + [qml.I()] + coeffs = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6] + obs = single_term_obs_list + [qml.I()] # test constant offset dev = NoTermsDevice(wires=2, shots=shots) @@ -331,9 +310,8 @@ def circuit(angles): circuit = split_to_single_terms(circuit) res = circuit(params) - if qml.operation.active_new_opmath(): - identity_results = [1] if len(np.shape(params)) == 1 else [[1, 1]] - expected_results = expected_results + identity_results + identity_results = [1] if len(np.shape(params)) == 1 else [[1, 1]] + expected_results = expected_results + identity_results expected = np.dot(coeffs, expected_results) @@ -391,8 +369,6 @@ def test_multiple_expval(self, shots, params, expected_results): dev = NoTermsDevice(wires=2, shots=shots) obs_list = complex_obs_list - if not qml.operation.active_new_opmath(): - obs_list = obs_list[:-1] # exclude the identity term @qml.qnode(dev) def circuit(angles): @@ -405,9 +381,6 @@ def circuit(angles): circuit = split_to_single_terms(circuit) res = circuit(params) - if not qml.operation.active_new_opmath(): - expected_results = expected_results[:-1] # exclude the identity term - if isinstance(shots, list): assert qml.math.shape(res) == (3, *np.shape(expected_results)) for i in range(3): @@ -462,8 +435,6 @@ def test_mixed_measurement_types(self, shots, params, expected_results): dev = NoTermsDevice(wires=2, shots=shots) obs_list = complex_obs_list - if not qml.operation.active_new_opmath(): - obs_list = obs_list[:-1] # exclude the identity term @qml.qnode(dev) def circuit(angles): @@ -482,9 +453,6 @@ def circuit(angles): circuit = split_to_single_terms(circuit) res = circuit(params) - if not qml.operation.active_new_opmath(): - expected_results = expected_results[:-1] # exclude the identity term - if isinstance(shots, list): assert len(res) == 3 for i in range(3): diff --git a/tests/transforms/test_transpile.py b/tests/transforms/test_transpile.py index b02ed2c15b6..fe2f6cfe695 100644 --- a/tests/transforms/test_transpile.py +++ b/tests/transforms/test_transpile.py @@ -78,24 +78,9 @@ def circuit(): # build circuit transpiled_qfunc = transpile(circuit, coupling_map=[(0, 1), (1, 2), (2, 3)]) transpiled_qnode = qml.QNode(transpiled_qfunc, dev) - err_msg = "Measuring expectation values of tensor products, Prods, or Hamiltonians is not yet supported" - with pytest.raises(NotImplementedError, match=err_msg): - transpiled_qnode() - - @pytest.mark.usefixtures("legacy_opmath_only") - def test_transpile_raise_not_implemented_tensorproduct_mmt(self): - """test that error is raised when measurement is expectation of a Tensor product""" - dev = qml.device("default.qubit", wires=[0, 1, 2, 3]) - - def circuit(): - qml.CNOT(wires=[0, 1]) - qml.CNOT(wires=[0, 3]) - return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) - - # build circuit - transpiled_qfunc = transpile(circuit, coupling_map=[(0, 1), (1, 2), (2, 3)]) - transpiled_qnode = qml.QNode(transpiled_qfunc, dev) - err_msg = r"Measuring expectation values of tensor products, Prods, or Hamiltonians is not yet supported" + err_msg = ( + "Measuring expectation values of tensor products or Hamiltonians is not yet supported" + ) with pytest.raises(NotImplementedError, match=err_msg): transpiled_qnode() @@ -111,7 +96,9 @@ def circuit(): # build circuit transpiled_qfunc = transpile(circuit, coupling_map=[(0, 1), (1, 2), (2, 3)]) transpiled_qnode = qml.QNode(transpiled_qfunc, dev) - err_msg = r"Measuring expectation values of tensor products, Prods, or Hamiltonians is not yet supported" + err_msg = ( + r"Measuring expectation values of tensor products or Hamiltonians is not yet supported" + ) with pytest.raises(NotImplementedError, match=err_msg): transpiled_qnode() From 08dc4a10439baa3c616f06153b13e308a2b91e66 Mon Sep 17 00:00:00 2001 From: ringo-but-quantum Date: Mon, 18 Nov 2024 09:51:43 +0000 Subject: [PATCH 04/35] [no ci] bump nightly version --- pennylane/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/_version.py b/pennylane/_version.py index 7fd42772a0b..c29e6b82d80 100644 --- a/pennylane/_version.py +++ b/pennylane/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.40.0-dev14" +__version__ = "0.40.0-dev15" From 0fda6682b94a29629ca73cb7f8fe3af5a3fb0210 Mon Sep 17 00:00:00 2001 From: ringo-but-quantum Date: Mon, 18 Nov 2024 15:37:49 +0000 Subject: [PATCH 05/35] [no ci] bump nightly version --- pennylane/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/_version.py b/pennylane/_version.py index c29e6b82d80..c2c60e53969 100644 --- a/pennylane/_version.py +++ b/pennylane/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.40.0-dev15" +__version__ = "0.40.0-dev16" From 8df1025bfdae341ea31fbb08f33cc9d4e56fdd44 Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Mon, 18 Nov 2024 12:18:57 -0500 Subject: [PATCH 06/35] Add workflow for updating durations (#6519) Third attempt at creating a workflow that updates the durations files automatically. Working workflow run [here](https://github.com/PennyLaneAI/pennylane/actions/runs/11785717395) PR opened by workflow [here](https://github.com/PennyLaneAI/pennylane/pull/6569) [sc-72992] --------- Co-authored-by: Rashid N H M <95639609+rashidnhm@users.noreply.github.com> --- .github/workflows/check_in_artifact.yml | 25 +++++----- .github/workflows/tests.yml | 20 -------- .github/workflows/update-durations.yml | 63 +++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/update-durations.yml diff --git a/.github/workflows/check_in_artifact.yml b/.github/workflows/check_in_artifact.yml index 8f84adc0414..04160c9599d 100644 --- a/.github/workflows/check_in_artifact.yml +++ b/.github/workflows/check_in_artifact.yml @@ -90,6 +90,7 @@ jobs: echo "has_changes=$(git status --porcelain | wc -l | awk '{print $1}')" >> $GITHUB_OUTPUT - name: Prepare Commit Author + id: prep_commit if: steps.changed.outputs.has_changes != '0' env: HEAD_BRANCH_NAME: ${{ inputs.pull_request_head_branch_name }} @@ -104,9 +105,11 @@ jobs: if git ls-remote --exit-code origin "refs/heads/$HEAD_BRANCH_NAME"; then echo "$HEAD_BRANCH_NAME exists! Checking out..." git checkout "$HEAD_BRANCH_NAME" + echo "branch_exists='true'" >> $GITHUB_OUTPUT else echo "$HEAD_BRANCH_NAME does not exist! Creating..." git checkout -b "$HEAD_BRANCH_NAME" + echo "branch_exists='false'" >> $GITHUB_OUTPUT fi - name: Stage changes @@ -130,20 +133,20 @@ jobs: PR_BODY: ${{ inputs.pull_request_body }} run: | EXISTING_PR="$(gh pr list --state open --base master --head $HEAD_BRANCH_NAME --json 'url' --jq '.[].url' | head -n 1)" + EXISTING_CLOSED_PR="$(gh pr list --state closed --base master --head $HEAD_BRANCH_NAME --json 'mergedAt,url' --jq '.[] | select(.mergedAt == null).url' | head -n 1)" - if [ -n "${EXISTING_PR}" ]; then - echo "PR already exists ==> ${EXISTING_PR}" - exit 0 - else + if [ '${{ steps.prep_commit.outputs.branch_exists }}' = 'false' ]; then echo "Creating PR..." gh pr create --title "$PR_TITLE" --body "$PR_BODY" exit 0 - fi - - EXISTING_CLOSED_PR="$(gh pr list --state closed --base master --head $HEAD_BRANCH_NAME --json 'mergedAt,url' --jq '.[] | select(.mergedAt == null).url' | head -n 1)" - - if [ -n "${EXISTING_CLOSED_PR}" ]; then - echo "Reopening PR... ${EXISTING_CLOSED_PR}" - gh pr reopen "${EXISTING_CLOSED_PR}" + elif [ -n $EXISTING_PR ]; then + echo "Open PR already exists ==> $EXISTING_PR" + echo "Editing with provided title and body..." + gh pr edit --title "$PR_TITLE" --body "$PR_BODY" + else + echo "Reopening PR... $EXISTING_CLOSED_PR" + gh pr reopen "$EXISTING_CLOSED_PR" + echo "Editing with provided title and body..." + gh pr edit --title "$PR_TITLE" --body "$PR_BODY" exit 0 fi diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8a607060930..e0bfa8d1fe7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -54,23 +54,3 @@ jobs: ``` git commit --allow-empty -m 'trigger ci' ``` - - # TODO: This will be added separately in a future PR - # upload-durations-files: - # needs: tests - # uses: ./.github/workflows/check_in_artifact.yml - # if: github.event_name == 'push' - # with: - # artifact_name_pattern: '*-durations.json' - # artifact_save_path: '.github/durations/' - # pull_request_head_branch_name: bot/durations-update - # commit_message_description: Durations Update - # pull_request_title: Update durations files - # pull_request_body: | - # Automatic update of durations files to snapshot valid python environments. - - # Because bots are not able to trigger CI on their own, please do so by pushing an empty commit to this branch using the following command: - - # ``` - # git commit --allow-empty -m 'trigger ci' - # ``` diff --git a/.github/workflows/update-durations.yml b/.github/workflows/update-durations.yml new file mode 100644 index 00000000000..c2ddf1d3dbb --- /dev/null +++ b/.github/workflows/update-durations.yml @@ -0,0 +1,63 @@ +# This workflow is designed to run unit tests and open a pull request to update +# JSON files that store test durations for better load balancing + +name: Update Durations +on: + workflow_dispatch: + # Scheduled trigger every Saturday at 2:47am UTC + schedule: + - cron: '47 2 * * 6' + +concurrency: + group: update-durations-${{ github.ref }} + cancel-in-progress: true + +jobs: + update-durations: + uses: ./.github/workflows/interface-unit-tests.yml + secrets: + codecov_token: ${{ secrets.CODECOV_TOKEN }} + with: + branch: master + upload_to_codecov: false + run_lightened_ci: true + skip_ci_test_jobs: 'torch-tests,autograd-tests,all-interfaces-tests,external-libraries-tests,qcut-tests,qchem-tests,gradients-tests,data-tests,device-tests' + + merge-durations-files: + needs: update-durations + runs-on: ubuntu-latest + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + pattern: durations-* + path: ./ + merge-multiple: false + + - name: Merge artifacts into single file + run: | + mkdir durations + jq -s 'add' durations-core-*/*.json > durations/core_tests_durations.json + jq -s 'add' durations-jax-*/*.json > durations/jax_tests_durations.json + jq -s 'add' durations-tf-*/*.json > durations/tf_tests_durations.json + + - name: Upload combined durations artifacts + uses: actions/upload-artifact@v4 + with: + name: merged-durations + path: ./durations + include-hidden-files: true + + upload-durations: + needs: + - update-durations + - merge-durations-files + uses: ./.github/workflows/check_in_artifact.yml + with: + artifact_name_pattern: "merged-durations" + artifact_save_path: ".github/durations" + merge_multiple: true + pull_request_head_branch_name: bot/update-durations + commit_message_description: Update test durations + pull_request_title: Update test durations + pull_request_body: Automatic update of test duration files From 9a3dbdd1c9225d694dd0c9ab9f19194f8b1b6b88 Mon Sep 17 00:00:00 2001 From: Andrija Paurevic <46359773+andrijapau@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:51:20 -0500 Subject: [PATCH 07/35] Deprecate `tape` and `qtape` properties on the `QNode` (#6583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** The QNode currently has `QNode.tape` and `QNode.qtape` properties, which track the last tape generated during a construct call. Users can check these to verify what was executed most recently. Now, we’ve added `construct_tape(qnode)(*args, **kwargs)` in the workflow module as a preferred method for constructing tapes. This function creates a tape without modifying the QNode, ensuring it doesn’t interfere with other parts of the codebase. **Description of the change** - _Hopefully_ removed all calls to `QNode.tape` and `QNode.qtape` in the PennyLane source code. - Since many tests rely on these properties to access tape information, I’ve temporarily suppressed the warnings. This needs to be revisited in the backlog and gradually updated to use the preferred code [sc-78317]. Found references in, - QML: https://github.com/PennyLaneAI/qml/pull/1266 - Qiskit: https://github.com/PennyLaneAI/pennylane-qiskit/pull/602 No references in, - Lightning - Catalyst - AQT - IONQ - Qulacs - Cirq **Benefits:** Removes problems and confusion with having a mutable tape property. **Possible Drawbacks:** None [sc-76836] --------- Co-authored-by: Christina Lee Co-authored-by: Mudit Pandey --- doc/development/deprecations.rst | 8 +- doc/introduction/inspecting_circuits.rst | 3 +- doc/releases/changelog-dev.md | 4 + pennylane/circuit_graph.py | 8 +- pennylane/debugging/snapshot.py | 4 +- pennylane/drawer/draw.py | 59 +++++------ pennylane/fourier/qnode_spectrum.py | 5 +- pennylane/gradients/classical_jacobian.py | 3 +- .../gradients/parameter_shift_hessian.py | 3 +- pennylane/gradients/pulse_gradient_odegen.py | 6 +- pennylane/math/multi_dispatch.py | 5 +- pennylane/ops/functions/map_wires.py | 3 +- pennylane/ops/functions/simplify.py | 3 +- pennylane/optimize/adaptive.py | 3 +- pennylane/optimize/qnspsa.py | 23 ++--- pennylane/optimize/riemannian_gradient.py | 3 +- pennylane/optimize/shot_adaptive.py | 3 +- .../transforms/core/transform_program.py | 7 +- pennylane/workflow/qnode.py | 24 +++-- tests/circuit_graph/test_circuit_graph.py | 8 ++ .../circuit_graph/test_circuit_graph_hash.py | 9 ++ tests/circuit_graph/test_qasm.py | 9 ++ tests/devices/test_default_mixed_jax.py | 10 ++ .../core/test_adjoint_metric_tensor.py | 10 ++ tests/gradients/core/test_pulse_gradient.py | 8 ++ tests/gradients/core/test_pulse_odegen.py | 9 ++ .../test_parameter_shift_hessian.py | 8 ++ tests/interfaces/test_autograd_qnode.py | 10 ++ tests/interfaces/test_jax_jit_qnode.py | 9 ++ tests/interfaces/test_jax_qnode.py | 8 ++ tests/interfaces/test_tensorflow_qnode.py | 10 ++ tests/interfaces/test_torch_qnode.py | 10 ++ tests/measurements/test_classical_shadow.py | 9 ++ tests/measurements/test_state.py | 9 ++ tests/numpy/test_numpy_wrapper.py | 9 ++ tests/ops/functions/test_map_wires.py | 9 ++ tests/ops/functions/test_simplify.py | 9 ++ tests/ops/op_math/test_condition.py | 10 ++ tests/ops/op_math/test_exp.py | 8 ++ tests/ops/op_math/test_linear_combination.py | 10 ++ tests/ops/op_math/test_prod.py | 10 ++ tests/ops/op_math/test_sum.py | 10 ++ tests/ops/test_meta.py | 9 ++ tests/optimize/test_adaptive.py | 9 ++ tests/qnn/test_keras.py | 9 ++ tests/qnn/test_qnn_torch.py | 9 ++ tests/resource/test_error/test_error.py | 9 ++ tests/shadow/test_shadow_transforms.py | 9 ++ tests/tape/test_tape.py | 8 ++ tests/test_compiler.py | 10 ++ tests/test_observable.py | 11 +++ tests/test_qnode.py | 95 +++++++++++------- tests/test_qnode_legacy.py | 98 ++++++++++++------- tests/test_queuing.py | 8 ++ tests/test_tracker.py | 9 ++ tests/transforms/test_cliffordt_transform.py | 9 ++ tests/transforms/test_compile.py | 8 ++ tests/transforms/test_defer_measurements.py | 8 ++ .../test_optimization/test_cancel_inverses.py | 9 ++ .../test_commute_controlled.py | 9 ++ .../test_merge_amplitude_embedding.py | 9 ++ .../test_optimization/test_merge_rotations.py | 9 ++ .../test_pattern_matching.py | 9 ++ .../test_single_qubit_fusion.py | 9 ++ .../test_optimization/test_undo_swaps.py | 9 ++ tests/transforms/test_qcut.py | 9 ++ tests/transforms/test_transpile.py | 8 ++ tests/transforms/test_unitary_to_rot.py | 9 ++ 68 files changed, 640 insertions(+), 155 deletions(-) diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst index 9fbcb25cb34..d38eb129e90 100644 --- a/doc/development/deprecations.rst +++ b/doc/development/deprecations.rst @@ -9,6 +9,12 @@ deprecations are listed below. Pending deprecations -------------------- +* The ``tape`` and ``qtape`` properties of ``QNode`` have been deprecated. + Instead, use the ``qml.workflow.construct_tape`` function. + + - Deprecated in v0.40 + - Will be removed in v0.41 + * The ``max_expansion`` argument in :func:`~pennylane.devices.preprocess.decompose` is deprecated. - Deprecated in v0.40 @@ -25,7 +31,7 @@ Pending deprecations - Will be removed in v0.41 * The ``QNode.get_best_method`` and ``QNode.best_method_str`` methods have been deprecated. - Instead, use the ``qml.workflow.get_best_diff_method``. + Instead, use the ``qml.workflow.get_best_diff_method`` function. - Deprecated in v0.40 - Will be removed in v0.41 diff --git a/doc/introduction/inspecting_circuits.rst b/doc/introduction/inspecting_circuits.rst index 8aabe88f0e8..4934a423e6b 100644 --- a/doc/introduction/inspecting_circuits.rst +++ b/doc/introduction/inspecting_circuits.rst @@ -279,6 +279,7 @@ or to check whether two gates causally influence each other. import pennylane as qml from pennylane import CircuitGraph + from pennylane.workflow import construct_tape dev = qml.device('lightning.qubit', wires=(0,1,2,3)) @@ -292,7 +293,7 @@ or to check whether two gates causally influence each other. circuit() - tape = circuit.qtape + tape = construct_tape(circuit)() ops = tape.operations obs = tape.observables g = CircuitGraph(ops, obs, tape.wires) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 3f1332c3261..6fa4c92cb57 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -127,6 +127,10 @@ following 4 sets of functions have been either moved or removed[(#6588)](https:/

Deprecations 👋

+* The `tape` and `qtape` properties of `QNode` have been deprecated. + Instead, use the `qml.workflow.construct_tape` function. + [(#6583)](https://github.com/PennyLaneAI/pennylane/pull/6583) + * The `max_expansion` argument in `qml.devices.preprocess.decompose` is deprecated and will be removed in v0.41. [(#6400)](https://github.com/PennyLaneAI/pennylane/pull/6400) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index 7e8365eaecb..5e405dcb48f 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -516,14 +516,14 @@ def max_simultaneous_measurements(self): >>> def circuit_measure_max_once(): ... return qml.expval(qml.X(0)) >>> qnode = qml.QNode(circuit_measure_max_once, dev) - >>> qnode() - >>> qnode.qtape.graph.max_simultaneous_measurements + >>> tape = qml.workflow.construct_tape(qnode)() + >>> tape.graph.max_simultaneous_measurements 1 >>> def circuit_measure_max_twice(): ... return qml.expval(qml.X(0)), qml.probs(wires=0) >>> qnode = qml.QNode(circuit_measure_max_twice, dev) - >>> qnode() - >>> qnode.qtape.graph.max_simultaneous_measurements + >>> tape = qml.workflow.construct_tape(qnode)() + >>> tape.graph.max_simultaneous_measurements 2 Returns: diff --git a/pennylane/debugging/snapshot.py b/pennylane/debugging/snapshot.py index 8ca58a47a34..0eb82ae63b2 100644 --- a/pennylane/debugging/snapshot.py +++ b/pennylane/debugging/snapshot.py @@ -219,8 +219,8 @@ def snapshots_qnode(self, qnode, targs, tkwargs): def get_snapshots(*args, **kwargs): # Need to construct to generate the tape and be able to validate - qnode.construct(args, kwargs) - qml.devices.preprocess.validate_measurements(qnode.tape) + tape = qml.workflow.construct_tape(qnode)(*args, **kwargs) + qml.devices.preprocess.validate_measurements(tape) old_interface = qnode.interface if old_interface == "auto": diff --git a/pennylane/drawer/draw.py b/pennylane/drawer/draw.py index 2a3f1202560..6276c04b2ca 100644 --- a/pennylane/drawer/draw.py +++ b/pennylane/drawer/draw.py @@ -314,41 +314,30 @@ def wrapper(*args, **kwargs): except TypeError: _wire_order = tapes[0].wires - if tapes is not None: - cache = {"tape_offset": 0, "matrices": []} - res = [ - tape_text( - t, - wire_order=_wire_order, - show_all_wires=show_all_wires, - decimals=decimals, - show_matrices=False, - show_wire_labels=show_wire_labels, - max_length=max_length, - cache=cache, - ) - for t in tapes - ] - if show_matrices and cache["matrices"]: - mat_str = "" - for i, mat in enumerate(cache["matrices"]): - if qml.math.requires_grad(mat) and hasattr(mat, "detach"): - mat = mat.detach() - mat_str += f"\nM{i} = \n{mat}" - if mat_str: - mat_str = "\n" + mat_str - return "\n\n".join(res) + mat_str - return "\n\n".join(res) - - return tape_text( - qnode.qtape, - wire_order=_wire_order, - show_all_wires=show_all_wires, - decimals=decimals, - show_matrices=show_matrices, - show_wire_labels=show_wire_labels, - max_length=max_length, - ) + cache = {"tape_offset": 0, "matrices": []} + res = [ + tape_text( + t, + wire_order=_wire_order, + show_all_wires=show_all_wires, + decimals=decimals, + show_matrices=False, + show_wire_labels=show_wire_labels, + max_length=max_length, + cache=cache, + ) + for t in tapes + ] + if show_matrices and cache["matrices"]: + mat_str = "" + for i, mat in enumerate(cache["matrices"]): + if qml.math.requires_grad(mat) and hasattr(mat, "detach"): + mat = mat.detach() + mat_str += f"\nM{i} = \n{mat}" + if mat_str: + mat_str = "\n" + mat_str + return "\n\n".join(res) + mat_str + return "\n\n".join(res) return wrapper diff --git a/pennylane/fourier/qnode_spectrum.py b/pennylane/fourier/qnode_spectrum.py index 366c9b7f16c..3e050fd4986 100644 --- a/pennylane/fourier/qnode_spectrum.py +++ b/pennylane/fourier/qnode_spectrum.py @@ -400,7 +400,8 @@ def wrapper(*args, **kwargs): ) # After construction, check whether invalid operations (for a spectrum) # are present in the QNode - for m in qnode.qtape.measurements: + tape = qml.workflow.construct_tape(qnode)(*args, **kwargs) + for m in tape.measurements: if not isinstance(m, (qml.measurements.ExpectationMP, qml.measurements.ProbabilityMP)): raise ValueError( f"The measurement {m.__class__.__name__} is not supported as it likely does " @@ -408,7 +409,7 @@ def wrapper(*args, **kwargs): ) cjacs = jac_fn(*args, **kwargs) spectra = {} - tape = qml.transforms.expand_multipar(qnode.qtape) + tape = qml.transforms.expand_multipar(tape) par_info = tape.par_info # Iterate over jacobians per argument diff --git a/pennylane/gradients/classical_jacobian.py b/pennylane/gradients/classical_jacobian.py index b0effa053b5..0434bf5725b 100644 --- a/pennylane/gradients/classical_jacobian.py +++ b/pennylane/gradients/classical_jacobian.py @@ -141,8 +141,7 @@ def classical_preprocessing(*args, **kwargs): """Returns the trainable gate parameters for a given QNode input.""" kwargs.pop("shots", None) kwargs.pop("argnums", None) - qnode.construct(args, kwargs) - tape = qnode.qtape + tape = qml.workflow.construct_tape(qnode)(*args, **kwargs) if expand_fn is not None: tape = expand_fn(tape) diff --git a/pennylane/gradients/parameter_shift_hessian.py b/pennylane/gradients/parameter_shift_hessian.py index 704fa6c8919..8e2d130ac82 100644 --- a/pennylane/gradients/parameter_shift_hessian.py +++ b/pennylane/gradients/parameter_shift_hessian.py @@ -521,8 +521,7 @@ def param_shift_hessian( the parameter-shifted tapes and a post-processing function to combine the execution results of these tapes into the Hessian: - >>> circuit(x) # generate the QuantumTape inside the QNode - >>> tape = circuit.qtape + >>> tape = qml.workflow.construct_tape(circuit)(x) >>> hessian_tapes, postproc_fn = qml.gradients.param_shift_hessian(tape) >>> len(hessian_tapes) 13 diff --git a/pennylane/gradients/pulse_gradient_odegen.py b/pennylane/gradients/pulse_gradient_odegen.py index 2cd2ead6537..db7300741cb 100644 --- a/pennylane/gradients/pulse_gradient_odegen.py +++ b/pennylane/gradients/pulse_gradient_odegen.py @@ -504,9 +504,9 @@ def circuit(params): Alternatively, we may apply the transform to the tape of the pulse program, obtaining the tapes with inserted ``PauliRot`` gates together with the post-processing function: - >>> circuit.construct((params,), {}) # Build the tape of the circuit. - >>> circuit.tape.trainable_params = [0, 1, 2] - >>> tapes, fun = qml.gradients.pulse_odegen(circuit.tape, argnum=[0, 1, 2]) + >>> tape = qml.workflow.construct_tape(circuit)(params) # Build the tape of the circuit. + >>> tape.trainable_params = [0, 1, 2] + >>> tapes, fun = qml.gradients.pulse_odegen(tape, argnum=[0, 1, 2]) >>> len(tapes) 12 diff --git a/pennylane/math/multi_dispatch.py b/pennylane/math/multi_dispatch.py index dfad58d5564..69923d3a73b 100644 --- a/pennylane/math/multi_dispatch.py +++ b/pennylane/math/multi_dispatch.py @@ -23,6 +23,8 @@ from autoray import numpy as np from numpy import ndarray +import pennylane as qml + from . import single_dispatch # pylint:disable=unused-import from .utils import cast, cast_like, get_interface, requires_grad @@ -1032,8 +1034,7 @@ def jax_argnums_to_tape_trainable(qnode, argnums, program, args, kwargs): for i, arg in enumerate(args) ] - qnode.construct(args_jvp, kwargs) - tape = qnode.qtape + tape = qml.workflow.construct_tape(qnode, level=0)(*args_jvp, **kwargs) tapes, _ = program((tape,)) del trace return tuple(tape.get_parameters(trainable_only=False) for tape in tapes) diff --git a/pennylane/ops/functions/map_wires.py b/pennylane/ops/functions/map_wires.py index 9266e91528d..91df391d29e 100644 --- a/pennylane/ops/functions/map_wires.py +++ b/pennylane/ops/functions/map_wires.py @@ -111,7 +111,8 @@ def map_wires( >>> mapped_circuit = qml.map_wires(circuit, wire_map) >>> mapped_circuit() tensor([0.92885434, 0.07114566], requires_grad=True) - >>> list(mapped_circuit.tape) + >>> tape = qml.workflow.construct_tape(mapped_circuit)() + >>> list(tape) [((RX(0.54, wires=[3]) @ X(2)) @ Z(1)) @ RY(1.23, wires=[0]), probs(wires=[3])] """ if isinstance(input, (Operator, MeasurementProcess)): diff --git a/pennylane/ops/functions/simplify.py b/pennylane/ops/functions/simplify.py index afe8ac61c3f..339bd468325 100644 --- a/pennylane/ops/functions/simplify.py +++ b/pennylane/ops/functions/simplify.py @@ -81,7 +81,8 @@ def simplify(input: Union[Operator, MeasurementProcess, QuantumScript, QNode, Ca ... return qml.probs(wires=0) >>> circuit() tensor([0.64596329, 0.35403671], requires_grad=True) - >>> list(circuit.tape) + >>> tape = qml.workflow.construct_tape(circuit)() + >>> list(tape) [RZ(11.566370614359172, wires=[0]) @ RY(11.566370614359172, wires=[0]) @ RX(11.566370614359172, wires=[0]), probs(wires=[0])] """ diff --git a/pennylane/optimize/adaptive.py b/pennylane/optimize/adaptive.py index c9c5b3e9179..2d18cdd8e8d 100644 --- a/pennylane/optimize/adaptive.py +++ b/pennylane/optimize/adaptive.py @@ -199,6 +199,7 @@ def step_and_cost(self, circuit, operator_pool, drain_pool=False, params_zero=Tr """ cost = circuit() qnode = copy.copy(circuit) + tape = qml.workflow.construct_tape(qnode)() if drain_pool: operator_pool = [ @@ -206,7 +207,7 @@ def step_and_cost(self, circuit, operator_pool, drain_pool=False, params_zero=Tr for gate in operator_pool if all( gate.name != operation.name or gate.wires != operation.wires - for operation in circuit.tape.operations + for operation in tape.operations ) ] diff --git a/pennylane/optimize/qnspsa.py b/pennylane/optimize/qnspsa.py index 2caad34ded3..d0f3502f3c2 100644 --- a/pennylane/optimize/qnspsa.py +++ b/pennylane/optimize/qnspsa.py @@ -369,10 +369,10 @@ def _get_spsa_grad_tapes(self, cost, args, kwargs): args_plus[index] = arg + self.finite_diff_step * direction args_minus[index] = arg - self.finite_diff_step * direction - cost.construct(args_plus, kwargs) - tape_plus = cost.tape.copy(copy_operations=True) - cost.construct(args_minus, kwargs) - tape_minus = cost.tape.copy(copy_operations=True) + tape = qml.workflow.construct_tape(cost)(*args_plus, **kwargs) + tape_plus = tape.copy(copy_operations=True) + tape = qml.workflow.construct_tape(cost)(*args_minus, **kwargs) + tape_minus = tape.copy(copy_operations=True) return [tape_plus, tape_minus], dirs def _update_tensor(self, tensor_raw): @@ -425,22 +425,23 @@ def _get_overlap_tape(self, cost, args1, args2, kwargs): op_inv = self._get_operations(cost, args2, kwargs) new_ops = op_forward + [qml.adjoint(op) for op in reversed(op_inv)] - return qml.tape.QuantumScript(new_ops, [qml.probs(wires=cost.tape.wires.labels)]) + tape = qml.workflow.construct_tape(cost)(*args1, **kwargs) + return qml.tape.QuantumScript(new_ops, [qml.probs(wires=tape.wires.labels)]) @staticmethod def _get_operations(cost, args, kwargs): - cost.construct(args, kwargs) - return cost.tape.operations + tape = qml.workflow.construct_tape(cost)(*args, **kwargs) + return tape.operations def _apply_blocking(self, cost, args, kwargs, params_next): - cost.construct(args, kwargs) - tape_loss_curr = cost.tape.copy(copy_operations=True) + tape = qml.workflow.construct_tape(cost)(*args, **kwargs) + tape_loss_curr = tape.copy(copy_operations=True) if not isinstance(params_next, list): params_next = [params_next] - cost.construct(params_next, kwargs) - tape_loss_next = cost.tape.copy(copy_operations=True) + tape = qml.workflow.construct_tape(cost)(*params_next, **kwargs) + tape_loss_next = tape.copy(copy_operations=True) program, _ = cost.device.preprocess() diff --git a/pennylane/optimize/riemannian_gradient.py b/pennylane/optimize/riemannian_gradient.py index f60c55678e8..e7cc7c720a0 100644 --- a/pennylane/optimize/riemannian_gradient.py +++ b/pennylane/optimize/riemannian_gradient.py @@ -381,8 +381,9 @@ def get_omegas(self): obs_groupings, _ = qml.pauli.group_observables(self.observables, self.coeffs) # get all circuits we need to calculate the coefficients + tape = qml.workflow.construct_tape(self.circuit)() circuits = algebra_commutator( - self.circuit.qtape, + tape, obs_groupings, self.lie_algebra_basis_names, self.nqubits, diff --git a/pennylane/optimize/shot_adaptive.py b/pennylane/optimize/shot_adaptive.py index 6fc69b01063..ed8657184a1 100644 --- a/pennylane/optimize/shot_adaptive.py +++ b/pennylane/optimize/shot_adaptive.py @@ -308,8 +308,7 @@ def _single_shot_qnode_gradients(self, qnode, args, kwargs): """Compute the single shot gradients of a QNode.""" self.check_device(qnode.device) - qnode.construct(args, kwargs) - tape = qnode.tape + tape = qml.workflow.construct_tape(qnode)(*args, **kwargs) [expval] = tape.measurements coeffs, observables = ( expval.obs.terms() diff --git a/pennylane/transforms/core/transform_program.py b/pennylane/transforms/core/transform_program.py index 548ec1fa808..6dc407dc251 100644 --- a/pennylane/transforms/core/transform_program.py +++ b/pennylane/transforms/core/transform_program.py @@ -386,8 +386,7 @@ def classical_preprocessing(program, *args, **kwargs): """Returns the trainable gate parameters for a given QNode input.""" kwargs.pop("shots", None) kwargs.pop("argnums", None) - qnode.construct(args, kwargs) - tape = qnode.qtape + tape = qml.workflow.construct_tape(qnode, level=0)(*args, **kwargs) tapes, _ = program((tape,)) res = tuple(qml.math.stack(tape.get_parameters(trainable_only=True)) for tape in tapes) if len(tapes) == 1: @@ -456,8 +455,8 @@ def _jacobian(*args, **kwargs): classical_jacobian = jacobian( classical_preprocessing, sub_program, argnums, *args, **kwargs ) - qnode.construct(args, kwargs) - tapes, _ = sub_program((qnode.tape,)) + tape = qml.workflow.construct_tape(qnode, level=0)(*args, **kwargs) + tapes, _ = sub_program((tape,)) multi_tapes = len(tapes) > 1 if not multi_tapes: classical_jacobian = [classical_jacobian] diff --git a/pennylane/workflow/qnode.py b/pennylane/workflow/qnode.py index 1e6a28e08db..9e921a14428 100644 --- a/pennylane/workflow/qnode.py +++ b/pennylane/workflow/qnode.py @@ -835,7 +835,19 @@ def best_method_str(device: SupportedDeviceAPIs, interface: SupportedInterfaceUs @property def tape(self) -> QuantumTape: - """The quantum tape""" + """The quantum tape + + .. warning:: + + This property is deprecated in v0.40 and will be removed in v0.41. + Instead, use the :func:`qml.workflow.construct_tape <.workflow.construct_tape>` function. + """ + + warnings.warn( + "The tape/qtape property is deprecated and will be removed in v0.41. " + "Instead, use the qml.workflow.get_best_diff_method function.", + qml.PennyLaneDeprecationWarning, + ) return self._tape qtape = tape # for backwards compatibility @@ -859,10 +871,10 @@ def construct(self, args, kwargs): # pylint: disable=too-many-branches self._tape = QuantumScript.from_queue(q, shots) - params = self.tape.get_parameters(trainable_only=False) - self.tape.trainable_params = qml.math.get_trainable_indices(params) + params = self._tape.get_parameters(trainable_only=False) + self._tape.trainable_params = qml.math.get_trainable_indices(params) - _validate_qfunc_output(self._qfunc_output, self.tape.measurements) + _validate_qfunc_output(self._qfunc_output, self._tape.measurements) def _execution_component(self, args: tuple, kwargs: dict) -> qml.typing.Result: """Construct the transform program and execute the tapes. Helper function for ``__call__`` @@ -883,7 +895,7 @@ def _execution_component(self, args: tuple, kwargs: dict) -> qml.typing.Result: gradient_fn = qml.gradients.param_shift else: gradient_fn = QNode.get_gradient_fn( - self.device, self.interface, self.diff_method, tape=self.tape + self.device, self.interface, self.diff_method, tape=self._tape )[0] execute_kwargs = copy.copy(self.execute_kwargs) @@ -949,7 +961,7 @@ def _execution_component(self, args: tuple, kwargs: dict) -> qml.typing.Result: # convert result to the interface in case the qfunc has no parameters if ( - len(self.tape.get_parameters(trainable_only=False)) == 0 + len(self._tape.get_parameters(trainable_only=False)) == 0 and not self.transform_program.is_informative ): res = _convert_to_interface(res, self.interface) diff --git a/tests/circuit_graph/test_circuit_graph.py b/tests/circuit_graph/test_circuit_graph.py index b97435144fa..4bba3b2933c 100644 --- a/tests/circuit_graph/test_circuit_graph.py +++ b/tests/circuit_graph/test_circuit_graph.py @@ -18,6 +18,7 @@ import contextlib import io +import warnings import numpy as np import pytest @@ -29,6 +30,13 @@ from pennylane.wires import Wires +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + @pytest.fixture(name="ops") def ops_fixture(): """A fixture of a complex example of operations that depend on previous operations.""" diff --git a/tests/circuit_graph/test_circuit_graph_hash.py b/tests/circuit_graph/test_circuit_graph_hash.py index 9622b81af97..fc06c899795 100644 --- a/tests/circuit_graph/test_circuit_graph_hash.py +++ b/tests/circuit_graph/test_circuit_graph_hash.py @@ -14,6 +14,8 @@ """ Unit and integration tests for creating the :mod:`pennylane` :attr:`QNode.qtape.graph.hash` attribute. """ +import warnings + import numpy as np import pytest @@ -22,6 +24,13 @@ from pennylane.wires import Wires +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class TestCircuitGraphHash: """Test the creation of a hash on a CircuitGraph""" diff --git a/tests/circuit_graph/test_qasm.py b/tests/circuit_graph/test_qasm.py index 11787ee1c98..05775d4e626 100644 --- a/tests/circuit_graph/test_qasm.py +++ b/tests/circuit_graph/test_qasm.py @@ -14,6 +14,8 @@ """ Unit tests for the :mod:`pennylane.circuit_graph.to_openqasm()` method. """ +import warnings + # pylint: disable=no-self-use,too-many-arguments,protected-access from textwrap import dedent @@ -24,6 +26,13 @@ from pennylane.wires import Wires +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class TestToQasmUnitTests: """Unit tests for the to_openqasm() method""" diff --git a/tests/devices/test_default_mixed_jax.py b/tests/devices/test_default_mixed_jax.py index 3b4f63ffc25..4e459cabe61 100644 --- a/tests/devices/test_default_mixed_jax.py +++ b/tests/devices/test_default_mixed_jax.py @@ -14,6 +14,8 @@ """ Tests for the ``default.mixed`` device for the JAX interface """ +import warnings + # pylint: disable=protected-access from functools import partial @@ -24,6 +26,14 @@ from pennylane import numpy as pnp from pennylane.devices.default_mixed import DefaultMixed + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + pytestmark = pytest.mark.jax jax = pytest.importorskip("jax") diff --git a/tests/gradients/core/test_adjoint_metric_tensor.py b/tests/gradients/core/test_adjoint_metric_tensor.py index 76b50567a33..38a09d00ee0 100644 --- a/tests/gradients/core/test_adjoint_metric_tensor.py +++ b/tests/gradients/core/test_adjoint_metric_tensor.py @@ -14,6 +14,8 @@ """ Unit tests for the adjoint_metric_tensor function. """ +import warnings + import numpy as onp # pylint: disable=protected-access @@ -22,6 +24,14 @@ import pennylane as qml from pennylane import numpy as np + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + fixed_pars = [-0.2, 0.2, 0.5, 0.3, 0.7] diff --git a/tests/gradients/core/test_pulse_gradient.py b/tests/gradients/core/test_pulse_gradient.py index f8f09bf0a45..1a52a02e66c 100644 --- a/tests/gradients/core/test_pulse_gradient.py +++ b/tests/gradients/core/test_pulse_gradient.py @@ -16,6 +16,7 @@ """ import copy +import warnings import numpy as np import pytest @@ -30,6 +31,13 @@ ) +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + # pylint: disable=too-few-public-methods @pytest.mark.jax class TestSplitEvolOps: diff --git a/tests/gradients/core/test_pulse_odegen.py b/tests/gradients/core/test_pulse_odegen.py index dd8d50c4887..128e6f76426 100644 --- a/tests/gradients/core/test_pulse_odegen.py +++ b/tests/gradients/core/test_pulse_odegen.py @@ -17,6 +17,7 @@ # pylint:disable=import-outside-toplevel, use-implicit-booleaness-not-comparison import copy +import warnings import numpy as np import pytest @@ -35,6 +36,14 @@ from pennylane.math import expand_matrix from pennylane.ops.qubit.special_unitary import pauli_basis_matrices, pauli_basis_strings + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + X, Y, Z = qml.PauliX, qml.PauliY, qml.PauliZ diff --git a/tests/gradients/parameter_shift/test_parameter_shift_hessian.py b/tests/gradients/parameter_shift/test_parameter_shift_hessian.py index 08f39601036..dce7831a050 100644 --- a/tests/gradients/parameter_shift/test_parameter_shift_hessian.py +++ b/tests/gradients/parameter_shift/test_parameter_shift_hessian.py @@ -13,6 +13,7 @@ # limitations under the License. """Tests for the gradients.param_shift_hessian module.""" +import warnings from itertools import product import pytest @@ -26,6 +27,13 @@ ) +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class TestProcessArgnum: """Tests for the helper method _process_argnum.""" diff --git a/tests/interfaces/test_autograd_qnode.py b/tests/interfaces/test_autograd_qnode.py index 8186707f636..573d8d13999 100644 --- a/tests/interfaces/test_autograd_qnode.py +++ b/tests/interfaces/test_autograd_qnode.py @@ -14,6 +14,8 @@ """Integration tests for using the autograd interface with a QNode""" # pylint: disable=no-member, too-many-arguments, unexpected-keyword-arg, use-dict-literal, no-name-in-module +import warnings + import autograd import autograd.numpy as anp import pytest @@ -24,6 +26,14 @@ from pennylane import qnode from pennylane.devices import DefaultQubit + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + # dev, diff_method, grad_on_execution, device_vjp qubit_device_and_diff_method = [ [qml.device("default.qubit"), "finite-diff", False, False], diff --git a/tests/interfaces/test_jax_jit_qnode.py b/tests/interfaces/test_jax_jit_qnode.py index f85720a86f1..ed3af0b330d 100644 --- a/tests/interfaces/test_jax_jit_qnode.py +++ b/tests/interfaces/test_jax_jit_qnode.py @@ -13,6 +13,8 @@ # limitations under the License. """Integration tests for using the JAX-JIT interface with a QNode""" +import warnings + # pylint: disable=too-many-arguments,too-few-public-methods,protected-access from functools import partial @@ -25,6 +27,13 @@ from pennylane.devices import DefaultQubit +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + def get_device(device_name, wires, seed): if device_name == "param_shift.qubit": return ParamShiftDerivativesDevice(seed=seed) diff --git a/tests/interfaces/test_jax_qnode.py b/tests/interfaces/test_jax_qnode.py index 5b21dbee1e9..6fe0e2dc16b 100644 --- a/tests/interfaces/test_jax_qnode.py +++ b/tests/interfaces/test_jax_qnode.py @@ -14,6 +14,7 @@ """Integration tests for using the JAX-Python interface with a QNode""" # pylint: disable=no-member, too-many-arguments, unexpected-keyword-arg, use-implicit-booleaness-not-comparison +import warnings from itertools import product import numpy as np @@ -24,6 +25,13 @@ from pennylane.devices import DefaultQubit +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + def get_device(device_name, wires, seed): if device_name == "lightning.qubit": return qml.device("lightning.qubit", wires=wires) diff --git a/tests/interfaces/test_tensorflow_qnode.py b/tests/interfaces/test_tensorflow_qnode.py index 05fd3b0f71a..e8441f7f61b 100644 --- a/tests/interfaces/test_tensorflow_qnode.py +++ b/tests/interfaces/test_tensorflow_qnode.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """Integration tests for using the TensorFlow interface with a QNode""" +import warnings + import numpy as np # pylint: disable=too-many-arguments,too-few-public-methods,comparison-with-callable, use-implicit-booleaness-not-comparison @@ -21,6 +23,14 @@ from pennylane import qnode from pennylane.devices import DefaultQubit + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + pytestmark = pytest.mark.tf tf = pytest.importorskip("tensorflow") diff --git a/tests/interfaces/test_torch_qnode.py b/tests/interfaces/test_torch_qnode.py index 059fdfd795e..fe33abedc7d 100644 --- a/tests/interfaces/test_torch_qnode.py +++ b/tests/interfaces/test_torch_qnode.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """Integration tests for using the Torch interface with a QNode""" +import warnings + # pylint: disable=too-many-arguments,unexpected-keyword-arg,no-member,comparison-with-callable, no-name-in-module # pylint: disable=use-implicit-booleaness-not-comparison, unnecessary-lambda-assignment, use-dict-literal import numpy as np @@ -22,6 +24,14 @@ from pennylane import qnode from pennylane.devices import DefaultQubit + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + pytestmark = pytest.mark.torch torch = pytest.importorskip("torch", minversion="1.3") diff --git a/tests/measurements/test_classical_shadow.py b/tests/measurements/test_classical_shadow.py index 5290224c490..90b2b76e2f8 100644 --- a/tests/measurements/test_classical_shadow.py +++ b/tests/measurements/test_classical_shadow.py @@ -14,6 +14,7 @@ """Unit tests for the classical shadows measurement processes""" import copy +import warnings import autograd.numpy import pytest @@ -23,6 +24,14 @@ from pennylane.measurements import ClassicalShadowMP from pennylane.measurements.classical_shadow import ShadowExpvalMP + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + # pylint: disable=dangerous-default-value, too-many-arguments diff --git a/tests/measurements/test_state.py b/tests/measurements/test_state.py index 78c8d17fb8c..30eccda6c32 100644 --- a/tests/measurements/test_state.py +++ b/tests/measurements/test_state.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """Unit tests for the state module""" +import warnings + import numpy as np import pytest @@ -24,6 +26,13 @@ from pennylane.wires import WireError, Wires +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class TestStateMP: """Tests for the State measurement process.""" diff --git a/tests/numpy/test_numpy_wrapper.py b/tests/numpy/test_numpy_wrapper.py index f89bc715b6e..8dbc68f320c 100644 --- a/tests/numpy/test_numpy_wrapper.py +++ b/tests/numpy/test_numpy_wrapper.py @@ -16,6 +16,8 @@ modifies Autograd NumPy arrays so that they have an additional property, ``requires_grad``, that marks them as trainable/non-trainable. """ +import warnings + import numpy as onp import pytest from autograd.numpy.numpy_boxes import ArrayBox @@ -25,6 +27,13 @@ from pennylane.numpy.tensor import tensor_to_arraybox +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + @pytest.mark.unit class TestExtractTensors: """Tests for the extract_tensors function""" diff --git a/tests/ops/functions/test_map_wires.py b/tests/ops/functions/test_map_wires.py index 0e7c4b03dad..b4a1b5e78d5 100644 --- a/tests/ops/functions/test_map_wires.py +++ b/tests/ops/functions/test_map_wires.py @@ -14,6 +14,8 @@ """ Unit tests for the qml.map_wires function """ +import warnings + # pylint: disable=too-few-public-methods from functools import partial @@ -25,6 +27,13 @@ from pennylane.wires import Wires +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + def build_op(): """Return function to build nested operator.""" diff --git a/tests/ops/functions/test_simplify.py b/tests/ops/functions/test_simplify.py index 74ee180035c..d555ba5439e 100644 --- a/tests/ops/functions/test_simplify.py +++ b/tests/ops/functions/test_simplify.py @@ -14,6 +14,8 @@ """ Unit tests for the qml.simplify function """ +import warnings + # pylint: disable=too-few-public-methods import pytest @@ -22,6 +24,13 @@ from pennylane.tape import QuantumScript +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + def build_op(): """Return function to build nested operator.""" diff --git a/tests/ops/op_math/test_condition.py b/tests/ops/op_math/test_condition.py index f5b9426d276..7476adec3cb 100644 --- a/tests/ops/op_math/test_condition.py +++ b/tests/ops/op_math/test_condition.py @@ -23,6 +23,8 @@ files. """ +import warnings + import numpy as np import pytest @@ -30,6 +32,14 @@ from pennylane.operation import Operator from pennylane.ops.op_math.condition import Conditional, ConditionalTransformError + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + terminal_meas = [ qml.probs(wires=[1, 0]), qml.expval(qml.PauliZ(0)), diff --git a/tests/ops/op_math/test_exp.py b/tests/ops/op_math/test_exp.py index e73ec05d703..4b9e52cde50 100644 --- a/tests/ops/op_math/test_exp.py +++ b/tests/ops/op_math/test_exp.py @@ -14,6 +14,7 @@ """Unit tests for the ``Exp`` class""" import copy import re +import warnings import pytest @@ -29,6 +30,13 @@ from pennylane.ops.op_math import Evolution, Exp +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + @pytest.mark.parametrize("constructor", (qml.exp, Exp)) class TestInitialization: """Test the initialization process and standard properties.""" diff --git a/tests/ops/op_math/test_linear_combination.py b/tests/ops/op_math/test_linear_combination.py index 728ec636966..a252a68ce4f 100644 --- a/tests/ops/op_math/test_linear_combination.py +++ b/tests/ops/op_math/test_linear_combination.py @@ -14,6 +14,8 @@ """ Tests for the LinearCombination class. """ +import warnings + # pylint: disable=too-many-public-methods, too-few-public-methods from collections.abc import Iterable from copy import copy @@ -29,6 +31,14 @@ from pennylane.pauli import PauliSentence, PauliWord from pennylane.wires import Wires + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + # Make test data in different interfaces, if installed COEFFS_PARAM_INTERFACE = [ ([-0.05, 0.17], 1.7, "autograd"), diff --git a/tests/ops/op_math/test_prod.py b/tests/ops/op_math/test_prod.py index d4e1e15d9ca..5e4e696497a 100644 --- a/tests/ops/op_math/test_prod.py +++ b/tests/ops/op_math/test_prod.py @@ -14,6 +14,8 @@ """ Unit tests for the Prod arithmetic class of qubit operations """ +import warnings + # pylint:disable=protected-access, unused-argument import gate_data as gd # a file containing matrix rep of each gate import numpy as np @@ -26,6 +28,14 @@ from pennylane.ops.op_math.prod import Prod, _swappable_ops, prod from pennylane.wires import Wires + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + X, Y, Z = qml.PauliX, qml.PauliY, qml.PauliZ no_mat_ops = ( diff --git a/tests/ops/op_math/test_sum.py b/tests/ops/op_math/test_sum.py index a9d1233ce64..22e2fc7d4c9 100644 --- a/tests/ops/op_math/test_sum.py +++ b/tests/ops/op_math/test_sum.py @@ -16,6 +16,8 @@ """ # pylint: disable=eval-used, unused-argument +import warnings + import gate_data as gd # a file containing matrix rep of each gate import numpy as np import pytest @@ -27,6 +29,14 @@ from pennylane.ops.op_math import Prod, Sum from pennylane.wires import Wires + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + no_mat_ops = ( qml.Barrier, qml.WireCut, diff --git a/tests/ops/test_meta.py b/tests/ops/test_meta.py index 19f5c303ddd..c451daff2dd 100644 --- a/tests/ops/test_meta.py +++ b/tests/ops/test_meta.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """Unit tests for the Snapshot operation.""" +import warnings + import numpy as np # pylint: disable=protected-access @@ -21,6 +23,13 @@ from pennylane import Snapshot +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class TestBarrier: """Tests that the Barrier gate is correct.""" diff --git a/tests/optimize/test_adaptive.py b/tests/optimize/test_adaptive.py index d710797bf26..06c6bb05780 100644 --- a/tests/optimize/test_adaptive.py +++ b/tests/optimize/test_adaptive.py @@ -15,12 +15,21 @@ Unit tests for the ``AdaptiveOptimizer``. """ import copy +import warnings import pytest import pennylane as qml from pennylane import numpy as np + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + symbols = ["H", "H", "H"] geometry = np.array( [[0.01076341, 0.04449877, 0.0], [0.98729513, 1.63059094, 0.0], [1.87262415, -0.00815842, 0.0]], diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 1115460922d..c761c3bb339 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -14,6 +14,7 @@ """ Tests for the pennylane.qnn.keras module. """ +import warnings from collections import defaultdict import numpy as np @@ -21,6 +22,14 @@ import pennylane as qml + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + KerasLayer = qml.qnn.keras.KerasLayer tf = pytest.importorskip("tensorflow", minversion="2") diff --git a/tests/qnn/test_qnn_torch.py b/tests/qnn/test_qnn_torch.py index e2642df0e4b..c4812c4c840 100644 --- a/tests/qnn/test_qnn_torch.py +++ b/tests/qnn/test_qnn_torch.py @@ -15,6 +15,7 @@ Tests for the pennylane.qnn.torch module. """ import math +import warnings from collections import defaultdict from unittest import mock @@ -24,6 +25,14 @@ import pennylane as qml from pennylane.qnn.torch import TorchLayer + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + torch = pytest.importorskip("torch") # pylint: disable=unnecessary-dunder-call diff --git a/tests/resource/test_error/test_error.py b/tests/resource/test_error/test_error.py index bcf3fc0538c..ef725e6ed27 100644 --- a/tests/resource/test_error/test_error.py +++ b/tests/resource/test_error/test_error.py @@ -14,6 +14,8 @@ """ Test base AlgorithmicError class and its associated methods. """ +import warnings + import numpy as np # pylint: disable=too-few-public-methods, unused-argument @@ -29,6 +31,13 @@ ) +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class SimpleError(AlgorithmicError): def combine(self, other): return self.__class__(self.error + other.error) diff --git a/tests/shadow/test_shadow_transforms.py b/tests/shadow/test_shadow_transforms.py index 91660d69e46..9630cc6a9e5 100644 --- a/tests/shadow/test_shadow_transforms.py +++ b/tests/shadow/test_shadow_transforms.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """Unit tests for the classical shadows transforms""" +import warnings + # pylint: disable=too-few-public-methods import pytest @@ -20,6 +22,13 @@ from pennylane.shadows.transforms import _replace_obs +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + def hadamard_circuit(wires, shots=10000, interface="autograd"): """Hadamard circuit to put all qubits in equal superposition (locally)""" dev = qml.device("default.qubit", wires=wires, shots=shots) diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index fe9e29f46a4..d3d47027985 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -14,6 +14,7 @@ """Unit tests for the QuantumTape""" # pylint: disable=protected-access,too-few-public-methods import copy +import warnings from collections import defaultdict import numpy as np @@ -34,6 +35,13 @@ from pennylane.tape import QuantumScript, QuantumTape, expand_tape_state_prep +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + def TestOperationMonkeypatching(): """Test that operations are monkeypatched only within the quantum tape""" with QuantumTape() as tape: diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 8d8b18e499c..80c400a5826 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -14,6 +14,8 @@ """ Unit tests for the compiler subpackage. """ +import warnings + # pylint: disable=import-outside-toplevel from unittest.mock import patch @@ -26,6 +28,14 @@ from pennylane.compiler.compiler import CompileError from pennylane.transforms.dynamic_one_shot import fill_in_value + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + catalyst = pytest.importorskip("catalyst") jax = pytest.importorskip("jax") diff --git a/tests/test_observable.py b/tests/test_observable.py index be379ea06bd..b373347613e 100644 --- a/tests/test_observable.py +++ b/tests/test_observable.py @@ -16,9 +16,20 @@ """ # pylint: disable=protected-access,cell-var-from-loop +import warnings + +import pytest + import pennylane as qml +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + def test_pass_positional_wires_to_observable(): """Tests whether the ability to pass wires as positional argument is retained""" dev = qml.device("default.qubit", wires=1) diff --git a/tests/test_qnode.py b/tests/test_qnode.py index 7bca28a490c..7886e73e94d 100644 --- a/tests/test_qnode.py +++ b/tests/test_qnode.py @@ -32,6 +32,21 @@ from pennylane.workflow.qnode import _prune_dynamic_transform +def test_tape_property_is_deprecated(): + """Test that the tape property is deprecated.""" + dev = qml.device("default.qubit") + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=0) + return qml.PauliY(0) + + with pytest.warns( + qml.PennyLaneDeprecationWarning, match="The tape/qtape property is deprecated" + ): + _ = circuit.tape + + def dummyfunc(): """dummy func.""" return None @@ -458,22 +473,30 @@ def func(x, y): y = pnp.array(0.54, requires_grad=True) res = qn(x, y) + with pytest.warns( + qml.PennyLaneDeprecationWarning, match="tape/qtape property is deprecated" + ): + tape = qn.tape - assert isinstance(qn.qtape, QuantumScript) - assert len(qn.qtape.operations) == 3 - assert len(qn.qtape.observables) == 1 - assert qn.qtape.num_params == 2 - assert qn.qtape.shots.total_shots is None + assert isinstance(tape, QuantumScript) + assert len(tape.operations) == 3 + assert len(tape.observables) == 1 + assert tape.num_params == 2 + assert tape.shots.total_shots is None - expected = qml.execute([qn.tape], dev, None) + expected = qml.execute([tape], dev, None) assert np.allclose(res, expected, atol=tol, rtol=0) # when called, a new quantum tape is constructed - old_tape = qn.qtape + old_tape = tape res2 = qn(x, y) + with pytest.warns( + qml.PennyLaneDeprecationWarning, match="tape/qtape property is deprecated" + ): + new_tape = qn.tape assert np.allclose(res, res2, atol=tol, rtol=0) - assert qn.qtape is not old_tape + assert new_tape is not old_tape def test_returning_non_measurements(self): """Test that an exception is raised if a non-measurement @@ -560,9 +583,9 @@ def func(x, y): return [m1, m2] qn = QNode(func, dev) - qn(5, 1) # evaluate the QNode - assert qn.qtape.operations == contents[0:3] - assert qn.qtape.measurements == contents[3:] + tape = qml.workflow.construct_tape(qn)(5, 1) + assert tape.operations == contents[0:3] + assert tape.measurements == contents[3:] @pytest.mark.jax def test_jit_counts_raises_error(self): @@ -621,21 +644,11 @@ def func(x, y): y = pnp.array(0.54, requires_grad=True) res = func(x, y) - - assert isinstance(func.qtape, QuantumScript) - assert len(func.qtape.operations) == 3 - assert len(func.qtape.observables) == 1 - assert func.qtape.num_params == 2 - - expected = qml.execute([func.tape], dev, None) + expected = np.cos(x) assert np.allclose(res, expected, atol=tol, rtol=0) - # when called, a new quantum tape is constructed - old_tape = func.qtape res2 = func(x, y) - assert np.allclose(res, res2, atol=tol, rtol=0) - assert func.qtape is not old_tape class TestIntegration: @@ -999,8 +1012,9 @@ def circuit(): with qml.queuing.AnnotatedQueue() as q: circuit() + tape = qml.workflow.construct_tape(circuit)() assert q.queue == [] # pylint: disable=use-implicit-booleaness-not-comparison - assert len(circuit.tape.operations) == 1 + assert len(tape.operations) == 1 def test_qnode_preserves_inferred_numpy_interface(self): """Tests that the QNode respects the inferred numpy interface.""" @@ -1077,13 +1091,16 @@ def circuit(a, shots=0): circuit = QNode(circuit, dev) assert len(circuit(0.8)) == 10 - assert circuit.qtape.operations[0].wires.labels == (0,) + tape = qml.workflow.construct_tape(circuit)(0.8) + assert tape.operations[0].wires.labels == (0,) assert len(circuit(0.8, shots=1)) == 10 - assert circuit.qtape.operations[0].wires.labels == (1,) + tape = qml.workflow.construct_tape(circuit)(0.8, shots=1) + assert tape.operations[0].wires.labels == (1,) assert len(circuit(0.8, shots=0)) == 10 - assert circuit.qtape.operations[0].wires.labels == (0,) + tape = qml.workflow.construct_tape(circuit)(0.8, shots=0) + assert tape.operations[0].wires.labels == (0,) # pylint: disable=unexpected-keyword-arg def test_no_shots_per_call_if_user_has_shots_qfunc_arg(self): @@ -1102,7 +1119,8 @@ def ansatz0(a, shots): circuit = QNode(ansatz0, dev) assert len(circuit(0.8, 1)) == 10 - assert circuit.qtape.operations[0].wires.labels == (1,) + tape = qml.workflow.construct_tape(circuit)(0.8, 1) + assert tape.operations[0].wires.labels == (1,) dev = qml.device("default.qubit", wires=2, shots=10) @@ -1116,7 +1134,8 @@ def ansatz1(a, shots): return qml.sample(qml.PauliZ(wires=0)) assert len(ansatz1(0.8, shots=0)) == 10 - assert ansatz1.qtape.operations[0].wires.labels == (0,) + tape = qml.workflow.construct_tape(circuit)(0.8, 0) + assert tape.operations[0].wires.labels == (0,) def test_shots_passed_as_unrecognized_kwarg(self): """Test that an error is raised if shots are passed to QNode initialization.""" @@ -1244,13 +1263,13 @@ def func(x, y): qn = QNode(func, dev) # No override - _ = qn(0.1, 0.2) - assert qn.tape.shots.total_shots == 5 + tape = qml.workflow.construct_tape(qn)(0.1, 0.2) + assert tape.shots.total_shots == 5 # Override - _ = qn(0.1, 0.2, shots=shots) - assert qn.tape.shots.total_shots == total_shots - assert qn.tape.shots.shot_vector == shot_vector + tape = qml.workflow.construct_tape(qn)(0.1, 0.2, shots=shots) + assert tape.shots.total_shots == total_shots + assert tape.shots.shot_vector == shot_vector # Decorator syntax @qnode(dev) @@ -1260,13 +1279,13 @@ def qn2(x, y): return qml.expval(qml.PauliZ(0)) # No override - _ = qn2(0.1, 0.2) - assert qn2.tape.shots.total_shots == 5 + tape = qml.workflow.construct_tape(qn2)(0.1, 0.2) + assert tape.shots.total_shots == 5 # Override - _ = qn2(0.1, 0.2, shots=shots) - assert qn2.tape.shots.total_shots == total_shots - assert qn2.tape.shots.shot_vector == shot_vector + tape = qml.workflow.construct_tape(qn2)(0.1, 0.2, shots=shots) + assert tape.shots.total_shots == total_shots + assert tape.shots.shot_vector == shot_vector class TestTransformProgramIntegration: diff --git a/tests/test_qnode_legacy.py b/tests/test_qnode_legacy.py index 4c953cbb810..c047a795dc0 100644 --- a/tests/test_qnode_legacy.py +++ b/tests/test_qnode_legacy.py @@ -32,6 +32,21 @@ from pennylane.typing import PostprocessingFn +def test_legacy_qtape_property_is_deprecated(): + """Test that the legacy qtape property is deprecated.""" + dev = qml.device("default.qubit") + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=0) + return qml.PauliY(0) + + with pytest.warns( + qml.PennyLaneDeprecationWarning, match="The tape/qtape property is deprecated" + ): + _ = circuit.qtape + + def dummyfunc(): """dummy func.""" return None @@ -458,22 +473,24 @@ def func(x, y): y = pnp.array(0.54, requires_grad=True) res = qn(x, y) + tape = qml.workflow.construct_tape(qn)(x, y) - assert isinstance(qn.qtape, QuantumScript) - assert len(qn.qtape.operations) == 3 - assert len(qn.qtape.observables) == 1 - assert qn.qtape.num_params == 2 - assert qn.qtape.shots.total_shots is None + assert isinstance(tape, QuantumScript) + assert len(tape.operations) == 3 + assert len(tape.observables) == 1 + assert tape.num_params == 2 + assert tape.shots.total_shots is None - expected = qml.execute([qn.tape], dev, None) + expected = qml.execute([tape], dev, None) assert np.allclose(res, expected, atol=tol, rtol=0) # when called, a new quantum tape is constructed - old_tape = qn.qtape + old_tape = tape res2 = qn(x, y) + new_tape = qml.workflow.construct_tape(qn)(x, y) assert np.allclose(res, res2, atol=tol, rtol=0) - assert qn.qtape is not old_tape + assert new_tape is not old_tape def test_returning_non_measurements(self): """Test that an exception is raised if a non-measurement @@ -560,9 +577,9 @@ def func(x, y): return [m1, m2] qn = QNode(func, dev) - qn(5, 1) # evaluate the QNode - assert qn.qtape.operations == contents[0:3] - assert qn.qtape.measurements == contents[3:] + tape = qml.workflow.construct_tape(qn)(5, 1) + assert tape.operations == contents[0:3] + assert tape.measurements == contents[3:] @pytest.mark.jax def test_jit_counts_raises_error(self): @@ -621,21 +638,23 @@ def func(x, y): y = pnp.array(0.54, requires_grad=True) res = func(x, y) + tape = qml.workflow.construct_tape(func)(x, y) - assert isinstance(func.qtape, QuantumScript) - assert len(func.qtape.operations) == 3 - assert len(func.qtape.observables) == 1 - assert func.qtape.num_params == 2 + assert isinstance(tape, QuantumScript) + assert len(tape.operations) == 3 + assert len(tape.observables) == 1 + assert tape.num_params == 2 - expected = qml.execute([func.tape], dev, None) + expected = qml.execute([tape], dev, None) assert np.allclose(res, expected, atol=tol, rtol=0) # when called, a new quantum tape is constructed - old_tape = func.qtape + old_tape = tape res2 = func(x, y) + new_tape = qml.workflow.construct_tape(func)(x, y) assert np.allclose(res, res2, atol=tol, rtol=0) - assert func.qtape is not old_tape + assert new_tape is not old_tape class TestIntegration: @@ -821,8 +840,9 @@ def circuit(): circuit.construct(tuple(), {}) spy.assert_not_called() - assert len(circuit.tape.operations) == 2 - assert isinstance(circuit.tape.operations[1], qml.measurements.MidMeasureMP) + tape = qml.workflow.construct_tape(circuit)() + assert len(tape.operations) == 2 + assert isinstance(tape.operations[1], qml.measurements.MidMeasureMP) @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) @pytest.mark.parametrize("first_par", np.linspace(0.15, np.pi - 0.3, 3)) @@ -1032,8 +1052,9 @@ def circuit(): with qml.queuing.AnnotatedQueue() as q: circuit() + tape = qml.workflow.construct_tape(circuit)() assert q.queue == [] # pylint: disable=use-implicit-booleaness-not-comparison - assert len(circuit.tape.operations) == 1 + assert len(tape.operations) == 1 class TestShots: @@ -1096,13 +1117,16 @@ def circuit(a, shots=0): circuit = QNode(circuit, dev) assert len(circuit(0.8)) == 10 - assert circuit.qtape.operations[0].wires.labels == (0,) + tape = qml.workflow.construct_tape(circuit)(0.8) + assert tape.operations[0].wires.labels == (0,) assert len(circuit(0.8, shots=1)) == 10 - assert circuit.qtape.operations[0].wires.labels == (1,) + tape = qml.workflow.construct_tape(circuit)(0.8, shots=1) + assert tape.operations[0].wires.labels == (1,) assert len(circuit(0.8, shots=0)) == 10 - assert circuit.qtape.operations[0].wires.labels == (0,) + tape = qml.workflow.construct_tape(circuit)(0.8, shots=0) + assert tape.operations[0].wires.labels == (0,) # pylint: disable=unexpected-keyword-arg def test_no_shots_per_call_if_user_has_shots_qfunc_arg(self): @@ -1121,7 +1145,8 @@ def ansatz0(a, shots): circuit = QNode(ansatz0, dev) assert len(circuit(0.8, 1)) == 10 - assert circuit.qtape.operations[0].wires.labels == (1,) + tape = qml.workflow.construct_tape(circuit)(0.8, 1) + assert tape.operations[0].wires.labels == (1,) dev = qml.device("default.mixed", wires=2, shots=10) @@ -1135,7 +1160,8 @@ def ansatz1(a, shots): return qml.sample(qml.PauliZ(wires=0)) assert len(ansatz1(0.8, shots=0)) == 10 - assert ansatz1.qtape.operations[0].wires.labels == (0,) + tape = qml.workflow.construct_tape(circuit)(0.8, 0) + assert tape.operations[0].wires.labels == (0,) # pylint: disable=unexpected-keyword-arg def test_shots_setting_does_not_mutate_device(self): @@ -1247,13 +1273,13 @@ def func(x, y): qn = QNode(func, dev) # No override - _ = qn(0.1, 0.2) - assert qn.tape.shots.total_shots == 5 + tape = qml.workflow.construct_tape(qn)(0.1, 0.2) + assert tape.shots.total_shots == 5 # Override - _ = qn(0.1, 0.2, shots=shots) - assert qn.tape.shots.total_shots == total_shots - assert qn.tape.shots.shot_vector == shot_vector + tape = qml.workflow.construct_tape(qn)(0.1, 0.2, shots=shots) + assert tape.shots.total_shots == total_shots + assert tape.shots.shot_vector == shot_vector # Decorator syntax @qnode(dev) @@ -1263,13 +1289,13 @@ def qn2(x, y): return qml.expval(qml.PauliZ(0)) # No override - _ = qn2(0.1, 0.2) - assert qn2.tape.shots.total_shots == 5 + tape = qml.workflow.construct_tape(qn2)(0.1, 0.2) + assert tape.shots.total_shots == 5 # Override - _ = qn2(0.1, 0.2, shots=shots) - assert qn2.tape.shots.total_shots == total_shots - assert qn2.tape.shots.shot_vector == shot_vector + tape = qml.workflow.construct_tape(qn2)(0.1, 0.2, shots=shots) + assert tape.shots.total_shots == total_shots + assert tape.shots.shot_vector == shot_vector class TestTransformProgramIntegration: diff --git a/tests/test_queuing.py b/tests/test_queuing.py index f9a953e174d..b1c394c59f6 100644 --- a/tests/test_queuing.py +++ b/tests/test_queuing.py @@ -14,6 +14,7 @@ """ Unit tests for the :mod:`pennylane` :class:`QueuingManager` class. """ +import warnings from multiprocessing.dummy import Pool as ThreadPool import numpy as np @@ -23,6 +24,13 @@ from pennylane.queuing import AnnotatedQueue, QueuingError, QueuingManager, WrappedObj +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + # pylint: disable=use-implicit-booleaness-not-comparison, unnecessary-dunder-call class TestStopRecording: """Test the stop_recording method of QueuingManager.""" diff --git a/tests/test_tracker.py b/tests/test_tracker.py index e4e33831254..4515404d840 100644 --- a/tests/test_tracker.py +++ b/tests/test_tracker.py @@ -14,12 +14,21 @@ """ Unit tests for the Tracker and constructor """ +import warnings + import pytest import pennylane as qml from pennylane import Tracker +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class TestTrackerCoreBehavior: """Unittests for the tracker class""" diff --git a/tests/transforms/test_cliffordt_transform.py b/tests/transforms/test_cliffordt_transform.py index 1eb6119f4b2..e9c371d3f95 100644 --- a/tests/transforms/test_cliffordt_transform.py +++ b/tests/transforms/test_cliffordt_transform.py @@ -14,6 +14,7 @@ """Unit tests for the Clifford+T transform.""" import math +import warnings from functools import reduce import pytest @@ -30,6 +31,14 @@ ) from pennylane.transforms.optimization.optimization_utils import _fuse_global_phases + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + _SKIP_GATES = (qml.Barrier, qml.Snapshot, qml.WireCut) _CLIFFORD_PHASE_GATES = _CLIFFORD_T_GATES + _SKIP_GATES diff --git a/tests/transforms/test_compile.py b/tests/transforms/test_compile.py index cb79df6b8b3..1f4ae3e5817 100644 --- a/tests/transforms/test_compile.py +++ b/tests/transforms/test_compile.py @@ -14,6 +14,7 @@ """ Unit tests for the ``compile`` transform. """ +import warnings from functools import partial import pytest @@ -33,6 +34,13 @@ from pennylane.wires import Wires +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + def build_qfunc(wires): def qfunc(x, y, z): qml.Hadamard(wires=wires[0]) diff --git a/tests/transforms/test_defer_measurements.py b/tests/transforms/test_defer_measurements.py index aee71070ead..acca15bbcc0 100644 --- a/tests/transforms/test_defer_measurements.py +++ b/tests/transforms/test_defer_measurements.py @@ -15,6 +15,7 @@ Tests for the transform implementing the deferred measurement principle. """ import math +import warnings # pylint: disable=too-few-public-methods, too-many-arguments from functools import partial @@ -28,6 +29,13 @@ from pennylane.ops import Controlled +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + def test_broadcasted_postselection(mocker): """Test that broadcast_expand is used iff broadcasting with postselection.""" spy = mocker.spy(qml.transforms, "broadcast_expand") diff --git a/tests/transforms/test_optimization/test_cancel_inverses.py b/tests/transforms/test_optimization/test_cancel_inverses.py index c71c271ed76..f62603aac2d 100644 --- a/tests/transforms/test_optimization/test_cancel_inverses.py +++ b/tests/transforms/test_optimization/test_cancel_inverses.py @@ -14,6 +14,8 @@ """ Unit tests for the optimization transform ``cancel_inverses``. """ +import warnings + import pytest from utils import compare_operation_lists @@ -23,6 +25,13 @@ from pennylane.wires import Wires +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class TestCancelInverses: """Test that adjacent inverse gates are cancelled.""" diff --git a/tests/transforms/test_optimization/test_commute_controlled.py b/tests/transforms/test_optimization/test_commute_controlled.py index 4193458c248..4ebc346cbd7 100644 --- a/tests/transforms/test_optimization/test_commute_controlled.py +++ b/tests/transforms/test_optimization/test_commute_controlled.py @@ -14,6 +14,8 @@ """ Unit tests for the optimization transform ``commute_controlled``. """ +import warnings + import pytest from utils import check_matrix_equivalence, compare_operation_lists @@ -23,6 +25,13 @@ from pennylane.wires import Wires +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class TestCommuteControlled: """Tests for single-qubit gates being pushed through controlled gates.""" diff --git a/tests/transforms/test_optimization/test_merge_amplitude_embedding.py b/tests/transforms/test_optimization/test_merge_amplitude_embedding.py index 00881a0ca95..c672a0d7a62 100644 --- a/tests/transforms/test_optimization/test_merge_amplitude_embedding.py +++ b/tests/transforms/test_optimization/test_merge_amplitude_embedding.py @@ -14,6 +14,8 @@ """ Unit tests for the optimization transform ``merge_amplitude_embedding``. """ +import warnings + import pytest import pennylane as qml @@ -21,6 +23,13 @@ from pennylane.transforms.optimization import merge_amplitude_embedding +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class TestMergeAmplitudeEmbedding: """Test that amplitude embedding gates are combined into a single.""" diff --git a/tests/transforms/test_optimization/test_merge_rotations.py b/tests/transforms/test_optimization/test_merge_rotations.py index c0aa9383459..d00f5a9e9e9 100644 --- a/tests/transforms/test_optimization/test_merge_rotations.py +++ b/tests/transforms/test_optimization/test_merge_rotations.py @@ -16,6 +16,8 @@ """ # pylint: disable=too-many-arguments +import warnings + import pytest from utils import compare_operation_lists @@ -25,6 +27,13 @@ from pennylane.wires import Wires +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class TestMergeRotations: """Test that adjacent rotation gates of the same type will add the angles.""" diff --git a/tests/transforms/test_optimization/test_pattern_matching.py b/tests/transforms/test_optimization/test_pattern_matching.py index c211e793119..698ca044c83 100644 --- a/tests/transforms/test_optimization/test_pattern_matching.py +++ b/tests/transforms/test_optimization/test_pattern_matching.py @@ -14,6 +14,8 @@ """ Unit tests for the optimization transform ``pattern_matching_optimization``. """ +import warnings + # pylint: disable=too-many-statements import pytest @@ -29,6 +31,13 @@ ) +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class TestPatternMatchingOptimization: """Pattern matching circuit optimization tests.""" diff --git a/tests/transforms/test_optimization/test_single_qubit_fusion.py b/tests/transforms/test_optimization/test_single_qubit_fusion.py index 9937e173ee6..a66752e571d 100644 --- a/tests/transforms/test_optimization/test_single_qubit_fusion.py +++ b/tests/transforms/test_optimization/test_single_qubit_fusion.py @@ -14,6 +14,8 @@ """ Unit tests for the optimization transform ``single_qubit_fusion``. """ +import warnings + import pytest from utils import check_matrix_equivalence, compare_operation_lists @@ -23,6 +25,13 @@ from pennylane.wires import Wires +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class TestSingleQubitFusion: """Test that sequences of any single-qubit rotations are fully fused.""" diff --git a/tests/transforms/test_optimization/test_undo_swaps.py b/tests/transforms/test_optimization/test_undo_swaps.py index de269fd8dd5..554f28d26b1 100644 --- a/tests/transforms/test_optimization/test_undo_swaps.py +++ b/tests/transforms/test_optimization/test_undo_swaps.py @@ -14,6 +14,8 @@ """ Unit tests for the optimization transform ``undo_swaps``. """ +import warnings + import pytest from utils import compare_operation_lists @@ -23,6 +25,13 @@ from pennylane.wires import Wires +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + class TestUndoSwaps: """Test that check the main functionalities of the `undo_swaps` transform""" diff --git a/tests/transforms/test_qcut.py b/tests/transforms/test_qcut.py index 1574f9aa45c..5fbc2d88b58 100644 --- a/tests/transforms/test_qcut.py +++ b/tests/transforms/test_qcut.py @@ -21,6 +21,7 @@ import itertools import string import sys +import warnings from functools import partial, reduce from itertools import product from os import environ @@ -39,6 +40,14 @@ from pennylane.queuing import WrappedObj from pennylane.wires import Wires + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + pytestmark = pytest.mark.qcut I, X, Y, Z = ( diff --git a/tests/transforms/test_transpile.py b/tests/transforms/test_transpile.py index fe2f6cfe695..de4b6df844f 100644 --- a/tests/transforms/test_transpile.py +++ b/tests/transforms/test_transpile.py @@ -2,6 +2,7 @@ Unit tests for transpiler function. """ +import warnings from math import isclose import pytest @@ -11,6 +12,13 @@ from pennylane.transforms.transpile import transpile +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + def build_qfunc_probs(wires): def qfunc(x, y, z): qml.Hadamard(wires=wires[0]) diff --git a/tests/transforms/test_unitary_to_rot.py b/tests/transforms/test_unitary_to_rot.py index 98c2cf59327..37952c9cd6c 100644 --- a/tests/transforms/test_unitary_to_rot.py +++ b/tests/transforms/test_unitary_to_rot.py @@ -14,6 +14,7 @@ """ Tests for the QubitUnitary decomposition transforms. """ +import warnings from itertools import product import pytest @@ -25,6 +26,14 @@ from pennylane.transforms import unitary_to_rot from pennylane.wires import Wires + +@pytest.fixture(autouse=True) +def suppress_tape_property_deprecation_warning(): + warnings.filterwarnings( + "ignore", "The tape/qtape property is deprecated", category=qml.PennyLaneDeprecationWarning + ) + + typeof_gates_zyz = (qml.RZ, qml.RY, qml.RZ) single_qubit_decompositions = [ (I, typeof_gates_zyz, [0.0, 0.0, 0.0]), From 1a7db332c92503e833a6e0449baa55ef554d288a Mon Sep 17 00:00:00 2001 From: Pietropaolo Frisoni Date: Mon, 18 Nov 2024 17:51:07 -0500 Subject: [PATCH 08/35] Extend `jax.vmap` to the multidimensional case (#6422) **Context:** This PR extends the captured `jax.vmap` version to the multidimensional input case. For further details, we refer to the description of the [first PR](https://github.com/PennyLaneAI/pennylane/pull/6349). **Description of the Change:** As above. For 'multidimensional input case' we mean something like the following: ``` qml.capture.enable() @qml.qnode(qml.device("default.qubit", wires=...)) ... jax.vmap(circuit)(jax.numpy.array([[0.1, 0.2], [0.3, 0.4]])) ``` **Benefits:** Now `jax.vmap` can be used with captured enabled if the input parameter is an array with a shape greater than 1. **Possible Drawbacks:** None that I can think of. **Related GitHub Issues:** None. **Related Shortcut Stories:** [sc-76381] --- doc/releases/changelog-dev.md | 1 + pennylane/capture/capture_diff.py | 1 - pennylane/capture/capture_qnode.py | 67 ++--- tests/capture/test_capture_qnode.py | 411 +++++++++++++++++++++++++++- 4 files changed, 433 insertions(+), 47 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 6fa4c92cb57..a43a03c6eec 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -44,6 +44,7 @@ * `jax.vmap` can be captured with `qml.capture.make_plxpr` and is compatible with quantum circuits. [(#6349)](https://github.com/PennyLaneAI/pennylane/pull/6349) + [(#6422)](https://github.com/PennyLaneAI/pennylane/pull/6422) * `qml.capture.PlxprInterpreter` base class has been added for easy transformation and execution of pennylane variant jaxpr. diff --git a/pennylane/capture/capture_diff.py b/pennylane/capture/capture_diff.py index 627ff0243ec..482f692df69 100644 --- a/pennylane/capture/capture_diff.py +++ b/pennylane/capture/capture_diff.py @@ -68,7 +68,6 @@ def _get_grad_prim(): def _(*args, argnum, jaxpr, n_consts, method, h): if method or h: # pragma: no cover raise ValueError(f"Invalid values '{method=}' and '{h=}' without QJIT.") - consts = args[:n_consts] args = args[n_consts:] diff --git a/pennylane/capture/capture_qnode.py b/pennylane/capture/capture_qnode.py index de19f464701..1f792f28401 100644 --- a/pennylane/capture/capture_qnode.py +++ b/pennylane/capture/capture_qnode.py @@ -48,23 +48,16 @@ def _is_scalar_tensor(arg) -> bool: if arg.shape == (): return True - if len(arg.shape) > 1: - raise ValueError( - "One argument has more than one dimension. " - "Currently, only single-dimension batching is supported." - ) - return False -def _get_batch_shape(args, batch_dims): +def _get_batch_shape(non_const_args, non_const_batch_dims): """Calculate the batch shape for the given arguments and batch dimensions.""" - if batch_dims is None: - return () - input_shapes = [ - (arg.shape[batch_dim],) for arg, batch_dim in zip(args, batch_dims) if batch_dim is not None + (arg.shape[batch_dim],) + for arg, batch_dim in zip(non_const_args, non_const_batch_dims) + if batch_dim is not None ] return jax.lax.broadcast_shapes(*input_shapes) @@ -117,8 +110,11 @@ def qfunc(*inner_args): qnode = qml.QNode(qfunc, device, **qnode_kwargs) if batch_dims is not None: + # pylint: disable=protected-access - return jax.vmap(partial(qnode._impl_call, shots=shots), batch_dims)(*non_const_args) + return jax.vmap(partial(qnode._impl_call, shots=shots), batch_dims[n_consts:])( + *jax.tree_util.tree_leaves(non_const_args) + ) # pylint: disable=protected-access return qnode._impl_call(*non_const_args, shots=shots) @@ -129,11 +125,14 @@ def _(*args, qnode, shots, device, qnode_kwargs, qfunc_jaxpr, n_consts, batch_di mps = qfunc_jaxpr.outvars + batch_shape = ( + _get_batch_shape(args[n_consts:], batch_dims[n_consts:]) + if batch_dims is not None + else () + ) + return _get_shapes_for( - *mps, - shots=shots, - num_device_wires=len(device.wires), - batch_shape=_get_batch_shape(args[n_consts:], batch_dims), + *mps, shots=shots, num_device_wires=len(device.wires), batch_shape=batch_shape ) def _qnode_batching_rule( @@ -152,29 +151,35 @@ def _qnode_batching_rule( This rule exploits the parameter broadcasting feature of the QNode to vectorize the circuit execution. """ - for i, (arg, batch_dim) in enumerate(zip(batched_args, batch_dims)): + for idx, (arg, batch_dim) in enumerate(zip(batched_args, batch_dims)): if _is_scalar_tensor(arg): continue - # Regardless of their shape, jax.vmap treats constants as scalars - # by automatically inserting `None` as the batch dimension. - if i < n_consts: - raise ValueError( - f"Constant argument at index {i} is not scalar. ", - "Only scalar constants are currently supported with jax.vmap.", - ) - - # To resolve this, we need to add more properties to the AbstractOperator - # class to indicate which operators support batching and check them here - if arg.size > 1 and batch_dim is None: + # Regardless of their shape, jax.vmap automatically inserts `None` as the batch dimension for constants. + # However, if the constant is not a standard JAX type, the batch dimension is not inserted at all. + # How to handle this case is still an open question. For now, we raise a warning and give the user full flexibility. + if idx < n_consts: warn( - f"Argument at index {i} has more than 1 element but is not batched. " + f"Constant argument at index {idx} is not scalar. " "This may lead to unintended behavior or wrong results if the argument is provided " "using parameter broadcasting to a quantum operation that supports batching.", UserWarning, ) + else: + + # To resolve this ambiguity, we might add more properties to the AbstractOperator + # class to indicate which operators support batching and check them here. + # As above, at this stage we raise a warning and give the user full flexibility. + if arg.size > 1 and batch_dim is None: + warn( + f"Argument at index {idx} has size > 1 but its batch dimension is None. " + "This may lead to unintended behavior or wrong results if the argument is provided " + "using parameter broadcasting to a quantum operation that supports batching.", + UserWarning, + ) + result = qnode_prim.bind( *batched_args, shots=shots, @@ -183,7 +188,7 @@ def _qnode_batching_rule( qnode_kwargs=qnode_kwargs, qfunc_jaxpr=qfunc_jaxpr, n_consts=n_consts, - batch_dims=batch_dims[n_consts:], + batch_dims=batch_dims, ) # The batch dimension is at the front (axis 0) for all elements in the result. @@ -268,6 +273,7 @@ def f(x): """ + if "shots" in kwargs: shots = qml.measurements.Shots(kwargs.pop("shots")) else: @@ -280,7 +286,6 @@ def f(x): raise NotImplementedError("devices must specify wires for integration with plxpr capture.") qfunc = partial(qnode.func, **kwargs) if kwargs else qnode.func - flat_fn = FlatFn(qfunc) qfunc_jaxpr = jax.make_jaxpr(flat_fn)(*args) diff --git a/tests/capture/test_capture_qnode.py b/tests/capture/test_capture_qnode.py index 3e625aecbc7..30abc1dc851 100644 --- a/tests/capture/test_capture_qnode.py +++ b/tests/capture/test_capture_qnode.py @@ -32,11 +32,31 @@ @pytest.fixture(autouse=True) def enable_disable_plxpr(): + """Enable and disable the PennyLane JAX capture context around each test.""" qml.capture.enable() yield qml.capture.disable() +def get_qnode_output_eqns(jaxpr): + """Extracts equations related to QNode outputs in the given JAX expression (jaxpr). + + Parameters: + jaxpr: A JAX expression with equations, containing QNode-related operations. + + Returns: + List of equations containing QNode outputs. + """ + + qnode_output_eqns = [] + + for eqn in jaxpr.eqns: + if eqn.primitive.name == "qnode": + qnode_output_eqns.append(eqn) + + return qnode_output_eqns + + def test_error_if_shot_vector(): """Test that a NotImplementedError is raised if a shot vector is provided.""" @@ -361,6 +381,7 @@ def circuit(x): assert qml.math.allclose(jvp, (qml.math.cos(x), -qml.math.sin(x) * xt)) +# pylint: disable=too-many-public-methods class TestQNodeVmapIntegration: """Tests for integrating JAX vmap with the QNode primitive.""" @@ -495,8 +516,8 @@ def circuit(x): res = jax.core.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, x) assert qml.math.allclose(res, circuit(x)) - def test_qnode_vmap_closure_error(self): - """Test that an error is raised when trying to vmap over a batched non-scalar closure variable.""" + def test_qnode_vmap_closure_warn(self): + """Test that a warning is raised when trying to vmap over a batched non-scalar closure variable.""" dev = qml.device("default.qubit", wires=2) const = jax.numpy.array([2.0, 6.6]) @@ -507,9 +528,7 @@ def circuit(x): qml.RX(const, wires=0) return qml.expval(qml.PauliZ(0)) - with pytest.raises( - ValueError, match="Only scalar constants are currently supported with jax.vmap." - ): + with pytest.warns(UserWarning, match="Constant argument at index 0 is not scalar. "): jax.make_jaxpr(jax.vmap(circuit))(jax.numpy.array([0.1, 0.2])) def test_vmap_overriding_shots(self): @@ -581,7 +600,7 @@ def circuit(param_array, param_array_2): param_array = jax.numpy.array([1.0, 1.2, 1.3]) param_array_2 = jax.numpy.array([2.0, 2.1, 2.2]) - with pytest.warns(UserWarning, match="Argument at index 1 has more"): + with pytest.warns(UserWarning, match="Argument at index 1 has size"): jax.make_jaxpr(jax.vmap(circuit, in_axes=(0, None)))(param_array, param_array_2) def test_qnode_pytree_input_vmap(self): @@ -637,15 +656,377 @@ def circuit(x): assert qml.math.allclose(out["b"], -jax.numpy.sin(x)) assert list(out.keys()) == ["a", "b"] - def test_error_multidimensional_batching(self): - """Test that an error is raised when trying to vmap over a multidimensional batched parameter.""" + def test_simple_multidim_case(self): + """Test vmap over a simple multidimensional case.""" - @qml.qnode(qml.device("default.qubit", wires=2)) + @qml.qnode(qml.device("default.qubit", wires=1)) def circuit(x): - qml.RX(x, 0) - return qml.expval(qml.Z(0)) + qml.RX(jax.numpy.pi * x[0], wires=0) + qml.RY(x[1] ** 2, wires=0) + qml.RX(x[1] * x[2], wires=0) + return qml.expval(qml.PauliZ(0)) - with pytest.raises( - ValueError, match="Currently, only single-dimension batching is supported" - ): - jax.make_jaxpr(jax.vmap(circuit))(jax.numpy.array([[0.1, 0.2], [0.3, 0.4]])) + def cost_fn(x): + result = circuit(x) + return jax.numpy.cos(result) ** 2 + + x = jax.numpy.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]) + + jaxpr = jax.make_jaxpr(jax.vmap(cost_fn))(x) + + assert len(jaxpr.eqns[0].outvars) == 1 + assert jaxpr.out_avals[0].shape == (2,) + + result = jax.core.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, x) + assert len(result[0]) == 2 + assert jax.numpy.allclose(result[0][0], cost_fn(x[0])) + assert jax.numpy.allclose(result[0][1], cost_fn(x[1])) + + def test_simple_multidim_case_2(self): + """Test vmap over a simple multidimensional case with a scalar and constant argument.""" + + # pylint: disable=import-outside-toplevel + from scipy.stats import unitary_group + + const = jax.numpy.array(2.0) + + @qml.qnode(qml.device("default.qubit", wires=4)) + def circuit(x, y, U): + qml.QubitUnitary(U, wires=[0, 1, 2, 3]) + qml.RX(x, wires=0) + qml.RY(y, wires=1) + qml.RX(x, wires=2) + qml.RY(const, wires=3) + return qml.expval(qml.Z(0) @ qml.X(1) @ qml.Z(2) @ qml.Z(3)) + + x = jax.numpy.array([0.4, 2.1, -1.3]) + y = 2.71 + U = jax.numpy.stack([unitary_group.rvs(16) for _ in range(3)]) + + jaxpr = jax.make_jaxpr(jax.vmap(circuit, in_axes=(0, None, 0)))(x, y, U) + assert len(jaxpr.eqns[0].invars) == 4 # 3 args + 1 const + assert len(jaxpr.eqns[0].outvars) == 1 + assert jaxpr.out_avals[0].shape == (3,) + + result = jax.core.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, x, y, U) + assert qml.math.allclose(result, circuit(x, y, U)) + + def test_vmap_circuit_inside(self): + """Test vmap of a hybrid workflow.""" + + def workflow(x): + @qml.qnode(qml.device("default.qubit", wires=1)) + def circuit(x): + qml.RX(jax.numpy.pi * x[0], wires=0) + qml.RY(x[1] ** 2, wires=0) + qml.RX(x[1] * x[2], wires=0) + return qml.expval(qml.PauliZ(0)) + + res1 = jax.vmap(circuit)(x) + res2 = jax.vmap(circuit, in_axes=0)(x) + res3 = jax.vmap(circuit, in_axes=(0,))(x) + return res1, res2, res3 + + x = jax.numpy.array( + [ + [0.1, 0.2, 0.3], + [0.4, 0.5, 0.6], + [0.7, 0.8, 0.9], + ] + ) + + jaxpr = jax.make_jaxpr(workflow)(x) + + qnode_output_eqns = get_qnode_output_eqns(jaxpr) + assert len(qnode_output_eqns) == 3 + for eqn in qnode_output_eqns: + assert len(eqn.outvars) == 1 + assert eqn.outvars[0].aval.shape == (3,) + + result = jax.core.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, x) + expected = jax.numpy.array([0.93005586, 0.00498127, -0.88789978]) + assert jax.numpy.allclose(result[0], expected) + assert jax.numpy.allclose(result[1], expected) + assert jax.numpy.allclose(result[2], expected) + + def test_vmap_nonzero_axes(self): + """Test vmap of a hybrid workflow with axes > 0.""" + + def workflow(x): + @qml.qnode(qml.device("default.qubit", wires=1)) + def circuit(x): + qml.RX(jax.numpy.pi * x[0], wires=0) + qml.RY(x[1] ** 2, wires=0) + qml.RX(x[1] * x[2], wires=0) + return qml.expval(qml.PauliZ(0)) + + res1 = jax.vmap(circuit, in_axes=1)(x) + res2 = jax.vmap(circuit, in_axes=(1,))(x) + return res1, res2 + + x = jax.numpy.array( + [ + [0.1, 0.4], + [0.2, 0.5], + [0.3, 0.6], + ] + ) + + jaxpr = jax.make_jaxpr(workflow)(x) + + qnode_output_eqns = get_qnode_output_eqns(jaxpr) + assert len(qnode_output_eqns) == 2 + for eqn in qnode_output_eqns: + assert len(eqn.outvars) == 1 + assert eqn.outvars[0].aval.shape == (2,) + + result = jax.core.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, x) + expected = jax.numpy.array([0.93005586, 0.00498127]) + assert jax.numpy.allclose(result[0], expected) + assert jax.numpy.allclose(result[1], expected) + + def test_vmap_nonzero_axes_2(self): + """Test vmap of a hybrid workflow with axes > 0.""" + + def workflow(y, x): + @qml.qnode(qml.device("default.qubit", wires=1)) + def circuit(y, x): + qml.RX(jax.numpy.pi * x[0] * y, wires=0) + qml.RY(x[1] ** 2, wires=0) + qml.RX(x[1] * x[2] * y, wires=0) + return qml.expval(qml.PauliZ(0)) + + res1 = jax.vmap(circuit, in_axes=(None, 1))(y[0], x) + res2 = jax.vmap(circuit, in_axes=(0, 1))(y, x) + return res1, res2 + + x = jax.numpy.array( + [ + [0.1, 0.4], + [0.2, 0.5], + [0.3, 0.6], + ] + ) + y = jax.numpy.array([1, 2]) + + jaxpr = jax.make_jaxpr(workflow)(y, x) + + qnode_output_eqns = get_qnode_output_eqns(jaxpr) + assert len(qnode_output_eqns) == 2 + for eqn in qnode_output_eqns: + assert len(eqn.outvars) == 1 + assert eqn.outvars[0].aval.shape == (2,) + + result = jax.core.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, y, x) + expected = jax.numpy.array([0.93005586, 0.00498127]) + expected2 = jax.numpy.array([0.93005586, -0.97884155]) + assert jax.numpy.allclose(result[0], expected) + assert jax.numpy.allclose(result[1], expected2) + + def test_vmap_tuple_in_axes(self): + """Test vmap of a hybrid workflow with tuple in_axes.""" + + def workflow(x, y, z): + @qml.qnode(qml.device("default.qubit", wires=1)) + def circuit(x, y): + qml.RX(jax.numpy.pi * x[0] + y - y, wires=0) + qml.RY(x[1] ** 2, wires=0) + qml.RX(x[1] * x[2], wires=0) + return qml.expval(qml.PauliZ(0)) + + def workflow2(x, y): + return circuit(x, y) * y + + def workflow3(y, x): + return circuit(x, y) * y + + def workflow4(y, x, z): + return circuit(x, y) * y * z + + res1 = jax.vmap(workflow2, in_axes=(0, None))(x, y) + res2 = jax.vmap(workflow3, in_axes=(None, 0))(y, x) + res3 = jax.vmap(workflow4, in_axes=(None, 0, None))(y, x, z) + return res1, res2, res3 + + y = jax.numpy.pi + x = jax.numpy.array( + [ + [0.1, 0.2, 0.3], + [0.4, 0.5, 0.6], + [0.7, 0.8, 0.9], + ] + ) + + jaxpr = jax.make_jaxpr(workflow)(x, y, 1) + + qnode_output_eqns = get_qnode_output_eqns(jaxpr) + assert len(qnode_output_eqns) == 3 + for eqn in qnode_output_eqns: + assert len(eqn.outvars) == 1 + assert eqn.outvars[0].aval.shape == (3,) + + result = jax.core.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, x, y, 1) + expected = jax.numpy.array([0.93005586, 0.00498127, -0.88789978]) * y + assert jax.numpy.allclose(result[0], expected) + assert jax.numpy.allclose(result[1], expected) + assert jax.numpy.allclose(result[2], expected) + + def test_vmap_pytree_in_axes(self): + """Test vmap of a hybrid workflow with pytree in_axes.""" + + def workflow(x, y, z): + @qml.qnode(qml.device("default.qubit", wires=1)) + def circuit(x, y): + qml.RX(jax.numpy.pi * x["arr"][0] + y - y, wires=0) + qml.RY(x["arr"][1] ** 2, wires=0) + qml.RX(x["arr"][1] * x["arr"][2], wires=0) + return qml.expval(qml.PauliZ(0)) + + def workflow2(x, y): + return circuit(x, y) * y + + def workflow3(y, x): + return circuit(x, y) * y + + def workflow4(y, x, z): + return circuit(x, y) * y * z + + res1 = jax.vmap(workflow2, in_axes=({"arr": 0, "foo": None}, None))(x, y) + res2 = jax.vmap(workflow2, in_axes=({"arr": 0, "foo": None}, None))(x, y) + res3 = jax.vmap(workflow3, in_axes=(None, {"arr": 0, "foo": None}))(y, x) + res4 = jax.vmap(workflow4, in_axes=(None, {"arr": 0, "foo": None}, None))(y, x, z) + return res1, res2, res3, res4 + + y = jax.numpy.pi + x = { + "arr": jax.numpy.array( + [ + [0.1, 0.2, 0.3], + [0.4, 0.5, 0.6], + [0.7, 0.8, 0.9], + ] + ), + "foo": None, + } + + jaxpr = jax.make_jaxpr(workflow)(x, y, 1) + + qnode_output_eqns = get_qnode_output_eqns(jaxpr) + assert len(qnode_output_eqns) == 4 + for eqn in qnode_output_eqns: + assert len(eqn.outvars) == 1 + assert eqn.outvars[0].aval.shape == (3,) + + result = jax.core.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, x, y, 1) + expected = jax.numpy.array([0.93005586, 0.00498127, -0.88789978]) * y + assert jax.numpy.allclose(result[0], expected) + assert jax.numpy.allclose(result[1], expected) + assert jax.numpy.allclose(result[2], expected) + assert jax.numpy.allclose(result[3], expected) + + def test_vmap_circuit_return_tensor(self): + """Test vmapping over a QNode that returns a tensor.""" + + def workflow(x): + @qml.qnode(qml.device("default.qubit", wires=1)) + def circuit(x): + qml.RX(jax.numpy.pi * x[0], wires=0) + qml.RY(x[1] ** 2, wires=0) + qml.RX(x[1] * x[2], wires=0) + return qml.state() + + res1 = jax.vmap(circuit)(x) + res2 = jax.vmap(circuit, out_axes=0)(x) + return res1, res2 + + x = jax.numpy.array([[0.1, 0.2, 0.3], [0.7, 0.8, 0.9]]) + + jaxpr = jax.make_jaxpr(workflow)(x) + + qnode_output_eqns = get_qnode_output_eqns(jaxpr) + assert len(qnode_output_eqns) == 2 + for eqn in qnode_output_eqns: + assert len(eqn.outvars) == 1 + assert eqn.outvars[0].aval.shape == (2, 2) + + result = jax.core.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, x) + expected = jax.numpy.array( + [ + [0.98235508 + 0.00253459j, 0.0198374 - 0.18595308j], + [0.10537427 + 0.2120056j, 0.23239136 - 0.94336851j], + ] + ) + assert jax.numpy.allclose(result[0], expected) + assert jax.numpy.allclose(result[1], expected) + + def test_vmap_circuit_return_tensor_pytree(self): + """Test vmapping over a QNode that returns a pytree tensor.""" + + def workflow(x): + @qml.qnode(qml.device("default.qubit", wires=1)) + def circuit(x): + qml.RX(jax.numpy.pi * x[0], wires=0) + qml.RY(x[1] ** 2, wires=0) + qml.RX(x[1] * x[2], wires=0) + return qml.state(), qml.probs(0) + + res1 = jax.vmap(circuit)(x) + return res1 + + x = jax.numpy.array([[0.1, 0.2, 0.3], [0.7, 0.8, 0.9]]) + + jaxpr = jax.make_jaxpr(workflow)(x) + + assert len(jaxpr.eqns[0].outvars) == 2 + assert jaxpr.out_avals[0].shape == (2, 2) + assert jaxpr.out_avals[1].shape == (2, 2) + + expected_state = jax.numpy.array( + [ + [0.98235508 + 0.00253459j, 0.0198374 - 0.18595308j], + [0.10537427 + 0.2120056j, 0.23239136 - 0.94336851j], + ] + ) + expected_probs = jax.numpy.array([[0.96502793, 0.03497207], [0.05605011, 0.94394989]]) + + result = jax.core.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, x) + assert jax.numpy.allclose(result[0], expected_state) + assert jax.numpy.allclose(result[1], expected_probs) + + def test_vmap_circuit_return_tensor_out_axes_multiple(self): + """Test vmapping over a QNode that returns a tensor with multiple out_axes.""" + + def workflow(x): + @qml.qnode(qml.device("default.qubit", wires=1)) + def circuit(x): + qml.RX(jax.numpy.pi * x[0], wires=0) + qml.RY(x[1] ** 2, wires=0) + qml.RX(x[1] * x[2], wires=0) + return qml.state(), qml.state() + + res1 = jax.vmap(circuit, out_axes=1)(x) + res2 = jax.vmap(circuit, out_axes=(0, 1))(x) + return res1, res2 + + x = jax.numpy.array([[0.1, 0.2, 0.3], [0.7, 0.8, 0.9]]) + + jaxpr = jax.make_jaxpr(workflow)(x) + + qnode_output_eqns = get_qnode_output_eqns(jaxpr) + assert len(qnode_output_eqns) == 2 + for eqn in qnode_output_eqns: + assert len(eqn.outvars) == 2 + assert eqn.outvars[0].aval.shape == (2, 2) + assert eqn.outvars[1].aval.shape == (2, 2) + + result = jax.core.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, x) + expected = jax.numpy.array( + [ + [0.98235508 + 0.00253459j, 0.0198374 - 0.18595308j], + [0.10537427 + 0.2120056j, 0.23239136 - 0.94336851j], + ] + ) + assert jax.numpy.allclose(jax.numpy.transpose(result[0], (1, 0)), expected) + assert jax.numpy.allclose(jax.numpy.transpose(result[1], (1, 0)), expected) + assert jax.numpy.allclose(result[2], expected) + assert jax.numpy.allclose(jax.numpy.transpose(result[3], (1, 0)), expected) From dff5d70b210a2dbc28ecf1352630af63d7f5e603 Mon Sep 17 00:00:00 2001 From: Jay Soni Date: Mon, 18 Nov 2024 18:28:54 -0500 Subject: [PATCH 09/35] Adding `Resources` and `CompressedResourcesOp` to labs (#6428) **Context:** As part of adding the experimental changes to core PennyLane, this PR adds some container classes for storing a light weight representation of the operator along with modifications to Resources class. **Description of the Change:** - Added changelog section for Labs - Setup documentation for features in `/labs/resource_estimation` - Added `CompressedResourcesOp`. - Modified `Resources` from core PennyLane. --------- Co-authored-by: Will Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com> --- doc/releases/changelog-dev.md | 8 +- pennylane/labs/.pylintrc | 2 +- pennylane/labs/__init__.py | 12 +- .../labs/resource_estimation/__init__.py | 38 ++ .../resource_estimation/resource_container.py | 259 ++++++++++++++ .../resource_estimation/resource_operator.py | 105 ++++++ .../test_resource_container.py | 332 ++++++++++++++++++ .../test_resource_operator.py | 103 ++++++ pennylane/templates/subroutines/trotter.py | 6 +- 9 files changed, 853 insertions(+), 12 deletions(-) create mode 100644 pennylane/labs/resource_estimation/__init__.py create mode 100644 pennylane/labs/resource_estimation/resource_container.py create mode 100644 pennylane/labs/resource_estimation/resource_operator.py create mode 100644 pennylane/labs/tests/resource_estimation/test_resource_container.py create mode 100644 pennylane/labs/tests/resource_estimation/test_resource_operator.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index a43a03c6eec..b80651b8127 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -65,6 +65,11 @@ * Expand `ExecutionConfig.gradient_method` to store `TransformDispatcher` type. [(#6455)](https://github.com/PennyLaneAI/pennylane/pull/6455) +

Labs 🧪

+ +* Added base class `Resources`, `CompressedResourceOp`, `ResourceOperator` for advanced resource estimation. + [(#6428)](https://github.com/PennyLaneAI/pennylane/pull/6428) +

Breaking changes 💔

* Legacy operator arithmetic has been removed. This includes `qml.ops.Hamiltonian`, `qml.operation.Tensor`, @@ -180,4 +185,5 @@ Austin Huang, Korbinian Kottmann, Christina Lee, Andrija Paurevic, -Justin Pickering +Justin Pickering, +Jay Soni, \ No newline at end of file diff --git a/pennylane/labs/.pylintrc b/pennylane/labs/.pylintrc index ed06299952c..51a87506549 100644 --- a/pennylane/labs/.pylintrc +++ b/pennylane/labs/.pylintrc @@ -30,7 +30,7 @@ ignored-classes=numpy,scipy,autograd,toml,appdir,autograd.numpy,autograd.numpy.l # it should appear only once). # Cyclical import checks are disabled for now as they are frequently used in # the code base, but this can be removed in the future once cycles are resolved. -disable=line-too-long,invalid-name,too-many-lines,redefined-builtin,too-many-locals,duplicate-code,cyclic-import,import-error,bad-option-value +disable=line-too-long,invalid-name,too-many-lines,redefined-builtin,too-many-locals,duplicate-code,cyclic-import,import-error,bad-option-value,too-few-public-methods [MISCELLANEOUS] diff --git a/pennylane/labs/__init__.py b/pennylane/labs/__init__.py index 56beca6ebef..2a0289f74c4 100644 --- a/pennylane/labs/__init__.py +++ b/pennylane/labs/__init__.py @@ -14,26 +14,24 @@ r""" .. currentmodule:: pennylane -This module module contains experimental features enabling +This module contains experimental features enabling advanced quantum computing research. -.. warning:: - - This module is experimental. Frequent changes will occur, - with no guarantees of stability or backwards compatibility. - .. currentmodule:: pennylane.labs Modules -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~ .. autosummary:: :toctree: api dla + resource_estimation """ from pennylane.labs import dla +from pennylane.labs import resource_estimation + __all__ = [] diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py new file mode 100644 index 00000000000..911cd0bf5ff --- /dev/null +++ b/pennylane/labs/resource_estimation/__init__.py @@ -0,0 +1,38 @@ +# Copyright 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. +r""" +This module contains experimental features for +resource estimation. + +.. warning:: + + This module is experimental. Frequent changes will occur, + with no guarantees of stability or backwards compatibility. + +.. currentmodule:: pennylane.labs.resource_estimation + +Resource Estimation Base Classes: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autosummary:: + :toctree: api + + ~Resources + ~CompressedResourceOp + ~ResourceOperator + +""" + +from .resource_operator import ResourceOperator +from .resource_container import CompressedResourceOp, Resources diff --git a/pennylane/labs/resource_estimation/resource_container.py b/pennylane/labs/resource_estimation/resource_container.py new file mode 100644 index 00000000000..23f8b4cc6ba --- /dev/null +++ b/pennylane/labs/resource_estimation/resource_container.py @@ -0,0 +1,259 @@ +# Copyright 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. +r"""Base classes for resource estimation.""" +import copy +from collections import defaultdict +from dataclasses import dataclass, field + +from pennylane.labs.resource_estimation import ResourceOperator + + +class CompressedResourceOp: + r"""Instantiate the light weight class corresponding to the operator type and parameters. + + Args: + op_type (Type): the class object of an operation which inherits from '~.ResourceOperator' + params (dict): a dictionary containing the minimal pairs of parameter names and values + required to compute the resources for the given operator + + .. details:: + + This representation is the minimal amount of information required to estimate resources for the operator. + + **Example** + + >>> op_tp = CompressedResourceOp(ResourceHadamard, {"num_wires":1}) + >>> print(op_tp) + Hadamard(num_wires=1) + """ + + def __init__(self, op_type, params: dict) -> None: + r"""Instantiate the light weight class corresponding to the operator type and parameters. + + Args: + op_type (Type): the class object for an operation which inherits from '~.ResourceOperator' + params (dict): a dictionary containing the minimal pairs of parameter names and values + required to compute the resources for the given operator + + .. details:: + + This representation is the minimal amount of information required to estimate resources for the operator. + + **Example** + + >>> op_tp = CompressedResourceOp(ResourceHadamard, {"num_wires":1}) + >>> print(op_tp) + Hadamard(num_wires=1) + """ + if not issubclass(op_type, ResourceOperator): + raise TypeError(f"op_type must be a subclass of ResourceOperator. Got {op_type}.") + + self._name = (op_type.__name__).replace("Resource", "") + self.op_type = op_type + self.params = params + self._hashable_params = tuple(params.items()) + + def __hash__(self) -> int: + return hash((self._name, self._hashable_params)) + + def __eq__(self, other: object) -> bool: + return (self.op_type == other.op_type) and (self.params == other.params) + + def __repr__(self) -> str: + op_type_str = self._name + "(" + params_str = ", ".join([f"{key}={self.params[key]}" for key in self.params]) + ")" + + return op_type_str + params_str + + +@dataclass +class Resources: + r"""Contains attributes which store key resources such as number of gates, number of wires, and gate types. + + Args: + num_wires (int): number of qubits + num_gates (int): number of gates + gate_types (dict): dictionary storing operation names (str) as keys + and the number of times they are used in the circuit (int) as values + + .. details:: + + The resources being tracked can be accessed as class attributes. + Additionally, the :code:`Resources` instance can be nicely displayed in the console. + + **Example** + + >>> r = Resources( + ... num_wires=2, + ... num_gates=2, + ... gate_types={"Hadamard": 1, "CNOT": 1} + ... ) + >>> print(r) + wires: 2 + gates: 2 + gate_types: + {'Hadamard': 1, 'CNOT': 1} + """ + + num_wires: int = 0 + num_gates: int = 0 + gate_types: defaultdict = field(default_factory=lambda: defaultdict(int)) + + def __add__(self, other: "Resources") -> "Resources": + """Add two resources objects in series""" + return add_in_series(self, other) + + def __mul__(self, scalar: int) -> "Resources": + """Scale a resources object in series""" + return mul_in_series(self, scalar) + + __rmul__ = __mul__ # same implementation + + def __iadd__(self, other: "Resources") -> "Resources": + """Add two resources objects in series""" + return add_in_series(self, other, in_place=True) + + def __imull__(self, scalar: int) -> "Resources": + """Scale a resources object in series""" + return mul_in_series(self, scalar, in_place=True) + + def __str__(self): + """String representation of the Resources object.""" + keys = ["wires", "gates"] + vals = [self.num_wires, self.num_gates] + items = "\n".join([str(i) for i in zip(keys, vals)]) + items = items.replace("('", "") + items = items.replace("',", ":") + items = items.replace(")", "") + + gate_type_str = ", ".join( + [f"'{gate_name}': {count}" for gate_name, count in self.gate_types.items()] + ) + items += "\ngate_types:\n{" + gate_type_str + "}" + return items + + def _ipython_display_(self): + """Displays __str__ in ipython instead of __repr__""" + print(str(self)) + + +def add_in_series(first: Resources, other: Resources, in_place=False) -> Resources: + r"""Add two resources assuming the circuits are executed in series. + + Args: + first (Resources): first resource object to combine + other (Resources): other resource object to combine with + in_place (bool): determines if the first Resources are modified in place (default False) + + Returns: + Resources: combined resources + """ + new_wires = max(first.num_wires, other.num_wires) + new_gates = first.num_gates + other.num_gates + new_gate_types = _combine_dict(first.gate_types, other.gate_types, in_place=in_place) + + if in_place: + first.num_wires = new_wires + first.num_gates = new_gates + return first + + return Resources(new_wires, new_gates, new_gate_types) + + +def add_in_parallel(first: Resources, other: Resources, in_place=False) -> Resources: + r"""Add two resources assuming the circuits are executed in parallel. + + Args: + first (Resources): first resource object to combine + other (Resources): other resource object to combine with + in_place (bool): determines if the first Resources are modified in place (default False) + + Returns: + Resources: combined resources + """ + new_wires = first.num_wires + other.num_wires + new_gates = first.num_gates + other.num_gates + new_gate_types = _combine_dict(first.gate_types, other.gate_types, in_place=in_place) + + if in_place: + first.num_wires = new_wires + first.num_gates = new_gates + return first + + return Resources(new_wires, new_gates, new_gate_types) + + +def mul_in_series(first: Resources, scalar: int, in_place=False) -> Resources: + r"""Multiply the resources by a scalar assuming the circuits are executed in series. + + Args: + first (Resources): first resource object to combine + scalar (int): integer value to scale the resources by + in_place (bool): determines if the first Resources are modified in place (default False) + + Returns: + Resources: combined resources + """ + new_gates = scalar * first.num_gates + new_gate_types = _scale_dict(first.gate_types, scalar, in_place=in_place) + + if in_place: + first.num_gates = new_gates + return first + + return Resources(first.num_wires, new_gates, new_gate_types) + + +def mul_in_parallel(first: Resources, scalar: int, in_place=False) -> Resources: + r"""Multiply the resources by a scalar assuming the circuits are executed in parallel. + + Args: + first (Resources): first resource object to combine + scalar (int): integer value to scale the resources by + in_place (bool): determines if the first Resources are modified in place (default False) + + Returns: + Resources: combined resources + """ + new_wires = scalar * first.num_wires + new_gates = scalar * first.num_gates + new_gate_types = _scale_dict(first.gate_types, scalar, in_place=in_place) + + if in_place: + first.num_wires = new_wires + first.num_gates = new_gates + return first + + return Resources(new_wires, new_gates, new_gate_types) + + +def _combine_dict(dict1: defaultdict, dict2: defaultdict, in_place=False): + r"""Private function which combines two dictionaries together.""" + combined_dict = dict1 if in_place else copy.copy(dict1) + + for k, v in dict2.items(): + combined_dict[k] += v + + return combined_dict + + +def _scale_dict(dict1: defaultdict, scalar: int, in_place=False): + r"""Private function which scales the values in a dictionary.""" + + combined_dict = dict1 if in_place else copy.copy(dict1) + + for k in combined_dict: + combined_dict[k] *= scalar + + return combined_dict diff --git a/pennylane/labs/resource_estimation/resource_operator.py b/pennylane/labs/resource_estimation/resource_operator.py new file mode 100644 index 00000000000..8064984ddfc --- /dev/null +++ b/pennylane/labs/resource_estimation/resource_operator.py @@ -0,0 +1,105 @@ +# Copyright 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. +r"""Abstract base class for resource operators.""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Callable, Dict + +if TYPE_CHECKING: + from pennylane.labs.resource_estimation import CompressedResourceOp + + +class ResourceOperator(ABC): + r"""Abstract class that defines the methods a PennyLane Operator + must implement in order to be used for resource estimation. + + .. details:: + + **Example** + + A PennyLane Operator can be extended for resource estimation by creating a new class that + inherits from both the ``Operator`` and ``ResourceOperator``. Here is an example showing how to + extend ``qml.QFT`` for resource estimation. + + .. code-block:: python + + import pennylane as qml + from pennylane.labs.resource_estimation import CompressedResourceOp, ResourceOperator + + class ResourceQFT(qml.QFT, ResourceOperator): + + @staticmethod + def _resource_decomp(num_wires) -> dict[CompressedResourceOp, int]: + gate_types = {} + + hadamard = ResourceHadamard.resource_rep() + swap = ResourceSWAP.resource_rep() + ctrl_phase_shift = ResourceControlledPhaseShift.resource_rep() + + gate_types[hadamard] = num_wires + gate_types[swap] = num_wires // 2 + gate_types[ctrl_phase_shift] = num_wires*(num_wires - 1) // 2 + + return gate_types + + def resource_params(self, num_wires) -> dict: + return {"num_wires": num_wires} + + @classmethod + def resource_rep(cls, num_wires) -> CompressedResourceOp: + params = {"num_wires": num_wires} + return CompressedResourceOp(cls, params) + + Which can be instantiated as a normal operation, but now contains the resources: + + .. code-block:: bash + + >>> op = ResourceQFT(range(3)) + >>> op.resources(**op.resource_params()) + {Hadamard(): 3, SWAP(): 1, ControlledPhaseShift(): 3} + + """ + + @staticmethod + @abstractmethod + def _resource_decomp(*args, **kwargs) -> Dict[CompressedResourceOp, int]: + """Returns a dictionary to be used for internal tracking of resources. This method is only to be used inside + the methods of classes inheriting from ResourceOperator.""" + + @classmethod + def resources(cls, *args, **kwargs) -> Dict[CompressedResourceOp, int]: + """Returns a dictionary containing the counts of each operator type used to + compute the resources of the operator.""" + return cls._resource_decomp(*args, **kwargs) + + @classmethod + def set_resources(cls, new_func: Callable) -> None: + """Set a custom resource method.""" + cls.resources = new_func + + @abstractmethod + def resource_params(self) -> dict: + """Returns a dictionary containing the minimal information needed to + compute a comparessed representation.""" + + @classmethod + @abstractmethod + def resource_rep(cls, *args, **kwargs) -> CompressedResourceOp: + """Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation.""" + + def resource_rep_from_op(self) -> CompressedResourceOp: + """Returns a compressed representation directly from the operator""" + return self.__class__.resource_rep(**self.resource_params()) diff --git a/pennylane/labs/tests/resource_estimation/test_resource_container.py b/pennylane/labs/tests/resource_estimation/test_resource_container.py new file mode 100644 index 00000000000..616b095e768 --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/test_resource_container.py @@ -0,0 +1,332 @@ +# Copyright 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. +""" +Test base Resource class and its associated methods +""" +# pylint:disable=protected-access, no-self-use, too-few-public-methods +import copy +from collections import defaultdict + +import pytest + +import pennylane as qml +from pennylane.labs.resource_estimation.resource_container import ( + CompressedResourceOp, + Resources, + _combine_dict, + _scale_dict, + add_in_parallel, + add_in_series, + mul_in_parallel, + mul_in_series, +) +from pennylane.labs.resource_estimation.resource_operator import ResourceOperator + + +class ResourceDummyX(ResourceOperator): + """Dummy testing class representing X gate""" + + +class ResourceDummyQFT(ResourceOperator): + """Dummy testing class representing QFT gate""" + + +class ResourceDummyQSVT(ResourceOperator): + """Dummy testing class representing QSVT gate""" + + +class ResourceDummyTrotterProduct(ResourceOperator): + """Dummy testing class representing TrotterProduct gate""" + + +class TestCompressedResourceOp: + """Testing the methods and attributes of the CompressedResourceOp class""" + + test_hamiltonian = qml.dot([1, -1, 0.5], [qml.X(0), qml.Y(1), qml.Z(0) @ qml.Z(1)]) + compressed_ops_and_params_lst = ( + ("DummyX", ResourceDummyX, {"num_wires": 1}), + ("DummyQFT", ResourceDummyQFT, {"num_wires": 5}), + ("DummyQSVT", ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}), + ( + "DummyTrotterProduct", + ResourceDummyTrotterProduct, + {"Hamiltonian": test_hamiltonian, "num_steps": 5, "order": 2}, + ), + ) + + compressed_op_reprs = ( + "DummyX(num_wires=1)", + "DummyQFT(num_wires=5)", + "DummyQSVT(num_wires=3, num_angles=5)", + "DummyTrotterProduct(Hamiltonian=X(0) + -1 * Y(1) + 0.5 * (Z(0) @ Z(1)), num_steps=5, order=2)", + ) + + @pytest.mark.parametrize("name, op_type, parameters", compressed_ops_and_params_lst) + def test_init(self, name, op_type, parameters): + """Test that we can correctly instantiate CompressedResourceOp""" + cr_op = CompressedResourceOp(op_type, parameters) + + assert cr_op._name == name + assert cr_op.op_type is op_type + assert cr_op.params == parameters + assert cr_op._hashable_params == tuple(parameters.items()) + + def test_hash(self): + """Test that the hash method behaves as expected""" + CmprssedQSVT1 = CompressedResourceOp(ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}) + CmprssedQSVT2 = CompressedResourceOp(ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}) + Other = CompressedResourceOp(ResourceDummyQFT, {"num_wires": 3}) + + assert hash(CmprssedQSVT1) == hash(CmprssedQSVT1) # compare same object + assert hash(CmprssedQSVT1) == hash(CmprssedQSVT2) # compare identical instance + assert hash(CmprssedQSVT1) != hash(Other) + + def test_equality(self): + """Test that the equality methods behaves as expected""" + CmprssedQSVT1 = CompressedResourceOp(ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}) + CmprssedQSVT2 = CompressedResourceOp(ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}) + CmprssedQSVT3 = CompressedResourceOp(ResourceDummyQSVT, {"num_angles": 5, "num_wires": 3}) + Other = CompressedResourceOp(ResourceDummyQFT, {"num_wires": 3}) + + assert CmprssedQSVT1 == CmprssedQSVT2 # compare identical instance + assert CmprssedQSVT1 == CmprssedQSVT3 # compare swapped parameters + assert CmprssedQSVT1 != Other + + @pytest.mark.parametrize("args, repr", zip(compressed_ops_and_params_lst, compressed_op_reprs)) + def test_repr(self, args, repr): + """Test that the repr method behaves as expected.""" + _, op_type, parameters = args + cr_op = CompressedResourceOp(op_type, parameters) + + assert str(cr_op) == repr + + +class TestResources: + """Test the methods and attributes of the Resource class""" + + resource_quantities = ( + Resources(), + Resources(5, 0, defaultdict(int, {})), + Resources(1, 3, defaultdict(int, {"Hadamard": 1, "PauliZ": 2})), + Resources(4, 2, defaultdict(int, {"Hadamard": 1, "CNOT": 1})), + ) + + resource_parameters = ( + (0, 0, defaultdict(int, {})), + (5, 0, defaultdict(int, {})), + (1, 3, defaultdict(int, {"Hadamard": 1, "PauliZ": 2})), + (4, 2, defaultdict(int, {"Hadamard": 1, "CNOT": 1})), + ) + + @pytest.mark.parametrize("r, attribute_tup", zip(resource_quantities, resource_parameters)) + def test_init(self, r, attribute_tup): + """Test that the Resource class is instantiated as expected.""" + num_wires, num_gates, gate_types = attribute_tup + + assert r.num_wires == num_wires + assert r.num_gates == num_gates + assert r.gate_types == gate_types + + expected_results_add_series = ( + Resources(2, 6, defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1})), + Resources(5, 6, defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1})), + Resources( + 2, 9, defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 2, "PauliZ": 2}) + ), + Resources(4, 8, defaultdict(int, {"RZ": 2, "CNOT": 2, "RY": 2, "Hadamard": 2})), + ) + + @pytest.mark.parametrize("in_place", (False, True)) + @pytest.mark.parametrize( + "resource_obj, expected_res_obj", zip(resource_quantities, expected_results_add_series) + ) + def test_add_in_series(self, resource_obj, expected_res_obj, in_place): + """Test the add_in_series function works with Resoruces""" + resource_obj = copy.deepcopy(resource_obj) + other_obj = Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + ) + + resultant_obj = add_in_series(resource_obj, other_obj, in_place=in_place) + assert resultant_obj == expected_res_obj + + if in_place: + assert resultant_obj is resource_obj + + expected_results_add_parallel = ( + Resources(2, 6, defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1})), + Resources(7, 6, defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1})), + Resources( + 3, 9, defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 2, "PauliZ": 2}) + ), + Resources(6, 8, defaultdict(int, {"RZ": 2, "CNOT": 2, "RY": 2, "Hadamard": 2})), + ) + + @pytest.mark.parametrize("in_place", (False, True)) + @pytest.mark.parametrize( + "resource_obj, expected_res_obj", zip(resource_quantities, expected_results_add_parallel) + ) + def test_add_in_parallel(self, resource_obj, expected_res_obj, in_place): + """Test the add_in_parallel function works with Resoruces""" + resource_obj = copy.deepcopy(resource_obj) + other_obj = Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + ) + + resultant_obj = add_in_parallel(resource_obj, other_obj, in_place=in_place) + assert resultant_obj == expected_res_obj + + if in_place: + assert resultant_obj is resource_obj + + expected_results_mul_series = ( + Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + ), + Resources( + num_wires=2, + num_gates=12, + gate_types=defaultdict(int, {"RZ": 4, "CNOT": 2, "RY": 4, "Hadamard": 2}), + ), + Resources( + num_wires=2, + num_gates=18, + gate_types=defaultdict(int, {"RZ": 6, "CNOT": 3, "RY": 6, "Hadamard": 3}), + ), + Resources( + num_wires=2, + num_gates=30, + gate_types=defaultdict(int, {"RZ": 10, "CNOT": 5, "RY": 10, "Hadamard": 5}), + ), + ) + + @pytest.mark.parametrize("in_place", (False, True)) + @pytest.mark.parametrize( + "scalar, expected_res_obj", zip((1, 2, 3, 5), expected_results_mul_series) + ) + def test_mul_in_series(self, scalar, expected_res_obj, in_place): + """Test the mul_in_series function works with Resoruces""" + resource_obj = Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + ) + + resultant_obj = mul_in_series(resource_obj, scalar, in_place=in_place) + assert resultant_obj == expected_res_obj + + if in_place: + assert resultant_obj is resource_obj + assert True + + expected_results_mul_parallel = ( + Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + ), + Resources( + num_wires=4, + num_gates=12, + gate_types=defaultdict(int, {"RZ": 4, "CNOT": 2, "RY": 4, "Hadamard": 2}), + ), + Resources( + num_wires=6, + num_gates=18, + gate_types=defaultdict(int, {"RZ": 6, "CNOT": 3, "RY": 6, "Hadamard": 3}), + ), + Resources( + num_wires=10, + num_gates=30, + gate_types=defaultdict(int, {"RZ": 10, "CNOT": 5, "RY": 10, "Hadamard": 5}), + ), + ) + + @pytest.mark.parametrize("in_place", (False, True)) + @pytest.mark.parametrize( + "scalar, expected_res_obj", zip((1, 2, 3, 5), expected_results_mul_parallel) + ) + def test_mul_in_parallel(self, scalar, expected_res_obj, in_place): + """Test the mul_in_parallel function works with Resoruces""" + resource_obj = Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + ) + + resultant_obj = mul_in_parallel(resource_obj, scalar, in_place=in_place) + assert resultant_obj == expected_res_obj + + if in_place: + assert resultant_obj is resource_obj + assert True + + test_str_data = ( + ("wires: 0\n" + "gates: 0\n" + "gate_types:\n" + "{}"), + ("wires: 5\n" + "gates: 0\n" + "gate_types:\n" + "{}"), + ("wires: 1\n" + "gates: 3\n" + "gate_types:\n" + "{'Hadamard': 1, 'PauliZ': 2}"), + ("wires: 4\n" + "gates: 2\n" + "gate_types:\n" + "{'Hadamard': 1, 'CNOT': 1}"), + ) + + @pytest.mark.parametrize("r, rep", zip(resource_quantities, test_str_data)) + def test_str(self, r, rep): + """Test the string representation of a Resources instance.""" + assert str(r) == rep + + @pytest.mark.parametrize("r, rep", zip(resource_quantities, test_str_data)) + def test_ipython_display(self, r, rep, capsys): + """Test that the ipython display prints the string representation of a Resources instance.""" + r._ipython_display_() # pylint: disable=protected-access + captured = capsys.readouterr() + assert rep in captured.out + + +@pytest.mark.parametrize("in_place", [False, True]) +def test_combine_dict(in_place): + """Test that we can combine dictionaries as expected.""" + d1 = defaultdict(int, {"a": 2, "b": 4, "c": 6}) + d2 = defaultdict(int, {"a": 1, "b": 2, "d": 3}) + + result = _combine_dict(d1, d2, in_place=in_place) + expected = defaultdict(int, {"a": 3, "b": 6, "c": 6, "d": 3}) + + assert result == expected + + if in_place: + assert result is d1 + else: + assert result is not d1 + + +@pytest.mark.parametrize("scalar", (1, 2, 3)) +@pytest.mark.parametrize("in_place", (False, True)) +def test_scale_dict(scalar, in_place): + """Test that we can scale the values of a dictionary as expected.""" + d1 = defaultdict(int, {"a": 2, "b": 4, "c": 6}) + + expected = defaultdict(int, {k: scalar * v for k, v in d1.items()}) + result = _scale_dict(d1, scalar, in_place=in_place) + + assert result == expected + + if in_place: + assert result is d1 + else: + assert result is not d1 diff --git a/pennylane/labs/tests/resource_estimation/test_resource_operator.py b/pennylane/labs/tests/resource_estimation/test_resource_operator.py new file mode 100644 index 00000000000..feb3471acdb --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/test_resource_operator.py @@ -0,0 +1,103 @@ +# Copyright 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. +""" +Test the abstract ResourceOperator class +""" +import pytest + +import pennylane.labs.resource_estimation as re + +# pylint: disable=abstract-class-instantiated,arguments-differ,missing-function-docstring,too-few-public-methods + + +def test_abstract_resource_decomp(): + """Test that the _resource_decomp method is abstract.""" + + class DummyClass(re.ResourceOperator): + """Dummy class for testing""" + + def resource_params(self): + return + + @staticmethod + def resource_rep(): + return + + with pytest.raises( + TypeError, + match="Can't instantiate abstract class DummyClass with abstract method _resource_decomp", + ): + DummyClass() + + +def test_abstract_resource_params(): + """Test that the resource_params method is abstract""" + + class DummyClass(re.ResourceOperator): + """Dummy class for testing""" + + @staticmethod + def _resource_decomp(): + return + + def resource_rep(self): + return + + with pytest.raises( + TypeError, + match="Can't instantiate abstract class DummyClass with abstract method resource_params", + ): + DummyClass() + + +def test_abstract_resource_rep(): + """Test that the resource_rep method is abstract""" + + class DummyClass(re.ResourceOperator): + """Dummy class for testing""" + + @staticmethod + def _resource_decomp(): + return + + def resource_params(self): + return + + with pytest.raises( + TypeError, + match="Can't instantiate abstract class DummyClass with abstract method resource_rep", + ): + DummyClass() + + +def test_set_resources(): + """Test that the resources method can be overriden""" + + class DummyClass(re.ResourceOperator): + """Dummy class for testing""" + + def resource_params(self): + return + + @staticmethod + def resource_rep(): + return + + @staticmethod + def _resource_decomp(): + return + + dummy = DummyClass() + DummyClass.set_resources(lambda _: 5) + assert DummyClass.resources(10) == 5 diff --git a/pennylane/templates/subroutines/trotter.py b/pennylane/templates/subroutines/trotter.py index 2429d43c8bd..2608d21d59f 100644 --- a/pennylane/templates/subroutines/trotter.py +++ b/pennylane/templates/subroutines/trotter.py @@ -253,11 +253,11 @@ def queue(self, context=qml.QueuingManager): context.append(self) return self - def resources(self) -> Resources: - """The resource requirements for a given instance of the Suzuki-Trotter product. + def resources(self) -> qml.resource.Resources: + r"""The resource requirements for a given instance of the Suzuki-Trotter product. Returns: - Resources: The resources for an instance of ``TrotterProduct``. + :class:`~.resource.Resources`: The resources for an instance of ``TrotterProduct``. """ with qml.QueuingManager.stop_recording(): decomp = self.compute_decomposition(*self.parameters, **self.hyperparameters) From 762ba083e9b448977108fbbb9f8066d9ae0d8476 Mon Sep 17 00:00:00 2001 From: "Yushao Chen (Jerry)" Date: Mon, 18 Nov 2024 19:33:02 -0500 Subject: [PATCH 10/35] fix doc (#6597) **Context:** a simplest doc-fix. This fix is important since it is dealing with an example in docs that generates a bug. **Description of the Change:** change the non-existing variable in docstr to correct variable name **Benefits:** Make the example code in docstring runnable **Possible Drawbacks:** No **Related GitHub Issues:** --- pennylane/devices/legacy_facade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/devices/legacy_facade.py b/pennylane/devices/legacy_facade.py index 04f879733c9..ea1f67c70d5 100644 --- a/pennylane/devices/legacy_facade.py +++ b/pennylane/devices/legacy_facade.py @@ -139,7 +139,7 @@ class LegacyDeviceFacade(Device): >>> from pennylane.devices import DefaultMixed, LegacyDeviceFacade >>> legacy_dev = DefaultMixed(wires=2) - >>> new_dev = LegacyDeviceFacade(dev) + >>> new_dev = LegacyDeviceFacade(legacy_dev) >>> new_dev.preprocess() (TransformProgram(legacy_device_batch_transform, legacy_device_expand_fn, defer_measurements), ExecutionConfig(grad_on_execution=None, use_device_gradient=None, use_device_jacobian_product=None, From 0b0f906954c91b29328c27318bb13733774c2551 Mon Sep 17 00:00:00 2001 From: ringo-but-quantum Date: Tue, 19 Nov 2024 09:51:49 +0000 Subject: [PATCH 11/35] [no ci] bump nightly version --- pennylane/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/_version.py b/pennylane/_version.py index c2c60e53969..9fd358cef0f 100644 --- a/pennylane/_version.py +++ b/pennylane/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.40.0-dev16" +__version__ = "0.40.0-dev17" From bbee938a908cdd7f1d79708951ebfb62e9e40187 Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 19 Nov 2024 09:41:59 -0500 Subject: [PATCH 12/35] Resource classes for QFT (#6447) Contains resources classes for QFT and its dependencies [sc-76756] --------- Co-authored-by: Jay Soni Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com> --- doc/releases/changelog-dev.md | 6 +- .../labs/resource_estimation/__init__.py | 43 +++++++- .../labs/resource_estimation/ops/__init__.py | 26 +++++ .../ops/op_math/__init__.py | 16 +++ .../ops/op_math/controlled_ops.py | 76 +++++++++++++ .../resource_estimation/ops/qubit/__init__.py | 24 ++++ .../ops/qubit/non_parametric_ops.py | 101 +++++++++++++++++ .../ops/qubit/parametric_ops_single_qubit.py | 53 +++++++++ .../resource_estimation/resource_operator.py | 4 + .../resource_estimation/templates/__init__.py | 15 +++ .../templates/subroutines.py | 59 ++++++++++ .../ops/op_math/test_controlled_ops.py | 103 ++++++++++++++++++ .../ops/qubit/test_non_parametric_ops.py | 96 ++++++++++++++++ .../qubit/test_parametric_ops_single_qubit.py | 66 +++++++++++ .../templates/test_resource_qft.py | 80 ++++++++++++++ .../test_resource_operator.py | 21 ++++ 16 files changed, 787 insertions(+), 2 deletions(-) create mode 100644 pennylane/labs/resource_estimation/ops/__init__.py create mode 100644 pennylane/labs/resource_estimation/ops/op_math/__init__.py create mode 100644 pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py create mode 100644 pennylane/labs/resource_estimation/ops/qubit/__init__.py create mode 100644 pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py create mode 100644 pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py create mode 100644 pennylane/labs/resource_estimation/templates/__init__.py create mode 100644 pennylane/labs/resource_estimation/templates/subroutines.py create mode 100644 pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py create mode 100644 pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py create mode 100644 pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py create mode 100644 pennylane/labs/tests/resource_estimation/templates/test_resource_qft.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index b80651b8127..0c52d945e23 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -70,6 +70,9 @@ * Added base class `Resources`, `CompressedResourceOp`, `ResourceOperator` for advanced resource estimation. [(#6428)](https://github.com/PennyLaneAI/pennylane/pull/6428) +* Added `ResourceOperator` classes for QFT and all operators in QFT's decomposition. + [(#6447)](https://github.com/PennyLaneAI/pennylane/pull/6447) +

Breaking changes 💔

* Legacy operator arithmetic has been removed. This includes `qml.ops.Hamiltonian`, `qml.operation.Tensor`, @@ -184,6 +187,7 @@ Pietropaolo Frisoni, Austin Huang, Korbinian Kottmann, Christina Lee, +William Maxwell, Andrija Paurevic, Justin Pickering, -Jay Soni, \ No newline at end of file +Jay Soni, diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index 911cd0bf5ff..1a59f926329 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -32,7 +32,48 @@ ~CompressedResourceOp ~ResourceOperator +Operators +~~~~~~~~~ + +.. autosummary:: + :toctree: api + + ~ResourceCNOT + ~ResourceControlledPhaseShift + ~ResourceHadamard + ~ResourceRZ + ~ResourceSWAP + ~ResourceT + +Templates +~~~~~~~~~ + +.. autosummary:: + :toctree: api + + ~ResourceQFT + +Exceptions +~~~~~~~~~~ + +.. autosummary:: + :toctree: api + + ~ResourcesNotDefined """ -from .resource_operator import ResourceOperator +from .resource_operator import ResourceOperator, ResourcesNotDefined from .resource_container import CompressedResourceOp, Resources + +from .ops import ( + ResourceCNOT, + ResourceControlledPhaseShift, + ResourceHadamard, + ResourceRZ, + ResourceSWAP, + ResourceT, +) + +from .templates import ( + ResourceQFT, +) diff --git a/pennylane/labs/resource_estimation/ops/__init__.py b/pennylane/labs/resource_estimation/ops/__init__.py new file mode 100644 index 00000000000..509a0a0230a --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/__init__.py @@ -0,0 +1,26 @@ +# Copyright 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. +r"""This module contains resource operators for PennyLane Operators""" + +from .qubit import ( + ResourceHadamard, + ResourceRZ, + ResourceSWAP, + ResourceT, +) + +from .op_math import ( + ResourceCNOT, + ResourceControlledPhaseShift, +) diff --git a/pennylane/labs/resource_estimation/ops/op_math/__init__.py b/pennylane/labs/resource_estimation/ops/op_math/__init__.py new file mode 100644 index 00000000000..b65c404b698 --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/op_math/__init__.py @@ -0,0 +1,16 @@ +# Copyright 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. +r"""This module contains experimental resource estimation functionality. """ + +from .controlled_ops import * diff --git a/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py b/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py new file mode 100644 index 00000000000..f9f3fddc338 --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py @@ -0,0 +1,76 @@ +# Copyright 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. +r"""Resource operators for controlled operations.""" +from typing import Dict + +import pennylane as qml +import pennylane.labs.resource_estimation as re + +# pylint: disable=arguments-differ,too-many-ancestors + + +class ResourceControlledPhaseShift(qml.ControlledPhaseShift, re.ResourceOperator): + r"""Resource class for the ControlledPhaseShift gate. + + Resources: + The resources come from the following identity expressing Controlled Phase Shift + as a product of Phase Shifts and CNOTs. + + + .. math:: + + CR_\phi(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & e^{i\phi} + \end{bmatrix} = + (R_\phi(\phi/2) \otimes I) \cdot CNOT \cdot (I \otimes R_\phi(-\phi/2)) \cdot CNOT \cdot (I \otimes R_\phi(\phi/2)) + + + """ + + @staticmethod + def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: + gate_types = {} + + cnot = re.ResourceCNOT.resource_rep() + rz = re.ResourceRZ.resource_rep() + + gate_types[cnot] = 2 + gate_types[rz] = 3 + + return gate_types + + def resource_params(self): + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) + + +class ResourceCNOT(qml.CNOT, re.ResourceOperator): + """Resource class for the CNOT gate.""" + + @staticmethod + def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: + raise re.ResourcesNotDefined + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) diff --git a/pennylane/labs/resource_estimation/ops/qubit/__init__.py b/pennylane/labs/resource_estimation/ops/qubit/__init__.py new file mode 100644 index 00000000000..960e4f116c2 --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/qubit/__init__.py @@ -0,0 +1,24 @@ +# Copyright 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. +r"""This module contains experimental resource estimation functionality. """ + +from .non_parametric_ops import ( + ResourceHadamard, + ResourceSWAP, + ResourceT, +) + +from .parametric_ops_single_qubit import ( + ResourceRZ, +) diff --git a/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py b/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py new file mode 100644 index 00000000000..e3eb9fd450e --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py @@ -0,0 +1,101 @@ +# Copyright 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. +r"""Resource operators for non parametric single qubit operations.""" +from typing import Dict + +import pennylane as qml +import pennylane.labs.resource_estimation as re + +# pylint: disable=arguments-differ + + +class ResourceHadamard(qml.Hadamard, re.ResourceOperator): + """Resource class for the Hadamard gate.""" + + @staticmethod + def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: + raise re.ResourcesNotDefined + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) + + +class ResourceSWAP(qml.SWAP, re.ResourceOperator): + r"""Resource class for the SWAP gate. + + Resources: + The resources come from the following identity expressing SWAP as the product of three CNOT gates: + + .. math:: + + SWAP = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & 1 & 0\\ + 0 & 1 & 0 & 0\\ + 0 & 0 & 0 & 1 + \end{bmatrix} + = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0\\ + 0 & 0 & 0 & 1\\ + 0 & 0 & 1 & 0 + \end{bmatrix} + \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 1\\ + 0 & 0 & 1 & 0\\ + 0 & 1 & 0 & 0 + \end{bmatrix} + \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0\\ + 0 & 0 & 0 & 1\\ + 0 & 0 & 1 & 0 + \end{bmatrix}. + + """ + + @staticmethod + def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: + gate_types = {} + cnot = re.ResourceCNOT.resource_rep() + gate_types[cnot] = 3 + + return gate_types + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) + + +class ResourceT(qml.T, re.ResourceOperator): + """Resource class for the T gate.""" + + @staticmethod + def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: + raise re.ResourcesNotDefined + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) diff --git a/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py b/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py new file mode 100644 index 00000000000..b22c5b7ce86 --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py @@ -0,0 +1,53 @@ +# Copyright 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. +r"""Resource operators for parametric single qubit operations.""" +from typing import Dict + +import numpy as np + +import pennylane as qml +import pennylane.labs.resource_estimation as re + +# pylint: disable=arguments-differ + + +def _rotation_resources(epsilon=10e-3): + """An estimate on the number of T gates needed to implement a Pauli rotation. The estimate is taken from https://arxiv.org/abs/1404.5320.""" + gate_types = {} + + num_gates = round(1.149 * np.log2(1 / epsilon) + 9.2) + t = re.ResourceT.resource_rep() + gate_types[t] = num_gates + + return gate_types + + +class ResourceRZ(qml.RZ, re.ResourceOperator): + r"""Resource class for the RZ gate. + + Resources: + The resources are estimated by approximating the gate with a series of T gates. + The estimate is taken from https://arxiv.org/abs/1404.5320. + """ + + @staticmethod + def _resource_decomp(config) -> Dict[re.CompressedResourceOp, int]: + return _rotation_resources(epsilon=config["error_rz"]) + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) diff --git a/pennylane/labs/resource_estimation/resource_operator.py b/pennylane/labs/resource_estimation/resource_operator.py index 8064984ddfc..cc580d6bafd 100644 --- a/pennylane/labs/resource_estimation/resource_operator.py +++ b/pennylane/labs/resource_estimation/resource_operator.py @@ -103,3 +103,7 @@ def resource_rep(cls, *args, **kwargs) -> CompressedResourceOp: def resource_rep_from_op(self) -> CompressedResourceOp: """Returns a compressed representation directly from the operator""" return self.__class__.resource_rep(**self.resource_params()) + + +class ResourcesNotDefined(Exception): + """Exception to be raised when a ``ResourceOperator`` does not implement _resource_decomp""" diff --git a/pennylane/labs/resource_estimation/templates/__init__.py b/pennylane/labs/resource_estimation/templates/__init__.py new file mode 100644 index 00000000000..328ab7a10bf --- /dev/null +++ b/pennylane/labs/resource_estimation/templates/__init__.py @@ -0,0 +1,15 @@ +# Copyright 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. +r"""This module contains resource operators for PennyLane templates. """ +from .subroutines import ResourceQFT diff --git a/pennylane/labs/resource_estimation/templates/subroutines.py b/pennylane/labs/resource_estimation/templates/subroutines.py new file mode 100644 index 00000000000..9970ef940ec --- /dev/null +++ b/pennylane/labs/resource_estimation/templates/subroutines.py @@ -0,0 +1,59 @@ +# Copyright 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. +r"""Resource operators for PennyLane subroutine templates.""" +from typing import Dict + +import pennylane as qml +from pennylane.labs.resource_estimation import ( + CompressedResourceOp, + ResourceControlledPhaseShift, + ResourceHadamard, + ResourceOperator, + ResourceSWAP, +) + +# pylint: disable=arguments-differ + + +class ResourceQFT(qml.QFT, ResourceOperator): + """Resource class for QFT. + + Resources: + The resources are obtained from the standard decomposition of QFT as presented + in (chapter 5) `Nielsen, M.A. and Chuang, I.L. (2011) Quantum Computation and Quantum Information + `_. + + """ + + @staticmethod + def _resource_decomp(num_wires, **kwargs) -> Dict[CompressedResourceOp, int]: + gate_types = {} + + hadamard = ResourceHadamard.resource_rep() + swap = ResourceSWAP.resource_rep() + ctrl_phase_shift = ResourceControlledPhaseShift.resource_rep() + + gate_types[hadamard] = num_wires + gate_types[swap] = num_wires // 2 + gate_types[ctrl_phase_shift] = num_wires * (num_wires - 1) // 2 + + return gate_types + + def resource_params(self) -> dict: + return {"num_wires": len(self.wires)} + + @classmethod + def resource_rep(cls, num_wires) -> CompressedResourceOp: + params = {"num_wires": num_wires} + return CompressedResourceOp(cls, params) diff --git a/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py b/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py new file mode 100644 index 00000000000..07d95ef8c9e --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py @@ -0,0 +1,103 @@ +# Copyright 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 controlled resource operators. +""" +import pytest + +import pennylane.labs.resource_estimation as re + +# pylint: disable=no-self-use, use-implicit-booleaness-not-comparison + + +class TestControlledPhaseShift: + """Test ResourceControlledPhaseShift""" + + params = [(1.2, [0, 1]), (2.4, [2, 3])] + + @pytest.mark.parametrize("phi, wires", params) + def test_resources(self, phi, wires): + """Test the resources method""" + + op = re.ResourceControlledPhaseShift(phi, wires) + + expected = { + re.CompressedResourceOp(re.ResourceCNOT, {}): 2, + re.CompressedResourceOp(re.ResourceRZ, {}): 3, + } + + assert op.resources() == expected + + @pytest.mark.parametrize("phi, wires", params) + def test_resource_params(self, phi, wires): + """Test the resource parameters""" + + op = re.ResourceControlledPhaseShift(phi, wires) + assert op.resource_params() == {} # pylint: disable=use-implicit-booleaness-not-comparison + + @pytest.mark.parametrize("phi, wires", params) + def test_resource_rep(self, phi, wires): + """Test the compressed representation""" + + op = re.ResourceControlledPhaseShift(phi, wires) + expected = re.CompressedResourceOp(re.ResourceControlledPhaseShift, {}) + + assert op.resource_rep() == expected + + @pytest.mark.parametrize("phi, wires", params) + def test_resource_rep_from_op(self, phi, wires): + """Test resource_rep_from_op method""" + + op = re.ResourceControlledPhaseShift(phi, wires) + assert op.resource_rep_from_op() == re.ResourceControlledPhaseShift.resource_rep( + **op.resource_params() + ) + + @pytest.mark.parametrize("phi, wires", params) + def test_resources_from_rep(self, phi, wires): + """Compute the resources from the compressed representation""" + + op = re.ResourceControlledPhaseShift(phi, wires) + + expected = { + re.CompressedResourceOp(re.ResourceCNOT, {}): 2, + re.CompressedResourceOp(re.ResourceRZ, {}): 3, + } + + op_compressed_rep = op.resource_rep_from_op() + op_resource_params = op_compressed_rep.params + op_compressed_rep_type = op_compressed_rep.op_type + + assert op_compressed_rep_type.resources(**op_resource_params) == expected + + +class TestCNOT: + """Test ResourceCNOT""" + + def test_resources(self): + """Test that the resources method is not implemented""" + op = re.ResourceCNOT([0, 1]) + with pytest.raises(re.ResourcesNotDefined): + op.resources() + + def test_resource_rep(self): + """Test the compressed representation""" + op = re.ResourceCNOT([0, 1]) + expected = re.CompressedResourceOp(re.ResourceCNOT, {}) + assert op.resource_rep() == expected + + def test_resource_params(self): + """Test that the resource params are correct""" + op = re.ResourceCNOT([0, 1]) + assert op.resource_params() == {} diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py new file mode 100644 index 00000000000..3ebc3a0ed9c --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py @@ -0,0 +1,96 @@ +# Copyright 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 non parametric resource operators. +""" +import pytest + +import pennylane.labs.resource_estimation as re + +# pylint: disable=no-self-use,use-implicit-booleaness-not-comparison + + +class TestHadamard: + """Tests for ResourceHadamard""" + + def test_resources(self): + """Test that ResourceHadamard does not implement a decomposition""" + op = re.ResourceHadamard(0) + with pytest.raises(re.ResourcesNotDefined): + op.resources() + + def test_resource_params(self): + """Test that the resource params are correct""" + op = re.ResourceHadamard(0) + assert op.resource_params() == {} + + def test_resource_rep(self): + """Test that the compact representation is correct""" + expected = re.CompressedResourceOp(re.ResourceHadamard, {}) + assert re.ResourceHadamard.resource_rep() == expected + + +class TestSWAP: + """Tests for ResourceSWAP""" + + def test_resources(self): + """Test that SWAP decomposes into three CNOTs""" + op = re.ResourceSWAP([0, 1]) + cnot = re.ResourceCNOT.resource_rep() + expected = {cnot: 3} + + assert op.resources() == expected + + def test_resource_params(self): + """Test that the resource params are correct""" + op = re.ResourceSWAP([0, 1]) + assert op.resource_params() == {} + + def test_resource_rep(self): + """Test the compact representation""" + expected = re.CompressedResourceOp(re.ResourceSWAP, {}) + assert re.ResourceSWAP.resource_rep() == expected + + def test_resources_from_rep(self): + """Test that the resources can be computed from the compressed representation""" + + op = re.ResourceSWAP([0, 1]) + cnot = re.ResourceCNOT.resource_rep() + expected = {cnot: 3} + + op_compressed_rep = op.resource_rep_from_op() + op_resource_params = op_compressed_rep.params + op_compressed_rep_type = op_compressed_rep.op_type + + assert op_compressed_rep_type.resources(**op_resource_params) == expected + + +class TestT: + """Tests for ResourceT""" + + def test_resources(self): + """Test that ResourceT does not implement a decomposition""" + op = re.ResourceT(0) + with pytest.raises(re.ResourcesNotDefined): + op.resources() + + def test_resource_params(self): + """Test that the resource params are correct""" + op = re.ResourceT(0) + assert op.resource_params() == {} + + def test_resource_rep(self): + """Test that the compact representation is correct""" + expected = re.CompressedResourceOp(re.ResourceT, {}) + assert re.ResourceT.resource_rep() == expected diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py new file mode 100644 index 00000000000..957282cae1e --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py @@ -0,0 +1,66 @@ +# Copyright 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 parametric single qubit resource operators. +""" +import pytest + +import pennylane.labs.resource_estimation as re +from pennylane.labs.resource_estimation.ops.qubit.parametric_ops_single_qubit import ( + _rotation_resources, +) + +# pylint: disable=no-self-use, use-implicit-booleaness-not-comparison + +params = list(zip([10e-3, 10e-4, 10e-5], [17, 21, 24])) + + +@pytest.mark.parametrize("epsilon, expected", params) +def test_rotation_resources(epsilon, expected): + """Test the hardcoded resources used for RX, RY, RZ""" + gate_types = {} + + t = re.CompressedResourceOp(re.ResourceT, {}) + gate_types[t] = expected + assert gate_types == _rotation_resources(epsilon=epsilon) + + +class TestRZ: + """Test ResourceRZ""" + + @pytest.mark.parametrize("epsilon", [10e-3, 10e-4, 10e-5]) + def test_resources(self, epsilon): + """Test the resources method""" + op = re.ResourceRZ(1.24, wires=0) + config = {"error_rz": epsilon} + assert op.resources(config) == _rotation_resources(epsilon=epsilon) + + def test_resource_rep(self): + """Test the compact representation""" + op = re.ResourceRZ(1.24, wires=0) + expected = re.CompressedResourceOp(re.ResourceRZ, {}) + + assert op.resource_rep() == expected + + def test_resource_params(self): + """Test that the resource params are correct""" + op = re.ResourceRZ(1.24, wires=0) + assert op.resource_params() == {} + + @pytest.mark.parametrize("epsilon", [10e-3, 10e-4, 10e-5]) + def test_resources_from_rep(self, epsilon): + """Test the resources can be obtained from the compact representation""" + config = {"error_rz": epsilon} + expected = _rotation_resources(epsilon=epsilon) + assert re.ResourceRZ.resources(config, **re.ResourceRZ.resource_rep().params) == expected diff --git a/pennylane/labs/tests/resource_estimation/templates/test_resource_qft.py b/pennylane/labs/tests/resource_estimation/templates/test_resource_qft.py new file mode 100644 index 00000000000..8bb4b1ef0ef --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/templates/test_resource_qft.py @@ -0,0 +1,80 @@ +# Copyright 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. +""" +Test the ResourceQFT class +""" +import pytest + +import pennylane.labs.resource_estimation as re + +# pylint: disable=no-self-use + + +class TestQFT: + """Test the ResourceQFT class""" + + @pytest.mark.parametrize( + "num_wires, num_hadamard, num_swap, num_ctrl_phase_shift", + [ + (1, 1, 0, 0), + (2, 2, 1, 1), + (3, 3, 1, 3), + (4, 4, 2, 6), + ], + ) + def test_resources(self, num_wires, num_hadamard, num_swap, num_ctrl_phase_shift): + """Test the resources method returns the correct dictionary""" + hadamard = re.CompressedResourceOp(re.ResourceHadamard, {}) + swap = re.CompressedResourceOp(re.ResourceSWAP, {}) + ctrl_phase_shift = re.CompressedResourceOp(re.ResourceControlledPhaseShift, {}) + + expected = {hadamard: num_hadamard, swap: num_swap, ctrl_phase_shift: num_ctrl_phase_shift} + + assert re.ResourceQFT.resources(num_wires) == expected + + @pytest.mark.parametrize("wires", [range(1), range(2), range(3), range(4)]) + def test_resource_params(self, wires): + """Test that the resource params are correct""" + op = re.ResourceQFT(wires) + assert op.resource_params() == {"num_wires": len(wires)} + + @pytest.mark.parametrize("num_wires", [1, 2, 3, 4]) + def test_resource_rep(self, num_wires): + """Test the resource_rep returns the correct CompressedResourceOp""" + + expected = re.CompressedResourceOp(re.ResourceQFT, {"num_wires": num_wires}) + assert re.ResourceQFT.resource_rep(num_wires) == expected + + @pytest.mark.parametrize( + "num_wires, num_hadamard, num_swap, num_ctrl_phase_shift", + [ + (1, 1, 0, 0), + (2, 2, 1, 1), + (3, 3, 1, 3), + (4, 4, 2, 6), + ], + ) + def test_resources_from_rep(self, num_wires, num_hadamard, num_swap, num_ctrl_phase_shift): + """Test that computing the resources from a compressed representation works""" + + hadamard = re.CompressedResourceOp(re.ResourceHadamard, {}) + swap = re.CompressedResourceOp(re.ResourceSWAP, {}) + ctrl_phase_shift = re.CompressedResourceOp(re.ResourceControlledPhaseShift, {}) + + expected = {hadamard: num_hadamard, swap: num_swap, ctrl_phase_shift: num_ctrl_phase_shift} + + rep = re.ResourceQFT.resource_rep(num_wires) + actual = rep.op_type.resources(**rep.params) + + assert actual == expected diff --git a/pennylane/labs/tests/resource_estimation/test_resource_operator.py b/pennylane/labs/tests/resource_estimation/test_resource_operator.py index feb3471acdb..f88026f86b7 100644 --- a/pennylane/labs/tests/resource_estimation/test_resource_operator.py +++ b/pennylane/labs/tests/resource_estimation/test_resource_operator.py @@ -101,3 +101,24 @@ def _resource_decomp(): dummy = DummyClass() DummyClass.set_resources(lambda _: 5) assert DummyClass.resources(10) == 5 + + +def test_resource_rep_from_op(): + """Test that the resource_rep_from_op method is the composition of resource_params and resource_rep""" + + class DummyClass(re.ResourceQFT, re.ResourceOperator): + """Dummy class for testing""" + + @staticmethod + def _resource_decomp(): + return + + def resource_params(self): + return {"foo": 1, "bar": 2} + + @classmethod + def resource_rep(cls, foo, bar): + return re.CompressedResourceOp(cls, {"foo": foo, "bar": bar}) + + op = DummyClass() + assert op.resource_rep_from_op() == op.__class__.resource_rep(**op.resource_params()) From 014c4d39c43af5e91ef412c52e21e8f73c1d8449 Mon Sep 17 00:00:00 2001 From: Andrija Paurevic <46359773+andrijapau@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:52:53 -0500 Subject: [PATCH 13/35] Apply gradient transforms after user transforms (#6590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** Currently, user-defined transforms are applied after gradient preprocessing. This ordering is unintuitive and introduces several issues, - tapes may contain unsupported operations for gradient transforms - user transforms cannot act on initial higher-order tape structures, as these are decomposed away during preprocessing Consider this circuit, ```python import pennylane as qml dev = qml.device("default.qubit") @qml.transforms.merge_rotations @qml.transforms.cancel_inverses @qml.qnode(dev, diff_method="parameter-shift") def decorated_circuit(weights): qml.RX(weights[0], wires=0) qml.RX(weights[1], wires=0) qml.RX(weights[2], wires=0) return qml.probs(wires=0) weights = qml.numpy.array([0.1, 0.2, 0.3], requires_grad=True) decorated_circuit(weights) ``` Before this fix, the transform program in `execute` was, ``` TransformProgram(_expand_transform_param_shift, cancel_inverses, merge_rotations) ``` **Description of the Change:** This change proposes applying gradient preprocessing *after* user transforms. This will ensure that user transforms can work on the initial tape structure without decomposition interference and that gradient transforms only receive preprocessed tapes with supported operations. This results in the *correct* transform program, ``` TransformProgram(cancel_inverses, merge_rotations, _expand_transform_param_shift) ``` **Benefits:** - **Logical Transform Flow**: Ensures user transforms work as expected on initial structures, like embeddings or entangling layers. - **Operation Compatibility**: Guarantees that gradient transforms only process compatible operations. - **Improved User Experience**: Aligns transform order with user expectations and avoids confusion. **Possible Drawbacks:** - **Breaking Change**: This change could disrupt existing setups. We’ll need to: - Issue a breaking change alert and be prepared to revert if needed. - Coordinate across teams to validate against potential issues with Lightning and Catalyst. - Update affected functions (`construct_batch`, `get_transform_program`) and resolve any failing tests or cross-project dependencies. [sc-72075] --- doc/releases/changelog-dev.md | 3 +++ pennylane/workflow/qnode.py | 14 +++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 0c52d945e23..ba413ff767a 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -75,6 +75,9 @@

Breaking changes 💔

+* Gradient transforms are now applied after the user's transform program. + [(#6590)](https://github.com/PennyLaneAI/pennylane/pull/6590) + * Legacy operator arithmetic has been removed. This includes `qml.ops.Hamiltonian`, `qml.operation.Tensor`, `qml.operation.enable_new_opmath`, `qml.operation.disable_new_opmath`, and `qml.operation.convert_to_legacy_H`. Note that `qml.Hamiltonian` will continue to dispatch to `qml.ops.LinearCombination`. For more information, diff --git a/pennylane/workflow/qnode.py b/pennylane/workflow/qnode.py index 9e921a14428..f7007df30b7 100644 --- a/pennylane/workflow/qnode.py +++ b/pennylane/workflow/qnode.py @@ -917,6 +917,13 @@ def _execution_component(self, args: tuple, kwargs: dict) -> qml.typing.Result: full_transform_program = qml.transforms.core.TransformProgram(self.transform_program) inner_transform_program = qml.transforms.core.TransformProgram() + # Add the gradient expand to the program if necessary + if getattr(gradient_fn, "expand_transform", False): + full_transform_program.add_transform( + qml.transform(gradient_fn.expand_transform), + **gradient_kwargs, + ) + config = _make_execution_config(self, gradient_fn, mcm_config) device_transform_program, config = self.device.preprocess(execution_config=config) @@ -925,13 +932,6 @@ def _execution_component(self, args: tuple, kwargs: dict) -> qml.typing.Result: else: inner_transform_program += device_transform_program - # Add the gradient expand to the program if necessary - if getattr(gradient_fn, "expand_transform", False): - full_transform_program.insert_front_transform( - qml.transform(gradient_fn.expand_transform), - **gradient_kwargs, - ) - # Calculate the classical jacobians if necessary full_transform_program.set_classical_component(self, args, kwargs) _prune_dynamic_transform(full_transform_program, inner_transform_program) From b7ebbccdd6e20c5a6caa226f47a23486902f84a4 Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:26:26 -0500 Subject: [PATCH 14/35] Allow accessing datasets uploaded using Tensor/legacy Hamiltonian (#6602) **Context:** We were already converting to `Prod` and `LinearCombination` when downloading datasets that were uploaded using legacy opmath, but we were relying on using the `Hamiltonian` and `Tensor` class in the supported ops mapping to recognize them and know how to handle them. As a result, when we removed legacy opmath, we removed the context needed to download legacy datasets. **Description of the Change:** We add them as keys in the ops dictionary and map them to `LinearCombination` and `Prod`. **Benefits:** The datasets will work [sc-78651][sc-77523] --- doc/releases/changelog-dev.md | 1 + pennylane/data/attributes/operator/operator.py | 6 ++++-- tests/data/attributes/operator/test_operator.py | 10 ++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index ba413ff767a..f6abe3c5002 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -83,6 +83,7 @@ Note that `qml.Hamiltonian` will continue to dispatch to `qml.ops.LinearCombination`. For more information, check out the [updated operator troubleshooting page](https://docs.pennylane.ai/en/stable/news/new_opmath.html). [(#6548)](https://github.com/PennyLaneAI/pennylane/pull/6548) + [(#6602)](https://github.com/PennyLaneAI/pennylane/pull/6602) * The developer-facing `qml.utils` module has been removed. Specifically, the following 4 sets of functions have been either moved or removed[(#6588)](https://github.com/PennyLaneAI/pennylane/pull/6588): diff --git a/pennylane/data/attributes/operator/operator.py b/pennylane/data/attributes/operator/operator.py index c9685640df7..82642ad71dc 100644 --- a/pennylane/data/attributes/operator/operator.py +++ b/pennylane/data/attributes/operator/operator.py @@ -249,7 +249,6 @@ def _hdf5_to_ops(self, bind: HDF5Group) -> list[Operator]: with qml.QueuingManager.stop_recording(): for i, op_class_name in enumerate(op_class_names): op_key = f"op_{i}" - op_cls = self._supported_ops_dict()[op_class_name] if op_cls is qml.ops.LinearCombination: ops.append( @@ -283,4 +282,7 @@ def _hdf5_to_ops(self, bind: HDF5Group) -> list[Operator]: @lru_cache(1) def _supported_ops_dict(cls) -> dict[str, Type[Operator]]: """Returns a dict mapping ``Operator`` subclass names to the class.""" - return {op.__name__: op for op in cls.supported_ops()} + ops_dict = {op.__name__: op for op in cls.supported_ops()} + ops_dict["Hamiltonian"] = qml.ops.LinearCombination + ops_dict["Tensor"] = qml.ops.Prod + return ops_dict diff --git a/tests/data/attributes/operator/test_operator.py b/tests/data/attributes/operator/test_operator.py index 29b28363dc5..2c346b87977 100644 --- a/tests/data/attributes/operator/test_operator.py +++ b/tests/data/attributes/operator/test_operator.py @@ -243,3 +243,13 @@ class NotSupported(Operator): # pylint: disable=too-few-public-methods, unneces TypeError, match="Serialization of operator type 'NotSupported' is not supported" ): DatasetOperator(NotSupported(1)) + + +def test_retrieve_operator_from_loaded_data(): + """Test that uploaded data can be downloaded and used to retrieve an + operation representing the Hamiltonian""" + + h2 = qml.data.load("qchem", molname="H2", bondlength=0.742, basis="STO-3G")[0] + H = h2.hamiltonian + + assert isinstance(H, qml.ops.LinearCombination) From 061c855902bce3be135436675e2d57161bea970a Mon Sep 17 00:00:00 2001 From: Mudit Pandey Date: Tue, 19 Nov 2024 15:56:21 -0500 Subject: [PATCH 15/35] Create global fixture for toggling capture in program capture tests (#6605) As name says. Most test files create their own fixture for doing this. I updated the logic to have a single fixture in `conftest,py` that test files can now use instead. [sc-78687] --- tests/capture/test_base_interpreter.py | 10 +--------- tests/capture/test_capture_cond.py | 12 ++---------- tests/capture/test_capture_diff.py | 9 +-------- tests/capture/test_capture_for_loop.py | 16 +++------------- tests/capture/test_capture_mid_measure.py | 11 ++--------- tests/capture/test_capture_module.py | 10 +--------- tests/capture/test_capture_qnode.py | 10 +--------- tests/capture/test_capture_while_loop.py | 10 +--------- tests/capture/test_make_plxpr.py | 9 --------- tests/capture/test_measurements_capture.py | 9 +-------- tests/capture/test_meta_type.py | 11 +---------- tests/capture/test_nested_plxpr.py | 9 +-------- tests/capture/test_operators.py | 9 +-------- tests/capture/test_templates.py | 9 +-------- tests/conftest.py | 8 ++++++++ 15 files changed, 25 insertions(+), 127 deletions(-) diff --git a/tests/capture/test_base_interpreter.py b/tests/capture/test_base_interpreter.py index f977ff154ef..4572b78eb01 100644 --- a/tests/capture/test_base_interpreter.py +++ b/tests/capture/test_base_interpreter.py @@ -32,15 +32,7 @@ while_loop_prim, ) -pytestmark = pytest.mark.jax - - -@pytest.fixture(autouse=True) -def enable_disable_plxpr(): - """Enable and disable the PennyLane JAX capture context manager.""" - qml.capture.enable() - yield - qml.capture.disable() +pytestmark = [pytest.mark.jax, pytest.mark.usefixtures("enable_disable_plxpr")] class SimplifyInterpreter(PlxprInterpreter): diff --git a/tests/capture/test_capture_cond.py b/tests/capture/test_capture_cond.py index 56ece6b7619..03d89570131 100644 --- a/tests/capture/test_capture_cond.py +++ b/tests/capture/test_capture_cond.py @@ -15,7 +15,7 @@ Tests for capturing conditionals into jaxpr. """ -# pylint: disable=redefined-outer-name, too-many-arguments +# pylint: disable=redefined-outer-name, too-many-arguments, too-many-positional-arguments # pylint: disable=no-self-use import numpy as np @@ -24,7 +24,7 @@ import pennylane as qml from pennylane.ops.op_math.condition import CondCallable, ConditionalTransformError -pytestmark = pytest.mark.jax +pytestmark = [pytest.mark.jax, pytest.mark.usefixtures("enable_disable_plxpr")] jax = pytest.importorskip("jax") @@ -32,14 +32,6 @@ from pennylane.capture.primitives import cond_prim # pylint: disable=wrong-import-position -@pytest.fixture(autouse=True) -def enable_disable_plxpr(): - """Enable and disable the PennyLane JAX capture context manager.""" - qml.capture.enable() - yield - qml.capture.disable() - - @pytest.fixture def testing_functions(): """Returns a set of functions for testing.""" diff --git a/tests/capture/test_capture_diff.py b/tests/capture/test_capture_diff.py index 07596e01b47..c6a210c7d8f 100644 --- a/tests/capture/test_capture_diff.py +++ b/tests/capture/test_capture_diff.py @@ -19,7 +19,7 @@ import pennylane as qml from pennylane.capture import qnode_prim -pytestmark = pytest.mark.jax +pytestmark = [pytest.mark.jax, pytest.mark.usefixtures("enable_disable_plxpr")] jax = pytest.importorskip("jax") @@ -31,13 +31,6 @@ jnp = jax.numpy -@pytest.fixture(autouse=True) -def enable_disable_plxpr(): - qml.capture.enable() - yield - qml.capture.disable() - - class TestExceptions: """Test that expected exceptions are correctly raised.""" diff --git a/tests/capture/test_capture_for_loop.py b/tests/capture/test_capture_for_loop.py index 6d955d4635d..7ba8e098db1 100644 --- a/tests/capture/test_capture_for_loop.py +++ b/tests/capture/test_capture_for_loop.py @@ -15,17 +15,15 @@ Tests for capturing for loops into jaxpr. """ -# pylint: disable=no-value-for-parameter -# pylint: disable=too-few-public-methods -# pylint: disable=too-many-arguments -# pylint: disable=no-self-use +# pylint: disable=no-value-for-parameter, too-few-public-methods, no-self-use +# pylint: disable=too-many-positional-arguments, too-many-arguments import numpy as np import pytest import pennylane as qml -pytestmark = pytest.mark.jax +pytestmark = [pytest.mark.jax, pytest.mark.usefixtures("enable_disable_plxpr")] jax = pytest.importorskip("jax") @@ -33,14 +31,6 @@ from pennylane.capture.primitives import for_loop_prim # pylint: disable=wrong-import-position -@pytest.fixture(autouse=True) -def enable_disable_plxpr(): - """Enable and disable the PennyLane JAX capture context manager.""" - qml.capture.enable() - yield - qml.capture.disable() - - class TestCaptureForLoop: """Tests for capturing for loops into jaxpr.""" diff --git a/tests/capture/test_capture_mid_measure.py b/tests/capture/test_capture_mid_measure.py index a44fd9e342c..fdd69d6527c 100644 --- a/tests/capture/test_capture_mid_measure.py +++ b/tests/capture/test_capture_mid_measure.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Tests for capturing mid-circuit measurements.""" -# pylint: disable=ungrouped-imports, wrong-import-order, wrong-import-position +# pylint: disable=ungrouped-imports, wrong-import-order, wrong-import-position, too-many-positional-arguments import pytest import pennylane as qml @@ -23,14 +23,7 @@ from pennylane.capture.primitives import AbstractOperator -pytestmark = pytest.mark.jax - - -@pytest.fixture(autouse=True) -def enable_disable_plxpr(): - qml.capture.enable() - yield - qml.capture.disable() +pytestmark = [pytest.mark.jax, pytest.mark.usefixtures("enable_disable_plxpr")] @pytest.mark.unit diff --git a/tests/capture/test_capture_module.py b/tests/capture/test_capture_module.py index 93ba81176f2..f2d65f0f580 100644 --- a/tests/capture/test_capture_module.py +++ b/tests/capture/test_capture_module.py @@ -20,15 +20,7 @@ jax = pytest.importorskip("jax") -pytestmark = pytest.mark.jax - - -@pytest.fixture(autouse=True) -def enable_disable_plxpr(): - """enable and disable capture around each test.""" - qml.capture.enable() - yield - qml.capture.disable() +pytestmark = [pytest.mark.jax, pytest.mark.usefixtures("enable_disable_plxpr")] def test_no_attribute_available(): diff --git a/tests/capture/test_capture_qnode.py b/tests/capture/test_capture_qnode.py index 30abc1dc851..41b1854956f 100644 --- a/tests/capture/test_capture_qnode.py +++ b/tests/capture/test_capture_qnode.py @@ -22,7 +22,7 @@ import pennylane as qml -pytestmark = pytest.mark.jax +pytestmark = [pytest.mark.jax, pytest.mark.usefixtures("enable_disable_plxpr")] jax = pytest.importorskip("jax") @@ -30,14 +30,6 @@ from pennylane.capture.primitives import qnode_prim # pylint: disable=wrong-import-position -@pytest.fixture(autouse=True) -def enable_disable_plxpr(): - """Enable and disable the PennyLane JAX capture context around each test.""" - qml.capture.enable() - yield - qml.capture.disable() - - def get_qnode_output_eqns(jaxpr): """Extracts equations related to QNode outputs in the given JAX expression (jaxpr). diff --git a/tests/capture/test_capture_while_loop.py b/tests/capture/test_capture_while_loop.py index 923eddd31bf..1d45a104d44 100644 --- a/tests/capture/test_capture_while_loop.py +++ b/tests/capture/test_capture_while_loop.py @@ -20,21 +20,13 @@ import pennylane as qml -pytestmark = pytest.mark.jax +pytestmark = [pytest.mark.jax, pytest.mark.usefixtures("enable_disable_plxpr")] jax = pytest.importorskip("jax") from pennylane.capture.primitives import while_loop_prim # pylint: disable=wrong-import-position -@pytest.fixture(autouse=True) -def enable_disable_plxpr(): - """Enable and disable the PennyLane JAX capture context manager.""" - qml.capture.enable() - yield - qml.capture.disable() - - class TestCaptureWhileLoop: """Tests for capturing for while loops into jaxpr.""" diff --git a/tests/capture/test_make_plxpr.py b/tests/capture/test_make_plxpr.py index bff656121ea..8adc86b67bb 100644 --- a/tests/capture/test_make_plxpr.py +++ b/tests/capture/test_make_plxpr.py @@ -30,15 +30,6 @@ from pennylane.capture import make_plxpr # pylint: disable=wrong-import-position -@pytest.fixture -def enable_disable_plxpr(): - # if 'noautofixt' in request.keywords: - # return - qml.capture.enable() - yield - qml.capture.disable() - - def test_error_is_raised_with_capture_disabled(): dev = qml.device("default.qubit", wires=1) diff --git a/tests/capture/test_measurements_capture.py b/tests/capture/test_measurements_capture.py index 21c186fb98c..deb8edd518f 100644 --- a/tests/capture/test_measurements_capture.py +++ b/tests/capture/test_measurements_capture.py @@ -41,14 +41,7 @@ AbstractMeasurement, ) -pytestmark = pytest.mark.jax - - -@pytest.fixture(autouse=True) -def enable_disable_plxpr(): - qml.capture.enable() - yield - qml.capture.disable() +pytestmark = [pytest.mark.jax, pytest.mark.usefixtures("enable_disable_plxpr")] def _get_shapes_for(*measurements, shots=qml.measurements.Shots(None), num_device_wires=0): diff --git a/tests/capture/test_meta_type.py b/tests/capture/test_meta_type.py index 768627cc17b..3977a3a1cd8 100644 --- a/tests/capture/test_meta_type.py +++ b/tests/capture/test_meta_type.py @@ -19,20 +19,11 @@ # pylint: disable=protected-access, undefined-variable import pytest -import pennylane as qml from pennylane.capture.capture_meta import CaptureMeta jax = pytest.importorskip("jax") -pytestmark = pytest.mark.jax - - -@pytest.fixture(autouse=True) -def enable_disable_plxpr(): - """enable and disable capture around each test.""" - qml.capture.enable() - yield - qml.capture.disable() +pytestmark = [pytest.mark.jax, pytest.mark.usefixtures("enable_disable_plxpr")] def test_custom_capture_meta(): diff --git a/tests/capture/test_nested_plxpr.py b/tests/capture/test_nested_plxpr.py index 2580dfc738e..8f4715a10a5 100644 --- a/tests/capture/test_nested_plxpr.py +++ b/tests/capture/test_nested_plxpr.py @@ -19,7 +19,7 @@ import pennylane as qml -pytestmark = pytest.mark.jax +pytestmark = [pytest.mark.jax, pytest.mark.usefixtures("enable_disable_plxpr")] jax = pytest.importorskip("jax") @@ -27,13 +27,6 @@ from pennylane.capture.primitives import adjoint_transform_prim, ctrl_transform_prim -@pytest.fixture(autouse=True) -def enable_disable_plxpr(): - qml.capture.enable() - yield - qml.capture.disable() - - class TestAdjointQfunc: """Tests for the adjoint transform.""" diff --git a/tests/capture/test_operators.py b/tests/capture/test_operators.py index 530c43a289f..52f91f274ff 100644 --- a/tests/capture/test_operators.py +++ b/tests/capture/test_operators.py @@ -23,14 +23,7 @@ from pennylane.capture.primitives import AbstractOperator # pylint: disable=wrong-import-position -pytestmark = pytest.mark.jax - - -@pytest.fixture(autouse=True) -def enable_disable_plxpr(): - qml.capture.enable() - yield - qml.capture.disable() +pytestmark = [pytest.mark.jax, pytest.mark.usefixtures("enable_disable_plxpr")] def test_abstract_operator(): diff --git a/tests/capture/test_templates.py b/tests/capture/test_templates.py index 784a6f248b4..eecda4069e9 100644 --- a/tests/capture/test_templates.py +++ b/tests/capture/test_templates.py @@ -27,17 +27,10 @@ jax = pytest.importorskip("jax") jnp = jax.numpy -pytestmark = pytest.mark.jax +pytestmark = [pytest.mark.jax, pytest.mark.usefixtures("enable_disable_plxpr")] original_op_bind_code = qml.operation.Operator._primitive_bind_call.__code__ -@pytest.fixture(autouse=True) -def enable_disable_plxpr(): - qml.capture.enable() - yield - qml.capture.disable() - - unmodified_templates_cases = [ (qml.AmplitudeEmbedding, (jnp.array([1.0, 0.0]), 2), {}), (qml.AmplitudeEmbedding, (jnp.eye(4)[2], [2, 3]), {"normalize": False}), diff --git a/tests/conftest.py b/tests/conftest.py index a01254df37b..eee8d60ae4e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -160,6 +160,14 @@ def test_something(seed): return original_seed +@pytest.fixture(scope="function") +def enable_disable_plxpr(): + """enable and disable capture around each test.""" + qml.capture.enable() + yield + qml.capture.disable() + + ####################################################################### try: From e2c729654fd22e423e88593777067d922ebc4727 Mon Sep 17 00:00:00 2001 From: ringo-but-quantum Date: Wed, 20 Nov 2024 09:51:48 +0000 Subject: [PATCH 16/35] [no ci] bump nightly version --- pennylane/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/_version.py b/pennylane/_version.py index 9fd358cef0f..3b74a713655 100644 --- a/pennylane/_version.py +++ b/pennylane/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.40.0-dev17" +__version__ = "0.40.0-dev18" From c9af833869b43d6ceaff853c4c66b3b6856dc9c1 Mon Sep 17 00:00:00 2001 From: Pietropaolo Frisoni Date: Wed, 20 Nov 2024 10:19:49 -0500 Subject: [PATCH 17/35] Pinning quimb in CI (#6612) ### Before submitting Please complete the following checklist when submitting a PR: - [ ] All new features must include a unit test. If you've fixed a bug or added code that should be tested, add a test to the test directory! - [ ] All new functions and code must be clearly commented and documented. If you do make documentation changes, make sure that the docs build and render correctly by running `make docs`. - [ ] Ensure that the test suite passes, by running `make test`. - [ ] Add a new entry to the `doc/releases/changelog-dev.md` file, summarizing the change, and including a link back to the PR. - [ ] The PennyLane source code conforms to [PEP8 standards](https://www.python.org/dev/peps/pep-0008/). We check all of our code against [Pylint](https://www.pylint.org/). To lint modified files, simply `pip install pylint`, and then run `pylint pennylane/path/to/file.py`. When all the above are checked, delete everything above the dashed line and fill in the pull request template. ------------------------------------------------------------------------------------------------------------ **Context:** **Description of the Change:** **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** --- .github/workflows/interface-unit-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/interface-unit-tests.yml b/.github/workflows/interface-unit-tests.yml index ad7356b0a07..b3489b40b53 100644 --- a/.github/workflows/interface-unit-tests.yml +++ b/.github/workflows/interface-unit-tests.yml @@ -465,7 +465,7 @@ jobs: pytest_markers: external pytest_additional_args: -W ${{ inputs.python_warning_level }} ${{ inputs.python_warning_level != 'default' && '--continue-on-collection-errors' || '' }} additional_pip_packages: | - pyzx matplotlib stim quimb mitiq ply + pyzx matplotlib stim quimb==1.8.4 mitiq ply git+https://github.com/PennyLaneAI/pennylane-qiskit.git@master ${{ needs.default-dependency-versions.outputs.jax-version }} ${{ needs.default-dependency-versions.outputs.tensorflow-version }} From eb37a95bc111d0059599aa4c4c738be18210225e Mon Sep 17 00:00:00 2001 From: Pietropaolo Frisoni Date: Wed, 20 Nov 2024 11:51:40 -0500 Subject: [PATCH 18/35] [BugFix] `KeyError` when running `QNode(qfun, dev)(graph)` with graph an instance of `networkx` Graph object (#6600) **Context:** Issue #6585 indicates that we don't currently allow arguments with types defined in libraries not listed in the supported interfaces. In #6225, the convention of treating internally as `interface=None` parameters of the `numpy` interface has been introduced. The idea of this PR is to allow non-trainable `qnode` inputs to "just work" as arguments, without any special consideration or treatment. We want to rely on the default `numpy` interface for libraries we know nothing about. **Description of the Change:** By default, we set `None` as the value of the interface if the latter is not found in the list of supported interfaces. According to the convention introduced in #6225, this should automatically map the interface to `numpy`. **Benefits:** Now the `qnode` accepts arguments with types defined in libraries that are not necessarily in the list of supported interfaces, such as the `Graph` class defined in `networkx`. **Possible Drawbacks:** None that I can think of. **Related GitHub Issues:** #6585 **Related Shortcut Stories:** [sc-78344] --------- Co-authored-by: Christina Lee --- doc/releases/changelog-dev.md | 9 +++++--- pennylane/workflow/execution.py | 4 ++-- pennylane/workflow/qnode.py | 6 ++--- tests/test_qnode.py | 41 +++++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index f6abe3c5002..02b304de38e 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -23,7 +23,6 @@ * Added `qml.devices.qubit_mixed` module for mixed-state qubit device support [(#6379)](https://github.com/PennyLaneAI/pennylane/pull/6379). This module introduces an `apply_operation` helper function that features: - * Two density matrix contraction methods using `einsum` and `tensordot` * Optimized handling of special cases including: Diagonal operators, Identity operators, CX (controlled-X), Multi-controlled X gates, Grover operators @@ -132,7 +131,6 @@ following 4 sets of functions have been either moved or removed[(#6588)](https:/ * The `expand_depth` argument for `qml.compile` has been removed. [(#6531)](https://github.com/PennyLaneAI/pennylane/pull/6531) - * The `qml.shadows.shadow_expval` transform has been removed. Instead, please use the `qml.shadow_expval` measurement process. [(#6530)](https://github.com/PennyLaneAI/pennylane/pull/6530) @@ -165,14 +163,19 @@ same information. [(#6549)](https://github.com/PennyLaneAI/pennylane/pull/6549)

Documentation 📝

+ * Add reporting of test warnings as failures. [(#6217)](https://github.com/PennyLaneAI/pennylane/pull/6217) -* Add a warning message to Gradients and training documentation about ComplexWarnings +* Add a warning message to Gradients and training documentation about ComplexWarnings. [(#6543)](https://github.com/PennyLaneAI/pennylane/pull/6543)

Bug fixes 🐛

+* `qml.QNode` now accepts arguments with types defined in libraries that are not necessarily + in the list of supported interfaces, such as the `Graph` class defined in `networkx`. + [(#6600)](https://github.com/PennyLaneAI/pennylane/pull/6600) + * `qml.math.get_deep_interface` now works properly for autograd arrays. [(#6557)](https://github.com/PennyLaneAI/pennylane/pull/6557) diff --git a/pennylane/workflow/execution.py b/pennylane/workflow/execution.py index deb58d12634..63f33164cd5 100644 --- a/pennylane/workflow/execution.py +++ b/pennylane/workflow/execution.py @@ -19,7 +19,7 @@ # pylint: disable=import-outside-toplevel,too-many-branches,not-callable,unexpected-keyword-arg # pylint: disable=unused-argument,unnecessary-lambda-assignment,inconsistent-return-statements # pylint: disable=invalid-unary-operand-type,isinstance-second-argument-not-valid-type -# pylint: disable=too-many-arguments,too-many-statements,function-redefined,too-many-function-args +# pylint: disable=too-many-arguments,too-many-statements,function-redefined,too-many-function-args,too-many-positional-arguments import inspect import logging @@ -269,7 +269,7 @@ def _get_interface_name(tapes, interface): params.extend(tape.get_parameters(trainable_only=False)) interface = qml.math.get_interface(*params) if interface != "numpy": - interface = INTERFACE_MAP[interface] + interface = INTERFACE_MAP.get(interface, None) if interface == "tf" and _use_tensorflow_autograph(): interface = "tf-autograph" if interface == "jax": diff --git a/pennylane/workflow/qnode.py b/pennylane/workflow/qnode.py index f7007df30b7..c2ec015729f 100644 --- a/pennylane/workflow/qnode.py +++ b/pennylane/workflow/qnode.py @@ -14,7 +14,7 @@ """ This module contains the QNode class and qnode decorator. """ -# pylint: disable=too-many-instance-attributes,too-many-arguments,protected-access,unnecessary-lambda-assignment, too-many-branches, too-many-statements, unused-argument +# pylint: disable=too-many-instance-attributes,too-many-arguments,protected-access,unnecessary-lambda-assignment, too-many-branches, too-many-statements, unused-argument, too-many-positional-arguments import copy import functools import inspect @@ -75,7 +75,7 @@ def _convert_to_interface(res, interface): "tf-autograph": "tensorflow", } - interface_name = interface_conversion_map[interface] + interface_name = interface_conversion_map.get(interface, None) return qml.math.asarray(res, like=interface_name) @@ -983,7 +983,7 @@ def _impl_call(self, *args, **kwargs) -> qml.typing.Result: else qml.math.get_interface(*args, *list(kwargs.values())) ) if interface != "numpy": - interface = INTERFACE_MAP[interface] + interface = INTERFACE_MAP.get(interface, None) self._interface = interface try: diff --git a/tests/test_qnode.py b/tests/test_qnode.py index 7886e73e94d..498c51b9f23 100644 --- a/tests/test_qnode.py +++ b/tests/test_qnode.py @@ -1030,6 +1030,47 @@ def circuit(x): res = circuit(x) assert qml.math.get_interface(res) == "numpy" + def test_qnode_default_interface(self): + """Tests that the default interface is set correctly for a QNode.""" + + # pylint: disable=import-outside-toplevel + import networkx as nx + + @qml.qnode(qml.device("default.qubit")) + def circuit(graph: nx.Graph): + for a in graph.nodes: + qml.Hadamard(wires=a) + for a, b in graph.edges: + qml.CZ(wires=[a, b]) + return qml.expval(qml.PauliZ(0)) + + graph = nx.complete_graph(3) + res = circuit(graph) + assert qml.math.get_interface(res) == "numpy" + + def test_qscript_default_interface(self): + """Tests that the default interface is set correctly for a QuantumScript.""" + + # pylint: disable=import-outside-toplevel + import networkx as nx + + dev = qml.device("default.qubit") + + # pylint: disable=too-few-public-methods + class DummyCustomGraphOp(qml.operation.Operation): + """Dummy custom operation for testing purposes.""" + + def __init__(self, graph: nx.Graph): + super().__init__(graph, wires=graph.nodes) + + def decomposition(self) -> list: + return [] + + graph = nx.complete_graph(3) + tape = qml.tape.QuantumScript([DummyCustomGraphOp(graph)], [qml.expval(qml.PauliZ(0))]) + res = qml.execute([tape], dev) + assert qml.math.get_interface(res) == "numpy" + class TestShots: """Unit tests for specifying shots per call.""" From 1c877d782329f9b489dc97118c97ba1a29561139 Mon Sep 17 00:00:00 2001 From: Christina Lee Date: Wed, 20 Nov 2024 12:19:29 -0500 Subject: [PATCH 19/35] Make `qml.math.jax_argnums_to_tape_trainable` private (#6609) **Context:** The `math` module should only depend on autoray, numpy, and all the various ML frameworks. We have one method though, `jax_argnums_to_tape_trainable`, that introduces a qnode dependency into the math module. This function is only used in `transform_program.py` as an implementation detail. **Description of the Change:** Moves `jax_argnums_to_tape_trainable` to `transform_program.py` and makes it private. **Benefits:** Cleaner dependency tree in pennylane. **Possible Drawbacks:** It could technically be considered a breaking change, but I really don't think such a function needs to be part of the public `math` interface. If we really wanted to make it part of the official `math` interface, we should have at least tested it. **Related GitHub Issues:** --------- Co-authored-by: Pietropaolo Frisoni Co-authored-by: David Wierichs --- doc/releases/changelog-dev.md | 4 +++ pennylane/math/__init__.py | 1 - pennylane/math/multi_dispatch.py | 36 ------------------- .../transforms/core/transform_program.py | 35 +++++++++++++++++- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 02b304de38e..d3cfef29904 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -74,6 +74,10 @@

Breaking changes 💔

+* `qml.math.jax_argnums_to_tape_trainable` is moved and made private to avoid a qnode dependency + in the math module. + [(#6609)](https://github.com/PennyLaneAI/pennylane/pull/6609) + * Gradient transforms are now applied after the user's transform program. [(#6590)](https://github.com/PennyLaneAI/pennylane/pull/6590) diff --git a/pennylane/math/__init__.py b/pennylane/math/__init__.py index afcf55316f1..b180d2c4d7a 100644 --- a/pennylane/math/__init__.py +++ b/pennylane/math/__init__.py @@ -50,7 +50,6 @@ gammainc, get_trainable_indices, iscomplex, - jax_argnums_to_tape_trainable, kron, matmul, multi_dispatch, diff --git a/pennylane/math/multi_dispatch.py b/pennylane/math/multi_dispatch.py index 69923d3a73b..585e3927198 100644 --- a/pennylane/math/multi_dispatch.py +++ b/pennylane/math/multi_dispatch.py @@ -23,8 +23,6 @@ from autoray import numpy as np from numpy import ndarray -import pennylane as qml - from . import single_dispatch # pylint:disable=unused-import from .utils import cast, cast_like, get_interface, requires_grad @@ -1006,40 +1004,6 @@ def detach(tensor, like=None): return tensor -def jax_argnums_to_tape_trainable(qnode, argnums, program, args, kwargs): - """This functions gets the tape parameters from the QNode construction given some argnums (only for Jax). - The tape parameters are transformed to JVPTracer if they are from argnums. This function imitates the behaviour - of Jax in order to mark trainable parameters. - - Args: - qnode(qml.QNode): the quantum node. - argnums(int, list[int]): the parameters that we want to set as trainable (on the QNode level). - program(qml.transforms.core.TransformProgram): the transform program to be applied on the tape. - - - Return: - list[float, jax.JVPTracer]: List of parameters where the trainable one are `JVPTracer`. - """ - import jax - - with jax.core.new_main(jax.interpreters.ad.JVPTrace) as main: - trace = jax.interpreters.ad.JVPTrace(main, 0) - - args_jvp = [ - ( - jax.interpreters.ad.JVPTracer(trace, arg, jax.numpy.zeros(arg.shape)) - if i in argnums - else arg - ) - for i, arg in enumerate(args) - ] - - tape = qml.workflow.construct_tape(qnode, level=0)(*args_jvp, **kwargs) - tapes, _ = program((tape,)) - del trace - return tuple(tape.get_parameters(trainable_only=False) for tape in tapes) - - @multi_dispatch(tensor_list=[1]) def set_index(array, idx, val, like=None): """Set the value at a specified index in an array. diff --git a/pennylane/transforms/core/transform_program.py b/pennylane/transforms/core/transform_program.py index 6dc407dc251..928ceb006eb 100644 --- a/pennylane/transforms/core/transform_program.py +++ b/pennylane/transforms/core/transform_program.py @@ -25,6 +25,39 @@ from .transform_dispatcher import TransformContainer, TransformDispatcher, TransformError +def _jax_argnums_to_tape_trainable(qnode, argnums, program, args, kwargs): + """This function gets the tape parameters from the QNode construction given some argnums (only for Jax). + The tape parameters are transformed to JVPTracer if they are from argnums. This function imitates the behaviour + of Jax in order to mark trainable parameters. + + Args: + qnode(qml.QNode): the quantum node. + argnums(int, list[int]): the parameters that we want to set as trainable (on the QNode level). + program(qml.transforms.core.TransformProgram): the transform program to be applied on the tape. + + Return: + list[float, jax.JVPTracer]: List of parameters where the trainable one are `JVPTracer`. + """ + import jax # pylint: disable=import-outside-toplevel + + with jax.core.new_main(jax.interpreters.ad.JVPTrace) as main: + trace = jax.interpreters.ad.JVPTrace(main, 0) + + args_jvp = [ + ( + jax.interpreters.ad.JVPTracer(trace, arg, jax.numpy.zeros(arg.shape)) + if i in argnums + else arg + ) + for i, arg in enumerate(args) + ] + + tape = qml.workflow.construct_tape(qnode, level=0)(*args_jvp, **kwargs) + tapes, _ = program((tape,)) + del trace + return tuple(tape.get_parameters(trainable_only=False) for tape in tapes) + + def _batch_postprocessing( results: ResultBatch, individual_fns: list[PostprocessingFn], slices: list[slice] ) -> ResultBatch: @@ -477,7 +510,7 @@ def _set_all_argnums(self, qnode, args, kwargs, argnums): argnums = [0] if qnode.interface in ["jax", "jax-jit"] and argnums is None else argnums # pylint: disable=protected-access if (transform._use_argnum or transform.classical_cotransform) and argnums: - params = qml.math.jax_argnums_to_tape_trainable( + params = _jax_argnums_to_tape_trainable( qnode, argnums, TransformProgram(self[0:index]), args, kwargs ) argnums_list.append([qml.math.get_trainable_indices(param) for param in params]) From ff67ecf9d5421ce8e60c5af4c01b08d89938205a Mon Sep 17 00:00:00 2001 From: Christina Lee Date: Wed, 20 Nov 2024 12:45:08 -0500 Subject: [PATCH 20/35] [Capture] Add a `DefaultQubitInterpreter` (#6328) **Context:** We are trying to develop a new workflow that can natively work with plxpr, instead of returning to the old execution pipeline. **Description of the Change:** Adds a `qml.devices.qubit.dq_interpreter.DefaultQubitInterpreter` class capable of natively executing plxpr using default qubit internal utilities. **Benefits:** **Possible Drawbacks:** Is `devices/qubit` the right place for this to live? How do we nicely handle the jax dependency for this class? **Related GitHub Issues:** [sc-72590] --------- Co-authored-by: David Wierichs Co-authored-by: Pietropaolo Frisoni --- doc/releases/changelog-dev.md | 5 +- pennylane/devices/qubit/dq_interpreter.py | 214 +++++++++ pennylane/measurements/mid_measure.py | 4 +- tests/devices/qubit/test_dq_interpreter.py | 526 +++++++++++++++++++++ 4 files changed, 745 insertions(+), 4 deletions(-) create mode 100644 pennylane/devices/qubit/dq_interpreter.py create mode 100644 tests/devices/qubit/test_dq_interpreter.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index d3cfef29904..40dd0ce8282 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -49,8 +49,11 @@ pennylane variant jaxpr. [(#6141)](https://github.com/PennyLaneAI/pennylane/pull/6141) +* A `DefaultQubitInterpreter` class has been added to provide plxpr execution using python based tools. + [(#6328)](https://github.com/PennyLaneAI/pennylane/pull/6328) + * An optional method `eval_jaxpr` is added to the device API for native execution of plxpr programs. -[(#6580)](https://github.com/PennyLaneAI/pennylane/pull/6580) + [(#6580)](https://github.com/PennyLaneAI/pennylane/pull/6580)

Other Improvements

diff --git a/pennylane/devices/qubit/dq_interpreter.py b/pennylane/devices/qubit/dq_interpreter.py new file mode 100644 index 00000000000..7299ad4faa8 --- /dev/null +++ b/pennylane/devices/qubit/dq_interpreter.py @@ -0,0 +1,214 @@ +# Copyright 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 a class for executing plxpr using default qubit tools. +""" +from copy import copy + +import jax +import numpy as np + +from pennylane.capture import disable, enable +from pennylane.capture.base_interpreter import PlxprInterpreter +from pennylane.capture.primitives import ( + adjoint_transform_prim, + cond_prim, + ctrl_transform_prim, + for_loop_prim, + measure_prim, + while_loop_prim, +) +from pennylane.measurements import MidMeasureMP, Shots + +from .apply_operation import apply_operation +from .initialize_state import create_initial_state +from .measure import measure +from .sampling import measure_with_samples + + +class DefaultQubitInterpreter(PlxprInterpreter): + """Implements a class for interpreting plxpr using python simulation tools. + + Args: + num_wires (int): the number of wires to initialize the state with + shots (int | None): the number of shots to use for the execution. Shot vectors are not supported yet. + key (None, jax.numpy.ndarray): the ``PRNGKey`` to use for random number generation. + + + >>> from pennylane.devices.qubit.dq_interpreter import DefaultQubitInterpreter + >>> qml.capture.enable() + >>> import jax + >>> key = jax.random.PRNGKey(1234) + >>> dq = DefaultQubitInterpreter(num_wires=2, shots=None, key=key) + >>> @qml.for_loop(2) + ... def g(i,y): + ... qml.RX(y,0) + ... return y + >>> def f(x): + ... g(x) + ... return qml.expval(qml.Z(0)) + >>> dq(f)(0.5) + Array(0.54030231, dtype=float64) + >>> jaxpr = jax.make_jaxpr(f)(0.5) + >>> dq.eval(jaxpr.jaxpr, jaxpr.consts, 0.5) + Array(0.54030231, dtype=float64) + + This execution can be differentiated via backprop and jitted as normal. Note that finite shot executions + still cannot be differentiated with backprop. + + >>> jax.grad(dq(f))(jax.numpy.array(0.5)) + Array(-1.68294197, dtype=float64, weak_type=True) + >>> jax.jit(dq(f))(jax.numpy.array(0.5)) + Array(0.54030231, dtype=float64) + """ + + def __init__( + self, num_wires: int, shots: int | None = None, key: None | jax.numpy.ndarray = None + ): + self.num_wires = num_wires + self.shots = Shots(shots) + if self.shots.has_partitioned_shots: + raise NotImplementedError( + "DefaultQubitInterpreter does not yet support partitioned shots." + ) + if key is None: + key = jax.random.PRNGKey(np.random.randint(100000)) + + self.initial_key = key + self.stateref = None + super().__init__() + + @property + def state(self) -> None | jax.numpy.ndarray: + """The current state of the system. None if not initialized.""" + return self.stateref["state"] if self.stateref else None + + @state.setter + def state(self, value: jax.numpy.ndarray | None): + if self.stateref is None: + raise AttributeError("execution not yet initialized.") + self.stateref["state"] = value + + @property + def key(self) -> jax.numpy.ndarray: + """A jax PRNGKey. ``initial_key`` if not yet initialized.""" + return self.stateref["key"] if self.stateref else self.initial_key + + @key.setter + def key(self, value): + if self.stateref is None: + raise AttributeError("execution not yet initialized.") + self.stateref["key"] = value + + def setup(self) -> None: + if self.stateref is None: + self.stateref = { + "state": create_initial_state(range(self.num_wires), like="jax"), + "key": self.initial_key, + } + # else set by copying a parent interpreter and we need to modify same stateref + + def cleanup(self) -> None: + self.initial_key = self.key # be cautious of leaked tracers, but we should be fine. + self.stateref = None + + def interpret_operation(self, op): + self.state = apply_operation(op, self.state) + + def interpret_measurement_eqn(self, eqn: "jax.core.JaxprEqn"): + if "mcm" in eqn.primitive.name: + raise NotImplementedError( + "DefaultQubitInterpreter does not yet support postprocessing mcms" + ) + return super().interpret_measurement_eqn(eqn) + + def interpret_measurement(self, measurement): + # measurements can sometimes create intermediary mps, but those intermediaries will not work with capture enabled + disable() + try: + if self.shots: + self.key, new_key = jax.random.split(self.key, 2) + # note that this does *not* group commuting measurements + # further work could figure out how to perform multiple measurements at the same time + output = measure_with_samples( + [measurement], self.state, shots=self.shots, prng_key=new_key + )[0] + else: + output = measure(measurement, self.state) + finally: + enable() + return output + + +@DefaultQubitInterpreter.register_primitive(measure_prim) +def _(self, *invals, reset, postselect): + mp = MidMeasureMP(invals, reset=reset, postselect=postselect) + self.key, new_key = jax.random.split(self.key, 2) + mcms = {} + self.state = apply_operation(mp, self.state, mid_measurements=mcms, prng_key=new_key) + return mcms[mp] + + +# pylint: disable=unused-argument +@DefaultQubitInterpreter.register_primitive(adjoint_transform_prim) +def _(self, *invals, jaxpr, n_consts, lazy=True): + # TODO: requires jaxpr -> list of ops first + raise NotImplementedError + + +# pylint: disable=too-many-arguments +@DefaultQubitInterpreter.register_primitive(ctrl_transform_prim) +def _(self, *invals, n_control, jaxpr, control_values, work_wires, n_consts): + # TODO: requires jaxpr -> list of ops first + raise NotImplementedError + + +# pylint: disable=too-many-arguments +@DefaultQubitInterpreter.register_primitive(for_loop_prim) +def _(self, start, stop, step, *invals, jaxpr_body_fn, consts_slice, args_slice): + consts = invals[consts_slice] + init_state = invals[args_slice] + + res = init_state + for i in range(start, stop, step): + res = copy(self).eval(jaxpr_body_fn, consts, i, *res) + + return res + + +# pylint: disable=too-many-arguments +@DefaultQubitInterpreter.register_primitive(while_loop_prim) +def _(self, *invals, jaxpr_body_fn, jaxpr_cond_fn, body_slice, cond_slice, args_slice): + consts_body = invals[body_slice] + consts_cond = invals[cond_slice] + init_state = invals[args_slice] + + fn_res = init_state + while copy(self).eval(jaxpr_cond_fn, consts_cond, *fn_res)[0]: + fn_res = copy(self).eval(jaxpr_body_fn, consts_body, *fn_res) + + return fn_res + + +@DefaultQubitInterpreter.register_primitive(cond_prim) +def _(self, *invals, jaxpr_branches, consts_slices, args_slice): + n_branches = len(jaxpr_branches) + conditions = invals[:n_branches] + args = invals[args_slice] + + for pred, jaxpr, const_slice in zip(conditions, jaxpr_branches, consts_slices): + consts = invals[const_slice] + if pred and jaxpr is not None: + return copy(self).eval(jaxpr, consts, *args) + return () diff --git a/pennylane/measurements/mid_measure.py b/pennylane/measurements/mid_measure.py index afe89e680da..5cdcd8cd708 100644 --- a/pennylane/measurements/mid_measure.py +++ b/pennylane/measurements/mid_measure.py @@ -25,9 +25,7 @@ from .measurements import MeasurementProcess, MidMeasure -def measure( - wires: Union[Hashable, Wires], reset: Optional[bool] = False, postselect: Optional[int] = None -): +def measure(wires: Union[Hashable, Wires], reset: bool = False, postselect: Optional[int] = None): r"""Perform a mid-circuit measurement in the computational basis on the supplied qubit. diff --git a/tests/devices/qubit/test_dq_interpreter.py b/tests/devices/qubit/test_dq_interpreter.py new file mode 100644 index 00000000000..220e3a63690 --- /dev/null +++ b/tests/devices/qubit/test_dq_interpreter.py @@ -0,0 +1,526 @@ +# Copyright 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 tests the default qubit interpreter. +""" +import pytest + +jax = pytest.importorskip("jax") +pytestmark = pytest.mark.jax + +from jax import numpy as jnp # pylint: disable=wrong-import-position + +import pennylane as qml # pylint: disable=wrong-import-position + +# must be below the importorskip +# pylint: disable=wrong-import-position +from pennylane.devices.qubit.dq_interpreter import DefaultQubitInterpreter + + +@pytest.fixture(autouse=True) +def enable_disable_plxpr(): + qml.capture.enable() + yield + qml.capture.disable() + + +def test_initialization(): + """Test that relevant properties are set on initialization.""" + dq = DefaultQubitInterpreter(num_wires=3, shots=None) + assert dq.num_wires == 3 + assert dq.shots == qml.measurements.Shots(None) + assert isinstance(dq.initial_key, jax.numpy.ndarray) + assert dq.stateref is None + + +def test_no_partitioned_shots(): + """Test that an error is raised if partitioned shots is requested.""" + + with pytest.raises(NotImplementedError, match="does not yet support partitioned shots"): + DefaultQubitInterpreter(num_wires=1, shots=(100, 100, 100)) + + +def test_setup_and_cleanup(): + """Test setup initializes the stateref dictionary and cleanup removes it.""" + key = jax.random.PRNGKey(1234) + dq = DefaultQubitInterpreter(num_wires=2, shots=2, key=key) + assert dq.stateref is None + + dq.setup() + assert isinstance(dq.stateref, dict) + assert list(dq.stateref.keys()) == ["state", "key"] + + assert dq.stateref["key"] is key + assert dq.key is key + + assert dq.state is dq.stateref["state"] + expected = jax.numpy.array([[1.0, 0.0], [0.0, 0.0]], dtype=complex) + assert qml.math.allclose(dq.state, expected) + + dq.cleanup() + assert dq.stateref is None + + +def test_working_state_key_before_setup(): + """Test that state and key can't be accessed before setup.""" + + key = jax.random.PRNGKey(9876) + + dq = DefaultQubitInterpreter(num_wires=1, key=key) + + assert dq.state is None + assert dq.key is key + + with pytest.raises(AttributeError, match="execution not yet initialized"): + dq.state = [1.0, 0.0] + + with pytest.raises(AttributeError, match="execution not yet initialized"): + dq.key = jax.random.PRNGKey(8765) + + +def test_simple_execution(): + """Test the execution, jitting, and gradient of a simple quantum circuit.""" + + @DefaultQubitInterpreter(num_wires=1, shots=None) + def f(x): + qml.RX(x, 0) + return qml.expval(qml.Z(0)) + + res = f(0.5) + assert qml.math.allclose(res, jax.numpy.cos(0.5)) + + jit_res = jax.jit(f)(0.5) + assert qml.math.allclose(jit_res, res) + + g = jax.grad(f)(jax.numpy.array(0.5)) + assert qml.math.allclose(g, -jax.numpy.sin(0.5)) + + +def test_capture_remains_enabled_if_measurement_error(): + """Test that capture remains enabled if there is a measurement error.""" + + @DefaultQubitInterpreter(num_wires=1, shots=None) + def g(): + return qml.sample(wires=0) # sampling with analytic execution. + + with pytest.raises(NotImplementedError): + g() + + assert qml.capture.enabled() + + +def test_pytree_function_output(): + """Test that the results respect the pytree output of the function.""" + + @DefaultQubitInterpreter(num_wires=1, shots=None) + def g(): + return { + "probs": qml.probs(wires=0), + "state": qml.state(), + "var_Z": qml.var(qml.Z(0)), + "var_X": qml.var(qml.X(0)), + } + + res = g() + assert qml.math.allclose(res["probs"], [1.0, 0.0]) + assert qml.math.allclose(res["state"], [1.0, 0.0 + 0j]) + assert qml.math.allclose(res["var_Z"], 0.0) + assert qml.math.allclose(res["var_X"], 1.0) + + +def test_mcm_reset(): + """Test that mid circuit measurements can reset the state.""" + + @DefaultQubitInterpreter(num_wires=1) + def f(): + qml.X(0) + qml.measure(0, reset=True) + return qml.state() + + out = f() + assert qml.math.allclose(out, jnp.array([1.0, 0.0])) # reset into zero state. + + +def test_operator_arithmetic(): + """Test that dq can execute operator arithmetic.""" + + @DefaultQubitInterpreter(num_wires=2) + def f(x): + qml.RY(1.0, 0) + qml.adjoint(qml.RY(x, 0)) + _ = qml.SX(1) ** 2 + return qml.expval(qml.Z(0) + 2 * qml.Z(1)) + + output = f(0.5) + expected = jnp.cos(1 - 0.5) - 2 * 1 + assert qml.math.allclose(output, expected) + + +class TestSampling: + """Test cases for generating samples.""" + + def test_known_sampling(self): + """Test sampling output with deterministic sampling output""" + + @DefaultQubitInterpreter(num_wires=2, shots=10) + def sampler(): + qml.X(0) + return qml.sample(wires=(0, 1)) + + results = sampler() + + expected0 = jax.numpy.ones((10,)) # zero wire + expected1 = jax.numpy.zeros((10,)) # one wire + expected = jax.numpy.vstack([expected0, expected1]).T + + assert qml.math.allclose(results, expected) + + def test_same_key_same_results(self): + """Test that two circuits with the same key give identical results.""" + key = jax.random.PRNGKey(1234) + + @DefaultQubitInterpreter(num_wires=1, shots=100, key=key) + def circuit1(): + qml.Hadamard(0) + return qml.sample(wires=0) + + @DefaultQubitInterpreter(num_wires=1, shots=100, key=key) + def circuit2(): + qml.Hadamard(0) + return qml.sample(wires=0) + + res1_first_exec = circuit1() + res2_first_exec = circuit2() + res1_second_exec = circuit1() + res2_second_exec = circuit2() + + assert qml.math.allclose(res1_first_exec, res2_first_exec) + assert qml.math.allclose(res1_second_exec, res2_second_exec) + + @pytest.mark.parametrize("mcm_value", (0, 1)) + def test_return_mcm(self, mcm_value): + """Test that the interpreter can return the result of mid circuit measurements""" + + @DefaultQubitInterpreter(num_wires=1) + def f(): + if mcm_value: + qml.X(0) + return qml.measure(0) + + output = f() + assert qml.math.allclose(output, mcm_value) + + def test_mcm_depends_on_key(self): + """Test that the value of an mcm depends on the key.""" + + def get_mcm_from_key(key): + @DefaultQubitInterpreter(num_wires=1, key=key) + def f(): + qml.H(0) + return qml.measure(0) + + return f() + + for key in range(0, 100, 10): + m1 = get_mcm_from_key(jax.random.PRNGKey(key)) + m2 = get_mcm_from_key(jax.random.PRNGKey(key)) + assert qml.math.allclose(m1, m2) + + samples = [int(get_mcm_from_key(jax.random.PRNGKey(key))) for key in range(0, 100, 1)] + assert set(samples) == {0, 1} + + def test_classical_transformation_mcm_value(self): + """Test that mid circuit measurements can be used in classical manipulations.""" + + @DefaultQubitInterpreter(num_wires=1) + def f(): + qml.X(0) + m0 = qml.measure(0) # 1 + qml.X(0) # reset to 0 + qml.RX(2 * m0, wires=0) + return qml.expval(qml.Z(0)) + + expected = jax.numpy.cos(2.0) + assert qml.math.allclose(f(), expected) + + @pytest.mark.parametrize("mp_type", (qml.sample, qml.expval, qml.probs)) + def test_mcm_measurements_not_yet_implemented(self, mp_type): + """Test that measurements of mcms are not yet implemented""" + + @DefaultQubitInterpreter(num_wires=1) + def f(): + m0 = qml.measure(0) + if mp_type == qml.probs: + return mp_type(op=m0) + return mp_type(m0) + + with pytest.raises(NotImplementedError): + f() + + def test_mcms_not_all_same_key(self): + """Test that each mid circuit measurement has a different key.""" + + @DefaultQubitInterpreter(num_wires=1, shots=None, key=jax.random.PRNGKey(87665)) + def g(): + qml.Hadamard(0) + m0 = qml.measure(0, reset=0) + qml.Hadamard(0) + m1 = qml.measure(0, reset=0) + qml.Hadamard(0) + m2 = qml.measure(0, reset=0) + qml.Hadamard(0) + m3 = qml.measure(0, reset=0) + qml.Hadamard(0) + m4 = qml.measure(0, reset=0) + return m0, m1, m2, m3, m4 + + output = g() + assert not all(qml.math.allclose(output[0], output[i]) for i in range(1, 5)) + # only way we could get different values between the mcms is if they had different seeds + + def test_each_measurement_has_different_key(self): + """Test that each sampling measurement is performed with a different key.""" + + @DefaultQubitInterpreter(num_wires=1, shots=100, key=jax.random.PRNGKey(87665)) + def g(): + qml.Hadamard(0) + return qml.sample(wires=0), qml.sample(wires=0) + + res1, res2 = g() + assert not qml.math.allclose(res1, res2) + + def test_more_executions_same_interpreter_different_results(self): + """Test that if multiple executions occur with the same interpreter, they will have different results.""" + + @DefaultQubitInterpreter(num_wires=1, shots=100, key=jax.random.PRNGKey(76543)) + def f(): + qml.Hadamard(0) + return qml.sample(wires=0) + + s1 = f() + s2 = f() # should be done with different key, leading to different results. + assert not qml.math.allclose(s1, s2) + + +class TestQuantumHOP: + """Tests for the quantum higher order primitives: adjoint and ctrl.""" + + def test_adjoint_transform(self): + """Test that the adjoint_transform is not yet implemented.""" + + @DefaultQubitInterpreter(num_wires=1, shots=None) + def circuit(x): + qml.adjoint(qml.RX)(x, 0) + return 1 + + with pytest.raises(NotImplementedError): + circuit(0.5) + + def test_ctrl_transform(self): + """Test that the ctrl_transform is not yet implemented.""" + + @DefaultQubitInterpreter(num_wires=2, shots=None) + def circuit(): + qml.ctrl(qml.X, control=1)(0) + + with pytest.raises(NotImplementedError): + circuit() + + +class TestClassicalComponents: + """Test execution of classical components.""" + + def test_classical_operations_in_circuit(self): + """Test that we can have classical operations in the circuit.""" + + @DefaultQubitInterpreter(num_wires=1) + def f(x, y, w): + qml.RX(2 * x + y, wires=w - 1) + return qml.expval(qml.Z(0)) + + x = jax.numpy.array(0.5) + y = jax.numpy.array(1.2) + w = jax.numpy.array(1) + + output = f(x, y, w) + expected = jax.numpy.cos(2 * x + y) + assert qml.math.allclose(output, expected) + + def test_for_loop(self): + """Test that the for loop can be executed.""" + + @DefaultQubitInterpreter(num_wires=4) + def f(y): + @qml.for_loop(4) + def f(i, x): + qml.RX(x, i) + return x + 0.1 + + f(y) + return [qml.expval(qml.Z(i)) for i in range(4)] + + output = f(1.0) + assert len(output) == 4 + assert qml.math.allclose(output[0], jax.numpy.cos(1.0)) + assert qml.math.allclose(output[1], jax.numpy.cos(1.1)) + assert qml.math.allclose(output[2], jax.numpy.cos(1.2)) + assert qml.math.allclose(output[3], jax.numpy.cos(1.3)) + + def test_for_loop_consts(self): + """Test that the for_loop can be executed properly when it has closure variables.""" + + @DefaultQubitInterpreter(num_wires=2) + def g(x): + @qml.for_loop(2) + def f(i): + qml.RX(x, i) # x is closure variable + + f() + return qml.expval(qml.Z(0)), qml.expval(qml.Z(1)) + + res1, res2 = g(jax.numpy.array(-0.654)) + expected = jnp.cos(-0.654) + assert qml.math.allclose(res1, expected) + assert qml.math.allclose(res2, expected) + + def test_while_loop(self): + """Test that the while loop can be executed.""" + + @DefaultQubitInterpreter(num_wires=4) + def f(): + def cond_fn(i): + return i < 4 + + @qml.while_loop(cond_fn) + def f(i): + qml.X(i) + return i + 1 + + f(0) + return [qml.expval(qml.Z(i)) for i in range(4)] + + output = f() + assert qml.math.allclose(output, [-1, -1, -1, -1]) + + def test_while_loop_with_consts(self): + """Test that both the cond_fn and body_fn can contain constants with the while loop.""" + + @DefaultQubitInterpreter(num_wires=2, shots=None, key=jax.random.PRNGKey(87665)) + def g(x, target): + def cond_fn(i): + return i < target + + @qml.while_loop(cond_fn) + def f(i): + qml.RX(x, 0) + return i + 1 + + f(0) + return qml.expval(qml.Z(0)) + + output = g(jnp.array(1.2), jnp.array(2)) + + assert qml.math.allclose(output, jnp.cos(2 * 1.2)) + + def test_cond_boolean(self): + """Test that cond can be used with normal classical values.""" + + def true_fn(x): + qml.RX(x, 0) + return x + 1 + + def false_fn(x): + return 2 * x + + @DefaultQubitInterpreter(num_wires=1) + def f(x, val): + out = qml.cond(val, true_fn, false_fn)(x) + return qml.probs(wires=0), out + + output_true = f(0.5, True) + expected0 = [jax.numpy.cos(0.5 / 2) ** 2, jax.numpy.sin(0.5 / 2) ** 2] + assert qml.math.allclose(output_true[0], expected0) + assert qml.math.allclose(output_true[1], 1.5) # 0.5 + 1 + + output_false = f(0.5, False) + assert qml.math.allclose(output_false[0], [1.0, 0.0]) + assert qml.math.allclose(output_false[1], 1.0) # 2 * 0.5 + + def test_cond_mcm(self): + """Test that cond can be used with the output of mcms.""" + + def true_fn(y): + qml.RX(y, 0) + + # pylint: disable=unused-argument + def false_fn(y): + qml.X(0) + + @DefaultQubitInterpreter(num_wires=1, shots=None) + def g(x): + qml.X(0) + m0 = qml.measure(0) + qml.X(0) + qml.cond(m0, true_fn, false_fn)(x) + return qml.probs(wires=0) + + output = g(0.5) + expected = [jnp.cos(0.5 / 2) ** 2, jnp.sin(0.5 / 2) ** 2] + assert qml.math.allclose(output, expected) + + def test_cond_false_no_false_fn(self): + """Test nothing is returned when the false_fn is not provided but the condition is false.""" + + def true_fn(w): + qml.X(w) + + @DefaultQubitInterpreter(num_wires=1) + def g(condition): + qml.cond(condition, true_fn)(0) + return qml.expval(qml.Z(0)) + + out = g(False) + assert qml.math.allclose(out, 1.0) + + def test_condition_with_consts(self): + """Test that each branch in a condition can contain consts.""" + + @DefaultQubitInterpreter(num_wires=1) + def circuit(x, y, z, condition0, condition1): + + def true_fn(): + qml.RX(x, 0) + + def false_fn(): + qml.RX(y, 0) + + def elif_fn(): + qml.RX(z, 0) + + qml.cond(condition0, true_fn, false_fn=false_fn, elifs=((condition1, elif_fn),))() + + return qml.expval(qml.Z(0)) + + x = jax.numpy.array(0.3) + y = jax.numpy.array(0.6) + z = jax.numpy.array(1.2) + + res0 = circuit(x, y, z, True, False) + assert qml.math.allclose(res0, jnp.cos(x)) + + res1 = circuit(x, y, z, False, True) + assert qml.math.allclose(res1, jnp.cos(z)) # elif branch = z + + res2 = circuit(x, y, z, False, False) + assert qml.math.allclose(res2, jnp.cos(y)) # false fn = y From cffb61fd6b32045f4ab3b0e89d1c570de362c04e Mon Sep 17 00:00:00 2001 From: Christina Lee Date: Wed, 20 Nov 2024 13:19:31 -0500 Subject: [PATCH 21/35] [Capture] add `eval_jaxpr` method to `DefaultQubit` (#6594) **Context:** Follows on from #6328 . This integrates the new class with `DefaultQubit`. **Description of the Change:** Implements `DefaultQubit.eval_jaxpr` with the `DefaultQubitInterpreter` class. **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** [sc-78504] --------- Co-authored-by: David Wierichs Co-authored-by: Pietropaolo Frisoni --- doc/releases/changelog-dev.md | 4 +- pennylane/devices/default_qubit.py | 24 ++++- .../default_qubit/test_default_qubit_plxpr.py | 97 +++++++++++++++++++ 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 tests/devices/default_qubit/test_default_qubit_plxpr.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 40dd0ce8282..44f9de633ac 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -49,7 +49,9 @@ pennylane variant jaxpr. [(#6141)](https://github.com/PennyLaneAI/pennylane/pull/6141) -* A `DefaultQubitInterpreter` class has been added to provide plxpr execution using python based tools. +* A `DefaultQubitInterpreter` class has been added to provide plxpr execution using python based tools, + and the `DefaultQubit.eval_jaxpr` method is now implemented. + [(#6594)](https://github.com/PennyLaneAI/pennylane/pull/6594) [(#6328)](https://github.com/PennyLaneAI/pennylane/pull/6328) * An optional method `eval_jaxpr` is added to the device API for native execution of plxpr programs. diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py index 4d2f4d5a76e..c8b6ff997dc 100644 --- a/pennylane/devices/default_qubit.py +++ b/pennylane/devices/default_qubit.py @@ -32,7 +32,7 @@ from pennylane.tape import QuantumScript, QuantumScriptBatch, QuantumScriptOrBatch from pennylane.transforms import convert_to_numpy_parameters from pennylane.transforms.core import TransformProgram -from pennylane.typing import PostprocessingFn, Result, ResultBatch +from pennylane.typing import PostprocessingFn, Result, ResultBatch, TensorLike from . import Device from .execution_config import DefaultExecutionConfig, ExecutionConfig @@ -891,6 +891,28 @@ def execute_and_compute_vjp( return tuple(zip(*results)) + # pylint: disable=import-outside-toplevel + def eval_jaxpr( + self, jaxpr: "jax.core.Jaxpr", consts: list[TensorLike], *args + ) -> list[TensorLike]: + from .qubit.dq_interpreter import DefaultQubitInterpreter + + if self.wires is None: + raise qml.DeviceError("Device wires are required for jaxpr execution.") + if self.shots.has_partitioned_shots: + raise qml.DeviceError("Shot vectors are unsupported with jaxpr execution.") + if self._prng_key is not None: + key = self.get_prng_keys()[0] + else: + import jax + + key = jax.random.PRNGKey(self._rng.integers(100000)) + + interpreter = DefaultQubitInterpreter( + num_wires=len(self.wires), shots=self.shots.total_shots, key=key + ) + return interpreter.eval(jaxpr, consts, *args) + def _simulate_wrapper(circuit, kwargs): return simulate(circuit, **kwargs) diff --git a/tests/devices/default_qubit/test_default_qubit_plxpr.py b/tests/devices/default_qubit/test_default_qubit_plxpr.py new file mode 100644 index 00000000000..14b315be323 --- /dev/null +++ b/tests/devices/default_qubit/test_default_qubit_plxpr.py @@ -0,0 +1,97 @@ +# Copyright 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 default qubit executing jaxpr.""" + +import pytest + +import pennylane as qml + +jax = pytest.importorskip("jax") +pytestmark = pytest.mark.jax + + +@pytest.fixture(autouse=True) +def enable_disable_plxpr(): + qml.capture.enable() + yield + qml.capture.disable() + + +def test_requires_wires(): + """Test that a device error is raised if device wires are not specified.""" + + jaxpr = jax.make_jaxpr(lambda x: x + 1)(0.1) + dev = qml.device("default.qubit") + + with pytest.raises(qml.DeviceError, match="Device wires are required."): + dev.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, 0.2) + + +def test_no_partitioned_shots(): + """Test that an error is raised if the device has partitioned shots.""" + + jaxpr = jax.make_jaxpr(lambda x: x + 1)(0.1) + dev = qml.device("default.qubit", wires=1, shots=(100, 100)) + + with pytest.raises(qml.DeviceError, match="Shot vectors are unsupported with jaxpr execution."): + dev.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, 0.2) + + +def test_use_device_prng(): + """Test that sampling depends on the device prng.""" + + key1 = jax.random.PRNGKey(1234) + key2 = jax.random.PRNGKey(1234) + + dev1 = qml.device("default.qubit", wires=1, shots=100, seed=key1) + dev2 = qml.device("default.qubit", wires=1, shots=100, seed=key2) + + def f(): + qml.H(0) + return qml.sample(wires=0) + + jaxpr = jax.make_jaxpr(f)() + + samples1 = dev1.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts) + samples2 = dev2.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts) + + assert qml.math.allclose(samples1, samples2) + + +def test_no_prng_key(): + """Test that that sampling works without a provided prng key.""" + + dev = qml.device("default.qubit", wires=1, shots=100) + + def f(): + return qml.sample(wires=0) + + jaxpr = jax.make_jaxpr(f)() + res = dev.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts) + assert qml.math.allclose(res, jax.numpy.zeros(100)) + + +def test_simple_execution(): + """Test the execution, jitting, and gradient of a simple quantum circuit.""" + + def f(x): + qml.RX(x, 0) + return qml.expval(qml.Z(0)) + + jaxpr = jax.make_jaxpr(f)(0.123) + + dev = qml.device("default.qubit", wires=1) + + res = dev.eval_jaxpr(jaxpr.jaxpr, jaxpr.consts, 0.5) + assert qml.math.allclose(res, jax.numpy.cos(0.5)) From cc8e32ddd728e59268d4dc0c33c067237f311b35 Mon Sep 17 00:00:00 2001 From: Andrija Paurevic <46359773+andrijapau@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:43:50 -0500 Subject: [PATCH 22/35] Add `_resolve_execution_config` helper function to `qnode.py` (#6498) **Context:** The goal is to streamline the process of updating the execution configuration with non-device-specific settings. This approach consolidates the setup and validation of the execution configuration in one spot in the code, before calling `qml.execute` in `qnode.py`. **Description of the Change:** This refactor organizes the setup of the initial `config` by moving it into a dedicated helper function. It also includes the setup and validation code for handling the `MCMConfig`. **Benefits:** Improves code cleaniness in `qnode.py` and allows for better testing of internal pipeline. **Possible Drawbacks:** None [sc-72086] --------- Co-authored-by: Christina Lee --- pennylane/workflow/__init__.py | 2 +- pennylane/workflow/execution.py | 50 ++---- pennylane/workflow/get_gradient_fn.py | 96 ---------- pennylane/workflow/qnode.py | 171 ++++++++++++------ pennylane/workflow/resolve_diff_method.py | 82 +++++++++ tests/logging/test_logging_autograd.py | 4 +- tests/test_qnode.py | 4 +- tests/test_qnode_legacy.py | 4 +- tests/transforms/test_add_noise.py | 2 +- tests/transforms/test_insert_ops.py | 4 +- tests/workflow/test_get_gradient_fn.py | 193 --------------------- tests/workflow/test_resolve_diff_method.py | 186 ++++++++++++++++++++ 12 files changed, 406 insertions(+), 392 deletions(-) delete mode 100644 pennylane/workflow/get_gradient_fn.py create mode 100644 pennylane/workflow/resolve_diff_method.py delete mode 100644 tests/workflow/test_get_gradient_fn.py create mode 100644 tests/workflow/test_resolve_diff_method.py diff --git a/pennylane/workflow/__init__.py b/pennylane/workflow/__init__.py index 451b0250692..b786625fd95 100644 --- a/pennylane/workflow/__init__.py +++ b/pennylane/workflow/__init__.py @@ -46,5 +46,5 @@ from .construct_tape import construct_tape from .execution import INTERFACE_MAP, SUPPORTED_INTERFACE_NAMES, execute from .get_best_diff_method import get_best_diff_method -from .get_gradient_fn import _get_gradient_fn +from .resolve_diff_method import _resolve_diff_method from .qnode import QNode, qnode diff --git a/pennylane/workflow/execution.py b/pennylane/workflow/execution.py index 63f33164cd5..4422f4ebe54 100644 --- a/pennylane/workflow/execution.py +++ b/pennylane/workflow/execution.py @@ -106,7 +106,15 @@ def _use_tensorflow_autograph(): - import tensorflow as tf + """Checks if TensorFlow is in graph mode, allowing Autograph for optimized execution""" + try: # pragma: no cover + import tensorflow as tf + except ImportError as e: # pragma: no cover + raise qml.QuantumFunctionError( # pragma: no cover + "tensorflow not found. Please install the latest " # pragma: no cover + "version of tensorflow supported by Pennylane " # pragma: no cover + "to enable the 'tensorflow' interface." # pragma: no cover + ) from e # pragma: no cover return not tf.executing_eagerly() @@ -286,28 +294,6 @@ def _get_interface_name(tapes, interface): return interface -def _update_mcm_config(mcm_config: "qml.devices.MCMConfig", interface: str, finite_shots: bool): - """Helper function to update the mid-circuit measurements configuration based on - execution parameters""" - if interface == "jax-jit" and mcm_config.mcm_method == "deferred": - # This is a current limitation of defer_measurements. "hw-like" behaviour is - # not yet accessible. - if mcm_config.postselect_mode == "hw-like": - raise ValueError( - "Using postselect_mode='hw-like' is not supported with jax-jit when using " - "mcm_method='deferred'." - ) - mcm_config.postselect_mode = "fill-shots" - - if ( - finite_shots - and "jax" in interface - and mcm_config.mcm_method in (None, "one-shot") - and mcm_config.postselect_mode in (None, "hw-like") - ): - mcm_config.postselect_mode = "pad-invalid-samples" - - def execute( tapes: QuantumScriptBatch, device: SupportedDeviceAPIs, @@ -451,7 +437,6 @@ def cost_fn(params, x): ### Specifying and preprocessing variables #### - _interface_user_input = interface interface = _get_interface_name(tapes, interface) # Only need to calculate derivatives with jax when we know it will be executed later. if interface in {"jax", "jax-jit"}: @@ -472,15 +457,6 @@ def cost_fn(params, x): diff_method, grad_on_execution, interface, device, device_vjp, mcm_config, gradient_kwargs ) - # Mid-circuit measurement configuration validation - # If the user specifies `interface=None`, regular execution considers it numpy, but the mcm - # workflow still needs to know if jax-jit is used - mcm_interface = ( - _get_interface_name(tapes, "auto") if _interface_user_input is None else interface - ) - finite_shots = any(tape.shots for tape in tapes) - _update_mcm_config(config.mcm_config, mcm_interface, finite_shots) - is_gradient_transform = isinstance(diff_method, qml.transforms.core.TransformDispatcher) transform_program, inner_transform = _make_transform_programs( device, config, inner_transform, transform_program, is_gradient_transform @@ -666,15 +642,9 @@ def _get_execution_config( diff_method, grad_on_execution, interface, device, device_vjp, mcm_config, gradient_kwargs ): """Helper function to get the execution config.""" - if diff_method is None: - _gradient_method = None - elif isinstance(diff_method, str): - _gradient_method = diff_method - else: - _gradient_method = "gradient-transform" config = qml.devices.ExecutionConfig( interface=interface, - gradient_method=_gradient_method, + gradient_method=diff_method, grad_on_execution=None if grad_on_execution == "best" else grad_on_execution, use_device_jacobian_product=device_vjp, mcm_config=mcm_config, diff --git a/pennylane/workflow/get_gradient_fn.py b/pennylane/workflow/get_gradient_fn.py deleted file mode 100644 index 15a448c8a1e..00000000000 --- a/pennylane/workflow/get_gradient_fn.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright 2018-2024 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Contains a function for retrieving the gradient function for a given device or tape. - -""" - -from typing import Optional, get_args - -import pennylane as qml -from pennylane.logging import debug_logger -from pennylane.transforms.core import TransformDispatcher -from pennylane.workflow.qnode import ( - SupportedDeviceAPIs, - SupportedDiffMethods, - _make_execution_config, -) - - -# pylint: disable=too-many-return-statements, unsupported-binary-operation -@debug_logger -def _get_gradient_fn( - device: SupportedDeviceAPIs, - diff_method: "TransformDispatcher | SupportedDiffMethods" = "best", - tape: Optional["qml.tape.QuantumTape"] = None, -): - """Determines the differentiation method for a given device and diff method. - - Args: - device (:class:`~.devices.Device`): PennyLane device - diff_method (str or :class:`~.TransformDispatcher`): The requested method of differentiation. Defaults to ``"best"``. - If a string, allowed options are ``"best"``, ``"backprop"``, ``"adjoint"``, - ``"device"``, ``"parameter-shift"``, ``"hadamard"``, ``"finite-diff"``, or ``"spsa"``. - Alternatively, a gradient transform can be provided. - tape (Optional[.QuantumTape]): the circuit that will be differentiated. Should include shots information. - - Returns: - str or :class:`~.TransformDispatcher` (the ``gradient_fn``) - """ - - if diff_method is None: - return None - - config = _make_execution_config(None, diff_method) - - if device.supports_derivatives(config, circuit=tape): - new_config = device.preprocess(config)[1] - return new_config.gradient_method - - if diff_method in {"backprop", "adjoint", "device"}: # device-only derivatives - raise qml.QuantumFunctionError( - f"Device {device} does not support {diff_method} with requested circuit." - ) - - if diff_method == "best": - if tape and any(isinstance(o, qml.operation.CV) for o in tape): - return qml.gradients.param_shift_cv - - return qml.gradients.param_shift - - if diff_method == "parameter-shift": - if tape and any(isinstance(o, qml.operation.CV) and o.name != "Identity" for o in tape): - return qml.gradients.param_shift_cv - return qml.gradients.param_shift - - gradient_transform_map = { - "finite-diff": qml.gradients.finite_diff, - "spsa": qml.gradients.spsa_grad, - "hadamard": qml.gradients.hadamard_grad, - } - - if diff_method in gradient_transform_map: - return gradient_transform_map[diff_method] - - if isinstance(diff_method, str): - raise qml.QuantumFunctionError( - f"Differentiation method {diff_method} not recognized. Allowed " - f"options are {tuple(get_args(SupportedDiffMethods))}." - ) - - if isinstance(diff_method, TransformDispatcher): - return diff_method - - raise qml.QuantumFunctionError( - f"Differentiation method {diff_method} must be a gradient transform or a string." - ) diff --git a/pennylane/workflow/qnode.py b/pennylane/workflow/qnode.py index c2ec015729f..3c5c5469c96 100644 --- a/pennylane/workflow/qnode.py +++ b/pennylane/workflow/qnode.py @@ -14,13 +14,14 @@ """ This module contains the QNode class and qnode decorator. """ -# pylint: disable=too-many-instance-attributes,too-many-arguments,protected-access,unnecessary-lambda-assignment, too-many-branches, too-many-statements, unused-argument, too-many-positional-arguments +# pylint: disable=too-many-instance-attributes,too-many-arguments,protected-access,unnecessary-lambda-assignment, too-many-branches, too-many-statements, unused-argument, too-many-positional-arguments, inconsistent-return-statements import copy import functools import inspect import logging import warnings from collections.abc import Callable, Sequence +from dataclasses import replace from typing import Any, Literal, Optional, Union, get_args from cachetools import Cache @@ -29,10 +30,15 @@ from pennylane.debugging import pldb_device_manager from pennylane.logging import debug_logger from pennylane.measurements import MidMeasureMP -from pennylane.tape import QuantumScript, QuantumTape +from pennylane.tape import QuantumScript, QuantumScriptBatch, QuantumTape from pennylane.transforms.core import TransformContainer, TransformDispatcher, TransformProgram -from .execution import INTERFACE_MAP, SUPPORTED_INTERFACE_NAMES, SupportedInterfaceUserInput +from .execution import ( + INTERFACE_MAP, + SUPPORTED_INTERFACE_NAMES, + SupportedInterfaceUserInput, + _get_interface_name, +) logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) @@ -83,26 +89,116 @@ def _convert_to_interface(res, interface): def _make_execution_config( circuit: Optional["QNode"], diff_method=None, mcm_config=None ) -> "qml.devices.ExecutionConfig": - if diff_method is None or isinstance(diff_method, str): - _gradient_method = diff_method - else: - _gradient_method = "gradient-transform" + circuit_interface = getattr(circuit, "interface", None) execute_kwargs = getattr(circuit, "execute_kwargs", {}) + gradient_kwargs = getattr(circuit, "gradient_kwargs", {}) grad_on_execution = execute_kwargs.get("grad_on_execution") if getattr(circuit, "interface", "") == "jax": grad_on_execution = False elif grad_on_execution == "best": grad_on_execution = None + # Mapping numpy to None here because `qml.execute` will map None back into + # numpy. If we do not do this, numpy will become autograd in `qml.execute`. + # If the user specified interface="numpy", it would've already been converted to + # "autograd", and it wouldn't be affected. return qml.devices.ExecutionConfig( - interface=getattr(circuit, "interface", None), - gradient_method=_gradient_method, + interface=None if circuit_interface == "numpy" else circuit_interface, + gradient_keyword_arguments=gradient_kwargs, + gradient_method=diff_method, grad_on_execution=grad_on_execution, use_device_jacobian_product=execute_kwargs.get("device_vjp", False), mcm_config=mcm_config or qml.devices.MCMConfig(), ) +def _resolve_mcm_config( + mcm_config: "qml.devices.MCMConfig", interface: str, finite_shots: bool +) -> "qml.devices.MCMConfig": + """Helper function to resolve the mid-circuit measurements configuration based on + execution parameters""" + updated_values = {} + + if not finite_shots: + updated_values["postselect_mode"] = None + if mcm_config.mcm_method == "one-shot": + raise ValueError( + f"Cannot use the '{mcm_config.mcm_method}' method for mid-circuit measurements with analytic mode." + ) + + if mcm_config.mcm_method == "single-branch-statistics": + raise ValueError("Cannot use mcm_method='single-branch-statistics' without qml.qjit.") + + if interface == "jax-jit" and mcm_config.mcm_method == "deferred": + # This is a current limitation of defer_measurements. "hw-like" behaviour is + # not yet accessible. + if mcm_config.postselect_mode == "hw-like": + raise ValueError( + "Using postselect_mode='hw-like' is not supported with jax-jit when using " + "mcm_method='deferred'." + ) + updated_values["postselect_mode"] = "fill-shots" + + if ( + finite_shots + and "jax" in interface + and mcm_config.mcm_method in (None, "one-shot") + and mcm_config.postselect_mode in (None, "hw-like") + ): + updated_values["postselect_mode"] = "pad-invalid-samples" + + return replace(mcm_config, **updated_values) + + +def _resolve_execution_config( + execution_config: "qml.devices.ExecutionConfig", + device: "qml.devices.Device", + tapes: QuantumScriptBatch, + transform_program: TransformProgram, +) -> "qml.devices.ExecutionConfig": + """Resolves the execution configuration for non-device specific properties. + + Args: + execution_config (qml.devices.ExecutionConfig): an execution config to be executed on the device + device (qml.devices.Device): a Pennylane device + tapes (QuantumScriptBatch): a batch of tapes + transform_program (TransformProgram): a program of transformations to be applied to the tapes + + Returns: + qml.devices.ExecutionConfig: resolved execution configuration + """ + updated_values = {} + updated_values["gradient_keyword_arguments"] = dict(execution_config.gradient_keyword_arguments) + + if ( + "lightning" in device.name + and qml.metric_tensor in transform_program + and execution_config.gradient_method == "best" + ): + execution_config = replace(execution_config, gradient_method=qml.gradients.param_shift) + else: + execution_config = qml.workflow._resolve_diff_method( + execution_config, device, tape=tapes[0] + ) + + if execution_config.gradient_method is qml.gradients.param_shift_cv: + updated_values["gradient_keyword_arguments"]["dev"] = device + + # Mid-circuit measurement configuration validation + # If the user specifies `interface=None`, regular execution considers it numpy, but the mcm + # workflow still needs to know if jax-jit is used + interface = _get_interface_name(tapes, execution_config.interface) + finite_shots = any(tape.shots for tape in tapes) + mcm_interface = ( + _get_interface_name(tapes, "auto") if execution_config.interface is None else interface + ) + mcm_config = _resolve_mcm_config(execution_config.mcm_config, mcm_interface, finite_shots) + + updated_values["mcm_config"] = mcm_config + + return replace(execution_config, **updated_values) + + def _to_qfunc_output_type( results: qml.typing.Result, qfunc_output, has_partitioned_shots ) -> qml.typing.Result: @@ -708,10 +804,6 @@ def get_gradient_fn( if isinstance(diff_method, qml.transforms.core.TransformDispatcher): return diff_method, {}, device - raise qml.QuantumFunctionError( - f"Differentiation method {diff_method} must be a gradient transform or a string." - ) - @staticmethod @debug_logger def get_best_method( @@ -887,46 +979,25 @@ def _execution_component(self, args: tuple, kwargs: dict) -> qml.typing.Result: Result """ - if ( - self.device.name == "lightning.qubit" - and qml.metric_tensor in self.transform_program - and self.diff_method == "best" - ): - gradient_fn = qml.gradients.param_shift - else: - gradient_fn = QNode.get_gradient_fn( - self.device, self.interface, self.diff_method, tape=self._tape - )[0] - execute_kwargs = copy.copy(self.execute_kwargs) - - gradient_kwargs = copy.copy(self.gradient_kwargs) - if gradient_fn is qml.gradients.param_shift_cv: - gradient_kwargs["dev"] = self.device + execute_kwargs = copy.copy(self.execute_kwargs) mcm_config = copy.copy(execute_kwargs["mcm_config"]) - if not self._tape.shots: - mcm_config.postselect_mode = None - if mcm_config.mcm_method == "one-shot": - raise ValueError( - f"Cannot use the '{mcm_config.mcm_method}' method for mid-circuit measurements with analytic mode." - ) - if mcm_config.mcm_method == "single-branch-statistics": - raise ValueError("Cannot use mcm_method='single-branch-statistics' without qml.qjit.") + config = _make_execution_config(self, self.diff_method, mcm_config=mcm_config) + config = _resolve_execution_config( + config, self.device, (self._tape,), self.transform_program + ) + device_transform_program, config = self.device.preprocess(execution_config=config) full_transform_program = qml.transforms.core.TransformProgram(self.transform_program) inner_transform_program = qml.transforms.core.TransformProgram() - # Add the gradient expand to the program if necessary - if getattr(gradient_fn, "expand_transform", False): + if getattr(config.gradient_method, "expand_transform", False): full_transform_program.add_transform( - qml.transform(gradient_fn.expand_transform), - **gradient_kwargs, + qml.transform(config.gradient_method.expand_transform), + **config.gradient_keyword_arguments, ) - config = _make_execution_config(self, gradient_fn, mcm_config) - device_transform_program, config = self.device.preprocess(execution_config=config) - if config.use_device_gradient: full_transform_program += device_transform_program else: @@ -936,24 +1007,16 @@ def _execution_component(self, args: tuple, kwargs: dict) -> qml.typing.Result: full_transform_program.set_classical_component(self, args, kwargs) _prune_dynamic_transform(full_transform_program, inner_transform_program) - execute_kwargs["mcm_config"] = mcm_config - - # Mapping numpy to None here because `qml.execute` will map None back into - # numpy. If we do not do this, numpy will become autograd in `qml.execute`. - # If the user specified interface="numpy", it would've already been converted to - # "autograd", and it wouldn't be affected. - interface = None if self.interface == "numpy" else self.interface - # pylint: disable=unexpected-keyword-arg res = qml.execute( (self._tape,), device=self.device, - diff_method=gradient_fn, - interface=interface, + diff_method=config.gradient_method, + interface=config.interface, transform_program=full_transform_program, inner_transform=inner_transform_program, config=config, - gradient_kwargs=gradient_kwargs, + gradient_kwargs=config.gradient_keyword_arguments, **execute_kwargs, ) res = res[0] diff --git a/pennylane/workflow/resolve_diff_method.py b/pennylane/workflow/resolve_diff_method.py new file mode 100644 index 00000000000..101996008c2 --- /dev/null +++ b/pennylane/workflow/resolve_diff_method.py @@ -0,0 +1,82 @@ +# 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. +"""Contains a function for resolving the differentiation method in an initial execution config based on device and tape information. + +""" + +from dataclasses import replace +from typing import get_args + +import pennylane as qml +from pennylane.logging import debug_logger +from pennylane.transforms.core import TransformDispatcher +from pennylane.workflow.qnode import SupportedDeviceAPIs, SupportedDiffMethods + + +@debug_logger +def _resolve_diff_method( + initial_config: "qml.devices.ExecutionConfig", + device: SupportedDeviceAPIs, + tape: "qml.tape.QuantumTape" = None, +) -> "qml.devices.ExecutionConfig": + """ + Resolves the differentiation method and updates the initial execution configuration accordingly. + + Args: + initial_config (qml.devices.ExecutionConfig): The initial execution configuration. + device (qml.devices.Device): A PennyLane device. + tape (Optional[qml.tape.QuantumTape]): The circuit that will be differentiated. Should include shots information. + + Returns: + qml.devices.ExecutionConfig: Updated execution configuration with the resolved differentiation method. + """ + diff_method = initial_config.gradient_method + updated_values = {"gradient_method": diff_method} + + if diff_method is None: + return initial_config + + if device.supports_derivatives(initial_config, circuit=tape): + new_config = device.preprocess(initial_config)[1] + return new_config + + if diff_method in {"backprop", "adjoint", "device"}: + raise qml.QuantumFunctionError( + f"Device {device} does not support {diff_method} with requested circuit." + ) + + if diff_method in {"best", "parameter-shift"}: + if tape and any(isinstance(op, qml.operation.CV) and op.name != "Identity" for op in tape): + updated_values["gradient_method"] = qml.gradients.param_shift_cv + else: + updated_values["gradient_method"] = qml.gradients.param_shift + + else: + gradient_transform_map = { + "finite-diff": qml.gradients.finite_diff, + "spsa": qml.gradients.spsa_grad, + "hadamard": qml.gradients.hadamard_grad, + } + + if diff_method in gradient_transform_map: + updated_values["gradient_method"] = gradient_transform_map[diff_method] + elif isinstance(diff_method, TransformDispatcher): + updated_values["gradient_method"] = diff_method + else: + raise qml.QuantumFunctionError( + f"Differentiation method {diff_method} not recognized. Allowed " + f"options are {tuple(get_args(SupportedDiffMethods))}." + ) + + return replace(initial_config, **updated_values) diff --git a/tests/logging/test_logging_autograd.py b/tests/logging/test_logging_autograd.py index f86c958187e..0d134501c75 100644 --- a/tests/logging/test_logging_autograd.py +++ b/tests/logging/test_logging_autograd.py @@ -80,8 +80,8 @@ def circuit(params): ["Calling Date: Wed, 20 Nov 2024 20:25:34 +0100 Subject: [PATCH 23/35] fix Resources.__str__ to match attribute names (#6581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minor thing: The resources repr and attribute names dont coincide, e.g. ``` >>> U = qml.TrotterProduct(qml.X(0) + qml.Y(0), time=0.5, n=2, order=2) >>> U.resources() wires: 1 gates: 8 depth: 8 shots: Shots(total=None) gate_types: {'Exp': 8} gate_sizes: {1: 8} ``` I would e.g. expect to be able to obtain the number of gates but neither U.resources().gates nor U.resources()["gates"] or getattr(U.resources(), "gates") work - because the attribute is actually called `num_gates`. This PR just fixes the str representation to match the attribute name. This is such a minor thing that it was easier and faster to just fix it than open an issue / sc story 😬 --------- Co-authored-by: Jay Soni --- doc/releases/changelog-dev.md | 3 +++ pennylane/devices/null_qubit.py | 4 ++-- pennylane/resource/__init__.py | 4 ++-- pennylane/resource/resource.py | 10 +++++----- pennylane/resource/specs.py | 12 ++++++------ pennylane/tape/qscript.py | 4 ++-- pennylane/tracker.py | 8 ++++---- tests/resource/test_resource.py | 17 +++++++++-------- 8 files changed, 33 insertions(+), 29 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 44f9de633ac..49e413da003 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -69,6 +69,9 @@ * Expand `ExecutionConfig.gradient_method` to store `TransformDispatcher` type. [(#6455)](https://github.com/PennyLaneAI/pennylane/pull/6455) +* Fix the string representation of `Resources` instances to match the attribute names. + [(#6581)](https://github.com/PennyLaneAI/pennylane/pull/6581) +

Labs 🧪

* Added base class `Resources`, `CompressedResourceOp`, `ResourceOperator` for advanced resource estimation. diff --git a/pennylane/devices/null_qubit.py b/pennylane/devices/null_qubit.py index 0d3eac368bd..6dac0a66215 100644 --- a/pennylane/devices/null_qubit.py +++ b/pennylane/devices/null_qubit.py @@ -182,8 +182,8 @@ def circuit(params): circuit(params) >>> tracker.history["resources"][0] - wires: 100 - gates: 10000 + num_wires: 100 + num_gates: 10000 depth: 502 shots: Shots(total=None) gate_types: diff --git a/pennylane/resource/__init__.py b/pennylane/resource/__init__.py index f6a4b6d629c..a92248594c2 100644 --- a/pennylane/resource/__init__.py +++ b/pennylane/resource/__init__.py @@ -110,8 +110,8 @@ def circuit(theta): >>> resources_lst = tracker.history['resources'] >>> print(resources_lst[0]) -wires: 3 -gates: 7 +num_wires: 3 +num_gates: 7 depth: 5 shots: Shots(None) gate_types: diff --git a/pennylane/resource/resource.py b/pennylane/resource/resource.py index 7e0e654a266..5a14facdd1c 100644 --- a/pennylane/resource/resource.py +++ b/pennylane/resource/resource.py @@ -46,8 +46,8 @@ class Resources: >>> r = Resources(num_wires=2, num_gates=2, gate_types={'Hadamard': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1}, depth=2) >>> print(r) - wires: 2 - gates: 2 + num_wires: 2 + num_gates: 2 depth: 2 shots: Shots(total=None) gate_types: @@ -64,7 +64,7 @@ class Resources: shots: Shots = field(default_factory=Shots) def __str__(self): - keys = ["wires", "gates", "depth"] + keys = ["num_wires", "num_gates", "depth"] vals = [self.num_wires, self.num_gates, self.depth] items = "\n".join([str(i) for i in zip(keys, vals)]) items = items.replace("('", "") @@ -114,8 +114,8 @@ def resources(self) -> Resources: ... >>> op = CustomOp(wires=[0, 1]) >>> print(op.resources()) - wires: 2 - gates: 3 + num_wires: 2 + num_gates: 3 depth: 2 shots: Shots(total=None) gate_types: diff --git a/pennylane/resource/specs.py b/pennylane/resource/specs.py index 8e476b6b377..c83f84c7aee 100644 --- a/pennylane/resource/specs.py +++ b/pennylane/resource/specs.py @@ -103,8 +103,8 @@ def circuit(x): return the same results: >>> print(qml.specs(circuit, level=0)(0.1)["resources"]) - wires: 2 - gates: 6 + num_wires: 2 + num_gates: 6 depth: 6 shots: Shots(total=None) gate_types: @@ -115,8 +115,8 @@ def circuit(x): We then check the resources after applying all transforms: >>> print(qml.specs(circuit, level=None)(0.1)["resources"]) - wires: 2 - gates: 2 + num_wires: 2 + num_gates: 2 depth: 1 shots: Shots(total=None) gate_types: @@ -127,8 +127,8 @@ def circuit(x): We can also notice that ``SWAP`` and ``PauliX`` are not present in the circuit if we set ``level=2``: >>> print(qml.specs(circuit, level=2)(0.1)["resources"]) - wires: 2 - gates: 3 + num_wires: 2 + num_gates: 3 depth: 3 shots: Shots(total=None) gate_types: diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index 47ffcad1954..a0caf54bd41 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -1169,8 +1169,8 @@ def specs(self) -> dict[str, Any]: >>> qscript.specs['gate_sizes'] defaultdict(, {1: 4, 2: 2}) >>> print(qscript.specs['resources']) - wires: 2 - gates: 6 + num_wires: 2 + num_gates: 6 depth: 4 shots: Shots(total=None) gate_types: diff --git a/pennylane/tracker.py b/pennylane/tracker.py index 0546b98e42b..34a59ac14b7 100644 --- a/pennylane/tracker.py +++ b/pennylane/tracker.py @@ -90,8 +90,8 @@ def circuit(x): >>> tracker.history['results'] [1.0, -0.3, 0.16] >>> print(tracker.history['resources'][0]) - wires: 1 - gates: 1 + num_wires: 1 + num_gates: 1 depth: 1 shots: Shots(total=100) gate_types: @@ -152,8 +152,8 @@ def circuit(x): ... >>> resources_lst = tracker.history['resources'] >>> print(resources_lst[0]) - wires: 1 - gates: 1 + num_wires: 1 + num_gates: 1 depth: 1 shots: Shots(total=10) gate_types: diff --git a/tests/resource/test_resource.py b/tests/resource/test_resource.py index 2c7b57a2f73..054aa3f004e 100644 --- a/tests/resource/test_resource.py +++ b/tests/resource/test_resource.py @@ -14,6 +14,7 @@ """ Test base Resource class and its associated methods """ +# pylint: disable=unnecessary-dunder-call from collections import defaultdict from dataclasses import FrozenInstanceError @@ -80,8 +81,8 @@ def test_set_attributes_error(self): test_str_data = ( ( - "wires: 0\n" - + "gates: 0\n" + "num_wires: 0\n" + + "num_gates: 0\n" + "depth: 0\n" + "shots: Shots(total=None)\n" + "gate_types:\n" @@ -90,8 +91,8 @@ def test_set_attributes_error(self): + "{}" ), ( - "wires: 5\n" - + "gates: 0\n" + "num_wires: 5\n" + + "num_gates: 0\n" + "depth: 0\n" + "shots: Shots(total=None)\n" + "gate_types:\n" @@ -100,8 +101,8 @@ def test_set_attributes_error(self): + "{}" ), ( - "wires: 1\n" - + "gates: 3\n" + "num_wires: 1\n" + + "num_gates: 3\n" + "depth: 3\n" + "shots: Shots(total=110, vector=[10 shots, 50 shots x 2])\n" + "gate_types:\n" @@ -110,8 +111,8 @@ def test_set_attributes_error(self): + "{1: 3}" ), ( - "wires: 4\n" - + "gates: 2\n" + "num_wires: 4\n" + + "num_gates: 2\n" + "depth: 2\n" + "shots: Shots(total=100)\n" + "gate_types:\n" From 0aac359f3fcb8d676afd2bfb8f87f6accd5c4202 Mon Sep 17 00:00:00 2001 From: Christina Lee Date: Wed, 20 Nov 2024 16:37:58 -0500 Subject: [PATCH 24/35] move `qnode_call` to the `workflow` module and make it private (#6620) **Context:** As we are integrating more things with the capture project, we are making changes across the entirety of pennylane. Previously, we stuck most capture related code in the capture module, and then imported in whichever relevant place we needed too. I don't think this makes sense anymore. As we are tying together the capture work to more parts of pennylane, the capture code should just be located next to the code it is capturing. **Description of the Change:** Move `qml.capture.qnode_call` to the workflow module and make it private. **Benefits:** Code for managing the workflow is located in the `workflow` module. **Possible Drawbacks:** This is all experimental, so we should be able to move things around whenever we see fit. I'm not concerned about this being "breaking". **Related GitHub Issues:** [sc-78505 --- doc/releases/changelog-dev.md | 3 +++ pennylane/capture/__init__.py | 5 +---- pennylane/capture/primitives.py | 2 +- .../capture_qnode.py => workflow/_capture_qnode.py} | 7 ++++--- pennylane/workflow/qnode.py | 3 ++- 5 files changed, 11 insertions(+), 9 deletions(-) rename pennylane/{capture/capture_qnode.py => workflow/_capture_qnode.py} (98%) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 49e413da003..705d33bc519 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -57,6 +57,9 @@ * An optional method `eval_jaxpr` is added to the device API for native execution of plxpr programs. [(#6580)](https://github.com/PennyLaneAI/pennylane/pull/6580) +* `qml.capture.qnode_call` has been made private and moved to the `workflow` module. + [(#6620)](https://github.com/PennyLaneAI/pennylane/pull/6620/) +

Other Improvements

* `qml.BasisRotation` template is now JIT compatible. diff --git a/pennylane/capture/__init__.py b/pennylane/capture/__init__.py index 080701f0d7d..c2d15a4f5d2 100644 --- a/pennylane/capture/__init__.py +++ b/pennylane/capture/__init__.py @@ -34,7 +34,6 @@ ~create_measurement_wires_primitive ~create_measurement_mcm_primitive ~make_plxpr - ~qnode_call ~PlxprInterpreter ~FlatFn @@ -156,7 +155,6 @@ def _(*args, **kwargs): create_measurement_wires_primitive, create_measurement_mcm_primitive, ) -from .capture_qnode import qnode_call from .flatfn import FlatFn from .make_plxpr import make_plxpr @@ -182,7 +180,7 @@ def __getattr__(key): return _get_abstract_measurement() if key == "qnode_prim": - from .capture_qnode import _get_qnode_prim + from ..workflow._capture_qnode import _get_qnode_prim return _get_qnode_prim() @@ -204,7 +202,6 @@ def __getattr__(key): "create_measurement_obs_primitive", "create_measurement_wires_primitive", "create_measurement_mcm_primitive", - "qnode_call", "AbstractOperator", "AbstractMeasurement", "qnode_prim", diff --git a/pennylane/capture/primitives.py b/pennylane/capture/primitives.py index 5755c432b86..db3f8a36b2a 100644 --- a/pennylane/capture/primitives.py +++ b/pennylane/capture/primitives.py @@ -22,11 +22,11 @@ from pennylane.ops.op_math.adjoint import _get_adjoint_qfunc_prim from pennylane.ops.op_math.condition import _get_cond_qfunc_prim from pennylane.ops.op_math.controlled import _get_ctrl_qfunc_prim +from pennylane.workflow._capture_qnode import _get_qnode_prim from .capture_diff import _get_grad_prim, _get_jacobian_prim from .capture_measurements import _get_abstract_measurement from .capture_operators import _get_abstract_operator -from .capture_qnode import _get_qnode_prim AbstractOperator = _get_abstract_operator() AbstractMeasurement = _get_abstract_measurement() diff --git a/pennylane/capture/capture_qnode.py b/pennylane/workflow/_capture_qnode.py similarity index 98% rename from pennylane/capture/capture_qnode.py rename to pennylane/workflow/_capture_qnode.py index 1f792f28401..ffe62d13a5b 100644 --- a/pennylane/capture/capture_qnode.py +++ b/pennylane/workflow/_capture_qnode.py @@ -21,10 +21,9 @@ from warnings import warn import pennylane as qml +from pennylane.capture import FlatFn from pennylane.typing import TensorLike -from .flatfn import FlatFn - has_jax = True try: import jax @@ -135,9 +134,11 @@ def _(*args, qnode, shots, device, qnode_kwargs, qfunc_jaxpr, n_consts, batch_di *mps, shots=shots, num_device_wires=len(device.wires), batch_shape=batch_shape ) + # pylint: disable=too-many-arguments def _qnode_batching_rule( batched_args, batch_dims, + *, qnode, shots, device, @@ -209,7 +210,7 @@ def _qnode_jvp(args, tangents, **impl_kwargs): return qnode_prim -def qnode_call(qnode: "qml.QNode", *args, **kwargs) -> "qml.typing.Result": +def capture_qnode(qnode: "qml.QNode", *args, **kwargs) -> "qml.typing.Result": """A capture compatible call to a QNode. This function is internally used by ``QNode.__call__``. Args: diff --git a/pennylane/workflow/qnode.py b/pennylane/workflow/qnode.py index 3c5c5469c96..267df623385 100644 --- a/pennylane/workflow/qnode.py +++ b/pennylane/workflow/qnode.py @@ -33,6 +33,7 @@ from pennylane.tape import QuantumScript, QuantumScriptBatch, QuantumTape from pennylane.transforms.core import TransformContainer, TransformDispatcher, TransformProgram +from ._capture_qnode import capture_qnode from .execution import ( INTERFACE_MAP, SUPPORTED_INTERFACE_NAMES, @@ -1059,7 +1060,7 @@ def _impl_call(self, *args, **kwargs) -> qml.typing.Result: def __call__(self, *args, **kwargs) -> qml.typing.Result: if qml.capture.enabled(): - return qml.capture.qnode_call(self, *args, **kwargs) + return capture_qnode(self, *args, **kwargs) return self._impl_call(*args, **kwargs) From a66553c8ab494a4ab6de62d9904ba812551c639d Mon Sep 17 00:00:00 2001 From: "Yushao Chen (Jerry)" Date: Wed, 20 Nov 2024 18:05:25 -0500 Subject: [PATCH 25/35] Add a temporary new class DefaultMixedNewAPI for cleaner development (#6607) **Context:** In our Epic of porting `DefaultMixed` to new API standard, we realized #https://github.com/PennyLaneAI/pennylane/pull/6601/#pullrequestreview-2446682427 that direct changes to the original src are potentially risky since the legacy version and the new version are drastically different. Here we introduce a temporary name `DefaultMixedNewAPI` implemented as a second class inside the same `qml.devices.default_mixed` serving as a temporary class for holding the implementation of `preprocess` and `execute` of the new API of mixed qubit. **Description of the Change:** A new class within `qml.devices.default_mixed` named `DefaultMixedNewAPI`: - similar as qutrit mixed counterpart, see `qml.devices.default_qutrit_mixed` - preserves some input arguments from DefaultMixed, except for `analytic`, which accroding to the docs has already been deprecated - a dumb `execute` to avoid abstractclass error; will be implemented in other branch - a `support_derivatives` to check the supported diff method. - some imports are separated lined and the isort checks within this file disabled. **Benefits:** Better separation from legacy code. **Possible Drawbacks:** **Related GitHub Issues:** [sc-78775] --- doc/releases/changelog-dev.md | 3 + pennylane/devices/default_mixed.py | 111 +++++++++++++++++++++++++++- tests/devices/test_default_mixed.py | 70 ++++++++++++++++++ 3 files changed, 183 insertions(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 705d33bc519..08524a684f9 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -30,6 +30,9 @@ * Added submodule 'initialize_state' featuring a `create_initial_state` function for initializing a density matrix from `qml.StatePrep` operations or `qml.QubitDensityMatrix` operations. [(#6503)](https://github.com/PennyLaneAI/pennylane/pull/6503) +* Added a second class `DefaultMixedNewAPI` to the `qml.devices.qubit_mixed` module, which is to be the replacement of legacy `DefaultMixed` which for now to hold the implementations of `preprocess` and `execute` methods. + [(#6607)](https://github.com/PennyLaneAI/pennylane/pull/6507) +

Improvements 🛠

* Added support for the `wire_options` dictionary to customize wire line formatting in `qml.draw_mpl` circuit diff --git a/pennylane/devices/default_mixed.py b/pennylane/devices/default_mixed.py index 39f8a9ed4eb..c541c88c5a3 100644 --- a/pennylane/devices/default_mixed.py +++ b/pennylane/devices/default_mixed.py @@ -18,7 +18,8 @@ qubit :doc:`operations `, providing a simple mixed-state simulation of qubit-based quantum circuits. """ - +# isort: skip_file +# pylint: disable=wrong-import-order import functools import itertools import logging @@ -50,6 +51,13 @@ from .._version import __version__ from ._qubit_device import QubitDevice +# We deliberately separate the imports to avoid confusion with the legacy device +from typing import Optional +from pennylane.tape import QuantumScript +from . import Device +from .execution_config import ExecutionConfig +from .modifiers import simulator_tracking, single_tape_support + logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) @@ -186,6 +194,7 @@ def _asarray(array, dtype=None): res = qnp.cast(array, dtype=dtype) return res + # pylint: disable=too-many-arguments @debug_logger_init def __init__( self, @@ -796,3 +805,103 @@ def apply(self, operations, rotations=None, **kwargs): for k in self.measured_wires: bit_flip = qml.BitFlip(self.readout_err, wires=k) self._apply_operation(bit_flip) + + +@simulator_tracking +@single_tape_support +class DefaultMixedNewAPI(Device): + r"""A PennyLane Python-based device for mixed-state qubit simulation. + + Args: + wires (int, Iterable[Number, str]): Number of wires present on the device, or iterable that + contains unique labels for the wires as numbers (i.e., ``[-1, 0, 2]``) or strings + (``['ancilla', 'q1', 'q2']``). + shots (int, Sequence[int], Sequence[Union[int, Sequence[int]]]): The default number of shots + to use in executions involving this device. + seed (Union[str, None, int, array_like[int], SeedSequence, BitGenerator, Generator, jax.random.PRNGKey]): 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. + If a ``jax.random.PRNGKey`` is passed as the seed, a JAX-specific sampling function using + ``jax.random.choice`` and the ``PRNGKey`` will be used for sampling rather than + ``numpy.random.default_rng``. + r_dtype (numpy.dtype): Real datatype to use for computations. Default is np.float64. + c_dtype (numpy.dtype): Complex datatype to use for computations. Default is np.complex128. + readout_prob (float): Probability of readout error for qubit measurements. Must be in :math:`[0,1]`. + """ + + _device_options = ("rng", "prng_key") # tuple of string names for all the device options. + + @property + def name(self): + """The name of the device.""" + return "default.mixed" + + # pylint: disable=too-many-positional-arguments + @debug_logger_init + def __init__( # pylint: disable=too-many-arguments + self, + wires=None, + shots=None, + seed="global", + # The following parameters are inherited from DefaultMixed + readout_prob=None, + ) -> None: + + if isinstance(wires, int) and wires > 23: + raise ValueError( + "This device does not currently support computations on more than 23 wires" + ) + + self.readout_err = readout_prob + # Check that the readout error probability, if entered, is either integer or float in [0,1] + if self.readout_err is not None: + if not isinstance(self.readout_err, float) and not isinstance(self.readout_err, int): + raise TypeError( + "The readout error probability should be an integer or a floating-point number in [0,1]." + ) + if self.readout_err < 0 or self.readout_err > 1: + raise ValueError("The readout error probability should be in the range [0,1].") + super().__init__(wires=wires, shots=shots) + + # Seed setting + seed = np.random.randint(0, high=10000000) if seed == "global" else seed + if qml.math.get_interface(seed) == "jax": + self._prng_key = seed + self._rng = np.random.default_rng(None) + else: + self._prng_key = None + self._rng = np.random.default_rng(seed) + + self._debugger = None + + @debug_logger + def supports_derivatives( + self, + execution_config: Optional[ExecutionConfig] = None, + circuit: Optional[QuantumScript] = None, + ) -> bool: + """Check whether or not derivatives are available for a given configuration and circuit. + + ``DefaultQubitMixed`` supports backpropagation derivatives 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 or execution_config.gradient_method in {"backprop", "best"}: + return circuit is None or not circuit.shots + return False + + @debug_logger + def execute( + self, + circuits: QuantumScript, + execution_config: Optional[ExecutionConfig] = None, + ) -> None: + raise NotImplementedError diff --git a/tests/devices/test_default_mixed.py b/tests/devices/test_default_mixed.py index abed12c5699..e298554b0c6 100644 --- a/tests/devices/test_default_mixed.py +++ b/tests/devices/test_default_mixed.py @@ -24,6 +24,7 @@ import pennylane as qml from pennylane import BasisState, DeviceError, StatePrep from pennylane.devices import DefaultMixed +from pennylane.devices.default_mixed import DefaultMixedNewAPI from pennylane.ops import ( CNOT, CZ, @@ -1296,3 +1297,72 @@ def test_analytic_deprecation(self): match=msg, ): qml.device("default.mixed", wires=1, shots=1, analytic=True) + + +class TestDefaultMixedNewAPIInit: + """Unit tests for DefaultMixedNewAPI initialization""" + + def test_name_property(self): + """Test the name property returns correct device name""" + dev = DefaultMixedNewAPI(wires=1) + assert dev.name == "default.mixed" + + @pytest.mark.parametrize("readout_prob", [-0.1, 1.1, 2.0]) + def test_readout_probability_validation(self, readout_prob): + """Test readout probability validation during initialization""" + with pytest.raises(ValueError, match="readout error probability should be in the range"): + DefaultMixedNewAPI(wires=1, readout_prob=readout_prob) + + @pytest.mark.parametrize("readout_prob", ["0.5", [0.5], (0.5,)]) + def test_readout_probability_type_validation(self, readout_prob): + """Test readout probability type validation""" + with pytest.raises(TypeError, match="readout error probability should be an integer"): + DefaultMixedNewAPI(wires=1, readout_prob=readout_prob) + + def test_seed_global(self): + """Test global seed initialization""" + dev = DefaultMixedNewAPI(wires=1, seed="global") + assert dev._rng is not None + assert dev._prng_key is None + + @pytest.mark.jax + def test_seed_jax(self): + """Test JAX PRNGKey seed initialization""" + # pylint: disable=import-outside-toplevel + import jax + + dev = DefaultMixedNewAPI(wires=1, seed=jax.random.PRNGKey(0)) + assert dev._rng is not None + assert dev._prng_key is not None + + def test_supports_derivatives(self): + """Test supports_derivatives method""" + dev = DefaultMixedNewAPI(wires=1) + assert dev.supports_derivatives() + assert not dev.supports_derivatives( + execution_config=qml.devices.execution_config.ExecutionConfig( + gradient_method="finite-diff" + ) + ) + + @pytest.mark.parametrize("nr_wires", [1, 2, 3, 10, 22]) + def test_valid_wire_numbers(self, nr_wires): + """Test initialization with different valid wire numbers""" + dev = DefaultMixedNewAPI(wires=nr_wires) + assert len(dev.wires) == nr_wires + + def test_wire_initialization_list(self): + """Test initialization with wire list""" + dev = DefaultMixedNewAPI(wires=["a", "b", "c"]) + assert dev.wires == qml.wires.Wires(["a", "b", "c"]) + + def test_too_many_wires(self): + """Test error raised when too many wires requested""" + with pytest.raises(ValueError, match="This device does not currently support"): + DefaultMixedNewAPI(wires=24) + + def test_execute(self): + """Test that the execute method is defined""" + dev = DefaultMixedNewAPI(wires=[0, 1]) + with pytest.raises(NotImplementedError): + dev.execute(qml.tape.QuantumScript()) From d377b76570e08541d82b4c39210ec063047eb4f8 Mon Sep 17 00:00:00 2001 From: Will Date: Thu, 21 Nov 2024 01:24:41 -0500 Subject: [PATCH 26/35] Single qubit resource operators (#6478) Adding single quibt operations [sc-76760] --------- Co-authored-by: Jay Soni Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com> --- .../labs/resource_estimation/__init__.py | 20 +++++ .../labs/resource_estimation/ops/__init__.py | 13 +++ .../labs/resource_estimation/ops/identity.py | 50 +++++++++++ .../resource_estimation/ops/qubit/__init__.py | 11 +-- .../ops/qubit/non_parametric_ops.py | 83 +++++++++++++++++ .../ops/qubit/parametric_ops_single_qubit.py | 80 +++++++++++++++++ .../resource_estimation/resource_operator.py | 2 +- .../templates/subroutines.py | 1 - .../ops/qubit/test_non_parametric_ops.py | 37 +++++++- .../qubit/test_parametric_ops_single_qubit.py | 89 +++++++++++++++---- .../resource_estimation/ops/test_identity.py | 77 ++++++++++++++++ 11 files changed, 431 insertions(+), 32 deletions(-) create mode 100644 pennylane/labs/resource_estimation/ops/identity.py create mode 100644 pennylane/labs/tests/resource_estimation/ops/test_identity.py diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index 1a59f926329..3c7f2a97861 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -40,10 +40,20 @@ ~ResourceCNOT ~ResourceControlledPhaseShift + ~ResourceGlobalPhase ~ResourceHadamard + ~ResourceIdentity + ~ResourcePhaseShift + ~ResourceRot + ~ResourceRX + ~ResourceRY ~ResourceRZ + ~ResourceS ~ResourceSWAP ~ResourceT + ~ResourceX + ~ResourceY + ~ResourceZ Templates ~~~~~~~~~ @@ -68,10 +78,20 @@ from .ops import ( ResourceCNOT, ResourceControlledPhaseShift, + ResourceGlobalPhase, ResourceHadamard, + ResourceIdentity, + ResourcePhaseShift, + ResourceRot, + ResourceRX, + ResourceRY, ResourceRZ, + ResourceS, ResourceSWAP, ResourceT, + ResourceX, + ResourceY, + ResourceZ, ) from .templates import ( diff --git a/pennylane/labs/resource_estimation/ops/__init__.py b/pennylane/labs/resource_estimation/ops/__init__.py index 509a0a0230a..23d45a734f5 100644 --- a/pennylane/labs/resource_estimation/ops/__init__.py +++ b/pennylane/labs/resource_estimation/ops/__init__.py @@ -13,11 +13,24 @@ # limitations under the License. r"""This module contains resource operators for PennyLane Operators""" +from .identity import ( + ResourceGlobalPhase, + ResourceIdentity, +) + from .qubit import ( ResourceHadamard, + ResourceRot, + ResourceRX, + ResourceRY, ResourceRZ, + ResourcePhaseShift, + ResourceS, ResourceSWAP, ResourceT, + ResourceX, + ResourceY, + ResourceZ, ) from .op_math import ( diff --git a/pennylane/labs/resource_estimation/ops/identity.py b/pennylane/labs/resource_estimation/ops/identity.py new file mode 100644 index 00000000000..7f9b59729db --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/identity.py @@ -0,0 +1,50 @@ +# Copyright 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. +r"""Resource operators for identity operations.""" +from typing import Dict + +import pennylane as qml +import pennylane.labs.resource_estimation as re + +# pylint: disable=arguments-differ,no-self-use,too-many-ancestors + + +class ResourceIdentity(qml.Identity, re.ResourceOperator): + """Resource class for the Identity gate.""" + + @staticmethod + def _resource_decomp(*args, **kwargs) -> Dict[re.CompressedResourceOp, int]: + return {} + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls, **kwargs) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) + + +class ResourceGlobalPhase(qml.GlobalPhase, re.ResourceOperator): + """Resource class for the GlobalPhase gate.""" + + @staticmethod + def _resource_decomp(*args, **kwargs) -> Dict[re.CompressedResourceOp, int]: + return {} + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls, **kwargs) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) diff --git a/pennylane/labs/resource_estimation/ops/qubit/__init__.py b/pennylane/labs/resource_estimation/ops/qubit/__init__.py index 960e4f116c2..429671eae0b 100644 --- a/pennylane/labs/resource_estimation/ops/qubit/__init__.py +++ b/pennylane/labs/resource_estimation/ops/qubit/__init__.py @@ -13,12 +13,5 @@ # limitations under the License. r"""This module contains experimental resource estimation functionality. """ -from .non_parametric_ops import ( - ResourceHadamard, - ResourceSWAP, - ResourceT, -) - -from .parametric_ops_single_qubit import ( - ResourceRZ, -) +from .non_parametric_ops import * +from .parametric_ops_single_qubit import * diff --git a/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py b/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py index e3eb9fd450e..7a3511022d4 100644 --- a/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py +++ b/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py @@ -35,6 +35,25 @@ def resource_rep(cls) -> re.CompressedResourceOp: return re.CompressedResourceOp(cls, {}) +class ResourceS(qml.S, re.ResourceOperator): + """Resource class for the S gate.""" + + @staticmethod + def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: + gate_types = {} + t = ResourceT.resource_rep(**kwargs) + gate_types[t] = 2 + + return gate_types + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) + + class ResourceSWAP(qml.SWAP, re.ResourceOperator): r"""Resource class for the SWAP gate. @@ -99,3 +118,67 @@ def resource_params(self) -> dict: @classmethod def resource_rep(cls) -> re.CompressedResourceOp: return re.CompressedResourceOp(cls, {}) + + +class ResourceX(qml.X, re.ResourceOperator): + """Resource class for the X gate.""" + + @staticmethod + def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: + s = re.ResourceS.resource_rep(**kwargs) + h = re.ResourceHadamard.resource_rep(**kwargs) + + gate_types = {} + gate_types[s] = 2 + gate_types[h] = 2 + + return gate_types + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) + + +class ResourceY(qml.Y, re.ResourceOperator): + """Resource class for the Y gate.""" + + @staticmethod + def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: + s = re.ResourceS.resource_rep(**kwargs) + h = re.ResourceHadamard.resource_rep(**kwargs) + + gate_types = {} + gate_types[s] = 6 + gate_types[h] = 2 + + return gate_types + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) + + +class ResourceZ(qml.Z, re.ResourceOperator): + """Resource class for the Z gate.""" + + @staticmethod + def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: + s = re.ResourceS.resource_rep(**kwargs) + + gate_types = {} + gate_types[s] = 2 + + return gate_types + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) diff --git a/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py b/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py index b22c5b7ce86..572c12f9ebd 100644 --- a/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py +++ b/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py @@ -33,6 +33,66 @@ def _rotation_resources(epsilon=10e-3): return gate_types +class ResourcePhaseShift(qml.PhaseShift, re.ResourceOperator): + r""" + Resource class for the PhaseShift gate. + + The resources are defined from the following identity: + + .. math:: R_\phi(\phi) = e^{i\phi/2}R_z(\phi) = \begin{bmatrix} + 1 & 0 \\ + 0 & e^{i\phi} + \end{bmatrix}. + """ + + @staticmethod + def _resource_decomp() -> Dict[re.CompressedResourceOp, int]: + gate_types = {} + rz = re.ResourceRZ.resource_rep() + global_phase = re.ResourceGlobalPhase.resource_rep() + gate_types[rz] = 1 + gate_types[global_phase] = 1 + + return gate_types + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) + + +class ResourceRX(qml.RX, re.ResourceOperator): + """Resource class for the RX gate.""" + + @staticmethod + def _resource_decomp(config) -> Dict[re.CompressedResourceOp, int]: + return _rotation_resources(epsilon=config["error_rx"]) + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) + + +class ResourceRY(qml.RY, re.ResourceOperator): + """Resource class for the RY gate.""" + + @staticmethod + def _resource_decomp(config) -> Dict[re.CompressedResourceOp, int]: + return _rotation_resources(epsilon=config["error_ry"]) + + def resource_params(self) -> dict: + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) + + class ResourceRZ(qml.RZ, re.ResourceOperator): r"""Resource class for the RZ gate. @@ -51,3 +111,23 @@ def resource_params(self) -> dict: @classmethod def resource_rep(cls) -> re.CompressedResourceOp: return re.CompressedResourceOp(cls, {}) + + +class ResourceRot(qml.Rot, re.ResourceOperator): + """Resource class for the Rot gate.""" + + @staticmethod + def _resource_decomp() -> Dict[re.CompressedResourceOp, int]: + rx = ResourceRX.resource_rep() + ry = ResourceRY.resource_rep() + rz = ResourceRZ.resource_rep() + + gate_types = {rx: 1, ry: 1, rz: 1} + return gate_types + + def resource_params(self): + return {} + + @classmethod + def resource_rep(cls) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {}) diff --git a/pennylane/labs/resource_estimation/resource_operator.py b/pennylane/labs/resource_estimation/resource_operator.py index cc580d6bafd..5455bbc8df2 100644 --- a/pennylane/labs/resource_estimation/resource_operator.py +++ b/pennylane/labs/resource_estimation/resource_operator.py @@ -41,7 +41,7 @@ class ResourceOperator(ABC): class ResourceQFT(qml.QFT, ResourceOperator): @staticmethod - def _resource_decomp(num_wires) -> dict[CompressedResourceOp, int]: + def _resource_decomp(num_wires) -> Dict[CompressedResourceOp, int]: gate_types = {} hadamard = ResourceHadamard.resource_rep() diff --git a/pennylane/labs/resource_estimation/templates/subroutines.py b/pennylane/labs/resource_estimation/templates/subroutines.py index 9970ef940ec..43514278e63 100644 --- a/pennylane/labs/resource_estimation/templates/subroutines.py +++ b/pennylane/labs/resource_estimation/templates/subroutines.py @@ -33,7 +33,6 @@ class ResourceQFT(qml.QFT, ResourceOperator): The resources are obtained from the standard decomposition of QFT as presented in (chapter 5) `Nielsen, M.A. and Chuang, I.L. (2011) Quantum Computation and Quantum Information `_. - """ @staticmethod diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py index 3ebc3a0ed9c..b59c519d71d 100644 --- a/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py +++ b/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py @@ -66,14 +66,43 @@ def test_resources_from_rep(self): """Test that the resources can be computed from the compressed representation""" op = re.ResourceSWAP([0, 1]) - cnot = re.ResourceCNOT.resource_rep() - expected = {cnot: 3} + expected = {re.ResourceCNOT.resource_rep(): 3} op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type op_resource_params = op_compressed_rep.params - op_compressed_rep_type = op_compressed_rep.op_type + assert op_resource_type.resources(**op_resource_params) == expected + + +class TestS: + """Tests for ResourceS""" + + def test_resources(self): + """Test that S decomposes into two Ts""" + op = re.ResourceS(0) + expected = {re.CompressedResourceOp(re.ResourceT, {}): 2} + assert op.resources() == expected - assert op_compressed_rep_type.resources(**op_resource_params) == expected + def test_resource_params(self): + """Test that the resource params are correct""" + op = re.ResourceS(0) + assert op.resource_params() == {} + + def test_resource_rep(self): + """Test that the compressed representation is correct""" + expected = re.CompressedResourceOp(re.ResourceS, {}) + assert re.ResourceS.resource_rep() == expected + + def test_resources_from_rep(self): + """Test that the resources can be computed from the compressed representation""" + + op = re.ResourceS(0) + expected = {re.ResourceT.resource_rep(): 2} + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resources(**op_resource_params) == expected class TestT: diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py index 957282cae1e..f93c363dbde 100644 --- a/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py +++ b/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py @@ -36,31 +36,86 @@ def test_rotation_resources(epsilon, expected): assert gate_types == _rotation_resources(epsilon=epsilon) -class TestRZ: - """Test ResourceRZ""" +class TestPauliRotation: + """Test ResourceRX, ResourceRY, and ResourceRZ""" - @pytest.mark.parametrize("epsilon", [10e-3, 10e-4, 10e-5]) - def test_resources(self, epsilon): + params_classes = [re.ResourceRX, re.ResourceRY, re.ResourceRZ] + params_errors = [10e-3, 10e-4, 10e-5] + + @pytest.mark.parametrize("resource_class", params_classes) + @pytest.mark.parametrize("epsilon", params_errors) + def test_resources(self, resource_class, epsilon): """Test the resources method""" - op = re.ResourceRZ(1.24, wires=0) - config = {"error_rz": epsilon} + + label = "error_" + resource_class.__name__.replace("Resource", "").lower() + config = {label: epsilon} + op = resource_class(1.24, wires=0) assert op.resources(config) == _rotation_resources(epsilon=epsilon) - def test_resource_rep(self): + @pytest.mark.parametrize("resource_class", params_classes) + @pytest.mark.parametrize("epsilon", params_errors) + def test_resource_rep(self, resource_class, epsilon): # pylint: disable=unused-argument """Test the compact representation""" - op = re.ResourceRZ(1.24, wires=0) - expected = re.CompressedResourceOp(re.ResourceRZ, {}) + op = resource_class(1.24, wires=0) + expected = re.CompressedResourceOp(resource_class, {}) + assert op.resource_rep() == expected + + @pytest.mark.parametrize("resource_class", params_classes) + @pytest.mark.parametrize("epsilon", params_errors) + def test_resources_from_rep(self, resource_class, epsilon): + """Test the resources can be obtained from the compact representation""" + + label = "error_" + resource_class.__name__.replace("Resource", "").lower() + config = {label: epsilon} + op = resource_class(1.24, wires=0) + expected = _rotation_resources(epsilon=epsilon) + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resources(**op_resource_params, config=config) == expected + + @pytest.mark.parametrize("resource_class", params_classes) + @pytest.mark.parametrize("epsilon", params_errors) + def test_resource_params(self, resource_class, epsilon): # pylint: disable=unused-argument + """Test that the resource params are correct""" + op = resource_class(1.24, wires=0) + assert op.resource_params() == {} + +class TestRot: + """Test ResourceRot""" + + def test_resources(self): + """Test the resources method""" + op = re.ResourceRot(0.1, 0.2, 0.3, wires=0) + rx = re.ResourceRX.resource_rep() + ry = re.ResourceRY.resource_rep() + rz = re.ResourceRZ.resource_rep() + expected = {rx: 1, ry: 1, rz: 1} + + assert op.resources() == expected + + def test_resource_rep(self): + """Test the compressed representation""" + op = re.ResourceRot(0.1, 0.2, 0.3, wires=0) + expected = re.CompressedResourceOp(re.ResourceRot, {}) assert op.resource_rep() == expected + def test_resources_from_rep(self): + """Test that the resources can be obtained from the compact representation""" + op = re.ResourceRot(0.1, 0.2, 0.3, wires=0) + rx = re.CompressedResourceOp(re.ResourceRX, {}) + ry = re.CompressedResourceOp(re.ResourceRY, {}) + rz = re.CompressedResourceOp(re.ResourceRZ, {}) + expected = {rx: 1, ry: 1, rz: 1} + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resources(**op_resource_params) == expected + def test_resource_params(self): """Test that the resource params are correct""" - op = re.ResourceRZ(1.24, wires=0) + op = re.ResourceRot(0.1, 0.2, 0.3, wires=0) assert op.resource_params() == {} - - @pytest.mark.parametrize("epsilon", [10e-3, 10e-4, 10e-5]) - def test_resources_from_rep(self, epsilon): - """Test the resources can be obtained from the compact representation""" - config = {"error_rz": epsilon} - expected = _rotation_resources(epsilon=epsilon) - assert re.ResourceRZ.resources(config, **re.ResourceRZ.resource_rep().params) == expected diff --git a/pennylane/labs/tests/resource_estimation/ops/test_identity.py b/pennylane/labs/tests/resource_estimation/ops/test_identity.py new file mode 100644 index 00000000000..c1cf7d04528 --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/ops/test_identity.py @@ -0,0 +1,77 @@ +# Copyright 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 identity resource operators +""" +import pennylane.labs.resource_estimation as re + +# pylint: disable=no-self-use,use-implicit-booleaness-not-comparison + + +class TestIdentity: + """Test ResourceIdentity""" + + def test_resources(self): + """ResourceIdentity should have empty resources""" + op = re.ResourceIdentity() + assert op.resources() == {} + + def test_resource_rep(self): + """Test the compressed representation""" + expected = re.CompressedResourceOp(re.ResourceIdentity, {}) + assert re.ResourceIdentity.resource_rep() == expected + + def test_resource_params(self): + """Test the resource params are correct""" + op = re.ResourceIdentity(0) + assert op.resource_params() == {} + + def test_resources_from_rep(self): + """Test that the resources can be computed from the compressed representation""" + op = re.ResourceIdentity() + expected = {} + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resources(**op_resource_params) == expected + + +class TestGlobalPhase: + """Test ResourceGlobalPhase""" + + def test_resources(self): + """ResourceGlobalPhase should have empty resources""" + op = re.ResourceGlobalPhase(0.1, wires=0) + assert op.resources() == {} + + def test_resource_rep(self): + """Test the compressed representation""" + expected = re.CompressedResourceOp(re.ResourceGlobalPhase, {}) + assert re.ResourceGlobalPhase.resource_rep() == expected + + def test_resource_params(self): + """Test the resource params are correct""" + op = re.ResourceGlobalPhase(0.1, wires=0) + assert op.resource_params() == {} + + def test_resources_from_rep(self): + """Test that the resources can be computed from the compressed representation""" + op = re.ResourceGlobalPhase(0.1, wires=0) + expected = {} + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resources(**op_resource_params) == expected From 08e694f9b3ab4f42363b06fd44e7de6357c526f2 Mon Sep 17 00:00:00 2001 From: Will Date: Thu, 21 Nov 2024 02:33:01 -0500 Subject: [PATCH 27/35] Add and Multiply operations for Resource objects (#6567) This PR adds `add_in_series`, `add_in_parallel`, `mul_in_series`, and `mul_in_parallel` for the `Resource` objects. [sc-77943]. --------- Co-authored-by: ANTH0NY <39093564+AntonNI8@users.noreply.github.com> Co-authored-by: Jay Soni Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com> --- doc/releases/changelog-dev.md | 3 + pennylane/measurements/__init__.py | 2 +- pennylane/measurements/shots.py | 37 +++ pennylane/resource/__init__.py | 22 +- pennylane/resource/resource.py | 403 ++++++++++++++++++++++++++++- tests/measurements/test_shots.py | 28 +- tests/resource/test_resource.py | 276 +++++++++++++++++++- 7 files changed, 766 insertions(+), 5 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 08524a684f9..0064f239c96 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -42,6 +42,9 @@ * Shortened the string representation for the `qml.S`, `qml.T`, and `qml.SX` operators. [(#6542)](https://github.com/PennyLaneAI/pennylane/pull/6542) +* Added functions and dunder methods to add and multiply Resources objects in series and in parallel. + [(#6567)](https://github.com/PennyLaneAI/pennylane/pull/6567) +

Capturing and representing hybrid programs

* `jax.vmap` can be captured with `qml.capture.make_plxpr` and is compatible with quantum circuits. diff --git a/pennylane/measurements/__init__.py b/pennylane/measurements/__init__.py index 3129a92069d..b7eb1fcfb99 100644 --- a/pennylane/measurements/__init__.py +++ b/pennylane/measurements/__init__.py @@ -294,7 +294,7 @@ def circuit(x): from .probs import ProbabilityMP, probs from .purity import PurityMP, purity from .sample import SampleMP, sample -from .shots import ShotCopies, Shots +from .shots import ShotCopies, Shots, add_shots from .state import DensityMatrixMP, StateMP, density_matrix, state from .var import VarianceMP, var from .vn_entropy import VnEntropyMP, vn_entropy diff --git a/pennylane/measurements/shots.py b/pennylane/measurements/shots.py index 66fafb5ae67..9268d1a5381 100644 --- a/pennylane/measurements/shots.py +++ b/pennylane/measurements/shots.py @@ -122,6 +122,11 @@ class Shots: >>> Shots([7, (100, 2)]) * 1.5 Shots(total_shots=310, shot_vector=(ShotCopies(10 shots x 1), ShotCopies(150 shots x 2))) + Example constructing a Shots instance by adding two existing instances together: + + >>> Shots(100) + Shots(((10,2),)) + Shots(total_shots=120, shot_vector=(ShotCopies(100 shots x 1), ShotCopies(10 shots x 2))) + One should also note that specifying a single tuple of length 2 is considered two different shot values, and *not* a tuple-pair representing shots and copies to avoid special behaviour depending on the iterable type: @@ -230,6 +235,9 @@ def __all_tuple_init__(self, shots: Sequence[tuple]): def __bool__(self): return self.total_shots is not None + def __add__(self, other): + return add_shots(self, other) + def __mul__(self, scalar): if not isinstance(scalar, (int, float)): raise TypeError("Can't multiply Shots with non-integer or float type.") @@ -282,3 +290,32 @@ def bins(self): ShotsLike = Union[Shots, None, int, Sequence[Union[int, tuple[int, int]]]] + + +def add_shots(s1: Shots, s2: Shots) -> Shots: + """Add two :class:`~.Shots` objects by concatenating their shot vectors. + + Args: + s1 (Shots): a Shots object to add + s2 (Shots): a Shots object to add + + Returns: + Shots: a :class:`~.Shots` object built by concatenating the shot vectors of ``s1`` and ``s2`` + + Example: + >>> s1 = Shots((5, (10, 2))) + >>> s2 = Shots((3, 2, (10, 3))) + >>> print(qml.measurements.add_shots(s1, s2)) + Shots(total=60, vector=[5 shots, 10 shots x 2, 3 shots, 2 shots, 10 shots x 3]) + """ + if s1.total_shots is None: + return s2 + + if s2.total_shots is None: + return s1 + + shot_vector = [] + for shot in s1.shot_vector + s2.shot_vector: + shot_vector.append((shot.shots, shot.copies)) + + return Shots(shots=shot_vector) diff --git a/pennylane/resource/__init__.py b/pennylane/resource/__init__.py index a92248594c2..1f8135bdb4f 100644 --- a/pennylane/resource/__init__.py +++ b/pennylane/resource/__init__.py @@ -72,6 +72,19 @@ ~Resources ~ResourcesOperation +Resource Functions +~~~~~~~~~~~~~~~~~~ + +.. currentmodule:: pennylane.resource + +.. autosummary:: + :toctree: api + + ~add_in_series + ~add_in_parallel + ~mul_in_series + ~mul_in_parallel + Tracking Resources for Custom Operations ---------------------------------------- @@ -122,6 +135,13 @@ def circuit(theta): from .error import AlgorithmicError, ErrorOperation, SpectralNormError from .first_quantization import FirstQuantization from .measurement import estimate_error, estimate_shots -from .resource import Resources, ResourcesOperation +from .resource import ( + Resources, + ResourcesOperation, + add_in_series, + add_in_parallel, + mul_in_series, + mul_in_parallel, +) from .second_quantization import DoubleFactorization from .specs import specs diff --git a/pennylane/resource/resource.py b/pennylane/resource/resource.py index 5a14facdd1c..63af52a36b3 100644 --- a/pennylane/resource/resource.py +++ b/pennylane/resource/resource.py @@ -14,11 +14,14 @@ """ Stores classes and logic to aggregate all the resource information from a quantum workflow. """ +from __future__ import annotations + +import copy from abc import abstractmethod from collections import defaultdict from dataclasses import dataclass, field -from pennylane.measurements import Shots +from pennylane.measurements import Shots, add_shots from pennylane.operation import Operation @@ -44,6 +47,7 @@ class Resources: **Example** + >>> from pennylane.resource import Resources >>> r = Resources(num_wires=2, num_gates=2, gate_types={'Hadamard': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1}, depth=2) >>> print(r) num_wires: 2 @@ -54,6 +58,30 @@ class Resources: {'Hadamard': 1, 'CNOT': 1} gate_sizes: {1: 1, 2: 1} + + :class:`~.Resources` objects can be added together or multiplied by a scalar. + + >>> from pennylane.resource import Resources + >>> r1 = Resources(num_wires=2, num_gates=2, gate_types={'Hadamard': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1}, depth=2) + >>> r2 = Resources(num_wires=2, num_gates=2, gate_types={'RX': 1, 'CNOT':1}, gate_sizes={1: 1, 2: 1}, depth=2) + >>> print(r1 + r2) + wires: 2 + gates: 4 + depth: 4 + shots: Shots(total=None) + gate_types: + {'Hadamard': 1, 'CNOT': 2, 'RX': 1} + gate_sizes: + {1: 2, 2: 2} + >>> print(r1 * 2) + wires: 2 + gates: 4 + depth: 4 + shots: Shots(total=None) + gate_types: + {'Hadamard': 2, 'CNOT': 2} + gate_sizes: + {1: 2, 2: 2} """ num_wires: int = 0 @@ -63,6 +91,103 @@ class Resources: depth: int = 0 shots: Shots = field(default_factory=Shots) + def __add__(self, other: Resources): + r"""Adds two :class:`~resource.Resources` objects together as if the circuits were executed in series. + + Args: + other (Resources): the resource object to add + + Returns: + Resources: the combined resources + + .. details:: + + **Example** + + First we build two :class:`~.resource.Resources` objects. + + .. code-block:: python3 + + from pennylane.measurements import Shots + from pennylane.resource import Resources + + r1 = Resources( + num_wires = 2, + num_gates = 2, + gate_types = {"Hadamard": 1, "CNOT": 1}, + gate_sizes = {1: 1, 2: 1}, + depth = 2, + shots = Shots(10) + ) + + r2 = Resources( + num_wires = 3, + num_gates = 2, + gate_types = {"RX": 1, "CNOT": 1}, + gate_sizes = {1: 1, 2: 1}, + depth = 1, + shots = Shots((5, (2, 10))) + ) + + Now we print their sum. + + >>> print(r1 + r2) + wires: 3 + gates: 4 + depth: 3 + shots: Shots(total=35, vector=[10 shots, 5 shots, 2 shots x 10]) + gate_types: + {'Hadamard': 1, 'CNOT': 2, 'RX': 1} + gate_sizes: + {1: 2, 2: 2} + """ + return add_in_series(self, other) + + def __mul__(self, scalar: int): + r"""Multiply the :class:`~resource.Resources` object by a scalar as if that many copies of the circuit were executed in series + + Args: + scalar (int): the scalar to multiply the resource object by + + Returns: + Resources: the combined resources + + .. details:: + + **Example** + + First we build a :class:`~.resource.Resources` object. + + .. code-block:: python3 + + from pennylane.measurements import Shots + from pennylane.resource import Resources + + resources = Resources( + num_wires = 2, + num_gates = 2, + gate_types = {"Hadamard": 1, "CNOT": 1}, + gate_sizes = {1: 1, 2: 1}, + depth = 2, + shots = Shots(10) + ) + + Now we print the product. + + >>> print(resources * 2) + wires: 2 + gates: 4 + depth: 4 + shots: Shots(total=20) + gate_types: + {'Hadamard': 2, 'CNOT': 2} + gate_sizes: + {1: 2, 2: 2} + """ + return mul_in_series(self, scalar) + + __rmul__ = __mul__ + def __str__(self): keys = ["num_wires", "num_gates", "depth"] vals = [self.num_wires, self.num_gates, self.depth] @@ -125,6 +250,282 @@ def resources(self) -> Resources: """ +def add_in_series(r1: Resources, r2: Resources) -> Resources: + r""" + Add two :class:`~.resource.Resources` objects assuming the circuits are executed in series. + + The gates in ``r1`` and ``r2`` are assumed to act on the same qubits. The resulting circuit + depth is the sum of the depths of ``r1`` and ``r2``. To add resources as if they were executed + in parallel see :func:`~.resource.add_in_parallel`. + + Args: + r1 (Resources): a :class:`~resource.Resources` to add + r2 (Resources): a :class:`~resource.Resources` to add + + Returns: + Resources: the combined resources + + .. details:: + + **Example** + + First we build two :class:`~.resource.Resources` objects. + + .. code-block:: python3 + + from pennylane.measurements import Shots + from pennylane.resource import Resources + + r1 = Resources( + num_wires = 2, + num_gates = 2, + gate_types = {"Hadamard": 1, "CNOT": 1}, + gate_sizes = {1: 1, 2: 1}, + depth = 2, + shots = Shots(10) + ) + + r2 = Resources( + num_wires = 3, + num_gates = 2, + gate_types = {"RX": 1, "CNOT": 1}, + gate_sizes = {1: 1, 2: 1}, + depth = 1, + shots = Shots((5, (2, 10))) + ) + + Now we print their sum. + + >>> print(qml.resource.add_in_series(r1, r2)) + wires: 3 + gates: 4 + depth: 3 + shots: Shots(total=35, vector=[10 shots, 5 shots, 2 shots x 10]) + gate_types: + {'Hadamard': 1, 'CNOT': 2, 'RX': 1} + gate_sizes: + {1: 2, 2: 2} + """ + + new_wires = max(r1.num_wires, r2.num_wires) + new_gates = r1.num_gates + r2.num_gates + new_gate_types = _combine_dict(r1.gate_types, r2.gate_types) + new_gate_sizes = _combine_dict(r1.gate_sizes, r2.gate_sizes) + new_shots = add_shots(r1.shots, r2.shots) + new_depth = r1.depth + r2.depth + + return Resources(new_wires, new_gates, new_gate_types, new_gate_sizes, new_depth, new_shots) + + +def add_in_parallel(r1: Resources, r2: Resources) -> Resources: + r""" + Add two :class:`~.resource.Resources` objects assuming the circuits are executed in parallel. + + The gates in ``r2`` and ``r2`` are assumed to act on disjoint sets of qubits. The resulting + circuit depth is the max depth of ``r1`` and ``r2``. To add resources as if they were executed + in series see :func:`~.resource.add_in_series`. + + Args: + r1 (Resources): a :class:`~.resource.Resources` object to add + r2 (Resources): a :class:`~.resource.Resources` object to add + + Returns: + Resources: the combined resources + + .. details:: + + **Example** + + First we build two :class:`~.resource.Resources` objects. + + .. code-block:: python3 + + from pennylane.measurements import Shots + from pennylane.resource import Resources + + r1 = Resources( + num_wires = 2, + num_gates = 2, + gate_types = {"Hadamard": 1, "CNOT": 1}, + gate_sizes = {1: 1, 2: 1}, + depth = 2, + shots = Shots(10) + ) + + r2 = Resources( + num_wires = 3, + num_gates = 2, + gate_types = {"RX": 1, "CNOT": 1}, + gate_sizes = {1: 1, 2: 1}, + depth = 1, + shots = Shots((5, (2, 10))) + ) + + Now we print their sum. + + >>> print(qml.resource.add_in_parallel(r1, r2)) + wires: 5 + gates: 4 + depth: 2 + shots: Shots(total=35, vector=[10 shots, 5 shots, 2 shots x 10]) + gate_types: + {'Hadamard': 1, 'CNOT': 2, 'RX': 1} + gate_sizes: + {1: 2, 2: 2} + """ + + new_wires = r1.num_wires + r2.num_wires + new_gates = r1.num_gates + r2.num_gates + new_gate_types = _combine_dict(r1.gate_types, r2.gate_types) + new_gate_sizes = _combine_dict(r1.gate_sizes, r2.gate_sizes) + new_shots = add_shots(r1.shots, r2.shots) + new_depth = max(r1.depth, r2.depth) + + return Resources(new_wires, new_gates, new_gate_types, new_gate_sizes, new_depth, new_shots) + + +def mul_in_series(resources: Resources, scalar: int) -> Resources: + """ + Multiply the :class:`~resource.Resources` object by a scalar as if the circuit was repeated that many times in series. + + The repeated copies of ``resources`` are assumed to act on the same + wires as ``resources``. The resulting circuit depth is the depth of ``resources`` multiplied by + ``scalar``. To multiply as if the circuit was repeated in parallel see + :func:`~.resource.mul_in_parallel`. + + Args: + resources (Resources): a :class:`~resource.Resources` to be scaled + scalar (int): the scalar to multiply the :class:`~resource.Resources` by + + Returns: + Resources: the combined resources + + .. details:: + + **Example** + + First we build a :class:`~.resource.Resources` object. + + .. code-block:: python3 + + from pennylane.measurements import Shots + from pennylane.resource import Resources + + resources = Resources( + num_wires = 2, + num_gates = 2, + gate_types = {"Hadamard": 1, "CNOT": 1}, + gate_sizes = {1: 1, 2: 1}, + depth = 2, + shots = Shots(10) + ) + + Now we print the product. + + >>> print(qml.resource.mul_in_series(resources, 2)) + wires: 2 + gates: 4 + depth: 4 + shots: Shots(total=20) + gate_types: + {'Hadamard': 2, 'CNOT': 2} + gate_sizes: + {1: 2, 2: 2} + """ + + new_wires = resources.num_wires + new_gates = scalar * resources.num_gates + new_gate_types = _scale_dict(resources.gate_types, scalar) + new_gate_sizes = _scale_dict(resources.gate_sizes, scalar) + new_shots = scalar * resources.shots + new_depth = scalar * resources.depth + + return Resources(new_wires, new_gates, new_gate_types, new_gate_sizes, new_depth, new_shots) + + +def mul_in_parallel(resources: Resources, scalar: int) -> Resources: + """ + Multiply the :class:`~resource.Resources` object by a scalar as if the circuit was repeated that many times in parallel. + + The repeated copies of ``resources`` are assumed to act on disjoint qubits. The resulting circuit + depth is equal to the depth of ``resources``. To multiply as if the repeated copies were + executed in series see :func:`~.resource.mul_in_series`. + + Args: + resources (Resources): a :class:`~resource.Resources` to be scaled + scalar (int): the scalar to multiply the :class:`~resource.Resources` by + + Returns: + Resources: The combined resources + + .. details:: + + **Example** + + First we build a :class:`~.resource.Resources` object. + + .. code-block:: python3 + + from pennylane.measurements import Shots + from pennylane.resource import Resources + + resources = Resources( + num_wires = 2, + num_gates = 2, + gate_types = {"Hadamard": 1, "CNOT": 1}, + gate_sizes = {1: 1, 2: 1}, + depth = 2, + shots = Shots(10) + ) + + Now we print the product. + + >>> print(qml.resource.mul_in_parallel(resources, 2)) + wires: 4 + gates: 4 + depth: 2 + shots: Shots(total=20) + gate_types: + {'Hadamard': 2, 'CNOT': 2} + gate_sizes: + {1: 2, 2: 2} + """ + + new_wires = scalar * resources.num_wires + new_gates = scalar * resources.num_gates + new_gate_types = _scale_dict(resources.gate_types, scalar) + new_gate_sizes = _scale_dict(resources.gate_sizes, scalar) + new_shots = scalar * resources.shots + + return Resources( + new_wires, new_gates, new_gate_types, new_gate_sizes, resources.depth, new_shots + ) + + +def _combine_dict(dict1: dict, dict2: dict): + r"""Combines two dictionaries and adds values of common keys.""" + combined_dict = copy.copy(dict1) + + for k, v in dict2.items(): + try: + combined_dict[k] += v + except KeyError: + combined_dict[k] = v + + return combined_dict + + +def _scale_dict(dict1: dict, scalar: int): + r"""Scales the values in a dictionary with a scalar.""" + + combined_dict = copy.copy(dict1) + + for k in combined_dict: + combined_dict[k] *= scalar + + return combined_dict + + def _count_resources(tape) -> Resources: """Given a quantum circuit (tape), this function counts the resources used by standard PennyLane operations. diff --git a/tests/measurements/test_shots.py b/tests/measurements/test_shots.py index 0d770958feb..849f20c148a 100644 --- a/tests/measurements/test_shots.py +++ b/tests/measurements/test_shots.py @@ -19,7 +19,7 @@ import pytest -from pennylane.measurements import ShotCopies, Shots +from pennylane.measurements import ShotCopies, Shots, add_shots ERROR_MSG = "Shots must be a single positive integer, a tuple" @@ -304,3 +304,29 @@ def test_when_shots_is_sequence_with_copies(self, sequence): """Tests that the method returns the correct bins when shots is a sequence with copies.""" shots = Shots(sequence) assert list(shots.bins()) == [(0, 1), (1, 2), (2, 5), (5, 9)] + + +shot_tests = [ + (Shots(shots=None), Shots(shots=None), Shots(shots=None)), + (Shots(shots=10), Shots(shots=None), Shots(shots=10)), + (Shots(shots=None), Shots(shots=10), Shots(shots=10)), + (Shots(shots=10), Shots(shots=10), Shots(shots=((10, 2),))), + (Shots(shots=(10, 9)), Shots(shots=(8, 7)), Shots(shots=(10, 9, 8, 7))), + (Shots(shots=(10, 9)), Shots(shots=None), Shots(shots=(10, 9))), + (Shots(shots=None), Shots(shots=(10, 9)), Shots(shots=(10, 9))), + (Shots(shots=(10, 9)), Shots(shots=8), Shots(shots=(10, 9, 8))), + (Shots(shots=8), Shots(shots=(10, 9)), Shots(shots=(8, 10, 9))), + (Shots(shots=(10, (9, 2), 8)), Shots(shots=(5, 1)), Shots(shots=(10, (9, 2), 8, 5, 1))), +] + + +@pytest.mark.parametrize("s1, s2, expected", shot_tests) +def test_add_shots(s1, s2, expected): + """Test the add_shots function""" + assert add_shots(s1, s2) == expected + + +@pytest.mark.parametrize("s1, s2, expected", shot_tests) +def test_add_shots_dunder(s1, s2, expected): + """Test the __add__ dunder method for Shots""" + assert s1 + s2 == expected diff --git a/tests/resource/test_resource.py b/tests/resource/test_resource.py index 054aa3f004e..9155a388fcc 100644 --- a/tests/resource/test_resource.py +++ b/tests/resource/test_resource.py @@ -23,7 +23,17 @@ import pennylane as qml from pennylane.measurements import Shots from pennylane.operation import Operation -from pennylane.resource.resource import Resources, ResourcesOperation, _count_resources +from pennylane.resource.resource import ( + Resources, + ResourcesOperation, + _combine_dict, + _count_resources, + _scale_dict, + add_in_parallel, + add_in_series, + mul_in_parallel, + mul_in_series, +) from pennylane.tape import QuantumScript @@ -181,6 +191,248 @@ def test_ipython_display(self, r, rep, capsys): captured = capsys.readouterr() assert rep in captured.out + expected_results_add_series = ( + Resources( + 2, + 6, + defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + defaultdict(int, {1: 5, 2: 1}), + 3, + Shots(10), + ), + Resources( + 5, + 6, + defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + defaultdict(int, {1: 5, 2: 1}), + 3, + Shots(10), + ), + Resources( + 2, + 9, + defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 2, "PauliZ": 2}), + defaultdict(int, {1: 8, 2: 1}), + 6, + Shots((10, (50, 2), 10)), + ), + Resources( + 4, + 8, + defaultdict(int, {"RZ": 2, "CNOT": 2, "RY": 2, "Hadamard": 2}), + defaultdict(int, {1: 6, 2: 2}), + 5, + Shots((100, 10)), + ), + ) + + @pytest.mark.parametrize( + "resource_obj, expected_res_obj", zip(resource_quantities, expected_results_add_series) + ) + def test_add_in_series(self, resource_obj, expected_res_obj): + """Test the add_in_series function works with Resoruces""" + other_obj = Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 5, 2: 1}), + depth=3, + shots=Shots(10), + ) + + resultant_obj = add_in_series(resource_obj, other_obj) + assert resultant_obj == expected_res_obj + + @pytest.mark.parametrize( + "resource_obj, expected_res_obj", zip(resource_quantities, expected_results_add_series) + ) + def test_dunder_add(self, resource_obj, expected_res_obj): + """Test the __add__ function""" + other_obj = Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 5, 2: 1}), + depth=3, + shots=Shots(10), + ) + + resultant_obj = resource_obj + other_obj + assert resultant_obj == expected_res_obj + + expected_results_add_parallel = ( + Resources( + 2, + 6, + defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + defaultdict(int, {1: 5, 2: 1}), + 3, + Shots(10), + ), + Resources( + 7, + 6, + defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + defaultdict(int, {1: 5, 2: 1}), + 3, + Shots(10), + ), + Resources( + 3, + 9, + defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 2, "PauliZ": 2}), + defaultdict(int, {1: 8, 2: 1}), + 3, + Shots((10, (50, 2), 10)), + ), + Resources( + 6, + 8, + defaultdict(int, {"RZ": 2, "CNOT": 2, "RY": 2, "Hadamard": 2}), + defaultdict(int, {1: 6, 2: 2}), + 3, + Shots((100, 10)), + ), + ) + + @pytest.mark.parametrize( + "resource_obj, expected_res_obj", zip(resource_quantities, expected_results_add_parallel) + ) + def test_add_in_parallel(self, resource_obj, expected_res_obj): + """Test the add_in_parallel function works with Resoruces""" + other_obj = Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 5, 2: 1}), + depth=3, + shots=Shots(10), + ) + + resultant_obj = add_in_parallel(resource_obj, other_obj) + assert resultant_obj == expected_res_obj + + expected_results_mul_series = ( + Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 5, 2: 1}), + depth=3, + shots=Shots(10), + ), + Resources( + num_wires=2, + num_gates=12, + gate_types=defaultdict(int, {"RZ": 4, "CNOT": 2, "RY": 4, "Hadamard": 2}), + gate_sizes=defaultdict(int, {1: 10, 2: 2}), + depth=6, + shots=Shots(20), + ), + Resources( + num_wires=2, + num_gates=18, + gate_types=defaultdict(int, {"RZ": 6, "CNOT": 3, "RY": 6, "Hadamard": 3}), + gate_sizes=defaultdict(int, {1: 15, 2: 3}), + depth=9, + shots=Shots(30), + ), + Resources( + num_wires=2, + num_gates=24, + gate_types=defaultdict(int, {"RZ": 8, "CNOT": 4, "RY": 8, "Hadamard": 4}), + gate_sizes=defaultdict(int, {1: 20, 2: 4}), + depth=12, + shots=Shots(40), + ), + ) + + @pytest.mark.parametrize( + "scalar, expected_res_obj", zip((1, 2, 3, 4), expected_results_mul_series) + ) + def test_mul_in_series(self, scalar, expected_res_obj): + """Test the mul_in_series function works with Resoruces""" + resource_obj = Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 5, 2: 1}), + depth=3, + shots=Shots(10), + ) + + resultant_obj = mul_in_series(resource_obj, scalar) + assert resultant_obj == expected_res_obj + + @pytest.mark.parametrize( + "scalar, expected_res_obj", zip((1, 2, 3, 4), expected_results_mul_series) + ) + def test_dunder_mul(self, scalar, expected_res_obj): + """Test the __mul__ function""" + resource_obj = Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 5, 2: 1}), + depth=3, + shots=Shots(10), + ) + + resultant_obj = resource_obj * scalar + assert resultant_obj == expected_res_obj + + expected_results_mul_parallel = ( + Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 5, 2: 1}), + depth=3, + shots=Shots(10), + ), + Resources( + num_wires=4, + num_gates=12, + gate_types=defaultdict(int, {"RZ": 4, "CNOT": 2, "RY": 4, "Hadamard": 2}), + gate_sizes=defaultdict(int, {1: 10, 2: 2}), + depth=3, + shots=Shots(20), + ), + Resources( + num_wires=6, + num_gates=18, + gate_types=defaultdict(int, {"RZ": 6, "CNOT": 3, "RY": 6, "Hadamard": 3}), + gate_sizes=defaultdict(int, {1: 15, 2: 3}), + depth=3, + shots=Shots(30), + ), + Resources( + num_wires=8, + num_gates=24, + gate_types=defaultdict(int, {"RZ": 8, "CNOT": 4, "RY": 8, "Hadamard": 4}), + gate_sizes=defaultdict(int, {1: 20, 2: 4}), + depth=3, + shots=Shots(40), + ), + ) + + @pytest.mark.parametrize( + "scalar, expected_res_obj", zip((1, 2, 3, 4), expected_results_mul_parallel) + ) + def test_mul_in_parallel(self, scalar, expected_res_obj): + """Test the mul_in_parallel function works with Resoruces""" + resource_obj = Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 5, 2: 1}), + depth=3, + shots=Shots(10), + ) + + resultant_obj = mul_in_parallel(resource_obj, scalar) + assert resultant_obj == expected_res_obj + class TestResourcesOperation: # pylint: disable=too-few-public-methods """Test that the ResourcesOperation class is constructed correctly""" @@ -291,3 +543,25 @@ def test_count_resources(ops_and_shots, expected_resources): ops, shots = ops_and_shots computed_resources = _count_resources(QuantumScript(ops=ops, shots=shots)) assert computed_resources == expected_resources + + +def test_combine_dict(): + """Test that we can combine dictionaries as expected.""" + d1 = defaultdict(int, {"a": 2, "b": 4, "c": 6}) + d2 = defaultdict(int, {"a": 1, "b": 2, "d": 3}) + + result = _combine_dict(d1, d2) + expected = defaultdict(int, {"a": 3, "b": 6, "c": 6, "d": 3}) + + assert result == expected + + +@pytest.mark.parametrize("scalar", (1, 2, 3)) +def test_scale_dict(scalar): + """Test that we can scale the values of a dictionary as expected.""" + d1 = defaultdict(int, {"a": 2, "b": 4, "c": 6}) + + expected = defaultdict(int, {k: scalar * v for k, v in d1.items()}) + result = _scale_dict(d1, scalar) + + assert result == expected From cb994bed2fc0a2f6900041c0719f4cfa02a5f721 Mon Sep 17 00:00:00 2001 From: ringo-but-quantum Date: Thu, 21 Nov 2024 09:51:43 +0000 Subject: [PATCH 28/35] [no ci] bump nightly version --- pennylane/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/_version.py b/pennylane/_version.py index 3b74a713655..b777d56f112 100644 --- a/pennylane/_version.py +++ b/pennylane/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.40.0-dev18" +__version__ = "0.40.0-dev19" From 76f9d7cf4b4b4b2134853a070e7ebc91ffd7ac43 Mon Sep 17 00:00:00 2001 From: Andrija Paurevic <46359773+andrijapau@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:28:41 -0500 Subject: [PATCH 29/35] Fix consistency of `QNode` results processing (#6568) **Context:** The output of QNode execution was not consistent based on the type of the qfunc output, ```python @qml.qnode(qml.device('default.qubit')) def circuit(t): return t([qml.expval(qml.Z(0))]) >>> circuit(tuple) 1.0 >>> circuit(list) [1.0] ``` **Description of the Change:** Updates how the results of the QNode execution gets "type-converted" according to the `qfunc_output`. After this fix we get consistency, ```python @qml.qnode(qml.device('default.qubit')) def circuit(t): return t([qml.expval(qml.Z(0))]) >>> circuit(tuple) (1.0,) >>> circuit(list) [1.0] ``` **Benefits:** Consistency **Possible Drawbacks:** Output of QNode execution may look different than people are used to. A direct example is needed to change some tests to squeeze out the new dimension that has been introduced. **Related GitHub Issue:** #6540 [sc-77682] --- doc/releases/changelog-dev.md | 3 +++ pennylane/workflow/qnode.py | 4 ++-- tests/test_qnode.py | 10 ++++++++++ tests/transforms/test_broadcast_expand.py | 5 +++-- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 0064f239c96..051f9d4cdf7 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -193,6 +193,9 @@ same information.

Bug fixes 🐛

+* `QNode` return behaviour is now consistent for lists and tuples. + [(#6568)](https://github.com/PennyLaneAI/pennylane/pull/6568) + * `qml.QNode` now accepts arguments with types defined in libraries that are not necessarily in the list of supported interfaces, such as the `Graph` class defined in `networkx`. [(#6600)](https://github.com/PennyLaneAI/pennylane/pull/6600) diff --git a/pennylane/workflow/qnode.py b/pennylane/workflow/qnode.py index 267df623385..c9d0158ff8b 100644 --- a/pennylane/workflow/qnode.py +++ b/pennylane/workflow/qnode.py @@ -208,8 +208,8 @@ def _to_qfunc_output_type( return tuple(_to_qfunc_output_type(r, qfunc_output, False) for r in results) # Special case of single Measurement in a list - if isinstance(qfunc_output, list) and len(qfunc_output) == 1: - results = [results] + if isinstance(qfunc_output, Sequence) and len(qfunc_output) == 1: + results = (results,) # If the return type is not tuple (list or ndarray) (Autograd and TF backprop removed) if isinstance(qfunc_output, (tuple, qml.measurements.MeasurementProcess)): diff --git a/tests/test_qnode.py b/tests/test_qnode.py index aa0d7152b7b..db332ade2ab 100644 --- a/tests/test_qnode.py +++ b/tests/test_qnode.py @@ -148,6 +148,16 @@ def f(): class TestValidation: """Tests for QNode creation and validation""" + @pytest.mark.parametrize("return_type", (tuple, list)) + def test_return_behaviour_consistency(self, return_type): + """Test that the QNode return typing stays consistent""" + + @qml.qnode(qml.device("default.qubit")) + def circuit(return_type): + return return_type([qml.expval(qml.Z(0))]) + + assert isinstance(circuit(return_type), return_type) + def test_expansion_strategy_error(self): """Test that an error is raised if expansion_strategy is passed to the qnode.""" diff --git a/tests/transforms/test_broadcast_expand.py b/tests/transforms/test_broadcast_expand.py index 0e0d71bb6b8..c783f5d8794 100644 --- a/tests/transforms/test_broadcast_expand.py +++ b/tests/transforms/test_broadcast_expand.py @@ -330,7 +330,7 @@ def cost(*params): assert all(qml.math.allclose(_jac, e_jac) for _jac, e_jac in zip(jac[0], exp_jac[0])) assert all(qml.math.allclose(_jac, e_jac) for _jac, e_jac in zip(jac[1], exp_jac[1])) else: - assert all(qml.math.allclose(_jac, e_jac) for _jac, e_jac in zip(jac, exp_jac)) + assert all(qml.math.allclose(_jac, e_jac) for _jac, e_jac in zip(jac[0], exp_jac)) @pytest.mark.slow @pytest.mark.tf @@ -393,6 +393,7 @@ def cost(*params): qml.math.stack([jac[i][j] for i in range(len(obs))]) for j in range(len(params)) ) else: - assert qml.math.allclose(res, exp_fn(*params)) + assert qml.math.allclose(res[0], exp_fn(*params)) + jac = jac[0] assert all(qml.math.allclose(_jac, e_jac) for _jac, e_jac in zip(jac, exp_jac)) From d5579f979ba92606b6d7e2ea0666fc1f9088783a Mon Sep 17 00:00:00 2001 From: Catalina Albornoz Date: Thu, 21 Nov 2024 10:24:18 -0500 Subject: [PATCH 30/35] Update bug_report.yml template (#6614) Change links to docs and GH issues in bug report and feature request templates --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- .github/ISSUE_TEMPLATE/feature_request.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 237b2b57e2d..25c3583f093 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -10,10 +10,10 @@ body: value: | ## Before posting a bug report Search existing GitHub issues to make sure the issue does not already exist: - https://github.com/xanaduai/pennylane/issues + https://github.com/PennyLaneAI/pennylane/issues For general technical details check out our documentation: - https://pennylane.readthedocs.io + https://docs.pennylane.ai # Issue description Description of the issue - include code snippets in the Source code section below and screenshots if relevant. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 501150d733e..2035141b09b 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -9,10 +9,10 @@ body: value: | ## Before posting a feature request Search existing GitHub issues to make sure the issue does not already exist: - https://github.com/xanaduai/pennylane/issues + https://github.com/PennyLaneAI/pennylane/issues For general technical details check out our documentation: - https://pennylane.readthedocs.io + https://docs.pennylane.ai # Feature description Description of the feature request - include code snippets and screenshots here if relevant. From 21a12a1dfc414eb179c07c88e5babec2fe4c5193 Mon Sep 17 00:00:00 2001 From: Will Date: Thu, 21 Nov 2024 11:15:07 -0500 Subject: [PATCH 31/35] Resource object substitution function (#6570) This PR adds the `substitute` function for the Resources object. [sc-77946]. --------- Co-authored-by: ANTH0NY <39093564+AntonNI8@users.noreply.github.com> Co-authored-by: Jay Soni Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com> Co-authored-by: Diego <67476785+DSGuala@users.noreply.github.com> --- pennylane/resource/__init__.py | 2 + pennylane/resource/resource.py | 98 ++++++++++++++++++++++++ tests/resource/test_resource.py | 130 ++++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+) diff --git a/pennylane/resource/__init__.py b/pennylane/resource/__init__.py index 1f8135bdb4f..2616a12956c 100644 --- a/pennylane/resource/__init__.py +++ b/pennylane/resource/__init__.py @@ -84,6 +84,7 @@ ~add_in_parallel ~mul_in_series ~mul_in_parallel + ~substitute Tracking Resources for Custom Operations ---------------------------------------- @@ -142,6 +143,7 @@ def circuit(theta): add_in_parallel, mul_in_series, mul_in_parallel, + substitute, ) from .second_quantization import DoubleFactorization from .specs import specs diff --git a/pennylane/resource/resource.py b/pennylane/resource/resource.py index 63af52a36b3..2c3e6bc2d32 100644 --- a/pennylane/resource/resource.py +++ b/pennylane/resource/resource.py @@ -20,6 +20,7 @@ from abc import abstractmethod from collections import defaultdict from dataclasses import dataclass, field +from typing import Tuple from pennylane.measurements import Shots, add_shots from pennylane.operation import Operation @@ -502,6 +503,103 @@ def mul_in_parallel(resources: Resources, scalar: int) -> Resources: ) +def substitute(initial_resources: Resources, gate_info: Tuple[str, int], replacement: Resources): + """Replaces a specified gate in a :class:`~.resource.Resources` object with the contents of another :class:`~.resource.Resources` object. + + Args: + initial_resources (Resources): the :class:`~resource.Resources` object to be modified + gate_info (Iterable(str, int)): sequence containing the name of the gate to be replaced and the number of wires it acts on + replacement (Resources): the :class:`~resource.Resources` containing the resources that will replace the gate + + Returns: + Resources: the updated :class:`~resource.Resources` after substitution + + .. details:: + + **Example** + + First we build the :class:`~.resource.Resources`. + + .. code-block:: python3 + + from pennylane.measurements import Shots + from pennylane.resource import Resources + + initial_resources = Resources( + num_wires = 2, + num_gates = 3, + gate_types = {"RX": 2, "CNOT": 1}, + gate_sizes = {1: 2, 2: 1}, + depth = 2, + shots = Shots(10) + ) + + # the RX gates will be replaced by the substitution + gate_info = ("RX", 1) + + replacement = Resources( + num_wires = 1, + num_gates = 7, + gate_types = {"Hadamard": 3, "S": 4}, + gate_sizes = {1: 7}, + depth = 7 + ) + + + Now we print the result of the substitution. + + >>> res = qml.resource.substitute(initial_resources, gate_info, replacement) + >>> print(res) + wires: 2 + gates: 15 + depth: 9 + shots: Shots(total=10) + gate_types: + {'CNOT': 1, 'H': 6, 'S': 8} + gate_sizes: + {1: 14, 2: 1} + """ + + gate_name, num_wires = gate_info + + if not num_wires in initial_resources.gate_sizes: + raise ValueError(f"initial_resources does not contain a gate acting on {num_wires} wires.") + + gate_count = initial_resources.gate_types.get(gate_name, 0) + + if gate_count > initial_resources.gate_sizes[num_wires]: + raise ValueError( + f"Found {gate_count} gates of type {gate_name}, but only {initial_resources.gate_sizes[num_wires]} gates act on {num_wires} wires in initial_resources." + ) + + if gate_count > 0: + new_wires = initial_resources.num_wires + new_gates = initial_resources.num_gates - gate_count + (gate_count * replacement.num_gates) + replacement_gate_types = _scale_dict(replacement.gate_types, gate_count) + replacement_gate_sizes = _scale_dict(replacement.gate_sizes, gate_count) + + new_gate_types = _combine_dict(initial_resources.gate_types, replacement_gate_types) + new_gate_types.pop(gate_name) + + new_gate_sizes = copy.copy(initial_resources.gate_sizes) + new_gate_sizes[num_wires] -= gate_count + new_gate_sizes = _combine_dict(new_gate_sizes, replacement_gate_sizes) + + new_depth = initial_resources.depth + replacement.depth + + wire_diff = num_wires - replacement.num_wires + if wire_diff < 0: + new_wires = initial_resources.num_wires + abs(wire_diff) + else: + new_wires = initial_resources.num_wires + + return Resources( + new_wires, new_gates, new_gate_types, new_gate_sizes, new_depth, initial_resources.shots + ) + + return initial_resources + + def _combine_dict(dict1: dict, dict2: dict): r"""Combines two dictionaries and adds values of common keys.""" combined_dict = copy.copy(dict1) diff --git a/tests/resource/test_resource.py b/tests/resource/test_resource.py index 9155a388fcc..1ad5cfa580d 100644 --- a/tests/resource/test_resource.py +++ b/tests/resource/test_resource.py @@ -33,6 +33,7 @@ add_in_series, mul_in_parallel, mul_in_series, + substitute, ) from pennylane.tape import QuantumScript @@ -433,6 +434,135 @@ def test_mul_in_parallel(self, scalar, expected_res_obj): resultant_obj = mul_in_parallel(resource_obj, scalar) assert resultant_obj == expected_res_obj + gate_info = (("RX", 1), ("RZ", 1), ("RZ", 1)) + + sub_obj = ( + Resources( + num_wires=1, + num_gates=5, + gate_types=defaultdict(int, {"RX": 5}), + gate_sizes=defaultdict(int, {1: 5}), + depth=1, + shots=Shots(shots=None), + ), + Resources( + num_wires=1, + num_gates=5, + gate_types=defaultdict(int, {"RX": 5}), + gate_sizes=defaultdict(int, {1: 5}), + depth=1, + shots=Shots(shots=None), + ), + Resources( + num_wires=5, + num_gates=5, + gate_types=defaultdict(int, {"RX": 5}), + gate_sizes=defaultdict(int, {1: 5}), + depth=1, + shots=Shots(shots=None), + ), + ) + + expected_results_sub = ( + Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 5, 2: 1}), + depth=2, + shots=Shots(10), + ), + Resources( + num_wires=2, + num_gates=14, + gate_types=defaultdict(int, {"RX": 10, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 13, 2: 1}), + depth=3, + shots=Shots(10), + ), + Resources( + num_wires=6, + num_gates=14, + gate_types=defaultdict(int, {"RX": 10, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 13, 2: 1}), + depth=3, + shots=Shots(10), + ), + ) + + @pytest.mark.parametrize( + "gate_info, sub_obj, expected_res_obj", zip(gate_info, sub_obj, expected_results_sub) + ) + def test_substitute(self, gate_info, sub_obj, expected_res_obj): + """Test the substitute function""" + resource_obj = Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 5, 2: 1}), + depth=2, + shots=Shots(10), + ) + + resultant_obj = substitute(resource_obj, gate_info, sub_obj) + assert resultant_obj == expected_res_obj + + def test_substitute_wire_error(self): + """Test that substitute raises an exception when the wire count does not exist in gate_sizes""" + + resource_obj = Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 5, 2: 1}), + depth=2, + shots=Shots(10), + ) + + sub_obj = Resources( + num_wires=1, + num_gates=5, + gate_types=defaultdict(int, {"RX": 5}), + gate_sizes=defaultdict(int, {1: 5}), + depth=1, + shots=Shots(shots=None), + ) + + gate_info = ("RZ", 100) + + with pytest.raises( + ValueError, match="initial_resources does not contain a gate acting on 100 wires." + ): + substitute(resource_obj, gate_info, sub_obj) + + def test_substitute_gate_count_error(self): + """Test that substitute raises an exception when the substitution would result in a negative value in gate_sizes""" + + resource_obj = Resources( + num_wires=2, + num_gates=6, + gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), + gate_sizes=defaultdict(int, {1: 5, 2: 1}), + depth=2, + shots=Shots(10), + ) + + sub_obj = Resources( + num_wires=1, + num_gates=5, + gate_types=defaultdict(int, {"RX": 5}), + gate_sizes=defaultdict(int, {1: 5}), + depth=1, + shots=Shots(shots=None), + ) + + gate_info = ("RZ", 2) + with pytest.raises( + ValueError, + match="Found 2 gates of type RZ, but only 1 gates act on 2 wires in initial_resources.", + ): + substitute(resource_obj, gate_info, sub_obj) + class TestResourcesOperation: # pylint: disable=too-few-public-methods """Test that the ResourcesOperation class is constructed correctly""" From 96d25872fc8114e1ee2a73fe41a09b1ccb10dcbb Mon Sep 17 00:00:00 2001 From: Andrija Paurevic <46359773+andrijapau@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:44:07 -0500 Subject: [PATCH 32/35] Move `_cache_transform` to its own file in `qml.workflow` (#6624) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Context:** Introduces a new file `_cache_transform.py` in `qml.workflow` to house the private `_cache_transform` helper. Previously it lived in `execution.py` despite having its own unit test file `workflow/test_cache_transform.py`. Also, update file name: `resolve_diff_method.py` → `_resolve_diff_method.py` as I should have done earlier. 😄 **Benefits:** Code consistency and clean-up --------- Co-authored-by: Yushao Chen (Jerry) --- doc/releases/changelog-dev.md | 4 ++ pennylane/workflow/__init__.py | 3 +- pennylane/workflow/_cache_transform.py | 70 +++++++++++++++++++ ...diff_method.py => _resolve_diff_method.py} | 0 pennylane/workflow/execution.py | 56 ++------------- tests/interfaces/test_jax_jit.py | 6 +- tests/logging/test_logging_autograd.py | 2 +- tests/workflow/test_cache_transform.py | 2 +- 8 files changed, 85 insertions(+), 58 deletions(-) create mode 100644 pennylane/workflow/_cache_transform.py rename pennylane/workflow/{resolve_diff_method.py => _resolve_diff_method.py} (100%) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 051f9d4cdf7..fbc5707213e 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -68,6 +68,10 @@

Other Improvements

+* `_cache_transform` transform has been moved to its own file located + at `qml.workflow._cache_transform.py`. + [(#6624)](https://github.com/PennyLaneAI/pennylane/pull/6624) + * `qml.BasisRotation` template is now JIT compatible. [(#6019)](https://github.com/PennyLaneAI/pennylane/pull/6019) diff --git a/pennylane/workflow/__init__.py b/pennylane/workflow/__init__.py index b786625fd95..00d25dc04d1 100644 --- a/pennylane/workflow/__init__.py +++ b/pennylane/workflow/__init__.py @@ -46,5 +46,6 @@ from .construct_tape import construct_tape from .execution import INTERFACE_MAP, SUPPORTED_INTERFACE_NAMES, execute from .get_best_diff_method import get_best_diff_method -from .resolve_diff_method import _resolve_diff_method from .qnode import QNode, qnode +from ._cache_transform import _cache_transform +from ._resolve_diff_method import _resolve_diff_method diff --git a/pennylane/workflow/_cache_transform.py b/pennylane/workflow/_cache_transform.py new file mode 100644 index 00000000000..26576ed2e51 --- /dev/null +++ b/pennylane/workflow/_cache_transform.py @@ -0,0 +1,70 @@ +# 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. +"""Contains the transform for caching the result of a ``tape``. + +""" + +import warnings +from collections.abc import MutableMapping + +from pennylane.tape import QuantumScript +from pennylane.transforms import transform +from pennylane.typing import Result, ResultBatch + +_CACHED_EXECUTION_WITH_FINITE_SHOTS_WARNINGS = ( + "Cached execution with finite shots detected!\n" + "Note that samples as well as all noisy quantities computed via sampling " + "will be identical across executions. This situation arises where tapes " + "are executed with identical operations, measurements, and parameters.\n" + "To avoid this behaviour, provide 'cache=False' to the QNode or execution " + "function." +) +"""str: warning message to display when cached execution is used with finite shots""" + + +@transform +def _cache_transform(tape: QuantumScript, cache: MutableMapping): + """Caches the result of ``tape`` using the provided ``cache``. + + .. note:: + + This function makes use of :attr:`.QuantumTape.hash` to identify unique tapes. + """ + + def cache_hit_postprocessing(_results: ResultBatch) -> Result: + result = cache[tape.hash] + if result is not None: + if tape.shots and getattr(cache, "_persistent_cache", True): + warnings.warn(_CACHED_EXECUTION_WITH_FINITE_SHOTS_WARNINGS, UserWarning) + return result + + raise RuntimeError( + "Result for tape is missing from the execution cache. " + "This is likely the result of a race condition." + ) + + if tape.hash in cache: + return [], cache_hit_postprocessing + + def cache_miss_postprocessing(results: ResultBatch) -> Result: + result = results[0] + cache[tape.hash] = result + return result + + # Adding a ``None`` entry to the cache indicates that a result will eventually be available for + # the tape. This assumes that post-processing functions are called in the same order in which + # the transforms are invoked. Otherwise, ``cache_hit_postprocessing()`` may be called before the + # result of the corresponding tape is placed in the cache by ``cache_miss_postprocessing()``. + cache[tape.hash] = None + return [tape], cache_miss_postprocessing diff --git a/pennylane/workflow/resolve_diff_method.py b/pennylane/workflow/_resolve_diff_method.py similarity index 100% rename from pennylane/workflow/resolve_diff_method.py rename to pennylane/workflow/_resolve_diff_method.py diff --git a/pennylane/workflow/execution.py b/pennylane/workflow/execution.py index 4422f4ebe54..f9351883d59 100644 --- a/pennylane/workflow/execution.py +++ b/pennylane/workflow/execution.py @@ -23,8 +23,7 @@ import inspect import logging -import warnings -from collections.abc import Callable, MutableMapping +from collections.abc import Callable from functools import partial from typing import Literal, Optional, Union, get_args from warnings import warn @@ -32,10 +31,10 @@ from cachetools import Cache, LRUCache import pennylane as qml -from pennylane.tape import QuantumScript, QuantumScriptBatch -from pennylane.transforms import transform -from pennylane.typing import Result, ResultBatch +from pennylane.tape import QuantumScriptBatch +from pennylane.typing import ResultBatch +from ._cache_transform import _cache_transform from .jacobian_products import DeviceDerivatives, DeviceJacobianProducts, TransformJacobianProducts logger = logging.getLogger(__name__) @@ -94,16 +93,6 @@ SUPPORTED_INTERFACE_NAMES = list(INTERFACE_MAP) """list[str]: allowed interface strings""" -_CACHED_EXECUTION_WITH_FINITE_SHOTS_WARNINGS = ( - "Cached execution with finite shots detected!\n" - "Note that samples as well as all noisy quantities computed via sampling " - "will be identical across executions. This situation arises where tapes " - "are executed with identical operations, measurements, and parameters.\n" - "To avoid this behaviour, provide 'cache=False' to the QNode or execution " - "function." -) -"""str: warning message to display when cached execution is used with finite shots""" - def _use_tensorflow_autograph(): """Checks if TensorFlow is in graph mode, allowing Autograph for optimized execution""" @@ -217,43 +206,6 @@ def inner_execute(tapes: QuantumScriptBatch, **_) -> ResultBatch: return inner_execute -@transform -def _cache_transform(tape: QuantumScript, cache: MutableMapping): - """Caches the result of ``tape`` using the provided ``cache``. - - .. note:: - - This function makes use of :attr:`.QuantumTape.hash` to identify unique tapes. - """ - - def cache_hit_postprocessing(_results: ResultBatch) -> Result: - result = cache[tape.hash] - if result is not None: - if tape.shots and getattr(cache, "_persistent_cache", True): - warnings.warn(_CACHED_EXECUTION_WITH_FINITE_SHOTS_WARNINGS, UserWarning) - return result - - raise RuntimeError( - "Result for tape is missing from the execution cache. " - "This is likely the result of a race condition." - ) - - if tape.hash in cache: - return [], cache_hit_postprocessing - - def cache_miss_postprocessing(results: ResultBatch) -> Result: - result = results[0] - cache[tape.hash] = result - return result - - # Adding a ``None`` entry to the cache indicates that a result will eventually be available for - # the tape. This assumes that post-processing functions are called in the same order in which - # the transforms are invoked. Otherwise, ``cache_hit_postprocessing()`` may be called before the - # result of the corresponding tape is placed in the cache by ``cache_miss_postprocessing()``. - cache[tape.hash] = None - return [tape], cache_miss_postprocessing - - def _get_interface_name(tapes, interface): """Helper function to get the interface name of a list of tapes diff --git a/tests/interfaces/test_jax_jit.py b/tests/interfaces/test_jax_jit.py index c6b2ce19de9..d252c2b4c8f 100644 --- a/tests/interfaces/test_jax_jit.py +++ b/tests/interfaces/test_jax_jit.py @@ -181,7 +181,7 @@ class TestCaching: def test_cache_maxsize(self, mocker): """Test the cachesize property of the cache""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.workflow.execution._cache_transform, "_transform") + spy = mocker.spy(qml.workflow._cache_transform, "_transform") def cost(a, cachesize): with qml.queuing.AnnotatedQueue() as q: @@ -209,7 +209,7 @@ def cost(a, cachesize): def test_custom_cache(self, mocker): """Test the use of a custom cache object""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.workflow.execution._cache_transform, "_transform") + spy = mocker.spy(qml.workflow._cache_transform, "_transform") def cost(a, cache): with qml.queuing.AnnotatedQueue() as q: @@ -236,7 +236,7 @@ def cost(a, cache): def test_custom_cache_multiple(self, mocker): """Test the use of a custom cache object with multiple tapes""" dev = qml.device("default.qubit", wires=1) - spy = mocker.spy(qml.workflow.execution._cache_transform, "_transform") + spy = mocker.spy(qml.workflow._cache_transform, "_transform") a = jax.numpy.array(0.1) b = jax.numpy.array(0.2) diff --git a/tests/logging/test_logging_autograd.py b/tests/logging/test_logging_autograd.py index 0d134501c75..2ead79fed42 100644 --- a/tests/logging/test_logging_autograd.py +++ b/tests/logging/test_logging_autograd.py @@ -80,7 +80,7 @@ def circuit(params): ["Calling ` below for more information on step 2. + +.. _device_capabilities: + +Device Capabilities +------------------- + +Optionally, you can add a ``config_filepath`` class variable pointing to your configuration file. +This file should be a `toml file `_ that describes which gates and features are +supported by your device, i.e., what the :meth:`~pennylane.devices.Device.execute` method accepts. + +.. code-block:: python + + from os import path + from pennylane.devices import Device + + class MyDevice(Device): + """My Documentation.""" + + config_filepath = path.join(path.dirname(__file__), "relative/path/to/config.toml") + +This configuration file will be loaded into another class variable :attr:`~pennylane.devices.Device.capabilities` +that is used in the default implementation of :meth:`~pennylane.devices.Device.preprocess` if you +choose not to override it yourself as described above. Note that this file must be declared as +package data as instructed at the end of :ref:`this section `. + +Below is an example configuration file defining all accepted fields, with inline descriptions of +how to fill these fields. All headers and fields are generally required, unless stated otherwise. + +.. code-block:: toml + + schema = 3 + + # The set of all gate types supported at the runtime execution interface of the + # device, i.e., what is supported by the `execute` method. The gate definitions + # should have the following format: + # + # GATE = { properties = [ PROPS ], conditions = [ CONDS ] } + # + # where PROPS and CONS are zero or more comma separated quoted strings. + # + # PROPS: additional support provided for each gate. + # - "controllable": if a controlled version of this gate is supported. + # - "invertible": if the adjoint of this operation is supported. + # - "differentiable": if device gradient is supported for this gate. + # CONDS: constraints on the support for each gate. + # - "analytic" or "finiteshots": if this operation is only supported in + # either analytic execution or with shots, respectively. + # + [operators.gates] + + PauliX = { properties = ["controllable", "invertible"] } + PauliY = { properties = ["controllable", "invertible"] } + PauliZ = { properties = ["controllable", "invertible"] } + RY = { properties = ["controllable", "invertible", "differentiable"] } + RZ = { properties = ["controllable", "invertible", "differentiable"] } + CRY = { properties = ["invertible", "differentiable"] } + CRZ = { properties = ["invertible", "differentiable"] } + CNOT = { properties = ["invertible"] } + + # Observables supported by the device for measurements. The observables defined + # in this section should have the following format: + # + # OBSERVABLE = { conditions = [ CONDS ] } + # + # where CONDS is zero or more comma separated quoted strings, same as above. + # + # CONDS: constraints on the support for each observable. + # - "analytic" or "finiteshots": if this observable is only supported in + # either analytic execution or with shots, respectively. + # - "terms-commute": if a composite operator is only supported under the + # condition that its terms commute. + # + [operators.observables] + + PauliX = { } + PauliY = { } + PauliZ = { } + Hamiltonian = { conditions = [ "terms-commute" ] } + Sum = { conditions = [ "terms-commute" ] } + SProd = { } + Prod = { } + + # Types of measurement processes supported on the device. The measurements in + # this section should have the following format: + # + # MEASUREMENT_PROCESS = { conditions = [ CONDS ] } + # + # where CONDS is zero or more comma separated quoted strings, same as above. + # + # CONDS: constraints on the support for each measurement process. + # - "analytic" or "finiteshots": if this measurement is only supported + # in either analytic execution or with shots, respectively. + # + [measurement_processes] + + ExpectationMP = { } + SampleMP = { } + CountsMP = { conditions = ["finiteshots"] } + StateMP = { conditions = ["analytic"] } + + # Additional support that the device may provide that informs the compilation + # process. All accepted fields and their default values are listed below. + [compilation] + + # Whether the device is compatible with qjit. + qjit_compatible = false + + # Whether the device requires run time generation of the quantum circuit. + runtime_code_generation = false + + # Whether the device supports allocating and releasing qubits during execution. + dynamic_qubit_management = false + + # Whether simultaneous measurements on overlapping wires is supported. + overlapping_observables = true + + # Whether simultaneous measurements of non-commuting observables is supported. + # If false, a circuit with multiple non-commuting measurements will have to be + # split into multiple executions for each subset of commuting measurements. + non_commuting_observables = false + + # Whether the device supports initial state preparation. + initial_state_prep = false + + # The methods of handling mid-circuit measurements that the device supports, + # e.g., "one-shot", "tree-traversal", "device", etc. An empty list indicates + # that the device does not support mid-circuit measurements. + supported_mcm_methods = [ ] + +This TOML configuration file is optional for PennyLane but required for Catalyst integration, +i.e., compatibility with ``qml.qjit``. For more details, see `Custom Devices `_. + +Mid Circuit Measurements +~~~~~~~~~~~~~~~~~~~~~~~~ + +PennyLane supports :ref:`mid-circuit measurements `, i.e., measurements +in the middle of a quantum circuit used to shape the structure of the circuit dynamically, and to +gather information about the quantum state during the circuit execution. This might not be natively +supported by all devices. + +If your device does not support mid-circuit measurements, the :ref:`deferred measurements ` +method will be applied. On the other hand, if your device is able to evaluate dynamic circuits by +executing them one shot at a time, sampling a dynamic execution path for each shot, you should +include ``"one-shot"`` as one of the ``supported_mcm_methods`` in your configuration file. When the +``"one-shot"`` method is requested on the ``QNode``, the :ref:`dynamic one-shot ` +method will be applied. + +Both methods mentioned above involve transform programs to be applied on the circuits that prepare +them for device execution and post-processing functions to aggregate the results. Alternatively, if +your device natively supports all mid-circuit measurement features provided in PennyLane, you should +include ``"device"`` as one of the ``supported_mcm_methods``. + Wires ----- @@ -205,7 +377,7 @@ Devices can now either: 3) Strictly require specific wire labels Option 2 allows workflows to change the number and labeling of wires over time, but sometimes users want -to enfore a wire convention and labels. If a user does provide wires, :meth:`~.devices.Device.preprocess` should +to enforce a wire convention and labels. If a user does provide wires, :meth:`~.devices.Device.preprocess` should validate that submitted circuits only have wires in the requested range. >>> dev = qml.device('default.qubit', wires=1) @@ -436,6 +608,8 @@ keyword-value pairs. For example, a device could also track cost and a job ID v self.tracker.update(price=price_for_execution, job_id=job_id) +.. _packaging: + Identifying and installing your device -------------------------------------- @@ -458,9 +632,9 @@ following keyword argument to the ``setup()`` function in your ``setup.py`` file .. code-block:: python devices_list = [ - 'example.mydevice1 = MyModule.MySubModule:MyDevice1' - 'example.mydevice2 = MyModule.MySubModule:MyDevice2' - ], + 'example.mydevice1 = MyModule.MySubModule:MyDevice1' + 'example.mydevice2 = MyModule.MySubModule:MyDevice2' + ], setup(entry_points={'pennylane.plugins': devices_list}) where @@ -474,3 +648,29 @@ where To ensure your device is working as expected, you can install it in developer mode using ``pip install -e pluginpath``, where ``pluginpath`` is the location of the plugin. It will then be accessible via PennyLane. + +If a :ref:`configuration file ` is defined for your device, you will need +to declare it as package data in ``setup.py``: + +.. code-block:: python + + from setuptools import setup, find_packages + + setup( + ... + include_package_data=True, + package_data={ + 'package_name' : ['path/to/config/device_name.toml'], + }, + ... + ) + +Alternatively, with ``include_package_data=True``, you can also declare the file in a ``MANIFEST.in``: + +.. code-block:: + + include path/to/config/device_name.toml + +See `packaging data files `_ +for a detailed explanation. This will ensure that PennyLane can correctly load the device and its +associated capabilities. diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index fbc5707213e..19983a537e2 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -3,17 +3,39 @@ # Release 0.40.0-dev (development release)

New features since last release

- -* A `DeviceCapabilities` data class is defined to contain all capabilities of the device's execution interface (i.e. its implementation of `Device.execute`). A TOML file can be used to define the capabilities of a device, and it can be loaded into a `DeviceCapabilities` object. - [(#6407)](https://github.com/PennyLaneAI/pennylane/pull/6407) - ```pycon - >>> from pennylane.devices.capabilities import load_toml_file, parse_toml_document, DeviceCapabilities - >>> document = load_toml_file("my_device.toml") - >>> capabilities = parse_toml_document(document) - >>> isinstance(capabilities, DeviceCapabilities) - True - ``` +* Developers of plugin devices now have the option of providing a TOML-formatted configuration file + to declare the capabilities of the device. See [Device Capabilities](https://docs.pennylane.ai/en/latest/development/plugins.html#device-capabilities) for details. + [(#6407)](https://github.com/PennyLaneAI/pennylane/pull/6407) + [(#6433)](https://github.com/PennyLaneAI/pennylane/pull/6433) + + * An internal module `pennylane.devices.capabilities` is added that defines a new `DeviceCapabilites` + data class, as well as functions that load and parse the TOML-formatted configuration files. + + ```pycon + >>> from pennylane.devices.capabilities import DeviceCapabilities + >>> capabilities = DeviceCapabilities.from_toml_file("my_device.toml") + >>> isinstance(capabilities, DeviceCapabilities) + True + ``` + + * Devices that extends `qml.devices.Device` now has an optional class attribute `capabilities` + that is an instance of the `DeviceCapabilities` data class, constructed from the configuration + file if it exists. Otherwise, it is set to `None`. + + ```python + from pennylane.devices import Device + + class MyDevice(Device): + + config_filepath = "path/to/config.toml" + + ... + ``` + ```pycon + >>> isinstance(MyDevice.capabilities, DeviceCapabilities) + True + ``` * Added a dense implementation of computing the Lie closure in a new function `lie_closure_dense` in `pennylane.labs.dla`. diff --git a/pennylane/devices/__init__.py b/pennylane/devices/__init__.py index 1f9c53bb991..463f6c3f163 100644 --- a/pennylane/devices/__init__.py +++ b/pennylane/devices/__init__.py @@ -24,7 +24,6 @@ .. autosummary:: :toctree: api - default_qubit default_gaussian default_mixed @@ -144,6 +143,7 @@ def execute(self, circuits, execution_config = qml.devices.DefaultExecutionConfi """ +from .capabilities import DeviceCapabilities from .execution_config import ExecutionConfig, DefaultExecutionConfig, MCMConfig from .device_constructor import device, refresh_devices from .device_api import Device diff --git a/pennylane/devices/capabilities.py b/pennylane/devices/capabilities.py index a0a58b3c989..e2b6016557d 100644 --- a/pennylane/devices/capabilities.py +++ b/pennylane/devices/capabilities.py @@ -14,11 +14,12 @@ """ Defines the DeviceCapabilities class, and tools to load it from a TOML file. """ - +import re import sys from dataclasses import dataclass, field, replace from enum import Enum from itertools import repeat +from typing import Optional if sys.version_info >= (3, 11): import tomllib as toml # pragma: no cover @@ -71,14 +72,36 @@ class OperatorProperties: conditions: list[ExecutionCondition] = field(default_factory=list) def __and__(self, other: "OperatorProperties") -> "OperatorProperties": + # Take the intersection of support but the union of constraints (conditions) return OperatorProperties( invertible=self.invertible and other.invertible, controllable=self.controllable and other.controllable, differentiable=self.differentiable and other.differentiable, - conditions=list(set(self.conditions) & set(other.conditions)), + conditions=list(set(self.conditions) | set(other.conditions)), ) +def _get_supported_base_op(op_name: str, op_dict: dict[str, OperatorProperties]) -> Optional[str]: + """Checks if the given operator is supported by name, returns the base op for nested ops""" + + if op_name in op_dict: + return op_name + + if match := re.match(r"Adjoint\((.*)\)", op_name): + base_op_name = match.group(1) + deep_supported_base = _get_supported_base_op(base_op_name, op_dict) + if deep_supported_base and op_dict[deep_supported_base].invertible: + return deep_supported_base + + if match := re.match(r"C\((.*)\)", op_name): + base_op_name = match.group(1) + deep_supported_base = _get_supported_base_op(base_op_name, op_dict) + if deep_supported_base and op_dict[deep_supported_base].controllable: + return deep_supported_base + + return None + + @dataclass class DeviceCapabilities: # pylint: disable=too-many-instance-attributes """Capabilities of a quantum device. @@ -94,7 +117,6 @@ class DeviceCapabilities: # pylint: disable=too-many-instance-attributes non_commuting_observables (bool): Whether the device supports measuring non-commuting observables on the same tape. initial_state_prep (bool): Whether the device supports initial state preparation. supported_mcm_methods (list[str]): List of supported methods of mid-circuit measurements. - options (dict[str, any]): Additional options for the device. """ operations: dict[str, OperatorProperties] = field(default_factory=dict) @@ -107,7 +129,6 @@ class DeviceCapabilities: # pylint: disable=too-many-instance-attributes non_commuting_observables: bool = False initial_state_prep: bool = False supported_mcm_methods: list[str] = field(default_factory=list) - options: dict[str, any] = field(default_factory=dict) def filter(self, finite_shots: bool) -> "DeviceCapabilities": """Returns the device capabilities conditioned on the given program features.""" @@ -152,6 +173,14 @@ def from_toml_file(cls, file_path: str, runtime_interface="pennylane") -> "Devic update_device_capabilities(capabilities, document, runtime_interface) return capabilities + def supports_operation(self, operation_name: str) -> bool: + """Checks if the given operation is supported by name.""" + return bool(_get_supported_base_op(operation_name, self.operations)) + + def supports_observable(self, observable_name: str) -> bool: + """Checks if the given observable is supported by name.""" + return bool(_get_supported_base_op(observable_name, self.observables)) + VALID_COMPILATION_OPTIONS = { "qjit_compatible", @@ -322,11 +351,6 @@ def _get_compilation_options(document: dict, prefix: str = "") -> dict[str, bool return section -def _get_options(document: dict) -> dict[str, str]: - """Get custom options""" - return document.get("options", {}) - - def parse_toml_document(document: dict) -> DeviceCapabilities: """Parses a TOML document into a DeviceCapabilities object. @@ -337,7 +361,7 @@ def parse_toml_document(document: dict) -> DeviceCapabilities: """ schema = int(document["schema"]) - assert schema in ALL_SUPPORTED_SCHEMAS, f"Unsupported capabilities TOML schema {schema}" + assert schema in ALL_SUPPORTED_SCHEMAS, f"Unsupported config TOML schema {schema}" operations = _get_operations(document) observables = _get_observables(document) measurement_processes = _get_measurement_processes(document) @@ -347,7 +371,6 @@ def parse_toml_document(document: dict) -> DeviceCapabilities: observables=observables, measurement_processes=measurement_processes, **compilation_options, - options=_get_options(document), ) diff --git a/pennylane/devices/device_api.py b/pennylane/devices/device_api.py index 90408fc317a..defbdfea9c6 100644 --- a/pennylane/devices/device_api.py +++ b/pennylane/devices/device_api.py @@ -29,6 +29,7 @@ from pennylane.typing import Result, ResultBatch, TensorLike from pennylane.wires import Wires +from .capabilities import DeviceCapabilities from .execution_config import DefaultExecutionConfig, ExecutionConfig @@ -45,7 +46,7 @@ class Device(abc.ABC): **Streamlined interface:** Only methods that are required to interact with the rest of PennyLane will be placed in the interface. Developers will be able to clearly see what they can change while still having a fully functional device. - **Reduction of duplicate methods:** Methods that solve similar problems are combined together. Only one place will have + **Reduction of duplicate methods:** Methods that solve similar problems are combined. Only one place will have to solve each individual problem. **Support for dynamic execution configurations:** Properties such as shots belong to specific executions. @@ -121,6 +122,19 @@ class Device(abc.ABC): """ + config_filepath: Optional[str] = None + """A device can use a `toml` file to specify the capabilities of the backend device. If this + is provided, the file will be loaded into a :class:`~.DeviceCapabilities` object assigned to + the :attr:`capabilities` attribute.""" + + capabilities: Optional[DeviceCapabilities] = None + """A :class:`~.DeviceCapabilities` object describing the capabilities of the backend device.""" + + def __init_subclass__(cls, **kwargs): + if cls.config_filepath is not None: + cls.capabilities = DeviceCapabilities.from_toml_file(cls.config_filepath) + super().__init_subclass__(**kwargs) + @property def name(self) -> str: """The name of the device or set of devices. diff --git a/pennylane/devices/legacy_facade.py b/pennylane/devices/legacy_facade.py index ea1f67c70d5..dd9bcec7778 100644 --- a/pennylane/devices/legacy_facade.py +++ b/pennylane/devices/legacy_facade.py @@ -164,6 +164,7 @@ def __init__(self, device: "qml.devices.LegacyDevice"): ) self._device = device + self.config_filepath = getattr(self._device, "config_filepath", None) @property def tracker(self): diff --git a/pennylane/devices/preprocess.py b/pennylane/devices/preprocess.py index 0d75b88284f..f8b4b2cb62f 100644 --- a/pennylane/devices/preprocess.py +++ b/pennylane/devices/preprocess.py @@ -21,7 +21,7 @@ from collections.abc import Callable, Generator, Sequence from copy import copy from itertools import chain -from typing import Optional, Union +from typing import Optional, Type, Union import pennylane as qml from pennylane import Snapshot, transform @@ -48,7 +48,7 @@ def _operator_decomposition_gen( max_expansion: Optional[int] = None, current_depth=0, name: str = "device", - error: Optional[Exception] = None, + error: Optional[Type[Exception]] = None, ) -> Generator[qml.operation.Operator, None, None]: """A generator that yields the next operation that is accepted.""" if error is None: @@ -302,7 +302,7 @@ def decompose( ] = None, max_expansion: Union[int, None] = None, name: str = "device", - error: Optional[Exception] = None, + error: Optional[Type[Exception]] = None, ) -> tuple[QuantumScriptBatch, PostprocessingFn]: """Decompose operations until the stopping condition is met. diff --git a/pennylane/devices/tests/conftest.py b/pennylane/devices/tests/conftest.py index 217aca0938e..56aa0640e8e 100755 --- a/pennylane/devices/tests/conftest.py +++ b/pennylane/devices/tests/conftest.py @@ -64,6 +64,18 @@ def _init_state(n): return _init_state +def get_legacy_capabilities(dev): + """Gets the capabilities dictionary of a device.""" + + if isinstance(dev, qml.devices.LegacyDeviceFacade): + return dev.target_device.capabilities() + + if isinstance(dev, qml.devices.LegacyDevice): + return dev.capabilities() + + return {} + + @pytest.fixture(scope="session") def skip_if(): """Fixture to skip tests.""" @@ -71,7 +83,8 @@ def skip_if(): def _skip_if(dev, capabilities): """Skip test if device has any of the given capabilities.""" - dev_capabilities = dev.capabilities() + dev_capabilities = get_legacy_capabilities(dev) + for capability, value in capabilities.items(): # skip if capability not found, or if capability has specific value if capability not in dev_capabilities or dev_capabilities[capability] == value: diff --git a/pennylane/devices/tests/test_compare_default_qubit.py b/pennylane/devices/tests/test_compare_default_qubit.py index 75e91809e2a..e7b10eea22c 100755 --- a/pennylane/devices/tests/test_compare_default_qubit.py +++ b/pennylane/devices/tests/test_compare_default_qubit.py @@ -22,6 +22,8 @@ from pennylane import numpy as pnp # Import from PennyLane to mirror the standard approach in demos from pennylane.templates.layers import RandomLayers +from .conftest import get_legacy_capabilities + pytestmark = pytest.mark.skip_unsupported @@ -147,9 +149,8 @@ def test_pauliz_expectation_analytic(self, device, tol): if dev.name == dev_def.name: pytest.skip("Device is default.qubit.") - supports_tensor = isinstance(dev, qml.devices.Device) or ( - "supports_tensor_observables" in dev.capabilities() - and dev.capabilities()["supports_tensor_observables"] + supports_tensor = isinstance(dev, qml.devices.Device) or get_legacy_capabilities(dev).get( + "supports_tensor_observables", False ) if not supports_tensor: @@ -186,9 +187,8 @@ def test_random_circuit(self, device, tol, ret): if dev.name == dev_def.name: pytest.skip("Device is default.qubit.") - supports_tensor = isinstance(dev, qml.devices.Device) or ( - "supports_tensor_observables" in dev.capabilities() - and dev.capabilities()["supports_tensor_observables"] + supports_tensor = isinstance(dev, qml.devices.Device) or get_legacy_capabilities(dev).get( + "supports_tensor_observables", False ) if not supports_tensor: diff --git a/pennylane/devices/tests/test_measurements.py b/pennylane/devices/tests/test_measurements.py index ef0bfdb7f94..1d378633e94 100644 --- a/pennylane/devices/tests/test_measurements.py +++ b/pennylane/devices/tests/test_measurements.py @@ -29,6 +29,8 @@ ) from pennylane.wires import Wires +from .conftest import get_legacy_capabilities + pytestmark = pytest.mark.skip_unsupported # ========================================================== @@ -139,9 +141,8 @@ def test_tensor_observables_can_be_implemented(self, device_kwargs): This test is skipped for devices that do not support tensor observables.""" device_kwargs["wires"] = 2 dev = qml.device(**device_kwargs) - supports_tensor = isinstance(dev, qml.devices.Device) or ( - "supports_tensor_observables" in dev.capabilities() - and dev.capabilities()["supports_tensor_observables"] + supports_tensor = isinstance(dev, qml.devices.Device) or get_legacy_capabilities(dev).get( + "supports_tensor_observables", False ) if not supports_tensor: pytest.skip("Device does not support tensor observables.") diff --git a/pennylane/devices/tests/test_properties.py b/pennylane/devices/tests/test_properties.py index c2a444dd636..4f0da490164 100755 --- a/pennylane/devices/tests/test_properties.py +++ b/pennylane/devices/tests/test_properties.py @@ -18,6 +18,8 @@ import pennylane as qml import pennylane.numpy as pnp +from .conftest import get_legacy_capabilities + try: import tensorflow as tf @@ -113,7 +115,7 @@ def test_has_capabilities_dictionary(self, device_kwargs): dev = qml.device(**device_kwargs) if isinstance(dev, qml.devices.Device): pytest.skip("test is old interface specific.") - cap = dev.capabilities() + cap = get_legacy_capabilities(dev) assert isinstance(cap, dict) def test_model_is_defined_valid_and_correct(self, device_kwargs): @@ -122,7 +124,7 @@ def test_model_is_defined_valid_and_correct(self, device_kwargs): dev = qml.device(**device_kwargs) if isinstance(dev, qml.devices.Device): pytest.skip("test is old interface specific.") - cap = dev.capabilities() + cap = get_legacy_capabilities(dev) assert "model" in cap assert cap["model"] in ["qubit", "cv"] @@ -149,7 +151,7 @@ def test_passthru_interface_is_correct(self, device_kwargs): dev = qml.device(**device_kwargs) if isinstance(dev, qml.devices.Device): pytest.skip("test is old interface specific.") - cap = dev.capabilities() + cap = get_legacy_capabilities(dev) if "passthru_interface" not in cap: pytest.skip("No passthru_interface capability specified by device.") @@ -200,7 +202,7 @@ def test_supports_tensor_observables(self, device_kwargs): dev = qml.device(**device_kwargs) if isinstance(dev, qml.devices.Device): pytest.skip("test is old interface specific.") - cap = dev.capabilities() + cap = get_legacy_capabilities(dev) if "supports_tensor_observables" not in cap: pytest.skip("No supports_tensor_observables capability specified by device.") @@ -226,7 +228,7 @@ def test_returns_state(self, device_kwargs): dev = qml.device(**device_kwargs) if isinstance(dev, qml.devices.Device): pytest.skip("test is old interface specific.") - cap = dev.capabilities() + cap = get_legacy_capabilities(dev) @qml.qnode(dev) def circuit(): @@ -263,7 +265,7 @@ def test_returns_probs(self, device_kwargs): dev = qml.device(**device_kwargs) if isinstance(dev, qml.devices.Device): pytest.skip("test is old interface specific.") - cap = dev.capabilities() + cap = get_legacy_capabilities(dev) if "returns_probs" not in cap: pytest.skip("No returns_probs capability specified by device.") @@ -290,7 +292,7 @@ def test_supports_broadcasting(self, device_kwargs, mocker): dev = qml.device(**device_kwargs) if isinstance(dev, qml.devices.Device): pytest.skip("test is old interface specific.") - cap = dev.capabilities() + cap = get_legacy_capabilities(dev) assert "supports_broadcasting" in cap diff --git a/pennylane/devices/tests/test_tracker.py b/pennylane/devices/tests/test_tracker.py index c46a5ad952e..52f7b8619b6 100644 --- a/pennylane/devices/tests/test_tracker.py +++ b/pennylane/devices/tests/test_tracker.py @@ -17,6 +17,8 @@ import pennylane as qml +from .conftest import get_legacy_capabilities + class TestTracker: """Tests that the device uses a tracker attribute properly""" @@ -26,7 +28,7 @@ def test_tracker_initialization(self, device): dev = device(1) - if isinstance(dev, qml.devices.LegacyDevice) and not dev.capabilities().get( + if isinstance(dev, qml.devices.LegacyDevice) and not get_legacy_capabilities(dev).get( "supports_tracker", False ): pytest.skip("Device does not support a tracker") @@ -38,7 +40,7 @@ def test_tracker_updated_in_execution_mode(self, device): dev = device(1) - if isinstance(dev, qml.devices.LegacyDevice) and not dev.capabilities().get( + if isinstance(dev, qml.devices.LegacyDevice) and not get_legacy_capabilities(dev).get( "supports_tracker", False ): pytest.skip("Device does not support a tracker") diff --git a/pennylane/devices/toml_check.py b/pennylane/devices/toml_check.py new file mode 100644 index 00000000000..1b6267459e0 --- /dev/null +++ b/pennylane/devices/toml_check.py @@ -0,0 +1,85 @@ +# Copyright 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 tool checks the syntax of quantum device configuration files. It is a strict parser of TOML +format, narrowed down to match our requirements. For the Lark's EBNF dialect syntax, see the +Lark grammar reference: + * https://lark-parser.readthedocs.io/en/latest/grammar.html +""" + +# pylint: disable=unused-import + +from textwrap import dedent # pragma: no cover + +try: # pragma: no cover + from lark import Lark, LarkError, UnexpectedInput # pragma: no cover +except ImportError as e: # pragma: no cover + raise RuntimeError( + "toml_check requires `lark` library. Consider `pip install lark`" + ) from e # pragma: no cover + +parser = Lark( # pragma: no cover + dedent( + """ + start: schema_body \ + gates_section \ + pennylane_gates_section? \ + qjit_gates_section? \ + observables_section \ + pennylane_observables_section? \ + qjit_observables_section? \ + measurement_processes_section \ + pennylane_measurement_processes_section? \ + qjit_measurement_processes_section? \ + compilation_section \ + pennylane_compilation_section? \ + qjit_compilation_section? + schema_body: schema_decl + gates_section: "[operators.gates]" operator_decl* + pennylane_gates_section: "[pennylane.operators.gates]" operator_decl* + qjit_gates_section: "[qjit.operators.gates]" operator_decl* + observables_section: "[operators.observables]" operator_decl* + pennylane_observables_section: "[pennylane.operators.observables]" operator_decl* + qjit_observables_section: "[qjit.operators.observables]" operator_decl* + measurement_processes_section: "[measurement_processes]" mp_decl* + pennylane_measurement_processes_section: "[pennylane.measurement_processes]" mp_decl* + qjit_measurement_processes_section: "[qjit.measurement_processes]" mp_decl* + compilation_section: "[compilation]" compilation_option_decl* + pennylane_compilation_section: "[pennylane.compilation]" compilation_option_decl* + qjit_compilation_section: "[qjit.compilation]" compilation_option_decl* + schema_decl: "schema" "=" "3" + operator_decl: name "=" "{" (operator_trait ("," operator_trait)*)? "}" + operator_trait: conditions | properties + conditions: "conditions" "=" "[" condition ("," condition)* "]" + properties: "properties" "=" "[" property ("," property)* "]" + condition: "\\"finiteshots\\"" | "\\"analytic\\"" | "\\"terms-commute\\"" + property: "\\"controllable\\"" | "\\"invertible\\"" | "\\"differentiable\\"" + mp_decl: name "=" "{" (mp_trait)? "}" + mp_trait: conditions + compilation_option_decl: boolean_option | mcm_option + boolean_option: ( \ + "qjit_compatible" | "runtime_code_generation" | "dynamic_qubit_management" | \ + "overlapping_observables" | "non_commuting_observables" | "initial_state_prep" | \ + ) "=" boolean + mcm_option: "supported_mcm_methods" "=" "[" ("\\"" name "\\"" ("," "\\"" name "\\"" )*)? "]" + name: /[a-zA-Z0-9_-]+/ + boolean: "true" | "false" + COMMENT: "#" /./* + %import common.WS + %ignore WS + %ignore COMMENT + """ + ) +) diff --git a/pennylane/labs/tests/conftest.py b/pennylane/labs/tests/conftest.py index 950b284d6dd..856df3b4b87 100755 --- a/pennylane/labs/tests/conftest.py +++ b/pennylane/labs/tests/conftest.py @@ -64,6 +64,18 @@ def _init_state(n): return _init_state +def get_legacy_capabilities(dev): + """Gets the capabilities dictionary of a device.""" + + if isinstance(dev, qml.devices.LegacyDeviceFacade): + return dev.target_device.capabilities() + + if isinstance(dev, qml.devices.LegacyDevice): + return dev.capabilities() + + return {} + + @pytest.fixture(scope="session") def skip_if(): """Fixture to skip tests.""" @@ -71,7 +83,7 @@ def skip_if(): def _skip_if(dev, capabilities): """Skip test if device has any of the given capabilities.""" - dev_capabilities = dev.capabilities() + dev_capabilities = get_legacy_capabilities(dev) for capability, value in capabilities.items(): # skip if capability not found, or if capability has specific value if capability not in dev_capabilities or dev_capabilities[capability] == value: diff --git a/pennylane/qcut/cutcircuit_mc.py b/pennylane/qcut/cutcircuit_mc.py index a0ff828a4f5..7e9e2b0abb0 100644 --- a/pennylane/qcut/cutcircuit_mc.py +++ b/pennylane/qcut/cutcircuit_mc.py @@ -463,8 +463,9 @@ def circuit(x): qml.map_wires(t, dict(zip(t.wires, device_wires)))[0][0] for t in fragment_tapes ] + seed = kwargs.get("seed", None) configurations, settings = expand_fragment_tapes_mc( - fragment_tapes, communication_graph, shots=shots + fragment_tapes, communication_graph, shots=shots, seed=seed ) tapes = tuple(tape for c in configurations for tape in c) @@ -593,7 +594,7 @@ def _pauliZ(wire): def expand_fragment_tapes_mc( - tapes: QuantumScriptBatch, communication_graph: MultiDiGraph, shots: int + tapes: QuantumScriptBatch, communication_graph: MultiDiGraph, shots: int, seed=None ) -> tuple[QuantumScriptBatch, np.ndarray]: """ Expands fragment tapes into a sequence of random configurations of the contained pairs of @@ -617,6 +618,7 @@ def expand_fragment_tapes_mc( communication_graph (nx.MultiDiGraph): the communication (quotient) graph of the fragmented full graph shots (int): number of shots + seed (int, optional): seed for the random number generator. Defaults to None. Returns: Tuple[Sequence[QuantumTape], np.ndarray]: the tapes corresponding to each configuration and the @@ -683,7 +685,7 @@ def expand_fragment_tapes_mc( """ pairs = [e[-1] for e in communication_graph.edges.data("pair")] - settings = np.random.choice(range(8), size=(len(pairs), shots), replace=True) + settings = np.random.default_rng(seed).choice(range(8), size=(len(pairs), shots), replace=True) meas_settings = {pair[0].obj.id: setting for pair, setting in zip(pairs, settings)} prep_settings = {pair[1].obj.id: setting for pair, setting in zip(pairs, settings)} diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index 75181aee72f..16b71ea0a9a 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -16,6 +16,8 @@ .. currentmodule:: pennylane +.. _transforms: + Custom transforms ----------------- diff --git a/pennylane/transforms/dynamic_one_shot.py b/pennylane/transforms/dynamic_one_shot.py index d5b25a3072f..a1fd972fbd9 100644 --- a/pennylane/transforms/dynamic_one_shot.py +++ b/pennylane/transforms/dynamic_one_shot.py @@ -166,6 +166,23 @@ def processing_fn(results, has_partitioned_shots=None, batched_results=None): return aux_tapes, processing_fn +def get_legacy_capabilities(dev): + """Gets the capabilities dictionary of a device.""" + assert isinstance(dev, qml.devices.LegacyDeviceFacade) + return dev.target_device.capabilities() + + +def _supports_one_shot(dev: "qml.devices.Device"): + """Checks whether a device supports one-shot.""" + + if isinstance(dev, qml.devices.LegacyDevice): + return get_legacy_capabilities(dev).get("supports_mid_measure", False) + + return dev.name in ("default.qubit", "lightning.qubit") or ( + dev.capabilities is not None and "one-shot" in dev.capabilities.supported_mcm_methods + ) + + @dynamic_one_shot.custom_qnode_transform def _dynamic_one_shot_qnode(self, qnode, targs, tkwargs): """Custom qnode transform for ``dynamic_one_shot``.""" @@ -175,16 +192,12 @@ def _dynamic_one_shot_qnode(self, qnode, targs, tkwargs): "when transforming a QNode." ) if qnode.device is not None: - support_mcms = hasattr(qnode.device, "capabilities") and qnode.device.capabilities().get( - "supports_mid_measure", False - ) - support_mcms = support_mcms or qnode.device.name in ("default.qubit", "lightning.qubit") - if not support_mcms: + if not _supports_one_shot(qnode.device): raise TypeError( - f"Device {qnode.device.name} does not support mid-circuit measurements " - "natively, and hence it does not support the dynamic_one_shot transform. " - "'default.qubit' and 'lightning.qubit' currently support mid-circuit " - "measurements and the dynamic_one_shot transform." + f"Device {qnode.device.name} does not support mid-circuit measurements and/or " + "one-shot execution mode natively, and hence it does not support the " + "dynamic_one_shot transform. 'default.qubit' and 'lightning.qubit' currently " + "support mid-circuit measurements and the dynamic_one_shot transform." ) tkwargs.setdefault("device", qnode.device) return self.default_qnode_transform(qnode, targs, tkwargs) diff --git a/tests/devices/conftest.py b/tests/devices/conftest.py new file mode 100644 index 00000000000..04a85dce1f2 --- /dev/null +++ b/tests/devices/conftest.py @@ -0,0 +1,34 @@ +# 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. +""" +Pytest configuration file for the devices test module. +""" + +from os import path +from tempfile import TemporaryDirectory +from textwrap import dedent + +import pytest + + +@pytest.fixture(scope="function") +def create_temporary_toml_file(request) -> str: + """Create a temporary TOML file with the given content.""" + content = request.param + with TemporaryDirectory() as temp_dir: + toml_file = path.join(temp_dir, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write(dedent(content)) + request.node.toml_file = toml_file + yield diff --git a/tests/devices/test_capabilities.py b/tests/devices/test_capabilities.py index 502b02c214e..474e4f6f52e 100644 --- a/tests/devices/test_capabilities.py +++ b/tests/devices/test_capabilities.py @@ -18,12 +18,10 @@ # pylint: disable=protected-access,trailing-whitespace import re -from os import path -from tempfile import TemporaryDirectory -from textwrap import dedent import pytest +import pennylane as qml from pennylane.devices.capabilities import ( DeviceCapabilities, ExecutionCondition, @@ -33,7 +31,6 @@ _get_measurement_processes, _get_observables, _get_operations, - _get_options, _get_toml_section, load_toml_file, parse_toml_document, @@ -41,18 +38,6 @@ ) -@pytest.fixture(scope="function") -def create_temporary_toml_file(request) -> str: - """Create a temporary TOML file with the given content.""" - content = request.param - with TemporaryDirectory() as temp_dir: - toml_file = path.join(temp_dir, "test.toml") - with open(toml_file, "w", encoding="utf-8") as f: - f.write(dedent(content)) - request.node.toml_file = toml_file - yield - - @pytest.mark.unit class TestTOML: """Unit tests for loading and parsing TOML files.""" @@ -84,11 +69,6 @@ class TestTOML: qjit_compatible = false supported_mcm_methods = ["one-shot", "device"] - - [options] - - option_key = "option_field" - """ ], indirect=True, @@ -119,9 +99,6 @@ def test_load_toml_file(self, request): assert compilation.get("qjit_compatible") is False assert compilation.get("supported_mcm_methods") == ["one-shot", "device"] - options = document.get("options") - assert options.get("option_key") == "option_field" - @pytest.mark.usefixtures("create_temporary_toml_file") @pytest.mark.parametrize( "create_temporary_toml_file", @@ -235,28 +212,6 @@ def test_get_compilation_flags(self, request): assert compilation_flags.get("supported_mcm_methods") == ["one-shot"] assert compilation_flags.get("runtime_code_generation") is False - @pytest.mark.usefixtures("create_temporary_toml_file") - @pytest.mark.parametrize( - "create_temporary_toml_file", - [ - """ - [options] - - option_key = "option_value" - option_boolean = true - """ - ], - indirect=True, - ) - def test_get_options(self, request): - """Tests parsing options.""" - - document = load_toml_file(request.node.toml_file) - options = _get_options(document) - assert len(options) == 2 - assert options.get("option_key") == "option_value" - assert options.get("option_boolean") is True - @pytest.mark.usefixtures("create_temporary_toml_file") @pytest.mark.parametrize( "create_temporary_toml_file", @@ -510,7 +465,7 @@ def test_operator_properties(): controllable=True, invertible=True, differentiable=False, - conditions=[ExecutionCondition.ANALYTIC_MODE_ONLY], + conditions=[], ) intersection = prop1 & prop2 assert intersection.controllable is True @@ -524,7 +479,7 @@ def test_operator_properties(): [operators.gates] -RY = { properties = ["controllable", "invertible", "differentiable"] } +RY = { properties = ["controllable", "differentiable"] } RZ = { properties = ["controllable", "invertible", "differentiable"] } CNOT = { properties = ["invertible"] } @@ -562,9 +517,6 @@ def test_operator_properties(): non_commuting_observables = true supported_mcm_methods = ["one-shot"] -[options] - -option_key = "option_field" """ @@ -580,7 +532,7 @@ def test_load_from_toml_file(self, request): device_capabilities = parse_toml_document(document) assert isinstance(device_capabilities, DeviceCapabilities) assert device_capabilities.operations == { - "RY": OperatorProperties(invertible=True, differentiable=True, controllable=True), + "RY": OperatorProperties(differentiable=True, controllable=True), "RZ": OperatorProperties(invertible=True, differentiable=True, controllable=True), "CNOT": OperatorProperties(invertible=True), } @@ -601,7 +553,6 @@ def test_load_from_toml_file(self, request): assert device_capabilities.overlapping_observables is True assert device_capabilities.non_commuting_observables is False assert device_capabilities.initial_state_prep is True - assert device_capabilities.options == {"option_key": "option_field"} @pytest.mark.usefixtures("create_temporary_toml_file") @pytest.mark.parametrize("create_temporary_toml_file", [EXAMPLE_TOML_FILE], indirect=True) @@ -671,7 +622,7 @@ def test_from_toml_file_pennylane(self, request): assert isinstance(capabilities, DeviceCapabilities) assert capabilities == DeviceCapabilities( operations={ - "RY": OperatorProperties(invertible=True, differentiable=True, controllable=True), + "RY": OperatorProperties(differentiable=True, controllable=True), "RZ": OperatorProperties(invertible=True, differentiable=True, controllable=True), "CNOT": OperatorProperties(invertible=True), }, @@ -696,7 +647,6 @@ def test_from_toml_file_pennylane(self, request): non_commuting_observables=True, initial_state_prep=True, supported_mcm_methods=["one-shot"], - options={"option_key": "option_field"}, ) @pytest.mark.usefixtures("create_temporary_toml_file") @@ -708,7 +658,7 @@ def test_from_toml_file_qjit(self, request): assert isinstance(capabilities, DeviceCapabilities) assert capabilities == DeviceCapabilities( operations={ - "RY": OperatorProperties(invertible=True, differentiable=True, controllable=True), + "RY": OperatorProperties(differentiable=True, controllable=True), "RZ": OperatorProperties(invertible=True, differentiable=True, controllable=True), "CNOT": OperatorProperties(invertible=True), }, @@ -729,5 +679,42 @@ def test_from_toml_file_qjit(self, request): non_commuting_observables=False, initial_state_prep=True, supported_mcm_methods=[], - options={"option_key": "option_field"}, ) + + @pytest.mark.usefixtures("create_temporary_toml_file") + @pytest.mark.parametrize("create_temporary_toml_file", [EXAMPLE_TOML_FILE], indirect=True) + def test_supports_operators(self, request): + """Tests that the supports_operators method returns the correct result.""" + + capabilities = DeviceCapabilities.from_toml_file(request.node.toml_file) + + for op in [ + qml.RY(0.5, wires=0), + qml.ops.Controlled(qml.RY(0.5, wires=0), control_wires=[1]), + qml.RZ(0.5, wires=0), + qml.ops.Controlled(qml.RZ(0.5, wires=0), control_wires=[1]), + qml.adjoint(qml.RZ(0.5, wires=0)), + qml.ops.Adjoint(qml.ops.Controlled(qml.RZ(0.5, wires=0), control_wires=[1])), + qml.ops.Controlled(qml.ops.Adjoint(qml.RZ(0.5, wires=0)), control_wires=[1]), + qml.CNOT(wires=[0, 1]), + qml.adjoint(qml.CNOT), + ]: + assert capabilities.supports_operation(op.name) is True + + for op in [ + qml.X(0), + qml.adjoint(qml.RY(0.5, wires=0)), + qml.adjoint(qml.ops.Controlled(qml.RY(0.5, wires=0), control_wires=[1])), + qml.ops.Controlled(qml.ops.Adjoint(qml.RY(0.5, wires=0)), control_wires=[1]), + qml.ops.Controlled(qml.CNOT(wires=[0, 1]), control_wires=[2]), + ]: + assert capabilities.supports_operation(op.name) is False + + for obs in [qml.X(0), qml.Y(0), qml.Z(0)]: + assert capabilities.supports_observable(obs.name) is True + + for obs in [ + qml.H(0), + qml.Hamiltonian([0.5], [qml.PauliZ(0)]), + ]: + assert capabilities.supports_observable(obs.name) is False diff --git a/tests/devices/test_default_gaussian.py b/tests/devices/test_default_gaussian.py index cb5181cc63e..e03aba09330 100644 --- a/tests/devices/test_default_gaussian.py +++ b/tests/devices/test_default_gaussian.py @@ -729,7 +729,7 @@ def test_defines_correct_capabilities(self): """Test that the device defines the right capabilities""" dev = qml.device("default.gaussian", wires=1) - cap = dev.capabilities() + cap = dev.target_device.capabilities() capabilities = { "model": "cv", "supports_finite_shots": True, diff --git a/tests/devices/test_default_mixed_autograd.py b/tests/devices/test_default_mixed_autograd.py index c3f98d5955f..0d76e5b1375 100644 --- a/tests/devices/test_default_mixed_autograd.py +++ b/tests/devices/test_default_mixed_autograd.py @@ -45,7 +45,7 @@ def test_defines_correct_capabilities(self): """Test that the device defines the right capabilities""" dev = qml.device("default.mixed", wires=1) - cap = dev.capabilities() + cap = dev.target_device.capabilities() capabilities = { "model": "qubit", "supports_finite_shots": True, @@ -69,7 +69,7 @@ def test_load_device(self): assert dev.num_wires == 2 assert dev.shots == qml.measurements.Shots(None) assert dev.short_name == "default.mixed" - assert dev.capabilities()["passthru_devices"]["autograd"] == "default.mixed" + assert dev.target_device.capabilities()["passthru_devices"]["autograd"] == "default.mixed" def test_qubit_circuit(self, tol): """Test that the device provides the correct diff --git a/tests/devices/test_default_mixed_jax.py b/tests/devices/test_default_mixed_jax.py index 4e459cabe61..adcb5b77b51 100644 --- a/tests/devices/test_default_mixed_jax.py +++ b/tests/devices/test_default_mixed_jax.py @@ -53,7 +53,7 @@ def test_load_device(self): assert dev.num_wires == 2 assert dev.shots == qml.measurements.Shots(None) assert dev.short_name == "default.mixed" - assert dev.capabilities()["passthru_devices"]["jax"] == "default.mixed" + assert dev.target_device.capabilities()["passthru_devices"]["jax"] == "default.mixed" def test_qubit_circuit(self, tol): """Test that the device provides the correct diff --git a/tests/devices/test_default_mixed_tf.py b/tests/devices/test_default_mixed_tf.py index 256f70a4668..48749d24ea9 100644 --- a/tests/devices/test_default_mixed_tf.py +++ b/tests/devices/test_default_mixed_tf.py @@ -44,7 +44,7 @@ def test_load_device(self): assert dev.num_wires == 2 assert dev.shots == qml.measurements.Shots(None) assert dev.short_name == "default.mixed" - assert dev.capabilities()["passthru_devices"]["tf"] == "default.mixed" + assert dev.target_device.capabilities()["passthru_devices"]["tf"] == "default.mixed" def test_qubit_circuit(self, tol): """Test that the device provides the correct diff --git a/tests/devices/test_default_mixed_torch.py b/tests/devices/test_default_mixed_torch.py index b5de0d0867a..5622f5010d6 100644 --- a/tests/devices/test_default_mixed_torch.py +++ b/tests/devices/test_default_mixed_torch.py @@ -38,7 +38,7 @@ def test_load_device(self): assert dev.num_wires == 2 assert dev.shots == qml.measurements.Shots(None) assert dev.short_name == "default.mixed" - assert dev.capabilities()["passthru_devices"]["torch"] == "default.mixed" + assert dev.target_device.capabilities()["passthru_devices"]["torch"] == "default.mixed" def test_qubit_circuit(self, tol): """Test that the device provides the correct diff --git a/tests/devices/test_default_qutrit.py b/tests/devices/test_default_qutrit.py index 820a4f07cd3..08fbc70e370 100644 --- a/tests/devices/test_default_qutrit.py +++ b/tests/devices/test_default_qutrit.py @@ -771,7 +771,7 @@ def test_defines_correct_capabilities(self): """Test that the device defines the right capabilities""" dev = qml.device("default.qutrit", wires=1) - cap = dev.capabilities() + cap = dev.target_device.capabilities() capabilities = { "model": "qutrit", "supports_finite_shots": True, diff --git a/tests/devices/experimental/test_device_api.py b/tests/devices/test_device_api.py similarity index 85% rename from tests/devices/experimental/test_device_api.py rename to tests/devices/test_device_api.py index 2849c059ad4..93b1a5ae18b 100644 --- a/tests/devices/experimental/test_device_api.py +++ b/tests/devices/test_device_api.py @@ -14,11 +14,14 @@ """ Tests for the basic default behavior of the Device API. """ -# pylint:disable=unused-argument + +# pylint:disable=unused-argument,too-few-public-methods,unused-variable + import pytest import pennylane as qml from pennylane.devices import DefaultExecutionConfig, Device, ExecutionConfig +from pennylane.devices.capabilities import DeviceCapabilities from pennylane.wires import Wires @@ -33,14 +36,62 @@ class BadDevice(Device): BadDevice() # pylint: disable=abstract-class-instantiated +EXAMPLE_TOML_FILE = """ +schema = 3 + +[operators.gates] + +[operators.observables] + +[pennylane.operators.observables] + +[measurement_processes] + +[pennylane.measurement_processes] + +[compilation] + +""" + + +class TestDeviceCapabilities: + """Tests for the capabilities of a device.""" + + @pytest.mark.usefixtures("create_temporary_toml_file") + @pytest.mark.parametrize("create_temporary_toml_file", [EXAMPLE_TOML_FILE], indirect=True) + def test_device_capabilities(self, request): + """Tests that the device capabilities object is correctly initialized""" + + class DeviceWithCapabilities(Device): + """A device with a capabilities config file defined.""" + + config_filepath = request.node.toml_file + + def execute(self, circuits, execution_config=DefaultExecutionConfig): + return (0,) + + dev = DeviceWithCapabilities() + assert isinstance(dev.capabilities, DeviceCapabilities) + + def test_device_invalid_filepath(self): + """Tests that the device raises an error when the config file does not exist.""" + + with pytest.raises(FileNotFoundError): + + class DeviceWithInvalidCapabilities(Device): + + config_filepath = "nonexistent_file.toml" + + def execute(self, circuits, execution_config=DefaultExecutionConfig): + return (0,) + + class TestMinimalDevice: """Tests for a device with only a minimal execute provided.""" - # pylint: disable=too-few-public-methods class MinimalDevice(Device): """A device with only a dummy execute method provided.""" - # pylint:disable=unused-argnument def execute(self, circuits, execution_config=DefaultExecutionConfig): return (0,) @@ -50,6 +101,10 @@ def test_device_name(self): """Test the default name is the name of the class""" assert self.dev.name == "MinimalDevice" + def test_no_capabilities(self): + """Test the default capabilities are empty""" + assert self.dev.capabilities is None + @pytest.mark.parametrize( "wires,shots,expected", [ diff --git a/tests/devices/experimental/test_execution_config.py b/tests/devices/test_execution_config.py similarity index 100% rename from tests/devices/experimental/test_execution_config.py rename to tests/devices/test_execution_config.py diff --git a/tests/measurements/test_expval.py b/tests/measurements/test_expval.py index 0aebcec600c..a98b85fb37a 100644 --- a/tests/measurements/test_expval.py +++ b/tests/measurements/test_expval.py @@ -50,9 +50,9 @@ class TestExpval: """Tests for the expval function""" @pytest.mark.parametrize("shots", [None, 1111, [1111, 1111]]) - def test_value(self, tol, shots): + def test_value(self, tol, shots, seed): """Test that the expval interface works""" - dev = qml.device("default.qubit", wires=2, shots=shots) + dev = qml.device("default.qubit", wires=2, shots=shots, seed=seed) @qml.qnode(dev, diff_method="parameter-shift") def circuit(x): @@ -89,11 +89,11 @@ def circuit(): @pytest.mark.parametrize("shots", [None, 1111, [1111, 1111]]) @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 3)) def test_observable_is_measurement_value( - self, shots, phi, tol, tol_stochastic + self, shots, phi, tol, tol_stochastic, seed ): # pylint: disable=too-many-arguments """Test that expectation values for mid-circuit measurement values are correct for a single measurement value.""" - dev = qml.device("default.qubit", wires=2, shots=shots) + dev = qml.device("default.qubit", wires=2, shots=shots, seed=seed) @qml.qnode(dev) def circuit(phi): diff --git a/tests/measurements/test_state.py b/tests/measurements/test_state.py index 30eccda6c32..4be631e38b7 100644 --- a/tests/measurements/test_state.py +++ b/tests/measurements/test_state.py @@ -377,7 +377,7 @@ def test_no_state_capability(self, monkeypatch): """Test if an error is raised for devices that are not capable of returning the state. This is tested by changing the capability of default.qubit""" dev = qml.device("default.mixed", wires=1) - capabilities = dev.capabilities().copy() + capabilities = dev.target_device.capabilities().copy() capabilities["returns_state"] = False @qml.qnode(dev) @@ -1022,7 +1022,7 @@ def test_no_state_capability(self, monkeypatch): """Test if an error is raised for devices that are not capable of returning the density matrix. This is tested by changing the capability of default.qubit""" dev = qml.device("default.mixed", wires=2) - capabilities = dev.capabilities().copy() + capabilities = dev.target_device.capabilities().copy() capabilities["returns_state"] = False @qml.qnode(dev) diff --git a/tests/test_qnode.py b/tests/test_qnode.py index db332ade2ab..2086bb83257 100644 --- a/tests/test_qnode.py +++ b/tests/test_qnode.py @@ -840,7 +840,7 @@ def test_dynamic_one_shot_if_mcm_unsupported(self): with pytest.raises( TypeError, - match="does not support mid-circuit measurements natively, and hence it does not support the dynamic_one_shot transform.", + match="does not support mid-circuit measurements and/or one-shot execution mode", ): @qml.transforms.dynamic_one_shot diff --git a/tests/transforms/test_dynamic_one_shot.py b/tests/transforms/test_dynamic_one_shot.py index 56d40256ce4..86f95ce32cd 100644 --- a/tests/transforms/test_dynamic_one_shot.py +++ b/tests/transforms/test_dynamic_one_shot.py @@ -14,7 +14,7 @@ """ Tests for the transform implementing the deferred measurement principle. """ -# pylint: disable=too-few-public-methods, too-many-arguments +from functools import partial import numpy as np import pytest @@ -33,6 +33,8 @@ parse_native_mid_circuit_measurements, ) +# pylint: disable=too-few-public-methods, too-many-arguments + @pytest.mark.parametrize( "measurement", @@ -55,7 +57,10 @@ def test_postselection_error_with_wrong_device(): """Test that an error is raised when a device does not support native execution.""" dev = qml.device("default.mixed", wires=2) - with pytest.raises(TypeError, match="does not support mid-circuit measurements natively"): + with pytest.raises( + TypeError, + match="does not support mid-circuit measurements and/or one-shot execution mode natively", + ): @qml.dynamic_one_shot @qml.qnode(dev) @@ -87,6 +92,27 @@ def f(x): assert np.all(res != np.iinfo(np.int32).min) +@pytest.mark.parametrize("postselect_mode", ["hw-like", "fill-shots"]) +def test_postselect_mode_transform(postselect_mode): + """Test that invalid shots are discarded if requested""" + shots = 100 + dev = qml.device("default.qubit", shots=shots) + + @partial(qml.dynamic_one_shot) + @qml.qnode(dev, postselect_mode=postselect_mode) + def f(x): + qml.RX(x, 0) + _ = qml.measure(0, postselect=1) + return qml.sample(wires=[0, 1]) + + res = f(np.pi / 2) + if postselect_mode == "hw-like": + assert len(res) < shots + else: + assert len(res) == shots + assert np.all(res != np.iinfo(np.int32).min) + + @pytest.mark.jax @pytest.mark.parametrize("use_jit", [True, False]) @pytest.mark.parametrize("diff_method", [None, "best"]) diff --git a/tests/transforms/test_qcut.py b/tests/transforms/test_qcut.py index 5fbc2d88b58..47576602bc7 100644 --- a/tests/transforms/test_qcut.py +++ b/tests/transforms/test_qcut.py @@ -15,7 +15,7 @@ Unit tests for the `pennylane.qcut` package. """ # pylint: disable=protected-access,too-few-public-methods,too-many-arguments -# pylint: disable=too-many-public-methods,comparison-with-callable +# pylint: disable=too-many-public-methods,comparison-with-callable,unused-argument # pylint: disable=no-value-for-parameter,no-member,not-callable, use-implicit-booleaness-not-comparison import copy import itertools @@ -1900,8 +1900,14 @@ def test_expand_mc(self, monkeypatch): communication_graph = MultiDiGraph([(0, 1, edge_data)]) fixed_choice = np.array([[4, 0, 1]]) + + class _MockRNG: + + def choice(self, *args, **kwargs): + return fixed_choice + with monkeypatch.context() as m: - m.setattr(onp.random, "choice", lambda a, size, replace: fixed_choice) + m.setattr(onp.random, "default_rng", lambda *_: _MockRNG()) fragment_configurations, settings = qcut.expand_fragment_tapes_mc( tapes, communication_graph, 3 ) @@ -1965,11 +1971,17 @@ def test_expand_multinode_frag(self, monkeypatch): communication_graph = MultiDiGraph(frag_edge_data) fixed_choice = np.array([[4, 6], [1, 2], [2, 3], [3, 0]]) + + class _MockRNG: + + def choice(self, *args, **kwargs): + return fixed_choice + with monkeypatch.context() as m: m.setattr( onp.random, - "choice", - lambda a, size, replace: fixed_choice, + "default_rng", + lambda *_: _MockRNG(), ) fragment_configurations, settings = qcut.expand_fragment_tapes_mc( frags, communication_graph, 2 @@ -2539,7 +2551,7 @@ def target_circuit(v): dev = dev_fn(wires=2, shots=20000, seed=seed) - @partial(qml.cut_circuit_mc, classical_processing_fn=fn) + @partial(qml.cut_circuit_mc, classical_processing_fn=fn, seed=seed) @qml.qnode(dev) def circuit(v): qml.RX(v, wires=0) From e8541af642e73740da52cec7e44a1d5c30fdf770 Mon Sep 17 00:00:00 2001 From: ringo-but-quantum Date: Fri, 22 Nov 2024 09:51:43 +0000 Subject: [PATCH 34/35] [no ci] bump nightly version --- pennylane/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/_version.py b/pennylane/_version.py index b777d56f112..c9182399991 100644 --- a/pennylane/_version.py +++ b/pennylane/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.40.0-dev19" +__version__ = "0.40.0-dev20" From 9c1292df7783329f1b7966806906d0305e35f14b Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Fri, 22 Nov 2024 09:45:06 -0500 Subject: [PATCH 35/35] Support the construction of bosonic operators (#6518) **Context:** We need `BoseWords` and `BoseSentences` to help us build vibrational hamiltonians. **Description of the Change:** Add `BoseWords` and `BoseSentences` **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** [sc-72640] --------- Co-authored-by: ddhawan11 Co-authored-by: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com> Co-authored-by: Utkarsh --- doc/releases/changelog-dev.md | 3 + pennylane/__init__.py | 1 + pennylane/bose/__init__.py | 3 + pennylane/bose/bosonic.py | 619 +++++++++++++++++ tests/bose/test_bosonic.py | 1230 +++++++++++++++++++++++++++++++++ 5 files changed, 1856 insertions(+) create mode 100644 pennylane/bose/__init__.py create mode 100644 pennylane/bose/bosonic.py create mode 100644 tests/bose/test_bosonic.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 19983a537e2..7d9dfbc1e56 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -51,6 +51,9 @@ * Added submodule 'initialize_state' featuring a `create_initial_state` function for initializing a density matrix from `qml.StatePrep` operations or `qml.QubitDensityMatrix` operations. [(#6503)](https://github.com/PennyLaneAI/pennylane/pull/6503) + +* Added support for constructing `BoseWord` and `BoseSentence`, similar to `FermiWord` and `FermiSentence`. + [(#6518)](https://github.com/PennyLaneAI/pennylane/pull/6518) * Added a second class `DefaultMixedNewAPI` to the `qml.devices.qubit_mixed` module, which is to be the replacement of legacy `DefaultMixed` which for now to hold the implementations of `preprocess` and `execute` methods. [(#6607)](https://github.com/PennyLaneAI/pennylane/pull/6507) diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 334b860e492..6f9798e86bb 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -39,6 +39,7 @@ parity_transform, bravyi_kitaev, ) +from pennylane.bose import BoseSentence, BoseWord from pennylane.qchem import ( taper, symmetry_generators, diff --git a/pennylane/bose/__init__.py b/pennylane/bose/__init__.py new file mode 100644 index 00000000000..eb082b4a7c1 --- /dev/null +++ b/pennylane/bose/__init__.py @@ -0,0 +1,3 @@ +"""A module containing utility functions and mappings for working with bosonic operators. """ + +from .bosonic import BoseWord, BoseSentence diff --git a/pennylane/bose/bosonic.py b/pennylane/bose/bosonic.py new file mode 100644 index 00000000000..02cf4375e07 --- /dev/null +++ b/pennylane/bose/bosonic.py @@ -0,0 +1,619 @@ +# Copyright 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. +"""The bosonic representation classes and functions.""" +from copy import copy + +import pennylane as qml +from pennylane.typing import TensorLike + +# pylint: disable= too-many-nested-blocks, too-many-branches, invalid-name + + +class BoseWord(dict): + r"""Dictionary used to represent a Bose word, a product of bosonic creation and + annihilation operators, that can be constructed from a standard dictionary. + + The keys of the dictionary are tuples of two integers. The first integer represents the + position of the creation/annihilation operator in the Bose word and the second integer + represents the mode it acts on. The values of the dictionary are one of ``'+'`` or ``'-'`` + symbols that denote creation and annihilation operators, respectively. The operator + :math:`b^{\dagger}_0 b_1` can then be constructed as + + >>> w = qml.bose.BoseWord({(0, 0) : '+', (1, 1) : '-'}) + >>> print(w) + b⁺(0) b(1) + """ + + # override the arithmetic dunder methods for numpy arrays so that the + # methods defined on this class are used instead + # (i.e. ensure `np.array + BoseWord` uses `BoseWord.__radd__` instead of `np.array.__add__`) + __numpy_ufunc__ = None + __array_ufunc__ = None + + def __init__(self, operator): + self.sorted_dic = dict(sorted(operator.items())) + indices = [i[0] for i in self.sorted_dic.keys()] + + if indices: + if list(range(max(indices) + 1)) != indices: + raise ValueError( + "The operator indices must belong to the set {0, ..., len(operator)-1}." + ) + + super().__init__(operator) + + def adjoint(self): + r"""Return the adjoint of BoseWord.""" + n = len(self.items()) + adjoint_dict = {} + for key, value in reversed(self.items()): + position = n - key[0] - 1 + orbital = key[1] + bose = "+" if value == "-" else "-" + adjoint_dict[(position, orbital)] = bose + + return BoseWord(adjoint_dict) + + def items(self): + """Returns the dictionary items in sorted order.""" + return self.sorted_dic.items() + + @property + def wires(self): + r"""Return wires in a BoseWord.""" + return set(i[1] for i in self.sorted_dic.keys()) + + def __missing__(self, key): + r"""Return empty string for a missing key in BoseWord.""" + return "" + + def update(self, item): + r"""Restrict updating BoseWord after instantiation.""" + raise TypeError("BoseWord object does not support assignment") + + def __setitem__(self, key, item): + r"""Restrict setting items after instantiation.""" + raise TypeError("BoseWord object does not support assignment") + + def __reduce__(self): + r"""Defines how to pickle and unpickle a BoseWord. Otherwise, un-pickling + would cause __setitem__ to be called, which is forbidden on PauliWord. + For more information, see: https://docs.python.org/3/library/pickle.html#object.__reduce__ + """ + return BoseWord, (dict(self),) + + def __copy__(self): + r"""Copy the BoseWord instance.""" + return BoseWord(dict(self.items())) + + def __deepcopy__(self, memo): + r"""Deep copy the BoseWord instance.""" + res = self.__copy__() + memo[id(self)] = res + return res + + def __hash__(self): + r"""Hash value of a BoseWord.""" + return hash(frozenset(self.items())) + + def to_string(self): + r"""Return a compact string representation of a BoseWord. Each operator in the word is + represented by the number of the wire it operates on, and a `+` or `-` to indicate either + a creation or annihilation operator. + + >>> w = qml.bose.BoseWord({(0, 0) : '+', (1, 1) : '-'}) + >>> w.to_string() + 'b⁺(0) b(1)' + """ + if len(self) == 0: + return "I" + + symbol_map = {"+": "\u207a", "-": ""} + + string = " ".join( + [ + "b" + symbol_map[j] + "(" + i + ")" + for i, j in zip( + [str(i[1]) for i in self.sorted_dic.keys()], self.sorted_dic.values() + ) + ] + ) + return string + + def __str__(self): + r"""String representation of a BoseWord.""" + return f"{self.to_string()}" + + def __repr__(self): + r"""Terminal representation of a BoseWord""" + return f"BoseWord({self.sorted_dic})" + + def __add__(self, other): + """Add a BoseSentence, BoseWord or constant to a BoseWord. Converts both + elements into BoseSentences, and uses the BoseSentence __add__ + method""" + + self_bs = BoseSentence({self: 1.0}) + + if isinstance(other, BoseSentence): + return self_bs + other + + if isinstance(other, BoseWord): + return self_bs + BoseSentence({other: 1.0}) + + if not isinstance(other, TensorLike): + raise TypeError(f"Cannot add {type(other)} to a BoseWord.") + + if qml.math.size(other) > 1: + raise ValueError( + f"Arithmetic Bose operations can only accept an array of length 1, " + f"but received {other} of length {len(other)}" + ) + + return self_bs + BoseSentence({BoseWord({}): other}) + + def __radd__(self, other): + """Add a BoseWord to a constant, i.e. `2 + BoseWord({...})`""" + return self.__add__(other) + + def __sub__(self, other): + """Subtract a BoseSentence, BoseWord or constant from a BoseWord. Converts both + elements into BoseSentences (with negative coefficient for `other`), and + uses the BoseSentence __add__ method""" + + self_bs = BoseSentence({self: 1.0}) + + if isinstance(other, BoseWord): + return self_bs + BoseSentence({other: -1.0}) + + if isinstance(other, BoseSentence): + other_bs = BoseSentence(dict(zip(other.keys(), [-v for v in other.values()]))) + return self_bs + other_bs + + if not isinstance(other, TensorLike): + raise TypeError(f"Cannot subtract {type(other)} from a BoseWord.") + + if qml.math.size(other) > 1: + raise ValueError( + f"Arithmetic Bose operations can only accept an array of length 1, " + f"but received {other} of length {len(other)}" + ) + + return self_bs + BoseSentence({BoseWord({}): -1 * other}) # -constant * I + + def __rsub__(self, other): + """Subtract a BoseWord to a constant, i.e. `2 - BoseWord({...})`""" + if not isinstance(other, TensorLike): + raise TypeError(f"Cannot subtract a BoseWord from {type(other)}.") + + if qml.math.size(other) > 1: + raise ValueError( + f"Arithmetic Bose operations can only accept an array of length 1, " + f"but received {other} of length {len(other)}" + ) + self_bs = BoseSentence({self: -1.0}) + other_bs = BoseSentence({BoseWord({}): other}) + return self_bs + other_bs + + def __mul__(self, other): + r"""Multiply a BoseWord with another BoseWord, a BoseSentence, or a constant. + + >>> w = qml.bose.BoseWord({(0, 0) : '+', (1, 1) : '-'}) + >>> print(w * w) + b⁺(0) b(1) b⁺(0) b(1) + """ + + if isinstance(other, BoseWord): + if len(self) == 0: + return copy(other) + + if len(other) == 0: + return copy(self) + + order_final = [i[0] + len(self) for i in other.sorted_dic.keys()] + other_wires = [i[1] for i in other.sorted_dic.keys()] + + dict_other = dict( + zip( + [(order_idx, other_wires[i]) for i, order_idx in enumerate(order_final)], + other.values(), + ) + ) + dict_self = dict(zip(self.keys(), self.values())) + + dict_self.update(dict_other) + + return BoseWord(dict_self) + + if isinstance(other, BoseSentence): + return BoseSentence({self: 1}) * other + + if not isinstance(other, TensorLike): + raise TypeError(f"Cannot multiply BoseWord by {type(other)}.") + + if qml.math.size(other) > 1: + raise ValueError( + f"Arithmetic Bose operations can only accept an array of length 1, " + f"but received {other} of length {len(other)}" + ) + + return BoseSentence({self: other}) + + def __rmul__(self, other): + r"""Reverse multiply a BoseWord + + Multiplies a BoseWord "from the left" with an object that can't be modified + to support __mul__ for BoseWord. Will be defaulted in for example + ``2 * BoseWord({(0, 0): "+"})``, where the ``__mul__`` operator on an integer + will fail to multiply with a BoseWord""" + + return self.__mul__(other) + + def __pow__(self, value): + r"""Exponentiate a Bose word to an integer power. + + >>> w = qml.bose.BoseWord({(0, 0) : '+', (1, 1) : '-'}) + >>> print(w**3) + b⁺(0) b(1) b⁺(0) b(1) b⁺(0) b(1) + """ + if value < 0 or not isinstance(value, int): + raise ValueError("The exponent must be a positive integer.") + + operator = BoseWord({}) + + for _ in range(value): + operator *= self + + return operator + + def normal_order(self): + r"""Convert a BoseWord to its normal-ordered form. + + >>> bw = qml.bose.BoseWord({(0, 0): "-", (1, 0): "-", (2, 0): "+", (3, 0): "+"}) + >>> print(bw.normal_order()) + 4.0 * b⁺(0) b(0) + + 2.0 * I + + 1.0 * b⁺(0) b⁺(0) b(0) b(0) + """ + bw_terms = sorted(self) + len_op = len(bw_terms) + bw_comm = BoseSentence({BoseWord({}): 0.0}) + + if len_op == 0: + return 1 * BoseWord({}) + + bw = self + + left_pointer = 0 + # The right pointer iterates through all operators in the BoseWord + for right_pointer in range(len_op): + # The right pointer finds the leftmost creation operator + if self[bw_terms[right_pointer]] == "+": + # This ensures that the left pointer starts at the leftmost annihilation term + if left_pointer == right_pointer: + left_pointer += 1 + continue + + # We shift the leftmost creation operator to the position of the left pointer + bs = bw.shift_operator(right_pointer, left_pointer) + bs_as_list = sorted(list(bs.items()), key=lambda x: len(x[0].keys()), reverse=True) + bw = bs_as_list[0][0] + # Sort by ascending index order + if left_pointer > 0: + bw_as_list = sorted(list(bw.keys())) + if bw_as_list[left_pointer - 1][1] > bw_as_list[left_pointer][1]: + temp_bs = bw.shift_operator(left_pointer - 1, left_pointer) + bw = list(temp_bs.items())[0][0] + + for i in range(1, len(bs_as_list)): + bw_comm += bs_as_list[i][0] * bs_as_list[i][1] + + # Left pointer now points to the new leftmost annihilation term + left_pointer += 1 + + ordered_op = bw + bw_comm.normal_order() + ordered_op.simplify(tol=1e-8) + return ordered_op + + def shift_operator(self, initial_position, final_position): + r"""Shifts an operator in the BoseWord from ``initial_position`` to ``final_position`` by applying the bosonic commutation relations. + + Args: + initial_position (int): the position of the operator to be shifted + final_position (int): the desired position of the operator + + Returns: + BoseSentence: The ``BoseSentence`` obtained after applying the commutator relations. + + Raises: + TypeError: if ``initial_position`` or ``final_position`` is not an integer + ValueError: if ``initial_position`` or ``final_position`` are outside the range ``[0, len(BoseWord) - 1]`` + where ``len(BoseWord)`` is the number of operators in the BoseWord. + """ + + if not isinstance(initial_position, int) or not isinstance(final_position, int): + raise TypeError("Positions must be integers.") + + if initial_position < 0 or final_position < 0: + raise ValueError("Positions must be positive integers.") + + if initial_position > len(self.sorted_dic) - 1 or final_position > len(self.sorted_dic) - 1: + raise ValueError("Positions are out of range.") + + if initial_position == final_position: + return BoseSentence({self: 1}) + + bw = self + bs = BoseSentence({bw: 1}) + delta = 1 if initial_position < final_position else -1 + current = initial_position + + while current != final_position: + indices = list(bw.sorted_dic.keys()) + next = current + delta + curr_idx, curr_val = indices[current], bw[indices[current]] + next_idx, next_val = indices[next], bw[indices[next]] + + # commuting identical terms + if curr_idx[1] == next_idx[1] and curr_val == next_val: + current += delta + continue + + coeff = bs.pop(bw) + + bw = dict(bw) + bw[(current, next_idx[1])] = next_val + bw[(next, curr_idx[1])] = curr_val + + if curr_idx[1] != next_idx[1]: + del bw[curr_idx], bw[next_idx] + + bw = BoseWord(bw) + + # commutator is 0 + if curr_val == next_val or curr_idx[1] != next_idx[1]: + current += delta + bs += coeff * bw + continue + + # commutator is 1 + _min = min(current, next) + _max = max(current, next) + items = list(bw.sorted_dic.items()) + + left = BoseWord({(i, key[1]): value for i, (key, value) in enumerate(items[:_min])}) + middle = BoseWord( + {(i, key[1]): value for i, (key, value) in enumerate(items[_min : _max + 1])} + ) + right = BoseWord( + {(i, key[1]): value for i, (key, value) in enumerate(items[_max + 1 :])} + ) + + terms = left * (1 + middle) * right + bs += coeff * terms + + current += delta + + return bs + + +# pylint: disable=useless-super-delegation +class BoseSentence(dict): + r"""Dictionary used to represent a Bose sentence, a linear combination of Bose words, + with the keys as BoseWord instances and the values correspond to coefficients. + + >>> w1 = qml.bose.BoseWord({(0, 0) : '+', (1, 1) : '-'}) + >>> w2 = qml.bose.BoseWord({(0, 1) : '+', (1, 2) : '-'}) + >>> s = BoseSentence({w1 : 1.2, w2: 3.1}) + >>> print(s) + 1.2 * b⁺(0) b(1) + + 3.1 * b⁺(1) b(2) + """ + + # override the arithmetic dunder methods for numpy arrays so that the + # methods defined on this class are used instead + # (i.e. ensure `np.array + BoseSentence` uses `BoseSentence.__radd__` + # instead of `np.array.__add__`) + __numpy_ufunc__ = None + __array_ufunc__ = None + + def __init__(self, operator): + super().__init__(operator) + + def adjoint(self): + r"""Return the adjoint of BoseSentence.""" + adjoint_dict = {} + for key, value in self.items(): + word = key.adjoint() + scalar = qml.math.conj(value) + adjoint_dict[word] = scalar + + return BoseSentence(adjoint_dict) + + @property + def wires(self): + r"""Return wires of the BoseSentence.""" + return set().union(*(bw.wires for bw in self.keys())) + + def __str__(self): + r"""String representation of a BoseSentence.""" + if len(self) == 0: + return "0 * I" + return "\n+ ".join(f"{coeff} * {bw.to_string()}" for bw, coeff in self.items()) + + def __repr__(self): + r"""Terminal representation for BoseSentence.""" + return f"BoseSentence({dict(self)})" + + def __missing__(self, key): + r"""If the BoseSentence does not contain a BoseWord then the associated value will be 0.""" + return 0.0 + + def __add__(self, other): + r"""Add a BoseSentence, BoseWord or constant to a BoseSentence by iterating over the + smaller one and adding its terms to the larger one.""" + + if not isinstance(other, (TensorLike, BoseWord, BoseSentence)): + raise TypeError(f"Cannot add {type(other)} to a BoseSentence.") + + if qml.math.size(other) > 1: + raise ValueError( + f"Arithmetic Bose operations can only accept an array of length 1, " + f"but received {other} of length {len(other)}" + ) + + if isinstance(other, BoseWord): + other = BoseSentence({other: 1}) + if isinstance(other, TensorLike): + other = BoseSentence({BoseWord({}): other}) + + smaller_bs, larger_bs = ( + (self, copy(other)) if len(self) < len(other) else (other, copy(self)) + ) + for key in smaller_bs: + larger_bs[key] += smaller_bs[key] + + return larger_bs + + def __radd__(self, other): + """Add a BoseSentence to a constant, i.e. `2 + BoseSentence({...})`""" + return self.__add__(other) + + def __sub__(self, other): + r"""Subtract a BoseSentence, BoseWord or constant from a BoseSentence""" + if isinstance(other, BoseWord): + other = BoseSentence({other: -1}) + return self.__add__(other) + + if isinstance(other, BoseSentence): + other = BoseSentence(dict(zip(other.keys(), [-1 * v for v in other.values()]))) + return self.__add__(other) + + if not isinstance(other, TensorLike): + raise TypeError(f"Cannot subtract {type(other)} from a BoseSentence.") + + if qml.math.size(other) > 1: + raise ValueError( + f"Arithmetic Bose operations can only accept an array of length 1, " + f"but received {other} of length {len(other)}" + ) + + other = BoseSentence({BoseWord({}): -1 * other}) # -constant * I + return self.__add__(other) + + def __rsub__(self, other): + """Subtract a BoseSentence to a constant, i.e. 2 - BoseSentence({...})""" + + if not isinstance(other, TensorLike): + raise TypeError(f"Cannot subtract a BoseSentence from {type(other)}.") + + if qml.math.size(other) > 1: + raise ValueError( + f"Arithmetic Bose operations can only accept an array of length 1, " + f"but received {other} of length {len(other)}" + ) + + self_bs = BoseSentence(dict(zip(self.keys(), [-1 * v for v in self.values()]))) + other_bs = BoseSentence({BoseWord({}): other}) # constant * I + return self_bs + other_bs + + def __mul__(self, other): + r"""Multiply two Bose sentences by iterating over each sentence and multiplying the Bose + words pair-wise""" + + if isinstance(other, BoseWord): + other = BoseSentence({other: 1}) + + if isinstance(other, BoseSentence): + if (len(self) == 0) or (len(other) == 0): + return BoseSentence({BoseWord({}): 0}) + + product = BoseSentence({}) + + for bw1, coeff1 in self.items(): + for bw2, coeff2 in other.items(): + product[bw1 * bw2] += coeff1 * coeff2 + + return product + + if not isinstance(other, TensorLike): + raise TypeError(f"Cannot multiply BoseSentence by {type(other)}.") + + if qml.math.size(other) > 1: + raise ValueError( + f"Arithmetic Bose operations can only accept an array of length 1, " + f"but received {other} of length {len(other)}" + ) + vals = [i * other for i in self.values()] + return BoseSentence(dict(zip(self.keys(), vals))) + + def __rmul__(self, other): + r"""Reverse multiply a BoseSentence + + Multiplies a BoseSentence "from the left" with an object that can't be modified + to support __mul__ for BoseSentence. Will be defaulted in for example when + multiplying ``2 * bose_sentence``, since the ``__mul__`` operator on an integer + will fail to multiply with a BoseSentence""" + + if not isinstance(other, TensorLike): + raise TypeError(f"Cannot multiply {type(other)} by BoseSentence.") + + if qml.math.size(other) > 1: + raise ValueError( + f"Arithmetic Bose operations can only accept an array of length 1, " + f"but received {other} of length {len(other)}" + ) + + vals = [i * other for i in self.values()] + return BoseSentence(dict(zip(self.keys(), vals))) + + def __pow__(self, value): + r"""Exponentiate a Bose sentence to an integer power.""" + if value < 0 or not isinstance(value, int): + raise ValueError("The exponent must be a positive integer.") + + operator = BoseSentence({BoseWord({}): 1}) # 1 times Identity + + for _ in range(value): + operator *= self + + return operator + + def simplify(self, tol=1e-8): + r"""Remove any BoseWords in the BoseSentence with coefficients less than the threshold + tolerance.""" + items = list(self.items()) + for bw, coeff in items: + if abs(coeff) <= tol: + del self[bw] + + def normal_order(self): + r"""Convert a BoseSentence to its normal-ordered form. + + >>> bw = qml.bose.BoseWord({(0, 0): "-", (1, 0): "-", (2, 0): "+", (3, 0): "+"}) + >>> bs = qml.bose.BoseSentence({bw: 1}) + >>> print(bw.normal_order()) + 4.0 * b⁺(0) b(0) + + 2.0 * I + + 1.0 * b⁺(0) b⁺(0) b(0) b(0) + """ + + bose_sen_ordered = BoseSentence({}) + + for bw, coeff in self.items(): + bose_word_ordered = bw.normal_order() + for bw_ord, coeff_ord in bose_word_ordered.items(): + bose_sen_ordered += coeff_ord * coeff * bw_ord + + return bose_sen_ordered diff --git a/tests/bose/test_bosonic.py b/tests/bose/test_bosonic.py new file mode 100644 index 00000000000..6ea71ea07a5 --- /dev/null +++ b/tests/bose/test_bosonic.py @@ -0,0 +1,1230 @@ +# Copyright 2024 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Unit Tests for the Bosonic representation classes.""" +import pickle +from copy import copy, deepcopy + +import numpy as np +import pytest + +from pennylane import numpy as pnp +from pennylane.bose import BoseSentence, BoseWord + +bw1 = BoseWord({(0, 0): "+", (1, 1): "-"}) +bw1_dag = BoseWord({(0, 1): "+", (1, 0): "-"}) + +bw2 = BoseWord({(0, 0): "+", (1, 0): "-"}) +bw2_dag = BoseWord({(0, 0): "+", (1, 0): "-"}) + +bw3 = BoseWord({(0, 0): "+", (1, 3): "-", (2, 0): "+", (3, 4): "-"}) +bw3_dag = BoseWord({(0, 4): "+", (1, 0): "-", (2, 3): "+", (3, 0): "-"}) + +bw4 = BoseWord({}) +bw4_dag = BoseWord({}) + +bw5 = BoseWord({(0, 10): "+", (1, 30): "-", (2, 0): "+", (3, 400): "-"}) +bw5_dag = BoseWord({(0, 400): "+", (1, 0): "-", (2, 30): "+", (3, 10): "-"}) + +bw6 = BoseWord({(0, 10): "+", (1, 30): "+", (2, 0): "-", (3, 400): "-"}) +bw6_dag = BoseWord({(0, 400): "+", (1, 0): "+", (2, 30): "-", (3, 10): "-"}) + +bw7 = BoseWord({(0, 10): "-", (1, 30): "+", (2, 0): "-", (3, 400): "+"}) +bw7_dag = BoseWord({(0, 400): "-", (1, 0): "+", (2, 30): "-", (3, 10): "+"}) + +bw8 = BoseWord({(0, 0): "-", (1, 1): "+"}) +bw8c = BoseWord({(0, 1): "+", (1, 0): "-"}) +bw8cs = BoseSentence({bw8c: -1}) + +bw9 = BoseWord({(0, 0): "-", (1, 1): "-"}) +bw9c = BoseWord({(0, 1): "-", (1, 0): "-"}) +bw9cs = BoseSentence({bw9c: -1}) + +bw10 = BoseWord({(0, 0): "+", (1, 1): "+"}) +bw10c = BoseWord({(0, 1): "+", (1, 0): "+"}) +bw10cs = BoseSentence({bw10c: -1}) + +bw11 = BoseWord({(0, 0): "-", (1, 0): "+"}) +bw11c = BoseWord({(0, 0): "+", (1, 0): "-"}) +bw11cs = 1 + BoseSentence({bw11c: -1}) + +bw12 = BoseWord({(0, 0): "+", (1, 0): "+"}) +bw12c = BoseWord({(0, 0): "+", (1, 0): "+"}) +bw12cs = BoseSentence({bw12c: 1}) + +bw13 = BoseWord({(0, 0): "-", (1, 0): "-"}) +bw13c = BoseWord({(0, 0): "-", (1, 0): "-"}) +bw13cs = BoseSentence({bw13c: 1}) + +bw14 = BoseWord({(0, 0): "+", (1, 0): "-"}) +bw14c = BoseWord({(0, 0): "-", (1, 0): "+"}) +bw14cs = 1 + BoseSentence({bw14c: -1}) + +bw15 = BoseWord({(0, 0): "-", (1, 1): "+", (2, 2): "+"}) +bw15c = BoseWord({(0, 1): "+", (1, 0): "-", (2, 2): "+"}) +bw15cs = BoseSentence({bw15c: -1}) + +bw16 = BoseWord({(0, 0): "-", (1, 1): "+", (2, 2): "-"}) +bw16c = BoseWord({(0, 0): "-", (1, 2): "-", (2, 1): "+"}) +bw16cs = BoseSentence({bw16c: -1}) + +bw17 = BoseWord({(0, 0): "-", (1, 0): "+", (2, 2): "-"}) +bw17c1 = BoseWord({(0, 2): "-"}) +bw17c2 = BoseWord({(0, 0): "+", (1, 0): "-", (2, 2): "-"}) +bw17cs = bw17c1 - bw17c2 + +bw18 = BoseWord({(0, 0): "+", (1, 1): "+", (2, 2): "-", (3, 3): "-"}) +bw18c = BoseWord({(0, 0): "+", (1, 3): "-", (2, 1): "+", (3, 2): "-"}) +bw18cs = BoseSentence({bw18c: 1}) + +bw19 = BoseWord({(0, 0): "+", (1, 1): "+", (2, 2): "-", (3, 2): "+"}) +bw19c1 = BoseWord({(0, 0): "+", (1, 1): "+"}) +bw19c2 = BoseWord({(0, 2): "+", (1, 0): "+", (2, 1): "+", (3, 2): "-"}) +bw19cs = BoseSentence({bw19c1: 1, bw19c2: -1}) + +bw20 = BoseWord({(0, 0): "-", (1, 0): "+", (2, 1): "-", (3, 0): "-", (4, 0): "+"}) +bw20c1 = BoseWord({(0, 0): "-", (1, 0): "+", (2, 1): "-"}) +bw20c2 = BoseWord({(0, 0): "+", (1, 1): "-", (2, 0): "-"}) +bw20c3 = BoseWord({(0, 0): "+", (1, 0): "-", (2, 0): "+", (3, 1): "-", (4, 0): "-"}) +bw20cs = bw20c1 + bw20c2 - bw20c3 + +bw21 = BoseWord({(0, 0): "-", (1, 1): "-", (2, 0): "+", (3, 1): "+", (4, 0): "+", (5, 2): "+"}) +bw22 = BoseWord({(0, 0): "-", (1, 0): "+"}) + + +# pylint: disable=too-many-public-methods +class TestBoseWord: + """ + Tests for BoseWord + """ + + # Expected bose sentences were computed manually or with openfermion + @pytest.mark.parametrize( + ("bose_sentence", "expected"), + [ + ( + BoseSentence( + { + BoseWord({(0, 0): "+", (1, 0): "+"}): 5.051e-06, + BoseWord({(0, 0): "+", (1, 0): "-"}): 5.051e-06, + BoseWord({(0, 0): "-", (1, 0): "+"}): 5.051e-06, + BoseWord({(0, 0): "-", (1, 0): "-"}): 5.051e-06, + } + ), + BoseSentence( + { + BoseWord({(0, 0): "+", (1, 0): "+"}): 5.051e-06, + BoseWord({(0, 0): "+", (1, 0): "-"}): 1.0102e-05, + BoseWord({}): 5.051e-06, + BoseWord({(0, 0): "-", (1, 0): "-"}): 5.051e-06, + } + ), + ), + ( + bw21, + BoseSentence( + { + BoseWord({(0, 0): "+", (1, 2): "+"}): 2.0, + BoseWord({(0, 0): "+", (1, 1): "+", (2, 2): "+", (3, 1): "-"}): 2.0, + BoseWord({(0, 0): "+", (1, 0): "+", (2, 2): "+", (3, 0): "-"}): 1.0, + BoseWord( + { + (0, 0): "+", + (1, 0): "+", + (2, 1): "+", + (3, 2): "+", + (4, 0): "-", + (5, 1): "-", + } + ): 1.0, + } + ), + ), + ], + ) + def test_normal_order(self, bose_sentence, expected): + """Test that normal_order correctly normal orders the BoseWord""" + assert bose_sentence.normal_order() == expected + + @pytest.mark.parametrize( + ("bw", "i", "j", "bs"), + [ + ( + bw22, + 0, + 1, + BoseSentence({BoseWord({(0, 0): "+", (1, 0): "-"}): 1.0, BoseWord({}): 1.0}), + ), + ( + bw22, + 0, + 0, + BoseSentence({bw22: 1}), + ), + ( + BoseWord({(0, 0): "-", (1, 0): "-", (2, 0): "+", (3, 0): "+"}), + 3, + 0, + BoseSentence( + { + BoseWord({(0, 0): "+", (1, 0): "-", (2, 0): "-", (3, 0): "+"}): 1.0, + BoseWord({(0, 0): "-", (1, 0): "+"}): 2.0, + } + ), + ), + ( + BoseWord({(0, 0): "+", (1, 0): "-", (2, 0): "-", (3, 0): "+"}), + 3, + 1, + BoseSentence( + { + BoseWord({(0, 0): "+", (1, 0): "+", (2, 0): "-", (3, 0): "-"}): 1.0, + BoseWord({(0, 0): "+", (1, 0): "-"}): 2.0, + } + ), + ), + ( + BoseWord({(0, 0): "-", (1, 1): "+"}), + 0, + 1, + BoseSentence({BoseWord({(0, 1): "+", (1, 0): "-"}): 1}), + ), + ( + BoseWord({(0, 0): "-", (1, 0): "+", (2, 0): "+", (3, 0): "-"}), + 0, + 2, + BoseSentence( + { + BoseWord({(0, 0): "+", (1, 0): "+", (2, 0): "-", (3, 0): "-"}): 1.0, + BoseWord({(0, 0): "+", (1, 0): "-"}): 2.0, + } + ), + ), + ], + ) + def test_shift_operator(self, bw, i, j, bs): + """Test that the shift_operator method correctly applies the commutator relations.""" + assert bw.shift_operator(i, j) == bs + + def test_shift_operator_errors(self): + """Test that the shift_operator method correctly raises exceptions.""" + with pytest.raises(TypeError, match="Positions must be integers."): + bw1.shift_operator(0.5, 1) + + with pytest.raises(ValueError, match="Positions must be positive integers."): + bw1.shift_operator(-1, 0) + + with pytest.raises(ValueError, match="Positions are out of range."): + bw1.shift_operator(1, 6) + + def test_missing(self): + """Test that empty string is returned for missing key.""" + bw = BoseWord({(0, 0): "+", (1, 1): "-"}) + assert (2, 3) not in bw.keys() + assert bw[(2, 3)] == "" + + def test_set_items(self): + """Test that setting items raises an error""" + bw = BoseWord({(0, 0): "+", (1, 1): "-"}) + with pytest.raises(TypeError, match="BoseWord object does not support assignment"): + bw[(2, 2)] = "+" + + def test_update_items(self): + """Test that updating items raises an error""" + bw = BoseWord({(0, 0): "+", (1, 1): "-"}) + with pytest.raises(TypeError, match="BoseWord object does not support assignment"): + bw.update({(2, 2): "+"}) + + with pytest.raises(TypeError, match="BoseWord object does not support assignment"): + bw[(1, 1)] = "+" + + def test_hash(self): + """Test that a unique hash exists for different BoseWords.""" + bw_1 = BoseWord({(0, 0): "+", (1, 1): "-"}) + bw_2 = BoseWord({(0, 0): "+", (1, 1): "-"}) # same as 1 + bw_3 = BoseWord({(1, 1): "-", (0, 0): "+"}) # same as 1 but reordered + bw_4 = BoseWord({(0, 0): "+", (1, 2): "-"}) # distinct from above + + assert bw_1.__hash__() == bw_2.__hash__() + assert bw_1.__hash__() == bw_3.__hash__() + assert bw_1.__hash__() != bw_4.__hash__() + + @pytest.mark.parametrize("bw", (bw1, bw2, bw3, bw4)) + def test_copy(self, bw): + """Test that the copy is identical to the original.""" + copy_bw = copy(bw) + deep_copy_bw = deepcopy(bw) + + assert copy_bw == bw + assert deep_copy_bw == bw + assert copy_bw is not bw + assert deep_copy_bw is not bw + + tup_bws_wires = ((bw1, {0, 1}), (bw2, {0}), (bw3, {0, 3, 4}), (bw4, set())) + + @pytest.mark.parametrize("bw, wires", tup_bws_wires) + def test_wires(self, bw, wires): + """Test that the wires are tracked correctly.""" + assert bw.wires == wires + + tup_bw_compact = ( + (bw1, "b\u207a(0) b(1)"), + (bw2, "b\u207a(0) b(0)"), + (bw3, "b\u207a(0) b(3) b\u207a(0) b(4)"), + (bw4, "I"), + (bw5, "b\u207a(10) b(30) b\u207a(0) b(400)"), + (bw6, "b\u207a(10) b\u207a(30) b(0) b(400)"), + (bw7, "b(10) b\u207a(30) b(0) b\u207a(400)"), + ) + + @pytest.mark.parametrize("bw, str_rep", tup_bw_compact) + def test_compact(self, bw, str_rep): + """Test string representation from to_string""" + assert bw.to_string() == str_rep + + @pytest.mark.parametrize("bw, str_rep", tup_bw_compact) + def test_str(self, bw, str_rep): + """Test __str__ and __repr__ methods""" + assert str(bw) == str_rep + assert repr(bw) == f"BoseWord({bw.sorted_dic})" + + def test_pickling(self): + """Check that BoseWords can be pickled and unpickled.""" + bw = BoseWord({(0, 0): "+", (1, 1): "-"}) + serialization = pickle.dumps(bw) + new_bw = pickle.loads(serialization) + assert bw == new_bw + + @pytest.mark.parametrize( + "operator", + [ + ({(0, 0): "+", (2, 1): "-"}), + ({(0, 0): "+", (1, 1): "-", (3, 0): "+", (4, 1): "-"}), + ({(-1, 0): "+", (0, 1): "-", (1, 0): "+", (2, 1): "-"}), + ], + ) + def test_init_error(self, operator): + """Test that an error is raised if the operator orders are not correct.""" + with pytest.raises(ValueError, match="The operator indices must belong to the set"): + BoseWord(operator) + + tup_bw_dag = ( + (bw1, bw1_dag), + (bw2, bw2_dag), + (bw3, bw3_dag), + (bw4, bw4_dag), + (bw5, bw5_dag), + (bw6, bw6_dag), + (bw7, bw7_dag), + ) + + @pytest.mark.parametrize("bw, bw_dag", tup_bw_dag) + def test_adjoint(self, bw, bw_dag): + assert bw.adjoint() == bw_dag + + +class TestBoseWordArithmetic: + WORDS_MUL = ( + ( + bw1, + bw1, + BoseWord({(0, 0): "+", (1, 1): "-", (2, 0): "+", (3, 1): "-"}), + BoseWord({(0, 0): "+", (1, 1): "-", (2, 0): "+", (3, 1): "-"}), + ), + ( + bw1, + bw1, + BoseWord({(0, 0): "+", (1, 1): "-", (2, 0): "+", (3, 1): "-"}), + BoseWord({(0, 0): "+", (1, 1): "-", (2, 0): "+", (3, 1): "-"}), + ), + ( + bw1, + bw3, + BoseWord( + {(0, 0): "+", (1, 1): "-", (2, 0): "+", (3, 3): "-", (4, 0): "+", (5, 4): "-"} + ), + BoseWord( + {(0, 0): "+", (1, 3): "-", (2, 0): "+", (3, 4): "-", (4, 0): "+", (5, 1): "-"} + ), + ), + ( + bw2, + bw1, + BoseWord({(0, 0): "+", (1, 0): "-", (2, 0): "+", (3, 1): "-"}), + BoseWord({(0, 0): "+", (1, 1): "-", (2, 0): "+", (3, 0): "-"}), + ), + (bw1, bw4, bw1, bw1), + (bw4, bw3, bw3, bw3), + (bw4, bw4, bw4, bw4), + ) + + @pytest.mark.parametrize("f1, f2, result_bw_right, result_bw_left", WORDS_MUL) + def test_mul_bose_words(self, f1, f2, result_bw_right, result_bw_left): + """Test that two BoseWords can be multiplied together and return a new + BoseWord, with operators in the expected order""" + assert f1 * f2 == result_bw_right + assert f2 * f1 == result_bw_left + + WORDS_AND_SENTENCES_MUL = ( + ( + bw1, + BoseSentence({bw3: 1.2}), + BoseSentence({bw1 * bw3: 1.2}), + ), + ( + bw2, + BoseSentence({bw3: 1.2, bw1: 3.7}), + BoseSentence({bw2 * bw3: 1.2, bw2 * bw1: 3.7}), + ), + ) + + @pytest.mark.parametrize("bw, bs, result", WORDS_AND_SENTENCES_MUL) + def test_mul_bose_word_and_sentence(self, bw, bs, result): + """Test that a BoseWord can be multiplied by a BoseSentence + and return a new BoseSentence""" + assert bw * bs == result + + WORDS_AND_NUMBERS_MUL = ( + (bw1, 2, BoseSentence({bw1: 2})), # int + (bw2, 3.7, BoseSentence({bw2: 3.7})), # float + (bw2, 2j, BoseSentence({bw2: 2j})), # complex + (bw2, np.array([2]), BoseSentence({bw2: 2})), # numpy array + (bw1, pnp.array([2]), BoseSentence({bw1: 2})), # pennylane numpy array + (bw1, pnp.array([2, 2])[0], BoseSentence({bw1: 2})), # pennylane tensor with no length + ) + + @pytest.mark.parametrize("bw, number, result", WORDS_AND_NUMBERS_MUL) + def test_mul_number(self, bw, number, result): + """Test that a BoseWord can be multiplied onto a number (from the left) + and return a BoseSentence""" + assert bw * number == result + + @pytest.mark.parametrize("bw, number, result", WORDS_AND_NUMBERS_MUL) + def test_rmul_number(self, bw, number, result): + """Test that a BoseWord can be multiplied onto a number (from the right) + and return a BoseSentence""" + assert number * bw == result + + tup_bw_mult_error = ((bw4, "string"),) + + @pytest.mark.parametrize("bw, bad_type", tup_bw_mult_error) + def test_mul_error(self, bw, bad_type): + """Test multiply with unsupported type raises an error""" + with pytest.raises(TypeError, match=f"Cannot multiply BoseWord by {type(bad_type)}."): + bw * bad_type # pylint: disable=pointless-statement + + @pytest.mark.parametrize("bw, bad_type", tup_bw_mult_error) + def test_rmul_error(self, bw, bad_type): + """Test __rmul__ with unsupported type raises an error""" + with pytest.raises(TypeError, match=f"Cannot multiply BoseWord by {type(bad_type)}."): + bad_type * bw # pylint: disable=pointless-statement + + @pytest.mark.parametrize("bw, bad_type", tup_bw_mult_error) + def test_add_error(self, bw, bad_type): + """Test __add__ with unsupported type raises an error""" + with pytest.raises(TypeError, match=f"Cannot add {type(bad_type)} to a BoseWord"): + bw + bad_type # pylint: disable=pointless-statement + + @pytest.mark.parametrize("bw, bad_type", tup_bw_mult_error) + def test_radd_error(self, bw, bad_type): + """Test __radd__ with unsupported type raises an error""" + with pytest.raises(TypeError, match=f"Cannot add {type(bad_type)} to a BoseWord"): + bad_type + bw # pylint: disable=pointless-statement + + @pytest.mark.parametrize("bw, bad_type", tup_bw_mult_error) + def test_sub_error(self, bw, bad_type): + """Test __sub__ with unsupported type raises an error""" + with pytest.raises(TypeError, match=f"Cannot subtract {type(bad_type)} from a BoseWord"): + bw - bad_type # pylint: disable=pointless-statement + + @pytest.mark.parametrize("bw, bad_type", tup_bw_mult_error) + def test_rsub_error(self, bw, bad_type): + """Test __rsub__ with unsupported type raises an error""" + with pytest.raises(TypeError, match=f"Cannot subtract a BoseWord from {type(bad_type)}"): + bad_type - bw # pylint: disable=pointless-statement + + WORDS_ADD = [ + (bw1, bw2, BoseSentence({bw1: 1, bw2: 1})), + (bw3, bw2, BoseSentence({bw2: 1, bw3: 1})), + (bw2, bw2, BoseSentence({bw2: 2})), + ] + + @pytest.mark.parametrize("f1, f2, res", WORDS_ADD) + def test_add_bose_words(self, f1, f2, res): + """Test that adding two BoseWords returns the expected BoseSentence""" + assert f1 + f2 == res + assert f2 + f1 == res + + WORDS_AND_SENTENCES_ADD = [ + (bw1, BoseSentence({bw1: 1.2, bw3: 3j}), BoseSentence({bw1: 2.2, bw3: 3j})), + (bw3, BoseSentence({bw1: 1.2, bw3: 3j}), BoseSentence({bw1: 1.2, bw3: (1 + 3j)})), + (bw1, BoseSentence({bw1: -1.2, bw3: 3j}), BoseSentence({bw1: -0.2, bw3: 3j})), + ] + + @pytest.mark.parametrize("w, s, res", WORDS_AND_SENTENCES_ADD) + def test_add_bose_words_and_sentences(self, w, s, res): + """Test that adding a BoseSentence to a BoseWord returns the expected BoseSentence""" + sum = w + s + # due to rounding, the actual result for floats is + # e.g. -0.19999999999999... instead of 0.2, so we round to compare + sum_rounded = BoseSentence( + {k: round(v, 10) if isinstance(v, float) else v for k, v in sum.items()} + ) + assert sum_rounded == res + + WORDS_AND_CONSTANTS_ADD = [ + (bw1, 5, BoseSentence({bw1: 1, bw4: 5})), # int + (bw2, 2.8, BoseSentence({bw2: 1, bw4: 2.8})), # float + (bw3, (1 + 3j), BoseSentence({bw3: 1, bw4: (1 + 3j)})), # complex + (bw1, np.array([5]), BoseSentence({bw1: 1, bw4: 5})), # numpy array + (bw2, pnp.array([2.8]), BoseSentence({bw2: 1, bw4: 2.8})), # pennylane numpy array + ( + bw1, + pnp.array([2, 2])[0], + BoseSentence({bw1: 1, bw4: 2}), + ), # pennylane tensor with no length + (bw4, 2, BoseSentence({bw4: 3})), # BoseWord is Identity + ] + + @pytest.mark.parametrize("w, c, res", WORDS_AND_CONSTANTS_ADD) + def test_add_bose_words_and_constants(self, w, c, res): + """Test that adding a constant (int, float or complex) to a BoseWord + returns the expected BoseSentence""" + sum = w + c + # due to rounding, the actual result for floats is + # e.g. -0.19999999999999... instead of 0.2, so we round to compare + sum_rounded = BoseSentence( + {k: round(v, 10) if isinstance(v, float) else v for k, v in sum.items()} + ) + assert sum_rounded == res + + @pytest.mark.parametrize("w, c, res", WORDS_AND_CONSTANTS_ADD) + def test_radd_bose_words_and_constants(self, w, c, res): + """Test that adding a Bose word to a constant (int, float or complex) + returns the expected BoseSentence (__radd__)""" + sum = c + w + # due to rounding, the actual result for floats is + # e.g. -0.19999999999999... instead of 0.2, so we round to compare + sum_rounded = BoseSentence( + {k: round(v, 10) if isinstance(v, float) else v for k, v in sum.items()} + ) + assert sum_rounded == res + + WORDS_SUB = [ + (bw1, bw2, BoseSentence({bw1: 1, bw2: -1}), BoseSentence({bw1: -1, bw2: 1})), + (bw2, bw3, BoseSentence({bw2: 1, bw3: -1}), BoseSentence({bw2: -1, bw3: 1})), + (bw2, bw2, BoseSentence({bw2: 0}), BoseSentence({bw2: 0})), + ] + + @pytest.mark.parametrize("f1, f2, res1, res2", WORDS_SUB) + def test_subtract_bose_words(self, f1, f2, res1, res2): + """Test that subtracting one BoseWord from another returns the expected BoseSentence""" + assert f1 - f2 == res1 + assert f2 - f1 == res2 + + WORDS_AND_SENTENCES_SUB = [ + (bw1, BoseSentence({bw1: 1.2, bw3: 3j}), BoseSentence({bw1: -0.2, bw3: -3j})), + (bw3, BoseSentence({bw1: 1.2, bw3: 3j}), BoseSentence({bw1: -1.2, bw3: (1 - 3j)})), + (bw1, BoseSentence({bw1: -1.2, bw3: 3j}), BoseSentence({bw1: 2.2, bw3: -3j})), + ] + + @pytest.mark.parametrize("w, s, res", WORDS_AND_SENTENCES_SUB) + def test_subtract_bose_words_and_sentences(self, w, s, res): + """Test that subtracting a BoseSentence from a BoseWord returns the expected BoseSentence""" + diff = w - s + # due to rounding, the actual result for floats is + # e.g. -0.19999999999999... instead of 0.2, so we round to compare + diff_rounded = BoseSentence( + {k: round(v, 10) if isinstance(v, float) else v for k, v in diff.items()} + ) + + assert diff_rounded == res + + WORDS_AND_CONSTANTS_SUBTRACT = [ + (bw1, 5, BoseSentence({bw1: 1, bw4: -5})), # int + (bw2, 2.8, BoseSentence({bw2: 1, bw4: -2.8})), # float + (bw3, (1 + 3j), BoseSentence({bw3: 1, bw4: -(1 + 3j)})), # complex + (bw1, np.array([5]), BoseSentence({bw1: 1, bw4: -5})), # numpy array + (bw2, pnp.array([2.8]), BoseSentence({bw2: 1, bw4: -2.8})), # pennylane numpy array + ( + bw1, + pnp.array([2, 2])[0], + BoseSentence({bw1: 1, bw4: -2}), + ), # pennylane tensor with no length + (bw4, 2, BoseSentence({bw4: -1})), # BoseWord is Identity + ] + + @pytest.mark.parametrize("w, c, res", WORDS_AND_CONSTANTS_SUBTRACT) + def test_subtract_constant_from_bose_word(self, w, c, res): + """Test that subtracting a constant (int, float or complex) from a BoseWord + returns the expected BoseSentence""" + diff = w - c + # due to rounding, the actual result for floats is + # e.g. -0.19999999999999... instead of 0.2, so we round to compare + diff_rounded = BoseSentence( + {k: round(v, 10) if isinstance(v, float) else v for k, v in diff.items()} + ) + assert diff_rounded == res + + CONSTANTS_AND_WORDS_SUBTRACT = [ + (bw1, 5, BoseSentence({bw1: -1, bw4: 5})), # int + (bw2, 2.8, BoseSentence({bw2: -1, bw4: 2.8})), # float + (bw3, (1 + 3j), BoseSentence({bw3: -1, bw4: (1 + 3j)})), # complex + (bw1, np.array([5]), BoseSentence({bw1: -1, bw4: 5})), # numpy array + (bw2, pnp.array([2.8]), BoseSentence({bw2: -1, bw4: 2.8})), # pennylane numpy array + ( + bw1, + pnp.array([2, 2])[0], + BoseSentence({bw1: -1, bw4: 2}), + ), # pennylane tensor with no length + (bw4, 2, BoseSentence({bw4: 1})), # BoseWord is Identity + ] + + @pytest.mark.parametrize("w, c, res", CONSTANTS_AND_WORDS_SUBTRACT) + def test_subtract_bose_words_from_constant(self, w, c, res): + """Test that subtracting a constant (int, float or complex) from a BoseWord + returns the expected BoseSentence""" + diff = c - w + # due to rounding, the actual result for floats is + # e.g. -0.19999999999999... instead of 0.2, so we round to compare + diff_rounded = BoseSentence( + {k: round(v, 10) if isinstance(v, float) else v for k, v in diff.items()} + ) + assert diff_rounded == res + + tup_bw_pow = ( + (bw1, 0, BoseWord({})), + (bw1, 1, BoseWord({(0, 0): "+", (1, 1): "-"})), + (bw1, 2, BoseWord({(0, 0): "+", (1, 1): "-", (2, 0): "+", (3, 1): "-"})), + ( + bw2, + 3, + BoseWord( + {(0, 0): "+", (1, 0): "-", (2, 0): "+", (3, 0): "-", (4, 0): "+", (5, 0): "-"} + ), + ), + ) + + @pytest.mark.parametrize("f1, pow, result_bw", tup_bw_pow) + def test_pow(self, f1, pow, result_bw): + """Test that raising a BoseWord to an integer power returns the expected BoseWord""" + assert f1**pow == result_bw + + tup_bw_pow_error = ((bw1, -1), (bw3, 1.5)) + + @pytest.mark.parametrize("f1, pow", tup_bw_pow_error) + def test_pow_error(self, f1, pow): + """Test that invalid values for the exponent raises an error""" + with pytest.raises(ValueError, match="The exponent must be a positive integer."): + f1**pow # pylint: disable=pointless-statement + + @pytest.mark.parametrize( + "method_name", ("__add__", "__sub__", "__mul__", "__radd__", "__rsub__", "__rmul__") + ) + def test_array_must_not_exceed_length_1(self, method_name): + with pytest.raises( + ValueError, match="Arithmetic Bose operations can only accept an array of length 1" + ): + method_to_test = getattr(bw1, method_name) + _ = method_to_test(np.array([1, 2])) + + +bs1 = BoseSentence({bw1: 1.23, bw2: 4j, bw3: -0.5}) +bs1_dag = BoseSentence({bw1_dag: 1.23, bw2_dag: -4j, bw3_dag: -0.5}) + +bs2 = BoseSentence({bw1: -1.23, bw2: -4j, bw3: 0.5}) +bs2_dag = BoseSentence({bw1_dag: -1.23, bw2_dag: 4j, bw3_dag: 0.5}) + +bs1_hamiltonian = BoseSentence({bw1: 1.23, bw2: 4, bw3: -0.5}) +bs1_hamiltonian_dag = BoseSentence({bw1_dag: 1.23, bw2_dag: 4, bw3_dag: -0.5}) + +bs2_hamiltonian = BoseSentence({bw1: -1.23, bw2: -4, bw3: 0.5}) +bs2_hamiltonian_dag = BoseSentence({bw1_dag: -1.23, bw2_dag: -4, bw3_dag: 0.5}) + +bs3 = BoseSentence({bw3: -0.5, bw4: 1}) +bs3_dag = BoseSentence({bw3_dag: -0.5, bw4_dag: 1}) + +bs4 = BoseSentence({bw4: 1}) +bs4_dag = BoseSentence({bw4_dag: 1}) + +bs5 = BoseSentence({}) +bs5_dag = BoseSentence({}) + +bs6 = BoseSentence({bw1: 1.2, bw2: 3.1}) +bs6_dag = BoseSentence({bw1_dag: 1.2, bw2_dag: 3.1}) + +bs7 = BoseSentence( + { + BoseWord({(0, 0): "+", (1, 1): "-"}): 1.23, # b+(0) b(1) + BoseWord({(0, 0): "+", (1, 0): "-"}): 4.0j, # b+(0) b(0) = n(0) (number operator) + BoseWord({(0, 0): "+", (1, 2): "-", (2, 1): "+"}): -0.5, # b+(0) b(2) b+(1) + } +) + +bs1_x_bs2 = BoseSentence( # bs1 * bs1, computed by hand + { + BoseWord({(0, 0): "+", (1, 1): "-", (2, 0): "+", (3, 1): "-"}): 1.5129, + BoseWord({(0, 0): "+", (1, 1): "-", (2, 0): "+", (3, 0): "-"}): 4.92j, + BoseWord( + { + (0, 0): "+", + (1, 1): "-", + (2, 0): "+", + (3, 3): "-", + (4, 0): "+", + (5, 4): "-", + } + ): -0.615, + BoseWord({(0, 0): "+", (1, 0): "-", (2, 0): "+", (3, 1): "-"}): 4.92j, + BoseWord({(0, 0): "+", (1, 0): "-", (2, 0): "+", (3, 0): "-"}): -16, + BoseWord( + { + (0, 0): "+", + (1, 0): "-", + (2, 0): "+", + (3, 3): "-", + (4, 0): "+", + (5, 4): "-", + } + ): (-0 - 2j), + BoseWord( + { + (0, 0): "+", + (1, 3): "-", + (2, 0): "+", + (3, 4): "-", + (4, 0): "+", + (5, 1): "-", + } + ): -0.615, + BoseWord( + { + (0, 0): "+", + (1, 3): "-", + (2, 0): "+", + (3, 4): "-", + (4, 0): "+", + (5, 0): "-", + } + ): (-0 - 2j), + BoseWord( + { + (0, 0): "+", + (1, 3): "-", + (2, 0): "+", + (3, 4): "-", + (4, 0): "+", + (5, 3): "-", + (6, 0): "+", + (7, 4): "-", + } + ): 0.25, + } +) + +bs8 = bw8 + bw9 +bs8c = bw8 + bw9cs + +bs9 = 1.3 * bw8 + (1.4 + 3.8j) * bw9 +bs9c = 1.3 * bw8 + (1.4 + 3.8j) * bw9cs + +bs10 = -1.3 * bw11 + 2.3 * bw9 +bs10c = -1.3 * bw11cs + 2.3 * bw9 + + +class TestBoseSentence: + def test_missing(self): + """Test the result when a missing key is indexed.""" + bw = BoseWord({(0, 0): "+", (1, 1): "-"}) + new_bw = BoseWord({(0, 2): "+", (1, 3): "-"}) + bs = BoseSentence({bw: 1.0}) + + assert new_bw not in bs.keys() + assert bs[new_bw] == 0.0 + + def test_set_items(self): + """Test that we can add a new key to a BoseSentence.""" + bw = BoseWord({(0, 0): "+", (1, 1): "-"}) + bs = BoseSentence({bw: 1.0}) + + new_bw = BoseWord({(0, 2): "+", (1, 3): "-"}) + assert new_bw not in bs.keys() + + bs[new_bw] = 3.45 + assert new_bw in bs.keys() and bs[new_bw] == 3.45 + + tup_bs_str = ( + ( + bs1, + "1.23 * b\u207a(0) b(1)\n" + + "+ 4j * b\u207a(0) b(0)\n" + + "+ -0.5 * b\u207a(0) b(3) b\u207a(0) b(4)", + ), + ( + bs2, + "-1.23 * b\u207a(0) b(1)\n" + + "+ (-0-4j) * b\u207a(0) b(0)\n" + + "+ 0.5 * b\u207a(0) b(3) b\u207a(0) b(4)", + ), + (bs3, "-0.5 * b\u207a(0) b(3) b\u207a(0) b(4)\n" + "+ 1 * I"), + (bs4, "1 * I"), + (bs5, "0 * I"), + ) + + @pytest.mark.parametrize("bs, str_rep", tup_bs_str) + def test_str(self, bs, str_rep): + """Test the string representation of the BoseSentence.""" + assert str(bs) == str_rep + assert repr(bs) == f"BoseSentence({dict(bs)})" + + tup_bs_wires = ( + (bs1, {0, 1, 3, 4}), + (bs2, {0, 1, 3, 4}), + (bs3, {0, 3, 4}), + (bs4, set()), + ) + + @pytest.mark.parametrize("bs, wires", tup_bs_wires) + def test_wires(self, bs, wires): + """Test the correct wires are given for the BoseSentence.""" + assert bs.wires == wires + + @pytest.mark.parametrize("bs", (bs1, bs2, bs3, bs4)) + def test_copy(self, bs): + """Test that the copy is identical to the original.""" + copy_bs = copy(bs) + deep_copy_bs = deepcopy(bs) + + assert copy_bs == bs + assert deep_copy_bs == bs + assert copy_bs is not bs + assert deep_copy_bs is not bs + + def test_simplify(self): + """Test that simplify removes terms in the BoseSentence with coefficient less than the + threshold.""" + un_simplified_bs = BoseSentence({bw1: 0.001, bw2: 0.05, bw3: 1}) + + expected_simplified_bs0 = BoseSentence({bw1: 0.001, bw2: 0.05, bw3: 1}) + expected_simplified_bs1 = BoseSentence({bw2: 0.05, bw3: 1}) + expected_simplified_bs2 = BoseSentence({bw3: 1}) + + un_simplified_bs.simplify() + assert un_simplified_bs == expected_simplified_bs0 # default tol = 1e-8 + un_simplified_bs.simplify(tol=1e-2) + assert un_simplified_bs == expected_simplified_bs1 + un_simplified_bs.simplify(tol=1e-1) + assert un_simplified_bs == expected_simplified_bs2 + + def test_pickling(self): + """Check that BoseSentences can be pickled and unpickled.""" + f1 = BoseWord({(0, 0): "+", (1, 1): "-"}) + f2 = BoseWord({(0, 0): "+", (1, 3): "-", (2, 0): "+", (3, 4): "-"}) + bs = BoseSentence({f1: 1.5, f2: -0.5}) + + serialization = pickle.dumps(bs) + new_bs = pickle.loads(serialization) + assert bs == new_bs + + bs_dag_tup = ( + (bs1, bs1_dag), + (bs2, bs2_dag), + (bs3, bs3_dag), + (bs4, bs4_dag), + (bs5, bs5_dag), + (bs6, bs6_dag), + (bs1_hamiltonian, bs1_hamiltonian_dag), + (bs2_hamiltonian, bs2_hamiltonian_dag), + ) + + @pytest.mark.parametrize("bs, bs_dag", bs_dag_tup) + def test_adjoint(self, bs, bs_dag): + assert bs.adjoint() == bs_dag + + +class TestBoseSentenceArithmetic: + tup_bs_mult = ( # computed by hand + ( + bs1, + bs1, + bs1_x_bs2, + ), + ( + bs3, + bs4, + BoseSentence( + { + BoseWord({(0, 0): "+", (1, 3): "-", (2, 0): "+", (3, 4): "-"}): -0.5, + BoseWord({}): 1, + } + ), + ), + ( + bs4, + bs4, + BoseSentence( + { + BoseWord({}): 1, + } + ), + ), + (bs5, bs3, bs5), + (bs3, bs5, bs5), + (bs4, bs3, bs3), + (bs3, bs4, bs3), + ( + BoseSentence({bw2: 1, bw3: 1, bw4: 1}), + BoseSentence({bw4: 1, bw2: 1}), + BoseSentence({bw2: 2, bw3: 1, bw4: 1, bw2 * bw2: 1, bw3 * bw2: 1}), + ), + ) + + @pytest.mark.parametrize("f1, f2, result", tup_bs_mult) + def test_mul_bose_sentences(self, f1, f2, result): + """Test that the correct result of multiplication between two + BoseSentences is produced.""" + + simplified_product = f1 * f2 + simplified_product.simplify() + + assert simplified_product == result + + SENTENCES_AND_WORDS_MUL = ( + ( + bw1, + BoseSentence({bw3: 1.2}), + BoseSentence({bw3 * bw1: 1.2}), + ), + ( + bw2, + BoseSentence({bw3: 1.2, bw1: 3.7}), + BoseSentence({bw3 * bw2: 1.2, bw1 * bw2: 3.7}), + ), + ) + + @pytest.mark.parametrize("bw, bs, result", SENTENCES_AND_WORDS_MUL) + def test_mul_bose_word_and_sentence(self, bw, bs, result): + """Test that a BoseWord and a BoseSentence can be multiplied together + and return a new BoseSentence""" + assert bs * bw == result + + SENTENCES_AND_NUMBERS_MUL = ( + (bs1, 2, BoseSentence({bw1: 1.23 * 2, bw2: 4j * 2, bw3: -0.5 * 2})), # int + (bs2, 3.4, BoseSentence({bw1: -1.23 * 3.4, bw2: -4j * 3.4, bw3: 0.5 * 3.4})), # float + (bs1, 3j, BoseSentence({bw1: 3.69j, bw2: -12, bw3: -1.5j})), # complex + (bs5, 10, BoseSentence({})), # null operator times constant + ( + bs1, + np.array([2]), + BoseSentence({bw1: 1.23 * 2, bw2: 4j * 2, bw3: -0.5 * 2}), + ), # numpy array + ( + bs1, + pnp.array([2]), + BoseSentence({bw1: 1.23 * 2, bw2: 4j * 2, bw3: -0.5 * 2}), + ), # pennylane numpy array + ( + bs1, + pnp.array([2, 2])[0], + BoseSentence({bw1: 1.23 * 2, bw2: 4j * 2, bw3: -0.5 * 2}), + ), # pennylane tensor with no length + ) + + @pytest.mark.parametrize("bs, number, result", SENTENCES_AND_NUMBERS_MUL) + def test_mul_number(self, bs, number, result): + """Test that a BoseSentence can be multiplied onto a number (from the left) + and return a BoseSentence""" + assert bs * number == result + + @pytest.mark.parametrize("bs, number, result", SENTENCES_AND_NUMBERS_MUL) + def test_rmul_number(self, bs, number, result): + """Test that a BoseSentence can be multiplied onto a number (from the right) + and return a BoseSentence""" + assert number * bs == result + + tup_bs_add = ( # computed by hand + (bs1, bs1, BoseSentence({bw1: 2.46, bw2: 8j, bw3: -1})), + (bs1, bs2, BoseSentence({})), + (bs1, bs3, BoseSentence({bw1: 1.23, bw2: 4j, bw3: -1, bw4: 1})), + (bs2, bs5, bs2), + ) + + @pytest.mark.parametrize("f1, f2, result", tup_bs_add) + def test_add_bose_sentences(self, f1, f2, result): + """Test that the correct result of addition is produced for two BoseSentences.""" + + simplified_product = f1 + f2 + simplified_product.simplify() + + assert simplified_product == result + + SENTENCES_AND_WORDS_ADD = [ + (bw1, BoseSentence({bw1: 1.2, bw3: 3j}), BoseSentence({bw1: 2.2, bw3: 3j})), + (bw3, BoseSentence({bw1: 1.2, bw3: 3j}), BoseSentence({bw1: 1.2, bw3: (1 + 3j)})), + (bw1, BoseSentence({bw1: -1.2, bw3: 3j}), BoseSentence({bw1: -0.2, bw3: 3j})), + ] + + @pytest.mark.parametrize("w, s, res", SENTENCES_AND_WORDS_ADD) + def test_add_bose_words_and_sentences(self, w, s, res): + """Test that adding a BoseWord to a BoseSentence returns the expected BoseSentence""" + sum = s + w + # due to rounding, the actual result for floats is + # e.g. -0.19999999999999... instead of 0.2, so we round to compare + sum_rounded = BoseSentence( + {k: round(v, 10) if isinstance(v, float) else v for k, v in sum.items()} + ) + assert sum_rounded == res + + SENTENCES_AND_CONSTANTS_ADD = [ + (BoseSentence({bw1: 1.2, bw3: 3j}), 3, BoseSentence({bw1: 1.2, bw3: 3j, bw4: 3})), # int + ( + BoseSentence({bw1: 1.2, bw3: 3j}), + 1.3, + BoseSentence({bw1: 1.2, bw3: 3j, bw4: 1.3}), + ), # float + ( + BoseSentence({bw1: -1.2, bw3: 3j}), # complex + (1 + 2j), + BoseSentence({bw1: -1.2, bw3: 3j, bw4: (1 + 2j)}), + ), + (BoseSentence({}), 5, BoseSentence({bw4: 5})), # null sentence + (BoseSentence({bw4: 3}), 1j, BoseSentence({bw4: 3 + 1j})), # identity only + ( + BoseSentence({bw1: 1.2, bw3: 3j}), + np.array([3]), + BoseSentence({bw1: 1.2, bw3: 3j, bw4: 3}), + ), # numpy array + ( + BoseSentence({bw1: 1.2, bw3: 3j}), + pnp.array([3]), + BoseSentence({bw1: 1.2, bw3: 3j, bw4: 3}), + ), # pennylane numpy array + ( + BoseSentence({bw1: 1.2, bw3: 3j}), + pnp.array([3, 0])[0], + BoseSentence({bw1: 1.2, bw3: 3j, bw4: 3}), + ), # pennylane tensor with no length + ] + + @pytest.mark.parametrize("s, c, res", SENTENCES_AND_CONSTANTS_ADD) + def test_add_bose_sentences_and_constants(self, s, c, res): + """Test that adding a constant to a BoseSentence returns the expected BoseSentence""" + sum = s + c + # due to rounding, the actual result for floats is + # e.g. -0.19999999999999... instead of 0.2, so we round to compare + sum_rounded = BoseSentence( + {k: round(v, 10) if isinstance(v, float) else v for k, v in sum.items()} + ) + assert sum_rounded == res + + @pytest.mark.parametrize("s, c, res", SENTENCES_AND_CONSTANTS_ADD) + def test_radd_bose_sentences_and_constants(self, s, c, res): + """Test that adding a BoseSentence to a constant (__radd___) returns the expected BoseSentence""" + sum = c + s + # due to rounding, the actual result for floats is + # e.g. -0.19999999999999... instead of 0.2, so we round to compare + sum_rounded = BoseSentence( + {k: round(v, 10) if isinstance(v, float) else v for k, v in sum.items()} + ) + assert sum_rounded == res + + SENTENCE_MINUS_WORD = ( # computed by hand + (bs1, bw1, BoseSentence({bw1: 0.23, bw2: 4j, bw3: -0.5})), + (bs2, bw3, BoseSentence({bw1: -1.23, bw2: -4j, bw3: -0.5})), + (bs3, bw4, BoseSentence({bw3: -0.5})), + (BoseSentence({bw1: 1.2, bw3: 3j}), bw1, BoseSentence({bw1: 0.2, bw3: 3j})), + (BoseSentence({bw1: 1.2, bw3: 3j}), bw3, BoseSentence({bw1: 1.2, bw3: (-1 + 3j)})), + (BoseSentence({bw1: -1.2, bw3: 3j}), bw1, BoseSentence({bw1: -2.2, bw3: 3j})), + ) + + @pytest.mark.parametrize("bs, bw, result", SENTENCE_MINUS_WORD) + def test_subtract_bose_word_from_bose_sentence(self, bs, bw, result): + """Test that the correct result is produced if a BoseWord is + subtracted from a BoseSentence""" + + simplified_diff = bs - bw + simplified_diff.simplify() + # due to rounding, the actual result for floats is + # e.g. -0.19999999999999... instead of 0.2, so we round to compare + simplified_diff = BoseSentence( + {k: round(v, 10) if isinstance(v, float) else v for k, v in simplified_diff.items()} + ) + + assert simplified_diff == result + + SENTENCE_MINUS_CONSTANT = ( # computed by hand + (bs1, 3, BoseSentence({bw1: 1.23, bw2: 4j, bw3: -0.5, bw4: -3})), # int + (bs2, -2.7, BoseSentence({bw1: -1.23, bw2: -4j, bw3: 0.5, bw4: 2.7})), # float + (bs3, 2j, BoseSentence({bw3: -0.5, bw4: (1 - 2j)})), # complex + ( + BoseSentence({bw1: 1.2, bw3: 3j}), + -4, + BoseSentence({bw1: 1.2, bw3: 3j, bw4: 4}), + ), # negative int + ( + BoseSentence({bw1: 1.2, bw3: 3j}), + np.array([3]), + BoseSentence({bw1: 1.2, bw3: 3j, bw4: -3}), + ), # numpy array + ( + BoseSentence({bw1: 1.2, bw3: 3j}), + pnp.array([3]), + BoseSentence({bw1: 1.2, bw3: 3j, bw4: -3}), + ), # pennylane numpy array + ( + BoseSentence({bw1: 1.2, bw3: 3j}), + pnp.array([3, 2])[0], + BoseSentence({bw1: 1.2, bw3: 3j, bw4: -3}), + ), # pennylane tensor with no len + ) + + @pytest.mark.parametrize("bs, c, result", SENTENCE_MINUS_CONSTANT) + def test_subtract_constant_from_bose_sentence(self, bs, c, result): + """Test that the correct result is produced if a BoseWord is + subtracted from a BoseSentence""" + + simplified_diff = bs - c + simplified_diff.simplify() + # due to rounding, the actual result for floats is + # e.g. -0.19999999999999... instead of 0.2, so we round to compare + simplified_diff = BoseSentence( + {k: round(v, 10) if isinstance(v, float) else v for k, v in simplified_diff.items()} + ) + assert simplified_diff == result + + CONSTANT_MINUS_SENTENCE = ( # computed by hand + (bs1, 3, BoseSentence({bw1: -1.23, bw2: -4j, bw3: 0.5, bw4: 3})), + (bs2, -2.7, BoseSentence({bw1: 1.23, bw2: 4j, bw3: -0.5, bw4: -2.7})), + (bs3, 2j, BoseSentence({bw3: 0.5, bw4: (-1 + 2j)})), + (BoseSentence({bw1: 1.2, bw3: 3j}), -4, BoseSentence({bw1: -1.2, bw3: -3j, bw4: -4})), + ( + BoseSentence({bw1: 1.2, bw3: 3j}), + np.array([3]), + BoseSentence({bw1: -1.2, bw3: -3j, bw4: 3}), + ), # numpy array + ( + BoseSentence({bw1: 1.2, bw3: 3j}), + pnp.array([3]), + BoseSentence({bw1: -1.2, bw3: -3j, bw4: 3}), + ), # pennylane numpy array + ( + BoseSentence({bw1: 1.2, bw3: 3j}), + pnp.array([3, 3])[0], + BoseSentence({bw1: -1.2, bw3: -3j, bw4: 3}), + ), # pennylane tensor with to len + ) + + @pytest.mark.parametrize("bs, c, result", CONSTANT_MINUS_SENTENCE) + def test_subtract_bose_sentence_from_constant(self, bs, c, result): + """Test that the correct result is produced if a BoseWord is + subtracted from a BoseSentence""" + + simplified_diff = c - bs + simplified_diff.simplify() + # due to rounding, the actual result for floats is + # e.g. -0.19999999999999... instead of 0.2, so we round to compare + simplified_diff = BoseSentence( + {k: round(v, 10) if isinstance(v, float) else v for k, v in simplified_diff.items()} + ) + assert simplified_diff == result + + tup_bs_subtract = ( # computed by hand + (bs1, bs1, BoseSentence({})), + (bs1, bs2, BoseSentence({bw1: 2.46, bw2: 8j, bw3: -1})), + (bs1, bs3, BoseSentence({bw1: 1.23, bw2: 4j, bw4: -1})), + (bs2, bs5, bs2), + ) + + @pytest.mark.parametrize("f1, f2, result", tup_bs_subtract) + def test_subtract_bose_sentences(self, f1, f2, result): + """Test that the correct result of subtraction is produced for two BoseSentences.""" + + simplified_product = f1 - f2 + simplified_product.simplify() + + assert simplified_product == result + + tup_bs_pow = ( + (bs1, 0, BoseSentence({BoseWord({}): 1})), + (bs1, 1, bs1), + (bs1, 2, bs1_x_bs2), + (bs3, 0, BoseSentence({BoseWord({}): 1})), + (bs3, 1, bs3), + (bs4, 0, BoseSentence({BoseWord({}): 1})), + (bs4, 3, bs4), + ) + + @pytest.mark.parametrize("f1, pow, result", tup_bs_pow) + def test_pow(self, f1, pow, result): + """Test that raising a BoseWord to an integer power returns the expected BoseWord""" + assert f1**pow == result + + tup_bs_pow_error = ((bs1, -1), (bs3, 1.5)) + + @pytest.mark.parametrize("f1, pow", tup_bs_pow_error) + def test_pow_error(self, f1, pow): + """Test that invalid values for the exponent raises an error""" + with pytest.raises(ValueError, match="The exponent must be a positive integer."): + f1**pow # pylint: disable=pointless-statement + + TYPE_ERRORS = ((bs4, "string"),) + + @pytest.mark.parametrize("bs, bad_type", TYPE_ERRORS) + def test_add_error(self, bs, bad_type): + """Test __add__ with unsupported type raises an error""" + with pytest.raises(TypeError, match=f"Cannot add {type(bad_type)} to a BoseSentence."): + bs + bad_type # pylint: disable=pointless-statement + + @pytest.mark.parametrize("bs, bad_type", TYPE_ERRORS) + def test_radd_error(self, bs, bad_type): + """Test __radd__ with unsupported type raises an error""" + with pytest.raises(TypeError, match=f"Cannot add {type(bad_type)} to a BoseSentence."): + bad_type + bs # pylint: disable=pointless-statement + + @pytest.mark.parametrize("bs, bad_type", TYPE_ERRORS) + def test_sub_error(self, bs, bad_type): + """Test __sub__ with unsupported type raises an error""" + with pytest.raises( + TypeError, match=f"Cannot subtract {type(bad_type)} from a BoseSentence." + ): + bs - bad_type # pylint: disable=pointless-statement + + @pytest.mark.parametrize("bs, bad_type", TYPE_ERRORS) + def test_rsub_error(self, bs, bad_type): + """Test __rsub__ with unsupported type raises an error""" + with pytest.raises( + TypeError, match=f"Cannot subtract a BoseSentence from {type(bad_type)}." + ): + bad_type - bs # pylint: disable=pointless-statement + + @pytest.mark.parametrize("bs, bad_type", TYPE_ERRORS) + def test_mul_error(self, bs, bad_type): + """Test __sub__ with unsupported type raises an error""" + with pytest.raises(TypeError, match=f"Cannot multiply BoseSentence by {type(bad_type)}."): + bs * bad_type # pylint: disable=pointless-statement + + @pytest.mark.parametrize("bs, bad_type", TYPE_ERRORS) + def test_rmul_error(self, bs, bad_type): + """Test __rsub__ with unsupported type raises an error""" + with pytest.raises(TypeError, match=f"Cannot multiply {type(bad_type)} by BoseSentence."): + bad_type * bs # pylint: disable=pointless-statement + + @pytest.mark.parametrize( + "method_name", ("__add__", "__sub__", "__mul__", "__radd__", "__rsub__", "__rmul__") + ) + def test_array_must_not_exceed_length_1(self, method_name): + with pytest.raises( + ValueError, match="Arithmetic Bose operations can only accept an array of length 1" + ): + method_to_test = getattr(bs1, method_name) + _ = method_to_test(np.array([1, 2]))